forked from alemi/upub
fix: outbound->delivery, local->outbound
also stop discarding duplicated deliveries, ouchhhh
This commit is contained in:
parent
0e779c3096
commit
f6d30b3bec
9 changed files with 143 additions and 139 deletions
4
main.rs
4
main.rs
|
@ -99,7 +99,7 @@ enum Mode {
|
||||||
#[derive(Debug, Clone, clap::ValueEnum)]
|
#[derive(Debug, Clone, clap::ValueEnum)]
|
||||||
enum Filter {
|
enum Filter {
|
||||||
All,
|
All,
|
||||||
Local,
|
Delivery,
|
||||||
Inbound,
|
Inbound,
|
||||||
Outbound,
|
Outbound,
|
||||||
}
|
}
|
||||||
|
@ -188,7 +188,7 @@ impl From<Filter> for Option<upub::model::job::JobType> {
|
||||||
fn from(value: Filter) -> Self {
|
fn from(value: Filter) -> Self {
|
||||||
match value {
|
match value {
|
||||||
Filter::All => None,
|
Filter::All => None,
|
||||||
Filter::Local => Some(upub::model::job::JobType::Local),
|
Filter::Delivery => Some(upub::model::job::JobType::Delivery),
|
||||||
Filter::Inbound => Some(upub::model::job::JobType::Inbound),
|
Filter::Inbound => Some(upub::model::job::JobType::Inbound),
|
||||||
Filter::Outbound => Some(upub::model::job::JobType::Outbound),
|
Filter::Outbound => Some(upub::model::job::JobType::Outbound),
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use sea_orm::entity::prelude::*;
|
||||||
pub enum JobType {
|
pub enum JobType {
|
||||||
Inbound = 1,
|
Inbound = 1,
|
||||||
Outbound = 2,
|
Outbound = 2,
|
||||||
Local = 3,
|
Delivery = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
|
|
@ -88,7 +88,7 @@ impl Addresser for crate::Context {
|
||||||
crate::model::job::ActiveModel {
|
crate::model::job::ActiveModel {
|
||||||
internal: sea_orm::ActiveValue::NotSet,
|
internal: sea_orm::ActiveValue::NotSet,
|
||||||
actor: Set(from.to_string()),
|
actor: Set(from.to_string()),
|
||||||
job_type: Set(crate::model::job::JobType::Outbound),
|
job_type: Set(crate::model::job::JobType::Delivery),
|
||||||
payload: Set(None),
|
payload: Set(None),
|
||||||
// TODO we should resolve each user by id and check its inbox because we can't assume
|
// TODO we should resolve each user by id and check its inbox because we can't assume
|
||||||
// it's /actors/{id}/inbox for every software, but oh well it's waaaaay easier now
|
// it's /actors/{id}/inbox for every software, but oh well it's waaaaay easier now
|
||||||
|
|
|
@ -56,7 +56,7 @@ pub async fn post(
|
||||||
let job = model::job::ActiveModel {
|
let job = model::job::ActiveModel {
|
||||||
internal: NotSet,
|
internal: NotSet,
|
||||||
activity: Set(aid.clone()),
|
activity: Set(aid.clone()),
|
||||||
job_type: Set(model::job::JobType::Local),
|
job_type: Set(model::job::JobType::Outbound),
|
||||||
actor: Set(uid.clone()),
|
actor: Set(uid.clone()),
|
||||||
target: Set(None),
|
target: Set(None),
|
||||||
published: Set(chrono::Utc::now()),
|
published: Set(chrono::Utc::now()),
|
||||||
|
|
63
upub/worker/src/delivery.rs
Normal file
63
upub/worker/src/delivery.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
|
use reqwest::Method;
|
||||||
|
|
||||||
|
use apb::{LD, Node, ActivityMut};
|
||||||
|
use upub::{Context, model, traits::Fetcher};
|
||||||
|
|
||||||
|
pub async fn process(ctx: Context, job: &model::job::Model) -> crate::JobResult<()> {
|
||||||
|
tracing::info!("delivering {} to {:?}", job.activity, job.target);
|
||||||
|
|
||||||
|
let payload = match model::activity::Entity::find_by_ap_id(&job.activity)
|
||||||
|
.find_also_related(model::object::Entity)
|
||||||
|
.one(ctx.db())
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some((activity, None)) => activity.ap().ld_context(),
|
||||||
|
Some((activity, Some(object))) => {
|
||||||
|
let always_embed = matches!(
|
||||||
|
activity.activity_type,
|
||||||
|
apb::ActivityType::Create
|
||||||
|
| apb::ActivityType::Undo
|
||||||
|
| apb::ActivityType::Update
|
||||||
|
| apb::ActivityType::Accept(_)
|
||||||
|
| apb::ActivityType::Reject(_)
|
||||||
|
);
|
||||||
|
if always_embed {
|
||||||
|
activity.ap().set_object(Node::object(object.ap())).ld_context()
|
||||||
|
} else {
|
||||||
|
activity.ap().ld_context()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
tracing::info!("skipping dispatch for deleted object {}", job.activity);
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(actor) = model::actor::Entity::find_by_ap_id(&job.actor)
|
||||||
|
.one(ctx.db())
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
tracing::error!("abandoning delivery from non existant actor {}: {job:#?}", job.actor);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(key) = actor.private_key
|
||||||
|
else {
|
||||||
|
tracing::error!("abandoning delivery from actor without private key {}: {job:#?}", job.actor);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = Context::request(
|
||||||
|
Method::POST, job.target.as_deref().unwrap_or(""),
|
||||||
|
Some(&serde_json::to_string(&payload).unwrap()),
|
||||||
|
&job.actor, &key, ctx.domain()
|
||||||
|
).await {
|
||||||
|
tracing::warn!("failed delivery of {} to {:?} : {e}", job.activity, job.target);
|
||||||
|
model::job::Entity::insert(job.clone().repeat())
|
||||||
|
.exec(ctx.db())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -104,12 +104,16 @@ impl JobDispatcher for Context {
|
||||||
restart!(now);
|
restart!(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(Some(_)) = model::activity::Entity::find_by_ap_id(&job.activity)
|
if job.job_type != model::job::JobType::Delivery {
|
||||||
.one(self.db())
|
// delivery jobs are all pre-processed activities
|
||||||
.await
|
// inbound/outbound jobs carry side effects which should only happen once
|
||||||
{
|
if let Ok(Some(_)) = model::activity::Entity::find_by_ap_id(&job.activity)
|
||||||
tracing::info!("dropping already processed job '{}'", job.activity);
|
.one(self.db())
|
||||||
restart!(now);
|
.await
|
||||||
|
{
|
||||||
|
tracing::info!("dropping already processed job '{}'", job.activity);
|
||||||
|
restart!(now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ctx = self.clone();
|
let _ctx = self.clone();
|
||||||
|
@ -117,7 +121,7 @@ impl JobDispatcher for Context {
|
||||||
let res = match job.job_type {
|
let res = match job.job_type {
|
||||||
model::job::JobType::Inbound => crate::inbound::process(_ctx.clone(), &job).await,
|
model::job::JobType::Inbound => crate::inbound::process(_ctx.clone(), &job).await,
|
||||||
model::job::JobType::Outbound => crate::outbound::process(_ctx.clone(), &job).await,
|
model::job::JobType::Outbound => crate::outbound::process(_ctx.clone(), &job).await,
|
||||||
model::job::JobType::Local => crate::local::process(_ctx.clone(), &job).await,
|
model::job::JobType::Delivery => crate::delivery::process(_ctx.clone(), &job).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
pub mod dispatcher;
|
pub mod dispatcher;
|
||||||
pub mod inbound;
|
pub mod inbound;
|
||||||
pub mod outbound;
|
pub mod outbound;
|
||||||
pub mod local;
|
pub mod delivery;
|
||||||
|
|
||||||
pub use dispatcher::{JobError, JobResult};
|
pub use dispatcher::{JobError, JobResult};
|
||||||
|
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
use apb::{target::Addressed, Activity, ActivityMut, BaseMut, Object, ObjectMut};
|
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns, TransactionTrait};
|
|
||||||
use upub::{model, traits::{Addresser, Processor}, Context};
|
|
||||||
|
|
||||||
|
|
||||||
pub async fn process(ctx: Context, job: &model::job::Model) -> crate::JobResult<()> {
|
|
||||||
let payload = job.payload.as_ref().ok_or(crate::JobError::MissingPayload)?;
|
|
||||||
let mut activity : serde_json::Value = serde_json::from_str(payload)?;
|
|
||||||
let mut t = activity.object_type()?;
|
|
||||||
let tx = ctx.db().begin().await?;
|
|
||||||
|
|
||||||
if matches!(t, apb::ObjectType::Note) {
|
|
||||||
activity = apb::new()
|
|
||||||
.set_activity_type(Some(apb::ActivityType::Create))
|
|
||||||
.set_object(apb::Node::object(activity));
|
|
||||||
t = apb::ObjectType::Activity(apb::ActivityType::Create);
|
|
||||||
}
|
|
||||||
|
|
||||||
activity = activity
|
|
||||||
.set_id(Some(&job.activity))
|
|
||||||
.set_actor(apb::Node::link(job.actor.clone()))
|
|
||||||
.set_published(Some(chrono::Utc::now()));
|
|
||||||
|
|
||||||
if matches!(t, apb::ObjectType::Activity(apb::ActivityType::Create)) {
|
|
||||||
let raw_oid = Context::new_id();
|
|
||||||
let oid = ctx.oid(&raw_oid);
|
|
||||||
// object must be embedded, wont dereference here
|
|
||||||
let object = activity.object().extract().ok_or(apb::FieldErr("object"))?;
|
|
||||||
// TODO regex hell here i come...
|
|
||||||
let re = regex::Regex::new(r"@(.+)@([^ ]+)").expect("failed compiling regex pattern");
|
|
||||||
let mut content = object.content().map(|x| x.to_string()).ok();
|
|
||||||
if let Some(c) = content {
|
|
||||||
let mut tmp = mdhtml::safe_markdown(&c);
|
|
||||||
for (full, [user, domain]) in re.captures_iter(&tmp.clone()).map(|x| x.extract()) {
|
|
||||||
if let Ok(Some(uid)) = model::actor::Entity::find()
|
|
||||||
.filter(model::actor::Column::PreferredUsername.eq(user))
|
|
||||||
.filter(model::actor::Column::Domain.eq(domain))
|
|
||||||
.select_only()
|
|
||||||
.select_column(model::actor::Column::Id)
|
|
||||||
.into_tuple::<String>()
|
|
||||||
.one(&tx)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tmp = tmp.replacen(full, &format!("<a href=\"{uid}\" class=\"u-url mention\">@{user}</a>"), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content = Some(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
activity = activity
|
|
||||||
.set_object(apb::Node::object(
|
|
||||||
object
|
|
||||||
.set_id(Some(&oid))
|
|
||||||
.set_content(content.as_deref())
|
|
||||||
.set_attributed_to(apb::Node::link(job.actor.clone()))
|
|
||||||
.set_published(Some(chrono::Utc::now()))
|
|
||||||
.set_url(apb::Node::maybe_link(ctx.cfg().frontend_url(&format!("/objects/{raw_oid}")))),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO we expand addressing twice, ugghhhhh
|
|
||||||
let targets = ctx.expand_addressing(activity.addressed()).await?;
|
|
||||||
|
|
||||||
ctx.process(activity, &tx).await?;
|
|
||||||
|
|
||||||
ctx.deliver_to(&job.activity, &job.actor, &targets, &tx).await?;
|
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,63 +1,71 @@
|
||||||
use sea_orm::EntityTrait;
|
use apb::{target::Addressed, Activity, ActivityMut, BaseMut, Object, ObjectMut};
|
||||||
use reqwest::Method;
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns, TransactionTrait};
|
||||||
|
use upub::{model, traits::{Addresser, Processor}, Context};
|
||||||
|
|
||||||
use apb::{LD, Node, ActivityMut};
|
|
||||||
use upub::{Context, model, traits::Fetcher};
|
|
||||||
|
|
||||||
pub async fn process(ctx: Context, job: &model::job::Model) -> crate::JobResult<()> {
|
pub async fn process(ctx: Context, job: &model::job::Model) -> crate::JobResult<()> {
|
||||||
tracing::info!("delivering {} to {:?}", job.activity, job.target);
|
let payload = job.payload.as_ref().ok_or(crate::JobError::MissingPayload)?;
|
||||||
|
let mut activity : serde_json::Value = serde_json::from_str(payload)?;
|
||||||
|
let mut t = activity.object_type()?;
|
||||||
|
let tx = ctx.db().begin().await?;
|
||||||
|
|
||||||
let payload = match model::activity::Entity::find_by_ap_id(&job.activity)
|
if matches!(t, apb::ObjectType::Note) {
|
||||||
.find_also_related(model::object::Entity)
|
activity = apb::new()
|
||||||
.one(ctx.db())
|
.set_activity_type(Some(apb::ActivityType::Create))
|
||||||
.await?
|
.set_object(apb::Node::object(activity));
|
||||||
{
|
t = apb::ObjectType::Activity(apb::ActivityType::Create);
|
||||||
Some((activity, None)) => activity.ap().ld_context(),
|
|
||||||
Some((activity, Some(object))) => {
|
|
||||||
let always_embed = matches!(
|
|
||||||
activity.activity_type,
|
|
||||||
apb::ActivityType::Create
|
|
||||||
| apb::ActivityType::Undo
|
|
||||||
| apb::ActivityType::Update
|
|
||||||
| apb::ActivityType::Accept(_)
|
|
||||||
| apb::ActivityType::Reject(_)
|
|
||||||
);
|
|
||||||
if always_embed {
|
|
||||||
activity.ap().set_object(Node::object(object.ap())).ld_context()
|
|
||||||
} else {
|
|
||||||
activity.ap().ld_context()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
tracing::info!("skipping dispatch for deleted object {}", job.activity);
|
|
||||||
return Ok(());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(actor) = model::actor::Entity::find_by_ap_id(&job.actor)
|
|
||||||
.one(ctx.db())
|
|
||||||
.await?
|
|
||||||
else {
|
|
||||||
tracing::error!("abandoning delivery from non existant actor {}: {job:#?}", job.actor);
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(key) = actor.private_key
|
|
||||||
else {
|
|
||||||
tracing::error!("abandoning delivery from actor without private key {}: {job:#?}", job.actor);
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = Context::request(
|
|
||||||
Method::POST, job.target.as_deref().unwrap_or(""),
|
|
||||||
Some(&serde_json::to_string(&payload).unwrap()),
|
|
||||||
&job.actor, &key, ctx.domain()
|
|
||||||
).await {
|
|
||||||
tracing::warn!("failed delivery of {} to {:?} : {e}", job.activity, job.target);
|
|
||||||
model::job::Entity::insert(job.clone().repeat())
|
|
||||||
.exec(ctx.db())
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activity = activity
|
||||||
|
.set_id(Some(&job.activity))
|
||||||
|
.set_actor(apb::Node::link(job.actor.clone()))
|
||||||
|
.set_published(Some(chrono::Utc::now()));
|
||||||
|
|
||||||
|
if matches!(t, apb::ObjectType::Activity(apb::ActivityType::Create)) {
|
||||||
|
let raw_oid = Context::new_id();
|
||||||
|
let oid = ctx.oid(&raw_oid);
|
||||||
|
// object must be embedded, wont dereference here
|
||||||
|
let object = activity.object().extract().ok_or(apb::FieldErr("object"))?;
|
||||||
|
// TODO regex hell here i come...
|
||||||
|
let re = regex::Regex::new(r"@(.+)@([^ ]+)").expect("failed compiling regex pattern");
|
||||||
|
let mut content = object.content().map(|x| x.to_string()).ok();
|
||||||
|
if let Some(c) = content {
|
||||||
|
let mut tmp = mdhtml::safe_markdown(&c);
|
||||||
|
for (full, [user, domain]) in re.captures_iter(&tmp.clone()).map(|x| x.extract()) {
|
||||||
|
if let Ok(Some(uid)) = model::actor::Entity::find()
|
||||||
|
.filter(model::actor::Column::PreferredUsername.eq(user))
|
||||||
|
.filter(model::actor::Column::Domain.eq(domain))
|
||||||
|
.select_only()
|
||||||
|
.select_column(model::actor::Column::Id)
|
||||||
|
.into_tuple::<String>()
|
||||||
|
.one(&tx)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tmp = tmp.replacen(full, &format!("<a href=\"{uid}\" class=\"u-url mention\">@{user}</a>"), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content = Some(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
activity = activity
|
||||||
|
.set_object(apb::Node::object(
|
||||||
|
object
|
||||||
|
.set_id(Some(&oid))
|
||||||
|
.set_content(content.as_deref())
|
||||||
|
.set_attributed_to(apb::Node::link(job.actor.clone()))
|
||||||
|
.set_published(Some(chrono::Utc::now()))
|
||||||
|
.set_url(apb::Node::maybe_link(ctx.cfg().frontend_url(&format!("/objects/{raw_oid}")))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO we expand addressing twice, ugghhhhh
|
||||||
|
let targets = ctx.expand_addressing(activity.addressed(), &tx).await?;
|
||||||
|
|
||||||
|
ctx.process(activity, &tx).await?;
|
||||||
|
|
||||||
|
ctx.deliver_to(&job.activity, &job.actor, &targets, &tx).await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue