From d2aa7bb9eb825e60cd46397fadea16ad623f3da7 Mon Sep 17 00:00:00 2001 From: Swayam Agrahari Date: Thu, 12 Mar 2026 23:46:58 +0530 Subject: [PATCH 1/2] feat: add member role management for guild events --- src/graphql/queries.rs | 59 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 33 ++++++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs index 7d118c6..fca9c94 100644 --- a/src/graphql/queries.rs +++ b/src/graphql/queries.rs @@ -146,4 +146,63 @@ impl GraphQLClient { ); Ok(attendance) } + + pub async fn save_member_roles( &self, discord_id: String, roles: Vec,) -> anyhow::Result<()> { + + let query = r#" + mutation($discordId: String!, $roles: [String!]!) { + saveMemberRoles(discordId: $discordId, roles: $roles) + }"#; + + let variables = serde_json::json!({ + "discordId": discord_id, + "roles": roles + }); + + let res = self.http() + .post(self.root_url()) + .bearer_auth(self.api_key()) + .json(&serde_json::json!({ + "query": query, + "variables": variables + })) + .send() + .await?; + Ok(()) + } + + pub async fn get_member_roles( &self, discord_id: String,) -> anyhow::Result> { + let query = r#" + query($discordId: String!) { + memberRoles(discordId: $discordId) + }"#; + + let variables = serde_json::json!({ + "discordId": discord_id + }); + + let response = self.http() + .post(self.root_url()) + .bearer_auth(self.api_key()) + .json(&serde_json::json!({ + "query": query, + "variables": variables + })) + .send() + .await? + .json::() + .await?; + + //remove @everyone role, a defult role that every member has and is not useful for our purposes. It has the same ID as the guild, so we can filter it out by comparing with the guild ID. + let roles = response["data"]["memberRoles"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .filter_map(|v| v.as_str()) + .map(|s| s.to_string()) + .collect(); + + Ok(roles) + } + } diff --git a/src/main.rs b/src/main.rs index 2e583d0..27d7d37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,7 +107,7 @@ fn prepare_data(config: &Config, reload_handle: ReloadHandle) -> Data { async fn build_client(config: &Config, data: Data) -> Result { ClientBuilder::new( config.discord_token.clone(), - GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT, + GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS, ) .framework(build_framework( config.owner_id, @@ -160,6 +160,37 @@ async fn event_handler( FullEvent::ReactionRemove { removed_reaction } => { handle_reaction(ctx, removed_reaction, data, false).await?; } + + FullEvent::GuildMemberRemoval { guild_id: _, user, member_data_if_available } => { + + if let Some(member) = member_data_if_available { + + let roles: Vec = member.roles + .iter() + .filter(|r| r.get() != member.guild_id.get()) + .map(|r| r.get().to_string()) + .collect(); + + if !roles.is_empty() { + data.graphql_client + .save_member_roles(user.id.to_string(), roles) + .await?; + } + } + } + FullEvent::GuildMemberAddition { new_member } => { + + let roles = data + .graphql_client + .get_member_roles(new_member.user.id.to_string()) + .await?; + + for role in roles { + if let Ok(role_id) = role.parse::() { + let _ = new_member.add_role(ctx, RoleId::new(role_id)).await; + } + } + } _ => {} } From 070aea0600b885588aad49992f5c56452c2b2007 Mon Sep 17 00:00:00 2001 From: Swayam Agrahari Date: Sat, 14 Mar 2026 11:42:20 +0530 Subject: [PATCH 2/2] feat: enhance member role management with error handling and code formatting --- src/graphql/queries.rs | 44 ++++++++++++++++++++++--------------- src/main.rs | 49 ++++++++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs index fca9c94..8440b48 100644 --- a/src/graphql/queries.rs +++ b/src/graphql/queries.rs @@ -147,8 +147,11 @@ impl GraphQLClient { Ok(attendance) } - pub async fn save_member_roles( &self, discord_id: String, roles: Vec,) -> anyhow::Result<()> { - + pub async fn save_member_roles( + &self, + discord_id: String, + roles: Vec, + ) -> anyhow::Result<()> { let query = r#" mutation($discordId: String!, $roles: [String!]!) { saveMemberRoles(discordId: $discordId, roles: $roles) @@ -159,19 +162,27 @@ impl GraphQLClient { "roles": roles }); - let res = self.http() - .post(self.root_url()) - .bearer_auth(self.api_key()) - .json(&serde_json::json!({ - "query": query, - "variables": variables - })) - .send() - .await?; + let res = self + .http() + .post(self.root_url()) + .bearer_auth(self.api_key()) + .json(&serde_json::json!({ + "query": query, + "variables": variables + })) + .send() + .await? + .error_for_status()?; + + let response: serde_json::Value = res.json().await?; + + if response.get("errors").is_some() { + anyhow::bail!("GraphQL error: {:?}", response["errors"]); + } Ok(()) } - pub async fn get_member_roles( &self, discord_id: String,) -> anyhow::Result> { + pub async fn get_member_roles(&self, discord_id: String) -> anyhow::Result> { let query = r#" query($discordId: String!) { memberRoles(discordId: $discordId) @@ -181,7 +192,8 @@ impl GraphQLClient { "discordId": discord_id }); - let response = self.http() + let response = self + .http() .post(self.root_url()) .bearer_auth(self.api_key()) .json(&serde_json::json!({ @@ -193,16 +205,14 @@ impl GraphQLClient { .json::() .await?; - //remove @everyone role, a defult role that every member has and is not useful for our purposes. It has the same ID as the guild, so we can filter it out by comparing with the guild ID. + // Collect roles returned by the API as strings; any filtering (e.g. removing @everyone) is handled by the caller. let roles = response["data"]["memberRoles"] .as_array() .unwrap_or(&vec![]) .iter() - .filter_map(|v| v.as_str()) + .filter_map(|v| v.as_str()) .map(|s| s.to_string()) .collect(); - Ok(roles) } - } diff --git a/src/main.rs b/src/main.rs index 27d7d37..79fa012 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,7 +107,9 @@ fn prepare_data(config: &Config, reload_handle: ReloadHandle) -> Data { async fn build_client(config: &Config, data: Data) -> Result { ClientBuilder::new( config.discord_token.clone(), - GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS, + GatewayIntents::non_privileged() + | GatewayIntents::MESSAGE_CONTENT + | GatewayIntents::GUILD_MEMBERS, ) .framework(build_framework( config.owner_id, @@ -161,29 +163,38 @@ async fn event_handler( handle_reaction(ctx, removed_reaction, data, false).await?; } - FullEvent::GuildMemberRemoval { guild_id: _, user, member_data_if_available } => { - - if let Some(member) = member_data_if_available { - - let roles: Vec = member.roles - .iter() - .filter(|r| r.get() != member.guild_id.get()) - .map(|r| r.get().to_string()) - .collect(); - - if !roles.is_empty() { - data.graphql_client - .save_member_roles(user.id.to_string(), roles) - .await?; - } + FullEvent::GuildMemberRemoval { + guild_id: _, + user, + member_data_if_available: Some(member), + } => { + let roles: Vec = member + .roles + .iter() + .filter(|r| r.get() != member.guild_id.get()) + .map(|r| r.get().to_string()) + .collect(); + + if let Err(e) = data + .graphql_client + .save_member_roles(user.id.to_string(), roles) + .await + { + println!("Failed to save roles: {:?}", e); } } FullEvent::GuildMemberAddition { new_member } => { - - let roles = data + let roles = match data .graphql_client .get_member_roles(new_member.user.id.to_string()) - .await?; + .await + { + Ok(r) => r, + Err(e) => { + println!("Failed to fetch roles for {}: {:?}", new_member.user.id, e); + Vec::new() + } + }; for role in roles { if let Ok(role_id) = role.parse::() {