From b07d12be3c66d9563749b5b8b8481c84fc0fe9c1 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Thu, 18 Jun 2026 18:00:15 +0200 Subject: [PATCH 1/2] fix: naming collision on ChannelConfig --- src/Generated/ModerationTrait.php | 8 +-- src/GeneratedModels/ChannelConfig.php | 51 +++++++++++++------ .../ChannelConfigOverrides.php | 33 ++++++++++++ src/GeneratedModels/ChannelDataUpdate.php | 2 +- src/GeneratedModels/ChannelInput.php | 2 +- src/GeneratedModels/FlagDetails.php | 10 +--- src/GeneratedModels/FlagItemResponse.php | 16 ++++++ src/GeneratedModels/FlagResponse.php | 16 +++++- src/GeneratedModels/ModerationBanResponse.php | 15 ++++++ 9 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 src/GeneratedModels/ChannelConfigOverrides.php create mode 100644 src/GeneratedModels/FlagItemResponse.php create mode 100644 src/GeneratedModels/ModerationBanResponse.php diff --git a/src/Generated/ModerationTrait.php b/src/Generated/ModerationTrait.php index 33c8e783..ee4ffc3a 100644 --- a/src/Generated/ModerationTrait.php +++ b/src/Generated/ModerationTrait.php @@ -197,7 +197,7 @@ public function bulkActionAppeals(GeneratedModels\BulkActionAppealsRequest $requ * Ban a user from a channel or the entire app * * @param GeneratedModels\BanRequest $requestData - * @return StreamResponse + * @return StreamResponse * @throws StreamException */ public function ban(GeneratedModels\BanRequest $requestData): StreamResponse { @@ -205,7 +205,7 @@ public function ban(GeneratedModels\BanRequest $requestData): StreamResponse { $queryParams = []; // Use the provided request data array directly - return StreamResponse::fromJson($this->makeRequest('POST', $path, $queryParams, $requestData), GeneratedModels\BanResponse::class); + return StreamResponse::fromJson($this->makeRequest('POST', $path, $queryParams, $requestData), GeneratedModels\ModerationBanResponse::class); } /** * Moderate multiple images in bulk using a CSV file @@ -391,7 +391,7 @@ public function v2UpsertTemplate(GeneratedModels\UpsertModerationTemplateRequest * Flag any type of content (messages, users, channels, activities) for moderation review. Supports custom content types and additional metadata for flagged content. * * @param GeneratedModels\FlagRequest $requestData - * @return StreamResponse + * @return StreamResponse * @throws StreamException */ public function flag(GeneratedModels\FlagRequest $requestData): StreamResponse { @@ -399,7 +399,7 @@ public function flag(GeneratedModels\FlagRequest $requestData): StreamResponse { $queryParams = []; // Use the provided request data array directly - return StreamResponse::fromJson($this->makeRequest('POST', $path, $queryParams, $requestData), GeneratedModels\FlagResponse::class); + return StreamResponse::fromJson($this->makeRequest('POST', $path, $queryParams, $requestData), GeneratedModels\FlagItemResponse::class); } /** * Returns the number of moderation flags created against a specific user's content. Optionally filter by entity type. diff --git a/src/GeneratedModels/ChannelConfig.php b/src/GeneratedModels/ChannelConfig.php index 5f26c92b..a283115c 100644 --- a/src/GeneratedModels/ChannelConfig.php +++ b/src/GeneratedModels/ChannelConfig.php @@ -3,28 +3,47 @@ declare(strict_types=1); namespace GetStream\GeneratedModels; -/** - * Channel configuration overrides - */ class ChannelConfig extends BaseModel { public function __construct( - public ?bool $typingEvents = null, // Enables or disables typing events - public ?bool $reactions = null, // Enables or disables reactions - public ?bool $replies = null, // Enables message replies (threads) - public ?bool $quotes = null, // Enables message quotes - public ?bool $uploads = null, // Enables or disables file uploads - public ?bool $urlEnrichment = null, // Enables or disables URL enrichment - public ?int $maxMessageLength = null, // Overrides max message length + public ?string $name = null, + public ?bool $typingEvents = null, + public ?bool $readEvents = null, + public ?bool $connectEvents = null, + public ?bool $deliveryEvents = null, + public ?bool $search = null, + public ?bool $reactions = null, + public ?bool $replies = null, + public ?bool $quotes = null, + public ?bool $mutes = null, + public ?bool $uploads = null, + public ?bool $urlEnrichment = null, + public ?bool $customEvents = null, + public ?bool $pushNotifications = null, + public ?bool $reminders = null, + public ?bool $markMessagesPending = null, + public ?bool $polls = null, + public ?bool $userMessageReminders = null, + public ?bool $sharedLocations = null, + public ?bool $countMessages = null, + public ?int $maxMessageLength = null, + public ?string $automod = null, + public ?string $automodBehavior = null, public ?string $blocklist = null, public ?string $blocklistBehavior = null, - public ?array $grants = null, - public ?array $commands = null, // List of commands that channel supports - public ?string $pushLevel = null, // Overrides the push notification level for this channel + /** @var array|null */ + #[ArrayOf(BlockListOptions::class)] + public ?array $blocklists = null, + public ?array $allowedFlagReasons = null, + public ?Thresholds $automodThresholds = null, + public ?int $partitionSize = null, + public ?string $partitionTtl = null, + public ?bool $skipLastMsgUpdateForSystemMsgs = null, + public ?string $pushLevel = null, public ?ChatPreferences $chatPreferences = null, - public ?bool $userMessageReminders = null, // Enable/disable user message reminders - public ?bool $sharedLocations = null, // Enable/disable shared locations - public ?bool $countMessages = null, // Enable/disable message counting + public ?\DateTime $createdAt = null, + public ?\DateTime $updatedAt = null, + public ?array $commands = null, // List of commands that channel supports ) { } diff --git a/src/GeneratedModels/ChannelConfigOverrides.php b/src/GeneratedModels/ChannelConfigOverrides.php new file mode 100644 index 00000000..096cdeb4 --- /dev/null +++ b/src/GeneratedModels/ChannelConfigOverrides.php @@ -0,0 +1,33 @@ +|null */ #[ArrayOf(ChannelMemberRequest::class)] public ?array $members = null, - public ?ChannelConfig $configOverrides = null, + public ?ChannelConfigOverrides $configOverrides = null, public ?array $filterTags = null, ) { } diff --git a/src/GeneratedModels/FlagDetails.php b/src/GeneratedModels/FlagDetails.php index cbd7a73b..79debaa0 100644 --- a/src/GeneratedModels/FlagDetails.php +++ b/src/GeneratedModels/FlagDetails.php @@ -3,19 +3,11 @@ declare(strict_types=1); namespace GetStream\GeneratedModels; -/** - * - * - * @property string $originalText - * @property object $extra - * @property AutomodDetails|null $automod - */ class FlagDetails extends BaseModel { public function __construct( + public ?AutomodDetailsResponse $automod = null, public ?string $originalText = null, - public ?object $extra = null, - public ?AutomodDetails $automod = null, ) { } diff --git a/src/GeneratedModels/FlagItemResponse.php b/src/GeneratedModels/FlagItemResponse.php new file mode 100644 index 00000000..8fc8f8fc --- /dev/null +++ b/src/GeneratedModels/FlagItemResponse.php @@ -0,0 +1,16 @@ + Date: Fri, 19 Jun 2026 14:56:29 +0200 Subject: [PATCH 2/2] fix: align flag test helper with renamed FlagItemResponse, document 7.4.0 changelog The integration helper flagContent() hardcoded FlagResponse::class, which the naming-collision regeneration repurposed into the rich flag object without itemID; point it at FlagItemResponse to match the generated flag() client. Changelog: rename staged 7.3.0 to 7.4.0, document the response-type naming collision fixes and CHA-2958 error handling, move the already-shipped webhook helpers to a 7.1.0 entry, drop the empty Unreleased section. --- CHANGELOG.md | 42 ++++++++++++++++++------------ tests/Integration/ChatTestCase.php | 4 +-- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7416b40..b2459fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [7.3.0] - 2026-MM-DD +## [7.4.0] - 2026-MM-DD + +### Fixed — response type naming collisions + +Corrected generated response models that previously shared a class name across endpoints with different shapes (chat-v1 vs v2-moderation). These align the SDK with the backend contract. No runtime behavior changes for real data paths, but the return types of `flag()` and `ban()` are renamed: any code that explicitly type-hints or uses `instanceof` against the old classes must update. + +- `Moderation::flag()` now returns `StreamResponse` (was `FlagResponse`). `FlagItemResponse` keeps the same `itemID` and `duration` fields, so reading them is unaffected. `FlagResponse` is repurposed to the rich flag object (`createdByAutomod`, `user`, `targetMessage`, `details`, etc.) returned by other flag endpoints. +- `Moderation::ban()` now returns `StreamResponse` (was `BanResponse`). `ModerationBanResponse` exposes `duration` — the only field `/api/v2/moderation/ban` actually returns. The previous `BanResponse` fields (`user`, `channel`, `expires`, `bannedBy`, ...) were never populated by this endpoint, so reads against them already returned `null`. +- `FlagDetails` no longer carries `extra`, and its `automod` field is now typed `AutomodDetailsResponse` (was `AutomodDetails`), matching the backend `FlagDetails` struct. The `extra` field belongs to `FlagDetailsResponse`, which is unchanged. +- `ChannelConfig` field set corrected to match the backend, resolving the naming collision that motivated this change. New `ChannelConfigOverrides` model added. + +### Breaking behavior changes (no API rename) + +- **`StreamApiException` structured fields are reshaped to match the canonical `APIError` envelope.** The constructor signature changes: `(string $message, int $statusCode, int $code, array $exceptionFields, bool $unrecoverable, string $rawResponseBody, ?string $moreInfo, mixed $details, ?\Throwable $previous)`. Replaced accessors: + * `getResponseBody(): ?string` → `getRawResponseBody(): string` + * `getErrorDetails(): array` (non-canonical bag) → `getExceptionFields(): array` (only the validation map from `exception_fields`) + * New: `isUnrecoverable(): bool`, `getMoreInfo(): ?string`, `getDetails(): mixed`. + * `getStatusCode()` and `getCode()` keep their existing semantics — both return the HTTP status (back-compat with pre-CHA-2958 callers that branched on `$e->getCode() === 429`). The canonical `APIError.code` is exposed via the new `getApiErrorCode(): int`. ### Added @@ -18,6 +35,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `HttpClientInterface::request()` gains an optional 5th `array $options = []` parameter for per-call overrides (e.g., `['timeout' => 2]`). Backward-compatible. - INFO log on `ClientBuilder::build()` listing the effective pool config. Emitted via `error_log()`; suppressed in PHPUnit runs. - `GuzzleHttpClient::getPoolConfig()` accessor for diagnostics. +- Error-handling spec rollout (CHA-2958, [spec](https://www.notion.so/3526a5d7f9f681e5a1b8d881cb5cbcf1)): + * New `StreamRateLimitException extends StreamApiException` for HTTP 429 responses. Exposes `getRetryAfter(): ?int` (seconds; `null` when the header is absent or unparseable). Both integer seconds (`Retry-After: 30`) and HTTP-date forms (`Retry-After: Fri, 31 Dec 2026 23:59:59 GMT`) are accepted per RFC 7231 §7.1.3; HTTP-date deltas are clamped to ≥ 0. + * New `StreamTransportException extends StreamException` for network-layer failures with no HTTP response (connection reset, timeout, TLS handshake failure, DNS failure). Exposes `getErrorType(): string` returning one of `connection_reset` · `timeout` · `dns_failure` · `tls_handshake_failed` · `unknown` (matches the logging spec's `error.type` enum). The original Guzzle exception is preserved via `getPrevious()`. + * New `StreamTaskException extends StreamException` thrown by `Client::waitForTask()` when a polled task settles into `status: "failed"`. Carries `getTaskId()`, `getErrorType()`, `getDescription()`, `getStackTrace()`, `getVersion()` from the task's `ErrorResult` payload. + * New `Client::waitForTask(string $taskId, int $pollIntervalSeconds = 1, int $timeoutSeconds = 60)`. Polls `/api/v2/tasks/{id}` until the task settles into `completed` (returns `GetTaskResponse`) or `failed` (throws `StreamTaskException`). On timeout it raises `StreamTransportException` with `errorType = "timeout"`. +- Cause-chain preservation: every wrap point in `GuzzleHttpClient` now passes the caught `GuzzleException` as the `$previous` argument to the SDK exception, fixing the broken chain in the prior `GuzzleHttpClient::request()` catch block. Unparseable error responses (HTTP layer succeeded, body is not a valid `APIError`) wrap a `\JsonException` cause and surface as a base `StreamApiException` with `code = 0` and `message = "failed to parse error response"`. ### Changed @@ -36,25 +59,10 @@ Under PHP-FPM (and one-shot CLI scripts) the PHP process exits at the end of eac - No env-var overrides. - No PSR-3 logger injection; INFO log goes through `error_log()`. -## [Unreleased] - -### Breaking behavior changes (no API rename) - -- **`StreamApiException` structured fields are reshaped to match the canonical `APIError` envelope.** The constructor signature changes: `(string $message, int $statusCode, int $code, array $exceptionFields, bool $unrecoverable, string $rawResponseBody, ?string $moreInfo, mixed $details, ?\Throwable $previous)`. Replaced accessors: - * `getResponseBody(): ?string` → `getRawResponseBody(): string` - * `getErrorDetails(): array` (non-canonical bag) → `getExceptionFields(): array` (only the validation map from `exception_fields`) - * New: `isUnrecoverable(): bool`, `getMoreInfo(): ?string`, `getDetails(): mixed`. - * `getStatusCode()` and `getCode()` keep their existing semantics — both return the HTTP status (back-compat with pre-CHA-2958 callers that branched on `$e->getCode() === 429`). The canonical `APIError.code` is exposed via the new `getApiErrorCode(): int`. +## [7.1.0] - 2026-05-19 ### Added -- Error-handling spec rollout (CHA-2958, [spec](https://www.notion.so/3526a5d7f9f681e5a1b8d881cb5cbcf1)): - * New `StreamRateLimitException extends StreamApiException` for HTTP 429 responses. Exposes `getRetryAfter(): ?int` (seconds; `null` when the header is absent or unparseable). Both integer seconds (`Retry-After: 30`) and HTTP-date forms (`Retry-After: Fri, 31 Dec 2026 23:59:59 GMT`) are accepted per RFC 7231 §7.1.3; HTTP-date deltas are clamped to ≥ 0. - * New `StreamTransportException extends StreamException` for network-layer failures with no HTTP response (connection reset, timeout, TLS handshake failure, DNS failure). Exposes `getErrorType(): string` returning one of `connection_reset` · `timeout` · `dns_failure` · `tls_handshake_failed` · `unknown` (matches the logging spec's `error.type` enum). The original Guzzle exception is preserved via `getPrevious()`. - * New `StreamTaskException extends StreamException` thrown by `Client::waitForTask()` when a polled task settles into `status: "failed"`. Carries `getTaskId()`, `getErrorType()`, `getDescription()`, `getStackTrace()`, `getVersion()` from the task's `ErrorResult` payload. - * New `Client::waitForTask(string $taskId, int $pollIntervalSeconds = 1, int $timeoutSeconds = 60)`. Polls `/api/v2/tasks/{id}` until the task settles into `completed` (returns `GetTaskResponse`) or `failed` (throws `StreamTaskException`). On timeout it raises `StreamTransportException` with `errorType = "timeout"`. -- Cause-chain preservation: every wrap point in `GuzzleHttpClient` now passes the caught `GuzzleException` as the `$previous` argument to the SDK exception, fixing the broken chain in the prior `GuzzleHttpClient::request()` catch block. Unparseable error responses (HTTP layer succeeded, body is not a valid `APIError`) wrap a `\JsonException` cause and surface as a base `StreamApiException` with `code = 0` and `message = "failed to parse error response"`. - - Webhook handling spec helpers (CHA-2961): `UnknownEvent` class for forward-compat; `gunzipPayload`, `decodeSqsPayload`, `decodeSnsPayload` primitives; `verifyAndParseWebhook` HTTP composite; `parseSqs` / `parseSns` diff --git a/tests/Integration/ChatTestCase.php b/tests/Integration/ChatTestCase.php index d2f8e4f6..9403085a 100644 --- a/tests/Integration/ChatTestCase.php +++ b/tests/Integration/ChatTestCase.php @@ -973,13 +973,13 @@ protected function unmuteUser(GeneratedModels\UnmuteRequest $request): StreamRes // ========================================================================= /** - * @return StreamResponse + * @return StreamResponse */ protected function flagContent(GeneratedModels\FlagRequest $request): StreamResponse { return StreamResponse::fromJson( $this->client->makeRequest('POST', '/api/v2/moderation/flag', [], $request), - GeneratedModels\FlagResponse::class, + GeneratedModels\FlagItemResponse::class, ); }