require guild id, show invite link if needed

small improvements on common functions
This commit is contained in:
Gabriel Fontes
2023-06-12 17:25:22 -03:00
parent 5bd8e2ab57
commit 074bc44d4d
6 changed files with 79 additions and 52 deletions

2
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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")]

View File

@@ -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)

View File

@@ -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)
} }