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.
This commit is contained in:
162
crates/quicprochat-server/src/v2_handlers/group.rs
Normal file
162
crates/quicprochat-server/src/v2_handlers/group.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
//! 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()))
|
||||
}
|
||||
Reference in New Issue
Block a user