460 lines
18 KiB
Rust
460 lines
18 KiB
Rust
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::<Vec<String>>()
|
|
.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::<Vec<(usize, String)>>()
|
|
} 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::<Vec<(usize, String)>>()
|
|
} 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::<Vec<(usize, String)>>()
|
|
};
|
|
|
|
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::<Vec<(usize, String)>>()
|
|
} 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::<Vec<(usize, String)>>()
|
|
} 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::<Vec<(usize, String)>>()
|
|
};
|
|
|
|
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<u64>,
|
|
pub channel_id: Option<u64>,
|
|
pub guild_id: Option<u64>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct UndoReminder {
|
|
pub user_id: serenity::UserId,
|
|
pub reminder_id: u32,
|
|
}
|