From fcf37449b3c10ecc35fa5c7798b4b8e734ca63ab Mon Sep 17 00:00:00 2001 From: Stachelbeere1248 Date: Thu, 11 Jul 2024 00:34:25 +0200 Subject: [PATCH] rewrote account management --- src/commands/accountv2.rs | 310 ++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 4 +- src/main.rs | 3 +- 3 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 src/commands/accountv2.rs diff --git a/src/commands/accountv2.rs b/src/commands/accountv2.rs new file mode 100644 index 0000000..2da2967 --- /dev/null +++ b/src/commands/accountv2.rs @@ -0,0 +1,310 @@ +use poise::CreateReply; +use reqwest::Response; +use serde::Deserialize; +use serenity::{ + all::{ + CreateAllowedMentions, + CreateButton, + CreateActionRow, + User, + ReactionType, + ChannelId, + CreateMessage + }, + json::JsonError +}; +use sqlx::{Pool, query_as, Sqlite}; + +use crate::{Context, Error}; + +#[derive(Deserialize)] +struct Links { + #[serde(rename = "DISCORD")] + pub discord: String, +} +#[derive(Deserialize)] +struct SocialMedia { + pub links: Links, +} +#[derive(Deserialize)] +struct HypixelPlayer { + #[serde(rename = "socialMedia")] + pub social_media: SocialMedia, +} +#[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 +} +impl Uuid { + fn new(uuid: &str) -> Self { + let uuid: String = uuid.to_string(); + Uuid { + uuid + } + } + fn get(&self) -> &str { + self.uuid.as_str() + } + async fn fetch(ign: &str) -> Result { + let url: String = format!("https://api.mojang.com/users/profiles/minecraft/{ign}"); + let response: Response = reqwest::get(url).await?; + match response.error_for_status() { + Ok(res) => { + let response_text = res.text().await.unwrap(); + let uuid = (serde_json::from_str(response_text.as_str()) + as Result) + .map(|mojang_player: MojangPlayer| Uuid {uuid: mojang_player.id})?; + Ok(uuid) + }, + Err(why) => Err(Error::from(format!( + "Mojang returned an error. Please make sure to enter a valid Minecraft username.\n\n\ + Details: {}", why).as_str())), + } + } +} +#[derive(PartialEq)] +struct DiscordId { + id: u64 +} +impl DiscordId { + fn new(id: u64) -> Self { + DiscordId { + id + } + } + fn new_from_unsigned(id: i64) -> Self { + DiscordId { + id: id.cast_unsigned() + } + } + async fn matches_fetch(user: &User, uuid: &str) -> Result { + let url: String = format!("https://api.hypixel.net/v2/player?uuid={}", uuid); + let response: Response = reqwest::get(url).await?; + match response.error_for_status() { + Ok(res) => { + let response_text = res.text().await.unwrap(); + let matches = (serde_json::from_str(response_text.as_str()) + as Result) + .map(|hypixel_player: HypixelPlayer| user.name == hypixel_player.social_media.links.discord)?; + Ok(matches) + }, + Err(why) => { + println!("Hypixel issue: {}", why); + Err(Error::from("Hypixel returned an error.")) + } + } + } +} +impl<'a, R: sqlx::Row> sqlx::FromRow<'a, R> for DiscordId +where &'a ::std::primitive::str: sqlx::ColumnIndex, + i64: ::sqlx::decode::Decode<'a, R::Database>, + i64: ::sqlx::types::Type { + fn from_row(row: &'a R) -> sqlx::Result { + let discord_id: i64 = row.try_get("discord_id")?; + Ok(DiscordId { + id: discord_id.cast_unsigned() + }) + } +} +struct Link { + link_id: u16, + discord_ids: Vec, + minecraft_accounts: Vec, +} +impl Link { + fn new(link_id: u16) -> Self { + Link { + link_id, + discord_ids: vec![], + minecraft_accounts: vec![], + } + } + async fn minecraft(mut self, pool: &Pool) -> Self { + 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.expect("Error getting Minecraft UUIDs."); + self + } + async fn discord(mut self, pool: &Pool) -> Self { + 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.expect("Error getting Discord IDs."); + self + } +} +#[poise::command(slash_command, subcommands("add", "list"))] +pub(crate) async fn account(_ctx: Context<'_>) -> Result<(), Error> { + // root of slash-commands is not invokable. + unreachable!() +} + + +#[poise::command(slash_command)] +pub(crate) async fn add<'a>( + ctx: Context<'_>, + + #[description = "Minecraft username"] + #[min_length = 2] + #[max_length = 16] + ign: String, + + user: Option, +) -> Result<(), Error> { + ctx.defer_ephemeral().await?; + let user = user.unwrap_or(ctx.author().clone()); + let uuid = Uuid::fetch(ign.as_str()).await?; + let valid = DiscordId::matches_fetch(&user, uuid.get()).await + .expect("This Minecraft account does not have a Discord account linked."); + match valid { + true => { + let r = CreateReply::default().ephemeral(false); + let pool: Pool = ctx.data().sqlite_pool.clone(); + let (status, link_id) = 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.cast_signed(), user.id.get()).as_str()) + .execute(&pool).await.expect("Database Error: inserting new minecraft value"); + sqlx::query(format!("INSERT INTO minecraft_links VALUES ({}, \"{}\");", id.cast_signed(), uuid.get()).as_str()) + .execute(&pool).await.expect("Database Error: inserting new minecraft value"); + ("Linked your Discord and Minecraft account.", id) + } + Some(dc_id) => { + sqlx::query(format!("INSERT INTO minecraft_links VALUES ({}, \"{}\");", dc_id.cast_signed(), uuid.get()).as_str()) + .execute(&pool).await.expect("Database Error: inserting new minecraft value"); + ("Your Discord account has previously had an account linked. Added the new link.", dc_id) + } + } + }, Some(mc_id) => { + match link_id_from_discord(&pool, user.id.get()).await { + None => { + sqlx::query(format!("INSERT INTO discord_links VALUES ({}, {});", mc_id.cast_signed(), user.id.get()).as_str()) + .execute(&pool).await.expect("Database Error: inserting new minecraft value"); + ("Your Minecraft account has previously had an account linked. Added the new link.", mc_id) + }, + Some(dc_id) => { + sqlx::query(format!("UPDATE minecraft_links SET link_id = {} WHERE link_id = {};", mc_id.cast_signed(), dc_id.cast_signed()).as_str()) + .execute(&pool).await.expect("Database Error: Merging Minecraft Accounts."); + sqlx::query(format!("UPDATE discord_links SET link_id = {} WHERE link_id = {};", mc_id.cast_signed(), dc_id.cast_signed()).as_str()) + .execute(&pool).await.expect("Database Error: Merging Discord Accounts."); + ("Both your Discord and Minecraft account had linked accounts. Merged all account links.", mc_id) + } + } + } + }; + ctx.send(r.content(status)).await?; + let link = Link::new(link_id).minecraft(&pool).await.discord(&pool).await; + let s = list_string(link, user.id.get()).await; + ChannelId::new(1257776992497959075).send_message( + ctx, + CreateMessage::new() + .content(s) + .allowed_mentions(CreateAllowedMentions::new().empty_roles().all_users(true)) + .components(vec![CreateActionRow::Buttons(vec![ + CreateButton::new("accept_verification").emoji(ReactionType::from('✅')), + CreateButton::new("deny_verification").emoji(ReactionType::from('❌')), + ])]) + ).await?; + } + false => { + let r = CreateReply::default().ephemeral(true) + .content(format!("The Discord account linked on Hypixel does not seem to match the specified account.\n\ + Expected account link: `{}`", user.name)); + ctx.send(r).await?; + } + } + Ok(()) +} + +#[poise::command(slash_command)] +pub(crate) async fn list( + ctx: Context<'_>, + user: Option +) -> Result<(), Error> { + ctx.defer().await?; + let user = user.unwrap_or(ctx.author().clone()); + let r = CreateReply::default().ephemeral(false); + let pool: Pool = ctx.data().sqlite_pool.clone(); + let link_id = link_id_from_discord(&pool, user.id.get()).await.expect("This user has no linked accounts"); + let link = Link::new(link_id).minecraft(&pool).await.discord(&pool).await; + let s = list_string(link, user.id.get()).await; + ctx.send(r.content(s).allowed_mentions(CreateAllowedMentions::new().empty_roles().empty_users())).await?; + Ok(()) +} + +#[poise::command(slash_command)] +pub(crate) async fn remove(_ctx: Context<'_>) -> Result<(), Error> { + unreachable!() +} + +async fn list_string(link: Link, user_id: u64) -> String { + 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.get()).as_str()); + } + format!("## Account list for <@{user_id}>:\n{}\n{}", discord_list.as_str(), minecraft_list.as_str()) +} + +#[derive(sqlx::FromRow)] +struct MinecraftLink { + link_id: i16, + minecraft_uuid: String, +} + +async fn link_id_from_minecraft(pool: &Pool, minecraft_uuid: &str) -> Option { + return query_as( + format!( + "SELECT * FROM minecraft_links WHERE minecraft_uuid = \"{minecraft_uuid}\" LIMIT 1;" + ) + .as_str(), + ) + .fetch_optional(pool) + .await + .expect("Database error: fetching link id by uuid") + .map(|minecraft_link: MinecraftLink| minecraft_link.link_id.cast_unsigned()); +} + +#[derive(sqlx::FromRow)] +struct DiscordLink { + link_id: i16, + discord_id: i64, +} +async fn link_id_from_discord(pool: &Pool, snowflake: u64) -> Option { + let discord_id: i64 = snowflake.cast_signed(); + return query_as( + format!("SELECT * FROM discord_links WHERE discord_id = {discord_id} LIMIT 1;").as_str(), + ) + .fetch_optional(pool) + .await + .expect("Database error: fetching link id by discord") + .map(|discord_link: DiscordLink| discord_link.link_id.cast_unsigned()); +} + +#[derive(sqlx::FromRow)] +struct LinkId { + link_id: i16, +} + +async fn new_link_id(pool: &Pool) -> u16 { + let result: Result = query_as("SELECT MAX(link_id) AS link_id FROM minecraft_links;") + .fetch_one(pool) + .await; + result + .expect("Database error: fetching new id") + .link_id.cast_unsigned() + 1 +} \ No newline at end of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b32caa5..59f4ada 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,6 +1,6 @@ -pub(crate) mod account; pub(crate) mod bots; pub(crate) mod command_helper; pub(crate) mod helpstart; pub(crate) mod lfg; -pub(crate) mod xd; \ No newline at end of file +pub(crate) mod xd; +pub(crate) mod accountv2; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e927346..ad683a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,7 @@ async fn main() { commands::xd::xd(), commands::helpstart::helpstart(), commands::bots::bots(), - commands::account::account(), + commands::accountv2::account(), ], manual_cooldowns: true, prefix_options: poise::PrefixFrameworkOptions { @@ -59,6 +59,7 @@ async fn main() { on_error: |error| { Box::pin(async move { match error { + other => poise::builtins::on_error(other).await.unwrap(), } })