//! Group management handlers — remove member, update metadata, list members, rotate keys. use std::sync::Arc; use bytes::Bytes; use prost::Message; use quicprochat_proto::qpc::v1; use quicprochat_rpc::error::RpcStatus; use quicprochat_rpc::method::{HandlerResult, RequestContext}; use crate::domain::groups::GroupService; use crate::domain::types::{ListGroupMembersReq, UpdateGroupMetadataReq}; use super::{domain_err, require_auth, ServerState}; /// Handle RemoveMember (410): track member removal server-side. /// /// Note: actual MLS removal (Remove proposal + Commit) is done client-side /// via the SDK. This handler records the membership change on the server. pub async fn handle_remove_member( state: Arc, ctx: RequestContext, ) -> HandlerResult { let identity_key = match require_auth(&state, &ctx) { Ok(ik) => ik, Err(e) => return e, }; let req = match v1::RemoveMemberRequest::decode(ctx.payload) { Ok(r) => r, Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")), }; if req.group_id.is_empty() || req.member_identity_key.is_empty() { return HandlerResult::err( RpcStatus::BadRequest, "group_id and member_identity_key required", ); } let svc = GroupService { store: Arc::clone(&state.store), }; match svc.remove_member(&req.group_id, &req.member_identity_key) { Ok(_) => { let _ = identity_key; // caller is authorized; removal tracked let proto = v1::RemoveMemberResponse { commit: Vec::new(), // commit is generated client-side }; HandlerResult::ok(Bytes::from(proto.encode_to_vec())) } Err(e) => domain_err(e), } } /// Handle UpdateGroupMetadata (411): store group name, description, avatar. pub async fn handle_update_group_metadata( state: Arc, ctx: RequestContext, ) -> HandlerResult { let identity_key = match require_auth(&state, &ctx) { Ok(ik) => ik, Err(e) => return e, }; let req = match v1::UpdateGroupMetadataRequest::decode(ctx.payload) { Ok(r) => r, Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")), }; let svc = GroupService { store: Arc::clone(&state.store), }; let domain_req = UpdateGroupMetadataReq { group_id: req.group_id, name: req.name, description: req.description, avatar_hash: req.avatar_hash, }; match svc.update_metadata(domain_req, &identity_key) { Ok(()) => { let proto = v1::UpdateGroupMetadataResponse { success: true }; HandlerResult::ok(Bytes::from(proto.encode_to_vec())) } Err(e) => domain_err(e), } } /// Handle ListGroupMembers (412): return member list with resolved usernames. pub async fn handle_list_group_members( state: Arc, ctx: RequestContext, ) -> HandlerResult { let _identity_key = match require_auth(&state, &ctx) { Ok(ik) => ik, Err(e) => return e, }; let req = match v1::ListGroupMembersRequest::decode(ctx.payload) { Ok(r) => r, Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")), }; let svc = GroupService { store: Arc::clone(&state.store), }; let domain_req = ListGroupMembersReq { group_id: req.group_id, }; match svc.list_members(domain_req) { Ok(resp) => { let proto = v1::ListGroupMembersResponse { members: resp .members .into_iter() .map(|m| v1::GroupMemberInfo { identity_key: m.identity_key, username: m.username, joined_at: m.joined_at, }) .collect(), }; HandlerResult::ok(Bytes::from(proto.encode_to_vec())) } Err(e) => domain_err(e), } } /// Handle RotateKeys (413): acknowledge key rotation. /// /// Actual MLS key rotation (Update proposal + Commit) is done client-side. /// This handler exists for server-side tracking and future rate limiting. pub async fn handle_rotate_keys( state: Arc, ctx: RequestContext, ) -> HandlerResult { let _identity_key = match require_auth(&state, &ctx) { Ok(ik) => ik, Err(e) => return e, }; let req = match v1::RotateKeysRequest::decode(ctx.payload) { Ok(r) => r, Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")), }; if req.group_id.is_empty() { return HandlerResult::err(RpcStatus::BadRequest, "group_id required"); } // Key rotation is handled entirely client-side in MLS. // This endpoint is for server-side auditing and future rate limiting. let proto = v1::RotateKeysResponse { commit: Vec::new(), // commit is generated client-side }; HandlerResult::ok(Bytes::from(proto.encode_to_vec())) }