about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2025-08-15 17:53:53 -0500
committergennyble <gen@nyble.dev>2025-08-15 17:53:53 -0500
commitdb62e4ab9c54a370e15ad6944963b2ceb8d2638b (patch)
tree24b7d59356a117b1c684a237bf7ecfb6477a50e7
parentd997d8a3d640cf36b0e705db249e1a8865f0b54f (diff)
downloadleaberblord-db62e4ab9c54a370e15ad6944963b2ceb8d2638b.tar.gz
leaberblord-db62e4ab9c54a370e15ad6944963b2ceb8d2638b.zip
refactor add_point and revise
-rw-r--r--TODO12
-rw-r--r--src/command/add_points.rs90
-rw-r--r--src/command/mod.rs138
-rw-r--r--src/command/revise.rs131
4 files changed, 200 insertions, 171 deletions
diff --git a/TODO b/TODO
index 585c4e9..7c01544 100644
--- a/TODO
+++ b/TODO
@@ -6,4 +6,14 @@ Use the cache!
 
 Better logging
 	There's like, an user now, so we should log better so we can find errors
-	more readily.
\ No newline at end of file
+	more readily.
+
+Make Date Parsing Better
+	I think we'll have to write the parser here. Or rip it from awake, really.
+
+	So we can have any of:
+		2025-06-12
+		2025-6-12
+		2025-6-12 4:24
+		2025-6-12 4:24:43
+		2025-6-12 4:24:43am
\ No newline at end of file
diff --git a/src/command/add_points.rs b/src/command/add_points.rs
index 453be2d..97e3981 100644
--- a/src/command/add_points.rs
+++ b/src/command/add_points.rs
@@ -1,21 +1,17 @@
 use std::sync::Arc;
 
 use twilight_model::{
-	application::interaction::application_command::{CommandData, CommandOptionValue},
+	application::interaction::application_command::CommandData,
 	gateway::payload::incoming::InteractionCreate,
 	http::interaction::InteractionResponseData,
-	id::{
-		Id,
-		marker::{GuildMarker, UserMarker},
-	},
+	id::{Id, marker::GuildMarker},
 };
 use twilight_util::builder::{InteractionResponseDataBuilder, embed::EmbedBuilder};
 
 use crate::{
-	bail,
 	brain::Brain,
-	database::{BoardRow, Error as DbError, PermissionSetting},
-	success, util,
+	command::{check_command_permissions, parse_points_command_data},
+	database::{BoardRow, Error as DbError},
 };
 
 pub async fn add_points(
@@ -24,71 +20,28 @@ pub async fn add_points(
 	create: &InteractionCreate,
 	data: &CommandData,
 ) -> InteractionResponseData {
-	let mut points = None;
-	let mut users: Vec<Id<UserMarker>> = vec![];
-
-	for opt in &data.options {
-		match opt.name.as_str() {
-			"points" => match opt.value {
-				CommandOptionValue::Integer(num) => points = Some(num),
-				_ => unreachable!(),
-			},
-			"users" => match &opt.value {
-				CommandOptionValue::String(raw) => {
-					let mentions = util::extract_user_mentions(&raw);
-					users = mentions;
-				}
-				_ => unreachable!(),
-			},
-			_ => unreachable!(),
-		}
-	}
-
-	if users.len() == 0 {
-		bail!("No users mentioned! Who do we add points to?")
-	}
-
-	let (points, points_display, points_verb) = match points {
-		Some(p) if p > 0 => (p, p, "added to"),
-		Some(p) if p < 0 => (p, -p, "removed from"),
-		Some(0) => {
-			return success!("adding 0 points is a no-operation! I won't do anything :)");
-		}
-		Some(_) | None => unreachable!(),
+	let point_data = match parse_points_command_data(data) {
+		Err(e) => return e,
+		Ok(pcd) => pcd,
 	};
 
 	brain.create_leaderboard_if_not_exists(guild);
-
-	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();
-			let found_role = member.roles.iter().find(|vrole| vrole.get() == role);
-
-			if found_role.is_none() {
-				bail!("You do not have the right permissions to change the score");
-			}
-		} else {
-			// Seeing as the role is a required input on the subcommand, this
-			// would only happen with direct database fiddling or like, some
-			// really weird arcane bullshit?
-			bail!(
-				"Permissions set to Role Required, but no role is set. \
-				This shouldn't be able to happen. \
-				Maybe try to set the permissions again?"
-			);
-		}
+	if let Some(err) = check_command_permissions(&brain, guild, create.member.clone().unwrap()) {
+		return err;
 	}
 
-	for user in &users {
-		match brain.db.give_user_points(guild.get(), user.get(), points) {
+	for user in &point_data.users {
+		match brain
+			.db
+			.give_user_points(guild.get(), user.get(), point_data.points)
+		{
 			Err(DbError::UserNotExist) => {
 				let member = brain.guild_member(guild, *user).await;
 				let row = BoardRow {
 					user_id: user.get(),
 					user_handle: member.handle,
 					user_nickname: member.nick.or(member.global_name),
-					points,
+					points: point_data.points,
 				};
 				brain.db.add_user_to_leaderboard(guild.get(), row).unwrap();
 			}
@@ -96,13 +49,14 @@ pub async fn add_points(
 		}
 	}
 
-	let users_string = users
-		.into_iter()
-		.map(|u| format!("<@{u}>"))
-		.collect::<Vec<String>>()
-		.join(", ");
+	let users_string = point_data.user_string();
+	let points_verb = point_data.points_verb;
+
+	let msg = format!(
+		"{} points {points_verb} {users_string}",
+		point_data.points.abs()
+	);
 
-	let msg = format!("{points_display} points {points_verb} {users_string}");
 	let embed = EmbedBuilder::new().description(msg).build();
 	InteractionResponseDataBuilder::new()
 		.embeds([embed])
diff --git a/src/command/mod.rs b/src/command/mod.rs
index 73b292a..c3dc7a9 100644
--- a/src/command/mod.rs
+++ b/src/command/mod.rs
@@ -5,6 +5,7 @@ mod revise;
 pub use add_points::add_points;
 pub use leaderboard::leaderboard;
 pub use revise::revise;
+use time::{Date, PrimitiveDateTime, Time, format_description};
 
 use std::sync::Arc;
 
@@ -14,8 +15,12 @@ use twilight_model::{
 		interaction::application_command::{CommandData, CommandOptionValue},
 	},
 	gateway::payload::incoming::InteractionCreate,
+	guild::PartialMember,
 	http::interaction::InteractionResponseData,
-	id::{Id, marker::GuildMarker},
+	id::{
+		Id,
+		marker::{GuildMarker, UserMarker},
+	},
 };
 use twilight_util::builder::{
 	InteractionResponseDataBuilder,
@@ -23,7 +28,7 @@ use twilight_util::builder::{
 	embed::{EmbedBuilder, EmbedFieldBuilder},
 };
 
-use crate::{bail, brain::Brain, database::PermissionSetting, fail, import, success};
+use crate::{bail, brain::Brain, database::PermissionSetting, fail, import, success, util};
 
 pub enum Commands {
 	About,
@@ -231,3 +236,132 @@ pub async fn import(
 		import::emboard_direct::import(brain, guild, create).await;
 	}
 }
+
+/// Checks the settings of the leaderboard associated with the provided guild.
+///
+/// Returns `None` if:
+/// - The guild does not have permissions set
+/// - The guild member has the appropriate permissions
+///
+/// Returns `Some` with InteractionResponseData if:
+/// - The guild member does not have proper permissions
+/// - The guild is set to role required, but no role is set, which should be
+///   impossible to happen, but.
+pub fn check_command_permissions(
+	brain: &Brain,
+	guild: Id<GuildMarker>,
+	member: PartialMember,
+) -> Option<InteractionResponseData> {
+	let settings = brain.db.get_leaderboard_settings(guild.get()).unwrap();
+	if let PermissionSetting::RoleRequired = settings.permission {
+		if let Some(role) = settings.role {
+			let member = member;
+			let found_role = member.roles.iter().find(|vrole| vrole.get() == role);
+
+			if found_role.is_none() {
+				Some(fail!(
+					"You do not have the right permissions to change the score"
+				))
+			} else {
+				None
+			}
+		} else {
+			// Seeing as the role is a required input on the subcommand, this
+			// would only happen with direct database fiddling or like, some
+			// really weird arcane bullshit?
+			Some(fail!(
+				"Permissions set to Role Required, but no role is set. \
+				This shouldn't be able to happen. \
+				Maybe try to set the permissions again?"
+			))
+		}
+	} else {
+		None
+	}
+}
+
+pub struct PointsCommandData {
+	points: i64,
+	points_verb: &'static str,
+	users: Vec<Id<UserMarker>>,
+	date: Result<PrimitiveDateTime, InteractionResponseData>,
+}
+
+impl PointsCommandData {
+	pub fn user_string(&self) -> String {
+		self.users
+			.iter()
+			.map(|u| format!("<@{u}>"))
+			.collect::<Vec<String>>()
+			.join(", ")
+	}
+}
+
+pub fn parse_points_command_data(
+	data: &CommandData,
+) -> Result<PointsCommandData, InteractionResponseData> {
+	let mut points = None;
+	let mut users: Vec<Id<UserMarker>> = vec![];
+	let mut date_string = None;
+
+	for opt in &data.options {
+		match opt.name.as_str() {
+			"points" => match opt.value {
+				CommandOptionValue::Integer(num) => points = Some(num),
+				_ => unreachable!(),
+			},
+			"users" => match &opt.value {
+				CommandOptionValue::String(raw) => {
+					let mentions = util::extract_user_mentions(&raw);
+					users = mentions;
+				}
+				_ => unreachable!(),
+			},
+			"date" => match &opt.value {
+				CommandOptionValue::String(raw) => {
+					date_string = Some(raw);
+				}
+				_ => unreachable!(),
+			},
+			_ => unreachable!(),
+		}
+	}
+
+	if users.len() == 0 {
+		return Err(fail!("No users mentioned! Who do we add points to?"));
+	}
+
+	let (points, _points_display, points_verb) = match points {
+		Some(p) if p >= 0 => (p, p, "added to"),
+		Some(p) if p < 0 => (p, -p, "removed from"),
+		Some(_) | None => unreachable!(),
+	};
+
+	let date_fmt = format_description::parse("[year]-[month]-[day]").unwrap();
+	let datetime_fmt =
+		format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap();
+
+	let date = match date_string {
+		None => Err(fail!("What date were these points earned?")),
+		Some(str) if str.is_empty() => Err(fail!("You must specify a date!")),
+		Some(date) => match Date::parse(date.trim(), &date_fmt) {
+			Err(_e) => match PrimitiveDateTime::parse(date.trim(), &datetime_fmt) {
+				Err(_e) => Err(fail!(
+					"Cannot parse the given date/time. Did you write it like \"2025-04-13\" or, with time, \"2025-04-13 02:33\"?"
+				)),
+				Ok(datetime) => Ok(datetime),
+			},
+			Ok(date) => Ok(PrimitiveDateTime::new(
+				date,
+				Time::from_hms(12, 0, 0).unwrap(),
+			)),
+		},
+	};
+
+	Ok(PointsCommandData {
+		points,
+		points_verb,
+		users,
+		date,
+	})
+}
diff --git a/src/command/revise.rs b/src/command/revise.rs
index ecb97d8..50b8c4e 100644
--- a/src/command/revise.rs
+++ b/src/command/revise.rs
@@ -1,24 +1,18 @@
 use std::sync::Arc;
 
-use time::{
-	Date, OffsetDateTime, PrimitiveDateTime, Time, format_description, macros::format_description,
-};
+use time::format_description;
 use twilight_model::{
-	application::interaction::application_command::{CommandData, CommandOptionValue},
+	application::interaction::application_command::CommandData,
 	gateway::payload::incoming::InteractionCreate,
 	http::interaction::InteractionResponseData,
-	id::{
-		Id,
-		marker::{GuildMarker, UserMarker},
-	},
+	id::{Id, marker::GuildMarker},
 };
 use twilight_util::builder::{InteractionResponseDataBuilder, embed::EmbedBuilder};
 
 use crate::{
-	bail,
 	brain::Brain,
-	database::{BoardRow, Error as DbError, PermissionSetting},
-	success, util,
+	command::{check_command_permissions, parse_points_command_data},
+	database::{BoardRow, Error as DbError},
 };
 
 pub async fn revise(
@@ -27,100 +21,31 @@ pub async fn revise(
 	create: &InteractionCreate,
 	data: &CommandData,
 ) -> InteractionResponseData {
-	let mut points = None;
-	let mut users: Vec<Id<UserMarker>> = vec![];
-	let mut date_string = None;
-
-	for opt in &data.options {
-		match opt.name.as_str() {
-			"points" => match opt.value {
-				CommandOptionValue::Integer(num) => points = Some(num),
-				_ => unreachable!(),
-			},
-			"users" => match &opt.value {
-				CommandOptionValue::String(raw) => {
-					let mentions = util::extract_user_mentions(&raw);
-					users = mentions;
-				}
-				_ => unreachable!(),
-			},
-			"date" => match &opt.value {
-				CommandOptionValue::String(raw) => {
-					date_string = Some(raw);
-				}
-				_ => unreachable!(),
-			},
-			_ => unreachable!(),
-		}
-	}
-
-	let date_fmt = format_description::parse("[year]-[month]-[day]").unwrap();
-	let datetime_fmt =
-		format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap();
-
-	if users.len() == 0 {
-		bail!("No users mentioned! Who do we add points to?")
-	}
-
-	if let Some(date) = date_string {
-		println!("date = \"{date}\"");
-	}
-
-	let date = match date_string {
-		None => bail!("What date were these points earned?"),
-		Some(str) if str.is_empty() => bail!("You must specify a date!"),
-		Some(date) => match Date::parse(date.trim(), &date_fmt) {
-			Err(_e) => match PrimitiveDateTime::parse(date.trim(), &datetime_fmt) {
-				Err(_e) => bail!(
-					"Cannot parse the given date/time. Did you write it like \"2025-04-13\" or, with time, \"2025-04-13 02:33\"?"
-				),
-				Ok(datetime) => datetime,
-			},
-			Ok(date) => PrimitiveDateTime::new(date, Time::from_hms(12, 0, 0).unwrap()),
+	let (point_data, date) = match parse_points_command_data(data) {
+		Err(e) => return e,
+		Ok(pcd) => match pcd.date {
+			Err(e) => return e,
+			Ok(date) => (pcd, date),
 		},
 	};
 
-	let (points, points_display, points_verb) = match points {
-		Some(p) if p > 0 => (p, p, "added to"),
-		Some(p) if p < 0 => (p, -p, "removed from"),
-		Some(0) => {
-			return success!("adding 0 points is a no-operation! I won't do anything :)");
-		}
-		Some(_) | None => unreachable!(),
-	};
-
 	brain.create_leaderboard_if_not_exists(guild);
-
-	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();
-			let found_role = member.roles.iter().find(|vrole| vrole.get() == role);
-
-			if found_role.is_none() {
-				bail!("You do not have the right permissions to change the score");
-			}
-		} else {
-			// Seeing as the role is a required input on the subcommand, this
-			// would only happen with direct database fiddling or like, some
-			// really weird arcane bullshit?
-			bail!(
-				"Permissions set to Role Required, but no role is set. \
-				This shouldn't be able to happen. \
-				Maybe try to set the permissions again?"
-			);
-		}
+	if let Some(err) = check_command_permissions(&brain, guild, create.member.clone().unwrap()) {
+		return err;
 	}
 
-	for user in &users {
-		match brain.db.give_user_points(guild.get(), user.get(), points) {
+	for user in &point_data.users {
+		match brain
+			.db
+			.give_user_points(guild.get(), user.get(), point_data.points)
+		{
 			Err(DbError::UserNotExist) => {
 				let member = brain.guild_member(guild, *user).await;
 				let row = BoardRow {
 					user_id: user.get(),
 					user_handle: member.handle,
 					user_nickname: member.nick.or(member.global_name),
-					points,
+					points: point_data.points,
 				};
 				brain.db.add_user_to_leaderboard(guild.get(), row).unwrap();
 			}
@@ -130,18 +55,24 @@ pub async fn revise(
 		//FIXME: gen 2025-08-14: do not assume UTC!
 		brain
 			.db
-			.revise_last_history_date(guild.get(), user.get(), points, date.assume_utc())
+			.revise_last_history_date(
+				guild.get(),
+				user.get(),
+				point_data.points,
+				date.assume_utc(),
+			)
 			.unwrap();
 	}
 
-	let users_string = users
-		.into_iter()
-		.map(|u| format!("<@{u}>"))
-		.collect::<Vec<String>>()
-		.join(", ");
+	let datetime_fmt =
+		format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap();
+
+	let points_verb = point_data.points_verb;
+	let users_string = point_data.user_string();
 
 	let msg = format!(
-		"{points_display} points {points_verb} {users_string} on date {}",
+		"{} points {points_verb} {users_string} on date {}",
+		point_data.points.abs(),
 		date.format(&datetime_fmt).unwrap()
 	);
 	let embed = EmbedBuilder::new().description(msg).build();