Sync your own workout data from your "Strong" app
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}