浏览代码

Update for Chatto v0.0.174: unified subscription, server model, message updates

- Instance → Server (queries renamed, rooms via server.rooms)
- Single myEvents subscription replaces myServerEvents + myInstanceEvents
- Queries moved to Room field resolvers (room.events, room.event.threadReplies)
- me → viewer.user; notifications/followedThreads under viewer
- editMessage → updateMessage (edit_message alias kept)
- user(id) → user(userId) arg rename
- Room: autoJoin removed, gained groupId
- Mutations renamed: updateMyProfile → updateProfile, uploadMyAvatar → uploadAvatar, markThreadAsOpened → markThreadAsRead
TheCodemancer 5 月之前
父节点
当前提交
b147c6884a
共有 7 个文件被更改,包括 227 次插入 和 244 次删除
  1. 10 10
      CLAUDE.md
  2. 1 1
      pyproject.toml
  3. 40 25
      src/chattolib/client.py
  4. 119 113
      src/chattolib/queries.py
  5. 5 47
      src/chattolib/subscriptions.py
  6. 1 1
      src/chattolib/types.py
  7. 51 47
      tests/test_client.py

+ 10 - 10
CLAUDE.md

@@ -42,17 +42,17 @@ The library uses **httpx** for async HTTP and **websockets** for GraphQL subscri
 
 | Domain | Queries | Mutations | Subscriptions |
 |---|---|---|---|
-| **Instance** | `instance` | `updateInstance`, logo/banner uploads | `myInstanceEvents` |
-| **Rooms** | `room`, `roomEvents`, `roomEventByEventId`, `threadEvents`, `roomEventsAround` | `createRoom`, `updateRoom`, `archiveRoom`, `joinRoom`, `leaveRoom`, `markRoomAsRead` | `myServerEvents` |
-| **Messages** | (via roomEvents) | `postMessage`, `editMessage`, `deleteMessage` | `MessagePostedEvent`, `MessageUpdatedEvent`, `MessageDeletedEvent` |
+| **Server** | `server` | `updateServer`, logo/banner uploads | `myEvents` |
+| **Rooms** | `room`, `room.events`, `room.event`, `room.eventsAround` | `createRoom`, `updateRoom`, `archiveRoom`, `joinRoom`, `leaveRoom`, `markRoomAsRead` | `myEvents` |
+| **Messages** | (via room.events) | `postMessage`, `editMessage`, `deleteMessage` | `MessagePostedEvent`, `MessageUpdatedEvent`, `MessageDeletedEvent` |
 | **Reactions** | (on message events) | `addReaction`, `removeReaction` | `ReactionAddedEvent`, `ReactionRemovedEvent` |
-| **Threads** | `threadEvents`, `myFollowedThreads` | `followThread`, `unfollowThread`, `markThreadAsOpened` | `ThreadFollowChangedEvent` |
-| **Users** | `me`, `user(id)`, `userByLogin`, `users` | `updateMyProfile`, `uploadMyAvatar`, `deleteMyAvatar` | `UserProfileUpdatedEvent`, `PresenceChangedEvent` |
+| **Threads** | `room.event.threadReplies`, `viewer.followedThreads` | `followThread`, `unfollowThread`, `markThreadAsRead` | `ThreadFollowChangedEvent` |
+| **Users** | `viewer.user`, `user(id)`, `userByLogin`, `users` | `updateProfile`, `uploadAvatar`, `deleteAvatar` | `UserProfileUpdatedEvent`, `PresenceChangedEvent` |
 | **DMs** | (via rooms) | `startDM` | `NewDirectMessageNotificationEvent` |
-| **Notifications** | `notifications`, `hasNotifications` | `dismissNotification`, `dismissAllNotifications` | `NotificationCreatedEvent`, `NotificationDismissedEvent` |
-| **Permissions/Roles** | via `admin` query, `instance.roles` | `grantInstancePermission`, `createRole`, `assignInstanceRole` | — |
-| **Voice calls** | `voiceCallToken`, `activeCallRoomIds`, `callParticipants` | — | `CallParticipantJoinedEvent`, `CallParticipantLeftEvent` |
-| **Admin** | `admin.systemInfo`, `admin.instanceConfig`, `admin.roles` | `admin.updateInstanceConfig`, `admin.resetInstanceConfig` | `myInstanceEvents` |
+| **Notifications** | `viewer.notifications`, `viewer.hasNotifications` | `dismissNotification`, `dismissAllNotifications` | `NotificationCreatedEvent`, `NotificationDismissedEvent` |
+| **Permissions/Roles** | via `admin` query, `server.roles` | `grantPermission`, `createRole`, `assignRole` | — |
+| **Voice calls** | `room.voiceCallToken`, `activeCallRoomIds`, `room.callParticipants` | — | `CallParticipantJoinedEvent`, `CallParticipantLeftEvent` |
+| **Admin** | `admin.systemInfo`, `admin.serverConfig`, `admin.roles` | `admin.updateServerConfig`, `admin.resetServerConfig` | via `myEvents` |
 
 ### GraphQL conventions
 
@@ -61,7 +61,7 @@ The library uses **httpx** for async HTTP and **websockets** for GraphQL subscri
 - File uploads use a custom `Upload` scalar (multipart form).
 - Pagination uses `limit`/`before`/`after` on room events (`before`/`after` are `Time` scalars, ISO timestamps).
 - All mutations take a single `input` argument with a corresponding `*Input` type.
-- Subscriptions: `myServerEvents` for room/message events, `myInstanceEvents` for instance-wide events.
+- Single unified subscription: `myEvents` for all server, room, and instance events.
 - Rooms have a `type` field: `CHANNEL` or `DM`.
 - Image URLs accept optional `width`, `height`, `fit` (enum: `CONTAIN`, `COVER`, `EXACT`) for server-side resizing.
 

+ 1 - 1
pyproject.toml

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

+ 40 - 25
src/chattolib/client.py

@@ -47,7 +47,7 @@ def _parse_room(data: dict[str, Any]) -> Room:
         type=RoomType(data["type"]) if data.get("type") else None,
         description=data.get("description"),
         archived=data.get("archived", False),
-        auto_join=data.get("autoJoin", False),
+        group_id=data.get("groupId"),
         has_unread=data.get("hasUnread", False),
         has_mention=data.get("hasMention", False),
     )
@@ -199,7 +199,6 @@ class ChattoClient:
         if response.status_code == 401:
             raise ChattoAuthError("Authentication failed")
 
-        # Chatto returns 422 with a JSON body for GraphQL validation errors
         body = response.json()
 
         if "errors" in body:
@@ -253,16 +252,15 @@ class ChattoClient:
 
     async def me(self) -> User:
         data = await self._execute(Q.QUERY_ME)
-        return _parse_user(data["me"])
+        return _parse_user(data["viewer"]["user"])
 
-    async def instance(self) -> dict[str, Any]:
-        data = await self._execute(Q.QUERY_INSTANCE)
-        return data["instance"]
+    async def server(self) -> dict[str, Any]:
+        data = await self._execute(Q.QUERY_SERVER)
+        return data["server"]
 
     async def rooms(self) -> list[Room]:
-        """Get all rooms from the instance."""
-        data = await self._execute(Q.QUERY_INSTANCE)
-        return [_parse_room(r) for r in data["instance"]["rooms"]]
+        data = await self._execute(Q.QUERY_SERVER)
+        return [_parse_room(r) for r in data["server"]["rooms"]]
 
     async def room(self, room_id: str) -> Room:
         data = await self._execute(Q.QUERY_ROOM, {"roomId": room_id})
@@ -284,7 +282,7 @@ class ChattoClient:
         if after is not None:
             variables["after"] = after
         data = await self._execute(Q.QUERY_ROOM_EVENTS, variables)
-        conn = data["roomEvents"]
+        conn = data["room"]["events"]
         return RoomEventsPage(
             events=[_parse_message_event(e) for e in conn["events"]],
             has_older=conn["hasOlder"],
@@ -300,12 +298,12 @@ class ChattoClient:
     ) -> list[MessageEvent]:
         data = await self._execute(
             Q.QUERY_THREAD_EVENTS,
-            {"roomId": room_id, "threadRootEventId": thread_root_event_id},
+            {"roomId": room_id, "eventId": thread_root_event_id},
         )
-        return [_parse_message_event(e) for e in data["threadEvents"]]
+        return [_parse_message_event(e) for e in data["room"]["event"]["threadReplies"]]
 
     async def user(self, user_id: str) -> User:
-        data = await self._execute(Q.QUERY_USER, {"id": user_id})
+        data = await self._execute(Q.QUERY_USER, {"userId": user_id})
         return _parse_user(data["user"])
 
     async def user_by_login(self, login: str) -> User:
@@ -318,7 +316,7 @@ class ChattoClient:
 
     async def notifications(self) -> list[dict[str, Any]]:
         data = await self._execute(Q.QUERY_NOTIFICATIONS)
-        return data["notifications"]
+        return data["viewer"]["notifications"]
 
     async def followed_threads(self) -> list[FollowedThread]:
         data = await self._execute(Q.QUERY_FOLLOWED_THREADS)
@@ -330,7 +328,7 @@ class ChattoClient:
                 last_reply_at=_parse_datetime(t.get("lastReplyAt")),
                 has_unread=t.get("hasUnread", False),
             )
-            for t in data["myFollowedThreads"]
+            for t in data["viewer"]["followedThreads"]
         ]
 
     # --- Mutations ---
@@ -357,17 +355,19 @@ class ChattoClient:
         data = await self._execute(Q.MUTATION_POST_MESSAGE, {"input": input_data})
         return data["postMessage"]
 
-    async def edit_message(
+    async def update_message(
         self,
         room_id: str,
         event_id: str,
         body: str,
     ) -> dict[str, Any]:
         data = await self._execute(
-            Q.MUTATION_EDIT_MESSAGE,
+            Q.MUTATION_UPDATE_MESSAGE,
             {"input": {"roomId": room_id, "eventId": event_id, "body": body}},
         )
-        return data["editMessage"]
+        return data["updateMessage"]
+
+    edit_message = update_message
 
     async def delete_message(self, room_id: str, event_id: str) -> Any:
         data = await self._execute(
@@ -421,6 +421,15 @@ class ChattoClient:
         )
         return data["markRoomAsRead"]
 
+    async def mark_thread_as_read(
+        self, room_id: str, thread_root_event_id: str
+    ) -> dict[str, Any]:
+        data = await self._execute(
+            Q.MUTATION_MARK_THREAD_AS_READ,
+            {"input": {"roomId": room_id, "threadRootEventId": thread_root_event_id}},
+        )
+        return data["markThreadAsRead"]
+
     async def follow_thread(
         self, room_id: str, thread_root_event_id: str
     ) -> Any:
@@ -454,27 +463,33 @@ class ChattoClient:
         )
         return data["startDM"]
 
-    async def update_my_profile(
+    async def update_profile(
         self,
         *,
+        user_id: str | None = None,
         display_name: str | None = None,
         login: str | None = None,
     ) -> User:
         input_data: dict[str, Any] = {}
+        if user_id is not None:
+            input_data["userId"] = user_id
         if display_name is not None:
             input_data["displayName"] = display_name
         if login is not None:
             input_data["login"] = login
-        data = await self._execute(Q.MUTATION_UPDATE_MY_PROFILE, {"input": input_data})
-        return _parse_user(data["updateMyProfile"])
+        data = await self._execute(Q.MUTATION_UPDATE_PROFILE, {"input": input_data})
+        return _parse_user(data["updateProfile"])
 
-    async def upload_my_avatar(self, file_path: str) -> dict[str, Any]:
+    async def upload_avatar(self, file_path: str, user_id: str | None = None) -> dict[str, Any]:
+        variables: dict[str, Any] = {"input": {"file": None}}
+        if user_id is not None:
+            variables["input"]["userId"] = user_id
         data = await self._execute_upload(
-            Q.MUTATION_UPLOAD_MY_AVATAR,
-            {"input": {"file": None}},
+            Q.MUTATION_UPLOAD_AVATAR,
+            variables,
             file_path,
         )
-        return data["uploadMyAvatar"]
+        return data["uploadAvatar"]
 
     async def update_presence(self, status: PresenceStatus) -> Any:
         data = await self._execute(

+ 119 - 113
src/chattolib/queries.py

@@ -4,31 +4,33 @@
 
 QUERY_ME = """
 query Me {
-    me {
-        id
-        login
-        displayName
-        createdAt
-        avatarUrl
-        presenceStatus
+    viewer {
+        user {
+            id
+            login
+            displayName
+            createdAt
+            avatarUrl
+            presenceStatus
+        }
     }
 }
 """
 
-QUERY_INSTANCE = """
-query Instance {
-    instance {
+QUERY_SERVER = """
+query Server {
+    server {
         version
         enabledAuthProviders
-        needsSetup
         pushNotificationsEnabled
         livekitUrl
         directRegistrationEnabled
         maxUploadSize
         maxVideoUploadSize
+        messageEditWindowSeconds
         memberCount
         roomCount
-        rooms { id type name description archived autoJoin hasUnread hasMention }
+        rooms { id type name description archived groupId hasUnread hasMention }
     }
 }
 """
@@ -41,7 +43,7 @@ query Room($roomId: ID!) {
         name
         description
         archived
-        autoJoin
+        groupId
         hasUnread
         hasMention
     }
@@ -50,49 +52,56 @@ query Room($roomId: ID!) {
 
 QUERY_ROOM_EVENTS = """
 query RoomEvents($roomId: ID!, $limit: Int, $before: Time, $after: Time) {
-    roomEvents(roomId: $roomId, limit: $limit, before: $before, after: $after) {
-        events {
-            id
-            createdAt
-            actorId
-            actor { id login displayName avatarUrl }
-            event {
-                ... on MessagePostedEvent {
-                    roomId
-                    body
-                    attachments { id filename contentType size url }
-                    reactions { emoji count hasReacted users { id login displayName } }
-                    inReplyTo
-                    inThread
-                    replyCount
-                    lastReplyAt
-                    linkPreview { url title description siteName }
+    room(roomId: $roomId) {
+        events(limit: $limit, before: $before, after: $after) {
+            events {
+                id
+                createdAt
+                actorId
+                actor { id login displayName avatarUrl }
+                event {
+                    ... on MessagePostedEvent {
+                        roomId
+                        body
+                        attachments { id filename contentType size url }
+                        reactions { emoji count hasReacted users { id login displayName } }
+                        inReplyTo
+                        inThread
+                        replyCount
+                        lastReplyAt
+                        linkPreview { url title description siteName }
+                    }
                 }
             }
+            hasOlder
+            hasNewer
+            startCursor
+            endCursor
         }
-        hasOlder
-        hasNewer
-        startCursor
-        endCursor
     }
 }
 """
 
 QUERY_THREAD_EVENTS = """
-query ThreadEvents($roomId: ID!, $threadRootEventId: ID!) {
-    threadEvents(roomId: $roomId, threadRootEventId: $threadRootEventId) {
-        id
-        createdAt
-        actorId
-        actor { id login displayName avatarUrl }
-        event {
-            ... on MessagePostedEvent {
-                roomId
-                body
-                attachments { id filename contentType size url }
-                reactions { emoji count hasReacted }
-                inReplyTo
-                inThread
+query ThreadEvents($roomId: ID!, $eventId: ID!) {
+    room(roomId: $roomId) {
+        event(eventId: $eventId) {
+            id
+            threadReplies {
+                id
+                createdAt
+                actorId
+                actor { id login displayName avatarUrl }
+                event {
+                    ... on MessagePostedEvent {
+                        roomId
+                        body
+                        attachments { id filename contentType size url }
+                        reactions { emoji count hasReacted }
+                        inReplyTo
+                        inThread
+                    }
+                }
             }
         }
     }
@@ -100,8 +109,8 @@ query ThreadEvents($roomId: ID!, $threadRootEventId: ID!) {
 """
 
 QUERY_USER = """
-query User($id: ID!) {
-    user(id: $id) {
+query User($userId: ID!) {
+    user(userId: $userId) {
         id
         login
         displayName
@@ -139,33 +148,35 @@ query Users {
 
 QUERY_NOTIFICATIONS = """
 query Notifications {
-    notifications {
-        ... on MentionNotificationItem {
-            id
-            createdAt
-            summary
-            room { id name }
-            eventId
-        }
-        ... on ReplyNotificationItem {
-            id
-            createdAt
-            summary
-            room { id name }
-            eventId
-        }
-        ... on RoomMessageNotificationItem {
-            id
-            createdAt
-            summary
-            room { id name }
-            eventId
-        }
-        ... on DMMessageNotificationItem {
-            id
-            createdAt
-            summary
-            room { id name }
+    viewer {
+        notifications {
+            ... on MentionNotificationItem {
+                id
+                createdAt
+                summary
+                room { id name }
+                eventId
+            }
+            ... on ReplyNotificationItem {
+                id
+                createdAt
+                summary
+                room { id name }
+                eventId
+            }
+            ... on RoomMessageNotificationItem {
+                id
+                createdAt
+                summary
+                room { id name }
+                eventId
+            }
+            ... on DMMessageNotificationItem {
+                id
+                createdAt
+                summary
+                room { id name }
+            }
         }
     }
 }
@@ -173,12 +184,14 @@ query Notifications {
 
 QUERY_FOLLOWED_THREADS = """
 query FollowedThreads {
-    myFollowedThreads {
-        roomId
-        threadRootEventId
-        replyCount
-        lastReplyAt
-        hasUnread
+    viewer {
+        followedThreads {
+            roomId
+            threadRootEventId
+            replyCount
+            lastReplyAt
+            hasUnread
+        }
     }
 }
 """
@@ -194,9 +207,9 @@ mutation PostMessage($input: PostMessageInput!) {
 }
 """
 
-MUTATION_EDIT_MESSAGE = """
-mutation EditMessage($input: EditMessageInput!) {
-    editMessage(input: $input) {
+MUTATION_UPDATE_MESSAGE = """
+mutation UpdateMessage($input: UpdateMessageInput!) {
+    updateMessage(input: $input) {
         id
     }
 }
@@ -254,6 +267,14 @@ mutation MarkRoomAsRead($input: MarkRoomAsReadInput!) {
 }
 """
 
+MUTATION_MARK_THREAD_AS_READ = """
+mutation MarkThreadAsRead($input: MarkThreadAsReadInput!) {
+    markThreadAsRead(input: $input) {
+        previousReadAt
+    }
+}
+"""
+
 MUTATION_FOLLOW_THREAD = """
 mutation FollowThread($input: FollowThreadInput!) {
     followThread(input: $input)
@@ -281,9 +302,9 @@ mutation StartDM($input: StartDMInput!) {
 }
 """
 
-MUTATION_UPDATE_MY_PROFILE = """
-mutation UpdateMyProfile($input: UpdateMyProfileInput!) {
-    updateMyProfile(input: $input) {
+MUTATION_UPDATE_PROFILE = """
+mutation UpdateProfile($input: UpdateProfileInput!) {
+    updateProfile(input: $input) {
         id
         login
         displayName
@@ -291,9 +312,9 @@ mutation UpdateMyProfile($input: UpdateMyProfileInput!) {
 }
 """
 
-MUTATION_UPLOAD_MY_AVATAR = """
-mutation UploadMyAvatar($input: UploadMyAvatarInput!) {
-    uploadMyAvatar(input: $input) {
+MUTATION_UPLOAD_AVATAR = """
+mutation UploadAvatar($input: UploadAvatarInput!) {
+    uploadAvatar(input: $input) {
         id
         avatarUrl
     }
@@ -320,9 +341,9 @@ mutation DismissAllNotifications {
 
 # --- Subscriptions ---
 
-SUBSCRIPTION_SERVER_EVENTS = """
-subscription ServerEvents {
-    myServerEvents {
+SUBSCRIPTION_EVENTS = """
+subscription MyEvents {
+    myEvents {
         id
         createdAt
         actorId
@@ -346,37 +367,22 @@ subscription ServerEvents {
             ... on CallParticipantLeftEvent { roomId }
             ... on VideoProcessingCompletedEvent { roomId attachmentId messageEventId }
             ... on PresenceChangedEvent { status }
-        }
-    }
-}
-"""
-
-SUBSCRIPTION_INSTANCE_EVENTS = """
-subscription InstanceEvents {
-    myInstanceEvents {
-        id
-        createdAt
-        actorId
-        actor { id login displayName avatarUrl }
-        event {
-            ... on InstanceConfigUpdatedEvent { instanceName motd welcomeMessage blockedUsernames }
+            ... on ServerConfigUpdatedEvent { serverName motd }
             ... on UserCreatedEvent { userId login displayName }
             ... on UserDeletedEvent { userId }
             ... on UserProfileUpdatedEvent { userId displayName avatarUrl login }
-            ... on InstanceUserPreferencesUpdatedEvent { timezone timeFormat }
+            ... on ServerUserPreferencesUpdatedEvent { timezone timeFormat }
             ... on NotificationLevelChangedEvent { roomId level effectiveLevel }
-            ... on UserJoinedServerEvent { userId }
-            ... on UserLeftServerEvent { userId }
             ... on ServerUpdatedEvent { name description }
             ... on MentionNotificationEvent { roomId }
             ... on NewDirectMessageNotificationEvent { roomId conversationName }
             ... on NotificationCreatedEvent { notificationId roomId eventId inReplyToId }
             ... on NotificationDismissedEvent { notificationId }
             ... on ThreadFollowChangedEvent { roomId threadRootEventId isFollowing }
-            ... on NewMessageInServerEvent { roomId }
             ... on RoomMarkedAsReadEvent { roomId }
             ... on RoomLayoutUpdatedEvent { changed }
             ... on SessionTerminatedEvent { reason }
+            ... on HeartbeatEvent { alive }
         }
     }
 }

+ 5 - 47
src/chattolib/subscriptions.py

@@ -38,9 +38,7 @@ async def _connect(url: str, token: str) -> ClientConnection:
         ping_interval=20,
         ping_timeout=20,
     )
-    # Send connection_init
     await ws.send(json.dumps({"type": _GQL_CONNECTION_INIT}))
-    # Wait for connection_ack
     ack = json.loads(await ws.recv())
     if ack.get("type") != _GQL_CONNECTION_ACK:
         await ws.close()
@@ -58,15 +56,15 @@ async def _keepalive(ws: ClientConnection) -> None:
         pass
 
 
-async def subscribe_server_events(
+async def subscribe_events(
     url: str,
     token: str,
     *,
     subscription_id: str = "1",
 ) -> AsyncIterator[dict[str, Any]]:
-    """Subscribe to real-time server events.
+    """Subscribe to all real-time events.
 
-    Yields event dicts from the myServerEvents subscription.
+    Yields event dicts from the unified myEvents subscription.
     """
     ws = await _connect(url, token)
     ping_task = asyncio.create_task(_keepalive(ws))
@@ -77,7 +75,7 @@ async def subscribe_server_events(
                     "id": subscription_id,
                     "type": _GQL_SUBSCRIBE,
                     "payload": {
-                        "query": Q.SUBSCRIPTION_SERVER_EVENTS,
+                        "query": Q.SUBSCRIPTION_EVENTS,
                     },
                 }
             )
@@ -86,47 +84,7 @@ async def subscribe_server_events(
             msg = json.loads(raw)
             msg_type = msg.get("type")
             if msg_type == _GQL_NEXT:
-                yield msg["payload"]["data"]["myServerEvents"]
-            elif msg_type == _GQL_PING:
-                await ws.send(json.dumps({"type": _GQL_PONG}))
-            elif msg_type == _GQL_PONG:
-                pass
-            elif msg_type == _GQL_ERROR:
-                raise ConnectionError(f"Subscription error: {msg.get('payload')}")
-            elif msg_type == _GQL_COMPLETE:
-                break
-    finally:
-        ping_task.cancel()
-        await ws.close()
-
-
-async def subscribe_instance_events(
-    url: str,
-    token: str,
-    *,
-    subscription_id: str = "1",
-) -> AsyncIterator[dict[str, Any]]:
-    """Subscribe to real-time instance-wide events.
-
-    Yields event dicts from the myInstanceEvents subscription.
-    """
-    ws = await _connect(url, token)
-    ping_task = asyncio.create_task(_keepalive(ws))
-    try:
-        await ws.send(
-            json.dumps(
-                {
-                    "id": subscription_id,
-                    "type": _GQL_SUBSCRIBE,
-                    "payload": {"query": Q.SUBSCRIPTION_INSTANCE_EVENTS},
-                }
-            )
-        )
-        async for raw in ws:
-            msg = json.loads(raw)
-            msg_type = msg.get("type")
-            if msg_type == _GQL_NEXT:
-                yield msg["payload"]["data"]["myInstanceEvents"]
+                yield msg["payload"]["data"]["myEvents"]
             elif msg_type == _GQL_PING:
                 await ws.send(json.dumps({"type": _GQL_PONG}))
             elif msg_type == _GQL_PONG:

+ 1 - 1
src/chattolib/types.py

@@ -68,7 +68,7 @@ class Room:
     type: RoomType | None = None
     description: str | None = None
     archived: bool = False
-    auto_join: bool = False
+    group_id: str | None = None
     has_unread: bool = False
     has_mention: bool = False
 

+ 51 - 47
tests/test_client.py

@@ -28,13 +28,15 @@ async def test_me(mock_api, client):
     mock_api.post("/api/graphql").mock(
         return_value=_gql_response(
             {
-                "me": {
-                    "id": "u1",
-                    "login": "alice",
-                    "displayName": "Alice",
-                    "createdAt": "2025-01-01T00:00:00Z",
-                    "avatarUrl": None,
-                    "presenceStatus": "ONLINE",
+                "viewer": {
+                    "user": {
+                        "id": "u1",
+                        "login": "alice",
+                        "displayName": "Alice",
+                        "createdAt": "2025-01-01T00:00:00Z",
+                        "avatarUrl": None,
+                        "presenceStatus": "ONLINE",
+                    }
                 }
             }
         )
@@ -50,7 +52,7 @@ async def test_rooms(mock_api, client):
     mock_api.post("/api/graphql").mock(
         return_value=_gql_response(
             {
-                "instance": {
+                "server": {
                     "rooms": [
                         {
                             "id": "r1",
@@ -58,7 +60,7 @@ async def test_rooms(mock_api, client):
                             "name": "general",
                             "description": None,
                             "archived": False,
-                            "autoJoin": True,
+                            "groupId": "g1",
                             "hasUnread": False,
                             "hasMention": False,
                         }
@@ -108,35 +110,37 @@ async def test_room_events(mock_api, client):
     mock_api.post("/api/graphql").mock(
         return_value=_gql_response(
             {
-                "roomEvents": {
-                    "events": [
-                        {
-                            "id": "e1",
-                            "createdAt": "2025-01-01T00:00:00Z",
-                            "actorId": "u1",
-                            "actor": {
-                                "id": "u1",
-                                "login": "alice",
-                                "displayName": "Alice",
-                                "avatarUrl": None,
-                            },
-                            "event": {
-                                "roomId": "r1",
-                                "body": "Hello",
-                                "attachments": [],
-                                "reactions": [],
-                                "inReplyTo": None,
-                                "inThread": None,
-                                "replyCount": 0,
-                                "lastReplyAt": None,
-                                "linkPreview": None,
-                            },
-                        }
-                    ],
-                    "hasOlder": False,
-                    "hasNewer": False,
-                    "startCursor": None,
-                    "endCursor": None,
+                "room": {
+                    "events": {
+                        "events": [
+                            {
+                                "id": "e1",
+                                "createdAt": "2025-01-01T00:00:00Z",
+                                "actorId": "u1",
+                                "actor": {
+                                    "id": "u1",
+                                    "login": "alice",
+                                    "displayName": "Alice",
+                                    "avatarUrl": None,
+                                },
+                                "event": {
+                                    "roomId": "r1",
+                                    "body": "Hello",
+                                    "attachments": [],
+                                    "reactions": [],
+                                    "inReplyTo": None,
+                                    "inThread": None,
+                                    "replyCount": 0,
+                                    "lastReplyAt": None,
+                                    "linkPreview": None,
+                                },
+                            }
+                        ],
+                        "hasOlder": False,
+                        "hasNewer": False,
+                        "startCursor": None,
+                        "endCursor": None,
+                    }
                 }
             }
         )
@@ -170,12 +174,12 @@ async def test_login_invalid():
             await ChattoClient.login("bad", "creds")
 
 
-async def test_edit_message(mock_api, client):
+async def test_update_message(mock_api, client):
     mock_api.post("/api/graphql").mock(
-        return_value=_gql_response({"editMessage": {"id": "e1"}})
+        return_value=_gql_response({"updateMessage": {"id": "e1"}})
     )
     async with client:
-        result = await client.edit_message("r1", "e1", "edited body")
+        result = await client.update_message("r1", "e1", "edited body")
     assert result["id"] == "e1"
 
 
@@ -218,11 +222,11 @@ async def test_create_room(mock_api, client):
     assert room.name == "general"
 
 
-async def test_update_my_profile(mock_api, client):
+async def test_update_profile(mock_api, client):
     mock_api.post("/api/graphql").mock(
         return_value=_gql_response(
             {
-                "updateMyProfile": {
+                "updateProfile": {
                     "id": "u1",
                     "login": "newname",
                     "displayName": "New Name",
@@ -231,7 +235,7 @@ async def test_update_my_profile(mock_api, client):
         )
     )
     async with client:
-        user = await client.update_my_profile(login="newname", display_name="New Name")
+        user = await client.update_profile(login="newname", display_name="New Name")
     assert user.login == "newname"
     assert user.display_name == "New Name"
 
@@ -245,16 +249,16 @@ async def test_update_presence(mock_api, client):
     assert result is True
 
 
-async def test_upload_my_avatar(mock_api, client, tmp_path):
+async def test_upload_avatar(mock_api, client, tmp_path):
     mock_api.post("/api/graphql").mock(
         return_value=_gql_response(
-            {"uploadMyAvatar": {"id": "u1", "avatarUrl": "https://example.com/avatar.jpg"}}
+            {"uploadAvatar": {"id": "u1", "avatarUrl": "https://example.com/avatar.jpg"}}
         )
     )
     avatar = tmp_path / "avatar.jpg"
     avatar.write_bytes(b"\xff\xd8\xff\xe0fake-jpeg")
     async with client:
-        result = await client.upload_my_avatar(str(avatar))
+        result = await client.upload_avatar(str(avatar))
     assert result["avatarUrl"] == "https://example.com/avatar.jpg"