diff --git a/src/api.rs b/src/api.rs index 540c26c..0d992fc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,7 @@ use crate::database::DatabaseHandle; use crate::error::MyResult; use crate::federation::activities::create_article::CreateArticle; -use crate::federation::activities::update_article::UpdateArticle; +use crate::federation::activities::update_remote_article::UpdateRemoteArticle; use crate::federation::objects::article::DbArticle; use crate::federation::objects::edit::{ApubEdit, DbEdit, EditVersion}; use crate::federation::objects::instance::DbInstance; @@ -10,6 +10,7 @@ use activitypub_federation::fetch::object_id::ObjectId; use anyhow::anyhow; +use crate::federation::activities::update_local_article::UpdateLocalArticle; use axum::extract::Query; use axum::routing::{get, post}; use axum::{Form, Json, Router}; @@ -93,9 +94,9 @@ async fn edit_article( article.clone() }; - UpdateArticle::send_to_followers(edit, updated_article.clone(), &data).await?; + UpdateLocalArticle::send(updated_article, &data).await?; } else { - UpdateArticle::send_to_origin( + UpdateRemoteArticle::send( edit, original_article.instance.dereference(&data).await?, &data, diff --git a/src/federation/activities/mod.rs b/src/federation/activities/mod.rs index df78b9c..c5ef681 100644 --- a/src/federation/activities/mod.rs +++ b/src/federation/activities/mod.rs @@ -2,4 +2,5 @@ pub mod accept; pub mod create_article; pub mod follow; pub mod reject; -pub mod update_article; +pub mod update_local_article; +pub mod update_remote_article; diff --git a/src/federation/activities/update_local_article.rs b/src/federation/activities/update_local_article.rs new file mode 100644 index 0000000..680194f --- /dev/null +++ b/src/federation/activities/update_local_article.rs @@ -0,0 +1,71 @@ +use crate::database::DatabaseHandle; +use crate::error::MyResult; +use crate::federation::objects::article::{ApubArticle, DbArticle}; + +use crate::federation::objects::instance::DbInstance; +use crate::utils::generate_activity_id; +use activitypub_federation::kinds::activity::UpdateType; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + protocol::helpers::deserialize_one_or_many, + traits::{ActivityHandler, Object}, +}; + +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UpdateLocalArticle { + pub actor: ObjectId, + #[serde(deserialize_with = "deserialize_one_or_many")] + pub to: Vec, + pub object: ApubArticle, + #[serde(rename = "type")] + pub kind: UpdateType, + pub id: Url, +} + +impl UpdateLocalArticle { + /// Sent from article origin instance + pub async fn send(article: DbArticle, data: &Data) -> MyResult<()> { + debug_assert!(article.local); + let local_instance = data.local_instance(); + let id = generate_activity_id(local_instance.ap_id.inner())?; + let update = UpdateLocalArticle { + actor: local_instance.ap_id.clone(), + to: local_instance.follower_ids(), + object: article.into_json(data).await?, + kind: Default::default(), + id, + }; + local_instance.send_to_followers(update, data).await?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl ActivityHandler for UpdateLocalArticle { + type DataType = DatabaseHandle; + type Error = crate::error::Error; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { + Ok(()) + } + + /// Received on article follower instances (where article is always remote) + async fn receive(self, data: &Data) -> Result<(), Self::Error> { + DbArticle::from_json(self.object, data).await?; + + Ok(()) + } +} diff --git a/src/federation/activities/update_article.rs b/src/federation/activities/update_remote_article.rs similarity index 57% rename from src/federation/activities/update_article.rs rename to src/federation/activities/update_remote_article.rs index b70c3c4..d72f051 100644 --- a/src/federation/activities/update_article.rs +++ b/src/federation/activities/update_remote_article.rs @@ -1,61 +1,45 @@ use crate::database::DatabaseHandle; use crate::error::MyResult; -use crate::federation::objects::article::DbArticle; + use crate::federation::objects::edit::{ApubEdit, DbEdit}; use crate::federation::objects::instance::DbInstance; use crate::utils::generate_activity_id; -use activitypub_federation::kinds::activity::CreateType; +use activitypub_federation::kinds::activity::UpdateType; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, traits::{ActivityHandler, Object}, }; +use diffy::{apply, Patch}; use crate::federation::activities::reject::RejectEdit; +use crate::federation::activities::update_local_article::UpdateLocalArticle; use serde::{Deserialize, Serialize}; use url::Url; #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct UpdateArticle { +pub struct UpdateRemoteArticle { pub actor: ObjectId, #[serde(deserialize_with = "deserialize_one_or_many")] pub to: Vec, pub object: ApubEdit, #[serde(rename = "type")] - pub kind: CreateType, + pub kind: UpdateType, pub id: Url, } -impl UpdateArticle { - pub async fn send_to_followers( - edit: DbEdit, - article: DbArticle, - data: &Data, - ) -> MyResult<()> { - debug_assert!(article.local); - let local_instance = data.local_instance(); - let id = generate_activity_id(local_instance.ap_id.inner())?; - let update = UpdateArticle { - actor: local_instance.ap_id.clone(), - to: local_instance.follower_ids(), - object: edit.into_json(data).await?, - kind: Default::default(), - id, - }; - local_instance.send_to_followers(update, data).await?; - Ok(()) - } - - pub async fn send_to_origin( +impl UpdateRemoteArticle { + /// Sent by a follower instance + pub async fn send( edit: DbEdit, article_instance: DbInstance, data: &Data, ) -> MyResult<()> { let local_instance = data.local_instance(); let id = generate_activity_id(local_instance.ap_id.inner())?; - let update = UpdateArticle { + let update = UpdateRemoteArticle { actor: local_instance.ap_id.clone(), to: vec![article_instance.ap_id.into_inner()], object: edit.into_json(data).await?, @@ -68,8 +52,9 @@ impl UpdateArticle { Ok(()) } } + #[async_trait::async_trait] -impl ActivityHandler for UpdateArticle { +impl ActivityHandler for UpdateRemoteArticle { type DataType = DatabaseHandle; type Error = crate::error::Error; @@ -85,23 +70,30 @@ impl ActivityHandler for UpdateArticle { Ok(()) } + /// Received on article origin instances async fn receive(self, data: &Data) -> Result<(), Self::Error> { - if DbEdit::from_json(self.object.clone(), data).await.is_ok() { - let article_local = { - let lock = data.articles.lock().unwrap(); - let article = lock.get(self.object.object.inner()).unwrap(); - article.local - }; + match DbEdit::from_json(self.object.clone(), data).await { + Ok(edit) => { + let article = { + let lock = data.articles.lock().unwrap(); + let article = lock.get(self.object.object.inner()).unwrap(); + article.clone() + }; + { + let patch = Patch::from_str(&edit.diff)?; + let applied = apply(&article.text, &patch)?; + let mut lock = data.articles.lock().unwrap(); + let article = lock.get_mut(edit.article_id.inner()).unwrap(); + article.edits.push(edit.clone()); + article.text = applied; + } - if article_local { - // No need to wrap in announce, we can construct a new activity as all important info - // is in the object and result fields. let local_instance = data.local_instance(); let id = generate_activity_id(local_instance.ap_id.inner())?; - let update = UpdateArticle { + let update = UpdateLocalArticle { actor: local_instance.ap_id.clone(), to: local_instance.follower_ids(), - object: self.object, + object: article.clone().into_json(data).await?, kind: Default::default(), id, }; @@ -109,9 +101,10 @@ impl ActivityHandler for UpdateArticle { .send_to_followers(update, data) .await?; } - } else { - let user_instance = self.actor.dereference(data).await?; - RejectEdit::send(self.object.clone(), user_instance, data).await?; + Err(_e) => { + let user_instance = self.actor.dereference(data).await?; + RejectEdit::send(self.object.clone(), user_instance, data).await?; + } } Ok(()) diff --git a/src/federation/objects/article.rs b/src/federation/objects/article.rs index b88abcc..d77541e 100644 --- a/src/federation/objects/article.rs +++ b/src/federation/objects/article.rs @@ -42,7 +42,7 @@ pub struct ApubArticle { pub(crate) attributed_to: ObjectId, #[serde(deserialize_with = "deserialize_one_or_many")] pub(crate) to: Vec, - edits: CollectionId, + pub edits: CollectionId, latest_version: EditVersion, content: String, name: String, diff --git a/src/federation/objects/edit.rs b/src/federation/objects/edit.rs index d0739ce..d3654d6 100644 --- a/src/federation/objects/edit.rs +++ b/src/federation/objects/edit.rs @@ -5,7 +5,7 @@ use crate::federation::objects::article::DbArticle; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::traits::Object; -use diffy::{apply, create_patch, Patch}; +use diffy::create_patch; use serde::{Deserialize, Serialize}; use sha2::Digest; use sha2::Sha224; @@ -104,21 +104,9 @@ impl Object for DbEdit { version: json.version, local: false, }; - let article_read = { - let lock = data.articles.lock().unwrap(); - lock.get(edit.article_id.inner()).unwrap().clone() - }; - let patch = Patch::from_str(&edit.diff)?; - // Dont apply the edit if we already fetched an update Article version. - // TODO: this assumes that we always receive edits in the correct order, probably need to - // include the parent for each edit - //if article_read.latest_version != edit.version { - let applied = apply(&article_read.text, &patch)?; let mut lock = data.articles.lock().unwrap(); let article = lock.get_mut(edit.article_id.inner()).unwrap(); article.edits.push(edit.clone()); - article.text = applied; Ok(edit) - //} } } diff --git a/src/federation/routes.rs b/src/federation/routes.rs index 1c17447..ac0632c 100644 --- a/src/federation/routes.rs +++ b/src/federation/routes.rs @@ -14,7 +14,8 @@ use axum::extract::Path; use crate::federation::activities::create_article::CreateArticle; use crate::federation::activities::reject::RejectEdit; -use crate::federation::activities::update_article::UpdateArticle; +use crate::federation::activities::update_local_article::UpdateLocalArticle; +use crate::federation::activities::update_remote_article::UpdateRemoteArticle; use crate::federation::objects::article::ApubArticle; use crate::federation::objects::articles_collection::{ArticleCollection, DbArticleCollection}; use crate::federation::objects::edits_collection::{ApubEditCollection, DbEditCollection}; @@ -85,7 +86,8 @@ pub enum InboxActivities { Follow(Follow), Accept(Accept), CreateArticle(CreateArticle), - UpdateArticle(UpdateArticle), + UpdateLocalArticle(UpdateLocalArticle), + UpdateRemoteArticle(UpdateRemoteArticle), RejectEdit(RejectEdit), }