diff --git a/src/api.rs b/src/api.rs index c11467a..33960f1 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,10 +1,9 @@ use crate::database::DatabaseHandle; - use crate::error::MyResult; -use crate::federation::activities::create_or_update_article::{ - CreateOrUpdateArticle, CreateOrUpdateType, -}; +use crate::federation::activities::create_article::CreateArticle; +use crate::federation::activities::update_article::UpdateArticle; use crate::federation::objects::article::DbArticle; +use crate::federation::objects::edit::DbEdit; use crate::federation::objects::instance::DbInstance; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; @@ -15,7 +14,6 @@ use axum::{Form, Json, Router}; use axum_macros::debug_handler; use serde::{Deserialize, Serialize}; use url::Url; -use crate::federation::objects::edit::DbEdit; pub fn api_routes() -> Router { Router::new() @@ -29,7 +27,7 @@ pub fn api_routes() -> Router { } #[derive(Deserialize, Serialize)] -pub struct CreateArticle { +pub struct CreateArticleData { pub title: String, pub text: String, } @@ -38,7 +36,7 @@ pub struct CreateArticle { #[debug_handler] async fn create_article( data: Data, - Form(create_article): Form, + Form(create_article): Form, ) -> MyResult> { let local_instance_id = data.local_instance().ap_id; let ap_id = Url::parse(&format!( @@ -61,27 +59,21 @@ async fn create_article( articles.insert(article.ap_id.inner().clone(), article.clone()); } - CreateOrUpdateArticle::send_to_local_followers( - article.clone(), - CreateOrUpdateType::Create, - &data, - ) - .await?; + CreateArticle::send_to_followers(article.clone(), &data).await?; Ok(Json(article)) } #[derive(Deserialize, Serialize)] -pub struct EditArticle { +pub struct EditArticleData { pub ap_id: ObjectId, pub new_text: String, } -// TODO: this should create an edit object #[debug_handler] async fn edit_article( data: Data, - Form(edit_article): Form, + Form(edit_article): Form, ) -> MyResult> { let original_article = { let mut lock = data.articles.lock().unwrap(); @@ -93,28 +85,23 @@ async fn edit_article( let mut lock = data.articles.lock().unwrap(); let article = lock.get_mut(edit_article.ap_id.inner()).unwrap(); article.text = edit_article.new_text; - article.edits.push(edit); + article.edits.push(edit.clone()); article.clone() }; - CreateOrUpdateArticle::send_to_local_followers( - updated_article.clone(), - CreateOrUpdateType::Update, - &data, - ) - .await?; + UpdateArticle::send_to_followers(updated_article.clone(), edit, &data).await?; Ok(Json(updated_article)) } #[derive(Deserialize, Serialize, Clone)] -pub struct GetArticle { +pub struct GetArticleData { pub title: String, } #[debug_handler] async fn get_article( - Query(query): Query, + Query(query): Query, data: Data, ) -> MyResult> { let articles = data.articles.lock().unwrap(); diff --git a/src/federation/activities/accept.rs b/src/federation/activities/accept.rs index 487be2b..6bd3934 100644 --- a/src/federation/activities/accept.rs +++ b/src/federation/activities/accept.rs @@ -1,6 +1,6 @@ use crate::error::MyResult; use crate::federation::objects::instance::DbInstance; -use crate::utils::generate_object_id; +use crate::utils::generate_activity_id; use crate::{database::DatabaseHandle, federation::activities::follow::Follow}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, kinds::activity::AcceptType, traits::ActivityHandler, @@ -20,7 +20,7 @@ pub struct Accept { impl Accept { pub fn new(actor: ObjectId, object: Follow) -> MyResult { - let id = generate_object_id(actor.inner())?; + let id = generate_activity_id(actor.inner())?; Ok(Accept { actor, object, diff --git a/src/federation/activities/create_or_update_article.rs b/src/federation/activities/create_article.rs similarity index 60% rename from src/federation/activities/create_or_update_article.rs rename to src/federation/activities/create_article.rs index ae18080..fa869c5 100644 --- a/src/federation/activities/create_or_update_article.rs +++ b/src/federation/activities/create_article.rs @@ -2,7 +2,8 @@ use crate::database::DatabaseHandle; use crate::error::MyResult; use crate::federation::objects::article::{ApubArticle, DbArticle}; use crate::federation::objects::instance::DbInstance; -use crate::utils::generate_object_id; +use crate::utils::generate_activity_id; +use activitypub_federation::kinds::activity::CreateType; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, @@ -12,57 +13,41 @@ use activitypub_federation::{ use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub enum CreateOrUpdateType { - Create, - Update, -} - -// TODO: temporary placeholder, later rework this to send diffs #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct CreateOrUpdateArticle { +pub struct CreateArticle { pub actor: ObjectId, #[serde(deserialize_with = "deserialize_one_or_many")] pub to: Vec, pub object: ApubArticle, #[serde(rename = "type")] - pub kind: CreateOrUpdateType, + pub kind: CreateType, pub id: Url, } -impl CreateOrUpdateArticle { - pub async fn send_to_local_followers( +impl CreateArticle { + pub async fn send_to_followers( article: DbArticle, - kind: CreateOrUpdateType, data: &Data, ) -> MyResult<()> { let local_instance = data.local_instance(); - let to = local_instance - .followers - .iter() - .map(|f| f.ap_id.inner().clone()) - .collect(); let object = article.clone().into_json(data).await?; - let id = generate_object_id(local_instance.ap_id.inner())?; - let create_or_update = CreateOrUpdateArticle { + let id = generate_activity_id(local_instance.ap_id.inner())?; + let create_or_update = CreateArticle { actor: local_instance.ap_id.clone(), - to, + to: local_instance.follower_ids(), object, - kind, + kind: Default::default(), id, }; - let inboxes = local_instance - .followers - .iter() - .map(|f| f.inbox.clone()) - .collect(); - local_instance.send(create_or_update, inboxes, data).await?; + local_instance + .send_to_followers(create_or_update, data) + .await?; Ok(()) } } #[async_trait::async_trait] -impl ActivityHandler for CreateOrUpdateArticle { +impl ActivityHandler for CreateArticle { type DataType = DatabaseHandle; type Error = crate::error::Error; diff --git a/src/federation/activities/follow.rs b/src/federation/activities/follow.rs index ffab0ff..de156d9 100644 --- a/src/federation/activities/follow.rs +++ b/src/federation/activities/follow.rs @@ -1,6 +1,8 @@ use crate::error::MyResult; use crate::federation::objects::instance::DbInstance; -use crate::{database::DatabaseHandle, federation::activities::accept::Accept, generate_object_id}; +use crate::{ + database::DatabaseHandle, federation::activities::accept::Accept, generate_activity_id, +}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, @@ -22,7 +24,7 @@ pub struct Follow { impl Follow { pub fn new(actor: ObjectId, object: ObjectId) -> MyResult { - let id = generate_object_id(actor.inner())?; + let id = generate_activity_id(actor.inner())?; Ok(Follow { actor, object, diff --git a/src/federation/activities/mod.rs b/src/federation/activities/mod.rs index b65cebb..a2cba29 100644 --- a/src/federation/activities/mod.rs +++ b/src/federation/activities/mod.rs @@ -1,3 +1,4 @@ pub mod accept; -pub mod create_or_update_article; +pub mod create_article; pub mod follow; +pub mod update_article; diff --git a/src/federation/activities/update_article.rs b/src/federation/activities/update_article.rs new file mode 100644 index 0000000..60edb36 --- /dev/null +++ b/src/federation/activities/update_article.rs @@ -0,0 +1,80 @@ +use crate::database::DatabaseHandle; +use crate::error::MyResult; +use crate::federation::objects::article::DbArticle; +use crate::federation::objects::edit::{ApubEdit, DbEdit}; +use crate::federation::objects::instance::DbInstance; +use crate::utils::generate_activity_id; +use activitypub_federation::kinds::activity::CreateType; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + protocol::helpers::deserialize_one_or_many, + traits::{ActivityHandler, Object}, +}; +use diffy::{apply, Patch}; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UpdateArticle { + pub actor: ObjectId, + #[serde(deserialize_with = "deserialize_one_or_many")] + pub to: Vec, + pub object: ObjectId, + pub result: ApubEdit, + #[serde(rename = "type")] + pub kind: CreateType, + pub id: Url, +} + +impl UpdateArticle { + pub async fn send_to_followers( + article: DbArticle, + edit: DbEdit, + data: &Data, + ) -> MyResult<()> { + let local_instance = data.local_instance(); + let id = generate_activity_id(local_instance.ap_id.inner())?; + let create_or_update = UpdateArticle { + actor: local_instance.ap_id.clone(), + to: local_instance.follower_ids(), + object: article.ap_id, + result: edit.into_json(data).await?, + kind: Default::default(), + id, + }; + local_instance + .send_to_followers(create_or_update, data) + .await?; + Ok(()) + } +} +#[async_trait::async_trait] +impl ActivityHandler for UpdateArticle { + type DataType = DatabaseHandle; + type Error = crate::error::Error; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { + Ok(()) + } + + async fn receive(self, data: &Data) -> Result<(), Self::Error> { + let edit = DbEdit::from_json(self.result.clone(), data).await?; + let mut lock = data.articles.lock().unwrap(); + let article = lock.get_mut(self.object.inner()).unwrap(); + article.edits.push(edit); + // TODO: probably better to apply patch inside DbEdit::from_json() + let patch = Patch::from_str(&self.result.diff)?; + article.text = apply(&article.text, &patch)?; + Ok(()) + } +} diff --git a/src/federation/diff.rs b/src/federation/diff.rs deleted file mode 100644 index 6108c10..0000000 --- a/src/federation/diff.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[test] -fn test_diff() { - use diffy::create_patch; - - let original = "The Way of Kings\nWords of Radiance\n"; - let modified = "The Way of Kings\nWords of Radiance\nOathbringer\n"; - - let patch = create_patch(original, modified); - assert_eq!("--- original\n+++ modified\n@@ -1,2 +1,3 @@\n The Way of Kings\n Words of Radiance\n+Oathbringer\n", patch.to_string()); -} diff --git a/src/federation/mod.rs b/src/federation/mod.rs index 2d88170..e451aa0 100644 --- a/src/federation/mod.rs +++ b/src/federation/mod.rs @@ -9,7 +9,6 @@ use std::sync::{Arc, Mutex}; use url::Url; pub mod activities; -mod diff; pub mod objects; pub mod routes; diff --git a/src/federation/objects/edit.rs b/src/federation/objects/edit.rs index cadb9b6..2b12182 100644 --- a/src/federation/objects/edit.rs +++ b/src/federation/objects/edit.rs @@ -6,8 +6,8 @@ use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::traits::Object; use diffy::create_patch; use serde::{Deserialize, Serialize}; +use sha2::Digest; use sha2::Sha224; -use sha2::{Digest}; use url::Url; /// Represents a single change to the article. @@ -28,7 +28,7 @@ impl DbEdit { Ok(DbEdit { id: edit_id, diff: diff.to_string(), - local: true + local: true, }) } } @@ -44,7 +44,7 @@ pub struct ApubEdit { #[serde(rename = "type")] kind: EditType, id: ObjectId, - diff: String, + pub(crate) diff: String, } #[async_trait::async_trait] diff --git a/src/federation/objects/edits_collection.rs b/src/federation/objects/edits_collection.rs index e6fda59..1eeaaa9 100644 --- a/src/federation/objects/edits_collection.rs +++ b/src/federation/objects/edits_collection.rs @@ -75,7 +75,11 @@ impl Collection for DbEditCollection { try_join_all(apub.items.into_iter().map(|i| DbEdit::from_json(i, data))).await?; let mut articles = data.articles.lock().unwrap(); let article = articles.get_mut(owner.ap_id.inner()).unwrap(); - let edit_ids = article.edits.iter().map(|e| e.id.clone()).collect::>(); + let edit_ids = article + .edits + .iter() + .map(|e| e.id.clone()) + .collect::>(); for e in edits.clone() { if !edit_ids.contains(&&e.id) { article.edits.push(e); diff --git a/src/federation/objects/instance.rs b/src/federation/objects/instance.rs index c46cbec..f624105 100644 --- a/src/federation/objects/instance.rs +++ b/src/federation/objects/instance.rs @@ -1,4 +1,4 @@ -use crate::error::Error; +use crate::error::{Error, MyResult}; use crate::federation::objects::articles_collection::DbArticleCollection; use crate::{database::DatabaseHandle, federation::activities::follow::Follow}; use activitypub_federation::activity_sending::SendActivityTask; @@ -40,10 +40,17 @@ pub struct Instance { } impl DbInstance { - pub fn followers_url(&self) -> Result { + pub fn followers_url(&self) -> MyResult { Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) } + pub fn follower_ids(&self) -> Vec { + self.followers + .iter() + .map(|f| f.ap_id.inner().clone()) + .collect() + } + pub async fn follow( &self, other: &DbInstance, @@ -55,7 +62,26 @@ impl DbInstance { Ok(()) } - pub(crate) async fn send( + pub async fn send_to_followers( + &self, + activity: Activity, + data: &Data, + ) -> Result<(), ::Error> + where + Activity: ActivityHandler + Serialize + Debug + Send + Sync, + ::Error: From, + { + let local_instance = data.local_instance(); + let inboxes = local_instance + .followers + .iter() + .map(|f| f.inbox.clone()) + .collect(); + local_instance.send(activity, inboxes, data).await?; + Ok(()) + } + + pub async fn send( &self, activity: Activity, recipients: Vec, diff --git a/src/federation/routes.rs b/src/federation/routes.rs index bd0cb57..b65b50a 100644 --- a/src/federation/routes.rs +++ b/src/federation/routes.rs @@ -12,7 +12,8 @@ use activitypub_federation::traits::Object; use activitypub_federation::traits::{ActivityHandler, Collection}; use axum::extract::Path; -use crate::federation::activities::create_or_update_article::CreateOrUpdateArticle; +use crate::federation::activities::create_article::CreateArticle; +use crate::federation::activities::update_article::UpdateArticle; use crate::federation::objects::article::ApubArticle; use crate::federation::objects::articles_collection::{ArticleCollection, DbArticleCollection}; use crate::federation::objects::edits_collection::{ApubEditCollection, DbEditCollection}; @@ -82,7 +83,8 @@ async fn http_get_article_edits( pub enum InboxActivities { Follow(Follow), Accept(Accept), - CreateOrUpdateArticle(CreateOrUpdateArticle), + CreateArticle(CreateArticle), + UpdateArticle(UpdateArticle), } #[debug_handler] diff --git a/src/lib.rs b/src/lib.rs index c7f29f2..18614af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use crate::utils::generate_object_id; +use crate::utils::generate_activity_id; use activitypub_federation::config::FederationMiddleware; use axum::{Router, Server}; diff --git a/src/utils.rs b/src/utils.rs index 03a0b28..65add18 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,7 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng}; use url::{ParseError, Url}; -/// Just generate random url as object id. In a real project, you probably want to use -/// an url which contains the database id for easy retrieval (or store the random id in db). -pub fn generate_object_id(domain: &Url) -> Result { +pub fn generate_activity_id(domain: &Url) -> Result { let port = domain.port().unwrap(); let domain = domain.domain().unwrap(); let id: String = thread_rng() @@ -11,5 +9,5 @@ pub fn generate_object_id(domain: &Url) -> Result { .take(7) .map(char::from) .collect(); - Url::parse(&format!("http://{}:{}/objects/{}", domain,port, id)) + Url::parse(&format!("http://{}:{}/objects/{}", domain, port, id)) } diff --git a/tests/test.rs b/tests/test.rs index 7278455..3d6f2ce 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -4,7 +4,7 @@ mod common; use crate::common::{follow_instance, get_query, patch, post, TestData}; use common::get; -use fediwiki::api::{CreateArticle, EditArticle, GetArticle, ResolveObject}; +use fediwiki::api::{CreateArticleData, EditArticleData, GetArticleData, ResolveObject}; use fediwiki::error::MyResult; use fediwiki::federation::objects::article::DbArticle; use fediwiki::federation::objects::instance::DbInstance; @@ -17,7 +17,7 @@ async fn test_create_and_read_article() -> MyResult<()> { let data = TestData::start(); // error on nonexistent article - let get_article = GetArticle { + let get_article = GetArticleData { title: "Manu_Chao".to_string(), }; let not_found = get_query::( @@ -29,7 +29,7 @@ async fn test_create_and_read_article() -> MyResult<()> { assert!(not_found.is_err()); // create article - let create_article = CreateArticle { + let create_article = CreateArticleData { title: get_article.title.to_string(), text: "Lorem ipsum".to_string(), }; @@ -80,7 +80,7 @@ async fn test_synchronize_articles() -> MyResult<()> { let data = TestData::start(); // create article on alpha - let create_article = CreateArticle { + let create_article = CreateArticleData { title: "Manu_Chao".to_string(), text: "Lorem ipsum".to_string(), }; @@ -89,7 +89,7 @@ async fn test_synchronize_articles() -> MyResult<()> { assert!(create_res.local); // article is not yet on beta - let get_article = GetArticle { + let get_article = GetArticleData { title: "Manu_Chao".to_string(), }; let get_res = get_query::( @@ -129,7 +129,7 @@ async fn test_federate_article_changes() -> MyResult<()> { follow_instance(data.hostname_alpha, data.hostname_beta).await?; // create new article - let create_form = CreateArticle { + let create_form = CreateArticleData { title: "Manu_Chao".to_string(), text: "Lorem ipsum".to_string(), }; @@ -137,7 +137,7 @@ async fn test_federate_article_changes() -> MyResult<()> { assert_eq!(create_res.title, create_form.title); // article should be federated to alpha - let get_article = GetArticle { + let get_article = GetArticleData { title: create_res.title.clone(), }; let get_res = @@ -147,17 +147,20 @@ async fn test_federate_article_changes() -> MyResult<()> { assert_eq!(create_res.text, get_res.text); // edit the article - let edit_form = EditArticle { + let edit_form = EditArticleData { ap_id: create_res.ap_id, new_text: "Lorem Ipsum 2".to_string(), }; let edit_res: DbArticle = patch(data.hostname_beta, "article", &edit_form).await?; assert_eq!(edit_res.text, edit_form.new_text); assert_eq!(edit_res.edits.len(), 1); - assert!(edit_res.edits[0].id.to_string().starts_with(&edit_res.ap_id.to_string())); + assert!(edit_res.edits[0] + .id + .to_string() + .starts_with(&edit_res.ap_id.to_string())); // edit should be federated to alpha - let get_article = GetArticle { + let get_article = GetArticleData { title: edit_res.title.clone(), }; let get_res =