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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
184 lines · 6.4 KB Raw History
//! 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(),
    }))
}