Changed return types to results

This commit is contained in:
jude 2022-07-24 20:06:37 +01:00
parent e19af54caf
commit 79c86d43f2
5 changed files with 378 additions and 298 deletions

View File

@ -146,7 +146,7 @@ pub async fn initialize(
routes::dashboard::guild::get_reminder_templates, routes::dashboard::guild::get_reminder_templates,
routes::dashboard::guild::create_reminder_template, routes::dashboard::guild::create_reminder_template,
routes::dashboard::guild::delete_reminder_template, routes::dashboard::guild::delete_reminder_template,
routes::dashboard::guild::create_reminder, routes::dashboard::guild::create_guild_reminder,
routes::dashboard::guild::get_reminders, routes::dashboard::guild::get_reminders,
routes::dashboard::guild::edit_reminder, routes::dashboard::guild::edit_reminder,
routes::dashboard::guild::delete_reminder, routes::dashboard::guild::delete_reminder,

View File

@ -1,7 +1,7 @@
macro_rules! check_length { macro_rules! check_length {
($max:ident, $field:expr) => { ($max:ident, $field:expr) => {
if $field.len() > $max { if $field.len() > $max {
return json!({ "error": format!("{} exceeded", stringify!($max)) }); return Err(json!({ "error": format!("{} exceeded", stringify!($max)) }));
} }
}; };
($max:ident, $field:expr, $($fields:expr),+) => { ($max:ident, $field:expr, $($fields:expr),+) => {
@ -25,7 +25,7 @@ macro_rules! check_length_opt {
macro_rules! check_url { macro_rules! check_url {
($field:expr) => { ($field:expr) => {
if !($field.starts_with("http://") || $field.starts_with("https://")) { if !($field.starts_with("http://") || $field.starts_with("https://")) {
return json!({ "error": "URL invalid" }); return Err(json!({ "error": "URL invalid" }));
} }
}; };
($field:expr, $($fields:expr),+) => { ($field:expr, $($fields:expr),+) => {
@ -60,7 +60,7 @@ macro_rules! check_authorization {
match member { match member {
Err(_) => { Err(_) => {
return json!({"error": "User not in guild"}) return Err(json!({"error": "User not in guild"}));
} }
Ok(_) => {} Ok(_) => {}
@ -68,13 +68,13 @@ macro_rules! check_authorization {
} }
None => { None => {
return json!({"error": "Bot not in guild"}) return Err(json!({"error": "Bot not in guild"}));
} }
} }
} }
None => { None => {
return json!({"error": "User not authorized"}); return Err(json!({"error": "User not authorized"}));
} }
} }
} }
@ -117,3 +117,9 @@ macro_rules! update_field {
update_field!($pool, $error, $reminder.[$($fields),+]); update_field!($pool, $error, $reminder.[$($fields),+]);
}; };
} }
macro_rules! json_err {
($message:expr) => {
Err(json!({ "error": $message }))
};
}

View File

@ -1,7 +1,7 @@
use csv::{QuoteStyle, WriterBuilder}; use csv::{QuoteStyle, WriterBuilder};
use rocket::{ use rocket::{
http::CookieJar, http::CookieJar,
serde::json::{json, Json, Value as JsonValue}, serde::json::{json, serde_json, Json},
State, State,
}; };
use serenity::{ use serenity::{
@ -10,7 +10,10 @@ use serenity::{
}; };
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use crate::routes::dashboard::{ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv}; use crate::routes::dashboard::{
create_reminder, generate_uid, ImportBody, JsonResult, Reminder, ReminderCsv,
ReminderTemplateCsv, TodoCsv,
};
#[get("/api/guild/<id>/export/reminders")] #[get("/api/guild/<id>/export/reminders")]
pub async fn export_reminders( pub async fn export_reminders(
@ -18,7 +21,7 @@ pub async fn export_reminders(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
@ -40,7 +43,7 @@ pub async fn export_reminders(
reminders.attachment, reminders.attachment,
reminders.attachment_name, reminders.attachment_name,
reminders.avatar, reminders.avatar,
channels.channel, CONCAT('#', channels.channel) AS channel,
reminders.content, reminders.content,
reminders.embed_author, reminders.embed_author,
reminders.embed_author_url, reminders.embed_author_url,
@ -77,21 +80,19 @@ pub async fn export_reminders(
match csv_writer.into_inner() { match csv_writer.into_inner() {
Ok(inner) => match String::from_utf8(inner) { Ok(inner) => match String::from_utf8(inner) {
Ok(encoded) => { Ok(encoded) => Ok(json!({ "body": encoded })),
json!({ "body": encoded })
}
Err(e) => { Err(e) => {
warn!("Failed to write UTF-8: {:?}", e); warn!("Failed to write UTF-8: {:?}", e);
json!({"error": "Failed to write UTF-8"}) Err(json!({"error": "Failed to write UTF-8"}))
} }
}, },
Err(e) => { Err(e) => {
warn!("Failed to extract CSV: {:?}", e); warn!("Failed to extract CSV: {:?}", e);
json!({"error": "Failed to extract CSV"}) Err(json!({"error": "Failed to extract CSV"}))
} }
} }
} }
@ -99,7 +100,7 @@ pub async fn export_reminders(
Err(e) => { Err(e) => {
warn!("Failed to complete SQL query: {:?}", e); warn!("Failed to complete SQL query: {:?}", e);
json!({"error": "Failed to query reminders"}) Err(json!({"error": "Failed to query reminders"}))
} }
} }
} }
@ -107,7 +108,7 @@ pub async fn export_reminders(
Err(e) => { Err(e) => {
warn!("Could not fetch channels from {}: {:?}", id, e); warn!("Could not fetch channels from {}: {:?}", id, e);
json!({"error": "Failed to get guild channels"}) Err(json!({"error": "Failed to get guild channels"}))
} }
} }
} }
@ -119,28 +120,86 @@ pub async fn import_reminders(
body: Json<ImportBody>, body: Json<ImportBody>,
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
let user_id =
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
match base64::decode(&body.body) { match base64::decode(&body.body) {
Ok(body) => { Ok(body) => {
let mut reader = csv::Reader::from_reader(body.as_slice()); let mut reader = csv::Reader::from_reader(body.as_slice());
for result in reader.deserialize::<ReminderCsv>() { for result in reader.deserialize::<ReminderCsv>() {
match result { match result {
Ok(record) => {} Ok(record) => {
let channel_id = record.channel.split_at(1).1;
Err(e) => { match channel_id.parse::<u64>() {
warn!("Couldn't deserialize CSV row: {:?}", e); Ok(channel_id) => {
} let reminder = Reminder {
} attachment: record.attachment,
} attachment_name: record.attachment_name,
avatar: record.avatar,
channel: channel_id,
content: record.content,
embed_author: record.embed_author,
embed_author_url: record.embed_author_url,
embed_color: record.embed_color,
embed_description: record.embed_description,
embed_footer: record.embed_footer,
embed_footer_url: record.embed_footer_url,
embed_image_url: record.embed_image_url,
embed_thumbnail_url: record.embed_thumbnail_url,
embed_title: record.embed_title,
embed_fields: record
.embed_fields
.map(|s| serde_json::from_str(&s).ok())
.flatten(),
enabled: record.enabled,
expires: record.expires,
interval_seconds: record.interval_seconds,
interval_months: record.interval_months,
name: record.name,
restartable: record.restartable,
tts: record.tts,
uid: generate_uid(),
username: record.username,
utc_time: record.utc_time,
};
json!({"error": "Not implemented"}) create_reminder(
ctx.inner(),
pool.inner(),
GuildId(id),
UserId(user_id),
reminder,
)
.await?;
} }
Err(_) => { Err(_) => {
json!({"error": "Malformed base64"}) return json_err!(format!(
"Failed to parse channel {}",
channel_id
));
}
}
}
Err(e) => {
warn!("Couldn't deserialize CSV row: {:?}", e);
return json_err!("Deserialize error. Aborted");
}
}
}
Ok(json!({}))
}
Err(_) => {
json_err!("Malformed base64")
} }
} }
} }
@ -151,7 +210,7 @@ pub async fn export_todos(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
@ -174,28 +233,27 @@ pub async fn export_todos(
match csv_writer.into_inner() { match csv_writer.into_inner() {
Ok(inner) => match String::from_utf8(inner) { Ok(inner) => match String::from_utf8(inner) {
Ok(encoded) => { Ok(encoded) => Ok(json!({ "body": encoded })),
json!({ "body": encoded })
}
Err(e) => { Err(e) => {
warn!("Failed to write UTF-8: {:?}", e); warn!("Failed to write UTF-8: {:?}", e);
json!({"error": "Failed to write UTF-8"}) json_err!("Failed to write UTF-8")
} }
}, },
Err(e) => { Err(e) => {
warn!("Failed to extract CSV: {:?}", e); warn!("Failed to extract CSV: {:?}", e);
json!({"error": "Failed to extract CSV"}) json_err!("Failed to extract CSV")
} }
} }
} }
Err(e) => { Err(e) => {
warn!("Could not fetch templates from {}: {:?}", id, e); warn!("Could not fetch templates from {}: {:?}", id, e);
json!({"error": "Failed to query templates"}) json_err!("Failed to query templates")
} }
} }
} }
@ -207,7 +265,7 @@ pub async fn import_todos(
body: Json<ImportBody>, body: Json<ImportBody>,
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
let channels_res = GuildId(id).channels(&ctx.inner()).await; let channels_res = GuildId(id).channels(&ctx.inner()).await;
@ -231,17 +289,18 @@ pub async fn import_todos(
if channels.contains_key(&ChannelId(channel_id)) { if channels.contains_key(&ChannelId(channel_id)) {
query_params.push((record.value, Some(channel_id), id)); query_params.push((record.value, Some(channel_id), id));
} else { } else {
return json!({ return json_err!(format!(
"error": "Invalid channel ID {}",
format!("Invalid channel ID {}", channel_id) channel_id
}); ));
} }
} }
Err(_) => { Err(_) => {
return json!({ return json_err!(format!(
"error": format!("Invalid channel ID {}", channel_id) "Invalid channel ID {}",
}); channel_id
));
} }
} }
} }
@ -254,7 +313,7 @@ pub async fn import_todos(
Err(e) => { Err(e) => {
warn!("Couldn't deserialize CSV row: {:?}", e); warn!("Couldn't deserialize CSV row: {:?}", e);
return json!({"error": "Deserialize error. Aborted"}); return json_err!("Deserialize error. Aborted");
} }
} }
} }
@ -279,27 +338,25 @@ pub async fn import_todos(
let res = query.execute(pool.inner()).await; let res = query.execute(pool.inner()).await;
match res { match res {
Ok(_) => { Ok(_) => Ok(json!({})),
json!({})
}
Err(e) => { Err(e) => {
warn!("Couldn't execute todo query: {:?}", e); warn!("Couldn't execute todo query: {:?}", e);
json!({"error": "An unexpected error occured."}) json_err!("An unexpected error occured.")
} }
} }
} }
Err(_) => { Err(_) => {
json!({"error": "Malformed base64"}) json_err!("Malformed base64")
} }
}, },
Err(e) => { Err(e) => {
warn!("Couldn't fetch channels for guild {}: {:?}", id, e); warn!("Couldn't fetch channels for guild {}: {:?}", id, e);
json!({"error": "Couldn't fetch channels."}) json_err!("Couldn't fetch channels.")
} }
} }
} }
@ -310,7 +367,7 @@ pub async fn export_reminder_templates(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
@ -348,28 +405,26 @@ pub async fn export_reminder_templates(
match csv_writer.into_inner() { match csv_writer.into_inner() {
Ok(inner) => match String::from_utf8(inner) { Ok(inner) => match String::from_utf8(inner) {
Ok(encoded) => { Ok(encoded) => Ok(json!({ "body": encoded })),
json!({ "body": encoded })
}
Err(e) => { Err(e) => {
warn!("Failed to write UTF-8: {:?}", e); warn!("Failed to write UTF-8: {:?}", e);
json!({"error": "Failed to write UTF-8"}) json_err!("Failed to write UTF-8")
} }
}, },
Err(e) => { Err(e) => {
warn!("Failed to extract CSV: {:?}", e); warn!("Failed to extract CSV: {:?}", e);
json!({"error": "Failed to extract CSV"}) json_err!("Failed to extract CSV")
} }
} }
} }
Err(e) => { Err(e) => {
warn!("Could not fetch templates from {}: {:?}", id, e); warn!("Could not fetch templates from {}: {:?}", id, e);
json!({"error": "Failed to query templates"}) json_err!("Failed to query templates")
} }
} }
} }

View File

@ -1,10 +1,8 @@
use std::env; use std::env;
use base64;
use chrono::Utc;
use rocket::{ use rocket::{
http::CookieJar, http::CookieJar,
serde::json::{json, Json, Value as JsonValue}, serde::json::{json, Json},
State, State,
}; };
use serde::Serialize; use serde::Serialize;
@ -18,16 +16,14 @@ use serenity::{
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use crate::{ use crate::{
check_guild_subscription, check_subscription,
consts::{ consts::{
DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, 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_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, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH,
MIN_INTERVAL,
}, },
routes::dashboard::{ routes::dashboard::{
create_database_channel, generate_uid, name_default, template_name_default, DeleteReminder, create_database_channel, create_reminder, template_name_default, DeleteReminder,
DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate, DeleteReminderTemplate, JsonResult, PatchReminder, Reminder, ReminderTemplate,
}, },
}; };
@ -44,7 +40,7 @@ pub async fn get_guild_patreon(
id: u64, id: u64,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
match GuildId(id).to_guild_cached(ctx.inner()) { match GuildId(id).to_guild_cached(ctx.inner()) {
@ -59,12 +55,10 @@ pub async fn get_guild_patreon(
.contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
}); });
json!({ "patreon": patreon }) Ok(json!({ "patreon": patreon }))
} }
None => { None => json_err!("Bot not in guild"),
json!({"error": "Bot not in guild"})
}
} }
} }
@ -73,7 +67,7 @@ pub async fn get_guild_channels(
id: u64, id: u64,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
match GuildId(id).to_guild_cached(ctx.inner()) { match GuildId(id).to_guild_cached(ctx.inner()) {
@ -97,12 +91,10 @@ pub async fn get_guild_channels(
}) })
.collect::<Vec<ChannelInfo>>(); .collect::<Vec<ChannelInfo>>();
json!(channel_info) Ok(json!(channel_info))
} }
None => { None => json_err!("Bot not in guild"),
json!({"error": "Bot not in guild"})
}
} }
} }
@ -113,7 +105,7 @@ struct RoleInfo {
} }
#[get("/api/guild/<id>/roles")] #[get("/api/guild/<id>/roles")]
pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonValue { pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
let roles_res = ctx.cache.guild_roles(id); let roles_res = ctx.cache.guild_roles(id);
@ -125,12 +117,12 @@ pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Conte
.map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() }) .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
.collect::<Vec<RoleInfo>>(); .collect::<Vec<RoleInfo>>();
json!(roles) Ok(json!(roles))
} }
None => { None => {
warn!("Could not fetch roles from {}", id); warn!("Could not fetch roles from {}", id);
json!({"error": "Could not get roles"}) json_err!("Could not get roles")
} }
} }
} }
@ -141,7 +133,7 @@ pub async fn get_reminder_templates(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
match sqlx::query_as_unchecked!( match sqlx::query_as_unchecked!(
@ -152,13 +144,11 @@ pub async fn get_reminder_templates(
.fetch_all(pool.inner()) .fetch_all(pool.inner())
.await .await
{ {
Ok(templates) => { Ok(templates) => Ok(json!(templates)),
json!(templates)
}
Err(e) => { Err(e) => {
warn!("Could not fetch templates from {}: {:?}", id, e); warn!("Could not fetch templates from {}: {:?}", id, e);
json!({"error": "Could not get templates"}) json_err!("Could not get templates")
} }
} }
} }
@ -170,7 +160,7 @@ pub async fn create_reminder_template(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
// validate lengths // validate lengths
@ -254,12 +244,12 @@ pub async fn create_reminder_template(
.await .await
{ {
Ok(_) => { Ok(_) => {
json!({}) Ok(json!({}))
} }
Err(e) => { Err(e) => {
warn!("Could not fetch templates from {}: {:?}", id, e); warn!("Could not fetch templates from {}: {:?}", id, e);
json!({"error": "Could not get templates"}) json_err!("Could not get templates")
} }
} }
} }
@ -271,7 +261,7 @@ pub async fn delete_reminder_template(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization!(cookies, ctx.inner(), id);
match sqlx::query!( match sqlx::query!(
@ -282,230 +272,41 @@ pub async fn delete_reminder_template(
.await .await
{ {
Ok(_) => { Ok(_) => {
json!({}) Ok(json!({}))
} }
Err(e) => { Err(e) => {
warn!("Could not delete template from {}: {:?}", id, e); warn!("Could not delete template from {}: {:?}", id, e);
json!({"error": "Could not delete template"}) json_err!("Could not delete template")
} }
} }
} }
#[post("/api/guild/<id>/reminders", data = "<reminder>")] #[post("/api/guild/<id>/reminders", data = "<reminder>")]
pub async fn create_reminder( pub async fn create_guild_reminder(
id: u64, id: u64,
reminder: Json<Reminder>, reminder: Json<Reminder>,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
serenity_context: &State<Context>, serenity_context: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
check_authorization!(cookies, serenity_context.inner(), id); check_authorization!(cookies, serenity_context.inner(), id);
let user_id = let user_id =
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
// validate channel create_reminder(
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(), serenity_context.inner(),
ChannelId(reminder.channel),
pool.inner(), pool.inner(),
GuildId(id),
UserId(user_id),
reminder.into_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,
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.restartable,
reminder.tts,
reminder.username,
reminder.utc_time,
)
.execute(pool.inner())
.await .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.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/<id>/reminders")] #[get("/api/guild/<id>/reminders")]
pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySql>>) -> JsonValue { pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySql>>) -> JsonResult {
let channels_res = GuildId(id).channels(&ctx.inner()).await; let channels_res = GuildId(id).channels(&ctx.inner()).await;
match channels_res { match channels_res {
@ -552,17 +353,17 @@ pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySq
) )
.fetch_all(pool.inner()) .fetch_all(pool.inner())
.await .await
.map(|r| json!(r)) .map(|r| Ok(json!(r)))
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
warn!("Failed to complete SQL query: {:?}", e); warn!("Failed to complete SQL query: {:?}", e);
json!({"error": "Could not load reminders"}) json_err!("Could not load reminders")
}) })
} }
Err(e) => { Err(e) => {
warn!("Could not fetch channels from {}: {:?}", id, e); warn!("Could not fetch channels from {}: {:?}", id, e);
json!([]) Ok(json!([]))
} }
} }
} }
@ -573,7 +374,7 @@ pub async fn edit_reminder(
reminder: Json<PatchReminder>, reminder: Json<PatchReminder>,
serenity_context: &State<Context>, serenity_context: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
let mut error = vec![]; let mut error = vec![];
update_field!(pool.inner(), error, reminder.[ update_field!(pool.inner(), error, reminder.[
@ -614,7 +415,7 @@ pub async fn edit_reminder(
reminder.channel, id reminder.channel, id
); );
return json!({"error": "Channel not found"}); return Err(json!({"error": "Channel not found"}));
} }
let channel = create_database_channel( let channel = create_database_channel(
@ -627,7 +428,9 @@ pub async fn edit_reminder(
if let Err(e) = channel { if let Err(e) = channel {
warn!("`create_database_channel` returned an error code: {:?}", e); warn!("`create_database_channel` returned an error code: {:?}", e);
return json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}); return Err(
json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
);
} }
let channel = channel.unwrap(); let channel = channel.unwrap();
@ -655,7 +458,7 @@ pub async fn edit_reminder(
reminder.channel, id reminder.channel, id
); );
return json!({"error": "Channel not found"}); return Err(json!({"error": "Channel not found"}));
} }
} }
} }
@ -695,12 +498,12 @@ pub async fn edit_reminder(
.fetch_one(pool.inner()) .fetch_one(pool.inner())
.await .await
{ {
Ok(reminder) => json!({"reminder": reminder, "errors": error}), Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})),
Err(e) => { Err(e) => {
warn!("Error exiting `edit_reminder': {:?}", e); warn!("Error exiting `edit_reminder': {:?}", e);
json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]}) Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]}))
} }
} }
} }
@ -709,19 +512,17 @@ pub async fn edit_reminder(
pub async fn delete_reminder( pub async fn delete_reminder(
reminder: Json<DeleteReminder>, reminder: Json<DeleteReminder>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonValue { ) -> JsonResult {
match sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid) match sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid)
.execute(pool.inner()) .execute(pool.inner())
.await .await
{ {
Ok(_) => { Ok(_) => Ok(json!({})),
json!({})
}
Err(e) => { Err(e) => {
warn!("Error in `delete_reminder`: {:?}", e); warn!("Error in `delete_reminder`: {:?}", e);
json!({"error": "Could not delete reminder"}) Err(json!({"error": "Could not delete reminder"}))
} }
} }
} }

View File

@ -1,15 +1,29 @@
use std::collections::HashMap; use std::collections::HashMap;
use chrono::naive::NaiveDateTime; use chrono::{naive::NaiveDateTime, Utc};
use rand::{rngs::OsRng, seq::IteratorRandom}; use rand::{rngs::OsRng, seq::IteratorRandom};
use rocket::{http::CookieJar, response::Redirect}; use rocket::{
http::CookieJar,
response::Redirect,
serde::json::{json, Value as JsonValue},
};
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serenity::{http::Http, model::id::ChannelId}; use serenity::{
use sqlx::{types::Json, Executor}; client::Context,
http::Http,
model::id::{ChannelId, GuildId, UserId},
};
use sqlx::{types::Json, Executor, MySql, Pool};
use crate::{ use crate::{
consts::{CHARACTERS, DEFAULT_AVATAR}, check_guild_subscription, check_subscription,
consts::{
CHARACTERS, DAY, DEFAULT_AVATAR, 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,
},
Database, Error, Database, Error,
}; };
@ -17,6 +31,7 @@ pub mod export;
pub mod guild; pub mod guild;
pub mod user; pub mod user;
pub type JsonResult = Result<JsonValue, JsonValue>;
type Unset<T> = Option<T>; type Unset<T> = Option<T>;
fn name_default() -> String { fn name_default() -> String {
@ -134,7 +149,7 @@ pub struct ReminderCsv {
attachment: Option<Vec<u8>>, attachment: Option<Vec<u8>>,
attachment_name: Option<String>, attachment_name: Option<String>,
avatar: Option<String>, avatar: Option<String>,
channel: u64, channel: String,
content: String, content: String,
embed_author: String, embed_author: String,
embed_author_url: Option<String>, embed_author_url: Option<String>,
@ -284,6 +299,209 @@ pub struct TodoCsv {
channel_id: Option<String>, channel_id: Option<String>,
} }
pub async fn create_reminder(
ctx: &Context,
pool: &Pool<MySql>,
guild_id: GuildId,
user_id: UserId,
reminder: Reminder,
) -> JsonResult {
// validate channel
let channel = ChannelId(reminder.channel).to_channel_cached(&ctx);
let channel_exists = channel.is_some();
let channel_matches_guild =
channel.map_or(false, |c| c.guild().map_or(false, |c| c.guild_id == guild_id));
if !channel_matches_guild || !channel_exists {
warn!(
"Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})",
reminder.channel, guild_id, channel_exists
);
return Err(json!({"error": "Channel not found"}));
}
let channel = create_database_channel(&ctx, ChannelId(reminder.channel), pool).await;
if let Err(e) = channel {
warn!("`create_database_channel` returned an error code: {:?}", e);
return Err(
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 Err(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 Err(json!({"error": "Interval too short"}));
}
}
// check patreon if necessary
if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() {
if !check_guild_subscription(&ctx, guild_id).await
&& !check_subscription(&ctx, user_id).await
{
return Err(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,
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.restartable,
reminder.tts,
reminder.username,
reminder.utc_time,
)
.execute(pool)
.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.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)
.await
.map(|r| Ok(json!(r)))
.unwrap_or_else(|e| {
warn!("Failed to complete SQL query: {:?}", e);
Err(json!({"error": "Could not load reminder"}))
}),
Err(e) => {
warn!("Error in `create_reminder`: Could not execute query: {:?}", e);
Err(json!({"error": "Unknown error"}))
}
}
}
async fn create_database_channel( async fn create_database_channel(
ctx: impl AsRef<Http>, ctx: impl AsRef<Http>,
channel: ChannelId, channel: ChannelId,