forked from alemi/upub
fix: following/followers collections
This commit is contained in:
parent
89c6a923dc
commit
03603a396d
1 changed files with 81 additions and 12 deletions
|
@ -1,38 +1,107 @@
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, SelectColumns};
|
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter, QuerySelect, SelectColumns};
|
||||||
|
|
||||||
use upub::{model, Context};
|
use upub::{model, Context};
|
||||||
|
|
||||||
use crate::{activitypub::Pagination, builders::JsonLD};
|
use crate::{activitypub::Pagination, builders::JsonLD, ApiError, AuthIdentity, Identity};
|
||||||
|
|
||||||
pub async fn get<const OUTGOING: bool>(
|
pub async fn get<const OUTGOING: bool>(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
|
AuthIdentity(auth): AuthIdentity,
|
||||||
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
|
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
|
||||||
let follow___ = if OUTGOING { "following" } else { "followers" };
|
let follow___ = if OUTGOING { "following" } else { "followers" };
|
||||||
use upub::model::relation::Column::{Follower, Following};
|
|
||||||
let count = model::relation::Entity::find()
|
|
||||||
.filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id)))
|
|
||||||
.count(ctx.db()).await.unwrap_or_else(|e| {
|
|
||||||
tracing::error!("failed counting {follow___} for {id}: {e}");
|
|
||||||
0
|
|
||||||
});
|
|
||||||
|
|
||||||
crate::builders::collection(&upub::url!(ctx, "/actors/{id}/{follow___}"), Some(count))
|
// TODO technically we could show remote instances following/followers count from that specific
|
||||||
|
// instance, but that's annoying because means running a COUNT every time, so likely this will
|
||||||
|
// keep answering 0
|
||||||
|
|
||||||
|
let count = match model::actor::Entity::find_by_ap_id(&ctx.oid(&id))
|
||||||
|
.find_also_related(model::config::Entity)
|
||||||
|
.one(ctx.db())
|
||||||
|
.await?
|
||||||
|
.ok_or_else(ApiError::not_found)?
|
||||||
|
{
|
||||||
|
(user, Some(config)) => {
|
||||||
|
let mut hide_count = if OUTGOING { !config.show_following } else { !config.show_followers };
|
||||||
|
if hide_count {
|
||||||
|
if let Some(internal) = auth.my_id() {
|
||||||
|
if internal == user.internal {
|
||||||
|
hide_count = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match (hide_count, OUTGOING) {
|
||||||
|
(true, _) => 0,
|
||||||
|
(false, true) => user.following_count,
|
||||||
|
(false, false) => user.followers_count,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(user, None) => {
|
||||||
|
if !auth.is_local() {
|
||||||
|
return Err(ApiError::forbidden());
|
||||||
|
}
|
||||||
|
if OUTGOING { user.following_count } else { user.followers_count }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
crate::builders::collection(&upub::url!(ctx, "/actors/{id}/{follow___}"), Some(count as u64))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn page<const OUTGOING: bool>(
|
pub async fn page<const OUTGOING: bool>(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Query(page): Query<Pagination>,
|
Query(page): Query<Pagination>,
|
||||||
|
AuthIdentity(auth): AuthIdentity,
|
||||||
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
|
) -> crate::ApiResult<JsonLD<serde_json::Value>> {
|
||||||
|
use upub::model::relation::Column::{Follower, Following, FollowerInstance, FollowingInstance};
|
||||||
let follow___ = if OUTGOING { "following" } else { "followers" };
|
let follow___ = if OUTGOING { "following" } else { "followers" };
|
||||||
|
|
||||||
let limit = page.batch.unwrap_or(20).min(50);
|
let limit = page.batch.unwrap_or(20).min(50);
|
||||||
let offset = page.offset.unwrap_or(0);
|
let offset = page.offset.unwrap_or(0);
|
||||||
|
|
||||||
use upub::model::relation::Column::{Follower, Following};
|
let mut filter = Condition::all()
|
||||||
|
.add(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id)));
|
||||||
|
|
||||||
|
let hidden = {
|
||||||
|
// TODO i could avoid this query if ctx.uid(id) == Identity::Local { id }
|
||||||
|
match model::actor::Entity::find_by_ap_id(&ctx.oid(&id))
|
||||||
|
.find_also_related(model::config::Entity)
|
||||||
|
.one(ctx.db())
|
||||||
|
.await?
|
||||||
|
.ok_or_else(ApiError::not_found)?
|
||||||
|
{
|
||||||
|
// assume all remote users have private followers
|
||||||
|
// this because we get to see some of their "private" followers if they follow local users,
|
||||||
|
// and there is no mechanism to broadcast privacy on/off, so we could be leaking followers. to
|
||||||
|
// mitigate this, just assume them all private: local users can only see themselves and remote
|
||||||
|
// fetchers can only see relations from their instance (meaning likely zero because we only
|
||||||
|
// store relations for which at least one end is on local instance)
|
||||||
|
(_, None) => true,
|
||||||
|
(_, Some(config)) => {
|
||||||
|
if OUTGOING { !config.show_followers } else { !config.show_following }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if hidden {
|
||||||
|
match auth {
|
||||||
|
Identity::Anonymous => return Err(ApiError::unauthorized()),
|
||||||
|
Identity::Local { id, internal } => {
|
||||||
|
if id != ctx.uid(&id) {
|
||||||
|
filter = filter.add(if OUTGOING { Following } else { Follower }.eq(internal));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Identity::Remote { internal, .. } => {
|
||||||
|
filter = filter.add(if OUTGOING { FollowingInstance } else { FollowerInstance }.eq(internal));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let following = model::relation::Entity::find()
|
let following = model::relation::Entity::find()
|
||||||
.filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id)))
|
.filter(filter)
|
||||||
.select_only()
|
.select_only()
|
||||||
.select_column(if OUTGOING { Following } else { Follower })
|
.select_column(if OUTGOING { Following } else { Follower })
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
|
Loading…
Reference in a new issue