~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
327 lines · 11.5 KB Raw History
//! What the appview answers from its index.

use crate::index::CheckFilter;
use crate::{App, AppError};
use axum::Json;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use knotless::records::RepoRecord;
use serde::Deserialize;
use serde_json::{Value, json};
use std::sync::Arc;

fn index(app: &App) -> Result<&crate::index::Index, AppError> {
    app.index
        .as_deref()
        .ok_or_else(|| AppError(StatusCode::NOT_FOUND, "this server keeps no index".into()))
}

#[derive(Deserialize)]
pub struct ForgeParams {
    repo: String,
    key: String,
    limit: Option<usize>,
    cursor: Option<String>,
}

/// The pulls, issues, comments and state changes about a repository: the
/// one its owner made with `key`, whatever it has been named. A page at a
/// time, by URI.
pub async fn get_forge(
    State(app): State<Arc<App>>,
    Query(p): Query<ForgeParams>,
) -> Result<Json<Value>, AppError> {
    let did = knotless::forge::parse_uri(&p.repo)
        .map_err(|e| AppError(StatusCode::BAD_REQUEST, e.to_string()))?
        .0;
    let mut entries = index(&app)?
        .forge(did, &p.key)
        .map_err(anyhow::Error::from)
        .map_err(AppError::internal)?;
    entries.sort_by(|a, b| a.uri.cmp(&b.uri));
    let after = p.cursor.unwrap_or_default();
    let limit = p.limit.unwrap_or(500).clamp(1, 1000);
    let mut page: Vec<_> = entries.into_iter().filter(|e| e.uri > after).collect();
    let more = page.len() > limit;
    page.truncate(limit);
    let mut out = json!({ "entries": page });
    if more {
        out["cursor"] = page[limit - 1].uri.clone().into();
    }
    Ok(Json(out))
}

#[derive(Deserialize)]
pub struct ChecksParams {
    repo: String,
    key: String,
    #[serde(rename = "ref")]
    refname: Option<String>,
    commit: Option<String>,
    pull: Option<String>,
    limit: Option<usize>,
    cursor: Option<String>,
}

/// The checks of a repository by the runners its record names, as the
/// index has that record now: newest first, a page at a time.
pub async fn list_checks(
    State(app): State<Arc<App>>,
    Query(p): Query<ChecksParams>,
) -> Result<Json<Value>, AppError> {
    let did = knotless::forge::parse_uri(&p.repo)
        .map_err(|e| AppError(StatusCode::BAD_REQUEST, e.to_string()))?
        .0;
    let index = index(&app)?;
    let name = index
        .name(did, &p.key)
        .map_err(anyhow::Error::from)
        .map_err(AppError::internal)?;
    let uri = knotless::forge::repo_uri(did, name.as_deref().unwrap_or_default());
    let record = index
        .repo(&uri)
        .map_err(anyhow::Error::from)
        .map_err(AppError::internal)?;
    let runners: Vec<String> = record
        .and_then(|v| serde_json::from_value::<RepoRecord>(v).ok())
        .map(|r| r.runners.into_iter().map(|r| r.did).collect())
        .unwrap_or_default();
    let filter = CheckFilter {
        refname: p.refname.as_deref(),
        commit: p.commit.as_deref(),
        pull: p.pull.as_deref(),
        ..CheckFilter::default()
    };
    let limit = p.limit.unwrap_or(50).clamp(1, 100);
    let (checks, cursor) = index
        .checks(did, &p.key, &runners, filter, limit, p.cursor.as_deref())
        .map_err(anyhow::Error::from)
        .map_err(AppError::internal)?;
    if let Some(index) = &app.index {
        app.recheck(index, &checks);
    }
    let checks: Vec<Value> = checks
        .into_iter()
        .map(|e| json!({"uri": e.uri, "runner": e.author, "value": e.value}))
        .collect();
    let mut out = json!({ "checks": checks });
    if let Some(cursor) = cursor {
        out["cursor"] = cursor.into();
    }
    Ok(Json(out))
}

/// What this appview offers the clients that use it: the runner a new
/// public repository names unless its owner names others, and the oldest
/// client version and iOS build it asks for.
pub async fn describe_server(State(app): State<Arc<App>>) -> Json<Value> {
    let runners = app.default_runner.iter().map(|did| json!({"did": did}));
    let mut out = json!({"runners": runners.collect::<Vec<_>>()});
    if let Some((minimum, reason)) = &app.client_minimum {
        out["client"] = json!({"minimum": minimum});
        if let Some(reason) = reason {
            out["client"]["reason"] = reason.as_str().into();
        }
    }
    if let Some((build, reason)) = &app.ios_minimum {
        out["ios"] = json!({"minimumBuild": build});
        if let Some(reason) = reason {
            out["ios"]["reason"] = reason.as_str().into();
        }
    }
    Json(out)
}

#[derive(Deserialize)]
pub struct ListParams {
    q: Option<String>,
    runner: Option<String>,
    triager: Option<String>,
    topic: Option<String>,
    limit: Option<usize>,
    cursor: Option<String>,
}

/// Repositories, newest first, matching `q` by name or description and
/// naming `runner` among their runners, `triager` among their triagers
/// and `topic` among their topics, a page at a time. The first page of a
/// search with none of these starts with the repositories named `q`
/// ([`Index::named`](crate::index::Index::named)), originals before forks;
/// asked as an account, its own of those first, and before them the
/// private repositories it may read here that match ([`readable`]), and
/// after them its other public ones that match. Each kind put first, when
/// all of it fits on that page, no page lists again.
pub async fn list_repos(
    State(app): State<Arc<App>>,
    axum::Extension(viewer): axum::Extension<crate::viewer::Viewer>,
    Query(p): Query<ListParams>,
) -> Result<Json<Value>, AppError> {
    let limit = p.limit.unwrap_or(50).clamp(1, 500);
    let q = p.q.as_deref().unwrap_or_default();
    let named = p.runner.is_some() || p.triager.is_some() || p.topic.is_some();
    let first = p.cursor.is_none() && !named;
    let index = index(&app)?;
    let (exact, own, not_named, not_owner) = first_of(index, q, limit, named, viewer.0.as_deref())?;
    let (repos, cursor) = index
        .repos_page(
            q,
            limit,
            p.cursor.as_deref(),
            crate::index::Among {
                not_named,
                not_owner,
                runner: p.runner.as_deref(),
                triager: p.triager.as_deref(),
                topic: p.topic.as_deref(),
                ..Default::default()
            },
        )
        .map_err(anyhow::Error::from)
        .map_err(AppError::internal)?;
    let private = match viewer.0.filter(|_| first).zip(app.spaces.as_deref()) {
        Some((viewer, spaces)) => readable(&app, spaces, &viewer, q, limit),
        None => Vec::new(),
    };
    let marked = private.len();
    let put = first.then(|| exact.into_iter().chain(own));
    let mut seen = std::collections::HashSet::new();
    let public = put.into_iter().flatten().chain(repos);
    let public = public.filter(|r| seen.insert(r.uri.clone()));
    let repos: Vec<_> = private.into_iter().chain(public).collect();
    let owners = repos.iter().map(|r| crate::web::actor(&app, &r.did));
    let owners = futures::future::join_all(owners).await;
    let repos: Vec<Value> = repos
        .into_iter()
        .zip(owners)
        .enumerate()
        .map(|(i, (r, owner))| {
            let mut repo = json!(r);
            repo["owner"] = json!(owner);
            if i < marked {
                repo["private"] = true.into();
            }
            repo
        })
        .collect();
    let mut out = json!({ "repos": repos });
    if let Some(cursor) = cursor {
        out["cursor"] = cursor.into();
    }
    Ok(Json(out))
}

/// What a search puts first: the repositories named as searched, the
/// viewer's own that match, then the name and the owner the rest leaves
/// out.
type First<'a> = (
    Vec<crate::index::Listed>,
    Vec<crate::index::Listed>,
    Option<&'a str>,
    Option<&'a str>,
);

/// What a search for `q` puts first, as [`list_repos`] says, at most
/// `limit` of each kind: the repositories named `q`, `viewer`'s first,
/// and `viewer`'s other ones that match; then the name and the owner
/// whose repositories the rest leaves out, each only when all of its kind
/// fit. None for no search, or one `named` by runner, triager or topic.
fn first_of<'a>(
    index: &crate::index::Index,
    q: &'a str,
    limit: usize,
    named: bool,
    viewer: Option<&'a str>,
) -> Result<First<'a>, AppError> {
    if q.is_empty() || named {
        return Ok((Vec::new(), Vec::new(), None, None));
    }
    let mut exact = index
        .named(q, limit + 1)
        .map_err(anyhow::Error::from)
        .map_err(AppError::internal)?;
    let all_named = exact.len() <= limit;
    exact.truncate(limit);
    // Stable: the viewer's own first, each kind as it was.
    exact.sort_by_key(|r| Some(r.did.as_str()) != viewer);
    let mut own = match viewer {
        Some(viewer) => {
            let among = crate::index::Among {
                owner: Some(viewer),
                ..Default::default()
            };
            let (page, _) = index
                .repos_page(q, limit + 1, None, among)
                .map_err(anyhow::Error::from)
                .map_err(AppError::internal)?;
            page
        }
        None => Vec::new(),
    };
    let all_own = own.len() <= limit;
    own.truncate(limit);
    let not_named = all_named.then_some(q);
    Ok((exact, own, not_named, viewer.filter(|_| all_own)))
}

/// How many private repositories [`readable`] looks through for those its
/// viewer may read.
const READABLE: usize = 500;

/// The private repositories, newest first and at most `limit`, whose name or
/// description holds `q` and which `viewer` may read here, as this server's
/// index of their spaces has them: their own, and those they were proven a
/// member of, as they read one or as they are asked after in the
/// background ([`crate::spaces::discover`]), which also looks for their
/// own yet to be found, for a look after.
fn readable(
    app: &Arc<App>,
    spaces: &crate::index::Index,
    viewer: &str,
    q: &str,
    limit: usize,
) -> Vec<crate::index::Listed> {
    crate::spaces::discover(app, viewer);
    let found = spaces.repos(q, READABLE).unwrap_or_default();
    let theirs = spaces.read_of(viewer).unwrap_or_default();
    let shown: Vec<_> = found
        .into_iter()
        .filter(|r| r.did == viewer || theirs.contains(&r.uri))
        .take(limit)
        .collect();
    if !shown.is_empty() {
        crate::private::touched();
    }
    shown
}

#[derive(Deserialize)]
pub struct OwnersParams {
    q: String,
    limit: Option<usize>,
}

/// The accounts owning repositories whose handle holds `q`, ignoring case:
/// those it starts with first, then the one with the newest repository.
/// Owners whose handles were never looked up are looked up first, together,
/// as pages look them up, and kept: once each.
pub async fn search_owners(
    State(app): State<Arc<App>>,
    Query(p): Query<OwnersParams>,
) -> Result<Json<Value>, AppError> {
    let limit = p.limit.unwrap_or(10).clamp(1, 50);
    let index = index(&app)?;
    let unnamed = index.unnamed_owners().map_err(anyhow::Error::from);
    let naming = unnamed.map_err(AppError::internal)?;
    let naming = naming.iter().map(|did| crate::web::handle(&app, did));
    futures::future::join_all(naming).await;
    let found = index.owners(p.q.trim(), limit).map_err(anyhow::Error::from);
    let found = found.map_err(AppError::internal)?;
    let shown = found.iter().map(|(did, ..)| crate::web::actor(&app, did));
    let shown = futures::future::join_all(shown).await;
    let owners: Vec<Value> = shown
        .into_iter()
        .zip(found)
        .map(|(owner, (.., repos))| json!({ "owner": owner, "repoCount": repos }))
        .collect();
    Ok(Json(json!({ "owners": owners })))
}