From 074bc44d4d100f0e8281da3e219537d89c1a92ef Mon Sep 17 00:00:00 2001 From: Gabriel Fontes Date: Mon, 12 Jun 2023 17:25:22 -0300 Subject: [PATCH] require guild id, show invite link if needed small improvements on common functions --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 6 ++-- src/config.rs | 2 +- src/discord/client.rs | 70 ++++++++++++++++++++++++++++++++----------- src/discord/common.rs | 49 +++++++++++++----------------- 6 files changed, 79 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf4c62f..729fe24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,7 +458,7 @@ dependencies = [ [[package]] name = "disconic" -version = "1.0.1" +version = "1.1.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index a97ab5d..82a6f65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "disconic" description = "Discord bot for interacting with subsonic music libraries" -version = "1.0.1" +version = "1.1.0" authors = [ "Gabriel Fontes " ] edition = "2021" homepage = "https://misterio.me" diff --git a/README.md b/README.md index bf5b1d3..c7a6507 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ With nix, you don't even need to clone the repo. Simply replace `.` with `github ## 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 diff --git a/src/config.rs b/src/config.rs index 480938f..4f06bed 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,7 +15,7 @@ pub struct Config { #[clap(long, env = "DISCONIC_SUBSONIC_PASSWORD")] subsonic_password: String, #[clap(long, env = "DISCONIC_DISCORD_GUILD")] - discord_guild: Option, + discord_guild: u64, #[clap(long, env = "DISCONIC_DISCORD_TOKEN")] discord_token: String, #[clap(long, env = "DISCONIC_LOG_LEVEL", default_value = "warn")] diff --git a/src/discord/client.rs b/src/discord/client.rs index b7b139e..7906a0f 100644 --- a/src/discord/client.rs +++ b/src/discord/client.rs @@ -1,7 +1,12 @@ use crate::discord::{commands, Data}; -use anyhow::Result; -use poise::samples::create_application_commands; -use serenity::{all::GuildId, client::Client as DiscordClient, prelude::GatewayIntents}; +use anyhow::{Error, Result}; +use poise::{samples::create_application_commands, Framework}; +use serenity::{ + all::{GuildId, Ready}, + builder::CreateCommand, + client::Client as DiscordClient, + prelude::{Context, GatewayIntents}, +}; use songbird::SerenityInit; 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, + ctx: &Context, + ready: &Ready, + framework: &Framework, +) -> Result { + 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( token: &str, - guild_id: Option, + guild_id: u64, subsonic_client: SubsonicClient, ) -> Result { let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; let commands = commands::commands(); let data = Data { subsonic_client }; + let guild = GuildId::new(guild_id); let create_commands = create_application_commands(&commands); let options = poise::FrameworkOptions { @@ -51,20 +90,15 @@ pub async fn create_client( on_error: |e| Box::pin(on_error(e)), ..Default::default() }; - let framework = poise::Framework::new(options, move |ctx, _ready, _framework| { - Box::pin(async move { - if let Some(id) = guild_id { - let guild = GuildId::new(id); - guild - .set_commands(&ctx.http, create_commands) - .await - .expect("Failed to register command"); - } 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 framework = poise::Framework::new(options, move |ctx, ready, framework| { + Box::pin(on_startup( + guild, + data, + create_commands, + ctx, + ready, + framework, + )) }); let client = DiscordClient::builder(token, intents) diff --git a/src/discord/common.rs b/src/discord/common.rs index f085b15..ad8d8ec 100644 --- a/src/discord/common.rs +++ b/src/discord/common.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context as ErrContext, Result}; use serenity::{ all::{ChannelId, Guild}, 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 { - let guild = ctx.guild().expect("No guild!").clone(); - log::info!("Got guild: {guild:?}"); - guild + ctx.guild() + .expect("Invalid (or no) guild configured!") + .to_owned() } pub fn get_channel(ctx: Context<'_>) -> Result { @@ -47,52 +47,45 @@ pub fn get_channel(ctx: Context<'_>) -> Result { .voice_states .get(&ctx.author().id) .ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?; - let channel = voice_state + voice_state .channel_id - .ok_or_else(|| anyhow!("You must be in a voice channel to use this command"))?; - - Ok(channel) + .ok_or_else(|| anyhow!("You must be in a voice channel to use this command")) } pub async fn get_call(ctx: Context<'_>) -> Result>> { let manager = get_manager(ctx).await?; let guild = get_guild(ctx); - let call = manager.get(guild.id); - - if let Some(c) = call { - Ok(c) - } else { - let channel = get_channel(ctx)?; - log::warn!("Not in a voice channel, trying to join {channel}"); - let _handler = manager.join(guild.id, channel).await; - manager - .get(guild.id) - .ok_or_else(|| anyhow!("Not in a voice channel, try running 'join'")) + match manager.get(guild.id) { + Some(c) => Ok(c), + None => { + let channel = get_channel(ctx)?; + log::warn!("Not in a voice channel, trying to join {channel}"); + manager + .join(guild.id, channel) + .await + .context("Couldn't join voice channel. Try running 'join' manually.") + } } } pub async fn get_song(track: &TrackHandle) -> Result { - let song = track + track .typemap() .read() .await .get::() .map(ToOwned::to_owned) - .ok_or_else(|| anyhow!("Sound information not found"))?; - Ok(song) + .ok_or_else(|| anyhow!("Sound information not found")) } pub async fn get_manager(ctx: Context<'_>) -> Result> { - let manager = songbird::get(ctx.discord()) + songbird::get(ctx.discord()) .await - .ok_or_else(|| anyhow!("Couldn't start manager"))?; - log::info!("Got manager: {manager:?}"); - Ok(manager) + .ok_or_else(|| anyhow!("Couldn't start manager")) } pub async fn load_song(song: &Song, client: &sunk::Client) -> Result { - log::info!("Loading song {song:?}"); 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) }