aboutsummaryrefslogtreecommitdiff
path: root/src/commands
diff options
context:
space:
mode:
authorNathan Perry <np@nathanperry.dev>2024-05-08 12:55:35 -0400
committerNathan Perry <np@nathanperry.dev>2024-05-08 14:16:01 -0400
commitffba60b278162707bc4eb004c3bfb6b2e9595213 (patch)
treeedf8172ecad59d46a6056944fd9e79f7dfb327c2 /src/commands
parentfe467f60d99efa54f2ef64606e7d39b9b06d7294 (diff)
rework to use songbird
Diffstat (limited to 'src/commands')
-rw-r--r--src/commands/help.rs23
-rw-r--r--src/commands/meme/create.rs84
-rw-r--r--src/commands/meme/history.rs216
-rw-r--r--src/commands/meme/invoke.rs18
-rw-r--r--src/commands/meme/mod.rs61
-rw-r--r--src/commands/mod.rs32
-rw-r--r--src/commands/playback.rs215
-rw-r--r--src/commands/roll.rs6
-rw-r--r--src/commands/sound_levels.rs166
-rw-r--r--src/commands/today/mod.rs56
10 files changed, 389 insertions, 488 deletions
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<UserId>,
) -> 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::<DieselError>()
@@ -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::<DieselError>()
@@ -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::<DieselError>() {
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::<usize>().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::<DieselError>() {
- if *variant != NotFound {
- error!("error encountered loading meme history: {}", e);
- }
- }
+ let resp = serenity::futures::stream::iter(records.into_iter().enumerate().rev())
+ .then(|(i, rec)| ir_info(ctx, i, rec, &mut conn))
+ .try_collect::<Vec<String>>()
+ .await?;
- 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");
+ let resp = resp.join("\n");
- util::send(ctx, msg.channel_id, &resp, false).await
+ 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<String, CommandError> {
+ 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::<DieselError>() {
+ 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::<Result<Vec<_>>>()?
+ .try_collect::<Vec<String>>()
+ .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<u64> = {
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<String, CommandError>
})
- .collect::<Result<Vec<_>>>()?
+ .try_collect::<Vec<String>>()
+ .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::<DieselError>() {
@@ -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::<PlayQueue>().cloned().unwrap();
- let mut play_queue = queue_lock.write().unwrap();
+ let (_sb, call) = songbird(ctx, msg).await?;
+ let mut call = call.lock().await;
- 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,
- });
+ 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<AudioStream<Box<dyn MediaSource>>, AudioStreamError> {
+ let ms = std::io::Cursor::new(self.data.clone());
+ let ms: Box<dyn MediaSource> = Box::new(ms);
+
+ Ok(AudioStream {
+ input: ms,
+ hint: None,
+ })
+ }
+
+ #[inline]
+ async fn create_async(
+ &mut self,
+ ) -> Result<AudioStream<Box<dyn MediaSource>>, 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;
- },
- };
+ 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);
+ 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<Songbird>, Arc<Mutex<Call>>), 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::<PlayQueue>().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::<String>()),
- sender_channel: msg.channel_id,
- start,
- end,
- });
+ let client = {
+ let data = ctx.data.read().await;
+ data.get::<HttpKey>().unwrap().clone()
+ };
+
+ let input = YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), url.conv::<String>());
+ 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::<PlayQueue>().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::<PlayQueue>().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 => {
- done().await?;
- return Ok(());
- },
- };
-
- let audio = current_item.audio.lock();
- audio.playing
- };
-
- 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::<VoiceManager>().cloned().unwrap();
- let mut manager = mgr_lock.lock();
+pub async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
+ let (_sb, call) = songbird(ctx, msg).await?;
- let queue_lock = data.get::<PlayQueue>().cloned().unwrap();
-
- 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::<VoiceManager>().cloned().unwrap();
- let mut manager = mgr_lock.lock();
+ let (_sb, call) = songbird(ctx, msg).await?;
- let queue_lock = data.get::<PlayQueue>().cloned().unwrap();
-
- {
- let mut play_queue = queue_lock.write().unwrap();
-
- play_queue.playing = None;
- play_queue.general_queue.clear();
- play_queue.meme_queue.clear();
- }
-
- 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::<PlayQueue>().cloned().unwrap();
- let play_queue = queue_lock.read().unwrap();
+ let (_sb, call) = songbird(ctx, msg).await?;
- let channel = msg.channel(&ctx).await.unwrap().guild().unwrap();
+ let call = call.lock().await;
+ let queue = call.queue();
- info!("listing queue");
- match play_queue.playing {
- Some(ref info) => {
- let audio = info.audio.lock();
- let status = if audio.playing {
- "playing"
- } else {
- "paused:"
- };
+ util::send(ctx, msg.channel_id, "(command fix work-in-progress)", msg.tts).await?;
- let playing_info = match info.init_args.data {
- Left(ref url) => format!(" `{}`", url),
- Right(_) => "memeing".to_owned(),
- };
+ for track in queue.current_queue().into_iter() {
+ let info = track.get_info().await?;
- util::send(
- ctx,
- msg.channel_id,
- &format!("Currently {} {} ({})", status, playing_info, info.init_args.initiator),
- msg.tts,
- )
+ 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<self::Rule> = {
+ static ref CLIMBER: PrecClimber<Rule> = {
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<self::Rule>) -> StdResult<f64, CalcError> {
+ fn eval_single_pair(pair: Pair<Rule>) -> StdResult<f64, CalcError> {
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<self::Rule>) -> StdResult<f64, CalcError> {
+ fn eval_expr(p: Pairs<Rule>) -> StdResult<f64, CalcError> {
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::<VoiceManager>().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::<VoiceManager>().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::<PlayQueue>().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::<f32>() {
- 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::<PlayQueue>().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::<PlayQueue>().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::<f32>() {
+// 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::<PlayQueue>().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<Duration>,
}
-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<fn(chrono::NaiveDateTime) -> 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::<PlayQueue>().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::<HttpKey>().unwrap().clone()
+ };
+
+ let input =
+ YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), play_args.url.conv::<String>());
- 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?;