orangeconnex package tracking

Signed-off-by: Christine Dodrill <me@christine.website>
This commit is contained in:
Cadey Ratio 2020-12-30 21:53:40 -05:00
parent 02ef9809c7
commit 3521b125cf
13 changed files with 309 additions and 11 deletions

View File

@ -0,0 +1,3 @@
DROP TABLE orangeconnex_packages;
DROP INDEX orangeconnex_traces_time;
DROP TABLE orangeconnex_traces;

View File

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

View File

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

View File

@ -0,0 +1 @@
pub mod orangeconnex;

View File

@ -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 = "<tn>")]
#[instrument(skip(conn), err)]
pub fn track(conn: MainDatabase, tn: StringBody, tok: paseto::Token) -> Result<String> {
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?<tn>")]
#[instrument(skip(conn), err)]
pub fn status(
conn: MainDatabase,
tn: String,
tok: paseto::Token,
) -> Result<Json<Vec<models::OrangeConnexTrace>>> {
use schema::orangeconnex_traces;
Ok(Json(
orangeconnex_traces::table
.load::<models::OrangeConnexTrace>(&*conn)
.map_err(Error::Database)?,
))
}
#[post("/packages/orangeconnex/delivered?<tn>")]
#[instrument(skip(conn), err)]
pub fn recieved(conn: MainDatabase, tn: String, tok: paseto::Token) -> Result<String> {
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))
}

View File

@ -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<models::Blogpost> = feed
.items
.into_iter()
.map(|item| {
let post: models::Blogpost = item.into();
post
})
.collect();
let posts: Vec<models::Blogpost> = feed.items.into_iter().map(Into::into).collect();
diesel::insert_into(schema::blogposts::table)
.values(&posts)
.execute(&conn)?;

View File

@ -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::<models::OrangeConnexPackage>(&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<models::OrangeConnexTrace> = 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(())
}

View File

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

View File

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

View File

@ -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<Text>,
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,

View File

@ -120,7 +120,7 @@ impl From<types::Forecast> 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

View File

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

View File

@ -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<Waybill> {
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<String>,
pub waybills: Vec<Waybill>,
}
/// 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<Trace>,
}
/// 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<String>,
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<String>,
}
/// Fetch tracking information from OrangeConnex.
pub fn get(tracking_number: String) -> Result<Info> {
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()),
})
}
}