From 71f78dafd679813c4ff387b21517c76119db65b7 Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Tue, 10 Nov 2026 14:46:36 -0600 Subject: [PATCH] fix: unban user v2 now also support remove_future_channels_ban --- ai-docs/ai-migration-v9-to-v10.md | 2 +- ai-docs/v10-breaking-changes.md | 4 +- ai-docs/v10-followups.md | 2 +- src/client.ts | 20 +++--- src/gen/chat/ChatApi.ts | 20 ++++++ src/gen/models/index.ts | 98 ++++++++++++++++++++++++++++- src/gen/moderation/ModerationApi.ts | 10 ++- src/types.ts | 4 +- test/unit/client.unbanUser.test.ts | 17 +++-- 9 files changed, 148 insertions(+), 29 deletions(-) diff --git a/ai-docs/ai-migration-v9-to-v10.md b/ai-docs/ai-migration-v9-to-v10.md index 70656c38c..fbf7764ec 100644 --- a/ai-docs/ai-migration-v9-to-v10.md +++ b/ai-docs/ai-migration-v9-to-v10.md @@ -388,7 +388,7 @@ const server = new StreamClient( const token = server.generateUserToken({ user_id: 'jane', validity_in_seconds: 60 * 60 }); ``` -Client-side relatives: `client.revokeTokens(before?)` now revokes only the connected user's tokens (Ref: revoke-tokens); `client.unbanUser` keeps its signature but goes through V2 `/moderation/unban` (v1 only for `remove_future_channels_ban: true`). +Client-side relatives: `client.revokeTokens(before?)` now revokes only the connected user's tokens (Ref: revoke-tokens); `client.unbanUser` keeps its signature but goes through V2 `/moderation/unban`. diff --git a/ai-docs/v10-breaking-changes.md b/ai-docs/v10-breaking-changes.md index af3dda891..d3a72fd10 100644 --- a/ai-docs/v10-breaking-changes.md +++ b/ai-docs/v10-breaking-changes.md @@ -499,7 +499,7 @@ Common rules for every generated or rewritten `StreamChat` method: path paramete **Kind:** behavior · **Impact:** medium · **PR:** #1760 - **v9:** endpoints at `baseURL + '/channels'`, `'/users'`, `'/messages/{id}'`, `'/uploads/file'`, ...; every request carried `user_id`, `api_key` and `connection_id`. -- **v10:** generated endpoints are `baseURL + '/api/v2/...'` (e.g. `/api/v2/chat/channels`, `/api/v2/users`, `/api/v2/chat/messages/{id}`, `/api/v2/uploads/file`). `user_id` and `api_key` are still on every request; `connection_id` only on requests that watch or subscribe to presence and on `stopWatchingChannel`. Hand-written `unbanUser` calls V2 `/api/v2/moderation/unban`, except with `remove_future_channels_ban: true`, which still goes to v1 `DELETE /moderation/ban`. +- **v10:** generated endpoints are `baseURL + '/api/v2/...'` (e.g. `/api/v2/chat/channels`, `/api/v2/users`, `/api/v2/chat/messages/{id}`, `/api/v2/uploads/file`). `user_id` and `api_key` are still on every request; `connection_id` only on requests that watch or subscribe to presence and on `stopWatchingChannel`. Hand-written `unbanUser` calls V2 `/api/v2/moderation/unban`. - **Migrate:** update reverse proxies, CSP allowlists, request mocks (msw / nock) and server-side request filters keyed on paths or on `connection_id`. - **Source:** `src/gen/chat/ChatApi.ts`, `src/api-client.ts:146-171,206-217`, `src/client.ts:1840` (v10); `src/client.ts:3929-3936` (v9) @@ -1582,7 +1582,7 @@ Common pattern for every generated or overridden `Channel` method: the trailing **Kind:** signature · **Impact:** medium - **v9:** `banUser(targetUserID, options: BanUserOptions)` (v1 `/moderation/ban` with `type` / `id`), `unbanUser(targetUserID, options?)`, `shadowBan(targetUserID, options)`, `removeShadowBan(targetUserID)`; all required initialization. -- **v10:** `banUser(targetUserId, options: Omit)` goes through `client.moderation.ban({ ...options, target_user_id, channel_cid })` (V2; returns `StreamResponse`; `delete_reactions`, `ban_from_future_channels` follow `BanRequest`); `unbanUser(targetUserId, options?)` keeps its signature and goes through V2 `client.moderation.unban({ target_user_id, channel_cid })` (v1 only for `remove_future_channels_ban: true`). Both require only an id. `shadowBan` / `removeShadowBan` removed. +- **v10:** `banUser(targetUserId, options: Omit)` goes through `client.moderation.ban({ ...options, target_user_id, channel_cid })` (V2; returns `StreamResponse`; `delete_reactions`, `ban_from_future_channels` follow `BanRequest`); `unbanUser(targetUserId, options?)` keeps its signature and goes through V2 `client.moderation.unban({ target_user_id, channel_cid, remove_future_channels_ban })`. With a channel, `remove_future_channels_ban` removes the channel creator's future channels ban (where the V2 ban keys it) rather than the caller's. Both require only an id. `shadowBan` / `removeShadowBan` removed. - **Migrate:** `channel.shadowBan(id, o)` -> `channel.banUser(id, { ...o, shadow: true })`; `channel.removeShadowBan(id)` -> `channel.unbanUser(id, { shadow: true })`. - **Source:** `src/channel.ts:2094-2144`, `src/types.ts:97,139-147` (v10); `src/channel.ts:1711-1800` (v9) diff --git a/ai-docs/v10-followups.md b/ai-docs/v10-followups.md index 168ef61bf..7a715266a 100644 --- a/ai-docs/v10-followups.md +++ b/ai-docs/v10-followups.md @@ -826,7 +826,7 @@ Non-breaking; land any time. Breaking simplifications are in [section 1](#fix-be - **Where:** `src/client.ts:1839-1844`, `src/types.ts:59-68` (`APIResponse`) - **What:** `unbanUser` still calls v1 `DELETE /moderation/ban` returning the legacy `APIResponse` envelope, while ban, mute and unmute moved to V2 `client.moderation.*`, which already has `unban`. `APIResponse`'s own comment says to delete it with these methods. RN V10 calls `client.unbanUser(id)` (`useMessageActionHandlers.ts:216`). - **Suggested change:** Route through `this.moderation.unban({ target_user_id, ...options })` while keeping the `unbanUser(targetUserID, options?)` signature (non-breaking, any time). Deleting `APIResponse` is a v9 break, decided with the [restated types](#lo-restated-types). -- **Status:** mostly done. `unbanUser` now calls `this.moderation.unban({ target_user_id, channel_cid })`, with `channel_cid` built from `type` / `id`. The exception is `remove_future_channels_ban: true`, which still goes to v1: the V2 `/unban` controller forwards only `channel_cid` to its `UnbanAction`, although that action supports the flag. The v1 path can go once the backend accepts the flag on V2. `APIResponse` stays, as the return type. +- **Status:** done. `unbanUser` calls `this.moderation.unban({ target_user_id, channel_cid, remove_future_channels_ban })`, with `channel_cid` built from `type` / `id`; the v1 fallback for `remove_future_channels_ban` went once the spec gained the flag on V2. With a channel, V2 removes the channel creator's future channels ban (where the V2 ban keys it), where v1 removed the caller's, so a non-creator moderator's unban now actually removes it. `APIResponse` stays, as the return type. diff --git a/src/client.ts b/src/client.ts index c2b640513..b57916f44 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1683,10 +1683,11 @@ export class StreamChat extends ChatApi { /** * Revokes a global ban, or a channel ban when `options.type` and `options.id` name a channel. * - * Goes through V2 `client.moderation.unban()`. The one exception is - * `remove_future_channels_ban`, which that endpoint does not accept, so a call with it set still - * goes to v1 `DELETE /moderation/ban`. `shadow` is accepted for compatibility but ignored: the - * server lifts whichever ban is in place, shadow or not, on both endpoints. + * Goes through V2 `client.moderation.unban()`. `shadow` is accepted for compatibility but ignored: + * the server lifts whichever ban is in place, shadow or not. With `remove_future_channels_ban`, + * the future channels ban is removed too: the channel creator's when a channel is named (where + * `channel.banUser` with `ban_from_future_channels` keys it), otherwise the caller's, and then + * only the future channels ban is removed. * * @param targetUserId - The user to unban. * @param options - Unban options (optional). @@ -1697,18 +1698,11 @@ export class StreamChat extends ChatApi { options?: UnBanUserOptions, ): Promise { const { id, remove_future_channels_ban, type } = options ?? {}; - const target_user_id = options?.target_user_id ?? targetUserId; - - if (remove_future_channels_ban) { - return await this.api.delete(this.baseURL + '/moderation/ban', { - ...options, - target_user_id, - }); - } return await this.moderation.unban({ - target_user_id, + target_user_id: options?.target_user_id ?? targetUserId, channel_cid: type && id ? `${type}:${id}` : undefined, + remove_future_channels_ban, }); } diff --git a/src/gen/chat/ChatApi.ts b/src/gen/chat/ChatApi.ts index 0850ce109..f655d0eb8 100644 --- a/src/gen/chat/ChatApi.ts +++ b/src/gen/chat/ChatApi.ts @@ -101,6 +101,8 @@ import type { Response, RewindChannelRequest, RewindChannelResponse, + SearchAllRequest, + SearchAllResponse, SearchPayload, SearchResponse, SearchRolesResponse, @@ -1269,6 +1271,24 @@ export class ChatApi { ); } + searchAll( + request?: SearchAllRequest & { connection_id?: string }, + requestOptions?: StreamRequestOptions, + ): Promise> { + const { connection_id, ...body } = request ?? {}; + const queryParams = { connection_id }; + + return this.apiClient.sendRequest( + 'POST', + '/api/v2/chat/search/all', + undefined, + queryParams, + body, + 'application/json', + requestOptions, + ); + } + sync( request: SyncRequest & { with_inaccessible_cids?: boolean; diff --git a/src/gen/models/index.ts b/src/gen/models/index.ts index 0d60df901..7fa496a45 100644 --- a/src/gen/models/index.ts +++ b/src/gen/models/index.ts @@ -2573,6 +2573,7 @@ export interface ClosedCaptionRuleParameters { threshold?: number; time_window?: string; harm_labels?: Array; + severities?: Array; llm_harm_labels?: Record; } @@ -5878,11 +5879,15 @@ export interface ModerationEngineConfigRequest { /** * LLM model used when the provider resolves to llm. Not allowed with provider nlp. Empty uses the app's moderation settings */ - model?: 'gpt-4o-mini' | 'gpt-4o'; + model?: 'gpt-4o-mini' | 'gpt-4o' | 'gpt-6-luna'; /** * llm or nlp. Empty follows the app's LLM moderation setting */ provider?: 'llm' | 'nlp'; + /** + * OpenAI reasoning effort for model. Requires model, and must be one the model accepts. Empty uses the model's default + */ + reasoning_effort?: string; } export interface ModerationEngineConfigResponse { @@ -5894,6 +5899,10 @@ export interface ModerationEngineConfigResponse { * llm or nlp. Empty follows the app's LLM moderation setting */ provider?: 'llm' | 'nlp'; + /** + * OpenAI reasoning effort for model. Requires model, and must be one the model accepts. Empty uses the model's default + */ + reasoning_effort?: string; } export interface ModerationFlagResponse { @@ -10073,6 +10082,93 @@ export interface ScheduledMessagesFilter { status?: Array; } +export interface SearchAllChannelsSection { + /** + * Opaque cursor for the next page of this section, present only when more results exist. Pass it back under next.channels with the same query + */ + next?: string; + /** + * Channels the caller is a member of whose name starts with the query, in the same shape as the query channels endpoint's results but without messages or members + */ + channels?: Array; + error?: SearchAllSectionError; + warning?: SearchAllWarning; +} + +export interface SearchAllMessagesSection { + /** + * Opaque cursor for the next page of this section, present only when more results exist. Pass it back under next.messages with the same query + */ + next?: string; + /** + * Matching messages, newest first, in the same shape as the search endpoint's results + */ + results?: Array; + error?: SearchAllSectionError; + results_warning?: SearchWarning; + warning?: SearchAllWarning; +} + +export interface SearchAllRequest { + /** + * Number of results per section, 1-30. Defaults to 10. Applies to every requested section; there is no per-section limit + */ + limit?: number; + /** + * Free-text search phrase, 1-80 characters after trimming whitespace. Required. Matched as a prefix of a user's name or username and of a channel's name, and as full-text against message text + */ + query?: string; + /** + * Which sections to search: any subset of messages, users and channels. Defaults to all three when omitted; must not be empty when provided. Response sections always appear in the order messages, users, channels + */ + entities?: Array; + /** + * Opaque pagination cursors from a previous response, keyed by entity. A cursor that fails to decode restarts that section from page 1 with a cursor_rejected warning; the request fails with 400 only when every cursor supplied for a requested entity is invalid. Cursors for entities not in entities are ignored + */ + next?: Record; +} + +export interface SearchAllResponse { + /** + * Duration of the request in milliseconds + */ + duration: string; + channels?: SearchAllChannelsSection; + messages?: SearchAllMessagesSection; + users?: SearchAllUsersSection; +} + +export interface SearchAllSectionError { + /** + * Error code. backend_timeout and backend_unavailable are retryable; unsupported (the app's configuration cannot serve this section) and internal_error are not + */ + code: 'backend_unavailable' | 'backend_timeout' | 'unsupported' | 'internal_error'; + /** + * Human-readable description of the failure + */ + message: string; +} + +export interface SearchAllUsersSection { + /** + * Opaque cursor for the next page of this section, present only when more results exist. Pass it back under next.users with the same query + */ + next?: string; + /** + * Users whose name or username starts with the query, in the same shape as the query users endpoint's results + */ + users?: Array; + error?: SearchAllSectionError; + warning?: SearchAllWarning; +} + +export interface SearchAllWarning { + /** + * Warning code. cursor_rejected: the cursor supplied for this section failed to decode, so the section restarted from page 1 + */ + code: 'cursor_rejected'; +} + export interface SearchPayload { /** * Channel filter conditions diff --git a/src/gen/moderation/ModerationApi.ts b/src/gen/moderation/ModerationApi.ts index e982f72c3..a427d22b5 100644 --- a/src/gen/moderation/ModerationApi.ts +++ b/src/gen/moderation/ModerationApi.ts @@ -489,11 +489,15 @@ export class ModerationApi { } unban( - request: UnbanRequest & { target_user_id: string; channel_cid?: string }, + request: UnbanRequest & { + target_user_id: string; + channel_cid?: string; + remove_future_channels_ban?: boolean; + }, requestOptions?: StreamRequestOptions, ): Promise> { - const { target_user_id, channel_cid, ...body } = request; - const queryParams = { target_user_id, channel_cid }; + const { target_user_id, channel_cid, remove_future_channels_ban, ...body } = request; + const queryParams = { target_user_id, channel_cid, remove_future_channels_ban }; return this.apiClient.sendRequest( 'POST', diff --git a/src/types.ts b/src/types.ts index c08353428..e5f3b4194 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,8 +51,8 @@ export type RequireAtLeastOne = { }[keyof T]; /** - * The return type of `StreamChat.unbanUser`, kept so its signature is unchanged. Its v1 fallback - * (`remove_future_channels_ban`) is the one hand-written `/moderation/*` call left. + * The return type of `StreamChat.unbanUser`, kept so its signature is unchanged. `unbanUser` itself + * goes through the generated V2 `moderation.unban()`. */ export type APIResponse = { duration: string; diff --git a/test/unit/client.unbanUser.test.ts b/test/unit/client.unbanUser.test.ts index cbdc8bb44..5dca81403 100644 --- a/test/unit/client.unbanUser.test.ts +++ b/test/unit/client.unbanUser.test.ts @@ -56,13 +56,13 @@ describe('unbanUser', () => { shadow: true, client_id: 'c', connection_id: 'x', - remove_future_channels_ban: false, }); expect(unbanCall()).toMatchObject({ queryParams: { target_user_id: 'bob', channel_cid: undefined }, body: {}, }); + expect(unbanCall().queryParams).not.toHaveProperty('shadow'); }); it('keeps options.target_user_id taking precedence, as before', async () => { @@ -71,14 +71,19 @@ describe('unbanUser', () => { expect(unbanCall().queryParams).toMatchObject({ target_user_id: 'alice' }); }); - it('falls back to v1 for remove_future_channels_ban, which V2 does not accept', async () => { - await client.unbanUser('bob', { remove_future_channels_ban: true }); - - expect(sendRequest).not.toHaveBeenCalled(); - expect(legacyDelete).toHaveBeenCalledWith(`${client.baseURL}/moderation/ban`, { + it('sends remove_future_channels_ban to the V2 endpoint', async () => { + await client.unbanUser('bob', { + type: 'messaging', + id: 'general', remove_future_channels_ban: true, + }); + + expect(unbanCall().queryParams).toEqual({ target_user_id: 'bob', + channel_cid: 'messaging:general', + remove_future_channels_ban: true, }); + expect(legacyDelete).not.toHaveBeenCalled(); }); });