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 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);

View file

@ -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,

View file

@ -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<Self, Self::Err> {
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<Self, Self::Err> {
Ok(RefreshToken(s.to_string()))
}
}
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;
impl Api {
/// Currently the only way to "authenticate" to an access token for this library.
pub async fn from_interactive_url() -> Result<Api, TeslatteError> {
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<Api, TeslatteError> {
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<BearerTokenResponse, TeslatteError> {
@ -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<RefreshTokenResponse, TeslatteError> {
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<D, TeslatteError>
async fn auth_post<'a, S, D>(url: &str, payload: &S) -> Result<D, TeslatteError>
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,

View file

@ -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<RefreshToken>,
client: Client,
}
impl Api {
pub fn new(access_token: &AccessToken) -> Self {
pub fn new(access_token: AccessToken, refresh_token: Option<RefreshToken>) -> 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<S>(&self, path: &str, body: S) -> Result<(), TeslatteError>
async fn post<S>(&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")
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: request(),
request: req_ctx(),
})?;
let body = response
.text()
.await
.map_err(|source| TeslatteError::FetchError {
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);
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
}
};