use crate::{ errors::DomainError, events::{DomainEvent, EventEnvelope}, models::{ api_key::ApiKey, feed::{FeedEntry, PageParams, Paginated, UserSummary}, notification::Notification, remote_actor::RemoteActor, social::{Block, Boost, Follow, FollowState, Like}, tag::Tag, thought::Thought, top_friend::TopFriend, user::User, }, value_objects::{ ApiKeyId, Content, Email, NotificationId, PasswordHash, ThoughtId, UserId, Username, }, }; use async_trait::async_trait; pub struct GeneratedToken { pub token: String, pub user_id: UserId, } #[async_trait] pub trait AuthService: Send + Sync { fn generate_token(&self, user_id: &UserId) -> Result; fn validate_token(&self, token: &str) -> Result; } #[async_trait] pub trait PasswordHasher: Send + Sync { async fn hash(&self, plain: &str) -> Result; async fn verify(&self, plain: &str, hash: &PasswordHash) -> Result; } #[async_trait] pub trait EventPublisher: Send + Sync { async fn publish(&self, event: &DomainEvent) -> Result<(), DomainError>; } pub trait EventConsumer: Send + Sync { fn consume(&self) -> futures::stream::BoxStream<'_, Result>; } #[async_trait] pub trait OutboxWriter: Send + Sync { async fn append(&self, event: &DomainEvent) -> Result<(), DomainError>; } #[async_trait] pub trait UserReader: Send + Sync { async fn find_by_id(&self, id: &UserId) -> Result, DomainError>; async fn find_by_username(&self, username: &Username) -> Result, DomainError>; async fn find_by_email(&self, email: &Email) -> Result, DomainError>; async fn list_with_stats(&self) -> Result, DomainError>; async fn count(&self) -> Result; } #[async_trait] pub trait UserWriter: Send + Sync { async fn save(&self, user: &User) -> Result<(), DomainError>; async fn update_profile( &self, user_id: &UserId, display_name: Option, bio: Option, avatar_url: Option, header_url: Option, custom_css: Option, ) -> Result<(), DomainError>; } /// Combined supertrait — `AppState.users` stays `Arc`. /// Blanket impl: any type implementing both sub-traits gets `UserRepository` for free. pub trait UserRepository: UserReader + UserWriter {} impl UserRepository for T {} #[async_trait] pub trait ThoughtRepository: Send + Sync { async fn save(&self, thought: &Thought) -> Result<(), DomainError>; async fn find_by_id(&self, id: &ThoughtId) -> Result, DomainError>; async fn delete(&self, id: &ThoughtId, user_id: &UserId) -> Result<(), DomainError>; async fn update_content(&self, id: &ThoughtId, content: &Content) -> Result<(), DomainError>; async fn get_thread(&self, id: &ThoughtId) -> Result, DomainError>; async fn list_by_user( &self, user_id: &UserId, page: &PageParams, ) -> Result, DomainError>; } #[async_trait] pub trait LikeRepository: Send + Sync { async fn save(&self, like: &Like) -> Result<(), DomainError>; async fn delete(&self, user_id: &UserId, thought_id: &ThoughtId) -> Result<(), DomainError>; async fn find( &self, user_id: &UserId, thought_id: &ThoughtId, ) -> Result, DomainError>; async fn count_for_thought(&self, thought_id: &ThoughtId) -> Result; } #[async_trait] pub trait BoostRepository: Send + Sync { async fn save(&self, boost: &Boost) -> Result<(), DomainError>; async fn delete(&self, user_id: &UserId, thought_id: &ThoughtId) -> Result<(), DomainError>; async fn find( &self, user_id: &UserId, thought_id: &ThoughtId, ) -> Result, DomainError>; async fn count_for_thought(&self, thought_id: &ThoughtId) -> Result; } #[async_trait] pub trait FollowRepository: Send + Sync { async fn save(&self, follow: &Follow) -> Result<(), DomainError>; async fn delete(&self, follower_id: &UserId, following_id: &UserId) -> Result<(), DomainError>; async fn find( &self, follower_id: &UserId, following_id: &UserId, ) -> Result, DomainError>; async fn update_state( &self, follower_id: &UserId, following_id: &UserId, state: &FollowState, ) -> Result<(), DomainError>; async fn list_followers( &self, user_id: &UserId, page: &PageParams, ) -> Result, DomainError>; async fn list_following( &self, user_id: &UserId, page: &PageParams, ) -> Result, DomainError>; async fn get_accepted_following_ids( &self, user_id: &UserId, ) -> Result, DomainError>; } #[async_trait] pub trait BlockRepository: Send + Sync { async fn save(&self, block: &Block) -> Result<(), DomainError>; async fn delete(&self, blocker_id: &UserId, blocked_id: &UserId) -> Result<(), DomainError>; async fn exists(&self, blocker_id: &UserId, blocked_id: &UserId) -> Result; } #[async_trait] pub trait TagRepository: Send + Sync { async fn find_or_create(&self, name: &str) -> Result; async fn attach_to_thought( &self, thought_id: &ThoughtId, tag_id: i32, ) -> Result<(), DomainError>; async fn detach_from_thought(&self, thought_id: &ThoughtId) -> Result<(), DomainError>; async fn list_for_thought(&self, thought_id: &ThoughtId) -> Result, DomainError>; async fn list_thoughts_by_tag( &self, tag_name: &str, page: &PageParams, ) -> Result, DomainError>; /// Returns (tag_name, thought_count) pairs ordered by usage, most popular first. async fn popular_tags(&self, limit: usize) -> Result, DomainError>; } #[async_trait] pub trait ApiKeyRepository: Send + Sync { async fn save(&self, key: &ApiKey) -> Result<(), DomainError>; async fn find_by_hash(&self, key_hash: &str) -> Result, DomainError>; async fn list_for_user(&self, user_id: &UserId) -> Result, DomainError>; async fn delete(&self, id: &ApiKeyId, user_id: &UserId) -> Result<(), DomainError>; } #[async_trait] pub trait TopFriendRepository: Send + Sync { async fn set_top_friends( &self, user_id: &UserId, friends: Vec<(UserId, i16)>, ) -> Result<(), DomainError>; async fn list_for_user(&self, user_id: &UserId) -> Result, DomainError>; } #[async_trait] pub trait NotificationRepository: Send + Sync { async fn save(&self, n: &Notification) -> Result<(), DomainError>; async fn list_for_user( &self, user_id: &UserId, page: &PageParams, ) -> Result, DomainError>; async fn count_unread(&self, user_id: &UserId) -> Result; async fn mark_read(&self, id: &NotificationId, user_id: &UserId) -> Result<(), DomainError>; async fn mark_all_read(&self, user_id: &UserId) -> Result<(), DomainError>; } #[async_trait] pub trait RemoteActorRepository: Send + Sync { async fn upsert(&self, actor: &RemoteActor) -> Result<(), DomainError>; async fn find_by_url(&self, url: &str) -> Result, DomainError>; } #[async_trait] pub trait RemoteActorConnectionRepository: Send + Sync { async fn upsert_connections( &self, actor_url: &str, connection_type: &str, page: u32, actors: &[crate::models::actor_connection_summary::ActorConnectionSummary], ) -> Result<(), DomainError>; async fn list_connections( &self, actor_url: &str, connection_type: &str, page: u32, ) -> Result, DomainError>; async fn connection_page_age( &self, actor_url: &str, connection_type: &str, page: u32, ) -> Result>, DomainError>; } #[async_trait] pub trait FederationLookupPort: Send + Sync { async fn lookup_actor(&self, handle: &str) -> Result; async fn actor_json(&self, user_id: &UserId) -> Result; async fn followers_collection_json( &self, user_id: &UserId, page: Option, ) -> Result; async fn following_collection_json( &self, user_id: &UserId, page: Option, ) -> Result; } #[async_trait] pub trait FederationFollowPort: Send + Sync { async fn follow_remote(&self, local_user_id: &UserId, handle: &str) -> Result<(), DomainError>; async fn unfollow_remote( &self, local_user_id: &UserId, handle: &str, ) -> Result<(), DomainError>; async fn get_remote_following(&self, user_id: &UserId) -> Result, DomainError>; } #[async_trait] pub trait FederationFollowRequestPort: Send + Sync { async fn get_pending_followers( &self, user_id: &UserId, ) -> Result, DomainError>; async fn accept_follow_request( &self, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError>; async fn reject_follow_request( &self, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError>; async fn get_remote_followers(&self, user_id: &UserId) -> Result, DomainError>; async fn remove_remote_follower( &self, user_id: &UserId, actor_url: &str, ) -> Result<(), DomainError>; } #[async_trait] pub trait FederationFetchPort: Send + Sync { async fn fetch_outbox_page( &self, outbox_url: &str, page: u32, ) -> Result, DomainError>; async fn fetch_actor_urls_from_collection( &self, collection_url: &str, ) -> Result, DomainError>; async fn resolve_actor_profiles( &self, urls: Vec, ) -> Vec; } pub trait FederationActionPort: FederationLookupPort + FederationFollowPort + FederationFollowRequestPort + FederationFetchPort { } impl< T: FederationLookupPort + FederationFollowPort + FederationFollowRequestPort + FederationFetchPort, > FederationActionPort for T { } #[async_trait] pub trait FeedRepository: Send + Sync { async fn home_feed( &self, following_ids: &[UserId], page: &PageParams, viewer_id: Option<&UserId>, ) -> Result, DomainError>; async fn public_feed( &self, page: &PageParams, viewer_id: Option<&UserId>, ) -> Result, DomainError>; async fn search( &self, query: &str, page: &PageParams, viewer_id: Option<&UserId>, ) -> Result, DomainError>; async fn tag_feed( &self, tag_name: &str, page: &PageParams, viewer_id: Option<&UserId>, ) -> Result, DomainError>; async fn user_feed( &self, user_id: &UserId, page: &PageParams, viewer_id: Option<&UserId>, ) -> Result, DomainError>; } #[async_trait] pub trait SearchPort: Send + Sync { /// Full-text search over public thoughts, ranked by trigram similarity. async fn search_thoughts( &self, query: &str, page: &PageParams, viewer_id: Option<&UserId>, ) -> Result, DomainError>; /// Search users by username or display_name, ranked by trigram similarity. async fn search_users( &self, query: &str, page: &PageParams, ) -> Result, DomainError>; } /// AP-protocol endpoints for a locally-stored user (local or interned remote). #[derive(Debug, Clone)] pub struct ActorApUrls { pub ap_id: String, pub inbox_url: String, } /// A local thought ready for AP serialization, with the author's username /// pre-joined so the handler can build AP URLs without a second query. #[derive(Debug, Clone)] pub struct OutboxEntry { pub thought: crate::models::thought::Thought, pub author_username: Username, } #[async_trait] pub trait ActivityPubRepository: Send + Sync { // ── Outbox (local → remote) ────────────────────────────────────── /// All public local thoughts for this actor. Used for outbox totals /// and full-collection delivery. async fn outbox_entries_for_actor( &self, user_id: &UserId, ) -> Result, DomainError>; /// Cursor page of public local thoughts, newest-first, before `before`. /// Used for OrderedCollectionPage responses. async fn outbox_page_for_actor( &self, user_id: &UserId, before: Option>, limit: usize, ) -> Result, DomainError>; // ── Remote actor resolution ────────────────────────────────────── /// Find the local UserId for a remote actor by its AP URL. async fn find_remote_actor_id(&self, actor_ap_url: &str) -> Result, DomainError>; /// Ensure a remote actor placeholder exists; create one if absent. /// Idempotent — safe to call multiple times with the same URL. async fn intern_remote_actor(&self, actor_ap_url: &str) -> Result; /// Update display_name and avatar_url for an already-interned remote actor. async fn update_remote_actor_display( &self, user_id: &UserId, display_name: Option<&str>, avatar_url: Option<&str>, ) -> Result<(), DomainError>; // ── Inbox processing (remote → local) ─────────────────────────── /// Persist an incoming remote Note. Idempotent on ap_id. #[allow(clippy::too_many_arguments)] async fn accept_note( &self, ap_id: &str, author_id: &UserId, content: &str, published: chrono::DateTime, sensitive: bool, content_warning: Option, visibility: &str, in_reply_to: Option<&str>, ) -> Result<(), DomainError>; /// Apply an Update to a previously accepted remote Note. async fn apply_note_update(&self, ap_id: &str, new_content: &str) -> Result<(), DomainError>; /// Remove a specific remote Note (Delete activity). Only touches /// remotely-originated thoughts. async fn retract_note(&self, ap_id: &str) -> Result<(), DomainError>; /// Remove all Notes from a remote actor (actor-level Delete/Tombstone). async fn retract_actor_notes(&self, actor_ap_url: &str) -> Result<(), DomainError>; // ── Node-level stats ───────────────────────────────────────────── /// Total locally-authored thought count for NodeInfo responses. async fn count_local_notes(&self) -> Result; /// Return the ActivityPub object URL for a thought, if one is stored. /// Returns None for local thoughts (caller constructs URL from base_url + thought_id). async fn get_thought_ap_id( &self, thought_id: &ThoughtId, ) -> Result, DomainError>; /// Return the AP actor URL and inbox URL for a user, if stored. /// Returns None for users that have not been federated. async fn get_actor_ap_urls(&self, user_id: &UserId) -> Result, DomainError>; } #[async_trait] pub trait OutboundFederationPort: Send + Sync { /// Fan out a new local Note to all accepted followers. async fn broadcast_create( &self, author_user_id: &UserId, thought: &Thought, author_username: &str, in_reply_to_url: Option<&str>, ) -> Result<(), DomainError>; /// Fan out a Delete tombstone for a now-deleted local Note. /// `thought_ap_id` is pre-constructed by the caller because the thought /// has already been deleted from the DB when this fires. async fn broadcast_delete( &self, author_user_id: &UserId, thought_ap_id: &str, ) -> Result<(), DomainError>; /// Fan out an Update(Note) for an edited local thought. async fn broadcast_update( &self, author_user_id: &UserId, thought: &Thought, author_username: &str, in_reply_to_url: Option<&str>, ) -> Result<(), DomainError>; /// Fan out an Announce(object_ap_id) for a boost. async fn broadcast_announce( &self, booster_user_id: &UserId, object_ap_id: &str, ) -> Result<(), DomainError>; /// Fan out an Undo(Announce) to followers when a boost is removed. async fn broadcast_undo_announce( &self, booster_user_id: &UserId, object_ap_id: &str, ) -> Result<(), DomainError>; /// Send a Like activity to a remote thought author's inbox. /// Only called when a LOCAL user likes a REMOTE thought (one with an ap_id). async fn broadcast_like( &self, liker_user_id: &UserId, object_ap_id: &str, author_inbox_url: &str, ) -> Result<(), DomainError>; /// Send Undo(Like) to a remote thought author's inbox. async fn broadcast_undo_like( &self, liker_user_id: &UserId, object_ap_id: &str, author_inbox_url: &str, ) -> Result<(), DomainError>; /// Fan out an Update(Actor) to all accepted followers after a profile change. async fn broadcast_actor_update(&self, user_id: &UserId) -> Result<(), DomainError>; } #[async_trait] pub trait FederationSchedulerPort: Send + Sync { async fn schedule_actor_posts_fetch( &self, actor_ap_url: &str, outbox_url: &str, ) -> Result<(), DomainError>; async fn schedule_connections_fetch( &self, actor_ap_url: &str, collection_url: &str, connection_type: &str, page: u32, ) -> Result<(), DomainError>; }