diff --git a/.sqlx/query-343e3787a742ae6ed8bcb167d2a5ebe8ccd6b29a5d52eca4cb4382391eca39f8.json b/.sqlx/query-343e3787a742ae6ed8bcb167d2a5ebe8ccd6b29a5d52eca4cb4382391eca39f8.json new file mode 100644 index 00000000..068d265e --- /dev/null +++ b/.sqlx/query-343e3787a742ae6ed8bcb167d2a5ebe8ccd6b29a5d52eca4cb4382391eca39f8.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO vote_reminders (user_id, site_id, next_reminder)\n VALUES ($1, 1, NOW() + INTERVAL '12 hours')\n ON CONFLICT (user_id, site_id)\n DO UPDATE SET next_reminder = NOW() + INTERVAL '12 hours'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "343e3787a742ae6ed8bcb167d2a5ebe8ccd6b29a5d52eca4cb4382391eca39f8" +} diff --git a/.sqlx/query-44e187a92daa9453a5465cd057fdeaa7237eb96c13395d7d099b714d85793eff.json b/.sqlx/query-44e187a92daa9453a5465cd057fdeaa7237eb96c13395d7d099b714d85793eff.json new file mode 100644 index 00000000..9d72e4a6 --- /dev/null +++ b/.sqlx/query-44e187a92daa9453a5465cd057fdeaa7237eb96c13395d7d099b714d85793eff.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT translate FROM guilds WHERE guild_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "translate", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "44e187a92daa9453a5465cd057fdeaa7237eb96c13395d7d099b714d85793eff" +} diff --git a/.sqlx/query-8d09c510bf7c60f5ad05c3be9054f2519432711eeef25dbd69a8528f8378c594.json b/.sqlx/query-8d09c510bf7c60f5ad05c3be9054f2519432711eeef25dbd69a8528f8378c594.json new file mode 100644 index 00000000..c592907d --- /dev/null +++ b/.sqlx/query-8d09c510bf7c60f5ad05c3be9054f2519432711eeef25dbd69a8528f8378c594.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO vote_reminders (user_id, site_id, next_reminder)\n VALUES ($1, 2, NOW() + INTERVAL '12 hours')\n ON CONFLICT (user_id, site_id)\n DO UPDATE SET next_reminder = NOW() + INTERVAL '12 hours'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "8d09c510bf7c60f5ad05c3be9054f2519432711eeef25dbd69a8528f8378c594" +} diff --git a/.sqlx/query-8f88862824d6f1d9a305cb1c96bf78bb19609c0f1067673af10308282706c59a.json b/.sqlx/query-8f88862824d6f1d9a305cb1c96bf78bb19609c0f1067673af10308282706c59a.json new file mode 100644 index 00000000..3db2cbbb --- /dev/null +++ b/.sqlx/query-8f88862824d6f1d9a305cb1c96bf78bb19609c0f1067673af10308282706c59a.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT vote_reminder_disabled FROM users WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "vote_reminder_disabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Bytea" + ] + }, + "nullable": [ + false + ] + }, + "hash": "8f88862824d6f1d9a305cb1c96bf78bb19609c0f1067673af10308282706c59a" +} diff --git a/.sqlx/query-a55baeff8a9d29d3f4bf93bdce5a8516107f98b21ef7eaf913e1d73cafbad1a1.json b/.sqlx/query-a55baeff8a9d29d3f4bf93bdce5a8516107f98b21ef7eaf913e1d73cafbad1a1.json new file mode 100644 index 00000000..f8ef5dea --- /dev/null +++ b/.sqlx/query-a55baeff8a9d29d3f4bf93bdce5a8516107f98b21ef7eaf913e1d73cafbad1a1.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO vote_reminders (user_id, site_id, next_reminder)\n VALUES ($1, 3, NOW() + INTERVAL '12 hours')\n ON CONFLICT (user_id, site_id)\n DO UPDATE SET next_reminder = NOW() + INTERVAL '12 hours'", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "a55baeff8a9d29d3f4bf93bdce5a8516107f98b21ef7eaf913e1d73cafbad1a1" +} diff --git a/.sqlx/query-a90b58705c2aa90bd947676b09f5c1bd7cf8de9ac78a57ae007ba1e233e23ff5.json b/.sqlx/query-a90b58705c2aa90bd947676b09f5c1bd7cf8de9ac78a57ae007ba1e233e23ff5.json new file mode 100644 index 00000000..315cd5f8 --- /dev/null +++ b/.sqlx/query-a90b58705c2aa90bd947676b09f5c1bd7cf8de9ac78a57ae007ba1e233e23ff5.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO guilds (guild_id, translate) VALUES ($1, $2) ON CONFLICT (guild_id) DO UPDATE SET translate = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "a90b58705c2aa90bd947676b09f5c1bd7cf8de9ac78a57ae007ba1e233e23ff5" +} diff --git a/Cargo.lock b/Cargo.lock index 6b0b3238..8c60694f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,7 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", + "headers", "http", "http-body", "hyper", @@ -1267,6 +1268,30 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.5", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -3036,6 +3061,7 @@ version = "1.0.0" dependencies = [ "axum", "scripty_bot_utils", + "scripty_botlists", "scripty_config", "scripty_db", "scripty_i18n", diff --git a/migrations/20231216214434_vote_reminder_table.sql b/migrations/20231216214434_vote_reminder_table.sql new file mode 100644 index 00000000..307f851c --- /dev/null +++ b/migrations/20231216214434_vote_reminder_table.sql @@ -0,0 +1,8 @@ +-- Add migration script here +CREATE TABLE vote_reminders ( + user_id BIGINT NOT NULL, + site_id SMALLINT NOT NULL, + next_reminder TIMESTAMP NOT NULL, + + PRIMARY KEY (user_id, site_id) +); diff --git a/migrations/20231217034034_vote_reminder_opt_out.sql b/migrations/20231217034034_vote_reminder_opt_out.sql new file mode 100644 index 00000000..d99dfcf7 --- /dev/null +++ b/migrations/20231217034034_vote_reminder_opt_out.sql @@ -0,0 +1,2 @@ +-- Add migration script here +ALTER TABLE users ADD COLUMN vote_reminder_disabled BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/scripty_audio_handler/src/events/voice_tick.rs b/scripty_audio_handler/src/events/voice_tick.rs index 321e8816..a92352d9 100644 --- a/scripty_audio_handler/src/events/voice_tick.rs +++ b/scripty_audio_handler/src/events/voice_tick.rs @@ -394,7 +394,10 @@ async fn finalize_stream( webhook_executor } Ok(_) => return (None, None), - Err(error) => handle_error(error, ssrc), + Err(e) => { + error!(%ssrc, "failed to get stream result: {}", e); + return (None, None); + } }; debug!(%ssrc, "got stream results"); diff --git a/scripty_bot_utils/src/background_tasks/tasks/bot_list_poster.rs b/scripty_bot_utils/src/background_tasks/tasks/bot_list_poster.rs index 500e8aab..195589da 100644 --- a/scripty_bot_utils/src/background_tasks/tasks/bot_list_poster.rs +++ b/scripty_bot_utils/src/background_tasks/tasks/bot_list_poster.rs @@ -1,7 +1,20 @@ use std::{sync::Arc, time::Duration}; use reqwest::Client; -use scripty_botlists::*; +use scripty_botlists::{ + botlist_me::BotListMe, + discord_bots_gg::DiscordBotsGG, + discordbotlist_com::DiscordBotListCom, + discordextremelist_xyz::DiscordExtremeListXyz, + discords_com::DiscordsCom, + discordservices_net::DiscordServicesNet, + disforge_com::DisforgeCom, + infinitybots_gg::InfinityBotsGG, + top_gg::TopGG, + voidbots_net::VoidBotsNet, + PostStats, + StatPoster, +}; use scripty_config::BotListsConfig; use serenity::client::Context; @@ -51,17 +64,12 @@ enum BotLists { BotListMe(BotListMe), DiscordBotsGG(DiscordBotsGG), DiscordBotListCom(DiscordBotListCom), - DiscordBotListEu(DiscordBotListEu), DiscordExtremeListXyz(DiscordExtremeListXyz), - DiscordListGG(DiscordListGG), DiscordsCom(DiscordsCom), DiscordServicesNet(DiscordServicesNet), DisforgeCom(DisforgeCom), InfinityBotsGG(InfinityBotsGG), - MotionDevelopmentTop(MotionDevelopmentTop), - RadarCordNet(RadarCordNet), TopGG(TopGG), - TopCordXyz(TopCordXyz), VoidBotsNet(VoidBotsNet), } @@ -76,17 +84,12 @@ impl StatPoster for BotLists { BotLists::BotListMe(item) => item.post_stats(client, stats).await, BotLists::DiscordBotsGG(item) => item.post_stats(client, stats).await, BotLists::DiscordBotListCom(item) => item.post_stats(client, stats).await, - BotLists::DiscordBotListEu(item) => item.post_stats(client, stats).await, BotLists::DiscordExtremeListXyz(item) => item.post_stats(client, stats).await, - BotLists::DiscordListGG(item) => item.post_stats(client, stats).await, BotLists::DiscordsCom(item) => item.post_stats(client, stats).await, BotLists::DiscordServicesNet(item) => item.post_stats(client, stats).await, BotLists::DisforgeCom(item) => item.post_stats(client, stats).await, BotLists::InfinityBotsGG(item) => item.post_stats(client, stats).await, - BotLists::MotionDevelopmentTop(item) => item.post_stats(client, stats).await, - BotLists::RadarCordNet(item) => item.post_stats(client, stats).await, BotLists::TopGG(item) => item.post_stats(client, stats).await, - BotLists::TopCordXyz(item) => item.post_stats(client, stats).await, BotLists::VoidBotsNet(item) => item.post_stats(client, stats).await, } } @@ -114,13 +117,6 @@ fn add_bot_lists(bot_lists: &mut Vec, bot_id: u64) { ))); } - if let Some(BotListsConfig::TokenOnly(token)) = bot_list_cfg.get("discordbotlist_eu") { - bot_lists.push(BotLists::DiscordBotListEu(DiscordBotListEu::new( - token.clone(), - bot_id, - ))); - } - if let Some(BotListsConfig::TokenOnly(token)) = bot_list_cfg.get("discordextremelist_xyz") { bot_lists.push(BotLists::DiscordExtremeListXyz(DiscordExtremeListXyz::new( token.clone(), @@ -128,13 +124,6 @@ fn add_bot_lists(bot_lists: &mut Vec, bot_id: u64) { ))); } - if let Some(BotListsConfig::TokenOnly(token)) = bot_list_cfg.get("discordlist_gg") { - bot_lists.push(BotLists::DiscordListGG(DiscordListGG::new( - token.clone(), - bot_id, - ))); - } - if let Some(BotListsConfig::TokenOnly(token)) = bot_list_cfg.get("discords_com") { bot_lists.push(BotLists::DiscordsCom(DiscordsCom::new( token.clone(), @@ -164,28 +153,10 @@ fn add_bot_lists(bot_lists: &mut Vec, bot_id: u64) { ))); } - if let Some(BotListsConfig::TokenOnly(token)) = bot_list_cfg.get("motiondevelopment_top") { - bot_lists.push(BotLists::MotionDevelopmentTop(MotionDevelopmentTop::new( - token.clone(), - bot_id, - ))); - } - - if let Some(BotListsConfig::TokenOnly(token)) = bot_list_cfg.get("radarcord_net") { - bot_lists.push(BotLists::RadarCordNet(RadarCordNet::new( - token.clone(), - bot_id, - ))); - } - if let Some(BotListsConfig::FullConfig { token, .. }) = bot_list_cfg.get("top_gg") { bot_lists.push(BotLists::TopGG(TopGG::new(token.clone(), bot_id))); } - if let Some(BotListsConfig::TokenOnly(token)) = bot_list_cfg.get("topcord_xyz") { - bot_lists.push(BotLists::TopCordXyz(TopCordXyz::new(token.clone(), bot_id))); - } - if let Some(BotListsConfig::TokenOnly(token)) = bot_list_cfg.get("voidbots_net") { bot_lists.push(BotLists::VoidBotsNet(VoidBotsNet::new( token.clone(), diff --git a/scripty_botlists/src/lists/discordbotlist_eu/mod.rs b/scripty_botlists/src/lists/discordbotlist_eu/mod.rs deleted file mode 100644 index a4f67262..00000000 --- a/scripty_botlists/src/lists/discordbotlist_eu/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod models; -mod trait_impl; - -pub use trait_impl::DiscordBotListEu; diff --git a/scripty_botlists/src/lists/discordbotlist_eu/models.rs b/scripty_botlists/src/lists/discordbotlist_eu/models.rs deleted file mode 100644 index 6f319dfd..00000000 --- a/scripty_botlists/src/lists/discordbotlist_eu/models.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(Serialize, Debug, Copy, Clone)] -pub struct PostStats { - pub guilds: usize, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct PostStatsResponse { - pub description: String, - pub message: String, - pub success: bool, -} diff --git a/scripty_botlists/src/lists/discordbotlist_eu/trait_impl.rs b/scripty_botlists/src/lists/discordbotlist_eu/trait_impl.rs deleted file mode 100644 index 3904301a..00000000 --- a/scripty_botlists/src/lists/discordbotlist_eu/trait_impl.rs +++ /dev/null @@ -1,53 +0,0 @@ -use reqwest::{Client, RequestBuilder}; - -use crate::common::{PostStats, StatPoster}; - -pub struct DiscordBotListEu { - token: String, - bot_id: u64, -} - -impl DiscordBotListEu { - pub fn new(token: String, bot_id: u64) -> Self { - Self { token, bot_id } - } - - pub fn token(&self) -> &str { - &self.token - } - - pub fn bot_id(&self) -> u64 { - self.bot_id - } -} - -#[async_trait] -impl StatPoster for DiscordBotListEu { - async fn post_stats( - &self, - client: &Client, - stats: PostStats, - ) -> Result { - let request: RequestBuilder = client - .patch("https://api.discord-botlist.eu/v1/update") - .header("Authorization", &self.token) - .json(&super::models::PostStats { - guilds: stats.server_count, - }); - let response = request.send().await?; - debug!("discord-botlist.eu response: {:?}", response); - let status = response.status(); - let maybe_error = if status.is_client_error() || status.is_server_error() { - Some(crate::common::Error::StatusCode(status)) - } else { - None - }; - let body = response.text().await?; - debug!("discord-botlist.com response body: <{}>", body); - if let Some(maybe_error) = maybe_error { - return Err(maybe_error); - } - let body: super::models::PostStatsResponse = serde_json::from_str(&body)?; - Ok(body.success) - } -} diff --git a/scripty_botlists/src/lists/discordlist_gg/mod.rs b/scripty_botlists/src/lists/discordlist_gg/mod.rs deleted file mode 100644 index 44fda560..00000000 --- a/scripty_botlists/src/lists/discordlist_gg/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod trait_impl; - -pub use trait_impl::DiscordListGG; diff --git a/scripty_botlists/src/lists/discordlist_gg/trait_impl.rs b/scripty_botlists/src/lists/discordlist_gg/trait_impl.rs deleted file mode 100644 index a84d8673..00000000 --- a/scripty_botlists/src/lists/discordlist_gg/trait_impl.rs +++ /dev/null @@ -1,53 +0,0 @@ -use reqwest::{Client, RequestBuilder}; - -use crate::common::{PostStats, StatPoster}; - -pub struct DiscordListGG { - token: String, - bot_id: u64, -} - -impl DiscordListGG { - pub fn new(token: String, bot_id: u64) -> Self { - Self { token, bot_id } - } - - pub fn token(&self) -> &str { - &self.token - } - - pub fn bot_id(&self) -> u64 { - self.bot_id - } -} - -#[async_trait] -impl StatPoster for DiscordListGG { - async fn post_stats( - &self, - client: &Client, - stats: PostStats, - ) -> Result { - let request: RequestBuilder = client - .put(format!( - "https://api.discordlist.gg/v0/bots/{}/guilds", - self.bot_id - )) - .header("Authorization", format!("Bearer {}", &self.token)) - .query(&[("count", stats.server_count)]); - let response = request.send().await?; - debug!("discordlist.gg response: {:?}", response); - let status = response.status(); - let maybe_error = if status.is_client_error() || status.is_server_error() { - Some(crate::common::Error::StatusCode(status)) - } else { - None - }; - let body = response.text().await?; - debug!("discordlist.gg response body: <{}>", body); - if let Some(maybe_error) = maybe_error { - return Err(maybe_error); - } - Ok(status != reqwest::StatusCode::OK) - } -} diff --git a/scripty_botlists/src/lists/discords_com/models.rs b/scripty_botlists/src/lists/discords_com/models.rs index 968ef793..0e3dcc17 100644 --- a/scripty_botlists/src/lists/discords_com/models.rs +++ b/scripty_botlists/src/lists/discords_com/models.rs @@ -1,37 +1,4 @@ -use crate::common::UserId; - #[derive(Debug, Serialize)] pub struct PostStats { pub server_count: usize, } - -#[derive(Debug, Deserialize, Clone)] -pub struct DiscordsComIncomingWebhook { - // both the bot and user fields are sent as a string, but they're actually numbers - #[serde(deserialize_with = "serde_aux::field_attributes::deserialize_number_from_string")] - pub bot: u64, - #[serde(deserialize_with = "serde_aux::field_attributes::deserialize_number_from_string")] - pub user: u64, - - pub votes: DiscordsComVotes, - #[serde(rename = "type")] - pub kind: DiscordsComWebhookType, -} - -#[derive(Debug, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct DiscordsComVotes { - pub has_voted: Vec, - pub has_voted24: Vec, - - pub total_votes: u64, - pub votes24: u64, - pub votes_month: u64, -} - -#[derive(Debug, Deserialize, Copy, Clone)] -#[serde(rename_all = "snake_case")] -pub enum DiscordsComWebhookType { - Vote, - Test, -} diff --git a/scripty_botlists/src/lists/discordservices_net/mod.rs b/scripty_botlists/src/lists/discordservices_net/mod.rs index d192b84f..856c24b3 100644 --- a/scripty_botlists/src/lists/discordservices_net/mod.rs +++ b/scripty_botlists/src/lists/discordservices_net/mod.rs @@ -1,4 +1,5 @@ mod models; mod trait_impl; +pub use models::*; pub use trait_impl::DiscordServicesNet; diff --git a/scripty_botlists/src/lists/discordservices_net/models.rs b/scripty_botlists/src/lists/discordservices_net/models.rs index ed7920f6..e8daf47c 100644 --- a/scripty_botlists/src/lists/discordservices_net/models.rs +++ b/scripty_botlists/src/lists/discordservices_net/models.rs @@ -12,16 +12,17 @@ pub struct PostStatsResponse { #[derive(Serialize, Deserialize)] pub struct DiscordServicesNetIncomingWebhook { - bot: Bot, - user: Bot, + pub bot: Bot, + pub user: Bot, } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Bot { - id: String, - name: String, + #[serde(deserialize_with = "serde_aux::field_attributes::deserialize_number_from_string")] + pub id: u64, + pub name: String, #[serde(rename = "discrim")] - discriminator: i64, - avatar_id: String, + pub discriminator: Option, + pub avatar_id: String, } diff --git a/scripty_botlists/src/lists/mod.rs b/scripty_botlists/src/lists/mod.rs index 99013714..5fb0ed37 100644 --- a/scripty_botlists/src/lists/mod.rs +++ b/scripty_botlists/src/lists/mod.rs @@ -1,31 +1,11 @@ -mod botlist_me; -mod discord_bots_gg; -mod discordbotlist_com; -mod discordbotlist_eu; -mod discordextremelist_xyz; -mod discordlist_gg; -mod discords_com; -mod discordservices_net; -mod disforge_com; -mod infinitybots_gg; -mod motiondevelopment_top; -mod radarcord_net; -mod top_gg; -mod topcord_xyz; -mod voidbots_net; - -pub use botlist_me::*; -pub use discord_bots_gg::*; -pub use discordbotlist_com::*; -pub use discordbotlist_eu::*; -pub use discordextremelist_xyz::*; -pub use discordlist_gg::*; -pub use discords_com::*; -pub use discordservices_net::*; -pub use disforge_com::*; -pub use infinitybots_gg::*; -pub use motiondevelopment_top::*; -pub use radarcord_net::*; -pub use top_gg::*; -pub use topcord_xyz::*; -pub use voidbots_net::*; +pub mod botlist_me; +pub mod discord_bots_gg; +pub mod discordbotlist_com; +pub mod discordextremelist_xyz; +pub mod discords_com; +pub mod discordservices_net; +pub mod disforge_com; +pub mod infinitybots_gg; +pub mod top_gg; +pub mod voidbots_net; +pub mod wumpus_store; diff --git a/scripty_botlists/src/lists/motiondevelopment_top/mod.rs b/scripty_botlists/src/lists/motiondevelopment_top/mod.rs deleted file mode 100644 index 0f923004..00000000 --- a/scripty_botlists/src/lists/motiondevelopment_top/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod models; -mod trait_impl; - -pub use trait_impl::MotionDevelopmentTop; diff --git a/scripty_botlists/src/lists/motiondevelopment_top/models.rs b/scripty_botlists/src/lists/motiondevelopment_top/models.rs deleted file mode 100644 index 6f319dfd..00000000 --- a/scripty_botlists/src/lists/motiondevelopment_top/models.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(Serialize, Debug, Copy, Clone)] -pub struct PostStats { - pub guilds: usize, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct PostStatsResponse { - pub description: String, - pub message: String, - pub success: bool, -} diff --git a/scripty_botlists/src/lists/motiondevelopment_top/trait_impl.rs b/scripty_botlists/src/lists/motiondevelopment_top/trait_impl.rs deleted file mode 100644 index 6ad270b7..00000000 --- a/scripty_botlists/src/lists/motiondevelopment_top/trait_impl.rs +++ /dev/null @@ -1,59 +0,0 @@ -use reqwest::{Client, RequestBuilder}; - -use crate::common::{PostStats, StatPoster}; - -pub struct MotionDevelopmentTop { - token: String, - bot_id: u64, -} - -impl MotionDevelopmentTop { - pub fn new(token: String, bot_id: u64) -> Self { - Self { token, bot_id } - } - - pub fn token(&self) -> &str { - &self.token - } - - pub fn bot_id(&self) -> u64 { - self.bot_id - } -} - -#[async_trait] -impl StatPoster for MotionDevelopmentTop { - async fn post_stats( - &self, - client: &Client, - stats: PostStats, - ) -> Result { - let request: RequestBuilder = client - .post(format!( - "https://motiondevelopment.top/api/v1.2/bots/{}/stats", - self.bot_id - )) - .header("Key", &self.token) - .json(&super::models::PostStats { - guilds: stats.server_count, - }); - let response = request.send().await?; - debug!("motiondevelopment.top response: {:?}", response); - let status = response.status(); - let maybe_error = if status.is_client_error() || status.is_server_error() { - Some(crate::common::Error::StatusCode(status)) - } else { - None - }; - let body = response.text().await?; - debug!("motiondevelopment.top response body: <{}>", body); - if let Some(maybe_error) = maybe_error { - return Err(maybe_error); - } - if status != reqwest::StatusCode::OK { - return Ok(false); - } - let body: super::models::PostStatsResponse = serde_json::from_str(&body)?; - Ok(body.success) - } -} diff --git a/scripty_botlists/src/lists/radarcord_net/mod.rs b/scripty_botlists/src/lists/radarcord_net/mod.rs deleted file mode 100644 index 87984563..00000000 --- a/scripty_botlists/src/lists/radarcord_net/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod models; -mod trait_impl; - -pub use trait_impl::RadarCordNet; diff --git a/scripty_botlists/src/lists/radarcord_net/models.rs b/scripty_botlists/src/lists/radarcord_net/models.rs deleted file mode 100644 index 53c37e04..00000000 --- a/scripty_botlists/src/lists/radarcord_net/models.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(Serialize, Debug, Copy, Clone)] -pub struct PostStats { - pub servers: usize, - pub shards: u16, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct PostStatsResponse { - pub code: u16, - pub message: String, -} diff --git a/scripty_botlists/src/lists/radarcord_net/trait_impl.rs b/scripty_botlists/src/lists/radarcord_net/trait_impl.rs deleted file mode 100644 index d746b2b1..00000000 --- a/scripty_botlists/src/lists/radarcord_net/trait_impl.rs +++ /dev/null @@ -1,56 +0,0 @@ -use reqwest::{Client, RequestBuilder}; - -use crate::common::{PostStats, StatPoster}; - -pub struct RadarCordNet { - token: String, - bot_id: u64, -} - -impl RadarCordNet { - pub fn new(token: String, bot_id: u64) -> Self { - Self { token, bot_id } - } - - pub fn token(&self) -> &str { - &self.token - } - - pub fn bot_id(&self) -> u64 { - self.bot_id - } -} - -#[async_trait] -impl StatPoster for RadarCordNet { - async fn post_stats( - &self, - client: &Client, - stats: PostStats, - ) -> Result { - let request: RequestBuilder = client - .post(format!( - "https://radarcord.net/api/bot/{}/stats", - self.bot_id - )) - .header("Authorization", &self.token) - .json(&super::models::PostStats { - servers: stats.server_count, - shards: stats.shard_count, - }); - let response = request.send().await?; - debug!("radarcord.net response: {:?}", response); - let status = response.status(); - let maybe_error = if status.is_client_error() || status.is_server_error() { - Some(crate::common::Error::StatusCode(status)) - } else { - None - }; - let body = response.text().await?; - debug!("motiondevelopment.top response body: <{}>", body); - if let Some(maybe_error) = maybe_error { - return Err(maybe_error); - } - Ok(status == reqwest::StatusCode::OK) - } -} diff --git a/scripty_botlists/src/lists/top_gg/mod.rs b/scripty_botlists/src/lists/top_gg/mod.rs index 94ef38db..cf93206c 100644 --- a/scripty_botlists/src/lists/top_gg/mod.rs +++ b/scripty_botlists/src/lists/top_gg/mod.rs @@ -1,4 +1,5 @@ mod models; mod trait_impl; +pub use models::*; pub use trait_impl::TopGG; diff --git a/scripty_botlists/src/lists/top_gg/models.rs b/scripty_botlists/src/lists/top_gg/models.rs index 92ce25bb..4aee255d 100644 --- a/scripty_botlists/src/lists/top_gg/models.rs +++ b/scripty_botlists/src/lists/top_gg/models.rs @@ -1,3 +1,9 @@ +#[derive(Debug, Serialize, Copy, Clone)] +pub struct PostStats { + pub server_count: usize, + pub shard_count: u16, +} + #[derive(Debug, Deserialize, Copy, Clone)] pub struct IncomingWebhook { pub bot: u64, @@ -7,15 +13,18 @@ pub struct IncomingWebhook { pub is_weekend: bool, } -#[derive(Debug, Deserialize, Copy, Clone)] +#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum VoteWebhookType { Upvote, Test, } +impl VoteWebhookType { + pub fn is_upvote(self) -> bool { + self == Self::Upvote + } -#[derive(Debug, Serialize)] -pub struct PostStats { - pub server_count: usize, - pub shard_count: u16, + pub fn is_test(self) -> bool { + self == Self::Test + } } diff --git a/scripty_botlists/src/lists/topcord_xyz/mod.rs b/scripty_botlists/src/lists/topcord_xyz/mod.rs deleted file mode 100644 index 9b2128bd..00000000 --- a/scripty_botlists/src/lists/topcord_xyz/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod models; -mod trait_impl; - -pub use trait_impl::TopCordXyz; diff --git a/scripty_botlists/src/lists/topcord_xyz/models.rs b/scripty_botlists/src/lists/topcord_xyz/models.rs deleted file mode 100644 index 53c37e04..00000000 --- a/scripty_botlists/src/lists/topcord_xyz/models.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(Serialize, Debug, Copy, Clone)] -pub struct PostStats { - pub servers: usize, - pub shards: u16, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct PostStatsResponse { - pub code: u16, - pub message: String, -} diff --git a/scripty_botlists/src/lists/topcord_xyz/trait_impl.rs b/scripty_botlists/src/lists/topcord_xyz/trait_impl.rs deleted file mode 100644 index bab4e7bf..00000000 --- a/scripty_botlists/src/lists/topcord_xyz/trait_impl.rs +++ /dev/null @@ -1,53 +0,0 @@ -use reqwest::{Client, RequestBuilder}; - -use crate::common::{PostStats, StatPoster}; - -pub struct TopCordXyz { - token: String, - bot_id: u64, -} - -impl TopCordXyz { - pub fn new(token: String, bot_id: u64) -> Self { - Self { token, bot_id } - } - - pub fn token(&self) -> &str { - &self.token - } - - pub fn bot_id(&self) -> u64 { - self.bot_id - } -} - -#[async_trait] -impl StatPoster for TopCordXyz { - async fn post_stats( - &self, - client: &Client, - stats: PostStats, - ) -> Result { - let request: RequestBuilder = client - .post(format!("https://api.topcord.xyz/bot/{}/stats", self.bot_id)) - .header("Authorization", &self.token) - .json(&super::models::PostStats { - servers: stats.server_count, - shards: stats.shard_count, - }); - let response = request.send().await?; - debug!("topcord.xyz response: {:?}", response); - let status = response.status(); - let maybe_error = if status.is_client_error() || status.is_server_error() { - Some(crate::common::Error::StatusCode(status)) - } else { - None - }; - let body = response.text().await?; - debug!("top.gg response body: <{}>", body); - if let Some(maybe_error) = maybe_error { - return Err(maybe_error); - } - Ok(status == reqwest::StatusCode::OK) - } -} diff --git a/scripty_botlists/src/lists/wumpus_store/mod.rs b/scripty_botlists/src/lists/wumpus_store/mod.rs new file mode 100644 index 00000000..164ec252 --- /dev/null +++ b/scripty_botlists/src/lists/wumpus_store/mod.rs @@ -0,0 +1,3 @@ +mod models; + +pub use models::*; diff --git a/scripty_botlists/src/lists/wumpus_store/models.rs b/scripty_botlists/src/lists/wumpus_store/models.rs new file mode 100644 index 00000000..520d064a --- /dev/null +++ b/scripty_botlists/src/lists/wumpus_store/models.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Deserialize, Copy, Clone)] +#[serde(rename_all = "camelCase")] +pub struct IncomingWebhook { + pub test_webhook: bool, + pub user_id: u64, + pub bot_id: u64, +} diff --git a/scripty_webserver/Cargo.toml b/scripty_webserver/Cargo.toml index 41d46cfb..30be5b9a 100644 --- a/scripty_webserver/Cargo.toml +++ b/scripty_webserver/Cargo.toml @@ -8,15 +8,16 @@ license = "EUPL-1.2" [dependencies] time = "0.3" -axum = "0.6" tracing = "0.1" serde_json = "1" scripty_db = { path = "../scripty_db" } scripty_i18n = { path = "../scripty_i18n" } scripty_utils = { path = "../scripty_utils" } scripty_config = { path = "../scripty_config" } -serde = { version = "1", features = ["derive"] } scripty_metrics = { path = "../scripty_metrics" } -tokio = { version = "1", features = ["parking_lot"] } +scripty_botlists = { path = "../scripty_botlists" } scripty_bot_utils = { path = "../scripty_bot_utils" } +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["parking_lot"] } +axum = { version = "0.6", features = ["headers", "json"] } sqlx = { version = "0.7", features = ["postgres", "macros", "migrate", "runtime-tokio-rustls"] } diff --git a/scripty_webserver/src/endpoints/mod.rs b/scripty_webserver/src/endpoints/mod.rs index f74d65af..eda158ab 100644 --- a/scripty_webserver/src/endpoints/mod.rs +++ b/scripty_webserver/src/endpoints/mod.rs @@ -2,6 +2,7 @@ pub mod bot_stats; pub mod languages; pub mod metrics; pub mod premium; +pub mod webhooks; pub fn router() -> axum::Router { axum::Router::new() @@ -9,4 +10,5 @@ pub fn router() -> axum::Router { .merge(metrics::router()) .merge(premium::router()) .merge(languages::router()) + .merge(webhooks::router()) } diff --git a/scripty_webserver/src/endpoints/webhooks/discordservices_net.rs b/scripty_webserver/src/endpoints/webhooks/discordservices_net.rs new file mode 100644 index 00000000..75d85dec --- /dev/null +++ b/scripty_webserver/src/endpoints/webhooks/discordservices_net.rs @@ -0,0 +1,96 @@ +use axum::{ + async_trait, + extract::FromRequestParts, + http::{request::Parts, StatusCode}, + Json, +}; +use scripty_bot_utils::extern_utils::{CreateEmbed, CreateMessage, UserId}; +use scripty_botlists::discordservices_net::{Bot, DiscordServicesNetIncomingWebhook}; + +use crate::errors::WebServerError; + +pub struct DiscordServicesNetAuthorization; +#[async_trait] +impl FromRequestParts for DiscordServicesNetAuthorization { + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let authorization = parts + .headers + .get("Authorization") + .ok_or((StatusCode::UNAUTHORIZED, "No Authorization header provided"))?; + let authorization = authorization.to_str().map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "Authorization header was not valid UTF-8", + ) + })?; + + let scripty_config::BotListsConfig::FullConfig { token: _, webhook } = + scripty_config::get_config() + .bot_lists + .get("discordservices_net") + .ok_or((StatusCode::UNAUTHORIZED, "Invalid token"))? + else { + return Err((StatusCode::UNAUTHORIZED, "Invalid token")); + }; + + if authorization != webhook { + Err((StatusCode::UNAUTHORIZED, "Invalid token")) + } else { + Ok(Self) + } + } +} + +pub async fn discordservices_net_incoming_webhook( + _authorization: DiscordServicesNetAuthorization, + Json(DiscordServicesNetIncomingWebhook { + user: Bot { id, .. }, + .. + }): Json, +) -> Result<(), WebServerError> { + // check if the user is opted out of notifications + let opted_out = sqlx::query!( + "SELECT vote_reminder_disabled FROM users WHERE user_id = $1", + scripty_utils::hash_user_id(id) + ) + .fetch_optional(scripty_db::get_db()) + .await? + .map(|row| row.vote_reminder_disabled) + .unwrap_or(false); + + // regardless, send them a message + let cache_http = scripty_bot_utils::extern_utils::get_cache_http(); + let dm_channel = UserId::new(id).create_dm_channel(&cache_http).await?; + dm_channel + .send_message( + &cache_http, + CreateMessage::new().embed( + CreateEmbed::new() + .title("Thanks for voting for Scripty on discordservices.net!") + .description(if opted_out { + "You can vote again in 12 hours. You're opted out of reminders, but if you \ + want to be notified, run `/vote_reminders True`. Thanks for your support!" + } else { + "You can vote again in 12 hours. We'll send you a reminder then. If you \ + don't want to be notified, run `/vote_reminders False`. Thanks for your \ + support!" + }), + ), + ) + .await?; + + // if they're opted in, set up a reminder for 12 hours from now + sqlx::query!( + "INSERT INTO vote_reminders (user_id, site_id, next_reminder) + VALUES ($1, 2, NOW() + INTERVAL '12 hours') + ON CONFLICT (user_id, site_id) + DO UPDATE SET next_reminder = NOW() + INTERVAL '12 hours'", + id as i64 + ) + .execute(scripty_db::get_db()) + .await?; + + Ok(()) +} diff --git a/scripty_webserver/src/endpoints/webhooks/mod.rs b/scripty_webserver/src/endpoints/webhooks/mod.rs new file mode 100644 index 00000000..6944f096 --- /dev/null +++ b/scripty_webserver/src/endpoints/webhooks/mod.rs @@ -0,0 +1,18 @@ +use axum::routing::post; + +mod discordservices_net; +mod top_gg; +mod wumpus_store; + +pub fn router() -> axum::Router { + axum::Router::new() + .route("/webhooks/top_gg", post(top_gg::top_gg_incoming_webhook)) + .route( + "/webhooks/wumpus_store", + post(wumpus_store::wumpus_store_incoming_webhook), + ) + .route( + "/webhooks/discordservices_net", + post(discordservices_net::discordservices_net_incoming_webhook), + ) +} diff --git a/scripty_webserver/src/endpoints/webhooks/top_gg.rs b/scripty_webserver/src/endpoints/webhooks/top_gg.rs new file mode 100644 index 00000000..4a0d1e3c --- /dev/null +++ b/scripty_webserver/src/endpoints/webhooks/top_gg.rs @@ -0,0 +1,98 @@ +use axum::{ + async_trait, + extract::FromRequestParts, + http::{request::Parts, StatusCode}, + Json, +}; +use scripty_bot_utils::extern_utils::{CreateEmbed, CreateEmbedFooter, CreateMessage, UserId}; +use scripty_botlists::top_gg::IncomingWebhook; + +use crate::errors::WebServerError; + +pub struct TopGgAuthorization; +#[async_trait] +impl FromRequestParts for TopGgAuthorization { + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let authorization = parts + .headers + .get("Authorization") + .ok_or((StatusCode::UNAUTHORIZED, "No Authorization header provided"))?; + let authorization = authorization.to_str().map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "Authorization header was not valid UTF-8", + ) + })?; + + let scripty_config::BotListsConfig::FullConfig { token: _, webhook } = + scripty_config::get_config() + .bot_lists + .get("top_gg") + .ok_or((StatusCode::UNAUTHORIZED, "Invalid token"))? + else { + return Err((StatusCode::UNAUTHORIZED, "Invalid token")); + }; + + if authorization != webhook { + Err((StatusCode::UNAUTHORIZED, "Invalid token")) + } else { + Ok(Self) + } + } +} + +pub async fn top_gg_incoming_webhook( + _authorization: TopGgAuthorization, + Json(IncomingWebhook { user, kind, .. }): Json, +) -> Result<(), WebServerError> { + // check if the user is opted out of notifications + let opted_out = sqlx::query!( + "SELECT vote_reminder_disabled FROM users WHERE user_id = $1", + scripty_utils::hash_user_id(user) + ) + .fetch_optional(scripty_db::get_db()) + .await? + .map(|row| row.vote_reminder_disabled) + .unwrap_or(false); + + // regardless, send them a message + let cache_http = scripty_bot_utils::extern_utils::get_cache_http(); + let dm_channel = UserId::new(user).create_dm_channel(&cache_http).await?; + dm_channel + .send_message( + &cache_http, + CreateMessage::new().embed( + CreateEmbed::new() + .title("Thanks for voting for Scripty on top.gg!") + .description(if opted_out { + "You can vote again in 12 hours. You're opted out of reminders, but if you \ + want to be notified, run `/vote_reminders True`. Thanks for your support!" + } else { + "You can vote again in 12 hours. We'll send you a reminder then. If you \ + don't want to be notified, run `/vote_reminders False`. Thanks for your \ + support!" + }) + .footer(CreateEmbedFooter::new(if kind.is_test() { + "Webhook test" + } else { + "" + })), + ), + ) + .await?; + + // if they're opted in, set up a reminder for 12 hours from now + sqlx::query!( + "INSERT INTO vote_reminders (user_id, site_id, next_reminder) + VALUES ($1, 1, NOW() + INTERVAL '12 hours') + ON CONFLICT (user_id, site_id) + DO UPDATE SET next_reminder = NOW() + INTERVAL '12 hours'", + user as i64 + ) + .execute(scripty_db::get_db()) + .await?; + + Ok(()) +} diff --git a/scripty_webserver/src/endpoints/webhooks/wumpus_store.rs b/scripty_webserver/src/endpoints/webhooks/wumpus_store.rs new file mode 100644 index 00000000..95ab0a8b --- /dev/null +++ b/scripty_webserver/src/endpoints/webhooks/wumpus_store.rs @@ -0,0 +1,102 @@ +use axum::{ + async_trait, + extract::FromRequestParts, + http::{request::Parts, StatusCode}, + Json, +}; +use scripty_bot_utils::extern_utils::{CreateEmbed, CreateEmbedFooter, CreateMessage, UserId}; +use scripty_botlists::wumpus_store::IncomingWebhook; + +use crate::errors::WebServerError; + +pub struct WumpusStoreAuthorization; +#[async_trait] +impl FromRequestParts for WumpusStoreAuthorization { + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let authorization = parts + .headers + .get("Authorization") + .ok_or((StatusCode::UNAUTHORIZED, "No Authorization header provided"))?; + let authorization = authorization.to_str().map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "Authorization header was not valid UTF-8", + ) + })?; + + let scripty_config::BotListsConfig::FullConfig { token: _, webhook } = + scripty_config::get_config() + .bot_lists + .get("wumpus_store") + .ok_or((StatusCode::UNAUTHORIZED, "Invalid token"))? + else { + return Err((StatusCode::UNAUTHORIZED, "Invalid token")); + }; + + if authorization != webhook { + Err((StatusCode::UNAUTHORIZED, "Invalid token")) + } else { + Ok(Self) + } + } +} + +pub async fn wumpus_store_incoming_webhook( + _authorization: WumpusStoreAuthorization, + Json(IncomingWebhook { + test_webhook, + user_id, + .. + }): Json, +) -> Result<(), WebServerError> { + // check if the user is opted out of notifications + let opted_out = sqlx::query!( + "SELECT vote_reminder_disabled FROM users WHERE user_id = $1", + scripty_utils::hash_user_id(user_id) + ) + .fetch_optional(scripty_db::get_db()) + .await? + .map(|row| row.vote_reminder_disabled) + .unwrap_or(false); + + // regardless, send them a message + let cache_http = scripty_bot_utils::extern_utils::get_cache_http(); + let dm_channel = UserId::new(user_id).create_dm_channel(&cache_http).await?; + dm_channel + .send_message( + &cache_http, + CreateMessage::new().embed( + CreateEmbed::new() + .title("Thanks for voting for Scripty on wumpus.store!") + .description(if opted_out { + "You can vote again in 12 hours. You're opted out of reminders, but if you \ + want to be notified, run `/vote_reminders True`. Thanks for your support!" + } else { + "You can vote again in 12 hours. We'll send you a reminder then. If you \ + don't want to be notified, run `/vote_reminders False`. Thanks for your \ + support!" + }) + .footer(CreateEmbedFooter::new(if test_webhook { + "Webhook test" + } else { + "" + })), + ), + ) + .await?; + + // if they're opted in, set up a reminder for 12 hours from now + sqlx::query!( + "INSERT INTO vote_reminders (user_id, site_id, next_reminder) + VALUES ($1, 3, NOW() + INTERVAL '12 hours') + ON CONFLICT (user_id, site_id) + DO UPDATE SET next_reminder = NOW() + INTERVAL '12 hours'", + user_id as i64 + ) + .execute(scripty_db::get_db()) + .await?; + + Ok(()) +}