From 3521b125cf225aec4c4107529f15563be9a3e653 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 30 Dec 2020 21:53:40 -0500 Subject: [PATCH] orangeconnex package tracking Signed-off-by: Christine Dodrill --- .../down.sql | 3 + .../up.sql | 20 ++++ backend/src/api/mod.rs | 1 + backend/src/api/package_tracking/mod.rs | 1 + .../src/api/package_tracking/orangeconnex.rs | 55 +++++++++ backend/src/bin/import_blogposts.rs | 12 +- backend/src/bin/package_track.rs | 51 ++++++++ backend/src/main.rs | 3 + backend/src/models.rs | 36 ++++++ backend/src/schema.rs | 23 ++++ backend/src/web/canada_weather/mod.rs | 2 +- backend/src/web/mod.rs | 1 + backend/src/web/orange_connex.rs | 112 ++++++++++++++++++ 13 files changed, 309 insertions(+), 11 deletions(-) create mode 100644 backend/migrations/2020-12-30-214016_orangeconnex_package_tracking/down.sql create mode 100644 backend/migrations/2020-12-30-214016_orangeconnex_package_tracking/up.sql create mode 100644 backend/src/api/package_tracking/mod.rs create mode 100644 backend/src/api/package_tracking/orangeconnex.rs create mode 100644 backend/src/bin/package_track.rs create mode 100644 backend/src/web/orange_connex.rs diff --git a/backend/migrations/2020-12-30-214016_orangeconnex_package_tracking/down.sql b/backend/migrations/2020-12-30-214016_orangeconnex_package_tracking/down.sql new file mode 100644 index 0000000..cb93f4e --- /dev/null +++ b/backend/migrations/2020-12-30-214016_orangeconnex_package_tracking/down.sql @@ -0,0 +1,3 @@ +DROP TABLE orangeconnex_packages; +DROP INDEX orangeconnex_traces_time; +DROP TABLE orangeconnex_traces; diff --git a/backend/migrations/2020-12-30-214016_orangeconnex_package_tracking/up.sql b/backend/migrations/2020-12-30-214016_orangeconnex_package_tracking/up.sql new file mode 100644 index 0000000..30e678d --- /dev/null +++ b/backend/migrations/2020-12-30-214016_orangeconnex_package_tracking/up.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS orangeconnex_packages + ( tracking_number TEXT UNIQUE NOT NULL PRIMARY KEY + , recieved BOOLEAN NOT NULL DEFAULT false + ); + +CREATE TABLE IF NOT EXISTS orangeconnex_traces + ( id TEXT UNIQUE NOT NULL PRIMARY KEY + , tracking_number TEXT NOT NULL + , description TEXT NOT NULL + , city TEXT + , country TEXT NOT NULL + , time_recorded TEXT NOT NULL + , time_zone TEXT NOT NULL + , ts INTEGER NOT NULL + , FOREIGN KEY (tracking_number) + REFERENCES orangeconnex_packages(tracking_number) + ); + +CREATE UNIQUE INDEX orangeconnex_traces_time + ON orangeconnex_traces(time_recorded, time_zone, ts); diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs index 3688f09..7c20572 100644 --- a/backend/src/api/mod.rs +++ b/backend/src/api/mod.rs @@ -12,6 +12,7 @@ use rocket::{ use rocket_contrib::json::Json; use std::io::Read; +pub mod package_tracking; pub mod posse; pub mod switch; pub mod webmention; diff --git a/backend/src/api/package_tracking/mod.rs b/backend/src/api/package_tracking/mod.rs new file mode 100644 index 0000000..3cff8bb --- /dev/null +++ b/backend/src/api/package_tracking/mod.rs @@ -0,0 +1 @@ +pub mod orangeconnex; diff --git a/backend/src/api/package_tracking/orangeconnex.rs b/backend/src/api/package_tracking/orangeconnex.rs new file mode 100644 index 0000000..a0f586a --- /dev/null +++ b/backend/src/api/package_tracking/orangeconnex.rs @@ -0,0 +1,55 @@ +use crate::{ + api::{Error, Result, StringBody}, + models, paseto, schema, MainDatabase, +}; +use diesel::prelude::*; +use rocket_contrib::json::Json; + +#[post("/packages/orangeconnex/track", data = "")] +#[instrument(skip(conn), err)] +pub fn track(conn: MainDatabase, tn: StringBody, tok: paseto::Token) -> Result { + use schema::orangeconnex_packages::table; + let tn = tn.unwrap(); + + diesel::insert_into(table) + .values(&models::OrangeConnexPackage { + tracking_number: tn.clone(), + recieved: false, + }) + .execute(&*conn) + .map_err(Error::Database)?; + + Ok(format!("now tracking package {}", tn)) +} + +#[get("/packages/orangeconnex/status?")] +#[instrument(skip(conn), err)] +pub fn status( + conn: MainDatabase, + tn: String, + tok: paseto::Token, +) -> Result>> { + use schema::orangeconnex_traces; + + Ok(Json( + orangeconnex_traces::table + .load::(&*conn) + .map_err(Error::Database)?, + )) +} + +#[post("/packages/orangeconnex/delivered?")] +#[instrument(skip(conn), err)] +pub fn recieved(conn: MainDatabase, tn: String, tok: paseto::Token) -> Result { + use schema::orangeconnex_packages::dsl::*; + + diesel::update(orangeconnex_packages.find(tn.clone())) + .set(&models::OrangeConnexPackage { + tracking_number: tn.clone(), + recieved: true, + }) + .execute(&*conn) + .map_err(Error::Database)?; + + Ok(format!("{} is marked as recieved", tn)) +} diff --git a/backend/src/bin/import_blogposts.rs b/backend/src/bin/import_blogposts.rs index 5949ec2..af36e1b 100644 --- a/backend/src/bin/import_blogposts.rs +++ b/backend/src/bin/import_blogposts.rs @@ -2,8 +2,7 @@ extern crate tracing; use color_eyre::eyre::Result; -use diesel::{prelude::*, SqliteConnection}; -use std::env; +use diesel::prelude::*; use mi::{api::posse::*, *}; @@ -16,14 +15,7 @@ fn main() -> Result<()> { let conn = establish_connection(); let feed = read_jsonfeed(BLOG_FEED_URL.to_string())?; - let posts: Vec = feed - .items - .into_iter() - .map(|item| { - let post: models::Blogpost = item.into(); - post - }) - .collect(); + let posts: Vec = feed.items.into_iter().map(Into::into).collect(); diesel::insert_into(schema::blogposts::table) .values(&posts) .execute(&conn)?; diff --git a/backend/src/bin/package_track.rs b/backend/src/bin/package_track.rs new file mode 100644 index 0000000..66a3743 --- /dev/null +++ b/backend/src/bin/package_track.rs @@ -0,0 +1,51 @@ +#[macro_use] +extern crate tracing; + +use color_eyre::eyre::Result; +use diesel::prelude::*; + +use mi::{api::Error, establish_connection, models, schema, web::orange_connex}; + +fn main() -> Result<()> { + color_eyre::install()?; + tracing_subscriber::fmt::init(); + + info!( + "{} package tracking updater starting up", + mi::APPLICATION_NAME + ); + let conn = establish_connection(); + + // OrangeConnex package tracking + { + use schema::{ + orangeconnex_packages::dsl::*, orangeconnex_traces::dsl::orangeconnex_traces, + }; + let packages = orangeconnex_packages + .filter(recieved.eq(false)) + .load::(&conn) + .map_err(Error::Database)?; + + for package in packages.into_iter() { + let info = orange_connex::get(package.tracking_number.clone())? + .get_waybill() + .unwrap(); + let tn = package.tracking_number; + + let traces: Vec = info + .traces + .into_iter() + .map(|tr| models::OrangeConnexTrace::from_trace(tr, tn.clone())) + .collect(); + + let _ = diesel::insert_into(orangeconnex_traces) + .values(&traces) + //.on_conflict((time_recorded, time_zone, ts)) + //.do_nothing() + .execute(&conn) + .map_err(Error::Database); + } + } + + Ok(()) +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 9a65a7c..5b4744f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -70,6 +70,9 @@ fn main() -> Result<()> { .mount( "/api", routes![ + api::package_tracking::orangeconnex::track, + api::package_tracking::orangeconnex::status, + api::package_tracking::orangeconnex::recieved, api::posse::notify, api::posse::refresh_blog, api::switch::current_front, diff --git a/backend/src/models.rs b/backend/src/models.rs index 6b072ae..a273288 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -72,3 +72,39 @@ pub struct Blogpost { pub url: String, pub title: String, } + +#[derive(Queryable, Associations, Insertable, AsChangeset)] +#[table_name = "orangeconnex_packages"] +pub struct OrangeConnexPackage { + pub tracking_number: String, + pub recieved: bool, +} + +#[derive(Queryable, Associations, Insertable, Serialize)] +#[table_name = "orangeconnex_traces"] +pub struct OrangeConnexTrace { + pub id: String, + pub tracking_number: String, + pub description: String, + pub city: Option, + pub country: String, + pub time_recorded: String, + pub time_zone: String, + pub ts: i32, +} + +impl OrangeConnexTrace { + pub fn from_trace(t: crate::web::orange_connex::Trace, tracking_number: String) -> Self { + use rusty_ulid::generate_ulid_string; + Self { + id: generate_ulid_string(), + tracking_number: tracking_number, + description: t.event_desc, + city: t.opr_city, + country: t.opr_country, + time_recorded: t.opr_time, + time_zone: t.opr_time_zone, + ts: t.opr_timestamp, + } + } +} diff --git a/backend/src/schema.rs b/backend/src/schema.rs index ddf019a..12a35c3 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -13,6 +13,26 @@ table! { } } +table! { + orangeconnex_packages (tracking_number) { + tracking_number -> Text, + recieved -> Bool, + } +} + +table! { + orangeconnex_traces (id) { + id -> Text, + tracking_number -> Text, + description -> Text, + city -> Nullable, + country -> Text, + time_recorded -> Text, + time_zone -> Text, + ts -> Integer, + } +} + table! { switches (id) { id -> Text, @@ -39,11 +59,14 @@ table! { } } +joinable!(orangeconnex_traces -> orangeconnex_packages (tracking_number)); joinable!(switches -> members (member_id)); allow_tables_to_appear_in_same_query!( blogposts, members, + orangeconnex_packages, + orangeconnex_traces, switches, weather, webmentions, diff --git a/backend/src/web/canada_weather/mod.rs b/backend/src/web/canada_weather/mod.rs index 22022d8..2aa0fbd 100644 --- a/backend/src/web/canada_weather/mod.rs +++ b/backend/src/web/canada_weather/mod.rs @@ -120,7 +120,7 @@ impl From for Forecast { } else { None }, - low: if f.temperatures.temperature.class.as_ref().unwrap() == "low" { + low: if f.temperatures.temperature.class.unwrap() == "low" { f.temperatures.temperature.value } else { None diff --git a/backend/src/web/mod.rs b/backend/src/web/mod.rs index c46bacf..6a8bcc8 100644 --- a/backend/src/web/mod.rs +++ b/backend/src/web/mod.rs @@ -2,6 +2,7 @@ pub mod bridgy; pub mod canada_weather; pub mod discord_webhook; pub mod mastodon; +pub mod orange_connex; pub mod pluralkit; pub mod switchcounter; pub mod twitter; diff --git a/backend/src/web/orange_connex.rs b/backend/src/web/orange_connex.rs new file mode 100644 index 0000000..6c0b693 --- /dev/null +++ b/backend/src/web/orange_connex.rs @@ -0,0 +1,112 @@ +/** +This module will fetch package tracking information from the OrangeConnex +API. I am not sure that this is completely kosher. Users of this function must +aggressively cache the results. +*/ +use super::{Error, Result}; +use serde::{Deserialize, Serialize}; + +/// An annoying wrapper type. +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Info { + pub has_business_exception: bool, + pub success: bool, + pub result: Wrapper, +} + +impl Info { + pub fn get_waybill(self) -> Option { + if self.success == true && self.result.waybills.len() != 0 { + Some(self.result.waybills[0].clone()) + } else { + None + } + } +} + +/// Another annoying wrapper type. +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Wrapper { + pub not_exists_tracking_numbers: Vec, + pub waybills: Vec, +} + +/// The information about the most current status of a package, as well as its +/// source and destination. +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Waybill { + pub consignee_city_code: String, + pub consignee_city_name: String, + pub consignee_country_code: String, + pub consignee_country_name: String, + pub consignee_zip_code: String, + pub consignment_city_code: String, + pub consignment_city_name: String, + pub consignment_country_code: String, + pub consignment_country_name: String, + pub consignment_zip_code: String, + pub is_event_code: String, + pub last_status: String, + pub last_time: String, + pub last_time_zone: String, + pub last_timestamp: i64, + pub last_zip_code: String, + pub traces: Vec, +} + +/// Each step in the package's journey to being delivered. +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Trace { + pub event_desc: String, + pub opr_city: Option, + pub opr_country: String, + pub opr_time: String, + pub opr_time_zone: String, + pub opr_timestamp: i32, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +struct PostBody { + tracking_numbers: Vec, +} + +/// Fetch tracking information from OrangeConnex. +pub fn get(tracking_number: String) -> Result { + let resp = ureq::post( + "https://azure-cn.orangeconnex.com/oc/capricorn-website/website/v1/tracking/traces", + ) + .set( + "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0", + ) + .set("Accept", "*/*") + .set("Accept-Language", "en-US") + .set("Content-Type", "application/json;charset=utf-8") + .set("Origin", "https://www.orangeconnex.com") + .set("Connection", "keep-alive") + .set( + "Referer", + &format!( + "https://www.orangeconnex.com/tracking?language=en&trackingnumber={}", + tracking_number + ), + ) + .set("Cache-Control", "max-age=0") + .send_json(serde_json::to_value(PostBody { + tracking_numbers: vec![tracking_number], + })?); + + if resp.ok() { + Ok(resp.into_json_deserialize()?) + } else { + Err(match resp.synthetic_error() { + Some(why) => Error::UReq(why.to_string()), + None => Error::HttpStatus(resp.status()), + }) + } +}