refactor: merge Authentication + Api
This commit is contained in:
parent
ce3552cdc7
commit
e405ef83f8
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
79
src/auth.rs
79
src/auth.rs
|
@ -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,
|
||||||
|
|
80
src/lib.rs
80
src/lib.rs
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue