Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ai-docs/ai-migration-v9-to-v10.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<a id="s2-client-construction-and-configuration"></a>

Expand Down
4 changes: 2 additions & 2 deletions ai-docs/v10-breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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<BanUserOptions, 'channel_cid'>)` goes through `client.moderation.ban({ ...options, target_user_id, channel_cid })` (V2; returns `StreamResponse<ModerationBanResponse>`; `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<BanUserOptions, 'channel_cid'>)` goes through `client.moderation.ban({ ...options, target_user_id, channel_cid })` (V2; returns `StreamResponse<ModerationBanResponse>`; `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)

Expand Down
2 changes: 1 addition & 1 deletion ai-docs/v10-followups.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="offline-cache-key"></a>

Expand Down
20 changes: 7 additions & 13 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -1697,18 +1698,11 @@ export class StreamChat extends ChatApi {
options?: UnBanUserOptions,
): Promise<APIResponse> {
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<APIResponse>(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,
});
}

Expand Down
20 changes: 20 additions & 0 deletions src/gen/chat/ChatApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ import type {
Response,
RewindChannelRequest,
RewindChannelResponse,
SearchAllRequest,
SearchAllResponse,
SearchPayload,
SearchResponse,
SearchRolesResponse,
Expand Down Expand Up @@ -1269,6 +1271,24 @@ export class ChatApi {
);
}

searchAll(
request?: SearchAllRequest & { connection_id?: string },
requestOptions?: StreamRequestOptions,
): Promise<StreamResponse<SearchAllResponse>> {
const { connection_id, ...body } = request ?? {};
const queryParams = { connection_id };

return this.apiClient.sendRequest<SearchAllResponse>(
'POST',
'/api/v2/chat/search/all',
undefined,
queryParams,
body,
'application/json',
requestOptions,
);
}

sync(
request: SyncRequest & {
with_inaccessible_cids?: boolean;
Expand Down
98 changes: 97 additions & 1 deletion src/gen/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,7 @@ export interface ClosedCaptionRuleParameters {
threshold?: number;
time_window?: string;
harm_labels?: Array<string>;
severities?: Array<string>;
llm_harm_labels?: Record<string, string>;
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -10073,6 +10082,93 @@ export interface ScheduledMessagesFilter {
status?: Array<ScheduledMessageQueryStatus>;
}

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<ChannelStateResponseFields>;
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<SearchResult>;
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<string>;
/**
* 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<string, string>;
}

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<FullUserResponse>;
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
Expand Down
10 changes: 7 additions & 3 deletions src/gen/moderation/ModerationApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StreamResponse<UnbanResponse>> {
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<UnbanResponse>(
'POST',
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export type RequireAtLeastOne<T> = {
}[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;
Expand Down
17 changes: 11 additions & 6 deletions test/unit/client.unbanUser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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();
});
});

Expand Down
Loading