feat: add logic for journey->train identifier resolving, add recording of journeys, build api bearer auth
This commit is contained in:
71
src/api/db_vendo_navigator.rs
Normal file
71
src/api/db_vendo_navigator.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use chrono::DateTime;
|
||||
use url::Url;
|
||||
use urlencoding::encode;
|
||||
use crate::error::train_order_api_error::ResolveTripNumberError;
|
||||
use crate::model::db_vendo_navigator_api::TrainOrdering;
|
||||
use crate::model::travelynx::TrainType;
|
||||
|
||||
pub async fn get_railcar_identifier_by_journey(
|
||||
train_type: TrainType,
|
||||
trip_number: usize,
|
||||
station_uic: usize,
|
||||
departure_time: usize
|
||||
) -> Result<u64, ResolveTripNumberError> {
|
||||
let train_ordering = query_train_order_api(train_type, trip_number, station_uic, departure_time).await?;
|
||||
println!("Received train ordering response {:?}", train_ordering);
|
||||
find_railcar_identifier(train_ordering)
|
||||
}
|
||||
|
||||
fn find_railcar_identifier(train_ordering: TrainOrdering) -> Result<u64, ResolveTripNumberError> {
|
||||
let trainset = train_ordering.train_sets.first()
|
||||
.ok_or(ResolveTripNumberError::Api("No items in field 'fahrzeuggruppe'".to_string()))?;
|
||||
let identifier_str = trainset.identifier.to_owned();
|
||||
let train_type = &trainset.journey.train_type;
|
||||
let identifier = crop_first_n_chars(identifier_str.as_str(), train_type.to_string().len())
|
||||
.to_string().parse::<u64>()?;
|
||||
Ok(identifier)
|
||||
}
|
||||
|
||||
async fn query_train_order_api(
|
||||
train_type: TrainType,
|
||||
trip_number: usize,
|
||||
station_uic: usize,
|
||||
departure_time: usize
|
||||
) -> reqwest::Result<TrainOrdering> {
|
||||
println!("Resolving trip {train_type} {trip_number} from {station_uic} at {departure_time}");
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.build()?;
|
||||
let api_url = build_api_url(train_type, trip_number, station_uic, departure_time);
|
||||
println!("Fetching {api_url}");
|
||||
client.get(api_url)
|
||||
.header("Accept", "application/x.db.vendo.mob.wagenreihung.v3+json")
|
||||
.header("X-Correlation-ID", "ABCDE")
|
||||
.send()
|
||||
.await?
|
||||
.json::<TrainOrdering>()
|
||||
.await
|
||||
}
|
||||
|
||||
fn build_api_url(
|
||||
train_type: TrainType,
|
||||
trip_number: usize,
|
||||
station_uic: usize,
|
||||
departure_time: usize
|
||||
) -> Url {
|
||||
let train_trip = format!("{}_{}", train_type.to_string(), trip_number);
|
||||
let departure = DateTime::from_timestamp(departure_time as i64, 0)
|
||||
.expect("invalid departure time");
|
||||
Url::parse(format!(
|
||||
"https://app.vendo.noncd.db.de/mob/zuglaeufe/{}/halte/by-abfahrt/{}_{}/wagenreihung",
|
||||
train_trip,
|
||||
station_uic,
|
||||
encode(departure.to_rfc3339().as_str())
|
||||
).as_str()).unwrap()
|
||||
}
|
||||
|
||||
fn crop_first_n_chars(s: &str, count: usize) -> &str {
|
||||
match s.char_indices().skip(count).next() {
|
||||
Some((pos, _)) => &s[pos..],
|
||||
None => ""
|
||||
}
|
||||
}
|
3
src/api/handlers/bingo_card.rs
Normal file
3
src/api/handlers/bingo_card.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub async fn get() -> impl axum::response::IntoResponse {
|
||||
todo!();
|
||||
}
|
2
src/api/handlers/mod.rs
Normal file
2
src/api/handlers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub(crate) mod bingo_card;
|
||||
pub(crate) mod webhook;
|
103
src/api/handlers/webhook.rs
Normal file
103
src/api/handlers/webhook.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
extract::State,
|
||||
};
|
||||
use axum_core::response::IntoResponse;
|
||||
use chrono::DateTime;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use sqlx::postgres::PgQueryResult;
|
||||
use crate::api::db_vendo_navigator::get_railcar_identifier_by_journey;
|
||||
use crate::error::train_order_api_error::{CheckInError, ResolveTripNumberError};
|
||||
use crate::model::app::AppState;
|
||||
use crate::model::database::User;
|
||||
use crate::model::travelynx::{CheckInReason, Train};
|
||||
use crate::util::axum::UserBearerTokenExtractor;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) enum CheckIn {
|
||||
Traewelling(crate::model::traewelling::CheckIn),
|
||||
Travelynx(crate::model::travelynx::CheckIn),
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub(crate) async fn receive_travelynx(
|
||||
State(app_state): State<AppState>,
|
||||
UserBearerTokenExtractor(user): UserBearerTokenExtractor,
|
||||
Json(body): Json<crate::model::travelynx::CheckIn>,
|
||||
) -> impl IntoResponse {
|
||||
receive_travelynx_checkin(body, user, app_state.db).await.into_response()
|
||||
}
|
||||
|
||||
async fn receive_travelynx_checkin(
|
||||
check_in: crate::model::travelynx::CheckIn,
|
||||
user: User,
|
||||
db: Arc<PgPool>
|
||||
) -> impl IntoResponse {
|
||||
match check_in.reason {
|
||||
CheckInReason::CHECKIN => {
|
||||
let railcar_identifier = get_railcar_identifier_by_journey(
|
||||
check_in.status.train.train_type.unwrap(),
|
||||
check_in.status.train.number.to_owned().unwrap().parse().unwrap(),
|
||||
check_in.status.from_station.uic,
|
||||
check_in.status.from_station.scheduled_time.unwrap()
|
||||
)
|
||||
.await
|
||||
.map_err(|e| CheckInError::from(e))?;
|
||||
let train = get_train_by_identifier(railcar_identifier as i32, db.clone())
|
||||
.await
|
||||
.map_err(|e| ResolveTripNumberError::from(e))
|
||||
.map_err(|e| CheckInError::from(e))?;
|
||||
println!("Train: {:?}", train);
|
||||
record_journey(user, train, check_in.status.from_station.scheduled_time.unwrap(), db)
|
||||
.await
|
||||
.expect("Failed to check in!");
|
||||
let message = format!(
|
||||
"Successfully checked into {} {} ({}), departing from station {} at {}",
|
||||
check_in.status.train.train_type.unwrap(),
|
||||
check_in.status.train.number.unwrap(),
|
||||
railcar_identifier,
|
||||
check_in.status.from_station.uic,
|
||||
check_in.status.from_station.scheduled_time.unwrap()
|
||||
);
|
||||
Ok::<_, CheckInError>((
|
||||
StatusCode::OK,
|
||||
message
|
||||
).into_response())
|
||||
},
|
||||
CheckInReason::UNDO => {
|
||||
Ok::<_, CheckInError>((
|
||||
StatusCode::OK,
|
||||
"Checkin undone"
|
||||
).into_response())
|
||||
},
|
||||
_ => Ok::<_, CheckInError>((
|
||||
StatusCode::OK,
|
||||
"Nothing to do!"
|
||||
).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_train_by_identifier(identifier: i32, db: Arc<PgPool>) -> sqlx::Result<crate::model::database::Train> {
|
||||
sqlx::query_as!(
|
||||
crate::model::database::Train,
|
||||
"SELECT * FROM triebzug WHERE tz_id = $1",
|
||||
identifier
|
||||
).fetch_one(db.as_ref()).await
|
||||
}
|
||||
|
||||
async fn record_journey(
|
||||
user: User,
|
||||
train: crate::model::database::Train,
|
||||
timestamp: usize,
|
||||
db: Arc<PgPool>
|
||||
) -> sqlx::Result<PgQueryResult> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO checkins VALUES (gen_random_uuid(), $1, $2, $3, NULL)",
|
||||
user.uuid,
|
||||
train.uuid,
|
||||
DateTime::from_timestamp(timestamp as i64, 0).unwrap().naive_utc()
|
||||
).execute(db.as_ref()).await
|
||||
}
|
2
src/api/mod.rs
Normal file
2
src/api/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub(crate) mod handlers;
|
||||
mod db_vendo_navigator;
|
Reference in New Issue
Block a user