feat(presentation): followers/following list endpoints for remote actors
This commit is contained in:
@@ -110,3 +110,20 @@ pub struct RemoteActorResponse {
|
|||||||
pub following_url: Option<String>,
|
pub following_url: Option<String>,
|
||||||
pub attachment: Vec<ProfileField>,
|
pub attachment: Vec<ProfileField>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ActorConnectionResponse {
|
||||||
|
pub handle: String,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ActorConnectionPageResponse {
|
||||||
|
pub items: Vec<ActorConnectionResponse>,
|
||||||
|
pub page: u32,
|
||||||
|
pub has_more: bool,
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use domain::{errors::DomainError, events::DomainEvent, ports::EventPublisher};
|
|||||||
use event_transport::EventPublisherAdapter;
|
use event_transport::EventPublisherAdapter;
|
||||||
use nats::NatsTransport;
|
use nats::NatsTransport;
|
||||||
use postgres::activitypub::PgActivityPubRepository;
|
use postgres::activitypub::PgActivityPubRepository;
|
||||||
|
use postgres::remote_actor_connections::PgRemoteActorConnectionRepository;
|
||||||
use postgres_federation::{PostgresApUserRepository, PostgresFederationRepository};
|
use postgres_federation::{PostgresApUserRepository, PostgresFederationRepository};
|
||||||
use presentation::state::AppState;
|
use presentation::state::AppState;
|
||||||
|
|
||||||
@@ -111,6 +112,7 @@ pub async fn build(cfg: &Config) -> Infrastructure {
|
|||||||
events: event_publisher,
|
events: event_publisher,
|
||||||
federation: ap_service.clone() as Arc<dyn domain::ports::FederationActionPort>,
|
federation: ap_service.clone() as Arc<dyn domain::ports::FederationActionPort>,
|
||||||
ap_repo: Arc::new(PgActivityPubRepository::new(pool.clone())),
|
ap_repo: Arc::new(PgActivityPubRepository::new(pool.clone())),
|
||||||
|
remote_actor_connections: Arc::new(PgRemoteActorConnectionRepository::new(pool.clone())),
|
||||||
};
|
};
|
||||||
|
|
||||||
Infrastructure { state, ap_service }
|
Infrastructure { state, ap_service }
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use crate::{
|
|||||||
errors::ApiError, extractors::OptionalAuthUser, handlers::feed::to_thought_response,
|
errors::ApiError, extractors::OptionalAuthUser, handlers::feed::to_thought_response,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
use api_types::requests::PaginationQuery;
|
use api_types::{
|
||||||
|
requests::PaginationQuery,
|
||||||
|
responses::{ActorConnectionPageResponse, ActorConnectionResponse},
|
||||||
|
};
|
||||||
use application::use_cases::feed::get_user_feed;
|
use application::use_cases::feed::get_user_feed;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
@@ -71,6 +74,85 @@ pub async fn remote_actor_posts_handler(
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CACHE_TTL_SECS: i64 = 3600;
|
||||||
|
|
||||||
|
pub async fn actor_followers_handler(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(handle): Path<String>,
|
||||||
|
Query(q): Query<PaginationQuery>,
|
||||||
|
) -> Result<Json<ActorConnectionPageResponse>, ApiError> {
|
||||||
|
actor_connections_handler(s, handle, "followers", q.page() as u32).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn actor_following_handler(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(handle): Path<String>,
|
||||||
|
Query(q): Query<PaginationQuery>,
|
||||||
|
) -> Result<Json<ActorConnectionPageResponse>, ApiError> {
|
||||||
|
actor_connections_handler(s, handle, "following", q.page() as u32).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn actor_connections_handler(
|
||||||
|
s: AppState,
|
||||||
|
handle: String,
|
||||||
|
connection_type: &str,
|
||||||
|
page: u32,
|
||||||
|
) -> Result<Json<ActorConnectionPageResponse>, ApiError> {
|
||||||
|
const PAGE_SIZE: usize = 20;
|
||||||
|
|
||||||
|
let actor = s.federation.lookup_actor(&handle).await?;
|
||||||
|
|
||||||
|
let collection_url = match connection_type {
|
||||||
|
"followers" => actor
|
||||||
|
.followers_url
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("actor has no followers URL".into()))?,
|
||||||
|
_ => actor
|
||||||
|
.following_url
|
||||||
|
.ok_or_else(|| ApiError::BadRequest("actor has no following URL".into()))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let items = s
|
||||||
|
.remote_actor_connections
|
||||||
|
.list_connections(&actor.url, connection_type, page)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let stale = match s
|
||||||
|
.remote_actor_connections
|
||||||
|
.connection_page_age(&actor.url, connection_type, page)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
None => true,
|
||||||
|
Some(age) => chrono::Utc::now().signed_duration_since(age).num_seconds() > CACHE_TTL_SECS,
|
||||||
|
};
|
||||||
|
|
||||||
|
if stale {
|
||||||
|
let _ = s
|
||||||
|
.events
|
||||||
|
.publish(&DomainEvent::FetchActorConnections {
|
||||||
|
actor_ap_url: actor.url.clone(),
|
||||||
|
collection_url,
|
||||||
|
connection_type: connection_type.to_string(),
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_more = items.len() >= PAGE_SIZE;
|
||||||
|
Ok(Json(ActorConnectionPageResponse {
|
||||||
|
items: items
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| ActorConnectionResponse {
|
||||||
|
handle: a.handle,
|
||||||
|
display_name: a.display_name,
|
||||||
|
avatar_url: a.avatar_url,
|
||||||
|
url: a.url,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
page,
|
||||||
|
has_more,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -127,6 +209,7 @@ mod tests {
|
|||||||
events: store.clone(),
|
events: store.clone(),
|
||||||
federation: store.clone(),
|
federation: store.clone(),
|
||||||
ap_repo: store.clone(),
|
ap_repo: store.clone(),
|
||||||
|
remote_actor_connections: store.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,13 +113,15 @@ mod tests {
|
|||||||
hasher: Arc::new(NoOpHasher),
|
hasher: Arc::new(NoOpHasher),
|
||||||
events: store.clone(),
|
events: store.clone(),
|
||||||
federation: store.clone(),
|
federation: store.clone(),
|
||||||
|
ap_repo: store.clone(),
|
||||||
|
remote_actor_connections: store.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app() -> Router {
|
fn app() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/notifications", patch(mark_all_read))
|
.route("/notifications", patch(mark_all_read))
|
||||||
.route("/notifications/:id", patch(mark_notification_read))
|
.route("/notifications/{id}", patch(mark_notification_read))
|
||||||
.with_state(make_state())
|
.with_state(make_state())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ mod tests {
|
|||||||
hasher: Arc::new(NoOpHasher),
|
hasher: Arc::new(NoOpHasher),
|
||||||
events: store.clone(),
|
events: store.clone(),
|
||||||
federation: store.clone(),
|
federation: store.clone(),
|
||||||
|
ap_repo: store.clone(),
|
||||||
|
remote_actor_connections: store.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ mod tests {
|
|||||||
events: store.clone(),
|
events: store.clone(),
|
||||||
federation: store.clone(),
|
federation: store.clone(),
|
||||||
ap_repo: store.clone(),
|
ap_repo: store.clone(),
|
||||||
|
remote_actor_connections: store.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ pub fn router() -> Router<AppState> {
|
|||||||
"/federation/actors/{handle}/posts",
|
"/federation/actors/{handle}/posts",
|
||||||
get(federation_actors::remote_actor_posts_handler),
|
get(federation_actors::remote_actor_posts_handler),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/federation/actors/{handle}/followers-list",
|
||||||
|
get(federation_actors::actor_followers_handler),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/federation/actors/{handle}/following-list",
|
||||||
|
get(federation_actors::actor_following_handler),
|
||||||
|
)
|
||||||
.route("/tags/popular", get(feed::get_popular_tags))
|
.route("/tags/popular", get(feed::get_popular_tags))
|
||||||
.route("/tags/{name}", get(feed::tag_thoughts_handler))
|
.route("/tags/{name}", get(feed::tag_thoughts_handler))
|
||||||
// notifications
|
// notifications
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ pub struct AppState {
|
|||||||
pub events: Arc<dyn EventPublisher>,
|
pub events: Arc<dyn EventPublisher>,
|
||||||
pub federation: Arc<dyn FederationActionPort>,
|
pub federation: Arc<dyn FederationActionPort>,
|
||||||
pub ap_repo: Arc<dyn ActivityPubRepository>,
|
pub ap_repo: Arc<dyn ActivityPubRepository>,
|
||||||
|
pub remote_actor_connections: Arc<dyn RemoteActorConnectionRepository>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user