steam_api_client/
steam_client.rs

1use std::fmt;
2
3use reqwest::{Client, Method, StatusCode, Url};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[allow(dead_code)]
8const DEFAULT_BASE_URL: &str = "http://api.steampowered.com";
9
10#[derive(Error, Debug)]
11#[allow(dead_code)]
12pub enum SteamClientError {
13    #[error("HTTP request failed: {0}")]
14    RequestFailed(#[from] reqwest::Error),
15    #[error("Failed to parse JSON: {0}")]
16    JsonParseError(#[from] serde_json::Error),
17    #[error("Bad Request: Invalid request parameters")]
18    BadRequest,
19    #[error("Unauthorized: Authentication required")]
20    Unauthorized,
21    #[error("Forbidden: Access denied")]
22    Forbidden,
23    #[error("Not Found")]
24    NotFound,
25    #[error("Unprocessable Entity: Server cannot process the request")]
26    UnprocessableEntity,
27    #[error("Too Many Requests: Rate limit exceeded")]
28    RateLimitExceeded,
29    #[error("Internal Server Error: Unexpected error occurred")]
30    InternalServerError,
31    #[error("API error: {status}, message: {message}")]
32    ApiError { status: StatusCode, message: String },
33    #[error("Unexpected error: {0}")]
34    Other(String),
35    #[error("Invalid input: {0}")]
36    InvalidInput(String),
37    #[error("Invalid url: {0}")]
38    InvalidUrl(String),
39}
40
41pub enum RelationshipType {
42    ALL,
43    FRIEND
44}
45
46impl fmt::Display for RelationshipType{
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match *self {
49            RelationshipType::ALL => write!(f, "all"),
50            RelationshipType::FRIEND => write!(f, "friend")
51        }
52    }
53}
54
55#[allow(dead_code)]
56pub struct SteamClient {
57    client: Client,
58    api_key: String,
59    base_url: Url
60}
61
62impl SteamClient {
63    pub fn new(api_key: String) -> Self {
64        SteamClient {
65            client: Client::new(),
66            api_key: api_key,
67            base_url: Url::parse(DEFAULT_BASE_URL).expect("base_url must be a valid URL")
68        }
69    }
70
71    async fn request<T: for<'de> Deserialize<'de>>(
72        &self,
73        path: &str
74    ) -> Result<T, SteamClientError> {
75        let url = self.base_url.join(path).expect("path must be a valid URL");
76        let request = self.client.request(Method::GET, url);
77
78        let response = request.send().await?;
79
80        match response.status() {
81            StatusCode::OK | StatusCode::CREATED | StatusCode::NO_CONTENT => {
82                Ok(response.json().await?)
83            }
84            StatusCode::BAD_REQUEST => Err(SteamClientError::BadRequest),
85            StatusCode::UNAUTHORIZED => Err(SteamClientError::Unauthorized),
86            StatusCode::FORBIDDEN => Err(SteamClientError::Forbidden),
87            StatusCode::NOT_FOUND => Err(SteamClientError::NotFound),
88            StatusCode::UNPROCESSABLE_ENTITY => Err(SteamClientError::UnprocessableEntity),
89            StatusCode::TOO_MANY_REQUESTS => Err(SteamClientError::RateLimitExceeded),
90            StatusCode::INTERNAL_SERVER_ERROR => Err(SteamClientError::InternalServerError),
91            status => {
92                let message = response
93                    .text()
94                    .await
95                    .unwrap_or_else(|_| "Unknown error".to_string());
96                Err(SteamClientError::ApiError { status, message })
97            }
98        }
99    }
100
101    pub async fn get_news_for_app(
102        &self, 
103        appid: i64, 
104        count: i64, 
105        maxlength: i64
106    ) -> Result<AppNewsResponse, SteamClientError> {
107        let path = format!("/ISteamNews/GetNewsForApp/v0002/?appid={appid}&count={count}&maxlength={maxlength}");
108        self.request(&path).await
109    }
110
111    pub async fn get_global_achievement_percentages_for_app(
112        &self, 
113        gameid: i64
114    ) -> Result<GlobalAchievementResponse, SteamClientError> {
115        let path = format!("/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v0002/?gameid={gameid}");
116        self.request(&path).await
117    }
118
119    pub async fn get_player_summaries(
120        &self,
121        steamids: Vec<String>
122    ) -> Result<PlayerSummariesResponse, SteamClientError> {
123        let path = format!("/ISteamUser/GetPlayerSummaries/v0002/?key={}&steamids={}", self.api_key, steamids.join(","));
124        self.request(&path).await
125    }
126
127    pub async fn get_friend_list(
128        &self,
129        steamid: String,
130        relationship: RelationshipType
131    ) -> Result<FriendsListResponse, SteamClientError> {
132        let path = format!("/ISteamUser/GetFriendList/v0001/?key={}&steamid={steamid}&relationship={relationship}", self.api_key);
133        self.request(&path).await
134    }
135
136    pub async fn get_player_achievements(
137        &self,
138        appid: i64,
139        steamid: String
140    ) -> Result<PlayerAchievementsResponse, SteamClientError> {
141        let path = format!("/ISteamUserStats/GetPlayerAchievements/v0001/?appid={appid}&key={}&steamid={steamid}", self.api_key);
142        self.request(&path).await
143    }
144
145    pub async fn get_user_stats_for_game(
146        &self,
147        appid: i64,
148        steamid: String
149    ) -> Result<PlayerStatsResponse, SteamClientError> {
150        let path = format!("/ISteamUserStats/GetUserStatsForGame/v0002/?appid={appid}&key={}&steamid={steamid}", self.api_key);
151        self.request(&path).await
152    }
153
154    pub async fn get_owned_games(
155        &self,
156        steamid: String,
157        include_appinfo: bool,
158        include_played_free_games: bool
159    ) -> Result<OwnedGamesResponse, SteamClientError> {
160        let path = format!("/IPlayerService/GetOwnedGames/v0001/?key={}&steamid={steamid}&include_appinfo={include_appinfo}&include_played_free_games={include_played_free_games}", self.api_key);
161        self.request(&path).await
162    }
163
164    pub async fn get_recently_played_games(
165        &self,
166        steamid: String,
167        count: i64
168    ) -> Result<RecentlyPlayedGamesResponse, SteamClientError> {
169        let path = format!("/IPlayerService/GetRecentlyPlayedGames/v0001/?key={}&steamid={steamid}&count={count}", self.api_key);
170        self.request(&path).await
171    }
172
173}
174
175#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
176#[serde(rename_all = "camelCase")]
177pub struct AppNewsResponse {
178    pub appnews: AppNews,
179}
180
181#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct AppNews {
184    pub appid: i64,
185    pub newsitems: Vec<NewsItem>,
186    pub count: i64,
187}
188
189#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct NewsItem {
192    pub gid: String,
193    pub title: String,
194    pub url: String,
195    #[serde(rename = "is_external_url")]
196    pub is_external_url: bool,
197    pub author: String,
198    pub contents: String,
199    pub feedlabel: String,
200    pub date: i64,
201    pub feedname: String,
202    #[serde(rename = "feed_type")]
203    pub feed_type: i64,
204    pub appid: i64,
205}
206
207#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct GlobalAchievementResponse {
210    pub achievementpercentages: GlobalAchievementPercentages,
211}
212
213#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct GlobalAchievementPercentages {
216    pub achievements: Vec<AchievementPercentage>,
217}
218
219#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct AchievementPercentage {
222    pub name: String,
223    pub percent: String,
224}
225
226#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct PlayerSummariesResponse {
229    #[serde(rename="response")]
230    pub playersummaries: PlayerSummaries,
231}
232
233#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct PlayerSummaries {
236    pub players: Vec<Player>,
237}
238
239#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct Player {
242    pub steamid: String,
243    pub communityvisibilitystate: i64,
244    pub profilestate: i64,
245    pub personaname: String,
246    pub profileurl: String,
247    pub avatar: String,
248    pub avatarmedium: String,
249    pub avatarfull: String,
250    pub avatarhash: String,
251    pub personastate: i64,
252    pub realname: Option<String>,
253    pub primaryclanid: Option<String>,
254    pub timecreated: Option<i64>,
255    pub personastateflags: Option<i64>,
256    pub loccountrycode: Option<String>,
257    pub locstatecode: Option<String>,
258    pub loccityid: Option<i64>,
259}
260
261#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct FriendsListResponse {
264    pub friendslist: FriendsList,
265}
266
267#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct FriendsList {
270    pub friends: Vec<Friend>,
271}
272
273#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct Friend {
276    pub steamid: String,
277    pub relationship: String,
278    #[serde(rename = "friend_since")]
279    pub friend_since: i64,
280}
281
282#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct PlayerAchievementsResponse {
285    pub playerstats: PlayerAchievements,
286}
287
288#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct PlayerAchievements {
291    #[serde(rename = "steamID")]
292    pub steam_id: String,
293    pub game_name: String,
294    pub achievements: Vec<Achievement>,
295    pub success: bool,
296}
297
298#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct Achievement {
301    pub apiname: String,
302    pub achieved: i64,
303    pub unlocktime: i64,
304}
305
306#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct PlayerStatsResponse {
309    pub playerstats: PlayerStats,
310}
311
312#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct PlayerStats {
315    #[serde(rename = "steamID")]
316    pub steam_id: String,
317    pub game_name: String,
318    pub stats: Vec<Stat>,
319    pub achievements: Vec<PlayerStatsAchievement>,
320}
321
322#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct Stat {
325    pub name: String,
326    pub value: i64,
327}
328
329#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct PlayerStatsAchievement {
332    pub name: String,
333    pub achieved: i64,
334}
335
336#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct OwnedGamesResponse {
339    pub response: OwnedGamesData,
340}
341
342#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct OwnedGamesData {
345    #[serde(rename = "game_count")]
346    pub game_count: i64,
347    pub games: Vec<OwnedGame>,
348}
349
350#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct OwnedGame {
353    pub appid: i64,
354    pub name: Option<String>,
355    #[serde(rename = "playtime_forever")]
356    pub playtime_forever: i64,
357    #[serde(rename = "img_icon_url")]
358    pub img_icon_url: Option<String>,
359    #[serde(rename = "content_descriptorids")]
360    #[serde(default)]
361    pub content_descriptorids: Vec<i64>,
362    #[serde(rename = "has_community_visible_stats")]
363    pub has_community_visible_stats: Option<bool>,
364    #[serde(rename = "has_leaderboards")]
365    pub has_leaderboards: Option<bool>,
366    #[serde(rename = "playtime_2weeks")]
367    pub playtime_2weeks: Option<i64>,
368}
369
370#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct RecentlyPlayedGamesResponse {
373    #[serde(rename = "response")]
374    pub recently_played_games: RecentlyPlayedGamesData,
375}
376
377#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct RecentlyPlayedGamesData {
380    #[serde(rename = "total_count")]
381    pub total_count: i64,
382    pub games: Vec<RecentlyPlayedGame>,
383}
384
385#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct RecentlyPlayedGame {
388    pub appid: i64,
389    pub name: String,
390    #[serde(rename = "playtime_2weeks")]
391    pub playtime_2weeks: i64,
392    #[serde(rename = "playtime_forever")]
393    pub playtime_forever: i64,
394    #[serde(rename = "img_icon_url")]
395    pub img_icon_url: String,
396}