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}