Apply patreon sharing across web/bot
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
16
src/utils.rs
16
src/utils.rs
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user