use anyhow::anyhow; use diesel::{ result::Error as DieselError, NotFound, }; use itertools::Itertools; use lazy_static::lazy_static; use log::{ debug, error, info, }; 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 crate::{ db::{ self, connection, InvocationRecord, Meme, Metadata, }, util, CONFIG, }; lazy_static! { static ref TIME_FORMATTER: Formatter = { let mut f = Formatter::new(); f.min_unit(TimeUnit::Minutes); f.num_items(2); f }; } static CLEAN_DATE_FORMAT: &str = "%b %-e %Y"; #[command] #[aliases("what", "hwaet", "hwæt")] pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { let mut conn = connection()?; let record = match InvocationRecord::last(&mut conn).await { Ok(x) => x, 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::send(ctx, msg.channel_id, "BAD MEME BAD MEME", msg.tts).await?; return Err(e.into()); }, }; let meme = Meme::find(&mut conn, record.meme_id).await; match meme { Ok(ref meme) => { 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( ctx, msg.channel_id, &format!( "that was \"{}\" by {} ({})", meme.title, author.mention(), metadata.created.date().format(CLEAN_DATE_FORMAT) ), msg.tts, ) .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::send(ctx, msg.channel_id, "do i look like i know what a jpeg is", msg.tts) .await?; return Err(e.into()); }, }; meme.map(|_| {}).map_err(CommandError::from) } #[command] pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let n = args.single_quoted::().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?; } let n = n.min(CONFIG.max_hist); let records = { let mut conn = connection()?; InvocationRecord::last_n(&mut conn, n).await? }; 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; } info!("reporting meme history (len {})", n); let resp = serenity::futures::stream::iter(records.into_iter().enumerate().rev()) .then(|(i, rec)| async move { let mut conn = connection()?; 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(&mut conn, rec.meme_id).await; .and_then(|meme| { Metadata::find(&mut conn, meme.metadata_id).map(|metadata| (metadata, meme)) }); let invoker_name = CONFIG .discord .guild() .member(&ctx, rec.user_id as u64) .await .map(|m| m.display_name().to_owned()) .unwrap_or("???".to_owned()); let result = match meme { Ok((metadata, meme)) => { let author_name = CONFIG .discord .guild() .member(&ctx, metadata.created_by as u64) .await .map(|m| m.display_name().to_owned()) .unwrap_or("???".to_owned()); format!( "{}. [{}{}] \"{}\" by {} ({}). invoked by {}.", i + 1, rand, ago, meme.title, author_name, metadata.created.date().format(CLEAN_DATE_FORMAT), invoker_name ) }, Err(e) => { if let Some(variant) = e.downcast_ref::() { if *variant != NotFound { error!("error encountered loading meme history: {}", e); } } format!("{}. [{}{}] not found. invoked by {}.", i + 1, rand, ago, invoker_name) }, }; Result::<_, CommandError>::Ok(result) }) .try_collect::>() .await?; let resp = resp.join("\n"); util::send(ctx, msg.channel_id, &resp, false).await.map_err(CommandError::from) } #[command] #[aliases("stat")] pub async fn stats(ctx: &Context, msg: &Message, _: Args) -> CommandResult { use db; use serenity::model::{ id::UserId, user::User, }; let mut conn = connection()?; let stats = db::stats(&mut conn).await?; debug!("reporting stats"); 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()).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#" **{}** memes stored **{}** memes with audio ({:0.1}%) **{}** memes with images ({:0.1}%) started recording meme invocations on *{}* ({}) **{}** total meme invocations recorded **{}** of which were random ({:0.1}%) and **{}** were audio ({:0.1}%) the most active day was *{}* with **{}** memes and the loudest day was *{}* with **{}** audio memes **{}** has invoked the most random memes ({}) **{}** has invoked the most memes by name ({}) *{}* was the meme specifically requested the most ({}) *{}* was the meme randomly invoked the most ({}) and *{}* was the most-memed overall ({})"#, stats.memes_overall, stats.audio_memes, (stats.audio_memes as f64) / (stats.memes_overall as f64) * 100., stats.image_memes, (stats.image_memes as f64) / (stats.memes_overall as f64) * 100., stats.started_recording.date().format(CLEAN_DATE_FORMAT), TIME_FORMATTER.convert((chrono::Utc::now() - stats.started_recording).to_std().unwrap()), stats.total_meme_invocations, stats.random_meme_invocations, (stats.random_meme_invocations as f64) / (stats.total_meme_invocations as f64) * 100., stats.audio_meme_invocations, (stats.audio_meme_invocations as f64) / (stats.total_meme_invocations as f64) * 100., stats.most_active_day.format(CLEAN_DATE_FORMAT), stats.most_active_day_count, stats.most_audio_active_day.format(CLEAN_DATE_FORMAT), stats.most_audio_active_count, rand_user, stats.most_random_meme_user_count, direct_user, stats.most_directly_named_meme_count, stats.most_popular_named_meme, stats.most_popular_named_meme_count, stats.most_popular_random_meme, stats.most_popular_random_meme_count, stats.most_popular_meme_overall, stats.most_popular_meme_overall_count, ); util::send(ctx, msg.channel_id, s, msg.tts).map_err(CommandError::from).await } #[command] pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { use serenity::model::id::UserId; let s = db::memers()? .into_iter() .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: *{}* ({})", username, info.random_memes + info.specific_memes, info.random_memes, info.specific_memes, info.most_used_meme, info.most_used_meme_count, ); Result::<_, CommandError>::Ok(res) }) .try_collect::>() .await? .into_iter() .join("\n"); util::send(ctx, msg.channel_id, &s, msg.tts).map_err(CommandError::from).await } #[command] pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { use regex::Regex; use serenity::model::id::UserId; use std::borrow::Borrow; use crate::{ db, game::get_user_id, CONFIG, }; lazy_static! { static ref CREATOR_REGEX: Regex = Regex::new(r"(?i)(?:by|creator)=(.*)").unwrap(); 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 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 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: **{}**", meme.title, username, metadata.created.date().format(CLEAN_DATE_FORMAT), 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 }) .try_collect::>() .await; let result = result? .into_iter() .scan(0, |state, line| { *state = *state + line.len() + 1; if *state < 2000 { Some(line) } else { None } }) .join("\n"); 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::send(ctx, msg.channel_id, &result, msg.tts).map_err(CommandError::from).await }