Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/_ci-relay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,8 @@ jobs:
cargo test -p buzz-test-client --test e2e_relay nip29_departure_wire -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip29_batch_put_user_wire -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip29_announce_channel_wire -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip29_unlisted_channel_wire -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip29_solo_share_and_convert_wire -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip29_member_raises_guest_agent -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip29_guest_is_read_only_in_normal_channel -- --ignored --nocapture
cargo test -p buzz-test-client --test e2e_relay nip29_relay_rejects_role_change_by_unprivileged_actor -- --ignored --nocapture
Expand Down
47 changes: 42 additions & 5 deletions NOSTR.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ PGPASSWORD=buzz_dev psql -h localhost -U buzz -d buzz -c \
| **Reactions (kind:7)** | ✅ | Standard NIP-25; channel derived from target event's `#e` tag (client `#h` ignored) |
| **Deletions (kind:5)** | ✅ | Standard NIP-09; self-authored only. `#h` optional, `#e` required |
| **User profiles (kind:0)** | ✅ | NIP-01 metadata; synced to users table (display_name, avatar, about, NIP-05). NIP-05 handles must canonicalize to this relay's domain — off-domain or invalid handles are silently cleared. If a NIP-05 handle collides with another user's (UNIQUE constraint), the handle is skipped but other profile fields (display_name, avatar, about) are still synced. |
| **Group creation (kind:9007)** | ✅ | NIP-29; include `name` tag, optional `visibility`, `channel_type` and label `t` tags |
| **Group creation (kind:9007)** | ✅ | NIP-29; include `name` tag, optional `visibility`, `channel_type`, label `t` and `unlisted` tags |
| **Add user (kind:9000)** | ✅ | Open: any user, subject to target's `channel_add_policy` (`owner_only`/`nobody` can block). Private: owner/admin only. Self-add bypasses agent policy but not private-channel auth. A guest cannot add anyone. A new member with no role tag gets the channel's `join_role`; a non-member adding themselves may ask for `join_role` or a lower role. One event may name up to 100 people: one `p` tag each, with an optional role as the `p` tag's third element (NIP-29), falling back to the event's `role` tag. Each person is checked and added on their own. When the event names several people, the OK message is `members: ` followed by JSON listing the non-empty `added`, `updated` (role changed), `already_member` and `rejected` (`[{"reason", "pubkeys"}]`) results. The event is refused only when it is malformed or nobody it names may be added; then one person keeps the single-add error, and several get `invalid: members: {…}`. A resent event returns `duplicate:` without results; to retry an `error:` rejection, send a new event. Re-adding someone at their current role posts no second `member_joined` notice. |
| **Remove user (kind:9001)** | ✅ | Self-remove allowed (with last-owner guard). Removing others: owner/admin only. |
| **Edit group metadata (kind:9002)** | ✅ | `name`/`about`/label `t`/`visibility`/`ttl`/`archived`/`join_role` tags: owner/admin. `topic`/`purpose` tags: any member except a guest. Buzz applies 9002 as a partial update: an absent tag leaves that setting unchanged. |
| **Edit group metadata (kind:9002)** | ✅ | `name`/`about`/label `t`/`visibility`/`ttl`/`archived`/`join_role`/`unlisted` tags: owner/admin. `topic`/`purpose` tags: any member except a guest. Buzz applies 9002 as a partial update: an absent tag leaves that setting unchanged. |
| **Admin delete event (kind:9005)** | ✅ | Event author can always delete own. Otherwise owner/admin required. Target must be in same channel. |
| **Group deletion (kind:9008)** | ✅ | Owner only. |
| **Leave group (kind:9022)** | ✅ | Any member. Last-owner guard prevents orphaned groups. |
| **Group metadata (kind:39000)** | ✅ | Relay-signed; always `d`, `name`, `t`, `P`, `created_at`, `join_role`; `public` or `private`; `restricted` and `closed` per the access flags below; `about` only if description non-empty; `hidden` for DM channels and private system channels |
| **Group metadata (kind:39000)** | ✅ | Relay-signed; always `d`, `name`, `t`, `P`, `created_at`, `join_role`; `public` or `private`; `restricted` and `closed` per the access flags below; `unlisted` when the channel is unlisted; `about` only if description non-empty; `hidden` for DM channels and private system channels |
| **Unlisted channels** | ✅ | kind:9002 `["unlisted", "true"\|"false"]`, owner/admin, or at creation on kind:9007. An unlisted open channel is left out of every read that does not name it, for non-members: channel lists, kind:39000 without `#d`, messages, COUNT, NIP-50 search and artifact queries. Naming it by `#h` (or `#d` on 39000–39003) reads it, and kind:9021 joins it, as for any open channel. See [Unlisted channels](#unlisted-channels). |
| **Group admins (kind:39001)** | ✅ | Relay-signed; `d`, channel type `t` and creator `P` tags + `p` tags with roles (`owner`, `admin`) |
| **Group members (kind:39002)** | ✅ | Relay-signed; `d`, channel type `t` and creator `P` tags + `p` tags for all members |
| **Membership notifications** | ✅ | kind:44100 (added) / kind:44101 (removed); relay-signed, community-global scope (`channel_id=None` inside the connected community) |
Expand Down Expand Up @@ -122,7 +123,7 @@ What each role can do:
| Topic, purpose (kind:9002) | No | Yes | Yes |
| Add people (kind:9000) | No | Yes, as member, guest or bot | Yes, any role |
| Push to the channel's git repositories | No | Yes | Yes |
| Name, about, visibility, TTL, `join_role` | No | No | Yes |
| Name, about, visibility, TTL, `join_role`, `unlisted` | No | No | Yes |

A non-member may write only where joining would give a role that can write:
an open channel with `join_role = member`. A refused write gets
Expand Down Expand Up @@ -155,6 +156,42 @@ explicit `["join_role", ...]` tag so clients can tell what joining gives:
| open | `guest` | | ✓ | ✓ | |
| private | either | ✓ | | ✓ | ✓ |

### Unlisted channels

An **unlisted** open channel works like a link-shared document: anyone in
the community who has its id (for example from a shared link) can read it
and join it, but it does not show up for people who are not members.

- Owners and admins set it with kind:9002 `["unlisted", "true"]` and clear
it with `["unlisted", "false"]`. A missing tag means "no change". It can be
sent in the same event as `["visibility", "open"]`, so a private channel
becomes open and unlisted in one step.
- kind:9007 also takes `["unlisted", "true"|"false"]`, so a channel can
start unlisted; a missing tag means listed. Any other value is refused
before the event is stored, as on kind:9002.
- For a non-member, an unlisted open channel, of any type, is left out of
every read that does not name it: a filter without `#h` (including
kind:39000 discovery without `#d`, and the system-channel opt-in by `#P`
or `#t:["system"]`), COUNT, NIP-50 search, artifact queries and the
channel list. A filter that names it by `#h`, or by `#d`
on a filter whose kinds are all 39000–39003, reads it under the ordinary
open-channel rules. This is the same "named-only" rule that applies to
system channels ([NIP-CL](docs/nips/NIP-CL.md)).
- Joining (kind:9021) and posting work as on any open channel; `join_role`
still decides what a joiner gets.
- Members see it everywhere, as before.
- The flag is stored on its own and matters only while the channel is open.
Private channels are already invisible to non-members. A private channel
can be marked unlisted ahead of time, so opening it later never lists it.
- kind:39000 carries a bare `["unlisted"]` flag whenever the setting is on,
whatever the visibility. It does not add NIP-29 `hidden`: the metadata
stays readable by id.
- Relays that support it advertise `buzz-unlisted-channels` in NIP-11
`supported_extensions`. An older relay applies `["visibility", "open"]`
and ignores `unlisted`, so a client must check for it before opening a
channel as unlisted.
- Unlisted is not an access control. Anyone with the id can read the channel.

### Group Discovery

The relay emits NIP-29 group state events when channels are created, updated, or membership changes.
Expand All @@ -164,7 +201,7 @@ All discovery events include a `d` tag set to the channel UUID (NIP-29 addressab

| Kind | Tags | Content |
|------|------|---------|
| **39000** | `d=<uuid>`, `name`, `t=<channel_type>`, `P=<creator>`, `created_at=<unix seconds>`, `join_role` (always); `public` or `private`; `restricted` (private channels, and `join_role=guest`); `closed` (private channels only); `about` (if description non-empty); `hidden` (DM channels and private system channels) | Group metadata. The access flags follow the channel settings (see [Announce channels](#announce-channels)). |
| **39000** | `d=<uuid>`, `name`, `t=<channel_type>`, `P=<creator>`, `created_at=<unix seconds>`, `join_role` (always); `public` or `private`; `restricted` (private channels, and `join_role=guest`); `closed` (private channels only); `unlisted` (unlisted channels); `about` (if description non-empty); `hidden` (DM channels and private system channels) | Group metadata. The access flags follow the channel settings (see [Announce channels](#announce-channels)). |
| **39001** | `d=<uuid>`, `t=<channel_type>`, `P=<creator>`, `p` tags with role label (`owner`, `admin`) | Admin list |
| **39002** | `d=<uuid>`, `t=<channel_type>`, `P=<creator>`, `p` tags for all members | Member list |

Expand Down
Loading
Loading