about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2025-04-20 08:07:08 -0500
committergennyble <gen@nyble.dev>2025-04-20 08:07:08 -0500
commit668484dcc06c5ad36aa840609dab383fa17aac88 (patch)
treedaa41e6eec816414cbfc78b82f01fbf010593f08 /src
parent2d667d4c3d63a44c4cc01f7fb7133e5714d60584 (diff)
downloadleaberblord-668484dcc06c5ad36aa840609dab383fa17aac88.tar.gz
leaberblord-668484dcc06c5ad36aa840609dab383fa17aac88.zip
code cleanup
Diffstat (limited to 'src')
-rw-r--r--src/database.rs6
-rw-r--r--src/main.rs225
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!(