diff --git a/fluxer_docs/astro.config.ts b/fluxer_docs/astro.config.ts index e8483057c..375d23e53 100644 --- a/fluxer_docs/astro.config.ts +++ b/fluxer_docs/astro.config.ts @@ -217,6 +217,9 @@ export default defineConfig({ items: [ 'http-api/channels', 'http-api/messages', + 'http-api/threads', + 'http-api/thread-members', + 'http-api/forums', 'http-api/read-states', 'http-api/memes', 'http-api/gifs', @@ -266,6 +269,7 @@ export default defineConfig({ 'gateway/commands', 'gateway/events', 'gateway/event-filtering', + 'gateway/threads', 'gateway/limits-and-rate-limits', 'gateway/opcodes-and-close-codes', ], @@ -295,6 +299,7 @@ export default defineConfig({ 'admin-api/api-keys', 'admin-api/users', 'admin-api/guilds', + 'admin-api/threads', 'admin-api/applications', 'admin-api/reports', 'admin-api/messages', diff --git a/fluxer_docs/scripts/VerifyDocsCoverage.ts b/fluxer_docs/scripts/VerifyDocsCoverage.ts index 84e50bf51..6914a9ed1 100644 --- a/fluxer_docs/scripts/VerifyDocsCoverage.ts +++ b/fluxer_docs/scripts/VerifyDocsCoverage.ts @@ -165,63 +165,6 @@ const OUT_OF_BAND_CREDENTIAL = new Map([ const HEALTH_AND_METRICS_PATHS = new Set(['/_health', '/_health/ready', '/_health/drain', '/_healthz', '/_metrics']); -const CHANNEL_THREADS_UNTIL_GA = { - routes: new Set([ - 'POST /channels/{}/messages/{}/threads', - 'POST /channels/{}/threads', - 'GET /guilds/{}/threads/active', - 'GET /channels/{}/threads/archived/public', - 'GET /channels/{}/threads/archived/private', - 'GET /channels/{}/users/@me/threads/archived/private', - 'GET /channels/{}/thread-members', - 'GET /channels/{}/thread-members/{}', - 'PUT /channels/{}/thread-members/@me', - 'PUT /channels/{}/thread-members/{}', - 'DELETE /channels/{}/thread-members/@me', - 'DELETE /channels/{}/thread-members/{}', - 'PATCH /channels/{}/thread-members/@me/settings', - 'GET /channels/{}/threads/search', - 'POST /channels/{}/post-data', - 'POST /channels/{}/tags', - 'PUT /channels/{}/tags/{}', - 'DELETE /channels/{}/tags/{}', - 'GET /admin/guilds/{}/threads', - 'DELETE /admin/channels/{}', - ]), - events: new Set([ - 'THREAD_CREATE', - 'THREAD_UPDATE', - 'THREAD_DELETE', - 'THREAD_LIST_SYNC', - 'THREAD_MEMBER_UPDATE', - 'THREAD_MEMBERS_UPDATE', - ]), - opcodes: new Set([28]), - auditActions: new Set(['delete_thread', 'list_guild_threads']), - errorCodes: new Set([ - 'CHANNEL_HAS_THREADS', - 'FORUM_TAG_NAMES_MUST_BE_UNIQUE', - 'FORUM_TAG_REQUIRED', - 'HIDE_MEDIA_DOWNLOAD_OPTION_MEDIA_ONLY', - 'INVALID_THREAD_NOTIFICATION_SETTINGS', - 'MAX_ACTIVE_THREADS', - 'MAX_FORUM_TAGS', - 'MAX_PINNED_THREADS_IN_FORUM', - 'MAX_THREAD_MEMBERS', - 'NO_TAGS_AVAILABLE_TO_NON_MODERATORS', - 'SEARCH_INDEX_NOT_READY', - 'THREAD_ALREADY_CREATED_FOR_MESSAGE', - 'THREAD_ARCHIVED', - 'THREAD_LOCKED', - 'UNKNOWN_FORUM_TAG', - 'UNKNOWN_THREAD_MEMBER', - 'WEBHOOK_FORUM_TARGET_CONFLICT', - 'WEBHOOK_FORUM_TARGET_REQUIRED', - 'WEBHOOK_SERVICE_FORUM_UNSUPPORTED', - 'WEBHOOK_THREAD_NAME_REQUIRES_FORUM', - ]), -}; - const LEGACY_REPORT_ROUTES = new Set(['POST /reports/message', 'POST /reports/user', 'POST /reports/guild']); interface ExemptionRule { @@ -286,19 +229,6 @@ const EXEMPTION_RULES: ReadonlyArray = [ anchors: [{file: 'packages/openapi/src/generator/OpenAPIGeneratorCatalog.ts', anchor: "'/reports/message'"}], covers: (shape) => LEGACY_REPORT_ROUTES.has(shape), }, - { - name: 'channel_threads', - justification: - 'threads, forum and media channels are behind the channel_threads experiment and ChannelThreadsRouteGuard answers 404 outside it. Their pages are withheld until general availability, which removes this rule', - mayCoverNothing: true, - anchors: [ - { - file: 'fluxer_api/src/api/channel/threads/ChannelThreadsRouteGuard.ts', - anchor: 'export function ChannelThreadsRouteGuard', - }, - ], - covers: (shape) => CHANNEL_THREADS_UNTIL_GA.routes.has(shape), - }, ]; const MEDIA_PROXY_ROUTES = new Map([ @@ -672,7 +602,7 @@ failures += section( if (aliasShapes.has(shape)) { return false; } - return !DELIBERATELY_UNDOCUMENTED.has(shape) && !CHANNEL_THREADS_UNTIL_GA.routes.has(shape); + return !DELIBERATELY_UNDOCUMENTED.has(shape); }) .map((operation) => `${operation.method} ${operation.path} [${operation.tags.join(', ')}]`) .sort(), @@ -696,12 +626,6 @@ const wronglyDocumented = [...DELIBERATELY_UNDOCUMENTED.entries()] .filter(([shape]) => documentedMain.has(shape)) .map(([shape, reason]) => `${shape} must not be documented: ${reason}`); failures += section('documented despite belonging to an unshipped system', wronglyDocumented); -failures += section( - 'published despite being withheld until channel_threads general availability', - [...CHANNEL_THREADS_UNTIL_GA.routes] - .filter((shape) => documentedMain.has(shape) || documentedAdmin.has(shape)) - .sort(), -); console.log( ` deliberately undocumented and correctly absent: ${(DELIBERATELY_UNDOCUMENTED.size - wronglyDocumented.length).toString()}/${DELIBERATELY_UNDOCUMENTED.size.toString()}`, ); @@ -757,7 +681,10 @@ console.log('gateway dispatch events'); await collect(gatewayDirectory); const erlang = erlangSources.join('\n'); - const eventsPage = await readFile(path.join(DOCS_ROOT, 'gateway/events.md'), 'utf8'); + const eventsPage = [ + await readFile(path.join(DOCS_ROOT, 'gateway/events.md'), 'utf8'), + await readFile(path.join(DOCS_ROOT, 'gateway/threads.md'), 'utf8'), + ].join('\n'); const documentedEvents = new Set(); for (const entry of eventsPage.matchAll(/^###\s+(?:]*><\/span>)?([A-Z][A-Z0-9_]{3,})\s*$/gmu)) { documentedEvents.add(entry[1]); @@ -774,19 +701,13 @@ console.log('gateway dispatch events'); }; const fabricated = [...documentedEvents].filter((event) => !isReal(event)).sort(); - const undocumented = [...apiEvents] - .filter((event) => !documentedEvents.has(event) && !CHANNEL_THREADS_UNTIL_GA.events.has(event)) - .sort(); + const undocumented = [...apiEvents].filter((event) => !documentedEvents.has(event)).sort(); console.log(` events in the GatewayDispatchEvent union: ${apiEvents.size.toString()}`); - console.log(` events documented in gateway/events.md: ${documentedEvents.size.toString()}`); + console.log(` events documented in gateway/events.md and gateway/threads.md: ${documentedEvents.size.toString()}`); console.log(' a documented event counts as real if it is in the union, or appears in fluxer_gateway'); console.log(' as an uppercase binary or a lowercase atom'); failures += section('documented but not emitted by any service', fabricated); failures += section('in the dispatch union but undocumented', undocumented); - failures += section( - 'published despite being withheld until channel_threads general availability', - [...CHANNEL_THREADS_UNTIL_GA.events].filter((event) => documentedEvents.has(event)).sort(), - ); } console.log('gateway opcodes and close codes'); @@ -826,14 +747,10 @@ console.log('gateway opcodes and close codes'); failures += section( 'opcodes in constants.erl but undocumented', [...realOpcodes] - .filter((code) => !docOpcodes.has(code) && !CHANNEL_THREADS_UNTIL_GA.opcodes.has(code)) + .filter((code) => !docOpcodes.has(code)) .sort((a, b) => a - b) .map(String), ); - failures += section( - 'opcodes published despite being withheld until channel_threads general availability', - [...CHANNEL_THREADS_UNTIL_GA.opcodes].filter((code) => docOpcodes.has(code)).map(String), - ); failures += section( 'close codes documented but absent from constants.erl', [...docCloseCodes] @@ -1060,12 +977,7 @@ console.log('admin audit actions'); failures += section( 'recorded but undocumented', [...recorded] - .filter( - ([action]) => - !documentedRead.has(action) && - !documentedWrite.has(action) && - !CHANNEL_THREADS_UNTIL_GA.auditActions.has(action), - ) + .filter(([action]) => !documentedRead.has(action) && !documentedWrite.has(action)) .map(([action, file]) => `${action} (${file})`) .sort(), ); @@ -1087,11 +999,22 @@ console.log('permission bits'); const constants = await readFile(path.join(REPO_ROOT, 'packages/constants/src/ChannelConstants.ts'), 'utf8'); const block = constants.match(/export const Permissions = \{([\s\S]*?)\} as const;/u); const livePermissions = new Map(); - if (block != null) { - for (const entry of block[1].matchAll(/([A-Z][A-Z0-9_]*):\s*1n\s*<<\s*(\d+)n/gu)) { + const threadConstants = await readFile( + path.join(REPO_ROOT, 'packages/constants/src/ThreadPermissionUtils.ts'), + 'utf8', + ); + const threadBlock = threadConstants.match(/export const ThreadPermissionFlags = \{([\s\S]*?)\} as const;/u); + for (const source of [block, threadBlock]) { + if (source == null) { + continue; + } + for (const entry of source[1].matchAll(/([A-Z][A-Z0-9_]*):\s*1n\s*<<\s*(\d+)n/gu)) { livePermissions.set(entry[1], Number.parseInt(entry[2], 10)); } } + if (threadBlock == null) { + failures += section('ThreadPermissionFlags block not found in ThreadPermissionUtils.ts', ['parser drift']); + } const page = (await readFile(path.join(DOCS_ROOT, 'http-api/permissions.mdx'), 'utf8')) .replace(/</gu, '<') .replace(/>/gu, '>'); @@ -1926,19 +1849,17 @@ console.log('error registry'); } const problems: Array = []; for (const code of registryCodes) { - if (!documentedCodes.has(code) && !CHANNEL_THREADS_UNTIL_GA.errorCodes.has(code)) { + if (!documentedCodes.has(code)) { problems.push(`APIErrorCodes.${code} is not listed in the errors page registry`); } } const documentedRegistryCodes = [...registryCodes].filter((c) => documentedCodes.has(c)).length; - const withheldRegistryCodes = [...registryCodes].filter((c) => CHANNEL_THREADS_UNTIL_GA.errorCodes.has(c)).length; const UNDOCUMENTED_VALIDATION_CODES = new Set(['EMAIL_DOMAIN_CANNOT_RECEIVE_MAIL']); - const expectedEntries = - registryCodes.size + validationCodes.size - UNDOCUMENTED_VALIDATION_CODES.size - withheldRegistryCodes; + const expectedEntries = registryCodes.size + validationCodes.size - UNDOCUMENTED_VALIDATION_CODES.size; if (documentedEntries < expectedEntries) { problems.push( - `errors.md code entries parsed fell to ${documentedEntries.toString()}, expected ${expectedEntries.toString()}, the ${registryCodes.size.toString()} API codes plus the ${validationCodes.size.toString()} validation codes less the ${UNDOCUMENTED_VALIDATION_CODES.size.toString()} named as undocumented and the ${withheldRegistryCodes.toString()} withheld until channel_threads general availability. Either a code lost its entry, or the registry parser stopped matching one of the \`| CODE |\` table row and \`### \`CODE\`\` or \`#### \`CODE\`\` heading shapes it reads`, + `errors.md code entries parsed fell to ${documentedEntries.toString()}, expected ${expectedEntries.toString()}, the ${registryCodes.size.toString()} API codes plus the ${validationCodes.size.toString()} validation codes less the ${UNDOCUMENTED_VALIDATION_CODES.size.toString()} named as undocumented. Either a code lost its entry, or the registry parser stopped matching one of the \`| CODE |\` table row and \`### \`CODE\`\` or \`#### \`CODE\`\` heading shapes it reads`, ); } for (const code of documentedCodes) { @@ -1946,19 +1867,12 @@ console.log('error registry'); problems.push(`errors.md documents ${code}, which is in neither code registry`); } } - if (documentedRegistryCodes < registryCodes.size - withheldRegistryCodes) { + if (documentedRegistryCodes < registryCodes.size) { problems.push( - `errors.md documents ${documentedRegistryCodes.toString()} of the ${registryCodes.size.toString()} registry codes, floor is every one of them less the ${withheldRegistryCodes.toString()} withheld until channel_threads general availability`, + `errors.md documents ${documentedRegistryCodes.toString()} of the ${registryCodes.size.toString()} registry codes, floor is every one of them`, ); } - for (const code of CHANNEL_THREADS_UNTIL_GA.errorCodes) { - if (!registryCodes.has(code)) { - problems.push(`CHANNEL_THREADS_UNTIL_GA withholds ${code}, which is not in the API code registry`); - } - if (documentedCodes.has(code)) { - problems.push(`errors.md publishes ${code}, which is withheld until channel_threads general availability`); - } - } + console.log(` registry codes: ${registryCodes.size.toString()}, documented: ${documentedRegistryCodes.toString()}`); failures += section('error registry disagreements', problems); } @@ -2336,7 +2250,7 @@ const adminTargetOnly = [...documentedAdmin.entries()] const adminLiveOnly = admin .filter((operation) => { const shape = routeShape(operation.method, operation.path); - return !documentedAdmin.has(shape) && !CHANNEL_THREADS_UNTIL_GA.routes.has(shape); + return !documentedAdmin.has(shape); }) .map((operation) => `${operation.method} ${operation.path}`) .sort(); diff --git a/fluxer_docs/scripts/VerifyDocsSchemas.ts b/fluxer_docs/scripts/VerifyDocsSchemas.ts index 9067046a4..ec5f7c542 100644 --- a/fluxer_docs/scripts/VerifyDocsSchemas.ts +++ b/fluxer_docs/scripts/VerifyDocsSchemas.ts @@ -22,37 +22,6 @@ import { const REPO_ROOT = fileURLToPath(new URL('../../', import.meta.url)); const MAIN_SPEC = path.join(REPO_ROOT, 'fluxer_api/src/api/openapi/openapi.json'); const ADMIN_SPEC = path.join(REPO_ROOT, 'fluxer_admin/openapi-admin.json'); -const CHANNEL_THREADS_FIELDS_UNTIL_GA = new Set([ - 'applied_tags', - 'archived', - 'auto_archive_duration', - 'available_tags', - 'channel_threads', - 'default_auto_archive_duration', - 'default_forum_layout', - 'default_reaction_emoji', - 'default_sort_order', - 'default_tag_setting', - 'default_thread_rate_limit_per_user', - 'flags', - 'invitable', - 'locked', - 'member', - 'member_count', - 'member_ids_preview', - 'members', - 'message_count', - 'message_send_cooldown_ms', - 'thread', - 'thread_create_cooldown_ms', - 'thread_id', - 'thread_metadata', - 'thread_name', - 'threads', - 'threads_active', - 'total_message_sent', -]); - export interface Mismatch { readonly page: string; readonly operation: string; @@ -72,7 +41,6 @@ export interface SchemaCounters { unlinkedObjects: number; typesCompared: number; optionalityCompared: number; - withheldFields: number; } export interface SchemaVerification { @@ -630,7 +598,6 @@ class Verifier { unlinkedObjects: 0, typesCompared: 0, optionalityCompared: 0, - withheldFields: 0, }; private readonly objects = new Map(); private readonly compared = new Set(); @@ -823,17 +790,6 @@ class Verifier { } private push(page: string, operation: string, kind: string, detail: string): void { - if (kind.endsWith('-missing')) { - const field = - detail - .replace(/ \(variant \d+\)$/u, '') - .split('.') - .pop() ?? detail; - if (CHANNEL_THREADS_FIELDS_UNTIL_GA.has(field)) { - this.counters.withheldFields += 1; - return; - } - } this.mismatches.push({page, operation, kind, detail}); } @@ -1012,12 +968,7 @@ class Verifier { ): void { const resolved = resolveRef(spec, schema); const allBranches = isUnion(resolved) ? [...(resolved?.oneOf ?? []), ...(resolved?.anyOf ?? [])] : [schema]; - const branches = allBranches.filter( - (branch) => - allBranches.length === 1 || - ![...collectPropertySchemas(spec, branch).keys()].some((field) => CHANNEL_THREADS_FIELDS_UNTIL_GA.has(field)), - ); - this.counters.withheldFields += allBranches.length - branches.length; + const branches = allBranches; if (branches.length > 1) this.counters.unionBodies += 1; const branchProperties = branches.map((branch) => new Set(collectPropertySchemas(spec, branch).keys())); const branchRequired = branches.map((branch) => collectRequired(spec, branch)); @@ -1339,7 +1290,6 @@ async function main(): Promise { ); console.log(`request and response field types compared: ${counters.typesCompared.toString()}`); console.log(`request body optionality compared: ${counters.optionalityCompared.toString()}`); - console.log(`channel_threads fields withheld until GA: ${counters.withheldFields.toString()}`); console.log(`optionality advisories: ${optionalityAdvisories.length.toString()}`); for (const entry of optionalityAdvisories) { console.log(` ${entry}`); diff --git a/fluxer_docs/scripts/VerifyDocsStyle.ts b/fluxer_docs/scripts/VerifyDocsStyle.ts index 476b2df2c..aeb449ca0 100644 --- a/fluxer_docs/scripts/VerifyDocsStyle.ts +++ b/fluxer_docs/scripts/VerifyDocsStyle.ts @@ -283,7 +283,7 @@ const ACCEPTED_TABLE_FINDINGS = new Map1 All are null when the owner account cannot be resolved @@ -564,7 +564,7 @@ The acting account must see the guild, hold [KICK_MEMBERS](/http-api/permissions ### Side effects -The user loses membership and disappears from guild member search. A later rejoin restores a communication timeout that is still in force. The rejoined member starts with no nickname and no roles. +The user loses membership and disappears from guild member search. Removal from the guild removes every membership in its threads. Fluxer emits a [Thread Members Update](/gateway/threads/#thread-members-update) for each of them, as [Thread member flags](/http-api/thread-members/#thread-member-flags) states. A later rejoin restores a communication timeout that is still in force. The rejoined member starts with no nickname and no roles. [Guild Member Remove](/gateway/events/#guild-member-remove) fires to the guild. A `MEMBER_KICK` entry is written to the guild's own [audit log](/http-api/guild-audit-logs/#audit-actions). The entry names the acting Admin account and has the audit reason. The write fires [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions holding [VIEW_AUDIT_LOG](/http-api/permissions/). @@ -625,7 +625,7 @@ The ban record names the acting Admin account as moderator. It has the expiry, t A positive deletion window queues a background job that deletes the target's matching messages after the response, which fires [Message Delete Bulk](/gateway/events/#message-delete-bulk) as deletion progresses. -[Guild Ban Add](/gateway/events/#guild-ban-add) fires to the guild. If the target was a member, they lose access, disappear from member search, and trigger [Guild Member Remove](/gateway/events/#guild-member-remove). A communication timeout in force at the ban is not restored on a later rejoin. This operation writes no entry to the guild's own audit log and emits no [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create). +[Guild Ban Add](/gateway/events/#guild-ban-add) fires to the guild. If the target was a member, they lose access, disappear from member search, and trigger [Guild Member Remove](/gateway/events/#guild-member-remove). Removal from the guild removes every membership in its threads. Fluxer emits a [Thread Members Update](/gateway/threads/#thread-members-update) for each of them, as [Thread member flags](/http-api/thread-members/#thread-member-flags) states. A communication timeout in force at the ban is not restored on a later rejoin. This operation writes no entry to the guild's own audit log and emits no [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create). One Admin audit entry is recorded with the action `ban_member`, the target type `guild_member`, and the banned user as the target. Its metadata has the guild ID, the user ID, the `delete_message_days` value, and any supplied reason and duration. @@ -764,7 +764,7 @@ Every entry in `processed` records its own Admin audit entry, with the numeric I Returns one page of the guild's own in-app audit log, without requiring guild membership or [VIEW_AUDIT_LOG](/http-api/permissions/). Requires `guild:audit_log:view`. -The response has the same fields, and its query parameters behave the same way, as in the public [List guild audit logs](/http-api/guild-audit-logs/#list-guild-audit-logs) operation, including its message deletion consolidation. +The query parameters behave the same way as in the public [List guild audit logs](/http-api/guild-audit-logs/#list-guild-audit-logs) operation, including its message deletion consolidation, except for the thread action values in `action_type`. ### Path parameters @@ -780,12 +780,14 @@ The response has the same fields, and its query parameters behave the same way, | before?1 | snowflake | Return entries older than this entry ID | | after?1 | snowflake | Return entries newer than this entry ID | | user_id?2 | snowflake | Return only entries recorded for this actor | -| action_type?2 | integer | Return only entries with this [audit action](/http-api/guild-audit-logs/#audit-actions) value | +| action_type?23 | integer | Return only entries with this [audit action](/http-api/guild-audit-logs/#audit-actions) value | 1 The cursors are mutually exclusive. Supplying both fails with 400 `INVALID_FORM_BODY` and the validation code `CANNOT_SPECIFY_BOTH_BEFORE_AND_AFTER` against `before` 2 Supplying either filter disables the consolidation of consecutive message deletion entries +3 The values 110, 111, and 112 return 400 `INVALID_FORM_BODY` + ### Response body | Field | Type | Description | @@ -794,6 +796,8 @@ The response has the same fields, and its query parameters behave the same way, | users | array[[partial user](/http-api/users/#partial-user-object) object] | Users referenced by the returned entries | | webhooks | array[[audit log webhook](/http-api/guild-audit-logs/#audit-log-webhook-object) object] | Webhooks referenced by the returned entries | +The schema also declares `threads`. This operation never returns it. + ### Response | Status | Body | Condition | @@ -843,7 +847,7 @@ A reload the main Gateway reports as failed returns 502 `BAD_GATEWAY`, an unansw ### Side effects -The main Gateway reloads the guild's current state from storage and fires one [Guild Update](/gateway/events/#guild-update) Dispatch to every session subscribed to the guild. The operation changes no guild data. +The main Gateway reloads the guild's current state from storage and fires one [Guild Update](/gateway/events/#guild-update) Dispatch to every session subscribed to the guild. The reload also reloads the thread state of the guild. Sessions then receive [Thread List Sync](/gateway/threads/#thread-list-sync) in the cases that entry lists. The operation changes no guild data. One Admin audit entry is recorded with the action `reload_guild`, the target type `guild`, and the guild ID in both the target and the metadata. @@ -893,7 +897,7 @@ One Admin audit entry is recorded with the action `shutdown_guild`, the target t -Queues an asynchronous archive of the guild's channels, messages, members, roles, and settings, and returns the created [archive](/admin-api/archives/#archive-object) object. Requires `archive:trigger:guild`. +Queues an asynchronous archive of the guild's channels, threads, messages, members, roles, and settings, and returns the created [archive](/admin-api/archives/#archive-object) object. Requires `archive:trigger:guild`. Archive status and downloads are read through [Archives](/admin-api/archives/). diff --git a/fluxer_docs/src/content/docs/admin-api/index.mdx b/fluxer_docs/src/content/docs/admin-api/index.mdx index 4b666a1c6..c14e91d2e 100644 --- a/fluxer_docs/src/content/docs/admin-api/index.mdx +++ b/fluxer_docs/src/content/docs/admin-api/index.mdx @@ -96,7 +96,7 @@ An entry stores an acting Admin, a target type, a target ID, an action name, an | target_id2 | string | The target ID in decimal, which is the literal `0` for a target that has no snowflake | | target_user3 | ?[Admin user summary](#admin-user-summary-object) object | The resolved target account, or null when the entry names no account target | | target_guild4 | ?[Admin guild summary](#admin-guild-summary-object) object | The resolved target guild, or null when the entry names no guild target | -| target_channel5 | ?[Admin channel summary](#admin-channel-summary-object) object | The resolved target channel, or null when the entry names no channel target | +| target_channel5 | ?[Admin channel summary](#admin-channel-summary-object) object | The resolved target channel, or null when the entry names no channel target or the channel no longer exists | | related_users6 | map[snowflake, [Admin user summary](#admin-user-summary-object) object] | The accounts the entry references, keyed by ID | | related_guilds6 | map[snowflake, [Admin guild summary](#admin-guild-summary-object) object] | The guilds the entry references, keyed by ID | | related_channels6 | map[snowflake, [Admin channel summary](#admin-channel-summary-object) object] | The channels the entry references, keyed by ID | @@ -114,7 +114,7 @@ An entry stores an acting Admin, a target type, a target ID, an action name, an 4 Resolved only when `target_type` is `guild` -5 Resolved when `target_type` is `channel`, and otherwise from a `channel_id` metadata key +5 Resolved when `target_type` is `channel`, and otherwise from a `channel_id` metadata key. The thread of a `delete_thread` entry no longer exists, and `target_channel` is null 6 Resolved once for the whole page and then repeated identically on every entry in it @@ -288,6 +288,7 @@ An entry with one of these actions has `access` set to `read`. | list_guild_members | The members of a guild were listed | | list_guild_memory_stats | Guild memory statistics were listed | | list_guild_stickers | The stickers of a guild were listed | +| list_guild_threads | The threads of a guild were listed | | list_report_reasons | The report reason catalogue was listed | | list_user_applications | The applications an account owns were read | | list_user_change_log | The contact change log of an account was read | @@ -349,6 +350,7 @@ An entry with any other action has `access` set to `write`. | delete_guild | A guild was deleted | | delete_message | One message was deleted | | delete_report | A report was deleted with its stored evidence | +| delete_thread | A thread was deleted | | delete_voice_region | A voice region was deleted | | delete_voice_server | A registered voice server was deleted | | delete_webauthn_credential | A WebAuthn credential was removed from an account | @@ -561,7 +563,7 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi | guild:force_add_member | Adds an account to a guild | | guild:kick_member | Removes a guild member | | guild:list:members | Reads guild member resources | -| guild:lookup | Reads and searches Admin [guild](/admin-api/guilds/) resources, and queues and reads [search index](/admin-api/search-indexes/) rebuilds | +| guild:lookup | Reads and searches Admin [guild](/admin-api/guilds/) resources, lists the [threads](/admin-api/threads/#list-guild-threads) of a guild, and queues and reads [search index](/admin-api/search-indexes/) rebuilds | | guild:reload | Reloads one guild on the main Gateway | | guild:shutdown | Stops one guild on the main Gateway | | guild:transfer_ownership5 | Transfers guild ownership | @@ -573,7 +575,7 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi | instance:config:view | Reads instance configuration | | instance:limit_config:update | Replaces the instance-wide limit configuration | | instance:limit_config:view | Reads the instance-wide limit configuration | -| message:delete_all | Queues deletion of every message one account sent | +| message:delete_all | Queues deletion of every message one account sent, and deletes a [thread](/admin-api/threads/#delete-thread) | | message:delete | Deletes one message | | message:lookup | Reads, browses, and searches [messages](/admin-api/messages/) through the Admin API | | message:shred | Queues an explicit message shred job and reads its state | diff --git a/fluxer_docs/src/content/docs/admin-api/jobs.mdx b/fluxer_docs/src/content/docs/admin-api/jobs.mdx index 10c32a843..0e8331680 100644 --- a/fluxer_docs/src/content/docs/admin-api/jobs.mdx +++ b/fluxer_docs/src/content/docs/admin-api/jobs.mdx @@ -119,7 +119,7 @@ The queue drops a job 7 days after it was queued. A job still recorded as `queue | realtime | Mention processing | | unfurl | Link previews | | lifecycle | Account, guild, moderation, archive and bulk work | -| batch | Scheduled maintenance and index work | +| batch | Scheduled maintenance, index work and thread maintenance | | crosspost | [Announcement channel](/topics/announcement-channels/) delivery, copy updates, and follower cleanup | ## Background job task types @@ -128,6 +128,8 @@ Use the returned `task_type` value to filter [List jobs](#list-jobs). The tasks queued by [Bulk jobs](/admin-api/bulk-jobs/) are `bulkUpdateUserFlags`, `bulkUpdateGuildFeatures`, `bulkAddGuildMembers`, and `bulkScheduleUserDeletion`. `harvestUserData` and `harvestGuildData` build [archives](/admin-api/archives/), `sendSystemDm` delivers a [system DM broadcast](/admin-api/system-dms/), and `refreshSearchIndex` rebuilds a [search index](/admin-api/search-indexes/). `removeChannelFollowers` runs on the `crosspost` lane after an announcement channel is deleted or converted into a text channel, and removes the channel follower webhooks that follow it. +Thread work runs on the `batch` lane. `deleteChannelThreads` deletes the threads of a deleted parent channel, `removeThreadMembershipsForGuildMember` removes the thread memberships of a user who is no longer a guild member, and `syncThreadSearchDocument` and `backfillThreadSearch` keep the thread search index current. `archiveInactiveThreads` runs every minute with no job record and archives the threads whose auto archive time has passed. `repairThreadIndexes` rebuilds the thread list entries of the threads it names. `seedThreadPermissions` runs once for a guild. It clears the thread permission bits from every overwrite mask. It then adds `CREATE_PUBLIC_THREADS`, `CREATE_PRIVATE_THREADS`, and `SEND_MESSAGES_IN_THREADS` to each overwrite mask that has `SEND_MESSAGES`, and to the everyone role when that role has `SEND_MESSAGES`. + ## List jobs diff --git a/fluxer_docs/src/content/docs/admin-api/messages.mdx b/fluxer_docs/src/content/docs/admin-api/messages.mdx index 71bc0d2b6..bd5b9f7cc 100644 --- a/fluxer_docs/src/content/docs/admin-api/messages.mdx +++ b/fluxer_docs/src/content/docs/admin-api/messages.mdx @@ -16,6 +16,8 @@ A flattened moderation view of one message. It has the author identity, the cont Every operation that returns Admin message objects also returns the matching public message objects in a parallel `message_responses` array, so a client that needs the full wire shape joins on `id`. +A `message_responses` entry has no `thread` field, and a thread started from a message lists no `THREAD_STARTER_MESSAGE`. [Thread messages](/http-api/threads/#thread-messages) defines both. + ### Structure | Field | Type | Description | @@ -45,6 +47,8 @@ Every operation that returns Admin message objects also returns the matching pub 2 Always absent from the objects these operations return. The same member is set on the [report message context object](/admin-api/reports/#report-message-context-object) +For a thread, `channel_nsfw` is the value of its parent channel, as [Thread object](/http-api/threads/#thread-object) describes. + ### Example ```json @@ -260,6 +264,8 @@ The message and its attachments are permanently removed, including from search r [Message Delete](/gateway/events/#message-delete) is sent to the guild when the channel belongs to one, and to each recipient when the channel is private. No other Dispatch is emitted, and deleting a pinned message here emits no [Channel Pins Update](/gateway/events/#channel-pins-update). +A deleted message in a thread no longer counts towards `message_count` of the [thread object](/http-api/threads/#thread-object). Fluxer emits no [Thread Update](/gateway/threads/#thread-update) for the change. + One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with the action `delete_message` and the target type `message`, targeting the message ID. Its metadata is `channel_id` and `message_id`. The entry is written whether or not a message was found. ### Rate limit diff --git a/fluxer_docs/src/content/docs/admin-api/reports.mdx b/fluxer_docs/src/content/docs/admin-api/reports.mdx index cc1318916..22683f8ac 100644 --- a/fluxer_docs/src/content/docs/admin-api/reports.mdx +++ b/fluxer_docs/src/content/docs/admin-api/reports.mdx @@ -130,6 +130,8 @@ A report is written once, when the reporter files it. [Update report](#update-re 18 On an instance with the `none` tag style the tag of an account that is not a bot is the bare username, with no `#0000` +For a report in a thread, `reported_channel_nsfw`, the three `reported_channel_effective_` fields, and `channel_nsfw` of each context entry resolve through the parent channel, as [Thread object](/http-api/threads/#thread-object) describes. + ### Example ```json diff --git a/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx b/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx index cbb29f63a..21edd6c81 100644 --- a/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx +++ b/fluxer_docs/src/content/docs/admin-api/search-indexes.mdx @@ -21,9 +21,10 @@ Both routes require the [ACL](/admin-api/#acl-evaluation) `guild:lookup`. | reports | Instance-wide | Every report, backing Admin report search | | audit_logs | Instance-wide | Every Admin audit entry, backing Admin audit search | | discovery1 | Instance-wide | Discovery metadata written onto the guild documents of approved listings | -| channel_messages2 | One guild | Messages of every channel in one guild, requiring `guild_id` | +| channel_messages2 | One guild | Messages of every channel and thread in one guild, requiring `guild_id` | | guild_members3 | One guild | Members of one guild, requiring `guild_id` | | favorite_memes4 | One user | Favourited [memes](/http-api/memes/) of one user, requiring `user_id` | +| threads5 | One guild | Threads of one guild, backing [Search threads](/http-api/threads/#search-threads), requiring `guild_id` | 1 Refreshes the description, category, primary language and tags of approved discovery listings without removing guilds from search @@ -33,6 +34,8 @@ Both routes require the [ACL](/admin-api/#acl-evaluation) `guild:lookup`. 4 Rebuilding this index is not supported. The request is accepted, but the job fails without reporting progress +5 [Search threads](/http-api/threads/#search-threads) returns 202 for the guild until the rebuild completes + Every instance-wide name other than `discovery` deletes its documents before the first batch is written, so search over that index is incomplete for the whole run. A `discovery` rebuild instead updates the existing guild documents in place, so those documents stay searchable for the whole run. @@ -123,7 +126,7 @@ Every field is optional. Fluxer reads an absent or empty body as an empty object | guild_id?1 | snowflake | The ID of the guild whose documents in the index are rebuilt | | user_id?2 | snowflake | The ID of the user whose documents in the index are rebuilt | -1 Required by `channel_messages` and `guild_members`. Every other index name ignores it +1 Required by `channel_messages`, `guild_members`, and `threads`. Every other index name ignores it 2 Required by `favorite_memes`. Every other index name ignores it diff --git a/fluxer_docs/src/content/docs/admin-api/threads.mdx b/fluxer_docs/src/content/docs/admin-api/threads.mdx index 7f9909bef..e880bbb88 100644 --- a/fluxer_docs/src/content/docs/admin-api/threads.mdx +++ b/fluxer_docs/src/content/docs/admin-api/threads.mdx @@ -2,7 +2,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later title: Admin threads description: Guild thread lookup and thread deletion. -draft: true --- import RouteHeader from '@/components/RouteHeader.astro'; @@ -33,6 +32,8 @@ Returns every thread of a guild, active and archived, newest first. Requires `gu | --- | --- | --- | | 200 | response body | Threads were listed | +The operation does not verify that the guild exists. An unknown guild ID returns an empty `threads` array. + ### Side effects One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with the action `list_guild_threads`, the target type `guild`, and the guild ID as the target. Its metadata key `result_count` is the number of threads returned. @@ -45,7 +46,7 @@ One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with t -Deletes a thread with its messages and memberships. Requires `message:delete_all`. Returns 204 with no body. Emits a [Thread Delete](/gateway/events/#thread-delete) Gateway event. +Deletes a thread with its messages, attachments, and memberships. Requires `message:delete_all`. Returns 204 with no body. ### Path parameters @@ -57,13 +58,15 @@ Deletes a thread with its messages and memberships. Requires `message:delete_all | Status | Body | Condition | | --- | --- | --- | -| 204 | None | Thread was deleted | -| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` | -| 404 | [error response](/http-api/#error-response) | Channel does not exist and the request returns `UNKNOWN_CHANNEL` | +| 204 | empty | Thread was deleted | +| 400 | [error response](/admin-api/#error-response) | `INVALID_CHANNEL_TYPE` when the channel is not a public, private, or announcement thread | +| 404 | [error response](/admin-api/#error-response) | `UNKNOWN_CHANNEL` when no channel has this ID | ### Side effects -One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with the action `delete_thread`, the target type `channel`, and the thread ID as the target. Its metadata is `guild_id`, `parent_id`, and `type`. The guild audit log records nothing. +Emits a [Thread Delete](/gateway/threads/#thread-delete) Gateway event. The source message of a thread started from a message loses `HAS_THREAD`, and Fluxer emits [Message Update](/gateway/events/#message-update) for it. Fluxer removes the thread and its messages from search. + +One [Admin audit entry](/admin-api/#admin-audit-entry-object) is recorded with the action `delete_thread`, the target type `channel`, and the thread ID as the target. Its metadata keys are `guild_id`, `parent_id`, and `type`. `type` is the channel type as a decimal string. The guild's own audit log records nothing. ### Rate limit diff --git a/fluxer_docs/src/content/docs/authentication.md b/fluxer_docs/src/content/docs/authentication.md index 96e679e45..37e31c0e5 100644 --- a/fluxer_docs/src/content/docs/authentication.md +++ b/fluxer_docs/src/content/docs/authentication.md @@ -98,6 +98,8 @@ A credential can affect even an unauthenticated operation. It selects account-ba A protected operation returns 401 `UNAUTHORIZED` for a missing, malformed, unknown, expired, or revoked credential. Bot tokens on Admin operations and non-bearer credentials on bearer-only operations also return 401. +The routes with a route header on [Threads](/http-api/threads/), [Thread members](/http-api/thread-members/) and [Forums](/http-api/forums/) are the exception. A bot token reaches them with no declaration. A user request reaches them only with a session token and the `channel_threads` client capability. Any other request returns 404 `NOT_FOUND`, including one with no credential or an OAuth2 access token. [Client capability](/http-api/threads/#client-capability) defines the rule. + A valid identity denied by the operation returns 403 `ACCESS_DENIED`. An [Authentication](/http-api/authentication/) operation that resolves a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`, as [Bot tokens](#bot-tokens) describes. Scope and Admin permission failures use the specific codes above. A 401 has no `WWW-Authenticate` header, so clients must inspect `code`. diff --git a/fluxer_docs/src/content/docs/gateway/commands.md b/fluxer_docs/src/content/docs/gateway/commands.md index 4f844fa8a..c7572e8e8 100644 --- a/fluxer_docs/src/content/docs/gateway/commands.md +++ b/fluxer_docs/src/content/docs/gateway/commands.md @@ -18,9 +18,10 @@ Except for [Heartbeat](#heartbeat), [Identify](#identify), and [Resume](#resume) | 4 | Voice State Update | [Voice State Update](/gateway/events/#voice-state-update) and [Voice Server Update](/gateway/events/#voice-server-update) when state changes | | 6 | Resume | Replayed Dispatches followed by [Resumed](/gateway/events/#resumed), Invalid Session, or a close frame | | 8 | Request Guild Members | One or more [Guild Members Chunk](/gateway/events/#guild-members-chunk) events | -| 14 | Lazy Request | [Guild Sync](/gateway/events/#guild-sync) and [Guild Member List Update](/gateway/events/#guild-member-list-update) | +| 14 | Lazy Request | [Guild Sync](/gateway/events/#guild-sync), [Guild Member List Update](/gateway/events/#guild-member-list-update), [Thread List Sync](/gateway/threads/#thread-list-sync), and [Thread Member List Update](/gateway/threads/#thread-member-list-update) | | 15 | Request Guild Counts | [Guild Counts Update](/gateway/events/#guild-counts-update) | | 16 | Request Channel Member Counts | [Channel Member Counts Update](/gateway/events/#channel-member-counts-update) | +| 28 | [Request Forum Unreads](/gateway/threads/#request-forum-unreads) | [Forum Unreads](/gateway/threads/#forum-unreads) | ## Payload handling @@ -28,7 +29,7 @@ A frame that fails the size, decompression, or decoding checks closes the connec Fluxer charges every command that gets past those checks against the [source IP, session, and connection payload budgets](/gateway/limits-and-rate-limits/#connection-and-command-rate-limits) before it handles the opcode. Each session has its own session budget, so two sessions of one account never share one, and Fluxer skips that budget while the connection is unauthenticated. -An opcode outside the registry closes with `4001` and reason `Unknown opcode` once a session is attached, and with `4003` while the connection is unauthenticated. A payload that has `op` but no `d` also closes with `4001`, except for Identify, which closes with `4005`. +An opcode outside the registry closes with `4001` and reason `Unknown opcode` once a session is attached, and with `4003` while the connection is unauthenticated. A payload that has `op` but no `d` also closes with `4001`, except for Identify, which closes with `4005`. [Request Forum Unreads](/gateway/threads/#request-forum-unreads) closes with `4001` for a `d` that is not an object and for a user session without `CHANNEL_THREADS`. A Boolean command field is set only by `true`, and by the string `"true"` where noted. Every other value resolves to false, except in the Lazy Request [guild subscription object](#guild-subscription-object), which states its own rule. @@ -124,8 +125,9 @@ A token the backend rejects closes with `4004` and reason `Invalid token`. A non | Value | Name | Description | | --- | --- | --- | | 1 << 1 | DEBOUNCE_MESSAGE_REACTIONS | Merge runs of reaction additions into [Message Reaction Add Many](/gateway/events/#message-reaction-add-many) | +| 1 << 2 | [CHANNEL_THREADS](/gateway/threads/#session-flag) | The client can parse thread, forum, and media channels | -Bit `0` and every bit above `1` are undefined. An undefined bit is accepted and ignored without closing the connection. +Bit `0` and every bit above `2` are undefined. An undefined bit is accepted and ignored without closing the connection. `DEBOUNCE_MESSAGE_REACTIONS` applies to a reaction in a direct message or group direct message. A reaction in a guild channel is never merged and arrives as its own [Message Reaction Add](/gateway/events/#message-reaction-add). @@ -358,11 +360,11 @@ A bot requesting the complete list is limited to one accepted request per guild 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. -Only one member request runs at a time on one WebSocket. While one runs, a newer request replaces any earlier pending request and starts when the active one finishes. [Bounded requests](#bounded-requests) covers the four-slot limit this command shares with the other three. +Only one member request runs at a time on one WebSocket. While one runs, a newer request replaces any earlier pending request and starts when the active one finishes. [Bounded requests](#bounded-requests) covers the four-slot limit this command shares with the other bounded commands. ## Lazy Request -Opcode `14` sets the per-guild subscriptions that decide member list, typing, and synchronisation traffic for the session. +Opcode `14` sets the per-guild subscriptions that decide member list, typing, synchronisation, and [thread](/gateway/threads/#lazy-request-thread-options) traffic for the session. | Field | Type | Description | | --- | --- | --- | @@ -405,15 +407,15 @@ Opcode `14` sets the per-guild subscriptions that decide member list, typing, an 2 Subscriptions may be combined over a 100 ms window. Ranges sent during that window are merged for the same channel, and an empty range list clears its pending ranges -Fluxer applies each option only when its key is present, in the order `active`, `sync`, `member_list_channels`, `members`, `typing`. +Fluxer applies each option only when its key is present, in the order `active`, `sync`, `member_list_channels`, `members`, `typing`. The [thread options](/gateway/threads/#lazy-request-thread-options) `threads` and `thread_member_lists` follow `typing`. -Marking a guild active changes how much traffic it produces, and [Event filtering](/gateway/event-filtering/) specifies the difference. A transition from passive to active, and a transition from active to passive, both imply a sync even when `sync` is absent. Every sync request, implied or explicit, is dropped when the guild is already marked synced for that session. Going passive clears that mark, so the next sync request produces a fresh Guild Sync. +Marking a guild active changes how much traffic it produces, and [Event filtering](/gateway/event-filtering/) specifies the difference. A transition from passive to active, and a transition from active to passive, both imply a sync even when `sync` is absent. Every sync request, implied or explicit, is dropped when the guild is already marked synced for that session. Going passive clears that mark, so the next sync request produces a fresh Guild Sync. Going passive also clears the [`threads`](/gateway/threads/#lazy-request-thread-options) subscription. `member_list_channels` maps a channel ID to a list of `[start, end]` ranges. A range needs `start` at least 0, `end` at least `start`, `end` at most 100,000, and `end - start` at most 99. Ranges that fail those bounds are dropped, and each channel keeps at most the first 10 that pass. A channel key that is not a Snowflake is skipped. -`VIEW_CHANNEL` and `VIEW_CHANNEL_MEMBERS` together control the member list subscription, and both are evaluated for each channel separately. A channel the session cannot view, or can view without holding `VIEW_CHANNEL_MEMBERS` there, receives no [Guild Member List Update](/gateway/events/#guild-member-list-update) while its siblings subscribe normally. +`VIEW_CHANNEL` and `VIEW_CHANNEL_MEMBERS` together control the member list subscription, and both are evaluated for each channel separately. A channel the session cannot view, or can view without holding `VIEW_CHANNEL_MEMBERS` there, receives no [Guild Member List Update](/gateway/events/#guild-member-list-update) while its siblings subscribe normally. A thread ID in `member_list_channels` subscribes to nothing. A thread member list uses [`thread_member_lists`](/gateway/threads/#lazy-request-thread-options). -Subscribing a channel to at least one range drops the session's other member list subscriptions in that guild, so one session holds at most one member list per guild. +Subscribing a channel to at least one range drops the session's other member list subscriptions in that guild. One session holds at most one guild member list per guild. Thread member lists are counted separately. `members` entries that are not Snowflakes are dropped, and the first 1,000 that pass are kept. A member the session shares no viewable channel with is dropped as well. @@ -472,6 +474,6 @@ Results arrive in one [Channel Member Counts Update](/gateway/events/#channel-me ## Bounded requests -One WebSocket processes at most four bounded requests at once across [Request Guild Members](#request-guild-members), [Lazy Request](#lazy-request), [Request Guild Counts](#request-guild-counts), and [Request Channel Member Counts](#request-channel-member-counts). A command that arrives when all four slots are taken is dropped, without a close and without a result. Each request has a 10,000 ms deadline, after which it produces no further events. +One WebSocket processes at most four bounded requests at once across [Request Guild Members](#request-guild-members), [Lazy Request](#lazy-request), [Request Guild Counts](#request-guild-counts), [Request Channel Member Counts](#request-channel-member-counts), and [Request Forum Unreads](/gateway/threads/#request-forum-unreads). A command that arrives when all four slots are taken is dropped, without a close and without a result. Request Forum Unreads from a user session without `CHANNEL_THREADS` still closes with `4001`, as [Gateway threads](/gateway/threads/#request-forum-unreads) describes. Each request has a 10,000 ms deadline, after which it produces no further events. Request Guild Members also keeps one replaceable pending request while another member request is active, whether or not a slot is free. See [Bounded commands](/gateway/limits-and-rate-limits/#bounded-commands). diff --git a/fluxer_docs/src/content/docs/gateway/event-filtering.md b/fluxer_docs/src/content/docs/gateway/event-filtering.md index 17076165f..5bd108ba4 100644 --- a/fluxer_docs/src/content/docs/gateway/event-filtering.md +++ b/fluxer_docs/src/content/docs/gateway/event-filtering.md @@ -4,7 +4,7 @@ title: Event filtering description: Event visibility, guild subscriptions, ignored events, and sharding. --- -A client controls [Dispatch](/gateway/events/) traffic with [Lazy Request](/gateway/commands/#lazy-request) subscriptions and the [Identify](/gateway/commands/#identify) `ignored_events` list. Guild availability, permissions and sharding also restrict delivery. +A client controls [Dispatch](/gateway/events/) traffic with [Lazy Request](/gateway/commands/#lazy-request) subscriptions, the [Identify](/gateway/commands/#identify) `ignored_events` list, and the Identify `CHANNEL_THREADS` [session flag](/gateway/threads/#session-flag). Guild availability, permissions and sharding also restrict delivery. 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 mechanisms. @@ -17,12 +17,14 @@ Fluxer evaluates a guild-scoped Dispatch against these gates in order. | 1 | Guild availability | Whether the guild dispatches anything except [Guild Update](/gateway/events/#guild-update) | | 2 | Permission and visibility | Which sessions may see the event at all | | 3 | Guild subscription state | Whether a passive session in a guild with more than 250 members still receives it, as [Active and passive guilds](#active-and-passive-guilds) lists | -| 4 | Session-level filters | Whether the shard filter, and then the `ignored_events` list, drops it inside the session after the guild has already chosen the recipients | +| 4 | Session-level filters | Whether the thread access filter, the shard filter, or the `ignored_events` list drops it inside the session after the guild has already chosen the recipients | A guild with the `UNAVAILABLE_FOR_EVERYONE` or `UNAVAILABLE_FOR_EVERYONE_BUT_STAFF` feature fails gate 1. `UNAVAILABLE_FOR_EVERYONE_BUT_STAFF` decides what a session receives when it connects. A staff session receives the guild's full state, and every other session receives an `unavailable` stub. Gate 1 has no staff exemption, so a staff session also receives nothing but Guild Update while the feature is set. An account-scoped Dispatch skips gates 1 through 3 and is subject only to gate 4. Direct message traffic, relationship changes, and account record changes arrive that way. +A session without [thread access](/gateway/threads/#session-flag) receives none of the events on [Gateway threads](/gateway/threads/#dispatch-events). + ## Permission and visibility The guild resolves each event to one of these recipient sets. @@ -32,14 +34,19 @@ The guild resolves each event to one of these recipient sets. | Channel-scoped | [Channel Create](/gateway/events/#channel-create), [Channel Update](/gateway/events/#channel-update), [Channel Delete](/gateway/events/#channel-delete)1, [Message Create](/gateway/events/#message-create), [Message Delete Bulk](/gateway/events/#message-delete-bulk), [Typing Start](/gateway/events/#typing-start), [Channel Pins Update](/gateway/events/#channel-pins-update), [Webhooks Update](/gateway/events/#webhooks-update) | Sessions that can view the channel | | Message-access filtered | [Message Update](/gateway/events/#message-update), [Message Delete](/gateway/events/#message-delete), [Message Reaction Add](/gateway/events/#message-reaction-add), [Message Reaction Remove](/gateway/events/#message-reaction-remove), [Message Reaction Remove All](/gateway/events/#message-reaction-remove-all), [Message Reaction Remove Emoji](/gateway/events/#message-reaction-remove-emoji) | Sessions that can view the channel and can access that message | | Invite | [Invite Create](/gateway/events/#invite-create), [Invite Delete](/gateway/events/#invite-delete) | Sessions holding `MANAGE_CHANNELS` on the invite's channel | -| Audit log | [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) | Sessions holding `VIEW_AUDIT_LOG` in the guild | +| Audit log | [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) | Sessions holding `VIEW_AUDIT_LOG` in the guild2 | +| Thread | The events on [Gateway threads](/gateway/threads/#dispatch-events) | The recipients each event states there | | Guild-wide | Everything else | Every session connected to the guild | 1 Channel Delete is filtered against the guild state as it was before the deletion, so the session that could see the channel is the session that learns it is gone +2 A session without [thread access](/gateway/threads/#session-flag) receives no `THREAD_CREATE`, `THREAD_UPDATE`, or `THREAD_DELETE` entry and no entry about a message in a thread + +An event in the first two rows whose channel is a thread reaches the sessions [Events in a thread](#events-in-a-thread) describes. Thread visibility and message access follow [Thread permissions](/http-api/threads/#thread-permissions). + Every one of those sets excludes a session that has not yet received the guild's initial state. -Channel visibility is `VIEW_CHANNEL` on the channel, plus extensions. A category is visible when at least one of its children is visible. A user with a live voice connection in a channel keeps virtual access to it whenever the channel would otherwise stop being visible. That covers a role or overwrite change removing `VIEW_CHANNEL`, and a move into a channel the user cannot view. Virtual access is keyed by user, so it applies to every session of that user. It is dropped when the user's voice connection to the channel ends. +Channel visibility is `VIEW_CHANNEL` on the channel, plus extensions. A category is visible when at least one of its children is visible. For a session without [thread access](/gateway/threads/#session-flag), a forum or media channel does not count as a visible child. A user with a live voice connection in a channel keeps virtual access to it whenever the channel would otherwise stop being visible. That covers a role or overwrite change removing `VIEW_CHANNEL`, and a move into a channel the user cannot view. Virtual access is keyed by user, so it applies to every session of that user. It is dropped when the user's voice connection to the channel ends. Message access is `READ_MESSAGE_HISTORY` on the channel. Without that permission a session still receives events for messages newer than the guild's [message history cutoff](/http-api/guilds/#guild-object). A guild that sets no cutoff offers no such fallback, so a session without `READ_MESSAGE_HISTORY` receives none of the message-access filtered events there. @@ -53,7 +60,7 @@ Message access is `READ_MESSAGE_HISTORY` on the channel. Without that permission ## Active and passive guilds -A user session is passive in every guild until [Lazy Request](/gateway/commands/#lazy-request) marks that guild `active: true`. A bot session is never passive. A guild with 250 members or fewer is active for every session, so the rule below applies only to a passive user session in a guild with more than 250 members. +A user session is passive in every guild until [Lazy Request](/gateway/commands/#lazy-request) marks that guild `active: true`. The guild named in the [Identify](/gateway/commands/#identify) `initial_guild_id` is active from the start. A bot session is never passive. A guild with 250 members or fewer is active for every session. The rule below applies only to a passive user session in a guild with more than 250 members. Thread message delivery is the exception, as [Events in a thread](#events-in-a-thread) describes. Such a session receives exactly this set: @@ -63,12 +70,15 @@ Such a session receives exactly this set: - [Channel Create](/gateway/events/#channel-create), [Channel Update](/gateway/events/#channel-update), [Channel Update Bulk](/gateway/events/#channel-update-bulk), and [Channel Delete](/gateway/events/#channel-delete) - [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) - [Passive Updates](/gateway/events/#passive-updates) -- [Message Create](/gateway/events/#message-create) when the message mentions the session's user +- [Message Create](/gateway/events/#message-create) when the message mentions the session's user or that user wrote it +- [Message Update](/gateway/events/#message-update) when the message has a mention that overrides the passive filter for [Message Create](/gateway/events/#message-create) - [Guild Member Update](/gateway/events/#guild-member-update) and [Guild Member Remove](/gateway/events/#guild-member-remove) when the subject is the session's own user +- The thread events [Gateway threads](/gateway/threads/#dispatch-events) states for a passive session Every other Dispatch the guild produces is suppressed: -- [Message Update](/gateway/events/#message-update), [Message Delete](/gateway/events/#message-delete), and [Message Delete Bulk](/gateway/events/#message-delete-bulk) +- [Message Update](/gateway/events/#message-update) for a message that does not mention the session's user +- [Message Delete](/gateway/events/#message-delete) and [Message Delete Bulk](/gateway/events/#message-delete-bulk) - Every reaction event - [Invite Create](/gateway/events/#invite-create) and [Invite Delete](/gateway/events/#invite-delete) - [Channel Pins Update](/gateway/events/#channel-pins-update) @@ -90,9 +100,13 @@ Every 30 seconds a passive session receives [Passive Updates](/gateway/events/#p The override applies to every session, including a bot session. The only way for a bot to stop Typing Start in one guild and keep it in other guilds is to set `typing` to false for that guild. +### Events in a thread + +A message, reaction, typing, or pin event in a thread follows the rules on this page and also needs one of the conditions in [Gateway threads](/gateway/threads/#dispatch-events). A session meets the [`threads`](/gateway/threads/#lazy-request-thread-options) condition only while the guild is active for it, through [Lazy Request](/gateway/commands/#lazy-request) `active` or the [Identify](/gateway/commands/#identify) `initial_guild_id`. A guild with 250 members or fewer gives no exception to that condition. + ### Member lists -[Guild Member List Update](/gateway/events/#guild-member-list-update) has its own subscription. A session receives it only for a channel it named in `member_list_channels`, and only while it can view that channel and holds `VIEW_CHANNEL_MEMBERS` on it. One session holds at most one member list subscription per guild. +[Guild Member List Update](/gateway/events/#guild-member-list-update) has its own subscription. A session receives it only for a channel it named in `member_list_channels`, and only while it can view that channel and holds `VIEW_CHANNEL_MEMBERS` on it. One session holds at most one member list subscription per guild. [Thread Member List Update](/gateway/threads/#thread-member-list-update) has a separate subscription, as [Lazy Request thread options](/gateway/threads/#lazy-request-thread-options) describes. ## Presence subscriptions diff --git a/fluxer_docs/src/content/docs/gateway/events.md b/fluxer_docs/src/content/docs/gateway/events.md index f7cfd466b..9c8162eff 100644 --- a/fluxer_docs/src/content/docs/gateway/events.md +++ b/fluxer_docs/src/content/docs/gateway/events.md @@ -28,13 +28,13 @@ A Dispatch reports a change or command result through the [Gateway](/gateway/ove ## Dispatch delivery -[Event filtering](/gateway/event-filtering/) defines delivery by guild availability, channel visibility, permissions, and session settings. Account-scoped Dispatches go through only the session-level filters, which are the shard filter and the `ignored_events` list. +[Event filtering](/gateway/event-filtering/) defines delivery by guild availability, channel visibility, permissions, and session settings. Account-scoped Dispatches go through the session-level filters alone. Those are the [thread access](/gateway/threads/#session-flag) filter, the shard filter, and the `ignored_events` list. 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. -A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it is [Guild Sync](#guild-sync), [Guild Member List Update](#guild-member-list-update), or [Guild Members Chunk](#guild-members-chunk). Those are delivered live and never retained. A single oversized Dispatch is delivered but not retained, as [Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) describes. [Ready](#ready) and the guild burst that follows it for a bot session are also sent outside the replay buffer. The initial [Call Create](#call-create) events are retained like any other Dispatch and are replayed on Resume. +A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it is [Guild Sync](#guild-sync), [Guild Member List Update](#guild-member-list-update), [Guild Members Chunk](#guild-members-chunk), [Thread List Sync](/gateway/threads/#thread-list-sync), or [Thread Member List Update](/gateway/threads/#thread-member-list-update). Those are delivered live and never retained. A single oversized Dispatch is delivered but not retained, as [Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) describes. [Ready](#ready) and the guild burst that follows it for a bot session are also sent outside the replay buffer. The initial [Call Create](#call-create) events are retained like any other Dispatch and are replayed on Resume. ## Dispatch events @@ -80,6 +80,14 @@ A Dispatch is buffered for [Resume](/gateway/commands/#resume) replay unless it | [Webhooks Update](#webhooks-update) | The webhook set of a viewable guild channel changes | Channel visibility | | [Invite Create](#invite-create) | An invite is created | Invite audience | | [Invite Delete](#invite-delete) | An invite is deleted | Invite audience | +| [Thread Create](/gateway/threads/#thread-create) | A thread is created or the session joins one | Thread visibility | +| [Thread Update](/gateway/threads/#thread-update) | A visible thread changes | Thread visibility | +| [Thread Delete](/gateway/threads/#thread-delete) | A thread is deleted | Thread visibility | +| [Thread List Sync](/gateway/threads/#thread-list-sync) | The session's active thread set is replaced | Guild connection | +| [Thread Member Update](/gateway/threads/#thread-member-update) | The session's own thread membership changes | Current user | +| [Thread Members Update](/gateway/threads/#thread-members-update) | Users join or leave a thread | Thread visibility | +| [Thread Member List Update](/gateway/threads/#thread-member-list-update) | A subscribed thread member list resyncs | Member list subscription | +| [Forum Unreads](/gateway/threads/#forum-unreads) | Unread counts for forum posts are returned | Command response | | [Guild Member Add](#guild-member-add) | A user becomes a member of a connected guild | Guild connection | | [Guild Member Update](#guild-member-update) | A member's guild state or public user representation changes | Guild connection | | [Guild Member Remove](#guild-member-remove) | A user stops being a member of a connected guild | Guild connection | @@ -130,9 +138,9 @@ The initial session state. Sent once after a successful [Identify](/gateway/comm | presences3 | array[[presence object](#presence-object)] | Visible presences at connection time | | users4 | array[[partial user](/http-api/users/#partial-user-object) object] | Users referenced by the payload | | sessions | array[[session presence object](#session-presence-object)] | The account's other live sessions | -| read_states | array[[read state](/http-api/read-states/#read-state-object) object] | Per-channel read state | +| read_states5 | array[[read state](/http-api/read-states/#read-state-object) object] | Per-channel read state | | user_settings | ?[user settings](/http-api/users/#user-settings-object) object | Account-wide settings | -| user_guild_settings | array[[user guild settings](/http-api/users/settings/#user-guild-settings-object) object] | Per-guild notification settings | +| user_guild_settings5 | array[[user guild settings](/http-api/users/settings/#user-guild-settings-object) object] | Per-guild notification settings | | notes | map[snowflake, string] | Private notes keyed by user ID | | pinned_dms | array[snowflake] | Pinned private channel IDs | | favorite_memes | array[[meme](/http-api/memes/#meme-object) object] | Saved memes | @@ -154,6 +162,8 @@ The initial session state. Sent once after a successful [Identify](/gateway/comm 4 Collected from the account's relationships, the recipients of its private channels, and the members in `guilds`, deduplicated by account ID. A bot session always receives an empty array here +5 A session without [thread access](/gateway/threads/#session-flag) receives no read state that has `flags`, no channel override of a forum or media channel, and no `flags` on an override + Ready is sent outside the replay buffer, so a [Resume](/gateway/commands/#resume) never replays it. A bot session receives one [Guild Create](#guild-create) per available guild immediately after Ready, and one [Guild Delete](#guild-delete) per unavailable guild. Together they resolve the entries of the `guilds` array. Those Dispatches are also sent outside the replay buffer. @@ -180,6 +190,7 @@ The same structure appears in Ready, [Guild Create](#guild-create), and [Guild S | joined_at | ?ISO8601 timestamp | When the account joined the guild, null when the account is not a member | | unavailable? | boolean | Whether the guild is unavailable | | unavailable_hidden? | boolean | Whether an unavailable guild is hidden from the client | +| threads?4 | array[[thread](/http-api/threads/#thread-object) object] | The active threads the session receives, as [Guild threads](/gateway/threads/#guild-threads) describes | 1 The session's own member object plus the member object of every participant named by `voice_states`, and nothing else. A client that needs the rest of the roster asks for it with [Request Guild Members](/gateway/commands/#request-guild-members) or subscribes to a member list through [Lazy Request](/gateway/commands/#lazy-request) @@ -187,6 +198,10 @@ The same structure appears in Ready, [Guild Create](#guild-create), and [Guild S 3 Guild presences arrive as separate [Presence Update](#presence-update) and [Presence Update Bulk](#presence-update-bulk) Dispatches +4 Present only for a bot session and for a user session that set [`CHANNEL_THREADS`](/gateway/threads/#session-flag) + +A user session without `CHANNEL_THREADS` receives reduced `roles` and `channels`, as [Session flag](/gateway/threads/#session-flag) describes. + An unavailable guild is reduced to `id` and `unavailable: true`, plus `unavailable_hidden: true` when the guild is hidden. It has none of the other fields. Every entry in a bot session's [Ready](#ready) `guilds` array has this form, with `id` and `unavailable: true` alone. Inside [Ready](#ready), and inside the [Guild Create](#guild-create) burst a bot receives immediately after Ready, each entry in `members` has its `user` replaced by `{"id": "..."}`. A bot session's Ready has no `members` at all, because each of its guilds is an unavailable guild, so a bot sees the reduced form in the burst alone. On a user session the removed accounts appear in the Ready payload's `users` array. A bot's `users` array is empty, so a bot pulls those accounts with [Request Guild Members](/gateway/commands/#request-guild-members). A [Guild Create](#guild-create) sent later in the session, and every [Guild Sync](#guild-sync), have the members with `user` intact. @@ -277,7 +292,7 @@ Fluxer republishes the account's presence on every settings update, whether or n ### USER_GUILD_SETTINGS_UPDATE -One guild's notification settings changed. The payload is that guild's complete user guild settings object. +One guild's notification settings changed. The payload is that guild's complete user guild settings object. A session without [thread access](/gateway/threads/#session-flag) receives it without the channel overrides of forum and media channels and without `flags` on an override. ### USER_NOTE_UPDATE @@ -342,6 +357,8 @@ A relationship ended. The current user saved a message. The payload is the complete [message object](/http-api/messages/#message-object) as that user sees it. +A session without [thread access](/gateway/threads/#session-flag) receives no Saved Message Create for a message in a thread. + ### SAVED_MESSAGE_DELETE The current user unsaved a message. @@ -380,7 +397,7 @@ The current user deleted a meme. A guild became available to the session. The payload is a [guild ready object](#guild-ready-object). -`roles`, `channels`, `emojis`, `stickers`, and `voice_states` are always complete. A client that stores any of them for the guild replaces its stored list with the new array. `members` is a partial list. A client adds or updates those members and keeps every other member it already stores. +`roles`, `channels`, `emojis`, `stickers`, and `voice_states` are always complete. A client that stores any of them for the guild replaces its stored list with the new array. `members` is a partial list. A client adds or updates those members and keeps every other member it already stores. `threads` is the complete set [Guild threads](/gateway/threads/#guild-threads) describes, and a client replaces its stored threads for the guild with it. Every session receives Guild Create when a guild becomes available after Ready, for example after joining one or after an unavailable guild recovers. A bot session also receives one for each available guild in the burst that follows Ready. @@ -475,9 +492,11 @@ A client that stores the guild's stickers replaces them with this array. Fluxer A channel became visible to the session, whether newly created or newly permitted. The payload is the complete [channel object](/http-api/channels/#channel-object), with `guild_id` present for a guild channel. +A session with thread access that gains view access to a channel with active threads then receives [Thread List Sync](/gateway/threads/#thread-list-sync) for that channel. + ### CHANNEL_UPDATE -A visible channel changed. The payload is the complete [channel object](/http-api/channels/#channel-object). Converting a text channel into an announcement channel, or back, emits it with the new `type`. +A visible channel changed. The payload is the complete [channel object](/http-api/channels/#channel-object). Converting a text channel into an announcement channel, or back, emits it with the new `type`. That conversion also emits [Thread Update](/gateway/threads/#thread-update) for each active public thread of the channel, as [Thread types](/http-api/threads/#thread-types) describes. ### CHANNEL_UPDATE_BULK @@ -496,6 +515,8 @@ A channel left the session's visibility, whether deleted or newly hidden. The pa Recipients are the sessions that could see the channel before it was deleted. +Deleting a channel deletes its threads, and Fluxer emits no [Thread Delete](/gateway/threads/#thread-delete) for them. A client removes every thread whose `parent_id` is the channel. + ### CHANNEL_RECIPIENT_ADD A user joined a group direct message the session belongs to. @@ -570,6 +591,8 @@ A user stopped being a member of a guild the session is connected to. In a guild with more than 250 members, a [passive](/gateway/event-filtering/#active-and-passive-guilds) session receives this event only when the subject is its own user. +Removal from the guild removes every membership in its threads. Fluxer emits a [Thread Members Update](/gateway/threads/#thread-members-update) for each of them, as [Thread member flags](/http-api/thread-members/#thread-member-flags) states. + ### GUILD_MEMBERS_CHUNK Answers [Request Guild Members](/gateway/commands/#request-guild-members) for the requesting session. @@ -641,7 +664,7 @@ An update that changes nothing records no entry, as the [audit actions](/http-ap The `ip` change key is stripped from `changes`, so an entry whose only change was `ip` has no `changes` at all. A client MUST treat an absent `options` or `changes` as an empty set. -Recipients are every session in the guild that holds `VIEW_AUDIT_LOG`, including the acting session. +Recipients are every session in the guild that holds `VIEW_AUDIT_LOG`, including the acting session. A session without [thread access](/gateway/threads/#session-flag) receives no `THREAD_CREATE`, `THREAD_UPDATE`, or `THREAD_DELETE` entry and no entry about a message in a thread. ### GUILD_BAN_ADD @@ -729,6 +752,8 @@ A passive session in a guild with more than 250 members receives missed changes `channels` contains only the channels whose `last_message_id` changed since the previous update for that session, and only channels that session can view. `voice_states` contains only the voice states whose `version` advanced. +For a session with [thread access](/gateway/threads/#session-flag), `channels` also contains the active threads its user joined. + ## Messages and reactions ### MESSAGE_CREATE @@ -747,7 +772,9 @@ A visible message was created. The payload is the complete [message object](/htt Each copy of a [published message](/http-api/messages/#crosspost-message) arrives in its following channel as Message Create, with the `IS_CROSSPOST` flag, and so does the `CHANNEL_FOLLOW_ADD` system message of a new follow. A copy mentions nobody. -Message Create alone overrides both the passive filter and the `ignored_events` list, and the two use different tests. A direct mention, a mention of one of the user's roles, an everyone mention, or a here mention overrides the passive filter. A direct, everyone, or here mention alone overrides the `ignored_events` list. +Message Create alone overrides both the passive filter and the `ignored_events` list, and the two use different tests. A direct mention, a mention of one of the user's roles, an everyone mention, or a here mention overrides the passive filter. A message the session's user wrote overrides the passive filter too. A direct, everyone, or here mention alone overrides the `ignored_events` list. + +A Message Create in a thread also needs one of the conditions in [Dispatch events](/gateway/threads/#dispatch-events). [Thread messages](/http-api/threads/#thread-messages) lists the members a message adds to the thread. ### MESSAGE_UPDATE @@ -755,8 +782,12 @@ A visible message changed. The payload is the complete current [message object]( Recipients must hold `READ_MESSAGE_HISTORY` on the channel, or the message must be newer than the guild's message history cutoff. +In a guild with more than 250 members, a [passive](/gateway/event-filtering/#active-and-passive-guilds) session receives this event only for a direct mention of its user, a mention of one of its roles, an everyone mention, or a here mention. + [Publishing](/http-api/messages/#crosspost-message) a message emits it in the announcement channel with the `CROSSPOSTED` flag set. An edit of a published message emits it for each copy once Fluxer has copied the change, and deleting the published message emits it for each copy with the `SOURCE_MESSAGE_DELETED` flag and the content removed. +[Start thread from message](/http-api/threads/#start-thread-from-message) emits it for the source message with `HAS_THREAD` and `thread`, and [Delete thread](/http-api/threads/#delete-thread) emits it with `HAS_THREAD` cleared. A session without [thread access](/gateway/threads/#session-flag) receives neither. + ### MESSAGE_DELETE One visible message was deleted. @@ -795,6 +826,11 @@ The current user's read state advanced for a channel, usually because another of | mention_count | integer | Remaining mention count for the channel | | manual? | boolean | Whether the acknowledgement was explicit | | version? | string | Read state version as a decimal string | +| flags?1 | integer | The [read state flags](/http-api/read-states/#read-state-flags) | + +1 Present only on the entry of a thread, a forum channel, or a media channel + +A session without [thread access](/gateway/threads/#session-flag) receives no Message ACK for an entry that has `flags`. ### MESSAGE_REACTION_ADD @@ -896,7 +932,7 @@ A visible user began typing in a channel. | guild_id? | snowflake | Guild the channel belongs to | | member? | [guild member](/http-api/guild-members/#guild-member-object) object | The typing user's guild member object, present in a guild channel | -The `typing` override set through [Lazy Request](/gateway/commands/#lazy-request) decides delivery in a guild. With no override, a session receives the event when it is [active](/gateway/event-filtering/#active-and-passive-guilds) in the guild or when the guild has 250 members or fewer, so a passive session in a small guild still receives it. A guild that sets the `TYPING_EVENTS` bit in its [disabled operations](/http-api/guilds/#disabled-guild-operations) produces the event for nobody. +The `typing` override set through [Lazy Request](/gateway/commands/#lazy-request) decides delivery in a guild. With no override, a session receives the event when it is [active](/gateway/event-filtering/#active-and-passive-guilds) in the guild or when the guild has 250 members or fewer, so a passive session in a small guild still receives it. A guild that sets the `TYPING_EVENTS` bit in its [disabled operations](/http-api/guilds/#disabled-guild-operations) produces the event for nobody. Typing in a thread also needs one of the conditions in [Dispatch events](/gateway/threads/#dispatch-events). ### CHANNEL_PINS_UPDATE @@ -910,13 +946,15 @@ A channel's most recent pin time changed. ### CHANNEL_PINS_ACK -The current user acknowledged a channel's pins. Every session of the account receives it, including the one that issued the acknowledgement. +The current user acknowledged a channel's pins. Every session of the account receives it, including the one that issued the acknowledgement, except as stated below. | Field | Type | Description | | --- | --- | --- | | channel_id | snowflake | Channel whose pins were acknowledged | | timestamp | ISO8601 timestamp | Time the acknowledgement recorded for the channel | +A session without [thread access](/gateway/threads/#session-flag) receives no Channel Pins ACK for a thread. + ## Voice and calls ### VOICE_STATE_UPDATE diff --git a/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md b/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md index 15c650795..285e323a0 100644 --- a/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md +++ b/fluxer_docs/src/content/docs/gateway/limits-and-rate-limits.md @@ -46,17 +46,19 @@ One source IP address makes 300 Identify attempts in each fixed 60-second window Presence Update accepts five commands per WebSocket in a rolling 20-second window. A further update is discarded without closing the connection. +[Request Forum Unreads](/gateway/threads/#request-forum-unreads) accepts five commands per WebSocket in a rolling 5-second window. A further command is discarded without closing the connection. + Voice State Update processes the first two commands per session in a rolling one-second window immediately. Later updates enter a per-session queue that holds at most 64 commands and drains one command every 500 ms. A newer update replaces an older queued update for the same `guild_id` and `connection_id` pair, and a full queue discards its oldest entry before accepting the new one. Request Guild Members has three command-specific budgets. Each account may send 12 requests in a rolling 10-second window, and each guild accepts 40 requests in a rolling 10-second window across all accounts. A request over either of these budgets is discarded without a result and without a close. A bot requesting a complete member list is limited to one accepted request per guild every 30 seconds, and a request inside that window produces [Rate Limited](/gateway/events/#rate-limited). The budget is keyed by the account and the guild together, so reconnecting does not reset it. ## Bounded commands -One WebSocket processes at most four bounded requests at once across [Request Guild Members](/gateway/commands/#request-guild-members), [Lazy Request](/gateway/commands/#lazy-request), [Request Guild Counts](/gateway/commands/#request-guild-counts), and [Request Channel Member Counts](/gateway/commands/#request-channel-member-counts). Each request has a 10,000 ms deadline, after which it stops. Events already emitted are not retracted, so a command that emits its result in several events can deliver a partial result and then stop. +One WebSocket processes at most four bounded requests at once across [Request Guild Members](/gateway/commands/#request-guild-members), [Lazy Request](/gateway/commands/#lazy-request), [Request Guild Counts](/gateway/commands/#request-guild-counts), [Request Channel Member Counts](/gateway/commands/#request-channel-member-counts), and [Request Forum Unreads](/gateway/threads/#request-forum-unreads). Each request has a 10,000 ms deadline, after which it stops. Events already emitted are not retracted. A command that emits its result in several events can deliver a partial result and then stop. Request Guild Members keeps one replaceable pending request while another member request is active. The most recent further request replaces any earlier pending one and starts when the active request finishes. -A bounded command that finds all four slots occupied is dropped. There is no queue, no result event, and no close. +A bounded command that finds all four slots occupied is dropped. There is no queue, no result event, and no close. Request Forum Unreads from a user session without `CHANNEL_THREADS` still closes with `4001`, as [Gateway threads](/gateway/threads/#request-forum-unreads) describes. Request Guild Counts queries each guild with a 2,000 ms deadline under an overall 3,000 ms batch deadline. Request Channel Member Counts queries its guild with a 2,000 ms deadline. A guild that misses its deadline is omitted from the result. @@ -70,7 +72,7 @@ Both byte bounds measure in-memory size, not wire bytes, so both figures are app [Guild Members Chunk](/gateway/events/#guild-members-chunk) is delivered live and is never retained for Resume, whatever its size. -[Guild Sync](/gateway/events/#guild-sync) and [Guild Member List Update](/gateway/events/#guild-member-list-update) are also excluded from replay. A replay can therefore have sequence gaps even within its retention window. +[Guild Sync](/gateway/events/#guild-sync), [Guild Member List Update](/gateway/events/#guild-member-list-update), [Thread List Sync](/gateway/threads/#thread-list-sync), and [Thread Member List Update](/gateway/threads/#thread-member-list-update) are also excluded from replay. A replay can therefore have sequence gaps even within its retention window. A heartbeat with a sequence discards every retained Dispatch at or below that sequence and records it as the acknowledged sequence. A client MUST acknowledge only a sequence whose events it has finished processing. A heartbeat with `null`, and a heartbeat with a sequence below the acknowledged sequence, change nothing. Resume neither acknowledges nor evicts. A client that never heartbeats with a sequence keeps its full window until the count or byte bound evicts from the front. @@ -84,9 +86,9 @@ Presence updates can be dropped under load without closing the connection. Request Guild Members accepts at most 100 user IDs, a nonce of at most 32 bytes, and a result limit from 0 through 100. Duplicate guild IDs are collapsed, an out-of-range limit is clamped, and an oversized nonce becomes null. An oversized `user_ids` array abandons the request. The nonce is echoed only when the request named exactly one guild. -Lazy Request accepts at most 10 member list ranges per channel, each with `end` at most 100,000 and `end - start` at most 99, and at most 1,000 explicit member IDs per guild. Ranges and IDs that fail validation are dropped, and valid entries past either ceiling are truncated. +Lazy Request accepts at most 10 member list ranges per channel, each with `end` at most 100,000 and `end - start` at most 99, and at most 1,000 explicit member IDs per guild. Ranges and IDs that fail validation are dropped, and valid entries past either ceiling are truncated. It keeps at most 10 [`thread_member_lists`](/gateway/threads/#lazy-request-thread-options) entries per guild. -Request Guild Counts accepts at most 100 guild IDs after deduplication. Request Channel Member Counts accepts at most 25 channel IDs after deduplication. Both nonces run from 1 through 64 bytes, and a nonce outside that bound is omitted from the result. +Request Guild Counts accepts at most 100 guild IDs after deduplication. Request Channel Member Counts accepts at most 25 channel IDs after deduplication. Both nonces run from 1 through 64 bytes, and a nonce outside that bound is omitted from the result. [Request Forum Unreads](/gateway/threads/#request-forum-unreads) accepts at most 40 posts after deduplication. Identify accepts at most 256 `ignored_events` entries. A longer array closes with `4002` and reason `Invalid identify payload`. Fluxer coerces or drops a value outside any other command payload bound. [Client commands](/gateway/commands/) states the exact coercion or drop rule for each field. diff --git a/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md b/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md index 2fe82f4e0..d54c4bada 100644 --- a/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md +++ b/fluxer_docs/src/content/docs/gateway/opcodes-and-close-codes.md @@ -26,6 +26,7 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa | 14 | Lazy Request | Client to server | | 15 | Request Guild Counts | Client to server | | 16 | Request Channel Member Counts | Client to server | +| 28 | Request Forum Unreads | Client to server | 1 Fluxer never sends Opcode 9 with `d: true`, so an Invalid Session is never an instruction to resume @@ -49,6 +50,7 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa | 14 | [Lazy Request object](/gateway/commands/#lazy-request) | The client replaces bounded guild subscriptions | | 15 | [Request Guild Counts object](/gateway/commands/#request-guild-counts) | The client requests current guild count records | | 16 | [Request Channel Member Counts object](/gateway/commands/#request-channel-member-counts) | The client requests channel count records | +| 28 | [Request Forum Unreads object](/gateway/threads/#request-forum-unreads) | The client requests unread counts for posts in one forum or media channel | 1 [Resumed](/gateway/events/#resumed) has the session's current sequence without advancing it, and a replayed Dispatch keeps the sequence it was first sent with @@ -62,7 +64,7 @@ An opcode is the number that names a [Gateway payload](/gateway/overview/#gatewa 6 After the frame, a socket whose session ended is unauthenticated. After a failed Resume, a socket that already held a session still holds it -The registry is complete. Opcode 13 and every value above 16 are undefined. +The registry is complete. Opcode 13, the values 17 through 27, and every value above 28 are undefined. Fluxer resolves an inbound payload in this order. @@ -70,7 +72,9 @@ Fluxer resolves an inbound payload in this order. 2. Heartbeat and Resume are handled whether or not a session is attached. 3. Identify closes with `4005` when a session is already attached. 4. Every remaining opcode closes with `4003` while no session is attached. -5. With a session attached, Presence Update, Voice State Update, Request Guild Members, Lazy Request, Request Guild Counts, and Request Channel Member Counts are handled. Every other opcode, including a server opcode and an undefined value, closes with `4001`. +5. With a session attached, Presence Update, Voice State Update, Request Guild Members, Lazy Request, Request Guild Counts, Request Channel Member Counts, and Request Forum Unreads are handled. Every other opcode, including a server opcode and an undefined value, closes with `4001`. + +Request Forum Unreads from a user session that did not set [`CHANNEL_THREADS`](/gateway/threads/#session-flag) also closes with `4001`. :::note[Unknown server opcodes are forward compatible] A client SHOULD log an unknown opcode and ignore the frame, and MUST NOT close or reconnect solely because the server used an opcode newer than this registry. @@ -81,7 +85,7 @@ A client SHOULD log an unknown opcode and ignore the frame, and MUST NOT close o | Code | Name | Meaning | | --- | --- | --- | | 4000 | Unknown error | Drain, an unclassified session creation error, or a Resume whose retained session could not be reached | -| 4001 | Unknown opcode | The opcode is undefined, is a server opcode, or the payload has no `d` | +| 4001 | Unknown opcode | An undefined or server opcode, a payload with no `d`, or opcode 28 without thread access or with a non-object `d` | | 4002 | Decode error | The payload size, compression stream, encoding, or command fields are invalid | | 4003 | Not authenticated | An authenticated command arrived before Identify or Resume attached a session | | 4004 | Authentication failed | The token is invalid, or it does not own the retained session named by Resume | @@ -154,7 +158,7 @@ The Gateway sends an exact reason string with every application close. | `Decode failed` | 4002 | The payload is not valid JSON, or decodes to something other than an object | | `Invalid payload` | 4002 | The decoded object has no `op` | | `Rate limited` | 4008 | The connection, source IP, or user client payload budget was exceeded | -| `Unknown opcode` | 4001 | The opcode is undefined, is a server opcode, or the payload has no `d` | +| `Unknown opcode` | 4001 | An undefined or server opcode, a payload with no `d`, or opcode 28 without [thread access](/gateway/threads/#session-flag) or with a non-object `d` | | `Not authenticated` | 4003 | An authenticated command arrived before a session was attached | | `Already authenticated` | 4005 | Identify arrived while a session was attached, or with no `d` | | `Invalid identify payload` | 4002 | Identify is missing `token` or `properties`, or a field has the wrong type | @@ -181,7 +185,7 @@ The Gateway sends an exact reason string with every application close. Reason strings are stable wire values. A client branches on the code and MAY record the reason for diagnosis. :::note[Some refusals send nothing] -A held or discarded Identify, an over-budget Presence Update, a dropped bounded request, and a rejected voice state update produce no close and no reason. [Client commands](/gateway/commands/) states which command behaves that way. +A held or discarded Identify, an over-budget Presence Update, an over-budget [Request Forum Unreads](/gateway/threads/#request-forum-unreads), a dropped bounded request, and a rejected voice state update produce no close and no reason. [Client commands](/gateway/commands/) states which command behaves that way. ::: ## Ordinary WebSocket closes diff --git a/fluxer_docs/src/content/docs/gateway/overview.md b/fluxer_docs/src/content/docs/gateway/overview.md index 87f39d434..908116301 100644 --- a/fluxer_docs/src/content/docs/gateway/overview.md +++ b/fluxer_docs/src/content/docs/gateway/overview.md @@ -86,7 +86,7 @@ A decoded payload that is not a JSON object closes with `4002` and reason `Decod A Dispatch is a server-to-client event payload. Every live Dispatch advances the session sequence by one. A session starts at sequence 0, so the [Ready](/gateway/events/#ready) sequence is 1. -A replayed Dispatch keeps its original sequence, and a replayed run can have gaps, because Guild Sync, Guild Member List Update, and Guild Members Chunk are delivered live and never retained. [Resumed](/gateway/events/#resumed) has the current sequence and does not advance it. The next live Dispatch after Resumed has that sequence plus one. The sequence is local to one Gateway session and has no meaning across sessions or shards. +A replayed Dispatch keeps its original sequence, and a replayed run can have gaps. [Dispatch delivery](/gateway/events/#dispatch-delivery) lists the events that are delivered live and never retained. [Resumed](/gateway/events/#resumed) has the current sequence and does not advance it. The next live Dispatch after Resumed has that sequence plus one. The sequence is local to one Gateway session and has no meaning across sessions or shards. ## Framing @@ -257,7 +257,7 @@ Unlike Identify, Resume is accepted in every open state. A socket that already h When the resumed session was attached to a different socket, that socket receives Opcode 7 Reconnect and then closes with `4000`. :::caution[Retention covers reconnection recovery only] -[Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) states the exact bounds, and Guild Sync, Guild Member List Update, and Guild Members Chunk are never retained. +[Limits and rate limits](/gateway/limits-and-rate-limits/#replay-and-backpressure) states the exact bounds, and [Dispatch delivery](/gateway/events/#dispatch-delivery) lists the events that are never retained. ::: ## Reconnect @@ -307,4 +307,4 @@ Fluxer has no large bot tier, no shard-count alignment requirement, and no Ident Dispatch ordering applies within one Gateway session. It creates no total order across shards, HTTP responses, or Media Proxy operations. -[Guild Create](/gateway/events/#guild-create) and [Guild Sync](/gateway/events/#guild-sync) send the complete roles, channels, emojis, stickers, and voice states for the guild they name, and a client replaces its stored lists with them, as Guild Create describes. Every other Dispatch for that guild changes part of that stored state. +[Guild Create](/gateway/events/#guild-create) and [Guild Sync](/gateway/events/#guild-sync) send the complete roles, channels, emojis, stickers, and voice states for the guild they name, and a client replaces its stored lists with them, as Guild Create describes. A client replaces its stored threads for the guild with `threads` the same way. Every other Dispatch for that guild changes part of that stored state. diff --git a/fluxer_docs/src/content/docs/gateway/threads.md b/fluxer_docs/src/content/docs/gateway/threads.md index c2b5fa376..43f936157 100644 --- a/fluxer_docs/src/content/docs/gateway/threads.md +++ b/fluxer_docs/src/content/docs/gateway/threads.md @@ -2,13 +2,12 @@ # SPDX-License-Identifier: AGPL-3.0-or-later title: Gateway threads description: Thread Dispatch events, thread subscriptions, forum unread counts, and the thread session flag. -draft: true --- -A session receives [threads](/http-api/threads/) through the Dispatch events below. A guild ready object in [Ready](/gateway/events/#ready), [Guild Create](/gateway/events/#guild-create), and [Guild Sync](/gateway/events/#guild-sync) has `threads` only when the session can view threads in that guild. A client treats the presence of that key as the signal that threads, forums, and media channels exist in the guild. +A session receives [threads](/http-api/threads/) through the Dispatch events below. A [guild ready object](/gateway/events/#guild-ready-object) in [Ready](/gateway/events/#ready), [Guild Create](/gateway/events/#guild-create), and [Guild Sync](/gateway/events/#guild-sync) has `threads` only for a session with thread access. -:::note[Threads need the `CHANNEL_THREADS` capability] -A user session receives thread data only when it sets `CHANNEL_THREADS` in Identify. A bot session needs no flag. +:::note[User sessions need the `CHANNEL_THREADS` session flag] +A user session has thread access only when it sets `CHANNEL_THREADS` in Identify. A bot session always has thread access and needs no flag. HTTP routes read a separate client capability, as [Client capability](/http-api/threads/#client-capability) describes. ::: ## Session flag @@ -17,28 +16,39 @@ A user session receives thread data only when it sets `CHANNEL_THREADS` in Ident | --- | --- | --- | | 1 << 2 | CHANNEL_THREADS | The client can parse thread, forum, and media channels | -The flag joins the [session flags](/gateway/commands/#session-flags) of Identify. A user session without it sees no channel of type 11, 12, 15, or 16, and no thread event. +The flag joins the [session flags](/gateway/commands/#session-flags) of Identify. A user session without it receives no channel of type 10, 11, 12, 15, or 16 and no event from this page. Its guild ready objects leave out a category that is visible only through a forum or media channel inside it. Its channels have no `flags` and no thread or forum fields. Its roles and permission overwrites have the thread permission bits cleared. [Ready](/gateway/events/#ready) leaves out every read state that has `flags`. It receives no [Message ACK](/gateway/events/#message-ack) for an entry that has `flags`. It receives no [Channel Pins ACK](/gateway/events/#channel-pins-ack) for a thread and no [Saved Message Create](/gateway/events/#saved-message-create) for a message in a thread. + +The same session receives no message of type 18 (`THREAD_CREATED`) or 21 (`THREAD_STARTER_MESSAGE`). Its messages have no `thread` field. The message flags `HAS_THREAD` (`1 << 5`) and `FAILED_TO_MENTION_SOME_ROLES_IN_THREAD` (`1 << 8`) are cleared. ## Guild threads -`threads` on a guild ready object is an array of [thread objects](/http-api/threads/#thread-object). A user session receives the active threads it joined, each with `member`. A bot session receives every active thread it can view. +`threads` on a guild ready object is an array of [thread objects](/http-api/threads/#thread-object). A user session receives the active threads it joined and can view, each with `member`. A bot session receives every active thread it can view, with `member` on the threads it joined. -The `member` of each entry has no `id`, `user_id`, or `member`. +The `member` of each entry has no `id`, `user_id`, `member`, or `presence`. A thread object in a guild ready object or in any event on this page has no `member_ids_preview`. ## Dispatch events | Event | Description | Scope | | --- | --- | --- | -| [Thread Create](#thread-create) | A thread was created or became visible | Thread | -| [Thread Update](#thread-update) | A thread changed | Thread | -| [Thread Delete](#thread-delete) | A thread was deleted | Thread | -| [Thread List Sync](#thread-list-sync) | The session's active thread set was replaced | Guild | -| [Thread Member Update](#thread-member-update) | The session's own membership changed | Current user | -| [Thread Members Update](#thread-members-update) | Users joined or left a thread | Thread | -| [Thread Member List Update](#thread-member-list-update) | A subscribed thread member list resynced | Command response | -| [Forum Unreads](#forum-unreads) | Unread counts for forum posts | Command response | +| [Thread Create](#thread-create) | A thread is created or the session joins one | Thread visibility | +| [Thread Update](#thread-update) | A visible thread changes | Thread visibility | +| [Thread Delete](#thread-delete) | A thread is deleted | Thread visibility | +| [Thread List Sync](#thread-list-sync) | The session's active thread set is replaced | Guild connection | +| [Thread Member Update](#thread-member-update) | The session's own thread membership changes | Current user | +| [Thread Members Update](#thread-members-update) | Users join or leave a thread | Thread visibility | +| [Thread Member List Update](#thread-member-list-update) | A subscribed thread member list resyncs | Member list subscription | +| [Forum Unreads](#forum-unreads) | Unread counts for forum posts are returned | Command response | -A session receives a thread event only when it can view the thread. Messages, reactions, typing, and pins in a thread reach a session that is a member of the thread, a bot session, or an active session that subscribed through `threads`. A message in a thread has `channel_type` set to the thread type. +A passive session in a guild with more than 250 members receives fewer thread events, as each event states. + +Messages, reactions, typing, and pins in a thread reach a session that can view the thread and meets one of these conditions. An event in the message access set of [Event filtering](/gateway/event-filtering/#permission-and-visibility) also needs message access in the thread. These events also follow [Active and passive guilds](/gateway/event-filtering/#active-and-passive-guilds). + +- The session is a bot session. +- The user is a member of the thread. +- The session lists the thread in `thread_member_lists`. +- The session set [`threads`](#lazy-request-thread-options) and the guild is active for it, through [Lazy Request](/gateway/commands/#lazy-request) `active` or the [Identify](/gateway/commands/#identify) `initial_guild_id`, whatever the guild size. + +[Message Create](/gateway/events/#message-create) in a thread has `channel_type` set to the thread type. ### THREAD_CREATE @@ -46,16 +56,20 @@ A thread was created, or the session was added to one. The payload is the [threa | Field | Type | Description | | --- | --- | --- | -| newly_created? | boolean | Whether the thread was just created, present only then | +| newly_created? | boolean | Always `true`, present only when the thread was just created | | member? | [thread member](/http-api/thread-members/#thread-member-object) object | The session's own membership, present only for a member | -A user session receives it for a thread it joined, a thread it was added to, or any thread while subscribed through `threads`. A passive session receives it only when it was added. +Recipients are the sessions that can view the thread and are a bot session, a member, or a session that set `threads` through [Lazy Request](#lazy-request-thread-options). A passive session in a guild with more than 250 members receives it only when its user is a member. + +A user added to an existing thread receives a Thread Create with `member` and no `newly_created`. It arrives before the matching [Thread Members Update](#thread-members-update). ### THREAD_UPDATE A thread changed. The payload is the [thread object](/http-api/threads/#thread-object). -A new message and a changed counter emit no Thread Update. A passive session receives it only for a thread it joined. +Recipients are the sessions that can view the thread and are a bot session, a member, or a session that set `threads`. Fluxer checks them before and after the change and sends to both sets. A passive session in a guild with more than 250 members receives it only for a thread it joined. + +A message sent to an archived thread unarchives it and emits Thread Update. A message sent to an active thread emits none. When a thread is unarchived, each session of a member that can view it also receives a [Thread Member Update](#thread-member-update). ### THREAD_DELETE @@ -68,7 +82,7 @@ A thread was deleted. | parent_id | snowflake | The ID of the parent channel | | type | integer | The [thread type](/http-api/threads/#thread-types) | -Recipients are the sessions that could view the thread before the deletion. +Recipients are the sessions that can view the thread as it stood before the deletion and are a bot session, a member, or a session that set `threads`. A member is a user the guild holds as a member of the thread, or one of up to 1,000 thread members Fluxer reads at deletion. A passive session in a guild with more than 250 members receives it only when its user is a member. ### THREAD_LIST_SYNC @@ -78,16 +92,26 @@ The session's active threads were replaced for a guild or for some of its channe | --- | --- | --- | | guild_id | snowflake | The ID of the guild | | channel_ids?1 | array[snowflake] | The parent channels whose threads are replaced | -| threads | array[[thread](/http-api/threads/#thread-object) object] | The active threads the session can view | +| threads2 | array[[thread](/http-api/threads/#thread-object) object] | The active threads the session can view | | members | array[[thread member](/http-api/thread-members/#thread-member-object) object] | The session's memberships in those threads | 1 Absent when the event replaces every thread in the guild -Fluxer sends it after `threads` is set to true in [Lazy Request](#lazy-request-thread-options), within 5,000 ms, and when the session gains access to a channel that has threads. +2 With `channel_ids`, every active thread in those channels. Without it, every active thread in the guild for a bot session and for a session that set `threads` + +Without `channel_ids`, a user session that did not set `threads` receives the active threads it joined. + +- Fluxer sends it when [Lazy Request](#lazy-request-thread-options) sets `threads` to true for a session that had not set it. The event follows after a random delay below 5,000 ms. +- Fluxer sends it when the guild reloads its thread state. A bot session, a session that set `threads`, and a user session that joined an active thread each receive one. +- Fluxer sends it when the session gains view access to channels that have active threads. `channel_ids` lists those channels. + +A guild sends a burst of up to 20 full syncs, then 20 more each second, and queues the rest. Thread List Sync is delivered live and never retained for Resume. ### THREAD_MEMBER_UPDATE -The session's own membership in a thread changed. The payload is the [thread member object](/http-api/thread-members/#thread-member-object) with `guild_id`. +The session's own membership in a thread changed. The payload is the [thread member object](/http-api/thread-members/#thread-member-object) with `guild_id`. It has `muted` and `mute_config`. + +Every session of the user that can view the thread receives it, passive or not. ### THREAD_MEMBERS_UPDATE @@ -101,9 +125,11 @@ Users joined or left a thread. | added_members?1 | array[[thread member](/http-api/thread-members/#thread-member-object) object] | The users who joined | | removed_member_ids? | array[snowflake] | The IDs of the users who left | -1 Each entry adds `member`, the guild member, and `presence`, the [presence object](/gateway/events/#presence-object). Either is null when Fluxer has none +1 Each entry has no `muted` or `mute_config`, and adds `member` and `presence` -A bot session receives every Thread Members Update it can view. A user session receives it when it was added or removed, is a member, or subscribed through `threads`. A passive session receives it only when it was added or removed. +`member` is the guild member, or null when the guild has none for the user. `presence` is the [presence object](/gateway/events/#presence-object) with `user` reduced to `id`, or null when the user is not visibly online to the guild. + +Every session of an added or removed user receives it. Any other session receives it when it can view the thread and is a bot session, a member, or a session that set `threads`. A passive session in a guild with more than 250 members receives it only when its user was added or removed. ### THREAD_MEMBER_LIST_UPDATE @@ -115,7 +141,9 @@ A thread member list the session subscribed to through `thread_member_lists` res | thread_id | snowflake | The ID of the thread | | members1 | array[thread member list item object] | The members of the thread, at most 1,000 | -1 Each item is `user_id`, `join_timestamp`, `flags`, `member`, and `presence`, where `member` is the guild member or null +1 Each item is `user_id`, `join_timestamp`, `flags`, `member`, and `presence` + +`member` is the guild member or null. `presence` is the guild's [presence object](/gateway/events/#presence-object) when the user is visibly online to the guild, and otherwise the placeholder `{"status": "offline", "mobile": false, "afk": false}`. The event is delivered live and never retained for Resume. @@ -127,20 +155,24 @@ The answer to [Request Forum Unreads](#request-forum-unreads). | --- | --- | --- | | guild_id | snowflake | The ID of the guild | | channel_id | snowflake | The ID of the forum or media channel | -| threads | array[forum unread object] | One entry for each requested post the session can view | +| threads | array[forum unread object] | One entry for each requested post found in the channel | -Each forum unread object is `thread_id` with either `count` or `missing`. `count` is the number of unread messages after the sent `ack_message_id`, capped at 25. `missing` is true when the request sent no `ack_message_id` for that post. +Each forum unread object is `thread_id` with either `count` or `missing`. `count` is the number of messages after the sent `ack_message_id`, capped at 25. `missing` is true when the request sent no valid `ack_message_id` for that post. ## Lazy Request thread options -The [guild subscription object](/gateway/commands/#guild-subscription-object) of [Lazy Request](/gateway/commands/#lazy-request) takes two more options. +The [guild subscription object](/gateway/commands/#guild-subscription-object) of [Lazy Request](/gateway/commands/#lazy-request) takes two more options. Fluxer applies them after `typing`. | Field | Type | Description | | --- | --- | --- | -| threads? | boolean | Whether the session receives every thread event and thread message in the guild | +| threads? | boolean | Whether the session receives events and messages for every thread it can view in the guild | | thread_member_lists? | array[snowflake] | The threads whose member lists the session subscribes to, at most 10 | -Fluxer ignores both options for a user session that did not set `CHANNEL_THREADS`. A thread the session cannot view is dropped from `thread_member_lists`. Each newly listed thread produces a [Thread Member List Update](#thread-member-list-update), and a membership change in it produces another. +Fluxer ignores both options for a user session that did not set `CHANNEL_THREADS`. `threads` set to false clears the subscription, and any value that is not a Boolean is ignored. Fluxer also clears it when the session marks the guild passive. + +`thread_member_lists` replaces the previous list, and an empty array clears it. Fluxer keeps the first 10 entries that are Snowflakes and removes duplicates. It then drops each thread the session cannot view or that is not active. A value that is not an array is ignored. + +Each newly listed thread produces a [Thread Member List Update](#thread-member-list-update) at once. A membership change in the thread, or a presence change of one of its listed members, produces another after a 250 ms batching delay. ## Request Forum Unreads @@ -152,7 +184,7 @@ Opcode `28` requests unread counts for posts in one forum or media channel. | channel_id | snowflake | The ID of the forum or media channel | | threads | array[forum unread request object] | The posts to count, deduplicated and truncated to 40 | -Each forum unread request object is `thread_id` and an optional `ack_message_id`, the newest message the client has read in that post. +Each forum unread request object is `thread_id` and an optional `ack_message_id`, the newest message the client has read in that post. An entry with no valid `thread_id` is dropped. An `ack_message_id` that is not a valid Snowflake counts as absent. ```json { @@ -165,6 +197,17 @@ Each forum unread request object is `thread_id` and an optional `ack_message_id` } ``` -Results arrive in one [Forum Unreads](#forum-unreads), where a post outside the channel has no entry. A channel the session cannot view, and a request with no valid `thread_id`, produce no Dispatch. A session that cannot view threads closes with `4001` and reason `Unknown opcode`. +Results arrive in one [Forum Unreads](#forum-unreads). A post that does not exist or is outside the channel has no entry. An archived post, and an active post that has a message after the sent `ack_message_id`, has no entry when Fluxer fails to count its messages. -A session sends at most five Request Forum Unreads commands in a rolling 5-second window. A further command is discarded without closing the connection. +No Dispatch follows in these cases. + +- The guild or channel ID is not a valid Snowflake. +- The guild is not connected to the session. +- The channel is not a forum or media channel. +- The session cannot view the channel. +- The request has no valid `thread_id`. +- The connection has too many requests in progress. + +A user session that did not set `CHANNEL_THREADS` closes with `4001` and reason `Unknown opcode`. A `d` that is not an object closes the same way. + +Request Forum Unreads accepts five commands per WebSocket in a rolling 5-second window. A further command is discarded without closing the connection. diff --git a/fluxer_docs/src/content/docs/http-api/channels.mdx b/fluxer_docs/src/content/docs/http-api/channels.mdx index ac833e54a..1d0e6c021 100644 --- a/fluxer_docs/src/content/docs/http-api/channels.mdx +++ b/fluxer_docs/src/content/docs/http-api/channels.mdx @@ -6,9 +6,9 @@ description: Channel objects, settings, recipients, permission overwrites, slowm import RouteHeader from '@/components/RouteHeader.astro'; -A channel is where a conversation happens, in text or in voice. A [guild](/http-api/guilds/) owns text, announcement, voice, category and link channels. Outside a guild, a channel is a direct message, a group direct message, or the personal notes channel. +A channel is where a conversation happens, in text or in voice. A [guild](/http-api/guilds/) owns text, announcement, voice, category, link, forum and media channels, and the threads inside them. Outside a guild, a channel is a direct message, a group direct message, or the personal notes channel. -[Messages](/http-api/messages/) defines message content, and [Guild channels](/http-api/guild-channels/) defines guild-scoped listing, creation and reordering. [Calls](/http-api/calls/) defines ringing, the region of a private call, and ending a call. [Streams](/http-api/streams/) defines the stream keys and previews of a Go Live screen share. +[Messages](/http-api/messages/) defines message content, and [Guild channels](/http-api/guild-channels/) defines guild-scoped listing, creation and reordering. [Threads](/http-api/threads/) defines threads, and [Forums](/http-api/forums/) defines forum and media channels. [Calls](/http-api/calls/) defines ringing, the region of a private call, and ending a call. [Streams](/http-api/streams/) defines the stream keys and previews of a Go Live screen share. ## Channel object @@ -22,26 +22,41 @@ A channel object always has its identity and type. Every other field is present | type | integer | The [type of the channel](#channel-types) | | guild_id? | snowflake | The ID of the guild, present only for a guild channel | | name?1 | string | The name of the channel, present for a guild channel and for a group direct message | -| topic? | ?string | The topic of the channel, present only for a guild text, announcement, or voice channel | +| topic? | ?string | The topic of the channel, or null when none is set, present only for a guild text, announcement, voice, forum, or media channel | | url? | ?string | The destination URL, present only for a guild link channel | | icon? | ?string | The icon hash, present only for a group direct message | -| owner_id? | ?snowflake | The ID of the owner, present only for a group direct message | -| position?2 | integer | The sort position, present only for a guild channel | -| parent_id? | ?snowflake | The ID of the parent category, null when the channel sits at the top level | +| owner_id? | ?snowflake | The ID of the owner, or null when the channel stores none, present only for a group direct message and a thread | +| position?2 | integer | The sort position, present only for a guild channel other than a thread | +| parent_id?13 | ?snowflake | The ID of the parent category, null when the channel sits at the top level | | bitrate?3 | ?integer | The voice bitrate in bits per second, present only for a guild voice channel | | user_limit?4 | ?integer | The configured member occupancy limit, present only for a guild voice channel | | voice_connection_limit?5 | ?integer | The number of simultaneous voice connections one user may hold, present only for a guild voice channel | | rtc_region?6 | ?string | The ID of the selected RTC region, 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?7 | array[[permission overwrite](#permission-overwrite-object) object] | The overwrites applied to this channel, present only for a guild channel | +| permission_overwrites?7 | array[[permission overwrite](#permission-overwrite-object) object] | The overwrites applied to this channel, present only for a guild channel other than a thread | | recipients?8 | array[[partial user](/http-api/users/#partial-user-object) object] | The other recipients of a direct message or group direct message (max 49) | -| nsfw?9 | boolean | Whether the channel has its own age restriction, present only for a guild channel | -| nsfw_override?9 | ?boolean | Whether this channel overrides the inherited age restriction, or null when it inherits | -| content_warning_level?10 | integer | The [content warning level](#content-warning-levels) stored on this channel, present only for a guild channel | -| content_warning_text?10 | ?string | The content warning text stored on this channel, or null when the channel inherits | -| rate_limit_per_user?11 | integer | The slowmode interval in seconds, present only for a guild text, announcement, or voice channel | +| nsfw?9 | boolean | Whether the channel has its own age restriction, present only for a guild channel other than a thread | +| nsfw_override?9 | ?boolean | Whether this channel overrides the inherited age restriction, or null when it inherits, present only for a guild channel other than a thread | +| content_warning_level?10 | integer | The [content warning level](#content-warning-levels) stored on this channel, present only for a guild channel other than a thread | +| content_warning_text?10 | ?string | The content warning text stored on this channel, or null when the channel inherits, present only for a guild channel other than a thread | +| rate_limit_per_user?11 | integer | The slowmode interval in seconds, present only for a thread and a text, announcement, voice, forum, or media channel | | nicks?12 | map[snowflake, string] | The group direct message nicknames keyed by the decimal user ID (each 1-32 characters) | +| flags?14 | integer | The [channel flags](/http-api/forums/#channel-flags), present only for a forum channel, a media channel, and a thread | +| thread_metadata? | [thread metadata](/http-api/threads/#thread-metadata-object) object | The archive and lock state, present only for a thread | +| message_count? | integer | The number of messages in the thread, present only for a thread | +| total_message_sent? | integer | The number of messages ever sent in the thread, present only for a thread | +| member_count? | integer | The number of thread members, capped at 50, present only for a thread | +| applied_tags?15 | array[snowflake] | The IDs of the [forum tags](/http-api/forums/#forum-tag-object) applied to a post (max 5) | +| member_ids_preview?15 | array[snowflake] | The IDs of the most recently joined members of a post, newest first (max 8) | +| member?16 | [thread member](/http-api/thread-members/#thread-member-object) object | The thread member object of the caller | +| default_auto_archive_duration?17 | ?integer | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations), which Fluxer does not apply to a new thread, or null when none is set | +| default_thread_rate_limit_per_user?17 | integer | The slowmode interval in seconds new threads inherit | +| available_tags? | array[[forum tag](/http-api/forums/#forum-tag-object) object] | The tags a post can apply (max 20), present only for a forum or media channel | +| default_reaction_emoji? | ?[default reaction](/http-api/forums/#default-reaction-object) object | The reaction clients show on each post, or null when none is set, present only for a forum or media channel | +| default_sort_order? | ?integer | The default [sort order](/http-api/forums/#forum-sort-orders) of posts, or null when none is set, present only for a forum or media channel | +| default_forum_layout? | integer | The default [layout](/http-api/forums/#forum-layouts) of posts, present only for a forum channel | +| default_tag_setting? | string | The default [tag matching](/http-api/forums/#forum-tag-settings) of a post search, present only for a forum or media channel | 1 Omitted when a group direct message stores no name, and never present on a direct message or on the personal notes channel @@ -55,7 +70,7 @@ A channel object always has its identity and type. Every other field is present 6 Null selects automatic routing. [List RTC regions](#list-rtc-regions) returns the available identifiers -7 Always present for a guild channel, and empty when the channel stores no overwrite +7 Always present for a guild channel other than a thread, and empty when the channel stores no overwrite 8 Excludes the authenticated user, omitted entirely when no other recipient remains, and never present on the personal notes channel @@ -67,6 +82,18 @@ A channel object always has its identity and type. Every other field is present 12 Omitted when the group stores no nickname. Clearing a nickname removes its key from the map +13 On a thread, the ID of the text, announcement, forum, or media channel the thread belongs to + +14 A forum or media channel that stores no flags reports `0` + +15 Present only for a thread whose parent is a forum or media channel + +16 Present only for a thread the caller is a member of, on the operations the [thread object](/http-api/threads/#thread-object) names + +17 Always present for a forum or media channel, where an unset duration is null and an unset interval is `0`. Present for a guild text or announcement channel only when the channel stores a value + +A [thread object](/http-api/threads/#thread-object) is a channel object with the thread fields. [Forum channel fields](/http-api/forums/#forum-channel-fields) lists the fields of a forum or media channel. + :::note[Omission and null are distinct] An absent field is one this channel type does not own. A field present with `null` is one the type owns and that has no value. ::: @@ -111,6 +138,11 @@ An age restriction and a content warning both resolve through this channel first | 3 | GROUP_DM | Group direct message channel | | 4 | GUILD_CATEGORY | Guild category, which owns no parent and no messages | | 5 | GUILD_ANNOUNCEMENT2 | Guild text channel whose messages can be published to the channels that follow it | +| 10 | ANNOUNCEMENT_THREAD3 | A thread in an announcement channel, visible to everyone who can view the parent | +| 11 | PUBLIC_THREAD3 | A thread visible to everyone who can view the parent | +| 12 | PRIVATE_THREAD3 | A thread in a guild text channel visible to its members and to thread moderators | +| 15 | GUILD_FORUM4 | Guild channel that holds posts and no messages of its own | +| 16 | GUILD_MEDIA4 | Forum channel that can [hide media download options](/http-api/forums/#channel-flags) | | 998 | GUILD_LINK | Guild link channel, which has a destination URL and no messages | | 999 | DM_PERSONAL_NOTES1 | Personal notes channel | @@ -118,6 +150,12 @@ An age restriction and a content warning both resolve through this channel first 2 It has the fields and the slowmode of a guild text channel. [Announcement channels](/topics/announcement-channels/) describes publishing and following +3 [Thread types](/http-api/threads/#thread-types) describes each type + +4 [Forums](/http-api/forums/) describes forum and media channels + +A thread, a forum channel, and a media channel are visible to a bot and to a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability). A route that resolves one by ID returns 404 `UNKNOWN_CHANNEL` to every other caller unless its own entry states another result. [Get channel](#get-channel) and [List guild channels](/http-api/guild-channels/#list-guild-channels) return `default_auto_archive_duration` and `default_thread_rate_limit_per_user` on a guild text or announcement channel to those two callers alone. `channels` on [Get guild](/http-api/guilds/#get-guild) has forum channels, media channels, and both fields for every caller. + ## Permission overwrite object A permission overwrite changes the effective guild permissions for one role or one member in one channel. `allow` and `deny` are decimal strings because a permission mask exceeds the range a JSON number preserves. [Permissions](/http-api/permissions/) defines the individual flags. @@ -179,6 +217,8 @@ The slowmode state combines the channel's configured interval with the authentic 2 True only for a non-bot caller holding `BYPASS_SLOWMODE` in a guild channel with a non-zero interval, so a bot caller and a private channel both report `false` even though neither is delayed +For a forum or media channel the state reports the delay before the user can create a post. + ### Example ```json @@ -261,10 +301,11 @@ Returns the [channel object](#channel-object) visible to the authenticated user. ### Limitations - A guild channel requires [VIEW_CHANNEL](/http-api/permissions/), and a satisfied age verification when it resolves to an age restriction. +- A thread requires `VIEW_CHANNEL` in its parent, and a private thread also requires membership or [thread moderator](/http-api/threads/#thread-moderators) status. - A private channel requires current recipient access. - The personal notes channel requires ownership. -Fluxer enforces age verification only for a guild text, announcement, voice, or link channel. Fluxer never requires age verification for a guild category, even when the category has the override its children inherit. +Fluxer enforces age verification only for a guild text, announcement, voice, link, forum, or media channel, and for a thread through its parent. Fluxer never requires age verification for a guild category, even when the category has the override its children inherit. ### Path parameters @@ -278,6 +319,7 @@ Fluxer enforces age verification only for a guild text, announcement, voice, or | --- | --- | --- | | 200 | [channel](#channel-object) object | Channel was returned | | 403 | [error response](/http-api/#error-response) | Caller is not a guild member or lacks `VIEW_CHANNEL`, each returning `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The guild exists but the caller's membership state cannot be resolved and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The resolved age restriction is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the guild that owns the channel does not exist and the request returns `UNKNOWN_GUILD` | @@ -308,6 +350,7 @@ Returns the authenticated user's [channel slowmode state object](#channel-slowmo | --- | --- | --- | | 200 | [channel slowmode state](#channel-slowmode-state-object) object | State was returned | | 403 | [error response](/http-api/#error-response) | Caller is not a guild member or lacks `VIEW_CHANNEL`, each returning `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The guild exists but the caller's membership state cannot be resolved and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The resolved age restriction is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the guild that owns the channel does not exist and the request returns `UNKNOWN_GUILD` | @@ -364,6 +407,8 @@ On a deployment that configures no voice topology, a missing channel, an invisib Modifies a guild channel or a group direct message and returns the updated [channel object](#channel-object). Emits a [Channel Update](/gateway/events/#channel-update) Gateway event. +[Modify thread](/http-api/threads/#modify-thread) describes this route for a thread. + ### Limitations - A guild channel requires [VIEW_CHANNEL](/http-api/permissions/) and [MANAGE_CHANNELS](/http-api/permissions/) in that channel. @@ -397,15 +442,14 @@ The submitted array becomes the complete overwrite collection of the channel. ### JSON body -The stored channel type selects the body variant. A `type` field converts a guild text channel into an announcement channel or back, and a `type` equal to the stored type changes nothing. A direct message and the personal notes channel match no variant, and Fluxer rejects both with 400 `INVALID_FORM_BODY`. +The stored channel type selects the body variant. A direct message and the personal notes channel match no variant, and Fluxer rejects both with 400 `INVALID_FORM_BODY`. A thread accepts every field of the [Modify thread body](/http-api/threads/#thread-body). #### Guild channel body -Every field is optional, and an omitted field preserves its current value. The guild variants share one field set, and a field the stored type does not own is accepted and discarded. +Every field is optional, and an omitted field preserves its current value. Every guild channel type other than a thread accepts the fields below, and a field the stored type does not own is accepted and discarded. | Field | Type | Description | | --- | --- | --- | -| type?14 | integer | The [channel type](#channel-types) to convert to, either `0` or `5` | | name?1 | ?string | The name of the channel (1-100 characters after normalisation) | | topic?2 | ?string | The topic of the channel (1-1,024 characters), or null to clear it | | url?3 | ?string | The destination of a guild link channel (1-2,048 characters, `http` or `https`), or null to clear it | @@ -418,12 +462,12 @@ Every field is optional, and an omitted field preserves its current value. The g | nsfw?8 | ?boolean | Whether the channel is age restricted, a legacy switch superseded by `nsfw_override` | | content_warning_level?9 | integer | The [content warning level](#content-warning-levels) to store on this channel | | content_warning_text?10 | ?string | The content warning text (max 200 characters) | -| rate_limit_per_user?11 | ?integer | The slowmode interval in seconds (0-21,600) for a guild text, announcement, or voice channel | +| rate_limit_per_user?11 | ?integer | The slowmode interval in seconds (0-21,600) | | rtc_region?12 | ?string | The ID of the RTC region (1-64 characters) for a guild voice channel, where null selects automatic routing | -1 An explicit null preserves the current name. In a guild without flexible channel names, Fluxer lowercases a guild text or announcement channel name, replaces its whitespace with hyphens, and removes disallowed punctuation +1 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 -2 An explicit null clears the stored topic +2 An explicit null clears the stored topic. A forum or media channel accepts 1-4,096 characters 3 Applied only to a guild link channel. An explicit null clears the destination @@ -441,15 +485,32 @@ Every field is optional, and an omitted field preserves its current value. The g 10 Trimmed. A value empty after trimming, or an explicit null, clears the stored text -11 Applied only to a guild text, announcement, or voice channel. An explicit null preserves the current interval +11 Applied only to a guild text, announcement, voice, forum, or media channel. An explicit null preserves the current interval 12 Applied only to a guild voice channel. Supplying the field at all, including as null, requires [UPDATE_RTC_REGION](/http-api/permissions/) -13 The stored value is capped at 96,000 unless the guild holds an [audio bitrate feature](/http-api/guilds/#guild-features). A higher value is stored at the cap rather than rejected +13 The stored value is capped at 96,000 unless the guild holds an [audio bitrate feature](/http-api/guilds/#guild-features). A higher value is stored at the cap -14 Absent, null, or equal to the stored type keeps the type. Any other conversion returns 400 `CHANNEL_TYPE_CONVERSION_NOT_SUPPORTED` +A guild text or announcement channel also accepts `type`, which converts a text channel into an announcement channel or back. -Converting a text channel into an announcement channel fails with 400 `CHANNEL_HAS_FOLLOWED_CHANNELS` while the text channel receives messages from any followed announcement channel. Delete its [channel follower webhooks](/http-api/webhooks/#webhook-types) first. Converting an announcement channel back into a text channel needs no preparation, and Fluxer removes every follow of it afterwards. +| Field | Type | Description | +| --- | --- | --- | +| type?14 | integer | The [channel type](#channel-types) to convert to, either `0` or `5` | + +14 Absent, null, or equal to the stored type keeps the type. A value other than `0` or `5` returns 400 `CHANNEL_TYPE_CONVERSION_NOT_SUPPORTED`. Every other stored type ignores `type` + +Converting a text channel into an announcement channel fails with 400 `CHANNEL_HAS_FOLLOWED_CHANNELS` while the text channel receives messages from any followed announcement channel. Delete its [channel follower webhooks](/http-api/webhooks/#webhook-types) first. The conversion also fails with 400 `CHANNEL_HAS_THREADS` while the text channel holds a private thread. A conversion changes the type of the public threads of the channel as [Thread types](/http-api/threads/#thread-types) describes. Converting an announcement channel back into a text channel needs no preparation, and Fluxer removes every follow of it afterwards. + +A guild text, announcement, forum, or media channel also accepts these fields. + +| Field | Type | Description | +| --- | --- | --- | +| default_auto_archive_duration?15 | ?integer | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations), which Fluxer does not apply to a new thread, or null to clear it | +| default_thread_rate_limit_per_user?15 | ?integer | The slowmode interval in seconds new threads inherit (0-21,600), or null to clear it | + +15 Fluxer discards both fields from a caller other than a bot or a user session that declares `channel_threads` + +A forum channel also accepts every field of the [forum field table](/http-api/forums/#creating-and-modifying-a-forum). A media channel accepts every field of the [forum field table](/http-api/forums/#creating-and-modifying-a-forum) except `default_forum_layout`. 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`. @@ -489,8 +550,9 @@ Fluxer trims a stored nickname, and a value that is null or empty after trimming | Status | Body | Condition | | --- | --- | --- | | 200 | [channel](#channel-object) object | Channel was modified, or the submitted values already matched the stored state | -| 400 | [error response](/http-api/#error-response) | The `type` conversion is not between `0` and `5`, returning `CHANNEL_TYPE_CONVERSION_NOT_SUPPORTED` | +| 400 | [error response](/http-api/#error-response) | The `type` on a text or announcement channel is neither `0` nor `5`, returning `CHANNEL_TYPE_CONVERSION_NOT_SUPPORTED` | | 400 | [error response](/http-api/#error-response) | The text channel receives followed channels, returning `CHANNEL_HAS_FOLLOWED_CHANNELS` | +| 400 | [error response](/http-api/#error-response) | The text channel holds a private thread, returning `CHANNEL_HAS_THREADS` | | 403 | [error response](/http-api/#error-response) | A required permission, ownership, or permission bit authority is absent and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | A concurrent change removed the caller from the group direct message and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The guild exists but the caller's membership state cannot be resolved and the request returns `ACCESS_DENIED` | @@ -502,9 +564,9 @@ Fluxer trims a stored nickname, and a value that is null or empty after trimming ### Side effects -A guild channel change emits [Channel Update](/gateway/events/#channel-update) to every session subscribed to its guild. 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. +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, so each user's next message is 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. 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. @@ -522,6 +584,8 @@ A group direct message change emits [Channel Update](/gateway/events/#channel-up Deletes a guild channel, closes a direct message, or removes the caller from a group direct message. Returns 204 with an empty body. Emits a [Channel Delete](/gateway/events/#channel-delete) Gateway event. +[Delete thread](/http-api/threads/#delete-thread) describes this route for a thread. + ### Limitations - Deleting a guild channel requires [VIEW_CHANNEL](/http-api/permissions/) and [MANAGE_CHANNELS](/http-api/permissions/). @@ -532,7 +596,7 @@ Deletes a guild channel, closes a direct message, or removes the caller from a g `MANAGE_CHANNELS` is an [elevated permission](/http-api/permissions/#elevated-permissions). It also 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 who holds the permission without that authenticator receives 400 [`TWO_FACTOR_REQUIRED`](/http-api/errors/). The sudo requirement on `delete_messages` applies at every guild MFA level. :::danger[A guild channel deletion is immediate and permanent] -Deleting a guild channel destroys the channel record, every message in it, every stored attachment of those messages, its invites, and its webhooks. There is no pending state, no grace period, and no restore. +Deleting a guild channel destroys the channel record, every message in it, every stored attachment of those messages, its invites, its webhooks, and every thread in it. There is no pending state, no grace period, and no restore. ::: ### Path parameters @@ -595,7 +659,11 @@ An account that stores no password, no TOTP secret, and no registered WebAuthn c Deleting a guild category first clears the `parent_id` of every child channel and emits [Channel Update](/gateway/events/#channel-update) for each one. The category itself is then deleted like any other guild channel. -Deleting a guild channel permanently removes its messages, attachments, invites and webhooks. Deleting an announcement channel also deletes every [channel follower webhook](/http-api/webhooks/#webhook-types) that follows it and turns each copy of its published messages into a deleted-source placeholder, both in the background after the response. Guild subscribers receive [Channel Delete](/gateway/events/#channel-delete), and the deletion appears in the audit log. When the deleted channel is the guild's system, rules or AFK channel, Fluxer clears that guild setting and emits [Guild Update](/gateway/events/#guild-update). +Deleting a guild channel permanently removes its messages, attachments, invites and webhooks. + +Deleting a guild text, announcement, forum, or media channel also deletes every thread in it with its messages, attachments, and memberships. The deletion runs in the background after the response. Fluxer emits no [Thread Delete](/gateway/threads/#thread-delete) for those threads and writes no audit log entry for them. A client removes every thread whose `parent_id` is the channel when it receives [Channel Delete](/gateway/events/#channel-delete). + +Deleting an announcement channel also deletes every [channel follower webhook](/http-api/webhooks/#webhook-types) that follows it and turns each copy of its published messages into a deleted-source placeholder, both in the background after the response. Guild subscribers receive [Channel Delete](/gateway/events/#channel-delete), and the deletion appears in the audit log. When the deleted channel is the guild's system, rules or AFK channel, Fluxer clears that guild setting and emits [Guild Update](/gateway/events/#guild-update). Closing a direct message marks the channel closed for the caller alone and emits [Channel Delete](/gateway/events/#channel-delete) to that caller. The channel, its messages, and the other recipient's view are untouched. @@ -713,7 +781,8 @@ The optional body has the same [sudo verification fields](#sudo-verification-fie | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Recipient was removed | -| 400 | [error response](/http-api/#error-response) | Channel is not a group direct message and the request returns `INVALID_CHANNEL_TYPE`, or the target is not a recipient and the request returns `INVALID_FORM_BODY` with the code `USER_NOT_IN_CHANNEL` | +| 400 | [error response](/http-api/#error-response) | Channel is not a group direct message and the request returns `INVALID_CHANNEL_TYPE` | +| 400 | [error response](/http-api/#error-response) | The target is not a recipient and the request returns `INVALID_FORM_BODY` with the code `USER_NOT_IN_CHANNEL` | | 403 | [error response](/http-api/#error-response) | Caller is not a recipient and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The caller is not the owner while removing another recipient and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | Sudo verification is required and the request returns `SUDO_MODE_REQUIRED` | @@ -740,6 +809,7 @@ Creates or replaces one permission overwrite on a guild channel and returns 204 ### Limitations - A caller without [ADMINISTRATOR](/http-api/permissions/) cannot allow a permission bit outside their own effective permissions in that channel, and cannot remove a denied bit outside them either. +- A thread has no permission overwrites, as [Thread permissions](/http-api/threads/#thread-permissions) describes. The operation does not require `VIEW_CHANNEL` and never returns `TWO_FACTOR_REQUIRED` or `NSFW_CONTENT_AGE_RESTRICTED`. @@ -781,6 +851,7 @@ A larger decimal string returns 400 `INVALID_FORM_BODY` with the code `INTEGER_O | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Overwrite was created or replaced | +| 400 | [error response](/http-api/#error-response) | Channel is a thread and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | `MANAGE_ROLES` in the channel or permission bit authority is absent, each returning `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, or is not a guild channel, each returning `UNKNOWN_CHANNEL` | @@ -803,6 +874,7 @@ Deletes one permission overwrite from a guild channel and returns 204 with an em ### Limitations - A caller without [ADMINISTRATOR](/http-api/permissions/) must already hold every permission bit the removed overwrite denied. +- A thread has no permission overwrites, as [Thread permissions](/http-api/threads/#thread-permissions) describes. The operation is idempotent, does not require `VIEW_CHANNEL`, and never returns `TWO_FACTOR_REQUIRED` or `NSFW_CONTENT_AGE_RESTRICTED`. @@ -820,6 +892,7 @@ The operation is idempotent, does not require `VIEW_CHANNEL`, and never returns | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Overwrite was deleted, or no overwrite existed for the identifier | +| 400 | [error response](/http-api/#error-response) | Channel is a thread and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | `MANAGE_ROLES` in the channel or permission bit authority is absent, each returning `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, or is not a guild channel, each returning `UNKNOWN_CHANNEL` | diff --git a/fluxer_docs/src/content/docs/http-api/discovery.mdx b/fluxer_docs/src/content/docs/http-api/discovery.mdx index f128b549a..7ebce459b 100644 --- a/fluxer_docs/src/content/docs/http-api/discovery.mdx +++ b/fluxer_docs/src/content/docs/http-api/discovery.mdx @@ -378,7 +378,7 @@ A client that wants a translated label supplies its own translation keyed on `id Returns the [discovery channel preview object](#discovery-channel-preview-object) for a channel or message link that points into a discoverable guild, so a client can show the guild and channel before the account joins. The caller needs no permission and no membership. -The route answers only when the guild holds an approved discovery application, the guild does not have the invites-disabled feature, the channel belongs to that guild, and the `@everyone` role can read the channel. Reading means `@everyone` holds both [VIEW_CHANNEL](/http-api/permissions/) and [READ_MESSAGE_HISTORY](/http-api/permissions/) after the channel's `@everyone` overwrite is applied. A guild whose `@everyone` role has [ADMINISTRATOR](/http-api/permissions/) passes regardless of overwrites. Role and member overwrites are not considered, and any channel type can pass. +The route answers only when the guild holds an approved discovery application, the guild does not have the invites-disabled feature, the channel belongs to that guild, and the `@everyone` role can read the channel. Reading means `@everyone` holds both [VIEW_CHANNEL](/http-api/permissions/) and [READ_MESSAGE_HISTORY](/http-api/permissions/) after the channel's `@everyone` overwrite is applied. A guild whose `@everyone` role has [ADMINISTRATOR](/http-api/permissions/) passes regardless of overwrites. Role and member overwrites are not considered. A thread returns 400 `DISCOVERY_NOT_DISCOVERABLE`. A forum or media channel returns the same code to a caller without the [`channel_threads` client capability](/http-api/threads/#client-capability). Every other channel type can pass. :::note[Every refusal returns the same error] A guild that does not exist, a guild without an approved listing, a channel that does not exist, a channel in another guild, and a channel `@everyone` cannot read all return 400 `DISCOVERY_NOT_DISCOVERABLE`. The response does not reveal which condition failed. @@ -399,6 +399,7 @@ A guild that does not exist, a guild without an approved listing, a channel that | 400 | [error response](/http-api/#error-response) | A path parameter is not a valid snowflake and the request returns `INVALID_FORM_BODY` | | 400 | [error response](/http-api/#error-response) | Discovery is disabled for the instance and the request returns `DISCOVERY_DISABLED` | | 400 | [error response](/http-api/#error-response) | The guild is not discoverable or `@everyone` cannot read the channel, and the request returns `DISCOVERY_NOT_DISCOVERABLE` | +| 400 | [error response](/http-api/#error-response) | A thread, or a forum or media channel for a caller without the capability, returns `DISCOVERY_NOT_DISCOVERABLE` | | 403 | [error response](/http-api/#error-response) | Caller is a bot, presents a bearer credential, or holds a revoked account and the request returns `ACCESS_DENIED` | ### Side effects diff --git a/fluxer_docs/src/content/docs/http-api/errors.md b/fluxer_docs/src/content/docs/http-api/errors.md index f6a0e4a36..cdd6e851a 100644 --- a/fluxer_docs/src/content/docs/http-api/errors.md +++ b/fluxer_docs/src/content/docs/http-api/errors.md @@ -102,7 +102,7 @@ Fluxer answers an unrecognised failure with 500 `INTERNAL_SERVER_ERROR` and a ge | 504 | GATEWAY_TIMEOUT | An upstream dependency did not respond in time | | default3 | GENERAL_ERROR | Any status without a specific mapping | -1 Also the code a request that matches no route receives, so an unrouted path and an unreadable resource are indistinguishable from the body alone +1 Also the code a request that matches no route receives, so an unrouted path and an unreadable resource are indistinguishable from the body alone. A route with a route header on [Threads](/http-api/threads/), [Thread members](/http-api/thread-members/) or [Forums](/http-api/forums/) returns it to a user request without the [`channel_threads` client capability](/http-api/threads/#client-capability) 2 Also the code an unrecognised failure receives, with the generic localised message @@ -306,6 +306,10 @@ This channel already receives updates from that announcement channel Remove the followed channels posting here before converting it to an announcement channel +### `CHANNEL_HAS_THREADS` + +Channels with threads can't be converted to announcement channels + ### `CHANNEL_TYPE_CONVERSION_NOT_SUPPORTED` Only text and announcement channels can be converted into each other @@ -446,6 +450,14 @@ Updates from an age-restricted channel can only go to an age-restricted channel Forbidden +### `FORUM_TAG_NAMES_MUST_BE_UNIQUE` + +Tag names must be unique + +### `FORUM_TAG_REQUIRED` + +A tag is required to create a post in this channel + ### `FRIEND_REQUEST_BLOCKED` User does not accept friend requests at this time @@ -518,6 +530,10 @@ Your data export request failed Your data export is still being prepared +### `HIDE_MEDIA_DOWNLOAD_OPTION_MEDIA_ONLY` + +Hiding media download options is only available in media channels + ### `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` This instance policy change is not allowed @@ -618,6 +634,10 @@ Invalid stream thumbnail payload Invalid system flag +### `INVALID_THREAD_NOTIFICATION_SETTINGS` + +Invalid thread notification settings + ### `INVALID_TIMESTAMP` Invalid timestamp @@ -662,6 +682,10 @@ IP authorization resend limit exceeded This IP address cannot be added to the blocklist +### `MAX_ACTIVE_THREADS` + +This community has reached its active thread limit of {count} + ### `MAX_APPLICATIONS` You've reached the maximum of {limit, plural, one {# application} other {# applications}} @@ -682,6 +706,10 @@ You've reached the maximum of {count, plural, one {# emoji} other {# emojis}} You've reached the maximum of {count, plural, one {# favorite meme} other {# favorite memes}} +### `MAX_FORUM_TAGS` + +This channel has reached its tag limit of {count} + ### `MAX_FRIENDS` You've reached the maximum of {count, plural, one {# friend} other {# friends}} @@ -714,6 +742,10 @@ You've reached the maximum of {count, plural, one {# community role} other {# co You've reached the maximum of {count, plural, one {# invite} other {# invites}} +### `MAX_PINNED_THREADS_IN_FORUM` + +This channel has reached its pinned post limit of {count} + ### `MAX_REACTIONS` You've reached the maximum of {count, plural, one {# reaction} other {# reactions}} @@ -722,6 +754,10 @@ You've reached the maximum of {count, plural, one {# reaction} other {# reaction You've reached the maximum of {count, plural, one {# sticker} other {# stickers}} +### `MAX_THREAD_MEMBERS` + +This thread has reached its member limit of {count} + ### `MAX_WEBHOOKS_PER_CHANNEL` You've reached the maximum of {count, plural, one {# webhook} other {# webhooks}} per channel @@ -806,6 +842,10 @@ No passkeys are registered Invalid request +### `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` + +A tag is required, but only moderators can apply the tags in this channel + ### `NO_USERS_WITH_FLUXERTAG_EXIST` There are too many users with this username @@ -894,6 +934,10 @@ This resource is being modified Response validation failed: {errors} +### `SEARCH_INDEX_NOT_READY` + +The search index isn't ready yet + ### `SERVICE_UNAVAILABLE` Service unavailable @@ -1030,6 +1074,18 @@ Test harness is disabled Test harness is forbidden +### `THREAD_ALREADY_CREATED_FOR_MESSAGE` + +A thread has already been created for this message + +### `THREAD_ARCHIVED` + +This thread is archived + +### `THREAD_LOCKED` + +This thread is locked + ### `TWO_FACTOR_REQUIRED` Two-factor authentication is required @@ -1106,6 +1162,10 @@ Unknown emoji Unknown favorite meme +### `UNKNOWN_FORUM_TAG` + +Tag wasn't found + ### `UNKNOWN_GIFT_CODE` Unknown gift code @@ -1158,6 +1218,10 @@ Unknown sticker Unknown store purchase +### `UNKNOWN_THREAD_MEMBER` + +Thread member wasn't found + ### `UNKNOWN_USER` User wasn't found @@ -1222,6 +1286,22 @@ Voice channel is full You've reached the maximum of {count, plural, one {# WebAuthn credential} other {# WebAuthn credentials}} +### `WEBHOOK_FORUM_TARGET_CONFLICT` + +You can't specify both `thread_id` and `thread_name` + +### `WEBHOOK_FORUM_TARGET_REQUIRED` + +Webhooks posting to this channel need a `thread_id` or a `thread_name` + +### `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` + +This webhook service can't post to a forum or media channel without a `thread_id` + +### `WEBHOOK_THREAD_NAME_REQUIRES_FORUM` + +`thread_name` and `applied_tags` only work in forum and media channels + ## Validation error code registry diff --git a/fluxer_docs/src/content/docs/http-api/experiments.mdx b/fluxer_docs/src/content/docs/http-api/experiments.mdx index 5e55dc6d6..0c9cb6024 100644 --- a/fluxer_docs/src/content/docs/http-api/experiments.mdx +++ b/fluxer_docs/src/content/docs/http-api/experiments.mdx @@ -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` and `plutonium_page`. Only `domain_migration` is configured by an operator. `plutonium_page` is 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`, and `channel_threads`. Only `domain_migration` is 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. @@ -34,6 +34,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 | Ignore unknown experiments and treat a missing experiment as off. @@ -65,6 +66,19 @@ The rollout is complete and has no operator setting. The key stays for clients t The assignment only changes what the client shows. An enabled client replaces the Plutonium settings tab with a full Plutonium page, makes app pages linkable in chat, and uses a minimal gift purchase modal. The server applies no part of it on its own. +## Channel threads assignment object + +The channel threads state for one account. This server version writes the key on every response. + +### Structure + +| Field | Type | Description | +| --- | --- | --- | +| active | boolean | Always true, whatever client capability the caller declares | +| config_version | integer | A non-negative version number, the same for every account | + +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). + ## Get experiment assignments diff --git a/fluxer_docs/src/content/docs/http-api/forums.mdx b/fluxer_docs/src/content/docs/http-api/forums.mdx index f53e86774..2ff055b7b 100644 --- a/fluxer_docs/src/content/docs/http-api/forums.mdx +++ b/fluxer_docs/src/content/docs/http-api/forums.mdx @@ -2,15 +2,14 @@ # SPDX-License-Identifier: AGPL-3.0-or-later title: Forums description: Forum and media channels, forum tags, post data, and webhooks that post into a forum. -draft: true --- import RouteHeader from '@/components/RouteHeader.astro'; -A forum channel holds posts, and every post is a public [thread](/http-api/threads/) with its own first message. A media channel is a forum channel whose posts center on attachments. Neither accepts a message of its own. +A forum channel holds posts, and every post is a public [thread](/http-api/threads/) with its own first message. A media channel is a forum channel that can hide media download options. Neither accepts a message of its own. -:::note[Every route needs the `channel_threads` client capability] -A route on this page answers 404 `NOT_FOUND` to a user session that does not declare `channel_threads` in the `X-Fluxer-Features` header. A bot needs no declaration. +:::note[User requests need the `channel_threads` client capability] +A route with a route header on this page returns 404 `NOT_FOUND` to a user request without the [`channel_threads` client capability](/http-api/threads/#client-capability). A `channel_id` that does not exist or names a channel outside a guild returns the same 404. ::: ## Forum channel fields @@ -21,22 +20,30 @@ A forum or media channel is a [channel object](/http-api/channels/#channel-objec | Field | Type | Description | | --- | --- | --- | -| topic | ?string | The guidelines of the channel (1-4,096 characters) | -| last_message_id1 | ?snowflake | The ID of the newest post | +| topic | ?string | The guidelines of the channel (1-4,096 characters), or null when none are set | +| last_message_id1 | ?snowflake | The ID of the newest post, or null when the channel has never had one | | flags | integer | The [channel flags](#channel-flags) of the channel | | available_tags | array[[forum tag](#forum-tag-object) object] | The tags a post can apply (max 20) | -| default_reaction_emoji | ?[default reaction](#default-reaction-object) object | The reaction a client shows on each post, or null when none is set | -| default_sort_order | ?integer | The [sort order](#forum-sort-orders) a client uses, or null when none is set | -| default_forum_layout?2 | integer | The [layout](#forum-layouts) a client uses | -| default_tag_setting | string | The [tag matching](#forum-tag-settings) search uses by default | -| default_auto_archive_duration | ?integer | The [auto archive duration](/http-api/threads/#auto-archive-durations) new posts inherit, or null when none is set | -| default_thread_rate_limit_per_user | integer | The slowmode interval in seconds new posts inherit | +| default_reaction_emoji | ?[default reaction](#default-reaction-object) object | The reaction clients show on each post, or null when none is set | +| default_sort_order | ?integer | The default [sort order](#forum-sort-orders) of posts, or null when none is set | +| default_forum_layout?2 | integer | The default [layout](#forum-layouts) of posts | +| default_tag_setting3 | string | The default [tag matching](#forum-tag-settings) of a post search | +| default_auto_archive_duration4 | ?integer | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations), or null when none is set | +| default_thread_rate_limit_per_user5 | integer | The slowmode interval in seconds new threads inherit | -1 [Channel Update](/gateway/events/#channel-update) is not emitted when this changes. A client advances it on [Thread Create](/gateway/events/#thread-create) +1 A new post sets it to the post ID and emits no [Channel Update](/gateway/events/#channel-update) -2 Present only on a forum channel +2 Present only on a forum channel. A forum channel that stores no layout reports `0` -`rate_limit_per_user` on the channel limits how often one member creates a post. Sending [Create message](/http-api/messages/#create-message) to the channel itself returns 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`, and so do reactions and pins. +3 A channel that stores no value reports `match_some` + +4 Fluxer does not apply it to a new post. A create request that omits `auto_archive_duration` gets 4320, as [Auto archive durations](/http-api/threads/#auto-archive-durations) states + +5 Applies when the request sets no `rate_limit_per_user`. A channel that stores no value reports `0` + +`rate_limit_per_user` on the channel limits how often one member creates a post. A bot and a member with `BYPASS_SLOWMODE` are exempt, and a denied post returns 400 `SLOWMODE_RATE_LIMITED`. + +[Create message](/http-api/messages/#create-message) to the channel itself returns 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`. Adding a reaction and pinning a message return the same code. [List channel messages](/http-api/messages/#list-channel-messages), listing pins, and the typing indicator return 400 `INVALID_CHANNEL_TYPE`. ### Example @@ -46,22 +53,27 @@ A forum or media channel is a [channel object](/http-api/channels/#channel-objec "type": 15, "guild_id": "1501314428688990000", "name": "help", - "topic": "One question per post", "position": 4, - "parent_id": null, "permission_overwrites": [], - "last_message_id": "1501320000000000000", + "parent_id": null, + "last_message_id": "1501320250000000000", + "last_pin_timestamp": null, + "topic": "One question per post", + "nsfw": false, + "nsfw_override": null, + "content_warning_level": 0, + "content_warning_text": null, "rate_limit_per_user": 0, "flags": 16, + "default_auto_archive_duration": null, + "default_thread_rate_limit_per_user": 0, "available_tags": [ - {"id": "1501314428688998301", "name": "Solved", "moderated": true, "emoji_id": null, "emoji_name": "✅"} + {"id": "1501314428688998301", "name": "Solved", "moderated": false, "emoji_id": null, "emoji_name": null} ], "default_reaction_emoji": null, "default_sort_order": null, - "default_forum_layout": 0, "default_tag_setting": "match_some", - "default_auto_archive_duration": null, - "default_thread_rate_limit_per_user": 0 + "default_forum_layout": 0 } ``` @@ -79,18 +91,22 @@ A forum or media channel is a [channel object](/http-api/channels/#channel-objec | Value | Name | Description | | --- | --- | --- | -| 15 | GUILD_FORUM | A guild channel whose messages live in posts | -| 16 | GUILD_MEDIA | A forum channel whose posts center on attachments | +| 15 | GUILD_FORUM | Guild channel that holds posts and no messages of its own | +| 16 | GUILD_MEDIA | Forum channel that can [hide media download options](#channel-flags) | ## Channel flags | Value | Name | Description | | --- | --- | --- | -| 1 << 1 | PINNED | The post is pinned to the top of its forum | -| 1 << 4 | REQUIRE_TAG | A new post must apply at least one tag | -| 1 << 15 | HIDE_MEDIA_DOWNLOAD_OPTIONS | Clients hide download, open, and copy actions on images and videos in a media channel | +| 1 << 1 | PINNED | The post is the pinned post of its channel | +| 1 << 4 | REQUIRE_TAG | A new post, or a change to the tags of a post, needs at least one tag | +| 1 << 15 | HIDE_MEDIA_DOWNLOAD_OPTIONS | Clients hide the media download options of the channel | -`PINNED` is set on a post, and a forum holds at most one pinned post. `REQUIRE_TAG` and `HIDE_MEDIA_DOWNLOAD_OPTIONS` are set on the channel. `HIDE_MEDIA_DOWNLOAD_OPTIONS` on a forum channel returns 400 `HIDE_MEDIA_DOWNLOAD_OPTION_MEDIA_ONLY`, and any other bit returns 400 `INVALID_FORM_BODY`. +`PINNED` is set on a post. A channel holds at most one pinned post, and pinning a second returns 400 `MAX_PINNED_THREADS_IN_FORUM`. Archiving a post clears `PINNED`. + +`REQUIRE_TAG` is set on a forum or media channel. A new post with no tag returns 400 `FORUM_TAG_REQUIRED`. [Modify thread](/http-api/threads/#modify-thread) with an empty `applied_tags` on a post returns the same code. + +`HIDE_MEDIA_DOWNLOAD_OPTIONS` is set on a media channel. On a forum channel it returns 400 `HIDE_MEDIA_DOWNLOAD_OPTION_MEDIA_ONLY`. Any other bit on a forum or media channel returns 400 `INVALID_FORM_BODY`. ## Forum tag object @@ -101,25 +117,27 @@ A forum or media channel is a [channel object](/http-api/channels/#channel-objec | id | snowflake | The ID of the tag | | name | string | The name of the tag (1-50 characters) | | moderated | boolean | Whether only thread moderators can apply or remove the tag | -| emoji_id | ?snowflake | The ID of a custom emoji of this guild, or null | -| emoji_name | ?string | A single Unicode emoji, or null | +| emoji_id | ?snowflake | The ID of a custom emoji of this guild, or null when the tag has none | +| emoji_name | ?string | A single Unicode emoji, or null when the tag has none | At most one of `emoji_id` and `emoji_name` is set. A tag name is unique within its channel, and the comparison is case-sensitive. +A member who is not a thread moderator and adds or removes a moderated tag on a post gets 403 `MISSING_PERMISSIONS`. + ## Default reaction object ### Structure | Field | Type | Description | | --- | --- | --- | -| emoji_id | ?snowflake | The ID of a custom emoji of this guild, or null | -| emoji_name | ?string | A single Unicode emoji, or null | +| emoji_id | ?snowflake | The ID of a custom emoji of this guild, or null for a Unicode emoji | +| emoji_name | ?string | A single Unicode emoji, or null for a custom emoji | ## Forum sort orders | Value | Name | Description | | --- | --- | --- | -| 0 | LATEST_ACTIVITY | Sort posts by their newest message | +| 0 | LATEST_ACTIVITY | Sort posts by latest activity | | 1 | CREATION_TIME | Sort posts by creation time | ## Forum layouts @@ -143,33 +161,35 @@ At most one of `emoji_id` and `emoji_name` is set. A tag name is unique within i | Field | Type | Description | | --- | --- | --- | -| topic? | ?string | The guidelines of the channel (1-4,096 characters) | +| topic? | ?string | The guidelines of the channel (1-4,096 characters), or null to clear them | | available_tags?1 | array[[forum tag](#forum-tag-object) object] | The complete tag set (max 20) | | default_reaction_emoji? | ?[default reaction](#default-reaction-object) object | The default reaction, or null to clear it | | default_sort_order? | ?integer | The [sort order](#forum-sort-orders), or null to clear it | -| default_forum_layout?2 | integer | The [layout](#forum-layouts) | -| default_tag_setting? | ?string | The [tag matching](#forum-tag-settings) default | -| default_auto_archive_duration? | ?integer | The [auto archive duration](/http-api/threads/#auto-archive-durations) new posts inherit, or null to clear it | -| default_thread_rate_limit_per_user? | integer | The slowmode interval in seconds new posts inherit (0-21,600) | +| default_forum_layout?2 | ?integer | The [layout](#forum-layouts), or null to reset it to `0` | +| default_tag_setting? | ?string | The [tag matching](#forum-tag-settings) default, or null to reset it to `match_some` | +| default_auto_archive_duration? | ?integer | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations), which Fluxer does not apply to a new thread, or null to clear it | +| default_thread_rate_limit_per_user? | ?integer | The slowmode interval in seconds new threads inherit (0-21,600), or null to clear it | | flags? | integer | The [channel flags](#channel-flags) of the channel | 1 An entry with `id` keeps that tag, and an entry without one creates a tag. A stored tag missing from the array is deleted -2 Read only for a forum channel +2 Accepted only on a forum channel. A media channel ignores it -A guild text channel accepts `default_auto_archive_duration` and `default_thread_rate_limit_per_user` as well. +A guild text or announcement channel accepts `default_auto_archive_duration` and `default_thread_rate_limit_per_user` as well. -A tag set over 20 returns 400 `MAX_FORUM_TAGS`, and a repeated name returns 400 `FORUM_TAG_NAMES_MUST_BE_UNIQUE`. An `id` the channel does not hold returns 404 `UNKNOWN_FORUM_TAG`. An `emoji_id` from another guild returns 404 `UNKNOWN_EMOJI`. +`available_tags` with more than 20 entries returns 400 `MAX_FORUM_TAGS`, and a repeated name returns 400 `FORUM_TAG_NAMES_MUST_BE_UNIQUE`. An `id` the channel does not hold returns 404 `UNKNOWN_FORUM_TAG`. An `emoji_id` that is not an emoji of this guild returns 404 `UNKNOWN_EMOJI`, except on a kept tag whose `emoji_id` is unchanged. A tag or default reaction with both `emoji_id` and `emoji_name`, or with an `emoji_name` that is not a single Unicode emoji, returns 400 `INVALID_FORM_BODY`. While another tag change holds the channel, a request with `available_tags` returns 429 `RESOURCE_LOCKED`. -`REQUIRE_TAG` on a channel whose every tag is moderated returns 400 `NO_TAGS_AVAILABLE_TO_NON_MODERATORS`. +Fluxer rejects a change that leaves `REQUIRE_TAG` set with no unmoderated tag with 400 `NO_TAGS_AVAILABLE_TO_NON_MODERATORS`. Two concurrent [Modify channel](/http-api/channels/#modify-channel) requests, one setting `REQUIRE_TAG` and one replacing `available_tags`, can each pass that check and together store that state. A post that names no tag then returns the same code to a caller who is not a thread moderator. -An invite to a forum or media channel returns 400 `INVALID_CHANNEL_TYPE`. +For a user request without the [`channel_threads` client capability](/http-api/threads/#client-capability), [Modify channel](/http-api/channels/#modify-channel) on a forum or media channel returns 404 `UNKNOWN_CHANNEL`. [Create guild channel](/http-api/guild-channels/#create-guild-channel) with `type` `15` or `16` returns 400 `INVALID_FORM_BODY`. + +[Create channel invite](/http-api/invites/#create-channel-invite) on a thread, a forum channel, or a media channel the caller can view returns 400 `INVALID_CHANNEL_TYPE`. ## Create forum tag -Adds a tag to a forum or media channel and returns the updated [channel object](/http-api/channels/#channel-object). Requires `MANAGE_CHANNELS`. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event. +Adds a tag to a forum or media channel. Requires `MANAGE_CHANNELS`. Returns the updated [channel object](/http-api/channels/#channel-object) on success. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event. ### Path parameters @@ -183,18 +203,26 @@ Adds a tag to a forum or media channel and returns the updated [channel object]( | --- | --- | --- | | name | string | The name of the tag (1-50 characters) | | moderated? | boolean | Whether only thread moderators can apply or remove the tag (default false) | -| emoji_id? | ?snowflake | The ID of a custom emoji of this guild | -| emoji_name? | ?string | A single Unicode emoji (max 64 characters) | +| emoji_id? | ?snowflake | The ID of a custom emoji of this guild, or null for none | +| emoji_name? | ?string | A single Unicode emoji (max 64 characters), or null for none | ### Response | Status | Body | Condition | | --- | --- | --- | | 200 | [channel](/http-api/channels/#channel-object) object | Tag was added | -| 400 | [error response](/http-api/#error-response) | Channel is not a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | +| 400 | [error response](/http-api/#error-response) | Channel is a guild channel other than a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | | 400 | [error response](/http-api/#error-response) | Channel has 20 tags and the request returns `MAX_FORUM_TAGS` | | 400 | [error response](/http-api/#error-response) | Channel has a tag with the name and the request returns `FORUM_TAG_NAMES_MUST_BE_UNIQUE` | -| 403 | [error response](/http-api/#error-response) | Caller lacks `MANAGE_CHANNELS` and the request returns `MISSING_PERMISSIONS` | +| 400 | [error response](/http-api/#error-response) | The new tag leaves `REQUIRE_TAG` with no unmoderated tag, returning `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` | +| 400 | [error response](/http-api/#error-response) | Body sets both emoji fields or an invalid emoji, returning `INVALID_FORM_BODY` | +| 400 | [error response](/http-api/#error-response) | Caller needs an authenticator and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `MANAGE_CHANNELS` and the request returns `MISSING_PERMISSIONS` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or is outside a guild, and the request returns `NOT_FOUND` | +| 404 | [error response](/http-api/#error-response) | Emoji is not an emoji of this guild and the request returns `UNKNOWN_EMOJI` | +| 429 | [error response](/http-api/#error-response) | Another tag change holds the channel, returning `RESOURCE_LOCKED` | + +The body is the forum or media channel with its [forum channel fields](#forum-channel-fields). It has no `thread_metadata`, `message_count`, `total_message_sent`, `member_count`, `applied_tags`, `member_ids_preview`, or `member`. ### Rate limit @@ -204,9 +232,43 @@ Adds a tag to a forum or media channel and returns the updated [channel object]( -Replaces a tag of a forum or media channel and returns the updated [channel object](/http-api/channels/#channel-object). Requires `MANAGE_CHANNELS`. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event. +Replaces a tag of a forum or media channel. Requires `MANAGE_CHANNELS`. Returns the updated [channel object](/http-api/channels/#channel-object) on success. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event. -The body matches [Create forum tag](#create-forum-tag). A `tag_id` the channel does not hold returns 404 `UNKNOWN_FORUM_TAG`. +An omitted `moderated` is stored as false, and an omitted emoji field is stored as null. + +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the forum or media channel | +| tag_id | snowflake | The ID of the tag | + +### JSON body + +| Field | Type | Description | +| --- | --- | --- | +| name | string | The name of the tag (1-50 characters) | +| moderated? | boolean | Whether only thread moderators can apply or remove the tag (default false) | +| emoji_id? | ?snowflake | The ID of a custom emoji of this guild, or null for none | +| emoji_name? | ?string | A single Unicode emoji (max 64 characters), or null for none | + +### Response + +| Status | Body | Condition | +| --- | --- | --- | +| 200 | [channel](/http-api/channels/#channel-object) object | Tag was replaced | +| 400 | [error response](/http-api/#error-response) | Channel is a guild channel other than a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | +| 400 | [error response](/http-api/#error-response) | Another tag has the name and the request returns `FORUM_TAG_NAMES_MUST_BE_UNIQUE` | +| 400 | [error response](/http-api/#error-response) | The change leaves `REQUIRE_TAG` with no unmoderated tag, returning `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` | +| 400 | [error response](/http-api/#error-response) | Body sets both emoji fields or an invalid emoji, returning `INVALID_FORM_BODY` | +| 400 | [error response](/http-api/#error-response) | Caller needs an authenticator and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `MANAGE_CHANNELS` and the request returns `MISSING_PERMISSIONS` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or is outside a guild, and the request returns `NOT_FOUND` | +| 404 | [error response](/http-api/#error-response) | Channel holds no tag with `tag_id` and the request returns `UNKNOWN_FORUM_TAG` | +| 404 | [error response](/http-api/#error-response) | The new `emoji_id` is not an emoji of this guild and the request returns `UNKNOWN_EMOJI` | +| 429 | [error response](/http-api/#error-response) | Another tag change holds the channel, returning `RESOURCE_LOCKED` | + +The body is the forum or media channel with its [forum channel fields](#forum-channel-fields). It has no `thread_metadata`, `message_count`, `total_message_sent`, `member_count`, `applied_tags`, `member_ids_preview`, or `member`. ### Rate limit @@ -216,9 +278,31 @@ The body matches [Create forum tag](#create-forum-tag). A `tag_id` the channel d -Removes a tag from a forum or media channel and returns the updated [channel object](/http-api/channels/#channel-object). Requires `MANAGE_CHANNELS`. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event. +Removes a tag from a forum or media channel. Requires `MANAGE_CHANNELS`. Returns the updated [channel object](/http-api/channels/#channel-object) on success. Emits a [Channel Update](/gateway/events/#channel-update) Gateway event. -A post that applied the tag keeps the ID until its tags next change, and a client ignores an ID absent from `available_tags`. +Every thread object an HTTP route returns afterwards leaves the deleted tag out of `applied_tags`, except in the `threads` array of the [guild audit log](/http-api/guild-audit-logs/). A Gateway thread payload can keep the ID until the post next changes. A post drops the stored ID the next time its tags change. + +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the forum or media channel | +| tag_id | snowflake | The ID of the tag | + +### Response + +| Status | Body | Condition | +| --- | --- | --- | +| 200 | [channel](/http-api/channels/#channel-object) object | Tag was removed | +| 400 | [error response](/http-api/#error-response) | Channel is a guild channel other than a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | +| 400 | [error response](/http-api/#error-response) | The removal leaves `REQUIRE_TAG` with no unmoderated tag, returning `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` | +| 400 | [error response](/http-api/#error-response) | Caller needs an authenticator and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `MANAGE_CHANNELS` and the request returns `MISSING_PERMISSIONS` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or is outside a guild, and the request returns `NOT_FOUND` | +| 404 | [error response](/http-api/#error-response) | Channel holds no tag with `tag_id` and the request returns `UNKNOWN_FORUM_TAG` | +| 429 | [error response](/http-api/#error-response) | Another tag change holds the channel, returning `RESOURCE_LOCKED` | + +The body is the forum or media channel with its [forum channel fields](#forum-channel-fields). It has no `thread_metadata`, `message_count`, `total_message_sent`, `member_count`, `applied_tags`, `member_ids_preview`, or `member`. ### Rate limit @@ -230,6 +314,12 @@ A post that applied the tag keeps the ID until its tags next change, and a clien Returns the owner and first message of each requested post in a forum or media channel. Requires `READ_MESSAGE_HISTORY`. +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the forum or media channel | + ### JSON body | Field | Type | Description | @@ -242,14 +332,28 @@ Returns the owner and first message of each requested post in a forum or media c | --- | --- | --- | | threads1 | map[snowflake, [post data](#post-data-object) object] | The post data keyed by post ID | -1 An ID that is not a post of this channel is left out +1 Has one key for each distinct requested ID. An ID that is not a post of this channel maps to null `owner` and null `first_message` #### Post data object | Field | Type | Description | | --- | --- | --- | -| owner | ?[guild member](/http-api/guild-members/#guild-member-object) object | The guild member who created the post, or null when they left the guild | -| first_message | ?[message](/http-api/messages/#message-object) object | The first message of the post, or null when it was deleted | +| owner1 | ?[guild member](/http-api/guild-members/#guild-member-object) object | The guild member who created the post | +| first_message2 | ?[message](/http-api/messages/#message-object) object | The first message of the post | + +1 Null when the creator is no longer a member of the guild, and for a post a webhook created + +2 Null when the first message was deleted + +### Response + +| Status | Body | Condition | +| --- | --- | --- | +| 200 | [post data](#response-body) map | Post data was returned | +| 400 | [error response](/http-api/#error-response) | Channel is a guild channel other than a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `READ_MESSAGE_HISTORY` and the request returns `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | The resolved age restriction is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or is outside a guild, and the request returns `NOT_FOUND` | ### Rate limit @@ -257,24 +361,17 @@ Returns the owner and first message of each requested post in a forum or media c ## Webhooks in forums -A webhook whose channel is a forum or media channel creates a post, or posts into an existing one. [Execute webhook](/http-api/webhooks/#execute-webhook) takes the fields below. +A webhook whose channel is a forum or media channel creates a post, or posts into an existing one. [Execute webhook](/http-api/webhooks/#execute-webhook) lists the `thread_id`, `thread_name`, and `applied_tags` fields and their error codes. -| Field | Type | Description | -| --- | --- | --- | -| thread_id?1 | snowflake | The query parameter naming the thread to post into | -| thread_name? | string | The name of a new post (1-100 characters) | -| applied_tags?2 | array[snowflake] | The IDs of the tags a new post applies (max 5) | - -1 The thread must belong to the channel of the webhook. Any other thread returns 404 `UNKNOWN_CHANNEL` - -2 A moderated tag returns 403 `MISSING_PERMISSIONS` - -`thread_id` also works on a webhook whose channel is a guild text channel, and on the webhook message routes. A forum execution without `thread_id` or `thread_name` returns 400 `WEBHOOK_FORUM_TARGET_REQUIRED`, and one with both returns 400 `WEBHOOK_FORUM_TARGET_CONFLICT`. - -`thread_name` or `applied_tags` on any other channel returns 400 `WEBHOOK_THREAD_NAME_REQUIRES_FORUM`. The Slack and GitHub formats need `thread_id` on a forum, and 400 `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` answers without one. - -A locked thread refuses a webhook message with 400 `THREAD_LOCKED`. An archived thread that is not locked is unarchived by it. +A locked thread refuses a webhook message with 400 `THREAD_LOCKED`. An archived thread that is not locked is unarchived by it, and Fluxer emits a [Thread Update](/gateway/threads/#thread-update) Gateway event. When the guild already has 1,000 active threads, the message returns 400 `MAX_ACTIVE_THREADS` and the thread stays archived. Editing a webhook message in an archived thread returns 400 `THREAD_ARCHIVED`, and in a locked thread 400 `THREAD_LOCKED`. ## New post notifications -A [channel notification override](/http-api/users/settings/#channel-notification-override-object) on a forum or media channel has `flags`. `NEW_FORUM_THREADS_OFF` (1 << 13) and `NEW_FORUM_THREADS_ON` (1 << 14) turn notifications for new posts off or on for the caller. +A [channel notification override](/http-api/users/settings/#channel-notification-override-object) has `flags`, and only these two bits are stored. + +| Value | Name | Description | +| --- | --- | --- | +| 1 << 13 | NEW_FORUM_THREADS_OFF | No push notification for a new post in the channel | +| 1 << 14 | NEW_FORUM_THREADS_ON | A value Fluxer stores and does not read | + +A new post sends a push notification to a member when the channel resolves to the all messages notification level. A mute on the guild, on the parent category, or on the channel suppresses it, and so does `NEW_FORUM_THREADS_OFF`. Fluxer ignores `flags` in a user request without the [`channel_threads` client capability](/http-api/threads/#client-capability), and the stored value stays. diff --git a/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx b/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx index 7f1ed4119..b25290a5c 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-audit-logs.mdx @@ -14,7 +14,7 @@ A route marked Audit reason accepts the `X-Audit-Log-Reason` request header, lis 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 channel and message routes that write entries record the header as well: [Modify channel](/http-api/channels/#modify-channel), [Delete or leave channel](/http-api/channels/#delete-or-leave-channel), [Set channel permission overwrite](/http-api/channels/#set-channel-permission-overwrite), [Delete channel permission overwrite](/http-api/channels/#delete-channel-permission-overwrite), [Delete message](/http-api/messages/#delete-message), [Bulk delete messages](/http-api/messages/#bulk-delete-messages), [Pin message](/http-api/messages/#pin-message), and [Unpin message](/http-api/messages/#unpin-message). +[Modify channel](/http-api/channels/#modify-channel), [Delete or leave channel](/http-api/channels/#delete-or-leave-channel), [Set channel permission overwrite](/http-api/channels/#set-channel-permission-overwrite), [Delete channel permission overwrite](/http-api/channels/#delete-channel-permission-overwrite), [Delete message](/http-api/messages/#delete-message), [Bulk delete messages](/http-api/messages/#bulk-delete-messages), [Pin message](/http-api/messages/#pin-message), and [Unpin message](/http-api/messages/#unpin-message) record the header as well. [Start thread from message](/http-api/threads/#start-thread-from-message) and [Start thread](/http-api/threads/#start-thread) record it on their thread entries. [Create forum tag](/http-api/forums/#create-forum-tag), [Modify forum tag](/http-api/forums/#modify-forum-tag), and [Delete forum tag](/http-api/forums/#delete-forum-tag) record it on their channel update entries. Two operations use a reason from the request body when the header supplies none: @@ -40,6 +40,7 @@ One page of audit history together with the accounts and webhooks it references. | audit_log_entries1 | array[[guild audit log entry](#guild-audit-log-entry-object) object] | The page of entries this request returned | | users2 | array[[partial user](/http-api/users/#partial-user-object) object] | The accounts the returned entries name | | webhooks3 | array[[audit log webhook](#audit-log-webhook-object) object] | The webhooks a returned webhook action targets | +| threads?4 | array[[thread](/http-api/threads/#thread-object) object] | The threads a returned thread action targets | 1 The page ordering is defined by the [query parameters](#query-parameters) of the request that produced it @@ -47,6 +48,8 @@ One page of audit history together with the accounts and webhooks it references. 3 One object for each distinct webhook that a returned webhook action targets and that still exists, so a deleted webhook resolves to no object +4 Present only for a bot and for a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability). One object for each distinct thread that a returned [thread action](#audit-actions) targets and that still exists + `users` has the actor of every returned entry and each account the entry references: - `target_id` of every action from [MEMBER_KICK](#audit-actions) through [MEMBER_DISCONNECT](#audit-actions), and of [BOT_ADD](#audit-actions) @@ -142,6 +145,9 @@ The Value column is the numeric `action_type`, which is also the value the `acti | 90 | STICKER_CREATE | Sticker was created. `target_id` is the sticker, and the action records no options | | 91 | STICKER_UPDATE | Sticker was changed. `target_id` is the sticker, and the action records no options | | 92 | STICKER_DELETE | Sticker was deleted. `target_id` is the sticker, and the action records no options | +| 110 | THREAD_CREATE | Thread was created. `target_id` is the thread, and the action records no options | +| 111 | THREAD_UPDATE | Thread was changed. `target_id` is the thread, and the action records `type` | +| 112 | THREAD_DELETE | Thread was deleted. `target_id` is the thread, and the action records `type` | 1 Recorded by [Modify guild](/http-api/guilds/#modify-guild), by [Modify guild custom invite URL](/http-api/guilds/#modify-guild-custom-invite-url), and by [Transfer guild ownership](/http-api/guild-members/#transfer-guild-ownership), and `target_id` is the guild in every case @@ -165,6 +171,8 @@ The Value column is the numeric `action_type`, which is also the value the `acti 11 [Follow announcement channel](/http-api/channels/#follow-announcement-channel) records `WEBHOOK_CREATE` in the following guild with `type` `2`, and deleting that webhook records `WEBHOOK_DELETE` the same way. Follower webhooks that Fluxer removes because the announcement channel was deleted or converted record no entry +Deleting a text, announcement, forum, or media channel deletes its threads and records only [CHANNEL_DELETE](#audit-actions). [Delete thread](/http-api/threads/#delete-thread) records `THREAD_DELETE`. Fluxer records [THREAD_UPDATE](#audit-actions) only when a [thread change field](#thread-change-fields) differs. + :::note[An update that changes nothing records no entry] GUILD_UPDATE, CHANNEL_UPDATE, CHANNEL_OVERWRITE_UPDATE, MEMBER_UPDATE, MEMBER_ROLE_UPDATE, MEMBER_MOVE, ROLE_UPDATE, WEBHOOK_UPDATE, EMOJI_UPDATE, and STICKER_UPDATE need a change other than `ip`. GUILD_UPDATE also ignores `guild_id`, `member_count`, and the image width and height fields. A reason alone records nothing. ::: @@ -203,7 +211,7 @@ Every field is present only for the actions listed against it in the [audit acti 5 Recorded by [MEMBER_ROLE_UPDATE](#audit-actions), [ROLE_UPDATE](#audit-actions), and an overwrite action whose target is a role other than `@everyone` -6 The value is a [channel type](/http-api/channels/#channel-types) for the channel actions, a [permission overwrite type](/http-api/channels/#permission-overwrite-types) for the overwrite actions, and a [webhook type](/http-api/webhooks/#webhook-types) for the webhook actions +6 The value is a [channel type](/http-api/channels/#channel-types) for the channel and thread actions, a [permission overwrite type](/http-api/channels/#permission-overwrite-types) for the overwrite actions, and a [webhook type](/http-api/webhooks/#webhook-types) for the webhook actions ## Audit log change object @@ -221,7 +229,7 @@ One field a mutation changed, with the value on each side of the change. 2 A creation has only `new_value`, a deletion has only `old_value`, and a modification has both, so a change object always has at least one of the two -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. +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. The `available_tags` and `default_reaction_emoji` [channel change fields](#channel-change-fields) record an array of objects and an object. :::note[A 64-bit domain value is a decimal string] 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. @@ -304,13 +312,45 @@ Recorded by [CHANNEL_CREATE](#audit-actions), [CHANNEL_UPDATE](#audit-actions), | bitrate | ?number | The voice bitrate in bits per second | | rtc_region | ?string | The pinned voice region, or null when the channel is routed automatically | | permission_overwrite_count2 | number | The number of permission overwrites the channel has | +| flags3 | number | The [channel flags](/http-api/forums/#channel-flags) of the channel | +| default_auto_archive_duration3 | number | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations) | +| default_thread_rate_limit_per_user3 | number | The slowmode interval in seconds new threads inherit | +| available_tags3 | array[[forum tag](/http-api/forums/#forum-tag-object) object] | The tags a post can apply | +| default_reaction_emoji3 | [default reaction](/http-api/forums/#default-reaction-object) object | The reaction clients show on each post | +| default_sort_order3 | number | The default [sort order](/http-api/forums/#forum-sort-orders) of posts | +| default_forum_layout3 | number | The default [layout](/http-api/forums/#forum-layouts) of posts | +| default_tag_setting3 | string | The default [tag matching](/http-api/forums/#forum-tag-settings) of a post search | 1 Recorded only for a `GUILD_LINK` [channel type](/http-api/channels/#channel-types) 2 No current channel operation records the field. An individual overwrite mutation records its own [permission overwrite](#permission-overwrite-change-fields) entry +3 Recorded only by [CHANNEL_CREATE](#audit-actions) and [CHANNEL_UPDATE](#audit-actions), and only for a channel that stores thread or forum settings. No value is null + +`default_auto_archive_duration` and `default_thread_rate_limit_per_user` apply to a guild text, announcement, forum, or media channel. `default_forum_layout` applies to a forum channel. `flags`, `available_tags`, `default_reaction_emoji`, `default_sort_order`, and `default_tag_setting` apply to a forum or media channel. + Changing `permission_overwrites` through [Modify channel](/http-api/channels/#modify-channel) records overwrite entries and adds no change to the [CHANNEL_UPDATE](#audit-actions) entry. +#### Thread change fields + +Recorded by [THREAD_CREATE](#audit-actions), [THREAD_UPDATE](#audit-actions), and [THREAD_DELETE](#audit-actions). + +| Field | Type | Description | +| --- | --- | --- | +| name | string | The name of the thread | +| type | number | The [thread type](/http-api/threads/#thread-types) | +| archived | boolean | Whether the thread is archived | +| locked | boolean | Whether the thread is locked | +| auto_archive_duration | number | The [auto archive duration](/http-api/threads/#auto-archive-durations) in minutes | +| rate_limit_per_user | number | The slowmode interval in seconds | +| flags | number | The [channel flags](/http-api/forums/#channel-flags) of the thread | +| invitable1 | boolean | Whether a member who is not a thread moderator can add members who are not thread moderators | +| applied_tags2 | array[string] | The decimal IDs of the applied [forum tags](/http-api/forums/#forum-tag-object) | + +1 Recorded only for a private thread + +2 Recorded only for a thread whose parent is a forum or media channel + #### Permission overwrite change fields Recorded by the [channel overwrite actions](#audit-actions). Every value in this family is a string. @@ -485,7 +525,7 @@ A guild ID that names no guild returns 404 `UNKNOWN_GUILD`. A non-member of an e | before?2 | snowflake | The cursor selecting entries older than this audit entry ID | | after?2 | snowflake | The cursor selecting entries newer than this audit entry ID | | user_id?3 | snowflake | The acting user ID the page is filtered by | -| action_type?3 | integer | The [audit action](#audit-actions) value the page is filtered by | +| action_type?3 4 | integer | The [audit action](#audit-actions) value the page is filtered by | 1 A negative value and a value above 100 are both rejected, and the accepted value `0` is processed as `1` @@ -493,10 +533,14 @@ A guild ID that names no guild returns 404 `UNKNOWN_GUILD`. A non-member of an e 3 Supplying either filter disables message deletion consolidation for the request, so a page filtered by actor or action returns the unconsolidated entries +4 A caller other than a bot or a user session that declares `channel_threads` receives 400 `INVALID_FORM_BODY` for 110, 111, or 112 + The read is ordered from newest to oldest by entry ID. `before` selects entries below the cursor and `after` selects entries above it, both read in that same descending order. An `after` page therefore begins with the newest entry above the cursor. Consolidated entries have new IDs but retain the position of the run they replace. Sort the page by ID if a strictly descending array is required. +A caller other than a bot or a user session that declares `channel_threads` receives no entry about a thread, a forum channel, or a media channel. Its remaining entries lose the thread and forum keys of the [channel change fields](#channel-change-fields) and every [thread permission bit](/http-api/threads/#thread-permissions). The `webhooks` array also omits each webhook of a forum or media channel. + A stored update entry that fails the [no-change rule](#audit-actions) is skipped and does not count towards `limit`. A page holds fewer entries than `limit` only when the guild has no further matching entries in that direction. ### Response diff --git a/fluxer_docs/src/content/docs/http-api/guild-channels.mdx b/fluxer_docs/src/content/docs/http-api/guild-channels.mdx index b62e16491..1d537b21e 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-channels.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-channels.mdx @@ -16,7 +16,7 @@ A request whose path names a guild that has [UNAVAILABLE_FOR_EVERYONE](/http-api ## Guild channel types -These are the [channel types](/http-api/channels/#channel-types) that can exist inside a guild, and exactly the values [Create guild channel](#create-guild-channel) accepts in `type`. +These are the [channel types](/http-api/channels/#channel-types) [Create guild channel](#create-guild-channel) accepts in `type`. A guild also holds threads, which the [Threads resource](/http-api/threads/) creates. | Value | Name | Description | | --- | --- | --- | @@ -25,6 +25,10 @@ These are the [channel types](/http-api/channels/#channel-types) that can exist | 5 | GUILD_ANNOUNCEMENT | Text channel whose messages can be [published](/topics/announcement-channels/) to the channels that follow it | | 4 | GUILD_CATEGORY | Category that groups other guild channels and supplies inherited permission overwrites | | 998 | GUILD_LINK | Channel whose only content is an external destination URL | +| 15 | GUILD_FORUM1 | Guild channel that holds [posts](/http-api/forums/) and no messages of its own | +| 16 | GUILD_MEDIA1 | [Forum channel](/http-api/forums/) that can [hide media download options](/http-api/forums/#channel-flags) | + +1 Accepted only from a bot or a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability). Any other caller receives 400 `INVALID_FORM_BODY` ## Create permission overwrite object @@ -103,13 +107,13 @@ Naming a category as the preceding sibling places the moved channel after that c Returns every [channel object](/http-api/channels/#channel-object) in the guild that the authenticated member can see. Membership is the only requirement. -Fluxer evaluates [channel visibility](/http-api/permissions/#permission-computation) per channel. A channel is included when the member holds [VIEW_CHANNEL](/http-api/permissions/) in it, or when the member holds temporary access to it through a voice transition. A category is included when at least one child grants the member `VIEW_CHANNEL`. A member with no visible channel receives an empty array. +Fluxer evaluates [channel visibility](/http-api/permissions/#permission-computation) per channel. A channel is included when the member holds [VIEW_CHANNEL](/http-api/permissions/) in it, or when the member holds temporary access to it through a voice transition. A forum or media channel is included only for a bot or a user session that declares `channel_threads`, and no thread is included. A category is included when at least one child grants the member `VIEW_CHANNEL`. A member with no visible channel receives an empty array. :::note[Every visible channel arrives in one body] The route accepts no limit and no cursor. Each entry is a complete channel object whose `permission_overwrites` array is always present and always complete. ::: -A guild holds at most the instance-configured `max_guild_channels` [limit](/http-api/instance/#limit-keys), which defaults to 500. +A guild holds at most the instance-configured `max_guild_channels` [limit](/http-api/instance/#limit-keys), which defaults to 500. A thread does not count towards this limit or towards `max_channels_per_category`. [Thread lifecycle](/http-api/threads/#thread-lifecycle) states the active thread limit. ### Path parameters @@ -139,7 +143,7 @@ Creates a guild channel and returns its [channel object](/http-api/channels/#cha - Supplying `permission_overwrites` also requires [MANAGE_ROLES](/http-api/permissions/) at guild level. -`MANAGE_CHANNELS` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it also 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. +`MANAGE_CHANNELS` is an [elevated permission](/http-api/permissions/#elevated-permissions). It also 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. ### Path parameters @@ -149,13 +153,13 @@ Creates a guild channel and returns its [channel object](/http-api/channels/#cha ### JSON body -`type` is required and decides which kind of channel is created. Every type accepts the same field set, so a field that has no meaning for the selected type is accepted and then ignored. +`type` is required and decides which kind of channel is created. Every type accepts the field set below. A field that has no meaning for the selected type is accepted and ignored. A forum channel also accepts every field of the [forum field table](/http-api/forums/#creating-and-modifying-a-forum). A media channel accepts every field of the [forum field table](/http-api/forums/#creating-and-modifying-a-forum) except `default_forum_layout`. | Field | Type | Description | | --- | --- | --- | | type | integer | [Guild channel type](#guild-channel-types) | | name12 | string | Channel name (1-100 characters after normalisation) | -| topic? | ?string | Channel topic (1-1024 characters) | +| topic? | ?string | Channel topic (1-1024 characters, or 1-4096 for a forum or media channel) | | url?3 | ?string | Destination URL for a link channel | | parent_id?4 | ?snowflake | Parent category | | bitrate?511 | ?integer | Voice channel bitrate in bits per second (8000-384000, default 64000) | @@ -167,10 +171,12 @@ Creates a guild channel and returns its [channel object](/http-api/channels/#cha | nsfw_override?8 | ?boolean | Explicit age restriction, or null to inherit from the parent category and then the guild | | content_warning_level?9 | integer | [Content warning level](/http-api/channels/#content-warning-levels) (default 0) | | content_warning_text?10 | ?string | Content warning text (max 200 characters), or null to inherit | +| default_auto_archive_duration?12 | ?integer | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations), which Fluxer does not apply to a new thread, or null to clear it | +| default_thread_rate_limit_per_user?12 | ?integer | The slowmode interval in seconds new threads inherit (0-21,600), or null to clear it | 1 Normalisation strips control, join, bidirectional, and tag code points, collapses whitespace runs to one space, and trims. An empty result is rejected with `NAME_EMPTY_AFTER_NORMALIZATION` -2 A text or announcement channel name is also lowercased, its whitespace replaced with hyphens, and ASCII punctuation other than `-`, `.`, and `_` removed, unless the guild has [TEXT_CHANNEL_FLEXIBLE_NAMES](/http-api/guilds/#guild-features). That pass rewrites the stored name and never fails the request +2 A text, announcement, forum, or media channel name is also lowercased, its whitespace replaced with hyphens, and ASCII punctuation other than `-`, `.`, and `_` removed, unless the guild has [TEXT_CHANNEL_FLEXIBLE_NAMES](/http-api/guilds/#guild-features). That pass rewrites the stored name and never fails the request 3 An `http` or `https` URL of at most 2048 characters with a host and no embedded credentials. It is stored for every channel type @@ -188,13 +194,15 @@ Creates a guild channel and returns its [channel object](/http-api/channels/#cha 10 The value is trimmed, and an empty result is stored as null -11 The stored value is capped at 96000 unless the guild holds an [audio bitrate feature](/http-api/guilds/#guild-features). A higher value is stored at the cap rather than rejected +11 The stored value is capped at 96000 unless the guild holds an [audio bitrate feature](/http-api/guilds/#guild-features). A higher value is stored at the cap + +12 Stored only for a text, announcement, forum, or media channel. Fluxer discards both fields from a caller other than a bot or a user session that declares `channel_threads` 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`. 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 error message states 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, announcement, or link sibling when the category holds no voice channel. A text, announcement, or link channel is placed after the last text, announcement, 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. +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, announcement, link, forum, or media sibling when the category holds no voice channel. Any other channel is placed after the last text, announcement, link, forum, or media sibling, and a channel with no such sibling is placed directly after the category itself. The rest of the guild is not renumbered. Two channels can hold the same stored position until the next [hierarchy update](#modify-guild-channel-positions) renumbers them. :::caution[Overwrites are checked against the parent category] Fluxer compares the allowed bits of every supplied overwrite against the caller's effective permissions in the parent category, or in the guild when no parent is supplied. A bit the caller does not hold there fails the entire request with 403 `MISSING_PERMISSIONS`. @@ -219,7 +227,7 @@ Only the allowed mask is compared, so a supplied deny is stored without any auth The operation consumes one guild channel slot and, when a parent is supplied, one slot in that category. It creates a [`CHANNEL_CREATE`](/http-api/guild-audit-logs/#audit-actions) guild audit log entry with the supplied reason, plus one [`CHANNEL_OVERWRITE_CREATE`](/http-api/guild-audit-logs/#audit-actions) entry for every stored overwrite, whether that overwrite was supplied or inherited. Reaching either channel limit fails the request without consuming a slot. -It emits [Channel Create](/gateway/events/#channel-create) with the complete channel object only to sessions that can view the new channel. +It emits [Channel Create](/gateway/events/#channel-create) with the complete channel object only to sessions that can view the new channel. For a forum or media channel the event reaches only a session with [thread access](/gateway/threads/#session-flag). ### Rate limit @@ -236,13 +244,13 @@ Applies a guild channel hierarchy update and returns 204 with an empty body. Req - Setting `lock_permissions` on an entry that changes the parent also requires [MANAGE_ROLES](/http-api/permissions/) in the moved channel. - The operation records no guild audit log entry, so an `X-Audit-Log-Reason` header has no effect. -`MANAGE_CHANNELS` is an [elevated permission](/http-api/permissions/#elevated-permissions), so it also 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. +`MANAGE_CHANNELS` is an [elevated permission](/http-api/permissions/#elevated-permissions). It also 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. Concurrent hierarchy updates to the same guild can return 423 [`GENERAL_ERROR`](/http-api/errors/) with a `Retry-After` header of two seconds. An unknown channel, parent or sibling rejects the entire request. Fluxer applies entries one at a time. When a later entry fails a check after that existence check, including the `lock_permissions` authority check, every earlier entry stays applied. Moving a category moves its children with it, and neither the category nor its children count as destination siblings. -After a move, positions run consecutively from 1 across the guild. Each category is followed by its children, with text and link channels before voice channels. +After a move, positions run consecutively from 1 across the guild. Each category is followed by its children, with voice channels last. :::note[A move can change other channels' positions] Renumbering can change positions of channels not named in the request. An entry that leaves the order unchanged has no effect. @@ -256,10 +264,10 @@ Renumbering can change positions of channels not named in the request. An entry ### JSON body -The top-level body is an array of [channel position objects](#channel-position-object) of any length, including zero. Every channel, parent, and sibling an entry names must exist in this guild. A category cannot be given a parent, and a parent that is not a category is refused. A channel cannot be positioned relative to itself or to one of its own children. Moving a channel into a full category is refused with 400 `MAX_CATEGORY_CHANNELS`. +The top-level body is an array of [channel position objects](#channel-position-object) of any length, including zero. Every channel, parent, and sibling an entry names must exist in this guild. For a caller other than a bot or a user session that declares `channel_threads`, a forum or media channel counts as absent. Naming one returns the same validation error as an unknown channel. A thread has no position and counts as absent for every caller. A category cannot be given a parent, and a parent that is not a category is refused. A channel cannot be positioned relative to itself or to one of its own children. Moving a channel into a full category is refused with 400 `MAX_CATEGORY_CHANNELS`. :::caution[Inside a category, voice channels come last] -Placing a voice channel above a text, announcement, or link sibling, or placing one of those below a voice sibling, returns `VOICE_CHANNELS_CANNOT_BE_ABOVE_TEXT_CHANNELS`. +Placing a voice channel above a text, announcement, link, forum, or media sibling, or placing one of those below a voice sibling, returns `VOICE_CHANNELS_CANNOT_BE_ABOVE_TEXT_CHANNELS`. ::: Channels at the guild root are exempt from this restriction. diff --git a/fluxer_docs/src/content/docs/http-api/guild-emojis.mdx b/fluxer_docs/src/content/docs/http-api/guild-emojis.mdx index c5fb20dc2..a9c299581 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-emojis.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-emojis.mdx @@ -6,7 +6,7 @@ description: Guild emoji objects, uploads, cloning, bulk creation, renaming, and 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. +An emoji is a custom image that one guild owns. Members use it in messages and reactions, and a forum channel can use it on a tag or as its default reaction. 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. 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. @@ -376,7 +376,7 @@ A later purge of the source emoji does not affect an existing clone in another g ### Side effects -Deletion frees one guild emoji slot, emits [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the remaining collection and records an [`EMOJI_DELETE`](/http-api/guild-audit-logs/#audit-actions) audit entry. Existing messages that reference the emoji are unchanged. +Deletion frees one guild emoji slot, emits [Guild Emojis Update](/gateway/events/#guild-emojis-update) with the remaining collection and records an [`EMOJI_DELETE`](/http-api/guild-audit-logs/#audit-actions) audit entry. Existing messages that reference the emoji are unchanged. A [forum tag](/http-api/forums/#forum-tag-object) or [default reaction](/http-api/forums/#default-reaction-object) that names the emoji keeps its `emoji_id`. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/guild-members.mdx b/fluxer_docs/src/content/docs/http-api/guild-members.mdx index f45957fc0..465305d6f 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-members.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-members.mdx @@ -393,6 +393,8 @@ Rejoining within one year resumes any communication timeout that has not expired The member loses their guild profile and roles. Read states, guild settings and guild folders are unchanged. +Removal from the guild removes every membership in its threads. Fluxer emits a [Thread Members Update](/gateway/threads/#thread-members-update) for each of them, as [Thread member flags](/http-api/thread-members/#thread-member-flags) states. + 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 diff --git a/fluxer_docs/src/content/docs/http-api/guild-moderation.mdx b/fluxer_docs/src/content/docs/http-api/guild-moderation.mdx index 581c0a921..1a81ef430 100644 --- a/fluxer_docs/src/content/docs/http-api/guild-moderation.mdx +++ b/fluxer_docs/src/content/docs/http-api/guild-moderation.mdx @@ -151,12 +151,14 @@ The second ban replaces the previous ban's details. Re-issuing can turn a perman ### Side effects -The operation records a [`MEMBER_BAN_ADD`](/http-api/guild-audit-logs/#audit-actions) audit entry with the [guild ban change fields](/http-api/guild-audit-logs/#guild-ban-change-fields). It emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) and [Guild Ban Add](/gateway/events/#guild-ban-add), subject to [event filtering](/gateway/event-filtering/). A positive deletion interval permanently deletes the target's guild messages within that interval. +The operation records a [`MEMBER_BAN_ADD`](/http-api/guild-audit-logs/#audit-actions) audit entry with the [guild ban change fields](/http-api/guild-audit-logs/#guild-ban-change-fields). It emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) and [Guild Ban Add](/gateway/events/#guild-ban-add), subject to [event filtering](/gateway/event-filtering/). A positive deletion interval permanently deletes the target's messages within that interval in every channel and thread of the guild. A current member is removed. The guild receives [Guild Member Remove](/gateway/events/#guild-member-remove) and the banned account receives [Guild Delete](/gateway/events/#guild-delete). No `MEMBER_KICK` audit entry is recorded. A ban does not preserve an active communication timeout for a later rejoin, and [Remove guild member](/http-api/guild-members/#remove-guild-member) does. The banned account loses its guild nickname, guild avatar hash, guild banner hash, biography, pronouns, accent colour, and role set. Its read states and guild settings stay in place. +Removal from the guild removes every membership in its threads. Fluxer emits a [Thread Members Update](/gateway/threads/#thread-members-update) for each of them, as [Thread member flags](/http-api/thread-members/#thread-member-flags) states. + From that point Fluxer refuses a join attempt by the banned account. The address and email matches described under the [guild ban object](#guild-ban-object) apply as well. ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/guilds.mdx b/fluxer_docs/src/content/docs/http-api/guilds.mdx index 6961cf747..25d92f584 100644 --- a/fluxer_docs/src/content/docs/http-api/guilds.mdx +++ b/fluxer_docs/src/content/docs/http-api/guilds.mdx @@ -64,6 +64,7 @@ A guild object contains the guild's configuration. The operation that returns it | online_count?14 | integer | Online presence count held by the main Gateway | | approximate_member_count?15 | integer | Approximate member count | | approximate_presence_count?15 | integer | Approximate online presence count | +| threads_active?16 | boolean | Whether the caller can use threads, forum channels, and media channels in the guild, always `true` when present | 1 An animated icon hash has the `a_` prefix, and that prefix is removed from the returned value while the guild lacks `ANIMATED_ICON` @@ -89,12 +90,16 @@ A guild object contains the guild's configuration. The operation that returns it 12 Only [Get guild](#get-guild) populates this field -13 Only [Get guild](#get-guild) populates this field, and the array is filtered to the channels the caller can currently view +13 Only [Get guild](#get-guild) populates this field, with the channels the caller can currently view. The array has forum and media channels for every caller and has no threads 14 Only [Get guild](#get-guild) populates these fields 15 Only [List current user guilds](#list-current-user-guilds) populates these fields, and only when `with_counts` is true. A guild for which the Gateway holds no cached counts reports 0 for both +16 Only [List current user guilds](#list-current-user-guilds) populates this field, and only for a bot or a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability) + +A caller other than a bot or a user session that declares `channel_threads` receives `permissions` and each role `permissions` with the [thread permission bits](/http-api/threads/#thread-permissions) removed. The `permission_overwrites` of `channels` keep the thread bits. + :::note[An absent field means the operation omitted it] `roles`, `emojis`, `stickers`, `channels`, and every count field arrive only from the operations named in the footnotes above. A guild returned without `roles` can still hold roles. ::: @@ -230,6 +235,8 @@ A creation template describes the roles and channels that [Create guild](#create 4 The value is stored on the created role. The everyone role is always created with no Unicode emoji +A resolved everyone mask that has `SEND_MESSAGES` and no [thread permission bit](/http-api/threads/#thread-permissions) also receives `CREATE_PUBLIC_THREADS`, `CREATE_PRIVATE_THREADS`, and `SEND_MESSAGES_IN_THREADS`. + ### Guild template channel object #### Structure @@ -251,7 +258,7 @@ A creation template describes the roles and channels that [Create guild](#create 1 A duplicate identifier in the same template rejects creation with 400 `GUILD_TEMPLATE_INVALID` -2 The value 0 creates a text channel, 2 a voice channel, 4 a category, and 5 an announcement channel. The value 13, the stage channel type of another platform, creates a voice channel. Fluxer skips every other value, so the channel is not created +2 The value 0 creates a text channel, 2 a voice channel, 4 a category, and 5 an announcement channel. The value 13, the stage channel type of another platform, creates a voice channel. Fluxer skips every other value, a forum channel (15) and a media channel (16) included, and creates no channel for it 3 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 @@ -390,7 +397,7 @@ Each value in the guild's `features` array is a capability or availability flag. | INVITE_SPLASH | Guild can use invite splash assets | | INVITES_DISABLED1 | Guild invite use is disabled | | RAID_DETECTED | Raid detection is active and invites are restricted | -| TEXT_CHANNEL_FLEXIBLE_NAMES1 | Text channel names keep uppercase letters, spaces, and punctuation | +| TEXT_CHANNEL_FLEXIBLE_NAMES1 | Text, announcement, forum, and media channel names keep uppercase letters, spaces, and punctuation | | HIDE_OWNER_CROWN1 | Guild owner crown is hidden | | MORE_EMOJI2 | Legacy increased emoji slot allowance | | MORE_STICKERS2 | Legacy increased sticker slot allowance | @@ -517,7 +524,7 @@ A `name` outside 1 to 100 characters after normalisation returns `STRING_LENGTH_ The `icon` base64 payload is bounded to 1 to 13981016 characters and is otherwise rejected with `BASE64_LENGTH_INVALID`, and a payload that is not valid base64 with `INVALID_BASE64_FORMAT`. The decoded image must fit within the instance `avatar_max_size` limit, which defaults to the 10 MiB ceiling, and a larger image is rejected with `IMAGE_SIZE_EXCEEDS_LIMIT`. PNG, JPEG, WebP, GIF, APNG, AVIF, HEIC, HEIF, JXL, and SVG are accepted. An animated AVIF is rejected with `INVALID_IMAGE_FORMAT`. -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. +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 plus `CREATE_PUBLIC_THREADS`, `CREATE_PRIVATE_THREADS`, and `SEND_MESSAGES_IN_THREADS`. The `general` channel becomes the system channel. :::note[Embedded collections arrive on the Guild Create Dispatch] The creation response has no roles, channels, emojis, stickers, or member counts. Read [Get guild](#get-guild) afterwards to fetch them over HTTP. @@ -720,7 +727,7 @@ Every successful request emits [Guild Update](/gateway/events/#guild-update) to A request that changes the stored value of at least one body field records a [`GUILD_UPDATE`](/http-api/guild-audit-logs/#audit-actions) audit entry with the previous and new values. That entry emits [Guild Audit Log Entry Create](/gateway/events/#guild-audit-log-entry-create) to sessions that can read the audit log. -Removing `TEXT_CHANNEL_FLEXIBLE_NAMES` renames every guild text channel whose stored name changes when Fluxer trims it, lowercases it, replaces each whitespace run with a hyphen, and removes disallowed punctuation. When at least one channel is renamed, the removal emits one [Channel Update Bulk](/gateway/events/#channel-update-bulk) Dispatch with every channel in the guild. +Removing `TEXT_CHANNEL_FLEXIBLE_NAMES` renames every guild text and announcement channel whose stored name changes when Fluxer trims it, lowercases it, replaces each whitespace run with a hyphen, and removes disallowed punctuation. A forum or media channel keeps its stored name until [Modify channel](/http-api/channels/#modify-channel) next sets `name`. When at least one channel is renamed, the removal emits one [Channel Update Bulk](/gateway/events/#channel-update-bulk) Dispatch with every channel in the guild. Replacing an image removes the previous asset. A failed replacement leaves the previous image unchanged. @@ -755,7 +762,7 @@ While `single_community_enabled` is true in the [instance policy](/admin-api/ins The body is the optional [sudo verification fields](#sudo-verification-fields). A request that already has a valid proof can omit it. :::danger[Deleting a guild destroys its messages and attachments] -Deletion removes the guild, memberships, roles, channels, messages, attachments, invites, webhooks and discovery listing. There is no grace period or way to restore them through the API. +Deletion removes the guild, memberships, roles, channels, threads, messages, attachments, invites, webhooks and discovery listing. There is no grace period or way to restore them through the API. ::: ### Response @@ -840,6 +847,8 @@ Remaining guild sessions receive [Guild Member Remove](/gateway/events/#guild-me Guild read states and settings are unchanged, but the guild profile and roles are lost. Rejoining within one year resumes any unexpired communication timeout. Leaving records no audit entry, and `X-Audit-Log-Reason` has no effect. +Removal from the guild removes every membership in its threads. Fluxer emits a [Thread Members Update](/gateway/threads/#thread-members-update) for each of them, as [Thread member flags](/http-api/thread-members/#thread-member-flags) states. + ### Rate limit 10 requests per 10 seconds for each authenticated user and guild, on the `guild:leave::guild_id` bucket. @@ -850,7 +859,7 @@ Guild read states and settings are unchanged, but the guild profile and roles ar Deletes every message the authenticated account has authored in the guild and returns 202 with an empty body. Requires membership and sudo mode, which a bot credential satisfies implicitly. The membership survives the request. -The deletion runs inside this request, so the 202 is sent only after every matching message in every guild channel has been removed. Each processed batch emits [Message Delete Bulk](/gateway/events/#message-delete-bulk) in its channel. +The deletion runs inside this request. Fluxer sends the 202 only after it has removed every matching message in every channel and thread of the guild. Each processed batch emits [Message Delete Bulk](/gateway/events/#message-delete-bulk) in its channel or thread. ### Path parameters diff --git a/fluxer_docs/src/content/docs/http-api/index.md b/fluxer_docs/src/content/docs/http-api/index.md index dc29d108c..06fe8f155 100644 --- a/fluxer_docs/src/content/docs/http-api/index.md +++ b/fluxer_docs/src/content/docs/http-api/index.md @@ -14,7 +14,7 @@ Read resource limits from [instance discovery](/http-api/instance/#limit-keys). ## Request format -Send JSON bodies with `Content-Type: application/json`. Responses are UTF-8 JSON unless an operation states otherwise. Message operations and webhook execution also accept multipart bodies, and OAuth2 token operations accept form bodies. +Send JSON bodies with `Content-Type: application/json`. Responses are UTF-8 JSON unless an operation states otherwise. Message operations, [Start thread](/http-api/threads/#start-thread), and webhook execution also accept multipart bodies, and OAuth2 token operations accept form bodies. An overloaded instance returns 503 `SERVICE_UNAVAILABLE` with `Retry-After: 1`. Wait before retrying. @@ -41,6 +41,8 @@ Use `application/json` for JSON and `application/x-www-form-urlencoded` for OAut 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. +[Start thread](/http-api/threads/#start-thread) reads `payload_json` from a multipart body, and reads `files[n]` for a forum or media post. + Field-name failures are rejected with their own code: - An index outside either bound returns `FILE_INDEX_EXCEEDS_MAXIMUM`. @@ -72,7 +74,7 @@ This normalisation applies to JSON and form bodies, query strings, path paramete A nested object containing only `null` values also becomes `null`. The root object never becomes `null`, even when it is empty or has only `null` values. An empty body is treated as `{}` and validated for required fields. Malformed JSON returns 400 `INVALID_FORM_BODY` with a validation error at path `body` and code `INVALID_FORMAT`. :::caution[Message operations preserve empty values] -[Create message](/http-api/messages/#create-message), [Modify message](/http-api/messages/#modify-message), and [Execute webhook](/http-api/webhooks/#execute-webhook) do not apply this normalisation. +[Create message](/http-api/messages/#create-message), [Modify message](/http-api/messages/#modify-message), [Execute webhook](/http-api/webhooks/#execute-webhook), and [Start thread](/http-api/threads/#start-thread) do not apply this normalisation. ::: Those operations document their own JSON validation errors. @@ -81,7 +83,7 @@ Those operations document their own JSON validation errors. [Authentication](/authentication/) defines the accepted `Authorization` schemes and links to the OAuth2 scope registry. Each operation states which credentials it accepts. The [sudo verification object](/http-api/users/mfa/#sudo-verification-object) defines the proof required for sensitive account operations. -An OAuth2 bearer access token is accepted only where a route opts in, and the resource page says so. Everywhere else a bearer credential is refused with 403 `ACCESS_DENIED`. +An OAuth2 bearer access token is accepted only where a route opts in, and the resource page says so. Everywhere else a bearer credential is refused with 403 `ACCESS_DENIED`. The routes with a route header on [Threads](/http-api/threads/), [Thread members](/http-api/thread-members/), and [Forums](/http-api/forums/) return 404 `NOT_FOUND` to a bearer credential, as [Client capability](/http-api/threads/#client-capability) describes. ## Standard request headers @@ -284,6 +286,9 @@ Fluxer produces at most one entry for each distinct pair of `path` and `code`, s | [Memes](/http-api/memes/) | The saved image, video, and audio collection and batch GIF URL resolution | | [Themes](/http-api/themes/) | Shareable custom CSS theme creation | | [Channels](/http-api/channels/) | Channel objects, private recipients, permission overwrites, slowmode, RTC regions | +| [Threads](/http-api/threads/) | Thread objects, starting and archiving threads, archived thread lists, thread search, thread permissions | +| [Thread members](/http-api/thread-members/) | Joining and leaving threads, member lists, thread notification settings | +| [Forums](/http-api/forums/) | Forum and media channels, forum tags, post data, webhooks that post into a forum | | [Calls](/http-api/calls/) | Call eligibility, region selection, ringing, and termination | | [Streams](/http-api/streams/) | Go Live stream keys, stream regions, preview image lifecycle | | [Entrance sounds](/http-api/entrance-sounds/) | The entrance sound collection, its per-scope selections, and playback | diff --git a/fluxer_docs/src/content/docs/http-api/instance.mdx b/fluxer_docs/src/content/docs/http-api/instance.mdx index e900f8273..dc8bfb439 100644 --- a/fluxer_docs/src/content/docs/http-api/instance.mdx +++ b/fluxer_docs/src/content/docs/http-api/instance.mdx @@ -395,6 +395,8 @@ Each key names one limit. A key whose name begins with `feature_` is a feature g 2 Sticker limits are enforced as one shared total against `max_guild_stickers` +No key sets a thread or forum limit. [Thread lifecycle](/http-api/threads/#thread-lifecycle), [Thread members](/http-api/thread-members/), and [Forum channel fields](/http-api/forums/#forum-channel-fields) state those fixed bounds. `max_guild_channels` counts forum and media channels and counts no thread. + 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 diff --git a/fluxer_docs/src/content/docs/http-api/invites.mdx b/fluxer_docs/src/content/docs/http-api/invites.mdx index 72421a5fe..388819042 100644 --- a/fluxer_docs/src/content/docs/http-api/invites.mdx +++ b/fluxer_docs/src/content/docs/http-api/invites.mdx @@ -301,6 +301,7 @@ Creates an invite for a channel, or returns an existing equivalent invite. Retur - 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 also requires an age-verified account. - A private channel requires the caller to be a current recipient, and no ownership is required. +- A [thread](/http-api/threads/), a [forum channel, or a media channel](/http-api/forums/) creates no invite. A caller who can view the channel receives 400 `INVALID_CHANNEL_TYPE`. A user request without the [`channel_threads` client capability](/http-api/threads/#client-capability) receives 404 `UNKNOWN_CHANNEL`. A caller who lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, receives 403 `MISSING_ACCESS`. This operation does not require elevated multi-factor authentication. @@ -337,12 +338,15 @@ Every field is optional and nullable, and an omitted or null value takes the sta | --- | --- | --- | | 200 | [invite metadata](#invite-metadata-object) object | Invite was created, or an equivalent existing invite was returned | | 400 | [error response](/http-api/#error-response) | The guild already holds its maximum number of invites and the request returns `MAX_INVITES` | +| 400 | [error response](/http-api/#error-response) | Channel is a thread, a forum channel, or a media channel the caller can view and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | Caller presents a bearer credential and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The guild is unavailable to the caller and the request returns `MISSING_ACCESS` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | Instant invites are temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` | | 403 | [error response](/http-api/#error-response) | The caller is not a guild member or lacks [VIEW_CHANNEL](/http-api/permissions/) or [CREATE_INSTANT_INVITE](/http-api/permissions/) and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | The channel is age restricted and the account is not age verified and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, is pending deletion, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the channel's guild no longer exists and the request returns `UNKNOWN_GUILD` | +| 404 | [error response](/http-api/#error-response) | Channel is a thread, a forum channel, or a media channel and the caller is neither a bot nor a user session that declares the `channel_threads` client capability, returning `UNKNOWN_CHANNEL` | ### Side effects diff --git a/fluxer_docs/src/content/docs/http-api/memes.mdx b/fluxer_docs/src/content/docs/http-api/memes.mdx index 34e1b267a..a55d79b8b 100644 --- a/fluxer_docs/src/content/docs/http-api/memes.mdx +++ b/fluxer_docs/src/content/docs/http-api/memes.mdx @@ -232,6 +232,8 @@ The operation emits one [Favorite Meme Create](/gateway/events/#favorite-meme-cr Copies one attachment or one embed asset from a readable message into the caller's own collection and returns the created [meme object](#meme-object). Requires [VIEW_CHANNEL](/http-api/permissions/) in a guild channel. Emits a [Favorite Meme Create](/gateway/events/#favorite-meme-create) Gateway event. +A message in a thread follows [Channel resolution](/http-api/messages/#channel-resolution). + A guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) can select media only from a message on or after the guild's [message history cutoff](/http-api/messages/). Attachments and embeds in the message's forwarded snapshots are selectable alongside its own. ### Path parameters @@ -273,6 +275,7 @@ An embed selection uses its image, video, or thumbnail, in that order. Media who | 400 | [error response](/http-api/#error-response) | The collection is at its limit with `MAX_FAVORITE_MEMES` | | 403 | [error response](/http-api/#error-response) | Caller is a bot or presents a bearer credential and the request returns `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The caller is not a member of the guild or lacks `VIEW_CHANNEL` and the request returns `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | The caller cannot view the thread that holds the message and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The channel requires age verification the account has not completed and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel is unavailable or the caller is not a recipient of it with `UNKNOWN_CHANNEL` | | 404 | [error response](/http-api/#error-response) | The message is unavailable or unreadable with `UNKNOWN_MESSAGE` | diff --git a/fluxer_docs/src/content/docs/http-api/messages.mdx b/fluxer_docs/src/content/docs/http-api/messages.mdx index c86ca88d4..6897f21ae 100644 --- a/fluxer_docs/src/content/docs/http-api/messages.mdx +++ b/fluxer_docs/src/content/docs/http-api/messages.mdx @@ -8,15 +8,17 @@ 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/). -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 a time stored on the guild, 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, a guild link channel, a forum channel, and a media channel. A thread is text-bearing. The message history cutoff is a time stored on the guild, and a member without [READ_MESSAGE_HISTORY](/http-api/permissions/) reads nothing created before it. ## Channel resolution -Fluxer resolves the channel before it applies any per-route authorisation. A private channel returns 404 `UNKNOWN_CHANNEL` to a caller who is not a recipient. A guild channel returns 403 `MISSING_PERMISSIONS` to a non-member and to a member without [VIEW_CHANNEL](/http-api/permissions/). A guild an operator has marked unavailable returns 403 `MISSING_ACCESS` before the route runs. A guild that no Gateway process serves returns 404 `UNKNOWN_GUILD`, or 403 `ACCESS_DENIED` when its stored record survives. +Fluxer resolves the channel before it applies any per-route authorisation. A private channel returns 404 `UNKNOWN_CHANNEL` to a caller who is not a recipient. A guild channel returns 403 `MISSING_PERMISSIONS` to a non-member and to a member without [VIEW_CHANNEL](/http-api/permissions/). A thread returns 403 `MISSING_ACCESS` to a member who lacks `VIEW_CHANNEL` in its parent or cannot view a private thread. A guild an operator has marked unavailable returns 403 `MISSING_ACCESS` before the route runs. A guild that no Gateway process serves returns 404 `UNKNOWN_GUILD`, or 403 `ACCESS_DENIED` when its stored record survives. + +[Thread permissions](/http-api/threads/#thread-permissions) and [Thread lifecycle](/http-api/threads/#thread-lifecycle) state the permission, lock, and archive rules of every operation on this page in a thread. In a thread, each Gateway event an operation on this page emits reaches the sessions that [Dispatch events](/gateway/threads/#dispatch-events) lists. The message read, create, modify, and delete routes, [Bulk delete messages](#bulk-delete-messages), [Bulk delete own messages](#bulk-delete-own-messages), and [List pinned messages](#list-pinned-messages) also enforce age verification on an age restricted guild channel. A guild channel is age restricted when its own setting says so. A channel with no setting of its own uses its parent category's setting, and then the guild's. They return 403 `NSFW_CONTENT_AGE_RESTRICTED` until the account satisfies it. The attachment routes, the pin routes, the reaction routes, and [Indicate typing](#indicate-typing) resolve the channel without that check. -[Clear channel read state](#clear-channel-read-state) and [Acknowledge message](#acknowledge-message) resolve no channel. [Acknowledge pins](#acknowledge-pins) reads the channel without checking the caller's access. None of the three checks a permission. +[Clear channel read state](#clear-channel-read-state) resolves no channel. [Acknowledge message](#acknowledge-message) checks no channel access. For a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability) it reads the channel type to set the [read state flags](/http-api/read-states/#read-state-flags). [Acknowledge pins](#acknowledge-pins) reads the channel without checking the caller's access, except for a thread. Clear channel read state and Acknowledge message check no permission. Acknowledge pins checks none outside a thread. ## Message object @@ -51,6 +53,7 @@ A message object is the full stored form of one post in a channel. | nonce?6 | string | Caller-supplied message nonce, echoed to the sender as a string of 1 through 32 characters | | call? | [message call](#message-call-object) object | Call state attached to a call message | | referenced_message?7 | ?[message](#message-object) object | Resolved referenced message without a nested `referenced_message` field | +| thread?8 | [thread](/http-api/threads/#thread-object) object | The thread started from this message | 1 Built for a webhook-authored message and for a stored author ID that no longer resolves @@ -66,6 +69,10 @@ A message object is the full stored form of one post in a channel. 7 The key is absent when the message has no reply reference, present and null when the reply target no longer resolves, and present with the message when it does. It is always absent on a copy of a [published message](#crosspost-message). A client must tell absent apart from null by key presence +8 Present only on a message with `HAS_THREAD` whose thread still exists + +A `THREAD_STARTER_MESSAGE` also has `referenced_message`. It is the message the thread started from, or null once that message is deleted, as [Thread messages](/http-api/threads/#thread-messages) describes. + A webhook-authored message has no stored author user. Fluxer builds its author from the webhook, with the webhook ID as `id`, the stored webhook name as `username`, the discriminator `0000`, the stored webhook avatar hash as `avatar`, and `bot` true. A message whose stored author ID no longer resolves to an account is served with a deleted-user placeholder. The placeholder has that ID, the discriminator `0000`, and no avatar. `mentions`, `mention_roles`, `embeds`, `attachments`, and `stickers` are always present and can be empty. A field marked `?` above is omitted from the object when it has no value. `referenced_message` is the one optional field that can also be present and null. A message whose author user and webhook are both absent is omitted from every read. @@ -107,22 +114,28 @@ A reply reference resolves into `referenced_message`, and a forward has none. Fl | Value | Name | Description | | --- | --- | --- | | 0 | DEFAULT1 2 | Ordinary message | -| 1 | RECIPIENT_ADD | Group direct message recipient-add system message | -| 2 | RECIPIENT_REMOVE | Group direct message recipient-remove system message | +| 1 | RECIPIENT_ADD | Recipient-add system message of a group direct message or a thread | +| 2 | RECIPIENT_REMOVE | Recipient-remove system message of a group direct message or a thread | | 3 | CALL | Private call system message | -| 4 | CHANNEL_NAME_CHANGE | Group direct message name-change system message | +| 4 | CHANNEL_NAME_CHANGE | Name-change system message of a group direct message or a thread | | 5 | CHANNEL_ICON_CHANGE | Group direct message icon-change system message | | 6 | CHANNEL_PINNED_MESSAGE2 | Channel pin system message | | 7 | USER_JOIN2 | User-join system message | | 12 | CHANNEL_FOLLOW_ADD2 3 | System message posted when a channel starts following an announcement channel | +| 18 | THREAD_CREATED2 4 | System message in the parent that announces a new public or announcement thread | | 19 | REPLY1 2 | Message with a reply reference | +| 21 | THREAD_STARTER_MESSAGE4 | First message of a thread started from a message, built on read and never stored | -1 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` +1 Only `DEFAULT` and `REPLY` can be modified, pinned, unpinned, or used as the target of a reply reference. Any other stored 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` -2 Only `DEFAULT`, `CHANNEL_PINNED_MESSAGE`, `USER_JOIN`, `CHANNEL_FOLLOW_ADD`, and `REPLY` can be deleted, and any other type rejects deletion with 403 `MISSING_PERMISSIONS` +2 Only `DEFAULT`, `CHANNEL_PINNED_MESSAGE`, `USER_JOIN`, `CHANNEL_FOLLOW_ADD`, `THREAD_CREATED`, and `REPLY` can be deleted, and any other stored type rejects deletion with 403 `MISSING_PERMISSIONS` 3 Its `content` is the name of the new [channel follower webhook](/http-api/webhooks/#webhook-types). Its `message_reference` names the followed announcement channel and its guild, has `type` `0`, and has no `message_id`. It never sends a push notification +4 [Thread messages](/http-api/threads/#thread-messages) describes this type + +A caller other than a bot or a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability) never receives a `THREAD_CREATED` message or a `thread` field. That caller receives a thread flag only from a token-authenticated [webhook](/http-api/webhooks/) operation, which returns the stored flags. + ## Message flags | Value | Name | Description | @@ -131,6 +144,8 @@ A reply reference resolves into `referenced_message`, and a forward has none. Fl | 1 << 1 | IS_CROSSPOST3 | Message is a copy delivered from a followed announcement channel | | 1 << 2 | SUPPRESS_EMBEDS1 | Suppress rendering of embeds | | 1 << 3 | SOURCE_MESSAGE_DELETED3 | The published message this copy came from was deleted | +| 1 << 5 | HAS_THREAD4 | A thread was started from this message | +| 1 << 8 | FAILED_TO_MENTION_SOME_ROLES_IN_THREAD4 | Some members of a mentioned role were not added to the thread | | 1 << 12 | SUPPRESS_NOTIFICATIONS | Do not generate ordinary mention notifications | | 1 << 13 | VOICE_MESSAGE2 | Message has one voice recording attachment | @@ -140,6 +155,8 @@ A reply reference resolves into `referenced_message`, and a forward has none. Fl 3 Set by the server. [Announcement channels](/topics/announcement-channels/) describes when each is set +4 Set by the server. [Thread messages](/http-api/threads/#thread-messages) describes when each is set + `SUPPRESS_EMBEDS`, `SUPPRESS_NOTIFICATIONS`, and `VOICE_MESSAGE` are 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. Any other bit in the request is discarded, and the request still succeeds. ## Message attachment object @@ -288,7 +305,7 @@ Each message has at most the resolved `max_reactions_per_message` distinct group ## Message reference object -A message reference names the message that a reply points at, that a forward was taken from, or that a copy was published from. +A message reference names the message that a reply points at, that a forward was taken from, or that a copy was published from. A `THREAD_CREATED` message references its thread, and a `THREAD_STARTER_MESSAGE` references the message its thread started from, as [Thread messages](/http-api/threads/#thread-messages) describes. ### Structure @@ -299,13 +316,13 @@ A message reference names the message that a reply points at, that a forward was | guild_id? | snowflake | The ID of the referenced guild, present only for a guild message | | type | integer | [Message reference type](#message-reference-types) | -1 Absent only on a `CHANNEL_FOLLOW_ADD` [system message](#message-types), whose reference names a channel and no message +1 Absent on a `CHANNEL_FOLLOW_ADD` and on a `THREAD_CREATED` [system message](#message-types). Each of those references names a channel and no message ## Message reference types | Value | Name | Description | | --- | --- | --- | -| 0 | DEFAULT1 3 | Reply reference, or the source of a published copy | +| 0 | DEFAULT1 3 | Reply reference, the source of a published copy, or a thread reference | | 1 | FORWARD2 | Forward reference represented by snapshots | 1 The default when a create message request supplies `message_reference` without a `type` @@ -690,7 +707,7 @@ Fluxer issues an upload URL for an attachment before any message can reference i An `upload_url` can point to object storage or the Fluxer [upload relay](/media-proxy/upload-relay/). Send it exactly as issued without assuming its shape. -An upload key can be referenced only by a message that the same identity creates in the same channel. +An upload key can be referenced only by a message that the same identity creates in the same channel. An upload key planned for a forum or media channel is referenced by the first message of a post that [Start thread](/http-api/threads/#start-thread) creates in that channel. ### Attachment upload request item object @@ -895,6 +912,7 @@ A caller without `READ_MESSAGE_HISTORY` in a guild channel with no history cutof | Status | Body | Condition | | --- | --- | --- | | 200 | array[[message](#message-object) object] | Message window was returned | +| 400 | [error response](/http-api/#error-response) | Channel is a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` and the request returns `MISSING_PERMISSIONS`, or the caller lacks age verification for the channel and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist or the caller is not a recipient of the private channel | @@ -981,7 +999,7 @@ Plans from 1 through 10 attachment uploads. Returns a [singlepart](#singlepart-a ### Limitations -- The channel must support messages, and one that does not fails with 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`. +- The channel must be a text-bearing type, a forum channel, or a media channel. Any other channel fails with 400 `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL`. - A guild channel requires [SEND_MESSAGES](/http-api/permissions/) and [ATTACH_FILES](/http-api/permissions/), and a timed-out member is refused with 403 `COMMUNICATION_DISABLED`. Fluxer checks each declared `file_size` separately against the attachment size limit it resolves for the caller and the guild context. A declaration above that limit returns 400 `FILE_SIZE_TOO_LARGE` with the resolved ceiling. By default the resolved ceiling is 26214400 bytes (25 MiB) for a caller without premium and 524288000 bytes (500 MiB) for a caller with premium. A bot credential is clamped to 52428800 bytes, the 50 MiB bot ceiling, even when the resolved limit is higher. @@ -1142,6 +1160,7 @@ Creates a message from a JSON body or from multipart form data. Returns the crea - The channel must be a text-bearing type. - A guild channel requires [VIEW_CHANNEL](/http-api/permissions/) and [SEND_MESSAGES](/http-api/permissions/), an untimed-out membership, and a membership that meets the guild's [verification level](/http-api/guilds/#verification-levels). +- A thread requires [SEND_MESSAGES_IN_THREADS](/http-api/permissions/) in its parent where a guild channel requires `SEND_MESSAGES`. - A timed-out member is refused with 403 `COMMUNICATION_DISABLED`. - Embeds require [EMBED_LINKS](/http-api/permissions/), attachments require [ATTACH_FILES](/http-api/permissions/), a favourite meme requires both, and active everyone mentions require [MENTION_EVERYONE](/http-api/permissions/). - Slowmode applies to a non-bot caller unless they hold [BYPASS_SLOWMODE](/http-api/permissions/). @@ -1205,7 +1224,7 @@ 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 from a guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) fails with 404 `UNKNOWN_MESSAGE` when its target was created before the guild's message history cutoff, or when the guild has no cutoff. A forward has no cutoff check on 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 reply from a guild caller without [READ_MESSAGE_HISTORY](/http-api/permissions/) fails with 404 `UNKNOWN_MESSAGE` when its target was created before the guild's message history cutoff, or when the guild has no cutoff. A forward has no cutoff check on its source. A forward whose source is a `THREAD_CREATED` or `THREAD_STARTER_MESSAGE` message fails with the field code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`. A reply whose target is a system message fails with the field code `CANNOT_REPLY_TO_SYSTEM_MESSAGE`, and any other reference that resolves to no message fails with 404 `UNKNOWN_MESSAGE`. Every `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. @@ -1270,6 +1289,8 @@ The resolved `max_voice_message_duration` limit defaults to 1200 seconds. | 400 | [error response](/http-api/#error-response) | Body, multipart form, content, reference, mention, attachment, embed, sticker, or voice message contract is invalid | | 400 | [error response](/http-api/#error-response) | The recipient's direct message policy denies the send and the request returns `CANNOT_SEND_MESSAGES_TO_USER` | | 400 | [error response](/http-api/#error-response) | Slowmode denies the send and the request returns `SLOWMODE_RATE_LIMITED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | +| 400 | [error response](/http-api/#error-response) | The send would unarchive a thread while the guild has 1,000 active threads, returning `MAX_ACTIVE_THREADS` | | 403 | [error response](/http-api/#error-response) | Caller lacks a required permission and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | Is timed out and the request returns `COMMUNICATION_DISABLED` | | 403 | [error response](/http-api/#error-response) | Is age restricted from the channel and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | @@ -1292,6 +1313,8 @@ It advances the author's read state unless the author is a bot, reopens a direct It emits [Message Create](/gateway/events/#message-create) to every session that can see the channel. Mention and reply notifications follow the resolved allowed mentions policy. +In a thread the operation also unarchives an archived thread, as [Thread lifecycle](/http-api/threads/#thread-lifecycle) describes. It adds a user sender and the mentioned users as thread members, as [Thread messages](/http-api/threads/#thread-messages) describes. + It writes no audit log entry. ### Rate limit @@ -1361,6 +1384,8 @@ An `id` that names no attachment on the message is skipped, and the edit still s | --- | --- | --- | | 200 | [message](#message-object) object | Message was modified | | 400 | [error response](/http-api/#error-response) | Body, message type, attachment, embed, or resulting payload is invalid, or the author is editing a message that has snapshots and the request returns `MESSAGES_WITH_SNAPSHOTS_CANNOT_BE_EDITED` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller is not the author and cannot moderate the message and the request returns `CANNOT_EDIT_OTHER_USER_MESSAGE` | | 403 | [error response](/http-api/#error-response) | The caller supplied `message_snapshots` or lacks `EMBED_LINKS` or `ATTACH_FILES` and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | The caller is timed out and the request returns `COMMUNICATION_DISABLED` | @@ -1504,29 +1529,29 @@ Returns the public profile of the guild a copy of a published message came from, -Deletes the authenticated identity's read state entry for one channel. Returns 204 with an empty body on success. +The operation is deprecated and changes no read state. Returns 204 with an empty body. ### Limitations -- Fluxer does not resolve the channel, so the operation changes only the caller's own read state. -- The operation is idempotent, and a channel with no stored entry still returns 204. +- Fluxer never checks that the channel exists. An unknown channel ID returns 204. +- The read state entry of the channel stays as it was. ### Path parameters | Field | Type | Description | | --- | --- | --- | -| channel_id | snowflake | The ID of the channel whose read state is deleted | +| channel_id | snowflake | The ID of a channel | ### Response | Status | Body | Condition | | --- | --- | --- | -| 204 | empty | Read state was absent or deleted | +| 204 | empty | Request was accepted | | 403 | [error response](/http-api/#error-response) | Credential type denies the request and it returns `ACCESS_DENIED` | ### Side effects -The operation deletes the caller's read-through marker, mention count, and pin acknowledgement for the channel. It emits no Gateway Dispatch. +The operation changes no read state and emits no Gateway Dispatch. ### Rate limit @@ -1570,6 +1595,8 @@ There is no grace period and no restore operation. The operation permanently deletes the message, its reactions, and its attachments and removes it from search. +Deleting a message that started a thread keeps the thread, as [Thread messages](/http-api/threads/#thread-messages) describes. Deleting a message in a thread lowers the `message_count` of the thread, except for the first message of a forum or media post. + Deleting a [published](#crosspost-message) message keeps each copy of it in the following channels and turns it into a placeholder in the background. The content of the placeholder is `[Original message deleted]`, its attachments, embeds, and stickers are removed, its `edited_timestamp` is set, and it has the `SOURCE_MESSAGE_DELETED` [flag](#message-flags). Deleting a copy removes that copy alone. It emits [Message Delete](/gateway/events/#message-delete) to every session that can see the channel. When the message was pinned, it also removes the channel pin entry and emits [Channel Pins Update](/gateway/events/#channel-pins-update) with the channel's unchanged last-pin timestamp. @@ -1669,7 +1696,7 @@ An ID that names no message in the channel is skipped, so a partially matching r The operation permanently removes every selected message, attachment, and reaction and removes each message from search. -When at least one ID matched, it emits one [Message Delete Bulk](/gateway/events/#message-delete-bulk) with every requested ID to sessions that can see the channel, including an ID that matched nothing. It emits no [Channel Pins Update](/gateway/events/#channel-pins-update) and no individual [Message Delete](/gateway/events/#message-delete). +When at least one ID matched, it emits one [Message Delete Bulk](/gateway/events/#message-delete-bulk) with every requested ID to sessions that can see the channel, including an ID that matched nothing. In a thread the Dispatch has only the IDs that matched a message. It emits no [Channel Pins Update](/gateway/events/#channel-pins-update) and no individual [Message Delete](/gateway/events/#message-delete). It writes one `MESSAGE_BULK_DELETE` guild audit log entry with the matched count and the supplied reason. @@ -1735,6 +1762,7 @@ Deletes every message the caller authored in one channel. Returns 202 with an em - The caller must be able to resolve the channel and be in [sudo mode](/http-api/users/mfa/#sudo-mode), established by the `X-Fluxer-Sudo-Mode-JWT` header or by supplying a proof in the body. - A caller who satisfies none of those receives 403 `SUDO_MODE_REQUIRED` with `has_mfa` and the available `methods`, which report `totp`, `webauthn`, and `backup_codes`. - An account that holds no verifiable credential at all, meaning it is not a bot, has no TOTP secret, no registered WebAuthn credential, and no stored password, satisfies the sudo requirement with an empty body. +- A thread is a separate channel. The operation leaves the caller's messages in the threads of the channel, and a forum or media channel has no message of its own to delete. The deletion runs inside the request despite the 202 status, so every matching message is gone by the time the response is returned. @@ -1825,6 +1853,7 @@ The cursor has no message ID, so a client that pages with the `pinned_at` of the | --- | --- | --- | | 200 | [channel pins page](#channel-pins-page-object) object | Pin page was returned | | 400 | [error response](/http-api/#error-response) | Path or cursor is invalid, or the channel has no messages and the request returns `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL` | +| 400 | [error response](/http-api/#error-response) | Channel is a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` and the request returns `MISSING_PERMISSIONS`, or the caller lacks age verification for the channel and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist or the caller is not a recipient of the private channel | @@ -1865,8 +1894,11 @@ Pins a `DEFAULT` or `REPLY` message. Returns 204 with an empty body on success. | 400 | [error response](/http-api/#error-response) | The channel has no messages and the request returns `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL` | | 400 | [error response](/http-api/#error-response) | The target is a system message and the request returns `CANNOT_MODIFY_SYSTEM_WEBHOOK` | | 400 | [error response](/http-api/#error-response) | The private channel's send policy denies the caller and the request returns `CANNOT_SEND_MESSAGES_TO_USER` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `PIN_MESSAGES` and the request returns `MISSING_PERMISSIONS`, or message sending is temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` | | 403 | [error response](/http-api/#error-response) | The caller is under a new conversation limit in a direct message the recipient has not written in, and the request returns `NEW_CONVERSATIONS_LIMITED` | +| 403 | [error response](/http-api/#error-response) | Caller is timed out in a thread and the request returns `COMMUNICATION_DISABLED` | | 404 | [error response](/http-api/#error-response) | Channel or message does not exist, or the message is outside the caller's message history cutoff | ### Side effects @@ -1908,7 +1940,10 @@ Unpins a message. Returns 204 with an empty body on success. Emits [Message Upda | 400 | [error response](/http-api/#error-response) | The channel has no messages and the request returns `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL` | | 400 | [error response](/http-api/#error-response) | The target is a system message and the request returns `CANNOT_MODIFY_SYSTEM_WEBHOOK` | | 400 | [error response](/http-api/#error-response) | The private channel's send policy denies the caller and the request returns `CANNOT_SEND_MESSAGES_TO_USER` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `PIN_MESSAGES` and the request returns `MISSING_PERMISSIONS`, or message sending is temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` | +| 403 | [error response](/http-api/#error-response) | Caller is timed out in a thread and the request returns `COMMUNICATION_DISABLED` | | 404 | [error response](/http-api/#error-response) | Channel or message does not exist, or the message is outside the caller's message history cutoff | ### Side effects @@ -1939,11 +1974,15 @@ The request has no body. Fluxer acknowledges the channel's own stored last-pin t A channel the caller cannot see is acknowledged like any other whenever it stores a last-pin timestamp. ::: +A thread is the exception. Fluxer checks that the caller can view it, as [Channel resolution](#channel-resolution) describes. A forum or media channel returns 400 `INVALID_CHANNEL_TYPE`. A user request without the [`channel_threads` client capability](/http-api/threads/#client-capability) returns 204 for a thread, a forum channel, or a media channel and writes nothing. + ### Response | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Pin state was absent or acknowledged | +| 400 | [error response](/http-api/#error-response) | Channel is a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | Credential type denies the request and it returns `ACCESS_DENIED` | ### Side effects @@ -2098,6 +2137,8 @@ Adds the authenticated identity's reaction. Returns 204 with an empty body. Emit | 400 | [error response](/http-api/#error-response) | The custom emoji is unknown and the request returns `CUSTOM_EMOJI_NOT_FOUND` | | 400 | [error response](/http-api/#error-response) | The external emoji requires the entitlement and the request returns `CUSTOM_EMOJIS_REQUIRE_PREMIUM_OUTSIDE_SOURCE` | | 400 | [error response](/http-api/#error-response) | The message has reached a reaction ceiling and the request returns `MAX_REACTIONS` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL`, `ADD_REACTIONS`, or `USE_EXTERNAL_EMOJIS` and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | The caller is timed out and the request returns `COMMUNICATION_DISABLED` | | 403 | [error response](/http-api/#error-response) | The caller has an unverified email and the request returns `REACTION_EMAIL_VERIFICATION_REQUIRED` | @@ -2147,6 +2188,8 @@ Removes the authenticated identity's own reaction. Returns 204 with an empty bod | --- | --- | --- | | 204 | empty | Reaction was absent or removed | | 400 | [error response](/http-api/#error-response) | Path, emoji, or session input is invalid, or the channel has no messages and the request returns `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` and the request returns `MISSING_PERMISSIONS`, or reactions are temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, or the message is outside the caller's message history cutoff | @@ -2197,6 +2240,8 @@ Removes one named user's reaction. Returns 204 with an empty body. Emits a [Mess | --- | --- | --- | | 204 | empty | Target reaction was absent or removed | | 400 | [error response](/http-api/#error-response) | Path, emoji, or session input is invalid, or the channel has no messages and the request returns `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `MANAGE_MESSAGES`, or the channel is private, each returning `MISSING_PERMISSIONS`, or reactions are temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, or the message is outside the caller's message history cutoff | @@ -2236,6 +2281,8 @@ Removes every reaction for one emoji. Returns 204 with an empty body. Emits a [M | --- | --- | --- | | 204 | empty | Emoji reactions were absent or removed | | 400 | [error response](/http-api/#error-response) | Path or emoji is invalid, or the channel has no messages and the request returns `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `MANAGE_MESSAGES`, or the channel is private, each returning `MISSING_PERMISSIONS`, or reactions are temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, or the message is outside the caller's message history cutoff | @@ -2272,6 +2319,8 @@ Removes every reaction from a message. Returns 204 with an empty body. Emits a [ | --- | --- | --- | | 204 | empty | Reactions were absent or removed | | 400 | [error response](/http-api/#error-response) | Path parameters are invalid, or the channel has no messages and the request returns `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` or `MANAGE_MESSAGES`, or the channel is private, each returning `MISSING_PERMISSIONS`, or reactions are temporarily disabled for the guild and the request returns `FEATURE_TEMPORARILY_DISABLED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist, or the message is outside the caller's message history cutoff | @@ -2287,11 +2336,12 @@ Fluxer deletes every reaction on the message and emits one [Message Reaction Rem -Publishes typing activity in a text-bearing channel. Returns 204 with an empty body. Emits a [Typing Start](/gateway/events/#typing-start) Gateway event. +Publishes typing activity in a text-bearing channel. Returns 204 with an empty body, or 200 with the cooldown body below. Emits a [Typing Start](/gateway/events/#typing-start) Gateway event. ### Limitations - The caller must hold [SEND_MESSAGES](/http-api/permissions/) in a guild channel and must not be timed out. +- A thread requires [SEND_MESSAGES_IN_THREADS](/http-api/permissions/) in its parent where a guild channel requires `SEND_MESSAGES`. - A private channel caller needs only recipient access, because this route applies no direct message send policy. ### Path parameters @@ -2300,12 +2350,28 @@ Publishes typing activity in a text-bearing channel. Returns 204 with an empty b | --- | --- | --- | | channel_id | snowflake | The ID of the channel where typing is shown | +### Response body + +| Field | Type | Description | +| --- | --- | --- | +| message_send_cooldown_ms?1 | integer | The milliseconds until the channel slowmode accepts a message from the caller | +| thread_create_cooldown_ms?1 | integer | The milliseconds until the channel slowmode accepts a new thread from the caller | + +1 The OpenAPI schema does not declare a response body for this route + +`thread_create_cooldown_ms` is present only for a guild text or announcement channel. + +Fluxer returns 200 only to a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability), in a guild channel with slowmode, while a cooldown is running and the caller lacks `BYPASS_SLOWMODE`. + ### Response | Status | Body | Condition | | --- | --- | --- | +| 200 | cooldown body | Typing operation was accepted and a slowmode cooldown is running | | 204 | empty | Typing operation was accepted | | 400 | [error response](/http-api/#error-response) | Path parameter is invalid, or the channel has no messages and the request returns `CANNOT_SEND_MESSAGES_IN_NON_TEXT_CHANNEL` | +| 400 | [error response](/http-api/#error-response) | Channel is a forum or media channel and the request returns `INVALID_CHANNEL_TYPE` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Caller lacks `SEND_MESSAGES` and the request returns `MISSING_PERMISSIONS`, or the caller is timed out and the request returns `COMMUNICATION_DISABLED` | | 404 | [error response](/http-api/#error-response) | Channel does not exist or the caller is not a recipient of the private channel | @@ -2326,7 +2392,7 @@ Advances the authenticated user's read state through one message. Returns 204 wi ### Limitations - This is a user-only operation, and a bot credential is refused with 403 `ACCESS_DENIED`. -- Fluxer resolves neither the channel nor the message, so the operation writes the supplied identifiers straight into the caller's own read state and changes nothing else. +- Fluxer never checks that the channel or the message exists. The operation writes the supplied identifiers into the caller's own read state and changes nothing else. ### Path parameters diff --git a/fluxer_docs/src/content/docs/http-api/oauth2.mdx b/fluxer_docs/src/content/docs/http-api/oauth2.mdx index b03d77c2f..d5f27528a 100644 --- a/fluxer_docs/src/content/docs/http-api/oauth2.mdx +++ b/fluxer_docs/src/content/docs/http-api/oauth2.mdx @@ -380,6 +380,12 @@ Installing a bot into a guild requires [MANAGE_GUILD or ADMINISTRATOR](/http-api A guild installation emits [Guild Create](/gateway/events/#guild-create), [Guild Member Add](/gateway/events/#guild-member-add), and, unless the guild suppresses join notifications, [Message Create](/gateway/events/#message-create). When a non-zero permission mask is requested it also emits [Guild Role Create](/gateway/events/#guild-role-create) and [Guild Member Update](/gateway/events/#guild-member-update). A group direct message installation emits [Channel Create](/gateway/events/#channel-create) to the bot, [Channel Recipient Add](/gateway/events/#channel-recipient-add) to the existing recipients, and [Message Create](/gateway/events/#message-create). +### Request headers + +| Field | Type | Description | +| --- | --- | --- | +| X-Fluxer-Features? | string | A comma-separated [feature declaration](/http-api/permissions/#feature-gated-permission-bits) | + ### JSON body | Field | Type | Description | @@ -403,6 +409,8 @@ A guild installation emits [Guild Create](/gateway/events/#guild-create), [Guild 4 The fields are mutually exclusive +Fluxer clears the four thread permission bits from `permissions` unless the request declares the [`channel_threads` client capability](/http-api/threads/#client-capability). [Feature-gated permission bits](/http-api/permissions/#feature-gated-permission-bits) lists them. + ### Response | Status | Body | Condition | @@ -423,7 +431,7 @@ 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. 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 the filtered permission mask, names the role after the application, and assigns it to the bot. The installation either succeeds with exactly that mask or fails with a missing-permissions error. +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 the filtered permission mask, names the role after the application, and assigns it to the bot. The installation either succeeds with exactly that mask or fails with a missing-permissions error. The filter includes the thread bit rule above. 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. diff --git a/fluxer_docs/src/content/docs/http-api/permissions.mdx b/fluxer_docs/src/content/docs/http-api/permissions.mdx index 078a33323..1a3aaf66c 100644 --- a/fluxer_docs/src/content/docs/http-api/permissions.mdx +++ b/fluxer_docs/src/content/docs/http-api/permissions.mdx @@ -29,7 +29,7 @@ Every bit Fluxer defines appears below. | 1 << 8 | PRIORITY_SPEAKER | Use priority speaker in a guild voice channel, evaluated by the client | | 1 << 9 | STREAM2 | Publish a screenshare track on a guild voice connection | | 1 << 10 | VIEW_CHANNEL3 | View a guild channel | -| 1 << 11 | SEND_MESSAGES | Send a message in a guild text, announcement, or voice channel, and publish the caller's own announcement | +| 1 << 11 | SEND_MESSAGES | Send a message in a guild text, announcement, or voice channel, create a post in a forum or media channel, and publish the caller's own announcement | | 1 << 12 | SEND_TTS_MESSAGES | Send a message that requests text-to-speech playback | | 1 << 13 | MANAGE_MESSAGES4 | Delete another member's message, bulk delete messages, remove another member's reaction, and edit the flags and attachments of another member's message | | 1 << 14 | EMBED_LINKS | Have a link in the caller's own message expanded into an embed | @@ -48,11 +48,15 @@ Every bit Fluxer defines appears below. | 1 << 28 | MANAGE_ROLES5 | Create, modify, position, and delete a role, assign a role to a member, and set a channel permission overwrite | | 1 << 29 | MANAGE_WEBHOOKS | Create, modify, and delete a webhook on a guild channel, and follow an announcement channel into it | | 1 << 30 | MANAGE_EXPRESSIONS6 | Modify and delete an emoji or sticker created by another member | +| 1 << 34 | MANAGE_THREADS8 | Archive, lock, rename, and delete a thread, view every private thread, and act in a locked thread | +| 1 << 35 | CREATE_PUBLIC_THREADS8 | Start a public thread or an announcement thread | +| 1 << 36 | CREATE_PRIVATE_THREADS8 | Start a private thread in a guild text channel | | 1 << 37 | USE_EXTERNAL_STICKERS | Use a sticker owned by another guild | +| 1 << 38 | SEND_MESSAGES_IN_THREADS8 | Send a message in a thread, and add another member to a thread | | 1 << 40 | MODERATE_MEMBERS | Apply and clear a member communication timeout | | 1 << 43 | CREATE_EXPRESSIONS6 | Create an emoji or sticker, and modify or delete the ones the caller created | | 1 << 51 | PIN_MESSAGES | Pin and unpin a message in a guild channel | -| 1 << 52 | BYPASS_SLOWMODE | Send a message without waiting for the channel slowmode interval | +| 1 << 52 | BYPASS_SLOWMODE | Send a message, start a thread, or create a forum post without waiting for the channel slowmode interval | | 1 << 53 | UPDATE_RTC_REGION | Change the voice region of a guild voice channel | | 1 << 54 | VIEW_CHANNEL_MEMBERS7 | View the member list of a guild channel | @@ -70,7 +74,9 @@ Every bit Fluxer defines appears below. 7 The bit is [feature-gated on write](#feature-gated-permission-bits), so a request that does not declare the feature cannot change it -Bits 19, 31 through 36, 38, 39, 41, 42, 44 through 50, and 55 through 63 are unassigned. +8 The bit is [feature-gated](#feature-gated-permission-bits). [Thread permissions](/http-api/threads/#thread-permissions) describes how Fluxer evaluates it + +Bits 19, 31 through 33, 39, 41, 42, 44 through 50, and 55 through 63 are unassigned. :::note[`STREAM` authorises a track on the existing connection] The track is a screenshare or a camera. Fluxer opens no second connection for it, and losing the bit mutes the published tracks while the voice connection stays up. @@ -88,13 +94,15 @@ Fluxer discards a bit it does not define. A role mutation intersects the request 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`. +Two features gate bits. `VIEW_CHANNEL_MEMBERS` requires the feature name `view_channel_members_permission`. `MANAGE_THREADS`, `CREATE_PUBLIC_THREADS`, `CREATE_PRIVATE_THREADS`, and `SEND_MESSAGES_IN_THREADS` require the [`channel_threads` client capability](/http-api/threads/#client-capability) from a user session token. An OAuth2 bearer token cannot write them even when it declares the feature. A bot writes them without declaring the feature. -On a role or overwrite write that does not declare the matching feature, Fluxer keeps the gated bit at its stored value, so the request can neither set nor clear it. Every other bit in the same mask is written normally. +On a role or overwrite write by a caller that cannot write a gated bit, Fluxer keeps that bit at its stored value. Every other bit in the same mask is written normally. + +A caller other than a bot or a user session that declares `channel_threads` also receives role masks, overwrite masks, and guild `permissions` values without the four thread bits. `channels` on [Get guild](/http-api/guilds/#get-guild) is the exception. 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. -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. +On [Create guild role](#create-guild-role), a request that supplies `permissions` creates the role with `VIEW_CHANNEL_MEMBERS` cleared unless it declares `view_channel_members_permission`. It clears the four thread bits unless the caller is a bot or a user session that declares `channel_threads`. A request that omits `permissions` copies the everyone role mask without applying the gate. ## Announcement channel permissions @@ -106,7 +114,7 @@ Delivery rechecks access. While the account that created a follow cannot view th ## Elevated permissions -`KICK_MEMBERS`, `BAN_MEMBERS`, `ADMINISTRATOR`, `MANAGE_CHANNELS`, `MANAGE_GUILD`, `MANAGE_MESSAGES`, `MANAGE_ROLES`, `MANAGE_WEBHOOKS`, and `MODERATE_MEMBERS` are elevated permissions. In a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated, only the guild owner uses them without an enrolled authenticator. +`KICK_MEMBERS`, `BAN_MEMBERS`, `ADMINISTRATOR`, `MANAGE_CHANNELS`, `MANAGE_GUILD`, `MANAGE_MESSAGES`, `MANAGE_ROLES`, `MANAGE_WEBHOOKS`, `MODERATE_MEMBERS`, and `MANAGE_THREADS` are elevated permissions. In a guild whose [MFA level](/http-api/guilds/#mfa-levels) is elevated, only the guild owner uses them without an enrolled authenticator. An operation that asserts an elevated permission the caller holds but cannot use returns 400 `TWO_FACTOR_REQUIRED`. The check runs after Fluxer has confirmed the permission itself, so a caller who lacks the permission outright receives 403 `MISSING_PERMISSIONS` instead. @@ -138,10 +146,14 @@ No deny overwrite in any channel restricts a member who holds the bit. Removing A guild channel is visible when its channel-scoped mask contains `VIEW_CHANNEL`, or when the member holds temporary access through an in-flight voice or call transition. A category is also visible when the mask of at least one child channel contains `VIEW_CHANNEL`. Temporary access applies only to the channel it was granted on. +A thread stores no overwrites. [Thread permissions](/http-api/threads/#thread-permissions) describes how Fluxer derives its mask from the parent channel. + ## Communication timeout 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. +A timed-out member who holds `MANAGE_THREADS` without `ADMINISTRATOR` is not a [thread moderator](/http-api/threads/#thread-moderators) until the timeout ends. That member receives 403 `MISSING_ACCESS` for a private thread they have not joined. + While the timeout is in the future, these operations fail with 403 `COMMUNICATION_DISABLED`: - Sending a message @@ -151,8 +163,11 @@ While the timeout is in the future, these operations fail with 403 `COMMUNICATIO - Requesting an attachment upload - Starting a typing indicator - Changing the caller's own nickname +- Starting, changing, or deleting a thread +- Adding or removing a thread member +- Pinning or unpinning a message in a thread -A timeout has no effect in a private channel. +[Thread permissions](/http-api/threads/#thread-permissions) states the thread cases. 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`. @@ -313,7 +328,7 @@ Creates a role and returns its [guild role object](#guild-role-object). Requires | color? | integer | The colour of the role as a 24-bit RGB integer (0-16777215, default 0) | | permissions?1 | decimal string \| integer | The [permission](#permissions) bitfield the role grants | -1 An omitted field copies the current permissions of the everyone role unchanged. A supplied value is intersected with the set of defined bits, Fluxer clears `VIEW_CHANNEL_MEMBERS` unless the caller's [feature declaration](#feature-gated-permission-bits) includes `view_channel_members_permission`, then checks the value against the caller's own permissions +1 An omitted field copies the current permissions of the everyone role unchanged. A supplied value is intersected with the set of defined bits, Fluxer clears `VIEW_CHANNEL_MEMBERS` unless the caller's [feature declaration](#feature-gated-permission-bits) includes `view_channel_members_permission`, clears the four thread bits unless the caller is a bot or a user session that declares `channel_threads`, then checks the value against the caller's own permissions ### Response @@ -372,7 +387,7 @@ Every field is optional, and an omitted field leaves the stored value unchanged. 1 The field is accepted and then ignored when the target is the everyone role, which cannot be renamed, hoisted, given a member list position, or made mentionable -2 The supplied value becomes the complete stored mask. Before it checks the caller's authority, Fluxer intersects it with the set of defined bits and keeps each feature-gated bit at its stored value unless the caller's [feature declaration](#feature-gated-permission-bits) names the feature for that bit +2 The supplied value becomes the complete stored mask. Before it checks the caller's authority, Fluxer intersects it with the set of defined bits and keeps each feature-gated bit the caller cannot write at its stored value, as [Feature-gated permission bits](#feature-gated-permission-bits) describes ### Response diff --git a/fluxer_docs/src/content/docs/http-api/read-states.mdx b/fluxer_docs/src/content/docs/http-api/read-states.mdx index d351e28be..fa72a09f4 100644 --- a/fluxer_docs/src/content/docs/http-api/read-states.mdx +++ b/fluxer_docs/src/content/docs/http-api/read-states.mdx @@ -6,7 +6,7 @@ description: Current account message read states and the batch acknowledgement o import RouteHeader from '@/components/RouteHeader.astro'; -A read state records how far the current account has read in one channel. It holds the highest message read through, the mention count, and the last acknowledged pin timestamp. The [Messages resource](/http-api/messages/) defines single-channel acknowledgement, read state deletion, and pin acknowledgement. +A read state records how far the current account has read in one channel. It holds the highest message read through, the mention count, and the last acknowledged pin timestamp. The [Messages resource](/http-api/messages/) defines single-channel acknowledgement and pin acknowledgement. Both routes on this page are user-only. Fluxer rejects a bot or OAuth2 credential with 403 `ACCESS_DENIED`. @@ -23,6 +23,7 @@ An account holds at most one read state per channel, keyed by the account and th | last_message_id | ?snowflake | The highest message ID the account has read through, or null when the entry stores no watermark | | last_pin_timestamp2 | ?ISO8601 timestamp | The time of the last acknowledged pin, or null when pins have never been acknowledged | | version?3 | string | The read state version as a canonical decimal unsigned 64-bit string | +| flags?4 | integer | The [read state flags](#read-state-flags) | 1 The value the last acknowledgement stored, incremented by the server as new mentions arrive and never recomputed from message history @@ -30,15 +31,17 @@ An account holds at most one read state per channel, keyed by the account and th 3 Present on every entry the API returns. The value is always `0` -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, the snowflake of the channel ID itself, so every message already in the channel stays unread. +4 Present only on the entry of a thread, a forum channel, or a media channel, and set by Fluxer -[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. +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. Creating a post in a forum or media channel acknowledges that channel through the post ID for a user caller, as [Start thread](/http-api/threads/#start-thread) describes. A pin acknowledgement writes `last_pin_timestamp` and the flags that [Read state flags](#read-state-flags) describes, 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, the snowflake of the channel ID itself, or one below it for a thread. Every message already in the channel stays unread. + +[Clear channel read state](/http-api/messages/#clear-channel-read-state) is deprecated and changes no read state. :::note[The aggregate is delivered through the Gateway] 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). 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. +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 post a user creates in a forum or media channel also emits Message ACK for that channel. A server-side mention increment changes the stored entry and emits nothing. 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 @@ -52,6 +55,15 @@ Only an acknowledgement produces a later Dispatch. A message acknowledgement emi } ``` +## Read state flags + +| Value | Name | Description | +| --- | --- | --- | +| 1 << 0 | IS_GUILD_CHANNEL | The entry belongs to a guild channel | +| 1 << 1 | IS_THREAD | The entry belongs to a thread | + +Fluxer sets the flags on an entry written by a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability). The entry Fluxer creates for a mention in a thread also has both flags. A session that does not declare it never receives an entry that has flags. + ## Read state acknowledgement object One entry in the body of [Acknowledge read states](#acknowledge-read-states). It names the channel whose entry changes, the message the account has read through, and the mention count to store for the channel. @@ -65,7 +77,7 @@ One entry in the body of [Acknowledge read states](#acknowledge-read-states). It | mention_count?3 | integer | The number of mentions to store for the channel (0-2,147,483,647, default `0`) | | manual?4 | boolean | Whether the stored watermark becomes exactly `message_id` | -1 Fluxer never resolves the channel. It stores the value as the read state key with no check that the channel exists or that the account can see it +1 Fluxer stores the value as the read state key with no check that the channel exists or that the account can see it 2 Fluxer does not resolve the message either. An entry equal to the stored watermark still produces a write, so it resets the stored mention count @@ -106,7 +118,7 @@ One entry in the body of [Mark channels as read](#mark-channels-as-read). It nam Applies 1 through 100 [read state acknowledgement objects](#read-state-acknowledgement-object) to the current account's aggregate and returns the resulting [read state](#read-state-object) entries. User-only. Emits one [Message ACK](/gateway/events/#message-ack) Gateway event to the caller's own sessions for each submitted entry. -The route resolves no channel and evaluates no permission. The write changes only the caller's own read states. +The route never checks that a channel exists and evaluates no permission. The write changes only the caller's own read states. ### JSON body @@ -122,7 +134,7 @@ The route resolves no channel and evaluates no permission. The write changes onl | --- | --- | --- | | read_states1 | array[[read state](#read-state-object) object] | The authoritative entries after the write | -1 One entry for each submitted acknowledgement, in the submitted order. An entry whose watermark did not move reports the values already stored +1 One entry for each submitted acknowledgement, in the submitted order, less any entry with flags that the session cannot receive. An entry whose watermark did not move reports the values already stored When any entry is manual or has a positive mention count, entries for the same channel apply in submitted order. Otherwise, each entry is evaluated against the state before the request. @@ -139,6 +151,8 @@ With `manual` true, an entry sets the channel watermark to `message_id`. With `m One [Message ACK](/gateway/events/#message-ack) goes to the caller's sessions for every submitted entry, including an unchanged one. It reports the resulting `message_id`, `mention_count`, and `version`. It also includes the submitted `manual` value when any entry is manual or has a positive mention count. +A session without [thread access](/gateway/threads/#session-flag) receives no Message ACK for an entry that has `flags`. + When Fluxer fails to send a Message ACK, the request still succeeds and no Dispatch arrives for that entry. A client reconciles from the entries this route returns or from a new Ready. Every entry also clears delivered push notifications for its channel through the submitted `message_id`. Fluxer uses the submitted value even when it discards the entry and keeps a higher stored watermark. @@ -153,7 +167,7 @@ Every entry also clears delivered push notifications for its channel through the Applies 1 through 100 [bulk read state acknowledgement objects](#bulk-read-state-acknowledgement-object) and returns 204 with an empty body. User-only. Emits one [Message ACK](/gateway/events/#message-ack) Gateway event to the caller's own sessions for each submitted entry. -Every entry advances its channel watermark monotonically and sets the channel's stored mention count to `0`. An entry whose `message_id` is strictly below the stored watermark is skipped and leaves the stored entry untouched. Like [Acknowledge read states](#acknowledge-read-states), the operation resolves no channel and evaluates no permission. +Every entry advances its channel watermark monotonically and sets the channel's stored mention count to `0`. An entry whose `message_id` is strictly below the stored watermark is skipped and leaves the stored entry untouched. Like [Acknowledge read states](#acknowledge-read-states), the operation never checks that a channel exists and evaluates no permission. ### JSON body diff --git a/fluxer_docs/src/content/docs/http-api/reports.mdx b/fluxer_docs/src/content/docs/http-api/reports.mdx index 36799b375..37d01e9f3 100644 --- a/fluxer_docs/src/content/docs/http-api/reports.mdx +++ b/fluxer_docs/src/content/docs/http-api/reports.mdx @@ -583,6 +583,7 @@ A message a bot sent is reported as the bot account, like any other account. A m - A reporter who authored the message receives 400 `CANNOT_REPORT_OWN_MESSAGE`. - A channel that does not exist and a direct message the reporter is not a recipient of both return 404 `UNKNOWN_CHANNEL`. - A guild channel the reporter is not a member of returns 403 `ACCESS_DENIED`, and a member without [VIEW_CHANNEL](/http-api/permissions/) receives 403 `MISSING_PERMISSIONS`. +- A thread the reporter cannot view returns 403 `MISSING_ACCESS`, as [Channel resolution](/http-api/messages/#channel-resolution) describes. A thread, a forum channel, or a media channel returns 404 `UNKNOWN_CHANNEL` to a user request without the [`channel_threads` client capability](/http-api/threads/#client-capability). - An age-restricted guild channel returns 403 `NSFW_CONTENT_AGE_RESTRICTED` for an account that is not age verified. - A message not stored under `channel_id`, a message with neither an author account nor a webhook, and any message a reporter without [READ_MESSAGE_HISTORY](/http-api/permissions/) cannot reach all return 404 `UNKNOWN_MESSAGE`. - An author whose account no longer exists returns 404 `UNKNOWN_USER`. @@ -619,6 +620,7 @@ A reporter without `READ_MESSAGE_HISTORY` can report a message sent at or after | 403 | [error response](/http-api/#error-response) | Its email is unverified and the request returns `REPORT_EMAIL_VERIFICATION_REQUIRED` | | 403 | [error response](/http-api/#error-response) | The account is report banned and the request returns `REPORT_BANNED` | | 403 | [error response](/http-api/#error-response) | The reporter lacks `VIEW_CHANNEL` and the request returns `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | The reporter cannot view the thread and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The channel is age restricted and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | The channel, the message, or the message author is not visible to the reporter | | 409 | [error response](/http-api/#error-response) | The answers came from an outdated flow and do not fit the current one, returning `REPORT_FLOW_OUTDATED` | diff --git a/fluxer_docs/src/content/docs/http-api/search.mdx b/fluxer_docs/src/content/docs/http-api/search.mdx index c370cbe58..376778fc5 100644 --- a/fluxer_docs/src/content/docs/http-api/search.mdx +++ b/fluxer_docs/src/content/docs/http-api/search.mdx @@ -6,7 +6,7 @@ description: Global message search, its scope resolution, filter vocabulary, and import RouteHeader from '@/components/RouteHeader.astro'; -Message search matches indexed messages against a text query and a filter set. One request covers a single channel, one guild, the caller's private channels, or several of those at once. The [Guild member search resource](/http-api/guild-member-search/) defines guild member search, and the [Discovery resource](/http-api/discovery/) defines discoverable guild search. +Message search matches indexed messages against a text query and a filter set. One request covers a single channel, one guild, the caller's private channels, or several of those at once. The [Guild member search resource](/http-api/guild-member-search/) defines guild member search, and the [Discovery resource](/http-api/discovery/) defines discoverable guild search. [Search threads](/http-api/threads/#search-threads) defines searching the threads of one channel by name and tag. Fluxer resolves the requested scope to the set of channels the caller may search, then runs the query over that set. A private channel search covers the authenticated account alone, and the set holds a guild channel only while the caller can read its history. @@ -26,15 +26,19 @@ The body a completed search returns, holding one page of matching messages. | hits_per_page | integer | The page size that was applied, echoed from the request | | page | integer | The page number that was applied, echoed from the request | | cursor?4 | array[string] | The opaque cursor the backend produced, which no backend accepts back | +| threads?5 | array[[thread](/http-api/threads/#thread-object) object] | The threads that contain a returned message | +| members?5 | array[[thread member](/http-api/thread-members/#thread-member-object) object] | A thread member object for each of those threads the caller joined | 1 Each entry is an ordinary message object with the `referenced_message` field removed, so a reply is returned without its resolved parent even though the reply's own `message_reference` is preserved -2 Contains one entry for each distinct channel represented in `messages`, and never contains a channel with no returned message +2 Contains one entry for each distinct channel represented in `messages` that is not a thread, and never contains a channel with no returned message. A single channel context that names a thread returns that thread here as a channel object 3 Counted over the whole resolved scope. In a single channel context without [READ_MESSAGE_HISTORY](/http-api/permissions/), `total` is the number of messages in `messages` 4 Present only when the backend produced a cursor. A single channel context, a Meilisearch instance, and a page Fluxer rebuilt after it found deleted messages in the backend results each return none +5 Present only when a returned message is in a thread and the request is not a single channel context + [Search messages](#search-messages) never honours a supplied cursor, so page through a result set with `page` alone. Deleted messages are omitted. When using `page`, the total counts only existing messages. Supplying a `cursor` can leave shorter pages even when more results exist. @@ -166,6 +170,7 @@ Searches indexed messages in the resolved scope. Returns a [message search resul - A bot is restricted to the `current` scope. - The caller must be a recipient of every private channel that contributes to the search. - A guild scope includes a guild channel only when the caller holds both [VIEW_CHANNEL](/http-api/permissions/) and [READ_MESSAGE_HISTORY](/http-api/permissions/) in it. +- A guild scope includes threads only for a bot or a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability). - A single channel context requires guild membership and [VIEW_CHANNEL](/http-api/permissions/) alone. Without `READ_MESSAGE_HISTORY` the guild's message history cutoff applies to each returned message. - An age-restricted guild or single channel context requires an account old enough for age-restricted content, and otherwise returns 403 `NSFW_CONTENT_AGE_RESTRICTED`. - An instance with no search backend configured returns 403 `FEATURE_TEMPORARILY_DISABLED`. @@ -258,7 +263,11 @@ The backends read `min_id` and `max_id` differently. An Elasticsearch instance c The `current` scope requires one of `context_guild_id` and `context_channel_id`. Supplying neither returns 400 with the field code [CONTEXT_CHANNEL_OR_GUILD_ID_REQUIRED](/http-api/errors/) on the path `context`. -Fluxer resolves a supplied `context_guild_id` before anything else. A guild that cannot be resolved returns 404 `UNKNOWN_GUILD`, and a guild the caller is not a member of returns 403 `MISSING_PERMISSIONS`. A guild context searches that guild, restricted to `channel_ids` when supplied and to every channel of the guild otherwise, and every channel in the resulting set must belong to that guild. A channel the caller cannot both view and read history in is dropped when the set came from the guild, and rejected with 403 `MISSING_PERMISSIONS` when the caller named it in `channel_ids`. An age-gated channel that `include_nsfw` does not admit is removed from the set before the permission check runs, so it never causes that refusal. +Fluxer resolves a supplied `context_guild_id` before anything else. A guild that cannot be resolved returns 404 `UNKNOWN_GUILD`, and a guild the caller is not a member of returns 403 `MISSING_PERMISSIONS`. A guild context searches that guild, restricted to `channel_ids` when supplied and to every channel of the guild otherwise, and every channel in the resulting set must belong to that guild. A channel the caller cannot both view and read history in is dropped when the set came from the guild, and rejected with 403 `MISSING_PERMISSIONS` when the caller named it in `channel_ids`. An age-gated channel that `include_nsfw` does not admit is removed from the set before the permission check runs, so it never causes that refusal. A named thread whose parent was removed that way returns 403 `MISSING_PERMISSIONS` when another named channel remains and the search returns no [search indexing](#search-indexing-object) object. With no other channel left, the search returns an empty page. + +For a bot or a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability), a guild scope that `channel_ids` does not narrow also includes the threads of each text, announcement, forum, and media channel it includes. A public or announcement thread is included for every such caller. A private thread is included for its members and for [thread moderators](/http-api/threads/#thread-moderators). Fluxer includes at most 5,000 threads, highest ID first. Any other caller searches no thread. + +`channel_ids` can name a thread. In a guild context, naming a forum or media channel searches its posts, and naming a text or announcement channel searches that channel without its threads. In a guild context, a caller other than a bot or a user session that declares `channel_threads` who names a thread, a forum channel, or a media channel receives 400 with the field code `ALL_CHANNELS_MUST_BELONG_TO_GUILD`. A forum or media channel never returns the search indexing object in a guild scope. A named thread returns it when its parent text or announcement channel has never been indexed or its index is stale. A guild whose NSFW level is age restricted refuses a caller who is not old enough with 403 `NSFW_CONTENT_AGE_RESTRICTED`. An old-enough caller searches every channel of such a guild, including its age-gated channels, whether or not `include_nsfw` is set. diff --git a/fluxer_docs/src/content/docs/http-api/streams.mdx b/fluxer_docs/src/content/docs/http-api/streams.mdx index 3f71d13bd..1159088de 100644 --- a/fluxer_docs/src/content/docs/http-api/streams.mdx +++ b/fluxer_docs/src/content/docs/http-api/streams.mdx @@ -52,7 +52,7 @@ Mutation access requires everything read access does. A guild channel also requi A caller can record a region and upload a preview for any voice connection it holds, whether or not it is streaming. ::: -Fluxer resolves a guild channel before its guild. 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`. +Fluxer resolves a guild channel before its guild. 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`. A caller who lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, receives 403 `MISSING_ACCESS`. A thread, a forum channel, or a media channel returns 404 `UNKNOWN_CHANNEL` to a user session that does not declare the [`channel_threads` client capability](/http-api/threads/#client-capability). An unsatisfied age restriction on a resolved guild channel other than a category 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. @@ -129,6 +129,7 @@ A region identifier is the `id` of an [RTC region object](/http-api/channels/#rt | 400 | [error response](/http-api/#error-response) | The resolved channel is not a guild voice channel or a private channel and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | Caller holds no matching voice state or has an unresolvable guild membership state, each returning `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The caller lacks `VIEW_CHANNEL`, `CONNECT`, or `STREAM` and the request returns `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The resolved age restriction of a guild channel is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel named by the stream key does not exist, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the guild that owns the channel does not exist and the request returns `UNKNOWN_GUILD` | @@ -159,6 +160,7 @@ Returns the stored preview bytes for a stream. The caller must satisfy the [read | 200 | preview bytes | A stored preview exists | | 400 | [error response](/http-api/#error-response) | Stream key is malformed and the request returns `INVALID_STREAM_KEY_FORMAT`, its scope disagrees with the resolved channel and the request returns `STREAM_KEY_SCOPE_MISMATCH`, the resolved channel is not a guild voice channel or a private channel and the request returns `INVALID_CHANNEL_TYPE`, or its channel segment is not the canonical form of the resolved channel ID and the request returns `STREAM_KEY_CHANNEL_MISMATCH` | | 403 | [error response](/http-api/#error-response) | Caller has an unresolvable guild membership state and the request returns `ACCESS_DENIED`, the caller lacks `VIEW_CHANNEL` or `CONNECT` and the request returns `MISSING_PERMISSIONS`, or the resolved age restriction of a guild channel is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 404 | empty | No preview is stored for this stream key | | 404 | [error response](/http-api/#error-response) | Channel named by the stream key does not exist, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the guild that owns the channel does not exist and the request returns `UNKNOWN_GUILD` | @@ -222,6 +224,7 @@ A `thumbnail` that is not [canonical base64](/topics/uploads/#stream-previews) r | 400 | [error response](/http-api/#error-response) | It exceeds 1000000 bytes and the request returns `FILE_SIZE_TOO_LARGE` | | 403 | [error response](/http-api/#error-response) | Caller holds no matching voice state or has an unresolvable guild membership state, each returning `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The caller lacks `VIEW_CHANNEL`, `CONNECT`, or `STREAM` and the request returns `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The resolved age restriction of a guild channel is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel named by the body `channel_id` does not exist, or the caller is not a recipient of that private channel, each returning `UNKNOWN_CHANNEL`, or the guild that owns the channel does not exist and the request returns `UNKNOWN_GUILD` | @@ -270,6 +273,7 @@ Returns a [stream preview upload object](#stream-preview-upload-object) granting | 400 | [error response](/http-api/#error-response) | The declared media type is not JPEG and the request returns `PREVIEW_MUST_BE_JPEG` | | 403 | [error response](/http-api/#error-response) | Caller holds no matching voice state or has an unresolvable guild membership state, each returning `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The caller lacks `VIEW_CHANNEL`, `CONNECT`, or `STREAM` and the request returns `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The resolved age restriction of a guild channel is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel named by the body `channel_id` does not exist, or the caller is not a recipient of that private channel, each returning `UNKNOWN_CHANNEL`, or the guild that owns the channel does not exist and the request returns `UNKNOWN_GUILD` | @@ -310,6 +314,7 @@ Deletes the stored preview for a stream and returns 204 with an empty body. Dele | 400 | [error response](/http-api/#error-response) | The resolved channel is not a guild voice channel or a private channel and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | Caller holds no matching voice state or has an unresolvable guild membership state, each returning `ACCESS_DENIED` | | 403 | [error response](/http-api/#error-response) | The caller lacks `VIEW_CHANNEL`, `CONNECT`, or `STREAM` and the request returns `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | The resolved age restriction of a guild channel is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | | 404 | [error response](/http-api/#error-response) | Channel named by the stream key does not exist, or the caller is not a recipient of the private channel, each returning `UNKNOWN_CHANNEL`, or the guild that owns the channel does not exist and the request returns `UNKNOWN_GUILD` | diff --git a/fluxer_docs/src/content/docs/http-api/thread-members.mdx b/fluxer_docs/src/content/docs/http-api/thread-members.mdx index 5f2b5fed5..61c22702c 100644 --- a/fluxer_docs/src/content/docs/http-api/thread-members.mdx +++ b/fluxer_docs/src/content/docs/http-api/thread-members.mdx @@ -2,17 +2,18 @@ # SPDX-License-Identifier: AGPL-3.0-or-later title: Thread members description: Thread member objects, joining and leaving threads, member lists, and thread notification settings. -draft: true --- import RouteHeader from '@/components/RouteHeader.astro'; A thread member records that a user joined a [thread](/http-api/threads/). A thread holds at most 1,000 members, and adding one more returns 400 `MAX_THREAD_MEMBERS`. -:::note[Every route needs the `channel_threads` client capability] -A route on this page answers 404 `NOT_FOUND` to a user session that does not declare `channel_threads` in the `X-Fluxer-Features` header. A bot needs no declaration. +:::note[User requests need the `channel_threads` client capability] +A request from a user without the [`channel_threads` client capability](/http-api/threads/#client-capability) returns 404 `NOT_FOUND`. [Get thread member](#get-thread-member) and [List thread members](#list-thread-members) return 404 `NOT_FOUND` to every user. A `channel_id` that does not exist or names a channel outside a guild returns the same 404. ::: +A route on this page returns 403 `MISSING_PERMISSIONS` when the caller is not a member of the guild, unless its own entry states an earlier result. A guild channel that is not a thread and that the caller cannot view also returns 403 `MISSING_PERMISSIONS`. A thread whose parent channel no longer exists returns 404 `UNKNOWN_CHANNEL`. + ## Thread member object ### Structure @@ -27,19 +28,19 @@ A route on this page answers 404 `NOT_FOUND` to a user session that does not dec | mute_config?2 | ?[guild mute configuration](/http-api/users/settings/#guild-mute-configuration-object) object | The mute expiry of the thread, or null when none is stored | | member?3 | [guild member](/http-api/guild-members/#guild-member-object) object | The guild member of the user | -1 Omitted on the `member` of a thread in [Ready](/gateway/events/#ready) and [Guild Create](/gateway/events/#guild-create) +1 Omitted on the `member` of a thread in [Ready](/gateway/events/#ready), [Guild Create](/gateway/events/#guild-create), and [Guild Sync](/gateway/events/#guild-sync) 2 Present only on the caller's own membership -3 Present only when the request sets `with_member` +3 In an HTTP response, present only when the request sets `with_member` and the user is still a guild member. [Thread Members Update](/gateway/threads/#thread-members-update) defines it on each `added_members` entry ### Example ```json { - "id": "1501320000000000000", + "id": "1501320100000000000", "user_id": "1501314428688992222", - "join_timestamp": "2026-09-28T09:14:02.000Z", + "join_timestamp": "2026-05-05T20:30:05.018Z", "flags": 3, "muted": false, "mute_config": null @@ -55,17 +56,17 @@ A route on this page answers 404 `NOT_FOUND` to a user session that does not dec | 1 << 2 | ONLY_MENTIONS | Notify the user only of mentions in the thread | | 1 << 3 | NO_MESSAGES | Notify the user of nothing in the thread | -1 Set only by Fluxer +1 Set only by Fluxer. A bot that creates a thread starts without it At most one of the three notification flags is set. With none set, the thread follows the notification level of its parent. -A member keeps the membership after losing access to the parent. Leaving the guild removes every membership in its threads. +A member keeps the membership after losing access to the parent. Removal from the guild removes every membership in its threads. Fluxer emits a [Thread Members Update](/gateway/threads/#thread-members-update) for each of them. ## Join thread -Adds the caller to a thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/events/#thread-members-update) Gateway event. +Adds the caller to a thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/threads/#thread-members-update) Gateway event. ### Limitations @@ -73,7 +74,9 @@ Adds the caller to a thread. Returns 204 with no body. Emits a [Thread Members U - The thread must not be archived. - A locked thread requires a thread moderator. -Joining a thread the caller already belongs to returns 204 and changes nothing. +Joining a thread the caller already belongs to runs the checks above, then returns 204 and changes nothing. Each session of a newly added user receives [Thread Create](/gateway/threads/#thread-create) when it has [thread access](/gateway/threads/#session-flag) and can view the thread. A session still connecting to the guild receives none. + +Joining a locked thread needs an enrolled authenticator while the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, unless the caller owns the guild. ### Path parameters @@ -91,10 +94,12 @@ Joining a thread the caller already belongs to returns 204 and changes nothing. | Status | Body | Condition | | --- | --- | --- | -| 204 | None | Caller is a member | +| 204 | empty | Caller is a member | | 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` | -| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED`, or locked and the request returns `THREAD_LOCKED` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 400 | [error response](/http-api/#error-response) | Thread has 1,000 members and the request returns `MAX_THREAD_MEMBERS` | +| 400 | [error response](/http-api/#error-response) | Caller needs an authenticator and the request returns `TWO_FACTOR_REQUIRED` | | 403 | [error response](/http-api/#error-response) | Caller cannot view the thread and the request returns `MISSING_ACCESS` | ### Rate limit @@ -105,16 +110,19 @@ Joining a thread the caller already belongs to returns 204 and changes nothing. -Adds another guild member to a thread. Requires `SEND_MESSAGES_IN_THREADS`. Returns 204 with no body. Emits a [Thread Members Update](/gateway/events/#thread-members-update) Gateway event. +Adds another guild member to a thread. Requires `SEND_MESSAGES_IN_THREADS`. Returns 204 with no body. Emits a [Thread Members Update](/gateway/threads/#thread-members-update) Gateway event. ### Limitations +- The caller must be able to view the thread. - The thread must not be archived. - A locked thread requires a thread moderator. - A private thread with `invitable` false requires the caller or the target to be a thread moderator. - The target must be a guild member who can view the parent. -`user_id` of `@me` behaves as [Join thread](#join-thread). +`user_id` of `@me` behaves as [Join thread](#join-thread). A request that names the caller's own ID runs the checks above, and Fluxer posts no message for it. Adding a user who is already a member runs the checks above, then returns 204 and changes nothing. Each session of a newly added user receives [Thread Create](/gateway/threads/#thread-create) when it has [thread access](/gateway/threads/#session-flag) and can view the thread. A session still connecting to the guild receives none. + +Adding to a locked thread needs an enrolled authenticator while the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, unless the caller owns the guild. Adding a target who is not a thread moderator to a private thread with `invitable` false needs one too. ### Path parameters @@ -123,21 +131,32 @@ Adds another guild member to a thread. Requires `SEND_MESSAGES_IN_THREADS`. Retu | channel_id | snowflake | The ID of the thread | | user_id | snowflake | The ID of the user to add | +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| location? | string | A value Fluxer accepts and ignores (max 100 characters) | + ### Response | Status | Body | Condition | | --- | --- | --- | -| 204 | None | Target is a member | -| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED`, or locked and the request returns `THREAD_LOCKED` | +| 204 | empty | Target is a member | +| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` | +| 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 400 | [error response](/http-api/#error-response) | Thread has 1,000 members and the request returns `MAX_THREAD_MEMBERS` | -| 403 | [error response](/http-api/#error-response) | Caller lacks `SEND_MESSAGES_IN_THREADS`, or the thread is not `invitable`, each returning `MISSING_PERMISSIONS` | +| 400 | [error response](/http-api/#error-response) | Caller needs an authenticator and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller cannot view the thread and the request returns `MISSING_ACCESS` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `SEND_MESSAGES_IN_THREADS` and the request returns `MISSING_PERMISSIONS` | +| 403 | [error response](/http-api/#error-response) | Neither user is a thread moderator in a private thread that is not `invitable`, returning `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | Target cannot view the parent and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` | | 404 | [error response](/http-api/#error-response) | Target is not a guild member and the request returns `UNKNOWN_MEMBER` | ### Side effects -Adding another user posts a `RECIPIENT_ADD` message in the thread that mentions the target. +Adding another user posts a `RECIPIENT_ADD` message in the thread that mentions the target. Fluxer emits [Message Create](/gateway/events/#message-create) for it. ### Rate limit @@ -147,14 +166,30 @@ Adding another user posts a `RECIPIENT_ADD` message in the thread that mentions -Removes the caller from a thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/events/#thread-members-update) Gateway event. +Removes the caller from a thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/threads/#thread-members-update) Gateway event. + +A timed-out member can leave a thread. A member who can no longer view the thread receives 403 `MISSING_ACCESS`. + +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the thread | + +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| location? | string | A value Fluxer accepts and ignores (max 100 characters) | ### Response | Status | Body | Condition | | --- | --- | --- | -| 204 | None | Caller left the thread | +| 204 | empty | Caller left the thread | +| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` | | 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 403 | [error response](/http-api/#error-response) | Caller cannot view the thread and the request returns `MISSING_ACCESS` | | 404 | [error response](/http-api/#error-response) | Caller is not a member and the request returns `UNKNOWN_THREAD_MEMBER` | ### Rate limit @@ -165,23 +200,47 @@ Removes the caller from a thread. Returns 204 with no body. Emits a [Thread Memb -Removes a member from a thread. Requires `MANAGE_THREADS`, or ownership of a private thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/events/#thread-members-update) Gateway event. +Removes a member from a thread. Requires `MANAGE_THREADS`, or ownership of a private thread. Returns 204 with no body. Emits a [Thread Members Update](/gateway/threads/#thread-members-update) Gateway event. + +### Limitations + +- The caller must be able to view the thread. +- The thread must not be archived. +- The caller must be a [thread moderator](/http-api/threads/#thread-moderators) or the owner of the private thread. `user_id` of `@me` or of the caller behaves as [Leave thread](#leave-thread). +Removing a member needs an enrolled authenticator while the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, unless the caller owns the guild or the private thread. + +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the thread | +| user_id | snowflake | The ID of the user to remove | + +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| location? | string | A value Fluxer accepts and ignores (max 100 characters) | + ### Response | Status | Body | Condition | | --- | --- | --- | -| 204 | None | Member was removed | +| 204 | empty | Member was removed | +| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` | | 400 | [error response](/http-api/#error-response) | Thread is archived and the request returns `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | Caller needs an authenticator and the request returns `TWO_FACTOR_REQUIRED` | +| 403 | [error response](/http-api/#error-response) | Caller cannot view the thread and the request returns `MISSING_ACCESS` | | 403 | [error response](/http-api/#error-response) | Caller is neither a thread moderator nor the owner of the private thread and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` | | 404 | [error response](/http-api/#error-response) | Target is not a member and the request returns `UNKNOWN_THREAD_MEMBER` | ### Side effects -Removing another user posts a `RECIPIENT_REMOVE` message in the thread that mentions the target. +Removing another user posts a `RECIPIENT_REMOVE` message in the thread that mentions the target. Fluxer emits [Message Create](/gateway/events/#message-create) for it. ### Rate limit @@ -191,7 +250,14 @@ Removing another user posts a `RECIPIENT_REMOVE` message in the thread that ment -Returns the [thread member object](#thread-member-object) of a user. Bot-only. +Returns the [thread member object](#thread-member-object) of a user. Requires a bot token. + +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the thread | +| user_id | snowflake | The ID of the user | ### Query parameters @@ -204,6 +270,8 @@ Returns the [thread member object](#thread-member-object) of a user. Bot-only. | Status | Body | Condition | | --- | --- | --- | | 200 | [thread member](#thread-member-object) object | Member was returned | +| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` | +| 403 | [error response](/http-api/#error-response) | Caller cannot view the thread and the request returns `MISSING_ACCESS` | | 404 | [error response](/http-api/#error-response) | User is not a member and the request returns `UNKNOWN_THREAD_MEMBER` | | 404 | [error response](/http-api/#error-response) | Caller is a user and the request returns `NOT_FOUND` | @@ -215,10 +283,16 @@ Returns the [thread member object](#thread-member-object) of a user. Bot-only. -Returns the [thread member objects](#thread-member-object) of a thread in ascending user ID order. Bot-only. +Returns the [thread member objects](#thread-member-object) of a thread in ascending user ID order. Requires a bot token. A user client reads member lists through the `thread_member_lists` option of [Lazy Request](/gateway/commands/#lazy-request). +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the thread | + ### Query parameters | Field | Type | Description | @@ -232,6 +306,8 @@ A user client reads member lists through the `thread_member_lists` option of [La | Status | Body | Condition | | --- | --- | --- | | 200 | array[[thread member](#thread-member-object) object] | Members were returned | +| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` | +| 403 | [error response](/http-api/#error-response) | Caller cannot view the thread and the request returns `MISSING_ACCESS` | | 404 | [error response](/http-api/#error-response) | Caller is a user and the request returns `NOT_FOUND` | ### Rate limit @@ -242,7 +318,13 @@ A user client reads member lists through the `thread_member_lists` option of [La -Changes the caller's notification settings for a thread they joined. User-only. Returns the updated [thread member object](#thread-member-object), or 204 when nothing changed. Emits a [Thread Member Update](/gateway/events/#thread-member-update) Gateway event. +Changes the caller's notification settings for a thread they joined. User-only. Returns the updated [thread member object](#thread-member-object), or 204 when nothing changed. Emits a [Thread Member Update](/gateway/threads/#thread-member-update) Gateway event. + +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the thread | ### JSON body @@ -252,17 +334,20 @@ Changes the caller's notification settings for a thread they joined. User-only. | muted? | boolean | Whether the thread is muted | | mute_config? | ?[guild mute configuration input](/http-api/users/settings/#guild-mute-configuration-input-object) object | The mute expiry of the thread, or null to clear it | -1 Replaces the three notification flags and keeps `HAS_INTERACTED` +1 Replaces the three notification flags and keeps `HAS_INTERACTED`. A value of 0 clears them -A muted thread sends no push notification, including for a mention. +While `muted` is true, the member receives no push notification from the thread, including for a mention. A mute with an `end_time` stops when that time passes. ### Response | Status | Body | Condition | | --- | --- | --- | | 200 | [thread member](#thread-member-object) object | Settings changed | -| 204 | None | The body matched the stored settings | +| 204 | empty | The body matched the stored settings | | 400 | [error response](/http-api/#error-response) | `flags` sets more than one notification flag or any other bit and the request returns `INVALID_THREAD_NOTIFICATION_SETTINGS` | +| 400 | [error response](/http-api/#error-response) | Channel is not a thread and the request returns `INVALID_CHANNEL_TYPE` | +| 403 | [error response](/http-api/#error-response) | Caller is a bot and the request returns `ACCESS_DENIED` | +| 403 | [error response](/http-api/#error-response) | Caller cannot view the thread and the request returns `MISSING_ACCESS` | | 404 | [error response](/http-api/#error-response) | Caller is not a member and the request returns `UNKNOWN_THREAD_MEMBER` | ### Rate limit diff --git a/fluxer_docs/src/content/docs/http-api/threads.mdx b/fluxer_docs/src/content/docs/http-api/threads.mdx index 095cca131..e36a293bd 100644 --- a/fluxer_docs/src/content/docs/http-api/threads.mdx +++ b/fluxer_docs/src/content/docs/http-api/threads.mdx @@ -2,7 +2,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later title: Threads description: Thread objects, starting and archiving threads, archived thread lists, thread search, and thread permissions. -draft: true --- import RouteHeader from '@/components/RouteHeader.astro'; @@ -11,10 +10,12 @@ A thread is a channel inside a guild text, announcement, forum, or media channel [Thread members](/http-api/thread-members/) defines joining, leaving, and member lists. [Forums](/http-api/forums/) defines forum and media channels, where every thread is a post. -:::note[Every route needs the `channel_threads` client capability] -A route on this page answers 404 `NOT_FOUND` to a user session that does not declare `channel_threads` in the `X-Fluxer-Features` header. A bot needs no declaration. +:::note[User requests need the `channel_threads` client capability] +A user request without the `channel_threads` client capability gets 404 `NOT_FOUND` from every route with a route header on this page. A bot needs no declaration. [Client capability](#client-capability) has the details. ::: +[List active guild threads](#list-active-guild-threads) returns 404 `NOT_FOUND` to every user. [Modify thread](#modify-thread) and [Delete thread](#delete-thread) return 404 `UNKNOWN_CHANNEL` to a user without the capability. + ## Thread object A thread is a [channel object](/http-api/channels/#channel-object) with the thread fields below. @@ -26,7 +27,7 @@ A thread is a [channel object](/http-api/channels/#channel-object) with the thre | id1 | snowflake | The ID of the thread | | type | integer | The [thread type](#thread-types) | | guild_id | snowflake | The ID of the guild | -| parent_id | snowflake | The ID of the text, forum, or media channel the thread belongs to | +| parent_id | snowflake | The ID of the text, announcement, forum, or media channel the thread belongs to | | owner_id | snowflake | The ID of the user or webhook that created the thread | | name | string | The name of the thread (1-100 characters) | | last_message_id | ?snowflake | The ID of the most recent message, or null when the thread has none | @@ -36,32 +37,30 @@ A thread is a [channel object](/http-api/channels/#channel-object) with the thre | thread_metadata | [thread metadata](#thread-metadata-object) object | The archive and lock state of the thread | | message_count3 | integer | The number of messages in the thread | | total_message_sent4 | integer | The number of messages ever sent in the thread | -| member_count5 | integer | The number of thread members, capped at 50 | -| applied_tags?6 | array[snowflake] | The IDs of the [forum tags](/http-api/forums/#forum-tag-object) applied to a post (max 5) | -| member_ids_preview?6 | array[snowflake] | The IDs of the most recently joined members of a post, newest first (max 8) | -| member?7 | [thread member](/http-api/thread-members/#thread-member-object) object | The thread member object of the caller | +| member_count | integer | The number of thread members, capped at 50 | +| applied_tags?5 | array[snowflake] | The IDs of the [forum tags](/http-api/forums/#forum-tag-object) applied to a post (max 5) | +| member_ids_preview?5 | array[snowflake] | The IDs of the most recently joined members of a post, newest first (max 8) | +| member?6 | [thread member](/http-api/thread-members/#thread-member-object) object | The thread member object of the caller | -1 A thread started from a message has the ID of that message. A forum post has the ID of its first message +1 A thread started from a message has the ID of that message. The first message of a forum post has the ID of the post 2 Only `PINNED` is ever set, and only on a forum or media post -3 Excludes the first message and every deleted message +3 Excludes the first message of a forum post and every deleted message -4 Includes deleted messages and never decreases +4 Excludes the first message of a forum post. Deleting a message does not lower it -5 The value stays at 50 when the thread has more members +5 Present only on a thread whose parent is a forum or media channel. A Gateway payload has no `member_ids_preview` -6 Present only on a thread whose parent is a forum or media channel +6 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) -7 Present only on [Get channel](/http-api/channels/#get-channel), on the `threads` list of a guild in [Ready](/gateway/events/#ready) and [Guild Create](/gateway/events/#guild-create), and on `thread` of a message, each when the caller is a member - -A thread has no `position`, no `permission_overwrites`, and no `nsfw` fields. It 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`, `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 ```json { - "id": "1501320000000000000", + "id": "1501320100000000000", "type": 11, "guild_id": "1501314428688990000", "parent_id": "1501314428688998182", @@ -74,9 +73,9 @@ A thread has no `position`, no `permission_overwrites`, and no `nsfw` fields. It "thread_metadata": { "archived": false, "auto_archive_duration": 4320, - "archive_timestamp": "2026-09-28T09:14:02.000Z", + "archive_timestamp": "2026-05-05T20:30:05.018Z", "locked": false, - "create_timestamp": "2026-09-28T09:14:02.000Z" + "create_timestamp": "2026-05-05T20:30:05.018Z" }, "message_count": 12, "total_message_sent": 14, @@ -94,12 +93,12 @@ The archive and lock state of a thread. | --- | --- | --- | | archived | boolean | Whether the thread is archived | | auto_archive_duration | integer | The [auto archive duration](#auto-archive-durations) in minutes | -| archive_timestamp1 | ISO8601 timestamp | The time the archive state last changed | +| archive_timestamp1 | ISO8601 timestamp | The time the thread was last archived, unarchived, or given a new auto archive duration | | locked | boolean | Whether only thread moderators can act in the thread | -| invitable?2 | boolean | Whether a member who is not a thread moderator can add other members | +| invitable?2 | boolean | Whether a member who is not a thread moderator can add members who are not thread moderators | | create_timestamp | ISO8601 timestamp | The time the thread was created | -1 Equal to `create_timestamp` until the thread is first archived or its duration changes +1 Equal to `create_timestamp` until the first of those changes 2 Present only on a private thread @@ -113,9 +112,9 @@ The archive and lock state of a thread. A forum or media post is always `PUBLIC_THREAD`. Every thread in an announcement channel is `ANNOUNCEMENT_THREAD`, and an announcement channel holds no private threads. -Converting a guild text channel into an announcement channel turns each of its public threads into an announcement thread, and converting it back turns them into public threads again. Each active thread emits [Thread Update](/gateway/events/#thread-update) with the new `type`. A guild text channel with a private thread cannot become an announcement channel, and the request returns 400 `CHANNEL_HAS_THREADS`. +Converting a guild text channel into an announcement channel turns each of its public threads into an announcement thread. Converting it back turns them into public threads again. Fluxer emits [Thread Update](/gateway/threads/#thread-update) with the new `type` for each active thread. A guild text channel with a private thread cannot become an announcement channel, and the request returns 400 `CHANNEL_HAS_THREADS`. -A message in an announcement thread cannot be published to following channels, and a follow never delivers it. Publishing the message a thread started from works as for any other announcement, and the copies carry neither `HAS_THREAD` nor `thread`. +A message in an announcement thread cannot be published to following channels. Publishing returns 400 `ANNOUNCEMENT_CHANNEL_REQUIRED`. Publishing the message a thread started from works as for any other announcement. A copy delivered to a following channel starts with neither `HAS_THREAD` nor `thread`. ## Auto archive durations @@ -126,15 +125,15 @@ A message in an announcement thread cannot be published to following channels, a | 4320 | Three days, the default | | 10080 | One week | -A thread inherits the `default_auto_archive_duration` of its parent when the create request omits the field, and uses 4320 when the parent has none. +A create request that omits `auto_archive_duration` gets 4320. Fluxer does not apply the `default_auto_archive_duration` of the parent. ## Thread lifecycle -Fluxer archives an active thread once no message has arrived for its `auto_archive_duration`. A pinned forum post is never archived automatically. Archiving a pinned post clears its `PINNED` flag. +Fluxer archives an active thread once `auto_archive_duration` minutes pass after the later of its last message and its `archive_timestamp`. It then emits [Thread Update](/gateway/threads/#thread-update). A pinned forum post is never archived automatically. Archiving a pinned post clears its `PINNED` flag. A guild holds at most 1,000 active threads. Creating or unarchiving a thread past that count returns 400 `MAX_ACTIVE_THREADS`. -Sending a message to an archived thread that is not locked unarchives it. Fluxer emits [Thread Update](/gateway/events/#thread-update) before the message. A locked thread accepts messages, reactions, and member changes only from a thread moderator. +Sending a message to an archived thread unarchives it. Fluxer emits [Thread Update](/gateway/threads/#thread-update) before the message. A locked thread returns 400 `THREAD_LOCKED` to a caller who is not a thread moderator for a message, a reaction, a pin, an edit, a join, or an added member. An archived thread accepts no reaction, pin, edit, join, leave, or member change, and each returns 400 `THREAD_ARCHIVED`. Deleting a message in an archived thread is allowed. @@ -146,18 +145,16 @@ A thread moderator is the guild owner, a member with `ADMINISTRATOR`, or a membe A thread has no permission overwrites of its own. Fluxer computes permissions in the parent channel and then replaces `SEND_MESSAGES` with `SEND_MESSAGES_IN_THREADS`. Posting in a forum or media channel itself uses the unreplaced `SEND_MESSAGES` of that channel. -| Value | Name | Description | -| --- | --- | --- | -| 1 << 34 | MANAGE_THREADS1 | Archive, lock, rename, and delete a thread, and act in a locked thread | -| 1 << 35 | CREATE_PUBLIC_THREADS | Start a public thread in a guild text channel | -| 1 << 36 | CREATE_PRIVATE_THREADS | Start a private thread in a guild text channel | -| 1 << 38 | SEND_MESSAGES_IN_THREADS | Send a message in a thread, and add another member to a thread | +[Permissions](/http-api/permissions/#permissions) lists the four thread bits, `MANAGE_THREADS`, `CREATE_PUBLIC_THREADS`, `CREATE_PRIVATE_THREADS`, and `SEND_MESSAGES_IN_THREADS`. `MANAGE_THREADS` is an [elevated permission](/http-api/permissions/#elevated-permissions). `ADMINISTRATOR` grants all four bits. The four bits are [feature-gated](/http-api/permissions/#feature-gated-permission-bits). -1 An [elevated permission](/http-api/permissions/#elevated-permissions). The thread owner and member alternatives below are checked first, so a thread owner acting on their own thread needs no authenticator +These actions need an enrolled authenticator while the guild [MFA level](/http-api/guilds/#mfa-levels) is elevated, unless the caller owns the guild. Without one, each returns 400 `TWO_FACTOR_REQUIRED`. -`ADMINISTRATOR` grants all four bits. A role or overwrite write keeps the stored value of the four bits unless the caller can view threads in the guild. +- [Delete thread](#delete-thread). +- [Modify thread](#modify-thread) with a change to `locked`, `rate_limit_per_user`, or `flags` that needs a thread moderator, or with unarchiving a locked thread. +- [Modify thread](#modify-thread) with a change to `name`, `auto_archive_duration`, or `invitable`, or with archiving, by a caller who is not the thread owner. +- [Join thread](/http-api/thread-members/#join-thread), [Add thread member](/http-api/thread-members/#add-thread-member), and [Remove thread member](/http-api/thread-members/#remove-thread-member) in the cases those entries state. -A timed out member can read a thread and can join or leave one. Every other thread write from a timed out member who is neither the guild owner nor an administrator returns 403 `COMMUNICATION_DISABLED`. +A timed-out member can read a thread and can join or leave one. A timed-out member who is neither the guild owner nor an administrator receives 403 `COMMUNICATION_DISABLED` for starting, modifying, or deleting a thread. The same applies to adding or removing a thread member, sending a message, adding a reaction, pinning, and editing their own message in a thread. A caller who can view the parent but is neither a member nor a thread moderator receives 403 `MISSING_ACCESS` for a private thread. Losing access to the parent keeps the membership, and the member receives no event from the thread until access returns. @@ -165,7 +162,7 @@ A caller who can view the parent but is neither a member nor a thread moderator -Starts a public thread from a message in a guild text channel, or an announcement thread from a message in an announcement channel. Requires `CREATE_PUBLIC_THREADS` and `READ_MESSAGE_HISTORY`. Returns the [thread object](#thread-object), which extends the [channel object](/http-api/channels/#channel-object). Emits a [Thread Create](/gateway/events/#thread-create) Gateway event. +Starts a public thread from a message in a guild text channel, or an announcement thread from a message in an announcement channel. Requires `CREATE_PUBLIC_THREADS` and `READ_MESSAGE_HISTORY`. Returns a [thread object](#thread-object) on success. Emits a [Thread Create](/gateway/threads/#thread-create) Gateway event. ### Limitations @@ -185,7 +182,7 @@ Starts a public thread from a message in a guild text channel, or an announcemen | Field | Type | Description | | --- | --- | --- | | name | string | The name of the thread (1-100 characters) | -| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes | +| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes (default 4320) | | rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) | | location? | string | A value Fluxer accepts and ignores (max 100 characters) | @@ -203,14 +200,16 @@ Starts a public thread from a message in a guild text channel, or an announcemen | 400 | [error response](/http-api/#error-response) | Parent slowmode denies the thread and the request returns `SLOWMODE_RATE_LIMITED` | | 403 | [error response](/http-api/#error-response) | Caller lacks a required permission and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` | -| 404 | [error response](/http-api/#error-response) | Channel does not exist and the request returns `UNKNOWN_CHANNEL` | +| 403 | [error response](/http-api/#error-response) | Caller account is limited and the request returns `ACCOUNT_LIMITED` | +| 403 | [error response](/http-api/#error-response) | The resolved age restriction is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or is outside a guild, and the request returns `NOT_FOUND` | | 404 | [error response](/http-api/#error-response) | Message does not exist and the request returns `UNKNOWN_MESSAGE` | ### Side effects - Fluxer sets `HAS_THREAD` on the message and emits [Message Update](/gateway/events/#message-update) with `thread`. -- The thread opens with a `THREAD_STARTER_MESSAGE`, which has the thread ID and references the source message. -- A source message at least five minutes old also produces a `THREAD_CREATED` message in the parent. +- Fluxer emits [Message Create](/gateway/events/#message-create) for the `THREAD_STARTER_MESSAGE` of the thread. +- A source message outside the five most recent messages of the parent also produces a `THREAD_CREATED` message in the parent. - The caller becomes the first thread member. Starting a thread uses the parent slowmode on its own counter. A bot and a member holding `BYPASS_SLOWMODE` are exempt. @@ -223,7 +222,7 @@ Starting a thread uses the parent slowmode on its own counter. A bot and a membe -Starts a thread with no source message, or creates a post in a forum or media channel. Returns the [thread object](#thread-object), which extends the [channel object](/http-api/channels/#channel-object). Emits a [Thread Create](/gateway/events/#thread-create) Gateway event. +Starts a thread with no source message, or creates a post in a forum or media channel. Returns a [thread object](#thread-object), or a [forum post object](/http-api/forums/#forum-post-object) for a post, on success. Emits a [Thread Create](/gateway/threads/#thread-create) Gateway event. ### Limitations @@ -240,7 +239,9 @@ Starts a thread with no source message, or creates a post in a forum or media ch ### JSON body -The parent type selects the body. A forum or media channel also accepts multipart form data with the JSON in a `payload_json` part and files in `files[n]` parts, as [Create message](/http-api/messages/#create-message) describes. +The parent type selects the body. Either body can be sent as multipart form data with the JSON in a `payload_json` part. A forum or media post can also attach files in `files[n]` parts, as [Create message](/http-api/messages/#create-message) describes. + +`rate_limit_per_user` defaults to the `default_thread_rate_limit_per_user` of the parent, or `0` when the parent has none. #### Text and announcement channel body @@ -248,12 +249,12 @@ The parent type selects the body. A forum or media channel also accepts multipar | --- | --- | --- | | name | string | The name of the thread (1-100 characters) | | type | integer | The [thread type](#thread-types) to start | -| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes | +| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes (default 4320) | | rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) | -| invitable?1 | boolean | Whether a member who is not a thread moderator can add other members (default true) | +| invitable?1 | boolean | Whether a member who is not a thread moderator can add members who are not thread moderators (default true) | | location? | string | A value Fluxer accepts and ignores (max 100 characters) | -1 Read only for a private thread +1 Applies only to a private thread #### Forum post body @@ -262,9 +263,9 @@ The parent type selects the body. A forum or media channel also accepts multipar | name | string | The name of the post (1-100 characters) | | message | object | The first message, with the fields of [Create message](/http-api/messages/#create-message) other than `message_reference`, `nonce`, `tts`, and `favorite_meme_id` | | applied_tags?1 | array[snowflake] | The IDs of the forum tags to apply (max 5) | -| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes | +| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes (default 4320) | | rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) | -| type?2 | integer | The thread type, which is always `11` | +| type?2 | integer | The thread type (`11` only) | | location? | string | A value Fluxer accepts and ignores (max 100 characters) | 1 Required when the channel has `REQUIRE_TAG`. A tag marked `moderated` needs a thread moderator @@ -281,17 +282,21 @@ The parent type selects the body. A forum or media channel also accepts multipar | 400 | [error response](/http-api/#error-response) | Guild has 1,000 active threads and the request returns `MAX_ACTIVE_THREADS` | | 400 | [error response](/http-api/#error-response) | Parent slowmode denies the thread and the request returns `SLOWMODE_RATE_LIMITED` | | 400 | [error response](/http-api/#error-response) | A forum post names no tag under `REQUIRE_TAG` and the request returns `FORUM_TAG_REQUIRED` | -| 400 | [error response](/http-api/#error-response) | As above while every tag is moderated, returning `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` | +| 400 | [error response](/http-api/#error-response) | A forum post names no tag, with `REQUIRE_TAG` and no unmoderated tag, returning `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` | | 403 | [error response](/http-api/#error-response) | Caller lacks a required permission or applies a moderated tag, returning `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` | -| 404 | [error response](/http-api/#error-response) | Channel does not exist and the request returns `UNKNOWN_CHANNEL` | +| 403 | [error response](/http-api/#error-response) | Caller account is limited and the request returns `ACCOUNT_LIMITED` | +| 403 | [error response](/http-api/#error-response) | The resolved age restriction is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or is outside a guild, and the request returns `NOT_FOUND` | | 404 | [error response](/http-api/#error-response) | A tag does not exist and the request returns `UNKNOWN_FORUM_TAG` | +In a channel with `REQUIRE_TAG` and no unmoderated tag, a caller who is not a thread moderator receives `NO_TAGS_AVAILABLE_TO_NON_MODERATORS`, and a thread moderator receives `FORUM_TAG_REQUIRED`. [Creating and modifying a forum](/http-api/forums/#creating-and-modifying-a-forum) states how a channel reaches that state. + ### Side effects -- A public thread in a guild text channel and an announcement thread post a `THREAD_CREATED` message in the parent. A private thread posts none. +- A public thread in a guild text channel and an announcement thread post a `THREAD_CREATED` message in the parent. A private thread and a forum post have none. - The first message of a forum post has the ID of the post, and a failed first message deletes the post again. -- Creating a forum post marks the forum as read for the caller. +- Creating a forum post marks the forum as read for a user caller. - The caller becomes the first thread member. ### Rate limit @@ -300,7 +305,7 @@ The parent type selects the body. A forum or media channel also accepts multipar ## Modify thread -[Modify channel](/http-api/channels/#modify-channel) on a thread takes the thread body below and returns the updated [thread object](#thread-object). It emits a [Thread Update](/gateway/events/#thread-update) Gateway event. +[Modify channel](/http-api/channels/#modify-channel) on a thread takes the thread body below and returns the updated [thread object](#thread-object). It emits a [Thread Update](/gateway/threads/#thread-update) Gateway event when a field changes. ### Limitations @@ -308,7 +313,7 @@ The parent type selects the body. A forum or media channel also accepts multipar - `locked` needs a thread moderator, and the thread owner can set it to true. - `rate_limit_per_user` and `flags` need a thread moderator. - `invitable` needs the thread owner or a thread moderator. -- Unarchiving needs a thread member or the thread owner, and needs a thread moderator when the thread is locked. +- Unarchiving needs a thread member, the thread owner, or a thread moderator, and needs a thread moderator when the thread is locked. - An archived thread accepts no other change unless the same body sets `archived` to false. ### Thread body @@ -316,19 +321,25 @@ The parent type selects the body. A forum or media channel also accepts multipar | Field | Type | Description | | --- | --- | --- | | name? | string | The name of the thread (1-100 characters) | -| archived? | boolean | Whether the thread is archived | +| archived?1 | ?boolean | Whether the thread is archived | | auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes | -| locked? | boolean | Whether only thread moderators can act in the thread | -| rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) | -| flags?1 | integer | The [channel flags](/http-api/forums/#channel-flags) of the thread | -| applied_tags?2 | array[snowflake] | The complete set of forum tag IDs (max 5) | -| invitable?3 | boolean | Whether a member who is not a thread moderator can add other members | +| locked?1 | ?boolean | Whether only thread moderators can act in the thread | +| rate_limit_per_user?2 | ?integer | The slowmode interval in seconds (0-21,600) | +| flags?3 | integer | The [channel flags](/http-api/forums/#channel-flags) of the thread | +| applied_tags?4 | array[snowflake] | The complete set of forum tag IDs (max 5) | +| invitable?5 | ?boolean | Whether a member who is not a thread moderator can add members who are not thread moderators | -1 Only `PINNED` on a forum or media post. Any other bit returns 400 `INVALID_FORM_BODY` +1 Null leaves the value unchanged -2 Only on a forum or media post, and only by the thread owner or a thread moderator. Any other thread returns 400 `INVALID_CHANNEL_TYPE` +2 Null sets `0` -3 Read only for a private thread +3 Only `PINNED` on a forum or media post. Any other bit returns 400 `INVALID_FORM_BODY` + +4 Only on a forum or media post, and only by the thread owner or a thread moderator + +5 Applies only to a private thread. Null leaves the value unchanged + +On a public thread in a guild text channel `applied_tags` returns 400 `INVALID_CHANNEL_TYPE`. A private or announcement thread ignores it. Pinning a second post in one forum returns 400 `MAX_PINNED_THREADS_IN_FORUM`. Changing `auto_archive_duration` resets `archive_timestamp`. Renaming a thread posts a `CHANNEL_NAME_CHANGE` message in it. @@ -341,8 +352,16 @@ Pinning a second post in one forum returns 400 `MAX_PINNED_THREADS_IN_FORUM`. Ch | 400 | [error response](/http-api/#error-response) | Thread is locked and the caller is not a thread moderator, returning `THREAD_LOCKED` | | 400 | [error response](/http-api/#error-response) | Unarchiving would exceed 1,000 active threads and the request returns `MAX_ACTIVE_THREADS` | | 400 | [error response](/http-api/#error-response) | Forum already has a pinned post and the request returns `MAX_PINNED_THREADS_IN_FORUM` | +| 400 | [error response](/http-api/#error-response) | `flags` sets a bit the thread cannot hold and the request returns `INVALID_FORM_BODY` | +| 400 | [error response](/http-api/#error-response) | `applied_tags` is set on a public thread in a guild text channel, returning `INVALID_CHANNEL_TYPE` | +| 400 | [error response](/http-api/#error-response) | `applied_tags` is empty under `REQUIRE_TAG` and the request returns `FORUM_TAG_REQUIRED` | +| 400 | [error response](/http-api/#error-response) | `applied_tags` is empty, with `REQUIRE_TAG` and no unmoderated tag, returning `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` | +| 400 | [error response](/http-api/#error-response) | Caller needs an authenticator and the request returns `TWO_FACTOR_REQUIRED` | | 403 | [error response](/http-api/#error-response) | Caller lacks the owner or moderator role the field needs and the request returns `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | Caller is timed out and the request returns `COMMUNICATION_DISABLED` | +| 404 | [error response](/http-api/#error-response) | A tag does not exist and the request returns `UNKNOWN_FORUM_TAG` | + +In a channel with `REQUIRE_TAG` and no unmoderated tag, a caller who is not a thread moderator receives `NO_TAGS_AVAILABLE_TO_NON_MODERATORS`, and a thread moderator receives `FORUM_TAG_REQUIRED`. [Creating and modifying a forum](/http-api/forums/#creating-and-modifying-a-forum) states how a channel reaches that state. ### Rate limit @@ -350,11 +369,12 @@ Shared with [Modify channel](/http-api/channels/#modify-channel). ## Delete thread -[Delete or leave channel](/http-api/channels/#delete-or-leave-channel) on a thread deletes it with its messages and memberships. It requires `MANAGE_THREADS` and emits a [Thread Delete](/gateway/events/#thread-delete) Gateway event. +[Delete or leave channel](/http-api/channels/#delete-or-leave-channel) on a thread deletes it with its messages, attachments, and memberships. It requires a [thread moderator](#thread-moderators) and emits a [Thread Delete](/gateway/threads/#thread-delete) Gateway event. ### Side effects -- The source message of the thread loses `HAS_THREAD`, and Fluxer emits [Message Update](/gateway/events/#message-update) for it. +- The source message of a thread started from a message loses `HAS_THREAD`, and Fluxer emits [Message Update](/gateway/events/#message-update) for it. +- Fluxer removes the thread and its messages from search. - The guild audit log records action type 112. ### Rate limit @@ -365,7 +385,7 @@ Shared with [Delete or leave channel](/http-api/channels/#delete-or-leave-channe -Returns every active thread in the guild that the bot can view, newest first. Bot-only. +Returns every active thread in the guild that the bot can view, newest first. Requires a bot token. A private thread is included when the bot is a member or a thread moderator in its parent. @@ -387,7 +407,9 @@ A private thread is included when the bot is a member or a thread moderator in i | Status | Body | Condition | | --- | --- | --- | | 200 | active threads object | Threads were returned | +| 403 | [error response](/http-api/#error-response) | Bot is not a member of the guild and the request returns `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Caller is a user, and the request returns `NOT_FOUND` | +| 404 | [error response](/http-api/#error-response) | Guild does not exist and the request returns `UNKNOWN_GUILD` | ### Rate limit @@ -427,6 +449,7 @@ Returns the archived public threads of a guild text, announcement, forum, or med | 200 | archived threads object | Threads were returned | | 400 | [error response](/http-api/#error-response) | Channel is not a thread parent and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | Caller lacks `READ_MESSAGE_HISTORY` and the request returns `MISSING_PERMISSIONS` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or is outside a guild, and the request returns `NOT_FOUND` | ### Rate limit @@ -436,9 +459,30 @@ Returns the archived public threads of a guild text, announcement, forum, or med -Returns the archived private threads of a guild text channel, most recently archived first. Requires `READ_MESSAGE_HISTORY` and `MANAGE_THREADS`. +Returns the archived private threads of a guild text channel, most recently archived first. Requires `READ_MESSAGE_HISTORY` and a [thread moderator](#thread-moderators). -The path parameters, query parameters, and response body match [List public archived threads](#list-public-archived-threads). An announcement, forum, or media channel returns 400 `INVALID_CHANNEL_TYPE`. +An announcement, forum, or media channel returns 400 `INVALID_CHANNEL_TYPE`. + +### Path parameters + +| Field | Type | Description | +| --- | --- | --- | +| channel_id | snowflake | The ID of the guild text channel | + +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| before? | ISO8601 timestamp | Return threads archived before this time | +| limit? | integer | The maximum number of threads to return (2-100, default 50) | + +### Response body + +| Field | Type | Description | +| --- | --- | --- | +| threads | array[[thread](#thread-object) object] | The archived threads | +| members | array[[thread member](/http-api/thread-members/#thread-member-object) object] | A thread member object for each returned thread the caller joined | +| has_more | boolean | Whether a later request can return more threads | ### Rate limit @@ -457,7 +501,15 @@ Returns the archived private threads of a guild text channel that the caller joi | before? | snowflake | Return threads with an ID below this thread ID | | limit? | integer | The maximum number of threads to return (2-100, default 50) | -The response body matches [List public archived threads](#list-public-archived-threads). +An announcement, forum, or media channel returns 400 `INVALID_CHANNEL_TYPE`. + +### Response body + +| Field | Type | Description | +| --- | --- | --- | +| threads | array[[thread](#thread-object) object] | The archived threads | +| members | array[[thread member](/http-api/thread-members/#thread-member-object) object] | A thread member object for each returned thread the caller joined | +| has_more | boolean | Whether a later request can return more threads | ### Rate limit @@ -467,7 +519,7 @@ The response body matches [List public archived threads](#list-public-archived-t -Searches the threads of a guild text, forum, or media channel by name and tag. Requires `READ_MESSAGE_HISTORY`. +Searches the threads of a guild text, announcement, forum, or media channel by name and tag. Requires `READ_MESSAGE_HISTORY`. A caller who is not a thread moderator finds public threads and the private threads they joined. @@ -476,17 +528,19 @@ A caller who is not a thread moderator finds public threads and the private thre | Field | Type | Description | | --- | --- | --- | | name? | string | The text to match in thread names (max 100 characters) | -| tag? | array[snowflake] | The forum tag IDs to filter by, repeated once per tag (max 20) | +| tag?1 | array[snowflake] | The forum tag IDs to filter by, repeated once per tag (max 20) | | tag_setting? | string | `match_some` or `match_all`, defaulting to the `default_tag_setting` of the channel | | archived? | boolean | Whether to return only archived or only active threads (default both) | | sort_by? | string | `last_message_time`, `archive_time`, `relevance`, or `creation_time` (default `last_message_time`) | | sort_order? | string | `asc` or `desc` (default `desc`) | | limit? | integer | The maximum number of threads to return (1-25, default 25) | | offset? | integer | The number of threads to skip (0-9,975, default 0) | -| max_id? | snowflake | Return threads created before this ID | -| min_id? | snowflake | Return threads created after this ID | +| max_id? | snowflake | Return threads with an ID below this ID | +| min_id? | snowflake | Return threads with an ID above this ID | | slop? | integer | A value Fluxer accepts and ignores (0-100, default 2) | +1 Fluxer drops a tag the channel does not have. Under `match_all` a dropped tag empties the result, and under `match_some` the result is empty when every tag is dropped + ### Response body | Field | Type | Description | @@ -499,6 +553,15 @@ A caller who is not a thread moderator finds public threads and the private thre 1 Present only for a forum or media channel +A 202 response has the body below. A search in a guild whose index is not built yet queues the build. + +| Field | Type | Description | +| --- | --- | --- | +| code | string | Always `SEARCH_INDEX_NOT_READY` | +| message | string | A description of the response | +| documents_indexed | integer | Always `0` | +| retry_after | number | The seconds to wait before retrying, always `2` | + ### Response | Status | Body | Condition | @@ -507,8 +570,9 @@ A caller who is not a thread moderator finds public threads and the private thre | 202 | index not ready object | The guild thread index is being built | | 400 | [error response](/http-api/#error-response) | Channel is not a thread parent and the request returns `INVALID_CHANNEL_TYPE` | | 403 | [error response](/http-api/#error-response) | Caller lacks `READ_MESSAGE_HISTORY` and the request returns `MISSING_PERMISSIONS` | - -The 202 body is `code` `SEARCH_INDEX_NOT_READY`, `message`, `documents_indexed` of `0`, and `retry_after` in seconds. The first search in a guild starts the index build. +| 403 | [error response](/http-api/#error-response) | Thread search is unavailable on the instance and the request returns `FEATURE_TEMPORARILY_DISABLED` | +| 403 | [error response](/http-api/#error-response) | The resolved age restriction is not satisfied and the request returns `NSFW_CONTENT_AGE_RESTRICTED` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or is outside a guild, and the request returns `NOT_FOUND` | ### Rate limit @@ -516,107 +580,38 @@ The 202 body is `code` `SEARCH_INDEX_NOT_READY`, `message`, `documents_indexed` ## Thread messages -Two message types belong to threads. - | Value | Name | Description | | --- | --- | --- | -| 18 | THREAD_CREATED | Posted in the parent when a public thread starts | -| 21 | THREAD_STARTER_MESSAGE | The first message of a thread started from a message | +| 18 | THREAD_CREATED | System message in the parent that announces a new public or announcement thread | +| 21 | THREAD_STARTER_MESSAGE | First message of a thread started from a message, built on read and never stored | -`message_reference.channel_id` of a `THREAD_CREATED` message is the thread, and it has no `message_id`. A `THREAD_STARTER_MESSAGE` has the ID of its thread, references the source message, and cannot be deleted. When the source message is deleted, the thread stays and its `referenced_message` becomes null. +`message_reference.channel_id` of a `THREAD_CREATED` message is the thread, and the reference names no message. -A message that started a thread has `thread`, the [thread object](#thread-object), and the message flag `HAS_THREAD` (1 << 5). Mentioning roles whose members would push the thread past 250 automatic additions sets `FAILED_TO_MENTION_SOME_ROLES_IN_THREAD` (1 << 8). Only Fluxer sets either flag. +Fluxer builds a `THREAD_STARTER_MESSAGE` when it reads the thread and stores none. It has the ID of its thread and references the source message. Deleting it returns 403 `MISSING_PERMISSIONS`. When the source message is deleted, the thread stays and its `referenced_message` becomes null. -Sending a message in a thread adds a user sender who is not a member. A mentioned user who can view the parent is added too, unless the thread is private and not `invitable`. +A message that started a thread has `thread`, the [thread object](#thread-object), and the message flag `HAS_THREAD` (1 << 5). A role mention in a public or announcement thread adds the members of the role, up to 250 per message. A message gets `FAILED_TO_MENTION_SOME_ROLES_IN_THREAD` (1 << 8) when that limit or the [thread member limit](/http-api/thread-members/) stops a role member from being added. Only Fluxer sets either flag. + +Sending a message in a thread adds a user sender who has not joined it. A mentioned user who can view the parent is added too. A private thread that is not `invitable` adds mentioned users only when the sender is a thread moderator. ## Thread fields on other objects | Object | Field | Description | | --- | --- | --- | -| Guild text channel | default_auto_archive_duration | The auto archive duration new threads inherit | -| Guild text channel | default_thread_rate_limit_per_user | The slowmode interval new threads inherit | -| Read state | flags | `IS_GUILD_CHANNEL` (1 << 0) and `IS_THREAD` (1 << 1), set by Fluxer | -| Message search result | threads | The threads that contain a returned message | -| Message search result | members | A thread member object for each of those threads the caller joined | -| Audit log | threads | The threads referenced by the returned entries | -| Experiment assignments | channel_threads | `active` and `config_version`, present for every account | +| [Text or announcement channel](/http-api/channels/#channel-object) | default_auto_archive_duration | The stored default [auto archive duration](/http-api/threads/#auto-archive-durations), which Fluxer does not apply to a new thread | +| [Text or announcement channel](/http-api/channels/#channel-object) | default_thread_rate_limit_per_user | The slowmode interval in seconds new threads inherit | +| [Read state](/http-api/read-states/#read-state-object) | flags | The [read state flags](/http-api/read-states/#read-state-flags) | +| [Message search result](/http-api/search/#message-search-result-object) | threads | The threads that contain a returned message | +| [Message search result](/http-api/search/#message-search-result-object) | members | A thread member object for each of those threads the caller joined | +| [Guild audit log response](/http-api/guild-audit-logs/#guild-audit-log-response-object) | threads | The threads a returned thread action targets | The guild audit log records thread creation, update, and deletion as action types 110, 111, and 112. -## Thread error codes +## Client capability -### `FORUM_TAG_NAMES_MUST_BE_UNIQUE` +A user request reaches a route with a route header on this page only with a session token and `channel_threads` in the `X-Fluxer-Features` header. Without both, Fluxer returns 404 `NOT_FOUND` before the rate limit and before the login check. An OAuth2 bearer token never has the capability. A request with no token gets the same 404. A bot token needs no declaration. -Tag names must be unique +A `channel_id` that does not exist or names a channel outside a guild returns the same 404. -### `FORUM_TAG_REQUIRED` +On other routes, a user request without the capability sees no threads. A route that resolves a thread, forum, or media channel by ID returns 404 `UNKNOWN_CHANNEL` unless its own entry states another result. Message lists drop `THREAD_CREATED` messages, and each message loses `thread` and both thread message flags. -A tag is required to create a post in this channel - -### `HIDE_MEDIA_DOWNLOAD_OPTION_MEDIA_ONLY` - -Hiding media download options is only available in media channels - -### `INVALID_THREAD_NOTIFICATION_SETTINGS` - -Invalid thread notification settings - -### `MAX_ACTIVE_THREADS` - -This community has reached its active thread limit of {count} - -### `MAX_FORUM_TAGS` - -This channel has reached its tag limit of {count} - -### `MAX_PINNED_THREADS_IN_FORUM` - -This channel has reached its pinned post limit of {count} - -### `MAX_THREAD_MEMBERS` - -This thread has reached its member limit of {count} - -### `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` - -A tag is required, but only moderators can apply the tags in this channel - -### `SEARCH_INDEX_NOT_READY` - -The search index isn't ready yet - -### `THREAD_ALREADY_CREATED_FOR_MESSAGE` - -A thread has already been created for this message - -### `THREAD_ARCHIVED` - -This thread is archived - -### `THREAD_LOCKED` - -This thread is locked - -### `UNKNOWN_FORUM_TAG` - -Tag wasn't found - -### `UNKNOWN_THREAD_MEMBER` - -Thread member wasn't found - -### `WEBHOOK_FORUM_TARGET_CONFLICT` - -You can't specify both `thread_id` and `thread_name` - -### `WEBHOOK_FORUM_TARGET_REQUIRED` - -Webhooks posting to this channel need a `thread_id` or a `thread_name` - -### `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` - -This webhook service can't post to a forum or media channel without a `thread_id` - -### `WEBHOOK_THREAD_NAME_REQUIRES_FORUM` - -`thread_name` and `applied_tags` only work in forum and media channels +A Gateway session reads the separate `CHANNEL_THREADS` [session flag](/gateway/threads/#session-flag). diff --git a/fluxer_docs/src/content/docs/http-api/users.mdx b/fluxer_docs/src/content/docs/http-api/users.mdx index 178d14d9a..0f484357b 100644 --- a/fluxer_docs/src/content/docs/http-api/users.mdx +++ b/fluxer_docs/src/content/docs/http-api/users.mdx @@ -227,6 +227,7 @@ The value `1` is unassigned. Any stored authenticator value outside this registr An account is limited while its [user object](#user-object) reports `account_limited` as true. A limited account can still read, join voice, change its own settings, and manage its sessions. The operations below return 403 [`ACCOUNT_LIMITED`](/http-api/errors/), and the error message tells the user to check their email. - [Create message](/http-api/messages/#create-message) and [Modify message](/http-api/messages/#modify-message) in any channel except the account's personal notes +- [Start thread from message](/http-api/threads/#start-thread-from-message) and [Start thread](/http-api/threads/#start-thread) - [Crosspost message](/http-api/messages/#crosspost-message) - [Add own reaction](/http-api/messages/#add-own-reaction) - [Create webhook](/http-api/webhooks/#create-webhook) diff --git a/fluxer_docs/src/content/docs/http-api/users/content.mdx b/fluxer_docs/src/content/docs/http-api/users/content.mdx index c44acbc69..fbe3b60b2 100644 --- a/fluxer_docs/src/content/docs/http-api/users/content.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/content.mdx @@ -47,6 +47,8 @@ The entry stores no copy of the message. Fluxer resolves the message against cur 1 Reported when resolving the channel fails with `MISSING_PERMISSIONS`, `UNKNOWN_CHANNEL`, `UNKNOWN_GUILD`, `ACCESS_DENIED`, or `NSFW_CONTENT_AGE_RESTRICTED`. Also reported when the channel resolves and a stored message needs `READ_MESSAGE_HISTORY` the caller does not hold. +An entry saved from a thread, a forum channel, or a media channel is omitted for a user session that does not declare the [`channel_threads` client capability](/http-api/threads/#client-capability). Fluxer reads up to three further batches of entries to replace it. + ## Bulk message deletion scopes | Value | Description | @@ -69,7 +71,9 @@ The entry stores no copy of the message. Fluxer resolves the message against cur Lists the caller's recent mention history. Returns an array of [message](/http-api/messages/#message-object) objects the caller can still read, in descending message ID order. -Fluxer resolves each retained entry against current permissions. A message that has been deleted, or whose channel the caller can no longer reach, is omitted without being removed from history. The filters and `limit` apply before that resolution, so a full page can return fewer messages than `limit`. +Fluxer resolves each retained entry against current permissions. A message that has been deleted, or whose channel the caller can no longer reach, other than a thread the caller cannot view, is omitted without being removed from history. The filters and `limit` apply before that resolution, so a full page can return fewer messages than `limit`. + +An entry in a thread, a forum channel, or a media channel is omitted for a user session that does not declare `channel_threads`. Fluxer reads up to three further batches of history to replace it. ### Query parameters @@ -92,7 +96,7 @@ Fluxer resolves each retained entry against current permissions. A message that 5 Every recorded entry has a guild, so setting this to false always returns an empty array :::note[Only guild channel mentions are recorded] -A direct message never produces an entry. Fluxer records nothing for the message author, for a bot account, or for a member who cannot view the channel. +A direct message never produces an entry. Fluxer records nothing for the message author, for a bot account, or for a member who cannot view the channel. In a thread, an `@everyone` or `@here` mention counts only for a [thread member](/http-api/thread-members/). ::: The response has no cursor metadata, so `before` and `limit` are the only pagination controls. @@ -100,15 +104,17 @@ The response has no cursor metadata, so `before` and `limit` are the only pagina Fluxer drops a direct mention at message creation when the mentioned account has blocked the author, so it never reaches the history. An `@everyone` or `@here` mention is not recorded while `suppress_everyone` is enabled for the guild, and a role mention is not recorded while `suppress_roles` is enabled. A message that also has a direct mention is still recorded. Changing those [guild notification settings](/http-api/users/settings/#user-guild-settings-object) later does not retroactively add or remove history. :::caution[One bad entry can fail the whole listing] -Fluxer omits an entry whose channel the caller cannot reach. An entry that needs age verification, or one whose membership state does not resolve, returns 403 for the whole request. Narrow the window with `before` to page around it. +Fluxer omits an entry whose channel the caller cannot reach. An entry in a thread the caller can no longer view returns 403 `MISSING_ACCESS` for the whole request. Narrow the window with `before` to page around it. ::: +Fluxer also omits an entry that needs age verification and one whose membership state does not resolve. [Thread permissions](/http-api/threads/#thread-permissions) states when a thread stops being viewable. + ### Response | Status | Body | Condition | | --- | --- | --- | | 200 | array[[message](/http-api/messages/#message-object) object] | Readable mentions were returned | -| 403 | [error response](/http-api/#error-response) | A retained entry is in a guild whose membership state cannot be resolved, returning `ACCESS_DENIED`, or in an age-restricted channel the account has not verified for, returning `NSFW_CONTENT_AGE_RESTRICTED` | +| 403 | [error response](/http-api/#error-response) | A retained entry is in a thread the caller cannot view and the request returns `MISSING_ACCESS` | ### Rate limit @@ -178,7 +184,7 @@ A matching entry is deleted and [Recent Mention Delete](/gateway/events/#recent- Lists the caller's saved message collection. Returns an array of [saved message](#saved-message-object) objects in descending message ID order. -The response is a bare array with no cursor metadata, so `before` and `limit` are the only pagination controls. An entry whose channel the caller can no longer reach, or whose message the caller can no longer read, comes back with status `missing_permissions` and a null message. An entry whose message no longer exists is deleted and omitted. The array can hold fewer entries than `limit`. +The response is a bare array with no cursor metadata, so `before` and `limit` are the only pagination controls. An entry whose channel the caller can no longer reach, or whose message the caller can no longer read, comes back with status `missing_permissions` and a null message. An entry in a thread the caller can no longer view returns 403 `MISSING_ACCESS` for the whole request. An entry whose message no longer exists is deleted and omitted. The array can hold fewer entries than `limit`. ### Query parameters @@ -202,7 +208,7 @@ A guild configuring [message_history_cutoff](/http-api/guilds/#guild-object) sti | Status | Body | Condition | | --- | --- | --- | | 200 | array[[saved message](#saved-message-object) object] | Saved entries were returned | -| 403 | [error response](/http-api/#error-response) | A stored entry is in a guild whose membership state cannot be resolved, returning `ACCESS_DENIED`, or in an age-restricted channel, returning `NSFW_CONTENT_AGE_RESTRICTED` | +| 403 | [error response](/http-api/#error-response) | A stored entry is in a thread the caller cannot view and the request returns `MISSING_ACCESS` | ### Side effects @@ -242,11 +248,12 @@ The check runs before Fluxer touches the channel or the message, so re-saving an | 204 | empty | Message is present in the saved collection | | 400 | [error response](/http-api/#error-response) | The collection is at its ceiling and the request returns `MAX_BOOKMARKS` | | 403 | [error response](/http-api/#error-response) | The caller lacks `VIEW_CHANNEL` or guild membership, returning `MISSING_PERMISSIONS`, the membership state cannot be resolved, returning `ACCESS_DENIED`, or the channel needs age verification, returning `NSFW_CONTENT_AGE_RESTRICTED` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 404 | [error response](/http-api/#error-response) | The channel returns `UNKNOWN_CHANNEL`, its guild returns `UNKNOWN_GUILD`, the message returns `UNKNOWN_MESSAGE`, or the account returns `UNKNOWN_USER` | ### Side effects -Saving an already stored message rewrites the entry. [Saved Message Create](/gateway/events/#saved-message-create) reaches the caller's own sessions on every accepted save, a repeat save included, with the complete message object as the caller can currently see it. +Saving an already stored message rewrites the entry. [Saved Message Create](/gateway/events/#saved-message-create) reaches the caller's own sessions on every accepted save, a repeat save included, with the message object as the caller can currently see it. A save of a message in a thread reaches only the sessions with [thread access](/gateway/threads/#session-flag). ### Rate limit @@ -302,7 +309,7 @@ The caller proves sudo mode either with the sudo fields below or with an existin | include_dms? | boolean | Whether one-to-one DMs the caller still has open are included (default true) | | include_dms_closed?1 | boolean | Whether one-to-one DMs the caller has closed are included (default true) | | include_group_dms?2 | boolean | Whether group DMs the caller is still a member of are included (default true) | -| include_guilds?2 | boolean | Whether channels in guilds the caller is still a member of are included (default true) | +| include_guilds?2 | boolean | Whether channels and threads in guilds the caller is still a member of are included (default true) | | guild_filter_mode?3 | string | The [guild filter mode](#guild-filter-modes), default `exclude` | | excluded_guild_ids? | array[snowflake] | The guilds left untouched in `exclude` mode (max 500 IDs, default empty) | | included_guild_ids? | array[snowflake] | The only guilds targeted in `include_only` mode (max 500 IDs, default empty) | @@ -343,7 +350,7 @@ Neither scope includes the caller's personal notes channel, so this operation ca An account holding a TOTP secret or a registered WebAuthn credential that proved sudo mode with MFA receives a fresh proof in the response header. A request that already had a valid proof gets that same token echoed back without an extended lifetime. -As deletion progresses, each affected channel emits [Message Delete Bulk](/gateway/events/#message-delete-bulk) in batches of at most 100 message IDs. The deleted messages' attachments are permanently removed. +As deletion progresses, each affected channel emits [Message Delete Bulk](/gateway/events/#message-delete-bulk) in batches of at most 100 message IDs. The deleted messages' attachments are permanently removed. A message deleted in a thread lowers the `message_count` of that [thread](/http-api/threads/#thread-object), except for the first message of a forum or media post. On completion the system account sends the caller a direct message in the account locale reporting the total deleted message count and the number of channels touched. That message arrives through the ordinary [Message Create](/gateway/events/#message-create) Dispatch. diff --git a/fluxer_docs/src/content/docs/http-api/users/data-harvest.mdx b/fluxer_docs/src/content/docs/http-api/users/data-harvest.mdx index 2aabf918c..90107e3ee 100644 --- a/fluxer_docs/src/content/docs/http-api/users/data-harvest.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/data-harvest.mdx @@ -129,7 +129,7 @@ The filter chooses which messages a harvest collects. Every non-message section | include_dms? | boolean | Whether to include one-to-one direct messages the caller still has open (default true) | | include_dms_closed?2 | boolean | Whether to include one-to-one direct messages the caller has closed (default true) | | include_group_dms? | boolean | Whether to include group DMs the caller is still a member of (default true) | -| include_guilds? | boolean | Whether to include text channels in guilds the caller is a member of (default true) | +| include_guilds? | boolean | Whether to include channels and threads in guilds the caller is a member of (default true) | | guild_filter_mode?3 | string | How the guild list is read, either `exclude` or `include_only` (default `exclude`) | | excluded_guild_ids? | array[snowflake] | The guilds to leave out in `exclude` mode (max 500, default empty) | | included_guild_ids? | array[snowflake] | The only guilds to include in `include_only` mode (max 500, default empty) | @@ -182,7 +182,7 @@ A harvest record is created with a new [snowflake](/snowflakes/), the request ti Fluxer prepares the archive in the background. It contains: -- `user.json`, the account document, including connections with their identifiers, visibility settings and verification dates but no credentials +- `user.json`, the account document, including connections with their identifiers, visibility settings and verification dates but no credentials. It lists the [thread memberships](/http-api/thread-members/#thread-member-object) of the account in its current guilds, with the thread, guild, and parent IDs, `join_timestamp`, `flags`, and `muted` - 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 diff --git a/fluxer_docs/src/content/docs/http-api/users/private-channels.mdx b/fluxer_docs/src/content/docs/http-api/users/private-channels.mdx index a68347309..8b6d4b09b 100644 --- a/fluxer_docs/src/content/docs/http-api/users/private-channels.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/private-channels.mdx @@ -219,7 +219,7 @@ 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`. 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 guild channel the caller cannot see fails resolution first and returns 403 `MISSING_PERMISSIONS`. A caller who lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, receives 403 `MISSING_ACCESS`. A thread, a forum channel, or a media channel returns 404 `UNKNOWN_CHANNEL` to a caller other than a bot or a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability). 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`. 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. @@ -244,7 +244,9 @@ Pinning an already pinned channel leaves the order untouched, yet the operation | 204 | empty | The channel is present in the pinned set | | 400 | [error response](/http-api/#error-response) | The channel is neither a direct message nor a group DM and the request returns `CHANNEL_MUST_BE_DM_OR_GROUP_DM` | | 403 | [error response](/http-api/#error-response) | A guild channel is not visible and the request returns `MISSING_PERMISSIONS`, or the guild exists while membership cannot be resolved and the request returns `ACCESS_DENIED` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 404 | [error response](/http-api/#error-response) | The channel does not exist or the caller is not a recipient, each returning `UNKNOWN_CHANNEL`, or a guild channel outlived its guild and the request returns `UNKNOWN_GUILD` | +| 404 | [error response](/http-api/#error-response) | Channel is a thread, a forum channel, or a media channel and the caller is neither a bot nor a user session that declares the `channel_threads` client capability, returning `UNKNOWN_CHANNEL` | ### Side effects @@ -275,7 +277,9 @@ Fluxer resolves and type checks the channel exactly as in [Pin private channel]( | 204 | empty | The channel is absent from the pinned set | | 400 | [error response](/http-api/#error-response) | The channel is neither a direct message nor a group DM and the request returns `CHANNEL_MUST_BE_DM_OR_GROUP_DM` | | 403 | [error response](/http-api/#error-response) | A guild channel is not visible and the request returns `MISSING_PERMISSIONS`, or the guild exists while membership cannot be resolved and the request returns `ACCESS_DENIED` | +| 403 | [error response](/http-api/#error-response) | Caller lacks `VIEW_CHANNEL` in the parent of a thread, or cannot view a private thread, each returning `MISSING_ACCESS` | | 404 | [error response](/http-api/#error-response) | The channel does not exist or the caller is not a recipient, each returning `UNKNOWN_CHANNEL`, or a guild channel outlived its guild and the request returns `UNKNOWN_GUILD` | +| 404 | [error response](/http-api/#error-response) | Channel is a thread, a forum channel, or a media channel and the caller is neither a bot nor a user session that declares the `channel_threads` client capability, returning `UNKNOWN_CHANNEL` | ### Side effects diff --git a/fluxer_docs/src/content/docs/http-api/users/push-notifications.mdx b/fluxer_docs/src/content/docs/http-api/users/push-notifications.mdx index d7ffff08c..af14d2fd6 100644 --- a/fluxer_docs/src/content/docs/http-api/users/push-notifications.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/push-notifications.mdx @@ -74,6 +74,8 @@ An `ios_apns` or `ios_apns_voip` registration with no `provider_environment` is Register an `https` endpoint. A Web Push registration whose `token` is not a valid URL is refused at `token`, and one whose host is a private or reserved address is refused with `URL_NOT_PUBLICLY_ROUTABLE`. +A registration receives a notification for a message in a thread, and for a [new post](/http-api/forums/#new-post-notifications), only when the registering request declares the [`channel_threads` client capability](/http-api/threads/#client-capability). Registering the same `platform`, `token`, `app_id`, and `provider_environment` again without the capability stops those notifications for the registration. + ### Response | Status | Body | Condition | @@ -121,7 +123,7 @@ The four values below identify the registration the same way [Register mobile pu ## Handling an incoming push -Fluxer posts one encrypted record to the registered endpoint for each notification. The push service that owns the endpoint hands that record to the client. +Fluxer posts one encrypted record to the registered endpoint for each notification. The push service that owns the endpoint hands that record to the client. [Register mobile push device](#register-mobile-push-device) states which registrations receive thread and new post notifications. | Header | Value | | --- | --- | diff --git a/fluxer_docs/src/content/docs/http-api/users/settings.mdx b/fluxer_docs/src/content/docs/http-api/users/settings.mdx index 13cd5e68e..6293e3577 100644 --- a/fluxer_docs/src/content/docs/http-api/users/settings.mdx +++ b/fluxer_docs/src/content/docs/http-api/users/settings.mdx @@ -46,7 +46,7 @@ Gateway presence also uses offline, which describes an account with no active se ## Guild mute configuration object -Mute expiry for a guild, a private channel group, or a channel override. +Mute expiry for a guild, a private channel group, a channel override, or a [thread member](/http-api/thread-members/#thread-member-object). ### Structure @@ -85,11 +85,14 @@ Notification settings stored for one channel. | muted | boolean | Whether channel notifications are muted | | mute_config | ?[guild mute configuration](#guild-mute-configuration-object) object | The mute configuration of the channel, or null when none is stored | | unread_badges1 2 | ?integer | [Notification level](#notification-levels) for unread badges, or null to inherit | +| flags?3 | integer | The [new post notification](/http-api/forums/#new-post-notifications) flags, `NEW_FORUM_THREADS_OFF` (`1 << 13`) and `NEW_FORUM_THREADS_ON` (`1 << 14`) | 1 The value controls client display and does not affect notification delivery 2 Every response has the member, with null meaning no level is set +3 Present only in the settings of a guild, when the stored value is non-zero and the caller is a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability) + ## Channel notification override input object The input form of the [channel notification override object](#channel-notification-override-object). @@ -103,6 +106,11 @@ The input form of the [channel notification override object](#channel-notificati | muted | boolean | Whether channel notifications are muted | | mute_config? | ?[guild mute configuration input](#guild-mute-configuration-input-object) object | The mute configuration of the channel, or null to clear it | | unread_badges? | ?integer | [Notification level](#notification-levels) for unread badges, or null to inherit | +| flags?1 | integer | The [new post notification](/http-api/forums/#new-post-notifications) flags | + +1 Fluxer discards every bit other than `NEW_FORUM_THREADS_OFF` and `NEW_FORUM_THREADS_ON` + +Fluxer ignores `flags` in the private channel settings and from a caller other than a user session that declares `channel_threads`. An omitted or ignored `flags` keeps the stored value. ## User guild settings object @@ -120,7 +128,7 @@ Notification settings for one guild, or for every private channel when `guild_id | suppress_everyone | boolean | Whether `@everyone` mentions are suppressed | | suppress_roles | boolean | Whether role mentions are suppressed | | hide_muted_channels3 | boolean | Whether muted channels are hidden | -| channel_overrides2 | ?map[snowflake, [channel notification override](#channel-notification-override-object) object] | The per-channel overrides, or null when none are set | +| channel_overrides2 6 | ?map[snowflake, [channel notification override](#channel-notification-override-object) object] | The per-channel overrides, or null when none are set | | unread_badges3 5 | ?integer | The default [notification level](#notification-levels) for unread badges, or null to follow `message_notifications` | | version4 | integer | The monotonic revision of this settings object | @@ -134,6 +142,8 @@ Notification settings for one guild, or for every private channel when `guild_id 5 Every response has the member, with null meaning no level is set +6 A user session that does not declare `channel_threads` receives no override for a forum or media channel + ### Example ```json @@ -170,13 +180,15 @@ Both notification settings operations accept exactly these fields. Every field i | channel_overrides?1 | ?map[snowflake, [channel notification override input](#channel-notification-override-input-object) object] | The complete per-channel override map, or null to clear it | | unread_badges? | ?integer | The default [notification level](#notification-levels) for unread badges, or null to follow `message_notifications` | -1 The supplied map becomes the complete override set, and an empty map clears every override +1 The supplied map becomes the complete override set, and an empty map clears every override. A write from a user session that does not declare `channel_threads` keeps the stored overrides of forum and media channels :::note[Which of these fields the server reads] -Fluxer reads `suppress_everyone` and `suppress_roles` when it counts the mentions in a guild message. Push notification delivery reads `muted`, `mute_config`, `mobile_push`, `message_notifications`, `suppress_everyone`, and `suppress_roles`, and the `muted` and `message_notifications` members of a channel override. Every other field is stored and returned unchanged. +Fluxer reads `suppress_everyone` and `suppress_roles` when it counts the mentions in a guild message. Push notification delivery reads `muted`, `mute_config`, `mobile_push`, `message_notifications`, `suppress_everyone`, and `suppress_roles`, and the `muted`, `mute_config`, `message_notifications`, and `flags` members of a channel override. Every other field is stored and returned unchanged. ::: -A direct mention of the account always counts. An `@everyone` or `@here` mention counts only while `suppress_everyone` is false, and a role mention only while `suppress_roles` is false. A mention that counts increments `mention_count` on the channel's [read state](/http-api/read-states/#read-state-object) and adds an entry to the [recent mention history](/http-api/users/content/#list-recent-mentions). A guild message whose every mention is suppressed records nothing. +For a message in a thread, push delivery reads the override of the parent channel and the `muted` and `mute_config` of the override of the parent category. It reads no override keyed by a thread ID. [Thread member flags](/http-api/thread-members/#thread-member-flags) and the thread mute set by [Modify thread member settings](/http-api/thread-members/#modify-thread-member-settings) apply to a member. A user who has not joined the thread is notified only for a direct mention or a role mention. + +A direct mention of the account always counts. An `@everyone` or `@here` mention counts only while `suppress_everyone` is false, and a role mention only while `suppress_roles` is false. In a thread, an `@everyone` or `@here` mention counts only for a [thread member](/http-api/thread-members/). A mention that counts increments `mention_count` on the channel's [read state](/http-api/read-states/#read-state-object) and adds an entry to the [recent mention history](/http-api/users/content/#list-recent-mentions). A guild message whose every mention is suppressed records nothing. Neither field is read outside a guild. A direct message and a group DM collect only direct mentions, and every one of them counts whatever the private channel settings hold. diff --git a/fluxer_docs/src/content/docs/http-api/webhooks.mdx b/fluxer_docs/src/content/docs/http-api/webhooks.mdx index 5d0f5d937..cbd3d595e 100644 --- a/fluxer_docs/src/content/docs/http-api/webhooks.mdx +++ b/fluxer_docs/src/content/docs/http-api/webhooks.mdx @@ -20,6 +20,10 @@ Fluxer resolves the guild before every management operation. A guild that does n [List guild webhooks](#list-guild-webhooks), [List channel webhooks](#list-channel-webhooks), and [Create webhook](#create-webhook) name a guild or a channel in their paths, so the availability gate applies. A guild with [UNAVAILABLE_FOR_EVERYONE](/http-api/guilds/#guild-features) refuses an authenticated request with 403 `MISSING_ACCESS` before the operation runs. [UNAVAILABLE_FOR_EVERYONE_BUT_STAFF](/http-api/guilds/#guild-features) does the same for an account without the instance staff flag. The remaining management routes name only a webhook ID and are not gated. +On a management operation, a caller other than a bot or a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability) sees no webhook of a forum or media channel. [List guild webhooks](#list-guild-webhooks) omits one, and [Get webhook](#get-webhook), [Update webhook](#update-webhook), and [Delete webhook](#delete-webhook) return 404 `UNKNOWN_WEBHOOK` for one. [List channel webhooks](#list-channel-webhooks) and [Create webhook](#create-webhook) on a forum or media channel return 404 `UNKNOWN_CHANNEL` to that caller. [Update webhook](#update-webhook) with a forum or media channel as `channel_id` returns the same. + +A token-authenticated operation returns a webhook of a forum or media channel to any holder of the token. + Fluxer scans a submitted webhook `name` against the instance phrase and URL blocklists, and a match returns 403 `CONTENT_BLOCKED`. The same lists scan the resolved content, embed text, and embed link URLs of a created or edited message, and a match returns the same code. The `avatar` member is exempt from that scan, and Fluxer checks its decoded bytes against the banned asset hash list when it stores them. ## Rate limit keying @@ -40,7 +44,7 @@ A request that sends no `Origin`, or any other `Origin` value, passes the check. ## Webhook object -A webhook belongs to exactly one guild and posts into exactly one guild text, voice, or announcement channel of that guild. Fluxer generates the execution token of an incoming webhook once at creation and never rotates it, so the pair of ID and token is a bearer credential for the lifetime of the webhook. A channel follower webhook has no usable token. +A webhook belongs to exactly one guild and posts into exactly one guild text, voice, announcement, forum, or media channel of that guild. Only those channel types can hold a webhook. Fluxer generates the execution token of an incoming webhook once at creation and never rotates it. The pair of ID and token is a bearer credential for the lifetime of the webhook. A channel follower webhook has no usable token. ### Structure @@ -142,7 +146,7 @@ Every operation returning a [webhook object](#webhook-object) of an incoming web ## Webhook message body -[Execute webhook](#execute-webhook) accepts this JSON body. Every field may be omitted, and `{}` passes schema validation and then returns 400 `CANNOT_SEND_EMPTY_MESSAGE`. +[Execute webhook](#execute-webhook) accepts this JSON body. Every field may be omitted, and `{}` passes schema validation and then returns 400 `CANNOT_SEND_EMPTY_MESSAGE`. A webhook of a forum or media channel that sends it without `thread_id` returns 400 `WEBHOOK_FORUM_TARGET_REQUIRED` first. ### Structure @@ -160,6 +164,8 @@ Every operation returning a [webhook object](#webhook-object) of an incoming web | nonce?7 | string \| integer | Client-generated message identifier (1-32 characters). A non-negative safe integer becomes its decimal string | | username?8 | ?string | Per-message webhook name override (1-80 characters), or null | | avatar_url?8 | ?string | Absolute `http` or `https` per-message webhook avatar URL override of at most 2,048 characters, or null | +| thread_name?9 | string | The name of a new forum post (1-100 characters) | +| applied_tags?9 | array[snowflake] | The IDs of the tags a new forum post applies (max 5) | 1 Webhook message content is limited to the resolved [max_message_length](/http-api/instance/#limit-keys) value for the guild or 4000 characters, whichever is larger. Exceeding it returns 400 `INVALID_FORM_BODY` with the validation code `CONTENT_EXCEEDS_MAX_LENGTH` @@ -173,10 +179,14 @@ Every operation returning a [webhook object](#webhook-object) of an incoming web 6 The field takes no part in the non-empty message check, so a body supplying `tts` and nothing else returns 400 `CANNOT_SEND_EMPTY_MESSAGE`. A webhook message is never marked text-to-speech -7 The nonce is remembered per webhook for five minutes and is echoed on the [Message Create](/gateway/events/#message-create) Dispatch. A repeated execution presenting it inside that window creates no second message and returns the first one, and a repeat after the webhook moved to another channel returns 404 `UNKNOWN_MESSAGE` +7 The nonce is remembered per webhook for five minutes and is echoed on the [Message Create](/gateway/events/#message-create) Dispatch. A repeated execution presenting it inside that window creates no second message and returns the first one, and a repeat that names another `thread_id`, that sends `thread_name` again, or that follows a move of the webhook to another channel returns 404 `UNKNOWN_MESSAGE` 8 Applies to the created message, and the stored webhook keeps its own name and avatar +9 Accepted only from a webhook whose channel is a forum or media channel, and any other channel returns 400 `WEBHOOK_THREAD_NAME_REQUIRES_FORUM` + +A forum or media channel webhook that sends `thread_id` ignores `applied_tags`. On a `thread_name` post, a moderated tag returns 403 `MISSING_PERMISSIONS`, and a tag the channel does not hold returns 404 `UNKNOWN_FORUM_TAG`. [Webhooks in forums](/http-api/forums/#webhooks-in-forums) describes posting into a forum or media channel. + When `embeds` is absent, Fluxer rewrites the singular key `embed` to a one-element `embeds` array, and an `embed` of null becomes an empty array. When both are present, `embed` is dropped. A multipart body accepts both [pre-uploaded attachments](/http-api/messages/#pre-uploaded-attachment-object) and [direct multipart attachment metadata](/http-api/messages/#direct-multipart-attachment-metadata-object). @@ -185,7 +195,7 @@ A multipart body accepts both [pre-uploaded attachments](/http-api/messages/#pre A webhook execution accepts the [message reference input](/http-api/messages/#message-reference-input-object) object. 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 `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 resolves in the webhook's own channel and ignores this field. With [`thread_id`](#execute-webhook), the reference resolves in that thread, and the `channel_id` of a forward reference must equal the thread ID. The referenced message must exist in the webhook's channel, and a missing message returns 404 `UNKNOWN_MESSAGE`. A reply reference also 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`. @@ -775,13 +785,13 @@ A webhook is returned only when the caller also holds both `VIEW_CHANNEL` and `M -Returns an array of [webhook objects](#webhook-object) in a guild text, voice, or announcement channel. The caller must be able to view the channel and hold the [MANAGE_WEBHOOKS](/http-api/permissions/) permission at guild level and in the channel. +Returns an array of [webhook objects](#webhook-object) in a guild text, voice, announcement, forum, or media channel. The caller must be able to view the channel and hold the [MANAGE_WEBHOOKS](/http-api/permissions/) permission at guild level and in the channel. ### Path parameters | Field | Type | Description | | --- | --- | --- | -| channel_id | snowflake | The ID of the guild text, voice, or announcement channel whose webhooks are returned | +| channel_id | snowflake | The ID of the guild text, voice, announcement, forum, or media channel whose webhooks are returned | ### Response @@ -795,7 +805,7 @@ Returns an array of [webhook objects](#webhook-object) in a guild text, voice, o | 4032 | [error response](/http-api/#error-response) | The caller is not a member of the channel's guild | | 4032 | [error response](/http-api/#error-response) | The caller lacks channel access or `MANAGE_WEBHOOKS` | | 4032 | [error response](/http-api/#error-response) | The channel is age restricted and the account is not age verified | -| 404 | [error response](/http-api/#error-response) | Channel does not exist or is not a guild text, voice, or announcement channel, each returning `UNKNOWN_CHANNEL` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or cannot hold a webhook, each returning `UNKNOWN_CHANNEL` | | 404 | [error response](/http-api/#error-response) | Its guild does not resolve, returning `UNKNOWN_GUILD` | 1 The missing-authenticator code is `TWO_FACTOR_REQUIRED`, returned only after Fluxer confirms the permission itself @@ -810,7 +820,7 @@ Returns an array of [webhook objects](#webhook-object) in a guild text, voice, o -Creates a webhook in a guild text, voice, or announcement channel and returns the new [webhook object](#webhook-object) with its execution token. The caller must be able to view the channel and hold the [MANAGE_WEBHOOKS](/http-api/permissions/) permission at guild level and in the channel. The operation accepts an audit reason. +Creates a webhook in a guild text, voice, announcement, forum, or media channel and returns the new [webhook object](#webhook-object) with its execution token. The caller must be able to view the channel and hold the [MANAGE_WEBHOOKS](/http-api/permissions/) permission at guild level and in the channel. The operation accepts an audit reason. Creation emits a [Webhooks Update](/gateway/events/#webhooks-update). @@ -820,7 +830,7 @@ The guild allowance is the resolved [max_webhooks_per_guild](/http-api/instance/ | Field | Type | Description | | --- | --- | --- | -| channel_id | snowflake | The ID of the guild text, voice, or announcement channel the webhook is created in | +| channel_id | snowflake | The ID of the guild text, voice, announcement, forum, or media channel the webhook is created in | ### JSON body @@ -849,7 +859,7 @@ The decoded bytes must be at most the resolved [avatar_max_size](/http-api/insta | 403 | [error response](/http-api/#error-response) | The channel is age restricted and the account is not age verified | | 403 | [error response](/http-api/#error-response) | The name is blocked | | 403 | [error response](/http-api/#error-response) | The avatar hash is banned | -| 404 | [error response](/http-api/#error-response) | Channel does not exist or is not a guild text, voice, or announcement channel, returning `UNKNOWN_CHANNEL` | +| 404 | [error response](/http-api/#error-response) | Channel does not exist or cannot hold a webhook, returning `UNKNOWN_CHANNEL` | | 404 | [error response](/http-api/#error-response) | Its guild does not resolve, returning `UNKNOWN_GUILD` | 1 An avatar failure names the `avatar` path with `BASE64_LENGTH_INVALID`, `INVALID_BASE64_FORMAT`, `IMAGE_SIZE_EXCEEDS_LIMIT`, or `INVALID_IMAGE_FORMAT` @@ -935,7 +945,7 @@ Updates a webhook and returns the modified [webhook object](#webhook-object). Em | --- | --- | --- | | name? | string | Replacement webhook name (1-80 characters) | | avatar?1 | ?base64 string | Base64-encoded replacement avatar, or null to remove the current avatar | -| channel_id?2 | snowflake | Destination guild text, voice, or announcement channel in the same guild | +| channel_id?2 | snowflake | Destination guild text, voice, announcement, forum, or media channel in the same guild | 1 An omitted avatar leaves the stored hash unchanged, and an explicit null clears it. The same encoding, size, and format rules as [create webhook](#create-webhook) apply. A channel follower webhook rejects the member, null included @@ -959,7 +969,7 @@ The returned webhook object has the destination channel. Copies a channel follow | 403 | [error response](/http-api/#error-response) | The name is blocked | | 403 | [error response](/http-api/#error-response) | The avatar hash is banned | | 404 | [error response](/http-api/#error-response) | Webhook does not exist, returning `UNKNOWN_WEBHOOK` | -| 404 | [error response](/http-api/#error-response) | The destination channel is missing or is not a guild text, voice, or announcement channel, returning `UNKNOWN_CHANNEL` | +| 404 | [error response](/http-api/#error-response) | The destination channel is missing or cannot hold a webhook, returning `UNKNOWN_CHANNEL` | | 404 | [error response](/http-api/#error-response) | The destination channel belongs to another guild, returning `UNKNOWN_CHANNEL` | | 404 | [error response](/http-api/#error-response) | The webhook's guild does not resolve, returning `UNKNOWN_GUILD` | @@ -1163,9 +1173,12 @@ The route refuses a call from a first-party web client. See [origin refusal](#or | Field | Type | Description | | --- | --- | --- | | wait?1 | boolean | Whether to return the created message (default false) | +| thread_id?2 | snowflake | The ID of a thread of the webhook's channel to post into | 1 Only the exact trimmed values `true`, `True`, and `1` are treated as true, and every other value, including `TRUE` and `yes`, is treated as false +2 A value that names no thread of the webhook's channel returns 404 `UNKNOWN_CHANNEL`. [Webhooks in forums](/http-api/forums/#webhooks-in-forums) describes posting into a forum or media channel + ### JSON body The JSON representation of the request body is a [webhook message body](#webhook-message-body). Any request whose `Content-Type` does not contain `multipart/form-data` is read as JSON. Fluxer reads a body that is empty, whitespace-only, or unparseable as `{}`, and the request then fails as an empty message. @@ -1201,11 +1214,21 @@ An attachment metadata entry whose `id` matches a supplied file index supplies t | 4001 | [error response](/http-api/#error-response) | The resolved payload has no content, embed, or attachment | | 4001 | [error response](/http-api/#error-response) | The content exceeds the effective maximum length | | 4001 | [error response](/http-api/#error-response) | The embed or attachment count exceeds its effective ceiling | +| 400 | [error response](/http-api/#error-response) | A forum or media channel execution has no `thread_id` or `thread_name`, returning `WEBHOOK_FORUM_TARGET_REQUIRED` | +| 400 | [error response](/http-api/#error-response) | A forum or media channel execution has `thread_id` and `thread_name`, returning `WEBHOOK_FORUM_TARGET_CONFLICT` | +| 400 | [error response](/http-api/#error-response) | `thread_name` or `applied_tags` targets any other channel, returning `WEBHOOK_THREAD_NAME_REQUIRES_FORUM` | +| 400 | [error response](/http-api/#error-response) | A `thread_name` post names no tag under `REQUIRE_TAG`, returning `FORUM_TAG_REQUIRED` | +| 400 | [error response](/http-api/#error-response) | A `thread_name` post names no tag, with `REQUIRE_TAG` and no unmoderated tag, returning `NO_TAGS_AVAILABLE_TO_NON_MODERATORS` | +| 400 | [error response](/http-api/#error-response) | `thread_id` names a locked thread, returning `THREAD_LOCKED` | +| 400 | [error response](/http-api/#error-response) | The guild has 1,000 active threads and `thread_name` creates a post, returning `MAX_ACTIVE_THREADS` | +| 400 | [error response](/http-api/#error-response) | The guild has 1,000 active threads and `thread_id` names an archived thread, returning `MAX_ACTIVE_THREADS` | | 403 | [error response](/http-api/#error-response) | Request has a first-party web client `Origin`, returning `INVALID_API_ORIGIN` | | 403 | [error response](/http-api/#error-response) | The resolved content or embed text is blocked, returning `CONTENT_BLOCKED` | +| 403 | [error response](/http-api/#error-response) | A `thread_name` post names a moderated tag in `applied_tags`, returning `MISSING_PERMISSIONS` | | 404 | [error response](/http-api/#error-response) | Webhook and token pair does not exist, returning `UNKNOWN_WEBHOOK` | -| 404 | [error response](/http-api/#error-response) | The target channel is missing or is not a guild text, voice, or announcement channel, returning `UNKNOWN_CHANNEL` | +| 404 | [error response](/http-api/#error-response) | The target channel is missing or cannot hold a webhook, or `thread_id` names no thread of it, returning `UNKNOWN_CHANNEL` | | 404 | [error response](/http-api/#error-response) | The referenced message does not exist in the webhook's channel, returning `UNKNOWN_MESSAGE` | +| 404 | [error response](/http-api/#error-response) | A `thread_name` post names a tag the channel does not hold in `applied_tags`, returning `UNKNOWN_FORUM_TAG` | 1 `CANNOT_SEND_EMPTY_MESSAGE` is returned as the 400 error code itself. Every other code below, including `CONTENT_EXCEEDS_MAX_LENGTH`, sits inside an `INVALID_FORM_BODY` body @@ -1249,6 +1272,14 @@ The route refuses a call from a first-party web client. See [origin refusal](#or 1 The message is resolved within the webhook's current channel, so a message the webhook created before it was moved to another channel is no longer reachable through this route +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| thread_id?1 | snowflake | The ID of the thread of the webhook's channel that holds the message | + +1 A value that names no thread of the webhook's channel returns 404 `UNKNOWN_CHANNEL` + ### Response | Status | Body | Condition | @@ -1280,6 +1311,14 @@ The route refuses a call from a first-party web client. See [origin refusal](#or | token | string | Webhook execution token (1-256 characters) | | message_id | snowflake | The ID of the message to update | +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| thread_id?1 | snowflake | The ID of the thread of the webhook's channel that holds the message | + +1 A value that names no thread of the webhook's channel returns 404 `UNKNOWN_CHANNEL` + ### JSON body The body is a [webhook message edit body](#webhook-message-edit-body). @@ -1298,6 +1337,8 @@ Any other type returns 400 `CANNOT_MODIFY_SYSTEM_WEBHOOK`, and a forward, which | 400 | [error response](/http-api/#error-response) | The content or embed count exceeds its effective ceiling | | 400 | [error response](/http-api/#error-response) | The target message is not an editable type | | 400 | [error response](/http-api/#error-response) | The webhook's stored channel no longer resolves to a guild channel | +| 400 | [error response](/http-api/#error-response) | `thread_id` names an archived thread, returning `THREAD_ARCHIVED` | +| 400 | [error response](/http-api/#error-response) | `thread_id` names a locked thread, returning `THREAD_LOCKED` | | 403 | [error response](/http-api/#error-response) | Request has a first-party web client `Origin`, returning `INVALID_API_ORIGIN` | | 403 | [error response](/http-api/#error-response) | The message was authored by another webhook, a user, or a bot, returning `MISSING_PERMISSIONS` | | 403 | [error response](/http-api/#error-response) | The replacement content or embed text is blocked, returning `CONTENT_BLOCKED` | @@ -1338,6 +1379,14 @@ The route refuses a call from a first-party web client. See [origin refusal](#or | token | string | Webhook execution token (1-256 characters) | | message_id | snowflake | The ID of the message to delete | +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| thread_id?1 | snowflake | The ID of the thread of the webhook's channel that holds the message | + +1 A value that names no thread of the webhook's channel returns 404 `UNKNOWN_CHANNEL` + ### Response | Status | Body | Condition | @@ -1373,6 +1422,14 @@ A rendered event emits a [Message Create](/gateway/events/#message-create). | webhook_id | snowflake | The ID of the webhook to execute | | token | string | Webhook execution token (1-256 characters) | +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| thread_id?1 | snowflake | The ID of a thread of the webhook's channel to post into | + +1 A value that names no thread of the webhook's channel returns 404 `UNKNOWN_CHANNEL`. A webhook whose channel is a forum or media channel needs it, and returns 400 `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` without it + ### Request headers | Field | Type | Description | @@ -1399,9 +1456,12 @@ A callback that renders nothing, and one whose message creation fails, both leav | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Callback was accepted, was not renderable, or repeated a delivery within the deduplication window | +| 400 | [error response](/http-api/#error-response) | The webhook's channel is a forum or media channel and `thread_id` is absent, returning `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` | +| 400 | [error response](/http-api/#error-response) | `thread_id` names a locked thread, returning `THREAD_LOCKED` | +| 400 | [error response](/http-api/#error-response) | The guild has 1,000 active threads and `thread_id` names an archived thread, returning `MAX_ACTIVE_THREADS` | | 403 | [error response](/http-api/#error-response) | Rendered embed text is blocked, returning `CONTENT_BLOCKED` | | 404 | [error response](/http-api/#error-response) | Webhook and token pair does not exist, returning `UNKNOWN_WEBHOOK` | -| 404 | [error response](/http-api/#error-response) | The target channel is missing or is not a guild text, voice, or announcement channel, returning `UNKNOWN_CHANNEL` | +| 404 | [error response](/http-api/#error-response) | The target channel is missing or cannot hold a webhook, or `thread_id` names no thread of it, returning `UNKNOWN_CHANNEL` | ### Side effects @@ -1428,6 +1488,14 @@ A successful callback emits a [Message Create](/gateway/events/#message-create). | webhook_id | snowflake | The ID of the webhook to execute | | token | string | Webhook execution token (1-256 characters) | +### Query parameters + +| Field | Type | Description | +| --- | --- | --- | +| thread_id?1 | snowflake | The ID of a thread of the webhook's channel to post into | + +1 A value that names no thread of the webhook's channel returns 404 `UNKNOWN_CHANNEL`. A webhook whose channel is a forum or media channel needs it, and returns 400 `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` without it + ### JSON body The body is a [Slack callback](#slack-callback-object) object. @@ -1441,9 +1509,12 @@ The body is a [Slack callback](#slack-callback-object) object. | 4001 | [error response](/http-api/#error-response) | The conversion yields neither content nor an embed | | 4001 | [error response](/http-api/#error-response) | The resulting content exceeds the effective maximum length | | 4001 | [error response](/http-api/#error-response) | The converted embed count exceeds its effective ceiling | +| 400 | [error response](/http-api/#error-response) | The webhook's channel is a forum or media channel and `thread_id` is absent, returning `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` | +| 400 | [error response](/http-api/#error-response) | `thread_id` names a locked thread, returning `THREAD_LOCKED` | +| 400 | [error response](/http-api/#error-response) | The guild has 1,000 active threads and `thread_id` names an archived thread, returning `MAX_ACTIVE_THREADS` | | 403 | [error response](/http-api/#error-response) | Converted content or embed text is blocked, returning `CONTENT_BLOCKED` | | 404 | [error response](/http-api/#error-response) | Webhook and token pair does not exist, returning `UNKNOWN_WEBHOOK` | -| 404 | [error response](/http-api/#error-response) | The target channel is missing or is not a guild text, voice, or announcement channel, returning `UNKNOWN_CHANNEL` | +| 404 | [error response](/http-api/#error-response) | The target channel is missing or cannot hold a webhook, or `thread_id` names no thread of it, returning `UNKNOWN_CHANNEL` | 1 The [error code](/http-api/errors/) is `CANNOT_SEND_EMPTY_MESSAGE` for a conversion that yields nothing, `CONTENT_EXCEEDS_MAX_LENGTH` for content that is too long, and `INVALID_FORM_BODY` with the validation code `TOO_MANY_EMBEDS` for the embed ceiling @@ -1493,9 +1564,10 @@ A callback that renders nothing, and one whose message creation fails, both leav | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Callback was accepted, was not renderable, or repeated a callback within the deduplication window | +| 400 | [error response](/http-api/#error-response) | The webhook's channel is a forum or media channel, returning `WEBHOOK_SERVICE_FORUM_UNSUPPORTED` | | 403 | [error response](/http-api/#error-response) | Rendered embed text is blocked, returning `CONTENT_BLOCKED` | | 404 | [error response](/http-api/#error-response) | Webhook and token pair does not exist, returning `UNKNOWN_WEBHOOK` | -| 404 | [error response](/http-api/#error-response) | The target channel is missing or is not a guild text, voice, or announcement channel, returning `UNKNOWN_CHANNEL` | +| 404 | [error response](/http-api/#error-response) | The target channel is missing or cannot hold a webhook, returning `UNKNOWN_CHANNEL` | ### Side effects diff --git a/fluxer_docs/src/content/docs/operator/configuration.mdx b/fluxer_docs/src/content/docs/operator/configuration.mdx index a7b7e2a78..9425cbca0 100644 --- a/fluxer_docs/src/content/docs/operator/configuration.mdx +++ b/fluxer_docs/src/content/docs/operator/configuration.mdx @@ -1059,7 +1059,7 @@ Explicit `false` values and supported null clears remain valid. Branding and int ## Limits -No environment variable changes an instance limit. Use the admin dashboard's Limit Config page or the [Admin API](/admin-api/). Effective limits combine saved settings, deployment defaults and premium policy. After a change is saved, every running `api` and `worker` process reloads the limits with no restart. Clients receive the [limit configuration object](/http-api/instance/#limit-configuration-object). +No environment variable changes an instance limit. Use the admin dashboard's Limit Config page or the [Admin API](/admin-api/). The thread and forum limits are fixed and have no limit key. [Thread lifecycle](/http-api/threads/#thread-lifecycle), [Thread members](/http-api/thread-members/) and [Forum channel fields](/http-api/forums/#forum-channel-fields) state them. Effective limits combine saved settings, deployment defaults and premium policy. After a change is saved, every running `api` and `worker` process reloads the limits with no restart. Clients receive the [limit configuration object](/http-api/instance/#limit-configuration-object). Request concurrency is separate from instance limits, and each process sets its own. All are optional. @@ -2159,15 +2159,15 @@ CORS origins are exactly the app endpoint, the `FLUXER_APP_ORIGIN_ALIASES` origi | --- | --- | --- | | postgres-data | Every account, message, and configuration row | Yes | | seaweedfs-data | Every uploaded file | Yes | -| valkey-data | Pending file deletions, other queues and shared cache | Yes | +| valkey-data | Pending file deletions, the thread auto archive schedule, other queues and shared cache | Yes | | nats-data | Pending and failed background jobs | Yes | | edge-data | Issued TLS certificates | Optional, a loss only costs a re-issue | | edge-config | The edge's own state | No | | meilisearch-data | The search index, rebuildable from Postgres | Optional | -Losing `valkey-data` can delay scheduled account and bulk-message deletions while their queues are rebuilt. Pending asset deletions and cache purges exist only here and are lost with it, so do not treat this volume as disposable cache. +Losing `valkey-data` can delay scheduled account and bulk-message deletions while their queues are rebuilt. Pending asset deletions and cache purges exist only here and are lost with it, so do not treat this volume as disposable cache. The thread auto archive schedule exists only here too. After a loss, an active thread is archived automatically again only once it is modified or unarchived. [Thread lifecycle](/http-api/threads/#thread-lifecycle) states the archive rule. -Losing `meilisearch-data` leaves search empty until it is rebuilt. Nothing rebuilds it automatically, because Postgres still records every channel as indexed. Rebuild each index with [`POST /admin/search/indexes/{index_name}/refreshes`](/admin-api/search-indexes/). The `channel_messages` and `guild_members` indexes rebuild one community at a time and need a `guild_id` for each. A backup of the volume avoids those calls on a large instance. +Losing `meilisearch-data` leaves search empty until it is rebuilt. Nothing rebuilds it automatically, because Postgres still records every channel as indexed. Rebuild each index with [`POST /admin/search/indexes/{index_name}/refreshes`](/admin-api/search-indexes/). The `channel_messages`, `guild_members` and `threads` indexes rebuild one community at a time and need a `guild_id` for each. A backup of the volume avoids those calls on a large instance. `nats-data` retains pending jobs in `JOBS` for up to 7 days and failed jobs in `JOBS_DLQ` for up to 30 days. A full jobs stream rejects new work. A full dead-letter stream drops its oldest entries, so investigate failures promptly. If dead-letter storage is unavailable, failed jobs remain in `JOBS` only until they expire. Losing this volume loses queued work, which is not automatically recovered from the database. diff --git a/fluxer_docs/src/content/docs/snowflakes.md b/fluxer_docs/src/content/docs/snowflakes.md index b9a997acf..4c0900923 100644 --- a/fluxer_docs/src/content/docs/snowflakes.md +++ b/fluxer_docs/src/content/docs/snowflakes.md @@ -38,6 +38,8 @@ To obtain the creation time in Unix milliseconds, shift the snowflake right by 2 timestamp_ms = (snowflake >> 22) + 1420070400000 ``` +A thread started from a message has the ID of that message. The formula then returns the time of the message, and `create_timestamp` in the [thread metadata](/http-api/threads/#thread-metadata-object) is the time the thread was created. + The inverse expression produces the smallest possible snowflake for a Unix millisecond timestamp. It is useful as a pagination boundary. ```text @@ -83,7 +85,7 @@ A collection endpoint that pages over a snowflake-ordered resource accepts a cur 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 uses the high bits, numeric snowflake order is creation time order at millisecond resolution. +Because the timestamp uses the high bits, numeric snowflake order is creation time order at millisecond resolution. Threads started from a message sort by the time of their source message. :::note[A cursor is a numeric boundary] A cursor does not need to identify a resource, so a snowflake derived from a timestamp can delimit a time range without a dedicated timestamp parameter. diff --git a/fluxer_docs/src/content/docs/topics/announcement-channels.md b/fluxer_docs/src/content/docs/topics/announcement-channels.md index a9099b6c2..aaafdf096 100644 --- a/fluxer_docs/src/content/docs/topics/announcement-channels.md +++ b/fluxer_docs/src/content/docs/topics/announcement-channels.md @@ -14,11 +14,13 @@ Every guild can create announcement channels. [Create guild channel](/http-api/g A text channel that receives copies from a followed announcement channel cannot become an announcement channel, because an announcement channel cannot follow another one. Delete its channel follower webhooks first. +A text channel that holds a private thread cannot become an announcement channel either, and the request returns 400 `CHANNEL_HAS_THREADS`. [Thread types](/http-api/threads/#thread-types) describes how a conversion changes the type of the public threads of the channel. + Converting an announcement channel into a text channel removes every follow of it. The copies already delivered stay, and they keep receiving later edits and deletions of the messages they came from. ## Publishing -[Crosspost message](/http-api/messages/#crosspost-message) publishes one message. The author needs `SEND_MESSAGES`. Publishing another member's message, or a webhook's, also needs `MANAGE_MESSAGES`. A message is published once, and only a `DEFAULT` message that is not a reply, a forward, or a copy can be published. +[Crosspost message](/http-api/messages/#crosspost-message) publishes one message. The author needs `SEND_MESSAGES`. Publishing another member's message, or a webhook's, also needs `MANAGE_MESSAGES`. A message is published once, and only a `DEFAULT` message that is not a reply, a forward, or a copy can be published. A message in an announcement thread cannot be published, as [Thread types](/http-api/threads/#thread-types) states. Publishing sets the `CROSSPOSTED` [message flag](/http-api/messages/#message-flags) and returns at once. The copies are created in the background, usually within seconds. A channel that follows the announcement channel after a message was published never receives that message. diff --git a/fluxer_docs/src/content/docs/topics/rate-limits.md b/fluxer_docs/src/content/docs/topics/rate-limits.md index efe01aa59..0f465521e 100644 --- a/fluxer_docs/src/content/docs/topics/rate-limits.md +++ b/fluxer_docs/src/content/docs/topics/rate-limits.md @@ -208,7 +208,9 @@ Fluxer consumes every multi-factor allowance before it checks the code, so a cor Slowmode limits how often one account sends a message in one channel. Fluxer reports a denial as an ordinary request failure. A denied send returns 400 `SLOWMODE_RATE_LIMITED` with a top-level `retry_after` in fractional seconds and a `Retry-After` header in whole seconds. The response has no `X-RateLimit-*` header, so a client tells it apart from a bucket denial by the status and the code. -The allowance is one message for each interval the channel configures in `rate_limit_per_user`, counted separately for each account and channel pair. Fluxer counts it only for a non-bot account sending in a guild channel whose configured interval is above zero. A caller holding [BYPASS_SLOWMODE](/http-api/permissions/) is exempt. [Get channel slowmode state](/http-api/channels/#get-channel-slowmode-state) reports the caller's remaining delay before a send is attempted. +The allowance is one message for each interval the channel configures in `rate_limit_per_user`, counted separately for each account and channel pair. Fluxer counts it for a non-bot account in a guild channel whose configured interval is above zero. A caller holding [BYPASS_SLOWMODE](/http-api/permissions/) is exempt. [Get channel slowmode state](/http-api/channels/#get-channel-slowmode-state) reports the caller's remaining delay before a send is attempted. + +Starting a thread uses the parent slowmode on its own counter, as [Start thread from message](/http-api/threads/#start-thread-from-message) states. The same applies to a post in a forum or media channel, as [Forum channel fields](/http-api/forums/#forum-channel-fields) states. For a guild text or announcement channel, [Get channel slowmode state](/http-api/channels/#get-channel-slowmode-state) reports the message counter alone. [Indicate typing](/http-api/messages/#indicate-typing) returns the remaining delay of both counters to a user session that declares the [`channel_threads` client capability](/http-api/threads/#client-capability). ## Other surfaces diff --git a/fluxer_docs/src/content/docs/topics/uploads.md b/fluxer_docs/src/content/docs/topics/uploads.md index a72bb1576..a710a3490 100644 --- a/fluxer_docs/src/content/docs/topics/uploads.md +++ b/fluxer_docs/src/content/docs/topics/uploads.md @@ -68,6 +68,8 @@ Completion checks permissions and file size limits again. See the endpoint's [re Include the `upload_filename` in a [pre-uploaded attachment](/http-api/messages/#pre-uploaded-attachment-object) when calling [Create message](/http-api/messages/#create-message) or [Modify message](/http-api/messages/#modify-message). Uploading alone does not create a message or emit a Gateway event. +The `message` of a forum or media post in [Start thread](/http-api/threads/#start-thread) claims uploads the same way. Request the plan for the forum or media channel. + 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]