diff --git a/src/commands/accountv2.rs b/src/commands/accountv2.rs index 88a43e2..18349d8 100644 --- a/src/commands/accountv2.rs +++ b/src/commands/accountv2.rs @@ -1,142 +1,19 @@ use poise::CreateReply; use reqwest::Client; -use serde::Deserialize; use serenity::all::ButtonStyle; use serenity::all::{ ChannelId, CreateActionRow, CreateAllowedMentions, CreateButton, CreateMessage, ReactionType, User, }; -use sqlx::{query_as, Pool, Sqlite}; -use std::ops::Add; +use sqlx::{Pool, Sqlite}; use crate::commands::command_helper::cooldown; +use crate::data::account_links::{Link, LinkId, Uuid}; use crate::error::Error; use crate::error::Error::Other; use crate::Caches; use crate::Context; -#[derive(Deserialize)] -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(PartialEq, sqlx::FromRow)] -pub(crate) struct Uuid { - pub(crate) uuid: String, -} - -impl Uuid { - fn get(&self) -> &str { - self.uuid.as_str() - } - 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, 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 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(), - ))? - == user.name; - Ok(matches) - } -} -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(), - }) - } -} - -pub(crate) struct Link { - link_id: u16, - discord_ids: Vec, - pub(crate) 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) -> 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?; - 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?; - Ok(self) - } -} #[poise::command( slash_command, subcommands("add", "list"), @@ -170,7 +47,12 @@ pub(crate) async fn add( true }; let user: User = user.unwrap_or(ctx.author().clone()); - let uuid: Uuid = Uuid::for_ign(&ign, &ctx.data().clients.general, &ctx.data().caches).await?; + let uuid: Uuid = Uuid::for_ign( + ign.as_str(), + &ctx.data().clients.general, + &ctx.data().caches, + ) + .await?; match force || uuid .has_discord_user(&user, &ctx.data().clients.hypixel_api_client) @@ -178,14 +60,13 @@ pub(crate) async fn add( { 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?; + let status: &str = match LinkId::try_from_minecraft(pool, uuid.as_str()).await { + Err(_) => match LinkId::try_from_discord(pool, user.id.get()).await { + Err(_) => { + let id = LinkId::new(pool).await?; sqlx::query( format!( - "INSERT INTO discord_links VALUES ({}, {});", - id.inner, + "INSERT INTO discord_links VALUES ({id}, {});", user.id.get() ) .as_str(), @@ -194,8 +75,7 @@ pub(crate) async fn add( .await?; sqlx::query( format!( - "INSERT INTO minecraft_links VALUES ({}, \"{}\");", - id.inner, + "INSERT INTO minecraft_links VALUES ({id}, \"{}\");", uuid.get() ) .as_str(), @@ -204,11 +84,10 @@ pub(crate) async fn add( .await?; "Linked your Discord and Minecraft account." } - Some(dc_id) => { + Ok(dc_id) => { sqlx::query( format!( - "INSERT INTO minecraft_links VALUES ({}, \"{}\");", - dc_id.inner, + "INSERT INTO minecraft_links VALUES ({dc_id}, \"{}\");", uuid.get() ) .as_str(), @@ -219,12 +98,11 @@ pub(crate) async fn add( link." } }, - Some(mc_id) => match link_id_from_discord(pool, user.id.get()).await { - None => { + Ok(mc_id) => match LinkId::try_from_discord(pool, user.id.get()).await { + Err(_) => { sqlx::query( format!( - "INSERT INTO discord_links VALUES ({}, {});", - mc_id.inner, + "INSERT INTO discord_links VALUES ({mc_id}, {});", user.id.get() ) .as_str(), @@ -234,11 +112,10 @@ pub(crate) async fn add( "Your Minecraft account has previously had an account linked. Added the \ new link." } - Some(dc_id) => { + Ok(dc_id) => { sqlx::query( format!( - "UPDATE minecraft_links SET link_id = {} WHERE link_id = {};", - mc_id.inner, dc_id.inner + "UPDATE minecraft_links SET link_id = {mc_id} WHERE link_id = {dc_id};", ) .as_str(), ) @@ -246,8 +123,7 @@ pub(crate) async fn add( .await?; sqlx::query( format!( - "UPDATE discord_links SET link_id = {} WHERE link_id = {};", - mc_id.inner, dc_id.inner + "UPDATE discord_links SET link_id = {mc_id} WHERE link_id = {dc_id};", ) .as_str(), ) @@ -316,37 +192,24 @@ pub(crate) async fn list(ctx: Context<'_>, user: User) -> Result<(), Error> { Ok(()) } -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::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 link = Link::try_from_discord(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()); + for dc in link.discord_ids() { + discord_list.push_str(format!("\n- <@{dc}>").as_str()); } let mut minecraft_list = String::from("### Minecraft:"); - for mc in link.minecraft_accounts { + for mc in link.minecraft_accounts() { minecraft_list.push_str(format!("\n- `{}`", mc.ign(c, cli).await?).as_str()); } Ok(format!( "## Account list for member #{}:\n{}\n{}", - link.link_id, + link.link_id(), discord_list.as_str(), minecraft_list.as_str() )) @@ -356,64 +219,3 @@ pub(crate) async fn list_string( pub(crate) async fn remove(_ctx: Context<'_>) -> Result<(), Error> { unimplemented!(); } - -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") -} -async fn link_id_from_discord(pool: &Pool, snowflake: u64) -> Option { - query_as( - format!( - "SELECT link_id FROM discord_links WHERE discord_id = {} LIMIT 1;", - snowflake.cast_signed() - ) - .as_str(), - ) - .fetch_optional(pool) - .await - .expect("Database error: fetching link_id for discord_id") -} - -#[derive(sqlx::FromRow)] -struct LinkId { - #[sqlx(rename = "link_id")] - inner: i16, -} - -impl From for LinkId { - fn from(unsigned: u16) -> Self { - Self { - inner: unsigned.cast_signed(), - } - } -} - -impl Into for LinkId { - fn into(self) -> u16 { - self.inner.cast_unsigned() - } -} - -impl Add for LinkId { - type Output = LinkId; - - fn add(mut self, rhs: i16) -> Self::Output { - self.inner += rhs; - self - } -} - -async fn new_link_id(pool: &Pool) -> Result { - let result: LinkId = query_as("SELECT MAX(link_id) AS link_id FROM minecraft_links;") - .fetch_one(pool) - .await?; - Ok(result + 1) -} diff --git a/src/commands/helpstart.rs b/src/commands/helpstart.rs index d3ae7b0..f35b7bf 100644 --- a/src/commands/helpstart.rs +++ b/src/commands/helpstart.rs @@ -12,6 +12,7 @@ use poise::serenity_prelude::CreateInteractionResponse; use poise::serenity_prelude::CreateInteractionResponseMessage; use poise::CreateReply; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use crate::data::account_links::{Link, Uuid}; #[poise::command( slash_command, @@ -22,14 +23,12 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; // Check for bots available to you. pub(crate) async fn helpstart(ctx: Context<'_>, user: Option) -> Result<(), Error> { ctx.defer_ephemeral().await?; - let links = super::accountv2::get_link(ctx.author(), &ctx.data().sqlite_pool).await?; let mc_accounts = match user { None => { futures::future::try_join_all( - links - .minecraft_accounts + Vec::::from(Link::try_from_discord(ctx.author(), &ctx.data().sqlite_pool).await?) .into_iter() - .map(|a| name(&ctx.data().caches, &ctx.data().clients.general, a.uuid)) + .map(|a| name(&ctx.data().caches, &ctx.data().clients.general, a.get())) .collect::>(), ) .await? diff --git a/src/data/account_links.rs b/src/data/account_links.rs index 8b13789..9092429 100644 --- a/src/data/account_links.rs +++ b/src/data/account_links.rs @@ -1 +1,206 @@ +use super::hypixel::HypixelResponse; +use crate::Caches; +use crate::Error::{self, *}; +use poise::serenity_prelude::User; +use reqwest::Client; +use sqlx::query_as; +use sqlx::Pool; +use sqlx::Sqlite; +#[derive(PartialEq, sqlx::FromRow)] +pub(crate) struct Uuid { + uuid: String, +} + +impl Uuid { + pub(crate) fn get(self) -> String { + self.uuid + } + + pub(crate) fn as_str(&self) -> &str { + self.uuid.as_str() + } + + pub(crate) async fn for_ign(ign: &str, cli: &Client, c: &Caches) -> Result { + let uuid = crate::data::mojang::uuid(c, cli, ign.to_owned()).await?; + Ok(Self { uuid }) + } + + pub(crate) async fn ign(&self, c: &Caches, cli: &Client) -> Result { + let ign = crate::data::mojang::name(c, cli, self.uuid.clone()).await?; + Ok(ign) + } +} + +impl Uuid { + pub(crate) async fn has_discord_user( + &self, + user: &User, + client: &Client, + ) -> Result { + let res = HypixelResponse::get(self.uuid.as_str(), client).await?; + let matches = res.discord().ok_or(Other( + "The Hypixel profile has no Discord account linked. Please follow the steps in \ + <#1256219552568840263>" + .to_string(), + ))? == user.name; + Ok(matches) + } +} + +#[derive(PartialEq)] +pub(crate) struct DiscordId { + inner: u64, +} + +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 { + inner: discord_id.cast_unsigned(), + }) + } +} + +impl std::fmt::Display for DiscordId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +pub(crate) struct Link { + link_id: LinkId, + discord_ids: Vec, + minecraft_accounts: Vec, +} + +impl From for Vec { + fn from(value: Link) -> Self { + value.minecraft_accounts + } +} + +impl Link { + pub(crate) fn minecraft_accounts(&self) -> &Vec { + &self.minecraft_accounts + } + + pub(crate) fn discord_ids(&self) -> &Vec { + &self.discord_ids + } + + pub(crate) fn link_id(&self) -> LinkId { + self.link_id + } + + async fn lookup_by_id(link_id: LinkId, pool: &Pool) -> Result { + let link = Link { + link_id, + discord_ids: query_as( + format!("SELECT discord_id FROM discord_links WHERE link_id = {link_id};").as_str(), + ) + .fetch_all(pool) + .await?, + 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(link) + } + + pub(crate) async fn try_from_discord(user: &User, pool: &Pool) -> Result { + let link_id = LinkId::try_from_discord(pool, user.id.get()).await?; + Link::lookup_by_id(link_id, pool).await + } + + /* #[allow(dead_code)] + * pub(crate) async fn try_from_minecraft( + * player: Uuid, + * pool: &Pool, + * ) -> Result { + * let link_id = LinkId::try_from_minecraft(pool, player.uuid.as_str()).await?; + * Link::lookup_by_id(link_id, pool).await + * } + */ +} + +#[derive(Clone, Copy)] +pub(crate) struct LinkId { + inner: u16, +} + +impl<'a, R: sqlx::Row> sqlx::FromRow<'a, R> for LinkId +where + &'a ::std::primitive::str: sqlx::ColumnIndex, + i16: ::sqlx::decode::Decode<'a, R::Database>, + i16: ::sqlx::types::Type, +{ + fn from_row(row: &'a R) -> sqlx::Result { + let link_id: i16 = row.try_get("link_id")?; + Ok(Self { + inner: link_id.cast_unsigned(), + }) + } +} + +impl std::fmt::Display for LinkId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +impl LinkId { + fn incremented(self) -> Self { + Self { + inner: self.inner + 1, + } + } + + pub(crate) async fn new(pool: &Pool) -> Result { + let highest: LinkId = query_as("SELECT MAX(link_id) AS link_id FROM minecraft_links;") + .fetch_one(pool) + .await?; + Ok(highest.incremented()) + } + + pub(crate) async fn try_from_discord( + pool: &Pool, + snowflake: u64, + ) -> Result { + query_as( + format!( + "SELECT link_id FROM discord_links WHERE discord_id = {} LIMIT 1;", + snowflake.cast_signed() + ) + .as_str(), + ) + .fetch_optional(pool) + .await? + .ok_or(Other("This user has no accounts linked.".to_string())) + } + + pub(crate) async fn try_from_minecraft( + pool: &Pool, + minecraft_uuid: &str, + ) -> Result { + query_as( + format!( + r#"SELECT link_id FROM minecraft_links WHERE minecraft_uuid = "{minecraft_uuid}" LIMIT 1;"# + ) + .as_str(), + ) + .fetch_optional(pool) + .await? + .ok_or(Other("This player has no accounts linked.".to_string())) + } +} diff --git a/src/data/hypixel.rs b/src/data/hypixel.rs new file mode 100644 index 0000000..7834cff --- /dev/null +++ b/src/data/hypixel.rs @@ -0,0 +1,46 @@ +use crate::Error; +use reqwest::Client; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Links { + #[serde(rename = "DISCORD")] + discord: Option, +} + +#[derive(Deserialize)] +struct SocialMedia { + links: Option, +} + +#[derive(Deserialize)] +struct HypixelPlayer { + #[serde(rename = "socialMedia")] + social_media: Option, +} + +#[derive(Deserialize)] +pub struct HypixelResponse { + #[serde(rename = "player")] + player: HypixelPlayer, +} + +impl HypixelResponse { + pub async fn get(uuid: &str, client: &Client) -> Result { + let player = client + .get(format!("https://api.hypixel.net/v2/player?uuid={uuid}")) + .send() + .await? + .error_for_status()? + .json::() + .await?; + Ok(player) + } + + pub fn discord(self) -> Option { + self.player + .social_media + .and_then(|p| p.links) + .and_then(|l| l.discord) + } +} diff --git a/src/data/mod.rs b/src/data/mod.rs index 17f34fa..4c10ccb 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod account_links; pub(crate) mod helpstart_api; +pub(crate) mod hypixel; pub(crate) mod mojang;