feat: implement unread notification count and enhance user listing with pagination
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m33s
test / unit (pull_request) Successful in 16m24s
test / integration (pull_request) Failing after 16m52s

This commit is contained in:
2026-05-15 12:04:00 +02:00
parent 5f61a71336
commit 6273635aeb
15 changed files with 253 additions and 154 deletions

View File

@@ -6,12 +6,14 @@ use api_types::{
requests::PaginationQuery,
responses::{ActorConnectionPageResponse, ActorConnectionResponse},
};
use application::use_cases::feed::get_user_feed;
use application::use_cases::federation_management::{
get_actor_connections_page, get_remote_actor_posts,
};
use axum::{
extract::{Path, Query, State},
Json,
};
use domain::{events::DomainEvent, models::feed::PageParams};
use domain::models::feed::PageParams;
pub async fn remote_actor_posts_handler(
State(s): State<AppState>,
@@ -19,53 +21,20 @@ pub async fn remote_actor_posts_handler(
Query(q): Query<PaginationQuery>,
OptionalAuthUser(viewer): OptionalAuthUser,
) -> Result<Json<serde_json::Value>, ApiError> {
tracing::info!(%handle, "remote_actor_posts: looking up actor");
let actor = s.federation.lookup_actor(&handle).await?;
tracing::info!(actor_url = %actor.url, has_outbox = actor.outbox_url.is_some(), "remote_actor_posts: actor found");
let ap_url = url::Url::parse(&actor.url).map_err(|e| ApiError::BadRequest(e.to_string()))?;
let author_id = match s.ap_repo.find_remote_actor_id(&ap_url).await? {
Some(id) => {
tracing::info!(?id, "remote_actor_posts: actor already interned");
id
}
None => {
tracing::info!("remote_actor_posts: interning actor");
let id = s.ap_repo.intern_remote_actor(&ap_url).await?;
tracing::info!(?id, "remote_actor_posts: actor interned");
id
}
};
let page = PageParams {
page: q.page(),
per_page: q.per_page(),
};
let result = get_user_feed(&*s.feed, &author_id, page, viewer.as_ref()).await?;
tracing::info!(
post_count = result.items.len(),
"remote_actor_posts: cached posts fetched"
);
match &actor.outbox_url {
Some(outbox_url) => {
tracing::info!(%outbox_url, "remote_actor_posts: publishing FetchRemoteActorPosts");
match s
.events
.publish(&DomainEvent::FetchRemoteActorPosts {
actor_ap_url: actor.url.clone(),
outbox_url: outbox_url.clone(),
})
.await
{
Ok(_) => tracing::info!("remote_actor_posts: event published"),
Err(e) => tracing::warn!("remote_actor_posts: event publish failed: {e}"),
}
}
None => tracing::warn!("remote_actor_posts: actor has no outbox_url, skipping fetch"),
}
let result = get_remote_actor_posts(
&*s.federation,
&*s.ap_repo,
&*s.feed,
&*s.events,
&handle,
page,
viewer.as_ref(),
)
.await?;
Ok(Json(serde_json::json!({
"total": result.total,
"page": result.page,
@@ -74,8 +43,6 @@ 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>,
@@ -98,46 +65,15 @@ async fn actor_connections_handler(
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;
let (items, has_more) = get_actor_connections_page(
&*s.federation,
&*s.remote_actor_connections,
&*s.events,
&handle,
connection_type,
page,
)
.await?;
Ok(Json(ActorConnectionPageResponse {
items: items
.into_iter()