diff options
-rw-r--r-- | Cargo.lock | 56 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/brain.rs | 125 | ||||
-rw-r--r-- | src/import.rs | 54 | ||||
-rw-r--r-- | src/import/mod.rs | 125 | ||||
-rw-r--r-- | src/main.rs | 130 |
6 files changed, 351 insertions, 144 deletions
diff --git a/Cargo.lock b/Cargo.lock index 9d048cd..1ea6df6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -488,6 +488,7 @@ dependencies = [ "twilight-http", "twilight-model", "twilight-util", + "ureq", ] [[package]] @@ -726,6 +727,7 @@ version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -747,6 +749,15 @@ dependencies = [ ] [[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1256,6 +1267,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] +name = "ureq" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1296,6 +1343,15 @@ dependencies = [ ] [[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 9ff4b6a..149bcd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,8 @@ twilight-gateway = "0.16.0" twilight-http = "0.16.0" twilight-model = "0.16.0" twilight-util = { version = "0.16.0", features = ["builder"] } +ureq = { version = "3.0.11", optional = true } + +[features] +default = ["emboard-direct"] +emboard-direct = ["ureq"] diff --git a/src/brain.rs b/src/brain.rs new file mode 100644 index 0000000..0ec6a59 --- /dev/null +++ b/src/brain.rs @@ -0,0 +1,125 @@ +use crate::database::Database; + +use twilight_cache_inmemory::DefaultInMemoryCache; +use twilight_gateway::Event; +use twilight_http::{Client as HttpClient, client::InteractionClient}; +use twilight_model::{ + channel::message::Embed, + gateway::payload::incoming::InteractionCreate, + http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}, + id::{ + Id, + marker::{ChannelMarker, GuildMarker, MessageMarker, UserMarker}, + }, +}; +const PROD_APP_ID: u64 = 1363055126264283136; +const DEV_APP_ID: u64 = 1363494986699771984; +const APP_ID: u64 = PROD_APP_ID; + +// A Absolutely Remarkably Terrible Thing +macro_rules! aumau { + ($meow:expr) => { + $meow.await.unwrap().model().await.unwrap() + }; +} + +pub struct Brain { + pub db: Database, + http: HttpClient, + cache: DefaultInMemoryCache, +} + +impl Brain { + pub fn new(db: Database, http: HttpClient, cache: DefaultInMemoryCache) -> Self { + Self { db, http, cache } + } + + pub fn update_cache(&self, event: &Event) { + self.cache.update(event); + } + + /// Get the interaction client. Used for responding to interactions + pub fn interaction(&self) -> InteractionClient<'_> { + self.http.interaction(Id::new(APP_ID)) + } + + /// Respond to the interaction indicated by `create`. This method will + /// respond as an `InteractionResponseType::ChannelMessageWithSource` + pub async fn interaction_respond( + &self, + create: &InteractionCreate, + data: InteractionResponseData, + ) { + self.interaction() + .create_response( + create.id, + &create.token, + &InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(data), + }, + ) + .await + .unwrap(); + } + + pub async fn send_message_with_embed<S: AsRef<str>>( + &self, + channel: Id<ChannelMarker>, + msg: S, + embed: Embed, + ) -> Id<MessageMarker> { + aumau!( + self.http + .create_message(channel) + .embeds(&[embed]) + .content(msg.as_ref()) + ) + .id + } + + /// Retrieve information about a member of a guild. This method checks the + /// cache first, and then makes a request if it isn't there + pub async fn guild_member(&self, guild_id: Id<GuildMarker>, user_id: Id<UserMarker>) -> Member { + let user = self.cache.user(user_id); + let member = self.cache.member(guild_id, user_id); + + match user.zip(member) { + Some((user, member)) => Member { + id: user_id, + handle: user.name.clone(), + global_name: user.global_name.clone(), + nick: member.nick().map(<_>::to_owned), + }, + None => { + let member = aumau!(self.http.guild_member(guild_id, user_id)); + + Member { + id: user_id, + handle: member.user.name, + global_name: member.user.global_name, + nick: member.nick, + } + } + } + } + + /// Check the database for a leaderboard belonging to this guild. If it + /// does not exist, create one. Returns whether or not one existed before + /// this function was called + pub fn create_leaderboard_if_not_exists(&self, guild: Id<GuildMarker>) -> bool { + if !self.db.leaderboard_exits(guild.get()) { + self.db.create_leaderboard(guild.get()).unwrap(); + false + } else { + true + } + } +} + +pub struct Member { + pub id: Id<UserMarker>, + pub handle: String, + pub global_name: Option<String>, + pub nick: Option<String>, +} diff --git a/src/import.rs b/src/import.rs deleted file mode 100644 index f7d9873..0000000 --- a/src/import.rs +++ /dev/null @@ -1,54 +0,0 @@ -use serde::Deserialize; - -use crate::database::{BoardRow, Database, Error}; - -#[derive(Debug, Deserialize)] -pub struct Emboard { - guildName: String, - leaderboard: Vec<EmboardRow>, -} - -#[derive(Debug, Deserialize)] -pub struct EmboardRow { - guildId: String, - discordId: String, - points: String, - username: String, -} - -pub fn import(db: &Database, json: String) { - let embaord: Emboard = match serde_json::from_str(&json) { - Ok(e) => e, - Err(e) => { - panic!("{e}"); - } - }; - - let Some(first) = embaord.leaderboard.first() else { - return; - }; - - let guild_id = u64::from_str_radix(&first.guildId, 10).unwrap(); - if db.get_leaderboard(guild_id).is_err() { - db.create_leaderboard(guild_id).unwrap(); - } - - for user in embaord.leaderboard { - let user_id = u64::from_str_radix(&user.discordId, 10).unwrap(); - let points = i64::from_str_radix(&user.points, 10).unwrap(); - - let res = db.give_user_points(guild_id, user_id, points); - if let Err(Error::UserNotExist) = res { - db.add_user_to_leaderboard( - guild_id, - BoardRow { - user_id, - user_handle: user.username, - user_nickname: None, - points, - }, - ) - .unwrap(); - } - } -} diff --git a/src/import/mod.rs b/src/import/mod.rs new file mode 100644 index 0000000..02429d8 --- /dev/null +++ b/src/import/mod.rs @@ -0,0 +1,125 @@ +use serde::Deserialize; + +use crate::database::{BoardRow, Database, Error}; + +#[derive(Debug, Deserialize)] +pub struct Emboard { + guildName: String, + leaderboard: Vec<EmboardRow>, +} + +#[derive(Debug, Deserialize)] +pub struct EmboardRow { + guildId: String, + discordId: String, + points: String, + username: String, +} + +pub fn import(db: &Database, json: String) { + let embaord: Emboard = match serde_json::from_str(&json) { + Ok(e) => e, + Err(e) => { + panic!("{e}"); + } + }; + + let Some(first) = embaord.leaderboard.first() else { + return; + }; + + let guild_id = u64::from_str_radix(&first.guildId, 10).unwrap(); + if db.get_leaderboard(guild_id).is_err() { + db.create_leaderboard(guild_id).unwrap(); + } + + for user in embaord.leaderboard { + let user_id = u64::from_str_radix(&user.discordId, 10).unwrap(); + let points = i64::from_str_radix(&user.points, 10).unwrap(); + + let res = db.give_user_points(guild_id, user_id, points); + if let Err(Error::UserNotExist) = res { + db.add_user_to_leaderboard( + guild_id, + BoardRow { + user_id, + user_handle: user.username, + user_nickname: None, + points, + }, + ) + .unwrap(); + } + } +} + +#[cfg(feature = "emboard-direct")] +pub mod emboard_direct { + use std::{sync::Arc, time::Duration}; + + use twilight_model::{ + gateway::payload::incoming::InteractionCreate, + http::interaction::{InteractionResponse, InteractionResponseType}, + id::{Id, marker::GuildMarker}, + }; + use twilight_util::builder::InteractionResponseDataBuilder; + use ureq::{Agent, RequestBuilder, config::Config, typestate::WithoutBody}; + + use crate::brain::Brain; + + const DATA_URL: &str = + "https://emboard.evolvedmesh.com/api/backend/dashboard/emboard_guild/leaderboard"; + + pub async fn import(brain: Arc<Brain>, guild_id: Id<GuildMarker>, create: &InteractionCreate) { + let member = create.member.clone().unwrap(); + let permissions = member.permissions.unwrap(); + if !permissions.contains(twilight_model::guild::Permissions::MANAGE_GUILD) { + brain + .interaction_respond( + create, + InteractionResponseDataBuilder::new() + .content( + "❌ You must have the Manage Server permission to perform this action", + ) + .build(), + ) + .await; + + return; + } + + let url = format!("{DATA_URL}/{guild_id}"); + let config = Config::builder() + .user_agent("leaberblord +gen@nyble.dev") + .build(); + + let data = InteractionResponseDataBuilder::new() + .content( + "⏳ waiting on emboard for the leaderboard. we'll display the leaderboard when it's imported", + ) + .build(); + + brain.interaction_respond(create, data).await; + + match config.new_agent().get(url).call() { + Err(_e) => todo!(), + Ok(mut resp) => { + let body: String = resp.body_mut().read_to_string().unwrap(); + super::import(&brain.db, body); + + let embed = crate::get_leaderboard(brain.clone(), guild_id) + .embeds + .unwrap()[0] + .clone(); + + brain + .send_message_with_embed( + create.channel.as_ref().unwrap().id, + "✅ imported leaderboard from emboard", + embed, + ) + .await; + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 13bac74..774d9e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use std::{env, error::Error, path::PathBuf, sync::Arc}; +use brain::Brain; use confindent::Confindent; use database::{BoardRow, Database, Error as DbError, PermissionSetting}; use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType}; @@ -26,12 +27,13 @@ use twilight_util::builder::{ embed::EmbedBuilder, }; +mod brain; mod database; mod import; const PROD_APP_ID: u64 = 1363055126264283136; const DEV_APP_ID: u64 = 1363494986699771984; -const APP_ID: u64 = PROD_APP_ID; +const APP_ID: u64 = DEV_APP_ID; macro_rules! bail { ($msg:expr) => { @@ -57,91 +59,6 @@ macro_rules! success { }; } -// A Absolutely Remarkably Terrible Thing -macro_rules! aumau { - ($meow:expr) => { - $meow.await.unwrap().model().await.unwrap() - }; -} - -struct Brain { - db: Database, - http: HttpClient, - cache: DefaultInMemoryCache, -} - -impl Brain { - /// Get the interaction client. Used for responding to interactions - pub fn interaction(&self) -> InteractionClient<'_> { - self.http.interaction(Id::new(APP_ID)) - } - - /// Respond to the interaction indicated by `create`. This method will - /// respond as an `InteractionResponseType::ChannelMessageWithSource` - pub async fn interaction_respond( - &self, - create: &InteractionCreate, - data: InteractionResponseData, - ) { - self.interaction() - .create_response( - create.id, - &create.token, - &InteractionResponse { - kind: InteractionResponseType::ChannelMessageWithSource, - data: Some(data), - }, - ) - .await - .unwrap(); - } - - /// Retrieve information about a member of a guild. This method checks the - /// cache first, and then makes a request if it isn't there - pub async fn guild_member(&self, guild_id: Id<GuildMarker>, user_id: Id<UserMarker>) -> Member { - let user = self.cache.user(user_id); - let member = self.cache.member(guild_id, user_id); - - match user.zip(member) { - Some((user, member)) => Member { - id: user_id, - handle: user.name.clone(), - global_name: user.global_name.clone(), - nick: member.nick().map(<_>::to_owned), - }, - None => { - let member = aumau!(self.http.guild_member(guild_id, user_id)); - - Member { - id: user_id, - handle: member.user.name, - global_name: member.user.global_name, - nick: member.nick, - } - } - } - } - - /// Check the database for a leaderboard belonging to this guild. If it - /// does not exist, create one. Returns whether or not one existed before - /// this function was called - pub fn create_leaderboard_if_not_exists(&self, guild: Id<GuildMarker>) -> bool { - if !self.db.leaderboard_exits(guild.get()) { - self.db.create_leaderboard(guild.get()).unwrap(); - false - } else { - true - } - } -} - -struct Member { - id: Id<UserMarker>, - handle: String, - global_name: Option<String>, - nick: Option<String>, -} - #[tokio::main] async fn main() -> anyhow::Result<()> { //dotenv::dotenv().ok(); @@ -212,7 +129,7 @@ async fn main() -> anyhow::Result<()> { ); } - let brain = Arc::new(Brain { db, http, cache }); + let brain = Arc::new(Brain::new(db, http, cache)); while let Some(item) = shard.next_event(EventTypeFlags::all()).await { let Ok(event) = item else { @@ -220,7 +137,7 @@ async fn main() -> anyhow::Result<()> { continue; }; - brain.cache.update(&event); + brain.update_cache(&event); tokio::spawn(handle_event(event, Arc::clone(&brain))); } @@ -249,6 +166,7 @@ async fn handle_event(event: Event, brain: Arc<Brain>) -> Result<(), Box<dyn Err "leaderboard" => Commands::Leaderboard, "points" => Commands::Points, "permission" => Commands::Permission, + "import" => Commands::Import, _ => panic!("'{}' is not a command", cmd.name), }; @@ -296,7 +214,14 @@ async fn commands() -> Vec<Command> { )) .build(); - vec![leaderboard, points, permission] + let import = CommandBuilder::new( + "import", + "import this server's emboard leaderboard by adding the points in emboard, to leaberblord's", + CommandType::ChatInput, + ) + .build(); + + vec![leaderboard, points, permission, import] } async fn command_handler( @@ -311,6 +236,10 @@ async fn command_handler( Commands::Leaderboard => get_leaderboard(brain.clone(), guild), Commands::Points => add_points(brain.clone(), guild, create, command_data).await, Commands::Permission => permission(brain.clone(), guild, create, command_data).await, + Commands::Import => { + import_cmd(brain.clone(), guild, create, command_data).await; + return; + } }; // Finally, send back the response @@ -442,7 +371,7 @@ async fn permission( let member = create.member.clone().unwrap(); let permissions = member.permissions.unwrap(); if !permissions.contains(twilight_model::guild::Permissions::MANAGE_GUILD) { - bail!("You must have the Manage Server permission to perform this command"); + bail!("You must have the Manage Server permission to perform this action"); } for opt in &data.options { @@ -499,10 +428,31 @@ fn permission_role( )) } +async fn import_cmd( + brain: Arc<Brain>, + guild: Id<GuildMarker>, + create: &InteractionCreate, + data: &CommandData, +) { + let member = create.member.clone().unwrap(); + let permissions = member.permissions.unwrap(); + if !permissions.contains(twilight_model::guild::Permissions::MANAGE_GUILD) { + brain + .interaction_respond( + create, + fail!("You must have the Manage Server permission to perform this action"), + ) + .await; + } else { + import::emboard_direct::import(brain, guild, create).await; + } +} + enum Commands { Leaderboard, Points, Permission, + Import, } fn extract_mentions(mut raw: &str) -> Vec<Id<UserMarker>> { |