v0.0.9: Group management — leave, kick, members
Server: - POST /groups/:name/leave — remove self from group - POST /groups/:name/kick — creator can kick members - GET /groups/:name/members — list with aliases + creator badge CLI TUI: - /gleave — leave current group - /gkick <fp_or_alias> — kick (creator only) - /gmembers — show member list with aliases and ★ for creator Web client: - Same commands: /gleave, /gkick, /gmembers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ pub fn routes() -> Router<AppState> {
|
||||
.route("/groups/:name", get(get_group))
|
||||
.route("/groups/:name/join", post(join_group))
|
||||
.route("/groups/:name/send", post(send_to_group))
|
||||
.route("/groups/:name/leave", post(leave_group))
|
||||
.route("/groups/:name/kick", post(kick_member))
|
||||
.route("/groups/:name/members", get(get_members))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -205,3 +208,90 @@ async fn send_to_group(
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true, "delivered": delivered })))
|
||||
}
|
||||
|
||||
async fn leave_group(
|
||||
State(state): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
Json(req): Json<JoinRequest>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let fp = normalize_fp(&req.fingerprint);
|
||||
|
||||
let mut group = match load_group(&state.db.groups, &name) {
|
||||
Some(g) => g,
|
||||
None => return Ok(Json(serde_json::json!({ "error": "group not found" }))),
|
||||
};
|
||||
|
||||
group.members.retain(|m| m != &fp);
|
||||
save_group(&state.db.groups, &group)?;
|
||||
tracing::info!("{} left group '{}' ({} remaining)", fp, name, group.members.len());
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true, "remaining": group.members.len() })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct KickRequest {
|
||||
fingerprint: String, // who is doing the kicking (must be creator)
|
||||
target: String, // who to kick
|
||||
}
|
||||
|
||||
async fn kick_member(
|
||||
State(state): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
Json(req): Json<KickRequest>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let fp = normalize_fp(&req.fingerprint);
|
||||
let target = normalize_fp(&req.target);
|
||||
|
||||
let mut group = match load_group(&state.db.groups, &name) {
|
||||
Some(g) => g,
|
||||
None => return Ok(Json(serde_json::json!({ "error": "group not found" }))),
|
||||
};
|
||||
|
||||
if group.creator != fp {
|
||||
return Ok(Json(serde_json::json!({ "error": "only the creator can kick members" })));
|
||||
}
|
||||
|
||||
if target == fp {
|
||||
return Ok(Json(serde_json::json!({ "error": "cannot kick yourself" })));
|
||||
}
|
||||
|
||||
let before = group.members.len();
|
||||
group.members.retain(|m| m != &target);
|
||||
if group.members.len() == before {
|
||||
return Ok(Json(serde_json::json!({ "error": "target is not a member" })));
|
||||
}
|
||||
|
||||
save_group(&state.db.groups, &group)?;
|
||||
tracing::info!("{} kicked {} from group '{}'", fp, target, name);
|
||||
|
||||
Ok(Json(serde_json::json!({ "ok": true, "kicked": target, "remaining": group.members.len() })))
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
State(state): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let group = match load_group(&state.db.groups, &name) {
|
||||
Some(g) => g,
|
||||
None => return Ok(Json(serde_json::json!({ "error": "group not found" }))),
|
||||
};
|
||||
|
||||
// Resolve aliases for each member
|
||||
let mut members_info: Vec<serde_json::Value> = Vec::new();
|
||||
for fp in &group.members {
|
||||
let alias = state.db.aliases.get(format!("fp:{}", fp).as_bytes())
|
||||
.ok().flatten()
|
||||
.map(|v| String::from_utf8_lossy(&v).to_string());
|
||||
members_info.push(serde_json::json!({
|
||||
"fingerprint": fp,
|
||||
"alias": alias,
|
||||
"is_creator": *fp == group.creator,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"name": group.name,
|
||||
"members": members_info,
|
||||
"count": members_info.len(),
|
||||
})))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user