From d2aa7bb9eb825e60cd46397fadea16ad623f3da7 Mon Sep 17 00:00:00 2001 From: Swayam Agrahari Date: Thu, 12 Mar 2026 23:46:58 +0530 Subject: [PATCH 1/3] 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/3] 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::() { From c83206e49198807e3fc7a65c65fe4b6f19ba5429 Mon Sep 17 00:00:00 2001 From: Swayam Agrahari Date: Sat, 21 Mar 2026 11:57:30 +0530 Subject: [PATCH 3/3] feat: update get_member_roles to return existence status and roles for improved member role management --- src/graphql/queries.rs | 18 ++++++++++++++---- src/main.rs | 19 +++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs index 8440b48..4a737fc 100644 --- a/src/graphql/queries.rs +++ b/src/graphql/queries.rs @@ -182,10 +182,16 @@ impl GraphQLClient { 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<(bool, Vec)> { let query = r#" query($discordId: String!) { - memberRoles(discordId: $discordId) + memberRoles(discordId: $discordId){ + exists + roles + } }"#; let variables = serde_json::json!({ @@ -206,13 +212,17 @@ impl GraphQLClient { .await?; // Collect roles returned by the API as strings; any filtering (e.g. removing @everyone) is handled by the caller. - let roles = response["data"]["memberRoles"] + let data = &response["data"]["memberRoles"]; + + let exists = data["exists"].as_bool().unwrap_or(false); + + let roles = data["roles"] .as_array() .unwrap_or(&vec![]) .iter() .filter_map(|v| v.as_str()) .map(|s| s.to_string()) .collect(); - Ok(roles) + Ok((exists, roles)) } } diff --git a/src/main.rs b/src/main.rs index 79fa012..ca94b3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -184,21 +184,28 @@ async fn event_handler( } } FullEvent::GuildMemberAddition { new_member } => { - let roles = match data + let (exists, roles) = match data .graphql_client .get_member_roles(new_member.user.id.to_string()) .await { Ok(r) => r, Err(e) => { - println!("Failed to fetch roles for {}: {:?}", new_member.user.id, e); - Vec::new() + println!("Failed to fetch roles: {:?}", e); + (false, vec![]) } }; - for role in roles { - if let Ok(role_id) = role.parse::() { - let _ = new_member.add_role(ctx, RoleId::new(role_id)).await; + if let Ok(member) = new_member.guild_id.member(ctx, new_member.user.id).await { + // restore roles + for role in roles { + if let Ok(role_id) = role.parse::() { + let _ = member.add_role(ctx, RoleId::new(role_id)).await; + } + } + // probated role + if exists { + let _ = member.add_role(ctx, RoleId::new(1484798446228475905)).await; } } }