dont apply edit to already updated text

This commit is contained in:
Felix Ableitner 2023-11-24 16:09:17 +01:00
parent d224016327
commit 1539795f03
5 changed files with 50 additions and 17 deletions

View File

@ -3,7 +3,7 @@ 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::edit::DbEdit;
use crate::federation::objects::edit::{DbEdit, EditVersion};
use crate::federation::objects::instance::DbInstance;
use activitypub_federation::config::Data;
use activitypub_federation::fetch::object_id::ObjectId;
@ -40,17 +40,17 @@ async fn create_article(
Form(create_article): Form<CreateArticleData>,
) -> MyResult<Json<DbArticle>> {
let local_instance_id = data.local_instance().ap_id;
let ap_id = Url::parse(&format!(
let ap_id = ObjectId::parse(&format!(
"http://{}:{}/article/{}",
local_instance_id.inner().domain().unwrap(),
local_instance_id.inner().port().unwrap(),
create_article.title
))?
.into();
))?;
let article = DbArticle {
title: create_article.title,
text: String::new(),
ap_id,
latest_version: EditVersion::default(),
edits: vec![],
instance: local_instance_id,
local: true,
@ -87,6 +87,7 @@ async fn edit_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.latest_version = edit.version.clone();
article.edits.push(edit.clone());
article.clone()
};

View File

@ -2,6 +2,7 @@ use crate::database::{Database, DatabaseHandle};
use crate::error::Error;
use crate::federation::objects::instance::DbInstance;
use activitypub_federation::config::FederationConfig;
use activitypub_federation::fetch::collection_id::CollectionId;
use activitypub_federation::http_signatures::generate_actor_keypair;
use chrono::Local;
use std::collections::HashMap;
@ -14,7 +15,7 @@ pub mod routes;
pub async fn federation_config(hostname: &str) -> Result<FederationConfig<DatabaseHandle>, Error> {
let ap_id = Url::parse(&format!("http://{}", hostname))?.into();
let articles_id = Url::parse(&format!("http://{}/all_articles", hostname))?.into();
let articles_id = CollectionId::parse(&format!("http://{}/all_articles", hostname))?;
let inbox = Url::parse(&format!("http://{}/inbox", hostname))?;
let keypair = generate_actor_keypair()?;
let local_instance = DbInstance {

View File

@ -1,5 +1,5 @@
use crate::error::MyResult;
use crate::federation::objects::edit::DbEdit;
use crate::federation::objects::edit::{DbEdit, EditVersion};
use crate::federation::objects::edits_collection::DbEditCollection;
use crate::federation::objects::instance::DbInstance;
use crate::{database::DatabaseHandle, error::Error};
@ -23,14 +23,13 @@ pub struct DbArticle {
pub instance: ObjectId<DbInstance>,
/// List of all edits which make up this article, oldest first.
pub edits: Vec<DbEdit>,
pub latest_version: EditVersion,
pub local: bool,
}
impl DbArticle {
fn edits_id(&self) -> MyResult<CollectionId<DbEditCollection>> {
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))
.unwrap()
.into())
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?)
}
}
@ -44,6 +43,7 @@ pub struct ApubArticle {
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
edits: CollectionId<DbEditCollection>,
latest_version: EditVersion,
content: String,
name: String,
}
@ -75,6 +75,7 @@ impl Object for DbArticle {
attributed_to: self.instance.clone(),
to: vec![public(), instance.followers_url()?],
edits: self.edits_id()?,
latest_version: self.latest_version,
content: self.text,
name: self.title,
})
@ -97,6 +98,7 @@ impl Object for DbArticle {
instance: json.attributed_to,
// TODO: shouldnt overwrite existing edits
edits: vec![],
latest_version: json.latest_version,
local: false,
};
@ -105,7 +107,7 @@ impl Object for DbArticle {
lock.insert(article.ap_id.inner().clone(), article.clone());
}
json.edits.dereference(&article, &data).await?;
json.edits.dereference(&article, data).await?;
Ok(article)
}

View File

@ -10,12 +10,24 @@ use sha2::Digest;
use sha2::Sha224;
use url::Url;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct EditVersion(String);
impl Default for EditVersion {
fn default() -> Self {
let sha224 = Sha224::new();
let hash = format!("{:X}", sha224.finalize());
EditVersion(hash)
}
}
/// Represents a single change to the article.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DbEdit {
pub id: ObjectId<DbEdit>,
pub diff: String,
pub article_id: ObjectId<DbArticle>,
pub version: EditVersion,
pub local: bool,
}
@ -30,6 +42,7 @@ impl DbEdit {
id: edit_id,
diff: diff.to_string(),
article_id: original_article.ap_id.clone(),
version: EditVersion(hash),
local: true,
})
}
@ -46,7 +59,8 @@ pub struct ApubEdit {
#[serde(rename = "type")]
kind: EditType,
id: ObjectId<DbEdit>,
pub(crate) content: String,
pub content: String,
pub version: EditVersion,
pub object: ObjectId<DbArticle>,
}
@ -68,6 +82,7 @@ impl Object for DbEdit {
kind: EditType::Edit,
id: self.id,
content: self.diff,
version: self.version,
object: self.article_id,
})
}
@ -85,15 +100,19 @@ impl Object for DbEdit {
id: json.id,
diff: json.content,
article_id: json.object,
version: json.version,
local: false,
};
let mut lock = data.articles.lock().unwrap();
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)?;
// 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.latest_version != edit.version {
article.text = apply(&article.text, &patch)?;
}
Ok(edit)
}

View File

@ -81,6 +81,13 @@ async fn test_synchronize_articles() -> MyResult<()> {
assert_eq!(0, create_res.edits.len());
assert!(create_res.local);
// edit the article
let edit_form = EditArticleData {
ap_id: create_res.ap_id.clone(),
new_text: "Lorem Ipsum 2".to_string(),
};
edit_article(data.hostname_alpha, &title, &edit_form).await?;
// article is not yet on beta
let get_res = get_article(data.hostname_beta, &create_res.title).await;
assert!(get_res.is_err());
@ -96,8 +103,8 @@ async fn test_synchronize_articles() -> MyResult<()> {
let get_res = get_article(data.hostname_beta, &create_res.title).await?;
assert_eq!(create_res.ap_id, get_res.ap_id);
assert_eq!(title, get_res.title);
assert_eq!(0, get_res.edits.len());
assert!(get_res.text.is_empty());
assert_eq!(1, get_res.edits.len());
assert_eq!(edit_form.new_text, get_res.text);
assert!(!get_res.local);
data.stop()
@ -231,7 +238,8 @@ async fn test_edit_conflict() -> MyResult<()> {
.to_string()
.starts_with(&edit_res.ap_id.to_string()));
// gamma also edits, as its not the latest version there is a conflict
// gamma also edits, as its not the latest version there is a conflict. local version should
// not be updated with this conflicting version, instead user needs to handle the conflict
let edit_form = EditArticleData {
ap_id: create_res.ap_id,
new_text: "aaaa".to_string(),
@ -241,5 +249,7 @@ async fn test_edit_conflict() -> MyResult<()> {
assert_eq!(0, edit_res.edits.len());
assert!(!edit_res.local);
// TODO: need to federate the conflict as `Reject` and then resolve it
data.stop()
}