mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-29 13:01:09 +00:00
dont apply edit to already updated text
This commit is contained in:
parent
d224016327
commit
1539795f03
5 changed files with 50 additions and 17 deletions
|
@ -3,7 +3,7 @@ use crate::error::MyResult;
|
||||||
use crate::federation::activities::create_article::CreateArticle;
|
use crate::federation::activities::create_article::CreateArticle;
|
||||||
use crate::federation::activities::update_article::UpdateArticle;
|
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::edit::{DbEdit, EditVersion};
|
||||||
use crate::federation::objects::instance::DbInstance;
|
use crate::federation::objects::instance::DbInstance;
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
|
@ -40,17 +40,17 @@ async fn create_article(
|
||||||
Form(create_article): Form<CreateArticleData>,
|
Form(create_article): Form<CreateArticleData>,
|
||||||
) -> MyResult<Json<DbArticle>> {
|
) -> MyResult<Json<DbArticle>> {
|
||||||
let local_instance_id = data.local_instance().ap_id;
|
let local_instance_id = data.local_instance().ap_id;
|
||||||
let ap_id = Url::parse(&format!(
|
let ap_id = ObjectId::parse(&format!(
|
||||||
"http://{}:{}/article/{}",
|
"http://{}:{}/article/{}",
|
||||||
local_instance_id.inner().domain().unwrap(),
|
local_instance_id.inner().domain().unwrap(),
|
||||||
local_instance_id.inner().port().unwrap(),
|
local_instance_id.inner().port().unwrap(),
|
||||||
create_article.title
|
create_article.title
|
||||||
))?
|
))?;
|
||||||
.into();
|
|
||||||
let article = DbArticle {
|
let article = DbArticle {
|
||||||
title: create_article.title,
|
title: create_article.title,
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
ap_id,
|
ap_id,
|
||||||
|
latest_version: EditVersion::default(),
|
||||||
edits: vec![],
|
edits: vec![],
|
||||||
instance: local_instance_id,
|
instance: local_instance_id,
|
||||||
local: true,
|
local: true,
|
||||||
|
@ -87,6 +87,7 @@ async fn edit_article(
|
||||||
let mut lock = data.articles.lock().unwrap();
|
let mut lock = data.articles.lock().unwrap();
|
||||||
let article = lock.get_mut(edit_article.ap_id.inner()).unwrap();
|
let article = lock.get_mut(edit_article.ap_id.inner()).unwrap();
|
||||||
article.text = edit_article.new_text;
|
article.text = edit_article.new_text;
|
||||||
|
article.latest_version = edit.version.clone();
|
||||||
article.edits.push(edit.clone());
|
article.edits.push(edit.clone());
|
||||||
article.clone()
|
article.clone()
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::database::{Database, DatabaseHandle};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::federation::objects::instance::DbInstance;
|
use crate::federation::objects::instance::DbInstance;
|
||||||
use activitypub_federation::config::FederationConfig;
|
use activitypub_federation::config::FederationConfig;
|
||||||
|
use activitypub_federation::fetch::collection_id::CollectionId;
|
||||||
use activitypub_federation::http_signatures::generate_actor_keypair;
|
use activitypub_federation::http_signatures::generate_actor_keypair;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -14,7 +15,7 @@ pub mod routes;
|
||||||
|
|
||||||
pub async fn federation_config(hostname: &str) -> Result<FederationConfig<DatabaseHandle>, Error> {
|
pub async fn federation_config(hostname: &str) -> Result<FederationConfig<DatabaseHandle>, Error> {
|
||||||
let ap_id = Url::parse(&format!("http://{}", hostname))?.into();
|
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 inbox = Url::parse(&format!("http://{}/inbox", hostname))?;
|
||||||
let keypair = generate_actor_keypair()?;
|
let keypair = generate_actor_keypair()?;
|
||||||
let local_instance = DbInstance {
|
let local_instance = DbInstance {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::error::MyResult;
|
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::edits_collection::DbEditCollection;
|
||||||
use crate::federation::objects::instance::DbInstance;
|
use crate::federation::objects::instance::DbInstance;
|
||||||
use crate::{database::DatabaseHandle, error::Error};
|
use crate::{database::DatabaseHandle, error::Error};
|
||||||
|
@ -23,14 +23,13 @@ pub struct DbArticle {
|
||||||
pub instance: ObjectId<DbInstance>,
|
pub instance: ObjectId<DbInstance>,
|
||||||
/// List of all edits which make up this article, oldest first.
|
/// List of all edits which make up this article, oldest first.
|
||||||
pub edits: Vec<DbEdit>,
|
pub edits: Vec<DbEdit>,
|
||||||
|
pub latest_version: EditVersion,
|
||||||
pub local: bool,
|
pub local: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DbArticle {
|
impl DbArticle {
|
||||||
fn edits_id(&self) -> MyResult<CollectionId<DbEditCollection>> {
|
fn edits_id(&self) -> MyResult<CollectionId<DbEditCollection>> {
|
||||||
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))
|
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?)
|
||||||
.unwrap()
|
|
||||||
.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +43,7 @@ pub struct ApubArticle {
|
||||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||||
pub(crate) to: Vec<Url>,
|
pub(crate) to: Vec<Url>,
|
||||||
edits: CollectionId<DbEditCollection>,
|
edits: CollectionId<DbEditCollection>,
|
||||||
|
latest_version: EditVersion,
|
||||||
content: String,
|
content: String,
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,7 @@ impl Object for DbArticle {
|
||||||
attributed_to: self.instance.clone(),
|
attributed_to: self.instance.clone(),
|
||||||
to: vec![public(), instance.followers_url()?],
|
to: vec![public(), instance.followers_url()?],
|
||||||
edits: self.edits_id()?,
|
edits: self.edits_id()?,
|
||||||
|
latest_version: self.latest_version,
|
||||||
content: self.text,
|
content: self.text,
|
||||||
name: self.title,
|
name: self.title,
|
||||||
})
|
})
|
||||||
|
@ -97,6 +98,7 @@ impl Object for DbArticle {
|
||||||
instance: json.attributed_to,
|
instance: json.attributed_to,
|
||||||
// TODO: shouldnt overwrite existing edits
|
// TODO: shouldnt overwrite existing edits
|
||||||
edits: vec![],
|
edits: vec![],
|
||||||
|
latest_version: json.latest_version,
|
||||||
local: false,
|
local: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,7 +107,7 @@ impl Object for DbArticle {
|
||||||
lock.insert(article.ap_id.inner().clone(), article.clone());
|
lock.insert(article.ap_id.inner().clone(), article.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
json.edits.dereference(&article, &data).await?;
|
json.edits.dereference(&article, data).await?;
|
||||||
|
|
||||||
Ok(article)
|
Ok(article)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,24 @@ use sha2::Digest;
|
||||||
use sha2::Sha224;
|
use sha2::Sha224;
|
||||||
use url::Url;
|
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.
|
/// Represents a single change to the article.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct DbEdit {
|
pub struct DbEdit {
|
||||||
pub id: ObjectId<DbEdit>,
|
pub id: ObjectId<DbEdit>,
|
||||||
pub diff: String,
|
pub diff: String,
|
||||||
pub article_id: ObjectId<DbArticle>,
|
pub article_id: ObjectId<DbArticle>,
|
||||||
|
pub version: EditVersion,
|
||||||
pub local: bool,
|
pub local: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +42,7 @@ impl DbEdit {
|
||||||
id: edit_id,
|
id: edit_id,
|
||||||
diff: diff.to_string(),
|
diff: diff.to_string(),
|
||||||
article_id: original_article.ap_id.clone(),
|
article_id: original_article.ap_id.clone(),
|
||||||
|
version: EditVersion(hash),
|
||||||
local: true,
|
local: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -46,7 +59,8 @@ pub struct ApubEdit {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
kind: EditType,
|
kind: EditType,
|
||||||
id: ObjectId<DbEdit>,
|
id: ObjectId<DbEdit>,
|
||||||
pub(crate) content: String,
|
pub content: String,
|
||||||
|
pub version: EditVersion,
|
||||||
pub object: ObjectId<DbArticle>,
|
pub object: ObjectId<DbArticle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +82,7 @@ impl Object for DbEdit {
|
||||||
kind: EditType::Edit,
|
kind: EditType::Edit,
|
||||||
id: self.id,
|
id: self.id,
|
||||||
content: self.diff,
|
content: self.diff,
|
||||||
|
version: self.version,
|
||||||
object: self.article_id,
|
object: self.article_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -85,15 +100,19 @@ impl Object for DbEdit {
|
||||||
id: json.id,
|
id: json.id,
|
||||||
diff: json.content,
|
diff: json.content,
|
||||||
article_id: json.object,
|
article_id: json.object,
|
||||||
|
version: json.version,
|
||||||
local: false,
|
local: false,
|
||||||
};
|
};
|
||||||
let mut lock = data.articles.lock().unwrap();
|
let mut lock = data.articles.lock().unwrap();
|
||||||
let article = lock.get_mut(edit.article_id.inner()).unwrap();
|
let article = lock.get_mut(edit.article_id.inner()).unwrap();
|
||||||
article.edits.push(edit.clone());
|
article.edits.push(edit.clone());
|
||||||
let patch = Patch::from_str(&edit.diff)?;
|
let patch = Patch::from_str(&edit.diff)?;
|
||||||
// TODO: this will give wrong result if new article text is federated, and then also new
|
// Dont apply the edit if we already fetched an update Article version.
|
||||||
// edit is applied. probably need to keep track of versions
|
// 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)?;
|
article.text = apply(&article.text, &patch)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(edit)
|
Ok(edit)
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,13 @@ async fn test_synchronize_articles() -> MyResult<()> {
|
||||||
assert_eq!(0, create_res.edits.len());
|
assert_eq!(0, create_res.edits.len());
|
||||||
assert!(create_res.local);
|
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
|
// article is not yet on beta
|
||||||
let get_res = get_article(data.hostname_beta, &create_res.title).await;
|
let get_res = get_article(data.hostname_beta, &create_res.title).await;
|
||||||
assert!(get_res.is_err());
|
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?;
|
let get_res = get_article(data.hostname_beta, &create_res.title).await?;
|
||||||
assert_eq!(create_res.ap_id, get_res.ap_id);
|
assert_eq!(create_res.ap_id, get_res.ap_id);
|
||||||
assert_eq!(title, get_res.title);
|
assert_eq!(title, get_res.title);
|
||||||
assert_eq!(0, get_res.edits.len());
|
assert_eq!(1, get_res.edits.len());
|
||||||
assert!(get_res.text.is_empty());
|
assert_eq!(edit_form.new_text, get_res.text);
|
||||||
assert!(!get_res.local);
|
assert!(!get_res.local);
|
||||||
|
|
||||||
data.stop()
|
data.stop()
|
||||||
|
@ -231,7 +238,8 @@ async fn test_edit_conflict() -> MyResult<()> {
|
||||||
.to_string()
|
.to_string()
|
||||||
.starts_with(&edit_res.ap_id.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 {
|
let edit_form = EditArticleData {
|
||||||
ap_id: create_res.ap_id,
|
ap_id: create_res.ap_id,
|
||||||
new_text: "aaaa".to_string(),
|
new_text: "aaaa".to_string(),
|
||||||
|
@ -241,5 +249,7 @@ async fn test_edit_conflict() -> MyResult<()> {
|
||||||
assert_eq!(0, edit_res.edits.len());
|
assert_eq!(0, edit_res.edits.len());
|
||||||
assert!(!edit_res.local);
|
assert!(!edit_res.local);
|
||||||
|
|
||||||
|
// TODO: need to federate the conflict as `Reject` and then resolve it
|
||||||
|
|
||||||
data.stop()
|
data.stop()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue