about summary refs log tree commit diff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs208
1 files changed, 172 insertions, 36 deletions
diff --git a/src/main.rs b/src/main.rs
index a10cf23..98e7a5f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
 use std::{env, error::Error, sync::Arc};
 
-use database::{BoardRow, Database, Error as DbError};
+use database::{BoardRow, Database, Error as DbError, PermissionSetting};
 use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
 use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
 use twilight_http::{Client as HttpClient, client::InteractionClient};
@@ -9,7 +9,7 @@ use twilight_model::{
 		command::{Command, CommandType},
 		interaction::{
 			InteractionData,
-			application_command::{CommandData, CommandOptionValue},
+			application_command::{CommandData, CommandDataOption, CommandOptionValue},
 		},
 	},
 	channel::message::MessageFlags,
@@ -22,7 +22,10 @@ use twilight_model::{
 };
 use twilight_util::builder::{
 	InteractionResponseDataBuilder,
-	command::{CommandBuilder, IntegerBuilder, NumberBuilder, StringBuilder, UserBuilder},
+	command::{
+		CommandBuilder, IntegerBuilder, MentionableBuilder, NumberBuilder, RoleBuilder,
+		StringBuilder, SubCommandBuilder, SubCommandGroupBuilder, UserBuilder,
+	},
 	embed::{EmbedBuilder, EmbedFieldBuilder},
 };
 
@@ -30,6 +33,30 @@ mod database;
 
 const APP_ID: u64 = 1363055126264283136;
 
+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()
+	};
+}
+
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
 	dotenv::dotenv()?;
@@ -104,7 +131,8 @@ async fn handle_event(
 				let command = match cmd.name.as_str() {
 					"leaderboard" => Commands::Leaderboard,
 					"points" => Commands::Points,
-					_ => panic!("meow"),
+					"permission" => Commands::Permission,
+					_ => panic!("'{}' is not a command", cmd.name),
 				};
 
 				command_handler(&db, http, guild, &create, command, cmd).await;
@@ -136,7 +164,22 @@ async fn commands() -> Vec<Command> {
 		.unwrap()
 		.build();
 
-	vec![leaderboard, points]
+	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(
@@ -147,47 +190,50 @@ async fn command_handler(
 	command: Commands,
 	command_data: &CommandData,
 ) {
-	match command {
-		Commands::Leaderboard => {
-			let data = get_leaderboard(&db, guild);
-
-			let response = InteractionResponse {
-				kind: InteractionResponseType::ChannelMessageWithSource,
-				data: Some(data),
-			};
+	// 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();
+	}
 
-			let iclient = http.interaction(Id::new(APP_ID));
-			iclient
-				.create_response(create.id, &create.token, &response)
-				.await
-				.unwrap();
-		}
-		Commands::Points => {
-			let data = add_points(&db, &http, guild, command_data).await;
+	// 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,
+	};
 
-			let response = InteractionResponse {
+	// Finally, send back the response
+	http.interaction(Id::new(APP_ID))
+		.create_response(
+			create.id,
+			&create.token,
+			&InteractionResponse {
 				kind: InteractionResponseType::ChannelMessageWithSource,
 				data: Some(data),
-			};
-
-			let iclient = http.interaction(Id::new(APP_ID));
-			iclient
-				.create_response(create.id, &create.token, &response)
-				.await
-				.unwrap();
-		}
-	}
+			},
+		)
+		.await
+		.unwrap();
 }
 
 fn get_leaderboard(db: &Database, guild: Id<GuildMarker>) -> InteractionResponseData {
 	let board = db.get_leaderboard(guild.get());
 
 	match board {
-		Err(DbError::TableNotExist) => InteractionResponseDataBuilder::new()
-			.content(
-				"❌ No leaderboard exists for this server! Create one by giving someone points.",
-			)
-			.build(),
+		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
@@ -206,6 +252,7 @@ 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()
@@ -216,6 +263,7 @@ async fn add_points(
 	db: &Database,
 	http: &HttpClient,
 	guild: Id<GuildMarker>,
+	create: &InteractionCreate,
 	data: &CommandData,
 ) -> InteractionResponseData {
 	let mut points = None;
@@ -245,10 +293,32 @@ 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();
 	}
 
+	let settings = 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 db.give_user_points(guild.get(), user.get(), points) {
 			Err(DbError::UserNotExist) => {
@@ -278,9 +348,75 @@ async fn add_points(
 		.build()
 }
 
+async fn permission(
+	db: &Database,
+	http: &HttpClient,
+	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(db, guild),
+			"role" => permission_role(db, guild, opt.value.clone()),
+			_ => panic!(),
+		};
+	}
+
+	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)
+			.unwrap();
+	}
+
+	success!("Permission for leaderboard changed to none. Anyone may now set the score.")
+}
+
+fn permission_role(
+	db: &Database,
+	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!()
+	};
+
+	// Make sure we have a leaderboard to work on
+	if db.get_leaderboard(guild.get()).is_err() {
+		db.create_leaderboard(guild.get()).unwrap();
+	}
+
+	db.set_leaderboard_permission(guild.get(), PermissionSetting::RoleRequired)
+		.unwrap();
+	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>> {