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 UserRepository: 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 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>; async fn list_with_stats(&self) -> Result, DomainError>; async fn count(&self) -> Result; } #[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 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 FederationActionPort: Send + Sync { async fn lookup_actor(&self, handle: &str) -> Result; async fn follow_remote(&self, local_user_id: &UserId, handle: &str) -> Result<(), DomainError>; 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 fn fetch_outbox_page( &self, outbox_url: &str, page: u32, ) -> Result, DomainError>; } #[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>; } /// 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: &url::Url, ) -> 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: &url::Url) -> Result; // ── 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: &url::Url, author_id: &UserId, content: &str, published: chrono::DateTime, sensitive: bool, content_warning: Option, visibility: &str, ) -> Result<(), DomainError>; /// Apply an Update to a previously accepted remote Note. async fn apply_note_update( &self, ap_id: &url::Url, new_content: &str, ) -> Result<(), DomainError>; /// Remove a specific remote Note (Delete activity). Only touches /// remotely-originated thoughts. async fn retract_note(&self, ap_id: &url::Url) -> Result<(), DomainError>; /// Remove all Notes from a remote actor (actor-level Delete/Tombstone). async fn retract_actor_notes(&self, actor_ap_url: &url::Url) -> Result<(), DomainError>; // ── Node-level stats ───────────────────────────────────────────── /// Total locally-authored thought count for NodeInfo responses. async fn count_local_notes(&self) -> Result; } #[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, ) -> 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, ) -> 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>; }