1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2024-12-23 09:01:22 +00:00

conflict moved to db

This commit is contained in:
Felix Ableitner 2023-12-05 01:17:02 +01:00
parent 37352a3e86
commit 5d2099c17c
19 changed files with 342 additions and 254 deletions

14
Cargo.lock generated
View file

@ -492,6 +492,7 @@ dependencies = [
"diesel_derives",
"itoa",
"pq-sys",
"uuid",
]
[[package]]
@ -679,6 +680,7 @@ dependencies = [
"enum_delegate",
"env_logger",
"futures",
"hex",
"once_cell",
"pretty_assertions",
"rand",
@ -688,6 +690,7 @@ dependencies = [
"tokio",
"tracing",
"url",
"uuid",
]
[[package]]
@ -924,6 +927,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "0.2.11"
@ -2276,11 +2285,12 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.5.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [
"getrandom",
"serde",
]
[[package]]

View file

@ -10,19 +10,21 @@ async-trait = "0.1.74"
axum = "0.6.20"
axum-macros = "0.3.8"
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_migrations = "2.1.0"
diffy = "0.3.0"
enum_delegate = "0.2.0"
env_logger = { version = "0.10.1", default-features = false }
futures = "0.3.29"
hex = "0.4.3"
rand = "0.8.5"
serde = "1.0.192"
sha2 = "0.10.8"
tokio = { version = "1.34.0", features = ["full"] }
tracing = "0.1.40"
url = "2.4.1"
uuid = { version = "1.6.1", features = ["serde"] }
[dev-dependencies]
once_cell = "1.18.0"

View file

@ -1,4 +1,5 @@
drop table conflict;
drop table edit;
drop table article;
drop table instance_follow;
drop table instance;
drop table instance;

View file

@ -28,9 +28,16 @@ create table article (
create table edit (
id serial primary key,
hash uuid not null,
ap_id varchar(255) not null unique,
diff text not null,
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
version text not null,
previous_version text not null
)
previous_version_id uuid 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
);

View file

@ -1,7 +1,9 @@
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::{DbConflict, MyDataHandle};
use crate::database::version::EditVersion;
use crate::database::MyDataHandle;
use crate::error::MyResult;
use crate::federation::activities::create_article::CreateArticle;
use crate::federation::activities::follow::Follow;
@ -9,14 +11,12 @@ 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 anyhow::anyhow;
use axum::extract::Query;
use axum::routing::{get, post};
use axum::{Form, Json, Router};
use axum_macros::debug_handler;
use diffy::create_patch;
use futures::future::try_join_all;
use rand::random;
use serde::{Deserialize, Serialize};
use url::Url;
@ -60,8 +60,7 @@ async fn create_article(
instance_id: local_instance.id,
local: true,
};
dbg!(&form.ap_id);
let article = dbg!(DbArticle::create(&form, &data.db_connection))?;
let article = DbArticle::create(&form, &data.db_connection)?;
CreateArticle::send_to_followers(article.clone(), &data).await?;
@ -77,17 +76,9 @@ pub struct EditArticleData {
pub new_text: String,
/// The version that this edit is based on, ie [DbArticle.latest_version] or
/// [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
pub resolve_conflict_id: Option<i32>,
}
#[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,
pub resolve_conflict_id: Option<EditVersion>,
}
/// Edit an existing article (local or remote).
@ -105,21 +96,17 @@ async fn edit_article(
Form(edit_form): Form<EditArticleData>,
) -> MyResult<Json<Option<ApiConflict>>> {
// resolve conflict if any
if let Some(resolve_conflict_id) = &edit_form.resolve_conflict_id {
let mut lock = data.conflicts.lock().unwrap();
if !lock.iter().any(|c| &c.id == resolve_conflict_id) {
return Err(anyhow!("invalid resolve conflict"))?;
}
lock.retain(|c| &c.id != resolve_conflict_id);
if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
DbConflict::delete(resolve_conflict_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
submit_article_update(
&data,
edit_form.new_text.clone(),
edit_form.previous_version,
edit_form.previous_version_id,
&original_article.article,
)
.await?;
@ -128,20 +115,18 @@ async fn edit_article(
// 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.
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 db_conflict = DbConflict {
id: random(),
let previous_version = DbEdit::read(&edit_form.previous_version_id, &data.db_connection)?;
let form = DbConflictForm {
id: EditVersion::new(&patch.to_string())?,
diff: patch.to_string(),
article_id: original_article.article.ap_id.clone(),
previous_version: edit_form.previous_version,
article_id: original_article.article.id,
previous_version_id: previous_version.hash,
};
{
let mut lock = data.conflicts.lock().unwrap();
lock.push(db_conflict.clone());
}
Ok(Json(db_conflict.to_api_conflict(&data).await?))
let conflict = DbConflict::create(&form, &data.db_connection)?;
Ok(Json(conflict.to_api_conflict(&data).await?))
}
}
@ -186,8 +171,8 @@ async fn resolve_article(
data: Data<MyDataHandle>,
) -> MyResult<Json<ArticleView>> {
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
let edits = DbEdit::for_article(&article, &data.db_connection)?;
let latest_version = edits.last().unwrap().version.clone();
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
let latest_version = edits.last().unwrap().hash.clone();
Ok(Json(ArticleView {
article,
edits,
@ -226,7 +211,7 @@ async fn follow_instance(
/// Get a list of all unresolved edit conflicts.
#[debug_handler]
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 data = data.reset_request_count();
async move { c.to_api_conflict(&data).await }
@ -289,10 +274,18 @@ async fn fork_article(
// copy edits to new article
// 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 {
let form = e.copy_to_local_fork(&article)?;
DbEdit::create(&form, &data.db_connection)?;
let ap_id = DbEditForm::generate_ap_id(&article, &e.hash)?;
// 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?;

View file

@ -1,4 +1,4 @@
use crate::database::edit::{DbEdit, EditVersion};
use crate::database::edit::DbEdit;
use crate::database::schema::article;
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::object_id::ObjectId;
use diesel::pg::PgConnection;
use diesel::BelongingToDsl;
use diesel::ExpressionMethods;
use diesel::{
insert_into, AsChangeset, BoolExpressionMethods, Identifiable, Insertable,
@ -14,6 +14,7 @@ use diesel::{
};
use serde::{Deserialize, Serialize};
use crate::database::version::EditVersion;
use std::ops::DerefMut;
use std::sync::Mutex;
@ -86,8 +87,7 @@ impl DbArticle {
article::table.find(id).get_result(conn.deref_mut())?
};
let latest_version = article.latest_edit_version(conn)?;
let mut conn = conn.lock().unwrap();
let edits: Vec<DbEdit> = DbEdit::belonging_to(&article).get_results(conn.deref_mut())?;
let edits: Vec<DbEdit> = DbEdit::read_for_article(&article, conn)?;
Ok(ArticleView {
article,
edits,
@ -137,9 +137,8 @@ impl DbArticle {
// TODO: shouldnt have to read all edits from db
pub fn latest_edit_version(&self, conn: &Mutex<PgConnection>) -> MyResult<EditVersion> {
let mut conn = conn.lock().unwrap();
let edits: Vec<DbEdit> = DbEdit::belonging_to(&self).get_results(conn.deref_mut())?;
match edits.last().map(|e| e.version.clone()) {
let edits: Vec<DbEdit> = DbEdit::read_for_article(self, conn)?;
match edits.last().map(|e| e.hash.clone()) {
Some(latest_version) => Ok(latest_version),
None => Ok(EditVersion::default()),
}

108
src/database/conflict.rs Normal file
View 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)?,
}))
}
}
}
}

View file

@ -1,80 +1,71 @@
use crate::database::schema::edit;
use crate::database::version::EditVersion;
use crate::database::DbArticle;
use crate::error::MyResult;
use activitypub_federation::fetch::object_id::ObjectId;
use diesel::ExpressionMethods;
use diesel::{
insert_into, AsChangeset, Identifiable, Insertable, PgConnection, Queryable, RunQueryDsl,
insert_into, AsChangeset, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl,
Selectable,
};
use diesel::{Associations, BelongingToDsl};
use diesel_derive_newtype::DieselNewType;
use diffy::create_patch;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha224};
use std::ops::DerefMut;
use std::sync::Mutex;
/// Represents a single change to the article.
#[derive(
Clone,
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))]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable)]
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
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,
/// UUID built from sha224 hash of diff
pub hash: EditVersion,
pub ap_id: ObjectId<DbEdit>,
pub diff: String,
pub article_id: i32,
pub version: EditVersion,
// TODO: could be an Option<DbEdit.id> instead
pub previous_version: EditVersion,
/// First edit of an article always has `EditVersion::default()` here
pub previous_version_id: EditVersion,
}
#[derive(Debug, Clone, Insertable, AsChangeset)]
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
pub struct DbEditForm {
pub hash: EditVersion,
pub ap_id: ObjectId<DbEdit>,
pub diff: String,
pub article_id: i32,
pub version: EditVersion,
pub previous_version: EditVersion,
pub previous_version_id: EditVersion,
}
impl DbEditForm {
pub fn new(
original_article: &DbArticle,
updated_text: &str,
previous_version: EditVersion,
previous_version_id: EditVersion,
) -> MyResult<Self> {
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 {
hash: version,
ap_id,
diff: diff.to_string(),
article_id: original_article.id,
version: EditVersion(hash),
previous_version,
previous_version_id,
})
}
fn generate_ap_id_and_hash(
pub(crate) fn generate_ap_id(
article: &DbArticle,
diff: Vec<u8>,
) -> MyResult<(ObjectId<DbEdit>, String)> {
let mut sha224 = Sha224::new();
sha224.update(diff);
let hash = format!("{:X}", sha224.finalize());
Ok((
ObjectId::parse(&format!("{}/{}", article.ap_id, hash))?,
hash,
))
version: &EditVersion,
) -> MyResult<ObjectId<DbEdit>> {
Ok(ObjectId::parse(&format!(
"{}/{}",
article.ap_id,
version.hash()
))?)
}
}
@ -88,31 +79,20 @@ impl DbEdit {
.set(form)
.get_result(conn.deref_mut())?)
}
pub fn for_article(article: &DbArticle, conn: &Mutex<PgConnection>) -> MyResult<Vec<Self>> {
pub fn read(version: &EditVersion, conn: &Mutex<PgConnection>) -> MyResult<Self> {
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, _) =
DbEditForm::generate_ap_id_and_hash(article, self.diff.clone().into_bytes())?;
Ok(DbEditForm {
ap_id,
diff: self.diff,
article_id: article.id,
version: self.version,
previous_version: self.previous_version,
})
}
}
#[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)
pub fn read_for_article(
article: &DbArticle,
conn: &Mutex<PgConnection>,
) -> MyResult<Vec<Self>> {
let mut conn = conn.lock().unwrap();
Ok(edit::table
.filter(edit::dsl::article_id.eq(article.id))
.get_results(conn.deref_mut())?)
}
}

View file

@ -1,90 +1,27 @@
use crate::api::ApiConflict;
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::sync::{Arc, Mutex};
pub mod article;
pub mod conflict;
pub mod edit;
pub mod instance;
mod schema;
pub mod version;
#[derive(Clone)]
pub struct MyData {
pub db_connection: Arc<Mutex<PgConnection>>,
pub fake_db: Arc<FakeDatabase>,
}
impl Deref for MyData {
type Target = Arc<FakeDatabase>;
type Target = Arc<Mutex<PgConnection>>;
fn deref(&self) -> &Self::Target {
&self.fake_db
&self.db_connection
}
}
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)?,
}))
}
}
}
}

View file

@ -12,15 +12,24 @@ diesel::table! {
}
}
diesel::table! {
conflict (id) {
id -> Uuid,
diff -> Text,
article_id -> Int4,
previous_version_id -> Uuid,
}
}
diesel::table! {
edit (id) {
id -> Int4,
hash -> Uuid,
#[max_length = 255]
ap_id -> Varchar,
diff -> Text,
article_id -> Int4,
version -> Text,
previous_version -> Text,
previous_version_id -> Uuid,
}
}
@ -49,6 +58,7 @@ diesel::table! {
}
diesel::joinable!(article -> instance (instance_id));
diesel::joinable!(conflict -> 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
View 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(())
}

View file

@ -1,6 +1,7 @@
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::version::EditVersion;
use crate::database::MyDataHandle;
use crate::error::Error;
use crate::federation::activities::update_local_article::UpdateLocalArticle;
@ -30,12 +31,12 @@ pub async fn submit_article_update(
} else {
// dont insert edit into db, might be invalid in case of conflict
let edit = DbEdit {
id: 0,
id: -1,
hash: form.hash,
ap_id: form.ap_id,
diff: form.diff,
article_id: form.article_id,
version: form.version,
previous_version: form.previous_version,
previous_version_id: form.previous_version_id,
};
let instance = DbInstance::read(original_article.instance_id, &data.db_connection)?;
UpdateRemoteArticle::send(edit, instance, data).await?;

View file

@ -1,4 +1,6 @@
use crate::database::conflict::{DbConflict, DbConflictForm};
use crate::database::instance::DbInstance;
use crate::database::version::EditVersion;
use crate::database::MyDataHandle;
use crate::error::MyResult;
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,
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 url::Url;
@ -68,16 +66,15 @@ impl ActivityHandler for RejectEdit {
}
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
let mut lock = data.conflicts.lock().unwrap();
let conflict = DbConflict {
id: random(),
let article = self.object.object.dereference(data).await?;
let form = DbConflictForm {
id: EditVersion::new(&self.object.content)?,
diff: self.object.content,
article_id: self.object.object,
previous_version: self.object.previous_version,
article_id: article.id,
previous_version_id: self.object.previous_version,
};
lock.push(conflict);
DbConflict::create(&form, &data.db_connection)?;
Ok(())
}
}

View file

@ -1,6 +1,6 @@
use crate::database::article::DbArticleForm;
use crate::database::edit::EditVersion;
use crate::database::instance::DbInstance;
use crate::database::version::EditVersion;
use crate::database::{article::DbArticle, MyDataHandle};
use crate::error::Error;
use crate::federation::objects::edits_collection::DbEditCollection;

View file

@ -1,5 +1,6 @@
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::error::Error;
use activitypub_federation::config::Data;
@ -44,9 +45,8 @@ impl Object for DbEdit {
kind: EditType::Edit,
id: self.ap_id,
content: self.diff,
version: self.version,
// TODO: this is wrong
previous_version: self.previous_version,
version: self.hash,
previous_version: self.previous_version_id,
object: article.ap_id,
})
}
@ -65,8 +65,8 @@ impl Object for DbEdit {
ap_id: json.id,
diff: json.content,
article_id: article.id,
version: json.version,
previous_version: json.previous_version,
hash: json.version,
previous_version_id: json.previous_version,
};
let edit = DbEdit::create(&form, &data.db_connection)?;
Ok(edit)

View file

@ -1,6 +1,6 @@
use crate::api::api_routes;
use crate::database::instance::{DbInstance, DbInstanceForm};
use crate::database::{FakeDatabase, MyData};
use crate::database::MyData;
use crate::error::MyResult;
use crate::federation::routes::federation_routes;
use crate::utils::generate_activity_id;
@ -28,10 +28,6 @@ mod utils;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
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)?));
db_connection
.lock()
@ -39,10 +35,7 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
.run_pending_migrations(MIGRATIONS)
.unwrap();
let data = MyData {
db_connection,
fake_db,
};
let data = MyData { db_connection };
let config = FederationConfig::builder()
.domain(hostname)
.app_data(data)

View file

@ -1,5 +1,5 @@
use crate::database::edit::DbEdit;
use crate::database::edit::EditVersion;
use crate::database::version::EditVersion;
use crate::error::MyResult;
use anyhow::anyhow;
use diffy::{apply, Patch};
@ -31,7 +31,7 @@ pub fn generate_article_version(edits: &Vec<DbEdit>, version: &EditVersion) -> M
for e in edits {
let patch = Patch::from_str(&e.diff)?;
generated = apply(&generated, &patch)?;
if &e.version == version {
if &e.hash == version {
return Ok(generated);
}
}

View file

@ -1,12 +1,14 @@
use anyhow::anyhow;
use fediwiki::api::{
ApiConflict, CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
};
use fediwiki::database::article::ArticleView;
use fediwiki::database::conflict::ApiConflict;
use fediwiki::database::instance::DbInstance;
use fediwiki::error::MyResult;
use fediwiki::start;
use once_cell::sync::Lazy;
use reqwest::Client;
use reqwest::{Client, RequestBuilder, StatusCode};
use serde::de::Deserialize;
use serde::ser::Serialize;
use std::env::current_dir;
@ -142,7 +144,7 @@ pub async fn create_article(hostname: &str, title: String) -> MyResult<ArticleVi
let edit_form = EditArticleData {
article_id: article.article.id,
new_text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
previous_version: article.latest_version,
previous_version_id: article.latest_version,
resolve_conflict_id: None,
};
edit_article(hostname, &edit_form).await
@ -157,13 +159,10 @@ pub async fn edit_article_with_conflict(
hostname: &str,
edit_form: &EditArticleData,
) -> MyResult<Option<ApiConflict>> {
Ok(CLIENT
let req = CLIENT
.patch(format!("http://{}/api/v1/article", hostname))
.form(edit_form)
.send()
.await?
.json()
.await?)
.form(edit_form);
handle_json_res(req).await
}
pub async fn edit_article(hostname: &str, edit_form: &EditArticleData) -> MyResult<ArticleView> {
@ -184,25 +183,34 @@ where
T: for<'de> Deserialize<'de>,
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 {
res = res.query(&query);
req = req.query(&query);
}
let alpha_instance: T = res.send().await?.json().await?;
Ok(alpha_instance)
handle_json_res(req).await
}
pub async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
where
R: for<'de> Deserialize<'de>,
{
Ok(CLIENT
let req = CLIENT
.post(format!("http://{}/api/v1/{}", hostname, endpoint))
.form(form)
.send()
.await?
.json()
.await?)
.form(form);
handle_json_res(req).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<()> {

View file

@ -7,12 +7,11 @@ use crate::common::{
get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT,
};
use common::get;
use fediwiki::api::{
ApiConflict, EditArticleData, ForkArticleData, ResolveObject, SearchArticleData,
};
use fediwiki::api::{EditArticleData, ForkArticleData, ResolveObject, SearchArticleData};
use fediwiki::database::article::{ArticleView, DbArticle};
use fediwiki::error::MyResult;
use fediwiki::database::conflict::ApiConflict;
use fediwiki::database::instance::{DbInstance, InstanceView};
use pretty_assertions::{assert_eq, assert_ne};
use url::Url;
@ -42,7 +41,7 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
let edit_form = EditArticleData {
article_id: create_res.article.id,
new_text: "Lorem Ipsum 2".to_string(),
previous_version: get_res.latest_version,
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
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 {
article_id: create_res.article.id,
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,
};
edit_article(&data.alpha.hostname, &edit_form).await?;
@ -179,7 +178,7 @@ async fn test_edit_local_article() -> MyResult<()> {
let edit_form = EditArticleData {
article_id: create_res.article.id,
new_text: "Lorem Ipsum 2".to_string(),
previous_version: get_res.latest_version,
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
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 {
article_id: create_res.article.id,
new_text: "Lorem Ipsum 2".to_string(),
previous_version: get_res.latest_version,
previous_version_id: get_res.latest_version,
resolve_conflict_id: None,
};
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 {
article_id: create_res.article.id,
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,
};
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 {
article_id: create_res.article.id,
new_text: "Ipsum Lorem\n".to_string(),
previous_version: create_res.latest_version,
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
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 {
article_id: create_res.article.id,
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),
};
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 {
article_id: create_res.article.id,
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,
};
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 {
article_id: create_res.article.id,
new_text: "aaaa\n".to_string(),
previous_version: create_res.latest_version,
previous_version_id: create_res.latest_version,
resolve_conflict_id: None,
};
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 {
article_id: create_res.article.id,
new_text: "aaaa\n".to_string(),
previous_version: conflicts[0].previous_version.clone(),
resolve_conflict_id: Some(conflicts[0].id),
previous_version_id: conflicts[0].previous_version_id.clone(),
resolve_conflict_id: Some(conflicts[0].id.clone()),
};
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
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 {
article_id: create_res.article.id,
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,
};
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 {
article_id: create_res.article.id,
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,
};
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!(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].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_eq!(resolve_res.latest_version, fork_res.latest_version);
assert_ne!(resolved_article.ap_id, forked_article.ap_id);