diff options
-rw-r--r-- | src/database.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 225 |
2 files changed, 143 insertions, 88 deletions
diff --git a/src/database.rs b/src/database.rs index 218a602..0e799d3 100644 --- a/src/database.rs +++ b/src/database.rs @@ -51,7 +51,7 @@ impl Database { Ok(()) } - pub fn leaderboard_id(&self, guild_id: u64) -> Result<LeaderboardTable, Error> { + fn leaderboard_id(&self, guild_id: u64) -> Result<LeaderboardTable, Error> { let conn = self.conn.lock().unwrap(); let leaderboard_id = conn @@ -65,6 +65,10 @@ impl Database { leaderboard_id } + pub fn leaderboard_exits(&self, guild_id: u64) -> bool { + self.leaderboard_id(guild_id).is_err() + } + pub fn get_leaderboard(&self, guild_id: u64) -> Result<Vec<BoardRow>, Error> { // Don't deadlock! let lb = self.leaderboard_id(guild_id)?; diff --git a/src/main.rs b/src/main.rs index af238dc..13bac74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,30 +10,28 @@ use twilight_model::{ command::{Command, CommandType}, interaction::{ InteractionData, - application_command::{CommandData, CommandDataOption, CommandOptionValue}, + application_command::{CommandData, CommandOptionValue}, }, }, - channel::message::MessageFlags, gateway::payload::incoming::InteractionCreate, http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}, id::{ Id, - marker::{GuildMarker, InteractionMarker, UserMarker}, + marker::{GuildMarker, UserMarker}, }, }; use twilight_util::builder::{ InteractionResponseDataBuilder, - command::{ - CommandBuilder, IntegerBuilder, MentionableBuilder, NumberBuilder, RoleBuilder, - StringBuilder, SubCommandBuilder, SubCommandGroupBuilder, UserBuilder, - }, - embed::{EmbedBuilder, EmbedFieldBuilder}, + command::{CommandBuilder, IntegerBuilder, RoleBuilder, StringBuilder, SubCommandBuilder}, + embed::EmbedBuilder, }; mod database; mod import; -const APP_ID: u64 = 1363055126264283136; +const PROD_APP_ID: u64 = 1363055126264283136; +const DEV_APP_ID: u64 = 1363494986699771984; +const APP_ID: u64 = PROD_APP_ID; macro_rules! bail { ($msg:expr) => { @@ -59,9 +57,94 @@ 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(); + //dotenv::dotenv().ok(); let conf_path = if !PathBuf::from("/etc/leaberblord.conf").exists() { "leaberblord.conf" @@ -77,7 +160,7 @@ async fn main() -> anyhow::Result<()> { .ok() .unwrap_or_else(|| conf.child_owned("DiscordToken").unwrap()); - let db = Arc::new(Database::new(db_dir)); + let db = Database::new(db_dir); db.create_tables(); let arg = std::env::args().nth(1); @@ -97,8 +180,9 @@ async fn main() -> anyhow::Result<()> { } let mut shard = Shard::new(ShardId::ONE, token.clone(), Intents::GUILD_MESSAGES); - let http = Arc::new(HttpClient::new(token)); - let mut cache = DefaultInMemoryCache::builder() + let http = HttpClient::new(token); + let cache = DefaultInMemoryCache::builder() + .resource_types(ResourceType::GUILD) .resource_types(ResourceType::MESSAGE) .resource_types(ResourceType::MEMBER) .build(); @@ -128,41 +212,30 @@ async fn main() -> anyhow::Result<()> { ); } + let brain = Arc::new(Brain { db, http, cache }); + while let Some(item) = shard.next_event(EventTypeFlags::all()).await { let Ok(event) = item else { eprintln!("error receiving event"); continue; }; - cache.update(&event); + brain.cache.update(&event); - tokio::spawn(handle_event(event, Arc::clone(&http), Arc::clone(&db))); + tokio::spawn(handle_event(event, Arc::clone(&brain))); } Ok(()) } -async fn handle_event( - event: Event, - http: Arc<HttpClient>, - db: Arc<Database>, -) -> Result<(), Box<dyn Error + Send + Sync>> { +async fn handle_event(event: Event, brain: Arc<Brain>) -> Result<(), Box<dyn Error + Send + Sync>> { match event { Event::InteractionCreate(create) => { // Don't allow direct message commanding if create.is_dm() { - let iclient = http.interaction(Id::new(APP_ID)); - iclient - .create_response( - create.id, - &create.token, - &InteractionResponse { - kind: InteractionResponseType::ChannelMessageWithSource, - data: Some(fail!("leaderboards not supported in DMs")), - }, - ) - .await - .unwrap(); + brain + .interaction_respond(&create, fail!("leaderboards not supported in DMs")) + .await; return Ok(()); } @@ -179,7 +252,7 @@ async fn handle_event( _ => panic!("'{}' is not a command", cmd.name), }; - command_handler(&db, http, guild, &create, command, cmd).await; + command_handler(brain, guild, &create, command, cmd).await; } } Event::GatewayClose(close) => { @@ -227,8 +300,7 @@ async fn commands() -> Vec<Command> { } async fn command_handler( - db: &Database, - http: Arc<HttpClient>, + brain: Arc<Brain>, guild: Id<GuildMarker>, create: &InteractionCreate, command: Commands, @@ -236,27 +308,17 @@ async fn command_handler( ) { // Handle the command and create our interaction response data let data = match command { - Commands::Leaderboard => get_leaderboard(&db, guild), - Commands::Points => add_points(&db, &http, guild, create, command_data).await, - Commands::Permission => permission(&db, &http, guild, create, command_data).await, + 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, }; // Finally, send back the response - http.interaction(Id::new(APP_ID)) - .create_response( - create.id, - &create.token, - &InteractionResponse { - kind: InteractionResponseType::ChannelMessageWithSource, - data: Some(data), - }, - ) - .await - .unwrap(); + brain.interaction_respond(create, data).await; } -fn get_leaderboard(db: &Database, guild: Id<GuildMarker>) -> InteractionResponseData { - let board = db.get_leaderboard(guild.get()); +fn get_leaderboard(brain: Arc<Brain>, guild: Id<GuildMarker>) -> InteractionResponseData { + let board = brain.db.get_leaderboard(guild.get()); match board { Err(DbError::TableNotExist) => { @@ -272,7 +334,6 @@ fn get_leaderboard(db: &Database, guild: Id<GuildMarker>) -> InteractionResponse .join("\n"); let embed = EmbedBuilder::new().description(str).build(); - InteractionResponseDataBuilder::new() .embeds([embed]) .build() @@ -280,16 +341,8 @@ fn get_leaderboard(db: &Database, guild: Id<GuildMarker>) -> InteractionResponse } } -// A Absolutely Remarkably Terrible Thing -macro_rules! aumau { - ($meow:expr) => { - $meow.await.unwrap().model().await.unwrap() - }; -} - async fn add_points( - db: &Database, - http: &HttpClient, + brain: Arc<Brain>, guild: Id<GuildMarker>, create: &InteractionCreate, data: &CommandData, @@ -328,12 +381,9 @@ async fn add_points( Some(_) | None => unreachable!(), }; - // Make sure we have a leaderboard to work on. - if db.get_leaderboard(guild.get()).is_err() { - db.create_leaderboard(guild.get()).unwrap(); - } + brain.create_leaderboard_if_not_exists(guild); - let settings = db.get_leaderboard_settings(guild.get()).unwrap(); + let settings = brain.db.get_leaderboard_settings(guild.get()).unwrap(); if let PermissionSetting::RoleRequired = settings.permission { if let Some(role) = settings.role { let member = create.member.clone().unwrap(); @@ -355,16 +405,16 @@ async fn add_points( } for user in &users { - match db.give_user_points(guild.get(), user.get(), points) { + match brain.db.give_user_points(guild.get(), user.get(), points) { Err(DbError::UserNotExist) => { - let member = aumau!(http.guild_member(guild, *user)); + let member = brain.guild_member(guild, *user).await; let row = BoardRow { user_id: user.get(), - user_handle: member.user.name, - user_nickname: member.nick.or(member.user.global_name), + user_handle: member.handle, + user_nickname: member.nick.or(member.global_name), points, }; - db.add_user_to_leaderboard(guild.get(), row).unwrap(); + brain.db.add_user_to_leaderboard(guild.get(), row).unwrap(); } _ => (), } @@ -384,8 +434,7 @@ async fn add_points( } async fn permission( - db: &Database, - http: &HttpClient, + brain: Arc<Brain>, guild: Id<GuildMarker>, create: &InteractionCreate, data: &CommandData, @@ -398,8 +447,8 @@ async fn permission( for opt in &data.options { return match opt.name.as_str() { - "none" => permission_none(db, guild), - "role" => permission_role(db, guild, opt.value.clone()), + "none" => permission_none(brain, guild), + "role" => permission_role(brain, guild, opt.value.clone()), _ => panic!(), }; } @@ -407,11 +456,12 @@ async fn permission( fail!("this should be unreachable. report the bug maybe? to gennyble on discord") } -fn permission_none(db: &Database, guild: Id<GuildMarker>) -> InteractionResponseData { - if db.get_leaderboard(guild.get()).is_err() { - db.create_leaderboard(guild.get()).unwrap(); - } else { - db.set_leaderboard_permission(guild.get(), PermissionSetting::None) +fn permission_none(brain: Arc<Brain>, guild: Id<GuildMarker>) -> InteractionResponseData { + if brain.create_leaderboard_if_not_exists(guild) { + // If it already existed, we might not be on default none. So set it. + brain + .db + .set_leaderboard_permission(guild.get(), PermissionSetting::None) .unwrap(); } @@ -419,7 +469,7 @@ fn permission_none(db: &Database, guild: Id<GuildMarker>) -> InteractionResponse } fn permission_role( - db: &Database, + brain: Arc<Brain>, guild: Id<GuildMarker>, data: CommandOptionValue, ) -> InteractionResponseData { @@ -432,14 +482,15 @@ fn permission_role( panic!() }; - // Make sure we have a leaderboard to work on - if db.get_leaderboard(guild.get()).is_err() { - db.create_leaderboard(guild.get()).unwrap(); - } + brain.create_leaderboard_if_not_exists(guild); - db.set_leaderboard_permission(guild.get(), PermissionSetting::RoleRequired) + brain + .db + .set_leaderboard_permission(guild.get(), PermissionSetting::RoleRequired) .unwrap(); - db.set_leaderboard_permission_role(guild.get(), role.get()) + brain + .db + .set_leaderboard_permission_role(guild.get(), role.get()) .unwrap(); success!(format!( |