Compare commits
25 commits
edad2578a9
...
0b2ae19085
Author | SHA1 | Date | |
---|---|---|---|
0b2ae19085 | |||
1c63afc4dc | |||
f3196d6c9f | |||
|
8791b08c4f | ||
|
edc2da5d9e | ||
|
5cf693b7bb | ||
|
890aee8f72 | ||
|
ae64f73176 | ||
|
f3732051af | ||
|
cd2bc321cd | ||
|
0e1167adb2 | ||
|
609fc65816 | ||
|
290faff5c2 | ||
|
b36f7843f0 | ||
|
912f024163 | ||
|
c07a164903 | ||
|
20dd4fc807 | ||
|
2586d490bf | ||
|
8573be6e23 | ||
|
06c9342ea7 | ||
|
cda0f946da | ||
|
cc52b99144 | ||
|
b8f4e199f6 | ||
|
a57be17dab | ||
|
daa2aedf0c |
23 changed files with 1652 additions and 554 deletions
560
Cargo.lock
generated
560
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
3
icons.toml
Normal file
3
icons.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
app_id = "de.j4ne.Paket"
|
||||||
|
|
||||||
|
icons = ["plus", "minus"]
|
|
@ -19,6 +19,7 @@ secrecy = { version = "0.8.0", features = ["serde"] }
|
||||||
serde = { version = "1.0.195", features = ["derive"] }
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
serde_json = "1.0.111"
|
serde_json = "1.0.111"
|
||||||
serde_repr = { version = "0.1.18", optional = true }
|
serde_repr = { version = "0.1.18", optional = true }
|
||||||
|
serde_ignored = "0.1"
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|
||||||
|
@ -35,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",
|
||||||
|
@ -73,4 +77,4 @@ locker_register_base = [
|
||||||
|
|
||||||
locker_register_regtoken = [
|
locker_register_regtoken = [
|
||||||
"locker_register_base"
|
"locker_register_base"
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
- 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
|
## Examples
|
||||||
|
|
||||||
In the examples error-handling is ignored for simplicity. You don’t want to do that.
|
In the examples error-handling is ignored for simplicity. You don’t want to do that.
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
pub use super::Advice;
|
pub use super::Advice;
|
||||||
use super::AdvicesResponse;
|
use super::AdvicesResponse;
|
||||||
|
|
||||||
use reqwest::header::HeaderMap;
|
|
||||||
use serde::Serialize;
|
|
||||||
use crate::constants::webview_user_agent;
|
use crate::constants::webview_user_agent;
|
||||||
use crate::LibraryResult;
|
use crate::LibraryResult;
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
pub struct AdviceClient {
|
pub struct AdviceClient {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
|
@ -27,7 +27,10 @@ impl AdviceClient {
|
||||||
pub async fn access_token<'t>(&self, advices: &AdvicesResponse) -> LibraryResult<UatToken> {
|
pub async fn access_token<'t>(&self, advices: &AdvicesResponse) -> LibraryResult<UatToken> {
|
||||||
mini_assert_inval!(advices.has_any_advices());
|
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
|
let req = self
|
||||||
.client
|
.client
|
||||||
|
@ -46,10 +49,13 @@ impl AdviceClient {
|
||||||
let res = res.unwrap();
|
let res = res.unwrap();
|
||||||
|
|
||||||
for cookie in res.cookies() {
|
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()));
|
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,...)
|
// FIXME: Parse errors here better (checking if we're unauthorized,...)
|
||||||
panic!("NO UAT Token in access_token");
|
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)
|
.get(&advice.image_url)
|
||||||
.header("Cookie", format!("UAT={}", uat.0))
|
.header("Cookie", format!("AccessToken={}", uat.0))
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let res = self.client.execute(req).await;
|
let res = self.client.execute(req).await;
|
||||||
|
@ -150,5 +157,5 @@ fn headers() -> HeaderMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn endpoint_access_tokens() -> &'static str {
|
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)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AdvicesResponse {
|
pub struct AdvicesResponse {
|
||||||
// access_token_url, basic_auth, grant_token is null if no advices are available
|
// access_token_url, basic_auth, grant_token is null if no advices are available
|
||||||
#[serde(rename = "accessTokenUrl")]
|
|
||||||
pub(super) access_token_url: Option<AdviceAccessTokenUrl>,
|
pub(super) access_token_url: Option<AdviceAccessTokenUrl>,
|
||||||
#[serde(rename = "basicAuth")]
|
|
||||||
pub(super) basic_auth: Option<String>,
|
pub(super) basic_auth: Option<String>,
|
||||||
#[serde(rename = "currentAdvice")]
|
|
||||||
current_advice: Option<AdvicesList>,
|
current_advice: Option<AdvicesList>,
|
||||||
#[serde(rename = "grantToken")]
|
|
||||||
pub(super) grant_token: Option<String>,
|
pub(super) grant_token: Option<String>,
|
||||||
|
|
||||||
#[serde(rename = "oldAdvices")]
|
|
||||||
old_advices: Vec<AdvicesList>,
|
old_advices: Vec<AdvicesList>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,33 +68,11 @@ fn endpoint_advices() -> url::Url {
|
||||||
impl crate::www::WebClient {
|
impl crate::www::WebClient {
|
||||||
// FIXME: more error parsing
|
// FIXME: more error parsing
|
||||||
pub async fn advices(&self, dhli: &crate::login::DHLIdToken) -> crate::LibraryResult<AdvicesResponse> {
|
pub async fn advices(&self, dhli: &crate::login::DHLIdToken) -> crate::LibraryResult<AdvicesResponse> {
|
||||||
let cookie_headervalue = CookieHeaderValueBuilder::new()
|
let res = request!(self.web_client, endpoint_advices,
|
||||||
.add_dhli(dhli)
|
header("Cookie", CookieHeaderValueBuilder::new().add_dhli(dhli).add_dhlcs(dhli).build_string())
|
||||||
.add_dhlcs(dhli)
|
);
|
||||||
.build_string();
|
|
||||||
|
|
||||||
let req = self.web_client
|
let res = parse_json_response!(res, AdvicesResponse);
|
||||||
.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.is_some() {
|
||||||
if res.access_token_url.as_ref().unwrap().as_str() != crate::advices::endpoint_access_tokens() {
|
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;
|
mod www;
|
||||||
pub use www::WebClient;
|
pub use www::WebClient;
|
||||||
|
|
||||||
|
@ -8,8 +11,6 @@ pub mod stammdaten;
|
||||||
pub use stammdaten::StammdatenClient;
|
pub use stammdaten::StammdatenClient;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
#[macro_use]
|
|
||||||
mod utils;
|
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
|
|
||||||
#[cfg(feature = "locker_base")]
|
#[cfg(feature = "locker_base")]
|
||||||
|
@ -20,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;*/
|
||||||
|
|
||||||
|
@ -37,6 +41,8 @@ pub enum LibraryError {
|
||||||
DecodeError(String),
|
DecodeError(String),
|
||||||
#[error("upstream api was changed. not continuing")]
|
#[error("upstream api was changed. not continuing")]
|
||||||
APIChange,
|
APIChange,
|
||||||
|
#[error("upstream api was changed. this method is deprecated")]
|
||||||
|
Deprecated,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type LibraryResult<T> = Result<T, LibraryError>;
|
pub type LibraryResult<T> = Result<T, LibraryError>;
|
||||||
|
@ -63,4 +69,4 @@ impl From<serde_json::Error> for LibraryError {
|
||||||
fn from(value: serde_json::Error) -> Self {
|
fn from(value: serde_json::Error) -> Self {
|
||||||
Self::DecodeError(value.to_string())
|
Self::DecodeError(value.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,7 @@ fn headers() -> HeaderMap {
|
||||||
/* ("accept", "application/json") */
|
/* ("accept", "application/json") */
|
||||||
("app-version", app_version()),
|
("app-version", app_version()),
|
||||||
("device-os", "Android"),
|
("device-os", "Android"),
|
||||||
("device-key", "") /* is the android id... */
|
("device-key", ""), /* is the android id... */
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut map = HeaderMap::new();
|
let mut map = HeaderMap::new();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use super::utils::{PrimitiveBuilder, PrimitiveReader};
|
||||||
|
|
||||||
use crate::{LibraryError, LibraryResult};
|
use crate::{LibraryError, LibraryResult};
|
||||||
|
|
||||||
|
@ -81,104 +82,6 @@ pub struct Command {
|
||||||
metadata: Vec<u8>
|
metadata: Vec<u8>
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PrimitiveBuilder {
|
|
||||||
bin: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimitiveBuilder {
|
|
||||||
fn new() -> Self {
|
|
||||||
PrimitiveBuilder {
|
|
||||||
bin: Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_u8(self, number: u8) -> Self {
|
|
||||||
self.write_array(&[number])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_u16(self, number: u16) -> Self {
|
|
||||||
self.write_array(&number.to_be_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_u32(self, number: u32) -> Self {
|
|
||||||
self.write_array(&number.to_be_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_array(mut self, new: &[u8]) -> Self {
|
|
||||||
for b in new {
|
|
||||||
self.bin.push(*b);
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_array_with_len(self, bin: &[u8]) -> Self {
|
|
||||||
self
|
|
||||||
.write_u32(bin.len() as u32)
|
|
||||||
.write_array(bin)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(self) -> Vec<u8> {
|
|
||||||
self.bin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PrimitiveReader<'a> {
|
|
||||||
offset: usize,
|
|
||||||
vec: &'a [u8],
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yes, I know Cursor exists and eio exists. from_be_bytes should be a stdlib trait tho.
|
|
||||||
impl<'a> PrimitiveReader<'a> {
|
|
||||||
fn read_u8(&mut self) -> u8 {
|
|
||||||
let number = self.vec[self.offset];
|
|
||||||
self.offset += 1;
|
|
||||||
number
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u16(&mut self) -> u16 {
|
|
||||||
let arr: [u8; 2] = self.read_arr_const();
|
|
||||||
u16::from_be_bytes(arr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u32(&mut self) -> u32 {
|
|
||||||
let arr: [u8; 4] = self.read_arr_const();
|
|
||||||
u32::from_be_bytes(arr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u64(&mut self) -> u64 {
|
|
||||||
let arr: [u8; 8] = self.read_arr_const();
|
|
||||||
u64::from_be_bytes(arr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_arr_const<const N: usize>(&mut self) -> [u8; N] {
|
|
||||||
let mut arr = [0u8; N];
|
|
||||||
for n in 0..N {
|
|
||||||
arr[n] = self.read_u8();
|
|
||||||
}
|
|
||||||
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_arr(&mut self, n: usize) -> Vec<u8> {
|
|
||||||
let mut arr: Vec<u8> = vec![];
|
|
||||||
for _ in 0..n {
|
|
||||||
arr.push(self.read_u8());
|
|
||||||
}
|
|
||||||
|
|
||||||
arr
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_arr_from_len(&mut self) -> Vec<u8> {
|
|
||||||
let size = self.read_u32() as usize;
|
|
||||||
self.read_arr(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn left_to_process(&self) -> usize {
|
|
||||||
self.vec.len() - self.offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
fn checksum(bin: &[u8]) -> u16 {
|
fn checksum(bin: &[u8]) -> u16 {
|
||||||
// CRC16 of some kind...
|
// CRC16 of some kind...
|
||||||
|
|
|
@ -6,9 +6,10 @@ use ed25519_dalek::Signer;
|
||||||
|
|
||||||
|
|
||||||
pub struct CustomerKeySeed {
|
pub struct CustomerKeySeed {
|
||||||
postnumber: String,
|
pub postnumber: String,
|
||||||
seed: Seed,
|
pub seed: Seed,
|
||||||
uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
|
pub device_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Seed {
|
pub struct Seed {
|
||||||
|
@ -44,17 +45,25 @@ impl CustomerKeySeed {
|
||||||
postnumber,
|
postnumber,
|
||||||
seed: Seed::random(),
|
seed: Seed::random(),
|
||||||
uuid: uuid::Uuid::new_v4(),
|
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 {
|
CustomerKeySeed {
|
||||||
postnumber: postnumber.clone(),
|
postnumber: postnumber.clone(),
|
||||||
seed: Seed::from_bytes(seed),
|
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 {
|
pub(crate) fn sign(&self, message: &[u8]) -> String {
|
||||||
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());
|
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
#[cfg(feature = "locker_ble")]
|
#[cfg(feature = "locker_ble")]
|
||||||
|
#[cfg(feature = "unstable")]
|
||||||
mod command;
|
mod command;
|
||||||
#[cfg(feature = "locker_ble")]
|
#[cfg(feature = "locker_ble")]
|
||||||
|
#[cfg(feature = "unstable")]
|
||||||
mod types;
|
mod types;
|
||||||
#[cfg(feature = "locker_ble")]
|
#[cfg(feature = "locker_ble")]
|
||||||
|
#[cfg(feature = "unstable")]
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "locker_ble")]
|
||||||
|
#[cfg(feature = "unstable")]
|
||||||
mod api;
|
mod api;
|
||||||
|
#[cfg(feature = "locker_ble")]
|
||||||
|
#[cfg(feature = "unstable")]
|
||||||
|
pub use api::*;
|
||||||
|
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
|
|
||||||
|
pub(crate) mod utils;
|
||||||
|
|
||||||
#[cfg(feature = "locker_register_base")]
|
#[cfg(feature = "locker_register_base")]
|
||||||
mod register_base;
|
mod register_base;
|
||||||
#[cfg(feature = "locker_register_regtoken")]
|
#[cfg(feature = "locker_register_regtoken")]
|
||||||
|
|
|
@ -1,23 +1,107 @@
|
||||||
use crate::LibraryError;
|
use btleplug::platform::PeripheralId;
|
||||||
|
|
||||||
|
use super::utils::PrimitiveReader;
|
||||||
|
use crate::{LibraryError, LibraryResult};
|
||||||
|
|
||||||
// 601e7028-0565-
|
// 601e7028-0565-
|
||||||
pub static LOCKER_SERVICE_UUID_PREFIX: (u32, u16) = (0x601e7028, 0x0565);
|
pub static LOCKER_SERVICE_UUID_PREFIX: (u32, u16) = (0x601e7028, 0x0565);
|
||||||
|
|
||||||
|
pub enum LockerVendor {
|
||||||
|
Keba,
|
||||||
|
Snbc,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for LockerVendor {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
if value == 1 {
|
||||||
|
Self::Keba
|
||||||
|
} else if value == 2 {
|
||||||
|
Self::Snbc
|
||||||
|
} else {
|
||||||
|
Self::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LockerVersion {
|
||||||
|
pub tlv_version: u8,
|
||||||
|
pub vendor: u8,
|
||||||
|
pub major: u8,
|
||||||
|
pub minor: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LockerDevice {
|
||||||
|
pub id: PeripheralId,
|
||||||
|
pub service_uuid: LockerServiceUUID,
|
||||||
|
pub version: LockerVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockerDevice {
|
||||||
|
pub(crate) fn new(
|
||||||
|
id: PeripheralId,
|
||||||
|
service_uuid: LockerServiceUUID,
|
||||||
|
service_data: &Vec<u8>,
|
||||||
|
) -> LibraryResult<Self> {
|
||||||
|
mini_assert_inval!(service_data.len() == 14);
|
||||||
|
|
||||||
|
let mut reader = PrimitiveReader {
|
||||||
|
offset: 0,
|
||||||
|
vec: &service_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
let version = LockerVersion {
|
||||||
|
tlv_version: reader.read_u8(),
|
||||||
|
vendor: reader.read_u8(),
|
||||||
|
major: reader.read_u8(),
|
||||||
|
minor: reader.read_u8(),
|
||||||
|
};
|
||||||
|
let part1: u16 = reader.read_u16();
|
||||||
|
let part2: u16 = reader.read_u16();
|
||||||
|
let part_last = reader.read_u32();
|
||||||
|
assert_eq!(reader.left_to_process(), 0);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"decoded: {:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>8x}",
|
||||||
|
LOCKER_SERVICE_UUID_PREFIX.0, LOCKER_SERVICE_UUID_PREFIX.1, part1, part2, part_last
|
||||||
|
);
|
||||||
|
println!("expected: {:?}", service_uuid);
|
||||||
|
|
||||||
|
Ok(LockerDevice {
|
||||||
|
id,
|
||||||
|
service_uuid,
|
||||||
|
version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct LockerServiceUUID {
|
pub struct LockerServiceUUID {
|
||||||
service_uuid: uuid::Uuid,
|
service_uuid: uuid::Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LockerServiceUUID {
|
||||||
|
pub fn get(&self) -> &uuid::Uuid {
|
||||||
|
&self.service_uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<uuid::Uuid> for LockerServiceUUID {
|
impl TryFrom<uuid::Uuid> for LockerServiceUUID {
|
||||||
type Error = crate::LibraryError;
|
type Error = crate::LibraryError;
|
||||||
|
|
||||||
fn try_from(value: uuid::Uuid) -> Result<Self, Self::Error> {
|
fn try_from(value: uuid::Uuid) -> LibraryResult<Self> {
|
||||||
let fields = value.as_fields();
|
let fields = value.as_fields();
|
||||||
if fields.0 != LOCKER_SERVICE_UUID_PREFIX.0 || fields.1 != LOCKER_SERVICE_UUID_PREFIX.1 {
|
if fields.0 != LOCKER_SERVICE_UUID_PREFIX.0 || fields.1 != LOCKER_SERVICE_UUID_PREFIX.1 {
|
||||||
return Err(LibraryError::InvalidArgument("TryFrom<Uuid> for LockerServiceUUID: prefix mismatch (expected 601e7028-0565-)".to_string()))
|
return Err(LibraryError::InvalidArgument(
|
||||||
|
"TryFrom<Uuid> for LockerServiceUUID: prefix mismatch (expected 601e7028-0565-)"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(LockerServiceUUID {
|
Ok(LockerServiceUUID {
|
||||||
service_uuid: value
|
service_uuid: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,4 +110,4 @@ impl Into<uuid::Uuid> for LockerServiceUUID {
|
||||||
fn into(self) -> uuid::Uuid {
|
fn into(self) -> uuid::Uuid {
|
||||||
self.service_uuid
|
self.service_uuid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
93
libpaket/src/locker/utils.rs
Normal file
93
libpaket/src/locker/utils.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
pub(crate) struct PrimitiveBuilder {
|
||||||
|
pub bin: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveBuilder {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
PrimitiveBuilder { bin: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_u8(self, number: u8) -> Self {
|
||||||
|
self.write_array(&[number])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_u16(self, number: u16) -> Self {
|
||||||
|
self.write_array(&number.to_be_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_u32(self, number: u32) -> Self {
|
||||||
|
self.write_array(&number.to_be_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_array(mut self, new: &[u8]) -> Self {
|
||||||
|
for b in new {
|
||||||
|
self.bin.push(*b);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_array_with_len(self, bin: &[u8]) -> Self {
|
||||||
|
self.write_u32(bin.len() as u32).write_array(bin)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish(self) -> Vec<u8> {
|
||||||
|
self.bin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PrimitiveReader<'a> {
|
||||||
|
pub offset: usize,
|
||||||
|
pub vec: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes, I know Cursor exists and eio exists. from_be_bytes should be a stdlib trait tho.
|
||||||
|
impl<'a> PrimitiveReader<'a> {
|
||||||
|
pub(crate) fn read_u8(&mut self) -> u8 {
|
||||||
|
let number = self.vec[self.offset];
|
||||||
|
self.offset += 1;
|
||||||
|
number
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u16(&mut self) -> u16 {
|
||||||
|
let arr: [u8; 2] = self.read_arr_const();
|
||||||
|
u16::from_be_bytes(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u32(&mut self) -> u32 {
|
||||||
|
let arr: [u8; 4] = self.read_arr_const();
|
||||||
|
u32::from_be_bytes(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u64(&mut self) -> u64 {
|
||||||
|
let arr: [u8; 8] = self.read_arr_const();
|
||||||
|
u64::from_be_bytes(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_arr_const<const N: usize>(&mut self) -> [u8; N] {
|
||||||
|
let mut arr = [0u8; N];
|
||||||
|
for n in 0..N {
|
||||||
|
arr[n] = self.read_u8();
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_arr(&mut self, n: usize) -> Vec<u8> {
|
||||||
|
let mut arr: Vec<u8> = vec![];
|
||||||
|
for _ in 0..n {
|
||||||
|
arr.push(self.read_u8());
|
||||||
|
}
|
||||||
|
|
||||||
|
arr
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_arr_from_len(&mut self) -> Vec<u8> {
|
||||||
|
let size = self.read_u32() as usize;
|
||||||
|
self.read_arr(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn left_to_process(&self) -> usize {
|
||||||
|
self.vec.len() - self.offset
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,13 +4,12 @@ mod openid_response;
|
||||||
pub mod openid_token;
|
pub mod openid_token;
|
||||||
mod utils;
|
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::openid_response::{RefreshToken, TokenResponse};
|
||||||
pub use self::utils::{CodeVerfier, create_nonce};
|
pub use self::utils::{create_nonce, CodeVerfier};
|
||||||
|
|
||||||
use super::common::APIResult;
|
use super::common::APIResult;
|
||||||
use crate::{LibraryError, LibraryResult};
|
use crate::LibraryResult;
|
||||||
|
|
||||||
|
|
||||||
pub struct OpenIdClient {
|
pub struct OpenIdClient {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
|
@ -31,27 +30,12 @@ impl OpenIdClient {
|
||||||
&self,
|
&self,
|
||||||
refresh_token: &RefreshToken,
|
refresh_token: &RefreshToken,
|
||||||
) -> LibraryResult<TokenResponse> {
|
) -> LibraryResult<TokenResponse> {
|
||||||
let req = self
|
let res = request_post!(self.client,
|
||||||
.client
|
(constants::token::endpoint()),
|
||||||
.post(constants::token::endpoint())
|
|
||||||
.form(constants::token::refresh_token_form(refresh_token.as_str()).as_slice())
|
.form(constants::token::refresh_token_form(refresh_token.as_str()).as_slice())
|
||||||
.build()
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let res = self.client.execute(req).await;
|
Ok(parse_json_response_from_apiresult!(res, TokenResponse))
|
||||||
|
|
||||||
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(
|
pub async fn token_authorization(
|
||||||
|
@ -78,23 +62,9 @@ impl OpenIdClient {
|
||||||
let headermap = req.headers_mut();
|
let headermap = req.headers_mut();
|
||||||
headermap.append("Content-Length", len.into());
|
headermap.append("Content-Length", len.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = self.client.execute(req).await;
|
let res = self.client.execute(req).await;
|
||||||
|
let res = parse_response_internal!(res);
|
||||||
|
|
||||||
println!("auth_code: {:?}", res);
|
Ok(parse_json_response_from_apiresult!(res, TokenResponse))
|
||||||
|
|
||||||
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)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
446
libpaket/src/tracking.rs
Normal file
446
libpaket/src/tracking.rs
Normal file
|
@ -0,0 +1,446 @@
|
||||||
|
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, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub 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, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SendungsVerlauf {
|
||||||
|
kurz_status: Option<String>,
|
||||||
|
icon_id: Option<String>,
|
||||||
|
datum_aktueller_status: Option<String>,
|
||||||
|
aktueller_status: Option<String>,
|
||||||
|
pub events: Option<Vec<SendungsVerlaufEvent>>,
|
||||||
|
|
||||||
|
farbe: u32,
|
||||||
|
pub fortschritt: u32,
|
||||||
|
pub 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 history: SendungsVerlauf,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
history: value.sendungsdetails.sendungsverlauf,
|
||||||
|
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 {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
@ -54,7 +55,7 @@ macro_rules! mini_assert_api_eq {
|
||||||
if $a != $b {
|
if $a != $b {
|
||||||
return Err(crate::LibraryError::APIChange);
|
return Err(crate::LibraryError::APIChange);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! mini_assert_api {
|
macro_rules! mini_assert_api {
|
||||||
|
@ -62,14 +63,163 @@ macro_rules! mini_assert_api {
|
||||||
if !$a {
|
if !$a {
|
||||||
return Err(crate::LibraryError::APIChange);
|
return Err(crate::LibraryError::APIChange);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
macro_rules! mini_assert_inval {
|
macro_rules! mini_assert_inval {
|
||||||
($a: expr) => {
|
($a: expr) => {
|
||||||
if !$a {
|
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")
|
("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",
|
||||||
|
|
|
@ -16,3 +16,4 @@ reqwest = "0.12"
|
||||||
libpaket = { path = "../libpaket" }
|
libpaket = { path = "../libpaket" }
|
||||||
glycin = { version = "2.0.0-beta", features = ["gdk4"] }
|
glycin = { version = "2.0.0-beta", features = ["gdk4"] }
|
||||||
oo7 = { version = "0.3" }
|
oo7 = { version = "0.3" }
|
||||||
|
relm4-icons = { version = "0.9" }
|
|
@ -1,13 +1,13 @@
|
||||||
|
use adw::{gio, glib};
|
||||||
|
use gtk::gdk;
|
||||||
use libpaket::advices::UatToken;
|
use libpaket::advices::UatToken;
|
||||||
use libpaket::LibraryError;
|
use libpaket::LibraryError;
|
||||||
use relm4::gtk;
|
use relm4::gtk;
|
||||||
use gtk::gdk;
|
|
||||||
use adw::{gio, glib};
|
|
||||||
|
|
||||||
use relm4::prelude::*;
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use glib::prelude::*;
|
|
||||||
use gio::prelude::*;
|
use gio::prelude::*;
|
||||||
|
use glib::prelude::*;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppAdviceMetadata {
|
pub struct AppAdviceMetadata {
|
||||||
|
@ -42,16 +42,24 @@ impl FactoryComponent for AppAdvice {
|
||||||
add_overlay = >k::Spinner {
|
add_overlay = >k::Spinner {
|
||||||
start: (),
|
start: (),
|
||||||
set_align: gtk::Align::Center,
|
set_align: gtk::Align::Center,
|
||||||
|
|
||||||
#[track(self.changed_texture())]
|
#[track(self.changed_texture())]
|
||||||
set_visible: self.texture.is_none(),
|
set_visible: self.texture.is_none(),
|
||||||
},
|
},
|
||||||
|
|
||||||
add_overlay = >k::Label {
|
add_overlay = >k::Box {
|
||||||
set_halign: gtk::Align::Center,
|
add_css_class: relm4::css::OSD,
|
||||||
set_valign: gtk::Align::Center,
|
add_css_class: relm4::css::TOOLBAR,
|
||||||
|
add_css_class: relm4::css::NUMERIC,
|
||||||
|
|
||||||
set_label: self.metadata.date.as_str(),
|
set_valign: gtk::Align::End,
|
||||||
|
set_halign: gtk::Align::End,
|
||||||
|
|
||||||
|
set_margin_all: 8,
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_label: self.metadata.date.as_str(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
|
@ -71,9 +79,11 @@ impl FactoryComponent for AppAdvice {
|
||||||
|
|
||||||
let advice = _self.metadata.advice.clone();
|
let advice = _self.metadata.advice.clone();
|
||||||
let uat = value.1;
|
let uat = value.1;
|
||||||
|
|
||||||
sender.oneshot_command(async move {
|
sender.oneshot_command(async move {
|
||||||
let res = libpaket::advices::AdviceClient::new().fetch_advice_image(&advice, &uat).await;
|
let res = libpaket::advices::AdviceClient::new()
|
||||||
|
.fetch_advice_image(&advice, &uat)
|
||||||
|
.await;
|
||||||
|
|
||||||
let res = match res {
|
let res = match res {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
|
@ -83,13 +93,21 @@ impl FactoryComponent for AppAdvice {
|
||||||
let file = {
|
let file = {
|
||||||
let (file, io_stream) = gio::File::new_tmp(None::<&std::path::Path>).unwrap();
|
let (file, io_stream) = gio::File::new_tmp(None::<&std::path::Path>).unwrap();
|
||||||
let output_stream = io_stream.output_stream();
|
let output_stream = io_stream.output_stream();
|
||||||
output_stream.write(res.as_slice(), None::<&gio::Cancellable>).unwrap();
|
output_stream
|
||||||
|
.write(res.as_slice(), None::<&gio::Cancellable>)
|
||||||
|
.unwrap();
|
||||||
file
|
file
|
||||||
};
|
};
|
||||||
|
|
||||||
let image = glycin::Loader::new(file).load().await.expect("Image decoding failed");
|
let image = glycin::Loader::new(file)
|
||||||
let frame = image.next_frame().await.expect("Image frame decoding failed");
|
.load()
|
||||||
|
.await
|
||||||
|
.expect("Image decoding failed");
|
||||||
|
let frame = image
|
||||||
|
.next_frame()
|
||||||
|
.await
|
||||||
|
.expect("Image frame decoding failed");
|
||||||
|
|
||||||
AppAdviceCmds::GotTexture(frame.texture())
|
AppAdviceCmds::GotTexture(frame.texture())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,8 +117,7 @@ impl FactoryComponent for AppAdvice {
|
||||||
fn update_cmd(&mut self, message: Self::CommandOutput, sender: FactorySender<Self>) {
|
fn update_cmd(&mut self, message: Self::CommandOutput, sender: FactorySender<Self>) {
|
||||||
match message {
|
match message {
|
||||||
AppAdviceCmds::GotTexture(texture) => self.set_texture(Some(texture)),
|
AppAdviceCmds::GotTexture(texture) => self.set_texture(Some(texture)),
|
||||||
AppAdviceCmds::Error(err) => todo!()
|
AppAdviceCmds::Error(err) => todo!(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -128,24 +128,28 @@ impl AsyncComponent for Login {
|
||||||
.expect("sender not worky");
|
.expect("sender not worky");
|
||||||
} else {
|
} else {
|
||||||
match keyring
|
match keyring
|
||||||
.search_items(&HashMap::from(KEYRING_ATTRIBUTES))
|
.search_items(&HashMap::from(KEYRING_ATTRIBUTES))
|
||||||
.await {
|
.await
|
||||||
|
{
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
if res.len() > 0 {
|
if res.len() > 0 {
|
||||||
let item = &res[0];
|
let item = &res[0];
|
||||||
let refresh_token = item.secret().await.unwrap();
|
let refresh_token = item.secret().await.unwrap();
|
||||||
let refresh_token = std::str::from_utf8(refresh_token.as_slice()).unwrap();
|
let refresh_token =
|
||||||
model.refresh_token = Some(RefreshToken::new(refresh_token.to_string()).unwrap());
|
std::str::from_utf8(refresh_token.as_slice()).unwrap();
|
||||||
|
model.refresh_token = Some(
|
||||||
|
RefreshToken::new(refresh_token.to_string()).unwrap(),
|
||||||
|
);
|
||||||
sender.input(LoginInput::NeedsRefresh);
|
sender.input(LoginInput::NeedsRefresh);
|
||||||
} else {
|
} else {
|
||||||
sender.input(LoginInput::NeedsLogin);
|
sender.input(LoginInput::NeedsLogin);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
sender
|
sender
|
||||||
.output(LoginOutput::KeyringError(err))
|
.output(LoginOutput::KeyringError(err))
|
||||||
.expect("sender not worky");
|
.expect("sender not worky");
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,7 +212,9 @@ impl AsyncComponent for Login {
|
||||||
}
|
}
|
||||||
LoginInput::BreakWorld => {
|
LoginInput::BreakWorld => {
|
||||||
self.set_state(LoginState::Offline);
|
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 shared_id_token = self.shared_id_token.lock().await;
|
||||||
let mut shared_id_token = shared_id_token.write();
|
let mut shared_id_token = shared_id_token.write();
|
||||||
|
@ -294,7 +300,15 @@ impl Login {
|
||||||
let future = async {
|
let future = async {
|
||||||
self.refresh_token = Some(res.refresh_token);
|
self.refresh_token = Some(res.refresh_token);
|
||||||
let keyring = KEYRING.get().unwrap();
|
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() {
|
if !res.id_token.is_expired() {
|
||||||
|
@ -323,62 +337,42 @@ impl Login {
|
||||||
libpaket::LibraryError::Unauthorized => {
|
libpaket::LibraryError::Unauthorized => {
|
||||||
sender.input(LoginInput::NeedsLogin);
|
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 {
|
macro_rules! received {
|
||||||
match err {
|
($func_name:ident, $args:tt, $calling:ident, $calling_args:tt) => {
|
||||||
LibraryError::NetworkFetch => ResponseType::Retry,
|
async fn $func_name$args -> LoginCommand {
|
||||||
LibraryError::Unauthorized => ResponseType::Okay,
|
let client = OpenIdClient::new();
|
||||||
LibraryError::InvalidArgument(_) => ResponseType::Okay,
|
let mut err = LibraryError::NetworkFetch;
|
||||||
LibraryError::DecodeError(_) => ResponseType::Okay,
|
for _ in 0..6 {
|
||||||
LibraryError::APIChange => ResponseType::Okay,
|
let result: Result<TokenResponse, LibraryError> = client
|
||||||
}
|
.$calling$calling_args
|
||||||
}
|
.await;
|
||||||
|
|
||||||
async fn received_auth_code(auth_code: String, code_verifier: CodeVerfier) -> LoginCommand {
|
err = match result {
|
||||||
let client = OpenIdClient::new();
|
Ok(result) => return LoginCommand::Token(Ok(result)),
|
||||||
let mut err = LibraryError::NetworkFetch;
|
Err(err) => err,
|
||||||
for _ in 0..6 {
|
};
|
||||||
let result: Result<TokenResponse, LibraryError> = client
|
|
||||||
.token_authorization(auth_code.clone(), &code_verifier)
|
if err == LibraryError::NetworkFetch {
|
||||||
.await;
|
continue;
|
||||||
|
} else {
|
||||||
if let Ok(result) = result {
|
return LoginCommand::Token(Err(err));
|
||||||
return LoginCommand::Token(Ok(result))
|
}
|
||||||
|
}
|
||||||
|
LoginCommand::Token(Err(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
received!(received_auth_code, (auth_code: String, code_verifier: CodeVerfier), token_authorization, (auth_code.clone(), &code_verifier));
|
||||||
let client = OpenIdClient::new();
|
received!(use_refresh_token, (refresh_token: RefreshToken), token_refresh, (&refresh_token));
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||||
use login::{Login, LoginOutput, LoginSharedState};
|
use login::{Login, LoginOutput, LoginSharedState};
|
||||||
use ready::{Ready, ReadyOutput};
|
use ready::{Ready, ReadyOutput};
|
||||||
use relm4::{
|
use relm4::{
|
||||||
|
RELM_THREADS,
|
||||||
adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex,
|
adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex,
|
||||||
AsyncComponentSender, SharedState,
|
AsyncComponentSender, SharedState,
|
||||||
};
|
};
|
||||||
|
@ -13,6 +14,7 @@ mod advices;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod login;
|
mod login;
|
||||||
mod ready;
|
mod ready;
|
||||||
|
mod tracking;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum AppState {
|
enum AppState {
|
||||||
|
@ -282,11 +284,19 @@ fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
||||||
long: "There is no feature on your account which is supported by this app. You need the offical app and register for one or more of:\n\"Briefasnkündigung\"".to_string(),
|
long: "There is no feature on your account which is supported by this app. You need the offical app and register for one or more of:\n\"Briefasnkündigung\"".to_string(),
|
||||||
}),
|
}),
|
||||||
ReadyOutput::Error(err) => AppInput::ErrorOccoured(AppError { short: "meow".to_string(), long: err.to_string() }),
|
ReadyOutput::Error(err) => AppInput::ErrorOccoured(AppError { short: "meow".to_string(), long: err.to_string() }),
|
||||||
|
ReadyOutput::Notification(value) => AppInput::Notification(value, 60),
|
||||||
ReadyOutput::Ready => AppInput::SwitchToReady,
|
ReadyOutput::Ready => AppInput::SwitchToReady,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
RELM_THREADS.set(4).unwrap();
|
||||||
|
gtk::init().unwrap();
|
||||||
|
let display = gtk::gdk::Display::default().unwrap();
|
||||||
|
let theme = gtk::IconTheme::for_display(&display);
|
||||||
|
theme.add_resource_path("/de/j4ne/Paket/icons/");
|
||||||
|
theme.add_resource_path("/de/j4ne/Paket/scalable/actions/");
|
||||||
|
relm4_icons::initialize_icons();
|
||||||
let app = RelmApp::new(constants::APP_ID);
|
let app = RelmApp::new(constants::APP_ID);
|
||||||
app.run_async::<App>(());
|
app.run_async::<App>(());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
// managed the various pages...
|
// managed the various pages...
|
||||||
|
|
||||||
|
use std::hash::DefaultHasher;
|
||||||
|
use std::string;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use libpaket::{
|
use libpaket::{
|
||||||
self,
|
self,
|
||||||
advices::{AdvicesList, UatToken},
|
advices::{AdvicesList, UatToken},
|
||||||
|
tracking::{Shipment, TrackingParams},
|
||||||
LibraryError, LibraryResult,
|
LibraryError, LibraryResult,
|
||||||
};
|
};
|
||||||
use relm4::{adw, factory::FactoryVecDeque, prelude::*};
|
use relm4::{
|
||||||
|
adw,
|
||||||
|
factory::{FactoryHashMap, FactorySender, FactoryVecDeque},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::advices::AppAdviceMetadata;
|
use crate::advices::AppAdviceMetadata;
|
||||||
|
|
||||||
|
@ -25,9 +32,13 @@ pub struct Ready {
|
||||||
login: crate::LoginSharedState,
|
login: crate::LoginSharedState,
|
||||||
activate: bool,
|
activate: bool,
|
||||||
have_service_advices: bool,
|
have_service_advices: bool,
|
||||||
|
|
||||||
#[do_not_track]
|
#[do_not_track]
|
||||||
advices_factory: FactoryVecDeque<crate::advices::AppAdvice>,
|
advices_factory: FactoryVecDeque<crate::advices::AppAdvice>,
|
||||||
advices_state: ReadyAdvicesState,
|
advices_state: ReadyAdvicesState,
|
||||||
|
|
||||||
|
#[do_not_track]
|
||||||
|
tracking_factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -35,6 +46,7 @@ pub enum ReadyOutput {
|
||||||
Ready,
|
Ready,
|
||||||
Error(LibraryError),
|
Error(LibraryError),
|
||||||
FatalError(LibraryError),
|
FatalError(LibraryError),
|
||||||
|
Notification(String),
|
||||||
NoServicesEnabled,
|
NoServicesEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +57,7 @@ pub enum ReadyCmds {
|
||||||
GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>),
|
GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>),
|
||||||
RetryAdvices,
|
RetryAdvices,
|
||||||
GotAdvices((LibraryResult<Vec<AppAdviceMetadata>>, Option<UatToken>)),
|
GotAdvices((LibraryResult<Vec<AppAdviceMetadata>>, Option<UatToken>)),
|
||||||
|
GotTracking(LibraryResult<Vec<Shipment>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -52,6 +65,8 @@ pub enum ReadyInput {
|
||||||
Activate,
|
Activate,
|
||||||
Deactivate,
|
Deactivate,
|
||||||
HaveAdvicesService,
|
HaveAdvicesService,
|
||||||
|
HavePaketankuendigungService,
|
||||||
|
SearchTracking(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(pub)]
|
#[relm4::component(pub)]
|
||||||
|
@ -77,18 +92,23 @@ impl Component for Ready {
|
||||||
set_title: "No mail notifications available."
|
set_title: "No mail notifications available."
|
||||||
},
|
},
|
||||||
#[name = "advices_page_have_some"]
|
#[name = "advices_page_have_some"]
|
||||||
add = >k::Box {
|
add = &adw::Clamp {
|
||||||
set_orientation: gtk::Orientation::Horizontal,
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
|
||||||
#[local_ref]
|
#[local_ref]
|
||||||
advices_carousel -> adw::Carousel {
|
advices_carousel -> adw::Carousel {
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
adw::CarouselIndicatorDots {
|
||||||
|
#[watch]
|
||||||
|
set_carousel: Some(advices_carousel),
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
adw::CarouselIndicatorDots {
|
|
||||||
set_carousel: Some(advices_carousel),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
#[track(model.changed_advices_state())]
|
#[track(model.changed_advices_state())]
|
||||||
|
@ -103,9 +123,44 @@ impl Component for Ready {
|
||||||
},
|
},
|
||||||
} -> /*page_advices: adw::ViewStackPage*/ {
|
} -> /*page_advices: adw::ViewStackPage*/ {
|
||||||
set_title: Some("Mail notification"),
|
set_title: Some("Mail notification"),
|
||||||
|
set_name: Some("page_advices"),
|
||||||
/*#[track(model.changed_have_service_advices())]
|
/*#[track(model.changed_have_service_advices())]
|
||||||
set_visible: model.have_service_advices,*/
|
set_visible: model.have_service_advices,*/
|
||||||
}
|
},
|
||||||
|
|
||||||
|
add = &adw::Bin {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::ScrolledWindow {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_margin_all: 8,
|
||||||
|
add_css_class: relm4::css::TOOLBAR,
|
||||||
|
|
||||||
|
#[name = "tracking_entry"]
|
||||||
|
gtk::Entry {
|
||||||
|
set_input_hints: gtk::InputHints::PRIVATE,
|
||||||
|
set_hexpand: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[name = "tracking_entry_button"]
|
||||||
|
gtk::Button {}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
#[local_ref]
|
||||||
|
tracking_box -> gtk::Box {
|
||||||
|
set_spacing: 8
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} -> /*page_tracking: adw::ViewStackPage*/ {
|
||||||
|
set_title: Some("Shipment tracking"),
|
||||||
|
set_name: Some("page_tracking"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,9 +169,9 @@ impl Component for Ready {
|
||||||
root: Self::Root,
|
root: Self::Root,
|
||||||
sender: ComponentSender<Self>,
|
sender: ComponentSender<Self>,
|
||||||
) -> ComponentParts<Self> {
|
) -> ComponentParts<Self> {
|
||||||
let advices_factory = FactoryVecDeque::builder()
|
let advices_factory = FactoryVecDeque::builder().launch_default().detach();
|
||||||
.launch(adw::Carousel::new())
|
|
||||||
.detach();
|
let tracking_factory = FactoryHashMap::builder().launch_default().detach();
|
||||||
|
|
||||||
let model = Ready {
|
let model = Ready {
|
||||||
have_service_advices: false,
|
have_service_advices: false,
|
||||||
|
@ -124,10 +179,12 @@ impl Component for Ready {
|
||||||
advices_state: ReadyAdvicesState::Loading,
|
advices_state: ReadyAdvicesState::Loading,
|
||||||
login: init.clone(),
|
login: init.clone(),
|
||||||
activate: false,
|
activate: false,
|
||||||
|
tracking_factory,
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let advices_carousel = model.advices_factory.widget();
|
let advices_carousel = model.advices_factory.widget();
|
||||||
|
let tracking_box = model.tracking_factory.widget();
|
||||||
|
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
{
|
{
|
||||||
|
@ -148,6 +205,21 @@ impl Component for Ready {
|
||||||
.drop_on_shutdown()
|
.drop_on_shutdown()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
let sender = sender.clone();
|
||||||
|
widgets.tracking_entry.connect_activate(move |entry| {
|
||||||
|
sender.input(ReadyInput::SearchTracking(entry.text().into()));
|
||||||
|
entry.set_text("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let sender = sender.clone();
|
||||||
|
let entry = widgets.tracking_entry.clone();
|
||||||
|
widgets.tracking_entry_button.connect_clicked(move |_| {
|
||||||
|
sender.input(ReadyInput::SearchTracking(entry.text().into()));
|
||||||
|
entry.set_text("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
|
@ -155,11 +227,11 @@ impl Component for Ready {
|
||||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
|
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
let token = self.login.clone();
|
|
||||||
match message {
|
match message {
|
||||||
ReadyInput::Activate => {
|
ReadyInput::Activate => {
|
||||||
self.set_activate(true);
|
self.set_activate(true);
|
||||||
if self.changed_activate() {
|
if self.changed_activate() {
|
||||||
|
let token = self.login.clone();
|
||||||
sender.oneshot_command(async move {
|
sender.oneshot_command(async move {
|
||||||
let token = crate::login::get_id_token(&token).await.unwrap();
|
let token = crate::login::get_id_token(&token).await.unwrap();
|
||||||
let client = libpaket::StammdatenClient::new();
|
let client = libpaket::StammdatenClient::new();
|
||||||
|
@ -167,10 +239,48 @@ impl Component for Ready {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ReadyInput::SearchTracking(value) => {
|
||||||
|
sender.oneshot_command(async move {
|
||||||
|
let client = libpaket::WebClient::new();
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
vec.push(value);
|
||||||
|
ReadyCmds::GotTracking(
|
||||||
|
client
|
||||||
|
.tracking_search(
|
||||||
|
TrackingParams {
|
||||||
|
language: Some("de".to_string()),
|
||||||
|
},
|
||||||
|
vec,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
ReadyInput::Deactivate => {
|
ReadyInput::Deactivate => {
|
||||||
self.set_activate(false);
|
self.set_activate(false);
|
||||||
}
|
}
|
||||||
|
ReadyInput::HavePaketankuendigungService => {
|
||||||
|
let token = self.login.clone();
|
||||||
|
sender.oneshot_command(async move {
|
||||||
|
// fetching advices
|
||||||
|
let dhli_token = crate::login::get_id_token(&token).await.unwrap();
|
||||||
|
let client = libpaket::WebClient::new();
|
||||||
|
ReadyCmds::GotTracking(
|
||||||
|
client
|
||||||
|
.tracking_search(
|
||||||
|
TrackingParams {
|
||||||
|
language: Some("de".to_string()),
|
||||||
|
},
|
||||||
|
Vec::new(),
|
||||||
|
Some(&dhli_token),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
ReadyInput::HaveAdvicesService => {
|
ReadyInput::HaveAdvicesService => {
|
||||||
|
let token = self.login.clone();
|
||||||
sender.oneshot_command(async move {
|
sender.oneshot_command(async move {
|
||||||
// fetching advices
|
// fetching advices
|
||||||
let dhli_token = crate::login::get_id_token(&token).await.unwrap();
|
let dhli_token = crate::login::get_id_token(&token).await.unwrap();
|
||||||
|
@ -217,28 +327,82 @@ impl Component for Ready {
|
||||||
ReadyCmds::LoggedOut => sender.input(ReadyInput::Deactivate),
|
ReadyCmds::LoggedOut => sender.input(ReadyInput::Deactivate),
|
||||||
ReadyCmds::GotCustomerDataFull(res) => match res {
|
ReadyCmds::GotCustomerDataFull(res) => match res {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let mut a_service_was_activated = false;
|
|
||||||
for service in &res.common.services {
|
for service in &res.common.services {
|
||||||
match service {
|
match service {
|
||||||
libpaket::stammdaten::CustomerDataService::Packstation => (),
|
libpaket::stammdaten::CustomerDataService::Packstation => (),
|
||||||
libpaket::stammdaten::CustomerDataService::Paketankuendigung => (),
|
libpaket::stammdaten::CustomerDataService::Paketankuendigung => {
|
||||||
|
sender.input(ReadyInput::HavePaketankuendigungService);
|
||||||
|
}
|
||||||
libpaket::stammdaten::CustomerDataService::PostfilialeDirekt => (),
|
libpaket::stammdaten::CustomerDataService::PostfilialeDirekt => (),
|
||||||
libpaket::stammdaten::CustomerDataService::Digiben => (),
|
libpaket::stammdaten::CustomerDataService::Digiben => (),
|
||||||
libpaket::stammdaten::CustomerDataService::GeraetAktiviert => (),
|
libpaket::stammdaten::CustomerDataService::GeraetAktiviert => (),
|
||||||
libpaket::stammdaten::CustomerDataService::Briefankuendigung => {
|
libpaket::stammdaten::CustomerDataService::Briefankuendigung => {
|
||||||
a_service_was_activated = true;
|
|
||||||
sender.input(ReadyInput::HaveAdvicesService);
|
sender.input(ReadyInput::HaveAdvicesService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a_service_was_activated {
|
|
||||||
sender.output(ReadyOutput::Ready).unwrap()
|
sender.output(ReadyOutput::Ready).unwrap()
|
||||||
} else {
|
|
||||||
sender.output(ReadyOutput::NoServicesEnabled).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(err) => sender.output(ReadyOutput::FatalError(err)).unwrap(),
|
Err(err) => sender.output(ReadyOutput::FatalError(err)).unwrap(),
|
||||||
},
|
},
|
||||||
|
ReadyCmds::GotTracking(res) => match res {
|
||||||
|
Ok(shipment_vec) => {
|
||||||
|
for item in shipment_vec {
|
||||||
|
if let Some(err) = item.error {
|
||||||
|
// TODO: gettext
|
||||||
|
if err.id_invalid {
|
||||||
|
sender
|
||||||
|
.output(ReadyOutput::Notification(format!(
|
||||||
|
"The id is invalid ({})",
|
||||||
|
item.id
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
} else if err.letter_not_found {
|
||||||
|
sender
|
||||||
|
.output(ReadyOutput::Notification(format!(
|
||||||
|
"The letter wasn't found ({})",
|
||||||
|
item.id
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
} else if err.id_not_searchable {
|
||||||
|
sender
|
||||||
|
.output(ReadyOutput::Notification(format!(
|
||||||
|
"The id is not searchable ({})",
|
||||||
|
item.id
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
} else if err.data_to_old {
|
||||||
|
sender
|
||||||
|
.output(ReadyOutput::Notification(format!(
|
||||||
|
"No data available with id ({}) (data expired)",
|
||||||
|
item.id
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
} else if err.not_from_dhl {
|
||||||
|
sender
|
||||||
|
.output(ReadyOutput::Notification(format!(
|
||||||
|
"The id is not from DHL ({})",
|
||||||
|
item.id
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
} else if err.no_data_available {
|
||||||
|
sender
|
||||||
|
.output(ReadyOutput::Notification(format!(
|
||||||
|
"No data available with id ({})",
|
||||||
|
item.id
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.tracking_factory.insert(item.id.clone(), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
sender.output(ReadyOutput::Error(err)).unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
ReadyCmds::GotAdvices(res) => match res.0 {
|
ReadyCmds::GotAdvices(res) => match res.0 {
|
||||||
Ok(advices_vec) => {
|
Ok(advices_vec) => {
|
||||||
{
|
{
|
||||||
|
|
193
paket/src/tracking.rs
Normal file
193
paket/src/tracking.rs
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
use adw::prelude::*;
|
||||||
|
use libpaket::tracking::Shipment;
|
||||||
|
use relm4::factory::FactoryComponent;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
use relm4::{adw, gtk};
|
||||||
|
|
||||||
|
pub struct ShipmentView {
|
||||||
|
model: Shipment,
|
||||||
|
|
||||||
|
// model abstraction
|
||||||
|
have_events: bool,
|
||||||
|
|
||||||
|
// state
|
||||||
|
expanded: bool,
|
||||||
|
|
||||||
|
// workarounds
|
||||||
|
list_box_history: gtk::ListBox,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ViewInput {
|
||||||
|
ToggleExpand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::factory(pub)]
|
||||||
|
impl FactoryComponent for ShipmentView {
|
||||||
|
type CommandOutput = ();
|
||||||
|
type Init = Shipment;
|
||||||
|
type Output = ();
|
||||||
|
type Input = ViewInput;
|
||||||
|
type ParentWidget = gtk::Box;
|
||||||
|
type Index = String;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[root]
|
||||||
|
gtk::Box {
|
||||||
|
add_css_class: relm4::css::CARD,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_margin_all: 8,
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
|
||||||
|
gtk::ProgressBar {
|
||||||
|
add_css_class: relm4::css::OSD,
|
||||||
|
set_margin_start: 8,
|
||||||
|
set_margin_end: 8,
|
||||||
|
set_margin_bottom: 1,
|
||||||
|
|
||||||
|
set_fraction: f64::from(self.model.history.maximal_fortschritt) / f64::from(self.model.history.fortschritt),
|
||||||
|
},
|
||||||
|
|
||||||
|
// title box
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_hexpand: true,
|
||||||
|
set_margin_all: 8,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_hexpand: true,
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_halign: gtk::Align::End,
|
||||||
|
add_css_class: relm4::css::FLAT,
|
||||||
|
|
||||||
|
#[watch]
|
||||||
|
set_icon_name: {
|
||||||
|
if self.expanded {
|
||||||
|
relm4_icons::icon_names::MINUS
|
||||||
|
} else {
|
||||||
|
relm4_icons::icon_names::PLUS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
connect_clicked => ViewInput::ToggleExpand,
|
||||||
|
}
|
||||||
|
}, // title box end
|
||||||
|
|
||||||
|
gtk::Revealer {
|
||||||
|
#[watch]
|
||||||
|
set_reveal_child: self.expanded,
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Box {
|
||||||
|
// history viewstack
|
||||||
|
adw::StatusPage {
|
||||||
|
set_visible: !self.have_events,
|
||||||
|
|
||||||
|
set_title: "No events",
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
append = &self.list_box_history.clone() {
|
||||||
|
set_visible: self.have_events,
|
||||||
|
|
||||||
|
add_css_class: relm4::css::BOXED_LIST,
|
||||||
|
|
||||||
|
set_selection_mode: gtk::SelectionMode::None,
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_model(
|
||||||
|
init: Self::Init,
|
||||||
|
index: &Self::Index,
|
||||||
|
sender: relm4::FactorySender<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let have_events = init.history.events.as_ref().is_some_and(|a| a.len() > 0);
|
||||||
|
|
||||||
|
let list_box_history = gtk::ListBox::new();
|
||||||
|
|
||||||
|
let _self = ShipmentView {
|
||||||
|
have_events,
|
||||||
|
model: init,
|
||||||
|
list_box_history,
|
||||||
|
expanded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for elem in _self.model.history.events.as_ref().unwrap() {
|
||||||
|
let label_datum = gtk::Label::builder()
|
||||||
|
.css_classes([relm4::css::NUMERIC])
|
||||||
|
.label(&elem.datum)
|
||||||
|
.halign(gtk::Align::Start)
|
||||||
|
.valign(gtk::Align::Start)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// TODO: is html, parse it
|
||||||
|
let label_status = gtk::Label::builder()
|
||||||
|
.wrap(true)
|
||||||
|
.halign(gtk::Align::Start)
|
||||||
|
.valign(gtk::Align::Start)
|
||||||
|
.build();
|
||||||
|
label_status.set_markup(&elem.status);
|
||||||
|
|
||||||
|
let boxie = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.margin_start(8)
|
||||||
|
.margin_end(8)
|
||||||
|
.spacing(8)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
boxie.append(&label_datum);
|
||||||
|
boxie.append(&label_status);
|
||||||
|
|
||||||
|
_self.list_box_history.append(&boxie);
|
||||||
|
}
|
||||||
|
|
||||||
|
_self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
|
||||||
|
match message {
|
||||||
|
ViewInput::ToggleExpand => {
|
||||||
|
self.expanded = !self.expanded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue