api.rs
1 use std::collections::HashMap; 2 use std::io::ErrorKind; 3 use std::path::Path; 4 use anyhow::{anyhow, Context, Result}; 5 use path_slash::PathExt; 6 use serde::de::DeserializeOwned; 7 use crate::data::secure::getRetroAchievementsAuth; 8 use super::{Payload_GetGameInfo, Payload_GetUserCompletionProgress, 9 Payload_GetUserProfile, RetroAchievementsAuth}; 10 11 pub struct RetroAchievementsApi; 12 13 impl RetroAchievementsApi 14 { 15 const BaseUrl: &str = "https://retroachievements.org/API/"; 16 const MediaUrl: &str = "https://media.retroachievements.org/"; 17 18 const Endpoint_GetGameInfo: &str = "API_GetGameInfoAndUserProgress.php"; 19 const Endpoint_GetUserGameCompletion: &str = "API_GetUserCompletionProgress.php"; 20 const Endpoint_GetUserProfile: &str = "API_GetUserProfile.php"; 21 22 pub const GetUserGameCompletion_Count: u64 = 100; 23 24 pub const BadgePath: &str = "Badge"; 25 pub const BadgeLockedSuffix: &str = "lock"; 26 27 const Parameter_ApiKey: &str = "y"; 28 const Parameter_ApiUsername: &str = "u"; 29 30 pub const Platform: &str = "RetroAchievements"; 31 32 pub fn sanitizeIconTitle(title: &String) -> String 33 { 34 return title 35 .replace("/", " - ") 36 .replace("\\", " - "); 37 } 38 39 /** 40 Call the GetGameInfoAndUserProgress endpoint to retrieve detailed information 41 about a specific game and the current user's progress for that game. 42 43 --- 44 45 # [GetGameInfoAndUserProgress](https://api-docs.retroachievements.org/v1/get-game-info-and-user-progress.html) 46 47 Example URL: `https://retroachievements.org/API/API_GetGameInfoAndUserProgress.php?a=1&g=#####&u=XXXXXXXX&y=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` 48 49 --- 50 51 ### Arguments 52 53 Name | Required | Description 54 :--|:--|:-- 55 y | Yes | Your web API key. 56 u | Yes | The target username or ULID. 57 g | Yes | The target game ID. 58 a | No | Set to "1" if user award metadata should be included (default: 0). 59 60 You must query the user by either their username or their ULID. Please note the username is not considered a stable value. As of 2025, users can change their usernames. Initially querying by username is a good way to fetch a ULID. 61 62 ### Return Value 63 64 A JSON response with the following properties: 65 66 Name | Description 67 :--|:-- 68 ID | int 69 Title | String 70 ConsoleID | int 71 ForumTopicID | int 72 Flags | int ; optional 73 ImageIcon | String 74 ImageTitle | String 75 ImageIngame | String 76 ImageBoxArt | String 77 Publisher | String 78 Developer | String 79 Genre | String 80 Released | Date time string 81 ReleasedAtGranularity | String 82 IsFinal | bool ; Deprecated, always returns false 83 RichPresencePatch | String 84 GuideURL | String ; optional 85 ConsoleName | String 86 ParentGameID | int ; optional 87 NumDistinctPlayers | int 88 NumAchievements | int 89 Achievements | Map\<int, Achievement\> 90 NumAwardedToUser | int 91 NumAwardedToUserHardcore | int 92 NumDistinctPlayersCasual | int 93 NumDistinctPlayersHardcore | int 94 UserCompletion | String 95 UserCompletionHardcore | String 96 HighestAwardKind | String ; optional 97 HighestAwardDate | Timestamp string ; optional 98 99 #### Achievement JSON properties: 100 101 Name | Description 102 :--|:-- 103 ID | int 104 Title | String 105 Description | String 106 Points | int 107 TrueRatio | int 108 Type | String ; optional 109 BadgeName | String 110 NumAwarded | int 111 NumAwardedHardcore | int 112 DisplayOrder | int 113 Author | String 114 AuthorULID | String 115 DateCreated | String 116 DateModified | String 117 MemAddr | String 118 DateEarned | String ; optional 119 DateEarnedHardcore | String ; optional 120 */ 121 #[allow(unused)] 122 pub fn getGameInfo(ulid: &String, gameId: u64) -> Result<Payload_GetGameInfo> 123 { 124 let auth = getRetroAchievementsAuth()?; 125 let mut parameters = Self::generateParameterMap(&auth); 126 parameters.remove(Self::Parameter_ApiUsername); 127 parameters.insert("u".into(), ulid.clone()); 128 parameters.insert("g".into(), gameId.to_string()); 129 parameters.insert("a".into(), "1".into()); 130 131 return Ok(Self::get::<Payload_GetGameInfo>( 132 &Self::Endpoint_GetGameInfo.into(), 133 ¶meters 134 ) 135 .context(format!( 136 "Error retrieving game info for {} from username {}", 137 gameId, 138 auth.username() 139 ))?); 140 } 141 142 /** 143 Call the GetUserCompletionProgress endpoint to retrieve the current user's 144 completion information for all games associated with their account. 145 146 --- 147 148 # [GetUserCompletionProgress](https://api-docs.retroachievements.org/v1/get-user-completion-progress.html) 149 150 Example URL: 151 `https://retroachievements.org/API/API_GetUserCompletionProgress.php?u=XXXXXXXX&y=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` 152 153 --- 154 155 ### Arguments 156 157 Name | Required | Description 158 :--|:--|:-- 159 y | Yes | Your web API key. 160 u | Yes | The target username or ULID. 161 c | No | Count, number of records to return (default: 100, max: 500). 162 o | No | Offset, number of entries to skip (default: 0). 163 164 You must query the user by either their username or their ULID. Please note the username is not considered a stable value. As of 2025, users can change their usernames. Initially querying by username is a good way to fetch a ULID. 165 166 ### Return Value 167 168 A JSON response with the following properties: 169 170 Name | Description 171 :--|:-- 172 Count | int 173 Total | int 174 Results | Array\<Game\> 175 176 #### Game JSON properties: 177 178 Name | Description 179 :--|:-- 180 GameID | int 181 Title | String 182 ImageIcon | String 183 ConsoleID | int 184 ConsoleName | String 185 MaxPossible | int 186 NumAwarded | int 187 NumAwardedHardcore | int 188 MostRecentAwardedDate | Timestamp string 189 HighestAwardKind | String 190 HighestAwardDate | Timestamp string 191 */ 192 pub fn getUserCompletionProgress(ulid: Option<String>, offset: Option<u64>) -> Result<Payload_GetUserCompletionProgress> 193 { 194 let auth = getRetroAchievementsAuth()?; 195 let mut parameters = Self::generateParameterMap(&auth); 196 parameters.remove(Self::Parameter_ApiUsername); 197 parameters.insert("u".into(), match ulid.clone() 198 { 199 Some(ulid) => ulid, 200 None => auth.username().clone(), 201 }); 202 parameters.insert("c".into(), Self::GetUserGameCompletion_Count.to_string()); 203 204 if let Some(o) = offset 205 { 206 parameters.insert("o".into(), o.to_string()); 207 } 208 209 return Ok(Self::get::<Payload_GetUserCompletionProgress>( 210 &Self::Endpoint_GetUserGameCompletion.into(), 211 ¶meters 212 ) 213 .context(format!( 214 "Error retrieving user completion progress for username {} (ulid {})", 215 auth.username(), 216 ulid.unwrap_or_default(), 217 ))?); 218 } 219 220 /** 221 Call the GetUserProfile endpoint to retrieve the current user's profile 222 information. 223 224 --- 225 226 # [GetUserProfile](https://api-docs.retroachievements.org/v1/get-user-profile.html) 227 228 Example URL: 229 `https://retroachievements.org/API/API_GetUserProfile.php?u=XXXXXXXX&y=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` 230 231 --- 232 233 ### Arguments 234 235 Name | Required | Description 236 :--|:--|:-- 237 y | Yes | Your web API key. 238 u | Yes | The target username or ULID. 239 240 You must query the user by either their username or their ULID. Please note the username is not considered a stable value. As of 2025, users can change their usernames. Initially querying by username is a good way to fetch a ULID. 241 242 ### Return Value 243 244 A JSON response with the following properties: 245 246 Name | Description 247 :--|:-- 248 User | String 249 ULID | String 250 UserPic | String 251 MemberSince | Date time string 252 RichPresenceMsg | String 253 LastGameID | int 254 ContribCount | int 255 ContribYield | int 256 TotalPoints | int 257 TotalSoftcorePoints | int 258 TotalTruePoints | int 259 Permissions | int 260 ID | int 261 UserWallActive | bool 262 Motto | String 263 */ 264 pub fn getUserProfile(ulid: Option<String>) -> Result<Payload_GetUserProfile> 265 { 266 let auth = getRetroAchievementsAuth()?; 267 let mut parameters = Self::generateParameterMap(&auth); 268 parameters.remove(Self::Parameter_ApiUsername); 269 parameters.insert("u".into(), match ulid 270 { 271 Some(ulid) => ulid, 272 None => auth.username().clone(), 273 }); 274 275 return Ok(Self::get::<Payload_GetUserProfile>( 276 &Self::Endpoint_GetUserProfile.into(), 277 ¶meters 278 ) 279 .context(format!( 280 "Error retrieving user profile for username {}", 281 auth.username() 282 ))?); 283 } 284 285 pub fn buildMediaUrl(endpoint: &str) -> Option<String> 286 { 287 return Self::buildUrl(Self::MediaUrl, endpoint); 288 } 289 290 /** 291 Generate a default parameter map containing the most commonly used parameters. 292 */ 293 fn buildUrl(base: &str, endpoint: &str) -> Option<String> 294 { 295 return Some( 296 Path::new(base) 297 .join(endpoint) 298 .to_slash()? 299 .into_owned() 300 ); 301 } 302 303 /** 304 Generate a default parameter map containing the most commonly used parameters. 305 */ 306 fn generateParameterMap(auth: &RetroAchievementsAuth) -> HashMap<String, String> 307 { 308 return HashMap::from([ 309 (Self::Parameter_ApiKey.into(), auth.key().clone()), 310 (Self::Parameter_ApiUsername.into(), auth.username().clone()), 311 ]); 312 } 313 314 /** 315 Execute an HTTP GET request. 316 */ 317 fn get<T>(endpoint: &String, parameters: &HashMap<String, String>) -> Result<T> 318 where T: DeserializeOwned 319 { 320 if let Some(url) = Self::buildUrl(Self::BaseUrl, endpoint) 321 { 322 let mut params = String::default(); 323 for (k, v) in parameters 324 { 325 params = format!("{}&{}={}", params, k, v); 326 } 327 328 let requestUrl = format!("{}?{}", url, params.split_off(1)); 329 330 let response = ureq::get(requestUrl) 331 .call() 332 .context("Error retrieving RetroAchievements API response")? 333 .body_mut() 334 .read_json::<T>() 335 .context("Error parsing RetroAchievements API response as JSON")?; 336 337 return Ok(response); 338 } 339 340 return Err(anyhow!(ErrorKind::InvalidInput)); 341 } 342 }