use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use axum::extract::State;
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use serde_json::json;
use crate::web::attention::{demand_of_subjects, Presence, Subject};
use crate::web::github::{self, Refusal};
use crate::workshop::WorkshopState;
use wsb::daemon::WorktreeId;
pub fn routes() -> Router<Arc<WorkshopState>> {
Router::new()
.route("/api/v1/github/attention", post(attention))
.route("/api/v1/github/refresh", post(refresh))
.route("/api/v1/github/budget", get(budget))
}
fn refused(status: StatusCode, error: impl Into<String>) -> Refusal {
(status, Json(crate::web::handlers::sessions::ErrorResponse { error: error.into() }))
}
async fn checkouts(workshop: &WorkshopState, subjects: &[Subject]) -> Result<BTreeMap<String, PathBuf>, Refusal> {
let mut found = BTreeMap::new();
for subject in subjects {
if let Subject::Worktree { id } = subject {
let parsed = WorktreeId::parse(id).map_err(|error| refused(StatusCode::BAD_REQUEST, error))?;
let runtime = workshop.open(&parsed).await.map_err(|error| refused(StatusCode::NOT_FOUND, error))?;
found.insert(id.clone(), runtime.registered.super_root.clone());
}
}
Ok(found)
}
async fn attention(
State(workshop): State<Arc<WorkshopState>>,
Json(presence): Json<Presence>,
) -> Result<StatusCode, Refusal> {
let github = github::of_workshop(&workshop)?;
let shows = if presence.visible {
let found = checkouts(&workshop, &presence.subjects).await?;
demand_of_subjects(&presence.subjects, &found).map_err(|error| refused(StatusCode::BAD_REQUEST, error))?
} else {
wsb_github::sync::Demand::default()
};
github.attention.page(&presence.client, presence.visible, shows, Instant::now());
Ok(StatusCode::NO_CONTENT)
}
async fn refresh(
State(workshop): State<Arc<WorkshopState>>,
Json(presence): Json<Presence>,
) -> Result<Json<wsb_github::sync::SyncReport>, Refusal> {
let github = github::of_workshop(&workshop)?;
let found = checkouts(&workshop, &presence.subjects).await?;
let mut shows =
demand_of_subjects(&presence.subjects, &found).map_err(|error| refused(StatusCode::BAD_REQUEST, error))?;
github.attention.page(&presence.client, true, shows.clone(), Instant::now());
shows.notifications = true;
if !github.attention.may_refresh(&presence.client, Instant::now()) {
return Ok(Json(wsb_github::sync::SyncReport::default()));
}
let client = match github::client(github) {
Ok(client) => client,
Err(_) => return Ok(Json(wsb_github::sync::SyncReport { no_token: true, ..Default::default() })),
};
let prj = github.prj.clone();
let full = tokio::task::spawn_blocking(move || wsb_github::targets::Targets::of(&prj))
.await
.map_err(|e| github::failure(wsb_github::Failure::Mirror(anyhow::anyhow!("join error: {e}"))))?
.map_err(|e| github::failure(wsb_github::Failure::Mirror(e)))?;
Ok(Json(wsb_github::sync::sync(github.service.mirror(), Some(&client), &full, &shows, true).await))
}
async fn budget(State(workshop): State<Arc<WorkshopState>>) -> Result<Json<serde_json::Value>, Refusal> {
let github = github::of_workshop(&workshop)?;
let budget = wsb_github::budget::Budget::new(github.service.mirror().clone(), wsb_github::budget::Class::Interactive);
let (standings, calls) = github::blocking(move || {
let now = chrono::Utc::now();
let read = || -> anyhow::Result<_> { Ok((budget.standings()?, budget.calls_this_hour(now)?)) };
read().map_err(wsb_github::Failure::Mirror)
})
.await?;
Ok(Json(json!({ "standings": standings, "calls": calls })))
}