diff --git a/.env.development.sample b/.env.development.sample index 5d2515c..6f309a6 100644 --- a/.env.development.sample +++ b/.env.development.sample @@ -1,2 +1,3 @@ WANIKANI_API_TOKEN="" -BUNPRO_API_TOKEN="" \ No newline at end of file +BUNPRO_API_TOKEN="" +SATORI_COOKIE="" \ No newline at end of file diff --git a/backend/src/api.rs b/backend/src/api.rs index 06a5275..a0a77ac 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -1,6 +1,7 @@ use axum::{http::StatusCode, Json}; pub mod bunpro; +pub mod satori; pub mod wanikani; #[derive(serde::Serialize)] diff --git a/backend/src/api/satori.rs b/backend/src/api/satori.rs new file mode 100644 index 0000000..2a6a16b --- /dev/null +++ b/backend/src/api/satori.rs @@ -0,0 +1,4 @@ +pub mod data; +pub mod request; + +pub use request::satori_handler; diff --git a/backend/src/api/satori/data.rs b/backend/src/api/satori/data.rs new file mode 100644 index 0000000..4be6c6a --- /dev/null +++ b/backend/src/api/satori/data.rs @@ -0,0 +1,37 @@ +use chrono::{DateTime, Utc}; + +#[derive(serde::Serialize)] +pub struct SatoriData { + data_updated_at: DateTime, + active_review_count: u32, + new_card_count: u32, +} + +impl SatoriData { + pub fn new( + current_cards: SatoriCurrentCardsResponse, + new_cards: SatoriNewCardsResponse, + ) -> Self { + Self { + data_updated_at: Utc::now(), + active_review_count: current_cards.result, + new_card_count: new_cards.result, + } + } +} + +#[derive(serde::Deserialize)] +pub struct SatoriCurrentCardsResponse { + result: u32, + success: bool, + message: Option, + exception: Option, +} + +#[derive(serde::Deserialize)] +pub struct SatoriNewCardsResponse { + result: u32, + success: bool, + message: Option, + exception: Option, +} diff --git a/backend/src/api/satori/fixtures/current_cards_with_no_reviews.json b/backend/src/api/satori/fixtures/current_cards_with_no_reviews.json new file mode 100644 index 0000000..a994214 --- /dev/null +++ b/backend/src/api/satori/fixtures/current_cards_with_no_reviews.json @@ -0,0 +1,6 @@ +{ + "result": 0, + "success": true, + "message": null, + "exception": null +} \ No newline at end of file diff --git a/backend/src/api/satori/fixtures/current_cards_with_pending_reviews.json b/backend/src/api/satori/fixtures/current_cards_with_pending_reviews.json new file mode 100644 index 0000000..9a865d5 --- /dev/null +++ b/backend/src/api/satori/fixtures/current_cards_with_pending_reviews.json @@ -0,0 +1,6 @@ +{ + "result": 6, + "success": true, + "message": null, + "exception": null +} \ No newline at end of file diff --git a/backend/src/api/satori/fixtures/new_cards_with_no_cards.json b/backend/src/api/satori/fixtures/new_cards_with_no_cards.json new file mode 100644 index 0000000..a994214 --- /dev/null +++ b/backend/src/api/satori/fixtures/new_cards_with_no_cards.json @@ -0,0 +1,6 @@ +{ + "result": 0, + "success": true, + "message": null, + "exception": null +} \ No newline at end of file diff --git a/backend/src/api/satori/fixtures/new_cards_with_pending_cards.json b/backend/src/api/satori/fixtures/new_cards_with_pending_cards.json new file mode 100644 index 0000000..1cdb83d --- /dev/null +++ b/backend/src/api/satori/fixtures/new_cards_with_pending_cards.json @@ -0,0 +1,6 @@ +{ + "result": 20, + "success": true, + "message": null, + "exception": null +} \ No newline at end of file diff --git a/backend/src/api/satori/request.rs b/backend/src/api/satori/request.rs new file mode 100644 index 0000000..b90ce54 --- /dev/null +++ b/backend/src/api/satori/request.rs @@ -0,0 +1,105 @@ +use std::env; + +use axum::Json; +use reqwest::{Client, StatusCode}; +use tokio::try_join; + +use crate::api::{internal_error, ErrorResponse}; + +use super::data::{SatoriCurrentCardsResponse, SatoriData, SatoriNewCardsResponse}; + +pub async fn satori_handler() -> Result, (StatusCode, Json)> { + let current_cards = get_current_cards(); + let new_cards = get_new_cards(); + + let (current_cards, new_cards) = try_join!(current_cards, new_cards).map_err(internal_error)?; + + let satori_data = SatoriData::new(current_cards, new_cards); + + Ok(Json(satori_data)) +} + +async fn get_current_cards() -> anyhow::Result { + let client = satori_client()?; + + client + .get("https://www.satorireader.com/api/studylist/due/count") + .send() + .await? + .text() + .await + .map(|body| serialize_current_cards_response(&body))? +} + +fn serialize_current_cards_response(body: &str) -> anyhow::Result { + let json_data: SatoriCurrentCardsResponse = serde_json::from_str(body)?; + + Ok(json_data) +} + +async fn get_new_cards() -> anyhow::Result { + let client = satori_client()?; + + client + .get("https://www.satorireader.com/api/studylist/pending-auto-importable/count") + .send() + .await? + .text() + .await + .map(|body| serialize_new_cards_response(&body))? +} + +fn serialize_new_cards_response(body: &str) -> anyhow::Result { + let json_data: SatoriNewCardsResponse = serde_json::from_str(body)?; + + Ok(json_data) +} + +fn satori_client() -> anyhow::Result { + let satori_cookie = env::var("SATORI_COOKIE")?; + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + "Cookie", + format!("SessionToken={}", satori_cookie).parse().unwrap(), + ); + + Ok(Client::builder().default_headers(headers).build()?) +} + +#[cfg(test)] +mod test_super { + use super::*; + + #[test] + fn test_current_cards_with_pending_reviews() { + let json_string = include_str!("./fixtures/current_cards_with_pending_reviews.json"); + let serialize_result = serialize_current_cards_response(json_string); + + assert!(serialize_result.is_ok()); + } + + #[test] + fn test_current_cards_with_no_reviews() { + let json_string = include_str!("./fixtures/current_cards_with_no_reviews.json"); + let serialize_result = serialize_current_cards_response(json_string); + + assert!(serialize_result.is_ok()); + } + + #[test] + fn test_new_card_with_pending_cards() { + let json_string = include_str!("./fixtures/new_cards_with_pending_cards.json"); + let serialized_result = serialize_new_cards_response(json_string); + + assert!(serialized_result.is_ok()); + } + + #[test] + fn test_new_card_with_no_cards() { + let json_string = include_str!("./fixtures/new_cards_with_no_cards.json"); + let serialized_result = serialize_new_cards_response(json_string); + + assert!(serialized_result.is_ok()); + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index a1310bb..6b36be5 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -5,7 +5,7 @@ use tokio::signal; use tower_http::{services::ServeDir, trace::TraceLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use crate::api::{bunpro::bunpro_handler, wanikani::wanikani_handler}; +use crate::api::{bunpro::bunpro_handler, satori::satori_handler, wanikani::wanikani_handler}; pub mod api; @@ -24,6 +24,7 @@ async fn main() { .route("/", get(root_handler)) .route("/api/wanikani", get(wanikani_handler)) .route("/api/bunpro", get(bunpro_handler)) + .route("/api/satori", get(satori_handler)) .layer(TraceLayer::new_for_http()); let address = SocketAddr::from(([0, 0, 0, 0], 3000));