johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! 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 })))
}