//! 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, } #[derive(Deserialize)] pub struct FollowInput { subject: String, } /// `dev.knotless.followActor`. pub async fn follow_actor( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { following(&app, viewer, &headers, &input.subject, true).await } /// `dev.knotless.unfollowActor`. pub async fn unfollow_actor( State(app): State>, Extension(viewer): Extension, headers: HeaderMap, axum::Json(input): axum::Json, ) -> Result { 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 { 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, cursor: Option, } #[derive(Serialize)] pub struct FollowsOutput { actors: Vec, #[serde(skip_serializing_if = "Option::is_none")] cursor: Option, } /// `dev.knotless.getFollowers`. pub async fn get_followers( State(app): State>, Query(p): Query, ) -> Response { follows(&app, p, true).await } /// `dev.knotless.getFollows`. pub async fn get_follows(State(app): State>, Query(p): Query) -> 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 { 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 })) }