steam_vent/
serverlist.rs

1use rand::prelude::*;
2use rand::rng;
3use reqwest::{Client, Error};
4use serde::Deserialize;
5use std::iter::Cycle;
6use std::net::SocketAddr;
7use std::sync::{Arc, Mutex};
8use std::vec::IntoIter;
9use thiserror::Error;
10use tracing::debug;
11
12#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum ServerDiscoveryError {
15    #[error("Failed send discovery request: {0:#}")]
16    Network(reqwest::Error),
17    #[error("steam returned an empty server list")]
18    NoServers,
19    #[error("steam returned an empty websocket server list")]
20    NoWsServers,
21}
22
23impl From<reqwest::Error> for ServerDiscoveryError {
24    fn from(value: Error) -> Self {
25        ServerDiscoveryError::Network(value)
26    }
27}
28
29#[derive(Default, Clone, Debug)]
30pub struct DiscoverOptions {
31    web_client: Option<Client>,
32    // todo: some smart cell based routing based on
33    // https://raw.githubusercontent.com/SteamDatabase/SteamTracking/6d23ebb0070998ae851278cfae5f38832f4ac28d/ClientExtracted/steam/cached/CellMap.vdf
34    cell: u8,
35}
36
37impl DiscoverOptions {
38    pub fn with_web_client(self, web_client: Client) -> Self {
39        DiscoverOptions {
40            web_client: Some(web_client),
41            ..self
42        }
43    }
44
45    pub fn with_cell(self, cell: u8) -> Self {
46        DiscoverOptions { cell, ..self }
47    }
48}
49
50#[derive(Debug, Clone)]
51pub struct ServerList {
52    servers: Arc<Mutex<Cycle<IntoIter<SocketAddr>>>>,
53    ws_servers: Arc<Mutex<Cycle<IntoIter<String>>>>,
54}
55
56impl ServerList {
57    pub async fn discover() -> Result<ServerList, ServerDiscoveryError> {
58        Self::discover_with(DiscoverOptions::default()).await
59    }
60
61    pub async fn discover_with(
62        options: DiscoverOptions,
63    ) -> Result<ServerList, ServerDiscoveryError> {
64        let client = options.web_client.unwrap_or_default();
65        let cell = options.cell;
66
67        let response: ServerListResponse = client
68            .get(format!(
69                "https://api.steampowered.com/ISteamDirectory/GetCMList/v1/?cellid={cell}"
70            ))
71            .send()
72            .await?
73            .json()
74            .await?;
75        if response.response.server_list.is_empty() {
76            return Err(ServerDiscoveryError::NoServers);
77        }
78        if response.response.server_list.is_empty() {
79            return Err(ServerDiscoveryError::NoWsServers);
80        }
81        Ok(response.into())
82    }
83
84    /// Pick a server from the server list, rotating them in a round-robin way for reconnects.
85    ///
86    /// # Returns
87    /// The selected `SocketAddr`
88    pub fn pick(&self) -> SocketAddr {
89        // SAFETY:
90        // `lock` cannot panic as we cannot lock again within the same thread.
91        // `unwrap` is safe as `discover_with` already checks for servers being present.
92        let addr = self.servers.lock().unwrap().next().unwrap();
93        debug!(addr = ?addr, "picked server from list");
94        addr
95    }
96
97    /// Pick a WebSocket server from the server list, rotating them in a round-robin way for reconnects.
98    ///
99    /// # Returns
100    /// A WebSocket URL to connect to, if the server list contains any servers.
101    pub fn pick_ws(&self) -> String {
102        // SAFETY: Same as for `pick`.
103        let addr = self.ws_servers.lock().unwrap().next().unwrap();
104        debug!(addr = ?addr, "picked websocket server from list");
105        format!("wss://{addr}/cmsocket/")
106    }
107}
108
109impl From<ServerListResponse> for ServerList {
110    fn from(value: ServerListResponse) -> Self {
111        let (mut servers, mut ws_servers) = (
112            value.response.server_list,
113            value.response.server_list_websockets,
114        );
115        servers.shuffle(&mut rng());
116        ws_servers.shuffle(&mut rng());
117
118        ServerList {
119            servers: Arc::new(Mutex::new(servers.into_iter().cycle())),
120            ws_servers: Arc::new(Mutex::new(ws_servers.into_iter().cycle())),
121        }
122    }
123}
124
125#[derive(Debug, Deserialize)]
126struct ServerListResponse {
127    response: ServerListResponseInner,
128}
129
130#[derive(Debug, Deserialize)]
131struct ServerListResponseInner {
132    #[serde(rename = "serverlist")]
133    server_list: Vec<SocketAddr>,
134    #[serde(rename = "serverlist_websockets")]
135    server_list_websockets: Vec<String>,
136}