johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! 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 }))
}