From 762ea09b565136e00716a22b9c96d240ba64ce1d Mon Sep 17 00:00:00 2001 From: Teascade Date: Tue, 25 Aug 2020 21:55:04 +0300 Subject: [PATCH] Separate commands, apistructs and add api command --- src/{api.rs => api/mod.rs} | 198 ++++++++++++-------------------- src/api/structs.rs | 227 +++++++++++++++++++++++++++++++++++++ src/{cmd.rs => args.rs} | 26 ++++- src/commands.rs | 194 +++++++++++++++++++++++++++++++ src/config.rs | 2 + src/gpx.rs | 12 +- src/main.rs | 174 ++++++---------------------- 7 files changed, 556 insertions(+), 277 deletions(-) rename src/{api.rs => api/mod.rs} (63%) create mode 100644 src/api/structs.rs rename src/{cmd.rs => args.rs} (84%) create mode 100644 src/commands.rs diff --git a/src/api.rs b/src/api/mod.rs similarity index 63% rename from src/api.rs rename to src/api/mod.rs index 9e1eb59..2b4bc95 100644 --- a/src/api.rs +++ b/src/api/mod.rs @@ -1,125 +1,16 @@ +pub mod structs; + use super::Config; use super::GenericError; use chrono::format::ParseError; use chrono::NaiveDateTime; use minreq::Response; -use serde::Deserialize; -use std::fmt::{Display, Formatter}; +use std::fmt::Display; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::thread; use std::time::{Duration, Instant}; - -pub trait Timestamped { - fn timestamp(&self) -> Option; -} - -#[derive(Deserialize, Debug)] -pub struct ErrorModel { - #[serde(rename(deserialize = "Message"))] - pub message: Option, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct TagModel { - pub id: Option, - pub imei: Option, - #[serde(rename(deserialize = "batteryValue"))] - pub battery_value: Option, - #[serde(rename(deserialize = "btId"))] - pub bt_id: Option, - #[serde(rename(deserialize = "nfcId"))] - pub nfc_id: Option, -} - -impl TagModel { - pub fn get_nick(&self, config: &Config) -> Option { - if let Some(id) = &self.id { - if let Some(n) = config.nicknames.get(id) { - Some(n.clone()) - } else { - None - } - } else { - None - } - } - - pub fn get_id(&self) -> Result { - match &self.id { - Some(id) => Ok(id.to_string()), - None => Err(GenericError::MessagedError( - "Could not find device id. Error probably on Yetzon server side.".to_owned(), - None, - )), - } - } -} - -impl Display for TagModel { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - let mut text = String::new(); - text += &format!( - " id = {}", - self.id.as_ref().unwrap_or(&"unknown".to_owned()) - ); - text += &format!( - "\n imei = {}", - self.imei.as_ref().unwrap_or(&"unknown".to_owned()) - ); - text += &format!( - "\n battery value = {}%", - &self - .battery_value - .map(|v| v.to_string()) - .unwrap_or("unknown".to_owned()) - ); - text += &format!( - "\n bluetooth id = {}", - &self.bt_id.as_ref().unwrap_or(&"unknown".to_owned()) - ); - text += &format!( - "\n NFC id = {}", - &self.nfc_id.as_ref().unwrap_or(&"unknown".to_owned()) - ); - write!(f, "{}", text) - } -} - -#[derive(Deserialize, Debug, Clone)] -#[serde(deny_unknown_fields)] -pub struct StateModel { - pub id: Option, - pub timestamp: Option, - pub state: Option, - #[serde(rename(deserialize = "realState"))] - pub real_state: Option, -} - -impl Timestamped for StateModel { - fn timestamp(&self) -> Option { - self.timestamp.clone() - } -} - -#[derive(Deserialize, Debug, Clone)] -#[serde(deny_unknown_fields)] -pub struct LocationModel { - pub timestamp: Option, - #[serde(rename(deserialize = "type"))] - pub loc_type: Option, - #[serde(rename(deserialize = "coordinateLat"))] - pub lat: Option, - #[serde(rename(deserialize = "coordinateLng"))] - pub lon: Option, - pub accuracy: Option, -} - -impl Timestamped for LocationModel { - fn timestamp(&self) -> Option { - self.timestamp.clone() - } -} +pub use structs::*; pub struct API { pub config: Config, @@ -134,8 +25,16 @@ impl API { } } - pub fn get_tags(&mut self) -> Result, GenericError> { - let response = self.request(self.config.tags_url.clone())?; + #[allow(dead_code)] + pub fn get_raw_string(&self, url: APIUrl) -> Result { + Ok(self + .request(API::api_url(url, &self.config))? + .as_str()? + .to_owned()) + } + + pub fn get_tags(&self) -> Result, GenericError> { + let response = self.request(API::api_url(APIUrl::Tags, &self.config))?; let tags = response.json(); if let Err(_) = tags { let err: ErrorModel = response.json()?; @@ -145,26 +44,39 @@ impl API { } } - pub fn get_states(&mut self, tag_id: &String) -> Result, GenericError> { - let response = self.request(str::replace(&self.config.states_url, "{tag}", &tag_id))?; + pub fn get_states(&self, tag_id: &String) -> Result, GenericError> { + let response = self.request(API::api_url(APIUrl::States(tag_id.clone()), &self.config))?; + Ok(response.json()?) + } + + pub fn get_current_locations( + &self, + tag_id: &String, + ) -> Result, GenericError> { + let response = self.request(API::api_url( + APIUrl::CurrLocations(tag_id.clone()), + &self.config, + ))?; Ok(response.json()?) } pub fn get_locations( - &mut self, + &self, tag_id: &String, state_id: &String, ) -> Result, GenericError> { - let url = str::replace(&self.config.locations_url, "{tag}", &tag_id); - let url = str::replace(&url, "{state}", &state_id); - let response = self.request(url)?; + let response = self.request(API::api_url( + APIUrl::Locations(tag_id.clone(), state_id.clone()), + &self.config, + ))?; Ok(response.json()?) } pub fn queue_location(&mut self, tag_id: &String, state_id: &String) { - let url = str::replace(&self.config.locations_url, "{tag}", &tag_id); - let url = str::replace(&url, "{state}", &state_id); - self.location_req_que.push(url); + self.location_req_que.push(API::api_url( + APIUrl::Locations(tag_id.clone(), state_id.clone()), + &self.config, + )); } pub fn begin_location_fetch( @@ -282,6 +194,35 @@ impl API { timestamped } + pub fn print_list(list: Result, GenericError>) { + match list { + Ok(items) => { + for item in items { + println!( + "\n{}\n{}", + item.timestamp().unwrap_or("No timestamp".to_owned()), + item + ); + } + } + Err(e) => eprintln!("Could not complete request: {}", e), + } + } + + pub fn api_url(url: APIUrl, config: &Config) -> String { + match url { + APIUrl::CurrLocations(tag) => { + str::replace(&config.current_locations_url, "{tag}", &tag) + } + APIUrl::Locations(tag, state) => { + let url = str::replace(&config.locations_url, "{tag}", &tag); + str::replace(&url, "{state}", &state) + } + APIUrl::States(tag) => str::replace(&config.states_url, "{tag}", &tag), + APIUrl::Tags => config.tags_url.clone(), + } + } + fn add_error_back( location: String, rec: &Result, ErrorModel>, GenericError>, @@ -299,7 +240,7 @@ impl API { } } - fn request(&mut self, url: String) -> Result { + fn request(&self, url: String) -> Result { let response = minreq::get(url) .with_header("content-type", "application/json") .with_header("x-api-key", &self.config.api_key) @@ -321,3 +262,10 @@ impl API { } } } + +pub enum APIUrl { + CurrLocations(String), + Locations(String, String), + States(String), + Tags, +} diff --git a/src/api/structs.rs b/src/api/structs.rs new file mode 100644 index 0000000..ea6983c --- /dev/null +++ b/src/api/structs.rs @@ -0,0 +1,227 @@ +use super::{Config, GenericError}; +use serde::Deserialize; +use std::fmt::{Display, Formatter}; + +pub trait Timestamped { + fn timestamp(&self) -> Option; +} + +#[derive(Deserialize, Debug)] +pub struct ErrorModel { + #[serde(rename(deserialize = "Message"))] + pub message: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +pub struct TagModel { + pub id: Option, + pub imei: Option, + pub battery_value: Option, + pub bt_id: Option, + pub nfc_id: Option, + pub pair_code: Option, + pub location: Option, + pub state: Option, + pub last_contact: Option, + pub charging: Option, + pub charger_connected: Option, + pub config: Option, + pub capabilities: Option, +} + +impl TagModel { + pub fn get_nick(&self, config: &Config) -> Option { + if let Some(id) = &self.id { + if let Some(n) = config.nicknames.get(id) { + Some(n.clone()) + } else { + None + } + } else { + None + } + } + + pub fn get_id(&self) -> Result { + match &self.id { + Some(id) => Ok(id.to_string()), + None => Err(GenericError::MessagedError( + "Could not find device id. Error probably on Yetzon server side.".to_owned(), + None, + )), + } + } +} + +impl Display for TagModel { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut text = String::new(); + text += &format!( + " id = {}", + self.id.as_ref().unwrap_or(&"unknown".to_owned()) + ); + text += &format!( + "\n imei = {}", + self.imei.as_ref().unwrap_or(&"unknown".to_owned()) + ); + text += &format!( + "\n battery value = {}%", + &self + .battery_value + .map(|v| v.to_string()) + .unwrap_or("unknown".to_owned()) + ); + text += &format!( + "\n bluetooth id = {}", + &self.bt_id.as_ref().unwrap_or(&"unknown".to_owned()) + ); + text += &format!( + "\n NFC id = {}", + &self.nfc_id.as_ref().unwrap_or(&"unknown".to_owned()) + ); + write!(f, "{}", text) + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct ConfigModel { + pub active_interval: Option, + pub sleep_interval: Option, + pub sos_alert_sound_enabled: Option, + pub mode: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct ChangingConfig { + current: Option, + pending: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct Capabilities { + rx: Option, + sos: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +pub struct StateModel { + pub id: Option, + pub timestamp: Option, + pub state: Option, + pub real_state: Option, +} + +impl StateModel { + pub fn get_id(&self) -> Result { + match &self.id { + Some(id) => Ok(id.to_string()), + None => Err(GenericError::MessagedError( + "Could not find state id. Error probably on Yetzon server side.".to_owned(), + None, + )), + } + } +} + +impl Timestamped for StateModel { + fn timestamp(&self) -> Option { + self.timestamp.clone() + } +} + +impl Display for StateModel { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut text = String::new(); + text += &format!( + " id = {}", + self.id.as_ref().unwrap_or(&"unknown".to_owned()) + ); + text += &format!( + "\n timestamp = {}", + self.timestamp.as_ref().unwrap_or(&"unknown".to_owned()) + ); + text += &format!( + "\n state = {}", + self.state.as_ref().unwrap_or(&"unknown".to_owned()) + ); + text += &format!( + "\n real state = {}", + self.real_state + .as_ref() + .unwrap_or(&"no real state".to_owned()) + ); + write!(f, "{}", text) + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct ChangingState { + current: Option, + pending: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct LocationModel { + pub timestamp: Option, + #[serde(rename(deserialize = "type"))] + pub loc_type: Option, + #[serde(rename(deserialize = "coordinateLat"))] + #[serde(alias = "latitude")] + pub lat: Option, + #[serde(rename(deserialize = "coordinateLng"))] + #[serde(alias = "longitude")] + pub lon: Option, + pub accuracy: Option, +} + +impl Timestamped for LocationModel { + fn timestamp(&self) -> Option { + self.timestamp.clone() + } +} + +impl Display for LocationModel { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut text = String::new(); + text += &format!( + " timestamp = {}", + self.timestamp().as_ref().unwrap_or(&"unknown".to_owned()) + ); + text += &format!( + "\n location type = {}", + self.loc_type.as_ref().unwrap_or(&"unknown".to_owned()) + ); + text += &format!( + "\n latitude = {}", + self.lat + .as_ref() + .map(|v| v.to_string()) + .unwrap_or("unknown".to_owned()) + ); + text += &format!( + "\n longitude = {}", + self.lon + .as_ref() + .map(|v| v.to_string()) + .unwrap_or("unknown".to_owned()) + ); + text += &format!( + "\n accuracy = {}", + self.accuracy + .as_ref() + .map(|v| v.to_string()) + .unwrap_or("unknown".to_owned()) + ); + write!(f, "{}", text) + } +} diff --git a/src/cmd.rs b/src/args.rs similarity index 84% rename from src/cmd.rs rename to src/args.rs index 827ebbd..0cd8b93 100644 --- a/src/cmd.rs +++ b/src/args.rs @@ -20,6 +20,7 @@ pub struct EnvOpt { pub enum Subcommand { Between(BetweenOpt), Init(InitOpt), + Api(ApiOpt), Nick(NickOpt), Get(GetOpt), } @@ -58,6 +59,14 @@ pub struct InitOpt { pub api_key: Option, } +#[derive(FromArgs)] +#[argh( + subcommand, + name = "api", + description = "check integrity of API and API-key validity" +)] +pub struct ApiOpt {} + #[derive(FromArgs)] #[argh( subcommand, @@ -88,11 +97,11 @@ pub struct NickListOpt {} #[argh( subcommand, name = "set", - description = "Set nickname for given device index. see index with nick list" + description = "Set nickname for given device. Give device index from nick list, or current nickname." )] pub struct NickSetOpt { - #[argh(positional, description = "device index, see index with nick list")] - pub index: i32, + #[argh(positional, description = "the device index or nickname")] + pub device: String, #[argh(positional, description = "the new nickname for the device")] pub nickname: String, } @@ -111,10 +120,19 @@ pub struct GetOpt { #[derive(FromArgs)] #[argh(subcommand)] pub enum GetSub { + Tags(GetTagsOpt), States(GetStatesOpt), Locations(GetLocationsOpt), } +#[derive(FromArgs)] +#[argh( + subcommand, + name = "tags", + description = "Get all tags connected to this API-key" +)] +pub struct GetTagsOpt {} + #[derive(FromArgs)] #[argh( subcommand, @@ -136,5 +154,5 @@ pub struct GetLocationsOpt { #[argh(positional, description = "the device in question")] pub device: String, #[argh(positional, description = "the state id")] - pub state: String, + pub state: Option, } diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..9fa1a9a --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,194 @@ +use super::api::{LocationModel, StateModel, TagModel, API}; +use super::config::Config; +use super::errors::GenericError; +use super::gpx; +use chrono::NaiveDateTime; +use std::io::prelude::*; +use std::path::PathBuf; +use std::sync::mpsc::TryRecvError; +use std::time::Duration; + +pub fn init(path: &PathBuf, api_key: Option) -> Result<(), GenericError> { + let mut config = Config::default(); + if let Some(api_key) = api_key { + config.api_key = api_key; + } + Ok(config.write_to(path)?) +} + +pub fn between( + api: &mut API, + tag_str: String, + from: Option, + to: Option, +) -> Result<(TagModel, Vec<(NaiveDateTime, LocationModel)>), GenericError> { + let tags = api.get_tags()?; + let tag = find_tag(tag_str, &tags, &api.config)?; + let tag_id = tag.get_id()?; + + print!("Preparing..\r"); + std::io::stdout().lock().flush().ok(); + + let state_list = api.get_states(&tag_id)?; + + let states = API::get_between(&state_list, from, to, true, &api.config); + + let mut locations = Vec::new(); + for (_, state) in states.iter() { + api.queue_location(&tag_id, state.id.as_ref().unwrap()); + } + let receiver = api.begin_location_fetch(); + let mut counter = 0; + loop { + match receiver.try_recv() { + Ok(res) => match res { + Ok(loc) => match loc { + Ok(mut loc) => { + counter += 1; + let remaining = exp_time(&api, states.len() as u64 - counter as u64); + print!( + "Done: {:<5.2}% Remaining: {:<5.2} seconds\r", + counter as f32 / states.len() as f32 * 100., + remaining.as_secs_f32() + ); + std::io::stdout().lock().flush().ok(); + locations.append(&mut loc); + } + Err(e) => eprintln!( + "Error fetching location data: {}", + e.message.unwrap_or(String::new()) + ), + }, + Err(e) => eprintln!("{}", e), + }, + Err(e) => { + if let TryRecvError::Disconnected = e { + break; + } + } + } + } + println!("{:<100}", "\nDone!"); + locations.sort_by(|loc1, loc2| loc2.timestamp.cmp(&loc1.timestamp)); + + let locs = API::get_between(&locations, from, to, false, &api.config); + + let gpx = gpx::generate_gpx(&tag, &locs, &api.config)?; + gpx::write_gpx(&gpx)?; + Ok((tag, locs)) +} + +pub fn check_api(api: &API) -> Result<(), GenericError> { + let tags = api.get_tags()?; + if let Some(tag) = tags.get(0) { + let tag_id = tag.get_id()?; + + api.get_current_locations(&tag_id)?; + + if let Some(state) = api.get_states(&tag_id)?.get(0) { + let state_id = state.get_id()?; + api.get_locations(&tag_id, &state_id)?; + Ok(()) + } else { + Err(GenericError::MessagedError( + "Could not find any states for the first device found.".to_owned(), + None, + )) + } + } else { + Err(GenericError::MessagedError("Could not find any devices. Please make sure this API-key is paired with a device first.".to_owned(), None)) + } +} + +pub fn nick_list(api: &API) -> Result, GenericError> { + let mut tags = api.get_tags()?; + tags.sort_by(|tag1, tag2| tag1.id.cmp(&tag2.id)); + let list = tags + .iter() + .enumerate() + .map(|(idx, tag)| { + ( + ( + idx + 1, + tag.get_nick(&api.config) + .unwrap_or("No current nick".to_owned()), + ), + tag.clone(), + ) + }) + .collect(); + Ok(list) +} + +pub fn nick_set( + api: &mut API, + config_path: &PathBuf, + tag_str: String, + nickname: String, +) -> Result<(), GenericError> { + let mut tags = api.get_tags()?; + tags.sort_by(|tag1, tag2| tag1.id.cmp(&tag2.id)); + let tag = find_tag(tag_str.clone(), &tags, &api.config)?; + if let Some(id) = &tag.id { + api.config.nicknames.insert(id.clone(), nickname); + api.config.write_to(config_path)?; + Ok(()) + } else { + Err(GenericError::MessagedError( + format!("Device {} does not have an id", tag_str), + None, + )) + } +} + +pub fn get_locations( + api: &API, + tag: String, + state: Option, +) -> Result, GenericError> { + let tags = api.get_tags()?; + let tag = find_tag(tag, &tags, &api.config)?; + if let Some(state) = state { + Ok(api.get_locations(&tag.get_id()?, &state)?) + } else { + Ok(api.get_current_locations(&tag.get_id()?)?) + } +} + +pub fn get_states(api: &API, tag: String) -> Result, GenericError> { + let tags = api.get_tags()?; + let tag = find_tag(tag, &tags, &api.config)?; + Ok(api.get_states(&tag.get_id()?)?) +} + +fn exp_time(api: &API, reqs_left: u64) -> Duration { + let interval = 1_000 / api.config.throttle as u64; + Duration::from_millis(interval * reqs_left) +} + +fn find_tag( + tag_str: String, + tags: &Vec, + config: &Config, +) -> Result { + let mut tag = None; + if let Ok(num) = tag_str.parse::() { + if let Some(found) = tags.get((num - 1).max(0) as usize) { + tag = Some(found.clone()); + } + } + for curr in tags.iter() { + if let Some(nick) = curr.get_nick(config) { + if nick == tag_str { + tag = Some(curr.clone()); + } + } + } + match tag { + Some(tag) => Ok(tag), + None => Err(GenericError::MessagedError( + format!("Could not find device {}", tag_str), + None, + )), + } +} diff --git a/src/config.rs b/src/config.rs index 0fd9714..0d3989c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ pub struct Config { pub tags_url: String, pub states_url: String, pub locations_url: String, + pub current_locations_url: String, pub timestamp_format: String, pub timedate_format: String, @@ -50,6 +51,7 @@ impl Default for Config { tags_url: "https://platform.yepzon.com/tags".to_owned(), states_url: "https://platform.yepzon.com/tags/{tag}/states".to_owned(), locations_url: "https://platform.yepzon.com/tags/{tag}/locations/{state}".to_owned(), + current_locations_url: "https://platform.yepzon.com/tags/{tag}/locations".to_owned(), timestamp_format: "%Y-%m-%dT%H:%M:%S%.fZ".to_owned(), diff --git a/src/gpx.rs b/src/gpx.rs index c4b0186..dbd0d9d 100644 --- a/src/gpx.rs +++ b/src/gpx.rs @@ -1,13 +1,13 @@ use super::{Config, GenericError, LocationModel, TagModel}; -use chrono::offset::{LocalResult, TimeZone, Utc as UTC_ZONE}; +use chrono::offset::Utc as UTC_ZONE; use chrono::{DateTime, NaiveDateTime, Utc}; use geo_types::Point; use gpx::{Gpx, GpxVersion, Metadata, Person, Route, Track, TrackSegment, Waypoint}; use std::fs::File; pub fn generate_gpx( - tag: TagModel, - locations: Vec<(NaiveDateTime, LocationModel)>, + tag: &TagModel, + locations: &Vec<(NaiveDateTime, LocationModel)>, config: &Config, ) -> Result { let route = Route::default(); @@ -47,14 +47,14 @@ pub fn write_gpx(gpx: &Gpx) -> Result<(), GenericError> { Ok(()) } -fn get_waypoint(point: (NaiveDateTime, LocationModel)) -> Result { +fn get_waypoint(point: &(NaiveDateTime, LocationModel)) -> Result { let (naive_time, location) = point; let now = Utc::now(); - let time: DateTime = DateTime::from_utc(naive_time, now.offset().clone()); + let time: DateTime = DateTime::from_utc(*naive_time, now.offset().clone()); if let (Some(lat), Some(lon)) = (location.lat, location.lon) { let mut waypoint = Waypoint::new(Point::new(lon, lat)); waypoint.time = Some(time); - waypoint.source = location.loc_type; + waypoint.source = location.loc_type.as_ref().cloned(); Ok(waypoint) } else { Err(GenericError::MessagedError( diff --git a/src/main.rs b/src/main.rs index ea533d0..6182923 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,16 @@ mod api; -mod cmd; +mod args; +mod commands; mod config; mod errors; mod gpx; use api::{LocationModel, TagModel, API}; +use args::*; use chrono::offset::Local; use chrono::{NaiveDate, NaiveDateTime, NaiveTime, ParseError}; -use cmd::*; use config::Config; use errors::{GenericError, MessagedError}; -use std::io::prelude::*; -use std::sync::mpsc::TryRecvError; -use std::time::Duration; fn main() { let env: EnvOpt = argh::from_env(); @@ -26,6 +24,7 @@ fn from_env(env: EnvOpt) -> Result<(), GenericError> { match env.subcommand { Subcommand::Between(opt) => { let config = Config::from_path(&env.config)?; + let mut api = API::new(config.clone()); let since = Some(try_get_datetime(opt.since, &config).with_msg("Failed to parse since")?); @@ -37,76 +36,60 @@ fn from_env(env: EnvOpt) -> Result<(), GenericError> { None => None, }; - let (tag, locs) = run(&config, opt.device, since, until)?; + let (_, locs) = commands::between(&mut api, opt.device, since, until)?; dbg!( &locs.iter().map(|loc| loc.0).collect::>(), locs.len(), ); - let gpx = gpx::generate_gpx(tag, locs, &config)?; - gpx::write_gpx(&gpx)?; Ok(()) } Subcommand::Init(opt) => { - let mut config = Config::default(); - if let Some(api_key) = opt.api_key { - config.api_key = api_key; - } - config.write_to(&env.config)?; + commands::init(&env.config, opt.api_key)?; Ok(()) } + Subcommand::Api(_) => { + let config = Config::from_path(&env.config)?; + let api = API::new(config.clone()); + match commands::check_api(&api) { + Ok(_) => { + println!("API verified, no issues"); + Ok(()) + } + Err(e) => Err(GenericError::MessagedError( + "API integrity failed, or API-key is not valid. ".to_owned(), + Some(Box::new(e)), + )), + } + } Subcommand::Nick(opt) => { - let mut config = Config::from_path(&env.config)?; + let config = Config::from_path(&env.config)?; let mut api = API::new(config.clone()); - let mut tags = api.get_tags()?; - tags.sort_by(|tag1, tag2| tag1.id.cmp(&tag2.id)); - match opt.subcommand { NickSub::List(_) => { - for (idx, tag) in tags.iter().enumerate() { - println!( - "{}. {}\n{}", - idx + 1, - tag.get_nick(&config) - .unwrap_or("No current nick".to_owned()), - tag - ); + for ((idx, nick), tag) in commands::nick_list(&api)? { + println!("{}. {}\n{}", idx, nick, tag); } - Ok(()) } NickSub::Set(opt) => { - if let Some(tag) = tags.get((opt.index - 1).max(0) as usize) { - if let Some(id) = &tag.id { - config.nicknames.insert(id.clone(), opt.nickname); - config.write_to(&env.config)?; - Ok(()) - } else { - Err(GenericError::MessagedError( - format!("Device with index {} does not have an id", opt.index), - None, - )) - } - } else { - Err(GenericError::MessagedError( - format!("Could not find device with index {}", opt.index), - None, - )) - } + commands::nick_set(&mut api, &env.config, opt.device, opt.nickname)?; } } + Ok(()) } Subcommand::Get(opt) => { let config = Config::from_path(&env.config)?; - let mut api = API::new(config.clone()); - let tags = api.get_tags()?; + let api = API::new(config.clone()); match opt.subcommand { + GetSub::Tags(_) => { + let tags = api.get_tags()?; + dbg!(&tags); + } GetSub::States(opt) => { - let tag = find_tag(opt.device, &tags, &config)?; - dbg!(api.get_states(&tag.get_id()?)); + API::print_list(commands::get_states(&api, opt.device)); } GetSub::Locations(opt) => { - let tag = find_tag(opt.device, &tags, &config)?; - dbg!(api.get_locations(&tag.get_id()?, &opt.state)); + API::print_list(commands::get_locations(&api, opt.device, opt.state)) } } Ok(()) @@ -114,72 +97,6 @@ fn from_env(env: EnvOpt) -> Result<(), GenericError> { } } -fn run( - config: &Config, - tag_str: String, - from: Option, - to: Option, -) -> Result<(TagModel, Vec<(NaiveDateTime, LocationModel)>), GenericError> { - let mut api = API::new(config.clone()); - - let tags = api.get_tags()?; - let tag = find_tag(tag_str, &tags, config)?; - let tag_id = tag.get_id()?; - - print!("Preparing..\r"); - std::io::stdout().lock().flush().ok(); - - let state_list = api.get_states(&tag_id)?; - - let states = API::get_between(&state_list, from, to, true, &config); - - let mut locations = Vec::new(); - for (_, state) in states.iter() { - api.queue_location(&tag_id, state.id.as_ref().unwrap()); - } - let receiver = api.begin_location_fetch(); - let mut counter = 0; - loop { - match receiver.try_recv() { - Ok(res) => match res { - Ok(loc) => match loc { - Ok(mut loc) => { - counter += 1; - let remaining = exp_time(&api, states.len() as u64 - counter as u64); - print!( - "Done: {:<5.2}% Remaining: {:<5.2} seconds\r", - counter as f32 / states.len() as f32 * 100., - remaining.as_secs_f32() - ); - std::io::stdout().lock().flush().ok(); - locations.append(&mut loc); - } - Err(e) => eprintln!( - "Error fetching location data: {}", - e.message.unwrap_or(String::new()) - ), - }, - Err(e) => eprintln!("{}", e), - }, - Err(e) => { - if let TryRecvError::Disconnected = e { - break; - } - } - } - } - println!("{:<100}", "\nDone!"); - locations.sort_by(|loc1, loc2| loc2.timestamp.cmp(&loc1.timestamp)); - - let locs = API::get_between(&locations, from, to, false, &config); - Ok((tag, locs)) -} - -fn exp_time(api: &API, reqs_left: u64) -> Duration { - let interval = 1_000 / api.config.throttle as u64; - Duration::from_millis(interval * reqs_left) -} - fn try_get_datetime(parsed: String, config: &Config) -> Result { match NaiveDateTime::parse_from_str(&parsed, &config.timedate_format) { Ok(timedate) => Ok(timedate), @@ -192,30 +109,3 @@ fn try_get_datetime(parsed: String, config: &Config) -> Result, - config: &Config, -) -> Result { - let mut tag = None; - if let Ok(num) = tag_str.parse::() { - if let Some(found) = tags.get((num - 1).max(0) as usize) { - tag = Some(found.clone()); - } - } - for curr in tags.iter() { - if let Some(nick) = curr.get_nick(config) { - if nick == tag_str { - tag = Some(curr.clone()); - } - } - } - match tag { - Some(tag) => Ok(tag), - None => Err(GenericError::MessagedError( - format!("Could not find device {}", tag_str), - None, - )), - } -}