From 1d8fd39d1344c43794f28c22d3e3715350d1ee6c Mon Sep 17 00:00:00 2001 From: jude Date: Mon, 20 Oct 2025 18:36:28 +0100 Subject: [PATCH] Apply patreon sharing across web/bot --- src/commands/patreon/link.rs | 22 ++--------- src/commands/patreon/unlink.rs | 19 ++++------ src/models/reminder/mod.rs | 6 ++- src/time_parser.rs | 1 - src/utils.rs | 16 ++++---- src/web/mod.rs | 33 ----------------- src/web/routes/dashboard/api/guild/mod.rs | 37 +++++++++++-------- .../routes/dashboard/api/guild/reminders.rs | 12 ++++-- src/web/routes/dashboard/api/user/models.rs | 4 +- .../routes/dashboard/api/user/reminders.rs | 6 ++- src/web/routes/dashboard/mod.rs | 6 +-- 11 files changed, 61 insertions(+), 101 deletions(-) diff --git a/src/commands/patreon/link.rs b/src/commands/patreon/link.rs index 4410d77..cd63efb 100644 --- a/src/commands/patreon/link.rs +++ b/src/commands/patreon/link.rs @@ -23,28 +23,12 @@ impl Recordable for Options { return Ok(()); } - let existing_link = sqlx::query!( - "SELECT linked_at FROM patreon_link WHERE user_id = ? AND linked_at > NOW() - INTERVAL 4 WEEK", - user_id.get() - ) - .fetch_optional(&ctx.data().database) - .await?; - - if existing_link.is_some() { - ctx.send( - CreateReply::default() - .content("❌ You can only link once every 4 weeks. Please try again later.") - .ephemeral(true), - ) - .await?; - return Ok(()); - } - // Insert or update the patreon_link entry sqlx::query!( "INSERT INTO patreon_link (user_id, guild_id, linked_at) VALUES (?, ?, NOW()) - ON DUPLICATE KEY UPDATE user_id = user_id", + ON DUPLICATE KEY UPDATE guild_id = ?", user_id.get(), + guild_id.get(), guild_id.get() ) .execute(&ctx.data().database) @@ -61,7 +45,7 @@ impl Recordable for Options { } } -/// Link your Patreon subscription to this server. This command can be run once every four weeks +/// Link your Patreon subscription to this server to allow other users Patreon access. #[poise::command( slash_command, rename = "link", diff --git a/src/commands/patreon/unlink.rs b/src/commands/patreon/unlink.rs index 1a0cbcf..f8f4c31 100644 --- a/src/commands/patreon/unlink.rs +++ b/src/commands/patreon/unlink.rs @@ -11,29 +11,24 @@ pub struct Options; impl Recordable for Options { async fn run(self, ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().ok_or("This command must be used in a server")?; let user_id = ctx.author().id; // Remove the patreon_link entry - let result = sqlx::query!( - "DELETE FROM patreon_link WHERE user_id = ? AND guild_id = ?", - user_id.get(), - guild_id.get() - ) - .execute(&ctx.data().database) - .await?; + let result = sqlx::query!("DELETE FROM patreon_link WHERE user_id = ?", user_id.get()) + .execute(&ctx.data().database) + .await?; if result.rows_affected() > 0 { ctx.send( CreateReply::default() - .content("✅ Successfully unlinked your Patreon subscription from this server!") + .content("✅ Successfully unlinked your Patreon subscription!") .ephemeral(true), ) .await?; } else { ctx.send( CreateReply::default() - .content("❌ No existing Patreon link found for this server.") + .content("❌ No existing Patreon link found.") .ephemeral(true), ) .await?; @@ -43,12 +38,12 @@ impl Recordable for Options { } } -/// Unlink your Patreon subscription from this server +/// Unlink your Patreon subscription #[poise::command( slash_command, rename = "unlink", identifying_name = "patreon_unlink", - guild_only = true + guild_only = false )] pub async fn unlink(ctx: Context<'_>) -> Result<(), Error> { (Options {}).run(ctx).await diff --git a/src/models/reminder/mod.rs b/src/models/reminder/mod.rs index 8d7a6a6..6f60fdd 100644 --- a/src/models/reminder/mod.rs +++ b/src/models/reminder/mod.rs @@ -532,7 +532,9 @@ pub async fn create_reminder( }; let (processed_interval, processed_expires) = if let Some(repeat) = &interval { - if check_subscription(&ctx, ctx.author().id, ctx.guild_id()).await { + if check_subscription(&ctx, &ctx.data().database, ctx.author().id, ctx.guild_id()) + .await + { ( parse_duration(repeat) .or_else(|_| parse_duration(&format!("1 {}", repeat))) @@ -547,7 +549,7 @@ pub async fn create_reminder( ) } else { ctx.send(CreateReply::default().content( - "`repeat` is only available to Patreon subscribers or self-hosted users", + "`interval` is only available to Patreon subscribers or self-hosted users", )) .await?; diff --git a/src/time_parser.rs b/src/time_parser.rs index c7fe36a..6d1aec4 100644 --- a/src/time_parser.rs +++ b/src/time_parser.rs @@ -7,7 +7,6 @@ use std::{ use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono_tz::Tz; use cron_parser::parse; -use std::str::FromStr; use tokio::process::Command; use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION}; diff --git a/src/utils.rs b/src/utils.rs index 8ee1d23..3dadb27 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,24 +9,25 @@ use poise::{ use crate::{ consts::{CNC_GUILD, SUBSCRIPTION_ROLES}, - ApplicationContext, Context, Error, + ApplicationContext, Context, Database, Error, }; /// Check if this user/guild combination should be considered subscribed. /// If the guild has a patreon linked, check the user involved in the link. /// Otherwise, check the user and the guild's owner pub async fn check_subscription( - ctx: &Context<'_>, + ctx: impl CacheHttp, + database: impl Executor<'_, Database = Database>, user_id: UserId, guild_id: Option, ) -> bool { if let Some(subscription_guild) = *CNC_GUILD { - let user_subscribed = check_user_subscription(ctx, user_id).await; + let user_subscribed = check_user_subscription(&ctx, user_id).await; let owner_subscribed = match guild_id { Some(guild_id) => { if let Some(owner) = ctx.cache().unwrap().guild(guild_id).map(|g| g.owner_id) { - check_user_subscription(ctx, owner).await + check_user_subscription(&ctx, owner).await } else { false } @@ -38,13 +39,13 @@ pub async fn check_subscription( let link_subscribed = match guild_id { Some(guild_id) => { if let Ok(row) = sqlx::query!( - "SELECT user_id FROM patreon_link WHERE user_id = ?", + "SELECT user_id FROM patreon_link WHERE guild_id = ?", guild_id.get() ) - .fetch_one(&ctx.data().database) + .fetch_one(database) .await { - check_user_subscription(ctx, row.user_id).await + check_user_subscription(&ctx, row.user_id).await } else { false } @@ -116,6 +117,7 @@ pub trait Extract { } pub use extract_derive::Extract; +use sqlx::Executor; macro_rules! extract_arg { ($ctx:ident, $name:ident, String) => { diff --git a/src/web/mod.rs b/src/web/mod.rs index 19be638..0183f88 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -233,39 +233,6 @@ pub async fn initialize( Ok(()) } -pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into) -> bool { - offline!(true); - - if let Some(subscription_guild) = *CNC_GUILD { - let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await; - - if let Ok(member) = guild_member { - for role in member.roles { - if SUBSCRIPTION_ROLES.contains(&role.get()) { - return true; - } - } - } - - false - } else { - true - } -} - -pub async fn check_guild_subscription( - cache_http: impl CacheHttp, - guild_id: impl Into, -) -> bool { - offline!(true); - - if let Some(owner) = cache_http.cache().unwrap().guild(guild_id).map(|guild| guild.owner_id) { - check_subscription(&cache_http, owner).await - } else { - false - } -} - pub async fn check_authorization( cookies: &CookieJar<'_>, ctx: &Context, diff --git a/src/web/routes/dashboard/api/guild/mod.rs b/src/web/routes/dashboard/api/guild/mod.rs index 33e2a79..f77437b 100644 --- a/src/web/routes/dashboard/api/guild/mod.rs +++ b/src/web/routes/dashboard/api/guild/mod.rs @@ -7,38 +7,43 @@ pub mod todos; use std::env; +use crate::utils::check_subscription; +use crate::web::guards::transaction::Transaction; +use crate::web::{check_authorization, routes::JsonResult}; pub use channels::get_guild_channels; pub use emojis::get_guild_emojis; pub use reminders::*; use rocket::{get, http::CookieJar, serde::json::json, State}; pub use roles::get_guild_roles; +use serenity::all::UserId; use serenity::{ client::Context, model::id::{GuildId, RoleId}, }; pub use templates::*; -use crate::web::{check_authorization, routes::JsonResult}; - #[get("/api/guild/")] -pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State) -> JsonResult { +pub async fn get_guild_info( + id: u64, + cookies: &CookieJar<'_>, + ctx: &State, + mut transaction: Transaction<'_>, +) -> 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 user_id = + cookies.get_private("userid").map(|c| c.value().parse::().ok()).flatten().unwrap(); - let patreon = member_res.map_or(false, |member| { - member - .roles - .contains(&RoleId::new(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) - }); + match GuildId::new(id).to_guild_cached(ctx.inner()).map(|guild| guild.name.clone()) { + Some(name) => { + let patreon = check_subscription( + ctx.inner(), + transaction.executor(), + UserId::from(user_id), + Some(GuildId::from(id)), + ) + .await; Ok(json!({ "patreon": patreon, "name": name })) } diff --git a/src/web/routes/dashboard/api/guild/reminders.rs b/src/web/routes/dashboard/api/guild/reminders.rs index 691f329..ac1792e 100644 --- a/src/web/routes/dashboard/api/guild/reminders.rs +++ b/src/web/routes/dashboard/api/guild/reminders.rs @@ -12,8 +12,9 @@ use serenity::{ }; use sqlx::{MySql, Pool}; +use crate::utils::check_subscription; use crate::web::{ - check_authorization, check_guild_subscription, check_subscription, + check_authorization, consts::MIN_INTERVAL, guards::transaction::Transaction, routes::{ @@ -186,8 +187,13 @@ pub async fn edit_reminder( || 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 + if check_subscription( + ctx.inner(), + transaction.executor(), + UserId::from(user_id), + Some(GuildId::from(id)), + ) + .await { let new_interval_length = match reminder.interval_days { Some(interval) => interval.unwrap_or(0), diff --git a/src/web/routes/dashboard/api/user/models.rs b/src/web/routes/dashboard/api/user/models.rs index 8705fef..f6f4fc0 100644 --- a/src/web/routes/dashboard/api/user/models.rs +++ b/src/web/routes/dashboard/api/user/models.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use serenity::{client::Context, model::id::UserId}; use sqlx::types::Json; +use crate::utils::check_subscription; 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, @@ -131,7 +131,7 @@ pub async fn create_reminder( || reminder.interval_days.is_some() || reminder.interval_months.is_some() { - if !check_subscription(&ctx, user_id).await { + if !check_subscription(&ctx, transaction.executor(), user_id, None).await { return Err(json!({"error": "Patreon is required to set intervals"})); } } diff --git a/src/web/routes/dashboard/api/user/reminders.rs b/src/web/routes/dashboard/api/user/reminders.rs index 0787374..ec663ff 100644 --- a/src/web/routes/dashboard/api/user/reminders.rs +++ b/src/web/routes/dashboard/api/user/reminders.rs @@ -9,8 +9,8 @@ use rocket::{ use serenity::{client::Context, model::id::UserId}; use sqlx::{MySql, Pool}; +use crate::utils::check_subscription; use crate::web::{ - check_subscription, guards::transaction::Transaction, routes::{ dashboard::{ @@ -162,7 +162,9 @@ pub async fn edit_reminder( || reminder.interval_months.flatten().is_some() || reminder.interval_seconds.flatten().is_some() { - if check_subscription(&ctx.inner(), user_id).await { + if check_subscription(&ctx.inner(), transaction.executor(), UserId::from(user_id), None) + .await + { let new_interval_length = match reminder.interval_days { Some(interval) => interval.unwrap_or(0), None => sqlx::query!( diff --git a/src/web/routes/dashboard/mod.rs b/src/web/routes/dashboard/mod.rs index 364abb7..1ee5c74 100644 --- a/src/web/routes/dashboard/mod.rs +++ b/src/web/routes/dashboard/mod.rs @@ -20,9 +20,9 @@ use serenity::{ use sqlx::types::Json; use sqlx::FromRow; +use crate::utils::check_subscription; 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, @@ -477,9 +477,7 @@ pub(crate) async fn create_reminder( || reminder.interval_days.is_some() || reminder.interval_months.is_some() { - if !check_guild_subscription(&ctx, guild_id).await - && !check_subscription(&ctx, user_id).await - { + if !check_subscription(&ctx, transaction.executor(), user_id, Some(guild_id)).await { return Err(json!({"error": "Patreon is required to set intervals"})); } }