pub(crate) mod pager; use std::io::Cursor; use base64::{engine::general_purpose, Engine}; use chrono_tz::Tz; use log::warn; use poise::{ serenity_prelude as serenity, serenity_prelude::{ builder::CreateEmbed, ComponentInteraction, ComponentInteractionDataKind, Context, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage, }, }; use rmp_serde::Serializer; use serde::{Deserialize, Serialize}; use crate::{ commands::{ command_macro::list_macro::{max_macro_page, show_macro_page}, delete::{max_delete_page, show_delete_page}, todo::{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::reply_to_interaction_response_message, 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(); general_purpose::STANDARD.encode(buf) } pub fn from_custom_id(data: &String) -> Self { let buf = general_purpose::STANDARD .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: &ComponentInteraction) { match self { ComponentDataModel::LookPager(pager) => { let flags = pager.flags; let channel_id = { let channel_opt = component.channel_id.to_channel_cached(&ctx.cache); if let Some(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 = channel_id.to_channel_cached(&ctx.cache).map(|channel| channel.name.clone()); 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 }) .take_while(|p| { char_count += p.len(); char_count < EMBED_DESCRIPTION_MAX_LENGTH }) .collect::>() .join(""); let embed = CreateEmbed::default() .title(format!( "Reminders{}", channel_name.map_or(String::new(), |n| format!(" on #{}", n)) )) .description(display) .footer(CreateEmbedFooter::new(format!("Page {} of {}", next_page + 1, pages))) .color(*THEME_COLOR); let _ = component .create_response( &ctx, CreateInteractionResponse::UpdateMessage( CreateInteractionResponseMessage::new() .embed(embed) .components(vec![pager.create_button_row(pages)]), ), ) .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_response( &ctx, CreateInteractionResponse::UpdateMessage( reply_to_interaction_response_message(resp), ), ) .await; } ComponentDataModel::DelSelector(selector) => { if let ComponentInteractionDataKind::StringSelect { values } = &component.data.kind { let placeholder = vec!["?"; values.len()].join(","); let sql = format!( "UPDATE reminders SET `status` = 'deleted' WHERE id IN ({placeholder})" ); let mut query = sqlx::query(&sql); for id in values { query = query.bind(id); } query.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_response( &ctx, CreateInteractionResponse::UpdateMessage( reply_to_interaction_response_message(resp), ), ) .await; } } ComponentDataModel::TodoPager(pager) => { if Some(component.user.id.get()) == 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 WHERE user_id = ? ", 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_response( &ctx, CreateInteractionResponse::UpdateMessage( reply_to_interaction_response_message(resp), ), ) .await; } else { let _ = component .create_response( &ctx, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new() .ephemeral(true) .content("Only the user who performed the command can use these components") ) ) .await; } } ComponentDataModel::TodoSelector(selector) => { if Some(component.user.id.get()) == selector.user_id || selector.user_id.is_none() { if let ComponentInteractionDataKind::StringSelect { values } = &component.data.kind { let placeholder = vec!["?"; values.len()].join(","); let sql = format!("DELETE FROM todos WHERE id IN ({placeholder})"); let mut query = sqlx::query(&sql); for id in values { query = query.bind(id); } query.execute(&data.database).await.unwrap(); let values = if let Some(uid) = selector.user_id { sqlx::query!( " SELECT todos.id, value FROM todos WHERE user_id = ? ", uid, ) .fetch_all(&data.database) .await .unwrap() .iter() .map(|row| (row.id as usize, row.value.clone())) .collect::>() } else if let Some(cid) = selector.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 = ? ", 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_response( &ctx, CreateInteractionResponse::UpdateMessage( reply_to_interaction_response_message(resp), ), ) .await; } } else { let _ = component .create_response( &ctx, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new() .ephemeral(true) .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_response( &ctx, CreateInteractionResponse::UpdateMessage( reply_to_interaction_response_message(resp), ), ) .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_response( &ctx, CreateInteractionResponse::UpdateMessage( CreateInteractionResponseMessage::new().embed( CreateEmbed::new() .title("Reminder Canceled") .description("This reminder has been canceled.") .color(*THEME_COLOR), ), ), ) .await; } Err(e) => { warn!("Error canceling reminder: {:?}", e); let _ = component .create_response( &ctx, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new().content( "An error occurred trying to cancel this reminder.", ).ephemeral(true), ), ) .await; } } } else { let _ = component .create_response( &ctx, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new().content( "The reminder could not be canceled. It may have already been deleted.", ).ephemeral(true), ), ) .await; } } else { let _ = component .create_response( &ctx, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new() .content("Only the user who performed the command can use these components") .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, }