geoguessr pings, run rustfmt on project

This commit is contained in:
Stachelbeere1248 2024-11-02 13:49:20 +01:00
parent 020de4d0b8
commit 741cd17c62
Signed by: Stachelbeere1248
SSH key fingerprint: SHA256:IozEKdw2dB8TZxkpPdMxcWSoWTIMwoLaCcZJ1AJnY2o
11 changed files with 118 additions and 315 deletions

View file

@ -1,258 +0,0 @@
use poise::CreateReply;
use serde::{Deserialize, Serialize};
use serenity::all::{ChannelId, CreateActionRow, CreateButton, CreateMessage, ReactionType, User};
use serenity::builder::CreateAllowedMentions;
use sqlx::{Pool, query_as, Sqlite};
use crate::{Context, Error};
#[poise::command(slash_command, subcommands("add", "list"))]
pub(crate) async fn account(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
#[poise::command(slash_command)]
pub(crate) async fn add(ctx: Context<'_>, ign: String) -> Result<(), Error> {
ctx.defer_ephemeral().await?;
let pool = ctx.data().sqlite_pool.clone();
let minecraft_uuid = minecraft_uuid_for_username(ign.clone()).await?;
let hypixel_linked_discord = linked_discord_for_uuid(
ctx.data().hypixel_api_client.clone(),
minecraft_uuid.as_str(),
)
.await?;
if hypixel_linked_discord.eq(ctx.author().name.as_str()) {
link(
ctx.author().id.get(),
minecraft_uuid.as_str(),
&pool,
)
.await;
let s = format!("## User <@{}> added an account:\n### added:\n- name: {}\n- uuid: {}",
ctx.author().id.get(),
ign.clone(),
minecraft_uuid
);
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?;
ctx.send(CreateReply::default().content("Linked accounts.")).await?;
} else {
ctx.send(CreateReply::default().content("This Minecraft account's link doesn't seem to match your discord username. Be sure to not link using the display name and remove the @.")).await?;
}
Ok(())
}
#[poise::command(slash_command)]
pub(crate) async fn remove(ctx: Context<'_>) -> Result<(), Error> {
ctx.say("hi").await?;
Ok(())
}
#[poise::command(slash_command)]
pub(crate) async fn list(ctx: Context<'_>, user: Option<User>) -> Result<(), Error> {
ctx.defer_ephemeral().await?;
let user_id = user.clone().map(|user| user.id.get());
let user_name = user.clone().map(|user| user.name);
let author_name = ctx.author().name.clone();
let pool = ctx.data().sqlite_pool.clone();
let link_id = link_id_from_discord(&pool, user_id.unwrap_or(ctx.author().id.get())).await;
let t = match link_id {
Some(id) => minecraft_uuids(&pool, id).await,
None => Vec::new(),
};
let mut content = format!(
"## {}'s linked accounts: ",
user_name.unwrap_or(author_name)
);
for l in t {
content.push_str(format!("\nuuid: {}", l).as_str())
}
let reply = CreateReply::default().ephemeral(true).content(content);
if let Err(why) = ctx.send(reply).await {
println!("Error sending message: {why}");
}
Ok(())
}
async fn link(discord_id: u64, uuid: &str, pool: &Pool<Sqlite>) {
let link_id = match link_id_from_minecraft(pool, uuid.to_string()).await {
None => new_link_id(pool).await,
Some(link_id_mc_old) => {
// merge sets
let new_link_id_discord = link_id_from_discord(pool, discord_id)
.await
.unwrap_or(u16::MAX)
.cast_signed();
sqlx::query(format!("UPDATE minecraft_links SET link_id = {} WHERE link_id = {new_link_id_discord};", link_id_mc_old.cast_signed()).as_str()).execute(pool).await.expect("Database Error: linking previously linked accounts by another user");
sqlx::query(
format!(
"UPDATE discord_links SET link_id = {} WHERE link_id = {new_link_id_discord};",
link_id_mc_old.cast_signed()
)
.as_str(),
)
.execute(pool)
.await.expect("Database Error: linking previously linked accounts by another user");
link_id_mc_old
}
};
let link_id = link_id.cast_signed();
let discord_id = discord_id.cast_signed();
sqlx::query(
format!("INSERT INTO minecraft_links VALUES ({link_id}, \"{uuid}\");").as_str(),
)
.execute(pool)
.await.expect("Database Error: inserting new minecraft value");
sqlx::query(
format!("INSERT INTO discord_links VALUES ({link_id}, \"{discord_id}\");").as_str(),
)
.execute(pool)
.await.expect("Database Error: inserting new discord value");
}
#[derive(Serialize, Deserialize)]
struct Links {
#[serde(rename = "DISCORD")]
pub discord: String,
}
#[derive(Serialize, Deserialize)]
struct SocialMedia {
pub links: Links,
pub prompt: bool,
}
#[derive(Serialize, Deserialize)]
struct HypixelPlayer {
#[serde(rename = "socialMedia")]
pub social_media: SocialMedia,
}
#[derive(Serialize, Deserialize)]
struct HypixelResponse {
#[serde(rename = "player")]
pub player: HypixelPlayer,
}
#[derive(Serialize, Deserialize)]
struct MojangPlayer {
pub id: String,
pub name: String,
}
async fn minecraft_uuid_for_username(name: String) -> Result<String, serde_json::Error> {
let url = format!("https://api.mojang.com/users/profiles/minecraft/{name}");
let response = reqwest::get(url).await.expect(format!("Failed retrieving hypixel response for {name}").as_str());
let response_text = response.text().await.unwrap();
return (serde_json::from_str(response_text.as_str())
as Result<MojangPlayer, serde_json::Error>)
.map(|mojang_player: MojangPlayer| mojang_player.id);
}
async fn linked_discord_for_uuid(
hypixel_client: reqwest::Client,
uuid: &str,
) -> Result<String, Error> {
let hypixel_url = format!("https://api.hypixel.net/v2/player?uuid={uuid}");
return match hypixel_client.get(hypixel_url).send().await {
Ok(response) => {
let response_text = response.text().await.unwrap();
match (serde_json::from_str(response_text.as_str())
as Result<HypixelResponse, serde_json::Error>)
.map(|hypixel_response: HypixelResponse| {
hypixel_response.player.social_media.links.discord
}) {
Ok(discord) => Ok(discord),
Err(why) => Err(Error::try_from(why).unwrap()),
}
}
Err(why) => Err(Error::try_from(why).unwrap()),
};
}
#[derive(sqlx::FromRow)]
struct DiscordLink {
link_id: i16,
discord_id: i64,
}
#[derive(sqlx::FromRow)]
struct MinecraftLink {
link_id: i16,
minecraft_uuid: String,
}
#[derive(sqlx::FromRow)]
struct LinkId {
link_id: i16,
}
async fn link_id_from_discord(pool: &Pool<Sqlite>, snowflake: u64) -> Option<u16> {
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());
}
async fn link_id_from_minecraft(pool: &Pool<Sqlite>, minecraft_uuid: String) -> Option<u16> {
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());
}
async fn new_link_id(pool: &Pool<Sqlite>) -> u16 {
let result: Result<LinkId, sqlx::Error> = query_as("SELECT link_id FROM minecraft_links WHERE link_id = (SELECT MAX(link_id) FROM minecraft_links) LIMIT 1;")
.fetch_one(pool)
.await;
result
.expect("Database error: fetching new id")
.link_id.cast_unsigned() + 1
}
async fn minecraft_uuids(pool: &Pool<Sqlite>, link_id: u16) -> Vec<String> {
let link_id: i16 = link_id.cast_signed();
let link_result: Result<Vec<MinecraftLink>, sqlx::Error> =
query_as(format!("SELECT * FROM minecraft_links WHERE link_id = {link_id};").as_str())
.fetch_all(pool)
.await;
return match link_result {
Ok(links) => links
.into_iter()
.map(|minecraft_link: MinecraftLink| minecraft_link.minecraft_uuid)
.collect(),
Err(why) => {
println!("Error: {}", why);
Vec::new()
}
};
}
/*
async fn discord_ids(pool: &Pool<Sqlite>, link_id: u16) -> Vec<u64> {
let link_id: i16 = link_id.cast_signed();
let link_result: Result<Vec<DiscordLink>, sqlx::Error> = query_as(format!("SELECT * FROM discord_links WHERE link_id = {link_id}").as_str())
.fetch_all(pool)
.await;
return match link_result {
Ok(links) => links.into_iter().map(|discord_link: DiscordLink| discord_link.discord_id.cast_unsigned()).collect(),
Err(why) => {
println!("Error: {}", why);
Vec::new()
}
}
}*/

View file

@ -3,15 +3,15 @@ use reqwest::{Client, Response};
use serde::Deserialize;
use serenity::{
all::{
ChannelId,
CreateActionRow,
CreateAllowedMentions,
CreateButton,
CreateActionRow,
User,
CreateMessage,
ReactionType,
ChannelId,
CreateMessage
User,
},
json::JsonError
json::JsonError,
};
use sqlx::{Pool, query_as, Sqlite};
@ -44,7 +44,7 @@ struct MojangPlayer {
#[derive(PartialEq, sqlx::FromRow)]
struct Uuid {
uuid: String
uuid: String,
}
impl Uuid {
fn get(&self) -> &str {
@ -60,7 +60,7 @@ impl Uuid {
as Result<MojangPlayer, JsonError>)
.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())),
@ -69,7 +69,7 @@ impl Uuid {
}
#[derive(PartialEq)]
struct DiscordId {
id: u64
id: u64,
}
impl DiscordId {
async fn matches_fetch(user: &User, uuid: &str, client: &Client) -> Result<bool, Error> {
@ -82,7 +82,7 @@ impl DiscordId {
as Result<HypixelResponse, JsonError>)
.map(|hypixel_response: HypixelResponse| user.name == hypixel_response.player.social_media.links.discord)?;
Ok(matches)
},
}
Err(why) => {
println!("Hypixel issue: {}", why);
Err(Error::from("Hypixel returned an error."))
@ -91,9 +91,11 @@ impl DiscordId {
}
}
impl<'a, R: sqlx::Row> sqlx::FromRow<'a, R> for DiscordId
where &'a ::std::primitive::str: sqlx::ColumnIndex<R>,
where
&'a ::std::primitive::str: sqlx::ColumnIndex<R>,
i64: ::sqlx::decode::Decode<'a, R::Database>,
i64: ::sqlx::types::Type<R::Database> {
i64: ::sqlx::types::Type<R::Database>,
{
fn from_row(row: &'a R) -> sqlx::Result<Self> {
let discord_id: i64 = row.try_get("discord_id")?;
Ok(DiscordId {
@ -137,12 +139,10 @@ pub(crate) async fn account(_ctx: Context<'_>) -> Result<(), Error> {
#[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<User>,
) -> Result<(), Error> {
ctx.defer_ephemeral().await?;
@ -155,7 +155,8 @@ pub(crate) async fn add<'a>(
let r = CreateReply::default().ephemeral(false);
let pool: Pool<Sqlite> = 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 => {
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())
@ -170,13 +171,14 @@ pub(crate) async fn add<'a>(
("Your Discord account has previously had an account linked. Added the new link.", dc_id)
}
}
}, Some(mc_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.");
@ -198,7 +200,7 @@ pub(crate) async fn add<'a>(
.components(vec![CreateActionRow::Buttons(vec![
CreateButton::new("accept_verification").emoji(ReactionType::from('✅')),
CreateButton::new("deny_verification").emoji(ReactionType::from('❌')),
])])
])]),
).await?;
}
false => {
@ -214,7 +216,7 @@ pub(crate) async fn add<'a>(
#[poise::command(slash_command)]
pub(crate) async fn list(
ctx: Context<'_>,
user: Option<User>
user: Option<User>,
) -> Result<(), Error> {
ctx.defer().await?;
let user = user.unwrap_or(ctx.author().clone());

View file

@ -1,7 +1,7 @@
use std::string::String;
use crate::commands::command_helper;
use crate::{Context, Error};
use crate::commands::command_helper;
#[poise::command(slash_command, guild_only, owners_only)]
pub(crate) async fn bots(

View file

@ -1,8 +1,8 @@
use poise::CreateReply;
use serenity::all::CreateAllowedMentions;
use crate::commands::command_helper;
use crate::{Context, Error};
use crate::commands::command_helper;
#[poise::command(slash_command, guild_only)]
pub(crate) async fn helpstart(

View file

@ -1,8 +1,9 @@
use poise::{ChoiceParameter, CreateReply};
use serenity::all::{CreateAllowedMentions, RoleId};
use crate::{Context, Error};
//from main.rs
use crate::commands::command_helper::cooldown;
use crate::{Context, Error};
//
use crate::commands::lfg::Difficulty::Normal;
use crate::commands::lfg::Map::*;
@ -44,29 +45,23 @@ pub enum Difficulty {
#[poise::command(slash_command, guild_only)]
pub(crate) async fn lfg(
ctx: Context<'_>,
#[rename = "map"] map: Map,
#[description = "Normal"]
#[rename = "difficulty"]
difficulty: Option<Difficulty>,
#[rename = "mode"]
#[description = "play-style"]
mode: Mode,
#[min = 1_u8]
#[max = 3_u8]
#[description = "default: 1"]
#[rename = "current"]
current_players: Option<u8>,
#[min = 2_u8]
#[max = 4_u8]
#[description = "default: 4"]
#[rename = "desired"]
desired_players: Option<u8>,
#[description = "optional extra message"]
#[rename = "message"]
note: Option<String>,
@ -142,31 +137,25 @@ enum ExpertMap {
#[name = "Speedrun"]
Speedrun,
}
#[poise::command(slash_command, guild_only, rename = "expert-lfg")]
#[poise::command(slash_command, guild_only, rename = "lfg-expert")]
pub(crate) async fn expert(
ctx: Context<'_>,
#[rename = "map"] mode: ExpertMap,
#[min = 1_u8]
#[max = 3_u8]
#[description = "default: 1"]
#[rename = "current"]
current_players: Option<u8>,
#[min = 2_u8]
#[max = 4_u8]
#[description = "default: 4"]
#[rename = "desired"]
desired_players: Option<u8>,
#[description = "extra message"]
#[rename = "message"]
note: String,
) -> Result<(), Error> {
let mut reply: CreateReply = CreateReply::default();
reply = match cooldown(&ctx, 600, 300) {
let reply: CreateReply = match cooldown(&ctx, 600, 300) {
Ok(_) => {
let current: u8 = current_players.unwrap_or(1);
let mut desired: u8 = desired_players.unwrap_or(4);
@ -199,17 +188,20 @@ pub(crate) async fn expert(
.iter()
.any(|user_role: &RoleId| allowed_roles.contains(&user_role.get()));
let reply_content: String = format!("{current}/{desired} <@&{ping}>: {note}");
match is_expert {
true => reply
true => CreateReply::default()
.content(reply_content)
.ephemeral(false)
.allowed_mentions(CreateAllowedMentions::new().roles(vec![ping])),
false => reply
false => CreateReply::default()
.content("You do not have any of the required expert ranks.")
.ephemeral(true),
}
}
Err(why) => reply.content(why.to_string()).ephemeral(true),
Err(why) => {
CreateReply::default().content(why.to_string()).ephemeral(true)
}
};
if let Err(why) = ctx.send(reply).await {
@ -218,6 +210,53 @@ pub(crate) async fn expert(
Ok(())
}
#[derive(Debug, poise::ChoiceParameter)]
enum OtherPing {
#[name = "GeoGuessr"]
GeoGuessr,
}
#[poise::command(slash_command, guild_only, rename = "lfg-other")]
pub(crate) async fn other(
ctx: Context<'_>,
#[rename = "game"]
game: OtherPing,
#[min = 1_u8]
#[max = 3_u8]
#[description = "default: 1"]
#[rename = "current"]
current_players: Option<u8>,
#[description = "extra message"]
#[rename = "message"]
note: String,
) -> Result<(), Error> {
let reply: CreateReply = match cooldown(&ctx, 0, 7200) {
Ok(_) => {
let current: u8 = current_players.unwrap_or(1);
let desired: u8 = match game {
OtherPing::GeoGuessr => 20_u8,
};
let ping: u64 = match game {
OtherPing::GeoGuessr => 1302249562999885824_u64,
};
let reply_content: String = format!("{current}/{desired} <@&{ping}>: {note}");
CreateReply::default()
.content(reply_content)
.ephemeral(false)
.allowed_mentions(CreateAllowedMentions::new().roles(vec![ping]))
}
Err(why) => {
CreateReply::default().content(why.to_string()).ephemeral(true)
}
};
if let Err(why) = ctx.send(reply).await {
println!("Error sending message: {why}");
}
Ok(())
}
const ROLE_LIST: [[u64; 6]; 9] = [ // [[basic, de, bb, aa, sr, star]; 9]
[1256229103678259311, 1256229192744304670, 1256229223450935377, 1256229498899271754, 1256229540900900996, 1256229575269154866], //novice
[1256230831131983932, 1256230750827577447, 1256230776828334143, 1256230793630715975, 1256230818444214333, 1256230734642024468], //seasoned

View file

@ -1,10 +1,11 @@
use serenity::all::{ComponentInteraction, ComponentInteractionDataKind, Context, CreateMessage, EditMessage, GuildId, Interaction, RoleId};
use crate::Error;
pub(crate) async fn component(ctx: &Context, interaction: &Interaction) -> Result<(), Error> {
let component = interaction.clone().message_component().unwrap();
match component.data.kind {
ComponentInteractionDataKind::Button => button(ctx, component, ).await,
ComponentInteractionDataKind::Button => button(ctx, component).await,
_ => Ok(())
}
}
@ -21,7 +22,7 @@ async fn button(ctx: &Context, mut component: ComponentInteraction) -> Result<()
member.remove_role(ctx, RoleId::new(1256253358701023232_u64)).await?;
component.message.edit(ctx, EditMessage::new().components(vec![])).await?;
Ok(())
},
}
"deny_verification" => {
let _dm = u.direct_message(ctx, CreateMessage::new()
.content("Your verified minecraft account was denied.")).await?;

View file

@ -1,12 +1,13 @@
use serenity::all::{Context, Message};
use crate::Error;
pub(crate) async fn create(ctx: &Context, msg: &Message) -> Result<(), Error> {
pub(crate) async fn on_create(ctx: &Context, msg: &Message) -> Result<(), Error> {
match msg.guild_id.map(|g| g.get()) {
None => Ok(()),
Some(1256217633959841853_u64) => {
zmp_create(ctx, msg).await
},
}
_ => Ok(())
}
}
@ -17,7 +18,7 @@ async fn zmp_create(ctx: &Context, msg: &Message) -> Result<(), Error> {
msg.react(ctx, '🇼').await?;
msg.react(ctx, '🇱').await?;
Ok(())
},
}
_ => Ok(())
}
}

View file

@ -1,2 +1,3 @@
pub(crate) mod bot_interaction;
pub(crate) mod message;
pub(crate) mod thread;

15
src/handlers/thread.rs Normal file
View file

@ -0,0 +1,15 @@
use serenity::all::{Context, GuildChannel};
use serenity::builder::EditThread;
use crate::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(7200_u16)).await?;
Ok(())
}
Some(_) => Ok(()),
None => Ok(())
}
}

View file

@ -9,7 +9,7 @@ use poise::serenity_prelude as serenity;
use serenity::{FullEvent, model::id::UserId};
use serenity::all::{ActivityData, InteractionType, RoleId};
use serenity::prelude::GatewayIntents;
use sqlx::{Sqlite};
use sqlx::Sqlite;
use tokio::sync::RwLock;
mod commands;
@ -59,7 +59,6 @@ async fn main() {
on_error: |error| {
Box::pin(async move {
match error {
other => poise::builtins::on_error(other).await.unwrap(),
}
})
@ -103,22 +102,25 @@ async fn event_handler(
match event {
FullEvent::Ready { data_about_bot, .. } => {
println!("Logged in as {}", data_about_bot.user.name);
},
}
FullEvent::GuildMemberAddition { new_member } => {
println!("join event");
if new_member.guild_id.get() == 1256217633959841853_u64 {
new_member.add_role(ctx, RoleId::new(1256253358701023232_u64)).await?;
println!("gave member role");
}
},
}
FullEvent::InteractionCreate { interaction } => {
if interaction.application_id().get() == 1165594074473037824
&& interaction.kind() == InteractionType::Component {
handlers::bot_interaction::component(ctx, interaction).await?;
}
},
}
FullEvent::Message { new_message } => {
handlers::message::create(ctx, new_message).await?;
handlers::message::on_create(ctx, new_message).await?;
}
FullEvent::ThreadCreate { thread } => {
handlers::thread::on_create(ctx, thread).await?;
}
_ => {}
}