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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
148 lines · 4.5 KB Raw History
//! Follows: `dev.knotless.followActor` and `unfollowActor`, as the account
//! logged in to the pages, through the same code as `knotless follow`, and
//! `getFollowers` and `getFollows`, from the index.

use super::stars::logged_in;
use super::*;
use crate::viewer::Viewer;
use axum::Extension;
use knotless::social;

/// `dev.knotless.defs#followState`.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FollowState {
    followers_count: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    following: Option<String>,
}

#[derive(Deserialize)]
pub struct FollowInput {
    subject: String,
}

/// `dev.knotless.followActor`.
pub async fn follow_actor(
    State(app): State<Arc<App>>,
    Extension(viewer): Extension<Viewer>,
    headers: HeaderMap,
    axum::Json(input): axum::Json<FollowInput>,
) -> Result<Response> {
    following(&app, viewer, &headers, &input.subject, true).await
}

/// `dev.knotless.unfollowActor`.
pub async fn unfollow_actor(
    State(app): State<Arc<App>>,
    Extension(viewer): Extension<Viewer>,
    headers: HeaderMap,
    axum::Json(input): axum::Json<FollowInput>,
) -> Result<Response> {
    following(&app, viewer, &headers, &input.subject, false).await
}

/// Follows the account `subject` as the account logged in to the pages,
/// or unfollows it, and indexes what that wrote at once, so the count it
/// answers, and the page after, have it.
async fn following(
    app: &App,
    viewer: Viewer,
    headers: &HeaderMap,
    subject: &str,
    on: bool,
) -> Result<Response> {
    let did = logged_in(app, viewer, headers, "follow")?;
    let Ok(id) = knotless::identity::resolve(&app.ctx, subject).await else {
        return Ok(no_profile(anyhow::anyhow!("{subject}: no such account")));
    };
    if on && id.did == did {
        let why = "you can't follow yourself";
        return Err(Shown(StatusCode::BAD_REQUEST, why.into()));
    }
    let ctx = crate::login::ctx_for(app, &did);
    let followed = social::follow(&ctx, &id.did, on)
        .await
        .map_err(AppError::upstream)?;
    let index = app.index.as_ref();
    for rkey in &followed.deleted {
        let deleted = index.map(|i| i.delete(&did, nsid::FOLLOW, rkey));
        deleted
            .transpose()
            .map_err(anyhow::Error::from)
            .map_err(AppError::internal)?;
    }
    if let Some((rkey, record)) = &followed.mark {
        let value = knotless::records::typed(nsid::FOLLOW, record);
        let put = index.map(|i| i.put(&did, nsid::FOLLOW, rkey, &value));
        put.transpose()
            .map_err(anyhow::Error::from)
            .map_err(AppError::internal)?;
    }
    let counts = index.map(|i| i.follow_counts(&id.did));
    let counts = counts
        .transpose()
        .map_err(anyhow::Error::from)
        .map_err(AppError::internal)?;
    Ok(answer(FollowState {
        followers_count: counts.unwrap_or_default().0,
        following: followed.uri(),
    }))
}

#[derive(Deserialize)]
pub struct FollowsParams {
    actor: String,
    limit: Option<u32>,
    cursor: Option<String>,
}

#[derive(Serialize)]
pub struct FollowsOutput {
    actors: Vec<ActorView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    cursor: Option<String>,
}

/// `dev.knotless.getFollowers`.
pub async fn get_followers(
    State(app): State<Arc<App>>,
    Query(p): Query<FollowsParams>,
) -> Response {
    follows(&app, p, true).await
}

/// `dev.knotless.getFollows`.
pub async fn get_follows(State(app): State<Arc<App>>, Query(p): Query<FollowsParams>) -> Response {
    follows(&app, p, false).await
}

/// A page of the accounts following `p.actor`, or with `followers` false
/// those it follows.
async fn follows(app: &App, p: FollowsParams, followers: bool) -> Response {
    let Some(index) = app.index.clone() else {
        return Shown(StatusCode::NOT_FOUND, "this server keeps no index".into()).into_response();
    };
    match knotless::identity::resolve(&app.ctx, &p.actor).await {
        Ok(id) => page(app, &index, &id.did, p, followers)
            .await
            .into_response(),
        Err(e) => no_profile(e),
    }
}

async fn page(
    app: &App,
    index: &Index,
    did: &str,
    p: FollowsParams,
    followers: bool,
) -> Result<Response> {
    let limit = p.limit.unwrap_or(50).clamp(1, 100);
    let page = index.follows(did, followers, limit, p.cursor.as_deref());
    let (dids, cursor) = page
        .map_err(anyhow::Error::from)
        .map_err(AppError::internal)?;
    let actors = futures::future::join_all(dids.iter().map(|did| actor(app, did))).await;
    Ok(answer(FollowsOutput { actors, cursor }))
}