use anyhow::{anyhow, Result}; use async_trait::async_trait; use chrono::{DateTime, Utc}; use std::sync::Arc; use url::Url; use crate::note::ThoughtNote; use crate::urls::ThoughtsUrls; use activitypub_base::ApObjectHandler; use domain::ports::ActivityPubRepository; use domain::value_objects::UserId; pub struct ThoughtsObjectHandler { repo: Arc, urls: ThoughtsUrls, } impl ThoughtsObjectHandler { pub fn new(repo: Arc, base_url: &str) -> Self { Self { repo, urls: ThoughtsUrls::new(base_url), } } } #[async_trait] impl ApObjectHandler for ThoughtsObjectHandler { async fn get_local_objects_for_user( &self, user_id: uuid::Uuid, ) -> Result> { let uid = UserId::from_uuid(user_id); let entries = self .repo .outbox_entries_for_actor(&uid) .await .map_err(|e| anyhow!("{e}"))?; entries .into_iter() .map(|e| { let note_url = self.urls.thought_url(e.thought.id.as_uuid()); let actor_url = self.urls.user_url(e.author_username.as_str()); let followers = self.urls.user_followers(e.author_username.as_str()); let in_reply_to = e .thought .in_reply_to_id .map(|id| self.urls.thought_url(id.as_uuid())); let note = ThoughtNote::new_public( note_url.clone(), actor_url, e.thought.content.as_str().to_owned(), e.thought.created_at, in_reply_to, e.thought.sensitive, e.thought.content_warning, followers, ); Ok((note_url, serde_json::to_value(¬e)?)) }) .collect() } async fn get_local_objects_page( &self, user_id: uuid::Uuid, before: Option>, limit: usize, ) -> Result)>> { let uid = UserId::from_uuid(user_id); let entries = self .repo .outbox_page_for_actor(&uid, before, limit) .await .map_err(|e| anyhow!("{e}"))?; entries .into_iter() .map(|e| { let created_at = e.thought.created_at; let note_url = self.urls.thought_url(e.thought.id.as_uuid()); let actor_url = self.urls.user_url(e.author_username.as_str()); let followers = self.urls.user_followers(e.author_username.as_str()); let in_reply_to = e .thought .in_reply_to_id .map(|id| self.urls.thought_url(id.as_uuid())); let note = ThoughtNote::new_public( note_url.clone(), actor_url, e.thought.content.as_str().to_owned(), created_at, in_reply_to, e.thought.sensitive, e.thought.content_warning, followers, ); Ok((note_url, serde_json::to_value(¬e)?, created_at)) }) .collect() } async fn on_create( &self, ap_id: &Url, actor_url: &Url, object: serde_json::Value, ) -> Result<()> { let note: ThoughtNote = serde_json::from_value(object)?; let author_id = self .repo .intern_remote_actor(actor_url) .await .map_err(|e| anyhow!("{e}"))?; // Derive visibility from AP addressing conventions. let as_public = "https://www.w3.org/ns/activitystreams#Public"; let in_to = note.to.iter().any(|s| s == as_public); let in_cc = note.cc.iter().any(|s| s == as_public); let has_followers = note.to.iter().any(|s| s.ends_with("/followers")) || note.cc.iter().any(|s| s.ends_with("/followers")); let visibility = if in_to { "public" } else if in_cc { "unlisted" } else if has_followers { "followers" } else { "direct" }; self.repo .accept_note( ap_id, &author_id, ¬e.content, note.published, note.sensitive, note.summary, visibility, ) .await .map_err(|e| anyhow!("{e}")) } async fn on_update( &self, ap_id: &Url, _actor_url: &Url, object: serde_json::Value, ) -> Result<()> { let note: ThoughtNote = serde_json::from_value(object)?; self.repo .apply_note_update(ap_id, ¬e.content) .await .map_err(|e| anyhow!("{e}")) } async fn on_delete(&self, ap_id: &Url, _actor_url: &Url) -> Result<()> { self.repo .retract_note(ap_id) .await .map_err(|e| anyhow!("{e}")) } async fn on_actor_removed(&self, actor_url: &Url) -> Result<()> { self.repo .retract_actor_notes(actor_url) .await .map_err(|e| anyhow!("{e}")) } async fn count_local_posts(&self) -> Result { self.repo .count_local_notes() .await .map_err(|e| anyhow!("{e}")) } }