feat: add federation processed activities table and update dependencies
Some checks failed
test / unit (push) Has been cancelled
lint / lint (push) Has been cancelled

- Created a new SQL migration to add the `federation_processed_activities` table with an index on `processed_at`.
- Updated dependencies in `Cargo.toml` files across `bootstrap` and `worker` crates, including version updates for `k-ap`.
- Enhanced the event publishing mechanism in the `factory.rs` file to include a new `KapPublisher` for handling federation events.
- Refactored the `build` function in `factory.rs` to accommodate the new event publisher and improve ActivityPub service initialization.
- Modified the worker's main loop to handle new federation event types and improved error handling for event processing.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-29 03:47:06 +02:00
parent 37d03a06dd
commit ecb61f9b8f
16 changed files with 1016 additions and 461 deletions

View File

@@ -4,18 +4,17 @@ version = "0.1.0"
edition = "2021"
[dependencies]
k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.10" }
domain = { workspace = true }
url = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
activitypub_federation = "0.7.0-beta.11"
reqwest = { workspace = true }
futures = { workspace = true }
tokio = { workspace = true }
axum = { workspace = true }
k-ap = { version = "0.3.0", registry = "gitea" }
domain = { workspace = true }
url = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
reqwest = { workspace = true }
futures = { workspace = true }
tokio = { workspace = true }
axum = { workspace = true }

View File

@@ -12,7 +12,7 @@ use crate::port::{AcceptNoteInput, ActivityPubRepository};
use crate::urls::ThoughtsUrls;
use domain::ports::{EventPublisher, TagRepository};
use domain::value_objects::UserId;
use k_ap::ApObjectHandler;
use k_ap::{ApContentReader, ApObjectHandler};
pub struct ThoughtsObjectHandler {
repo: Arc<dyn ActivityPubRepository>,
@@ -37,43 +37,10 @@ impl ThoughtsObjectHandler {
}
}
#[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(&user_id.to_string());
let followers = self.urls.user_followers(&user_id.to_string());
let in_reply_to = e
.thought
.in_reply_to_id
.map(|id| self.urls.thought_url(id.as_uuid()));
let note = ThoughtNote::new_public(ThoughtNoteInput {
id: note_url.clone(),
actor_url,
content: e.thought.content.as_str().to_owned(),
published: e.thought.created_at,
in_reply_to,
sensitive: e.thought.sensitive,
summary: e.thought.content_warning,
followers_url: followers,
});
Ok((note_url, serde_json::to_value(&note)?))
})
.collect()
}
// ── ApContentReader ───────────────────────────────────────────────────────────
#[async_trait]
impl ApContentReader for ThoughtsObjectHandler {
async fn get_local_objects_page(
&self,
user_id: uuid::Uuid,
@@ -112,6 +79,18 @@ impl ApObjectHandler for ThoughtsObjectHandler {
.collect()
}
async fn count_local_posts(&self) -> Result<u64> {
self.repo
.count_local_notes()
.await
.map_err(|e| anyhow!("{e}"))
}
}
// ── ApObjectHandler ───────────────────────────────────────────────────────────
#[async_trait]
impl ApObjectHandler for ThoughtsObjectHandler {
async fn on_create(
&self,
ap_id: &Url,
@@ -128,7 +107,6 @@ impl ApObjectHandler for ThoughtsObjectHandler {
.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);
@@ -161,7 +139,6 @@ impl ApObjectHandler for ThoughtsObjectHandler {
.await
.map_err(|e| anyhow!("{e}"))?;
// Extract and index hashtags from the AP tag array.
let hashtag_names: Vec<String> = note
.tag
.iter()
@@ -177,7 +154,6 @@ impl ApObjectHandler for ThoughtsObjectHandler {
}
}
// Fire mention notifications for local @mentions in the note's tag array.
let base_url = url::Url::parse(&self.urls.base_url)
.ok()
.and_then(|u| u.host_str().map(|h| h.to_string()))
@@ -408,10 +384,7 @@ impl ApObjectHandler for ThoughtsObjectHandler {
Ok(())
}
async fn count_local_posts(&self) -> Result<u64> {
self.repo
.count_local_notes()
.await
.map_err(|e| anyhow!("{e}"))
async fn on_announce_of_remote(&self, _object_url: &Url, _actor_url: &Url) -> Result<()> {
Ok(())
}
}

View File

@@ -97,6 +97,17 @@ fn build_note_json(
note
}
fn thought_to_ap_visibility(
v: &domain::models::thought::Visibility,
) -> k_ap::ApVisibility {
match v {
domain::models::thought::Visibility::Public => k_ap::ApVisibility::Public,
domain::models::thought::Visibility::Unlisted => k_ap::ApVisibility::Public,
domain::models::thought::Visibility::Followers => k_ap::ApVisibility::FollowersOnly,
domain::models::thought::Visibility::Direct => k_ap::ApVisibility::Private,
}
}
fn k_ap_actor_to_domain(a: k_ap::RemoteActor) -> DomainRemoteActor {
DomainRemoteActor {
url: a.url,
@@ -264,7 +275,12 @@ impl crate::port::OutboundFederationPort for ApFederationAdapter {
in_reply_to_url,
);
self.inner
.broadcast_create_note(user_uuid, note)
.broadcast_create_note(
user_uuid,
note,
thought_to_ap_visibility(&thought.visibility),
vec![],
)
.await
.map_err(|e| DomainError::Internal(e.to_string()))
}
@@ -300,7 +316,12 @@ impl crate::port::OutboundFederationPort for ApFederationAdapter {
in_reply_to_url,
);
self.inner
.broadcast_update_note(user_uuid, note)
.broadcast_update_note(
user_uuid,
note,
thought_to_ap_visibility(&thought.visibility),
vec![],
)
.await
.map_err(|e| DomainError::Internal(e.to_string()))
}
@@ -384,7 +405,7 @@ impl FederationSchedulerPort for ApFederationAdapter {
let actor = actor_ap_url.to_string();
let outbox = outbox_url.to_string();
tokio::spawn(async move {
if let Err(e) = service.backfill_outbox(&outbox, &actor).await {
if let Err(e) = service.import_remote_outbox(&outbox, &actor).await {
tracing::warn!(actor = %actor, error = %e, "posts backfill failed");
}
});
@@ -517,7 +538,7 @@ impl FederationLookupPort for ApFederationAdapter {
last_fetched_at: chrono::Utc::now(),
bio: actor.bio,
banner_url: actor.banner_url.as_ref().map(|u| u.to_string()),
also_known_as: actor.also_known_as,
also_known_as: actor.also_known_as.into_iter().next(),
followers_url: actor.followers_url.as_ref().map(|u| u.to_string()),
following_url: actor.following_url.as_ref().map(|u| u.to_string()),
attachment: actor