docs: rewrite reference prose and correct field code citations (#2592)

This commit is contained in:
Hampus
2026-09-08 17:13:37 +02:00
committed by GitHub
parent 3d38d3f694
commit 2f008b8653
76 changed files with 680 additions and 457 deletions
@@ -8,7 +8,11 @@ import RouteHeader from '@/components/RouteHeader.astro';
An Admin API key is a long-lived credential for the [Admin API](/admin-api/). It authenticates as the account that created it, has its own [ACL](/admin-api/#acl-registry) set, and is honoured only on paths below `/v1/admin`.
Every operation on this page requires `admin_api_key:manage`. None records an Admin audit entry or reads the `X-Audit-Log-Reason` header. An operation that addresses one key returns 404 `ADMIN_API_KEY_NOT_FOUND` when no key has that identifier, when another account created the key, or when the key has expired.
Every operation on this page requires `admin_api_key:manage`. None records an Admin audit entry or reads the `X-Audit-Log-Reason` header. An operation that addresses one key returns 404 `ADMIN_API_KEY_NOT_FOUND` when any of these holds:
- No key has that identifier.
- Another account created the key.
- The key has expired.
:::caution[Key management is scoped to the creating account]
The wildcard ACL reaches no key created by another account, and such a key reports `ADMIN_API_KEY_NOT_FOUND`.
@@ -18,7 +22,7 @@ The wildcard ACL reaches no key created by another account, and such a key repor
A key is owned by the account that created it, and it stores its own ACL set. Narrowing the owner and narrowing the key are separate actions.
Fluxer checks both sets when a request presents a key. A request passes when all three of these hold:
Fluxer checks both sets when a request presents a key. A request passes when all of these hold:
- The owning account holds `admin:authenticate` or `*`.
- The owning account holds the ACL the operation requires, or `*`.
@@ -38,7 +42,7 @@ A key therefore never has wider permission than the account behind it.
| last_used_at<sup>3</sup> | ?ISO8601 timestamp | Time the key last authenticated a request, or null when it never has |
| expires_at<sup>4</sup> | ?ISO8601 timestamp | Time the key expires, or null when the key does not expire |
<sup>1</sup> Bounded at 111 entries, the size of the [ACL registry](/admin-api/#acl-registry). Every value written through this API is a registry member
<sup>1</sup> Bounded at the size of the [ACL registry](/admin-api/#acl-registry). Every value written through this API is a registry member
<sup>2</sup> Never changes, so a key cannot be transferred to another account
@@ -140,7 +144,7 @@ The acting credential must already have every value in `acls`, unless it has `*`
<sup>3</sup> An empty array produces a key that satisfies no operation. Fluxer compares `acls` against the presenting key's own ACLs, so a key cannot mint a broader key
An ungrantable ACL fails with 403 `MISSING_ACL` on the first offending value, and a request that names several ungrantable ACLs still reports only that one.
Fluxer rejects the request with 403 `MISSING_ACL` on the first ungrantable value. A request that names several ungrantable ACLs reports only that one.
### Response
@@ -185,7 +189,7 @@ This operation never returns the raw credential.
Renames a key or replaces its ACL set, and returns the updated [Admin API key](#admin-api-key-object) object. Requires `admin_api_key:manage`.
An omitted field is left unchanged, and the supplied fields take effect on the key's next authenticated request. The acting credential must already have every value in a supplied `acls`, unless it has `*`. A key with an expiry keeps it.
Fluxer leaves an omitted field unchanged, and the supplied fields take effect on the key's next authenticated request. The acting credential must already have every value in a supplied `acls`, unless it has `*`. A key with an expiry keeps it.
:::caution[Supplying acls replaces the whole stored set]
An update never rotates the credential, and no field on this route changes the expiry, so revoking the key is the only way to disable it.
@@ -227,7 +231,7 @@ An update never rotates the credential, and no field on this route changes the e
<RouteHeader method="DELETE" path="/v1/admin/api-keys/{key_id}" />
Revokes an Admin API key and answers HTTP 200 with a response body. Requires `admin_api_key:manage`.
Revokes an Admin API key and returns HTTP 200 with a response body. Requires `admin_api_key:manage`.
The credential stops authenticating on its next use. A request already in flight runs to completion.
@@ -80,9 +80,14 @@ The Admin view of one application. An application and its bot account share one
## The built-in Admin application
Fluxer serves one synthetic application for its own Admin OAuth2 client. Its ID is the fixed constant `1234567890123456789`, its `name` is `Fluxer Admin`, its `owner_user_id` is the system account `0`, and its only redirect URI is the configured Admin endpoint followed by `/oauth2_callback`.
Fluxer serves one synthetic application for its own Admin OAuth2 client. The application has no bot, so every `bot_` field is null or false and `has_bot_token` is false. It also reports these values:
It has no bot, so every `bot_` field is null or false and `has_bot_token` is false. `has_client_secret` is true on every response that has it, `client_secret_created_at` is null, and `version` is always 1.
- `id` is the fixed constant `1234567890123456789`.
- `name` is `Fluxer Admin`.
- `owner_user_id` is the system account `0`.
- `oauth2_redirect_uris` has one entry, the configured Admin endpoint followed by `/oauth2_callback`.
- `has_client_secret` is true on every response that has it, and `client_secret_created_at` is null.
- `version` is always 1.
:::caution[The built-in application is read-only]
[Get application](#get-application) resolves it only while a client secret is configured for the deployment, and answers a null `application` otherwise. [List applications](#list-applications) never returns it. [Transfer application ownership](#transfer-application-ownership) answers 403 `FORBIDDEN`.
@@ -200,7 +205,7 @@ Ownership is the only field this operation writes. The new owner need not be rel
### Side effects
Fluxer rewrites the record with the new `owner_user_id` and increments its `version`. The application moves in both the `owner_id` selector of [List applications](#list-applications) and [List user applications](/admin-api/users/#list-user-applications). Fluxer emits no Gateway Dispatch.
Fluxer rewrites the record with the new `owner_user_id` and increments its `version`. The application moves in both the `owner_id` selector of [List applications](#list-applications) and [List user applications](/admin-api/users/#list-user-applications). The operation emits no Gateway Dispatch.
Fluxer records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with the target type `application`, the target ID equal to the application ID, and the action `transfer_ownership`. Its metadata is `old_owner_id` and `new_owner_id`. Neither owner is resolved into `related_users`.
@@ -8,7 +8,11 @@ import RouteHeader from '@/components/RouteHeader.astro';
An archive is a snapshot of one user account or one guild, built in the background and downloaded as a file. [Create user archive](/admin-api/users/#create-user-archive) and [Create guild archive](/admin-api/guilds/#create-guild-archive) request one. The operations here read archive state and issue time-limited download URLs.
Every operation needs an [ACL](/admin-api/#acl-evaluation) covering the subject type it touches. A user archive needs `archive:view_all` or `archive:trigger:user`. A guild archive needs `archive:view_all` or `archive:trigger:guild`. Reading both types at once needs `archive:view_all`, or `archive:trigger:user` and `archive:trigger:guild` together.
Every operation needs an [ACL](/admin-api/#acl-evaluation) covering the subject type it touches.
- A user archive needs `archive:view_all` or `archive:trigger:user`.
- A guild archive needs `archive:view_all` or `archive:trigger:guild`.
- Reading both types at once needs `archive:view_all`, or `archive:trigger:user` and `archive:trigger:guild` together.
:::note[An expired archive stops being readable]
`expires_at` is 365 days after the archive is requested and is never extended. The archive record is removed at that instant, and no operation reads the stored file afterwards.
@@ -122,7 +126,7 @@ Returns [archive](#archive-object) objects matching the supplied filters, newest
<sup>5</sup> The value counts as true only for `true`, `True`, or `1`. Expired archives are dropped after `limit` rows have been read, so a response can hold fewer than `limit` archives
:::note[The default filter narrows to the account's ACLs]
`all` is the default, and Fluxer resolves it to the subject types the account's ACLs cover. An account holding only `archive:trigger:user` reads user archives, an account holding only `archive:trigger:guild` reads guild archives, and an account holding `archive:view_all` reads both. An account holding none of the three is refused with 403 `MISSING_ACL`.
`all` is the default, and Fluxer resolves it to the subject types the account's ACLs cover. An account holding only `archive:trigger:user` reads user archives, one holding only `archive:trigger:guild` reads guild archives, and one holding `archive:view_all` reads both. An account holding none of the three is refused with 403 `MISSING_ACL`.
:::
### Response body
@@ -149,8 +153,6 @@ Returns [archive](#archive-object) objects matching the supplied filters, newest
Returns one [archive](#archive-object) object. Requires an ACL covering the subject type.
An archive is addressed by its subject and its identifier together.
### Path parameters
| Field | Type | Description |
@@ -176,7 +178,7 @@ An archive is addressed by its subject and its identifier together.
### Side effects
The read returns archive metadata and issues no download grant.
The read issues no download grant.
### Rate limit
@@ -6,11 +6,11 @@ description: The nine safety blocklists, their value forms, and the operations t
import RouteHeader from '@/components/RouteHeader.astro';
A blocklist is a stored set of values Fluxer checks account access and user content against. Nine lists exist. Each has one canonical value form, one matching rule, and its own stored fields, and Fluxer canonicalises every value before storage and before every check.
A blocklist is a stored set of values Fluxer checks account access and user content against. Nine lists exist. Each has one canonical value form, one matching rule, and its own stored fields. Fluxer canonicalises every value before storage and before every check.
Each list has its own three [Admin ACLs](/admin-api/#acl-registry). A read needs the selected list's `check` permission, an addition or an update needs its `add` permission, and a removal needs its `remove` permission, so an account holding `ban:ip:add` writes to the `ip` list and to no other. Every write records the audit reason on the [Admin audit entries](/admin-api/#admin-audit-entry-object) it produces. The reads record nothing.
A list's three values are `ban:`, the list name with each hyphen written as an underscore, and the verb, so the `url-domain` list uses `ban:url_domain:check`, `ban:url_domain:add`, and `ban:url_domain:remove`. The `email-domain-suspicious` list is the one exception and uses `suspicious_email_domain:check`, `suspicious_email_domain:add`, and `suspicious_email_domain:remove`.
Fluxer builds each permission name from `ban:`, the list name with each hyphen written as an underscore, and the verb, so the `url-domain` list uses `ban:url_domain:check`, `ban:url_domain:add`, and `ban:url_domain:remove`. The `email-domain-suspicious` list is the one exception and uses `suspicious_email_domain:check`, `suspicious_email_domain:add`, and `suspicious_email_domain:remove`.
:::note[The Admin API exposes no other blocklist]
Fluxer synchronises disposable email domains from external feeds every six hours. No operation on this page reads or writes them.
@@ -32,7 +32,7 @@ Fluxer synchronises disposable email domains from external feeds every six hours
| avatar-hash<sup>8</sup> | Avatar hashes barred from being set |
| profile-substring<sup>4</sup> <sup>9</sup> | Substrings barred from one named profile field |
<sup>1</sup> Two guards refuse an address with 400 `IP_BAN_DECLINED`: the instance exemption list, and the high blast-radius carrier network check. Fluxer records both refusals in the Admin audit log
<sup>1</sup> Fluxer refuses an address with 400 `IP_BAN_DECLINED` when it is on the instance exemption list, or when the high blast-radius carrier network check matches, and records both refusals in the Admin audit log
<sup>2</sup> Stored lowercased, so a mixed-case value does not create a second row
@@ -56,7 +56,7 @@ ACL `bulk:add:guild_members`. Adds every targeted user to one guild. Runs as `bu
ACL `bulk:delete:users`. Schedules account deletion for every targeted user. Runs as `bulkScheduleUserDeletion`.
Each worker task is recorded as `task_type` on the job and listed under [background job task types](/admin-api/jobs/#background-job-task-types). All five run in the `lifecycle` [processing lane](/admin-api/jobs/#processing-lanes).
Each worker task is recorded as `task_type` on the job and listed under [background job task types](/admin-api/jobs/#background-job-task-types). Every bulk task runs in the `lifecycle` [processing lane](/admin-api/jobs/#processing-lanes).
## Queue bulk job
@@ -67,7 +67,7 @@ Queues one bulk operation. Returns a [bulk job creation](#bulk-job-creation-obje
### Limitations
- The body matches exactly one `task` variant.
- The caller needs at least one of the five [bulk ACLs](#bulk-task-types) and then the exact ACL that `task` selects.
- The caller needs at least one of the [bulk ACLs](#bulk-task-types) and then the exact ACL that `task` selects.
:::caution[Authorisation depends on the validated body]
Body validation runs ahead of authentication and ACL evaluation, so a malformed body answers 400 even when the request has no credential.
@@ -75,7 +75,7 @@ Body validation runs ahead of authentication and ACL evaluation, so a malformed
### JSON body
The `task` discriminator selects one of these five structures. Every ID array has an upper bound and no lower bound, so an empty array queues a job that processes nothing and settles as `succeeded`.
The `task` discriminator selects one of these structures. Every ID array has an upper bound and no lower bound, so an empty array queues a job that processes nothing and settles as `succeeded`.
#### Update user flags structure
@@ -86,7 +86,7 @@ The `task` discriminator selects one of these five structures. Every ID array ha
| add_flags?<sup>1</sup> | array[string] | [Account flag](/admin-api/users/#account-flags) values to add (max 64, default empty) |
| remove_flags?<sup>1</sup> | array[string] | [Account flag](/admin-api/users/#account-flags) values to remove (max 64, default empty) |
<sup>1</sup> Each entry is one 64-bit flag value as an unsigned decimal string, such as `1024`, rather than a symbolic name. Additions are applied before removals, so a value named in both arrays ends up cleared
<sup>1</sup> Each entry is one 64-bit flag value written as an unsigned decimal string, such as `1024`. The boundary rejects a symbolic name. Additions are applied before removals, so a value named in both arrays ends up cleared
#### Update suspicious activity flags structure
@@ -148,22 +148,24 @@ The 200 confirms that the job was queued. [Get job](/admin-api/jobs/#get-job) re
This operation writes no Admin audit entry. It records one [Jobs](/admin-api/jobs/) ledger row naming the acting Admin as the requester and storing the audit reason, then enqueues the worker task. When the ledger row cannot be written, Fluxer returns 500 `INTERNAL_SERVER_ERROR` and enqueues nothing.
The worker processes entities in the submitted order, one at a time. A cancellation check runs before each entity, so [Cancel job](/admin-api/jobs/#cancel-job) stops the run between two entities and settles the job as `cancelled`. Every entity changed before cancellation or failure stays changed. An entity that fails, including an ID that resolves to nothing, is counted as failed and skipped, and the run continues through the rest of the set. The worker reports progress before the run starts, after every 25 entities, and once at the end. `schedule_user_deletion` reports after every 10 accounts instead. The closing progress message has the successful and failed counts.
The worker processes entities in the submitted order, one at a time. A cancellation check runs before each entity, so [Cancel job](/admin-api/jobs/#cancel-job) stops the run between two entities and settles the job as `cancelled`. Every entity changed before cancellation or failure stays changed. An entity that fails, including an ID that resolves to nothing, is counted as failed and skipped, and the run continues through the rest of the set.
Every task writes one summary Admin audit entry when it finishes, with the action `bulk_update_user_flags`, `bulk_update_suspicious_activity_flags`, `bulk_update_guild_features`, `bulk_add_guild_members`, or `bulk_schedule_deletion`. The summary has the audit reason, the entity count, the operation-specific parameters, and the successful and failed counts. Its `target_id` is the guild for `add_guild_members` and `0` for the four tasks that target a set. A cancelled or failed job writes no summary entry.
The worker reports progress before the run starts, after every 25 entities, and once at the end. `schedule_user_deletion` reports after every 10 accounts instead. The closing progress message has the successful and failed counts.
Updating account flags writes one `update_flags` entry for each account and dispatches [User Update](/gateway/events/#user-update) to the account's sessions. A change to a publicly visible flag also dispatches [Guild Member Update](/gateway/events/#guild-member-update) to every guild the account is in.
Every task writes one summary Admin audit entry when it finishes, with the action `bulk_update_user_flags`, `bulk_update_suspicious_activity_flags`, `bulk_update_guild_features`, `bulk_add_guild_members`, or `bulk_schedule_deletion`. The summary has the audit reason, the entity count, the operation-specific parameters, and the successful and failed counts. Its `target_id` is the guild for `add_guild_members` and `0` for every other task. A cancelled or failed job writes no summary entry.
Updating suspicious activity flags rewrites each account's verification requirements and dispatches [User Update](/gateway/events/#user-update). No [Guild Member Update](/gateway/events/#guild-member-update) follows. The task writes no per-account audit entry.
`update_user_flags` writes one `update_flags` entry for each account and dispatches [User Update](/gateway/events/#user-update) to the account's sessions. A change to a publicly visible flag also dispatches [Guild Member Update](/gateway/events/#guild-member-update) to every guild the account is in.
Updating guild features writes one `update_features` entry for each guild, dispatches [Guild Update](/gateway/events/#guild-update), and reindexes the guild for search. Fluxer reconciles a guild that already has a discovery application record against the new feature set, so gaining `DISCOVERABLE` approves the record and losing it marks the record removed. A guild with no discovery record is left alone.
`update_suspicious_activity_flags` rewrites each account's verification requirements and dispatches [User Update](/gateway/events/#user-update). No [Guild Member Update](/gateway/events/#guild-member-update) follows. The task writes no per-account audit entry.
Adding guild members bypasses the ban check and the risk gate. The task suppresses the join system message, records the join source as an Admin force add, and dispatches [Guild Member Add](/gateway/events/#guild-member-add) to the guild and [Guild Create](/gateway/events/#guild-create) to the added account's sessions. The task still enforces the per-account guild cap and the guild member cap, so an account at either ceiling is counted as failed. An account that is already a member is left unchanged and counted as successful, with no second membership and no Dispatch. Adding a bot account also records a `BOT_ADD` guild audit log entry attributed to the acting Admin.
`update_guild_features` writes one `update_features` entry for each guild, dispatches [Guild Update](/gateway/events/#guild-update), and reindexes the guild for search. Fluxer reconciles a guild that already has a discovery application record against the new feature set, so gaining `DISCOVERABLE` approves the record and losing it marks the record removed. A guild with no discovery record is left alone.
Scheduling deletion marks each account deleted. The task stores the reason code, public reason, and audit reason on the account, and reschedules its pending deletion. It dispatches [User Update](/gateway/events/#user-update), writes one `schedule_deletion` entry for each account, and emails the account holder when an address is on file. A failed email is logged and does not fail the entity.
`add_guild_members` bypasses the ban check and the risk gate. The task suppresses the join system message, records the join source as an Admin force add, and dispatches [Guild Member Add](/gateway/events/#guild-member-add) to the guild and [Guild Create](/gateway/events/#guild-create) to the added account's sessions. The task still enforces the per-account guild cap and the guild member cap, so an account at either ceiling is counted as failed. An account that is already a member is left unchanged and counted as successful, with no second membership and no Dispatch. Adding a bot account also records a `BOT_ADD` guild audit log entry attributed to the acting Admin.
`schedule_user_deletion` marks each account deleted. The task stores the reason code, public reason, and audit reason on the account, and reschedules its pending deletion. It dispatches [User Update](/gateway/events/#user-update), writes one `schedule_deletion` entry for each account, and emails the account holder when an address is on file. A failed email is logged and does not fail the entity.
:::caution[Bulk scheduling is narrower than the single-account operation]
The worker only schedules the deletion. It does not terminate sessions, cancel or refund a Stripe subscription, ban the account's identifiers, or resolve pending reports, all of which [Schedule user deletion](/admin-api/users/#schedule-user-deletion) performs for one account, the last two only when the reason is not `USER_REQUESTED`.
The worker does not terminate sessions, cancel or refund a Stripe subscription, ban the account's identifiers, or resolve pending reports. [Schedule user deletion](/admin-api/users/#schedule-user-deletion) performs each of those for one account, the last two only when the reason is not `USER_REQUESTED`.
:::
### Rate limit
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
Gateway control is the Admin view of the running [main Gateway](/gateway/overview/) cluster. It reads live node state and submits guild reload requests, and it exposes no session, presence, or call payload.
Fluxer answers every operation on this page with one RPC call to the main Gateway. A call that goes unanswered returns 504 `GATEWAY_TIMEOUT`. An overloaded cluster, or one with no responder, returns 503 `SERVICE_UNAVAILABLE`. A reply that cannot be interpreted returns 502 `BAD_GATEWAY`.
Fluxer answers every operation on this page with one RPC call to the main Gateway. An unanswered call returns 504 `GATEWAY_TIMEOUT`. An overloaded cluster, or one with no responder, returns 503 `SERVICE_UNAVAILABLE`. A reply that cannot be interpreted returns 502 `BAD_GATEWAY`.
:::note[Every value is live state]
The Gateway cluster answers each read at request time. Two consecutive reads can differ without any Admin action, and a node that is restarting can be absent from one response and present in the next.
@@ -81,13 +81,13 @@ One entry for each node the request polled, including nodes that did not answer
<sup>1</sup> A node reports its `HOSTNAME` environment value when that value is a non-blank string, and its Erlang node name otherwise
<sup>2</sup> `healthy` for a node that answered and `unavailable` for one that did not. A node that did not answer reports null in place of every counter here and contributes zero to every cluster total
<sup>2</sup> `healthy` for a node that answered and `unavailable` for one that did not. A node that did not answer reports null for every counter here and contributes zero to every cluster total
A node entry can have further diagnostic members. A client treats a member it does not recognise as absent.
## Node memory object
Byte counts for one node, or for every polled node summed together. Every value is a decimal string because a JSON number cannot preserve a 64-bit byte count.
Byte counts for one node, or for every polled node summed together. Each count is a decimal string because a JSON number cannot preserve a 64-bit byte count.
### Structure
@@ -73,7 +73,7 @@ Reading a code back requires presenting it to [Get gift](/http-api/gifts/#get-gi
### Side effects
Each code is 32 characters drawn from the uppercase letters, the lowercase letters, and the digits. Each code is created unredeemed and records the system account with ID `0` as its creator, which is the creator the [gift object](/http-api/gifts/#gift-object) reports to the redeemer.
Each code is 32 characters drawn from the uppercase letters, the lowercase letters, and the digits. Fluxer creates it unredeemed and records the system account with ID `0` as its creator, which is the creator the [gift object](/http-api/gifts/#gift-object) reports to the redeemer.
Fluxer writes the codes one at a time. A failure part way through leaves the codes already written redeemable while the response reports the failure. Those codes appear in no response.
@@ -6,7 +6,7 @@ description: Guild search, detail, mutation, deletion, membership, expressions,
import RouteHeader from '@/components/RouteHeader.astro';
Admin guild operations read and change the guilds the public [Guilds](/http-api/guilds/) resource exposes. Most of them need no membership in the guild and no guild permission, and [Remove guild member](#remove-guild-member) and [Ban guild member](#ban-guild-member) are the two exceptions.
Admin guild operations read and change the guilds the public [Guilds](/http-api/guilds/) resource exposes. Most of them need no membership in the guild and no guild permission, and [Remove guild member](#remove-guild-member) and [Ban guild member](#ban-guild-member) are the exceptions.
[Archives](/admin-api/archives/) owns the archive lifecycle and the downloads.
@@ -385,7 +385,14 @@ Supplying `add_features` or `remove_features` reconciles an existing discovery a
Every applied group refreshes the guild's entry in the guild search index and fires one [Guild Update](/gateway/events/#guild-update) Dispatch to every session that can see the guild, including when the write changes no value.
Each applied group records one Admin audit entry with the target type `guild` and the guild ID as the target. The actions are `clear_fields` with the cleared field names, `update_settings` with each applied setting, and `update_features` with the added, removed, and resulting feature sets. The remaining actions are `update_name` with the old and new names, `update_vanity` with the old and new codes, and `transfer_ownership` with the old and new owner IDs.
Each applied group records one Admin audit entry with the target type `guild` and the guild ID as the target.
- `clear_fields` records the cleared field names.
- `update_settings` records each applied setting.
- `update_features` records the added, removed, and resulting feature sets.
- `update_name` records the old and new names.
- `update_vanity` records the old and new codes.
- `transfer_ownership` records the old and new owner IDs.
### Rate limit
@@ -512,7 +519,7 @@ Only the Admin ACL is evaluated.
Fluxer creates the membership with the Admin force-add join source and restores a communication timeout still in force from a previous membership. The guild ban list is not consulted, so a banned user can be admitted. The suspicious activity phone gate does not run. The per-user guild limit and the guild member limit are still enforced.
[Guild Member Add](/gateway/events/#guild-member-add) fires to the guild, the user's sessions are joined to the guild on the main Gateway, and the member enters guild member search when the guild has an indexed member set. The ordinary join system message is created, and with it a [Message Create](/gateway/events/#message-create) Dispatch, unless the guild sets `SUPPRESS_JOIN_NOTIFICATIONS` or has no usable system channel. A bot target additionally records a `BOT_ADD` entry in the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions).
[Guild Member Add](/gateway/events/#guild-member-add) fires to the guild, the user's sessions are joined to the guild on the main Gateway, and the member enters guild member search when the guild has an indexed member set. The ordinary join system message is created, and with it a [Message Create](/gateway/events/#message-create) Dispatch, unless the guild sets `SUPPRESS_JOIN_NOTIFICATIONS` or has no usable system channel. A bot target also records a `BOT_ADD` entry in the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions).
A user who is already a member keeps their existing membership. No membership is created, no counter moves, and no Dispatch is emitted. The Admin audit entry is still written.
@@ -12,7 +12,13 @@ The Admin API is the privileged surface for administering an instance: its accou
Every Admin operation accepts a user session token as a bare `Authorization` value, an access token issued by Fluxer's built-in Admin OAuth2 application as `Bearer <token>`, or an Admin API key as `Admin <token>`. Fluxer resolves a value in the user session token form as a session token even under the `Bearer` prefix. [HTTP authentication](/authentication/) defines the credential syntax.
Fluxer evaluates the credential in a fixed order. A request that resolves no account is refused with 401 `UNAUTHORIZED`, as is a credential presented with the `Bot` scheme. An access token issued to any OAuth2 application other than the built-in Admin application is refused with 403 `ACCESS_DENIED`. An account holding neither `admin:authenticate` nor the wildcard `*` is refused with 403 `MISSING_PERMISSIONS`. An account that clears both gates but satisfies none of the [ACLs](#acl-evaluation) the operation names is refused with 403 `MISSING_ACL`.
Fluxer evaluates the credential in a fixed order.
1. A request that resolves no account is refused with 401 `UNAUTHORIZED`.
2. A credential presented with the `Bot` scheme is refused with 401 `UNAUTHORIZED`.
3. An access token issued to any OAuth2 application other than the built-in Admin application is refused with 403 `ACCESS_DENIED`.
4. An account holding neither `admin:authenticate` nor the wildcard `*` is refused with 403 `MISSING_PERMISSIONS`.
5. An account that clears both gates but satisfies none of the [ACLs](#acl-evaluation) the operation names is refused with 403 `MISSING_ACL`.
:::caution[A denial names no requirement]
The `MISSING_ACL` body is a fixed sentence that has no ACL name and no requirement list, so a response never discloses which requirement failed.
@@ -30,15 +36,29 @@ Admin authorisation runs before request validation on every operation except [Qu
An operation names one or more ACLs and admits the request when the caller holds at least one of them. The wildcard `*` satisfies every requirement on its own.
An operation can stack several requirements, each evaluated separately, and the caller must satisfy all of them. [Create NCMEC report](/admin-api/messages/#create-ncmec-report) is the widest example and stacks `csam:submit_ncmec`, `message:delete`, `user:delete`, and `archive:trigger:user` as four independent requirements.
Where an operation stacks several requirements, Fluxer evaluates each separately and the caller must satisfy all of them. [Create NCMEC report](/admin-api/messages/#create-ncmec-report) is the widest example and stacks `csam:submit_ncmec`, `message:delete`, `user:delete`, and `archive:trigger:user` as independent requirements.
The effective ACL set of a session or an Admin OAuth2 bearer credential is the set stored on the account. The effective set of an Admin API key is the set stored on the key, subject to the owner check above. [Set user ACLs](/admin-api/users/#set-user-acls) writes an account's set. Holding `admin:authenticate` or `*` is the whole definition of an account that can reach the Admin API.
Two operations derive their required ACLs from the validated request body.
[Update guild](/admin-api/guilds/#update-guild) maps each present body field to one ACL and requires every ACL in that set. `name` maps to `guild:update:name`, `vanity_url_code` maps to `guild:update:vanity`, `new_owner_id` maps to `guild:transfer_ownership`, `add_features` and `remove_features` map to `guild:update:features`, and `fields` together with every remaining setting maps to `guild:update:settings`. A body with none of those fields resolves to no ACL at all and applies no change. The route also names those five ACLs as an ordinary any-of requirement, evaluated before the body is read.
[Update guild](/admin-api/guilds/#update-guild) maps each present body field to one ACL and requires every ACL in that set.
[Queue bulk job](/admin-api/bulk-jobs/#queue-bulk-job) maps its `task` discriminator to one ACL and requires that one. `update_user_flags` maps to `bulk:update:user_flags`, `update_suspicious_activity_flags` maps to `bulk:update:suspicious_activity`, `update_guild_features` maps to `bulk:update:guild_features`, `add_guild_members` maps to `bulk:add:guild_members`, and `schedule_user_deletion` maps to `bulk:delete:users`.
- `name` maps to `guild:update:name`.
- `vanity_url_code` maps to `guild:update:vanity`.
- `new_owner_id` maps to `guild:transfer_ownership`.
- `add_features` and `remove_features` map to `guild:update:features`.
- `fields` together with every remaining setting maps to `guild:update:settings`.
A body with none of those fields resolves to no ACL at all and applies no change. The route also names those ACLs as an ordinary any-of requirement, evaluated before the body is read.
[Queue bulk job](/admin-api/bulk-jobs/#queue-bulk-job) maps its `task` discriminator to one ACL and requires that one.
- `update_user_flags` maps to `bulk:update:user_flags`.
- `update_suspicious_activity_flags` maps to `bulk:update:suspicious_activity`.
- `update_guild_features` maps to `bulk:update:guild_features`.
- `add_guild_members` maps to `bulk:add:guild_members`.
- `schedule_user_deletion` maps to `bulk:delete:users`.
Fluxer bounds every grant separately. [Set user ACLs](/admin-api/users/#set-user-acls) and [Create Admin API key](/admin-api/api-keys/#create-admin-api-key) both refuse to write an ACL the acting Admin does not itself hold, with 403 `MISSING_ACL`. A wildcard holder is exempt. Set user ACLs additionally refuses the acting Admin's own account with 403 `ACCESS_DENIED`, and it resolves the target before the bound is evaluated, so an unknown ID fails first with 404 `UNKNOWN_USER`.
@@ -96,9 +116,9 @@ An entry stores an acting Admin, a target type, a target ID, an action name, an
<sup>7</sup> Keys are 1 to 256 characters and values are 0 to 4,000 characters. The keys an operation records are stated by that operation
Fluxer populates the three `related_` maps from the acting Admin, from the resolved target, and from the metadata. A metadata key contributes when its value is a decimal integer and its name is `user_id`, `guild_id`, `channel_id`, `target_user_id`, or `admin_user_id`, or ends in `_user_id`, `_guild_id`, or `_channel_id`. An identifier that no longer resolves is omitted from the map.
Fluxer populates the `related_` maps from the acting Admin, from the resolved target, and from the metadata. A metadata key contributes when its value is a decimal integer and its name is `user_id`, `guild_id`, `channel_id`, `target_user_id`, or `admin_user_id`, or ends in `_user_id`, `_guild_id`, or `_channel_id`. An identifier that no longer resolves is omitted from the map.
An entry has no request method, request path, status, duration, IP address, or field change list. An entry is never redacted on read, so a caller holding `audit_log:view` reads `audit_log_reason` and every metadata value in full.
An entry has no request method, request path, status, duration, IP address, or field change list. Fluxer never redacts an entry on read, so a caller holding `audit_log:view` reads `audit_log_reason` and every metadata value in full.
### Example
@@ -432,6 +452,7 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi
| ban:profile_substring:remove | Removes `profile-substring` blocklist entries |
| bulk:add:guild_members | Queues the bulk guild member addition task |
| bulk:delete:users | Queues the bulk account deletion task |
| bulk:delete:user_messages | Queues the bulk message deletion task |
| bulk:update:guild_features | Queues the bulk guild feature task |
| bulk:update:suspicious_activity | Queues the bulk suspicious activity flag task |
| bulk:update:user_flags | Queues the bulk account flag task |
@@ -337,7 +337,7 @@ Returned when `message_id` or `attachment_id` was supplied. It is the same body
| --- | --- | --- |
| 200 | search or lookup response body | Search or lookup completed |
An unknown channel, a message that does not exist, and an attachment whose filename does not match all return an empty result. This operation never answers 404.
The result is empty for an unknown channel, for a message that does not exist, and for an attachment whose filename does not match. This operation never answers 404.
With no message search service configured the search mode returns an empty `messages` array and `total` zero. The two lookup modes are unaffected.
@@ -15,7 +15,7 @@ Every write takes effect once each API node reloads its topology. No write moves
:::
:::caution[Each write runs in three separate steps]
It stores the record, notifies every node to reload, then records the audit entry. A failure at the second or third step answers 500 with the record already stored, so read the record back before retrying.
A write stores the record, notifies every node to reload, then records the audit entry. A failure at the second or third step answers 500 with the record already stored, so read the record back before retrying.
:::
## Media transport
@@ -44,7 +44,7 @@ With a guild present, a guild named by `allowed_guild_ids` is admitted at once a
A region record has the identity a client sees, the coordinate placement measures distance from, and the eligibility fields described under [placement eligibility](#placement-eligibility). It has no capacity, no health, and no server count.
The operator supplies `id` on creation, and it is the primary key. It is the value a channel stores as its `rtc_region` and the value [Modify call region](/http-api/calls/#modify-call-region) accepts, so changing it means creating a new region and deleting the old one.
The operator supplies `id` on creation, and it is the primary key. A channel stores it as its `rtc_region`, and [Modify call region](/http-api/calls/#modify-call-region) accepts it, so changing it means creating a new region and deleting the old one.
### Structure
@@ -95,7 +95,7 @@ The operator supplies `id` on creation, and it is the primary key. It is the val
A server record names one LiveKit deployment, the API key pair the instance authenticates to it with, and its own copy of the eligibility fields. A server is reachable for placement only when its region is also reachable.
The `region_id` and `server_id` pair addresses one server, and a server belongs to exactly one region. The same server identifier can exist in two regions, and moving a server between regions means deleting it and recreating it. Both are operator-chosen strings of 1 to 64 characters, and no operation renames either one.
The `region_id` and `server_id` pair addresses one server, and a server belongs to exactly one region. The same server identifier can exist in two regions. Moving a server between regions means deleting it and recreating it. Both identifiers are operator-chosen strings of 1 to 64 characters, and no operation renames either one.
A server can have its own coordinate. When it does, placement measures distance from that coordinate to pick the closest server for an automatically placed session. When it does not, the server takes no part in distance comparison.
@@ -125,7 +125,7 @@ A server can have its own coordinate. When it does, placement measures distance
<sup>4</sup> Duplicate entries collapse and the returned order is not the submitted order
:::caution[Server credentials are never returned]
`api_key` and `api_secret` are stored on the record and accepted by the create and update bodies, but no read returns them. A lost secret has to be replaced with an update.
`api_key` and `api_secret` are stored on the record and accepted by the create and update bodies. No read returns them, so a lost secret has to be replaced with an update.
:::
### Example
+16 -19
View File
@@ -20,11 +20,11 @@ An `Authorization` credential MUST NOT be copied into a URL. Webhook tokens, sig
## Authorization header
The header value must have no leading or trailing whitespace. A padded value never authenticates. A value beginning with `Bot `, `Bearer `, or `Admin ` selects that scheme, and the remainder must be non-empty and must have no surrounding whitespace either. The three prefixes match exactly, so any other spelling is not recognised as a scheme.
The header value must have no leading or trailing whitespace, and a padded value never authenticates. A value beginning with `Bot `, `Bearer `, or `Admin ` selects that scheme, and the remainder must be non-empty and must have no surrounding whitespace either. The prefixes match exactly, so any other spelling is not recognised as a scheme.
A value containing no space is parsed as a bare user session token. A value containing a space without a recognised scheme prefix is invalid.
An invalid, unknown, or unresolvable credential leaves the request unauthenticated, and the matched operation's authorisation policy decides the outcome. An operation that requires a credential returns 401 `UNAUTHORIZED`.
Fluxer leaves the request unauthenticated when the credential is invalid, unknown, or unresolvable, and the matched operation's authorisation policy decides the outcome. An operation that requires a credential returns 401 `UNAUTHORIZED`.
A user session token is sent bare, with no scheme prefix.
@@ -44,7 +44,7 @@ The value after `Bearer ` is an OAuth2 access token.
Authorization: Bearer IE867jBd9L4M0_tGI8OUOppXVezR1u6x8Yj-Lduilxg
```
The value after `Admin ` is an Admin API key, which is read on a route below `/v1/admin` and nowhere else.
The value after `Admin ` is an Admin API key.
```text
Authorization: Admin fa_1508923117441703936_KaqkNax1BF3YSWHGkEPjDRKeO48jGb9F
@@ -92,9 +92,7 @@ Rotation applies to a bot token and a client secret, and rotating a bot token al
## User session tokens
A user session token authenticates an ordinary user account. It is issued by the login, registration, and session exchange operations documented in [Authentication](/http-api/authentication/). A token that does not identify a live session leaves the request unauthenticated.
A session token is also the credential the Gateway [Identify](/gateway/commands/#identify) command accepts for a user session.
A user session token authenticates an ordinary user account. It is issued by the login, registration, and session exchange operations documented in [Authentication](/http-api/authentication/). A token that does not identify a live session leaves the request unauthenticated. The Gateway accepts a user session token in [Identify](/gateway/commands/#identify).
The `Authorization` header holds a single credential. A [sudo mode](#sudo-mode) proof travels separately, in the `X-Fluxer-Sudo-Mode-JWT` header, and it proves that the already resolved account recently re-verified.
@@ -130,23 +128,24 @@ The response body has this member alongside `code` and `message`.
An Admin API key is read only on a route below `/v1/admin`, and an unknown, expired, or invalid key leaves the request unauthenticated. A valid key authenticates as the user who created it, and the request has the ACLs stored on the key.
An Admin operation also accepts a user session token and an OAuth2 bearer token, and a bearer token is accepted only when it belongs to the built-in Admin OAuth2 application. A bearer token from any other application returns 403 `ACCESS_DENIED`. A request with a bot token returns 401 `UNAUTHORIZED`.
Fluxer also accepts a user session token or an OAuth2 bearer token on an Admin operation, and it accepts the bearer token only when it belongs to the built-in Admin OAuth2 application. A bearer token from any other application returns 403 `ACCESS_DENIED`. A request with a bot token returns 401 `UNAUTHORIZED`.
On every Admin request the resolved user must hold the `admin:authenticate` ACL or the wildcard, and a user without either returns 403 `MISSING_PERMISSIONS`. A key-authenticated request is checked twice, and either failure returns 403 `MISSING_ACL`. The [Admin API](/admin-api/) hub defines the complete ACL registry, the evaluation modes, the double check, and the audit contract.
## Authorisation outcomes
An operation that requires a credential declares one authorisation policy. There are four.
An operation that requires a credential declares one of the four authorisation policies:
A user operation requires a resolved user and rejects an OAuth2 bearer credential it has not opted into. A user-only operation rejects a bot account as well. A bot operation accepts a bot token, which resolves the application's bot account as the request identity. An OAuth2 operation requires the `Bearer` scheme together with the scope it names. An Admin operation requires a session, Admin OAuth2 bearer, or Admin API key credential together with the required ACLs.
- A user operation requires a resolved user and rejects an OAuth2 bearer credential it has not opted into. A user-only operation rejects a bot account as well.
- A bot operation accepts a bot token, which resolves the application's bot account as the request identity.
- An OAuth2 operation requires the `Bearer` scheme together with the scope it names.
- An Admin operation requires a session, Admin OAuth2 bearer, or Admin API key credential together with the required ACLs.
No authorisation policy requires the `Bot` scheme itself. [`GET /v1/applications/@me`](/http-api/applications/#get-bot-application) is the only operation that requires the prefix.
Fluxer still parses and resolves a credential sent to an operation that requires none. The resolved account keys the [rate limit](/topics/rate-limits/) buckets and can waive a [captcha](/topics/captcha/) requirement. Some operations read the resolved account or the raw header, and each states that on its own page.
Fluxer still parses and resolves a credential sent to an operation that requires none. The [rate limit](/topics/rate-limits/) buckets are keyed by the resolved account, and that account can waive a [captcha](/topics/captcha/) requirement. Some operations read the resolved account or the raw header, and each states that on its own page.
Fluxer answers with 401 when it resolves no usable identity, and with 403 when it resolves one the operation refuses.
A missing, malformed, unknown, expired, or revoked credential returns 401 `UNAUTHORIZED`. A bot token on an Admin operation and a non-bearer credential on a bearer-only operation return 401 as well.
Fluxer answers with 401 when it resolves no usable identity, and with 403 when it resolves one the operation refuses. A missing, malformed, unknown, expired, or revoked credential returns 401 `UNAUTHORIZED`. A bot token on an Admin operation and a non-bearer credential on a bearer-only operation return 401 as well.
A resolved identity that the operation refuses returns 403 `ACCESS_DENIED`. That is the outcome for a bot account on a user-only operation and for a bearer credential on an operation that did not opt into OAuth2. An Admin OAuth2 bearer credential issued to an application other than the built-in Admin application returns 403 `ACCESS_DENIED` as well.
@@ -175,9 +174,7 @@ Enforcement applies at those operations only. It does not gate password change o
## Account state gates
The ordinary login requirement rejects an account that has effective suspicious activity flags with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
A requirement disappears from the response as soon as it is met.
The ordinary login requirement rejects an account that has effective suspicious activity flags with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. A requirement disappears from the response as soon as it is met.
### Account suspicious activity body
@@ -200,17 +197,17 @@ No shared gate rejects a deleted or disabled account. Each operation that reads
## Failed authentication
An unknown, expired, revoked, or malformed credential all return 401 `UNAUTHORIZED`. A client cannot tell the four apart from the response. A valid credential whose identity the operation resolves and refuses returns 403 `ACCESS_DENIED`, as [authorisation outcomes](#authorisation-outcomes) sets out.
An unknown, expired, revoked, or malformed credential returns 401 `UNAUTHORIZED`, and the response does not say which. A valid credential whose identity the operation resolves and refuses returns 403 `ACCESS_DENIED`, as [authorisation outcomes](#authorisation-outcomes) sets out.
Fluxer records a malformed header and a credential that resolves nothing against the originating address. An Admin API key presented outside `/v1/admin` records nothing.
Two triggers ban an address. Enough distinct rejected tokens inside the tracking window ban it on the first crossing. A failure score over its threshold bans it only after it crosses that threshold in several separate windows. The window and both thresholds are instance configuration. Fluxer never applies an automatic ban to an address it classifies as mobile. A banned address is refused before the operation runs, as [Errors](/http-api/errors/) sets out.
Two triggers ban an address. Fluxer bans it on the first crossing of the distinct rejected token threshold inside the tracking window. A failure score over its threshold bans the address only after the score crosses that threshold in several separate windows. The window and both thresholds are instance configuration. Fluxer never applies an automatic ban to an address it classifies as mobile. A banned address is refused before the operation runs, as [Errors](/http-api/errors/) sets out.
## Sudo mode
Sudo mode is a short-lived proof that the account holder recently re-verified a credential. Each operation that requires it states that on its own page, and [Multi-factor authentication](/http-api/users/mfa/#sudo-mode) defines the accepted proofs, the [sudo verification object](/http-api/users/mfa/#sudo-verification-object) fields, and the [sudo mode methods object](/http-api/users/mfa/#sudo-mode-methods-object) returned with 403 `SUDO_MODE_REQUIRED`.
A sudo proof is an HS256 JSON Web Token with the account ID as its subject, the fixed claim `type` set to `sudo`, an issue time, and an expiry five minutes after issue. A client presents it in the `X-Fluxer-Sudo-Mode-JWT` request header. An invalid, expired, or account-mismatched token is treated as absent. A bad proof and a missing proof produce the same response.
A sudo proof is an HS256 JSON Web Token with the account ID as its subject, the fixed claim `type` set to `sudo`, an issue time, and an expiry five minutes after issue. A client presents it in the `X-Fluxer-Sudo-Mode-JWT` request header. An invalid, expired, or account-mismatched token produces the same response as a missing one.
Fluxer mints a token only for an account holding a multi-factor authenticator, so a password-only account re-verifies for each operation that requires sudo mode. [Create WebAuthn registration options](/http-api/users/mfa/#create-webauthn-registration-options) and [Disable current account](/http-api/users/current-user/#disable-current-account) mint no token and return no header even for a multi-factor account. A bot account satisfies sudo mode immediately. So does an account that has neither a password nor a multi-factor authenticator.
+8 -5
View File
@@ -14,7 +14,7 @@ Normative force does not depend on a keyword appearing. A direct statement such
## Protocol subjects
These subjects have the meanings below throughout the reference.
Each subject below has the same meaning on every reference page.
| Subject | Description |
| --- | --- |
@@ -67,7 +67,7 @@ A state-transition table uses `Event and condition`, `Action`, and `Next state`
On a request that modifies a stored entity and accepts a subset of its fields, omitting an optional field leaves the stored value unchanged. Sending `null` for a nullable field clears it. A field that is optional but not nullable can be set or left unchanged. A field that is nullable but not optional is always present, even when its value is `null`.
An operation that departs from either default states the departure in the field's description, in a footnote, or beside its body table. A departure can run in either direction, so an operation can accept `null` without clearing and can change a stored value that the request never named. An operation can also define an empty string or an empty array as the clearing value, and a supplied array replaces the stored collection completely.
Where an operation departs from either default, it states the departure in the field's description, in a footnote, or beside its body table. A departure can run in either direction, so an operation can accept `null` without clearing and can change a stored value that the request never named. An operation can also define an empty string or an empty array as the clearing value, and a supplied array replaces the stored collection completely.
[Modify meme](/http-api/memes/#modify-meme) accepts this body, which leaves the stored tags unchanged, clears the alt text, and sets the name:
@@ -111,15 +111,18 @@ Prose then states the contract. The subsections that apply follow it in this ord
A subsection that does not apply is omitted. An object that a page defines has its own field table under `Structure`.
A response table uses `Status`, `Body`, and `Condition` columns. A `Body` cell uses the same type notation as a wire table, names `empty` where the response has no body, and names `response body` where the preceding subsection defines it. A response table has no header column. The shared contract is defined once under [standard response headers](/http-api/#standard-response-headers). A header an operation sets for itself is stated in prose under the operation.
A response table uses `Status`, `Body`, and `Condition` columns. A `Body` cell uses the same type notation as a wire table, names `empty` where the response has no body, and names `response body` where the preceding subsection defines it. A response table has no header column. The shared contract is defined once under [standard response headers](/http-api/#standard-response-headers), and a header an operation sets for itself is stated in prose under the operation.
## Describing behaviour
A present-tense statement about Fluxer states an observable contract. An internal storage, service, queue, or worker detail appears only where it determines an observable ordering rule, durability guarantee, limit, timeout, error, or security boundary.
Each failure names the value a client branches on. An HTTP API or Admin API failure names its status and its stable error `code`. An OAuth2 protocol failure answers with the RFC 6749 envelope, which has no Fluxer `code` and is matched on its `error` value. A Media Proxy failure has a plain-text reason phrase and no machine-readable code, so a client branches on its HTTP status. A WebSocket failure names its close code and, where the protocol defines one, the exact close reason.
Each failure names the value a client branches on. A human-readable message can be localised, so a client matches only the machine-readable value its surface defines.
A human-readable message can be localised, so a client matches only the machine-readable value its surface defines.
- An HTTP API or Admin API failure names its status and its stable error `code`.
- An OAuth2 protocol failure answers with the RFC 6749 envelope, which has no Fluxer `code` and is matched on its `error` value.
- A Media Proxy failure has a plain-text reason phrase and no machine-readable code, so a client branches on its HTTP status.
- A WebSocket failure names its close code and, where the protocol defines one, the exact close reason.
## Limits and bounds
@@ -115,7 +115,7 @@ A malformed `shard` closes with `4010` and reason `Invalid shard`.
Fluxer refuses a bot session that resolves to more than 2,500 guilds after any `shard` filter is applied. The connection closes with `4011` and reason `Sharding required`. A user session is never refused for its guild count.
A token the backend rejects closes with `4004` and reason `Invalid token`. A non-bot account that already holds 100 live sessions closes with `4008` and reason `Too many sessions`, and a bot credential is not bounded by that count. An Identify sent on a socket that already has a session attached closes with `4005` and reason `Already authenticated`, whether or not it has `d`.
A token the backend rejects closes with `4004` and reason `Invalid token`. A non-bot account that already holds 100 live sessions closes with `4008` and reason `Too many sessions`. A bot credential is not bounded by that count. An Identify sent on a socket that already has a session attached closes with `4005` and reason `Already authenticated`, whether or not it has `d`.
### Session flags
@@ -201,9 +201,9 @@ Opcode `6` restores a retained session.
| session_id | string | The session ID from [Ready](/gateway/events/#ready) |
| seq | integer | The last Dispatch sequence the client processed |
All three fields are required. A missing field, a non-string `token` or `session_id`, or a `seq` that is not an integer closes with `4002` and reason `Invalid resume payload`.
All fields are required. A missing field, a non-string `token` or `session_id`, or a `seq` that is not an integer closes with `4002` and reason `Invalid resume payload`.
An unknown or expired session produces Opcode `9` with `d: false` and leaves the socket unauthenticated. A token that does not own the session closes with `4004` and reason `Invalid token`. A `seq` above the session's current sequence, or below the sequence it has already acknowledged, closes with `4007` and reason `Invalid sequence`. A `seq` below the [replay floor](/gateway/limits-and-rate-limits/#replay-and-backpressure), the highest sequence already dropped from the buffer, also produces Opcode `9` with `d: false`. A negative `seq` closes with `4000` and reason `Session unavailable`, and so does a session that cannot be reached. None of those closes destroys a separately retained session.
An unknown or expired session produces Opcode `9` with `d: false` and leaves the socket unauthenticated. A `seq` below the [replay floor](/gateway/limits-and-rate-limits/#replay-and-backpressure), the highest sequence already dropped from the buffer, produces the same frame. A token that does not own the session closes with `4004` and reason `Invalid token`. A `seq` above the session's current sequence, or below the sequence it has already acknowledged, closes with `4007` and reason `Invalid sequence`. A negative `seq` closes with `4000` and reason `Session unavailable`, and so does a session that cannot be reached. None of those closes destroys a separately retained session.
A successful Resume replays every retained Dispatch strictly above `seq` in order and finishes with [Resumed](/gateway/events/#resumed). It also replaces the session's socket, and the displaced socket receives Opcode `7` followed by a close.
@@ -365,7 +365,9 @@ Fluxer skips a guild the session is not currently connected to. A request that r
A bot requests one guild at a time, and a bot request naming two or more guilds is abandoned.
An empty `query`, a `limit` of `0`, and an empty `user_ids` together request the complete member list. A human account requesting the complete list needs `MANAGE_ROLES`, `KICK_MEMBERS`, and `BAN_MEMBERS` together in that guild, and a request holding only some of them is dropped silently. The guild owner and any member with `ADMINISTRATOR` satisfy that check. A bot requesting the complete list is limited to one accepted request per guild every 30 seconds. A request inside that window produces [Rate Limited](/gateway/events/#rate-limited) and no member chunk.
An empty `query`, a `limit` of `0`, and an empty `user_ids` together request the complete member list. A human account requesting the complete list needs `MANAGE_ROLES`, `KICK_MEMBERS`, and `BAN_MEMBERS` together in that guild, and a request holding only some of them is dropped silently. The guild owner and any member with `ADMINISTRATOR` satisfy that check.
A bot requesting the complete list is limited to one accepted request per guild every 30 seconds. A request inside that window produces [Rate Limited](/gateway/events/#rate-limited) and no member chunk.
Results arrive as [Guild Members Chunk](/gateway/events/#guild-members-chunk) in pages of at most 1,000 members, each with `chunk_index` and `chunk_count`. Those chunks are delivered live and are never retained for Resume replay.
@@ -6,7 +6,7 @@ description: The four gates a Dispatch passes before it reaches a socket, and th
A [Dispatch](/gateway/events/) is one event Fluxer sends to a connected client. Each one passes four independent gates on its way to a socket, and a client shapes its traffic with [Lazy Request](/gateway/commands/#lazy-request) subscriptions and the [Identify](/gateway/commands/#identify) `ignored_events` list.
Fluxer has no intent bitfield. A client ported from a protocol that uses intents replaces its intent mask with those two mechanisms. There is no `intents` field, no intent close code, and no privileged-intent approval.
Fluxer has no `intents` field, no intent close code, and no privileged-intent approval. A client ported from a protocol that uses intents replaces its intent mask with those two mechanisms.
## The four gates
@@ -25,7 +25,7 @@ An account-scoped Dispatch skips gates 1 through 3 and is subject only to gate 4
## Permission and visibility
The guild resolves each event to one of five recipient sets.
The guild resolves each event to one of these recipient sets.
| Event class | Events | Recipients |
| --- | --- | --- |
@@ -108,7 +108,7 @@ A bot session holds no friend or group direct message presence subscriptions, so
## Ignored events
Identify accepts `ignored_events`, an array of up to 256 Dispatch event names. Fluxer upper-cases and deduplicates the names at Identify. An absent field and a JSON `null` both mean the empty list. Any other value that is not an array of strings, and any array holding more than 256 entries, close the connection with `4002` and reason `Invalid identify payload`. A Dispatch whose `t` appears in the list is dropped and never enters the replay buffer.
Identify accepts `ignored_events`, an array of up to 256 Dispatch event names. Fluxer upper-cases and deduplicates the names at Identify. An absent field and a JSON `null` both mean the empty list. Fluxer closes the connection with `4002` and reason `Invalid identify payload` for any other value that is not an array of strings, and for any array of more than 256 entries. A Dispatch whose `t` appears in the list is dropped and never enters the replay buffer.
```json
{
@@ -121,7 +121,7 @@ Identify accepts `ignored_events`, an array of up to 256 Dispatch event names. F
}
```
One exception overrides the list. [Message Create](/gateway/events/#message-create) is delivered even when `MESSAGE_CREATE` is ignored, if the message names the session's user in `mentions`, sets `mention_here`, or sets `mention_everyone`. A role mention does not defeat the list.
[Message Create](/gateway/events/#message-create) is the one exception. Fluxer delivers it even when `MESSAGE_CREATE` is ignored, if the message names the session's user in `mentions`, sets `mention_here`, or sets `mention_everyone`. A role mention does not defeat the list.
A suppressed Dispatch consumes no sequence number, so a client MUST NOT expect a gap in the sequence where the list dropped one.
@@ -30,7 +30,7 @@ A Dispatch is a message from the [Gateway](/gateway/overview/). It tells a clien
A guild-scoped Dispatch is filtered by guild availability, then by channel visibility and permissions, then by the session's active or passive state, and finally by the session-level shard filter and `ignored_events` list. An account-scoped Dispatch is subject only to the session-level filters. [Event filtering](/gateway/event-filtering/) defines each gate.
Most guild-scoped Dispatches have a `guild_id` string. Three identify the guild as `id`: [Guild Create](#guild-create), [Guild Sync](#guild-sync), and every [Guild Delete](#guild-delete) other than the one the guild itself dispatches when the guild is deleted. [Guild Counts Update](#guild-counts-update) and [Channel Member Counts Update](#channel-member-counts-update) have no top-level `guild_id`, and each entry in their `counts` array has its own.
Most guild-scoped Dispatches have a `guild_id` string. [Guild Create](#guild-create) and [Guild Sync](#guild-sync) identify the guild as `id`, and so does every [Guild Delete](#guild-delete) other than the one the guild itself dispatches when the guild is deleted. [Guild Counts Update](#guild-counts-update) and [Channel Member Counts Update](#channel-member-counts-update) have no top-level `guild_id`, and each entry in their `counts` array has its own.
The originating session is excluded from a Dispatch only for [Message Reaction Add](#message-reaction-add) and [Message Reaction Remove](#message-reaction-remove) in a guild channel, and only when the request supplied a `session_id`. That field is removed from the payload. The same field on a direct message or group direct message reaction is forwarded to every recipient unchanged and excludes nobody. The actor that issues any other mutation receives the resulting Dispatch like every other eligible session.
@@ -393,7 +393,7 @@ Fluxer sends a sync when the subscription flips the guild between active and pas
A guild's configuration changed. The payload is the complete [guild object](/http-api/guilds/#guild-object) with `guild_id` added, which repeats the object's own `id`.
An unavailable guild still dispatches Guild Update, and nothing else. A client learns from it that a guild entered or left the unavailable state.
Guild Update is the only Dispatch an unavailable guild sends. A client learns from it that a guild entered or left the unavailable state.
### <span id="guild-delete"></span>GUILD_DELETE
@@ -485,7 +485,7 @@ One operation changed several channels together, most often a reorder.
| guild_id | snowflake | Guild the channels belong to |
| channels | array[[channel](/http-api/channels/#channel-object) object] | Every changed channel in its complete updated representation |
Each recipient's copy of `channels` is trimmed to the channels that recipient can view, so two sessions in the same guild can receive different arrays from one operation. A recipient whose trimmed array would be empty receives no Dispatch at all and consumes no sequence number.
Fluxer trims each recipient's copy of `channels` to the channels that recipient can view, so two sessions in the same guild can receive different arrays from one operation. A recipient whose trimmed array would be empty receives no Dispatch at all and consumes no sequence number.
### <span id="channel-delete"></span>CHANNEL_DELETE
@@ -744,7 +744,7 @@ Message Create alone overrides both the passive filter and the `ignored_events`
A visible message changed. The payload is the complete current [message object](/http-api/messages/#message-object). In a guild channel it is extended with `guild_id` and with `member`, the author's guild member object with its `user` field removed. It has no `channel_type`, `nicks`, or `mention_here`.
A recipient must hold `READ_MESSAGE_HISTORY` on the channel, or the message must be newer than the guild's message history cutoff.
Recipients must hold `READ_MESSAGE_HISTORY` on the channel, or the message must be newer than the guild's message history cutoff.
### <span id="message-delete"></span>MESSAGE_DELETE
@@ -825,7 +825,7 @@ A session that set the `DEBOUNCE_MESSAGE_REACTIONS` [session flag](/gateway/comm
<sup>1</sup> Taken from the first addition of the group. The window is per session, and the session groups the additions by guild, channel, and message when the window closes. Each group is one Dispatch, so every addition in `reactions` is on the message these fields name
A window that closes holding exactly one addition sends [Message Reaction Add](#message-reaction-add) instead, and a session without the flag receives one Message Reaction Add per addition.
When the window closes holding exactly one addition, the session sends [Message Reaction Add](#message-reaction-add) instead. A session without the flag receives one Message Reaction Add per addition.
#### Reaction addition object
@@ -1131,8 +1131,8 @@ A channel the session cannot view, and a channel on which it lacks `VIEW_CHANNEL
## Resource representation
Every resource object named on this page has the representation defined by the [HTTP API](/http-api/). A Dispatch payload with a resource object has the same fields, with the guild-scoped events adding `guild_id` and the message and reaction events adding `member` as documented above.
Every resource object named on this page has the representation defined by the [HTTP API](/http-api/). A Dispatch payload with a resource object has the same fields, with the guild-scoped events adding `guild_id` and the message and reaction events adding `member`.
Two reductions are specific to the Gateway and appear nowhere in the HTTP API. [Ready](#ready) strips `user` from each relationship and from each guild member and hoists those accounts into its `users` array. The `member` added to a message event has its own `user` removed, and the account is in the message's `author`. A client MUST resolve those accounts from the surrounding payload.
Every Dispatch payload also drops six fields the Gateway keeps for its own indexing: `recipient_ids`, `role_index`, `channel_index`, `member_role_index`, `role_perms_cache`, and `overwrite_perms_cache`.
Every Dispatch payload also drops the fields the Gateway keeps for its own indexing: `recipient_ids`, `role_index`, `channel_index`, `member_role_index`, `role_perms_cache`, and `overwrite_perms_cache`.
@@ -38,7 +38,19 @@ The Gateway refuses a session start for draining, capacity, paused starts, the r
## Connection and command rate limits
A Gateway node running with `FLUXER_DISABLE_RATE_LIMITS` set to `1`, `true`, or `TRUE` disables nine budgets together. Six are the connection payload budget, the session payload budget, the source IP payload budget, the source IP connection ceiling, the Presence Update budget, and the Voice State Update queue. The other three are the source IP Identify budget, the per-user session count, and the 30-second complete member list budget. The figures below are the enforced defaults.
A Gateway node running with `FLUXER_DISABLE_RATE_LIMITS` set to `1`, `true`, or `TRUE` disables nine budgets together:
- The connection payload budget
- The session payload budget
- The source IP payload budget
- The source IP connection ceiling
- The Presence Update budget
- The Voice State Update queue
- The source IP Identify budget
- The per-user session count
- The 30-second complete member list budget
The figures below are the enforced defaults.
One WebSocket accepts 600 client payloads in a rolling 60-second window. One authenticated session accepts 600 client payloads in each fixed 60-second bucket. One source IP address accepts 6,000 client payloads in each fixed 60-second bucket. Exceeding any of these budgets closes the current connection with `4008` and reason `Rate limited`.
@@ -118,7 +118,7 @@ Code 4006 is unassigned, and no code above 4012 is defined. [Event filtering](/g
| 4011 | No | Closed. No session was created |
| 4012 | No | Closed before Hello. No session exists |
<sup>1</sup> A session the close leaves without a socket is retained for 60,000 ms measured from the moment the socket ends, so a close begins a fresh window. A session displaced by a Resume from a new socket is already attached to that socket and enters no window
<sup>1</sup> A close that leaves a session without a socket begins a fresh 60,000 ms retention window, measured from the moment the socket ends. A session displaced by a Resume from a new socket is already attached to that socket and enters no window
<sup>2</sup> A Resume that fails token verification leaves the named session in place for the rest of its retention window, so a later Resume with the owning token still recovers it. An Identify that fails token verification leaves nothing to recover
@@ -10,9 +10,9 @@ An application is an OAuth2 client owned by one user account, and every bot acco
## Access rules
Every route except [Get public application](#get-public-application) requires a credential. A user-only route rejects a bot token and an OAuth2 bearer with 403 `ACCESS_DENIED`. An account with an outstanding required action is accepted everywhere here.
Every route except [Get public application](#get-public-application) requires a credential. A user-only route rejects a bot token and an OAuth2 bearer with 403 `ACCESS_DENIED`. Fluxer accepts an account with an outstanding required action everywhere here.
The three sudo-gated routes read `X-Fluxer-Sudo-Mode-JWT`. A valid proof for the authenticated account satisfies [sudo mode](/http-api/users/mfa/#sudo-mode) on its own, and Fluxer echoes it back in the response header. An account with no authenticator proves sudo mode with `password` in the body, because Fluxer issues it no token.
The three sudo-gated routes read `X-Fluxer-Sudo-Mode-JWT`. A valid proof for the authenticated account satisfies [sudo mode](/http-api/users/mfa/#sudo-mode) on its own, and Fluxer echoes it back in the response header. Fluxer issues no token to an account with no authenticator, so that account proves sudo mode with `password` in the body.
:::caution[Credentials are shown once]
A client secret is returned only by [Create application](#create-application) and [Reset client secret](#reset-client-secret), and a bot token only by [Create application](#create-application) and [Reset bot token](#reset-bot-token). Both are stored as one-way hashes.
@@ -173,11 +173,11 @@ The bot account's profile fields after an update. [Update bot profile](#update-b
| 1 &lt;&lt; 4 | FRIENDLY_BOT | The bot accepts friend requests from users |
| 1 &lt;&lt; 5 | FRIENDLY_BOT_MANUAL_APPROVAL | The bot requires manual approval for friend requests |
These two bits are a subset of the account's [public user flags](/http-api/users/#public-user-flags), so a bot account that also has `STAFF`, `PARTNER`, `BUG_HUNTER`, or `SPAMMER` reports that bit in the same field. A client MUST ignore a bit it does not recognise.
These bits are a subset of the account's [public user flags](/http-api/users/#public-user-flags), so a bot account that also has `STAFF`, `PARTNER`, `BUG_HUNTER`, or `SPAMMER` reports that bit in the same field. A client MUST ignore a bit it does not recognise.
## Bot token reset object
The response to [Reset bot token](#reset-bot-token). The fields are the new token and the bot account it belongs to.
The response to [Reset bot token](#reset-bot-token).
### Structure
@@ -197,11 +197,15 @@ The parsed device metadata recorded for a session or a pending handoff.
<sup>1</sup> A native or Electron client reports null, and a session created by an unparseable request reports null. A handoff omits the member entirely
Fluxer recognises a native Fluxer client from a `User-Agent` beginning `Fluxer Android`, `Fluxer iOS`, `Fluxer Linux`, `Fluxer Desktop`, or `Fluxer Client`. Its `platform` is the instance product name followed by the resolved operating system, with `Lite` inserted before the operating system for a non-mobile one. A native client that resolves no operating system reports the product name followed by `Lite`, so `platform` is never null for a native client. An Electron client reports the product name followed by the resolved operating system, and the product name alone when no operating system resolves. Fluxer parses every other client from the recorded `User-Agent`, and that client's platform is the browser name, or the operating system name when no browser resolves, and null when neither resolves.
Fluxer recognises a native Fluxer client from a `User-Agent` beginning `Fluxer Android`, `Fluxer iOS`, `Fluxer Linux`, `Fluxer Desktop`, or `Fluxer Client`.
- A native client's `platform` is the instance product name followed by the resolved operating system, with `Lite` inserted before the operating system for a non-mobile one. A native client that resolves no operating system reports the product name followed by `Lite`, so `platform` is never null for a native client.
- An Electron client's `platform` is the product name followed by the resolved operating system, and the product name alone when no operating system resolves.
- Every other client is parsed from the recorded `User-Agent`, and its `platform` is the browser name, or the operating system name when no browser resolves, and null when neither resolves.
Fluxer resolves `device` from the recorded `User-Agent` and, for a native client, from the operating system reported through `X-Fluxer-Client-Properties`. A native or Electron client is `mobile` only when the resolved operating system is iOS or Android. Every other client is `mobile` when the parsed platform type is mobile or tablet, and `desktop` otherwise.
A session reports a null `location` when the recorded IP address resolves to no location. A handoff always reports the object, and each of its three members is null when that component is unavailable. For a session, an address with a known region and country but no known city reports the region as `city`, the country as `region`, and null as `country`.
A session reports a null `location` when the recorded IP address resolves to no location. A handoff always reports the object, and each of its members is null when that component is unavailable. For a session, an address with a known region and country but no known city reports the region as `city`, the country as `region`, and null as `country`.
## Client location object
@@ -240,7 +244,7 @@ The state of one IP authorisation ticket, as observed by the device whose sign-i
| user_id?<sup>1</sup> | ?snowflake | The authenticated user ID |
| user?<sup>1</sup> | ?[partial user](/http-api/users/#partial-user-object) object | The public representation of the authenticated account |
<sup>1</sup> The three fields are present together only when `completed` is true, and a still-pending ticket returns `completed` alone
<sup>1</sup> The fields are present together only when `completed` is true, and a still-pending ticket returns `completed` alone
## Username suggestions object
@@ -300,7 +304,7 @@ The state of one desktop handoff as observed by the device that initiated it.
<sup>1</sup> An unknown code and an expired code both report `expired`
<sup>2</sup> The three fields are present together only when the status is `completed`
<sup>2</sup> The fields are present together only when the status is `completed`
## Get SSO status
@@ -401,7 +405,7 @@ A verified provider email that adopts a bot account returns 403 `BOT_USER_AUTH_S
### Side effects
Fluxer consumes the SSO state exactly once, before it attempts the provider exchange. A failed exchange still burns the state. A client MUST then start a fresh flow. On first sign-in, the provider identity becomes exclusive to the new account. The account starts with a verified email, no password, no authenticator, and its default settings. Provisioning joins no guild and accepts no invite, so no [Guild Member Add](/gateway/events/#guild-member-add) is emitted.
Fluxer consumes the SSO state exactly once, before it attempts the provider exchange. A failed exchange still spends the state. A client MUST then start a fresh flow. On first sign-in, the provider identity becomes exclusive to the new account. The account starts with a verified email, no password, no authenticator, and its default settings. Provisioning joins no guild and accepts no invite, so no [Guild Member Add](/gateway/events/#guild-member-add) is emitted.
Approval mode registration instead returns 403 `REGISTRATION_PENDING_APPROVAL` and creates no session. Otherwise the operation creates one authentication session and clears an expired temporary suspension.
@@ -415,7 +419,7 @@ Approval mode registration instead returns 403 `REGISTRATION_PENDING_APPROVAL` a
Creates an ordinary account. Returns an [authentication token response](#authentication-token-response) when the instance admits the account immediately and a [registration pending approval response](#registration-pending-approval-response-object) when it does not. Emits a [Guild Member Add](/gateway/events/#guild-member-add) Gateway event when an invite or instance community admission takes effect.
This is a local authentication operation and it verifies [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. Registration additionally permits 3 attempts per hour for each client IP address and 15 per hour for each client subnet, which is the IPv4 /24 or IPv6 /48 network. A supplied email address permits 3 attempts per 15 minutes of its own. Those three allowances are separate from the route bucket, and only a deployment that relaxes registration rate limits disables them.
This is a local authentication operation and it verifies [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. Registration permits 3 attempts per hour for each client IP address and 15 per hour for each client subnet, which is the IPv4 /24 or IPv6 /48 network. A supplied email address permits 3 attempts per 15 minutes of its own. Those allowances are separate from the route bucket, and only a deployment that relaxes registration rate limits disables them.
### Request headers
@@ -570,7 +574,6 @@ Consumes an MFA ticket and validates a time-based one-time password or an uncons
This is a local authentication operation. MFA verification additionally permits 10 failed attempts per account in 15 minutes and destroys the ticket after 5 failed attempts.
### JSON body
| Field | Type | Description |
@@ -580,7 +583,7 @@ This is a local authentication operation. MFA verification additionally permits
<sup>1</sup> A validated authenticator code is claimed for 30 seconds, so the same code cannot be presented twice. A backup code is consumed permanently on the attempt that accepts it
An expired or unknown ticket returns the field code `SESSION_TIMEOUT` on `ticket`. An account with no TOTP enrolment returns the field code `TOTP_NOT_ENABLED` on `code`. An incorrect code, and any code presented after the per-account or per-ticket attempt allowance is exhausted, returns the field code `INVALID_CODE` on `code`. A ticket that resolves to a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`.
An expired or unknown ticket returns the field code `SESSION_TIMEOUT` on `ticket`. An account with no TOTP enrolment returns the field code `TOTP_NOT_ENABLED` on `code`. Both an incorrect code and any code presented after the per-account or per-ticket attempt allowance is exhausted return the field code `INVALID_CODE` on `code`. A ticket that resolves to a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`.
### Response
@@ -659,7 +662,7 @@ This is a local authentication operation.
An expired or unknown ticket returns the field code `SESSION_TIMEOUT` on `ticket`. A challenge mismatch, an unknown credential, a stored public key that cannot be decoded, a signature counter that the authenticator did not advance, and a failed signature verification all return 401 `PASSKEY_AUTHENTICATION_FAILED`. A verified assertion whose reported signature counter cannot be read returns 500 `INVALID_WEBAUTHN_AUTHENTICATION_COUNTER`.
This operation shares the MFA attempt allowances of [complete login with TOTP](#complete-login-with-totp). It permits 10 failed attempts per account in 15 minutes and destroys the ticket after 5 failed attempts, and an exhausted allowance returns the field code `INVALID_CODE` on `ticket`.
This operation shares the MFA attempt allowances of [complete login with TOTP](#complete-login-with-totp). It permits 10 failed attempts per account in 15 minutes and destroys the ticket after 5 failed attempts. An exhausted allowance returns the field code `INVALID_CODE` on `ticket`.
### Response
@@ -848,7 +851,7 @@ The operation creates one 64-character verification token bound to the account a
### Rate limit
10 requests per minute, on the `auth:verify:resend` bucket, and the target address additionally permits 3 messages in 15 minutes.
10 requests per minute, on the `auth:verify:resend` bucket, and the target address permits 3 messages in 15 minutes.
## Request password recovery
@@ -898,7 +901,7 @@ Fluxer sends one 64-character reset token by email to an address that resolves t
### Rate limit
5 requests per minute, on the `auth:forgot` bucket, and recovery additionally permits 20 attempts per client IP address and 5 attempts per email address in each 30-minute window.
5 requests per minute, on the `auth:forgot` bucket, and recovery permits 20 attempts per client IP address and 5 attempts per email address in each 30-minute window.
## Validate a password reset token
@@ -987,7 +990,7 @@ A successful reset terminates every authentication session on the account, inclu
The operation replaces the password, records the change time, clears an expired temporary suspension, terminates every authentication session on the account, and consumes the presented reset token. Every terminated session's Gateway connection receives Invalid Session with `d: false` and stays open, unauthenticated. Other outstanding reset tokens are not invalidated, so a second recovery link issued earlier still works.
An account with no second factor then receives one new session and its token. An account with a second factor receives a five-minute MFA ticket instead, and the session is created by the MFA completion.
An account with no second factor then receives one new session and its token. An account with a second factor receives a five-minute MFA ticket instead, and the MFA completion creates the session.
### Rate limit
@@ -1016,7 +1019,7 @@ Completing a reversion terminates every authentication session, clears the accou
An unknown or already consumed token, and a token bound to an account that no longer exists, return the field code `INVALID_OR_EXPIRED_REVERT_TOKEN`. Account suspension returns 403 `ACCOUNT_SUSPENDED_TEMPORARILY` or 403 `ACCOUNT_SUSPENDED_PERMANENTLY`, and a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`.
The replacement password is checked against the same breached-password corpus described under [reset a password](#reset-a-password) before the token is spent.
Fluxer checks the replacement password against the same breached-password corpus described under [reset a password](#reset-a-password) before it spends the token.
### Response
@@ -1204,7 +1207,7 @@ Fluxer sends the authorisation message to the address associated with the login
### Rate limit
5 requests per minute, on the `auth:ip_authorization_resend` bucket, and each ticket additionally permits exactly one resend, no earlier than 30 seconds after the ticket was issued.
5 requests per minute, on the `auth:ip_authorization_resend` bucket, and each ticket permits exactly one resend, no earlier than 30 seconds after the ticket was issued.
## Poll IP authorisation
@@ -1329,7 +1332,7 @@ Reading the handoff information marks the code as inspected but does not approve
Anyone who can reach the API and knows the code can mark it inspected. Declining the request in a client discards only what that client is showing, and the inspected state remains until the handoff is completed or the code expires.
A code that is not exactly 12 characters from the handoff alphabet after hyphens and whitespace are removed returns 400 `INVALID_HANDOFF_CODE`. A code whose three lookups are already spent, and a request from a client IP address that has exhausted its failed-attempt allowance, return the same error code. An unknown or expired code returns 200 with the `expired` status and records one failed attempt.
A code that is not exactly 12 characters from the handoff alphabet after hyphens and whitespace are removed returns 400 `INVALID_HANDOFF_CODE`. Both a code whose three lookups are already spent and a request from a client IP address that has exhausted its failed-attempt allowance return the same error code. An unknown or expired code returns 200 with the `expired` status and records one failed attempt.
### Response
@@ -30,7 +30,7 @@ A private call has no moderator and no permission overwrites. No account other t
Silencing another participant happens in the client. A client MAY mute a participant or change per-participant volume. The client MUST keep each setting local to the listening device, so neither reaches the Gateway or any other participant.
Two operations on this page name another recipient. [Ring call recipients](#ring-call-recipients) adds named recipients to the ringing set of a call and [Stop ringing call recipients](#stop-ringing-call-recipients) removes them from it. Any current recipient MAY call either one. Neither changes anything for a recipient who has already connected, and neither writes a voice state.
Two operations on this page name another recipient. [Ring call recipients](#ring-call-recipients) adds named recipients to the ringing set of a call, and [Stop ringing call recipients](#stop-ringing-call-recipients) removes them from it. Any current recipient MAY call either one. Neither changes anything for a recipient who has already connected, and neither writes a voice state.
## Call eligibility object
@@ -72,7 +72,7 @@ Returns the [call eligibility object](#call-eligibility-object) for a direct mes
A direct message reports `ringable` as false when the other recipient's incoming call policy excludes the caller. That policy is the `incoming_call_flags` bitfield of the recipient's [user settings](/http-api/users/settings/). Fluxer reads the nobody flag, the friends-only flag, an existing friendship, a mutual friend, a mutual guild, and finally the everyone flag, in that order. The silent-everyone flag reports `silent` as true for a caller admitted by the mutual friend, mutual guild, or everyone branch.
A recipient who has never stored settings reports `ringable` as true and `silent` as false, and a direct message that has lost its other recipient reports the same pair. A group direct message reports `ringable` as true unless the caller is already connected to its call, and it always reports `silent` as false.
For a recipient who has never stored settings, `ringable` is true and `silent` is false. A direct message that has lost its other recipient reports the same pair. In a group direct message `ringable` is true unless the caller is already connected to its call, and `silent` is always false.
Fluxer evaluates the same policy again on a later [Ring call recipients](#ring-call-recipients) request.
@@ -120,11 +120,18 @@ The caller does not have to be connected to the call.
<sup>2</sup> Validated for length and then discarded, so it never affects the selected region
Any other unknown or inaccessible `region` returns 400 `INVALID_FORM_BODY` with the validation code `INVALID_OR_RESTRICTED_RTC_REGION` on the `region` field. The body can be omitted. Fluxer treats a missing, empty, or whitespace-only body as an empty object, which requests no change. A body that is not valid JSON returns 400 `INVALID_FORM_BODY` with the validation code `INVALID_FORMAT` at the `body` path.
Any other unknown or inaccessible `region` returns 400 `INVALID_FORM_BODY` with the validation code `INVALID_OR_RESTRICTED_RTC_REGION` on the `region` field.
The body can be omitted. Fluxer treats a missing, empty, or whitespace-only body as an empty object, which requests no change. A body that is not valid JSON returns 400 `INVALID_FORM_BODY` with the validation code `INVALID_FORMAT` at the `body` path.
A region identifier is the `id` of an [RTC region object](/http-api/channels/#rtc-region-object). No route enumerates the regions a call accepts, and [List RTC regions](/http-api/channels/#list-rtc-regions) answers only for a guild voice channel.
A region or voice server restricted to named guilds, to a guild feature, or to VIP voice is never selectable for a call. A region or voice server whose user allowlist excludes the caller is unselectable, and so is a region whose servers are all inactive or all closed to the caller. An instance with voice disabled accepts any region string.
A region or voice server is unselectable for a call on any of these grounds:
- It is restricted to named guilds, to a guild feature, or to VIP voice.
- Its user allowlist excludes the caller.
A region whose servers are all inactive or all closed to the caller is unselectable as well. An instance with voice disabled accepts any region string.
### Response
@@ -176,7 +183,7 @@ The body can be omitted. Fluxer treats a missing, empty, or whitespace-only body
An identifier that names a non-recipient or the caller itself returns 400 `INVALID_FORM_BODY` with the validation code `USER_NOT_IN_CHANNEL`. A repeated identifier is accepted and rings that recipient once.
A named recipient is only rung when the incoming call policy described by [Get call eligibility](#get-call-eligibility) admits the caller audibly, so a recipient admitted only under the silent-everyone flag is notified without being rung. A direct message ignores the named set when choosing whom to ring and always evaluates the policy against the other recipient.
A named recipient is only rung when the incoming call policy described by [Get call eligibility](#get-call-eligibility) admits the caller audibly, so a recipient admitted only under the silent-everyone flag is notified without being rung. In a direct message Fluxer ignores the named set when choosing whom to ring and always evaluates the policy against the other recipient.
### Response
@@ -198,7 +205,9 @@ When no call exists, Fluxer then stores a call system message with the initial p
When a call already exists, Fluxer instead extends the ringing set and emits [Call Update](/gateway/events/#call-update) when the set grows. A recipient who is already connected to the call is never added to the ringing set.
Each rung recipient holds a ringing entry for 30 seconds. Fluxer then drops the entry and emits [Call Update](/gateway/events/#call-update) again. When that expiry leaves the call with no connected participant and no other ringing recipient, Fluxer removes the call in the same step, which emits [Call Delete](/gateway/events/#call-delete) and stamps the call system message with its ended timestamp. A call that rang at least one recipient and that nobody joined therefore ends 30 seconds after the ring. A call created with an empty ringing set arms no ring timer, so it ends on the 120 second idle timer instead. An explicit empty `recipients` array produces such a call, and so does a ring that admits no candidate audibly.
Each rung recipient holds a ringing entry for 30 seconds. Fluxer then drops the entry and emits [Call Update](/gateway/events/#call-update) again. When that expiry leaves the call with no connected participant and no other ringing recipient, Fluxer removes the call in the same step, which emits [Call Delete](/gateway/events/#call-delete) and stamps the call system message with its ended timestamp. A call that rang at least one recipient and that nobody joined therefore ends 30 seconds after the ring.
Fluxer arms no ring timer for a call created with an empty ringing set, so that call ends on the 120 second idle timer instead. An explicit empty `recipients` array produces such a call, and so does a ring that admits no candidate audibly.
Whenever Fluxer removes a call, it rewrites that call's system message with the ended timestamp and with every account that ever connected, and publishes the rewritten message to every recipient of the private channel as [Message Update](/gateway/events/#message-update).
@@ -51,7 +51,7 @@ A channel object always has its identity and type. Every other field is present
<sup>4</sup> The value `0` means no occupancy limit
<sup>5</sup> A guild voice channel that stores no per-user connection limit reports the default `5`
<sup>5</sup> Defaults to `5` on a guild voice channel that stores no per-user connection limit
<sup>6</sup> Null selects automatic routing. [List RTC regions](#list-rtc-regions) returns the available identifiers
@@ -61,9 +61,9 @@ A channel object always has its identity and type. Every other field is present
<sup>9</sup> A channel with no explicit override reports `nsfw` false and `nsfw_override` null
<sup>10</sup> A channel that inherits reports `content_warning_level` `0` and `content_warning_text` null
<sup>10</sup> On a channel that inherits, `content_warning_level` is `0` and `content_warning_text` is null
<sup>11</sup> A channel that configures no slowmode reports `0`
<sup>11</sup> The interval is `0` when the channel configures no slowmode
<sup>12</sup> Omitted when the group stores no nickname. Clearing a nickname removes its key from the map
@@ -408,13 +408,11 @@ Every field is optional, and an omitted field preserves its current value. The f
A `url` that is not an absolute `http` or `https` URL with a host returns 400 `INVALID_FORM_BODY` with the code `INVALID_URL_FORMAT` on the path `url`.
Three failures apply to `parent_id`. A parent that does not exist in the same guild returns 400 `INVALID_FORM_BODY` with the code `INVALID_PARENT_CHANNEL` on the path `parent_id`. A parent that is not a category returns `PARENT_MUST_BE_CATEGORY` the same way.
A parent that does not exist in the same guild returns 400 `INVALID_FORM_BODY` with the code `INVALID_PARENT_CHANNEL` on the path `parent_id`. A parent that is not a category returns `PARENT_MUST_BE_CATEGORY` the same way.
Naming a category other than the current parent also checks capacity. A full category returns 400 `MAX_CATEGORY_CHANNELS` with the ceiling it reached, which is the deployment's [`max_channels_per_category`](/http-api/instance/#limit-keys) limit and defaults to 50.
Fluxer accepts an overwrite mask in two encodings. A decimal string holds at most 9223372036854775807, and a larger one returns 400 `INVALID_FORM_BODY` with the code `INTEGER_OUT_OF_INT64_RANGE`. A JSON integer holds at most 9007199254740991, and a number outside the safe integer range returns `INVALID_INTEGER_FORMAT`.
Every bit outside the defined permission set is discarded. A [feature-gated bit](/http-api/permissions/#feature-gated-permission-bits) the `X-Fluxer-Features` declaration does not name is copied from the stored overwrite.
Fluxer accepts an overwrite mask as a decimal string or as a JSON integer. The decimal string holds at most 9223372036854775807, and a larger one returns 400 `INVALID_FORM_BODY` with the code `INTEGER_OUT_OF_INT64_RANGE`. The JSON integer holds at most 9007199254740991, and a number outside the safe integer range returns `INVALID_INTEGER_FORMAT`. Every bit outside the defined permission set is discarded.
A non-null `rtc_region` names a region [List RTC regions](#list-rtc-regions) returns for this guild. Any other value returns 400 `INVALID_FORM_BODY` with the code `INVALID_OR_RESTRICTED_RTC_REGION` on the path `rtc_region`. A deployment that configures no voice topology validates no region and stores any accepted string.
@@ -6,7 +6,7 @@ description: The hosted-only routes and the instance flags that report deploymen
A small set of routes exists only on the deployment Fluxer hosts. An operator runs the same release, and most of the HTTP API is identical on both.
A self-hosted deployment does not register those routes. A request to one returns 404 `NOT_FOUND` with no feature-specific code. A caller cannot tell an unavailable route from an unrecognised path.
A self-hosted deployment does not register those routes. A request to one returns 404 `NOT_FOUND` with no feature-specific code, so a caller cannot tell an unavailable route from an unrecognised path.
The API decides registration once at process start from the deployment configuration. No credential, permission, premium state, or OAuth2 scope changes the answer. A client resolves the deployment kind from instance discovery.
@@ -20,7 +20,11 @@ Every deployment reports its kind in `self_hosted` on the [instance features obj
Neither flag promises that a provider-dependent operation succeeds.
:::
Without a provider client, the answer depends on the operation. An operation that has to reach the provider fails with 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) fails with 400 `STRIPE_WEBHOOK_NOT_AVAILABLE`. Three read operations report the absence in a 200 body instead. [Get refund eligibility](/http-api/billing/#get-refund-eligibility) reports `eligible` false with the reason `feature_unavailable`, [Get current subscription price](/http-api/premium/#get-current-subscription-price) reports null, and [Get price IDs](/http-api/premium/#get-price-ids) reports the configured price IDs with every amount null.
Without a provider client, the answer depends on the operation. An operation that has to reach the provider fails with 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) fails with 400 `STRIPE_WEBHOOK_NOT_AVAILABLE`. These read operations report the absence in a 200 body instead:
- [Get refund eligibility](/http-api/billing/#get-refund-eligibility) reports `eligible` false with the reason `feature_unavailable`.
- [Get current subscription price](/http-api/premium/#get-current-subscription-price) reports null.
- [Get price IDs](/http-api/premium/#get-price-ids) reports the configured price IDs with every amount null.
## Hosted-only routes
@@ -12,7 +12,7 @@ Discovery is the public directory of guilds any account can browse and join. A g
On a guild an operator has marked unavailable, Fluxer refuses [Apply for discovery](#apply-for-discovery), [Edit discovery application](#edit-discovery-application), [Withdraw discovery application](#withdraw-discovery-application), and [Get discovery status](#get-discovery-status) with 403 `MISSING_ACCESS` before the route runs. The gate does not cover [Join discovery guild](#join-discovery-guild).
The four routes under `/v1/guilds/{guild_id}/discovery` require [MANAGE_GUILD](/http-api/permissions/), which is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller other than the owner also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. A guild that does not exist returns 404 `UNKNOWN_GUILD`. Both a non-member and a member without the permission return 403 `MISSING_PERMISSIONS`. The routes answer 503 `SERVICE_UNAVAILABLE` when the check cannot be admitted, and 504 `GATEWAY_TIMEOUT` when it does not answer in time.
The routes under `/v1/guilds/{guild_id}/discovery` require [MANAGE_GUILD](/http-api/permissions/), which is an [elevated permission](/http-api/permissions/#elevated-permissions). While the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller other than the owner also needs an enrolled authenticator, and receives 400 `TWO_FACTOR_REQUIRED` without one. A guild that does not exist returns 404 `UNKNOWN_GUILD`. Both a non-member and a member without the permission return 403 `MISSING_PERMISSIONS`. The routes answer 503 `SERVICE_UNAVAILABLE` when the check cannot be admitted, and 504 `GATEWAY_TIMEOUT` when it does not answer in time.
:::note[Discovery is an optional instance capability]
Searching, applying, editing, withdrawing, and joining fail with 400 `DISCOVERY_DISABLED` while an operator has it disabled. [List discovery categories](#list-discovery-categories) and [Get discovery status](#get-discovery-status) keep answering.
@@ -20,7 +20,7 @@ Searching, applying, editing, withdrawing, and joining fail with 400 `DISCOVERY_
## Discovery guild object
One search result. Every field except the two counts is read from the discovery search index.
One search result. Every field except `member_count` and `online_count` is read from the discovery search index.
### Structure
@@ -39,7 +39,7 @@ One search result. Every field except the two counts is read from the discovery
| features | array[string] | The [guild features](/http-api/guilds/#guild-features) the guild has |
| verification_level<sup>4</sup> | integer | The effective [verification level](/http-api/guilds/#verification-levels) of the guild |
<sup>1</sup> The four listing fields come from the guild's approved application as it stood at index time, and an entry that stores no category is reported as category `0`
<sup>1</sup> These listing fields come from the guild's approved application as it stood at index time, and an entry that stores no category is reported as category `0`
<sup>2</sup> Refreshed from the main Gateway at response time. When that refresh fails, the operation still succeeds and reports the indexed member count with an `online_count` of `0`
@@ -199,9 +199,7 @@ The release fixes the set, so an operator cannot add, rename, or remove a catego
| 7 | MOVIES_AND_TV | Movies & TV |
| 8 | OTHER | Other |
Every category value on the wire is one of these integers.
A value outside 0 through 8 fails body or query validation with the validation code `INVALID_FORMAT`, so [Apply for discovery](#apply-for-discovery) and [Edit discovery application](#edit-discovery-application) reject it before the listing is stored.
Every category value on the wire is one of these integers. A value outside 0 through 8 fails body or query validation with the validation code `INVALID_FORMAT`, so [Apply for discovery](#apply-for-discovery) and [Edit discovery application](#edit-discovery-application) reject it before the listing is stored.
The display names above are the strings [List discovery categories](#list-discovery-categories) returns. They are not localised, so the same ID has the same name for every account.
@@ -72,7 +72,7 @@ Accepts a request for a single-use donation management link. Returns 204 with an
A failure of any of those returns 400 `INVALID_FORM_BODY` with an `errors` entry on `email`.
:::caution[Every address answers the same 204]
An address that resolves to no donor receives no email, and the response looks identical either way.
An address that resolves to no donor receives no email.
:::
### JSON body
@@ -110,7 +110,7 @@ Fluxer creates a single-use token of 64 lowercase hexadecimal characters, valid
Consumes a donation management token. Answers 302 with `Location` set to the externally hosted billing portal for the matching donor. The token is the credential.
Fluxer checks the token in a fixed order. An unknown token returns 400 `DONATION_MAGIC_LINK_INVALID`. A token past its 15 minute lifetime returns 400 `DONATION_MAGIC_LINK_EXPIRED`. An already consumed token returns 400 `DONATION_MAGIC_LINK_USED`, and a token that is both expired and consumed is reported as expired. None of those three consumes the token.
Fluxer checks the token in a fixed order. An unknown token returns 400 `DONATION_MAGIC_LINK_INVALID`. A token past its 15 minute lifetime returns 400 `DONATION_MAGIC_LINK_EXPIRED`. An already consumed token returns 400 `DONATION_MAGIC_LINK_USED`, and a token that is both expired and consumed is reported as expired. None of those refusals consumes the token.
A token whose address no longer resolves to a donor holding a payment provider customer is still consumed, and the redirect goes to the public donation page. For a donor holding a customer, a deployment with no configured payment provider returns 400 `STRIPE_PAYMENT_NOT_AVAILABLE` and a provider failure returns 400 `STRIPE_ERROR`, both after the token has already been consumed.
@@ -133,7 +133,7 @@ A token whose address no longer resolves to a donor holding a payment provider c
### Side effects
The token is consumed. A resolved donor holding a payment provider customer receives a portal session, and closing that session returns the donor to the public donation page. An address that resolves to no donor, or to a donor with no customer, redirects to the public donation page and creates no portal session.
The token is consumed. A resolved donor holding a payment provider customer receives a portal session. Closing that session returns the donor to the public donation page. Where the address resolves to no donor, or to a donor with no customer, Fluxer redirects to the public donation page and creates no portal session.
### Rate limit
@@ -145,9 +145,9 @@ The token is consumed. A resolved donor holding a payment provider customer rece
Creates a one-off or recurring donation checkout session. Returns a [donation checkout](#donation-checkout-object) object on success.
An amount outside the selected currency's bounds fails validation with 400 `INVALID_FORM_BODY` and an `errors` entry on `amount_cents`. A deployment with no configured payment provider returns 400 `STRIPE_PAYMENT_NOT_AVAILABLE` before the address is resolved, and a provider failure while creating the session returns 400 `STRIPE_ERROR`.
An amount outside the selected currency's bounds fails validation with 400 `INVALID_FORM_BODY` and an `errors` entry on `amount_cents`. Where the deployment has no configured payment provider, the route returns 400 `STRIPE_PAYMENT_NOT_AVAILABLE` before it resolves the address. A provider failure while creating the session returns 400 `STRIPE_ERROR`.
A recurring donation for an address that already holds an active recurring donation returns the public donation management page for that address, with the percent-encoded address in `email` and the literal value `active_subscription` in `alert`. An address holds an active recurring donation when its donor record has a subscription identifier and a current period end still in the future, and has no scheduled cancellation.
A recurring donation for an address that already holds an active recurring donation returns the public donation management page for that address, with the percent-encoded address in `email` and the literal value `active_subscription` in `alert`. An address holds an active recurring donation when its donor record has a subscription identifier, a current period end still in the future, and no scheduled cancellation.
### JSON body
@@ -22,13 +22,13 @@ Every other route on this reference takes the `/v1` form, and [HTTP API](/http-a
## Methods
Every route answers `GET`. Five registrations also bind `HEAD`: the two checksum routes, the two artifact routes, and the catch-all. [Get latest desktop version](#get-latest-desktop-version) and [List desktop versions](#list-desktop-versions) bind `GET` alone, and a `HEAD` still reaches them under the [shared rule for HEAD](/http-api/#request-format).
Every route answers `GET`. The two checksum routes, the two artifact routes, and the catch-all also bind `HEAD`. [Get latest desktop version](#get-latest-desktop-version) and [List desktop versions](#list-desktop-versions) bind `GET` alone, and a `HEAD` still reaches them under the [shared rule for HEAD](/http-api/#request-format).
A `HEAD` on either JSON route runs the same resolution as the `GET` and returns its status and headers with no body. It can therefore answer 404.
On a checksum route, a `HEAD` returns 200 with no body. The headers are `Content-Type`, `Content-Disposition`, `Cache-Control`, and the `Content-Length` of the checksum line.
A checksum route answers a `HEAD` with 200 and no body. The headers are `Content-Type`, `Content-Disposition`, `Cache-Control`, and the `Content-Length` of the checksum line.
A `HEAD` on an artifact route or the catch-all reads object metadata. It returns 200 with `Content-Type`, `Content-Disposition`, `Accept-Ranges`, `Cache-Control`, `Content-Length`, and, where storage reports them, `ETag` and `Last-Modified`. It ignores `Range` and answers neither 206 nor 416. It never redirects to a [presigned storage URL](#redirects). Fluxer resolves a [country redirect](#redirects) before it reads the method, so a `HEAD` receives that 302 exactly as a `GET` does.
The artifact routes and the catch-all read object metadata for a `HEAD`. They return 200 with `Content-Type`, `Content-Disposition`, `Accept-Ranges`, `Cache-Control`, `Content-Length`, and, where storage reports them, `ETag` and `Last-Modified`. They ignore `Range`, answer neither 206 nor 416, and never redirect to a [presigned storage URL](#redirects). Fluxer resolves a [country redirect](#redirects) before it reads the method, so a `HEAD` receives that 302 exactly as a `GET` does.
## Release channels
@@ -155,7 +155,7 @@ Fluxer reads the hash from the sibling `.sha256` object or from the manifest ent
Fluxer resolves a coordinate to one storage key before it answers. It reads `manifest.json` under the coordinate prefix first and takes the filename that manifest records for the requested format. Fluxer falls back to a listing when that manifest is absent, is not valid JSON, is not a manifest object, describes another coordinate, or names a file storage does not hold. The fallback lists the objects under the prefix and takes the highest version whose filename parses for the requested format.
The listing skips any name containing `/`, the names `manifest.json`, `RELEASES.json`, and `releases.json`, and any name ending in `.sha256`, `.blockmap`, or `.yml`.
The listing skips any name containing `/` and any name ending in `.sha256`, `.blockmap`, or `.yml`. It also skips `manifest.json`, `RELEASES.json`, and `releases.json`.
A coordinate that resolves no file returns 404 with the plain text body `Not Found`.
@@ -172,7 +172,12 @@ Every artifact response has `Accept-Ranges: bytes` and a `Content-Disposition` o
| Any `desktop/` artifact on a deployment that configures country redirects | `private, no-store` |
| A redirect to a presigned storage URL | `no-store` |
A release feed filename is `manifest.json`, a name ending in `.yml` or `.yaml`, a name beginning `RELEASES`, or a name beginning `releases` or `assets` and ending in `.json`.
A release feed filename is any of these:
- `manifest.json`
- A name ending in `.yml` or `.yaml`
- A name beginning `RELEASES`
- A name beginning `releases` or `assets` and ending in `.json`
## Redirects
@@ -30,7 +30,7 @@ The [instance discovery](/http-api/instance/#limit-keys) document publishes a `f
## Supported containers
The container is detected from the decoded bytes, so a filename, a `data:` media type, or any other declared type has no effect on the result. The detected container resolves to one of four stored extensions, and every other input fails with `ENTRANCE_SOUND_INVALID_FORMAT`.
The container is detected from the decoded bytes, so a filename, a `data:` media type, or any other declared type has no effect on the result. The detected container resolves to a stored extension, and every other input fails with `ENTRANCE_SOUND_INVALID_FORMAT`.
| Detected container | Stored extension | Stored content type |
| --- | --- | --- |
@@ -122,7 +122,7 @@ A scope with no assigned sound has no selection object. Clearing a scope deletes
| dms | Applies in direct message and group calls |
| `guild:{guild_id}` | Applies in one named guild, where the ID is 1 through 20 digits |
`scope_id` must match one of the four forms exactly. Any other value fails with the validation code `INVALID_FORMAT` at the `scope_id` path.
`scope_id` must match one of these forms exactly. Any other value fails with the validation code `INVALID_FORMAT` at the `scope_id` path.
:::note[Fluxer stores scopes but never resolves them]
A selection records which sound belongs to which scope and nothing more. [Play entrance sound](#play-entrance-sound) names the sound explicitly, so the client decides which selection applies to a channel.
@@ -194,7 +194,7 @@ Every failure below is a 400 `INVALID_FORM_BODY`. One response can report two of
The first four rows are checked before the quota, so an account already holding 8 clips receives `BASE64_LENGTH_INVALID` or `INVALID_BASE64_FORMAT` for a malformed or over-long payload.
Both duration bounds report the same code at the `audio` path, and the entry has no member naming which of the two was crossed.
The validation entry has no member naming which duration bound the clip crossed.
### Response
@@ -343,7 +343,7 @@ Nothing is published to the media servers. Each recipient receives a Dispatch na
Fluxer evaluates no channel permission and checks no guild membership.
:::
An ID naming no channel is refused exactly like a channel the caller is not connected to, so the response tells the two apart in neither direction. The connection check runs before the clip is looked up, so a caller who is not connected never learns whether the named clip exists.
An ID naming no channel is refused exactly like a channel the caller is not connected to. The connection check runs before the clip is looked up, so a caller who is not connected never learns whether the named clip exists.
### Side effects
@@ -12,7 +12,7 @@ The `message` field is rendered from a template, is translated per request, and
## Selecting the error code
An error response always has `code` and `message`. A failure with more to report adds its own members beside those two. Every added member sits at the top level of the response. A rate limit denial adds `global` and `retry_after`, a missing scope adds `required_scope`, and a validation failure adds `errors`.
An error response always has `code` and `message`. A failure with more to report adds its own members beside those two, at the top level of the response. A rate limit denial adds `global` and `retry_after`, a missing scope adds `required_scope`, and a validation failure adds `errors`.
:::note[OAuth2 endpoints use a different envelope]
An OAuth2 protocol failure raised by the [OAuth2 resource](/http-api/oauth2/) answers with the RFC 6749 shape. That body is `error` and `error_description` and nothing else, so its values appear in no registry here.
@@ -22,7 +22,13 @@ An OAuth2 protocol failure raised by the [OAuth2 resource](/http-api/oauth2/) an
The error code determines which supplementary members a failure has, and most codes have none. A client reads only the members documented for the code it matched. `errors` is the list of field violations. `retry_after` is the delay before another attempt is admitted, `global` is `true` on a global rate limit denial and `false` on a route one, `required_scope` is the OAuth2 scope the request is missing, and `has_mfa` and `methods` are the [sudo mode](/http-api/users/mfa/#sudo-mode) proofs an account can supply.
The two IP ban codes have their own members. `GLOBAL_IP_BANNED` and `GLOBAL_IP_TEMPORARILY_BANNED` both have `ip_address` with the normalised client address, `appeal_email` with the address an appeal is sent to, `appeals_supported` which is `true` only for the permanent ban, and `ban_kind` which is `permanent` or `temporary_24h`. `expires_at` is an ISO 8601 timestamp when the ban records an expiry and `null` otherwise, including on every permanent ban.
`GLOBAL_IP_BANNED` and `GLOBAL_IP_TEMPORARILY_BANNED` have their own members:
- `ip_address` is the normalised client address.
- `appeal_email` is the address an appeal is sent to.
- `appeals_supported` is `true` only for the permanent ban.
- `ban_kind` is `permanent` or `temporary_24h`.
- `expires_at` is an ISO 8601 timestamp when the ban records an expiry, and `null` otherwise, including on every permanent ban.
## Validation failure codes
@@ -56,7 +62,7 @@ The `path` of an element is the dot-joined position of the failed value, so a ne
An empty or whitespace-only body becomes `{}`, so the response reports the fields the schema then finds missing. A body that does not parse as JSON returns 400 `INVALID_FORM_BODY` with one element at path `body` and code `INVALID_FORMAT`.
:::
Fluxer normalises empty values on all four targets before validation runs. An empty string becomes `null` wherever it appears, including inside an array element. A nested object becomes `null` when it holds no members, and it becomes `null` when every one of its members is `null` after Fluxer has applied the same rule to each of them. The top-level object itself is never replaced, so a request that sends nothing still reaches the schema as an object and fails on the fields the schema requires.
Fluxer normalises empty values on all four targets before validation runs. An empty string becomes `null` wherever it appears, including inside an array element. A nested object becomes `null` when it holds no members. It also becomes `null` when every one of its members is `null` after Fluxer has applied the same rule to each of them. The top-level object itself is never replaced, so a request that sends nothing still reaches the schema as an object and fails on the fields the schema requires.
:::caution[An enumerated validation failure answers 400 alone]
A validation failure whose elements have enumerated codes answers 400 with its elements in `errors`, and its top-level code is `INVALID_FORM_BODY` everywhere except [Modify current user settings](/http-api/users/settings/#modify-current-user-settings). Any other status, retry guidance, or response header from the original failure is dropped.
@@ -79,9 +85,9 @@ A boundary schema constraint can name its own [validation code](#validation-erro
## HTTP status fallback codes
A failure that has an HTTP status and no recognised Fluxer error code takes the fallback `code` its status selects below. An unclassified 401, 413, 422, or 429 falls back to `GENERAL_ERROR`. An error named by an operation or by one of the registries below keeps that more specific code.
A failure that has an HTTP status and no recognised Fluxer error code takes the fallback `code` its status selects below. Where an operation or one of the registries below names the error, Fluxer keeps that more specific code. An unclassified 401, 413, 422, or 429 falls back to `GENERAL_ERROR`.
An unrecognised failure returns 500 with `INTERNAL_SERVER_ERROR` and a generic message that names no detail. A failure that has a registered API code but no status returns 400, except `GENERAL_ERROR`, which returns 500.
Fluxer answers an unrecognised failure with 500 `INTERNAL_SERVER_ERROR` and a generic message that names no detail. A failure that has a registered API code but no status returns 400, except `GENERAL_ERROR`, which returns 500.
| Status | Code | Description |
| --- | --- | --- |
@@ -118,14 +124,14 @@ The window length, both thresholds, and the number of windows the score trigger
## API error code registry
These codes appear in the top-level `code` field of an error response, transmitted as the exact JSON string shown. The registry is closed and holds exactly 264 codes. Each entry states the leading sentence of the English source message, without its final full stop. Those messages call a [guild](/http-api/guilds/) a community.
These codes appear in the top-level `code` field of an error response, transmitted as the exact JSON string shown. The registry is closed. Each entry states the leading sentence of the English source message, without its final full stop. Those messages call a [guild](/http-api/guilds/) a community.
:::note[The rendered `message` fills in the braced values]
A description containing a value in braces is an ICU MessageFormat template. `You've reached the maximum of {count, plural, one {# emoji} other {# emojis}}` renders as a complete sentence with the applicable limit.
:::
:::note[A rendered `message` can run longer than the registry entry]
Several source messages append a further recovery sentence with a template value such as a maximum size, a format list, or a retry delay. A client branches on `code` and reads the members the operation documents.
Several source messages append a further recovery sentence with a template value such as a maximum size, a format list, or a retry delay.
:::
### `ACCESS_DENIED`
@@ -1187,7 +1193,7 @@ You've reached the maximum of {count, plural, one {# WebAuthn credential} other
## Validation error code registry
These codes appear in the `code` field of an element in the top-level `errors` array on an `INVALID_FORM_BODY` response. Each names one specific input constraint. A schema failure whose constraint names no code of its own reports one of the [default schema failure codes](#default-schema-failure-codes). The registry is closed and holds exactly 236 codes. Each entry states the leading sentence of the English source message, the same way.
These codes appear in the `code` field of an element in the top-level `errors` array on an `INVALID_FORM_BODY` response. Each names one specific input constraint. A schema failure whose constraint names no code of its own reports one of the [default schema failure codes](#default-schema-failure-codes). The registry is closed. Each entry states the leading sentence of the English source message, the same way.
### `ACCENT_COLOR_CHANGED_TOO_MANY_TIMES`
@@ -2142,4 +2148,4 @@ A failure raised before the request locale is resolved, such as an IP ban denial
Fluxer localises the `message` of a validation element that has a `code` the same way. A validation element with no `code` has the fixed English string written at the failure site. The `code` field is never localised, either in the envelope or in a validation element.
A code whose catalogue entry is missing for the resolved locale falls back to its English source template. A code with no registered template at all falls back to the message the failure supplied or to the code itself.
A code whose catalogue entry is missing for the resolved locale falls back to its English source template. Where no template is registered at all, the `message` falls back to the one the failure supplied, or to the code itself.
@@ -6,18 +6,16 @@ description: Cross-guild lookup of a custom emoji or sticker by its own ID.
import RouteHeader from '@/components/RouteHeader.astro';
An expression is a custom emoji or a sticker that one guild owns. The two routes here resolve an expression from its own identifier, without membership of the owning guild.
An expression is a custom emoji or a sticker that one guild owns. The routes here resolve an expression from its own identifier, without membership of the owning guild.
| Object | Route | Owning resource |
| --- | --- | --- |
| [Emoji metadata](#emoji-metadata-object) | [Get emoji metadata](#get-emoji-metadata) | [Guild emojis](/http-api/guild-emojis/) |
| [Sticker metadata](#sticker-metadata-object) | [Get sticker metadata](#get-sticker-metadata) | [Guild stickers](/http-api/guild-stickers/) |
Both routes are read-only, and neither writes an audit entry or emits a [Gateway Dispatch](/gateway/events/).
Both routes are read-only, and neither writes an audit entry or emits a [Gateway Dispatch](/gateway/events/). Each one returns the metadata even when the owning guild has [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features).
An identifier that names no stored expression returns 404 `UNKNOWN_EMOJI` or 404 `UNKNOWN_STICKER`. A stored expression whose guild no longer exists returns 404 `UNKNOWN_GUILD`.
Both routes return the metadata even when the owning guild has [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features).
When an identifier names no stored expression, Fluxer returns 404 `UNKNOWN_EMOJI` or 404 `UNKNOWN_STICKER`. A stored expression whose guild no longer exists returns 404 `UNKNOWN_GUILD`.
:::note[Tags and the uploader need a guild-scoped operation]
Tags, the uploader, and the adult content classification come from the guild-scoped operations, which require membership of the owning guild.
@@ -53,7 +53,7 @@ Fluxer resolves only `url` from deployment configuration. `shards` and `session_
## Session start limit object
A session start limit reports how many new Gateway sessions a bot may open in a window. Fluxer publishes four constants here for client-library compatibility.
A session start limit reports how many new Gateway sessions a bot may open in a window. Fluxer publishes constants here for client-library compatibility.
### Structure
@@ -70,7 +70,7 @@ A session start limit reports how many new Gateway sessions a bot may open in a
<sup>3</sup> Fluxer runs no Identify concurrency bucket, so a bot MAY identify its shards without pacing them against this value
The limits the Gateway actually enforces live in [Session lifecycle](/gateway/limits-and-rate-limits/#session-lifecycle). Fluxer budgets Identify per source address. It caps a user account at a fixed number of concurrent sessions. Neither bound is reported here.
The limits the Gateway actually enforces are in [Session lifecycle](/gateway/limits-and-rate-limits/#session-lifecycle). Fluxer budgets Identify per source address and caps a user account at a fixed number of concurrent sessions. Neither bound is reported here.
:::caution[A remaining session start does not guarantee admission]
A non-zero `remaining` reserves nothing, and neither does the constant `max_concurrency`. [Session admission](/gateway/limits-and-rate-limits/#session-lifecycle) can still hold or reject a connection.
@@ -18,13 +18,13 @@ A provider outage, a request that outlives its deadline, and an unreadable provi
## Locale and country
Every route takes an optional `locale` drawn from the [supported locale registry](/topics/locales/#supported-locales), in the query string on the four GET routes and in the body of [Register a GIF share](#register-a-gif-share). It defaults to `en-US`. A tag outside the registry fails with 400 `INVALID_FORM_BODY`.
Every route takes an optional `locale` drawn from the [supported locale registry](/topics/locales/#supported-locales), in the query string on the GET routes and in the body of [Register a GIF share](#register-a-gif-share). It defaults to `en-US`. A tag outside the registry fails with 400 `INVALID_FORM_BODY`.
Fluxer also derives a two-letter country from the requesting address by geolocation, and uses `US` when geolocation resolves none. No route accepts the country as a parameter, so a client cannot override it. [Get GIF search suggestions](#get-gif-search-suggestions) is the one route that ignores it.
## Provider headers
Every response under `/gifs`, `/tenor`, and `/klipy` has these three headers, including error responses and responses to a path no route claims. The three values repeat the [GIF provider](/http-api/instance/#gif-provider-object) object, so a client follows a provider change without refetching `/.well-known/fluxer`.
Every response under `/gifs`, `/tenor`, and `/klipy` has these headers, including error responses and responses to a path no route claims. The values repeat the [GIF provider](/http-api/instance/#gif-provider-object) object, so a client follows a provider change without refetching `/.well-known/fluxer`.
| Field | Type | Description |
| --- | --- | --- |
@@ -126,7 +126,7 @@ To open the category, a client sends `name` as the `q` of [Search GIFs](#search-
## Deprecated vendor paths
Two vendor-named prefixes serve the same five handlers as `/gifs`. Each answers exactly as its successor does and has the same [provider headers](#provider-headers).
Two vendor-named prefixes serve the same handlers as `/gifs`. Each answers exactly as its successor does and has the same [provider headers](#provider-headers).
| Method | Deprecated path | Successor |
| --- | --- | --- |
@@ -142,10 +142,10 @@ Two vendor-named prefixes serve the same five handlers as `/gifs`. Each answers
| POST | `/v1/klipy/register-share` | [Register a GIF share](#register-a-gif-share) |
:::caution[The vendor prefixes spell trending differently]
Trending is `/trending` under `/gifs` and `/trending-gifs` under both vendor prefixes. The other four paths keep the same last segment on all three prefixes.
Trending is `/trending` under `/gifs` and `/trending-gifs` under both vendor prefixes. The other paths keep the same last segment on all three prefixes.
:::
A response from one of the ten has three further headers.
A response from a deprecated path has these further headers.
| Field | Type | Description |
| --- | --- | --- |
@@ -223,7 +223,7 @@ Fluxer resolves the category list against the country `US` for every request, wh
| 403 | [error response](/http-api/#error-response) | The instance has bound no provider key and the request returns `FEATURE_TEMPORARILY_DISABLED` |
| 503 | [error response](/http-api/#error-response) | The provider failed, the request outlived its deadline, or the `gifs` service answered with a payload the API could not read, each returning `SERVICE_UNAVAILABLE` |
Fluxer fetches both halves in one operation. A failure on either one fails the whole request and returns no partial body.
Fluxer fetches both halves in one operation. A failure on either `gifs` or `categories` fails the whole request and returns no partial body.
### Rate limit
@@ -18,7 +18,7 @@ A gift records no recipient, so Fluxer binds the code to whichever eligible acco
A gift records a duration. Fluxer computes the entitlement window at redemption time from the redeemer's existing state.
Both creation paths record a creator. A completed gift checkout records the purchaser, and an Admin API gift records the system account with ID `0` rather than the administrator that requested it. No operation unredeems a code.
Both creation paths record a creator. A completed gift checkout records the purchaser. An Admin API gift records the system account with ID `0`, and no field names the administrator that requested it. No operation unredeems a code.
### Structure
@@ -77,7 +77,7 @@ A purchased gift is always whole months or whole years, because a purchase of tw
Reads a gift by its code. Returns the [gift](#gift-object) object on success.
A code that does not exist and a code that has been revoked both return 404 `UNKNOWN_GIFT_CODE`, so a revoked code is never distinguishable from one that was never issued.
A code that does not exist and one that has been revoked both return 404 `UNKNOWN_GIFT_CODE`, so a revoked code is never distinguishable from one that was never issued.
A chargeback or a refund for the purchase revokes the gift when [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) processes it and the gift is still unredeemed. The gift also leaves [List current user gifts](/http-api/users/gifts/#list-current-user-gifts), so the buyer has no route that reports the reversal. A gift that was already redeemed when the same event arrives stays readable, and Fluxer recomputes the redeemer's entitlement from their remaining redeemed gifts.
@@ -151,7 +151,14 @@ One redemption can be in flight for a code across the whole deployment, and a se
The gift becomes redeemed, so [Get gift](#get-gift) reports `redeemed` as true and [List current user gifts](/http-api/users/gifts/#list-current-user-gifts) shows the redemption time and the redeemer to the buyer.
A gift with a positive quantity extends recurring premium. Fluxer stacks the duration onto the payment provider subscription only when the account's premium type is subscription, its premium end is unset or in the future, it has a stored subscription identity, and the instance has a payment provider configured. Stacking adds the duration to the existing trial end, or to the current period end when no trial end is set, and applies the update without proration.
A gift with a positive quantity extends recurring premium. Fluxer stacks the duration onto the payment provider subscription only when all of these hold:
- The account's premium type is subscription.
- The account's premium end is unset or in the future.
- The account has a stored subscription identity.
- The instance has a payment provider configured.
Stacking adds the duration to the existing trial end, or to the current period end when no trial end is set, and applies the update without proration.
The stacked path and the unstacked path both set the gift extension end to the gift duration added to the latest of the current time, the current premium end and the existing gift extension end. Both also clear an active grace deadline and set the premium type when the account has none. The stacked path also records the account as having ever purchased.
@@ -12,7 +12,7 @@ An audit log entry records one change made to a guild and the account that made
An audit-capable route accepts the `X-Audit-Log-Reason` request header, listed with the other [standard request headers](/http-api/#standard-request-headers). Fluxer reads the value verbatim and never percent-decodes it, so a caller that percent-encodes the reason stores and reads back the percent-encoded form.
Fluxer trims the value. A value that is blank before trimming, empty after it, or longer than 512 characters after it counts as no reason at all. None of the three fails the request, so a reason that misses the bound is dropped and the operation still succeeds.
Fluxer trims the value. A value that is blank before trimming, empty after it, or longer than 512 characters after it counts as no reason at all. None of those values fails the request, so a reason that misses the bound is dropped and the operation still succeeds.
The operation writes the accepted reason onto its entry, returns it as the entry `reason` field, and sends it in the [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) Dispatch.
@@ -42,7 +42,7 @@ One page of audit history together with the accounts and webhooks it references.
## Guild audit log entry object
One recorded guild change. An entry is immutable once written, except for a run of message deletion entries, which [List guild audit logs](#list-guild-audit-logs) can replace with a single consolidated entry. The entry ID is a [snowflake](/snowflakes/), so it has the exact creation time.
One recorded guild change. An entry is immutable once written, except for a run of message deletion entries, which [List guild audit logs](#list-guild-audit-logs) can replace with a single consolidated entry.
### Structure
@@ -193,7 +193,7 @@ One field a mutation changed, with the value on each side of the change.
A change value is a string, a JSON number, a boolean, `null`, an array of strings, an array of numbers, or an object with `added` and `removed` string arrays. The last shape comes only from the [permissions_diff](#guild-role-change-fields) key of a role modification.
:::note[A 64-bit domain value is a decimal string]
A permission mask or a snowflake is serialised as a decimal string. A bounded number such as a position or a colour remains a JSON number.
Fluxer serialises a permission mask or a snowflake as a decimal string. A bounded number such as a position or a colour remains a JSON number.
:::
:::caution[A change list holds only the differing fields]
@@ -402,7 +402,7 @@ Recorded by [STICKER_CREATE](#audit-actions), [STICKER_UPDATE](#audit-actions),
## Audit log webhook object
A reduced [webhook object](/http-api/webhooks/#webhook-object). It omits the execution token, application ID, and creating user, and it names the avatar hash `avatar_hash` rather than `avatar`.
A reduced [webhook object](/http-api/webhooks/#webhook-object). It omits the execution token, application ID, and creating user, and it names the avatar hash `avatar_hash` where the full object names it `avatar`.
### Structure
@@ -469,7 +469,7 @@ A request supplying neither `user_id` nor `action_type` deletes every run of two
:::
:::note[The same consolidation also runs in the background]
Fluxer also applies it to the 250 most recent entries about 30 seconds after a message deletion entry is written.
Fluxer also applies consolidation to the 250 most recent entries about 30 seconds after a message deletion entry is written.
:::
### Side effects
@@ -10,7 +10,7 @@ A guild channel is one of the rooms a guild is divided into, including the categ
## Access rules
A caller who is not a member of an existing guild receives 403 `MISSING_PERMISSIONS`. Every operation here returns 404 `UNKNOWN_GUILD` for a guild that does not exist.
Every operation here returns 404 `UNKNOWN_GUILD` for a guild that does not exist. A caller who is not a member of an existing guild receives 403 `MISSING_PERMISSIONS`.
A request whose path names a guild that has [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features) is refused with 403 `MISSING_ACCESS` before the operation runs.
@@ -189,7 +189,7 @@ Creates a guild channel and returns its [channel object](/http-api/channels/#cha
A value longer than 10000 characters is rejected with `STRING_LENGTH_INVALID` before normalisation runs. A parent that does not exist in this guild returns `INVALID_PARENT_CHANNEL`, and one that is not a category returns `PARENT_MUST_BE_CATEGORY`.
A parent category holds at most the instance-configured `max_channels_per_category` [limit](/http-api/instance/#limit-keys), defaulting to 50, and the guild holds at most `max_guild_channels`, defaulting to 500. Reaching the guild limit returns 400 `MAX_GUILD_CHANNELS` and reaching the category limit returns 400 `MAX_CATEGORY_CHANNELS`. Each message has the resolved limit. The new channel is created with no RTC region.
The guild holds at most the instance-configured `max_guild_channels` [limit](/http-api/instance/#limit-keys), defaulting to 500, and a parent category holds at most `max_channels_per_category`, defaulting to 50. Reaching the guild limit returns 400 `MAX_GUILD_CHANNELS` and reaching the category limit returns 400 `MAX_CATEGORY_CHANNELS`. Each message has the resolved limit. The new channel is created with no RTC region.
The new channel takes a position derived from its siblings. A category, and a channel created with no parent, is placed after the highest position in the guild. A voice channel created inside a category is placed after the last voice sibling, or after the last text or link sibling when the category holds no voice channel. A text or link channel is placed after the last text or link sibling, and a channel with no such sibling is placed directly after the category itself. The rest of the guild is not renumbered, so two channels can hold the same stored position until the next [hierarchy update](#modify-guild-channel-positions) renumbers them.
@@ -8,9 +8,9 @@ import RouteHeader from '@/components/RouteHeader.astro';
An emoji is a custom image that one guild owns. Members use it in messages and reactions. Its ID is a [snowflake](/snowflakes/) that is unique across every guild. [List guild emojis](#list-guild-emojis) is the only guild-scoped read, and [Get emoji metadata](/http-api/expressions/#get-emoji-metadata) resolves a single emoji without membership of its guild.
Every route names a guild in its path. A guild that has [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features) rejects the request with 403 `MISSING_ACCESS` before the operation runs, and [UNAVAILABLE_FOR_EVERYONE_BUT_STAFF](/http-api/guilds/#guild-features) does the same for an account without the instance staff flag.
Every route names a guild in its path. When that guild has [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features), Fluxer rejects the request with 403 `MISSING_ACCESS` before the operation runs. Fluxer does the same for a guild with [UNAVAILABLE_FOR_EVERYONE_BUT_STAFF](/http-api/guilds/#guild-features) when the account has no instance staff flag.
A guild that does not exist returns 404 `UNKNOWN_GUILD`. A caller who is not a current member of an existing guild returns 403 `MISSING_PERMISSIONS`, so guild existence is distinguishable from guild membership. On [Modify guild emoji](#modify-guild-emoji) a non-member receives the same 403 `MISSING_PERMISSIONS`, and a guild that does not exist returns 404 `UNKNOWN_EMOJI`.
A guild that does not exist returns 404 `UNKNOWN_GUILD`. Fluxer returns 403 `MISSING_PERMISSIONS` to a caller who is not a current member of an existing guild, so guild existence is distinguishable from guild membership. On [Modify guild emoji](#modify-guild-emoji) a non-member receives the same 403 `MISSING_PERMISSIONS`, and a guild that does not exist returns 404 `UNKNOWN_EMOJI`.
The instance phrase and URL blocklists screen a submitted emoji `name` before the operation runs, and a match returns 403 `CONTENT_BLOCKED`. A name of fewer than three characters is not scanned. Neither blocklist reads `image`. Fluxer checks its decoded bytes against the banned asset hash list when it stores them.
@@ -112,7 +112,7 @@ One item a bulk creation rejected, identified only by the name the caller submit
<sup>2</sup> Rendered in the locale of the authenticated account, so its value changes with the caller's locale
There is no machine-readable code, so a client that needs to branch on the reason retries the item on its own. An item that fails because the guild is full renders the emoji limit message with the resolved limit. Every other failure with a registered [error code](/http-api/errors/) renders that code's own message. An undecodable or unaccepted image renders the generic `INVALID_FORM_BODY` message with no per-field validation code. A banned image hash renders the `CONTENT_BLOCKED` message. A failure with no registered code, such as an object storage fault, renders a fixed unknown-error message.
There is no machine-readable code, so a client that needs to branch on the reason retries the item on its own. An item rejected because the guild is full renders the emoji limit message with the resolved limit. Every other failure with a registered [error code](/http-api/errors/) renders that code's own message. An undecodable or unaccepted image renders the generic `INVALID_FORM_BODY` message with no per-field validation code. A banned image hash renders the `CONTENT_BLOCKED` message. A failure with no registered code, such as an object storage fault, renders a fixed unknown-error message.
## List guild emojis
@@ -175,7 +175,7 @@ The body is one [emoji create object](#emoji-create-object).
<sup>2</sup> The [error code](/http-api/errors/) is `CONTENT_BLOCKED` for a blocked name and for a banned image hash, `MISSING_ACCESS` for an unavailable guild, and `MISSING_PERMISSIONS` for a membership or permission failure
An image the decoder cannot read returns `INVALID_IMAGE_FORMAT`. The name scan runs before the route is reached, so it precedes the rate limit bucket and the credential check. The image hash check runs at upload time, after every other admission step has passed.
The name scan runs before the route is reached, so it precedes the rate limit bucket and the credential check. Fluxer checks the image hash at upload time, after every other admission step has passed.
### Side effects
@@ -228,7 +228,7 @@ Every item name is scanned together before the route is reached, so one blocked
### Side effects
Each successful item consumes one guild emoji slot, stores its image, and writes an [`EMOJI_CREATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the supplied reason. One [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the guild's complete emoji collection is emitted after the batch when at least one item succeeded, and it is emitted before the audit entries are written. A failed item leaves no emoji, audit entry, or slot consumption.
Each successful item consumes one guild emoji slot, stores its image, and writes an [`EMOJI_CREATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the supplied reason. When at least one item succeeded, Fluxer emits one [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the guild's complete emoji collection after the batch, then writes the audit entries. A failed item leaves no emoji, audit entry, or slot consumption.
### Rate limit
@@ -144,7 +144,7 @@ Searches the guild member index. Returns a [guild member search response](#guild
### Limitations
- The caller must also hold at least one of [MANAGE_GUILD](/http-api/permissions/), [MANAGE_ROLES](/http-api/permissions/), [MANAGE_NICKNAMES](/http-api/permissions/), [BAN_MEMBERS](/http-api/permissions/), [MODERATE_MEMBERS](/http-api/permissions/), or [KICK_MEMBERS](/http-api/permissions/).
- Five of the six qualifying bits are [elevated permissions](/http-api/permissions/#elevated-permissions). In a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller other than the guild owner who holds one of those five and has no enrolled authenticator receives 400 `TWO_FACTOR_REQUIRED`. `MANAGE_NICKNAMES` is not elevated, so a caller who qualifies through it alone never receives that code.
- Every qualifying bit except `MANAGE_NICKNAMES` is one of the [elevated permissions](/http-api/permissions/#elevated-permissions). In a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller other than the guild owner who holds one of those elevated bits and has no enrolled authenticator receives 400 `TWO_FACTOR_REQUIRED`. A caller who qualifies through `MANAGE_NICKNAMES` alone never receives that code.
- A non-member of an existing guild receives the same 403 `MISSING_PERMISSIONS` as a member holding none of the qualifying bits, so a caller cannot tell the two apart.
A guild that does not exist returns 404 `UNKNOWN_GUILD`.
@@ -193,9 +193,9 @@ Every field is optional, and an omitted filter is not applied. The route reads a
[Input normalisation](/http-api/#input-normalisation) rewrites `""` to null, which the `query` field rejects with 400 `INVALID_FORM_BODY`. `" "` is accepted.
With no `sort_by`, the route sorts by join time and defaults `sort_order` to `desc`. Two members with the same join time are ordered by descending `id`. `relevance` ignores `sort_order`. A Meilisearch instance orders `relevance` by its own ranking rules. An Elasticsearch instance orders `relevance` by descending `id`.
With no `sort_by`, the route sorts by join time and defaults `sort_order` to `desc`. Two members with the same join time are ordered by descending `id`. `relevance` ignores `sort_order`. A Meilisearch instance orders a `relevance` search by its own ranking rules, and an Elasticsearch instance by descending `id`.
Text matching runs over the username, the discriminator, the global display name, the guild nickname, and the user ID, and a query can match the middle of a username as well as its start. Term matching and typo tolerance belong to the search backend. Filter matching is exact in every case.
Text matching runs over the username, the discriminator, the global display name, the guild nickname, and the user ID. A query can match the middle of a username as well as its start. The search backend defines term matching and typo tolerance. Filter matching is exact in every case.
:::caution[A deep `offset` returns 200 with an empty page]
Elasticsearch walks past its 10000 document window with an internal cursor and serves an arbitrary `offset`. Meilisearch caps the window at 10000 and returns an empty page beyond it.
@@ -13,7 +13,7 @@ A guild that no Gateway process serves returns 404 `UNKNOWN_GUILD`. An existing
The two modify operations return the resulting membership, [Transfer guild ownership](#transfer-guild-ownership) returns the updated guild, and every other mutation returns 204 with an empty body.
:::note[Fluxer reads the target before authorising the caller]
This applies to the two modify operations, [Remove guild member](#remove-guild-member), [Add guild member role](#add-guild-member-role), and [Remove guild member role](#remove-guild-member-role). A request whose target is not a member returns 404 `UNKNOWN_MEMBER` whether or not the caller can see the guild.
A request whose target is not a member returns 404 `UNKNOWN_MEMBER` whether or not the caller can see the guild. The rule covers the two modify operations, [Remove guild member](#remove-guild-member), [Add guild member role](#add-guild-member-role), and [Remove guild member role](#remove-guild-member-role).
:::
## Guild member object
@@ -49,7 +49,7 @@ Every field except `user` describes state that belongs to the membership.
<sup>6</sup> Omitted while the stored value is 0, so absence means no flag is set and, for `mention_flags`, that the account-level preference applies
Premium sanitisation hides the stored avatar and banner hashes and keeps them on the membership. Fluxer applies it once the account has lost its premium entitlement, when the membership holds a guild avatar, banner, biography, or accent colour.
Premium sanitisation hides the stored avatar and banner hashes and keeps them on the membership. Fluxer applies it after the account loses its premium entitlement, to each membership that holds a guild avatar, banner, biography, or accent colour.
A client compares `communication_disabled_until` against the current time, because a non-null value can name a moment that has already passed. A [communication timeout](/http-api/permissions/#communication-timeout) does not reduce the member's computed permission mask.
@@ -76,7 +76,7 @@ A client compares `communication_disabled_until` against the current time, becau
## Guild member profile flags
A guild profile asset has three states. With no flag and no hash the membership inherits the account-level asset. A stored hash sets a guild-specific asset. The flag below suppresses inheritance and renders the default.
A guild profile asset has three states. A membership with no flag and no stored hash inherits the account-level asset. A stored hash sets a guild-specific asset. The flag below suppresses inheritance and renders the default.
| Value | Name | Description |
| --- | --- | --- |
@@ -127,7 +127,7 @@ The request body shared by [Modify current guild member](#modify-current-guild-m
A `roles` entry that does not resolve to an existing role of the guild is dropped from the replacement, so a request naming only unknown roles clears the member's role set.
An `avatar` or `banner` base64 payload longer than 13981016 characters returns `BASE64_LENGTH_INVALID`, and a malformed one returns `INVALID_BASE64_FORMAT`. The decoded bytes are then checked against the instance-configured `avatar_max_size` [limit](/http-api/instance/#limit-keys), whose stock value is 10485760, and the same ceiling covers both fields. The decoded image must also pass the format allowlist and the animation rules of the asset policy for the field it sets. Pixel dimensions are not enforced. A value that is too large returns `IMAGE_SIZE_EXCEEDS_LIMIT`, and one whose format or animation is not allowed returns `INVALID_IMAGE_FORMAT`.
An `avatar` or `banner` base64 payload longer than 13981016 characters returns `BASE64_LENGTH_INVALID`, and a malformed one returns `INVALID_BASE64_FORMAT`. Fluxer then checks the decoded bytes against the instance-configured `avatar_max_size` [limit](/http-api/instance/#limit-keys), whose stock value is 10485760, and the same ceiling covers both fields. The decoded image must also pass the format allowlist and the animation rules of the asset policy for the field it sets. Pixel dimensions are not enforced. A value that is too large returns `IMAGE_SIZE_EXCEEDS_LIMIT`, and one whose format or animation is not allowed returns `INVALID_IMAGE_FORMAT`.
Null and any `communication_disabled_until` that is not in the future both clear the timeout. A time more than 365.25 days ahead returns `TIMEOUT_CANNOT_EXCEED_365_DAYS`, and a value that passes the schema but is not a real instant returns `INVALID_TIMEOUT_VALUE`. `timeout_reason` has no effect unless `communication_disabled_until` is also supplied.
@@ -240,7 +240,7 @@ A caller without `CHANGE_NICKNAME` does not fail the request. Fluxer discards th
A supplied `nick`, `bio`, or `pronouns` is scanned against the instance phrase, URL, and profile substring blocklists, and a match returns 403 `CONTENT_BLOCKED`.
Guild avatar, banner, biography, and accent colour additionally require the instance-configured `feature_per_guild_profiles` [limit](/http-api/instance/#limit-keys) for the calling account. Fluxer discards any of those four fields supplied without that capability and applies every other field. Pronouns, `profile_flags`, and `mention_flags` are not gated that way.
Guild avatar, banner, biography, and accent colour additionally require the instance-configured `feature_per_guild_profiles` [limit](/http-api/instance/#limit-keys) for the calling account. Fluxer discards any of those fields supplied without that capability and applies every other field. Pronouns, `profile_flags`, and `mention_flags` do not require it.
A `channel_id` destination must be a guild voice channel, and the caller must hold both [VIEW_CHANNEL](/http-api/permissions/) and [CONNECT](/http-api/permissions/) there. Supplying `channel_id` for a member with no live voice connection returns 400 `USER_NOT_IN_VOICE`. Supplying `mute` or `deaf` for such a member succeeds, stores the flags, and emits no voice Dispatch.
@@ -283,7 +283,7 @@ The target's own sessions always receive the Dispatch. Other guild sessions rece
Supplying `channel_id` moves or disconnects every voice connection the member holds, or only the one named by `connection_id`. It records one [`MEMBER_MOVE`](/http-api/guild-audit-logs/#audit-actions) or [`MEMBER_DISCONNECT`](/http-api/guild-audit-logs/#audit-actions) audit entry for the whole request, whatever the number of connections it touched, and emits [Voice State Update](/gateway/events/#voice-state-update) to sessions that can view the affected channel. Supplying `mute` or `deaf` updates the membership, records no separate audit entry, and emits the same voice Dispatch once for each live connection.
Every audit entry also emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create). A request that fails after uploading a new avatar or banner leaves the previous asset in place.
Fluxer emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) for every audit entry. A request that fails after uploading a new avatar or banner leaves the previous asset in place.
### Rate limit
@@ -307,13 +307,13 @@ A supplied `nick` is scanned against the instance phrase, URL, and profile subst
Hierarchy authority over the target is not consulted for `roles`, so a caller who outranks every affected role can replace the role set of a member who outranks them. The everyone role in the array is rejected with the field code `INVALID_ROLE_ID`.
The target's own permissions in the destination are never consulted, so a member can be moved into a channel they could not join themselves. Disconnecting sets `channel_id` to null and requires the same permission.
Fluxer never checks the target's own permissions in the destination, so a member can be moved into a channel they could not join themselves. Disconnecting sets `channel_id` to null and requires the same permission.
Supplying `channel_id` for a target with no live voice connection, or a `connection_id` that is not one of the target's own, returns 400 `USER_NOT_IN_VOICE`. Supplying `mute` or `deaf` for such a target succeeds and stores the flags. A destination that does not exist returns the field code `CHANNEL_DOES_NOT_EXIST`, and one that is not a voice channel `CHANNEL_MUST_BE_VOICE`.
`MANAGE_ROLES` and `MODERATE_MEMBERS` are [elevated permissions](/http-api/permissions/#elevated-permissions), so replacing `roles` or applying `communication_disabled_until` additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner.
A caller addressing their own user ID here falls under the self-targeted rules of [Modify current guild member](#modify-current-guild-member) for the guild profile fields, and the body remains the complete update object, so `roles` is accepted. Replacing their own role set still requires `MANAGE_ROLES` and hierarchy authority, and `communication_disabled_until` is still rejected with 403 `MISSING_PERMISSIONS`.
A caller addressing their own user ID here falls under the self-targeted rules of [Modify current guild member](#modify-current-guild-member) for the guild profile fields. The body remains the complete update object, so `roles` is accepted. Replacing their own role set still requires `MANAGE_ROLES` and hierarchy authority, and `communication_disabled_until` is still rejected with 403 `MISSING_PERMISSIONS`.
:::note[Guild profile fields belong to their own account]
`avatar`, `banner`, `bio`, `pronouns`, `accent_color`, `profile_flags`, and `mention_flags` describe the target's own profile, so a moderator cannot set them. Supplying one for another member leaves it unwritten, and the request still applies every field the caller may set.
@@ -394,7 +394,7 @@ The operation retains the member's first join time, leave time, and any stored c
It deletes the membership row and decreases the guild's member count, so the guild nickname, guild avatar hash, guild banner hash, biography, pronouns, accent colour, and role set are lost. The removed account's read states and guild settings are left untouched, and the guild is not removed from that account's guild folders.
The operation records a [`MEMBER_KICK`](/http-api/guild-audit-logs/#audit-actions) audit entry with no change list and emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create). Remaining guild sessions receive [Guild Member Remove](/gateway/events/#guild-member-remove), subject to [event filtering](/gateway/event-filtering/). The removed account's sessions receive [Guild Delete](/gateway/events/#guild-delete).
Fluxer records a [`MEMBER_KICK`](/http-api/guild-audit-logs/#audit-actions) audit entry with no change list and emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create). Remaining guild sessions receive [Guild Member Remove](/gateway/events/#guild-member-remove), subject to [event filtering](/gateway/event-filtering/). The removed account's sessions receive [Guild Delete](/gateway/events/#guild-delete).
### Rate limit
@@ -470,7 +470,7 @@ Adds one role to a member and returns 204 with an empty body. Requires membershi
- The guild owner bypasses the permission and the hierarchy check.
- The everyone role cannot be assigned and is rejected with the field code `INVALID_ROLE_ID`.
`MANAGE_ROLES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. Every accepted request emits [Guild Member Update](/gateway/events/#guild-member-update), including one that changes nothing.
`MANAGE_ROLES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner.
The guild owner receives 404 `UNKNOWN_ROLE` for a role that does not exist in the guild. Any other caller receives 403 `MISSING_PERMISSIONS`.
@@ -497,7 +497,7 @@ The guild owner receives 404 `UNKNOWN_ROLE` for a role that does not exist in th
### Side effects
When the member does not already hold the role, the operation adds it and makes a temporary membership permanent. It then updates member search results, records a [`MEMBER_ROLE_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Member Update](/gateway/events/#guild-member-update), and it does all four whether or not the role set changed. A request naming a role the member already holds still writes an audit entry with an empty change list, published without a `changes` member.
When the member does not already hold the role, the operation adds it and makes a temporary membership permanent. It then updates member search results, records a [`MEMBER_ROLE_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Member Update](/gateway/events/#guild-member-update). All four run whether or not the role set changed. A request naming a role the member already holds still writes an audit entry with an empty change list, published without a `changes` member.
The audit log response names the role only through the change list.
@@ -516,7 +516,7 @@ Removes one role from a member and returns 204 with an empty body. Requires memb
- The guild owner bypasses the permission and the hierarchy check.
- The everyone role cannot be removed and is rejected with the field code `INVALID_ROLE_ID`.
`MANAGE_ROLES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. Every accepted request emits [Guild Member Update](/gateway/events/#guild-member-update), including one that changes nothing. A role that does not exist is reported exactly as it is for [Add guild member role](#add-guild-member-role).
`MANAGE_ROLES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it additionally requires an enrolled multi-factor authenticator when the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated and the caller is not the guild owner. A role that does not exist is reported exactly as it is for [Add guild member role](#add-guild-member-role).
### Path parameters
@@ -541,7 +541,7 @@ Removes one role from a member and returns 204 with an empty body. Requires memb
### Side effects
When the member holds the role, the operation removes it. It then updates member search results, records a [`MEMBER_ROLE_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Member Update](/gateway/events/#guild-member-update), and it does all four whether or not the role set changed. Removing a role does not change whether the membership is temporary.
When the member holds the role, the operation removes it. It then updates member search results, records a [`MEMBER_ROLE_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry, emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create), and emits [Guild Member Update](/gateway/events/#guild-member-update). All four run whether or not the role set changed. Removing a role does not change whether the membership is temporary.
### Rate limit
@@ -50,7 +50,7 @@ A ban also stores the target's last known IP address and the lower-cased address
Accepting an [invite](/http-api/invites/) compares the joining account against the banned account identifier, the stored address, and the stored email. The address comparison uses a normalised decision key: the exact address for IPv4, the mapped IPv4 address for an IPv4-mapped IPv6 address, and the `/64` network for every other IPv6 address. An address match refuses the join with 403 `USER_IP_BANNED_FROM_GUILD`.
Fluxer skips the address match when the joining address is on the instance exemption list, and also when the banned address is a single address and the joining address is reported as high mobile or carrier-grade NAT blast radius. The match runs when that report is unavailable or fails.
Fluxer skips the address match when the joining address is on the instance exemption list. It also skips the match when the banned address is a single address and the joining address is reported as high mobile or carrier-grade NAT blast radius. The match runs when that report is unavailable or fails.
The email comparison is exact and runs only when an invite is accepted. An email match refuses the join with 403 `USER_BANNED_FROM_GUILD`. Every other path that adds a member compares the account identifier and the address alone, and several compare nothing. [Grant OAuth2 consent](/http-api/oauth2/#grant-oauth2-consent) installing a bot, the admin membership operations, the stock community auto-join, and the premium entitlement guild join all add the member without consulting the ban list.
@@ -255,7 +255,7 @@ A creation template describes the roles and channels that [Create guild](#create
<sup>3</sup> The identifier is applied only when it resolves to a category in the same template, and every other value leaves the channel at the guild root
<sup>4</sup> The three voice fields are stored only on a voice channel and are null on every other channel type
<sup>4</sup> These voice fields are stored only on a voice channel and are null on every other channel type
<sup>5</sup> An entry is applied only when its resolved `type` is 0 and its `id` resolves to a role in the same template, and the identifier `0` resolves to the everyone role. Fluxer skips every other entry, and a channel whose entries are all skipped is created with no overwrites
@@ -303,7 +303,7 @@ The guild owner, a bot, and any member holding at least one role bypass the chec
<sup>1</sup> Only the guild owner can change this value, the owner account needs a second factor already configured, and the change requires sudo mode
While the level is ELEVATED, a caller other than the guild owner exercises the nine [elevated permissions](/http-api/permissions/#elevated-permissions) only with an enrolled authenticator, and a bot inherits the enrolment state of its application owner. An operation that asserts an elevated permission the caller holds but cannot exercise returns 400 `TWO_FACTOR_REQUIRED` after the permission itself has been confirmed.
While the level is ELEVATED, a caller other than the guild owner exercises the [elevated permissions](/http-api/permissions/#elevated-permissions) only with an enrolled authenticator, and a bot inherits the enrolment state of its application owner. An operation that asserts an elevated permission the caller holds but cannot exercise returns 400 `TWO_FACTOR_REQUIRED` after the permission itself has been confirmed.
## Splash card alignments
@@ -407,7 +407,7 @@ A feature is a capability or availability flag in the guild's `features` array.
<sup>1</sup> The feature is added or removed through [Modify guild](#modify-guild), as part of the complete array. Send every feature without this marker back to `features` exactly as the guild holds it
<sup>2</sup> The feature changes no HTTP API behaviour. An instance can name it in a filter of the ordered [limit configuration](/http-api/instance/#limit-keys), and the stock configuration names none of the three
<sup>2</sup> The feature changes no HTTP API behaviour. An instance can name it in a filter of the ordered [limit configuration](/http-api/instance/#limit-keys), and the stock configuration names none of them
<sup>3</sup> The expression operations raise the slot ceiling directly, outside the instance limit configuration
@@ -465,7 +465,7 @@ These fields prove [sudo mode](/http-api/users/mfa/#sudo-mode) when an operation
<sup>2</sup> The MFA proof is accepted only when the account has a second factor, and it returns the field code `INVALID_MFA_CODE` on any failure
A bot credential satisfies sudo mode without any proof. An account that has no password hash and no second factor also satisfies it without any proof. Every other account that supplies no usable proof receives 403 `SUDO_MODE_REQUIRED`, whose error object has `has_mfa` and a `methods` object reporting whether `totp` and `webauthn` are available.
A bot credential satisfies sudo mode without any proof, and so does an account that has no password hash and no second factor. Every other account that supplies no usable proof receives 403 `SUDO_MODE_REQUIRED`, whose error object has `has_mfa` and a `methods` object reporting whether `totp` and `webauthn` are available.
Fluxer issues a newly generated proof in the `X-Fluxer-Sudo-Mode-JWT` header of the success response, and only for an account that has a second factor. A proof supplied on the request is echoed back in that same header.
@@ -507,7 +507,7 @@ The `icon` base64 payload is bounded to 1 to 13981016 characters and is otherwis
Without a template the guild is created with a `Text Channels` category holding a text channel named `general`, a `Voice Channels` category holding a voice channel named `General`, and an everyone role with the default permission set. The `general` channel becomes the system channel.
:::note[Embedded collections arrive on the Guild Create Dispatch]
The creation response has none of them. Read [Get guild](#get-guild) afterwards to fetch roles, channels, emojis, stickers, and member counts over HTTP.
The creation response has no roles, channels, emojis, stickers, or member counts. Read [Get guild](#get-guild) afterwards to fetch them over HTTP.
:::
### Response
@@ -549,7 +549,7 @@ Returns an array of [guild objects](#guild-object), one for every guild the auth
<sup>2</sup> A guild whose counts cannot be fetched is returned without them
A membership whose guild record can no longer be resolved is dropped before the cursor and `limit` are applied, so the page still has up to `limit` guilds when further guilds remain. This route runs no availability screen, and a guild with `UNAVAILABLE_FOR_EVERYONE` is still listed.
Fluxer drops a membership whose guild record can no longer be resolved before it applies the cursor and `limit`, so the page still has up to `limit` guilds when further guilds remain. This route runs no availability screen, and a guild with `UNAVAILABLE_FOR_EVERYONE` is still listed.
:::caution[`permissions` needs a limit of 100 or lower]
A page of more than 100 guilds, or a failed lookup, omits `permissions` from every guild. The request still succeeds.
@@ -20,7 +20,7 @@ The API applies no generic byte limit to a request body. An operation that accep
Every request counts against one in-flight request ceiling for the whole instance. A request that arrives while the instance is at that ceiling returns 503 `SERVICE_UNAVAILABLE` with `Retry-After: 1` before the operation runs. The `/_health`, `/_healthz`, and `/_metrics` probe paths are exempt.
A request to a path that matches no route returns 404 `NOT_FOUND`. A request whose path is registered but not for the request method is answered the same way, and the response has no `Allow` header. Routing is strict, so a trailing slash is significant.
A request to a path that matches no route returns 404 `NOT_FOUND`. A request using a method the path does not register returns the same 404, and the response has no `Allow` header. Routing is strict, so a trailing slash is significant.
The `GET` registered for a path also serves `HEAD`. A path that registers no `GET` serves no `HEAD` either. A `HEAD` response has the status and headers that `GET` returns, with no body. The request still reports `HEAD` as its method, so the [same-host origin check](#cross-origin-requests) can refuse a `HEAD` that has no `Origin` where the identical `GET` succeeds.
@@ -45,7 +45,13 @@ Form bodies accept `application/x-www-form-urlencoded` and `multipart/form-data`
The legacy names `file` and `file` followed by an index are accepted as file fields as well, and a bare `file` takes the next free legacy index.
Five field-name failures are rejected with their own code. An index outside either bound returns `FILE_INDEX_EXCEEDS_MAXIMUM`. Any other name beginning with `files[` returns `INVALID_FILE_FIELD_NAME`. Two file fields resolving to the same index return `DUPLICATE_FILE_INDEX`. More than one file supplied for one index returns `MULTIPLE_FILES_FOR_INDEX_NOT_ALLOWED`. Where the resolved limit is 0, any file field at all returns `ATTACHMENTS_NOT_ALLOWED_FOR_MESSAGE`.
Field-name failures are rejected with their own code:
- An index outside either bound returns `FILE_INDEX_EXCEEDS_MAXIMUM`.
- Any other name beginning with `files[` returns `INVALID_FILE_FIELD_NAME`.
- Two file fields resolving to the same index return `DUPLICATE_FILE_INDEX`.
- More than one file supplied for one index returns `MULTIPLE_FILES_FOR_INDEX_NOT_ALLOWED`.
- Where the resolved limit is 0, any file field at all returns `ATTACHMENTS_NOT_ALLOWED_FOR_MESSAGE`.
The `Content-Type` header supplies the multipart boundary. Each part's field name is in `Content-Disposition`. A body the multipart parser cannot read is rejected with `FAILED_TO_PARSE_MULTIPART_FORM_DATA`. A field name the operation does not recognise is ignored, and a `files[n]` part whose value is not a file is ignored once its index has been bounds-checked.
@@ -6,7 +6,7 @@ description: Instance discovery, client geolocation, the limit key registry, and
import RouteHeader from '@/components/RouteHeader.astro';
The Instance resource describes how one Fluxer deployment is set up. It publishes the discovery document, the client geolocation lookup, and the served OpenAPI document, and it owns the [limit key](#limit-keys) registry that every other page cites.
The Instance resource describes how one Fluxer deployment is set up. It publishes the discovery document, the client geolocation lookup, and the served OpenAPI document, and it owns the [limit key](#limit-keys) registry.
[Get instance discovery](#get-instance-discovery) is the entry point of the API. A client that knows only a Fluxer origin reads `/.well-known/fluxer` first, and that one unauthenticated response has every field of the [instance discovery object](#instance-discovery-object). Those values include the API base URLs, the [main Gateway](/gateway/overview/) WebSocket URL, and the [Media Proxy](/media-proxy/overview/) base URL, and they override the [endpoint path defaults](#instance-endpoints-object) a deployment would otherwise serve from its canonical public origin.
@@ -239,13 +239,19 @@ The ordered rules a client evaluates to work out the limits that apply to an acc
<sup>2</sup> The value is computed from the release's default limit values alone, so an operator editing a rule leaves it unchanged. It changes when a release changes the defaults, which invalidates a resolution a client cached against the older ones
Each rule publishes only the keys whose values differ from the release defaults. The defaults themselves are not published in this document, so a client MUST hold its own copy of them. A client MUST also rebuild every rule into a full limit map before it resolves anything, taking the default value of every [limit key](#limit-keys) and overlaying that rule's `overrides` on it.
Each rule publishes only the keys whose values differ from the release defaults. The defaults themselves are not published in this document, so a client MUST hold its own copy of them. Before it resolves anything, a client MUST also rebuild every rule into a full limit map, taking the default value of every [limit key](#limit-keys) and overlaying that rule's `overrides` on it.
A client starts from the default value of every key. It evaluates the rules from least specific to most specific, where specificity is the total number of trait and guild feature names in a rule's filters. Two rules of equal specificity keep their published order. A rule matches when every trait it names is present in the evaluation context and every guild feature it names is present.
The client starts from the default value of every key. It evaluates the rules from least specific to most specific, where specificity is the total number of trait and guild feature names in a rule's filters. Two rules of equal specificity keep their published order. A rule matches when every trait it names is present in the evaluation context and every guild feature it names is present.
A rule that names no trait and no guild feature replaces the current value for each key it has. A rule that names at least one raises the current value to its own value when that is higher. Because a rebuilt rule has every key, an unfiltered rule returns each key it did not override to the default. A matching filtered rule raises each key it did not override to at least the default.
Two evaluation contexts exist. A user evaluation applies only keys whose scope is user or is both. A guild evaluation applies every key whose scope is both. It applies a user-scoped key only when the rule has no guild feature filter. It applies a guild-scoped key when the rule has a guild feature filter, or when the rule has no trait filter at all. The [limit key](#limit-keys) registry states the scope of each key.
Two evaluation contexts exist. A client reads the key's scope and the rule's filters to decide which keys an evaluation applies. The [limit key](#limit-keys) registry states the scope of each key.
| Scope | User evaluation | Guild evaluation |
| --- | --- | --- |
| user | Applied | Applied only when the rule has no guild feature filter |
| guild | Not applied | Applied when the rule has a guild feature filter, or when the rule has no trait filter at all |
| both | Applied | Applied |
:::note[The operation applies its own authoritative bound]
The document lets a client present the correct bounds before it attempts an operation. A request that exceeds the resolved value still fails with the operation's own documented error.
@@ -255,7 +261,7 @@ The document lets a client present the correct bounds before it attempts an oper
The document publishes neither the requesting account's traits nor any guild feature set. A client supplies that context from its own authenticated state.
:::
`traitDefinitions` names the traits the deployment advertises. Nothing validates a rule's `traits` filter against it, so a rule can filter on a name the collection omits. A self-hosted deployment whose premium mode grants every account the stock limits publishes an empty collection and has no rule that filters on `premium`.
Nothing validates a rule's `traits` filter against `traitDefinitions`, so a rule can filter on a name that collection omits. A self-hosted deployment whose premium mode grants every account the stock limits publishes an empty collection and has no rule that filters on `premium`.
## Limit rule object
@@ -347,11 +353,11 @@ Each key names one limit. A key whose name begins with `feature_` is a feature g
| max_webhooks_per_guild | Maximum webhooks per guild, in guild scope |
| sticker_max_size | Maximum file size for sticker uploads in bytes, in guild scope |
<sup>1</sup> Emoji limits are enforced as one shared total against `max_guild_emojis`, and the four aliases exist only so an older client reads a plausible value
<sup>1</sup> Emoji limits are enforced as one shared total against `max_guild_emojis`, and the aliases exist only so an older client reads a plausible value
<sup>2</sup> Sticker limits are enforced as one shared total against `max_guild_stickers`
Where a rule sets `max_guild_emojis`, `max_guild_stickers`, or one of the five compatibility aliases, it republishes that key in its own `overrides` even when the value matches the default.
Where a rule sets `max_guild_emojis`, `max_guild_stickers`, or one of the compatibility aliases, it republishes that key in its own `overrides` even when the value matches the default.
## Public application configuration object
@@ -397,7 +403,7 @@ Whether the deployment has finished its initial configuration, and where an oper
## Public legal configuration object
Two public URLs a client links from its registration form.
The public URLs a client links from its registration form.
### Structure
@@ -437,7 +443,7 @@ The approximate location Fluxer resolved for the request, together with the regi
<sup>3</sup> The value is a decimal string
The two geo collections are fixed by the release, so they are identical in every response. A client resolves its own country and subdivision from the detected fields and then selects the matching entry itself.
The geo collections are fixed by the release, so they are identical in every response. A client resolves its own country and subdivision from the detected fields and then selects the matching entry itself.
### Example
@@ -479,7 +485,7 @@ The path has no `/v1` prefix.
### Rate limit
60 requests per minute for each authenticated user, or for each client IP address when the request has no credential, on the `instance:info` bucket. [Get OpenAPI document](#get-openapi-document) draws on the same bucket, so the two operations share one allowance.
60 requests per minute for each authenticated user, or for each client IP address when the request has no credential, on the `instance:info` bucket. [Get OpenAPI document](#get-openapi-document) shares this bucket.
## Get client geolocation
@@ -12,7 +12,7 @@ An invite is a code that admits an account into a guild or a group direct messag
## Invite object
An invite object describes one code and the target it admits into. The `type` selects the representation. A guild invite has the guild it admits into and that guild's online count. A group direct message invite exposes the group's current recipients through its partial channel.
An invite object describes one code and the target it admits into. The `type` selects the representation. A guild invite has the guild it admits into and that guild's online count, and a group direct message invite lists the group's current recipients in its partial channel.
### Structure
@@ -299,7 +299,7 @@ Creates an invite for a channel, or returns an existing equivalent invite. Retur
### Limitations
- A guild channel requires guild membership, [VIEW_CHANNEL](/http-api/permissions/), and [CREATE_INSTANT_INVITE](/http-api/permissions/) in that exact channel.
- An age-restricted guild channel additionally requires an age-verified account.
- An age-restricted guild channel also requires an age-verified account.
- A private channel requires the caller to be a current recipient, and no ownership is required.
`CREATE_INSTANT_INVITE` is read directly from the main Gateway, so this operation never requires elevated multi-factor authentication.
@@ -367,7 +367,7 @@ Returns the standard invites of one channel as an array of [invite metadata obje
### Limitations
- A guild channel requires guild membership, [VIEW_CHANNEL](/http-api/permissions/) in that channel, and [MANAGE_CHANNELS](/http-api/permissions/) in the guild.
- An age-restricted guild channel additionally requires an age-verified account.
- An age-restricted guild channel also requires an age-verified account.
- A group direct message requires the caller to be a current recipient and its owner.
`MANAGE_CHANNELS` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller who is neither the guild owner nor enrolled in multi-factor authentication receives 400 [`TWO_FACTOR_REQUIRED`](/http-api/errors/) in a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated.
@@ -14,7 +14,7 @@ The routes here are user-only and operate on the caller's own collection. Fluxer
Fluxer caps a collection at the instance-configured `max_favorite_memes` [limit](/http-api/instance/#limit-keys), with a fallback of 50. Both creation operations check the count before they touch the channel, the message, or the source URL, so an account at its limit is refused with 400 `MAX_FAVORITE_MEMES` even when the rest of the request would have failed. The error body has `max_favorite_memes` reporting the ceiling reached.
A meme has at most `max_favorite_meme_tags` tags, with a fallback of 10. A request that exceeds the allowance returns 400 `INVALID_FORM_BODY` with the validation code `MAX_FAVORITE_MEME_TAGS_EXCEEDED` at the `tags` path.
A meme has at most `max_favorite_meme_tags` tags, with a fallback of 10. Fluxer rejects a longer list with 400 `INVALID_FORM_BODY` and the validation code `MAX_FAVORITE_MEME_TAGS_EXCEEDED` at the `tags` path.
Fluxer normalises and trims a tag before counting it, and the result must be 1 through 30 characters. A tag that normalises to the empty string fails with 400 `INVALID_FORM_BODY` and `STRING_LENGTH_INVALID`.
@@ -203,7 +203,7 @@ Fetches an absolute media URL, stores a durable Fluxer copy, and returns the cre
<sup>4</sup> Persisted only when the request resolved to a provider GIF and the map is non-empty, and discarded otherwise
The stored filename is the last path segment of `url` after percent-decoding. Fluxer appends an extension inferred from the resolved content type when that segment has no dot, then normalises the result so whitespace becomes `_` and every character outside letters, digits, combining marks, `_`, `.`, and `-` is dropped. A URL path with no last segment yields `media.{extension}`. A segment that normalises to nothing, or to only dots and underscores, yields `unnamed`, and the extension is `bin` when the content type maps to none.
The stored filename is the last path segment of `url` after percent-decoding. Fluxer appends an extension inferred from the resolved content type when that segment has no dot, then normalises the result so whitespace becomes `_` and every character outside letters, digits, combining marks, `_`, `.`, and `-` is dropped. A URL path with no last segment yields `media.{extension}`. Where the segment normalises to nothing, or to only dots and underscores, the filename is `unnamed`. The extension is `bin` when the content type maps to none.
The stored `gif_provider` is always the configured provider's own name, so new provider GIFs are sourced from KLIPY even when the request names another provider.
@@ -267,7 +267,7 @@ When neither key selects anything, Fluxer takes the message's first attachment.
Only an `image/*`, `video/*`, or `audio/*` asset can be saved. `attachment_id` is matched only when the message or one of its snapshots has at least one attachment, and an ID matching none of them fails with `ATTACHMENT_ID_NOT_FOUND_IN_MESSAGE` at the `attachment_id` path. When the message has no attachment at all, Fluxer ignores the ID and takes the first usable embed. Only the selected attachment is tried, so a message whose chosen attachment has an unsupported type falls through to its embeds. A selection that resolves to no supported media fails with `NO_VALID_MEDIA_IN_MESSAGE` at the `media` path.
An embed selection uses the embed's image, video, or thumbnail, in that order. A URL the instance's own media endpoint serves is copied without a fetch. Any other URL is fetched over the network, and a fetch that resolves no metadata fails with 400 `MEDIA_METADATA_ERROR`.
An embed selection uses the embed's image, video, or thumbnail, in that order. When the URL is one the instance's own media endpoint serves, Fluxer copies the asset without a fetch. Any other URL is fetched over the network, and a fetch that resolves no metadata fails with 400 `MEDIA_METADATA_ERROR`.
### Response
@@ -443,7 +443,7 @@ Fluxer derives a two-letter country from the requesting address by geolocation,
<sup>1</sup> Exactly one entry per submitted URL, in the submitted order, so a client may index the result positionally against its request
Fluxer resolves a URL in three stages. It offers the URL to the configured GIF provider, then reads it as direct external media, and then unfurls it as a page when the direct read produced no renderable image or video. A failure in any stage downgrades that URL's entry alone. A URL that none of the three stages resolves still returns an entry, with its signed proxy URL, an empty `media` map, and whatever the direct read reported. Dimensions are zero and `content_type` is the empty string when the direct read reported nothing.
Fluxer resolves a URL in three stages. It offers the URL to the configured GIF provider, then reads it as direct external media, and then unfurls it as a page when the direct read produced no renderable image or video. When a stage fails, Fluxer downgrades that URL's entry alone. A URL that none of the three stages resolves still returns an entry, with its signed proxy URL, an empty `media` map, and whatever the direct read reported. Dimensions are zero and `content_type` is the empty string when the direct read reported nothing.
### Response
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
A message is one post in a channel, together with the text, attachments, embeds, stickers, and reactions stored with it. Channel metadata and membership are defined by the [Channels resource](/http-api/channels/).
Two terms recur below. A text-bearing channel is a [channel type](/http-api/channels/#channel-types) that stores messages, which is every type except a guild category and a guild link channel. The message history cutoff is an instant a guild stores on itself, and a member without [READ_MESSAGE_HISTORY](/http-api/permissions/) reads nothing created before it.
A text-bearing channel is a [channel type](/http-api/channels/#channel-types) that stores messages, which is every type except a guild category and a guild link channel. The message history cutoff is an instant a guild stores on itself, and a member without [READ_MESSAGE_HISTORY](/http-api/permissions/) reads nothing created before it.
## Channel resolution
@@ -117,7 +117,7 @@ A reply reference resolves into `referenced_message`, and a forward has none. Fl
| 7 | USER_JOIN<sup>2</sup> | User-join system message |
| 19 | REPLY<sup>1</sup> <sup>2</sup> | Message with a reply reference |
<sup>1</sup> Only `DEFAULT` and `REPLY` can be modified, pinned, unpinned, or used as the target of a reply reference. Any other type rejects modification, pinning, and unpinning with 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`, and rejects being replied to with 400 `CANNOT_REPLY_TO_SYSTEM_MESSAGE`
<sup>1</sup> Only `DEFAULT` and `REPLY` can be modified, pinned, unpinned, or used as the target of a reply reference. Any other type rejects modification, pinning, and unpinning with 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`, and rejects being replied to with the field code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`
<sup>2</sup> Only `DEFAULT`, `CHANNEL_PINNED_MESSAGE`, `USER_JOIN`, and `REPLY` can be deleted, and any other type rejects deletion with 403 `MISSING_PERMISSIONS`
@@ -133,7 +133,7 @@ A reply reference resolves into `referenced_message`, and a forward has none. Fl
<sup>2</sup> Setting this flag binds the message to the voice message contract described under [Create message](#create-message)
These three bits are also the complete sendable set. A create masks the supplied value down to them, and a modify replaces only them and leaves every other stored bit untouched. A bit outside this table is discarded, and the request still succeeds.
These bits are also the complete sendable set. A create masks the supplied value down to them, and a modify replaces only them and leaves every other stored bit untouched. A bit outside this table is discarded, and the request still succeeds.
## Message attachment object
@@ -486,7 +486,7 @@ An allowed mentions object decides which mentions written in the message text be
<sup>1</sup> Defaults to every category when the object is absent, to the empty set when `users` or `roles` is supplied without it, and to the empty set when the object is present with none of its four fields
<sup>2</sup> A non-empty `parse` combined with a non-empty `users` or `roles` is rejected with 400 `PARSE_AND_USERS_OR_ROLES_CANNOT_BE_USED_TOGETHER`
<sup>2</sup> A non-empty `parse` combined with a non-empty `users` or `roles` is rejected with the field code `PARSE_AND_USERS_OR_ROLES_CANNOT_BE_USED_TOGETHER`
<sup>3</sup> Ignored in a one-to-one direct message and in personal notes, and ignored when the referenced message has the same author as the new message
@@ -529,7 +529,7 @@ A new message can refer to a previously uploaded attachment or attach a direct m
<sup>2</sup> The presence of this key is the discriminator that selects this variant over the [direct multipart metadata](#direct-multipart-attachment-metadata-object) variant
<sup>3</sup> Both are required when the message has the `VOICE_MESSAGE` flag. A `waveform` on an attachment whose resolved media type is not `audio/*` fails with 400 `VOICE_MESSAGES_ATTACHMENT_MUST_BE_AUDIO`
<sup>3</sup> Both are required when the message has the `VOICE_MESSAGE` flag. A `waveform` on an attachment whose resolved media type is not `audio/*` fails with the field code `VOICE_MESSAGES_ATTACHMENT_MUST_BE_AUDIO`
Only `IS_SPOILER` and `CONTAINS_EXPLICIT_MEDIA` survive from a caller-supplied `flags` value. `IS_ANIMATED` is set by media inspection alone.
@@ -548,7 +548,7 @@ Only `IS_SPOILER` and `CONTAINS_EXPLICIT_MEDIA` survive from a caller-supplied `
| duration?<sup>2</sup> | ?integer | Voice media duration in seconds (0-2,147,483,647) |
| waveform?<sup>2</sup> | ?string | Base64 waveform of 1 through 4,096 characters |
<sup>1</sup> An entry whose `id` matches no supplied file index and that has a `filename` fails with 400 `NO_FILE_FOR_ATTACHMENT_METADATA`. An entry whose `id` matches no supplied file and that has no `filename` is read as an [existing attachment reference](#existing-attachment-reference-object) instead
<sup>1</sup> An entry whose `id` matches no supplied file index and that has a `filename` fails with the field code `NO_FILE_FOR_ATTACHMENT_METADATA`. An entry whose `id` matches no supplied file and that has no `filename` is read as an [existing attachment reference](#existing-attachment-reference-object) instead
<sup>2</sup> Both are required when the message has the `VOICE_MESSAGE` flag
@@ -570,11 +570,11 @@ Fluxer discards a key outside the table above and accepts the rest of the entry.
<sup>1</sup> A retained attachment is rebuilt from its stored record, and only `title` and `description` are written back onto it
<sup>2</sup> Rejected with 400 `CANNOT_EDIT_ATTACHMENT_METADATA` when the caller is a guild moderator editing a message they did not author, because that caller can supply only `id`, `title`, `description`, and `flags`
<sup>2</sup> Rejected with the field code `CANNOT_EDIT_ATTACHMENT_METADATA` when the caller is a guild moderator editing a message they did not author, because that caller can supply only `id`, `title`, `description`, and `flags`
## Rich embed input objects
A rich embed input is an embed a client supplies on a message, and Fluxer always stores it with the `rich` [embed type](#embed-types). The number of embeds on one message is bounded by the resolved `max_embeds_per_message` limit, which defaults to 10, and exceeding it fails with 400 `TOO_MANY_EMBEDS`.
A rich embed input is an embed a client supplies on a message, and Fluxer always stores it with the `rich` [embed type](#embed-types). The number of embeds on one message is bounded by the resolved `max_embeds_per_message` limit, which defaults to 10, and exceeding it fails with the field code `TOO_MANY_EMBEDS`.
### Rich embed object
@@ -616,7 +616,7 @@ A rich embed input is an embed a client supplies on a message, and Fluxer always
<sup>1</sup> Either an `http` or `https` URL, or an `attachment://<filename>` reference naming a `png`, `jpg`, `jpeg`, `webp`, or `gif` attachment on the same message, and from 1 through 2,048 characters in either form. Only `image` and `thumbnail` accept the `attachment://` form
An `attachment://` reference on a request that has no attachment fails with 400 `CANNOT_REFERENCE_ATTACHMENTS_WITHOUT_ATTACHMENTS`. A reference to a filename that no attachment on the request supplies fails with 400 `REFERENCED_ATTACHMENT_NOT_FOUND`, and a reference to an attachment whose extension is not a supported image extension fails with 400 `ATTACHMENT_MUST_BE_IMAGE`.
An `attachment://` reference on a request that has no attachment fails with the field code `CANNOT_REFERENCE_ATTACHMENTS_WITHOUT_ATTACHMENTS`. A reference to a filename that no attachment on the request supplies fails with the field code `REFERENCED_ATTACHMENT_NOT_FOUND`, and a reference to an attachment whose extension is not a supported image extension fails with the field code `ATTACHMENT_MUST_BE_IMAGE`.
### Embed footer input object
@@ -1008,7 +1008,7 @@ Fluxer aborts the multipart upload and discards its parts when no part was uploa
:::
:::caution[An unowned upload is reported as invalid input]
Fluxer reports 400 `UPLOADED_ATTACHMENT_NOT_FOUND` for an upload key the identity does not own, one that belongs to another channel, one planned as singlepart, and one a message already consumed. A caller cannot tell the four apart, so it cannot probe another identity's upload state.
Fluxer reports the field code `UPLOADED_ATTACHMENT_NOT_FOUND` for an upload key the identity does not own, one that belongs to another channel, one planned as singlepart, and one a message already consumed. A caller cannot tell them apart, so it cannot probe another identity's upload state.
:::
### Side effects
@@ -1034,9 +1034,9 @@ Creates a message from a JSON body or from multipart form data. Returns the crea
- Slowmode applies to a non-bot caller unless they hold [BYPASS_SLOWMODE](/http-api/permissions/).
- A one-to-one direct message additionally applies the recipient's direct message policy and relationship state, and a denial returns 400 `CANNOT_SEND_MESSAGES_TO_USER`.
- An unclaimed account can send only to its own personal notes channel, and a send to any other channel is refused with 400 `UNCLAIMED_ACCOUNT_CANNOT_SEND_MESSAGES` before the channel is resolved.
- A non-bot caller must have started a session, and one that has not is refused with 400 `MUST_START_SESSION_BEFORE_SENDING`.
- A non-bot caller must have started a session, and one that has not is refused with the field code `MUST_START_SESSION_BEFORE_SENDING`.
The request body is read as a multipart form when `Content-Type` contains `multipart/form-data`, and as JSON otherwise. An empty JSON body is read as `{}`. A JSON body that is neither valid JSON nor valid against the schema fails with 400 `INVALID_MESSAGE_DATA` on the path `message_data`, which has no per-field detail. A multipart body whose merged payload fails the same schema instead fails with a per-field 400 `INVALID_FORM_BODY` naming each offending path.
The request body is read as a multipart form when `Content-Type` contains `multipart/form-data`, and as JSON otherwise. An empty JSON body is read as `{}`. A JSON body that is neither valid JSON nor valid against the schema fails with the field code `INVALID_MESSAGE_DATA` on the path `message_data`, which has no per-field detail. A multipart body whose merged payload fails the same schema instead fails with a per-field 400 `INVALID_FORM_BODY` naming each offending path.
### Path parameters
@@ -1069,9 +1069,9 @@ The request body is read as a multipart form when `Content-Type` contains `multi
<sup>5</sup> Fluxer forces the value to false when the caller lacks [SEND_TTS_MESSAGES](/http-api/permissions/) in a guild, and the request still succeeds
<sup>6</sup> Bounded by the resolved `max_embeds_per_message` limit, which defaults to 10. Exceeding it fails with 400 `TOO_MANY_EMBEDS` on the path `embeds`
<sup>6</sup> Bounded by the resolved `max_embeds_per_message` limit, which defaults to 10. Exceeding it fails with the field code `TOO_MANY_EMBEDS` on the path `embeds`
<sup>7</sup> Bounded by the resolved `max_attachments_per_message` limit, which defaults to 10. Exceeding it fails with 400 `TOO_MANY_FILES` on the path `attachments`
<sup>7</sup> Bounded by the resolved `max_attachments_per_message` limit, which defaults to 10. Exceeding it fails with the field code `TOO_MANY_FILES` on the path `attachments`
The smallest body that works is one line of text:
@@ -1091,11 +1091,11 @@ A nonce is remembered for the authenticated identity for five minutes, so a clie
Reusing a nonce in the same channel inside that window returns the message the first request created. Reusing it in a different channel fails with 404 `UNKNOWN_MESSAGE`. The replay check runs after authorisation and body validation, so a retry that is otherwise invalid still fails, and a retry after the window has passed creates a second message.
A reply applies the caller's message history cutoff to its target, and a forward applies none to its source. A reply whose target is a system message fails with 400 `CANNOT_REPLY_TO_SYSTEM_MESSAGE`, and a reference that resolves to no message fails with 404 `UNKNOWN_MESSAGE`.
A reply applies the caller's message history cutoff to its target, and a forward applies none to its source. A reply whose target is a system message fails with the field code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`, and a reference that resolves to no message fails with 404 `UNKNOWN_MESSAGE`.
A `FORWARD` reference has `channel_id`. One that does not is refused by body validation before the operation runs, with 400 `INVALID_MESSAGE_DATA` on a JSON body and a per-field 400 `INVALID_FORM_BODY` on a multipart body.
A forward request has no `content`, `embeds`, `attachments`, or `sticker_ids`, and one that does fails with 400 `FORWARD_MESSAGES_CANNOT_CONTAIN_CONTENT`. A `guild_id` that disagrees with the source channel's guild fails with 400 `GUILD_ID_MUST_MATCH_REFERENCED_MESSAGE`. Reading the source channel requires [VIEW_CHANNEL](/http-api/permissions/) when that channel belongs to a guild.
A forward request has no `content`, `embeds`, `attachments`, or `sticker_ids`, and one that does fails with the field code `FORWARD_MESSAGES_CANNOT_CONTAIN_CONTENT`. A `guild_id` that disagrees with the source channel's guild fails with the field code `GUILD_ID_MUST_MATCH_REFERENCED_MESSAGE`. Reading the source channel requires [VIEW_CHANNEL](/http-api/permissions/) when that channel belongs to a guild.
Snapshots with embeds require `EMBED_LINKS` in the destination guild channel, and snapshots with attachments require `ATTACH_FILES`, each returning 403 `MISSING_PERMISSIONS` when absent.
@@ -1116,24 +1116,37 @@ Snapshots with embeds require `EMBED_LINKS` in the destination guild channel, an
<sup>2</sup> The index runs from 0 through the resolved `max_attachments_per_message` limit minus one
A body that cannot be parsed as a multipart form fails with 400 `FAILED_TO_PARSE_MULTIPART_FORM_DATA`, and a `payload_json` field that is not a JSON string fails with 400 `INVALID_JSON_IN_PAYLOAD_JSON`. A field name beginning with `files[` that does not match `files[<N>]` fails with 400 `INVALID_FILE_FIELD_NAME`.
A body that cannot be parsed as a multipart form fails with the field code `FAILED_TO_PARSE_MULTIPART_FORM_DATA`, and a `payload_json` field that is not a JSON string fails with the field code `INVALID_JSON_IN_PAYLOAD_JSON`. A field name beginning with `files[` that does not match `files[<N>]` fails with the field code `INVALID_FILE_FIELD_NAME`.
| File field condition | Error |
| --- | --- |
| Index below 0 or above 10000 | 400 `FILE_INDEX_EXCEEDS_MAXIMUM` reporting a `maxIndex` of 10000 |
| Resolved attachment limit is 0 | 400 `ATTACHMENTS_NOT_ALLOWED_FOR_MESSAGE` |
| Index at or above a non-zero resolved limit | 400 `FILE_INDEX_EXCEEDS_MAXIMUM` reporting that limit minus one |
| Index repeated across fields | 400 `DUPLICATE_FILE_INDEX` |
| More than one file under one index | 400 `MULTIPLE_FILES_FOR_INDEX_NOT_ALLOWED` |
| More files than the resolved limit | 400 `TOO_MANY_FILES` |
| Index below 0 or above 10000 | `FILE_INDEX_EXCEEDS_MAXIMUM` |
| Resolved attachment limit is 0 | `ATTACHMENTS_NOT_ALLOWED_FOR_MESSAGE` |
| Index at or above a non-zero resolved limit | `FILE_INDEX_EXCEEDS_MAXIMUM` |
| Index repeated across fields | `DUPLICATE_FILE_INDEX` |
| More than one file under one index | `MULTIPLE_FILES_FOR_INDEX_NOT_ALLOWED` |
| More files than the resolved limit | `TOO_MANY_FILES` |
The `attachments` array inside `payload_json` uses [direct multipart attachment metadata](#direct-multipart-attachment-metadata-object), and an entry with `upload_filename` is instead read as a [pre-uploaded attachment](#pre-uploaded-attachment-object). An entry whose `id` matches a supplied file index supplies that file's metadata, and two entries claiming the same file fail with 400 `DUPLICATE_ATTACHMENT_IDS_NOT_ALLOWED`.
Each is a field code inside a 400 `INVALID_FORM_BODY` body. A `FILE_INDEX_EXCEEDS_MAXIMUM` element reports a `maxIndex` member, 10000 against the absolute bound and the resolved limit minus one otherwise.
The `attachments` array inside `payload_json` uses [direct multipart attachment metadata](#direct-multipart-attachment-metadata-object), and an entry with `upload_filename` is instead read as a [pre-uploaded attachment](#pre-uploaded-attachment-object). An entry whose `id` matches a supplied file index supplies that file's metadata, and two entries claiming the same file fail with the field code `DUPLICATE_ATTACHMENT_IDS_NOT_ALLOWED`.
Fluxer permits index gaps and attaches the files in ascending index order. A supplied file that no metadata entry claims is attached with its own multipart filename and no title or description, so omitting `attachments` entirely attaches every supplied file that way.
A message with `VOICE_MESSAGE` set has exactly one attachment and nothing else. Each rule fails with its own 400 code.
A message with `VOICE_MESSAGE` set has exactly one attachment and nothing else.
Any count other than one attachment returns `VOICE_MESSAGES_REQUIRE_SINGLE_ATTACHMENT`. A missing `waveform` returns `VOICE_MESSAGES_ATTACHMENT_WAVEFORM_REQUIRED` and a missing `duration` returns `VOICE_MESSAGES_ATTACHMENT_DURATION_REQUIRED`. Content, embeds, stickers, and a favourite meme return `VOICE_MESSAGES_CANNOT_HAVE_CONTENT`, `VOICE_MESSAGES_CANNOT_HAVE_EMBEDS`, `VOICE_MESSAGES_CANNOT_HAVE_STICKERS`, and `VOICE_MESSAGES_CANNOT_HAVE_FAVORITE_MEMES`. A duration above the resolved `max_voice_message_duration` limit, which defaults to 1200 seconds, returns `VOICE_MESSAGES_DURATION_EXCEEDS_LIMIT`.
| Voice message condition | Error |
| --- | --- |
| Content present | `VOICE_MESSAGES_CANNOT_HAVE_CONTENT` |
| Embeds present | `VOICE_MESSAGES_CANNOT_HAVE_EMBEDS` |
| Favourite meme present | `VOICE_MESSAGES_CANNOT_HAVE_FAVORITE_MEMES` |
| Stickers present | `VOICE_MESSAGES_CANNOT_HAVE_STICKERS` |
| Any count other than one attachment | `VOICE_MESSAGES_REQUIRE_SINGLE_ATTACHMENT` |
| Missing `waveform` | `VOICE_MESSAGES_ATTACHMENT_WAVEFORM_REQUIRED` |
| Missing `duration` | `VOICE_MESSAGES_ATTACHMENT_DURATION_REQUIRED` |
| Duration above the resolved limit | `VOICE_MESSAGES_DURATION_EXCEEDS_LIMIT` |
The resolved `max_voice_message_duration` limit defaults to 1200 seconds.
### Response
@@ -1176,7 +1189,7 @@ Modifies a message. Returns the updated [message](#message-object) object. Emits
### Limitations
- The target must be a `DEFAULT` or `REPLY` message, and any other type fails with 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`.
- A message that has [message snapshots](#message-snapshot-object) cannot be edited by its author and fails with 400 `MESSAGES_WITH_SNAPSHOTS_CANNOT_BE_EDITED`. A non-author moderator holding `MANAGE_MESSAGES` can still toggle `SUPPRESS_EMBEDS` and change existing attachment metadata on a forwarded message.
- A message that has [message snapshots](#message-snapshot-object) cannot be edited by its author and fails with the field code `MESSAGES_WITH_SNAPSHOTS_CANNOT_BE_EDITED`. A non-author moderator holding `MANAGE_MESSAGES` can still toggle `SUPPRESS_EMBEDS` and change existing attachment metadata on a forwarded message.
- The author can modify every supported field, and a timed-out author is refused with 403 `COMMUNICATION_DISABLED`.
- A caller who is not the author can act only in a guild channel, must hold [MANAGE_MESSAGES](/http-api/permissions/), and can change only `SUPPRESS_EMBEDS` and the metadata of attachments that already exist. A non-author edit that does not satisfy all three conditions fails with 403 `CANNOT_EDIT_OTHER_USER_MESSAGE`.
- `MANAGE_MESSAGES` is an [elevated permission](/http-api/permissions/#elevated-permissions), so a caller holding the bit without an enrolled authenticator receives 400 [`TWO_FACTOR_REQUIRED`](/http-api/errors/) in a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated, unless they own the guild. A caller who does not hold the bit at all is treated as any other non-author and receives 403 `CANNOT_EDIT_OTHER_USER_MESSAGE`.
@@ -1382,14 +1395,14 @@ Deletes from 1 through 100 messages in one guild channel. Returns 204 with an em
### JSON body
At least one of the two fields below must be present. Supplying neither fails with 400 `INVALID_FORM_BODY` on the path `message_ids`, and supplying both uses `message_ids` and ignores `messages`.
At least one of the fields below must be present. Supplying neither fails with 400 `INVALID_FORM_BODY` on the path `message_ids`, and supplying both uses `message_ids` and ignores `messages`.
| Field | Type | Description |
| --- | --- | --- |
| message_ids?<sup>1</sup> | array[snowflake] | Message IDs to delete, at most 100 entries |
| messages?<sup>1</sup> | array[snowflake] | Alias for `message_ids` |
<sup>1</sup> An empty array fails with 400 `MESSAGE_IDS_CANNOT_BE_EMPTY` on the path `message_ids`. More than 100 entries is rejected by body validation with 400 `INVALID_FORM_BODY` before the operation runs
<sup>1</sup> An empty array fails with the field code `MESSAGE_IDS_CANNOT_BE_EMPTY` on the path `message_ids`. More than 100 entries is rejected by body validation with 400 `INVALID_FORM_BODY` before the operation runs
:::danger[Bulk deletion removes attachments and search entries]
Every selected message, with its reactions and attachments, is permanently deleted and removed from search.
@@ -1458,7 +1471,7 @@ The operation deletes every message it finds, with no age boundary and no confir
The operation permanently deletes every personal note, its attachments, and its reactions and removes each from search. The caller's sessions receive one [Message Delete Bulk](/gateway/events/#message-delete-bulk) Dispatch for each page of at most 100 messages.
The operation emits no [Channel Pins Update](/gateway/events/#channel-pins-update) and no individual [Message Delete](/gateway/events/#message-delete), and it writes no audit log entry.
It emits no [Channel Pins Update](/gateway/events/#channel-pins-update) and no individual [Message Delete](/gateway/events/#message-delete), and it writes no audit log entry.
### Rate limit
@@ -1503,9 +1516,9 @@ The deletion runs inside the request despite the 202 status, so every matching m
| webauthn_response? | [WebAuthn assertion](/http-api/authentication/#webauthn-assertion) object | WebAuthn assertion for sudo verification |
| webauthn_challenge? | string | Challenge bound to the sudo mode assertion |
<sup>1</sup> Accepted only when the account has no enrolled authenticator, and an incorrect password fails with 400 `INVALID_PASSWORD`. An account with neither a stored password nor an enrolled authenticator never reaches this check
<sup>1</sup> Accepted only when the account has no enrolled authenticator, and an incorrect password fails with the field code `INVALID_PASSWORD`. An account with neither a stored password nor an enrolled authenticator never reaches this check
<sup>2</sup> Accepted only when the account has an enrolled authenticator. A failed verification returns 400 `INVALID_MFA_CODE`
<sup>2</sup> Accepted only when the account has an enrolled authenticator. A failed verification returns the field code `INVALID_MFA_CODE`
:::danger[Every message the caller authored is destroyed]
The deletion has no age boundary and no selection. It permanently removes every message the caller has authored in the channel, together with its attachments and reactions, and removes each from search. There is no cancellation and no restore.
@@ -1699,7 +1712,7 @@ When the Dispatch fails, the request returns 502, 503, or 504 after the read-sta
The `emoji` path value is URI-encoded and is 1 through 64 characters. A custom emoji uses `name:id`, where `id` is the trailing decimal custom emoji snowflake and `name` is everything before the final colon. Any other decoded value is read as a Unicode emoji.
Fluxer validates the emoji in full only when a request would create a new reaction group on the message. At that point a Unicode value must be exactly one valid emoji, and one that is not fails with 400 `NOT_A_VALID_UNICODE_EMOJI`. A custom emoji must exist, and one that does not fails with 400 `CUSTOM_EMOJI_NOT_FOUND`. Every other reaction operation parses the value without validating it, so removing a reaction with a nonsensical emoji succeeds silently.
Fluxer validates the emoji in full only when a request would create a new reaction group on the message. At that point a Unicode value must be exactly one valid emoji, and one that is not fails with the field code `NOT_A_VALID_UNICODE_EMOJI`. A custom emoji must exist, and one that does not fails with the field code `CUSTOM_EMOJI_NOT_FOUND`. Every other reaction operation parses the value without validating it, so removing a reaction with a nonsensical emoji succeeds silently.
## List reaction users unpaged
@@ -1795,9 +1808,9 @@ Adds the authenticated identity's ordinary reaction. Returns 204 with an empty b
- The caller must be able to view the text-bearing channel, must not be timed out, and must reach the message under the message history cutoff.
- A non-bot caller must have a verified email, and one that does not receives 403 `REACTION_EMAIL_VERIFICATION_REQUIRED`.
- Creating a new emoji reaction group in a guild requires [ADD_REACTIONS](/http-api/permissions/), while adding to an existing group does not.
- A custom emoji whose source guild is not the channel's guild requires the `feature_global_expressions` entitlement, and a caller without it receives 400 `CUSTOM_EMOJIS_REQUIRE_PREMIUM_OUTSIDE_SOURCE`.
- A custom emoji whose source guild is not the channel's guild requires the `feature_global_expressions` entitlement, and a caller without it receives the field code `CUSTOM_EMOJIS_REQUIRE_PREMIUM_OUTSIDE_SOURCE`.
- In a guild channel a custom emoji additionally requires [USE_EXTERNAL_EMOJIS](/http-api/permissions/).
- A non-bot caller must have started a session, and one that has not receives 400 `MUST_START_SESSION_BEFORE_SENDING`.
- A non-bot caller must have started a session, and one that has not receives the field code `MUST_START_SESSION_BEFORE_SENDING`.
- An unclaimed account can react only in its personal notes channel, and elsewhere receives 400 `UNCLAIMED_ACCOUNT_CANNOT_ADD_REACTIONS`.
- The operation is idempotent, and this route applies no direct message send policy.
@@ -58,7 +58,7 @@ An unrecognised scope value is rejected when the authorisation request is comple
Scope order has no semantic meaning, so a client MUST compare scopes as a set.
:::
The `scope` string of an [OAuth2 token](#oauth2-token-object) and of an [OAuth2 introspection](#oauth2-introspection-object) is sorted into the registry order above. The `scopes` array of a [current OAuth2 authorisation](#current-oauth2-authorisation-object) has the stored scopes in no defined order, with unrecognised values removed. The `scopes` array of an [OAuth2 authorisation](#oauth2-authorisation-object) is in no defined order, and it can hold `bot` and an unrecognised value once more than one refresh token contributes.
Fluxer sorts the `scope` string of an [OAuth2 token](#oauth2-token-object) and of an [OAuth2 introspection](#oauth2-introspection-object) into the registry order above. The `scopes` array of a [current OAuth2 authorisation](#current-oauth2-authorisation-object) has the stored scopes in no defined order, with unrecognised values removed. The `scopes` array of an [OAuth2 authorisation](#oauth2-authorisation-object) is in no defined order, and it can hold `bot` and an unrecognised value once more than one refresh token contributes.
## OAuth2 error response object
@@ -73,7 +73,7 @@ The failure envelope the OAuth2 routes return.
<sup>1</sup> Always present, and some descriptions are a bare API error code identifier
These two members are the whole body, so the routine that reads the ordinary [error response](/http-api/#error-response) cannot parse it. Fluxer returns every OAuth2 failure with status 400. A client MUST key on `error` and never on `error_description`.
These members are the whole body, so the routine that reads the ordinary [error response](/http-api/#error-response) cannot parse it. Fluxer returns every OAuth2 failure with status 400. A client MUST key on `error` and never on `error_description`.
The consent, token, introspection, and revocation routes return this object for a grant, client authentication, scope, redirect, response type, or permission mask failure. Every other failure, including request validation, authentication, authorisation, and rate limiting, uses the ordinary [error response](/http-api/#error-response) with an [API error code](/http-api/errors/).
@@ -319,7 +319,7 @@ Fluxer builds the URL from the registered redirect URI the request resolved to,
<RouteHeader method="GET" path="/v1/oauth2/authorize" unauthenticated />
Starts an authorisation request. A query that passes request validation always answers with a redirect, and authentication is optional.
Starts an authorisation request. Fluxer returns a redirect for every query that passes request validation. Authentication is optional.
### Query parameters
@@ -427,11 +427,11 @@ A guild installation emits [Guild Create](/gateway/events/#guild-create), [Guild
Consent issues one authorisation code bound to the application, user, resolved redirect URI, granted scopes, and PKCE challenge, and returns the callback URL that has it.
With the `bot` scope and a guild target, consent adds the bot to the guild with the bot-invite join source and the caller recorded as inviter, bypassing both the bot account's own guild count limit and the guild ban list. The guild member limit still applies. When a non-zero permission mask is requested, it also creates one role with exactly those permissions, names the role after the application, and assigns it to the bot.
With the `bot` scope and a guild target, consent adds the bot to the guild with the bot-invite join source and the caller recorded as inviter. Consent bypasses both the bot account's own guild count limit and the guild ban list. The guild member limit still applies. When a non-zero permission mask is requested, consent also creates one role with exactly those permissions, names the role after the application, and assigns it to the bot.
That role is created at position 1 with no colour and is neither hoisted nor mentionable. It consumes a guild role slot and is not removed when the bot leaves. The installation records bot add, role create, and member role update [audit log](/http-api/guild-audit-logs/) entries.
With the `bot` scope and a group direct message target, it adds the bot as a recipient and stores the recipient-add system message.
With the `bot` scope and a group direct message target, consent adds the bot as a recipient and stores the recipient-add system message.
When any part of the installation fails, Fluxer cancels the newly issued authorisation code, so the caller never receives a usable code.
@@ -10,7 +10,7 @@ A permission grants a member one ability in a guild, such as sending a message o
Only [List guild roles](#list-guild-roles) accepts an OAuth2 bearer credential, and every other route here rejects one with 403 `ACCESS_DENIED`. A guild with [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features) rejects an authenticated request with 403 `MISSING_ACCESS`, and [UNAVAILABLE_FOR_EVERYONE_BUT_STAFF](/http-api/guilds/#guild-features) does the same for an account without the instance staff flag.
Fluxer resolves the guild before any role. A guild that does not exist returns 404 `UNKNOWN_GUILD`. A caller who is not a member of an existing guild returns 403 `MISSING_PERMISSIONS`, so guild existence is visible to any authenticated caller. A route that names a role asserts `MANAGE_ROLES` before it reads the role. A caller without the permission receives 403 `MISSING_PERMISSIONS` even for a role that does not exist.
Fluxer resolves the guild before any role. A guild that does not exist returns 404 `UNKNOWN_GUILD`. When the guild exists and the caller is not a member, the request returns 403 `MISSING_PERMISSIONS`, so guild existence is visible to any authenticated caller. Every route that names a role asserts `MANAGE_ROLES` before it reads the role. A caller without the permission receives 403 `MISSING_PERMISSIONS` even for a role that does not exist.
## Permissions
@@ -86,7 +86,7 @@ Fluxer discards a bit it does not define. A role mutation intersects the request
### Feature-gated permission bits
Fluxer gates some bits on a client feature declaration. A client sends the feature set in the `X-Fluxer-Features` request header as a comma-separated list.
Fluxer gates some permission bits on a feature declaration, which a client sends in the `X-Fluxer-Features` request header as a comma-separated list.
One bit is gated today. `VIEW_CHANNEL_MEMBERS` requires the feature name `view_channel_members_permission`.
@@ -94,7 +94,7 @@ On a role or overwrite write that does not declare the matching feature, Fluxer
Fluxer trims and lowercases each name in the header before matching it. A name that is empty, longer than 64 characters, or containing a character outside `a-z`, `0-9`, and `_` is ignored. Fluxer reads at most 64 names from one header. An absent or empty header declares no features.
A [Create guild role](#create-guild-role) request that supplies `permissions` without declaring the feature always creates the role with `VIEW_CHANNEL_MEMBERS` cleared. A request that omits `permissions` copies the everyone role mask without applying the gate.
On [Create guild role](#create-guild-role), a request that supplies `permissions` without declaring the feature always creates the role with `VIEW_CHANNEL_MEMBERS` cleared. A request that omits `permissions` copies the everyone role mask without applying the gate.
## Elevated permissions
@@ -116,13 +116,13 @@ The guild owner receives the complete 64-bit mask. A user who is not a member re
Otherwise Fluxer starts from the permissions of the everyone role, whose [snowflake](/snowflakes/) equals the guild snowflake, then adds the permissions of every role assigned to the member with a bitwise union. If the result contains `ADMINISTRATOR` at that point, Fluxer returns the complete 64-bit mask and runs no further step.
When no channel was named, the union is the final result. When a channel was named, Fluxer applies that channel's stored [permission overwrites](/http-api/channels/#permission-overwrite-object) to the union in three steps:
When no channel was named, the union is the final result. When a channel was named, Fluxer applies that channel's stored [permission overwrites](/http-api/channels/#permission-overwrite-object) to the union in this order:
1. The everyone role overwrite, by removing its denied bits and then adding its allowed bits.
2. The overwrites targeting roles assigned to the member, accumulated into one union of allowed bits and one union of denied bits and applied as a single step in the same order. An allow on any one of the member's roles defeats a deny on another.
3. The member overwrite, again denied bits first and allowed bits second.
A named channel that does not exist in the guild leaves the guild-level union unchanged.
When the named channel does not exist in the guild, Fluxer leaves the guild-level union unchanged.
:::caution[`ADMINISTRATOR` grants every bit and skips overwrites]
No deny overwrite in any channel restricts a member who holds the bit. Removing `ADMINISTRATOR` from every role they hold is the only remedy.
@@ -134,7 +134,16 @@ A guild channel is visible when its channel-scoped mask contains `VIEW_CHANNEL`,
A communication timeout stops a member from communicating in a guild until it expires. It does not change the computed mask. A member whose [communication_disabled_until](/http-api/guild-members/#guild-member-object) is in the future keeps every permission their roles grant, and Fluxer applies the restriction as a separate precondition on each communicating operation.
Sending a message, editing the caller's own message, adding a reaction, requesting an attachment upload, starting a typing indicator, and changing the caller's own nickname each fail with 403 `COMMUNICATION_DISABLED` while the timeout is in the future. A timeout has no effect in a private channel.
While the timeout is in the future, these operations fail with 403 `COMMUNICATION_DISABLED`:
- Sending a message
- Editing the caller's own message
- Adding a reaction
- Requesting an attachment upload
- Starting a typing indicator
- Changing the caller's own nickname
A timeout has no effect in a private channel.
The order of the timeout precondition and the permission check varies by operation, so a caller who is both timed out and short of the permission can receive either `COMMUNICATION_DISABLED` or `MISSING_PERMISSIONS`.
@@ -30,7 +30,7 @@ An account holds at most one read state per channel, keyed by the account and th
<sup>3</sup> Present on every entry the API returns. The value is always `0`
An entry is created by an acknowledgement, by [Acknowledge pins](/http-api/messages/#acknowledge-pins), and by the server when a mention arrives in a channel the account has no entry for. A pin acknowledgement writes only `last_pin_timestamp`, and the entry it creates reports `last_message_id` as null. An entry the server creates for a mention starts from the channel's own baseline watermark, which is the snowflake of the channel ID itself, so every message already in the channel stays unread.
An acknowledgement creates an entry. So does [Acknowledge pins](/http-api/messages/#acknowledge-pins), and so does the server when a mention arrives in a channel the account has no entry for. A pin acknowledgement writes only `last_pin_timestamp`, and the entry it creates reports `last_message_id` as null. The entry the server creates for a mention starts from the channel's own baseline watermark, which is the snowflake of the channel ID itself, so every message already in the channel stays unread.
[Clear channel read state](/http-api/messages/#clear-channel-read-state) deletes the whole entry, so it drops the watermark and the mention count along with the pin timestamp.
@@ -38,7 +38,7 @@ An entry is created by an acknowledgement, by [Acknowledge pins](/http-api/messa
There is no HTTP read for the read state aggregate. The complete entry array arrives once as `read_states` in the main Gateway [Ready](/gateway/events/#ready) payload.
:::
Only an acknowledgement produces a later Dispatch. A message acknowledgement emits [Message ACK](/gateway/events/#message-ack), and a pin acknowledgement emits [Channel Pins ACK](/gateway/events/#channel-pins-ack). A server-side mention increment and [Clear channel read state](/http-api/messages/#clear-channel-read-state) both change the stored entry and emit nothing, so the Dispatch stream is not a complete change feed. A client that needs the authoritative aggregate reconciles from a new Ready, or from the entries [Acknowledge read states](#acknowledge-read-states) returns.
Only an acknowledgement produces a later Dispatch. A message acknowledgement emits [Message ACK](/gateway/events/#message-ack), and a pin acknowledgement emits [Channel Pins ACK](/gateway/events/#channel-pins-ack). Both a server-side mention increment and [Clear channel read state](/http-api/messages/#clear-channel-read-state) change the stored entry and emit nothing, so the Dispatch stream is not a complete change feed. A client that needs the authoritative aggregate reconciles from a new Ready, or from the entries [Acknowledge read states](#acknowledge-read-states) returns.
### Example
@@ -256,7 +256,14 @@ The snapshot also records a conversation window of up to 25 messages before the
10 requests per hour, on the `report:create` bucket, which is shared with [Report user](#report-user) and [Report guild](#report-guild).
Four further allowances apply, each counted over one hour. One reporter creates at most 5 reports across all report types and at most 3 message reports in one channel, and one message receives at most 20 reports across all reporters. When the reported message is in a guild, one reporter creates at most 4 message reports in that guild. A duplicate submission returns 409 `CONFLICT` before any of the four is consumed.
Further allowances apply, each counted over one hour:
- One reporter creates at most 5 reports across all report types.
- One reporter creates at most 3 message reports in one channel.
- One message receives at most 20 reports across all reporters.
- When the reported message is in a guild, one reporter creates at most 4 message reports in that guild.
A duplicate submission returns 409 `CONFLICT` before any of them is consumed.
## Report user
@@ -315,7 +322,13 @@ Creates and returns a [report object](#report-object) for a guild.
- A `guild_id` that resolves to no guild returns 404 `UNKNOWN_GUILD`.
- The guild owner reporting their own guild returns 400 `CANNOT_REPORT_OWN_GUILD`.
Fluxer establishes access to the guild in one of three ways, tried in that order. A current member needs nothing further. Any reporter can report a guild with the `DISCOVERABLE` [guild feature](/http-api/guilds/#guild-features). Any other reporter supplies `invite_code` for an invite pointing at that exact guild. A reporter satisfying none of the three returns 403 `CANNOT_REPORT_GUILD`.
Fluxer establishes access to the guild in one of these ways, tried in order:
1. A current member needs nothing further.
2. Any reporter can report a guild with the `DISCOVERABLE` [guild feature](/http-api/guilds/#guild-features).
3. Any other reporter supplies `invite_code` for an invite pointing at that exact guild.
A reporter satisfying none of them returns 403 `CANNOT_REPORT_GUILD`.
### JSON body
@@ -34,7 +34,7 @@ A key with a different segment count, a non-numeric channel segment, an empty co
[Modify stream region](#modify-stream-region), [Get stream preview](#get-stream-preview), and [Delete stream preview](#delete-stream-preview) resolve the channel from the channel segment of the key. [Upload stream preview](#upload-stream-preview) and [Create stream preview upload URL](#create-stream-preview-upload-url) instead resolve it from the `channel_id` in their request body and compare that value against the key afterwards.
The scope segment is checked against the resolved channel. A guild-scoped key whose channel turns out to be private, a `dm` key whose channel turns out to belong to a guild, and a guild-scoped key whose guild segment is not the resolved channel's guild each return 400 `STREAM_KEY_SCOPE_MISMATCH`.
Fluxer checks the scope segment against the resolved channel. A guild-scoped key whose resolved channel is private returns 400 `STREAM_KEY_SCOPE_MISMATCH`, and so does a `dm` key whose resolved channel belongs to a guild. A guild-scoped key whose guild segment is not the resolved channel's guild returns the same code.
The route then compares the channel segment against the resolved channel ID, and a mismatch returns 400 `STREAM_KEY_CHANNEL_MISMATCH`. On the two routes that have a body `channel_id`, a caller MUST send the same value in the body and in the key.
@@ -46,13 +46,13 @@ Two boundaries apply across this page.
Read access matches [Get channel](/http-api/channels/#get-channel) for the [resolved channel](#stream-key). A guild channel additionally requires the [CONNECT](/http-api/permissions/) permission. The caller does not have to own the stream, so any member who can join the channel can read its preview.
Mutation access adds two requirements to read access. A guild channel also requires the [STREAM](/http-api/permissions/) permission. The caller must hold a voice state in exactly the resolved channel and the connection identifier the key names. Fluxer rejects a caller that holds no such voice state with 403 `ACCESS_DENIED`, even when it owns the channel.
Mutation access requires everything read access does. A guild channel also requires the [STREAM](/http-api/permissions/) permission. The caller must hold a voice state in exactly the resolved channel and the connection identifier the key names. Fluxer rejects a caller that holds no such voice state with 403 `ACCESS_DENIED`, even when it owns the channel.
:::caution[A preview can exist with no live stream]
A caller can record a region and upload a preview for any voice connection it holds, whether or not it is streaming.
:::
A guild channel resolves before its guild does. A channel row that outlives its guild returns 404 `UNKNOWN_GUILD`. A guild record that exists but whose membership state cannot be resolved returns 403 `ACCESS_DENIED`, and a caller that is not a member or that lacks [VIEW_CHANNEL](/http-api/permissions/) returns 403 `MISSING_PERMISSIONS`. An unsatisfied age restriction on a guild text, voice, or link channel returns 403 `NSFW_CONTENT_AGE_RESTRICTED`.
A guild channel resolves before its guild does. A channel row that outlives its guild returns 404 `UNKNOWN_GUILD`. Where the guild record exists and Fluxer cannot resolve the membership state, the route returns 403 `ACCESS_DENIED`, and a caller that is not a member or that lacks [VIEW_CHANNEL](/http-api/permissions/) returns 403 `MISSING_PERMISSIONS`. An unsatisfied age restriction on a guild text, voice, or link channel returns 403 `NSFW_CONTENT_AGE_RESTRICTED`.
A private channel whose recipient set does not contain the caller returns 404 `UNKNOWN_CHANNEL`. An absent channel returns the same code, so recipient membership is never distinguishable from private channel existence.
@@ -204,7 +204,7 @@ When the image should travel out of band, use [Create stream preview upload URL]
<sup>3</sup> A value containing `jpeg` or `jpg` in any case is accepted without inspecting the bytes. Any other value, and an absent field, is accepted only when the decoded bytes begin with `FF D8` and end with `FF D9`
Fluxer stores the accepted `content_type` verbatim and returns it from [Get stream preview](#get-stream-preview). An absent field stores the preview as `image/jpeg`.
Fluxer stores the accepted `content_type` verbatim and returns it from [Get stream preview](#get-stream-preview). When the field is absent, the stored media type is `image/jpeg`.
A `thumbnail` that is not canonical base64 returns 400 `INVALID_STREAM_THUMBNAIL_PAYLOAD`. Fluxer re-encodes the decoded bytes and rejects the value when the result is not what the client sent. The 2000000 character ceiling admits 1500000 decoded bytes, so an oversized JPEG reaches the byte check and returns `FILE_SIZE_TOO_LARGE`. The format check runs before the size check. An oversized payload that is not a JPEG returns `PREVIEW_MUST_BE_JPEG`.
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
A theme is a custom CSS stylesheet stored under a short code, and anyone holding the code can fetch it. The HTTP API exposes one operation, which stores a theme. The [Media Proxy](/media-proxy/overview/) serves the stored stylesheet back through [Get theme CSS](/media-proxy/routes/#get-theme-css).
The single route here is user-only. A bot token and an OAuth2 bearer credential are both rejected with 403 `ACCESS_DENIED`.
The route is user-only. A bot token and an OAuth2 bearer credential are both rejected with 403 `ACCESS_DENIED`.
## Theme identifier
@@ -43,11 +43,11 @@ An account holds no theme quota. The route bucket is the only bound on how many
The instance-wide content filter screens the submitted document before the route runs. It checks a `css` value of at least 3 characters against the instance phrase blocklist and the instance URL blocklist. A match on either returns 403 `CONTENT_BLOCKED`. A shorter value is never screened.
Fluxer scans a body under every content type except `multipart/form-data` and `application/x-www-form-urlencoded`. A body that does not parse as JSON is skipped as well. The route parses the body from the raw request text and reads no content type, so a JSON document sent under either form content type is stored without passing the blocklists.
Fluxer scans a body under every content type except `multipart/form-data` and `application/x-www-form-urlencoded`. The filter also skips a body that does not parse as JSON. The route parses the body from the raw request text and reads no content type, so a JSON document sent under either form content type is stored without passing the blocklists.
The screen precedes the rate limit bucket, the credential check, and the request schema, so a blocked document returns `CONTENT_BLOCKED` even when the request has no credential.
The screen is the only inspection the document receives. Fluxer stores the CSS verbatim and never parses, validates, minifies, or rewrites it.
Nothing else inspects the document. Fluxer stores the CSS verbatim and never parses, validates, minifies, or rewrites it.
## Create theme
@@ -93,7 +93,7 @@ The object has the identifier and nothing else.
Fluxer stores the UTF-8 encoding of the submitted document with the content type `text/css; charset=utf-8`. The identifier addresses the document by the time the caller receives it.
Nothing Fluxer stores links a theme to the account that created it. The [Media Proxy](/media-proxy/overview/) then serves the stored document from `/themes/{id}.css`, and that route accepts no credential.
Nothing Fluxer stores links a theme to the account that created it. The [Media Proxy](/media-proxy/overview/) serves the stored document from `/themes/{id}.css`, and that route accepts no credential.
:::danger[Theme CSS is public and permanent]
The identifier is the only thing guarding a stored document, and the read route is unauthenticated. The HTTP API replaces or deletes no stored theme. A caller MUST NOT put a credential, a token, or any other secret in a submitted document.
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
Unfurl resolves one URL into the preview a message would show for it. That preview is an array of [embed](/http-api/messages/#embed-object) objects, the same objects the ordinary message path attaches.
The single route here is user-only. Fluxer rejects a bot token and an OAuth2 bearer credential with 403 `ACCESS_DENIED`.
The route is user-only. Fluxer rejects a bot token and an OAuth2 bearer credential with 403 `ACCESS_DENIED`.
## Site resolvers
@@ -48,7 +48,15 @@ A resolved embed has at most one nested embed in `children`, and a nested embed
## Network policy
Fluxer applies its network policy before every fetch and again to every redirect target. It refuses a URL that is empty, exceeds 8192 characters, contains a control character, uses a scheme other than HTTP or HTTPS, or has user information. Fluxer also refuses a URL whose port is `0` or whose host is not a valid public hostname. A host that resolves to a loopback, private, link-local, or otherwise reserved address is refused, and so is a host that resolves to no address at all.
Fluxer applies its network policy before every fetch and again to every redirect target. It refuses a URL on any of these grounds:
- The URL is empty, exceeds 8192 characters, or contains a control character.
- The scheme is neither HTTP nor HTTPS.
- The URL has user information.
- The port is `0`.
- The host is not a valid public hostname.
- The host resolves to a loopback, private, link-local, or otherwise reserved address.
- The host resolves to no address at all.
Every HTTP request has a timeout of at most 10 seconds. The FxTwitter timeout is 8 seconds, and a Hacker News, Wikipedia, Bluesky, ActivityPub, oEmbed, MediaWiki extract, or media metadata call has 5 seconds. Every fetch identifies itself as `Mozilla/5.0 (compatible; Fluxerbot/1.0; +https://fluxer.app)`.
@@ -75,7 +75,7 @@ A system account never receives the deleted representation. An account only sche
## Reply mention preferences
The account-wide preference applied when another account replies to one of this account's messages. `mention_flags` is exactly one of the three values below, and it is omitted from a serialised user while the stored value is `NO_PREFERENCE`.
The account-wide preference applied when another account replies to one of this account's messages. `mention_flags` is exactly one of the values below, and it is omitted from a serialised user while the stored value is `NO_PREFERENCE`.
| Value | Name | Description |
| --- | --- | --- |
@@ -159,7 +159,7 @@ The private representation of the current account, returned by [Get current user
<sup>5</sup> For an OAuth2 bearer credential without the `email` [scope](/http-api/oauth2/#oauth2-scopes), `email` is `null`
<sup>6</sup> Phone numbers are no longer stored on the account record. The field is retained so an older client keeps parsing the response
<sup>6</sup> The account record stores no phone number. The field is retained so an older client keeps parsing the response
<sup>7</sup> The pair is present only while the account has the staff flag, and it is absent for every other account
@@ -169,7 +169,7 @@ The private representation of the current account, returned by [Get current user
<sup>10</sup> The value is forced to `0` and `premium_since` is forced to `null` while premium entitlements are not currently active
<sup>11</sup> The value is the later of the subscription end and any stacked gift extension, and it is reported even when premium entitlements are no longer active
<sup>11</sup> `premium_until` is the later of the subscription end and any stacked gift extension. Fluxer reports it even when premium entitlements are no longer active
<sup>12</sup> While the value is set, premium entitlements remain active until it passes, and it replaces the default grace interval that otherwise follows `premium_until`
@@ -438,7 +438,7 @@ One entry of the saved, user-owned guild sidebar layout. Joining a guild prepend
## Guild sensitive media filters
Guild channels accept only these two values.
Guild channels accept only these values.
| Value | Name | Description |
| --- | --- | --- |
@@ -604,7 +604,7 @@ Returns a [full user profile](#full-user-profile-object) object for one target a
### Limitations
- A caller reading another account needs one of four relationships with the target: friendship, a pending friend request in either direction, a shared guild, or a shared group direct message channel.
- A caller reading another account needs one of these relationships with the target: friendship, a pending friend request in either direction, a shared guild, or a shared group direct message channel.
- Reading a bot target is always permitted.
- Reading the caller's own profile bypasses both the access check and profile privacy.
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
Recent mentions and saved messages are private lists that only the account owning them can read. Two further routes delete the messages the caller authored, one filtered and immediate, the other unfiltered and delayed by a day. Data harvests live on [Data harvests](/http-api/users/data-harvest/).
Every route here is user-only. Fluxer rejects a bot or OAuth2 bearer credential with 403 `ACCESS_DENIED`, and an account that has an outstanding required action with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
These routes are user-only. Fluxer rejects a bot or OAuth2 bearer credential with 403 `ACCESS_DENIED`, and an account that has an outstanding required action with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
Every route except [Delete current user's messages](#delete-current-users-messages) reaches the main Gateway, and a Gateway failure returns 502 `BAD_GATEWAY`, 503 `SERVICE_UNAVAILABLE`, or 504 `GATEWAY_TIMEOUT`. [Save message](#save-message) resolves the channel before it writes, so a failure there stores nothing. The request still returns 204 when a [Saved Message Create](/gateway/events/#saved-message-create), [Saved Message Delete](/gateway/events/#saved-message-delete), or [Recent Mention Delete](/gateway/events/#recent-mention-delete) Dispatch fails to publish after the write.
@@ -20,7 +20,7 @@ Every route except [Delete current user's messages](#delete-current-users-messag
A saved entry is a private bookmark pairing one message with the channel it was saved from. Saving a message notifies nobody, including its author.
An entry stores no copy of the message. Fluxer resolves the message against current permissions on every read, so one entry can have a message on one read and none on the next.
The entry stores no copy of the message. Fluxer resolves the message against current permissions on every read, so one entry can have a message on one read and none on the next.
### Structure
@@ -294,7 +294,7 @@ Every message the filter selects is deleted from its channel for every recipient
### JSON body
The caller proves sudo mode either with the five sudo fields below or with an existing proof in the request. An existing proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header.
The caller proves sudo mode either with the sudo fields below or with an existing proof in the `X-Fluxer-Sudo-Mode-JWT` request header.
| Field | Type | Description |
| --- | --- | --- |
@@ -322,7 +322,7 @@ The caller proves sudo mode either with the five sudo fields below or with an ex
<sup>4</sup> Supplying both bounds requires `start_date` strictly earlier than `end_date`, and an equal pair fails validation on `end_date`
<sup>5</sup> The five sudo fields are the [sudo verification object](/http-api/users/mfa/#sudo-verification-object). Which combination is accepted depends on the account's configured authenticators
<sup>5</sup> These fields are the [sudo verification object](/http-api/users/mfa/#sudo-verification-object). Which combination is accepted depends on the account's configured authenticators
The `selected` scope requires at least one of the four context toggles to be true, and a request that disables all four fails validation on `include_dms`. The `inaccessible_only` scope ignores the toggles and the guild filter.
@@ -12,7 +12,7 @@ Every route except [Get current user](#get-current-user) is user-only. Those rou
## Sudo verification
Sudo verification asks the account to prove itself again inside the request. Security-sensitive fields and destructive lifecycle operations require it. Proof comes in one of three forms.
Sudo verification asks the account to prove itself again inside the request. Security-sensitive fields and destructive lifecycle operations require it. The accepted proof depends on the account state.
| Account state | Accepted proof |
| --- | --- |
@@ -20,13 +20,13 @@ Sudo verification asks the account to prove itself again inside the request. Sec
| An authenticator configured | `mfa_method` of `totp` with `mfa_code`, or `mfa_method` of `webauthn` with `webauthn_response` and `webauthn_challenge` |
| No password credential and no authenticator | None, the requirement is already satisfied |
An already valid sudo token replaces any of those forms. Once an account configures an authenticator its `password` stops working here.
Once an account configures an authenticator, its `password` stops working here. An already valid sudo token replaces any of those forms.
A sudo token is an HS256 JSON Web Token valid for five minutes. It has the account ID as its subject and is rejected for any other account. A successful verification returns the token in the `X-Fluxer-Sudo-Mode-JWT` response header. Fluxer mints a fresh token only when an authenticator satisfied the requirement. A request presenting an already valid token gets that same token back without an extended lifetime.
A request that does not satisfy the requirement returns 403 [`SUDO_MODE_REQUIRED`](/http-api/errors/), whose body has `has_mfa` and `methods` as top-level members. An invalid authenticator code, backup code, or WebAuthn assertion returns [`INVALID_MFA_CODE`](/http-api/errors/) on the `mfa_code` path. An incorrect password returns [`INVALID_PASSWORD`](/http-api/errors/) on the `password` path.
A request that does not satisfy the requirement returns 403 [`SUDO_MODE_REQUIRED`](/http-api/errors/). The body has `has_mfa` and `methods` as top-level members. An invalid authenticator code, backup code, or WebAuthn assertion returns [`INVALID_MFA_CODE`](/http-api/errors/) on the `mfa_code` path. An incorrect password returns [`INVALID_PASSWORD`](/http-api/errors/) on the `password` path.
A request presents an existing token in the `X-Fluxer-Sudo-Mode-JWT` request header. Retain the response header value and send it back on each later operation in the same sudo window.
A client sends an existing token in the `X-Fluxer-Sudo-Mode-JWT` request header. Retain the response header value and send it back on each later operation in the same sudo window.
## Get current user
@@ -127,9 +127,9 @@ Modifies the current account and returns the resulting [user](/http-api/users/#u
To skip the body proof fields, send the sudo token in the `X-Fluxer-Sudo-Mode-JWT` request header.
A body that has no `email_token` and nothing beyond `mfa_method`, `mfa_code`, `webauthn_response`, and `webauthn_challenge` returns the current account unchanged. That check runs before the required action barrier, so it succeeds even while an action is outstanding. A body that has only `password` is an update with nothing to change, and it still emits [User Update](/gateway/events/#user-update).
A body that has no `email_token` and nothing beyond `mfa_method`, `mfa_code`, `webauthn_response`, and `webauthn_challenge` returns the current account unchanged. That check runs before the required action barrier, so it succeeds even while an action is outstanding. Supplying `password` alone is an update with nothing to change, and it still emits [User Update](/gateway/events/#user-update).
Fluxer rejects an account with an outstanding required action, with one exemption. A body that has `email_token` and nothing beyond the sudo verification fields is accepted. Any other defined field removes the exemption.
Fluxer rejects an account with an outstanding required action. The one exemption is a body that has `email_token` and nothing beyond the sudo verification fields. Any other defined field removes the exemption.
A claimed account needs a verified email before changing the username, discriminator, display name, avatar, banner, biography, pronouns, accent colour, timezone, timezone privacy, or any premium badge field. A request without one returns 403 [`PROFILE_EMAIL_VERIFICATION_REQUIRED`](/http-api/errors/).
@@ -286,7 +286,7 @@ Records acceptance of the current terms of service and privacy policy. Returns t
### JSON body
The body is an empty object. An absent or empty body is read as an empty object, and any supplied property is discarded. A body that is not valid JSON returns 400 `INVALID_FORM_BODY` with `INVALID_FORMAT` at the `body` path. A body that parses as valid JSON but is not an object is rejected with 400.
The body is an empty object. An absent or empty body is read as an empty object, and any supplied property is discarded. Invalid JSON returns 400 `INVALID_FORM_BODY` with `INVALID_FORMAT` at the `body` path. Valid JSON that is not an object is rejected with 400.
### Response
@@ -25,7 +25,7 @@ The status is derived from the recorded timestamps in a fixed order, testing `fa
| completed | The archive was written and is ready for download |
| failed | The latest attempt recorded a failure |
Fluxer retries the underlying job. A retry clears `failed_at` and `error_message`, sets `started_at` again, resets the reported progress to 0, and reports `processing` while it runs. A retry that succeeds records `completed_at`, `file_size`, and a download deadline, and reports `completed`.
Fluxer retries the underlying job. A retry clears `failed_at` and `error_message`, sets `started_at` again, resets the reported progress to 0, and reports `processing` while it runs. On success it records `completed_at`, `file_size`, and a download deadline, and reports `completed`.
## Harvest creation object
@@ -144,7 +144,11 @@ The filter chooses which messages a harvest collects. Every non-message section
<sup>4</sup> Compared against the creation time embedded in the message [snowflake](/snowflakes/), so editing an old message does not move it into a later window
Under the `selected` scope a one-to-one direct message is collected when the conversation is still open and `include_dms` is true, or when it has been closed and `include_dms_closed` is true. A group DM is collected only while the caller is still a recipient and `include_group_dms` is true. A guild channel is collected only while the caller is still a member, `include_guilds` is true, and the guild is admitted by the configured list mode.
Under the `selected` scope, each context has its own condition:
- A one-to-one direct message is collected when the conversation is still open and `include_dms` is true, or when it has been closed and `include_dms_closed` is true.
- A group DM is collected only while the caller is still a recipient and `include_group_dms` is true.
- A guild channel is collected only while the caller is still a member, `include_guilds` is true, and the guild is admitted by the configured list mode.
The `inaccessible_only` scope selects messages in guilds the caller has left or been removed from and in group DMs the caller has left. It never selects one-to-one direct messages, and it ignores the four toggles, the guild filter mode, and both guild lists.
@@ -176,9 +180,18 @@ The operation takes no request body and always returns before the archive exists
A harvest record is created with a new [snowflake](/snowflakes/), the request time as `created_at`, progress 0, the step `Queued`, and every other timestamp null. Creating a harvest removes no earlier record and enforces no ceiling on how many an account holds, so the route bucket is the only bound.
Fluxer prepares the archive asynchronously. It contains `user.json` with the account document, one `channels/{channel_id}/messages.json` file for each channel that contributed a message, `payments/payment_history.json`, `integrations/oauth.json` listing the [applications](/http-api/applications/) the account owns rather than the ones it authorised, and `account/security.json`. The account avatar and banner are added under `assets/user/` when present. Messages inside each channel file are ordered oldest first. Attachment metadata appears with its message and has the attachment ID, filename, size, content type, CDN URL, and pixel dimensions. The attachment files themselves are never included.
Fluxer prepares the archive asynchronously. It contains:
At most 100,000 authored messages are collected. The collector skips a message it cannot read, and the harvest still succeeds. When the archive completes, Fluxer sends one email containing a download URL if the account has an email address and the instance has email delivery enabled. That URL expires seven days after it was minted.
- `user.json`, the account document
- one `channels/{channel_id}/messages.json` file for each channel that contributed a message, with its messages ordered oldest first
- `payments/payment_history.json`
- `integrations/oauth.json`, the [applications](/http-api/applications/) the account owns, with no entry for an application it only authorised
- `account/security.json`
- the account avatar and banner under `assets/user/` when present
Attachment metadata appears with its message and has the attachment ID, filename, size, content type, CDN URL, and pixel dimensions. The attachment files themselves are never included.
At most 100,000 authored messages are collected. Fluxer skips a message it cannot read, and the harvest still succeeds. When the archive completes, Fluxer sends one email containing a download URL if the account has an email address and the instance has email delivery enabled. That URL expires seven days after it was minted.
### Rate limit
@@ -303,12 +316,10 @@ The URL takes one of two forms. When the instance has presigned harvest download
Streams the archive of one completed harvest.
The signed `token` query parameter issued by [Get data harvest download URL](#get-data-harvest-download-url) is the whole authorisation, so the link stays usable from the harvest completion email and from a plain browser.
The signed `token` query parameter issued by [Get data harvest download URL](#get-data-harvest-download-url) is the whole authorisation, so the link stays usable from the harvest completion email and from a plain browser. It binds the account, the harvest ID, the stored object key, and an expiry.
The operation is active only while the instance has presigned harvest downloads disabled. That setting is enabled by default, so a default deployment answers every request here as 404 without inspecting the token.
The token binds the account, the harvest ID, the stored object key, and an expiry.
:::note[Every rejection looks identical]
A missing, malformed, forged, or expired token, a token for another harvest, a harvest that never completed, a harvest that recorded a failure, a harvest past its deadline, and a stale object key all return 404 with the plain text body `Not Found`.
:::
@@ -34,7 +34,14 @@ Each of these values can advance a security-sensitive account change. Keep them
Every ticket, code, address, proof, token, and password failure named on this page arrives as HTTP 400 whose top-level code is `INVALID_FORM_BODY`. The named value is the `code` of one [validation error](/http-api/#validation-error-object) entry, and that entry's `path` is the field it belongs to.
:::
A ticket failure uses the path `ticket`, a code failure `code`, and an address failure `new_email` or `email`. A proof failure uses `original_proof` or `verification_proof`, an email token failure uses `email_token`, and a password failure uses `password` or `new_password`. The account-level codes `ACCESS_DENIED`, `ACCOUNT_SUSPICIOUS_ACTIVITY`, `SUDO_MODE_REQUIRED`, and `RATE_LIMITED` arrive as the top-level `code`.
- A ticket failure uses the path `ticket`.
- A code failure uses the path `code`.
- An address failure uses the path `new_email` or `email`.
- A proof failure uses the path `original_proof` or `verification_proof`.
- An email token failure uses the path `email_token`.
- A password failure uses the path `password` or `new_password`.
The account-level codes `ACCESS_DENIED`, `ACCOUNT_SUSPICIOUS_ACTIVITY`, `SUDO_MODE_REQUIRED`, and `RATE_LIMITED` arrive as the top-level `code`.
## Email send controls
@@ -49,7 +56,7 @@ Each send-bearing operation passes through its own route bucket and, separately,
## Email change ticket states
The email change ticket moves through three states. A step legal in one state is rejected in every other with the transition code named below.
The email change ticket is pending-original, pending-new, or completed. A step legal in one state is rejected in every other with the transition code named below.
| Current state | Operation | Next state |
| --- | --- | --- |
@@ -61,7 +68,7 @@ The email change ticket moves through three states. A step legal in one state is
A ticket created without original email verification starts in the pending-new state and receives its `original_proof` directly from [Start email change](#start-email-change). Requesting or verifying a new address while the ticket is still pending original fails with `ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST`.
Verifying the original stage on a ticket that never required it fails with `ORIGINAL_VERIFICATION_NOT_REQUIRED`. Resending the original code fails with `ORIGINAL_EMAIL_ALREADY_VERIFIED` on a cleared ticket and on a ticket that never required the stage, so the response does not tell the two apart. Verifying the original address again on a cleared ticket returns the same `original_proof` unchanged.
Verifying the original stage on a ticket that never required it fails with `ORIGINAL_VERIFICATION_NOT_REQUIRED`. Resending the original code fails with `ORIGINAL_EMAIL_ALREADY_VERIFIED` on a cleared ticket and on a ticket that never required the stage, so the response does not tell the two apart. On a cleared ticket, verifying the original address again returns the same `original_proof`.
Resending the new address code before an address has been requested fails with `NO_NEW_EMAIL_REQUESTED`. A pending-original ticket returns the same code.
@@ -280,7 +287,14 @@ Binds a new address to an active ticket and sends it a verification code. Return
A ticket that has not cleared original address verification fails with `ORIGINAL_EMAIL_MUST_BE_VERIFIED_FIRST`. An `original_proof` that does not match the ticket fails with `INVALID_PROOF_TOKEN`.
Fluxer trims the address and then checks it in order. An empty address fails with `EMAIL_IS_REQUIRED`, an address equal to the ticket's recorded original with `NEW_EMAIL_MUST_BE_DIFFERENT`, a domain that fails DNS eligibility with `INVALID_EMAIL_ADDRESS`, and an address another account already owns with `EMAIL_ALREADY_IN_USE`. Ownership is checked again when the resulting email token is applied, so passing here reserves nothing. Calling the operation again with a different address replaces the pending address on the same ticket.
Fluxer trims the address and then checks it in order.
- An empty address fails with `EMAIL_IS_REQUIRED`.
- An address equal to the ticket's recorded original fails with `NEW_EMAIL_MUST_BE_DIFFERENT`.
- A domain without DNS eligibility fails with `INVALID_EMAIL_ADDRESS`.
- An address another account already owns fails with `EMAIL_ALREADY_IN_USE`.
Ownership is checked again when the resulting email token is applied, so passing here reserves nothing. Calling the operation again with a different address replaces the pending address on the same ticket.
The send consumes the new address control, and the ticket enforces its own 30-second cooldown.
@@ -385,17 +399,15 @@ The ticket moves to the completed state and a 30-minute email token is issued fo
Consumes an email token and writes its address to the account. Returns the updated [user](/http-api/users/#user-object) object on success. Emits a [User Update](/gateway/events/#user-update) Gateway event.
A claimed account proves [sudo mode](/http-api/users/mfa/#sudo-mode) before the route examines the token. The route accepts suspicious account state.
A claimed account proves [sudo mode](/http-api/users/mfa/#sudo-mode) before the route examines the token. An unclaimed account is exempt and applies the change with its session alone. The route accepts suspicious account state.
A token that belongs to another account fails with `INVALID_EMAIL_TOKEN`, and one past its 30-minute lifetime fails with `EMAIL_TOKEN_EXPIRED`. An expired token is deleted when rejected. Fluxer checks the address against existing account ownership again here, and an address another account has taken since fails with `EMAIL_ALREADY_IN_USE` without consuming the token.
An unclaimed account is exempt from sudo mode and applies the change with its session alone.
[Modify current user](/http-api/users/current-user/#modify-current-user) accepts the same token alongside an unrelated profile edit. The token is single use, so a retry after success fails with `INVALID_EMAIL_TOKEN`.
### JSON body
The body extends the [sudo verification object](/http-api/users/mfa/#sudo-verification-object) with `email_token`, and the five sudo fields it merges in are repeated below. An existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header.
The body extends the [sudo verification object](/http-api/users/mfa/#sudo-verification-object) with `email_token`, and the sudo fields it merges in are repeated below. An existing sudo proof travels in the `X-Fluxer-Sudo-Mode-JWT` request header.
| Field | Type | Description |
| --- | --- | --- |
@@ -447,7 +459,7 @@ For an ordinary change from an existing address, Fluxer sends a revert email to
## Bounced email recovery
An account whose stored address the mail provider rejected has the bounced marker, reported as `email_bounced` on the [user object](/http-api/users/#user-object). The bounce also clears the verified email state and imposes a required action. The three operations below bind a replacement address and finish without the sudo-gated apply step.
An account whose stored address the mail provider rejected has the bounced marker, reported as `email_bounced` on the [user object](/http-api/users/#user-object). The bounce also clears the verified email state and imposes a required action. The operations below bind a replacement address and finish without the sudo-gated apply step.
Every one of them requires the bounced marker. An account without it is rejected with 403 `ACCESS_DENIED`, and an account with no address at all is rejected with `MUST_HAVE_EMAIL_TO_CHANGE_IT`.
@@ -484,7 +496,7 @@ When the account does hold a verified address the request fails with `ORIGINAL_E
### Side effects
A ticket is created pending-new with the replacement address bound to it, and one verification email goes to that address. No account field changes. Retain the returned `ticket`, because the two remaining steps take no other identifier.
A ticket is created pending-new with the replacement address bound to it, and one verification email goes to that address. No account field changes. Retain the returned `ticket`, because the remaining steps take no other identifier.
### Rate limit
@@ -11,7 +11,7 @@ A note is short private text the caller stores about another account.
Every route here requires a user session. A bot or OAuth2 bearer credential is refused with 403 `ACCESS_DENIED`. An account with an outstanding required action is refused with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
:::note[A note is private to its author]
The target account never receives it in an HTTP response or [Gateway Dispatch](/gateway/events/). A note has no relationship requirement, so the caller can hold one for an account that is neither a friend nor blocked.
The target account never receives the note in an HTTP response or [Gateway Dispatch](/gateway/events/). A note has no relationship requirement, so the caller can hold one for an account that is neither a friend nor blocked.
:::
## User note object
@@ -103,10 +103,10 @@ Creates, replaces, or deletes the caller's note for one target account. Returns
The supplied text becomes the complete stored note. An unresolved target ID returns 404 `UNKNOWN_USER`, even when the request would only delete a note. The caller can store a note for any account that exists, including a bot account, its own account, and an account it holds no relationship with.
:::caution[An omitted, null, or empty note deletes it]
An empty string is [read as `null`](/http-api/#input-normalisation), so the three forms are indistinguishable. A value that normalises to the empty string, such as one made only of whitespace, fails validation with `STRING_LENGTH_INVALID`. There is no separate delete route.
An empty string is [read as `null`](/http-api/#input-normalisation), so the three forms are indistinguishable. Any value that normalises to the empty string, such as one made only of whitespace, fails validation with `STRING_LENGTH_INVALID`. There is no separate delete route.
:::
A `note` of at least 3 characters matching the instance phrase blocklist, or with a URL matching the URL blocklist, is rejected with 403 `CONTENT_BLOCKED`. That screen runs ahead of the route's own credential and body checks, so a blocked value is refused with `CONTENT_BLOCKED` even when the request would also fail one of those checks.
A `note` of at least 3 characters matching the instance phrase blocklist, or with a URL matching the URL blocklist, is rejected with 403 `CONTENT_BLOCKED`. The blocklist check runs before the route's own credential and body checks, so a blocked value is refused with `CONTENT_BLOCKED` even when the request would also fail one of those checks.
### Path parameters
@@ -136,7 +136,7 @@ Fluxer reads an omitted body as an empty object and deletes the stored note.
### Side effects
Supplying text creates or replaces the note, and a request that supplies none deletes it. [User Note Update](/gateway/events/#user-note-update) reaches the caller's own sessions for every accepted request, including one that supplies the current value and one that deletes an absent note. Its payload has the target ID in `id` and the note text in `note`. A deletion has the empty string.
Supplying text creates or replaces the note, and a request that supplies none deletes it. [User Note Update](/gateway/events/#user-note-update) reaches the caller's own sessions for every accepted request, including one that supplies the current value and one that deletes an absent note. Its payload has the target ID in `id` and the note text in `note`. On a deletion the note text is the empty string.
### Rate limit
@@ -12,7 +12,7 @@ Every route here needs a non-bot user session, and each one admits a session wit
## Eligibility
[Send phone verification](#send-phone-verification) and [Verify phone code](#verify-phone-code) admit only an account that satisfies at least one of four conditions.
[Send phone verification](#send-phone-verification) and [Verify phone code](#verify-phone-code) admit only an account that satisfies at least one of these conditions.
- The account already holds a verified phone.
- A TOTP [authenticator](/http-api/users/#authenticator-types) is enrolled on the account.
@@ -29,11 +29,11 @@ The [required actions](/http-api/users/#required-actions) array is filtered: emp
## Deferred phone requirement
An instance can defer a new account's phone requirement instead of imposing it at registration. The [deferred phone gate](/admin-api/instance/#deferred-phone-gate-object) policy sets the window after registration in which a qualifying join makes the requirement due, and the member count above which a guild is a qualifying one. A guild with the `DISCOVERABLE` [feature](/http-api/guilds/#guild-features) qualifies at any member count.
At registration, an instance can defer a new account's phone requirement. The [deferred phone gate](/admin-api/instance/#deferred-phone-gate-object) policy sets the window after registration in which a qualifying join makes the requirement due, and the member count above which a guild is a qualifying one. A guild with the `DISCOVERABLE` [feature](/http-api/guilds/#guild-features) qualifies at any member count.
A deferred requirement is absent from the [required actions](/http-api/users/#required-actions) array and restricts nothing. An attempt to join a qualifying guild inside the window makes it due, and [Accept invite](/http-api/invites/#accept-invite) refuses that attempt with 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`. The account then owes `REQUIRE_VERIFIED_PHONE` or `REQUIRE_REVERIFIED_PHONE`, and every ordinary user route refuses it with the same code.
[Get phone gate escape preview](#get-phone-gate-escape-preview) and [Run phone gate escape](#run-phone-gate-escape) set that requirement aside again in place of verifying a number. Both admit an account the rest of the API refuses. The escape is available only while all six of these hold.
[Get phone gate escape preview](#get-phone-gate-escape-preview) and [Run phone gate escape](#run-phone-gate-escape) set that requirement aside again without verifying a number. Both admit an account the rest of the API refuses. The escape is available only while all of these hold.
- The instance has the deferred phone gate enabled.
- The account holds no verified phone.
@@ -205,13 +205,17 @@ What [Run phone gate escape](#run-phone-gate-escape) would do for the current ac
Starts verification of the submitted number. Requires an account satisfying [eligibility](#eligibility). Returns an [SMS delivery](#sms-delivery-object) object or an [inbound challenge](#inbound-challenge-object) object on success.
Several independent policies can force the inbound channel, and each one overrides a requested `sms`. Fluxer routes the attempt inbound before it contacts the lookup and SMS providers in three cases. The caller asks for `inbound_challenge`. The account has the flag that forces inbound verification. The configured inbound-required prefix policy matches the number while the account holds no verified phone.
Several independent policies can force the inbound channel, and each one overrides a requested `sms`. Fluxer routes the attempt inbound before it contacts the lookup and SMS providers when any of these holds.
- The caller asks for `inbound_challenge`.
- The account has the flag that forces inbound verification.
- The configured inbound-required prefix policy matches the number while the account holds no verified phone.
An attempt that survives that step passes through phone attempt risk controls keyed by account, by client IP address, and by /24 network. A hard block returns 429 `PHONE_RATE_LIMIT_EXCEEDED` with a `Retry-After` of 86400. A captcha decision returns 400 `CAPTCHA_REQUIRED`, and this route reads no [CAPTCHA](/topics/captcha/) solution header, so the attempt cannot be retried with a solution. An inbound decision returns a challenge with the `behavioural_risk` reason.
An earlier attempt against the same account or number can record a provider cooldown, and a later request under that cooldown returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The destination is then resolved through [provider lookup](#provider-lookup-verdicts). A number that has already completed verification twice is refused with 400 `PHONE_ALREADY_USED`.
Two further controls bound outbound delivery: 3 sends per 6 hours for each account, and 3 sends per 5 days for each number. Either denial returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The provider can also refuse the send: an invalid destination returns 400 `INVALID_PHONE_NUMBER`, a throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED`, and every other refusal, an unreachable provider included, returns 400 `SMS_VERIFICATION_UNAVAILABLE`. An attempt routed inbound on an instance where the inbound flow or its receiving number is unconfigured returns the same code.
Further controls bound outbound delivery to 3 sends per 6 hours for each account and 3 sends per 5 days for each number. Either denial returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The provider can also refuse the send. An invalid destination returns 400 `INVALID_PHONE_NUMBER`, and a throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. Every other refusal, an unreachable provider included, returns 400 `SMS_VERIFICATION_UNAVAILABLE`. On an instance where the inbound flow or its receiving number is unconfigured, an attempt routed inbound returns the same code.
### JSON body
@@ -276,7 +280,7 @@ Verifies an outbound SMS code. Requires an account satisfying [eligibility](#eli
The number is resolved through [provider lookup](#provider-lookup-verdicts) again before the code is checked, so a number that has become ineligible since the code was sent is refused.
The configured SMS provider checks the number and the code together, so the provider decides the code lifetime and the attempt allowance. A rejected code returns 400 `INVALID_PHONE_VERIFICATION_CODE`. A provider throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED` and records a cooldown that also denies [Send phone verification](#send-phone-verification) until it expires. An unreachable provider, or one answering with a server error, returns 400 `SMS_VERIFICATION_UNAVAILABLE`. An account record that has been deleted, or can no longer be read, fails with 400 `PHONE_VERIFICATION_REQUIRED` after the code has been accepted.
The configured SMS provider checks the number and the code together, so the provider decides the code lifetime and the attempt allowance. A rejected code returns 400 `INVALID_PHONE_VERIFICATION_CODE`. A provider throttle returns 429 `PHONE_RATE_LIMIT_EXCEEDED` and records a cooldown that also denies [Send phone verification](#send-phone-verification) until it expires. An unreachable provider, or one answering with a server error, returns 400 `SMS_VERIFICATION_UNAVAILABLE`. Once the code has been accepted, an account record that has been deleted or can no longer be read fails with 400 `PHONE_VERIFICATION_REQUIRED`.
### JSON body
@@ -343,7 +347,7 @@ Leaves the qualifying guilds the account does not own and defers the phone requi
### JSON body
The body is an empty object. An absent or empty body is read as an empty object, and any supplied property is discarded. A body that is not valid JSON returns 400 `INVALID_FORM_BODY` with `INVALID_FORMAT` at the `body` path. A body that parses as valid JSON but is not an object is rejected with 400.
The body is an empty object. Fluxer reads an absent or empty body as an empty object and discards any supplied property. A body that is not valid JSON returns 400 `INVALID_FORM_BODY` with `INVALID_FORMAT` at the `body` path. Valid JSON that is not an object is rejected with 400.
### Response
@@ -356,7 +360,7 @@ The body is an empty object. An absent or empty body is read as an empty object,
Fluxer leaves each qualifying guild the account does not own, at most 25 in one request, with the same effects as [Leave guild](/http-api/guilds/#leave-guild). A qualifying guild the account owns is kept and does not block the escape.
A request that leaves the whole set then defers the requirement again and dispatches [User Update](/gateway/events/#user-update). A request that stopped at the limit writes no account field, dispatches nothing, and returns the account with the requirement still due. Repeat the request until the returned `required_actions` has no phone entry.
A request that leaves the whole set then defers the requirement again and dispatches [User Update](/gateway/events/#user-update). One that stopped at the limit writes no account field, dispatches nothing, and returns the account with the requirement still due. Repeat the request until the returned `required_actions` has no phone entry.
### Rate limit
@@ -11,7 +11,7 @@ A private channel is a conversation outside any guild. It is either a direct mes
The routes here take a user session or a bot token. An OAuth2 bearer credential receives 403 `ACCESS_DENIED`, and an account with an outstanding required action receives 403 `ACCOUNT_SUSPICIOUS_ACTIVITY`.
:::note[Open state is per participant]
A direct message channel exists once, and each participant holds its own open state. Closing one leaves the channel and its messages in place, so reopening the pair resolves the stored channel.
A direct message channel exists once, and each participant holds its own open state. Closing one side leaves the channel and its messages in place, so reopening the pair resolves the stored channel.
:::
## Preloaded messages object
@@ -56,11 +56,11 @@ One recipient that could not be added to a group DM, and the reason. [Create pri
Returns the caller's open direct message and group DM [channel](/http-api/channels/#channel-object) objects.
The route returns only the channels the caller currently has open, so a closed direct message is absent even though the channel and its history still exist. The response has no ordering guarantee and is unpaginated. The caller's personal notes channel is never returned.
A closed direct message is absent even though the channel and its history still exist. The response has no ordering guarantee and is unpaginated. The caller's personal notes channel is never returned.
Each returned channel has its recipients with the caller removed, so a direct message holds exactly one recipient and a group DM one fewer than its participant count. A group DM whose only participant is the caller omits the `recipients` key entirely.
An open entry whose channel no longer lists the caller among its recipients is omitted, so leaving a group DM needs no separate cleanup call.
Fluxer omits an open entry whose channel no longer lists the caller among its recipients, so leaving a group DM needs no separate cleanup call.
### Response
@@ -215,9 +215,9 @@ Adds a direct message or group DM to the caller's pinned order. Returns 204 with
Fluxer resolves the channel with the caller's ordinary channel authorisation first. A private channel the caller is not a recipient of returns 404 `UNKNOWN_CHANNEL`, and a channel that does not exist returns the same. The channel then has to be a direct message or a group DM, so the caller's personal notes channel and a visible guild channel are rejected with 400 `CHANNEL_MUST_BE_DM_OR_GROUP_DM`, a field code on `channel_id` under `INVALID_FORM_BODY`.
A guild channel the caller cannot see fails resolution first and returns 403 `MISSING_PERMISSIONS`. A channel row that outlived its guild returns 404 `UNKNOWN_GUILD`. A guild record that still exists while the caller's membership state cannot be resolved returns 403 `ACCESS_DENIED`.
A guild channel the caller cannot see fails resolution first and returns 403 `MISSING_PERMISSIONS`. Fluxer returns 404 `UNKNOWN_GUILD` for a channel row that outlived its guild. Where the guild record still exists and the caller's membership state cannot be resolved, the request returns 403 `ACCESS_DENIED`.
A newly pinned channel is appended after every channel already pinned. A channel later closed, left, or deleted stays in the pinned set, and only [Unpin private channel](#unpin-private-channel) or deletion of the caller's account removes an entry.
Fluxer appends a newly pinned channel after every channel already pinned. A channel later closed, left, or deleted stays in the pinned set, and only [Unpin private channel](#unpin-private-channel) or deletion of the caller's account removes an entry.
:::caution[Unpinning needs the channel to resolve]
Unpinning resolves the channel first, so an entry whose channel the caller can no longer resolve stays in the pinned set.
@@ -32,7 +32,7 @@ The [global HTTP limit](/topics/rate-limits/) applies to every management route.
## Origin refusal
Fluxer refuses a call from the official web client on four routes. A request whose `Origin` header is exactly `https://web.fluxer.app` or `https://web.canary.fluxer.app` returns 403 `INVALID_API_ORIGIN` on [Execute webhook](#execute-webhook), [Get webhook message](#get-webhook-message), [Edit webhook message](#edit-webhook-message), and [Delete webhook message](#delete-webhook-message).
Fluxer refuses a call from the official web client on [Execute webhook](#execute-webhook), [Get webhook message](#get-webhook-message), [Edit webhook message](#edit-webhook-message), and [Delete webhook message](#delete-webhook-message). A request whose `Origin` header is exactly `https://web.fluxer.app` or `https://web.canary.fluxer.app` returns 403 `INVALID_API_ORIGIN`.
A request that sends no `Origin`, or any other `Origin` value, passes the check. It runs after the route rate limit and before path validation.
@@ -169,13 +169,13 @@ Every entry supplied in a JSON body is dropped, and it counts towards neither th
## Message references
A webhook execution accepts the [message reference input](/http-api/messages/#message-reference-input-object) object defined by the Messages resource. Three constraints are specific to webhooks.
A webhook execution accepts the [message reference input](/http-api/messages/#message-reference-input-object) object defined by the Messages resource. The constraints below are specific to webhooks.
The `channel_id` field is required for a forward reference and must equal the webhook's own channel. A value naming any other channel returns 404 `UNKNOWN_MESSAGE`. A reply reference always resolves in the webhook's own channel and ignores this field.
The referenced message must exist in the webhook's channel, and a missing message returns 404 `UNKNOWN_MESSAGE`. A reply reference additionally requires the referenced message to be an ordinary or reply message. Any other type returns 400 `INVALID_FORM_BODY` with the validation code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`.
A forward reference must not accompany content, embeds, or attachments, and one that does returns 400 `INVALID_FORM_BODY` with the validation code `FORWARD_MESSAGES_CANNOT_CONTAIN_CONTENT`. A forward reference that omits `channel_id` or `message_id` returns the same status with `FORWARD_REFERENCE_REQUIRES_CHANNEL_AND_MESSAGE`.
A forward reference must not accompany content, embeds, or attachments, and one that does returns 400 `INVALID_FORM_BODY` with the validation code `FORWARD_MESSAGES_CANNOT_CONTAIN_CONTENT`. A missing `channel_id` or `message_id` on a forward reference returns the same status with `FORWARD_REFERENCE_REQUIRES_CHANNEL_AND_MESSAGE`.
## Webhook message edit body
@@ -264,7 +264,7 @@ No per-embed title, description, footer, or field maximum applies to a Slack val
The Slack schema bounds only `username`, which must be 1 through 80 characters and rejects a longer value with `WEBHOOK_NAME_LENGTH_INVALID`. Fluxer applies no bound to any other Slack string or to either array.
Three checks do apply. A conversion yielding neither content nor an embed returns 400 `CANNOT_SEND_EMPTY_MESSAGE`. Content longer than the effective maximum returns 400 `INVALID_FORM_BODY` with the validation code `CONTENT_EXCEEDS_MAX_LENGTH`, and more converted embeds than the resolved ceiling returns the same status with `TOO_MANY_EMBEDS`. Fluxer discards an attachment that produces no embed property.
A conversion yielding neither content nor an embed returns 400 `CANNOT_SEND_EMPTY_MESSAGE`. Content longer than the effective maximum returns 400 `INVALID_FORM_BODY` with the validation code `CONTENT_EXCEEDS_MAX_LENGTH`, and more converted embeds than the resolved ceiling returns the same status with `TOO_MANY_EMBEDS`. Fluxer discards an attachment that produces no embed property.
## GitHub callback objects
@@ -1301,11 +1301,9 @@ Any other type returns 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`, and a forward, which
| Target message type cannot be edited | 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK` |
| Stored channel no longer resolves to a guild channel | 400 `CANNOT_EXECUTE_ON_DM` |
| Body has no visible content, no non-empty embeds, and no `flags` | 400 `CANNOT_SEND_EMPTY_MESSAGE` |
| Content longer than the effective maximum | 400 `CONTENT_EXCEEDS_MAX_LENGTH` |
| Content longer than the effective maximum | 400 `INVALID_FORM_BODY` with the field code `CONTENT_EXCEEDS_MAX_LENGTH` |
| Anything else | 400 `INVALID_FORM_BODY` |
`CONTENT_EXCEEDS_MAX_LENGTH` is a validation code inside a 400 `INVALID_FORM_BODY` body.
### Side effects
The operation replaces the supplied fields, and a content change marks the message edited. An edit does not re-extract mentions. The stored mention lists are kept, and `allowed_mentions` is accepted and not read. Supplying embeds replaces the complete embed collection and revalidates every attachment reference the embeds make. The operation emits [Message Update](/gateway/events/#message-update) to sessions that can read the channel.
@@ -1475,7 +1473,7 @@ A rendered callback emits a [Message Create](/gateway/events/#message-create).
The body is an [Instatus callback](#instatus-callback-object) object. A body the schema rejects returns 400 `INVALID_FORM_BODY`.
:::note[Deduplication is per webhook and callback]
Only the first callback for a webhook and a given incident update, maintenance update, or component update is processed within a 24-hour window. A repeated callback is acknowledged and creates no second message. A callback with no such identifier is processed every time.
Only the first callback for a webhook and a given incident update, maintenance update, or component update is processed within a 24-hour window. A repeated callback is acknowledged and creates no second message. Fluxer processes every callback that has no such identifier.
:::
A callback that renders nothing, and one whose message creation fails, both leave the identifier free for a later retry.
+10 -4
View File
@@ -37,17 +37,23 @@ A [snowflake](/snowflakes/) is the identifier all four surfaces share. Voice run
## Endpoint discovery
A client that knows only a Fluxer origin reads endpoint discovery first. `GET /.well-known/fluxer` is unversioned, accepts no credential, and is readable from any origin.
A client that knows only a Fluxer origin sends `GET /.well-known/fluxer` first. The route is unversioned, accepts no credential, and is readable from any origin.
```text
GET https://example.com/.well-known/fluxer
```
The instance Fluxer hosts answers discovery at `https://fluxer.app/.well-known/fluxer`. That origin is the one thing a client is given. Every base URL below it still comes from the response.
The instance Fluxer hosts answers discovery at `https://fluxer.app/.well-known/fluxer`. That origin is the one thing a client is given.
It returns the [instance discovery object](/http-api/instance/#instance-discovery-object). Every base URL a client uses comes from the [instance endpoints object](/http-api/instance/#instance-endpoints-object) inside it. A client MUST read every base URL from that response, and it MUST NOT derive one from the origin it was given or assume an official Fluxer domain.
The response is the [instance discovery object](/http-api/instance/#instance-discovery-object). Every base URL a client uses comes from the [instance endpoints object](/http-api/instance/#instance-endpoints-object) inside it. A client MUST read every base URL from that response, and it MUST NOT derive one from the origin it was given or assume an official Fluxer domain.
Take the base URL for the kind of client being built, then send a credential in the `Authorization` header. A bot, a library, or any other third-party client takes `endpoints.api_public`. `endpoints.api_client` is the endpoint the first-party web application uses, and `endpoints.api` repeats it.
The base URL a client takes depends on its kind.
- `endpoints.api_public` is the endpoint a bot, a library, or any other third-party client uses.
- `endpoints.api_client` is the endpoint the first-party web application uses.
- `endpoints.api` repeats `endpoints.api_client`.
A credential goes in the `Authorization` header.
```text
GET https://api.example.com/v1/users/@me
@@ -23,7 +23,7 @@ The Media Proxy serves Fluxer attachments, image assets, themes, entrance sound
<sup>2</sup> Served by a `static` endpoint alone
[Transformations](/media-proxy/transformations/) defines representation selection for every family that has one. [Responses and limits](/media-proxy/responses-and-limits/) consolidates statuses, size bounds, deadlines, and cache policies.
[Transformations](/media-proxy/transformations/) defines representation selection for every family that has one. [Responses and limits](/media-proxy/responses-and-limits/) lists statuses, size bounds, deadlines, and cache policies.
## Base URLs
@@ -120,7 +120,7 @@ A reversed range, a zero-length suffix, a start outside the representation, or a
The route forwards a range to the origin only when no transformation is requested. It sends the range verbatim when the value after `bytes=` is non-empty and every byte of it is an ASCII graphic character, so a multiple range reaches the origin and the origin decides how to answer it. A value with a space anywhere is dropped, and no range is sent. The route relays the origin partial response with the origin `Content-Range` unchanged.
A transforming request forwards no range to the origin and applies the client range to the transformed bytes, so it still returns 206 or 416. An origin 200 on a non-transforming request is relayed as that 200 when its declared type is trustworthy and the response is not SVG by declared type, filename, or leading bytes. A relayed 200 does not reapply the client range. An SVG response is rasterised and the client range is applied to the rasterised bytes.
A transforming request forwards no range to the origin and applies the client range to the transformed bytes, so it still returns 206 or 416. On a non-transforming request, an origin 200 is relayed as that 200 when its declared type is trustworthy and the response is not SVG by declared type, filename, or leading bytes. The relayed 200 does not reapply the client range. Fluxer rasterises an SVG response and applies the client range to the rasterised bytes.
A trustworthy type is a normalised `image/`, `video/`, or `audio/` type other than `application/octet-stream`. An absent or empty `Content-Type`, `text/plain`, `application/pdf`, and `application/zip` are all untrustworthy. Fluxer buffers the body of a 200 under an untrustworthy type and applies the client range to those bytes, so that read returns 206.
@@ -163,7 +163,7 @@ Disposition follows that declared type, so SVG mislabelled as an image or video
Every successful media representation uses `Cache-Control: public, max-age=31536000` and `CDN-Cache-Control: public, max-age=31536000`. An audio or video representation adds `no-transform` to the browser-facing policy only. No response repeats its policy in an `Expires` header. [Cache policies](/media-proxy/responses-and-limits/#cache-policies) lists the responses that have no policy at all.
No read route sends an `ETag`. The upload relay is the only route that returns one, and it relays the object storage value for the stored object.
The upload relay is the only route that returns an `ETag`, and it relays the object storage value for the stored object.
:::note[External media is cached for a year too]
The signed path is derived from the target URL, so the bytes behind one unchanged target are cached for a year at both layers.
@@ -4,7 +4,7 @@ title: Responses and limits
description: Media Proxy statuses, error bodies, size bounds, work admission, deadlines, and cache policies.
---
Almost every failed Media Proxy request answers with a short `text/plain` reason phrase and nothing else. Every bound below is a resource bound, because the Media Proxy has no request-count rate limit.
Almost every failed Media Proxy request answers with a short `text/plain` reason phrase and nothing else. The Media Proxy has no request-count rate limit, so every bound below is a resource bound.
## Media error response
@@ -27,7 +27,7 @@ An unsuccessful response body is an English reason phrase under the content type
<sup>1</sup> The relay also answers 400 when the client connection fails part way through the body
<sup>2</sup> The six registered paths, `/_health`, `/_metrics`, `/_metadata`, `/_thumbnail`, `/_frames`, and `/v1/relay/{key}`, answer an unaccepted method with an empty body, no `Content-Type`, and an `Allow` header
<sup>2</sup> The registered paths, `/_health`, `/_metrics`, `/_metadata`, `/_thumbnail`, `/_frames`, and `/v1/relay/{key}`, answer an unaccepted method with an empty body, no `Content-Type`, and an `Allow` header
<sup>3</sup> `/_metadata` is the only endpoint that remaps an origin 429. The signed external read route retains 429 as 429
@@ -60,7 +60,7 @@ Every `HEAD` response has an empty body, so the Body column describes `GET`, `PU
| 503 | [Media error response](#media-error-response) | Upload spool capacity is exhausted, an external buffer reservation or allocation failed, or an external origin answered `/_metadata` with 429<sup>5</sup> |
| 504 | [Media error response](#media-error-response) | Transformation capacity was unavailable or a transformation deadline expired |
<sup>1</sup> Four [internal endpoints](/media-proxy/routes/#operator-and-internal-endpoints) answer 200 with a non-media body. `/_metadata` and `/_frames` answer with JSON, `/_health` with plain text, and `/_metrics` with the Prometheus text exposition
<sup>1</sup> The [internal endpoints](/media-proxy/routes/#operator-and-internal-endpoints) `/_metadata` and `/_frames` answer 200 with JSON, `/_health` with plain text, and `/_metrics` with the Prometheus text exposition
<sup>2</sup> A path served by the read fallback answers with the [media error response](#media-error-response). Each registered path answers with an empty body, no `Content-Type`, and an `Allow` header
@@ -75,14 +75,14 @@ An external origin status of 400, 401, 403, 404, 405, 406, 408, 409, 410, 411, 4
The Media Proxy evaluates no conditional request header and never redirects a noncanonical target, so it returns no 304 and no 308.
:::note[A retained status describes the external URL]
The third-party origin chose it, and Fluxer passed the status through.
The third-party origin chose the status, and Fluxer passed it through.
:::
## Request and media limits
Proxied or stored media is limited to 500 MiB, and exceeding that returns 413. The bound applies to a streamed object, a buffered object, an external response body, and any input selected for transformation. When a streamed external body passes the bound only after the response head is committed, the Media Proxy truncates it.
A decoded signed external target URL is limited to 8,192 bytes, and the route follows at most five redirects. A longer URL returns 400. A sixth redirect returns 502, and so does a redirect back to an already visited URL. A redirect target is subject to the same bound and the same address policy as the original URL. Content detection inspects the leading 8,192 bytes of a body.
A decoded signed external target URL is limited to 8,192 bytes, and a longer URL returns 400. The route follows at most five redirects. A sixth redirect returns 502, and so does a redirect back to an already visited URL. Every redirect target is subject to the same bound and the same address policy as the original URL. Content detection inspects the leading 8,192 bytes of a body.
Buffered external bodies share one endpoint budget of 500 MiB for every [work admission](#work-admission) slot plus 512 KiB. A body the budget cannot cover returns 503, and so does a failed buffer allocation.
@@ -94,7 +94,12 @@ An internal `/_metadata`, `/_thumbnail`, or `/_frames` request body is limited t
## Work admission
A transformation first takes an admission slot without waiting. The pool holds one slot for every concurrent native transform plus one for every queued transform. Native transform concurrency defaults to the process parallelism clamped to 2 through 8 and can be configured from 1 through 128. The queue defaults to eight times that concurrency and can be configured from 1 through 8192. When no slot is free the request returns 504 immediately.
A transformation first takes an admission slot without waiting. The pool holds one slot for every concurrent native transform plus one for every queued transform. When no slot is free the request returns 504 immediately.
| Setting | Default | Configurable range |
| --- | --- | --- |
| Native transform concurrency | The process parallelism clamped to 2 through 8 | 1 through 128 |
| Queue depth | Eight times the native transform concurrency | 1 through 8192 |
Once admitted, the transformation waits for a native transform permit until the transformation deadline. A wait that outlives the deadline also returns 504.
@@ -85,7 +85,13 @@ The [common responses](#common-responses) also apply.
Returns external media through Fluxer, optionally transformed. Fluxer verifies the signature before it decodes or fetches the target.
Fluxer constructs the signed path and exposes it on `proxy_url` of an [embed media object](/http-api/messages/#embed-media-object), `proxy_icon_url` of an [embed author object](/http-api/messages/#embed-author-object), `proxy_icon_url` of an [embed footer object](/http-api/messages/#embed-footer-object), `proxy_url` of a [resolved GIF entry](/http-api/memes/#resolved-gif-entry-object), and `proxy_src` of a [GIF media format object](/http-api/memes/#gif-media-format-object).
Fluxer constructs the signed path and exposes it on these fields:
- `proxy_url` of an [embed media object](/http-api/messages/#embed-media-object)
- `proxy_icon_url` of an [embed author object](/http-api/messages/#embed-author-object)
- `proxy_icon_url` of an [embed footer object](/http-api/messages/#embed-footer-object)
- `proxy_url` of a [resolved GIF entry](/http-api/memes/#resolved-gif-entry-object)
- `proxy_src` of a [GIF media format object](/http-api/memes/#gif-media-format-object)
A field holds a signed path only when its source URL is external. A URL already under the Media Proxy endpoint, and any URL the deployment cannot sign, is returned verbatim, so a client MUST request the value exactly as issued and MUST NOT assume it has the signed shape.
@@ -129,7 +135,7 @@ The route follows at most five redirects. A further redirect, a repeated URL, or
A transformation begins when `width`, `height`, `format`, or `quality` is present, when `animated` resolves to true, when the target filename ends in `.svg`, or when the origin body is SVG by media type or by its first bytes. A transforming request forwards no `Range` to the origin and applies the client range to the transformed bytes, so it still selects 206 or 416.
A non-transforming request forwards the client range to the origin under the filter [byte ranges](/media-proxy/overview/#byte-ranges) defines, and relays an origin 206 with its `Content-Range` and `Content-Length` unchanged. An origin that ignores the range answers 200, and Fluxer streams that whole body through when the origin names an `image/`, `video/`, or `audio/` media type other than `image/svg+xml`. Fluxer buffers a transforming request, an SVG body, and any body whose declared media type is empty, `application/octet-stream`, or outside those three categories. A buffered response has the range applied to the bytes the route finally serves, so a client range over a buffered origin 200 produces 206.
A non-transforming request forwards the client range to the origin under the filter [byte ranges](/media-proxy/overview/#byte-ranges) defines, and relays an origin 206 with its `Content-Range` and `Content-Length` unchanged. An origin that ignores the range answers 200, and Fluxer streams that whole body through when the origin names an `image/`, `video/`, or `audio/` media type other than `image/svg+xml`. Fluxer buffers a transforming request, an SVG body, and any body whose declared media type is empty, `application/octet-stream`, or outside those categories. A buffered response has the range applied to the bytes the route finally serves, so a client range over a buffered origin 200 produces 206.
Buffering reserves the whole body from a process-wide external buffer budget of 500 MiB for each configured transform slot and queue slot, plus 512 KiB. A reservation the budget cannot cover returns 503.
@@ -217,7 +223,7 @@ The [common responses](#common-responses) also apply.
## Image asset contract
An image asset is a stored picture Fluxer serves at a requested size and format, such as an avatar, a guild icon, or an emoji. A resource object exposes the asset hash, and the path combines the owning resource, that hash, and a file extension. An emoji or sticker path has its own identifier as the filename. The grammar, query parameters, and responses below govern every image asset route. [Asset size selection](/media-proxy/transformations/#asset-size-selection) defines every size class.
An image asset is a stored picture Fluxer serves at a requested size and format, such as an avatar, a guild icon, or an emoji. The asset hash comes from the resource object, and the path combines the owning resource, that hash, and a file extension. On an emoji or sticker path the filename is the identifier itself. The grammar, query parameters, and responses below govern every image asset route. [Asset size selection](/media-proxy/transformations/#asset-size-selection) defines every size class.
A client builds the complete URL from the Media Proxy [base URL](/media-proxy/overview/#base-urls), the path template for the asset class, the hash, and a file extension.
@@ -498,4 +504,4 @@ These paths are not part of the public API surface. They are exempt from the med
| `/_thumbnail` | POST | Produces a WebP thumbnail of a staged upload for the HTTP API |
| `/_frames` | POST | Extracts one JPEG video frame for the HTTP API |
`/_metadata`, `/_thumbnail`, and `/_frames` require `Authorization: Bearer {deployment secret key}` and return 401 without it. Their JSON request bodies are bounded, and a body beyond the bound returns 413 on all three.
`/_metadata`, `/_thumbnail`, and `/_frames` require `Authorization: Bearer {deployment secret key}` and return 401 without it. Their JSON request bodies are bounded, and a body beyond the bound returns 413.
@@ -57,7 +57,7 @@ Transformations never enlarge an image.
`width` alone scales proportionally to the requested width, and `height` alone scales proportionally to the requested height. A fit inside a rectangle uses the smaller of the two ratios, so a dimension requested larger than the source still shrinks when the other requested dimension is smaller than the source.
A cover crop scales a still image to cover the requested rectangle and crops it centrally. An attachment or signed external request asks for one by supplying both `width` and `height`. An emoji or sticker asset always uses one. Every other image asset fits inside the selected square and preserves its full aspect ratio.
A cover crop scales a still image to cover the requested rectangle and crops it centrally. The Media Proxy applies one to an attachment or signed external request that supplies both `width` and `height`, and to every emoji or sticker asset. Every other image asset fits inside the selected square and preserves its full aspect ratio.
:::note[An animated transformation fits the whole frame]
The Media Proxy downgrades a cover crop to a plain fit whenever it opens the decoder for every page. An animated emoji or sticker is fitted inside its square.
@@ -76,7 +76,7 @@ An asset request snaps `size` to the ladder and then clamps the result into the
<sup>1</sup> The banner maximum of 2400 is not a ladder rung and is reachable only by clamping, so 3072 and every larger request collapse onto it
An absent `size` resolves to 128 before clamping, so an avatar defaults to 128 and a banner defaults to 480. An emoji and a sticker also default to 128. Every ladder value below a class minimum collapses onto that minimum, and every value above a class maximum collapses onto that maximum.
An absent `size` resolves to 128 before clamping, so an avatar defaults to 128 and a banner defaults to 480. Emoji and sticker assets also default to 128. Every ladder value below a class minimum collapses onto that minimum, and every value above a class maximum collapses onto that maximum.
## Attachment and external formats
@@ -124,7 +124,7 @@ Quality names are matched exactly and are case-sensitive. An unrecognised value
<sup>3</sup> Lossless applies to WebP alone, and JPEG at quality 100 is still a lossy encode
An image asset defaults to `high`. An attachment or signed external image defaults to `lossless`, except that a JPEG, HEIC, or HEIF source defaults to `high`. Animated WebP output defaults to `auto` on every route that reads `quality`. A video thumbnail is extracted at `high`, and `quality` then applies only to the resize step that `width` or `height` requests. A non-transforming SVG rasterisation always uses `lossless`.
An image asset defaults to `high`. An attachment or signed external image defaults to `lossless`, except that a JPEG, HEIC, or HEIF source defaults to `high`. Animated WebP output defaults to `auto` on every route that reads `quality`. Fluxer extracts a video thumbnail at `high`, and `quality` then applies only to the resize step that `width` or `height` requests. A non-transforming SVG rasterisation always uses `lossless`.
Encoder effort defaults to 2 for animated output or `low` quality and 4 otherwise, and it applies to WebP output only. JPEG and PNG have fixed encoder settings, and GIF always encodes at effort 7. The attachment-only `effort` parameter replaces the default and is clamped to 9. Static WebP output clamps it again to 6, and so does lossy animated WebP. Only lossless animated WebP uses 7 through 9.
@@ -150,13 +150,13 @@ Fluxer extracts one thumbnail from a video only when the request supplies an exp
An attachment video request with another transformation parameter but no `format` returns 400. A signed external video request without `format` returns the original bytes instead.
An attachment source that is neither an image nor a video returns 400 when `format` is present and is otherwise returned unchanged. A signed external source that is neither an image nor a video is always returned unchanged.
An attachment source that is neither an image nor a video returns 400 when `format` is present and is otherwise returned unchanged. The signed external route always returns such a source unchanged.
## Original representations
The Media Proxy returns the original bytes when the source already has the selected format, no resize or crop is required, and no encoder option requires a new representation. A source whose bytes sniff as animated also requires a request that resolves to animated, and a static request against it is encoded.
An `effort` value forces encoding, and a `quality` value forces encoding for every source except GIF. An animated attachment or signed external request for the source's own GIF, WebP, or APNG format with neither `width` nor `height` bypasses both tests and can still reuse the original animation.
An `effort` value forces encoding, and a `quality` value forces encoding for every source except GIF. With neither `width` nor `height`, an animated attachment or signed external request for the source's own GIF, WebP, or APNG format bypasses both tests and can still reuse the original animation.
The Media Proxy derives the response `Content-Type` from the content when the stored media type is empty, is case-insensitively `application/octet-stream`, or is outside the set `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `image/apng`, `image/avif`, `image/heic`, `image/heif`, `image/jxl`, and `image/svg+xml`. An original response can therefore use a different media type from the stored metadata. A stored media type from that set is trusted even when it disagrees with the bytes and is served unchanged.
@@ -164,7 +164,7 @@ The Media Proxy derives the response `Content-Type` from the content when the st
Proxied or stored media is limited to 500 MiB. A stored object above that bound returns 413, and so does an external origin that declares or delivers more. The same bound applies to a transforming request and to every buffer it produces.
Decoded images are limited to 16,384 pixels on either edge and 268,435,456 pixels in total. Animated input is additionally limited to 20,000 decoded frames and 1,073,741,824 decoded pixels across all frames. These four bounds are fixed. Exceeding a decoded image or animation limit fails the transformation.
Decoded images are limited to 16,384 pixels on either edge and 268,435,456 pixels in total. Animated input is additionally limited to 20,000 decoded frames and 1,073,741,824 decoded pixels across all frames. These bounds are fixed. Exceeding a decoded image or animation limit fails the transformation.
Animated WebP and animated APNG output is bounded again at encode time, and exceeding one of those bounds truncates the output. The encoder stops adding frames after 20,000 frames, or once the accumulated frame delays reach 30,000 ms of playback, and emits the frames it already has. An operator can configure the frame cap from 1 through 100,000 and the playback cap from 100 through 600,000 ms. Animated GIF output has neither cap on either of its paths, so the decode limits above are its only bound.
@@ -10,9 +10,9 @@ The upload relay is where a client sends the bytes of a file. Each upload URL ha
## Base URL and authorisation
Both paths here are relative to the relay base in the issued upload URL. That base is a deployment setting.
Paths here are relative to the relay base in the issued upload URL. That base is a deployment setting.
The capability is the complete authorisation. The relay reads no HTTP API `Authorization` header and has no request-count rate limit. The `PUT` has a [deployment mode](/media-proxy/overview/#deployment-modes) gate and returns 404 outside `upload` mode. Every path below `/v1/relay/` is exempt from the [media access policy](/media-proxy/overview/#access-restrictions).
The capability is the complete authorisation. The relay reads no HTTP API `Authorization` header and has no request-count rate limit. Every path below `/v1/relay/` is exempt from the [media access policy](/media-proxy/overview/#access-restrictions). The `PUT` has a [deployment mode](/media-proxy/overview/#deployment-modes) gate and returns 404 outside `upload` mode.
:::note[Some upload URLs address object storage directly]
The HTTP API decides per request whether to relay, from the caller's country. A caller on the instance's direct-upload list receives a presigned object storage URL, and this contract does not describe it.
@@ -74,7 +74,7 @@ The relay answers 401 to a malformed capability, a signature that is not 32 byte
<RouteHeader method="PUT" path="/v1/relay/{key}" />
Stores the exact object the [relay capability](#relay-capability-object) authorises and returns 200 with an empty body. An `ETag` accompanies it when the object store supplied one. An S3 backend supplies one and a local filesystem backend does not.
Stores the exact object the [relay capability](#relay-capability-object) authorises and returns 200 with an empty body. The response has an `ETag` when the object store supplied one. An S3 backend supplies one and a local filesystem backend does not.
### Path parameters
@@ -160,8 +160,15 @@ A successful request writes the object the capability selects to the uploads buc
## Streaming limits
The endpoint body limit defaults to 500 MiB and an operator can configure it from 1 byte through 5 GiB. The effective limit for one request is the smaller of that value and the capability `mb` field.
| Setting | Default | Configurable range |
| --- | --- | --- |
| Endpoint body limit | 500 MiB | 1 byte through 5 GiB |
| Spool buffer | 1 MiB | 64 KiB through 64 MiB |
| Endpoint spool budget | 8 GiB | Up to 256 GiB |
| Object storage write deadline | 900,000 ms | 1,000 through 3,600,000 ms |
A spooled body is written through a 1 MiB buffer by default, configurable from 64 KiB through 64 MiB. Spooled bodies share an 8 GiB endpoint budget by default, configurable up to 256 GiB. A request that arrives with no `Content-Length` reserves the complete effective limit from that budget before the relay reads a byte. The endpoint refuses to start when its body limit exceeds that budget, so one reservation always fits an idle endpoint.
The effective limit for one request is the smaller of the endpoint body limit and the capability `mb` field.
The object storage write has a 900,000 ms deadline by default, configurable from 1,000 through 3,600,000 ms. For a streamed body the relay extends the deadline by one second for every 16 KiB of declared length.
The relay writes a spooled body to its temporary file through the spool buffer. Spooled bodies share one endpoint budget, and a request that arrives with no `Content-Length` reserves the complete effective limit from that budget before the relay reads a byte. The endpoint refuses to start when its body limit exceeds that budget, so one reservation always fits an idle endpoint.
For a streamed body the relay extends the object storage write deadline by one second for every 16 KiB of declared length.
@@ -8,24 +8,24 @@ description: Every variable a self-hosted instance reads, grouped by concern.
We are grateful to everyone supporting the project through [Fluxer Plutonium](https://fluxer.app/plutonium) or [donations](https://fluxer.app/donate). All of our code is free and open source on [GitHub](https://github.com/fluxerapp/fluxer). The Operator Pass is coming, and adds a direct line to the team for help and feedback.
:::
Fluxer reads its settings from environment variables. They live in a file named `.env`, in the same directory as `docker-compose.yml`. This page names every variable that file can hold.
Fluxer reads its settings from environment variables. They live in a file named `.env`, in the same directory as `docker-compose.yml`.
A first run touches two sections. [Core identity and public address](#core-identity-and-public-address) has `FLUXER_DOMAIN`, the hostname people type into a browser. [Secrets you must generate](#secrets-you-must-generate) has the fourteen values that ship as `CHANGE_ME`. Everything after those two is optional and already has a working value, so read it when you want to change something.
A first run touches two sections. [Core identity and public address](#core-identity-and-public-address) has `FLUXER_DOMAIN`, the hostname people type into a browser. [Secrets you must generate](#secrets-you-must-generate) has the values that ship as `CHANGE_ME`. Everything after those two is optional and already has a working value, so read it when you want to change something.
The installer in [Get started](/operator/get-started/) writes `.env` for you and fills in all fourteen secrets. [Upgrading](/operator/upgrading/) covers moving between releases.
The installer in [Get started](/operator/get-started/) writes `.env` for you and fills in every secret. [Upgrading](/operator/upgrading/) covers moving between releases.
## How configuration is loaded
Compose reads `.env` and passes the values it names into containers. A name reaches a container only when `docker-compose.yml` lists it, either in the shared `x-fluxer-env` block or in that service's own `environment` block. No service declares `env_file`, so a name in `.env` that appears in neither block never arrives, whatever it is set to.
A `$` inside a value is a variable reference to Compose, not a character. `POSTGRES_PASSWORD=ab$cd` reaches the container as `ab`, and every command against the stack prints `The "cd" variable is not set. Defaulting to a blank string.` first. Write the `$` as `$$`, or put single quotes around the whole value. Both deliver one literal `$`:
Compose reads a `$` inside a value as a variable reference. `POSTGRES_PASSWORD=ab$cd` reaches the container as `ab`, and every command against the stack prints `The "cd" variable is not set. Defaulting to a blank string.` first. Write the `$` as `$$`, or put single quotes around the whole value. Both deliver one literal `$`:
```ini
POSTGRES_PASSWORD=ab$$cd
POSTGRES_PASSWORD='ab$cd'
```
Double quotes do not escape it. `docker compose config` prints a literal `$` back as `$$`, so a value that reads `ab$$cd` in that output is the correct one. The fourteen secrets the installer generates are hex or base64 and hold no `$`, so this reaches an instance through a password, an API key or an SMTP secret pasted in by hand.
Double quotes do not escape it. `docker compose config` prints a literal `$` back as `$$`, so a value that reads `ab$$cd` in that output is the correct one. The secrets the installer generates are hex or base64 and hold no `$`, so this reaches an instance through a password, an API key or an SMTP secret pasted in by hand.
A container's environment is fixed when the container is created, and `api` and `worker` cache their configuration at first load. Either way a change needs the process restarted, which `docker compose up -d` does by recreating the service.
@@ -78,35 +78,35 @@ Default `http`. The scheme for internal service URLs. Must be `http` or `https`,
## Secrets you must generate
Every value below ships as `CHANGE_ME`. Replace all fourteen before the first start. Compose refuses to start when one is unset or empty, and names the variable. The installer in [Get started](/operator/get-started/) generates all fourteen, so come here to rotate one value later or to set them up by hand.
Every value below ships as `CHANGE_ME`. Replace them all before the first start. Compose refuses to start when one is unset or empty, and names the variable. Generate each with `openssl rand -hex 32`, except the relay secret and the VAPID pair, whose own entries name the command. The installer in [Get started](/operator/get-started/) generates them all, so come here to rotate one value later or to set them up by hand.
#### `POSTGRES_PASSWORD`
Generate with `openssl rand -hex 32`. The database login, reused as `FLUXER_POSTGRES_PASSWORD`. Requires changing the stored role password too.
The database login, reused as `FLUXER_POSTGRES_PASSWORD`. Requires changing the stored role password too.
#### `MEILI_MASTER_KEY`
Generate with `openssl rand -hex 32`. The Meilisearch master key, reused as `FLUXER_SEARCH_API_KEY`. Recreate `meilisearch` and every service that searches.
The Meilisearch master key, reused as `FLUXER_SEARCH_API_KEY`. Recreate `meilisearch` and every service that searches.
#### `FLUXER_S3_SECRET_KEY`
Generate with `openssl rand -hex 32`. The object-storage secret, reused as `FLUXER_S3_SECRET_ACCESS_KEY`. `seaweedfs-init` installs it as the object store's only S3 identity on every `compose up`, so a changed value takes effect on the next start.
The object-storage secret, reused as `FLUXER_S3_SECRET_ACCESS_KEY`. `seaweedfs-init` installs it as the object store's only S3 identity on every `compose up`, so a changed value takes effect on the next start.
#### `FLUXER_SUDO_MODE_SECRET`
Generate with `openssl rand -hex 32`. Sudo mode JWTs, as a raw HS256 key. Invalidates every elevated session.
Sudo mode JWTs, as a raw HS256 key. Invalidates every elevated session.
#### `FLUXER_CONNECTION_INITIATION_SECRET`
Generate with `openssl rand -hex 32`. Connection initiation tokens and harvest download links. Invalidates in-flight authorisations and issued download links.
Connection initiation tokens and harvest download links. Invalidates in-flight authorisations and issued download links.
#### `FLUXER_GATEWAY_RPC_AUTH_TOKEN`
Generate with `openssl rand -hex 32`. Internal RPC between the API and the Gateway. Must be byte-identical on `api`, `worker`, and `gateway`.
Internal RPC between the API and the Gateway. Must be byte-identical on `api`, `worker`, and `gateway`.
#### `FLUXER_MEDIA_PROXY_SECRET_KEY`
Generate with `openssl rand -hex 32`. Media Proxy URLs. Must match across `api`, `worker`, `media-proxy`, `gifs`, and `unfurl`.
Media Proxy URLs. Must match across `api`, `worker`, `media-proxy`, `gifs`, and `unfurl`.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64`
@@ -114,19 +114,19 @@ Generate with `openssl rand -base64 32`. [Upload relay](/media-proxy/upload-rela
#### `FLUXER_ADMIN_SECRET_KEY_BASE`
Generate with `openssl rand -hex 32`. Admin sessions, CSRF tokens, and OAuth state. Signs every admin out. The admin service refuses to start when it is empty.
Admin sessions, CSRF tokens, and OAuth state. Signs every admin out. The admin service refuses to start when it is empty.
#### `FLUXER_ADMIN_OAUTH_CLIENT_SECRET`
Generate with `openssl rand -hex 32`. The admin OAuth2 client secret. The API requires a non-empty value to serve the admin application.
The admin OAuth2 client secret. The API requires a non-empty value to serve the admin application.
#### `FLUXER_ERLANG_COOKIE`
Generate with `openssl rand -hex 32`. The BEAM distribution secret. Only the Gateway reads it. Rotate it on every Gateway node at once when clustering.
The BEAM distribution secret. Only the Gateway reads it. Rotate it on every Gateway node at once when clustering.
#### `LIVEKIT_API_SECRET`
Generate with `openssl rand -hex 32`. LiveKit access tokens. Must be at least 32 characters.
LiveKit access tokens. Must be at least 32 characters.
#### `FLUXER_VAPID_PUBLIC_KEY`
@@ -148,7 +148,7 @@ Two more values ship with a usable value. Both are required.
## Endpoint derivation from FLUXER_BASE_DOMAIN
Fluxer builds eleven public endpoints. Ten of them come from the scheme, the base domain, and the port, and the docs endpoint is a fixed value. A port of `443` under `https` or `wss`, or `80` under `http` or `ws`, is omitted.
Fluxer builds its public endpoints from the scheme, the base domain, and the port. The docs endpoint is a fixed value. A port of `443` under `https` or `wss`, or `80` under `http` or `ws`, is omitted.
| Endpoint | Derived value |
| --- | --- |
@@ -467,7 +467,7 @@ Default `http://localhost:3900`. The S3 API address. `media-proxy` defaults to e
#### `FLUXER_S3_PUBLIC_ENDPOINT`
No default. The host substituted into presigned URLs. The name misleads. Presigned URLs are the only place it appears.
No default. The host substituted into presigned URLs, which are the only place it appears.
#### `FLUXER_S3_FORCE_PATH_STYLE`
@@ -625,7 +625,11 @@ Defaults to 192 for messages, 320 for snowflakes, 64 otherwise. In-flight reques
No default. The shard ordinal source and node identity. Also read by the Gateway and by API RPC timing.
The API tunes its NATS clients through fourteen further names, none of which are in the override table or in `.env.example`: `FLUXER_SNOWFLAKE_SERVICE_SUBJECT`, `FLUXER_SNOWFLAKE_SERVICE_NATS_CLIENT_NAME`, `FLUXER_SNOWFLAKE_SERVICE_BATCH_SIZE`, `FLUXER_SNOWFLAKE_SERVICE_LOW_WATERMARK`, `FLUXER_SNOWFLAKE_SERVICE_MAX_BUFFER_AGE_MS`, `FLUXER_SNOWFLAKE_SERVICE_REQUEST_TIMEOUT_MS`, `FLUXER_USERS_SERVICE_SUBJECT`, `FLUXER_USERS_SERVICE_NATS_CLIENT_NAME`, `FLUXER_USERS_SERVICE_TIMEOUT_MS`, `FLUXER_USERS_SERVICE_INFLIGHT_MAX_ENTRIES`, `FLUXER_GIF_SERVICE_SUBJECT`, `FLUXER_GIF_SERVICE_NATS_CLIENT_NAME`, `FLUXER_GIF_SERVICE_TIMEOUT_MS`, and `FLUXER_GIF_SERVICE_REGISTER_SHARE_TIMEOUT_MS`. Each falls back to its default when the value is not a positive number.
The API tunes its NATS clients through these names, none of which are in the override table or in `.env.example`. Each falls back to its default when the value is not a positive number.
- Snowflake service: `FLUXER_SNOWFLAKE_SERVICE_SUBJECT`, `FLUXER_SNOWFLAKE_SERVICE_NATS_CLIENT_NAME`, `FLUXER_SNOWFLAKE_SERVICE_BATCH_SIZE`, `FLUXER_SNOWFLAKE_SERVICE_LOW_WATERMARK`, `FLUXER_SNOWFLAKE_SERVICE_MAX_BUFFER_AGE_MS`, and `FLUXER_SNOWFLAKE_SERVICE_REQUEST_TIMEOUT_MS`.
- Users service: `FLUXER_USERS_SERVICE_SUBJECT`, `FLUXER_USERS_SERVICE_NATS_CLIENT_NAME`, `FLUXER_USERS_SERVICE_TIMEOUT_MS`, and `FLUXER_USERS_SERVICE_INFLIGHT_MAX_ENTRIES`.
- GIF service: `FLUXER_GIF_SERVICE_SUBJECT`, `FLUXER_GIF_SERVICE_NATS_CLIENT_NAME`, `FLUXER_GIF_SERVICE_TIMEOUT_MS`, and `FLUXER_GIF_SERVICE_REGISTER_SHARE_TIMEOUT_MS`.
## Voice and LiveKit
@@ -633,7 +637,7 @@ LiveKit is the media server for voice and video. `FLUXER_LIVEKIT_API_KEY` and `F
#### `FLUXER_LIVEKIT_ENABLED`
Default `false`. The master voice and video switch. The name misleads. It gates all voice. Compose sets `true`.
Default `false`. The master switch for all voice and video. Compose sets `true`.
#### `FLUXER_LIVEKIT_API_KEY`
@@ -1010,11 +1014,25 @@ Defaults to the inverse of `FLUXER_SELF_HOSTED`. External blocklist feeds. Off b
A second family, unrelated to the rules above, tunes the IP auto-banner: `FLUXER_ABUSE_WINDOW_MS`, four `FLUXER_ABUSE_THRESHOLD_` names, four `FLUXER_ABUSE_TOKEN_DIVERSITY_` names, `FLUXER_ABUSE_BAN_TTL_SEC`, `FLUXER_ABUSE_BATCH_FLUSH_MS`, `FLUXER_ABUSE_MAX_BATCH_TICKS`, `FLUXER_ABUSE_MAX_NEW_TOKENS_PER_TICK`, `FLUXER_ABUSE_MAX_TRACKED_IPS`, `FLUXER_ABUSE_MAX_TOKEN_HASHES_PER_IP`, `FLUXER_ABUSE_MIN_SCORE_FOR_LOOKUP`, `FLUXER_ABUSE_MIN_TOKENS_FOR_LOOKUP`, and `FLUXER_ABUSE_REQUIRED_SCORE_WINDOWS_FOR_AUTO_BAN`. All are read directly from the environment, none are in `.env.example` or the Compose file, and a non-finite value or one at or below zero falls back to the default.
Five more names in that family limit how often the auto-banner buys an IP classification from ipinfo. Before them, every API replica looked up the same attacking IP at the same moment, so one IP cost one lookup per replica. `FLUXER_ABUSE_IP_CLASS_CLAIM_ENABLED` defaults to `1` and gates the shared claim that now lets one replica do the lookup for all of them. `FLUXER_ABUSE_IP_CLASS_CLAIM_TTL_SEC` defaults to `15` and sets how long a replica holds that claim. `FLUXER_ABUSE_IP_CLASS_PENDING_TTL_MS` defaults to `20000` and sets how long a replica that lost the claim waits before it tries again. `FLUXER_ABUSE_IP_CLASS_NEGATIVE_TTL_MS` defaults to `300000` and sets how long a failed classification is remembered. `FLUXER_ABUSE_IP_CLASS_HINT_TTL_MS` defaults to `600000` and sets how long a class sent by another replica stays usable. The claim key is `abuse:ipclass:claim:` plus the ban key, and replicas send classes to each other on the `abuse_tracker:ipclass` key-value channel. The four numeric names are read through the same helper as the names above, so a non-finite value or one at or below zero falls back to the default. `FLUXER_ABUSE_IP_CLASS_CLAIM_ENABLED` is read as a string instead, and only `0` turns the claim off.
Five more names in that family limit how often the auto-banner buys an IP classification from ipinfo. A shared claim lets one replica do the lookup for the others. With the claim off, every API replica looks up the same attacking IP at the same moment, so one IP costs one lookup per replica.
A `FLUXER_IPINFO_BUDGET_` family caps what the instance spends at ipinfo. `FLUXER_IPINFO_BUDGET_ENABLED` defaults to `1`, and `0` turns off all shedding. `FLUXER_IPINFO_BUDGET_MONTHLY_MAX` defaults to `140000` and is the ceiling for one UTC calendar month. Lookups run at three priorities. Admin IP bans and scheduled deletion checks are critical and reach the full ceiling. Registration risk is standard and stops at `FLUXER_IPINFO_BUDGET_STANDARD_MONTHLY_PCT` percent of it, default `90`. The IP auto-banner is background and stops at `FLUXER_IPINFO_BUDGET_BACKGROUND_MONTHLY_PCT` percent, default `60`. The lower ceilings mean background lookups stop first and critical lookups stop last. Each priority also has a token bucket for bursts, sized by `FLUXER_IPINFO_BUDGET_CRITICAL_BURST` (`60`), `FLUXER_IPINFO_BUDGET_STANDARD_BURST` (`240`) and `FLUXER_IPINFO_BUDGET_BACKGROUND_BURST` (`120`), refilled once a minute by `FLUXER_IPINFO_BUDGET_CRITICAL_REFILL_PER_MIN` (`60`), `FLUXER_IPINFO_BUDGET_STANDARD_REFILL_PER_MIN` (`120`) and `FLUXER_IPINFO_BUDGET_BACKGROUND_REFILL_PER_MIN` (`30`). The counters live in the key-value store under `ipinfo:budget:burst:` and `ipinfo:budget:month:`. A shed lookup returns an unavailable result instead of an error, and any key-value failure admits the lookup at every priority, so a cache outage never stops an admin ban or the auto-banner.
- `FLUXER_ABUSE_IP_CLASS_CLAIM_ENABLED` defaults to `1` and gates the shared claim. It is read as a string, and only `0` turns the claim off.
- `FLUXER_ABUSE_IP_CLASS_CLAIM_TTL_SEC` defaults to `15` seconds and sets how long a replica holds that claim.
- `FLUXER_ABUSE_IP_CLASS_PENDING_TTL_MS` defaults to `20000` milliseconds and sets how long a replica that lost the claim waits before it tries again.
- `FLUXER_ABUSE_IP_CLASS_NEGATIVE_TTL_MS` defaults to `300000` milliseconds and sets how long a failed classification is remembered.
- `FLUXER_ABUSE_IP_CLASS_HINT_TTL_MS` defaults to `600000` milliseconds and sets how long a class sent by another replica stays usable.
Two names let the local MaxMind databases answer the registration risk lookup instead of ipinfo. `FLUXER_RISK_IPINFO_PRESCREEN_ENABLED` is off by default and only `1` or `true`, in any letter case, turns it on. `FLUXER_RISK_IPINFO_PRESCREEN_ALLOW_ASNS` is a comma-separated list of ASN numbers, empty by default, and non-numeric entries are dropped. The list is empty out of the box, so the pre-screen does nothing until an operator fills it in. An IP skips ipinfo only when the local city database returns a country, the local ASN database returns a number, that number is in the list, and the ASN organization name is not a commercial privacy provider, an education network or a cellular network. Everything else still goes to ipinfo.
The claim key is `abuse:ipclass:claim:` plus the ban key, and replicas send classes to each other on the `abuse_tracker:ipclass` key-value channel. The four numeric names are read through the same helper as the names above, so a non-finite value or one at or below zero falls back to the default.
A `FLUXER_IPINFO_BUDGET_` family caps what the instance spends at ipinfo. `FLUXER_IPINFO_BUDGET_ENABLED` defaults to `1`, and `0` turns off all shedding. `FLUXER_IPINFO_BUDGET_MONTHLY_MAX` defaults to `140000` and is the ceiling for one UTC calendar month. Lookups run at three priorities, each with a share of that ceiling and a token bucket for bursts refilled once a minute.
- Admin IP bans and scheduled deletion checks are critical. They reach the full ceiling, with a burst of `60` from `FLUXER_IPINFO_BUDGET_CRITICAL_BURST` refilled at `60` a minute by `FLUXER_IPINFO_BUDGET_CRITICAL_REFILL_PER_MIN`.
- Registration risk is standard. It stops at `FLUXER_IPINFO_BUDGET_STANDARD_MONTHLY_PCT` percent of the ceiling, default `90`, with a burst of `240` from `FLUXER_IPINFO_BUDGET_STANDARD_BURST` refilled at `120` a minute by `FLUXER_IPINFO_BUDGET_STANDARD_REFILL_PER_MIN`.
- The IP auto-banner is background. It stops at `FLUXER_IPINFO_BUDGET_BACKGROUND_MONTHLY_PCT` percent, default `60`, with a burst of `120` from `FLUXER_IPINFO_BUDGET_BACKGROUND_BURST` refilled at `30` a minute by `FLUXER_IPINFO_BUDGET_BACKGROUND_REFILL_PER_MIN`.
The lower ceilings mean background lookups stop first and critical lookups stop last. The counters live in the key-value store under `ipinfo:budget:burst:` and `ipinfo:budget:month:`. A shed lookup returns an unavailable result and raises no error, and any key-value failure admits the lookup at every priority, so a cache outage never stops an admin ban or the auto-banner.
Two names let the local MaxMind databases answer the registration risk lookup. `FLUXER_RISK_IPINFO_PRESCREEN_ENABLED` is off by default and only `1` or `true`, in any letter case, turns it on. `FLUXER_RISK_IPINFO_PRESCREEN_ALLOW_ASNS` is a comma-separated list of ASN numbers, empty by default, and non-numeric entries are dropped. The list is empty out of the box, so the pre-screen does nothing until an operator fills it in. An IP skips ipinfo only when the local city database returns a country, the local ASN database returns a number, that number is in the list, and the ASN organization name is not a commercial privacy provider, an education network or a cellular network. Everything else still goes to ipinfo.
## Limits
@@ -1508,7 +1526,7 @@ Default `8080`. The listen port. Compose sets it explicitly.
#### `FLUXER_STATIC_DIR`
Default `./static`. Where the client bundle lives. The name misleads. It is the SPA bundle directory, unrelated to `static-proxy`.
Default `./static`. Where the client's SPA bundle lives, unrelated to `static-proxy`.
#### `FLUXER_APP_PROXY_INDEX_UPSTREAM_URL`
@@ -1564,9 +1582,9 @@ Defaults to the crate version. The reported build. `BUILD_VERSION` is preferred
## Content Security Policy
`app-proxy` builds a per-request nonce-based policy for the client HTML and the assets it serves. Each variable appends sources to one directive on top of the built-in ones. All are empty by default, and Compose forwards all eleven.
`app-proxy` builds a per-request nonce-based policy for the client HTML and the assets it serves. Each variable appends sources to one directive on top of the built-in ones. All are empty by default, and Compose forwards every one.
Every name below goes in `.env`. `app-proxy` reads its environment at container start, so a change takes effect on `docker compose up -d app-proxy` and not on `docker compose restart app-proxy`.
Every name below goes in `.env`. `app-proxy` reads its environment at container start, so a change takes effect on `docker compose up -d app-proxy`. A `docker compose restart app-proxy` does not apply it.
`FLUXER_CSP_EXTRA_DEFAULT_SRC`, `FLUXER_CSP_EXTRA_CONNECT_SRC`, `FLUXER_CSP_EXTRA_IMG_SRC`, `FLUXER_CSP_EXTRA_MEDIA_SRC`, `FLUXER_CSP_EXTRA_FONT_SRC`, `FLUXER_CSP_EXTRA_SCRIPT_SRC`, `FLUXER_CSP_EXTRA_STYLE_SRC`, `FLUXER_CSP_EXTRA_FRAME_SRC`, `FLUXER_CSP_EXTRA_WORKER_SRC`, and `FLUXER_CSP_EXTRA_MANIFEST_SRC` take one or more sources separated by commas, spaces, tabs, or newlines. Blank entries and sources the directive already lists are dropped. `FLUXER_CSP_REPORT_URI` sets a single `report-uri` value.
@@ -1576,7 +1594,7 @@ A front proxy must not add a Content-Security-Policy of its own.
## Keys in .env.example that no service reads
`.env.example` has names that no Fluxer process ever sees. Docker Compose or the edge container consumes most of them, and each of those reaches a service under a different name. Setting one outside Compose, in Kubernetes or a systemd unit, does nothing. Each entry below says what consumes it.
Docker Compose or the edge container consumes most of the names below, and each of those reaches a service under a different name. Setting one outside Compose, in Kubernetes or a systemd unit, does nothing.
#### `FLUXER_DOMAIN`
@@ -1741,9 +1759,9 @@ The five internal services each run a router, which takes requests and holds no
## Resources
Each of the 25 services has a memory limit and four also have a memory reservation, all under `deploy.resources`. Compose reads `gb` as 1024 MiB and `mb` as 1 MiB, so `5gb` is 5368709120 bytes. Plain `docker compose up` applies both keys on a single host, with no Swarm and no `--compatibility` flag. The engine rejects any limit below `6mb`, and rejects a limit lower than the same service's reservation with `Minimum memory limit can not be less than memory reservation limit`.
Every service has a memory limit and four also have a memory reservation, all under `deploy.resources`. Compose reads `gb` as 1024 MiB and `mb` as 1 MiB, so `5gb` is 5368709120 bytes. Plain `docker compose up` applies both keys on a single host, with no Swarm and no `--compatibility` flag. The engine rejects any limit below `6mb`, and rejects a limit lower than the same service's reservation with `Minimum memory limit can not be less than memory reservation limit`.
A limit is a ceiling. The 25 limits below sum to 16.75 GiB and the stack does not need a host that large, because a container costs what it touches.
A limit is a ceiling. The limits below sum to 16.75 GiB and the stack does not need a host that large, because a container costs what it touches.
`deploy.resources.reservations.memory` becomes the container's cgroup v2 `memory.low`, which biases kernel reclaim toward other containers under host pressure. It reserves nothing on its own.
@@ -1751,7 +1769,7 @@ All are optional.
#### `FLUXER_CADDY_MEMORY_LIMIT`
Default `256mb`. The ceiling for `edge`. The name says Caddy and the service is named `edge`.
Default `256mb`. The ceiling for `edge`.
#### `FLUXER_POSTGRES_MEMORY_LIMIT`
@@ -1857,7 +1875,7 @@ Default `256mb`. The ceiling for `unfurl-shard`. Fetches remote pages, so a slow
Default `256mb`. The ceiling for `admin`. Serves the dashboard and proxies no media.
Four services have a reservation. Losing any of the four takes the instance down, so the kernel reclaims from everything else first. All are optional.
A reservation goes to the four services whose loss takes the instance down, so the kernel reclaims from everything else first. All are optional.
#### `FLUXER_POSTGRES_MEMORY_RESERVATION`
@@ -70,7 +70,7 @@ An unconfigured instance takes registrations from anyone, and lets any account t
## Step 3: Open the ports
Open six ports on the host:
Open these ports on the host:
| Port | Protocol | Used by |
| --- | --- | --- |
@@ -197,7 +197,7 @@ foreach ($p in '/_health', '/api/_health', '/gateway/_health', '/media/_health',
}
```
All six return 200. `/.well-known/fluxer` lists the endpoints clients use, with your own hostname in them.
Every path returns 200. `/.well-known/fluxer` lists the endpoints clients use, with your own hostname in them.
<a id="first-run"></a>
@@ -209,7 +209,7 @@ The wizard runs in two halves. Before you are signed in it walks a welcome step,
Create the owner account with an email address at a domain you control. On a self-hosted instance the first registration that supplies an email address receives the wildcard admin ACL, which is full access to the admin dashboard, unless registration is set to hold new accounts for approval. Finishing the wizard also grants the wildcard ACL to the account that completes it when that account holds no admin ACL yet.
`.env.example` ships `FLUXER_EMAIL_ENABLED=false`, which marks every address verified at creation and sends no mail at all. Nothing goes out by mail, so a forgotten owner password has no email reset. Record it, and register a passkey or a second admin account before you open registration.
`.env.example` ships `FLUXER_EMAIL_ENABLED=false`, which marks every address verified at creation and sends no mail at all. A forgotten owner password therefore has no email reset. Record it, and register a passkey or a second admin account before you open registration.
Then sign in to the admin dashboard at `https://chat.example.com/admin` with the account holding the wildcard ACL. The **Instance Config** page has everything the wizard asked, plus registration mode, approvals and integration keys. **Limit Config** holds the instance limits published to clients. **Voice Regions** and **Voice Servers** come seeded, so voice needs no setup there.
@@ -257,7 +257,7 @@ A dump and a tarball in `backups`, and every service back to `running`, is the s
### Remove the instance
:::danger[This deletes every account, message and upload]
`-v` deletes all seven named volumes. Every account, message, upload, search index and certificate the instance holds is gone, and no `docker compose up -d` brings any of it back. Take the copies above first. To upgrade rather than delete, the command is `sh install.sh --update`, below.
`-v` deletes all seven named volumes. Every account, message, upload, search index and certificate the instance holds is gone, and no `docker compose up -d` brings any of it back. Take the copies above first. To upgrade, the command is `sh install.sh --update`, below.
:::
Take the instance down and delete its data:
@@ -170,7 +170,7 @@ chat.example.com {
}
```
Caddy trusts no proxy by default and takes the client address from the connection. Set the global `trusted_proxies` option when another proxy or a CDN sits in front of Caddy, or it records that hop as the client. Caddy appends its own peer to the address list rather than replacing it, and Fluxer reads the first entry.
Caddy trusts no proxy by default and takes the client address from the connection. Set the global `trusted_proxies` option when another proxy or a CDN sits in front of Caddy, or it records that hop as the client. Caddy appends its own peer to the end of the address list, and Fluxer reads the first entry.
## Traefik
@@ -49,7 +49,7 @@ A failed run leaves the instance running on the images it already had.
An instance older than the `/livekit` routing needs one edit no upgrade can make for it. [Voice signalling moved to /livekit](#voice-signalling-moved-to-livekit) has it, and voice stays silent until it is made.
The refreshed `docker-compose.yml` renames the `caddy` service to `edge`, and the two publish the same host ports. `--remove-orphans` takes the old container down in the same call, so the new one can bind them. Without it the step stops with `Bind for 0.0.0.0:443 failed`. It also removes any container in the project whose service the loaded Compose files no longer define, so keep `COMPOSE_FILE` the same across an upgrade. [Keep a local compose change](#keep-a-local-compose-change) has the file layout that survives one, and [When the compose file list names a missing file](#when-the-compose-file-list-names-a-missing-file) has the failure a line naming an absent file produces.
The refreshed `docker-compose.yml` renames the `caddy` service to `edge`, and the two publish the same host ports. `--remove-orphans` takes the old container down in the same call, so the new one can bind them. Without it the step stops with `Bind for 0.0.0.0:443 failed`. It also removes any container in the project whose service the loaded Compose files no longer define, so keep `COMPOSE_FILE` the same across an upgrade. [Keep a local compose change](#keep-a-local-compose-change) has the file layout that survives one. [When the compose file list names a missing file](#when-the-compose-file-list-names-a-missing-file) has the failure a line naming an absent file produces.
The rename also moves the `caddy-data` and `caddy-config` volumes to `edge-data` and `edge-config`, so the old two are left unused and the edge requests its certificate again on the first start.
@@ -63,9 +63,9 @@ To write it by hand, run the generator in a shell:
printf '\nFLUXER_ERLANG_COOKIE=%s\n' "$(openssl rand -hex 32)" >> .env
```
Run that as a command. Pasting `FLUXER_ERLANG_COOKIE=$(openssl rand -hex 32)` into `.env` as text stores those characters as the value, because Compose reads a line literally and runs nothing in it. The leading newline is what keeps the key on its own line when the last line of `.env` ends without one, which an editor can leave behind and which would otherwise join the two.
Run that as a command. Pasting `FLUXER_ERLANG_COOKIE=$(openssl rand -hex 32)` into `.env` as text stores those characters as the value, because Compose reads a line literally and runs nothing in it. The leading newline keeps the key on its own line. An editor can leave the last line of `.env` without a newline, and the two would otherwise join.
`api` and `media-proxy` now refuse to start unless `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64` decodes to at least 32 bytes. The line has been in `.env.example` for a while as `CHANGE_ME`, which decodes to 6 bytes, and nothing read it before, so an instance can be running today with the placeholder. `--update` replaces an absent or `CHANGE_ME` value with a generated one before it reads anything. Both services read the same value, so they take one secret rather than two, and writing it in one place is what gives them that. A `CHANGE_ME` value stops `api` at boot with `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must decode to at least 32 bytes`, and an empty one with `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required for the API`.
`api` and `media-proxy` now refuse to start unless `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64` decodes to at least 32 bytes. The line has been in `.env.example` for a while as `CHANGE_ME`, which decodes to 6 bytes, and nothing read it before, so an instance can be running today with the placeholder. `--update` replaces an absent or `CHANGE_ME` value with a generated one before it reads anything. Both services read the same value from one `.env` line. A `CHANGE_ME` value stops `api` at boot with `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must decode to at least 32 bytes`, and an empty one with `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required for the API`.
To write it by hand, run the generator in a shell the same way:
@@ -87,11 +87,11 @@ The supported way to hold a local choice is a separate file, listed in `COMPOSE_
COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml:local.compose.yml
```
Compose merges the files left to right, so `local.compose.yml` wins over the ones before it. An upgrade refreshes the five stack files and nothing else, so a file outside that set survives every upgrade untouched. A record copies `.env` and those five files, and no other file from the working directory, so an override file is never backed up with them and belongs wherever the rest of the configuration lives.
Compose merges the files left to right, so `local.compose.yml` wins over the ones before it. An upgrade refreshes the five stack files and nothing else, so a file outside that set survives every upgrade untouched. A record copies `.env` and those five files, and no other file from the working directory. An override file is never backed up with them, and it belongs wherever the rest of the configuration lives.
`.env` needs no such file. The only value either upgrade mode replaces there is `FLUXER_IMAGE_TAG`, and the Mint step adds a required key the file does not hold at all.
[Enable the overlay](/operator/reverse-proxy/#enable-the-overlay) has the overlay this is most often used for, and [Docker labels](/operator/reverse-proxy/#docker-labels) has a worked third file.
[Enable the overlay](/operator/reverse-proxy/#enable-the-overlay) has the overlay this is most often used for. [Docker labels](/operator/reverse-proxy/#docker-labels) has a worked third file.
## When the compose file list names a missing file
@@ -127,7 +127,15 @@ cd ~/fluxer
sh install.sh --update --dry-run
```
It prints the images the stack runs now, the backup it intends to take, which stack files the ref changes, which services that makes it restart, and any refusal that would stop the run. It writes nothing outside a temporary directory it removes on exit.
It prints:
- The images the stack runs now
- The backup it intends to take
- The stack files the ref changes
- The services those changes make it restart
- Any refusal that would stop the run
It writes nothing outside a temporary directory it removes on exit.
Then run it:
@@ -190,7 +198,7 @@ Each upgrade writes one record directory, named `record-` and a UTC stamp, under
| `seaweedfs-data.tgz` | Every upload, avatar, report and harvest |
| `.env` | Every secret the instance was built with |
The five stack files sit in the record beside those three, so a rollback puts back the exact files the instance was running. The record directory is created `0700` and the `.env` copy inside it is `0600`. Keep records wherever you already keep secrets.
The five stack files sit in the record beside them, so a rollback puts back the exact files the instance was running. The record directory is created `0700` and the `.env` copy inside it is `0600`. Keep records wherever you already keep secrets.
The dump costs no downtime. The uploads copy stops the stack, and the script starts the stack again on the old images before it goes any further.
@@ -215,7 +223,7 @@ sh install.sh --rollback
On Windows it is `.\install.ps1 -Rollback`. It takes the newest record, puts back the images and the stack files it holds, recreates, restarts `edge`, and verifies. `--dry-run` prints that plan too.
A rollback never pulls, so it needs the old images still on the host. Run [Reclaim disk](#reclaim-disk) once an upgrade satisfies you and not before.
A rollback never pulls, so it needs the old images still on the host. Run [Reclaim disk](#reclaim-disk) only after an upgrade satisfies you.
The database does not move. `api`, `worker`, `users-shard` and `messages-shard` apply schema work in place while they start, and an older image does not undo it. Across a release that changed the schema, the dump in the record is the only way back, and putting it back is a decision you make.
@@ -312,7 +320,7 @@ Each upgrade leaves the previous images behind, which adds up to a few gigabytes
docker image prune -f
```
On the default moving tag, the pull moves `v1` onto the new images and leaves the ones it replaced untagged, so this removes them. Those are the images a rollback on a moving tag needs, so prune only once the upgrade satisfies you. A pinned release keeps its own tag and survives the prune. Remove one of those by naming its tag in `docker image rm`.
On the default moving tag, the pull moves `v1` onto the new images and leaves the ones it replaced untagged, so this removes them. A rollback on a moving tag needs those images. Prune only once the upgrade satisfies you. A pinned release keeps its own tag and survives the prune. Remove one of those by naming its tag in `docker image rm`.
`docker system prune -a` reclaims more. It also deletes every image that no container references, including ones unrelated to Fluxer.
+10 -6
View File
@@ -14,7 +14,7 @@ Snowflake values exceed the exact integer range of a JSON double, so parsing one
## Format
A snowflake packs three fields into 64 bits. The worker and sequence fields distinguish identifiers minted during the same millisecond. Bit 63 is always zero, so an issued snowflake fits a signed 64-bit integer.
A snowflake packs a timestamp, a worker ID, and a sequence into 64 bits. The worker and sequence fields distinguish identifiers minted during the same millisecond. Bit 63 is always zero, so an issued snowflake fits a signed 64-bit integer.
| Field | Bits | Description |
| --- | --- | --- |
@@ -65,9 +65,7 @@ Fluxer emits a snowflake as an unsigned decimal string in every JSON body, path
Both HTTP validation codes are element codes inside a 400 [`INVALID_FORM_BODY`](/http-api/errors/#api-error-code-registry) response. Each Gateway [command](/gateway/commands/) states what its own rejection does, from discarding the field to abandoning the whole command.
A snowflake sent as a JSON number keeps its exact value at any size.
A fractional JSON number is an ordinary type failure and reports [`INVALID_FORMAT`](/http-api/errors/#validation-error-code-registry). An exponent form is read as the number it denotes, and one above 9007199254740991 reports [`INVALID_SNOWFLAKE_FORMAT`](/http-api/errors/#validation-error-code-registry). A path parameter and a query string parameter always arrive as text.
A snowflake sent as a JSON number keeps its exact value at any size. A fractional JSON number is an ordinary type failure and reports [`INVALID_FORMAT`](/http-api/errors/#validation-error-code-registry). Fluxer reads an exponent form as the number it denotes, and one above 9007199254740991 reports [`INVALID_SNOWFLAKE_FORMAT`](/http-api/errors/#validation-error-code-registry). Path parameters and query string parameters always arrive as text.
:::note[`0` is the beginning of snowflake time]
An HTTP pagination cursor accepts `0`. An HTTP path parameter or field that identifies a real resource also accepts `0`, and the request then receives the ordinary not-found result for that resource.
@@ -77,9 +75,15 @@ An HTTP pagination cursor accepts `0`. An HTTP path parameter or field that iden
A collection endpoint that pages over a snowflake-ordered resource accepts a cursor such as `before`, `after`, or `around`. The endpoint defines which cursors it supports, whether they are mutually exclusive, the result order, and how `limit` is applied.
The `before` cursor selects identifiers lower than the cursor value, and the `after` cursor selects identifiers higher than it. Both exclude the cursor value itself. The `around` cursor selects a window centred on the cursor value and includes that value. [List channel messages](/http-api/messages/#list-channel-messages) accepts it as a query string parameter and [List messages from multiple channels](/http-api/messages/#list-messages-from-multiple-channels) accepts it on each entry of its request body. No other operation accepts it.
| Cursor | Selects |
| --- | --- |
| `before` | Identifiers lower than the cursor value, excluding it |
| `after` | Identifiers higher than the cursor value, excluding it |
| `around` | A window centred on the cursor value, including it |
The type of a `before` or `after` cursor follows the operation. [List pinned messages](/http-api/messages/#list-pinned-messages) pages on the pin time, so its `before` is an ISO 8601 timestamp rather than a message ID. [List blocklist entries](/admin-api/blocklists/#list-blocklist-entries) pages on the entry value, and its `after` is the stored value of the last entry on the previous page. Each operation states the type of its own cursors.
[List channel messages](/http-api/messages/#list-channel-messages) accepts `around` as a query string parameter, and [List messages from multiple channels](/http-api/messages/#list-messages-from-multiple-channels) accepts it on each entry of its request body. No other operation accepts it.
The type of a `before` or `after` cursor follows the operation. [List pinned messages](/http-api/messages/#list-pinned-messages) pages on the pin time, so its `before` is an ISO 8601 timestamp. [List blocklist entries](/admin-api/blocklists/#list-blocklist-entries) pages on the entry value. Its `after` is the stored value of the last entry on the previous page. Each operation states the type of its own cursors.
Because the timestamp occupies the high bits, numeric snowflake order is creation time order at millisecond resolution.
@@ -30,19 +30,17 @@ The following operations verify a CAPTCHA while the instance enforces verificati
Create private channel is gated only on the group direct message path, where the request body has a `recipients` member. A one-to-one direct message request omits the field and is never gated.
Create application, redeem gift, create private channel, and add group direct message recipient reject an unauthenticated request before Fluxer reads the CAPTCHA. The three authentication operations accept a request with no credential, and each of them returns 403 `SSO_REQUIRED` before Fluxer reads the CAPTCHA when the instance enforces single sign-on.
Create application, redeem gift, create private channel, and add group direct message recipient reject an unauthenticated request before Fluxer reads the CAPTCHA. The authentication operations accept a request with no credential. When the instance enforces single sign-on, each of them returns 403 `SSO_REQUIRED` before Fluxer reads the CAPTCHA.
## Exemption
Two exemptions skip the challenge. Fluxer tests both of them before it reads the token. A request that passes either one proceeds as though the instance had no provider configured.
Two exemptions skip the challenge. Fluxer tests both before it reads the token. A request that passes either one proceeds as though the instance had no provider configured.
The first exemption is the `captcha_exempt` capability the instance account policy grants to a contact address. A policy rule matches the address itself or the domain it belongs to, so one grant can cover a whole domain. Fluxer tests it against the resolved account's email address alone, so an unauthenticated request never matches this exemption.
The instance account policy grants the `captcha_exempt` capability to a contact address. A policy rule matches the address itself or the domain it belongs to, so one grant can cover a whole domain. Fluxer tests the capability against the resolved account's email address alone. An unauthenticated request never matches this exemption.
The second exemption is the `APP_STORE_REVIEWER` user flag. Fluxer tests it against the resolved account, then against the account an `email` member of the request body resolves to.
The other exemption is the `APP_STORE_REVIEWER` user flag. Fluxer tests it against the resolved account, then against the account an `email` member of the request body resolves to. The body check parses the request body as JSON and reads a string `email` member, and a body that is absent, is not JSON, or is not a JSON object yields no address. That check exempts a login or a registration attempt before any account is resolved.
The body check parses the request body as JSON and reads a string `email` member, and a body that is absent, is not JSON, or is not a JSON object yields no address. That check exempts a login or a registration attempt before any account is resolved.
The exemptions run before request validation on the three authentication operations, on create application, and on redeem gift. Create private channel and add group direct message recipient validate the request first, so an invalid request is rejected before any exemption is tested.
The exemptions run before request validation on the authentication operations, on create application, and on redeem gift. Create private channel and add group direct message recipient validate the request first, so an invalid request is rejected before any exemption is tested.
No exemption is visible in an API response. A client cannot predict one and handles a challenge on every gated operation.
+15 -5
View File
@@ -8,7 +8,7 @@ A locale tells Fluxer which language to write human-readable text in. Each one i
## Supported locales
This registry is the complete set for every Fluxer surface. Wherever Fluxer stores a locale, such as in [user settings](/http-api/users/settings/), it requires an exact value from this table.
The registry below is the complete set for every Fluxer surface. Wherever Fluxer stores a locale, such as in [user settings](/http-api/users/settings/), it requires an exact value from this table.
| Value | Description |
| --- | --- |
@@ -57,17 +57,27 @@ Fluxer resolves the response locale once for each request. When a request resolv
An account created by password registration stores the locale negotiated from its own registration request, so the `Accept-Language` header on that request sets the stored value. An account provisioned through single sign-on stores no account locale. Its [user settings](/http-api/users/settings/) locale reads `en-US`, and its requests negotiate `Accept-Language` until the locale setting is changed.
Fluxer splits the header on commas. It trims each member and then splits it on semicolons. The text before the first semicolon is the language range, and Fluxer reads only the first parameter after it, looking for a `q=` weight. A member with no readable `q=` value has weight 1. Fluxer orders the members by descending weight, and members of equal weight keep their header order.
Fluxer splits the header on commas. It trims each member and then splits it on semicolons. The text before the first semicolon is the language range, and Fluxer reads a `q=` weight from only the first parameter after it. A member with no readable `q=` value has weight 1. Fluxer orders the members by descending weight, and members of equal weight keep their header order.
Fluxer then runs two passes over that ordered list.
The first pass takes the earliest member whose range names a registry value exactly. Fluxer trims the range, replaces every underscore with a hyphen, and lowercases it before comparing, so `EN-GB` and `en_gb` both name `en-GB`. The bare tags `en` and `sv` are registered aliases for `en-US` and `sv-SE` and match in this pass.
The second pass runs only when the first selects nothing. It reduces each member in the same order to its language subtag. A language subtag with a declared preference selects that value. The declared preferences are `en` to `en-US`, `es` to `es-ES`, `pt` to `pt-BR`, `zh` to `zh-CN`, and `sv` to `sv-SE`, so `en-AU` selects `en-US` and `pt-PT` selects `pt-BR`. A language subtag without a declared preference selects the first registry value whose tag begins with that subtag and a hyphen.
The second pass runs only when the first selects nothing. It reduces each member in the same order to its language subtag. A language subtag with a declared preference selects that value. The declared preferences are:
No registry tag begins with a subtag outside those five followed by a hyphen, so `de-AT` and `xx-YY` both select nothing. A weight of 0 orders a member last, and that member can still be selected. A range of `*` matches no registry value.
| Language subtag | Selected locale |
| --- | --- |
| `en` | `en-US` |
| `es` | `es-ES` |
| `pt` | `pt-BR` |
| `zh` | `zh-CN` |
| `sv` | `sv-SE` |
The resolved locale selects the localised `message` in an [error response](/http-api/#error-response) and in each element of a validation `errors` array. The `code` field is never localised.
Under those preferences, `en-AU` selects `en-US` and `pt-PT` selects `pt-BR`. A language subtag without a declared preference selects the first registry value whose tag begins with that subtag and a hyphen.
Every registry tag with a hyphen begins with one of those five subtags, so `de-AT` and `xx-YY` both select nothing. A weight of 0 orders a member last, and that member can still be selected. A range of `*` matches no registry value.
The resolved locale selects the localised `message` in an [error response](/http-api/#error-response) and in each element of a validation `errors` array.
:::note[Locale selection changes human-readable text only]
A field name, an enumeration value, an error `code`, a [snowflake](/snowflakes/), and a timestamp representation are identical under every locale.
@@ -16,11 +16,11 @@ A request that resolves no account is keyed by the client IP address, exactly fo
Fluxer also evaluates a route bucket against the global bucket unless the route declares that bucket exempt. The global bucket is keyed by the same identity, so a request that resolves no account consumes the global allowance of its client IP address.
Seven buckets are exempt, and each is the only bucket its route declares. Those routes draw on no global allowance: `webhook:execute::webhook_id`, `webhook:message_get::webhook_id`, `webhook:message_edit::webhook_id`, `webhook:message_delete::webhook_id`, `webhook:github::webhook_id`, `webhook:instatus::webhook_id`, and `stripe:webhook`. The `user:group_dm:create` and `user:group_dm:recipient:add` buckets are exempt as well. Each sits on a route that already consumed a non-exempt bucket, so both routes still draw on the global allowance.
Seven buckets are exempt, and each is the only bucket its route declares: `webhook:execute::webhook_id`, `webhook:message_get::webhook_id`, `webhook:message_edit::webhook_id`, `webhook:message_delete::webhook_id`, `webhook:github::webhook_id`, `webhook:instatus::webhook_id`, and `stripe:webhook`. Those routes draw on no global allowance. The `user:group_dm:create` and `user:group_dm:recipient:add` buckets are exempt as well. Each sits on a route that already consumed a non-exempt bucket, so both routes still draw on the global allowance.
Every HTTP API and Admin API operation declares a bucket, apart from the seven [desktop download](/http-api/downloads/) routes, which declare none. A caller that sends no credential on a [Bluesky client document](/http-api/connections/#get-bluesky-client-metadata) is keyed by the client IP address.
The global window is one second. The default allowance is 50 requests per second, and an account holding the [`HIGH_GLOBAL_RATE_LIMIT`](/admin-api/users/#account-flags) flag receives 1,200 requests per second instead. An account holding the [`RATE_LIMIT_BYPASS`](/admin-api/users/#account-flags) flag is evaluated against neither the global bucket nor any route bucket, and receives no rate limit header on a successful response.
The global window is one second. The default allowance is 50 requests per second, and an account holding the [`HIGH_GLOBAL_RATE_LIMIT`](/admin-api/users/#account-flags) flag receives 1,200 requests per second instead. The [`RATE_LIMIT_BYPASS`](/admin-api/users/#account-flags) flag exempts an account from the global bucket and from every route bucket. A successful response to that account has no rate limit header.
Some operations enforce a further limit inside the handler. `RATE_LIMIT_BYPASS` exempts an account from none of them. [Limits enforced inside a handler](#limits-enforced-inside-a-handler) has the complete set.
@@ -131,9 +131,9 @@ The [cross-origin policy](/http-api/#cross-origin-requests) exposes only `X-Flux
## Limits enforced inside a handler
Some operations bound a further allowance inside the handler. Each one is keyed independently of the route bucket and of the global bucket, so exhausting it denies the request while both buckets still have room. The set below is complete.
An allowance enforced inside a handler is keyed independently of the route bucket and of the global bucket, so exhausting it denies the request while both buckets still have room. The set below is complete.
Two deployment switches disable part of this set. `disable_rate_limits` turns off the two login allowances along with both buckets. `relax_registration_rate_limits` turns off the three registration allowances. Every other allowance below is enforced on every deployment.
The `disable_rate_limits` deployment switch turns off the two login allowances along with both buckets. `relax_registration_rate_limits` turns off the three registration allowances. Every other allowance below is enforced on every deployment.
A denial takes one of two shapes. A send or submission allowance answers 429 with the [rate limit response object](#rate-limit-response-object) and the [rate limit headers](#rate-limit-headers) minus `X-RateLimit-Bucket`. A change allowance answers 400 `INVALID_FORM_BODY` with one [validation error](/http-api/#validation-error-object) entry whose `code` names the exhausted allowance.
@@ -195,7 +195,7 @@ Fluxer consumes every multi-factor allowance before it checks the code, so a cor
### Allowances answering neither shape
[Get desktop handoff information](/http-api/authentication/#get-desktop-handoff-information) and [Complete desktop handoff](/http-api/authentication/#complete-desktop-handoff) share one failed-attempt counter keyed by the client IP address. Five failures block both operations for 15 minutes from the most recent failure, and a blocked request returns 400 `INVALID_HANDOFF_CODE` as a top-level code rather than as a validation entry. Get desktop handoff information separately permits three successful lookups for each handoff code and reports a fourth with the same top-level code.
[Get desktop handoff information](/http-api/authentication/#get-desktop-handoff-information) and [Complete desktop handoff](/http-api/authentication/#complete-desktop-handoff) share one failed-attempt counter keyed by the client IP address. Five failures block both operations for 15 minutes from the most recent failure, and a blocked request returns 400 `INVALID_HANDOFF_CODE` as a top-level code with no validation entry. Get desktop handoff information separately permits three successful lookups for each handoff code and reports a fourth with the same top-level code.
[Refund latest purchase](/http-api/billing/#refund-latest-purchase) permits one self-serve refund every 30 days for each account and reports a request inside that window as 403 `STRIPE_REFUND_COOLDOWN_ACTIVE`.
+11 -13
View File
@@ -35,7 +35,7 @@ The response returns one entry for each declaration, in request order, discrimin
## Part geometry
Fluxer derives multipart geometry from the file size. The part size is the declared size divided by 20, rounded up to a whole mebibyte, and never below 10485760 bytes. Every part is exactly `part_size` bytes except the last, which is the remainder.
The multipart `part_size` is the declared file size divided by 20, rounded up to a whole mebibyte, and never below 10485760 bytes. Every part is exactly `part_size` bytes except the last, which is the remainder.
A plan is bounded at 10,000 parts. One that would need more returns 400 `FILE_SIZE_TOO_LARGE` before the storage multipart upload is opened. The resolved file size limit is the only binding constraint.
@@ -43,9 +43,9 @@ A plan is bounded at 10,000 parts. One that would need more returns 400 `FILE_SI
Each `upload_url` is a `PUT` target with its own authorisation in its query string. A direct storage URL has the object store's own signature and a relay URL has the signed relay capability in its `t` parameter, so neither shape reads an `Authorization` header.
The direct storage capability signs the exact byte count, so a `PUT` of any other length is rejected. A relay capability bounds the length at the same value and answers 413 above it, and the relay applies its own body ceiling, 500 MiB by default, on top of that. A transfer that declares no `Content-Length` is spooled to the smaller of the two bounds and answers 413 past it. A relay request whose capability is missing, malformed, or expired answers 401. A client sends exactly the authorised byte count either way.
The direct storage capability signs the exact byte count, so a `PUT` of any other length is rejected. A relay capability bounds the length at the same value and answers 413 above it, and the relay applies its own body ceiling, 500 MiB by default, on top of that. Without a declared `Content-Length`, the relay spools the body to the smaller of the two bounds and answers 413 past it. The relay answers 401 when the capability is missing, malformed, or expired. Either way, a client sends exactly the authorised byte count.
A singlepart transfer sends the whole file and must send the entry's `content_type` as its `Content-Type` header. A relay capability takes the media type from the capability and ignores the header. A multipart part transfer sends only that part's bytes and has no signed media type.
A singlepart transfer sends the whole file and must send the entry's `content_type` as its `Content-Type` header. The relay takes the media type from its capability and ignores the header. A multipart part transfer sends only that part's bytes and has no signed media type.
The instance decides per request whether to relay. It resolves the caller's country from the client IP address and issues a direct storage URL only when that country is on the deployment's direct-upload list. Every other caller, including one whose geolocation lookup fails, receives a URL on the [upload relay](/media-proxy/upload-relay/). A client treats both shapes the same way and MUST NOT parse, rewrite, or reorder the query string of either.
@@ -80,25 +80,21 @@ Two failures are reported as [validation error object](/http-api/#validation-err
<sup>2</sup> The multipart upload is aborted before the error is returned
Fluxer then sums the listed part sizes. A total above the resolved file size limit aborts the upload and returns 400 `FILE_SIZE_TOO_LARGE`. A storage failure during assembly aborts it as well. An aborted upload discards its parts, and its `upload_filename` can never be claimed. A client requests a new plan for the file.
A client never sends a singlepart upload to this operation.
Fluxer then sums the listed part sizes. A total above the resolved file size limit aborts the upload and returns 400 `FILE_SIZE_TOO_LARGE`. A storage failure during assembly aborts it as well. An aborted upload discards its parts, and its `upload_filename` can never be claimed, so a client requests a new plan for the file.
## Claiming the upload
Nothing before this step creates a message, changes a channel, or emits a Gateway Dispatch. The attachment exists only once a [Create message](/http-api/messages/#create-message) or [Modify message](/http-api/messages/#modify-message) request has the `upload_filename` in a [pre-uploaded attachment](/http-api/messages/#pre-uploaded-attachment-object) entry.
An upload is bound to the identity and the channel that planned it. A key the authenticated identity does not own, a key planned for another channel, and a key an attachment has already consumed each return 400 `INVALID_FORM_BODY` with `UPLOADED_ATTACHMENT_NOT_FOUND` on `attachments.{index}.upload_filename`. A key whose object is absent from storage, which is what an untransferred plan leaves behind, returns the same status with `FILE_NOT_FOUND` on the same path.
A key is single use, and a second claim of the same key returns the same `UPLOADED_ATTACHMENT_NOT_FOUND` entry.
An upload is bound to the identity and the channel that planned it, and a key is single use. A key the authenticated identity does not own, a key planned for another channel, and a key an attachment has already consumed each return 400 `INVALID_FORM_BODY` with `UPLOADED_ATTACHMENT_NOT_FOUND` on `attachments.{index}.upload_filename`. Where the object is absent from storage, which is what an untransferred plan leaves behind, the claim returns the same status with `FILE_NOT_FOUND` on the same path.
:::caution[There is no resume operation]
A capability that expires cannot be refreshed and a plan cannot be re-read. A client that loses its plan, or whose part capabilities expire mid transfer, requests a new plan for the file and starts again.
Once a capability expires it cannot be refreshed, and a plan cannot be re-read. A client that loses its plan, or whose part capabilities expire mid transfer, requests a new plan for the file and starts again.
:::
## Stream previews
A stream preview is a still image attached to one voice connection. It does not use the attachment flow. A preview belongs to the voice connection that publishes the stream. The [stream key](/http-api/streams/#stream-key) names the scope, the channel, and that connection ID. Every preview operation is user-only.
A stream preview is a still image attached to one voice connection, the one that publishes the stream. It does not use the attachment flow. The [stream key](/http-api/streams/#stream-key) names the scope, the channel, and that connection ID. Every preview operation is user-only.
Uploading the image, issuing an upload capability, and deleting the preview each require the caller to hold a voice state in that channel whose connection ID matches the key. In a guild channel they require the [STREAM](/http-api/permissions/) permission as well. Reading the preview requires only access to the channel, and [CONNECT](/http-api/permissions/) in a guild channel, so any member who can join can read it.
@@ -106,11 +102,13 @@ Uploading the image, issuing an upload capability, and deleting the preview each
A voice state has no flag the check reads, so a caller can upload and read a preview for a connection that is publishing nothing.
:::
[Upload stream preview](/http-api/streams/#upload-stream-preview) posts the image inline. Its JSON body has the `channel_id` the connection is in, the base64-encoded image in `thumbnail` of 1 through 2000000 characters, and an optional `content_type` of 1 through 64 characters. A `thumbnail` that is not canonical base64 returns 400 `INVALID_STREAM_THUMBNAIL_PAYLOAD`. Canonical base64 here means the standard alphabet, a length that is a multiple of four, at most two trailing `=`, and decoded bytes that re-encode to the string the request sent. A `content_type` containing `jpeg` or `jpg` in any case is accepted without inspecting the bytes. Every other value, an absent field included, is accepted only when the decoded bytes begin with `FF D8` and end with `FF D9`. A failure returns 400 `PREVIEW_MUST_BE_JPEG`. Decoded bytes above 1000000 return 400 `FILE_SIZE_TOO_LARGE`.
[Upload stream preview](/http-api/streams/#upload-stream-preview) posts the image inline. Its JSON body has the `channel_id` the connection is in, the base64-encoded image in `thumbnail` of 1 through 2000000 characters, and an optional `content_type` of 1 through 64 characters. A `thumbnail` that is not canonical base64 returns 400 `INVALID_STREAM_THUMBNAIL_PAYLOAD`. Canonical base64 here means the standard alphabet, a length that is a multiple of four, at most two trailing `=`, and decoded bytes that re-encode to the string the request sent.
A `content_type` containing `jpeg` or `jpg` in any case is accepted without inspecting the bytes. Every other value, an absent field included, is accepted only when the decoded bytes begin with `FF D8` and end with `FF D9`. A failure returns 400 `PREVIEW_MUST_BE_JPEG`. Decoded bytes above 1000000 return 400 `FILE_SIZE_TOO_LARGE`.
The operation answers 204 once the image is accepted. Fluxer absorbs a transient storage failure, so a 204 confirms acceptance alone.
[Create stream preview upload URL](/http-api/streams/#create-stream-preview-upload-url) instead issues a reusable `PUT` capability for the same purpose. Its `content_type` must contain `jpeg` or `jpg` in any case, and every other value returns 400 `PREVIEW_MUST_BE_JPEG`. It answers with `upload_url`, `method` fixed to `PUT`, the `content_type` the client sends, `expires_at`, `expires_in`, and `max_bytes`, which is always 1000000. A direct storage capability lasts one day and a relay capability lasts the relay token lifetime, so `expires_in` differs between the two shapes. The capability writes the same object every time it is used, and a publisher refreshes the thumbnail without asking for a new URL.
[Create stream preview upload URL](/http-api/streams/#create-stream-preview-upload-url) issues a reusable `PUT` capability for the same purpose. Its `content_type` must contain `jpeg` or `jpg` in any case, and every other value returns 400 `PREVIEW_MUST_BE_JPEG`. It answers with `upload_url`, `method` fixed to `PUT`, the `content_type` the client sends, `expires_at`, `expires_in`, and `max_bytes`, which is always 1000000. A direct storage capability lasts one day and a relay capability lasts the relay token lifetime, so `expires_in` differs between the two shapes. The capability writes the same object every time it is used, and a publisher refreshes the thumbnail without asking for a new URL.
Nothing inspects the bytes written through that capability. A relay capability still refuses a declared length above `max_bytes` with 413, and a direct storage capability enforces nothing beyond its signed media type. A publisher encodes a valid JPEG of at most 1000000 bytes itself.
+7 -7
View File
@@ -48,7 +48,7 @@ The grant also names the track sources the connection may publish. SPEAK admits
A deployment can be configured without voice. The [instance discovery document](/http-api/instance/#instance-features-object) reports that state as `features.voice_enabled`. No other surface warns a client in advance.
A deployment that reports false mints no media credential, so a placement request is refused with `VOICE_TOKEN_FAILED`. [List RTC regions](/http-api/channels/#list-rtc-regions) answers 200 with an empty array before it resolves the channel, and [Modify call region](/http-api/calls/#modify-call-region) accepts any region string.
Where `features.voice_enabled` is false, Fluxer mints no media credential, so a placement request is refused with `VOICE_TOKEN_FAILED`. [List RTC regions](/http-api/channels/#list-rtc-regions) answers 200 with an empty array before it resolves the channel, and [Modify call region](/http-api/calls/#modify-call-region) accepts any region string.
## Placement
@@ -105,7 +105,7 @@ A grant is evaluated when it is minted, and the guild re-evaluates a connection
The guild applies the new result to a live connection and issues no new [Voice Server Update](/gateway/events/#voice-server-update). The media server mutes a published microphone, camera, or screen share track the member may no longer publish, and drops a connection that fails the VIEW_CHANNEL and CONNECT check.
Moderation is an HTTP operation on the guild membership. [Modify guild member](/http-api/guild-members/#modify-guild-member) and [Modify current guild member](/http-api/guild-members/#modify-current-guild-member) share one request body. Each applies a moderator mute, applies a moderator deafen, moves a member between guild voice channels, and forces a disconnect. Both hold the caller to MUTE_MEMBERS, DEAFEN_MEMBERS, and MOVE_MEMBERS, including when the target is the caller itself. No other HTTP route and no Gateway command does any of it.
Moderation is an HTTP operation on the guild membership. [Modify guild member](/http-api/guild-members/#modify-guild-member) and [Modify current guild member](/http-api/guild-members/#modify-current-guild-member) share one request body. Each applies a moderator mute and a moderator deafen, moves a member between guild voice channels, and forces a disconnect. Both hold the caller to MUTE_MEMBERS, DEAFEN_MEMBERS, and MOVE_MEMBERS, including when the target is the caller itself. No other HTTP route and no Gateway command does any of it.
That mute and that deafen reach the media server without a new credential. The change applies to every connection the account holds in that channel, and no [Voice Server Update](/gateway/events/#voice-server-update) follows.
@@ -125,7 +125,7 @@ That mute and that deafen reach the media server without a new credential. The c
A private call reads no `voice_connection_limit` and applies a fixed ceiling of 5 connections for each member.
A member whose `communication_disabled_until` is still in the future is refused with `VOICE_MEMBER_TIMED_OUT` before any permission or capacity check runs. An account that has never claimed its credentials is refused with `VOICE_UNCLAIMED_ACCOUNT` for a one-on-one direct message call and for any guild voice channel whose guild it does not own. A group direct message call is not refused. A session that did not identify with `e2ee_capable` is refused with `VOICE_E2EE_REQUIRED` while the guild has voice encryption enabled and every connection already in the channel is capable, and a bot is exempt from that one.
A member whose `communication_disabled_until` is still in the future is refused with `VOICE_MEMBER_TIMED_OUT` before any permission or capacity check runs. An account that has never claimed its credentials is refused with `VOICE_UNCLAIMED_ACCOUNT` for a one-on-one direct message call and for any guild voice channel whose guild it does not own. A group direct message call is not refused. A session that did not identify with `e2ee_capable` is refused with `VOICE_E2EE_REQUIRED` while the guild has voice encryption enabled and every connection already in the channel is capable. A bot is exempt from that one.
### Regions
@@ -133,7 +133,7 @@ A member whose `communication_disabled_until` is still in the future is refused
`rtc_region` is written by [Modify channel](/http-api/channels/#modify-channel) and requires UPDATE_RTC_REGION. A null value selects automatic routing, and so does a stored value the placing account cannot reach.
The first placement in the channel pins one voice server for it, and every later placement inherits that pinned server whatever its own coordinates are. A placement that finds no usable pin takes the accessible server nearest to the `latitude` and `longitude` the placement command supplied. A command that supplied no usable coordinates falls back to the deployment's default region, and then to the first accessible region.
The first placement in the channel pins one voice server for it, and every later placement inherits that pinned server whatever its own coordinates are. A placement that finds no usable pin takes the accessible server nearest to the `latitude` and `longitude` the placement command supplied. Where the command supplied no usable coordinates, the placement falls back to the deployment's default region, and then to the first accessible region.
The pin drops when the channel's `rtc_region` changes, when a call changes region, when the pinned server stops being accessible, or when the media server reports the room finished. That last case also disconnects every connection in a guild voice channel.
@@ -157,7 +157,7 @@ A recipient's [incoming call flags](/http-api/users/#incoming-call-flags) decide
Going live publishes a screen share track, and its screen share audio track, on the LiveKit participant the member already holds in the channel. There is no second connection, no second participant, and no second `connection_id`. A member that held STREAM at placement already has both screen share sources in its grant, so no new credential is minted and no [Voice Server Update](/gateway/events/#voice-server-update) follows.
The publisher advertises the stream by setting `self_stream` on that connection with [Voice State Update](/gateway/commands/#voice-state-update), naming the connection's own `connection_id`. The server bumps the voice state `version` and rebroadcasts the state as one [Voice State Update](/gateway/events/#voice-state-update).
The publisher advertises the stream by setting `self_stream` on that connection with [Voice State Update](/gateway/commands/#voice-state-update), naming the connection's own `connection_id`. The server increments the voice state `version` and rebroadcasts the state as one [Voice State Update](/gateway/events/#voice-state-update).
The voice state of a connection without STREAM in its channel has `self_stream` and `self_video` false, whatever the client sent. The guild clears both when a live connection loses STREAM, and rebroadcasts the state.
@@ -175,11 +175,11 @@ Fluxer removes screen share audio from the grant together with screen share, and
## Entrance sounds
An entrance sound is a short clip an account plays for everyone already connected to a voice channel. An account keeps a personal library of at most eight clips, each 100 through 5200 milliseconds and at most 1048576 decoded bytes, stored as `mp3`, `ogg`, `m4a`, or `wav`. It then assigns one clip per [scope](/http-api/entrance-sounds/#entrance-sound-scopes), and the four scopes are `global`, `guilds`, `dms`, and `guild:{guild_id}`.
An entrance sound is a short clip an account plays for everyone already connected to a voice channel. An account keeps a personal library of at most eight clips, each 100 through 5200 milliseconds and at most 1048576 decoded bytes, stored as `mp3`, `ogg`, `m4a`, or `wav`. It then assigns one clip per [scope](/http-api/entrance-sounds/#entrance-sound-scopes): `global`, `guilds`, `dms`, and `guild:{guild_id}`.
Fluxer records which clip belongs to which scope and nothing more. [Play entrance sound](/http-api/entrance-sounds/#play-entrance-sound) names the clip explicitly, so the client decides which selection applies to a given channel.
Playback requires a voice state in the target channel and no channel permission. A caller that holds none is refused at the `channel_id` path with the validation code `ENTRANCE_SOUND_INVALID_SCOPE`, and a channel ID naming no channel is refused the same way. A successful call fans one [ENTRANCE_SOUND_PLAY](/gateway/events/#entrance-sound-play) Dispatch out to every other account with a voice state in the channel, at most once per account and never back to the caller. That Dispatch has the clip's CDN URL, and each recipient fetches and plays it locally, so no audio track is published for it.
Playback requires a voice state in the target channel and no channel permission. A caller that holds none is refused at the `channel_id` path with the validation code `ENTRANCE_SOUND_INVALID_SCOPE`, and a channel ID naming no channel is refused the same way. A successful call sends one [ENTRANCE_SOUND_PLAY](/gateway/events/#entrance-sound-play) Dispatch to every other account with a voice state in the channel, at most once per account and never back to the caller. That Dispatch has the clip's CDN URL, and each recipient fetches and plays it locally, so no audio track is published for it.
Every session the account holds receives the Dispatch, including sessions that are not in the channel. A client filters on `channel_id`.