Skip to content
Merged
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
42 changes: 25 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<FlagItemResponse>` (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<ModerationBanResponse>` (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<string,string>` (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

Expand All @@ -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

Expand All @@ -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<string,string>` (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`
Expand Down
8 changes: 4 additions & 4 deletions src/Generated/ModerationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,15 +197,15 @@ public function bulkActionAppeals(GeneratedModels\BulkActionAppealsRequest $requ
* Ban a user from a channel or the entire app
*
* @param GeneratedModels\BanRequest $requestData
* @return StreamResponse<GeneratedModels\BanResponse>
* @return StreamResponse<GeneratedModels\ModerationBanResponse>
* @throws StreamException
*/
public function ban(GeneratedModels\BanRequest $requestData): StreamResponse {
$path = '/api/v2/moderation/ban';

$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
Expand Down Expand Up @@ -391,15 +391,15 @@ 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<GeneratedModels\FlagResponse>
* @return StreamResponse<GeneratedModels\FlagItemResponse>
* @throws StreamException
*/
public function flag(GeneratedModels\FlagRequest $requestData): StreamResponse {
$path = '/api/v2/moderation/flag';

$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.
Expand Down
51 changes: 35 additions & 16 deletions src/GeneratedModels/ChannelConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<BlockListOptions>|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
) {
}

Expand Down
33 changes: 33 additions & 0 deletions src/GeneratedModels/ChannelConfigOverrides.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace GetStream\GeneratedModels;
/**
* Channel configuration overrides
*/
class ChannelConfigOverrides 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 $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
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
) {
}

// BaseModel automatically handles jsonSerialize(), toArray(), and fromJson() using constructor types!
// Use #[JsonKey('user_id')] to override field names if needed.
}
2 changes: 1 addition & 1 deletion src/GeneratedModels/ChannelDataUpdate.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public function __construct(
public ?bool $disabled = null,
public ?object $custom = null,
public ?string $team = null,
public ?ChannelConfig $configOverrides = null,
public ?ChannelConfigOverrides $configOverrides = null,
public ?bool $autoTranslationEnabled = null,
public ?string $autoTranslationLanguage = null,
) {
Expand Down
2 changes: 1 addition & 1 deletion src/GeneratedModels/ChannelInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct(
/** @var array<ChannelMemberRequest>|null */
#[ArrayOf(ChannelMemberRequest::class)]
public ?array $members = null,
public ?ChannelConfig $configOverrides = null,
public ?ChannelConfigOverrides $configOverrides = null,
public ?array $filterTags = null,
) {
}
Expand Down
10 changes: 1 addition & 9 deletions src/GeneratedModels/FlagDetails.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
}

Expand Down
16 changes: 16 additions & 0 deletions src/GeneratedModels/FlagItemResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace GetStream\GeneratedModels;
class FlagItemResponse extends BaseModel
{
public function __construct(
public ?string $itemID = null, // Unique identifier of the created moderation item
public ?string $duration = null,
) {
}

// BaseModel automatically handles jsonSerialize(), toArray(), and fromJson() using constructor types!
// Use #[JsonKey('user_id')] to override field names if needed.
}
16 changes: 14 additions & 2 deletions src/GeneratedModels/FlagResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,20 @@
class FlagResponse extends BaseModel
{
public function __construct(
public ?string $itemID = null, // Unique identifier of the created moderation item
public ?string $duration = null,
public ?bool $createdByAutomod = null,
public ?UserResponse $user = null,
public ?string $targetMessageID = null,
public ?MessageResponse $targetMessage = null,
public ?UserResponse $targetUser = null,
public ?\DateTime $createdAt = null,
public ?\DateTime $updatedAt = null,
public ?\DateTime $reviewedAt = null,
public ?string $reviewedBy = null,
public ?\DateTime $approvedAt = null,
public ?\DateTime $rejectedAt = null,
public ?string $reason = null,
public ?FlagDetails $details = null,
public ?object $custom = null,
) {
}

Expand Down
15 changes: 15 additions & 0 deletions src/GeneratedModels/ModerationBanResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace GetStream\GeneratedModels;
class ModerationBanResponse extends BaseModel
{
public function __construct(
public ?string $duration = null,
) {
}

// BaseModel automatically handles jsonSerialize(), toArray(), and fromJson() using constructor types!
// Use #[JsonKey('user_id')] to override field names if needed.
}
4 changes: 2 additions & 2 deletions tests/Integration/ChatTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -973,13 +973,13 @@ protected function unmuteUser(GeneratedModels\UnmuteRequest $request): StreamRes
// =========================================================================

/**
* @return StreamResponse<GeneratedModels\FlagResponse>
* @return StreamResponse<GeneratedModels\FlagItemResponse>
*/
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,
);
}

Expand Down
Loading