diff --git a/29.md b/29.md index 6c648df49c..c26c6b9a6e 100644 --- a/29.md +++ b/29.md @@ -44,6 +44,187 @@ The roles supported by the group as to having some special privilege assigned to Users with any roles that have any privilege can be considered _admins_ in a broad sense and be returned in the `kind:39001` event for a group. +## Group states + +A group's policy is described by three sections, **admission**, **visibility** and **participation**, plus two escalating emergency overrides, **lock** and **quarantine**. Each state is a single one-word tag on the relay-generated `kind:39000` metadata event. + +All of these live on `kind:39000` and are set with a `kind:9002` `edit-metadata` action. A `kind:9002` carries the complete metadata and replaces it atomically (it restates all of the fields), so a client edits by reading the current `kind:39000`, changing it and resending the whole set. The relay then re-renders the `kind:39000` and enforces the resulting states. + +Each section below is a ladder ordered from the most open option to the most restrictive. A group SHOULD carry one tag per section. Reading rules: + +- A *missing* section is read as its most open value, the first one in the table; a group SHOULD nonetheless state every value explicitly. +- If *several* tags from the same section are present, the most restrictive one, lowest in the table, applies. +- Tags are bare words with no section marker, so an implementation that does not recognise a tag just ignores it. Relays SHOULD therefore always emit the explicit tags they enforce, and clients SHOULD surface an unrecognised tag as an unknown restriction rather than drop it silently. + +### Admission + +How a non-member becomes a member. The first three are a ladder, pick one (default `open`); `invite` is independent and combines with any of them: + +| Tag | Behaviour | +| --- | --- | +| `open` | `kind:9021` join requests are accepted automatically | +| `approval` | A join request is held pending for an admin to accept (`kind:9000`) or ignore | +| `closed` | No new members are admitted, every join request is rejected | +| `invite` | A join request carrying a valid `code` is accepted immediately, overriding the value above, even `closed` | + +When `invite` is set the relay honors codes created with `kind:9009`; without it the codes are ignored. This lets an admin turn code-based joining on and off without deleting the code events, and it is the way a user reaches a `hidden` group, since the code conveys the otherwise-undiscoverable id. + +### Visibility + +Controls who can discover the group and read its content (default `public`): + +| Tag | Listed in relay-wide queries | Metadata readable by | Content readable by | +| --- | --- | --- | --- | +| `public` | Yes | Anyone | Anyone | +| `preview` | Yes | Anyone | Roots by anyone, replies by members only | +| `private` | Yes | Anyone | Members only | +| `hidden` | No | Members only | Members only | + +`preview` lets outsiders read the root events (announcements, thread openers) while keeping the discussion, the replies, for members. `hidden` is not listed in relay-wide queries and keeps even its metadata for members, so it is reached only by someone who already knows the group id. + +A *root* is an in-group event that does not reference another in-group event as its parent; a *reply* is one that does (e.g. for [NIP-7D](7D.md) a `kind:11` is a root and a `kind:1111` pointing to it is a reply), and a reply MUST reference an existing root in the same group. This distinction applies only to kinds that have a root/reply structure; for flat kinds without it (e.g. chat `kind:9`) it has no effect, so `preview` and the `replies` value under [Participation](#participation) fall back to letting members post and read normally. + +Because the group `d` id is permanent and cannot be rotated, a "hidden from listings but world-readable by id" state is intentionally not offered: once the id leaked that would be unrevokable exposure, not access control. Membership is the only revocation lever. + +### Participation + +Governs what a regular member may post; admins may always post (default `writable`): + +| Tag | Who may post | What they may post | +| --- | --- | --- | +| `writable` | Anyone | Roots and replies | +| `restricted` | Members only | Roots and replies | +| `replies` | Members only | Replies only, since admins create the threads members reply to | +| `disabled` | Admins only | Read-only group; members cannot post, so an active discussion stops where it is, while admins MAY still post | + +The `replies` value uses the root/reply distinction defined under [Visibility](#visibility): admins create the threads (roots) and members may only reply to them. + +### Lock and quarantine (emergency) + +Two binary overrides for incidents, escalating in severity. They change nothing permanently, delete nothing and are reversible: + +- `locked` freezes the group: it rejects all posting and all join requests, overriding the **participation** and **admission** sections. Admins may still post. The group stays visible, so members can still read its content while it is frozen. +- `quarantined` is the full seal: it does everything `locked` does and additionally hides the group and its metadata from everyone, members included, overriding the **visibility** section. Only the admins and the relay operator keep access; the operator may further remove admins (`kind:9001`) to become the sole party with access. + +So a moderator can either freeze a group in place, still readable by its members, or make it vanish entirely, all while the retained content is inspected. + +### Derived presets (non-normative) + +Some familiar group types are just combinations of the tags above and need no dedicated tag: + +| Preset | Admission | Visibility | Participation | What it is | +| --- | --- | --- | --- | --- | +| **Moderated forum** | `approval` | `public` | `restricted` | Anyone reads, but you must be approved before you can post | +| **Private community** | `approval` | `private` | `writable` | Outsiders only see that the group exists and can request to join; approved members read and post | +| **Teaser group** | `approval` | `preview` | `restricted` | Outsiders read the headline root posts; getting approved unlocks the discussion and posting | +| **Announcement / broadcast channel** | `open` | `public` | `disabled` | Only admins post, everyone reads; with `open` admission anyone can follow by auto-joining, use `replies` to let members reply without starting threads | +| **Archived** | `closed` | Unchanged | `disabled` | Finished and read-only but still browsable | + +#### Events examples + +Each preset above rendered as a `kind:39000` event, plus an emergency quarantine. + +A **moderated forum**, anyone reads but posting needs approval: + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Town Hall"], + ["approval"], + ["public"], + ["restricted"] + ] +} +``` + +A **private community**, vetted members read and post, outsiders only see it exists: + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Project Updates"], + ["approval"], + ["private"], + ["writable"] + ] +} +``` + +A **teaser group**, outsiders read the root posts, joining unlocks the discussion: + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Inner Circle"], + ["approval"], + ["preview"], + ["restricted"] + ] +} +``` + +An **announcement / broadcast channel**, only admins post and everyone reads: + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Release Notes"], + ["open"], + ["public"], + ["disabled"] + ] +} +``` + +An **archived** group, finished and read-only but still browsable: + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "2024 Archive"], + ["closed"], + ["public"], + ["disabled"] + ] +} +``` + +A group under emergency **quarantine**, frozen and invisible to everyone but its admins and the relay operator, pending inspection (the override wins over the other sections, whatever they were): + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Suspended Group"], + ["open"], + ["public"], + ["writable"], + ["quarantined"] + ] +} +``` + +## Invite codes + +A `kind:9009` `create-invite` registers a code. The codes are honored only when the group sets the `invite` tag (see [Admission](#admission)): a `kind:9021` join request carrying a valid code is then accepted immediately, whatever the admission value is, even `closed`. They are also the means by which a user reaches a `hidden` group, since the code carries the otherwise-undiscoverable id. Without the `invite` tag the codes are ignored. + ## Live audio/video (AV) spaces Groups may indicate they support live audio and/or video chat by means of a [LiveKit](https://docs.livekit.io/transport/) server by having the tag `livekit` in the group announcement event. An empty `supported_kinds` tag should be used if the group doesn't support text messages, only live AV chat. This setup allows for both Discord-style independent AV rooms and text rooms and Telegram-style single-roomgroup with support for both text and AV. @@ -74,6 +255,8 @@ These are events that can be sent by users to manage their situation in a group, Any user can send a kind `9021` event to the relay in order to request admission to the group. Relays MUST reject the request if the user has not been added to the group. The accompanying error message SHOULD explain whether the rejection is final, if the request is pending review, or if some other special handling is relevant (e.g. if payment is required). If a user is already a member, the event MUST be rejected with `duplicate: ` as the error message prefix. +Under `approval` admission the relay SHOULD retain the pending request so admins can review and accept it later (by issuing a `kind:9000`), even though the requester receives `OK: false`. + ```json { "kind": 9021, @@ -85,7 +268,7 @@ Any user can send a kind `9021` event to the relay in order to request admission } ``` -The optional `code` tag may be used by the relay to preauthorize acceptance, together with the `kind:9009` `create-invite` moderation event. +The optional `code` tag carries an invite code created with a `kind:9009` `create-invite` moderation event. When the group sets the `invite` tag (see [Group states](#group-states)) a valid code is accepted immediately, overriding the admission value, even `closed`, and it is the way a user joins a `hidden` group, since the code conveys the otherwise-undiscoverable group id. When the group does not set `invite` the code is ignored. - *leave request* (`kind:9022`) @@ -154,7 +337,8 @@ If the group is forked and hosted in multiple relays, there will be multiple ver ["picture", "https://pizza.com/pizza.png"], ["about", "a group for people who love pizza"], ["private"], - ["closed"], + ["approval"], + ["replies"], ["supported_kinds", "9", "11"] ] // other fields... @@ -162,10 +346,7 @@ If the group is forked and hosted in multiple relays, there will be multiple ver ``` - `name`, `picture` and `about` are basic metadata for the group for display purposes. -- `private` indicates that only members can _read_ group messages. Omitting this tag indicates that anyone can read group messages. -- `restricted` indicates that only members can _write_ messages to the group. Omitting this tag indicates that anyone can send messages to the group. -- `hidden` indicates that relays should hide group metadata from non-members. Omitting this tag indicates that anyone can request group metadata events. -- `closed` indicates that join requests are ignored. Omitting this tag indicates that users can expect join requests to be honored. +- The group's **visibility**, **participation** and **admission** policy, the `invite` toggle and the emergency `locked`/`quarantined` overrides are expressed with the one-word tags defined in [Group states](#group-states) above. - `livekit` indicates that a group supports LiveKit-powered media rooms (live audio/video). - `supported_kinds` is a list of stringified kinds numbers the group supports. When not specified all kinds are assumed to be supported. It can contain no items, in which case no kinds are supported (this is the case for AV-only groups, for example). @@ -259,7 +440,7 @@ The latest of either `kind:9000` or `kind:9001` events present in a group should ### Adding yourself to a group -Anyone can send a `kind:9021` join request to a group. The relay may then generate a `kind:9000` event immediately, or defer that decision to an administator. If a group is `closed`, join requests are not honored unless they include an invite code. +Anyone can send a `kind:9021` join request to a group. How it is handled follows the group's **admission** section (see [Group states](#group-states)): with `open` the relay generates a `kind:9000` immediately; with `approval` the request is held for an admin to accept; with `closed` no request is honored. Independently, if the group sets the `invite` tag, a request carrying a valid `code` is accepted at once whatever the admission value is. ### Storing your list of groups