use base64; use chrono::Utc; use rocket::{ http::CookieJar, serde::json::{json, Json, Value as JsonValue}, State, }; use serde::Serialize; use serenity::{ client::Context, model::{ channel::GuildChannel, id::{ChannelId, GuildId}, }, }; use sqlx::{MySql, Pool}; use crate::{ check_guild_subscription, check_subscription, consts::{ DAY, DISCORD_CDN, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, }, routes::dashboard::{ create_database_channel, generate_uid, name_default, DeleteReminder, PatchReminder, Reminder, }, }; #[derive(Serialize)] struct ChannelInfo { id: String, name: String, webhook_avatar: Option, webhook_name: Option, } #[get("/api/guild//channels")] pub async fn get_guild_channels( id: u64, cookies: &CookieJar<'_>, ctx: &State, pool: &State>, ) -> JsonValue { check_authorization!(cookies, ctx.inner(), id); match GuildId(id).to_guild_cached(ctx.inner()) { Some(guild) => { let mut channel_info = vec![]; let mut channels = guild .channels .iter() .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) .filter(|(_, channel)| channel.is_text_based()) .collect::>(); channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); for (channel_id, channel) in channels { let mut ch = ChannelInfo { name: channel.name.to_string(), id: channel_id.to_string(), webhook_avatar: None, webhook_name: None, }; if let Ok(webhook_details) = sqlx::query!( "SELECT webhook_id, webhook_token FROM channels WHERE channel = ?", channel.id.as_u64() ) .fetch_one(pool.inner()) .await { if let (Some(webhook_id), Some(webhook_token)) = (webhook_details.webhook_id, webhook_details.webhook_token) { let webhook_res = ctx.http.get_webhook_with_token(webhook_id, &webhook_token).await; if let Ok(webhook) = webhook_res { ch.webhook_avatar = webhook.avatar.map(|a| { format!("{}/{}/{}.webp?size=128", DISCORD_CDN, webhook_id, a) }); ch.webhook_name = webhook.name; } } } channel_info.push(ch); } json!(channel_info) } None => { json!({"error": "Bot not in guild"}) } } } #[derive(Serialize)] struct RoleInfo { id: String, name: String, } #[get("/api/guild//roles")] pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State) -> JsonValue { check_authorization!(cookies, ctx.inner(), id); let roles_res = ctx.cache.guild_roles(id); match roles_res { Some(roles) => { let roles = roles .iter() .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() }) .collect::>(); json!(roles) } None => { warn!("Could not fetch roles from {}", id); json!({"error": "Could not get roles"}) } } } #[post("/api/guild//reminders", data = "")] pub async fn create_reminder( id: u64, reminder: Json, cookies: &CookieJar<'_>, serenity_context: &State, pool: &State>, ) -> JsonValue { check_authorization!(cookies, serenity_context.inner(), id); let user_id = cookies.get_private("userid").map(|c| c.value().parse::().ok()).flatten().unwrap(); // validate channel let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner()); let channel_exists = channel.is_some(); let channel_matches_guild = channel.map_or(false, |c| c.guild().map_or(false, |c| c.guild_id.0 == id)); if !channel_matches_guild || !channel_exists { warn!( "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})", reminder.channel, id, channel_exists ); return json!({"error": "Channel not found"}); } let channel = create_database_channel( serenity_context.inner(), ChannelId(reminder.channel), pool.inner(), ) .await; if let Err(e) = channel { warn!("`create_database_channel` returned an error code: {:?}", e); return json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}); } let channel = channel.unwrap(); // validate lengths check_length!(MAX_CONTENT_LENGTH, reminder.content); check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder.embed_description); check_length!(MAX_EMBED_TITLE_LENGTH, reminder.embed_title); check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder.embed_author); check_length!(MAX_EMBED_FOOTER_LENGTH, reminder.embed_footer); check_length_opt!(MAX_EMBED_FIELDS, reminder.embed_fields); if let Some(fields) = &reminder.embed_fields { for field in &fields.0 { check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value); check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title); } } check_length_opt!(MAX_USERNAME_LENGTH, reminder.username); check_length_opt!( MAX_URL_LENGTH, reminder.embed_footer_url, reminder.embed_thumbnail_url, reminder.embed_author_url, reminder.embed_image_url, reminder.avatar ); // validate urls check_url_opt!( reminder.embed_footer_url, reminder.embed_thumbnail_url, reminder.embed_author_url, reminder.embed_image_url, reminder.avatar ); // validate time and interval if reminder.utc_time < Utc::now().naive_utc() { return json!({"error": "Time must be in the future"}); } if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() { if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32 + reminder.interval_seconds.unwrap_or(0) < *MIN_INTERVAL { return json!({"error": "Interval too short"}); } } // check patreon if necessary if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() { if !check_guild_subscription(serenity_context.inner(), GuildId(id)).await && !check_subscription(serenity_context.inner(), user_id).await { return json!({"error": "Patreon is required to set intervals"}); } } // base64 decode error dropped here let attachment_data = reminder.attachment.as_ref().map(|s| base64::decode(s).ok()).flatten(); let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; let new_uid = generate_uid(); // write to db match sqlx::query!( "INSERT INTO reminders ( uid, attachment, attachment_name, channel_id, avatar, content, embed_author, embed_author_url, embed_color, embed_description, embed_footer, embed_footer_url, embed_image_url, embed_thumbnail_url, embed_title, embed_fields, enabled, expires, interval_seconds, interval_months, name, pin, restartable, tts, username, `utc_time` ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new_uid, attachment_data, reminder.attachment_name, channel, reminder.avatar, reminder.content, reminder.embed_author, reminder.embed_author_url, reminder.embed_color, reminder.embed_description, reminder.embed_footer, reminder.embed_footer_url, reminder.embed_image_url, reminder.embed_thumbnail_url, reminder.embed_title, reminder.embed_fields, reminder.enabled, reminder.expires, reminder.interval_seconds, reminder.interval_months, name, reminder.pin, reminder.restartable, reminder.tts, reminder.username, reminder.utc_time, ) .execute(pool.inner()) .await { Ok(_) => sqlx::query_as_unchecked!( Reminder, "SELECT reminders.attachment, reminders.attachment_name, reminders.avatar, channels.channel, reminders.content, reminders.embed_author, reminders.embed_author_url, reminders.embed_color, reminders.embed_description, reminders.embed_footer, reminders.embed_footer_url, reminders.embed_image_url, reminders.embed_thumbnail_url, reminders.embed_title, reminders.embed_fields, reminders.enabled, reminders.expires, reminders.interval_seconds, reminders.interval_months, reminders.name, reminders.pin, reminders.restartable, reminders.tts, reminders.uid, reminders.username, reminders.utc_time FROM reminders LEFT JOIN channels ON channels.id = reminders.channel_id WHERE uid = ?", new_uid ) .fetch_one(pool.inner()) .await .map(|r| json!(r)) .unwrap_or_else(|e| { warn!("Failed to complete SQL query: {:?}", e); json!({"error": "Could not load reminder"}) }), Err(e) => { warn!("Error in `create_reminder`: Could not execute query: {:?}", e); json!({"error": "Unknown error"}) } } } #[get("/api/guild//reminders")] pub async fn get_reminders(id: u64, ctx: &State, pool: &State>) -> JsonValue { let channels_res = GuildId(id).channels(&ctx.inner()).await; match channels_res { Ok(channels) => { let channels = channels .keys() .into_iter() .map(|k| k.as_u64().to_string()) .collect::>() .join(","); sqlx::query_as_unchecked!( Reminder, "SELECT reminders.attachment, reminders.attachment_name, reminders.avatar, channels.channel, reminders.content, reminders.embed_author, reminders.embed_author_url, reminders.embed_color, reminders.embed_description, reminders.embed_footer, reminders.embed_footer_url, reminders.embed_image_url, reminders.embed_thumbnail_url, reminders.embed_title, reminders.embed_fields, reminders.enabled, reminders.expires, reminders.interval_seconds, reminders.interval_months, reminders.name, reminders.pin, reminders.restartable, reminders.tts, reminders.uid, reminders.username, reminders.utc_time FROM reminders LEFT JOIN channels ON channels.id = reminders.channel_id WHERE FIND_IN_SET(channels.channel, ?)", channels ) .fetch_all(pool.inner()) .await .map(|r| json!(r)) .unwrap_or_else(|e| { warn!("Failed to complete SQL query: {:?}", e); json!({"error": "Could not load reminders"}) }) } Err(e) => { warn!("Could not fetch channels from {}: {:?}", id, e); json!([]) } } } #[patch("/api/guild//reminders", data = "")] pub async fn edit_reminder( id: u64, reminder: Json, serenity_context: &State, pool: &State>, ) -> JsonValue { let mut error = vec![]; update_field!(pool.inner(), error, reminder.[ attachment, attachment_name, avatar, content, embed_author, embed_author_url, embed_color, embed_description, embed_footer, embed_footer_url, embed_image_url, embed_thumbnail_url, embed_title, embed_fields, enabled, expires, interval_seconds, interval_months, name, pin, restartable, tts, username, utc_time ]); if reminder.channel > 0 { let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner()); match channel { Some(channel) => { let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); if !channel_matches_guild { warn!( "Error in `edit_reminder`: channel {:?} not found for guild {}", reminder.channel, id ); return json!({"error": "Channel not found"}); } let channel = create_database_channel( serenity_context.inner(), ChannelId(reminder.channel), pool.inner(), ) .await; if let Err(e) = channel { warn!("`create_database_channel` returned an error code: {:?}", e); return json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}); } let channel = channel.unwrap(); match sqlx::query!( "UPDATE reminders SET channel_id = ? WHERE uid = ?", channel, reminder.uid ) .execute(pool.inner()) .await { Ok(_) => {} Err(e) => { warn!("Error setting channel: {:?}", e); error.push("Couldn't set channel".to_string()) } } } None => { warn!( "Error in `edit_reminder`: channel {:?} not found for guild {}", reminder.channel, id ); return json!({"error": "Channel not found"}); } } } match sqlx::query_as_unchecked!( Reminder, "SELECT reminders.attachment, reminders.attachment_name, reminders.avatar, channels.channel, reminders.content, reminders.embed_author, reminders.embed_author_url, reminders.embed_color, reminders.embed_description, reminders.embed_footer, reminders.embed_footer_url, reminders.embed_image_url, reminders.embed_thumbnail_url, reminders.embed_title, reminders.embed_fields, reminders.enabled, reminders.expires, reminders.interval_seconds, reminders.interval_months, reminders.name, reminders.pin, reminders.restartable, reminders.tts, reminders.uid, reminders.username, reminders.utc_time FROM reminders LEFT JOIN channels ON channels.id = reminders.channel_id WHERE uid = ?", reminder.uid ) .fetch_one(pool.inner()) .await { Ok(reminder) => json!({"reminder": reminder, "errors": error}), Err(e) => { warn!("Error exiting `edit_reminder': {:?}", e); json!({"reminder": Option::::None, "errors": vec!["Unknown error"]}) } } } #[delete("/api/guild//reminders", data = "")] pub async fn delete_reminder( id: u64, reminder: Json, pool: &State>, ) -> JsonValue { match sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid) .execute(pool.inner()) .await { Ok(_) => { json!({}) } Err(e) => { warn!("Error in `delete_reminder`: {:?}", e); json!({"error": "Could not delete reminder"}) } } }