mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 13:31:08 +00:00
conflict moved to db
This commit is contained in:
parent
37352a3e86
commit
5d2099c17c
19 changed files with 342 additions and 254 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -492,6 +492,7 @@ dependencies = [
|
||||||
"diesel_derives",
|
"diesel_derives",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pq-sys",
|
"pq-sys",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -679,6 +680,7 @@ dependencies = [
|
||||||
"enum_delegate",
|
"enum_delegate",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
|
"hex",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"rand",
|
"rand",
|
||||||
|
@ -688,6 +690,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -924,6 +927,12 @@ version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
@ -2276,11 +2285,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.5.0"
|
version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -10,19 +10,21 @@ async-trait = "0.1.74"
|
||||||
axum = "0.6.20"
|
axum = "0.6.20"
|
||||||
axum-macros = "0.3.8"
|
axum-macros = "0.3.8"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
diesel = {version = "2.1.4", features = ["postgres", "chrono"] }
|
diesel = {version = "2.1.4", features = ["postgres", "chrono", "uuid"] }
|
||||||
diesel-derive-newtype = "2.1.0"
|
diesel-derive-newtype = "2.1.0"
|
||||||
diesel_migrations = "2.1.0"
|
diesel_migrations = "2.1.0"
|
||||||
diffy = "0.3.0"
|
diffy = "0.3.0"
|
||||||
enum_delegate = "0.2.0"
|
enum_delegate = "0.2.0"
|
||||||
env_logger = { version = "0.10.1", default-features = false }
|
env_logger = { version = "0.10.1", default-features = false }
|
||||||
futures = "0.3.29"
|
futures = "0.3.29"
|
||||||
|
hex = "0.4.3"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
serde = "1.0.192"
|
serde = "1.0.192"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
tokio = { version = "1.34.0", features = ["full"] }
|
tokio = { version = "1.34.0", features = ["full"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
url = "2.4.1"
|
url = "2.4.1"
|
||||||
|
uuid = { version = "1.6.1", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
drop table conflict;
|
||||||
drop table edit;
|
drop table edit;
|
||||||
drop table article;
|
drop table article;
|
||||||
drop table instance_follow;
|
drop table instance_follow;
|
||||||
drop table instance;
|
drop table instance;
|
||||||
|
|
|
@ -28,9 +28,16 @@ create table article (
|
||||||
|
|
||||||
create table edit (
|
create table edit (
|
||||||
id serial primary key,
|
id serial primary key,
|
||||||
|
hash uuid not null,
|
||||||
ap_id varchar(255) not null unique,
|
ap_id varchar(255) not null unique,
|
||||||
diff text not null,
|
diff text not null,
|
||||||
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||||
version text not null,
|
previous_version_id uuid not null
|
||||||
previous_version text not null
|
);
|
||||||
)
|
|
||||||
|
create table conflict (
|
||||||
|
id uuid primary key,
|
||||||
|
diff text not null,
|
||||||
|
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||||
|
previous_version_id uuid not null
|
||||||
|
);
|
73
src/api.rs
73
src/api.rs
|
@ -1,7 +1,9 @@
|
||||||
use crate::database::article::{ArticleView, DbArticle, DbArticleForm};
|
use crate::database::article::{ArticleView, DbArticle, DbArticleForm};
|
||||||
use crate::database::edit::{DbEdit, EditVersion};
|
use crate::database::conflict::{ApiConflict, DbConflict, DbConflictForm};
|
||||||
|
use crate::database::edit::{DbEdit, DbEditForm};
|
||||||
use crate::database::instance::{DbInstance, InstanceView};
|
use crate::database::instance::{DbInstance, InstanceView};
|
||||||
use crate::database::{DbConflict, MyDataHandle};
|
use crate::database::version::EditVersion;
|
||||||
|
use crate::database::MyDataHandle;
|
||||||
use crate::error::MyResult;
|
use crate::error::MyResult;
|
||||||
use crate::federation::activities::create_article::CreateArticle;
|
use crate::federation::activities::create_article::CreateArticle;
|
||||||
use crate::federation::activities::follow::Follow;
|
use crate::federation::activities::follow::Follow;
|
||||||
|
@ -9,14 +11,12 @@ use crate::federation::activities::submit_article_update;
|
||||||
use crate::utils::generate_article_version;
|
use crate::utils::generate_article_version;
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
use anyhow::anyhow;
|
|
||||||
use axum::extract::Query;
|
use axum::extract::Query;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{Form, Json, Router};
|
use axum::{Form, Json, Router};
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
use diffy::create_patch;
|
use diffy::create_patch;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use rand::random;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -60,8 +60,7 @@ async fn create_article(
|
||||||
instance_id: local_instance.id,
|
instance_id: local_instance.id,
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
dbg!(&form.ap_id);
|
let article = DbArticle::create(&form, &data.db_connection)?;
|
||||||
let article = dbg!(DbArticle::create(&form, &data.db_connection))?;
|
|
||||||
|
|
||||||
CreateArticle::send_to_followers(article.clone(), &data).await?;
|
CreateArticle::send_to_followers(article.clone(), &data).await?;
|
||||||
|
|
||||||
|
@ -77,17 +76,9 @@ pub struct EditArticleData {
|
||||||
pub new_text: String,
|
pub new_text: String,
|
||||||
/// The version that this edit is based on, ie [DbArticle.latest_version] or
|
/// The version that this edit is based on, ie [DbArticle.latest_version] or
|
||||||
/// [ApiConflict.previous_version]
|
/// [ApiConflict.previous_version]
|
||||||
pub previous_version: EditVersion,
|
pub previous_version_id: EditVersion,
|
||||||
/// If you are resolving a conflict, pass the id to delete conflict from the database
|
/// If you are resolving a conflict, pass the id to delete conflict from the database
|
||||||
pub resolve_conflict_id: Option<i32>,
|
pub resolve_conflict_id: Option<EditVersion>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub struct ApiConflict {
|
|
||||||
pub id: i32,
|
|
||||||
pub three_way_merge: String,
|
|
||||||
pub article_id: ObjectId<DbArticle>,
|
|
||||||
pub previous_version: EditVersion,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Edit an existing article (local or remote).
|
/// Edit an existing article (local or remote).
|
||||||
|
@ -105,21 +96,17 @@ async fn edit_article(
|
||||||
Form(edit_form): Form<EditArticleData>,
|
Form(edit_form): Form<EditArticleData>,
|
||||||
) -> MyResult<Json<Option<ApiConflict>>> {
|
) -> MyResult<Json<Option<ApiConflict>>> {
|
||||||
// resolve conflict if any
|
// resolve conflict if any
|
||||||
if let Some(resolve_conflict_id) = &edit_form.resolve_conflict_id {
|
if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
|
||||||
let mut lock = data.conflicts.lock().unwrap();
|
DbConflict::delete(resolve_conflict_id, &data.db_connection)?;
|
||||||
if !lock.iter().any(|c| &c.id == resolve_conflict_id) {
|
|
||||||
return Err(anyhow!("invalid resolve conflict"))?;
|
|
||||||
}
|
|
||||||
lock.retain(|c| &c.id != resolve_conflict_id);
|
|
||||||
}
|
}
|
||||||
let original_article = DbArticle::read_view(edit_form.article_id, &data.db_connection)?;
|
let original_article = DbArticle::read_view(edit_form.article_id, &data.db_connection)?;
|
||||||
|
|
||||||
if edit_form.previous_version == original_article.latest_version {
|
if edit_form.previous_version_id == original_article.latest_version {
|
||||||
// No intermediate changes, simply submit new version
|
// No intermediate changes, simply submit new version
|
||||||
submit_article_update(
|
submit_article_update(
|
||||||
&data,
|
&data,
|
||||||
edit_form.new_text.clone(),
|
edit_form.new_text.clone(),
|
||||||
edit_form.previous_version,
|
edit_form.previous_version_id,
|
||||||
&original_article.article,
|
&original_article.article,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -128,20 +115,18 @@ async fn edit_article(
|
||||||
// There have been other changes since this edit was initiated. Get the common ancestor
|
// There have been other changes since this edit was initiated. Get the common ancestor
|
||||||
// version and generate a diff to find out what exactly has changed.
|
// version and generate a diff to find out what exactly has changed.
|
||||||
let ancestor =
|
let ancestor =
|
||||||
generate_article_version(&original_article.edits, &edit_form.previous_version)?;
|
generate_article_version(&original_article.edits, &edit_form.previous_version_id)?;
|
||||||
let patch = create_patch(&ancestor, &edit_form.new_text);
|
let patch = create_patch(&ancestor, &edit_form.new_text);
|
||||||
|
|
||||||
let db_conflict = DbConflict {
|
let previous_version = DbEdit::read(&edit_form.previous_version_id, &data.db_connection)?;
|
||||||
id: random(),
|
let form = DbConflictForm {
|
||||||
|
id: EditVersion::new(&patch.to_string())?,
|
||||||
diff: patch.to_string(),
|
diff: patch.to_string(),
|
||||||
article_id: original_article.article.ap_id.clone(),
|
article_id: original_article.article.id,
|
||||||
previous_version: edit_form.previous_version,
|
previous_version_id: previous_version.hash,
|
||||||
};
|
};
|
||||||
{
|
let conflict = DbConflict::create(&form, &data.db_connection)?;
|
||||||
let mut lock = data.conflicts.lock().unwrap();
|
Ok(Json(conflict.to_api_conflict(&data).await?))
|
||||||
lock.push(db_conflict.clone());
|
|
||||||
}
|
|
||||||
Ok(Json(db_conflict.to_api_conflict(&data).await?))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,8 +171,8 @@ async fn resolve_article(
|
||||||
data: Data<MyDataHandle>,
|
data: Data<MyDataHandle>,
|
||||||
) -> MyResult<Json<ArticleView>> {
|
) -> MyResult<Json<ArticleView>> {
|
||||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||||
let edits = DbEdit::for_article(&article, &data.db_connection)?;
|
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
||||||
let latest_version = edits.last().unwrap().version.clone();
|
let latest_version = edits.last().unwrap().hash.clone();
|
||||||
Ok(Json(ArticleView {
|
Ok(Json(ArticleView {
|
||||||
article,
|
article,
|
||||||
edits,
|
edits,
|
||||||
|
@ -226,7 +211,7 @@ async fn follow_instance(
|
||||||
/// Get a list of all unresolved edit conflicts.
|
/// Get a list of all unresolved edit conflicts.
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> {
|
async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> {
|
||||||
let conflicts = { data.conflicts.lock().unwrap().to_vec() };
|
let conflicts = DbConflict::list(&data.db_connection)?;
|
||||||
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
|
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
|
||||||
let data = data.reset_request_count();
|
let data = data.reset_request_count();
|
||||||
async move { c.to_api_conflict(&data).await }
|
async move { c.to_api_conflict(&data).await }
|
||||||
|
@ -289,10 +274,18 @@ async fn fork_article(
|
||||||
|
|
||||||
// copy edits to new article
|
// copy edits to new article
|
||||||
// TODO: convert to sql
|
// TODO: convert to sql
|
||||||
let edits = DbEdit::for_article(&original_article, &data.db_connection)?;
|
let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?;
|
||||||
for e in edits {
|
for e in edits {
|
||||||
let form = e.copy_to_local_fork(&article)?;
|
let ap_id = DbEditForm::generate_ap_id(&article, &e.hash)?;
|
||||||
DbEdit::create(&form, &data.db_connection)?;
|
// TODO: id gives db unique violation
|
||||||
|
let form = DbEditForm {
|
||||||
|
ap_id,
|
||||||
|
diff: e.diff,
|
||||||
|
article_id: article.id,
|
||||||
|
hash: e.hash,
|
||||||
|
previous_version_id: e.previous_version_id,
|
||||||
|
};
|
||||||
|
dbg!(DbEdit::create(&form, &data.db_connection))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateArticle::send_to_followers(article.clone(), &data).await?;
|
CreateArticle::send_to_followers(article.clone(), &data).await?;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::database::edit::{DbEdit, EditVersion};
|
use crate::database::edit::DbEdit;
|
||||||
|
|
||||||
use crate::database::schema::article;
|
use crate::database::schema::article;
|
||||||
use crate::error::MyResult;
|
use crate::error::MyResult;
|
||||||
|
@ -6,7 +6,7 @@ use crate::federation::objects::edits_collection::DbEditCollection;
|
||||||
use activitypub_federation::fetch::collection_id::CollectionId;
|
use activitypub_federation::fetch::collection_id::CollectionId;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
use diesel::pg::PgConnection;
|
use diesel::pg::PgConnection;
|
||||||
use diesel::BelongingToDsl;
|
|
||||||
use diesel::ExpressionMethods;
|
use diesel::ExpressionMethods;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
insert_into, AsChangeset, BoolExpressionMethods, Identifiable, Insertable,
|
insert_into, AsChangeset, BoolExpressionMethods, Identifiable, Insertable,
|
||||||
|
@ -14,6 +14,7 @@ use diesel::{
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::database::version::EditVersion;
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
@ -86,8 +87,7 @@ impl DbArticle {
|
||||||
article::table.find(id).get_result(conn.deref_mut())?
|
article::table.find(id).get_result(conn.deref_mut())?
|
||||||
};
|
};
|
||||||
let latest_version = article.latest_edit_version(conn)?;
|
let latest_version = article.latest_edit_version(conn)?;
|
||||||
let mut conn = conn.lock().unwrap();
|
let edits: Vec<DbEdit> = DbEdit::read_for_article(&article, conn)?;
|
||||||
let edits: Vec<DbEdit> = DbEdit::belonging_to(&article).get_results(conn.deref_mut())?;
|
|
||||||
Ok(ArticleView {
|
Ok(ArticleView {
|
||||||
article,
|
article,
|
||||||
edits,
|
edits,
|
||||||
|
@ -137,9 +137,8 @@ impl DbArticle {
|
||||||
|
|
||||||
// TODO: shouldnt have to read all edits from db
|
// TODO: shouldnt have to read all edits from db
|
||||||
pub fn latest_edit_version(&self, conn: &Mutex<PgConnection>) -> MyResult<EditVersion> {
|
pub fn latest_edit_version(&self, conn: &Mutex<PgConnection>) -> MyResult<EditVersion> {
|
||||||
let mut conn = conn.lock().unwrap();
|
let edits: Vec<DbEdit> = DbEdit::read_for_article(self, conn)?;
|
||||||
let edits: Vec<DbEdit> = DbEdit::belonging_to(&self).get_results(conn.deref_mut())?;
|
match edits.last().map(|e| e.hash.clone()) {
|
||||||
match edits.last().map(|e| e.version.clone()) {
|
|
||||||
Some(latest_version) => Ok(latest_version),
|
Some(latest_version) => Ok(latest_version),
|
||||||
None => Ok(EditVersion::default()),
|
None => Ok(EditVersion::default()),
|
||||||
}
|
}
|
||||||
|
|
108
src/database/conflict.rs
Normal file
108
src/database/conflict.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
use crate::database::article::DbArticle;
|
||||||
|
use crate::database::edit::DbEdit;
|
||||||
|
use crate::database::schema::conflict;
|
||||||
|
use crate::database::version::EditVersion;
|
||||||
|
use crate::database::MyDataHandle;
|
||||||
|
use crate::error::MyResult;
|
||||||
|
use crate::federation::activities::submit_article_update;
|
||||||
|
use crate::utils::generate_article_version;
|
||||||
|
use activitypub_federation::config::Data;
|
||||||
|
|
||||||
|
use diesel::{
|
||||||
|
delete, insert_into, Identifiable, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl,
|
||||||
|
Selectable,
|
||||||
|
};
|
||||||
|
use diffy::{apply, merge, Patch};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::ops::DerefMut;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
/// A local only object which represents a merge conflict. It is created
|
||||||
|
/// when a local user edit conflicts with another concurrent edit.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)]
|
||||||
|
#[diesel(table_name = conflict, check_for_backend(diesel::pg::Pg), belongs_to(DbArticle, foreign_key = article_id))]
|
||||||
|
pub struct DbConflict {
|
||||||
|
pub id: EditVersion,
|
||||||
|
pub diff: String,
|
||||||
|
pub article_id: i32,
|
||||||
|
pub previous_version_id: EditVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct ApiConflict {
|
||||||
|
pub id: EditVersion,
|
||||||
|
pub three_way_merge: String,
|
||||||
|
pub article_id: i32,
|
||||||
|
pub previous_version_id: EditVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Insertable)]
|
||||||
|
#[diesel(table_name = conflict, check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct DbConflictForm {
|
||||||
|
pub id: EditVersion,
|
||||||
|
pub diff: String,
|
||||||
|
pub article_id: i32,
|
||||||
|
pub previous_version_id: EditVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbConflict {
|
||||||
|
pub fn create(form: &DbConflictForm, conn: &Mutex<PgConnection>) -> MyResult<Self> {
|
||||||
|
let mut conn = conn.lock().unwrap();
|
||||||
|
Ok(insert_into(conflict::table)
|
||||||
|
.values(form)
|
||||||
|
.get_result(conn.deref_mut())?)
|
||||||
|
}
|
||||||
|
pub fn list(conn: &Mutex<PgConnection>) -> MyResult<Vec<Self>> {
|
||||||
|
let mut conn = conn.lock().unwrap();
|
||||||
|
Ok(conflict::table.get_results(conn.deref_mut())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a merge conflict after it is resolved.
|
||||||
|
pub fn delete(id: EditVersion, conn: &Mutex<PgConnection>) -> MyResult<Self> {
|
||||||
|
let mut conn = conn.lock().unwrap();
|
||||||
|
// TODO: should throw error on invalid id param
|
||||||
|
Ok(delete(conflict::table.find(id)).get_result(conn.deref_mut())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn to_api_conflict(
|
||||||
|
&self,
|
||||||
|
data: &Data<MyDataHandle>,
|
||||||
|
) -> MyResult<Option<ApiConflict>> {
|
||||||
|
let article = DbArticle::read(self.article_id, &data.db_connection)?;
|
||||||
|
// Make sure to get latest version from origin so that all conflicts can be resolved
|
||||||
|
let original_article = article.ap_id.dereference_forced(data).await?;
|
||||||
|
|
||||||
|
// create common ancestor version
|
||||||
|
let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?;
|
||||||
|
let ancestor = generate_article_version(&edits, &self.previous_version_id)?;
|
||||||
|
|
||||||
|
let patch = Patch::from_str(&self.diff)?;
|
||||||
|
// apply self.diff to ancestor to get `ours`
|
||||||
|
let ours = apply(&ancestor, &patch)?;
|
||||||
|
match merge(&ancestor, &ours, &original_article.text) {
|
||||||
|
Ok(new_text) => {
|
||||||
|
// patch applies cleanly so we are done
|
||||||
|
// federate the change
|
||||||
|
submit_article_update(
|
||||||
|
data,
|
||||||
|
new_text,
|
||||||
|
self.previous_version_id.clone(),
|
||||||
|
&original_article,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
DbConflict::delete(self.id.clone(), &data.db_connection)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(three_way_merge) => {
|
||||||
|
// there is a merge conflict, user needs to do three-way-merge
|
||||||
|
Ok(Some(ApiConflict {
|
||||||
|
id: self.id.clone(),
|
||||||
|
three_way_merge,
|
||||||
|
article_id: original_article.id,
|
||||||
|
previous_version_id: original_article
|
||||||
|
.latest_edit_version(&data.db_connection)?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,80 +1,71 @@
|
||||||
use crate::database::schema::edit;
|
use crate::database::schema::edit;
|
||||||
|
use crate::database::version::EditVersion;
|
||||||
use crate::database::DbArticle;
|
use crate::database::DbArticle;
|
||||||
use crate::error::MyResult;
|
use crate::error::MyResult;
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
use activitypub_federation::fetch::object_id::ObjectId;
|
||||||
|
use diesel::ExpressionMethods;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
insert_into, AsChangeset, Identifiable, Insertable, PgConnection, Queryable, RunQueryDsl,
|
insert_into, AsChangeset, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl,
|
||||||
Selectable,
|
Selectable,
|
||||||
};
|
};
|
||||||
use diesel::{Associations, BelongingToDsl};
|
|
||||||
use diesel_derive_newtype::DieselNewType;
|
|
||||||
use diffy::create_patch;
|
use diffy::create_patch;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha224};
|
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
/// Represents a single change to the article.
|
/// Represents a single change to the article.
|
||||||
#[derive(
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable)]
|
||||||
Clone,
|
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
|
||||||
Debug,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
PartialEq,
|
|
||||||
Queryable,
|
|
||||||
Selectable,
|
|
||||||
Identifiable,
|
|
||||||
Associations,
|
|
||||||
)]
|
|
||||||
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg), belongs_to(DbArticle, foreign_key = article_id))]
|
|
||||||
pub struct DbEdit {
|
pub struct DbEdit {
|
||||||
|
// TODO: we could use hash as primary key, but that gives errors on forking because
|
||||||
|
// the same edit is used for multiple articles
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
/// UUID built from sha224 hash of diff
|
||||||
|
pub hash: EditVersion,
|
||||||
pub ap_id: ObjectId<DbEdit>,
|
pub ap_id: ObjectId<DbEdit>,
|
||||||
pub diff: String,
|
pub diff: String,
|
||||||
pub article_id: i32,
|
pub article_id: i32,
|
||||||
pub version: EditVersion,
|
/// First edit of an article always has `EditVersion::default()` here
|
||||||
// TODO: could be an Option<DbEdit.id> instead
|
pub previous_version_id: EditVersion,
|
||||||
pub previous_version: EditVersion,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
||||||
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
|
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
|
||||||
pub struct DbEditForm {
|
pub struct DbEditForm {
|
||||||
|
pub hash: EditVersion,
|
||||||
pub ap_id: ObjectId<DbEdit>,
|
pub ap_id: ObjectId<DbEdit>,
|
||||||
pub diff: String,
|
pub diff: String,
|
||||||
pub article_id: i32,
|
pub article_id: i32,
|
||||||
pub version: EditVersion,
|
pub previous_version_id: EditVersion,
|
||||||
pub previous_version: EditVersion,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DbEditForm {
|
impl DbEditForm {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
original_article: &DbArticle,
|
original_article: &DbArticle,
|
||||||
updated_text: &str,
|
updated_text: &str,
|
||||||
previous_version: EditVersion,
|
previous_version_id: EditVersion,
|
||||||
) -> MyResult<Self> {
|
) -> MyResult<Self> {
|
||||||
let diff = create_patch(&original_article.text, updated_text);
|
let diff = create_patch(&original_article.text, updated_text);
|
||||||
let (ap_id, hash) = Self::generate_ap_id_and_hash(original_article, diff.to_bytes())?;
|
let version = EditVersion::new(&diff.to_string())?;
|
||||||
|
let ap_id = Self::generate_ap_id(original_article, &version)?;
|
||||||
Ok(DbEditForm {
|
Ok(DbEditForm {
|
||||||
|
hash: version,
|
||||||
ap_id,
|
ap_id,
|
||||||
diff: diff.to_string(),
|
diff: diff.to_string(),
|
||||||
article_id: original_article.id,
|
article_id: original_article.id,
|
||||||
version: EditVersion(hash),
|
previous_version_id,
|
||||||
previous_version,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_ap_id_and_hash(
|
pub(crate) fn generate_ap_id(
|
||||||
article: &DbArticle,
|
article: &DbArticle,
|
||||||
diff: Vec<u8>,
|
version: &EditVersion,
|
||||||
) -> MyResult<(ObjectId<DbEdit>, String)> {
|
) -> MyResult<ObjectId<DbEdit>> {
|
||||||
let mut sha224 = Sha224::new();
|
Ok(ObjectId::parse(&format!(
|
||||||
sha224.update(diff);
|
"{}/{}",
|
||||||
let hash = format!("{:X}", sha224.finalize());
|
article.ap_id,
|
||||||
Ok((
|
version.hash()
|
||||||
ObjectId::parse(&format!("{}/{}", article.ap_id, hash))?,
|
))?)
|
||||||
hash,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,31 +79,20 @@ impl DbEdit {
|
||||||
.set(form)
|
.set(form)
|
||||||
.get_result(conn.deref_mut())?)
|
.get_result(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
|
pub fn read(version: &EditVersion, conn: &Mutex<PgConnection>) -> MyResult<Self> {
|
||||||
pub fn for_article(article: &DbArticle, conn: &Mutex<PgConnection>) -> MyResult<Vec<Self>> {
|
|
||||||
let mut conn = conn.lock().unwrap();
|
let mut conn = conn.lock().unwrap();
|
||||||
Ok(DbEdit::belonging_to(&article).get_results(conn.deref_mut())?)
|
Ok(edit::table
|
||||||
|
.filter(edit::dsl::hash.eq(version))
|
||||||
|
.get_result(conn.deref_mut())?)
|
||||||
}
|
}
|
||||||
pub fn copy_to_local_fork(self, article: &DbArticle) -> MyResult<DbEditForm> {
|
|
||||||
let (ap_id, _) =
|
pub fn read_for_article(
|
||||||
DbEditForm::generate_ap_id_and_hash(article, self.diff.clone().into_bytes())?;
|
article: &DbArticle,
|
||||||
Ok(DbEditForm {
|
conn: &Mutex<PgConnection>,
|
||||||
ap_id,
|
) -> MyResult<Vec<Self>> {
|
||||||
diff: self.diff,
|
let mut conn = conn.lock().unwrap();
|
||||||
article_id: article.id,
|
Ok(edit::table
|
||||||
version: self.version,
|
.filter(edit::dsl::article_id.eq(article.id))
|
||||||
previous_version: self.previous_version,
|
.get_results(conn.deref_mut())?)
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, DieselNewType)]
|
|
||||||
pub struct EditVersion(pub String);
|
|
||||||
|
|
||||||
impl Default for EditVersion {
|
|
||||||
fn default() -> Self {
|
|
||||||
let sha224 = Sha224::new();
|
|
||||||
let hash = format!("{:X}", sha224.finalize());
|
|
||||||
EditVersion(hash)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,90 +1,27 @@
|
||||||
use crate::api::ApiConflict;
|
|
||||||
use crate::database::article::DbArticle;
|
use crate::database::article::DbArticle;
|
||||||
use crate::database::edit::DbEdit;
|
|
||||||
use crate::error::MyResult;
|
|
||||||
use crate::federation::activities::submit_article_update;
|
|
||||||
use crate::utils::generate_article_version;
|
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use activitypub_federation::fetch::object_id::ObjectId;
|
|
||||||
use diesel::PgConnection;
|
|
||||||
use diffy::{apply, merge, Patch};
|
|
||||||
use edit::EditVersion;
|
|
||||||
|
|
||||||
|
use diesel::PgConnection;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
pub mod article;
|
pub mod article;
|
||||||
|
pub mod conflict;
|
||||||
pub mod edit;
|
pub mod edit;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
pub mod version;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MyData {
|
pub struct MyData {
|
||||||
pub db_connection: Arc<Mutex<PgConnection>>,
|
pub db_connection: Arc<Mutex<PgConnection>>,
|
||||||
pub fake_db: Arc<FakeDatabase>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for MyData {
|
impl Deref for MyData {
|
||||||
type Target = Arc<FakeDatabase>;
|
type Target = Arc<Mutex<PgConnection>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.fake_db
|
&self.db_connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MyDataHandle = MyData;
|
pub type MyDataHandle = MyData;
|
||||||
|
|
||||||
pub struct FakeDatabase {
|
|
||||||
pub conflicts: Mutex<Vec<DbConflict>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct DbConflict {
|
|
||||||
pub id: i32,
|
|
||||||
pub diff: String,
|
|
||||||
pub article_id: ObjectId<DbArticle>,
|
|
||||||
pub previous_version: EditVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbConflict {
|
|
||||||
pub async fn to_api_conflict(
|
|
||||||
&self,
|
|
||||||
data: &Data<MyDataHandle>,
|
|
||||||
) -> MyResult<Option<ApiConflict>> {
|
|
||||||
// Make sure to get latest version from origin so that all conflicts can be resolved
|
|
||||||
let original_article = self.article_id.dereference_forced(&data).await?;
|
|
||||||
|
|
||||||
// create common ancestor version
|
|
||||||
let edits = DbEdit::for_article(&original_article, &data.db_connection)?;
|
|
||||||
let ancestor = generate_article_version(&edits, &self.previous_version)?;
|
|
||||||
|
|
||||||
let patch = Patch::from_str(&self.diff)?;
|
|
||||||
// apply self.diff to ancestor to get `ours`
|
|
||||||
let ours = apply(&ancestor, &patch)?;
|
|
||||||
match dbg!(merge(&ancestor, &ours, &original_article.text)) {
|
|
||||||
Ok(new_text) => {
|
|
||||||
// patch applies cleanly so we are done
|
|
||||||
// federate the change
|
|
||||||
submit_article_update(
|
|
||||||
data,
|
|
||||||
new_text,
|
|
||||||
self.previous_version.clone(),
|
|
||||||
&original_article,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
// remove conflict from db
|
|
||||||
let mut lock = data.conflicts.lock().unwrap();
|
|
||||||
lock.retain(|c| c.id != self.id);
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
Err(three_way_merge) => {
|
|
||||||
// there is a merge conflict, user needs to do three-way-merge
|
|
||||||
Ok(Some(ApiConflict {
|
|
||||||
id: self.id,
|
|
||||||
three_way_merge,
|
|
||||||
article_id: original_article.ap_id.clone(),
|
|
||||||
previous_version: original_article.latest_edit_version(&data.db_connection)?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,15 +12,24 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
conflict (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
diff -> Text,
|
||||||
|
article_id -> Int4,
|
||||||
|
previous_version_id -> Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
edit (id) {
|
edit (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
|
hash -> Uuid,
|
||||||
#[max_length = 255]
|
#[max_length = 255]
|
||||||
ap_id -> Varchar,
|
ap_id -> Varchar,
|
||||||
diff -> Text,
|
diff -> Text,
|
||||||
article_id -> Int4,
|
article_id -> Int4,
|
||||||
version -> Text,
|
previous_version_id -> Uuid,
|
||||||
previous_version -> Text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +58,7 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::joinable!(article -> instance (instance_id));
|
diesel::joinable!(article -> instance (instance_id));
|
||||||
|
diesel::joinable!(conflict -> article (article_id));
|
||||||
diesel::joinable!(edit -> article (article_id));
|
diesel::joinable!(edit -> article (article_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(article, edit, instance, instance_follow,);
|
diesel::allow_tables_to_appear_in_same_query!(article, conflict, edit, instance, instance_follow,);
|
||||||
|
|
43
src/database/version.rs
Normal file
43
src/database/version.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use crate::error::MyResult;
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
use diesel_derive_newtype::DieselNewType;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff
|
||||||
|
/// and using the first 16 bytes so that it fits into UUID.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, DieselNewType)]
|
||||||
|
pub struct EditVersion(Uuid);
|
||||||
|
|
||||||
|
impl EditVersion {
|
||||||
|
pub fn new(diff: &str) -> MyResult<Self> {
|
||||||
|
let mut sha256 = Sha256::new();
|
||||||
|
sha256.update(diff);
|
||||||
|
let hash_bytes = sha256.finalize();
|
||||||
|
let uuid = Uuid::from_slice(&hash_bytes.as_slice()[..16])?;
|
||||||
|
Ok(EditVersion(uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(&self) -> String {
|
||||||
|
hex::encode(self.0.into_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EditVersion {
|
||||||
|
fn default() -> Self {
|
||||||
|
EditVersion::new("").unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_versions() -> MyResult<()> {
|
||||||
|
let default = EditVersion::default();
|
||||||
|
assert_eq!("e3b0c44298fc1c149afbf4c8996fb924", default.hash());
|
||||||
|
|
||||||
|
let version = EditVersion::new("test")?;
|
||||||
|
assert_eq!("9f86d081884c7d659a2feaa0c55ad015", version.hash());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::database::article::DbArticle;
|
use crate::database::article::DbArticle;
|
||||||
use crate::database::edit::{DbEdit, DbEditForm, EditVersion};
|
use crate::database::edit::{DbEdit, DbEditForm};
|
||||||
use crate::database::instance::DbInstance;
|
use crate::database::instance::DbInstance;
|
||||||
|
use crate::database::version::EditVersion;
|
||||||
use crate::database::MyDataHandle;
|
use crate::database::MyDataHandle;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::federation::activities::update_local_article::UpdateLocalArticle;
|
use crate::federation::activities::update_local_article::UpdateLocalArticle;
|
||||||
|
@ -30,12 +31,12 @@ pub async fn submit_article_update(
|
||||||
} else {
|
} else {
|
||||||
// dont insert edit into db, might be invalid in case of conflict
|
// dont insert edit into db, might be invalid in case of conflict
|
||||||
let edit = DbEdit {
|
let edit = DbEdit {
|
||||||
id: 0,
|
id: -1,
|
||||||
|
hash: form.hash,
|
||||||
ap_id: form.ap_id,
|
ap_id: form.ap_id,
|
||||||
diff: form.diff,
|
diff: form.diff,
|
||||||
article_id: form.article_id,
|
article_id: form.article_id,
|
||||||
version: form.version,
|
previous_version_id: form.previous_version_id,
|
||||||
previous_version: form.previous_version,
|
|
||||||
};
|
};
|
||||||
let instance = DbInstance::read(original_article.instance_id, &data.db_connection)?;
|
let instance = DbInstance::read(original_article.instance_id, &data.db_connection)?;
|
||||||
UpdateRemoteArticle::send(edit, instance, data).await?;
|
UpdateRemoteArticle::send(edit, instance, data).await?;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
use crate::database::conflict::{DbConflict, DbConflictForm};
|
||||||
use crate::database::instance::DbInstance;
|
use crate::database::instance::DbInstance;
|
||||||
|
use crate::database::version::EditVersion;
|
||||||
use crate::database::MyDataHandle;
|
use crate::database::MyDataHandle;
|
||||||
use crate::error::MyResult;
|
use crate::error::MyResult;
|
||||||
use crate::federation::objects::edit::ApubEdit;
|
use crate::federation::objects::edit::ApubEdit;
|
||||||
|
@ -8,11 +10,7 @@ use activitypub_federation::{
|
||||||
config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many,
|
config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many,
|
||||||
traits::ActivityHandler,
|
traits::ActivityHandler,
|
||||||
};
|
};
|
||||||
use rand::random;
|
|
||||||
|
|
||||||
use crate::database::article::DbArticle;
|
|
||||||
use crate::database::DbConflict;
|
|
||||||
use crate::federation::activities::update_local_article::UpdateLocalArticle;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -68,16 +66,15 @@ impl ActivityHandler for RejectEdit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||||
dbg!(&self);
|
|
||||||
// cant convert this to DbEdit as it tries to apply patch and fails
|
// cant convert this to DbEdit as it tries to apply patch and fails
|
||||||
let mut lock = data.conflicts.lock().unwrap();
|
let article = self.object.object.dereference(data).await?;
|
||||||
let conflict = DbConflict {
|
let form = DbConflictForm {
|
||||||
id: random(),
|
id: EditVersion::new(&self.object.content)?,
|
||||||
diff: self.object.content,
|
diff: self.object.content,
|
||||||
article_id: self.object.object,
|
article_id: article.id,
|
||||||
previous_version: self.object.previous_version,
|
previous_version_id: self.object.previous_version,
|
||||||
};
|
};
|
||||||
lock.push(conflict);
|
DbConflict::create(&form, &data.db_connection)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::database::article::DbArticleForm;
|
use crate::database::article::DbArticleForm;
|
||||||
use crate::database::edit::EditVersion;
|
|
||||||
use crate::database::instance::DbInstance;
|
use crate::database::instance::DbInstance;
|
||||||
|
use crate::database::version::EditVersion;
|
||||||
use crate::database::{article::DbArticle, MyDataHandle};
|
use crate::database::{article::DbArticle, MyDataHandle};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::federation::objects::edits_collection::DbEditCollection;
|
use crate::federation::objects::edits_collection::DbEditCollection;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::database::article::DbArticle;
|
use crate::database::article::DbArticle;
|
||||||
use crate::database::edit::{DbEdit, DbEditForm, EditVersion};
|
use crate::database::edit::{DbEdit, DbEditForm};
|
||||||
|
use crate::database::version::EditVersion;
|
||||||
use crate::database::MyDataHandle;
|
use crate::database::MyDataHandle;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
|
@ -44,9 +45,8 @@ impl Object for DbEdit {
|
||||||
kind: EditType::Edit,
|
kind: EditType::Edit,
|
||||||
id: self.ap_id,
|
id: self.ap_id,
|
||||||
content: self.diff,
|
content: self.diff,
|
||||||
version: self.version,
|
version: self.hash,
|
||||||
// TODO: this is wrong
|
previous_version: self.previous_version_id,
|
||||||
previous_version: self.previous_version,
|
|
||||||
object: article.ap_id,
|
object: article.ap_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -65,8 +65,8 @@ impl Object for DbEdit {
|
||||||
ap_id: json.id,
|
ap_id: json.id,
|
||||||
diff: json.content,
|
diff: json.content,
|
||||||
article_id: article.id,
|
article_id: article.id,
|
||||||
version: json.version,
|
hash: json.version,
|
||||||
previous_version: json.previous_version,
|
previous_version_id: json.previous_version,
|
||||||
};
|
};
|
||||||
let edit = DbEdit::create(&form, &data.db_connection)?;
|
let edit = DbEdit::create(&form, &data.db_connection)?;
|
||||||
Ok(edit)
|
Ok(edit)
|
||||||
|
|
11
src/lib.rs
11
src/lib.rs
|
@ -1,6 +1,6 @@
|
||||||
use crate::api::api_routes;
|
use crate::api::api_routes;
|
||||||
use crate::database::instance::{DbInstance, DbInstanceForm};
|
use crate::database::instance::{DbInstance, DbInstanceForm};
|
||||||
use crate::database::{FakeDatabase, MyData};
|
use crate::database::MyData;
|
||||||
use crate::error::MyResult;
|
use crate::error::MyResult;
|
||||||
use crate::federation::routes::federation_routes;
|
use crate::federation::routes::federation_routes;
|
||||||
use crate::utils::generate_activity_id;
|
use crate::utils::generate_activity_id;
|
||||||
|
@ -28,10 +28,6 @@ mod utils;
|
||||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||||
|
|
||||||
pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
|
pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
|
||||||
let fake_db = Arc::new(FakeDatabase {
|
|
||||||
conflicts: Mutex::new(vec![]),
|
|
||||||
});
|
|
||||||
|
|
||||||
let db_connection = Arc::new(Mutex::new(PgConnection::establish(database_url)?));
|
let db_connection = Arc::new(Mutex::new(PgConnection::establish(database_url)?));
|
||||||
db_connection
|
db_connection
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -39,10 +35,7 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
|
||||||
.run_pending_migrations(MIGRATIONS)
|
.run_pending_migrations(MIGRATIONS)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let data = MyData {
|
let data = MyData { db_connection };
|
||||||
db_connection,
|
|
||||||
fake_db,
|
|
||||||
};
|
|
||||||
let config = FederationConfig::builder()
|
let config = FederationConfig::builder()
|
||||||
.domain(hostname)
|
.domain(hostname)
|
||||||
.app_data(data)
|
.app_data(data)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::database::edit::DbEdit;
|
use crate::database::edit::DbEdit;
|
||||||
use crate::database::edit::EditVersion;
|
use crate::database::version::EditVersion;
|
||||||
use crate::error::MyResult;
|
use crate::error::MyResult;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use diffy::{apply, Patch};
|
use diffy::{apply, Patch};
|
||||||
|
@ -31,7 +31,7 @@ pub fn generate_article_version(edits: &Vec<DbEdit>, version: &EditVersion) -> M
|
||||||
for e in edits {
|
for e in edits {
|
||||||
let patch = Patch::from_str(&e.diff)?;
|
let patch = Patch::from_str(&e.diff)?;
|
||||||
generated = apply(&generated, &patch)?;
|
generated = apply(&generated, &patch)?;
|
||||||
if &e.version == version {
|
if &e.hash == version {
|
||||||
return Ok(generated);
|
return Ok(generated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
use fediwiki::api::{
|
use fediwiki::api::{
|
||||||
ApiConflict, CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
|
CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
|
||||||
};
|
};
|
||||||
use fediwiki::database::article::ArticleView;
|
use fediwiki::database::article::ArticleView;
|
||||||
|
use fediwiki::database::conflict::ApiConflict;
|
||||||
use fediwiki::database::instance::DbInstance;
|
use fediwiki::database::instance::DbInstance;
|
||||||
use fediwiki::error::MyResult;
|
use fediwiki::error::MyResult;
|
||||||
use fediwiki::start;
|
use fediwiki::start;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::Client;
|
use reqwest::{Client, RequestBuilder, StatusCode};
|
||||||
use serde::de::Deserialize;
|
use serde::de::Deserialize;
|
||||||
use serde::ser::Serialize;
|
use serde::ser::Serialize;
|
||||||
use std::env::current_dir;
|
use std::env::current_dir;
|
||||||
|
@ -142,7 +144,7 @@ pub async fn create_article(hostname: &str, title: String) -> MyResult<ArticleVi
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: article.article.id,
|
article_id: article.article.id,
|
||||||
new_text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
|
new_text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
|
||||||
previous_version: article.latest_version,
|
previous_version_id: article.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
edit_article(hostname, &edit_form).await
|
edit_article(hostname, &edit_form).await
|
||||||
|
@ -157,13 +159,10 @@ pub async fn edit_article_with_conflict(
|
||||||
hostname: &str,
|
hostname: &str,
|
||||||
edit_form: &EditArticleData,
|
edit_form: &EditArticleData,
|
||||||
) -> MyResult<Option<ApiConflict>> {
|
) -> MyResult<Option<ApiConflict>> {
|
||||||
Ok(CLIENT
|
let req = CLIENT
|
||||||
.patch(format!("http://{}/api/v1/article", hostname))
|
.patch(format!("http://{}/api/v1/article", hostname))
|
||||||
.form(edit_form)
|
.form(edit_form);
|
||||||
.send()
|
handle_json_res(req).await
|
||||||
.await?
|
|
||||||
.json()
|
|
||||||
.await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn edit_article(hostname: &str, edit_form: &EditArticleData) -> MyResult<ArticleView> {
|
pub async fn edit_article(hostname: &str, edit_form: &EditArticleData) -> MyResult<ArticleView> {
|
||||||
|
@ -184,25 +183,34 @@ where
|
||||||
T: for<'de> Deserialize<'de>,
|
T: for<'de> Deserialize<'de>,
|
||||||
R: Serialize,
|
R: Serialize,
|
||||||
{
|
{
|
||||||
let mut res = CLIENT.get(format!("http://{}/api/v1/{}", hostname, endpoint));
|
let mut req = CLIENT.get(format!("http://{}/api/v1/{}", hostname, endpoint));
|
||||||
if let Some(query) = query {
|
if let Some(query) = query {
|
||||||
res = res.query(&query);
|
req = req.query(&query);
|
||||||
}
|
}
|
||||||
let alpha_instance: T = res.send().await?.json().await?;
|
handle_json_res(req).await
|
||||||
Ok(alpha_instance)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
|
pub async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
|
||||||
where
|
where
|
||||||
R: for<'de> Deserialize<'de>,
|
R: for<'de> Deserialize<'de>,
|
||||||
{
|
{
|
||||||
Ok(CLIENT
|
let req = CLIENT
|
||||||
.post(format!("http://{}/api/v1/{}", hostname, endpoint))
|
.post(format!("http://{}/api/v1/{}", hostname, endpoint))
|
||||||
.form(form)
|
.form(form);
|
||||||
.send()
|
handle_json_res(req).await
|
||||||
.await?
|
}
|
||||||
.json()
|
|
||||||
.await?)
|
async fn handle_json_res<T>(req: RequestBuilder) -> MyResult<T>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
let res = req.send().await?;
|
||||||
|
if res.status() == StatusCode::OK {
|
||||||
|
Ok(res.json().await?)
|
||||||
|
} else {
|
||||||
|
let text = res.text().await?;
|
||||||
|
Err(anyhow!("Post API response {text}").into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn follow_instance(api_instance: &str, follow_instance: &str) -> MyResult<()> {
|
pub async fn follow_instance(api_instance: &str, follow_instance: &str) -> MyResult<()> {
|
||||||
|
|
|
@ -7,12 +7,11 @@ use crate::common::{
|
||||||
get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT,
|
get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT,
|
||||||
};
|
};
|
||||||
use common::get;
|
use common::get;
|
||||||
use fediwiki::api::{
|
use fediwiki::api::{EditArticleData, ForkArticleData, ResolveObject, SearchArticleData};
|
||||||
ApiConflict, EditArticleData, ForkArticleData, ResolveObject, SearchArticleData,
|
|
||||||
};
|
|
||||||
use fediwiki::database::article::{ArticleView, DbArticle};
|
use fediwiki::database::article::{ArticleView, DbArticle};
|
||||||
use fediwiki::error::MyResult;
|
use fediwiki::error::MyResult;
|
||||||
|
|
||||||
|
use fediwiki::database::conflict::ApiConflict;
|
||||||
use fediwiki::database::instance::{DbInstance, InstanceView};
|
use fediwiki::database::instance::{DbInstance, InstanceView};
|
||||||
use pretty_assertions::{assert_eq, assert_ne};
|
use pretty_assertions::{assert_eq, assert_ne};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -42,7 +41,7 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "Lorem Ipsum 2".to_string(),
|
new_text: "Lorem Ipsum 2".to_string(),
|
||||||
previous_version: get_res.latest_version,
|
previous_version_id: get_res.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||||
|
@ -125,7 +124,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "Lorem Ipsum 2\n".to_string(),
|
new_text: "Lorem Ipsum 2\n".to_string(),
|
||||||
previous_version: create_res.latest_version,
|
previous_version_id: create_res.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
edit_article(&data.alpha.hostname, &edit_form).await?;
|
edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||||
|
@ -179,7 +178,7 @@ async fn test_edit_local_article() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "Lorem Ipsum 2".to_string(),
|
new_text: "Lorem Ipsum 2".to_string(),
|
||||||
previous_version: get_res.latest_version,
|
previous_version_id: get_res.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.beta.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.beta.hostname, &edit_form).await?;
|
||||||
|
@ -225,7 +224,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "Lorem Ipsum 2".to_string(),
|
new_text: "Lorem Ipsum 2".to_string(),
|
||||||
previous_version: get_res.latest_version,
|
previous_version_id: get_res.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||||
|
@ -265,7 +264,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "Lorem Ipsum\n".to_string(),
|
new_text: "Lorem Ipsum\n".to_string(),
|
||||||
previous_version: create_res.latest_version.clone(),
|
previous_version_id: create_res.latest_version.clone(),
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||||
|
@ -276,7 +275,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "Ipsum Lorem\n".to_string(),
|
new_text: "Ipsum Lorem\n".to_string(),
|
||||||
previous_version: create_res.latest_version,
|
previous_version_id: create_res.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article_with_conflict(&data.alpha.hostname, &edit_form)
|
let edit_res = edit_article_with_conflict(&data.alpha.hostname, &edit_form)
|
||||||
|
@ -292,7 +291,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "Lorem Ipsum and Ipsum Lorem\n".to_string(),
|
new_text: "Lorem Ipsum and Ipsum Lorem\n".to_string(),
|
||||||
previous_version: edit_res.previous_version,
|
previous_version_id: edit_res.previous_version_id,
|
||||||
resolve_conflict_id: Some(edit_res.id),
|
resolve_conflict_id: Some(edit_res.id),
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||||
|
@ -333,7 +332,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "Lorem Ipsum\n".to_string(),
|
new_text: "Lorem Ipsum\n".to_string(),
|
||||||
previous_version: create_res.latest_version.clone(),
|
previous_version_id: create_res.latest_version.clone(),
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||||
|
@ -350,7 +349,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "aaaa\n".to_string(),
|
new_text: "aaaa\n".to_string(),
|
||||||
previous_version: create_res.latest_version,
|
previous_version_id: create_res.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
|
||||||
|
@ -367,8 +366,8 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "aaaa\n".to_string(),
|
new_text: "aaaa\n".to_string(),
|
||||||
previous_version: conflicts[0].previous_version.clone(),
|
previous_version_id: conflicts[0].previous_version_id.clone(),
|
||||||
resolve_conflict_id: Some(conflicts[0].id),
|
resolve_conflict_id: Some(conflicts[0].id.clone()),
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
|
||||||
assert_eq!(edit_form.new_text, edit_res.article.text);
|
assert_eq!(edit_form.new_text, edit_res.article.text);
|
||||||
|
@ -395,7 +394,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "my\nexample\ntext\n".to_string(),
|
new_text: "my\nexample\ntext\n".to_string(),
|
||||||
previous_version: create_res.latest_version.clone(),
|
previous_version_id: create_res.latest_version.clone(),
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||||
|
@ -406,7 +405,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
|
||||||
let edit_form = EditArticleData {
|
let edit_form = EditArticleData {
|
||||||
article_id: create_res.article.id,
|
article_id: create_res.article.id,
|
||||||
new_text: "some\nexample\narticle\n".to_string(),
|
new_text: "some\nexample\narticle\n".to_string(),
|
||||||
previous_version: create_res.latest_version,
|
previous_version_id: create_res.latest_version,
|
||||||
resolve_conflict_id: None,
|
resolve_conflict_id: None,
|
||||||
};
|
};
|
||||||
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||||
|
@ -448,7 +447,7 @@ async fn test_fork_article() -> MyResult<()> {
|
||||||
assert_eq!(resolved_article.text, forked_article.text);
|
assert_eq!(resolved_article.text, forked_article.text);
|
||||||
assert_eq!(resolve_res.edits.len(), fork_res.edits.len());
|
assert_eq!(resolve_res.edits.len(), fork_res.edits.len());
|
||||||
assert_eq!(resolve_res.edits[0].diff, fork_res.edits[0].diff);
|
assert_eq!(resolve_res.edits[0].diff, fork_res.edits[0].diff);
|
||||||
assert_eq!(resolve_res.edits[0].version, fork_res.edits[0].version);
|
assert_eq!(resolve_res.edits[0].hash, fork_res.edits[0].hash);
|
||||||
assert_ne!(resolve_res.edits[0].id, fork_res.edits[0].id);
|
assert_ne!(resolve_res.edits[0].id, fork_res.edits[0].id);
|
||||||
assert_eq!(resolve_res.latest_version, fork_res.latest_version);
|
assert_eq!(resolve_res.latest_version, fork_res.latest_version);
|
||||||
assert_ne!(resolved_article.ap_id, forked_article.ap_id);
|
assert_ne!(resolved_article.ap_id, forked_article.ap_id);
|
||||||
|
|
Loading…
Reference in a new issue