Change routing. Remove a macro

This commit is contained in:
jude 2023-10-05 18:54:53 +01:00
parent 2681280a39
commit e3d3418f99
8 changed files with 132 additions and 93 deletions

View File

@ -7,20 +7,20 @@ use sqlx::Pool;
use crate::Database; use crate::Database;
pub(crate) struct Transaction<'a>(sqlx::Transaction<'a, Database>); pub struct Transaction<'a>(sqlx::Transaction<'a, Database>);
impl Transaction<'_> { impl Transaction<'_> {
pub(crate) fn executor(&mut self) -> impl sqlx::Executor<'_, Database = Database> { pub fn executor(&mut self) -> impl sqlx::Executor<'_, Database = Database> {
&mut *(self.0) &mut *(self.0)
} }
pub(crate) async fn commit(self) -> Result<(), sqlx::Error> { pub async fn commit(self) -> Result<(), sqlx::Error> {
self.0.commit().await self.0.commit().await
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum TransactionError { pub enum TransactionError {
Error(sqlx::Error), Error(sqlx::Error),
Missing, Missing,
} }

View File

@ -11,7 +11,12 @@ mod routes;
use std::{env, path::Path}; use std::{env, path::Path};
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use rocket::{fs::FileServer, serde::json::Value as JsonValue, tokio::sync::broadcast::Sender}; use rocket::{
fs::FileServer,
http::CookieJar,
serde::json::{json, Value as JsonValue},
tokio::sync::broadcast::Sender,
};
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serenity::{ use serenity::{
client::Context, client::Context,
@ -186,3 +191,68 @@ pub async fn check_guild_subscription(
false false
} }
} }
pub async fn check_authorization(
cookies: &CookieJar<'_>,
ctx: &Context,
guild: u64,
) -> Result<(), JsonValue> {
let user_id = cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
if std::env::var("OFFLINE").map_or(true, |v| v != "1") {
match user_id {
Some(user_id) => {
println!("{:?}", std::env::var("ADMIN_ID"));
println!("{:?}", user_id);
let admin_id = std::env::var("ADMIN_ID")
.map_or(false, |u| u.parse::<u64>().map_or(false, |u| u == user_id));
if admin_id {
return Ok(());
}
match GuildId(guild).to_guild_cached(ctx) {
Some(guild) => {
let member_res = guild.member(ctx, UserId(user_id)).await;
match member_res {
Err(_) => {
return Err(json!({"error": "User not in guild"}));
}
Ok(member) => {
let permissions_res = member.permissions(ctx);
match permissions_res {
Err(_) => {
return Err(json!({"error": "Couldn't fetch permissions"}));
}
Ok(permissions) => {
if !(permissions.manage_messages()
|| permissions.manage_guild()
|| permissions.administrator())
{
return Err(json!({"error": "Incorrect permissions"}));
}
}
}
}
}
}
None => {
return Err(json!({"error": "Bot not in guild"}));
}
}
}
None => {
return Err(json!({"error": "User not authorized"}));
}
}
}
Ok(())
}

View File

@ -54,56 +54,6 @@ macro_rules! check_url_opt {
}; };
} }
macro_rules! check_authorization {
($cookies:expr, $ctx:expr, $guild:expr) => {
use serenity::model::id::UserId;
let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
if std::env::var("OFFLINE").map_or(true, |v| v != "1") {
match user_id {
Some(user_id) => {
match GuildId($guild).to_guild_cached($ctx) {
Some(guild) => {
let member_res = guild.member($ctx, UserId(user_id)).await;
match member_res {
Err(_) => {
return Err(json!({"error": "User not in guild"}));
}
Ok(member) => {
let permissions_res = member.permissions($ctx);
match permissions_res {
Err(_) => {
return Err(json!({"error": "Couldn't fetch permissions"}));
}
Ok(permissions) => {
if !(permissions.manage_messages() || permissions.manage_guild() || permissions.administrator()) {
return Err(json!({"error": "Incorrect permissions"}));
}
}
}
}
}
}
None => {
return Err(json!({"error": "Bot not in guild"}));
}
}
}
None => {
return Err(json!({"error": "User not authorized"}));
}
}
}
}
}
macro_rules! update_field { macro_rules! update_field {
($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => { ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => {
if let Some(value) = &$reminder.$field { if let Some(value) = &$reminder.$field {

View File

@ -6,11 +6,12 @@ use rocket::{
}; };
use serenity::{ use serenity::{
client::Context, client::Context,
model::id::{ChannelId, GuildId}, model::id::{ChannelId, GuildId, UserId},
}; };
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use crate::{ use crate::{
check_authorization,
guards::transaction::Transaction, guards::transaction::Transaction,
routes::{ routes::{
dashboard::{ dashboard::{
@ -28,7 +29,7 @@ pub async fn export_reminders(
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
@ -128,7 +129,7 @@ pub(crate) async fn import_reminders(
ctx: &State<Context>, ctx: &State<Context>,
mut transaction: Transaction<'_>, mut transaction: Transaction<'_>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let user_id = let user_id =
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
@ -231,7 +232,7 @@ pub async fn export_todos(
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
@ -286,7 +287,7 @@ pub async fn import_todos(
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let channels_res = GuildId(id).channels(&ctx.inner()).await; let channels_res = GuildId(id).channels(&ctx.inner()).await;
@ -381,7 +382,7 @@ pub async fn export_reminder_templates(
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);

View File

@ -10,13 +10,13 @@ use serenity::{
client::Context, client::Context,
model::{ model::{
channel::GuildChannel, channel::GuildChannel,
id::{ChannelId, GuildId, RoleId}, id::{ChannelId, GuildId, RoleId, UserId},
}, },
}; };
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use crate::{ use crate::{
check_guild_subscription, check_subscription, check_authorization, check_guild_subscription, check_subscription,
consts::{ consts::{
MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
@ -49,7 +49,7 @@ pub async fn get_guild_patreon(
ctx: &State<Context>, ctx: &State<Context>,
) -> JsonResult { ) -> JsonResult {
offline!(Ok(json!({ "patreon": true }))); offline!(Ok(json!({ "patreon": true })));
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
match GuildId(id).to_guild_cached(ctx.inner()) { match GuildId(id).to_guild_cached(ctx.inner()) {
Some(guild) => { Some(guild) => {
@ -82,7 +82,7 @@ pub async fn get_guild_channels(
webhook_avatar: None, webhook_avatar: None,
webhook_name: None, webhook_name: None,
}]))); }])));
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
match GuildId(id).to_guild_cached(ctx.inner()) { match GuildId(id).to_guild_cached(ctx.inner()) {
Some(guild) => { Some(guild) => {
@ -121,7 +121,7 @@ struct RoleInfo {
#[get("/api/guild/<id>/roles")] #[get("/api/guild/<id>/roles")]
pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { 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() }]))); offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }])));
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let roles_res = ctx.cache.guild_roles(id); let roles_res = ctx.cache.guild_roles(id);
@ -149,7 +149,7 @@ pub async fn get_reminder_templates(
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
match sqlx::query_as_unchecked!( match sqlx::query_as_unchecked!(
ReminderTemplate, ReminderTemplate,
@ -176,7 +176,7 @@ pub async fn create_reminder_template(
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
// validate lengths // validate lengths
check_length!(MAX_CONTENT_LENGTH, reminder_template.content); check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
@ -283,7 +283,7 @@ pub async fn delete_reminder_template(
ctx: &State<Context>, ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, ctx.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
match sqlx::query!( match sqlx::query!(
"DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?", "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
@ -304,20 +304,20 @@ pub async fn delete_reminder_template(
} }
#[post("/api/guild/<id>/reminders", data = "<reminder>")] #[post("/api/guild/<id>/reminders", data = "<reminder>")]
pub(crate) async fn create_guild_reminder( pub async fn create_guild_reminder(
id: u64, id: u64,
reminder: Json<Reminder>, reminder: Json<Reminder>,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
serenity_context: &State<Context>, ctx: &State<Context>,
mut transaction: Transaction<'_>, mut transaction: Transaction<'_>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, serenity_context.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let user_id = let user_id =
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
match create_reminder( match create_reminder(
serenity_context.inner(), ctx.inner(),
&mut transaction, &mut transaction,
GuildId(id), GuildId(id),
UserId(user_id), UserId(user_id),
@ -342,10 +342,9 @@ pub async fn get_reminders(
id: u64, id: u64,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &State<Context>, ctx: &State<Context>,
serenity_context: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, serenity_context.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let channels_res = GuildId(id).channels(&ctx.inner()).await; let channels_res = GuildId(id).channels(&ctx.inner()).await;
@ -413,12 +412,12 @@ pub async fn get_reminders(
pub(crate) async fn edit_reminder( pub(crate) async fn edit_reminder(
id: u64, id: u64,
reminder: Json<PatchReminder>, reminder: Json<PatchReminder>,
serenity_context: &State<Context>, ctx: &State<Context>,
mut transaction: Transaction<'_>, mut transaction: Transaction<'_>,
pool: &State<Pool<Database>>, pool: &State<Pool<Database>>,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, serenity_context.inner(), id); check_authorization(cookies, ctx.inner(), id).await?;
let mut error = vec![]; let mut error = vec![];
@ -460,8 +459,8 @@ pub(crate) async fn edit_reminder(
|| reminder.interval_months.flatten().is_some() || reminder.interval_months.flatten().is_some()
|| reminder.interval_seconds.flatten().is_some() || reminder.interval_seconds.flatten().is_some()
{ {
if check_guild_subscription(&serenity_context.inner(), id).await if check_guild_subscription(&ctx.inner(), id).await
|| check_subscription(&serenity_context.inner(), user_id).await || check_subscription(&ctx.inner(), user_id).await
{ {
let new_interval_length = match reminder.interval_days { let new_interval_length = match reminder.interval_days {
Some(interval) => interval.unwrap_or(0), Some(interval) => interval.unwrap_or(0),
@ -520,7 +519,7 @@ pub(crate) async fn edit_reminder(
} }
if reminder.channel > 0 { if reminder.channel > 0 {
let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner()); let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner());
match channel { match channel {
Some(channel) => { Some(channel) => {
let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id);
@ -535,7 +534,7 @@ pub(crate) async fn edit_reminder(
} }
let channel = create_database_channel( let channel = create_database_channel(
serenity_context.inner(), ctx.inner(),
ChannelId(reminder.channel), ChannelId(reminder.channel),
&mut transaction, &mut transaction,
) )
@ -630,11 +629,16 @@ pub(crate) async fn edit_reminder(
} }
} }
#[delete("/api/guild/<_>/reminders", data = "<reminder>")] #[delete("/api/guild/<id>/reminders", data = "<reminder>")]
pub async fn delete_reminder( pub async fn delete_reminder(
cookies: &CookieJar<'_>,
id: u64,
reminder: Json<DeleteReminder>, reminder: Json<DeleteReminder>,
ctx: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
check_authorization(cookies, ctx.inner(), id).await?;
match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid)
.execute(pool.inner()) .execute(pool.inner())
.await .await

View File

@ -668,7 +668,7 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirec
} }
} }
#[get("/<_>")] #[get("/<_..>")]
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
if cookies.get_private("userid").is_some() { if cookies.get_private("userid").is_some() {
let map: HashMap<&str, String> = HashMap::new(); let map: HashMap<&str, String> = HashMap::new();

View File

@ -33,7 +33,16 @@ let globalPatreon = false;
let guildPatreon = false; let guildPatreon = false;
function guildId() { function guildId() {
return document.querySelector(".guildList a.is-active").dataset["guild"]; return window.location.pathname.match(/dashboard\/(\d+)/)[1];
}
function pane() {
const match = window.location.pathname.match(/dashboard\/\d+\/(.+)/);
if (match === null) {
return null;
} else {
return match[1];
}
} }
function colorToInt(r, g, b) { function colorToInt(r, g, b) {
@ -454,15 +463,16 @@ document.addEventListener("guildSwitched", async (e) => {
let hasError = false; let hasError = false;
if ($anchor === null) { if ($anchor !== null) {
switch_pane("user-error"); $anchor.classList.add("is-active");
hasError = true;
return;
} }
switch_pane($anchor.dataset["pane"]); if (pane() === null) {
window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`);
}
switch_pane(pane());
reset_guild_pane(); reset_guild_pane();
$anchor.classList.add("is-active");
if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
document document
@ -695,11 +705,15 @@ document.addEventListener("DOMContentLoaded", async () => {
); );
$anchor.dataset["guild"] = guild.id; $anchor.dataset["guild"] = guild.id;
$anchor.dataset["name"] = guild.name; $anchor.dataset["name"] = guild.name;
$anchor.href = `/dashboard/${guild.id}?name=${guild.name}`; $anchor.href = `/dashboard/${guild.id}/reminders`;
$anchor.addEventListener("click", async (e) => { $anchor.addEventListener("click", async (e) => {
e.preventDefault(); e.preventDefault();
window.history.pushState({}, "", `/dashboard/${guild.id}`); window.history.pushState(
{},
"",
`/dashboard/${guild.id}/reminders`
);
const event = new CustomEvent("guildSwitched", { const event = new CustomEvent("guildSwitched", {
detail: { detail: {
guild_name: guild.name, guild_name: guild.name,

View File

@ -325,7 +325,7 @@
<p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p>
</div> </div>
</section> </section>
<section id="guild" class="is-hidden"> <section id="reminders" class="is-hidden">
{% include "reminder_dashboard/reminder_dashboard" %} {% include "reminder_dashboard/reminder_dashboard" %}
</section> </section>
<section id="reminder-errors" class="is-hidden"> <section id="reminder-errors" class="is-hidden">