From 8b30c04e451b32f75f3ae3b6e314dbf5afbe6357 Mon Sep 17 00:00:00 2001 From: Dustin Rouillard Date: Sat, 5 Oct 2024 14:52:34 -0600 Subject: [PATCH] Add routes for boosted ride activity (#4) * Remove ls from dockerfile Fix issue with spotify history repeat inserting * spotify: Add recent listens route with limit param github: Add github route for pinned repos spotify: Stop from querying db every second, *clueless* * Add boosted api hooks Add boosted api endpoints Clean up json responses --- src/config.rs | 16 ++++++++-- src/main.rs | 4 +++ src/modules/spotify.rs | 3 -- src/services/boosted/factory.rs | 7 +++++ src/services/boosted/mod.rs | 3 ++ src/services/boosted/routes.rs | 39 +++++++++++++++++++++++ src/services/boosted/structs.rs | 35 +++++++++++++++++++++ src/services/hooks/boosted.rs | 55 +++++++++++++++++++++++++++++++++ src/services/hooks/factory.rs | 7 +++++ src/services/hooks/mod.rs | 3 ++ src/services/hooks/structs.rs | 49 +++++++++++++++++++++++++++++ src/services/mod.rs | 2 ++ src/services/spotify/routes.rs | 6 ++-- 13 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 src/services/boosted/factory.rs create mode 100644 src/services/boosted/mod.rs create mode 100644 src/services/boosted/routes.rs create mode 100644 src/services/boosted/structs.rs create mode 100644 src/services/hooks/boosted.rs create mode 100644 src/services/hooks/factory.rs create mode 100644 src/services/hooks/mod.rs create mode 100644 src/services/hooks/structs.rs diff --git a/src/config.rs b/src/config.rs index 5669d1e..6aa961c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,10 +5,10 @@ pub struct Config { #[envconfig(from = "ENV", default = "dev")] pub env: String, - #[envconfig(from = "LISTEN_HOST", default = "0.0.0.0")] + #[envconfig(from = "HOST", default = "0.0.0.0")] pub listen_host: String, - #[envconfig(from = "LISTEN_PORT", default = "8080")] + #[envconfig(from = "PORT", default = "8080")] pub listen_port: u16, #[envconfig(from = "METRICS_LISTEN_PORT", default = "8081")] @@ -91,4 +91,16 @@ pub struct Config { #[envconfig(from = "INFLUXDB_BUCKET", default = "api")] pub influxdb_bucket: String, + + #[envconfig(from = "BOOSTED_HOOK_SECRET", default = "")] + pub boosted_hook_token: String, + + #[envconfig( + from = "BOOSTED_API_ENDPOINT", + default = "https://boosted-rides.dstn.to" + )] + pub boosted_api_endpoint: String, + + #[envconfig(from = "BOOSTED_API_TOKEN", default = "")] + pub boosted_api_token: String, } diff --git a/src/main.rs b/src/main.rs index b249924..55fddde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,6 +84,8 @@ async fn main() -> Result<(), Box> { )); } }); + } else { + tracing::debug!("Spotify runner skipped due to being in DEV Mode") } let api_server = HttpServer::new(move || { @@ -113,6 +115,8 @@ async fn main() -> Result<(), Box> { .service(services::base::health) .service(services::analytics::factory::analytics_factory()) .service(services::weather::factory::weather_factory()) + .service(services::hooks::factory::hooks_factory()) + .service(services::boosted::factory::boosted_factory()) .service(services::uploads::factory::uploads_factory()) .service(services::spotify::factory::spotify_factory()) .service(services::github::factory::github_factory()) diff --git a/src/modules/spotify.rs b/src/modules/spotify.rs index 474d26d..dde9e7a 100644 --- a/src/modules/spotify.rs +++ b/src/modules/spotify.rs @@ -14,9 +14,6 @@ use crate::{ ServerState, }; -extern crate chrono; -extern crate serde_json; - pub(crate) async fn fetch_spotify_current(data: web::Data) { let valkey = &mut data.valkey.clone(); let rabbit = &mut data.rabbit.clone(); diff --git a/src/services/boosted/factory.rs b/src/services/boosted/factory.rs new file mode 100644 index 0000000..9eecff6 --- /dev/null +++ b/src/services/boosted/factory.rs @@ -0,0 +1,7 @@ +use actix_web::{web, Scope}; + +use crate::services; + +pub fn boosted_factory() -> Scope { + web::scope("/boosted").service(services::boosted::routes::ride_stats) +} diff --git a/src/services/boosted/mod.rs b/src/services/boosted/mod.rs new file mode 100644 index 0000000..df70ee7 --- /dev/null +++ b/src/services/boosted/mod.rs @@ -0,0 +1,3 @@ +pub mod factory; +pub mod routes; +pub mod structs; diff --git a/src/services/boosted/routes.rs b/src/services/boosted/routes.rs new file mode 100644 index 0000000..8d1d212 --- /dev/null +++ b/src/services/boosted/routes.rs @@ -0,0 +1,39 @@ +use actix_web::{get, http::Error, web, HttpResponse}; +use envconfig::Envconfig; +use redis::aio::ConnectionManager; +use serde_json::json; + +use crate::{ + config::Config, services::boosted::structs::BoostedStats, ServerState, +}; + +#[get("/stats")] +async fn ride_stats( + state: web::Data, +) -> Result { + let config = Config::init_from_env().unwrap(); + let valkey = &mut state.valkey.clone(); + + let in_ride = redis::cmd("GET") + .arg("boosted/in-ride") + .query_async::(&mut valkey.cm) + .await + .unwrap_or(String::from("false")) + == "true"; + + let client = reqwest::Client::new(); + let res = client + .get(format!("{}/v1/users/stats", config.boosted_api_endpoint)) + .header("Authorization", config.boosted_api_token) + .send() + .await + .unwrap(); + + let json = res.json::().await.unwrap(); + + Ok(HttpResponse::Ok().json(json!({"boosted": { + "riding": in_ride, + "latest_ride": json.latest_ride, + "stats": json.stats + }}))) +} diff --git a/src/services/boosted/structs.rs b/src/services/boosted/structs.rs new file mode 100644 index 0000000..7b5830e --- /dev/null +++ b/src/services/boosted/structs.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct BoostedStats { + pub latest_ride: RideStats, + pub stats: Stats, +} + +#[derive(Serialize, Deserialize)] +pub struct RideStats { + pub started_at: String, + pub ended_at: String, + pub duration: f64, + pub distance: f64, +} + +#[derive(Serialize, Deserialize)] +pub struct Stats { + pub boards: Boards, + pub rides: ValueEntry, + pub duration: ValueEntry, + pub distance: ValueEntry, +} + +#[derive(Serialize, Deserialize)] +pub struct Boards { + pub distance: f64, +} + +#[derive(Serialize, Deserialize)] +pub struct ValueEntry { + pub day: f64, + pub week: f64, + pub month: f64, +} diff --git a/src/services/hooks/boosted.rs b/src/services/hooks/boosted.rs new file mode 100644 index 0000000..8a514d3 --- /dev/null +++ b/src/services/hooks/boosted.rs @@ -0,0 +1,55 @@ +use actix_web::{http::Error, post, web, HttpRequest, HttpResponse}; +use envconfig::Envconfig as _; +use redis::aio::ConnectionManager; + +use crate::{ + config::Config, + services::hooks::structs::{BoostedHookPayload, BoostedHookType}, + ServerState, +}; + +#[post("/boosted")] +async fn execute( + req: HttpRequest, + state: web::Data, + payload: web::Json, +) -> Result { + let config = Config::init_from_env().unwrap(); + + let auth_header = req.headers().get("authorization"); + if auth_header.is_none() { + return Ok(HttpResponse::BadRequest().finish()); + } + + let auth_header = auth_header.unwrap().to_str().unwrap(); + + if auth_header != config.boosted_hook_token { + return Ok(HttpResponse::BadRequest().finish()); + } + + let valkey = &mut state.valkey.clone(); + + match payload.hook_type { + BoostedHookType::RideStarted => { + let _ = redis::cmd("SET") + .arg("boosted/in-ride") + .arg("true") + .query_async::(&mut valkey.cm) + .await; + } + BoostedHookType::RideEnded => { + let _ = redis::cmd("DEL") + .arg("boosted/in-ride") + .query_async::(&mut valkey.cm) + .await; + } + BoostedHookType::RideDiscarded => { + let _ = redis::cmd("DEL") + .arg("boosted/in-ride") + .query_async::(&mut valkey.cm) + .await; + } + } + + Ok(HttpResponse::NoContent().finish()) +} diff --git a/src/services/hooks/factory.rs b/src/services/hooks/factory.rs new file mode 100644 index 0000000..aa831c9 --- /dev/null +++ b/src/services/hooks/factory.rs @@ -0,0 +1,7 @@ +use actix_web::{web, Scope}; + +use crate::services; + +pub fn hooks_factory() -> Scope { + web::scope("/hooks").service(services::hooks::boosted::execute) +} diff --git a/src/services/hooks/mod.rs b/src/services/hooks/mod.rs new file mode 100644 index 0000000..7b6ef95 --- /dev/null +++ b/src/services/hooks/mod.rs @@ -0,0 +1,3 @@ +pub mod boosted; +pub mod factory; +pub mod structs; diff --git a/src/services/hooks/structs.rs b/src/services/hooks/structs.rs new file mode 100644 index 0000000..e2609d0 --- /dev/null +++ b/src/services/hooks/structs.rs @@ -0,0 +1,49 @@ +use chrono::{DateTime, FixedOffset}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub enum BoostedHookType { + #[serde(rename = "ride_started")] + RideStarted, + #[serde(rename = "ride_ended")] + RideEnded, + #[serde(rename = "ride_discarded")] + RideDiscarded, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RideSummary { + pub ride_id: i64, + pub distance: f64, + pub max_speed: f64, + pub avg_speed: f64, + pub elevation_gain: Option, + pub elevation_loss: Option, + pub ride_points: usize, + pub start_time: DateTime, + pub end_time: DateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RideStartedHookBody { + pub ride_id: i64, + pub started_at: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RideEndedHookBody { + pub ride_id: i64, + pub summary: RideSummary, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RideDiscardedHookBody { + pub ride_id: i64, + pub code: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BoostedHookPayload { + pub hook_type: BoostedHookType, + pub body: serde_json::Value, +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 7f13440..976e2a7 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,7 +1,9 @@ pub mod analytics; pub mod base; pub mod blog; +pub mod boosted; pub mod github; +pub mod hooks; pub mod spotify; pub mod uploads; pub mod weather; diff --git a/src/services/spotify/routes.rs b/src/services/spotify/routes.rs index 82c281c..53c3b8a 100644 --- a/src/services/spotify/routes.rs +++ b/src/services/spotify/routes.rs @@ -91,7 +91,7 @@ async fn authorize( "message": "Spotify is already setup." }); - return Ok(HttpResponse::BadRequest().body(json.to_string())); + return Ok(HttpResponse::BadRequest().json(json)); } let config = Config::init_from_env().unwrap(); @@ -101,7 +101,7 @@ async fn authorize( let url = format!("https://accounts.spotify.com/authorize?client_id={}&response_type=code&scope={}&redirect_uri={}", config.spotify_client_id, scope, redirect_uri); let json = json!({ "url": url }); - Ok(HttpResponse::Ok().body(json.to_string())) + Ok(HttpResponse::Ok().json(json)) } #[get("/setup")] @@ -123,7 +123,7 @@ async fn setup( "message": "Spotify is already setup." }); - return Ok(HttpResponse::BadRequest().body(json.to_string())); + return Ok(HttpResponse::BadRequest().json(json)); } let config = Config::init_from_env().unwrap();