diff --git a/src/api.rs b/src/api.rs index 237e29e..0b9ff32 100644 --- a/src/api.rs +++ b/src/api.rs @@ -273,11 +273,10 @@ async fn fork_article( let article = DbArticle::create(&form, &data.db_connection)?; // copy edits to new article - // TODO: convert to sql + // this could also be done in sql let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?; for e in edits { let ap_id = DbEditForm::generate_ap_id(&article, &e.hash)?; - // TODO: id gives db unique violation let form = DbEditForm { ap_id, diff: e.diff, @@ -285,7 +284,7 @@ async fn fork_article( hash: e.hash, previous_version_id: e.previous_version_id, }; - dbg!(DbEdit::create(&form, &data.db_connection))?; + DbEdit::create(&form, &data.db_connection)?; } CreateArticle::send_to_followers(article.clone(), &data).await?; diff --git a/src/database/article.rs b/src/database/article.rs index 830d635..963ac57 100644 --- a/src/database/article.rs +++ b/src/database/article.rs @@ -1,6 +1,6 @@ use crate::database::edit::DbEdit; -use crate::database::schema::article; +use crate::database::schema::{article, edit}; use crate::error::MyResult; use crate::federation::objects::edits_collection::DbEditCollection; use activitypub_federation::fetch::collection_id::CollectionId; @@ -76,7 +76,7 @@ impl DbArticle { .get_result::(conn.deref_mut())?) } - pub fn read(id: i32, conn: &Mutex) -> MyResult { + pub fn read(id: i32, conn: &Mutex) -> MyResult { let mut conn = conn.lock().unwrap(); Ok(article::table.find(id).get_result(conn.deref_mut())?) } @@ -98,14 +98,14 @@ impl DbArticle { pub fn read_from_ap_id( ap_id: &ObjectId, conn: &Mutex, - ) -> MyResult { + ) -> MyResult { let mut conn = conn.lock().unwrap(); Ok(article::table .filter(article::dsl::ap_id.eq(ap_id)) .get_result(conn.deref_mut())?) } - pub fn read_local_title(title: &str, conn: &Mutex) -> MyResult { + pub fn read_local_title(title: &str, conn: &Mutex) -> MyResult { let mut conn = conn.lock().unwrap(); Ok(article::table .filter(article::dsl::title.eq(title)) @@ -113,14 +113,14 @@ impl DbArticle { .get_result(conn.deref_mut())?) } - pub fn read_all_local(conn: &Mutex) -> MyResult> { + pub fn read_all_local(conn: &Mutex) -> MyResult> { let mut conn = conn.lock().unwrap(); Ok(article::table .filter(article::dsl::local.eq(true)) .get_results(conn.deref_mut())?) } - pub fn search(query: &str, conn: &Mutex) -> MyResult> { + pub fn search(query: &str, conn: &Mutex) -> MyResult> { let mut conn = conn.lock().unwrap(); let replaced = query .replace('%', "\\%") @@ -135,10 +135,16 @@ impl DbArticle { .get_results(conn.deref_mut())?) } - // TODO: shouldnt have to read all edits from db pub fn latest_edit_version(&self, conn: &Mutex) -> MyResult { - let edits: Vec = DbEdit::read_for_article(self, conn)?; - match edits.last().map(|e| e.hash.clone()) { + let mut conn = conn.lock().unwrap(); + let latest_version: Option = edit::table + .filter(edit::dsl::article_id.eq(self.id)) + .order_by(edit::dsl::id.desc()) + .limit(1) + .select(edit::dsl::hash) + .get_result(conn.deref_mut()) + .ok(); + match latest_version { Some(latest_version) => Ok(latest_version), None => Ok(EditVersion::default()), } diff --git a/src/database/conflict.rs b/src/database/conflict.rs index 3f10645..24b388d 100644 --- a/src/database/conflict.rs +++ b/src/database/conflict.rs @@ -52,6 +52,7 @@ impl DbConflict { .values(form) .get_result(conn.deref_mut())?) } + pub fn list(conn: &Mutex) -> MyResult> { let mut conn = conn.lock().unwrap(); Ok(conflict::table.get_results(conn.deref_mut())?) @@ -60,7 +61,6 @@ impl DbConflict { /// Delete a merge conflict after it is resolved. pub fn delete(id: EditVersion, conn: &Mutex) -> MyResult { 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())?) } diff --git a/src/database/edit.rs b/src/database/edit.rs index b2b2d6f..7e43497 100644 --- a/src/database/edit.rs +++ b/src/database/edit.rs @@ -79,6 +79,7 @@ impl DbEdit { .set(form) .get_result(conn.deref_mut())?) } + pub fn read(version: &EditVersion, conn: &Mutex) -> MyResult { let mut conn = conn.lock().unwrap(); Ok(edit::table @@ -86,6 +87,13 @@ impl DbEdit { .get_result(conn.deref_mut())?) } + pub fn read_from_ap_id(ap_id: &ObjectId, conn: &Mutex) -> MyResult { + let mut conn = conn.lock().unwrap(); + Ok(edit::table + .filter(edit::dsl::ap_id.eq(ap_id)) + .get_result(conn.deref_mut())?) + } + pub fn read_for_article( article: &DbArticle, conn: &Mutex, diff --git a/src/database/instance.rs b/src/database/instance.rs index e776ed1..6bd9f95 100644 --- a/src/database/instance.rs +++ b/src/database/instance.rs @@ -1,14 +1,10 @@ use crate::database::schema::{instance, instance_follow}; use crate::database::MyDataHandle; -use crate::error::{Error, MyResult}; - +use crate::error::MyResult; use crate::federation::objects::articles_collection::DbArticleCollection; -use activitypub_federation::activity_sending::SendActivityTask; use activitypub_federation::config::Data; use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::fetch::object_id::ObjectId; -use activitypub_federation::protocol::context::WithContext; -use activitypub_federation::traits::ActivityHandler; use chrono::{DateTime, Utc}; use diesel::ExpressionMethods; use diesel::{ @@ -19,8 +15,6 @@ use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::ops::DerefMut; use std::sync::Mutex; -use tracing::warn; -use url::Url; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)] #[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))] @@ -59,62 +53,6 @@ pub struct InstanceView { } impl DbInstance { - pub fn followers_url(&self) -> MyResult { - Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) - } - - pub fn follower_ids(&self, data: &Data) -> MyResult> { - Ok(DbInstance::read_followers(self.id, &data.db_connection)? - .into_iter() - .map(|f| f.ap_id.into()) - .collect()) - } - - pub async fn send_to_followers( - &self, - activity: Activity, - extra_recipients: Vec, - data: &Data, - ) -> Result<(), ::Error> - where - Activity: ActivityHandler + Serialize + Debug + Send + Sync, - ::Error: From, - ::Error: From, - { - let mut inboxes: Vec<_> = DbInstance::read_followers(self.id, &data.db_connection)? - .iter() - .map(|f| Url::parse(&f.inbox_url).unwrap()) - .collect(); - inboxes.extend( - extra_recipients - .into_iter() - .map(|i| Url::parse(&i.inbox_url).unwrap()), - ); - self.send(activity, inboxes, data).await?; - Ok(()) - } - - pub async fn send( - &self, - activity: Activity, - recipients: Vec, - data: &Data, - ) -> Result<(), ::Error> - where - Activity: ActivityHandler + Serialize + Debug + Send + Sync, - ::Error: From, - { - let activity = WithContext::new_default(activity); - let sends = SendActivityTask::prepare(&activity, self, recipients, data).await?; - for send in sends { - let send = send.sign_and_send(data).await; - if let Err(e) = send { - warn!("Failed to send activity {:?}: {e}", activity); - } - } - Ok(()) - } - pub fn create(form: &DbInstanceForm, conn: &Mutex) -> MyResult { let mut conn = conn.lock().unwrap(); Ok(insert_into(instance::table) diff --git a/src/federation/objects/edit.rs b/src/federation/objects/edit.rs index 54fd9bf..3dad5ec 100644 --- a/src/federation/objects/edit.rs +++ b/src/federation/objects/edit.rs @@ -33,10 +33,10 @@ impl Object for DbEdit { type Error = Error; async fn read_from_id( - _object_id: Url, - _data: &Data, + object_id: Url, + data: &Data, ) -> Result, Self::Error> { - todo!() + Ok(DbEdit::read_from_ap_id(&object_id.into(), data).ok()) } async fn into_json(self, data: &Data) -> Result { diff --git a/src/federation/objects/instance.rs b/src/federation/objects/instance.rs index 002939e..6c824c5 100644 --- a/src/federation/objects/instance.rs +++ b/src/federation/objects/instance.rs @@ -1,9 +1,12 @@ use crate::database::instance::{DbInstance, DbInstanceForm}; use crate::database::MyDataHandle; -use crate::error::Error; +use crate::error::{Error, MyResult}; use crate::federation::objects::articles_collection::DbArticleCollection; +use activitypub_federation::activity_sending::SendActivityTask; use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::kinds::actor::ServiceType; +use activitypub_federation::protocol::context::WithContext; +use activitypub_federation::traits::ActivityHandler; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, @@ -13,6 +16,7 @@ use activitypub_federation::{ use chrono::{DateTime, Local, Utc}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; +use tracing::log::warn; use url::Url; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -26,6 +30,64 @@ pub struct ApubInstance { public_key: PublicKey, } +impl DbInstance { + pub fn followers_url(&self) -> MyResult { + Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) + } + + pub fn follower_ids(&self, data: &Data) -> MyResult> { + Ok(DbInstance::read_followers(self.id, &data.db_connection)? + .into_iter() + .map(|f| f.ap_id.into()) + .collect()) + } + + pub async fn send_to_followers( + &self, + activity: Activity, + extra_recipients: Vec, + data: &Data, + ) -> Result<(), ::Error> + where + Activity: ActivityHandler + Serialize + Debug + Send + Sync, + ::Error: From, + ::Error: From, + { + let mut inboxes: Vec<_> = DbInstance::read_followers(self.id, &data.db_connection)? + .iter() + .map(|f| Url::parse(&f.inbox_url).unwrap()) + .collect(); + inboxes.extend( + extra_recipients + .into_iter() + .map(|i| Url::parse(&i.inbox_url).unwrap()), + ); + self.send(activity, inboxes, data).await?; + Ok(()) + } + + pub async fn send( + &self, + activity: Activity, + recipients: Vec, + data: &Data, + ) -> Result<(), ::Error> + where + Activity: ActivityHandler + Serialize + Debug + Send + Sync, + ::Error: From, + { + let activity = WithContext::new_default(activity); + let sends = SendActivityTask::prepare(&activity, self, recipients, data).await?; + for send in sends { + let send = send.sign_and_send(data).await; + if let Err(e) = send { + warn!("Failed to send activity {:?}: {e}", activity); + } + } + Ok(()) + } +} + #[async_trait::async_trait] impl Object for DbInstance { type DataType = MyDataHandle; diff --git a/src/utils.rs b/src/utils.rs index d579323..5cf1a4d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -37,3 +37,54 @@ pub fn generate_article_version(edits: &Vec, version: &EditVersion) -> M } Err(anyhow!("failed to generate article version").into()) } + +#[cfg(test)] +mod test { + use super::*; + use activitypub_federation::fetch::object_id::ObjectId; + use diffy::create_patch; + + fn create_edits() -> MyResult> { + let generate_edit = |a, b| -> MyResult { + let diff = create_patch(a, b).to_string(); + Ok(DbEdit { + id: 0, + hash: EditVersion::new(&diff)?, + ap_id: ObjectId::parse("http://example.com")?, + diff, + article_id: 0, + previous_version_id: Default::default(), + }) + }; + Ok([ + generate_edit("", "test\n")?, + generate_edit("test\n", "sda\n")?, + generate_edit("sda\n", "123\n")?, + ] + .to_vec()) + } + + #[test] + fn test_generate_article_version() -> MyResult<()> { + let edits = create_edits()?; + let generated = generate_article_version(&edits, &edits[1].hash)?; + assert_eq!("sda\n", generated); + Ok(()) + } + + #[test] + fn test_generate_invalid_version() -> MyResult<()> { + let edits = create_edits()?; + let generated = generate_article_version(&edits, &EditVersion::new("invalid")?); + assert!(generated.is_err()); + Ok(()) + } + + #[test] + fn test_generate_first_version() -> MyResult<()> { + let edits = create_edits()?; + let generated = generate_article_version(&edits, &EditVersion::default())?; + assert_eq!("", generated); + Ok(()) + } +} diff --git a/tests/test.rs b/tests/test.rs index 89dd363..feb0709 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -16,7 +16,6 @@ use fediwiki::database::instance::{DbInstance, InstanceView}; use pretty_assertions::{assert_eq, assert_ne}; use url::Url; -// TODO: can run tests in parallel if we use different ports #[tokio::test] async fn test_create_read_and_edit_article() -> MyResult<()> { let data = TestData::start(); @@ -354,8 +353,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { }; let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?; assert_ne!(edit_form.new_text, edit_res.article.text); - // TODO - //assert_eq!(2, edit_res.edits.len()); + assert_eq!(1, edit_res.edits.len()); assert!(!edit_res.article.local); let conflicts: Vec =