From 580de4b8becb505e92cc1b725b2d9b78817b3c2d Mon Sep 17 00:00:00 2001 From: dtonon Date: Sun, 7 Jun 2026 13:53:01 +0200 Subject: [PATCH 1/3] NIP-29 Replace group flags with explicit read/write/admission state axex plus lock --- 29.md | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 137 insertions(+), 8 deletions(-) diff --git a/29.md b/29.md index 6c648df49c..2bed8b103a 100644 --- a/29.md +++ b/29.md @@ -44,6 +44,135 @@ 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 independent axes, **read**, **write** and **admission**, plus an emergency **lock** override. Each axis is a single tag carrying one value, and together they replace the older ad-hoc `private`/`restricted`/`hidden`/`closed` boolean flags. + +All of these live on the relay-generated `kind:39000` metadata event 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`, which MUST state the `read`, `write` and `admission` values explicitly, and clients SHOULD do the same on the `kind:9002`. Relays enforce these states. + +Two fallbacks cover non-conformant events: a *missing* value is read as the open default in each table below (older groups had no such tag and were open), while an *unrecognised* value is treated as the most restrictive option on that axis. + +### Read + +Set with `["read", ]`. Controls who can discover the group and read its content (default `public`): + +| value | listed in relay-wide queries | metadata readable by | content readable by | +| --- | --- | --- | --- | +| `public` | yes | anyone | anyone | +| `private` | yes | anyone | members only | +| `secret` | no | members only | members only | + +`secret` implies `private`: a group hidden from discovery is always members-only for both its metadata and its content (including the member list in `kind:39002`). 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, since once the id leaked that would be unrevokable exposure, not access control. Membership is the only revocation lever, so every confidential state keys off membership. + +### Write + +Set with `["write", ]`. Governs what a regular member may post; admins may always post. Writing at all requires membership, with no anonymous posting. To let "anyone" participate, set `admission: open` (below) so join requests are auto-accepted; the writers are still members. Default `everything`: + +| value | a member may post | +| --- | --- | +| `everything` | root events (threads) and replies | +| `replies` | only replies, since admins create the threads that members reply to (curated groups) | +| `disabled` | nothing (read-only); admins MAY still post, to freeze a conversation | + +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. The `replies` restriction applies only to kinds that have this root/reply structure; for flat kinds without it (e.g. chat `kind:9`) it has no effect and members may post normally. + +### Admission + +Set with `["admission", ]`. How a non-member becomes a member (default `open`): + +| value | 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; a valid invite `code` is accepted immediately | +| `closed` | no new members are admitted, so every join request is rejected, invite codes included | + +### Lock (emergency) + +`["locked"]` is a binary override that, regardless of the axes above, hides the group from everyone, rejects all writes and rejects all joins. The group's admins and the relay operator keep read access so the retained content can be inspected; nothing is deleted and the state is reversible. The relay operator may additionally remove admins (`kind:9001`), so it can become the sole party with access. This lets a moderator or the operator immediately contain abuse without destroying evidence. + +### Backwards compatibility + +The previous boolean tags are accepted as aliases when a relay reads older state: `["private"]` → `["read", "private"]`, `["hidden"]` → `["read", "secret"]`, `["closed"]` → `["admission", "closed"]`. `["restricted"]` (members-only writing) is now the baseline for every group and corresponds to the default `["write", "everything"]`; conversely a group that previously omitted `restricted` (anyone could write) becomes members-only under that default, a deliberate tightening, never a loosening. + +### Derived presets (non-normative) + +Some familiar states are just combinations of the axes and need no dedicated flag: + +- **archived**: `write: disabled` plus `admission: closed`, read level unchanged, so it is finished and read-only but still browsable. +- **announcement channel**: `admission: closed`, so only the existing admins remain members and thus the only writers; to let members reply instead, use `write: replies` on an `open` or `approval` group. + +### Examples + +A fully public group, where anyone reads, members post freely and anyone can join (every axis at its default, but a generated `kind:39000` still states them explicitly): + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Public Square"], + ["about", "open to everyone"], + ["read", "public"], + ["write", "everything"], + ["admission", "open"] + ] +} +``` + +A private curated group, listed so people can find and request to join, but where only members read, only admins start threads and members reply: + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Project Updates"], + ["read", "private"], + ["write", "replies"], + ["admission", "approval"] + ] +} +``` + +A secret group, not listed and members-only, joinable only with an invite code (the code conveys the id): + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Inner Circle"], + ["read", "secret"], + ["write", "everything"], + ["admission", "approval"] + ] +} +``` + +A group under emergency lock, hidden and frozen for everyone but its admins and the relay operator, pending inspection (the lock overrides the other axes, whatever they were): + +```jsonc +{ + "kind": 39000, + "content": "", + "tags": [ + ["d", ""], + ["name", "Suspended Group"], + ["read", "public"], + ["write", "everything"], + ["admission", "open"], + ["locked"] + ] +} +``` + +## Invite codes + +A `kind:9009` `create-invite` registers a code. Codes apply to any group whose admission is `approval`, and are the means by which a user reaches a `secret` group (the code carries the otherwise-undiscoverable id). They never bypass `closed`. + ## 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 +203,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 `admission: approval` 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 +216,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. A valid code preauthorises acceptance into an `admission: approval` group (see [Group states](#group-states)) and is the way a user joins a `secret` group, since the code conveys the otherwise-undiscoverable group id. Codes never override `admission: closed`. - *leave request* (`kind:9022`) @@ -153,8 +284,9 @@ If the group is forked and hosted in multiple relays, there will be multiple ver ["name", "Pizza Lovers"], ["picture", "https://pizza.com/pizza.png"], ["about", "a group for people who love pizza"], - ["private"], - ["closed"], + ["read", "private"], + ["admission", "approval"], + ["write", "replies"], ["supported_kinds", "9", "11"] ] // other fields... @@ -162,10 +294,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 **read**, **write** and **admission** policy and the emergency **lock** are expressed with the `read`, `write`, `admission` and `locked` 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 +388,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** axis (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 unless it carries a valid invite `code`, which is accepted at once; with `closed` no request is honored, codes included. ### Storing your list of groups From e2b2224affd6f375fd550b31406efec85d0d7fe2 Mon Sep 17 00:00:00 2001 From: dtonon Date: Fri, 12 Jun 2026 19:03:43 +0200 Subject: [PATCH 2/3] NIP-29 Review and extend group flags --- 29.md | 181 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 120 insertions(+), 61 deletions(-) diff --git a/29.md b/29.md index 2bed8b103a..22347231dc 100644 --- a/29.md +++ b/29.md @@ -46,64 +46,85 @@ Users with any roles that have any privilege can be considered _admins_ in a bro ## Group states -A group's policy is described by three independent axes, **read**, **write** and **admission**, plus an emergency **lock** override. Each axis is a single tag carrying one value, and together they replace the older ad-hoc `private`/`restricted`/`hidden`/`closed` boolean flags. +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. The model keeps the older `private`/`restricted`/`hidden`/`closed` tag names with the same meaning and only adds new ones next to them, so existing events keep working (see [Backwards compatibility](#backwards-compatibility)). -All of these live on the relay-generated `kind:39000` metadata event 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`, which MUST state the `read`, `write` and `admission` values explicitly, and clients SHOULD do the same on the `kind:9002`. Relays enforce these states. +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. -Two fallbacks cover non-conformant events: a *missing* value is read as the open default in each table below (older groups had no such tag and were open), while an *unrecognised* value is treated as the most restrictive option on that axis. +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: -### Read +- A *missing* section is read as its most open value, the first one in the table (older groups had no such tag and were fully open). New groups SHOULD still 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. -Set with `["read", ]`. Controls who can discover the group and read its content (default `public`): +### Admission -| value | listed in relay-wide queries | metadata readable by | content readable by | -| --- | --- | --- | --- | -| `public` | yes | anyone | anyone | -| `private` | yes | anyone | members only | -| `secret` | no | members only | members only | +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: -`secret` implies `private`: a group hidden from discovery is always members-only for both its metadata and its content (including the member list in `kind:39002`). 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, since once the id leaked that would be unrevokable exposure, not access control. Membership is the only revocation lever, so every confidential state keys off membership. +| 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` | -### Write +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. -Set with `["write", ]`. Governs what a regular member may post; admins may always post. Writing at all requires membership, with no anonymous posting. To let "anyone" participate, set `admission: open` (below) so join requests are auto-accepted; the writers are still members. Default `everything`: +### Visibility -| value | a member may post | -| --- | --- | -| `everything` | root events (threads) and replies | -| `replies` | only replies, since admins create the threads that members reply to (curated groups) | -| `disabled` | nothing (read-only); admins MAY still post, to freeze a conversation | +Controls who can discover the group and read its content (default `public`): -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. The `replies` restriction applies only to kinds that have this root/reply structure; for flat kinds without it (e.g. chat `kind:9`) it has no effect and members may post normally. +| 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 | -### Admission +`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. -Set with `["admission", ]`. How a non-member becomes a member (default `open`): +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. -| value | 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; a valid invite `code` is accepted immediately | -| `closed` | no new members are admitted, so every join request is rejected, invite codes included | +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. -### Lock (emergency) +### Participation -`["locked"]` is a binary override that, regardless of the axes above, hides the group from everyone, rejects all writes and rejects all joins. The group's admins and the relay operator keep read access so the retained content can be inspected; nothing is deleted and the state is reversible. The relay operator may additionally remove admins (`kind:9001`), so it can become the sole party with access. This lets a moderator or the operator immediately contain abuse without destroying evidence. +Governs what a regular member may post; admins may always post (default `writable`): -### Backwards compatibility +| 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. -The previous boolean tags are accepted as aliases when a relay reads older state: `["private"]` → `["read", "private"]`, `["hidden"]` → `["read", "secret"]`, `["closed"]` → `["admission", "closed"]`. `["restricted"]` (members-only writing) is now the baseline for every group and corresponds to the default `["write", "everything"]`; conversely a group that previously omitted `restricted` (anyone could write) becomes members-only under that default, a deliberate tightening, never a loosening. +### 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 states are just combinations of the axes and need no dedicated flag: +Some familiar group types are just combinations of the tags above and need no dedicated tag: -- **archived**: `write: disabled` plus `admission: closed`, read level unchanged, so it is finished and read-only but still browsable. -- **announcement channel**: `admission: closed`, so only the existing admins remain members and thus the only writers; to let members reply instead, use `write: replies` on an `open` or `approval` group. +| 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 | -### Examples +#### Events examples -A fully public group, where anyone reads, members post freely and anyone can join (every axis at its default, but a generated `kind:39000` still states them explicitly): +Each preset above rendered as a `kind:39000` event, plus an emergency quarantine. + +A **moderated forum**, anyone reads but posting needs approval: ```jsonc { @@ -111,16 +132,15 @@ A fully public group, where anyone reads, members post freely and anyone can joi "content": "", "tags": [ ["d", ""], - ["name", "Public Square"], - ["about", "open to everyone"], - ["read", "public"], - ["write", "everything"], - ["admission", "open"] + ["name", "Town Hall"], + ["approval"], + ["public"], + ["restricted"] ] } ``` -A private curated group, listed so people can find and request to join, but where only members read, only admins start threads and members reply: +A **private community**, vetted members read and post, outsiders only see it exists: ```jsonc { @@ -129,14 +149,14 @@ A private curated group, listed so people can find and request to join, but wher "tags": [ ["d", ""], ["name", "Project Updates"], - ["read", "private"], - ["write", "replies"], - ["admission", "approval"] + ["approval"], + ["private"], + ["writable"] ] } ``` -A secret group, not listed and members-only, joinable only with an invite code (the code conveys the id): +A **teaser group**, outsiders read the root posts, joining unlocks the discussion: ```jsonc { @@ -145,14 +165,46 @@ A secret group, not listed and members-only, joinable only with an invite code ( "tags": [ ["d", ""], ["name", "Inner Circle"], - ["read", "secret"], - ["write", "everything"], - ["admission", "approval"] + ["approval"], + ["preview"], + ["restricted"] ] } ``` -A group under emergency lock, hidden and frozen for everyone but its admins and the relay operator, pending inspection (the lock overrides the other axes, whatever they were): +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 { @@ -161,17 +213,24 @@ A group under emergency lock, hidden and frozen for everyone but its admins and "tags": [ ["d", ""], ["name", "Suspended Group"], - ["read", "public"], - ["write", "everything"], - ["admission", "open"], - ["locked"] + ["open"], + ["public"], + ["writable"], + ["quarantined"] ] } ``` +### Backwards compatibility + +The four old tag names keep the same meaning and section, so existing `kind:39000` events keep working: `private` and `hidden` in **visibility**, `restricted` in **participation**, `closed` in **admission**. A group with no state tags at all is still read as fully open (`public` / `writable` / `open`), exactly as before. Two edges are tightened on purpose and accepted as such: + +- `hidden` now also keeps the metadata for members, where some relays previously still served it on a direct id request. +- `closed` now rejects code-carrying requests too, unless the group also sets the new `invite` tag; an old `closed` group that relied on invite codes restores its behaviour by adding `invite`. + ## Invite codes -A `kind:9009` `create-invite` registers a code. Codes apply to any group whose admission is `approval`, and are the means by which a user reaches a `secret` group (the code carries the otherwise-undiscoverable id). They never bypass `closed`. +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 @@ -203,7 +262,7 @@ 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 `admission: approval` 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`. +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 { @@ -216,7 +275,7 @@ Under `admission: approval` the relay SHOULD retain the pending request so admin } ``` -The optional `code` tag carries an invite code created with a `kind:9009` `create-invite` moderation event. A valid code preauthorises acceptance into an `admission: approval` group (see [Group states](#group-states)) and is the way a user joins a `secret` group, since the code conveys the otherwise-undiscoverable group id. Codes never override `admission: closed`. +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`) @@ -284,9 +343,9 @@ If the group is forked and hosted in multiple relays, there will be multiple ver ["name", "Pizza Lovers"], ["picture", "https://pizza.com/pizza.png"], ["about", "a group for people who love pizza"], - ["read", "private"], - ["admission", "approval"], - ["write", "replies"], + ["private"], + ["approval"], + ["replies"], ["supported_kinds", "9", "11"] ] // other fields... @@ -294,7 +353,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. -- The group's **read**, **write** and **admission** policy and the emergency **lock** are expressed with the `read`, `write`, `admission` and `locked` tags defined in [Group states](#group-states) above. +- 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). @@ -388,7 +447,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. How it is handled follows the group's **admission** axis (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 unless it carries a valid invite `code`, which is accepted at once; with `closed` no request is honored, codes included. +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 From e878693461e7c4e76b233c9e2cfe7a04b222ecef Mon Sep 17 00:00:00 2001 From: dtonon Date: Wed, 1 Jul 2026 16:32:39 +0200 Subject: [PATCH 3/3] Remove references to the previous version and backward compatibility --- 29.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/29.md b/29.md index 22347231dc..c26c6b9a6e 100644 --- a/29.md +++ b/29.md @@ -46,13 +46,13 @@ Users with any roles that have any privilege can be considered _admins_ in a bro ## 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. The model keeps the older `private`/`restricted`/`hidden`/`closed` tag names with the same meaning and only adds new ones next to them, so existing events keep working (see [Backwards compatibility](#backwards-compatibility)). +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 (older groups had no such tag and were fully open). New groups SHOULD still state every value explicitly. +- 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. @@ -221,13 +221,6 @@ A group under emergency **quarantine**, frozen and invisible to everyone but its } ``` -### Backwards compatibility - -The four old tag names keep the same meaning and section, so existing `kind:39000` events keep working: `private` and `hidden` in **visibility**, `restricted` in **participation**, `closed` in **admission**. A group with no state tags at all is still read as fully open (`public` / `writable` / `open`), exactly as before. Two edges are tightened on purpose and accepted as such: - -- `hidden` now also keeps the metadata for members, where some relays previously still served it on a direct id request. -- `closed` now rejects code-carrying requests too, unless the group also sets the new `invite` tag; an old `closed` group that relied on invite codes restores its behaviour by adding `invite`. - ## 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.