vapour_protocol/
serverlist.rs1use std::sync::Arc;
2
3use tokio::sync::Mutex;
4
5use crate::error::{Error, Result};
6
7const CM_LIST_URL: &str = "https://api.steampowered.com/ISteamDirectory/GetCMListForConnect/v1/?cellid=0&cmtype=websockets&format=json";
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct CmServer {
11 pub endpoint: String,
12 pub legacy_endpoint: Option<String>,
13 pub data_center: Option<String>,
14 pub realm: Option<String>,
15 pub load: Option<u32>,
16 pub weighted_load: Option<f64>,
17}
18
19impl CmServer {
20 pub fn websocket_url(&self) -> String {
21 format!("wss://{}/cmsocket/", self.endpoint)
22 }
23}
24
25#[derive(Clone, Debug)]
26pub struct ServerListCache {
27 client: reqwest::Client,
28 cache: Arc<Mutex<Vec<CmServer>>>,
29}
30
31impl Default for ServerListCache {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl ServerListCache {
38 pub fn new() -> Self {
39 Self {
40 client: reqwest::Client::builder()
41 .user_agent("vapour-protocol/0.1")
42 .build()
43 .expect("reqwest client builder is valid"),
44 cache: Arc::new(Mutex::new(Vec::new())),
45 }
46 }
47
48 pub async fn list(&self, force_refresh: bool) -> Result<Vec<CmServer>> {
49 let mut cache = self.cache.lock().await;
50 if force_refresh || cache.is_empty() {
51 *cache = fetch_cm_list(&self.client).await?;
52 }
53
54 Ok(cache.clone())
55 }
56}
57
58pub async fn fetch_cm_list(client: &reqwest::Client) -> Result<Vec<CmServer>> {
59 let response = client.get(CM_LIST_URL).send().await?.error_for_status()?;
60 let directory: DirectoryResponse = response.json().await?;
61
62 if !directory.response.success {
63 return Err(Error::Protocol(
64 "Steam CM directory returned failure".to_owned(),
65 ));
66 }
67
68 let mut servers: Vec<CmServer> = directory
69 .response
70 .serverlist
71 .into_iter()
72 .map(|server| CmServer {
73 endpoint: server.endpoint,
74 legacy_endpoint: server.legacy_endpoint,
75 data_center: server.data_center,
76 realm: server.realm,
77 load: server.load,
78 weighted_load: server.weighted_load,
79 })
80 .collect();
81
82 servers.sort_by(|left, right| {
83 left.weighted_load
84 .partial_cmp(&right.weighted_load)
85 .unwrap_or(std::cmp::Ordering::Equal)
86 });
87
88 if servers.is_empty() {
89 return Err(Error::InvalidResponse(
90 "Steam CM directory returned no servers",
91 ));
92 }
93
94 Ok(servers)
95}
96
97#[derive(Debug, serde::Deserialize)]
98struct DirectoryResponse {
99 response: DirectoryBody,
100}
101
102#[derive(Debug, serde::Deserialize)]
103struct DirectoryBody {
104 #[serde(default)]
105 serverlist: Vec<DirectoryServer>,
106 success: bool,
107}
108
109#[derive(Debug, serde::Deserialize)]
110struct DirectoryServer {
111 endpoint: String,
112 #[serde(default)]
113 legacy_endpoint: Option<String>,
114 #[serde(default, rename = "dc")]
115 data_center: Option<String>,
116 #[serde(default)]
117 realm: Option<String>,
118 #[serde(default)]
119 load: Option<u32>,
120 #[serde(default, rename = "wtd_load")]
121 weighted_load: Option<f64>,
122}