diff --git a/config.example.toml b/config.example.toml index d67fd4a..a9d1e2c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -4,7 +4,7 @@ secret = "" dev_secret = "" dev_routes = ["/v2/player"] exposed_secret = "" -exposed_routes = ["/v2/patreon", "/v2/discord/user", "/v2/discord/member"] +exposed_routes = ["/v2/patreon", "/v2/patreon/patrons", "/v2/discord/user", "/v2/discord/member"] cli_colors = true log_level = "normal" diff --git a/src/byond/status.rs b/src/byond/status.rs index 27efd07..feca6af 100644 --- a/src/byond/status.rs +++ b/src/byond/status.rs @@ -168,7 +168,9 @@ pub async fn status(address: &str) -> super::Result { "shuttle_timer" => status.shuttle_timer = value.parse()?, _ => { #[cfg(debug_assertions)] - tracing::warn!("Status topic responsed with unknown param: {key} = {value} ({address})"); + tracing::warn!( + "Status topic responsed with unknown param: {key} = {value} ({address})" + ); } } } diff --git a/src/database/verify.rs b/src/database/verify.rs index a8a0dc5..e64a361 100644 --- a/src/database/verify.rs +++ b/src/database/verify.rs @@ -109,7 +109,7 @@ pub async fn unverify_discord( unreachable!() } -async fn ckey_by_discord_id( +pub async fn ckey_by_discord_id( discord_id: &str, connection: &mut PoolConnection, ) -> Result { diff --git a/src/http/discord.rs b/src/http/discord.rs index d9275f9..8ae2b61 100644 --- a/src/http/discord.rs +++ b/src/http/discord.rs @@ -25,10 +25,8 @@ pub struct User { pub async fn get_user(id: i64, token: &str) -> Result { let _lock = DISCORD_API_LOCK.lock().await; - let url = format!("https://discord.com/api/v10/users/{id}"); - let response = REQWEST_CLIENT - .get(url) + .get(format!("https://discord.com/api/v10/users/{id}")) .header("Authorization", format!("Bot {token}")) .send() .await? @@ -45,7 +43,9 @@ pub async fn get_user(id: i64, token: &str) -> Result { #[derive(Debug, Serialize, Deserialize)] pub struct GuildMember { - pub roles: HashSet, // other fields are not required for now (https://discord.com/developers/docs/resources/guild#guild-member-object) + // https://discord.com/developers/docs/resources/guild#guild-member-object + pub roles: HashSet, + pub user: User, } pub async fn get_guild_member( @@ -55,10 +55,10 @@ pub async fn get_guild_member( ) -> Result { let _lock = DISCORD_API_LOCK.lock().await; - let url = format!("https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}"); - let response = REQWEST_CLIENT - .get(url) + .get(format!( + "https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}" + )) .header("Authorization", format!("Bot {token}")) .send() .await? @@ -72,3 +72,42 @@ pub async fn get_guild_member( Ok(member) } + +pub async fn search_members( + guild_id: i64, + query: String, + token: &str, +) -> Result, Error> { + let _lock = DISCORD_API_LOCK.lock().await; + + let response = REQWEST_CLIENT + .post(format!( + "https://discord.com/api/v10/guilds/{guild_id}/members-search" + )) + .header("Authorization", format!("Bot {token}")) + .header("Content-Type", "application/json") + .body(query) + .send() + .await? + .text() + .await?; + + #[derive(Deserialize)] + struct Response { + pub members: Vec, + } + + #[derive(Deserialize)] + struct ResponseMember { + pub member: GuildMember, + } + + let Ok(response) = serde_json::from_str::(&response) else { + let error: ErrorMessage = serde_json::from_str(&response)?; + return Err(Error::Discord(error.code)); + }; + + let members = response.members.into_iter().map(|m| m.member).collect(); + + Ok(members) +} diff --git a/src/routes/v2/mod.rs b/src/routes/v2/mod.rs index 7b2b90c..6c27dfb 100644 --- a/src/routes/v2/mod.rs +++ b/src/routes/v2/mod.rs @@ -15,6 +15,7 @@ pub fn mount(rocket: Rocket) -> Rocket { "/v2", routes![ patreon::index, + patreon::patrons, player::index, player::ban, player::characters, diff --git a/src/routes/v2/patreon.rs b/src/routes/v2/patreon.rs index 8b75147..444afcb 100644 --- a/src/routes/v2/patreon.rs +++ b/src/routes/v2/patreon.rs @@ -5,7 +5,10 @@ use sqlx::MySqlPool; use crate::{ config::{self, Config}, database::{error::Error, *}, - http::{self, discord::get_guild_member}, + http::{ + self, + discord::{get_guild_member, search_members}, + }, Database, }; @@ -45,3 +48,41 @@ async fn is_patron(ckey: &str, pool: &MySqlPool, discord: &config::Discord) -> R Ok(member.roles.contains(&discord.patreon_role.to_string())) } + +#[get("/patreon/patrons")] +pub async fn patrons( + database: &State, + config: &State, + _api_key: ApiKey, +) -> Result, Status> { + let Ok(patrons) = get_patrons(&database.pool, &config.discord).await else { + return Err(Status::InternalServerError); + }; + + Ok(Json::Ok(json!({ "patrons": patrons }))) +} + +async fn get_patrons(pool: &MySqlPool, discord: &config::Discord) -> Result, Error> { + let mut connection = pool.acquire().await?; + + let query = format!( + "{{\"or_query\":{{}},\"and_query\":{{\"role_ids\":{{\"and_query\":[\"{}\"]}}}},\"limit\":1000}}", + discord.patreon_role + ); + + let members = search_members(discord.guild, query, &discord.token).await?; + + let mut ckeys = Vec::new(); + + for member in members { + match ckey_by_discord_id(&member.user.id, &mut connection).await { + Ok(ckey) => ckeys.push(ckey), + Err(Error::NotLinked) => continue, + Err(e) => return Err(e), + } + } + + connection.close().await?; + + Ok(ckeys) +}