johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! Who is asking, and stars: `dev.knotless.getViewer`, `starRepo` and
//! `unstarRepo`. The pages star as the account logged in to them, with
//! the session the server keeps for it ([`crate::login`]), through the
//! same code as `knotless repo star`.
use super::*;
use crate::viewer::Viewer;
use axum::Extension;
use knotless::social;
/// `dev.knotless.getViewer`'s answer.
#[derive(Serialize)]
pub struct ViewerOutput {
#[serde(skip_serializing_if = "Option::is_none")]
actor: Option<ActorView>,
login: bool,
/// The other accounts the browser asking is logged in to.
#[serde(skip_serializing_if = "Vec::is_empty")]
accounts: Vec<ActorView>,
/// Set for an operator, whom `KNOTLESS_ADMIN_DIDS` names.
#[serde(skip_serializing_if = "std::ops::Not::not")]
admin: bool,
/// Whether their PDS has spaces, for private repositories; absent
/// until known.
#[serde(skip_serializing_if = "Option::is_none")]
spaces: Option<bool>,
/// Set when this server's gateway writes for the account asking.
#[serde(skip_serializing_if = "std::ops::Not::not")]
delegated: bool,
/// Set when the account asking is an organization.
#[serde(skip_serializing_if = "std::ops::Not::not")]
organization: bool,
/// Set when their login to the pages is older than what they ask for.
#[serde(skip_serializing_if = "std::ops::Not::not")]
reauth: bool,
/// Set when the gateway writes for them under an older grant than it
/// asks for now.
#[serde(skip_serializing_if = "std::ops::Not::not")]
redelegate: bool,
}
/// `dev.knotless.getViewer`.
pub async fn get_viewer(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
) -> Response {
let actor = match &viewer.0 {
Some(did) => Some(actor(&app, did).await),
None => None,
};
let mut accounts = Vec::new();
for did in crate::login::accounts(&app, &headers) {
accounts.push(super::actor(&app, &did).await);
}
let login = app.gateway.is_some();
let spaces = viewer.0.as_deref().and_then(|did| app.has_spaces(did));
if let (Some(did), None) = (&viewer.0, spaces) {
// Known for a look after: as they logged in, unless before this
// server last started.
crate::spaces::discover(&app, did);
}
let admin = (viewer.0.as_ref()).is_some_and(|did| app.ctx.env.admin_dids.contains(did));
let delegated = (viewer.0.as_ref()).is_some_and(|did| crate::gateway::writes_for(&app, did));
let organization = (viewer.0.as_ref()).is_some_and(|did| super::orgs::is_org(&app, did));
let reauth = (viewer.0.as_ref()).is_some_and(|did| crate::login::behind(&app, did));
let redelegate = (viewer.0.as_ref()).is_some_and(|did| crate::gateway::grant_behind(&app, did));
answer(ViewerOutput {
actor,
login,
accounts,
admin,
delegated,
spaces,
organization,
reauth,
redelegate,
})
}
/// `dev.knotless.defs#starState`.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StarState {
star_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
star: Option<String>,
}
#[derive(Deserialize)]
pub struct StarInput {
repo: String,
}
/// `dev.knotless.starRepo`.
pub async fn star_repo(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<StarInput>,
) -> Result<Response> {
starring(&app, viewer, &headers, &input.repo, true).await
}
/// `dev.knotless.unstarRepo`.
pub async fn unstar_repo(
State(app): State<Arc<App>>,
Extension(viewer): Extension<Viewer>,
headers: HeaderMap,
axum::Json(input): axum::Json<StarInput>,
) -> Result<Response> {
starring(&app, viewer, &headers, &input.repo, false).await
}
/// The account logged in to the pages, for a write from them: refused
/// from another site, or for anyone without a session here to `act` with.
pub(super) fn logged_in(
app: &App,
viewer: Viewer,
headers: &HeaderMap,
act: &str,
) -> Result<String> {
if !crate::login::same_site(headers) {
return Err(Shown(StatusCode::FORBIDDEN, "not from this site".into()));
}
// The pages' writes are made only with their cookie: a service token
// names someone, who may have a session here, but is not them asking.
let token = headers.contains_key(header::AUTHORIZATION);
let kept = |did: &String| session::load(&crate::login::ctx_for(app, did), did).is_some();
let Some(did) = viewer.0.filter(|_| !token).filter(kept) else {
let why = format!("log in to this site's pages to {act}");
return Err(Shown(StatusCode::UNAUTHORIZED, why));
};
Ok(did)
}
/// Stars the repository `uri` names as the account logged in to the
/// pages, or takes its star back, and indexes what that wrote at once, so
/// the count it answers, and the page after, have it.
async fn starring(
app: &App,
viewer: Viewer,
headers: &HeaderMap,
uri: &str,
on: bool,
) -> Result<Response> {
let did = logged_in(app, viewer, headers, "star")?;
let ctx = crate::login::ctx_for(app, &did);
let found = repo::found(app, uri).await?;
let public = Some(found).filter(|r| r.in_space.is_none());
let private = anyhow::anyhow!("{uri}: no such public repository");
let repo = public.ok_or(not_found(private))?;
let url = RepoUrl {
authority: repo.did.clone(),
name: repo.name.clone(),
};
let starred = social::star(&ctx, &url, on)
.await
.map_err(AppError::upstream)?;
let index = app.index.as_ref();
for rkey in &starred.deleted {
let deleted = index.map(|i| i.delete(&did, nsid::STAR, rkey));
deleted
.transpose()
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
}
if let Some((rkey, record)) = &starred.mark {
let value = knotless::records::typed(nsid::STAR, record);
let put = index.map(|i| i.put(&did, nsid::STAR, rkey, &value));
put.transpose()
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
}
let count = index.map(|i| i.star_count(&repo.did, &repo.key));
let star_count = count
.transpose()
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
Ok(answer(StarState {
star_count: star_count.unwrap_or_default(),
star: starred.uri(),
}))
}