Files
quicproquo/crates/quicprochat-server/src/v2_handlers/group.rs
Christian Nennemann a710037dde chore: rename quicproquo → quicprochat in Rust workspace
Rename all crate directories, package names, binary names, proto
package/module paths, ALPN strings, env var prefixes, config filenames,
mDNS service names, and plugin ABI symbols from quicproquo/qpq to
quicprochat/qpc.
2026-03-21 19:14:06 +01:00

163 lines
5.0 KiB
Rust

//! 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<ServerState>,
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<ServerState>,
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<ServerState>,
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<ServerState>,
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()))
}