Compare commits

...

9 commits

Author SHA1 Message Date
jane400
02b129fb84 feat: paket: add icons used in ready view
icon_names were used with ebde744290
2024-09-17 19:27:16 +02:00
jane400
e2ff1594f8 format: paket.paket 2024-09-17 19:26:23 +02:00
jane400
0dc60250e6 format: paket.login 2024-09-17 19:24:57 +02:00
jane400
6fbdc6193c feat: add extra warning if shipment api breaks 2024-09-17 19:23:12 +02:00
jane400
ebde744290 fix: paket: actually deactive views 2024-09-17 19:22:48 +02:00
jane400
ef63f18a77 feat: utils: add generic cookie adder 2024-09-17 19:19:36 +02:00
jane400
b222695c16 feat: more optional fields in stammdaten 2024-09-17 11:54:36 +02:00
jane400
5b205ce1e0 fix: add more paramters to tracking (fingers crossed) 2024-09-17 11:53:47 +02:00
jane400
ec5896df52 feat: libpaket utils APIChange detection 2024-09-17 11:52:46 +02:00
9 changed files with 190 additions and 140 deletions

View file

@ -1,3 +1,3 @@
app_id = "de.j4ne.Paket"
icons = ["plus", "minus"]
icons = ["plus", "minus", "package-x-generic", "mail"]

View file

@ -15,6 +15,14 @@ pub fn device_string() -> &'static str {
"OnePlus 6T Build/RQ3A.211001.001"
}
pub fn okhttp_user_agent() -> String {
format!(
"okhttp/4.11.0 Post & DHL/{} ({})",
app_version(),
linux_android_version()
)
}
pub fn web_user_agent() -> String {
format!("Mozilla/5.0 ({}; {}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/117.0.0.0 Mobile Safari/537.36",
linux_android_version(), device_string())

View file

@ -1,8 +1,8 @@
use reqwest::{header::HeaderMap, Request, RequestBuilder};
use crate::www::authorized_credentials;
use crate::constants::{app_version, linux_android_version};
use crate::common::APIResult;
use crate::constants::okhttp_user_agent;
use crate::www::authorized_credentials;
use crate::{login::DHLIdToken, LibraryResult};
pub struct StammdatenClient {
@ -15,18 +15,19 @@ impl StammdatenClient {
StammdatenClient {
client: reqwest::ClientBuilder::new()
.default_headers(headers())
.user_agent(user_agent())
.user_agent(okhttp_user_agent())
.build()
.unwrap(),
}
}
pub(crate) fn base_request(&self, request_builder: RequestBuilder, dhli: &DHLIdToken) -> Request {
pub(crate) fn base_request(
&self,
request_builder: RequestBuilder,
dhli: &DHLIdToken,
) -> Request {
request_builder
.basic_auth(
authorized_credentials().0,
Some(authorized_credentials().1),
)
.basic_auth(authorized_credentials().0, Some(authorized_credentials().1))
.headers(headers())
.header("cookie", format!("dhli={}", dhli.as_str()))
.build()
@ -68,16 +69,6 @@ impl StammdatenClient {
Err(err) => Err(err.into()),
}
}
}
fn user_agent() -> String {
format!(
"okhttp/4.11.0 Post & DHL/{} ({})",
app_version(),
linux_android_version()
)
}
fn headers() -> HeaderMap {
@ -144,9 +135,13 @@ pub enum CustomerDataService {
}
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CustomerDataFull {
#[serde(flatten)]
pub common: CustomerData,
pub requested_services: Option<Vec<CustomerDataService>>,
//pub customer_actions: Option,
pub address: CustomerAddress,
}

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::{login::DHLIdToken, LibraryResult};
use crate::{login::DHLIdToken, LibraryError, LibraryResult};
#[derive(Default)]
pub struct TrackingParams {
@ -32,15 +32,16 @@ impl crate::WebClient {
endpoint_data_search,
query(&query_parameters_data_search(&params, ids)),
header(api_key.0, api_key.1),
header("Cookie", cookie_value)
header("cookie", cookie_value),
header("x-requested-with", "de.dhl.paket"),
header("sec-ch-ua-platform", "\"Android\""),
header("sec-ch-ua", r#""Chromium";v="122", "Not(A:Brand";v="24", "Android WebView";v="122""#),
header("sec-ch-ua-mobile", "?1"),
header("content-type", "application/json"),
header("accept", "application/json")
)
} else {
request!(
self.web_client,
endpoint_data_search,
query(&query_parameters_data_search(&params, ids)),
header(api_key.0, api_key.1)
)
return Err(LibraryError::InvalidArgument("only supported with a logged-in session".to_string()));
};
let resp = parse_json_response!(res, Response);
@ -414,13 +415,17 @@ fn query_parameters_data_search(
params: &TrackingParams,
mut shippingnumbers: Vec<String>,
) -> Vec<(String, String)> {
let mut out = vec![("noRedirect".to_string(), "true".to_string())];
let mut out = vec![
("noRedirect".to_string(), "true".to_string()),
("cid".to_string(), "app".to_string()),
];
if let Some(lang) = params.language.as_ref() {
out.push(("language".to_string(), lang.clone()));
}
if shippingnumbers.len() > 0 {
out.push(("inputSearch".to_string(), "true".to_string()));
let mut shippingnumbers_string = shippingnumbers.pop().unwrap();
for number in shippingnumbers {
shippingnumbers_string = format!(",{}", number);

View file

@ -26,6 +26,13 @@ impl CookieHeaderValueBuilder {
self
}
pub fn add_key_value(mut self, key: String, value: String) -> Self {
self.list
.push((key, value));
self
}
pub fn build_string(self) -> String {
let name_value = self
.list
@ -179,15 +186,23 @@ macro_rules! request_json {
macro_rules! parse_json_response {
($res: expr, $type: ty) => {{
let res = $res.text().await.unwrap();
let status = $res.status();
let res: String = $res.text().await.unwrap();
// Catch HTML Response early
if status == 200 {
let res = res.clone();
let res = res.trim();
if res.starts_with("<!DOCTYPE html>") {
println!("got html, exptected json with type {}", stringify!($type));
return Err(crate::LibraryError::APIChange);
}
}
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,
@ -200,15 +215,23 @@ macro_rules! parse_json_response {
macro_rules! parse_json_response_from_apiresult {
($res: expr, $type: ty) => {{
let res = $res.text().await.unwrap();
let status = $res.status();
let res: String = $res.text().await.unwrap();
// Catch HTML Response early
if status == 200 {
let res = res.clone();
let res = res.trim();
if res.starts_with("<!DOCTYPE html>") {
println!("got html, exptected json with type {}", stringify!($type));
return Err(crate::LibraryError::APIChange);
}
}
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(),

View file

@ -6,9 +6,11 @@ license.workspace = true
version.workspace = true
[dependencies]
relm4 = { version = "0.9" , features = [ "libadwaita", "macros" ] }
relm4-components = { version = "0.9" }
relm4-macros = { version = "0.9" }
# using git version, for https://github.com/Relm4/Relm4/pull/677
relm4 = { version = "0.9", features = [ "libadwaita", "macros" ], git = "https://github.com/Relm4/Relm4.git" }
relm4-components = { version = "0.9", git = "https://github.com/Relm4/Relm4.git" }
relm4-macros = { version = "0.9", git = "https://github.com/Relm4/Relm4.git" }
relm4-icons = { version = "0.9", git = "https://github.com/Relm4/icons.git" }
tracker = "0.2"
adw = {package = "libadwaita", version = "0.7", features = [ "v1_5" ]}
webkit = { package = "webkit6", version = "0.4" }
@ -16,6 +18,5 @@ reqwest = "0.12"
libpaket = { path = "../libpaket" }
glycin = { version = "2.0.0-beta", features = ["gdk4"] }
oo7 = { version = "0.3" }
relm4-icons = { version = "0.9" }
futures = "0.3"
gtk = { package = "gtk4", version = "0.9", features = ["v4_16"]}

View file

@ -1,15 +1,8 @@
use std::sync::Arc;
use adw::{self, glib, prelude::*};
use gtk;
use paket::login::{new_login_shared_state, Login, LoginOutput};
use paket::ready::{Ready, ReadyOutput};
use relm4::{
RELM_THREADS,
adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex,
AsyncComponentSender, SharedState,
};
use gtk::prelude::*;
use adw::{glib, prelude::*};
use relm4::{main_adw_application, prelude::*, AsyncComponentSender, RELM_THREADS};
#[derive(Debug, PartialEq)]
enum AppState {
@ -241,7 +234,13 @@ impl AsyncComponent for App {
}
AppInput::FatalErrorOccoured(error) => {
widgets.fatal_status_page.set_title(&error.short);
widgets.fatal_status_page.set_description(Some(format!("{}\nThis error is fatal, the app can't continue.", &error.long).as_str()));
widgets.fatal_status_page.set_description(Some(
format!(
"{}\nThis error is fatal, the app can't continue.",
&error.long
)
.as_str(),
));
self.set_state(AppState::FatalError);
}
AppInput::SwitchToReady => {
@ -292,6 +291,8 @@ fn main() {
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(paket::constants::APP_ID);
app.run_async::<App>(());
}

View file

@ -6,13 +6,11 @@ use std::{
};
use adw::prelude::*;
use gtk::prelude::*;
use libpaket::{
login::{create_nonce, CodeVerfier, DHLIdToken, RefreshToken, TokenResponse},
LibraryError, LibraryResult, OpenIdClient,
};
use relm4::{
adw, gtk,
prelude::*,
tokio::{sync::Mutex, time::sleep},
AsyncComponentSender, SharedState,
@ -119,18 +117,16 @@ impl AsyncComponent for Login {
tracker: 0,
};
{
let result = oo7::Keyring::new().await;
match result {
Ok(keyring) => {
KEYRING.set(keyring).unwrap();
{
let keyring = KEYRING.get().unwrap();
if let Err(err) = keyring.unlock().await {
if let Err(err) = KEYRING.get().unwrap().unlock().await {
sender
.output(LoginOutput::KeyringError(err))
.expect("sender not worky");
} else {
let keyring = KEYRING.get().unwrap();
match keyring
.search_items(&HashMap::from(KEYRING_ATTRIBUTES))
.await
@ -141,9 +137,8 @@ impl AsyncComponent for Login {
let refresh_token = item.secret().await.unwrap();
let refresh_token =
std::str::from_utf8(refresh_token.as_slice()).unwrap();
model.refresh_token = Some(
RefreshToken::new(refresh_token.to_string()).unwrap(),
);
model.refresh_token =
Some(RefreshToken::new(refresh_token.to_string()).unwrap());
sender.input(LoginInput::NeedsRefresh);
} else {
sender.input(LoginInput::NeedsLogin);
@ -157,14 +152,12 @@ impl AsyncComponent for Login {
};
}
}
}
Err(err) => {
sender
.output(LoginOutput::KeyringError(err))
.expect("sender not worky");
}
};
}
let webcontext = WebContext::builder().build();
{
@ -204,7 +197,10 @@ impl AsyncComponent for Login {
match message {
LoginInput::NeedsRefresh => {
let refresh_token = self.refresh_token.as_ref().unwrap().clone();
sender.oneshot_command(async { use_refresh_token(refresh_token).await })
sender.oneshot_command(async {
let res = use_refresh_token(refresh_token).await;
res
});
}
LoginInput::ReceivedAuthCode(auth_code) => {
self.set_state(LoginState::Offline);
@ -254,12 +250,6 @@ impl AsyncComponent for Login {
}
}
#[derive(PartialEq)]
enum ResponseType {
Retry,
Okay,
}
impl Login {
fn construct_request_uri(&self) -> Option<URIRequest> {
if self.state != LoginState::InFlow {

View file

@ -17,9 +17,7 @@ use relm4::{
prelude::*,
};
use crate::advices::{AdvicesViewInput, AdvicesView};
use crate::advices::{AdvicesView, AdvicesViewInput};
#[tracker::track]
pub struct Ready {
@ -27,6 +25,7 @@ pub struct Ready {
login: crate::LoginSharedState,
activate: bool,
have_service_advices: bool,
have_service_tracking: bool,
#[do_not_track]
advices_component: AsyncController<AdvicesView>,
@ -51,12 +50,18 @@ pub enum ReadyCmds {
GotTracking(LibraryResult<Vec<Shipment>>),
}
#[derive(Debug)]
pub enum Services {
Advices,
SendungVerfolgung,
}
#[derive(Debug)]
pub enum ReadyInput {
Activate,
Deactivate,
HaveAdvicesService,
HavePaketankuendigungService,
HaveService(Services),
ServiceBorked(Services),
SearchTracking(String),
}
@ -70,18 +75,23 @@ impl Component for Ready {
view! {
#[root]
adw::ViewStack {
add = model.advices_component.widget() -> &gtk::ScrolledWindow {
/*#[track(model.changed_have_service_advices())]
set_visible: model.have_service_advices,*/
add = &model.advices_component.widget().clone() -> gtk::ScrolledWindow {
#[track(model.changed_have_service_advices())]
set_visible: model.have_service_advices,
} -> /*page_advices: adw::ViewStackPage*/ {
} -> page_advices: adw::ViewStackPage {
set_title: Some("Mail notification"),
set_name: Some("page_advices"),
/*#[track(model.changed_have_service_advices())]
set_visible: model.have_service_advices,*/
set_icon_name: Some(relm4_icons::icon_names::MAIL),
#[track(model.changed_have_service_advices())]
set_visible: model.have_service_advices,
},
add = &adw::Bin {
#[track(model.changed_have_service_tracking())]
set_visible: model.have_service_tracking,
#[wrap(Some)]
set_child = &gtk::ScrolledWindow {
#[wrap(Some)]
@ -110,9 +120,13 @@ impl Component for Ready {
},
}
}
} -> {
} -> page_tracking: adw::ViewStackPage {
set_title: Some("Shipment tracking"),
set_name: Some("page_tracking"),
set_icon_name: Some(relm4_icons::icon_names::PACKAGE_X_GENERIC),
#[track(model.changed_have_service_tracking())]
set_visible: model.have_service_tracking,
},
}
}
@ -127,6 +141,7 @@ impl Component for Ready {
let model = Ready {
have_service_advices: false,
have_service_tracking: true,
login: init.clone(),
activate: false,
tracking_factory,
@ -210,7 +225,13 @@ impl Component for Ready {
ReadyInput::Deactivate => {
self.set_activate(false);
}
ReadyInput::HavePaketankuendigungService => {
ReadyInput::HaveService(service) => match service {
Services::Advices => {
self.set_have_service_advices(true);
self.advices_component.emit(AdvicesViewInput::Fetch);
}
Services::SendungVerfolgung => {
self.set_have_service_tracking(true);
let token = self.login.clone();
sender.oneshot_command(async move {
// fetching advices
@ -229,10 +250,11 @@ impl Component for Ready {
)
});
}
ReadyInput::HaveAdvicesService => {
self.have_service_advices = true;
self.advices_component.emit(AdvicesViewInput::Fetch);
}
},
ReadyInput::ServiceBorked(service) => match service {
Services::Advices => self.set_have_service_advices(false),
Services::SendungVerfolgung => self.set_have_service_tracking(false),
},
}
}
@ -251,13 +273,13 @@ impl Component for Ready {
match service {
libpaket::stammdaten::CustomerDataService::Packstation => (),
libpaket::stammdaten::CustomerDataService::Paketankuendigung => {
sender.input(ReadyInput::HavePaketankuendigungService);
sender.input(ReadyInput::HaveService(Services::SendungVerfolgung))
}
libpaket::stammdaten::CustomerDataService::PostfilialeDirekt => (),
libpaket::stammdaten::CustomerDataService::Digiben => (),
libpaket::stammdaten::CustomerDataService::GeraetAktiviert => (),
libpaket::stammdaten::CustomerDataService::Briefankuendigung => {
sender.input(ReadyInput::HaveAdvicesService);
sender.input(ReadyInput::HaveService(Services::Advices))
}
}
}
@ -320,10 +342,15 @@ impl Component for Ready {
}
}
Err(err) => {
if err == LibraryError::APIChange {
println!("Upstream API for parcel tracking broke");
sender.input(ReadyInput::ServiceBorked(Services::SendungVerfolgung));
sender.output(ReadyOutput::Notification("Shipment Tracking API has changed. Deactivating that service.".to_string())).unwrap();
} else {
sender.output(ReadyOutput::Error(err)).unwrap();
}
}
},
}
}
}