wsb-daemon 0.291.8591

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the host
//! Issues: read from the workshop's mirror, written through GitHub.
//!
//! Every read answers from the mirror — instantly, offline, with no token —
//! and carries how current each repository's copy is. Refreshing is the
//! daemon keeping current what a page shows, or a button (`POST
//! /github/refresh`), never a side effect of a read.

use axum::{
    extract::{Path, Query, State},
    Json,
};
use serde::Deserialize;

use crate::web::github::{self, Refusal};
use crate::web::state::WebState;
use wsb_github::client::{CreateIssueRequest, GitHubComment, GitHubIssue, UpdateIssueRequest};
use wsb_github::service::IssueState;

#[derive(Deserialize)]
pub struct ListIssuesQuery {
    pub state: Option<String>,
    pub repo: Option<String>,
}

fn issue_state(query: &ListIssuesQuery) -> Result<IssueState, Refusal> {
    query
        .state
        .as_deref()
        .unwrap_or("open")
        .parse()
        .map_err(|error: String| github::failure(wsb_github::Failure::Invalid(error)))
}

/// GET /api/v1/issues — every mirrored issue of this worktree's repositories.
pub async fn list_issues(
    State(state): State<WebState>,
    Query(query): Query<ListIssuesQuery>,
) -> Result<Json<wsb_github::service::IssueList>, Refusal> {
    let wanted = issue_state(&query)?;
    let service = github::of(&state)?.service.clone();
    let mut repos = github::repository_names(&state).await?;
    if let Some(repo) = &query.repo {
        repos.retain(|held| held == repo);
    }
    github::blocking(move || service.issues(&repos, wanted))
        .await
        .map(Json)
}

/// GET /api/v1/issues/:owner/:repo — one repository's mirrored issues.
pub async fn list_repo_issues(
    State(state): State<WebState>,
    Path((owner, repo)): Path<(String, String)>,
    Query(query): Query<ListIssuesQuery>,
) -> Result<Json<serde_json::Value>, Refusal> {
    let wanted = issue_state(&query)?;
    let service = github::of(&state)?.service.clone();
    let name = format!("{owner}/{repo}");
    let repos = vec![name.clone()];
    let mut list = github::blocking(move || service.issues(&repos, wanted)).await?;
    Ok(Json(serde_json::json!({
        "repo": name,
        "issues": list.issues,
        "sync": list.sync.remove(&name),
    })))
}

/// POST /api/v1/issues/:owner/:repo
pub async fn create_issue(
    State(state): State<WebState>,
    Path((owner, repo)): Path<(String, String)>,
    Json(body): Json<CreateIssueRequest>,
) -> Result<Json<GitHubIssue>, Refusal> {
    let github = github::of(&state)?;
    let client = github::client(github)?;
    github
        .service
        .create_issue(&client, &owner, &repo, &body)
        .await
        .map(Json)
        .map_err(github::failure)
}

/// PATCH /api/v1/issues/:owner/:repo/:number
pub async fn update_issue(
    State(state): State<WebState>,
    Path((owner, repo, number)): Path<(String, String, i64)>,
    Json(body): Json<UpdateIssueRequest>,
) -> Result<Json<GitHubIssue>, Refusal> {
    let github = github::of(&state)?;
    let client = github::client(github)?;
    github
        .service
        .update_issue(&client, &owner, &repo, number, &body)
        .await
        .map(Json)
        .map_err(github::failure)
}

#[derive(Deserialize)]
pub struct CommentRequest {
    pub body: String,
}

/// GET /api/v1/issues/:owner/:repo/:number/comments
///
/// Opening a thread puts it in the sweep; the first open reads it now, when
/// there is a token to read with.
pub async fn list_comments(
    State(state): State<WebState>,
    Path((owner, repo, number)): Path<(String, String, i64)>,
) -> Result<Json<serde_json::Value>, Refusal> {
    let github = github::of(&state)?;
    let client = github::client(github).ok();
    let listed = github
        .service
        .comments(client.as_ref(), &format!("{owner}/{repo}"), number)
        .await
        .map_err(github::failure)?;
    Ok(Json(serde_json::json!({ "comments": listed.items, "sync": listed.sync })))
}

/// POST /api/v1/issues/:owner/:repo/:number/comments
pub async fn add_comment(
    State(state): State<WebState>,
    Path((owner, repo, number)): Path<(String, String, i64)>,
    Json(body): Json<CommentRequest>,
) -> Result<Json<GitHubComment>, Refusal> {
    let github = github::of(&state)?;
    let client = github::client(github)?;
    github
        .service
        .add_comment(&client, &owner, &repo, number, &body.body)
        .await
        .map(Json)
        .map_err(github::failure)
}