Pārlūkot izejas kodu

Track Chatto v0.5.0-beta.17

Wire delta b15 -> b17: (b16) ServerRuntimeConfig.max_video_upload_size is now a deprecated alias of max_upload_size (one upload size limit for all attachments); admin GetRole/UserPermissionMatrix return the complete matrix (page request/response fields reserved - chattolib wrappers never paged, so no change needed); additive ListFollowedThreadsRequest.root_author_filter with new ThreadRootAuthorFilter enum, surfaced as list_followed_threads(root_author_filter=...). (b17) no wire change: server-side best-effort federation + frontend fixes. Client adaptations: ThreadRootAuthorFilter StrEnum in types.py, wrapper parameter in client.list_followed_threads, exported from __init__, AGENTS.md service table + Gotchas entries, test coverage for the filter.

Verification: ruff format/check clean (27 files, all pass); mypy 6 errors, identical to pre-change baseline (pre-existing google.protobuf import-untyped only); pytest 95 passed, 2 skipped; live GetServer against https://chat.chatto.run returns version 0.5.0-beta.17; bot integration tests 2 passed, 1 skipped (second-bot key absent).

Bumped version 0.5.0b15.post2 -> 0.5.0b17.
the-codemancer 2 nedēļas atpakaļ
vecāks
revīzija
0c6d48d126

+ 4 - 2
AGENTS.md

@@ -97,7 +97,7 @@ to its grants.
 | Room directory | `RoomDirectoryService` | `ListRooms`, `ListRoomGroups`, `GetRoomGroup`, `BatchGetRoomGroups`, `GetRoom`, `BatchGetRooms` |
 | Room directory | `RoomDirectoryService` | `ListRooms`, `ListRoomGroups`, `GetRoomGroup`, `BatchGetRoomGroups`, `GetRoom`, `BatchGetRooms` |
 | Rooms | `RoomService` | `CreateRoom`, `UpdateRoom`, `ArchiveRoom`, `UnarchiveRoom`, `JoinRoom`, `JoinRoomGroup`, `StartDM`, `LeaveRoom`, `AddMember`, `RemoveMember`, `ListMembers`, `GetMember`, `BatchGetMembers`, `RemoveUser`, `LiftSuspension`, `ListSuspensions`, `RefreshTypingIndicator`, `GetRoomEvents`, `GetRoomEventsAround`, `MarkRoomAsRead`, `ListRoomAttachments` |
 | Rooms | `RoomService` | `CreateRoom`, `UpdateRoom`, `ArchiveRoom`, `UnarchiveRoom`, `JoinRoom`, `JoinRoomGroup`, `StartDM`, `LeaveRoom`, `AddMember`, `RemoveMember`, `ListMembers`, `GetMember`, `BatchGetMembers`, `RemoveUser`, `LiftSuspension`, `ListSuspensions`, `RefreshTypingIndicator`, `GetRoomEvents`, `GetRoomEventsAround`, `MarkRoomAsRead`, `ListRoomAttachments` |
 | Messages | `MessageService` | `FetchLinkPreview`, `CreateMessage`, `UpdateMessage`, `DeleteMessage`, `DeleteAttachment`, `DeleteLinkPreview`, `GetMessage`, `BatchGetMessages`, `AddReaction`, `RemoveReaction` |
 | Messages | `MessageService` | `FetchLinkPreview`, `CreateMessage`, `UpdateMessage`, `DeleteMessage`, `DeleteAttachment`, `DeleteLinkPreview`, `GetMessage`, `BatchGetMessages`, `AddReaction`, `RemoveReaction` |
-| Threads | `ThreadService` | `FollowThread`, `UnfollowThread`, `ListFollowedThreads` (`unread_only` filters to threads with unread replies, since 0.5.0b8), `GetThreadEvents`, `GetThreadEventsAround`, `MarkThreadAsRead` |
+| Threads | `ThreadService` | `FollowThread`, `UnfollowThread`, `ListFollowedThreads` (`unread_only` filters to threads with unread replies, since 0.5.0b8; `root_author_filter` — `ThreadRootAuthorFilter.VIEWER`/`OTHERS` — filters by the author of the thread *root*, since 0.5.0b16), `GetThreadEvents`, `GetThreadEventsAround`, `MarkThreadAsRead` |
 | Notifications | `NotificationService` | `ListNotifications`, `GetNotification`, `BatchGetNotifications`, `ListRoomNotifications`, `ListRoomNotificationCounts`, `HasNotifications`, `DismissNotification`, `DismissAllNotifications` |
 | Notifications | `NotificationService` | `ListNotifications`, `GetNotification`, `BatchGetNotifications`, `ListRoomNotifications`, `ListRoomNotificationCounts`, `HasNotifications`, `DismissNotification`, `DismissAllNotifications` |
 | Notification prefs | `NotificationPreferencesService` | `Get`/`Update` × `Server`/`Room` |
 | Notification prefs | `NotificationPreferencesService` | `Get`/`Update` × `Server`/`Room` |
 | Notification policy | `NotificationPolicyService` | `GetNotificationPolicy`, `BatchGetNotificationPolicies`, `UpdateNotificationPolicy` — all scoped (server / room group / room) via `NotificationPolicyScope` |
 | Notification policy | `NotificationPolicyService` | `GetNotificationPolicy`, `BatchGetNotificationPolicies`, `UpdateNotificationPolicy` — all scoped (server / room group / room) via `NotificationPolicyScope` |
@@ -111,7 +111,7 @@ to its grants.
 | Admin: roles | `chatto.admin.v1.AdminRoleService` | `ListRoles`, `GetRole`, `CreateRole`, `UpdateRole`, `DeleteRole`, `ReorderRoles` |
 | Admin: roles | `chatto.admin.v1.AdminRoleService` | `ListRoles`, `GetRole`, `CreateRole`, `UpdateRole`, `DeleteRole`, `ReorderRoles` |
 | Admin: event log | `chatto.admin.v1.AdminEventLogService` | `ListEvents`, `ListEventTypes`, `GetEvent` (raw response) |
 | Admin: event log | `chatto.admin.v1.AdminEventLogService` | `ListEvents`, `ListEventTypes`, `GetEvent` (raw response) |
 | Admin: diagnostics | `chatto.admin.v1.AdminDiagnosticsService` | `GetSystemInfo` (raw response; since 0.5.0b14 also carries a `livekit` `AdminLiveKitStatus` block — setup/config state, server-API reachability `connection_state`, webhook times; never secrets) |
 | Admin: diagnostics | `chatto.admin.v1.AdminDiagnosticsService` | `GetSystemInfo` (raw response; since 0.5.0b14 also carries a `livekit` `AdminLiveKitStatus` block — setup/config state, server-API reachability `connection_state`, webhook times; never secrets) |
-| Admin: permissions | `chatto.admin.v1.AdminPermissionService` | `GetRole/UserPermissionMatrix`, `ListRole/UserPermissionDecisions`, `ExplainPermissions`, `SetRolePermission`, `SetUserPermission` (raw responses where the permission shape is server-version-dependent) |
+| Admin: permissions | `chatto.admin.v1.AdminPermissionService` | `GetRole/UserPermissionMatrix` (since 0.5.0b16 returns the **complete** matrix — the `page` request/response fields are reserved; no pagination), `ListRole/UserPermissionDecisions`, `ExplainPermissions`, `SetRolePermission`, `SetUserPermission` (raw responses where the permission shape is server-version-dependent) |
 | Voice calls | `VoiceCallService` | `ListActiveCalls`, `GetActiveCall`, `BatchGetActiveCalls`, `JoinCall`, `LeaveCall`, `CreateCallToken` |
 | Voice calls | `VoiceCallService` | `ListActiveCalls`, `GetActiveCall`, `BatchGetActiveCalls`, `JoinCall`, `LeaveCall`, `CreateCallToken` |
 | Realtime (WS) | `chatto.realtime.v1` protobuf WS (protocol v4) | `stream_events(client)` / `RealtimeConnection` — the client sends one `RealtimeSubscribe` handshake; the server streams `event` (a 48-member oneof of thin, caller-scoped hints, incl. `viewer_presence_preference_changed` added in 0.5.0b6), `snapshot`, `caught_up`, `heartbeat`, and `close` (with a `RealtimeCloseCode` and `retry_after`) |
 | Realtime (WS) | `chatto.realtime.v1` protobuf WS (protocol v4) | `stream_events(client)` / `RealtimeConnection` — the client sends one `RealtimeSubscribe` handshake; the server streams `event` (a 48-member oneof of thin, caller-scoped hints, incl. `viewer_presence_preference_changed` added in 0.5.0b6), `snapshot`, `caught_up`, `heartbeat`, and `close` (with a `RealtimeCloseCode` and `retry_after`) |
 
 
@@ -143,6 +143,8 @@ to its grants.
 - **0.5.0b14: pinning is gated by a dedicated `message.pin` permission (server-side behavior change; no wire change).** `RoomService.CreatePinnedMessage`/`DeletePinnedMessage` required `room.manage` until 0.5.0-beta.13; since 0.5.0-beta.14 they require the new `message.pin` permission instead. The RPCs, their request/response messages, and chattolib's `pin_message`/`unpin_message`/`list_pinned_messages` wrappers are unchanged — but a bot/role that pinned via `room.manage` must now hold `message.pin` (or the server returns `PERMISSION_DENIED`). Same release added an additive `livekit` (`AdminLiveKitStatus`) field 6 to admin `GetSystemInfoResponse` (LiveKit setup/config state, server-API reachability as `AdminLiveKitConnectionState`, webhook receipt times; never the API/webhook secrets) — `admin_get_system_info()` returns it raw in `data["livekit"]` with no client change.
 - **0.5.0b14: pinning is gated by a dedicated `message.pin` permission (server-side behavior change; no wire change).** `RoomService.CreatePinnedMessage`/`DeletePinnedMessage` required `room.manage` until 0.5.0-beta.13; since 0.5.0-beta.14 they require the new `message.pin` permission instead. The RPCs, their request/response messages, and chattolib's `pin_message`/`unpin_message`/`list_pinned_messages` wrappers are unchanged — but a bot/role that pinned via `room.manage` must now hold `message.pin` (or the server returns `PERMISSION_DENIED`). Same release added an additive `livekit` (`AdminLiveKitStatus`) field 6 to admin `GetSystemInfoResponse` (LiveKit setup/config state, server-API reachability as `AdminLiveKitConnectionState`, webhook receipt times; never the API/webhook secrets) — `admin_get_system_info()` returns it raw in `data["livekit"]` with no client change.
 - **Never regenerate protos from `chattocorp/chatto@main`.** `scripts/generate_pb.sh` defaults to a tag derived from `pyproject.toml`: `v<base>` for finals, and `v<base>-<kind>.<n>` for PEP 440 pre-releases (`0.5.0b6` → `v0.5.0-beta.6`), probing candidates with `git ls-remote` (exact ref match; the REST `/tags/{name}` endpoint 404s on lightweight tags). Override with `CHATTO_REF=<tag>` only when you know the deployed server has caught up. chattolib 0.4.19 shipped once from `main` and broke realtime for every downstream client because Chatto's main had a protocol-v2 rewrite that the deployed 0.4.19 server did not speak.
 - **Never regenerate protos from `chattocorp/chatto@main`.** `scripts/generate_pb.sh` defaults to a tag derived from `pyproject.toml`: `v<base>` for finals, and `v<base>-<kind>.<n>` for PEP 440 pre-releases (`0.5.0b6` → `v0.5.0-beta.6`), probing candidates with `git ls-remote` (exact ref match; the REST `/tags/{name}` endpoint 404s on lightweight tags). Override with `CHATTO_REF=<tag>` only when you know the deployed server has caught up. chattolib 0.4.19 shipped once from `main` and broke realtime for every downstream client because Chatto's main had a protocol-v2 rewrite that the deployed 0.4.19 server did not speak.
 - **0.5.0b15.post1: the protoc plugin's `_method_name` is acronym-aware.** The old splitter inserted `_` before *every* capital, so `StartDM` generated `start_d_m` — while `client.py` called `start_dm`, making `start_dm()` an `AttributeError` at call time (shipped broken since the 0.5.x ConnectRPC regeneration; `StartDM` is the only RPC in the current proto set with an acronym run, so nothing else changed). Fixed in `src/chattolib/_protoc_plugin.py` (`StartDM` → `start_dm`; `GetRoomDMStats` → `get_room_dm_stats`); regenerate to pick it up. Note the server side: a **bot key cannot call `StartDM` at all** (`PERMISSION_DENIED` — DMs are human-initiated; a bot can only answer inside a DM room a human already opened), so the wrapper is kept for human-login-style use but is not exercised by bot integration tests. `types._snake_case` (the same bug class, used for timeline/notification `kind` names) was hardened the same way in 0.5.0b15.post2 — latent today: no oneof field in the current proto set has an acronym run, so its output is unchanged.
 - **0.5.0b15.post1: the protoc plugin's `_method_name` is acronym-aware.** The old splitter inserted `_` before *every* capital, so `StartDM` generated `start_d_m` — while `client.py` called `start_dm`, making `start_dm()` an `AttributeError` at call time (shipped broken since the 0.5.x ConnectRPC regeneration; `StartDM` is the only RPC in the current proto set with an acronym run, so nothing else changed). Fixed in `src/chattolib/_protoc_plugin.py` (`StartDM` → `start_dm`; `GetRoomDMStats` → `get_room_dm_stats`); regenerate to pick it up. Note the server side: a **bot key cannot call `StartDM` at all** (`PERMISSION_DENIED` — DMs are human-initiated; a bot can only answer inside a DM room a human already opened), so the wrapper is kept for human-login-style use but is not exercised by bot integration tests. `types._snake_case` (the same bug class, used for timeline/notification `kind` names) was hardened the same way in 0.5.0b15.post2 — latent today: no oneof field in the current proto set has an acronym run, so its output is unchanged.
+- **0.5.0b16: one upload size limit + complete permission matrices.** `ServerRuntimeConfig.max_video_upload_size` is now a **deprecated alias** of `max_upload_size` (both fields carry the same value — one limit applies to all attachments, GIFs/videos included); read `max_upload_size`. Admin `GetRolePermissionMatrix`/`GetUserPermissionMatrix` now return the **complete** matrix: the `page` fields on both request (field 3) and response (field 2) are `reserved` — chattolib's `admin_get_role_permission_matrix()`/`admin_get_user_permission_matrix()` never sent a page and return the raw response, so no client change was needed, but code that paged through `data["page"]` must stop (decision *lists* — `ListRole/UserPermissionDecisions` — still page). Additive: `ListFollowedThreadsRequest.root_author_filter` (new `ThreadRootAuthorFilter` enum: `UNSPECIFIED`/`VIEWER`/`OTHERS`) filters followed threads by the author of the thread **root** before pagination; exposed as `list_followed_threads(root_author_filter=ThreadRootAuthorFilter.VIEWER)` — older servers ignore the field. Remaining b16 changes (OIDC group→role mapping, group-restricted sign-in) are auth-provider-side with no public-API surface.
+- **0.5.0b17: no wire change.** Server-side best-effort federation access to older remote servers plus frontend fixes; the proto set is identical to b16.
 
 
 ### Known bugs
 ### Known bugs
 
 

+ 18 - 18
proto/chatto/admin/v1/permissions.proto

@@ -110,7 +110,7 @@ message PermissionMatrixCell {
   optional bool allow_permitted = 5;
   optional bool allow_permitted = 5;
 }
 }
 
 
-// Permission matrix for one role within a scope page.
+// Complete permission matrix for one role at the matching scopes.
 message RolePermissionMatrix {
 message RolePermissionMatrix {
   // Role name.
   // Role name.
   string role_name = 1;
   string role_name = 1;
@@ -124,27 +124,27 @@ message RolePermissionMatrix {
 
 
 // Request one role's permission matrix.
 // Request one role's permission matrix.
 message GetRolePermissionMatrixRequest {
 message GetRolePermissionMatrixRequest {
+  reserved 3;
+  reserved "page";
   // Stable role name.
   // Stable role name.
   string role_name = 1 [(buf.validate.field).string.min_len = 1];
   string role_name = 1 [(buf.validate.field).string.min_len = 1];
   // Include the direct-message scope column. Defaults to false so an older
   // Include the direct-message scope column. Defaults to false so an older
   // client does not receive a scope kind that it cannot interpret.
   // client does not receive a scope kind that it cannot interpret.
   bool include_direct_message_scope = 2;
   bool include_direct_message_scope = 2;
-  // Scope page. Default 20 scopes, maximum 100. Offsets count scopes, not decisions.
-  chatto.api.v1.PageRequest page = 3;
-  // Optional exact scope filter. Missing or inaccessible scopes produce an empty page.
+  // Optional exact scope filter. Omit to read all scopes. Missing or inaccessible scopes produce an empty matrix.
   // SERVER and DM require an empty ID; GROUP and ROOM require an ID.
   // SERVER and DM require an empty ID; GROUP and ROOM require an ID.
   PermissionScope scope = 4;
   PermissionScope scope = 4;
 }
 }
 
 
 // Response containing one role's permission matrix.
 // Response containing one role's permission matrix.
 message GetRolePermissionMatrixResponse {
 message GetRolePermissionMatrixResponse {
+  reserved 2;
+  reserved "page";
   // Requested matrix.
   // Requested matrix.
   RolePermissionMatrix matrix = 1;
   RolePermissionMatrix matrix = 1;
-  // Total matching scopes and whether another scope page exists.
-  chatto.api.v1.PageInfo page = 2;
 }
 }
 
 
-// Permission matrix for one user within a scope page.
+// Complete permission matrix for one user at the matching scopes.
 message UserPermissionMatrix {
 message UserPermissionMatrix {
   // User ID.
   // User ID.
   string user_id = 1;
   string user_id = 1;
@@ -294,24 +294,24 @@ message ExplainPermissionsResponse {
 
 
 // Request one user's permission matrix.
 // Request one user's permission matrix.
 message GetUserPermissionMatrixRequest {
 message GetUserPermissionMatrixRequest {
+  reserved 3;
+  reserved "page";
   // User ID.
   // User ID.
   string user_id = 1 [(buf.validate.field).string.min_len = 1];
   string user_id = 1 [(buf.validate.field).string.min_len = 1];
   // Include the direct-message scope column. Defaults to false so an older
   // Include the direct-message scope column. Defaults to false so an older
   // client does not receive a scope kind that it cannot interpret.
   // client does not receive a scope kind that it cannot interpret.
   bool include_direct_message_scope = 2;
   bool include_direct_message_scope = 2;
-  // Scope page. Default 20 scopes, maximum 100. Offsets count scopes, not decisions.
-  chatto.api.v1.PageRequest page = 3;
-  // Optional exact scope filter. Missing or inaccessible scopes produce an empty page.
+  // Optional exact scope filter. Omit to read all scopes. Missing or inaccessible scopes produce an empty matrix.
   // SERVER and DM require an empty ID; GROUP and ROOM require an ID.
   // SERVER and DM require an empty ID; GROUP and ROOM require an ID.
   PermissionScope scope = 4;
   PermissionScope scope = 4;
 }
 }
 
 
 // Response containing one user's permission matrix.
 // Response containing one user's permission matrix.
 message GetUserPermissionMatrixResponse {
 message GetUserPermissionMatrixResponse {
+  reserved 2;
+  reserved "page";
   // Requested matrix.
   // Requested matrix.
   UserPermissionMatrix matrix = 1;
   UserPermissionMatrix matrix = 1;
-  // Total matching scopes and whether another scope page exists.
-  chatto.api.v1.PageInfo page = 2;
 }
 }
 
 
 // Request to set one role permission state.
 // Request to set one role permission state.
@@ -357,10 +357,10 @@ message SetUserPermissionResponse {
 }
 }
 
 
 // Provides permission reads and admin permission writes.
 // Provides permission reads and admin permission writes.
-// Matrix and decision pages use the same scope order: SERVER, DM, then each GROUP
-// followed by its ROOM scopes. Groups and rooms within each group use ascending
-// resource IDs. Only requested scopes are evaluated.
-// Pages are live reads; layout or visibility changes can shift offsets. Keep the
+// Matrices return all matching scopes. Decision lists return scope pages.
+// Both use the order SERVER, DM, then each GROUP followed by its ROOM scopes.
+// Groups and rooms follow the configured sidebar order. Only matching scopes are evaluated.
+// Decision pages are live reads; layout or visibility changes can shift offsets. Keep the
 // same filters on each request and advance by the number of returned scopes.
 // same filters on each request and advance by the number of returned scopes.
 // A scope filter of DM selects DM even when include_direct_message_scope is false.
 // A scope filter of DM selects DM even when include_direct_message_scope is false.
 service AdminPermissionService {
 service AdminPermissionService {
@@ -368,13 +368,13 @@ service AdminPermissionService {
   // role.manage; group and room scopes require role.manage or effective
   // role.manage; group and room scopes require role.manage or effective
   // room.manage at the requested resource.
   // room.manage at the requested resource.
   rpc GetRolePermissionTierMatrix(GetRolePermissionTierMatrixRequest) returns (GetRolePermissionTierMatrixResponse);
   rpc GetRolePermissionTierMatrix(GetRolePermissionTierMatrixRequest) returns (GetRolePermissionTierMatrixResponse);
-  // Gets one page of a role's permission matrix. Requires role.manage. Returns
+  // Gets a complete role permission matrix. Requires role.manage. Returns
   // NOT_FOUND when the role does not exist.
   // NOT_FOUND when the role does not exist.
   rpc GetRolePermissionMatrix(GetRolePermissionMatrixRequest) returns (GetRolePermissionMatrixResponse);
   rpc GetRolePermissionMatrix(GetRolePermissionMatrixRequest) returns (GetRolePermissionMatrixResponse);
   // Lists one role's permission decisions as resource-oriented rows. Requires
   // Lists one role's permission decisions as resource-oriented rows. Requires
   // role.manage. Returns NOT_FOUND when the role does not exist.
   // role.manage. Returns NOT_FOUND when the role does not exist.
   rpc ListRolePermissionDecisions(ListRolePermissionDecisionsRequest) returns (ListRolePermissionDecisionsResponse);
   rpc ListRolePermissionDecisions(ListRolePermissionDecisionsRequest) returns (ListRolePermissionDecisionsResponse);
-  // Gets one page of a user's permission matrix. Human targets require
+  // Gets a complete user permission matrix. Human targets require
   // user.manage-permissions; bot targets require ownership or bot.manage.
   // user.manage-permissions; bot targets require ownership or bot.manage.
   // Returns NOT_FOUND when the user does not exist.
   // Returns NOT_FOUND when the user does not exist.
   rpc GetUserPermissionMatrix(GetUserPermissionMatrixRequest) returns (GetUserPermissionMatrixResponse);
   rpc GetUserPermissionMatrix(GetUserPermissionMatrixRequest) returns (GetUserPermissionMatrixResponse);

+ 3 - 3
proto/chatto/api/v1/server_state.proto

@@ -34,10 +34,10 @@ message ServerRuntimeConfig {
   reserved "direct_registration_enabled";
   reserved "direct_registration_enabled";
   // Whether video processing is enabled.
   // Whether video processing is enabled.
   bool video_processing_enabled = 5;
   bool video_processing_enabled = 5;
-  // Maximum general upload size in bytes.
+  // Maximum upload size in bytes for all files, including GIFs and videos.
   int64 max_upload_size = 6;
   int64 max_upload_size = 6;
-  // Maximum video upload size in bytes.
-  int64 max_video_upload_size = 7;
+  // Deprecated alias of max_upload_size. Both fields have the same value.
+  int64 max_video_upload_size = 7 [deprecated = true];
   // Message edit window in seconds.
   // Message edit window in seconds.
   int32 message_edit_window_seconds = 8;
   int32 message_edit_window_seconds = 8;
 }
 }

+ 13 - 0
proto/chatto/api/v1/threads.proto

@@ -9,6 +9,16 @@ import "chatto/api/v1/read_state.proto";
 import "chatto/api/v1/room_timeline.proto";
 import "chatto/api/v1/room_timeline.proto";
 import "chatto/api/v1/rooms.proto";
 import "chatto/api/v1/rooms.proto";
 
 
+// Selects threads by the author of their root message, not their replies.
+enum ThreadRootAuthorFilter {
+  // Include all root authors.
+  THREAD_ROOT_AUTHOR_FILTER_UNSPECIFIED = 0;
+  // Include only roots authored by the authenticated viewer.
+  THREAD_ROOT_AUTHOR_FILTER_VIEWER = 1;
+  // Include only roots authored by other users.
+  THREAD_ROOT_AUTHOR_FILTER_OTHERS = 2;
+}
+
 // Current follow state for one thread and viewer.
 // Current follow state for one thread and viewer.
 message ThreadFollowState {
 message ThreadFollowState {
   // Room containing the thread.
   // Room containing the thread.
@@ -83,6 +93,9 @@ message ListFollowedThreadsRequest {
   // total then counts only these threads. Older servers ignore this field and
   // total then counts only these threads. Older servers ignore this field and
   // return all followed threads.
   // return all followed threads.
   bool unread_only = 5;
   bool unread_only = 5;
+  // Filter root authors before pagination and total counts. Threads whose
+  // root author is unavailable are omitted when a filter is selected.
+  ThreadRootAuthorFilter root_author_filter = 6 [(buf.validate.field).enum.defined_only = true];
 }
 }
 
 
 // Response containing one followed-thread page.
 // Response containing one followed-thread page.

+ 1 - 1
pyproject.toml

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 
 [project]
 [project]
 name = "chattolib"
 name = "chattolib"
-version = "0.5.0b15.post2"
+version = "0.5.0b17"
 description = "Async Python client library for the Chatto webchat Connect API"
 description = "Async Python client library for the Chatto webchat Connect API"
 requires-python = ">=3.11"
 requires-python = ">=3.11"
 license = "MPL-2.0 AND Apache-2.0"
 license = "MPL-2.0 AND Apache-2.0"

+ 2 - 0
src/chattolib/__init__.py

@@ -69,6 +69,7 @@ from chattolib.types import (
     ServerProfile,
     ServerProfile,
     ServerRuntimeConfig,
     ServerRuntimeConfig,
     SidebarLink,
     SidebarLink,
+    ThreadRootAuthorFilter,
     ThreadSummary,
     ThreadSummary,
     TimeFormat,
     TimeFormat,
     TimelineEvent,
     TimelineEvent,
@@ -146,6 +147,7 @@ __all__ = [
     "ServerProfile",
     "ServerProfile",
     "ServerRuntimeConfig",
     "ServerRuntimeConfig",
     "SidebarLink",
     "SidebarLink",
+    "ThreadRootAuthorFilter",
     "ThreadSummary",
     "ThreadSummary",
     "TimeFormat",
     "TimeFormat",
     "TimelineEvent",
     "TimelineEvent",

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/chattolib/_pb/chatto/admin/v1/permissions_pb2.py


+ 10 - 8
src/chattolib/_pb/chatto/api/v1/server_state_pb2.py

@@ -25,13 +25,15 @@ _sym_db = _symbol_database.Default()
 from chatto.api.v1 import server_pb2 as chatto_dot_api_dot_v1_dot_server__pb2
 from chatto.api.v1 import server_pb2 as chatto_dot_api_dot_v1_dot_server__pb2
 
 
 
 
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n chatto/api/v1/server_state.proto\x12\rchatto.api.v1\x1a\x1a\x63hatto/api/v1/server.proto\"\x19\n\x17GetServerProfileRequest\"O\n\x18GetServerProfileResponse\x12\x33\n\x07profile\x18\x01 \x01(\x0b\x32\".chatto.api.v1.ServerPublicProfile\"\x10\n\x0eGetMotdRequest\"-\n\x0fGetMotdResponse\x12\x11\n\x04motd\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_motd\"\xb9\x02\n\x13ServerRuntimeConfig\x12\"\n\x1apush_notifications_enabled\x18\x01 \x01(\x08\x12\x1d\n\x10vapid_public_key\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0blivekit_url\x18\x03 \x01(\tH\x01\x88\x01\x01\x12 \n\x18video_processing_enabled\x18\x05 \x01(\x08\x12\x17\n\x0fmax_upload_size\x18\x06 \x01(\x03\x12\x1d\n\x15max_video_upload_size\x18\x07 \x01(\x03\x12#\n\x1bmessage_edit_window_seconds\x18\x08 \x01(\x05\x42\x13\n\x11_vapid_public_keyB\x0e\n\x0c_livekit_urlJ\x04\x08\x04\x10\x05R\x1b\x64irect_registration_enabled\"\x19\n\x17GetRuntimeConfigRequest\"O\n\x18GetRuntimeConfigResponse\x12\x33\n\x07runtime\x18\x01 \x01(\x0b\x32\".chatto.api.v1.ServerRuntimeConfig2\xa3\x02\n\rServerService\x12\x63\n\x10GetServerProfile\x12&.chatto.api.v1.GetServerProfileRequest\x1a\'.chatto.api.v1.GetServerProfileResponse\x12H\n\x07GetMotd\x12\x1d.chatto.api.v1.GetMotdRequest\x1a\x1e.chatto.api.v1.GetMotdResponse\x12\x63\n\x10GetRuntimeConfig\x12&.chatto.api.v1.GetRuntimeConfigRequest\x1a\'.chatto.api.v1.GetRuntimeConfigResponseb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n chatto/api/v1/server_state.proto\x12\rchatto.api.v1\x1a\x1a\x63hatto/api/v1/server.proto\"\x19\n\x17GetServerProfileRequest\"O\n\x18GetServerProfileResponse\x12\x33\n\x07profile\x18\x01 \x01(\x0b\x32\".chatto.api.v1.ServerPublicProfile\"\x10\n\x0eGetMotdRequest\"-\n\x0fGetMotdResponse\x12\x11\n\x04motd\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_motd\"\xbd\x02\n\x13ServerRuntimeConfig\x12\"\n\x1apush_notifications_enabled\x18\x01 \x01(\x08\x12\x1d\n\x10vapid_public_key\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0blivekit_url\x18\x03 \x01(\tH\x01\x88\x01\x01\x12 \n\x18video_processing_enabled\x18\x05 \x01(\x08\x12\x17\n\x0fmax_upload_size\x18\x06 \x01(\x03\x12!\n\x15max_video_upload_size\x18\x07 \x01(\x03\x42\x02\x18\x01\x12#\n\x1bmessage_edit_window_seconds\x18\x08 \x01(\x05\x42\x13\n\x11_vapid_public_keyB\x0e\n\x0c_livekit_urlJ\x04\x08\x04\x10\x05R\x1b\x64irect_registration_enabled\"\x19\n\x17GetRuntimeConfigRequest\"O\n\x18GetRuntimeConfigResponse\x12\x33\n\x07runtime\x18\x01 \x01(\x0b\x32\".chatto.api.v1.ServerRuntimeConfig2\xa3\x02\n\rServerService\x12\x63\n\x10GetServerProfile\x12&.chatto.api.v1.GetServerProfileRequest\x1a\'.chatto.api.v1.GetServerProfileResponse\x12H\n\x07GetMotd\x12\x1d.chatto.api.v1.GetMotdRequest\x1a\x1e.chatto.api.v1.GetMotdResponse\x12\x63\n\x10GetRuntimeConfig\x12&.chatto.api.v1.GetRuntimeConfigRequest\x1a\'.chatto.api.v1.GetRuntimeConfigResponseb\x06proto3')
 
 
 _globals = globals()
 _globals = globals()
 _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
 _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
 _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'chatto.api.v1.server_state_pb2', _globals)
 _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'chatto.api.v1.server_state_pb2', _globals)
 if not _descriptor._USE_C_DESCRIPTORS:
 if not _descriptor._USE_C_DESCRIPTORS:
   DESCRIPTOR._loaded_options = None
   DESCRIPTOR._loaded_options = None
+  _globals['_SERVERRUNTIMECONFIG'].fields_by_name['max_video_upload_size']._loaded_options = None
+  _globals['_SERVERRUNTIMECONFIG'].fields_by_name['max_video_upload_size']._serialized_options = b'\030\001'
   _globals['_GETSERVERPROFILEREQUEST']._serialized_start=79
   _globals['_GETSERVERPROFILEREQUEST']._serialized_start=79
   _globals['_GETSERVERPROFILEREQUEST']._serialized_end=104
   _globals['_GETSERVERPROFILEREQUEST']._serialized_end=104
   _globals['_GETSERVERPROFILERESPONSE']._serialized_start=106
   _globals['_GETSERVERPROFILERESPONSE']._serialized_start=106
@@ -41,11 +43,11 @@ if not _descriptor._USE_C_DESCRIPTORS:
   _globals['_GETMOTDRESPONSE']._serialized_start=205
   _globals['_GETMOTDRESPONSE']._serialized_start=205
   _globals['_GETMOTDRESPONSE']._serialized_end=250
   _globals['_GETMOTDRESPONSE']._serialized_end=250
   _globals['_SERVERRUNTIMECONFIG']._serialized_start=253
   _globals['_SERVERRUNTIMECONFIG']._serialized_start=253
-  _globals['_SERVERRUNTIMECONFIG']._serialized_end=566
-  _globals['_GETRUNTIMECONFIGREQUEST']._serialized_start=568
-  _globals['_GETRUNTIMECONFIGREQUEST']._serialized_end=593
-  _globals['_GETRUNTIMECONFIGRESPONSE']._serialized_start=595
-  _globals['_GETRUNTIMECONFIGRESPONSE']._serialized_end=674
-  _globals['_SERVERSERVICE']._serialized_start=677
-  _globals['_SERVERSERVICE']._serialized_end=968
+  _globals['_SERVERRUNTIMECONFIG']._serialized_end=570
+  _globals['_GETRUNTIMECONFIGREQUEST']._serialized_start=572
+  _globals['_GETRUNTIMECONFIGREQUEST']._serialized_end=597
+  _globals['_GETRUNTIMECONFIGRESPONSE']._serialized_start=599
+  _globals['_GETRUNTIMECONFIGRESPONSE']._serialized_end=678
+  _globals['_SERVERSERVICE']._serialized_start=681
+  _globals['_SERVERSERVICE']._serialized_end=972
 # @@protoc_insertion_point(module_scope)
 # @@protoc_insertion_point(module_scope)

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/chattolib/_pb/chatto/api/v1/threads_pb2.py


+ 10 - 1
src/chattolib/client.py

@@ -111,6 +111,7 @@ from chattolib.types import (
     ServerLogin,
     ServerLogin,
     ServerProfile,
     ServerProfile,
     ServerRuntimeConfig,
     ServerRuntimeConfig,
+    ThreadRootAuthorFilter,
     TimeFormat,
     TimeFormat,
     TimelinePage,
     TimelinePage,
     User,
     User,
@@ -1113,12 +1114,16 @@ class ChattoClient:
         offset: int | None = None,
         offset: int | None = None,
         include_direct_message_threads: bool | None = None,
         include_direct_message_threads: bool | None = None,
         unread_only: bool | None = None,
         unread_only: bool | None = None,
+        root_author_filter: ThreadRootAuthorFilter | None = None,
     ) -> FollowedThreadsPage:
     ) -> FollowedThreadsPage:
         """Followed threads, newest activity first.
         """Followed threads, newest activity first.
 
 
         ``unread_only`` returns only threads with unread replies (older
         ``unread_only`` returns only threads with unread replies (older
         servers ignore it); ``include_direct_message_threads`` also lists
         servers ignore it); ``include_direct_message_threads`` also lists
-        followed direct-message threads.
+        followed direct-message threads; ``root_author_filter`` (since
+        0.5.0b16) filters by the author of the thread *root* —
+        ``VIEWER`` for threads you started, ``OTHERS`` for threads started
+        by someone else (older servers ignore it).
         """
         """
         req = threads_pb2.ListFollowedThreadsRequest()
         req = threads_pb2.ListFollowedThreadsRequest()
         page = _page_pb(limit, offset)
         page = _page_pb(limit, offset)
@@ -1128,6 +1133,10 @@ class ChattoClient:
             req.include_direct_message_threads = include_direct_message_threads
             req.include_direct_message_threads = include_direct_message_threads
         if unread_only is not None:
         if unread_only is not None:
             req.unread_only = unread_only
             req.unread_only = unread_only
+        if root_author_filter is not None:
+            req.root_author_filter = threads_pb2.ThreadRootAuthorFilter.Value(
+                root_author_filter.value
+            )
         resp = await self._rpc(
         resp = await self._rpc(
             self._svc.threads.list_followed_threads(req, headers=self._headers())
             self._svc.threads.list_followed_threads(req, headers=self._headers())
         )
         )

+ 8 - 0
src/chattolib/types.py

@@ -83,6 +83,12 @@ class NotificationLevel(StrEnum):
     ALL_MESSAGES = "NOTIFICATION_LEVEL_ALL_MESSAGES"
     ALL_MESSAGES = "NOTIFICATION_LEVEL_ALL_MESSAGES"
 
 
 
 
+class ThreadRootAuthorFilter(StrEnum):
+    UNSPECIFIED = "THREAD_ROOT_AUTHOR_FILTER_UNSPECIFIED"
+    VIEWER = "THREAD_ROOT_AUTHOR_FILTER_VIEWER"
+    OTHERS = "THREAD_ROOT_AUTHOR_FILTER_OTHERS"
+
+
 class ImageFitMode(StrEnum):
 class ImageFitMode(StrEnum):
     UNSPECIFIED = "IMAGE_FIT_MODE_UNSPECIFIED"
     UNSPECIFIED = "IMAGE_FIT_MODE_UNSPECIFIED"
     CONTAIN = "IMAGE_FIT_MODE_CONTAIN"
     CONTAIN = "IMAGE_FIT_MODE_CONTAIN"
@@ -434,6 +440,8 @@ class ServerRuntimeConfig:
     livekit_url: str | None = None
     livekit_url: str | None = None
     video_processing_enabled: bool = False
     video_processing_enabled: bool = False
     max_upload_size: int = 0
     max_upload_size: int = 0
+    # Deprecated since 0.5.0b16: an alias of ``max_upload_size`` (same value).
+    # Since 0.5.0b16 one size limit applies to all attachments, GIFs/videos included.
     max_video_upload_size: int = 0
     max_video_upload_size: int = 0
     message_edit_window_seconds: int = 0
     message_edit_window_seconds: int = 0
 
 

+ 12 - 0
tests/test_client.py

@@ -48,6 +48,7 @@ from chattolib.types import (
     NotificationLevel,
     NotificationLevel,
     PresenceStatus,
     PresenceStatus,
     RoomKind,
     RoomKind,
+    ThreadRootAuthorFilter,
     TimeFormat,
     TimeFormat,
 )
 )
 
 
@@ -577,6 +578,10 @@ async def test_list_followed_threads(client):
             offset=5,
             offset=5,
             include_direct_message_threads=True,
             include_direct_message_threads=True,
             unread_only=True,
             unread_only=True,
+            root_author_filter=ThreadRootAuthorFilter.VIEWER,
+        )
+        await client.list_followed_threads(
+            root_author_filter=ThreadRootAuthorFilter.OTHERS,
         )
         )
 
 
     assert page.page.total_count == 1
     assert page.page.total_count == 1
@@ -586,8 +591,15 @@ async def test_list_followed_threads(client):
     calls = [c.args[0] for c in mocked.call_args_list]
     calls = [c.args[0] for c in mocked.call_args_list]
     assert calls[0].include_direct_message_threads is False
     assert calls[0].include_direct_message_threads is False
     assert calls[0].unread_only is False
     assert calls[0].unread_only is False
+    assert calls[0].root_author_filter == 0
     assert calls[1].include_direct_message_threads is True
     assert calls[1].include_direct_message_threads is True
     assert calls[1].unread_only is True
     assert calls[1].unread_only is True
+    assert calls[1].root_author_filter == threads_pb2.ThreadRootAuthorFilter.Value(
+        "THREAD_ROOT_AUTHOR_FILTER_VIEWER"
+    )
+    assert calls[2].root_author_filter == threads_pb2.ThreadRootAuthorFilter.Value(
+        "THREAD_ROOT_AUTHOR_FILTER_OTHERS"
+    )
     assert calls[1].page.limit == 5
     assert calls[1].page.limit == 5
     assert calls[1].page.offset == 5
     assert calls[1].page.offset == 5
 
 

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels