From 6f997edb91152644e4678fb12f745bcc9e7b3007 Mon Sep 17 00:00:00 2001 From: Stachelbeere1248 Date: Tue, 18 Feb 2025 19:25:07 +0100 Subject: [PATCH] impl api caching, helpstart bot list --- .rustfmt.toml | 4 +- Cargo.lock | 56 +++++++- Cargo.toml | 2 + src/commands/accountv2.rs | 224 ++++++++++++++++++++++---------- src/commands/bots.rs | 27 ---- src/commands/helpstart.rs | 71 +++++----- src/commands/lfg.rs | 35 ++++- src/commands/mod.rs | 1 - src/commands/xd.rs | 21 ++- src/data/account_links.rs | 1 + src/data/helpstart_api.rs | 50 +++++++ src/data/mod.rs | 3 + src/data/mojang.rs | 67 ++++++++++ src/error.rs | 20 ++- src/handlers/bot_interaction.rs | 54 ++++++-- src/handlers/thread.rs | 5 +- src/main.rs | 114 +++++++++++----- 17 files changed, 563 insertions(+), 192 deletions(-) delete mode 100644 src/commands/bots.rs create mode 100644 src/data/account_links.rs create mode 100644 src/data/helpstart_api.rs create mode 100644 src/data/mod.rs create mode 100644 src/data/mojang.rs diff --git a/.rustfmt.toml b/.rustfmt.toml index 6585b03..981b3fb 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,4 @@ -max_width = 140 +max_width = 100 use_small_heuristics = "Default" reorder_imports = true -format_strings = true \ No newline at end of file +format_strings = true diff --git a/Cargo.lock b/Cargo.lock index ad61a83..bda99c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,6 +398,20 @@ dependencies = [ "serde", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -717,6 +731,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getset" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded738faa0e88d3abc9d1a13cb11adc2073c400969eeb8793cf7132589959fc" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1315,7 +1341,7 @@ checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" dependencies = [ "crossbeam-channel", "crossbeam-utils", - "dashmap", + "dashmap 5.5.3", "skeptic", "smallvec", "tagptr", @@ -1620,6 +1646,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -2090,7 +2138,7 @@ dependencies = [ "bytes", "chrono", "command_attr", - "dashmap", + "dashmap 5.5.3", "flate2", "futures", "fxhash", @@ -2842,7 +2890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549e54551d85ba6718a95333d9bc4367f69793d7aba638de30f8d25a1f554a1d" dependencies = [ "chrono", - "dashmap", + "dashmap 5.5.3", "hashbrown 0.14.5", "mini-moka", "parking_lot", @@ -3419,7 +3467,9 @@ dependencies = [ name = "zmp-bot" version = "0.1.0" dependencies = [ + "dashmap 6.1.0", "futures", + "getset", "poise", "reqwest 0.12.9", "serde", diff --git a/Cargo.toml b/Cargo.toml index 034ea7c..0005b42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,5 @@ tokio = { version = "1.42.0", features = ["rt-multi-thread"] } tracing = { version = "0.1.41" } sqlx = { version = "0.8.2", features = ["sqlite", "sqlx-sqlite", "runtime-tokio"]} futures = "0.3.31" +dashmap = "6.1.0" +getset = "0.1.4" diff --git a/src/commands/accountv2.rs b/src/commands/accountv2.rs index 53d99d5..88a43e2 100644 --- a/src/commands/accountv2.rs +++ b/src/commands/accountv2.rs @@ -1,14 +1,18 @@ use poise::CreateReply; -use reqwest::{Client, Response}; +use reqwest::Client; use serde::Deserialize; use serenity::all::ButtonStyle; -use serenity::all::{ChannelId, CreateActionRow, CreateAllowedMentions, CreateButton, CreateMessage, ReactionType, User}; +use serenity::all::{ + ChannelId, CreateActionRow, CreateAllowedMentions, CreateButton, CreateMessage, ReactionType, + User, +}; use sqlx::{query_as, Pool, Sqlite}; use std::ops::Add; use crate::commands::command_helper::cooldown; use crate::error::Error; use crate::error::Error::Other; +use crate::Caches; use crate::Context; #[derive(Deserialize)] @@ -16,65 +20,68 @@ struct Links { #[serde(rename = "DISCORD")] pub discord: Option, } + #[derive(Deserialize)] struct SocialMedia { pub links: Option, } + #[derive(Deserialize)] struct HypixelPlayer { #[serde(rename = "socialMedia")] pub social_media: Option, } + #[derive(Deserialize)] struct HypixelResponse { #[serde(rename = "player")] pub player: HypixelPlayer, } -#[derive(Deserialize)] -struct MojangPlayer { - pub id: String, - pub name: String, -} #[derive(PartialEq, sqlx::FromRow)] -struct Uuid { - uuid: String, +pub(crate) struct Uuid { + pub(crate) uuid: String, } + impl Uuid { fn get(&self) -> &str { self.uuid.as_str() } - async fn for_ign(ign: &str) -> Result { - let url: String = format!("https://api.mojang.com/users/profiles/minecraft/{ign}"); - let response_400: Response = reqwest::get(url).await?.error_for_status()?; - let deserialized = response_400.json::().await?; - let uuid = Uuid { uuid: deserialized.id }; - Ok(uuid) + async fn for_ign(ign: &String, cli: &Client, c: &Caches) -> Result { + let uuid = crate::data::mojang::uuid(c, cli, ign.clone()).await?; + Ok(Self { uuid }) } - async fn ign(&self) -> Result { - let url: String = format!("https://sessionserver.mojang.com/session/minecraft/profile/{}", self.uuid); - let response_400: Response = reqwest::get(url).await?.error_for_status()?; - let deserialized = response_400.json::().await?; - Ok(deserialized.name) + async fn ign(&self, c: &Caches, cli: &Client) -> Result { + let ign = crate::data::mojang::name(c, cli, self.uuid.clone()).await?; + Ok(ign) } } + #[derive(PartialEq)] struct DiscordId { id: u64, } + impl Uuid { async fn has_discord_user(&self, user: &User, client: &Client) -> Result { let url: String = format!("https://api.hypixel.net/v2/player?uuid={}", self.uuid); - let response_400: Response = client.get(url).send().await?.error_for_status()?; - let deserialized = response_400.json::().await?; - let matches = deserialized + let res: HypixelResponse = client + .get(url) + .send() + .await? + .error_for_status()? + .json::() + .await?; + let matches = res .player .social_media .and_then(|sm| sm.links) .and_then(|l| l.discord) .ok_or(Other( - "The Hypixel profile has no Discord account linked. Please follow the steps in <#1256219552568840263>".to_string(), + "The Hypixel profile has no Discord account linked. Please follow the steps in \ + <#1256219552568840263>" + .to_string(), ))? == user.name; Ok(matches) @@ -93,11 +100,13 @@ where }) } } -struct Link { + +pub(crate) struct Link { link_id: u16, discord_ids: Vec, - minecraft_accounts: Vec, + pub(crate) minecraft_accounts: Vec, } + impl Link { fn new(link_id: u16) -> Self { Link { @@ -108,17 +117,23 @@ impl Link { } async fn minecraft(mut self, pool: &Pool) -> Result { let link_id: i16 = self.link_id.cast_signed(); - self.minecraft_accounts = - query_as(format!("SELECT minecraft_uuid AS uuid FROM minecraft_links WHERE link_id = {link_id};").as_str()) - .fetch_all(pool) - .await?; + self.minecraft_accounts = query_as( + format!( + "SELECT minecraft_uuid AS uuid FROM minecraft_links WHERE link_id = {link_id};" + ) + .as_str(), + ) + .fetch_all(pool) + .await?; Ok(self) } async fn discord(mut self, pool: &Pool) -> Result { let link_id: i16 = self.link_id.cast_signed(); - self.discord_ids = query_as(format!("SELECT discord_id FROM discord_links WHERE link_id = {link_id};").as_str()) - .fetch_all(pool) - .await?; + self.discord_ids = query_as( + format!("SELECT discord_id FROM discord_links WHERE link_id = {link_id};").as_str(), + ) + .fetch_all(pool) + .await?; Ok(self) } } @@ -126,7 +141,7 @@ impl Link { slash_command, subcommands("add", "list"), install_context = "User|Guild", - interaction_context = "Guild|BotDm|PrivateChannel", + interaction_context = "Guild|BotDm|PrivateChannel" )] pub(crate) async fn account(_ctx: Context<'_>) -> Result<(), Error> { // root of slash-commands is not invokable. @@ -145,42 +160,79 @@ pub(crate) async fn add( #[description = "admin-only"] force: Option, ) -> Result<(), Error> { ctx.defer().await?; - let force: bool = force.unwrap_or(false) && ctx.framework().options.owners.contains(&ctx.author().id) && { - let _ = user.as_ref().ok_or(Other( - "Warning: attempted to run forced account add without specifying a target Discord account.".to_string(), - ))?; - true - }; + let force: bool = + force.unwrap_or(false) && ctx.framework().options.owners.contains(&ctx.author().id) && { + let _ = user.as_ref().ok_or(Other( + "Warning: attempted to run forced account add without specifying a target Discord \ + account." + .to_string(), + ))?; + true + }; let user: User = user.unwrap_or(ctx.author().clone()); - let uuid: Uuid = Uuid::for_ign(ign.as_str()).await?; - match force || uuid.has_discord_user(&user, &ctx.data().hypixel_api_client).await? { + let uuid: Uuid = Uuid::for_ign(&ign, &ctx.data().clients.general, &ctx.data().caches).await?; + match force + || uuid + .has_discord_user(&user, &ctx.data().clients.hypixel_api_client) + .await? + { true => { let pool = &ctx.data().sqlite_pool; let status: &str = match link_id_from_minecraft(pool, uuid.get()).await { None => match link_id_from_discord(pool, user.id.get()).await { None => { let id = new_link_id(pool).await?; - sqlx::query(format!("INSERT INTO discord_links VALUES ({}, {});", id.inner, user.id.get()).as_str()) - .execute(pool) - .await?; - sqlx::query(format!("INSERT INTO minecraft_links VALUES ({}, \"{}\");", id.inner, uuid.get()).as_str()) - .execute(pool) - .await?; + sqlx::query( + format!( + "INSERT INTO discord_links VALUES ({}, {});", + id.inner, + user.id.get() + ) + .as_str(), + ) + .execute(pool) + .await?; + sqlx::query( + format!( + "INSERT INTO minecraft_links VALUES ({}, \"{}\");", + id.inner, + uuid.get() + ) + .as_str(), + ) + .execute(pool) + .await?; "Linked your Discord and Minecraft account." } Some(dc_id) => { - sqlx::query(format!("INSERT INTO minecraft_links VALUES ({}, \"{}\");", dc_id.inner, uuid.get()).as_str()) - .execute(pool) - .await?; - "Your Discord account has previously had an account linked. Added the new link." + sqlx::query( + format!( + "INSERT INTO minecraft_links VALUES ({}, \"{}\");", + dc_id.inner, + uuid.get() + ) + .as_str(), + ) + .execute(pool) + .await?; + "Your Discord account has previously had an account linked. Added the new \ + link." } }, Some(mc_id) => match link_id_from_discord(pool, user.id.get()).await { None => { - sqlx::query(format!("INSERT INTO discord_links VALUES ({}, {});", mc_id.inner, user.id.get()).as_str()) - .execute(pool) - .await?; - "Your Minecraft account has previously had an account linked. Added the new link." + sqlx::query( + format!( + "INSERT INTO discord_links VALUES ({}, {});", + mc_id.inner, + user.id.get() + ) + .as_str(), + ) + .execute(pool) + .await?; + "Your Minecraft account has previously had an account linked. Added the \ + new link." } Some(dc_id) => { sqlx::query( @@ -201,17 +253,24 @@ pub(crate) async fn add( ) .execute(pool) .await?; - "Both your Discord and Minecraft account had linked accounts. Merged all account links." + "Both your Discord and Minecraft account had linked accounts. Merged all \ + account links." } }, }; - let s = format!("Verification request for <@{}> with IGN `{}`", user.id.get(), ign); + let s = format!( + "Verification request for <@{}> with IGN `{}`", + user.id.get(), + ign + ); ChannelId::new(1257776992497959075) .send_message( ctx, CreateMessage::new() .content(s) - .allowed_mentions(CreateAllowedMentions::new().empty_roles().all_users(true)) + .allowed_mentions( + CreateAllowedMentions::new().empty_roles().all_users(true), + ) .components(vec![CreateActionRow::Buttons(vec![ CreateButton::new("accept_verification") .emoji(ReactionType::from('✅')) @@ -229,20 +288,25 @@ pub(crate) async fn add( Ok(()) } false => Err(Error::Other(format!( - "The Discord account linked on Hypixel does not match the specified discord account.\nPlease set your linked Discord account \ - on Hypixel to `{}`.", + "The Discord account linked on Hypixel does not match the specified discord \ + account.\nPlease set your linked Discord account on Hypixel to `{}`.", user.name ))), } } -#[poise::command(slash_command, ephemeral = "true", context_menu_command = "Account list")] +#[poise::command( + slash_command, + ephemeral = "true", + context_menu_command = "Account list" +)] /// List a users linked minecraft Accounts. pub(crate) async fn list(ctx: Context<'_>, user: User) -> Result<(), Error> { ctx.defer().await?; cooldown(&ctx, 600, 300)?; let pool: &Pool = &ctx.data().sqlite_pool; - let s: String = list_string(pool, &user).await?; + let s: String = + list_string(pool, &user, &ctx.data().caches, &ctx.data().clients.general).await?; ctx.send( CreateReply::default() .content(s) @@ -252,19 +316,33 @@ pub(crate) async fn list(ctx: Context<'_>, user: User) -> Result<(), Error> { Ok(()) } -pub(crate) async fn list_string(pool: &Pool, user: &User) -> Result { +pub(crate) async fn get_link(user: &User, pool: &Pool) -> Result { let link_id: u16 = link_id_from_discord(pool, user.id.get()) .await .expect("This user has no linked accounts") .into(); - let link: Link = Link::new(link_id).minecraft(pool).await?.discord(pool).await?; + let link = Link::new(link_id) + .minecraft(pool) + .await? + .discord(pool) + .await?; + Ok(link) +} + +pub(crate) async fn list_string( + pool: &Pool, + user: &User, + c: &Caches, + cli: &Client, +) -> Result { + let link = get_link(user, pool).await?; let mut discord_list = String::from("### Discord:"); for dc in link.discord_ids { discord_list.push_str(format!("\n- <@{}>", dc.id).as_str()); } let mut minecraft_list = String::from("### Minecraft:"); for mc in link.minecraft_accounts { - minecraft_list.push_str(format!("\n- `{}`", mc.ign().await?).as_str()); + minecraft_list.push_str(format!("\n- `{}`", mc.ign(c, cli).await?).as_str()); } Ok(format!( "## Account list for member #{}:\n{}\n{}", @@ -280,10 +358,16 @@ pub(crate) async fn remove(_ctx: Context<'_>) -> Result<(), Error> { } async fn link_id_from_minecraft(pool: &Pool, minecraft_uuid: &str) -> Option { - query_as(format!("SELECT link_id FROM minecraft_links WHERE minecraft_uuid = \"{minecraft_uuid}\" LIMIT 1;").as_str()) - .fetch_optional(pool) - .await - .expect("Database error: fetching link id by uuid") + query_as( + format!( + "SELECT link_id FROM minecraft_links WHERE minecraft_uuid = \"{minecraft_uuid}\" \ + LIMIT 1;" + ) + .as_str(), + ) + .fetch_optional(pool) + .await + .expect("Database error: fetching link id by uuid") } async fn link_id_from_discord(pool: &Pool, snowflake: u64) -> Option { query_as( diff --git a/src/commands/bots.rs b/src/commands/bots.rs deleted file mode 100644 index 0b294a6..0000000 --- a/src/commands/bots.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::string::String; - -use poise::CreateReply; - -use crate::error::Error; -use crate::Context; - -#[poise::command( - slash_command, - owners_only, - install_context = "User", - interaction_context = "Guild|BotDm|PrivateChannel", - ephemeral = "false" -)] -/// Change how many helpstart bots are online, to limit usage of helpstart pings. -pub(crate) async fn bots( - ctx: Context<'_>, - #[min = 0_u8] - #[description = "default: 0"] - bots: u8, -) -> Result<(), Error> { - ctx.defer_ephemeral().await?; - *ctx.data().bots.write().await = bots; - let content = format!("{} bots are now registered as available", bots).to_string(); - ctx.send(CreateReply::default().content(content)).await?; - Ok(()) -} diff --git a/src/commands/helpstart.rs b/src/commands/helpstart.rs index 09364a1..3076187 100644 --- a/src/commands/helpstart.rs +++ b/src/commands/helpstart.rs @@ -1,40 +1,45 @@ -use poise::CreateReply; -use serenity::all::CreateAllowedMentions; - -use crate::commands::command_helper; +use crate::data::helpstart_api::fetch_all; +use crate::data::helpstart_api::ListType::*; +use crate::data::mojang::name; use crate::error::Error; use crate::Context; +use poise::CreateReply; -#[poise::command(slash_command, install_context = "Guild", interaction_context = "Guild", ephemeral = "false")] -/// Ping the @helpstart to fill a queue. -pub(crate) async fn helpstart( - ctx: Context<'_>, - #[min = 1_u8] - #[max = 3_u8] - #[description = "amount of players in your party, DO NOT include bots"] - #[rename = "current"] - current_players: u8, -) -> Result<(), Error> { - let needed_players = 4 - current_players; - let bots = *ctx.data().bots.read().await; - let g = ctx.guild_id().unwrap().get(); - let mut reply = CreateReply::default(); - let ping = match g { - 1256217633959841853_u64 => 1257411572092113017_u64, - _ => 0_u64, - }; +#[poise::command( + slash_command, + install_context = "Guild|User", + interaction_context = "Guild|BotDm", + ephemeral = "true" +)] +// Check for bots available to you. +pub(crate) async fn helpstart(ctx: Context<'_>) -> Result<(), Error> { + let links = super::accountv2::get_link(ctx.author(), &ctx.data().sqlite_pool).await?; + let mc_accounts = links + .minecraft_accounts + .into_iter() + .map(|a| name(&ctx.data().caches, &ctx.data().clients.general, a.uuid)) + .collect::>(); + let mc_accounts = futures::future::try_join_all(mc_accounts).await?; + let bots = fetch_all(&ctx.data().clients.local_api_client).await?.bots; + let usable = bots + .iter() + .filter_map(|b| { + if match b.list_type() { + Whitelist => b.list().iter().any(|w| mc_accounts.contains(w)), + Blacklist => mc_accounts.iter().any(|m| !b.list().contains(m)), + } { + Some(b.username().as_str()) + } else { + None + } + }) + .collect::>(); + let s: String = usable.join(", "); - reply = if bots >= needed_players { - reply - .content("Bots available. Please use <@424767825001971715> in the bot-commands channel instead.") - .ephemeral(true) - } else { - command_helper::cooldown(&ctx, 1200, 600)?; - reply - .content(format!("## <@&{ping}>\nneed: {}", needed_players - bots)) - .ephemeral(false) - .allowed_mentions(CreateAllowedMentions::new().roles(vec![ping])) - }; + let reply = CreateReply::default().content(format!( + "Accounts you can use: {s}\nTotal registered bots: {}", + bots.len() + )); ctx.send(reply).await?; Ok(()) } diff --git a/src/commands/lfg.rs b/src/commands/lfg.rs index f5ae501..1b18f22 100644 --- a/src/commands/lfg.rs +++ b/src/commands/lfg.rs @@ -43,7 +43,12 @@ pub enum Difficulty { #[name = "R.I.P."] Rip, } -#[poise::command(slash_command, install_context = "Guild", interaction_context = "Guild", ephemeral = "false")] +#[poise::command( + slash_command, + install_context = "Guild", + interaction_context = "Guild", + ephemeral = "false" +)] /// Find a team for Hypixel Zombies. pub(crate) async fn lfg( ctx: Context<'_>, @@ -166,19 +171,39 @@ pub(crate) async fn expert( let (ping, allowed_roles): (u64, Vec) = match mode { ExpertMap::Speedrun => ( 1295322375637958716, - ROLE_LIST.iter().skip(2).map(|tier| [tier[4], tier[5]]).flatten().collect(), + ROLE_LIST + .iter() + .skip(2) + .map(|tier| [tier[4], tier[5]]) + .flatten() + .collect(), ), ExpertMap::DeadEnd => ( 1295321319344177172, - ROLE_LIST.iter().skip(2).map(|tier| [tier[1], tier[5]]).flatten().collect(), + ROLE_LIST + .iter() + .skip(2) + .map(|tier| [tier[1], tier[5]]) + .flatten() + .collect(), ), ExpertMap::BadBlood => ( 1295322259631640607, - ROLE_LIST.iter().skip(2).map(|tier| [tier[2], tier[5]]).flatten().collect(), + ROLE_LIST + .iter() + .skip(2) + .map(|tier| [tier[2], tier[5]]) + .flatten() + .collect(), ), ExpertMap::AlienArcadium => ( 1295322327910842441, - ROLE_LIST.iter().skip(2).map(|tier| [tier[3], tier[5]]).flatten().collect(), + ROLE_LIST + .iter() + .skip(2) + .map(|tier| [tier[3], tier[5]]) + .flatten() + .collect(), ), }; let is_expert: bool = ctx diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 906a531..1f23f92 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,5 +1,4 @@ pub(crate) mod accountv2; -pub(crate) mod bots; pub(crate) mod command_helper; pub(crate) mod helpstart; pub(crate) mod lfg; diff --git a/src/commands/xd.rs b/src/commands/xd.rs index df353ab..bd689b4 100644 --- a/src/commands/xd.rs +++ b/src/commands/xd.rs @@ -1,13 +1,20 @@ use crate::error::Error; use crate::Context; -const XD: &str = "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⡿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⡿⠿⠿⠿⠿⠿⠿⠿⢿⡿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\\ - n⣿⣿⣿⣧⣄⡀⠀⠀⠀⢀⣠⣼⣿⣿⣿⣿⣧⣄⡀⠀⠀⠀⣀⣤⣼⣷⣦⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⢿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠻⣿⣿⣿⣿⣿⣿⠟⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣶⣦⣄⡀⠀⠀⠙⢿⣿⣿⣿⣿\\ - n⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠙⣿⣿⣿⣿⠋⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠈⢿⣿⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠈⢿⡿⠁⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⢸⣿⣿⣿\\ - n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⣿⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡗⠀⠀⠀⠐⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠀⣿⣿⣿\\ - n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⣠⡀⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⣰⣿⣷⡄⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⣾⣿⣿⣿\\ - n⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀⢀⣼⣿⣿⣿⣿⣆⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⢀⣼⣿⣿⣿⣿\n⣿⣿⣿⣿⣿⣿⠋⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠙⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⠿⠛⠁⠀⠀⣠⣾⣿⣿⣿⣿⣿\\ - n⣿⣿⣿⠿⠛⠁⠀⠀⠀⠙⠻⣿⣿⣿⣿⣿⡿⠟⠋⠀⠀⠀⠈⠛⠻⡿⠟⠛⠁⠀⠀⠈⠉⠉⠉⠉⠀⠀⠀⠀⣀⣴⣾⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⣶⣶⣶⣶⣶⣶⣶⣶⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣶⣶⣷⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\\ +const XD: &str = "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\\ + n⣿⣿⣿⡿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⡿⠿⠿⠿⠿⠿⠿⠿⢿⡿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\\ + n⣿⣿⣿⣧⣄⡀⠀⠀⠀⢀⣠⣼⣿⣿⣿⣿⣧⣄⡀⠀⠀⠀⣀⣤⣼⣷⣦⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⢿⣿⣿⣿⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠻⣿⣿⣿⣿⣿⣿⠟⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣶⣦⣄⡀⠀⠀⠙⢿⣿⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠙⣿⣿⣿⣿⠋⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠈⢿⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠈⢿⡿⠁⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⢸⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡗⠀⠀⠀⠐⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠀⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⣠⡀⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⣰⣿⣷⡄⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⣾⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀⢀⣼⣿⣿⣿⣿⣆⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⢀⣼⣿⣿⣿⣿\\ + n⣿⣿⣿⣿⣿⣿⠋⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠙⣿⣿⣿⣿⣿⣿⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⠿⠛⠁⠀⠀⣠⣾⣿⣿⣿⣿⣿\\ + n⣿⣿⣿⠿⠛⠁⠀⠀⠀⠙⠻⣿⣿⣿⣿⣿⡿⠟⠋⠀⠀⠀⠈⠛⠻⡿⠟⠛⠁⠀⠀⠈⠉⠉⠉⠉⠀⠀⠀⠀⣀⣴⣾⣿⣿⣿⣿⣿⣿⣿\\ + n⣿⣿⣿⣶⣶⣶⣶⣶⣶⣶⣶⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣶⣶⣷⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\\ n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n"; #[poise::command( diff --git a/src/data/account_links.rs b/src/data/account_links.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/data/account_links.rs @@ -0,0 +1 @@ + diff --git a/src/data/helpstart_api.rs b/src/data/helpstart_api.rs new file mode 100644 index 0000000..f4c458c --- /dev/null +++ b/src/data/helpstart_api.rs @@ -0,0 +1,50 @@ +use crate::Error; +use getset::Getters; +use reqwest::Client; +use serde::Deserialize; + +#[derive(Deserialize, Getters)] +pub(crate) struct Response { + pub(crate) bots: Vec, +} + +#[derive(Deserialize, Getters)] +#[getset(get = "pub(crate)")] +pub(crate) struct Bot { + username: String, + list_type: ListType, + list: Vec, + strict: bool, + /* we don't care abt lobby data + * lobby_name: String, + * lobby_number: u8, + */ + in_party: bool, + /* we don't care what script the bot is running + * client_gui_version: String, + * client_version: String, + */ + last_updated: f64, + last_updated_utc: String, //TODO: DateTime + note: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub(crate) enum ListType { + Whitelist, + Blacklist, +} + +pub(crate) async fn fetch_all(client: &Client) -> Result { + let url = "localhost:6969/list"; + let response: Response = client + .get(url) + .send() + .await? + .error_for_status()? + .json() + .await?; + + Ok(response) +} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..17f34fa --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod account_links; +pub(crate) mod helpstart_api; +pub(crate) mod mojang; diff --git a/src/data/mojang.rs b/src/data/mojang.rs new file mode 100644 index 0000000..a778b82 --- /dev/null +++ b/src/data/mojang.rs @@ -0,0 +1,67 @@ +use crate::Caches; +use crate::Error; +use reqwest::Client; +use std::time::Duration; +use std::time::Instant; + +const TTL: Duration = Duration::from_days(1); + +#[derive(serde::Deserialize)] +struct Response { + #[serde(rename = "id")] + uuid: String, + name: String, +} + +// :trollface: +macro_rules! cache_hit_handler { + ($name:ident, $url:expr) => { + macro_rules! inner1 { + ($c:expr, $input:expr) => { + $c.$name.get($input).and_then(|a| { + if a.1 > Instant::now() + TTL { + None + } else { + Some(a.0.clone()) + } + }) + }; + } + + macro_rules! inner2 { + ($cli:expr, $input:expr, $c:expr) => {{ + let a = $cli + .get(format!($url, $input.as_str())) + .send() + .await? + .error_for_status()? + .json::() + .await? + .$name; + let _old = $c.$name.insert($input, (a.clone(), Instant::now())); + a + }}; + } + + pub(crate) async fn $name<'a>( + c: &'a Caches, + cli: &'a Client, + input: String, + ) -> Result { + match inner1!(c, &input) { + None => { + let updated = inner2!(cli, input, c); + Ok(updated) + } + Some(hit) => Ok(hit), + } + } + }; +} + +cache_hit_handler!( + name, + "https://api.minecraftservices.com/minecraft/profile/lookup/{}" +); + +cache_hit_handler!(uuid, "https://api.mojang.com/users/profiles/minecraft/{}"); diff --git a/src/error.rs b/src/error.rs index 807420b..40898c3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,7 +26,11 @@ impl Display for Error { Error::SqlxError(e) => write!(f, "SQLx Error: {}", e), Error::ApiError(e) => write!(f, "HTTPS Error (Hypixel / Mojang API):\n{}", e), Error::SerenityError(e) => write!(f, "Discord Error:\n {}", e), - Error::OnCooldown(d) => write!(f, "This command is on cooldown. {}s remaining.", d.as_secs()), + Error::OnCooldown(d) => write!( + f, + "This command is on cooldown. {}s remaining.", + d.as_secs() + ), Error::Other(s) => write!(f, "{}", s), } } @@ -53,14 +57,20 @@ impl From for Error { pub(crate) async fn handle_error<'a>(error: FrameworkError<'a, Data, Error>) { match error { FrameworkError::Command { error, ctx, .. } => { - reply_fail_handler!(ctx.send(CreateReply::default().content(error.to_string()).ephemeral(true))) + reply_fail_handler!(ctx.send( + CreateReply::default() + .content(error.to_string()) + .ephemeral(true) + )) } - FrameworkError::CommandStructureMismatch { description, ctx, .. } => { + FrameworkError::CommandStructureMismatch { + description, ctx, .. + } => { reply_fail_handler!(ctx.send( CreateReply::default() .content(format!( - "# Command arguments did not match. The command probably has been updated recently. Try reloading Discord. \ - Description:\n{}", + "# Command arguments did not match. The command probably has been updated \ + recently. Try reloading Discord. Description:\n{}", description )) .ephemeral(true) diff --git a/src/handlers/bot_interaction.rs b/src/handlers/bot_interaction.rs index cf12e6a..2d09f85 100644 --- a/src/handlers/bot_interaction.rs +++ b/src/handlers/bot_interaction.rs @@ -16,7 +16,11 @@ use serenity::all::{ComponentInteractionDataKind, CreateInteractionResponse}; use crate::error::Error; use crate::Data; -pub(crate) async fn component(ctx: &Context, interaction: &Interaction, data: &Data) -> Result<(), Error> { +pub(crate) async fn component( + ctx: &Context, + interaction: &Interaction, + data: &Data, +) -> Result<(), Error> { let component = interaction.clone().message_component().unwrap(); match component.data.kind { ComponentInteractionDataKind::Button => button(ctx, component, data).await, @@ -24,8 +28,17 @@ pub(crate) async fn component(ctx: &Context, interaction: &Interaction, data: &D } } -async fn button(ctx: &Context, mut interaction: ComponentInteraction, data: &Data) -> Result<(), Error> { - let u = interaction.message.mentions.first().expect("Message did not mention a user.").id; +async fn button( + ctx: &Context, + mut interaction: ComponentInteraction, + data: &Data, +) -> Result<(), Error> { + let u = interaction + .message + .mentions + .first() + .expect("Message did not mention a user.") + .id; match interaction.data.custom_id.as_str() { "accept_verification" => { let member = interaction @@ -37,7 +50,10 @@ async fn button(ctx: &Context, mut interaction: ComponentInteraction, data: &Dat let (_, _, _dm, _) = futures::try_join!( member.add_role(ctx, RoleId::new(1256218805911425066_u64)), member.remove_role(ctx, RoleId::new(1256253358701023232_u64)), - u.direct_message(ctx, CreateMessage::new().content("Your verified minecraft account was approved.")), + u.direct_message( + ctx, + CreateMessage::new().content("Your verified minecraft account was approved.") + ), interaction.message.edit( ctx, EditMessage::new().components(vec![CreateActionRow::Buttons(vec![ @@ -55,12 +71,17 @@ async fn button(ctx: &Context, mut interaction: ComponentInteraction, data: &Dat ])]), ) )?; - interaction.create_response(ctx, CreateInteractionResponse::Acknowledge).await?; + interaction + .create_response(ctx, CreateInteractionResponse::Acknowledge) + .await?; Ok(()) } "deny_verification" => { let (_dm, _) = futures::try_join!( - u.direct_message(ctx, CreateMessage::new().content("Your verified minecraft account was denied.")), + u.direct_message( + ctx, + CreateMessage::new().content("Your verified minecraft account was denied.") + ), interaction.message.edit( ctx, EditMessage::new().components(vec![CreateActionRow::Buttons(vec![ @@ -78,14 +99,29 @@ async fn button(ctx: &Context, mut interaction: ComponentInteraction, data: &Dat ])]), ) )?; - interaction.create_response(ctx, CreateInteractionResponse::Acknowledge).await?; + interaction + .create_response(ctx, CreateInteractionResponse::Acknowledge) + .await?; Ok(()) } "list_accounts" => { let user = interaction.message.mentions.first().unwrap(); - let s: String = crate::commands::accountv2::list_string(&data.sqlite_pool, user).await?; + let s: String = crate::commands::accountv2::list_string( + &data.sqlite_pool, + user, + &data.caches, + &data.clients.general, + ) + .await?; interaction - .create_response(ctx, Message(CreateInteractionResponseMessage::new().content(s).ephemeral(true))) + .create_response( + ctx, + Message( + CreateInteractionResponseMessage::new() + .content(s) + .ephemeral(true), + ), + ) .await?; Ok(()) } diff --git a/src/handlers/thread.rs b/src/handlers/thread.rs index a53845f..655d10e 100644 --- a/src/handlers/thread.rs +++ b/src/handlers/thread.rs @@ -6,7 +6,10 @@ use crate::error::Error; pub(crate) async fn on_create(ctx: &Context, thread: &GuildChannel) -> Result<(), Error> { match thread.parent_id.map(|parent| parent.get()) { Some(1295108216388325386) => { - thread.id.edit_thread(ctx, EditThread::new().rate_limit_per_user(60_u16)).await?; + thread + .id + .edit_thread(ctx, EditThread::new().rate_limit_per_user(60_u16)) + .await?; Ok(()) } Some(_) => Ok(()), diff --git a/src/main.rs b/src/main.rs index fc845a1..6e168cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,92 @@ #![feature(integer_sign_cast)] +#![feature(duration_constructors)] use std::collections::HashSet; use std::convert::Into; -use std::sync::Arc; use std::time::Duration; +use dashmap::DashMap; use poise::serenity_prelude as serenity; use serenity::all::{ActivityData, InteractionType, RoleId}; use serenity::prelude::GatewayIntents; use serenity::{model::id::UserId, FullEvent}; use sqlx::Sqlite; -use tokio::sync::RwLock; use error::Error; mod commands; +mod data; mod error; mod handlers; -struct Data { - bots: Arc>, - sqlite_pool: sqlx::Pool, +struct Caches { + name: DashMap, + uuid: DashMap, +} + +impl Default for Caches { + fn default() -> Self { + Self { + name: DashMap::new(), + uuid: DashMap::new(), + } + } +} + +struct ApiClients { hypixel_api_client: reqwest::Client, -} // User data, which is stored and accessible in all command invocations + local_api_client: reqwest::Client, + general: reqwest::Client, +} + +impl Default for ApiClients { + fn default() -> Self { + Self { + hypixel_api_client: { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + "API-Key", + reqwest::header::HeaderValue::try_from( + std::env::var("HYPIXEL_API_KEY").unwrap(), + ) + .unwrap(), + ); + reqwest::ClientBuilder::new() + .default_headers(headers) + .build() + .unwrap() + }, + local_api_client: reqwest::ClientBuilder::new() + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(), + general: reqwest::ClientBuilder::default().build().unwrap(), + } + } +} + +struct Data { + sqlite_pool: sqlx::Pool, + clients: ApiClients, + caches: Caches, +} + +impl Default for Data { + fn default() -> Self { + Self { + sqlite_pool: sqlx::sqlite::SqlitePoolOptions::new() + .idle_timeout(Duration::from_secs(10)) + .connect_lazy("sqlite:accounts.db") + .unwrap(), + caches: Caches::default(), + clients: ApiClients::default(), + } + } +} type Context<'a> = poise::Context<'a, Data, Error>; #[tokio::main] async fn main() { - let sqlite_pool = sqlx::sqlite::SqlitePoolOptions::new() - .idle_timeout(Duration::from_secs(10)) - .connect_lazy("sqlite:accounts.db") - .unwrap(); - - let hypixel_api: String = std::env::var("HYPIXEL_API_KEY").unwrap(); - let hypixel_api_client = { - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert("API-Key", reqwest::header::HeaderValue::try_from(hypixel_api).unwrap()); - reqwest::ClientBuilder::new().default_headers(headers).build().unwrap() - }; - let options = poise::FrameworkOptions { commands: vec![ commands::lfg::lfg(), @@ -46,7 +94,6 @@ async fn main() { commands::lfg::other(), commands::xd::xd(), commands::helpstart::helpstart(), - commands::bots::bots(), commands::accountv2::account(), ], manual_cooldowns: true, @@ -59,8 +106,15 @@ async fn main() { error::handle_error(error).await; }) }, - owners: { HashSet::from([UserId::new(449579075531440128_u64), UserId::new(659112817508745216_u64)]) }, - event_handler: |ctx, event, framework, data| Box::pin(event_handler(ctx, event, framework, data)), + owners: { + HashSet::from([ + UserId::new(449579075531440128_u64), + UserId::new(659112817508745216_u64), + ]) + }, + event_handler: |ctx, event, framework, data| { + Box::pin(event_handler(ctx, event, framework, data)) + }, ..Default::default() }; @@ -69,17 +123,15 @@ async fn main() { .setup(move |ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; - Ok(Data { - bots: Arc::new(RwLock::new(0)), - sqlite_pool, - hypixel_api_client, - }) + Ok(Data::default()) }) }) .build(); let token = std::env::var("DISCORD_TOKEN").unwrap(); - let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS; + let intents = GatewayIntents::non_privileged() + | GatewayIntents::MESSAGE_CONTENT + | GatewayIntents::GUILD_MEMBERS; let client = serenity::ClientBuilder::new(token, intents) .framework(framework) .activity(ActivityData::custom("NPC moment...")) @@ -99,11 +151,15 @@ async fn event_handler( } FullEvent::GuildMemberAddition { new_member } => { if new_member.guild_id.get() == 1256217633959841853_u64 { - new_member.add_role(ctx, RoleId::new(1256253358701023232_u64)).await?; + new_member + .add_role(ctx, RoleId::new(1256253358701023232_u64)) + .await?; } } FullEvent::InteractionCreate { interaction } => { - if interaction.application_id().get() == 1165594074473037824 && interaction.kind() == InteractionType::Component { + if interaction.application_id().get() == 1165594074473037824 + && interaction.kind() == InteractionType::Component + { handlers::bot_interaction::component(ctx, interaction, data).await?; } }