diff --git a/src/api.rs b/src/api.rs index 4187296..b3828a5 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,10 +1,10 @@ use crate::database::DatabaseHandle; -use crate::error::{MyResult}; +use crate::error::MyResult; use crate::federation::activities::create_article::CreateArticle; use crate::federation::activities::update_article::UpdateArticle; -use crate::federation::objects::article::{DbArticle}; +use crate::federation::objects::article::DbArticle; use crate::federation::objects::edit::DbEdit; -use crate::federation::objects::instance::{DbInstance}; +use crate::federation::objects::instance::DbInstance; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; @@ -75,24 +75,34 @@ pub struct EditArticleData { async fn edit_article( data: Data, Form(edit_article): Form, -) -> MyResult> { +) -> MyResult<()> { 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.clone()); - article.clone() - }; + if original_article.local { + 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.clone()); + article.clone() + }; - UpdateArticle::send_to_followers(updated_article.clone(), edit, &data).await?; + UpdateArticle::send_to_followers(edit, updated_article.clone(), &data).await?; + } else { + UpdateArticle::send_to_origin( + edit, + // TODO: should be dereference(), but then article is refetched which breaks test_edit_conflict() + original_article.instance.dereference_local(&data).await?, + &data, + ) + .await?; + } - Ok(Json(updated_article)) + Ok(()) } #[derive(Deserialize, Serialize, Clone)] diff --git a/src/federation/activities/update_article.rs b/src/federation/activities/update_article.rs index be16a6c..eaf00e8 100644 --- a/src/federation/activities/update_article.rs +++ b/src/federation/activities/update_article.rs @@ -29,34 +29,41 @@ pub struct UpdateArticle { impl UpdateArticle { pub async fn send_to_followers( - article: DbArticle, 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( + edit: DbEdit, + article_instance: DbInstance, data: &Data, ) -> MyResult<()> { let local_instance = data.local_instance(); let id = generate_activity_id(local_instance.ap_id.inner())?; - if article.local { - 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?; - } 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: edit.into_json(data).await?, - kind: Default::default(), - id, - }; - local_instance - .send(update, vec![article_instance.inbox], data) - .await?; - } + let update = UpdateArticle { + actor: local_instance.ap_id.clone(), + to: vec![article_instance.ap_id.into_inner()], + object: edit.into_json(data).await?, + kind: Default::default(), + id, + }; + local_instance + .send(update, vec![article_instance.inbox], data) + .await?; Ok(()) } } diff --git a/src/federation/objects/edit.rs b/src/federation/objects/edit.rs index 42578c6..f50bf1d 100644 --- a/src/federation/objects/edit.rs +++ b/src/federation/objects/edit.rs @@ -91,6 +91,8 @@ impl Object for DbEdit { let article = lock.get_mut(edit.article_id.inner()).unwrap(); article.edits.push(edit.clone()); let patch = Patch::from_str(&edit.diff)?; + // TODO: this will give wrong result if new article text is federated, and then also new + // edit is applied. probably need to keep track of versions article.text = apply(&article.text, &patch)?; Ok(edit) diff --git a/src/federation/objects/instance.rs b/src/federation/objects/instance.rs index fddd21c..f03bd63 100644 --- a/src/federation/objects/instance.rs +++ b/src/federation/objects/instance.rs @@ -13,6 +13,7 @@ use activitypub_federation::{ use chrono::{DateTime, Local, Utc}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; +use tracing::warn; use url::Url; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -94,7 +95,10 @@ impl DbInstance { let activity = WithContext::new_default(activity); let sends = SendActivityTask::prepare(&activity, self, recipients, data).await?; for send in sends { - send.sign_and_send(data).await?; + let send = send.sign_and_send(data).await; + if let Err(e) = send { + warn!("Failed to send activity {:?}: {e}", activity); + } } Ok(()) } diff --git a/tests/common.rs b/tests/common.rs index 46665ac..c31f239 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,5 +1,6 @@ -use fediwiki::api::{FollowInstance, ResolveObject}; +use fediwiki::api::{EditArticleData, FollowInstance, GetArticleData, ResolveObject}; use fediwiki::error::MyResult; +use fediwiki::federation::objects::article::DbArticle; use fediwiki::federation::objects::instance::DbInstance; use fediwiki::start; use once_cell::sync::Lazy; @@ -63,6 +64,24 @@ impl TestData { } } +pub async fn edit_article( + hostname: &str, + title: &str, + edit_form: &EditArticleData, +) -> MyResult { + CLIENT + .patch(format!("http://{}/api/v1/article", hostname)) + .form(edit_form) + .send() + .await?; + let get_article = GetArticleData { + title: title.to_string(), + }; + let updated_article: DbArticle = + get_query(hostname, &"article".to_string(), Some(get_article)).await?; + Ok(updated_article) +} + pub async fn get(hostname: &str, endpoint: &str) -> MyResult where T: for<'de> Deserialize<'de>, @@ -96,19 +115,6 @@ where .await?) } -pub async fn patch(hostname: &str, endpoint: &str, form: &T) -> MyResult -where - R: for<'de> Deserialize<'de>, -{ - Ok(CLIENT - .patch(format!("http://{}/api/v1/{}", hostname, endpoint)) - .form(form) - .send() - .await? - .json() - .await?) -} - pub async fn follow_instance(follow_instance: &str, followed_instance: &str) -> MyResult<()> { // fetch beta instance on alpha let resolve_form = ResolveObject { diff --git a/tests/test.rs b/tests/test.rs index 27a0545..ae09bf9 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -2,7 +2,7 @@ extern crate fediwiki; mod common; -use crate::common::{follow_instance, get_query, patch, post, TestData}; +use crate::common::{edit_article, follow_instance, get_query, post, TestData}; use common::get; use fediwiki::api::{CreateArticleData, EditArticleData, GetArticleData, ResolveObject}; use fediwiki::error::MyResult; @@ -52,7 +52,7 @@ async fn test_create_read_and_edit_article() -> MyResult<()> { 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?; + let edit_res = edit_article(data.hostname_alpha, &create_res.title, &edit_form).await?; assert_eq!(edit_form.new_text, edit_res.text); assert_eq!(1, edit_res.edits.len()); @@ -163,7 +163,7 @@ async fn test_edit_local_article() -> MyResult<()> { ap_id: create_res.ap_id, new_text: "Lorem Ipsum 2".to_string(), }; - let edit_res: DbArticle = patch(data.hostname_beta, "article", &edit_form).await?; + let edit_res = edit_article(data.hostname_beta, &create_res.title, &edit_form).await?; assert_eq!(edit_res.text, edit_form.new_text); assert_eq!(edit_res.edits.len(), 1); assert!(edit_res.edits[0] @@ -222,7 +222,7 @@ async fn test_edit_remote_article() -> MyResult<()> { 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?; + let edit_res = edit_article(data.hostname_alpha, &create_res.title, &edit_form).await?; assert_eq!(edit_res.text, edit_form.new_text); assert_eq!(edit_res.edits.len(), 1); assert!(!edit_res.local); @@ -266,12 +266,20 @@ async fn test_edit_conflict() -> MyResult<()> { assert_eq!(create_res.title, create_form.title); assert!(create_res.local); + // fetch article to gamma + let resolve_object = ResolveObject { + id: create_res.ap_id.inner().clone(), + }; + let resolve_res: DbArticle = + get_query(data.hostname_gamma, "resolve_article", Some(resolve_object)).await?; + assert_eq!(create_res.text, resolve_res.text); + // alpha edits article let edit_form = EditArticleData { ap_id: create_res.ap_id.clone(), new_text: "Lorem Ipsum".to_string(), }; - let edit_res: DbArticle = patch(data.hostname_alpha, "article", &edit_form).await?; + let edit_res = edit_article(data.hostname_alpha, &create_res.title, &edit_form).await?; assert_eq!(edit_res.text, edit_form.new_text); assert_eq!(edit_res.edits.len(), 1); assert!(!edit_res.local); @@ -280,28 +288,15 @@ async fn test_edit_conflict() -> MyResult<()> { .to_string() .starts_with(&edit_res.ap_id.to_string())); - // fetch article to gamma - let resolve_object = ResolveObject { - id: create_res.ap_id.inner().clone(), - }; - get_query::(data.hostname_gamma, "resolve_article", Some(resolve_object)).await?; - // gamma also edits, as its not the latest version there is a conflict - // TODO: get this working let edit_form = EditArticleData { ap_id: create_res.ap_id, - new_text: "Ipsum Lorem".to_string(), + new_text: "aaaa".to_string(), }; - dbg!(&edit_form); - let edit_res: DbArticle = dbg!(patch(data.hostname_gamma, "article", &edit_form).await)?; - dbg!(&edit_res); - assert_eq!(edit_res.text, edit_form.new_text); - assert_eq!(edit_res.edits.len(), 1); + let edit_res = edit_article(data.hostname_gamma, &create_res.title, &edit_form).await?; + assert_eq!(create_res.text, edit_res.text); + assert_eq!(0, edit_res.edits.len()); assert!(!edit_res.local); - assert!(edit_res.edits[0] - .id - .to_string() - .starts_with(&edit_res.ap_id.to_string())); data.stop() }