Sync your own workout data from your "Strong" app
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 282 lines 9.3 kB view raw
1use crate::models::auth::LoginResponse; 2use crate::models::error::ApiErrorResponse; 3use crate::models::measurement::MeasurementsResponse; 4use crate::models::workout::UserResponse; 5use reqwest::{ 6 Client, Url, 7 header::{HeaderMap, HeaderName, HeaderValue}, 8}; 9use serde_json::json; 10use std::fmt; 11 12#[derive(Debug)] 13pub struct StrongApi { 14 url: Url, 15 headers: HeaderMap, 16 client: Client, 17 pub refresh_token: Option<String>, 18 pub access_token: Option<String>, 19 pub user_id: Option<String>, 20} 21 22#[derive(Debug)] 23pub enum Includes { 24 Log, 25 Measurement, 26 Tag, 27 Widget, 28 Template, 29 Folder, 30 MeasuredValue, 31} 32 33impl fmt::Display for Includes { 34 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 35 let value = match self { 36 Includes::Log => "log", 37 Includes::Measurement => "measurement", 38 Includes::Tag => "tag", 39 Includes::Widget => "widget", 40 Includes::Template => "template", 41 Includes::Folder => "folder", 42 Includes::MeasuredValue => "measuredValue", 43 }; 44 write!(f, "{}", value) 45 } 46} 47 48impl StrongApi { 49 /// Creates a new StrongApi instance with the provided backend URL. 50 pub fn new(url: Url) -> Self { 51 Self { 52 url, 53 headers: Self::default_headers(), 54 client: Client::new(), 55 refresh_token: None, 56 access_token: None, 57 user_id: None, 58 } 59 } 60 61 /// Creates the default headers used for API requests. 62 fn default_headers() -> HeaderMap { 63 let mut headers = HeaderMap::with_capacity(5); 64 headers.insert( 65 HeaderName::from_static("user-agent"), 66 HeaderValue::from_static("Strong Android"), 67 ); 68 headers.insert( 69 HeaderName::from_static("content-type"), 70 HeaderValue::from_static("application/json"), 71 ); 72 headers.insert( 73 HeaderName::from_static("accept"), 74 HeaderValue::from_static("application/json"), 75 ); 76 headers.insert( 77 HeaderName::from_static("x-client-build"), 78 HeaderValue::from_static("600013"), 79 ); 80 headers.insert( 81 HeaderName::from_static("x-client-platform"), 82 HeaderValue::from_static("android"), 83 ); 84 headers 85 } 86 87 /// Logs in to the Strong backend using the provided username/e-mail and password. 88 pub async fn login( 89 &mut self, 90 username: &str, 91 password: &str, 92 ) -> Result<(), Box<dyn std::error::Error>> { 93 let url = self 94 .url 95 .join("auth/login") 96 .expect("joining auth/login has failed, check the base URL"); 97 let body = json!({ 98 "usernameOrEmail": username, 99 "password": password 100 }); 101 102 let response = self 103 .client 104 .post(url) 105 .headers(self.headers.clone()) 106 .json(&body) 107 .send() 108 .await?; 109 let response_text = response.text().await.expect("failed to read response body"); 110 111 let parsed: LoginResponse = serde_json::from_str(&response_text)?; 112 113 self.access_token = parsed.access_token; 114 self.refresh_token = parsed.refresh_token; 115 self.user_id = parsed.user_id; 116 117 Ok(()) 118 } 119 120 /// Refreshes the access token using tokens obtained during login. 121 pub async fn refresh(&mut self) -> Result<(), Box<dyn std::error::Error>> { 122 let url = self 123 .url 124 .join("auth/login/refresh") 125 .expect("joining auth/login/refresh has failed, check the base URL"); 126 let body = json!({ 127 "accessToken": self.access_token, 128 "refreshToken": self.refresh_token 129 }); 130 131 // Ensure the access token exists 132 let access_token = self.access_token.clone().ok_or("Missing access token")?; 133 let response = self 134 .client 135 .post(url) 136 .bearer_auth(&access_token) 137 .headers(self.headers.clone()) 138 .json(&body) 139 .send() 140 .await?; 141 142 let response_text = response.text().await.expect("failed to read response body"); 143 let parsed: LoginResponse = serde_json::from_str(&response_text)?; 144 145 self.access_token = parsed.access_token; 146 self.refresh_token = parsed.refresh_token; 147 148 Ok(()) 149 } 150 151 /// Refreshes the access token using tokens passed as parameters. 152 #[cfg(feature = "full")] 153 pub async fn refresh_by_tokens( 154 &mut self, 155 access_token: String, 156 refresh_token: String, 157 ) -> Result<(), Box<dyn std::error::Error>> { 158 let url = self 159 .url 160 .join("auth/login/refresh") 161 .expect("joining auth/login/refresh has failed, check the base URL"); 162 let body = json!({ 163 "accessToken": access_token.clone(), 164 "refreshToken": refresh_token, 165 }); 166 167 let response = self 168 .client 169 .post(url) 170 .bearer_auth(&access_token) 171 .headers(self.headers.clone()) 172 .json(&body) 173 .send() 174 .await?; 175 let response_text = response.text().await.expect("failed to read response body"); 176 let parsed: LoginResponse = serde_json::from_str(&response_text)?; 177 178 self.access_token = parsed.access_token; 179 self.refresh_token = parsed.refresh_token; 180 181 Ok(()) 182 } 183 184 /// Gets the user data for the currently logged-in user. 185 /// The `continuation` parameter is used to paginate the results. 186 /// The `limit` parameter specifies the number of results to return. 187 /// The `includes` parameter specifies which related entities to include in the response. See the `Includes` enum for possible values. 188 pub async fn get_user( 189 &self, 190 continuation: &str, 191 limit: i16, 192 includes: Vec<Includes>, 193 ) -> Result<UserResponse, Box<dyn std::error::Error>> { 194 let user_id = self 195 .user_id 196 .as_ref() 197 .ok_or("Missing user id. Use `login` before calling `get_user`")?; 198 let mut url = self 199 .url 200 .join(&format!("api/users/{user_id}")) 201 .expect("joining api/users/{user_id} has failed, check the base URL"); 202 203 { 204 // Use query_pairs_mut to build the query string. 205 let mut query_pairs = url.query_pairs_mut(); 206 query_pairs.append_pair("limit", &limit.to_string()); 207 query_pairs.append_pair("continuation", continuation); 208 for include in includes { 209 query_pairs.append_pair("include", &include.to_string()); 210 } 211 } 212 213 let response = self 214 .client 215 .get(url) 216 .bearer_auth(self.access_token.as_ref().ok_or("Missing access token")?) 217 .headers(self.headers.clone()) 218 .send() 219 .await?; 220 221 // Capture the status before consuming the response. 222 let status = response.status(); 223 let response_text = response.text().await.expect("failed to read response body"); 224 225 if !status.is_success() { 226 let api_error: ApiErrorResponse = serde_json::from_str(&response_text)?; 227 return Err(Box::new(api_error)); 228 } 229 230 let parsed: UserResponse = serde_json::from_str(&response_text)?; 231 Ok(parsed) 232 } 233 234 /// Measurements are exercises that are available in the Strong app. 235 /// This function retrieves a list of measurements. 236 /// The `page` parameter is used to paginate the results. 237 /// Check the measurements.total and the length of measurements.embedded.measurements to determine if there are more pages. 238 pub async fn get_measurements( 239 &self, 240 page: i8, 241 ) -> Result<MeasurementsResponse, Box<dyn std::error::Error>> { 242 let mut url = self 243 .url 244 .join("api/measurements") 245 .expect("joining api/measurements has failed, check the base URL"); 246 247 { 248 let mut query_pairs = url.query_pairs_mut(); 249 query_pairs.append_pair("page", &page.to_string()); 250 } 251 252 let response = self 253 .client 254 .get(url) 255 .headers(Self::default_headers()) 256 .send() 257 .await?; 258 let response_text = response.text().await.expect("failed to read response body"); 259 260 let response: MeasurementsResponse = serde_json::from_str(&response_text)?; 261 262 Ok(response) 263 } 264 265 pub async fn get_logs_raw(&self) -> Result<String, Box<dyn std::error::Error>> { 266 let user_id = self.user_id.as_ref().ok_or("Missing user id")?; 267 let url = self 268 .url 269 .join(&format!("api/logs/{user_id}")) 270 .expect("joining api/logs/{user_id} has failed, check the base URL"); 271 let response = self 272 .client 273 .get(url) 274 .bearer_auth(self.access_token.as_ref().ok_or("Missing access token")?) 275 .headers(self.headers.clone()) 276 .send() 277 .await?; 278 let response_text = response.text().await.expect("failed to read response body"); 279 280 Ok(response_text) 281 } 282}