Add patreon-sharing option
This commit is contained in:
11
migrations/20250924203400_patreon_link.sql
Normal file
11
migrations/20250924203400_patreon_link.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE patreon_link
|
||||||
|
(
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
guild_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
linked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY (user_id),
|
||||||
|
INDEX `idx_user_id` (user_id),
|
||||||
|
INDEX `idx_guild_id` (guild_id),
|
||||||
|
INDEX `idx_linked_at` (linked_at)
|
||||||
|
);
|
@@ -12,8 +12,6 @@ pub mod dashboard;
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod donate;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
pub mod help;
|
pub mod help;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod info;
|
pub mod info;
|
||||||
@@ -26,6 +24,8 @@ pub mod nudge;
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod offset;
|
pub mod offset;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
|
pub mod patreon;
|
||||||
|
#[cfg(not(test))]
|
||||||
pub mod pause;
|
pub mod pause;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod remind;
|
pub mod remind;
|
||||||
|
@@ -15,8 +15,8 @@ impl Recordable for Options {
|
|||||||
let footer = footer(ctx);
|
let footer = footer(ctx);
|
||||||
|
|
||||||
ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
|
ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
|
||||||
.description("Thinking of adding a monthly contribution?
|
.description("Thinking of subscribing?
|
||||||
Click below for my Patreon and official bot server :)
|
Click below for my Patreon and official bot server
|
||||||
|
|
||||||
**https://www.patreon.com/jellywx/**
|
**https://www.patreon.com/jellywx/**
|
||||||
**https://discord.jellywx.com/**
|
**https://discord.jellywx.com/**
|
||||||
@@ -26,7 +26,7 @@ With your new rank, you'll be able to:
|
|||||||
• Set repeating reminders with `/remind` or the dashboard
|
• Set repeating reminders with `/remind` or the dashboard
|
||||||
• Use unlimited uploads on SoundFX
|
• Use unlimited uploads on SoundFX
|
||||||
|
|
||||||
(Also, members of servers you __own__ will be able to set repeating reminders via commands)
|
Members of servers you __own__ will be able to set repeating reminders via commands. You can also choose to share your membership with one other server.
|
||||||
|
|
||||||
Just $2 USD/month!
|
Just $2 USD/month!
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ Just $2 USD/month!
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Details on supporting the bot and Patreon benefits
|
/// Show Patreon information
|
||||||
#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
|
#[poise::command(slash_command, rename = "info", identifying_name = "patreon_info")]
|
||||||
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
(Options {}).run(ctx).await
|
(Options {}).run(ctx).await
|
||||||
}
|
}
|
73
src/commands/patreon/link.rs
Normal file
73
src/commands/patreon/link.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
use poise::CreateReply;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
utils::{check_user_subscription, Extract, Recordable},
|
||||||
|
Context, Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Extract)]
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Check if user has Patreon subscription
|
||||||
|
if !check_user_subscription(ctx, user_id).await {
|
||||||
|
ctx.send(CreateReply::default()
|
||||||
|
.content("❌ You must be a Patreon subscriber to use this command. Use `/patreon info` for more information.")
|
||||||
|
.ephemeral(true)
|
||||||
|
).await?;
|
||||||
|
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",
|
||||||
|
user_id.get(),
|
||||||
|
guild_id.get()
|
||||||
|
)
|
||||||
|
.execute(&ctx.data().database)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.content("✅ Successfully linked your Patreon subscription to this server!")
|
||||||
|
.ephemeral(true),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Link your Patreon subscription to this server. This command can be run once every four weeks
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "link",
|
||||||
|
identifying_name = "patreon_link",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
|
pub async fn link(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
(Options {}).run(ctx).await
|
||||||
|
}
|
11
src/commands/patreon/mod.rs
Normal file
11
src/commands/patreon/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod info;
|
||||||
|
pub mod link;
|
||||||
|
pub mod unlink;
|
||||||
|
|
||||||
|
use crate::{Context, Error};
|
||||||
|
|
||||||
|
/// Manage Patreon subscription features
|
||||||
|
#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
|
||||||
|
pub async fn command(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
55
src/commands/patreon/unlink.rs
Normal file
55
src/commands/patreon/unlink.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use poise::CreateReply;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
utils::{Extract, Recordable},
|
||||||
|
Context, Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Extract)]
|
||||||
|
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?;
|
||||||
|
|
||||||
|
if result.rows_affected() > 0 {
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.content("✅ Successfully unlinked your Patreon subscription from this server!")
|
||||||
|
.ephemeral(true),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.content("❌ No existing Patreon link found for this server.")
|
||||||
|
.ephemeral(true),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unlink your Patreon subscription from this server
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "unlink",
|
||||||
|
identifying_name = "patreon_unlink",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
|
pub async fn unlink(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
(Options {}).run(ctx).await
|
||||||
|
}
|
13
src/main.rs
13
src/main.rs
@@ -48,8 +48,8 @@ use crate::test::TestContext;
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::{
|
commands::{
|
||||||
allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
|
allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
|
||||||
delete, donate, help, info, look, multiline, nudge, offset, pause, remind, settings, timer,
|
delete, help, info, look, multiline, nudge, offset, patreon, pause, remind, settings,
|
||||||
timezone, todo, webhook,
|
timer, timezone, todo, webhook,
|
||||||
},
|
},
|
||||||
consts::THEME_COLOR,
|
consts::THEME_COLOR,
|
||||||
event_handlers::listener,
|
event_handlers::listener,
|
||||||
@@ -165,7 +165,14 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
|||||||
help::command(),
|
help::command(),
|
||||||
info::command(),
|
info::command(),
|
||||||
clock::command(),
|
clock::command(),
|
||||||
donate::command(),
|
poise::Command {
|
||||||
|
subcommands: vec![
|
||||||
|
patreon::link::link(),
|
||||||
|
patreon::unlink::unlink(),
|
||||||
|
patreon::info::info(),
|
||||||
|
],
|
||||||
|
..patreon::command()
|
||||||
|
},
|
||||||
clock_context_menu(),
|
clock_context_menu(),
|
||||||
dashboard::command(),
|
dashboard::command(),
|
||||||
timezone::command(),
|
timezone::command(),
|
||||||
|
@@ -43,7 +43,7 @@ pub enum RecordedCommand {
|
|||||||
#[serde(rename = "delete")]
|
#[serde(rename = "delete")]
|
||||||
Delete(crate::commands::delete::Options),
|
Delete(crate::commands::delete::Options),
|
||||||
#[serde(rename = "donate")]
|
#[serde(rename = "donate")]
|
||||||
Donate(crate::commands::donate::Options),
|
Donate(crate::commands::patreon::info::Options),
|
||||||
#[serde(rename = "help")]
|
#[serde(rename = "help")]
|
||||||
Help(crate::commands::help::Options),
|
Help(crate::commands::help::Options),
|
||||||
#[serde(rename = "info")]
|
#[serde(rename = "info")]
|
||||||
@@ -111,7 +111,7 @@ impl RecordedCommand {
|
|||||||
"clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
|
"clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
|
||||||
"dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
|
"dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
|
||||||
"delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
|
"delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
|
||||||
"donate" => Some(Self::Donate(crate::commands::donate::Options::extract(ctx))),
|
"donate" => Some(Self::Donate(crate::commands::patreon::info::Options::extract(ctx))),
|
||||||
"help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
|
"help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
|
||||||
"info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
|
"info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
|
||||||
"look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
|
"look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
|
||||||
|
@@ -17,7 +17,7 @@ use crate::{
|
|||||||
CtxData,
|
CtxData,
|
||||||
},
|
},
|
||||||
time_parser::{cron_next_timestamp, natural_parser},
|
time_parser::{cron_next_timestamp, natural_parser},
|
||||||
utils::{check_guild_subscription, check_subscription},
|
utils::check_subscription,
|
||||||
Context, Database, Error,
|
Context, Database, Error,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
@@ -532,10 +532,7 @@ 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).await
|
if check_subscription(&ctx, ctx.author().id, ctx.guild_id()).await {
|
||||||
|| (ctx.guild_id().is_some()
|
|
||||||
&& check_guild_subscription(&ctx, ctx.guild_id().unwrap()).await)
|
|
||||||
{
|
|
||||||
(
|
(
|
||||||
parse_duration(repeat)
|
parse_duration(repeat)
|
||||||
.or_else(|_| parse_duration(&format!("1 {}", repeat)))
|
.or_else(|_| parse_duration(&format!("1 {}", repeat)))
|
||||||
|
@@ -3,6 +3,8 @@ use poise::{
|
|||||||
CreateReply,
|
CreateReply,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use serenity::all::Http;
|
||||||
|
use serenity::http::CacheHttp;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{Data, Error};
|
use crate::{Data, Error};
|
||||||
@@ -19,6 +21,12 @@ pub(crate) struct TestContext<'a> {
|
|||||||
pub(crate) shard_id: usize,
|
pub(crate) shard_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CacheHttp for TestContext<'_> {
|
||||||
|
fn http(&self) -> &Http {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct MockUser {
|
pub(crate) struct MockUser {
|
||||||
pub(crate) id: UserId,
|
pub(crate) id: UserId,
|
||||||
}
|
}
|
||||||
|
64
src/utils.rs
64
src/utils.rs
@@ -12,7 +12,58 @@ use crate::{
|
|||||||
ApplicationContext, Context, Error,
|
ApplicationContext, Context, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
|
/// 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<'_>,
|
||||||
|
user_id: UserId,
|
||||||
|
guild_id: Option<GuildId>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(subscription_guild) = *CNC_GUILD {
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let link_subscribed = match guild_id {
|
||||||
|
Some(guild_id) => {
|
||||||
|
if let Ok(row) = sqlx::query!(
|
||||||
|
"SELECT user_id FROM patreon_link WHERE user_id = ?",
|
||||||
|
guild_id.get()
|
||||||
|
)
|
||||||
|
.fetch_one(&ctx.data().database)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
check_user_subscription(ctx, row.user_id).await
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
user_subscribed || owner_subscribed || link_subscribed
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check a user's subscription status, ignoring Patreon linkage
|
||||||
|
pub async fn check_user_subscription(
|
||||||
|
cache_http: impl CacheHttp,
|
||||||
|
user_id: impl Into<UserId>,
|
||||||
|
) -> bool {
|
||||||
if let Some(subscription_guild) = *CNC_GUILD {
|
if let Some(subscription_guild) = *CNC_GUILD {
|
||||||
let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
|
let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
|
||||||
|
|
||||||
@@ -30,17 +81,6 @@ pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<U
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_guild_subscription(
|
|
||||||
cache_http: impl CacheHttp,
|
|
||||||
guild_id: impl Into<GuildId>,
|
|
||||||
) -> bool {
|
|
||||||
if let Some(owner) = cache_http.cache().unwrap().guild(guild_id).map(|g| g.owner_id) {
|
|
||||||
check_subscription(&cache_http, owner).await
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reply_to_interaction_response_message(
|
pub fn reply_to_interaction_response_message(
|
||||||
reply: CreateReply,
|
reply: CreateReply,
|
||||||
) -> CreateInteractionResponseMessage {
|
) -> CreateInteractionResponseMessage {
|
||||||
|
Reference in New Issue
Block a user