1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use anyhow::Result;
use regex::Regex;
use serde::Deserialize;

const DHL_JSON_REGEXP: &str = r#".*initialState: JSON\.parse\((.+")\).*"#;

/// Parcel history events (where did you come from, where did you go?)
#[derive(Deserialize, Debug)]
pub struct DHLPackageItemHistoryEvent {
    /// Date
    #[serde(rename = "datum")]
    pub date: String,
    /// Status text
    #[serde(rename = "status")]
    pub status: String,
    /// If it was returned (?)
    #[serde(rename = "ruecksendung")]
    pub return_shipment: bool,
    /// Location where this happened
    #[serde(rename = "ort")]
    pub location: Option<String>,
}

/// Parcel history
#[derive(Deserialize, Debug)]
pub struct DHLPackageItemHistory {
    /// The parcel may or may not have made a few stops already, this Vec may contain them.
    #[serde(rename = "events")]
    pub events: Option<Vec<DHLPackageItemHistoryEvent>>,
    /// Current state of the parcel
    #[serde(rename = "aktuellerStatus")]
    pub current_status: Option<String>,
    /// Number of steps there are (or should be?) - might as well use `events.len()` 🤷‍♀️
    #[serde(rename = "fortschritt")]
    pub steps: u64,
}

/// Information why a parcel tracking code may not have been found.
/// This is only set when no tracking information was found (yet?).
#[derive(Deserialize, Debug)]
pub struct DHLPackageNotFoundInfo {
    /// If `true`, no data is available for this tracking code.
    #[serde(rename = "keineDatenVerfuegbar")]
    pub no_data_available: bool,
    /// If `true`, this is probably not a DHL tracking code.
    #[serde(rename = "keineDhlPaketSendung")]
    pub not_a_dhl_package: bool,
}

/// Parcel item details
#[derive(Deserialize, Debug)]
pub struct DHLPackageItemDetails {
    /// History of the parcel
    #[serde(rename = "sendungsverlauf")]
    pub history: DHLPackageItemHistory,
    /// Destination country of the parcel
    #[serde(rename = "zielland")]
    pub destination_country: Option<String>,
}

/// Parcel item elements
#[derive(Deserialize, Debug)]
pub struct DHLPackageItem {
    /// Tracking code, just for keeping track.
    pub id: String,

    /// (unsure what this means, if you know let me know ❤️)
    #[serde(rename = "hasCompleteDetails")]
    pub has_complete_details: bool,

    /// Details for this parcel
    #[serde(rename = "sendungsdetails")]
    pub item_details: DHLPackageItemDetails,

    /// If no parcel was found, this variable is **not `None`** and may contains reasons as to why.
    /// Otherwise it should be `None`.
    #[serde(rename = "sendungNichtGefunden")]
    pub package_not_found: Option<DHLPackageNotFoundInfo>,
}

/// Root element, contains a vector with parcel items
#[derive(Deserialize, Debug)]
pub struct DHLPackageStatus {
    /// The items returned for the query.
    #[serde(rename = "sendungen")]
    pub items: Vec<DHLPackageItem>,
}

fn find_and_derez_json(body: &str) -> Result<DHLPackageStatus, anyhow::Error> {
    let rex = Regex::new(DHL_JSON_REGEXP).unwrap();

    let caps = rex.captures(&body).unwrap();

    let json_escaped = &caps[1];
    let json = json_escaped.trim()[1..json_escaped.len() - 1].replace(r#"\""#, "\"");

    //println!("{}", json);

    let r: DHLPackageStatus = serde_json::from_str(&json)?;
    Ok(r)
}

/// Returns a DHLPackageStatus struct. You should check if it actually has any data with: `DHLPackageStatus.items[0].package_not_found.has_some()`  
/// `DHLPackageItem.package_not_found` will only be set if no package was found for that tracking code.
///
/// # Arguments
///
/// * `package_id` - Tracking code as `&str` of the parcel you wish to query, usually a number but sometimes contains letters.
///
/// # Examples
///
/// ``` ignore
/// use dhl_api::get_dhl_package_status;
/// let status = get_dhl_package_status("123456789").await?;
/// for item in status.items {
///     if item.package_not_found.is_some() {
///         // This item was not found
///
///         let why_not_found = item.package_not_found.unwrap();
///         // if why_not_found.no_data_available { ...
///         // if why_not_found.not_a_dhl_package { ...
///
///         continue;
///     }
///
///     let tracking_code = &item.id;
///
///     if item.has_complete_details {
///         let details = &item.item_details;
///
///         for event in &details.history.events.unwrap() {
///             // Do whatever you need 🦈
///         }
///     }
/// }
/// ```
pub async fn get_dhl_package_status(package_id: &str) -> Result<DHLPackageStatus, anyhow::Error> {
    let my_url = format!("https://www.dhl.de/int-verfolgen/?lang=en&domain=de&lang=en&domain=de&lang=en&domain=de&lang=en&domain=de&piececode={}", package_id);
    let body = reqwest::get(&my_url).await?.text().await?;

    find_and_derez_json(&body)
}

/// Try to parse a HTML body and look for the shipping information, usually you need not to call this
/// unless you wish to use your own HTTP client or backend URL or whatever other wizardry you're into.
/// 
/// # Arguments
/// 
/// `htmlbody` - HTML code to scan, check out the sourcecode to find the regexp and what it expects.
pub fn get_dhl_package_from_html(htmlbody: &str) -> Result<DHLPackageStatus, anyhow::Error> {
    find_and_derez_json(&htmlbody)
}