refactor: merge Authentication + Api

This commit is contained in:
gak 2022-07-25 13:47:43 +10:00
parent ce3552cdc7
commit e405ef83f8
4 changed files with 92 additions and 114 deletions

View file

@ -1,6 +1,6 @@
use clap::Parser; use clap::Parser;
use std::env; use std::env;
use teslatte::auth::{AccessToken, Authentication}; use teslatte::auth::AccessToken;
use teslatte::vehicles::SetChargeLimit; use teslatte::vehicles::SetChargeLimit;
use teslatte::Api; use teslatte::Api;
@ -8,19 +8,11 @@ use teslatte::Api;
async fn main() { async fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let access_token = match env::var("TESLA_ACCESS_TOKEN") { let api = match env::var("TESLA_ACCESS_TOKEN") {
Ok(t) => AccessToken(t), Ok(t) => Api::new(AccessToken(t), None),
Err(_) => { Err(_) => Api::from_interactive_url().await.unwrap(),
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 = Api::new(&access_token);
let vehicles = api.vehicles().await.unwrap(); let vehicles = api.vehicles().await.unwrap();
dbg!(&vehicles); dbg!(&vehicles);

View file

@ -5,7 +5,7 @@ use clap::{Args, Parser, Subcommand};
use cli_vehicle::VehicleArgs; use cli_vehicle::VehicleArgs;
use miette::{miette, IntoDiagnostic, WrapErr}; use miette::{miette, IntoDiagnostic, WrapErr};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use teslatte::auth::{AccessToken, Authentication, RefreshToken}; use teslatte::auth::{AccessToken, RefreshToken};
use teslatte::calendar_history::{CalendarHistoryValues, HistoryKind, HistoryPeriod}; use teslatte::calendar_history::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
use teslatte::energy::EnergySiteId; use teslatte::energy::EnergySiteId;
use teslatte::powerwall::{PowerwallEnergyHistoryValues, PowerwallId}; use teslatte::powerwall::{PowerwallEnergyHistoryValues, PowerwallId};
@ -179,9 +179,8 @@ async fn main() -> miette::Result<()> {
match args.command { match args.command {
Command::Auth { save } => { Command::Auth { save } => {
let auth = Authentication::new()?; let api = Api::from_interactive_url().await?;
let (access_token, refresh_token) = auth.interactive_get_access_token().await?; updated_tokens(save, &api);
updated_tokens(save, access_token, refresh_token);
} }
Command::Refresh { refresh_token } => { Command::Refresh { refresh_token } => {
let (save, refresh_token) = match refresh_token { let (save, refresh_token) = match refresh_token {
@ -192,20 +191,22 @@ async fn main() -> miette::Result<()> {
} }
}; };
let auth = Authentication::new()?; let api = Api::from_refresh_token(&refresh_token).await?;
let response = auth.refresh_access_token(&refresh_token).await?; updated_tokens(save, &api);
updated_tokens(save, response.access_token, refresh_token);
} }
Command::Api(api_args) => { Command::Api(api_args) => {
let access_token = match &api_args.access_token { let (access_token, refresh_token) = match &api_args.access_token {
Some(a) => a.clone(), Some(a) => (a.clone(), None),
None => { None => {
let config = Config::load(); 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 { match api_args.command {
ApiCommand::Vehicles => { ApiCommand::Vehicles => {
dbg!(api.vehicles().await?); dbg!(api.vehicles().await?);
@ -228,9 +229,11 @@ async fn main() -> miette::Result<()> {
Ok(()) Ok(())
} }
fn updated_tokens(save: bool, access_token: AccessToken, refresh_token: RefreshToken) { fn updated_tokens(save: bool, api: &Api) {
println!("Access token: {}", access_token.0); let access_token = api.access_token.clone();
println!("Refresh token: {}", refresh_token.0); let refresh_token = api.refresh_token.clone().unwrap();
println!("Access token: {}", access_token);
println!("Refresh token: {}", refresh_token);
if save { if save {
Config { Config {
access_token, access_token,

View file

@ -1,72 +1,45 @@
use crate::TeslatteError;
use crate::TeslatteError::UnhandledReqwestError; use crate::TeslatteError::UnhandledReqwestError;
use crate::{Api, TeslatteError};
use derive_more::{Display, FromStr};
use rand::Rng; use rand::Rng;
use reqwest::Client; use reqwest::Client;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::{stdin, stdout, Write}; use std::io::{stdin, stdout, Write};
use std::str::FromStr;
use url::Url; use url::Url;
const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize"; const AUTHORIZE_URL: &str = "https://auth.tesla.com/oauth2/v3/authorize";
const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token"; const TOKEN_URL: &str = "https://auth.tesla.com/oauth2/v3/token";
pub struct Authentication { #[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)]
client: Client,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessToken(pub String); pub struct AccessToken(pub String);
impl FromStr for AccessToken { #[derive(Debug, Clone, Serialize, Deserialize, FromStr, Display)]
type Err = TeslatteError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(AccessToken(s.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefreshToken(pub String); pub struct RefreshToken(pub String);
impl FromStr for RefreshToken { impl Api {
type Err = TeslatteError; /// Currently the only way to "authenticate" to an access token for this library.
fn from_str(s: &str) -> Result<Self, Self::Err> { pub async fn from_interactive_url() -> Result<Api, TeslatteError> {
Ok(RefreshToken(s.to_string())) let login_form = Self::get_login_url_for_user().await;
}
}
impl Authentication {
pub fn new() -> Result<Self, TeslatteError> {
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;
dbg!(&login_form); dbg!(&login_form);
let callback_url = let callback_url =
ask_input("Enter the URL of the 404 error page after you've logged in: "); 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 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 pub async fn from_refresh_token(refresh_token: &RefreshToken) -> Result<Api, TeslatteError> {
.exchange_auth_for_bearer(&login_form.code, &callback_code) let response = Self::refresh_token(&refresh_token).await?;
.await?; Ok(Api::new(
let refresh_token = bearer.refresh_token.clone(); response.access_token,
Some(response.refresh_token),
Ok((
AccessToken(bearer.access_token),
RefreshToken(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 code = Code::new();
let state = random_string(8); let state = random_string(8);
let url = Self::login_url(&code, &state); let url = Self::login_url(&code, &state);
@ -74,7 +47,6 @@ impl Authentication {
} }
async fn exchange_auth_for_bearer( async fn exchange_auth_for_bearer(
&self,
code: &Code, code: &Code,
callback_code: &str, callback_code: &str,
) -> Result<BearerTokenResponse, TeslatteError> { ) -> Result<BearerTokenResponse, TeslatteError> {
@ -86,11 +58,10 @@ impl Authentication {
code_verifier: code.verifier.clone(), code_verifier: code.verifier.clone(),
redirect_uri: "https://auth.tesla.com/void/callback".into(), 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( pub async fn refresh_token(
&self,
refresh_token: &RefreshToken, refresh_token: &RefreshToken,
) -> Result<RefreshTokenResponse, TeslatteError> { ) -> Result<RefreshTokenResponse, TeslatteError> {
let url = "https://auth.tesla.com/oauth2/v3/token"; let url = "https://auth.tesla.com/oauth2/v3/token";
@ -100,16 +71,15 @@ impl Authentication {
refresh_token: refresh_token.0.clone(), refresh_token: refresh_token.0.clone(),
scope: "openid email offline_access".into(), 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<D, TeslatteError> async fn auth_post<'a, S, D>(url: &str, payload: &S) -> Result<D, TeslatteError>
where where
S: Serialize, S: Serialize,
D: DeserializeOwned, D: DeserializeOwned,
{ {
let response = self let response = Client::new()
.client
.post(url) .post(url)
.header("Accept", "application/json") .header("Accept", "application/json")
.json(payload) .json(payload)
@ -187,7 +157,6 @@ pub struct LoginForm {
state: String, state: String,
} }
// These can be probably &str.
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct BearerTokenRequest { struct BearerTokenRequest {
grant_type: String, grant_type: String,

View file

@ -1,4 +1,4 @@
use crate::auth::AccessToken; use crate::auth::{AccessToken, RefreshToken};
use crate::error::TeslatteError; use crate::error::TeslatteError;
use chrono::{DateTime, SecondsFormat, TimeZone}; use chrono::{DateTime, SecondsFormat, TimeZone};
use derive_more::{Display, FromStr}; use derive_more::{Display, FromStr};
@ -15,7 +15,7 @@ pub mod error;
pub mod powerwall; pub mod powerwall;
pub mod vehicles; 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 { trait Values {
fn format(&self, url: &str) -> String; fn format(&self, url: &str) -> String;
@ -34,14 +34,16 @@ pub struct VehicleId(u64);
pub struct ExternalVehicleId(u64); pub struct ExternalVehicleId(u64);
pub struct Api { pub struct Api {
access_token: AccessToken, pub access_token: AccessToken,
pub refresh_token: Option<RefreshToken>,
client: Client, client: Client,
} }
impl Api { impl Api {
pub fn new(access_token: &AccessToken) -> Self { pub fn new(access_token: AccessToken, refresh_token: Option<RefreshToken>) -> Self {
Api { Api {
access_token: access_token.clone(), access_token,
refresh_token,
client: Client::builder() client: Client::builder()
.timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(10))
.build() .build()
@ -86,44 +88,47 @@ impl Api {
} }
#[instrument(skip(self))] #[instrument(skip(self))]
async fn post<S>(&self, path: &str, body: S) -> Result<(), TeslatteError> async fn post<S>(&self, url: &str, body: S) -> Result<(), TeslatteError>
where where
S: Serialize + Debug, S: Serialize + Debug,
{ {
trace!("Fetching"); trace!("Fetching");
let url = format!("{}{}", API_URL, path); let req_ctx = || {
let request = || {
let payload = let payload =
serde_json::to_string(&body).expect("Should not fail creating the request struct."); serde_json::to_string(&body).expect("Should not fail creating the request struct.");
format!("POST {} {payload}", &url) format!("POST {} {payload}", &url)
}; };
let response = self
.client let mut request = self.client.post(url).header("Accept", "application/json");
.post(&url) let auth = true;
.header("Authorization", format!("Bearer {}", self.access_token.0)) if auth {
.header("Accept", "application/json") request = request.header("Authorization", format!("Bearer {}", self.access_token.0));
.json(&body) }
.send() let response =
.await request
.map_err(|source| TeslatteError::FetchError { .json(&body)
source, .send()
request: request(), .await
})?; .map_err(|source| TeslatteError::FetchError {
source,
request: req_ctx(),
})?;
let body = response let body = response
.text() .text()
.await .await
.map_err(|source| TeslatteError::FetchError { .map_err(|source| TeslatteError::FetchError {
source, source,
request: request(), request: req_ctx(),
})?; })?;
let json = Self::parse_json::<PostResponse, _>(&body, request)?; let json = Self::parse_json::<PostResponse, _>(&body, req_ctx)?;
trace!(?json); trace!(?json);
if json.result { if json.result {
Ok(()) Ok(())
} else { } else {
Err(TeslatteError::ServerError { Err(TeslatteError::ServerError {
request: request(), request: req_ctx(),
msg: json.reason, msg: json.reason,
description: None, description: None,
}) })
@ -198,8 +203,8 @@ struct Empty {}
/// GET /api/1/[url] /// GET /api/1/[url]
macro_rules! get { macro_rules! get {
($name:ident, $return_type:ty, $url:expr) => { ($name:ident, $return_type:ty, $url:expr) => {
pub async fn $name(&self) -> Result<$return_type, TeslatteError> { pub async fn $name(&self) -> Result<$return_type, crate::error::TeslatteError> {
let url = format!("/api/1{}", $url); let url = format!("{}{}", crate::API_URL, $url);
self.get(&url).await 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. /// Pass in the URL as a format string with one arg, which has to impl Display.
macro_rules! get_arg { macro_rules! get_arg {
($name:ident, $return_type:ty, $url:expr, $arg_type:ty) => { ($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!($url, arg);
let url = format!("/api/1{}", url); let url = format!("{}{}", crate::API_URL, url);
self.get(&url).await self.get(&url).await
} }
}; };
@ -223,9 +231,12 @@ pub(crate) use get_arg;
/// GET /api/1/[url] with a struct. /// GET /api/1/[url] with a struct.
macro_rules! get_args { macro_rules! get_args {
($name:ident, $return_type:ty, $url:expr, $args:ty) => { ($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 = values.format($url);
let url = format!("/api/1{}", url); let url = format!("{}{}", crate::API_URL, url);
self.get(&url).await self.get(&url).await
} }
}; };
@ -239,9 +250,9 @@ macro_rules! post_arg {
&self, &self,
arg: &$arg_type, arg: &$arg_type,
data: &$request_type, data: &$request_type,
) -> miette::Result<(), TeslatteError> { ) -> miette::Result<(), crate::error::TeslatteError> {
let url = format!($url, arg); let url = format!($url, arg);
let url = format!("/api/1{}", url); let url = format!("{}{}", crate::API_URL, url);
self.post(&url, data).await 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. /// Post like above but with an empty body using the Empty struct.
macro_rules! post_arg_empty { macro_rules! post_arg_empty {
($name:ident, $url:expr, $arg_type:ty) => { ($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!($url, arg);
let url = format!("/api/1{}", url); let url = format!("{}{}", crate::API_URL, url);
self.post(&url, &Empty {}).await self.post(&url, &Empty {}).await
} }
}; };