diff --git a/Cargo.lock b/Cargo.lock index 5d437eac..5fadc290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2818,6 +2818,7 @@ dependencies = [ "scripty_utils", "serenity", "sqlx", + "time", "tokio", "tracing", "uuid", diff --git a/scripty_bot_utils/Cargo.toml b/scripty_bot_utils/Cargo.toml index 4746022f..c9fd6916 100644 --- a/scripty_bot_utils/Cargo.toml +++ b/scripty_bot_utils/Cargo.toml @@ -8,6 +8,7 @@ license = "EUPL-1.2" [dependencies] uuid = { version = "1", features = ["rand"] } +time = "0.3" dashmap = "5" tracing = "0.1" once_cell = "1" diff --git a/scripty_bot_utils/src/background_tasks/core.rs b/scripty_bot_utils/src/background_tasks/core.rs index d405c0f5..d443604c 100644 --- a/scripty_bot_utils/src/background_tasks/core.rs +++ b/scripty_bot_utils/src/background_tasks/core.rs @@ -68,4 +68,5 @@ pub fn init_background_tasks(ctx: Context) { init_task!(crate::background_tasks::tasks::StatusUpdater, ctx); init_task!(crate::background_tasks::tasks::CommandLatencyClearer, ctx); init_task!(crate::background_tasks::tasks::BotListUpdater, ctx); + init_task!(crate::background_tasks::tasks::VoteReminderTask, ctx); } diff --git a/scripty_bot_utils/src/background_tasks/tasks/bot_vote_reminder.rs b/scripty_bot_utils/src/background_tasks/tasks/bot_vote_reminder.rs new file mode 100644 index 00000000..9bf19f9c --- /dev/null +++ b/scripty_bot_utils/src/background_tasks/tasks/bot_vote_reminder.rs @@ -0,0 +1,112 @@ +use std::{fmt, time::Duration}; + +use serenity::{ + all::UserId, + builder::{CreateEmbed, CreateMessage}, + client::Context as SerenityContext, + futures::StreamExt, +}; + +use crate::{background_tasks::core::BackgroundTask, Error}; + +/// Sends vote reminders to users every minute. +pub struct VoteReminderTask { + ctx: SerenityContext, +} + +#[async_trait] +impl BackgroundTask for VoteReminderTask { + async fn init(ctx: SerenityContext) -> Result { + Ok(Self { ctx }) + } + + fn interval(&mut self) -> Duration { + Duration::from_secs(60) + } + + async fn run(&mut self) { + let mut vote_query = sqlx::query!( + "DELETE FROM vote_reminders WHERE next_reminder < NOW() RETURNING user_id, site_id, \ + next_reminder" + ) + .fetch_many(scripty_db::get_db()); + + while let Some(user) = vote_query.next().await { + let user = match user.map(|u| u.right()) { + Ok(Some(user)) => user, + Ok(None) => { + error!("got no user from vote reminder query"); + continue; + } + Err(err) => { + error!("failed to get vote reminder: {}", err); + continue; + } + }; + let site: VoteList = user.site_id.into(); + let user_id = user.user_id as u64; + let reminder_unix_ts = user.next_reminder.assume_utc().unix_timestamp(); + + let msg = + CreateMessage::new().embed(CreateEmbed::new().title("Vote reminder").description( + format!( + "You can vote for Scripty on {} again, as of . You can do so at \ + {}. Thanks for your support!", + site, + reminder_unix_ts, + site.vote_url() + ), + )); + let ctx2 = self.ctx.clone(); + tokio::spawn(async move { + let res = match UserId::new(user_id).create_dm_channel(&ctx2.http).await { + Ok(channel) => channel.send_message(&ctx2.http, msg).await.map(|_| ()), + Err(e) => Err(e), + }; + if let Err(e) = res { + error!("failed to send vote reminder: {}", e); + } + }); + } + } + + fn timeout(&mut self) -> Option { + Some(Duration::from_secs(5)) + } +} + +pub enum VoteList { + TopGg = 1, + DiscordServicesNet = 2, + WumpusStore = 3, +} +impl From for VoteList { + fn from(i: i16) -> Self { + match i { + 1 => Self::TopGg, + 2 => Self::DiscordServicesNet, + 3 => Self::WumpusStore, + _ => panic!("invalid vote list id"), + } + } +} + +impl fmt::Display for VoteList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TopGg => write!(f, "top.gg"), + Self::DiscordServicesNet => write!(f, "discordservices.net"), + Self::WumpusStore => write!(f, "wumpus.store"), + } + } +} + +impl VoteList { + pub fn vote_url(&self) -> &'static str { + match self { + Self::TopGg => "https://top.gg/bot/699453633624064849/vote", + Self::DiscordServicesNet => "https://discordservices.net/bot/scripty", + Self::WumpusStore => "https://wumpus.store/bot/811652199100317726/vote", + } + } +} diff --git a/scripty_bot_utils/src/background_tasks/tasks/mod.rs b/scripty_bot_utils/src/background_tasks/tasks/mod.rs index 057d0c0f..45361187 100644 --- a/scripty_bot_utils/src/background_tasks/tasks/mod.rs +++ b/scripty_bot_utils/src/background_tasks/tasks/mod.rs @@ -1,11 +1,13 @@ mod basic_stats_update; mod bot_list_poster; +mod bot_vote_reminder; mod cmd_latency_clear; mod prometheus_latency_update; mod status_update; pub use basic_stats_update::*; pub use bot_list_poster::*; +pub use bot_vote_reminder::*; pub use cmd_latency_clear::*; pub use prometheus_latency_update::*; pub use status_update::*; diff --git a/scripty_bot_utils/src/dm_support.rs b/scripty_bot_utils/src/dm_support.rs index 47a8ac5a..9847bb2e 100644 --- a/scripty_bot_utils/src/dm_support.rs +++ b/scripty_bot_utils/src/dm_support.rs @@ -67,7 +67,7 @@ impl DmSupportStatus { .author .global_name .as_ref() - .unwrap_or_else(|| &message.author.name) + .unwrap_or(&message.author.name) .to_string(), ); diff --git a/scripty_commands/src/cmds/mod.rs b/scripty_commands/src/cmds/mod.rs index 8be84b30..58f96010 100644 --- a/scripty_commands/src/cmds/mod.rs +++ b/scripty_commands/src/cmds/mod.rs @@ -13,6 +13,7 @@ pub mod premium; mod register_cmds; mod terms_of_service; mod throw_error; +mod vote_reminders; pub use admin::*; pub use data_storage::*; diff --git a/scripty_commands/src/cmds/vote_reminders.rs b/scripty_commands/src/cmds/vote_reminders.rs new file mode 100644 index 00000000..75c7bc84 --- /dev/null +++ b/scripty_commands/src/cmds/vote_reminders.rs @@ -0,0 +1,36 @@ +use crate::{Context, Error}; + +/// Opt in or out of vote reminders +#[poise::command(prefix_command, slash_command)] +pub async fn vote_reminder(ctx: Context<'_>, enabled: bool) -> Result<(), Error> { + let resolved_language = + scripty_i18n::get_resolved_language(ctx.author().id.get(), ctx.guild_id().map(|g| g.get())) + .await; + + let db = scripty_db::get_db(); + sqlx::query!( + "INSERT INTO users (user_id) VALUES ($1) ON CONFLICT ON CONSTRAINT users_pkey DO NOTHING", + ctx.author().id.get() as i64 + ) + .execute(db) + .await?; + sqlx::query!( + "UPDATE users SET vote_reminder_disabled = $1 WHERE user_id = $2", + enabled, + ctx.author().id.get() as i64 + ) + .execute(db) + .await?; + + ctx.say(format_message!( + resolved_language, + if enabled { + "vote-reminders-enabled" + } else { + "vote-reminders-disabled" + } + )) + .await?; + + Ok(()) +} diff --git a/scripty_i18n/locales/en.ftl b/scripty_i18n/locales/en.ftl index dc86fb33..9e02e447 100644 --- a/scripty_i18n/locales/en.ftl +++ b/scripty_i18n/locales/en.ftl @@ -353,6 +353,14 @@ automod-list-rules-embed-field-value = Type: { $ruleType } automod-list-rules-footer = Page { $page } of { $maxPage } automod-list-rules-no-rules = You don't have any rules! +## vote reminder command +cmds_vote_reminder = vote_reminder + .description = Toggle whether Scripty will remind you to vote for the bot after the time limit has passed. + .enabled = enabled + .enabled-description = Enable vote reminders? +vote-reminders-enabled = Vote reminders enabled. +vote-reminders-disabled = Vote reminders disabled. + ## blocked entities description blocked-entity-no-reason-given = No reason was given for the block. diff --git a/scripty_webserver/src/endpoints/webhooks/discordservices_net.rs b/scripty_webserver/src/endpoints/webhooks/discordservices_net.rs index 0dc5c6fd..f613ef1a 100644 --- a/scripty_webserver/src/endpoints/webhooks/discordservices_net.rs +++ b/scripty_webserver/src/endpoints/webhooks/discordservices_net.rs @@ -99,6 +99,10 @@ pub async fn discordservices_net_incoming_webhook( .await?; // if they're opted in, set up a reminder for 20 hours from now + if opted_out { + return Ok(()); + } + sqlx::query!( "INSERT INTO vote_reminders (user_id, site_id, next_reminder) VALUES ($1, 2, NOW() + INTERVAL '20 hours') diff --git a/scripty_webserver/src/endpoints/webhooks/top_gg.rs b/scripty_webserver/src/endpoints/webhooks/top_gg.rs index 8d6d5428..828f565d 100644 --- a/scripty_webserver/src/endpoints/webhooks/top_gg.rs +++ b/scripty_webserver/src/endpoints/webhooks/top_gg.rs @@ -95,6 +95,10 @@ pub async fn top_gg_incoming_webhook( .await?; // if they're opted in, set up a reminder for 12 hours from now + if opted_out { + return Ok(()); + } + sqlx::query!( "INSERT INTO vote_reminders (user_id, site_id, next_reminder) VALUES ($1, 1, NOW() + INTERVAL '12 hours') diff --git a/scripty_webserver/src/endpoints/webhooks/wumpus_store.rs b/scripty_webserver/src/endpoints/webhooks/wumpus_store.rs index ddc5205a..f116d8e4 100644 --- a/scripty_webserver/src/endpoints/webhooks/wumpus_store.rs +++ b/scripty_webserver/src/endpoints/webhooks/wumpus_store.rs @@ -102,6 +102,10 @@ pub async fn wumpus_store_incoming_webhook( .await?; // if they're opted in, set up a reminder for 12 hours from now + if opted_out { + return Ok(()); + } + sqlx::query!( "INSERT INTO vote_reminders (user_id, site_id, next_reminder) VALUES ($1, 3, NOW() + INTERVAL '12 hours')