feat: add first attempt for Sendungsverfolgung api

This commit is contained in:
jane400 2024-08-21 18:22:41 +02:00 committed by jane400
parent 912f024163
commit b36f7843f0
5 changed files with 531 additions and 3 deletions

View file

@ -36,9 +36,12 @@ thiserror = "1.0.56"
[features] [features]
default = [ default = [
"advices", "advices",
"locker_all" "locker_all",
"unstable",
] ]
unstable = []
advices = [ advices = [
#"dep:sha2", #"dep:sha2",
"dep:uuid", "dep:uuid",

View file

@ -21,6 +21,9 @@ pub mod advices;
#[cfg(feature = "advices")] #[cfg(feature = "advices")]
pub use advices::AdviceClient; pub use advices::AdviceClient;
#[cfg(feature = "unstable")]
pub mod tracking;
/*#[cfg(test)] /*#[cfg(test)]
pub(crate) mod private;*/ pub(crate) mod private;*/

440
libpaket/src/tracking.rs Normal file
View file

@ -0,0 +1,440 @@
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);
Ok(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);
Ok(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: 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 Vec<Shipment> {
fn from(value: Response) -> Self {
let mut arr = Vec::new();
for item in value.sendungen {
arr.push(item.into());
}
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

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

78
paket/src/tracking.rs Normal file
View 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
}
}