verification stat checker

This commit is contained in:
Stachelbeere1248 2025-07-02 18:57:11 +02:00
parent 7886c6b46d
commit ce999bd271
Signed by: Stachelbeere1248
SSH key fingerprint: SHA256:IozEKdw2dB8TZxkpPdMxcWSoWTIMwoLaCcZJ1AJnY2o
8 changed files with 19899 additions and 185 deletions

19524
api_examples/player.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,15 @@
use poise::CreateReply; use poise::{ChoiceParameter, CreateReply};
use reqwest::Client; use reqwest::Client;
use serenity::all::ButtonStyle;
use serenity::all::{ use serenity::all::{
ChannelId, CreateActionRow, CreateAllowedMentions, CreateButton, CreateMessage, ReactionType, ButtonStyle, ChannelId, CreateActionRow, CreateAllowedMentions, CreateButton, CreateEmbed,
User, CreateMessage, ReactionType, User,
}; };
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use crate::commands::command_helper::cooldown; use crate::commands::command_helper::cooldown;
use crate::data::account_links::{Link, LinkId, Uuid}; use crate::data::account_links::{link, Link, Uuid};
use crate::data::hypixel::Arcade;
use crate::error::Error; use crate::error::Error;
use crate::error::Error::Other;
use crate::Caches; use crate::Caches;
use crate::Context; use crate::Context;
@ -25,6 +24,16 @@ pub(crate) async fn account(_ctx: Context<'_>) -> Result<(), Error> {
unreachable!() unreachable!()
} }
#[derive(ChoiceParameter, PartialEq, Eq)]
enum Mode {
#[name = "normal"]
Normal,
#[name = "forced"]
Forced,
#[name = "forced + no hypixel api (instant accept)"]
NoApi,
}
#[poise::command(slash_command, ephemeral = "false")] #[poise::command(slash_command, ephemeral = "false")]
/// Verify a Minecraft account on the Zombies MultiPlayer Discord. /// Verify a Minecraft account on the Zombies MultiPlayer Discord.
pub(crate) async fn add( pub(crate) async fn add(
@ -33,120 +42,74 @@ pub(crate) async fn add(
#[min_length = 2] #[min_length = 2]
#[max_length = 16] #[max_length = 16]
ign: String, ign: String,
#[description = "Discord User"] user: Option<User>, #[description = "Discord User"] target: Option<User>,
#[description = "admin-only"] force: Option<bool>, #[description = "ZMP-admin only"] mode: Option<Mode>,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.defer().await?; ctx.defer().await?;
let force: bool = let mode = if ctx.framework().options().owners.contains(&ctx.author().id) {
force.unwrap_or(false) && ctx.framework().options.owners.contains(&ctx.author().id) && { mode.unwrap_or(Mode::Normal)
let _ = user.as_ref().ok_or(Other( } else {
"Warning: attempted to run forced account add without specifying a target Discord \ Mode::Normal
account."
.to_string(),
))?;
true
}; };
let user: User = user.unwrap_or(ctx.author().clone()); let user: User = target.unwrap_or(if mode == Mode::Normal {
ctx.author().clone()
} else {
return Err(Error::Other(
"Force mode cannot be ran without specifying a different Discord account other than \
your own.".to_string(),
));
});
let uuid: Uuid = Uuid::for_ign( let uuid: Uuid = Uuid::for_ign(
ign.as_str(), ign.as_str(),
&ctx.data().clients.general, &ctx.data().clients.general,
&ctx.data().caches, &ctx.data().caches,
) )
.await?; .await?;
match force let link_status = match mode {
|| uuid Mode::Normal => {
.has_discord_user(&user, &ctx.data().clients.hypixel_api_client) let profile = uuid
.hypixel_player_data(&ctx.data().clients.hypixel_api_client)
.await? .await?
{ .map_if_discord(user.name.as_str())?;
true => { let done = link(&ctx.data().sqlite_pool, uuid, &user).await?;
let pool = &ctx.data().sqlite_pool; ChannelId::new(1257776992497959075_u64)
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}, {});",
user.id.get()
)
.as_str(),
)
.execute(pool)
.await?;
sqlx::query(
format!(
"INSERT INTO minecraft_links VALUES ({id}, \"{}\");",
uuid.get()
)
.as_str(),
)
.execute(pool)
.await?;
"Linked your Discord and Minecraft account."
}
Ok(dc_id) => {
sqlx::query(
format!(
"INSERT INTO minecraft_links VALUES ({dc_id}, \"{}\");",
uuid.get()
)
.as_str(),
)
.execute(pool)
.await?;
"Your Discord account has previously had an account linked. Added the new \
link."
}
},
Ok(mc_id) => match LinkId::try_from_discord(pool, user.id.get()).await {
Err(_) => {
sqlx::query(
format!(
"INSERT INTO discord_links VALUES ({mc_id}, {});",
user.id.get()
)
.as_str(),
)
.execute(pool)
.await?;
"Your Minecraft account has previously had an account linked. Added the \
new link."
}
Ok(dc_id) => {
sqlx::query(
format!(
"UPDATE minecraft_links SET link_id = {mc_id} WHERE link_id = {dc_id};",
)
.as_str(),
)
.execute(pool)
.await?;
sqlx::query(
format!(
"UPDATE discord_links SET link_id = {mc_id} WHERE link_id = {dc_id};",
)
.as_str(),
)
.execute(pool)
.await?;
"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
);
ChannelId::new(1257776992497959075)
.send_message( .send_message(
ctx, ctx,
CreateMessage::new() create_verification_message(profile.arcade_stats().copied().unwrap_or_default(), &user, ign)
.content(s)
.allowed_mentions(
CreateAllowedMentions::new().empty_roles().all_users(true),
) )
.await?;
done
}
Mode::Forced => {
let profile = uuid
.hypixel_player_data(&ctx.data().clients.hypixel_api_client)
.await?;
let done = link(&ctx.data().sqlite_pool, uuid, &user).await?;
ChannelId::new(1257776992497959075_u64)
.send_message(
ctx,
create_verification_message(profile.arcade_stats().copied().unwrap_or_default(), &user, ign)
)
.await?;
done
}
Mode::NoApi => {
let done = link(&ctx.data().sqlite_pool, uuid, &user).await?;
done
}
};
ctx.send(CreateReply::default().content(link_status))
.await?;
Ok(())
}
fn create_verification_message(stats: Arcade, user: &User, ign: String) -> CreateMessage {
let embed = CreateEmbed::new()
.fields(Vec::<(&'static str, String, bool)>::from(stats))
.title(format!("Verification request for {user} `{ign}`"));
CreateMessage::new()
.embed(embed)
.allowed_mentions(CreateAllowedMentions::new().empty_roles().all_users(true))
.components(vec![CreateActionRow::Buttons(vec![ .components(vec![CreateActionRow::Buttons(vec![
CreateButton::new("accept_verification") CreateButton::new("accept_verification")
.emoji(ReactionType::from('✅')) .emoji(ReactionType::from('✅'))
@ -157,18 +120,7 @@ pub(crate) async fn add(
CreateButton::new("list_accounts") CreateButton::new("list_accounts")
.emoji(ReactionType::from('📜')) .emoji(ReactionType::from('📜'))
.style(ButtonStyle::Primary), .style(ButtonStyle::Primary),
])]), ])])
)
.await?;
ctx.send(CreateReply::default().content(status)).await?;
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 `{}`.",
user.name
))),
}
} }
#[poise::command( #[poise::command(

View file

@ -1,3 +1,4 @@
use crate::data::account_links::{Link, Uuid};
use crate::data::helpstart_api::fetch_all; use crate::data::helpstart_api::fetch_all;
use crate::data::helpstart_api::ListType::*; use crate::data::helpstart_api::ListType::*;
use crate::data::mojang::name; use crate::data::mojang::name;
@ -12,7 +13,6 @@ use poise::serenity_prelude::CreateInteractionResponse;
use poise::serenity_prelude::CreateInteractionResponseMessage; use poise::serenity_prelude::CreateInteractionResponseMessage;
use poise::CreateReply; use poise::CreateReply;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::data::account_links::{Link, Uuid};
#[poise::command( #[poise::command(
slash_command, slash_command,
@ -26,9 +26,11 @@ pub(crate) async fn helpstart(ctx: Context<'_>, user: Option<String>) -> Result<
let mc_accounts = match user { let mc_accounts = match user {
None => { None => {
futures::future::try_join_all( futures::future::try_join_all(
Vec::<Uuid>::from(Link::try_from_discord(ctx.author(), &ctx.data().sqlite_pool).await?) Vec::<Uuid>::from(
Link::try_from_discord(ctx.author(), &ctx.data().sqlite_pool).await?,
)
.into_iter() .into_iter()
.map(|a| name(&ctx.data().caches, &ctx.data().clients.general, a.get())) .map(|a| name(&ctx.data().caches, &ctx.data().clients.general, a.uuid))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
) )
.await? .await?
@ -39,9 +41,19 @@ pub(crate) async fn helpstart(ctx: Context<'_>, user: Option<String>) -> Result<
let bots = fetch_all(&ctx.data().clients.local_api_client).await?; let bots = fetch_all(&ctx.data().clients.local_api_client).await?;
let usable = bots let usable = bots
.iter() .iter()
.filter(|b| match b.list_type() { .filter(|bot| match bot.list_type() {
Whitelist => b.list().iter().any(|w| mc_accounts.contains(w)), Whitelist => bot.list().iter().map(|wl| wl.to_lowercase()).any(|wl| {
Blacklist => mc_accounts.iter().any(|m| !b.list().contains(m)), mc_accounts
.iter()
.map(|acc| acc.to_lowercase())
.any(|acc| acc == wl)
}),
Blacklist => mc_accounts.iter().map(|acc| acc.to_lowercase()).any(|acc| {
bot.list()
.iter()
.map(|bl| bl.to_lowercase())
.all(|b| b != acc)
}),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -50,7 +50,7 @@ pub enum Difficulty {
#[poise::command( #[poise::command(
slash_command, slash_command,
install_context = "Guild", install_context = "Guild",
interaction_context = "Guild", interaction_context = "Guild"
)] )]
/// Find a team for Hypixel Zombies. /// Find a team for Hypixel Zombies.
pub(crate) async fn lfg( pub(crate) async fn lfg(
@ -103,7 +103,10 @@ pub(crate) async fn lfg(
AlienArcadium => Normal, AlienArcadium => Normal,
}; };
let mut reply_content: String = format!("-# LFG by {}\n## <@&{ping}> {current}/{desired} {map_name}", ctx.author()); let mut reply_content: String = format!(
"-# LFG by {}\n## <@&{ping}> {current}/{desired} {map_name}",
ctx.author()
);
match difficulty { match difficulty {
Normal => {} Normal => {}
Difficulty::Hard | Difficulty::Rip => { Difficulty::Hard | Difficulty::Rip => {

View file

@ -1,6 +1,8 @@
use super::hypixel::HypixelResponse; use std::fmt::Display;
use crate::data::hypixel::HypixelPlayer;
use crate::Caches; use crate::Caches;
use crate::Error::{self, *}; use crate::Error;
use poise::serenity_prelude::User; use poise::serenity_prelude::User;
use reqwest::Client; use reqwest::Client;
use sqlx::query_as; use sqlx::query_as;
@ -9,14 +11,16 @@ use sqlx::Sqlite;
#[derive(PartialEq, sqlx::FromRow)] #[derive(PartialEq, sqlx::FromRow)]
pub(crate) struct Uuid { pub(crate) struct Uuid {
uuid: String, pub uuid: String,
}
impl Display for Uuid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.uuid)
}
} }
impl Uuid { impl Uuid {
pub(crate) fn get(self) -> String {
self.uuid
}
pub(crate) fn as_str(&self) -> &str { pub(crate) fn as_str(&self) -> &str {
self.uuid.as_str() self.uuid.as_str()
} }
@ -30,21 +34,13 @@ impl Uuid {
let ign = crate::data::mojang::name(c, cli, self.uuid.clone()).await?; let ign = crate::data::mojang::name(c, cli, self.uuid.clone()).await?;
Ok(ign) Ok(ign)
} }
}
impl Uuid { pub(crate) async fn hypixel_player_data(
pub(crate) async fn has_discord_user(
&self, &self,
user: &User,
client: &Client, client: &Client,
) -> Result<bool, Error> { ) -> Result<HypixelPlayer, Error> {
let res = HypixelResponse::get(self.uuid.as_str(), client).await?; let p = HypixelPlayer::get(self.uuid.as_str(), client).await?;
let matches = res.discord().ok_or(Other( Ok(p)
"The Hypixel profile has no Discord account linked. Please follow the steps in \
<#1256219552568840263>"
.to_string(),
))? == user.name;
Ok(matches)
} }
} }
@ -186,7 +182,9 @@ impl LinkId {
) )
.fetch_optional(pool) .fetch_optional(pool)
.await? .await?
.ok_or(Other("This user has no accounts linked.".to_string())) .ok_or(Error::Other(
"This user has no accounts linked.".to_string(),
))
} }
pub(crate) async fn try_from_minecraft( pub(crate) async fn try_from_minecraft(
@ -201,6 +199,79 @@ impl LinkId {
) )
.fetch_optional(pool) .fetch_optional(pool)
.await? .await?
.ok_or(Other("This player has no accounts linked.".to_string())) .ok_or(Error::Other("This player has no accounts linked.".to_string()))
}
}
pub async fn link(pool: &Pool<Sqlite>, uuid: Uuid, user: &User) -> Result<&'static str, Error> {
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}, {});",
user.id.get()
)
.as_str(),
)
.execute(pool)
.await?;
sqlx::query(
format!("INSERT INTO minecraft_links VALUES ({id}, \"{uuid}\");",).as_str(),
)
.execute(pool)
.await?;
Ok("Linked your Discord and Minecraft account.")
}
Ok(dc_id) => {
sqlx::query(
format!("INSERT INTO minecraft_links VALUES ({dc_id}, \"{uuid}\");",).as_str(),
)
.execute(pool)
.await?;
Ok(
"Your Discord account has previously had an account linked. Added the new \
link.",
)
}
},
Ok(mc_id) => match LinkId::try_from_discord(pool, user.id.get()).await {
Err(_) => {
sqlx::query(
format!(
"INSERT INTO discord_links VALUES ({mc_id}, {});",
user.id.get()
)
.as_str(),
)
.execute(pool)
.await?;
Ok(
"Your Minecraft account has previously had an account linked. Added the new \
link.",
)
}
Ok(dc_id) => {
sqlx::query(
format!(
"UPDATE minecraft_links SET link_id = {mc_id} WHERE link_id = {dc_id};",
)
.as_str(),
)
.execute(pool)
.await?;
sqlx::query(
format!("UPDATE discord_links SET link_id = {mc_id} WHERE link_id = {dc_id};",)
.as_str(),
)
.execute(pool)
.await?;
Ok(
"Both your Discord and Minecraft account had linked accounts. Merged all \
account links.",
)
}
},
} }
} }

View file

@ -1,46 +1,164 @@
use crate::Error; use crate::{error::LinkingError, Error};
use getset::{CopyGetters, Getters};
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize, Getters)]
#[getset(get)]
struct Links { struct Links {
#[serde(rename = "DISCORD")] #[serde(rename = "DISCORD")]
discord: Option<String>, discord: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize, Getters)]
#[getset(get)]
struct SocialMedia { struct SocialMedia {
links: Option<Links>, links: Option<Links>,
} }
#[derive(Deserialize)] #[derive(Deserialize, CopyGetters, Clone, Copy)]
struct HypixelPlayer { #[getset(get_copy)]
pub struct Arcade {
#[serde(rename = "wins_zombies")]
wins: Option<u16>, //one day slies will overflow this
#[serde(rename = "fastest_time_30_zombies_deadend_normal")]
de_r30: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_deadend_hard")]
deh_r30: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_deadend_rip")]
der_r30: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_badblood_normal")]
bb_r30: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_badblood_hard")]
bbh_r30: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_badblood_rip")]
bbr_r30: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_alienarcadium_normal")]
aa_win: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_prison_normal")]
p_r30: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_prison_hard")]
ph_r30: Option<u16>,
#[serde(rename = "fastest_time_30_zombies_prison_rip")]
pr_r30: Option<u16>,
}
impl From<Arcade> for Vec<(&'static str, String, bool)> {
fn from(arcade: Arcade) -> Vec<(&'static str, String, bool)> {
let mut vec = Vec::<(&'static str, String, bool)>::with_capacity(11);
fn format(sec: u16) -> String {
let h = sec / 3600;
let m = (sec % 3600) / 60;
let s = sec % 60;
if h > 0 {
format!("{}:{}:{}", h, m, s)
} else {
format!("{}:{}", m, s)
}
}
if let Some(wins) = arcade.wins {
vec.push(("Wins", wins.to_string(), true));
}
macro_rules! push_time_if_some {
($field:ident, $name:expr) => {
if let Some(val) = arcade.$field() {
let formatted = format(val);
vec.push(($name, formatted, true));
}
};
}
push_time_if_some!(de_r30, "DE");
push_time_if_some!(deh_r30, "DE Hard");
push_time_if_some!(der_r30, "DE RIP");
push_time_if_some!(bb_r30, "BB");
push_time_if_some!(bbh_r30, "BB Hard");
push_time_if_some!(bbr_r30, "BB RIP");
push_time_if_some!(aa_win, "AA");
push_time_if_some!(p_r30, "Prison");
push_time_if_some!(ph_r30, "Prison Hard");
push_time_if_some!(pr_r30, "Prison RIP");
vec
}
}
#[derive(Deserialize, Getters)]
struct Stats {
#[getset(get)]
#[serde(rename = "Arcade")]
arcade: Option<Arcade>,
}
#[derive(Deserialize, Getters)]
#[getset(get)]
pub struct HypixelPlayer {
#[serde(rename = "socialMedia")] #[serde(rename = "socialMedia")]
social_media: Option<SocialMedia>, social_media: Option<SocialMedia>,
stats: Option<Stats>,
}
impl Default for Arcade {
fn default() -> Self {
Self {
wins: None,
de_r30: None,
deh_r30: None,
der_r30: None,
bb_r30: None,
bbh_r30: None,
bbr_r30: None,
aa_win: None,
p_r30: None,
ph_r30: None,
pr_r30: None,
}
}
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct HypixelResponse { struct HypixelResponse {
#[serde(rename = "player")] #[serde(rename = "player")]
player: HypixelPlayer, player: HypixelPlayer,
} }
impl HypixelResponse { impl HypixelPlayer {
pub async fn get(uuid: &str, client: &Client) -> Result<Self, Error> { pub async fn get(uuid: &str, client: &Client) -> Result<Self, Error> {
let player = client let player = client
.get(format!("https://api.hypixel.net/v2/player?uuid={uuid}")) .get(format!("https://api.hypixel.net/v2/player?uuid={uuid}"))
.send() .send()
.await? .await?
.error_for_status()? .error_for_status()?
.json::<Self>() .json::<HypixelResponse>()
.await?; .await?
.player;
Ok(player) Ok(player)
} }
pub fn discord(self) -> Option<String> { fn discord(&self) -> Option<&str> {
self.player self.social_media()
.social_media .as_ref()
.and_then(|p| p.links) .and_then(|p| p.links().as_ref())
.and_then(|l| l.discord) .and_then(|l| l.discord().as_ref().map(|s| s.as_str()))
}
pub fn map_if_discord(self, discord: &str) -> Result<Self, LinkingError> {
match self.discord() {
Some(d) => {
if d == discord {
Ok(self)
} else {
Err(LinkingError::WrongLink(discord.to_string()))
}
}
None => Err(LinkingError::NotLinked),
}
}
pub fn arcade_stats(&self) -> Option<&Arcade> {
self.stats().as_ref().and_then(|s| s.arcade().as_ref())
} }
} }

View file

@ -64,4 +64,7 @@ cache_hit_handler!(
"https://api.minecraftservices.com/minecraft/profile/lookup/{}" "https://api.minecraftservices.com/minecraft/profile/lookup/{}"
); );
cache_hit_handler!(uuid, "https://api.minecraftservices.com/minecraft/profile/lookup/name/{}"); cache_hit_handler!(
uuid,
"https://api.minecraftservices.com/minecraft/profile/lookup/name/{}"
);

View file

@ -11,46 +11,77 @@ macro_rules! reply_fail_handler {
}}; }};
} }
#[derive(Debug)]
pub enum LinkingError {
NotLinked,
WrongLink(String),
}
impl Display for LinkingError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Self::NotLinked => write!(
f,
"This Minecraft account's Hypixel profile has no Discord account linked. Please \
follow the steps in <#1256219552568840263>."
),
Self::WrongLink(name) => write!(
f,
"This Minecraft account's Hypixel profile has a different Discord account linked. \
If you actually own this account, please set the linked discord to `{name}`"
),
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Sqlx(sqlx::Error), Sqlx(sqlx::Error),
Api(reqwest::Error), Api(reqwest::Error),
Serenity(serenity::Error), Serenity(serenity::Error),
OnCooldown(std::time::Duration), OnCooldown(std::time::Duration),
LinkingError(LinkingError),
Other(String), Other(String),
} }
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> FmtResult { fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self { match self {
Error::Sqlx(e) => write!(f, "SQLx Error: {}", e), Self::Sqlx(e) => write!(f, "SQLx Error: {e}"),
Error::Api(e) => write!(f, "HTTPS Error:\n{}", e), Self::Api(e) => write!(f, "HTTPS Error:\n{e}"),
Error::Serenity(e) => write!(f, "Discord Error:\n {}", e), Self::Serenity(e) => write!(f, "Discord Error:\n {e}"),
Error::OnCooldown(d) => write!( Self::OnCooldown(d) => write!(
f, f,
"This command is on cooldown. {}s remaining.", "This command is on cooldown. {}s remaining.",
d.as_secs() d.as_secs()
), ),
Error::Other(s) => write!(f, "{}", s), Self::LinkingError(l) => write!(f, "{l}"),
Self::Other(s) => write!(f, "{s}"),
} }
} }
} }
impl From<sqlx::Error> for Error { impl From<sqlx::Error> for Error {
fn from(error: sqlx::Error) -> Self { fn from(error: sqlx::Error) -> Self {
Error::Sqlx(error) Self::Sqlx(error)
} }
} }
impl From<reqwest::Error> for Error { impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self { fn from(error: reqwest::Error) -> Self {
Error::Api(error) Self::Api(error)
} }
} }
impl From<serenity::Error> for Error { impl From<serenity::Error> for Error {
fn from(error: serenity::Error) -> Self { fn from(error: serenity::Error) -> Self {
Error::Serenity(error) Self::Serenity(error)
}
}
impl From<LinkingError> for Error {
fn from(value: LinkingError) -> Self {
Self::LinkingError(value)
} }
} }