From ca13fd4fa7596d905822ab85e319c72e55955d42 Mon Sep 17 00:00:00 2001 From: jude Date: Sun, 8 Oct 2023 18:24:04 +0100 Subject: [PATCH] Restructure code --- web/src/lib.rs | 26 +- .../routes/dashboard/api/guild/channels.rs | 61 ++ web/src/routes/dashboard/api/guild/mod.rs | 42 ++ .../routes/dashboard/api/guild/reminders.rs | 373 ++++++++++ web/src/routes/dashboard/api/guild/roles.rs | 35 + .../routes/dashboard/api/guild/templates.rs | 181 +++++ web/src/routes/dashboard/api/mod.rs | 2 + web/src/routes/dashboard/api/user/guilds.rs | 81 +++ web/src/routes/dashboard/api/user/mod.rs | 97 +++ web/src/routes/dashboard/api/user/models.rs | 20 + .../routes/dashboard/api/user/reminders.rs | 29 + web/src/routes/dashboard/guild.rs | 649 ------------------ web/src/routes/dashboard/mod.rs | 2 +- web/src/routes/dashboard/user.rs | 172 ----- web/static/js/main.js | 9 +- .../reminder_errors.html.tera | 2 +- 16 files changed, 941 insertions(+), 840 deletions(-) create mode 100644 web/src/routes/dashboard/api/guild/channels.rs create mode 100644 web/src/routes/dashboard/api/guild/mod.rs create mode 100644 web/src/routes/dashboard/api/guild/reminders.rs create mode 100644 web/src/routes/dashboard/api/guild/roles.rs create mode 100644 web/src/routes/dashboard/api/guild/templates.rs create mode 100644 web/src/routes/dashboard/api/mod.rs create mode 100644 web/src/routes/dashboard/api/user/guilds.rs create mode 100644 web/src/routes/dashboard/api/user/mod.rs create mode 100644 web/src/routes/dashboard/api/user/models.rs create mode 100644 web/src/routes/dashboard/api/user/reminders.rs delete mode 100644 web/src/routes/dashboard/user.rs diff --git a/web/src/lib.rs b/web/src/lib.rs index 954a7e6..7e02265 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -121,19 +121,19 @@ pub async fn initialize( routes![ routes::dashboard::dashboard, routes::dashboard::dashboard_home, - routes::dashboard::user::get_user_info, - routes::dashboard::user::update_user_info, - routes::dashboard::user::get_user_guilds, - routes::dashboard::guild::get_guild_info, - routes::dashboard::guild::get_guild_channels, - routes::dashboard::guild::get_guild_roles, - routes::dashboard::guild::get_reminder_templates, - routes::dashboard::guild::create_reminder_template, - routes::dashboard::guild::delete_reminder_template, - routes::dashboard::guild::create_guild_reminder, - routes::dashboard::guild::get_reminders, - routes::dashboard::guild::edit_reminder, - routes::dashboard::guild::delete_reminder, + routes::dashboard::api::user::get_user_info, + routes::dashboard::api::user::update_user_info, + routes::dashboard::api::user::get_user_guilds, + routes::dashboard::api::guild::get_guild_info, + routes::dashboard::api::guild::get_guild_channels, + routes::dashboard::api::guild::get_guild_roles, + routes::dashboard::api::guild::get_reminder_templates, + routes::dashboard::api::guild::create_reminder_template, + routes::dashboard::api::guild::delete_reminder_template, + routes::dashboard::api::guild::create_guild_reminder, + routes::dashboard::api::guild::get_reminders, + routes::dashboard::api::guild::edit_reminder, + routes::dashboard::api::guild::delete_reminder, routes::dashboard::export::export_reminders, routes::dashboard::export::export_reminder_templates, routes::dashboard::export::export_todos, diff --git a/web/src/routes/dashboard/api/guild/channels.rs b/web/src/routes/dashboard/api/guild/channels.rs new file mode 100644 index 0000000..bc07dc3 --- /dev/null +++ b/web/src/routes/dashboard/api/guild/channels.rs @@ -0,0 +1,61 @@ +use rocket::{http::CookieJar, serde::json::json, State}; +use serde::Serialize; +use serenity::{ + client::Context, + model::{ + channel::GuildChannel, + id::{ChannelId, GuildId}, + }, +}; + +use crate::{check_authorization, routes::JsonResult}; + +#[derive(Serialize)] +struct ChannelInfo { + id: String, + name: String, + webhook_avatar: Option, + webhook_name: Option, +} + +#[get("/api/guild//channels")] +pub async fn get_guild_channels( + id: u64, + cookies: &CookieJar<'_>, + ctx: &State, +) -> 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(id).to_guild_cached(ctx.inner()) { + Some(guild) => { + let mut channels = guild + .channels + .iter() + .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) + .filter(|(_, channel)| channel.is_text_based()) + .collect::>(); + + channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); + + 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::>(); + + Ok(json!(channel_info)) + } + + None => json_err!("Bot not in guild"), + } +} diff --git a/web/src/routes/dashboard/api/guild/mod.rs b/web/src/routes/dashboard/api/guild/mod.rs new file mode 100644 index 0000000..0798150 --- /dev/null +++ b/web/src/routes/dashboard/api/guild/mod.rs @@ -0,0 +1,42 @@ +mod channels; +mod reminders; +mod roles; +mod templates; + +use std::env; + +pub use channels::*; +pub use reminders::*; +use rocket::{http::CookieJar, serde::json::json, State}; +pub use roles::*; +use serenity::{ + client::Context, + model::id::{GuildId, RoleId}, +}; +pub use templates::*; + +use crate::{check_authorization, routes::JsonResult}; + +#[get("/api/guild/")] +pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State) -> JsonResult { + offline!(Ok(json!({ "patreon": true, "name": "Guild" }))); + check_authorization(cookies, ctx.inner(), id).await?; + + match GuildId(id).to_guild_cached(ctx.inner()) { + Some(guild) => { + let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) + .member(&ctx.inner(), guild.owner_id) + .await; + + let patreon = member_res.map_or(false, |member| { + member + .roles + .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) + }); + + Ok(json!({ "patreon": patreon, "name": guild.name })) + } + + None => json_err!("Bot not in guild"), + } +} diff --git a/web/src/routes/dashboard/api/guild/reminders.rs b/web/src/routes/dashboard/api/guild/reminders.rs new file mode 100644 index 0000000..1cb67d2 --- /dev/null +++ b/web/src/routes/dashboard/api/guild/reminders.rs @@ -0,0 +1,373 @@ +use rocket::{ + http::CookieJar, + serde::json::{json, Json}, + State, +}; +use serenity::{ + client::Context, + model::id::{ChannelId, GuildId, UserId}, +}; +use sqlx::{MySql, Pool}; + +use crate::{ + check_authorization, check_guild_subscription, check_subscription, + consts::MIN_INTERVAL, + guards::transaction::Transaction, + routes::{ + dashboard::{ + create_database_channel, create_reminder, DeleteReminder, PatchReminder, Reminder, + }, + JsonResult, + }, + Database, +}; + +#[post("/api/guild//reminders", data = "")] +pub async fn create_guild_reminder( + id: u64, + reminder: Json, + cookies: &CookieJar<'_>, + ctx: &State, + mut transaction: Transaction<'_>, +) -> JsonResult { + check_authorization(cookies, ctx.inner(), id).await?; + + let user_id = + cookies.get_private("userid").map(|c| c.value().parse::().ok()).flatten().unwrap(); + + match create_reminder( + ctx.inner(), + &mut transaction, + GuildId(id), + UserId(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//reminders")] +pub async fn get_reminders( + id: u64, + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> JsonResult { + check_authorization(cookies, ctx.inner(), id).await?; + + let channels_res = GuildId(id).channels(&ctx.inner()).await; + + match channels_res { + Ok(channels) => { + let channels = channels + .keys() + .into_iter() + .map(|k| k.as_u64().to_string()) + .collect::>() + .join(","); + + sqlx::query_as_unchecked!( + Reminder, + "SELECT + reminders.attachment, + reminders.attachment_name, + reminders.avatar, + channels.channel, + reminders.content, + reminders.embed_author, + reminders.embed_author_url, + reminders.embed_color, + reminders.embed_description, + reminders.embed_footer, + reminders.embed_footer_url, + reminders.embed_image_url, + reminders.embed_thumbnail_url, + reminders.embed_title, + 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 + LEFT 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//reminders", data = "")] +pub async fn edit_reminder( + id: u64, + reminder: Json, + ctx: &State, + mut transaction: Transaction<'_>, + pool: &State>, + 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::().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::::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::::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::::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 + ]); + } + } + } + + if reminder.channel > 0 { + let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner()); + match channel { + Some(channel) => { + let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); + + if !channel_matches_guild { + warn!( + "Error in `edit_reminder`: channel {:?} not found for guild {}", + reminder.channel, id + ); + + return Err(json!({"error": "Channel not found"})); + } + + let channel = create_database_channel( + ctx.inner(), + ChannelId(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::::None, "errors": vec!["Unknown error"]})) + } + } +} + +#[delete("/api/guild//reminders", data = "")] +pub async fn delete_reminder( + cookies: &CookieJar<'_>, + id: u64, + reminder: Json, + ctx: &State, + pool: &State>, +) -> JsonResult { + check_authorization(cookies, ctx.inner(), id).await?; + + 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"})) + } + } +} diff --git a/web/src/routes/dashboard/api/guild/roles.rs b/web/src/routes/dashboard/api/guild/roles.rs new file mode 100644 index 0000000..fdb665f --- /dev/null +++ b/web/src/routes/dashboard/api/guild/roles.rs @@ -0,0 +1,35 @@ +use rocket::{http::CookieJar, serde::json::json, State}; +use serde::Serialize; +use serenity::client::Context; + +use crate::{check_authorization, routes::JsonResult}; + +#[derive(Serialize)] +struct RoleInfo { + id: String, + name: String, +} + +#[get("/api/guild//roles")] +pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State) -> 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::>(); + + Ok(json!(roles)) + } + None => { + warn!("Could not fetch roles from {}", id); + + json_err!("Could not get roles") + } + } +} diff --git a/web/src/routes/dashboard/api/guild/templates.rs b/web/src/routes/dashboard/api/guild/templates.rs new file mode 100644 index 0000000..e05aad8 --- /dev/null +++ b/web/src/routes/dashboard/api/guild/templates.rs @@ -0,0 +1,181 @@ +use rocket::{ + http::CookieJar, + serde::json::{json, Json}, + State, +}; +use serenity::client::Context; +use sqlx::{MySql, Pool}; + +use crate::{ + 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//templates")] +pub async fn get_reminder_templates( + id: u64, + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> 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//templates", data = "")] +pub async fn create_reminder_template( + id: u64, + reminder_template: Json, + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> 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//templates", data = "")] +pub async fn delete_reminder_template( + id: u64, + delete_reminder_template: Json, + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> 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") + } + } +} diff --git a/web/src/routes/dashboard/api/mod.rs b/web/src/routes/dashboard/api/mod.rs new file mode 100644 index 0000000..56d877f --- /dev/null +++ b/web/src/routes/dashboard/api/mod.rs @@ -0,0 +1,2 @@ +pub mod guild; +pub mod user; diff --git a/web/src/routes/dashboard/api/user/guilds.rs b/web/src/routes/dashboard/api/user/guilds.rs new file mode 100644 index 0000000..e26a3f0 --- /dev/null +++ b/web/src/routes/dashboard/api/user/guilds.rs @@ -0,0 +1,81 @@ +use reqwest::Client; +use rocket::{ + http::CookieJar, + serde::json::{json, Value as JsonValue}, + State, +}; +use serde::{Deserialize, Serialize}; +use serenity::model::{id::GuildId, permissions::Permissions}; + +use crate::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, +} + +#[get("/api/user/guilds")] +pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State) -> 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::>().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::>(); + + 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"}) + } +} diff --git a/web/src/routes/dashboard/api/user/mod.rs b/web/src/routes/dashboard/api/user/mod.rs new file mode 100644 index 0000000..7e3079d --- /dev/null +++ b/web/src/routes/dashboard/api/user/mod.rs @@ -0,0 +1,97 @@ +mod guilds; + +use std::env; + +use chrono_tz::Tz; +pub use guilds::*; +use rocket::{ + http::CookieJar, + 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, +} + +#[derive(Deserialize)] +pub struct UpdateUser { + timezone: String, +} + +#[get("/api/user")] +pub async fn get_user_info( + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> 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::().ok()).flatten() + { + let member_res = GuildId(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("DiscordUser#0000".to_string(), |c| c.value().to_string()), + patreon: member_res.map_or(false, |member| { + member + .roles + .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) + }), + timezone, + }; + + json!(user_info) + } else { + json!({"error": "Not authorized"}) + } +} + +#[patch("/api/user", data = "")] +pub async fn update_user_info( + cookies: &CookieJar<'_>, + user: Json, + pool: &State>, +) -> JsonValue { + if let Some(user_id) = + cookies.get_private("userid").map(|u| u.value().parse::().ok()).flatten() + { + if user.timezone.parse::().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"}) + } +} diff --git a/web/src/routes/dashboard/api/user/models.rs b/web/src/routes/dashboard/api/user/models.rs new file mode 100644 index 0000000..339e7c3 --- /dev/null +++ b/web/src/routes/dashboard/api/user/models.rs @@ -0,0 +1,20 @@ +use std::env; + +use chrono_tz::Tz; +use reqwest::Client; +use rocket::{ + http::CookieJar, + serde::json::{json, Json, Value as JsonValue}, + State, +}; +use serde::{Deserialize, Serialize}; +use serenity::{ + client::Context, + model::{ + id::{GuildId, RoleId}, + permissions::Permissions, + }, +}; +use sqlx::{MySql, Pool}; + +use crate::{consts::DISCORD_API, routes::JsonResult}; diff --git a/web/src/routes/dashboard/api/user/reminders.rs b/web/src/routes/dashboard/api/user/reminders.rs new file mode 100644 index 0000000..39d32be --- /dev/null +++ b/web/src/routes/dashboard/api/user/reminders.rs @@ -0,0 +1,29 @@ +use std::env; + +use chrono_tz::Tz; +use reqwest::Client; +use rocket::{ + http::CookieJar, + serde::json::{json, Json, Value as JsonValue}, + State, +}; +use serde::{Deserialize, Serialize}; +use serenity::{ + client::Context, + model::{ + id::{GuildId, RoleId}, + permissions::Permissions, + }, +}; +use sqlx::{MySql, Pool}; + +use crate::{consts::DISCORD_API, routes::JsonResult}; + +#[get("/api/user/reminders")] +pub async fn get_reminders( + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> JsonResult { + Ok(json! {}) +} diff --git a/web/src/routes/dashboard/guild.rs b/web/src/routes/dashboard/guild.rs index bdf591c..8b13789 100644 --- a/web/src/routes/dashboard/guild.rs +++ b/web/src/routes/dashboard/guild.rs @@ -1,650 +1 @@ -use std::env; -use rocket::{ - http::CookieJar, - serde::json::{json, Json}, - State, -}; -use serde::Serialize; -use serenity::{ - client::Context, - model::{ - channel::GuildChannel, - id::{ChannelId, GuildId, RoleId, UserId}, - }, -}; -use sqlx::{MySql, Pool}; - -use crate::{ - check_authorization, check_guild_subscription, check_subscription, - 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, - MIN_INTERVAL, - }, - guards::transaction::Transaction, - routes::{ - dashboard::{ - create_database_channel, create_reminder, template_name_default, DeleteReminder, - DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate, - }, - JsonResult, - }, - Database, -}; - -#[derive(Serialize)] -struct ChannelInfo { - id: String, - name: String, - webhook_avatar: Option, - webhook_name: Option, -} - -#[get("/api/guild/")] -pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State) -> JsonResult { - offline!(Ok(json!({ "patreon": true, "name": "Guild" }))); - check_authorization(cookies, ctx.inner(), id).await?; - - match GuildId(id).to_guild_cached(ctx.inner()) { - Some(guild) => { - let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) - .member(&ctx.inner(), guild.owner_id) - .await; - - let patreon = member_res.map_or(false, |member| { - member - .roles - .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) - }); - - Ok(json!({ "patreon": patreon, "name": guild.name })) - } - - None => json_err!("Bot not in guild"), - } -} - -#[get("/api/guild//channels")] -pub async fn get_guild_channels( - id: u64, - cookies: &CookieJar<'_>, - ctx: &State, -) -> 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(id).to_guild_cached(ctx.inner()) { - Some(guild) => { - let mut channels = guild - .channels - .iter() - .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) - .filter(|(_, channel)| channel.is_text_based()) - .collect::>(); - - channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); - - 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::>(); - - Ok(json!(channel_info)) - } - - None => json_err!("Bot not in guild"), - } -} - -#[derive(Serialize)] -struct RoleInfo { - id: String, - name: String, -} - -#[get("/api/guild//roles")] -pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State) -> 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::>(); - - Ok(json!(roles)) - } - None => { - warn!("Could not fetch roles from {}", id); - - json_err!("Could not get roles") - } - } -} - -#[get("/api/guild//templates")] -pub async fn get_reminder_templates( - id: u64, - cookies: &CookieJar<'_>, - ctx: &State, - pool: &State>, -) -> 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//templates", data = "")] -pub async fn create_reminder_template( - id: u64, - reminder_template: Json, - cookies: &CookieJar<'_>, - ctx: &State, - pool: &State>, -) -> 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//templates", data = "")] -pub async fn delete_reminder_template( - id: u64, - delete_reminder_template: Json, - cookies: &CookieJar<'_>, - ctx: &State, - pool: &State>, -) -> 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") - } - } -} - -#[post("/api/guild//reminders", data = "")] -pub async fn create_guild_reminder( - id: u64, - reminder: Json, - cookies: &CookieJar<'_>, - ctx: &State, - mut transaction: Transaction<'_>, -) -> JsonResult { - check_authorization(cookies, ctx.inner(), id).await?; - - let user_id = - cookies.get_private("userid").map(|c| c.value().parse::().ok()).flatten().unwrap(); - - match create_reminder( - ctx.inner(), - &mut transaction, - GuildId(id), - UserId(user_id), - reminder.into_inner(), - ) - .await - { - Ok(r) => match transaction.commit().await { - Ok(_) => Ok(r), - Err(e) => { - warn!("Could'nt commit transaction: {:?}", e); - json_err!("Couldn't commit transaction.") - } - }, - - Err(e) => Err(e), - } -} - -#[get("/api/guild//reminders")] -pub async fn get_reminders( - id: u64, - cookies: &CookieJar<'_>, - ctx: &State, - pool: &State>, -) -> JsonResult { - check_authorization(cookies, ctx.inner(), id).await?; - - let channels_res = GuildId(id).channels(&ctx.inner()).await; - - match channels_res { - Ok(channels) => { - let channels = channels - .keys() - .into_iter() - .map(|k| k.as_u64().to_string()) - .collect::>() - .join(","); - - sqlx::query_as_unchecked!( - Reminder, - "SELECT - reminders.attachment, - reminders.attachment_name, - reminders.avatar, - channels.channel, - reminders.content, - reminders.embed_author, - reminders.embed_author_url, - reminders.embed_color, - reminders.embed_description, - reminders.embed_footer, - reminders.embed_footer_url, - reminders.embed_image_url, - reminders.embed_thumbnail_url, - reminders.embed_title, - 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 - LEFT 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//reminders", data = "")] -pub(crate) async fn edit_reminder( - id: u64, - reminder: Json, - ctx: &State, - mut transaction: Transaction<'_>, - pool: &State>, - 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::().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::::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::::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::::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 - ]); - } - } - } - - if reminder.channel > 0 { - let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner()); - match channel { - Some(channel) => { - let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); - - if !channel_matches_guild { - warn!( - "Error in `edit_reminder`: channel {:?} not found for guild {}", - reminder.channel, id - ); - - return Err(json!({"error": "Channel not found"})); - } - - let channel = create_database_channel( - ctx.inner(), - ChannelId(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::::None, "errors": vec!["Unknown error"]})) - } - } -} - -#[delete("/api/guild//reminders", data = "")] -pub async fn delete_reminder( - cookies: &CookieJar<'_>, - id: u64, - reminder: Json, - ctx: &State, - pool: &State>, -) -> JsonResult { - check_authorization(cookies, ctx.inner(), id).await?; - - 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"})) - } - } -} diff --git a/web/src/routes/dashboard/mod.rs b/web/src/routes/dashboard/mod.rs index 56a19d5..80e3e7a 100644 --- a/web/src/routes/dashboard/mod.rs +++ b/web/src/routes/dashboard/mod.rs @@ -25,9 +25,9 @@ use crate::{ Error, }; +pub mod api; pub mod export; pub mod guild; -pub mod user; type Unset = Option; diff --git a/web/src/routes/dashboard/user.rs b/web/src/routes/dashboard/user.rs deleted file mode 100644 index 454756a..0000000 --- a/web/src/routes/dashboard/user.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::env; - -use chrono_tz::Tz; -use reqwest::Client; -use rocket::{ - http::CookieJar, - serde::json::{json, Json, Value as JsonValue}, - State, -}; -use serde::{Deserialize, Serialize}; -use serenity::{ - client::Context, - model::{ - id::{GuildId, RoleId}, - permissions::Permissions, - }, -}; -use sqlx::{MySql, Pool}; - -use crate::consts::DISCORD_API; - -#[derive(Serialize)] -struct UserInfo { - name: String, - patreon: bool, - timezone: Option, -} - -#[derive(Deserialize)] -pub struct UpdateUser { - timezone: String, -} - -#[derive(Serialize)] -struct GuildInfo { - id: String, - name: String, -} - -#[derive(Deserialize)] -pub struct PartialGuild { - pub id: GuildId, - pub icon: Option, - pub name: String, - #[serde(default)] - pub owner: bool, - #[serde(rename = "permissions_new")] - pub permissions: Option, -} - -#[get("/api/user")] -pub async fn get_user_info( - cookies: &CookieJar<'_>, - ctx: &State, - pool: &State>, -) -> 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::().ok()).flatten() - { - let member_res = GuildId(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("DiscordUser#0000".to_string(), |c| c.value().to_string()), - patreon: member_res.map_or(false, |member| { - member - .roles - .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) - }), - timezone, - }; - - json!(user_info) - } else { - json!({"error": "Not authorized"}) - } -} - -#[patch("/api/user", data = "")] -pub async fn update_user_info( - cookies: &CookieJar<'_>, - user: Json, - pool: &State>, -) -> JsonValue { - if let Some(user_id) = - cookies.get_private("userid").map(|u| u.value().parse::().ok()).flatten() - { - if user.timezone.parse::().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"}) - } -} - -#[get("/api/user/guilds")] -pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State) -> 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::>().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::>(); - - 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"}) - } -} diff --git a/web/static/js/main.js b/web/static/js/main.js index cb0d46d..e734116 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -463,15 +463,16 @@ document.addEventListener("guildSwitched", async (e) => { let hasError = false; - if ($anchor !== null) { - $anchor.classList.add("is-active"); - } - if (pane() === null) { window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`); } switch_pane(pane()); + + if ($anchor !== null) { + $anchor.classList.add("is-active"); + } + reset_guild_pane(); if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { diff --git a/web/templates/reminder_dashboard/reminder_errors.html.tera b/web/templates/reminder_dashboard/reminder_errors.html.tera index 70fdf45..a53f2f8 100644 --- a/web/templates/reminder_dashboard/reminder_errors.html.tera +++ b/web/templates/reminder_dashboard/reminder_errors.html.tera @@ -2,4 +2,4 @@ - +