Steam Chat #29

Open
opened 2026-08-24 00:15:33 -04:00 by Salastil · 0 comments
Owner

Add Steam Chat crib ideas from Pidgin plugin

Steam Chat as a fifth-plus protocol beside IRC, Discord, Matrix, Kick and Sneedchat: a Steam account signs in, its friends appear as direct-message conversations with their presence, and messages go both ways. A chat client only: no Store, Library, Market, Workshop, or game launching.

Branch: protocol/steam in both moho and nobilis (Gitea only). development is merged into it often. Code: nobilis/src/backend/steam/, a folder of its own like irc/, matrix/ and kick/, plus the window's account form, presence and service mark.

As each item is finished its commit goes beside it, as (moho abc1234, nobilis def5678).

Sources

  • The decompiled official client, Salastil/steam (decomp/protos/*.proto are the message definitions extracted from steamclient.so, decomp/linux64_steamclient.so/ is its pseudo-C and strings). This is the ground truth for message shapes and job names. Read for facts, never copied.
  • Open source, for behaviour and cross-checking. MIT/public domain can be learned from freely; LGPL (SteamKit) is read, not copied, because moho is GPL-3.0-or-later and the licences must stay compatible.
    • icewind1991/steam-vent (Rust, MIT): WebSocket transport, auth sessions, service methods.
    • DoctorMcKay/node-steam-user (JS, MIT): friends, persona and chat flows, error handling in the wild.
    • ValvePython/steam (Python, MIT).
    • SteamRE/SteamKit (C#, LGPL): the most complete picture of the CM protocol and its quirks.
    • SteamDatabase/Protobufs (public domain): every .proto, with history.
    • The Pidgin plugin named above, for what a third-party chat client has to cover.

How the protocol works

Connecting. The official client asks https://api.steampowered.com/ISteamDirectory/GetCMListForConnect/v1/?cellid=<n>&qoslevel=<n>&cmtype=websockets for a list of connection managers (CMs): each has endpoint (host:443), type, dc, load. It connects a WebSocket to wss://<endpoint>/cmsocket/. TLS protects the channel, so the TCP-era channel-encryption handshake (ChannelEncryptRequest) is not needed. The connection goes through the account's route like every other protocol (Clearnet, Tor, SOCKS5).

Packets. Each WebSocket binary message is one packet: u32 LE EMsg with bit 0x80000000 set for protobuf, u32 LE header length, a CMsgProtoBufHeader, then the message body. The header carries steamid, client_sessionid, jobid_source/jobid_target (request/response matching), target_job_name, eresult. EMsg 1 (Multi) holds a CMsgMulti whose body, gzip-compressed when size_unzipped is set, is a run of u32 length, packet.

Service methods. Most modern features are unified-service calls: EMsg 151 (authenticated) or 9804 (before logon) with target_job_name of Service.Method#1 and the request protobuf as the body; the answer is EMsg 147 carrying the same job id and an eresult. The server also pushes notifications to the client as EMsg 152 with target_job_name such as FriendMessagesClient.IncomingMessage#1. The older fixed messages (ClientLogon 5514, ClientFriendsList 767, ClientPersonaState 766, ClientChangeStatus 716, ClientHeartBeat 703, ClientLogOff 706, ClientLoggedOff 757) remain for session, friends and presence.

Signing in.

  1. ClientHello (9805) with the protocol version.
  2. Authentication.GetPasswordRSAPublicKey#1 (account name) returns publickey_mod, publickey_exp (hex) and a timestamp. The password is RSA-encrypted (PKCS#1 v1.5) with it and sent as base64.
  3. Authentication.BeginAuthSessionViaCredentials#1 returns client_id, request_id, a poll interval, the steamid, and allowed_confirmations: None, EmailCode, DeviceCode (the mobile authenticator's TOTP), DeviceConfirmation (approve on the phone), EmailConfirmation, MachineToken.
  4. A code is sent with Authentication.UpdateAuthSessionWithSteamGuardCode#1; a phone approval needs nothing sent. Authentication.PollAuthSessionStatus#1 is polled at the interval until it returns a refresh token and an access token, plus new_guard_data (remembers this machine so the next sign-in is not challenged).
  5. BeginAuthSessionViaQR is the passwordless alternative: show a QR code, the phone approves it.
  6. ClientLogon carries the account name and the token in access_token; the header steamid is the account's SteamID64. The current client's protocol version is 65581 in its code (steam-vent still sends 65580 and is accepted). The response gives the eresult, the heartbeat_seconds interval and, in the header, the client_sessionid used from then on. Authentication.GenerateAccessTokenForApp#1 renews tokens.

Only the token is kept, never the password; sign-in again only when the token is refused or revoked.

Presence and friends. After logon the server sends ClientFriendsList (full, then incremental) with each relationship, and ClientPersonaState with names, state, avatar hash, the game being played, rich presence and persona_state_flags (mobile, web, big picture, Steam Deck client types). ClientRequestFriendData asks for more, ClientChangeStatus sets our own state. Persona states: Offline 0, Online 1, Busy 2, Away 3, Snooze 4, LookingToTrade 5, LookingToPlay 6, Invisible 7. Relationships: None 0, Blocked 1, RequestRecipient 2, Friend 3, RequestInitiator 4, Ignored 5, IgnoredFriend 6. Avatars are https://avatars.steamstatic.com/<hash>_full.jpg (also _medium, and the bare hash for small).

Direct messages. FriendMessages.SendMessage#1 (steamid, chat_entry_type 1 for a message, text, contains_bbcode, client_message_id, echo_to_sender) answers with the server timestamp, an ordinal that orders messages sharing a second, and the text as the server stored it. Incoming messages arrive as FriendMessagesClient.IncomingMessage#1 (also local_echo for what was sent from another of our sessions, and message_no_bbcode). Typing is chat_entry_type 2. GetRecentMessages#1 and GetActiveMessageSessions#1 give history and unread state; AckMessage marks read and is echoed to our other sessions (NotifyAckMessageEcho). Formatting is BBCode; emoticons are :name:.

Chat groups (Steam Chat). The ChatRoom service: groups with text rooms, roles and permissions, members, invites and invite links, history, reactions, voice rooms; pushes in ChatRoomClient.

Open questions, to settle by trying them on a real account.

  • Coexisting with the desktop Steam client on the same account: as a second desktop-type session it can be answered with LoggedInElsewhere (EResult 6). The mobile chat app and the web chat are different session types and live beside the desktop client; the sign-in already has the fields for that (EAuthTokenPlatformType, EAuthTokenAppType, gaming_device_type, persona_state_flags). Which one moho presents as decides how it behaves beside Steam.
  • Rate limits on sign-in (RateLimitExceeded, 84) and what supports_rate_limit_response changes.
  • What is the least a chat-only session must send in ClientLogon.

Round 1: sign in, friends, presence, direct messages

Signing in and out

  • Account type steam:<account name> in accounts.toml, added, edited, removed and listed like the others; service mark and a name in the account form
  • CM discovery (GetCMListForConnect, least-loaded first) and a WebSocket to /cmsocket/, through the account's route
  • Packet reading and writing; Multi/gzip unpacking; tests against hand-built packets
  • The protobuf messages this needs, written from the extracted definitions
  • Service-method calls before and after logon, answers matched to requests by job id, timeouts
  • Password sign-in: RSA, begin, poll
  • Steam Guard: the authenticator's code, an emailed code, and approval on the phone, asked for in the account form as they come
  • Token kept in the account's credentials the way the others are (private file); used for every later connect, password never stored
  • ClientLogon with the token; the response read for the session, the heartbeat interval and the failure reasons, each said in words (wrong password, throttled, refused token)
  • Heartbeat; reconnect with a growing wait; another CM when told to try one; sign-out by another client reported as that
  • Sign out (ClientLogOff) and disconnect cleanly, with an account switched off staying off

Friends and presence

  • Friends list read (ClientFriendsList, incremental updates) and listed: each friend a direct-message conversation
  • Names, state and avatar for each friend (ClientPersonaState, ClientRequestFriendData); avatars fetched through the account's route and cached like the others
  • Each friend's presence shown: online, away, busy, snooze, offline, and the game they are in
  • Our own presence set: Online, Idle (Away) and Invisible (ClientChangeStatus), from the plaque, the tray and the status popout; only those offered for a Steam account
  • Idle detection moves a Steam account to Idle and back, like the other services

Messages

  • Send a direct message to a friend (FriendMessages.SendMessage), the server's timestamp and ordinal kept, refusals said (not a friend, blocked, limited account)
  • Receive direct messages (IncomingMessage), including ones sent from our other sessions, in order
  • Messages stored in the scrollback, unread and notification behaviour as a direct message has elsewhere
  • A live-account probe (ignored by default, credentials from the environment) that signs in and exchanges a message; unit tests for everything that can be tested without one

Round 2: history and read state

  • History when a conversation opens (GetRecentMessages), paging back
  • Recent conversations and unread counts (GetActiveMessageSessions) on sign-in
  • Reading a conversation here marks it read everywhere (AckMessage) and the reverse (NotifyAckMessageEcho)
  • Typing indicator, sent and received
  • Catching up after a reconnect without duplicating or losing messages

Round 3: how messages look

  • BBCode drawn (bold, italic, underline, strike, spoiler, code, quote, links) and composed from the formatting bar
  • Emoticons (:name:) drawn and offered in the picker (Player.GetEmoticonList), stickers and effects
  • Links and images: the link filter's warnings, picture previews, the existing link cards
  • Message reactions, added and removed, seen live (UpdateMessageReaction, MessageReaction)
  • Session notices (SessionNotice, such as a suspicious-account warning) shown and dismissable

Round 4: friends, profiles and games

  • Add a friend by SteamID, account name or invite link; friend requests received and answered; remove; block and ignore
  • Nicknames (Player.GetNicknameList, SetNickname) shown and edited
  • Friend groups/categories and favourites
  • Profile card: name, avatar, level, country, summary, mutual friends
  • What a friend is playing, rich presence text, how long, joinable games and game invites
  • Which client a friend is on (desktop, mobile, web, Steam Deck, Big Picture)
  • Last seen / last online
  • Per-friend notification preferences

Round 5: account and security

  • QR sign-in (BeginAuthSessionViaQR)
  • Remembered machine (guard_data) so Steam Guard is not asked every time
  • Token renewal and revocation; list and sign out other sessions
  • Limited-account state shown (a limited account cannot message everyone)
  • Family View restrictions respected and explained
  • The text filter (Player.GetTextFilterWords) honoured where the user has it on
  • Privacy settings read

Round 6: Steam Chat groups

  • My groups listed (GetMyChatRoomGroups) as servers, their rooms as channels
  • Joining by invite link or friend invitation; leaving
  • Read and send in a room; history; unread and mention counts
  • Member list with roles and presence; the member-list view updates
  • Mentions (@name, @all) and notifications
  • Reactions, emoticons, replies
  • Roles and permissions shown; creating, renaming, deleting rooms and groups for those allowed to
  • Moderation: kick, ban and unban, mute, delete messages
  • Invite links: make, list, revoke
  • Group avatar, tagline, notices
  • Clan (community group) chat rooms
  • Voice chat rooms (undecided: needs Steam's voice transport)

Out of scope

Store, Library, game launching and downloads, Market, Workshop, community posts and comments, achievements, trading, the overlay and in-game voice. Steam notifications (SteamNotificationClient) only if a chat-relevant use turns up.

Add Steam Chat *crib ideas from Pidgin plugin* Steam Chat as a fifth-plus protocol beside IRC, Discord, Matrix, Kick and Sneedchat: a Steam account signs in, its friends appear as direct-message conversations with their presence, and messages go both ways. A chat client only: no Store, Library, Market, Workshop, or game launching. **Branch:** `protocol/steam` in both moho and nobilis (Gitea only). `development` is merged into it often. **Code:** `nobilis/src/backend/steam/`, a folder of its own like `irc/`, `matrix/` and `kick/`, plus the window's account form, presence and service mark. As each item is finished its commit goes beside it, as `(moho abc1234, nobilis def5678)`. ## Sources - The decompiled official client, `Salastil/steam` (`decomp/protos/*.proto` are the message definitions extracted from `steamclient.so`, `decomp/linux64_steamclient.so/` is its pseudo-C and strings). This is the ground truth for message shapes and job names. Read for facts, never copied. - Open source, for behaviour and cross-checking. MIT/public domain can be learned from freely; LGPL (SteamKit) is read, not copied, because moho is GPL-3.0-or-later and the licences must stay compatible. - `icewind1991/steam-vent` (Rust, MIT): WebSocket transport, auth sessions, service methods. - `DoctorMcKay/node-steam-user` (JS, MIT): friends, persona and chat flows, error handling in the wild. - `ValvePython/steam` (Python, MIT). - `SteamRE/SteamKit` (C#, LGPL): the most complete picture of the CM protocol and its quirks. - `SteamDatabase/Protobufs` (public domain): every `.proto`, with history. - The Pidgin plugin named above, for what a third-party chat client has to cover. ## How the protocol works **Connecting.** The official client asks `https://api.steampowered.com/ISteamDirectory/GetCMListForConnect/v1/?cellid=<n>&qoslevel=<n>&cmtype=websockets` for a list of connection managers (CMs): each has `endpoint` (`host:443`), `type`, `dc`, `load`. It connects a WebSocket to `wss://<endpoint>/cmsocket/`. TLS protects the channel, so the TCP-era channel-encryption handshake (`ChannelEncryptRequest`) is not needed. The connection goes through the account's route like every other protocol (Clearnet, Tor, SOCKS5). **Packets.** Each WebSocket binary message is one packet: `u32 LE EMsg` with bit `0x80000000` set for protobuf, `u32 LE header length`, a `CMsgProtoBufHeader`, then the message body. The header carries `steamid`, `client_sessionid`, `jobid_source`/`jobid_target` (request/response matching), `target_job_name`, `eresult`. `EMsg 1` (Multi) holds a `CMsgMulti` whose body, gzip-compressed when `size_unzipped` is set, is a run of `u32 length, packet`. **Service methods.** Most modern features are unified-service calls: `EMsg 151` (authenticated) or `9804` (before logon) with `target_job_name` of `Service.Method#1` and the request protobuf as the body; the answer is `EMsg 147` carrying the same job id and an `eresult`. The server also pushes notifications to the client as `EMsg 152` with `target_job_name` such as `FriendMessagesClient.IncomingMessage#1`. The older fixed messages (`ClientLogon` 5514, `ClientFriendsList` 767, `ClientPersonaState` 766, `ClientChangeStatus` 716, `ClientHeartBeat` 703, `ClientLogOff` 706, `ClientLoggedOff` 757) remain for session, friends and presence. **Signing in.** 1. `ClientHello` (9805) with the protocol version. 2. `Authentication.GetPasswordRSAPublicKey#1` (account name) returns `publickey_mod`, `publickey_exp` (hex) and a timestamp. The password is RSA-encrypted (PKCS#1 v1.5) with it and sent as base64. 3. `Authentication.BeginAuthSessionViaCredentials#1` returns `client_id`, `request_id`, a poll `interval`, the `steamid`, and `allowed_confirmations`: `None`, `EmailCode`, `DeviceCode` (the mobile authenticator's TOTP), `DeviceConfirmation` (approve on the phone), `EmailConfirmation`, `MachineToken`. 4. A code is sent with `Authentication.UpdateAuthSessionWithSteamGuardCode#1`; a phone approval needs nothing sent. `Authentication.PollAuthSessionStatus#1` is polled at the interval until it returns a **refresh token** and an access token, plus `new_guard_data` (remembers this machine so the next sign-in is not challenged). 5. `BeginAuthSessionViaQR` is the passwordless alternative: show a QR code, the phone approves it. 6. `ClientLogon` carries the account name and the token in `access_token`; the header `steamid` is the account's SteamID64. The current client's protocol version is 65581 in its code (steam-vent still sends 65580 and is accepted). The response gives the `eresult`, the `heartbeat_seconds` interval and, in the header, the `client_sessionid` used from then on. `Authentication.GenerateAccessTokenForApp#1` renews tokens. Only the token is kept, never the password; sign-in again only when the token is refused or revoked. **Presence and friends.** After logon the server sends `ClientFriendsList` (full, then incremental) with each relationship, and `ClientPersonaState` with names, state, avatar hash, the game being played, rich presence and `persona_state_flags` (mobile, web, big picture, Steam Deck client types). `ClientRequestFriendData` asks for more, `ClientChangeStatus` sets our own state. Persona states: Offline 0, Online 1, Busy 2, Away 3, Snooze 4, LookingToTrade 5, LookingToPlay 6, Invisible 7. Relationships: None 0, Blocked 1, RequestRecipient 2, Friend 3, RequestInitiator 4, Ignored 5, IgnoredFriend 6. Avatars are `https://avatars.steamstatic.com/<hash>_full.jpg` (also `_medium`, and the bare hash for small). **Direct messages.** `FriendMessages.SendMessage#1` (steamid, `chat_entry_type` 1 for a message, text, `contains_bbcode`, `client_message_id`, `echo_to_sender`) answers with the server timestamp, an `ordinal` that orders messages sharing a second, and the text as the server stored it. Incoming messages arrive as `FriendMessagesClient.IncomingMessage#1` (also `local_echo` for what was sent from another of our sessions, and `message_no_bbcode`). Typing is `chat_entry_type` 2. `GetRecentMessages#1` and `GetActiveMessageSessions#1` give history and unread state; `AckMessage` marks read and is echoed to our other sessions (`NotifyAckMessageEcho`). Formatting is BBCode; emoticons are `:name:`. **Chat groups (Steam Chat).** The `ChatRoom` service: groups with text rooms, roles and permissions, members, invites and invite links, history, reactions, voice rooms; pushes in `ChatRoomClient`. **Open questions, to settle by trying them on a real account.** - Coexisting with the desktop Steam client on the same account: as a second desktop-type session it can be answered with `LoggedInElsewhere` (EResult 6). The mobile chat app and the web chat are different session types and live beside the desktop client; the sign-in already has the fields for that (`EAuthTokenPlatformType`, `EAuthTokenAppType`, `gaming_device_type`, `persona_state_flags`). Which one moho presents as decides how it behaves beside Steam. - Rate limits on sign-in (`RateLimitExceeded`, 84) and what `supports_rate_limit_response` changes. - What is the least a chat-only session must send in `ClientLogon`. ## Round 1: sign in, friends, presence, direct messages *Signing in and out* - [ ] Account type `steam:<account name>` in `accounts.toml`, added, edited, removed and listed like the others; service mark and a name in the account form - [ ] CM discovery (`GetCMListForConnect`, least-loaded first) and a WebSocket to `/cmsocket/`, through the account's route - [ ] Packet reading and writing; Multi/gzip unpacking; tests against hand-built packets - [ ] The protobuf messages this needs, written from the extracted definitions - [ ] Service-method calls before and after logon, answers matched to requests by job id, timeouts - [ ] Password sign-in: RSA, begin, poll - [ ] Steam Guard: the authenticator's code, an emailed code, and approval on the phone, asked for in the account form as they come - [ ] Token kept in the account's credentials the way the others are (private file); used for every later connect, password never stored - [ ] `ClientLogon` with the token; the response read for the session, the heartbeat interval and the failure reasons, each said in words (wrong password, throttled, refused token) - [ ] Heartbeat; reconnect with a growing wait; another CM when told to try one; sign-out by another client reported as that - [ ] Sign out (`ClientLogOff`) and disconnect cleanly, with an account switched off staying off *Friends and presence* - [ ] Friends list read (`ClientFriendsList`, incremental updates) and listed: each friend a direct-message conversation - [ ] Names, state and avatar for each friend (`ClientPersonaState`, `ClientRequestFriendData`); avatars fetched through the account's route and cached like the others - [ ] Each friend's presence shown: online, away, busy, snooze, offline, and the game they are in - [ ] Our own presence set: Online, Idle (Away) and Invisible (`ClientChangeStatus`), from the plaque, the tray and the status popout; only those offered for a Steam account - [ ] Idle detection moves a Steam account to Idle and back, like the other services *Messages* - [ ] Send a direct message to a friend (`FriendMessages.SendMessage`), the server's timestamp and ordinal kept, refusals said (not a friend, blocked, limited account) - [ ] Receive direct messages (`IncomingMessage`), including ones sent from our other sessions, in order - [ ] Messages stored in the scrollback, unread and notification behaviour as a direct message has elsewhere - [ ] A live-account probe (ignored by default, credentials from the environment) that signs in and exchanges a message; unit tests for everything that can be tested without one ## Round 2: history and read state - [ ] History when a conversation opens (`GetRecentMessages`), paging back - [ ] Recent conversations and unread counts (`GetActiveMessageSessions`) on sign-in - [ ] Reading a conversation here marks it read everywhere (`AckMessage`) and the reverse (`NotifyAckMessageEcho`) - [ ] Typing indicator, sent and received - [ ] Catching up after a reconnect without duplicating or losing messages ## Round 3: how messages look - [ ] BBCode drawn (bold, italic, underline, strike, spoiler, code, quote, links) and composed from the formatting bar - [ ] Emoticons (`:name:`) drawn and offered in the picker (`Player.GetEmoticonList`), stickers and effects - [ ] Links and images: the link filter's warnings, picture previews, the existing link cards - [ ] Message reactions, added and removed, seen live (`UpdateMessageReaction`, `MessageReaction`) - [ ] Session notices (`SessionNotice`, such as a suspicious-account warning) shown and dismissable ## Round 4: friends, profiles and games - [ ] Add a friend by SteamID, account name or invite link; friend requests received and answered; remove; block and ignore - [ ] Nicknames (`Player.GetNicknameList`, `SetNickname`) shown and edited - [ ] Friend groups/categories and favourites - [ ] Profile card: name, avatar, level, country, summary, mutual friends - [ ] What a friend is playing, rich presence text, how long, joinable games and game invites - [ ] Which client a friend is on (desktop, mobile, web, Steam Deck, Big Picture) - [ ] Last seen / last online - [ ] Per-friend notification preferences ## Round 5: account and security - [ ] QR sign-in (`BeginAuthSessionViaQR`) - [ ] Remembered machine (`guard_data`) so Steam Guard is not asked every time - [ ] Token renewal and revocation; list and sign out other sessions - [ ] Limited-account state shown (a limited account cannot message everyone) - [ ] Family View restrictions respected and explained - [ ] The text filter (`Player.GetTextFilterWords`) honoured where the user has it on - [ ] Privacy settings read ## Round 6: Steam Chat groups - [ ] My groups listed (`GetMyChatRoomGroups`) as servers, their rooms as channels - [ ] Joining by invite link or friend invitation; leaving - [ ] Read and send in a room; history; unread and mention counts - [ ] Member list with roles and presence; the member-list view updates - [ ] Mentions (`@name`, `@all`) and notifications - [ ] Reactions, emoticons, replies - [ ] Roles and permissions shown; creating, renaming, deleting rooms and groups for those allowed to - [ ] Moderation: kick, ban and unban, mute, delete messages - [ ] Invite links: make, list, revoke - [ ] Group avatar, tagline, notices - [ ] Clan (community group) chat rooms - [ ] Voice chat rooms (undecided: needs Steam's voice transport) ## Out of scope Store, Library, game launching and downloads, Market, Workshop, community posts and comments, achievements, trading, the overlay and in-game voice. Steam notifications (`SteamNotificationClient`) only if a chat-relevant use turns up.
Salastil added the feature label 2026-08-24 00:15:33 -04:00
Salastil added protocol and removed feature labels 2026-08-28 00:00:54 -04:00
Salastil added the feature label 2026-09-03 13:11:22 -04:00
Salastil added this to the (deleted) milestone 2026-09-10 17:44:07 -04:00
Salastil added this to the Protocols project 2026-09-11 14:57:05 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#29