Compare commits

..

25 commits

Author SHA1 Message Date
0b2ae19085 chore: git: ignore result symlink created by nix 2024-08-28 15:47:16 +02:00
1c63afc4dc nix: add support for building using nix
This patch adds a default nix which invokes a `callPackage`
to build sources from the current working directory.

To invoke the build run `nix-build`.
2024-08-28 15:47:07 +02:00
f3196d6c9f chore: add Cargo.lock file 2024-08-28 15:46:44 +02:00
jane400
8791b08c4f feat: ble data parsing: version 2024-08-28 15:21:29 +02:00
jane400
edc2da5d9e chore: mark packstation api as unstable 2024-08-28 15:17:16 +02:00
jane400
5cf693b7bb chore: move primitive{reader,build} to utils 2024-08-28 15:14:42 +02:00
jane400
890aee8f72 chore: remove unused imports 2024-08-28 15:11:55 +02:00
jane400
ae64f73176 feat: more details button in SendungsVerfolgung 2024-08-28 15:09:18 +02:00
jane400
f3732051af fix: ui tweaks for advices 2024-08-28 15:07:32 +02:00
jane400
cd2bc321cd feat: paket: sendungsverfolgung bringup 2024-08-22 13:20:00 +02:00
jane400
0e1167adb2 feat: visual improvments for advices 2024-08-22 13:18:01 +02:00
jane400
609fc65816 fix: tracking: parse rate limited response
Technically NetworkFetch isn't intended for this. /shrug for now
2024-08-22 12:42:38 +02:00
jane400
290faff5c2 chore: remove Cargo.lock, we're unstable 2024-08-21 18:23:55 +02:00
jane400
b36f7843f0 feat: add first attempt for Sendungsverfolgung api 2024-08-21 18:22:41 +02:00
jane400
912f024163 wip: verbose logging with serde_ignored
this is for using the crate `tracing` in the future
2024-08-21 18:19:03 +02:00
jane400
c07a164903 chore: briefankuendigung: adjust to dhl changes 2024-08-21 18:17:56 +02:00
jane400
20dd4fc807 utils: add request and parse_response macros 2024-08-20 19:39:00 +02:00
jane400
2586d490bf format: libpaket.utils 2024-08-18 18:14:21 +02:00
jane400
8573be6e23 format: libpaket.utils 2024-08-18 18:12:26 +02:00
jane400
06c9342ea7 format: libpaket.locker.api 2024-08-18 18:10:20 +02:00
jane400
cda0f946da feat: add device_id to CustomerKeySeed 2024-08-18 18:09:27 +02:00
jane400
cc52b99144 format: use serde rename_all 2024-08-18 18:07:20 +02:00
jane400
b8f4e199f6 feat: document deprecation policy 2024-08-18 18:05:21 +02:00
jane400
a57be17dab format: paket login 2024-08-18 18:04:06 +02:00
jane400
daa2aedf0c feat: add deprecated LibraryError 2024-08-18 18:03:15 +02:00
23 changed files with 1652 additions and 554 deletions

560
Cargo.lock generated

File diff suppressed because it is too large Load diff

3
icons.toml Normal file
View file

@ -0,0 +1,3 @@
app_id = "de.j4ne.Paket"
icons = ["plus", "minus"]

View file

@ -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"
] ]

View file

@ -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 dont want to do that. In the examples error-handling is ignored for simplicity. You dont want to do that.

View file

@ -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"
} }

View file

@ -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() {

View file

@ -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())
} }
} }

View file

@ -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();

View file

@ -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...

View file

@ -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());

View file

@ -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")]

View file

@ -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
} }
} }

View 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
}
}

View file

@ -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
View 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(&params, ids)),
header(api_key.0, api_key.1),
header("Cookie", cookie_value)
)
} else {
request!(
self.web_client,
endpoint_data_search,
query(&query_parameters_data_search(&params, ids)),
header(api_key.0, api_key.1)
)
};
let resp = parse_json_response!(res, Response);
resp.into()
}
pub async fn tracking_shipment(
&self,
params: TrackingParams,
body: ShipmentRequest,
) -> LibraryResult<Vec<Shipment>> {
let api_key = crate::www::api_key_header();
let res = request_json!(
self.web_client,
endpoint_data_shipment,
body,
query(&query_parameters_data_shipment(&params)),
header(api_key.0, api_key.1)
);
let resp = parse_json_response!(res, Response);
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
}

View file

@ -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
}};
}

View file

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

View file

@ -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" }

View file

@ -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 = &gtk::Spinner { add_overlay = &gtk::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 = &gtk::Label { add_overlay = &gtk::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!(),
}; };
} }
}
}

View file

@ -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))
}

View file

@ -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>(());
} }

View file

@ -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 = &gtk::Box { add = &adw::Clamp {
set_orientation: gtk::Orientation::Horizontal, #[wrap(Some)]
set_child = &gtk::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 = &gtk::ScrolledWindow {
#[wrap(Some)]
set_child = &gtk::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
View 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 = &gtk::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;
}
}
}
}