//! Invites to private repositories, left for their runners: nothing //! written in a space reaches the firehose, so an owner says here that a //! repository names a runner, and the runner asks what it was told //! ([`knotless::invites`]). Each signs for itself with a service token. An //! invite is only a hint, which anyone can leave: the runner reads the //! repository's record, as a member of its space, before running anything. use crate::gateway::caller; use crate::moderation::{bad, index}; use crate::{App, AppError}; use axum::Json; use axum::extract::{Query, State}; use axum::http::HeaderMap; use knotless::invites::{INVITE, LIST}; use serde::Deserialize; use serde_json::{Value, json}; use std::sync::Arc; #[derive(Deserialize)] pub struct Invite { runner: String, name: String, } pub async fn invite( State(app): State>, headers: HeaderMap, Json(body): Json, ) -> Result, AppError> { let index = index(&app)?; let did = caller(&app, &headers, INVITE).await?; if !body.runner.starts_with("did:") { return Err(bad("a runner is named by its DID")); } if !knotless::keys::valid_name(&body.name) { return Err(bad("a repository's name is letters, digits and ._~-")); } let left = index.invite(&body.runner, &did, &body.name); left.map_err(anyhow::Error::from) .map_err(AppError::internal)?; Ok(Json(json!({}))) } #[derive(Deserialize)] pub struct ListParams { limit: Option, cursor: Option, } pub async fn list( State(app): State>, headers: HeaderMap, Query(p): Query, ) -> Result, AppError> { let index = index(&app)?; let runner = caller(&app, &headers, LIST).await?; let limit = p.limit.unwrap_or(100).clamp(1, 500); let after = match p.cursor.as_deref().map(str::parse::) { None => 0, Some(Ok(id)) => id, Some(Err(_)) => return Err(bad("a cursor is one this method gave")), }; let page = index .invites(&runner, after, limit) .map_err(anyhow::Error::from) .map_err(AppError::internal)?; let last = page.last().map_or(after, |(id, _)| *id); let invites: Vec = page.into_iter().map(|(_, invite)| invite).collect(); Ok(Json( json!({ "invites": invites, "cursor": last.to_string() }), )) }