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
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:
@@ -1,8 +1,20 @@
|
||||
use domain::{
|
||||
errors::DomainError, models::remote_actor::RemoteActor, ports::FederationActionPort,
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::{
|
||||
actor_connection_summary::ActorConnectionSummary,
|
||||
feed::{FeedEntry, PageParams, Paginated},
|
||||
remote_actor::RemoteActor,
|
||||
},
|
||||
ports::{
|
||||
ActivityPubRepository, EventPublisher, FederationActionPort, FeedRepository,
|
||||
FollowRepository, RemoteActorConnectionRepository, UserRepository,
|
||||
},
|
||||
value_objects::UserId,
|
||||
};
|
||||
|
||||
use super::social;
|
||||
|
||||
pub async fn list_pending_requests(
|
||||
federation: &dyn FederationActionPort,
|
||||
user_id: &UserId,
|
||||
@@ -48,6 +60,87 @@ pub async fn list_remote_following(
|
||||
federation.get_remote_following(user_id).await
|
||||
}
|
||||
|
||||
pub async fn remove_remote_following(
|
||||
follows: &dyn FollowRepository,
|
||||
users: &dyn UserRepository,
|
||||
federation: &dyn FederationActionPort,
|
||||
events: &dyn EventPublisher,
|
||||
user_id: &UserId,
|
||||
handle: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
social::unfollow_actor(follows, users, federation, events, user_id, handle).await
|
||||
}
|
||||
|
||||
pub async fn get_remote_actor_posts(
|
||||
federation: &dyn FederationActionPort,
|
||||
ap_repo: &dyn ActivityPubRepository,
|
||||
feed: &dyn FeedRepository,
|
||||
events: &dyn EventPublisher,
|
||||
handle: &str,
|
||||
page: PageParams,
|
||||
viewer_id: Option<&UserId>,
|
||||
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||
let actor = federation.lookup_actor(handle).await?;
|
||||
let ap_url = url::Url::parse(&actor.url).map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
let author_id = match ap_repo.find_remote_actor_id(&ap_url).await? {
|
||||
Some(id) => id,
|
||||
None => ap_repo.intern_remote_actor(&ap_url).await?,
|
||||
};
|
||||
let result = feed.user_feed(&author_id, &page, viewer_id).await?;
|
||||
if let Some(outbox_url) = actor.outbox_url {
|
||||
let _ = events
|
||||
.publish(&DomainEvent::FetchRemoteActorPosts {
|
||||
actor_ap_url: actor.url,
|
||||
outbox_url,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
const ACTOR_CONNECTIONS_CACHE_TTL_SECS: i64 = 3600;
|
||||
|
||||
pub async fn get_actor_connections_page(
|
||||
federation: &dyn FederationActionPort,
|
||||
connections: &dyn RemoteActorConnectionRepository,
|
||||
events: &dyn EventPublisher,
|
||||
handle: &str,
|
||||
connection_type: &str,
|
||||
page: u32,
|
||||
) -> Result<(Vec<ActorConnectionSummary>, bool), DomainError> {
|
||||
const PAGE_SIZE: usize = 20;
|
||||
let actor = federation.lookup_actor(handle).await?;
|
||||
let collection_url = match connection_type {
|
||||
"followers" => actor.followers_url.ok_or(DomainError::NotFound)?,
|
||||
_ => actor.following_url.ok_or(DomainError::NotFound)?,
|
||||
};
|
||||
let items = connections
|
||||
.list_connections(&actor.url, connection_type, page)
|
||||
.await?;
|
||||
let stale = match connections
|
||||
.connection_page_age(&actor.url, connection_type, page)
|
||||
.await?
|
||||
{
|
||||
None => true,
|
||||
Some(age) => {
|
||||
chrono::Utc::now().signed_duration_since(age).num_seconds()
|
||||
> ACTOR_CONNECTIONS_CACHE_TTL_SECS
|
||||
}
|
||||
};
|
||||
if stale {
|
||||
let _ = events
|
||||
.publish(&DomainEvent::FetchActorConnections {
|
||||
actor_ap_url: actor.url,
|
||||
collection_url,
|
||||
connection_type: connection_type.to_string(),
|
||||
page,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
let has_more = items.len() >= PAGE_SIZE;
|
||||
Ok((items, has_more))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -74,6 +74,26 @@ pub async fn list_users(users: &dyn UserRepository) -> Result<Vec<UserSummary>,
|
||||
users.list_with_stats().await
|
||||
}
|
||||
|
||||
pub async fn list_users_paginated(
|
||||
users: &dyn UserRepository,
|
||||
page: PageParams,
|
||||
) -> Result<Paginated<UserSummary>, DomainError> {
|
||||
let all = users.list_with_stats().await?;
|
||||
let total = all.len() as i64;
|
||||
let start = ((page.page.saturating_sub(1)) * page.per_page) as usize;
|
||||
let items: Vec<UserSummary> = all
|
||||
.into_iter()
|
||||
.skip(start)
|
||||
.take(page.per_page as usize)
|
||||
.collect();
|
||||
Ok(Paginated {
|
||||
items,
|
||||
total,
|
||||
page: page.page,
|
||||
per_page: page.per_page,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_popular_tags(
|
||||
tags: &dyn TagRepository,
|
||||
limit: usize,
|
||||
|
||||
@@ -14,17 +14,34 @@ pub async fn list_notifications(
|
||||
repo.list_for_user(user_id, &page).await
|
||||
}
|
||||
|
||||
pub async fn count_unread_notifications(
|
||||
repo: &dyn NotificationRepository,
|
||||
user_id: &UserId,
|
||||
) -> Result<u64, DomainError> {
|
||||
repo.count_unread(user_id).await
|
||||
}
|
||||
|
||||
pub async fn mark_notification_read(
|
||||
repo: &dyn NotificationRepository,
|
||||
id: &NotificationId,
|
||||
user_id: &UserId,
|
||||
is_read: bool,
|
||||
) -> Result<(), DomainError> {
|
||||
repo.mark_read(id, user_id).await
|
||||
if is_read {
|
||||
repo.mark_read(id, user_id).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mark_all_notifications_read(
|
||||
repo: &dyn NotificationRepository,
|
||||
user_id: &UserId,
|
||||
is_read: bool,
|
||||
) -> Result<(), DomainError> {
|
||||
repo.mark_all_read(user_id).await
|
||||
if is_read {
|
||||
repo.mark_all_read(user_id).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,36 @@ pub async fn reject_follow(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn block_by_username(
|
||||
blocks: &dyn BlockRepository,
|
||||
users: &dyn UserRepository,
|
||||
events: &dyn EventPublisher,
|
||||
blocker_id: &UserId,
|
||||
username: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let uname = Username::new(username).map_err(|_| DomainError::NotFound)?;
|
||||
let target = users
|
||||
.find_by_username(&uname)
|
||||
.await?
|
||||
.ok_or(DomainError::NotFound)?;
|
||||
block_user(blocks, events, blocker_id, &target.id).await
|
||||
}
|
||||
|
||||
pub async fn unblock_by_username(
|
||||
blocks: &dyn BlockRepository,
|
||||
users: &dyn UserRepository,
|
||||
events: &dyn EventPublisher,
|
||||
blocker_id: &UserId,
|
||||
username: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let uname = Username::new(username).map_err(|_| DomainError::NotFound)?;
|
||||
let target = users
|
||||
.find_by_username(&uname)
|
||||
.await?
|
||||
.ok_or(DomainError::NotFound)?;
|
||||
unblock_user(blocks, events, blocker_id, &target.id).await
|
||||
}
|
||||
|
||||
pub async fn block_user(
|
||||
blocks: &dyn BlockRepository,
|
||||
events: &dyn EventPublisher,
|
||||
|
||||
Reference in New Issue
Block a user