diff --git a/.claude/skills/docs-sync/SKILL.md b/.claude/skills/docs-sync/SKILL.md index b1479a99..75de6639 100644 --- a/.claude/skills/docs-sync/SKILL.md +++ b/.claude/skills/docs-sync/SKILL.md @@ -1,6 +1,6 @@ --- name: docs-sync -description: Sync the stream-node docs snippet tests (__tests__/docs-snippets) with a getstream.io docs checkout. Asks for the docs repo, first sweeps the docs for missing or mislabelled Node.js snippets (server-side code labelled JavaScript, JavaScript-only snippets the node SDK can run), then runs `yarn test:docs:sync` against it, brings the tests up to date with docs changes (docs win), adds tests for new Node.js snippets, and writes fixes for broken snippets back to the docs. Use when the user asks to sync, check or update the docs snippets or docs snippet tests. +description: Sync the stream-node docs snippet tests (__tests__/docs-snippets) with a getstream.io docs checkout. Asks for the docs repo, first sweeps the docs for missing or mislabelled Node.js snippets (server-side code labelled JavaScript, JavaScript-only snippets the node SDK can run), then runs `yarn test:docs:sync` against it, brings the tests up to date with docs changes (docs win), adds tests for new Node.js snippets, writes fixes for broken snippets back to the docs, and backfills missing stream-chat-js@9 fences from existing v9 code. Use when the user asks to sync, check or update the docs snippets or docs snippet tests. argument-hint: "[path to the getstream.io checkout or its content/docs folder]" --- @@ -72,8 +72,8 @@ To check what the node SDK can do, read `src/StreamClient.ts`, `src/StreamChatCl ### Writing the Node.js fence - Mirror the `JavaScript` fence: same comments, structure and placeholder literals (`"user-1"`, `"messaging:general"`). -- Use the node SDK's shapes and types: `client.chat.*` / `client.feeds.*`, request objects (`filter_conditions`, `preferences: [...]`), `Date` where the types say `Date` (not `.toISOString()`), and `user_id` where the client code acts as the connected user (server-side `queryChannels` with `hidden` / `muted`, drafts, devices need it). -- Don't write a `Node.js (stream-chat-js@9)` fence for it: list the group in the report so the v9 version can be added by hand. +- Use the node SDK's shapes and types: `client.chat.*` / `client.feeds.*`, request objects (`filter_conditions`, `preferences: [...]`), `Date` where the request types say `Date` (not `.toISOString()`), the SDK time helpers (`nsToDate`, `nsToMs`) for response timestamps (unix-ns numbers, not `Date`s), and `user_id` where the client code acts as the connected user (server-side `queryChannels` with `hidden` / `muted`, drafts, devices need it). +- Don't write its `Node.js (stream-chat-js@9)` partner here: step 6 backfills it from existing v9 code. - Put it before the group's ``. A fence that isn't in a `` block: wrap it and the new fence in ``. - From `$DOCS_DIR`, run `npx prettier --write ` and `node scripts/tab-lint.mjs ` (it puts the tabs in the canonical order: client SDKs first, then server SDKs). @@ -104,7 +104,7 @@ Work one docs page at a time. ### Mislabelled fence -Label it `Node.js` for `@stream-io/node-sdk` (server-side) code, `JavaScript` for client code. Code that works on both sides (e.g. a bare filter object) gets `JavaScript`: an unlabelled `js` fence already renders like `label="JavaScript"` (the JavaScript route's own sample, a titled fallback on the Node route), so the page doesn't change. Whether it also needs a `Node.js` tab is decided by the sweep (step 3). Otherwise, `JavaScript`, `React` and `React Native` fences, the stream-chat-js@9 fences (`JavaScript (stream-chat-js@9)`, `Node.js (stream-chat-js@9)`), and `Node.js` fences inside a `` block (reference implementations without the SDK), are out of scope: never test or change them. +Label it `Node.js` for `@stream-io/node-sdk` (server-side) code, `JavaScript` for client code. Code that works on both sides (e.g. a bare filter object) gets `JavaScript`: an unlabelled `js` fence already renders like `label="JavaScript"` (the JavaScript route's own sample, a titled fallback on the Node route), so the page doesn't change. Whether it also needs a `Node.js` tab is decided by the sweep (step 3). Otherwise, `JavaScript`, `React` and `React Native` fences, the stream-chat-js@9 fences (`JavaScript (stream-chat-js@9)`, `Node.js (stream-chat-js@9)`), and `Node.js` fences inside a `` block (reference implementations without the SDK), are out of scope: never test or change them. The one exception: step 6 adds missing stream-chat-js@9 fences. ### Region differs from its fence (docs changed) @@ -155,7 +155,37 @@ Only when the docs snippet itself doesn't work (wrong method name or signature, Docs bugs you leave unfixed (prose, non-`Node.js` fences, or waiting for a decision) go in the report (see "Report"). -## 6. Gates +## 6. Backfill stream-chat-js@9 fences + +The chat and platform pages show every stream-chat-js snippet twice: the current version (`JavaScript`, `Node.js`) and the v9 one (`JavaScript (stream-chat-js@9)`, `Node.js (stream-chat-js@9)`). Fences added on the docs `main` branch, or rewritten in this run, often arrive without their v9 partner. Run this step after the fixes, since they can relabel or rewrite fences. + +### Scope + +- Pages: the chat and platform sidebar pages. Feeds, video and moderation pages have no stream-chat-js code: skip them. +- Fences: every `JavaScript` and `Node.js` fence whose group has no partner with the matching v9 label. Skip comment-only fences, `` blocks, `EXCLUDED_FENCES`, and `Node.js` fences of APIs stream-chat-js never had (`client.video.*`, `client.feeds.*`, `client.moderation.*`). + +List them with a throwaway script in the scratchpad (page, heading, line, the group's labels). + +### Finding the v9 code + +Never write v9 code from scratch: copy it from one of these sources, in this order, and record which one you used. + +1. **The docs `main` branch** (`git fetch origin main`). `main` is the v9 docs: its plain `JavaScript` / `Node.js` / unlabelled `js` fence under the same heading is the v9 code. It only counts if it is v9 code (`serverClient`, `updateAppSettings`, positional arguments like `updateChannelType("messaging", {...})`, `channel.create()`). A fence identical to the branch's node SDK code (`payload: { filter_conditions }`, `client.chat.*`) means the page never had v9 code there. +2. **The page's history on the docs branch**: the fence before the commit that rewrote it for stream-chat-js@10 (`git log -S'' -- `, then `git show ^:`). +3. **A fence replaced in this run**: when the sweep rewrote a server-side `JavaScript` fence to a `Node.js` one, the old code (`git diff` in `$DOCS_DIR`) is the v9 partner. +4. **A `JavaScript` fence that is v9 server code** next to a `Node.js` fence (e.g. `client.partialUpdateUser(...)` beside `client.updateUsersPartial(...)`): relabel it `Node.js (stream-chat-js@9)`. +5. **The same call in both versions** (e.g. `client.blockUser("user-to-block")`): only when another fence on the docs pairs that exact code with an identical v9 copy. Copy it. + +No source: leave the group as it is and list it in the report ("never had v9 code"). Don't ask whether to write one. + +### Writing the v9 fence + +- Copy the code unchanged. Label it `JavaScript (stream-chat-js@9)` for a `JavaScript` fence, `Node.js (stream-chat-js@9)` for a `Node.js` fence. +- Put it right after its partner, in the same ``. A fence that isn't in a `` block: wrap it and the new fence in ``. +- When a group's fences match their source fences by position, copy them in that order. When they don't (some added, some removed), match them by heading and code, and leave any you can't pair. +- From `$DOCS_DIR`, run `npx prettier --write ` and `node scripts/tab-lint.mjs `, then `DOCS_DIR=$DOCS_DIR yarn test:docs:sync`: v9 fences aren't tested, but the check must stay green. + +## 7. Gates All must pass before you finish: @@ -176,14 +206,14 @@ Run `yarn test:docs` only on the test files you changed, never two runs at once, - Don't commit, push, stash or create branches in either repo. - Never write the API key, secret or push credentials to any file except the repo root `.env`. - Don't edit `src/`, `__tests__/docs-snippets/AGENTS.md`, the helpers or the configs. Propose changes in the report instead. -- Change docs snippets as little as possible: only when they don't work, or to add or relabel `Node.js` fences in the sweep (step 3). +- Change docs snippets as little as possible: only when they don't work, to add or relabel `Node.js` fences in the sweep (step 3), or to backfill stream-chat-js@9 fences (step 6). ## Report End with: 1. **Docs folder** used, and both repos' branches. -2. **Sweep**: `Node.js` fences added or relabelled, per page, and the groups you left without one and why (client SDKs only, a server-side section on the page, no node SDK equivalent, prose says client-side). +2. **Sweep**: `Node.js` fences added or relabelled, per page, and the groups you left without one and why (client SDKs only, a server-side section on the page, no node SDK equivalent, prose says client-side). Then the stream-chat-js@9 fences backfilled, per page, each with its source (`main`, the commit, a fence replaced in this run, a relabel), and the groups left without one because they never had v9 code. 3. **Tests updated from the docs**: per page, the regions changed and why the setup/assertions changed. 4. **New and removed tests**: snippets added, regions removed with their fences, renamed markers. 5. **Docs changes**: every fence changed in `$DOCS_DIR` (fixes, labels, client-side code), each with before/after, reason and evidence. These need human review. diff --git a/__tests__/docs-snippets/AGENTS.md b/__tests__/docs-snippets/AGENTS.md index 2f38809b..6b39333e 100644 --- a/__tests__/docs-snippets/AGENTS.md +++ b/__tests__/docs-snippets/AGENTS.md @@ -23,6 +23,7 @@ New entries are added only after the user approves them (agents propose them in - `helpers/call-types.ts`: `createCallType(client, cleanup)` creates a `uniqueId` call type, waits until it reads back, and registers it on `cleanup.callTypes`; `trackCallTypeCreates(client, cleanup)` does the same for fences that create the type themselves (spy, call before the region); `retryOnCallTypeMissing(fn)` retries calls into a new type (successful `getCallType` reads don't prove every node knows it: `getOrCreate` can still fail with code 16 "call type does not exist", wrap it in a spy inside regions). A call type with calls can't be deleted (code 4 "multiple calls of that type exist"): `Cleanup` hard-deletes `cleanup.calls` (waiting for the delete task) before `cleanup.callTypes`. `updateChannelType(client, name, patch)` (`helpers/channel-types.ts`) fills in the fields the request requires; `grantMessagingMembers` (`helpers/grants.ts`) adds `messaging` member grants and registers the restore. `helpers/server.ts` has repeated lookups: `sendServerMessage`, `getServerUser` / `getServerMember` / `getServerChannel` (throw unless exactly one result matches), `deletePollsCreatedBy`, `waitForAppSetting`, `waitForTeamIdSupport` (multi-tenancy toggles) `waitForReviewItem` (review queue item of an entity once its flags are there), `deleteFeedModerationTemplate` (the generated `v2DeleteTemplate()` can't send the name) and `saveAndRestoreEventHooks(client, cleanup)` (saves `event_hooks`, registers the restore, returns `{ original, restore }`; `waitForEventHooks`, `withoutTimestamps`). `helpers/feeds.ts`: `retryOnFeedGroupMissing`, `addActivityOnce`, `copyFeedGroup(client, cleanup, builtInId)` (a `uniqueId` group with a built-in group's settings, on `cleanup.feedGroups`), `notificationTypes` / `waitForNotificationTypes` (a user's built-in `notification` feed). `helpers/uploads.ts`: upload fixtures (`JPEG_BASE64` / `jpeg`, `MINIMAL_PDF`) and `ignoreMissingFile` (swallows the 404 of deleting a gone upload). - One `it` per docs snippet (or per closely related group of snippets in the same tab group). - Add basic `expect`s after each snippet to prove it did what the docs claim. +- **Timestamps**: server-sent dates on responses and events are unix-**nanosecond** numbers (`TimestampNS`), not `Date`s; request date fields still take a `Date`. Use the SDK's time helpers (`nsToMs` for arithmetic against `Date.now()`, `nsToDate` to pass one back in a request, `convertTimestampToDate` for optional ones), assert presence with `expect.any(Number)`, and compare two server timestamps directly. Never `new Date(t)` or `.getTime()` on a response field. ### Test app @@ -134,7 +135,7 @@ New entries are added only after the user approves them (agents propose them in - An app setting that changes API behavior (e.g. `enforce_unique_usernames`) can break later tests in the same file: restore it at the end of the test that changes it, and keep the `cleanup.add` restore as a fallback. - Video calls: create them with `uniqueId` ids and push the cid to `cleanup.calls` (hard deleted, "not found" ignored). Call types: `cleanup.callTypes` (see `helpers/call-types.ts`). - Feeds v3 works on the test app (feed groups `user`, `timeline`, `notification`, ...): create feeds with `uniqueId` ids (`client.feeds.feed(group, id).getOrCreate({ user_id })`) and push the fid (`group:id`) to `cleanup.feeds` (hard deleted, "not found" / "can't be found" ignored, so a snippet may delete it first). After a soft `deleteFeedUserData`, `getActivity` on the user's activity rejects with `metadata.responseCode: 404`. -- Feed groups are app-wide and eventually consistent: create them with a `uniqueId` id (hyphens are accepted) and push it to `cleanup.feedGroups` (deleted after `cleanup.feeds`). Calls into a new group can fail with code 16 "Feed group with ID ... not found" for a while (usually 1–2s): `retryOnFeedGroupMissing(client, ['getOrCreateFeed', 'follow', ...])` (`helpers/feeds.ts`) spies on the generated methods and retries them (`StreamFeed` methods go through `client.feeds`). `addActivity` into a new group can answer "not found" after storing the activity, so a blind retry adds it twice: in setup use `addActivityOnce(client, request)` (explicit id, retries only while `getActivity` doesn't find it). `updateFeedGroup` changes take a few seconds to reach reads: wait for several reads in a row to agree. An `updateFeedGroup` fence that leaves fields out clears them, including `aggregation` and `push_notification.push_types`. Feed group `ranking.type` accepts only `expression` and `interest` (code 4); recency is the default when `ranking` is omitted. `updateFeedGroup({ id })` with no other fields clears `custom` (updates replace every field). `deleteFeedGroup` without `hard_delete` soft-deletes (listed only with `include_soft_deleted: true`). +- Feed groups are app-wide and eventually consistent: create them with a `uniqueId` id (hyphens are accepted) and push it to `cleanup.feedGroups` (deleted after `cleanup.feeds`). Calls into a new group can fail with code 16 "Feed group with ID ... not found" for a while (usually 1–2s): `retryOnFeedGroupMissing(client, ['getOrCreateFeed', 'follow', ...])` (`helpers/feeds.ts`) spies on the generated methods and retries them (`StreamFeed` methods go through `client.feeds`). `addActivity` into a new group can answer "not found" after storing the activity, so a blind retry adds it twice: in setup use `addActivityOnce(client, request)` (explicit id, retries only while `getActivity` doesn't find it). A fence that calls `addActivity` into a group the setup created: `waitForFeedGroup(client, id)` first (several `getFeedGroup` reads in a row). `updateFeedGroup` changes take a few seconds to reach reads: wait for several reads in a row to agree. An `updateFeedGroup` fence that leaves fields out clears them, including `aggregation` and `push_notification.push_types`. Feed group `ranking.type` accepts only `expression` and `interest` (code 4); recency is the default when `ranking` is omitted. `updateFeedGroup({ id })` with no other fields clears `custom` (updates replace every field). `deleteFeedGroup` without `hard_delete` soft-deletes (listed only with `include_soft_deleted: true`). - Feed views are app-wide and eventually consistent: `uniqueId` ids and `cleanup.add(() => client.feeds.deleteFeedView({ id }))` (ignore "not found"). `getFeedView` and `getOrCreate({ view })` answer code 16 "... not found" for 1–3s after a create (`retryOnFeedGroupMissing` / `isFeedGroupMissing` match it), `listFeedViews` lags ~4s, `ranking.type: "recency"` is rejected, `updateFeedView({ id })` replaces every field. To read a spy that `retryOnFeedGroupMissing` installed, call `vi.spyOn(client.feeds, 'method')` again (returns the existing mock); `vi.mocked(client.feeds.method)` fails `unbound-method`. - `updateApp({ max_aggregated_activities_length })` returns 200 but `getApp` keeps 0 and the limit isn't applied (2026-10-16); values over 100 are rejected (code 4). - Feed ranking: ranked results settle within ~1–3s; make test popularity the reverse of recency so a passing order proves the ranking. Popularity = reactions (one per type per user: use several types) + shares × 3. Ranking expressions are validated on create (unknown variables such as `externals`, fields without defaults, non-map arguments to map functions, `min_popularity` < 1 for the popular selector). `preference_score` is 0 until the user gives `activityFeedback`. A group created without `activity_selectors` defaults to `current_feed`. Map key order in responses (`ranking.functions`) isn't stable: sort before comparing. A duplicate `createFeedGroup` id (or a built-in id like `timeline`) fails with "already exists": a docs bug. @@ -233,7 +234,7 @@ New entries are added only after the user approves them (agents propose them in - A new channel type starts with empty `guest` grants (no `messaging` defaults). To test removing guest grants on a `uniqueId` type, first copy the guest grants from the built-in type (read only), then `waitForChannelTypePropagation()`; with create + update waits, give that `beforeAll` a 180s timeout. - Uploads are checked by content as well as extension: a tiny CSV (`a,b`) is read as `text/plain` and fails "File type text/plain is not supported" under `allowed_mime_types: ["text/csv"]`. Use a few rows of real CSV. - To prove `disable_auth_checks`, send a dev token (`
..devtoken`, base64url JSON) to `POST /channels?api_key=...` with `stream-auth-type: jwt`: 201 while checks are off, 401 when on. The 401 message differs per API node ("development tokens are not allowed for this application" or "Token signature is invalid", the latter for over a minute on some nodes): accept both, and `retry` until the status matches. -- The same request proves a user token works or is revoked: `POST /channels?api_key=...` with `Authorization: ` and `stream-auth-type: jwt` returns 201 when accepted and 401 code 40 "This token has been revoked, please generate a new token" after a revocation (takes effect within seconds: `retry`). The SDK sets `iat` one second in the past by default, so a token made right after revoking with `new Date()` can still count as revoked: retry the fresh-token check, or give the tokens to revoke an `iat` well in the past. `revoke_tokens_issued_before` reads back as a `Date` that can be 1 ms off: compare with a ±1 ms tolerance. +- The same request proves a user token works or is revoked: `POST /channels?api_key=...` with `Authorization: ` and `stream-auth-type: jwt` returns 201 when accepted and 401 code 40 "This token has been revoked, please generate a new token" after a revocation (takes effect within seconds: `retry`). The SDK sets `iat` one second in the past by default, so a token made right after revoking with `new Date()` can still count as revoked: retry the fresh-token check, or give the tokens to revoke an `iat` well in the past. `revoke_tokens_issued_before` reads back (unix ns) up to 1 ms off the `Date` sent: compare `nsToMs(...)` with a ±1 ms tolerance. - `file_upload_config` / `image_upload_config` come back with every list present (`[]`) and `size_limit: 0`: restore the saved original and `waitForAppSetting` (deep compare) for it. - A new custom role isn't known to every API node right away: `updateUsersPartial({ set: { role } })` just after `createRole` can fail with code 4 `role "..." provided but not defined`. Spy on `client.updateUsersPartial` with a `mockImplementation` that calls the bound original through `retry(..., { retryIf: })`. `deleteRole` right after `createRole` fails the same way with `role "..." does not exist`: retry it like that on `'does not exist'`. - `updateChannelType({ grants: null })` resets grants to the API defaults, which differ from a new type's initial grants (they add e.g. `create-attachment`, `send-poll`, `create-mention`): assert against the update response. New and built-in types start with `guest: []` and `anonymous: []`: to prove a removal snippet works, give those roles grants first. diff --git a/__tests__/docs-snippets/global-setup.ts b/__tests__/docs-snippets/global-setup.ts index 76de4943..5a776d21 100644 --- a/__tests__/docs-snippets/global-setup.ts +++ b/__tests__/docs-snippets/global-setup.ts @@ -5,6 +5,7 @@ import { createdAtFromId, DOCS_TEST_PREFIX } from './helpers/ids'; import { describeLeftovers, findLeftovers, + leftoverCreatedAt, removeLeftovers, } from './helpers/leaks'; import type { Leftover } from './helpers/leaks'; @@ -28,7 +29,7 @@ const createdAt = (leftover: Leftover) => { // anything else falls back to the server timestamp. const fromId = createdAtFromId(leftover.id.split(' ')[0]); if (fromId) return fromId; - return leftover.createdAt ? new Date(leftover.createdAt) : undefined; + return leftoverCreatedAt(leftover); }; const loadEnv = () => { diff --git a/__tests__/docs-snippets/helpers/feeds.ts b/__tests__/docs-snippets/helpers/feeds.ts index e7913463..4a16b1ab 100644 --- a/__tests__/docs-snippets/helpers/feeds.ts +++ b/__tests__/docs-snippets/helpers/feeds.ts @@ -40,6 +40,18 @@ export const retryOnFeedGroupMissing = ( } }; +/** + * Waits until a new feed group can be read back several times in a row, for setup + * before a region that calls `addActivity` into it (which can't be retried blindly). + */ +export const waitForFeedGroup = async (client: StreamClient, id: string) => { + for (let i = 0; i < 5; i++) { + await retry(() => client.feeds.getFeedGroup({ id }), { + retryIf: isFeedGroupMissing, + }); + } +}; + /** * Adds an activity (setup only) into a feed whose group may be new. `addActivity` can * answer "Feed group ... not found" after it already stored the activity, so a blind diff --git a/__tests__/docs-snippets/helpers/leaks.ts b/__tests__/docs-snippets/helpers/leaks.ts index 977fb4df..8e365e96 100644 --- a/__tests__/docs-snippets/helpers/leaks.ts +++ b/__tests__/docs-snippets/helpers/leaks.ts @@ -1,4 +1,9 @@ -import type { StreamClient } from '../../../index'; +import { nsToDate } from '../../../index'; +import type { + PushProviderName, + StreamClient, + TimestampNS, +} from '../../../index'; import { DOCS_TEST_PREFIX } from './ids'; import { deleteMembershipLevel } from './feeds'; import { @@ -8,12 +13,23 @@ import { import { hardDeleteUsers } from './users'; import { retry, retryOnRateLimit, waitForTask } from './wait'; +/** A leftover's `createdAt` as a `Date`, or `undefined` when it has none. */ +export const leftoverCreatedAt = ({ createdAt }: Leftover) => { + if (createdAt === undefined) return undefined; + return typeof createdAt === 'number' + ? nsToDate(createdAt) + : new Date(createdAt); +}; + /** Something a docs test created and didn't remove. */ export type Leftover = { kind: string; id: string; - /** Used by the sweep to skip resources of runs that may still be in progress. */ - createdAt?: Date | string; + /** + * Used by the sweep to skip resources of runs that may still be in progress. A unix-ns + * timestamp from a response, or an RFC3339 string; read it with `leftoverCreatedAt`. + */ + createdAt?: number | string; remove: () => Promise; /** * Removes many leftovers of this kind in one call (e.g. users in one `deleteUsers`). @@ -112,7 +128,7 @@ const scanners: Record = { async channels(client, { matches, ids: channelIds = [], createdSince }) { const found = new Map(); - const add = (channel: { cid: string; created_at: Date }) => + const add = (channel: { cid: string; created_at: number }) => found.set(channel.cid, { kind: 'channel', id: channel.cid, @@ -337,7 +353,9 @@ const scanners: Record = { createdAt: provider.created_at, remove: () => client.deletePushProvider({ - type: provider.type, + // The spec types `PushProviderResponse.type` as a plain string, but the + // delete endpoint takes a `PushProviderName`: the listed type is one of them. + type: provider.type as PushProviderName, name: provider.name, }), })); @@ -402,12 +420,17 @@ const scanners: Record = { // without one it fails with code 4 "cannot specify sort and next/prev at the same // time" (2026-10-16). const scan = async (filter_conditions: Record) => { - let after: Date | undefined; + let after: TimestampNS | undefined; for (;;) { const response = await retryOnRateLimit(() => client.video.queryCalls({ filter_conditions: after - ? { $and: [filter_conditions, { created_at: { $gt: after } }] } + ? { + $and: [ + filter_conditions, + { created_at: { $gt: nsToDate(after) } }, + ], + } : filter_conditions, sort: [{ field: 'created_at', direction: 1 }], limit: 100, diff --git a/__tests__/docs-snippets/maintenance/sweep.ts b/__tests__/docs-snippets/maintenance/sweep.ts index 7a2bc3df..f99d701a 100644 --- a/__tests__/docs-snippets/maintenance/sweep.ts +++ b/__tests__/docs-snippets/maintenance/sweep.ts @@ -4,6 +4,7 @@ import { createdAtFromId, DOCS_TEST_PREFIX } from '../helpers/ids'; import { describeLeftovers, findLeftovers, + leftoverCreatedAt, removeLeftovers, } from '../helpers/leaks'; import type { Leftover } from '../helpers/leaks'; @@ -20,9 +21,8 @@ const dryRun = process.env.DOCS_SWEEP_DRY_RUN === '1'; const minAgeMinutes = Number(process.env.DOCS_SWEEP_MIN_AGE ?? 30); const isOldEnough = (leftover: Leftover) => { - const createdAt = leftover.createdAt - ? new Date(leftover.createdAt) - : createdAtFromId(leftover.id.split(' ')[0]); + const createdAt = + leftoverCreatedAt(leftover) ?? createdAtFromId(leftover.id.split(' ')[0]); // Unknown age: only sweep it when explicitly asked to ignore age. if (!createdAt || Number.isNaN(createdAt.getTime())) return minAgeMinutes === 0; diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/01-basics/01-quickstart.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/01-basics/01-quickstart.test.ts index abbb7e49..f73aadaf 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/01-basics/01-quickstart.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/01-basics/01-quickstart.test.ts @@ -185,7 +185,7 @@ describe(DOCS, () => { return response; }); expect(after.notification_status?.unread).toBe(0); - expect(after.notification_status?.last_read_at).toBeInstanceOf(Date); + expect(after.notification_status?.last_read_at).toEqual(expect.any(Number)); }); it('creates a poll, attaches it to an activity and votes', async () => { diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/01-feed-groups.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/01-feed-groups.test.ts index 24f0d59e..3c131a4f 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/01-feed-groups.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/01-feed-groups.test.ts @@ -440,6 +440,6 @@ describe(DOCS, () => { }); return all.groups[feedGroupId]; }); - expect(deleted?.deleted_at).toBeInstanceOf(Date); + expect(deleted?.deleted_at).toEqual(expect.any(Number)); }); }); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/04-custom_ranking.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/04-custom_ranking.test.ts index 1aa27b6d..c535fc2a 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/04-custom_ranking.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/04-custom_ranking.test.ts @@ -19,7 +19,6 @@ import type { import { getServerClient } from '../../../../helpers/clients'; import { Cleanup } from '../../../../helpers/cleanup'; import { - addActivityOnce, isFeedGroupMissing, retryOnFeedGroupMissing, } from '../../../../helpers/feeds'; @@ -53,11 +52,11 @@ const createUser = async (name: string) => { * user feed with three activities, posted oldest first (a0, a1, a2), so * recency alone would order them a2, a1, a0: * - * | activity | popularity | custom | interest_tags | - * | -------- | -------------------- | ------------------------------------------- | ------------- | - * | a0 | 5 (5 reactions) | stock: tesla, stocks: [] | exercise | - * | a1 | 4 (4 reactions) | isBoosted, stock: nokia, stocks: [microsoft] | food | - * | a2 | 3 (1 share, 3 points) | stock: apple, stocks: [apple, tesla] | travel | + * | activity | popularity | + * | -------- | --------------------- | + * | a0 | 5 (5 reactions) | + * | a1 | 4 (4 reactions) | + * | a2 | 3 (1 share, 3 points) | */ let authorId: string; let readerId: string; @@ -121,12 +120,7 @@ describe(DOCS, () => { // (activities in new groups are added with `addActivityOnce`: a retried // addActivity can add them twice). beforeEach(() => - retryOnFeedGroupMissing(client, [ - 'getOrCreateFeed', - 'follow', - 'markActivity', - 'activityFeedback', - ]), + retryOnFeedGroupMissing(client, ['getOrCreateFeed', 'follow']), ); afterEach(() => vi.restoreAllMocks()); afterAll(() => cleanup.run()); @@ -140,33 +134,19 @@ describe(DOCS, () => { .feed('user', authorId) .getOrCreate({ user_id: authorId }); await client.feeds.feed('user', fanId).getOrCreate({ user_id: fanId }); - const post = async ( - name: string, - custom: Record, - tag: string, - ) => { + const post = async (name: string) => { const { activity } = await client.feeds.addActivity({ type: 'post', feeds: [`user:${authorId}`], text: name, - custom, - interest_tags: [tag], user_id: authorId, }); names.set(activity.id, name); return activity.id; }; - const a0 = await post('a0', { stock: 'tesla', stocks: [] }, 'exercise'); - const a1 = await post( - 'a1', - { isBoosted: true, stock: 'nokia', stocks: ['microsoft'] }, - 'food', - ); - const a2 = await post( - 'a2', - { stock: 'apple', stocks: ['apple', 'tesla'] }, - 'travel', - ); + const a0 = await post('a0'); + const a1 = await post('a1'); + const a2 = await post('a2'); const types = ['like', 'love', 'wow', 'haha', 'sad']; for (const type of types) { await client.feeds.addActivityReaction({ @@ -282,622 +262,6 @@ describe(DOCS, () => { } }); - it('ranks with custom data and defaults', async () => { - const myId = newFeedGroupId('myid'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Activity data" tab="Node.js" index=1 - // COPY: myId="myid" - await serverClient.feeds.createFeedGroup({ - id: myId, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - score: 'popularity * (custom.isBoosted ? 1.5 : 1)', - defaults: { - custom: { - isBoosted: false, - }, - }, - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(feed_group.ranking?.defaults).toEqual({ - custom: { isBoosted: false }, - }); - // a1 (popularity 4) is boosted to 6, above a0 (5). - const feed = await rankedFeed(myId, ['a1', 'a0', 'a2']); - expect(byName(feed).a1.score).toBe(6); - // Without the default, the expression is rejected. - await expect( - client.feeds.createFeedGroup({ - id: newFeedGroupId('no-default'), - ranking: { - type: 'expression', - score: 'popularity * (custom.isBoosted ? 1.5 : 1)', - }, - }), - ).rejects.toThrow('expression references fields without defaults'); - }); - - it('ranks with decay functions', async () => { - const myId = newFeedGroupId('myid'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Parameters" tab="Node.js" index=1 - // COPY: myId="myid" - await serverClient.feeds.createFeedGroup({ - id: myId, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - functions: { - simple_gauss: { - base: 'decay_gauss', - scale: '5d', - offset: '1d', - decay: '0.3', - }, - popularity_gauss: { - base: 'decay_gauss', - scale: '100', - offset: '5', - decay: '0.5', - }, - }, - score: 'simple_gauss(time)*popularity_gauss(popularity)', - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(Object.keys(feed_group.ranking?.functions ?? {}).sort()).toEqual([ - 'popularity_gauss', - 'simple_gauss', - ]); - const feed = await followAuthor(myId); - const response = await retry(async () => { - const r = await feed.getOrCreate({ user_id: readerId }); - if (ours(r).length !== 3) throw new Error('activities missing'); - return r; - }); - // (2026-10-16) Every activity scores 0: popularity_gauss has no - // `origin`, which defaults to now (a UNIX timestamp), so popularity is - // never near it. - expect(ours(response).map((a) => a.score)).toEqual([0, 0, 0]); - // simple_gauss alone gives activities younger than the offset a 1. - const timeOnlyId = newFeedGroupId('time-only'); - await client.feeds.createFeedGroup({ - id: timeOnlyId, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - functions: feed_group.ranking?.functions, - score: 'simple_gauss(time)', - }, - }); - const timeOnly = await followAuthor(timeOnlyId); - const timeOnlyResponse = await retry(async () => { - const r = await timeOnly.getOrCreate({ user_id: readerId }); - if (ours(r).length !== 3) throw new Error('activities missing'); - return r; - }); - expect(ours(timeOnlyResponse).map((a) => a.score)).toEqual([1, 1, 1]); - }); - - it('ranks by interest weights', async () => { - const myId = newFeedGroupId('myid'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Interest weights" tab="Node.js" index=1 - // COPY: myId="myid" - await serverClient.feeds.createFeedGroup({ - id: myId, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'interest', - score: 'interest_score * popularity', - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(feed_group.ranking?.type).toBe('interest'); - - const timeline = await followAuthor(myId); - // The reader has no stored interests: every interest_score is 0. - const plain = await retry(async () => { - const r = await timeline.getOrCreate({ user_id: readerId }); - if (ours(r).length !== 3) throw new Error('activities missing'); - return r; - }); - expect(ours(plain).map((a) => a.score)).toEqual([0, 0, 0]); - // Settle the ranking with the docs' weights before the region. - await waitForOrder(timeline, ['a2', 'a1', 'a0'], { - interest_weights: { travel: 1, food: 0.5, exercise: -1 }, - }); - // The spy retryOnFeedGroupMissing installed. - const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); - getOrCreate.mockClear(); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="User interests" tab="Node.js" index=1 - // COPY: readerId="" - await timeline.getOrCreate({ - interest_weights: { - travel: 1, - food: 0.5, - exercise: -1, - }, - user_id: readerId, - }); - // #endregion snippet - - const response = await (getOrCreate.mock.results[0] - .value as Promise); - // travel (a2) > food (a1) > exercise (a0, a negative score). - expect(order(response)).toEqual(['a2', 'a1', 'a0']); - const got = byName(response); - expect(got.a1.score).toBeGreaterThan(0); - expect(got.a0.score).toBeLessThan(0); - }); - - it('ranks by preference score', async () => { - const myId = newFeedGroupId('myid'); - const myIdInterest = newFeedGroupId('myid-interest'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Preference score" tab="Node.js" index=1 - // COPY: myId="myid", myIdInterest="myid_interest" - // Using preference_score with expression type - await serverClient.feeds.createFeedGroup({ - id: myId, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - score: 'preference_score * popularity', - }, - }); - - // Using preference_score with interest type - await serverClient.feeds.createFeedGroup({ - id: myIdInterest, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'interest', - score: 'preference_score * interest_score * popularity', - }, - }); - // #endregion snippet - - const groups = await createdGroups(create); - expect(groups.map((g) => g.feed_group.ranking?.type)).toEqual([ - 'expression', - 'interest', - ]); - // A user of their own: activity feedback changes their preferences. - const userId = await createUser('preferences'); - const feed = await followAuthor(myId, userId); - // Without activity feedback preference_score is 0 for every activity. - const before = await retry(async () => { - const r = await feed.getOrCreate({ user_id: userId }); - if (ours(r).length !== 3) throw new Error('activities missing'); - return r; - }); - expect(ours(before).map((a) => a.score)).toEqual([0, 0, 0]); - // "Show more" on a2 (travel) moves it to the top. - await client.feeds.activityFeedback({ - activity_id: byName(before).a2.id, - show_more: true, - user_id: userId, - }); - const after = await waitForOrder(feed, ['a2', 'a1', 'a0'], {}, userId); - expect(byName(after).a2.score).toBeGreaterThan(0); - expect(byName(after).a1.score).toBe(0); - }); - - it('ranks by selector source', async () => { - const selectorTimelineId = newFeedGroupId('selector-timeline'); - const multiSelectorTimelineId = newFeedGroupId('multi-selector-timeline'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Ranking by Selector Source" tab="Node.js" index=1 - // COPY: selectorTimelineId="selector_timeline", multiSelectorTimelineId="multi_selector_timeline" - // Example 1: Simple boost - following activities get 2x multiplier - await serverClient.feeds.createFeedGroup({ - id: selectorTimelineId, - activity_selectors: [ - { type: 'following' }, - { type: 'popular', min_popularity: 1 }, - ], - ranking: { - type: 'expression', - score: 'selector_source == "following" ? popularity * 2 : popularity', - defaults: { - selector_source: '', // Required when using selector_source in expressions - }, - }, - }); - - // Example 2: Multiple selectors with different bonuses - await serverClient.feeds.createFeedGroup({ - id: multiSelectorTimelineId, - activity_selectors: [ - { type: 'following' }, - { type: 'interest' }, - { type: 'popular', min_popularity: 1 }, - ], - ranking: { - type: 'expression', - score: ` - (selector_source == "following" ? 10 : 0) + - (selector_source == "interest" ? 5 : 0) + - popularity - `, - defaults: { - selector_source: '', - }, - }, - }); - // #endregion snippet - - const groups = await createdGroups(create); - expect(groups.map((g) => g.feed_group.activity_selectors?.length)).toEqual([ - 2, 3, - ]); - // The author's activities come from the following selector (the first - // one): their popularity is doubled / gets +10. - const first = await rankedFeed(selectorTimelineId, ['a0', 'a1', 'a2']); - for (const activity of ours(first)) { - expect(activity.selector_source).toBe('following'); - expect(activity.score).toBe(activity.popularity * 2); - } - const second = await rankedFeed(multiSelectorTimelineId, [ - 'a0', - 'a1', - 'a2', - ]); - for (const activity of ours(second)) { - expect(activity.selector_source).toBe('following'); - expect(activity.score).toBe(activity.popularity + 10); - } - }); - - it('ranks read activities lower', async () => { - const rankedTimelineId = newFeedGroupId('ranked-timeline'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Ranking by Read/Seen Status" tab="Node.js" index=1 - // COPY: rankedTimelineId="ranked_timeline" - await serverClient.feeds.createFeedGroup({ - id: rankedTimelineId, - activity_marks: { - track_seen: true, - track_read: true, - }, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - score: 'popularity - (is_read ? 100 : 0)', - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(feed_group.activity_marks).toMatchObject({ track_read: true }); - const feed = await followAuthor(rankedTimelineId); - const unread = await waitForOrder(feed, ['a0', 'a1', 'a2']); - const a0 = byName(unread).a0.id; - await feed.markActivity({ mark_read: [a0], user_id: readerId }); - // The most popular activity drops to the bottom once read. - const response = await waitForOrder(feed, ['a1', 'a2', 'a0']); - expect(byName(response).a0).toMatchObject({ is_read: true, score: -95 }); - }); - - it('ranks unseen activities first', async () => { - const unseenTimelineId = newFeedGroupId('unseen-timeline'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Ranking by Read/Seen Status" tab="Node.js" index=2 - // COPY: unseenTimelineId="unseen_timeline" - await serverClient.feeds.createFeedGroup({ - id: unseenTimelineId, - activity_marks: { - track_seen: true, - track_read: true, - }, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - score: 'is_seen ? 0 : 100', - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(feed_group.ranking?.score).toBe('is_seen ? 0 : 100'); - const feed = await followAuthor(unseenTimelineId); - const before = await retry(async () => { - const r = await feed.getOrCreate({ user_id: readerId }); - if (ours(r).length !== 3) throw new Error('activities missing'); - return r; - }); - expect(ours(before).map((a) => a.score)).toEqual([100, 100, 100]); - const a1 = byName(before).a1.id; - await feed.markActivity({ mark_seen: [a1], user_id: readerId }); - const response = await retry(async () => { - const r = await feed.getOrCreate({ user_id: readerId }); - if (order(r)[2] !== 'a1') throw new Error('a1 not last yet'); - return r; - }); - expect(byName(response).a1).toMatchObject({ is_seen: true, score: 0 }); - }); - - it('ranks with external data', async () => { - const myId = newFeedGroupId('myid'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Accessing external data in ranking expressions" tab="Node.js" index=1 - // COPY: myId="myid" - await serverClient.feeds.createFeedGroup({ - id: myId, - ranking: { - type: 'expression', - score: - 'popularity * external.popularity_multiplier + share_count * external.share_multiplier', - defaults: { - external: { - // Provide default values if external data isn't provided when reading the feed - popularity_multiplier: 1, - share_multiplier: 1, - }, - }, - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(feed_group.ranking?.defaults).toEqual({ - external: { popularity_multiplier: 1, share_multiplier: 1 }, - }); - // No selectors: the group ranks the feed's own activities - // (current_feed). b0 has popularity 5, b1 one share (popularity 3). - cleanup.feeds.push(`${myId}:${readerId}`); - const timeline = client.feeds.feed(myId, readerId); - await retry(() => timeline.getOrCreate({ user_id: readerId }), { - retryIf: isFeedGroupMissing, - }); - const post = async (name: string) => { - const activityId = await addActivityOnce(client, { - type: 'post', - feeds: [timeline.feed], - text: name, - user_id: authorId, - }); - names.set(activityId, name); - return activityId; - }; - const b0 = await post('b0'); - const b1 = await post('b1'); - for (const type of ['like', 'love', 'wow', 'haha', 'sad']) { - await client.feeds.addActivityReaction({ - activity_id: b0, - type, - user_id: readerId, - }); - } - await addActivityOnce(client, { - type: 'post', - feeds: [timeline.feed], - parent_id: b1, - text: 'shared', - user_id: authorId, - }); - // With the defaults: b0 5, b1 3 + 1 share. - const plain = await waitForOrder(timeline, ['b0', 'b1']); - expect(byName(plain).b1.score).toBe(4); - await waitForOrder(timeline, ['b1', 'b0'], { - external_ranking: { popularity_multiplier: 1, share_multiplier: 100 }, - }); - // The spy retryOnFeedGroupMissing installed. - const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); - getOrCreate.mockClear(); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Providing external data when reading the feed" tab="Node.js" index=1 - // COPY: readerId="" - await timeline.getOrCreate({ - external_ranking: { - popularity_multiplier: 1, // normal weight for popularity - share_multiplier: 100, // very high boost for share_count - }, - user_id: readerId, - }); - // #endregion snippet - - const response = await (getOrCreate.mock.results[0] - .value as Promise); - // The shared activity jumps to the top: 3 + 1 * 100. - expect(order(response)).toEqual(['b1', 'b0']); - expect(byName(response).b1.score).toBe(103); - }); - - it('ranks with map_lookup', async () => { - const myId = newFeedGroupId('myid'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Arrays and Maps" tab="Node.js" index=1 - // COPY: myId="myid" - await serverClient.feeds.createFeedGroup({ - id: myId, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - score: 'popularity + map_lookup(stock, external.stock_boosts)', - defaults: { - stock: '', - external: { - stock_boosts: {}, - }, - }, - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(feed_group.ranking?.defaults).toEqual({ - stock: '', - external: { stock_boosts: {} }, - }); - const timeline = await followAuthor(myId); - await waitForOrder(timeline, ['a0', 'a1', 'a2']); - // The spy retryOnFeedGroupMissing installed. - const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); - getOrCreate.mockClear(); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Arrays and Maps" tab="Node.js" index=2 - // COPY: readerId="" - await timeline.getOrCreate({ - external_ranking: { - stock_boosts: { - apple: 0.9, - tesla: 0.5, - microsoft: 0.3, - }, - }, - user_id: readerId, - }); - // #endregion snippet - - const response = await (getOrCreate.mock.results[0] - .value as Promise); - // a0 (tesla) 5 + 0.5, a1 (nokia, not in the map) 4 + 0, a2 (apple) 3 + 0.9. - const got = byName(response); - expect(got.a0.score).toBeCloseTo(5.5, 5); - expect(got.a1.score).toBeCloseTo(4, 5); - expect(got.a2.score).toBeCloseTo(3.9, 5); - }); - - it('ranks with sum_map_lookup', async () => { - const myId = newFeedGroupId('myid'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Arrays and Maps" tab="Node.js" index=3 - // COPY: myId="myid" - await serverClient.feeds.createFeedGroup({ - id: myId, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - score: 'popularity + sum_map_lookup(stocks, external.stock_boosts)', - defaults: { - stocks: [], - external: { - stock_boosts: {}, - }, - }, - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(feed_group.ranking?.defaults).toEqual({ - stocks: [], - external: { stock_boosts: {} }, - }); - const timeline = await followAuthor(myId); - // a0 (no stocks) 5, a1 (microsoft) 4 + 2, a2 (apple, tesla) 3 + 15. - await waitForOrder(timeline, ['a2', 'a1', 'a0'], { - external_ranking: { - stock_boosts: { apple: 10.0, tesla: 5.0, microsoft: 2.0 }, - }, - }); - // The spy retryOnFeedGroupMissing installed. - const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); - getOrCreate.mockClear(); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Arrays and Maps" tab="Node.js" index=4 - // COPY: readerId="" - await timeline.getOrCreate({ - external_ranking: { - stock_boosts: { - apple: 10.0, - tesla: 5.0, - microsoft: 2.0, - }, - }, - user_id: readerId, - }); - - // score = popularity + 10.0 + 5.0 = popularity + 15.0 (for activity with stocks: ['apple', 'tesla']) - // #endregion snippet - - const response = await (getOrCreate.mock.results[0] - .value as Promise); - expect(order(response)).toEqual(['a2', 'a1', 'a0']); - const got = byName(response); - expect(got.a2.score).toBe(18); - expect(got.a1.score).toBe(6); - expect(got.a0.score).toBe(5); - }); - - it('ranks with in_array', async () => { - const myId = newFeedGroupId('myid'); - const create = vi.spyOn(client.feeds, 'createFeedGroup'); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Arrays and Maps" tab="Node.js" index=5 - // COPY: myId="myid" - await serverClient.feeds.createFeedGroup({ - id: myId, - activity_selectors: [{ type: 'following' }], - ranking: { - type: 'expression', - score: 'popularity + in_array(stock, external.watchlist) * 10', - defaults: { - stock: '', - external: { - watchlist: [], - }, - }, - }, - }); - // #endregion snippet - - const [{ feed_group }] = await createdGroups(create); - expect(feed_group.ranking?.defaults).toEqual({ - stock: '', - external: { watchlist: [] }, - }); - const timeline = await followAuthor(myId); - // a0 (tesla) 5 + 10, a1 (nokia) 4, a2 (apple) 3 + 10. - await waitForOrder(timeline, ['a0', 'a2', 'a1'], { - external_ranking: { watchlist: ['apple', 'tesla', 'microsoft'] }, - }); - // The spy retryOnFeedGroupMissing installed. - const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); - getOrCreate.mockClear(); - - // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/04-custom_ranking.md" heading="Arrays and Maps" tab="Node.js" index=6 - // COPY: readerId="" - await timeline.getOrCreate({ - external_ranking: { - watchlist: ['apple', 'tesla', 'microsoft'], - }, - user_id: readerId, - }); - - // activities with stocks in the watchlist get a +10 boost, others get no boost. - // #endregion snippet - - const response = await (getOrCreate.mock.results[0] - .value as Promise); - expect(order(response)).toEqual(['a0', 'a2', 'a1']); - const got = byName(response); - expect(got.a0.score).toBe(15); - expect(got.a2.score).toBe(13); - expect(got.a1.score).toBe(4); - }); - it('inspects ranking variables', async () => { // The docs' "sara": a timeline that follows the author. const saraId = await createUser('sara'); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/05-activity_selectors.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/05-activity_selectors.test.ts index 46637150..96f2598e 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/05-activity_selectors.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/05-activity_selectors.test.ts @@ -9,6 +9,7 @@ import { vi, } from 'vitest'; import type { + CreateFeedViewResponse, GetOrCreateFeedRequest, GetOrCreateFeedResponse, UpdateFeedGroupResponse, @@ -44,6 +45,8 @@ const createUser = async (name: string) => { }; const amsterdam = { lat: 52.373558, lng: 4.885261 }; +// About 17 km west of `amsterdam`. +const haarlem = { lat: 52.3874, lng: 4.6462 }; const withinAmsterdam = { within_bounds: { $eq: { @@ -359,6 +362,130 @@ describe(DOCS, () => { ); }); + it('reads a proximity feed around a viewer location', async () => { + // The docs read john's timeline; the built-in timeline group has no + // proximity selector, so the fence reads a uniqueId group with one. + const timelineId = newFeedGroupId('timeline'); + await client.feeds.createFeedGroup({ + id: timelineId, + activity_selectors: [{ type: 'proximity' }], + }); + const johnId = await createUser('john'); + cleanup.feeds.push(`${timelineId}:${johnId}`); + // john has no stored location: without viewer_location the read fails. + await expect( + client.feeds.getOrCreateFeed({ + feed_group_id: timelineId, + feed_id: johnId, + user_id: johnId, + }), + ).rejects.toThrow('no near or within_bounds filter provided'); + const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); + getOrCreate.mockClear(); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/05-activity_selectors.md" heading="Viewer location" tab="Node.js" index=1 + // COPY: timelineId="timeline", johnId="john" + await client.feeds.getOrCreateFeed({ + feed_group_id: timelineId, + feed_id: johnId, + user_id: johnId, + viewer_location: { lat: 52.373558, lng: 4.885261 }, + }); + // #endregion snippet + + const response = await (getOrCreate.mock.results[0] + .value as Promise); + expect(response.feed.feed).toBe(`${timelineId}:${johnId}`); + // hot and warm are posted in Amsterdam, within the default 10 km. + expect([...ours(response)].sort()).toEqual(['hot', 'warm']); + // Haarlem, ~17 km away: outside the default radius. + const far = await client.feeds.getOrCreateFeed({ + feed_group_id: timelineId, + feed_id: johnId, + user_id: johnId, + viewer_location: haarlem, + }); + expect(ours(far)).toEqual([]); + // Out-of-range coordinates are rejected with a 400. + await expect( + client.feeds.getOrCreateFeed({ + feed_group_id: timelineId, + feed_id: johnId, + user_id: johnId, + viewer_location: { lat: 91, lng: 4.885261 }, + }), + ).rejects.toMatchObject({ metadata: { responseCode: 400 } }); + }); + + it('creates a feed view with a proximity radius', async () => { + const nearbyId = uniqueId('nearby'); + cleanup.add(() => + client.feeds.deleteFeedView({ id: nearbyId }).catch((error: unknown) => { + if (!String(error).includes('not found')) throw error; + }), + ); + const create = vi.spyOn(client.feeds, 'createFeedView'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/05-activity_selectors.md" heading="Viewer location" tab="Node.js" index=2 + // COPY: nearbyId="nearby" + await client.feeds.createFeedView({ + id: nearbyId, + activity_selectors: [ + { + type: 'proximity', + // Search 50 miles around the viewer's location + viewer_location_radius_km: 80.4672, + filter: { activity_type: { $in: ['post', 'event'] } }, + }, + ], + }); + // #endregion snippet + + const { feed_view } = await (create.mock.results[0] + .value as Promise); + // (2026-10-29) The API drops viewer_location_radius_km: the view is + // stored without it (also on getFeedView, and a radius of 300 is + // accepted although the docs say at most 250). + expect(feed_view.activity_selectors).toEqual([ + { + type: 'proximity', + filter: { activity_type: { $in: ['post', 'event'] } }, + }, + ]); + // Read a plain group through the view around Amsterdam: hot and warm + // (posts). A workout in Amsterdam is left out by the activity_type filter. + const workoutId = await addActivityOnce(client, { + type: 'workout', + feeds: [`user:${authorId}`], + text: 'nearby workout', + location: amsterdam, + user_id: authorId, + }); + const plainId = newFeedGroupId('plain'); + await client.feeds.createFeedGroup({ id: plainId }); + const feed = readerFeed(plainId); + // The view is new too: retry until every API node knows it. + const response = await retry(async () => { + const r = await feed.getOrCreate({ + view: nearbyId, + viewer_location: amsterdam, + user_id: readerId, + }); + if (ours(r).length !== 2) throw new Error(`got ${ours(r).join()}`); + return r; + }); + expect([...ours(response)].sort()).toEqual(['hot', 'warm']); + expect(response.activities.map((a) => a.id)).not.toContain(workoutId); + // (2026-10-29) From Haarlem (~17 km away) the view finds nothing: the + // default 10 km radius applies, not the 80 km the docs set. + const far = await feed.getOrCreate({ + view: nearbyId, + viewer_location: haarlem, + user_id: readerId, + }); + expect(ours(far)).toEqual([]); + }); + it('creates a group with the interest selector', async () => { const myId = newFeedGroupId('myid'); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/06-notification_feeds.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/06-notification_feeds.test.ts index 916071e1..3cfa8d81 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/06-notification_feeds.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/06-notification_feeds.test.ts @@ -589,10 +589,10 @@ describe(DOCS, () => { // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/06-notification_feeds.md" heading="Legacy: Client-side computation" tab="Node.js" index=1 // Deprecated — use group.is_read / group.is_seen instead const isRead = - (lastReadAt && group.updated_at.getTime() < lastReadAt.getTime()) || + (lastReadAt && group.updated_at < lastReadAt) || readActivities.includes(group.group); const isSeen = - (lastSeenAt && group.updated_at.getTime() < lastSeenAt.getTime()) || + (lastSeenAt && group.updated_at < lastSeenAt) || seenActivities.includes(group.group); // #endregion snippet diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/08-for_you_feed.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/08-for_you_feed.test.ts index e40b4e27..0be07b44 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/08-for_you_feed.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/08-for_you_feed.test.ts @@ -14,6 +14,7 @@ import { copyFeedGroup, isFeedGroupMissing, retryOnFeedGroupMissing, + waitForFeedGroup, } from '../../../../helpers/feeds'; import { uniqueId } from '../../../../helpers/ids'; import { retry } from '../../../../helpers/wait'; @@ -71,6 +72,8 @@ describe(DOCS, () => { oneFid = await createFeed('user', oneId, oneId); const stockGroupId = newFeedGroupId('stock'); await client.feeds.createFeedGroup({ id: stockGroupId }); + // The fence adds an activity into this group, and addActivity can't be retried. + await waitForFeedGroup(client, stockGroupId); appleFid = await createFeed(stockGroupId, uniqueId('apple'), jackId); }); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/09-stories.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/09-stories.test.ts index 8d880782..4b3fd328 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/09-stories.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/09-stories.test.ts @@ -1,4 +1,5 @@ import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; +import { nsToMs } from '../../../../../../index'; import { getServerClient } from '../../../../helpers/clients'; import { Cleanup } from '../../../../helpers/cleanup'; import { uniqueId } from '../../../../helpers/ids'; @@ -125,7 +126,7 @@ describe(DOCS, () => { ); // The stored time can be 1 ms off the ISO string that was sent. expect( - Math.abs((story.expires_at?.getTime() ?? 0) - tomorrow.getTime()), + Math.abs(nsToMs(story.expires_at ?? 0) - tomorrow.getTime()), ).toBeLessThanOrEqual(1); }); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/12-ranking_expressions.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/12-ranking_expressions.test.ts new file mode 100644 index 00000000..e8859670 --- /dev/null +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/12-ranking_expressions.test.ts @@ -0,0 +1,375 @@ +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import type { + ActivityResponse, + CreateFeedGroupResponse, + GetOrCreateFeedRequest, + GetOrCreateFeedResponse, + StreamFeed, +} from '../../../../../../index'; +import { getServerClient } from '../../../../helpers/clients'; +import { Cleanup } from '../../../../helpers/cleanup'; +import { + isFeedGroupMissing, + retryOnFeedGroupMissing, +} from '../../../../helpers/feeds'; +import { uniqueId } from '../../../../helpers/ids'; +import { retry } from '../../../../helpers/wait'; + +const DOCS = + 'activity-feeds/v3-latest/_default/03-feed_groups/12-ranking_expressions.md'; + +// The docs use both names for the server client. +const client = getServerClient(); +const serverClient = client; +const cleanup = new Cleanup(client); + +// Feed groups are app-wide: every group a fence creates gets a uniqueId id. +const newFeedGroupId = (name: string) => { + const id = uniqueId(name); + cleanup.feedGroups.push(id); + return id; +}; + +const createUser = async (name: string) => { + const id = uniqueId(name); + cleanup.users.push(id); + await client.upsertUsers([{ id }]); + return id; +}; + +/* + * Shared data: a reader whose feeds (one per ranked group) follow an author's + * user feed with three activities, posted oldest first (a0, a1, a2), so + * recency alone would order them a2, a1, a0: + * + * | activity | popularity | custom | + * | -------- | --------------------- | --------- | + * | a0 | 5 (5 reactions) | | + * | a1 | 4 (4 reactions) | isBoosted | + * | a2 | 3 (1 share, 3 points) | | + */ +let authorId: string; +let readerId: string; +const names = new Map(); + +/** Our activities in feed order, by name. */ +const order = (response: GetOrCreateFeedResponse) => + ours(response).map((a) => names.get(a.id)); +const ours = (response: GetOrCreateFeedResponse) => + response.activities.filter((a) => names.has(a.id)); +const byName = (response: GetOrCreateFeedResponse) => + Object.fromEntries(ours(response).map((a) => [names.get(a.id), a])) as Record< + string, + ActivityResponse + >; + +/** The reader's feed in a new group, following the author. */ +const followAuthor = async (groupId: string, userId = readerId) => { + cleanup.feeds.push(`${groupId}:${userId}`); + const feed = client.feeds.feed(groupId, userId); + await retry(() => feed.getOrCreate({ user_id: userId }), { + retryIf: isFeedGroupMissing, + }); + await retry( + () => + client.feeds.follow({ source: feed.feed, target: `user:${authorId}` }), + { retryIf: isFeedGroupMissing }, + ); + return feed; +}; + +/** Reads the feed until all three activities are there in `expected` order. */ +const waitForOrder = ( + feed: StreamFeed, + expected: string[], + request: Omit = {}, + userId = readerId, +) => + retry(async () => { + const response = await feed.getOrCreate({ user_id: userId, ...request }); + const got = order(response); + if (got.join() !== expected.join()) { + throw new Error(`order is ${got.join()}, expected ${expected.join()}`); + } + return response; + }); + +/** The reader's feed in `groupId`, once ranked in `expected` order. */ +const rankedFeed = async (groupId: string, expected: string[]) => { + const feed = await followAuthor(groupId); + return waitForOrder(feed, expected); +}; + +const createdGroups = (spy: { mock: { results: Array<{ value: unknown }> } }) => + Promise.all( + spy.mock.results.map((r) => r.value as Promise), + ); + +/** + * popularity_gauss from the "Parameters" fence (origin 100, direction right, + * scale 100, offset 5, decay 0.5) for values below the origin. + */ +const gauss = (popularity: number) => { + const distance = Math.max(0, 100 - popularity - 5); + const sigma2 = -(100 ** 2) / (2 * Math.log(0.5)); + return Math.exp(-(distance ** 2) / (2 * sigma2)); +}; + +describe(DOCS, () => { + // Calls into a new group can reach API nodes that don't know it yet + // (activities in new groups are added with `addActivityOnce`: a retried + // addActivity can add them twice). + beforeEach(() => + retryOnFeedGroupMissing(client, ['getOrCreateFeed', 'follow']), + ); + afterEach(() => vi.restoreAllMocks()); + afterAll(() => cleanup.run()); + + beforeAll(async () => { + authorId = await createUser('author'); + readerId = await createUser('reader'); + const fanId = await createUser('fan'); + cleanup.feeds.push(`user:${authorId}`, `user:${fanId}`); + await client.feeds + .feed('user', authorId) + .getOrCreate({ user_id: authorId }); + await client.feeds.feed('user', fanId).getOrCreate({ user_id: fanId }); + const post = async (name: string, custom: Record = {}) => { + const { activity } = await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${authorId}`], + text: name, + custom, + user_id: authorId, + }); + names.set(activity.id, name); + return activity.id; + }; + const a0 = await post('a0'); + const a1 = await post('a1', { isBoosted: true }); + const a2 = await post('a2'); + const types = ['like', 'love', 'wow', 'haha', 'sad']; + for (const type of types) { + await client.feeds.addActivityReaction({ + activity_id: a0, + type, + user_id: fanId, + }); + } + for (const type of types.slice(0, 4)) { + await client.feeds.addActivityReaction({ + activity_id: a1, + type, + user_id: fanId, + }); + } + // A share: an activity with a2 as parent (in the fan's own feed). + await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${fanId}`], + parent_id: a2, + text: 'shared', + user_id: fanId, + }); + await retry(async () => { + const response = await client.feeds + .feed('user', authorId) + .getOrCreate({ user_id: authorId }); + const got = byName(response); + const popularity = ['a0', 'a1', 'a2'].map((n) => got[n]?.popularity); + if (popularity.join() !== '5,4,3' || got.a2.share_count !== 1) { + throw new Error(`popularity is ${popularity.join()}`); + } + }); + }); + + it('ranks with custom data and defaults', async () => { + const myId = newFeedGroupId('myid'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/12-ranking_expressions.md" heading="Activity data" tab="Node.js" index=1 + // COPY: myId="myid" + await serverClient.feeds.createFeedGroup({ + id: myId, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'expression', + score: 'popularity * (custom.isBoosted ? 1.5 : 1)', + defaults: { + custom: { + isBoosted: false, + }, + }, + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(feed_group.ranking?.defaults).toEqual({ + custom: { isBoosted: false }, + }); + // a1 (popularity 4) is boosted to 6, above a0 (5). + const feed = await rankedFeed(myId, ['a1', 'a0', 'a2']); + expect(byName(feed).a1.score).toBe(6); + // Without the default, the expression is rejected. + await expect( + client.feeds.createFeedGroup({ + id: newFeedGroupId('no-default'), + ranking: { + type: 'expression', + score: 'popularity * (custom.isBoosted ? 1.5 : 1)', + }, + }), + ).rejects.toThrow('expression references fields without defaults'); + }); + + it('ranks with decay functions', async () => { + const myId = newFeedGroupId('myid'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/12-ranking_expressions.md" heading="Parameters" tab="Node.js" index=1 + // COPY: myId="myid" + await serverClient.feeds.createFeedGroup({ + id: myId, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'expression', + functions: { + simple_gauss: { + base: 'decay_gauss', + scale: '5d', + offset: '1d', + decay: '0.3', + }, + popularity_gauss: { + base: 'decay_gauss', + origin: '100', + direction: 'right', + scale: '100', + offset: '5', + decay: '0.5', + }, + }, + score: 'simple_gauss(time)*popularity_gauss(popularity)', + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(Object.keys(feed_group.ranking?.functions ?? {}).sort()).toEqual([ + 'popularity_gauss', + 'simple_gauss', + ]); + // A second author with one activity at the origin (popularity 100: + // 50 comments), which should score 1. + const starId = await createUser('star'); + cleanup.feeds.push(`user:${starId}`); + await client.feeds.feed('user', starId).getOrCreate({ user_id: starId }); + const { activity: star } = await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${starId}`], + text: 'star', + user_id: starId, + }); + await client.feeds.addCommentsBatch({ + comments: Array.from({ length: 50 }, (_, i) => ({ + object_id: star.id, + object_type: 'activity', + comment: `comment ${i}`, + user_id: starId, + })), + }); + const feed = await followAuthor(myId); + await client.feeds.follow({ source: feed.feed, target: `user:${starId}` }); + // simple_gauss(time) is 1 for activities younger than a day, so the + // score is popularity_gauss(popularity): 1 at the origin, lower below it. + const response = await waitForOrder(feed, ['a0', 'a1', 'a2']); + const got = byName(response); + expect(got.a0.score).toBeCloseTo(gauss(5), 2); + expect(got.a1.score).toBeCloseTo(gauss(4), 2); + expect(got.a2.score).toBeCloseTo(gauss(3), 2); + const starRead = await retry(async () => { + const r = await feed.getOrCreate({ user_id: readerId }); + const found = r.activities.find((a) => a.id === star.id); + if (found?.popularity !== 100) throw new Error('star not ranked yet'); + return found; + }); + expect(starRead.score).toBeCloseTo(1, 5); + }); + + it('ranks by selector source', async () => { + const selectorTimelineId = newFeedGroupId('selector-timeline'); + const multiSelectorTimelineId = newFeedGroupId('multi-selector-timeline'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/12-ranking_expressions.md" heading="Ranking by Selector Source" tab="Node.js" index=1 + // COPY: selectorTimelineId="selector_timeline", multiSelectorTimelineId="multi_selector_timeline" + // Example 1: Simple boost - following activities get 2x multiplier + await serverClient.feeds.createFeedGroup({ + id: selectorTimelineId, + activity_selectors: [ + { type: 'following' }, + { type: 'popular', min_popularity: 1 }, + ], + ranking: { + type: 'expression', + score: 'selector_source == "following" ? popularity * 2 : popularity', + defaults: { + selector_source: '', // Required when using selector_source in expressions + }, + }, + }); + + // Example 2: Multiple selectors with different bonuses + await serverClient.feeds.createFeedGroup({ + id: multiSelectorTimelineId, + activity_selectors: [ + { type: 'following' }, + { type: 'interest' }, + { type: 'popular', min_popularity: 1 }, + ], + ranking: { + type: 'expression', + score: ` + (selector_source == "following" ? 10 : 0) + + (selector_source == "interest" ? 5 : 0) + + popularity + `, + defaults: { + selector_source: '', + }, + }, + }); + // #endregion snippet + + const groups = await createdGroups(create); + expect(groups.map((g) => g.feed_group.activity_selectors?.length)).toEqual([ + 2, 3, + ]); + // The author's activities come from the following selector (the first + // one): their popularity is doubled / gets +10. + const first = await rankedFeed(selectorTimelineId, ['a0', 'a1', 'a2']); + for (const activity of ours(first)) { + expect(activity.selector_source).toBe('following'); + expect(activity.score).toBe(activity.popularity * 2); + } + const second = await rankedFeed(multiSelectorTimelineId, [ + 'a0', + 'a1', + 'a2', + ]); + for (const activity of ours(second)) { + expect(activity.selector_source).toBe('following'); + expect(activity.score).toBe(activity.popularity + 10); + } + }); +}); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/13-interest_ranking.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/13-interest_ranking.test.ts new file mode 100644 index 00000000..560e6e3d --- /dev/null +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/13-interest_ranking.test.ts @@ -0,0 +1,292 @@ +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import type { + ActivityResponse, + CreateFeedGroupResponse, + GetOrCreateFeedRequest, + GetOrCreateFeedResponse, + StreamFeed, +} from '../../../../../../index'; +import { getServerClient } from '../../../../helpers/clients'; +import { Cleanup } from '../../../../helpers/cleanup'; +import { + isFeedGroupMissing, + retryOnFeedGroupMissing, +} from '../../../../helpers/feeds'; +import { uniqueId } from '../../../../helpers/ids'; +import { retry } from '../../../../helpers/wait'; + +const DOCS = + 'activity-feeds/v3-latest/_default/03-feed_groups/13-interest_ranking.md'; + +// The docs use both names for the server client. +const client = getServerClient(); +const serverClient = client; +const cleanup = new Cleanup(client); + +// Feed groups are app-wide: every group a fence creates gets a uniqueId id. +const newFeedGroupId = (name: string) => { + const id = uniqueId(name); + cleanup.feedGroups.push(id); + return id; +}; + +const createUser = async (name: string) => { + const id = uniqueId(name); + cleanup.users.push(id); + await client.upsertUsers([{ id }]); + return id; +}; + +/* + * Shared data: a reader whose feeds (one per ranked group) follow an author's + * user feed with three activities, posted oldest first (a0, a1, a2), so + * recency alone would order them a2, a1, a0: + * + * | activity | popularity | interest_tags | + * | -------- | --------------------- | ------------- | + * | a0 | 5 (5 reactions) | exercise | + * | a1 | 4 (4 reactions) | food | + * | a2 | 3 (1 share, 3 points) | travel | + */ +let authorId: string; +let readerId: string; +const names = new Map(); + +/** Our activities in feed order, by name. */ +const order = (response: GetOrCreateFeedResponse) => + ours(response).map((a) => names.get(a.id)); +const ours = (response: GetOrCreateFeedResponse) => + response.activities.filter((a) => names.has(a.id)); +const byName = (response: GetOrCreateFeedResponse) => + Object.fromEntries(ours(response).map((a) => [names.get(a.id), a])) as Record< + string, + ActivityResponse + >; + +/** The reader's feed in a new group, following the author. */ +const followAuthor = async (groupId: string, userId = readerId) => { + cleanup.feeds.push(`${groupId}:${userId}`); + const feed = client.feeds.feed(groupId, userId); + await retry(() => feed.getOrCreate({ user_id: userId }), { + retryIf: isFeedGroupMissing, + }); + await retry( + () => + client.feeds.follow({ source: feed.feed, target: `user:${authorId}` }), + { retryIf: isFeedGroupMissing }, + ); + return feed; +}; + +/** Reads the feed until all three activities are there in `expected` order. */ +const waitForOrder = ( + feed: StreamFeed, + expected: string[], + request: Omit = {}, + userId = readerId, +) => + retry(async () => { + const response = await feed.getOrCreate({ user_id: userId, ...request }); + const got = order(response); + if (got.join() !== expected.join()) { + throw new Error(`order is ${got.join()}, expected ${expected.join()}`); + } + return response; + }); + +const createdGroups = (spy: { mock: { results: Array<{ value: unknown }> } }) => + Promise.all( + spy.mock.results.map((r) => r.value as Promise), + ); + +describe(DOCS, () => { + // Calls into a new group can reach API nodes that don't know it yet + // (activities in new groups are added with `addActivityOnce`: a retried + // addActivity can add them twice). + beforeEach(() => + retryOnFeedGroupMissing(client, [ + 'getOrCreateFeed', + 'follow', + 'activityFeedback', + ]), + ); + afterEach(() => vi.restoreAllMocks()); + afterAll(() => cleanup.run()); + + beforeAll(async () => { + authorId = await createUser('author'); + readerId = await createUser('reader'); + const fanId = await createUser('fan'); + cleanup.feeds.push(`user:${authorId}`, `user:${fanId}`); + await client.feeds + .feed('user', authorId) + .getOrCreate({ user_id: authorId }); + await client.feeds.feed('user', fanId).getOrCreate({ user_id: fanId }); + const post = async (name: string, tag: string) => { + const { activity } = await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${authorId}`], + text: name, + interest_tags: [tag], + user_id: authorId, + }); + names.set(activity.id, name); + return activity.id; + }; + const a0 = await post('a0', 'exercise'); + const a1 = await post('a1', 'food'); + const a2 = await post('a2', 'travel'); + const types = ['like', 'love', 'wow', 'haha', 'sad']; + for (const type of types) { + await client.feeds.addActivityReaction({ + activity_id: a0, + type, + user_id: fanId, + }); + } + for (const type of types.slice(0, 4)) { + await client.feeds.addActivityReaction({ + activity_id: a1, + type, + user_id: fanId, + }); + } + // A share: an activity with a2 as parent (in the fan's own feed). + await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${fanId}`], + parent_id: a2, + text: 'shared', + user_id: fanId, + }); + await retry(async () => { + const response = await client.feeds + .feed('user', authorId) + .getOrCreate({ user_id: authorId }); + const got = byName(response); + const popularity = ['a0', 'a1', 'a2'].map((n) => got[n]?.popularity); + if (popularity.join() !== '5,4,3' || got.a2.share_count !== 1) { + throw new Error(`popularity is ${popularity.join()}`); + } + }); + }); + + it('ranks by interest weights', async () => { + const myId = newFeedGroupId('myid'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/13-interest_ranking.md" heading="" tab="Node.js" index=1 + // COPY: myId="myid" + await serverClient.feeds.createFeedGroup({ + id: myId, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'interest', + score: 'interest_score * popularity', + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(feed_group.ranking?.type).toBe('interest'); + + const timeline = await followAuthor(myId); + // The reader has no stored interests: every interest_score is 0. + const plain = await retry(async () => { + const r = await timeline.getOrCreate({ user_id: readerId }); + if (ours(r).length !== 3) throw new Error('activities missing'); + return r; + }); + expect(ours(plain).map((a) => a.score)).toEqual([0, 0, 0]); + // Settle the ranking with the docs' weights before the region. + await waitForOrder(timeline, ['a2', 'a1', 'a0'], { + interest_weights: { travel: 1, food: 0.5, exercise: -1 }, + }); + // The spy retryOnFeedGroupMissing installed. + const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); + getOrCreate.mockClear(); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/13-interest_ranking.md" heading="User interests" tab="Node.js" index=1 + // COPY: readerId="" + await timeline.getOrCreate({ + interest_weights: { + travel: 1, + food: 0.5, + exercise: -1, + }, + user_id: readerId, + }); + // #endregion snippet + + const response = await (getOrCreate.mock.results[0] + .value as Promise); + // travel (a2) > food (a1) > exercise (a0, a negative score). + expect(order(response)).toEqual(['a2', 'a1', 'a0']); + const got = byName(response); + expect(got.a1.score).toBeGreaterThan(0); + expect(got.a0.score).toBeLessThan(0); + }); + + it('ranks by preference score', async () => { + const myId = newFeedGroupId('myid'); + const myIdInterest = newFeedGroupId('myid-interest'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/13-interest_ranking.md" heading="Preference score" tab="Node.js" index=1 + // COPY: myId="myid", myIdInterest="myid_interest" + // Using preference_score with expression type + await serverClient.feeds.createFeedGroup({ + id: myId, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'expression', + score: 'preference_score * popularity', + }, + }); + + // Using preference_score with interest type + await serverClient.feeds.createFeedGroup({ + id: myIdInterest, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'interest', + score: 'preference_score * interest_score * popularity', + }, + }); + // #endregion snippet + + const groups = await createdGroups(create); + expect(groups.map((g) => g.feed_group.ranking?.type)).toEqual([ + 'expression', + 'interest', + ]); + // A user of their own: activity feedback changes their preferences. + const userId = await createUser('preferences'); + const feed = await followAuthor(myId, userId); + // Without activity feedback preference_score is 0 for every activity. + const before = await retry(async () => { + const r = await feed.getOrCreate({ user_id: userId }); + if (ours(r).length !== 3) throw new Error('activities missing'); + return r; + }); + expect(ours(before).map((a) => a.score)).toEqual([0, 0, 0]); + // "Show more" on a2 (travel) moves it to the top. + await client.feeds.activityFeedback({ + activity_id: byName(before).a2.id, + show_more: true, + user_id: userId, + }); + const after = await waitForOrder(feed, ['a2', 'a1', 'a0'], {}, userId); + expect(byName(after).a2.score).toBeGreaterThan(0); + expect(byName(after).a1.score).toBe(0); + }); +}); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/14-read_seen_ranking.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/14-read_seen_ranking.test.ts new file mode 100644 index 00000000..218f1fda --- /dev/null +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/14-read_seen_ranking.test.ts @@ -0,0 +1,251 @@ +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import type { + ActivityResponse, + CreateFeedGroupResponse, + GetOrCreateFeedRequest, + GetOrCreateFeedResponse, + StreamFeed, +} from '../../../../../../index'; +import { getServerClient } from '../../../../helpers/clients'; +import { Cleanup } from '../../../../helpers/cleanup'; +import { + isFeedGroupMissing, + retryOnFeedGroupMissing, +} from '../../../../helpers/feeds'; +import { uniqueId } from '../../../../helpers/ids'; +import { retry } from '../../../../helpers/wait'; + +const DOCS = + 'activity-feeds/v3-latest/_default/03-feed_groups/14-read_seen_ranking.md'; + +// The docs use both names for the server client. +const client = getServerClient(); +const serverClient = client; +const cleanup = new Cleanup(client); + +// Feed groups are app-wide: every group a fence creates gets a uniqueId id. +const newFeedGroupId = (name: string) => { + const id = uniqueId(name); + cleanup.feedGroups.push(id); + return id; +}; + +const createUser = async (name: string) => { + const id = uniqueId(name); + cleanup.users.push(id); + await client.upsertUsers([{ id }]); + return id; +}; + +/* + * Shared data: a reader whose feeds (one per ranked group) follow an author's + * user feed with three activities, posted oldest first (a0, a1, a2), so + * recency alone would order them a2, a1, a0: + * + * | activity | popularity | + * | -------- | --------------------- | + * | a0 | 5 (5 reactions) | + * | a1 | 4 (4 reactions) | + * | a2 | 3 (1 share, 3 points) | + */ +let authorId: string; +let readerId: string; +const names = new Map(); + +/** Our activities in feed order, by name. */ +const order = (response: GetOrCreateFeedResponse) => + ours(response).map((a) => names.get(a.id)); +const ours = (response: GetOrCreateFeedResponse) => + response.activities.filter((a) => names.has(a.id)); +const byName = (response: GetOrCreateFeedResponse) => + Object.fromEntries(ours(response).map((a) => [names.get(a.id), a])) as Record< + string, + ActivityResponse + >; + +/** The reader's feed in a new group, following the author. */ +const followAuthor = async (groupId: string, userId = readerId) => { + cleanup.feeds.push(`${groupId}:${userId}`); + const feed = client.feeds.feed(groupId, userId); + await retry(() => feed.getOrCreate({ user_id: userId }), { + retryIf: isFeedGroupMissing, + }); + await retry( + () => + client.feeds.follow({ source: feed.feed, target: `user:${authorId}` }), + { retryIf: isFeedGroupMissing }, + ); + return feed; +}; + +/** Reads the feed until all three activities are there in `expected` order. */ +const waitForOrder = ( + feed: StreamFeed, + expected: string[], + request: Omit = {}, + userId = readerId, +) => + retry(async () => { + const response = await feed.getOrCreate({ user_id: userId, ...request }); + const got = order(response); + if (got.join() !== expected.join()) { + throw new Error(`order is ${got.join()}, expected ${expected.join()}`); + } + return response; + }); + +const createdGroups = (spy: { mock: { results: Array<{ value: unknown }> } }) => + Promise.all( + spy.mock.results.map((r) => r.value as Promise), + ); + +describe(DOCS, () => { + // Calls into a new group can reach API nodes that don't know it yet + // (activities in new groups are added with `addActivityOnce`: a retried + // addActivity can add them twice). + beforeEach(() => + retryOnFeedGroupMissing(client, [ + 'getOrCreateFeed', + 'follow', + 'markActivity', + ]), + ); + afterEach(() => vi.restoreAllMocks()); + afterAll(() => cleanup.run()); + + beforeAll(async () => { + authorId = await createUser('author'); + readerId = await createUser('reader'); + const fanId = await createUser('fan'); + cleanup.feeds.push(`user:${authorId}`, `user:${fanId}`); + await client.feeds + .feed('user', authorId) + .getOrCreate({ user_id: authorId }); + await client.feeds.feed('user', fanId).getOrCreate({ user_id: fanId }); + const post = async (name: string) => { + const { activity } = await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${authorId}`], + text: name, + user_id: authorId, + }); + names.set(activity.id, name); + return activity.id; + }; + const a0 = await post('a0'); + const a1 = await post('a1'); + const a2 = await post('a2'); + const types = ['like', 'love', 'wow', 'haha', 'sad']; + for (const type of types) { + await client.feeds.addActivityReaction({ + activity_id: a0, + type, + user_id: fanId, + }); + } + for (const type of types.slice(0, 4)) { + await client.feeds.addActivityReaction({ + activity_id: a1, + type, + user_id: fanId, + }); + } + // A share: an activity with a2 as parent (in the fan's own feed). + await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${fanId}`], + parent_id: a2, + text: 'shared', + user_id: fanId, + }); + await retry(async () => { + const response = await client.feeds + .feed('user', authorId) + .getOrCreate({ user_id: authorId }); + const got = byName(response); + const popularity = ['a0', 'a1', 'a2'].map((n) => got[n]?.popularity); + if (popularity.join() !== '5,4,3' || got.a2.share_count !== 1) { + throw new Error(`popularity is ${popularity.join()}`); + } + }); + }); + + it('ranks read activities lower', async () => { + const rankedTimelineId = newFeedGroupId('ranked-timeline'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/14-read_seen_ranking.md" heading="" tab="Node.js" index=1 + // COPY: rankedTimelineId="ranked_timeline" + await serverClient.feeds.createFeedGroup({ + id: rankedTimelineId, + activity_marks: { + track_seen: true, + track_read: true, + }, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'expression', + score: 'popularity - (is_read ? 100 : 0)', + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(feed_group.activity_marks).toMatchObject({ track_read: true }); + const feed = await followAuthor(rankedTimelineId); + const unread = await waitForOrder(feed, ['a0', 'a1', 'a2']); + const a0 = byName(unread).a0.id; + await feed.markActivity({ mark_read: [a0], user_id: readerId }); + // The most popular activity drops to the bottom once read. + const response = await waitForOrder(feed, ['a1', 'a2', 'a0']); + expect(byName(response).a0).toMatchObject({ is_read: true, score: -95 }); + }); + + it('ranks unseen activities first', async () => { + const unseenTimelineId = newFeedGroupId('unseen-timeline'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/14-read_seen_ranking.md" heading="" tab="Node.js" index=2 + // COPY: unseenTimelineId="unseen_timeline" + await serverClient.feeds.createFeedGroup({ + id: unseenTimelineId, + activity_marks: { + track_seen: true, + track_read: true, + }, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'expression', + score: 'is_seen ? 0 : 100', + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(feed_group.ranking?.score).toBe('is_seen ? 0 : 100'); + const feed = await followAuthor(unseenTimelineId); + const before = await retry(async () => { + const r = await feed.getOrCreate({ user_id: readerId }); + if (ours(r).length !== 3) throw new Error('activities missing'); + return r; + }); + expect(ours(before).map((a) => a.score)).toEqual([100, 100, 100]); + const a1 = byName(before).a1.id; + await feed.markActivity({ mark_seen: [a1], user_id: readerId }); + const response = await retry(async () => { + const r = await feed.getOrCreate({ user_id: readerId }); + if (order(r)[2] !== 'a1') throw new Error('a1 not last yet'); + return r; + }); + expect(byName(response).a1).toMatchObject({ is_seen: true, score: 0 }); + }); +}); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/15-external_ranking_data.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/15-external_ranking_data.test.ts new file mode 100644 index 00000000..10205f41 --- /dev/null +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/03-feed_groups/15-external_ranking_data.test.ts @@ -0,0 +1,443 @@ +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import type { + ActivityResponse, + CreateFeedGroupResponse, + GetOrCreateFeedRequest, + GetOrCreateFeedResponse, + StreamFeed, +} from '../../../../../../index'; +import { getServerClient } from '../../../../helpers/clients'; +import { Cleanup } from '../../../../helpers/cleanup'; +import { + addActivityOnce, + isFeedGroupMissing, + retryOnFeedGroupMissing, +} from '../../../../helpers/feeds'; +import { uniqueId } from '../../../../helpers/ids'; +import { retry } from '../../../../helpers/wait'; + +const DOCS = + 'activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md'; + +// The docs use both names for the server client. +const client = getServerClient(); +const serverClient = client; +const cleanup = new Cleanup(client); + +// Feed groups are app-wide: every group a fence creates gets a uniqueId id. +const newFeedGroupId = (name: string) => { + const id = uniqueId(name); + cleanup.feedGroups.push(id); + return id; +}; + +const createUser = async (name: string) => { + const id = uniqueId(name); + cleanup.users.push(id); + await client.upsertUsers([{ id }]); + return id; +}; + +/* + * Shared data: a reader whose feeds (one per ranked group) follow an author's + * user feed with three activities, posted oldest first (a0, a1, a2), so + * recency alone would order them a2, a1, a0: + * + * | activity | popularity | custom | + * | -------- | --------------------- | ------------------------------------ | + * | a0 | 5 (5 reactions) | stock: tesla, stocks: [] | + * | a1 | 4 (4 reactions) | stock: nokia, stocks: [microsoft] | + * | a2 | 3 (1 share, 3 points) | stock: apple, stocks: [apple, tesla] | + */ +let authorId: string; +let readerId: string; +const names = new Map(); + +/** Our activities in feed order, by name. */ +const order = (response: GetOrCreateFeedResponse) => + ours(response).map((a) => names.get(a.id)); +const ours = (response: GetOrCreateFeedResponse) => + response.activities.filter((a) => names.has(a.id)); +const byName = (response: GetOrCreateFeedResponse) => + Object.fromEntries(ours(response).map((a) => [names.get(a.id), a])) as Record< + string, + ActivityResponse + >; + +/** The reader's feed in a new group, following the author. */ +const followAuthor = async (groupId: string, userId = readerId) => { + cleanup.feeds.push(`${groupId}:${userId}`); + const feed = client.feeds.feed(groupId, userId); + await retry(() => feed.getOrCreate({ user_id: userId }), { + retryIf: isFeedGroupMissing, + }); + await retry( + () => + client.feeds.follow({ source: feed.feed, target: `user:${authorId}` }), + { retryIf: isFeedGroupMissing }, + ); + return feed; +}; + +/** Reads the feed until all three activities are there in `expected` order. */ +const waitForOrder = ( + feed: StreamFeed, + expected: string[], + request: Omit = {}, + userId = readerId, +) => + retry(async () => { + const response = await feed.getOrCreate({ user_id: userId, ...request }); + const got = order(response); + if (got.join() !== expected.join()) { + throw new Error(`order is ${got.join()}, expected ${expected.join()}`); + } + return response; + }); + +const createdGroups = (spy: { mock: { results: Array<{ value: unknown }> } }) => + Promise.all( + spy.mock.results.map((r) => r.value as Promise), + ); + +describe(DOCS, () => { + // Calls into a new group can reach API nodes that don't know it yet + // (activities in new groups are added with `addActivityOnce`: a retried + // addActivity can add them twice). + beforeEach(() => + retryOnFeedGroupMissing(client, ['getOrCreateFeed', 'follow']), + ); + afterEach(() => vi.restoreAllMocks()); + afterAll(() => cleanup.run()); + + beforeAll(async () => { + authorId = await createUser('author'); + readerId = await createUser('reader'); + const fanId = await createUser('fan'); + cleanup.feeds.push(`user:${authorId}`, `user:${fanId}`); + await client.feeds + .feed('user', authorId) + .getOrCreate({ user_id: authorId }); + await client.feeds.feed('user', fanId).getOrCreate({ user_id: fanId }); + const post = async (name: string, custom: Record) => { + const { activity } = await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${authorId}`], + text: name, + custom, + user_id: authorId, + }); + names.set(activity.id, name); + return activity.id; + }; + const a0 = await post('a0', { stock: 'tesla', stocks: [] }); + const a1 = await post('a1', { stock: 'nokia', stocks: ['microsoft'] }); + const a2 = await post('a2', { stock: 'apple', stocks: ['apple', 'tesla'] }); + const types = ['like', 'love', 'wow', 'haha', 'sad']; + for (const type of types) { + await client.feeds.addActivityReaction({ + activity_id: a0, + type, + user_id: fanId, + }); + } + for (const type of types.slice(0, 4)) { + await client.feeds.addActivityReaction({ + activity_id: a1, + type, + user_id: fanId, + }); + } + // A share: an activity with a2 as parent (in the fan's own feed). + await client.feeds.addActivity({ + type: 'post', + feeds: [`user:${fanId}`], + parent_id: a2, + text: 'shared', + user_id: fanId, + }); + await retry(async () => { + const response = await client.feeds + .feed('user', authorId) + .getOrCreate({ user_id: authorId }); + const got = byName(response); + const popularity = ['a0', 'a1', 'a2'].map((n) => got[n]?.popularity); + if (popularity.join() !== '5,4,3' || got.a2.share_count !== 1) { + throw new Error(`popularity is ${popularity.join()}`); + } + }); + }); + + it('ranks with external data', async () => { + const myId = newFeedGroupId('myid'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md" heading="Accessing external data in ranking expressions" tab="Node.js" index=1 + // COPY: myId="myid" + await serverClient.feeds.createFeedGroup({ + id: myId, + ranking: { + type: 'expression', + score: + 'popularity * external.popularity_multiplier + share_count * external.share_multiplier', + defaults: { + external: { + // Required: used when a read doesn't provide these fields + popularity_multiplier: 1, + share_multiplier: 1, + }, + }, + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(feed_group.ranking?.defaults).toEqual({ + external: { popularity_multiplier: 1, share_multiplier: 1 }, + }); + // No selectors: the group ranks the feed's own activities + // (current_feed). b0 has popularity 5, b1 one share (popularity 3). + cleanup.feeds.push(`${myId}:${readerId}`); + const timeline = client.feeds.feed(myId, readerId); + await retry(() => timeline.getOrCreate({ user_id: readerId }), { + retryIf: isFeedGroupMissing, + }); + const post = async (name: string) => { + const activityId = await addActivityOnce(client, { + type: 'post', + feeds: [timeline.feed], + text: name, + user_id: authorId, + }); + names.set(activityId, name); + return activityId; + }; + const b0 = await post('b0'); + const b1 = await post('b1'); + for (const type of ['like', 'love', 'wow', 'haha', 'sad']) { + await client.feeds.addActivityReaction({ + activity_id: b0, + type, + user_id: readerId, + }); + } + await addActivityOnce(client, { + type: 'post', + feeds: [timeline.feed], + parent_id: b1, + text: 'shared', + user_id: authorId, + }); + // With the defaults: b0 5, b1 3 + 1 share. + const plain = await waitForOrder(timeline, ['b0', 'b1']); + expect(byName(plain).b1.score).toBe(4); + await waitForOrder(timeline, ['b1', 'b0'], { + external_ranking: { popularity_multiplier: 1, share_multiplier: 100 }, + }); + // The spy retryOnFeedGroupMissing installed. + const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); + getOrCreate.mockClear(); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md" heading="Providing external data when reading the feed" tab="Node.js" index=1 + // COPY: readerId="" + await timeline.getOrCreate({ + external_ranking: { + popularity_multiplier: 1, // normal weight for popularity + share_multiplier: 100, // very high boost for share_count + }, + user_id: readerId, + }); + // #endregion snippet + + const response = await (getOrCreate.mock.results[0] + .value as Promise); + // The shared activity jumps to the top: 3 + 1 * 100. + expect(order(response)).toEqual(['b1', 'b0']); + expect(byName(response).b1.score).toBe(103); + }); + + it('ranks with map_lookup', async () => { + const myId = newFeedGroupId('myid'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md" heading="Arrays and Maps" tab="Node.js" index=1 + // COPY: myId="myid" + await serverClient.feeds.createFeedGroup({ + id: myId, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'expression', + score: 'popularity + map_lookup(stock, external.stock_boosts)', + defaults: { + stock: '', + external: { + stock_boosts: {}, + }, + }, + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(feed_group.ranking?.defaults).toEqual({ + stock: '', + external: { stock_boosts: {} }, + }); + const timeline = await followAuthor(myId); + await waitForOrder(timeline, ['a0', 'a1', 'a2']); + // The spy retryOnFeedGroupMissing installed. + const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); + getOrCreate.mockClear(); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md" heading="Arrays and Maps" tab="Node.js" index=2 + // COPY: readerId="" + await timeline.getOrCreate({ + external_ranking: { + stock_boosts: { + apple: 0.9, + tesla: 0.5, + microsoft: 0.3, + }, + }, + user_id: readerId, + }); + // #endregion snippet + + const response = await (getOrCreate.mock.results[0] + .value as Promise); + // a0 (tesla) 5 + 0.5, a1 (nokia, not in the map) 4 + 0, a2 (apple) 3 + 0.9. + const got = byName(response); + expect(got.a0.score).toBeCloseTo(5.5, 5); + expect(got.a1.score).toBeCloseTo(4, 5); + expect(got.a2.score).toBeCloseTo(3.9, 5); + }); + + it('ranks with sum_map_lookup', async () => { + const myId = newFeedGroupId('myid'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md" heading="Arrays and Maps" tab="Node.js" index=3 + // COPY: myId="myid" + await serverClient.feeds.createFeedGroup({ + id: myId, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'expression', + score: 'popularity + sum_map_lookup(stocks, external.stock_boosts)', + defaults: { + stocks: [], + external: { + stock_boosts: {}, + }, + }, + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(feed_group.ranking?.defaults).toEqual({ + stocks: [], + external: { stock_boosts: {} }, + }); + const timeline = await followAuthor(myId); + // a0 (no stocks) 5, a1 (microsoft) 4 + 2, a2 (apple, tesla) 3 + 15. + await waitForOrder(timeline, ['a2', 'a1', 'a0'], { + external_ranking: { + stock_boosts: { apple: 10.0, tesla: 5.0, microsoft: 2.0 }, + }, + }); + // The spy retryOnFeedGroupMissing installed. + const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); + getOrCreate.mockClear(); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md" heading="Arrays and Maps" tab="Node.js" index=4 + // COPY: readerId="" + await timeline.getOrCreate({ + external_ranking: { + stock_boosts: { + apple: 10.0, + tesla: 5.0, + microsoft: 2.0, + }, + }, + user_id: readerId, + }); + + // score = popularity + 10.0 + 5.0 = popularity + 15.0 (for activity with stocks: ['apple', 'tesla']) + // #endregion snippet + + const response = await (getOrCreate.mock.results[0] + .value as Promise); + expect(order(response)).toEqual(['a2', 'a1', 'a0']); + const got = byName(response); + expect(got.a2.score).toBe(18); + expect(got.a1.score).toBe(6); + expect(got.a0.score).toBe(5); + }); + + it('ranks with in_array', async () => { + const myId = newFeedGroupId('myid'); + const create = vi.spyOn(client.feeds, 'createFeedGroup'); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md" heading="Arrays and Maps" tab="Node.js" index=5 + // COPY: myId="myid" + await serverClient.feeds.createFeedGroup({ + id: myId, + activity_selectors: [{ type: 'following' }], + ranking: { + type: 'expression', + score: 'popularity + in_array(stock, external.watchlist) * 10', + defaults: { + stock: '', + external: { + watchlist: [], + }, + }, + }, + }); + // #endregion snippet + + const [{ feed_group }] = await createdGroups(create); + expect(feed_group.ranking?.defaults).toEqual({ + stock: '', + external: { watchlist: [] }, + }); + const timeline = await followAuthor(myId); + // a0 (tesla) 5 + 10, a1 (nokia) 4, a2 (apple) 3 + 10. + await waitForOrder(timeline, ['a0', 'a2', 'a1'], { + external_ranking: { watchlist: ['apple', 'tesla', 'microsoft'] }, + }); + // The spy retryOnFeedGroupMissing installed. + const getOrCreate = vi.spyOn(client.feeds, 'getOrCreateFeed'); + getOrCreate.mockClear(); + + // #region snippet docs="activity-feeds/v3-latest/_default/03-feed_groups/15-external_ranking_data.md" heading="Arrays and Maps" tab="Node.js" index=6 + // COPY: readerId="" + await timeline.getOrCreate({ + external_ranking: { + watchlist: ['apple', 'tesla', 'microsoft'], + }, + user_id: readerId, + }); + + // activities with stocks in the watchlist get a +10 boost, others get no boost. + // #endregion snippet + + const response = await (getOrCreate.mock.results[0] + .value as Promise); + expect(order(response)).toEqual(['a0', 'a2', 'a1']); + const got = byName(response); + expect(got.a0.score).toBe(15); + expect(got.a2.score).toBe(13); + expect(got.a1.score).toBe(4); + }); +}); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/02-search-feeds.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/02-search-feeds.test.ts index f648642d..fc639c86 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/02-search-feeds.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/04-feeds-and-follows/02-search-feeds.test.ts @@ -95,8 +95,8 @@ describe(DOCS, () => { expect(firstPage.next).toBeTruthy(); expect(secondPage.feeds).toHaveLength(2); expect([...fids(firstPage), ...fids(secondPage)].sort()).toEqual(expected); - const times = [...firstPage.feeds, ...secondPage.feeds].map((f) => - f.created_at.getTime(), + const times = [...firstPage.feeds, ...secondPage.feeds].map( + (f) => f.created_at, ); expect(times).toEqual([...times].sort((a, b) => b - a)); }); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/01-activities.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/01-activities.test.ts index 81be65dc..8dd19e43 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/01-activities.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/01-activities.test.ts @@ -290,7 +290,7 @@ describe(DOCS, () => { expect(response.activity.custom).toEqual({ color: 'red', size: 'L' }); expect(unsetResponse.activity.text).toBe('Japan has over 6,800 islands.'); expect(unsetResponse.activity.custom).toEqual({ size: 'L' }); - expect(unsetResponse.activity.edited_at).toBeInstanceOf(Date); + expect(unsetResponse.activity.edited_at).toEqual(expect.any(Number)); expect(log).toHaveBeenCalledTimes(2); }); @@ -312,7 +312,7 @@ describe(DOCS, () => { // #endregion snippet expect(response.activity.text).toBe('Updated text'); - expect(response.activity.edited_at).toBeInstanceOf(Date); + expect(response.activity.edited_at).toEqual(expect.any(Number)); expect(log).toHaveBeenCalledWith(response.activity.edited_at); }); @@ -349,7 +349,7 @@ describe(DOCS, () => { id, include_soft_deleted_activities: true, }); - expect(activity.deleted_at).toBeInstanceOf(Date); + expect(activity.deleted_at).toEqual(expect.any(Number)); } }); @@ -399,7 +399,7 @@ describe(DOCS, () => { // #endregion snippet expect(activity.id).toBe(activityId); - expect(activity.deleted_at).toBeInstanceOf(Date); + expect(activity.deleted_at).toEqual(expect.any(Number)); expect(log).toHaveBeenCalledWith(undefined, activity.deleted_at); }); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/02-querying-activities.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/02-querying-activities.test.ts index edd27940..ae751f73 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/02-querying-activities.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/05-activities/02-querying-activities.test.ts @@ -95,7 +95,7 @@ describe(DOCS, () => { expect(response.activities.every((a) => a.type === 'post')).toBe(true); // Newest first: the activity just created comes first. expect(response.activities[0].id).toBe(activityId); - const times = response.activities.map((a) => a.created_at.getTime()); + const times = response.activities.map((a) => a.created_at); expect(times).toEqual([...times].sort((a, b) => b - a)); }); @@ -218,7 +218,7 @@ describe(DOCS, () => { // #endregion snippet const deleted = response.activities.find((a) => a.id === deletedId); - expect(deleted?.deleted_at).toBeInstanceOf(Date); + expect(deleted?.deleted_at).toEqual(expect.any(Number)); }); it('queries moderated activities', async () => { diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/03-comments.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/03-comments.test.ts index c8f43d65..18f279d0 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/03-comments.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/03-comments.test.ts @@ -160,7 +160,7 @@ describe(DOCS, () => { // #endregion snippet expect(response.comment.text).toBe('Updated comment'); - expect(response.comment.edited_at).toBeInstanceOf(Date); + expect(response.comment.edited_at).toEqual(expect.any(Number)); expect(log).toHaveBeenCalledWith(response.comment.edited_at); }); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/10-moderation.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/10-moderation.test.ts index 69d3445e..0e4c6555 100644 --- a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/10-moderation.test.ts +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/10-moderation.test.ts @@ -7,6 +7,7 @@ import { it, vi, } from 'vitest'; +import { nsToMs } from '../../../../../../index'; import type { QueryModerationConfigsResponse, QueryReviewQueueResponse, @@ -57,10 +58,7 @@ describe(DOCS, () => { payload: { filter_conditions: { user_id: { $eq: userId } } }, }); expect(bans).toHaveLength(1); - return ( - ((bans[0].expires?.getTime() ?? 0) - bans[0].created_at.getTime()) / - 60_000 - ); + return (nsToMs(bans[0].expires ?? 0) - nsToMs(bans[0].created_at)) / 60_000; }; beforeAll(async () => { diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/13-translation.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/13-translation.test.ts new file mode 100644 index 00000000..ee2189ea --- /dev/null +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/13-translation.test.ts @@ -0,0 +1,166 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { getServerClient } from '../../../../helpers/clients'; +import { Cleanup } from '../../../../helpers/cleanup'; +import { uniqueId } from '../../../../helpers/ids'; +import { getServerUser } from '../../../../helpers/server'; + +const DOCS = 'activity-feeds/v3-latest/_default/06-features/13-translation.md'; + +// Translation works on the test app with auto-translation off. +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + + const authorId = uniqueId('author'); + const johnId = uniqueId('john'); + + /** A new user with `language: "en"` and their user feed, registered for cleanup. */ + const createUser = async (id: string) => { + cleanup.users.push(id); + cleanup.feeds.push(`user:${id}`); + await client.upsertUsers([{ id, language: 'en' }]); + await client.feeds.feed('user', id).getOrCreate({ user_id: id }); + }; + + /** A new activity by `userId` in their user feed. */ + const createActivity = async (userId: string, text: string) => + ( + await client.feeds.addActivity({ + feeds: [`user:${userId}`], + type: 'post', + text, + user_id: userId, + }) + ).activity; + + beforeAll(async () => { + await createUser(authorId); + await createUser(johnId); + // John's feed has one English activity with a stored Spanish translation. + const activity = await createActivity(johnId, 'Hello world'); + await client.feeds.translateActivity({ id: activity.id, language: 'es' }); + }); + + afterAll(() => cleanup.run()); + + it('translates an activity', async () => { + const activityId = ( + await createActivity( + authorId, + 'Hello, I would like to know more about your product.', + ) + ).id; + + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/13-translation.md" heading="Translate an activity" tab="Node.js" index=1 + // COPY: activityId="activity_123" + const response = await client.feeds.translateActivity({ + id: activityId, + language: 'fr', + }); + + console.log(response.activity.i18n?.fr_text); + // "Bonjour, j'aimerais en savoir plus sur votre produit." + // #endregion snippet + + expect(response.activity.id).toBe(activityId); + expect(response.activity.i18n?.language).toBe('en'); + expect(typeof response.activity.i18n?.fr_text).toBe('string'); + expect(response.activity.i18n?.fr_text).not.toBe(response.activity.text); + }); + + it('translates a comment', async () => { + const activity = await createActivity(authorId, 'setup'); + const commentId = ( + await client.feeds.addComment({ + object_id: activity.id, + object_type: 'activity', + comment: 'Good morning, how are you?', + user_id: authorId, + }) + ).comment.id; + + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/13-translation.md" heading="Translate a comment" tab="Node.js" index=1 + // COPY: commentId="comment_456" + const response = await client.feeds.translateComment({ + id: commentId, + language: 'es', + }); + + console.log(response.comment.i18n?.es_text); + // #endregion snippet + + expect(response.comment.id).toBe(commentId); + expect(response.comment.i18n?.language).toBe('en'); + expect(typeof response.comment.i18n?.es_text).toBe('string'); + expect(response.comment.i18n?.es_text).not.toBe(response.comment.text); + }); + + it('reads a feed with the default projection', async () => { + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/13-translation.md" heading="Default projection (`translate_text=false`)" tab="Node.js" index=1 + // COPY: johnId="john" + const feed = client.feeds.feed('user', johnId); + const response = await feed.getOrCreate({ + user_id: johnId, + language: 'es', + }); + + // text is still the original English + console.log(response.activities[0].text); + // "Hello world" + + // projected i18n includes Spanish when translated + console.log(response.activities[0].i18n?.es_text); + // "Hola mundo" + // #endregion snippet + + expect(response.activities).toHaveLength(1); + const [activity] = response.activities; + expect(activity.text).toBe('Hello world'); + expect(activity.i18n?.language).toBe('en'); + expect(typeof activity.i18n?.es_text).toBe('string'); + expect(activity.i18n?.es_text).not.toBe('Hello world'); + }); + + it('reads a feed with the translated text', async () => { + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/13-translation.md" heading="Substitute `text` (`translate_text=true`)" tab="Node.js" index=1 + // COPY: johnId="john" + const feed = client.feeds.feed('user', johnId); + const response = await feed.getOrCreate({ + user_id: johnId, + language: 'es', + translate_text: true, + }); + + console.log(response.activities[0].text); + // "Hola mundo" + + console.log(response.activities[0].i18n?.language); + // "en" + // #endregion snippet + + expect(response.activities).toHaveLength(1); + const [activity] = response.activities; + expect(activity.i18n?.language).toBe('en'); + expect(activity.i18n?.en_text).toBe('Hello world'); + expect(activity.text).not.toBe('Hello world'); + expect(activity.text).toBe(activity.i18n?.es_text); + }); + + it('sets the user language', async () => { + const user123Id = uniqueId('user'); + cleanup.users.push(user123Id); + + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/13-translation.md" heading="Set user language" tab="Node.js" index=1 + // COPY: user123Id="user_123" + await client.updateUsers({ + users: { + user_123: { id: user123Id, language: 'en', name: 'Jane' }, + }, + }); + // #endregion snippet + + const user = await getServerUser(client, user123Id); + expect(user.language).toBe('en'); + expect(user.name).toBe('Jane'); + }); +}); diff --git a/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/14-data-retention.test.ts b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/14-data-retention.test.ts new file mode 100644 index 00000000..43a499b8 --- /dev/null +++ b/__tests__/docs-snippets/pages/activity-feeds/v3-latest/06-features/14-data-retention.test.ts @@ -0,0 +1,226 @@ +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from 'vitest'; +import type { + FeedsRetentionPolicy, + SetFeedsRetentionPolicyResponse, + StreamClient, +} from '../../../../../../index'; +import { getServerClient } from '../../../../helpers/clients'; +import { Cleanup } from '../../../../helpers/cleanup'; +import { uniqueId } from '../../../../helpers/ids'; +import { retry } from '../../../../helpers/wait'; + +const DOCS = + 'activity-feeds/v3-latest/_default/06-features/14-data-retention.md'; + +/** The app's feeds retention policies, without server-managed fields. */ +const policyConfigs = (policies: FeedsRetentionPolicy[]) => + policies + .map((p) => ({ policy: p.policy, config: p.config })) + .sort((a, b) => a.policy.localeCompare(b.policy)); + +/** + * The `enabled` flag of `feedsGetRetentionPolicy` is eventually consistent (reads can + * return the old value for ~6s after a change, while `policies` is up to date at once): + * waits until 5 reads in a row return `expected`. + */ +const waitForRetentionEnabled = async ( + client: StreamClient, + expected: boolean, +) => { + let streak = 0; + await retry( + async () => { + const { enabled } = await client.feeds.feedsGetRetentionPolicy(); + streak = enabled === expected ? streak + 1 : 0; + if (streak < 5) { + throw new Error(`retention enabled is ${enabled}, want ${expected}`); + } + }, + { interval: 500, timeout: 30000 }, + ); +}; + +/** + * Puts back the original feeds retention policies (and the enabled flag) and deletes + * every policy the file added. Feeds retention policies are app-level and not part of + * the leak / drift checks. + */ +const restoreFeedsRetention = async ( + client: StreamClient, + original: { enabled: boolean; policies: FeedsRetentionPolicy[] }, +) => { + const { policies: current } = await client.feeds.feedsGetRetentionPolicy(); + for (const p of current) { + if (!original.policies.some((o) => o.policy === p.policy)) { + await client.feeds.feedsDeleteRetentionPolicy({ policy: p.policy }); + } + } + for (const p of original.policies) { + await client.feeds.feedsSetRetentionPolicy({ + policy: p.policy, + max_age_hours: p.config.max_age_hours, + feed_groups: p.config.feed_groups, + enabled: original.enabled, + }); + } +}; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + + // The docs' own policy name ("stories"); the feed group stays the built-in `stories`. + const storiesPolicy = uniqueId('stories'); + + /** The `enabled` flag of each `feedsSetRetentionPolicy` response so far. */ + const setResponses = async (spy: { + mock: { results: Array<{ value: unknown }> }; + }) => + ( + await Promise.all( + spy.mock.results.map( + (r) => r.value as Promise, + ), + ) + ).map((r) => r.enabled); + + const getPolicy = async (name: string) => + (await client.feeds.feedsGetRetentionPolicy()).policies.find( + (p) => p.policy === name, + ); + + beforeAll(async () => { + const original = await client.feeds.feedsGetRetentionPolicy(); + cleanup.add(async () => { + await restoreFeedsRetention(client, original); + // Check that the app is back to where it started. + const { policies } = await client.feeds.feedsGetRetentionPolicy(); + expect(policyConfigs(policies)).toEqual(policyConfigs(original.policies)); + await waitForRetentionEnabled(client, original.enabled); + }); + }); + + afterEach(() => vi.restoreAllMocks()); + afterAll(() => cleanup.run()); + + it('sets the app-wide retention policy', async () => { + const set = vi.spyOn(client.feeds, 'feedsSetRetentionPolicy'); + + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/14-data-retention.md" heading="Setting a retention policy" tab="Node.js" index=1 + // Delete all activities older than one year (8,760 hours) + await client.feeds.feedsSetRetentionPolicy({ + policy: 'old-activities', + max_age_hours: 8760, + }); + // #endregion snippet + + const policy = await getPolicy('old-activities'); + expect(policy?.config.max_age_hours).toBe(8760); + expect(policy?.config.feed_groups ?? []).toEqual([]); + // Setting a policy turns retention on. + expect(await setResponses(set)).toEqual([true]); + await waitForRetentionEnabled(client, true); + }); + + it('sets a retention policy for some feed groups', async () => { + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/14-data-retention.md" heading="Setting a retention policy" tab="Node.js" index=2 + // COPY: storiesPolicy="stories" + // Delete activities posted to stories feeds after 30 days (720 hours) + await client.feeds.feedsSetRetentionPolicy({ + policy: storiesPolicy, + max_age_hours: 720, + feed_groups: ['stories'], + }); + // #endregion snippet + + const policy = await getPolicy(storiesPolicy); + expect(policy?.config).toEqual({ + max_age_hours: 720, + feed_groups: ['stories'], + }); + }); + + it('pauses retention', async () => { + await waitForRetentionEnabled(client, true); + const set = vi.spyOn(client.feeds, 'feedsSetRetentionPolicy'); + + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/14-data-retention.md" heading="Pausing retention" tab="Node.js" index=1 + await client.feeds.feedsSetRetentionPolicy({ + policy: 'old-activities', + max_age_hours: 8760, + enabled: false, + }); + // #endregion snippet + + expect(await setResponses(set)).toEqual([false]); + await waitForRetentionEnabled(client, false); + const response = await client.feeds.feedsGetRetentionPolicy(); + // Pausing keeps the policies. + expect(response.policies.map((p) => p.policy)).toEqual( + expect.arrayContaining(['old-activities', storiesPolicy]), + ); + }); + + it('retrieves retention policies', async () => { + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/14-data-retention.md" heading="Retrieving retention policies" tab="Node.js" index=1 + const response = await client.feeds.feedsGetRetentionPolicy(); + console.log(response.enabled); + console.log(response.policies); + // [ + // { policy: "old-activities", config: { max_age_hours: 8760 }, enabled_at: "..." }, + // { policy: "stories", config: { max_age_hours: 720, feed_groups: ["stories"] }, enabled_at: "..." } + // ] + // #endregion snippet + + expect(response.enabled).toBe(false); + const stories = response.policies.find((p) => p.policy === storiesPolicy); + expect(stories?.config).toEqual({ + max_age_hours: 720, + feed_groups: ['stories'], + }); + expect(typeof stories?.enabled_at).toBe('number'); + }); + + it('deletes a retention policy', async () => { + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/14-data-retention.md" heading="Deleting a retention policy" tab="Node.js" index=1 + // COPY: storiesPolicy="stories" + await client.feeds.feedsDeleteRetentionPolicy({ policy: storiesPolicy }); + // #endregion snippet + + expect(await getPolicy(storiesPolicy)).toBeUndefined(); + expect(await getPolicy('old-activities')).toBeDefined(); + }); + + it('queries the cleanup run history', async () => { + // #region snippet docs="activity-feeds/v3-latest/_default/06-features/14-data-retention.md" heading="Viewing cleanup run history" tab="Node.js" index=1 + // COPY: storiesPolicy="stories" + const response = await client.feeds.feedsGetRetentionPolicyRuns({ + filter_conditions: { policy: { $eq: storiesPolicy } }, + sort: [{ field: 'date', direction: -1 }], + limit: 10, + }); + + console.log(response.runs); + // [ + // { + // app_pk: 12345, + // policy: "stories", + // date: "2026-10-20", + // stats: { activities_deleted: 15420 } + // } + // ] + // #endregion snippet + + // A new policy's first cleanup runs after its 24-hour grace period. + expect(Array.isArray(response.runs)).toBe(true); + for (const run of response.runs) expect(run.policy).toBe(storiesPolicy); + }); +}); diff --git a/__tests__/docs-snippets/pages/chat/03-channels/04-query_members.test.ts b/__tests__/docs-snippets/pages/chat/03-channels/04-query_members.test.ts index 11a4eae7..b0afe2d6 100644 --- a/__tests__/docs-snippets/pages/chat/03-channels/04-query_members.test.ts +++ b/__tests__/docs-snippets/pages/chat/03-channels/04-query_members.test.ts @@ -1,4 +1,5 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { nsToDate } from '../../../../../index'; import type { MembersResponse } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; @@ -107,7 +108,7 @@ describe(DOCS, () => { filter_conditions: {}, sort: [{ field: 'created_at', direction: -1 }], limit: 100, - created_at_before: lastByCreatedAt.created_at, + created_at_before: nsToDate(lastByCreatedAt.created_at), }, }); diff --git a/__tests__/docs-snippets/pages/chat/03-channels/06-channel_members.test.ts b/__tests__/docs-snippets/pages/chat/03-channels/06-channel_members.test.ts index b370a6a5..35ef6c60 100644 --- a/__tests__/docs-snippets/pages/chat/03-channels/06-channel_members.test.ts +++ b/__tests__/docs-snippets/pages/chat/03-channels/06-channel_members.test.ts @@ -343,4 +343,33 @@ describe(DOCS, () => { expect(member.channel_role).toBe('channel_moderator'); expect(member.custom).toEqual({ key1: 'new value 1', key2: 'new value 2' }); }); + + it('sets app-wide and channel badges', async () => { + const channelId = await newChannel([janeId]); + const cid = `messaging:${channelId}`; + const channel = client.chat.channel('messaging', channelId); + + // #region snippet docs="chat/_default/03-channels/06-channel_members.md" heading="User Badges" tab="Node.js" index=1 + // COPY: janeId="jane" + // App-wide badge, returned with the user (for example on message.user) + await client.updateUsersPartial({ + users: [{ id: janeId, set: { badges: ['verified'] } }], + }); + + // Channel badge, returned with this channel's members + await channel.updateMemberPartial({ + user_id: janeId, + set: { badges: ['host'] }, + }); + // #endregion snippet + + const message = await sendServerMessage(client, cid, { + text: 'hi', + user_id: janeId, + }); + expect(message.user?.custom).toMatchObject({ badges: ['verified'] }); + const member = await getServerMember(client, cid, janeId); + expect(member.custom).toEqual({ badges: ['host'] }); + expect(member.user?.custom).toMatchObject({ badges: ['verified'] }); + }); }); diff --git a/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/10-muting.test.ts b/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/10-muting.test.ts index dd09d2a3..fb6e6d5d 100644 --- a/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/10-muting.test.ts +++ b/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/10-muting.test.ts @@ -1,4 +1,5 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { nsToMs } from '../../../../../../index'; import type { MuteChannelResponse, QueryChannelsResponse, @@ -82,14 +83,12 @@ describe(DOCS, () => { const now = Date.now(); const twoWeeksExpires = muteOf(twoWeeks)?.expires; expect(twoWeeksExpires).toBeDefined(); - expect(new Date(twoWeeksExpires ?? 0).getTime()).toBeGreaterThan( + expect(nsToMs(twoWeeksExpires ?? 0)).toBeGreaterThan( now + 13 * 24 * 60 * 60 * 1000, ); const tenSecondsExpires = muteOf(tenSeconds)?.expires; expect(tenSecondsExpires).toBeDefined(); - expect(new Date(tenSecondsExpires ?? 0).getTime()).toBeLessThan( - now + 60 * 1000, - ); + expect(nsToMs(tenSecondsExpires ?? 0)).toBeLessThan(now + 60 * 1000); expect(await mutedCids()).toEqual([cid]); }); diff --git a/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/13-deleting.test.ts b/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/13-deleting.test.ts index 2166fa5f..e39093f9 100644 --- a/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/13-deleting.test.ts +++ b/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/13-deleting.test.ts @@ -102,7 +102,7 @@ describe(DOCS, () => { deleteSpy.mockRestore(); expect(soft.channel?.cid).toBe(cid); - expect(soft.channel?.deleted_at).toBeInstanceOf(Date); + expect(soft.channel?.deleted_at).toEqual(expect.any(Number)); expect(hard.channel?.cid).toBe(otherCid); expect(await countChannels([cid, otherCid])).toBe(0); }); diff --git a/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/14-freezing.test.ts b/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/14-freezing.test.ts index 5fd3ffef..18e77f88 100644 --- a/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/14-freezing.test.ts +++ b/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/14-freezing.test.ts @@ -121,13 +121,8 @@ describe(DOCS, () => { const adminGrants = [...channelType.grants.admin, 'use-frozen-channel']; - // automod, automod_behavior and max_message_length are required on update and - // are not preserved when omitted, so read them back from the current type. await client.chat.updateChannelType({ name: 'messaging', - automod: channelType.automod, - automod_behavior: channelType.automod_behavior, - max_message_length: channelType.max_message_length, grants: { admin: adminGrants }, }); // #endregion snippet diff --git a/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/15-truncating.test.ts b/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/15-truncating.test.ts index 6bc5bff4..2ea92705 100644 --- a/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/15-truncating.test.ts +++ b/__tests__/docs-snippets/pages/chat/03-channels/07-channel_management/15-truncating.test.ts @@ -7,6 +7,7 @@ import { it, vi, } from 'vitest'; +import { nsToMs } from '../../../../../../index'; import type { TruncateChannelResponse } from '../../../../../../index'; import { getServerClient } from '../../../../helpers/clients'; import { Cleanup } from '../../../../helpers/cleanup'; @@ -136,11 +137,9 @@ describe(DOCS, () => { expect(visibleAfter[2]).toEqual([]); // `truncated_at` an hour ago keeps the messages sent since then. const truncatedAt = responses[3].channel?.truncated_at; - expect(truncatedAt).toBeInstanceOf(Date); + expect(truncatedAt).toEqual(expect.any(Number)); if (truncatedAt) { - expect(Date.now() - truncatedAt.getTime()).toBeGreaterThan( - 59 * 60 * 1000, - ); + expect(Date.now() - nsToMs(truncatedAt)).toBeGreaterThan(59 * 60 * 1000); } expect(visibleAfter[3]).toEqual(['recent', 'before-4']); }); diff --git a/__tests__/docs-snippets/pages/chat/04-messages/05-pinned_messages.test.ts b/__tests__/docs-snippets/pages/chat/04-messages/05-pinned_messages.test.ts index 5f95840c..7fcb6844 100644 --- a/__tests__/docs-snippets/pages/chat/04-messages/05-pinned_messages.test.ts +++ b/__tests__/docs-snippets/pages/chat/04-messages/05-pinned_messages.test.ts @@ -1,4 +1,5 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { convertTimestampToDate, nsToMs } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; @@ -83,10 +84,12 @@ describe(DOCS, () => { expect(message.pinned).toBe(true); expect(message.pinned_by?.id).toBe(john); - expect(message.pin_expires?.toISOString()).toBe('2077-01-01T00:00:00.000Z'); + expect(convertTimestampToDate(message.pin_expires)?.toISOString()).toBe( + '2077-01-01T00:00:00.000Z', + ); expect(timed.message?.pinned).toBe(true); - const timedExpiry = timed.message?.pin_expires?.getTime() ?? 0; + const timedExpiry = nsToMs(timed.message?.pin_expires ?? 0); expect(timedExpiry).toBeGreaterThanOrEqual(before + 110 * 1000); expect(timedExpiry).toBeLessThan(Date.now() + 130 * 1000); diff --git a/__tests__/docs-snippets/pages/chat/04-messages/08-unread_reminders.test.ts b/__tests__/docs-snippets/pages/chat/04-messages/08-unread_reminders.test.ts index 66f3cf73..0da4a8b4 100644 --- a/__tests__/docs-snippets/pages/chat/04-messages/08-unread_reminders.test.ts +++ b/__tests__/docs-snippets/pages/chat/04-messages/08-unread_reminders.test.ts @@ -41,16 +41,9 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/04-messages/08-unread_reminders.md" heading="Enabling Unread Reminders" tab="Node.js" index=1 // COPY: channelType="messaging" - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: channelType }); - // Enable reminders for the messaging channel type await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, reminders: true, }); @@ -64,8 +57,8 @@ describe(DOCS, () => { return response; }); expect(updated.reminders).toBe(true); - expect(updated.automod).toBe(current.automod); - expect(updated.max_message_length).toBe(current.max_message_length); + expect(updated.automod).toBe(before.automod); + expect(updated.max_message_length).toBe(before.max_message_length); await waitForAppSetting(client, 'reminders_interval', 3600); const { app } = await client.getApp(); diff --git a/__tests__/docs-snippets/pages/chat/04-messages/09-message_reminders.test.ts b/__tests__/docs-snippets/pages/chat/04-messages/09-message_reminders.test.ts index 22be9d31..a16798ca 100644 --- a/__tests__/docs-snippets/pages/chat/04-messages/09-message_reminders.test.ts +++ b/__tests__/docs-snippets/pages/chat/04-messages/09-message_reminders.test.ts @@ -1,4 +1,5 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { nsToMs } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; @@ -104,22 +105,16 @@ describe(DOCS, () => { }); // Enabling it for a channel type. - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: channelType }); const update = await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, user_message_reminders: true, }); // #endregion snippet await waitForChannelTypePropagation(); expect(update.user_message_reminders).toBe(true); - expect(update.automod).toBe(current.automod); - expect(update.max_message_length).toBe(current.max_message_length); + expect(update.automod).toBe(before.automod); + expect(update.max_message_length).toBe(before.max_message_length); const updated = await client.chat.getChannelType({ name: channelType }); expect(updated.user_message_reminders).toBe(true); @@ -168,8 +163,10 @@ describe(DOCS, () => { [messageId, anotherMessageId].sort(), ); const timed = byMessage.get(messageId)?.remind_at; - expect(timed).toBeInstanceOf(Date); - expect(timed?.getTime()).toBeGreaterThanOrEqual(startedAt + 3600000 - 1000); + expect(timed).toEqual(expect.any(Number)); + expect(nsToMs(timed ?? 0)).toBeGreaterThanOrEqual( + startedAt + 3600000 - 1000, + ); expect(byMessage.get(anotherMessageId)?.remind_at ?? null).toBeNull(); }); @@ -203,8 +200,8 @@ describe(DOCS, () => { // #endregion snippet const newDueDate = updated.reminder.remind_at; - expect(newDueDate).toBeInstanceOf(Date); - expect(newDueDate?.getTime()).toBeGreaterThanOrEqual( + expect(newDueDate).toEqual(expect.any(Number)); + expect(nsToMs(newDueDate ?? 0)).toBeGreaterThanOrEqual( startedAt + 7200000 - 1000, ); expect(log.mock.calls[0]).toEqual([newDueDate]); diff --git a/__tests__/docs-snippets/pages/chat/04-messages/10-message_receipts.test.ts b/__tests__/docs-snippets/pages/chat/04-messages/10-message_receipts.test.ts index e9e68f2f..a7b6702c 100644 --- a/__tests__/docs-snippets/pages/chat/04-messages/10-message_receipts.test.ts +++ b/__tests__/docs-snippets/pages/chat/04-messages/10-message_receipts.test.ts @@ -46,16 +46,8 @@ describe(DOCS, () => { }); // When updating an existing channel type. - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ - name: targetChannelType, - }); await client.chat.updateChannelType({ name: targetChannelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, delivery_events: true, }); // #endregion snippet @@ -67,14 +59,13 @@ describe(DOCS, () => { ), ); expect(created.delivery_events).toBe(true); - expect(current.delivery_events).toBe(true); - expect(current.automod).toBe('disabled'); const [updated] = await Promise.all( updateSpy.mock.results.map( (r) => r.value as ReturnType, ), ); expect(updated.delivery_events).toBe(true); + expect(updated.automod).toBe('disabled'); const after = await client.chat.getChannelType({ name: targetChannelType }); expect(after.delivery_events).toBe(true); expect(after.max_message_length).toBe(5000); @@ -120,16 +111,8 @@ describe(DOCS, () => { }); // When updating an existing channel type. - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ - name: targetChannelType, - }); await client.chat.updateChannelType({ name: targetChannelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, read_events: true, }); // #endregion snippet @@ -141,13 +124,13 @@ describe(DOCS, () => { ), ); expect(created.read_events).toBe(true); - expect(current.read_events).toBe(true); const [updated] = await Promise.all( updateSpy.mock.results.map( (r) => r.value as ReturnType, ), ); expect(updated.read_events).toBe(true); + expect(updated.automod).toBe('disabled'); const after = await client.chat.getChannelType({ name: targetChannelType }); expect(after.read_events).toBe(true); expect(after.max_message_length).toBe(5000); diff --git a/__tests__/docs-snippets/pages/chat/05-features/03-unread.test.ts b/__tests__/docs-snippets/pages/chat/05-features/03-unread.test.ts index f4343064..cbb6d0d2 100644 --- a/__tests__/docs-snippets/pages/chat/05-features/03-unread.test.ts +++ b/__tests__/docs-snippets/pages/chat/05-features/03-unread.test.ts @@ -228,7 +228,7 @@ describe(DOCS, () => { ); expect(unreadByUser).toEqual({ [sender]: 0, [userID]: 1, [userID2]: 0 }); expect(log).toHaveBeenCalledTimes(3); - expect(log).toHaveBeenCalledWith(userID, expect.any(Date), 1); + expect(log).toHaveBeenCalledWith(userID, expect.any(Number), 1); }); it('reads the unread messages per channel', async () => { diff --git a/__tests__/docs-snippets/pages/chat/05-features/06-campaign_api.test.ts b/__tests__/docs-snippets/pages/chat/05-features/06-campaign_api.test.ts index 022d11c4..dbd3c513 100644 --- a/__tests__/docs-snippets/pages/chat/05-features/06-campaign_api.test.ts +++ b/__tests__/docs-snippets/pages/chat/05-features/06-campaign_api.test.ts @@ -7,6 +7,7 @@ import { it, vi, } from 'vitest'; +import { nsToMs } from '../../../../../index'; import { StreamError } from '../../../../../index'; import type { CampaignResponse, @@ -137,7 +138,7 @@ describe(DOCS, () => { expect(['scheduled', 'in_progress', 'completed']).toContain( campaign?.status, ); - expect(campaign?.scheduled_for?.getTime() ?? 0).toBeLessThan( + expect(nsToMs(campaign?.scheduled_for ?? 0)).toBeLessThan( Date.now() + 60000, ); }; @@ -306,8 +307,8 @@ describe(DOCS, () => { id: anotherCampaignId, }); expect(scheduled?.status).toBe('scheduled'); - expect(scheduled?.scheduled_for).toBeInstanceOf(Date); - expect(scheduled?.stop_at).toBeInstanceOf(Date); + expect(scheduled?.scheduled_for).toEqual(expect.any(Number)); + expect(scheduled?.stop_at).toEqual(expect.any(Number)); }); it('updates, starts, stops, deletes and queries a campaign', async () => { @@ -837,7 +838,7 @@ describe(DOCS, () => { expect(typeof stats?.progress).toBe('number'); expect(typeof stats?.stats_messages_sent).toBe('number'); expect(typeof stats?.stats_users_sent).toBe('number'); - expect(stats?.stats_started_at).toBeInstanceOf(Date); + expect(stats?.stats_started_at).toEqual(expect.any(Number)); }); }); diff --git a/__tests__/docs-snippets/pages/chat/05-features/08-location_sharing.test.ts b/__tests__/docs-snippets/pages/chat/05-features/08-location_sharing.test.ts index f72f065e..25668756 100644 --- a/__tests__/docs-snippets/pages/chat/05-features/08-location_sharing.test.ts +++ b/__tests__/docs-snippets/pages/chat/05-features/08-location_sharing.test.ts @@ -8,6 +8,7 @@ import { vi, type MockInstance, } from 'vitest'; +import { nsToMs } from '../../../../../index'; import type { StreamClient } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; @@ -145,15 +146,8 @@ describe(DOCS, () => { }); // Enabling it for a channel type. - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, shared_locations: true, }); // #endregion snippet @@ -246,7 +240,7 @@ describe(DOCS, () => { longitude: 10, message_id: message.id, }); - expect(message.shared_location?.end_at?.getTime()).toBeGreaterThan( + expect(nsToMs(message.shared_location?.end_at ?? 0)).toBeGreaterThan( Date.now(), ); expect(await activeLiveLocationIds()).toContain(message.id); @@ -274,10 +268,8 @@ describe(DOCS, () => { StreamClient['updateLiveLocation'] >); expect(stopped.message_id).toBe(messageID); - expect(stopped.end_at?.getTime()).toBeLessThanOrEqual(Date.now()); - expect((await storedLocation(messageID))?.end_at?.getTime()).toBe( - stopped.end_at?.getTime(), - ); + expect(nsToMs(stopped.end_at ?? 0)).toBeLessThanOrEqual(Date.now()); + expect((await storedLocation(messageID))?.end_at).toBe(stopped.end_at); expect(await activeLiveLocationIds()).not.toContain(messageID); }); diff --git a/__tests__/docs-snippets/pages/chat/05-features/10-advanced/15-pending_messages.test.ts b/__tests__/docs-snippets/pages/chat/05-features/10-advanced/15-pending_messages.test.ts index 62ee1e07..059b075d 100644 --- a/__tests__/docs-snippets/pages/chat/05-features/10-advanced/15-pending_messages.test.ts +++ b/__tests__/docs-snippets/pages/chat/05-features/10-advanced/15-pending_messages.test.ts @@ -83,15 +83,8 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/05-features/10-advanced/15-pending_messages.md" heading="Sending Pending Messages" tab="Node.js" index=1 // COPY: channelType="messaging" - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: channelType }); - const response = await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, mark_messages_pending: true, }); // #endregion snippet diff --git a/__tests__/docs-snippets/pages/chat/05-features/10-advanced/16-dynamic_partitioning.test.ts b/__tests__/docs-snippets/pages/chat/05-features/10-advanced/16-dynamic_partitioning.test.ts index ca72376d..614b3299 100644 --- a/__tests__/docs-snippets/pages/chat/05-features/10-advanced/16-dynamic_partitioning.test.ts +++ b/__tests__/docs-snippets/pages/chat/05-features/10-advanced/16-dynamic_partitioning.test.ts @@ -103,19 +103,13 @@ describe(DOCS, () => { }); it('updates the partition size', async () => { + const before = await client.chat.getChannelType({ name: channelType }); const updateSpy = vi.spyOn(client.chat, 'updateChannelType'); // #region snippet docs="chat/_default/05-features/10-advanced/16-dynamic_partitioning.md" heading="Updating dynamic partitioning" tab="Node.js" index=1 // COPY: channelType="example" - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, partition_size: 200, }); // #endregion snippet @@ -123,8 +117,8 @@ describe(DOCS, () => { const updated = await (updateSpy.mock.results[0] .value as Promise); expect(updated.partition_size).toBe(200); - expect(updated.automod).toBe(current.automod); - expect(updated.max_message_length).toBe(current.max_message_length); + expect(updated.automod).toBe(before.automod); + expect(updated.max_message_length).toBe(before.max_message_length); await waitForChannelTypePropagation(); const type = await waitForChannelType((r) => r.partition_size === 200); @@ -148,13 +142,8 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/05-features/10-advanced/16-dynamic_partitioning.md" heading="Disabling dynamic partitioning" tab="Node.js" index=1 // COPY: channelType="example" - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, partition_size: 0, }); // #endregion snippet @@ -188,13 +177,8 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/05-features/10-advanced/16-dynamic_partitioning.md" heading="Partition TTL" tab="Node.js" index=1 // COPY: channelType="example" - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, partition_ttl: '24h', }); // #endregion snippet diff --git a/__tests__/docs-snippets/pages/chat/06-app_and_channel_settings/01-channel_types.test.ts b/__tests__/docs-snippets/pages/chat/06-app_and_channel_settings/01-channel_types.test.ts index 9bd9281a..673da120 100644 --- a/__tests__/docs-snippets/pages/chat/06-app_and_channel_settings/01-channel_types.test.ts +++ b/__tests__/docs-snippets/pages/chat/06-app_and_channel_settings/01-channel_types.test.ts @@ -94,15 +94,8 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/06-app_and_channel_settings/01-channel_types.md" heading="Restricting guest access" tab="Node.js" index=1 // COPY: agentType="agent" - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: agentType }); - await client.chat.updateChannelType({ name: agentType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, grants: { guest: [], // guests can no longer create agent channels or act as owners }, @@ -146,8 +139,6 @@ describe(DOCS, () => { // Update an existing channel type await client.chat.updateChannelType({ name: myChannelType, - automod: 'disabled', - automod_behavior: 'flag', max_message_length: 1000, reactions: false, }); @@ -214,15 +205,11 @@ describe(DOCS, () => { }); it('edits commands and replies', async () => { + const before = await client.chat.getChannelType({ name: publicType }); // #region snippet docs="chat/_default/06-app_and_channel_settings/01-channel_types.md" heading="Edit a Channel Type" tab="Node.js" index=1 // COPY: publicType="public" - const current = await client.chat.getChannelType({ name: publicType }); - const update = await client.chat.updateChannelType({ name: publicType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, replies: false, commands: ['all'], }); @@ -233,7 +220,7 @@ describe(DOCS, () => { expect(update.commands).toEqual( expect.arrayContaining(['ban', 'giphy', 'mute', 'unban', 'unmute']), ); - expect(update.max_message_length).toBe(current.max_message_length); + expect(update.max_message_length).toBe(before.max_message_length); await waitForChannelTypePropagation(); const type = await waitForChannelType(publicType, (r) => !r.replies); @@ -245,13 +232,8 @@ describe(DOCS, () => { it('updates feature flags', async () => { // #region snippet docs="chat/_default/06-app_and_channel_settings/01-channel_types.md" heading="Edit a Channel Type" tab="Node.js" index=2 // COPY: publicType="public" - const current = await client.chat.getChannelType({ name: publicType }); - const update = await client.chat.updateChannelType({ name: publicType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, typing_events: false, read_events: true, connect_events: true, @@ -283,12 +265,8 @@ describe(DOCS, () => { it('updates settings with new values', async () => { // #region snippet docs="chat/_default/06-app_and_channel_settings/01-channel_types.md" heading="Edit a Channel Type" tab="Node.js" index=3 // COPY: publicType="public" - const current = await client.chat.getChannelType({ name: publicType }); - const update = await client.chat.updateChannelType({ name: publicType, - automod: current.automod, - automod_behavior: current.automod_behavior, max_message_length: 140, // the value this example is changing commands: ['giphy', 'ban'], }); diff --git a/__tests__/docs-snippets/pages/chat/06-app_and_channel_settings/03-chat_permission_policies.test.ts b/__tests__/docs-snippets/pages/chat/06-app_and_channel_settings/03-chat_permission_policies.test.ts index 915ad1c3..275a65e3 100644 --- a/__tests__/docs-snippets/pages/chat/06-app_and_channel_settings/03-chat_permission_policies.test.ts +++ b/__tests__/docs-snippets/pages/chat/06-app_and_channel_settings/03-chat_permission_policies.test.ts @@ -249,9 +249,6 @@ describe(DOCS, () => { // update "channel_member" role grants in "messaging" scope await client.chat.updateChannelType({ name: channelType, - automod: 'disabled', - automod_behavior: 'flag', - max_message_length: 5000, grants: { channel_member: [ 'read-channel', // allow access to the channel @@ -287,13 +284,8 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/06-app_and_channel_settings/03-chat_permission_policies.md" heading="Granting permissions" tab="Node.js" index=3 // COPY: channelType="messaging" - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, grants: { guest: [], // removes all grants of "guest" role anonymous: [], // removes all grants of "anonymous" role @@ -320,15 +312,8 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/06-app_and_channel_settings/03-chat_permission_policies.md" heading="Granting permissions" tab="Node.js" index=4 // COPY: channelType="messaging" - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, grants: null, // resets the whole scope to default settings }); // #endregion snippet diff --git a/__tests__/docs-snippets/pages/chat/12-best_practices/02-moderation.test.ts b/__tests__/docs-snippets/pages/chat/12-best_practices/02-moderation.test.ts index 235908f8..2bb27ffc 100644 --- a/__tests__/docs-snippets/pages/chat/12-best_practices/02-moderation.test.ts +++ b/__tests__/docs-snippets/pages/chat/12-best_practices/02-moderation.test.ts @@ -103,16 +103,9 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/12-best_practices/02-moderation.md" heading="Disabling the permission to post links or add attachments" tab="Node.js" index=1 // COPY: channelType="messaging" - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: channelType }); - // Remove link and attachment permissions for channel_member role await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, grants: { channel_member: [ 'read-channel', @@ -320,16 +313,9 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/12-best_practices/02-moderation.md" heading="Slash commands for banning" tab="Node.js" index=1 // COPY: channelType="messaging" - // automod, automod_behavior and max_message_length are required here, so read - // the current values and carry them through. - const current = await client.chat.getChannelType({ name: channelType }); - // Enable ban/unban commands for a channel type await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, commands: ['giphy', 'ban', 'unban', 'mute', 'unmute', 'flag'], }); // #endregion snippet @@ -490,6 +476,68 @@ describe(DOCS, () => { expect(log.mock.calls.at(-1)).toEqual([response.next]); }); + it('turns on two-way blocking', async () => { + const { app } = await client.getApp(); + const original = app.two_way_block_effect_enabled; + expect(original).toBe(false); + const restore = async () => { + await client.updateApp({ two_way_block_effect_enabled: original }); + await waitForAppSetting(client, 'two_way_block_effect_enabled', original); + }; + cleanup.add(restore); + + // A blocks B, and both are in a 1-on-1 channel and a group channel. + const blockerId = await createUser('blocker'); + const blockedId = await createUser('blocked'); + const otherId = await createUser('other'); + await client.blockUsers({ blocked_user_id: blockedId, user_id: blockerId }); + const newChannel = async (members: string[]) => { + const id = uniqueId('channel'); + cleanup.channels.push(`messaging:${id}`); + await client.chat.channel('messaging', id).getOrCreate({ + data: { + created_by_id: blockerId, + members: members.map((user_id) => ({ user_id })), + }, + }); + return `messaging:${id}`; + }; + const oneOnOneCid = await newChannel([blockerId, blockedId]); + const groupCid = await newChannel([blockerId, blockedId, otherId]); + const unreadFor = async (cid: string, userId: string) => { + const channel = await getServerChannel(client, cid, { user_id: userId }); + return channel.read?.find((r) => r.user.id === userId)?.unread_messages; + }; + + // #region snippet docs="chat/_default/12-best_practices/02-moderation.md" heading="Two-way blocking" tab="Node.js" index=1 + await client.updateApp({ + two_way_block_effect_enabled: true, + }); + // #endregion snippet + + await waitForAppSetting(client, 'two_way_block_effect_enabled', true, { + agreeing: 5, + }); + + // A's messages no longer count towards B's unread counts in their 1-on-1 + // channel; group channels are unaffected (the positive control). + await sendServerMessage(client, oneOnOneCid, { + text: 'hi', + user_id: blockerId, + }); + await sendServerMessage(client, groupCid, { + text: 'hi all', + user_id: blockerId, + }); + await retry(async () => { + expect(await unreadFor(groupCid, blockedId)).toBe(1); + }); + expect(await unreadFor(oneOnOneCid, blockedId)).toBe(0); + + // Restore right away: the setting changes chat behavior app-wide. + await restore(); + }); + it('blocks, lists and unblocks a user', async () => { const user1Id = await createUser('user1'); const user2Id = await createUser('user2'); diff --git a/__tests__/docs-snippets/pages/chat/12-best_practices/05-marketplace_best_practices.test.ts b/__tests__/docs-snippets/pages/chat/12-best_practices/05-marketplace_best_practices.test.ts index 1a3ad23e..3da60163 100644 --- a/__tests__/docs-snippets/pages/chat/12-best_practices/05-marketplace_best_practices.test.ts +++ b/__tests__/docs-snippets/pages/chat/12-best_practices/05-marketplace_best_practices.test.ts @@ -78,16 +78,9 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/12-best_practices/05-marketplace_best_practices.md" heading="Unread Message Reminders" tab="Node.js" index=1 // COPY: channelType="messaging" - // Enable reminders for the messaging channel type. automod, automod_behavior and - // max_message_length are required on this endpoint, so carry the current values - // through to avoid overwriting them. - const current = await client.chat.getChannelType({ name: channelType }); - + // Enable reminders for the messaging channel type await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, reminders: true, }); @@ -98,7 +91,7 @@ describe(DOCS, () => { await waitForChannelTypePropagation(); const updated = await client.chat.getChannelType({ name: featuresType }); expect(updated.reminders).toBe(true); - expect(updated.max_message_length).toBe(current.max_message_length); + expect(updated.max_message_length).toBe(before.max_message_length); await waitForAppSetting(client, 'reminders_interval', 3600); }); @@ -151,15 +144,8 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/12-best_practices/05-marketplace_best_practices.md" heading="Pending Messages" tab="Node.js" index=1 // COPY: channelType="messaging" - // automod, automod_behavior and max_message_length are required on this endpoint, - // so carry the current values through to avoid overwriting them. - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, mark_messages_pending: true, }); // #endregion snippet @@ -185,15 +171,8 @@ describe(DOCS, () => { // #region snippet docs="chat/_default/12-best_practices/05-marketplace_best_practices.md" heading="Recommended Settings" tab="Node.js" index=1 // COPY: channelType="messaging" - // automod, automod_behavior and max_message_length are required on this endpoint, - // so carry the current values through to avoid overwriting them. - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, typing_events: true, // Show when users are typing read_events: true, // Track read status for messages reactions: true, // Allow reactions for quick responses diff --git a/__tests__/docs-snippets/pages/moderation/02-quick-start/03-quick-start-feeds-moderation.test.ts b/__tests__/docs-snippets/pages/moderation/02-quick-start/03-quick-start-feeds-moderation.test.ts index 129c53e6..6a554027 100644 --- a/__tests__/docs-snippets/pages/moderation/02-quick-start/03-quick-start-feeds-moderation.test.ts +++ b/__tests__/docs-snippets/pages/moderation/02-quick-start/03-quick-start-feeds-moderation.test.ts @@ -3,6 +3,7 @@ import type { QueryFeedModerationTemplatesResponse } from '../../../../../index' import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; +import { waitForReviewItem } from '../../../helpers/server'; const DOCS = 'moderation/_default/02-quick-start/03-quick-start-feeds-moderation.md'; @@ -82,6 +83,7 @@ describe(DOCS, () => { // arguments (TS2554) and sends no name, so it fails with code 4 "name is // a required field". Cleanup deletes the template instead. it.skip('BLOCKED: SDK issue: v2DeleteTemplate() cannot send the template name', async () => { + // @ts-expect-error SDK issue: generated v2DeleteTemplate() takes no arguments, so the template name is not sent // #region snippet docs="moderation/_default/02-quick-start/03-quick-start-feeds-moderation.md" heading="Delete Template" tab="Node.js" index=1 // COPY: templateName="mod_tmpl_1" await client.moderation.v2DeleteTemplate({ name: templateName }); @@ -89,4 +91,39 @@ describe(DOCS, () => { expect(await findTemplate(templateName)).toBeUndefined(); }); + + // The node SDK can't create Feeds v2 activities (the test app has no Feeds v2 data). + // Don't run this against the shared app: the failing flag still stores a review + // queue item, and every read that includes it (getReviewQueueItem, a + // queryReviewQueue page with it, submitAction on it) fails with the same empty + // code -1 error (2026-10-29). + it.skip('BLOCKED: flagging a Feeds v2 activity id that does not exist fails with code -1 (500), message: V2Flag failed with error: ""', async () => { + // The docs' undefined app values: a Feeds v2 activity (the moderation API takes + // any entity id) and its users. + const activityId = uniqueId('activity'); + const actorId = uniqueId('actor'); + const reporterId = uniqueId('reporter'); + cleanup.users.push(actorId, reporterId); + await client.upsertUsers([{ id: actorId }, { id: reporterId }]); + + // #region snippet docs="moderation/_default/02-quick-start/03-quick-start-feeds-moderation.md" heading="User-Driven Actions" tab="Node.js" index=1 + await client.moderation.flag({ + entity_type: 'stream:feeds:v2:activity', + entity_id: activityId, + entity_creator_id: actorId, + reason: 'inappropriate_content', + user_id: reporterId, + }); + // #endregion snippet + + const item = await waitForReviewItem(client, activityId); + expect(item).toMatchObject({ + entity_type: 'stream:feeds:v2:activity', + entity_creator_id: actorId, + }); + expect(item.flags[0]).toMatchObject({ + reason: 'inappropriate_content', + user_id: reporterId, + }); + }); }); diff --git a/__tests__/docs-snippets/pages/moderation/02-quick-start/04-custom-content-moderation.test.ts b/__tests__/docs-snippets/pages/moderation/02-quick-start/04-custom-content-moderation.test.ts new file mode 100644 index 00000000..c7bf65ff --- /dev/null +++ b/__tests__/docs-snippets/pages/moderation/02-quick-start/04-custom-content-moderation.test.ts @@ -0,0 +1,111 @@ +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from 'vitest'; +import { getServerClient } from '../../../helpers/clients'; +import { Cleanup } from '../../../helpers/cleanup'; +import { uniqueId } from '../../../helpers/ids'; +import { waitForReviewItem } from '../../../helpers/server'; +import { retry } from '../../../helpers/wait'; + +const DOCS = + 'moderation/_default/02-quick-start/04-custom-content-moderation.md'; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + // The docs' "custom:my-content-platform-1" policy, test-owned: a blocklist that + // flags a word of the docs text. + const configKey = `custom:${uniqueId('my-content-platform')}`; + const blocklist = uniqueId('custom-content-words'); + // The docs' "entity_type" placeholder, test-owned, so the review queue query of the + // next snippet only matches this file's items. + const entityType = uniqueId('entity-type'); + const entityId = uniqueId('entity'); + const creatorId = uniqueId('entity-creator'); + + beforeAll(async () => { + cleanup.users.push(creatorId); + await client.upsertUsers([{ id: creatorId }]); + await client.createBlockList({ name: blocklist, words: ['bullshit'] }); + cleanup.add(() => client.deleteBlockList({ name: blocklist })); + // A new blocklist isn't known to the moderation API right away ("Blocklist not found"). + await retry( + () => + client.moderation.upsertConfig({ + key: configKey, + block_list_config: { rules: [{ name: blocklist, action: 'flag' }] }, + }), + { retryIf: (error) => String(error).includes('Blocklist not found') }, + ); + cleanup.add(() => client.moderation.deleteConfig({ key: configKey })); + // A new policy can take a moment to apply. + await retry(async () => { + const { recommended_action } = await client.moderation.check({ + entity_type: entityType, + entity_id: uniqueId('warmup'), + entity_creator_id: creatorId, + config_key: configKey, + moderation_payload: { texts: ['bullshit'] }, + }); + if (recommended_action !== 'flag') { + throw new Error(`recommended_action: ${recommended_action}`); + } + }); + }); + + afterEach(() => vi.restoreAllMocks()); + afterAll(() => cleanup.run()); + + it('submits custom content to the Check API', async () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + // #region snippet docs="moderation/_default/02-quick-start/04-custom-content-moderation.md" heading="Submit custom content to the moderation Check API" tab="Node.js" index=1 + // COPY: entityType="entity_type", entityId="entity_id", creatorId="entity_creator_id", configKey="custom:my-content-platform-1" + const response = await client.moderation.check({ + entity_type: entityType, + entity_id: entityId, + entity_creator_id: creatorId, + moderation_payload: { + texts: ['this is bullshit', 'f*ck you as***le'], + images: ['example.com/test.jpg'], + }, + config_key: configKey, + }); + + console.log(response.recommended_action); // keep | flag | remove | shadow + // #endregion snippet + + expect(response.recommended_action).toBe('flag'); + expect(log.mock.calls[0]).toEqual(['flag']); + expect(['complete', 'partial']).toContain(response.status); + expect(response.item).toMatchObject({ + entity_id: entityId, + entity_type: entityType, + }); + }); + + it('queries flagged content', async () => { + // The flag of the previous snippet, once it reached the review queue. + const flagged = await waitForReviewItem(client, entityId); + + // #region snippet docs="moderation/_default/02-quick-start/04-custom-content-moderation.md" heading="Query Flagged Content" tab="Node.js" index=1 + // COPY: entityType="entity_type" + const response = await client.moderation.queryReviewQueue({ + filter: { entity_type: entityType }, + sort: [{ field: 'created_at', direction: -1 }], + }); + // #endregion snippet + + // The newest item is the checked entity; the warm-up checks come after it. + expect(response.items[0].id).toBe(flagged.id); + for (const item of response.items) { + expect(item.entity_type).toBe(entityType); + } + }); +}); diff --git a/__tests__/docs-snippets/pages/moderation/02-quick-start/07-quick-start-call-moderation.test.ts b/__tests__/docs-snippets/pages/moderation/02-quick-start/07-quick-start-call-moderation.test.ts index f71a9d45..dc809a89 100644 --- a/__tests__/docs-snippets/pages/moderation/02-quick-start/07-quick-start-call-moderation.test.ts +++ b/__tests__/docs-snippets/pages/moderation/02-quick-start/07-quick-start-call-moderation.test.ts @@ -7,37 +7,9 @@ import { uniqueId } from '../../../helpers/ids'; const DOCS = 'moderation/_default/02-quick-start/07-quick-start-call-moderation.md'; -// Values the module-level region below uses (it must come after them: TDZ). -const apiKey = process.env.STREAM_API_KEY as string; -const secret = process.env.STREAM_SECRET as string; -// The docs' "my-room-12345" entity and "user-abc" creator, test-owned. -const roomEntityId = uniqueId('my-room'); -const creatorId = uniqueId('user-abc'); -// A real image: an unreachable one makes the check return `status: "partial"`. -const keyframeUrl = 'https://getstream.io/images/og/OG_Home.png'; -await getServerClient().upsertUsers([{ id: creatorId }]); - -// #region snippet docs="moderation/_default/02-quick-start/07-quick-start-call-moderation.md" heading="Sending Keyframes" tab="Node.js" index=1 -// COPY: roomEntityId="my-room-12345", creatorId="user-abc", keyframeUrl="https://your-cdn.com/keyframe-001.jpg" -import { StreamClient } from '@stream-io/node-sdk'; - -const client = new StreamClient(apiKey, secret); - -const response = await client.moderation.check({ - entity_type: 'external:call', - entity_id: roomEntityId, - entity_creator_id: creatorId, - content_published_at: new Date(), - moderation_payload: { - images: [keyframeUrl], - }, -}); -// #endregion snippet - describe(DOCS, () => { - const serverClient = getServerClient(); - const cleanup = new Cleanup(serverClient); - cleanup.users.push(creatorId); + const client = getServerClient(); + const cleanup = new Cleanup(client); /** Deletes the rules `upsertModerationRule` created in a region, by the ids it returned. */ const deleteUpsertedRules = (spy: { @@ -49,7 +21,7 @@ describe(DOCS, () => { await (r.value as Promise); if (!rule) throw new Error('upsertModerationRule returned no rule'); cleanup.add(() => - serverClient.moderation.deleteModerationRule({ id: rule.id }), + client.moderation.deleteModerationRule({ id: rule.id }), ); return rule; }), @@ -63,7 +35,7 @@ describe(DOCS, () => { const callRuleName = uniqueId('inappropriate-content-detection'); const upsertRule = vi.spyOn(client.moderation, 'upsertModerationRule'); - // #region snippet docs="moderation/_default/02-quick-start/07-quick-start-call-moderation.md" heading="Configure Via API" tab="Node.js" index=1 + // #region snippet docs="moderation/_default/02-quick-start/07-quick-start-call-moderation.md" heading="Configure via API" tab="Node.js" index=1 // COPY: callRuleName="Inappropriate Content Detection" await client.moderation.upsertModerationRule({ name: callRuleName, @@ -143,7 +115,7 @@ describe(DOCS, () => { const userRuleName = uniqueId('ban-repeat-call-offenders'); const upsertRule = vi.spyOn(client.moderation, 'upsertModerationRule'); - // #region snippet docs="moderation/_default/02-quick-start/07-quick-start-call-moderation.md" heading="Configure Via API" tab="Node.js" index=2 + // #region snippet docs="moderation/_default/02-quick-start/07-quick-start-call-moderation.md" heading="Configure via API" tab="Node.js" index=2 // COPY: userRuleName="Ban Repeat Call Offenders" await client.moderation.upsertModerationRule({ name: userRuleName, @@ -174,76 +146,4 @@ describe(DOCS, () => { ], }); }); - - it('sends keyframes', () => { - // The check ran at module level (the fence imports the SDK). - expect(client).toBeInstanceOf(StreamClient); - expect(response.status).toBe('complete'); - expect(response.recommended_action).toBe('keep'); - }); - - it('sends closed captions', async () => { - // #region snippet docs="moderation/_default/02-quick-start/07-quick-start-call-moderation.md" heading="Sending Closed Captions" tab="Node.js" index=1 - // COPY: roomEntityId="my-room-12345", creatorId="user-abc" - const response = await client.moderation.check({ - entity_type: 'external:call', - entity_id: roomEntityId, - entity_creator_id: creatorId, - content_published_at: new Date(), - moderation_payload: { - texts: ['transcribed audio segment from the call'], - }, - }); - // #endregion snippet - - expect(response.status).toBe('complete'); - expect(response.recommended_action).toBe('keep'); - }); - - it('runs a continuous moderation loop', async () => { - const roomId = uniqueId('room'); - - // #region snippet docs="moderation/_default/02-quick-start/07-quick-start-call-moderation.md" heading="Example: Continuous Moderation Loop" tab="Node.js" index=1 - const ENTITY_ID = `room-${roomId}`; - const INTERVAL_MS = 5000; - - async function moderateKeyframe(imageUrl: string, userId: string) { - return client.moderation.check({ - entity_type: 'external:call', - entity_id: ENTITY_ID, - entity_creator_id: userId, - content_published_at: new Date(), - moderation_payload: { - images: [imageUrl], - }, - }); - } - - async function moderateCaption(text: string, userId: string) { - return client.moderation.check({ - entity_type: 'external:call', - entity_id: ENTITY_ID, - entity_creator_id: userId, - content_published_at: new Date(), - moderation_payload: { - texts: [text], - }, - }); - } - // #endregion snippet - - expect(INTERVAL_MS).toBe(5000); - const check = vi.spyOn(client.moderation, 'check'); - const keyframe = await moderateKeyframe(keyframeUrl, creatorId); - const caption = await moderateCaption('hello everyone', creatorId); - // With the page's call rule (keyframe condition) in place, the image is analysed - // asynchronously: `status: "partial"` plus a `task_id`. - expect(['complete', 'partial']).toContain(keyframe.status); - expect(keyframe.recommended_action).toBe('keep'); - expect(caption.status).toBe('complete'); - expect(check.mock.calls.map(([request]) => request.entity_id)).toEqual([ - ENTITY_ID, - ENTITY_ID, - ]); - }); }); diff --git a/__tests__/docs-snippets/pages/moderation/02-quick-start/09-your-own-video.test.ts b/__tests__/docs-snippets/pages/moderation/02-quick-start/09-your-own-video.test.ts new file mode 100644 index 00000000..14a24153 --- /dev/null +++ b/__tests__/docs-snippets/pages/moderation/02-quick-start/09-your-own-video.test.ts @@ -0,0 +1,62 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { getServerClient } from '../../../helpers/clients'; +import { Cleanup } from '../../../helpers/cleanup'; +import { uniqueId } from '../../../helpers/ids'; + +const DOCS = 'moderation/_default/02-quick-start/09-your-own-video.md'; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + // The docs' "my-room-12345" entity and "user-abc" creator, test-owned. + const roomEntityId = uniqueId('my-room'); + const creatorId = uniqueId('user-abc'); + + beforeAll(async () => { + cleanup.users.push(creatorId); + await client.upsertUsers([{ id: creatorId }]); + }); + + afterAll(() => cleanup.run()); + + it('sends a keyframe', async () => { + // A real image: an unreachable one makes the check return `status: "partial"`. + const keyframeUrl = 'https://getstream.io/images/og/OG_Home.png'; + + // #region snippet docs="moderation/_default/02-quick-start/09-your-own-video.md" heading="Send a keyframe" tab="Node.js" index=1 + // COPY: roomEntityId="my-room-12345", creatorId="user-abc", keyframeUrl="https://your-cdn.com/keyframe-001.jpg" + const response = await client.moderation.check({ + entity_type: 'external:call', + entity_id: roomEntityId, + entity_creator_id: creatorId, + content_published_at: new Date(), + moderation_payload: { + images: [keyframeUrl], + }, + }); + // #endregion snippet + + // With a call rule (keyframe condition) on the app, images are analysed + // asynchronously: `status: "partial"` plus a `task_id`. + expect(['complete', 'partial']).toContain(response.status); + expect(response.recommended_action).toBe('keep'); + }); + + it('sends a caption', async () => { + // #region snippet docs="moderation/_default/02-quick-start/09-your-own-video.md" heading="Send a caption" tab="Node.js" index=1 + // COPY: roomEntityId="my-room-12345", creatorId="user-abc" + const response = await client.moderation.check({ + entity_type: 'external:call', + entity_id: roomEntityId, + entity_creator_id: creatorId, + content_published_at: new Date(), + moderation_payload: { + texts: ['transcribed audio segment'], + }, + }); + // #endregion snippet + + expect(response.status).toBe('complete'); + expect(response.recommended_action).toBe('keep'); + }); +}); diff --git a/__tests__/docs-snippets/pages/moderation/03-moderation-engines/01-ai-llm-text-harm-detection-engine.test.ts b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/01-ai-llm-text-harm-detection-engine.test.ts index 89fdbf76..f5f437c5 100644 --- a/__tests__/docs-snippets/pages/moderation/03-moderation-engines/01-ai-llm-text-harm-detection-engine.test.ts +++ b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/01-ai-llm-text-harm-detection-engine.test.ts @@ -121,4 +121,60 @@ describe(DOCS, () => { expect(response.status).toBe('complete'); expect(['keep', 'flag', 'remove']).toContain(response.recommended_action); }); + + it('configures the LLM engine', async () => { + // The docs' app-wide "chat:messaging" policy, test-owned. + const policyKey = uniqueId('llm-policy'); + cleanup.add(() => client.moderation.deleteConfig({ key: policyKey })); + + // #region snippet docs="moderation/_default/03-moderation-engines/01-ai-llm-text-harm-detection-engine.md" heading="Configuration" tab="Node.js" index=1 + // COPY: policyKey="chat:messaging" + await client.moderation.upsertConfig({ + key: policyKey, + llm_config: { + app_context: + 'A social chat app for adults. Casual tone is fine. Threats and scams are not.', + rules: [ + { + label: 'SCAM', + description: + 'Fraud, phishing, or requests for payment credentials.', + action: 'remove', + }, + { + label: 'HATE_SPEECH', + description: 'Hatred, discrimination, or violence against a group.', + action: 'flag', + }, + ], + severity_descriptions: { + low: 'Rude but not targeting a person.', + high: 'A direct attack or a credible threat.', + }, + }, + }); + // #endregion snippet + + const { config }: GetConfigResponse = await client.moderation.getConfig({ + key: policyKey, + }); + expect(config?.llm_config).toMatchObject({ + enabled: true, + app_context: + 'A social chat app for adults. Casual tone is fine. Threats and scams are not.', + severity_descriptions: { + low: 'Rude but not targeting a person.', + high: 'A direct attack or a credible threat.', + }, + }); + expect( + config?.llm_config?.rules?.map(({ label, action }) => ({ + label, + action, + })), + ).toEqual([ + { label: 'SCAM', action: 'remove' }, + { label: 'HATE_SPEECH', action: 'flag' }, + ]); + }); }); diff --git a/__tests__/docs-snippets/pages/moderation/03-moderation-engines/02-ai-text-harm-detection-engine.test.ts b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/02-ai-text-harm-detection-engine.test.ts new file mode 100644 index 00000000..58d4b4ff --- /dev/null +++ b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/02-ai-text-harm-detection-engine.test.ts @@ -0,0 +1,62 @@ +import { afterAll, describe, expect, it } from 'vitest'; +import { getServerClient } from '../../../helpers/clients'; +import { Cleanup } from '../../../helpers/cleanup'; +import { uniqueId } from '../../../helpers/ids'; + +const DOCS = + 'moderation/_default/03-moderation-engines/02-ai-text-harm-detection-engine.md'; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + // The docs' app-wide "chat:messaging" policy, test-owned. + const policyKey = uniqueId('ai-text-policy'); + + afterAll(() => cleanup.run()); + + it('configures the NLP engine with actions and severity rules', async () => { + cleanup.add(() => client.moderation.deleteConfig({ key: policyKey })); + + // #region snippet docs="moderation/_default/03-moderation-engines/02-ai-text-harm-detection-engine.md" heading="Configuration" tab="Node.js" index=1 + // COPY: policyKey="chat:messaging" + await client.moderation.upsertConfig({ + key: policyKey, + ai_text_config: { + enabled: true, + rules: [ + // One action for every match + { label: 'INSULT', action: 'flag' }, + // Labels without severity can only take one action + { label: 'PLATFORM_BYPASS', action: 'flag' }, + // An action per severity + { + label: 'HATRED', + severity_rules: [ + { severity: 'low', action: 'flag' }, + { severity: 'medium', action: 'flag' }, + { severity: 'high', action: 'remove' }, + { severity: 'critical', action: 'remove' }, + ], + }, + ], + }, + }); + // #endregion snippet + + const { config } = await client.moderation.getConfig({ key: policyKey }); + expect(config?.ai_text_config?.enabled).toBe(true); + expect(config?.ai_text_config?.rules).toMatchObject([ + { label: 'INSULT', action: 'flag' }, + { label: 'PLATFORM_BYPASS', action: 'flag' }, + { + label: 'HATRED', + severity_rules: [ + { severity: 'low', action: 'flag' }, + { severity: 'medium', action: 'flag' }, + { severity: 'high', action: 'remove' }, + { severity: 'critical', action: 'remove' }, + ], + }, + ]); + }); +}); diff --git a/__tests__/docs-snippets/pages/moderation/03-moderation-engines/05-image-moderation.test.ts b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/05-image-moderation.test.ts new file mode 100644 index 00000000..d907c597 --- /dev/null +++ b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/05-image-moderation.test.ts @@ -0,0 +1,117 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { getServerClient } from '../../../helpers/clients'; +import { Cleanup } from '../../../helpers/cleanup'; +import { uniqueId } from '../../../helpers/ids'; + +const DOCS = 'moderation/_default/03-moderation-engines/05-image-moderation.md'; + +// A small image that Stream's servers can download. +const REACHABLE_IMAGE = + 'https://theme.zdassets.com/theme_assets/9442057/efc3820e436f9150bc8cf34267fff4df052a1f9c.png'; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + // The docs' app-wide "chat:messaging" and "marketplace" policies, test-owned. + const policyKey = uniqueId('image-policy'); + const marketplaceKey = uniqueId('marketplace'); + const creatorId = uniqueId('user-42'); + + beforeAll(async () => { + cleanup.users.push(creatorId); + await client.upsertUsers([{ id: creatorId }]); + cleanup.add(() => client.moderation.deleteConfig({ key: policyKey })); + cleanup.add(() => client.moderation.deleteConfig({ key: marketplaceKey })); + // The "marketplace" policy the "Send images" check uses: async image moderation. + await client.moderation.upsertConfig({ + key: marketplaceKey, + ai_image_config: { + rules: [{ label: 'Explicit', action: 'remove', min_confidence: 70 }], + }, + }); + }); + + afterAll(() => cleanup.run()); + + it('configures image moderation', async () => { + // #region snippet docs="moderation/_default/03-moderation-engines/05-image-moderation.md" heading="Configuration" tab="Node.js" index=1 + // COPY: policyKey="chat:messaging" + await client.moderation.upsertConfig({ + key: policyKey, + ai_image_config: { + rules: [ + { label: 'Explicit', action: 'remove', min_confidence: 70 }, + { label: 'Violence', action: 'flag', min_confidence: 50 }, + ], + ocr_rules: [{ label: 'PII', action: 'flag' }], + }, + }); + // #endregion snippet + + const { config } = await client.moderation.getConfig({ key: policyKey }); + expect(config?.ai_image_config).toMatchObject({ + enabled: true, + rules: [ + { label: 'Explicit', action: 'remove', min_confidence: 70 }, + { label: 'Violence', action: 'flag', min_confidence: 50 }, + ], + ocr_rules: [{ label: 'PII', action: 'flag' }], + }); + }); + + it('sends images to the Check API', async () => { + const listingId = uniqueId('listing-123'); + + // #region snippet docs="moderation/_default/03-moderation-engines/05-image-moderation.md" heading="Send images" tab="Node.js" index=1 + // COPY: listingId="listing-123", creatorId="user-42", marketplaceKey="marketplace" + const response = await client.moderation.check({ + entity_type: 'listing', + entity_id: listingId, + entity_creator_id: creatorId, + config_key: marketplaceKey, + moderation_payload: { + images: ['https://cdn.example.com/listings/123/photo-1.jpg'], + }, + }); + // #endregion snippet + + // Async image moderation: the check returns before the analysis, with a task id. + // The docs say `status: "pending"`; the test app answers `"partial"`. + expect(['pending', 'partial']).toContain(response.status); + expect(typeof response.task_id).toBe('string'); + expect(response.recommended_action).toBe('keep'); + }); + + it('turns on synchronous image moderation', async () => { + // #region snippet docs="moderation/_default/03-moderation-engines/05-image-moderation.md" heading="Synchronous moderation" tab="Node.js" index=1 + // COPY: marketplaceKey="marketplace" + await client.moderation.upsertConfig({ + key: marketplaceKey, + ai_image_config: { + async: false, + rules: [{ label: 'Explicit', action: 'remove', min_confidence: 50 }], + }, + }); + // #endregion snippet + + const { config } = await client.moderation.getConfig({ + key: marketplaceKey, + }); + expect(config?.ai_image_config).toMatchObject({ + async: false, + rules: [{ label: 'Explicit', action: 'remove', min_confidence: 50 }], + }); + + // Check now returns the image verdict in its response: no task id. + const check = await client.moderation.check({ + entity_type: 'listing', + entity_id: uniqueId('listing'), + entity_creator_id: creatorId, + config_key: marketplaceKey, + moderation_payload: { images: [REACHABLE_IMAGE] }, + }); + expect(check.status).toBe('complete'); + expect(check.task_id).toBeUndefined(); + expect(check.recommended_action).toBe('keep'); + }); +}); diff --git a/__tests__/docs-snippets/pages/moderation/03-moderation-engines/06-ai-video-file-moderation.test.ts b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/06-ai-video-file-moderation.test.ts new file mode 100644 index 00000000..75fc56ee --- /dev/null +++ b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/06-ai-video-file-moderation.test.ts @@ -0,0 +1,82 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { getServerClient } from '../../../helpers/clients'; +import { Cleanup } from '../../../helpers/cleanup'; +import { uniqueId } from '../../../helpers/ids'; + +const DOCS = + 'moderation/_default/03-moderation-engines/06-ai-video-file-moderation.md'; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + // The docs' app-wide "chat:messaging" and "marketplace" policies, test-owned. + const policyKey = uniqueId('video-policy'); + const marketplaceKey = uniqueId('marketplace'); + const creatorId = uniqueId('user-42'); + + beforeAll(async () => { + cleanup.users.push(creatorId); + await client.upsertUsers([{ id: creatorId }]); + cleanup.add(() => client.moderation.deleteConfig({ key: policyKey })); + cleanup.add(() => client.moderation.deleteConfig({ key: marketplaceKey })); + // The "marketplace" policy the check uses, with video moderation on. + await client.moderation.upsertConfig({ + key: marketplaceKey, + ai_video_config: { + rules: [{ label: 'Explicit', action: 'remove', min_confidence: 70 }], + }, + }); + }); + + afterAll(() => cleanup.run()); + + it('configures video moderation', async () => { + // #region snippet docs="moderation/_default/03-moderation-engines/06-ai-video-file-moderation.md" heading="Configuration" tab="Node.js" index=1 + // COPY: policyKey="chat:messaging" + await client.moderation.upsertConfig({ + key: policyKey, + ai_video_config: { + rules: [ + { label: 'Explicit', action: 'remove', min_confidence: 70 }, + { label: 'Violence', action: 'flag', min_confidence: 50 }, + ], + }, + }); + // #endregion snippet + + const { config } = await client.moderation.getConfig({ key: policyKey }); + expect(config?.ai_video_config).toMatchObject({ + enabled: true, + rules: [ + { label: 'Explicit', action: 'remove', min_confidence: 70 }, + { label: 'Violence', action: 'flag', min_confidence: 50 }, + ], + }); + // Video and image are set separately. + expect(config?.ai_image_config).toBeNull(); + }); + + it('sends videos to the Check API', async () => { + const listingId = uniqueId('listing-123'); + + // #region snippet docs="moderation/_default/03-moderation-engines/06-ai-video-file-moderation.md" heading="Configuration" tab="Node.js" index=2 + // COPY: listingId="listing-123", creatorId="user-42", marketplaceKey="marketplace" + const response = await client.moderation.check({ + entity_type: 'listing', + entity_id: listingId, + entity_creator_id: creatorId, + config_key: marketplaceKey, + moderation_payload: { + videos: ['https://cdn.example.com/listings/123/tour.mp4'], + }, + }); + // response.status is "pending" until the video is analyzed + // #endregion snippet + + // Video analysis is always async: the check returns at once with a task id. + // The docs say `"pending"`; the test app answers `"partial"`. + expect(['pending', 'partial']).toContain(response.status); + expect(typeof response.task_id).toBe('string'); + expect(response.recommended_action).toBe('keep'); + }); +}); diff --git a/__tests__/docs-snippets/pages/moderation/03-moderation-engines/07-ai-audio-file-moderation.test.ts b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/07-ai-audio-file-moderation.test.ts new file mode 100644 index 00000000..6145ea69 --- /dev/null +++ b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/07-ai-audio-file-moderation.test.ts @@ -0,0 +1,70 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { getServerClient } from '../../../helpers/clients'; +import { Cleanup } from '../../../helpers/cleanup'; +import { uniqueId } from '../../../helpers/ids'; + +const DOCS = + 'moderation/_default/03-moderation-engines/07-ai-audio-file-moderation.md'; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + // The docs' app-wide "chat:messaging" policy, test-owned (both fences use it). + const policyKey = uniqueId('audio-policy'); + const creatorId = uniqueId('user-42'); + + beforeAll(async () => { + cleanup.users.push(creatorId); + await client.upsertUsers([{ id: creatorId }]); + cleanup.add(() => client.moderation.deleteConfig({ key: policyKey })); + }); + + afterAll(() => cleanup.run()); + + it('configures audio moderation', async () => { + // #region snippet docs="moderation/_default/03-moderation-engines/07-ai-audio-file-moderation.md" heading="Configuration" tab="Node.js" index=1 + // COPY: policyKey="chat:messaging" + await client.moderation.upsertConfig({ + key: policyKey, + ai_audio_config: { + ai_text_config: { + rules: [{ label: 'HATRED', action: 'flag' }], + }, + }, + }); + // #endregion snippet + + const { config } = await client.moderation.getConfig({ key: policyKey }); + // A rule with an action turns audio moderation on. + expect(config?.ai_audio_config?.enabled).toBe(true); + expect(config?.ai_audio_config?.ai_text_config?.rules).toMatchObject([ + { label: 'HATRED', action: 'flag' }, + ]); + // Text policy rules don't apply to transcripts: the policy has no text config. + expect(config?.ai_text_config).toBeNull(); + }); + + it('sends audio to the Check API', async () => { + const voiceNoteId = uniqueId('voice-note-123'); + + // #region snippet docs="moderation/_default/03-moderation-engines/07-ai-audio-file-moderation.md" heading="Send audio" tab="Node.js" index=1 + // COPY: voiceNoteId="voice-note-123", creatorId="user-42", policyKey="chat:messaging" + const response = await client.moderation.check({ + entity_type: 'voice_note', + entity_id: voiceNoteId, + entity_creator_id: creatorId, + config_key: policyKey, + moderation_payload: { + audios: ['https://cdn.example.com/voice/123.m4a'], + }, + }); + // response.status is "pending" until transcription and classification finish + // #endregion snippet + + // Audio is always async: the check returns at once with a task id. + // The docs say `"pending"`; the test app answers `"partial"`. + expect(['pending', 'partial']).toContain(response.status); + expect(typeof response.task_id).toBe('string'); + expect(response.recommended_action).toBe('keep'); + }); +}); diff --git a/__tests__/docs-snippets/pages/moderation/03-moderation-engines/08-usernames-and-images.test.ts b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/08-usernames-and-images.test.ts new file mode 100644 index 00000000..285377eb --- /dev/null +++ b/__tests__/docs-snippets/pages/moderation/03-moderation-engines/08-usernames-and-images.test.ts @@ -0,0 +1,100 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { getServerClient } from '../../../helpers/clients'; +import { Cleanup } from '../../../helpers/cleanup'; +import { uniqueId } from '../../../helpers/ids'; + +const DOCS = + 'moderation/_default/03-moderation-engines/08-usernames-and-images.md'; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + // The docs' app-wide "username:default" and "user_profile:default" policies, + // test-owned. `labels` fails with code 4 "configuration not found" without the policy. + const usernamePolicy = `username:${uniqueId('usernames')}`; + const profilePolicy = `user_profile:${uniqueId('profiles')}`; + const userId = uniqueId('user'); + + beforeAll(async () => { + cleanup.users.push(userId); + await client.upsertUsers([{ id: userId }]); + cleanup.add(() => client.moderation.deleteConfig({ key: usernamePolicy })); + cleanup.add(() => client.moderation.deleteConfig({ key: profilePolicy })); + await client.moderation.upsertConfig({ + key: usernamePolicy, + ai_text_config: { rules: [{ label: 'INSULT', action: 'flag' }] }, + }); + await client.moderation.upsertConfig({ + key: profilePolicy, + ai_text_config: { rules: [{ label: 'INSULT', action: 'flag' }] }, + ai_image_config: { + rules: [{ label: 'Explicit', action: 'remove', min_confidence: 50 }], + }, + }); + }); + + afterAll(() => cleanup.run()); + + it('checks a username with the labels fast path', async () => { + // #region snippet docs="moderation/_default/03-moderation-engines/08-usernames-and-images.md" heading="Check Username" tab="Node.js" index=1 + // COPY: usernamePolicy="username:default" + const response = await client.moderation.labels({ + content: 'username_to_check', + content_type: 'username', + policy: usernamePolicy, + }); + + if (response.recommended_action === 'keep') { + // Username is acceptable + } + // #endregion snippet + + expect(response.recommended_action).toBe('keep'); + }); + + it('checks a username against a user profile policy', async () => { + // #region snippet docs="moderation/_default/03-moderation-engines/08-usernames-and-images.md" heading="Check Username" tab="Node.js" index=2 + // COPY: profilePolicy="user_profile:default" + const response = await client.moderation.check({ + entity_type: 'stream:v1:user_profile', + entity_id: userId, + entity_creator_id: userId, + moderation_payload: { texts: ['username_to_check'] }, + config_key: profilePolicy, + }); + + // Response will indicate if the username is acceptable + if (response.recommended_action === 'keep') { + // Username is acceptable, proceed with user creation/update + } else { + // Username violates moderation rules, ask user to choose a different name + } + // #endregion snippet + + expect(response.status).toBe('complete'); + expect(response.recommended_action).toBe('keep'); + }); + + it('checks a profile image', async () => { + // #region snippet docs="moderation/_default/03-moderation-engines/08-usernames-and-images.md" heading="Check Profile Images" tab="Node.js" index=1 + // COPY: profilePolicy="user_profile:default" + const response = await client.moderation.check({ + entity_type: 'stream:v1:user_profile', + entity_id: userId, + entity_creator_id: userId, + moderation_payload: { images: ['https://example.com/profile.jpg'] }, + config_key: profilePolicy, + }); + + // Handle the moderation result + if (response.recommended_action === 'keep') { + // Image is safe to use as profile picture + } else { + // Image violates moderation rules + } + // #endregion snippet + + expect(['complete', 'partial']).toContain(response.status); + expect(response.recommended_action).toBe('keep'); + }); +}); diff --git a/__tests__/docs-snippets/pages/moderation/04-configuration/01-policies.test.ts b/__tests__/docs-snippets/pages/moderation/04-configuration/01-policies.test.ts index 72e70de7..0bb79df0 100644 --- a/__tests__/docs-snippets/pages/moderation/04-configuration/01-policies.test.ts +++ b/__tests__/docs-snippets/pages/moderation/04-configuration/01-policies.test.ts @@ -150,7 +150,7 @@ describe(DOCS, () => { expect(configs.length).toBeGreaterThan(0); expect(configs.length).toBeLessThanOrEqual(10); // Newest first: the config this file just created is among them. - const createdAt = configs.map(({ created_at }) => created_at.getTime()); + const createdAt = configs.map(({ created_at }) => created_at); expect(createdAt).toEqual([...createdAt].sort((a, b) => b - a)); expect(configs.map(({ key }) => key)).toContain(configKey); }); diff --git a/__tests__/docs-snippets/pages/moderation/04-configuration/05-evaluations.test.ts b/__tests__/docs-snippets/pages/moderation/04-configuration/05-evaluations.test.ts index 0d0753c0..a9849565 100644 --- a/__tests__/docs-snippets/pages/moderation/04-configuration/05-evaluations.test.ts +++ b/__tests__/docs-snippets/pages/moderation/04-configuration/05-evaluations.test.ts @@ -151,6 +151,7 @@ describe(DOCS, () => { // The generated `listPolicyTestSets()` takes no arguments (TS2554), so // `limit` isn't sent: the API returns its default page size. it('lists evaluation sets', async () => { + // @ts-expect-error SDK issue: generated listPolicyTestSets() takes no arguments, so limit is not sent // #region snippet docs="moderation/_default/04-configuration/05-evaluations.md" heading="List evaluation sets" tab="Node.js" index=1 const response = await client.moderation.listPolicyTestSets({ limit: 50 }); // #endregion snippet diff --git a/__tests__/docs-snippets/pages/moderation/05-content-moderation/02-flag-mute-ban.test.ts b/__tests__/docs-snippets/pages/moderation/05-content-moderation/02-flag-mute-ban.test.ts index a7c009ff..047ec03c 100644 --- a/__tests__/docs-snippets/pages/moderation/05-content-moderation/02-flag-mute-ban.test.ts +++ b/__tests__/docs-snippets/pages/moderation/05-content-moderation/02-flag-mute-ban.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import type { StreamClient, UserResponse } from '../../../../../index'; +import { nsToMs } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; @@ -144,9 +144,9 @@ describe(DOCS, () => { const { mutes } = await getServerUser(client, mutingUserId); expect(mutes.map((m) => m.target?.id)).toEqual([mutedUser]); // 60 minutes - expect( - (mutes[0].expires?.getTime() ?? 0) - mutes[0].created_at.getTime(), - ).toBe(60 * 60 * 1000); + expect(nsToMs(mutes[0].expires ?? 0) - nsToMs(mutes[0].created_at)).toBe( + 60 * 60 * 1000, + ); }); it('unmutes a user', async () => { @@ -194,9 +194,9 @@ describe(DOCS, () => { expect(bans[0].banned_by?.id).toBe(moderatorId); expect(bans[0].channel).toBeUndefined(); // 1440 minutes - expect( - (bans[0].expires?.getTime() ?? 0) - bans[0].created_at.getTime(), - ).toBe(1440 * 60 * 1000); + expect(nsToMs(bans[0].expires ?? 0) - nsToMs(bans[0].created_at)).toBe( + 1440 * 60 * 1000, + ); }); it('bans a user from one channel', async () => { @@ -314,7 +314,9 @@ describe(DOCS, () => { await retry( async () => { const user = await getServerUser(client, expiredBanned); - if ((user.ban_expires?.getTime() ?? Infinity) > Date.now()) { + if ( + (user.ban_expires ? nsToMs(user.ban_expires) : Infinity) > Date.now() + ) { throw new Error('ban not expired yet'); } }, @@ -398,26 +400,4 @@ describe(DOCS, () => { true, ); }); - - it.skip('BLOCKED: SDK issue: the node SDK has no getUserModerationReport (GET /api/v2/moderation/user_report is left out of the spec)', async () => { - const reportUser = await createUser('report-user'); - // The node SDK has no getUserModerationReport: type the missing method (from the - // API's response) so the skipped region typechecks and lints. It isn't there at runtime. - const client = getServerClient() as StreamClient & { - moderation: { - getUserModerationReport: (request: { - user_id: string; - }) => Promise<{ user: UserResponse }>; - }; - }; - - // #region snippet docs="moderation/_default/05-content-moderation/02-flag-mute-ban.md" heading="User Moderation Report" tab="Node.js" index=1 - // COPY: reportUser="user_123" - const response = await client.moderation.getUserModerationReport({ - user_id: reportUser, - }); - // #endregion snippet - - expect(response.user.id).toBe(reportUser); - }); }); diff --git a/__tests__/docs-snippets/pages/moderation/06-review-queue/01-review-queue.test.ts b/__tests__/docs-snippets/pages/moderation/06-review-queue/01-review-queue.test.ts index 9417fa5a..c0e0facb 100644 --- a/__tests__/docs-snippets/pages/moderation/06-review-queue/01-review-queue.test.ts +++ b/__tests__/docs-snippets/pages/moderation/06-review-queue/01-review-queue.test.ts @@ -1,8 +1,18 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from 'vitest'; +import type { SubmitModerationFeedbackResponse } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; import { sendServerMessage, waitForReviewItem } from '../../../helpers/server'; +import { retry } from '../../../helpers/wait'; const DOCS = 'moderation/_default/06-review-queue/01-review-queue.md'; @@ -42,8 +52,94 @@ describe(DOCS, () => { cleanup.channels.push(cid); }); + afterEach(() => vi.restoreAllMocks()); afterAll(() => cleanup.run()); + it('queries flagged items that are not reviewed yet', async () => { + // Manual flags have `recommended_action: "keep"`: flag messages through a + // channel-level blocklist policy (action `flag`) of a new channel instead. + const policyChannelId = uniqueId('policy-channel'); + const policyCid = `messaging:${policyChannelId}`; + const configKey = `chat:${policyCid}`; + const blocklist = uniqueId('review-words'); + await client.chat + .channel('messaging', policyChannelId) + .getOrCreate({ data: { created_by_id: moderatorId } }); + cleanup.channels.push(policyCid); + await client.createBlockList({ name: blocklist, words: ['frobnicate'] }); + cleanup.add(() => client.deleteBlockList({ name: blocklist })); + // A new blocklist isn't known to the moderation API right away. + await retry( + () => + client.moderation.upsertConfig({ + key: configKey, + block_list_config: { rules: [{ name: blocklist, action: 'flag' }] }, + }), + { retryIf: (error) => String(error).includes('Blocklist not found') }, + ); + cleanup.add(() => client.moderation.deleteConfig({ key: configKey })); + // A new policy can take a moment to apply. + await retry(async () => { + const { recommended_action } = await client.moderation.check({ + entity_type: 'stream:chat:v1:message', + entity_id: uniqueId('warmup'), + entity_creator_id: creatorId, + config_key: configKey, + moderation_payload: { texts: ['frobnicate'] }, + }); + expect(recommended_action).toBe('flag'); + }); + /** Sends a message the policy flags, and returns its review queue item. */ + // The channel's policy can apply to messages a while after `check` sees it: + // send again until a message reaches the review queue. + const policyFlaggedMessage = () => + retry(async () => { + const message = await sendServerMessage( + client, + policyCid, + { text: 'please frobnicate now', user_id: creatorId }, + { force_moderation: true }, + ); + return retry( + async () => { + const { items } = await client.moderation.queryReviewQueue({ + filter: { entity_id: message.id }, + }); + expect(items).toHaveLength(1); + return items[0]; + }, + { timeout: 5000 }, + ); + }); + // The filter matches every unreviewed flagged item of the app: flag a fresh + // message just before, so it is the newest one. A newer item marked as reviewed + // must not show up. + const flagged = await policyFlaggedMessage(); + const reviewed = await policyFlaggedMessage(); + await client.moderation.submitAction({ + action_type: 'mark_reviewed', + item_id: reviewed.id, + user_id: moderatorId, + }); + + // #region snippet docs="moderation/_default/06-review-queue/01-review-queue.md" heading="Query Review Queue" tab="Node.js" index=1 + const response = await client.moderation.queryReviewQueue({ + filter: { recommended_action: 'flag', reviewed: false }, + sort: [{ field: 'created_at', direction: -1 }], + }); + // #endregion snippet + + expect(response.items[0].id).toBe(flagged.id); + expect(response.items[0].recommended_action).toBe('flag'); + // The bare `recommended_action: "flag"` equality doesn't narrow the results on + // the test app (items with "keep" come back too; `{ $in: ["flag"] }` does narrow + // them, 2026-10-29), so only the `reviewed` part is asserted for every item. + for (const item of response.items) { + expect(item.reviewed_at).toBeFalsy(); + } + expect(response.items.map((item) => item.id)).not.toContain(reviewed.id); + }); + it('queries the review queue', async () => { // The filter matches every unreviewed message item of the app: flag a fresh // message just before, so it is the newest one. A newer item marked as reviewed @@ -56,7 +152,7 @@ describe(DOCS, () => { user_id: moderatorId, }); - // #region snippet docs="moderation/_default/06-review-queue/01-review-queue.md" heading="Query Review Queue" tab="Node.js" index=1 + // #region snippet docs="moderation/_default/06-review-queue/01-review-queue.md" heading="Query Review Queue" tab="Node.js" index=2 const response = await client.moderation.queryReviewQueue({ filter: { entity_type: 'stream:chat:v1:message', @@ -97,4 +193,30 @@ describe(DOCS, () => { user_id: moderatorId, }); }); + + it('submits moderator feedback', async () => { + const submitFeedback = vi.spyOn( + client.moderation, + 'submitModerationFeedback', + ); + + // #region snippet docs="moderation/_default/06-review-queue/01-review-queue.md" heading="Moderator feedback" tab="Node.js" index=1 + await client.moderation.submitModerationFeedback({ + message: 'the moderated text', + published_at: '2026-10-20T13:13:38Z', + reference: 'provider-reference', + current_labels: ['HATRED'], + current_recommended_action: 'remove', + expected_labels: ['INSULT'], + expected_recommended_action: 'flag', + }); + // #endregion snippet + + const [response] = await Promise.all( + submitFeedback.mock.results.map( + (r) => r.value as Promise, + ), + ); + expect(response.duration).toBeTruthy(); + }); }); diff --git a/__tests__/docs-snippets/pages/moderation/06-review-queue/02-actions.test.ts b/__tests__/docs-snippets/pages/moderation/06-review-queue/02-actions.test.ts index cfc1de72..ecfbf0f5 100644 --- a/__tests__/docs-snippets/pages/moderation/06-review-queue/02-actions.test.ts +++ b/__tests__/docs-snippets/pages/moderation/06-review-queue/02-actions.test.ts @@ -1,9 +1,19 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import type { StreamClient } from '../../../../../index'; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from 'vitest'; +import { nsToMs } from '../../../../../index'; +import type { UpsertActionConfigResponse } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; import { sendServerMessage, waitForReviewItem } from '../../../helpers/server'; +import { retry } from '../../../helpers/wait'; const DOCS = 'moderation/_default/06-review-queue/02-actions.md'; @@ -13,6 +23,7 @@ describe(DOCS, () => { const moderatorId = uniqueId('moderator'); const channelId = uniqueId('actions-channel'); const cid = `messaging:${channelId}`; + const upsertConfig = client.moderation.upsertConfig.bind(client.moderation); /** * Flags a fresh message of a new member (so bans of one test don't affect the @@ -70,6 +81,7 @@ describe(DOCS, () => { cleanup.channels.push(cid); }); + afterEach(() => vi.restoreAllMocks()); afterAll(() => cleanup.run()); it('marks an item as reviewed', async () => { @@ -134,10 +146,9 @@ describe(DOCS, () => { expect(bans[0].channel?.cid).toBe(cid); expect(bans[0].reason).toBe('Repeated harassment'); expect(bans[0].expires).toBeDefined(); - expect( - new Date(bans[0].expires ?? 0).getTime() - - new Date(bans[0].created_at).getTime(), - ).toBe(1440 * 60 * 1000); + expect(nsToMs(bans[0].expires ?? 0) - nsToMs(bans[0].created_at)).toBe( + 1440 * 60 * 1000, + ); const { users } = await client.queryUsers({ payload: { filter_conditions: { id: creatorId } }, }); @@ -230,55 +241,120 @@ describe(DOCS, () => { ); }); - it.skip("BLOCKED: SDK issue: the node SDK has no bulkSubmitAction (POST /api/v2/moderation/bulk_submit_action is `Ignore: true` in the spec), and the API rejects the `actions` list: code 4 V2BulkSubmitAction failed with error: \"action is a required field, filters 'BulkSubmitActionRequest.filters' Error:Field validation for 'filters' failed on the 'required_without' tag, review_queue_item_ids 'BulkSubmitActionRequest.review_queue_item_ids' Error:Field validation for 'review_queue_item_ids' failed on the 'required_without' tag\"", async () => { - const [first, second, third] = await Promise.all([ - flagMessage(), - flagMessage(), - flagMessage(), - ]); - const itemId1 = first.itemId; - const itemId2 = second.itemId; - const itemId3 = third.itemId; - // The node SDK has no bulkSubmitAction: type the missing method (with the request - // shape the docs use) so the skipped region typechecks and lints. It isn't there - // at runtime. - const client = getServerClient() as StreamClient & { - moderation: { - bulkSubmitAction: (request: { - actions: Array<{ - action_type: string; - item_id: string; - user_id: string; - }>; - }) => Promise; - }; - }; - - // #region snippet docs="moderation/_default/06-review-queue/02-actions.md" heading="Bulk Submit Action" tab="Node.js" index=1 - // COPY: itemId1="item_id_1", itemId2="item_id_2", itemId3="item_id_3" - await client.moderation.bulkSubmitAction({ - actions: [ - { - action_type: 'mark_reviewed', - item_id: itemId1, - user_id: moderatorId, - }, - { - action_type: 'delete_message', - item_id: itemId2, - user_id: moderatorId, - }, - { - action_type: 'mark_reviewed', - item_id: itemId3, - user_id: moderatorId, - }, - ], + it('customizes a review queue button', async () => { + const entityType = 'stream:chat:v1:message'; + /** The app's own (non-default) buttons for chat messages. */ + const listButtons = async () => + ( + await client.moderation.getActionConfig({ + entity_type: entityType, + exclude_defaults: true, + }) + ).action_config[entityType] ?? []; + const original = await listButtons(); + const upsertActionConfig = vi.spyOn( + client.moderation, + 'upsertActionConfig', + ); + + // #region snippet docs="moderation/_default/06-review-queue/02-actions.md" heading="Customize review queue buttons" tab="Node.js" index=1 + await client.moderation.upsertActionConfig({ + entity_type: 'stream:chat:v1:message', + order: 1, + action: 'delete_message', + description: 'Delete message', }); // #endregion snippet - expect((await getItem(itemId1))?.latest_moderator_action).toBe( - 'mark_reviewed', + const [{ action_config: created }] = await Promise.all( + upsertActionConfig.mock.results.map( + (r) => r.value as Promise, + ), + ); + const createdId = created?.id; + if (!createdId) throw new Error('upsertActionConfig returned no id'); + // Without an `id` the request creates a new button (the app's own buttons stay). + expect(original.map(({ id }) => id)).not.toContain(createdId); + cleanup.add(async () => { + await client.moderation.deleteActionConfig({ id: createdId }); + expect((await listButtons()).map(({ id }) => id)).toEqual( + original.map(({ id }) => id), + ); + }); + expect(created).toMatchObject({ + entity_type: 'stream:chat:v1:message', + order: 1, + action: 'delete_message', + description: 'Delete message', + }); + expect( + (await listButtons()).find(({ id }) => id === createdId), + ).toMatchObject({ + action: 'delete_message', + description: 'Delete message', + }); + }); + + it('masks blocklist matches with mask_flag', async () => { + // The docs' app-wide "chat" policy, test-owned: a channel-level policy of a new + // channel, so only that channel is moderated by it. + const maskChannelId = uniqueId('mask-channel'); + const maskCid = `messaging:${maskChannelId}`; + const chatConfigKey = `chat:${maskCid}`; + const blocklistName = uniqueId('phone-numbers'); + await client.chat + .channel('messaging', maskChannelId) + .getOrCreate({ data: { created_by_id: moderatorId } }); + cleanup.channels.push(maskCid); + await client.createBlockList({ + name: blocklistName, + words: ['frobnicate'], + }); + cleanup.add(() => client.deleteBlockList({ name: blocklistName })); + cleanup.add(() => client.moderation.deleteConfig({ key: chatConfigKey })); + // A new blocklist isn't known to the moderation API right away. + vi.spyOn(client.moderation, 'upsertConfig').mockImplementation((request) => + retry(() => upsertConfig(request), { + retryIf: (error) => String(error).includes('Blocklist not found'), + }), ); + + // #region snippet docs="moderation/_default/06-review-queue/02-actions.md" heading="Mask" tab="Node.js" index=1 + // COPY: chatConfigKey="chat" + await client.moderation.upsertConfig({ + key: chatConfigKey, + block_list_config: { + enabled: true, + rules: [{ name: blocklistName, action: 'mask_flag', team: '' }], + }, + }); + // #endregion snippet + + const { config } = await client.moderation.getConfig({ + key: chatConfigKey, + }); + expect(config?.block_list_config?.rules).toMatchObject([ + { name: blocklistName, action: 'mask_flag' }, + ]); + // A matching message is published with the word masked, and flagged. + const senderId = uniqueId('sender'); + cleanup.users.push(senderId); + await client.upsertUsers([{ id: senderId }]); + const message = await retry(async () => { + const sent = await sendServerMessage( + client, + maskCid, + { text: 'please frobnicate now', user_id: senderId }, + { force_moderation: true }, + ); + // The matched word is replaced (on the test app with four U+26B9 characters, + // whatever the word's length, 2026-10-29). + expect(sent.text).not.toContain('frobnicate'); + expect(sent.text).toMatch(/^please \S+ now$/); + return sent; + }); + expect(message.type).toBe('regular'); + const item = await waitForReviewItem(client, message.id); + expect(item.entity_id).toBe(message.id); }); }); diff --git a/__tests__/docs-snippets/pages/moderation/06-review-queue/03-appeals.test.ts b/__tests__/docs-snippets/pages/moderation/06-review-queue/03-appeals.test.ts index 9ca49631..780fedd9 100644 --- a/__tests__/docs-snippets/pages/moderation/06-review-queue/03-appeals.test.ts +++ b/__tests__/docs-snippets/pages/moderation/06-review-queue/03-appeals.test.ts @@ -388,4 +388,42 @@ describe(DOCS, () => { expect(submitted.items.map((item) => item.id)).not.toContain(withoutAppeal); expect(byAppeal.items.map((item) => item.id)).toEqual([itemId]); }); + + it('restores content for several appeals at once', async () => { + const first = await deletedMessage(); + const second = await deletedMessage(); + const appealIds = await Promise.all( + [first, second].map(async ({ creatorId, messageId }) => { + const { appeal_id } = await client.moderation.appeal({ + appeal_reason: 'Not spam', + entity_id: messageId, + entity_type: 'stream:chat:v1:message', + user_id: creatorId, + }); + return appeal_id; + }), + ); + const [appealId1, appealId2] = appealIds; + + // #region snippet docs="moderation/_default/06-review-queue/03-appeals.md" heading="Bulk action" tab="Node.js" index=1 + // COPY: appealId1="appeal_1", appealId2="appeal_2" + await client.moderation.bulkActionAppeals({ + action_type: 'restore', + appeal_ids: [appealId1, appealId2], + restore: { decision_reason: 'Restored after review' }, + user_id: moderatorId, + }); + // #endregion snippet + + for (const appealId of appealIds) { + expect(await getAppeal(appealId)).toMatchObject({ + status: 'accepted', + decision_reason: 'Restored after review', + }); + } + for (const { messageId } of [first, second]) { + const { message } = await client.chat.getMessage({ id: messageId }); + expect(message.deleted_at).toBeUndefined(); + } + }); }); diff --git a/__tests__/docs-snippets/pages/moderation/11-guides/09-rule-triggered-webhooks.test.ts b/__tests__/docs-snippets/pages/moderation/08-webhooks/01-webhooks.test.ts similarity index 91% rename from __tests__/docs-snippets/pages/moderation/11-guides/09-rule-triggered-webhooks.test.ts rename to __tests__/docs-snippets/pages/moderation/08-webhooks/01-webhooks.test.ts index 3f233628..66c63bbc 100644 --- a/__tests__/docs-snippets/pages/moderation/11-guides/09-rule-triggered-webhooks.test.ts +++ b/__tests__/docs-snippets/pages/moderation/08-webhooks/01-webhooks.test.ts @@ -4,7 +4,7 @@ import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; -const DOCS = 'moderation/_default/11-guides/09-rule-triggered-webhooks.md'; +const DOCS = 'moderation/_default/08-webhooks/01-webhooks.md'; describe(DOCS, () => { const client = getServerClient(); @@ -22,7 +22,7 @@ describe(DOCS, () => { const ruleName = uniqueId('high-profile-harms'); const upsertRule = vi.spyOn(client.moderation, 'upsertModerationRule'); - // #region snippet docs="moderation/_default/11-guides/09-rule-triggered-webhooks.md" heading="Content rule: notify on a harm label" tab="Node.js" index=1 + // #region snippet docs="moderation/_default/08-webhooks/01-webhooks.md" heading="Content rule: notify on a harm label" tab="Node.js" index=1 // COPY: configKey="chat:messaging", livestreamConfigKey="chat:livestream", ruleName="High-profile harms" await client.moderation.upsertModerationRule({ name: ruleName, diff --git a/__tests__/docs-snippets/pages/moderation/09-logs-and-analytics/01-logs-and-analytics.test.ts b/__tests__/docs-snippets/pages/moderation/09-logs-and-analytics/01-logs-and-analytics.test.ts index b43a97e9..35a12026 100644 --- a/__tests__/docs-snippets/pages/moderation/09-logs-and-analytics/01-logs-and-analytics.test.ts +++ b/__tests__/docs-snippets/pages/moderation/09-logs-and-analytics/01-logs-and-analytics.test.ts @@ -1,5 +1,4 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import type { StreamClient, UserResponse } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; @@ -82,95 +81,6 @@ describe(DOCS, () => { ); }); - it.skip('BLOCKED: SDK issue: the node SDK has no exportModerationLogs (POST /api/v2/moderation/logs/export is `Ignore: true` in the spec)', async () => { - // The node SDK has no exportModerationLogs: type the missing method (with the - // request shape the docs use) so the skipped region typechecks and lints. It isn't - // there at runtime. - const client = getServerClient() as StreamClient & { - moderation: { - exportModerationLogs: (request: { - filter: Record; - }) => Promise<{ task_id: string }>; - }; - }; - - // #region snippet docs="moderation/_default/09-logs-and-analytics/01-logs-and-analytics.md" heading="Export Moderation Logs" tab="Node.js" index=1 - const response = await client.moderation.exportModerationLogs({ - filter: { created_at: { $gte: '2024-01-01T00:00:00Z' } }, - }); - // #endregion snippet - - expect(response.task_id).toBeTruthy(); - }); - - it.skip('BLOCKED: SDK issue: the node SDK has no getModerationAnalytics (POST /api/v2/moderation/analytics is `Ignore: true` in the spec)', async () => { - // The node SDK has no getModerationAnalytics: type the missing method (with the - // request shape the docs use) so the skipped region typechecks and lints. It isn't - // there at runtime. - const client = getServerClient() as StreamClient & { - moderation: { - getModerationAnalytics: (request: { - from: string; - to: string; - }) => Promise<{ analytics: Record }>; - }; - }; - - // #region snippet docs="moderation/_default/09-logs-and-analytics/01-logs-and-analytics.md" heading="Get Moderation Analytics" tab="Node.js" index=1 - const response = await client.moderation.getModerationAnalytics({ - from: '2024-01-01T00:00:00Z', - to: '2024-01-31T23:59:59Z', - }); - // #endregion snippet - - expect(response.analytics).toBeDefined(); - }); - - it.skip('BLOCKED: SDK issue: the node SDK has no queryUsageStats (POST /api/v2/moderation/usage_stats is `Ignore: true` in the spec)', async () => { - // The node SDK has no queryUsageStats: type the missing method (with the request - // shape the docs use) so the skipped region typechecks and lints. It isn't there - // at runtime. - const client = getServerClient() as StreamClient & { - moderation: { - queryUsageStats: (request: { - from: string; - to: string; - }) => Promise<{ items: unknown[] }>; - }; - }; - - // #region snippet docs="moderation/_default/09-logs-and-analytics/01-logs-and-analytics.md" heading="Query Usage Stats" tab="Node.js" index=1 - const response = await client.moderation.queryUsageStats({ - from: '2024-01-01T00:00:00Z', - to: '2024-01-31T23:59:59Z', - }); - // #endregion snippet - - expect(Array.isArray(response.items)).toBe(true); - }); - - it.skip('BLOCKED: SDK issue: the node SDK has no getUserModerationReport (GET /api/v2/moderation/user_report is left out of the spec)', async () => { - const reportUser = await createMember('report-user'); - // The node SDK has no getUserModerationReport: type the missing method (from the - // API's response) so the skipped region typechecks and lints. It isn't there at runtime. - const client = getServerClient() as StreamClient & { - moderation: { - getUserModerationReport: (request: { - user_id: string; - }) => Promise<{ user: UserResponse }>; - }; - }; - - // #region snippet docs="moderation/_default/09-logs-and-analytics/01-logs-and-analytics.md" heading="Get User Moderation Report" tab="Node.js" index=1 - // COPY: reportUser="user_123" - const response = await client.moderation.getUserModerationReport({ - user_id: reportUser, - }); - // #endregion snippet - - expect(response.user.id).toBe(reportUser); - }); - it('gets the flag count of a user', async () => { const flaggedUser = await createMember('flagged'); await flagMessage(flaggedUser); diff --git a/__tests__/docs-snippets/pages/moderation/11-guides/06-bypass-user-from-moderation.test.ts b/__tests__/docs-snippets/pages/moderation/11-guides/06-bypass-user-from-moderation.test.ts new file mode 100644 index 00000000..cc295e70 --- /dev/null +++ b/__tests__/docs-snippets/pages/moderation/11-guides/06-bypass-user-from-moderation.test.ts @@ -0,0 +1,37 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { getServerClient } from '../../../helpers/clients'; +import { Cleanup } from '../../../helpers/cleanup'; +import { uniqueId } from '../../../helpers/ids'; +import { getServerUser } from '../../../helpers/server'; + +const DOCS = 'moderation/_default/11-guides/06-bypass-user-from-moderation.md'; + +describe(DOCS, () => { + const client = getServerClient(); + const cleanup = new Cleanup(client); + // The docs' "user-123", test-owned. + const userId = uniqueId('user'); + + beforeAll(async () => { + cleanup.users.push(userId); + await client.upsertUsers([{ id: userId }]); + }); + + afterAll(() => cleanup.run()); + + it('enables moderation bypass for a user', async () => { + expect( + (await getServerUser(client, userId)).bypass_moderation ?? false, + ).toBe(false); + + // #region snippet docs="moderation/_default/11-guides/06-bypass-user-from-moderation.md" heading="Enable or Disable Bypass" tab="Node.js" index=1 + // COPY: userId="user-123" + await client.moderation.bypass({ + target_user_id: userId, + enabled: true, + }); + // #endregion snippet + + expect((await getServerUser(client, userId)).bypass_moderation).toBe(true); + }); +}); diff --git a/__tests__/docs-snippets/pages/moderation/node/01-getting-started/01-getting-started.test.ts b/__tests__/docs-snippets/pages/moderation/node/01-getting-started/01-getting-started.test.ts index 5b442fe4..7d578e4b 100644 --- a/__tests__/docs-snippets/pages/moderation/node/01-getting-started/01-getting-started.test.ts +++ b/__tests__/docs-snippets/pages/moderation/node/01-getting-started/01-getting-started.test.ts @@ -112,22 +112,22 @@ describe(DOCS, () => { }); it('checks content', async () => { - const messageId = uniqueId('message'); + const postId = uniqueId('post'); const log = vi.spyOn(console, 'log').mockImplementation(() => undefined); // #region snippet docs="moderation/node/01-getting-started/01-getting-started.md" heading="Check content" tab="Node.js" index=1 - // COPY: messageId="message-123", userId="user-456", myConfig="my_config" + // COPY: postId="post-123", userId="user-456", myConfig="my_config" const response = await client.moderation.check({ - entity_type: 'stream:chat:v1:message', - entity_id: messageId, + entity_type: 'post', + entity_id: postId, entity_creator_id: userId, moderation_payload: { - texts: ['Hello, this is a test message'], + texts: ['Selling a bike, pickup in the city'], }, config_key: myConfig, }); - console.log(response.recommended_action); // "keep", "flag", or "remove" + console.log(response.recommended_action); // "keep", "flag", "remove", "shadow", or "bounce" // #endregion snippet expect(response.status).toBe('complete'); @@ -155,19 +155,25 @@ describe(DOCS, () => { // #region snippet docs="moderation/node/01-getting-started/01-getting-started.md" heading="Handle the response" tab="Node.js" index=1 if (response.recommended_action === 'keep') { - // Content is safe, no action needed + // Publish the content } else if (response.recommended_action === 'flag') { - // Content is suspicious; Stream has queued it for review + // Publish it, and review the queued item console.log('Flagged for review:', response.item); - } else if (response.recommended_action === 'remove') { - // Content violates policies, remove it - console.log('Content removed:', response.item); + } else if (response.recommended_action === 'shadow') { + // Show it only to the author + } else if ( + response.recommended_action === 'remove' || + response.recommended_action === 'bounce' || + response.recommended_action === 'bounce_remove' + ) { + // Do not publish it. Removed items stay in the review queue. + console.log('Not published:', response.item); } // #endregion snippet expect(response.recommended_action).toBe('remove'); expect(log.mock.calls).toHaveLength(1); - expect(log.mock.calls[0][0]).toBe('Content removed:'); + expect(log.mock.calls[0][0]).toBe('Not published:'); expect(log.mock.calls[0][1]).toBe(response.item); expect(typeof response.item?.id).toBe('string'); }); diff --git a/__tests__/docs-snippets/pages/platform/02-identity-and-access/01-authentication.test.ts b/__tests__/docs-snippets/pages/platform/02-identity-and-access/01-authentication.test.ts index e3f93b0f..0e428d2c 100644 --- a/__tests__/docs-snippets/pages/platform/02-identity-and-access/01-authentication.test.ts +++ b/__tests__/docs-snippets/pages/platform/02-identity-and-access/01-authentication.test.ts @@ -9,6 +9,7 @@ import { it, vi, } from 'vitest'; +import { nsToMs } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; @@ -198,8 +199,7 @@ describe(DOCS, () => { // Stored with sub-millisecond rounding: compare to the millisecond. expect( Math.abs( - (user.revoke_tokens_issued_before?.getTime() ?? 0) - - revokeDate.getTime(), + nsToMs(user.revoke_tokens_issued_before ?? 0) - revokeDate.getTime(), ), ).toBeLessThanOrEqual(1); } diff --git a/__tests__/docs-snippets/pages/platform/02-identity-and-access/02-users.test.ts b/__tests__/docs-snippets/pages/platform/02-identity-and-access/02-users.test.ts index 6ac44594..081a3526 100644 --- a/__tests__/docs-snippets/pages/platform/02-identity-and-access/02-users.test.ts +++ b/__tests__/docs-snippets/pages/platform/02-identity-and-access/02-users.test.ts @@ -317,7 +317,7 @@ describe(DOCS, () => { expect(response.users.length).toBeGreaterThan(0); expect(response.users.length).toBeLessThanOrEqual(10); expect(response.users.every((u) => u.role === 'admin')).toBe(true); - const created = response.users.map((u) => u.created_at.getTime()); + const created = response.users.map((u) => u.created_at); expect(created).toEqual([...created].sort((a, b) => b - a)); // The newest admin is the one created just now. expect(response.users.map((u) => u.id)).toContain(adminId); @@ -451,7 +451,7 @@ describe(DOCS, () => { (r) => r.value as Promise, ), ); - expect(deactivated.user?.deactivated_at).toBeInstanceOf(Date); + expect(deactivated.user?.deactivated_at).toEqual(expect.any(Number)); // Bulk reactivation is asynchronous too. const [reactivated] = await Promise.all( reactivateUsers.mock.results.map( @@ -462,7 +462,7 @@ describe(DOCS, () => { expect((await findUser(userId))?.deactivated_at).toBeUndefined(); await waitForTask(client, deactivateResponse.task_id); for (const id of [bulkUser1, bulkUser2]) { - expect((await findUser(id))?.deactivated_at).toBeInstanceOf(Date); + expect((await findUser(id))?.deactivated_at).toEqual(expect.any(Number)); } }); @@ -489,7 +489,7 @@ describe(DOCS, () => { const { message: stored } = await client.chat.getMessage({ id: message.id, }); - expect(stored.deleted_at).toBeInstanceOf(Date); + expect(stored.deleted_at).toEqual(expect.any(Number)); }); return result; }); @@ -511,7 +511,7 @@ describe(DOCS, () => { // #endregion snippet expect(deactivate.user).toMatchObject({ id: userID }); - expect(deactivate.user?.deactivated_at).toBeInstanceOf(Date); + expect(deactivate.user?.deactivated_at).toEqual(expect.any(Number)); expect(reactivate.user).toMatchObject({ id: userID, name: 'I am back' }); expect(reactivate.user?.deactivated_at).toBeUndefined(); await retry(async () => { diff --git a/__tests__/docs-snippets/pages/platform/03-events-and-notifications/04-push-devices.test.ts b/__tests__/docs-snippets/pages/platform/03-events-and-notifications/04-push-devices.test.ts index e427bb83..99f80ea9 100644 --- a/__tests__/docs-snippets/pages/platform/03-events-and-notifications/04-push-devices.test.ts +++ b/__tests__/docs-snippets/pages/platform/03-events-and-notifications/04-push-devices.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import type { DeviceResponse } from '../../../../../index'; +import type { DeviceResponse, TimestampNS } from '../../../../../index'; import { getServerClient } from '../../../helpers/clients'; import { Cleanup } from '../../../helpers/cleanup'; import { uniqueId } from '../../../helpers/ids'; @@ -128,9 +128,9 @@ describe(DOCS, () => { it('device object (Listing from the feeds client SDKs)', () => { // #region snippet docs="platform/03-events-and-notifications/04-push-devices.md" heading="Listing from the feeds client SDKs" tab="Node.js" index=2 type Device = { - created_at: Date; + created_at: TimestampNS; // unix nanoseconds, nsToDate() converts it id: string; - push_provider: string; + push_provider: 'firebase' | 'apn' | 'huawei' | 'xiaomi'; user_id: string; disabled?: boolean; disabled_reason?: string; diff --git a/__tests__/docs-snippets/pages/platform/03-events-and-notifications/05-push-preferences.test.ts b/__tests__/docs-snippets/pages/platform/03-events-and-notifications/05-push-preferences.test.ts index 2fa17b89..450326fe 100644 --- a/__tests__/docs-snippets/pages/platform/03-events-and-notifications/05-push-preferences.test.ts +++ b/__tests__/docs-snippets/pages/platform/03-events-and-notifications/05-push-preferences.test.ts @@ -78,15 +78,8 @@ describe(DOCS, () => { // #region snippet docs="platform/03-events-and-notifications/05-push-preferences.md" heading="Channel Configuration Push Level" tab="Node.js" index=1 // COPY: channelType="messaging", channelId="announcements" // Set push_level on a channel type - // automod, automod_behavior and max_message_length are required on this endpoint, - // so carry the current values through to avoid overwriting them. - const current = await client.chat.getChannelType({ name: channelType }); - await client.chat.updateChannelType({ name: channelType, - automod: current.automod, - automod_behavior: current.automod_behavior, - max_message_length: current.max_message_length, push_level: 'all_mentions', }); @@ -106,8 +99,8 @@ describe(DOCS, () => { return response; }); expect(updated.push_level).toBe('all_mentions'); - expect(updated.automod).toBe(current.automod); - expect(updated.max_message_length).toBe(current.max_message_length); + expect(updated.automod).toBe(before.automod); + expect(updated.max_message_length).toBe(before.max_message_length); // The override wins over the type's `all_mentions` in the merged channel config. const { channel: response } = await channel.getOrCreate({}); @@ -273,34 +266,4 @@ describe(DOCS, () => { }, ); }); - - it('Setting Call Push Preferences', async () => { - const upsert = vi.spyOn(client, 'updatePushNotificationPreferences'); - - // #region snippet docs="platform/03-events-and-notifications/05-push-preferences.md" heading="Setting Call Push Preferences" tab="Node.js" index=1 - // COPY: userId1="user-1" - // Set call-level preferences with temporary disabling - const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); - - await client.updatePushNotificationPreferences({ - preferences: [ - { - user_id: userId1, - call_level: 'all', - disabled_until: oneHourFromNow, - }, - ], - }); - // #endregion snippet - - const [response] = await upsertResults(upsert); - const preferences = response.user_preferences[userId1]; - expect(preferences?.call_level).toBe('all'); - expect( - Math.abs( - (preferences?.disabled_until?.getTime() ?? 0) - - oneHourFromNow.getTime(), - ), - ).toBeLessThan(1000); - }); }); diff --git a/__tests__/docs-snippets/pages/video/api/01-basics/03-calls.test.ts b/__tests__/docs-snippets/pages/video/api/01-basics/03-calls.test.ts index 608f5d62..d651c6af 100644 --- a/__tests__/docs-snippets/pages/video/api/01-basics/03-calls.test.ts +++ b/__tests__/docs-snippets/pages/video/api/01-basics/03-calls.test.ts @@ -7,6 +7,7 @@ import { it, vi, } from 'vitest'; +import { nsToMs } from '../../../../../../index'; import { OwnCapability } from '../../../../../../index'; import { getServerClient } from '../../../../helpers/clients'; import { Cleanup } from '../../../../helpers/cleanup'; @@ -315,7 +316,7 @@ describe(DOCS, () => { // #endregion snippet const { call: stored } = await call.get(); - expect(stored.ended_at).toBeInstanceOf(Date); + expect(stored.ended_at).toEqual(expect.any(Number)); }); it('queries calls with sorting and pagination', async () => { @@ -403,7 +404,9 @@ describe(DOCS, () => { >); expect(calls.length).toBeGreaterThan(0); for (const { call } of calls) { - expect(call.starts_at?.getTime()).toBeGreaterThan(inNext30mins.getTime()); + expect(nsToMs(call.starts_at ?? 0)).toBeGreaterThan( + inNext30mins.getTime(), + ); } }); diff --git a/__tests__/docs-snippets/pages/video/api/01-basics/04-ring-calls.test.ts b/__tests__/docs-snippets/pages/video/api/01-basics/04-ring-calls.test.ts index bc6c5473..d6cfb2da 100644 --- a/__tests__/docs-snippets/pages/video/api/01-basics/04-ring-calls.test.ts +++ b/__tests__/docs-snippets/pages/video/api/01-basics/04-ring-calls.test.ts @@ -153,6 +153,57 @@ describe(DOCS, () => { expect(stored.settings.ring).toMatchObject(expected); }); + it('sets the lifecycle mode on a call type', async () => { + const callTypeName = await createCallType(client, cleanup); + expect( + (await client.video.getCallType({ name: callTypeName })).settings + .lifecycle.mode, + ).toBe('reusable'); + + // #region snippet docs="video/api/01-basics/04-ring-calls.md" heading="Setting the mode" tab="Node.js" index=1 + await client.video.updateCallType({ + name: callTypeName, + settings: { + lifecycle: { mode: 'single_use' }, + }, + }); + // #endregion snippet + + const callType = await client.video.getCallType({ name: callTypeName }); + expect(callType.settings.lifecycle.mode).toBe('single_use'); + }); + + it('sets the lifecycle mode on one call', async () => { + const getOrCreate = vi.spyOn(client.video, 'getOrCreateCall'); + + // #region snippet docs="video/api/01-basics/04-ring-calls.md" heading="Setting the mode" tab="Node.js" index=2 + // COPY: myselfId="myself", friendId="my friend" + const call = client.video.call('default', crypto.randomUUID()); + await call.getOrCreate({ + ring: true, + data: { + created_by_id: myselfId, + members: [{ user_id: myselfId }, { user_id: friendId }], + settings_override: { + lifecycle: { mode: 'single_use' }, + }, + }, + }); + // #endregion snippet + cleanup.calls.push(call.cid); + + const response = await (getOrCreate.mock.results[0].value as ReturnType< + typeof client.video.getOrCreateCall + >); + expect(response.created).toBe(true); + expect(response.call.settings.lifecycle.mode).toBe('single_use'); + // The built-in `default` type itself stays reusable. + expect( + (await client.video.getCallType({ name: 'default' })).settings.lifecycle + .mode, + ).toBe('reusable'); + }); + it('configures push notifications on a call type', async () => { const pushCallType = uniqueId('call-type'); const create = trackCallTypeCreates(client, cleanup); diff --git a/__tests__/docs-snippets/pages/video/api/03-streaming/02-backstage.test.ts b/__tests__/docs-snippets/pages/video/api/03-streaming/02-backstage.test.ts index 8141f4aa..b46ceba3 100644 --- a/__tests__/docs-snippets/pages/video/api/03-streaming/02-backstage.test.ts +++ b/__tests__/docs-snippets/pages/video/api/03-streaming/02-backstage.test.ts @@ -7,6 +7,7 @@ import { it, vi, } from 'vitest'; +import { nsToMs } from '../../../../../../index'; import { OwnCapability } from '../../../../../../index'; import { createCallType } from '../../../../helpers/call-types'; import { getServerClient } from '../../../../helpers/clients'; @@ -69,7 +70,7 @@ describe(DOCS, () => { >); expect(response.created).toBe(true); expect(response.call.backstage).toBe(true); - expect(response.call.starts_at?.getTime()).toBe(startsAt.getTime()); + expect(nsToMs(response.call.starts_at ?? 0)).toBe(startsAt.getTime()); expect(response.call.join_ahead_time_seconds).toBe(300); expect(response.call.settings.backstage).toMatchObject({ enabled: true, diff --git a/__tests__/docs-snippets/pages/video/api/04-quality/03-stats.test.ts b/__tests__/docs-snippets/pages/video/api/04-quality/03-stats.test.ts index d82e5b5e..846e237d 100644 --- a/__tests__/docs-snippets/pages/video/api/04-quality/03-stats.test.ts +++ b/__tests__/docs-snippets/pages/video/api/04-quality/03-stats.test.ts @@ -21,11 +21,9 @@ describe(DOCS, () => { // #endregion snippet // The status covers the last minute. - expect(response.start_time).toBeInstanceOf(Date); - expect(response.end_time).toBeInstanceOf(Date); - expect( - response.end_time.getTime() - response.start_time.getTime(), - ).toBeGreaterThan(0); + expect(response.start_time).toEqual(expect.any(Number)); + expect(response.end_time).toEqual(expect.any(Number)); + expect(response.end_time - response.start_time).toBeGreaterThan(0); // The test app usually has no running calls, so the numbers can be 0. expect(typeof response.summary?.active_calls).toBe('number'); expect(typeof response.summary?.active_publishers).toBe('number'); diff --git a/__tests__/docs-snippets/pages/video/api/06-moderation/01-overview.test.ts b/__tests__/docs-snippets/pages/video/api/06-moderation/01-overview.test.ts index bf0d832d..f58e92a0 100644 --- a/__tests__/docs-snippets/pages/video/api/06-moderation/01-overview.test.ts +++ b/__tests__/docs-snippets/pages/video/api/06-moderation/01-overview.test.ts @@ -14,6 +14,7 @@ import type { GetBlockedUsersResponse, ModerationBanResponse, MuteUsersResponse, + TimestampNS, UpdateUserPermissionsResponse, } from '../../../../../../index'; import { getServerClient } from '../../../../helpers/clients'; @@ -217,7 +218,10 @@ describe(DOCS, () => { const originalBan = client.moderation.ban.bind(client.moderation); const ban = vi.spyOn(client.moderation, 'ban'); const unban = vi.spyOn(client.moderation, 'unban'); - const bansAfterEachCall: Array<{ reason?: string; expires?: Date }> = []; + const bansAfterEachCall: Array<{ + reason?: string; + expires?: TimestampNS; + }> = []; ban.mockImplementation(async (request) => { const response = await originalBan(request); const { bans } = await client.queryBannedUsers({ @@ -267,7 +271,7 @@ describe(DOCS, () => { expect(bansAfterEachCall).toHaveLength(3); expect(bansAfterEachCall[0]).toMatchObject({ reason: banReason }); expect(bansAfterEachCall[0].expires).toBeUndefined(); - expect(bansAfterEachCall[1].expires).toBeInstanceOf(Date); + expect(bansAfterEachCall[1].expires).toEqual(expect.any(Number)); const { bans } = await client.queryBannedUsers({ payload: { filter_conditions: { user_id: badUserId } }, }); @@ -318,7 +322,7 @@ describe(DOCS, () => { expect((await findUser(userId))?.deactivated_at).toBeUndefined(); await waitForTask(client, deactivateResponse.task_id); for (const id of [bulkUser1, bulkUser2]) { - expect((await findUser(id))?.deactivated_at).toBeInstanceOf(Date); + expect((await findUser(id))?.deactivated_at).toEqual(expect.any(Number)); } }); diff --git a/__tests__/docs-snippets/sync/parse.ts b/__tests__/docs-snippets/sync/parse.ts index a7d8ed06..08816208 100644 --- a/__tests__/docs-snippets/sync/parse.ts +++ b/__tests__/docs-snippets/sync/parse.ts @@ -99,7 +99,10 @@ const PAGE_PREFIXES = [ * Sidebar pages that are deliberately not validated (page path relative to * `content/docs`, reason). */ -export const EXCLUDED_PAGES: Record = {}; +export const EXCLUDED_PAGES: Record = { + 'activity-feeds/v3-latest/_default/13-importing_data/03_v2_to_v3_code_migration.md': + 'Feeds v2 to v3 porting guide: before/after code in fences titled "Feeds v2" / "Feeds v3", not SDK tabs (a `label` would replace the visible title)', +}; const FEEDS_V2_CLIENT = 'Feeds v2 code for the `getstream` package (a different SDK), not @stream-io/node-sdk'; diff --git a/__tests__/integration/agent.test.ts b/__tests__/integration/agent.test.ts index fe9c8591..2a8802ed 100644 --- a/__tests__/integration/agent.test.ts +++ b/__tests__/integration/agent.test.ts @@ -67,7 +67,7 @@ describe.skip('AI agent integration', () => { const [, agent] = await createTestStreamAndRealtimeClients(); let errorEvent: any = null; - agent.on('realtime.event', ({ event }) => { + agent.on('realtime.event', ({ event }: { event: any }) => { if (event.type === 'error') { errorEvent = event; } @@ -147,7 +147,7 @@ describe.skip('AI agent integration', () => { required: ['lat', 'lng', 'location'], }, }, - async ({ lat, lng }) => { + async ({ lat, lng }: { lat: number; lng: number }) => { const result = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,wind_speed_10m`, ); diff --git a/__tests__/integration/date-transform.test.ts b/__tests__/integration/date-transform.test.ts index 0eb59d8a..392e587b 100644 --- a/__tests__/integration/date-transform.test.ts +++ b/__tests__/integration/date-transform.test.ts @@ -3,8 +3,9 @@ import { randomUUID } from 'crypto'; import { createTestClient } from './create-test-client'; import { StreamClient } from '../../src/StreamClient'; import { UserRequest } from '../../src/gen/models'; +import { nsToDate, nsToMs } from '../../src/utils/time'; -describe('Date conversion', () => { +describe('dates on responses', () => { let client: StreamClient; const user = { id: 'stream-node-test-user', @@ -36,18 +37,18 @@ describe('Date conversion', () => { const createdAt = response.call.created_at; - expect(createdAt instanceof Date).toBe(true); - expect(now - createdAt.getTime()).toBeLessThan(oneMin); + expect(typeof createdAt).toBe('number'); + expect(now - nsToMs(createdAt)).toBeLessThan(oneMin); - expect(response.call.starts_at instanceof Date).toBe(true); - expect(startsAt.getTime()).toEqual(response.call.starts_at?.getTime()); + expect(typeof response.call.starts_at).toBe('number'); + expect(startsAt.getTime()).toEqual(nsToMs(response.call.starts_at!)); expect(response.call.ended_at).toBeNull(); expect(response.members.length).toBeGreaterThan(0); response.members.forEach((m) => { - expect(m.created_at instanceof Date).toBe(true); + expect(typeof m.created_at).toBe('number'); }); const queryResult = await client.video.queryCalls({ @@ -75,28 +76,26 @@ describe('Date conversion', () => { const createdAt = response.channel!.created_at; - expect(createdAt instanceof Date).toBe(true); - expect(now - createdAt.getTime()).toBeLessThan(oneMin); + expect(typeof createdAt).toBe('number'); + expect(now - nsToMs(createdAt)).toBeLessThan(oneMin); expect(response.channel!.deleted_at).toBeUndefined(); expect(response.members.length).toBeGreaterThan(0); response.members.forEach((m) => { - expect(m.created_at instanceof Date).toBe(true); + expect(typeof m.created_at).toBe('number'); }); const queryResult = await client.chat.queryChannels({ filter_conditions: { - created_at: { $lte: createdAt.toISOString() }, + created_at: { $lte: nsToDate(createdAt).toISOString() }, }, limit: 10, }); queryResult.channels.forEach((c) => { - expect(c.channel!.created_at.getTime()).toBeLessThanOrEqual( - createdAt.getTime(), - ); + expect(c.channel!.created_at).toBeLessThanOrEqual(createdAt); }); await channel.delete(); @@ -113,7 +112,7 @@ describe('Date conversion', () => { const response = await client.upsertUsers([newUser]); - expect(response.users[newUser.id].created_at instanceof Date).toBe(true); + expect(typeof response.users[newUser.id].created_at).toBe('number'); expect( typeof response.users[newUser.id].custom.created_at === 'string', diff --git a/__tests__/integration/devices-push.test.ts b/__tests__/integration/devices-push.test.ts index 30aad6ff..c665fdef 100644 --- a/__tests__/integration/devices-push.test.ts +++ b/__tests__/integration/devices-push.test.ts @@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { randomUUID } from 'crypto'; import { createTestClient } from './create-test-client'; import { StreamClient } from '../../src/StreamClient'; -import { CreateDeviceRequest, PushProvider } from '../../src/gen/models'; +import { CreateDeviceRequest, PushProviderRequest } from '../../src/gen/models'; describe('devices and push', () => { let client: StreamClient; @@ -16,7 +16,7 @@ describe('devices and push', () => { push_provider_name: 'firebase', user_id: user.id, }; - const pushProvider: PushProvider = { + const pushProvider: PushProviderRequest = { name: 'test-push-provider', type: 'xiaomi', xiaomi_app_secret: '', diff --git a/__tests__/integration/messages.test.ts b/__tests__/integration/messages.test.ts index e6fa2db4..dafa4326 100644 --- a/__tests__/integration/messages.test.ts +++ b/__tests__/integration/messages.test.ts @@ -57,7 +57,6 @@ describe('messages API', () => { }); it('thread replies', async () => { - const now = new Date(); const response = await channel.sendMessage({ message: { text: 'Hello from Stream Node SDK', @@ -77,7 +76,6 @@ describe('messages API', () => { const getResponse = await client.chat.getReplies({ parent_id: response.message.id, - created_at_after: now, }); expect( diff --git a/__tests__/integration/rate-limit.test.ts b/__tests__/integration/rate-limit.test.ts index 425a3d7e..361e62be 100644 --- a/__tests__/integration/rate-limit.test.ts +++ b/__tests__/integration/rate-limit.test.ts @@ -1,6 +1,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { createTestClient } from './create-test-client'; import { StreamClient } from '../../src/StreamClient'; +import { StreamError } from '../../src/types'; describe('rate limit', () => { let client: StreamClient; @@ -61,12 +62,14 @@ describe('rate limit', () => { try { await call.startTranscription(); throw new Error(`Method didn't throw`); - } catch (error) { + } catch (err) { + const error = err as StreamError; + expect(error.code).toBeDefined(); expect(error.metadata).toBeDefined(); expect(error.metadata.responseCode).toBe(404); - const rateLimit = error.metadata.rateLimit; + const rateLimit = error.metadata.rateLimit!; expect(rateLimit.rateLimit).toBeDefined(); } diff --git a/generate-openapi.sh b/generate-openapi.sh index 7f360b12..0fbc4d20 100755 --- a/generate-openapi.sh +++ b/generate-openapi.sh @@ -6,6 +6,6 @@ CHAT_DIR="../chat" rm -rf $OUTPUT_DIR -( cd $CHAT_DIR ; make openapi ; make -C tools/openapi build ; ./build/openapi generate-client --language ts --spec ./releases/v2/serverside-api.yaml --output $OUTPUT_DIR ) +( cd $CHAT_DIR ; make openapi ; make -C tools/openapi build ; ./build/openapi generate-client --language ts --spec ./releases/v2/serverside-api.yaml --opt response_dates_as_number=true --output $OUTPUT_DIR ) yarn lint:gen diff --git a/index.ts b/index.ts index 969e9ca3..62427801 100644 --- a/index.ts +++ b/index.ts @@ -11,4 +11,14 @@ export { InvalidWebhookErrorMessages, verifyAndParseWebhook, } from './src/utils/webhook'; +export { + convertTimestampToDate, + dateToNs, + msToNs, + nowNs, + NS_PER_MS, + nsToDate, + nsToMs, + nsToRfc3339, +} from './src/utils/time'; export { StreamError } from './src/types'; diff --git a/src/gen/chat/ChannelApi.ts b/src/gen/chat/ChannelApi.ts index 1c4f16c5..435c1b97 100644 --- a/src/gen/chat/ChannelApi.ts +++ b/src/gen/chat/ChannelApi.ts @@ -4,6 +4,10 @@ import { ChannelStateResponse, DeleteChannelResponse, EventResponse, + ForkChannelRequest, + ForkChannelResponse, + ForkMessageIDsRequest, + ForkMessageIDsResponse, GetDraftResponse, GetManyMessagesResponse, GetPinnedMessagesResponse, @@ -15,6 +19,8 @@ import { QueryChannelBansRequest, QueryChannelBansResponse, Response, + RewindChannelRequest, + RewindChannelResponse, SendEventRequest, SendMessageRequest, SendMessageResponse, @@ -204,6 +210,38 @@ export class ChannelApi { }); } + fork( + request: ForkChannelRequest, + ): Promise> { + if (!this.id) { + throw new Error( + `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, + ); + } + + return this.chatApi.forkChannel({ + id: this.id, + type: this.type, + ...request, + }); + } + + forkMessageIDs( + request: ForkMessageIDsRequest, + ): Promise> { + if (!this.id) { + throw new Error( + `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, + ); + } + + return this.chatApi.forkMessageIDs({ + id: this.id, + type: this.type, + ...request, + }); + } + hide( request?: HideChannelRequest, ): Promise> { @@ -359,6 +397,22 @@ export class ChannelApi { return this.chatApi.markRead({ id: this.id, type: this.type, ...request }); } + rewind( + request: RewindChannelRequest, + ): Promise> { + if (!this.id) { + throw new Error( + `Channel isn't yet created, call getOrCreateDistinctChannel() before this operation`, + ); + } + + return this.chatApi.rewindChannel({ + id: this.id, + type: this.type, + ...request, + }); + } + show( request?: ShowChannelRequest, ): Promise> { diff --git a/src/gen/chat/ChatApi.ts b/src/gen/chat/ChatApi.ts index ea7c71b8..5d77ef60 100644 --- a/src/gen/chat/ChatApi.ts +++ b/src/gen/chat/ChatApi.ts @@ -23,6 +23,8 @@ import { DeleteChannelsRequest, DeleteChannelsResponse, DeleteCommandResponse, + DeleteHistoryWindowTeamsRequest, + DeleteHistoryWindowTeamsResponse, DeleteMessageResponse, DeleteReactionResponse, DeleteReminderResponse, @@ -33,10 +35,15 @@ import { EventResponse, ExportChannelsRequest, ExportChannelsResponse, + ForkChannelRequest, + ForkChannelResponse, + ForkMessageIDsRequest, + ForkMessageIDsResponse, GetCampaignResponse, GetChannelTypeResponse, GetCommandResponse, GetDraftResponse, + GetHistoryWindowTeamsResponse, GetManyMessagesResponse, GetMessageResponse, GetPinnedMessagesResponse, @@ -99,6 +106,8 @@ import { QueryThreadsRequest, QueryThreadsResponse, Response, + RewindChannelRequest, + RewindChannelResponse, SearchPayload, SearchResponse, SendEventRequest, @@ -108,6 +117,8 @@ import { SendReactionResponse, SendScheduledMessageNowResponse, SendUserCustomEventRequest, + SetHistoryWindowTeamsRequest, + SetHistoryWindowTeamsResponse, SetRetentionPolicyRequest, SetRetentionPolicyResponse, ShowChannelRequest, @@ -507,6 +518,7 @@ export class ChatApi { }; const body = { hide_for_creator: request?.hide_for_creator, + include_scheduled_messages: request?.include_scheduled_messages, state: request?.state, thread_unread_counts: request?.thread_unread_counts, member_custom_include: request?.member_custom_include, @@ -829,6 +841,60 @@ export class ChatApi { return response; } + async forkChannel( + request: ForkChannelRequest & { type: string; id: string }, + ): Promise> { + const pathParams = { + type: request?.type, + id: request?.id, + }; + const body = { + message_id: request?.message_id, + channel: request?.channel, + exclude_attachments: request?.exclude_attachments, + user_id: request?.user_id, + user: request?.user, + }; + + const response = await this.apiClient.sendRequest( + 'POST', + '/api/v2/chat/channels/{type}/{id}/fork', + pathParams, + undefined, + body, + ); + + decoders['ForkChannelResponse']?.(response); + + return response; + } + + async forkMessageIDs( + request: ForkMessageIDsRequest & { type: string; id: string }, + ): Promise> { + const pathParams = { + type: request?.type, + id: request?.id, + }; + const body = { + source_message_ids: request?.source_message_ids, + user_id: request?.user_id, + user: request?.user, + }; + + const response = await this.apiClient.sendRequest( + 'POST', + '/api/v2/chat/channels/{type}/{id}/fork/message_ids', + pathParams, + undefined, + body, + ); + + decoders['ForkMessageIDsResponse']?.(response); + + return response; + } + async hideChannel( request: HideChannelRequest & { type: string; id: string }, ): Promise> { @@ -1063,6 +1129,7 @@ export class ChatApi { }; const body = { hide_for_creator: request?.hide_for_creator, + include_scheduled_messages: request?.include_scheduled_messages, state: request?.state, thread_unread_counts: request?.thread_unread_counts, member_custom_include: request?.member_custom_include, @@ -1112,6 +1179,34 @@ export class ChatApi { return response; } + async rewindChannel( + request: RewindChannelRequest & { type: string; id: string }, + ): Promise> { + const pathParams = { + type: request?.type, + id: request?.id, + }; + const body = { + expected_last_message_id: request?.expected_last_message_id, + message_id: request?.message_id, + inclusive: request?.inclusive, + user_id: request?.user_id, + user: request?.user, + }; + + const response = await this.apiClient.sendRequest( + 'POST', + '/api/v2/chat/channels/{type}/{id}/rewind', + pathParams, + undefined, + body, + ); + + decoders['RewindChannelResponse']?.(response); + + return response; + } + async showChannel( request: ShowChannelRequest & { type: string; id: string }, ): Promise> { @@ -1515,6 +1610,64 @@ export class ChatApi { return response; } + async getHistoryWindowTeams(): Promise< + StreamResponse + > { + const response = + await this.apiClient.sendRequest( + 'GET', + '/api/v2/chat/history_window/teams', + undefined, + undefined, + ); + + decoders['GetHistoryWindowTeamsResponse']?.(response); + + return response; + } + + async setHistoryWindowTeams( + request: SetHistoryWindowTeamsRequest, + ): Promise> { + const body = { + teams: request?.teams, + }; + + const response = + await this.apiClient.sendRequest( + 'POST', + '/api/v2/chat/history_window/teams', + undefined, + undefined, + body, + ); + + decoders['SetHistoryWindowTeamsResponse']?.(response); + + return response; + } + + async deleteHistoryWindowTeams( + request: DeleteHistoryWindowTeamsRequest, + ): Promise> { + const body = { + teams: request?.teams, + }; + + const response = + await this.apiClient.sendRequest( + 'POST', + '/api/v2/chat/history_window/teams/delete', + undefined, + undefined, + body, + ); + + decoders['DeleteHistoryWindowTeamsResponse']?.(response); + + return response; + } + async queryMembers(request?: { payload?: QueryMembersPayload; }): Promise> { @@ -2810,9 +2963,11 @@ export class ChatApi { async unreadCounts(request?: { user_id?: string; + include_large_channels?: boolean; }): Promise> { const queryParams = { user_id: request?.user_id, + include_large_channels: request?.include_large_channels, }; const response = diff --git a/src/gen/common/CommonApi.ts b/src/gen/common/CommonApi.ts index 557da90d..61eaff68 100644 --- a/src/gen/common/CommonApi.ts +++ b/src/gen/common/CommonApi.ts @@ -14,6 +14,8 @@ import { CheckSNSResponse, CheckSQSRequest, CheckSQSResponse, + ClaimGuestUserRequest, + ClaimGuestUserResponse, CreateBlockListRequest, CreateBlockListResponse, CreateDeviceRequest, @@ -76,6 +78,7 @@ import { PollOptionResponse, PollResponse, PollVotesResponse, + PushProviderName, QueryPollVotesRequest, QueryPollsRequest, QueryPollsResponse, @@ -186,6 +189,7 @@ export class CommonApi { sqs_key: request?.sqs_key, sqs_secret: request?.sqs_secret, sqs_url: request?.sqs_url, + two_way_block_effect_enabled: request?.two_way_block_effect_enabled, user_response_time_enabled: request?.user_response_time_enabled, video_primary_use_case: request?.video_primary_use_case, webhook_url: request?.webhook_url, @@ -1430,7 +1434,7 @@ export class CommonApi { } async deletePushProvider(request: { - type: string; + type: PushProviderName; name: string; }): Promise> { const pathParams = { @@ -1451,7 +1455,7 @@ export class CommonApi { } async getPushTemplates(request: { - push_provider_type: string; + push_provider_type: PushProviderName; push_provider_name?: string; }): Promise> { const queryParams = { @@ -2008,6 +2012,27 @@ export class CommonApi { return response; } + async claimGuestUser( + request: ClaimGuestUserRequest, + ): Promise> { + const body = { + guest_user_id: request?.guest_user_id, + user_id: request?.user_id, + }; + + const response = await this.apiClient.sendRequest( + 'POST', + '/api/v2/users/claim_guest', + undefined, + undefined, + body, + ); + + decoders['ClaimGuestUserResponse']?.(response); + + return response; + } + async deactivateUsers( request: DeactivateUsersRequest, ): Promise> { diff --git a/src/gen/feeds/FeedsApi.ts b/src/gen/feeds/FeedsApi.ts index 3bde870a..43bb2065 100644 --- a/src/gen/feeds/FeedsApi.ts +++ b/src/gen/feeds/FeedsApi.ts @@ -24,6 +24,7 @@ import { BatchQueryActivityReactionsResponse, BatchQueryCommentReactionsRequest, BatchQueryCommentReactionsResponse, + CancelExportFeedsResponse, CastPollVoteRequest, ChangeFeedVisibilityRequest, ChangeFeedVisibilityResponse, @@ -60,6 +61,9 @@ import { DeleteFeedsRetentionPolicyResponse, DeleteUserInterestsResponse, ExportFeedUserDataResponse, + ExportFeedsByTypeRequest, + ExportFeedsResponse, + FeedVisibility, FollowBatchRequest, FollowBatchResponse, FollowRequest, @@ -1675,6 +1679,64 @@ export class FeedsApi { return response; } + async exportFeedsApp(): Promise> { + const response = await this.apiClient.sendRequest( + 'POST', + '/api/v2/feeds/export/app', + undefined, + undefined, + ); + + decoders['ExportFeedsResponse']?.(response); + + return response; + } + + async cancelExportFeeds(request: { + task_id: string; + }): Promise> { + const pathParams = { + task_id: request?.task_id, + }; + + const response = + await this.apiClient.sendRequest( + 'POST', + '/api/v2/feeds/export/{task_id}/cancel', + pathParams, + undefined, + ); + + decoders['CancelExportFeedsResponse']?.(response); + + return response; + } + + async exportFeedsByType( + request: ExportFeedsByTypeRequest & { type: string }, + ): Promise> { + const pathParams = { + type: request?.type, + }; + const body = { + created_at_from: request?.created_at_from, + created_at_to: request?.created_at_to, + user_id: request?.user_id, + }; + + const response = await this.apiClient.sendRequest( + 'POST', + '/api/v2/feeds/export/{type}', + pathParams, + undefined, + body, + ); + + decoders['ExportFeedsResponse']?.(response); + + return response; + } + async listFeedGroups(request?: { include_soft_deleted?: boolean; }): Promise> { @@ -2446,7 +2508,7 @@ export class FeedsApi { } async getFeedVisibility(request: { - name: string; + name: FeedVisibility; }): Promise> { const pathParams = { name: request?.name, @@ -2466,7 +2528,7 @@ export class FeedsApi { } async updateFeedVisibility( - request: UpdateFeedVisibilityRequest & { name: string }, + request: UpdateFeedVisibilityRequest & { name: FeedVisibility }, ): Promise> { const pathParams = { name: request?.name, @@ -2699,6 +2761,7 @@ export class FeedsApi { return response; } + /** @deprecated This endpoint is deprecated. */ async followBatch( request: FollowBatchRequest, ): Promise> { @@ -3082,6 +3145,7 @@ export class FeedsApi { return response; } + /** @deprecated This endpoint is deprecated. */ async unfollowBatch( request: UnfollowBatchRequest, ): Promise> { diff --git a/src/gen/model-decoders/decoders.ts b/src/gen/model-decoders/decoders.ts index 73661040..a43d0fb7 100644 --- a/src/gen/model-decoders/decoders.ts +++ b/src/gen/model-decoders/decoders.ts @@ -34,6494 +34,25 @@ const decode = (typeMappings: TypeMapping, input?: Record) => { return input; }; -decoders['AcceptFeedMemberInviteResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - member: { type: 'FeedMemberResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AcceptFollowResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follow: { type: 'FollowResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActionLogResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - review_queue_item: { type: 'ReviewQueueItemResponse', isSingle: true }, - - target_user: { type: 'UserResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityFeedbackEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityMarkEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityPinResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityPinnedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - pinned_activity: { type: 'PinActivityResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityReactionAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityReactionDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityReactionUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityRemovedFromFeedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - comments: { type: 'CommentResponse', isSingle: false }, - - latest_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_bookmarks: { type: 'BookmarkResponse', isSingle: false }, - - own_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - collections: { type: 'EnrichedCollectionResponse', isSingle: false }, - - reaction_groups: { type: 'FeedsReactionGroupResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - edited_at: { type: 'DatetimeType', isSingle: true }, - - expires_at: { type: 'DatetimeType', isSingle: true }, - - friend_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - latest_shares: { type: 'ShareResponse', isSingle: false }, - - current_feed: { type: 'FeedResponse', isSingle: true }, - - parent: { type: 'ActivityResponse', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityRestoredEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivitySelectorConfigResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - cutoff_time: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityUnpinnedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - pinned_activity: { type: 'PinActivityResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ActivityUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AddActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AddBookmarkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bookmark: { type: 'BookmarkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AddCommentBookmarkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bookmark: { type: 'BookmarkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AddCommentReactionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comment: { type: 'CommentResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - - reference_activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AddCommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comment: { type: 'CommentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AddCommentsBatchResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comments: { type: 'CommentResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['AddReactionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - - reference_activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AddUserGroupMembersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user_group: { type: 'UserGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AggregatedActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - activities: { type: 'ActivityResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - decoders['AppResponseFields'] = (input?: { [key: string]: any }) => { const typeMappings: TypeMapping = { event_hooks: { type: 'EventHook', isSingle: false }, - - call_types: { type: 'CallType', isSingle: false }, - - channel_configs: { type: 'ChannelConfig', isSingle: false }, - - push_notifications: { type: 'PushNotificationFields', isSingle: true }, - - revoke_tokens_issued_before: { type: 'DatetimeType', isSingle: true }, }; return decode(typeMappings, input); }; -decoders['AppealAcceptedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - appeal: { type: 'AppealItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AppealCreatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - appeal: { type: 'AppealItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AppealItemResponse'] = (input?: { [key: string]: any }) => { +decoders['EventHook'] = (input?: { [key: string]: any }) => { const typeMappings: TypeMapping = { created_at: { type: 'DatetimeType', isSingle: true }, updated_at: { type: 'DatetimeType', isSingle: true }, - - actions: { type: 'ActionLogResponse', isSingle: false }, - - flags: { type: 'ModerationFlagResponse', isSingle: false }, - - moderation_action: { type: 'ActionLogResponse', isSingle: true }, - - original_moderation_action: { type: 'ActionLogResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AppealRejectedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - appeal: { type: 'AppealItemResponse', isSingle: true }, }; return decode(typeMappings, input); }; -decoders['AsyncBulkImageModerationEvent'] = (input?: { - [key: string]: any; -}) => { +decoders['GetApplicationResponse'] = (input?: { [key: string]: any }) => { const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AsyncExportChannelsEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AsyncExportErrorEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AsyncExportLabelResultsEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AsyncExportModerationLogsEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AsyncExportReviewQueueEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AsyncExportUsersEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AsyncImportErrorEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AsyncImportSuccessEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['AutomodDetailsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - result: { type: 'MessageModerationResult', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BanInfoResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - expires: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelMetadata', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BanResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - expires: { type: 'DatetimeType', isSingle: true }, - - banned_by: { type: 'UserResponse', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BatchQueryActivityReactionsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - reactions: { type: 'FeedsReactionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['BatchQueryCommentReactionsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - reactions: { type: 'FeedsReactionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['BlockListResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BlockUsersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BlockedUserEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - - blocked_by_user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BlockedUserResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - blocked_user: { type: 'UserResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BookmarkAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - bookmark: { type: 'BookmarkResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BookmarkDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - bookmark: { type: 'BookmarkResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BookmarkFolderDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BookmarkFolderResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BookmarkFolderUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BookmarkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - - folder: { type: 'BookmarkFolderResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BookmarkUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - bookmark: { type: 'BookmarkResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['BulkActionAppealsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - results: { type: 'BulkAppealResult', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['BulkAppealResult'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - appeal_item: { type: 'AppealItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallAcceptedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallClosedCaptionsFailedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallClosedCaptionsStartedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallClosedCaptionsStoppedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallCreatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallDTMFEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - timestamp: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallEndedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - - members: { type: 'MemberResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallFrameRecordingFailedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallFrameRecordingFrameReadyEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - captured_at: { type: 'DatetimeType', isSingle: true }, - - created_at: { type: 'DatetimeType', isSingle: true }, - - users: { type: 'UserResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['CallFrameRecordingStartedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallFrameRecordingStoppedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallHLSBroadcastingFailedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallHLSBroadcastingStartedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallHLSBroadcastingStoppedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallLiveStartedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallMemberAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallMemberRemovedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallMemberUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallMemberUpdatedPermissionEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallMissedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallModerationBlurEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallModerationWarningEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallNotificationEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallParticipantResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - joined_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallParticipantTimeline'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - timestamp: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallReactionEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRecording'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - end_time: { type: 'DatetimeType', isSingle: true }, - - start_time: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRecordingFailedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRecordingReadyEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call_recording: { type: 'CallRecording', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRecordingStartedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRecordingStoppedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRejectedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallReportResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - ended_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - egress: { type: 'EgressResponse', isSingle: true }, - - ended_at: { type: 'DatetimeType', isSingle: true }, - - starts_at: { type: 'DatetimeType', isSingle: true }, - - session: { type: 'CallSessionResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRingEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRingSettledEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - rang_at: { type: 'DatetimeType', isSingle: true }, - - settled_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRtmpBroadcastFailedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRtmpBroadcastStartedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallRtmpBroadcastStoppedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallSessionEndedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallSessionParticipantCountsUpdatedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallSessionParticipantJoinedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - participant: { type: 'CallParticipantResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallSessionParticipantLeftEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - participant: { type: 'CallParticipantResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallSessionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - participants: { type: 'CallParticipantResponse', isSingle: false }, - - accepted_by: { type: 'DatetimeType', isSingle: false }, - - missed_by: { type: 'DatetimeType', isSingle: false }, - - rejected_by: { type: 'DatetimeType', isSingle: false }, - - ended_at: { type: 'DatetimeType', isSingle: true }, - - live_ended_at: { type: 'DatetimeType', isSingle: true }, - - live_started_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - - timer_ends_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallSessionStartedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallStateResponseFields'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallStatsParticipant'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - sessions: { type: 'CallStatsParticipantSession', isSingle: false }, - - latest_activity_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallStatsParticipantSession'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - ended_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallStatsReportReadyEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - participants_overview: { type: 'CallStatsParticipant', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['CallStatsReportSummaryResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - first_stats_time: { type: 'DatetimeType', isSingle: true }, - - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallStatsSessionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - generated_at: { type: 'DatetimeType', isSingle: true }, - - call_ended_at: { type: 'DatetimeType', isSingle: true }, - - call_started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallTranscription'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - end_time: { type: 'DatetimeType', isSingle: true }, - - start_time: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallTranscriptionFailedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallTranscriptionReadyEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call_transcription: { type: 'CallTranscription', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallTranscriptionStartedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallTranscriptionStoppedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallType'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallTypeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallUserFeedbackSubmittedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CallUserMutedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CampaignCompletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - campaign: { type: 'CampaignResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CampaignResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - segments: { type: 'Segment', isSingle: false }, - - users: { type: 'UserResponse', isSingle: false }, - - stats: { type: 'CampaignStatsResponse', isSingle: true }, - - scheduled_for: { type: 'DatetimeType', isSingle: true }, - - stop_at: { type: 'DatetimeType', isSingle: true }, - - sender: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CampaignStartedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - campaign: { type: 'CampaignResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CampaignStatsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - stats_completed_at: { type: 'DatetimeType', isSingle: true }, - - stats_started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChangeFeedVisibilityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed: { type: 'FeedResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelBanResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel_ban: { type: 'ChannelBanScopeResponse', isSingle: true }, - - future_channel_ban: { type: 'ChannelBanScopeResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelBanScopeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - expires: { type: 'DatetimeType', isSingle: true }, - - banned_by: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelBatchCompletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - batch_created_at: { type: 'DatetimeType', isSingle: true }, - - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelBatchStartedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - batch_created_at: { type: 'DatetimeType', isSingle: true }, - - created_at: { type: 'DatetimeType', isSingle: true }, - - finished_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelConfig'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelConfigWithInfo'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - commands: { type: 'Command', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelContextResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_by: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelCreatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelFrozenEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelHiddenEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelMemberResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - archived_at: { type: 'DatetimeType', isSingle: true }, - - ban_expires: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - future_channel_ban_expires: { type: 'DatetimeType', isSingle: true }, - - invite_accepted_at: { type: 'DatetimeType', isSingle: true }, - - invite_rejected_at: { type: 'DatetimeType', isSingle: true }, - - pinned_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelMetadata'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - last_message_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelMute'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - expires: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelMutedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - mutes: { type: 'ChannelMute', isSingle: false }, - - mute: { type: 'ChannelMute', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelPushPreferencesResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - disabled_until: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - hide_messages_before: { type: 'DatetimeType', isSingle: true }, - - last_message_at: { type: 'DatetimeType', isSingle: true }, - - mute_expires_at: { type: 'DatetimeType', isSingle: true }, - - truncated_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'ChannelMemberResponse', isSingle: false }, - - config: { type: 'ChannelConfigWithInfo', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - truncated_by: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelStateResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'ChannelMemberResponse', isSingle: false }, - - messages: { type: 'MessageResponse', isSingle: false }, - - pinned_messages: { type: 'MessageResponse', isSingle: false }, - - threads: { type: 'ThreadStateResponse', isSingle: false }, - - hide_messages_before: { type: 'DatetimeType', isSingle: true }, - - active_live_locations: { - type: 'SharedLocationResponseData', - isSingle: false, - }, - - pending_messages: { type: 'PendingMessageResponse', isSingle: false }, - - read: { type: 'ReadStateResponse', isSingle: false }, - - scheduled_messages: { type: 'ScheduledMessageResponse', isSingle: false }, - - watchers: { type: 'UserResponse', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - draft: { type: 'DraftResponse', isSingle: true }, - - membership: { type: 'ChannelMemberResponse', isSingle: true }, - - push_preferences: { - type: 'ChannelPushPreferencesResponse', - isSingle: true, - }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelStateResponseFields'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'ChannelMemberResponse', isSingle: false }, - - messages: { type: 'MessageResponse', isSingle: false }, - - pinned_messages: { type: 'MessageResponse', isSingle: false }, - - threads: { type: 'ThreadStateResponse', isSingle: false }, - - hide_messages_before: { type: 'DatetimeType', isSingle: true }, - - active_live_locations: { - type: 'SharedLocationResponseData', - isSingle: false, - }, - - pending_messages: { type: 'PendingMessageResponse', isSingle: false }, - - read: { type: 'ReadStateResponse', isSingle: false }, - - scheduled_messages: { type: 'ScheduledMessageResponse', isSingle: false }, - - watchers: { type: 'UserResponse', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - draft: { type: 'DraftResponse', isSingle: true }, - - membership: { type: 'ChannelMemberResponse', isSingle: true }, - - push_preferences: { - type: 'ChannelPushPreferencesResponse', - isSingle: true, - }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelTruncatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelTypeConfig'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - commands: { type: 'Command', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelUnFrozenEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelUnmutedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - mutes: { type: 'ChannelMute', isSingle: false }, - - mute: { type: 'ChannelMute', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChannelVisibleEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatActivityStatsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - messages: { type: 'MessageStatsResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatDraftPayloadResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - mentioned_users: { type: 'UserResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatDraftResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'ChatDraftPayloadResponse', isSingle: true }, - - parent_message: { type: 'ChatMessageResponse', isSingle: true }, - - quoted_message: { type: 'ChatMessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions: { type: 'ChatReactionResponse', isSingle: false }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_reactions: { type: 'ChatReactionResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - message_text_updated_at: { type: 'DatetimeType', isSingle: true }, - - pin_expires: { type: 'DatetimeType', isSingle: true }, - - pinned_at: { type: 'DatetimeType', isSingle: true }, - - mentioned_groups: { type: 'UserGroupResponse', isSingle: false }, - - thread_participants: { type: 'UserResponse', isSingle: false }, - - draft: { type: 'ChatDraftResponse', isSingle: true }, - - pinned_by: { type: 'UserResponse', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - quoted_message: { type: 'ChatMessageResponse', isSingle: true }, - - reaction_groups: { type: 'ChatReactionGroupResponse', isSingle: false }, - - reminder: { type: 'ChatReminderResponseData', isSingle: true }, - - shared_location: { type: 'ChatSharedLocationResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatReactionGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - first_reaction_at: { type: 'DatetimeType', isSingle: true }, - - last_reaction_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions_by: { - type: 'ChatReactionGroupUserResponse', - isSingle: false, - }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatReactionGroupUserResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatReactionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatReminderResponseData'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - remind_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'ChatMessageResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ChatSharedLocationResponseData'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - end_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'ChatMessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CheckResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - item: { type: 'ReviewQueueItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ClosedCaptionEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CollectionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['Command'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentFeedbackEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentReactionAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentReactionDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentReactionUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - edited_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - reaction_groups: { type: 'FeedsReactionGroupResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentRestoredEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CommentUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ConfigResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CountByMinuteResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - start_ts: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateBlockListResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - blocklist: { type: 'BlockListResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateCallTypeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateCampaignResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - campaign: { type: 'CampaignResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateChannelTypeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateCollectionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - collections: { type: 'CollectionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateCommandResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - command: { type: 'Command', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateFeedGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_group: { type: 'FeedGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateFeedViewResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_view: { type: 'FeedViewResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateFeedsBatchResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feeds: { type: 'FeedResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateGuestResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateImportResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - import_task: { type: 'ImportTask', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateImportV2TaskResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateMembershipLevelResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - membership_level: { type: 'MembershipLevelResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreatePredefinedFilterResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - predefined_filter: { type: 'PredefinedFilterResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateReminderResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - reminder: { type: 'ReminderResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateRoleResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - role: { type: 'Role', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateSIPTrunkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - sip_trunk: { type: 'SIPTrunkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateSegmentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - segment: { type: 'SegmentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CreateUserGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user_group: { type: 'UserGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CustomCheckResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - item: { type: 'ReviewQueueItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CustomEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['CustomVideoEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeactivateUserResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteActivityReactionResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteBookmarkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bookmark: { type: 'BookmarkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteCallResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteChannelResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channel: { type: 'ChannelResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteCommentBookmarkResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - bookmark: { type: 'BookmarkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteCommentReactionResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - comment: { type: 'CommentResponse', isSingle: true }, - - reaction: { type: 'FeedsReactionResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteCommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteReactionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - - reaction: { type: 'ReactionResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeleteScheduledMessageResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - scheduled_message: { type: 'ScheduledMessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DeviceResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['DraftPayloadResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - mentioned_users: { type: 'UserResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['DraftResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'DraftPayloadResponse', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - parent_message: { type: 'MessageResponse', isSingle: true }, - - quoted_message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['EgressRTMPResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['EgressResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - rtmps: { type: 'EgressRTMPResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['EnrichedCollectionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['EntityCreatorResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - ban_expires: { type: 'DatetimeType', isSingle: true }, - - deactivated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - last_active: { type: 'DatetimeType', isSingle: true }, - - revoke_tokens_issued_before: { type: 'DatetimeType', isSingle: true }, - - devices: { type: 'DeviceResponse', isSingle: false }, - - push_notifications: { - type: 'PushNotificationSettingsResponse', - isSingle: true, - }, - }; - return decode(typeMappings, input); -}; - -decoders['EventHook'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['EventResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - event: { type: 'WSEvent', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ExportUserResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - messages: { type: 'MessageResponse', isSingle: false }, - - reactions: { type: 'ReactionResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedCreatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'FeedMemberResponse', isSingle: false }, - - feed: { type: 'FeedResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedGroupChangedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedGroupDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - activity_selectors: { - type: 'ActivitySelectorConfigResponse', - isSingle: false, - }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedGroupRestoredEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedMemberAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - member: { type: 'FeedMemberResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedMemberRemovedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedMemberResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - - invite_accepted_at: { type: 'DatetimeType', isSingle: true }, - - invite_rejected_at: { type: 'DatetimeType', isSingle: true }, - - membership_level: { type: 'MembershipLevelResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedMemberUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - member: { type: 'FeedMemberResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedOwnData'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - own_followings: { type: 'FollowResponse', isSingle: false }, - - own_follows: { type: 'FollowResponse', isSingle: false }, - - own_membership: { type: 'FeedMemberResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - own_followings: { type: 'FollowResponse', isSingle: false }, - - own_follows: { type: 'FollowResponse', isSingle: false }, - - own_membership: { type: 'FeedMemberResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedSuggestionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - own_followings: { type: 'FollowResponse', isSingle: false }, - - own_follows: { type: 'FollowResponse', isSingle: false }, - - own_membership: { type: 'FeedMemberResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - feed: { type: 'FeedResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedViewResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - last_used_at: { type: 'DatetimeType', isSingle: true }, - - activity_selectors: { - type: 'ActivitySelectorConfigResponse', - isSingle: false, - }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsBookmarkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsEnrichedCollectionResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsFeedResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsReactionGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - first_reaction_at: { type: 'DatetimeType', isSingle: true }, - - last_reaction_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsReactionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsRetentionPolicy'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - enabled_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsShareResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsV3ActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - comments: { type: 'FeedsV3CommentResponse', isSingle: false }, - - latest_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_bookmarks: { type: 'FeedsBookmarkResponse', isSingle: false }, - - own_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - collections: { type: 'FeedsEnrichedCollectionResponse', isSingle: false }, - - reaction_groups: { type: 'FeedsReactionGroupResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - edited_at: { type: 'DatetimeType', isSingle: true }, - - expires_at: { type: 'DatetimeType', isSingle: true }, - - friend_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - latest_shares: { type: 'FeedsShareResponse', isSingle: false }, - - current_feed: { type: 'FeedsFeedResponse', isSingle: true }, - - parent: { type: 'FeedsV3ActivityResponse', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FeedsV3CommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - edited_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - reaction_groups: { type: 'FeedsReactionGroupResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['FlagDetailsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - automod: { type: 'AutomodDetailsResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FlagFeedbackResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FlagUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FollowBatchResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created: { type: 'FollowResponse', isSingle: false }, - - follows: { type: 'FollowResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['FollowCreatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - follow: { type: 'FollowResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FollowDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - follow: { type: 'FollowResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FollowResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - source_feed: { type: 'FeedResponse', isSingle: true }, - - target_feed: { type: 'FeedResponse', isSingle: true }, - - request_accepted_at: { type: 'DatetimeType', isSingle: true }, - - request_rejected_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FollowUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - follow: { type: 'FollowResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FullUserResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - channel_mutes: { type: 'ChannelMute', isSingle: false }, - - devices: { type: 'DeviceResponse', isSingle: false }, - - mutes: { type: 'UserMuteResponse', isSingle: false }, - - ban_expires: { type: 'DatetimeType', isSingle: true }, - - deactivated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - last_active: { type: 'DatetimeType', isSingle: true }, - - revoke_tokens_issued_before: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['FutureChannelBanResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - expires: { type: 'DatetimeType', isSingle: true }, - - banned_by: { type: 'UserResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetActiveCallsStatusResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - end_time: { type: 'DatetimeType', isSingle: true }, - - start_time: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetAppealResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - item: { type: 'AppealItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetApplicationResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - app: { type: 'AppResponseFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetBlockListResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - blocklist: { type: 'BlockListResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetBlockedUsersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - blocks: { type: 'BlockedUserResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCallParticipantSessionMetricsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - joined_at: { type: 'DatetimeType', isSingle: true }, - - published_tracks: { type: 'PublishedTrackMetrics', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCallReportResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - report: { type: 'ReportResponse', isSingle: true }, - - video_reactions: { type: 'VideoReactionsResponse', isSingle: false }, - - chat_activity: { type: 'ChatActivityStatsResponse', isSingle: true }, - - session: { type: 'CallSessionResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCallResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCallSessionParticipantStatsDetailsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - timeframe: { type: 'ParticipantSeriesTimeframe', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCallTypeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCampaignResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - campaign: { type: 'CampaignResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetChannelTypeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - commands: { type: 'Command', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCommandResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCommentRepliesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comments: { type: 'ThreadedCommentResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comment: { type: 'CommentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetCommentsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comments: { type: 'ThreadedCommentResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetConfigResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - config: { type: 'ConfigResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetDraftResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - draft: { type: 'DraftResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetExternalStorageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetFeedGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_group: { type: 'FeedGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetFeedViewResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_view: { type: 'FeedViewResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetFeedsRetentionPolicyResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - policies: { type: 'FeedsRetentionPolicy', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetFollowSuggestionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - suggestions: { type: 'FeedSuggestionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetImportResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - import_task: { type: 'ImportTask', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetImportV2TaskResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetManyMessagesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - messages: { type: 'MessageResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageWithChannelResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetModerationRuleResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - rule: { type: 'ModerationRuleV2Response', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetOrCreateCallResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetOrCreateFeedGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_group: { type: 'FeedGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetOrCreateFeedResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activities: { type: 'ActivityResponse', isSingle: false }, - - aggregated_activities: { - type: 'AggregatedActivityResponse', - isSingle: false, - }, - - followers: { type: 'FollowResponse', isSingle: false }, - - following: { type: 'FollowResponse', isSingle: false }, - - members: { type: 'FeedMemberResponse', isSingle: false }, - - pinned_activities: { type: 'ActivityPinResponse', isSingle: false }, - - feed: { type: 'FeedResponse', isSingle: true }, - - notification_status: { type: 'NotificationStatusResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetOrCreateFeedViewResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_view: { type: 'FeedViewResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetOrCreateFollowResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follow: { type: 'FollowResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetOrCreateUnfollowResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follow: { type: 'FollowResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetPinnedMessagesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - messages: { type: 'MessageResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetPredefinedFilterResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - predefined_filter: { type: 'PredefinedFilterResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetPushTemplatesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - templates: { type: 'PushTemplateResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetReactionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - reactions: { type: 'ReactionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetRepliesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - messages: { type: 'MessageResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetRetentionPolicyResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - policies: { type: 'RetentionPolicy', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GetReviewQueueItemResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - item: { type: 'ReviewQueueItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetSegmentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - segment: { type: 'SegmentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetSetupSessionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - setup_session: { type: 'SetupSession', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetTaskResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetThreadResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - thread: { type: 'ThreadStateResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GetUserGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user_group: { type: 'UserGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GoLiveResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['GroupedChannelsBucket'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channels: { type: 'ChannelStateResponseFields', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['GroupedQueryChannelsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - groups: { type: 'GroupedChannelsBucket', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ImportTask'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - history: { type: 'ImportTaskHistory', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ImportTaskHistory'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ImportV2TaskItem'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['IngressErrorEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['IngressStartedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['IngressStoppedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['KickedUserEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - - kicked_by_user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['LabelResultResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ListBlockListResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - blocklists: { type: 'BlockListResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListCallTypeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - call_types: { type: 'CallTypeResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListChannelTypesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channel_types: { type: 'ChannelTypeConfig', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListCommandsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - commands: { type: 'Command', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListDevicesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - devices: { type: 'DeviceResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListFeedGroupsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - groups: { type: 'FeedGroupResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListFeedViewsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - views: { type: 'FeedViewResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListImportV2TasksResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - import_tasks: { type: 'ImportV2TaskItem', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListImportsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - import_tasks: { type: 'ImportTask', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListPushProvidersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - push_providers: { type: 'PushProviderResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListQueuesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - queues: { type: 'ModerationQueueResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListRecordingsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - recordings: { type: 'CallRecording', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListRolesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - roles: { type: 'Role', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListSIPInboundRoutingRuleResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - sip_inbound_routing_rules: { - type: 'SIPInboundRoutingRuleResponse', - isSingle: false, - }, - }; - return decode(typeMappings, input); -}; - -decoders['ListSIPTrunksResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - sip_trunks: { type: 'SIPTrunkResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListTranscriptionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - transcriptions: { type: 'CallTranscription', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ListUserGroupsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user_groups: { type: 'UserGroupResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['MarkReadResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - event: { type: 'MarkReadResponseEvent', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MarkReadResponseEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel_last_message_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - thread: { type: 'ThreadResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MaxStreakChangedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MemberAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - member: { type: 'ChannelMemberResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MemberRemovedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - member: { type: 'ChannelMemberResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MemberResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MemberUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - member: { type: 'ChannelMemberResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MembersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'ChannelMemberResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['MembershipLevelResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageActionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageFlagResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - approved_at: { type: 'DatetimeType', isSingle: true }, - - rejected_at: { type: 'DatetimeType', isSingle: true }, - - reviewed_at: { type: 'DatetimeType', isSingle: true }, - - details: { type: 'FlagDetailsResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - moderation_feedback: { type: 'FlagFeedbackResponse', isSingle: true }, - - moderation_result: { type: 'MessageModerationResult', isSingle: true }, - - reviewed_by: { type: 'UserResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageFlaggedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - details: { type: 'MessageModerationResult', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageHistoryEntryResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message_updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageModerationResult'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageNewEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - thread_participants: { type: 'UserResponseCommonFields', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageReadEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - thread: { type: 'ThreadResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions: { type: 'ReactionResponse', isSingle: false }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_reactions: { type: 'ReactionResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - message_text_updated_at: { type: 'DatetimeType', isSingle: true }, - - pin_expires: { type: 'DatetimeType', isSingle: true }, - - pinned_at: { type: 'DatetimeType', isSingle: true }, - - mentioned_groups: { type: 'UserGroupResponse', isSingle: false }, - - thread_participants: { type: 'UserResponse', isSingle: false }, - - draft: { type: 'DraftResponse', isSingle: true }, - - pinned_by: { type: 'UserResponse', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - quoted_message: { type: 'MessageResponse', isSingle: true }, - - reaction_groups: { type: 'ReactionGroupResponse', isSingle: false }, - - reminder: { type: 'ReminderResponseData', isSingle: true }, - - shared_location: { type: 'SharedLocationResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageStatsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - count_over_time: { type: 'CountByMinuteResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageUnblockedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageUndeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MessageWithChannelResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions: { type: 'ReactionResponse', isSingle: false }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_reactions: { type: 'ReactionResponse', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - message_text_updated_at: { type: 'DatetimeType', isSingle: true }, - - pin_expires: { type: 'DatetimeType', isSingle: true }, - - pinned_at: { type: 'DatetimeType', isSingle: true }, - - mentioned_groups: { type: 'UserGroupResponse', isSingle: false }, - - thread_participants: { type: 'UserResponse', isSingle: false }, - - draft: { type: 'DraftResponse', isSingle: true }, - - pinned_by: { type: 'UserResponse', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - quoted_message: { type: 'MessageResponse', isSingle: true }, - - reaction_groups: { type: 'ReactionGroupResponse', isSingle: false }, - - reminder: { type: 'ReminderResponseData', isSingle: true }, - - shared_location: { type: 'SharedLocationResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationAnalysisFailedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationCallResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - ended_at: { type: 'DatetimeType', isSingle: true }, - - starts_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationCheckCompletedEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationCustomActionEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - review_queue_item: { type: 'ReviewQueueItemResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationFlagResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - content_published_at: { type: 'DatetimeType', isSingle: true }, - - review_queue_item: { type: 'ReviewQueueItemResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationFlaggedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationImageAnalysisCompleteEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationMarkReviewedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - item: { type: 'ReviewQueueItemResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationQueueResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationRuleV2Response'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationRulesTriggeredEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ModerationTextAnalysisCompleteEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MuteChannelResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channel_mutes: { type: 'ChannelMute', isSingle: false }, - - channel_mute: { type: 'ChannelMute', isSingle: true }, - - own_user: { type: 'OwnUserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['MuteResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - mutes: { type: 'UserMuteResponse', isSingle: false }, - - own_user: { type: 'OwnUserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['NotificationFeedUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - aggregated_activities: { - type: 'AggregatedActivityResponse', - isSingle: false, - }, - - notification_status: { type: 'NotificationStatusResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['NotificationMarkUnreadEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - last_read_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['NotificationStatusResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - last_read_at: { type: 'DatetimeType', isSingle: true }, - - last_seen_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['NotificationThreadMessageNewEvent'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - thread_participants: { type: 'UserResponseCommonFields', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['OwnBatchResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - data: { type: 'FeedOwnData', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['OwnUserResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - channel_mutes: { type: 'ChannelMute', isSingle: false }, - - devices: { type: 'DeviceResponse', isSingle: false }, - - mutes: { type: 'UserMuteResponse', isSingle: false }, - - deactivated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - last_active: { type: 'DatetimeType', isSingle: true }, - - revoke_tokens_issued_before: { type: 'DatetimeType', isSingle: true }, - - push_preferences: { type: 'PushPreferencesResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ParticipantCountByMinuteResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - start_ts: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ParticipantCountOverTimeResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - by_minute: { type: 'ParticipantCountByMinuteResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ParticipantReportResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - count_over_time: { - type: 'ParticipantCountOverTimeResponse', - isSingle: true, - }, - }; - return decode(typeMappings, input); -}; - -decoders['ParticipantSeriesTimeframe'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - since: { type: 'DatetimeType', isSingle: true }, - - until: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ParticipantSessionDetails'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - joined_at: { type: 'DatetimeType', isSingle: true }, - - left_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PendingMessageEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PendingMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PerformanceAnalysisResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - last_analyzed: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PermissionRequestEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PinActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PolicyTestResult'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PolicyTestRun'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - completed_at: { type: 'DatetimeType', isSingle: true }, - - config_updated_at: { type: 'DatetimeType', isSingle: true }, - - started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PolicyTestRunResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - results: { type: 'PolicyTestResult', isSingle: false }, - - run: { type: 'PolicyTestRun', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PolicyTestSet'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - last_run: { type: 'PolicyTestRun', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PolicyTestSetListResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - sets: { type: 'PolicyTestSet', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['PolicyTestSetResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - recent_runs: { type: 'PolicyTestRun', isSingle: false }, - - set: { type: 'PolicyTestSet', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollClosedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - poll: { type: 'PollResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollResponseData'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - latest_answers: { type: 'PollVoteResponseData', isSingle: false }, - - own_votes: { type: 'PollVoteResponseData', isSingle: false }, - - created_by: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollVoteCastedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - poll_vote: { type: 'PollVoteResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollVoteChangedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - poll_vote: { type: 'PollVoteResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollVoteRemovedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - poll_vote: { type: 'PollVoteResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollVoteResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - poll: { type: 'PollResponseData', isSingle: true }, - - vote: { type: 'PollVoteResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollVoteResponseData'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PollVotesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - votes: { type: 'PollVoteResponseData', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['PredefinedFilterResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - performance: { type: 'PerformanceAnalysisResponse', isSingle: true }, - - stats: { type: 'PredefinedFilterStatsResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PredefinedFilterStatsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - last_seen: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PublishedTrackMetrics'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - warnings: { type: 'SessionWarningResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['PushNotificationFields'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - providers: { type: 'PushProvider', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['PushNotificationSettingsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - disabled_until: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PushPreferencesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - disabled_until: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PushProvider'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - disabled_at: { type: 'DatetimeType', isSingle: true }, - - push_templates: { type: 'PushTemplate', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['PushProviderResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - disabled_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PushTemplate'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['PushTemplateResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryActivitiesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activities: { type: 'ActivityResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryActivityReactionsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - reactions: { type: 'FeedsReactionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryActivitySharesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - shares: { type: 'ShareResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryAppealsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - items: { type: 'AppealItemResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryBannedUsersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bans: { type: 'BanResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryBookmarkFoldersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bookmark_folders: { type: 'BookmarkFolderResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryBookmarksResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bookmarks: { type: 'BookmarkResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallMembersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'MemberResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallParticipantSessionsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - participants_sessions: { - type: 'ParticipantSessionDetails', - isSingle: false, - }, - - session: { type: 'CallSessionResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallParticipantsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - members: { type: 'MemberResponse', isSingle: false }, - - participants: { type: 'CallParticipantResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallRingAttemptsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - ring_attempts: { type: 'RingAttemptResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallSessionParticipantStatsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - participants: { type: 'CallStatsParticipant', isSingle: false }, - - call_ended_at: { type: 'DatetimeType', isSingle: true }, - - call_started_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallSessionParticipantStatsTimelineResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - events: { type: 'CallParticipantTimeline', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallSessionStatsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - call_stats: { type: 'CallStatsSessionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallStatsMapResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - call_ended_at: { type: 'DatetimeType', isSingle: true }, - - call_started_at: { type: 'DatetimeType', isSingle: true }, - - end_time: { type: 'DatetimeType', isSingle: true }, - - generated_at: { type: 'DatetimeType', isSingle: true }, - - start_time: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallStatsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - reports: { type: 'CallStatsReportSummaryResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCallsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - calls: { type: 'CallStateResponseFields', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCampaignsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - campaigns: { type: 'CampaignResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryChannelBansResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bans: { type: 'ChannelBanResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryChannelsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channels: { type: 'ChannelStateResponseFields', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCollectionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - collections: { type: 'CollectionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCommentReactionsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - reactions: { type: 'FeedsReactionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryCommentsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comments: { type: 'CommentResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryDraftsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - drafts: { type: 'DraftResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryFeedMembersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'FeedMemberResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryFeedModerationTemplate'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryFeedModerationTemplatesResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - templates: { type: 'QueryFeedModerationTemplate', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryFeedsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feeds: { type: 'FeedResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryFollowsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follows: { type: 'FollowResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryFutureChannelBansResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - bans: { type: 'FutureChannelBanResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryLabelResultsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - label_results: { type: 'LabelResultResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryMembershipLevelsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - membership_levels: { type: 'MembershipLevelResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryMessageFlagsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - flags: { type: 'MessageFlagResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryMessageHistoryResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message_history: { type: 'MessageHistoryEntryResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryModerationConfigsResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - configs: { type: 'ConfigResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryModerationFlagsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - flags: { type: 'ModerationFlagResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryModerationLogsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - logs: { type: 'ActionLogResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryModerationRulesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - rules: { type: 'ModerationRuleV2Response', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryPinnedActivitiesResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - pinned_activities: { type: 'ActivityPinResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryPollsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - polls: { type: 'PollResponseData', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryPredefinedFiltersResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - predefined_filters: { type: 'PredefinedFilterResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryReactionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - reactions: { type: 'ReactionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryRemindersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - reminders: { type: 'ReminderResponseData', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryReviewQueueResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - items: { type: 'ReviewQueueItemResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryRevisionHistoryResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - revisions: { type: 'RevisionHistoryResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryScheduledMessagesResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - scheduled_messages: { type: 'ScheduledMessageResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QuerySegmentTargetsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - targets: { type: 'SegmentTargetResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QuerySegmentsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - segments: { type: 'SegmentResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryThreadsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - threads: { type: 'ThreadStateResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueryUsersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - users: { type: 'FullUserResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['QueueResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - queue: { type: 'ModerationQueueResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['Reaction'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReactionDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - thread_participants: { type: 'UserResponseCommonFields', isSingle: false }, - - message: { type: 'MessageResponse', isSingle: true }, - - reaction: { type: 'ReactionResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReactionGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - first_reaction_at: { type: 'DatetimeType', isSingle: true }, - - last_reaction_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions_by: { type: 'ReactionGroupUserResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ReactionGroupUserResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReactionNewEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - thread_participants: { type: 'UserResponseCommonFields', isSingle: false }, - - message: { type: 'MessageResponse', isSingle: true }, - - reaction: { type: 'ReactionResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReactionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReactionUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - reaction: { type: 'ReactionResponse', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReactivateUserResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReadCollectionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - collections: { type: 'CollectionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['ReadStateResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - last_read: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - - last_delivered_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RejectFeedMemberInviteResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - member: { type: 'FeedMemberResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RejectFollowResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follow: { type: 'FollowResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReminderCreatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - reminder: { type: 'ReminderResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReminderDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - reminder: { type: 'ReminderResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReminderNotificationEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - reminder: { type: 'ReminderResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReminderResponseData'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - expires_at: { type: 'DatetimeType', isSingle: true }, - - remind_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReminderUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - reminder: { type: 'ReminderResponseData', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RemoveUserGroupMembersResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - user_group: { type: 'UserGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReportResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - call: { type: 'CallReportResponse', isSingle: true }, - - participants: { type: 'ParticipantReportResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ResolveSipInboundResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - sip_routing_rule: { type: 'SIPInboundRoutingRuleResponse', isSingle: true }, - - sip_trunk: { type: 'SIPTrunkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RestoreActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RestoreCommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - - comment: { type: 'CommentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RestoreFeedGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_group: { type: 'FeedGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RetentionPolicy'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - enabled_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReviewQueueItemNewEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - flags: { type: 'ModerationFlagResponse', isSingle: false }, - - action: { type: 'ActionLogResponse', isSingle: true }, - - review_queue_item: { type: 'ReviewQueueItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReviewQueueItemResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - actions: { type: 'ActionLogResponse', isSingle: false }, - - bans: { type: 'BanInfoResponse', isSingle: false }, - - flags: { type: 'ModerationFlagResponse', isSingle: false }, - - completed_at: { type: 'DatetimeType', isSingle: true }, - - escalated_at: { type: 'DatetimeType', isSingle: true }, - - reviewed_at: { type: 'DatetimeType', isSingle: true }, - - appeal: { type: 'AppealItemResponse', isSingle: true }, - - assigned_to: { type: 'UserResponse', isSingle: true }, - - call: { type: 'ModerationCallResponse', isSingle: true }, - - entity_creator: { type: 'EntityCreatorResponse', isSingle: true }, - - feeds_v2_reaction: { type: 'Reaction', isSingle: true }, - - feeds_v3_activity: { type: 'FeedsV3ActivityResponse', isSingle: true }, - - feeds_v3_comment: { type: 'FeedsV3CommentResponse', isSingle: true }, - - message: { type: 'ChatMessageResponse', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - reaction: { type: 'Reaction', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ReviewQueueItemUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - flags: { type: 'ModerationFlagResponse', isSingle: false }, - - action: { type: 'ActionLogResponse', isSingle: true }, - - review_queue_item: { type: 'ReviewQueueItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RevisionHistoryResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RingAttemptResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - expires_at: { type: 'DatetimeType', isSingle: true }, - - targets: { type: 'RingTargetResponse', isSingle: false }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - settled_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RingDeliveryResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - sent_at: { type: 'DatetimeType', isSingle: true }, - - acked_at: { type: 'DatetimeType', isSingle: true }, - - displayed_at: { type: 'DatetimeType', isSingle: true }, - - fetched_at: { type: 'DatetimeType', isSingle: true }, - - provider_accepted_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['RingTargetResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - state_at: { type: 'DatetimeType', isSingle: true }, - - deliveries: { type: 'RingDeliveryResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['Role'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SIPInboundRoutingRuleResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SIPTrunkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ScheduledMessageContentResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - pin_expires: { type: 'DatetimeType', isSingle: true }, - - shared_location: { type: 'ScheduledMessageLocation', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ScheduledMessageFailedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - scheduled_message: { type: 'ScheduledMessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ScheduledMessageLocation'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - end_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ScheduledMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - scheduled_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - message: { type: 'ScheduledMessageContentResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ScheduledMessageSentEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - scheduled_message: { type: 'ScheduledMessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SearchResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - results: { type: 'SearchResult', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['SearchResult'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'SearchResultMessage', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SearchResultMessage'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions: { type: 'ReactionResponse', isSingle: false }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_reactions: { type: 'ReactionResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - message_text_updated_at: { type: 'DatetimeType', isSingle: true }, - - pin_expires: { type: 'DatetimeType', isSingle: true }, - - pinned_at: { type: 'DatetimeType', isSingle: true }, - - mentioned_groups: { type: 'UserGroupResponse', isSingle: false }, - - thread_participants: { type: 'UserResponse', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - draft: { type: 'DraftResponse', isSingle: true }, - - pinned_by: { type: 'UserResponse', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - quoted_message: { type: 'MessageResponse', isSingle: true }, - - reaction_groups: { type: 'ReactionGroupResponse', isSingle: false }, - - reminder: { type: 'ReminderResponseData', isSingle: true }, - - shared_location: { type: 'SharedLocationResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SearchRolesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - roles: { type: 'Role', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['SearchUserGroupsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user_groups: { type: 'UserGroupResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['Segment'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SegmentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SegmentTargetResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SendMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - - channel_context: { type: 'ChannelContextResponse', isSingle: true }, - - scheduled_message: { type: 'ScheduledMessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SendReactionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - - reaction: { type: 'ReactionResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SendScheduledMessageNowResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - scheduled_message: { type: 'ScheduledMessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SessionWarningResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - time: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SetFeedsRetentionPolicyResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - policy: { type: 'FeedsRetentionPolicy', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SetRetentionPolicyResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - policy: { type: 'RetentionPolicy', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SetupSession'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - completed_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ShareResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SharedLocationResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - end_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SharedLocationResponseData'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - end_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SharedLocationsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - active_live_locations: { - type: 'SharedLocationResponseData', - isSingle: false, - }, - }; - return decode(typeMappings, input); -}; - -decoders['SingleFollowResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follow: { type: 'FollowResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['StartCampaignResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - campaign: { type: 'CampaignResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['StopCampaignResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - campaign: { type: 'CampaignResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['StopLiveResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['StoriesFeedUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - activities: { type: 'ActivityResponse', isSingle: false }, - - aggregated_activities: { - type: 'AggregatedActivityResponse', - isSingle: false, - }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['SubmitActionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - appeal_item: { type: 'AppealItemResponse', isSingle: true }, - - item: { type: 'ReviewQueueItemResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ThreadParticipant'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - last_read_at: { type: 'DatetimeType', isSingle: true }, - - last_thread_message_at: { type: 'DatetimeType', isSingle: true }, - - left_thread_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ThreadResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - last_message_at: { type: 'DatetimeType', isSingle: true }, - - thread_participants: { type: 'ThreadParticipant', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - parent_message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ThreadStateResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - latest_replies: { type: 'MessageResponse', isSingle: false }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - last_message_at: { type: 'DatetimeType', isSingle: true }, - - read: { type: 'ReadStateResponse', isSingle: false }, - - thread_participants: { type: 'ThreadParticipant', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - draft: { type: 'DraftResponse', isSingle: true }, - - parent_message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ThreadUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - thread: { type: 'ThreadResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['ThreadedCommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - mentioned_users: { type: 'UserResponse', isSingle: false }, - - own_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - user: { type: 'UserResponse', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - edited_at: { type: 'DatetimeType', isSingle: true }, - - latest_reactions: { type: 'FeedsReactionResponse', isSingle: false }, - - replies: { type: 'ThreadedCommentResponse', isSingle: false }, - - reaction_groups: { type: 'FeedsReactionGroupResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['TranslateActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['TranslateCommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comment: { type: 'CommentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['TranslateMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['TruncateChannelResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UnblockedUserEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UndeleteMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UnfollowBatchResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follows: { type: 'FollowResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UnfollowResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follow: { type: 'FollowResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UnpinActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UnreadCountsBatchResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - counts_by_user: { type: 'UnreadCountsResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UnreadCountsChannel'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - last_read: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UnreadCountsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channels: { type: 'UnreadCountsChannel', isSingle: false }, - - threads: { type: 'UnreadCountsThread', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UnreadCountsThread'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - last_read: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateActivitiesPartialBatchResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - activities: { type: 'ActivityResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateActivityPartialResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateActivityResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activity: { type: 'ActivityResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateBlockListResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - blocklist: { type: 'BlockListResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateBookmarkFolderResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateBookmarkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - bookmark: { type: 'BookmarkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCallMembersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'MemberResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCallResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'MemberResponse', isSingle: false }, - - call: { type: 'CallResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCallTypeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCampaignResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - campaign: { type: 'CampaignResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateChannelPartialResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'ChannelMemberResponse', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateChannelResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - members: { type: 'ChannelMemberResponse', isSingle: false }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateChannelTypeResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCollectionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - collections: { type: 'CollectionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCommandResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - command: { type: 'Command', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCommentBookmarkResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - bookmark: { type: 'BookmarkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCommentPartialResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comment: { type: 'CommentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateCommentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - comment: { type: 'CommentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateFeedGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_group: { type: 'FeedGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateFeedMembersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - added: { type: 'FeedMemberResponse', isSingle: false }, - - updated: { type: 'FeedMemberResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateFeedResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed: { type: 'FeedResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateFeedViewResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - feed_view: { type: 'FeedViewResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateFollowResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - follow: { type: 'FollowResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateMemberPartialResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channel_member: { type: 'ChannelMemberResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateMembershipLevelResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - membership_level: { type: 'MembershipLevelResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateMessagePartialResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateMessageResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - message: { type: 'MessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdatePredefinedFilterResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - predefined_filter: { type: 'PredefinedFilterResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateReminderResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - reminder: { type: 'ReminderResponseData', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateSIPInboundRoutingRuleResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - sip_inbound_routing_rule: { - type: 'SIPInboundRoutingRuleResponse', - isSingle: true, - }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateSIPTrunkResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - sip_trunk: { type: 'SIPTrunkResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateScheduledMessageResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - scheduled_message: { type: 'ScheduledMessageResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateSegmentResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - segment: { type: 'SegmentResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateThreadPartialResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - thread: { type: 'ThreadResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateUserGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - user_group: { type: 'UserGroupResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdateUsersResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - users: { type: 'FullUserResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UpdatedCallPermissionsEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertActivitiesResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - activities: { type: 'ActivityResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertCollectionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - collections: { type: 'CollectionResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertConfigResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - config: { type: 'ConfigResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertModerationRuleResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - rule: { type: 'ModerationRuleV2Response', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertModerationTemplateResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertPushPreferencesResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - user_preferences: { type: 'PushPreferencesResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertPushProviderResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - push_provider: { type: 'PushProviderResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertPushTemplateResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - template: { type: 'PushTemplateResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UpsertSetupSessionResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - setup_session: { type: 'SetupSession', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserBannedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - expiration: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserDeactivatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserFlaggedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - target_user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserGroupCreatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserGroupDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserGroupMember'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserGroupMemberAddedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserGroupMemberRemovedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserGroupResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - members: { type: 'UserGroupMember', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['UserGroupUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserMessagesDeletedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserMuteResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - expires: { type: 'DatetimeType', isSingle: true }, - - target: { type: 'UserResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserMutedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - target_users: { type: 'UserResponseCommonFields', isSingle: false }, - - target_user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserReactivatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - ban_expires: { type: 'DatetimeType', isSingle: true }, - - deactivated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - last_active: { type: 'DatetimeType', isSingle: true }, - - revoke_tokens_issued_before: { type: 'DatetimeType', isSingle: true }, - - devices: { type: 'DeviceResponse', isSingle: false }, - - push_notifications: { - type: 'PushNotificationSettingsResponse', - isSingle: true, - }, - }; - return decode(typeMappings, input); -}; - -decoders['UserResponseCommonFields'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - updated_at: { type: 'DatetimeType', isSingle: true }, - - deactivated_at: { type: 'DatetimeType', isSingle: true }, - - deleted_at: { type: 'DatetimeType', isSingle: true }, - - last_active: { type: 'DatetimeType', isSingle: true }, - - revoke_tokens_issued_before: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserUnbannedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - created_by: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserUnmutedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - - target_users: { type: 'UserResponseCommonFields', isSingle: false }, - - target_user: { type: 'UserResponseCommonFields', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserUnreadReminderEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - user: { type: 'UserResponseCommonFields', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['UserUpdatedEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - received_at: { type: 'DatetimeType', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['VideoReactionOverTimeResponse'] = (input?: { - [key: string]: any; -}) => { - const typeMappings: TypeMapping = { - by_minute: { type: 'CountByMinuteResponse', isSingle: false }, - }; - return decode(typeMappings, input); -}; - -decoders['VideoReactionsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - count_over_time: { type: 'VideoReactionOverTimeResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['WSEvent'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - created_at: { type: 'DatetimeType', isSingle: true }, - - channel_last_message_at: { type: 'DatetimeType', isSingle: true }, - - channel: { type: 'ChannelResponse', isSingle: true }, - - created_by: { type: 'UserResponse', isSingle: true }, - - me: { type: 'OwnUserResponse', isSingle: true }, - - member: { type: 'ChannelMemberResponse', isSingle: true }, - - message: { type: 'MessageResponse', isSingle: true }, - - poll: { type: 'PollResponseData', isSingle: true }, - - poll_vote: { type: 'PollVoteResponseData', isSingle: true }, - - reaction: { type: 'ReactionResponse', isSingle: true }, - - thread: { type: 'ThreadResponse', isSingle: true }, - - user: { type: 'UserResponse', isSingle: true }, - }; - return decode(typeMappings, input); -}; - -decoders['WrappedUnreadCountsResponse'] = (input?: { [key: string]: any }) => { - const typeMappings: TypeMapping = { - channels: { type: 'UnreadCountsChannel', isSingle: false }, - - threads: { type: 'UnreadCountsThread', isSingle: false }, + app: { type: 'AppResponseFields', isSingle: true }, }; return decode(typeMappings, input); }; diff --git a/src/gen/models/index.ts b/src/gen/models/index.ts index 753382ed..ac7bf548 100644 --- a/src/gen/models/index.ts +++ b/src/gen/models/index.ts @@ -1,3 +1,29 @@ +declare const timestampNsBrand: unique symbol; + +/** + * A unix-nanosecond timestamp, exactly as the server sends it. + * + * NOT milliseconds. `new Date(t)` is out of range and yields an Invalid Date, whose + * `.toISOString()` then throws; `models/timestamp-guard.ts` turns that into a + * compile error. Comparing or sorting two timestamps is fine -- same unit, plain + * numbers. Everything else should go through this SDK's time helpers: + * + * nsToDate(t) a Date + * convertTimestampToDate(t) a Date, or undefined when the value is absent or NaN + * nsToMs(t) epoch ms, for arithmetic against Date.now() + * nsToRfc3339(t) an RFC3339 string keeping sub-millisecond precision + * dateToNs(d), msToNs(ms) back to a wire timestamp + * nowNs() the local clock, wire-comparable + * asTimestampNS(n) brand a number already in ns (DB rows, fixtures) -- no conversion + * + * A request date field is typed `Date`, not this -- pass `nsToDate(t)` when handing a + * server-sent timestamp back to the API, or `nsToRfc3339(t)` where precision matters. + * + * Values above `Number.MAX_SAFE_INTEGER` are quantised to ~256ns, so ordering holds + * but exact equality after a JSON round-trip does not. + */ +export type TimestampNS = number & { readonly [timestampNsBrand]: true }; + export interface AIAudioConfigRequest { /** * Bodyguard credentials profile to classify transcripts with. Applies to the NLP engine only; ignored when LLM configurability is enabled. Empty falls back to the app-pinned profile, then the organization's top-level credentials @@ -152,8 +178,10 @@ export interface APNSPayload { } export interface AWSRekognitionRule { - action: - 'flag' | 'shadow' | 'remove' | 'bounce' | 'bounce_flag' | 'bounce_remove'; + /** + * What a moderation rule does when it matches. + */ + action: ModerationAction; label: string; min_confidence: number; subclassifications?: Record; @@ -209,7 +237,7 @@ export interface ActionLogResponse { /** * Timestamp when the action was taken */ - created_at: Date; + created_at: TimestampNS; /** * Unique identifier of the action log */ @@ -299,7 +327,7 @@ export interface ActivityAddedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; custom: Record; @@ -307,8 +335,11 @@ export interface ActivityAddedEvent { * The type of event: "feeds.activity.added" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -316,7 +347,7 @@ export interface ActivityDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; custom: Record; @@ -324,8 +355,11 @@ export interface ActivityDeletedEvent { * The type of event: "feeds.activity.deleted" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -333,14 +367,14 @@ export interface ActivityFeedbackEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; activity_feedback: ActivityFeedbackEventPayload; custom: Record; /** * The type of event: "feeds.activity.feedback" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -356,11 +390,11 @@ export interface ActivityFeedbackEventPayload { /** * When the feedback was created */ - created_at: Date; + created_at: TimestampNS; /** * When the feedback was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * The feedback value (true/false) */ @@ -410,14 +444,17 @@ export interface ActivityMarkEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; /** * The type of event: "feeds.activity.marked" in this case */ type: string; - feed_visibility?: string; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; /** * Whether all activities were marked as read */ @@ -426,7 +463,7 @@ export interface ActivityMarkEvent { * Whether all activities were marked as seen */ mark_all_seen?: boolean; - received_at?: Date; + received_at?: TimestampNS; /** * The IDs of activities marked as read */ @@ -457,7 +494,7 @@ export interface ActivityPinResponse { /** * When the pin was created */ - created_at: Date; + created_at: TimestampNS; /** * ID of the feed where activity is pinned */ @@ -465,7 +502,7 @@ export interface ActivityPinResponse { /** * When the pin was last updated */ - updated_at: Date; + updated_at: TimestampNS; activity: ActivityResponse; /** * User response object @@ -477,7 +514,7 @@ export interface ActivityPinnedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the feed */ @@ -488,8 +525,11 @@ export interface ActivityPinnedEvent { * The type of event: "feeds.activity.pinned" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -527,7 +567,7 @@ export interface ActivityReactionAddedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; custom: Record; @@ -536,8 +576,11 @@ export interface ActivityReactionAddedEvent { * The type of event: "feeds.activity.reaction.added" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -545,7 +588,7 @@ export interface ActivityReactionDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; custom: Record; @@ -554,8 +597,11 @@ export interface ActivityReactionDeletedEvent { * The type of the reaction that was removed */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -563,7 +609,7 @@ export interface ActivityReactionUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; custom: Record; @@ -572,8 +618,11 @@ export interface ActivityReactionUpdatedEvent { * The type of event: "feeds.activity.reaction.updated" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -581,7 +630,7 @@ export interface ActivityRemovedFromFeedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; custom: Record; @@ -589,8 +638,11 @@ export interface ActivityRemovedFromFeedEvent { * The type of event: "feeds.activity.removed_from_feed" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -631,7 +683,7 @@ export interface ActivityRequest { /** * Controls who can add comments/replies to this activity. One of: everyone, people_i_follow, nobody */ - restrict_replies?: 'everyone' | 'people_i_follow' | 'nobody'; + restrict_replies?: RestrictReplies; /** * Whether to skip URL enrichment for the activity */ @@ -651,7 +703,7 @@ export interface ActivityRequest { /** * Visibility setting for the activity. One of: public, private, tag */ - visibility?: 'public' | 'private' | 'tag'; + visibility?: ActivityVisibility; /** * If visibility is 'tag', this is the tag name and is required */ @@ -703,7 +755,7 @@ export interface ActivityResponse { /** * When the activity was created */ - created_at: Date; + created_at: TimestampNS; /** * If this activity is hidden by this user (using activity feedback) */ @@ -727,7 +779,7 @@ export interface ActivityResponse { /** * Controls who can add comments/replies to this activity. One of: everyone, people_i_follow, nobody */ - restrict_replies: 'everyone' | 'people_i_follow' | 'nobody'; + restrict_replies: RestrictReplies; /** * Ranking score for this activity */ @@ -743,11 +795,11 @@ export interface ActivityResponse { /** * When the activity was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Visibility setting for the activity. One of: public, private, tag */ - visibility: 'public' | 'private' | 'tag'; + visibility: ActivityVisibility; /** * Media attachments for the activity */ @@ -807,15 +859,15 @@ export interface ActivityResponse { /** * When the activity was deleted */ - deleted_at?: Date; + deleted_at?: TimestampNS; /** * When the activity was last edited */ - edited_at?: Date; + edited_at?: TimestampNS; /** * When the activity will expire */ - expires_at?: Date; + expires_at?: TimestampNS; /** * Total count of reactions from friends on this activity */ @@ -872,7 +924,7 @@ export interface ActivityRestoredEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; custom: Record; @@ -880,8 +932,11 @@ export interface ActivityRestoredEvent { * The type of the event */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -889,14 +944,7 @@ export interface ActivitySelectorConfig { /** * Type of selector. One of: popular, proximity, following, current_feed, query, interest, follow_suggestion */ - type: - | 'popular' - | 'proximity' - | 'following' - | 'current_feed' - | 'query' - | 'interest' - | 'follow_suggestion'; + type: ActivitySelectorType; /** * Time threshold for activity selection (string). Expected RFC3339 format (e.g., 2006-01-02T15:04:05Z07:00). Cannot be used together with cutoff_window */ @@ -917,6 +965,10 @@ export interface ActivitySelectorConfig { * Maximum time in milliseconds this selector may take (25-5000). A selector that runs over its budget is dropped from that read instead of failing it, and stays out for the rest of that pagination session. The budget can only tighten the default, never extend it. Omit to use the default. Only supported on the popular, proximity, interest, query and follow_suggestion selectors */ time_budget_ms?: number; + /** + * Radius in kilometers the proximity selector searches around the viewer's location (the request's viewer_location, else the user's custom.location) when its filter has no near or within_bounds. Greater than 0 and at most 250. Omit to use the default (10). Only supported on the proximity selector + */ + viewer_location_radius_km?: number; /** * Sort parameters for activity selection */ @@ -933,18 +985,11 @@ export interface ActivitySelectorConfigResponse { /** * Type of selector */ - type: - | 'popular' - | 'proximity' - | 'following' - | 'current_feed' - | 'query' - | 'interest' - | 'follow_suggestion'; + type: ActivitySelectorType; /** * Time threshold for activity selection (timestamp) */ - cutoff_time?: Date; + cutoff_time?: TimestampNS; /** * Flexible relative time window for activity selection (e.g., '1h', '3d', '1y') */ @@ -961,6 +1006,10 @@ export interface ActivitySelectorConfigResponse { * Maximum time in milliseconds this selector may take before it is dropped from the read */ time_budget_ms?: number; + /** + * Radius in kilometers the proximity selector searches around the viewer's location + */ + viewer_location_radius_km?: number; /** * Sort parameters for activity selection */ @@ -976,11 +1025,20 @@ export interface ActivitySelectorConfigResponse { params?: Record; } +export type ActivitySelectorType = + | 'popular' + | 'proximity' + | 'following' + | 'current_feed' + | 'query' + | 'interest' + | 'follow_suggestion'; + export interface ActivityUnpinnedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the feed */ @@ -991,8 +1049,11 @@ export interface ActivityUnpinnedEvent { * The type of event: "feeds.activity.unpinned" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -1000,7 +1061,7 @@ export interface ActivityUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; custom: Record; @@ -1008,11 +1069,16 @@ export interface ActivityUpdatedEvent { * The type of the event */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } +export type ActivityVisibility = 'public' | 'private' | 'tag'; + export interface AddActivityRequest { /** * Type of activity @@ -1053,7 +1119,7 @@ export interface AddActivityRequest { /** * Controls who can add comments/replies to this activity. One of: everyone, people_i_follow, nobody */ - restrict_replies?: 'everyone' | 'people_i_follow' | 'nobody'; + restrict_replies?: RestrictReplies; /** * Whether to skip URL enrichment for the activity */ @@ -1074,7 +1140,7 @@ export interface AddActivityRequest { /** * Visibility setting for the activity. One of: public, private, tag */ - visibility?: 'public' | 'private' | 'tag'; + visibility?: ActivityVisibility; /** * If visibility is 'tag', this is the tag name and is required */ @@ -1439,7 +1505,7 @@ export interface AggregatedActivityResponse { /** * When the aggregation was created */ - created_at: Date; + created_at: TimestampNS; /** * Grouping identifier */ @@ -1451,7 +1517,7 @@ export interface AggregatedActivityResponse { /** * When the aggregation was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Number of unique users in this aggregation */ @@ -1515,6 +1581,10 @@ export interface AnalyzeImageField { * Hierarchical list of L1 (parent) classifications. Each entry: `name`, `confidence` (0–1), and nested `subclassifications` (L2 leaves with their own confidence). Resolved against the app's effective taxonomy (custom taxonomy when configured, otherwise the standard Bodyguard catalogue). */ classifications?: Array; + /** + * URLs found in the image, as `{url, source}` objects: `source` is `qr` (decoded from a QR code) or `ocr` (in text visible in the image). Independent of the action; omitted when none were found. + */ + detected_urls?: Array; /** * Flat list of Bodyguard OCR text-moderation labels on the image's extracted text (e.g. VULGARITY, PII). Each entry: `name` + `severity`. Populated when BG's OCR pipeline returned non-empty results for this image. */ @@ -1535,7 +1605,8 @@ export interface AnalyzeRequest { */ content_published_at?: Date; /** - * ISO 3166-1 alpha-2 country the content is aimed at (e.g. US, DE). Forwarded to the AI text provider as country context so it can resolve words whose meaning changes between countries. + * @deprecated + * ISO 3166-1 alpha-2 country the content is aimed at (e.g. US, DE). Ignored when country_codes is set, but an invalid value is still rejected with 400. */ country_code?: string; /** @@ -1551,6 +1622,10 @@ export interface AnalyzeRequest { */ entity_type?: string; user_id?: string; + /** + * ISO 3166-1 alpha-2 countries the content is aimed at (e.g. US, DE), up to 300. The first is the primary country, which AI text providers receive as country context. Takes precedence over country_code. + */ + country_codes?: Array; /** * Optional map from a content label (either a `texts` key or an `image:` multipart label) to a caller-supplied per-instance identifier. Echoed on per-field verdicts and surfaced in `matched_contents` when an aggregation rule fires. */ @@ -1618,6 +1693,34 @@ export interface AnalyzeTextField { * Flat list of detected Bodyguard text labels (e.g. INSULT, VULGARITY). Each entry carries `name` and `severity`. */ classifications?: Array; + /** + * URLs found in the text, as `{url, source}` objects with `source` = `text`. Independent of the action; omitted when none were found. + */ + detected_urls?: Array; +} + +export interface AppChannelCountParameters { + threshold?: number; + time_window?: string; +} + +export interface AppCountInfo { + /** + * The app-wide count condition that fired: `app_channel_count` or `app_message_count`. + */ + condition_type: string; + /** + * Channels created or messages sent in the app within the window when the rule fired, the triggering event included. + */ + count: number; + /** + * The configured threshold. The rule fires when the count reaches it. + */ + threshold: number; + /** + * The configured time window, e.g. `5m`. + */ + time_window: string; } export interface AppDefaults { @@ -1631,6 +1734,11 @@ export interface AppDefaults { query_channels: QueryChannelsDefaults; } +export interface AppMessageCountParameters { + threshold?: number; + time_window?: string; +} + export interface AppResponseFields { allow_multi_user_devices: boolean; /** @@ -1680,6 +1788,7 @@ export interface AppResponseFields { sqs_url: string; suspended: boolean; suspended_explanation: string; + two_way_block_effect_enabled: boolean; use_hook_v2: boolean; user_response_time_enabled: boolean; webhook_url: string; @@ -1702,7 +1811,7 @@ export interface AppResponseFields { chat_primary_use_case?: string; moderation_onboarding_complete?: boolean; moderation_s3_image_access_role_arn?: string; - revoke_tokens_issued_before?: Date; + revoke_tokens_issued_before?: TimestampNS; video_primary_use_case?: string; allowed_flag_reasons?: Array; geofences?: Array; @@ -1713,18 +1822,18 @@ export interface AppResponseFields { } export interface AppealAcceptedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; appeal?: AppealItemResponse; } export interface AppealCreatedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; appeal?: AppealItemResponse; } @@ -1736,7 +1845,7 @@ export interface AppealItemResponse { /** * When the flag was created */ - created_at: Date; + created_at: TimestampNS; /** * ID of the entity */ @@ -1753,7 +1862,7 @@ export interface AppealItemResponse { /** * When the flag was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Text severity level assigned by the AI provider */ @@ -1820,10 +1929,10 @@ export interface AppealItemResponse { } export interface AppealRejectedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; appeal?: AppealItemResponse; } @@ -1867,107 +1976,134 @@ export interface AppealResponse { duration: string; } +export interface ArrangeConfig { + /** + * Activity field that defines a source. One of: type, user_id + */ + key?: string; + /** + * Arrangement mode. Currently only 'blend' is supported; 'diversify' and 'slot' are reserved + */ + mode?: string; + /** + * Activities scoring strictly below this value are moved to the end of the feed, in score order, before the mode runs. 0 is a valid value. Must be greater than exclude_below when both are set + */ + tail_below?: number; + blend?: BlendArrangeConfig; +} + export interface AsyncBulkImageModerationEvent { - created_at: Date; - finished_at: Date; - started_at: Date; + created_at: TimestampNS; + finished_at: TimestampNS; + started_at: TimestampNS; task_id: string; url: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface AsyncExportChannelsEvent { - created_at: Date; - finished_at: Date; - started_at: Date; + created_at: TimestampNS; + finished_at: TimestampNS; + started_at: TimestampNS; task_id: string; url: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface AsyncExportErrorEvent { - created_at: Date; + created_at: TimestampNS; error: string; - finished_at: Date; - started_at: Date; + finished_at: TimestampNS; + started_at: TimestampNS; + task_id: string; + custom: Record; + type: string; + received_at?: TimestampNS; +} + +export interface AsyncExportFeedsSuccessEvent { + created_at: TimestampNS; + finished_at: TimestampNS; + manifest_path: string; + started_at: TimestampNS; task_id: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface AsyncExportLabelResultsEvent { - created_at: Date; - finished_at: Date; - started_at: Date; + created_at: TimestampNS; + finished_at: TimestampNS; + started_at: TimestampNS; task_id: string; url: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface AsyncExportModerationLogsEvent { - created_at: Date; - finished_at: Date; - started_at: Date; + created_at: TimestampNS; + finished_at: TimestampNS; + started_at: TimestampNS; task_id: string; url: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface AsyncExportReviewQueueEvent { - created_at: Date; - finished_at: Date; - started_at: Date; + created_at: TimestampNS; + finished_at: TimestampNS; + started_at: TimestampNS; task_id: string; url: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface AsyncExportUsersEvent { - created_at: Date; - finished_at: Date; - started_at: Date; + created_at: TimestampNS; + finished_at: TimestampNS; + started_at: TimestampNS; task_id: string; url: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface AsyncImportErrorEvent { - created_at: Date; + created_at: TimestampNS; error: string; error_code: string; - finished_at: Date; + finished_at: TimestampNS; product: string; state: string; task_id: string; custom: Record; type: string; - received_at?: Date; - started_at?: Date; + received_at?: TimestampNS; + started_at?: TimestampNS; } export interface AsyncImportSuccessEvent { - created_at: Date; - finished_at: Date; + created_at: TimestampNS; + finished_at: TimestampNS; product: string; state: string; task_id: string; custom: Record; type: string; - received_at?: Date; - started_at?: Date; + received_at?: TimestampNS; + started_at?: TimestampNS; } export interface AsyncModerationCallbackConfig { @@ -2022,9 +2158,14 @@ export interface Audience { shape?: string; } +export type AudioDefaultDevice = 'speaker' | 'earpiece'; + export interface AudioSettings { access_request_enabled: boolean; - default_device: 'speaker' | 'earpiece'; + /** + * The device a call routes audio to by default. + */ + default_device: AudioDefaultDevice; hifi_audio_enabled: boolean; mic_default_on: boolean; opus_dtx_enabled: boolean; @@ -2034,7 +2175,10 @@ export interface AudioSettings { } export interface AudioSettingsRequest { - default_device: 'speaker' | 'earpiece'; + /** + * The device a call routes audio to by default. + */ + default_device: AudioDefaultDevice; access_request_enabled?: boolean; hifi_audio_enabled?: boolean; mic_default_on?: boolean; @@ -2046,7 +2190,10 @@ export interface AudioSettingsRequest { export interface AudioSettingsResponse { access_request_enabled: boolean; - default_device: 'speaker' | 'earpiece'; + /** + * The device a call routes audio to by default. + */ + default_device: AudioDefaultDevice; hifi_audio_enabled: boolean; mic_default_on: boolean; opus_dtx_enabled: boolean; @@ -2066,6 +2213,8 @@ export interface AutomodDetailsResponse { result?: MessageModerationResult; } +export type AutomodMode = 'disabled' | 'simple' | 'AI'; + export interface AutomodPlatformCircumventionConfig { async?: boolean; enabled?: boolean; @@ -2073,8 +2222,10 @@ export interface AutomodPlatformCircumventionConfig { } export interface AutomodRule { - action: - 'flag' | 'shadow' | 'remove' | 'bounce' | 'bounce_flag' | 'bounce_remove'; + /** + * What a moderation rule does when it matches. + */ + action: ModerationAction; label: string; threshold: number; } @@ -2086,8 +2237,10 @@ export interface AutomodSemanticFiltersConfig { } export interface AutomodSemanticFiltersRule { - action: - 'flag' | 'shadow' | 'remove' | 'bounce' | 'bounce_flag' | 'bounce_remove'; + /** + * What a moderation rule does when it matches. + */ + action: ModerationAction; name: string; threshold: number; } @@ -2145,7 +2298,7 @@ export interface BanActionRequestPayload { /** * Message deletion mode: soft, pruning, or hard */ - delete_messages?: 'soft' | 'pruning' | 'hard'; + delete_messages?: DeleteMessages; /** * Whether to ban by IP address */ @@ -2172,7 +2325,7 @@ export interface BanInfoResponse { /** * When the ban was created */ - created_at: Date; + created_at: TimestampNS; /** * The channel this ban applies to. Empty if this is an app-wide (global) ban rather than a per-channel ban. */ @@ -2180,7 +2333,7 @@ export interface BanInfoResponse { /** * When the ban expires */ - expires?: Date; + expires?: TimestampNS; /** * Reason for the ban */ @@ -2201,7 +2354,10 @@ export interface BanInfoResponse { } export interface BanOptions { - delete_messages?: 'soft' | 'pruning' | 'hard'; + /** + * How a ban or user deletion treats the user's messages. + */ + delete_messages?: DeleteMessages; duration?: number; ip_ban?: boolean; reason?: string; @@ -2225,7 +2381,10 @@ export interface BanRequest { * Channel where the ban applies */ channel_cid?: string; - delete_messages?: 'soft' | 'pruning' | 'hard'; + /** + * How a ban or user deletion treats the user's messages. + */ + delete_messages?: DeleteMessages; /** * Whether to delete reactions by the banned user on other users' messages */ @@ -2253,8 +2412,8 @@ export interface BanRequest { } export interface BanResponse { - created_at: Date; - expires?: Date; + created_at: TimestampNS; + expires?: TimestampNS; reason?: string; shadow?: boolean; /** @@ -2337,6 +2496,21 @@ export interface BatchQueryCommentReactionsResponse { prev?: string; } +export interface BlendArrangeConfig { + /** + * Minimum gap, 0 to 50, for every source without a min_gap entry + */ + default_min_gap?: number; + /** + * Minimum number of slots between two items of a source, per key value, 0 to 50. The key '*' requires weights['*']. Combinations of weights and gaps that cannot be satisfied are rejected + */ + min_gap?: Record; + /** + * Relative weight per key value, greater than 0, at most 8 entries. Weights are normalized, so 80/20 equals 4/1. The key '*' is the catch-all source for key values without their own weight; unweighted values are otherwise placed after the blended section + */ + weights?: Record; +} + export interface BlockActionRequestPayload { /** * Reason for blocking @@ -2355,7 +2529,7 @@ export interface BlockListOptions { /** * Blocklist behavior. One of: flag, block, shadow_block */ - behavior: 'flag' | 'block' | 'shadow_block'; + behavior: ChannelModerationBehavior; /** * Blocklist name */ @@ -2382,14 +2556,14 @@ export interface BlockListResponse { /** * Date/time of creation */ - created_at?: Date; + created_at?: TimestampNS; id?: string; owner_user_id?: string; team?: string; /** * Date/time of the last update */ - updated_at?: Date; + updated_at?: TimestampNS; } export interface BlockListRule { @@ -2444,7 +2618,7 @@ export interface BlockUsersResponse { /** * Timestamp when the user was blocked */ - created_at: Date; + created_at: TimestampNS; /** * Duration of the request in milliseconds */ @@ -2453,7 +2627,7 @@ export interface BlockUsersResponse { export interface BlockedUserEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * User response object */ @@ -2473,7 +2647,7 @@ export interface BlockedUserResponse { * ID of the user who got blocked */ blocked_user_id: string; - created_at: Date; + created_at: TimestampNS; /** * ID of the user who blocked another user */ @@ -2488,6 +2662,8 @@ export interface BlockedUserResponse { user: UserResponse; } +export type BlocklistBehavior = 'flag' | 'block'; + export interface BodyguardImageAnalysisConfig { rules?: Array; } @@ -2530,14 +2706,14 @@ export interface BookmarkAddedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; bookmark: BookmarkResponse; custom: Record; /** * The type of event: "feeds.bookmark.added" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -2545,14 +2721,14 @@ export interface BookmarkDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; bookmark: BookmarkResponse; custom: Record; /** * The type of event: "feeds.bookmark.deleted" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -2560,14 +2736,14 @@ export interface BookmarkFolderDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; bookmark_folder: BookmarkFolderResponse; custom: Record; /** * The type of event: "feeds.bookmark_folder.deleted" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -2575,7 +2751,7 @@ export interface BookmarkFolderResponse { /** * When the folder was created */ - created_at: Date; + created_at: TimestampNS; /** * Unique identifier for the folder */ @@ -2587,7 +2763,7 @@ export interface BookmarkFolderResponse { /** * When the folder was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * User response object */ @@ -2602,14 +2778,14 @@ export interface BookmarkFolderUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; bookmark_folder: BookmarkFolderResponse; custom: Record; /** * The type of event: "feeds.bookmark_folder.updated" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -2617,7 +2793,7 @@ export interface BookmarkResponse { /** * When the bookmark was created */ - created_at: Date; + created_at: TimestampNS; /** * ID of the bookmarked object */ @@ -2629,7 +2805,7 @@ export interface BookmarkResponse { /** * When the bookmark was last updated */ - updated_at: Date; + updated_at: TimestampNS; activity: ActivityResponse; /** * User response object @@ -2648,14 +2824,14 @@ export interface BookmarkUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; bookmark: BookmarkResponse; custom: Record; /** * The type of event: "feeds.bookmark.updated" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -2879,7 +3055,7 @@ export interface BypassResponse { export interface CallAcceptedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Represents a call */ @@ -2905,11 +3081,11 @@ export interface CallActionOptions { } export interface CallClosedCaption { - end_time: Date; + end_time: TimestampNS; id: string; language: string; speaker_id: string; - start_time: Date; + start_time: TimestampNS; text: string; translated: boolean; /** @@ -2921,7 +3097,7 @@ export interface CallClosedCaption { export interface CallClosedCaptionsFailedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The type of event: "call.closed_captions_failed" in this case */ @@ -2930,7 +3106,7 @@ export interface CallClosedCaptionsFailedEvent { export interface CallClosedCaptionsStartedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The type of event: "call.closed_captions_started" in this case */ @@ -2939,7 +3115,7 @@ export interface CallClosedCaptionsStartedEvent { export interface CallClosedCaptionsStoppedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The type of event: "call.transcription_stopped" in this case */ @@ -2948,7 +3124,7 @@ export interface CallClosedCaptionsStoppedEvent { export interface CallCreatedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * the members added to this call */ @@ -2970,7 +3146,7 @@ export interface CallCustomPropertyParameters { export interface CallDTMFEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The DTMF digit (0-9, *, #, A-D) */ @@ -2986,7 +3162,7 @@ export interface CallDTMFEvent { /** * When the digit press ended and was detected */ - timestamp: Date; + timestamp: TimestampNS; /** * User response object */ @@ -2999,7 +3175,7 @@ export interface CallDTMFEvent { export interface CallDeletedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Represents a call */ @@ -3020,7 +3196,7 @@ export interface CallDurationReportResponse { export interface CallEndedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Represents a call */ @@ -3045,7 +3221,7 @@ export interface CallEndedEvent { export interface CallFrameRecordingFailedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * Represents a call @@ -3062,8 +3238,8 @@ export interface CallFrameRecordingFrameReadyEvent { /** * The time the frame was captured */ - captured_at: Date; - created_at: Date; + captured_at: TimestampNS; + created_at: TimestampNS; egress_id: string; /** * Call session ID @@ -3089,7 +3265,7 @@ export interface CallFrameRecordingFrameReadyEvent { export interface CallFrameRecordingStartedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * Represents a call @@ -3103,7 +3279,7 @@ export interface CallFrameRecordingStartedEvent { export interface CallFrameRecordingStoppedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * Represents a call @@ -3117,7 +3293,7 @@ export interface CallFrameRecordingStoppedEvent { export interface CallHLSBroadcastingFailedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The type of event: "call.hls_broadcasting_failed" in this case */ @@ -3126,7 +3302,7 @@ export interface CallHLSBroadcastingFailedEvent { export interface CallHLSBroadcastingStartedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; hls_playlist_url: string; /** * Represents a call @@ -3140,7 +3316,7 @@ export interface CallHLSBroadcastingStartedEvent { export interface CallHLSBroadcastingStoppedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The type of event: "call.hls_broadcasting_stopped" in this case */ @@ -3163,9 +3339,11 @@ export interface CallLevelEventPayload { payload?: Record; } +export type CallLifecycleMode = 'reusable' | 'single_use'; + export interface CallLiveStartedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Represents a call */ @@ -3178,7 +3356,7 @@ export interface CallLiveStartedEvent { export interface CallMemberAddedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * the members added to this call */ @@ -3195,7 +3373,7 @@ export interface CallMemberAddedEvent { export interface CallMemberRemovedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * the list of member IDs removed from the call */ @@ -3212,7 +3390,7 @@ export interface CallMemberRemovedEvent { export interface CallMemberUpdatedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The list of members that were updated */ @@ -3229,7 +3407,7 @@ export interface CallMemberUpdatedEvent { export interface CallMemberUpdatedPermissionEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The list of members that were updated */ @@ -3250,7 +3428,7 @@ export interface CallMemberUpdatedPermissionEvent { export interface CallMissedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; notify_user: boolean; /** * Call session ID @@ -3276,7 +3454,7 @@ export interface CallMissedEvent { export interface CallModerationBlurEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The user ID whose video stream is being blurred */ @@ -3293,7 +3471,7 @@ export interface CallModerationBlurEvent { export interface CallModerationWarningEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The warning message */ @@ -3314,7 +3492,7 @@ export interface CallModerationWarningEvent { export interface CallNotificationEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Call session ID */ @@ -3346,7 +3524,7 @@ export interface CallParticipantCountReportResponse { } export interface CallParticipantResponse { - joined_at: Date; + joined_at: TimestampNS; role: string; user_session_id: string; /** @@ -3357,14 +3535,14 @@ export interface CallParticipantResponse { export interface CallParticipantTimeline { severity: string; - timestamp: Date; + timestamp: TimestampNS; type: string; data: Record; } export interface CallReactionEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; reaction: VideoReactionResponse; /** * The type of event: "call.reaction_new" in this case @@ -3373,22 +3551,25 @@ export interface CallReactionEvent { } export interface CallRecording { - end_time: Date; + end_time: TimestampNS; filename: string; - recording_type: string; + /** + * The layout a call recording is produced in. + */ + recording_type: RecordingType; session_id: string; - start_time: Date; + start_time: TimestampNS; url: string; } export interface CallRecordingFailedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * The type of recording */ - recording_type: 'composite' | 'individual' | 'raw'; + recording_type: RecordingType; /** * The type of event: "call.recording_failed" in this case */ @@ -3397,12 +3578,12 @@ export interface CallRecordingFailedEvent { export interface CallRecordingReadyEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * The type of recording */ - recording_type: 'composite' | 'individual' | 'raw'; + recording_type: RecordingType; /** * CallRecording represents a recording of a call. */ @@ -3415,12 +3596,12 @@ export interface CallRecordingReadyEvent { export interface CallRecordingStartedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * The type of recording */ - recording_type: 'composite' | 'individual' | 'raw'; + recording_type: RecordingType; /** * The type of event: "call.recording_started" in this case */ @@ -3429,12 +3610,12 @@ export interface CallRecordingStartedEvent { export interface CallRecordingStoppedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * The type of recording */ - recording_type: 'composite' | 'individual' | 'raw'; + recording_type: RecordingType; /** * The type of event: "call.recording_stopped" in this case */ @@ -3443,7 +3624,7 @@ export interface CallRecordingStoppedEvent { export interface CallRejectedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Represents a call */ @@ -3464,8 +3645,8 @@ export interface CallRejectedEvent { export interface CallReportResponse { score: number; - ended_at?: Date; - started_at?: Date; + ended_at?: TimestampNS; + started_at?: TimestampNS; } export interface CallRequest { @@ -3493,7 +3674,7 @@ export interface CallResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; current_session_id: string; /** * Call ID @@ -3509,7 +3690,7 @@ export interface CallResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; blocked_user_ids: Array; /** * User response object @@ -3529,7 +3710,7 @@ export interface CallResponse { /** * Date/time when the call ended */ - ended_at?: Date; + ended_at?: TimestampNS; join_ahead_time_seconds?: number; /** * 10-digit routing number for SIP routing @@ -3538,7 +3719,7 @@ export interface CallResponse { /** * Date/time when the call will start */ - starts_at?: Date; + starts_at?: TimestampNS; team?: string; session?: CallSessionResponse; thumbnails?: ThumbnailResponse; @@ -3546,7 +3727,7 @@ export interface CallResponse { export interface CallRingEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Call session ID */ @@ -3576,7 +3757,7 @@ export interface CallRingEvent { export interface CallRingSettledEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * ID of the user who rang */ @@ -3588,7 +3769,7 @@ export interface CallRingSettledEvent { /** * When the ring was sent */ - rang_at: Date; + rang_at: TimestampNS; /** * Identifies this ring of the call session, as carried on call.ring */ @@ -3604,7 +3785,7 @@ export interface CallRingSettledEvent { /** * When the outcome left ringing */ - settled_at: Date; + settled_at: TimestampNS; /** * ID of the user whose action closed the ring, or 'server' for the missed-call deadline and automatic ends */ @@ -3627,7 +3808,7 @@ export interface CallRtmpBroadcastFailedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Name of the given RTMP broadcast */ @@ -3646,7 +3827,7 @@ export interface CallRtmpBroadcastStartedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Name of the given RTMP broadcast */ @@ -3665,7 +3846,7 @@ export interface CallRtmpBroadcastStoppedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Name of the given RTMP broadcast */ @@ -3684,7 +3865,7 @@ export interface CallRuleActionSequence { export interface CallSessionEndedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Call session ID */ @@ -3702,7 +3883,7 @@ export interface CallSessionEndedEvent { export interface CallSessionParticipantCountsUpdatedEvent { anonymous_participant_count: number; call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Call session ID */ @@ -3716,7 +3897,7 @@ export interface CallSessionParticipantCountsUpdatedEvent { export interface CallSessionParticipantJoinedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Call session ID */ @@ -3730,7 +3911,7 @@ export interface CallSessionParticipantJoinedEvent { export interface CallSessionParticipantLeftEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The duration participant was in the session in seconds */ @@ -3754,20 +3935,20 @@ export interface CallSessionResponse { anonymous_participant_count: number; id: string; participants: Array; - accepted_by: Record; - missed_by: Record; + accepted_by: Record; + missed_by: Record; participants_count_by_role: Record; - rejected_by: Record; - ended_at?: Date; - live_ended_at?: Date; - live_started_at?: Date; - started_at?: Date; - timer_ends_at?: Date; + rejected_by: Record; + ended_at?: TimestampNS; + live_ended_at?: TimestampNS; + live_started_at?: TimestampNS; + started_at?: TimestampNS; + timer_ends_at?: TimestampNS; } export interface CallSessionStartedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Call session ID */ @@ -3918,7 +4099,7 @@ export interface CallStatsMapSubscribers { export interface CallStatsParticipant { user_id: string; sessions: Array; - latest_activity_at?: Date; + latest_activity_at?: TimestampNS; name?: string; roles?: Array; } @@ -3951,7 +4132,7 @@ export interface CallStatsParticipantSession { current_ip?: string; current_sfu?: string; distance_to_sfu_kilometers?: number; - ended_at?: Date; + ended_at?: TimestampNS; freezes_duration_ms?: number; ingress?: string; jitter_ms?: number; @@ -3960,7 +4141,7 @@ export interface CallStatsParticipantSession { publisher_type?: string; sdk?: string; sdk_version?: string; - started_at?: Date; + started_at?: TimestampNS; unified_session_id?: string; webrtc_version?: string; location?: CallStatsLocation; @@ -3968,7 +4149,7 @@ export interface CallStatsParticipantSession { export interface CallStatsReportReadyEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Call session ID */ @@ -3993,8 +4174,8 @@ export interface CallStatsReportSummaryResponse { call_duration_seconds: number; call_session_id: string; call_status: string; - first_stats_time: Date; - created_at?: Date; + first_stats_time: TimestampNS; + created_at?: TimestampNS; min_user_rating?: number; quality_score?: number; } @@ -4003,23 +4184,23 @@ export interface CallStatsSessionResponse { call_id: string; call_session_id: string; call_type: string; - generated_at: Date; + generated_at: TimestampNS; counts: CallStatsParticipantCounts; - call_ended_at?: Date; - call_started_at?: Date; + call_ended_at?: TimestampNS; + call_started_at?: TimestampNS; } export interface CallTranscription { - end_time: Date; + end_time: TimestampNS; filename: string; session_id: string; - start_time: Date; + start_time: TimestampNS; url: string; } export interface CallTranscriptionFailedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * The type of event: "call.transcription_failed" in this case @@ -4033,7 +4214,7 @@ export interface CallTranscriptionFailedEvent { export interface CallTranscriptionReadyEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * CallTranscription represents a transcription of a call. @@ -4047,7 +4228,7 @@ export interface CallTranscriptionReadyEvent { export interface CallTranscriptionStartedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * The type of event: "call.transcription_started" in this case @@ -4057,7 +4238,7 @@ export interface CallTranscriptionStartedEvent { export interface CallTranscriptionStoppedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; egress_id: string; /** * The type of event: "call.transcription_stopped" in this case @@ -4067,11 +4248,11 @@ export interface CallTranscriptionStoppedEvent { export interface CallType { app: number; - created_at: Date; + created_at: TimestampNS; id: number; name: string; recording_external_storage: string; - updated_at: Date; + updated_at: TimestampNS; notification_settings?: NotificationSettings; settings?: CallSettings; } @@ -4080,7 +4261,7 @@ export interface CallTypeResponse { /** * the time the call type was created */ - created_at: Date; + created_at: TimestampNS; /** * the name of the call type */ @@ -4088,7 +4269,7 @@ export interface CallTypeResponse { /** * the time the call type was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * the permissions granted to each role */ @@ -4107,7 +4288,7 @@ export interface CallTypeRuleParameters { export interface CallUpdatedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Represents a call */ @@ -4124,7 +4305,7 @@ export interface CallUpdatedEvent { export interface CallUserFeedbackSubmittedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The rating given by the user (1-5) */ @@ -4155,7 +4336,7 @@ export interface CallUserFeedbackSubmittedEvent { export interface CallUserMutedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; from_user_id: string; reason: string; muted_user_ids: Array; @@ -4178,6 +4359,8 @@ export interface CallsPerDayReportResponse { daily: Array; } +export type CameraFacing = 'front' | 'back' | 'external'; + export interface CampaignChannelMember { user_id: string; channel_role?: string; @@ -4194,10 +4377,10 @@ export interface CampaignChannelTemplate { } export interface CampaignCompletedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; campaign?: CampaignResponse; } @@ -4211,25 +4394,31 @@ export interface CampaignMessageTemplate { export interface CampaignResponse { create_channels: boolean; - created_at: Date; + created_at: TimestampNS; description: string; id: string; name: string; sender_id: string; - sender_mode: string; - sender_visibility: string; show_channels: boolean; skip_push: boolean; skip_webhook: boolean; status: string; - updated_at: Date; + updated_at: TimestampNS; segment_ids: Array; segments: Array; user_ids: Array; users: Array; stats: CampaignStatsResponse; - scheduled_for?: Date; - stop_at?: Date; + scheduled_for?: TimestampNS; + /** + * Whether a campaign includes or excludes its sender. + */ + sender_mode?: CampaignSenderMode; + /** + * How a campaign appears to its sender. + */ + sender_visibility?: CampaignSenderVisibility; + stop_at?: TimestampNS; channel_template?: CampaignChannelTemplate; message_template?: CampaignMessageTemplate; /** @@ -4238,24 +4427,32 @@ export interface CampaignResponse { sender?: UserResponse; } +export type CampaignSenderMode = 'exclude' | 'include'; + +export type CampaignSenderVisibility = 'hidden' | 'archived'; + export interface CampaignStartedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; campaign?: CampaignResponse; } export interface CampaignStatsResponse { progress: number; stats_channels_created: number; - stats_completed_at: Date; + stats_completed_at: TimestampNS; stats_messages_sent: number; - stats_started_at: Date; + stats_started_at: TimestampNS; stats_users_read: number; stats_users_sent: number; } +export interface CancelExportFeedsResponse { + duration: string; +} + export interface CancelImportV2TaskResponse { /** * Duration of the request in milliseconds @@ -4284,7 +4481,7 @@ export interface ChangeFeedVisibilityRequest { /** * Feed visibility level: public, visible, followers, members, or private */ - visibility: 'public' | 'visible' | 'followers' | 'members' | 'private'; + visibility: FeedVisibility; /** * What to do with existing pending follows when loosening visibility from 'followers': auto_approve (default) or reject */ @@ -4297,7 +4494,7 @@ export interface ChangeFeedVisibilityResponse { } export interface ChannelBanResponse { - created_at: Date; + created_at: TimestampNS; scopes: Array; channel_ban?: ChannelBanScopeResponse; future_channel_ban?: ChannelBanScopeResponse; @@ -4308,9 +4505,9 @@ export interface ChannelBanResponse { } export interface ChannelBanScopeResponse { - created_at: Date; + created_at: TimestampNS; shadow: boolean; - expires?: Date; + expires?: TimestampNS; reason?: string; /** * User response object @@ -4324,9 +4521,9 @@ export interface ChannelBansScopeTotals { } export interface ChannelBatchCompletedEvent { - batch_created_at: Date; - created_at: Date; - finished_at: Date; + batch_created_at: TimestampNS; + created_at: TimestampNS; + finished_at: TimestampNS; operation: string; status: string; success_channels_count: number; @@ -4334,7 +4531,7 @@ export interface ChannelBatchCompletedEvent { failed_channels: Array; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ChannelBatchMemberRequest { @@ -4343,9 +4540,9 @@ export interface ChannelBatchMemberRequest { } export interface ChannelBatchStartedEvent { - batch_created_at: Date; - created_at: Date; - finished_at: Date; + batch_created_at: TimestampNS; + created_at: TimestampNS; + finished_at: TimestampNS; operation: string; status: string; success_channels_count: number; @@ -4353,7 +4550,7 @@ export interface ChannelBatchStartedEvent { failed_channels: Array; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ChannelBatchUpdateRequest { @@ -4380,7 +4577,7 @@ export interface ChannelBatchUpdateRequest { */ hide_history_before?: Date; /** - * For updateData only. Requires a root cids $eq or $in filter with at most 100 CIDs and no root $or/$and. Split larger selections into requests of at most 100 CIDs. A success_channels_count response means the database update completed; a task_id response means it was queued and must be polled, including on older API nodes. + * For updateData and hide only. Requires a root cids $eq or $in filter with at most 100 CIDs and no root $or/$and. A synchronous hide also requires 1 to 10 distinct member user IDs. Split larger selections into requests of at most 100 CIDs. A success_channels_count response means the database update completed; a task_id response means it was queued and must be polled, including on older API nodes. */ synchronous?: boolean; /** @@ -4398,7 +4595,7 @@ export interface ChannelBatchUpdateRequest { export interface ChannelBatchUpdateResponse { duration: string; /** - * Positive count of channels selected for a completed synchronous database update, not an affected-row count. Concurrent deletion may reduce the rows written. task_id is absent. + * Count of channels selected for a completed synchronous database update, not an affected-row count. Concurrent deletion may reduce the rows written. Omitted when the count is 0, which happens for a synchronous hide where none of the member users exist. task_id is absent. */ success_channels_count?: number; /** @@ -4408,11 +4605,17 @@ export interface ChannelBatchUpdateResponse { } export interface ChannelConfig { - automod: 'disabled' | 'simple' | 'AI'; - automod_behavior: 'flag' | 'block' | 'shadow_block'; + /** + * Automod level applied to a channel type. + */ + automod: AutomodMode; + /** + * What automod or a blocklist does when it matches. + */ + automod_behavior: ChannelModerationBehavior; connect_events: boolean; count_messages: boolean; - created_at: Date; + created_at: TimestampNS; custom_events: boolean; delivery_events: boolean; mark_messages_pending: boolean; @@ -4431,7 +4634,7 @@ export interface ChannelConfig { shared_locations: boolean; skip_last_msg_update_for_system_msgs: boolean; typing_events: boolean; - updated_at: Date; + updated_at: TimestampNS; uploads: boolean; url_enrichment: boolean; user_message_reminders: boolean; @@ -4440,13 +4643,16 @@ export interface ChannelConfig { */ commands: Array; blocklist?: string; - blocklist_behavior?: 'flag' | 'block' | 'shadow_block'; + /** + * What automod or a blocklist does when it matches. + */ + blocklist_behavior?: ChannelModerationBehavior; partition_size?: number; partition_ttl?: string; /** * Sets the push notification level for a channel type */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; allowed_flag_reasons?: Array; blocklists?: Array; /** @@ -4458,7 +4664,10 @@ export interface ChannelConfig { export interface ChannelConfigOverrides { blocklist?: string; - blocklist_behavior?: 'flag' | 'block'; + /** + * What a blocklist does when it matches. shadow_block is not accepted on a channel override. + */ + blocklist_behavior?: BlocklistBehavior; /** * Enable/disable message counting */ @@ -4470,7 +4679,7 @@ export interface ChannelConfigOverrides { /** * Overrides the push notification level for this channel */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; /** * Enables message quotes */ @@ -4512,11 +4721,17 @@ export interface ChannelConfigOverrides { } export interface ChannelConfigWithInfo { - automod: 'disabled' | 'simple' | 'AI'; - automod_behavior: 'flag' | 'block' | 'shadow_block'; + /** + * Automod level applied to a channel type. + */ + automod: AutomodMode; + /** + * What automod or a blocklist does when it matches. + */ + automod_behavior: ChannelModerationBehavior; connect_events: boolean; count_messages: boolean; - created_at: Date; + created_at: TimestampNS; custom_events: boolean; delivery_events: boolean; mark_messages_pending: boolean; @@ -4535,19 +4750,38 @@ export interface ChannelConfigWithInfo { shared_locations: boolean; skip_last_msg_update_for_system_msgs: boolean; typing_events: boolean; - updated_at: Date; + updated_at: TimestampNS; uploads: boolean; url_enrichment: boolean; user_message_reminders: boolean; commands: Array; blocklist?: string; - blocklist_behavior?: 'flag' | 'block' | 'shadow_block'; + /** + * What automod or a blocklist does when it matches. + */ + blocklist_behavior?: ChannelModerationBehavior; partition_size?: number; partition_ttl?: string; /** * Sets the push notification level for a channel type */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; + /** + * How far ahead, in seconds, a message can be scheduled. Present only when scheduled messages are enabled for the app. A later scheduled_at returns error code 116 (scheduled_at_too_far), whose max_lead_time_seconds carries the same value + */ + scheduled_messages_max_lead_time_seconds?: number; + /** + * Maximum number of scheduled messages (scheduled or sending) a user can have in this channel. Present only when scheduled messages are enabled for the app. Scheduling more returns error code 117 (scheduled_limit_channel) + */ + scheduled_messages_max_per_channel?: number; + /** + * Maximum number of scheduled messages (scheduled or sending) a user can have across all channels. Present only when scheduled messages are enabled for the app. Scheduling more returns error code 118 (scheduled_limit_user) + */ + scheduled_messages_max_per_user?: number; + /** + * Minimum time ahead, in seconds, a message is scheduled for. Present only when scheduled messages are enabled for the app. A future scheduled_at closer than this is moved later, to the first whole minute at least this far ahead; it is never rejected + */ + scheduled_messages_min_lead_time_seconds?: number; allowed_flag_reasons?: Array; blocklists?: Array; /** @@ -4581,7 +4815,7 @@ export interface ChannelCreatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -4608,7 +4842,7 @@ export interface ChannelCreatedEvent { * The CID of the channel which was created */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -4634,7 +4868,7 @@ export interface ChannelDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -4661,7 +4895,7 @@ export interface ChannelDeletedEvent { * The CID of the channel which was deleted */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -4690,11 +4924,121 @@ export interface ChannelExport { type?: string; } +export interface ChannelForkResponse { + /** + * CID of the fork + */ + channel_cid: string; + /** + * Number of messages copied into the fork, once completed + */ + copied_message_count: number; + /** + * Date/time of the fork + */ + created_at: TimestampNS; + /** + * CID of the channel the fork was copied from + */ + source_channel_cid: string; + /** + * ID of the message of the source channel the fork was made at + */ + source_message_id: string; + /** + * `completed` once every message is copied. `backfilling` while a background task copies the history older than the newest page; the fork is usable meanwhile, and the newest messages, which an agent reads, are already there + */ + status: string; + /** + * Maps the ID of each message the request copied, the newest page, to its ID in the fork. A fork message ID is a UUIDv5 of the fork CID and the source message ID; the fork message IDs endpoint maps any others + */ + message_id_map: Record; + /** + * ID of the task copying the older history of a backfilling fork + */ + task_id?: string; +} + +export interface ChannelForkSummary { + /** + * CID of the fork + */ + channel_cid: string; + /** + * Number of messages copied into the fork, once completed + */ + copied_message_count: number; + /** + * Date/time of the fork + */ + created_at: TimestampNS; + /** + * CID of the channel the fork was copied from + */ + source_channel_cid: string; + /** + * ID of the message of the source channel the fork was made at + */ + source_message_id: string; + /** + * `completed` once every message is copied. `backfilling` while a background task copies the history older than the newest page; the fork is usable meanwhile, and the newest messages, which an agent reads, are already there + */ + status: string; + /** + * ID of the task copying the older history of a backfilling fork + */ + task_id?: string; +} + +export interface ChannelForkedEvent { + /** + * Date/time of creation + */ + created_at: TimestampNS; + /** + * Represents channel in chat + */ + channel: ChannelResponse; + custom: Record; + /** + * A fork of an agent channel + */ + fork: ChannelForkSummary; + /** + * The type of event: "channel.forked" in this case + */ + type: string; + /** + * The ID of the channel which was forked + */ + channel_id?: string; + /** + * The number of members in the channel + */ + channel_member_count?: number; + channel_message_count?: number; + /** + * The type of the channel which was forked + */ + channel_type?: string; + /** + * The CID of the channel which was forked + */ + cid?: string; + received_at?: TimestampNS; + /** + * The team ID + */ + team?: string; + channel_custom?: Record; + user?: UserResponseCommonFields; +} + export interface ChannelFrozenEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "channel.frozen" in this case @@ -4712,7 +5056,7 @@ export interface ChannelFrozenEvent { * The CID of the channel which was frozen */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ChannelGetOrCreateRequest { @@ -4720,6 +5064,10 @@ export interface ChannelGetOrCreateRequest { * Whether this channel will be hidden for the user who created the channel or not */ hide_for_creator?: boolean; + /** + * Adds scheduled_messages to the response: the acting user's own scheduled, sending and failed messages in this channel (server-side requests name the user with data.created_by_id or data.created_by; without one the field is omitted). Absent from the response when there are more than 100 or they could not be loaded; use the scheduled messages query then + */ + include_scheduled_messages?: boolean; /** * Refresh channel state */ @@ -4743,7 +5091,7 @@ export interface ChannelHiddenEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -4770,7 +5118,7 @@ export interface ChannelHiddenEvent { * The CID of the channel which was hidden */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -4869,7 +5217,7 @@ export interface ChannelMemberResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; notifications_muted: boolean; /** * Whether member is shadow banned in this channel or not @@ -4878,31 +5226,31 @@ export interface ChannelMemberResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; user_id: string; custom: Record; - archived_at?: Date; + archived_at?: TimestampNS; /** * Expiration date of the ban */ - ban_expires?: Date; + ban_expires?: TimestampNS; /** * Whether the member's ban also applies to channels the channel's creator will create in the future (an active future channel ban by the creator targets this member) */ ban_from_future_channels?: boolean; - deleted_at?: Date; + deleted_at?: TimestampNS; /** * Expiration date of the future channel ban; absent when the future channel ban is permanent */ - future_channel_ban_expires?: Date; + future_channel_ban_expires?: TimestampNS; /** * Date when invite was accepted */ - invite_accepted_at?: Date; + invite_accepted_at?: TimestampNS; /** * Date when invite was rejected */ - invite_rejected_at?: Date; + invite_rejected_at?: TimestampNS; /** * Whether member was invited or not */ @@ -4911,7 +5259,7 @@ export interface ChannelMemberResponse { * Whether member is channel moderator or not */ is_moderator?: boolean; - pinned_at?: Date; + pinned_at?: TimestampNS; /** * Permission level of the member in the channel (DEPRECATED: use channel_role instead). One of: member, moderator, admin, owner */ @@ -4945,26 +5293,31 @@ export interface ChannelMetadata { id: string; type: string; custom: Record; - last_message_at?: Date; + last_message_at?: TimestampNS; member_count?: number; message_count?: number; - push_level?: string; + /** + * How much a channel notifies its members. + */ + push_level?: PushLevel; team?: string; } +export type ChannelModerationBehavior = 'flag' | 'block' | 'shadow_block'; + export interface ChannelMute { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * Date/time of mute expiration */ - expires?: Date; + expires?: TimestampNS; /** * Represents channel in chat */ @@ -4979,13 +5332,13 @@ export interface ChannelMutedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "channel.muted" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; /** * The mute objects */ @@ -5051,7 +5404,7 @@ export interface ChannelPushPreferencesResponse { /** * If provided the notifications will be disabled until the set date. */ - disabled_until?: Date; + disabled_until?: TimestampNS; chat_preferences?: ChatPreferencesResponse; } @@ -5063,7 +5416,7 @@ export interface ChannelResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; disabled: boolean; /** * Whether channel is frozen or not @@ -5080,7 +5433,7 @@ export interface ChannelResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * Custom data for this object */ @@ -5108,7 +5461,7 @@ export interface ChannelResponse { /** * Date/time of deletion */ - deleted_at?: Date; + deleted_at?: TimestampNS; /** * Whether this channel is hidden by current user or not */ @@ -5116,11 +5469,11 @@ export interface ChannelResponse { /** * Date since when the message history is accessible */ - hide_messages_before?: Date; + hide_messages_before?: TimestampNS; /** * Date of the last message sent */ - last_message_at?: Date; + last_message_at?: TimestampNS; /** * Number of members in the channel */ @@ -5132,7 +5485,7 @@ export interface ChannelResponse { /** * Date of mute expiration */ - mute_expires_at?: Date; + mute_expires_at?: TimestampNS; /** * Whether this channel is muted or not */ @@ -5144,7 +5497,7 @@ export interface ChannelResponse { /** * Date of the latest truncation of the channel */ - truncated_at?: Date; + truncated_at?: TimestampNS; /** * List of filter tags associated with the channel */ @@ -5168,45 +5521,112 @@ export interface ChannelResponse { truncated_by?: UserResponse; } -export interface ChannelStateResponse { - duration: string; +export interface ChannelRewindResponse { /** - * List of channel members + * Date/time of the rewind */ - members: Array; + created_at: TimestampNS; /** - * List of channel messages + * Number of messages the rewind hid that were not hidden already */ - messages: Array; + hidden_count_at_rewind: number; /** - * List of pinned messages in the channel + * ID of the rewind */ - pinned_messages: Array; - threads: Array; + id: string; /** - * Milliseconds until the current user's slow mode cool down period ends. Only set on client-side get or create channel and get channel requests while the user is under cool down + * Whether `message_id` was hidden too */ - cooldown_remaining_ms?: number; + inclusive: boolean; /** - * Whether this channel is hidden or not + * ID of the message the channel was rewound to */ - hidden?: boolean; + message_id: string; +} + +export interface ChannelRewoundEvent { /** - * Messages before this date are hidden from the user + * Date/time of creation */ - hide_messages_before?: Date; + created_at: TimestampNS; /** - * Number of channel watchers + * Represents channel in chat */ - watcher_count?: number; + channel: ChannelResponse; + custom: Record; /** - * Active live locations in the channel + * A rewind of an agent channel */ - active_live_locations?: Array; + rewind: ChannelRewindResponse; /** - * Pending messages that this user has sent + * The type of event: "channel.rewound" in this case */ - pending_messages?: Array; + type: string; + /** + * The ID of the channel which was rewound + */ + channel_id?: string; + /** + * The number of members in the channel + */ + channel_member_count?: number; + channel_message_count?: number; + /** + * The type of the channel which was rewound + */ + channel_type?: string; + /** + * The CID of the channel which was rewound + */ + cid?: string; + received_at?: TimestampNS; + /** + * The team ID + */ + team?: string; + channel_custom?: Record; + user?: UserResponseCommonFields; +} + +export interface ChannelStateResponse { + duration: string; + /** + * List of channel members + */ + members: Array; + /** + * List of channel messages + */ + messages: Array; + /** + * List of pinned messages in the channel + */ + pinned_messages: Array; + threads: Array; + /** + * Milliseconds until the current user's slow mode cool down period ends. Only set on client-side get or create channel and get channel requests while the user is under cool down + */ + cooldown_remaining_ms?: number; + /** + * Whether this channel is hidden or not + */ + hidden?: boolean; + /** + * Messages before this date are hidden from the user + */ + hide_messages_before?: TimestampNS; + /** + * Number of channel watchers + */ + watcher_count?: number; + /** + * Active live locations in the channel + */ + active_live_locations?: Array; + /** + * Pending messages that this user has sent + */ + pending_messages?: Array; /** * List of read states */ @@ -5256,7 +5676,7 @@ export interface ChannelStateResponseFields { /** * Messages before this date are hidden from the user */ - hide_messages_before?: Date; + hide_messages_before?: TimestampNS; /** * Number of channel watchers */ @@ -5297,7 +5717,7 @@ export interface ChannelTruncatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -5325,7 +5745,7 @@ export interface ChannelTruncatedEvent { */ cid?: string; message_id?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -5339,11 +5759,17 @@ export interface ChannelTruncatedEvent { } export interface ChannelTypeConfig { - automod: 'disabled' | 'simple' | 'AI'; - automod_behavior: 'flag' | 'block' | 'shadow_block'; + /** + * Automod level applied to a channel type. + */ + automod: AutomodMode; + /** + * What automod or a blocklist does when it matches. + */ + automod_behavior: ChannelModerationBehavior; connect_events: boolean; count_messages: boolean; - created_at: Date; + created_at: TimestampNS; custom_events: boolean; delivery_events: boolean; mark_messages_pending: boolean; @@ -5362,7 +5788,7 @@ export interface ChannelTypeConfig { shared_locations: boolean; skip_last_msg_update_for_system_msgs: boolean; typing_events: boolean; - updated_at: Date; + updated_at: TimestampNS; uploads: boolean; url_enrichment: boolean; user_message_reminders: boolean; @@ -5370,13 +5796,16 @@ export interface ChannelTypeConfig { permissions: Array; grants: Record>; blocklist?: string; - blocklist_behavior?: 'flag' | 'block' | 'shadow_block'; + /** + * What automod or a blocklist does when it matches. + */ + blocklist_behavior?: ChannelModerationBehavior; partition_size?: number; partition_ttl?: string; /** * Sets the push notification level for a channel type */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; allowed_flag_reasons?: Array; blocklists?: Array; /** @@ -5390,7 +5819,7 @@ export interface ChannelUnFrozenEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "channel.unfrozen" in this case @@ -5408,20 +5837,20 @@ export interface ChannelUnFrozenEvent { * The CID of the channel which was unfrozen */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ChannelUnmutedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "channel.unmuted" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; /** * The mute objects */ @@ -5434,7 +5863,7 @@ export interface ChannelUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -5462,7 +5891,7 @@ export interface ChannelUpdatedEvent { */ cid?: string; message_id?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -5479,7 +5908,7 @@ export interface ChannelVisibleEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -5506,7 +5935,7 @@ export interface ChannelVisibleEvent { * The CID of the channel which was shown */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -5537,7 +5966,7 @@ export interface ChatDraftPayloadResponse { export interface ChatDraftResponse { channel_cid: string; - created_at: Date; + created_at: TimestampNS; message: ChatDraftPayloadResponse; parent_id?: string; parent_message?: ChatMessageResponse; @@ -5546,7 +5975,7 @@ export interface ChatDraftResponse { export interface ChatMessageResponse { cid: string; - created_at: Date; + created_at: TimestampNS; deleted_reply_count: number; html: string; id: string; @@ -5558,7 +5987,7 @@ export interface ChatMessageResponse { silent: boolean; text: string; type: string; - updated_at: Date; + updated_at: TimestampNS; attachments: Array; latest_reactions: Array; mentioned_users: Array; @@ -5572,13 +6001,13 @@ export interface ChatMessageResponse { */ user: UserResponse; command?: string; - deleted_at?: Date; + deleted_at?: TimestampNS; deleted_for_me?: boolean; - message_text_updated_at?: Date; + message_text_updated_at?: TimestampNS; mml?: string; parent_id?: string; - pin_expires?: Date; - pinned_at?: Date; + pin_expires?: TimestampNS; + pinned_at?: TimestampNS; poll_id?: string; quoted_message_id?: string; show_in_channel?: boolean; @@ -5646,14 +6075,14 @@ export interface ChatPreferencesResponse { export interface ChatReactionGroupResponse { count: number; - first_reaction_at: Date; - last_reaction_at: Date; + first_reaction_at: TimestampNS; + last_reaction_at: TimestampNS; sum_scores: number; latest_reactions_by: Array; } export interface ChatReactionGroupUserResponse { - created_at: Date; + created_at: TimestampNS; user_id: string; /** * User response object @@ -5662,11 +6091,11 @@ export interface ChatReactionGroupUserResponse { } export interface ChatReactionResponse { - created_at: Date; + created_at: TimestampNS; message_id: string; score: number; type: string; - updated_at: Date; + updated_at: TimestampNS; user_id: string; custom: Record; /** @@ -5677,11 +6106,11 @@ export interface ChatReactionResponse { export interface ChatReminderResponseData { channel_cid: string; - created_at: Date; + created_at: TimestampNS; message_id: string; - updated_at: Date; + updated_at: TimestampNS; user_id: string; - remind_at?: Date; + remind_at?: TimestampNS; message?: ChatMessageResponse; /** * User response object @@ -5691,14 +6120,14 @@ export interface ChatReminderResponseData { export interface ChatSharedLocationResponseData { channel_cid: string; - created_at: Date; + created_at: TimestampNS; created_by_device_id: string; latitude: number; longitude: number; message_id: string; - updated_at: Date; + updated_at: TimestampNS; user_id: string; - end_at?: Date; + end_at?: TimestampNS; message?: ChatMessageResponse; } @@ -5780,7 +6209,7 @@ export interface CheckPushRequest { /** * Push provider type. One of: firebase, apn, huawei, xiaomi */ - push_provider_type?: string; + push_provider_type?: PushProviderName; /** * Don't require existing devices to render templates */ @@ -5841,7 +6270,8 @@ export interface CheckRequest { */ content_published_at?: Date; /** - * ISO 3166-1 alpha-2 country the content is aimed at (e.g. US, DE), used as country context by AI text providers + * @deprecated + * ISO 3166-1 alpha-2 country the content is aimed at (e.g. US, DE). Ignored when country_codes is set, but an invalid value is still rejected with 400. */ country_code?: string; /** @@ -5849,6 +6279,10 @@ export interface CheckRequest { */ test_mode?: boolean; user_id?: string; + /** + * ISO 3166-1 alpha-2 countries the content is aimed at (e.g. US, DE), up to 300. The first is the primary country, which AI text providers receive as country context. Takes precedence over country_code. + */ + country_codes?: Array; config?: ModerationConfig; moderation_payload?: ModerationPayload; /** @@ -5979,6 +6413,28 @@ export interface CheckSQSResponse { data?: Record; } +export interface ClaimGuestUserRequest { + /** + * ID of the guest user to claim + */ + guest_user_id: string; + /** + * ID of the regular user that claims the guest and receives its channels, messages and reactions + */ + user_id: string; +} + +export interface ClaimGuestUserResponse { + /** + * Duration of the request in milliseconds + */ + duration: string; + /** + * ID of the task that moves the guest's channels, messages and reactions to the claiming user + */ + task_id: string; +} + export interface Classification { name: string; confidence?: number; @@ -6118,7 +6574,7 @@ export interface ClientOSDataResponse { export interface ClosedCaptionEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * CallClosedCaption represents a closed caption of a call. */ @@ -6184,11 +6640,11 @@ export interface CollectionResponse { /** * When the collection was created */ - created_at?: Date; + created_at?: TimestampNS; /** * When the collection was last updated */ - updated_at?: Date; + updated_at?: TimestampNS; /** * ID of the user who owns this collection */ @@ -6199,6 +6655,8 @@ export interface CollectionResponse { custom?: Record; } +export type CollectionStatus = 'ok' | 'notfound'; + export interface Command { /** * Arguments help text, shown in commands auto-completion @@ -6219,18 +6677,18 @@ export interface Command { /** * Date/time of creation */ - created_at?: Date; + created_at?: TimestampNS; /** * Date/time of the last update */ - updated_at?: Date; + updated_at?: TimestampNS; } export interface CommentAddedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; comment: CommentResponse; @@ -6239,8 +6697,11 @@ export interface CommentAddedEvent { * The type of event: "feeds.comment.added" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -6248,7 +6709,7 @@ export interface CommentDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; comment: CommentResponse; custom: Record; @@ -6256,8 +6717,11 @@ export interface CommentDeletedEvent { * The type of event: "feeds.comment.deleted" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -6265,14 +6729,14 @@ export interface CommentFeedbackEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; comment_feedback: CommentFeedbackEventPayload; custom: Record; /** * The type of event: "feeds.comment.feedback" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -6284,7 +6748,7 @@ export interface CommentFeedbackEventPayload { /** * When the feedback was created */ - created_at: Date; + created_at: TimestampNS; /** * Whether the comment is hidden for the user */ @@ -6292,7 +6756,7 @@ export interface CommentFeedbackEventPayload { /** * When the feedback was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * User response object */ @@ -6327,7 +6791,7 @@ export interface CommentReactionAddedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; comment: CommentResponse; @@ -6337,8 +6801,11 @@ export interface CommentReactionAddedEvent { * The type of event: "feeds.comment.reaction.added" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -6346,7 +6813,7 @@ export interface CommentReactionDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; comment: CommentResponse; custom: Record; @@ -6355,15 +6822,18 @@ export interface CommentReactionDeletedEvent { * The type of reaction that was removed */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; } export interface CommentReactionUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; activity: ActivityResponse; comment: CommentResponse; @@ -6373,8 +6843,11 @@ export interface CommentReactionUpdatedEvent { * The type of event: "feeds.comment.reaction.updated" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -6387,7 +6860,7 @@ export interface CommentResponse { /** * When the comment was created */ - created_at: Date; + created_at: TimestampNS; /** * Number of downvotes for this comment */ @@ -6423,11 +6896,11 @@ export interface CommentResponse { /** * Status of the comment. One of: active, deleted, removed, hidden */ - status: 'active' | 'deleted' | 'removed' | 'hidden' | 'shadow_blocked'; + status: CommentStatus; /** * When the comment was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Number of upvotes for this comment */ @@ -6451,11 +6924,11 @@ export interface CommentResponse { /** * When the comment was deleted */ - deleted_at?: Date; + deleted_at?: TimestampNS; /** * When the comment was last edited */ - edited_at?: Date; + edited_at?: TimestampNS; /** * ID of parent comment for nested replies */ @@ -6488,7 +6961,7 @@ export interface CommentRestoredEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; comment: CommentResponse; custom: Record; @@ -6496,16 +6969,22 @@ export interface CommentRestoredEvent { * The type of event: "feeds.comment.restored" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } +export type CommentStatus = + 'active' | 'deleted' | 'removed' | 'hidden' | 'shadow_blocked'; + export interface CommentUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; comment: CommentResponse; custom: Record; @@ -6513,8 +6992,11 @@ export interface CommentUpdatedEvent { * The type of event: "feeds.comment.updated" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -6538,7 +7020,7 @@ export interface ConfigOverridesRequest { /** * Blocklist behavior. One of: flag, block */ - blocklist_behavior?: 'flag' | 'block'; + blocklist_behavior?: BlocklistBehavior; /** * Enable/disable message counting */ @@ -6550,7 +7032,7 @@ export interface ConfigOverridesRequest { /** * Overrides the push notification level for this channel */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; /** * Enable/disable quotes */ @@ -6602,7 +7084,7 @@ export interface ConfigResponse { /** * When the configuration was created */ - created_at: Date; + created_at: TimestampNS; /** * Unique identifier for the moderation configuration */ @@ -6614,7 +7096,7 @@ export interface ConfigResponse { /** * When the configuration was last updated */ - updated_at: Date; + updated_at: TimestampNS; supported_video_call_harm_types: Array; /** * Configurable image moderation label definitions for dashboard rendering @@ -6678,7 +7160,7 @@ export interface CoordinatesResponse { export interface CountByMinuteResponse { count: number; - start_ts: Date; + start_ts: TimestampNS; } export interface Coverage { @@ -6748,7 +7230,7 @@ export interface CreateCallTypeResponse { /** * the time the call type was created */ - created_at: Date; + created_at: TimestampNS; duration: string; /** * the name of the call type @@ -6757,7 +7239,7 @@ export interface CreateCallTypeResponse { /** * the time the call type was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * the permissions granted to each role */ @@ -6792,11 +7274,11 @@ export interface CreateCampaignRequest { /** * The sender mode of the campaign */ - sender_mode?: 'exclude' | 'include'; + sender_mode?: CampaignSenderMode; /** * The visibility of the created channels for the sender */ - sender_visibility?: 'hidden' | 'archived'; + sender_visibility?: CampaignSenderVisibility; /** * Whether the campaign should show channels, if they are hidden */ @@ -6833,11 +7315,11 @@ export interface CreateChannelTypeRequest { /** * Automod. One of: disabled, simple, AI */ - automod: 'disabled' | 'simple' | 'AI'; + automod: AutomodMode; /** * Automod behavior. One of: flag, block */ - automod_behavior: 'flag' | 'block'; + automod_behavior: BlocklistBehavior; /** * Max message length */ @@ -6853,7 +7335,7 @@ export interface CreateChannelTypeRequest { /** * Blocklist behavior. One of: flag, block, shadow_block */ - blocklist_behavior?: 'flag' | 'block' | 'shadow_block'; + blocklist_behavior?: ChannelModerationBehavior; /** * Connect events */ @@ -6894,7 +7376,7 @@ export interface CreateChannelTypeRequest { /** * Default push notification level for the channel type. One of: all, all_mentions, mentions, direct_mentions, none */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; /** * Push notifications */ @@ -6965,11 +7447,17 @@ export interface CreateChannelTypeRequest { } export interface CreateChannelTypeResponse { - automod: 'disabled' | 'simple' | 'AI'; - automod_behavior: 'flag' | 'block' | 'shadow_block'; + /** + * Automod level applied to a channel type. + */ + automod: AutomodMode; + /** + * What automod or a blocklist does when it matches. + */ + automod_behavior: ChannelModerationBehavior; connect_events: boolean; count_messages: boolean; - created_at: Date; + created_at: TimestampNS; custom_events: boolean; delivery_events: boolean; /** @@ -6992,7 +7480,7 @@ export interface CreateChannelTypeResponse { shared_locations: boolean; skip_last_msg_update_for_system_msgs: boolean; typing_events: boolean; - updated_at: Date; + updated_at: TimestampNS; uploads: boolean; url_enrichment: boolean; user_message_reminders: boolean; @@ -7000,13 +7488,16 @@ export interface CreateChannelTypeResponse { permissions: Array; grants: Record>; blocklist?: string; - blocklist_behavior?: 'flag' | 'block' | 'shadow_block'; + /** + * What automod or a blocklist does when it matches. + */ + blocklist_behavior?: ChannelModerationBehavior; partition_size?: number; partition_ttl?: string; /** * Sets the push notification level for a channel type */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; allowed_flag_reasons?: Array; blocklists?: Array; /** @@ -7071,7 +7562,7 @@ export interface CreateDeviceRequest { /** * Push provider */ - push_provider: 'firebase' | 'apn' | 'huawei' | 'xiaomi'; + push_provider: PushProviderName; /** * Stable physical device identifier used to deduplicate pushes across push providers (e.g. APNs VoIP and Firebase on the same iOS device). Distinct from 'id', which is the push token. */ @@ -7141,8 +7632,7 @@ export interface CreateFeedGroupRequest { /** * Default visibility for the feed group, can be 'public', 'visible', 'followers', 'members', or 'private'. Defaults to 'visible' if not provided. */ - default_visibility?: - 'public' | 'visible' | 'followers' | 'members' | 'private'; + default_visibility?: FeedVisibility; /** * Configuration for activity processors */ @@ -7272,7 +7762,7 @@ export interface CreateImportV2TaskRequest { export interface CreateImportV2TaskResponse { app_pk: number; - created_at: Date; + created_at: TimestampNS; /** * Duration of the request in milliseconds */ @@ -7280,7 +7770,7 @@ export interface CreateImportV2TaskResponse { id: string; product: string; state: number; - updated_at: Date; + updated_at: TimestampNS; settings: ImportV2TaskSettings; } @@ -7435,7 +7925,7 @@ export interface CreatePollRequest { /** * Represents the visibility of votes in a poll. */ - voting_visibility?: 'anonymous' | 'public'; + voting_visibility?: VotingVisibility; options?: Array; /** * Custom data for this object @@ -7687,15 +8177,15 @@ export interface CustomCheckResponse { } export interface CustomEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface CustomVideoEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Custom data for this object */ @@ -8129,6 +8619,24 @@ export interface DeleteFeedsRetentionPolicyResponse { duration: string; } +export interface DeleteHistoryWindowTeamsRequest { + /** + * Teams to put back on the app's default history window, 1 to 10, each listed once. A team without full history is ignored + */ + teams: Array; +} + +export interface DeleteHistoryWindowTeamsResponse { + /** + * Duration of the request in milliseconds + */ + duration: string; + /** + * The teams of the request that had full history and are now back on the default window + */ + teams: Array; +} + export interface DeleteImportV2TaskResponse { /** * Duration of the request in milliseconds @@ -8166,6 +8674,8 @@ export interface DeleteMessageResponse { message: MessageResponse; } +export type DeleteMessages = 'soft' | 'pruning' | 'hard'; + export interface DeleteModerationConfigResponse { duration: string; } @@ -8260,7 +8770,10 @@ export interface DeleteReminderResponse { } export interface DeleteRetentionPolicyRequest { - policy: 'old-messages' | 'inactive-channels'; + /** + * What a retention policy applies to. + */ + policy: RetentionPolicyType; } export interface DeleteRetentionPolicyResponse { @@ -8315,7 +8828,7 @@ export interface DeleteUserMessagesRequestPayload { /** * Message deletion mode: soft, pruning, or hard */ - delete_messages: 'soft' | 'pruning' | 'hard'; + delete_messages: DeleteMessages; /** * Optional: scope deletion to a single channel (alternative to app-wide deletion) */ @@ -8447,6 +8960,17 @@ export interface DeliveryZoneSegment { watch_share_pct?: number; } +export interface DetectedURLResponse { + /** + * The URL as found. + */ + url: string; + /** + * Where it was found: `qr` (decoded from a QR code in an image), `ocr` (in text visible in an image) or `text` (in a moderated text). + */ + source?: string; +} + export interface DeviceDataResponse { name?: string; version?: string; @@ -8462,7 +8986,7 @@ export interface DeviceResponse { /** * The date when the device was created. */ - created_at: Date; + created_at: TimestampNS; /** * The device identifier. */ @@ -8470,7 +8994,7 @@ export interface DeviceResponse { /** * Push provider */ - push_provider: string; + push_provider: PushProviderName; /** * User ID */ @@ -8548,7 +9072,7 @@ export interface DraftPayloadResponse { export interface DraftResponse { channel_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Contains the draft message content */ @@ -8603,7 +9127,7 @@ export interface EgressHLSResponse { export interface EgressRTMPResponse { name: string; - started_at: Date; + started_at: TimestampNS; stream_key?: string; stream_url?: string; } @@ -8640,14 +9164,14 @@ export interface EncryptionSettingsRequest { /** * Encryption mode. One of: available, disabled, auto-on */ - mode?: 'available' | 'disabled' | 'auto-on'; + mode?: FeatureActivationMode; } export interface EncryptionSettingsResponse { /** * the resolved encryption mode for the call */ - mode: 'available' | 'disabled' | 'auto-on'; + mode: FeatureActivationMode; } export interface EndCallResponse { @@ -8685,15 +9209,15 @@ export interface EnrichedCollectionResponse { /** * Enrichment status of the collection. One of: ok, notfound */ - status: 'ok' | 'notfound'; + status: CollectionStatus; /** * When the collection was created */ - created_at?: Date; + created_at?: TimestampNS; /** * When the collection was last updated */ - updated_at?: Date; + updated_at?: TimestampNS; /** * ID of the user who owns this collection */ @@ -8813,7 +9337,7 @@ export interface EntityCreatorResponse { */ ban_count: number; banned: boolean; - created_at: Date; + created_at: TimestampNS; /** * Number of major actions performed on the user */ @@ -8828,19 +9352,19 @@ export interface EntityCreatorResponse { online: boolean; role: string; shadow_banned: boolean; - updated_at: Date; + updated_at: TimestampNS; blocked_user_ids: Array; teams: Array; custom: Record; avg_response_time?: number; - ban_expires?: Date; + ban_expires?: TimestampNS; bypass_moderation?: boolean; - deactivated_at?: Date; - deleted_at?: Date; + deactivated_at?: TimestampNS; + deleted_at?: TimestampNS; image?: string; - last_active?: Date; + last_active?: TimestampNS; name?: string; - revoke_tokens_issued_before?: Date; + revoke_tokens_issued_before?: TimestampNS; devices?: Array; /** * The privacy settings of the user. @@ -9002,6 +9526,29 @@ export interface ExportFeedUserDataResponse { task_id: string; } +export interface ExportFeedsByTypeRequest { + /** + * Only export rows created at or after this time. On reactions and follows it requires user_id; on comments soft-deleted rows are left out + */ + created_at_from?: Date; + /** + * Only export rows created before this time. On reactions and follows it requires user_id; on comments soft-deleted rows are left out + */ + created_at_to?: Date; + /** + * Only export rows of this user: activities, comments and reactions by the user; follows where the user created the source or the target feed. With user_id, soft-deleted activities and comments are left out + */ + user_id?: string; +} + +export interface ExportFeedsResponse { + duration: string; + /** + * The task ID for the export task + */ + task_id: string; +} + export interface ExportUserResponse { /** * Duration of the request in milliseconds @@ -9053,11 +9600,13 @@ export interface FailedChannelUpdates { cids: Array; } +export type FeatureActivationMode = 'available' | 'disabled' | 'auto-on'; + export interface FeedCreatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; members: Array; custom: Record; @@ -9067,39 +9616,48 @@ export interface FeedCreatedEvent { * The type of event: "feeds.feed.created" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; } export interface FeedDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; /** * The type of event: "feeds.feed.deleted" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } export interface FeedGroup { aggregation_version: number; app_pk: number; - created_at: Date; + created_at: TimestampNS; default_follower_role: string; - default_visibility: string; + /** + * Who can see and follow a feed. + */ + default_visibility: FeedVisibility; group_id: string; - updated_at: Date; + updated_at: TimestampNS; activity_processors: Array; activity_selectors: Array; custom: Record; - deleted_at?: Date; - last_feed_get_at?: Date; + deleted_at?: TimestampNS; + last_feed_get_at?: TimestampNS; activity_filter?: ActivityFilterConfig; aggregation?: AggregationConfig; notification?: NotificationConfig; @@ -9112,15 +9670,18 @@ export interface FeedGroupChangedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; /** * The type of event: "feeds.feed_group.changed" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; feed_group?: FeedGroup; user?: UserResponseCommonFields; } @@ -9129,7 +9690,7 @@ export interface FeedGroupDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; /** * The ID of the feed group that was deleted @@ -9140,15 +9701,18 @@ export interface FeedGroupDeletedEvent { * The type of event: "feeds.feed_group.deleted" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; } export interface FeedGroupResponse { /** * When the feed group was created */ - created_at: Date; + created_at: TimestampNS; /** * Identifier within the group */ @@ -9156,7 +9720,7 @@ export interface FeedGroupResponse { /** * When the feed group was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Role new followers of feeds in this group are given. Either a built-in (feed_follower, feed_member_viewer) or any role your app has defined. Empty means feed_follower. Applied when the follow is accepted, so a follow that starts pending picks it up on approval */ @@ -9164,9 +9728,8 @@ export interface FeedGroupResponse { /** * Default visibility for activities. One of: public, visible, followers, members, private */ - default_visibility?: - 'public' | 'visible' | 'followers' | 'members' | 'private'; - deleted_at?: Date; + default_visibility?: FeedVisibility; + deleted_at?: TimestampNS; /** * Configuration for activity processors */ @@ -9193,7 +9756,7 @@ export interface FeedGroupRestoredEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; /** * The ID of the feed group that was restored @@ -9204,8 +9767,11 @@ export interface FeedGroupRestoredEvent { * The type of event: "feeds.feed_group.restored" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; } export interface FeedGroupScope { @@ -9222,7 +9788,10 @@ export interface FeedGroupScope { export interface FeedInput { description?: string; name?: string; - visibility?: 'public' | 'visible' | 'followers' | 'members' | 'private'; + /** + * Who can see and follow a feed. + */ + visibility?: FeedVisibility; filter_tags?: Array; members?: Array; custom?: Record; @@ -9233,7 +9802,7 @@ export interface FeedMemberAddedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; member: FeedMemberResponse; @@ -9241,8 +9810,11 @@ export interface FeedMemberAddedEvent { * The type of event: "feeds.feed_member.added" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -9250,7 +9822,7 @@ export interface FeedMemberRemovedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; member_id: string; custom: Record; @@ -9258,8 +9830,11 @@ export interface FeedMemberRemovedEvent { * The type of event: "feeds.feed_member.removed" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -9290,7 +9865,7 @@ export interface FeedMemberResponse { /** * When the membership was created */ - created_at: Date; + created_at: TimestampNS; /** * Role of the member in the feed */ @@ -9298,11 +9873,11 @@ export interface FeedMemberResponse { /** * Status of the membership. One of: member, pending, rejected */ - status: 'member' | 'pending' | 'rejected'; + status: FeedMemberStatus; /** * When the membership was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * User response object */ @@ -9310,11 +9885,11 @@ export interface FeedMemberResponse { /** * When the invite was accepted */ - invite_accepted_at?: Date; + invite_accepted_at?: TimestampNS; /** * When the invite was rejected */ - invite_rejected_at?: Date; + invite_rejected_at?: TimestampNS; /** * Custom data for the membership */ @@ -9322,11 +9897,13 @@ export interface FeedMemberResponse { membership_level?: MembershipLevelResponse; } +export type FeedMemberStatus = 'member' | 'pending' | 'rejected'; + export interface FeedMemberUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; member: FeedMemberResponse; @@ -9334,8 +9911,11 @@ export interface FeedMemberUpdatedEvent { * The type of event: "feeds.feed_member.updated" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -9414,7 +9994,7 @@ export interface FeedRequest { /** * Visibility setting for the feed. One of: public, visible, followers, members, private */ - visibility?: 'public' | 'visible' | 'followers' | 'members' | 'private'; + visibility?: FeedVisibility; /** * Tags used for filtering feeds */ @@ -9435,7 +10015,7 @@ export interface FeedResponse { /** * When the feed was created */ - created_at: Date; + created_at: TimestampNS; /** * Description of the feed */ @@ -9475,7 +10055,7 @@ export interface FeedResponse { /** * When the feed was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * User response object */ @@ -9483,11 +10063,11 @@ export interface FeedResponse { /** * When the feed was deleted */ - deleted_at?: Date; + deleted_at?: TimestampNS; /** * Visibility setting for the feed */ - visibility?: 'public' | 'visible' | 'followers' | 'members' | 'private'; + visibility?: FeedVisibility; /** * Tags used for filtering feeds */ @@ -9517,7 +10097,7 @@ export interface FeedSuggestionResponse { /** * When the feed was created */ - created_at: Date; + created_at: TimestampNS; /** * Description of the feed */ @@ -9557,7 +10137,7 @@ export interface FeedSuggestionResponse { /** * When the feed was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * User response object */ @@ -9565,13 +10145,13 @@ export interface FeedSuggestionResponse { /** * When the feed was deleted */ - deleted_at?: Date; + deleted_at?: TimestampNS; reason?: string; recommendation_score?: number; /** * Visibility setting for the feed */ - visibility?: 'public' | 'visible' | 'followers' | 'members' | 'private'; + visibility?: FeedVisibility; /** * Tags used for filtering feeds */ @@ -9598,7 +10178,7 @@ export interface FeedSuggestionResponse { } export interface FeedUpdatedEvent { - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; feed: FeedResponse; @@ -9606,8 +10186,11 @@ export interface FeedUpdatedEvent { * The type of event: "feeds.feed.updated" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -9619,7 +10202,7 @@ export interface FeedViewResponse { /** * When the feed view was last used */ - last_used_at?: Date; + last_used_at?: TimestampNS; /** * Configured activity selectors */ @@ -9628,6 +10211,9 @@ export interface FeedViewResponse { ranking?: RankingConfig; } +export type FeedVisibility = + 'public' | 'visible' | 'followers' | 'members' | 'private'; + export interface FeedVisibilityResponse { /** * Name of the feed visibility level @@ -9649,10 +10235,10 @@ export interface FeedsActivityLocation { } export interface FeedsBookmarkResponse { - created_at: Date; + created_at: TimestampNS; object_id: string; object_type: string; - updated_at: Date; + updated_at: TimestampNS; /** * User response object */ @@ -9662,18 +10248,21 @@ export interface FeedsBookmarkResponse { } export interface FeedsEnrichedCollectionResponse { - created_at: Date; + created_at: TimestampNS; id: string; name: string; - status: string; - updated_at: Date; + /** + * Enrichment status of a collection entry. + */ + status: CollectionStatus; + updated_at: TimestampNS; user_id: string; custom: Record; } export interface FeedsFeedResponse { activity_count: number; - created_at: Date; + created_at: TimestampNS; description: string; feed: string; follower_count: number; @@ -9683,13 +10272,16 @@ export interface FeedsFeedResponse { member_count: number; name: string; pin_count: number; - updated_at: Date; + updated_at: TimestampNS; /** * User response object */ created_by: UserResponse; - deleted_at?: Date; - visibility?: string; + deleted_at?: TimestampNS; + /** + * Who can see and follow a feed. + */ + visibility?: FeedVisibility; filter_tags?: Array; custom?: Record; location?: FeedsActivityLocation; @@ -9799,15 +10391,15 @@ export interface FeedsPreferencesResponse { export interface FeedsReactionGroupResponse { count: number; - first_reaction_at: Date; - last_reaction_at: Date; + first_reaction_at: TimestampNS; + last_reaction_at: TimestampNS; } export interface FeedsReactionResponse { activity_id: string; - created_at: Date; + created_at: TimestampNS; type: string; - updated_at: Date; + updated_at: TimestampNS; /** * User response object */ @@ -9818,7 +10410,7 @@ export interface FeedsReactionResponse { export interface FeedsRetentionPolicy { app_pk: number; - enabled_at: Date; + enabled_at: TimestampNS; policy: string; config: FeedsPolicyConfig; } @@ -9832,7 +10424,7 @@ export interface FeedsRetentionRunResponse { export interface FeedsShareResponse { activity_id: string; - created_at: Date; + created_at: TimestampNS; /** * User response object */ @@ -9842,18 +10434,24 @@ export interface FeedsShareResponse { export interface FeedsV3ActivityResponse { bookmark_count: number; comment_count: number; - created_at: Date; + created_at: TimestampNS; hidden: boolean; id: string; popularity: number; preview: boolean; reaction_count: number; - restrict_replies: string; + /** + * Who is allowed to reply to an activity. + */ + restrict_replies: RestrictReplies; score: number; share_count: number; type: string; - updated_at: Date; - visibility: string; + updated_at: TimestampNS; + /** + * Who can see an activity. + */ + visibility: ActivityVisibility; attachments: Array; comments: Array; feeds: Array; @@ -9871,9 +10469,9 @@ export interface FeedsV3ActivityResponse { * User response object */ user: UserResponse; - deleted_at?: Date; - edited_at?: Date; - expires_at?: Date; + deleted_at?: TimestampNS; + edited_at?: TimestampNS; + expires_at?: TimestampNS; friend_reaction_count?: number; is_read?: boolean; is_seen?: boolean; @@ -9898,7 +10496,7 @@ export interface FeedsV3ActivityResponse { export interface FeedsV3CommentResponse { bookmark_count: number; confidence_score: number; - created_at: Date; + created_at: TimestampNS; downvote_count: number; hidden: boolean; id: string; @@ -9907,8 +10505,11 @@ export interface FeedsV3CommentResponse { reaction_count: number; reply_count: number; score: number; - status: string; - updated_at: Date; + /** + * Moderation status of a comment. + */ + status: CommentStatus; + updated_at: TimestampNS; upvote_count: number; mentioned_users: Array; own_reactions: Array; @@ -9917,8 +10518,8 @@ export interface FeedsV3CommentResponse { */ user: UserResponse; controversy_score?: number; - deleted_at?: Date; - edited_at?: Date; + deleted_at?: TimestampNS; + edited_at?: TimestampNS; parent_id?: string; text?: string; attachments?: Array; @@ -10035,7 +10636,7 @@ export interface FlagDetailsResponse { } export interface FlagFeedbackResponse { - created_at: Date; + created_at: TimestampNS; message_id: string; labels: Array; } @@ -10085,13 +10686,13 @@ export interface FlagRequest { } export interface FlagResponse { - created_at: Date; + created_at: TimestampNS; created_by_automod: boolean; - updated_at: Date; - approved_at?: Date; + updated_at: TimestampNS; + approved_at?: TimestampNS; reason?: string; - rejected_at?: Date; - reviewed_at?: Date; + rejected_at?: TimestampNS; + reviewed_at?: TimestampNS; reviewed_by?: string; target_message_id?: string; custom?: Record; @@ -10111,10 +10712,10 @@ export interface FlagResponse { } export interface FlagUpdatedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; /** * User response object */ @@ -10209,7 +10810,7 @@ export interface FollowCreatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; follow: FollowResponse; @@ -10217,15 +10818,18 @@ export interface FollowCreatedEvent { * The type of event: "feeds.follow.created" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; } export interface FollowDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; follow: FollowResponse; @@ -10233,10 +10837,15 @@ export interface FollowDeletedEvent { * The type of event: "feeds.follow.deleted" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; } +export type FollowPushPreference = 'all' | 'none'; + export interface FollowRequest { /** * Fully qualified ID of the source feed @@ -10270,7 +10879,7 @@ export interface FollowRequest { /** * Push preference for the follow relationship */ - push_preference?: 'all' | 'none'; + push_preference?: FollowPushPreference; /** * Whether to skip push for this follow */ @@ -10278,7 +10887,7 @@ export interface FollowRequest { /** * Status of the follow relationship. One of: accepted, pending, rejected */ - status?: 'accepted' | 'pending' | 'rejected'; + status?: FollowStatus; /** * Custom data for the follow relationship */ @@ -10289,7 +10898,7 @@ export interface FollowResponse { /** * When the follow relationship was created */ - created_at: Date; + created_at: TimestampNS; /** * Role of the follower (source user) in the follow relationship, as stored. A reserved name, or a role your app no longer defines, is reported as stored but evaluated as 'feed_follower'. */ @@ -10297,36 +10906,38 @@ export interface FollowResponse { /** * Push preference for notifications. One of: all, none */ - push_preference: 'all' | 'none'; + push_preference: FollowPushPreference; /** * Status of the follow relationship. One of: accepted, pending, rejected */ - status: 'accepted' | 'pending' | 'rejected'; + status: FollowStatus; /** * When the follow relationship was last updated */ - updated_at: Date; + updated_at: TimestampNS; source_feed: FeedResponse; target_feed: FeedResponse; /** * When the follow request was accepted */ - request_accepted_at?: Date; + request_accepted_at?: TimestampNS; /** * When the follow request was rejected */ - request_rejected_at?: Date; + request_rejected_at?: TimestampNS; /** * Custom data for the follow relationship */ custom?: Record; } +export type FollowStatus = 'accepted' | 'pending' | 'rejected'; + export interface FollowUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; fid: string; custom: Record; follow: FollowResponse; @@ -10334,80 +10945,227 @@ export interface FollowUpdatedEvent { * The type of event: "feeds.follow.updated" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; -} - -export interface FrameRecordSettings { - capture_interval_in_seconds: number; - mode: 'available' | 'disabled' | 'auto-on'; - quality?: string; -} - -export interface FrameRecordingResponse { - status: string; -} - -export interface FrameRecordingSettingsRequest { - capture_interval_in_seconds: number; - mode: 'available' | 'disabled' | 'auto-on'; - quality?: '360p' | '480p' | '720p' | '1080p' | '1440p'; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; } -export interface FrameRecordingSettingsResponse { - capture_interval_in_seconds: number; - mode: 'available' | 'disabled' | 'auto-on'; - quality?: string; +export interface ForYouConfig { + /** + * Turns For You ranking on. Groups: only the non-aggregated 'foryou' group. Views: applies only when read on that group + */ + enabled?: boolean; + /** + * Name of a registered ranking preset that stands in for 'score' when score is empty. One of: for_you_v2, hot + */ + preset?: string; } -export interface FriendReactionsOptions { +export interface ForkChannelRequest { /** - * Default: false. When true, fetches friend reactions for activities. + * ID of the top-level message to fork at. The fork copies the visible messages created up to it, itself included, and their replies up to it. Fork only at a message the agent has finished writing: a message still being streamed is copied as it is at the time of the fork */ - enabled?: boolean; + message_id: string; /** - * Default: 3, Max: 10. The maximum number of friend reactions to return per activity. + * The channel a fork creates */ - limit?: number; + channel: ForkChannelTarget; /** - * Default: 'following'. The type of friend relationship to use. 'following' = users you follow, 'mutual' = users with mutual follows. One of: following, mutual + * When true, the copied messages carry no attachments. By default attachments are copied */ - type?: 'following' | 'mutual'; + exclude_attachments?: boolean; + user_id?: string; + /** + * User request object + */ + user?: UserRequest; } -export interface FullUserResponse { - banned: boolean; - created_at: Date; - id: string; - invisible: boolean; - language: string; - online: boolean; - role: string; - shadow_banned: boolean; - total_unread_count: number; - unread_channels: number; +export interface ForkChannelResponse { + duration: string; /** - * @deprecated - * Deprecated: Use total_unread_count instead. + * List of channel members */ - unread_count: number; - unread_threads: number; - updated_at: Date; - blocked_user_ids: Array; - channel_mutes: Array; - devices: Array; + members: Array; + /** + * List of channel messages + */ + messages: Array; + /** + * List of pinned messages in the channel + */ + pinned_messages: Array; + threads: Array; + /** + * A fork of an agent channel + */ + fork: ChannelForkResponse; + /** + * Milliseconds until the current user's slow mode cool down period ends. Only set on client-side get or create channel and get channel requests while the user is under cool down + */ + cooldown_remaining_ms?: number; + /** + * Whether this channel is hidden or not + */ + hidden?: boolean; + /** + * Messages before this date are hidden from the user + */ + hide_messages_before?: TimestampNS; + /** + * Number of channel watchers + */ + watcher_count?: number; + /** + * Active live locations in the channel + */ + active_live_locations?: Array; + /** + * Pending messages that this user has sent + */ + pending_messages?: Array; + /** + * List of read states + */ + read?: Array; + /** + * The caller's own scheduled, sending and failed messages in this channel. Set only by a client-side GetOrCreateChannel or QueryChannels with include_scheduled_messages; absent when not requested or not known, in which case use the scheduled messages query + */ + scheduled_messages?: Array; + /** + * List of user who is watching the channel + */ + watchers?: Array; + /** + * Represents channel in chat + */ + channel?: ChannelResponse; + draft?: DraftResponse; + membership?: ChannelMemberResponse; + /** + * The push preference details. + */ + push_preferences?: ChannelPushPreferencesResponse; +} + +export interface ForkChannelTarget { + /** + * ID of the new agent channel + */ + id: string; + /** + * Members of the new channel. Defaults to the source channel's members + */ + members?: Array; + data?: ChannelInput; +} + +export interface ForkMessageIDsRequest { + /** + * IDs of messages of the source channel, up to 1000 + */ + source_message_ids: Array; + user_id?: string; + /** + * User request object + */ + user?: UserRequest; +} + +export interface ForkMessageIDsResponse { + /** + * Duration of the request in milliseconds + */ + duration: string; + /** + * Maps each given source message ID the fork holds a copy of to the copy's ID. IDs the fork holds no copy of are left out + */ + message_id_map: Record; +} + +export interface FrameRecordSettings { + capture_interval_in_seconds: number; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; + quality?: string; +} + +export interface FrameRecordingResponse { + status: string; +} + +export interface FrameRecordingSettingsRequest { + capture_interval_in_seconds: number; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; + quality?: '360p' | '480p' | '720p' | '1080p' | '1440p'; +} + +export interface FrameRecordingSettingsResponse { + capture_interval_in_seconds: number; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; + quality?: string; +} + +export type FriendReactionType = 'following' | 'mutual'; + +export interface FriendReactionsOptions { + /** + * Default: false. When true, fetches friend reactions for activities. + */ + enabled?: boolean; + /** + * Default: 3, Max: 10. The maximum number of friend reactions to return per activity. + */ + limit?: number; + /** + * Default: 'following'. The type of friend relationship to use. 'following' = users you follow, 'mutual' = users with mutual follows. One of: following, mutual + */ + type?: FriendReactionType; +} + +export interface FullUserResponse { + banned: boolean; + created_at: TimestampNS; + id: string; + invisible: boolean; + language: string; + online: boolean; + role: string; + shadow_banned: boolean; + total_unread_count: number; + unread_channels: number; + /** + * @deprecated + * Deprecated: Use total_unread_count instead. + */ + unread_count: number; + unread_threads: number; + updated_at: TimestampNS; + blocked_user_ids: Array; + channel_mutes: Array; + devices: Array; mutes: Array; teams: Array; custom: Record; avg_response_time?: number; - ban_expires?: Date; + ban_expires?: TimestampNS; bypass_moderation?: boolean; - deactivated_at?: Date; - deleted_at?: Date; + deactivated_at?: TimestampNS; + deleted_at?: TimestampNS; image?: string; - last_active?: Date; + last_active?: TimestampNS; name?: string; - revoke_tokens_issued_before?: Date; + revoke_tokens_issued_before?: TimestampNS; latest_hidden_channels?: Array; /** * The privacy settings of the user. @@ -10417,8 +11175,8 @@ export interface FullUserResponse { } export interface FutureChannelBanResponse { - created_at: Date; - expires?: Date; + created_at: TimestampNS; + expires?: TimestampNS; reason?: string; shadow?: boolean; /** @@ -10463,11 +11221,11 @@ export interface GetActiveCallsStatusResponse { /** * End time of the status period */ - end_time: Date; + end_time: TimestampNS; /** * Start time of the status period */ - start_time: Date; + start_time: TimestampNS; metrics?: ActiveCallsMetrics; summary?: ActiveCallsSummary; } @@ -10519,7 +11277,7 @@ export interface GetCallParticipantSessionMetricsResponse { duration: string; is_publisher?: boolean; is_subscriber?: boolean; - joined_at?: Date; + joined_at?: TimestampNS; publisher_type?: string; user_id?: string; user_session_id?: string; @@ -10567,10 +11325,10 @@ export interface GetCallSessionParticipantStatsDetailsResponse { } export interface GetCallTypeResponse { - created_at: Date; + created_at: TimestampNS; duration: string; name: string; - updated_at: Date; + updated_at: TimestampNS; grants: Record>; notification_settings: NotificationSettingsResponse; settings: CallSettingsResponse; @@ -10587,11 +11345,17 @@ export interface GetCampaignResponse { } export interface GetChannelTypeResponse { - automod: 'disabled' | 'simple' | 'AI'; - automod_behavior: 'flag' | 'block' | 'shadow_block'; + /** + * Automod level applied to a channel type. + */ + automod: AutomodMode; + /** + * What automod or a blocklist does when it matches. + */ + automod_behavior: ChannelModerationBehavior; connect_events: boolean; count_messages: boolean; - created_at: Date; + created_at: TimestampNS; custom_events: boolean; delivery_events: boolean; /** @@ -10614,7 +11378,7 @@ export interface GetChannelTypeResponse { shared_locations: boolean; skip_last_msg_update_for_system_msgs: boolean; typing_events: boolean; - updated_at: Date; + updated_at: TimestampNS; uploads: boolean; url_enrichment: boolean; user_message_reminders: boolean; @@ -10622,13 +11386,16 @@ export interface GetChannelTypeResponse { permissions: Array; grants: Record>; blocklist?: string; - blocklist_behavior?: 'flag' | 'block' | 'shadow_block'; + /** + * What automod or a blocklist does when it matches. + */ + blocklist_behavior?: ChannelModerationBehavior; partition_size?: number; partition_ttl?: string; /** * Sets the push notification level for a channel type */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; allowed_flag_reasons?: Array; blocklists?: Array; /** @@ -10644,8 +11411,8 @@ export interface GetCommandResponse { duration: string; name: string; set: string; - created_at?: Date; - updated_at?: Date; + created_at?: TimestampNS; + updated_at?: TimestampNS; } export interface GetCommentRepliesResponse { @@ -10749,13 +11516,13 @@ export interface GetExternalStorageGCSResponse { } export interface GetExternalStorageResponse { - created_at: Date; + created_at: TimestampNS; /** * Duration of the request in milliseconds */ duration: string; type: string; - updated_at: Date; + updated_at: TimestampNS; aws_s3?: GetExternalStorageAWSS3Response; gcs?: GetExternalStorageGCSResponse; } @@ -10884,6 +11651,21 @@ export interface GetFollowSuggestionsResponse { algorithm_used?: string; } +export interface GetHistoryWindowTeamsResponse { + /** + * History window, in hours, of every team without full history; 0 is full history + */ + default_window_hours: number; + /** + * Duration of the request in milliseconds + */ + duration: string; + /** + * Teams with full history + */ + teams: Array; +} + export interface GetImportResponse { /** * Duration of the request in milliseconds @@ -10894,7 +11676,7 @@ export interface GetImportResponse { export interface GetImportV2TaskResponse { app_pk: number; - created_at: Date; + created_at: TimestampNS; /** * Duration of the request in milliseconds */ @@ -10902,7 +11684,7 @@ export interface GetImportV2TaskResponse { id: string; product: string; state: number; - updated_at: Date; + updated_at: TimestampNS; settings: ImportV2TaskSettings; result?: Record; } @@ -11038,8 +11820,7 @@ export interface GetOrCreateFeedGroupRequest { /** * Default visibility for the feed group, can be 'public', 'visible', 'followers', 'members', or 'private'. Defaults to 'visible' if not provided. */ - default_visibility?: - 'public' | 'visible' | 'followers' | 'members' | 'private'; + default_visibility?: FeedVisibility; /** * Configuration for activity processors */ @@ -11321,20 +12102,20 @@ export interface GetSetupSessionResponse { } export interface GetTaskResponse { - created_at: Date; + created_at: TimestampNS; duration: string; /** - * Current status of task + * Current status of the whole operation. A task whose result reports an in-progress status of its own is returned as running until that status is terminal, so completed means the operation itself finished rather than that its first step was accepted. A result that never reaches a terminal status stops being reported as running after a fixed period and falls back to the task's own status. */ status: string; /** * ID of task */ task_id: string; - updated_at: Date; + updated_at: TimestampNS; error?: ErrorResult; /** - * Result produced by task after completion + * Result produced by task after completion. A task that does its work in stages may carry a status of its own here, describing that work rather than the task; the top-level status already accounts for it, so use that to tell whether the operation has finished. */ result?: Record; } @@ -11405,7 +12186,7 @@ export interface GroupedChannelsBucket { export interface GroupedChannelsGroupRequest { /** - * Max channels for this group; omitted or zero uses the request limit + * Max channels for this group; values above 15 are capped at 15. Omitted or zero uses the request limit */ limit?: number; /** @@ -11424,7 +12205,7 @@ export interface GroupedChannelsGroupRequest { export interface GroupedQueryChannelsRequest { /** - * Default max channels per group (default 10) + * Default max channels per group (default 10); values above 15 are capped at 15 */ limit?: number; user_id?: string; @@ -11515,6 +12296,21 @@ export interface HideChannelResponse { duration: string; } +export interface HistoryWindowTeam { + /** + * Date/time the team was first given full history + */ + created_at: TimestampNS; + /** + * The team, as set on channels and users + */ + team: string; + /** + * Date/time the team was last marked + */ + updated_at: TimestampNS; +} + export interface HuaweiConfig { disabled?: boolean; id?: string; @@ -11625,29 +12421,29 @@ export interface ImportBlockListResponse { } export interface ImportTask { - created_at: Date; + created_at: TimestampNS; id: string; mode: string; path: string; state: string; - updated_at: Date; + updated_at: TimestampNS; history: Array; size?: number; } export interface ImportTaskHistory { - created_at: Date; + created_at: TimestampNS; next_state: string; prev_state: string; } export interface ImportV2TaskItem { app_pk: number; - created_at: Date; + created_at: TimestampNS; id: string; product: string; state: number; - updated_at: Date; + updated_at: TimestampNS; settings: ImportV2TaskSettings; result?: Record; } @@ -11675,7 +12471,10 @@ export interface Incident { } export interface IndividualRecordSettings { - mode: 'available' | 'disabled' | 'auto-on'; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; output_types?: Array; } @@ -11687,7 +12486,7 @@ export interface IndividualRecordingSettingsRequest { /** * Recording mode. One of: available, disabled, auto-on */ - mode: 'available' | 'disabled' | 'auto-on'; + mode: FeatureActivationMode; /** * Output types to include: audio_only, video_only, audio_video, screenshare_audio_only, screenshare_video_only, screenshare_audio_video */ @@ -11695,19 +12494,22 @@ export interface IndividualRecordingSettingsRequest { } export interface IndividualRecordingSettingsResponse { - mode: 'available' | 'disabled' | 'auto-on'; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; output_types?: Array; } export interface IngressAudioEncodingOptions { bitrate: number; - channels: '1' | '2'; + channels: number; enable_dtx: boolean; } export interface IngressAudioEncodingOptionsRequest { bitrate: number; - channels: '1' | '2'; + channels: number; enable_dtx?: boolean; } @@ -11719,7 +12521,7 @@ export interface IngressAudioEncodingResponse { export interface IngressErrorEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Human-readable error message */ @@ -11767,7 +12569,7 @@ export interface IngressSource { } export interface IngressSourceRequest { - fps: '30' | '60'; + fps: number; height: number; width: number; } @@ -11780,7 +12582,7 @@ export interface IngressSourceResponse { export interface IngressStartedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Unique identifier for this stream */ @@ -11813,7 +12615,7 @@ export interface IngressStartedEvent { export interface IngressStoppedEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * Unique identifier for the stream */ @@ -11845,7 +12647,10 @@ export interface IngressVideoEncodingResponse { export interface IngressVideoLayer { bitrate: number; - codec: 'h264' | 'vp8'; + /** + * The codec an ingress video layer is encoded with. + */ + codec: VideoCodec; frame_rate: number; max_dimension: number; min_dimension: number; @@ -11853,7 +12658,10 @@ export interface IngressVideoLayer { export interface IngressVideoLayerRequest { bitrate: number; - codec: 'h264' | 'vp8'; + /** + * The codec an ingress video layer is encoded with. + */ + codec: VideoCodec; frame_rate_limit: number; max_dimension: number; min_dimension: number; @@ -12068,7 +12876,7 @@ export interface KickUserResponse { export interface KickedUserEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * User response object */ @@ -12126,7 +12934,7 @@ export interface LabelResultResponse { /** * Timestamp */ - created_at: Date; + created_at: TimestampNS; /** * High-level harm category */ @@ -12256,16 +13064,25 @@ export interface LabelsResponse { labels?: Array; } +export type LayoutName = + 'spotlight' | 'grid' | 'single-participant' | 'mobile' | 'custom'; + export interface LayoutSettings { external_app_url: string; external_css_url: string; - name: 'spotlight' | 'grid' | 'single-participant' | 'mobile' | 'custom'; + /** + * The layout a call recording or broadcast is composed with. + */ + name: LayoutName; detect_orientation?: boolean; options?: Record; } export interface LayoutSettingsRequest { - name: 'spotlight' | 'grid' | 'single-participant' | 'mobile' | 'custom'; + /** + * The layout a call recording or broadcast is composed with. + */ + name: LayoutName; detect_orientation?: boolean; external_app_url?: string; external_css_url?: string; @@ -12275,7 +13092,10 @@ export interface LayoutSettingsRequest { export interface LayoutSettingsResponse { external_app_url: string; external_css_url: string; - name: 'spotlight' | 'grid' | 'single-participant' | 'mobile' | 'custom'; + /** + * The layout a call recording or broadcast is composed with. + */ + name: LayoutName; detect_orientation?: boolean; options?: Record; } @@ -12284,14 +13104,14 @@ export interface LifecycleSettingsRequest { /** * Call lifecycle mode. One of: reusable, single_use. A single_use call cannot be reused once it has ended; the server ends it when its ring settles without an answer (currently: when the caller cancels the ring). Fixed at call creation */ - mode?: 'reusable' | 'single_use'; + mode?: CallLifecycleMode; } export interface LifecycleSettingsResponse { /** * Call lifecycle mode. One of: reusable, single_use. A single_use call cannot be reused once it has ended; the server ends it when its ring settles without an answer (currently: when the caller cancels the ring). Fixed at call creation */ - mode: 'reusable' | 'single_use'; + mode: CallLifecycleMode; } export interface LimitInfoResponse { @@ -12592,9 +13412,9 @@ export interface MarkReadResponseEvent { channel_id: string; channel_type: string; cid: string; - created_at: Date; + created_at: TimestampNS; type: string; - channel_last_message_at?: Date; + channel_last_message_at?: TimestampNS; last_read_message_id?: string; team?: string; /** @@ -12648,7 +13468,7 @@ export interface MatchedContent { /** * `content_published_at` from the contributing `/analyze` request, or server receive time when that field was omitted. */ - published_at: Date; + published_at: TimestampNS; /** * Content type that contributed this entry: `image` or `text`. */ @@ -12673,17 +13493,32 @@ export interface MatchedContent { } export interface MaxStreakChangedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; +} + +export interface MediaDisplayResponse { + /** + * Reveals must be recorded via the reveal media endpoint (blurred CSAM or gore) + */ + audit_reveal: boolean; + /** + * Default rendering before a reveal + */ + mode: 'normal' | 'blur' | 'blur_grayscale'; + /** + * Only users with the RevealProtectedMedia permission may reveal the media (CSAM) + */ + reveal_restricted: boolean; } export interface MemberAddedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -12714,7 +13549,7 @@ export interface MemberAddedEvent { * The CID of the channel to which the member was added */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -12727,7 +13562,7 @@ export interface MemberRemovedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -12758,7 +13593,7 @@ export interface MemberRemovedEvent { * The CID of the channel from which the member was removed */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -12780,11 +13615,11 @@ export interface MemberResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; user_id: string; /** * Custom member response data @@ -12797,7 +13632,7 @@ export interface MemberResponse { /** * Date/time of deletion */ - deleted_at?: Date; + deleted_at?: TimestampNS; role?: string; } @@ -12805,7 +13640,7 @@ export interface MemberUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -12836,7 +13671,7 @@ export interface MemberUpdatedEvent { * The CID of the channel in which the member was updated */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -12876,7 +13711,7 @@ export interface MembershipLevelResponse { /** * When the membership level was created */ - created_at: Date; + created_at: TimestampNS; /** * Unique identifier for the membership level */ @@ -12892,7 +13727,7 @@ export interface MembershipLevelResponse { /** * When the membership level was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Activity tags this membership level gives access to */ @@ -12946,7 +13781,7 @@ export interface MessageDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Whether the message was hard deleted */ @@ -12985,7 +13820,7 @@ export interface MessageDeletedEvent { * Whether the message was deleted only for the current user */ deleted_for_me?: boolean; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -12995,13 +13830,13 @@ export interface MessageDeletedEvent { } export interface MessageFlagResponse { - created_at: Date; + created_at: TimestampNS; created_by_automod: boolean; - updated_at: Date; - approved_at?: Date; + updated_at: TimestampNS; + approved_at?: TimestampNS; reason?: string; - rejected_at?: Date; - reviewed_at?: Date; + rejected_at?: TimestampNS; + reviewed_at?: TimestampNS; custom?: Record; details?: FlagDetailsResponse; /** @@ -13027,7 +13862,7 @@ export interface MessageFlaggedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; message_id: string; /** * Represents any chat message @@ -13061,7 +13896,7 @@ export interface MessageFlaggedEvent { * The reason for the flag */ reason?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -13086,7 +13921,7 @@ export interface MessageFlaggedEvent { export interface MessageHistoryEntryResponse { is_deleted: boolean; message_id: string; - message_updated_at: Date; + message_updated_at: TimestampNS; message_updated_by_id: string; text: string; attachments: Array; @@ -13101,7 +13936,7 @@ export interface MessageModerationResult { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * ID of the message */ @@ -13109,7 +13944,7 @@ export interface MessageModerationResult { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * Whether user has bad karma */ @@ -13144,7 +13979,7 @@ export interface MessageNewEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; message_id: string; /** * The number of watchers @@ -13183,7 +14018,7 @@ export interface MessageNewEvent { * The author of the parent message */ parent_author?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -13263,7 +14098,7 @@ export interface MessageReadEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "message.read" in this case @@ -13293,7 +14128,7 @@ export interface MessageReadEvent { * The ID of the last read message */ last_read_message_id?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -13393,7 +14228,7 @@ export interface MessageResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; deleted_reply_count: number; /** * Contains HTML markup of the message. Can only be set when using server-side API @@ -13438,7 +14273,7 @@ export interface MessageResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * Array of message attachments */ @@ -13479,9 +14314,9 @@ export interface MessageResponse { /** * Date/time of deletion */ - deleted_at?: Date; + deleted_at?: TimestampNS; deleted_for_me?: boolean; - message_text_updated_at?: Date; + message_text_updated_at?: TimestampNS; /** * Should be empty if `text` is provided. Can only be set when using server-side API */ @@ -13493,11 +14328,11 @@ export interface MessageResponse { /** * Date when pinned message expires */ - pin_expires?: Date; + pin_expires?: TimestampNS; /** * Date when message got pinned */ - pinned_at?: Date; + pinned_at?: TimestampNS; /** * Identifier of the poll to include in the message */ @@ -13560,7 +14395,7 @@ export interface MessageUnblockedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; message_id: string; custom: Record; /** @@ -13575,7 +14410,7 @@ export interface MessageUnblockedEvent { * The CID of the channel where the message was unblocked */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; } @@ -13583,7 +14418,7 @@ export interface MessageUndeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; message_id: string; custom: Record; /** @@ -13614,7 +14449,7 @@ export interface MessageUndeletedEvent { * The CID of the channel where the message was sent */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -13631,7 +14466,7 @@ export interface MessageUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; message_id: string; custom: Record; /** @@ -13662,7 +14497,7 @@ export interface MessageUpdatedEvent { * The CID of the channel where the message was sent */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -13680,7 +14515,7 @@ export interface MessageWithChannelResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; deleted_reply_count: number; /** * Contains HTML markup of the message. Can only be set when using server-side API @@ -13725,7 +14560,7 @@ export interface MessageWithChannelResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * Array of message attachments */ @@ -13770,9 +14605,9 @@ export interface MessageWithChannelResponse { /** * Date/time of deletion */ - deleted_at?: Date; + deleted_at?: TimestampNS; deleted_for_me?: boolean; - message_text_updated_at?: Date; + message_text_updated_at?: TimestampNS; /** * Should be empty if `text` is provided. Can only be set when using server-side API */ @@ -13784,11 +14619,11 @@ export interface MessageWithChannelResponse { /** * Date when pinned message expires */ - pin_expires?: Date; + pin_expires?: TimestampNS; /** * Date when message got pinned */ - pinned_at?: Date; + pinned_at?: TimestampNS; /** * Identifier of the poll to include in the message */ @@ -13880,6 +14715,9 @@ export interface MetricsPct { quality_score?: number; } +export type ModerationAction = + 'flag' | 'shadow' | 'remove' | 'bounce' | 'bounce_flag' | 'bounce_remove'; + export interface ModerationActionConfigResponse { /** * The action to take @@ -13913,7 +14751,7 @@ export interface ModerationActionConfigResponse { } export interface ModerationAnalysisFailedEvent { - created_at: Date; + created_at: TimestampNS; type: string; /** * The moderation policy key the request targeted. @@ -13931,7 +14769,7 @@ export interface ModerationAnalysisFailedEvent { * Echo of the `entity_type` on the /analyze request. */ entity_type?: string; - received_at?: Date; + received_at?: TimestampNS; /** * Echo of the request's `content_ids`, keyed by text/image label. On keyframe and caption streams every request repeats the same entity_type/entity_id/entity_creator_id, so this is what identifies the specific submission that went unscreened. */ @@ -13950,21 +14788,21 @@ export interface ModerationCallResponse { backstage: boolean; captioning: boolean; cid: string; - created_at: Date; + created_at: TimestampNS; current_session_id: string; id: string; recording: boolean; transcribing: boolean; translating: boolean; type: string; - updated_at: Date; + updated_at: TimestampNS; blocked_user_ids: Array; custom: Record; channel_cid?: string; - ended_at?: Date; + ended_at?: TimestampNS; join_ahead_time_seconds?: number; routing_number?: string; - starts_at?: Date; + starts_at?: TimestampNS; team?: string; /** * User response object @@ -13973,7 +14811,7 @@ export interface ModerationCallResponse { } export interface ModerationCheckCompletedEvent { - created_at: Date; + created_at: TimestampNS; /** * The ID of entity which was moderated */ @@ -13992,7 +14830,7 @@ export interface ModerationCheckCompletedEvent { review_queue_item_id: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ModerationConfig { @@ -14022,11 +14860,11 @@ export interface ModerationCustomActionEvent { * The ID of the custom action that was executed */ action_id: string; - created_at: Date; + created_at: TimestampNS; custom: Record; review_queue_item: ReviewQueueItemResponse; type: string; - received_at?: Date; + received_at?: TimestampNS; /** * Additional options passed to the custom action */ @@ -14049,23 +14887,163 @@ export interface ModerationDashboardPreferences { flag_user_on_flagged_content?: boolean; include_attachment_payload?: boolean; media_queue_blur_enabled?: boolean; + text_redaction_enabled?: boolean; allowed_moderation_action_reasons?: Array; escalation_reasons?: Array; filterable_custom_keys?: Array; keyframe_classifications_map?: Record>; + media_label_display?: Record; overview_dashboard?: OverviewDashboardConfig; } +export interface ModerationFeedbackItemResponse { + /** + * When the item was submitted; all items of one submission share it + */ + created_at: TimestampNS; + /** + * Feedback item ID + */ + id: string; + /** + * false_positive or false_negative + */ + kind: string; + /** + * Bodyguard's ID for the delivered feedback + */ + bodyguard_feedback_id?: string; + /** + * When Bodyguard accepted the feedback; unset if not sent + */ + bodyguard_sent_at?: TimestampNS; + /** + * Channel of a single-message feedback + */ + channel_id?: string; + /** + * Moderator's confidence + */ + confidence?: string; + /** + * Action the content had + */ + current_recommended_action?: string; + /** + * Moderator's description + */ + description?: string; + /** + * ID of the moderated entity + */ + entity_id?: string; + /** + * Type of the moderated entity + */ + entity_type?: string; + /** + * Action the moderator expected + */ + expected_recommended_action?: string; + /** + * Flag the feedback disputes + */ + flag_id?: string; + /** + * Image content of the item + */ + image_url?: string; + /** + * Publish time of a single-message feedback + */ + published_at?: string; + /** + * Which content of the entity the item is about + */ + reference?: string; + /** + * Moderator who submitted the feedback + */ + reporter_id?: string; + /** + * Review queue item the feedback is about + */ + review_queue_item_id?: string; + /** + * Text content of the item + */ + text?: string; + /** + * Labels the content had + */ + current_labels?: Array; + /** + * Labels the moderator expected + */ + expected_labels?: Array; +} + +export interface ModerationFeedbackSubmissionResponse { + /** + * When the submission was made + */ + created_at: TimestampNS; + /** + * Number of items in the submission, matching the filter or not + */ + item_count: number; + /** + * Number of items in the submission that match the filter, including those left out by item_limit + */ + matched_count: number; + /** + * Number of items in the submission that Bodyguard accepted + */ + sent_count: number; + /** + * The submission's items that match the filter + */ + items: Array; + /** + * ID of the moderated entity + */ + entity_id?: string; + /** + * Type of the moderated entity + */ + entity_type?: string; + /** + * Moderator who submitted the feedback + */ + reporter_id?: string; + /** + * Review queue item the feedback is about + */ + review_queue_item_id?: string; + /** + * Distinct labels the submission's items had + */ + current_labels?: Array; + /** + * Distinct labels the moderator expected across the submission's items + */ + expected_labels?: Array; +} + export interface ModerationFlagResponse { - created_at: Date; + created_at: TimestampNS; entity_id: string; entity_type: string; type: string; - updated_at: Date; + updated_at: TimestampNS; user_id: string; result: Array>; - content_published_at?: Date; + content_published_at?: TimestampNS; entity_creator_id?: string; + /** + * Unique identifier of the flag + */ + id?: string; reason?: string; review_queue_item_id?: string; labels?: Array; @@ -14086,18 +15064,18 @@ export interface ModerationFlaggedEvent { * The type of content that was flagged */ content_type: string; - created_at: Date; + created_at: TimestampNS; /** * The ID of the flagged content */ object_id: string; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ModerationImageAnalysisCompleteEvent { - created_at: Date; + created_at: TimestampNS; type: string; /** * The moderation policy key that was applied. @@ -14115,7 +15093,7 @@ export interface ModerationImageAnalysisCompleteEvent { * Echo of the `entity_type` on the /analyze request. */ entity_type?: string; - received_at?: Date; + received_at?: TimestampNS; /** * Review queue row ID for deep-linking into the dashboard. */ @@ -14135,11 +15113,11 @@ export interface ModerationImageAnalysisCompleteEvent { } export interface ModerationMarkReviewedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; item: ReviewQueueItemResponse; type: string; - received_at?: Date; + received_at?: TimestampNS; /** * Represents any chat message */ @@ -14149,6 +15127,7 @@ export interface ModerationMarkReviewedEvent { export interface ModerationPayload { country_code?: string; audios?: Array; + country_codes?: Array; image_ordered_keys?: Array; images?: Array; other_media?: Array; @@ -14227,14 +15206,14 @@ export interface ModerationPayloadResponse { } export interface ModerationQueueResponse { - created_at: Date; + created_at: TimestampNS; created_by: string; description: string; id: string; item_count: number; name: string; type: string; - updated_at: Date; + updated_at: TimestampNS; sort: Array>; filters: Record; } @@ -14266,14 +15245,14 @@ export interface ModerationRuleInfo { } export interface ModerationRuleV2Response { - created_at: Date; + created_at: TimestampNS; description: string; enabled: boolean; id: string; name: string; rule_type: string; team: string; - updated_at: Date; + updated_at: TimestampNS; config_keys: Array; cooldown_period?: string; logic?: string; @@ -14284,7 +15263,7 @@ export interface ModerationRuleV2Response { } export interface ModerationRulesTriggeredEvent { - created_at: Date; + created_at: TimestampNS; /** * The ID of the entity that triggered the rule */ @@ -14304,11 +15283,35 @@ export interface ModerationRulesTriggeredEvent { custom: Record; rule: ModerationRuleInfo; type: string; - received_at?: Date; + /** + * Application ID the rule belongs to. + */ + app_id?: number; + /** + * The `category` sent on the labels request. Omitted when the event was not produced by the labels API. + */ + category?: string; + /** + * CID of the channel the moderated content belongs to. Omitted when the content has no channel. + */ + channel_cid?: string; + received_at?: TimestampNS; /** * The review queue item ID if applicable */ review_queue_item_id?: string; + /** + * Highest severity (`LOW` / `MEDIUM` / `HIGH` / `CRITICAL`) among the moderation results that carried a matched label. Omitted when no label matched or the provider reports no severity. + */ + severity?: string; + /** + * Configured count that fires the condition. This is the number stored on the rule, not the live counter. + */ + threshold?: number; + /** + * Configured window of the condition that fired, for example 10m. + */ + time_window?: string; /** * The violation number for call rules (optional) */ @@ -14317,11 +15320,24 @@ export interface ModerationRulesTriggeredEvent { * Ordered list of contents whose verdicts contributed to an aggregation rule's threshold. Populated only for aggregation rules when callers supplied `content_ids`. */ matched_contents?: Array; + /** + * Harm labels on the moderated content that the rule's text label conditions (`text_content`, `text_rule`) matched. Omitted when the rule has no such condition. + */ + matched_labels?: Array; + /** + * Cross-user webhook_only flood rules only: the distinct users who sent the content since the previous webhook for this rule. Omitted on other events. + */ + matched_user_ids?: Array; + /** + * The rule's config_keys. Omitted when the rule is application-wide. + */ + scope?: Array; + app_count?: AppCountInfo; intent?: IntentEvaluationInfo; } export interface ModerationTextAnalysisCompleteEvent { - created_at: Date; + created_at: TimestampNS; type: string; /** * The moderation policy key that was applied. @@ -14339,7 +15355,7 @@ export interface ModerationTextAnalysisCompleteEvent { * Echo of the `entity_type` on the /analyze request. */ entity_type?: string; - received_at?: Date; + received_at?: TimestampNS; /** * Review queue row ID for deep-linking into the dashboard. */ @@ -14449,7 +15465,10 @@ export interface NetworkMetricsReportResponse { } export interface NoiseCancellationSettings { - mode: 'available' | 'disabled' | 'auto-on'; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; } export interface NotificationComment { @@ -14499,7 +15518,7 @@ export interface NotificationFeedUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the feed */ @@ -14509,8 +15528,11 @@ export interface NotificationFeedUpdatedEvent { * The type of event: "feeds.notification_feed.updated" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; /** * Aggregated activities for notification feeds */ @@ -14523,7 +15545,7 @@ export interface NotificationMarkUnreadEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "notification.mark_unread" in this case @@ -14553,12 +15575,12 @@ export interface NotificationMarkUnreadEvent { /** * The time when the channel/thread was marked as unread */ - last_read_at?: Date; + last_read_at?: TimestampNS; /** * The ID of the last read message */ last_read_message_id?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -14647,11 +15669,11 @@ export interface NotificationStatusResponse { /** * When notifications were last read */ - last_read_at?: Date; + last_read_at?: TimestampNS; /** * When notifications were last seen */ - last_seen_at?: Date; + last_seen_at?: TimestampNS; /** * Deprecated: use is_read on each activity/group instead. IDs of activities that have been read. Capped at ~101 entries for aggregated feeds. */ @@ -14699,7 +15721,7 @@ export interface NotificationThreadMessageNewEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; message_id: string; /** * The ID of the thread @@ -14737,7 +15759,7 @@ export interface NotificationThreadMessageNewEvent { */ cid?: string; parent_author?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -14774,8 +15796,10 @@ export interface OCRContentParameters { } export interface OCRRule { - action: - 'flag' | 'shadow' | 'remove' | 'bounce' | 'bounce_flag' | 'bounce_remove'; + /** + * What a moderation rule does when it matches. + */ + action: ModerationAction; label: string; } @@ -14855,7 +15879,7 @@ export type OwnCapability = (typeof OwnCapability)[keyof typeof OwnCapability]; export interface OwnUserResponse { banned: boolean; - created_at: Date; + created_at: TimestampNS; id: string; invisible: boolean; language: string; @@ -14869,7 +15893,7 @@ export interface OwnUserResponse { */ unread_count: number; unread_threads: number; - updated_at: Date; + updated_at: TimestampNS; blocked_user_ids: Array; channel_mutes: Array; devices: Array; @@ -14877,12 +15901,12 @@ export interface OwnUserResponse { teams: Array; custom: Record; avg_response_time?: number; - deactivated_at?: Date; - deleted_at?: Date; + deactivated_at?: TimestampNS; + deleted_at?: TimestampNS; image?: string; - last_active?: Date; + last_active?: TimestampNS; name?: string; - revoke_tokens_issued_before?: Date; + revoke_tokens_issued_before?: TimestampNS; latest_hidden_channels?: Array; /** * The privacy settings of the user. @@ -14934,7 +15958,7 @@ export interface ParticipantCountByMinuteResponse { last: number; max: number; min: number; - start_ts: Date; + start_ts: TimestampNS; } export interface ParticipantCountOverTimeResponse { @@ -14979,9 +16003,9 @@ export interface ParticipantSeriesSubscriptionTrackMetrics { export interface ParticipantSeriesTimeframe { max_points: number; - since: Date; + since: TimestampNS; step_seconds: number; - until: Date; + until: TimestampNS; } export interface ParticipantSeriesTrackMetrics { @@ -15009,15 +16033,15 @@ export interface ParticipantSessionDetails { user_session_id: string; roles: Array; duration_in_seconds?: number; - joined_at?: Date; - left_at?: Date; + joined_at?: TimestampNS; + left_at?: TimestampNS; } export interface PendingMessageEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The method used for the pending message */ @@ -15027,7 +16051,7 @@ export interface PendingMessageEvent { * The type of event: "message.pending" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; /** * Represents channel in chat */ @@ -15080,7 +16104,7 @@ export interface PerformanceAnalysisResponse { unindexed_fields: Array; unindexed_sort_fields: Array; warnings: Array; - last_analyzed?: Date; + last_analyzed?: TimestampNS; scan_type?: string; } @@ -15160,7 +16184,7 @@ export interface PermissionRequest { export interface PermissionRequestEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The list of permissions requested by the user */ @@ -15191,7 +16215,7 @@ export interface PinActivityResponse { /** * When the activity was pinned */ - created_at: Date; + created_at: TimestampNS; duration: string; /** * Fully qualified ID of the feed the activity was pinned to @@ -15230,11 +16254,11 @@ export interface PlatformDataResponse { export interface Policy { action: number; - created_at: Date; + created_at: TimestampNS; name: string; owner: boolean; priority: number; - updated_at: Date; + updated_at: TimestampNS; resources: Array; roles: Array; } @@ -15273,7 +16297,7 @@ export interface PolicyTestLabelDrift { } export interface PolicyTestResult { - created_at: Date; + created_at: TimestampNS; id: number; message_text: string; row_index: number; @@ -15299,7 +16323,7 @@ export interface PolicyTestRow { export interface PolicyTestRun { config_key: string; - created_at: Date; + created_at: TimestampNS; id: string; rows_completed: number; rows_total: number; @@ -15307,10 +16331,10 @@ export interface PolicyTestRun { status: string; task_id: string; triggered_by: string; - completed_at?: Date; - config_updated_at?: Date; + completed_at?: TimestampNS; + config_updated_at?: TimestampNS; error_message?: string; - started_at?: Date; + started_at?: TimestampNS; metrics?: PolicyTestRunMetrics; } @@ -15342,13 +16366,13 @@ export interface PolicyTestSeedSpec { export interface PolicyTestSet { config_key: string; - created_at: Date; + created_at: TimestampNS; created_by: string; id: string; mode: string; name: string; row_count: number; - updated_at: Date; + updated_at: TimestampNS; team?: string; rows?: Array; last_run?: PolicyTestRun; @@ -15387,7 +16411,7 @@ export interface PollClosedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; poll: PollResponseData; /** @@ -15403,14 +16427,14 @@ export interface PollClosedEvent { * The ID of the message containing the poll */ message_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface PollDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; poll: PollResponseData; /** @@ -15426,7 +16450,7 @@ export interface PollDeletedEvent { * The ID of the message containing the poll */ message_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface PollOptionInput { @@ -15467,18 +16491,18 @@ export interface PollResponseData { allow_answers: boolean; allow_user_suggested_options: boolean; answers_count: number; - created_at: Date; + created_at: TimestampNS; created_by_id: string; description: string; enforce_unique_vote: boolean; id: string; name: string; - updated_at: Date; + updated_at: TimestampNS; vote_count: number; /** * Voting visibility of the poll */ - voting_visibility: 'anonymous' | 'public'; + voting_visibility: VotingVisibility; latest_answers: Array; options: Array; own_votes: Array; @@ -15499,7 +16523,7 @@ export interface PollUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; poll: PollResponseData; /** @@ -15515,14 +16539,14 @@ export interface PollUpdatedEvent { * The ID of the message containing the poll */ message_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface PollVoteCastedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; poll: PollResponseData; poll_vote: PollVoteResponseData; @@ -15539,14 +16563,14 @@ export interface PollVoteCastedEvent { * The ID of the message containing the poll */ message_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface PollVoteChangedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; poll: PollResponseData; poll_vote: PollVoteResponseData; @@ -15563,14 +16587,14 @@ export interface PollVoteChangedEvent { * The ID of the message containing the poll */ message_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface PollVoteRemovedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; poll: PollResponseData; poll_vote: PollVoteResponseData; @@ -15587,7 +16611,7 @@ export interface PollVoteRemovedEvent { * The ID of the message containing the poll */ message_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface PollVoteResponse { @@ -15600,11 +16624,11 @@ export interface PollVoteResponse { } export interface PollVoteResponseData { - created_at: Date; + created_at: TimestampNS; id: string; option_id: string; poll_id: string; - updated_at: Date; + updated_at: TimestampNS; answer_text?: string; is_answer?: boolean; user_id?: string; @@ -15646,10 +16670,10 @@ export interface PoorTail { } export interface PredefinedFilterResponse { - created_at: Date; + created_at: TimestampNS; name: string; operation: string; - updated_at: Date; + updated_at: TimestampNS; filter: Record; description?: string; query_id?: number; @@ -15661,7 +16685,7 @@ export interface PredefinedFilterResponse { export interface PredefinedFilterStatsResponse { calls: number; max_latency_ms: number; - last_seen?: Date; + last_seen?: TimestampNS; } export interface PrivacySettingsResponse { @@ -15744,6 +16768,9 @@ export interface PushConfig { offline_only?: boolean; } +export type PushLevel = + 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + export interface PushNotificationConfig { /** * Whether push notifications are enabled for this feed group @@ -15767,7 +16794,7 @@ export interface PushNotificationFields { export interface PushNotificationSettingsResponse { disabled?: boolean; - disabled_until?: Date; + disabled_until?: TimestampNS; } export interface PushPreferenceInput { @@ -15826,7 +16853,7 @@ export interface PushPreferencesResponse { /** * If provided the notifications will be disabled until the set date. */ - disabled_until?: Date; + disabled_until?: TimestampNS; feeds_level?: string; chat_preferences?: ChatPreferencesResponse; feeds_preferences?: FeedsPreferencesResponse; @@ -15837,10 +16864,10 @@ export interface PushPreferencesResponse { } export interface PushProvider { - created_at: Date; + created_at: TimestampNS; name: string; type: string; - updated_at: Date; + updated_at: TimestampNS; apn_auth_key?: string; apn_auth_type?: string; apn_development?: boolean; @@ -15851,7 +16878,7 @@ export interface PushProvider { apn_team_id?: string; apn_topic?: string; description?: string; - disabled_at?: Date; + disabled_at?: TimestampNS; disabled_reason?: string; firebase_apn_template?: string; firebase_credentials?: string; @@ -15867,6 +16894,8 @@ export interface PushProvider { push_templates?: Array; } +export type PushProviderName = 'firebase' | 'apn' | 'huawei' | 'xiaomi'; + export interface PushProviderRequest { name: string; apn_auth_key?: string; @@ -15895,10 +16924,10 @@ export interface PushProviderRequest { } export interface PushProviderResponse { - created_at: Date; + created_at: TimestampNS; name: string; type: string; - updated_at: Date; + updated_at: TimestampNS; apn_auth_key?: string; apn_auth_type?: string; apn_development?: boolean; @@ -15911,7 +16940,7 @@ export interface PushProviderResponse { apn_team_id?: string; apn_topic?: string; description?: string; - disabled_at?: Date; + disabled_at?: TimestampNS; disabled_reason?: string; firebase_apn_template?: string; firebase_credentials?: string; @@ -15948,7 +16977,7 @@ export interface PushScheduleResponse { } export interface PushTemplate { - created_at: Date; + created_at: TimestampNS; enable_push: boolean; event_type: | 'message.new' @@ -15961,7 +16990,7 @@ export interface PushTemplate { | 'feeds.comment.reaction.added' | 'feeds.follow.created' | 'feeds.notification_feed.updated'; - updated_at: Date; + updated_at: TimestampNS; template?: string; } @@ -15969,7 +16998,7 @@ export interface PushTemplateResponse { /** * Time when the template was created */ - created_at: Date; + created_at: TimestampNS; /** * Whether push notification is enabled for this event */ @@ -15985,7 +17014,7 @@ export interface PushTemplateResponse { /** * Time when the template was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * The push notification template */ @@ -16366,8 +17395,8 @@ export interface QueryCallSessionParticipantStatsResponse { duration: string; participants: Array; counts: CallStatsParticipantCounts; - call_ended_at?: Date; - call_started_at?: Date; + call_ended_at?: TimestampNS; + call_started_at?: TimestampNS; next?: string; prev?: string; tmp_data_source?: string; @@ -16420,12 +17449,12 @@ export interface QueryCallStatsMapResponse { */ duration: string; counts: CallStatsParticipantCounts; - call_ended_at?: Date; - call_started_at?: Date; + call_ended_at?: TimestampNS; + call_started_at?: TimestampNS; data_source?: string; - end_time?: Date; - generated_at?: Date; - start_time?: Date; + end_time?: TimestampNS; + generated_at?: TimestampNS; + start_time?: TimestampNS; publishers?: CallStatsMapPublishers; sfus?: CallStatsMapSFUs; subscribers?: CallStatsMapSubscribers; @@ -16544,11 +17573,11 @@ export interface QueryChannelBansResponse { export interface QueryChannelsDefaults { /** - * Number of channels returned when `limit` is omitted. + * Number of channels returned when `limit` is omitted. A request asking for at most one message and one member per channel can return more, up to the higher ceiling described on `max_limit`. */ limit: number; /** - * Largest number of channels returned for one request. A bigger `limit` is silently clamped to this value rather than rejected. An app with no per-app override gets a higher ceiling for the narrow case of a request asking for at most one message and one member per channel. + * Largest number of channels returned for one request. A bigger `limit` is silently clamped to this value rather than rejected. An app with no per-app `max_limit` gets a higher ceiling for the narrow case of a request asking for at most one message and one member per channel. */ max_limit: number; /** @@ -16794,7 +17823,7 @@ export interface QueryFeedModerationTemplate { /** * When the template was created */ - created_at: Date; + created_at: TimestampNS; /** * Name of the moderation template */ @@ -16802,7 +17831,7 @@ export interface QueryFeedModerationTemplate { /** * When the template was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Configuration for a feeds moderation template */ @@ -16877,7 +17906,7 @@ export interface QueryFeedsUsageStatsResponse { export interface QueryFollowsRequest { /** - * When true (server-side only), the response includes group_counts: accepted following counts keyed by the followed feed's group. Requires exactly one source_feed / source_fid string equality. HTTP 400 if target_feed appears, if two source FIDs are given, or if $in / $or / $nor mentions those keys. Independent of paging and of other filters, including status. + * When true (server-side only), the response includes group_counts: accepted following counts keyed by the followed feed's group. Requires exactly one source_feed / source_fid string equality. HTTP 400 if target_feed appears, if two source FIDs are given, or if $in / $or mentions those keys. Independent of paging and of other filters, including status. */ include_group_counts?: boolean; limit?: number; @@ -17134,6 +18163,40 @@ export interface QueryModerationConfigsResponse { prev?: string; } +export interface QueryModerationFeedbackRequest { + /** + * Maximum number of matching items returned per submission, 1 to 50; all by default + */ + item_limit?: number; + limit?: number; + next?: string; + prev?: string; + user_id?: string; + /** + * Sorting parameters: created_at only, newest first by default + */ + sort?: Array; + /** + * Filter conditions + */ + filter?: Record; + /** + * User request object + */ + user?: UserRequest; +} + +export interface QueryModerationFeedbackResponse { + duration: string; + /** + * Feedback submissions with at least one item matching the filter, each with its matching items + */ + submissions: Array; + next?: string; + prev?: string; + filter_config?: FilterConfigResponse; +} + export interface QueryModerationFlagsRequest { limit?: number; next?: string; @@ -17852,13 +18915,15 @@ export interface RankingConfig { */ exclude_below?: number; /** - * Scoring formula. Required when type is 'expression' or 'interest' + * Scoring formula. Required when type is 'expression' (unless for_you.preset is set) or 'interest' */ score?: string; + arrange?: ArrangeConfig; /** * Default values for ranking */ defaults?: Record; + for_you?: ForYouConfig; /** * Decay functions configuration */ @@ -17866,7 +18931,10 @@ export interface RankingConfig { } export interface RawRecordSettings { - mode: 'available' | 'disabled' | 'auto-on'; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; audio_only?: boolean; } @@ -17878,7 +18946,7 @@ export interface RawRecordingSettingsRequest { /** * Recording mode. One of: available, disabled, auto-on */ - mode: 'available' | 'disabled' | 'auto-on'; + mode: FeatureActivationMode; /** * If true, only audio tracks will be recorded */ @@ -17886,17 +18954,20 @@ export interface RawRecordingSettingsRequest { } export interface RawRecordingSettingsResponse { - mode: 'available' | 'disabled' | 'auto-on'; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; audio_only?: boolean; } export interface Reaction { activity_id: string; - created_at: Date; + created_at: TimestampNS; kind: string; - updated_at: Date; + updated_at: TimestampNS; user_id: string; - deleted_at?: Date; + deleted_at?: TimestampNS; id?: string; parent?: string; score?: number; @@ -17914,7 +18985,7 @@ export interface ReactionDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -17945,7 +19016,7 @@ export interface ReactionDeletedEvent { */ cid?: string; message_id?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -17971,11 +19042,11 @@ export interface ReactionGroupResponse { /** * FirstReactionAt is the time of the first reaction of this type. This is the same also if all reaction of this type are deleted, because if someone will react again with the same type, will be preserved the sorting. */ - first_reaction_at: Date; + first_reaction_at: TimestampNS; /** * LastReactionAt is the time of the last reaction of this type. */ - last_reaction_at: Date; + last_reaction_at: TimestampNS; /** * SumScores is the sum of all scores of reactions of this type. Medium allows you to clap articles more than once and shows the sum of all claps from all users. For example, you can send `clap` x5 using `score: 5`. */ @@ -17990,7 +19061,7 @@ export interface ReactionGroupUserResponse { /** * The time when the user reacted. */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the user who reacted. */ @@ -18005,7 +19076,7 @@ export interface ReactionNewEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Represents channel in chat */ @@ -18036,7 +19107,7 @@ export interface ReactionNewEvent { */ cid?: string; message_id?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -18083,7 +19154,7 @@ export interface ReactionResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Message ID */ @@ -18099,7 +19170,7 @@ export interface ReactionResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * User ID */ @@ -18118,7 +19189,7 @@ export interface ReactionUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; message_id: string; /** * Represents channel in chat @@ -18153,7 +19224,7 @@ export interface ReactionUpdatedEvent { * The CID of the channel containing the message */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * The team ID */ @@ -18226,13 +19297,13 @@ export interface ReadReceiptsResponse { } export interface ReadStateResponse { - last_read: Date; + last_read: TimestampNS; unread_messages: number; /** * User response object */ user: UserResponse; - last_delivered_at?: Date; + last_delivered_at?: TimestampNS; last_delivered_message_id?: string; last_read_message_id?: string; } @@ -18248,7 +19319,7 @@ export interface RecordSettingsRequest { /** * Recording mode. One of: available, disabled, auto-on */ - mode: 'available' | 'disabled' | 'auto-on'; + mode: FeatureActivationMode; /** * Whether to record audio only */ @@ -18272,11 +19343,16 @@ export interface RecordSettingsRequest { export interface RecordSettingsResponse { audio_only: boolean; - mode: string; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; quality: string; layout: LayoutSettingsResponse; } +export type RecordingType = 'composite' | 'individual' | 'raw'; + export interface RejectAppealRequestPayload { /** * Reason for rejecting the appeal @@ -18321,7 +19397,7 @@ export interface ReminderCreatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the message for which the reminder was created */ @@ -18340,7 +19416,7 @@ export interface ReminderCreatedEvent { * The ID of the parent message, if the reminder is for a thread message */ parent_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ReminderDeletedEvent { @@ -18351,7 +19427,7 @@ export interface ReminderDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the message for which the reminder was created */ @@ -18370,7 +19446,7 @@ export interface ReminderDeletedEvent { * The ID of the parent message, if the reminder is for a thread message */ parent_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ReminderNotificationEvent { @@ -18381,7 +19457,7 @@ export interface ReminderNotificationEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the message for which the reminder was created */ @@ -18397,17 +19473,17 @@ export interface ReminderNotificationEvent { */ type: string; parent_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ReminderResponseData { channel_cid: string; - created_at: Date; + created_at: TimestampNS; message_id: string; - updated_at: Date; + updated_at: TimestampNS; user_id: string; - expires_at?: Date; - remind_at?: Date; + expires_at?: TimestampNS; + remind_at?: TimestampNS; /** * Represents channel in chat */ @@ -18430,7 +19506,7 @@ export interface ReminderUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the message for which the reminder was created */ @@ -18449,7 +19525,7 @@ export interface ReminderUpdatedEvent { * The ID of the parent message, if the reminder is for a thread message */ parent_id?: string; - received_at?: Date; + received_at?: TimestampNS; } export interface RemoveUserGroupMembersRequest { @@ -18649,13 +19725,17 @@ export interface RestoreUsersRequest { user_ids: Array; } +export type RestrictReplies = 'everyone' | 'people_i_follow' | 'nobody'; + export interface RetentionPolicy { app_pk: number; - enabled_at: Date; + enabled_at: TimestampNS; policy: string; config: PolicyConfig; } +export type RetentionPolicyType = 'old-messages' | 'inactive-channels'; + export interface RetentionRunResponse { app_pk: number; date: string; @@ -18663,11 +19743,31 @@ export interface RetentionRunResponse { stats: RunStats; } +export interface RevealMediaRequest { + /** + * ID of the review queue item whose media is revealed + */ + item_id: string; + user_id?: string; + /** + * User request object + */ + user?: UserRequest; +} + +export interface RevealMediaResponse { + duration: string; + /** + * Display policy for a review queue item's images and video + */ + media_display: MediaDisplayResponse; +} + export interface ReviewQueueItemNewEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; /** * The flags associated with this review queue item */ @@ -18684,7 +19784,7 @@ export interface ReviewQueueItemResponse { /** * When the item was created */ - created_at: Date; + created_at: TimestampNS; /** * ID of the entity being reviewed */ @@ -18722,7 +19822,7 @@ export interface ReviewQueueItemResponse { /** * When the item was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Moderation actions taken */ @@ -18739,10 +19839,14 @@ export interface ReviewQueueItemResponse { * Detected languages in the content */ languages: Array; + /** + * Display policy for a review queue item's images and video + */ + media_display: MediaDisplayResponse; /** * When the review was completed */ - completed_at?: Date; + completed_at?: TimestampNS; /** * Highest per-label confidence (0-1) any provider reported across the item's flags; absent when no flag carried one */ @@ -18756,7 +19860,7 @@ export interface ReviewQueueItemResponse { /** * When the item was escalated */ - escalated_at?: Date; + escalated_at?: TimestampNS; /** * ID of the moderator who escalated the item */ @@ -18764,7 +19868,12 @@ export interface ReviewQueueItemResponse { /** * When the item was reviewed */ - reviewed_at?: Date; + reviewed_at?: TimestampNS; + country_codes?: Array; + /** + * IDs of the item's flags a moderator marked as false positives + */ + false_positive_flags?: Array; /** * Teams associated with this item */ @@ -18792,10 +19901,10 @@ export interface ReviewQueueItemResponse { } export interface ReviewQueueItemUpdatedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; - received_at?: Date; + received_at?: TimestampNS; /** * The flags associated with this review queue item */ @@ -18807,7 +19916,7 @@ export interface ReviewQueueItemUpdatedEvent { export interface RevisionHistoryResponse { action_type: string; actor_type: string; - created_at: Date; + created_at: TimestampNS; object_id: string; object_type: string; user_id: string; @@ -18815,6 +19924,41 @@ export interface RevisionHistoryResponse { previous_obj_serialized?: Record; } +export interface RewindChannelRequest { + /** + * ID of the newest visible message the caller saw. The rewind fails with a conflict when it is no longer the newest + */ + expected_last_message_id: string; + /** + * ID of the top-level message to rewind the channel to. Every message created after it is hidden + */ + message_id: string; + /** + * When true, `message_id` is hidden too and the message before it becomes the newest visible message + */ + inclusive?: boolean; + user_id?: string; + /** + * User request object + */ + user?: UserRequest; +} + +export interface RewindChannelResponse { + /** + * Duration of the request in milliseconds + */ + duration: string; + /** + * A rewind of an agent channel + */ + rewind: ChannelRewindResponse; + /** + * Represents channel in chat + */ + channel?: ChannelResponse; +} + export interface RingAttemptResponse { /** * The application this ring belongs to @@ -18831,11 +19975,11 @@ export interface RingAttemptResponse { /** * When the ring went out */ - created_at: Date; + created_at: TimestampNS; /** * When the ring expires, i.e. when the missed call deadline fires */ - expires_at: Date; + expires_at: TimestampNS; /** * The user that started the ring */ @@ -18871,11 +20015,11 @@ export interface RingAttemptResponse { /** * When the ring was deleted */ - deleted_at?: Date; + deleted_at?: TimestampNS; /** * When the outcome left ringing */ - settled_at?: Date; + settled_at?: TimestampNS; } export interface RingCallRequest { @@ -18913,11 +20057,11 @@ export interface RingDeliveryResponse { /** * When the ring was dispatched */ - sent_at: Date; + sent_at: TimestampNS; /** * When the device acknowledged the ring; not populated yet */ - acked_at?: Date; + acked_at?: TimestampNS; /** * The device the ring was dispatched to, empty for websocket deliveries which fan out to every connection */ @@ -18925,15 +20069,15 @@ export interface RingDeliveryResponse { /** * When the device displayed the ring; not populated yet */ - displayed_at?: Date; + displayed_at?: TimestampNS; /** * When the device fetched the call; not populated yet */ - fetched_at?: Date; + fetched_at?: TimestampNS; /** * When the push provider accepted the dispatch; not populated yet */ - provider_accepted_at?: Date; + provider_accepted_at?: TimestampNS; } export interface RingSettings { @@ -18965,7 +20109,7 @@ export interface RingSettingsResponse { export interface RingSettledTarget { state: string; - state_at: Date; + state_at: TimestampNS; reason?: string; } @@ -18981,7 +20125,7 @@ export interface RingTargetResponse { /** * When the target last changed state */ - state_at: Date; + state_at: TimestampNS; /** * The dispatches of this ring to the target's devices */ @@ -18992,7 +20136,7 @@ export interface Role { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Whether this is a custom role or built-in */ @@ -19004,7 +20148,7 @@ export interface Role { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * List of scopes where this role is currently present. `.app` means that role is present in app-level grants */ @@ -19040,6 +20184,14 @@ export interface RuleBuilderAction { export interface RuleBuilderCondition { confidence?: number; type?: string; + /** + * app_channel_count params: fires when the app creates at least threshold distinct channels within time_window + */ + app_channel_count_params?: AppChannelCountParameters; + /** + * app_message_count params: fires when the app sends at least threshold chat messages within time_window + */ + app_message_count_params?: AppMessageCountParameters; call_custom_property_params?: CallCustomPropertyParameters; call_type_rule_params?: CallTypeRuleParameters; call_violation_count_params?: CallViolationCountParameters; @@ -19361,7 +20513,7 @@ export interface SIPInboundRoutingRuleResponse { /** * Creation timestamp */ - created_at: Date; + created_at: TimestampNS; duration: string; /** * Unique identifier of the SIP Inbound Routing Rule @@ -19374,7 +20526,7 @@ export interface SIPInboundRoutingRuleResponse { /** * Last update timestamp */ - updated_at: Date; + updated_at: TimestampNS; /** * List of called numbers */ @@ -19451,7 +20603,7 @@ export interface SIPTrunkResponse { /** * Creation timestamp */ - created_at: Date; + created_at: TimestampNS; /** * Unique identifier for the SIP trunk */ @@ -19467,7 +20619,7 @@ export interface SIPTrunkResponse { /** * Last update timestamp */ - updated_at: Date; + updated_at: TimestampNS; /** * The URI for the SIP trunk */ @@ -19542,7 +20694,7 @@ export interface ScheduledMessageContentResponse { /** * Date/time the pin expires */ - pin_expires?: Date; + pin_expires?: TimestampNS; /** * Whether the message will be pinned when sent */ @@ -19594,7 +20746,7 @@ export interface ScheduledMessageFailedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponseCommonFields; /** @@ -19605,13 +20757,26 @@ export interface ScheduledMessageFailedEvent { * The CID of the channel the message is scheduled in */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * A message scheduled to be sent later */ scheduled_message?: ScheduledMessageResponse; } +export type ScheduledMessageFailureReason = + | 'banned' + | 'no_permission' + | 'channel_frozen' + | 'channel_deleted' + | 'user_deleted' + | 'moderation_blocked' + | 'hook_rejected' + | 'expired' + | 'internal_error' + | 'invalid_message' + | 'pending_not_supported'; + export interface ScheduledMessageLocation { /** * The ID of the device that created the location. @@ -19621,7 +20786,7 @@ export interface ScheduledMessageLocation { * The date when the location sharing ends. * If it's empty, it means the location sharing is static instead of live. */ - end_at?: Date; + end_at?: TimestampNS; /** * The latitude of the location. */ @@ -19632,6 +20797,8 @@ export interface ScheduledMessageLocation { longitude?: number; } +export type ScheduledMessageQueryStatus = 'scheduled' | 'sending' | 'failed'; + export interface ScheduledMessageResponse { /** * The CID of the channel the message will be sent to @@ -19640,7 +20807,7 @@ export interface ScheduledMessageResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Unique identifier of the scheduled message */ @@ -19648,15 +20815,15 @@ export interface ScheduledMessageResponse { /** * Date/time the message is scheduled to be sent */ - scheduled_at: Date; + scheduled_at: TimestampNS; /** * Status of the scheduled message. One of: scheduled, sending, failed, canceled, sent. A message is sending from its first send attempt until it is sent or fails, including while it waits to retry a failed attempt; it cannot be edited, canceled or sent now while sending */ - status: string; + status: ScheduledMessageStatus; /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * The message a scheduled message will send */ @@ -19668,7 +20835,7 @@ export interface ScheduledMessageResponse { /** * Why sending the message failed. Only set when status is failed. One of: banned, no_permission, channel_frozen, channel_deleted, user_deleted, moderation_blocked, hook_rejected, expired, internal_error, invalid_message, pending_not_supported */ - failure_reason?: string; + failure_reason?: ScheduledMessageFailureReason; /** * The channel of a scheduled message */ @@ -19679,7 +20846,7 @@ export interface ScheduledMessageSentEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the message that was sent */ @@ -19694,13 +20861,16 @@ export interface ScheduledMessageSentEvent { * The CID of the channel the message was sent to */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * A message scheduled to be sent later */ scheduled_message?: ScheduledMessageResponse; } +export type ScheduledMessageStatus = + 'scheduled' | 'sending' | 'failed' | 'canceled' | 'sent'; + export interface ScheduledMessagesFilter { /** * Only scheduled messages of this channel @@ -19709,7 +20879,7 @@ export interface ScheduledMessagesFilter { /** * Only these statuses, as reported in each scheduled message's status. One or more of: scheduled, sending, failed */ - status?: Array; + status?: Array; } export interface ScoreBands { @@ -19796,7 +20966,7 @@ export interface SearchResult { export interface SearchResultMessage { cid: string; - created_at: Date; + created_at: TimestampNS; deleted_reply_count: number; html: string; id: string; @@ -19808,7 +20978,7 @@ export interface SearchResultMessage { silent: boolean; text: string; type: string; - updated_at: Date; + updated_at: TimestampNS; attachments: Array; latest_reactions: Array; mentioned_users: Array; @@ -19822,13 +20992,13 @@ export interface SearchResultMessage { */ user: UserResponse; command?: string; - deleted_at?: Date; + deleted_at?: TimestampNS; deleted_for_me?: boolean; - message_text_updated_at?: Date; + message_text_updated_at?: TimestampNS; mml?: string; parent_id?: string; - pin_expires?: Date; - pinned_at?: Date; + pin_expires?: TimestampNS; + pinned_at?: TimestampNS; poll_id?: string; quoted_message_id?: string; show_in_channel?: boolean; @@ -19898,13 +21068,13 @@ export interface SearchWarning { export interface Segment { all_sender_channels: boolean; all_users: boolean; - created_at: Date; + created_at: TimestampNS; id: string; name: string; size: number; type: string; - updated_at: Date; - deleted_at?: Date; + updated_at: TimestampNS; + deleted_at?: TimestampNS; description?: string; task_id?: string; filter?: Record; @@ -19913,20 +21083,20 @@ export interface Segment { export interface SegmentResponse { all_sender_channels: boolean; all_users: boolean; - created_at: Date; - deleted_at: Date; + created_at: TimestampNS; + deleted_at: TimestampNS; description: string; id: string; name: string; size: number; type: string; - updated_at: Date; + updated_at: TimestampNS; filter: Record; } export interface SegmentTargetResponse { app_pk: number; - created_at: Date; + created_at: TimestampNS; segment_id: string; target_id: string; } @@ -19996,7 +21166,7 @@ export interface SendMessageRequest { keep_channel_hidden?: boolean; pending?: boolean; /** - * When set, the message is not sent now: it is stored and sent at this time (UTC). The response then carries scheduled_message. Must be at least 1 minute and at most the app's maximum lead time (120 days by default) from now. Slash commands cannot be scheduled + * When set to a future time, the message is not sent now: it is stored and sent at that time (UTC), rounded up to the first whole minute that is at least 1 minute from now, never earlier than requested. The response then carries scheduled_message with the rounded time. Must be at most the app's maximum lead time (120 days by default) from now. A time that is now or in the past sends the message now as if scheduled_at was not set, unless the request's message ID belongs to a scheduled message that was canceled or already sent. Slash commands cannot be scheduled */ scheduled_at?: Date; skip_enrich_url?: boolean; @@ -20096,7 +21266,7 @@ export interface SessionSettingsResponse { export interface SessionWarningResponse { code: string; warning: string; - time?: Date; + time?: TimestampNS; } export interface SetFeedsRetentionPolicyRequest { @@ -20115,9 +21285,30 @@ export interface SetFeedsRetentionPolicyResponse { policy: FeedsRetentionPolicy; } +export interface SetHistoryWindowTeamsRequest { + /** + * Teams to give full history, 1 to 10, each listed once. Marking a team that already has full history keeps its created_at + */ + teams: Array; +} + +export interface SetHistoryWindowTeamsResponse { + /** + * Duration of the request in milliseconds + */ + duration: string; + /** + * The teams of the request, each with full history + */ + teams: Array; +} + export interface SetRetentionPolicyRequest { max_age_hours: number; - policy: 'old-messages' | 'inactive-channels'; + /** + * What a retention policy applies to. + */ + policy: RetentionPolicyType; } export interface SetRetentionPolicyResponse { @@ -20129,14 +21320,24 @@ export interface SetRetentionPolicyResponse { } export interface SetupSession { - created_at: Date; - current_step: string; - status: string; - updated_at: Date; + created_at: TimestampNS; + /** + * A step in the moderation setup wizard. + */ + current_step: SetupStep; + /** + * Whether a moderation setup session is still in progress. + */ + status: SetupStatus; + updated_at: TimestampNS; setup_data: Record; - completed_at?: Date; + completed_at?: TimestampNS; } +export type SetupStatus = 'in_progress' | 'completed'; + +export type SetupStep = 'welcome' | 'input' | 'configure' | 'live'; + export interface ShadowBlockActionRequestPayload { /** * Reason for shadow blocking @@ -20152,7 +21353,7 @@ export interface ShareResponse { /** * When the share occurred */ - created_at: Date; + created_at: TimestampNS; /** * User response object */ @@ -20174,7 +21375,7 @@ export interface SharedLocationResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Device ID that created the live location */ @@ -20195,7 +21396,7 @@ export interface SharedLocationResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * User ID */ @@ -20203,7 +21404,7 @@ export interface SharedLocationResponse { /** * Time when the live location expires */ - end_at?: Date; + end_at?: TimestampNS; /** * Represents channel in chat */ @@ -20222,7 +21423,7 @@ export interface SharedLocationResponseData { /** * The date when the location was created. */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the device that created the location. */ @@ -20242,7 +21443,7 @@ export interface SharedLocationResponseData { /** * The date when the location was updated. */ - updated_at: Date; + updated_at: TimestampNS; /** * The ID of the user that created the location. */ @@ -20251,7 +21452,7 @@ export interface SharedLocationResponseData { * The date when the location sharing ends. * If it's empty, it means the location sharing is static instead of live. */ - end_at?: Date; + end_at?: TimestampNS; /** * Represents channel in chat */ @@ -20340,9 +21541,11 @@ export interface SortParamRequest { /** * Type of field to sort by. Empty string or omitted means string type (default). One of: number, boolean */ - type?: string; + type?: SortParamType; } +export type SortParamType = '' | 'number' | 'boolean'; + export interface SourceHealth { co_host_peak: number; dead_air_s: number; @@ -20382,50 +21585,13 @@ export interface StartClosedCaptionsRequest { */ enable_transcription?: boolean; /** - * Which external storage to use for transcriptions (only applicable if enable_transcription is true) - */ - external_storage?: string; - /** - * The spoken language in the call, if not provided the language defined in the transcription settings will be used. One of: auto, ar, bg, ca, cs, da, de, el, en, es, et, fi, fr, he, hi, hr, hu, id, it, ja, ko, ms, nl, no, pl, pt, ro, ru, sk, sl, sv, ta, th, tl, tr, uk, zh - */ - language?: - | 'auto' - | 'en' - | 'fr' - | 'es' - | 'de' - | 'it' - | 'nl' - | 'pt' - | 'pl' - | 'ca' - | 'cs' - | 'da' - | 'el' - | 'fi' - | 'id' - | 'ja' - | 'ru' - | 'sv' - | 'ta' - | 'th' - | 'tr' - | 'hu' - | 'ro' - | 'zh' - | 'ar' - | 'tl' - | 'he' - | 'hi' - | 'hr' - | 'ko' - | 'ms' - | 'no' - | 'uk' - | 'bg' - | 'et' - | 'sl' - | 'sk'; + * Which external storage to use for transcriptions (only applicable if enable_transcription is true) + */ + external_storage?: string; + /** + * The spoken language in the call, if not provided the language defined in the transcription settings will be used. One of: auto, ar, bg, ca, cs, da, de, el, en, es, et, fi, fr, he, hi, hr, hu, id, it, ja, ko, ms, nl, no, pl, pt, ro, ru, sk, sl, sv, ta, th, tl, tr, uk, zh + */ + language?: TranscriptionLanguage; speech_segment_config?: SpeechSegmentConfig; } @@ -20485,44 +21651,7 @@ export interface StartTranscriptionRequest { /** * The spoken language in the call, if not provided the language defined in the transcription settings will be used. One of: auto, ar, bg, ca, cs, da, de, el, en, es, et, fi, fr, he, hi, hr, hu, id, it, ja, ko, ms, nl, no, pl, pt, ro, ru, sk, sl, sv, ta, th, tl, tr, uk, zh */ - language?: - | 'auto' - | 'en' - | 'fr' - | 'es' - | 'de' - | 'it' - | 'nl' - | 'pt' - | 'pl' - | 'ca' - | 'cs' - | 'da' - | 'el' - | 'fi' - | 'id' - | 'ja' - | 'ru' - | 'sv' - | 'ta' - | 'th' - | 'tr' - | 'hu' - | 'ro' - | 'zh' - | 'ar' - | 'tl' - | 'he' - | 'hi' - | 'hr' - | 'ko' - | 'ms' - | 'no' - | 'uk' - | 'bg' - | 'et' - | 'sl' - | 'sk'; + language?: TranscriptionLanguage; /** * Store transcriptions in this external storage */ @@ -20639,7 +21768,7 @@ export interface StoriesFeedUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The ID of the feed */ @@ -20649,8 +21778,11 @@ export interface StoriesFeedUpdatedEvent { * The type of event: "feeds.stories_feed.updated" in this case */ type: string; - feed_visibility?: string; - received_at?: Date; + /** + * Who can see and follow a feed. + */ + feed_visibility?: FeedVisibility; + received_at?: TimestampNS; /** * Individual activities for stories feeds */ @@ -20796,23 +21928,46 @@ export interface SubmitActionResponse { item?: ReviewQueueItemResponse; } -export interface SubmitModerationFeedbackRequest { +export interface SubmitModerationFeedbackItem { /** - * The moderated content the moderator is providing feedback on + * Action originally produced for this item */ - message: string; + current_recommended_action?: string; + /** + * ID of the moderation flag the feedback is about + */ + flag_id?: string; + /** + * Image URL, required for kind=image + */ + image_url?: string; + /** + * Content kind: text or image + */ + kind?: 'text' | 'image'; /** - * Original publication time of the moderated content (RFC3339) + * Identifies this piece of content within the entity (keyframe ID, caption ID, message ID, title, description, thumbnail, ...) */ - published_at: string; + reference?: string; /** - * Provider-side reference identifying the moderated content + * Text content, for kind=text */ - reference: string; + text?: string; + /** + * Classifications originally produced for this item + */ + current_labels?: Array; +} + +export interface SubmitModerationFeedbackRequest { /** * Optional moderation channel UUID for context */ channel_id?: string; + /** + * Moderator's confidence in this feedback (LOW, MEDIUM, HIGH, VERY_HIGH); forwarded to the moderation provider. Optional + */ + confidence?: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'; /** * Action originally produced by the moderation system */ @@ -20821,10 +21976,47 @@ export interface SubmitModerationFeedbackRequest { * Optional free-form note explaining why the classification was wrong */ description?: string; + /** + * ID of the user who created the entity + */ + entity_creator_id?: string; + /** + * ID of the entity the items belong to + */ + entity_id?: string; + /** + * Type of the entity the items belong to + */ + entity_type?: string; /** * Optional moderator-supplied action */ expected_recommended_action?: string; + /** + * Whether the referenced flags are marked (true, default) or unmarked (false) as false positives + */ + false_positive?: boolean; + /** + * false_positive (default) disputes existing detections; false_negative reports missed content: items carry the content, expected_labels is required and flag_id is not allowed + */ + feedback_type?: 'false_positive' | 'false_negative'; + /** + * The moderated content the moderator is providing feedback on. Required unless items is provided + */ + message?: string; + /** + * Original publication time of the moderated content (RFC3339). Required unless items is provided + */ + published_at?: string; + /** + * Provider-side reference identifying the moderated content. Required unless items is provided + */ + reference?: string; + /** + * Review queue item the feedback targets, as an alternative to entity_type and entity_id + */ + review_queue_item_id?: string; + user_id?: string; /** * Classifications originally produced by the moderation system */ @@ -20833,10 +22025,34 @@ export interface SubmitModerationFeedbackRequest { * Optional moderator-supplied classifications (up to 16 entries) */ expected_labels?: Array; + /** + * Pieces of content of one entity the feedback is about (1 to 50) + */ + items?: Array; + /** + * User request object + */ + user?: UserRequest; } export interface SubmitModerationFeedbackResponse { duration: string; + /** + * ID of the first stored feedback item, when any was stored + */ + feedback_id?: string; + /** + * Review queue item the feedback was applied to + */ + review_queue_item_id?: string; + /** + * IDs of the item's flags marked as false positives after this request + */ + false_positive_flags?: Array; + /** + * IDs of the stored feedback items, one per submitted item + */ + feedback_item_ids?: Array; } export interface SubscriberAllMetrics { @@ -20978,14 +22194,14 @@ export interface ThreadParticipant { /** * Date/time of creation */ - created_at: Date; - last_read_at: Date; + created_at: TimestampNS; + last_read_at: TimestampNS; custom: Record; - last_thread_message_at?: Date; + last_thread_message_at?: TimestampNS; /** * Left Thread At is the time when the user left the thread */ - left_thread_at?: Date; + left_thread_at?: TimestampNS; /** * Thead ID is unique string identifier of the thread */ @@ -21012,7 +22228,7 @@ export interface ThreadResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Created By User ID */ @@ -21036,7 +22252,7 @@ export interface ThreadResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; /** * Custom data for this object */ @@ -21044,11 +22260,11 @@ export interface ThreadResponse { /** * Deleted At */ - deleted_at?: Date; + deleted_at?: TimestampNS; /** * Last Message At */ - last_message_at?: Date; + last_message_at?: TimestampNS; /** * Thread Participants */ @@ -21079,7 +22295,7 @@ export interface ThreadStateResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Created By User ID */ @@ -21103,7 +22319,7 @@ export interface ThreadStateResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; latest_replies: Array; /** * Custom data for this object @@ -21112,11 +22328,11 @@ export interface ThreadStateResponse { /** * Deleted At */ - deleted_at?: Date; + deleted_at?: TimestampNS; /** * Last Message At */ - last_message_at?: Date; + last_message_at?: TimestampNS; read?: Array; /** * Thread Participants @@ -21138,20 +22354,20 @@ export interface ThreadStateResponse { } export interface ThreadUpdatedEvent { - created_at: Date; + created_at: TimestampNS; custom: Record; type: string; channel_id?: string; channel_type?: string; cid?: string; - received_at?: Date; + received_at?: TimestampNS; thread?: ThreadResponse; } export interface ThreadedCommentResponse { bookmark_count: number; confidence_score: number; - created_at: Date; + created_at: TimestampNS; downvote_count: number; hidden: boolean; id: string; @@ -21163,8 +22379,8 @@ export interface ThreadedCommentResponse { /** * Status of the comment. One of: active, deleted, removed, hidden */ - status: 'active' | 'deleted' | 'removed' | 'hidden' | 'shadow_blocked'; - updated_at: Date; + status: CommentStatus; + updated_at: TimestampNS; upvote_count: number; mentioned_users: Array; own_reactions: Array; @@ -21173,8 +22389,8 @@ export interface ThreadedCommentResponse { */ user: UserResponse; controversy_score?: number; - deleted_at?: Date; - edited_at?: Date; + deleted_at?: TimestampNS; + edited_at?: TimestampNS; parent_id?: string; text?: string; attachments?: Array; @@ -21286,143 +22502,92 @@ export interface TrackStatsResponse { track_type: string; } +export type TranscriptionLanguage = + | 'auto' + | 'en' + | 'fr' + | 'es' + | 'de' + | 'it' + | 'nl' + | 'pt' + | 'pl' + | 'ca' + | 'cs' + | 'da' + | 'el' + | 'fi' + | 'id' + | 'ja' + | 'ru' + | 'sv' + | 'ta' + | 'th' + | 'tr' + | 'hu' + | 'ro' + | 'zh' + | 'ar' + | 'tl' + | 'he' + | 'hi' + | 'hr' + | 'ko' + | 'ms' + | 'no' + | 'uk' + | 'bg' + | 'et' + | 'sl' + | 'sk'; + export interface TranscriptionSettings { /** * One of: available, disabled, auto-on */ - closed_caption_mode: 'available' | 'disabled' | 'auto-on'; + closed_caption_mode: FeatureActivationMode; /** * The language used in this call as a two letter code */ - language: - | 'auto' - | 'en' - | 'fr' - | 'es' - | 'de' - | 'it' - | 'nl' - | 'pt' - | 'pl' - | 'ca' - | 'cs' - | 'da' - | 'el' - | 'fi' - | 'id' - | 'ja' - | 'ru' - | 'sv' - | 'ta' - | 'th' - | 'tr' - | 'hu' - | 'ro' - | 'zh' - | 'ar' - | 'tl' - | 'he' - | 'hi' - | 'hr' - | 'ko' - | 'ms' - | 'no' - | 'uk' - | 'bg' - | 'et' - | 'sl' - | 'sk'; - mode: 'available' | 'disabled' | 'auto-on'; + language: TranscriptionLanguage; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; speech_segment_config?: SpeechSegmentConfig; translation?: TranslationSettings; } export interface TranscriptionSettingsRequest { - closed_caption_mode?: 'available' | 'disabled' | 'auto-on'; - language?: - | 'auto' - | 'en' - | 'fr' - | 'es' - | 'de' - | 'it' - | 'nl' - | 'pt' - | 'pl' - | 'ca' - | 'cs' - | 'da' - | 'el' - | 'fi' - | 'id' - | 'ja' - | 'ru' - | 'sv' - | 'ta' - | 'th' - | 'tr' - | 'hu' - | 'ro' - | 'zh' - | 'ar' - | 'tl' - | 'he' - | 'hi' - | 'hr' - | 'ko' - | 'ms' - | 'no' - | 'uk' - | 'bg' - | 'et' - | 'sl' - | 'sk'; - mode?: 'available' | 'disabled' | 'auto-on'; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + closed_caption_mode?: FeatureActivationMode; + /** + * Language a call is transcribed or captioned in. + */ + language?: TranscriptionLanguage; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode?: FeatureActivationMode; speech_segment_config?: SpeechSegmentConfig; translation?: TranslationSettings; } export interface TranscriptionSettingsResponse { - closed_caption_mode: 'available' | 'disabled' | 'auto-on'; - language: - | 'auto' - | 'en' - | 'fr' - | 'es' - | 'de' - | 'it' - | 'nl' - | 'pt' - | 'pl' - | 'ca' - | 'cs' - | 'da' - | 'el' - | 'fi' - | 'id' - | 'ja' - | 'ru' - | 'sv' - | 'ta' - | 'th' - | 'tr' - | 'hu' - | 'ro' - | 'zh' - | 'ar' - | 'tl' - | 'he' - | 'hi' - | 'hr' - | 'ko' - | 'ms' - | 'no' - | 'uk' - | 'bg' - | 'et' - | 'sl' - | 'sk'; - mode: 'available' | 'disabled' | 'auto-on'; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + closed_caption_mode: FeatureActivationMode; + /** + * Language a call is transcribed or captioned in. + */ + language: TranscriptionLanguage; + /** + * Whether a call feature is available, disabled, or turned on automatically. + */ + mode: FeatureActivationMode; speech_segment_config?: SpeechSegmentConfig; translation?: TranslationSettings; } @@ -21683,7 +22848,7 @@ export interface UnblockUsersResponse { export interface UnblockedUserEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * User response object */ @@ -21847,10 +23012,14 @@ export interface UnreadCountsChannel { * The channel CID (type:id). */ channel_id: string; + /** + * Whether the channel has messages newer than the user's last read. Large channels have no unread_count and only report this. + */ + has_unread: boolean; /** * The date which the current user last read the channel. */ - last_read: Date; + last_read: TimestampNS; /** * The number of unread messages inside the channel. */ @@ -21885,7 +23054,7 @@ export interface UnreadCountsThread { /** * The date which the current user last read the thread. */ - last_read: Date; + last_read: TimestampNS; /** * The id of the last reply which the current user read in the thread. */ @@ -22014,7 +23183,7 @@ export interface UpdateActivityRequest { /** * Controls who can add comments/replies to this activity. One of: everyone, people_i_follow, nobody */ - restrict_replies?: 'everyone' | 'people_i_follow' | 'nobody'; + restrict_replies?: RestrictReplies; /** * If true, runs activity processors on the updated activity. Processors will only run if the activity text and/or attachments are changed. Defaults to false. */ @@ -22031,7 +23200,7 @@ export interface UpdateActivityRequest { /** * Visibility setting for the activity */ - visibility?: 'public' | 'private' | 'tag'; + visibility?: ActivityVisibility; /** * If visibility is 'tag', this is the tag name and is required */ @@ -22136,6 +23305,7 @@ export interface UpdateAppRequest { sqs_key?: string; sqs_secret?: string; sqs_url?: string; + two_way_block_effect_enabled?: boolean; user_response_time_enabled?: boolean; video_primary_use_case?: | 'video-calling' @@ -22293,7 +23463,7 @@ export interface UpdateCallTypeResponse { /** * the time the call type was created */ - created_at: Date; + created_at: TimestampNS; duration: string; /** * the name of the call type @@ -22302,7 +23472,7 @@ export interface UpdateCallTypeResponse { /** * the time the call type was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * the permissions granted to each role */ @@ -22322,8 +23492,14 @@ export interface UpdateCampaignRequest { description?: string; id?: string; name?: string; - sender_mode?: 'exclude' | 'include'; - sender_visibility?: 'hidden' | 'archived'; + /** + * Whether a campaign includes or excludes its sender. + */ + sender_mode?: CampaignSenderMode; + /** + * How a campaign appears to its sender. + */ + sender_visibility?: CampaignSenderVisibility; show_channels?: boolean; skip_push?: boolean; skip_webhook?: boolean; @@ -22455,10 +23631,19 @@ export interface UpdateChannelResponse { } export interface UpdateChannelTypeRequest { - automod?: 'disabled' | 'simple' | 'AI'; - automod_behavior?: 'flag' | 'block' | 'shadow_block'; + /** + * Automod level applied to a channel type. + */ + automod?: AutomodMode; + /** + * What automod or a blocklist does when it matches. + */ + automod_behavior?: ChannelModerationBehavior; blocklist?: string; - blocklist_behavior?: 'flag' | 'block' | 'shadow_block'; + /** + * What automod or a blocklist does when it matches. + */ + blocklist_behavior?: ChannelModerationBehavior; connect_events?: boolean; count_messages?: boolean; custom_events?: boolean; @@ -22473,7 +23658,7 @@ export interface UpdateChannelTypeRequest { /** * Sets the push notification level for a channel type */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; push_notifications?: boolean; quotes?: boolean; reactions?: boolean; @@ -22503,11 +23688,17 @@ export interface UpdateChannelTypeRequest { } export interface UpdateChannelTypeResponse { - automod: 'disabled' | 'simple' | 'AI'; - automod_behavior: 'flag' | 'block' | 'shadow_block'; + /** + * Automod level applied to a channel type. + */ + automod: AutomodMode; + /** + * What automod or a blocklist does when it matches. + */ + automod_behavior: ChannelModerationBehavior; connect_events: boolean; count_messages: boolean; - created_at: Date; + created_at: TimestampNS; custom_events: boolean; delivery_events: boolean; /** @@ -22530,7 +23721,7 @@ export interface UpdateChannelTypeResponse { shared_locations: boolean; skip_last_msg_update_for_system_msgs: boolean; typing_events: boolean; - updated_at: Date; + updated_at: TimestampNS; uploads: boolean; url_enrichment: boolean; user_message_reminders: boolean; @@ -22538,13 +23729,16 @@ export interface UpdateChannelTypeResponse { permissions: Array; grants: Record>; blocklist?: string; - blocklist_behavior?: 'flag' | 'block' | 'shadow_block'; + /** + * What automod or a blocklist does when it matches. + */ + blocklist_behavior?: ChannelModerationBehavior; partition_size?: number; partition_ttl?: string; /** * Sets the push notification level for a channel type */ - push_level?: 'all' | 'all_mentions' | 'mentions' | 'direct_mentions' | 'none'; + push_level?: PushLevel; allowed_flag_reasons?: Array; blocklists?: Array; /** @@ -22770,8 +23964,7 @@ export interface UpdateFeedGroupRequest { /** * Default visibility for the feed group. One of: public, visible, followers, members, private */ - default_visibility?: - 'public' | 'visible' | 'followers' | 'members' | 'private'; + default_visibility?: FeedVisibility; /** * Configuration for activity processors */ @@ -22923,7 +24116,7 @@ export interface UpdateFollowRequest { /** * Push preference for the follow relationship */ - push_preference?: 'all' | 'none'; + push_preference?: FollowPushPreference; /** * Whether to skip push for this follow */ @@ -22931,7 +24124,7 @@ export interface UpdateFollowRequest { /** * Status of the follow relationship. One of: accepted, pending, rejected */ - status?: 'accepted' | 'pending' | 'rejected'; + status?: FollowStatus; /** * Custom data for the follow relationship */ @@ -23160,7 +24353,7 @@ export interface UpdatePollRequest { /** * Voting visibility */ - voting_visibility?: 'anonymous' | 'public'; + voting_visibility?: VotingVisibility; /** * Poll options */ @@ -23305,7 +24498,7 @@ export interface UpdateSIPTrunkResponse { export interface UpdateScheduledMessageRequest { /** - * New send time (UTC). Must be at least 1 minute and at most the app's maximum lead time from now + * New send time (UTC). Must be in the future and at most the app's maximum lead time from now. Rounded up to the first whole minute that is at least 1 minute from now, never earlier than requested. Resending the stored time leaves it unchanged */ scheduled_at?: Date; /** @@ -23440,7 +24633,7 @@ export interface UpdateUsersResponse { export interface UpdatedCallPermissionsEvent { call_cid: string; - created_at: Date; + created_at: TimestampNS; /** * The capabilities of the current user */ @@ -23768,7 +24961,7 @@ export interface UpsertModerationTemplateResponse { /** * When the template was created */ - created_at: Date; + created_at: TimestampNS; duration: string; /** * Name of the moderation template @@ -23777,7 +24970,7 @@ export interface UpsertModerationTemplateResponse { /** * When the template was last updated */ - updated_at: Date; + updated_at: TimestampNS; /** * Configuration for a feeds moderation template */ @@ -23839,7 +25032,7 @@ export interface UpsertPushTemplateRequest { /** * Push provider type. One of: firebase, apn, huawei, xiaomi */ - push_provider_type: 'firebase' | 'apn' | 'huawei' | 'xiaomi'; + push_provider_type: PushProviderName; /** * Whether to send push notification for this event */ @@ -23866,11 +25059,11 @@ export interface UpsertSetupSessionRequest { /** * The current step of the setup wizard. One of: welcome, input, configure, live */ - current_step: 'welcome' | 'input' | 'configure' | 'live'; + current_step: SetupStep; /** * The status of the setup session. One of: in_progress, completed */ - status: 'in_progress' | 'completed'; + status: SetupStatus; /** * Per-step data keyed by step name (welcome, input, configure, live) */ @@ -23906,7 +25099,7 @@ export interface UserBannedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponseCommonFields; /** @@ -23930,12 +25123,12 @@ export interface UserBannedEvent { /** * The expiration date of the ban */ - expiration?: Date; + expiration?: TimestampNS; /** * The reason for the ban */ reason?: string; - received_at?: Date; + received_at?: TimestampNS; /** * ID of the review queue item (flagged message) that triggered the ban, if the ban was applied from the moderation review queue */ @@ -23964,6 +25157,21 @@ export interface UserChannelCountRuleParameters { time_window?: string; } +export interface UserClaimedEvent { + /** + * Date/time of creation + */ + created_at: TimestampNS; + claimed_by: UserResponseCommonFields; + custom: Record; + user: UserResponseCommonFields; + /** + * The type of event: "user.claimed" in this case + */ + type: string; + received_at?: TimestampNS; +} + export interface UserCreatedWithinParameters { max_age?: string; } @@ -23982,14 +25190,14 @@ export interface UserDeactivatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponseCommonFields; /** * The type of event: "user.deactivated" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; created_by?: UserResponseCommonFields; } @@ -23997,7 +25205,7 @@ export interface UserDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The type of deletion that was used for the user's conversations. One of: hard, soft, pruning, (empty string) */ @@ -24028,7 +25236,7 @@ export interface UserDeletedEvent { * The type of event: "user.deleted" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface UserFeedbackReport { @@ -24056,7 +25264,7 @@ export interface UserFlaggedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The reason for the flag */ @@ -24070,7 +25278,7 @@ export interface UserFlaggedEvent { * The type of event: "user.flagged" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; /** * Custom data */ @@ -24080,10 +25288,10 @@ export interface UserFlaggedEvent { export interface UserGroup { app_pk: number; - created_at: Date; + created_at: TimestampNS; id: string; name: string; - updated_at: Date; + updated_at: TimestampNS; created_by?: string; description?: string; team_id?: string; @@ -24094,13 +25302,13 @@ export interface UserGroupCreatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "user_group.created" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; user_group?: UserGroup; } @@ -24109,20 +25317,20 @@ export interface UserGroupDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "user_group.deleted" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; user_group?: UserGroup; } export interface UserGroupMember { app_pk: number; - created_at: Date; + created_at: TimestampNS; group_id: string; is_admin: boolean; user_id: string; @@ -24132,7 +25340,7 @@ export interface UserGroupMemberAddedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The user IDs that were added */ @@ -24142,7 +25350,7 @@ export interface UserGroupMemberAddedEvent { * The type of event: "user_group.member_added" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; user_group?: UserGroup; } @@ -24151,7 +25359,7 @@ export interface UserGroupMemberRemovedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The user IDs that were removed */ @@ -24161,16 +25369,16 @@ export interface UserGroupMemberRemovedEvent { * The type of event: "user_group.member_removed" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; user_group?: UserGroup; } export interface UserGroupResponse { - created_at: Date; + created_at: TimestampNS; id: string; name: string; - updated_at: Date; + updated_at: TimestampNS; created_by?: string; description?: string; team_id?: string; @@ -24181,13 +25389,13 @@ export interface UserGroupUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; /** * The type of event: "user_group.updated" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; user?: UserResponseCommonFields; user_group?: UserGroup; } @@ -24219,7 +25427,7 @@ export interface UserMessagesDeletedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponseCommonFields; /** @@ -24244,7 +25452,7 @@ export interface UserMessagesDeletedEvent { * Whether Messages were hard deleted */ hard_delete?: boolean; - received_at?: Date; + received_at?: TimestampNS; /** * The team of the channel where the target user's messages were deleted */ @@ -24253,9 +25461,9 @@ export interface UserMessagesDeletedEvent { } export interface UserMuteResponse { - created_at: Date; - updated_at: Date; - expires?: Date; + created_at: TimestampNS; + updated_at: TimestampNS; + expires?: TimestampNS; /** * User response object */ @@ -24270,14 +25478,14 @@ export interface UserMutedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponseCommonFields; /** * The type of event: "user.muted" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; /** * The target users that were muted */ @@ -24300,14 +25508,14 @@ export interface UserReactivatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponseCommonFields; /** * The type of event: "user.reactivated" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; created_by?: UserResponseCommonFields; } @@ -24356,7 +25564,7 @@ export interface UserResponse { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * Unique user identifier */ @@ -24381,7 +25589,7 @@ export interface UserResponse { /** * Date/time of the last update */ - updated_at: Date; + updated_at: TimestampNS; blocked_user_ids: Array; /** * List of teams user is a part of @@ -24395,21 +25603,21 @@ export interface UserResponse { /** * Date when ban expires */ - ban_expires?: Date; + ban_expires?: TimestampNS; bypass_moderation?: boolean; /** * Date of deactivation */ - deactivated_at?: Date; + deactivated_at?: TimestampNS; /** * Date/time of deletion */ - deleted_at?: Date; + deleted_at?: TimestampNS; image?: string; /** * Date of last activity */ - last_active?: Date; + last_active?: TimestampNS; /** * Optional name of user */ @@ -24417,7 +25625,7 @@ export interface UserResponse { /** * Revocation date for tokens */ - revoke_tokens_issued_before?: Date; + revoke_tokens_issued_before?: TimestampNS; /** * List of devices user is using */ @@ -24432,44 +25640,44 @@ export interface UserResponse { export interface UserResponseCommonFields { banned: boolean; - created_at: Date; + created_at: TimestampNS; id: string; language: string; online: boolean; role: string; - updated_at: Date; + updated_at: TimestampNS; blocked_user_ids: Array; teams: Array; custom: Record; avg_response_time?: number; - deactivated_at?: Date; - deleted_at?: Date; + deactivated_at?: TimestampNS; + deleted_at?: TimestampNS; image?: string; - last_active?: Date; + last_active?: TimestampNS; name?: string; - revoke_tokens_issued_before?: Date; + revoke_tokens_issued_before?: TimestampNS; teams_role?: Record; } export interface UserResponsePrivacyFields { banned: boolean; - created_at: Date; + created_at: TimestampNS; id: string; language: string; online: boolean; role: string; - updated_at: Date; + updated_at: TimestampNS; blocked_user_ids: Array; teams: Array; custom: Record; avg_response_time?: number; - deactivated_at?: Date; - deleted_at?: Date; + deactivated_at?: TimestampNS; + deleted_at?: TimestampNS; image?: string; invisible?: boolean; - last_active?: Date; + last_active?: TimestampNS; name?: string; - revoke_tokens_issued_before?: Date; + revoke_tokens_issued_before?: TimestampNS; /** * The privacy settings of the user. */ @@ -24490,7 +25698,7 @@ export interface UserUnbannedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponseCommonFields; /** @@ -24511,7 +25719,7 @@ export interface UserUnbannedEvent { * The CID of the channel where the target user was unbanned */ cid?: string; - received_at?: Date; + received_at?: TimestampNS; /** * Whether the target user was shadow unbanned */ @@ -24528,14 +25736,14 @@ export interface UserUnmutedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponseCommonFields; /** * The type of event: "user.unmuted" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; /** * The target users that were unmuted */ @@ -24547,7 +25755,7 @@ export interface UserUnreadReminderEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; /** * The channels with unread messages */ @@ -24558,21 +25766,21 @@ export interface UserUnreadReminderEvent { * The type of event: "user.unread_message_reminder" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface UserUpdatedEvent { /** * Date/time of creation */ - created_at: Date; + created_at: TimestampNS; custom: Record; user: UserResponsePrivacyFields; /** * The type of event: "user.updated" in this case */ type: string; - received_at?: Date; + received_at?: TimestampNS; } export interface ValidateExternalStorageResponse { @@ -24615,6 +25823,8 @@ export interface VideoCallRuleConfig { rules?: Array; } +export type VideoCodec = 'h264' | 'vp8'; + export interface VideoContentParameters { label_operator?: string; harm_labels?: Array; @@ -24648,7 +25858,10 @@ export interface VideoRuleParameters { export interface VideoSettings { access_request_enabled: boolean; camera_default_on: boolean; - camera_facing: 'front' | 'back' | 'external'; + /** + * Which camera a call opens with. + */ + camera_facing: CameraFacing; enabled: boolean; target_resolution: TargetResolution; } @@ -24656,7 +25869,10 @@ export interface VideoSettings { export interface VideoSettingsRequest { access_request_enabled?: boolean; camera_default_on?: boolean; - camera_facing?: 'front' | 'back' | 'external'; + /** + * Which camera a call opens with. + */ + camera_facing?: CameraFacing; enabled?: boolean; target_resolution?: TargetResolution; } @@ -24664,7 +25880,10 @@ export interface VideoSettingsRequest { export interface VideoSettingsResponse { access_request_enabled: boolean; camera_default_on: boolean; - camera_facing: 'front' | 'back' | 'external'; + /** + * Which camera a call opens with. + */ + camera_facing: CameraFacing; enabled: boolean; target_resolution: TargetResolution; } @@ -24685,6 +25904,8 @@ export interface VoteData { option_id?: string; } +export type VotingVisibility = 'anonymous' | 'public'; + export type WHEvent = | ({ type: '*' } & CustomEvent) | ({ type: 'appeal.accepted' } & AppealAcceptedEvent) @@ -24760,10 +25981,12 @@ export type WHEvent = | ({ type: 'campaign.started' } & CampaignStartedEvent) | ({ type: 'channel.created' } & ChannelCreatedEvent) | ({ type: 'channel.deleted' } & ChannelDeletedEvent) + | ({ type: 'channel.forked' } & ChannelForkedEvent) | ({ type: 'channel.frozen' } & ChannelFrozenEvent) | ({ type: 'channel.hidden' } & ChannelHiddenEvent) | ({ type: 'channel.max_streak_changed' } & MaxStreakChangedEvent) | ({ type: 'channel.muted' } & ChannelMutedEvent) + | ({ type: 'channel.rewound' } & ChannelRewoundEvent) | ({ type: 'channel.truncated' } & ChannelTruncatedEvent) | ({ type: 'channel.unfrozen' } & ChannelUnFrozenEvent) | ({ type: 'channel.unmuted' } & ChannelUnmutedEvent) @@ -24815,6 +26038,8 @@ export type WHEvent = | ({ type: 'feeds.comment.reaction.updated' } & CommentReactionUpdatedEvent) | ({ type: 'feeds.comment.restored' } & CommentRestoredEvent) | ({ type: 'feeds.comment.updated' } & CommentUpdatedEvent) + | ({ type: 'feeds.export.error' } & AsyncExportErrorEvent) + | ({ type: 'feeds.export.success' } & AsyncExportFeedsSuccessEvent) | ({ type: 'feeds.feed.created' } & FeedCreatedEvent) | ({ type: 'feeds.feed.deleted' } & FeedDeletedEvent) | ({ type: 'feeds.feed.updated' } & FeedUpdatedEvent) @@ -24881,6 +26106,7 @@ export type WHEvent = | ({ type: 'scheduled_message.sent' } & ScheduledMessageSentEvent) | ({ type: 'thread.updated' } & ThreadUpdatedEvent) | ({ type: 'user.banned' } & UserBannedEvent) + | ({ type: 'user.claimed' } & UserClaimedEvent) | ({ type: 'user.deactivated' } & UserDeactivatedEvent) | ({ type: 'user.deleted' } & UserDeletedEvent) | ({ type: 'user.flagged' } & UserFlaggedEvent) @@ -24905,12 +26131,12 @@ export interface WHIPIngress { } export interface WSEvent { - created_at: Date; + created_at: TimestampNS; type: string; custom: Record; automoderation?: boolean; channel_id?: string; - channel_last_message_at?: Date; + channel_last_message_at?: TimestampNS; channel_type?: string; cid?: string; connection_id?: string; diff --git a/src/gen/models/timestamp-guard.ts b/src/gen/models/timestamp-guard.ts new file mode 100644 index 00000000..966c56aa --- /dev/null +++ b/src/gen/models/timestamp-guard.ts @@ -0,0 +1,27 @@ +import type { TimestampNS } from './index'; + +/** + * Makes `new Date(ns)` a deprecated constructor, and a compile error for a server-sent nanosecond timestamp. + * + * Any tsconfig whose `include` covers this directory picks this file up; `exclude` it to defer. + * + * The `0 extends 1 & T` guard excludes `any`. Without it this overload would capture + * every `new Date()` in the program -- `any` is assignable to the brand, and a + * merged declaration is tried before lib.es5.d.ts -- breaking each `JSON.parse` path + * and index-signature read on the next regeneration. + */ +declare global { + interface DateConstructor { + /** + * @deprecated Unix nanoseconds, not milliseconds. Use SDK's provided nsToDate(t), or nsToMs(t) for arithmetic. + */ + new ( + t: 0 extends 1 & T ? never : T, + ): { + readonly ERROR_use_SDKs_nsToDate_helper_instead: never; + }; + } +} + +// Keeps this a module, which `declare global` requires, independent of the import above. +export {}; diff --git a/src/gen/moderation/ModerationApi.ts b/src/gen/moderation/ModerationApi.ts index 4667a9f9..38cfdb4b 100644 --- a/src/gen/moderation/ModerationApi.ts +++ b/src/gen/moderation/ModerationApi.ts @@ -56,6 +56,8 @@ import { QueryLabelResultsResponse, QueryModerationConfigsRequest, QueryModerationConfigsResponse, + QueryModerationFeedbackRequest, + QueryModerationFeedbackResponse, QueryModerationFlagsRequest, QueryModerationFlagsResponse, QueryModerationLogsRequest, @@ -66,6 +68,8 @@ import { QueryReviewQueueResponse, QueueResponse, Response, + RevealMediaRequest, + RevealMediaResponse, SubmitActionRequest, SubmitActionResponse, SubmitModerationFeedbackRequest, @@ -257,6 +261,7 @@ export class ModerationApi { entity_id: request?.entity_id, entity_type: request?.entity_type, user_id: request?.user_id, + country_codes: request?.country_codes, content_ids: request?.content_ids, custom: request?.custom, texts: request?.texts, @@ -460,6 +465,7 @@ export class ModerationApi { country_code: request?.country_code, test_mode: request?.test_mode, user_id: request?.user_id, + country_codes: request?.country_codes, config: request?.config, moderation_payload: request?.moderation_payload, options: request?.options, @@ -643,6 +649,34 @@ export class ModerationApi { return response; } + async queryModerationFeedback( + request?: QueryModerationFeedbackRequest, + ): Promise> { + const body = { + item_limit: request?.item_limit, + limit: request?.limit, + next: request?.next, + prev: request?.prev, + user_id: request?.user_id, + sort: request?.sort, + filter: request?.filter, + user: request?.user, + }; + + const response = + await this.apiClient.sendRequest( + 'POST', + '/api/v2/moderation/feedback_items', + undefined, + undefined, + body, + ); + + decoders['QueryModerationFeedbackResponse']?.(response); + + return response; + } + async v2DeleteTemplate(): Promise< StreamResponse > { @@ -1211,6 +1245,28 @@ export class ModerationApi { return response; } + async revealMedia( + request: RevealMediaRequest, + ): Promise> { + const body = { + item_id: request?.item_id, + user_id: request?.user_id, + user: request?.user, + }; + + const response = await this.apiClient.sendRequest( + 'POST', + '/api/v2/moderation/reveal_media', + undefined, + undefined, + body, + ); + + decoders['RevealMediaResponse']?.(response); + + return response; + } + async queryReviewQueue( request?: QueryReviewQueueRequest, ): Promise> { @@ -1344,18 +1400,28 @@ export class ModerationApi { } async submitModerationFeedback( - request: SubmitModerationFeedbackRequest, + request?: SubmitModerationFeedbackRequest, ): Promise> { const body = { - message: request?.message, - published_at: request?.published_at, - reference: request?.reference, channel_id: request?.channel_id, + confidence: request?.confidence, current_recommended_action: request?.current_recommended_action, description: request?.description, + entity_creator_id: request?.entity_creator_id, + entity_id: request?.entity_id, + entity_type: request?.entity_type, expected_recommended_action: request?.expected_recommended_action, + false_positive: request?.false_positive, + feedback_type: request?.feedback_type, + message: request?.message, + published_at: request?.published_at, + reference: request?.reference, + review_queue_item_id: request?.review_queue_item_id, + user_id: request?.user_id, current_labels: request?.current_labels, expected_labels: request?.expected_labels, + items: request?.items, + user: request?.user, }; const response = diff --git a/src/gen/video/CallApi.ts b/src/gen/video/CallApi.ts index 9a2b9401..e81629c2 100644 --- a/src/gen/video/CallApi.ts +++ b/src/gen/video/CallApi.ts @@ -27,6 +27,7 @@ import { QueryCallParticipantSessionsResponse, QueryCallParticipantsRequest, QueryCallParticipantsResponse, + RecordingType, RingCallRequest, RingCallResponse, SendCallEventRequest, @@ -208,7 +209,7 @@ export class CallApi { } startRecording( - request: StartRecordingRequest & { recording_type: string }, + request: StartRecordingRequest & { recording_type: RecordingType }, ): Promise> { return this.videoApi.startRecording({ id: this.id, @@ -218,7 +219,7 @@ export class CallApi { } stopRecording( - request: StopRecordingRequest & { recording_type: string }, + request: StopRecordingRequest & { recording_type: RecordingType }, ): Promise> { return this.videoApi.stopRecording({ id: this.id, diff --git a/src/gen/video/VideoApi.ts b/src/gen/video/VideoApi.ts index 47675ac5..8df32f66 100644 --- a/src/gen/video/VideoApi.ts +++ b/src/gen/video/VideoApi.ts @@ -58,6 +58,7 @@ import { QueryCallsResponse, QueryUserFeedbackRequest, QueryUserFeedbackResponse, + RecordingType, ReportClientEventRequest, ReportClientEventResponse, ResolveSipAuthRequest, @@ -678,7 +679,7 @@ export class VideoApi { request: StartRecordingRequest & { type: string; id: string; - recording_type: string; + recording_type: RecordingType; }, ): Promise> { const pathParams = { @@ -707,7 +708,7 @@ export class VideoApi { request: StopRecordingRequest & { type: string; id: string; - recording_type: string; + recording_type: RecordingType; }, ): Promise> { const pathParams = { diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 00000000..99d0e9e0 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,84 @@ +/** + * Unit conversions between the API's timestamps and JavaScript's. + * + * **The invariant this module exists to hold:** a timestamp is a unix-**nanosecond** `number` — + * that is what the API puts on the wire and what every server-sent date field on a generated + * response or event type carries. A duration, interval or delay stays in **milliseconds**, because + * that is what `setTimeout` and `ApiConfig.timeout` speak. + * + * Two consequences make the helpers below mandatory rather than convenient: + * + * - **Every `Date`-based path is out of range.** `Date` tops out around 8.64e15 ms while a current + * nanosecond timestamp is ~1.79e18, so a bare wire number read as milliseconds lands on an + * invalid instance: `new Date(ns).toISOString()` throws `RangeError`, and a date library handed + * the same number formats it as the literal string `'Invalid Date'`. Neither is a type error. + * - **A unit mix-up between two `number`s is the silent one.** Comparing a wire timestamp against + * `Date.now()`, or adding a millisecond duration to one, produces a plausible-looking number and + * no complaint at all. + * + * Outgoing **request** date fields are unaffected: they are still typed `Date`, because + * `JSON.stringify` emits RFC3339 for a `Date` and that is the format the request spec declares. + * Use {@link nsToDate} when handing a server-sent timestamp back to the API. + * + * Precision note: nanosecond epoch values exceed `Number.MAX_SAFE_INTEGER` (~9.01e15), so they are + * quantised to roughly 256 ns steps. Ordering is unaffected; exact equality against a value that + * has round-tripped through JSON is not guaranteed. + */ + +/** Nanoseconds per millisecond — the only magic number in this module. */ +export const NS_PER_MS = 1e6; + +/** A wire timestamp as epoch milliseconds, for arithmetic against `Date.now()` or a `Date`. */ +export const nsToMs = (ns: number): number => Math.floor(ns / NS_PER_MS); + +/** Epoch milliseconds as a wire timestamp. */ +export const msToNs = (ms: number): number => ms * NS_PER_MS; + +/** + * The local clock as a wire-comparable timestamp, for building API-shaped objects. + * + * Millisecond resolution, so two calls within the same millisecond produce equal timestamps. + * Callers that order by timestamp must tie-break on something else. + */ +export const nowNs = (): number => msToNs(Date.now()); + +/** A wire timestamp as a `Date`, for request payloads and date libraries. */ +export const nsToDate = (ns: number): Date => new Date(nsToMs(ns)); + +/** + * A wire timestamp as a nanosecond-precision RFC3339 string, for an outgoing request date field. + * {@link nsToDate} is lossy here — `Date` holds only milliseconds. + */ +export const nsToRfc3339 = (ns: number): string => { + const ms = nsToMs(ns); + const subMs = String(ns - ms * NS_PER_MS).padStart(6, '0'); + return new Date(ms).toISOString().replace(/\.(\d{3})Z$/, `.$1${subMs}Z`); +}; + +/** A `Date` as a wire timestamp. */ +export const dateToNs = (date: Date): number => msToNs(date.getTime()); + +/** + * A server-sent timestamp as a `Date`, or `undefined` when there is none. + * + * The guarded companion to {@link nsToDate}, for the boundary where a wire timestamp becomes + * something a date library or a caller consumes. Use {@link nsToDate} when the value is known to + * be present; use this when it comes straight off a response, an event or persisted state. + * + * The guard is the whole point, and it covers two cases that are silent rather than loud: + * + * - **Absent.** Many timestamps are optional in practice even where the generated type marks them + * required. `nsToDate(undefined as never)` yields an `Invalid Date`, and `.toISOString()` on one + * throws `RangeError: Invalid time value`. + * - **Not finite.** A malformed payload or a hand-built fixture can produce `NaN`, which reaches + * the same `RangeError` by a different route. + * + * Returning `undefined` rather than throwing lets a caller fall back to whatever it already does + * for a missing timestamp. + */ +export const convertTimestampToDate = ( + timestamp?: number | null, +): Date | undefined => { + if (timestamp == null || !Number.isFinite(timestamp)) return undefined; + return nsToDate(timestamp); +}; diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 00000000..94361238 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": ["node", "vitest/globals"] + }, + "include": ["./src", "index.ts", "./__tests__"], + "exclude": ["./__tests__/docs-snippets"] +} diff --git a/vite.config.mts b/vite.config.mts index b6a8cf92..babc25fe 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -41,6 +41,11 @@ export default defineConfig({ }, testTimeout: 60000, include: ["__tests__/integration/**/*.test.ts"], + typecheck: { + enabled: true, + tsconfig: "./tsconfig.test.json", + include: ["__tests__/integration/**/*.test.ts"], + }, includeSource: ["src/**/*.ts"], retry: 3, },