diff --git a/Cargo.lock b/Cargo.lock index bb3b9cc..f205217 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 3 [[package]] name = "activitypub_federation" version = "0.5.0-beta.5" -source = "git+https://github.com/LemmyNet/activitypub-federation-rust.git?branch=parse-impl#b80408d80619ac014a5cedf5079967c20058532d" +source = "git+https://github.com/LemmyNet/activitypub-federation-rust.git?branch=parse-impl#2aa64ad1de7943840677f4b96a20a11d38e2be56" dependencies = [ "activitystreams-kinds", "async-trait", diff --git a/src/api.rs b/src/api.rs index 33960f1..fa1fee4 100644 --- a/src/api.rs +++ b/src/api.rs @@ -29,10 +29,8 @@ pub fn api_routes() -> Router { #[derive(Deserialize, Serialize)] pub struct CreateArticleData { pub title: String, - pub text: String, } -// TODO: new article should be created with empty content #[debug_handler] async fn create_article( data: Data, @@ -48,7 +46,7 @@ async fn create_article( .into(); let article = DbArticle { title: create_article.title, - text: create_article.text, + text: String::new(), ap_id, edits: vec![], instance: local_instance_id, diff --git a/src/federation/activities/create_article.rs b/src/federation/activities/create_article.rs index fa869c5..9a2ae33 100644 --- a/src/federation/activities/create_article.rs +++ b/src/federation/activities/create_article.rs @@ -33,16 +33,14 @@ impl CreateArticle { let local_instance = data.local_instance(); let object = article.clone().into_json(data).await?; let id = generate_activity_id(local_instance.ap_id.inner())?; - let create_or_update = CreateArticle { + let create = CreateArticle { actor: local_instance.ap_id.clone(), to: local_instance.follower_ids(), object, kind: Default::default(), id, }; - local_instance - .send_to_followers(create_or_update, data) - .await?; + local_instance.send_to_followers(create, data).await?; Ok(()) } } @@ -64,7 +62,10 @@ impl ActivityHandler for CreateArticle { } async fn receive(self, data: &Data) -> Result<(), Self::Error> { - DbArticle::from_json(self.object, data).await?; + let article = DbArticle::from_json(self.object.clone(), data).await?; + if article.local { + data.local_instance().send_to_followers(self, data).await?; + } Ok(()) } } diff --git a/src/federation/activities/update_article.rs b/src/federation/activities/update_article.rs index 60edb36..c8e8f91 100644 --- a/src/federation/activities/update_article.rs +++ b/src/federation/activities/update_article.rs @@ -36,17 +36,30 @@ impl UpdateArticle { ) -> MyResult<()> { let local_instance = data.local_instance(); let id = generate_activity_id(local_instance.ap_id.inner())?; - let create_or_update = UpdateArticle { - actor: local_instance.ap_id.clone(), - to: local_instance.follower_ids(), - object: article.ap_id, - result: edit.into_json(data).await?, - kind: Default::default(), - id, - }; - local_instance - .send_to_followers(create_or_update, data) - .await?; + if article.local { + let update = UpdateArticle { + actor: local_instance.ap_id.clone(), + to: local_instance.follower_ids(), + object: article.ap_id, + result: edit.into_json(data).await?, + kind: Default::default(), + id, + }; + local_instance.send_to_followers(update, data).await?; + } else { + let article_instance = article.instance.dereference(data).await?; + let update = UpdateArticle { + actor: local_instance.ap_id.clone(), + to: vec![article_instance.ap_id.into_inner()], + object: article.ap_id, + result: edit.into_json(data).await?, + kind: Default::default(), + id, + }; + local_instance + .send(update, vec![article_instance.inbox], data) + .await?; + } Ok(()) } } @@ -68,13 +81,35 @@ impl ActivityHandler for UpdateArticle { } async fn receive(self, data: &Data) -> Result<(), Self::Error> { - let edit = DbEdit::from_json(self.result.clone(), data).await?; - let mut lock = data.articles.lock().unwrap(); - let article = lock.get_mut(self.object.inner()).unwrap(); - article.edits.push(edit); - // TODO: probably better to apply patch inside DbEdit::from_json() - let patch = Patch::from_str(&self.result.diff)?; - article.text = apply(&article.text, &patch)?; + let article_local = { + let edit = DbEdit::from_json(self.result.clone(), data).await?; + let mut lock = data.articles.lock().unwrap(); + let article = lock.get_mut(self.object.inner()).unwrap(); + article.edits.push(edit); + // TODO: probably better to apply patch inside DbEdit::from_json() + let patch = Patch::from_str(&self.result.diff)?; + article.text = apply(&article.text, &patch)?; + article.local + }; + + 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 { + actor: local_instance.ap_id.clone(), + to: local_instance.follower_ids(), + object: self.object, + result: self.result, + kind: Default::default(), + id, + }; + data.local_instance() + .send_to_followers(update, data) + .await?; + } + Ok(()) } } diff --git a/src/federation/objects/articles_collection.rs b/src/federation/objects/articles_collection.rs index 4162f1d..c40cf02 100644 --- a/src/federation/objects/articles_collection.rs +++ b/src/federation/objects/articles_collection.rs @@ -81,10 +81,7 @@ impl Collection for DbArticleCollection { .map(|i| DbArticle::from_json(i, data)), ) .await?; - let mut lock = data.articles.lock().unwrap(); - for a in &articles { - lock.insert(a.ap_id.inner().clone(), a.clone()); - } + // TODO: return value propably not needed Ok(DbArticleCollection(articles)) } diff --git a/tests/common.rs b/tests/common.rs index ac1f3f8..55922bd 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -16,8 +16,10 @@ pub static CLIENT: Lazy = Lazy::new(Client::new); pub struct TestData { pub hostname_alpha: &'static str, pub hostname_beta: &'static str, + pub hostname_gamma: &'static str, handle_alpha: JoinHandle<()>, handle_beta: JoinHandle<()>, + handle_gamma: JoinHandle<()>, } impl TestData { @@ -33,23 +35,30 @@ impl TestData { let hostname_alpha = "localhost:8131"; let hostname_beta = "localhost:8132"; + let hostname_gamma = "localhost:8133"; let handle_alpha = tokio::task::spawn(async { start(hostname_alpha).await.unwrap(); }); let handle_beta = tokio::task::spawn(async { start(hostname_beta).await.unwrap(); }); + let handle_gamma = tokio::task::spawn(async { + start(hostname_gamma).await.unwrap(); + }); Self { hostname_alpha, hostname_beta, + hostname_gamma, handle_alpha, handle_beta, + handle_gamma, } } pub fn stop(self) -> MyResult<()> { self.handle_alpha.abort(); self.handle_beta.abort(); + self.handle_gamma.abort(); Ok(()) } } diff --git a/tests/test.rs b/tests/test.rs index 3d6f2ce..abaedc6 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -13,7 +13,7 @@ use url::Url; #[tokio::test] #[serial] -async fn test_create_and_read_article() -> MyResult<()> { +async fn test_create_read_and_edit_article() -> MyResult<()> { let data = TestData::start(); // error on nonexistent article @@ -31,7 +31,6 @@ async fn test_create_and_read_article() -> MyResult<()> { // create article let create_article = CreateArticleData { title: get_article.title.to_string(), - text: "Lorem ipsum".to_string(), }; let create_res: DbArticle = post(data.hostname_alpha, "article", &create_article).await?; assert_eq!(create_article.title, create_res.title); @@ -45,9 +44,18 @@ async fn test_create_and_read_article() -> MyResult<()> { ) .await?; assert_eq!(create_article.title, get_res.title); - assert_eq!(create_article.text, get_res.text); + assert!(get_res.text.is_empty()); assert!(get_res.local); + // edit article + let edit_form = EditArticleData { + ap_id: create_res.ap_id.clone(), + new_text: "Lorem Ipsum 2".to_string(), + }; + let edit_res: DbArticle = patch(data.hostname_alpha, "article", &edit_form).await?; + assert_eq!(edit_form.new_text, edit_res.text); + assert_eq!(1, edit_res.edits.len()); + data.stop() } @@ -82,10 +90,10 @@ async fn test_synchronize_articles() -> MyResult<()> { // create article on alpha let create_article = CreateArticleData { title: "Manu_Chao".to_string(), - text: "Lorem ipsum".to_string(), }; let create_res: DbArticle = post(data.hostname_alpha, "article", &create_article).await?; assert_eq!(create_article.title, create_res.title); + assert_eq!(0, create_res.edits.len()); assert!(create_res.local); // article is not yet on beta @@ -115,7 +123,8 @@ async fn test_synchronize_articles() -> MyResult<()> { .await?; assert_eq!(create_res.ap_id, get_res.ap_id); assert_eq!(create_article.title, get_res.title); - assert_eq!(create_article.text, get_res.text); + assert_eq!(0, get_res.edits.len()); + assert!(get_res.text.is_empty()); assert!(!get_res.local); data.stop() @@ -123,7 +132,7 @@ async fn test_synchronize_articles() -> MyResult<()> { #[tokio::test] #[serial] -async fn test_federate_article_changes() -> MyResult<()> { +async fn test_edit_local_article() -> MyResult<()> { let data = TestData::start(); follow_instance(data.hostname_alpha, data.hostname_beta).await?; @@ -131,10 +140,10 @@ async fn test_federate_article_changes() -> MyResult<()> { // create new article let create_form = CreateArticleData { title: "Manu_Chao".to_string(), - text: "Lorem ipsum".to_string(), }; let create_res: DbArticle = post(data.hostname_beta, "article", &create_form).await?; assert_eq!(create_res.title, create_form.title); + assert!(create_res.local); // article should be federated to alpha let get_article = GetArticleData { @@ -144,6 +153,8 @@ async fn test_federate_article_changes() -> MyResult<()> { get_query::(data.hostname_alpha, "article", Some(get_article.clone())) .await?; assert_eq!(create_res.title, get_res.title); + assert_eq!(0, get_res.edits.len()); + assert!(!get_res.local); assert_eq!(create_res.text, get_res.text); // edit the article @@ -167,6 +178,74 @@ async fn test_federate_article_changes() -> MyResult<()> { get_query::(data.hostname_alpha, "article", Some(get_article.clone())) .await?; assert_eq!(edit_res.title, get_res.title); + assert_eq!(edit_res.edits.len(), 1); + assert_eq!(edit_res.text, get_res.text); + + data.stop() +} + +#[tokio::test] +#[serial] +async fn test_edit_remote_article() -> MyResult<()> { + let data = TestData::start(); + + follow_instance(data.hostname_alpha, data.hostname_beta).await?; + follow_instance(data.hostname_gamma, data.hostname_beta).await?; + + // create new article + let create_form = CreateArticleData { + title: "Manu_Chao".to_string(), + }; + let create_res: DbArticle = post(data.hostname_beta, "article", &create_form).await?; + assert_eq!(create_res.title, create_form.title); + assert!(create_res.local); + + // article should be federated to alpha and gamma + let get_article = GetArticleData { + title: create_res.title.clone(), + }; + let get_res = + get_query::(data.hostname_alpha, "article", Some(get_article.clone())) + .await?; + assert_eq!(create_res.title, get_res.title); + assert_eq!(0, get_res.edits.len()); + assert!(!get_res.local); + assert_eq!(create_res.text, get_res.text); + + let get_res = + get_query::(data.hostname_gamma, "article", Some(get_article.clone())) + .await?; + assert_eq!(create_res.title, get_res.title); + assert_eq!(create_res.text, get_res.text); + + let edit_form = EditArticleData { + ap_id: create_res.ap_id, + new_text: "Lorem Ipsum 2".to_string(), + }; + let edit_res: DbArticle = patch(data.hostname_alpha, "article", &edit_form).await?; + assert_eq!(edit_res.text, edit_form.new_text); + assert_eq!(edit_res.edits.len(), 1); + assert!(!edit_res.local); + assert!(edit_res.edits[0] + .id + .to_string() + .starts_with(&edit_res.ap_id.to_string())); + + // edit should be federated to beta and gamma + let get_article = GetArticleData { + title: edit_res.title.clone(), + }; + let get_res = + get_query::(data.hostname_beta, "article", Some(get_article.clone())).await?; + assert_eq!(edit_res.title, get_res.title); + assert_eq!(edit_res.edits.len(), 1); + assert_eq!(edit_res.text, get_res.text); + + let get_res = + get_query::(data.hostname_gamma, "article", Some(get_article.clone())) + .await?; + assert_eq!(edit_res.title, get_res.title); + assert_eq!(edit_res.edits.len(), 1); assert_eq!(edit_res.text, get_res.text); data.stop()