Apply patreon sharing across web/bot

This commit is contained in:
jude
2025-10-20 18:36:28 +01:00
parent 91310d47d3
commit 1d8fd39d13
11 changed files with 61 additions and 101 deletions

View File

@@ -23,28 +23,12 @@ impl Recordable for Options {
return Ok(()); 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 // Insert or update the patreon_link entry
sqlx::query!( sqlx::query!(
"INSERT INTO patreon_link (user_id, guild_id, linked_at) VALUES (?, ?, NOW()) "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(), user_id.get(),
guild_id.get(),
guild_id.get() guild_id.get()
) )
.execute(&ctx.data().database) .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( #[poise::command(
slash_command, slash_command,
rename = "link", rename = "link",

View File

@@ -11,29 +11,24 @@ pub struct Options;
impl Recordable for Options { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> { 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; let user_id = ctx.author().id;
// Remove the patreon_link entry // Remove the patreon_link entry
let result = sqlx::query!( let result = sqlx::query!("DELETE FROM patreon_link WHERE user_id = ?", user_id.get())
"DELETE FROM patreon_link WHERE user_id = ? AND guild_id = ?",
user_id.get(),
guild_id.get()
)
.execute(&ctx.data().database) .execute(&ctx.data().database)
.await?; .await?;
if result.rows_affected() > 0 { if result.rows_affected() > 0 {
ctx.send( ctx.send(
CreateReply::default() CreateReply::default()
.content("✅ Successfully unlinked your Patreon subscription from this server!") .content("✅ Successfully unlinked your Patreon subscription!")
.ephemeral(true), .ephemeral(true),
) )
.await?; .await?;
} else { } else {
ctx.send( ctx.send(
CreateReply::default() CreateReply::default()
.content("❌ No existing Patreon link found for this server.") .content("❌ No existing Patreon link found.")
.ephemeral(true), .ephemeral(true),
) )
.await?; .await?;
@@ -43,12 +38,12 @@ impl Recordable for Options {
} }
} }
/// Unlink your Patreon subscription from this server /// Unlink your Patreon subscription
#[poise::command( #[poise::command(
slash_command, slash_command,
rename = "unlink", rename = "unlink",
identifying_name = "patreon_unlink", identifying_name = "patreon_unlink",
guild_only = true guild_only = false
)] )]
pub async fn unlink(ctx: Context<'_>) -> Result<(), Error> { pub async fn unlink(ctx: Context<'_>) -> Result<(), Error> {
(Options {}).run(ctx).await (Options {}).run(ctx).await

View File

@@ -532,7 +532,9 @@ pub async fn create_reminder(
}; };
let (processed_interval, processed_expires) = if let Some(repeat) = &interval { 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) parse_duration(repeat)
.or_else(|_| parse_duration(&format!("1 {}", repeat))) .or_else(|_| parse_duration(&format!("1 {}", repeat)))
@@ -547,7 +549,7 @@ pub async fn create_reminder(
) )
} else { } else {
ctx.send(CreateReply::default().content( 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?; .await?;

View File

@@ -7,7 +7,6 @@ use std::{
use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono::{DateTime, Datelike, Timelike, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use cron_parser::parse; use cron_parser::parse;
use std::str::FromStr;
use tokio::process::Command; use tokio::process::Command;
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION}; use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};

View File

@@ -9,24 +9,25 @@ use poise::{
use crate::{ use crate::{
consts::{CNC_GUILD, SUBSCRIPTION_ROLES}, consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
ApplicationContext, Context, Error, ApplicationContext, Context, Database, Error,
}; };
/// Check if this user/guild combination should be considered subscribed. /// Check if this user/guild combination should be considered subscribed.
/// If the guild has a patreon linked, check the user involved in the link. /// If the guild has a patreon linked, check the user involved in the link.
/// Otherwise, check the user and the guild's owner /// Otherwise, check the user and the guild's owner
pub async fn check_subscription( pub async fn check_subscription(
ctx: &Context<'_>, ctx: impl CacheHttp,
database: impl Executor<'_, Database = Database>,
user_id: UserId, user_id: UserId,
guild_id: Option<GuildId>, guild_id: Option<GuildId>,
) -> bool { ) -> bool {
if let Some(subscription_guild) = *CNC_GUILD { 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 { let owner_subscribed = match guild_id {
Some(guild_id) => { Some(guild_id) => {
if let Some(owner) = ctx.cache().unwrap().guild(guild_id).map(|g| g.owner_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 { } else {
false false
} }
@@ -38,13 +39,13 @@ pub async fn check_subscription(
let link_subscribed = match guild_id { let link_subscribed = match guild_id {
Some(guild_id) => { Some(guild_id) => {
if let Ok(row) = sqlx::query!( 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() guild_id.get()
) )
.fetch_one(&ctx.data().database) .fetch_one(database)
.await .await
{ {
check_user_subscription(ctx, row.user_id).await check_user_subscription(&ctx, row.user_id).await
} else { } else {
false false
} }
@@ -116,6 +117,7 @@ pub trait Extract {
} }
pub use extract_derive::Extract; pub use extract_derive::Extract;
use sqlx::Executor;
macro_rules! extract_arg { macro_rules! extract_arg {
($ctx:ident, $name:ident, String) => { ($ctx:ident, $name:ident, String) => {

View File

@@ -233,39 +233,6 @@ pub async fn initialize(
Ok(()) Ok(())
} }
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> 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<GuildId>,
) -> 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( pub async fn check_authorization(
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
ctx: &Context, ctx: &Context,

View File

@@ -7,38 +7,43 @@ pub mod todos;
use std::env; 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 channels::get_guild_channels;
pub use emojis::get_guild_emojis; pub use emojis::get_guild_emojis;
pub use reminders::*; pub use reminders::*;
use rocket::{get, http::CookieJar, serde::json::json, State}; use rocket::{get, http::CookieJar, serde::json::json, State};
pub use roles::get_guild_roles; pub use roles::get_guild_roles;
use serenity::all::UserId;
use serenity::{ use serenity::{
client::Context, client::Context,
model::id::{GuildId, RoleId}, model::id::{GuildId, RoleId},
}; };
pub use templates::*; pub use templates::*;
use crate::web::{check_authorization, routes::JsonResult};
#[get("/api/guild/<id>")] #[get("/api/guild/<id>")]
pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { pub async fn get_guild_info(
id: u64,
cookies: &CookieJar<'_>,
ctx: &State<Context>,
mut transaction: Transaction<'_>,
) -> JsonResult {
offline!(Ok(json!({ "patreon": true, "name": "Guild" }))); offline!(Ok(json!({ "patreon": true, "name": "Guild" })));
check_authorization(cookies, ctx.inner(), id).await?; check_authorization(cookies, ctx.inner(), id).await?;
match GuildId::new(id) let user_id =
.to_guild_cached(ctx.inner()) cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
.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| { match GuildId::new(id).to_guild_cached(ctx.inner()).map(|guild| guild.name.clone()) {
member Some(name) => {
.roles let patreon = check_subscription(
.contains(&RoleId::new(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) ctx.inner(),
}); transaction.executor(),
UserId::from(user_id),
Some(GuildId::from(id)),
)
.await;
Ok(json!({ "patreon": patreon, "name": name })) Ok(json!({ "patreon": patreon, "name": name }))
} }

View File

@@ -12,8 +12,9 @@ use serenity::{
}; };
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use crate::utils::check_subscription;
use crate::web::{ use crate::web::{
check_authorization, check_guild_subscription, check_subscription, check_authorization,
consts::MIN_INTERVAL, consts::MIN_INTERVAL,
guards::transaction::Transaction, guards::transaction::Transaction,
routes::{ routes::{
@@ -186,8 +187,13 @@ pub 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(&ctx.inner(), id).await if check_subscription(
|| check_subscription(&ctx.inner(), user_id).await ctx.inner(),
transaction.executor(),
UserId::from(user_id),
Some(GuildId::from(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),

View File

@@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
use serenity::{client::Context, model::id::UserId}; use serenity::{client::Context, model::id::UserId};
use sqlx::types::Json; use sqlx::types::Json;
use crate::utils::check_subscription;
use crate::web::{ use crate::web::{
check_subscription,
consts::{ consts::{
DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, 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_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_days.is_some()
|| reminder.interval_months.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"})); return Err(json!({"error": "Patreon is required to set intervals"}));
} }
} }

View File

@@ -9,8 +9,8 @@ use rocket::{
use serenity::{client::Context, model::id::UserId}; use serenity::{client::Context, model::id::UserId};
use sqlx::{MySql, Pool}; use sqlx::{MySql, Pool};
use crate::utils::check_subscription;
use crate::web::{ use crate::web::{
check_subscription,
guards::transaction::Transaction, guards::transaction::Transaction,
routes::{ routes::{
dashboard::{ dashboard::{
@@ -162,7 +162,9 @@ pub 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_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 { let new_interval_length = match reminder.interval_days {
Some(interval) => interval.unwrap_or(0), Some(interval) => interval.unwrap_or(0),
None => sqlx::query!( None => sqlx::query!(

View File

@@ -20,9 +20,9 @@ use serenity::{
use sqlx::types::Json; use sqlx::types::Json;
use sqlx::FromRow; use sqlx::FromRow;
use crate::utils::check_subscription;
use crate::web::{ use crate::web::{
catchers::internal_server_error, catchers::internal_server_error,
check_guild_subscription, check_subscription,
consts::{ consts::{
CHARACTERS, DAY, DEFAULT_AVATAR, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, 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_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_days.is_some()
|| reminder.interval_months.is_some() || reminder.interval_months.is_some()
{ {
if !check_guild_subscription(&ctx, guild_id).await if !check_subscription(&ctx, transaction.executor(), user_id, Some(guild_id)).await {
&& !check_subscription(&ctx, user_id).await
{
return Err(json!({"error": "Patreon is required to set intervals"})); return Err(json!({"error": "Patreon is required to set intervals"}));
} }
} }