require guild id, show invite link if needed
small improvements on common functions
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -458,7 +458,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "disconic"
|
name = "disconic"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "disconic"
|
name = "disconic"
|
||||||
description = "Discord bot for interacting with subsonic music libraries"
|
description = "Discord bot for interacting with subsonic music libraries"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
authors = [ "Gabriel Fontes <eu@misterio.me>" ]
|
authors = [ "Gabriel Fontes <eu@misterio.me>" ]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://misterio.me"
|
homepage = "https://misterio.me"
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ With nix, you don't even need to clone the repo. Simply replace `.` with `github
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Start by creating a discord app, getting its bot token, and inviting it to your server. Also get your guild (server) ID. These steps are already documented elsewhere, so will not be covered here.
|
Start by creating a discord app and getting its bot token. Also get your guild (server) ID. These steps are already documented elsewhere, so will not be covered here.
|
||||||
|
|
||||||
You can configure the application through CLI arguments (very convenient) or environment variables (better for deployments, dotenv is also supported). Use `--help` to see what the arguments or environment variables are.
|
You can configure the application through CLI arguments or environment variables. Use `--help` to see what the arguments and environment variables are.
|
||||||
|
|
||||||
Simply run the binary and everything will be setup for you. The bot will automatically register its commands on your server (if guild_id is set). If you don't see the commands, try sending a message mentioning the bot and `register` (all commands can be ran like that, too).
|
Simply run the binary and everything will be setup for you. If your bot is not a member of the guild, disconic will error out and print the invitation URL. The bot will also automatically register its slash commands on your server whenever it starts.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub struct Config {
|
|||||||
#[clap(long, env = "DISCONIC_SUBSONIC_PASSWORD")]
|
#[clap(long, env = "DISCONIC_SUBSONIC_PASSWORD")]
|
||||||
subsonic_password: String,
|
subsonic_password: String,
|
||||||
#[clap(long, env = "DISCONIC_DISCORD_GUILD")]
|
#[clap(long, env = "DISCONIC_DISCORD_GUILD")]
|
||||||
discord_guild: Option<u64>,
|
discord_guild: u64,
|
||||||
#[clap(long, env = "DISCONIC_DISCORD_TOKEN")]
|
#[clap(long, env = "DISCONIC_DISCORD_TOKEN")]
|
||||||
discord_token: String,
|
discord_token: String,
|
||||||
#[clap(long, env = "DISCONIC_LOG_LEVEL", default_value = "warn")]
|
#[clap(long, env = "DISCONIC_LOG_LEVEL", default_value = "warn")]
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
use crate::discord::{commands, Data};
|
use crate::discord::{commands, Data};
|
||||||
use anyhow::Result;
|
use anyhow::{Error, Result};
|
||||||
use poise::samples::create_application_commands;
|
use poise::{samples::create_application_commands, Framework};
|
||||||
use serenity::{all::GuildId, client::Client as DiscordClient, prelude::GatewayIntents};
|
use serenity::{
|
||||||
|
all::{GuildId, Ready},
|
||||||
|
builder::CreateCommand,
|
||||||
|
client::Client as DiscordClient,
|
||||||
|
prelude::{Context, GatewayIntents},
|
||||||
|
};
|
||||||
use songbird::SerenityInit;
|
use songbird::SerenityInit;
|
||||||
use sunk::Client as SubsonicClient;
|
use sunk::Client as SubsonicClient;
|
||||||
|
|
||||||
@@ -36,14 +41,48 @@ async fn on_error(error: poise::FrameworkError<'_, Data, anyhow::Error>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn on_startup(
|
||||||
|
guild: GuildId,
|
||||||
|
data: Data,
|
||||||
|
create_commands: Vec<CreateCommand>,
|
||||||
|
ctx: &Context,
|
||||||
|
ready: &Ready,
|
||||||
|
framework: &Framework<Data, Error>,
|
||||||
|
) -> Result<Data> {
|
||||||
|
let is_on_guild = (&ready.guilds).iter().any(|x| x.id == guild);
|
||||||
|
|
||||||
|
let bot_id = framework.bot_id().await;
|
||||||
|
let permissions = "311388293184";
|
||||||
|
let scope = "bot%20applications.commands";
|
||||||
|
|
||||||
|
if !is_on_guild {
|
||||||
|
let invite_link = format!(
|
||||||
|
"https://discord.com/oauth2/authorize?client_id={}&permissions={}&scope={}",
|
||||||
|
bot_id, permissions, scope
|
||||||
|
);
|
||||||
|
log::error!("The bot is not your on your guild.");
|
||||||
|
log::error!("Invite it with:\n {}", invite_link);
|
||||||
|
framework.shard_manager().lock().await.shutdown_all().await;
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
guild
|
||||||
|
.set_commands(&ctx.http, create_commands)
|
||||||
|
.await
|
||||||
|
.expect("Failed to register command");
|
||||||
|
log::info!("Registered commands");
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_client(
|
pub async fn create_client(
|
||||||
token: &str,
|
token: &str,
|
||||||
guild_id: Option<u64>,
|
guild_id: u64,
|
||||||
subsonic_client: SubsonicClient,
|
subsonic_client: SubsonicClient,
|
||||||
) -> Result<DiscordClient> {
|
) -> Result<DiscordClient> {
|
||||||
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
|
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
|
||||||
let commands = commands::commands();
|
let commands = commands::commands();
|
||||||
let data = Data { subsonic_client };
|
let data = Data { subsonic_client };
|
||||||
|
let guild = GuildId::new(guild_id);
|
||||||
|
|
||||||
let create_commands = create_application_commands(&commands);
|
let create_commands = create_application_commands(&commands);
|
||||||
let options = poise::FrameworkOptions {
|
let options = poise::FrameworkOptions {
|
||||||
@@ -51,20 +90,15 @@ pub async fn create_client(
|
|||||||
on_error: |e| Box::pin(on_error(e)),
|
on_error: |e| Box::pin(on_error(e)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let framework = poise::Framework::new(options, move |ctx, _ready, _framework| {
|
let framework = poise::Framework::new(options, move |ctx, ready, framework| {
|
||||||
Box::pin(async move {
|
Box::pin(on_startup(
|
||||||
if let Some(id) = guild_id {
|
guild,
|
||||||
let guild = GuildId::new(id);
|
data,
|
||||||
guild
|
create_commands,
|
||||||
.set_commands(&ctx.http, create_commands)
|
ctx,
|
||||||
.await
|
ready,
|
||||||
.expect("Failed to register command");
|
framework,
|
||||||
} else {
|
))
|
||||||
log::warn!("Guild ID not configured. You'll have to run 'register' to register the slash commands.")
|
|
||||||
}
|
|
||||||
log::info!("Registered commands");
|
|
||||||
Ok(data)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let client = DiscordClient::builder(token, intents)
|
let client = DiscordClient::builder(token, intents)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Context as ErrContext, Result};
|
||||||
use serenity::{
|
use serenity::{
|
||||||
all::{ChannelId, Guild},
|
all::{ChannelId, Guild},
|
||||||
prelude::TypeMapKey,
|
prelude::TypeMapKey,
|
||||||
@@ -36,9 +36,9 @@ pub async fn queue_song(ctx: Context<'_>, song: &Song, client: &sunk::Client) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_guild(ctx: Context<'_>) -> Guild {
|
pub fn get_guild(ctx: Context<'_>) -> Guild {
|
||||||
let guild = ctx.guild().expect("No guild!").clone();
|
ctx.guild()
|
||||||
log::info!("Got guild: {guild:?}");
|
.expect("Invalid (or no) guild configured!")
|
||||||
guild
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_channel(ctx: Context<'_>) -> Result<ChannelId> {
|
pub fn get_channel(ctx: Context<'_>) -> Result<ChannelId> {
|
||||||
@@ -47,52 +47,45 @@ pub fn get_channel(ctx: Context<'_>) -> Result<ChannelId> {
|
|||||||
.voice_states
|
.voice_states
|
||||||
.get(&ctx.author().id)
|
.get(&ctx.author().id)
|
||||||
.ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?;
|
.ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?;
|
||||||
let channel = voice_state
|
voice_state
|
||||||
.channel_id
|
.channel_id
|
||||||
.ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?;
|
.ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))
|
||||||
|
|
||||||
Ok(channel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_call(ctx: Context<'_>) -> Result<Arc<Mutex<songbird::Call>>> {
|
pub async fn get_call(ctx: Context<'_>) -> Result<Arc<Mutex<songbird::Call>>> {
|
||||||
let manager = get_manager(ctx).await?;
|
let manager = get_manager(ctx).await?;
|
||||||
let guild = get_guild(ctx);
|
let guild = get_guild(ctx);
|
||||||
let call = manager.get(guild.id);
|
match manager.get(guild.id) {
|
||||||
|
Some(c) => Ok(c),
|
||||||
if let Some(c) = call {
|
None => {
|
||||||
Ok(c)
|
let channel = get_channel(ctx)?;
|
||||||
} else {
|
log::warn!("Not in a voice channel, trying to join {channel}");
|
||||||
let channel = get_channel(ctx)?;
|
manager
|
||||||
log::warn!("Not in a voice channel, trying to join {channel}");
|
.join(guild.id, channel)
|
||||||
let _handler = manager.join(guild.id, channel).await;
|
.await
|
||||||
manager
|
.context("Couldn't join voice channel. Try running 'join' manually.")
|
||||||
.get(guild.id)
|
}
|
||||||
.ok_or_else(|| anyhow!("Not in a voice channel, try running 'join'"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_song(track: &TrackHandle) -> Result<Song> {
|
pub async fn get_song(track: &TrackHandle) -> Result<Song> {
|
||||||
let song = track
|
track
|
||||||
.typemap()
|
.typemap()
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
.get::<SongHandle>()
|
.get::<SongHandle>()
|
||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
.ok_or_else(|| anyhow!("Sound information not found"))?;
|
.ok_or_else(|| anyhow!("Sound information not found"))
|
||||||
Ok(song)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_manager(ctx: Context<'_>) -> Result<Arc<Songbird>> {
|
pub async fn get_manager(ctx: Context<'_>) -> Result<Arc<Songbird>> {
|
||||||
let manager = songbird::get(ctx.discord())
|
songbird::get(ctx.discord())
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| anyhow!("Couldn't start manager"))?;
|
.ok_or_else(|| anyhow!("Couldn't start manager"))
|
||||||
log::info!("Got manager: {manager:?}");
|
|
||||||
Ok(manager)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_song(song: &Song, client: &sunk::Client) -> Result<Track> {
|
pub async fn load_song(song: &Song, client: &sunk::Client) -> Result<Track> {
|
||||||
log::info!("Loading song {song:?}");
|
|
||||||
let url = song.stream_url(client)?;
|
let url = song.stream_url(client)?;
|
||||||
let track: Track = HttpRequest::new(client.reqclient.clone(), url).into();
|
let track = HttpRequest::new(client.reqclient.clone(), url).into();
|
||||||
Ok(track)
|
Ok(track)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user