From ffba60b278162707bc4eb004c3bfb6b2e9595213 Mon Sep 17 00:00:00 2001 From: Nathan Perry Date: Wed, 8 May 2024 12:55:35 -0400 Subject: rework to use songbird --- src/commands/help.rs | 23 +++-- src/commands/meme/create.rs | 84 ++++++++-------- src/commands/meme/history.rs | 220 ++++++++++++++++++++++-------------------- src/commands/meme/invoke.rs | 18 ++-- src/commands/meme/mod.rs | 63 ++++++++---- src/commands/mod.rs | 34 ++++--- src/commands/playback.rs | 223 ++++++++++++------------------------------- src/commands/roll.rs | 6 +- src/commands/sound_levels.rs | 166 ++++++++++++-------------------- src/commands/today/mod.rs | 56 ++++++----- 10 files changed, 397 insertions(+), 496 deletions(-) (limited to 'src/commands') diff --git a/src/commands/help.rs b/src/commands/help.rs index 588cdf7..0d84b2d 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -1,23 +1,20 @@ use std::collections::HashSet; use serenity::{ + framework::standard::{ + help_commands, + macros::help, + Args, + CommandGroup, + CommandResult, + HelpOptions, + }, model::{ channel::Message, id::UserId, }, - framework::{ - standard::{ - macros::help, - help_commands, - Args, - HelpOptions, - CommandGroup, - }, - }, prelude::*, }; -use serenity::framework::standard::CommandResult; - #[help] pub async fn help( @@ -28,5 +25,7 @@ pub async fn help( groups: &[&'static CommandGroup], owners: HashSet, ) -> CommandResult { - help_commands::with_embeds(ctx, msg, args, opts, groups, owners) + help_commands::with_embeds(ctx, msg, args, opts, groups, owners).await?; + + Ok(()) } diff --git a/src/commands/meme/create.rs b/src/commands/meme/create.rs index 97c5276..1c12f2a 100644 --- a/src/commands/meme/create.rs +++ b/src/commands/meme/create.rs @@ -1,50 +1,41 @@ -use std::{ - io::Read, - process::{ - Command, - Stdio, - }, -}; +use std::process::Stdio; +use anyhow::anyhow; use diesel::result::Error as DieselError; +use lazy_static::lazy_static; use log::{ debug, error, warn, }; use serenity::{ + all::ReactionType, framework::standard::{ macros::command, Args, + CommandError, + CommandResult, Delimiter, }, + futures::TryFutureExt, model::channel::Message, prelude::*, }; -use url::Url; - -use anyhow::anyhow; -use lazy_static::lazy_static; -use serenity::{ - all::ReactionType, - framework::standard::{ - CommandError, - CommandResult, - }, - futures::TryFutureExt, +use tap::Pipe; +use tokio::{ + io::AsyncReadExt, + process::Command, }; +use url::Url; use crate::{ - audio::{ - parse_times, - ytdl_url, - }, db::{ connection, Audio, Image, NewMeme, }, + parse_times, util, FFMPEG_COMMAND, }; @@ -77,12 +68,12 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult .await; } - let image_id = image - .map(|att| { - let data = att.download()?; - Image::create(&mut conn, &att.filename, data, msg.author.id.get()) - }) - .transpose()?; + let mut image_id = None; + + if let Some(att) = image { + let data = att.download().await?; + image_id = Some(Image::create(&mut conn, &att.filename, data, msg.author.id.get())?); + }; let save_result = NewMeme { title, @@ -96,7 +87,9 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult use diesel::result::DatabaseErrorKind; match save_result { - Ok(_) => msg.react(&ctx, "👌"), + Ok(_) => { + msg.react(&ctx, ReactionType::Unicode("👌".to_string())).await?; + }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::() @@ -111,6 +104,8 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult return Err(e.into()); }, } + + Ok(()) } #[command] @@ -131,7 +126,7 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe let opts = elems[1..].join(" "); let (start, end) = parse_times(opts); - let youtube_url = ytdl_url(audio_link.as_str())?; + let youtube_url = util::ytdl_url(audio_link.as_str()).await?; let duration_opts = if let Some(e) = end { vec![ @@ -178,18 +173,17 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe let mut conn = connection()?; - let image = msg - .attachments - .first() - .ok_or(anyhow!("no attachment")) - .and_then(|att| { - let data = att.download()?; - Image::create(&mut conn, &att.filename, data, msg.author.id.get()) - }) - .ok(); + let image_att = msg.attachments.first().ok_or(anyhow!("no attachment")); + + let mut image_id = None; + + if let Ok(att) = image_att { + let data = att.download().await?; + image_id = Image::create(&mut conn, &att.filename, data, msg.author.id.get())?.pipe(Some); + } let mut audio_data = Vec::new(); - let bytes = audio_reader.read_to_end(&mut audio_data)?; + let bytes = audio_reader.read_to_end(&mut audio_data).await?; if bytes == 0 { debug!("read 0 bytes from audio reader"); @@ -203,7 +197,7 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe let save_result = NewMeme { title, content: text, - image_id: image, + image_id, audio_id: Some(audio_id), metadata_id: 0, } @@ -212,7 +206,9 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe use diesel::result::DatabaseErrorKind; match save_result { - Ok(_) => msg.react(&ctx, ReactionType::Unicode("👌".to_owned())), + Ok(_) => { + msg.react(&ctx, ReactionType::Unicode("👌".to_owned())).await?; + }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::() @@ -224,7 +220,9 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe .await; } - return Err(e); + return Err(e.into()); }, } + + Ok(()) } diff --git a/src/commands/meme/history.rs b/src/commands/meme/history.rs index 5e200b1..ed50e27 100644 --- a/src/commands/meme/history.rs +++ b/src/commands/meme/history.rs @@ -1,7 +1,11 @@ +use anyhow::anyhow; use diesel::{ result::Error as DieselError, NotFound, + PgConnection, }; +use itertools::Itertools; +use lazy_static::lazy_static; use log::{ debug, error, @@ -11,25 +15,23 @@ use serenity::{ framework::standard::{ macros::command, Args, + CommandError, + CommandResult, + }, + futures::{ + StreamExt, + TryFutureExt, + TryStreamExt, }, model::channel::Message, prelude::*, }; +use tap::Pipe; use timeago::{ Formatter, TimeUnit, }; -use anyhow::anyhow; -use lazy_static::lazy_static; -use serenity::{ - framework::standard::{ - CommandError, - CommandResult, - }, - futures::TryFutureExt, -}; - use crate::{ db::{ self, @@ -39,7 +41,6 @@ use crate::{ Metadata, }, util, - Result, CONFIG, }; @@ -53,10 +54,10 @@ lazy_static! { }; } -static CLEAN_DATE_FORMAT: &'static str = "%b %-e %Y"; +static CLEAN_DATE_FORMAT: &str = "%b %-e %Y"; #[command] -#[aliases("what")] +#[aliases("what", "hwaet", "hwæt")] pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { let mut conn = connection()?; @@ -80,7 +81,7 @@ pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { match meme { Ok(ref meme) => { let metadata = Metadata::find(&mut conn, meme.metadata_id)?; - let author = CONFIG.discord.guild().member(&ctx, metadata.created_by as u64)?; + let author = CONFIG.discord.guild().member(&ctx, metadata.created_by as u64).await?; util::send( ctx, @@ -98,22 +99,22 @@ pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Err(e) => { if let Some(NotFound) = e.downcast_ref::() { info!("last meme not found in database"); - return util::send(ctx, msg.channel_id, "heuueueeeeh?", msg.tts).await; + return util::send(ctx, msg.channel_id, "heuueueeeeh?", msg.tts) + .await + .map_err(CommandError::from); } util::send(ctx, msg.channel_id, "do i look like i know what a jpeg is", msg.tts) .await?; - return Err(e); + return Err(e.into()); }, }; - meme.map(|_| {}) + meme.map(|_| {}).map_err(CommandError::from) } #[command] pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - use itertools::Itertools; - let mut conn = connection()?; let n = args.single_quoted::().unwrap_or(CONFIG.default_hist); @@ -135,66 +136,76 @@ pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes } info!("reporting meme history (len {})", n); - let resp = records - .into_iter() - .enumerate() - .rev() - .map(|(i, rec)| { - let dt = chrono::DateTime::from_utc(rec.time, chrono::Utc {}); - let ago = TIME_FORMATTER.convert((chrono::Utc::now() - dt).to_std().unwrap()); - - let rand = if rec.random { - "R, " - } else { - "" - }; - Meme::find(&mut conn, rec.meme_id) - .and_then(|meme| { - Metadata::find(&mut conn, meme.metadata_id).map(|metadata| (metadata, meme)) - }) - .map(|(metadata, meme)| { - let author_name = CONFIG - .discord - .guild() - .member(&ctx, metadata.created_by as u64) - .map(|m| m.display_name().into_owned()) - .unwrap_or("???".to_owned()); - let invoker_name = CONFIG - .discord - .guild() - .member(&ctx, rec.user_id as u64) - .map(|m| m.display_name().into_owned()) - .unwrap_or("???".to_owned()); - format!( - "{}. [{}{}] \"{}\" by {} ({}). invoked by {}.", - i + 1, - rand, - ago, - meme.title, - author_name, - metadata.created.date().format(CLEAN_DATE_FORMAT), - invoker_name - ) - }) - .unwrap_or_else(|e| { - if let Some(variant) = e.downcast_ref::() { - if *variant != NotFound { - error!("error encountered loading meme history: {}", e); - } - } - - let invoker_name = CONFIG - .discord - .guild() - .member(&ctx, rec.user_id as u64) - .map(|m| m.display_name().into_owned()) - .unwrap_or("???".to_owned()); - format!("{}. [{}{}] not found. invoked by {}.", i + 1, rand, ago, invoker_name) - }) - }) - .join("\n"); - util::send(ctx, msg.channel_id, &resp, false).await + let resp = serenity::futures::stream::iter(records.into_iter().enumerate().rev()) + .then(|(i, rec)| ir_info(ctx, i, rec, &mut conn)) + .try_collect::>() + .await?; + + let resp = resp.join("\n"); + + util::send(ctx, msg.channel_id, &resp, false).await.map_err(CommandError::from) +} + +async fn ir_info( + ctx: &Context, + i: usize, + rec: InvocationRecord, + conn: &mut PgConnection, +) -> Result { + let dt = chrono::DateTime::from_utc(rec.time, chrono::Utc {}); + let ago = TIME_FORMATTER.convert((chrono::Utc::now() - dt).to_std().unwrap()); + + let rand = if rec.random { + "R, " + } else { + "" + }; + + let meme = Meme::find(conn, rec.meme_id) + .and_then(|meme| Metadata::find(conn, meme.metadata_id).map(|metadata| (metadata, meme))); + + let invoker_name = CONFIG + .discord + .guild() + .member(&ctx, rec.user_id as u64) + .await + .map(|m| m.display_name().to_owned()) + .unwrap_or("???".to_owned()); + + let result = match meme { + Ok((metadata, meme)) => { + let author_name = CONFIG + .discord + .guild() + .member(&ctx, metadata.created_by as u64) + .await + .map(|m| m.display_name().to_owned()) + .unwrap_or("???".to_owned()); + + format!( + "{}. [{}{}] \"{}\" by {} ({}). invoked by {}.", + i + 1, + rand, + ago, + meme.title, + author_name, + metadata.created.date().format(CLEAN_DATE_FORMAT), + invoker_name + ) + }, + Err(e) => { + if let Some(variant) = e.downcast_ref::() { + if *variant != NotFound { + error!("error encountered loading meme history: {}", e); + } + } + + format!("{}. [{}{}] not found. invoked by {}.", i + 1, rand, ago, invoker_name) + }, + }; + + Ok(result) } #[command] @@ -211,11 +222,12 @@ pub async fn stats(ctx: &Context, msg: &Message, _: Args) -> CommandResult { debug!("reporting stats"); - let rand_user: User = UserId::new(stats.most_random_meme_user).to_user(&ctx)?; - let direct_user: User = UserId::new(stats.most_directly_named_meme_user).to_user(&ctx)?; + let rand_user: User = UserId::new(stats.most_random_meme_user).to_user(&ctx).await?; + let direct_user: User = UserId::new(stats.most_directly_named_meme_user).to_user(&ctx).await?; - let rand_user = rand_user.nick_in(&ctx, CONFIG.discord.guild()).unwrap_or(rand_user.name); - let direct_user = direct_user.nick_in(&ctx, CONFIG.discord.guild()).unwrap_or(direct_user.name); + let rand_user = rand_user.nick_in(&ctx, CONFIG.discord.guild()).await.unwrap_or(rand_user.name); + let direct_user = + direct_user.nick_in(&ctx, CONFIG.discord.guild()).await.unwrap_or(direct_user.name); let s = format!( r#" @@ -270,15 +282,14 @@ and *{}* was the most-memed overall ({})"#, #[command] pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - use db; - use itertools::Itertools; use serenity::model::id::UserId; let s = db::memers()? .into_iter() - .map(|info| { - let user = UserId::new(info.user_id).to_user(&ctx)?; - let username = user.nick_in(&ctx, CONFIG.discord.guild()).unwrap_or(user.name); + .pipe(serenity::futures::stream::iter) + .then(|info| async move { + let user = UserId::new(info.user_id).to_user(&ctx).await?; + let username = user.nick_in(&ctx, CONFIG.discord.guild()).await.unwrap_or(user.name); let res = format!( "**{}**: {} total, {} random, {} specific. favorite meme: *{}* ({})", @@ -290,9 +301,10 @@ pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult info.most_used_meme_count, ); - Ok(res) + Result::<_, CommandError>::Ok(res) }) - .collect::>>()? + .try_collect::>() + .await? .into_iter() .join("\n"); @@ -301,11 +313,9 @@ pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult #[command] pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - use std::borrow::Borrow; - - use itertools::Itertools; use regex::Regex; use serenity::model::id::UserId; + use std::borrow::Borrow; use crate::{ db, @@ -318,11 +328,10 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul static ref AGE_REGEX: Regex = Regex::new(r"(?i)(?:age|order)=(.*)").unwrap(); } - let guild = msg.channel_id.to_channel(&ctx)?.guild().ok_or(anyhow!("couldn't find guild"))?; - - let guild = guild.read().guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; + let guild = + msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; - let guild = guild.read(); + let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; let creator: Option = { let creator = args.quoted().current().map(|s| CREATOR_REGEX.is_match(s)).unwrap_or(false); @@ -354,11 +363,13 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul } }; - let result = db::query_meme(args.rest(), creator, order)? - .into_iter() - .map(|(meme, metadata)| { - let user = UserId::new(metadata.created_by as u64).to_user(&ctx)?; - let username = user.nick_in(&ctx, CONFIG.discord.guild()).unwrap_or(user.name); + let iter = db::query_meme(args.rest(), creator, order)?.into_iter(); + + let result = iter + .pipe(serenity::futures::stream::iter) + .then(|(meme, metadata)| async move { + let user = UserId::new(metadata.created_by as u64).to_user(&ctx).await?; + let username = user.nick_in(&ctx, CONFIG.discord.guild()).await.unwrap_or(user.name); Ok(format!( "*{}* by **{}** ({}). text length: **{}**, image: **{}**, audio: **{}**", @@ -368,9 +379,12 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul meme.content.map_or(0, |s| s.len()), meme.image_id.map_or("NO", |_s| "YES"), meme.audio_id.map_or("NO", |_s| "YES"), - )) + )) as Result }) - .collect::>>()? + .try_collect::>() + .await; + + let result = result? .into_iter() .scan(0, |state, line| { *state = *state + line.len() + 1; diff --git a/src/commands/meme/invoke.rs b/src/commands/meme/invoke.rs index 03c6251..13996da 100644 --- a/src/commands/meme/invoke.rs +++ b/src/commands/meme/invoke.rs @@ -30,37 +30,37 @@ use crate::{ #[command] #[aliases("mem")] pub async fn meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Optional) + _meme(ctx, msg, args, AudioPlayback::Optional).await } #[command] pub async fn omen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Optional) + _meme(ctx, msg, args, AudioPlayback::Optional).await } #[command] pub async fn silentomen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Prohibited) + _meme(ctx, msg, args, AudioPlayback::Prohibited).await } #[command] pub async fn audioomen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Required) + _meme(ctx, msg, args, AudioPlayback::Required).await } #[command] #[aliases("audiomeme", "audiomem")] pub async fn audio_meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Required) + _meme(ctx, msg, args, AudioPlayback::Required).await } #[command] #[aliases("silentmeme", "silentmem")] pub async fn silent_meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Prohibited) + _meme(ctx, msg, args, AudioPlayback::Prohibited).await } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -152,7 +152,7 @@ async fn rand_meme( #[command] #[aliases("rarememe", "raremem")] pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let should_audio = ctx.users_listening()?; + let should_audio = util::users_listening(ctx).await?; let mut conn = connection()?; @@ -160,7 +160,7 @@ pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResu match meme { Ok(meme) => { InvocationRecord::create(&mut conn, msg.author.id.get(), msg.id.get(), meme.id, true)?; - send_meme(ctx, &meme, &mut conn, msg) + send_meme(ctx, &meme, &mut conn, msg).await }, Err(e) => { match e.downcast_ref::() { @@ -177,7 +177,7 @@ pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResu .map_err(CommandError::from) .await?; - Err(e) + Err(e.into()) }, } } diff --git a/src/commands/meme/mod.rs b/src/commands/meme/mod.rs index 31d9b78..24fc50d 100644 --- a/src/commands/meme/mod.rs +++ b/src/commands/meme/mod.rs @@ -3,6 +3,7 @@ use log::debug; use rand::random; use serenity::{ all::ReactionType, + async_trait, builder::{ CreateAttachment, CreateMessage, @@ -14,13 +15,21 @@ use serenity::{ model::channel::Message, prelude::*, }; +use songbird::input::{ + core::io::MediaSource, + AudioStream, + AudioStreamError, + Compose, + Input, +}; use crate::{ - audio::{ - PlayArgs, - PlayQueue, + commands::songbird, + db::{ + Audio, + Meme, }, - db::Meme, + CONFIG, }; pub use self::{ @@ -94,27 +103,45 @@ async fn send_meme( }, }; - // note: slight edge-case race condition here: there could have been something queued since we - // checked whether anything was playing. not a significant negative impact and unlikely, so i'm - // not worrying about it if let Some(audio) = audio { let audio = audio?; - { - let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); - let mut play_queue = queue_lock.write().unwrap(); - - play_queue.meme_queue.push_back(PlayArgs { - initiator: msg.author.name.clone(), - data: ::either::Right(audio.data.clone()), - sender_channel: msg.channel_id, - start: None, - end: None, - }); + let (_sb, call) = songbird(ctx, msg).await?; + let mut call = call.lock().await; + + if call.current_channel().is_none() { + call.join(CONFIG.discord.voice_channel()).await?; } + call.enqueue_input(Input::Lazy(Box::new(audio))).await; + msg.react(ctx, ReactionType::Unicode("📣".to_owned())).await?; } Ok(()) } + +#[async_trait] +impl Compose for Audio { + fn create(&mut self) -> Result>, AudioStreamError> { + let ms = std::io::Cursor::new(self.data.clone()); + let ms: Box = Box::new(ms); + + Ok(AudioStream { + input: ms, + hint: None, + }) + } + + #[inline] + async fn create_async( + &mut self, + ) -> Result>, AudioStreamError> { + self.create() + } + + #[inline] + fn should_create_async(&self) -> bool { + false + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4893e73..c8a7014 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,4 @@ +use crate::util; use log::info; use serenity::framework::{ standard::macros::group, @@ -9,10 +10,7 @@ pub use self::meme::*; pub use self::{ playback::*, roll::ROLL_COMMAND, - today::{ - today, - TODAY_COMMAND, - }, + today::TODAY_COMMAND, }; pub(crate) mod playback; @@ -38,16 +36,24 @@ pub fn register_commands(f: StandardFramework) -> StandardFramework { let result = result.group(&crate::game::GAME_GROUP); result.help(&help::HELP).unrecognised_command(|ctx, msg, unrec| { - let url = match msg.content.split_whitespace().skip(1).next() { - Some(x) if x.starts_with("http") => x, - _ => { - info!("bad command formatting: '{}'", unrec); - let _ = ctx.send(msg.channel_id, "format your commands right. fuck you.", msg.tts); - return; - }, - }; - - let _ = _play(ctx, msg, &url); + Box::pin(async move { + let url = match msg.content.split_whitespace().skip(1).next() { + Some(x) if x.starts_with("http") => x, + _ => { + info!("bad command formatting: '{}'", unrec); + let _ = util::send( + ctx, + msg.channel_id, + "format your commands right. fuck you.", + msg.tts, + ) + .await; + return; + }, + }; + + let _ = _play(ctx, msg, &url); + }) }) } diff --git a/src/commands/playback.rs b/src/commands/playback.rs index 21393a2..7ecef47 100644 --- a/src/commands/playback.rs +++ b/src/commands/playback.rs @@ -1,7 +1,3 @@ -use either::{ - Left, - Right, -}; use log::{ debug, error, @@ -18,32 +14,43 @@ use serenity::{ CommandError, CommandResult, }, - futures::TryFutureExt, model::channel::Message, prelude::*, }; -use tap::{ - Conv, - Pipe, +use songbird::{ + input::YoutubeDl, + Call, + Songbird, }; +use std::sync::Arc; +use tap::Conv; use crate::{ - audio::{ - parse_times, - PlayArgs, - PlayQueue, - VoiceManager, - }, + bot::HttpKey, commands::sound_levels::*, util, CONFIG, }; #[group] -#[commands(skip, pause, resume, list, die, mute, unmute, play, volume)] +#[commands(skip, pause, resume, list, die, mute, unmute, play)] #[only_in(guild)] struct Playback; +pub async fn songbird( + ctx: &Context, + msg: &Message, +) -> Result<(Arc, Arc>), CommandError> { + let Some(gid) = msg.guild_id else { + return Err(anyhow::anyhow!("no guild id").into()); + }; + + let sb = songbird::get(ctx).await.expect("acquiring songbird handle"); + let call = sb.get_or_insert(gid); + + Ok((sb, call)) +} + pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { use url::{ Host, @@ -83,18 +90,20 @@ pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { return Ok(()); } - let (start, end) = parse_times(&msg.content); + let (_sb, call) = songbird(ctx, msg).await?; + let mut call = call.lock().await; - let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); - let mut play_queue = queue_lock.write().unwrap(); + if call.current_channel().is_none() { + call.join(CONFIG.discord.voice_channel()).await?; + } - play_queue.general_queue.push_back(PlayArgs { - initiator: msg.author.name.clone(), - data: Left(url.conv::()), - sender_channel: msg.channel_id, - start, - end, - }); + let client = { + let data = ctx.data.read().await; + data.get::().unwrap().clone() + }; + + let input = YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), url.conv::()); + call.enqueue_input(input.into()).await; Ok(()) } @@ -115,37 +124,15 @@ pub async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult }, }; - _play(ctx, msg, &url) + _play(ctx, msg, &url).await } #[command] pub async fn pause(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); + let (_sb, call) = songbird(ctx, msg).await?; - let done = || util::send(ctx, msg.channel_id, "r u srs", msg.tts).map_err(CommandError::from); - let playing = { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => return done().await, - }; - - let audio = current_item.audio.lock(); - audio.playing - }; - - if !playing { - return done().await; - } - - { - let queue = queue_lock.write().unwrap(); - let ref audio = queue.playing.clone().unwrap().audio; - audio.lock().pause(); - - info!("paused playback"); - } + let call = call.lock().await; + call.queue().pause()?; Ok(()) } @@ -157,58 +144,21 @@ pub async fn resume(ctx: &Context, msg: &Message, _: Args) -> CommandResult { } async fn _resume(ctx: &Context, msg: &Message) -> CommandResult { - let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); - - let done = || util::send(ctx, msg.channel_id, "r u srs", msg.tts).map_err(CommandError::from); - let playing = { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => { - done().await?; - return Ok(()); - }, - }; - - let audio = current_item.audio.lock(); - audio.playing - }; + let (_sb, call) = songbird(ctx, msg).await?; - if playing { - done().await?; - debug!("attempted to resume playback while sound was already playing"); - return Ok(()); - } - - { - let queue = queue_lock.write().unwrap(); - let ref audio = queue.playing.clone().unwrap().audio; - audio.lock().play(); - info!("playback resumed"); - } + let call = call.lock().await; + call.queue().resume()?; Ok(()) } #[command] #[aliases("next")] -pub async fn skip(ctx: &Context, _msg: &Message, _args: Args) -> CommandResult { - let data = ctx.data.write().await; - - let mgr_lock = data.get::().cloned().unwrap(); - let mut manager = mgr_lock.lock(); - - let queue_lock = data.get::().cloned().unwrap(); +pub async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let (_sb, call) = songbird(ctx, msg).await?; - if let Some(handler) = manager.get_mut(CONFIG.discord.guild()) { - handler.stop(); - let mut play_queue = queue_lock.write().unwrap(); - play_queue.playing = None; - info!("skipped currently-playing audio"); - } else { - debug!("got skip with no handler attached"); - } + let call = call.lock().await; + call.queue().skip()?; Ok(()) } @@ -216,29 +166,10 @@ pub async fn skip(ctx: &Context, _msg: &Message, _args: Args) -> CommandResult { #[command] #[aliases("sudoku", "fuckoff", "stop")] pub async fn die(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let data = ctx.data.write().await; - - let mgr_lock = data.get::().cloned().unwrap(); - let mut manager = mgr_lock.lock(); - - let queue_lock = data.get::().cloned().unwrap(); - - { - let mut play_queue = queue_lock.write().unwrap(); - - play_queue.playing = None; - play_queue.general_queue.clear(); - play_queue.meme_queue.clear(); - } + let (_sb, call) = songbird(ctx, msg).await?; - if let Some(handler) = manager.get_mut(CONFIG.discord.guild()) { - info!("killing playback"); - handler.stop(); - handler.leave(); - } else { - util::send(ctx, msg.channel_id, "YOU die", msg.tts).await?; - debug!("got die with no handler attached"); - } + let call = call.lock().await; + call.queue().stop(); Ok(()) } @@ -246,55 +177,19 @@ pub async fn die(ctx: &Context, msg: &Message, _: Args) -> CommandResult { #[command] #[aliases("queue")] pub async fn list(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); - let play_queue = queue_lock.read().unwrap(); - - let channel = msg.channel(&ctx).await.unwrap().guild().unwrap(); - - info!("listing queue"); - match play_queue.playing { - Some(ref info) => { - let audio = info.audio.lock(); - let status = if audio.playing { - "playing" - } else { - "paused:" - }; - - let playing_info = match info.init_args.data { - Left(ref url) => format!(" `{}`", url), - Right(_) => "memeing".to_owned(), - }; - - util::send( - ctx, - msg.channel_id, - &format!("Currently {} {} ({})", status, playing_info, info.init_args.initiator), - msg.tts, - ) + let (_sb, call) = songbird(ctx, msg).await?; + + let call = call.lock().await; + let queue = call.queue(); + + util::send(ctx, msg.channel_id, "(command fix work-in-progress)", msg.tts).await?; + + for track in queue.current_queue().into_iter() { + let info = track.get_info().await?; + + util::send(ctx, msg.channel_id, format!("track playing for {:?}", info.play_time), msg.tts) .await?; - }, - None => { - debug!("`list` called with no items in queue"); - util::send(ctx, msg.channel_id, "Nothing is playing you meme", msg.tts).await?; - return Ok(()); - }, } - play_queue - .meme_queue - .iter() - .chain(play_queue.general_queue.iter()) - .pipe(serenity::futures::stream::iter) - .for_each(|info| async move { - let playing_info = match info.data { - Left(ref url) => format!("`{}`", url), - Right(_) => "meme".to_owned(), - }; - - let _ = channel.say(&ctx, &format!("{} ({})", playing_info, info.initiator)).await; - }) - .await; - Ok(()) } diff --git a/src/commands/roll.rs b/src/commands/roll.rs index 6aefe34..45e3ba8 100644 --- a/src/commands/roll.rs +++ b/src/commands/roll.rs @@ -56,7 +56,7 @@ impl Calc { use self::Rule::*; lazy_static! { - static ref CLIMBER: PrecClimber = { + static ref CLIMBER: PrecClimber = { use pest::prec_climber::{ Assoc::*, Operator, @@ -75,7 +75,7 @@ impl Calc { let result = Calc::parse(calc, s.as_ref()).map_err(|_| CalcError::Pest)?; - fn eval_single_pair(pair: Pair) -> StdResult { + fn eval_single_pair(pair: Pair) -> StdResult { let result = match pair.as_rule() { oct | hex | binary => { let base = match pair.as_rule() { @@ -159,7 +159,7 @@ impl Calc { Ok(result) } - fn eval_expr(p: Pairs) -> StdResult { + fn eval_expr(p: Pairs) -> StdResult { CLIMBER.climb(p, eval_single_pair, |lhs, op, rhs| { let lhs = lhs?; let rhs = rhs?; diff --git a/src/commands/sound_levels.rs b/src/commands/sound_levels.rs index db0b6a6..8c75b37 100644 --- a/src/commands/sound_levels.rs +++ b/src/commands/sound_levels.rs @@ -1,132 +1,88 @@ -use log::{ - error, - info, - trace, - warn, -}; use serenity::{ framework::standard::{ macros::command, Args, - CommandError, CommandResult, }, - futures::TryFutureExt, model::channel::Message, prelude::*, }; -use crate::{ - audio::{ - PlayQueue, - VoiceManager, - }, - util, - CONFIG, -}; +use crate::commands::songbird; pub const DEFAULT_VOLUME: f32 = 0.20; const MAX_VOLUME: f32 = 5.0; #[command] -pub async fn mute(ctx: &Context, _: &Message, _: Args) -> CommandResult { - let mgr_lock = ctx.data.write().await.get::().cloned().unwrap(); - let mut manager = mgr_lock.lock(); +pub async fn mute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { + let (_sb, call) = songbird(ctx, msg).await?; - manager.get_mut(CONFIG.discord.guild()).map(|handler| { - if handler.self_mute { - trace!("Already muted.") - } else { - handler.mute(true); - trace!("Muted"); - } - }); + let mut call = call.lock().await; + call.mute(true).await?; Ok(()) } #[command] pub async fn unmute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let mgr_lock = ctx.data.write().await.get::().cloned().unwrap(); - let mut manager = mgr_lock.lock(); + let (_sb, call) = songbird(ctx, msg).await?; - if let Some(handler) = manager.get_mut(CONFIG.discord.guild()) { - if !handler.self_mute { - trace!("Already unmuted.") - } else { - handler.mute(false); - trace!("Unmuted"); - let _ = util::send(ctx, msg.channel_id, "REEEEEEEEEEEEEE", msg.tts) - .map_err(CommandError::from) - .await; - } - } + let mut call = call.lock().await; + call.mute(true).await?; Ok(()) } -#[command] -pub async fn volume(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - if args.len() == 0 { - let vol = { - let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); - let play_queue = queue_lock.read().unwrap(); - (play_queue.volume / DEFAULT_VOLUME * 100.0) as usize - }; - - trace!("reporting volume {}", vol); - - return util::send(ctx, msg.channel_id, &format!("volume: {}%", vol), msg.tts) - .map_err(CommandError::from) - .await; - } - - let vol: usize = match args.single::() { - Ok(vol) if vol.is_nan() => { - warn!("reporting NaN volume"); - return util::send(ctx, msg.channel_id, "you're a fuck", msg.tts) - .map_err(CommandError::from) - .await; - }, - Ok(vol) => vol as usize, - Err(e) => { - error!("parsing volume arg: {}", e); - return util::send(ctx, msg.channel_id, "???????", msg.tts) - .map_err(CommandError::from) - .await; - }, - }; - - let mut vol: f32 = (vol as f32) / 100.0; // force aliasing to reasonable values - let adjusted_text = if vol > MAX_VOLUME { - format!(" ({:.0}% max)", MAX_VOLUME * 100.0) - } else { - "".to_owned() - }; - - vol = vol.clamp(0.0, MAX_VOLUME); - - let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); - - { - let mut play_queue = queue_lock.write().unwrap(); - play_queue.volume = vol * DEFAULT_VOLUME; - info!("volume updated to {}", vol); - } - - util::send(ctx, msg.channel_id, format!("volume adjusted{}", adjusted_text), msg.tts).await?; - - { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => return Ok(()), - }; - - let mut audio = current_item.audio.lock(); - audio.volume(play_queue.volume); - } - - Ok(()) -} +// #[command] +// pub async fn volume(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { +// if args.len() == 0 { +// let vol = { +// let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); +// let play_queue = queue_lock.read().unwrap(); +// (play_queue.volume / DEFAULT_VOLUME * 100.0) as usize +// }; +// +// trace!("reporting volume {}", vol); +// +// return util::send(ctx, msg.channel_id, &format!("volume: {}%", vol), msg.tts) +// .map_err(CommandError::from) +// .await; +// } +// +// let vol: usize = match args.single::() { +// Ok(vol) if vol.is_nan() => { +// warn!("reporting NaN volume"); +// return util::send(ctx, msg.channel_id, "you're a fuck", msg.tts) +// .map_err(CommandError::from) +// .await; +// }, +// Ok(vol) => vol as usize, +// Err(e) => { +// error!("parsing volume arg: {}", e); +// return util::send(ctx, msg.channel_id, "???????", msg.tts) +// .map_err(CommandError::from) +// .await; +// }, +// }; +// +// let mut vol: f32 = (vol as f32) / 100.0; // force aliasing to reasonable values +// let adjusted_text = if vol > MAX_VOLUME { +// format!(" ({:.0}% max)", MAX_VOLUME * 100.0) +// } else { +// "".to_owned() +// }; +// +// vol = vol.clamp(0.0, MAX_VOLUME); +// +// let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); +// +// { +// let mut play_queue = queue_lock.write().unwrap(); +// play_queue.volume = vol * DEFAULT_VOLUME; +// info!("volume updated to {}", vol); +// } +// +// util::send(ctx, msg.channel_id, format!("volume adjusted{}", adjusted_text), msg.tts).await?; +// +// Ok(()) +// } diff --git a/src/commands/today/mod.rs b/src/commands/today/mod.rs index 0a0ba7b..7f1dca7 100644 --- a/src/commands/today/mod.rs +++ b/src/commands/today/mod.rs @@ -1,5 +1,4 @@ use chrono::Duration; -use either::Left; use lazy_static::lazy_static; use log::debug; use rand::{ @@ -15,13 +14,14 @@ use serenity::{ model::channel::Message, prelude::*, }; +use songbird::input::YoutubeDl; +use tap::Conv; use crate::{ - audio::{ - PlayArgs, - PlayQueue, - }, + bot::HttpKey, + commands::songbird, util, + CONFIG, }; mod prelude; @@ -50,19 +50,6 @@ pub struct TodayArgs { pub end: Option, } -impl TodayArgs { - #[inline] - pub fn as_play_args(&self, msg: &Message) -> PlayArgs { - PlayArgs { - initiator: "you have done this to yourself :^)".to_string(), - data: Left(self.url.to_owned()), - sender_channel: msg.channel_id, - start: self.start, - end: self.end, - } - } -} - lazy_static! { static ref ALL: Vec TodayIter> = vec![ sept_21::sept_21, @@ -112,17 +99,36 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { debug!("{} options for {}", options.len(), today); - let play_args = options.choose(&mut thread_rng()).map(|x| x.as_play_args(msg)); + let play_args = options.choose(&mut thread_rng()); if let Some(play_args) = play_args { - play_args.data.as_ref().left().iter().for_each(|url| { - debug!("today selected: {}", url); - }); + let (_sb, call) = songbird(ctx, msg).await?; + let mut call = call.lock().await; - let queue_lock = ctx.data.write().await.get::().cloned().unwrap(); - let mut play_queue = queue_lock.write().unwrap(); + if call.current_channel().is_none() { + call.join(CONFIG.discord.voice_channel()).await?; + } + + let client = { + let data = ctx.data.read().await; + data.get::().unwrap().clone() + }; + + let input = + YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), play_args.url.conv::()); - play_queue.general_queue.push_front(play_args); + call.enqueue_input(input.into()).await; + + let q = call.queue(); + q.pause()?; + q.modify_queue(move |q| { + let last = q.pop_back(); + + if let Some(last) = last { + q.push_front(last); + } + }); + q.resume()?; } else { util::send(ctx, msg.channel_id, "no", false).await?; util::send(ctx, msg.channel_id, ":angry:", false).await?; -- cgit v1.3.1