Files
thoughts/crates/adapters/activitypub/src/handler.rs
Gabriel Kaszewski 8602614e7c
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 9m25s
test / unit (pull_request) Successful in 16m39s
test / integration (pull_request) Failing after 17m35s
fix(ap): visibility-aware addressing — correct to/cc outbound, parse inbound to/cc
2026-05-14 19:34:43 +02:00

180 lines
5.5 KiB
Rust

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<dyn ActivityPubRepository>,
urls: ThoughtsUrls,
}
impl ThoughtsObjectHandler {
pub fn new(repo: Arc<dyn ActivityPubRepository>, 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<Vec<(Url, serde_json::Value)>> {
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(&note)?))
})
.collect()
}
async fn get_local_objects_page(
&self,
user_id: uuid::Uuid,
before: Option<DateTime<Utc>>,
limit: usize,
) -> Result<Vec<(Url, serde_json::Value, DateTime<Utc>)>> {
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(&note)?, 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,
&note.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, &note.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<u64> {
self.repo
.count_local_notes()
.await
.map_err(|e| anyhow!("{e}"))
}
}