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

@@ -8,26 +8,29 @@ name = "thoughts"
path = "src/main.rs"
[dependencies]
presentation = { workspace = true }
domain = { workspace = true }
postgres = { workspace = true }
postgres-search = { workspace = true }
presentation = { workspace = true }
domain = { workspace = true }
postgres = { workspace = true }
postgres-search = { workspace = true }
postgres-federation = { workspace = true }
activitypub = { workspace = true }
k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.10" }
nats = { workspace = true }
event-transport = { workspace = true }
auth = { workspace = true }
storage = { workspace = true }
application = { workspace = true }
sqlx = { workspace = true }
async-nats = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true, features = ["full"] }
axum = { workspace = true }
tower-http = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
dotenvy = { workspace = true }
tower_governor = "0.8"
http = "1"
activitypub = { workspace = true }
k-ap = { version = "0.3.0", registry = "gitea" }
serde_json = { workspace = true }
anyhow = { workspace = true }
nats = { workspace = true }
event-transport = { workspace = true }
event-payload = { workspace = true }
auth = { workspace = true }
storage = { workspace = true }
application = { workspace = true }
sqlx = { workspace = true }
async-nats = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true, features = ["full"] }
axum = { workspace = true }
tower-http = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
dotenvy = { workspace = true }
tower_governor = "0.8"
http = "1"

View File

@@ -15,8 +15,8 @@ use domain::{
events::DomainEvent,
ports::{EventPublisher, OutboxWriter},
};
use event_transport::EventPublisherAdapter;
use k_ap::ActivityPubService;
use event_transport::{EventPublisherAdapter, Transport};
use k_ap::{ActivityPubService, FederationEvent};
use nats::NatsTransport;
use postgres::activitypub::PgActivityPubRepository;
use postgres::engagement::PgEngagementRepository;
@@ -42,6 +42,39 @@ impl EventPublisher for NoOpEventPublisher {
}
}
struct KapPublisher(NatsTransport);
#[async_trait]
impl k_ap::data::EventPublisher for KapPublisher {
async fn publish(&self, event: FederationEvent) -> anyhow::Result<()> {
let (subject, payload) = match event {
FederationEvent::DeliveryRequested { inbox, activity, signing_actor_id } => (
"federation.delivery.requested",
serde_json::to_vec(&event_payload::EventPayload::FederationDeliveryRequested {
inbox: inbox.to_string(),
activity,
signing_actor_id: signing_actor_id.to_string(),
})?,
),
FederationEvent::BackfillRequested { owner_user_id, follower_inbox_url } => (
"federation.backfill.requested",
serde_json::to_vec(&event_payload::EventPayload::FederationBackfillRequested {
owner_user_id: owner_user_id.to_string(),
follower_inbox_url,
})?,
),
FederationEvent::DeliveryFailed { inbox, error, .. } => {
tracing::warn!(%inbox, %error, "AP delivery failed permanently");
return Ok(());
}
};
self.0
.publish_bytes(subject, &payload)
.await
.map_err(|e| anyhow::anyhow!(e))
}
}
pub async fn build(cfg: &Config) -> Infrastructure {
// 1. Database connection + migrations
let pool = PgPool::connect(&cfg.database_url)
@@ -54,49 +87,64 @@ pub async fn build(cfg: &Config) -> Infrastructure {
tracing::info!("Database connected and migrations applied");
// 2. Event publisher — real NATS or no-op fallback
let event_publisher: Arc<dyn EventPublisher> = match &cfg.nats_url {
let nats_client: Option<async_nats::Client> = match &cfg.nats_url {
Some(url) => match async_nats::connect(url).await {
Ok(client) => {
tracing::info!("Connected to NATS at {url}");
if let Err(e) = nats::ensure_stream(&client).await {
tracing::warn!("JetStream stream setup failed: {e} — events may be lost");
}
Arc::new(EventPublisherAdapter::new(NatsTransport::new(client)))
Some(client)
}
Err(e) => {
tracing::warn!("NATS connect failed ({e}) — falling back to no-op publisher");
Arc::new(NoOpEventPublisher)
None
}
},
None => {
tracing::info!("NATS_URL not set — using no-op event publisher");
Arc::new(NoOpEventPublisher)
None
}
};
let event_publisher: Arc<dyn EventPublisher> = match &nats_client {
Some(client) => Arc::new(EventPublisherAdapter::new(NatsTransport::new(client.clone()))),
None => Arc::new(NoOpEventPublisher),
};
let kap_publisher: Option<Arc<dyn k_ap::data::EventPublisher>> = nats_client
.as_ref()
.map(|c| Arc::new(KapPublisher(NatsTransport::new(c.clone()))) as _);
// 3. ActivityPub federation
let connections_repo = Arc::new(PgRemoteActorConnectionRepository::new(pool.clone()));
let raw_ap_service = Arc::new(
ActivityPubService::builder(
Arc::new(PostgresFederationRepository::new(pool.clone())),
Arc::new(PostgresApUserRepository::new(
pool.clone(),
cfg.base_url.clone(),
)),
Arc::new(ThoughtsObjectHandler::new(
Arc::new(PgActivityPubRepository::new(pool.clone())),
&cfg.base_url,
Some(event_publisher.clone()),
Arc::new(postgres::tag::PgTagRepository::new(pool.clone())),
)),
let fed_repo = Arc::new(PostgresFederationRepository::new(pool.clone()));
let ap_handler = Arc::new(ThoughtsObjectHandler::new(
Arc::new(PgActivityPubRepository::new(pool.clone())),
&cfg.base_url,
Some(event_publisher.clone()),
Arc::new(postgres::tag::PgTagRepository::new(pool.clone())),
));
let mut ap_builder = ActivityPubService::builder(cfg.base_url.clone())
.activity_repo(fed_repo.clone())
.follow_repo(fed_repo.clone())
.actor_repo(fed_repo.clone())
.blocklist_repo(fed_repo.clone())
.user_repo(Arc::new(PostgresApUserRepository::new(
pool.clone(),
cfg.base_url.clone(),
)
)))
.content_reader(ap_handler.clone())
.object_handler(ap_handler)
.allow_registration(cfg.allow_registration)
.software_name("thoughts")
.debug(cfg.debug)
.build()
.await
.expect("Failed to build ActivityPubService"),
.debug(cfg.debug);
if let Some(publisher) = kap_publisher {
ap_builder = ap_builder.event_publisher(publisher);
}
let raw_ap_service = Arc::new(
ap_builder
.build()
.await
.expect("Failed to build ActivityPubService"),
);
let ap_service = Arc::new(ApFederationAdapter::new(raw_ap_service, connections_repo));