Compare commits

..

No commits in common. "609fc6581697ab82876bd2712c52467ae6e7ea0e" and "a1089b5af7db4f8c87f48a0ee1953b6c41a6c019" have entirely different histories.

14 changed files with 3910 additions and 803 deletions

3752
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -19,7 +19,6 @@ secrecy = { version = "0.8.0", features = ["serde"] }
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
serde_repr = { version = "0.1.18", optional = true }
serde_ignored = "0.1"
url = "2.5.0"
base64 = "0.22"
@ -36,12 +35,9 @@ thiserror = "1.0.56"
[features]
default = [
"advices",
"locker_all",
"unstable",
"locker_all"
]
unstable = []
advices = [
#"dep:sha2",
"dep:uuid",

View file

@ -10,13 +10,6 @@ This is an unofficial client to various DHL APIs, more specific the ones that th
- app-driven parcel lockers (App-gesteuerte Packstation)
## Deprecation notice
It's recommended that consumers of this crate are always tracking the latest version. If the upstream API changes too much or a better API here is created, there are two ways to mark a function or struct deprecated:
- Soft deprecation/Crate API changes: The normal deprecation notice via `#[deprecated]` (see the [RFC#1270](https://github.com/rust-lang/rfcs/blob/master/text/1270-deprecation.md) or the [rust reference docs](https://doc.rust-lang.org/reference/attributes/diagnostics.html)).
- (Fatal) Upstream API Changes: The soft deprecation marked with `deny(deprecated)`, you can try to override this, but functions with a `LibraryResult` will always return `LibraryError::Deprecated`. (NOTE: `LibrarayError::APIChange` is reserved for the case where the API-change is unknown. Please upgrade to a or wait for a newer crate version.)
## Examples
In the examples error-handling is ignored for simplicity. You dont want to do that.

View file

@ -1,10 +1,10 @@
pub use super::Advice;
use super::AdvicesResponse;
use crate::constants::webview_user_agent;
use crate::LibraryResult;
use reqwest::header::HeaderMap;
use serde::Serialize;
use crate::constants::webview_user_agent;
use crate::LibraryResult;
pub struct AdviceClient {
client: reqwest::Client,
@ -27,10 +27,7 @@ impl AdviceClient {
pub async fn access_token<'t>(&self, advices: &AdvicesResponse) -> LibraryResult<UatToken> {
mini_assert_inval!(advices.has_any_advices());
mini_assert_api_eq!(
advices.access_token_url.as_ref().unwrap().as_str(),
endpoint_access_tokens()
);
mini_assert_api_eq!(advices.access_token_url.as_ref().unwrap().as_str(), endpoint_access_tokens());
let req = self
.client
@ -49,13 +46,10 @@ impl AdviceClient {
let res = res.unwrap();
for cookie in res.cookies() {
println!("UAT: cookie: {:?}={:?}", cookie.name(), cookie.value());
if cookie.name() == "AccessToken" {
if cookie.name() == "UAT" {
return Ok(UatToken(cookie.value().to_string()));
}
}
println!("UAT: headers: {:?}", res.headers());
println!("UAT: text: {:?}", res.text().await);
// FIXME: Parse errors here better (checking if we're unauthorized,...)
panic!("NO UAT Token in access_token");
}
@ -92,10 +86,9 @@ impl AdviceClient {
)
};
let req = self
.client
let req = self.client
.get(&advice.image_url)
.header("Cookie", format!("AccessToken={}", uat.0))
.header("Cookie", format!("UAT={}", uat.0))
.build()
.unwrap();
let res = self.client.execute(req).await;
@ -157,5 +150,5 @@ fn headers() -> HeaderMap {
}
pub fn endpoint_access_tokens() -> &'static str {
"https://briefankuendigung.enplify.dhl.de/pdapp-web/access-tokens"
"https://briefankuendigung.dhl.de/pdapp-web/access-tokens"
}

View file

@ -29,13 +29,18 @@ newtype! {
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AdvicesResponse {
// access_token_url, basic_auth, grant_token is null if no advices are available
#[serde(rename = "accessTokenUrl")]
pub(super) access_token_url: Option<AdviceAccessTokenUrl>,
#[serde(rename = "basicAuth")]
pub(super) basic_auth: Option<String>,
#[serde(rename = "currentAdvice")]
current_advice: Option<AdvicesList>,
#[serde(rename = "grantToken")]
pub(super) grant_token: Option<String>,
#[serde(rename = "oldAdvices")]
old_advices: Vec<AdvicesList>,
}
@ -68,11 +73,33 @@ fn endpoint_advices() -> url::Url {
impl crate::www::WebClient {
// FIXME: more error parsing
pub async fn advices(&self, dhli: &crate::login::DHLIdToken) -> crate::LibraryResult<AdvicesResponse> {
let res = request!(self.web_client, endpoint_advices,
header("Cookie", CookieHeaderValueBuilder::new().add_dhli(dhli).add_dhlcs(dhli).build_string())
);
let cookie_headervalue = CookieHeaderValueBuilder::new()
.add_dhli(dhli)
.add_dhlcs(dhli)
.build_string();
let res = parse_json_response!(res, AdvicesResponse);
let req = self.web_client
.get(endpoint_advices().clone())
.header("Cookie", cookie_headervalue)
.build()
.unwrap();
let res = self.web_client.execute(req).await;
if let Err(err) = res {
return Err(err.into())
}
let res = res.unwrap();
let res_text = res.text().await.unwrap();
let res = serde_json::from_str::<AdvicesResponse>(&res_text);
let res = match res {
Ok(res) => res,
Err(err) => {
return Err(err.into())
}
};
if res.access_token_url.is_some() {
if res.access_token_url.as_ref().unwrap().as_str() != crate::advices::endpoint_access_tokens() {

View file

@ -1,6 +1,3 @@
#[macro_use]
mod utils;
mod www;
pub use www::WebClient;
@ -11,6 +8,8 @@ pub mod stammdaten;
pub use stammdaten::StammdatenClient;
mod common;
#[macro_use]
mod utils;
pub mod constants;
#[cfg(feature = "locker_base")]
@ -21,9 +20,6 @@ pub mod advices;
#[cfg(feature = "advices")]
pub use advices::AdviceClient;
#[cfg(feature = "unstable")]
pub mod tracking;
/*#[cfg(test)]
pub(crate) mod private;*/
@ -41,8 +37,6 @@ pub enum LibraryError {
DecodeError(String),
#[error("upstream api was changed. not continuing")]
APIChange,
#[error("upstream api was changed. this method is deprecated")]
Deprecated,
}
pub type LibraryResult<T> = Result<T, LibraryError>;

View file

@ -27,7 +27,8 @@ fn headers() -> HeaderMap {
/* ("accept", "application/json") */
("app-version", app_version()),
("device-os", "Android"),
("device-key", ""), /* is the android id... */
("device-key", "") /* is the android id... */
];
let mut map = HeaderMap::new();

View file

@ -6,10 +6,9 @@ use ed25519_dalek::Signer;
pub struct CustomerKeySeed {
pub postnumber: String,
pub seed: Seed,
pub uuid: Uuid,
pub device_id: Option<String>,
postnumber: String,
seed: Seed,
uuid: Uuid,
}
pub struct Seed {
@ -45,25 +44,17 @@ impl CustomerKeySeed {
postnumber,
seed: Seed::random(),
uuid: uuid::Uuid::new_v4(),
device_id: None,
}
}
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid, device_id: String) -> Self {
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid) -> Self {
CustomerKeySeed {
postnumber: postnumber.clone(),
seed: Seed::from_bytes(seed),
uuid: uuid.clone(),
device_id: Some(device_id),
uuid: uuid.clone()
}
}
pub fn set_device_id(&mut self, device_id: String) {
assert!(self.device_id.is_none());
self.device_id = Some(device_id);
}
pub(crate) fn sign(&self, message: &[u8]) -> String {
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());

View file

@ -4,13 +4,14 @@ mod openid_response;
pub mod openid_token;
mod utils;
pub use self::dhl_claims::{DHLCs, DHLIdToken};
pub use self::dhl_claims::{DHLIdToken, DHLCs};
pub use self::openid_response::{RefreshToken, TokenResponse};
pub use self::utils::{create_nonce, CodeVerfier};
pub use self::utils::{CodeVerfier, create_nonce};
use super::common::APIResult;
use crate::{LibraryError, LibraryResult};
pub struct OpenIdClient {
client: reqwest::Client,
}
@ -30,12 +31,27 @@ impl OpenIdClient {
&self,
refresh_token: &RefreshToken,
) -> LibraryResult<TokenResponse> {
let res = request_post!(self.client,
(constants::token::endpoint()),
let req = self
.client
.post(constants::token::endpoint())
.form(constants::token::refresh_token_form(refresh_token.as_str()).as_slice())
);
.build()
.unwrap();
Ok(parse_json_response_from_apiresult!(res, TokenResponse))
let res = self.client.execute(req).await;
match res {
Ok(res) => {
let res = res.text().await.unwrap();
let res = serde_json::from_str::<APIResult<TokenResponse>>(&res);
match res {
Ok(res) => return res.into(),
Err(err) => Err(LibraryError::from(err)),
}
}
Err(err) => Err(LibraryError::from(err)),
}
}
pub async fn token_authorization(
@ -62,9 +78,23 @@ impl OpenIdClient {
let headermap = req.headers_mut();
headermap.append("Content-Length", len.into());
}
let res = self.client.execute(req).await;
let res = parse_response_internal!(res);
Ok(parse_json_response_from_apiresult!(res, TokenResponse))
let res = self.client.execute(req).await;
println!("auth_code: {:?}", res);
match res {
Ok(res) => {
let res = res.text().await.unwrap();
println!("auth_code reply: {:?}", res);
let res = serde_json::from_str::<APIResult<TokenResponse>>(&res);
match res {
Ok(res) => res.into(),
Err(err) => Err(LibraryError::from(err)),
}
}
Err(err) => Err(LibraryError::from(err)),
}
}
}

View file

@ -1,444 +0,0 @@
use serde::{Deserialize, Serialize};
use crate::{login::DHLIdToken, LibraryResult};
#[derive(Default)]
pub struct TrackingParams {
pub language: Option<String>,
}
#[derive(Serialize)]
pub struct ShipmentRequest {
international: bool,
piececode: String,
zip: String,
}
impl crate::WebClient {
pub async fn tracking_search(
&self,
params: TrackingParams,
ids: Vec<String>,
dhli: Option<&DHLIdToken>,
) -> LibraryResult<Vec<Shipment>> {
let api_key = crate::www::api_key_header();
let res = if let Some(dhli) = dhli {
let cookie_value = crate::utils::CookieHeaderValueBuilder::new()
.add_dhli(&dhli)
.add_dhlcs(&dhli)
.build_string();
request!(
self.web_client,
endpoint_data_search,
query(&query_parameters_data_search(&params, ids)),
header(api_key.0, api_key.1),
header("Cookie", cookie_value)
)
} else {
request!(
self.web_client,
endpoint_data_search,
query(&query_parameters_data_search(&params, ids)),
header(api_key.0, api_key.1)
)
};
let resp = parse_json_response!(res, Response);
resp.into()
}
pub async fn tracking_shipment(
&self,
params: TrackingParams,
body: ShipmentRequest,
) -> LibraryResult<Vec<Shipment>> {
let api_key = crate::www::api_key_header();
let res = request_json!(
self.web_client,
endpoint_data_shipment,
body,
query(&query_parameters_data_shipment(&params)),
header(api_key.0, api_key.1)
);
let resp = parse_json_response!(res, Response);
resp.into()
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct SendungEmpfaenger {
name: String,
ort: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendungsInfo {
gesuchte_sendungsnummer: String,
sendungsrichtung: String,
sendungsname: Option<String>,
sendungsliste: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendungsVerlaufEvent {
pub datum: String,
ort: Option<String>,
pub ruecksendung: bool,
pub status: String,
}
impl SendungsVerlaufEvent {
fn get_ort(&self) -> Option<String> {
if let Some(ort) = self.ort.as_ref() {
if ort.len() > 0 {
return Some(ort.clone());
}
}
None
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendungsVerlauf {
kurz_status: Option<String>,
icon_id: Option<String>,
datum_aktueller_status: Option<String>,
aktueller_status: Option<String>,
events: Option<Vec<SendungsVerlaufEvent>>,
farbe: u32,
fortschritt: u32,
maximal_fortschritt: u32,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendungZustellung {
abholcode_available: Option<bool>, // probably always there
benachrichtigt_in_filiale: Option<bool>, // probably always there
zustellzeitfenster_bis: Option<String>,
zustellzeitfenster_von: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendungServices {
statusbenachrichtigung: SendungServiceStatusBenachrichtigung,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendungServiceStatusBenachrichtigung {
aktueller_status: Option<bool>,
erfolgte_zustellung: Option<bool>,
geplante_zustellung: Option<bool>,
authentication_required: Option<bool>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum SendungsQuelle {
TTBRIEF,
PAKET,
SVB,
OPTIMA,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Nachhaltigkeitsstatus {
bahnpaket: bool,
co2_freie_zustellung: bool,
gg_versender: bool,
ggp_empfaenger: bool,
ggp_versender: bool,
klimafreundlicher_empfang: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendungDetails {
quelle: SendungsQuelle, // PAKET
express_sendung: Option<bool>,
is_shipper_plz: Option<bool>,
ist_zugestellt: Option<bool>,
retoure: Option<bool>,
ruecksendung: Option<bool>,
sendungsverlauf: SendungsVerlauf,
show_quality_level_hint: Option<bool>,
two_man_handling: Option<bool>,
unplausibel: Option<bool>,
mehr_informationen_verfuegbar: Option<bool>,
nachhaltigkeitsstatus: Option<Nachhaltigkeitsstatus>,
brief_sendung: Option<bool>,
invalid_time_of_day: Option<bool>,
bahnpaket: Option<bool>,
email: Option<String>,
international: Option<bool>,
pan_empfaenger: Option<SendungEmpfaenger>,
produkt_name: Option<String>,
//sendungsnummern: (),
services: Option<SendungServiceStatusBenachrichtigung>,
show_digital_notification_cta_hint: Option<bool>,
warenpost: Option<bool>,
zielland: Option<String>, // localized string?
zustellung: Option<SendungZustellung>,
}
#[derive(Debug)]
pub struct ShipmentNotFoundError {
pub no_data_available: bool,
pub not_from_dhl: bool,
pub id_invalid: bool,
pub letter_not_found: bool,
pub data_to_old: bool,
pub id_not_searchable: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendungNichtGefunden {
keine_daten_verfuegbar: Option<bool>,
keine_dhl_paket_sendung: Option<bool>,
sendungsnummer_ungueltig: Option<bool>,
brief_nicht_gefunden: Option<bool>,
sendungsdaten_zu_alt: Option<bool>,
sendungsnummer_nicht_suchbar: Option<bool>,
fehlertext: Option<String>,
fehlertextApp: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Sendung {
id: String,
has_complete_details: bool,
sendung_nicht_gefunden: Option<SendungNichtGefunden>,
sendungsdetails: SendungDetails,
sendungsinfo: SendungsInfo,
versand_datum_benoetigt: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
sendungen: Option<Vec<Sendung>>,
// TODO: parse RECEIVE_MERGED_SHIPMENTS, what is that?
//merged_anonymous_shipment_list_ids: Option<Vec<()>>,
is_rate_limited: bool,
}
impl From<SendungNichtGefunden> for ShipmentNotFoundError {
fn from(value: SendungNichtGefunden) -> Self {
Self {
no_data_available: optional_default_false(value.keine_daten_verfuegbar),
not_from_dhl: optional_default_false(value.keine_dhl_paket_sendung),
id_invalid: optional_default_false(value.sendungsnummer_ungueltig),
letter_not_found: optional_default_false(value.brief_nicht_gefunden),
data_to_old: optional_default_false(value.sendungsdaten_zu_alt),
id_not_searchable: optional_default_false(value.sendungsnummer_nicht_suchbar),
}
}
}
impl From<Nachhaltigkeitsstatus> for Vec<GoGreenWashing> {
fn from(value: Nachhaltigkeitsstatus) -> Self {
let mut out = Vec::new();
if value.bahnpaket {
out.push(GoGreenWashing::ShippedByRailway);
}
if value.co2_freie_zustellung {
out.push(GoGreenWashing::CO2FreeSupposedly);
}
if value.gg_versender {
out.push(GoGreenWashing::GoGreenwashingByShipper);
}
if value.ggp_empfaenger {
out.push(GoGreenWashing::GoGreenwashingPlusByRecipient);
}
if value.ggp_versender {
out.push(GoGreenWashing::GoGreenwashingPlusByShipper);
}
if value.klimafreundlicher_empfang {
out.push(GoGreenWashing::SupposedlyClimateFriendlyReception);
}
out
}
}
enum GoGreenWashing {
ShippedByRailway,
CO2FreeSupposedly,
GoGreenwashingByShipper,
GoGreenwashingPlusByShipper,
GoGreenwashingPlusByRecipient,
SupposedlyClimateFriendlyReception,
}
#[derive(Debug)]
pub struct ShipmentSpecialDetails {
abholcode_available: bool,
benachrichtigt_in_filiale: bool,
zustellzeitfenster_bis: Option<String>,
zustellzeitfenster_von: Option<String>,
railway_shipment: Option<bool>,
express_shipment: Option<bool>,
warenpost: Option<bool>,
retoure: Option<bool>,
ruecksendung: Option<bool>,
two_man_handling: Option<bool>,
unplausibel: Option<bool>,
target_country: Option<String>,
recipient: Option<SendungEmpfaenger>,
pub product_name: Option<String>,
pub shipment_name: Option<String>,
}
#[derive(Debug)]
pub struct Shipment {
pub id: String,
pub api_has_complete_details: bool,
pub needs_shipment_date: bool,
pub needs_plz: bool,
pub quelle: SendungsQuelle,
// probably not optional
pub international: Option<bool>,
// probably not optional
pub has_shipped: Option<bool>,
pub special: ShipmentSpecialDetails,
pub error: Option<ShipmentNotFoundError>,
}
fn optional_default_false(value: Option<bool>) -> bool {
if let Some(value) = value {
value
} else {
false
}
}
impl From<Sendung> for Shipment {
fn from(value: Sendung) -> Self {
let (
abholcode_available,
benachrichtigt_in_filiale,
zustellzeitfenster_bis,
zustellzeitfenster_von,
) = {
if let Some(zustellung) = value.sendungsdetails.zustellung {
(
optional_default_false(zustellung.abholcode_available),
optional_default_false(zustellung.benachrichtigt_in_filiale),
zustellung.zustellzeitfenster_bis,
zustellung.zustellzeitfenster_von,
)
} else {
(false, false, None, None)
}
};
Self {
id: value.id,
api_has_complete_details: value.has_complete_details,
needs_shipment_date: value.versand_datum_benoetigt,
needs_plz: optional_default_false(value.sendungsdetails.mehr_informationen_verfuegbar),
international: value.sendungsdetails.international,
has_shipped: value.sendungsdetails.ist_zugestellt,
quelle: value.sendungsdetails.quelle,
error: {
if let Some(err) = value.sendung_nicht_gefunden {
Some(err.into())
} else {
None
}
},
special: ShipmentSpecialDetails {
abholcode_available,
benachrichtigt_in_filiale,
zustellzeitfenster_bis,
zustellzeitfenster_von,
shipment_name: value.sendungsinfo.sendungsname,
railway_shipment: value.sendungsdetails.bahnpaket,
express_shipment: value.sendungsdetails.express_sendung,
warenpost: value.sendungsdetails.warenpost,
retoure: value.sendungsdetails.retoure,
ruecksendung: value.sendungsdetails.ruecksendung,
two_man_handling: value.sendungsdetails.two_man_handling,
unplausibel: value.sendungsdetails.unplausibel,
target_country: value.sendungsdetails.zielland,
recipient: value.sendungsdetails.pan_empfaenger,
product_name: value.sendungsdetails.produkt_name,
},
}
}
}
impl From<Response> for LibraryResult<Vec<Shipment>> {
fn from(value: Response) -> Self {
if value.is_rate_limited {
Err(crate::LibraryError::NetworkFetch)
} else {
let mut arr = Vec::new();
for item in value.sendungen.unwrap() {
arr.push(item.into());
}
Ok(arr)
}
}
}
fn endpoint_data_search() -> &'static str {
"https://www.dhl.de/int-verfolgen/data/search"
}
fn query_parameters_data_search(
params: &TrackingParams,
mut shippingnumbers: Vec<String>,
) -> Vec<(String, String)> {
let mut out = vec![("noRedirect".to_string(), "true".to_string())];
if let Some(lang) = params.language.as_ref() {
out.push(("language".to_string(), lang.clone()));
}
if shippingnumbers.len() > 0 {
let mut shippingnumbers_string = shippingnumbers.pop().unwrap();
for number in shippingnumbers {
shippingnumbers_string = format!(",{}", number);
}
out.push(("piececode".to_string(), shippingnumbers_string));
}
out
}
fn endpoint_data_shipment() -> &'static str {
"https://www.dhl.de/int-verfolgen/data/shipment"
}
fn query_parameters_data_shipment(params: &TrackingParams) -> Vec<(String, String)> {
let mut out = vec![];
if let Some(lang) = params.language.as_ref() {
out.push(("language".to_string(), lang.clone()));
}
out
}

View file

@ -20,8 +20,7 @@ impl CookieHeaderValueBuilder {
}
pub fn add_dhlcs(mut self, dhli: &DHLIdToken) -> Self {
self.list
.push(("dhlcs".to_string(), dhli.get_dhlcs().to_string()));
self.list.push(("dhlcs".to_string(), dhli.get_dhlcs().to_string()));
self
}
@ -55,7 +54,7 @@ macro_rules! mini_assert_api_eq {
if $a != $b {
return Err(crate::LibraryError::APIChange);
}
};
}
}
macro_rules! mini_assert_api {
@ -63,163 +62,14 @@ macro_rules! mini_assert_api {
if !$a {
return Err(crate::LibraryError::APIChange);
}
};
}
}
macro_rules! mini_assert_inval {
($a: expr) => {
if !$a {
return Err(crate::LibraryError::InvalidArgument(format!(
"MiniAssert failed: $a"
)));
return Err(crate::LibraryError::InvalidArgument(format!("MiniAssert failed: $a")));
}
};
}
macro_rules! parse_response_internal {
($res: expr) => {{
if let Err(err) = $res {
return Err(err.into());
}
let res = $res.unwrap();
res
}};
}
macro_rules! request_internal {
(
$client: tt,
$method: ident,
$endpoint: tt
$(#$func:ident$args:tt)*
) => {
{
let req = $client.$method($endpoint)
$(
.$func$args
)*
.build()
.unwrap();
let res = $client.execute(req).await;
parse_response_internal!(res)
}
};
}
macro_rules! request {
(
$self:ident.$client:ident,
$endpoint: tt,
$($func:ident$args:tt),*
) => {
request_internal!(($self.$client), get, ($endpoint()) $(#$func$args)*)
};
(
$self:ident.$client:ident,
$endpoint: ident,
$($func:ident$args:tt),*
) => {
request_internal!(($self.$client), get, $endpoint $(#$func$args)*)
};
}
macro_rules! request_post {
(
$self:ident.$client:ident,
$endpoint: ident,
$(.$func:ident$args:tt)*
) => {
request_internal!(
($self.$client), post, $endpoint $(# $func$args)*)
};
(
$self:ident.$client:ident,
$endpoint: tt,
$(.$func:ident$args:tt)*
) => {
request_internal!(
($self.$client), post, $endpoint $(# $func$args)*)
};
}
macro_rules! request_json {
(
$self:ident.$client:ident,
$endpoint: ident,
$body: tt,
$($func:ident$args:tt),*
) => {
request_json!($self.$client, ($endpoint()), $body, $( .$func$args)*)
};
(
$self:ident.$client:ident,
$endpoint: tt,
$body: tt,
$(.$func:ident$args:tt)*
) => {{
let body = serde_json::to_string(&$body).unwrap();
request_post!(
$self.$client, $endpoint,
$(
.$func$args
)*
.header("content-type", "application/json")
.header("content-length", body.len())
.body(body)
)
}};
}
macro_rules! parse_json_response {
($res: expr, $type: ty) => {{
let res = $res.text().await.unwrap();
let jd = &mut serde_json::Deserializer::from_str(res.as_str());
let mut unused = std::collections::BTreeSet::new();
println!("res({}): {}", stringify!($type), res);
let res: Result<$type,_> = serde_ignored::deserialize(jd, |path| {
unused.insert(path.to_string());
});
println!("res({}): {:?}", stringify!($type), unused);
let res: $type = match res {
Ok(res) => res,
Err(err) => return Err(crate::LibraryError::from(err)),
};
res
}};
}
macro_rules! parse_json_response_from_apiresult {
($res: expr, $type: ty) => {{
let res = $res.text().await.unwrap();
let jd = &mut serde_json::Deserializer::from_str(res.as_str());
let mut unused = std::collections::BTreeSet::new();
println!("res({}): {}", stringify!($type), res);
let res: Result<APIResult<$type>,_> = serde_ignored::deserialize(jd, |path| {
unused.insert(path.to_string());
});
println!("res({}): {:?}", stringify!($type), unused);
let res: LibraryResult<$type> = match res {
Ok(res) => res.into(),
Err(err) => return Err(crate::LibraryError::from(err)),
};
let res = match res {
Ok(res) => res,
Err(err) => return Err(crate::LibraryError::from(err)),
};
res
}};
}

View file

@ -44,10 +44,6 @@ pub(crate) fn authorized_credentials() -> (&'static str, &'static str) {
("erkennen", "8XRUfutM8PTvUz3A")
}
pub(crate) fn api_key_header() -> (&'static str, &'static str) {
("x-api-key", "a0d5b9049ba8918871e6e20bd5c49974")
}
// "/int-push/", "X-APIKey", "5{@8*nB=?\\.@t,XwWK>Y[=yY^*Y8&myzDE7_"
// /int-stammdaten/", null, "zAuoC3%7*qbRVmiXdNGyYz9iJ7N@Ph3Cw4zV"
// "/int-verfolgen/data/packstation/v2/", null, "a0d5b9049ba8918871e6e20bd5c49974",

View file

@ -129,27 +129,23 @@ impl AsyncComponent for Login {
} else {
match keyring
.search_items(&HashMap::from(KEYRING_ATTRIBUTES))
.await
{
.await {
Ok(res) => {
if res.len() > 0 {
let item = &res[0];
let refresh_token = item.secret().await.unwrap();
let refresh_token =
std::str::from_utf8(refresh_token.as_slice()).unwrap();
model.refresh_token = Some(
RefreshToken::new(refresh_token.to_string()).unwrap(),
);
let refresh_token = std::str::from_utf8(refresh_token.as_slice()).unwrap();
model.refresh_token = Some(RefreshToken::new(refresh_token.to_string()).unwrap());
sender.input(LoginInput::NeedsRefresh);
} else {
sender.input(LoginInput::NeedsLogin);
}
}
},
Err(err) => {
sender
.output(LoginOutput::KeyringError(err))
.expect("sender not worky");
}
},
};
}
}
@ -212,9 +208,7 @@ impl AsyncComponent for Login {
}
LoginInput::BreakWorld => {
self.set_state(LoginState::Offline);
sender
.output(LoginOutput::Error(libpaket::LibraryError::APIChange))
.unwrap();
sender.output(LoginOutput::Error(libpaket::LibraryError::APIChange)).unwrap();
{
let shared_id_token = self.shared_id_token.lock().await;
let mut shared_id_token = shared_id_token.write();
@ -300,15 +294,7 @@ impl Login {
let future = async {
self.refresh_token = Some(res.refresh_token);
let keyring = KEYRING.get().unwrap();
keyring
.create_item(
"Refresh Token",
&HashMap::from(KEYRING_ATTRIBUTES),
self.refresh_token.as_ref().unwrap().to_string(),
true,
)
.await
.unwrap();
keyring.create_item("Refresh Token", &HashMap::from(KEYRING_ATTRIBUTES), self.refresh_token.as_ref().unwrap().to_string(), true).await.unwrap();
};
if !res.id_token.is_expired() {
@ -337,42 +323,62 @@ impl Login {
libpaket::LibraryError::Unauthorized => {
sender.input(LoginInput::NeedsLogin);
}
libpaket::LibraryError::Deprecated => {
sender.output(LoginOutput::Error(res)).unwrap();
sender.input(LoginInput::BreakWorld);
}
}
}
}
}
}
macro_rules! received {
($func_name:ident, $args:tt, $calling:ident, $calling_args:tt) => {
async fn $func_name$args -> LoginCommand {
fn convert_library_error_to_response_type(err: &LibraryError) -> ResponseType {
match err {
LibraryError::NetworkFetch => ResponseType::Retry,
LibraryError::Unauthorized => ResponseType::Okay,
LibraryError::InvalidArgument(_) => ResponseType::Okay,
LibraryError::DecodeError(_) => ResponseType::Okay,
LibraryError::APIChange => ResponseType::Okay,
}
}
async fn received_auth_code(auth_code: String, code_verifier: CodeVerfier) -> LoginCommand {
let client = OpenIdClient::new();
let mut err = LibraryError::NetworkFetch;
for _ in 0..6 {
let result: Result<TokenResponse, LibraryError> = client
.$calling$calling_args
.token_authorization(auth_code.clone(), &code_verifier)
.await;
err = match result {
Ok(result) => return LoginCommand::Token(Ok(result)),
Err(err) => err,
};
if let Ok(result) = result {
return LoginCommand::Token(Ok(result))
}
if err == LibraryError::NetworkFetch {
err = result.unwrap_err();
let response_type = convert_library_error_to_response_type(&err);
if response_type == ResponseType::Retry {
continue;
} else {
return LoginCommand::Token(Err(err));
}
}
LoginCommand::Token(Err(err))
}
};
}
received!(received_auth_code, (auth_code: String, code_verifier: CodeVerfier), token_authorization, (auth_code.clone(), &code_verifier));
received!(use_refresh_token, (refresh_token: RefreshToken), token_refresh, (&refresh_token));
async fn use_refresh_token(refresh_token: RefreshToken) -> LoginCommand {
let client = OpenIdClient::new();
let mut err = LibraryError::NetworkFetch;
for _ in 0..6 {
let result: Result<TokenResponse, LibraryError> =
client.token_refresh(&refresh_token).await;
err = match result {
Ok(result) => return LoginCommand::Token(Ok(result)),
Err(err) => err,
};
let response_type = convert_library_error_to_response_type(&err);
if response_type == ResponseType::Retry {
continue;
} else {
return LoginCommand::Token(Err(err));
}
}
LoginCommand::Token(Err(err))
}

View file

@ -1,78 +0,0 @@
use adw::prelude::*;
use libpaket::tracking::Shipment;
use relm4::factory::FactoryComponent;
use relm4::prelude::*;
use relm4::{adw, gtk};
pub struct ShipmentView {
model: Shipment,
}
#[relm4::factory(pub)]
impl FactoryComponent for ShipmentView {
type CommandOutput = ();
type Init = Shipment;
type Output = ();
type Input = ();
type ParentWidget = gtk::Box;
type Index = String;
view! {
#[root]
gtk::Box {
add_css_class: relm4::css::CARD,
set_hexpand: true,
set_margin_all: 8,
// title box
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 8,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Start,
gtk::Label {
add_css_class: relm4::css::NUMERIC,
add_css_class: relm4::css::CAPTION_HEADING,
set_halign: gtk::Align::Start,
set_label: &self.model.id,
},
gtk::Label {
add_css_class: relm4::css::HEADING,
set_halign: gtk::Align::Start,
set_label: {
// TODO: gettext
if let Some(shipment_name) = &self.model.special.shipment_name {
shipment_name.as_str()
} else if let Some(product_name) = &self.model.special.product_name {
product_name.as_str()
} else {
match self.model.quelle {
libpaket::tracking::SendungsQuelle::TTBRIEF => "Letter",
libpaket::tracking::SendungsQuelle::PAKET => "Parcel",
libpaket::tracking::SendungsQuelle::SVB => "quelle: SVB",
libpaket::tracking::SendungsQuelle::OPTIMA => "quelle: OPTIMA",
}
}
}
}
}
}
}
}
fn init_model(
init: Self::Init,
index: &Self::Index,
sender: relm4::FactorySender<Self>,
) -> Self {
let model = ShipmentView { model: init };
model
}
}