mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
feat(voice): add opt-in peer-to-peer calls and country rollouts (#3367)
This commit is contained in:
1 parent
e5a568acca
commit
bef5e33eec
215 files changed
+15840
-2693
No files matched your search
@@ -100,6 +100,8 @@ Changes the RTC region of an active direct message or group direct message call.
|
||||
|
||||
The caller does not have to be connected to the call.
|
||||
|
||||
Supplying `region` on a [peer-to-peer](/gateway/events/#peer-to-peer-voice) call moves the call to a voice server, even when the value names the region the call already has. The move is one way for the life of that call.
|
||||
|
||||
### Path parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|
||||
@@ -32,6 +32,7 @@ A channel object always has its identity and type. Every other field is present
|
||||
| user_limit?<sup>4</sup> | ?integer | The configured member occupancy limit, present only for a guild voice channel |
|
||||
| voice_connection_limit?<sup>5</sup> | ?integer | The number of simultaneous voice connections one user may hold, present only for a guild voice channel |
|
||||
| rtc_region?<sup>6</sup> | ?string | The ID of the selected RTC region, present only for a guild voice channel |
|
||||
| rtc_p2p? | boolean | Whether the channel is pinned to [peer-to-peer voice](/gateway/events/#peer-to-peer-voice), present only for a guild voice channel |
|
||||
| last_message_id? | ?snowflake | The ID of the most recent message, null when the channel has none |
|
||||
| last_pin_timestamp? | ?ISO8601 timestamp | The time a message was most recently pinned, or null when nothing has ever been pinned |
|
||||
| permission_overwrites?<sup>7</sup> | array[[permission overwrite](#permission-overwrite-object) object] | The overwrites applied to this channel, present only for a guild channel other than a thread |
|
||||
@@ -464,6 +465,7 @@ Every field is optional, and an omitted field preserves its current value. Every
|
||||
| content_warning_text?<sup>10</sup> | ?string | The content warning text (max 200 characters) |
|
||||
| rate_limit_per_user?<sup>11</sup> | ?integer | The slowmode interval in seconds (0-21,600) |
|
||||
| rtc_region?<sup>12</sup> | ?string | The ID of the RTC region (1-64 characters) for a guild voice channel, where null selects automatic routing |
|
||||
| rtc_p2p? | boolean | Whether to pin a guild voice channel to [peer-to-peer voice](/gateway/events/#peer-to-peer-voice). Ignored on every other channel type |
|
||||
|
||||
<sup>1</sup> An explicit null preserves the current name. In a guild without flexible channel names, Fluxer lowercases a guild text, announcement, forum, or media channel name, replaces its whitespace with hyphens, and removes disallowed punctuation
|
||||
|
||||
@@ -572,7 +574,7 @@ A group marked for mature content accepts media the scanner flags as sensitive,
|
||||
|
||||
A guild channel change emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to its guild. For a forum or media channel the event reaches only a session with [thread access](/gateway/threads/#session-flag). Replacing a category's permission overwrites also replaces them on each child whose overwrites still exactly match the category's previous values, with one Dispatch per changed child. A child whose overwrites had differed is left unchanged.
|
||||
|
||||
Changing `rate_limit_per_user` clears the remaining slowmode delay of every user in the channel. Each user's next message is then checked against the new interval. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region.
|
||||
Changing `rate_limit_per_user` clears the remaining slowmode delay of every user in the channel. Each user's next message is then checked against the new interval. Changing `rtc_region` on a guild voice channel moves its active voice connections to the selected region. A [peer-to-peer](/gateway/events/#peer-to-peer-voice) call in the channel moves to a voice server with that change, unless the channel stores `rtc_p2p` true. Changing `rtc_p2p` leaves a running call as it is and applies to the next call that starts after the channel empties. Changing it to true fails with the field code `GUILD_FEATURE_NOT_TOGGLEABLE` unless the caller's [peer-to-peer voice assignment](/http-api/experiments/#peer-to-peer-voice-assignment-object) is enabled.
|
||||
|
||||
Converting an announcement channel into a text channel deletes every [channel follower webhook](/http-api/webhooks/#webhook-types) that follows it. The deletion runs in the background after the response, emits [Webhooks Update](/gateway/events/#webhooks-update) for each target channel, and writes no audit log entry. Copies already delivered stay in the target channels, and later edits and deletions of the published messages still reach them.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: The experiment assignments envelope, the revalidation and polling c
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
An experiment is an instance-wide rollout. For each account, Fluxer works out whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `domain_migration`, `plutonium_page`, and `channel_threads`. Only `domain_migration` is configured by an operator. `plutonium_page` and `channel_threads` are on for every account.
|
||||
An experiment is an instance-wide rollout. For each account, Fluxer works out whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `domain_migration`, `plutonium_page`, `channel_threads`, and `voice_p2p`. Only `domain_migration` and `voice_p2p` are configured by an operator. `plutonium_page` and `channel_threads` are on for every account.
|
||||
|
||||
Every assignment is advice. A client that ignores one behaves as it does with the rollout off, and no route and no Gateway event reports what a client actually ran.
|
||||
|
||||
@@ -35,6 +35,7 @@ One entry per experiment. The envelope reports this object even when it is empty
|
||||
| domain_migration? | [domain migration assignment](#domain-migration-assignment-object) object | The caller's web domain migration assignment |
|
||||
| plutonium_page? | [Plutonium page assignment](#plutonium-page-assignment-object) object | The caller's Plutonium page assignment |
|
||||
| channel_threads? | [channel threads assignment](#channel-threads-assignment-object) object | The caller's channel threads assignment |
|
||||
| voice_p2p? | [peer-to-peer voice assignment](#peer-to-peer-voice-assignment-object) object | The caller's peer-to-peer voice assignment |
|
||||
|
||||
Ignore unknown experiments and treat a missing experiment as off.
|
||||
|
||||
@@ -48,7 +49,7 @@ One resolution of the instance web domain migration rollout against one account.
|
||||
| --- | --- | --- |
|
||||
| enabled | boolean | Whether the caller's web client moves to the new web origin |
|
||||
|
||||
A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. `enabled` is false in every other case, the rollout being off included.
|
||||
A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. When the operator lists rollout countries, the sampled share only draws a caller whose request comes from a listed country. `enabled` is false in every other case, the rollout being off included.
|
||||
|
||||
Only the official web client acts on this assignment, and only on its legacy origins. Every other client ignores it. The logged-out share and the instance-wide switch are published in the [instance discovery document](/http-api/instance/#domain-migration-object) instead, because a client that holds no credential cannot read this route.
|
||||
|
||||
@@ -79,6 +80,25 @@ The channel threads state for one account. This server version writes the key on
|
||||
|
||||
The key stays for clients that still read it. Threads, forum channels, and media channels need the [`channel_threads` client capability](/http-api/threads/#client-capability).
|
||||
|
||||
## Peer-to-peer voice assignment object
|
||||
|
||||
One resolution of the instance peer-to-peer voice rollout against one account. This server version writes the key on every response.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| enabled | boolean | Whether the caller may start a peer-to-peer voice call |
|
||||
| max_participants | integer | Most participants a peer-to-peer call holds (2-4) |
|
||||
|
||||
A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. When the operator lists rollout countries, the sampled share only draws a caller whose request comes from a listed country. `enabled` is false in every other case, the rollout being off included.
|
||||
|
||||
A selected client may ask for a peer-to-peer connection when it starts a voice call, and shows the peer-to-peer setting of a guild voice channel. It offers a peer-to-peer call in a direct message or group direct message only when the channel has at most `max_participants` members, the caller included. `max_participants` is the same for every account, selected or not, and a join past it is [refused as full](/gateway/events/#peer-to-peer-voice). The assignment does not decide who joins. While the rollout is on, any account that agrees to a direct connection may join a peer-to-peer call that is already running. The server checks the rollout again when a call starts, so a client that ignores this assignment gains nothing.
|
||||
|
||||
## Request country
|
||||
|
||||
Fluxer resolves the country of each request to this route from the client address. An account can receive a different assignment from another network when an operator lists rollout countries. A request whose country is unknown matches no listed country.
|
||||
|
||||
## Get experiment assignments
|
||||
|
||||
<RouteHeader method="GET" path="/v1/experiments" bot />
|
||||
@@ -112,7 +132,7 @@ A client sends the `ETag` it last received. Fluxer compares it against the tag o
|
||||
| Cache-Control | string | The literal value `private, no-cache` |
|
||||
| Vary | string | The literal value `Authorization`, replaced by `Origin` where the [cross-origin policy](/http-api/#cross-origin-requests) echoed an allowed origin |
|
||||
|
||||
The `ETag` changes when the response changes. Treat it as an opaque value.
|
||||
The `ETag` changes when the response changes, which includes a change of [request country](#request-country). Treat it as an opaque value.
|
||||
|
||||
`ETag` is listed in `Access-Control-Expose-Headers` and `If-None-Match` in `Access-Control-Allow-Headers`, so a cross-origin client reads the tag and revalidates with it.
|
||||
|
||||
|
||||
@@ -311,6 +311,7 @@ Recorded by [CHANNEL_CREATE](#audit-actions), [CHANNEL_UPDATE](#audit-actions),
|
||||
| voice_connection_limit | ?number | The configured per-user voice connection limit |
|
||||
| bitrate | ?number | The voice bitrate in bits per second |
|
||||
| rtc_region | ?string | The pinned voice region, or null when the channel is routed automatically |
|
||||
| rtc_p2p | boolean | Whether the channel is pinned to peer-to-peer voice |
|
||||
| permission_overwrite_count<sup>2</sup> | number | The number of permission overwrites the channel has |
|
||||
| flags<sup>3</sup> | number | The [channel flags](/http-api/forums/#channel-flags) of the channel |
|
||||
| default_auto_archive_duration<sup>3</sup> | number | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations) |
|
||||
|
||||
@@ -308,7 +308,7 @@ Hierarchy authority over the target is not checked for `roles`, so a caller who
|
||||
|
||||
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`.
|
||||
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`. A destination that is [peer-to-peer](/gateway/events/#peer-to-peer-voice) also returns 400 `USER_NOT_IN_VOICE`.
|
||||
|
||||
`MANAGE_ROLES` and `MODERATE_MEMBERS` are [elevated permissions](/http-api/permissions/#elevated-permissions). When the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, a caller who is not the guild owner also needs an enrolled multi-factor authenticator to replace `roles` or apply `communication_disabled_until`.
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
title: Peer-to-peer voice
|
||||
description: Connection outcome reports for the peer connections of a peer-to-peer voice call.
|
||||
---
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
A peer-to-peer voice call connects its participants directly, as [Peer-to-peer voice](/gateway/events/#peer-to-peer-voice) describes. Fluxer hands out STUN servers only and never relays media, so whether two participants connect depends on their networks. Clients report how each peer connection settled, so an operator can see which networks connect directly.
|
||||
|
||||
The route on this page is user-only. Fluxer rejects a bot or OAuth2 credential with 403 `ACCESS_DENIED`.
|
||||
|
||||
## Connection report object
|
||||
|
||||
The outcome of one peer connection, as seen from the reporting client. A client sends one report for each remote peer of a call, once the connection to that peer reaches connected or fails.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| channel_id<sup>1</sup> | snowflake | The ID of the guild voice channel or private channel of the call |
|
||||
| guild_id | ?snowflake | The ID of the guild of the channel, null for a call |
|
||||
| participant_count | integer | Participants in the call when the connection settled (2-4) |
|
||||
| outcome | string | `connected` or `failed` |
|
||||
| local_candidate_type<sup>2</sup> | ?string | Candidate type of the local end of the selected pair |
|
||||
| remote_candidate_type<sup>2</sup> | ?string | Candidate type of the remote end of the selected pair |
|
||||
| ip_family<sup>3</sup> | ?string | `ipv4` or `ipv6`, the address family of the selected pair |
|
||||
| protocol<sup>3</sup> | ?string | `udp` or `tcp`, the transport of the selected pair |
|
||||
| setup_ms<sup>4</sup> | ?integer | Milliseconds from creating the connection to its first connected state (0-2,147,483,647) |
|
||||
| ice_restarted | boolean | Whether the connection ran an ICE restart |
|
||||
|
||||
<sup>1</sup> Fluxer stores the value with no check that the channel exists or that the caller was in its call
|
||||
|
||||
<sup>2</sup> One of `host`, `srflx`, `prflx`, or `relay`, and null when the connection has no selected candidate pair
|
||||
|
||||
<sup>3</sup> Null when the connection has no selected candidate pair
|
||||
|
||||
<sup>4</sup> Null for a connection that never reached connected
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_id": "1189375284394692610",
|
||||
"guild_id": null,
|
||||
"participant_count": 2,
|
||||
"outcome": "connected",
|
||||
"local_candidate_type": "srflx",
|
||||
"remote_candidate_type": "host",
|
||||
"ip_family": "ipv6",
|
||||
"protocol": "udp",
|
||||
"setup_ms": 840,
|
||||
"ice_restarted": false
|
||||
}
|
||||
```
|
||||
|
||||
## Report connection outcomes
|
||||
|
||||
<RouteHeader method="POST" path="/v1/voice/p2p/connection-reports" />
|
||||
|
||||
Records 1 through 8 [connection report objects](#connection-report-object) and returns 204 with an empty body. User-only. Emits no Gateway event.
|
||||
|
||||
A client batches its reports and sends them best effort. A failed or rate-limited request changes nothing about the call, and a client drops the batch rather than retrying more than once.
|
||||
|
||||
### JSON body
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| reports<sup>1</sup> | array[[connection report](#connection-report-object) object] | The reports to record (1-8) |
|
||||
|
||||
<sup>1</sup> A list of fewer than 1 or more than 8 entries, or any invalid entry, fails with 400 `INVALID_FORM_BODY` and records nothing
|
||||
|
||||
### Response
|
||||
|
||||
| Status | Body | Condition |
|
||||
| --- | --- | --- |
|
||||
| 204 | empty | Every report was recorded, or peer-to-peer voice is disabled on the instance and nothing was recorded |
|
||||
| 400 | [error response](/http-api/#error-response) | The body is invalid and the request returns `INVALID_FORM_BODY` |
|
||||
| 403 | [error response](/http-api/#error-response) | Caller is a bot or presents a bearer credential and the request returns `ACCESS_DENIED` |
|
||||
|
||||
### Side effects
|
||||
|
||||
Fluxer stores each report with the caller's user ID, the time of the request, the request address and its country, and the client platform and operating system of the caller's session. The country is null when the address resolves to none. Reports expire after 180 days.
|
||||
|
||||
Fluxer keeps each report for 180 days. No route reads reports back.
|
||||
|
||||
### Rate limit
|
||||
|
||||
10 requests per minute for each authenticated user, on the `voice:p2p:connection_reports` bucket.
|
||||
@@ -54,7 +54,7 @@ A thread is a [channel object](/http-api/channels/#channel-object) with the thre
|
||||
|
||||
<sup>6</sup> Present only when the caller is a member, on [Get channel](/http-api/channels/#get-channel), on [Modify channel](/http-api/channels/#modify-channel), on `thread` of a message, on [Thread Create](/gateway/threads/#thread-create), and on the `threads` list of a guild in [Ready](/gateway/events/#ready), [Guild Create](/gateway/events/#guild-create), and [Guild Sync](/gateway/events/#guild-sync)
|
||||
|
||||
The schema also declares `topic`, `url`, `icon`, `position`, `bitrate`, `user_limit`, `voice_connection_limit`, `rtc_region`, `permission_overwrites`, `recipients`, `nsfw`, `nsfw_override`, `content_warning_level`, `content_warning_text`, `nicks`, `default_auto_archive_duration`, `default_thread_rate_limit_per_user`, `available_tags`, `default_reaction_emoji`, `default_sort_order`, `default_forum_layout`, and `default_tag_setting`. Fluxer never sends them on a thread. A thread uses the permissions of its parent as [Thread permissions](#thread-permissions) describes. An age restriction and a content warning resolve through the parent, then the parent category, and finally the guild.
|
||||
The schema also declares `topic`, `url`, `icon`, `position`, `bitrate`, `user_limit`, `voice_connection_limit`, `rtc_region`, `rtc_p2p`, `permission_overwrites`, `recipients`, `nsfw`, `nsfw_override`, `content_warning_level`, `content_warning_text`, `nicks`, `default_auto_archive_duration`, `default_thread_rate_limit_per_user`, `available_tags`, `default_reaction_emoji`, `default_sort_order`, `default_forum_layout`, and `default_tag_setting`. Fluxer never sends them on a thread. A thread uses the permissions of its parent as [Thread permissions](#thread-permissions) describes. An age restriction and a content warning resolve through the parent, then the parent category, and finally the guild.
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
Reference in new issue
Block a user