diff --git a/Cargo.toml b/Cargo.toml index 85124b88..a9f3a81e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garmin_rust" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/fitbit_bot/Cargo.toml b/fitbit_bot/Cargo.toml index c7410534..a7577071 100644 --- a/fitbit_bot/Cargo.toml +++ b/fitbit_bot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fitbit_bot" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/fitbit_lib/Cargo.toml b/fitbit_lib/Cargo.toml index e50c75a8..72c2fd6c 100644 --- a/fitbit_lib/Cargo.toml +++ b/fitbit_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fitbit_lib" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/fitbit_lib/src/fitbit_client.rs b/fitbit_lib/src/fitbit_client.rs deleted file mode 100644 index 75030587..00000000 --- a/fitbit_lib/src/fitbit_client.rs +++ /dev/null @@ -1,1290 +0,0 @@ -use anyhow::{format_err, Error}; -use base64::{engine::general_purpose::STANDARD, Engine}; -use crossbeam_utils::atomic::AtomicCell; -use futures::{future, future::try_join_all, stream::FuturesUnordered, TryStreamExt}; -use itertools::Itertools; -use log::debug; -use maplit::hashmap; -use once_cell::sync::Lazy; -use reqwest::{header::HeaderMap, Client, Response, Url}; -use serde::{Deserialize, Serialize}; -use stack_string::{format_sstr, StackString}; -use std::{ - collections::{BTreeSet, HashMap, HashSet}, - path::PathBuf, -}; -use time::{ - format_description::well_known::Rfc3339, macros::format_description, Date, Duration, - OffsetDateTime, UtcOffset, -}; -use time_tz::{timezones::db::UTC, OffsetDateTimeExt}; -use tokio::{ - fs::File, - io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - task::spawn_blocking, - time::sleep, -}; - -use garmin_lib::{date_time_wrapper::DateTimeWrapper, garmin_config::GarminConfig}; -use garmin_models::{ - fitbit_activity::FitbitActivity, - garmin_summary::{get_list_of_activities_from_db, GarminSummary}, -}; -use garmin_utils::{garmin_util::get_random_string, pgpool::PgPool}; - -use crate::{ - fitbit_heartrate::{FitbitBodyWeightFat, FitbitHeartRate}, - scale_measurement::ScaleMeasurement, - GarminConnectHrData, -}; - -static CSRF_TOKEN: Lazy>> = Lazy::new(|| AtomicCell::new(None)); - -#[derive(Default, Debug, Clone)] -pub struct FitbitClient { - pub config: GarminConfig, - pub user_id: StackString, - pub access_token: StackString, - pub refresh_token: StackString, - pub client: Client, - pub offset: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -struct AccessTokenResponse { - access_token: StackString, - token_type: StackString, - expires_in: u64, - refresh_token: StackString, - user_id: StackString, -} - -impl FitbitClient { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// # Errors - /// Returns error if auth fails - pub async fn with_auth(config: GarminConfig) -> Result { - let mut client = Self::from_file(config).await?; - if let Ok(offset) = client.get_client_offset().await { - client.offset = Some(offset); - } else { - let body = client.refresh_fitbit_access_token().await?; - debug!("{}", body); - client.to_file().await?; - client.offset = Some(client.get_client_offset().await?); - } - Ok(client) - } - - /// # Errors - /// Returns error if reading auth from file fails - pub async fn from_file(config: GarminConfig) -> Result { - let mut client = Self { - config, - client: Client::builder().cookie_store(true).build()?, - ..Self::default() - }; - let filename = &client.config.fitbit_tokenfile; - if !filename.exists() { - return Err(format_err!("file {filename:?} does not exist")); - } - let f = File::open(filename).await?; - let mut b = BufReader::new(f); - let mut line = String::new(); - loop { - line.clear(); - if b.read_line(&mut line).await? == 0 { - break; - } - let mut items = line.split('='); - if let Some(key) = items.next() { - if let Some(val) = items.next() { - match key { - "user_id" => client.user_id = val.trim().into(), - "access_token" => client.access_token = val.trim().into(), - "refresh_token" => client.refresh_token = val.trim().into(), - _ => {} - } - } - } - } - Ok(client) - } - - /// # Errors - /// Returns error if storing to file fails - pub async fn to_file(&self) -> Result<(), Error> { - let mut f = tokio::fs::File::create(&self.config.fitbit_tokenfile).await?; - f.write_all(format_sstr!("user_id={}\n", self.user_id).as_bytes()) - .await?; - f.write_all(format_sstr!("access_token={}\n", self.access_token).as_bytes()) - .await?; - f.write_all(format_sstr!("refresh_token={}\n", self.refresh_token).as_bytes()) - .await?; - Ok(()) - } - - #[must_use] - pub fn get_offset(&self) -> UtcOffset { - self.offset.unwrap_or(UtcOffset::UTC) - } - - async fn get_url(&self, url: Url, headers: HeaderMap) -> Result { - let resp = self - .client - .get(url.clone()) - .headers(headers.clone()) - .send() - .await?; - if resp.status() == 429 { - if let Some(retry_after) = resp.headers().get("retry-after") { - let retry_seconds: u64 = retry_after.to_str()?.parse()?; - if retry_seconds < 60 { - sleep(std::time::Duration::from_secs(retry_seconds)).await; - let headers = self.get_auth_headers()?; - return self - .client - .get(url) - .headers(headers) - .send() - .await - .map_err(Into::into); - } - println!("Wait at least {retry_seconds} seconds before retrying"); - return Err(format_err!("{}", resp.text().await?)); - } - } - Ok(resp) - } - - /// # Errors - /// Returns error if api call fails - pub async fn get_user_profile(&self) -> Result { - #[derive(Deserialize)] - struct UserResp { - user: FitbitUserProfile, - } - - let headers = self.get_auth_headers()?; - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("1/user/-/")? - .join("profile.json")?; - let resp: UserResp = self - .get_url(url, headers) - .await? - .error_for_status()? - .json() - .await?; - Ok(resp.user) - } - - async fn get_client_offset(&self) -> Result { - let profile = self.get_user_profile().await?; - let offset = (profile.offset_from_utc_millis / 1000) as i32; - let offset = UtcOffset::from_whole_seconds(offset)?; - Ok(offset) - } - - /// # Errors - /// Returns error if api call fails - pub fn get_fitbit_auth_url(&self) -> Result { - let redirect_uri = format_sstr!("https://{}/garmin/fitbit/callback", self.config.domain); - let scopes = &[ - "activity", - "nutrition", - "heartrate", - "location", - "profile", - "settings", - "sleep", - "social", - "weight", - ]; - let state = get_random_string(); - let fitbit_oauth_authorize = self - .config - .fitbit_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("oauth2/authorize")?; - let url = Url::parse_with_params( - fitbit_oauth_authorize.as_str(), - &[ - ("response_type", "code"), - ("client_id", self.config.fitbit_clientid.as_str()), - ("redirect_url", redirect_uri.as_str()), - ("scope", scopes.join(" ").as_str()), - ("state", state.as_str()), - ], - )?; - CSRF_TOKEN.store(Some(state)); - Ok(url) - } - - fn get_basic_headers(&self) -> Result { - let mut headers = HeaderMap::new(); - headers.insert("Content-type", "application/x-www-form-urlencoded".parse()?); - headers.insert( - "Authorization", - format_sstr!( - "Basic {}", - STANDARD.encode(format_sstr!( - "{}:{}", - self.config.fitbit_clientid, - self.config.fitbit_clientsecret - )) - ) - .parse()?, - ); - Ok(headers) - } - - fn get_auth_headers(&self) -> Result { - let mut headers = HeaderMap::new(); - headers.insert( - "Authorization", - format_sstr!("Bearer {}", self.access_token,).parse()?, - ); - headers.insert("Accept-Language", "en_US".parse()?); - headers.insert("Accept-Locale", "en_US".parse()?); - Ok(headers) - } - - /// # Errors - /// Returns error if api call fails - pub async fn refresh_fitbit_access_token(&mut self) -> Result { - let headers = self.get_basic_headers()?; - let data = hashmap! { - "grant_type" => "refresh_token", - "refresh_token" => self.refresh_token.as_str(), - }; - let fitbit_oauth_token = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("oauth2/token")?; - let auth_resp: AccessTokenResponse = self - .client - .post(fitbit_oauth_token) - .headers(headers) - .form(&data) - .send() - .await? - .error_for_status()? - .json() - .await?; - self.user_id = auth_resp.user_id; - self.access_token = auth_resp.access_token; - self.refresh_token = auth_resp.refresh_token; - let success = r#" -

You are now authorized to access the Fitbit API!

-

You can close this window

- - "# - .into(); - Ok(success) - } - - /// # Errors - /// Returns error if api call fails - pub async fn get_fitbit_access_token( - &mut self, - code: &str, - state: &str, - ) -> Result { - if let Some(current_state) = CSRF_TOKEN.swap(None) { - if state != current_state.as_str() { - return Err(format_err!("Incorrect state")); - } - let headers = self.get_basic_headers()?; - let redirect_uri = - format_sstr!("https://{}/garmin/fitbit/callback", self.config.domain); - let data = hashmap! { - "code" => code, - "grant_type" => "authorization_code", - "client_id" => self.config.fitbit_clientid.as_str(), - "redirect_uri" => redirect_uri.as_str(), - "state" => state, - }; - let fitbit_oauth_token = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("oauth2/token")?; - let auth_resp: AccessTokenResponse = self - .client - .post(fitbit_oauth_token) - .headers(headers) - .form(&data) - .send() - .await? - .error_for_status()? - .json() - .await?; - self.user_id = auth_resp.user_id; - self.access_token = auth_resp.access_token; - self.refresh_token = auth_resp.refresh_token; - let success = r#" -

You are now authorized to access the Fitbit API!

-

You can close this window

- - "# - .into(); - Ok(success) - } else { - Err(format_err!("No state")) - } - } - - /// # Errors - /// Returns error if api call fails - pub async fn get_fitbit_intraday_time_series_heartrate( - &self, - date: Date, - ) -> Result, Error> { - #[derive(Deserialize)] - struct HeartRateResp { - #[serde(rename = "activities-heart-intraday")] - intraday: HrDs, - } - #[derive(Deserialize)] - struct HrDs { - dataset: Vec, - } - #[derive(Deserialize)] - struct HrDataSet { - time: StackString, - value: i32, - } - - let headers = self.get_auth_headers()?; - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("1/user/-/")? - .join(&format_sstr!("activities/heart/date/{date}/1d/1min.json"))?; - let dataset: HeartRateResp = self - .client - .get(url) - .headers(headers) - .send() - .await? - .error_for_status()? - .json() - .await?; - let offset = self.get_offset(); - let mut result = dataset - .intraday - .dataset - .into_iter() - .map(|entry| { - let (h, m, _) = offset.as_hms(); - let offset = format_sstr!( - "{s}{h:02}:{m:02}", - s = if offset.is_negative() { '-' } else { '+' }, - h = h.abs(), - m = m.abs() - ); - let datetime = format_sstr!("{date}T{t}{offset}", t = entry.time); - let datetime = OffsetDateTime::parse(&datetime, &Rfc3339)? - .to_timezone(UTC) - .into(); - let value = entry.value; - Ok(FitbitHeartRate { datetime, value }) - }) - .collect::, Error>>()?; - result.shrink_to_fit(); - Ok(result) - } - - /// # Errors - /// Returns error if api call fails - pub async fn import_fitbit_heartrate(&self, date: Date) -> Result, Error> { - let heartrates = self.get_fitbit_intraday_time_series_heartrate(date).await?; - let config = self.config.clone(); - spawn_blocking(move || { - FitbitHeartRate::merge_slice_to_avro(&config, &heartrates)?; - Ok(heartrates) - }) - .await? - } - - /// # Errors - /// Returns error if api call fails - pub async fn import_garmin_connect_heartrate( - config: GarminConfig, - heartrate_data: &GarminConnectHrData, - ) -> Result, Error> { - let heartrates = FitbitHeartRate::from_garmin_connect_hr(heartrate_data); - spawn_blocking(move || FitbitHeartRate::merge_slice_to_avro(&config, &heartrates)).await? - } - - /// # Errors - /// Returns error if api call fails - pub async fn get_fitbit_bodyweightfat(&self) -> Result, Error> { - #[derive(Deserialize)] - struct BodyWeight { - weight: Vec, - } - #[derive(Deserialize)] - struct WeightEntry { - date: Date, - fat: Option, - time: StackString, - weight: f64, - } - let headers = self.get_auth_headers()?; - let offset = self.get_offset(); - let date = OffsetDateTime::now_utc().to_offset(offset).date(); - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("1/user/-/")? - .join(&format_sstr!("body/log/weight/date/{date}/30d.json"))?; - let body_weight: BodyWeight = self - .client - .get(url) - .headers(headers.clone()) - .send() - .await? - .error_for_status()? - .json() - .await?; - let mut result: Vec<_> = body_weight - .weight - .into_iter() - .filter_map(|bw| { - let (h, m, _) = offset.as_hms(); - let offset = format_sstr!( - "{s}{h:02}:{m:02}", - s = if offset.is_negative() { '-' } else { '+' }, - h = h.abs(), - m = m.abs() - ); - let datetime = format_sstr!("{d}T{t}{offset}", d = bw.date, t = bw.time); - let datetime = OffsetDateTime::parse(&datetime, &Rfc3339) - .ok()? - .to_timezone(UTC) - .into(); - let weight = bw.weight; - let fat = bw.fat?; - Some(FitbitBodyWeightFat { - datetime, - weight, - fat, - }) - }) - .collect(); - result.shrink_to_fit(); - Ok(result) - } - - /// # Errors - /// Returns error if api call fails - #[allow(clippy::similar_names)] - pub async fn update_fitbit_bodyweightfat<'a>( - &self, - updates: impl IntoIterator, - ) -> Result<(), Error> { - let headers = self.get_auth_headers()?; - let offset = self.get_offset(); - let futures: FuturesUnordered<_> = updates - .into_iter() - .map(|update| { - let headers = headers.clone(); - async move { - let datetime = update.datetime.to_offset(offset); - let date = datetime.date(); - let date_str = date - .format(format_description!("[year]-[month]-[day]")) - .unwrap_or_else(|_| String::new()) - .into(); - let time_str = date - .format(format_description!("[hour]:[minute]:[second]")) - .unwrap_or_else(|_| String::new()) - .into(); - let weight_str = StackString::from_display(update.mass); - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("1/user/-/")? - .join("body/log/weight.json")?; - let data = hashmap! { - "weight" => &weight_str, - "date" => &date_str, - "time" => &time_str, - }; - self.client - .post(url) - .form(&data) - .headers(headers.clone()) - .send() - .await? - .error_for_status()?; - - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("1/user/-/")? - .join("body/log/fat.json")?; - let fat_pct_str = StackString::from_display(update.fat_pct); - let data = hashmap! { - "fat" => &fat_pct_str, - "date" => &date_str, - "time" => &time_str, - }; - self.client - .post(url) - .form(&data) - .headers(headers) - .send() - .await? - .error_for_status()?; - Ok(()) - } - }) - .collect(); - futures.try_collect().await - } - - /// # Errors - /// Returns error if api call fails - pub async fn get_activities( - &self, - start_date: Date, - offset: Option, - ) -> Result, Error> { - #[derive(Deserialize)] - struct AcivityListResp { - activities: Vec, - } - - let offset = offset.unwrap_or(0); - - let headers = self.get_auth_headers()?; - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("1/user/-/")? - .join(&format_sstr!( - "activities/list.json?afterDate={start_date}&offset={offset}&limit=20&sort=asc" - ))?; - let activities: AcivityListResp = self - .get_url(url, headers) - .await? - .error_for_status()? - .json() - .await?; - Ok(activities.activities) - } - - /// # Errors - /// Returns error if api call fails - pub async fn get_all_activities(&self, start_date: Date) -> Result, Error> { - let mut activities = Vec::new(); - loop { - let new_activities: Vec<_> = self - .get_activities(start_date, Some(activities.len())) - .await?; - if new_activities.is_empty() { - break; - } - activities.extend_from_slice(&new_activities); - } - Ok(activities) - } - - /// # Errors - /// Returns error if api call fails - pub async fn get_tcx_urls( - &self, - start_date: Date, - ) -> Result, Error> { - let activities = self.get_activities(start_date, None).await?; - - let mut activities = activities - .into_iter() - .filter_map(|entry| { - let res = || { - if entry.log_type != "tracker" { - return Ok(None); - } - let start_time = entry.start_time; - entry - .tcx_link - .map_or(Ok(None), |link| Ok(Some((start_time, link)))) - }; - res().transpose() - }) - .collect::, Error>>()?; - activities.shrink_to_fit(); - Ok(activities) - } - - /// # Errors - /// Returns error if api call fails - pub async fn download_tcx(&self, tcx_url: &str) -> Result { - let headers = self.get_auth_headers()?; - self.client - .get(tcx_url) - .headers(headers) - .send() - .await? - .error_for_status()? - .bytes() - .await - .map_err(Into::into) - } - - /// # Errors - /// Returns error if api call fails - pub async fn get_fitbit_activity_types( - &self, - ) -> Result, Error> { - #[derive(Deserialize)] - struct FitbitActivityType { - name: StackString, - id: u64, - } - #[derive(Deserialize)] - struct FitbitSubCategory { - activities: Vec, - id: u64, - name: StackString, - } - #[derive(Deserialize)] - struct FitbitCategory { - activities: Vec, - #[serde(rename = "subCategories")] - sub_categories: Option>, - id: u64, - name: StackString, - } - #[derive(Deserialize)] - struct FitbitActivityCategories { - categories: Vec, - } - - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("1/activities.json")?; - let headers = self.get_auth_headers()?; - let categories: FitbitActivityCategories = self - .client - .get(url) - .headers(headers) - .send() - .await? - .error_for_status()? - .json() - .await?; - let mut id_map: HashMap = HashMap::new(); - for category in &categories.categories { - let id_str = StackString::from_display(category.id); - id_map.insert(id_str, category.name.clone()); - for activity in &category.activities { - let name = format_sstr!("{}/{}", category.name, activity.name); - let id_str = StackString::from_display(activity.id); - id_map.insert(id_str, name); - } - if let Some(sub_categories) = category.sub_categories.as_ref() { - for sub_category in sub_categories { - let name = format_sstr!("{}/{}", category.name, sub_category.name); - let id_str = StackString::from_display(sub_category.id); - id_map.insert(id_str, name); - for sub_activity in &sub_category.activities { - let name = format_sstr!( - "{}/{}/{}", - category.name, - sub_category.name, - sub_activity.name - ); - let id_str = StackString::from_display(sub_activity.id); - id_map.insert(id_str, name); - } - } - } - } - Ok(id_map) - } - - async fn log_fitbit_activity(&self, entry: &ActivityLoggingEntry) -> Result<(u64, u64), Error> { - #[derive(Deserialize)] - struct ActivityLogEntry { - #[serde(rename = "activityId")] - activity_id: u64, - steps: u64, - } - - #[derive(Deserialize)] - struct ActivityLogResp { - #[serde(rename = "activityLog")] - activity_log: ActivityLogEntry, - } - - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .unwrap() - .join("1/user/-/")? - .join("activities.json")?; - let headers = self.get_auth_headers()?; - let resp: ActivityLogResp = self - .client - .post(url) - .headers(headers) - .form(entry) - .send() - .await? - .error_for_status()? - .json() - .await?; - Ok((resp.activity_log.activity_id, resp.activity_log.steps)) - } - - /// # Errors - /// Returns error if api call fails - pub async fn remove_duplicate_entries(&self, pool: &PgPool) -> Result, Error> { - let mut last_entry = None; - let futures = FitbitActivity::read_from_db(pool, None, None, None, None) - .await? - .into_iter() - .map(|activity| { - let start_time_str = activity - .start_time - .format(format_description!("[year]-[month]-[day]T[hour]:[minute]")) - .unwrap_or_else(|_| String::new()); - (start_time_str, activity) - }) - .sorted_by(|x, y| x.0.cmp(&y.0)) - .filter_map(|(k, v)| { - let mut keep = false; - if let Some(k_) = last_entry.take() { - if k_ == k { - keep = true; - } - } - last_entry.replace(k); - if keep { - Some(v.log_id) - } else { - None - } - }) - .map(|log_id| async move { - if self.delete_fitbit_activity(log_id as u64).await.is_err() { - debug!("Failed to delete fitbit activity {}", log_id); - } - if let Some(activity) = FitbitActivity::get_by_id(pool, log_id).await? { - activity.delete_from_db(pool).await?; - Ok(format_sstr!("fully deleted {log_id}")) - } else { - Ok(format_sstr!("not fully deleted {log_id}")) - } - }); - let result: Result, Error> = try_join_all(futures).await; - let mut result = result?; - result.shrink_to_fit(); - Ok(result) - } - - /// # Errors - /// Returns error if api call fails - #[allow(clippy::manual_filter_map)] - pub async fn sync_fitbit_activities( - &self, - begin_datetime: OffsetDateTime, - pool: &PgPool, - ) -> Result, Error> { - let offset = self.get_offset(); - let date = begin_datetime.to_offset(offset).date(); - - // Get all activities - let new_activities: Vec<_> = self.get_all_activities(date).await?; - - // Get id's for walking and running activities with 0 steps - let mut activities_to_delete: HashSet<_> = new_activities - .iter() - .filter_map(|activity| { - if (activity.steps.unwrap_or(0) == 0) - && (activity.activity_type_id == Some(90009) - || activity.activity_type_id == Some(90013)) - { - Some(activity.log_id) - } else { - None - } - }) - .collect(); - activities_to_delete.shrink_to_fit(); - - // delete 0 step activities from fitbit and DB - let futures = activities_to_delete.iter().map(|log_id| { - let pool = pool.clone(); - async move { - if self.delete_fitbit_activity(*log_id as u64).await.is_err() { - debug!("Failed to delete fitbit activity {}", log_id); - } - if let Some(activity) = FitbitActivity::get_by_id(&pool, *log_id).await? { - activity.delete_from_db(&pool).await?; - } - Ok(()) - } - }); - let results: Result, Error> = try_join_all(futures).await; - results?; - - let mut new_activities: HashMap<_, _> = new_activities - .into_iter() - .filter_map(|activity| { - if activities_to_delete.contains(&activity.log_id) { - None - } else { - let start_time_str = activity - .start_time - .format(format_description!("[year]-[month]-[day]T[hour]:[minute]")) - .unwrap_or_else(|_| String::new()); - Some((start_time_str, activity)) - } - }) - .collect(); - new_activities.shrink_to_fit(); - - // Get existing activities - let mut existing_activities: HashMap<_, _> = - FitbitActivity::read_from_db(pool, Some(date), None, None, None) - .await? - .into_iter() - .map(|activity| (activity.log_id, activity)) - .collect(); - existing_activities.shrink_to_fit(); - - let futures: FuturesUnordered<_> = new_activities - .values() - .filter(|activity| !existing_activities.contains_key(&activity.log_id)) - .map(|activity| { - let pool = pool.clone(); - async move { - activity.insert_into_db(&pool).await?; - Ok(()) - } - }) - .collect(); - let results: Result<(), Error> = futures.try_collect().await; - results?; - - let mut old_activities: Vec<_> = get_list_of_activities_from_db( - &format_sstr!("begin_datetime >= '{begin_datetime}'"), - pool, - ) - .await? - .try_filter(|(d, _)| { - future::ready({ - let key = d - .format(format_description!("[year]-[month]-[day]T[hour]:[minute]")) - .unwrap_or_else(|_| String::new()); - !new_activities.contains_key(&key) - }) - }) - .try_collect() - .await?; - old_activities.shrink_to_fit(); - - let futures: FuturesUnordered<_> = old_activities - .into_iter() - .map(|(d, f)| { - let pool = pool.clone(); - async move { - if let Some(activity) = - GarminSummary::read_summary_from_postgres(&pool, &f).await? - { - if let Some(activity) = - ActivityLoggingEntry::from_summary(&activity, offset) - { - self.log_fitbit_activity(&activity).await?; - return Ok(Some(d)); - } - } - Ok(None) - } - }) - .collect(); - let result: Result, Error> = futures - .try_filter_map(|x| async move { Ok(x) }) - .try_collect() - .await; - let mut result = result?; - result.shrink_to_fit(); - Ok(result) - } - - /// # Errors - /// Returns error if api call fails - pub async fn delete_fitbit_activity(&self, log_id: u64) -> Result<(), Error> { - let url = self - .config - .fitbit_api_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join("1/user/-/")? - .join(&format_sstr!("activities/{log_id}.json"))?; - let headers = self.get_auth_headers()?; - self.client - .delete(url) - .headers(headers) - .send() - .await? - .error_for_status()?; - Ok(()) - } - - /// # Errors - /// Returns error if api call fails - pub async fn sync_everything( - &self, - pool: &PgPool, - ) -> Result { - let offset = self.get_offset(); - let start_datetime = OffsetDateTime::now_utc() - Duration::days(30); - let start_date: Date = start_datetime.to_offset(offset).date(); - let local = DateTimeWrapper::local_tz(); - - let mut existing_map: HashMap<_, _> = self - .get_fitbit_bodyweightfat() - .await? - .into_iter() - .map(|entry| { - let date = entry.datetime.to_timezone(local).date(); - (date, entry) - }) - .collect(); - existing_map.shrink_to_fit(); - - let mut measurements: Vec<_> = - ScaleMeasurement::read_from_db(pool, Some(start_date), None, None, None) - .await? - .into_iter() - .filter(|entry| { - let date = entry.datetime.to_timezone(local).date(); - !existing_map.contains_key(&date) - }) - .collect(); - measurements.shrink_to_fit(); - self.update_fitbit_bodyweightfat(&measurements).await?; - - Ok(FitbitBodyWeightFatUpdateOutput { measurements }) - } - - /// # Errors - /// Returns error if api call fails - #[allow(clippy::manual_filter_map)] - pub async fn sync_tcx(&self, start_date: Date) -> Result, Error> { - let futures = self - .get_tcx_urls(start_date) - .await? - .into_iter() - .filter_map(|(start_time, tcx_url)| { - let fname = self - .config - .gps_dir - .join( - start_time - .format(format_description!( - "[year]-[month]-[day]_[hour]-[minute]-[second]_1_1" - )) - .unwrap_or_else(|_| String::new()), - ) - .with_extension("tcx"); - if fname.exists() { - None - } else { - Some((fname, tcx_url)) - } - }) - .map(|(fname, tcx_url)| async move { - let data = self.download_tcx(&tcx_url).await?; - tokio::fs::write(&fname, &data).await?; - Ok(fname) - }); - try_join_all(futures).await - } -} - -#[derive(Debug, Serialize)] -pub struct FitbitBodyWeightFatUpdateOutput { - pub measurements: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -struct ActivityLoggingEntry { - #[serde(rename = "activityId")] - activity_id: Option, - #[serde(rename = "startTime")] - start_time: StackString, - #[serde(rename = "durationMillis")] - duration_millis: u64, - date: Date, - distance: Option, - #[serde(rename = "distanceUnit")] - distance_unit: Option, -} - -impl ActivityLoggingEntry { - fn from_summary(item: &GarminSummary, offset: UtcOffset) -> Option { - let start_time_str = item - .begin_datetime - .to_offset(offset) - .format(format_description!("[hour]:[minute]")) - .ok()? - .into(); - item.sport.to_fitbit_activity_id().map(|activity_id| Self { - activity_id: Some(activity_id), - start_time: start_time_str, - duration_millis: (item.total_duration * 1000.0) as u64, - date: item.begin_datetime.to_offset(offset).date(), - distance: Some(item.total_distance / 1000.0), - distance_unit: Some("Kilometer".into()), - }) - } -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub struct FitbitUserProfile { - #[serde(alias = "averageDailySteps")] - pub average_daily_steps: u64, - pub country: StackString, - #[serde(alias = "dateOfBirth")] - pub date_of_birth: StackString, - #[serde(alias = "displayName")] - pub display_name: StackString, - #[serde(alias = "distanceUnit")] - pub distance_unit: StackString, - #[serde(alias = "encodedId")] - pub encoded_id: StackString, - #[serde(alias = "firstName")] - pub first_name: StackString, - #[serde(alias = "lastName")] - pub last_name: StackString, - #[serde(alias = "fullName")] - pub full_name: StackString, - pub gender: StackString, - pub height: f64, - #[serde(alias = "heightUnit")] - pub height_unit: StackString, - pub timezone: StackString, - #[serde(alias = "offsetFromUTCMillis")] - pub offset_from_utc_millis: i64, - #[serde(alias = "strideLengthRunning")] - pub stride_length_running: f64, - #[serde(alias = "strideLengthWalking")] - pub stride_length_walking: f64, - pub weight: f64, - #[serde(alias = "weightUnit")] - pub weight_unit: StackString, -} - -#[cfg(test)] -mod tests { - use crate::fitbit_client::{FitbitActivity, FitbitClient}; - use anyhow::Error; - use futures::future::try_join_all; - use log::debug; - use std::collections::HashMap; - use tempfile::NamedTempFile; - use time::{macros::format_description, Duration, OffsetDateTime}; - use time_tz::OffsetDateTimeExt; - - use garmin_lib::{date_time_wrapper::DateTimeWrapper, garmin_config::GarminConfig}; - use garmin_utils::pgpool::PgPool; - - #[tokio::test] - #[ignore] - async fn test_fitbit_client_from_file() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let client = FitbitClient::from_file(config).await?; - let url = client.get_fitbit_auth_url()?; - debug!("{:?} {}", client, url); - assert!(url.as_str().len() > 0); - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_get_tcx_urls() -> Result<(), Error> { - let local = DateTimeWrapper::local_tz(); - let config = GarminConfig::get_config(None)?; - let client = FitbitClient::with_auth(config.clone()).await?; - let start_date = (OffsetDateTime::now_utc() - Duration::days(10)).date(); - let results = client.get_tcx_urls(start_date).await?; - debug!("{:?}", results); - for (start_time, tcx_url) in results { - let fname = config - .gps_dir - .join( - start_time - .to_timezone(local) - .format(format_description!( - "[year]-[month]-[day]_[hour]-[minute]-[second]_1_1" - )) - .unwrap(), - ) - .with_extension("tcx"); - if fname.exists() { - debug!("{:?} exists", fname); - } else { - debug!("{:?} does not exist", fname); - } - - { - use std::io::Write; - let mut f = NamedTempFile::new()?; - let data = client.download_tcx(&tcx_url).await?; - f.write_all(&data)?; - - let metadata = f.as_file().metadata()?; - debug!("{start_time} {metadata:?} {l}", l = metadata.len()); - assert!(metadata.len() > 0); - } - break; - } - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_get_client_offset() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let client = FitbitClient::with_auth(config.clone()).await?; - let offset = client.offset.unwrap(); - assert!(offset.whole_seconds() == -5 * 3600 || offset.whole_seconds() == -4 * 3600); - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_get_fitbit_bodyweightfat() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let client = FitbitClient::with_auth(config.clone()).await?; - let bodyweight = client.get_fitbit_bodyweightfat().await?; - debug!("{:#?}", bodyweight); - assert!(bodyweight.len() > 10); - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_get_all_activities() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let client = FitbitClient::with_auth(config.clone()).await?; - - let offset = client.get_offset(); - let begin_datetime = (OffsetDateTime::now_utc() - Duration::days(7)).to_offset(offset); - - let date = begin_datetime.date(); - let new_activities = client.get_all_activities(date).await?; - debug!("{:#?}", new_activities); - assert!(new_activities.len() > 0); - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_sync_fitbit_activities() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let client = FitbitClient::with_auth(config.clone()).await?; - - let begin_datetime = OffsetDateTime::now_utc() - Duration::days(30); - - let pool = PgPool::new(&config.pgurl)?; - let dates = client.sync_fitbit_activities(begin_datetime, &pool).await?; - debug!("{:?}", dates); - assert_eq!(dates.len(), 0); - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_get_user_profile() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let client = FitbitClient::with_auth(config.clone()).await?; - let resp = client.get_user_profile().await?; - assert_eq!(resp.country.as_str(), "US"); - assert_eq!(resp.display_name.as_str(), "Daniel B."); - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_dump_fitbit_activities() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let client = FitbitClient::with_auth(config.clone()).await?; - let pool = PgPool::new(&config.pgurl)?; - let mut activities: HashMap<_, _> = - FitbitActivity::read_from_db(&pool, None, None, None, None) - .await? - .into_iter() - .map(|activity| (activity.log_id, activity)) - .collect(); - activities.shrink_to_fit(); - - let offset = client.get_offset(); - let begin_datetime = (OffsetDateTime::now_utc() - Duration::days(7)).to_offset(offset); - let start_date = begin_datetime.date(); - - let mut new_activities: Vec<_> = client - .get_all_activities(start_date) - .await? - .into_iter() - .filter(|activity| !activities.contains_key(&activity.log_id)) - .collect(); - new_activities.shrink_to_fit(); - let futures = new_activities.iter().map(|activity| { - let pool = pool.clone(); - async move { - activity.insert_into_db(&pool).await?; - Ok(()) - } - }); - let results: Result, Error> = try_join_all(futures).await; - results?; - assert_eq!(new_activities.len(), 0); - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn test_delete_duplicate_activities() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let pool = PgPool::new(&config.pgurl)?; - let client = FitbitClient::with_auth(config.clone()).await?; - - let output = client.remove_duplicate_entries(&pool).await?; - debug!("{:?}", output); - assert_eq!(output.len(), 0); - Ok(()) - } -} diff --git a/fitbit_lib/src/lib.rs b/fitbit_lib/src/lib.rs index 20e862f0..adefd3c5 100644 --- a/fitbit_lib/src/lib.rs +++ b/fitbit_lib/src/lib.rs @@ -6,7 +6,6 @@ #![allow(clippy::cast_possible_wrap)] pub mod fitbit_archive; -pub mod fitbit_client; pub mod fitbit_heartrate; pub mod fitbit_statistics_summary; pub mod scale_measurement; diff --git a/garmin_cli/Cargo.toml b/garmin_cli/Cargo.toml index 04d8ae3f..8cde60a4 100644 --- a/garmin_cli/Cargo.toml +++ b/garmin_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garmin_cli" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/garmin_cli/src/garmin_cli_opts.rs b/garmin_cli/src/garmin_cli_opts.rs index 03ced61e..0eb78c18 100644 --- a/garmin_cli/src/garmin_cli_opts.rs +++ b/garmin_cli/src/garmin_cli_opts.rs @@ -1,4 +1,4 @@ -use anyhow::{format_err, Error}; +use anyhow::Error; use clap::Parser; use futures::{future::try_join_all, TryStreamExt}; use itertools::Itertools; @@ -25,7 +25,6 @@ use fitbit_lib::{ fitbit_archive::{ archive_fitbit_heartrates, get_heartrate_values, get_number_of_heartrate_values, }, - fitbit_client::FitbitClient, fitbit_heartrate::{import_garmin_heartrate_file, FitbitHeartRate}, fitbit_statistics_summary::FitbitStatisticsSummary, scale_measurement::ScaleMeasurement, @@ -88,15 +87,6 @@ pub enum GarminCliOpts { end_date: Option, }, Sync, - #[clap(alias = "fit")] - Fitbit { - #[clap(short, long)] - all: bool, - #[clap(short, long)] - start_date: Option, - #[clap(short, long)] - end_date: Option, - }, Strava, Import { #[clap(short, long)] @@ -150,13 +140,6 @@ impl GarminCliOpts { } .process_opts(&config) .await?; - Self::Fitbit { - all: false, - start_date: None, - end_date: None, - } - .process_opts(&config) - .await?; Self::Strava.process_opts(&config).await?; Self::Sync.process_opts(&config).await } else { @@ -193,57 +176,14 @@ impl GarminCliOpts { Self::SyncAll => { return Ok(()); } - Self::Fitbit { - all, - start_date, - end_date, - } => { - if start_date > end_date { - return Err(format_err!("Invalid date range")); - } - let cli = GarminCli::with_config()?; - let client = FitbitClient::with_auth(config.clone()).await?; - let updates = client.sync_everything(&pool).await?; - let start_date = start_date.map_or_else( - || (OffsetDateTime::now_utc() - Duration::days(3)).date(), - Into::into, - ); - let end_date = - end_date.map_or_else(|| OffsetDateTime::now_utc().date(), Into::into); - let mut date = start_date; - while date <= end_date { - client.import_fitbit_heartrate(date).await?; - FitbitHeartRate::calculate_summary_statistics(&client.config, &pool, date) - .await?; - date += Duration::days(1); - } - cli.stdout.send(format_sstr!("{updates:?}")); - - let start_date = (OffsetDateTime::now_utc() - Duration::days(10)).date(); - let filenames = client.sync_tcx(start_date).await?; - if !filenames.is_empty() { - let mut buf = cli.proc_everything().await?; - buf.extend_from_slice(&cli.sync_everything().await?); - } - let filenames = filenames - .into_iter() - .map(|p| p.to_string_lossy().into_owned()) - .join("\n"); - cli.stdout.send(filenames); - - if all { - FitbitHeartRate::get_all_summary_statistics(&client.config, &pool).await?; - } - return cli.stdout.close().await.map_err(Into::into); - } Self::Strava => { let cli = GarminCli::with_config()?; - let filenames = Self::sync_with_strava(&cli) + let activity_names = Self::sync_with_strava(&cli) .await? .into_iter() - .map(|p| p.to_string_lossy().into_owned()) + .map(|a| a.name) .join("\n"); - cli.stdout.send(filenames); + cli.stdout.send(activity_names); return cli.stdout.close().await.map_err(Into::into); } Self::Import { table, filepath } => { @@ -510,13 +450,6 @@ impl GarminCliOpts { .await?; if !filenames.is_empty() || !input_files.is_empty() || !dates.is_empty() { buf.extend_from_slice(&cli.sync_everything().await?); - if let Ok(client) = FitbitClient::with_auth(cli.config.clone()).await { - let result = client.sync_everything(&cli.pool).await?; - buf.push(format_sstr!( - "Syncing Fitbit Heartrate {hr}", - hr = result.measurements.len(), - )); - } } Ok(buf) } @@ -694,23 +627,21 @@ impl GarminCliOpts { /// # Errors /// Return error if various function fail - pub async fn sync_with_strava(cli: &GarminCli) -> Result, Error> { + pub async fn sync_with_strava(cli: &GarminCli) -> Result, Error> { let config = cli.config.clone(); let start_datetime = Some(OffsetDateTime::now_utc() - Duration::days(15)); let end_datetime = Some(OffsetDateTime::now_utc()); let client = StravaClient::with_auth(config).await?; - let filenames = client + let activities = client .sync_with_client(start_datetime, end_datetime, &cli.pool) .await?; - if !filenames.is_empty() { - cli.process_filenames(&filenames).await?; - StravaActivity::fix_summary_id_in_db(&cli.pool).await?; + if !activities.is_empty() { cli.proc_everything().await?; } - Ok(filenames) + Ok(activities) } } diff --git a/garmin_http/Cargo.toml b/garmin_http/Cargo.toml index ef56109f..d9e24079 100644 --- a/garmin_http/Cargo.toml +++ b/garmin_http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garmin_http" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/garmin_http/src/garmin_elements.rs b/garmin_http/src/garmin_elements.rs index 1a333bc7..fe320a85 100644 --- a/garmin_http/src/garmin_elements.rs +++ b/garmin_http/src/garmin_elements.rs @@ -8,10 +8,7 @@ use std::{collections::HashMap, fmt::Write}; use time::{macros::format_description, Date, Duration, OffsetDateTime}; use time_tz::OffsetDateTimeExt; -use fitbit_lib::{ - fitbit_client::FitbitUserProfile, fitbit_heartrate::FitbitHeartRate, - scale_measurement::ScaleMeasurement, -}; +use fitbit_lib::{fitbit_heartrate::FitbitHeartRate, scale_measurement::ScaleMeasurement}; use garmin_lib::{ date_time_wrapper::{iso8601::convert_datetime_to_str, DateTimeWrapper}, garmin_config::GarminConfig, @@ -1602,11 +1599,6 @@ fn get_buttons(demo: bool) -> Element { "onclick": "stravaAthlete();", "Strava Athlete", }, - button { - "type": "submit", - "onclick": "fitbitProfile();", - "Fitbit Profile", - }, button { "type": "submit", "onclick": "heartrateSync();", @@ -2119,76 +2111,6 @@ fn StravaElement(athlete: StravaAthlete) -> Element { } } -/// # Errors -/// Returns error if formatting fails -pub fn fitbit_body(profile: FitbitUserProfile) -> Result { - let mut app = VirtualDom::new_with_props(FitbitElement, FitbitElementProps { profile }); - app.rebuild_in_place(); - let mut renderer = dioxus_ssr::Renderer::default(); - let mut buffer = String::new(); - renderer - .render_to(&mut buffer, &app) - .map_err(Into::::into)?; - Ok(buffer) -} - -#[component] -fn FitbitElement(profile: FitbitUserProfile) -> Element { - let average_daily_steps = profile.average_daily_steps; - let country = &profile.country; - let date_of_birth = &profile.date_of_birth; - let display_name = &profile.display_name; - let distance_unit = &profile.distance_unit; - let encoded_id = &profile.encoded_id; - let first_name = &profile.first_name; - let last_name = &profile.last_name; - let full_name = &profile.full_name; - let gender = &profile.gender; - let height = profile.height; - let height_unit = &profile.height_unit; - let timezone = &profile.timezone; - let offset_from_utc_millis = profile.offset_from_utc_millis; - let stride_length_running = profile.stride_length_running; - let stride_length_walking = profile.stride_length_walking; - let weight = profile.weight; - let weight_unit = &profile.weight_unit; - - let offset_positive = offset_from_utc_millis >= 0; - let offset_abs_sec = offset_from_utc_millis.abs() / 1000; - - let mut offset_str = - print_h_m_s(offset_abs_sec as f64, true).unwrap_or_else(|_| "00:00:00".into()); - if !offset_positive { - offset_str = format_sstr!("-{offset_str}"); - } - - rsx! { - table { - "border": "1", - tbody { - tr {td {"Encoded ID"}, td {"{encoded_id}"}}, - tr {td {"First Name"}, td {"{first_name}"}}, - tr {td {"Last Name"}, td {"{last_name}"}}, - tr {td {"Full Name"}, td {"{full_name}"}}, - tr {td {"Avg Daily Steps"}, td {"{average_daily_steps}"}}, - tr {td {"Country"}, td {"{country}"}}, - tr {td {"DOB"}, td {"{date_of_birth}"}}, - tr {td {"Display Name"}, td {"{display_name}"}}, - tr {td {"Distance Unit"}, td {"{distance_unit}"}}, - tr {td {"Gender"}, td {"{gender}"}}, - tr {td {"Height"}, td {"{height:0.2}"}}, - tr {td {"Height Unit"}, td {"{height_unit}"}}, - tr {td {"Timezone"}, td {"{timezone}"}}, - tr {td {"Offset"}, td {"{offset_str}"}}, - tr {td {"Stride Length Running"}, td {"{stride_length_running:0.2}"}}, - tr {td {"Stride Length Walking"}, td {"{stride_length_walking:0.2}"}}, - tr {td {"Weight"}, td {"{weight}"}}, - tr {td {"Weight Unit"}, td {"{weight_unit}"}}, - }, - } - } -} - /// # Errors /// Returns error if formatting fails pub fn scale_measurement_manual_input_body() -> Result { diff --git a/garmin_http/src/garmin_requests.rs b/garmin_http/src/garmin_requests.rs index ecbd6616..b9b26fc6 100644 --- a/garmin_http/src/garmin_requests.rs +++ b/garmin_http/src/garmin_requests.rs @@ -3,21 +3,20 @@ use rweb::Schema; use rweb_helper::{DateTimeType, DateType}; use serde::{Deserialize, Serialize}; use stack_string::{format_sstr, StackString}; -use std::{collections::BTreeSet, path::PathBuf}; +use std::collections::BTreeSet; use time::{macros::time, Date, Duration, OffsetDateTime}; use time_tz::OffsetDateTimeExt; use tokio::task::spawn_blocking; use url::Url; use fitbit_lib::{ - fitbit_client::FitbitClient, fitbit_heartrate::FitbitHeartRate, - fitbit_statistics_summary::FitbitStatisticsSummary, + fitbit_heartrate::FitbitHeartRate, fitbit_statistics_summary::FitbitStatisticsSummary, }; use garmin_cli::garmin_cli::{GarminCli, GarminRequest}; use garmin_lib::{date_time_wrapper::DateTimeWrapper, garmin_config::GarminConfig}; use garmin_models::{ - fitbit_activity::FitbitActivity, garmin_correction_lap::GarminCorrectionLap, - garmin_summary::GarminSummary, strava_activity::StravaActivity, + garmin_correction_lap::GarminCorrectionLap, garmin_summary::GarminSummary, + strava_activity::StravaActivity, }; use garmin_reports::garmin_constraints::GarminConstraints; use garmin_utils::pgpool::PgPool; @@ -59,7 +58,7 @@ impl StravaSyncRequest { &self, pool: &PgPool, config: &GarminConfig, - ) -> Result, Error> { + ) -> Result, Error> { let gcli = GarminCli::from_pool(pool)?; let start_datetime = self @@ -72,18 +71,17 @@ impl StravaSyncRequest { .or_else(|| Some(OffsetDateTime::now_utc())); let client = StravaClient::with_auth(config.clone()).await?; - let filenames = client + let activities = client .sync_with_client(start_datetime, end_datetime, pool) .await?; - if !filenames.is_empty() { - gcli.process_filenames(&filenames).await?; + if !activities.is_empty() { gcli.sync_everything().await?; gcli.proc_everything().await?; } StravaActivity::fix_summary_id_in_db(pool).await?; - Ok(filenames) + Ok(activities) } } @@ -128,28 +126,6 @@ pub struct FitbitTcxSyncRequest { pub start_date: Option, } -impl FitbitTcxSyncRequest { - /// # Errors - /// Returns error if db query fails - pub async fn process( - &self, - pool: &PgPool, - config: &GarminConfig, - ) -> Result, Error> { - let client = FitbitClient::with_auth(config.clone()).await?; - let start_date = self.start_date.map_or_else( - || (OffsetDateTime::now_utc() - Duration::days(10)).date(), - Into::into, - ); - let filenames = client.sync_tcx(start_date).await?; - - let gcli = GarminCli::from_pool(pool)?; - gcli.sync_everything().await?; - gcli.proc_everything().await?; - Ok(filenames) - } -} - #[derive(Serialize, Deserialize, Debug, Clone, Copy, Schema)] pub struct ScaleMeasurementRequest { #[schema(description = "Start Date")] @@ -449,31 +425,6 @@ pub struct FitbitActivitiesRequest { pub start_date: Option, } -impl FitbitActivitiesRequest { - /// # Errors - /// Returns error if db query fails - pub async fn get_activities( - &self, - config: &GarminConfig, - ) -> Result, Error> { - let local = DateTimeWrapper::local_tz(); - let config = config.clone(); - let client = FitbitClient::with_auth(config).await?; - let start_date = self.start_date.map_or_else( - || { - (OffsetDateTime::now_utc() - Duration::days(14)) - .to_timezone(local) - .date() - }, - Into::into, - ); - client - .get_all_activities(start_date) - .await - .map_err(Into::into) - } -} - #[derive(Serialize, Deserialize, Schema)] pub struct GarminConnectActivitiesRequest { pub start_date: Option, diff --git a/garmin_http/src/garmin_rust_app.rs b/garmin_http/src/garmin_rust_app.rs index d41d501c..603d7c0e 100644 --- a/garmin_http/src/garmin_rust_app.rs +++ b/garmin_http/src/garmin_rust_app.rs @@ -13,7 +13,6 @@ use stack_string::format_sstr; use std::{net::SocketAddr, sync::Arc}; use tokio::{task::spawn, time::interval}; -use fitbit_lib::fitbit_client::FitbitClient; use garmin_cli::{garmin_cli::GarminCli, garmin_cli_opts::GarminCliOpts}; use garmin_lib::garmin_config::GarminConfig; use garmin_models::garmin_correction_lap::GarminCorrectionMap; @@ -22,14 +21,11 @@ use garmin_utils::pgpool::PgPool; use crate::{ errors::error_response, garmin_rust_routes::{ - add_garmin_correction, fitbit_activities, fitbit_activities_db, - fitbit_activities_db_update, fitbit_activity_types, fitbit_auth, fitbit_bodyweight, - fitbit_bodyweight_sync, fitbit_callback, fitbit_heartrate_api, fitbit_heartrate_cache, - fitbit_heartrate_cache_update, fitbit_plots, fitbit_plots_demo, fitbit_profile, - fitbit_refresh, fitbit_sync, fitbit_tcx_sync, garmin, garmin_connect_activities_db, - garmin_connect_activities_db_update, garmin_demo, garmin_scripts_demo_js, - garmin_scripts_js, garmin_sync, garmin_upload, heartrate_plots, heartrate_plots_demo, - heartrate_statistics_plots, heartrate_statistics_plots_demo, + add_garmin_correction, fitbit_activities_db, fitbit_activities_db_update, + fitbit_heartrate_cache, fitbit_heartrate_cache_update, fitbit_plots, fitbit_plots_demo, + garmin, garmin_connect_activities_db, garmin_connect_activities_db_update, garmin_demo, + garmin_scripts_demo_js, garmin_scripts_js, garmin_sync, garmin_upload, heartrate_plots, + heartrate_plots_demo, heartrate_statistics_plots, heartrate_statistics_plots_demo, heartrate_statistics_summary_db, heartrate_statistics_summary_db_update, initialize_map_js, line_plot_js, race_result_flag, race_result_import, race_result_plot, race_result_plot_demo, race_results_db, race_results_db_update, scale_measurement, @@ -74,14 +70,6 @@ pub async fn start_app() -> Result<(), Error> { for line in cli.sync_everything().await.unwrap_or(Vec::new()) { info!("{line}"); } - if let Ok(client) = FitbitClient::with_auth(cli.config.clone()).await { - if let Ok(result) = client.sync_everything(&cli.pool).await { - info!( - "Syncing Fitbit Heartrate {hr}", - hr = result.measurements.len(), - ); - } - } } } i.tick().await; @@ -130,25 +118,15 @@ fn get_garmin_path(app: &AppState) -> BoxedFilter<(impl Reply,)> { .boxed(); let garmin_sync_path = garmin_sync(app.clone()).boxed(); let strava_sync_path = strava_sync(app.clone()).boxed(); - let fitbit_auth_path = fitbit_auth(app.clone()).boxed(); - let fitbit_refresh_path = fitbit_refresh(app.clone()).boxed(); - let fitbit_callback_path = fitbit_callback(app.clone()).boxed(); - let fitbit_heartrate_api_path = fitbit_heartrate_api(app.clone()).boxed(); let heartrate_cache_get = fitbit_heartrate_cache(app.clone()).boxed(); let heartrate_cache_post = fitbit_heartrate_cache_update(app.clone()).boxed(); let heartrate_cache_path = heartrate_cache_get.or(heartrate_cache_post).boxed(); - let fitbit_sync_path = fitbit_sync(app.clone()).boxed(); - let fitbit_bodyweight_path = fitbit_bodyweight(app.clone()).boxed(); - let fitbit_bodyweight_sync_path = fitbit_bodyweight_sync(app.clone()).boxed(); let fitbit_plots_path = fitbit_plots(app.clone()).boxed(); let fitbit_plots_demo_path = fitbit_plots_demo(app.clone()).boxed(); let heartrate_statistics_plots_path = heartrate_statistics_plots(app.clone()).boxed(); let heartrate_statistics_plots_demo_path = heartrate_statistics_plots_demo(app.clone()).boxed(); let heartrate_plots_path = heartrate_plots(app.clone()).boxed(); let heartrate_plots_demo_path = heartrate_plots_demo(app.clone()).boxed(); - let fitbit_tcx_sync_path = fitbit_tcx_sync(app.clone()).boxed(); - let fitbit_activity_types_path = fitbit_activity_types(app.clone()).boxed(); - let fitbit_activities_path = fitbit_activities(app.clone()).boxed(); let fitbit_activities_db_get = fitbit_activities_db(app.clone()).boxed(); let fitbit_activities_db_post = fitbit_activities_db_update(app.clone()).boxed(); let fitbit_activities_db_path = fitbit_activities_db_get @@ -160,27 +138,15 @@ fn get_garmin_path(app: &AppState) -> BoxedFilter<(impl Reply,)> { let heartrate_statistics_summary_db_path = heartrate_statistics_summary_db_get .or(heartrate_statistics_summary_db_post) .boxed(); - let fitbit_profile_path = fitbit_profile(app.clone()).boxed(); - let fitbit_path = fitbit_auth_path - .or(fitbit_refresh_path) - .or(fitbit_callback_path) - .or(fitbit_heartrate_api_path) - .or(heartrate_cache_path) - .or(fitbit_sync_path) - .or(fitbit_bodyweight_path) - .or(fitbit_bodyweight_sync_path) + let fitbit_path = heartrate_cache_path .or(fitbit_plots_path) .or(fitbit_plots_demo_path) .or(heartrate_statistics_plots_path) .or(heartrate_statistics_plots_demo_path) .or(heartrate_plots_path) .or(heartrate_plots_demo_path) - .or(fitbit_tcx_sync_path) - .or(fitbit_activity_types_path) - .or(fitbit_activities_path) .or(fitbit_activities_db_path) .or(heartrate_statistics_summary_db_path) - .or(fitbit_profile_path) .boxed(); let scale_measurements_get = scale_measurement(app.clone()).boxed(); let scale_measurements_post = scale_measurement_update(app.clone()).boxed(); diff --git a/garmin_http/src/garmin_rust_routes.rs b/garmin_http/src/garmin_rust_routes.rs index 1aed2aa4..34206b7e 100644 --- a/garmin_http/src/garmin_rust_routes.rs +++ b/garmin_http/src/garmin_rust_routes.rs @@ -20,7 +20,7 @@ use tokio::{fs::File, io::AsyncWriteExt, task::spawn_blocking}; use tokio_stream::StreamExt; use fitbit_lib::{ - fitbit_archive, fitbit_client::FitbitClient, fitbit_heartrate::FitbitHeartRate, + fitbit_archive, fitbit_heartrate::FitbitHeartRate, fitbit_statistics_summary::FitbitStatisticsSummary, scale_measurement::ScaleMeasurement, }; use garmin_cli::garmin_cli::{GarminCli, GarminRequest}; @@ -46,31 +46,29 @@ use strava_lib::strava_client::StravaClient; use crate::{ errors::ServiceError as Error, garmin_elements::{ - create_fitbit_table, fitbit_body, index_new_body, scale_measurement_manual_input_body, - strava_body, table_body, IndexConfig, + index_new_body, scale_measurement_manual_input_body, strava_body, table_body, IndexConfig, }, garmin_requests::{ - AddGarminCorrectionRequest, FitbitActivitiesRequest, FitbitHeartrateCacheRequest, - FitbitHeartratePlotRequest, FitbitHeartrateUpdateRequest, FitbitStatisticsPlotRequest, - FitbitTcxSyncRequest, GarminConnectActivitiesDBUpdateRequest, GarminHtmlRequest, + AddGarminCorrectionRequest, FitbitHeartrateCacheRequest, FitbitHeartratePlotRequest, + FitbitHeartrateUpdateRequest, FitbitStatisticsPlotRequest, + GarminConnectActivitiesDBUpdateRequest, GarminHtmlRequest, HeartrateStatisticsSummaryDBUpdateRequest, ScaleMeasurementPlotRequest, ScaleMeasurementRequest, ScaleMeasurementUpdateRequest, StravaActivitiesRequest, StravaCreateRequest, StravaSyncRequest, StravaUpdateRequest, StravaUploadRequest, }, garmin_rust_app::AppState, logged_user::{LoggedUser, Session}, - FitbitActivityTypesWrapper, FitbitActivityWrapper, FitbitBodyWeightFatUpdateOutputWrapper, - FitbitBodyWeightFatWrapper, FitbitHeartRateWrapper, FitbitStatisticsSummaryWrapper, - GarminConnectActivityWrapper, RaceResultsWrapper, RaceTypeWrapper, ScaleMeasurementWrapper, - StravaActivityWrapper, + FitbitActivityTypesWrapper, FitbitActivityWrapper, FitbitHeartRateWrapper, + FitbitStatisticsSummaryWrapper, GarminConnectActivityWrapper, RaceResultsWrapper, + RaceTypeWrapper, ScaleMeasurementWrapper, StravaActivityWrapper, }; pub type WarpResult = Result; pub type HttpResult = Result; #[derive(Deserialize, Schema)] -pub struct FilterRequest { - pub filter: Option, +struct FilterRequest { + filter: Option, } fn proc_pattern_wrapper>( @@ -351,16 +349,6 @@ async fn save_file(file_path: &str, field: Part) -> Result { Ok(file_size) } -#[derive(Serialize, Deserialize, Schema)] -pub struct GarminConnectHrSyncRequest { - pub date: DateType, -} - -#[derive(Serialize, Deserialize, Schema)] -pub struct GarminConnectHrApiRequest { - pub date: DateType, -} - #[derive(RwebResponse)] #[response(description = "Garmin Sync", content = "html")] struct GarminSyncResponse(HtmlBase); @@ -393,7 +381,7 @@ pub async fn strava_sync( .run_sync(&state.db, &state.config) .await? .into_iter() - .map(|p| p.to_string_lossy().into_owned()) + .map(|a| a.name) .join("\n") .into(); let body = table_body(body)?.into(); @@ -448,11 +436,11 @@ pub async fn strava_refresh( #[derive(Debug, Serialize, Deserialize, Schema)] #[schema(component = "StravaCallbackRequest")] -pub struct StravaCallbackRequest { +struct StravaCallbackRequest { #[schema(description = "Authorization Code")] - pub code: StackString, + code: StackString, #[schema(description = "CSRF State")] - pub state: StackString, + state: StackString, } #[derive(RwebResponse)] @@ -562,8 +550,8 @@ pub async fn strava_activities_db( #[derive(Debug, Serialize, Deserialize, Schema)] #[schema(component = "StravaActiviesDBUpdateRequest")] -pub struct StravaActiviesDBUpdateRequest { - pub updates: Vec, +struct StravaActiviesDBUpdateRequest { + updates: Vec, } #[derive(RwebResponse)] @@ -640,73 +628,10 @@ pub async fn strava_create( Ok(HtmlBase::new(body).into()) } -#[derive(RwebResponse)] -#[response(description = "Fitbit Auth", content = "html")] -struct FitbitAuthResponse(HtmlBase); - -#[get("/garmin/fitbit/auth")] -pub async fn fitbit_auth( - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let client = FitbitClient::from_file(state.config.clone()) - .await - .map_err(Into::::into)?; - let url = client.get_fitbit_auth_url().map_err(Into::::into)?; - let body = url.as_str().into(); - Ok(HtmlBase::new(body).into()) -} - -#[derive(RwebResponse)] -#[response(description = "Fitbit Refresh Auth", content = "html")] -struct FitbitRefreshResponse(HtmlBase); - -#[get("/garmin/fitbit/refresh_auth")] -pub async fn fitbit_refresh( - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let mut client = FitbitClient::from_file(state.config.clone()) - .await - .map_err(Into::::into)?; - let body = client - .refresh_fitbit_access_token() - .await - .map_err(Into::::into)?; - client.to_file().await.map_err(Into::::into)?; - Ok(HtmlBase::new(body).into()) -} - -#[derive(Serialize, Deserialize, Schema)] -pub struct FitbitHeartrateApiRequest { - date: DateType, -} - #[derive(RwebResponse)] #[response(description = "Fitbit Heartrate")] struct FitbitHeartRateResponse(JsonBase, Error>); -#[get("/garmin/fitbit/heartrate_api")] -pub async fn fitbit_heartrate_api( - query: Query, - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let query = query.into_inner(); - let client = FitbitClient::with_auth(state.config.clone()) - .await - .map_err(Into::::into)?; - let mut hlist: Vec<_> = client - .get_fitbit_intraday_time_series_heartrate(query.date.into()) - .await - .map_err(Into::::into)? - .into_iter() - .map(Into::into) - .collect(); - hlist.shrink_to_fit(); - Ok(JsonBase::new(hlist).into()) -} - #[get("/garmin/fitbit/heartrate_cache")] pub async fn fitbit_heartrate_cache( query: Query, @@ -742,101 +667,16 @@ pub async fn fitbit_heartrate_cache_update( Ok(HtmlBase::new(format_sstr!("Finished {dates:?}")).into()) } -#[derive(RwebResponse)] -#[response(description = "Fitbit Body Weight")] -struct FitbitBodyWeightFatResponse(JsonBase, Error>); - -#[get("/garmin/fitbit/bodyweight")] -pub async fn fitbit_bodyweight( - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let client = FitbitClient::with_auth(state.config.clone()) - .await - .map_err(Into::::into)?; - let mut hlist: Vec<_> = client - .get_fitbit_bodyweightfat() - .await - .map_err(Into::::into)? - .into_iter() - .map(Into::into) - .collect(); - hlist.shrink_to_fit(); - Ok(JsonBase::new(hlist).into()) -} - -#[derive(RwebResponse)] -#[response(description = "Fitbit Body Weight Sync")] -struct FitbitBodyWeightFatUpdateResponse(JsonBase); - -#[post("/garmin/fitbit/bodyweight_sync")] -pub async fn fitbit_bodyweight_sync( - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let client = FitbitClient::with_auth(state.config.clone()) - .await - .map_err(Into::::into)?; - let hlist = client - .sync_everything(&state.db) - .await - .map_err(Into::::into)?; - Ok(JsonBase::new(hlist.into()).into()) -} - #[derive(RwebResponse)] #[response(description = "Fitbit Activities")] struct FitbitActivitiesResponse(JsonBase, Error>); -#[get("/garmin/fitbit/fitbit_activities")] -pub async fn fitbit_activities( - query: Query, - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let mut hlist: Vec<_> = query - .into_inner() - .get_activities(&state.config) - .await? - .into_iter() - .map(Into::into) - .collect(); - hlist.shrink_to_fit(); - Ok(JsonBase::new(hlist).into()) -} - -#[derive(Deserialize, Schema)] -#[schema(component = "FitbitCallbackRequest")] -pub struct FitbitCallbackRequest { - #[schema(description = "Authorization Code")] - code: StackString, - #[schema(description = "CSRF State")] - state: StackString, -} - #[derive(RwebResponse)] #[response(description = "Fitbit Callback", content = "html")] struct FitbitCallbackResponse(HtmlBase); -#[get("/garmin/fitbit/callback")] -pub async fn fitbit_callback( - query: Query, - #[data] state: AppState, -) -> WarpResult { - let query = query.into_inner(); - let mut client = FitbitClient::from_file(state.config.clone()) - .await - .map_err(Into::::into)?; - let body = client - .get_fitbit_access_token(&query.code, &query.state) - .await - .map_err(Into::::into)?; - client.to_file().await.map_err(Into::::into)?; - Ok(HtmlBase::new(body).into()) -} - #[derive(Serialize, Deserialize, Schema)] -pub struct FitbitSyncRequest { +struct FitbitSyncRequest { date: DateType, } @@ -844,34 +684,6 @@ pub struct FitbitSyncRequest { #[response(description = "Fitbit Sync", content = "html")] struct FitbitSyncResponse(HtmlBase); -#[post("/garmin/fitbit/sync")] -pub async fn fitbit_sync( - query: Query, - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let query = query.into_inner(); - let date = query.date.into(); - let client = FitbitClient::with_auth(state.config.clone()) - .await - .map_err(Into::::into)?; - let mut heartrates = client - .import_fitbit_heartrate(date) - .await - .map_err(Into::::into)?; - FitbitHeartRate::calculate_summary_statistics(&client.config, &state.db, date) - .await - .map_err(Into::::into)?; - let start = if heartrates.len() > 20 { - heartrates.len() - 20 - } else { - 0 - }; - let heartrates = heartrates.split_off(start); - let body = create_fitbit_table(heartrates)?.into(); - Ok(HtmlBase::new(body).into()) -} - #[derive(RwebResponse)] #[response(description = "Fitbit Heartrate Statistics Plots", content = "html")] struct FitbitStatisticsPlotResponse(HtmlBase); @@ -1157,23 +969,6 @@ pub async fn heartrate_plots_demo( #[response(description = "Fitbit Tcx Sync")] struct FitbitTcxSyncResponse(JsonBase, Error>); -#[post("/garmin/fitbit/fitbit_tcx_sync")] -pub async fn fitbit_tcx_sync( - query: Query, - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let mut flist: Vec<_> = query - .into_inner() - .process(&state.db, &state.config) - .await? - .into_iter() - .map(|x| x.to_string_lossy().into()) - .collect(); - flist.shrink_to_fit(); - Ok(JsonBase::new(flist).into()) -} - #[derive(Debug, Serialize, Deserialize, Schema)] #[schema(component = "PaginatedScaleMeasurement")] struct PaginatedScaleMeasurement { @@ -1307,33 +1102,6 @@ pub async fn scale_measurement_manual_input( Ok(HtmlBase::new(body.into()).into()) } -#[derive(Serialize)] -pub struct GpsList { - pub gps_list: Vec, -} - -#[derive(Serialize)] -pub struct TimeValue { - pub time: StackString, - pub value: f64, -} - -#[derive(Serialize)] -pub struct HrData { - pub hr_data: Vec, -} - -#[derive(Serialize)] -pub struct HrPace { - pub hr: f64, - pub pace: f64, -} - -#[derive(Serialize)] -pub struct HrPaceList { - pub hr_pace: Vec, -} - #[derive(RwebResponse)] #[response(description = "Logged in User")] struct UserResponse(JsonBase); @@ -1362,21 +1130,6 @@ pub async fn add_garmin_correction( #[response(description = "Fitbit Activity Types")] struct FitbitActivityTypesResponse(JsonBase); -#[get("/garmin/fitbit/fitbit_activity_types")] -pub async fn fitbit_activity_types( - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let client = FitbitClient::with_auth(state.config) - .await - .map_err(Into::::into)?; - let result = client - .get_fitbit_activity_types() - .await - .map_err(Into::::into)?; - Ok(JsonBase::new(result.into()).into()) -} - #[derive(RwebResponse)] #[response(description = "Strava Athlete")] struct StravaAthleteResponse(HtmlBase); @@ -1401,22 +1154,6 @@ pub async fn strava_athlete( #[response(description = "Fitbit Profile")] struct FitbitProfileResponse(HtmlBase); -#[get("/garmin/fitbit/profile")] -pub async fn fitbit_profile( - #[filter = "LoggedUser::filter"] _: LoggedUser, - #[data] state: AppState, -) -> WarpResult { - let client = FitbitClient::with_auth(state.config) - .await - .map_err(Into::::into)?; - let result = client - .get_user_profile() - .await - .map_err(Into::::into)?; - let body = fitbit_body(result)?.into(); - Ok(HtmlBase::new(body).into()) -} - #[derive(Debug, Serialize, Deserialize, Schema)] #[schema(component = "PaginatedGarminConnectActivity")] struct PaginatedGarminConnectActivity { @@ -1537,8 +1274,8 @@ pub async fn fitbit_activities_db( } #[derive(Debug, Serialize, Deserialize, Schema)] -pub struct FitbitActivitiesDBUpdateRequest { - pub updates: Vec, +struct FitbitActivitiesDBUpdateRequest { + updates: Vec, } #[derive(RwebResponse)] @@ -1648,11 +1385,11 @@ pub async fn heartrate_statistics_summary_db_update( #[derive(Serialize, Deserialize, Schema)] #[schema(component = "RaceResultPlotRequest")] -pub struct RaceResultPlotRequest { +struct RaceResultPlotRequest { #[schema(description = "Race Type")] - pub race_type: RaceTypeWrapper, + race_type: RaceTypeWrapper, #[schema(description = "Demo Flag")] - pub demo: Option, + demo: Option, } async fn race_result_plot_impl( @@ -1710,8 +1447,8 @@ pub async fn race_result_plot_demo( } #[derive(Serialize, Deserialize, Schema)] -pub struct RaceResultFlagRequest { - pub id: UuidWrapper, +struct RaceResultFlagRequest { + id: UuidWrapper, } #[derive(RwebResponse)] @@ -1744,8 +1481,8 @@ pub async fn race_result_flag( } #[derive(Serialize, Deserialize, Schema)] -pub struct RaceResultImportRequest { - pub filename: StackString, +struct RaceResultImportRequest { + filename: StackString, } #[derive(RwebResponse)] @@ -1789,9 +1526,9 @@ pub async fn race_result_import( } #[derive(Serialize, Deserialize, Schema)] -pub struct RaceResultsDBRequest { +struct RaceResultsDBRequest { #[schema(description = "Race Type")] - pub race_type: Option, + race_type: Option, } #[derive(RwebResponse)] @@ -1821,8 +1558,8 @@ pub async fn race_results_db( #[derive(Serialize, Deserialize, Schema)] #[schema(component = "RaceResultsDBUpdateRequest")] -pub struct RaceResultsDBUpdateRequest { - pub updates: Vec, +struct RaceResultsDBUpdateRequest { + updates: Vec, } #[derive(RwebResponse)] diff --git a/garmin_http/src/lib.rs b/garmin_http/src/lib.rs index 05594c35..cbd8ac0c 100644 --- a/garmin_http/src/lib.rs +++ b/garmin_http/src/lib.rs @@ -30,7 +30,6 @@ use stack_string::StackString; use std::{borrow::Cow, collections::HashMap}; use fitbit_lib::{ - fitbit_client::FitbitBodyWeightFatUpdateOutput, fitbit_heartrate::{FitbitBodyWeightFat, FitbitHeartRate}, fitbit_statistics_summary::FitbitStatisticsSummary, scale_measurement::ScaleMeasurement, @@ -151,21 +150,6 @@ struct _ScaleMeasurementWrapper { bone_pct: f64, } -#[derive(Debug, Serialize, Into, From)] -pub struct FitbitBodyWeightFatUpdateOutputWrapper(FitbitBodyWeightFatUpdateOutput); - -derive_rweb_schema!( - FitbitBodyWeightFatUpdateOutputWrapper, - _FitbitBodyWeightFatUpdateOutputWrapper -); - -#[derive(Debug, Serialize, Schema)] -#[schema(component = "FitbitBodyWeightFatUpdate")] -struct _FitbitBodyWeightFatUpdateOutputWrapper { - #[schema(description = "Measurements")] - measurements: Vec, -} - #[derive(Serialize, Deserialize, Clone, Debug, Into, From)] pub struct FitbitActivityWrapper(FitbitActivity); @@ -316,10 +300,9 @@ mod test { use rweb_helper::derive_rweb_test; use crate::{ - FitbitActivityWrapper, FitbitBodyWeightFatUpdateOutputWrapper, FitbitBodyWeightFatWrapper, - FitbitHeartRateWrapper, FitbitStatisticsSummaryWrapper, GarminConnectActivityWrapper, - RaceResultsWrapper, RaceTypeWrapper, ScaleMeasurementWrapper, StravaActivityWrapper, - _FitbitActivityWrapper, _FitbitBodyWeightFatUpdateOutputWrapper, + FitbitActivityWrapper, FitbitBodyWeightFatWrapper, FitbitHeartRateWrapper, + FitbitStatisticsSummaryWrapper, GarminConnectActivityWrapper, RaceResultsWrapper, + RaceTypeWrapper, ScaleMeasurementWrapper, StravaActivityWrapper, _FitbitActivityWrapper, _FitbitBodyWeightFatWrapper, _FitbitHeartRateWrapper, _FitbitStatisticsSummaryWrapper, _GarminConnectActivityWrapper, _RaceResultsWrapper, _RaceTypeWrapper, _ScaleMeasurementWrapper, _StravaActivityWrapper, @@ -331,10 +314,6 @@ mod test { derive_rweb_test!(StravaActivityWrapper, _StravaActivityWrapper); derive_rweb_test!(FitbitBodyWeightFatWrapper, _FitbitBodyWeightFatWrapper); derive_rweb_test!(ScaleMeasurementWrapper, _ScaleMeasurementWrapper); - derive_rweb_test!( - FitbitBodyWeightFatUpdateOutputWrapper, - _FitbitBodyWeightFatUpdateOutputWrapper - ); derive_rweb_test!(FitbitActivityWrapper, _FitbitActivityWrapper); derive_rweb_test!(GarminConnectActivityWrapper, _GarminConnectActivityWrapper); derive_rweb_test!( diff --git a/garmin_lib/Cargo.toml b/garmin_lib/Cargo.toml index 31e1e3bf..7d5cbf4b 100644 --- a/garmin_lib/Cargo.toml +++ b/garmin_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garmin_lib" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/garmin_models/Cargo.toml b/garmin_models/Cargo.toml index 24755392..5aebcfeb 100644 --- a/garmin_models/Cargo.toml +++ b/garmin_models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garmin_models" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2021" diff --git a/garmin_parser/Cargo.toml b/garmin_parser/Cargo.toml index 220884a7..f17ac746 100644 --- a/garmin_parser/Cargo.toml +++ b/garmin_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garmin_parser" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2021" diff --git a/garmin_reports/Cargo.toml b/garmin_reports/Cargo.toml index fd4248b8..ee771e5b 100644 --- a/garmin_reports/Cargo.toml +++ b/garmin_reports/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garmin_reports" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/garmin_utils/Cargo.toml b/garmin_utils/Cargo.toml index 1fa84338..621186be 100644 --- a/garmin_utils/Cargo.toml +++ b/garmin_utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garmin_utils" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2021" diff --git a/race_result_analysis/Cargo.toml b/race_result_analysis/Cargo.toml index 19c6a44b..c0e02c88 100644 --- a/race_result_analysis/Cargo.toml +++ b/race_result_analysis/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "race_result_analysis" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/strava_lib/Cargo.toml b/strava_lib/Cargo.toml index 07ae98f9..4528fa48 100644 --- a/strava_lib/Cargo.toml +++ b/strava_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "strava_lib" -version = "0.14.25" +version = "0.14.26" authors = ["Daniel Boline "] edition = "2018" diff --git a/strava_lib/src/strava_client.rs b/strava_lib/src/strava_client.rs index 2123da5f..8cd38f56 100644 --- a/strava_lib/src/strava_client.rs +++ b/strava_lib/src/strava_client.rs @@ -1,6 +1,5 @@ use anyhow::{format_err, Error}; use crossbeam_utils::atomic::AtomicCell; -use futures::{future::try_join_all, TryStreamExt}; use log::warn; use maplit::hashmap; use once_cell::sync::Lazy; @@ -9,32 +8,25 @@ use reqwest::{ multipart::{Form, Part}, Client, Url, }; -use select::{document::Document, predicate::Attr}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use stack_string::{format_sstr, StackString}; -use std::{ - collections::HashSet, - path::{Path, PathBuf}, -}; +use std::path::Path; use tempfile::Builder; use time::{macros::format_description, OffsetDateTime}; use time_tz::{OffsetDateTimeExt, Tz}; use tokio::{ - fs::{create_dir_all, File}, + fs::File, io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, task::spawn_blocking, time::sleep, }; -use tokio_stream::StreamExt; use garmin_lib::{ date_time_wrapper::{iso8601::convert_datetime_to_str, DateTimeWrapper}, garmin_config::GarminConfig, }; -use garmin_models::{ - garmin_summary::get_list_of_activities_from_db, strava_activity::StravaActivity, -}; +use garmin_models::strava_activity::StravaActivity; use garmin_utils::{ garmin_util::{get_random_string, gzip_file}, pgpool::PgPool, @@ -43,13 +35,6 @@ use garmin_utils::{ }; static CSRF_TOKEN: Lazy>> = Lazy::new(|| AtomicCell::new(None)); -static WEB_CSRF: Lazy>> = Lazy::new(|| AtomicCell::new(None)); - -#[derive(Clone, Debug)] -struct WebCsrf { - param: StackString, - token: StackString, -} #[derive(Debug, Copy, Clone)] pub enum StravaAuthType { @@ -126,136 +111,6 @@ impl StravaClient { Ok(client) } - /// # Errors - /// Return error if authorization calls fail - pub async fn webauth(&self) -> Result<(), Error> { - let strava_endpoint = self - .config - .strava_endpoint - .as_ref() - .ok_or_else(|| format_err!("Missing strava url"))?; - - let login_url = strava_endpoint.join("login")?; - let session_url = strava_endpoint.join("session")?; - let email = self - .config - .strava_email - .as_ref() - .ok_or_else(|| format_err!("No Strava Email"))?; - let password = self - .config - .strava_password - .as_ref() - .ok_or_else(|| format_err!("No Strava Password"))?; - - let text = self - .client - .get(login_url) - .send() - .await? - .error_for_status()? - .text() - .await?; - let (param, token) = Self::extract_web_csrf(&text)?; - let data = hashmap! { - "email" => email.as_str(), - "password" => password.as_str(), - "remember_me" => "on", - param.as_str() => token.as_str(), - }; - self.client - .post(session_url) - .form(&data) - .send() - .await? - .error_for_status()?; - WEB_CSRF.store(Some(WebCsrf { param, token })); - Ok(()) - } - - async fn export_original(&self, activity_id: u64) -> Result { - if let Some(web_csrf) = WEB_CSRF.swap(None) { - WEB_CSRF.swap(Some(web_csrf)); - } else { - self.webauth().await?; - } - let url = self - .config - .strava_endpoint - .as_ref() - .unwrap() - .join(&format_sstr!("activities/{activity_id}/export_original"))?; - let resp = self.client.get(url).send().await?.error_for_status()?; - - create_dir_all(&self.config.download_directory).await?; - - let id_str = StackString::from_display(activity_id); - let fname = self - .config - .download_directory - .join(id_str) - .with_extension("fit"); - - let mut f = File::create(&fname).await?; - let mut stream = resp.bytes_stream(); - while let Some(item) = stream.next().await { - f.write_all(&item?).await?; - } - - Ok(fname) - } - - /// # Errors - /// Return error if api calls fail - pub async fn delete_activity(&self, activity_id: u64) -> Result<(), Error> { - let web_csrf = if let Some(web_csrf) = WEB_CSRF.swap(None) { - web_csrf - } else { - self.webauth().await?; - if let Some(web_csrf) = WEB_CSRF.swap(None) { - web_csrf - } else { - return Err(format_err!("Auth failure")); - } - }; - let url = self - .config - .strava_endpoint - .as_ref() - .ok_or_else(|| format_err!("Bad URL"))? - .join(&format_sstr!("activities/{activity_id}"))?; - let data = hashmap! { - "_method" => "delete", - web_csrf.param.as_str() => web_csrf.token.as_str(), - }; - self.client - .post(url) - .form(&data) - .send() - .await? - .error_for_status()?; - WEB_CSRF.swap(Some(web_csrf)); - Ok(()) - } - - fn extract_web_csrf(text: &str) -> Result<(StackString, StackString), Error> { - let document = Document::from(text); - if let Some(param) = document - .find(Attr("name", "csrf-param")) - .next() - .and_then(|node| node.attr("content")) - { - if let Some(token) = document - .find(Attr("name", "csrf-token")) - .next() - .and_then(|node| node.attr("content")) - { - return Ok((param.into(), token.into())); - } - } - Err(format_err!("No csrf token")) - } - /// # Errors /// Return error if writing config to file fails pub async fn to_file(&self) -> Result<(), Error> { @@ -712,7 +567,7 @@ impl StravaClient { start_datetime: Option, end_datetime: Option, pool: &PgPool, - ) -> Result, Error> { + ) -> Result, Error> { let new_activities: Vec<_> = self .get_all_strava_activites(start_datetime, end_datetime) .await?; @@ -720,38 +575,7 @@ impl StravaClient { StravaActivity::upsert_activities(&new_activities, pool).await?; StravaActivity::fix_summary_id_in_db(pool).await?; - let mut constraints: SmallVec<[StackString; 2]> = SmallVec::new(); - if let Some(start_datetime) = start_datetime { - constraints.push(format_sstr!("begin_datetime >= '{start_datetime}'")); - } - if let Some(end_datetime) = end_datetime { - constraints.push(format_sstr!("begin_datetime <= '{end_datetime}'")); - } - let constraints = constraints.join(" AND "); - - let mut old_activities: HashSet<_> = get_list_of_activities_from_db(&constraints, pool) - .await? - .map_ok(|(d, _)| d) - .try_collect() - .await?; - old_activities.shrink_to_fit(); - - #[allow(clippy::manual_filter_map)] - let futures = new_activities - .into_iter() - .filter_map(|activity| { - if old_activities.contains(&activity.start_date) { - None - } else { - Some(activity.id) - } - }) - .map(|activity_id| async move { - self.export_original(activity_id as u64) - .await - .map_err(Into::into) - }); - try_join_all(futures).await + Ok(new_activities) } } @@ -807,7 +631,7 @@ mod tests { use time::{Duration, OffsetDateTime}; use garmin_lib::garmin_config::GarminConfig; - use garmin_utils::{garmin_util::get_md5sum, pgpool::PgPool, sport_types::SportTypes}; + use garmin_utils::{pgpool::PgPool, sport_types::SportTypes}; use crate::strava_client::{StravaActivity, StravaClient}; @@ -906,36 +730,4 @@ mod tests { assert_eq!(new_activities.len(), 0); Ok(()) } - - #[tokio::test] - #[ignore] - async fn test_webauth() -> Result<(), Error> { - let config = GarminConfig::get_config(None)?; - let client = StravaClient::with_auth(config).await?; - client.webauth().await?; - client.export_original(3862793062).await?; - - let fname = client - .config - .download_directory - .join("3862793062") - .with_extension("fit"); - assert!(fname.exists()); - assert_eq!(&get_md5sum(&fname)?, "6365f391e3873cfdfeb5d716195f7271"); - - Ok(()) - } - - #[test] - fn test_extract_web_csrf() -> Result<(), Error> { - let text = include_str!("../../tests/data/strava_login_page.html"); - let (name, token) = StravaClient::extract_web_csrf(&text)?; - assert_eq!(name.as_str(), "authenticity_token"); - assert_eq!( - token.as_str(), - "1YVkvKYefXvFw1a++rprn9XM1xgT88O6A8UumIH99P4OVYl+wm9GyZp0zBrxNc8hRPqa8wzwJcJ/\ - 9YHsQAIZaQ==" - ); - Ok(()) - } }