about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock57
-rw-r--r--Cargo.toml6
-rw-r--r--README.md33
-rw-r--r--TODO19
-rw-r--r--src/brain.rs123
-rw-r--r--src/command/add_points.rs64
-rw-r--r--src/command/leaderboard.rs91
-rw-r--r--src/command/mod.rs367
-rw-r--r--src/command/revise.rs82
-rw-r--r--src/database.rs86
-rw-r--r--src/import.rs54
-rw-r--r--src/import/mod.rs126
-rw-r--r--src/main.rs477
-rw-r--r--src/util.rs97
14 files changed, 1196 insertions, 486 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9d048cd..4202860 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -482,12 +482,14 @@ dependencies = [
  "rusqlite",
  "serde",
  "serde_json",
+ "time",
  "tokio",
  "twilight-cache-inmemory",
  "twilight-gateway",
  "twilight-http",
  "twilight-model",
  "twilight-util",
+ "ureq",
 ]
 
 [[package]]
@@ -726,6 +728,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 +750,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 +1268,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 +1344,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..0671666 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,3 +16,9 @@ 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 }
+time = { version = "0.3.41", features = ["parsing", "formatting"] }
+
+[features]
+default = ["emboard-direct"]
+emboard-direct = ["ureq"]
diff --git a/README.md b/README.md
index e515a22..3ea8a9e 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,35 @@
-EMBOARD is end-of-life and I have a need for a leaderboard bot.
+EMBOARD is end-of-life and I have a need for a leaderboard bot.  
 How hard could it be?
 
 Uh oh, the code is kinda bad.
 
 [Invite Link](https://discord.com/oauth2/authorize?client_id=1363055126264283136&permissions=2048&integration_type=0&scope=bot+applications.commands)
 
-Requirements- We want to do what emboard can at least. here is what we do so far.  
-- `leaderboard`: Displays the leaderboard.  
-- `points [points] [user]`: Change the points of the mentioned users  
+<!-- [Invite Link](https://discord.com/oauth2/authorize?client_id=1363494986699771984&permissions=2048&integration_type=0&scope=bot+applications.commands) -->
+
+**Commands**  
+- `about`: Get information about the bot.  
+- `leaderboard`: Displays the leaderboard. With an optional `[style]` argument to change how it displays.  
+- `points [points] [users]`: Change the points of the mentioned users  
 - `permission none`: No permissions are required to change the score; anyone may set the score. This command requires the Manage Server permission.  
-- `permission role [role]`: A specific role is required to change the score. This command requires the Manage Server permission.  
\ No newline at end of file
+- `permission role [role]`: A specific role is required to change the score. This command requires the Manage Server permission.  
+- `revise [points] [users] [date]`: Change the points of the mentioned users, and make that change apply to some different date than today.  
+
+**Leaderboard Display Options**  
+The leaderboard display options, the `style` argument of the leaderboard command, change how ties are displayed. Ties will always be ordered by who was there sooner.
+
+`Ties Equal` puts ties in the same placement. It will look like this:  
+```raw
+1. genny: 10
+2. medley: 5
+   inann: 5
+4. taiga: 3
+```
+
+`Ties Broken` puts ties in different placements. It will look like this:  
+```raw
+1. genny: 10
+2. medley: 5
+3. inann: 5
+4. taiga: 3
+```
diff --git a/TODO b/TODO
index 0057cf8..aef2fc3 100644
--- a/TODO
+++ b/TODO
@@ -2,4 +2,21 @@ Use the cache!
 	We do not- I mean, we set the cache /up/ but we do not make use of it
 	kind of really at all.
 	I think the only place we kind of need this is in `add_points` where
-	we get the members of the guild.
\ No newline at end of file
+	we get the members of the guild.
+
+Better logging
+	There's like, an user now, so we should log better so we can find errors
+	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
+
+Wrong count of noun used when 1 point is added
+	"1 points added"
\ No newline at end of file
diff --git a/src/brain.rs b/src/brain.rs
new file mode 100644
index 0000000..daa6f6f
--- /dev/null
+++ b/src/brain.rs
@@ -0,0 +1,123 @@
+use crate::{APP_ID, 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},
+	},
+};
+
+// 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 {
+	#[allow(dead_code)]
+	pub id: Id<UserMarker>,
+	pub handle: String,
+	pub global_name: Option<String>,
+	pub nick: Option<String>,
+}
diff --git a/src/command/add_points.rs b/src/command/add_points.rs
new file mode 100644
index 0000000..97e3981
--- /dev/null
+++ b/src/command/add_points.rs
@@ -0,0 +1,64 @@
+use std::sync::Arc;
+
+use twilight_model::{
+	application::interaction::application_command::CommandData,
+	gateway::payload::incoming::InteractionCreate,
+	http::interaction::InteractionResponseData,
+	id::{Id, marker::GuildMarker},
+};
+use twilight_util::builder::{InteractionResponseDataBuilder, embed::EmbedBuilder};
+
+use crate::{
+	brain::Brain,
+	command::{check_command_permissions, parse_points_command_data},
+	database::{BoardRow, Error as DbError},
+};
+
+pub async fn add_points(
+	brain: Arc<Brain>,
+	guild: Id<GuildMarker>,
+	create: &InteractionCreate,
+	data: &CommandData,
+) -> InteractionResponseData {
+	let point_data = match parse_points_command_data(data) {
+		Err(e) => return e,
+		Ok(pcd) => pcd,
+	};
+
+	brain.create_leaderboard_if_not_exists(guild);
+	if let Some(err) = check_command_permissions(&brain, guild, create.member.clone().unwrap()) {
+		return err;
+	}
+
+	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: point_data.points,
+				};
+				brain.db.add_user_to_leaderboard(guild.get(), row).unwrap();
+			}
+			_ => (),
+		}
+	}
+
+	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 embed = EmbedBuilder::new().description(msg).build();
+	InteractionResponseDataBuilder::new()
+		.embeds([embed])
+		.build()
+}
diff --git a/src/command/leaderboard.rs b/src/command/leaderboard.rs
new file mode 100644
index 0000000..97971f5
--- /dev/null
+++ b/src/command/leaderboard.rs
@@ -0,0 +1,91 @@
+use std::sync::Arc;
+
+use twilight_model::{
+	application::interaction::application_command::{CommandData, CommandOptionValue},
+	http::interaction::InteractionResponseData,
+	id::{Id, marker::GuildMarker},
+};
+use twilight_util::builder::{InteractionResponseDataBuilder, embed::EmbedBuilder};
+
+use crate::{brain::Brain, database::Error as DbError, fail, util};
+
+enum Style {
+	TiesEqual,
+	TiesBroken,
+}
+
+pub fn leaderboard(
+	brain: Arc<Brain>,
+	guild: Id<GuildMarker>,
+	data: Option<&CommandData>,
+) -> InteractionResponseData {
+	let board = brain.db.get_leaderboard(guild.get());
+
+	let mut style = Style::TiesBroken;
+	if let Some(data) = data {
+		for opt in &data.options {
+			match opt.name.as_str() {
+				"style" => match &opt.value {
+					CommandOptionValue::String(raw) => match raw.as_str() {
+						"equal" => style = Style::TiesEqual,
+						"broken" => style = Style::TiesBroken,
+						_ => {
+							return fail!(format!(
+								"{raw} is not a valid style option. Try 'equal' or 'broken'"
+							));
+						}
+					},
+					_ => unreachable!(),
+				},
+				_ => unreachable!(),
+			}
+		}
+	}
+
+	match board {
+		Err(DbError::TableNotExist) => {
+			fail!("No leaderboard exists for this server! Create one by giving someone points.")
+		}
+		Err(DbError::UserNotExist) => unreachable!(),
+		Ok(data) => {
+			let placed = util::tiebreak_shared_positions(data);
+
+			let str = match style {
+				Style::TiesEqual => placed
+					.into_iter()
+					.map(|placement| {
+						if placement.placement_first_of_tie {
+							format!(
+								"`{:>2}` <@{}>: {}",
+								placement.placement_tie,
+								placement.row.user_id,
+								placement.row.points
+							)
+						} else {
+							format!(
+								"`  ` <@{}>: {}",
+								placement.row.user_id, placement.row.points
+							)
+						}
+					})
+					.collect::<Vec<String>>()
+					.join("\n"),
+				Style::TiesBroken => placed
+					.into_iter()
+					.map(|placement| {
+						format!(
+							"`{:>2}` <@{}>: {}",
+							placement.placement, placement.row.user_id, placement.row.points
+						)
+					})
+					.collect::<Vec<String>>()
+					.join("\n"),
+			};
+
+			let embed = EmbedBuilder::new().description(str).build();
+			InteractionResponseDataBuilder::new()
+				.embeds([embed])
+				.build()
+		}
+	}
+}
diff --git a/src/command/mod.rs b/src/command/mod.rs
new file mode 100644
index 0000000..c3dc7a9
--- /dev/null
+++ b/src/command/mod.rs
@@ -0,0 +1,367 @@
+mod add_points;
+mod leaderboard;
+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;
+
+use twilight_model::{
+	application::{
+		command::{Command, CommandType},
+		interaction::application_command::{CommandData, CommandOptionValue},
+	},
+	gateway::payload::incoming::InteractionCreate,
+	guild::PartialMember,
+	http::interaction::InteractionResponseData,
+	id::{
+		Id,
+		marker::{GuildMarker, UserMarker},
+	},
+};
+use twilight_util::builder::{
+	InteractionResponseDataBuilder,
+	command::{CommandBuilder, IntegerBuilder, RoleBuilder, StringBuilder, SubCommandBuilder},
+	embed::{EmbedBuilder, EmbedFieldBuilder},
+};
+
+use crate::{bail, brain::Brain, database::PermissionSetting, fail, import, success, util};
+
+pub enum Commands {
+	About,
+	Leaderboard,
+	Points,
+	Revise,
+	Permission,
+	Import,
+}
+
+pub async fn handler(
+	brain: Arc<Brain>,
+	guild: Id<GuildMarker>,
+	create: &InteractionCreate,
+	command: Commands,
+	command_data: &CommandData,
+) {
+	// Handle the command and create our interaction response data
+	let data = match command {
+		Commands::About => about(brain.clone(), guild),
+		Commands::Leaderboard => leaderboard(brain.clone(), guild, Some(command_data)),
+		Commands::Points => add_points(brain.clone(), guild, create, command_data).await,
+		Commands::Revise => revise(brain.clone(), guild, create, command_data).await,
+		Commands::Permission => permission(brain.clone(), guild, create, command_data).await,
+		Commands::Import => {
+			import(brain.clone(), guild, create, command_data).await;
+			return;
+		}
+	};
+
+	// Finally, send back the response
+	brain.interaction_respond(create, data).await;
+}
+
+pub async fn build() -> Vec<Command> {
+	let about = CommandBuilder::new(
+		"about",
+		"Get information about the bot",
+		CommandType::ChatInput,
+	)
+	.build();
+
+	let leaderboard = CommandBuilder::new(
+		"leaderboard",
+		"View the server leaderboard",
+		CommandType::ChatInput,
+	)
+	.option(
+		StringBuilder::new("style", "style of leaderboard to display")
+			.choices([("Ties Equal", "equal"), ("Ties Broken", "broken")]),
+	)
+	.build();
+
+	let points = CommandBuilder::new("points", "Add and remove points!", CommandType::ChatInput)
+		.option(IntegerBuilder::new("points", "number of points. - or +").required(true))
+		.option(
+			StringBuilder::new("users", "mention people to modify their points!!").required(true),
+		)
+		.validate()
+		.unwrap()
+		.build();
+
+	let revise = CommandBuilder::new(
+		"revise",
+		"Change history by adding/removing points at a specific point in time",
+		CommandType::ChatInput,
+	)
+	.option(IntegerBuilder::new("points", "number of points. - or +").required(true))
+	.option(StringBuilder::new("users", "mention people to modify their points!!").required(true))
+	.option(
+		StringBuilder::new(
+			"date",
+			"Date string in the YYYY-DD-MM format, with optional time component (HH:MM:SS, 24-hour time)",
+		)
+		.required(true),
+	)
+	.validate()
+	.unwrap()
+	.build();
+
+	let permission = CommandBuilder::new(
+		"permission",
+		"set who is allowed to change points",
+		CommandType::ChatInput,
+	)
+	.option(
+		SubCommandBuilder::new("role", "require a role to change the score")
+			.option(RoleBuilder::new("role", "role required").required(true)),
+	)
+	.option(SubCommandBuilder::new(
+		"none",
+		"anyone can change the score",
+	))
+	.build();
+
+	let import = CommandBuilder::new(
+		"import",
+		"import this server's emboard leaderboard by adding the points in emboard, to leaberblord's",
+		CommandType::ChatInput,
+	)
+	.build();
+
+	vec![about, leaderboard, points, revise, permission, import]
+}
+
+pub fn about(_brain: Arc<Brain>, _guild: Id<GuildMarker>) -> InteractionResponseData {
+	let embed = EmbedBuilder::new()
+		.title("About Leaberblord")
+		.description(
+			"A single-purpose bot for keeping score. Written in Rust using the twilight set of crates.",
+		).field(EmbedFieldBuilder::new("Source", "The source is available on the author's [cgit instance](https://git.dreamy.place/whimsy/leaberblord/about)"))
+		.field(EmbedFieldBuilder::new("Author", "Written by @gennyble. Her homepage is [dreamy.place](https://dreamy.place)"))
+		.color(0x33aa88).build();
+
+	InteractionResponseDataBuilder::new()
+		.embeds([embed])
+		.build()
+}
+
+pub async fn permission(
+	brain: Arc<Brain>,
+	guild: Id<GuildMarker>,
+	create: &InteractionCreate,
+	data: &CommandData,
+) -> InteractionResponseData {
+	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 action");
+	}
+
+	for opt in &data.options {
+		return match opt.name.as_str() {
+			"none" => permission_none(brain, guild),
+			"role" => permission_role(brain, guild, opt.value.clone()),
+			_ => {
+				eprintln!("permission command, unknown option '{}'", opt.name);
+				fail!("this should be unreachable. report the bug maybe? to gennyble on discord")
+			}
+		};
+	}
+
+	fail!("this should be unreachable. report the bug maybe? to gennyble on discord")
+}
+
+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();
+	}
+
+	success!("Permission for leaderboard changed to none. Anyone may now set the score.")
+}
+
+fn permission_role(
+	brain: Arc<Brain>,
+	guild: Id<GuildMarker>,
+	data: CommandOptionValue,
+) -> InteractionResponseData {
+	let role = if let CommandOptionValue::SubCommand(data) = data {
+		match data.iter().find(|d| d.name == "role").unwrap().value {
+			CommandOptionValue::Role(role) => role,
+			_ => panic!(),
+		}
+	} else {
+		panic!()
+	};
+
+	brain.create_leaderboard_if_not_exists(guild);
+
+	brain
+		.db
+		.set_leaderboard_permission(guild.get(), PermissionSetting::RoleRequired)
+		.unwrap();
+	brain
+		.db
+		.set_leaderboard_permission_role(guild.get(), role.get())
+		.unwrap();
+
+	success!(format!(
+		"Permission for leaderboard changed to Role Required. \
+		Only members of <@&{role}> may now set the score"
+	))
+}
+
+pub async fn import(
+	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;
+	}
+}
+
+/// 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
new file mode 100644
index 0000000..50b8c4e
--- /dev/null
+++ b/src/command/revise.rs
@@ -0,0 +1,82 @@
+use std::sync::Arc;
+
+use time::format_description;
+use twilight_model::{
+	application::interaction::application_command::CommandData,
+	gateway::payload::incoming::InteractionCreate,
+	http::interaction::InteractionResponseData,
+	id::{Id, marker::GuildMarker},
+};
+use twilight_util::builder::{InteractionResponseDataBuilder, embed::EmbedBuilder};
+
+use crate::{
+	brain::Brain,
+	command::{check_command_permissions, parse_points_command_data},
+	database::{BoardRow, Error as DbError},
+};
+
+pub async fn revise(
+	brain: Arc<Brain>,
+	guild: Id<GuildMarker>,
+	create: &InteractionCreate,
+	data: &CommandData,
+) -> InteractionResponseData {
+	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),
+		},
+	};
+
+	brain.create_leaderboard_if_not_exists(guild);
+	if let Some(err) = check_command_permissions(&brain, guild, create.member.clone().unwrap()) {
+		return err;
+	}
+
+	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: point_data.points,
+				};
+				brain.db.add_user_to_leaderboard(guild.get(), row).unwrap();
+			}
+			_ => (),
+		}
+
+		//FIXME: gen 2025-08-14: do not assume UTC!
+		brain
+			.db
+			.revise_last_history_date(
+				guild.get(),
+				user.get(),
+				point_data.points,
+				date.assume_utc(),
+			)
+			.unwrap();
+	}
+
+	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 {points_verb} {users_string} on date {}",
+		point_data.points.abs(),
+		date.format(&datetime_fmt).unwrap()
+	);
+	let embed = EmbedBuilder::new().description(msg).build();
+	InteractionResponseDataBuilder::new()
+		.embeds([embed])
+		.build()
+}
diff --git a/src/database.rs b/src/database.rs
index 0e799d3..f496e65 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -1,6 +1,7 @@
 use std::{path::Path, sync::Mutex};
 
 use rusqlite::{Connection, OptionalExtension, params};
+use time::{OffsetDateTime, format_description};
 
 #[derive(Debug)]
 pub struct Database {
@@ -66,24 +67,25 @@ impl Database {
 	}
 
 	pub fn leaderboard_exits(&self, guild_id: u64) -> bool {
-		self.leaderboard_id(guild_id).is_err()
+		self.leaderboard_id(guild_id).is_ok()
 	}
 
 	pub fn get_leaderboard(&self, guild_id: u64) -> Result<Vec<BoardRow>, Error> {
 		// Don't deadlock!
 		let lb = self.leaderboard_id(guild_id)?;
+		let query = lb.sql_leaderboard_query();
 		let conn = self.conn.lock().unwrap();
 
-		let mut query = conn
-			.prepare(&lb.sql("SELECT * FROM leaderboard_LBID"))
-			.unwrap();
+		// The query that get's run sorts ties by preferring who got to that score first
+		let mut query = conn.prepare(&lb.sql(query)).unwrap();
+
 		let vec = query
 			.query_map((), |row| {
 				Ok(BoardRow {
-					user_id: row.get(0)?,
-					user_handle: row.get(1)?,
-					user_nickname: row.get(2)?,
-					points: row.get(3)?,
+					user_id: row.get(1)?,
+					user_handle: row.get(2)?,
+					user_nickname: row.get(3)?,
+					points: row.get(4)?,
 				})
 			})
 			.optional()
@@ -167,7 +169,7 @@ impl Database {
 		let conn = self.conn.lock().unwrap();
 
 		let sql = lb.sql("SELECT * FROM leaderboard_LBID WHERE user_id=?1");
-		let user_handle: String = conn
+		let _user_handle: String = conn
 			.query_row(&sql, params![user], |row| row.get(1))
 			.map_err(|_| Error::UserNotExist)?;
 
@@ -176,6 +178,42 @@ impl Database {
 
 		Ok(())
 	}
+
+	pub fn revise_last_history_date(
+		&self,
+		guild_id: u64,
+		user: u64,
+		points: i64,
+		date: OffsetDateTime,
+	) -> Result<(), Error> {
+		let lb = self.leaderboard_id(guild_id)?;
+		let conn = self.conn.lock().unwrap();
+
+		let sql = lb.sql(
+			"SELECT id, user_id, timestamp, points FROM leaderboard_history_LBID \
+					WHERE user_id=?1 AND points=?2 \
+					ORDER BY timestamp DESC LIMIT 1",
+		);
+
+		let hist_id: i64 = conn
+			.query_row(&sql, params![user, points], |row| row.get(0))
+			.unwrap();
+
+		println!("hist_id = {hist_id}");
+
+		let update_sql = lb.sql("UPDATE leaderboard_history_LBID SET timestamp = ?1 WHERE id = ?2");
+
+		let datetime_fmt =
+			format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap();
+
+		conn.execute(
+			&update_sql,
+			params![date.format(&datetime_fmt).unwrap(), hist_id],
+		)
+		.unwrap();
+
+		Ok(())
+	}
 }
 
 pub enum PermissionSetting {
@@ -244,6 +282,19 @@ const CREATE_TRIGGER_LDBHIST_INSERT: &'static str = "\
 			INSERT INTO leaderboard_history_LBID(user_id, points) VALUES(new.user_id, new.points);
 		END;";
 
+const LEADERBOARD_QUERY: &'static str = "\
+	SELECT
+		history.timestamp,
+		leaderboard_LBID.*
+	FROM leaderboard_LBID
+	JOIN (
+		SELECT user_id, max(timestamp) as timestamp
+		FROM leaderboard_history_LBID
+		GROUP BY user_id
+	) history
+	ON history.user_id = leaderboard_LBID.user_id
+	ORDER BY leaderboard_LBID.points DESC, history.timestamp ASC;";
+
 struct LeaderboardTable {
 	id: usize,
 }
@@ -272,6 +323,10 @@ impl LeaderboardTable {
 	pub fn sql_create_insert_trigger_history_table(&self) -> String {
 		CREATE_TRIGGER_LDBHIST_INSERT.replace("LBID", &self.id.to_string())
 	}
+
+	pub fn sql_leaderboard_query(&self) -> String {
+		LEADERBOARD_QUERY.replace("LBID", &self.id.to_string())
+	}
 }
 
 #[derive(Clone, Debug)]
@@ -282,6 +337,19 @@ pub struct BoardRow {
 	pub points: i64,
 }
 
+#[derive(Clone, Debug)]
+pub struct Placement {
+	pub row: BoardRow,
+
+	/// Absolute placement sorting by points and then
+	/// TODO: sort by date points attained
+	pub placement: usize,
+	/// Placement allowing ties, and placing ties in the same place
+	pub placement_tie: usize,
+	/// Whether or not this placement was first of a tie. True if untied.
+	pub placement_first_of_tie: bool,
+}
+
 #[derive(Debug)]
 pub enum Error {
 	TableNotExist,
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..03e2bed
--- /dev/null
+++ b/src/import/mod.rs
@@ -0,0 +1,126 @@
+#![allow(non_snake_case)]
+use serde::Deserialize;
+
+use crate::database::{BoardRow, Database, Error};
+
+#[derive(Debug, Deserialize)]
+pub struct Emboard {
+	#[allow(dead_code)]
+	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;
+
+	use twilight_model::{
+		gateway::payload::incoming::InteractionCreate,
+		id::{Id, marker::GuildMarker},
+	};
+	use twilight_util::builder::InteractionResponseDataBuilder;
+	use ureq::config::Config;
+
+	use crate::{brain::Brain, command};
+
+	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 = command::leaderboard(brain.clone(), guild_id, None)
+					.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..d02c9c1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,147 +1,26 @@
 use std::{env, error::Error, path::PathBuf, sync::Arc};
 
+use brain::Brain;
 use confindent::Confindent;
-use database::{BoardRow, Database, Error as DbError, PermissionSetting};
+use database::Database;
 use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
 use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
-use twilight_http::{Client as HttpClient, client::InteractionClient};
-use twilight_model::{
-	application::{
-		command::{Command, CommandType},
-		interaction::{
-			InteractionData,
-			application_command::{CommandData, CommandOptionValue},
-		},
-	},
-	gateway::payload::incoming::InteractionCreate,
-	http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType},
-	id::{
-		Id,
-		marker::{GuildMarker, UserMarker},
-	},
-};
-use twilight_util::builder::{
-	InteractionResponseDataBuilder,
-	command::{CommandBuilder, IntegerBuilder, RoleBuilder, StringBuilder, SubCommandBuilder},
-	embed::EmbedBuilder,
-};
+use twilight_http::Client as HttpClient;
+use twilight_model::{application::interaction::InteractionData, id::Id};
 
+use crate::command::Commands;
+
+mod brain;
+mod command;
 mod database;
 mod import;
+mod util;
 
 const PROD_APP_ID: u64 = 1363055126264283136;
+#[allow(dead_code)]
 const DEV_APP_ID: u64 = 1363494986699771984;
 const APP_ID: u64 = PROD_APP_ID;
 
-macro_rules! bail {
-	($msg:expr) => {
-		return InteractionResponseDataBuilder::new()
-			.content(&format!("❌ {}", $msg))
-			.build()
-	};
-}
-
-macro_rules! fail {
-	($msg:expr) => {
-		InteractionResponseDataBuilder::new()
-			.content(&format!("❌ {}", $msg))
-			.build()
-	};
-}
-
-macro_rules! success {
-	($msg:expr) => {
-		InteractionResponseDataBuilder::new()
-			.content(&format!("✅ {}", $msg))
-			.build()
-	};
-}
-
-// 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();
@@ -156,13 +35,12 @@ async fn main() -> anyhow::Result<()> {
 	let db_dir = conf
 		.child_owned("Database")
 		.unwrap_or("/var/leaberblord/leaberblord.sqlite".to_string());
-	let token = env::var("DISCORD_TOKEN")
-		.ok()
-		.unwrap_or_else(|| conf.child_owned("DiscordToken").unwrap());
 
 	let db = Database::new(db_dir);
 	db.create_tables();
 
+	// Look for an `import` CLI-thing so we know if we should impor and emboard
+	// database from local
 	let arg = std::env::args().nth(1);
 	match arg.as_deref() {
 		Some("import") => {
@@ -179,7 +57,46 @@ async fn main() -> anyhow::Result<()> {
 		_ => (),
 	}
 
-	let mut shard = Shard::new(ShardId::ONE, token.clone(), Intents::GUILD_MESSAGES);
+	// Finally, setup and start the bot itself
+	let TwilightStuff {
+		mut shard,
+		http,
+		cache,
+	} = setup_twilight(&conf).await;
+	let brain = Arc::new(Brain::new(db, http, cache));
+
+	while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
+		let Ok(event) = item else {
+			eprintln!("error receiving event");
+			continue;
+		};
+
+		brain.update_cache(&event);
+
+		tokio::spawn(handle_event(event, Arc::clone(&brain)));
+	}
+
+	Ok(())
+}
+
+/// A more proper container than a tuple for everything we get from [setup_twilight]
+struct TwilightStuff {
+	shard: Shard,
+	http: HttpClient,
+	cache: DefaultInMemoryCache,
+}
+
+/// Configure enough to get off the ground with twilight.
+/// - Get the `DISCORD_TOKEN` from the environment or `DiscordToken` from the conf file, with that preference
+/// - Setup ourself as Shard 1 and take the `GUILD_MESSAGES` intent
+/// - Register with the API and create our `InMemoryCache`
+/// - Register all of our commands
+async fn setup_twilight(conf: &Confindent) -> TwilightStuff {
+	let token = env::var("DISCORD_TOKEN")
+		.ok()
+		.unwrap_or_else(|| conf.child_owned("DiscordToken").unwrap());
+
+	let shard = Shard::new(ShardId::ONE, token.clone(), Intents::GUILD_MESSAGES);
 	let http = HttpClient::new(token);
 	let cache = DefaultInMemoryCache::builder()
 		.resource_types(ResourceType::GUILD)
@@ -188,7 +105,7 @@ async fn main() -> anyhow::Result<()> {
 		.build();
 
 	let iclient = http.interaction(Id::new(APP_ID));
-	let commands_vec = commands().await;
+	let commands_vec = command::build().await;
 	iclient.set_global_commands(&commands_vec).await.unwrap();
 
 	println!("Set commands!");
@@ -212,22 +129,11 @@ 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;
-		};
-
-		brain.cache.update(&event);
-
-		tokio::spawn(handle_event(event, Arc::clone(&brain)));
-	}
-
-	Ok(())
+	TwilightStuff { shard, http, cache }
 }
 
+/// Handle all of our events. This is our main work function and what main
+/// tends to branch into, but we don't loop in the function itself.
 async fn handle_event(event: Event, brain: Arc<Brain>) -> Result<(), Box<dyn Error + Send + Sync>> {
 	match event {
 		Event::InteractionCreate(create) => {
@@ -246,13 +152,16 @@ async fn handle_event(event: Event, brain: Arc<Brain>) -> Result<(), Box<dyn Err
 				let guild = create.guild.as_ref().unwrap().id.unwrap();
 
 				let command = match cmd.name.as_str() {
+					"about" => Commands::About,
 					"leaderboard" => Commands::Leaderboard,
 					"points" => Commands::Points,
+					"revise" => Commands::Revise,
 					"permission" => Commands::Permission,
+					"import" => Commands::Import,
 					_ => panic!("'{}' is not a command", cmd.name),
 				};
 
-				command_handler(brain, guild, &create, command, cmd).await;
+				command::handler(brain, guild, &create, command, cmd).await;
 			}
 		}
 		Event::GatewayClose(close) => {
@@ -263,269 +172,3 @@ async fn handle_event(event: Event, brain: Arc<Brain>) -> Result<(), Box<dyn Err
 
 	Ok(())
 }
-
-async fn commands() -> Vec<Command> {
-	let leaderboard = CommandBuilder::new(
-		"leaderboard",
-		"View the server leaderboard",
-		CommandType::ChatInput,
-	)
-	.build();
-
-	let points = CommandBuilder::new("points", "Add and remove points!", CommandType::ChatInput)
-		.option(IntegerBuilder::new("points", "number of points. - or +").required(true))
-		.option(
-			StringBuilder::new("users", "mention people to modify their points!!").required(true),
-		)
-		.validate()
-		.unwrap()
-		.build();
-
-	let permission = CommandBuilder::new(
-		"permission",
-		"set who is allowed to change points",
-		CommandType::ChatInput,
-	)
-	.option(
-		SubCommandBuilder::new("role", "require a role to change the score")
-			.option(RoleBuilder::new("role", "role required").required(true)),
-	)
-	.option(SubCommandBuilder::new(
-		"none",
-		"anyone can change the score",
-	))
-	.build();
-
-	vec![leaderboard, points, permission]
-}
-
-async fn command_handler(
-	brain: Arc<Brain>,
-	guild: Id<GuildMarker>,
-	create: &InteractionCreate,
-	command: Commands,
-	command_data: &CommandData,
-) {
-	// Handle the command and create our interaction response data
-	let data = match command {
-		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
-	brain.interaction_respond(create, data).await;
-}
-
-fn get_leaderboard(brain: Arc<Brain>, guild: Id<GuildMarker>) -> InteractionResponseData {
-	let board = brain.db.get_leaderboard(guild.get());
-
-	match board {
-		Err(DbError::TableNotExist) => {
-			fail!("No leaderboard exists for this server! Create one by giving someone points.")
-		}
-		Err(DbError::UserNotExist) => unreachable!(),
-		Ok(data) => {
-			let str = data
-				.into_iter()
-				.enumerate()
-				.map(|(idx, br)| format!("{idx}. <@{}>: {}", br.user_id, br.points))
-				.collect::<Vec<String>>()
-				.join("\n");
-
-			let embed = EmbedBuilder::new().description(str).build();
-			InteractionResponseDataBuilder::new()
-				.embeds([embed])
-				.build()
-		}
-	}
-}
-
-async fn add_points(
-	brain: Arc<Brain>,
-	guild: Id<GuildMarker>,
-	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) => {
-					println!("raw: {raw}");
-					let mentions = extract_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!(),
-	};
-
-	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?"
-			);
-		}
-	}
-
-	for user in &users {
-		match brain.db.give_user_points(guild.get(), user.get(), 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,
-				};
-				brain.db.add_user_to_leaderboard(guild.get(), row).unwrap();
-			}
-			_ => (),
-		}
-	}
-
-	let users_string = users
-		.into_iter()
-		.map(|u| format!("<@{u}>"))
-		.collect::<Vec<String>>()
-		.join(", ");
-
-	let msg = format!("{points_display} points {points_verb} {users_string}");
-	let embed = EmbedBuilder::new().description(msg).build();
-	InteractionResponseDataBuilder::new()
-		.embeds([embed])
-		.build()
-}
-
-async fn permission(
-	brain: Arc<Brain>,
-	guild: Id<GuildMarker>,
-	create: &InteractionCreate,
-	data: &CommandData,
-) -> InteractionResponseData {
-	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");
-	}
-
-	for opt in &data.options {
-		return match opt.name.as_str() {
-			"none" => permission_none(brain, guild),
-			"role" => permission_role(brain, guild, opt.value.clone()),
-			_ => panic!(),
-		};
-	}
-
-	fail!("this should be unreachable. report the bug maybe? to gennyble on discord")
-}
-
-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();
-	}
-
-	success!("Permission for leaderboard changed to none. Anyone may now set the score.")
-}
-
-fn permission_role(
-	brain: Arc<Brain>,
-	guild: Id<GuildMarker>,
-	data: CommandOptionValue,
-) -> InteractionResponseData {
-	let role = if let CommandOptionValue::SubCommand(data) = data {
-		match data.iter().find(|d| d.name == "role").unwrap().value {
-			CommandOptionValue::Role(role) => role,
-			_ => panic!(),
-		}
-	} else {
-		panic!()
-	};
-
-	brain.create_leaderboard_if_not_exists(guild);
-
-	brain
-		.db
-		.set_leaderboard_permission(guild.get(), PermissionSetting::RoleRequired)
-		.unwrap();
-	brain
-		.db
-		.set_leaderboard_permission_role(guild.get(), role.get())
-		.unwrap();
-
-	success!(format!(
-		"Permission for leaderboard changed to Role Required. \
-		Only members of <@&{role}> may now set the score"
-	))
-}
-
-enum Commands {
-	Leaderboard,
-	Points,
-	Permission,
-}
-
-fn extract_mentions(mut raw: &str) -> Vec<Id<UserMarker>> {
-	let mut ret = vec![];
-
-	loop {
-		let Some(start) = raw.find("<@") else {
-			break;
-		};
-
-		let Some(end) = raw.find('>') else {
-			break;
-		};
-
-		let id_str = &raw[start + 2..end];
-		raw = &raw[end + 1..];
-
-		let Ok(id) = u64::from_str_radix(id_str, 10) else {
-			continue;
-		};
-
-		ret.push(Id::new(id));
-	}
-
-	ret
-}
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..a746cb3
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,97 @@
+//! Uh-oh, a `util.rs` file. That can't be good.
+
+use twilight_model::id::{Id, marker::UserMarker};
+
+use crate::database::{BoardRow, Placement};
+
+#[macro_export]
+macro_rules! bail {
+	($msg:expr) => {
+		return twilight_util::builder::InteractionResponseDataBuilder::new()
+			.content(&format!("❌ {}", $msg))
+			.build()
+	};
+}
+
+#[macro_export]
+macro_rules! fail {
+	($msg:expr) => {
+		twilight_util::builder::InteractionResponseDataBuilder::new()
+			.content(&format!("❌ {}", $msg))
+			.build()
+	};
+}
+
+#[macro_export]
+macro_rules! success {
+	($msg:expr) => {
+		twilight_util::builder::InteractionResponseDataBuilder::new()
+			.content(&format!("✅ {}", $msg))
+			.build()
+	};
+}
+
+pub fn tiebreak_shared_positions(board: Vec<BoardRow>) -> Vec<Placement> {
+	let first = match board.first() {
+		Some(r) => r.clone(),
+		None => return vec![],
+	};
+
+	let mut last_score = first.points;
+	let mut last_placed = 1;
+	let mut placed = vec![Placement {
+		row: first,
+		placement: 1,
+		placement_tie: 1,
+		placement_first_of_tie: true,
+	}];
+
+	for (idx, row) in board.into_iter().enumerate().skip(1) {
+		if row.points == last_score {
+			placed.push(Placement {
+				row,
+				placement: idx + 1,
+				placement_tie: last_placed,
+				placement_first_of_tie: false,
+			});
+		} else {
+			last_score = row.points;
+			last_placed = idx + 1;
+
+			placed.push(Placement {
+				row,
+				placement: idx + 1,
+				placement_tie: last_placed,
+				placement_first_of_tie: true,
+			});
+		}
+	}
+
+	placed
+}
+
+/// Takes a `&str` and extracts all mentions of users, sticking them in a Vec
+pub fn extract_user_mentions(mut raw: &str) -> Vec<Id<UserMarker>> {
+	let mut ret = vec![];
+
+	loop {
+		let Some(start) = raw.find("<@") else {
+			break;
+		};
+
+		let Some(end) = raw.find('>') else {
+			break;
+		};
+
+		let id_str = &raw[start + 2..end];
+		raw = &raw[end + 1..];
+
+		let Ok(id) = u64::from_str_radix(id_str, 10) else {
+			continue;
+		};
+
+		ret.push(Id::new(id));
+	}
+
+	ret
+}