Skip to content

Commit

Permalink
Merge pull request #13 from thoiberg/add-satori
Browse files Browse the repository at this point in the history
Add Satori API
  • Loading branch information
thoiberg authored Jul 4, 2023
2 parents e0f376a + c595c94 commit 13c6ee5
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .env.development.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
WANIKANI_API_TOKEN="<Token goes here>"
BUNPRO_API_TOKEN="<Token goes here>"
BUNPRO_API_TOKEN="<Token goes here>"
SATORI_COOKIE="<Cookie goes here>"
1 change: 1 addition & 0 deletions backend/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use axum::{http::StatusCode, Json};

pub mod bunpro;
pub mod satori;
pub mod wanikani;

#[derive(serde::Serialize)]
Expand Down
4 changes: 4 additions & 0 deletions backend/src/api/satori.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod data;
pub mod request;

pub use request::satori_handler;
37 changes: 37 additions & 0 deletions backend/src/api/satori/data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use chrono::{DateTime, Utc};

#[derive(serde::Serialize)]
pub struct SatoriData {
data_updated_at: DateTime<Utc>,
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<String>,
exception: Option<String>,
}

#[derive(serde::Deserialize)]
pub struct SatoriNewCardsResponse {
result: u32,
success: bool,
message: Option<String>,
exception: Option<String>,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"result": 0,
"success": true,
"message": null,
"exception": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"result": 6,
"success": true,
"message": null,
"exception": null
}
6 changes: 6 additions & 0 deletions backend/src/api/satori/fixtures/new_cards_with_no_cards.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"result": 0,
"success": true,
"message": null,
"exception": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"result": 20,
"success": true,
"message": null,
"exception": null
}
105 changes: 105 additions & 0 deletions backend/src/api/satori/request.rs
Original file line number Diff line number Diff line change
@@ -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<Json<SatoriData>, (StatusCode, Json<ErrorResponse>)> {
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<SatoriCurrentCardsResponse> {
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<SatoriCurrentCardsResponse> {
let json_data: SatoriCurrentCardsResponse = serde_json::from_str(body)?;

Ok(json_data)
}

async fn get_new_cards() -> anyhow::Result<SatoriNewCardsResponse> {
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<SatoriNewCardsResponse> {
let json_data: SatoriNewCardsResponse = serde_json::from_str(body)?;

Ok(json_data)
}

fn satori_client() -> anyhow::Result<Client> {
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());
}
}
3 changes: 2 additions & 1 deletion backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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));
Expand Down

0 comments on commit 13c6ee5

Please sign in to comment.