feat(rust): Teams from first-party clans (phase 9, protocol 6)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 46s
PR Checks / client-build (pull_request) Successful in 8m3s

A first-party Rust clan is a Team (R5). This module becomes the site's
Team provider and answers core from the plugin's `clans` board. Design
of record: docs/modules/rust/PLAN.md §24, D47-D58.

- The store: rust_clans, rust_clan_members and rust_clan_boards. A clan's
  identity is <serverId>:<clanId>:<createdMs> (D52), because the game
  restarts clan ids whenever its clan database version changes.
- The provider (D53): getTeams is complete only when every server's
  board is fresh, supported and untruncated. It is partial when some
  are, and refuses when none are. Freshness is judged by the website's
  clock, from when the board's `t` last advanced.
- Only a complete board may mark a clan gone. A board at the game's
  100-clan ceiling (D55), or one with an unreadable row, proves nothing
  about what it leaves out.
- Leadership is diffed board to board and published (D54). The five clan
  events are published as team.* kinds, and written to the Team feed as
  members-only lines (D49).
- Core only writes feed items for a Team it already holds. So the last 10
  minutes of clan events are re-offered on each board refresh, deduped by
  a sha1 key: core clamps a dedupeKey to 40 characters, and a readable key
  would be truncated into collisions.
- projectRoster and the clan page share one audience rule (D48): the
  clan's linked members and staff by default, re-read from the users row.
  The setting lives on Admin > Rust visibility, which also warns about
  uMod Clans (D47) and the ceiling.
- Public: GET servers/:id/clans (the list is public, D58) and
  GET clans/:externalId. The client adds a Clans tab and
  /rust/clans/:externalId, with three module slots for core's notify,
  activity and forum contributions (D56).
- Linking and unlinking an account ask core to reconcile Teams (D57).
- The clan kinds are staff-class in the public feed allowlist.
- PROTOCOL_VERSION is now 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-23 05:14:18 -05:00
parent da1a393702
commit c94271104f
33 changed files with 3456 additions and 32 deletions

View File

@@ -688,8 +688,8 @@
"tags": [
"Admin · Rust"
],
"summary": "Who may see who is online",
"description": "The fleet default and every servers optional override. It governs the Online list, every feed item that names a player who was on the server (connects, respawns, deaths, chat, tallies) and the leaderboards `lastSeen`. The default is `staff`: nothing names who is online until an operator widens it. The player count is public at every setting.",
"summary": "Who may see who is online, and who may see a clan roster",
"description": "The presence fleet default and every servers optional override. It governs the Online list, every feed item that names a player who was on the server (connects, respawns, deaths, chat, tallies) and the leaderboards `lastSeen`. The default is `staff`: nothing names who is online until an operator widens it. The player count is public at every setting. `clans` carries the clan roster audience (default `members`: the clans own linked members, and staff) and each servers clan board — whether it is current, at the games 100-clan ceiling, or running the uMod Clans plugin, whose clans are not Teams.",
"responses": {
"200": {
"description": "The fleet default and each server",
@@ -710,8 +710,8 @@
"tags": [
"Admin · Rust"
],
"summary": "Change who may see who is online",
"description": "Sets the fleet default, one or more server overrides, or both. A server set to `null` follows the fleet default again. Validated whole before anything is written: a request naming a server that does not exist changes nothing.",
"summary": "Change who may see who is online, or who may see a clan roster",
"description": "Sets the presence fleet default, one or more server overrides, the clan roster audience, or any of them together. A server set to `null` follows the fleet default again. Validated whole before anything is written: a request naming a server that does not exist changes nothing. Widening the clan roster audience also shows which members are online to that audience, because a roster row carries it.",
"responses": {
"200": {
"description": "Saved; answers the new state",
@@ -1246,6 +1246,44 @@
}
}
},
"/api/v1/public/rust/clans/{externalId}": {
"get": {
"tags": [
"Public · Rust"
],
"summary": "One Rust clan",
"description": "A clan and, when the viewer is inside the operators clan roster audience, its roster. The audience defaults to the clans own members (a website account linked to one of them) and staff. Below it the clan is still described and `roster.visible` is false with no names. The roster never carries a Steam id or a website account id. `externalId` is `<server>:<clan>:<created>`, the same identity the sites Team pages use.",
"parameters": [
{
"name": "externalId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "The clans identity: server, clan id and creation time in epoch ms, joined by colons"
}
],
"responses": {
"200": {
"description": "The clan",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RustClan"
}
}
}
},
"404": {
"description": "No such clan"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/public/rust/servers": {
"get": {
"tags": [
@@ -1301,6 +1339,41 @@
}
}
},
"/api/v1/public/rust/servers/{id}/clans": {
"get": {
"tags": [
"Public · Rust"
],
"summary": "The clans on one Rust server",
"description": "Every clan on the servers clan board, best score first: name, colour, score and member count. Public, because none of it names a player. `board` says whether the list is current and complete — a server whose bridge cannot read its clans answers an empty list and the reason, and a server at the games 100-clan ceiling says `truncated`.",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "The servers slug"
}
],
"responses": {
"200": {
"description": "The clans",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RustClanList"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/public/rust/servers/{id}/events": {
"get": {
"tags": [
@@ -4263,6 +4336,756 @@
}
}
}
},
"clans": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"description": {
"type": "string",
"example": "Who may see a clan roster, and each servers clan board."
},
"properties": {
"type": "object",
"properties": {
"audiences": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "array"
},
"items": {
"$ref": "#/components/schemas/RustClanAudience"
}
}
},
"roster": {
"$ref": "#/components/schemas/RustClanAudience"
},
"servers": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "array"
},
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"properties": {
"type": "object",
"properties": {
"id": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "main"
}
}
},
"name": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Main · Vanilla"
}
}
},
"supported": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"enabled": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"fresh": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"truncated": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": false
}
}
},
"reason": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"nullable": {
"type": "boolean",
"example": true
}
}
},
"clans": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"example": {
"type": "number",
"example": 14
}
}
},
"umodClans": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"description": {
"type": "string",
"example": "Is the uMod Clans plugin loaded? Its clans are a separate system and are not Teams."
},
"example": {
"type": "boolean",
"example": false
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
},
"RustClanAudience": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"enum": {
"type": "array",
"example": [
"members",
"signed_in",
"public"
],
"items": {
"type": "string"
}
},
"description": {
"type": "string",
"example": "Who may see a clans roster: the clans own members (a website account linked to one of them) and staff, any signed-in account, or anybody. Widening it also shows which members are online to that audience."
},
"example": {
"type": "string",
"example": "members"
}
}
},
"RustClanBoard": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"description": {
"type": "string",
"example": "Whether a servers clan list can be trusted right now."
},
"properties": {
"type": "object",
"properties": {
"supported": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"description": {
"type": "string",
"example": "Could the bridge read this servers clans at all?"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"enabled": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"description": {
"type": "string",
"example": "Is the games clan system switched on?"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"fresh": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"description": {
"type": "string",
"example": "Has the board been re-sent within the last three minutes?"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"truncated": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"description": {
"type": "string",
"example": "At the games 100-clan ceiling, or too large for one line: there may be clans the list does not show."
},
"example": {
"type": "boolean",
"example": false
}
}
},
"reason": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"nullable": {
"type": "boolean",
"example": true
},
"description": {
"type": "string",
"example": "Why the clans cannot be read, when they cannot."
}
}
}
}
}
}
},
"RustClanList": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"description": {
"type": "string",
"example": "One servers clans (GET /public/rust/servers/{id}/clans). Public: nothing here names a player."
},
"properties": {
"type": "object",
"properties": {
"clans": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "array"
},
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"properties": {
"type": "object",
"properties": {
"externalId": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "main:12:1790142840535"
}
}
},
"name": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Northwatch"
}
}
},
"color": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "string",
"example": "#3fa9f5"
}
}
},
"score": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"example": {
"type": "number",
"example": 140
}
}
},
"memberCount": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"example": {
"type": "number",
"example": 6
}
}
},
"maxMembers": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "number",
"example": 100
}
}
}
}
}
}
}
}
},
"board": {
"$ref": "#/components/schemas/RustClanBoard"
}
}
}
}
},
"RustClan": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"description": {
"type": "string",
"example": "One clan (GET /public/rust/clans/{externalId}) and, inside the roster audience, its roster."
},
"properties": {
"type": "object",
"properties": {
"clan": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"properties": {
"type": "object",
"properties": {
"externalId": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "main:12:1790142840535"
}
}
},
"name": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Northwatch"
}
}
},
"color": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "string",
"example": "#3fa9f5"
}
}
},
"score": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"example": {
"type": "number",
"example": 140
}
}
},
"memberCount": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"example": {
"type": "number",
"example": 6
}
}
},
"maxMembers": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "number",
"example": 100
}
}
},
"serverId": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "main"
}
}
},
"serverName": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Main · Vanilla"
}
}
},
"founded": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"nullable": {
"type": "boolean",
"example": true
},
"description": {
"type": "string",
"example": "When the clan was founded, epoch milliseconds."
}
}
},
"gone": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"description": {
"type": "string",
"example": "The clan has been disbanded, or has left its servers board."
},
"example": {
"type": "boolean",
"example": false
}
}
}
}
}
}
},
"roster": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"properties": {
"type": "object",
"properties": {
"visible": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"description": {
"type": "string",
"example": "Is this viewer inside the roster audience? When false, `members` is empty."
},
"example": {
"type": "boolean",
"example": false
}
}
},
"audience": {
"$ref": "#/components/schemas/RustClanAudience"
},
"members": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "array"
},
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"properties": {
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "string",
"example": "Wanderer"
}
}
},
"role": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "string",
"example": "Leader"
}
}
},
"leader": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"online": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": false
}
}
},
"joined": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "integer"
},
"nullable": {
"type": "boolean",
"example": true
},
"description": {
"type": "string",
"example": "Epoch milliseconds."
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
@@ -4326,6 +5149,9 @@
}
}
}
},
"clanRoster": {
"$ref": "#/components/schemas/RustClanAudience"
}
}
}