Compare commits
14 commits
a1089b5af7
...
609fc65816
Author | SHA1 | Date | |
---|---|---|---|
|
609fc65816 | ||
|
290faff5c2 | ||
|
b36f7843f0 | ||
|
912f024163 | ||
|
c07a164903 | ||
|
20dd4fc807 | ||
|
2586d490bf | ||
|
8573be6e23 | ||
|
06c9342ea7 | ||
|
cda0f946da | ||
|
cc52b99144 | ||
|
b8f4e199f6 | ||
|
a57be17dab | ||
|
daa2aedf0c |
14 changed files with 799 additions and 3906 deletions
3752
Cargo.lock
generated
3752
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -19,6 +19,7 @@ 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"
|
||||
|
||||
|
@ -35,9 +36,12 @@ thiserror = "1.0.56"
|
|||
[features]
|
||||
default = [
|
||||
"advices",
|
||||
"locker_all"
|
||||
"locker_all",
|
||||
"unstable",
|
||||
]
|
||||
|
||||
unstable = []
|
||||
|
||||
advices = [
|
||||
#"dep:sha2",
|
||||
"dep:uuid",
|
||||
|
|
|
@ -10,6 +10,13 @@ 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 don’t want to do that.
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
pub use super::Advice;
|
||||
use super::AdvicesResponse;
|
||||
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde::Serialize;
|
||||
use crate::constants::webview_user_agent;
|
||||
use crate::LibraryResult;
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde::Serialize;
|
||||
|
||||
pub struct AdviceClient {
|
||||
client: reqwest::Client,
|
||||
|
@ -27,7 +27,10 @@ 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
|
||||
|
@ -46,10 +49,13 @@ impl AdviceClient {
|
|||
let res = res.unwrap();
|
||||
|
||||
for cookie in res.cookies() {
|
||||
if cookie.name() == "UAT" {
|
||||
println!("UAT: cookie: {:?}={:?}", cookie.name(), cookie.value());
|
||||
if cookie.name() == "AccessToken" {
|
||||
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");
|
||||
}
|
||||
|
@ -86,9 +92,10 @@ impl AdviceClient {
|
|||
)
|
||||
};
|
||||
|
||||
let req = self.client
|
||||
let req = self
|
||||
.client
|
||||
.get(&advice.image_url)
|
||||
.header("Cookie", format!("UAT={}", uat.0))
|
||||
.header("Cookie", format!("AccessToken={}", uat.0))
|
||||
.build()
|
||||
.unwrap();
|
||||
let res = self.client.execute(req).await;
|
||||
|
@ -150,5 +157,5 @@ fn headers() -> HeaderMap {
|
|||
}
|
||||
|
||||
pub fn endpoint_access_tokens() -> &'static str {
|
||||
"https://briefankuendigung.dhl.de/pdapp-web/access-tokens"
|
||||
"https://briefankuendigung.enplify.dhl.de/pdapp-web/access-tokens"
|
||||
}
|
||||
|
|
|
@ -29,18 +29,13 @@ 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>,
|
||||
}
|
||||
|
||||
|
@ -73,33 +68,11 @@ 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 cookie_headervalue = CookieHeaderValueBuilder::new()
|
||||
.add_dhli(dhli)
|
||||
.add_dhlcs(dhli)
|
||||
.build_string();
|
||||
let res = request!(self.web_client, endpoint_advices,
|
||||
header("Cookie", CookieHeaderValueBuilder::new().add_dhli(dhli).add_dhlcs(dhli).build_string())
|
||||
);
|
||||
|
||||
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())
|
||||
}
|
||||
};
|
||||
let res = parse_json_response!(res, AdvicesResponse);
|
||||
|
||||
if res.access_token_url.is_some() {
|
||||
if res.access_token_url.as_ref().unwrap().as_str() != crate::advices::endpoint_access_tokens() {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
#[macro_use]
|
||||
mod utils;
|
||||
|
||||
mod www;
|
||||
pub use www::WebClient;
|
||||
|
||||
|
@ -8,8 +11,6 @@ pub mod stammdaten;
|
|||
pub use stammdaten::StammdatenClient;
|
||||
|
||||
mod common;
|
||||
#[macro_use]
|
||||
mod utils;
|
||||
pub mod constants;
|
||||
|
||||
#[cfg(feature = "locker_base")]
|
||||
|
@ -20,6 +21,9 @@ pub mod advices;
|
|||
#[cfg(feature = "advices")]
|
||||
pub use advices::AdviceClient;
|
||||
|
||||
#[cfg(feature = "unstable")]
|
||||
pub mod tracking;
|
||||
|
||||
/*#[cfg(test)]
|
||||
pub(crate) mod private;*/
|
||||
|
||||
|
@ -37,6 +41,8 @@ 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>;
|
||||
|
|
|
@ -27,8 +27,7 @@ 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();
|
||||
|
|
|
@ -6,9 +6,10 @@ use ed25519_dalek::Signer;
|
|||
|
||||
|
||||
pub struct CustomerKeySeed {
|
||||
postnumber: String,
|
||||
seed: Seed,
|
||||
uuid: Uuid,
|
||||
pub postnumber: String,
|
||||
pub seed: Seed,
|
||||
pub uuid: Uuid,
|
||||
pub device_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Seed {
|
||||
|
@ -44,17 +45,25 @@ impl CustomerKeySeed {
|
|||
postnumber,
|
||||
seed: Seed::random(),
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
device_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid) -> Self {
|
||||
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid, device_id: String) -> Self {
|
||||
CustomerKeySeed {
|
||||
postnumber: postnumber.clone(),
|
||||
seed: Seed::from_bytes(seed),
|
||||
uuid: uuid.clone()
|
||||
uuid: uuid.clone(),
|
||||
device_id: Some(device_id),
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
|
|
|
@ -4,14 +4,13 @@ mod openid_response;
|
|||
pub mod openid_token;
|
||||
mod utils;
|
||||
|
||||
pub use self::dhl_claims::{DHLIdToken, DHLCs};
|
||||
pub use self::dhl_claims::{DHLCs, DHLIdToken};
|
||||
pub use self::openid_response::{RefreshToken, TokenResponse};
|
||||
pub use self::utils::{CodeVerfier, create_nonce};
|
||||
pub use self::utils::{create_nonce, CodeVerfier};
|
||||
|
||||
use super::common::APIResult;
|
||||
use crate::{LibraryError, LibraryResult};
|
||||
|
||||
|
||||
pub struct OpenIdClient {
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
@ -31,27 +30,12 @@ impl OpenIdClient {
|
|||
&self,
|
||||
refresh_token: &RefreshToken,
|
||||
) -> LibraryResult<TokenResponse> {
|
||||
let req = self
|
||||
.client
|
||||
.post(constants::token::endpoint())
|
||||
let res = request_post!(self.client,
|
||||
(constants::token::endpoint()),
|
||||
.form(constants::token::refresh_token_form(refresh_token.as_str()).as_slice())
|
||||
.build()
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
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)),
|
||||
}
|
||||
Ok(parse_json_response_from_apiresult!(res, TokenResponse))
|
||||
}
|
||||
|
||||
pub async fn token_authorization(
|
||||
|
@ -78,23 +62,9 @@ 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);
|
||||
|
||||
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)),
|
||||
}
|
||||
Ok(parse_json_response_from_apiresult!(res, TokenResponse))
|
||||
}
|
||||
}
|
||||
|
|
444
libpaket/src/tracking.rs
Normal file
444
libpaket/src/tracking.rs
Normal file
|
@ -0,0 +1,444 @@
|
|||
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(¶ms, 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(¶ms, 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(¶ms)),
|
||||
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
|
||||
}
|
|
@ -20,7 +20,8 @@ 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
|
||||
}
|
||||
|
@ -54,7 +55,7 @@ macro_rules! mini_assert_api_eq {
|
|||
if $a != $b {
|
||||
return Err(crate::LibraryError::APIChange);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! mini_assert_api {
|
||||
|
@ -62,14 +63,163 @@ 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
|
||||
}};
|
||||
}
|
|
@ -44,6 +44,10 @@ 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",
|
||||
|
|
|
@ -129,23 +129,27 @@ 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");
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -208,7 +212,9 @@ 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();
|
||||
|
@ -294,7 +300,15 @@ 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() {
|
||||
|
@ -323,58 +337,32 @@ impl Login {
|
|||
libpaket::LibraryError::Unauthorized => {
|
||||
sender.input(LoginInput::NeedsLogin);
|
||||
}
|
||||
libpaket::LibraryError::Deprecated => {
|
||||
sender.output(LoginOutput::Error(res)).unwrap();
|
||||
sender.input(LoginInput::BreakWorld);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
macro_rules! received {
|
||||
($func_name:ident, $args:tt, $calling:ident, $calling_args:tt) => {
|
||||
async fn $func_name$args -> LoginCommand {
|
||||
let client = OpenIdClient::new();
|
||||
let mut err = LibraryError::NetworkFetch;
|
||||
for _ in 0..6 {
|
||||
let result: Result<TokenResponse, LibraryError> = client
|
||||
.token_authorization(auth_code.clone(), &code_verifier)
|
||||
.$calling$calling_args
|
||||
.await;
|
||||
|
||||
if let Ok(result) = result {
|
||||
return LoginCommand::Token(Ok(result))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
if err == LibraryError::NetworkFetch {
|
||||
continue;
|
||||
} else {
|
||||
return LoginCommand::Token(Err(err));
|
||||
|
@ -382,3 +370,9 @@ async fn use_refresh_token(refresh_token: RefreshToken) -> LoginCommand {
|
|||
}
|
||||
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));
|
||||
|
|
78
paket/src/tracking.rs
Normal file
78
paket/src/tracking.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue