diff --git a/examples/basic.rs b/examples/basic.rs index 203a6e1..48a004c 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,6 +1,6 @@ use clap::Parser; use std::env; -use teslatte::auth::{AccessToken, Authentication}; +use teslatte::auth::AccessToken; use teslatte::vehicles::SetChargeLimit; use teslatte::Api; @@ -8,19 +8,11 @@ use teslatte::Api; async fn main() { tracing_subscriber::fmt::init(); - let access_token = match env::var("TESLA_ACCESS_TOKEN") { - Ok(t) => AccessToken(t), - Err(_) => { - let auth = Authentication::new().unwrap(); - let (access_token, refresh_token) = auth.interactive_get_access_token().await.unwrap(); - println!("Access token: {}", access_token.0); - println!("Refresh token: {}", refresh_token.0); - access_token - } + let api = match env::var("TESLA_ACCESS_TOKEN") { + Ok(t) => Api::new(AccessToken(t), None), + Err(_) => Api::from_interactive_url().await.unwrap(), }; - let api = Api::new(&access_token); - let vehicles = api.vehicles().await.unwrap(); dbg!(&vehicles); diff --git a/examples/cli.rs b/examples/cli.rs index 6709245..43e2a8d 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -5,7 +5,7 @@ use clap::{Args, Parser, Subcommand}; use cli_vehicle::VehicleArgs; use miette::{miette, IntoDiagnostic, WrapErr}; use serde::{Deserialize, Serialize}; -use teslatte::auth::{AccessToken, Authentication, RefreshToken}; +use teslatte::auth::{AccessToken, RefreshToken}; use teslatte::calendar_history::{CalendarHistoryValues, HistoryKind, HistoryPeriod}; use teslatte::energy::EnergySiteId; use teslatte::powerwall::{PowerwallEnergyHistoryValues, PowerwallId}; @@ -179,9 +179,8 @@ async fn main() -> miette::Result<()> { match args.command { Command::Auth { save } => { - let auth = Authentication::new()?; - let (access_token, refresh_token) = auth.interactive_get_access_token().await?; - updated_tokens(save, access_token, refresh_token); + let api = Api::from_interactive_url().await?; + updated_tokens(save, &api); } Command::Refresh { refresh_token } => { let (save, refresh_token) = match refresh_token { @@ -192,20 +191,22 @@ async fn main() -> miette::Result<()> { } }; - let auth = Authentication::new()?; - let response = auth.refresh_access_token(&refresh_token).await?; - updated_tokens(save, response.access_token, refresh_token); + let api = Api::from_refresh_token(&refresh_token).await?; + updated_tokens(save, &api); } Command::Api(api_args) => { - let access_token = match &api_args.access_token { - Some(a) => a.clone(), + let (access_token, refresh_token) = match &api_args.access_token { + Some(a) => (a.clone(), None), None => { let config = Config::load(); - config.access_token.clone() + ( + config.access_token.clone(), + Some(config.refresh_token.clone()), + ) } }; - let api = Api::new(&access_token); + let api = Api::new(access_token, refresh_token); match api_args.command { ApiCommand::Vehicles => { dbg!(api.vehicles().await?); @@ -228,9 +229,11 @@ async fn main() -> miette::Result<()> { Ok(()) } -fn updated_tokens(save: bool, access_token: AccessToken, refresh_token: RefreshToken) { - println!("Access token: {}", access_token.0); - println!("Refresh token: {}", refresh_token.0); +fn updated_tokens(save: bool, api: &Api) { + let access_token = api.access_token.clone(); + let refresh_token = api.refresh_token.clone().unwrap(); + println!("Access token: {}", access_token); + println!("Refresh token: {}", refresh_token); if save { Config { access_token, diff --git a/src/auth.rs b/src/auth.rs index 595ce9e..b99234c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,72 +1,45 @@ -use crate::TeslatteError; use crate::TeslatteError::UnhandledReqwestError; +use crate::{Api, TeslatteError}; +use derive_more::{Display, FromStr}; use rand::Rng; use reqwest::Client; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::io::{stdin, stdout, Write}; -use std::str::FromStr; use url::Url; const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize"; const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token"; -pub struct Authentication { - client: Client, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)] pub struct AccessToken(pub String); -impl FromStr for AccessToken { - type Err = TeslatteError; - fn from_str(s: &str) -> Result { - Ok(AccessToken(s.to_string())) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)] pub struct RefreshToken(pub String); -impl FromStr for RefreshToken { - type Err = TeslatteError; - fn from_str(s: &str) -> Result { - Ok(RefreshToken(s.to_string())) - } -} - -impl Authentication { - pub fn new() -> Result { - let client = Client::builder() - .cookie_store(false) - .build() - .map_err(UnhandledReqwestError)?; - Ok(Self { client }) - } - - /// Currently the only way to get an access token via this library. - pub async fn interactive_get_access_token( - &self, - ) -> Result<(AccessToken, RefreshToken), TeslatteError> { - let login_form = self.get_login_url_for_user().await; +impl Api { + /// Currently the only way to "authenticate" to an access token for this library. + pub async fn from_interactive_url() -> Result { + let login_form = Self::get_login_url_for_user().await; dbg!(&login_form); - let callback_url = ask_input("Enter the URL of the 404 error page after you've logged in: "); let callback_code = Self::extract_callback_code_from_url(&callback_url)?; + let bearer = Self::exchange_auth_for_bearer(&login_form.code, &callback_code).await?; + let access_token = AccessToken(bearer.access_token); + let refresh_token = RefreshToken(bearer.refresh_token); + Ok(Api::new(access_token, Some(refresh_token))) + } - let bearer = self - .exchange_auth_for_bearer(&login_form.code, &callback_code) - .await?; - let refresh_token = bearer.refresh_token.clone(); - - Ok(( - AccessToken(bearer.access_token), - RefreshToken(refresh_token), + pub async fn from_refresh_token(refresh_token: &RefreshToken) -> Result { + let response = Self::refresh_token(&refresh_token).await?; + Ok(Api::new( + response.access_token, + Some(response.refresh_token), )) } - pub async fn get_login_url_for_user(&self) -> LoginForm { + pub async fn get_login_url_for_user() -> LoginForm { let code = Code::new(); let state = random_string(8); let url = Self::login_url(&code, &state); @@ -74,7 +47,6 @@ impl Authentication { } async fn exchange_auth_for_bearer( - &self, code: &Code, callback_code: &str, ) -> Result { @@ -86,11 +58,10 @@ impl Authentication { code_verifier: code.verifier.clone(), redirect_uri: "https://auth.tesla.com/void/callback".into(), }; - self.post(url, &payload).await + Self::auth_post(url, &payload).await } - pub async fn refresh_access_token( - &self, + pub async fn refresh_token( refresh_token: &RefreshToken, ) -> Result { let url = "https://auth.tesla.com/oauth2/v3/token"; @@ -100,16 +71,15 @@ impl Authentication { refresh_token: refresh_token.0.clone(), scope: "openid email offline_access".into(), }; - self.post(url, &payload).await + Self::auth_post(url, &payload).await } - async fn post<'a, S, D>(&self, url: &str, payload: &S) -> Result + async fn auth_post<'a, S, D>(url: &str, payload: &S) -> Result where S: Serialize, D: DeserializeOwned, { - let response = self - .client + let response = Client::new() .post(url) .header("Accept", "application/json") .json(payload) @@ -187,7 +157,6 @@ pub struct LoginForm { state: String, } -// These can be probably &str. #[derive(Debug, Serialize)] struct BearerTokenRequest { grant_type: String, diff --git a/src/lib.rs b/src/lib.rs index a6aa52f..5ca0311 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use crate::auth::AccessToken; +use crate::auth::{AccessToken, RefreshToken}; use crate::error::TeslatteError; use chrono::{DateTime, SecondsFormat, TimeZone}; use derive_more::{Display, FromStr}; @@ -15,7 +15,7 @@ pub mod error; pub mod powerwall; pub mod vehicles; -const API_URL: &str = "https://owner-api.teslamotors.com"; +const API_URL: &str = "https://owner-api.teslamotors.com/api/1"; trait Values { fn format(&self, url: &str) -> String; @@ -34,14 +34,16 @@ pub struct VehicleId(u64); pub struct ExternalVehicleId(u64); pub struct Api { - access_token: AccessToken, + pub access_token: AccessToken, + pub refresh_token: Option, client: Client, } impl Api { - pub fn new(access_token: &AccessToken) -> Self { + pub fn new(access_token: AccessToken, refresh_token: Option) -> Self { Api { - access_token: access_token.clone(), + access_token, + refresh_token, client: Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() @@ -86,44 +88,47 @@ impl Api { } #[instrument(skip(self))] - async fn post(&self, path: &str, body: S) -> Result<(), TeslatteError> + async fn post(&self, url: &str, body: S) -> Result<(), TeslatteError> where S: Serialize + Debug, { trace!("Fetching"); - let url = format!("{}{}", API_URL, path); - let request = || { + let req_ctx = || { let payload = serde_json::to_string(&body).expect("Should not fail creating the request struct."); format!("POST {} {payload}", &url) }; - let response = self - .client - .post(&url) - .header("Authorization", format!("Bearer {}", self.access_token.0)) - .header("Accept", "application/json") - .json(&body) - .send() - .await - .map_err(|source| TeslatteError::FetchError { - source, - request: request(), - })?; + + let mut request = self.client.post(url).header("Accept", "application/json"); + let auth = true; + if auth { + request = request.header("Authorization", format!("Bearer {}", self.access_token.0)); + } + let response = + request + .json(&body) + .send() + .await + .map_err(|source| TeslatteError::FetchError { + source, + request: req_ctx(), + })?; + let body = response .text() .await .map_err(|source| TeslatteError::FetchError { source, - request: request(), + request: req_ctx(), })?; - let json = Self::parse_json::(&body, request)?; + let json = Self::parse_json::(&body, req_ctx)?; trace!(?json); if json.result { Ok(()) } else { Err(TeslatteError::ServerError { - request: request(), + request: req_ctx(), msg: json.reason, description: None, }) @@ -198,8 +203,8 @@ struct Empty {} /// GET /api/1/[url] macro_rules! get { ($name:ident, $return_type:ty, $url:expr) => { - pub async fn $name(&self) -> Result<$return_type, TeslatteError> { - let url = format!("/api/1{}", $url); + pub async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> { + let url = format!("{}{}", crate::API_URL, $url); self.get(&url).await } }; @@ -211,9 +216,12 @@ pub(crate) use get; /// Pass in the URL as a format string with one arg, which has to impl Display. macro_rules! get_arg { ($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => { - pub async fn $name(&self, arg: &$arg_type) -> miette::Result<$return_type, TeslatteError> { + pub async fn $name( + &self, + arg: &$arg_type, + ) -> miette::Result<$return_type, crate::error::TeslatteError> { let url = format!($url, arg); - let url = format!("/api/1{}", url); + let url = format!("{}{}", crate::API_URL, url); self.get(&url).await } }; @@ -223,9 +231,12 @@ pub(crate) use get_arg; /// GET /api/1/[url] with a struct. macro_rules! get_args { ($name:ident, $return_type:ty, $url:expr, $args:ty) => { - pub async fn $name(&self, values: &$args) -> miette::Result<$return_type, TeslatteError> { + pub async fn $name( + &self, + values: &$args, + ) -> miette::Result<$return_type, crate::error::TeslatteError> { let url = values.format($url); - let url = format!("/api/1{}", url); + let url = format!("{}{}", crate::API_URL, url); self.get(&url).await } }; @@ -239,9 +250,9 @@ macro_rules! post_arg { &self, arg: &$arg_type, data: &$request_type, - ) -> miette::Result<(), TeslatteError> { + ) -> miette::Result<(), crate::error::TeslatteError> { let url = format!($url, arg); - let url = format!("/api/1{}", url); + let url = format!("{}{}", crate::API_URL, url); self.post(&url, data).await } }; @@ -251,9 +262,12 @@ pub(crate) use post_arg; /// Post like above but with an empty body using the Empty struct. macro_rules! post_arg_empty { ($name:ident, $url:expr, $arg_type:ty) => { - pub async fn $name(&self, arg: &$arg_type) -> miette::Result<(), TeslatteError> { + pub async fn $name( + &self, + arg: &$arg_type, + ) -> miette::Result<(), crate::error::TeslatteError> { let url = format!($url, arg); - let url = format!("/api/1{}", url); + let url = format!("{}{}", crate::API_URL, url); self.post(&url, &Empty {}).await } };