From b36f7843f0b00e87935e34847cefcf3805b97358 Mon Sep 17 00:00:00 2001 From: jane400 Date: Wed, 21 Aug 2024 18:22:41 +0200 Subject: [PATCH] feat: add first attempt for Sendungsverfolgung api --- libpaket/Cargo.toml | 7 +- libpaket/src/lib.rs | 5 +- libpaket/src/tracking.rs | 440 +++++++++++++++++++++++++++++++++++++++ libpaket/src/www.rs | 4 + paket/src/tracking.rs | 78 +++++++ 5 files changed, 531 insertions(+), 3 deletions(-) create mode 100644 libpaket/src/tracking.rs create mode 100644 paket/src/tracking.rs diff --git a/libpaket/Cargo.toml b/libpaket/Cargo.toml index a84146d..aa278f8 100644 --- a/libpaket/Cargo.toml +++ b/libpaket/Cargo.toml @@ -36,9 +36,12 @@ thiserror = "1.0.56" [features] default = [ "advices", - "locker_all" + "locker_all", + "unstable", ] +unstable = [] + advices = [ #"dep:sha2", "dep:uuid", @@ -74,4 +77,4 @@ locker_register_base = [ locker_register_regtoken = [ "locker_register_base" -] \ No newline at end of file +] diff --git a/libpaket/src/lib.rs b/libpaket/src/lib.rs index d8c9c58..3181520 100644 --- a/libpaket/src/lib.rs +++ b/libpaket/src/lib.rs @@ -21,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;*/ @@ -66,4 +69,4 @@ impl From for LibraryError { fn from(value: serde_json::Error) -> Self { Self::DecodeError(value.to_string()) } -} \ No newline at end of file +} diff --git a/libpaket/src/tracking.rs b/libpaket/src/tracking.rs new file mode 100644 index 0000000..0def858 --- /dev/null +++ b/libpaket/src/tracking.rs @@ -0,0 +1,440 @@ +use serde::{Deserialize, Serialize}; + +use crate::{login::DHLIdToken, LibraryResult}; + +#[derive(Default)] +pub struct TrackingParams { + pub language: Option, +} + +#[derive(Serialize)] +pub struct ShipmentRequest { + international: bool, + piececode: String, + zip: String, +} + +impl crate::WebClient { + pub async fn tracking_search( + &self, + params: TrackingParams, + ids: Vec, + dhli: Option<&DHLIdToken>, + ) -> LibraryResult> { + 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); + Ok(resp.into()) + } + + pub async fn tracking_shipment( + &self, + params: TrackingParams, + body: ShipmentRequest, + ) -> LibraryResult> { + 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); + 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, + sendungsliste: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SendungsVerlaufEvent { + pub datum: String, + ort: Option, + pub ruecksendung: bool, + pub status: String, +} + +impl SendungsVerlaufEvent { + fn get_ort(&self) -> Option { + 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, + icon_id: Option, + datum_aktueller_status: Option, + aktueller_status: Option, + events: Option>, + + farbe: u32, + fortschritt: u32, + maximal_fortschritt: u32, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SendungZustellung { + abholcode_available: Option, // probably always there + benachrichtigt_in_filiale: Option, // probably always there + zustellzeitfenster_bis: Option, + zustellzeitfenster_von: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SendungServices { + statusbenachrichtigung: SendungServiceStatusBenachrichtigung, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SendungServiceStatusBenachrichtigung { + aktueller_status: Option, + erfolgte_zustellung: Option, + geplante_zustellung: Option, + authentication_required: Option, +} + +#[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, + is_shipper_plz: Option, + ist_zugestellt: Option, + retoure: Option, + ruecksendung: Option, + sendungsverlauf: SendungsVerlauf, + show_quality_level_hint: Option, + two_man_handling: Option, + unplausibel: Option, + mehr_informationen_verfuegbar: Option, + + nachhaltigkeitsstatus: Option, + brief_sendung: Option, + invalid_time_of_day: Option, + bahnpaket: Option, + email: Option, + international: Option, + pan_empfaenger: Option, + produkt_name: Option, + //sendungsnummern: (), + services: Option, + show_digital_notification_cta_hint: Option, + warenpost: Option, + zielland: Option, // localized string? + zustellung: Option, +} + +#[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, + keine_dhl_paket_sendung: Option, + sendungsnummer_ungueltig: Option, + brief_nicht_gefunden: Option, + sendungsdaten_zu_alt: Option, + sendungsnummer_nicht_suchbar: Option, + fehlertext: Option, + fehlertextApp: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct Sendung { + id: String, + has_complete_details: bool, + sendung_nicht_gefunden: Option, + sendungsdetails: SendungDetails, + sendungsinfo: SendungsInfo, + versand_datum_benoetigt: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct Response { + sendungen: Vec, + // TODO: parse RECEIVE_MERGED_SHIPMENTS, what is that? + //merged_anonymous_shipment_list_ids: Option>, + is_rate_limited: bool, +} + +impl From 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 for Vec { + 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, + zustellzeitfenster_von: Option, + railway_shipment: Option, + express_shipment: Option, + warenpost: Option, + retoure: Option, + ruecksendung: Option, + two_man_handling: Option, + unplausibel: Option, + target_country: Option, + recipient: Option, + pub product_name: Option, + pub shipment_name: Option, +} + +#[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, + // probably not optional + pub has_shipped: Option, + + pub special: ShipmentSpecialDetails, + + pub error: Option, +} + +fn optional_default_false(value: Option) -> bool { + if let Some(value) = value { + value + } else { + false + } +} + +impl From 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 for Vec { + 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, +) -> 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 +} diff --git a/libpaket/src/www.rs b/libpaket/src/www.rs index 7e6bd07..f376100 100644 --- a/libpaket/src/www.rs +++ b/libpaket/src/www.rs @@ -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", diff --git a/paket/src/tracking.rs b/paket/src/tracking.rs new file mode 100644 index 0000000..725d467 --- /dev/null +++ b/paket/src/tracking.rs @@ -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 { + let model = ShipmentView { model: init }; + + model + } +}