Kaynağa Gözat

Upgrade chattolib to 0.5.0a3 (Chatto v0.5.0-alpha.3)

- Regenerate protos + _pb bindings pinned to v0.5.0-alpha.3
- Neighbors: public ServerDiscoveryService.ListNeighbors; admin
  List/Get/Create/Update/DeleteNeighbor (server.manage-neighbors);
  new Neighbor dataclass
- share_timezone: new optional bool on UserSettings and
  UpdateSettingsRequest; update_settings() accepts it
- Bump version to 0.5.0a3
the-codemancer 2 ay önce
ebeveyn
işleme
ff5dfbfef3

+ 71 - 0
proto/chatto/admin/v1/server.proto

@@ -140,6 +140,65 @@ message UpdateBlockedUsernamesResponse {
   repeated string blocked_usernames = 1;
 }
 
+// One server advertised in the public Neighbor directory.
+message Neighbor {
+  // Stable opaque resource ID.
+  string id = 1;
+  // Canonical HTTP or HTTPS server origin.
+  string origin = 2;
+  // Opaque revision required for update and delete requests.
+  string revision = 3;
+}
+
+// Request to list configured Neighbors.
+message ListNeighborsRequest {}
+
+// Configured Neighbors. The response has no ordering contract.
+message ListNeighborsResponse {
+  repeated Neighbor neighbors = 1;
+}
+
+// Request for one configured Neighbor.
+message GetNeighborRequest {
+  string neighbor_id = 1 [(buf.validate.field).string = {min_len: 1, max_len: 64}];
+}
+
+// One configured Neighbor.
+message GetNeighborResponse {
+  Neighbor neighbor = 1;
+}
+
+// Request to advertise one server origin.
+message CreateNeighborRequest {
+  string origin = 1 [(buf.validate.field).string = {min_len: 1, max_len: 2048}];
+}
+
+// Newly configured Neighbor.
+message CreateNeighborResponse {
+  Neighbor neighbor = 1;
+}
+
+// Request to change one advertised server origin.
+message UpdateNeighborRequest {
+  string neighbor_id = 1 [(buf.validate.field).string = {min_len: 1, max_len: 64}];
+  string origin = 2 [(buf.validate.field).string = {min_len: 1, max_len: 2048}];
+  string revision = 3 [(buf.validate.field).string = {min_len: 1, max_len: 64}];
+}
+
+// Updated Neighbor.
+message UpdateNeighborResponse {
+  Neighbor neighbor = 1;
+}
+
+// Request to stop advertising one server origin.
+message DeleteNeighborRequest {
+  string neighbor_id = 1 [(buf.validate.field).string = {min_len: 1, max_len: 64}];
+  string revision = 2 [(buf.validate.field).string = {min_len: 1, max_len: 64}];
+}
+
+// Empty result after a Neighbor is deleted.
+message DeleteNeighborResponse {}
+
 // Provides server-administration commands for profile, branding, and security
 // settings.
 service AdminServerService {
@@ -162,4 +221,16 @@ service AdminServerService {
   rpc GetServerSecurityConfig(GetServerSecurityConfigRequest) returns (GetServerSecurityConfigResponse);
   // Updates the blocked-username list. This RPC requires server.manage.
   rpc UpdateBlockedUsernames(UpdateBlockedUsernamesRequest) returns (UpdateBlockedUsernamesResponse);
+  // Lists configured Neighbors. This RPC requires server.manage-neighbors.
+  rpc ListNeighbors(ListNeighborsRequest) returns (ListNeighborsResponse);
+  // Gets one configured Neighbor. This RPC requires server.manage-neighbors.
+  rpc GetNeighbor(GetNeighborRequest) returns (GetNeighborResponse);
+  // Advertises one server origin. This RPC requires server.manage-neighbors.
+  rpc CreateNeighbor(CreateNeighborRequest) returns (CreateNeighborResponse);
+  // Changes one advertised server origin. This RPC requires
+  // server.manage-neighbors.
+  rpc UpdateNeighbor(UpdateNeighborRequest) returns (UpdateNeighborResponse);
+  // Stops advertising one server origin. This RPC requires
+  // server.manage-neighbors.
+  rpc DeleteNeighbor(DeleteNeighborRequest) returns (DeleteNeighborResponse);
 }

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

@@ -83,6 +83,8 @@ message UpdateSettingsRequest {
   optional string timezone = 1;
   // Preferred time format.
   optional TimeFormat time_format = 2;
+  // Whether the stored time zone may appear on the user's public profile.
+  optional bool share_timezone = 3;
 }
 
 // Result of updating display preferences.

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

@@ -25,8 +25,8 @@ message User {
   bool is_bot = 8;
   // Short self-authored biography shown on the user's profile, when set.
   optional string bio = 9;
-  // IANA time zone the user shares on their profile (for example
-  // "Europe/Berlin"), when set. Absent means the user has not chosen a
-  // shareable time zone.
+  // IANA time zone the user chose to share on their profile (for example
+  // "Europe/Berlin"). Absent means the user has not enabled time-zone
+  // sharing or has no stored time zone.
   optional string timezone = 10;
 }

+ 4 - 0
proto/chatto/api/v1/viewer.proto

@@ -25,6 +25,10 @@ message UserSettings {
   optional string timezone = 1;
   // Preferred time format.
   TimeFormat time_format = 2;
+  // Whether the stored time zone may appear on the user's public profile.
+  // New servers always set this field. Its absence means the server does not
+  // support private time zones and may publish any stored time zone.
+  optional bool share_timezone = 3;
 }
 
 // Current authenticated user's public profile plus self-only settings.

+ 13 - 0
proto/chatto/discovery/v1/server.proto

@@ -17,6 +17,15 @@ message GetServerResponse {
   chatto.api.v1.ServerLogin login = 2;
 }
 
+// Request for the public Neighbor directory.
+message ListNeighborsRequest {}
+
+// Publicly advertised canonical server origins. The response has no ordering
+// contract.
+message ListNeighborsResponse {
+  repeated string origins = 1;
+}
+
 // Provides unauthenticated server discovery metadata.
 service ServerDiscoveryService {
   // Returns public server metadata, branding, registration status, and login
@@ -25,4 +34,8 @@ service ServerDiscoveryService {
   rpc GetServer(GetServerRequest) returns (GetServerResponse) {
     option idempotency_level = NO_SIDE_EFFECTS;
   }
+  // Returns the public Neighbor directory without requiring a session.
+  rpc ListNeighbors(ListNeighborsRequest) returns (ListNeighborsResponse) {
+    option idempotency_level = NO_SIDE_EFFECTS;
+  }
 }

+ 1 - 1
pyproject.toml

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

+ 2 - 0
src/chattolib/__init__.py

@@ -38,6 +38,7 @@ from chattolib.types import (
     Message,
     MessageAttachment,
     MessageReaction,
+    Neighbor,
     NotificationLevel,
     NotificationMessageReference,
     NotificationOccurrence,
@@ -112,6 +113,7 @@ __all__ = [
     "Message",
     "MessageAttachment",
     "MessageReaction",
+    "Neighbor",
     "NotificationLevel",
     "NotificationMessageReference",
     "NotificationOccurrence",

+ 325 - 0
src/chattolib/_pb/chatto/admin/v1/server_connect.py

@@ -41,6 +41,21 @@ class AdminServerService(Protocol):
     async def update_blocked_usernames(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.UpdateBlockedUsernamesRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.UpdateBlockedUsernamesResponse:
         raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
 
+    async def list_neighbors(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+
+    async def get_neighbor(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+
+    async def create_neighbor(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+
+    async def update_neighbor(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+
+    async def delete_neighbor(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+
 
 class AdminServerServiceASGIApplication(ConnectASGIApplication[AdminServerService]):
     def __init__(self, service: AdminServerService | AsyncGenerator[AdminServerService], *, interceptors: Iterable[Interceptor]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None) -> None:
@@ -127,6 +142,56 @@ class AdminServerServiceASGIApplication(ConnectASGIApplication[AdminServerServic
                     ),
                     function=svc.update_blocked_usernames,
                 ),
+                "/chatto.admin.v1.AdminServerService/ListNeighbors": Endpoint.unary(
+                    method=MethodInfo(
+                        name="ListNeighbors",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=svc.list_neighbors,
+                ),
+                "/chatto.admin.v1.AdminServerService/GetNeighbor": Endpoint.unary(
+                    method=MethodInfo(
+                        name="GetNeighbor",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=svc.get_neighbor,
+                ),
+                "/chatto.admin.v1.AdminServerService/CreateNeighbor": Endpoint.unary(
+                    method=MethodInfo(
+                        name="CreateNeighbor",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=svc.create_neighbor,
+                ),
+                "/chatto.admin.v1.AdminServerService/UpdateNeighbor": Endpoint.unary(
+                    method=MethodInfo(
+                        name="UpdateNeighbor",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=svc.update_neighbor,
+                ),
+                "/chatto.admin.v1.AdminServerService/DeleteNeighbor": Endpoint.unary(
+                    method=MethodInfo(
+                        name="DeleteNeighbor",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=svc.delete_neighbor,
+                ),
             },
             interceptors=interceptors,
             read_max_bytes=read_max_bytes,
@@ -300,6 +365,106 @@ class AdminServerServiceClient(ConnectClient):
             timeout_ms=timeout_ms,
         )
 
+    async def list_neighbors(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsResponse:
+        return await self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="ListNeighbors",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
+    async def get_neighbor(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborResponse:
+        return await self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="GetNeighbor",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
+    async def create_neighbor(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborResponse:
+        return await self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="CreateNeighbor",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
+    async def update_neighbor(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborResponse:
+        return await self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="UpdateNeighbor",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
+    async def delete_neighbor(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborResponse:
+        return await self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="DeleteNeighbor",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
 
 class AdminServerServiceSync(Protocol):
     def get_server_config(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.GetServerConfigRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.GetServerConfigResponse:
@@ -318,6 +483,16 @@ class AdminServerServiceSync(Protocol):
         raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
     def update_blocked_usernames(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.UpdateBlockedUsernamesRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.UpdateBlockedUsernamesResponse:
         raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+    def list_neighbors(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+    def get_neighbor(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+    def create_neighbor(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+    def update_neighbor(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+    def delete_neighbor(self, request: chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborRequest, ctx: RequestContext) -> chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
 
 
 class AdminServerServiceWSGIApplication(ConnectWSGIApplication):
@@ -404,6 +579,56 @@ class AdminServerServiceWSGIApplication(ConnectWSGIApplication):
                     ),
                     function=service.update_blocked_usernames,
                 ),
+                "/chatto.admin.v1.AdminServerService/ListNeighbors": EndpointSync.unary(
+                    method=MethodInfo(
+                        name="ListNeighbors",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=service.list_neighbors,
+                ),
+                "/chatto.admin.v1.AdminServerService/GetNeighbor": EndpointSync.unary(
+                    method=MethodInfo(
+                        name="GetNeighbor",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=service.get_neighbor,
+                ),
+                "/chatto.admin.v1.AdminServerService/CreateNeighbor": EndpointSync.unary(
+                    method=MethodInfo(
+                        name="CreateNeighbor",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=service.create_neighbor,
+                ),
+                "/chatto.admin.v1.AdminServerService/UpdateNeighbor": EndpointSync.unary(
+                    method=MethodInfo(
+                        name="UpdateNeighbor",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=service.update_neighbor,
+                ),
+                "/chatto.admin.v1.AdminServerService/DeleteNeighbor": EndpointSync.unary(
+                    method=MethodInfo(
+                        name="DeleteNeighbor",
+                        service_name="chatto.admin.v1.AdminServerService",
+                        input=chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborRequest,
+                        output=chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborResponse,
+                        idempotency_level=IdempotencyLevel.UNKNOWN,
+                    ),
+                    function=service.delete_neighbor,
+                ),
             },
             interceptors=interceptors,
             read_max_bytes=read_max_bytes,
@@ -576,3 +801,103 @@ class AdminServerServiceClientSync(ConnectClientSync):
             headers=headers,
             timeout_ms=timeout_ms,
         )
+
+    def list_neighbors(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsResponse:
+        return self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="ListNeighbors",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.ListNeighborsResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
+    def get_neighbor(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborResponse:
+        return self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="GetNeighbor",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.GetNeighborResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
+    def create_neighbor(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborResponse:
+        return self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="CreateNeighbor",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.CreateNeighborResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
+    def update_neighbor(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborResponse:
+        return self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="UpdateNeighbor",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.UpdateNeighborResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )
+
+    def delete_neighbor(
+        self,
+        request: chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+    ) -> chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborResponse:
+        return self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="DeleteNeighbor",
+                service_name="chatto.admin.v1.AdminServerService",
+                input=chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborRequest,
+                output=chatto_dot_admin_dot_v1_dot_server__pb2.DeleteNeighborResponse,
+                idempotency_level=IdempotencyLevel.UNKNOWN,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+        )

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
src/chattolib/_pb/chatto/admin/v1/server_pb2.py


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
src/chattolib/_pb/chatto/api/v1/account_pb2.py


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
src/chattolib/_pb/chatto/api/v1/viewer_pb2.py


+ 69 - 0
src/chattolib/_pb/chatto/discovery/v1/server_connect.py

@@ -20,6 +20,9 @@ class ServerDiscoveryService(Protocol):
     async def get_server(self, request: chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerRequest, ctx: RequestContext) -> chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerResponse:
         raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
 
+    async def list_neighbors(self, request: chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsRequest, ctx: RequestContext) -> chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+
 
 class ServerDiscoveryServiceASGIApplication(ConnectASGIApplication[ServerDiscoveryService]):
     def __init__(self, service: ServerDiscoveryService | AsyncGenerator[ServerDiscoveryService], *, interceptors: Iterable[Interceptor]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None) -> None:
@@ -36,6 +39,16 @@ class ServerDiscoveryServiceASGIApplication(ConnectASGIApplication[ServerDiscove
                     ),
                     function=svc.get_server,
                 ),
+                "/chatto.discovery.v1.ServerDiscoveryService/ListNeighbors": Endpoint.unary(
+                    method=MethodInfo(
+                        name="ListNeighbors",
+                        service_name="chatto.discovery.v1.ServerDiscoveryService",
+                        input=chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsRequest,
+                        output=chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsResponse,
+                        idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
+                    ),
+                    function=svc.list_neighbors,
+                ),
             },
             interceptors=interceptors,
             read_max_bytes=read_max_bytes,
@@ -71,10 +84,34 @@ class ServerDiscoveryServiceClient(ConnectClient):
             use_get=use_get,
         )
 
+    async def list_neighbors(
+        self,
+        request: chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+        use_get: bool = False,
+    ) -> chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsResponse:
+        return await self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="ListNeighbors",
+                service_name="chatto.discovery.v1.ServerDiscoveryService",
+                input=chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsRequest,
+                output=chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsResponse,
+                idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+            use_get=use_get,
+        )
+
 
 class ServerDiscoveryServiceSync(Protocol):
     def get_server(self, request: chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerRequest, ctx: RequestContext) -> chatto_dot_discovery_dot_v1_dot_server__pb2.GetServerResponse:
         raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
+    def list_neighbors(self, request: chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsRequest, ctx: RequestContext) -> chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsResponse:
+        raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
 
 
 class ServerDiscoveryServiceWSGIApplication(ConnectWSGIApplication):
@@ -91,6 +128,16 @@ class ServerDiscoveryServiceWSGIApplication(ConnectWSGIApplication):
                     ),
                     function=service.get_server,
                 ),
+                "/chatto.discovery.v1.ServerDiscoveryService/ListNeighbors": EndpointSync.unary(
+                    method=MethodInfo(
+                        name="ListNeighbors",
+                        service_name="chatto.discovery.v1.ServerDiscoveryService",
+                        input=chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsRequest,
+                        output=chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsResponse,
+                        idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
+                    ),
+                    function=service.list_neighbors,
+                ),
             },
             interceptors=interceptors,
             read_max_bytes=read_max_bytes,
@@ -125,3 +172,25 @@ class ServerDiscoveryServiceClientSync(ConnectClientSync):
             timeout_ms=timeout_ms,
             use_get=use_get,
         )
+
+    def list_neighbors(
+        self,
+        request: chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsRequest,
+        *,
+        headers: Headers | Mapping[str, str] | None = None,
+        timeout_ms: int | None = None,
+        use_get: bool = False,
+    ) -> chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsResponse:
+        return self.execute_unary(
+            request=request,
+            method=MethodInfo(
+                name="ListNeighbors",
+                service_name="chatto.discovery.v1.ServerDiscoveryService",
+                input=chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsRequest,
+                output=chatto_dot_discovery_dot_v1_dot_server__pb2.ListNeighborsResponse,
+                idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
+            ),
+            headers=headers,
+            timeout_ms=timeout_ms,
+            use_get=use_get,
+        )

+ 9 - 3
src/chattolib/_pb/chatto/discovery/v1/server_pb2.py

@@ -26,7 +26,7 @@ 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\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')
+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.ServerLogin\"\x16\n\x14ListNeighborsRequest\"(\n\x15ListNeighborsResponse\x12\x0f\n\x07origins\x18\x01 \x03(\t2\xe6\x01\n\x16ServerDiscoveryService\x12_\n\tGetServer\x12%.chatto.discovery.v1.GetServerRequest\x1a&.chatto.discovery.v1.GetServerResponse\"\x03\x90\x02\x01\x12k\n\rListNeighbors\x12).chatto.discovery.v1.ListNeighborsRequest\x1a*.chatto.discovery.v1.ListNeighborsResponse\"\x03\x90\x02\x01\x62\x06proto3')
 
 _globals = globals()
 _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -35,10 +35,16 @@ if not _descriptor._USE_C_DESCRIPTORS:
   DESCRIPTOR._loaded_options = None
   _globals['_SERVERDISCOVERYSERVICE'].methods_by_name['GetServer']._loaded_options = None
   _globals['_SERVERDISCOVERYSERVICE'].methods_by_name['GetServer']._serialized_options = b'\220\002\001'
+  _globals['_SERVERDISCOVERYSERVICE'].methods_by_name['ListNeighbors']._loaded_options = None
+  _globals['_SERVERDISCOVERYSERVICE'].methods_by_name['ListNeighbors']._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
+  _globals['_LISTNEIGHBORSREQUEST']._serialized_start=256
+  _globals['_LISTNEIGHBORSREQUEST']._serialized_end=278
+  _globals['_LISTNEIGHBORSRESPONSE']._serialized_start=280
+  _globals['_LISTNEIGHBORSRESPONSE']._serialized_end=320
+  _globals['_SERVERDISCOVERYSERVICE']._serialized_start=323
+  _globals['_SERVERDISCOVERYSERVICE']._serialized_end=553
 # @@protoc_insertion_point(module_scope)

+ 75 - 0
src/chattolib/client.py

@@ -94,6 +94,7 @@ from chattolib.types import (
     LinkedExternalIdentity,
     LinkPreview,
     Message,
+    Neighbor,
     NotificationLevel,
     NotificationOccurrence,
     NotificationOccurrencesPage,
@@ -281,6 +282,19 @@ class ChattoClient:
             ServerLogin.parse(pb_to_dict(resp.login)),
         )
 
+    async def list_neighbors(self) -> list[str]:
+        """Public Neighbor directory: the advertised canonical server origins.
+
+        Does not require auth. The response has no ordering contract.
+        """
+        resp = await self._rpc(
+            self._svc.server_discovery.list_neighbors(
+                discovery_server_pb2.ListNeighborsRequest(),
+                headers=self._headers(),
+            )
+        )
+        return list(resp.origins)
+
     async def get_motd(self) -> str | None:
         resp = await self._rpc(
             self._svc.server.get_motd(server_state_pb2.GetMotdRequest(), headers=self._headers())
@@ -388,12 +402,15 @@ class ChattoClient:
         *,
         timezone: str | None = None,
         time_format: TimeFormat | None = None,
+        share_timezone: bool | None = None,
     ) -> UserSettings:
         req = account_pb2.UpdateSettingsRequest()
         if timezone is not None:
             req.timezone = timezone
         if time_format is not None:
             req.time_format = time_format.value
+        if share_timezone is not None:
+            req.share_timezone = share_timezone
         resp = await self._rpc(self._svc.account.update_settings(req, headers=self._headers()))
         return UserSettings.parse(pb_to_dict(resp.settings))
 
@@ -1748,6 +1765,64 @@ class ChattoClient:
         )
         return list(resp.blocked_usernames)
 
+    # --- Admin: neighbors ---------------------------------------------
+
+    async def admin_list_neighbors(self) -> list[Neighbor]:
+        """List configured Neighbors. Requires ``server.manage-neighbors``."""
+        resp = await self._rpc(
+            self._svc.admin_server.list_neighbors(
+                admin_server_pb2.ListNeighborsRequest(),
+                headers=self._headers(),
+            )
+        )
+        data = pb_to_dict(resp)
+        return [n for n in (Neighbor.parse(x) for x in data.get("neighbors") or []) if n]
+
+    async def admin_get_neighbor(self, neighbor_id: str) -> Neighbor | None:
+        """Get one configured Neighbor. Requires ``server.manage-neighbors``."""
+        resp = await self._rpc(
+            self._svc.admin_server.get_neighbor(
+                admin_server_pb2.GetNeighborRequest(neighbor_id=neighbor_id),
+                headers=self._headers(),
+            )
+        )
+        return Neighbor.parse(pb_to_dict(resp).get("neighbor"))
+
+    async def admin_create_neighbor(self, origin: str) -> Neighbor:
+        """Advertise one server origin. Requires ``server.manage-neighbors``."""
+        resp = await self._rpc(
+            self._svc.admin_server.create_neighbor(
+                admin_server_pb2.CreateNeighborRequest(origin=origin),
+                headers=self._headers(),
+            )
+        )
+        neighbor = Neighbor.parse(pb_to_dict(resp).get("neighbor"))
+        assert neighbor is not None
+        return neighbor
+
+    async def admin_update_neighbor(self, neighbor_id: str, origin: str, revision: str) -> Neighbor:
+        """Change one advertised origin. Requires ``server.manage-neighbors``."""
+        resp = await self._rpc(
+            self._svc.admin_server.update_neighbor(
+                admin_server_pb2.UpdateNeighborRequest(
+                    neighbor_id=neighbor_id, origin=origin, revision=revision
+                ),
+                headers=self._headers(),
+            )
+        )
+        neighbor = Neighbor.parse(pb_to_dict(resp).get("neighbor"))
+        assert neighbor is not None
+        return neighbor
+
+    async def admin_delete_neighbor(self, neighbor_id: str, revision: str) -> None:
+        """Stop advertising one origin. Requires ``server.manage-neighbors``."""
+        await self._rpc(
+            self._svc.admin_server.delete_neighbor(
+                admin_server_pb2.DeleteNeighborRequest(neighbor_id=neighbor_id, revision=revision),
+                headers=self._headers(),
+            )
+        )
+
     # --- Admin: room layout & sidebar links ---------------------------
 
     async def admin_list_room_groups(self) -> list[AdminRoomLayoutGroup]:

+ 25 - 0
src/chattolib/types.py

@@ -191,6 +191,7 @@ class User:
 class UserSettings:
     timezone: str | None = None
     time_format: TimeFormat = TimeFormat.UNSPECIFIED
+    share_timezone: bool = False
 
     @classmethod
     def parse(cls, data: dict[str, Any] | None) -> UserSettings:
@@ -198,6 +199,7 @@ class UserSettings:
         return cls(
             timezone=data.get("timezone"),
             time_format=_parse_enum(TimeFormat, data.get("timeFormat"), TimeFormat.UNSPECIFIED),  # type: ignore[arg-type]
+            share_timezone=bool(data.get("shareTimezone", False)),
         )
 
 
@@ -310,6 +312,29 @@ class ServerLogin:
         )
 
 
+@dataclass
+class Neighbor:
+    """One server advertised in the public Neighbor directory.
+
+    ``origin`` is the canonical HTTP(S) origin; ``revision`` is an opaque
+    token required to update or delete the entry (admin side).
+    """
+
+    id: str
+    origin: str
+    revision: str = ""
+
+    @classmethod
+    def parse(cls, data: dict[str, Any] | None) -> Neighbor | None:
+        if not data:
+            return None
+        return cls(
+            id=data.get("id", ""),
+            origin=data.get("origin", ""),
+            revision=data.get("revision", ""),
+        )
+
+
 @dataclass
 class ServerRuntimeConfig:
     push_notifications_enabled: bool = False

+ 72 - 0
tests/test_client.py

@@ -762,3 +762,75 @@ async def test_admin_update_server_config(client):
 
     assert config.server_name == "MyServer"
     assert profile.version == "0.4.2"
+
+
+# --- Neighbors (0.5.0-alpha.3) -----------------------------------------
+
+
+async def test_list_neighbors_public(client):
+    resp = discovery_server_pb2.ListNeighborsResponse()
+    resp.origins.extend(["https://a.example", "https://b.example"])
+    _mock_method(client, "server_discovery", "list_neighbors", resp)
+
+    async with client:
+        origins = await client.list_neighbors()
+
+    assert origins == ["https://a.example", "https://b.example"]
+
+
+async def test_admin_neighbor_crud(client):
+    list_resp = admin_server_pb2.ListNeighborsResponse()
+    n = list_resp.neighbors.add()
+    n.id = "n1"
+    n.origin = "https://a.example"
+    n.revision = "rev1"
+    _mock_method(client, "admin_server", "list_neighbors", list_resp)
+
+    get_resp = admin_server_pb2.GetNeighborResponse()
+    get_resp.neighbor.id = "n1"
+    get_resp.neighbor.origin = "https://a.example"
+    get_resp.neighbor.revision = "rev1"
+    _mock_method(client, "admin_server", "get_neighbor", get_resp)
+
+    create_resp = admin_server_pb2.CreateNeighborResponse()
+    create_resp.neighbor.id = "n2"
+    create_resp.neighbor.origin = "https://b.example"
+    create_resp.neighbor.revision = "rev2"
+    _mock_method(client, "admin_server", "create_neighbor", create_resp)
+
+    update_resp = admin_server_pb2.UpdateNeighborResponse()
+    update_resp.neighbor.id = "n2"
+    update_resp.neighbor.origin = "https://b2.example"
+    update_resp.neighbor.revision = "rev3"
+    _mock_method(client, "admin_server", "update_neighbor", update_resp)
+
+    del_resp = admin_server_pb2.DeleteNeighborResponse()
+    _mock_method(client, "admin_server", "delete_neighbor", del_resp)
+
+    async with client:
+        listed = await client.admin_list_neighbors()
+        assert [x.origin for x in listed] == ["https://a.example"]
+
+        got = await client.admin_get_neighbor("n1")
+        assert got is not None and got.revision == "rev1"
+
+        created = await client.admin_create_neighbor("https://b.example")
+        assert created.id == "n2" and created.revision == "rev2"
+
+        updated = await client.admin_update_neighbor("n2", "https://b2.example", "rev2")
+        assert updated.origin == "https://b2.example" and updated.revision == "rev3"
+
+        await client.admin_delete_neighbor("n2", "rev3")
+
+
+async def test_update_settings_share_timezone(client):
+    resp = account_pb2.UpdateSettingsResponse()
+    resp.settings.timezone = "Europe/Berlin"
+    resp.settings.share_timezone = True
+    _mock_method(client, "account", "update_settings", resp)
+
+    async with client:
+        settings = await client.update_settings(timezone="Europe/Berlin", share_timezone=True)
+
+    assert settings.timezone == "Europe/Berlin"
+    assert settings.share_timezone is True

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor