Compare commits

..

No commits in common. "f5768998b99f2a54d64d9d42891de4ea847d00bd" and "4b03762390d019359794dff0503b39faabe8c437" have entirely different histories.

26 changed files with 335 additions and 684 deletions

11
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "cargo" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

22
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --release --verbose
- name: Run tests
run: cargo test --verbose

8
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

13
.idea/ZMP-bot.iml generated Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/commands/T/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/src/commands/T/target" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,11 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="RsMainFunctionNotFound" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

7
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/../zmp-bot/.idea/ZMP-bot.iml" filepath="$PROJECT_DIR$/../zmp-bot/.idea/ZMP-bot.iml" />
</modules>
</component>
</project>

7
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../zmp-bot" vcs="Git" />
</component>
</project>

View file

@ -1,4 +1,4 @@
max_width = 100 max_width = 140
use_small_heuristics = "Default" use_small_heuristics = "Default"
reorder_imports = true reorder_imports = true
format_strings = true format_strings = true

58
Cargo.lock generated
View file

@ -398,20 +398,6 @@ dependencies = [
"serde", "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]] [[package]]
name = "data-encoding" name = "data-encoding"
version = "2.6.0" version = "2.6.0"
@ -609,7 +595,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor",
"futures-io", "futures-io",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
@ -731,18 +716,6 @@ dependencies = [
"wasi", "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]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.1" version = "0.31.1"
@ -1341,7 +1314,7 @@ checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"crossbeam-utils", "crossbeam-utils",
"dashmap 5.5.3", "dashmap",
"skeptic", "skeptic",
"smallvec", "smallvec",
"tagptr", "tagptr",
@ -1646,28 +1619,6 @@ dependencies = [
"zerocopy", "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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.92" version = "1.0.92"
@ -2138,7 +2089,7 @@ dependencies = [
"bytes", "bytes",
"chrono", "chrono",
"command_attr", "command_attr",
"dashmap 5.5.3", "dashmap",
"flate2", "flate2",
"futures", "futures",
"fxhash", "fxhash",
@ -2890,7 +2841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549e54551d85ba6718a95333d9bc4367f69793d7aba638de30f8d25a1f554a1d" checksum = "549e54551d85ba6718a95333d9bc4367f69793d7aba638de30f8d25a1f554a1d"
dependencies = [ dependencies = [
"chrono", "chrono",
"dashmap 5.5.3", "dashmap",
"hashbrown 0.14.5", "hashbrown 0.14.5",
"mini-moka", "mini-moka",
"parking_lot", "parking_lot",
@ -3467,9 +3418,6 @@ dependencies = [
name = "zmp-bot" name = "zmp-bot"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dashmap 6.1.0",
"futures",
"getset",
"poise", "poise",
"reqwest 0.12.9", "reqwest 0.12.9",
"serde", "serde",

View file

@ -12,7 +12,4 @@ serde = { version = "1.0.216", features = ["derive"] }
reqwest = { version = "0.12.9", features = ["json"] } reqwest = { version = "0.12.9", features = ["json"] }
tokio = { version = "1.42.0", features = ["rt-multi-thread"] } tokio = { version = "1.42.0", features = ["rt-multi-thread"] }
tracing = { version = "0.1.41" } tracing = { version = "0.1.41" }
sqlx = { version = "0.8.2", features = ["sqlite", "sqlx-sqlite", "runtime-tokio"]} sqlx = { version = "0.8.2", features = ["sqlite", "sqlx-sqlite", "runtime-tokio"]}
futures = "0.3.31"
dashmap = "6.1.0"
getset = "0.1.4"

View file

@ -1,18 +1,14 @@
use poise::CreateReply; use poise::CreateReply;
use reqwest::Client; use reqwest::{Client, Response};
use serde::Deserialize; use serde::Deserialize;
use serenity::all::ButtonStyle; use serenity::all::ButtonStyle;
use serenity::all::{ use serenity::all::{ChannelId, CreateActionRow, CreateAllowedMentions, CreateButton, CreateMessage, ReactionType, User};
ChannelId, CreateActionRow, CreateAllowedMentions, CreateButton, CreateMessage, ReactionType,
User,
};
use sqlx::{query_as, Pool, Sqlite}; use sqlx::{query_as, Pool, Sqlite};
use std::ops::Add; use std::ops::Add;
use crate::commands::command_helper::cooldown; use crate::commands::command_helper::cooldown;
use crate::error::Error; use crate::error::Error;
use crate::error::Error::Other; use crate::error::Error::Other;
use crate::Caches;
use crate::Context; use crate::Context;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -20,69 +16,69 @@ struct Links {
#[serde(rename = "DISCORD")] #[serde(rename = "DISCORD")]
pub discord: Option<String>, pub discord: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct SocialMedia { struct SocialMedia {
pub links: Option<Links>, pub links: Option<Links>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct HypixelPlayer { struct HypixelPlayer {
#[serde(rename = "socialMedia")] #[serde(rename = "socialMedia")]
pub social_media: Option<SocialMedia>, pub social_media: Option<SocialMedia>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct HypixelResponse { struct HypixelResponse {
#[serde(rename = "player")] #[serde(rename = "player")]
pub player: HypixelPlayer, pub player: HypixelPlayer,
} }
#[derive(Deserialize)]
#[derive(PartialEq, sqlx::FromRow)] struct MojangPlayer {
pub(crate) struct Uuid { pub id: String,
pub(crate) uuid: String, pub name: String,
} }
#[derive(PartialEq, sqlx::FromRow)]
struct Uuid {
uuid: String,
}
impl Uuid { impl Uuid {
fn get(&self) -> &str { fn get(&self) -> &str {
self.uuid.as_str() self.uuid.as_str()
} }
async fn for_ign(ign: &String, cli: &Client, c: &Caches) -> Result<Self, Error> { async fn for_ign(ign: &str) -> Result<Self, Error> {
let uuid = crate::data::mojang::uuid(c, cli, ign.clone()).await?; let url: String = format!("https://api.mojang.com/users/profiles/minecraft/{ign}");
Ok(Self { uuid }) let response_400: Response = reqwest::get(url).await?.error_for_status()?;
let deserialized = response_400.json::<MojangPlayer>().await?;
let uuid = Uuid { uuid: deserialized.id };
Ok(uuid)
} }
async fn ign(&self, c: &Caches, cli: &Client) -> Result<String, Error> { async fn ign(&self) -> Result<String, Error> {
let ign = crate::data::mojang::name(c, cli, self.uuid.clone()).await?; let url: String = format!("https://sessionserver.mojang.com/session/minecraft/profile/{}", self.uuid);
Ok(ign) let response_400: Response = reqwest::get(url).await?.error_for_status()?;
let deserialized = response_400.json::<MojangPlayer>().await?;
Ok(deserialized.name)
} }
} }
#[derive(PartialEq)] #[derive(PartialEq)]
struct DiscordId { struct DiscordId {
id: u64, id: u64,
} }
impl Uuid { impl Uuid {
async fn has_discord_user(&self, user: &User, client: &Client) -> Result<bool, Error> { async fn has_discord_user(&self, user: &User, client: &Client) -> Result<bool, Error> {
let url: String = format!("https://api.hypixel.net/v2/player?uuid={}", self.uuid); let url: String = format!("https://api.hypixel.net/v2/player?uuid={}", self.uuid);
let res: HypixelResponse = client let response_400: Response = client.get(url).send().await?.error_for_status()?;
.get(url) let deserialized = response_400.json::<HypixelResponse>().await?;
.send() let matches = deserialized
.await?
.error_for_status()?
.json::<HypixelResponse>()
.await?;
let matches = res
.player .player
.social_media .social_media
.and_then(|sm| sm.links) .map(|sm| sm.links)
.and_then(|l| l.discord) .flatten()
.ok_or(Other( .map(|l| l.discord)
"The Hypixel profile has no Discord account linked. Please follow the steps in \ .flatten()
<#1256219552568840263>" .ok_or(Other(format!(
.to_string(), "The Hypixel profile has no Discord account linked. Please follow the steps in {}",
))? ChannelId::new(1256219552568840263_u64)
)))?
== user.name; == user.name;
Ok(matches) Ok(matches)
} }
@ -100,13 +96,11 @@ where
}) })
} }
} }
struct Link {
pub(crate) struct Link {
link_id: u16, link_id: u16,
discord_ids: Vec<DiscordId>, discord_ids: Vec<DiscordId>,
pub(crate) minecraft_accounts: Vec<Uuid>, minecraft_accounts: Vec<Uuid>,
} }
impl Link { impl Link {
fn new(link_id: u16) -> Self { fn new(link_id: u16) -> Self {
Link { Link {
@ -117,40 +111,34 @@ impl Link {
} }
async fn minecraft(mut self, pool: &Pool<Sqlite>) -> Result<Self, Error> { async fn minecraft(mut self, pool: &Pool<Sqlite>) -> Result<Self, Error> {
let link_id: i16 = self.link_id.cast_signed(); let link_id: i16 = self.link_id.cast_signed();
self.minecraft_accounts = query_as( self.minecraft_accounts =
format!( query_as(format!("SELECT minecraft_uuid AS uuid FROM minecraft_links WHERE link_id = {link_id};").as_str())
"SELECT minecraft_uuid AS uuid FROM minecraft_links WHERE link_id = {link_id};" .fetch_all(pool)
) .await?;
.as_str(),
)
.fetch_all(pool)
.await?;
Ok(self) Ok(self)
} }
async fn discord(mut self, pool: &Pool<Sqlite>) -> Result<Self, Error> { async fn discord(mut self, pool: &Pool<Sqlite>) -> Result<Self, Error> {
let link_id: i16 = self.link_id.cast_signed(); let link_id: i16 = self.link_id.cast_signed();
self.discord_ids = query_as( self.discord_ids = query_as(format!("SELECT discord_id FROM discord_links WHERE link_id = {link_id};").as_str())
format!("SELECT discord_id FROM discord_links WHERE link_id = {link_id};").as_str(), .fetch_all(pool)
) .await?;
.fetch_all(pool)
.await?;
Ok(self) Ok(self)
} }
} }
#[poise::command( #[poise::command(slash_command, subcommands("add", "list"))]
slash_command,
subcommands("add", "list"),
install_context = "User|Guild",
interaction_context = "Guild|BotDm|PrivateChannel"
)]
pub(crate) async fn account(_ctx: Context<'_>) -> Result<(), Error> { pub(crate) async fn account(_ctx: Context<'_>) -> Result<(), Error> {
// root of slash-commands is not invokable. // root of slash-commands is not invokable.
unreachable!() unreachable!()
} }
#[poise::command(slash_command, ephemeral = "false")] #[poise::command(
slash_command,
install_context = "User|Guild",
interaction_context = "Guild|BotDm|PrivateChannel",
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<'a>(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Minecraft username"] #[description = "Minecraft username"]
#[min_length = 2] #[min_length = 2]
@ -160,79 +148,42 @@ pub(crate) async fn add(
#[description = "admin-only"] force: Option<bool>, #[description = "admin-only"] force: Option<bool>,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.defer().await?; ctx.defer().await?;
let force: bool = let force: bool = force.unwrap_or(false) && ctx.framework().options.owners.contains(&ctx.author().id) && {
force.unwrap_or(false) && ctx.framework().options.owners.contains(&ctx.author().id) && { let _ = user.as_ref().ok_or(Other(
let _ = user.as_ref().ok_or(Other( "Warning: attempted to run forced account add without specifying a target Discord account.".to_string(),
"Warning: attempted to run forced account add without specifying a target Discord \ ))?;
account." true
.to_string(), };
))?;
true
};
let user: User = user.unwrap_or(ctx.author().clone()); 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()).await?;
match force match force || uuid.has_discord_user(&user, &ctx.data().hypixel_api_client).await? {
|| uuid
.has_discord_user(&user, &ctx.data().clients.hypixel_api_client)
.await?
{
true => { true => {
let pool = &ctx.data().sqlite_pool; let pool = &ctx.data().sqlite_pool;
let status: &str = match link_id_from_minecraft(pool, uuid.get()).await { let status: &str = 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 => { None => {
let id = new_link_id(pool).await?; let id = new_link_id(pool).await?;
sqlx::query( sqlx::query(format!("INSERT INTO discord_links VALUES ({}, {});", id.inner, user.id.get()).as_str())
format!( .execute(pool)
"INSERT INTO discord_links VALUES ({}, {});", .await?;
id.inner, sqlx::query(format!("INSERT INTO minecraft_links VALUES ({}, \"{}\");", id.inner, uuid.get()).as_str())
user.id.get() .execute(pool)
) .await?;
.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." "Linked your Discord and Minecraft account."
} }
Some(dc_id) => { Some(dc_id) => {
sqlx::query( sqlx::query(format!("INSERT INTO minecraft_links VALUES ({}, \"{}\");", dc_id.inner, uuid.get()).as_str())
format!( .execute(pool)
"INSERT INTO minecraft_links VALUES ({}, \"{}\");", .await?;
dc_id.inner, "Your Discord account has previously had an account linked. Added the new link."
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 { Some(mc_id) => match link_id_from_discord(pool, user.id.get()).await {
None => { None => {
sqlx::query( sqlx::query(format!("INSERT INTO discord_links VALUES ({}, {});", mc_id.inner, user.id.get()).as_str())
format!( .execute(pool)
"INSERT INTO discord_links VALUES ({}, {});", .await?;
mc_id.inner, "Your Minecraft account has previously had an account linked. Added the new link."
user.id.get()
)
.as_str(),
)
.execute(pool)
.await?;
"Your Minecraft account has previously had an account linked. Added the \
new link."
} }
Some(dc_id) => { Some(dc_id) => {
sqlx::query( sqlx::query(
@ -253,24 +204,17 @@ pub(crate) async fn add(
) )
.execute(pool) .execute(pool)
.await?; .await?;
"Both your Discord and Minecraft account had linked accounts. Merged all \ "Both your Discord and Minecraft account had linked accounts. Merged all account links."
account links."
} }
}, },
}; };
let s = format!( let s = format!("Verification request for <@{}> with IGN `{}`", user.id.get(), ign);
"Verification request for <@{}> with IGN `{}`",
user.id.get(),
ign
);
ChannelId::new(1257776992497959075) ChannelId::new(1257776992497959075)
.send_message( .send_message(
ctx, ctx,
CreateMessage::new() CreateMessage::new()
.content(s) .content(s)
.allowed_mentions( .allowed_mentions(CreateAllowedMentions::new().empty_roles().all_users(true))
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('✅'))
@ -288,8 +232,8 @@ pub(crate) async fn add(
Ok(()) Ok(())
} }
false => Err(Error::Other(format!( false => Err(Error::Other(format!(
"The Discord account linked on Hypixel does not match the specified discord \ "The Discord account linked on Hypixel does not match the specified discord account.\nPlease set your linked Discord account \
account.\nPlease set your linked Discord account on Hypixel to `{}`.", on Hypixel to `{}`.",
user.name user.name
))), ))),
} }
@ -297,6 +241,8 @@ pub(crate) async fn add(
#[poise::command( #[poise::command(
slash_command, slash_command,
install_context = "User|Guild",
interaction_context = "Guild|BotDm|PrivateChannel",
ephemeral = "true", ephemeral = "true",
context_menu_command = "Account list" context_menu_command = "Account list"
)] )]
@ -305,8 +251,7 @@ pub(crate) async fn list(ctx: Context<'_>, user: User) -> Result<(), Error> {
ctx.defer().await?; ctx.defer().await?;
cooldown(&ctx, 600, 300)?; cooldown(&ctx, 600, 300)?;
let pool: &Pool<Sqlite> = &ctx.data().sqlite_pool; let pool: &Pool<Sqlite> = &ctx.data().sqlite_pool;
let s: String = let s: String = list_string(pool, &user).await?;
list_string(pool, &user, &ctx.data().caches, &ctx.data().clients.general).await?;
ctx.send( ctx.send(
CreateReply::default() CreateReply::default()
.content(s) .content(s)
@ -316,33 +261,19 @@ pub(crate) async fn list(ctx: Context<'_>, user: User) -> Result<(), Error> {
Ok(()) Ok(())
} }
pub(crate) async fn get_link(user: &User, pool: &Pool<Sqlite>) -> Result<Link, Error> { pub(crate) async fn list_string(pool: &Pool<Sqlite>, user: &User) -> Result<String, Error> {
let link_id: u16 = link_id_from_discord(pool, user.id.get()) let link_id: u16 = link_id_from_discord(pool, user.id.get())
.await .await
.expect("This user has no linked accounts") .expect("This user has no linked accounts")
.into(); .into();
let link = Link::new(link_id) let link: Link = Link::new(link_id).minecraft(pool).await?.discord(pool).await?;
.minecraft(pool)
.await?
.discord(pool)
.await?;
Ok(link)
}
pub(crate) async fn list_string(
pool: &Pool<Sqlite>,
user: &User,
c: &Caches,
cli: &Client,
) -> Result<String, Error> {
let link = get_link(user, pool).await?;
let mut discord_list = String::from("### Discord:"); let mut discord_list = String::from("### Discord:");
for dc in link.discord_ids { for dc in link.discord_ids {
discord_list.push_str(format!("\n- <@{}>", dc.id).as_str()); discord_list.push_str(format!("\n- <@{}>", dc.id).as_str());
} }
let mut minecraft_list = String::from("### Minecraft:"); 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()); minecraft_list.push_str(format!("\n- `{}`", mc.ign().await?).as_str());
} }
Ok(format!( Ok(format!(
"## Account list for member #{}:\n{}\n{}", "## Account list for member #{}:\n{}\n{}",
@ -358,16 +289,10 @@ pub(crate) async fn remove(_ctx: Context<'_>) -> Result<(), Error> {
} }
async fn link_id_from_minecraft(pool: &Pool<Sqlite>, minecraft_uuid: &str) -> Option<LinkId> { async fn link_id_from_minecraft(pool: &Pool<Sqlite>, minecraft_uuid: &str) -> Option<LinkId> {
query_as( query_as(format!("SELECT link_id FROM minecraft_links WHERE minecraft_uuid = \"{minecraft_uuid}\" LIMIT 1;").as_str())
format!( .fetch_optional(pool)
"SELECT link_id FROM minecraft_links WHERE minecraft_uuid = \"{minecraft_uuid}\" \ .await
LIMIT 1;" .expect("Database error: fetching link id by uuid")
)
.as_str(),
)
.fetch_optional(pool)
.await
.expect("Database error: fetching link id by uuid")
} }
async fn link_id_from_discord(pool: &Pool<Sqlite>, snowflake: u64) -> Option<LinkId> { async fn link_id_from_discord(pool: &Pool<Sqlite>, snowflake: u64) -> Option<LinkId> {
query_as( query_as(

27
src/commands/bots.rs Normal file
View file

@ -0,0 +1,27 @@
use std::string::String;
use poise::CreateReply;
use crate::Context;
use crate::error::Error;
#[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(())
}

View file

@ -1,7 +1,7 @@
use std::time::Duration; use std::time::Duration;
use crate::error::Error;
use crate::Context; use crate::Context;
use crate::error::Error;
pub(crate) fn cooldown(ctx: &Context, user: u64, guild: u64) -> Result<(), Error> { pub(crate) fn cooldown(ctx: &Context, user: u64, guild: u64) -> Result<(), Error> {
let mut cooldown_tracker = ctx.command().cooldowns.lock().unwrap(); let mut cooldown_tracker = ctx.command().cooldowns.lock().unwrap();

View file

@ -1,136 +1,45 @@
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::serenity_prelude::ButtonStyle;
use poise::serenity_prelude::ComponentInteractionCollector;
use poise::serenity_prelude::CreateActionRow;
use poise::serenity_prelude::CreateButton;
use poise::serenity_prelude::CreateEmbed;
use poise::serenity_prelude::CreateInteractionResponse;
use poise::serenity_prelude::CreateInteractionResponseMessage;
use poise::CreateReply; use poise::CreateReply;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use serenity::all::CreateAllowedMentions;
use crate::commands::command_helper;
use crate::Context;
use crate::error::Error;
#[poise::command( #[poise::command(
slash_command, slash_command,
install_context = "Guild|User", install_context = "Guild",
interaction_context = "Guild|BotDm|PrivateChannel", interaction_context = "Guild",
ephemeral = "true" ephemeral = "false",
)] )]
// Check for bots available to you. /// Ping the @helpstart to fill a queue.
pub(crate) async fn helpstart(ctx: Context<'_>, user: Option<String>) -> Result<(), Error> { pub(crate) async fn helpstart(
ctx.defer_ephemeral().await?; ctx: Context<'_>,
let links = super::accountv2::get_link(ctx.author(), &ctx.data().sqlite_pool).await?; #[min = 1_u8]
let mc_accounts = match user { #[max = 3_u8]
None => { #[description = "amount of players in your party, DO NOT include bots"]
futures::future::try_join_all( #[rename = "current"]
links current_players: u8,
.minecraft_accounts ) -> Result<(), Error> {
.into_iter() let needed_players = 4 - current_players;
.map(|a| name(&ctx.data().caches, &ctx.data().clients.general, a.uuid)) let bots = *ctx.data().bots.read().await;
.collect::<Vec<_>>(), let g = ctx.guild_id().unwrap().get();
) let mut reply = CreateReply::default();
.await? let ping = match g {
} 1256217633959841853_u64 => 1257411572092113017_u64,
Some(name) => vec![name], _ => 0_u64,
}; };
let bots = fetch_all(&ctx.data().clients.local_api_client).await?; reply = if bots >= needed_players {
let usable = bots reply
.iter() .content("Bots available. Please use <@424767825001971715> in the bot-commands channel instead.")
.filter(|b| match b.list_type() {
Whitelist => b.list().iter().any(|w| mc_accounts.contains(w)),
Blacklist => mc_accounts.iter().any(|m| !b.list().contains(m)),
})
.collect::<Vec<_>>();
let s: String = usable
.iter()
.map(|b| b.username().as_str())
.collect::<Vec<_>>()
.join(", ");
let ready: String = usable
.iter()
.filter_map(|b| {
if !*b.in_party()
&& *b.last_updated()
> (SystemTime::now().duration_since(UNIX_EPOCH).ok()? - Duration::from_secs(5))
.as_secs_f64()
{
Some(b.username().as_str())
} else {
println!(
"{}, {}",
*b.last_updated(),
(SystemTime::now().duration_since(UNIX_EPOCH).ok()? - Duration::from_secs(5))
.as_secs_f64()
);
None
}
})
.collect::<Vec<_>>()
.join(", ");
let bid = ctx.id();
let components = vec![CreateActionRow::Buttons(vec![CreateButton::new(
bid.to_string(),
)
.style(ButtonStyle::Primary)
.label("📜")])];
let reply = CreateReply::default()
.content(format!(
"Bots that are ready for use: {ready}\nBots you can use: {s}\nTotal registered bots: \
{}",
bots.len()
))
.components(components)
.ephemeral(true);
let m = ctx.send(reply).await?;
while let Some(i) = ComponentInteractionCollector::new(ctx)
.author_id(ctx.author().id)
.channel_id(ctx.channel_id())
.timeout(std::time::Duration::from_secs(60))
.filter(move |i| i.data.custom_id == bid.to_string())
.await
{
let embed = CreateEmbed::new()
.fields(bots.iter().filter_map(|b| {
if b.note().trim().is_empty() || usable.iter().any(|&u| std::ptr::eq(u, b)) {
None
} else {
Some((b.username(), b.note(), true))
}
}))
.title("Notes")
.description(
"Below is the note of each bot that you cannot use. It might help you get \
whitelisted.",
);
i.create_response(
ctx,
CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new()
.embed(embed)
.ephemeral(true),
),
)
.await?;
}
m.edit(
ctx,
CreateReply::default()
.content(format!(
"Bots that are ready for use: {ready}\nBots you can use: {s}\nTotal registered \
bots: {}",
bots.len()
))
.ephemeral(true) .ephemeral(true)
.components(vec![]), } else {
) command_helper::cooldown(&ctx, 1200, 600)?;
.await?; reply
.content(format!("## <@&{ping}>\nneed: {}", needed_players - bots))
.ephemeral(false)
.allowed_mentions(CreateAllowedMentions::new().roles(vec![ping]))
};
ctx.send(reply).await?;
Ok(()) Ok(())
} }

View file

@ -7,8 +7,8 @@ use crate::commands::command_helper::cooldown;
use crate::commands::lfg::Difficulty::Normal; use crate::commands::lfg::Difficulty::Normal;
use crate::commands::lfg::Map::*; use crate::commands::lfg::Map::*;
use crate::commands::lfg::Mode::*; use crate::commands::lfg::Mode::*;
use crate::error::Error;
use crate::Context; use crate::Context;
use crate::error::Error;
#[derive(Debug, poise::ChoiceParameter, PartialEq)] #[derive(Debug, poise::ChoiceParameter, PartialEq)]
pub enum Map { pub enum Map {
@ -47,7 +47,7 @@ pub enum Difficulty {
slash_command, slash_command,
install_context = "Guild", install_context = "Guild",
interaction_context = "Guild", interaction_context = "Guild",
ephemeral = "false" ephemeral = "false",
)] )]
/// Find a team for Hypixel Zombies. /// Find a team for Hypixel Zombies.
pub(crate) async fn lfg( pub(crate) async fn lfg(
@ -102,7 +102,7 @@ pub(crate) async fn lfg(
AlienArcadium => Normal, AlienArcadium => Normal,
}; };
let mut reply_content: String = format!("## <@&{ping}> {current}/{desired} {map_name}",); let mut reply_content: String = format!("## <@&{ping}> {current}/{desired} {map_name}", );
match difficulty { match difficulty {
Normal => {} Normal => {}
Difficulty::Hard | Difficulty::Rip => { Difficulty::Hard | Difficulty::Rip => {
@ -171,39 +171,19 @@ pub(crate) async fn expert(
let (ping, allowed_roles): (u64, Vec<u64>) = match mode { let (ping, allowed_roles): (u64, Vec<u64>) = match mode {
ExpertMap::Speedrun => ( ExpertMap::Speedrun => (
1295322375637958716, 1295322375637958716,
ROLE_LIST ROLE_LIST.iter().skip(2).map(|tier| [tier[4], tier[5]]).flatten().collect(),
.iter()
.skip(2)
.map(|tier| [tier[4], tier[5]])
.flatten()
.collect(),
), ),
ExpertMap::DeadEnd => ( ExpertMap::DeadEnd => (
1295321319344177172, 1295321319344177172,
ROLE_LIST ROLE_LIST.iter().skip(2).map(|tier| [tier[1], tier[5]]).flatten().collect(),
.iter()
.skip(2)
.map(|tier| [tier[1], tier[5]])
.flatten()
.collect(),
), ),
ExpertMap::BadBlood => ( ExpertMap::BadBlood => (
1295322259631640607, 1295322259631640607,
ROLE_LIST ROLE_LIST.iter().skip(2).map(|tier| [tier[2], tier[5]]).flatten().collect(),
.iter()
.skip(2)
.map(|tier| [tier[2], tier[5]])
.flatten()
.collect(),
), ),
ExpertMap::AlienArcadium => ( ExpertMap::AlienArcadium => (
1295322327910842441, 1295322327910842441,
ROLE_LIST ROLE_LIST.iter().skip(2).map(|tier| [tier[3], tier[5]]).flatten().collect(),
.iter()
.skip(2)
.map(|tier| [tier[3], tier[5]])
.flatten()
.collect(),
), ),
}; };
let is_expert: bool = ctx let is_expert: bool = ctx

View file

@ -1,4 +1,5 @@
pub(crate) mod accountv2; pub(crate) mod accountv2;
pub(crate) mod bots;
pub(crate) mod command_helper; pub(crate) mod command_helper;
pub(crate) mod helpstart; pub(crate) mod helpstart;
pub(crate) mod lfg; pub(crate) mod lfg;

View file

@ -1,28 +1,21 @@
use crate::error::Error;
use crate::Context; use crate::Context;
use crate::error::Error;
const XD: &str = "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\\ const XD: &str = "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⡿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⡿⠿⠿⠿⠿⠿⠿⠿⢿⡿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n\
n\\ \n\n\
n\\ \n\n\
n\\ \n\n\
n\\ \n\n\
n\\ \n\n\
n\\ \n\n\
n\\ \n";
n\\
n\\
n\\
n\\
n\\
n\\
n\n";
#[poise::command( #[poise::command(
slash_command, slash_command,
owners_only, owners_only,
install_context = "User|Guild", install_context = "User|Guild",
interaction_context = "Guild|BotDm|PrivateChannel", interaction_context = "Guild|BotDm|PrivateChannel",
ephemeral = "false" ephemeral = "false",
)] )]
/// Useless command to check if the bot is online. /// Useless command to check if the bot is online.
pub(crate) async fn xd(ctx: Context<'_>) -> Result<(), Error> { pub(crate) async fn xd(ctx: Context<'_>) -> Result<(), Error> {

View file

@ -1 +0,0 @@

View file

@ -1,45 +0,0 @@
use crate::Error;
use getset::Getters;
use reqwest::Client;
use serde::Deserialize;
#[derive(Deserialize, Getters)]
#[getset(get = "pub(crate)")]
pub(crate) struct Bot {
username: String,
list_type: ListType,
list: Vec<String>,
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<Vec<Bot>, Error> {
let url = "http://localhost:6969/list";
let response: Vec<Bot> = client
.get(url)
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(response)
}

View file

@ -1,3 +0,0 @@
pub(crate) mod account_links;
pub(crate) mod helpstart_api;
pub(crate) mod mojang;

View file

@ -1,67 +0,0 @@
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::<Response>()
.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<String, Error> {
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/{}");

View file

@ -1,5 +1,5 @@
use poise::{CreateReply, FrameworkError};
use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fmt::{Display, Formatter, Result as FmtResult};
use poise::{CreateReply, FrameworkError};
use crate::Data; use crate::Data;
@ -13,9 +13,9 @@ macro_rules! reply_fail_handler {
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Sqlx(sqlx::Error), SqlxError(sqlx::Error),
Api(reqwest::Error), ApiError(reqwest::Error),
Serenity(serenity::Error), SerenityError(serenity::Error),
OnCooldown(std::time::Duration), OnCooldown(std::time::Duration),
Other(String), Other(String),
} }
@ -23,14 +23,10 @@ pub enum Error {
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), Error::SqlxError(e) => write!(f, "SQLx Error: {}", e),
Error::Api(e) => write!(f, "HTTPS Error:\n{}", e), Error::ApiError(e) => write!(f, "HTTPS Error (Hypixel / Mojang API):\n{}", e),
Error::Serenity(e) => write!(f, "Discord Error:\n {}", e), Error::SerenityError(e) => write!(f, "Discord Error:\n {}", e),
Error::OnCooldown(d) => write!( Error::OnCooldown(d) => write!(f, "This command is on cooldown. {}s remaining.", d.as_secs()),
f,
"This command is on cooldown. {}s remaining.",
d.as_secs()
),
Error::Other(s) => write!(f, "{}", s), Error::Other(s) => write!(f, "{}", s),
} }
} }
@ -38,39 +34,33 @@ impl Display for Error {
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) Error::SqlxError(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) Error::ApiError(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) Error::SerenityError(error)
} }
} }
pub(crate) async fn handle_error(error: FrameworkError<'_, Data, Error>) { pub(crate) async fn handle_error<'a>(error: FrameworkError<'a, Data, Error>) {
match error { match error {
FrameworkError::Command { error, ctx, .. } => { FrameworkError::Command { error, ctx, .. } => {
reply_fail_handler!(ctx.send( reply_fail_handler!(ctx.send(CreateReply::default().content(error.to_string()).ephemeral(true)))
CreateReply::default() },
.content(error.to_string()) FrameworkError::CommandStructureMismatch { description, ctx, .. } => {
.ephemeral(true)
))
}
FrameworkError::CommandStructureMismatch {
description, ctx, ..
} => {
reply_fail_handler!(ctx.send( reply_fail_handler!(ctx.send(
CreateReply::default() CreateReply::default()
.content(format!( .content(format!(
"# Command arguments did not match. The command probably has been updated \ "# Command arguments did not match. The command probably has been updated recently. Try reloading Discord. \
recently. Try reloading Discord. Description:\n{}", Description:\n{}",
description description
)) ))
.ephemeral(true) .ephemeral(true)

View file

@ -1,4 +1,5 @@
use serenity::all::ButtonStyle::Success; use serenity::all::ButtonStyle::Success;
use serenity::all::ComponentInteractionDataKind;
use serenity::all::Context; use serenity::all::Context;
use serenity::all::CreateActionRow; use serenity::all::CreateActionRow;
use serenity::all::CreateButton; use serenity::all::CreateButton;
@ -11,16 +12,11 @@ use serenity::all::Interaction;
use serenity::all::ReactionType; use serenity::all::ReactionType;
use serenity::all::RoleId; use serenity::all::RoleId;
use serenity::all::{ButtonStyle, ComponentInteraction}; use serenity::all::{ButtonStyle, ComponentInteraction};
use serenity::all::{ComponentInteractionDataKind, CreateInteractionResponse};
use crate::error::Error; use crate::error::Error;
use crate::Data; use crate::Data;
pub(crate) async fn component( pub(crate) async fn component(ctx: &Context, interaction: &Interaction, data: &Data) -> Result<(), Error> {
ctx: &Context,
interaction: &Interaction,
data: &Data,
) -> Result<(), Error> {
let component = interaction.clone().message_component().unwrap(); let component = interaction.clone().message_component().unwrap();
match component.data.kind { match component.data.kind {
ComponentInteractionDataKind::Button => button(ctx, component, data).await, ComponentInteractionDataKind::Button => button(ctx, component, data).await,
@ -28,33 +24,24 @@ pub(crate) async fn component(
} }
} }
async fn button( async fn button(ctx: &Context, mut interaction: ComponentInteraction, data: &Data) -> Result<(), Error> {
ctx: &Context, let m = &interaction.message;
mut interaction: ComponentInteraction, let u = m.mentions.first().expect("Message did not mention a user.");
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() { match interaction.data.custom_id.as_str() {
"accept_verification" => { "accept_verification" => {
let member = interaction let member = m
.message
.guild_id .guild_id
.unwrap_or(GuildId::new(1256217633959841853_u64)) .unwrap_or(GuildId::new(1256217633959841853_u64))
.member(ctx, u) .member(ctx, u.id)
.await?; .await?;
let (_, _, _dm, _) = futures::try_join!( member.add_role(ctx, RoleId::new(1256218805911425066_u64)).await?;
member.add_role(ctx, RoleId::new(1256218805911425066_u64)), member.remove_role(ctx, RoleId::new(1256253358701023232_u64)).await?;
member.remove_role(ctx, RoleId::new(1256253358701023232_u64)), let _dm = u
u.direct_message( .direct_message(ctx, CreateMessage::new().content("Your verified minecraft account was approved."))
ctx, .await?;
CreateMessage::new().content("Your verified minecraft account was approved.") interaction
), .message
interaction.message.edit( .edit(
ctx, ctx,
EditMessage::new().components(vec![CreateActionRow::Buttons(vec![ EditMessage::new().components(vec![CreateActionRow::Buttons(vec![
CreateButton::new("accept_verification") CreateButton::new("accept_verification")
@ -70,19 +57,16 @@ async fn button(
.style(ButtonStyle::Primary), .style(ButtonStyle::Primary),
])]), ])]),
) )
)?;
interaction
.create_response(ctx, CreateInteractionResponse::Acknowledge)
.await?; .await?;
Ok(()) Ok(())
} }
"deny_verification" => { "deny_verification" => {
let (_dm, _) = futures::try_join!( let _dm = u
u.direct_message( .direct_message(ctx, CreateMessage::new().content("Your verified minecraft account was denied."))
ctx, .await?;
CreateMessage::new().content("Your verified minecraft account was denied.") interaction
), .message
interaction.message.edit( .edit(
ctx, ctx,
EditMessage::new().components(vec![CreateActionRow::Buttons(vec![ EditMessage::new().components(vec![CreateActionRow::Buttons(vec![
CreateButton::new("accept_verification") CreateButton::new("accept_verification")
@ -98,30 +82,15 @@ async fn button(
.style(ButtonStyle::Primary), .style(ButtonStyle::Primary),
])]), ])]),
) )
)?;
interaction
.create_response(ctx, CreateInteractionResponse::Acknowledge)
.await?; .await?;
Ok(()) Ok(())
} }
"list_accounts" => { "list_accounts" => {
let user = interaction.message.mentions.first().unwrap(); let user = interaction.message.mentions.first().unwrap();
let s: String = crate::commands::accountv2::list_string( let s: String = crate::commands::accountv2::list_string(&data.sqlite_pool, user).await?;
&data.sqlite_pool,
user,
&data.caches,
&data.clients.general,
)
.await?;
interaction interaction
.create_response( .create_response(ctx, Message(CreateInteractionResponseMessage::new().content(s).ephemeral(true)))
ctx,
Message(
CreateInteractionResponseMessage::new()
.content(s)
.ephemeral(true),
),
)
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -6,10 +6,7 @@ use crate::error::Error;
pub(crate) async fn on_create(ctx: &Context, thread: &GuildChannel) -> Result<(), Error> { pub(crate) async fn on_create(ctx: &Context, thread: &GuildChannel) -> Result<(), Error> {
match thread.parent_id.map(|parent| parent.get()) { match thread.parent_id.map(|parent| parent.get()) {
Some(1295108216388325386) => { Some(1295108216388325386) => {
thread thread.id.edit_thread(ctx, EditThread::new().rate_limit_per_user(60_u16)).await?;
.id
.edit_thread(ctx, EditThread::new().rate_limit_per_user(60_u16))
.await?;
Ok(()) Ok(())
} }
Some(_) => Ok(()), Some(_) => Ok(()),

View file

@ -1,92 +1,44 @@
#![feature(integer_sign_cast)] #![feature(integer_sign_cast)]
#![feature(duration_constructors)]
use std::collections::HashSet; use std::collections::HashSet;
use std::convert::Into; use std::convert::Into;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use dashmap::DashMap;
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
use serenity::{FullEvent, model::id::UserId};
use serenity::all::{ActivityData, InteractionType, RoleId}; use serenity::all::{ActivityData, InteractionType, RoleId};
use serenity::prelude::GatewayIntents; use serenity::prelude::GatewayIntents;
use serenity::{model::id::UserId, FullEvent};
use sqlx::Sqlite; use sqlx::Sqlite;
use tokio::sync::RwLock;
use error::Error; use error::Error;
mod commands; mod commands;
mod data;
mod error; mod error;
mod handlers; mod handlers;
struct Caches {
name: DashMap<String, (String, std::time::Instant)>,
uuid: DashMap<String, (String, std::time::Instant)>,
}
impl Default for Caches {
fn default() -> Self {
Self {
name: DashMap::new(),
uuid: DashMap::new(),
}
}
}
struct ApiClients {
hypixel_api_client: reqwest::Client,
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 { struct Data {
bots: Arc<RwLock<u8>>,
sqlite_pool: sqlx::Pool<Sqlite>, sqlite_pool: sqlx::Pool<Sqlite>,
clients: ApiClients, hypixel_api_client: reqwest::Client,
caches: Caches, } // User data, which is stored and accessible in all command invocations
}
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>; type Context<'a> = poise::Context<'a, Data, Error>;
#[tokio::main] #[tokio::main]
async fn 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 { let options = poise::FrameworkOptions {
commands: vec![ commands: vec![
commands::lfg::lfg(), commands::lfg::lfg(),
@ -94,6 +46,7 @@ async fn main() {
commands::lfg::other(), commands::lfg::other(),
commands::xd::xd(), commands::xd::xd(),
commands::helpstart::helpstart(), commands::helpstart::helpstart(),
commands::bots::bots(),
commands::accountv2::account(), commands::accountv2::account(),
], ],
manual_cooldowns: true, manual_cooldowns: true,
@ -106,15 +59,8 @@ async fn main() {
error::handle_error(error).await; error::handle_error(error).await;
}) })
}, },
owners: { owners: { HashSet::from([UserId::new(449579075531440128_u64), UserId::new(659112817508745216_u64)]) },
HashSet::from([ event_handler: |ctx, event, framework, data| Box::pin(event_handler(ctx, event, framework, data)),
UserId::new(449579075531440128_u64),
UserId::new(659112817508745216_u64),
])
},
event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data))
},
..Default::default() ..Default::default()
}; };
@ -123,15 +69,17 @@ async fn main() {
.setup(move |ctx, _ready, framework| { .setup(move |ctx, _ready, framework| {
Box::pin(async move { Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?; poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data::default()) Ok(Data {
bots: Arc::new(RwLock::new(0)),
sqlite_pool,
hypixel_api_client,
})
}) })
}) })
.build(); .build();
let token = std::env::var("DISCORD_TOKEN").unwrap(); let token = std::env::var("DISCORD_TOKEN").unwrap();
let intents = GatewayIntents::non_privileged() let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS;
| GatewayIntents::MESSAGE_CONTENT
| GatewayIntents::GUILD_MEMBERS;
let client = serenity::ClientBuilder::new(token, intents) let client = serenity::ClientBuilder::new(token, intents)
.framework(framework) .framework(framework)
.activity(ActivityData::custom("NPC moment...")) .activity(ActivityData::custom("NPC moment..."))
@ -148,27 +96,23 @@ async fn event_handler(
match event { match event {
FullEvent::Ready { data_about_bot, .. } => { FullEvent::Ready { data_about_bot, .. } => {
println!("Logged in as '{}'!", data_about_bot.user.name); println!("Logged in as '{}'!", data_about_bot.user.name);
} },
FullEvent::GuildMemberAddition { new_member } => { FullEvent::GuildMemberAddition { new_member } => {
if new_member.guild_id.get() == 1256217633959841853_u64 { if new_member.guild_id.get() == 1256217633959841853_u64 {
new_member new_member.add_role(ctx, RoleId::new(1256253358701023232_u64)).await?;
.add_role(ctx, RoleId::new(1256253358701023232_u64))
.await?;
} }
} }
FullEvent::InteractionCreate { interaction } => { FullEvent::InteractionCreate { interaction } => {
if interaction.application_id().get() == 1165594074473037824 if interaction.application_id().get() == 1165594074473037824 && interaction.kind() == InteractionType::Component {
&& interaction.kind() == InteractionType::Component
{
handlers::bot_interaction::component(ctx, interaction, data).await?; handlers::bot_interaction::component(ctx, interaction, data).await?;
} }
} },
FullEvent::Message { new_message } => { FullEvent::Message { new_message } => {
handlers::message::on_create(ctx, new_message).await?; handlers::message::on_create(ctx, new_message).await?;
} },
FullEvent::ThreadCreate { thread } => { FullEvent::ThreadCreate { thread } => {
handlers::thread::on_create(ctx, thread).await?; handlers::thread::on_create(ctx, thread).await?;
} },
_ => {} _ => {}
} }
Ok(()) Ok(())