Переглянути джерело

Track Chatto 0.4.9

Chatto HQ jumped from 0.4.3 to 0.4.9. Per the version-tracks-Chatto
policy, chattolib bumps to match.

Upstream proto deltas:

- Additive: Message.deleted_at (Timestamp). Surfaced on the Message
  dataclass and its parser.
- Additive: GetSystemInfoResponse.asset_cleanup (AdminAssetCleanupStatus)
  with a new AdminAssetCleanupHealth enum. Passes through the existing
  raw-dict admin_get_system_info() return; no client code change.
- Metadata-only: DeleteAvatar, DeleteCustomStatus, DismissNotification,
  Unsubscribe, and GetServer now advertise idempotency_level. No wire
  change.
- Server-side validation: CreateMessageRequest.attachment_asset_ids
  now capped at 10 items with per-item length limits. Server-enforced;
  no client change.

Live-verified against chat.chatto.run (0.4.9) with the RoboChatto
account: login, me(), list_rooms(), get_motd() all decode correctly.
ruff + mypy --strict + pytest all clean.
TheCodemancer 3 місяців тому
батько
коміт
eb66ff29f6

+ 28 - 0
.beans/chattolib-5ggo--update-chattolib-to-chatto-049.md

@@ -0,0 +1,28 @@
+---
+# chattolib-5ggo
+title: Update chattolib to Chatto 0.4.9
+status: completed
+type: task
+priority: high
+created_at: 2026-07-12T23:46:37Z
+updated_at: 2026-07-12T23:48:14Z
+---
+
+Chatto HQ is now serving 0.4.9. Regenerate proto bindings from upstream, diff the API surface for breaking or additive changes, adapt the client if needed, bump chattolib to 0.4.9, run the full check + live-verify, publish, and push.
+
+## Summary of Changes
+Regenerated proto bindings from upstream. Delta from 0.4.3:
+
+**Additive fields**
+- `Message.deleted_at` (google.protobuf.Timestamp) — surface added to the `Message` dataclass and its `.parse()` classmethod.
+- `GetSystemInfoResponse.asset_cleanup` (AdminAssetCleanupStatus) with a new AdminAssetCleanupHealth enum. Passes through the existing raw-dict `admin_get_system_info()` return; no code change needed.
+
+**Metadata-only**
+- Several RPCs now advertise `idempotency_level`: `DeleteAvatar`, `DeleteCustomStatus`, `DismissNotification`, `Unsubscribe`, `GetServer`. No wire change.
+
+**Server-side validation tightening**
+- `CreateMessageRequest.attachment_asset_ids` now capped at 10 items with per-item length constraints. Server-enforced; no client change.
+
+Bumped `pyproject.toml` from `0.4.3` to `0.4.9`. `ruff` + `mypy --strict` + `pytest` (48 passed, 4 skipped) all clean.
+
+Live-verified against Chatto HQ (0.4.9): login, viewer, list_rooms, get_motd all decode correctly.

+ 14 - 12
proto/buf/validate/validate.proto

@@ -88,7 +88,7 @@ extend google.protobuf.FieldOptions {
   //
   // ```proto
   // extend buf.validate.Int32Rules {
-  //   bool is_zero [(buf.validate.predefined).cel = {
+  //   bool is_zero = 1001 [(buf.validate.predefined).cel = {
   //     id: "int32.is_zero",
   //     message: "must be zero",
   //     expression: "!rule || this == 0",
@@ -1747,7 +1747,7 @@ message UInt64Rules {
   // message MyUInt64 {
   //   uint64 value = 1 [
   //     (buf.validate.field).uint64.example = 1,
-  //     (buf.validate.field).uint64.example = -10
+  //     (buf.validate.field).uint64.example = 10
   //   ];
   // }
   // ```
@@ -3028,8 +3028,8 @@ message BoolRules {
   // ```proto
   // message MyBool {
   //   bool value = 1 [
-  //     (buf.validate.field).bool.example = 1,
-  //     (buf.validate.field).bool.example = 2
+  //     (buf.validate.field).bool.example = true,
+  //     (buf.validate.field).bool.example = false
   //   ];
   // }
   // ```
@@ -3239,7 +3239,7 @@ message StringRules {
   // ```proto
   // message MyString {
   //   // must be in list ["apple", "banana"]
-  //   string value = 1 [(buf.validate.field).string.in = "apple", (buf.validate.field).string.in = "banana"];
+  //   string value = 1 [(buf.validate.field).string = { in: ["apple", "banana"] }];
   // }
   // ```
   repeated string in = 10 [(predefined).cel = {
@@ -3253,7 +3253,7 @@ message StringRules {
   // ```proto
   // message MyString {
   //   // value must not be in list ["orange", "grape"]
-  //   string value = 1 [(buf.validate.field).string.not_in = "orange", (buf.validate.field).string.not_in = "grape"];
+  //   string value = 1 [(buf.validate.field).string = { not_in: ["orange", "grape"] }];
   // }
   // ```
   repeated string not_in = 11 [(predefined).cel = {
@@ -4025,7 +4025,7 @@ message BytesRules {
   // ```proto
   // message MyBytes {
   //   // value must in ["\x01\x02", "\x02\x03", "\x03\x04"]
-  //   optional bytes value = 1 [(buf.validate.field).bytes.in = {"\x01\x02", "\x02\x03", "\x03\x04"}];
+  //   optional bytes value = 1 [(buf.validate.field).bytes = { in: ["\x01\x02", "\x02\x03", "\x03\x04"] }];
   // }
   // ```
   repeated bytes in = 8 [(predefined).cel = {
@@ -4041,7 +4041,7 @@ message BytesRules {
   // ```proto
   // message MyBytes {
   //   // value must not in ["\x01\x02", "\x02\x03", "\x03\x04"]
-  //   optional bytes value = 1 [(buf.validate.field).bytes.not_in = {"\x01\x02", "\x02\x03", "\x03\x04"}];
+  //   optional bytes value = 1 [(buf.validate.field).bytes = { not_in: ["\x01\x02", "\x02\x03", "\x03\x04"] }];
   // }
   // ```
   repeated bytes not_in = 9 [(predefined).cel = {
@@ -4258,8 +4258,10 @@ message EnumRules {
   // }
   //
   // message MyMessage {
+  //   MyEnum value = 1 [
   //     (buf.validate.field).enum.example = 1,
   //     (buf.validate.field).enum.example = 2
+  //   ];
   // }
   // ```
   repeated int32 example = 5 [(predefined).cel = {
@@ -4647,7 +4649,7 @@ message DurationRules {
   // message MyDuration {
   //   google.protobuf.Duration value = 1 [
   //     (buf.validate.field).duration.example = { seconds: 1 },
-  //     (buf.validate.field).duration.example = { seconds: 2 },
+  //     (buf.validate.field).duration.example = { seconds: 2 }
   //   ];
   // }
   // ```
@@ -4727,7 +4729,7 @@ message FieldMaskRules {
   // message MyFieldMask {
   //   google.protobuf.FieldMask value = 1 [
   //     (buf.validate.field).field_mask.example = { paths: ["a", "b"] },
-  //     (buf.validate.field).field_mask.example = { paths: ["c.a", "d"] },
+  //     (buf.validate.field).field_mask.example = { paths: ["c.a", "d"] }
   //   ];
   // }
   // ```
@@ -4938,7 +4940,7 @@ message TimestampRules {
   // message MyTimestamp {
   //   google.protobuf.Timestamp value = 1 [
   //     (buf.validate.field).timestamp.example = { seconds: 1672444800 },
-  //     (buf.validate.field).timestamp.example = { seconds: 1672531200 },
+  //     (buf.validate.field).timestamp.example = { seconds: 1672531200 }
   //   ];
   // }
   // ```
@@ -5038,7 +5040,7 @@ message Violation {
   //   bool b = 2 [(buf.validate.field).cel = {
   //     id: "custom_rule",
   //     expression: "!this ? 'b must be true': ''"
-  //   }]
+  //   }];
   // }
   // ```
   //

+ 40 - 0
proto/chatto/admin/v1/diagnostics.proto

@@ -2,6 +2,8 @@ syntax = "proto3";
 
 package chatto.admin.v1;
 
+import "google/protobuf/timestamp.proto";
+
 // Request to read owner-only server diagnostics.
 message GetSystemInfoRequest {}
 
@@ -11,6 +13,44 @@ message GetSystemInfoResponse {
   AdminSystemInfo system_info = 1;
   // Event-sourced projection health and memory estimates.
   repeated AdminProjectionState projections = 2;
+  // Recoverable physical deletion worker health.
+  AdminAssetCleanupStatus asset_cleanup = 3;
+}
+
+// Current health of recoverable message-owned asset deletion.
+message AdminAssetCleanupStatus {
+  // Operator-facing worker state derived from lease and heartbeat state.
+  AdminAssetCleanupHealth health = 1;
+  // Deletion effects currently waiting for a successful retry.
+  int64 pending_count = 2;
+  // Creation time of the oldest pending deletion, when known.
+  google.protobuf.Timestamp oldest_pending_at = 3;
+  // Whether the elected worker is currently running a cleanup pass.
+  bool pass_in_progress = 4;
+  // Completion time of the most recent pass.
+  google.protobuf.Timestamp last_pass_at = 5;
+  // Completion time of the most recent fully successful pass.
+  google.protobuf.Timestamp last_successful_pass_at = 6;
+  // Time of the latest elected-worker heartbeat.
+  google.protobuf.Timestamp updated_at = 7;
+  // Whether the most recent completed pass had one or more failures.
+  bool last_pass_failed = 8;
+  // Global EVT sequence through which deletion facts were inspected.
+  string last_inspected_sequence = 9;
+  // Current latest global EVT sequence matching asset deletion facts.
+  string latest_deletion_sequence = 10;
+}
+
+// Operator-facing state for the elected asset cleanup worker.
+enum AdminAssetCleanupHealth {
+  ADMIN_ASSET_CLEANUP_HEALTH_UNSPECIFIED = 0;
+  ADMIN_ASSET_CLEANUP_HEALTH_INACTIVE = 1;
+  ADMIN_ASSET_CLEANUP_HEALTH_INITIALIZING = 2;
+  ADMIN_ASSET_CLEANUP_HEALTH_HEALTHY = 3;
+  ADMIN_ASSET_CLEANUP_HEALTH_RETRYING = 4;
+  ADMIN_ASSET_CLEANUP_HEALTH_STALLED = 5;
+  // Cleanup diagnostics could not be read; other system diagnostics remain valid.
+  ADMIN_ASSET_CLEANUP_HEALTH_UNAVAILABLE = 6;
 }
 
 // Broker and application diagnostics shown on the server admin system page.

+ 7 - 2
proto/chatto/api/v1/account.proto

@@ -9,6 +9,7 @@ import "chatto/api/v1/presence.proto";
 import "chatto/api/v1/user_status.proto";
 import "chatto/api/v1/users.proto";
 import "chatto/api/v1/viewer.proto";
+import "google/protobuf/descriptor.proto";
 
 // Request to update the authenticated user's profile. At least one field must
 // be present.
@@ -119,7 +120,9 @@ service MyAccountService {
   // Uploads and sets the authenticated user's avatar.
   rpc UploadAvatar(UploadAvatarRequest) returns (UploadAvatarResponse);
   // Deletes the authenticated user's avatar. The call is idempotent.
-  rpc DeleteAvatar(DeleteAvatarRequest) returns (DeleteAvatarResponse);
+  rpc DeleteAvatar(DeleteAvatarRequest) returns (DeleteAvatarResponse) {
+    option idempotency_level = IDEMPOTENT;
+  }
   // Updates or adds the authenticated user's password.
   rpc UpdatePassword(UpdatePasswordRequest) returns (UpdatePasswordResponse);
   // Updates the authenticated user's display preferences.
@@ -142,7 +145,9 @@ service MyAccountService {
   rpc UpdateCustomStatus(UpdateCustomStatusRequest) returns (UpdateCustomStatusResponse);
   // Deletes the current user's custom status. The call is idempotent and returns
   // the resulting empty status state.
-  rpc DeleteCustomStatus(DeleteCustomStatusRequest) returns (DeleteCustomStatusResponse);
+  rpc DeleteCustomStatus(DeleteCustomStatusRequest) returns (DeleteCustomStatusResponse) {
+    option idempotency_level = IDEMPOTENT;
+  }
   // Issues a short-lived token used to confirm self-deletion.
   rpc RequestAccountDeletion(RequestAccountDeletionRequest) returns (RequestAccountDeletionResponse);
   // Permanently deletes the authenticated account after token validation.

+ 3 - 0
proto/chatto/api/v1/message_types.proto

@@ -172,4 +172,7 @@ message Message {
   repeated MessageReaction reactions = 19;
   // Aggregated thread state, when known for a thread root message.
   ThreadSummary thread = 20;
+  // Time when the message content was deleted through retraction or account
+  // crypto-shredding. Absent when unavailable body content is not a deletion.
+  google.protobuf.Timestamp deleted_at = 21;
 }

+ 10 - 1
proto/chatto/api/v1/messages.proto

@@ -28,7 +28,16 @@ message CreateMessageRequest {
   // Message body text. Required unless attachment_asset_ids is non-empty.
   string body = 2;
   // Existing room-scoped attachment asset IDs to include with the message.
-  repeated string attachment_asset_ids = 3;
+  // At most 10 IDs may be supplied.
+  repeated string attachment_asset_ids = 3 [(buf.validate.field).repeated = {
+    max_items: 10
+    items: {
+      string: {
+        min_len: 1
+        max_bytes: 15
+      }
+    }
+  }];
   // Event ID of the thread root message when posting a thread reply.
   string thread_root_event_id = 4;
   // Event ID this message replies to for attribution.

+ 4 - 1
proto/chatto/api/v1/notifications.proto

@@ -6,6 +6,7 @@ import "buf/validate/validate.proto";
 import "chatto/api/v1/pagination.proto";
 import "chatto/api/v1/rooms.proto";
 import "chatto/api/v1/users.proto";
+import "google/protobuf/descriptor.proto";
 import "google/protobuf/timestamp.proto";
 
 // Direct-message notification payload.
@@ -217,7 +218,9 @@ service NotificationService {
   rpc HasNotifications(HasNotificationsRequest) returns (HasNotificationsResponse);
   // Dismisses one pending notification. Already-dismissed notifications are
   // treated as idempotent success.
-  rpc DismissNotification(DismissNotificationRequest) returns (DismissNotificationResponse);
+  rpc DismissNotification(DismissNotificationRequest) returns (DismissNotificationResponse) {
+    option idempotency_level = IDEMPOTENT;
+  }
   // Dismisses all pending notifications.
   rpc DismissAllNotifications(DismissAllNotificationsRequest) returns (DismissAllNotificationsResponse);
 }

+ 4 - 1
proto/chatto/api/v1/push_notifications.proto

@@ -3,6 +3,7 @@ syntax = "proto3";
 package chatto.api.v1;
 
 import "buf/validate/validate.proto";
+import "google/protobuf/descriptor.proto";
 
 // Browser Web Push subscription commands for the current user.
 service PushNotificationService {
@@ -15,7 +16,9 @@ service PushNotificationService {
   // Removes the caller's browser push subscription by endpoint.
   //
   // The call is idempotent: removing an unknown endpoint still succeeds.
-  rpc Unsubscribe(UnsubscribePushRequest) returns (UnsubscribePushResponse);
+  rpc Unsubscribe(UnsubscribePushRequest) returns (UnsubscribePushResponse) {
+    option idempotency_level = IDEMPOTENT;
+  }
 }
 
 // Request to store a PushSubscription returned by the browser Push API.

+ 4 - 1
proto/chatto/discovery/v1/server.proto

@@ -3,6 +3,7 @@ syntax = "proto3";
 package chatto.discovery.v1;
 
 import "chatto/api/v1/server.proto";
+import "google/protobuf/descriptor.proto";
 
 // Request for public server metadata.
 message GetServerRequest {}
@@ -21,5 +22,7 @@ service ServerDiscoveryService {
   // Returns public server metadata, branding, registration status, and login
   // provider information. This fully public RPC is available before login so
   // clients can render the first server screen and authentication choices.
-  rpc GetServer(GetServerRequest) returns (GetServerResponse);
+  rpc GetServer(GetServerRequest) returns (GetServerResponse) {
+    option idempotency_level = NO_SIDE_EFFECTS;
+  }
 }

+ 1 - 1
pyproject.toml

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "chattolib"
-version = "0.4.3"
+version = "0.4.9"
 description = "Async Python client library for the Chatto webchat Connect API"
 requires-python = ">=3.11"
 license = "MIT"

Різницю між файлами не показано, бо вона завелика
+ 1 - 0
src/chattolib/_pb/chatto/admin/v1/diagnostics_pb2.py


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

@@ -91,7 +91,7 @@ class MyAccountServiceASGIApplication(ConnectASGIApplication[MyAccountService]):
                         service_name="chatto.api.v1.MyAccountService",
                         input=chatto_dot_api_dot_v1_dot_account__pb2.DeleteAvatarRequest,
                         output=chatto_dot_api_dot_v1_dot_account__pb2.DeleteAvatarResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.IDEMPOTENT,
                     ),
                     function=svc.delete_avatar,
                 ),
@@ -171,7 +171,7 @@ class MyAccountServiceASGIApplication(ConnectASGIApplication[MyAccountService]):
                         service_name="chatto.api.v1.MyAccountService",
                         input=chatto_dot_api_dot_v1_dot_user__status__pb2.DeleteCustomStatusRequest,
                         output=chatto_dot_api_dot_v1_dot_user__status__pb2.DeleteCustomStatusResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.IDEMPOTENT,
                     ),
                     function=svc.delete_custom_status,
                 ),
@@ -262,7 +262,7 @@ class MyAccountServiceClient(ConnectClient):
                 service_name="chatto.api.v1.MyAccountService",
                 input=chatto_dot_api_dot_v1_dot_account__pb2.DeleteAvatarRequest,
                 output=chatto_dot_api_dot_v1_dot_account__pb2.DeleteAvatarResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.IDEMPOTENT,
             ),
             headers=headers,
             timeout_ms=timeout_ms,
@@ -422,7 +422,7 @@ class MyAccountServiceClient(ConnectClient):
                 service_name="chatto.api.v1.MyAccountService",
                 input=chatto_dot_api_dot_v1_dot_user__status__pb2.DeleteCustomStatusRequest,
                 output=chatto_dot_api_dot_v1_dot_user__status__pb2.DeleteCustomStatusResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.IDEMPOTENT,
             ),
             headers=headers,
             timeout_ms=timeout_ms,
@@ -528,7 +528,7 @@ class MyAccountServiceWSGIApplication(ConnectWSGIApplication):
                         service_name="chatto.api.v1.MyAccountService",
                         input=chatto_dot_api_dot_v1_dot_account__pb2.DeleteAvatarRequest,
                         output=chatto_dot_api_dot_v1_dot_account__pb2.DeleteAvatarResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.IDEMPOTENT,
                     ),
                     function=service.delete_avatar,
                 ),
@@ -608,7 +608,7 @@ class MyAccountServiceWSGIApplication(ConnectWSGIApplication):
                         service_name="chatto.api.v1.MyAccountService",
                         input=chatto_dot_api_dot_v1_dot_user__status__pb2.DeleteCustomStatusRequest,
                         output=chatto_dot_api_dot_v1_dot_user__status__pb2.DeleteCustomStatusResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.IDEMPOTENT,
                     ),
                     function=service.delete_custom_status,
                 ),
@@ -699,7 +699,7 @@ class MyAccountServiceClientSync(ConnectClientSync):
                 service_name="chatto.api.v1.MyAccountService",
                 input=chatto_dot_api_dot_v1_dot_account__pb2.DeleteAvatarRequest,
                 output=chatto_dot_api_dot_v1_dot_account__pb2.DeleteAvatarResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.IDEMPOTENT,
             ),
             headers=headers,
             timeout_ms=timeout_ms,
@@ -859,7 +859,7 @@ class MyAccountServiceClientSync(ConnectClientSync):
                 service_name="chatto.api.v1.MyAccountService",
                 input=chatto_dot_api_dot_v1_dot_user__status__pb2.DeleteCustomStatusRequest,
                 output=chatto_dot_api_dot_v1_dot_user__status__pb2.DeleteCustomStatusResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.IDEMPOTENT,
             ),
             headers=headers,
             timeout_ms=timeout_ms,

Різницю між файлами не показано, бо вона завелика
+ 1 - 0
src/chattolib/_pb/chatto/api/v1/account_pb2.py


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
src/chattolib/_pb/chatto/api/v1/message_types_pb2.py


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
src/chattolib/_pb/chatto/api/v1/messages_pb2.py


+ 4 - 4
src/chattolib/_pb/chatto/api/v1/notifications_connect.py

@@ -113,7 +113,7 @@ class NotificationServiceASGIApplication(ConnectASGIApplication[NotificationServ
                         service_name="chatto.api.v1.NotificationService",
                         input=chatto_dot_api_dot_v1_dot_notifications__pb2.DismissNotificationRequest,
                         output=chatto_dot_api_dot_v1_dot_notifications__pb2.DismissNotificationResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.IDEMPOTENT,
                     ),
                     function=svc.dismiss_notification,
                 ),
@@ -274,7 +274,7 @@ class NotificationServiceClient(ConnectClient):
                 service_name="chatto.api.v1.NotificationService",
                 input=chatto_dot_api_dot_v1_dot_notifications__pb2.DismissNotificationRequest,
                 output=chatto_dot_api_dot_v1_dot_notifications__pb2.DismissNotificationResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.IDEMPOTENT,
             ),
             headers=headers,
             timeout_ms=timeout_ms,
@@ -390,7 +390,7 @@ class NotificationServiceWSGIApplication(ConnectWSGIApplication):
                         service_name="chatto.api.v1.NotificationService",
                         input=chatto_dot_api_dot_v1_dot_notifications__pb2.DismissNotificationRequest,
                         output=chatto_dot_api_dot_v1_dot_notifications__pb2.DismissNotificationResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.IDEMPOTENT,
                     ),
                     function=service.dismiss_notification,
                 ),
@@ -551,7 +551,7 @@ class NotificationServiceClientSync(ConnectClientSync):
                 service_name="chatto.api.v1.NotificationService",
                 input=chatto_dot_api_dot_v1_dot_notifications__pb2.DismissNotificationRequest,
                 output=chatto_dot_api_dot_v1_dot_notifications__pb2.DismissNotificationResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.IDEMPOTENT,
             ),
             headers=headers,
             timeout_ms=timeout_ms,

Різницю між файлами не показано, бо вона завелика
+ 1 - 0
src/chattolib/_pb/chatto/api/v1/notifications_pb2.py


+ 4 - 4
src/chattolib/_pb/chatto/api/v1/push_notifications_connect.py

@@ -45,7 +45,7 @@ class PushNotificationServiceASGIApplication(ConnectASGIApplication[PushNotifica
                         service_name="chatto.api.v1.PushNotificationService",
                         input=chatto_dot_api_dot_v1_dot_push__notifications__pb2.UnsubscribePushRequest,
                         output=chatto_dot_api_dot_v1_dot_push__notifications__pb2.UnsubscribePushResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.IDEMPOTENT,
                     ),
                     function=svc.unsubscribe,
                 ),
@@ -96,7 +96,7 @@ class PushNotificationServiceClient(ConnectClient):
                 service_name="chatto.api.v1.PushNotificationService",
                 input=chatto_dot_api_dot_v1_dot_push__notifications__pb2.UnsubscribePushRequest,
                 output=chatto_dot_api_dot_v1_dot_push__notifications__pb2.UnsubscribePushResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.IDEMPOTENT,
             ),
             headers=headers,
             timeout_ms=timeout_ms,
@@ -130,7 +130,7 @@ class PushNotificationServiceWSGIApplication(ConnectWSGIApplication):
                         service_name="chatto.api.v1.PushNotificationService",
                         input=chatto_dot_api_dot_v1_dot_push__notifications__pb2.UnsubscribePushRequest,
                         output=chatto_dot_api_dot_v1_dot_push__notifications__pb2.UnsubscribePushResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.IDEMPOTENT,
                     ),
                     function=service.unsubscribe,
                 ),
@@ -181,7 +181,7 @@ class PushNotificationServiceClientSync(ConnectClientSync):
                 service_name="chatto.api.v1.PushNotificationService",
                 input=chatto_dot_api_dot_v1_dot_push__notifications__pb2.UnsubscribePushRequest,
                 output=chatto_dot_api_dot_v1_dot_push__notifications__pb2.UnsubscribePushResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.IDEMPOTENT,
             ),
             headers=headers,
             timeout_ms=timeout_ms,

+ 14 - 11
src/chattolib/_pb/chatto/api/v1/push_notifications_pb2.py

@@ -23,9 +23,10 @@ _sym_db = _symbol_database.Default()
 
 
 from buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2
+from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
 
 
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&chatto/api/v1/push_notifications.proto\x12\rchatto.api.v1\x1a\x1b\x62uf/validate/validate.proto\"\x9c\x01\n\x14SubscribePushRequest\x12\x1c\n\x08\x65ndpoint\x18\x01 \x01(\tB\n\xbaH\x07r\x05\x10\x01\x18\x80 \x12\x1a\n\x06p256dh\x18\x02 \x01(\tB\n\xbaH\x07r\x05\x10\x01\x18\x80\x02\x12\x18\n\x04\x61uth\x18\x03 \x01(\tB\n\xbaH\x07r\x05\x10\x01\x18\x80\x01\x12!\n\nuser_agent\x18\x04 \x01(\tB\x08\xbaH\x05r\x03\x18\x80\x04H\x00\x88\x01\x01\x42\r\n\x0b_user_agent\"+\n\x15SubscribePushResponse\x12\x12\n\nsubscribed\x18\x01 \x01(\x08\"6\n\x16UnsubscribePushRequest\x12\x1c\n\x08\x65ndpoint\x18\x01 \x01(\tB\n\xbaH\x07r\x05\x10\x01\x18\x80 \"/\n\x17UnsubscribePushResponse\x12\x14\n\x0cunsubscribed\x18\x01 \x01(\x08\x32\xcf\x01\n\x17PushNotificationService\x12V\n\tSubscribe\x12#.chatto.api.v1.SubscribePushRequest\x1a$.chatto.api.v1.SubscribePushResponse\x12\\\n\x0bUnsubscribe\x12%.chatto.api.v1.UnsubscribePushRequest\x1a&.chatto.api.v1.UnsubscribePushResponseb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&chatto/api/v1/push_notifications.proto\x12\rchatto.api.v1\x1a\x1b\x62uf/validate/validate.proto\x1a google/protobuf/descriptor.proto\"\x9c\x01\n\x14SubscribePushRequest\x12\x1c\n\x08\x65ndpoint\x18\x01 \x01(\tB\n\xbaH\x07r\x05\x10\x01\x18\x80 \x12\x1a\n\x06p256dh\x18\x02 \x01(\tB\n\xbaH\x07r\x05\x10\x01\x18\x80\x02\x12\x18\n\x04\x61uth\x18\x03 \x01(\tB\n\xbaH\x07r\x05\x10\x01\x18\x80\x01\x12!\n\nuser_agent\x18\x04 \x01(\tB\x08\xbaH\x05r\x03\x18\x80\x04H\x00\x88\x01\x01\x42\r\n\x0b_user_agent\"+\n\x15SubscribePushResponse\x12\x12\n\nsubscribed\x18\x01 \x01(\x08\"6\n\x16UnsubscribePushRequest\x12\x1c\n\x08\x65ndpoint\x18\x01 \x01(\tB\n\xbaH\x07r\x05\x10\x01\x18\x80 \"/\n\x17UnsubscribePushResponse\x12\x14\n\x0cunsubscribed\x18\x01 \x01(\x08\x32\xd4\x01\n\x17PushNotificationService\x12V\n\tSubscribe\x12#.chatto.api.v1.SubscribePushRequest\x1a$.chatto.api.v1.SubscribePushResponse\x12\x61\n\x0bUnsubscribe\x12%.chatto.api.v1.UnsubscribePushRequest\x1a&.chatto.api.v1.UnsubscribePushResponse\"\x03\x90\x02\x02\x62\x06proto3')
 
 _globals = globals()
 _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -42,14 +43,16 @@ if not _descriptor._USE_C_DESCRIPTORS:
   _globals['_SUBSCRIBEPUSHREQUEST'].fields_by_name['user_agent']._serialized_options = b'\272H\005r\003\030\200\004'
   _globals['_UNSUBSCRIBEPUSHREQUEST'].fields_by_name['endpoint']._loaded_options = None
   _globals['_UNSUBSCRIBEPUSHREQUEST'].fields_by_name['endpoint']._serialized_options = b'\272H\007r\005\020\001\030\200 '
-  _globals['_SUBSCRIBEPUSHREQUEST']._serialized_start=87
-  _globals['_SUBSCRIBEPUSHREQUEST']._serialized_end=243
-  _globals['_SUBSCRIBEPUSHRESPONSE']._serialized_start=245
-  _globals['_SUBSCRIBEPUSHRESPONSE']._serialized_end=288
-  _globals['_UNSUBSCRIBEPUSHREQUEST']._serialized_start=290
-  _globals['_UNSUBSCRIBEPUSHREQUEST']._serialized_end=344
-  _globals['_UNSUBSCRIBEPUSHRESPONSE']._serialized_start=346
-  _globals['_UNSUBSCRIBEPUSHRESPONSE']._serialized_end=393
-  _globals['_PUSHNOTIFICATIONSERVICE']._serialized_start=396
-  _globals['_PUSHNOTIFICATIONSERVICE']._serialized_end=603
+  _globals['_PUSHNOTIFICATIONSERVICE'].methods_by_name['Unsubscribe']._loaded_options = None
+  _globals['_PUSHNOTIFICATIONSERVICE'].methods_by_name['Unsubscribe']._serialized_options = b'\220\002\002'
+  _globals['_SUBSCRIBEPUSHREQUEST']._serialized_start=121
+  _globals['_SUBSCRIBEPUSHREQUEST']._serialized_end=277
+  _globals['_SUBSCRIBEPUSHRESPONSE']._serialized_start=279
+  _globals['_SUBSCRIBEPUSHRESPONSE']._serialized_end=322
+  _globals['_UNSUBSCRIBEPUSHREQUEST']._serialized_start=324
+  _globals['_UNSUBSCRIBEPUSHREQUEST']._serialized_end=378
+  _globals['_UNSUBSCRIBEPUSHRESPONSE']._serialized_start=380
+  _globals['_UNSUBSCRIBEPUSHRESPONSE']._serialized_end=427
+  _globals['_PUSHNOTIFICATIONSERVICE']._serialized_start=430
+  _globals['_PUSHNOTIFICATIONSERVICE']._serialized_end=642
 # @@protoc_insertion_point(module_scope)

+ 8 - 4
src/chattolib/_pb/chatto/discovery/v1/server_connect.py

@@ -32,7 +32,7 @@ class ServerDiscoveryServiceASGIApplication(ConnectASGIApplication[ServerDiscove
                         service_name="chatto.discovery.v1.ServerDiscoveryService",
                         input=chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerRequest,
                         output=chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
                     ),
                     function=svc.get_server,
                 ),
@@ -55,6 +55,7 @@ class ServerDiscoveryServiceClient(ConnectClient):
         *,
         headers: Headers | Mapping[str, str] | None = None,
         timeout_ms: int | None = None,
+        use_get: bool = False,
     ) -> chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerResponse:
         return await self.execute_unary(
             request=request,
@@ -63,10 +64,11 @@ class ServerDiscoveryServiceClient(ConnectClient):
                 service_name="chatto.discovery.v1.ServerDiscoveryService",
                 input=chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerRequest,
                 output=chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
             ),
             headers=headers,
             timeout_ms=timeout_ms,
+            use_get=use_get,
         )
 
 
@@ -85,7 +87,7 @@ class ServerDiscoveryServiceWSGIApplication(ConnectWSGIApplication):
                         service_name="chatto.discovery.v1.ServerDiscoveryService",
                         input=chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerRequest,
                         output=chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerResponse,
-                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                        idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
                     ),
                     function=service.get_server,
                 ),
@@ -108,6 +110,7 @@ class ServerDiscoveryServiceClientSync(ConnectClientSync):
         *,
         headers: Headers | Mapping[str, str] | None = None,
         timeout_ms: int | None = None,
+        use_get: bool = False,
     ) -> chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerResponse:
         return self.execute_unary(
             request=request,
@@ -116,8 +119,9 @@ class ServerDiscoveryServiceClientSync(ConnectClientSync):
                 service_name="chatto.discovery.v1.ServerDiscoveryService",
                 input=chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerRequest,
                 output=chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerResponse,
-                idempotency_level=IdempotencyLevel.UNKNOWN,
+                idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
             ),
             headers=headers,
             timeout_ms=timeout_ms,
+            use_get=use_get,
         )

+ 10 - 7
src/chattolib/_pb/chatto/discovery/v1/server_pb2.py

@@ -23,19 +23,22 @@ _sym_db = _symbol_database.Default()
 
 
 from chatto.api.v1 import server_pb2 as chatto_dot_api_dot_v1_dot_server__pb2
+from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
 
 
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n chatto/discovery/v1/server.proto\x12\x13\x63hatto.discovery.v1\x1a\x1a\x63hatto/api/v1/server.proto\"\x12\n\x10GetServerRequest\"s\n\x11GetServerResponse\x12\x33\n\x07profile\x18\x01 \x01(\x0b\x32\".chatto.api.v1.ServerPublicProfile\x12)\n\x05login\x18\x02 \x01(\x0b\x32\x1a.chatto.api.v1.ServerLogin2t\n\x16ServerDiscoveryService\x12Z\n\tGetServer\x12%.chatto.discovery.v1.GetServerRequest\x1a&.chatto.discovery.v1.GetServerResponseb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n chatto/discovery/v1/server.proto\x12\x13\x63hatto.discovery.v1\x1a\x1a\x63hatto/api/v1/server.proto\x1a google/protobuf/descriptor.proto\"\x12\n\x10GetServerRequest\"s\n\x11GetServerResponse\x12\x33\n\x07profile\x18\x01 \x01(\x0b\x32\".chatto.api.v1.ServerPublicProfile\x12)\n\x05login\x18\x02 \x01(\x0b\x32\x1a.chatto.api.v1.ServerLogin2y\n\x16ServerDiscoveryService\x12_\n\tGetServer\x12%.chatto.discovery.v1.GetServerRequest\x1a&.chatto.discovery.v1.GetServerResponse\"\x03\x90\x02\x01\x62\x06proto3')
 
 _globals = globals()
 _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
 _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'chatto.discovery.v1.server_pb2', _globals)
 if not _descriptor._USE_C_DESCRIPTORS:
   DESCRIPTOR._loaded_options = None
-  _globals['_GETSERVERREQUEST']._serialized_start=85
-  _globals['_GETSERVERREQUEST']._serialized_end=103
-  _globals['_GETSERVERRESPONSE']._serialized_start=105
-  _globals['_GETSERVERRESPONSE']._serialized_end=220
-  _globals['_SERVERDISCOVERYSERVICE']._serialized_start=222
-  _globals['_SERVERDISCOVERYSERVICE']._serialized_end=338
+  _globals['_SERVERDISCOVERYSERVICE'].methods_by_name['GetServer']._loaded_options = None
+  _globals['_SERVERDISCOVERYSERVICE'].methods_by_name['GetServer']._serialized_options = b'\220\002\001'
+  _globals['_GETSERVERREQUEST']._serialized_start=119
+  _globals['_GETSERVERREQUEST']._serialized_end=137
+  _globals['_GETSERVERRESPONSE']._serialized_start=139
+  _globals['_GETSERVERRESPONSE']._serialized_end=254
+  _globals['_SERVERDISCOVERYSERVICE']._serialized_start=256
+  _globals['_SERVERDISCOVERYSERVICE']._serialized_end=377
 # @@protoc_insertion_point(module_scope)

+ 2 - 0
src/chattolib/types.py

@@ -646,6 +646,7 @@ class Message:
     channel_echo_event_id: str = ""
     reactions: list[MessageReaction] = field(default_factory=list)
     thread: ThreadSummary | None = None
+    deleted_at: datetime | None = None
 
     @classmethod
     def parse(cls, data: dict[str, Any] | None) -> Message | None:
@@ -667,6 +668,7 @@ class Message:
             channel_echo_event_id=data.get("channelEchoEventId", ""),
             reactions=[MessageReaction.parse(r) for r in data.get("reactions") or []],
             thread=ThreadSummary.parse(data.get("thread")),
+            deleted_at=parse_datetime(data.get("deletedAt")),
         )
 
 

Деякі файли не було показано, через те що забагато файлів було змінено