Files
thoughts/crates/application/src/use_cases/federation_management.rs

191 lines
5.8 KiB
Rust

use domain::{
errors::DomainError,
models::{
actor_connection_summary::ActorConnectionSummary,
feed::{FeedEntry, PageParams, Paginated},
remote_actor::RemoteActor,
},
ports::{
ActivityPubRepository, EventPublisher, FederationActionPort, FederationSchedulerPort,
FeedRepository, FollowRepository, RemoteActorConnectionRepository, UserRepository,
},
value_objects::UserId,
};
use super::social;
pub async fn list_pending_requests(
federation: &dyn FederationActionPort,
user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
federation.get_pending_followers(user_id).await
}
pub async fn accept_follow_request(
federation: &dyn FederationActionPort,
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError> {
federation.accept_follow_request(user_id, actor_url).await
}
pub async fn reject_follow_request(
federation: &dyn FederationActionPort,
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError> {
federation.reject_follow_request(user_id, actor_url).await
}
pub async fn list_remote_followers(
federation: &dyn FederationActionPort,
user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
federation.get_remote_followers(user_id).await
}
pub async fn remove_remote_follower(
federation: &dyn FederationActionPort,
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError> {
federation.remove_remote_follower(user_id, actor_url).await
}
pub async fn list_remote_following(
federation: &dyn FederationActionPort,
user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
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,
scheduler: &dyn FederationSchedulerPort,
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 _ = scheduler
.schedule_actor_posts_fetch(&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,
scheduler: &dyn FederationSchedulerPort,
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 _ = scheduler
.schedule_connections_fetch(&actor.url, &collection_url, connection_type, page)
.await;
}
let has_more = items.len() >= PAGE_SIZE;
Ok((items, has_more))
}
#[cfg(test)]
mod tests {
use super::*;
use domain::testing::TestStore;
#[tokio::test]
async fn list_pending_returns_empty_by_default() {
let store = TestStore::default();
let uid = UserId::new();
let result = list_pending_requests(&store, &uid).await.unwrap();
assert!(result.is_empty());
}
#[tokio::test]
async fn accept_follow_request_returns_ok() {
let store = TestStore::default();
let uid = UserId::new();
accept_follow_request(&store, &uid, "https://mastodon.social/users/alice")
.await
.unwrap();
}
#[tokio::test]
async fn reject_follow_request_returns_ok() {
let store = TestStore::default();
let uid = UserId::new();
reject_follow_request(&store, &uid, "https://mastodon.social/users/alice")
.await
.unwrap();
}
#[tokio::test]
async fn list_remote_followers_returns_empty_by_default() {
let store = TestStore::default();
let uid = UserId::new();
let result = list_remote_followers(&store, &uid).await.unwrap();
assert!(result.is_empty());
}
#[tokio::test]
async fn remove_remote_follower_returns_ok() {
let store = TestStore::default();
let uid = UserId::new();
remove_remote_follower(&store, &uid, "https://mastodon.social/users/alice")
.await
.unwrap();
}
#[tokio::test]
async fn list_remote_following_returns_empty_by_default() {
let store = TestStore::default();
let uid = UserId::new();
let result = list_remote_following(&store, &uid).await.unwrap();
assert!(result.is_empty());
}
}