diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs index 7d118c6..8440b48 100644 --- a/src/graphql/queries.rs +++ b/src/graphql/queries.rs @@ -146,4 +146,73 @@ 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? + .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> { + 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?; + + // 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()) + .map(|s| s.to_string()) + .collect(); + Ok(roles) + } } diff --git a/src/main.rs b/src/main.rs index 2e583d0..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::non_privileged() + | GatewayIntents::MESSAGE_CONTENT + | GatewayIntents::GUILD_MEMBERS, ) .framework(build_framework( config.owner_id, @@ -160,6 +162,46 @@ 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: 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 = 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() + } + }; + + for role in roles { + if let Ok(role_id) = role.parse::() { + let _ = new_member.add_role(ctx, RoleId::new(role_id)).await; + } + } + } _ => {} }