about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2025-04-20 12:32:38 -0500
committergennyble <gen@nyble.dev>2025-04-20 12:32:38 -0500
commitb03793553fe74dd0fcd42bf7b480a9062e804cd5 (patch)
treebf8e581058dfeb93db7f1a77d094917c13e48901
parent668484dcc06c5ad36aa840609dab383fa17aac88 (diff)
downloadleaberblord-b03793553fe74dd0fcd42bf7b480a9062e804cd5.tar.gz
leaberblord-b03793553fe74dd0fcd42bf7b480a9062e804cd5.zip
emboard import main
-rw-r--r--Cargo.lock56
-rw-r--r--Cargo.toml5
-rw-r--r--src/brain.rs125
-rw-r--r--src/import.rs54
-rw-r--r--src/import/mod.rs125
-rw-r--r--src/main.rs130
6 files changed, 351 insertions, 144 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9d048cd..1ea6df6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -488,6 +488,7 @@ dependencies = [
  "twilight-http",
  "twilight-model",
  "twilight-util",
+ "ureq",
 ]
 
 [[package]]
@@ -726,6 +727,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 +749,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 +1267,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 +1343,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..149bcd7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,3 +16,8 @@ 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 }
+
+[features]
+default = ["emboard-direct"]
+emboard-direct = ["ureq"]
diff --git a/src/brain.rs b/src/brain.rs
new file mode 100644
index 0000000..0ec6a59
--- /dev/null
+++ b/src/brain.rs
@@ -0,0 +1,125 @@
+use crate::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},
+	},
+};
+const PROD_APP_ID: u64 = 1363055126264283136;
+const DEV_APP_ID: u64 = 1363494986699771984;
+const APP_ID: u64 = PROD_APP_ID;
+
+// 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 {
+	pub id: Id<UserMarker>,
+	pub handle: String,
+	pub global_name: Option<String>,
+	pub nick: Option<String>,
+}
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..02429d8
--- /dev/null
+++ b/src/import/mod.rs
@@ -0,0 +1,125 @@
+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();
+		}
+	}
+}
+
+#[cfg(feature = "emboard-direct")]
+pub mod emboard_direct {
+	use std::{sync::Arc, time::Duration};
+
+	use twilight_model::{
+		gateway::payload::incoming::InteractionCreate,
+		http::interaction::{InteractionResponse, InteractionResponseType},
+		id::{Id, marker::GuildMarker},
+	};
+	use twilight_util::builder::InteractionResponseDataBuilder;
+	use ureq::{Agent, RequestBuilder, config::Config, typestate::WithoutBody};
+
+	use crate::brain::Brain;
+
+	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 = crate::get_leaderboard(brain.clone(), guild_id)
+					.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..774d9e6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
 use std::{env, error::Error, path::PathBuf, sync::Arc};
 
+use brain::Brain;
 use confindent::Confindent;
 use database::{BoardRow, Database, Error as DbError, PermissionSetting};
 use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
@@ -26,12 +27,13 @@ use twilight_util::builder::{
 	embed::EmbedBuilder,
 };
 
+mod brain;
 mod database;
 mod import;
 
 const PROD_APP_ID: u64 = 1363055126264283136;
 const DEV_APP_ID: u64 = 1363494986699771984;
-const APP_ID: u64 = PROD_APP_ID;
+const APP_ID: u64 = DEV_APP_ID;
 
 macro_rules! bail {
 	($msg:expr) => {
@@ -57,91 +59,6 @@ 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();
@@ -212,7 +129,7 @@ async fn main() -> anyhow::Result<()> {
 		);
 	}
 
-	let brain = Arc::new(Brain { db, http, cache });
+	let brain = Arc::new(Brain::new(db, http, cache));
 
 	while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
 		let Ok(event) = item else {
@@ -220,7 +137,7 @@ async fn main() -> anyhow::Result<()> {
 			continue;
 		};
 
-		brain.cache.update(&event);
+		brain.update_cache(&event);
 
 		tokio::spawn(handle_event(event, Arc::clone(&brain)));
 	}
@@ -249,6 +166,7 @@ async fn handle_event(event: Event, brain: Arc<Brain>) -> Result<(), Box<dyn Err
 					"leaderboard" => Commands::Leaderboard,
 					"points" => Commands::Points,
 					"permission" => Commands::Permission,
+					"import" => Commands::Import,
 					_ => panic!("'{}' is not a command", cmd.name),
 				};
 
@@ -296,7 +214,14 @@ async fn commands() -> Vec<Command> {
 	))
 	.build();
 
-	vec![leaderboard, points, permission]
+	let import = CommandBuilder::new(
+		"import",
+		"import this server's emboard leaderboard by adding the points in emboard, to leaberblord's",
+		CommandType::ChatInput,
+	)
+	.build();
+
+	vec![leaderboard, points, permission, import]
 }
 
 async fn command_handler(
@@ -311,6 +236,10 @@ async fn command_handler(
 		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,
+		Commands::Import => {
+			import_cmd(brain.clone(), guild, create, command_data).await;
+			return;
+		}
 	};
 
 	// Finally, send back the response
@@ -442,7 +371,7 @@ async fn permission(
 	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");
+		bail!("You must have the Manage Server permission to perform this action");
 	}
 
 	for opt in &data.options {
@@ -499,10 +428,31 @@ fn permission_role(
 	))
 }
 
+async fn import_cmd(
+	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;
+	}
+}
+
 enum Commands {
 	Leaderboard,
 	Points,
 	Permission,
+	Import,
 }
 
 fn extract_mentions(mut raw: &str) -> Vec<Id<UserMarker>> {