Expand description
§vapour-protocol
Rust implementation of the Steam client protocol for building native Steam applications.
vapour-protocol speaks directly to Steam CM servers over WebSocket and exposes higher-level
Rust APIs for client-style Steam features — authentication, friends, chat, and your game library —
without depending on the Steam client or Web API key. It was built for, and is used by,
Vapour, but it is a standalone crate you can use on its own.
§Features
- Steam authentication: QR, username/password (with Steam Guard), and refresh-token reuse.
- CM server discovery and WebSocket transport.
- Friends/persona state and real-time friend presence events.
- 1-on-1 friend chat: send, receive, typing indicators, and history.
- Owned/recently-played library loading through protocol messages.
- Per-user achievements and PICS app metadata (type, install dir, launch options).
- Service method calls for authenticated unified Steam services.
§Status
This crate is usable and evolving toward a stable public API. It is 0.x: expect breaking changes
between minor releases while protocol coverage grows. Talking to Steam requires a real Steam account,
and you are responsible for using it in line with the Steam Subscriber Agreement.
§Requirements
- Rust 1.85+ (the crate uses edition 2024).
- A Tokio runtime — the API is async.
- A real Steam account to authenticate against.
§Install
cargo add vapour-protocolvapour-protocol pulls in tokio, so add it too if you don’t already depend on it:
cargo add tokio --features full§Quickstart
Log in with a QR code, then load your owned games and echo any incoming chat message. Every type used here is re-exported from the crate root.
use tokio::sync::mpsc;
use vapour_protocol::{AuthEvent, AuthMethod, Error, FriendsEvent, RunCommand, SteamClient};
#[tokio::main]
async fn main() -> vapour_protocol::Result<()> {
let mut client = SteamClient::new();
// 1. Authenticate. QR is shown here; see "Authentication" for credentials / refresh tokens.
let mut auth = client.begin_auth(AuthMethod::Qr).await?;
let session = loop {
match auth.recv().await {
Some(AuthEvent::QrChallenge(url)) => {
// Render `url` as a QR code and scan it in the Steam Mobile app.
println!("Scan to sign in: {url}");
}
Some(AuthEvent::GuardRequired(kind)) => println!("Steam Guard required: {kind:?}"),
Some(AuthEvent::Success(session)) => break session,
Some(AuthEvent::Failure(error)) => return Err(error),
None => return Err(Error::Closed),
}
};
println!("Logged in as {} ({})", session.account_name, session.steamid);
// Persist `session.refresh_token` to log in again later without re-scanning
// (see AuthMethod::RefreshToken below).
// 2. Drive the session over two channels: you send `RunCommand`s in and
// receive `FriendsEvent`s back.
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
// Handle events — and react with new commands — on a background task.
let commands = cmd_tx.clone();
tokio::spawn(async move {
while let Some(event) = event_rx.recv().await {
match event {
FriendsEvent::OwnedGames(games) => {
for game in games {
println!("{} — {} min played", game.name, game.playtime_forever);
}
}
FriendsEvent::IncomingMessage(msg) => {
println!("{}: {}", msg.steamid, msg.message);
let _ = commands.send(RunCommand::SendMessage {
steamid: msg.steamid,
message: "got it".to_owned(),
});
}
_ => {}
}
}
});
// Ask for the owned library; results arrive as the events handled above.
let _ = cmd_tx.send(RunCommand::GetLibrary);
// `run` owns the connection loop and returns when Steam disconnects.
client.run(cmd_rx, event_tx).await
}§Session model
A SteamClient has three stages:
SteamClient::new()— create the client.begin_auth(method)— connect and start an auth flow. Returns a receiver ofAuthEvents; wait forAuthEvent::Success(LoggedOn).run(commands, events)— take over the connection. You drive the session by sendingRunCommands on thecommandschannel and reacting toFriendsEvents on theeventschannel.runsets your status online, requests friend data as it arrives, and resolves until the connection closes.
§Commands you send (RunCommand)
| Command | Effect | Result event |
|---|---|---|
SetPersonaState(PersonaState) | Set your online status | — |
RequestFriendData(Vec<u64>) | Request persona data for SteamIDs | PersonaStates |
GetLibrary | Load owned + recently-played games | RecentlyPlayedGames, then OwnedGames |
GetPlayerAchievements(u32) | Achievements for an appid | PlayerAchievements |
SendMessage { steamid, message } | Send a 1-on-1 chat message | MessageSent |
SendTyping { steamid } | Send a typing indicator (best-effort) | — |
GetRecentMessages { steamid } | Fetch recent chat history | RecentMessages |
§Events you receive (FriendsEvent)
| Event | Meaning |
|---|---|
FriendsList(Vec<Friend>) | Your friends list (pushed after login) |
PersonaStates(Vec<Persona>) | Presence/profile updates |
RecentlyPlayedGames(Vec<ProtocolGame>) | Recently-played subset of the library |
OwnedGames(Vec<ProtocolGame>) | Full owned-game catalog |
PlayerAchievements { appid, achievements } | Achievements for one app |
IncomingMessage(ChatMessage) | A 1-on-1 message arrived |
MessageSent(ChatMessage) | Your send confirmed (with server timestamp) |
TypingNotification { steamid } | A friend is typing |
RecentMessages { steamid, messages } | History for one conversation |
§Authentication
begin_auth takes one of three AuthMethods and reports progress as AuthEvents
(QrChallenge, GuardRequired, Success, Failure).
-
AuthMethod::Qr— emitsQrChallenge(url); render it as a QR code and scan it in the Steam Mobile app. -
AuthMethod::Credentials { account, password }— may emitGuardRequired(kind). ForEmailCode/DeviceCode, collect the code and callsubmit_guard_code; forDeviceConfirmation, approve the prompt in the Steam Mobile app (no code needed):ⓘlet mut auth = client .begin_auth(AuthMethod::Credentials { account: "username".to_owned(), password: "password".to_owned(), }) .await?; while let Some(event) = auth.recv().await { match event { AuthEvent::GuardRequired(GuardKind::EmailCode | GuardKind::DeviceCode) => { client.submit_guard_code("ABCDE")?; // a code you collected from the user } AuthEvent::Success(session) => { /* logged in; break and call run() */ } AuthEvent::Failure(error) => return Err(error), _ => {} } } -
AuthMethod::RefreshToken(token)— reuse therefresh_tokenfrom a previousLoggedOnto sign in non-interactively. Pair it withset_account_name_hintif you have the account name. Store refresh tokens securely; they grant access to the account.
§Development
Run the same checks as CI before opening a PR:
cargo fmt --all -- --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked
cargo package --lockedIf you are developing vapour-protocol alongside Vapour, point Vapour at your local checkout with a
path dependency so changes to both land together:
vapour-protocol = { path = "../vapour-protocol" }Release steps are documented in how_to_release.md.
§License
This repository is licensed under MIT.
Re-exports§
pub use chat::ChatMessage;pub use client::AuthEvent;pub use client::AuthMethod;pub use client::GuardKind;pub use client::LoggedOn;pub use client::RunCommand;pub use client::SteamClient;pub use error::Error;pub use error::Result;pub use friends::Friend;pub use friends::FriendsEvent;pub use friends::LaunchEntry;pub use friends::Persona;pub use friends::PersonaState;pub use friends::ProtocolAchievement;pub use friends::ProtocolGame;
Modules§
- achievements
- auth
- chat
- 1-on-1 friend chat over the modern unified
FriendMessages.*service. - client
- connection
- emsg
- eresult
- error
- friends
- library
- message
- pics
- protobuf
- serverlist
- service_
method - token
- transport