From 72d9bbe15220c21909dec8e30fb80729a24cec72 Mon Sep 17 00:00:00 2001 From: Nathan Perry Date: Tue, 6 Aug 2024 10:45:06 -0400 Subject: first pass convert to poise --- src/commands/game.rs | 572 +++++++++++++++++++++++++++++++++++++++++++ src/commands/help.rs | 31 --- src/commands/meme/create.rs | 104 ++++---- src/commands/meme/delete.rs | 29 +-- src/commands/meme/history.rs | 157 ++++++------ src/commands/meme/invoke.rs | 149 +++++------ src/commands/meme/mod.rs | 75 +++--- src/commands/mod.rs | 94 +++---- src/commands/playback.rs | 153 +++++------- src/commands/roll.rs | 111 ++++----- src/commands/sound_levels.rs | 79 +----- src/commands/today/mod.rs | 32 +-- 12 files changed, 986 insertions(+), 600 deletions(-) create mode 100644 src/commands/game.rs delete mode 100644 src/commands/help.rs (limited to 'src/commands') diff --git a/src/commands/game.rs b/src/commands/game.rs new file mode 100644 index 0000000..72633b5 --- /dev/null +++ b/src/commands/game.rs @@ -0,0 +1,572 @@ +use std::{ + fs, + iter, + path::PathBuf, + result::Result as StdResult, + str::{ + self, + FromStr, + }, +}; + +use anyhow::anyhow; +use fnv::{ + FnvHashMap, + FnvHashSet, +}; +use itertools::Itertools; +use lazy_static::lazy_static; +use log::{ + debug, + error, + info, +}; +use serde::Deserialize; +use serenity::model::{ + guild::Guild, + id::UserId, +}; +use tap::Pipe; +use url::Url; + +use crate::{ + bot::HttpKey, + util, + PoiseContext, + Result, + CONFIG, +}; + +lazy_static! { + static ref SPREADSHEET_URL: Url = Url::parse(&format!( + "https://sheets.googleapis.com/v4/spreadsheets/{}/values:batchGet", + &CONFIG.sheets.spreadsheet, + )) + .expect("parsing spreadsheet url"); +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +struct UserInfo { + name: String, + + #[serde(flatten)] + profile: ProfileInfo, +} + +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct ProfileInfo { + #[serde(rename = "steam")] + steam_id: Option, + + #[serde(rename = "discord")] + discord_user_id: u64, +} + +lazy_static! { + static ref USER_MAP_STR: String = { + let default_path = PathBuf::from_str("user_id_mapping").unwrap(); + let mapping_path = CONFIG.user_id_mapping.as_ref().unwrap_or(&default_path); + + fs::read_to_string(mapping_path).unwrap_or("{}".to_owned()) + }; + static ref USER_INFO_MAP: FnvHashMap = { + let v: Vec = serde_json::from_str(&USER_MAP_STR).unwrap(); + + let result = v + .into_iter() + .map(|ui| { + let UserInfo { + name, + profile, + } = ui; + + (name, profile) + }) + .collect::>(); + + info!( + "loaded user info for {} users ({:#?})", + result.len(), + result.keys().collect::>() + ); + + result + }; + static ref DISCORD_MAP: FnvHashMap = { + USER_INFO_MAP + .clone() + .into_iter() + .map(|(name, profile)| (UserId::new(profile.discord_user_id), name)) + .collect::>() + }; + static ref STEAM_MAP: FnvHashMap = { + USER_INFO_MAP + .clone() + .into_iter() + .filter_map(|(_, profile)| { + profile.steam_id.map(|sid| (UserId::new(profile.discord_user_id), sid)) + }) + .collect::>() + }; + static ref ALPHABET: Vec = (0..26).map(|x| (x + b'a') as char).collect(); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)] +enum GameStatus { + Installed, + NotInstalled, + NotOwned, + Unknown, +} + +impl FromStr for GameStatus { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + use std::char; + + if s.starts_with('y') { + Ok(GameStatus::Installed) + } else if s.starts_with("n/i") { + Ok(GameStatus::NotInstalled) + } else if s.starts_with('n') { + Ok(GameStatus::NotOwned) + } else if s.chars().all(char::is_whitespace) { + Ok(GameStatus::Unknown) + } else { + Err(anyhow!(format!("unexpected status '{}'", s))) + } + } +} + +pub fn commands() -> Vec> { + vec![installedgame(), ownedgame(), game(), updategaem()] +} + +#[poise::command(prefix_command, guild_only, category = "gaem", aliases("installedgaem"))] +pub async fn installedgame(ctx: PoiseContext<'_>, args: util::RestVec) -> anyhow::Result<()> { + _game(ctx, args.into_inner(), GameStatus::Installed).await +} + +#[poise::command(prefix_command, guild_only, category = "gaem", aliases("ownedgaem"))] +pub async fn ownedgame(ctx: PoiseContext<'_>, args: util::RestVec) -> anyhow::Result<()> { + _game(ctx, args.into_inner(), GameStatus::NotInstalled).await +} + +#[derive(Copy, Clone, Debug, thiserror::Error, PartialEq, Eq, Hash)] +pub enum UserLookupError { + #[error("too many possible options ({}) for query", _0)] + Ambiguous(usize), + + #[error("user wasn't found in the guild")] + NotFound, +} + +pub fn get_user_id>(g: &Guild, s: S) -> StdResult { + let s = s.as_ref().trim_start_matches('@').to_lowercase(); + + if let Some(info) = USER_INFO_MAP.get(&s) { + return Ok(UserId::new(info.discord_user_id)); + } + + let nicks = g.members_nick_containing(&s, false, false); + + { + let exact_match = nicks.iter().find(|(m, _)| m.display_name().to_lowercase() == s); + + if let Some((m, _)) = exact_match { + return Ok(m.user.id); + } + } + + let usernames = g.members_username_containing(&s, false, false); + + { + let exact_match = usernames.iter().find(|(m, _)| m.user.name.to_lowercase() == s); + + if let Some((m, _)) = exact_match { + return Ok(m.user.id); + } + } + + let opts = nicks + .into_iter() + .chain(usernames) + .map(|(member, _)| member.user.id) + .collect::>(); + + match opts.len() { + 0 => Err(UserLookupError::NotFound), + 1 => Ok(opts.into_iter().next().unwrap()), + x => Err(UserLookupError::Ambiguous(x)), + } +} + +#[poise::command(prefix_command, guild_only, category = "gaem", aliases("gaem"))] +async fn game(ctx: PoiseContext<'_>, args: util::RestVec) -> anyhow::Result<()> { + _game(ctx, args.into_inner(), GameStatus::Installed).await +} + +async fn _game( + ctx: PoiseContext<'_>, + user_args: Vec, + min_status: GameStatus, +) -> anyhow::Result<()> { + use serenity::futures::StreamExt; + + let users = { + let guild = ctx + .channel_id() + .to_channel(&ctx) + .await? + .guild() + .ok_or(anyhow!("couldn't find guild"))?; + + let mut users = user_args + .into_iter() + .pipe(serenity::futures::stream::iter) + .filter_map(|u| { + let guild = &guild; + async move { + use std::borrow::Borrow; + + let possible = { + let Ok(guild) = + guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild")) + else { + error!("failed retrieving guild"); + return None; + }; + + get_user_id(guild.borrow(), &u) + }; + + debug!("parsed userid {:?}", possible); + + match possible { + Ok(x) => Some(x), + Err(UserLookupError::NotFound) => { + let _ = util::reply(ctx, format!("didn't recognize {u}")).await; + + None + }, + Err(UserLookupError::Ambiguous(x)) => { + let _ = + util::reply(ctx, format!("too many matches ({x}) for {u}")).await; + None + }, + } + } + }) + .filter_map(|uid| async move { + let res = DISCORD_MAP.get(&uid).map(|s| s.to_lowercase()); + + if res.is_none() { + info!("user {uid} is not recognized"); + } + + res + }) + .collect::>() + .await; + + if users.is_empty() { + let guild = guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild"))?; + + let pairs = guild + .voice_states + .iter() + .filter_map(|(uid, voice)| voice.channel_id.map(|cid| (*uid, cid))) + .collect::>(); + + let channel = + pairs.get(&ctx.author().id).cloned().unwrap_or(CONFIG.discord.voice_channel()); + + users = pairs + .iter() + .filter_map(|(uid, cid)| { + if *cid == channel { + DISCORD_MAP.get(uid).map(|s| s.to_lowercase()) + } else { + None + } + }) + .collect::>(); + } + + users + }; + + let inferred = users.is_empty(); + + if inferred && users.len() < 2 || !inferred && users.is_empty() { + info!("too few known users to make game comparison"); + util::reply(ctx, "yer too lonely").await?; + return Ok(()); + } + + let client = { + let data = ctx.serenity_context().data.read().await; + data.get::().unwrap().clone() + }; + let data = load_spreadsheet(&client).await?; + + let user_indexes = (0..data.len()) + .filter_map(|i| { + let user = data[i][0].to_lowercase(); + + if users.contains(&user) { + Some((user, i)) + } else { + None + } + }) + .collect::>(); + + let data_ref = &data; + let user_games = user_indexes + .iter() + .map(|(user, col)| { + let empty_hash_set: FnvHashSet<_> = vec![].into_iter().collect(); + + let mut game_map = vec![ + (GameStatus::Installed, empty_hash_set.clone()), + (GameStatus::NotInstalled, empty_hash_set.clone()), + (GameStatus::NotOwned, empty_hash_set.clone()), + (GameStatus::Unknown, empty_hash_set), + ] + .into_iter() + .collect::>(); + + (1..data[*col].len()).for_each(|i| { + let status = + &data_ref[*col][i].parse::().unwrap_or(GameStatus::Unknown); + let game = &data_ref[0][i]; + + game_map.get_mut(status).unwrap().insert(game); + }); + + (user, game_map) + }) + .collect::>(); + + let statuses = vec![ + GameStatus::Installed, + GameStatus::NotOwned, + GameStatus::NotInstalled, + GameStatus::Unknown, + ] + .into_iter() + .filter(|s| s <= &min_status) + .collect::>(); + + let mut games_in_common = { + let game_map = user_games.values().next().unwrap(); + + statuses.iter().fold(iter::empty().collect::>(), |acc, s| { + acc.union(&game_map[s]).cloned().collect() + }) + }; + + for (_user, game_map) in user_games.iter() { + let relevant_games = + statuses.iter().fold(iter::empty().collect::>(), |acc, s| { + acc.union(&game_map[s]).cloned().collect() + }); + + games_in_common = games_in_common.intersection(&relevant_games).cloned().collect(); + } + + let mut games_formatted = + games_in_common.iter().sorted_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())).join("\n"); + + if games_formatted.is_empty() { + games_formatted = "**LITERALLY NOTHING**".to_owned(); + } + + util::reply(ctx, games_formatted).await?; + + Ok(()) +} + +async fn load_spreadsheet(client: &reqwest::Client) -> Result>> { + let mut u = SPREADSHEET_URL.clone(); + + u.query_pairs_mut() + .append_pair("ranges", &format!("a1:{}", &CONFIG.sheets.max_column)) + .append_pair("valueRenderOption", "FORMATTED_VALUE") + .append_pair("majorDimension", "COLUMNS") + .append_pair("key", &CONFIG.sheets.api_key); + + let req = reqwest::Request::new(reqwest::Method::GET, u); + let resp = client.execute(req).await?; + + #[derive(Deserialize)] + struct Resp { + #[serde(rename = "valueRanges")] + value_ranges: Vec, + } + + #[derive(Deserialize)] + struct Inner { + values: Vec>, + } + + let resp = resp.json::().await?; + + Ok(resp.value_ranges.into_iter().next().unwrap().values) +} + +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "gaem", + aliases("updategame") +)] +pub async fn updategaem(ctx: PoiseContext<'_>, user: Option) -> anyhow::Result<()> { + use regex::Regex; + use std::borrow::Borrow; + + let client = { + let data = ctx.serenity_context().data.read().await; + data.get::().unwrap().clone() + }; + + let user = match user { + None => ctx.author().id, + Some(user) => { + let guild = ctx + .channel_id() + .to_channel(&ctx) + .await? + .guild() + .ok_or(anyhow!("couldn't find guild"))?; + + let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; + + get_user_id(guild.borrow(), user).map_err(anyhow::Error::from)? + }, + }; + + debug!("parsed userid {:?}", user); + + let username = match DISCORD_MAP.get(&user) { + Some(s) => s, + None => { + util::reply(ctx, "WHO THE FUCK ARE YE").await?; + return Ok(()); + }, + }; + + let steam_id = match STEAM_MAP.get(&user) { + Some(u) => u, + None => { + util::reply(ctx, "WHO ARE YE ON STEAM").await?; + return Ok(()); + }, + }; + + let spreadsheet = load_spreadsheet(&client).await?; + + let user_column = (0..spreadsheet.len()) + .find(|x| spreadsheet[*x][0].to_lowercase() == username.to_lowercase()); + + let user_column = match user_column { + Some(c) => &spreadsheet[c][1..], + None => { + util::reply(ctx, "YER NOT IN THE SPREADSHEET").await?; + return Ok(()); + }, + }; + + lazy_static! { + static ref APPID_REGEX: Regex = Regex::new(r#"(?i)^\s*app\s*id\s*$"#).unwrap(); + } + + let appid_column = (0..spreadsheet.len()).find(|x| APPID_REGEX.is_match(&spreadsheet[*x][0])); + + let appid_column = match appid_column { + Some(c) => &spreadsheet[c][1..], + None => { + error!("didn't find an appid column in the spreadsheet"); + util::reply(ctx, "SPREADSHEET BROKE").await?; + return Ok(()); + }, + }; + + let missing_appids = (0..user_column.len()) + .filter_map(|x| user_column[x].parse::().ok().map(|s| (x, s))) + .filter(|(_, s)| *s == GameStatus::Unknown || *s == GameStatus::NotOwned) + .filter_map(|(x, _)| { + appid_column.get(x).and_then(|s| s.parse::().ok().map(|appid| (appid, x))) + }); + + let mut u = Url::parse("https://api.steampowered.com/IPlayerService/GetOwnedGames/v1")?; + + u.query_pairs_mut() + .append_pair("key", &CONFIG.steam_api_key) + .append_pair("include_played_free_games", "1") + .append_pair("steamid", &steam_id.to_string()); + + #[derive(Deserialize, Clone, Debug, PartialEq, Eq, Hash)] + struct SteamResp { + response: SteamInner, + } + + #[derive(Deserialize, Clone, Debug, PartialEq, Eq, Hash)] + struct SteamInner { + games: Vec, + } + + #[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] + struct SteamGameEntry { + #[serde(rename = "appid")] + app_id: u64, + + #[serde(rename = "playtime_forever")] + play_time: u64, + } + + let client = { + let data = ctx.serenity_context().data.read().await; + data.get::().unwrap().clone() + }; + + let games_owned = client + .get(u) + .send() + .await? + .json::() + .await? + .response + .games + .into_iter() + .map(|ge| ge.app_id) + .collect::>(); + + let found_games = missing_appids + .filter_map(|(ai, x)| { + if games_owned.contains(&ai) { + Some(&spreadsheet[0][x + 1]) + } else { + None + } + }) + .join("\n"); + + if !found_games.is_empty() { + let n_missing = found_games.chars().filter(|x| *x == '\n').count() + 1; + util::reply( + ctx, + format!( + "{n_missing} games owned on steam that are missing from the list:\n{found_games}" + ), + ) + .await?; + } else { + util::reply(ctx, "up to date").await?; + } + + Ok(()) +} diff --git a/src/commands/help.rs b/src/commands/help.rs deleted file mode 100644 index 0d84b2d..0000000 --- a/src/commands/help.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::collections::HashSet; - -use serenity::{ - framework::standard::{ - help_commands, - macros::help, - Args, - CommandGroup, - CommandResult, - HelpOptions, - }, - model::{ - channel::Message, - id::UserId, - }, - prelude::*, -}; - -#[help] -pub async fn help( - ctx: &Context, - msg: &Message, - args: Args, - opts: &'static HelpOptions, - groups: &[&'static CommandGroup], - owners: HashSet, -) -> CommandResult { - 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 9aff370..cad9bfc 100644 --- a/src/commands/meme/create.rs +++ b/src/commands/meme/create.rs @@ -2,25 +2,12 @@ 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 serenity::all::ReactionType; use tap::Pipe; use tokio::{ io::AsyncReadExt, @@ -37,20 +24,16 @@ use crate::{ }, parse_times, util, + PoiseContext, FFMPEG_COMMAND, }; -lazy_static! { - static ref DELIMS: Vec = vec![' '.into(), '\n'.into(), '\t'.into()]; -} - -#[command] -pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let mut args = Args::new(args.rest(), DELIMS.as_ref()); - - let title = args.single_quoted::()?; - let text = args.rest().to_owned(); - +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn addmeme( + ctx: PoiseContext<'_>, + title: String, + #[rest] text: String, +) -> anyhow::Result<()> { let text = if text.is_empty() { None } else { @@ -59,20 +42,21 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult let mut conn = connection().await?; - let image = msg.attachments.first(); + let image = util::msg(ctx).and_then(|msg| msg.attachments.first()); if image.is_none() && text.is_none() { warn!("tried to create non-audio meme with no image or text"); - return util::send(ctx, msg.channel_id, "hahAA it's empty xdddd", msg.tts) - .map_err(CommandError::from) - .await; + + util::reply(ctx, "hahAA it's empty xdddd").await?; + return Ok(()); } 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()).await?); + image_id = + Some(Image::create(&mut conn, &att.filename, data, ctx.author().id.get()).await?); }; let save_result = NewMeme { @@ -82,24 +66,25 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult audio_id: None, metadata_id: 0, } - .save(&mut conn, msg.author.id.get()) + .save(&mut conn, ctx.author().id.get()) .await .map(|_| {}); use diesel::result::DatabaseErrorKind; match save_result { Ok(_) => { - msg.react(&ctx, ReactionType::Unicode("👌".to_string())).await?; + util::react(ctx, ReactionType::Unicode("👌".to_string())).await?; }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::() { error!("tried to create meme that already exists"); - msg.react(&ctx, ReactionType::Unicode("❌".to_owned())).await?; - return util::send(ctx, msg.channel_id, "that meme already exists", msg.tts) - .map_err(CommandError::from) - .await; + + util::react(ctx, ReactionType::Unicode("❌".to_owned())).await?; + util::reply(ctx, "that meme already exists").await?; + + return Ok(()); } return Err(e.into()); @@ -109,19 +94,19 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult Ok(()) } -#[command] -pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn addaudiomeme( + ctx: PoiseContext<'_>, + title: String, + audio_str: String, + #[rest] rest: String, +) -> anyhow::Result<()> { debug!("running addaudiomeme"); - let mut args = Args::new(args.rest(), DELIMS.as_ref()); - - let title = args.single_quoted::()?; - let audio_str = args.single_quoted::()?; - let elems = audio_str.split_whitespace().collect::>(); if elems.is_empty() { - util::send(ctx, msg.channel_id, "are you stupid", msg.tts).await?; + util::reply(ctx, "are you stupid").await?; return Err(anyhow!("no audio link was provided").into()); } @@ -168,23 +153,23 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe let mut audio_reader = ffmpeg_command.stdout.unwrap(); - let text = args.rest().to_owned(); - let text = if text.is_empty() { + let text = if rest.is_empty() { None } else { - Some(text) + Some(rest) }; let mut conn = connection().await?; - let image_att = msg.attachments.first().ok_or(anyhow!("no attachment")); + let image_att = + util::msg(ctx).and_then(|x| x.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()).await?.pipe(Some); + Image::create(&mut conn, &att.filename, data, ctx.author().id.get()).await?.pipe(Some); } let mut audio_data = Vec::new(); @@ -193,12 +178,12 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe if bytes == 0 { debug!("read 0 bytes from audio reader"); - return util::send(ctx, msg.channel_id, "🔇🔇🔇🔕🔕🔕🔕🔕🔇🔕🔕🔇🔕🔕📣📢📣📢📣", msg.tts) - .map_err(CommandError::from) - .await; + + util::reply(ctx, "🔇🔇🔇🔕🔕🔕🔕🔕🔇🔕🔕🔇🔕🔕📣📢📣📢📣").await?; + return Ok(()); } - let audio_id = Audio::create(&mut conn, audio_data, msg.author.id.get()).await?; + let audio_id = Audio::create(&mut conn, audio_data, ctx.author().id.get()).await?; let save_result = NewMeme { title, @@ -207,24 +192,25 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe audio_id: Some(audio_id), metadata_id: 0, } - .save(&mut conn, msg.author.id.get()) + .save(&mut conn, ctx.author().id.get()) .await .map(|_| {}); use diesel::result::DatabaseErrorKind; match save_result { Ok(_) => { - msg.react(&ctx, ReactionType::Unicode("👌".to_owned())).await?; + util::react(ctx, ReactionType::Unicode("👌".to_owned())).await?; }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::() { error!("tried to create meme that already exists"); - msg.react(&ctx, ReactionType::Unicode("❌".to_owned())).await?; - return util::send(ctx, msg.channel_id, "that meme already exists", msg.tts) - .map_err(CommandError::from) - .await; + + util::react(ctx, ReactionType::Unicode("❌".to_owned())).await?; + util::reply(ctx, "that meme already exists").await?; + + return Ok(()); } return Err(e.into()); diff --git a/src/commands/meme/delete.rs b/src/commands/meme/delete.rs index 6af1b6b..25ddf0d 100644 --- a/src/commands/meme/delete.rs +++ b/src/commands/meme/delete.rs @@ -3,42 +3,33 @@ use diesel::{ NotFound, }; use log::info; -use serenity::{ - all::ReactionType, - framework::standard::{ - macros::command, - Args, - CommandResult, - }, - model::channel::Message, - prelude::*, -}; +use serenity::all::ReactionType; use crate::{ db::{ connection, delete_meme, }, + msg, util, + PoiseContext, }; -#[command] -#[aliases("delmem")] -pub async fn delmeme(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let title = args.single_quoted::()?; - +#[poise::command(slash_command, prefix_command, guild_only, category = "memes", aliases("delmem"))] +pub async fn delmeme(ctx: PoiseContext<'_>, title: String) -> anyhow::Result<()> { let mut conn = connection().await?; - match delete_meme(&mut conn, &title, msg.author.id.get()).await { + match delete_meme(&mut conn, &title, ctx.author().id.get()).await { Ok(_) => { - msg.react(ctx, ReactionType::Unicode("💀".to_owned())).await?; + util::react(ctx, ReactionType::Unicode("💀".to_owned())).await?; Ok(()) }, Err(e) => { if let Some(NotFound) = e.downcast_ref::() { - msg.react(&ctx, ReactionType::Unicode("❓".to_owned())).await?; info!("attempted to delete nonexistent meme: '{}'", title); - util::send(ctx, msg.channel_id, "nice try", msg.tts).await?; + + util::react(ctx, ReactionType::Unicode("❓".to_owned())).await?; + util::reply(ctx, "nice try").await?; return Ok(()); } diff --git a/src/commands/meme/history.rs b/src/commands/meme/history.rs index edc75cd..cfd78df 100644 --- a/src/commands/meme/history.rs +++ b/src/commands/meme/history.rs @@ -1,4 +1,3 @@ -use anyhow::anyhow; use diesel::{ result::Error as DieselError, NotFound, @@ -11,18 +10,10 @@ use log::{ info, }; use serenity::{ - framework::standard::{ - macros::command, - Args, - CommandError, - CommandResult, - }, futures::{ StreamExt, - TryFutureExt, TryStreamExt, }, - model::channel::Message, prelude::*, }; use tap::Pipe; @@ -32,6 +23,7 @@ use timeago::{ }; use crate::{ + commands::game::get_user_id, db::{ self, connection, @@ -40,6 +32,7 @@ use crate::{ Metadata, }, util, + PoiseContext, CONFIG, }; @@ -55,9 +48,14 @@ lazy_static! { static CLEAN_DATE_FORMAT: &str = "%b %-e %Y"; -#[command] -#[aliases("what", "hwaet", "hwæt")] -pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "memes", + aliases("what", "hwaet", "hwæt") +)] +pub async fn wat(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let mut conn = connection().await?; let record = match InvocationRecord::last(&mut conn).await { @@ -65,12 +63,12 @@ pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Err(e) => { if let Some(NotFound) = e.downcast_ref::() { info!("found no memes in history"); - return util::send(ctx, msg.channel_id, "no one has ever memed before", msg.tts) - .map_err(CommandError::from) - .await; + + util::reply(ctx, "no one has ever memed before").await?; + return Ok(()); } - util::send(ctx, msg.channel_id, "BAD MEME BAD MEME", msg.tts).await?; + util::reply(ctx, "BAD MEME BAD MEME").await?; return Err(e.into()); }, }; @@ -82,44 +80,41 @@ pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { let metadata = Metadata::find(&mut conn, meme.metadata_id).await?; let author = CONFIG.discord.guild().member(&ctx, metadata.created_by as u64).await?; - util::send( + util::reply( ctx, - msg.channel_id, &format!( "that was \"{}\" by {} ({})", meme.title, author.mention(), metadata.created.date().format(CLEAN_DATE_FORMAT) ), - msg.tts, ) - .await? + .await?; }, 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 - .map_err(CommandError::from); + + util::reply(ctx, "heuueueeeeh?").await?; + return Ok(()); } - util::send(ctx, msg.channel_id, "do i look like i know what a jpeg is", msg.tts) - .await?; + util::reply(ctx, "do i look like i know what a jpeg is").await?; return Err(e.into()); }, }; - meme.map(|_| {}).map_err(CommandError::from) + let _meme = meme?; + Ok(()) } -#[command] -#[aliases("hist")] -pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let n = args.single_quoted::().unwrap_or(CONFIG.default_hist); - +#[poise::command(slash_command, prefix_command, guild_only, category = "memes", aliases("hist"))] +pub async fn history(ctx: PoiseContext<'_>, n: Option) -> anyhow::Result<()> { + let n = n.unwrap_or(CONFIG.default_hist); + if n > CONFIG.max_hist { debug!("user requested more than MAX_HIST ({}) items from history", CONFIG.max_hist); - util::send(ctx, msg.channel_id, "YER PUSHIN ME OVER THE FUCKIN LINE", true).await?; + util::reply(ctx, "YER PUSHIN ME OVER THE FUCKIN LINE").await?; } let n = n.min(CONFIG.max_hist); @@ -131,9 +126,9 @@ pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes if records.is_empty() { info!("no memes in history"); - return util::send(ctx, msg.channel_id, "i don't remember anything :(", msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "i don't remember anything :(").await?; + + return Ok(()); } info!("reporting meme history (len {})", n); @@ -198,19 +193,19 @@ pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes }, }; - Result::<_, CommandError>::Ok(result) + anyhow::Ok(result) }) .try_collect::>() .await?; let resp = resp.join("\n"); + util::reply(ctx, resp).await?; - util::send(ctx, msg.channel_id, &resp, false).await.map_err(CommandError::from) + Ok(()) } -#[command] -#[aliases("stat")] -pub async fn stats(ctx: &Context, msg: &Message, _: Args) -> CommandResult { +#[poise::command(slash_command, prefix_command, guild_only, category = "memes", aliases("stat"))] +pub async fn stats(ctx: PoiseContext<'_>) -> anyhow::Result<()> { use db; use serenity::model::{ id::UserId, @@ -277,11 +272,12 @@ and *{}* was the most-memed overall ({})"#, stats.most_popular_meme_overall_count, ); - util::send(ctx, msg.channel_id, s, msg.tts).map_err(CommandError::from).await + util::reply(ctx, s).await?; + Ok(()) } -#[command] -pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn memers(ctx: PoiseContext<'_>) -> anyhow::Result<()> { use serenity::model::id::UserId; let s = db::memers() @@ -302,25 +298,25 @@ pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult info.most_used_meme_count, ); - Result::<_, CommandError>::Ok(res) + anyhow::Ok(res) }) .try_collect::>() .await? .into_iter() .join("\n"); - util::send(ctx, msg.channel_id, &s, msg.tts).map_err(CommandError::from).await + util::reply(ctx, s).await?; + + Ok(()) } -#[command] -pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { +#[poise::command(prefix_command, guild_only, category = "memes")] +pub async fn query(ctx: PoiseContext<'_>, rest: util::RestVec) -> anyhow::Result<()> { use regex::Regex; use serenity::model::id::UserId; - use std::borrow::Borrow; use crate::{ db, - game::get_user_id, CONFIG, }; @@ -329,42 +325,31 @@ 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 creator: Option = { - let guild = - msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; - - let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; - - let creator = args.quoted().current().map(|s| CREATOR_REGEX.is_match(s)).unwrap_or(false); - if creator { - args.single_quoted::() - .ok() - .and_then(|s| { - CREATOR_REGEX.captures(&s).and_then(|c| c.get(1)).map(|x| x.as_str().to_owned()) - }) - .and_then(|s| get_user_id(guild.borrow(), s).ok().map(UserId::get)) - } else { - None - } + let mut rest = rest.into_inner(); + + let creator: Option = try { + let fst = rest.first()?; + let captures = CREATOR_REGEX.captures(fst)?; + let creator = captures.get(1)?.as_str().to_owned(); + + let guild = ctx.guild()?; + let user_id = get_user_id(&guild, creator).ok()?.get(); + rest.pop(); + + user_id }; - let order = { - let order = args.quoted().current().map(|s| AGE_REGEX.is_match(s)).unwrap_or(false); - - if order { - args.single_quoted::() - .ok() - .and_then(|s| { - AGE_REGEX.captures(&s).and_then(|c| c.get(1)).map(|x| x.as_str().to_owned()) - }) - .map(|s: String| s.contains("new")) - .unwrap_or(true) - } else { - true - } + let order: Option = try { + let fst = rest.first()?; + let captures = AGE_REGEX.captures(fst)?; + let order = captures.get(1)?.as_str().to_owned(); + + order }; - let iter = db::query_meme(args.rest(), creator, order).await?.into_iter(); + let order = order.is_some_and(|o| o.contains("new")); + + let iter = db::query_meme(rest.join(" "), creator, order).await?.into_iter(); let result = iter .pipe(serenity::futures::stream::iter) @@ -380,7 +365,7 @@ 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 + )) as anyhow::Result }) .try_collect::>() .await; @@ -401,10 +386,10 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul if result.is_empty() { info!("no memes matched query"); - return util::send(ctx, msg.channel_id, "no match".to_owned(), msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "no match").await?; + return Ok(()); } - util::send(ctx, msg.channel_id, &result, msg.tts).map_err(CommandError::from).await + util::reply(ctx, result).await?; + Ok(()) } diff --git a/src/commands/meme/invoke.rs b/src/commands/meme/invoke.rs index 1400452..e399e82 100644 --- a/src/commands/meme/invoke.rs +++ b/src/commands/meme/invoke.rs @@ -2,19 +2,7 @@ use diesel::{ result::Error as DieselError, NotFound, }; -use itertools::Itertools; use log::info; -use serenity::{ - framework::standard::{ - macros::command, - Args, - CommandError, - CommandResult, - }, - futures::TryFutureExt, - model::channel::Message, - prelude::*, -}; use crate::{ commands::meme::send_meme, @@ -25,42 +13,49 @@ use crate::{ InvocationRecord, }, util, + PoiseContext, }; -#[command] -#[aliases("mem")] -pub async fn meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Optional).await +#[poise::command(slash_command, prefix_command, guild_only, category = "memes", aliases("mem"))] +pub async fn meme(ctx: PoiseContext<'_>, #[rest] rest: String) -> anyhow::Result<()> { + _meme(ctx, rest, 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).await +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn omen(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + _meme(ctx, "", 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).await +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn silentomen(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + _meme(ctx, "", 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).await +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn audioomen(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + _meme(ctx, "", 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).await +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "memes", + aliases("audiomeme", "audiomem") +)] +pub async fn audio_meme(ctx: PoiseContext<'_>, #[rest] rest: String) -> anyhow::Result<()> { + _meme(ctx, rest, 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).await +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "memes", + aliases("silentmeme", "silentmem") +)] +pub async fn silent_meme(ctx: PoiseContext<'_>, #[rest] rest: String) -> anyhow::Result<()> { + _meme(ctx, rest, AudioPlayback::Prohibited).await } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -71,21 +66,20 @@ enum AudioPlayback { } async fn _meme( - ctx: &Context, - msg: &Message, - args: Args, + ctx: PoiseContext<'_>, + args: impl AsRef, audio_playback: AudioPlayback, -) -> CommandResult { +) -> anyhow::Result<()> { + let args = args.as_ref().trim(); + if args.is_empty() || audio_playback != AudioPlayback::Optional { - return rand_meme(ctx, msg, audio_playback).await; + return rand_meme(ctx, audio_playback).await; } - let search = args.raw().join(" "); - let mut conn = connection().await?; - let mem = match find_meme(&mut conn, search).await { + let mem = match find_meme(&mut conn, args).await { Ok(x) => { - InvocationRecord::create(&mut conn, msg.author.id.get(), msg.id.get(), x.id, false) + InvocationRecord::create(&mut conn, ctx.author().id.get(), ctx.id(), x.id, false) .await?; x @@ -93,27 +87,23 @@ async fn _meme( Err(e) => { return if let Some(NotFound) = e.downcast_ref::() { info!("requested meme not found in database"); - util::send(ctx, msg.channel_id, "c'mon baby, guesstimate", msg.tts) - .await - .map_err(CommandError::from) + + util::reply(ctx, "c'mon baby, guesstimate").await?; + Ok(()) } else { - util::send(ctx, msg.channel_id, "what in ryan's name", msg.tts).await?; + util::reply(ctx, "what in ryan's name").await?; Err(e.into()) }; }, }; - send_meme(ctx, &mem, &mut conn, msg).await + send_meme(ctx, &mem, &mut conn).await } -async fn rand_meme( - ctx: &Context, - message: &Message, - audio_playback: AudioPlayback, -) -> CommandResult { +async fn rand_meme(ctx: PoiseContext<'_>, audio_playback: AudioPlayback) -> anyhow::Result<()> { let mut conn = connection().await?; - let should_audio = util::users_listening(ctx).await?; + let should_audio = util::users_listening(ctx.serenity_context()).await?; let mem = match audio_playback { AudioPlayback::Required => db::rand_audio_meme(&mut conn).await, @@ -123,56 +113,53 @@ async fn rand_meme( match mem { Ok(mem) => { - InvocationRecord::create( - &mut conn, - message.author.id.get(), - message.id.get(), - mem.id, - true, - ) - .await?; - send_meme(ctx, &mem, &mut conn, message).await?; + InvocationRecord::create(&mut conn, ctx.author().id.get(), ctx.id(), mem.id, true) + .await?; + send_meme(ctx, &mem, &mut conn).await?; Ok(()) }, Err(e) => { if let Some(NotFound) = e.downcast_ref::() { info!("random meme not found"); - return util::send(ctx, message.channel_id, "i don't know any :(", message.tts) - .map_err(CommandError::from) - .await; + + util::reply(ctx, "i don't know any :(").await?; + return Ok(()); } - util::send(ctx, message.channel_id, "HELP", message.tts).await?; + util::reply(ctx, "HELP").await?; Err(e.into()) }, } } -#[command] -#[aliases("rarememe", "raremem")] -pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let should_audio = util::users_listening(ctx).await?; +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "memes", + aliases("raremem", "rarememe") +)] +pub async fn rare_meme(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let should_audio = util::users_listening(ctx.serenity_context()).await?; let mut conn = connection().await?; let meme = db::rare_meme(&mut conn, should_audio).await; match meme { Ok(meme) => { - InvocationRecord::create(&mut conn, msg.author.id.get(), msg.id.get(), meme.id, true) + InvocationRecord::create(&mut conn, ctx.author().id.get(), ctx.id(), meme.id, true) .await?; - send_meme(ctx, &meme, &mut conn, msg).await + send_meme(ctx, &meme, &mut conn).await }, Err(e) => { if let Some(NotFound) = e.downcast_ref::() { info!("rare meme not found"); - return util::send(ctx, msg.channel_id, "i don't know any :(", msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "i don't know any :(").await?; + + return Ok(()); } - util::send(ctx, msg.channel_id, "THE MEME MARKET IS IN FREEFALL", msg.tts) - .map_err(CommandError::from) - .await?; + util::reply(ctx, "THE MEME MARKET IS IN FREEFALL").await?; Err(e.into()) }, diff --git a/src/commands/meme/mod.rs b/src/commands/meme/mod.rs index cfe02ee..0108219 100644 --- a/src/commands/meme/mod.rs +++ b/src/commands/meme/mod.rs @@ -8,12 +8,6 @@ use serenity::{ CreateAttachment, CreateMessage, }, - framework::standard::{ - macros::group, - CommandResult, - }, - model::channel::Message, - prelude::*, }; use songbird::input::{ core::{ @@ -26,53 +20,54 @@ use songbird::input::{ Input, }; +pub use self::{ + create::*, + delete::*, + history::*, + invoke::*, +}; use crate::{ - commands::songbird, + commands::playback::songbird, db::{ Audio, Meme, }, + msg, + util, + PoiseContext, CONFIG, }; -pub use self::{ - create::*, - delete::*, - history::*, - invoke::*, -}; - mod create; mod delete; mod history; mod invoke; -#[group] -#[commands( - meme, - audio_meme, - silent_meme, - omen, - audioomen, - silentomen, - addmeme, - addaudiomeme, - delmeme, - wat, - stats, - history, - rare_meme, - memers, - query -)] -struct Memes; +pub fn commands() -> Vec> { + vec![ + meme(), + silent_meme(), + audio_meme(), + rare_meme(), + omen(), + silentomen(), + audioomen(), + addmeme(), + addaudiomeme(), + delmeme(), + history(), + stats(), + memers(), + wat(), + query(), + ] +} async fn send_meme( - ctx: &Context, + ctx: PoiseContext<'_>, t: &Meme, conn: &mut AsyncPgConnection, - msg: &Message, -) -> CommandResult { +) -> anyhow::Result<()> { let should_tts = t.content.as_ref().map(|t| !t.is_empty()).unwrap_or(false) && random::() % 25 == 0; @@ -95,12 +90,12 @@ async fn send_meme( let image = image?; let att = CreateAttachment::bytes(image.data.as_slice(), &image.filename); - msg.channel_id.send_files(ctx, vec![att], cmsg).await?; + ctx.channel_id().send_files(ctx, vec![att], cmsg).await?; }, None => { if t.content.is_some() { - msg.channel_id.send_message(ctx, cmsg).await?; + ctx.channel_id().send_message(ctx, cmsg).await?; } }, }; @@ -108,7 +103,7 @@ async fn send_meme( if let Some(audio) = audio { let audio = audio?; - let (_sb, call) = songbird(ctx, msg).await?; + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { @@ -117,7 +112,7 @@ async fn send_meme( call.enqueue_input(Input::Lazy(Box::new(audio))).await; - msg.react(ctx, ReactionType::Unicode("📣".to_owned())).await?; + util::react(ctx, ReactionType::Unicode("📣".to_owned())).await?; } Ok(()) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 69c9185..ba87adb 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,18 +1,14 @@ -use crate::util; -use log::info; -use serenity::framework::{ - standard::macros::group, - StandardFramework, -}; +use poise::builtins::PrettyHelpConfiguration; -#[cfg(feature = "db")] -pub use self::meme::*; -pub use self::{ - playback::*, - roll::ROLL_COMMAND, - today::TODAY_COMMAND, +use crate::{ + commands::playback::_play, + util, + PoiseContext, }; +#[cfg(feature = "games")] +pub mod game; + #[cfg(feature = "db")] pub(crate) mod meme; pub(crate) mod playback; @@ -20,40 +16,52 @@ pub(crate) mod roll; pub(crate) mod sound_levels; pub(crate) mod today; -mod help; +#[cfg(feature = "db")] +pub use self::meme::*; -#[group] -#[only_in(guild)] -#[commands(roll, today)] -struct General; +pub fn commands() -> Vec> { + let mut commands = vec![ + playback::play(), + playback::pause(), + playback::resume(), + playback::die(), + playback::list(), + sound_levels::mute(), + sound_levels::unmute(), + roll::roll(), + help(), + ]; -pub fn register_commands(f: StandardFramework) -> StandardFramework { - let result = f.group(&PLAYBACK_GROUP).group(&GENERAL_GROUP); + #[cfg(feature = "games")] + commands.extend(game::commands()); #[cfg(feature = "db")] - let result = result.group(&MEMES_GROUP); + commands.extend(meme::commands()); - #[cfg(feature = "games")] - let result = result.group(&crate::game::GAME_GROUP); - - result.help(&help::HELP).unrecognised_command(|ctx, msg, unrec| { - Box::pin(async move { - let url = match msg.content.split_whitespace().nth(1) { - 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).await; - }) - }) + commands +} + +#[poise::command(slash_command, prefix_command, aliases("halp"))] +pub async fn help(ctx: PoiseContext<'_>, command: Option) -> anyhow::Result<()> { + poise::builtins::pretty_help( + ctx, + command.as_ref().map(|x| x.as_str()), + PrettyHelpConfiguration { + ..Default::default() + }, + ) + .await?; + + Ok(()) +} + +pub async fn unrecognized(ctx: PoiseContext<'_>, u: url::Url) -> anyhow::Result<()> { + if !u.scheme().starts_with("http") { + util::reply(ctx, "format your commands right. fuck you.").await?; + return Ok(()); + } + + let _ = _play(ctx, &u).await?; + + Ok(()) } diff --git a/src/commands/playback.rs b/src/commands/playback.rs index 1c3ab95..98ae613 100644 --- a/src/commands/playback.rs +++ b/src/commands/playback.rs @@ -1,78 +1,46 @@ +use std::sync::Arc; + use log::{ debug, - error, info, warn, }; -use serenity::{ - framework::standard::{ - macros::{ - command, - group, - }, - Args, - CommandError, - CommandResult, - }, - model::channel::Message, - prelude::*, -}; +use serenity::prelude::*; use songbird::{ input::YoutubeDl, Call, Songbird, }; -use std::sync::Arc; -use tap::Conv; use crate::{ bot::HttpKey, - commands::sound_levels::*, util, + PoiseContext, CONFIG, }; -#[group] -#[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 { +pub async fn songbird(ctx: PoiseContext<'_>) -> anyhow::Result<(Arc, Arc>)> { + let Some(gid) = ctx.guild_id() else { return Err(anyhow::anyhow!("no guild id").into()); }; - let sb = songbird::get(ctx).await.expect("acquiring songbird handle"); + let sb = songbird::get(ctx.serenity_context()).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, - Url, - }; +pub async fn _play(ctx: PoiseContext<'_>, url: &url::Url) -> anyhow::Result<()> { + use url::Host; debug!("playing '{}'", url); - if !url.starts_with("http") { + if !url.scheme().starts_with("http") { warn!("got bad url argument to play: {}", url); - util::send(ctx, msg.channel_id, "bAD LiNk", msg.tts).await?; + + util::reply(ctx, "bAD LiNk").await?; return Ok(()); } - let url = match Url::parse(url) { - Err(e) => { - error!("bad url: {}", e); - util::send(ctx, msg.channel_id, "INVALID URL", msg.tts).await?; - return Ok(()); - }, - Ok(u) => u, - }; - let host = url.host().and_then(|u| match u { Host::Domain(h) => Some(h.to_owned()), _ => None, @@ -81,16 +49,16 @@ pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { if host.map(|h| h.to_lowercase().contains("imgur")).unwrap_or(false) { info!("detected imgur link"); - if msg.author.id.get() == 106160362109272064 { - util::send(ctx, msg.channel_id, "fuck you conway", true).await?; + if ctx.author().id == 106160362109272064 { + util::reply(ctx, "fuck you conway").await?; } else { - util::send(ctx, msg.channel_id, "IMGUR IS BAD, YOU TRASH CAN MAN", msg.tts).await?; + util::reply(ctx, "IMGUR IS BAD, YOU TRASH CAN MAN").await?; } return Ok(()); } - let (_sb, call) = songbird(ctx, msg).await?; + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { @@ -98,38 +66,31 @@ pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { } let client = { - let data = ctx.data.read().await; + let data = ctx.serenity_context().data.read().await; data.get::().unwrap().clone() }; - let input = YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), url.conv::()); + let input = YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), url.to_string()); call.enqueue_input(input.into()).await; Ok(()) } -#[command] -pub async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - if args.is_empty() { - return _resume(ctx, msg).await; - } - - let url = match args.single::() { - Ok(url) => url, - Err(e) => { - error!("unable to parse url from args: {}", e); - return util::send(ctx, msg.channel_id, "BAD LINK", msg.tts) - .await - .map_err(CommandError::from); - }, +#[poise::command(slash_command, prefix_command, guild_only, category = "playback")] +pub async fn play( + ctx: PoiseContext<'_>, + #[description = "link to play (if absent, resumes playback)"] u: Option, +) -> anyhow::Result<()> { + let Some(u) = u else { + return _resume(ctx).await; }; - _play(ctx, msg, &url).await + _play(ctx, &u).await } -#[command] -pub async fn pause(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command(slash_command, prefix_command, guild_only, category = "playback")] +pub async fn pause(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().pause()?; @@ -137,14 +98,19 @@ pub async fn pause(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Ok(()) } -#[command] -#[aliases("continue")] -pub async fn resume(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - _resume(ctx, msg).await +#[poise::command( + slash_command, + prefix_command, + guild_only, + aliases("continue"), + category = "playback" +)] +pub async fn resume(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + _resume(ctx).await } -async fn _resume(ctx: &Context, msg: &Message) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +async fn _resume(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().resume()?; @@ -152,10 +118,9 @@ async fn _resume(ctx: &Context, msg: &Message) -> CommandResult { Ok(()) } -#[command] -#[aliases("next")] -pub async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command(slash_command, prefix_command, guild_only, category = "playback", aliases("next"))] +pub async fn skip(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().skip()?; @@ -163,10 +128,15 @@ pub async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { Ok(()) } -#[command] -#[aliases("sudoku", "fuckoff", "stop")] -pub async fn die(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "playback", + aliases("sudoku", "fuckoff", "stop") +)] +pub async fn die(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; call.queue().stop(); @@ -176,21 +146,26 @@ pub async fn die(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Ok(()) } -#[command] -#[aliases("queue")] -pub async fn list(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "playback", + aliases("queue") +)] +pub async fn list(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; let queue = call.queue(); - util::send(ctx, msg.channel_id, "(command fix work-in-progress)", msg.tts).await?; + util::reply(ctx, "(command fix work-in-progress)").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?; + let fmt = format!("track playing for {:?}", info.play_time); + util::reply(ctx, fmt).await?; } Ok(()) diff --git a/src/commands/roll.rs b/src/commands/roll.rs index 45e3ba8..6cd084c 100644 --- a/src/commands/roll.rs +++ b/src/commands/roll.rs @@ -1,31 +1,18 @@ use std::result::Result as StdResult; +use lazy_static::lazy_static; use log::{ debug, error, }; use rand::prelude::*; -use serenity::{ - framework::standard::{ - macros::command, - Args, - }, - model::channel::Message, - prelude::*, -}; use thiserror::Error; -use lazy_static::lazy_static; -use serenity::{ - framework::standard::{ - CommandError, - CommandResult, - }, - futures::TryFutureExt, +use crate::{ + util, + PoiseContext, }; -use crate::util; - #[derive(pest_derive::Parser)] #[grammar = "commands/calc.pest"] struct Calc; @@ -49,27 +36,26 @@ impl Calc { Pair, Pairs, }, - prec_climber::PrecClimber, + pratt_parser::PrattParser, Parser, }; use self::Rule::*; lazy_static! { - static ref CLIMBER: PrecClimber = { - use pest::prec_climber::{ + static ref CLIMBER: PrattParser = { + use pest::pratt_parser::{ Assoc::*, - Operator, + Op as Operator, }; - PrecClimber::new(vec![ - Operator::new(add, Left) - | Operator::new(sub, Left) - | Operator::new(modulo, Left), - Operator::new(mul, Left) | Operator::new(div, Left), - Operator::new(dice, Left), - Operator::new(pow, Right), - ]) + PrattParser::new() + .op(Operator::infix(add, Left) + | Operator::infix(sub, Left) + | Operator::infix(modulo, Left)) + .op(Operator::infix(mul, Left) | Operator::infix(div, Left)) + .op(Operator::infix(dice, Left)) + .op(Operator::infix(pow, Right)) }; } @@ -160,29 +146,33 @@ impl Calc { } fn eval_expr(p: Pairs) -> StdResult { - CLIMBER.climb(p, eval_single_pair, |lhs, op, rhs| { - let lhs = lhs?; - let rhs = rhs?; - - let result = match op.as_rule() { - add => lhs + rhs, - sub => lhs - rhs, - mul => lhs * rhs, - div => lhs / rhs, - pow => lhs.powf(rhs), - dice => { - let dice_count = lhs as usize; - let dice_faces = rhs as usize; - - let mut rng = thread_rng(); - (0..dice_count).map(|_| rng.gen_range(1..(dice_faces + 1))).sum::() - as f64 - }, - _ => unreachable!(), - }; + CLIMBER + .map_primary(eval_single_pair) + .map_infix(|lhs, op, rhs| { + let lhs = lhs?; + let rhs = rhs?; + + let result = match op.as_rule() { + add => lhs + rhs, + sub => lhs - rhs, + mul => lhs * rhs, + div => lhs / rhs, + pow => lhs.powf(rhs), + dice => { + let dice_count = lhs as usize; + let dice_faces = rhs as usize; + + let mut rng = thread_rng(); + (0..dice_count) + .map(|_| rng.gen_range(1..(dice_faces + 1))) + .sum::() as f64 + }, + _ => unreachable!(), + }; - Ok(result) - }) + Ok(result) + }) + .parse(p) } eval_expr(result) @@ -212,21 +202,18 @@ mod test { } } -#[command] -#[aliases("calc", "calculate")] -pub async fn roll(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - match Calc::eval(args.rest()) { +#[poise::command(slash_command, prefix_command, guild_only, aliases("calc", "calculate"))] +pub async fn roll(ctx: PoiseContext<'_>, #[rest] rest: String) -> anyhow::Result<()> { + match Calc::eval(&rest) { Ok(result) => { debug!("got calc result '{}'", result); - util::send(ctx, msg.channel_id, &format!("{}", result), msg.tts) - .map_err(CommandError::from) - .await + util::reply(ctx, result.to_string()).await?; }, Err(e) => { - error!("error encountered reading calc '{}': {}", args.rest(), e); - util::send(ctx, msg.channel_id, "I COULDN'T READ THAT YOU FUCK", msg.tts) - .map_err(CommandError::from) - .await + error!("error encountered reading calc '{}': {}", rest, e); + util::reply(ctx, "I COULDN'T READ THAT YOU FUCK").await?; }, } + + Ok(()) } diff --git a/src/commands/sound_levels.rs b/src/commands/sound_levels.rs index 8c75b37..9a6cfc6 100644 --- a/src/commands/sound_levels.rs +++ b/src/commands/sound_levels.rs @@ -1,21 +1,14 @@ -use serenity::{ - framework::standard::{ - macros::command, - Args, - CommandResult, - }, - model::channel::Message, - prelude::*, +use crate::{ + commands::playback::songbird, + PoiseContext, }; -use crate::commands::songbird; - pub const DEFAULT_VOLUME: f32 = 0.20; const MAX_VOLUME: f32 = 5.0; -#[command] -pub async fn mute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command(slash_command, prefix_command, guild_only)] +pub async fn mute(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; call.mute(true).await?; @@ -23,66 +16,12 @@ pub async fn mute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Ok(()) } -#[command] -pub async fn unmute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command(slash_command, prefix_command, guild_only)] +pub async fn unmute(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).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?; -// -// Ok(()) -// } diff --git a/src/commands/today/mod.rs b/src/commands/today/mod.rs index 7f1dca7..c1a02d5 100644 --- a/src/commands/today/mod.rs +++ b/src/commands/today/mod.rs @@ -5,22 +5,14 @@ use rand::{ seq::SliceRandom, thread_rng, }; -use serenity::{ - framework::standard::{ - macros::command, - Args, - CommandResult, - }, - model::channel::Message, - prelude::*, -}; use songbird::input::YoutubeDl; use tap::Conv; use crate::{ bot::HttpKey, - commands::songbird, + commands::playback::songbird, util, + PoiseContext, CONFIG, }; @@ -66,16 +58,16 @@ lazy_static! { ]; } -#[command] -pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { +#[poise::command(slash_command, prefix_command, guild_only)] +pub async fn today(ctx: PoiseContext<'_>, #[rest] _rest: String) -> anyhow::Result<()> { let today = { #[allow(unused_mut)] let mut result = chrono::Local::now().naive_local(); #[cfg(debug_assertions)] { - let dt = _args.parse::().or_else(|_| { - _args.parse::().map(|date| { + let dt = _rest.parse::().or_else(|_| { + _rest.parse::().map(|date| { let time = chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(); date.and_time(time) }) @@ -83,11 +75,11 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { match dt { Ok(dt) => { - log::debug!("overriding with datetime: {}", dt); + debug!("overriding with datetime: {dt}"); result = dt; }, Err(e) => { - log::debug!("parsing datetime: {:?}", e); + debug!("parsing datetime: {e:?}"); }, }; } @@ -102,7 +94,7 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let play_args = options.choose(&mut thread_rng()); if let Some(play_args) = play_args { - let (_sb, call) = songbird(ctx, msg).await?; + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { @@ -110,7 +102,7 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { } let client = { - let data = ctx.data.read().await; + let data = ctx.serenity_context().data.read().await; data.get::().unwrap().clone() }; @@ -130,8 +122,8 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { }); q.resume()?; } else { - util::send(ctx, msg.channel_id, "no", false).await?; - util::send(ctx, msg.channel_id, ":angry:", false).await?; + util::reply(ctx, "no").await?; + util::reply(ctx, ":angry:").await?; } Ok(()) -- cgit v1.3.1