diff --git a/Cargo.lock b/Cargo.lock index 24b532a..bb3b9cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -625,6 +625,7 @@ dependencies = [ "serde", "serde_json", "serial_test", + "sha2", "tokio", "tracing", "url", diff --git a/Cargo.toml b/Cargo.toml index da24dc5..b806626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ futures = "0.3.29" rand = "0.8.5" serde = "1.0.192" serde_json = "1.0.108" +sha2 = "0.10.8" tokio = { version = "1.34.0", features = ["full"] } tracing = "0.1.40" url = "2.4.1" diff --git a/src/api.rs b/src/api.rs index 93e594a..c11467a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -15,6 +15,7 @@ use axum::{Form, Json, Router}; use axum_macros::debug_handler; use serde::{Deserialize, Serialize}; use url::Url; +use crate::federation::objects::edit::DbEdit; pub fn api_routes() -> Router { Router::new() @@ -82,21 +83,28 @@ async fn edit_article( data: Data, Form(edit_article): Form, ) -> MyResult> { - let article = { + let original_article = { + let mut lock = data.articles.lock().unwrap(); + let article = lock.get_mut(edit_article.ap_id.inner()).unwrap(); + article.clone() + }; + let edit = DbEdit::new(&original_article, &edit_article.new_text)?; + let updated_article = { let mut lock = data.articles.lock().unwrap(); let article = lock.get_mut(edit_article.ap_id.inner()).unwrap(); article.text = edit_article.new_text; + article.edits.push(edit); article.clone() }; CreateOrUpdateArticle::send_to_local_followers( - article.clone(), + updated_article.clone(), CreateOrUpdateType::Update, &data, ) .await?; - Ok(Json(article)) + Ok(Json(updated_article)) } #[derive(Deserialize, Serialize, Clone)] diff --git a/src/federation/objects/edit.rs b/src/federation/objects/edit.rs index b67d8e1..cadb9b6 100644 --- a/src/federation/objects/edit.rs +++ b/src/federation/objects/edit.rs @@ -1,10 +1,13 @@ use crate::database::DatabaseHandle; -use crate::error::Error; +use crate::error::{Error, MyResult}; 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::create_patch; use serde::{Deserialize, Serialize}; +use sha2::Sha224; +use sha2::{Digest}; use url::Url; /// Represents a single change to the article. @@ -15,6 +18,21 @@ pub struct DbEdit { pub local: bool, } +impl DbEdit { + pub fn new(original_article: &DbArticle, updated_text: &str) -> MyResult { + let diff = create_patch(&original_article.text, updated_text); + let mut sha224 = Sha224::new(); + sha224.update(diff.to_bytes()); + let hash = format!("{:X}", sha224.finalize()); + let edit_id = ObjectId::parse(&format!("{}/{}", original_article.ap_id, hash))?; + Ok(DbEdit { + id: edit_id, + diff: diff.to_string(), + local: true + }) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub enum EditType { Edit, @@ -26,7 +44,6 @@ pub struct ApubEdit { #[serde(rename = "type")] kind: EditType, id: ObjectId, - article_id: ObjectId, diff: String, } @@ -44,7 +61,11 @@ impl Object for DbEdit { } async fn into_json(self, _data: &Data) -> Result { - todo!() + Ok(ApubEdit { + kind: EditType::Edit, + id: self.id, + diff: self.diff, + }) } async fn verify( @@ -52,13 +73,17 @@ impl Object for DbEdit { _expected_domain: &Url, _data: &Data, ) -> Result<(), Self::Error> { - todo!() + Ok(()) } async fn from_json( - _json: Self::Kind, + json: Self::Kind, _data: &Data, ) -> Result { - todo!() + Ok(Self { + id: json.id, + diff: json.diff, + local: false, + }) } } diff --git a/src/federation/objects/edits_collection.rs b/src/federation/objects/edits_collection.rs index bae2570..e6fda59 100644 --- a/src/federation/objects/edits_collection.rs +++ b/src/federation/objects/edits_collection.rs @@ -75,9 +75,11 @@ impl Collection for DbEditCollection { try_join_all(apub.items.into_iter().map(|i| DbEdit::from_json(i, data))).await?; let mut articles = data.articles.lock().unwrap(); let article = articles.get_mut(owner.ap_id.inner()).unwrap(); + let edit_ids = article.edits.iter().map(|e| e.id.clone()).collect::>(); for e in edits.clone() { - // TODO: edits need a unique id to avoid pushing duplicates - article.edits.push(e); + if !edit_ids.contains(&&e.id) { + article.edits.push(e); + } } // TODO: return value propably not needed Ok(DbEditCollection(edits)) diff --git a/tests/test.rs b/tests/test.rs index cb60d0b..7278455 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -153,6 +153,8 @@ async fn test_federate_article_changes() -> MyResult<()> { }; let edit_res: DbArticle = patch(data.hostname_beta, "article", &edit_form).await?; assert_eq!(edit_res.text, edit_form.new_text); + assert_eq!(edit_res.edits.len(), 1); + assert!(edit_res.edits[0].id.to_string().starts_with(&edit_res.ap_id.to_string())); // edit should be federated to alpha let get_article = GetArticle {