pub(crate) mod pager; use std::io::Cursor; use chrono_tz::Tz; use log::warn; use poise::{ serenity::{ builder::CreateEmbed, client::Context, model::{ application::interaction::{ message_component::MessageComponentInteraction, InteractionResponseType, MessageFlags, }, channel::Channel, }, }, serenity_prelude as serenity, }; use rmp_serde::Serializer; use serde::{Deserialize, Serialize}; use crate::{ commands::{ moderation_cmds::{max_macro_page, show_macro_page}, reminder_cmds::{max_delete_page, show_delete_page}, todo_cmds::{max_todo_page, show_todo_page}, }, component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager}, consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, models::reminder::Reminder, utils::send_as_initial_response, Data, }; #[derive(Deserialize, Serialize)] #[serde(tag = "type")] #[repr(u8)] pub enum ComponentDataModel { LookPager(LookPager), DelPager(DelPager), TodoPager(TodoPager), DelSelector(DelSelector), TodoSelector(TodoSelector), MacroPager(MacroPager), UndoReminder(UndoReminder), } impl ComponentDataModel { pub fn to_custom_id(&self) -> String { let mut buf = Vec::new(); self.serialize(&mut Serializer::new(&mut buf)).unwrap(); base64::encode(buf) } pub fn from_custom_id(data: &String) -> Self { let buf = base64::decode(data) .map_err(|e| format!("Could not decode `custom_id' {}: {:?}", data, e)) .unwrap(); let cur = Cursor::new(buf); rmp_serde::from_read(cur).unwrap() } pub async fn act(&self, ctx: &Context, data: &Data, component: &MessageComponentInteraction) { match self { ComponentDataModel::LookPager(pager) => { let flags = pager.flags; let channel_opt = component.channel_id.to_channel_cached(&ctx); let channel_id = if let Some(Channel::Guild(channel)) = channel_opt { if Some(channel.guild_id) == component.guild_id { flags.channel_id.unwrap_or(component.channel_id) } else { component.channel_id } } else { component.channel_id }; let reminders = Reminder::from_channel(&data.database, channel_id, &flags).await; let pages = reminders .iter() .map(|reminder| reminder.display(&flags, &pager.timezone)) .fold(0, |t, r| t + r.len()) .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH); let channel_name = if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) { Some(channel.name) } else { None }; let next_page = pager.next_page(pages); let mut char_count = 0; let mut skip_char_count = 0; let display = reminders .iter() .map(|reminder| reminder.display(&flags, &pager.timezone)) .skip_while(|p| { skip_char_count += p.len(); skip_char_count < EMBED_DESCRIPTION_MAX_LENGTH * next_page as usize }) .take_while(|p| { char_count += p.len(); char_count < EMBED_DESCRIPTION_MAX_LENGTH }) .collect::>() .join("\n"); let mut embed = CreateEmbed::default(); embed .title(format!( "Reminders{}", channel_name.map_or(String::new(), |n| format!(" on #{}", n)) )) .description(display) .footer(|f| f.text(format!("Page {} of {}", next_page + 1, pages))) .color(*THEME_COLOR); let _ = component .create_interaction_response(&ctx, |r| { r.kind(InteractionResponseType::UpdateMessage).interaction_response_data( |response| { response.set_embeds(vec![embed]).components(|comp| { pager.create_button_row(pages, comp); comp }) }, ) }) .await; } ComponentDataModel::DelPager(pager) => { let reminders = Reminder::from_guild( &ctx, &data.database, component.guild_id, component.user.id, ) .await; let max_pages = max_delete_page(&reminders, &pager.timezone); let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone); let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::UpdateMessage).interaction_response_data( |d| { send_as_initial_response(resp, d); d }, ) }) .await; } ComponentDataModel::DelSelector(selector) => { let selected_id = component.data.values.join(","); sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id) .execute(&data.database) .await .unwrap(); let reminders = Reminder::from_guild( &ctx, &data.database, component.guild_id, component.user.id, ) .await; let resp = show_delete_page(&reminders, selector.page, selector.timezone); let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::UpdateMessage).interaction_response_data( |d| { send_as_initial_response(resp, d); d }, ) }) .await; } ComponentDataModel::TodoPager(pager) => { if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() { let values = if let Some(uid) = pager.user_id { sqlx::query!( "SELECT todos.id, value FROM todos INNER JOIN users ON todos.user_id = users.id WHERE users.user = ?", uid, ) .fetch_all(&data.database) .await .unwrap() .iter() .map(|row| (row.id as usize, row.value.clone())) .collect::>() } else if let Some(cid) = pager.channel_id { sqlx::query!( "SELECT todos.id, value FROM todos INNER JOIN channels ON todos.channel_id = channels.id WHERE channels.channel = ?", cid, ) .fetch_all(&data.database) .await .unwrap() .iter() .map(|row| (row.id as usize, row.value.clone())) .collect::>() } else { sqlx::query!( "SELECT todos.id, value FROM todos INNER JOIN guilds ON todos.guild_id = guilds.id WHERE guilds.guild = ?", pager.guild_id, ) .fetch_all(&data.database) .await .unwrap() .iter() .map(|row| (row.id as usize, row.value.clone())) .collect::>() }; let max_pages = max_todo_page(&values); let resp = show_todo_page( &values, pager.next_page(max_pages), pager.user_id, pager.channel_id, pager.guild_id, ); let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::UpdateMessage) .interaction_response_data(|d| { send_as_initial_response(resp, d); d }) }) .await; } else { let _ = component .create_interaction_response(&ctx, |r| { r.kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|d| { d.flags( MessageFlags::EPHEMERAL, ) .content("Only the user who performed the command can use these components") }) }) .await; } } ComponentDataModel::TodoSelector(selector) => { if Some(component.user.id.0) == selector.user_id || selector.user_id.is_none() { let selected_id = component.data.values.join(","); sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id) .execute(&data.database) .await .unwrap(); let values = sqlx::query!( // fucking braindead mysql use <=> instead of = for null comparison "SELECT id, value FROM todos WHERE user_id <=> ? AND channel_id <=> ? AND guild_id <=> ?", selector.user_id, selector.channel_id, selector.guild_id, ) .fetch_all(&data.database) .await .unwrap() .iter() .map(|row| (row.id as usize, row.value.clone())) .collect::>(); let resp = show_todo_page( &values, selector.page, selector.user_id, selector.channel_id, selector.guild_id, ); let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::UpdateMessage) .interaction_response_data(|d| { send_as_initial_response(resp, d); d }) }) .await; } else { let _ = component .create_interaction_response(&ctx, |r| { r.kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|d| { d.flags( MessageFlags::EPHEMERAL, ) .content("Only the user who performed the command can use these components") }) }) .await; } } ComponentDataModel::MacroPager(pager) => { let macros = data.command_macros(component.guild_id.unwrap()).await.unwrap(); let max_page = max_macro_page(¯os); let page = pager.next_page(max_page); let resp = show_macro_page(¯os, page); let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::UpdateMessage).interaction_response_data( |d| { send_as_initial_response(resp, d); d }, ) }) .await; } ComponentDataModel::UndoReminder(undo_reminder) => { if component.user.id == undo_reminder.user_id { let reminder = Reminder::from_id(&data.database, undo_reminder.reminder_id).await; if let Some(reminder) = reminder { match reminder.delete(&data.database).await { Ok(()) => { let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::UpdateMessage) .interaction_response_data(|d| { d.embed(|e| { e.title("Reminder Canceled") .description( "This reminder has been canceled.", ) .color(*THEME_COLOR) }) .components(|c| c) }) }) .await; } Err(e) => { warn!("Error canceling reminder: {:?}", e); let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|d| { d.content( "The reminder could not be canceled: it may have already been deleted. Check `/del`!") .ephemeral(true) }) }) .await; } } } else { let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|d| { d.content( "The reminder could not be canceled: it may have already been deleted. Check `/del`!") .ephemeral(true) }) }) .await; } } else { let _ = component .create_interaction_response(&ctx, |f| { f.kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|d| { d.content( "Only the user who performed the command can use this button.") .ephemeral(true) }) }) .await; } } } } } #[derive(Serialize, Deserialize)] pub struct DelSelector { pub page: usize, pub timezone: Tz, } #[derive(Serialize, Deserialize)] pub struct TodoSelector { pub page: usize, pub user_id: Option, pub channel_id: Option, pub guild_id: Option, } #[derive(Serialize, Deserialize)] pub struct UndoReminder { pub user_id: serenity::UserId, pub reminder_id: u32, }