{"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":"validation","errorClass":"IngestError::Rejected","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":458,"sourceCode":"                    // NOTE: DB ENUM constraint prevents unknown values from being stored.\n                    // If a new policy value is added to the ENUM, update this match.\n                    _ => {}\n                }\n            }\n\n            Ok(())\n        }\n        9001 => {\n            // REMOVE_USER: self-remove allowed unless actor is the last owner; removing others requires owner/admin\n            let target_pubkey =\n                extract_p_tag(event).ok_or_else(|| anyhow::anyhow!(\"missing p tag\"))?;\n            if target_pubkey == actor_bytes {\n                // Self-removal: 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            } else {\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                    Some(m) if m.role == \"owner\" || m.role == \"admin\" => Ok(()),\n                    Some(_) => {\n                        if state\n                            .db","sourceCodeStart":440,"sourceCodeEnd":476,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L440-L476","documentation":"Thrown by the relay when processing a kind:9001 (REMOVE_USER) event whose p tag targets the actor's own key (self-leave), but the actor has no membership row in the channel. The relay loads the member list with get_members(community, channel_id) and fails when the actor's pubkey is absent. The event is rejected before storage, so the leave is a no-op.","triggerScenarios":"Publishing kind:9001 with [\"p\", <your own pubkey>] to a channel you never joined, already left, or were removed from by an admin. Also fires when the h tag channel id is stale (channel recreated/deleted and you are sending an old channel uuid).","commonSituations":"Retrying a leave after being force-removed by an admin (double-leave); copying an old channel id from a deep link; agents re-using a cached channel uuid after the channel was deleted and recreated; test scripts that leave before joining.","solutions":["Confirm you actually intend to leave — if you were already removed, treat this error as success (idempotent no-op) and drop the channel from local state.","Verify the h tag channel id is current by listing your channels (buzz channels list) and re-publish with the correct uuid.","If you were never a member and want out of the UI only, remove the channel locally instead of sending a 9001 event.","If you believe you are a member, check kind:39002 membership state for that channel to see what the relay thinks before retrying."],"exampleFix":"// before — leaving a channel you may not be in anymore\nconst ev = await sdk.removeUser(channelId, myPubkey); // kind:9001 p=self → rejected\n\n// after — check membership first, then leave\nconst members = await buzz.channelsMembersList(channelId);\nif (members.some((m) => m.pubkey === myPubkey)) {\n  await sdk.removeUser(channelId, myPubkey);\n} else {\n  // already out — clear local channel state only\n  await localStore.dropChannel(channelId);\n}","handlingStrategy":"validation","validationCode":"// Fetch the member list before attempting a self-leave (kind:9001 p=self)\nconst members = await buzz.channelsMembersList(channelId);\nconst isMember = members.some((m) => m.pubkey === myPubkey);\nif (!isMember) {\n  // treat as already-left: clear local state, do not publish\n  await localStore.dropChannel(channelId);\n}","typeGuard":"function isChannelMember(members: { pubkey: string }[], pubkey: string): boolean {\n  return members.some((m) => m.pubkey.toLowerCase() === pubkey.toLowerCase());\n}","tryCatchPattern":"try {\n  await sdk.removeUser(channelId, myPubkey);\n} catch (e) {\n  if (String(e).includes(\"actor is not an active member\")) {\n    // Already out — converge local state instead of surfacing an error\n    await localStore.dropChannel(channelId);\n  } else throw e;\n}","preventionTips":["Make leave operations idempotent in the client: treat 'not an active member' as success.","Always resolve channel ids from a fresh channels list rather than cached deep links.","In bulk-leave scripts, pre-fetch membership for all channels and skip non-members."],"tags":["nostr","authorization","channel-membership","kind-9001","relay"],"backgroundTag":"group-membership-required","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}