about summary refs log tree commit diff
path: root/src/main.rs
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2025-04-19 06:36:34 -0500
committergennyble <gen@nyble.dev>2025-04-19 06:36:34 -0500
commit0fbebbe9af03f6880b3c0d585b757f7809aa27e7 (patch)
tree0e446b6a9b9c7516ef1893a2fe5d18fa1511941e /src/main.rs
downloadleaberblord-0fbebbe9af03f6880b3c0d585b757f7809aa27e7.tar.gz
leaberblord-0fbebbe9af03f6880b3c0d585b757f7809aa27e7.zip
works
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs293
1 files changed, 293 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e590460
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,293 @@
+use std::{env, error::Error, sync::Arc};
+
+use database::{BoardRow, Database, Error as DbError};
+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},
+		},
+	},
+	channel::message::MessageFlags,
+	gateway::payload::incoming::InteractionCreate,
+	http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType},
+	id::{
+		Id,
+		marker::{GuildMarker, InteractionMarker, UserMarker},
+	},
+};
+use twilight_util::builder::{
+	InteractionResponseDataBuilder,
+	command::{CommandBuilder, IntegerBuilder, NumberBuilder, StringBuilder, UserBuilder},
+};
+
+mod database;
+
+const APP_ID: u64 = 1363055126264283136;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+	dotenv::dotenv()?;
+
+	let db = Arc::new(Database::new("kindbloot.db"));
+	db.create_tables();
+
+	let mut shard = Shard::new(
+		ShardId::ONE,
+		env::var("DISCORD_TOKEN")?,
+		Intents::GUILD_MESSAGES,
+	);
+
+	let http = Arc::new(HttpClient::new(env::var("DISCORD_TOKEN")?));
+
+	let mut cache = DefaultInMemoryCache::builder()
+		.resource_types(ResourceType::MESSAGE)
+		.resource_types(ResourceType::MEMBER)
+		.build();
+
+	let iclient = http.interaction(Id::new(APP_ID));
+	let commands_vec = commands(&iclient).await;
+	iclient.set_global_commands(&commands_vec).await.unwrap();
+
+	println!("Set commands!");
+
+	let cmd_ap = commands_vec[1].clone();
+	let cmd_ap_id = cmd_ap.id;
+
+	let cmd_ap_global = iclient
+		.global_commands()
+		.await
+		.unwrap()
+		.models()
+		.await
+		.unwrap();
+
+	for cmd in cmd_ap_global {
+		println!("got appoints global? {}: {}", cmd.name, cmd.description);
+	}
+
+	while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
+		let Ok(event) = item else {
+			eprintln!("error receiving event");
+			continue;
+		};
+
+		cache.update(&event);
+
+		tokio::spawn(handle_event(event, Arc::clone(&http), Arc::clone(&db)));
+	}
+
+	Ok(())
+}
+
+async fn handle_event(
+	event: Event,
+	http: Arc<HttpClient>,
+	db: Arc<Database>,
+) -> Result<(), Box<dyn Error + Send + Sync>> {
+	match event {
+		Event::InteractionCreate(create) => {
+			let data = create.data.as_ref().unwrap();
+
+			if let InteractionData::ApplicationCommand(cmd) = data {
+				let guild = create.guild.as_ref().unwrap().id.unwrap();
+
+				let command = match cmd.name.as_str() {
+					"leaderboard" => Commands::Leaderboard,
+					"addpoints" => Commands::AddPoints,
+					_ => panic!("meow"),
+				};
+
+				command_handler(&db, http, guild, &create, command, cmd).await;
+			}
+		}
+		Event::GatewayClose(close) => {
+			println!("GatewayClose - {close:?}")
+		}
+		_ => (),
+	}
+
+	Ok(())
+}
+
+async fn commands(ic: &InteractionClient<'_>) -> Vec<Command> {
+	let leaderboard = CommandBuilder::new(
+		"leaderboard",
+		"View the server leaderboard",
+		CommandType::ChatInput,
+	)
+	.build();
+
+	let addpoints = CommandBuilder::new(
+		"addpoints",
+		"Give someone, or multiple people, points!",
+		CommandType::ChatInput,
+	)
+	.option(IntegerBuilder::new("points", "number of points").required(true))
+	.option(StringBuilder::new("users", "person to give the points").required(true))
+	.validate()
+	.unwrap()
+	.build();
+
+	vec![leaderboard, addpoints]
+}
+
+async fn command_handler(
+	db: &Database,
+	http: Arc<HttpClient>,
+	guild: Id<GuildMarker>,
+	create: &InteractionCreate,
+	command: Commands,
+	command_data: &CommandData,
+) {
+	match command {
+		Commands::Leaderboard => {
+			let data = get_leaderboard(&db, guild);
+
+			let response = 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();
+		}
+		Commands::AddPoints => {
+			let data = add_points(&db, &http, guild, command_data).await;
+
+			let response = 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();
+		}
+	}
+}
+
+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 a leaderboard by giving someone points!",
+			)
+			.build(),
+		Err(DbError::UserNotExist) => unreachable!(),
+		Ok(data) => {
+			let str = data
+				.into_iter()
+				.map(|br| format!("{}: {}", br.user_handle, br.points))
+				.collect::<Vec<String>>()
+				.join("\n");
+
+			InteractionResponseDataBuilder::new().content(str).build()
+		}
+	}
+}
+
+macro_rules! aumau {
+	($meow:expr) => {
+		$meow.await.unwrap().model().await.unwrap()
+	};
+}
+
+async fn add_points(
+	db: &Database,
+	http: &HttpClient,
+	guild: Id<GuildMarker>,
+	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 db.get_leaderboard(guild.get()).is_err() {
+		db.create_leaderboard(guild.get()).unwrap();
+	}
+
+	for user in &users {
+		match db.give_user_points(guild.get(), user.get(), points.unwrap()) {
+			Err(DbError::UserNotExist) => {
+				let member = aumau!(http.guild_member(guild, *user));
+				let row = BoardRow {
+					user_id: user.get(),
+					user_handle: member.user.name,
+					user_nickname: member.nick.or(member.user.global_name),
+					points: points.unwrap(),
+				};
+				db.add_user_to_leaderboard(guild.get(), row).unwrap();
+			}
+			_ => (),
+		}
+	}
+
+	let meow = users
+		.into_iter()
+		.map(|u| format!("<@{u}>"))
+		.collect::<Vec<String>>()
+		.join(", ");
+
+	InteractionResponseDataBuilder::new()
+		.content(format!("added {} points to {meow}", points.unwrap()))
+		.build()
+}
+
+enum Commands {
+	Leaderboard,
+	AddPoints,
+}
+
+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
+}