{"record":{"id":"5c57b2e7bce7e900","repo":"block/buzz","slug":"actor-is-not-an-active-member","errorCode":null,"errorMessage":"actor is not an active member","messagePattern":"actor is not an active member","errorType":"exception","errorClass":"anyhow::Error","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":746,"sourceCode":"                    // Allow the owning human of any active owner-role agent in the\n                    // channel, even when the human is not a channel member —\n                    // diverges from kind:9001 intentionally.\n                    if actor_owns_any_owner_agent(state, tenant.community(), &members, &actor_bytes)\n                        .await?\n                    {\n                        return Ok(());\n                    }\n                    Err(anyhow::anyhow!(\"only owner can delete group\"))\n                }\n            }\n        }\n        9022 => {\n            // LEAVE_REQUEST: must be an active member, and cannot be the last owner.\n            let members = state.db.get_members(tenant.community(), channel_id).await?;\n            let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);\n            match actor_member {\n                None => {\n                    return Err(anyhow::anyhow!(\"actor is not an active member\"));\n                }\n                Some(m) if m.role == \"owner\" => {\n                    let owner_count = members.iter().filter(|m| m.role == \"owner\").count();\n                    if owner_count <= 1 {\n                        return Err(anyhow::anyhow!(\"cannot remove the last owner\"));\n                    }\n                }\n                _ => {}\n            }\n            Ok(())\n        }\n        _ => Ok(()),\n    }\n}\n\n/// Emit a system message (kind 40099) signed by the relay keypair.\n///\n/// `idempotency_ts` is used as the event's `created_at`. Passing a stable","sourceCodeStart":728,"sourceCodeEnd":764,"githubUrl":"https://github.com/block/buzz/blob/eed74bde2f4797714335ac10c56c0b0244c1def4/crates/buzz-relay/src/handlers/side_effects.rs#L728-L764","documentation":"Buzz-relay rejects a kind 9022 (LEAVE_REQUEST) event when the signing actor's pubkey is not present in the target channel's member list. The handler loads members via db.get_members(community, channel_id) and requires an exact pubkey match; a miss means the actor never joined, already left, or was removed. The error aborts side-effect validation, so the event is rejected instead of stored.","triggerScenarios":"Publishing kind 9022 whose h tag points at a channel where the signer is not an active member: they were kicked by an owner, a previous leave already succeeded, or the h tag UUID is simply the wrong channel. Also hit when an agent harness replays a leave request after membership was revoked.","commonSituations":"Clients caching a stale channel list and leaving a channel the user was removed from; automation that iterates all known channels and leaves each one, including channels the signer never joined; test scripts signing with a fresh key that never went through join.","solutions":["Query the channel's member list (kinds [39002] scoped to the channel, or `buzz channels members`) and confirm the signer's pubkey is present before publishing the leave","Double-check the h tag channel UUID matches the channel you actually belong to","If membership was already revoked or a prior leave succeeded, no action is needed — the relay state is already what you asked for","Treat this rejection as idempotent success in leave-all automation: catch it and move on instead of retrying"],"exampleFix":"// before: publish leave unconditionally\nlet event = EventBuilder::leave_request(channel_id, actor_keys);\nrelay.publish(event).await?; // rejected: actor is not an active member\n\n// after: gate on active membership first\nlet members = client.query_members(channel_id).await?;\nif members.iter().any(|m| m.pubkey == actor_pubkey) {\n    relay.publish(EventBuilder::leave_request(channel_id, actor_keys)).await?;\n}","handlingStrategy":"validation","validationCode":"// Before publishing kind 9022, confirm active membership:\nlet members = client.query_members(channel_id).await?;\nlet is_member = members.iter().any(|m| m.pubkey == actor_pubkey.to_hex());\nif !is_member {\n    // skip: already removed / never joined — nothing to leave\n    return Ok(());\n}","typeGuard":"fn is_active_member(members: &[Member], actor: &str) -> bool {\n    members.iter().any(|m| m.pubkey == actor)\n}","tryCatchPattern":"// Treat as terminal, not retryable:\nif err.to_string().contains(\"actor is not an active member\") {\n    // idempotent no-op for leave-all flows; do not re-send\n}","preventionTips":["Always resolve the channel member list at publish time instead of trusting a cached channel list","In leave-all automation, catch membership-rejection and continue instead of retrying","Log the h tag channel UUID alongside leave attempts so misdirected leaves are diagnosable"],"tags":["nostr","channel-membership","kind-9022","authorization","buzz-relay"],"backgroundTag":"channel-membership-required","analyzedSha":"eed74bde2f4797714335ac10c56c0b0244c1def4","analyzedAt":"2026-08-20T04:38:24.874Z","contentChangedAt":"2026-08-20T04:38:24.874Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}