diff --git a/migrations/2025-01-17-090151_edit_pending/down.sql b/migrations/2025-01-17-090151_edit_pending/down.sql new file mode 100644 index 0000000..2a57939 --- /dev/null +++ b/migrations/2025-01-17-090151_edit_pending/down.sql @@ -0,0 +1 @@ +alter table edit drop column pending; \ No newline at end of file diff --git a/migrations/2025-01-17-090151_edit_pending/up.sql b/migrations/2025-01-17-090151_edit_pending/up.sql new file mode 100644 index 0000000..3cfcd84 --- /dev/null +++ b/migrations/2025-01-17-090151_edit_pending/up.sql @@ -0,0 +1 @@ +alter table edit add column pending bool not null default false; \ No newline at end of file diff --git a/src/backend/api/article.rs b/src/backend/api/article.rs index 0b760c8..aa31a42 100644 --- a/src/backend/api/article.rs +++ b/src/backend/api/article.rs @@ -237,6 +237,7 @@ pub(in crate::backend::api) async fn fork_article( hash: e.hash, previous_version_id: e.previous_version_id, published: Utc::now(), + pending: false, }; DbEdit::create(&form, &data)?; } diff --git a/src/backend/api/mod.rs b/src/backend/api/mod.rs index b77f6a9..7d7ed47 100644 --- a/src/backend/api/mod.rs +++ b/src/backend/api/mod.rs @@ -92,6 +92,7 @@ pub(in crate::backend::api) async fn site_view( #[debug_handler] pub async fn edit_list( Query(query): Query, + user: Option>, data: Data, ) -> MyResult>> { let params = if let Some(article_id) = query.article_id { @@ -101,7 +102,7 @@ pub async fn edit_list( } else { return Err(anyhow!("Must provide article_id or person_id").into()); }; - Ok(Json(DbEdit::view(params, &data)?)) + Ok(Json(DbEdit::view(params, &user.map(|u| u.0), &data)?)) } /// Trims the string param, and converts to None if it is empty diff --git a/src/backend/database/conflict.rs b/src/backend/database/conflict.rs index 0b5c9bf..f0bbe47 100644 --- a/src/backend/database/conflict.rs +++ b/src/backend/database/conflict.rs @@ -1,6 +1,9 @@ use crate::{ backend::{ - database::{schema::conflict, IbisData}, + database::{ + schema::{conflict, edit}, + IbisData, + }, federation::activities::submit_article_update, utils::{error::MyResult, generate_article_version}, }, @@ -69,14 +72,21 @@ impl DbConflict { } /// Delete merge conflict which was created by specific user - pub fn delete(id: ConflictId, creator_id: PersonId, data: &IbisData) -> MyResult { + pub fn delete(id: ConflictId, creator_id: PersonId, data: &IbisData) -> MyResult<()> { let mut conn = data.db_pool.get()?; - Ok(delete( + let conflict: Self = delete( conflict::table .filter(conflict::dsl::creator_id.eq(creator_id)) .find(id), ) - .get_result(conn.deref_mut())?) + .get_result(conn.deref_mut())?; + delete( + edit::table + .filter(edit::dsl::creator_id.eq(creator_id)) + .filter(edit::dsl::hash.eq(conflict.hash)), + ) + .execute(conn.deref_mut())?; + Ok(()) } pub async fn to_api_conflict(&self, data: &Data) -> MyResult> { diff --git a/src/backend/database/edit.rs b/src/backend/database/edit.rs index 9010d7f..4426862 100644 --- a/src/backend/database/edit.rs +++ b/src/backend/database/edit.rs @@ -7,11 +7,21 @@ use crate::{ common::{ article::{DbArticle, DbEdit, EditVersion, EditView}, newtypes::{ArticleId, PersonId}, + user::LocalUserView, }, }; use activitypub_federation::fetch::object_id::ObjectId; use chrono::{DateTime, Utc}; -use diesel::{insert_into, AsChangeset, ExpressionMethods, Insertable, QueryDsl, RunQueryDsl}; +use diesel::{ + dsl::not, + insert_into, + AsChangeset, + BoolExpressionMethods, + ExpressionMethods, + Insertable, + QueryDsl, + RunQueryDsl, +}; use diffy::create_patch; use std::ops::DerefMut; @@ -26,6 +36,7 @@ pub struct DbEditForm { pub article_id: ArticleId, pub previous_version_id: EditVersion, pub published: DateTime, + pub pending: bool, } impl DbEditForm { @@ -35,6 +46,7 @@ impl DbEditForm { updated_text: &str, summary: String, previous_version_id: EditVersion, + pending: bool, ) -> MyResult { let diff = create_patch(&original_article.text, updated_text); let version = EditVersion::new(&diff.to_string()); @@ -48,6 +60,7 @@ impl DbEditForm { previous_version_id, summary, published: Utc::now(), + pending, }) } @@ -96,11 +109,18 @@ impl DbEdit { .get_results(conn.deref_mut())?) } - pub fn view(params: ViewEditParams, data: &IbisData) -> MyResult> { + pub fn view( + params: ViewEditParams, + user: &Option, + data: &IbisData, + ) -> MyResult> { let mut conn = data.db_pool.get()?; + let person_id = user.as_ref().map(|u| u.person.id).unwrap_or(PersonId(-1)); let query = edit::table .inner_join(article::table) .inner_join(person::table) + // only the creator can view pending edits + .filter(not(edit::pending).or(edit::creator_id.eq(person_id))) .into_boxed(); let query = match params { diff --git a/src/backend/database/schema.rs b/src/backend/database/schema.rs index e17ec4a..9e58f0f 100644 --- a/src/backend/database/schema.rs +++ b/src/backend/database/schema.rs @@ -40,6 +40,7 @@ diesel::table! { article_id -> Int4, previous_version_id -> Uuid, published -> Timestamptz, + pending -> Bool, } } diff --git a/src/backend/federation/activities/mod.rs b/src/backend/federation/activities/mod.rs index 4e9f52b..f8e507f 100644 --- a/src/backend/federation/activities/mod.rs +++ b/src/backend/federation/activities/mod.rs @@ -10,11 +10,10 @@ use crate::{ common::{ article::{DbArticle, DbEdit, EditVersion}, instance::DbInstance, - newtypes::{EditId, PersonId}, + newtypes::PersonId, }, }; use activitypub_federation::config::Data; -use chrono::Utc; pub mod accept; pub mod create_article; @@ -31,12 +30,13 @@ pub async fn submit_article_update( creator_id: PersonId, data: &Data, ) -> Result<(), Error> { - let form = DbEditForm::new( + let mut form = DbEditForm::new( original_article, creator_id, &new_text, summary, previous_version, + false, )?; if original_article.local { let edit = DbEdit::create(&form, data)?; @@ -44,18 +44,9 @@ pub async fn submit_article_update( UpdateLocalArticle::send(updated_article, vec![], data).await?; } else { - // dont insert edit into db, might be invalid in case of conflict - let edit = DbEdit { - id: EditId(-1), - creator_id, - hash: form.hash, - ap_id: form.ap_id, - diff: form.diff, - summary: form.summary, - article_id: form.article_id, - previous_version_id: form.previous_version_id, - published: Utc::now(), - }; + // insert edit as pending, so only the creator can see it + form.pending = true; + let edit = DbEdit::create(&form, data)?; let instance = DbInstance::read(original_article.instance_id, data)?; UpdateRemoteArticle::send(edit, instance, data).await?; } diff --git a/src/backend/federation/objects/edit.rs b/src/backend/federation/objects/edit.rs index 591363f..8d0945c 100644 --- a/src/backend/federation/objects/edit.rs +++ b/src/backend/federation/objects/edit.rs @@ -98,6 +98,7 @@ impl Object for DbEdit { hash: json.version, previous_version_id: json.previous_version, published: json.published, + pending: false, }; let edit = DbEdit::create(&form, data)?; Ok(edit) diff --git a/src/backend/utils/mod.rs b/src/backend/utils/mod.rs index 858f47a..d7c5bc6 100644 --- a/src/backend/utils/mod.rs +++ b/src/backend/utils/mod.rs @@ -57,6 +57,18 @@ pub(super) fn generate_article_version( Err(anyhow!("failed to generate article version").into()) } +/// Use a single static keypair during testing which is signficantly faster than +/// generating dozens of keys from scratch. +pub fn generate_keypair() -> MyResult { + if cfg!(debug_assertions) { + static KEYPAIR: LazyLock = + LazyLock::new(|| generate_actor_keypair().expect("generate keypair")); + Ok(KEYPAIR.clone()) + } else { + Ok(generate_actor_keypair()?) + } +} + #[cfg(test)] mod test { use super::*; @@ -81,6 +93,7 @@ mod test { article_id: ArticleId(0), previous_version_id: Default::default(), published: Utc::now(), + pending: false, }) }; Ok([ @@ -115,15 +128,3 @@ mod test { Ok(()) } } - -/// Use a single static keypair during testing which is signficantly faster than -/// generating dozens of keys from scratch. -pub fn generate_keypair() -> MyResult { - if cfg!(debug_assertions) { - static KEYPAIR: LazyLock = - LazyLock::new(|| generate_actor_keypair().expect("generate keypair")); - Ok(KEYPAIR.clone()) - } else { - Ok(generate_actor_keypair()?) - } -} diff --git a/src/common/article.rs b/src/common/article.rs index 097fca9..a277a16 100644 --- a/src/common/article.rs +++ b/src/common/article.rs @@ -123,6 +123,7 @@ pub struct DbEdit { /// First edit of an article always has `EditVersion::default()` here pub previous_version_id: EditVersion, pub published: DateTime, + pub pending: bool, } #[derive(Deserialize, Serialize, Clone, Debug, Default)] diff --git a/src/frontend/components/edit_list.rs b/src/frontend/components/edit_list.rs index 6fcec2d..8fecb02 100644 --- a/src/frontend/components/edit_list.rs +++ b/src/frontend/components/edit_list.rs @@ -42,9 +42,16 @@ pub fn EditList(edits: Vec, for_article: bool) -> impl IntoView { view! {
  • - - {edit.edit.summary} - +
    + + {edit.edit.summary} + + + + Pending + + +

    {second_line}

  • diff --git a/src/frontend/pages/diff.rs b/src/frontend/pages/diff.rs index 684d8a9..cb91a74 100644 --- a/src/frontend/pages/diff.rs +++ b/src/frontend/pages/diff.rs @@ -27,8 +27,16 @@ pub fn EditDiff() -> impl IntoView { edit.edit.summary, render_date_time(edit.edit.published), ); + let pending = edit.edit.pending; view! { -

    {label}

    +
    +

    {label}

    + + + Pending + + +

    "by " {user_link(&edit.creator)}

    diff --git a/tests/common.rs b/tests/common.rs
    index 93e73b5..8e9bea9 100644
    --- a/tests/common.rs
    +++ b/tests/common.rs
    @@ -32,8 +32,8 @@ impl TestData {
             INIT.call_once(|| {
                 env_logger::builder()
                     .filter_level(LevelFilter::Warn)
    -                .filter_module("activitypub_federation", LevelFilter::Info)
    -                .filter_module("ibis", LevelFilter::Info)
    +                //.filter_module("activitypub_federation", LevelFilter::Info)
    +                //.filter_module("ibis", LevelFilter::Info)
                     .init();
             });
     
    diff --git a/tests/test.rs b/tests/test.rs
    index 7b903e8..815130a 100644
    --- a/tests/test.rs
    +++ b/tests/test.rs
    @@ -28,7 +28,7 @@ async fn test_create_read_and_edit_local_article() -> Result<()> {
         let TestData(alpha, beta, gamma) = TestData::start(false).await;
     
         // create article
    -    const TITLE: &'static str = "Manu_Chao";
    +    const TITLE: &str = "Manu_Chao";
         let create_form = CreateArticleForm {
             title: "Manu Chao".to_string(),
             text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
    @@ -337,8 +337,8 @@ async fn test_edit_remote_article() -> Result<()> {
             .starts_with(&edit_res.article.ap_id.to_string()));
     
         // edit should be federated to beta and gamma
    -    let get_res = alpha.get_article(get_article_data_alpha).await.unwrap();
    -    let edits = alpha.get_article_edits(get_res.article.id).await.unwrap();
    +    let get_res = beta.get_article(get_article_data_alpha).await.unwrap();
    +    let edits = beta.get_article_edits(get_res.article.id).await.unwrap();
         assert_eq!(edit_res.article.title, get_res.article.title);
         assert_eq!(edits.len(), 2);
         assert_eq!(edit_res.article.text, get_res.article.text);
    @@ -456,7 +456,7 @@ async fn test_federated_edit_conflict() -> Result<()> {
         let edit_form = EditArticleForm {
             article_id: get_res.article.id,
             new_text: "Lorem Ipsum\n".to_string(),
    -        summary: "summary".to_string(),
    +        summary: "first edit".to_string(),
             previous_version_id: create_res.latest_version.clone(),
             resolve_conflict_id: None,
         };
    @@ -476,14 +476,15 @@ async fn test_federated_edit_conflict() -> Result<()> {
         let edit_form = EditArticleForm {
             article_id: resolve_res.article.id,
             new_text: "aaaa\n".to_string(),
    -        summary: "summary".to_string(),
    +        summary: "second edit".to_string(),
             previous_version_id: create_res.latest_version,
             resolve_conflict_id: None,
         };
         let edit_res = gamma.edit_article(&edit_form).await.unwrap();
         let gamma_edits = gamma.get_article_edits(edit_res.article.id).await.unwrap();
         assert_ne!(edit_form.new_text, edit_res.article.text);
    -    assert_eq!(1, gamma_edits.len());
    +    assert_eq!(2, gamma_edits.len());
    +    assert!(gamma_edits[1].edit.pending);
         assert!(!edit_res.article.local);
     
         assert_eq!(1, gamma.notifications_count().await.unwrap());
    @@ -497,7 +498,7 @@ async fn test_federated_edit_conflict() -> Result<()> {
         let edit_form = EditArticleForm {
             article_id: resolve_res.article.id,
             new_text: "aaaa\n".to_string(),
    -        summary: "summary".to_string(),
    +        summary: "resolve conflict".to_string(),
             previous_version_id: conflict.previous_version_id.clone(),
             resolve_conflict_id: Some(conflict.id),
         };
    @@ -505,6 +506,7 @@ async fn test_federated_edit_conflict() -> Result<()> {
         let gamma_edits = gamma.get_article_edits(edit_res.article.id).await.unwrap();
         assert_eq!(edit_form.new_text, edit_res.article.text);
         assert_eq!(3, gamma_edits.len());
    +    assert!(gamma_edits.iter().all(|e| !e.edit.pending));
     
         assert_eq!(0, gamma.notifications_count().await.unwrap());
         let notifications = gamma.notifications_list().await.unwrap();