Move postman and web inside src
This commit is contained in:
61
src/web/routes/dashboard/api/guild/channels.rs
Normal file
61
src/web/routes/dashboard/api/guild/channels.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use rocket::{get, http::CookieJar, serde::json::json, State};
|
||||
use serde::Serialize;
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::{
|
||||
channel::GuildChannel,
|
||||
id::{ChannelId, GuildId},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::web::{check_authorization, routes::JsonResult};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ChannelInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
webhook_avatar: Option<String>,
|
||||
webhook_name: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/channels")]
|
||||
pub async fn get_guild_channels(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
) -> JsonResult {
|
||||
offline!(Ok(json!(vec![ChannelInfo {
|
||||
name: "general".to_string(),
|
||||
id: "1".to_string(),
|
||||
webhook_avatar: None,
|
||||
webhook_name: None,
|
||||
}])));
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match GuildId::new(id).to_guild_cached(ctx.inner()) {
|
||||
Some(guild) => {
|
||||
let mut channels = guild
|
||||
.channels
|
||||
.iter()
|
||||
.filter(|(_, channel)| channel.is_text_based())
|
||||
.map(|(id, channel)| (id.to_owned(), channel.to_owned()))
|
||||
.collect::<Vec<(ChannelId, GuildChannel)>>();
|
||||
|
||||
channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position));
|
||||
|
||||
let channel_info = channels
|
||||
.iter()
|
||||
.map(|(channel_id, channel)| ChannelInfo {
|
||||
name: channel.name.to_string(),
|
||||
id: channel_id.to_string(),
|
||||
webhook_avatar: None,
|
||||
webhook_name: None,
|
||||
})
|
||||
.collect::<Vec<ChannelInfo>>();
|
||||
|
||||
Ok(json!(channel_info))
|
||||
}
|
||||
|
||||
None => json_err!("Bot not in guild"),
|
||||
}
|
||||
}
|
45
src/web/routes/dashboard/api/guild/mod.rs
Normal file
45
src/web/routes/dashboard/api/guild/mod.rs
Normal file
@ -0,0 +1,45 @@
|
||||
mod channels;
|
||||
mod reminders;
|
||||
mod roles;
|
||||
mod templates;
|
||||
|
||||
use std::env;
|
||||
|
||||
pub use channels::*;
|
||||
pub use reminders::*;
|
||||
use rocket::{get, http::CookieJar, serde::json::json, State};
|
||||
pub use roles::*;
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::id::{GuildId, RoleId},
|
||||
};
|
||||
pub use templates::*;
|
||||
|
||||
use crate::web::{check_authorization, routes::JsonResult};
|
||||
|
||||
#[get("/api/guild/<id>")]
|
||||
pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
|
||||
offline!(Ok(json!({ "patreon": true, "name": "Guild" })));
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match GuildId::new(id)
|
||||
.to_guild_cached(ctx.inner())
|
||||
.map(|guild| (guild.owner_id, guild.name.clone()))
|
||||
{
|
||||
Some((owner_id, name)) => {
|
||||
let member_res = GuildId::new(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
|
||||
.member(&ctx.inner(), owner_id)
|
||||
.await;
|
||||
|
||||
let patreon = member_res.map_or(false, |member| {
|
||||
member
|
||||
.roles
|
||||
.contains(&RoleId::new(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
|
||||
});
|
||||
|
||||
Ok(json!({ "patreon": patreon, "name": name }))
|
||||
}
|
||||
|
||||
None => json_err!("Bot not in guild"),
|
||||
}
|
||||
}
|
367
src/web/routes/dashboard/api/guild/reminders.rs
Normal file
367
src/web/routes/dashboard/api/guild/reminders.rs
Normal file
@ -0,0 +1,367 @@
|
||||
use log::warn;
|
||||
use rocket::{
|
||||
get,
|
||||
http::CookieJar,
|
||||
patch, post,
|
||||
serde::json::{json, Json},
|
||||
State,
|
||||
};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::web::{
|
||||
check_authorization, check_guild_subscription, check_subscription,
|
||||
consts::MIN_INTERVAL,
|
||||
guards::transaction::Transaction,
|
||||
routes::{
|
||||
dashboard::{create_database_channel, create_reminder, PatchReminder, Reminder},
|
||||
JsonResult,
|
||||
},
|
||||
Database,
|
||||
};
|
||||
|
||||
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||
pub async fn create_guild_reminder(
|
||||
id: u64,
|
||||
reminder: Json<Reminder>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
mut transaction: Transaction<'_>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
|
||||
match create_reminder(
|
||||
ctx.inner(),
|
||||
&mut transaction,
|
||||
GuildId::new(id),
|
||||
UserId::new(user_id),
|
||||
reminder.into_inner(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => match transaction.commit().await {
|
||||
Ok(_) => Ok(r),
|
||||
Err(e) => {
|
||||
warn!("Couldn't commit transaction: {:?}", e);
|
||||
json_err!("Couldn't commit transaction.")
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/reminders")]
|
||||
pub async fn get_reminders(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let channels_res = GuildId::new(id).channels(&ctx.inner()).await;
|
||||
|
||||
match channels_res {
|
||||
Ok(channels) => {
|
||||
let channels = channels
|
||||
.keys()
|
||||
.into_iter()
|
||||
.map(|k| k.get().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.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,
|
||||
IFNULL(reminders.embed_fields, '[]') AS embed_fields,
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
reminders.username,
|
||||
reminders.utc_time
|
||||
FROM reminders
|
||||
INNER JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
|
||||
channels
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
.map(|r| Ok(json!(r)))
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("Failed to complete SQL query: {:?}", e);
|
||||
|
||||
json_err!("Could not load reminders")
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not fetch channels from {}: {:?}", id, e);
|
||||
|
||||
Ok(json!([]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||
pub async fn edit_reminder(
|
||||
id: u64,
|
||||
reminder: Json<PatchReminder>,
|
||||
ctx: &State<Context>,
|
||||
mut transaction: Transaction<'_>,
|
||||
pool: &State<Pool<Database>>,
|
||||
cookies: &CookieJar<'_>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let mut error = vec![];
|
||||
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
|
||||
if reminder.message_ok() {
|
||||
update_field!(transaction.executor(), error, reminder.[
|
||||
content,
|
||||
embed_author,
|
||||
embed_description,
|
||||
embed_footer,
|
||||
embed_title,
|
||||
embed_fields,
|
||||
username
|
||||
]);
|
||||
} else {
|
||||
error.push("Message exceeds limits.".to_string());
|
||||
}
|
||||
|
||||
update_field!(transaction.executor(), error, reminder.[
|
||||
attachment,
|
||||
attachment_name,
|
||||
avatar,
|
||||
embed_author_url,
|
||||
embed_color,
|
||||
embed_footer_url,
|
||||
embed_image_url,
|
||||
embed_thumbnail_url,
|
||||
enabled,
|
||||
expires,
|
||||
name,
|
||||
restartable,
|
||||
tts,
|
||||
utc_time
|
||||
]);
|
||||
|
||||
if reminder.interval_days.flatten().is_some()
|
||||
|| reminder.interval_months.flatten().is_some()
|
||||
|| reminder.interval_seconds.flatten().is_some()
|
||||
{
|
||||
if check_guild_subscription(&ctx.inner(), id).await
|
||||
|| check_subscription(&ctx.inner(), user_id).await
|
||||
{
|
||||
let new_interval_length = match reminder.interval_days {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_days AS days FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.days
|
||||
.unwrap_or(0),
|
||||
} * 86400 + match reminder.interval_months {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_months AS months FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.months
|
||||
.unwrap_or(0),
|
||||
} * 2592000 + match reminder.interval_seconds {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.seconds
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
if new_interval_length < *MIN_INTERVAL {
|
||||
error.push(String::from("New interval is too short."));
|
||||
} else {
|
||||
update_field!(transaction.executor(), error, reminder.[
|
||||
interval_days,
|
||||
interval_months,
|
||||
interval_seconds
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE reminders
|
||||
SET interval_seconds = NULL, interval_days = NULL, interval_months = NULL
|
||||
WHERE uid = ?
|
||||
",
|
||||
reminder.uid
|
||||
)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?;
|
||||
}
|
||||
|
||||
if reminder.channel > 0 {
|
||||
let channel_guild = ChannelId::new(reminder.channel)
|
||||
.to_channel_cached(&ctx.cache)
|
||||
.map(|channel| channel.guild_id);
|
||||
match channel_guild {
|
||||
Some(channel_guild) => {
|
||||
let channel_matches_guild = channel_guild.get() == id;
|
||||
|
||||
if !channel_matches_guild {
|
||||
warn!(
|
||||
"Error in `edit_reminder`: channel {:?} not found for guild {}",
|
||||
reminder.channel, id
|
||||
);
|
||||
|
||||
return Err(json!({"error": "Channel not found"}));
|
||||
}
|
||||
|
||||
let channel = create_database_channel(
|
||||
ctx.inner(),
|
||||
ChannelId::new(reminder.channel),
|
||||
&mut transaction,
|
||||
)
|
||||
.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();
|
||||
|
||||
match sqlx::query!(
|
||||
"UPDATE reminders SET channel_id = ? WHERE uid = ?",
|
||||
channel,
|
||||
reminder.uid
|
||||
)
|
||||
.execute(transaction.executor())
|
||||
.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 Err(json!({"error": "Channel not found"}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = transaction.commit().await {
|
||||
warn!("Couldn't commit transaction: {:?}", e);
|
||||
return json_err!("Couldn't commit transaction");
|
||||
}
|
||||
|
||||
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_days,
|
||||
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 = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error exiting `edit_reminder': {:?}", e);
|
||||
|
||||
Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]}))
|
||||
}
|
||||
}
|
||||
}
|
36
src/web/routes/dashboard/api/guild/roles.rs
Normal file
36
src/web/routes/dashboard/api/guild/roles.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use log::warn;
|
||||
use rocket::{get, http::CookieJar, serde::json::json, State};
|
||||
use serde::Serialize;
|
||||
use serenity::client::Context;
|
||||
|
||||
use crate::web::{check_authorization, routes::JsonResult};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RoleInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/roles")]
|
||||
pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
|
||||
offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }])));
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
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::<Vec<RoleInfo>>();
|
||||
|
||||
Ok(json!(roles))
|
||||
}
|
||||
None => {
|
||||
warn!("Could not fetch roles from {}", id);
|
||||
|
||||
json_err!("Could not get roles")
|
||||
}
|
||||
}
|
||||
}
|
184
src/web/routes/dashboard/api/guild/templates.rs
Normal file
184
src/web/routes/dashboard/api/guild/templates.rs
Normal file
@ -0,0 +1,184 @@
|
||||
use log::warn;
|
||||
use rocket::{
|
||||
delete, get,
|
||||
http::CookieJar,
|
||||
post,
|
||||
serde::json::{json, Json},
|
||||
State,
|
||||
};
|
||||
use serenity::client::Context;
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::web::{
|
||||
check_authorization,
|
||||
consts::{
|
||||
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,
|
||||
},
|
||||
routes::{
|
||||
dashboard::{template_name_default, DeleteReminderTemplate, ReminderTemplate},
|
||||
JsonResult,
|
||||
},
|
||||
};
|
||||
|
||||
#[get("/api/guild/<id>/templates")]
|
||||
pub async fn get_reminder_templates(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match sqlx::query_as_unchecked!(
|
||||
ReminderTemplate,
|
||||
"SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(templates) => Ok(json!(templates)),
|
||||
Err(e) => {
|
||||
warn!("Could not fetch templates from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not get templates")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/guild/<id>/templates", data = "<reminder_template>")]
|
||||
pub async fn create_reminder_template(
|
||||
id: u64,
|
||||
reminder_template: Json<ReminderTemplate>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
// validate lengths
|
||||
check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
|
||||
check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
|
||||
check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
|
||||
check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
|
||||
check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
|
||||
check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
|
||||
if let Some(fields) = &reminder_template.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_template.username);
|
||||
check_length_opt!(
|
||||
MAX_URL_LENGTH,
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.avatar
|
||||
);
|
||||
|
||||
// validate urls
|
||||
check_url_opt!(
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.avatar
|
||||
);
|
||||
|
||||
let name = if reminder_template.name.is_empty() {
|
||||
template_name_default()
|
||||
} else {
|
||||
reminder_template.name.clone()
|
||||
};
|
||||
|
||||
match sqlx::query!(
|
||||
"INSERT INTO reminder_template
|
||||
(guild_id,
|
||||
name,
|
||||
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,
|
||||
interval_seconds,
|
||||
interval_days,
|
||||
interval_months,
|
||||
tts,
|
||||
username
|
||||
) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?)",
|
||||
id,
|
||||
name,
|
||||
reminder_template.attachment,
|
||||
reminder_template.attachment_name,
|
||||
reminder_template.avatar,
|
||||
reminder_template.content,
|
||||
reminder_template.embed_author,
|
||||
reminder_template.embed_author_url,
|
||||
reminder_template.embed_color,
|
||||
reminder_template.embed_description,
|
||||
reminder_template.embed_footer,
|
||||
reminder_template.embed_footer_url,
|
||||
reminder_template.embed_image_url,
|
||||
reminder_template.embed_thumbnail_url,
|
||||
reminder_template.embed_title,
|
||||
reminder_template.embed_fields,
|
||||
reminder_template.interval_seconds,
|
||||
reminder_template.interval_days,
|
||||
reminder_template.interval_months,
|
||||
reminder_template.tts,
|
||||
reminder_template.username,
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(json!({})),
|
||||
Err(e) => {
|
||||
warn!("Could not create template for {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not create template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")]
|
||||
pub async fn delete_reminder_template(
|
||||
id: u64,
|
||||
delete_reminder_template: Json<DeleteReminderTemplate>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
match sqlx::query!(
|
||||
"DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
|
||||
id, delete_reminder_template.id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
Ok(json!({}))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not delete template from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Could not delete template")
|
||||
}
|
||||
}
|
||||
}
|
42
src/web/routes/dashboard/api/mod.rs
Normal file
42
src/web/routes/dashboard/api/mod.rs
Normal file
@ -0,0 +1,42 @@
|
||||
pub mod guild;
|
||||
pub mod user;
|
||||
|
||||
use log::warn;
|
||||
use rocket::{
|
||||
delete,
|
||||
http::CookieJar,
|
||||
serde::json::{json, Json},
|
||||
State,
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::web::routes::{dashboard::DeleteReminder, JsonResult};
|
||||
|
||||
#[delete("/api/reminders", data = "<reminder>")]
|
||||
pub async fn delete_reminder(
|
||||
cookies: &CookieJar<'_>,
|
||||
reminder: Json<DeleteReminder>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
match cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten() {
|
||||
Some(_) => {
|
||||
match sqlx::query!(
|
||||
"UPDATE reminders SET `status` = 'deleted' WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.execute(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(json!({})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error in `delete_reminder`: {:?}", e);
|
||||
|
||||
Err(json!({"error": "Could not delete reminder"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => Err(json!({"error": "User not authorized"})),
|
||||
}
|
||||
}
|
83
src/web/routes/dashboard/api/user/guilds.rs
Normal file
83
src/web/routes/dashboard/api/user/guilds.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use log::warn;
|
||||
use reqwest::Client;
|
||||
use rocket::{
|
||||
get,
|
||||
http::CookieJar,
|
||||
serde::json::{json, Value as JsonValue},
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::model::{id::GuildId, permissions::Permissions};
|
||||
|
||||
use crate::web::consts::DISCORD_API;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GuildInfo {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PartialGuild {
|
||||
pub id: GuildId,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub owner: bool,
|
||||
#[serde(rename = "permissions_new")]
|
||||
pub permissions: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/api/user/guilds")]
|
||||
pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue {
|
||||
offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }]));
|
||||
|
||||
if let Some(access_token) = cookies.get_private("access_token") {
|
||||
let request_res = reqwest_client
|
||||
.get(format!("{}/users/@me/guilds", DISCORD_API))
|
||||
.bearer_auth(access_token.value())
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match request_res {
|
||||
Ok(response) => {
|
||||
let guilds_res = response.json::<Vec<PartialGuild>>().await;
|
||||
|
||||
match guilds_res {
|
||||
Ok(guilds) => {
|
||||
let reduced_guilds = guilds
|
||||
.iter()
|
||||
.filter(|g| {
|
||||
g.owner
|
||||
|| g.permissions.as_ref().map_or(false, |p| {
|
||||
let permissions =
|
||||
Permissions::from_bits_truncate(p.parse().unwrap());
|
||||
|
||||
permissions.manage_messages()
|
||||
|| permissions.manage_guild()
|
||||
|| permissions.administrator()
|
||||
})
|
||||
})
|
||||
.map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() })
|
||||
.collect::<Vec<GuildInfo>>();
|
||||
|
||||
json!(reduced_guilds)
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error constructing user from request: {:?}", e);
|
||||
|
||||
json!({"error": "Could not get user details"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error getting user guilds: {:?}", e);
|
||||
|
||||
json!({"error": "Could not reach Discord"})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
102
src/web/routes/dashboard/api/user/mod.rs
Normal file
102
src/web/routes/dashboard/api/user/mod.rs
Normal file
@ -0,0 +1,102 @@
|
||||
mod guilds;
|
||||
mod models;
|
||||
mod reminders;
|
||||
|
||||
use std::env;
|
||||
|
||||
use chrono_tz::Tz;
|
||||
pub use guilds::*;
|
||||
pub use reminders::*;
|
||||
use rocket::{
|
||||
get,
|
||||
http::CookieJar,
|
||||
patch,
|
||||
serde::json::{json, Json, Value as JsonValue},
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::id::{GuildId, RoleId},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UserInfo {
|
||||
name: String,
|
||||
patreon: bool,
|
||||
timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateUser {
|
||||
timezone: String,
|
||||
}
|
||||
|
||||
#[get("/api/user")]
|
||||
pub async fn get_user_info(
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonValue {
|
||||
offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None }));
|
||||
|
||||
if let Some(user_id) =
|
||||
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
|
||||
{
|
||||
let member_res = GuildId::new(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
|
||||
.member(&ctx.inner(), user_id)
|
||||
.await;
|
||||
|
||||
let timezone = sqlx::query!(
|
||||
"SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?",
|
||||
user_id
|
||||
)
|
||||
.fetch_one(pool.inner())
|
||||
.await
|
||||
.map_or(None, |q| Some(q.timezone));
|
||||
|
||||
let user_info = UserInfo {
|
||||
name: cookies
|
||||
.get_private("username")
|
||||
.map_or("Discord User".to_string(), |c| c.value().to_string()),
|
||||
patreon: member_res.map_or(false, |member| {
|
||||
member
|
||||
.roles
|
||||
.contains(&RoleId::new(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
|
||||
}),
|
||||
timezone,
|
||||
};
|
||||
|
||||
json!(user_info)
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
||||
|
||||
#[patch("/api/user", data = "<user>")]
|
||||
pub async fn update_user_info(
|
||||
cookies: &CookieJar<'_>,
|
||||
user: Json<UpdateUser>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonValue {
|
||||
if let Some(user_id) =
|
||||
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
|
||||
{
|
||||
if user.timezone.parse::<Tz>().is_ok() {
|
||||
let _ = sqlx::query!(
|
||||
"UPDATE users SET timezone = ? WHERE user = ?",
|
||||
user.timezone,
|
||||
user_id,
|
||||
)
|
||||
.execute(pool.inner())
|
||||
.await;
|
||||
|
||||
json!({})
|
||||
} else {
|
||||
json!({"error": "Timezone not recognized"})
|
||||
}
|
||||
} else {
|
||||
json!({"error": "Not authorized"})
|
||||
}
|
||||
}
|
231
src/web/routes/dashboard/api/user/models.rs
Normal file
231
src/web/routes/dashboard/api/user/models.rs
Normal file
@ -0,0 +1,231 @@
|
||||
use chrono::{naive::NaiveDateTime, Utc};
|
||||
use futures::TryFutureExt;
|
||||
use log::warn;
|
||||
use rocket::serde::json::json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{client::Context, model::id::UserId};
|
||||
use sqlx::types::Json;
|
||||
|
||||
use crate::web::{
|
||||
check_subscription,
|
||||
consts::{
|
||||
DAY, 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_NAME_LENGTH, MAX_URL_LENGTH,
|
||||
MIN_INTERVAL,
|
||||
},
|
||||
guards::transaction::Transaction,
|
||||
routes::{
|
||||
dashboard::{create_database_channel, generate_uid, name_default, Attachment, EmbedField},
|
||||
JsonResult,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Reminder {
|
||||
pub attachment: Option<Attachment>,
|
||||
pub attachment_name: Option<String>,
|
||||
pub content: String,
|
||||
pub embed_author: String,
|
||||
pub embed_author_url: Option<String>,
|
||||
pub embed_color: u32,
|
||||
pub embed_description: String,
|
||||
pub embed_footer: String,
|
||||
pub embed_footer_url: Option<String>,
|
||||
pub embed_image_url: Option<String>,
|
||||
pub embed_thumbnail_url: Option<String>,
|
||||
pub embed_title: String,
|
||||
pub embed_fields: Option<Json<Vec<EmbedField>>>,
|
||||
pub enabled: bool,
|
||||
pub expires: Option<NaiveDateTime>,
|
||||
pub interval_seconds: Option<u32>,
|
||||
pub interval_days: Option<u32>,
|
||||
pub interval_months: Option<u32>,
|
||||
#[serde(default = "name_default")]
|
||||
pub name: String,
|
||||
pub tts: bool,
|
||||
#[serde(default)]
|
||||
pub uid: String,
|
||||
pub utc_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
pub async fn create_reminder(
|
||||
ctx: &Context,
|
||||
transaction: &mut Transaction<'_>,
|
||||
user_id: UserId,
|
||||
reminder: Reminder,
|
||||
) -> JsonResult {
|
||||
let channel = user_id
|
||||
.create_dm_channel(&ctx)
|
||||
.map_err(|e| Error::Serenity(e))
|
||||
.and_then(|dm_channel| create_database_channel(&ctx, dm_channel.id, transaction))
|
||||
.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."}));
|
||||
}
|
||||
|
||||
let channel = channel.unwrap();
|
||||
|
||||
// validate lengths
|
||||
check_length!(MAX_NAME_LENGTH, reminder.name);
|
||||
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_URL_LENGTH,
|
||||
reminder.embed_footer_url,
|
||||
reminder.embed_thumbnail_url,
|
||||
reminder.embed_author_url,
|
||||
reminder.embed_image_url
|
||||
);
|
||||
|
||||
// validate urls
|
||||
check_url_opt!(
|
||||
reminder.embed_footer_url,
|
||||
reminder.embed_thumbnail_url,
|
||||
reminder.embed_author_url,
|
||||
reminder.embed_image_url
|
||||
);
|
||||
|
||||
// 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_days.is_some()
|
||||
|| reminder.interval_months.is_some()
|
||||
{
|
||||
if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32
|
||||
+ reminder.interval_days.unwrap_or(0) * 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_days.is_some()
|
||||
|| reminder.interval_months.is_some()
|
||||
{
|
||||
if !check_subscription(&ctx, user_id).await {
|
||||
return Err(json!({"error": "Patreon is required to set intervals"}));
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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_days,
|
||||
interval_months,
|
||||
name,
|
||||
tts,
|
||||
`utc_time`
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
new_uid,
|
||||
reminder.attachment,
|
||||
reminder.attachment_name,
|
||||
channel,
|
||||
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_days,
|
||||
reminder.interval_months,
|
||||
name,
|
||||
reminder.tts,
|
||||
reminder.utc_time,
|
||||
)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
{
|
||||
Ok(_) => sqlx::query_as_unchecked!(
|
||||
Reminder,
|
||||
"SELECT
|
||||
reminders.attachment,
|
||||
reminders.attachment_name,
|
||||
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_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
reminders.utc_time
|
||||
FROM reminders
|
||||
WHERE uid = ?",
|
||||
new_uid
|
||||
)
|
||||
.fetch_one(transaction.executor())
|
||||
.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"}))
|
||||
}
|
||||
}
|
||||
}
|
284
src/web/routes/dashboard/api/user/reminders.rs
Normal file
284
src/web/routes/dashboard/api/user/reminders.rs
Normal file
@ -0,0 +1,284 @@
|
||||
use log::warn;
|
||||
use rocket::{
|
||||
get,
|
||||
http::CookieJar,
|
||||
patch, post,
|
||||
serde::json::{json, Json},
|
||||
State,
|
||||
};
|
||||
use serenity::{client::Context, model::id::UserId};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::web::{
|
||||
check_subscription,
|
||||
guards::transaction::Transaction,
|
||||
routes::{
|
||||
dashboard::{
|
||||
api::user::models::{create_reminder, Reminder},
|
||||
PatchReminder, MIN_INTERVAL,
|
||||
},
|
||||
JsonResult,
|
||||
},
|
||||
Database,
|
||||
};
|
||||
|
||||
#[post("/api/user/reminders", data = "<reminder>")]
|
||||
pub async fn create_user_reminder(
|
||||
reminder: Json<Reminder>,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
mut transaction: Transaction<'_>,
|
||||
) -> JsonResult {
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
|
||||
match create_reminder(
|
||||
ctx.inner(),
|
||||
&mut transaction,
|
||||
UserId::new(user_id),
|
||||
reminder.into_inner(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => match transaction.commit().await {
|
||||
Ok(_) => Ok(r),
|
||||
Err(e) => {
|
||||
warn!("Couldn't commit transaction: {:?}", e);
|
||||
json_err!("Couldn't commit transaction.")
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/user/reminders")]
|
||||
pub async fn get_reminders(
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
let channel = UserId::new(user_id).create_dm_channel(ctx.inner()).await;
|
||||
|
||||
match channel {
|
||||
Ok(channel) => sqlx::query_as_unchecked!(
|
||||
Reminder,
|
||||
"
|
||||
SELECT
|
||||
reminders.attachment,
|
||||
reminders.attachment_name,
|
||||
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,
|
||||
IFNULL(reminders.embed_fields, '[]') AS embed_fields,
|
||||
reminders.enabled,
|
||||
reminders.expires,
|
||||
reminders.interval_seconds,
|
||||
reminders.interval_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
reminders.utc_time
|
||||
FROM reminders
|
||||
INNER JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE `status` = 'pending' AND channels.channel = ?
|
||||
",
|
||||
channel.id.get()
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
.map(|r| Ok(json!(r)))
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("Failed to complete SQL query: {:?}", e);
|
||||
|
||||
json_err!("Could not load reminders")
|
||||
}),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Couldn't get DM channel: {:?}", e);
|
||||
|
||||
json_err!("Could not find a DM channel")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[patch("/api/user/reminders", data = "<reminder>")]
|
||||
pub async fn edit_reminder(
|
||||
reminder: Json<PatchReminder>,
|
||||
ctx: &State<Context>,
|
||||
mut transaction: Transaction<'_>,
|
||||
pool: &State<Pool<Database>>,
|
||||
cookies: &CookieJar<'_>,
|
||||
) -> JsonResult {
|
||||
let user_id_cookie =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
|
||||
|
||||
if user_id_cookie.is_none() {
|
||||
return Err(json!({"error": "User not authorized"}));
|
||||
}
|
||||
|
||||
let mut error = vec![];
|
||||
let user_id = user_id_cookie.unwrap();
|
||||
|
||||
if reminder.message_ok() {
|
||||
update_field!(transaction.executor(), error, reminder.[
|
||||
content,
|
||||
embed_author,
|
||||
embed_description,
|
||||
embed_footer,
|
||||
embed_title,
|
||||
embed_fields
|
||||
]);
|
||||
} else {
|
||||
error.push("Message exceeds limits.".to_string());
|
||||
}
|
||||
|
||||
update_field!(transaction.executor(), error, reminder.[
|
||||
attachment,
|
||||
attachment_name,
|
||||
embed_author_url,
|
||||
embed_color,
|
||||
embed_footer_url,
|
||||
embed_image_url,
|
||||
embed_thumbnail_url,
|
||||
enabled,
|
||||
expires,
|
||||
name,
|
||||
tts,
|
||||
utc_time
|
||||
]);
|
||||
|
||||
if reminder.interval_days.flatten().is_some()
|
||||
|| reminder.interval_months.flatten().is_some()
|
||||
|| reminder.interval_seconds.flatten().is_some()
|
||||
{
|
||||
if check_subscription(&ctx.inner(), user_id).await {
|
||||
let new_interval_length = match reminder.interval_days {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_days AS days FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.days
|
||||
.unwrap_or(0),
|
||||
} * 86400 + match reminder.interval_months {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_months AS months FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.months
|
||||
.unwrap_or(0),
|
||||
} * 2592000 + match reminder.interval_seconds {
|
||||
Some(interval) => interval.unwrap_or(0),
|
||||
None => sqlx::query!(
|
||||
"SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?",
|
||||
reminder.uid
|
||||
)
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?
|
||||
.seconds
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
if new_interval_length < *MIN_INTERVAL {
|
||||
error.push(String::from("New interval is too short."));
|
||||
} else {
|
||||
update_field!(transaction.executor(), error, reminder.[
|
||||
interval_days,
|
||||
interval_months,
|
||||
interval_seconds
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE reminders
|
||||
SET interval_seconds = NULL, interval_days = NULL, interval_months = NULL
|
||||
WHERE uid = ?
|
||||
",
|
||||
reminder.uid
|
||||
)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Error updating reminder interval: {:?}", e);
|
||||
json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Err(e) = transaction.commit().await {
|
||||
warn!("Couldn't commit transaction: {:?}", e);
|
||||
return json_err!("Couldn't commit transaction");
|
||||
}
|
||||
|
||||
match sqlx::query_as_unchecked!(
|
||||
Reminder,
|
||||
"
|
||||
SELECT reminders.attachment,
|
||||
reminders.attachment_name,
|
||||
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_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.tts,
|
||||
reminders.uid,
|
||||
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) => Ok(json!({"reminder": reminder, "errors": error})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Error exiting `edit_reminder': {:?}", e);
|
||||
|
||||
Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]}))
|
||||
}
|
||||
}
|
||||
}
|
451
src/web/routes/dashboard/export.rs
Normal file
451
src/web/routes/dashboard/export.rs
Normal file
@ -0,0 +1,451 @@
|
||||
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||
use csv::{QuoteStyle, WriterBuilder};
|
||||
use log::warn;
|
||||
use rocket::{
|
||||
get,
|
||||
http::CookieJar,
|
||||
put,
|
||||
serde::json::{json, Json},
|
||||
State,
|
||||
};
|
||||
use serenity::{
|
||||
client::Context,
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
use crate::web::{
|
||||
check_authorization,
|
||||
guards::transaction::Transaction,
|
||||
routes::{
|
||||
dashboard::{
|
||||
create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv,
|
||||
TodoCsv,
|
||||
},
|
||||
JsonResult,
|
||||
},
|
||||
};
|
||||
|
||||
#[get("/api/guild/<id>/export/reminders")]
|
||||
pub async fn export_reminders(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||
|
||||
let channels_res = GuildId::new(id).channels(&ctx.inner()).await;
|
||||
|
||||
match channels_res {
|
||||
Ok(channels) => {
|
||||
let channels = channels
|
||||
.keys()
|
||||
.into_iter()
|
||||
.map(|k| k.get().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
|
||||
let result = sqlx::query_as_unchecked!(
|
||||
ReminderCsv,
|
||||
"SELECT
|
||||
reminders.attachment,
|
||||
reminders.attachment_name,
|
||||
reminders.avatar,
|
||||
CONCAT('#', channels.channel) AS 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_days,
|
||||
reminders.interval_months,
|
||||
reminders.name,
|
||||
reminders.restartable,
|
||||
reminders.tts,
|
||||
reminders.username,
|
||||
reminders.utc_time
|
||||
FROM reminders
|
||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||
WHERE FIND_IN_SET(channels.channel, ?) AND status = 'pending'",
|
||||
channels
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(reminders) => {
|
||||
reminders.iter().for_each(|reminder| {
|
||||
csv_writer.serialize(reminder).unwrap();
|
||||
});
|
||||
|
||||
match csv_writer.into_inner() {
|
||||
Ok(inner) => match String::from_utf8(inner) {
|
||||
Ok(encoded) => Ok(json!({ "body": encoded })),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to write UTF-8: {:?}", e);
|
||||
|
||||
Err(json!({"error": "Failed to write UTF-8"}))
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to extract CSV: {:?}", e);
|
||||
|
||||
Err(json!({"error": "Failed to extract CSV"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to complete SQL query: {:?}", e);
|
||||
|
||||
Err(json!({"error": "Failed to query reminders"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Could not fetch channels from {}: {:?}", id, e);
|
||||
|
||||
Err(json!({"error": "Failed to get guild channels"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/api/guild/<id>/export/reminders", data = "<body>")]
|
||||
pub(crate) async fn import_reminders(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
body: Json<ImportBody>,
|
||||
ctx: &State<Context>,
|
||||
mut transaction: Transaction<'_>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let user_id =
|
||||
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
|
||||
|
||||
match BASE64_STANDARD.decode(&body.body) {
|
||||
Ok(body) => {
|
||||
let mut reader = csv::Reader::from_reader(body.as_slice());
|
||||
let mut count = 0;
|
||||
|
||||
for result in reader.deserialize::<ReminderCsv>() {
|
||||
match result {
|
||||
Ok(record) => {
|
||||
let channel_id = record.channel.split_at(1).1;
|
||||
|
||||
match channel_id.parse::<u64>() {
|
||||
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_days: record.interval_days,
|
||||
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,
|
||||
};
|
||||
|
||||
create_reminder(
|
||||
ctx.inner(),
|
||||
&mut transaction,
|
||||
GuildId::new(id),
|
||||
UserId::new(user_id),
|
||||
reminder,
|
||||
)
|
||||
.await?;
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match transaction.commit().await {
|
||||
Ok(_) => Ok(json!({
|
||||
"message": format!("Imported {} reminders", count)
|
||||
})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to commit transaction: {:?}", e);
|
||||
json_err!("Couldn't commit transaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
json_err!("Malformed base64")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/export/todos")]
|
||||
pub async fn export_todos(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||
|
||||
match sqlx::query_as_unchecked!(
|
||||
TodoCsv,
|
||||
"SELECT value, CONCAT('#', channels.channel) AS channel_id FROM todos
|
||||
LEFT JOIN channels ON todos.channel_id = channels.id
|
||||
INNER JOIN guilds ON todos.guild_id = guilds.id
|
||||
WHERE guilds.guild = ?",
|
||||
id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(todos) => {
|
||||
todos.iter().for_each(|todo| {
|
||||
csv_writer.serialize(todo).unwrap();
|
||||
});
|
||||
|
||||
match csv_writer.into_inner() {
|
||||
Ok(inner) => match String::from_utf8(inner) {
|
||||
Ok(encoded) => Ok(json!({ "body": encoded })),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to write UTF-8: {:?}", e);
|
||||
|
||||
json_err!("Failed to write UTF-8")
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to extract CSV: {:?}", e);
|
||||
|
||||
json_err!("Failed to extract CSV")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
warn!("Could not fetch templates from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Failed to query templates")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/api/guild/<id>/export/todos", data = "<body>")]
|
||||
pub async fn import_todos(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
body: Json<ImportBody>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let channels_res = GuildId::new(id).channels(&ctx.inner()).await;
|
||||
|
||||
match channels_res {
|
||||
Ok(channels) => match BASE64_STANDARD.decode(&body.body) {
|
||||
Ok(body) => {
|
||||
let mut reader = csv::Reader::from_reader(body.as_slice());
|
||||
|
||||
let query_placeholder = "(?, (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?))";
|
||||
let mut query_params = vec![];
|
||||
|
||||
for result in reader.deserialize::<TodoCsv>() {
|
||||
match result {
|
||||
Ok(record) => match record.channel_id {
|
||||
Some(channel_id) => {
|
||||
let channel_id = channel_id.split_at(1).1;
|
||||
|
||||
match channel_id.parse::<u64>() {
|
||||
Ok(channel_id) => {
|
||||
if channels.contains_key(&ChannelId::new(channel_id)) {
|
||||
query_params.push((record.value, Some(channel_id), id));
|
||||
} else {
|
||||
return json_err!(format!(
|
||||
"Invalid channel ID {}",
|
||||
channel_id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
return json_err!(format!(
|
||||
"Invalid channel ID {}",
|
||||
channel_id
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
query_params.push((record.value, None, id));
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
warn!("Couldn't deserialize CSV row: {:?}", e);
|
||||
|
||||
return json_err!("Deserialize error. Aborted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let query_str = format!(
|
||||
"INSERT INTO todos (value, channel_id, guild_id) VALUES {}",
|
||||
vec![query_placeholder].repeat(query_params.len()).join(",")
|
||||
);
|
||||
let mut query = sqlx::query(&query_str);
|
||||
|
||||
for param in query_params {
|
||||
query = query.bind(param.0).bind(param.1).bind(param.2);
|
||||
}
|
||||
|
||||
let res = query.execute(pool.inner()).await;
|
||||
|
||||
match res {
|
||||
Ok(_) => Ok(json!({})),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Couldn't execute todo query: {:?}", e);
|
||||
|
||||
json_err!("An unexpected error occured.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
json_err!("Malformed base64")
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
warn!("Couldn't fetch channels for guild {}: {:?}", id, e);
|
||||
|
||||
json_err!("Couldn't fetch channels.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/guild/<id>/export/reminder_templates")]
|
||||
pub async fn export_reminder_templates(
|
||||
id: u64,
|
||||
cookies: &CookieJar<'_>,
|
||||
ctx: &State<Context>,
|
||||
pool: &State<Pool<MySql>>,
|
||||
) -> JsonResult {
|
||||
check_authorization(cookies, ctx.inner(), id).await?;
|
||||
|
||||
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
|
||||
|
||||
match sqlx::query_as_unchecked!(
|
||||
ReminderTemplateCsv,
|
||||
"SELECT
|
||||
name,
|
||||
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,
|
||||
interval_seconds,
|
||||
interval_days,
|
||||
interval_months,
|
||||
tts,
|
||||
username
|
||||
FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||
id
|
||||
)
|
||||
.fetch_all(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(templates) => {
|
||||
templates.iter().for_each(|template| {
|
||||
csv_writer.serialize(template).unwrap();
|
||||
});
|
||||
|
||||
match csv_writer.into_inner() {
|
||||
Ok(inner) => match String::from_utf8(inner) {
|
||||
Ok(encoded) => Ok(json!({ "body": encoded })),
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to write UTF-8: {:?}", e);
|
||||
|
||||
json_err!("Failed to write UTF-8")
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to extract CSV: {:?}", e);
|
||||
|
||||
json_err!("Failed to extract CSV")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not fetch templates from {}: {:?}", id, e);
|
||||
|
||||
json_err!("Failed to query templates")
|
||||
}
|
||||
}
|
||||
}
|
718
src/web/routes/dashboard/mod.rs
Normal file
718
src/web/routes/dashboard/mod.rs
Normal file
@ -0,0 +1,718 @@
|
||||
use std::path::Path;
|
||||
|
||||
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||
use chrono::{naive::NaiveDateTime, Utc};
|
||||
use log::warn;
|
||||
use rand::{rngs::OsRng, seq::IteratorRandom};
|
||||
use rocket::{
|
||||
fs::NamedFile, get, http::CookieJar, response::Redirect, serde::json::json, Responder,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serenity::{
|
||||
all::CacheHttp,
|
||||
builder::CreateWebhook,
|
||||
client::Context,
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
};
|
||||
use sqlx::types::Json;
|
||||
|
||||
use crate::web::{
|
||||
catchers::internal_server_error,
|
||||
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_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL,
|
||||
},
|
||||
guards::transaction::Transaction,
|
||||
routes::JsonResult,
|
||||
Error,
|
||||
};
|
||||
|
||||
pub mod api;
|
||||
pub mod export;
|
||||
|
||||
type Unset<T> = Option<T>;
|
||||
|
||||
fn name_default() -> String {
|
||||
"Reminder".to_string()
|
||||
}
|
||||
|
||||
fn template_name_default() -> String {
|
||||
"Template".to_string()
|
||||
}
|
||||
|
||||
fn channel_default() -> u64 {
|
||||
0
|
||||
}
|
||||
|
||||
fn id_default() -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn interval_default() -> Unset<Option<u32>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct Attachment(Vec<u8>);
|
||||
|
||||
impl<'de> Deserialize<'de> for Attachment {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Attachment, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
Ok(Attachment(BASE64_STANDARD.decode(string).map_err(de::Error::custom)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Attachment {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.collect_str(&BASE64_STANDARD.encode(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ReminderTemplate {
|
||||
#[serde(default = "id_default")]
|
||||
id: u32,
|
||||
#[serde(default = "id_default")]
|
||||
guild_id: u32,
|
||||
#[serde(default = "template_name_default")]
|
||||
name: String,
|
||||
attachment: Option<Attachment>,
|
||||
attachment_name: Option<String>,
|
||||
avatar: Option<String>,
|
||||
content: String,
|
||||
embed_author: String,
|
||||
embed_author_url: Option<String>,
|
||||
embed_color: u32,
|
||||
embed_description: String,
|
||||
embed_footer: String,
|
||||
embed_footer_url: Option<String>,
|
||||
embed_image_url: Option<String>,
|
||||
embed_thumbnail_url: Option<String>,
|
||||
embed_title: String,
|
||||
embed_fields: Option<Json<Vec<EmbedField>>>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
tts: bool,
|
||||
username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ReminderTemplateCsv {
|
||||
#[serde(default = "template_name_default")]
|
||||
name: String,
|
||||
attachment: Option<Attachment>,
|
||||
attachment_name: Option<String>,
|
||||
avatar: Option<String>,
|
||||
content: String,
|
||||
embed_author: String,
|
||||
embed_author_url: Option<String>,
|
||||
embed_color: u32,
|
||||
embed_description: String,
|
||||
embed_footer: String,
|
||||
embed_footer_url: Option<String>,
|
||||
embed_image_url: Option<String>,
|
||||
embed_thumbnail_url: Option<String>,
|
||||
embed_title: String,
|
||||
embed_fields: Option<String>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
tts: bool,
|
||||
username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteReminderTemplate {
|
||||
id: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EmbedField {
|
||||
title: String,
|
||||
value: String,
|
||||
inline: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Reminder {
|
||||
attachment: Option<Attachment>,
|
||||
attachment_name: Option<String>,
|
||||
avatar: Option<String>,
|
||||
#[serde(with = "string")]
|
||||
channel: u64,
|
||||
content: String,
|
||||
embed_author: String,
|
||||
embed_author_url: Option<String>,
|
||||
embed_color: u32,
|
||||
embed_description: String,
|
||||
embed_footer: String,
|
||||
embed_footer_url: Option<String>,
|
||||
embed_image_url: Option<String>,
|
||||
embed_thumbnail_url: Option<String>,
|
||||
embed_title: String,
|
||||
embed_fields: Option<Json<Vec<EmbedField>>>,
|
||||
enabled: bool,
|
||||
expires: Option<NaiveDateTime>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
#[serde(default = "name_default")]
|
||||
name: String,
|
||||
restartable: bool,
|
||||
tts: bool,
|
||||
#[serde(default)]
|
||||
uid: String,
|
||||
username: Option<String>,
|
||||
utc_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ReminderCsv {
|
||||
attachment: Option<Attachment>,
|
||||
attachment_name: Option<String>,
|
||||
avatar: Option<String>,
|
||||
channel: String,
|
||||
content: String,
|
||||
embed_author: String,
|
||||
embed_author_url: Option<String>,
|
||||
embed_color: u32,
|
||||
embed_description: String,
|
||||
embed_footer: String,
|
||||
embed_footer_url: Option<String>,
|
||||
embed_image_url: Option<String>,
|
||||
embed_thumbnail_url: Option<String>,
|
||||
embed_title: String,
|
||||
embed_fields: Option<String>,
|
||||
enabled: bool,
|
||||
expires: Option<NaiveDateTime>,
|
||||
interval_seconds: Option<u32>,
|
||||
interval_days: Option<u32>,
|
||||
interval_months: Option<u32>,
|
||||
#[serde(default = "name_default")]
|
||||
name: String,
|
||||
restartable: bool,
|
||||
tts: bool,
|
||||
username: Option<String>,
|
||||
utc_time: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PatchReminder {
|
||||
uid: String,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
attachment: Unset<Option<Attachment>>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
attachment_name: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
avatar: Unset<Option<String>>,
|
||||
#[serde(default = "channel_default")]
|
||||
#[serde(with = "string")]
|
||||
channel: u64,
|
||||
#[serde(default)]
|
||||
content: Unset<String>,
|
||||
#[serde(default)]
|
||||
embed_author: Unset<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
embed_author_url: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
embed_color: Unset<u32>,
|
||||
#[serde(default)]
|
||||
embed_description: Unset<String>,
|
||||
#[serde(default)]
|
||||
embed_footer: Unset<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
embed_footer_url: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
embed_image_url: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
embed_thumbnail_url: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
embed_title: Unset<String>,
|
||||
#[serde(default)]
|
||||
embed_fields: Unset<Json<Vec<EmbedField>>>,
|
||||
#[serde(default)]
|
||||
enabled: Unset<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
expires: Unset<Option<NaiveDateTime>>,
|
||||
#[serde(default = "interval_default")]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
interval_seconds: Unset<Option<u32>>,
|
||||
#[serde(default = "interval_default")]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
interval_days: Unset<Option<u32>>,
|
||||
#[serde(default = "interval_default")]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
interval_months: Unset<Option<u32>>,
|
||||
#[serde(default)]
|
||||
name: Unset<String>,
|
||||
#[serde(default)]
|
||||
restartable: Unset<bool>,
|
||||
#[serde(default)]
|
||||
tts: Unset<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||
username: Unset<Option<String>>,
|
||||
#[serde(default)]
|
||||
utc_time: Unset<NaiveDateTime>,
|
||||
}
|
||||
|
||||
impl PatchReminder {
|
||||
fn message_ok(&self) -> bool {
|
||||
self.content.as_ref().map_or(true, |c| c.len() <= MAX_CONTENT_LENGTH)
|
||||
&& self.embed_author.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_AUTHOR_LENGTH)
|
||||
&& self
|
||||
.embed_description
|
||||
.as_ref()
|
||||
.map_or(true, |c| c.len() <= MAX_EMBED_DESCRIPTION_LENGTH)
|
||||
&& self.embed_footer.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_FOOTER_LENGTH)
|
||||
&& self.embed_title.as_ref().map_or(true, |c| c.len() <= MAX_EMBED_TITLE_LENGTH)
|
||||
&& self.embed_fields.as_ref().map_or(true, |c| {
|
||||
c.0.len() <= MAX_EMBED_FIELDS
|
||||
&& c.0.iter().all(|f| {
|
||||
f.title.len() <= MAX_EMBED_FIELD_TITLE_LENGTH
|
||||
&& f.value.len() <= MAX_EMBED_FIELD_VALUE_LENGTH
|
||||
})
|
||||
})
|
||||
&& self
|
||||
.username
|
||||
.as_ref()
|
||||
.map_or(true, |c| c.as_ref().map_or(true, |v| v.len() <= MAX_USERNAME_LENGTH))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_uid() -> String {
|
||||
let mut generator: OsRng = Default::default();
|
||||
|
||||
(0..64)
|
||||
.map(|_| CHARACTERS.chars().choose(&mut generator).unwrap().to_owned().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
Ok(Some(Option::deserialize(deserializer)?))
|
||||
}
|
||||
|
||||
// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
|
||||
mod string {
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: Display,
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.collect_str(value)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: Display,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteReminder {
|
||||
uid: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ImportBody {
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TodoCsv {
|
||||
value: String,
|
||||
channel_id: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) async fn create_reminder(
|
||||
ctx: &Context,
|
||||
transaction: &mut Transaction<'_>,
|
||||
guild_id: GuildId,
|
||||
user_id: UserId,
|
||||
reminder: Reminder,
|
||||
) -> JsonResult {
|
||||
// check guild in db
|
||||
match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.get())
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
{
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.get())
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err(json!({"error": "Guild could not be created"}));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
{
|
||||
// validate channel
|
||||
let channel = ChannelId::new(reminder.channel).to_channel_cached(&ctx.cache);
|
||||
let channel_exists = channel.is_some();
|
||||
|
||||
let channel_matches_guild =
|
||||
channel.map_or(false, |c| c.guild(&ctx.cache).map_or(false, |c| c.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::new(reminder.channel), transaction).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_NAME_LENGTH, reminder.name);
|
||||
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_days.is_some()
|
||||
|| reminder.interval_months.is_some()
|
||||
{
|
||||
if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32
|
||||
+ reminder.interval_days.unwrap_or(0) * 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_days.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"}));
|
||||
}
|
||||
}
|
||||
|
||||
let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() };
|
||||
let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) {
|
||||
None
|
||||
} else {
|
||||
reminder.username
|
||||
};
|
||||
|
||||
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_days,
|
||||
interval_months,
|
||||
name,
|
||||
restartable,
|
||||
tts,
|
||||
username,
|
||||
`utc_time`
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
new_uid,
|
||||
reminder.attachment,
|
||||
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_days,
|
||||
reminder.interval_months,
|
||||
name,
|
||||
reminder.restartable,
|
||||
reminder.tts,
|
||||
username,
|
||||
reminder.utc_time,
|
||||
)
|
||||
.execute(transaction.executor())
|
||||
.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_days,
|
||||
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(transaction.executor())
|
||||
.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(
|
||||
ctx: impl CacheHttp,
|
||||
channel: ChannelId,
|
||||
transaction: &mut Transaction<'_>,
|
||||
) -> Result<u32, Error> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT webhook_token, webhook_id FROM channels WHERE channel = ?",
|
||||
channel.get()
|
||||
)
|
||||
.fetch_one(transaction.executor())
|
||||
.await;
|
||||
|
||||
match row {
|
||||
Ok(row) => {
|
||||
let is_dm =
|
||||
channel.to_channel(&ctx).await.map_err(|e| Error::Serenity(e))?.private().is_some();
|
||||
if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
|
||||
let webhook = channel
|
||||
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
||||
.await
|
||||
.map_err(|e| Error::Serenity(e))?;
|
||||
|
||||
let token = webhook.token.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE channels SET webhook_id = ?, webhook_token = ? WHERE channel = ?
|
||||
",
|
||||
webhook.id.get(),
|
||||
token.expose_secret(),
|
||||
channel.get()
|
||||
)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| Error::SQLx(e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
// create webhook
|
||||
let webhook = channel
|
||||
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
||||
.await
|
||||
.map_err(|e| Error::Serenity(e))?;
|
||||
|
||||
let token = webhook.token.unwrap();
|
||||
|
||||
// create database entry
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO channels (
|
||||
webhook_id,
|
||||
webhook_token,
|
||||
channel
|
||||
) VALUES (?, ?, ?)
|
||||
",
|
||||
webhook.id.get(),
|
||||
token.expose_secret(),
|
||||
channel.get()
|
||||
)
|
||||
.execute(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| Error::SQLx(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Err(e) => Err(Error::SQLx(e)),
|
||||
}?;
|
||||
|
||||
let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.get())
|
||||
.fetch_one(transaction.executor())
|
||||
.await
|
||||
.map_err(|e| Error::SQLx(e))?;
|
||||
|
||||
Ok(row.id)
|
||||
}
|
||||
|
||||
#[derive(Responder)]
|
||||
pub enum DashboardPage {
|
||||
#[response(status = 200)]
|
||||
Ok(NamedFile),
|
||||
#[response(status = 302)]
|
||||
Unauthorised(Redirect),
|
||||
#[response(status = 500)]
|
||||
NotConfigured(Template),
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
|
||||
if cookies.get_private("userid").is_some() {
|
||||
match NamedFile::open(Path::new(path!("static/index.html"))).await {
|
||||
Ok(f) => DashboardPage::Ok(f),
|
||||
Err(e) => {
|
||||
warn!("Couldn't render dashboard: {:?}", e);
|
||||
|
||||
DashboardPage::NotConfigured(internal_server_error().await)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DashboardPage::Unauthorised(Redirect::to("/login/discord"))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/<_..>")]
|
||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
|
||||
if cookies.get_private("userid").is_some() {
|
||||
match NamedFile::open(Path::new(path!("static/index.html"))).await {
|
||||
Ok(f) => DashboardPage::Ok(f),
|
||||
Err(e) => {
|
||||
warn!("Couldn't render dashboard: {:?}", e);
|
||||
|
||||
DashboardPage::NotConfigured(internal_server_error().await)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DashboardPage::Unauthorised(Redirect::to("/login/discord"))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user