diff --git a/Cargo.lock b/Cargo.lock index 1ab1867..ccb2e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "activitypub_federation" -version = "0.7.0-beta.3" +version = "0.7.0-beta.4" dependencies = [ "activitystreams-kinds", "actix-web", diff --git a/docs/08_receiving_activities.md b/docs/08_receiving_activities.md index 7e0cd03..5e1b750 100644 --- a/docs/08_receiving_activities.md +++ b/docs/08_receiving_activities.md @@ -1,6 +1,6 @@ ## Sending and receiving activities -Activitypub propagates actions across servers using `Activities`. For this each actor has an inbox and a public/private key pair. We already defined a `Person` actor with keypair. Whats left is to define an activity. This is similar to the way we defined `Person` and `Note` structs before. In this case we need to implement the [ActivityHandler](trait@crate::traits::ActivityHandler) trait. +Activitypub propagates actions across servers using `Activities`. For this each actor has an inbox and a public/private key pair. We already defined a `Person` actor with keypair. Whats left is to define an activity. This is similar to the way we defined `Person` and `Note` structs before. In this case we need to implement the [Activity](trait@crate::traits::Activity) trait. ``` # use serde::{Deserialize, Serialize}; @@ -10,7 +10,7 @@ Activitypub propagates actions across servers using `Activities`. For this each # use activitypub_federation::fetch::object_id::ObjectId; # use activitypub_federation::traits::tests::{DbConnection, DbUser}; # use activitystreams_kinds::activity::FollowType; -# use activitypub_federation::traits::ActivityHandler; +# use activitypub_federation::traits::Activity; # use activitypub_federation::config::Data; # async fn send_accept() -> Result<(), Error> { Ok(()) } @@ -25,7 +25,7 @@ pub struct Follow { } #[async_trait] -impl ActivityHandler for Follow { +impl Activity for Follow { type DataType = DbConnection; type Error = Error; @@ -59,14 +59,14 @@ Next its time to setup the actual HTTP handler for the inbox. For this we first # use activitypub_federation::axum::inbox::{ActivityData, receive_activity}; # use activitypub_federation::config::Data; # use activitypub_federation::protocol::context::WithContext; -# use activitypub_federation::traits::ActivityHandler; +# use activitypub_federation::traits::Activity; # use activitypub_federation::traits::tests::{DbConnection, DbUser, Follow}; # use serde::{Deserialize, Serialize}; # use url::Url; #[derive(Deserialize, Serialize, Debug)] #[serde(untagged)] -#[enum_delegate::implement(ActivityHandler)] +#[enum_delegate::implement(Activity)] pub enum PersonAcceptedActivities { Follow(Follow), } diff --git a/docs/10_fetching_objects_with_unknown_type.md b/docs/10_fetching_objects_with_unknown_type.md index 96392d4..793ec70 100644 --- a/docs/10_fetching_objects_with_unknown_type.md +++ b/docs/10_fetching_objects_with_unknown_type.md @@ -32,6 +32,13 @@ impl Object for SearchableDbObjects { type Kind = SearchableObjects; type Error = anyhow::Error; + fn id(&self) -> &Url { + match self { + SearchableDbObjects::User(p) => &p.federation_id, + SearchableDbObjects::Post(n) => &n.federation_id, + } + } + async fn read_from_id( object_id: Url, data: &Data, diff --git a/examples/live_federation/activities/create_post.rs b/examples/live_federation/activities/create_post.rs index 51ee5e5..0e7d559 100644 --- a/examples/live_federation/activities/create_post.rs +++ b/examples/live_federation/activities/create_post.rs @@ -11,7 +11,7 @@ use activitypub_federation::{ fetch::object_id::ObjectId, kinds::activity::CreateType, protocol::{context::WithContext, helpers::deserialize_one_or_many}, - traits::{ActivityHandler, Object}, + traits::{Activity, Object}, }; use serde::{Deserialize, Serialize}; use url::Url; @@ -50,7 +50,7 @@ impl CreatePost { } #[async_trait::async_trait] -impl ActivityHandler for CreatePost { +impl Activity for CreatePost { type DataType = DatabaseHandle; type Error = crate::error::Error; diff --git a/examples/live_federation/objects/person.rs b/examples/live_federation/objects/person.rs index d9439ea..44d0844 100644 --- a/examples/live_federation/objects/person.rs +++ b/examples/live_federation/objects/person.rs @@ -5,7 +5,7 @@ use activitypub_federation::{ http_signatures::generate_actor_keypair, kinds::actor::PersonType, protocol::{public_key::PublicKey, verification::verify_domains_match}, - traits::{ActivityHandler, Actor, Object}, + traits::{Activity, Actor, Object}, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -29,7 +29,7 @@ pub struct DbUser { /// List of all activities which this actor can receive. #[derive(Deserialize, Serialize, Debug)] #[serde(untagged)] -#[enum_delegate::implement(ActivityHandler)] +#[enum_delegate::implement(Activity)] pub enum PersonAcceptedActivities { CreateNote(CreatePost), } @@ -69,6 +69,10 @@ impl Object for DbUser { type Kind = Person; type Error = Error; + fn id(&self) -> &Url { + self.ap_id.inner() + } + fn last_refreshed_at(&self) -> Option> { Some(self.last_refreshed_at) } @@ -122,10 +126,6 @@ impl Object for DbUser { } impl Actor for DbUser { - fn id(&self) -> Url { - self.ap_id.inner().clone() - } - fn public_key_pem(&self) -> &str { &self.public_key } diff --git a/examples/live_federation/objects/post.rs b/examples/live_federation/objects/post.rs index 1b19fac..1d8d7e6 100644 --- a/examples/live_federation/objects/post.rs +++ b/examples/live_federation/objects/post.rs @@ -50,6 +50,10 @@ impl Object for DbPost { type Kind = Note; type Error = Error; + fn id(&self) -> &Url { + self.ap_id.inner() + } + async fn read_from_id( _object_id: Url, _data: &Data, diff --git a/examples/local_federation/activities/accept.rs b/examples/local_federation/activities/accept.rs index c18945f..888c530 100644 --- a/examples/local_federation/activities/accept.rs +++ b/examples/local_federation/activities/accept.rs @@ -3,7 +3,7 @@ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, kinds::activity::AcceptType, - traits::ActivityHandler, + traits::Activity, }; use serde::{Deserialize, Serialize}; use url::Url; @@ -30,7 +30,7 @@ impl Accept { } #[async_trait::async_trait] -impl ActivityHandler for Accept { +impl Activity for Accept { type DataType = DatabaseHandle; type Error = crate::error::Error; diff --git a/examples/local_federation/activities/create_post.rs b/examples/local_federation/activities/create_post.rs index 6f85e2d..62ef005 100644 --- a/examples/local_federation/activities/create_post.rs +++ b/examples/local_federation/activities/create_post.rs @@ -8,7 +8,7 @@ use activitypub_federation::{ fetch::object_id::ObjectId, kinds::activity::CreateType, protocol::helpers::deserialize_one_or_many, - traits::{ActivityHandler, Object}, + traits::{Activity, Object}, }; use serde::{Deserialize, Serialize}; use url::Url; @@ -38,7 +38,7 @@ impl CreatePost { } #[async_trait::async_trait] -impl ActivityHandler for CreatePost { +impl Activity for CreatePost { type DataType = DatabaseHandle; type Error = crate::error::Error; diff --git a/examples/local_federation/activities/follow.rs b/examples/local_federation/activities/follow.rs index 02879d4..68bba5c 100644 --- a/examples/local_federation/activities/follow.rs +++ b/examples/local_federation/activities/follow.rs @@ -8,7 +8,7 @@ use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, kinds::activity::FollowType, - traits::{ActivityHandler, Actor}, + traits::{Activity, Actor}, }; use serde::{Deserialize, Serialize}; use url::Url; @@ -35,7 +35,7 @@ impl Follow { } #[async_trait::async_trait] -impl ActivityHandler for Follow { +impl Activity for Follow { type DataType = DatabaseHandle; type Error = crate::error::Error; diff --git a/examples/local_federation/actix_web/http.rs b/examples/local_federation/actix_web/http.rs index 6298014..678a6fc 100644 --- a/examples/local_federation/actix_web/http.rs +++ b/examples/local_federation/actix_web/http.rs @@ -8,7 +8,7 @@ use activitypub_federation::{ config::{Data, FederationConfig, FederationMiddleware}, fetch::webfinger::{build_webfinger_response, extract_webfinger_name}, protocol::context::WithContext, - traits::{Actor, Object}, + traits::Object, FEDERATION_CONTENT_TYPE, }; use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer}; diff --git a/examples/local_federation/objects/person.rs b/examples/local_federation/objects/person.rs index 0ae402f..7dd6165 100644 --- a/examples/local_federation/objects/person.rs +++ b/examples/local_federation/objects/person.rs @@ -13,7 +13,7 @@ use activitypub_federation::{ http_signatures::generate_actor_keypair, kinds::actor::PersonType, protocol::{context::WithContext, public_key::PublicKey, verification::verify_domains_match}, - traits::{ActivityHandler, Actor, Object}, + traits::{Activity, Actor, Object}, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -37,7 +37,7 @@ pub struct DbUser { /// List of all activities which this actor can receive. #[derive(Deserialize, Serialize, Debug)] #[serde(untagged)] -#[enum_delegate::implement(ActivityHandler)] +#[enum_delegate::implement(Activity)] pub enum PersonAcceptedActivities { Follow(Follow), Accept(Accept), @@ -103,16 +103,16 @@ impl DbUser { Ok(()) } - pub(crate) async fn send( + pub(crate) async fn send( &self, - activity: Activity, + activity: A, recipients: Vec, use_queue: bool, data: &Data, ) -> Result<(), Error> where - Activity: ActivityHandler + Serialize + Debug + Send + Sync, - ::Error: From + From, + A: Activity + Serialize + Debug + Send + Sync, + ::Error: From + From, { let activity = WithContext::new_default(activity); // Send through queue in some cases and bypass it in others to test both code paths @@ -134,6 +134,10 @@ impl Object for DbUser { type Kind = Person; type Error = Error; + fn id(&self) -> &Url { + self.ap_id.inner() + } + fn last_refreshed_at(&self) -> Option> { Some(self.last_refreshed_at) } @@ -187,10 +191,6 @@ impl Object for DbUser { } impl Actor for DbUser { - fn id(&self) -> Url { - self.ap_id.inner().clone() - } - fn public_key_pem(&self) -> &str { &self.public_key } diff --git a/examples/local_federation/objects/post.rs b/examples/local_federation/objects/post.rs index cbdf8e8..0de23fd 100644 --- a/examples/local_federation/objects/post.rs +++ b/examples/local_federation/objects/post.rs @@ -47,6 +47,10 @@ impl Object for DbPost { type Kind = Note; type Error = Error; + fn id(&self) -> &Url { + self.ap_id.inner() + } + async fn read_from_id( object_id: Url, data: &Data, diff --git a/src/activity_queue.rs b/src/activity_queue.rs index c666e7a..8f17d4f 100644 --- a/src/activity_queue.rs +++ b/src/activity_queue.rs @@ -6,7 +6,7 @@ use crate::{ activity_sending::{build_tasks, SendActivityTask}, config::Data, error::Error, - traits::{ActivityHandler, Actor}, + traits::{Activity, Actor}, }; use futures_core::Future; @@ -37,14 +37,14 @@ use url::Url; /// - `inboxes`: List of remote actor inboxes that should receive the activity. Ignores local actor /// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox] /// for each target actor. -pub async fn queue_activity( - activity: &Activity, +pub async fn queue_activity( + activity: &A, actor: &ActorType, inboxes: Vec, data: &Data, ) -> Result<(), Error> where - Activity: ActivityHandler + Serialize + Debug, + A: Activity + Serialize + Debug, Datatype: Clone, ActorType: Actor, { diff --git a/src/activity_sending.rs b/src/activity_sending.rs index 4971485..b734088 100644 --- a/src/activity_sending.rs +++ b/src/activity_sending.rs @@ -7,7 +7,7 @@ use crate::{ error::Error, http_signatures::sign_request, reqwest_shim::ResponseExt, - traits::{ActivityHandler, Actor}, + traits::{Activity, Actor}, FEDERATION_CONTENT_TYPE, }; use bytes::Bytes; @@ -54,14 +54,14 @@ impl SendActivityTask { /// - `inboxes`: List of remote actor inboxes that should receive the activity. Ignores local actor /// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox] /// for each target actor. - pub async fn prepare( - activity: &Activity, + pub async fn prepare( + activity: &A, actor: &ActorType, inboxes: Vec, data: &Data, ) -> Result, Error> where - Activity: ActivityHandler + Serialize + Debug, + A: Activity + Serialize + Debug, Datatype: Clone, ActorType: Actor, { @@ -136,14 +136,14 @@ impl SendActivityTask { } } -pub(crate) async fn build_tasks( - activity: &Activity, +pub(crate) async fn build_tasks( + activity: &A, actor: &ActorType, inboxes: Vec, data: &Data, ) -> Result, Error> where - Activity: ActivityHandler + Serialize + Debug, + A: Activity + Serialize + Debug, Datatype: Clone, ActorType: Actor, { @@ -190,7 +190,7 @@ where // PKey is internally like an Arc<>, so cloning is ok data.config .actor_pkey_cache - .try_get_with_by_ref(&actor_id, async { + .try_get_with_by_ref(actor_id, async { let private_key_pem = actor.private_key_pem().ok_or_else(|| { Error::Other(format!( "Actor {actor_id} does not contain a private key for signing" diff --git a/src/actix_web/inbox.rs b/src/actix_web/inbox.rs index b441055..d14983c 100644 --- a/src/actix_web/inbox.rs +++ b/src/actix_web/inbox.rs @@ -6,7 +6,7 @@ use crate::{ error::Error, http_signatures::{verify_body_hash, verify_signature}, parse_received_activity, - traits::{ActivityHandler, Actor, Object}, + traits::{Activity, Actor, Object}, }; use actix_web::{web::Bytes, HttpRequest, HttpResponse}; use serde::de::DeserializeOwned; @@ -14,77 +14,77 @@ use tracing::debug; /// Handles incoming activities, verifying HTTP signatures and other checks /// -/// After successful validation, activities are passed to respective [trait@ActivityHandler]. -pub async fn receive_activity( +/// After successful validation, activities are passed to respective [trait@Activity]. +pub async fn receive_activity( request: HttpRequest, body: Bytes, data: &Data, -) -> Result::Error> +) -> Result::Error> where - Activity: ActivityHandler + DeserializeOwned + Send + 'static, + A: Activity + DeserializeOwned + Send + 'static, ActorT: Object + Actor + Send + 'static, for<'de2> ::Kind: serde::Deserialize<'de2>, - ::Error: From + From<::Error>, + ::Error: From + From<::Error>, ::Error: From, Datatype: Clone, { - let (activity, _) = do_stuff::(request, body, data).await?; + let (activity, _) = do_stuff::(request, body, data).await?; do_more_stuff(activity, data).await } /// Workaround required so we can use references for the hook, instead of cloning data. -pub trait ReceiveActivityHook +pub trait ReceiveActivityHook where - Activity: ActivityHandler + DeserializeOwned + Send + Clone + 'static, + A: Activity + DeserializeOwned + Send + Clone + 'static, ActorT: Object + Actor + Send + Clone + 'static, for<'de2> ::Kind: serde::Deserialize<'de2>, - ::Error: From + From<::Error>, + ::Error: From + From<::Error>, ::Error: From, Datatype: Clone, { /// Called when a new activity is recived fn hook( self, - activity: &Activity, + activity: &A, actor: &ActorT, data: &Data, - ) -> impl std::future::Future::Error>>; + ) -> impl std::future::Future::Error>>; } /// Same as [receive_activity], only that it calls the provided hook function before /// calling activity verify and receive functions. -pub async fn receive_activity_with_hook( +pub async fn receive_activity_with_hook( request: HttpRequest, body: Bytes, - hook: impl ReceiveActivityHook, + hook: impl ReceiveActivityHook, data: &Data, -) -> Result::Error> +) -> Result::Error> where - Activity: ActivityHandler + DeserializeOwned + Send + Clone + 'static, + A: Activity + DeserializeOwned + Send + Clone + 'static, ActorT: Object + Actor + Send + Clone + 'static, for<'de2> ::Kind: serde::Deserialize<'de2>, - ::Error: From + From<::Error>, + ::Error: From + From<::Error>, ::Error: From, Datatype: Clone, { - let (activity, actor) = do_stuff::(request, body, data).await?; + let (activity, actor) = do_stuff::(request, body, data).await?; hook.hook(&activity, &actor, data).await?; do_more_stuff(activity, data).await } -async fn do_stuff( +async fn do_stuff( request: HttpRequest, body: Bytes, data: &Data, -) -> Result<(Activity, ActorT), ::Error> +) -> Result<(A, ActorT), ::Error> where - Activity: ActivityHandler + DeserializeOwned + Send + 'static, + A: Activity + DeserializeOwned + Send + 'static, ActorT: Object + Actor + Send + 'static, for<'de2> ::Kind: serde::Deserialize<'de2>, - ::Error: From + From<::Error>, + ::Error: From + From<::Error>, ::Error: From, Datatype: Clone, { @@ -94,7 +94,7 @@ where .map(http_compat::header_value); verify_body_hash(digest_header.as_ref(), &body)?; - let (activity, actor) = parse_received_activity::(&body, data).await?; + let (activity, actor) = parse_received_activity::(&body, data).await?; let headers = http_compat::header_map(request.headers()); let method = http_compat::method(request.method()); @@ -104,12 +104,12 @@ where Ok((activity, actor)) } -async fn do_more_stuff( - activity: Activity, +async fn do_more_stuff( + activity: A, data: &Data, -) -> Result::Error> +) -> Result::Error> where - Activity: ActivityHandler + DeserializeOwned + Send + 'static, + A: Activity + DeserializeOwned + Send + 'static, Datatype: Clone, { debug!("Receiving activity {}", activity.id().to_string()); @@ -160,21 +160,21 @@ mod test { struct Dummy; - impl ReceiveActivityHook for Dummy + impl ReceiveActivityHook for Dummy where - Activity: ActivityHandler + DeserializeOwned + Send + Clone + 'static, + A: Activity + DeserializeOwned + Send + Clone + 'static, ActorT: Object + Actor + Send + Clone + 'static, for<'de2> ::Kind: serde::Deserialize<'de2>, - ::Error: From + From<::Error>, + ::Error: From + From<::Error>, ::Error: From, Datatype: Clone, { async fn hook( self, - _activity: &Activity, + _activity: &A, _actor: &ActorT, _data: &Data, - ) -> Result<(), ::Error> { + ) -> Result<(), ::Error> { // ensure that hook gets called by returning this value Err(Error::Other("test-error".to_string()).into()) } diff --git a/src/actix_web/mod.rs b/src/actix_web/mod.rs index 7683d4b..6b7430e 100644 --- a/src/actix_web/mod.rs +++ b/src/actix_web/mod.rs @@ -4,6 +4,7 @@ mod http_compat; pub mod inbox; #[doc(hidden)] pub mod middleware; +pub mod response; use crate::{ config::Data, diff --git a/src/actix_web/response.rs b/src/actix_web/response.rs new file mode 100644 index 0000000..1968d9b --- /dev/null +++ b/src/actix_web/response.rs @@ -0,0 +1,50 @@ +//! Generate HTTP responses for Activitypub ojects + +use crate::{ + protocol::{context::WithContext, tombstone::Tombstone}, + FEDERATION_CONTENT_TYPE, +}; +use actix_web::HttpResponse; +use http02::header::VARY; +use serde::Serialize; +use serde_json::Value; +use url::Url; + +/// Generates HTTP response to serve the object for fetching from other instances. +/// +/// If possible use [Object.http_response] +/// which also handles redirects for remote objects and deletions. +/// +/// `federation_context` is the value of `@context`. +pub fn create_http_response( + data: T, + federation_context: &Value, +) -> Result { + let json = serde_json::to_string_pretty(&WithContext::new(data, federation_context.clone()))?; + + Ok(HttpResponse::Ok() + .content_type(FEDERATION_CONTENT_TYPE) + .insert_header((VARY, "Accept")) + .body(json)) +} + +pub(crate) fn create_tombstone_response( + id: Url, + federation_context: &Value, +) -> Result { + let tombstone = Tombstone::new(id); + let json = + serde_json::to_string_pretty(&WithContext::new(tombstone, federation_context.clone()))?; + + Ok(HttpResponse::Gone() + .content_type(FEDERATION_CONTENT_TYPE) + .status(actix_web::http::StatusCode::GONE) + .insert_header((VARY, "Accept")) + .body(json)) +} + +pub(crate) fn redirect_remote_object(url: &Url) -> HttpResponse { + let mut res = HttpResponse::PermanentRedirect(); + res.insert_header((actix_web::http::header::LOCATION, url.as_str())); + res.finish() +} diff --git a/src/axum/inbox.rs b/src/axum/inbox.rs index dfa981c..5b49791 100644 --- a/src/axum/inbox.rs +++ b/src/axum/inbox.rs @@ -7,7 +7,7 @@ use crate::{ error::Error, http_signatures::verify_signature, parse_received_activity, - traits::{ActivityHandler, Actor, Object}, + traits::{Activity, Actor, Object}, }; use axum::{ body::Body, @@ -20,20 +20,20 @@ use serde::de::DeserializeOwned; use tracing::debug; /// Handles incoming activities, verifying HTTP signatures and other checks -pub async fn receive_activity( +pub async fn receive_activity( activity_data: ActivityData, data: &Data, -) -> Result<(), ::Error> +) -> Result<(), ::Error> where - Activity: ActivityHandler + DeserializeOwned + Send + 'static, + A: Activity + DeserializeOwned + Send + 'static, ActorT: Object + Actor + Send + 'static, for<'de2> ::Kind: serde::Deserialize<'de2>, - ::Error: From + From<::Error>, + ::Error: From + From<::Error>, ::Error: From, Datatype: Clone, { let (activity, actor) = - parse_received_activity::(&activity_data.body, data).await?; + parse_received_activity::(&activity_data.body, data).await?; verify_signature( &activity_data.headers, diff --git a/src/config.rs b/src/config.rs index 038dc4f..9eb0b97 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,7 +19,7 @@ use crate::{ error::Error, http_signatures::sign_request, protocol::verification::verify_domains_match, - traits::{ActivityHandler, Actor}, + traits::{Activity, Actor}, }; use async_trait::async_trait; use bytes::Bytes; @@ -125,12 +125,9 @@ impl FederationConfig { FederationConfigBuilder::default() } - pub(crate) async fn verify_url_and_domain( - &self, - activity: &Activity, - ) -> Result<(), Error> + pub(crate) async fn verify_url_and_domain(&self, activity: &A) -> Result<(), Error> where - Activity: ActivityHandler + DeserializeOwned + Send + 'static, + A: Activity + DeserializeOwned + Send + 'static, { verify_domains_match(activity.id(), activity.actor())?; self.verify_url_valid(activity.id()).await?; @@ -258,7 +255,7 @@ impl FederationConfigBuilder { let private_key = RsaPrivateKey::from_pkcs8_pem(&private_key_pem).expect("Could not decode PEM data"); - self.signed_fetch_actor = Some(Some(Arc::new((actor.id(), private_key)))); + self.signed_fetch_actor = Some(Some(Arc::new((actor.id().clone(), private_key)))); self } diff --git a/src/lib.rs b/src/lib.rs index b7fe013..6dec179 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ use crate::{ config::Data, error::Error, fetch::object_id::ObjectId, - traits::{ActivityHandler, Actor, Object}, + traits::{Activity, Actor, Object}, }; pub use activitystreams_kinds as kinds; @@ -40,19 +40,19 @@ pub const FEDERATION_CONTENT_TYPE: &str = "application/activity+json"; /// Deserialize incoming inbox activity to the given type, perform basic /// validation and extract the actor. -async fn parse_received_activity( +async fn parse_received_activity( body: &[u8], data: &Data, -) -> Result<(Activity, ActorT), ::Error> +) -> Result<(A, ActorT), ::Error> where - Activity: ActivityHandler + DeserializeOwned + Send + 'static, + A: Activity + DeserializeOwned + Send + 'static, ActorT: Object + Actor + Send + 'static, for<'de2> ::Kind: serde::Deserialize<'de2>, - ::Error: From + From<::Error>, + ::Error: From + From<::Error>, ::Error: From, Datatype: Clone, { - let activity: Activity = serde_json::from_slice(body).map_err(|err| { + let activity: A = serde_json::from_slice(body).map_err(|err| { // Attempt to include activity id in error message let id = extract_id(body).ok(); Error::ParseReceivedActivity { err, id } diff --git a/src/protocol/context.rs b/src/protocol/context.rs index 027ff15..4934c10 100644 --- a/src/protocol/context.rs +++ b/src/protocol/context.rs @@ -19,7 +19,7 @@ //! Ok::<(), serde_json::error::Error>(()) //! ``` -use crate::{config::Data, traits::ActivityHandler}; +use crate::{config::Data, traits::Activity}; use serde::{Deserialize, Serialize}; use serde_json::Value; use url::Url; @@ -55,12 +55,12 @@ impl WithContext { } #[async_trait::async_trait] -impl ActivityHandler for WithContext +impl Activity for WithContext where - T: ActivityHandler + Send + Sync, + T: Activity + Send + Sync, { - type DataType = ::DataType; - type Error = ::Error; + type DataType = ::DataType; + type Error = ::Error; fn id(&self) -> &Url { self.inner.id() diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 1b5818a..674be2d 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -3,5 +3,6 @@ pub mod context; pub mod helpers; pub mod public_key; +pub mod tombstone; pub mod values; pub mod verification; diff --git a/src/protocol/tombstone.rs b/src/protocol/tombstone.rs new file mode 100644 index 0000000..157b50d --- /dev/null +++ b/src/protocol/tombstone.rs @@ -0,0 +1,27 @@ +//! Tombstone is used to serve deleted objects + +use crate::kinds::object::TombstoneType; +use serde::{Deserialize, Serialize}; +use url::Url; + +/// Represents a local object that was deleted +/// +/// +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Tombstone { + /// Id of the deleted object + pub id: Url, + #[serde(rename = "type")] + pub(crate) kind: TombstoneType, +} + +impl Tombstone { + /// Create a new tombstone for the given object id + pub fn new(id: Url) -> Tombstone { + Tombstone { + id, + kind: TombstoneType::Tombstone, + } + } +} diff --git a/src/traits/either.rs b/src/traits/either.rs index 95c2411..ba5a07c 100644 --- a/src/traits/either.rs +++ b/src/traits/either.rs @@ -29,6 +29,14 @@ where type Kind = UntaggedEither; type Error = E; + /// `id` field of the object + fn id(&self) -> &Url { + match self { + Either::Left(l) => l.id(), + Either::Right(r) => r.id(), + } + } + fn last_refreshed_at(&self) -> Option> { match self { Either::Left(l) => l.last_refreshed_at(), @@ -58,6 +66,13 @@ where } } + fn is_deleted(&self) -> bool { + match self { + Either::Left(l) => l.is_deleted(), + Either::Right(r) => r.is_deleted(), + } + } + async fn into_json(self, data: &Data) -> Result { Ok(match self { Either::Left(l) => UntaggedEither::Left(l.into_json(data).await?), @@ -95,13 +110,6 @@ where D: Sync + Send + Clone, E: From + Debug, { - fn id(&self) -> Url { - match self { - Either::Left(l) => l.id(), - Either::Right(r) => r.id(), - } - } - fn public_key_pem(&self) -> &str { match self { Either::Left(l) => l.public_key_pem(), diff --git a/src/traits/mod.rs b/src/traits/mod.rs index d99d812..030a3ee 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -9,6 +9,7 @@ use url::Url; /// `Either` implementations for traits pub mod either; +pub mod tests; /// Helper for converting between database structs and federated protocol structs. /// @@ -52,6 +53,8 @@ pub mod either; /// type Kind = Note; /// type Error = anyhow::Error; /// +/// fn id(&self) -> &Url { self.ap_id.inner() } +/// /// async fn read_from_id(object_id: Url, data: &Data) -> Result, Self::Error> { /// // Attempt to read object from local database. Return Ok(None) if not found. /// let post: Option = data.read_post_from_json_id(object_id).await?; @@ -106,6 +109,9 @@ pub trait Object: Sized + Debug { /// Error type returned by handler methods type Error; + /// `id` field of the object + fn id(&self) -> &Url; + /// Returns the last time this object was updated. /// /// If this returns `Some` and the value is too long ago, the object is refetched from the @@ -134,6 +140,11 @@ pub trait Object: Sized + Debug { Ok(()) } + /// Returns true if the object was deleted + fn is_deleted(&self) -> bool { + false + } + /// Convert database type to Activitypub type. /// /// Called when a local object gets fetched by another instance over HTTP, or when an object @@ -159,6 +170,40 @@ pub trait Object: Sized + Debug { /// should write the received object to database. Note that there is no distinction between /// create and update, so an `upsert` operation should be used. async fn from_json(json: Self::Kind, data: &Data) -> Result; + + /// Generates HTTP response to serve the object for fetching from other instances. + /// + /// - If the object has a remote domain, sends a redirect to the original instance. + /// - If [Object.is_deleted] returns true, returns a [crate::protocol::tombstone::Tombstone] instead. + /// - Otherwise serves the object JSON using [Object.into_json] and pretty-print + /// + /// `federation_context` is the value of `@context`. + #[cfg(feature = "actix-web")] + async fn http_response( + self, + federation_context: &serde_json::Value, + data: &Data, + ) -> Result + where + Self::Error: From, + Self::Kind: serde::Serialize + Send, + { + use crate::actix_web::response::{ + create_http_response, + create_tombstone_response, + redirect_remote_object, + }; + let id = self.id(); + let res = if !data.config.is_local_url(id) { + redirect_remote_object(id) + } else if !self.is_deleted() { + let json = self.into_json(data).await?; + create_http_response(json, federation_context)? + } else { + create_tombstone_response(id.clone(), federation_context)? + }; + Ok(res) + } } /// Handler for receiving incoming activities. @@ -168,7 +213,7 @@ pub trait Object: Sized + Debug { /// # use url::Url; /// # use activitypub_federation::fetch::object_id::ObjectId; /// # use activitypub_federation::config::Data; -/// # use activitypub_federation::traits::ActivityHandler; +/// # use activitypub_federation::traits::Activity; /// # use activitypub_federation::traits::tests::{DbConnection, DbUser}; /// #[derive(serde::Deserialize)] /// struct Follow { @@ -180,7 +225,7 @@ pub trait Object: Sized + Debug { /// } /// /// #[async_trait::async_trait] -/// impl ActivityHandler for Follow { +/// impl Activity for Follow { /// type DataType = DbConnection; /// type Error = anyhow::Error; /// @@ -206,7 +251,7 @@ pub trait Object: Sized + Debug { /// ``` #[async_trait] #[enum_delegate::register] -pub trait ActivityHandler { +pub trait Activity { /// App data type passed to handlers. Must be identical to /// [crate::config::FederationConfigBuilder::app_data] type. type DataType: Clone + Send + Sync; @@ -234,9 +279,6 @@ pub trait ActivityHandler { /// Trait to allow retrieving common Actor data. pub trait Actor: Object + Send + 'static { - /// `id` field of the actor - fn id(&self) -> Url; - /// The actor's public key for verifying signatures of incoming activities. /// /// Use [generate_actor_keypair](crate::http_signatures::generate_actor_keypair) to create the @@ -254,7 +296,7 @@ pub trait Actor: Object + Send + 'static { /// Generates a public key struct for use in the actor json representation fn public_key(&self) -> PublicKey { - PublicKey::new(self.id(), self.public_key_pem().to_string()) + PublicKey::new(self.id().clone(), self.public_key_pem().to_string()) } /// The actor's shared inbox, if any @@ -270,9 +312,9 @@ pub trait Actor: Object + Send + 'static { /// Allow for boxing of enum variants #[async_trait] -impl ActivityHandler for Box +impl Activity for Box where - T: ActivityHandler + Send + Sync, + T: Activity + Send + Sync, { type DataType = T::DataType; type Error = T::Error; @@ -334,208 +376,3 @@ pub trait Collection: Sized { data: &Data, ) -> Result; } - -/// Some impls of these traits for use in tests. Dont use this from external crates. -/// -/// TODO: Should be using `cfg[doctest]` but blocked by -#[doc(hidden)] -#[allow(clippy::unwrap_used)] -pub mod tests { - use super::{async_trait, ActivityHandler, Actor, Data, Debug, Object, PublicKey, Url}; - use crate::{ - error::Error, - fetch::object_id::ObjectId, - http_signatures::{generate_actor_keypair, Keypair}, - protocol::verification::verify_domains_match, - }; - use activitystreams_kinds::{activity::FollowType, actor::PersonType}; - use serde::{Deserialize, Serialize}; - use std::sync::LazyLock; - - #[derive(Clone)] - pub struct DbConnection; - - impl DbConnection { - pub async fn read_post_from_json_id(&self, _: Url) -> Result, Error> { - Ok(None) - } - pub async fn read_local_user(&self, _: &str) -> Result { - todo!() - } - pub async fn upsert(&self, _: &T) -> Result<(), Error> { - Ok(()) - } - pub async fn add_follower(&self, _: DbUser, _: DbUser) -> Result<(), Error> { - Ok(()) - } - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - #[serde(rename_all = "camelCase")] - pub struct Person { - #[serde(rename = "type")] - pub kind: PersonType, - pub preferred_username: String, - pub id: ObjectId, - pub inbox: Url, - pub public_key: PublicKey, - } - #[derive(Debug, Clone)] - pub struct DbUser { - pub name: String, - pub federation_id: Url, - pub inbox: Url, - pub public_key: String, - #[allow(dead_code)] - private_key: Option, - pub followers: Vec, - pub local: bool, - } - - pub static DB_USER_KEYPAIR: LazyLock = - LazyLock::new(|| generate_actor_keypair().unwrap()); - - pub static DB_USER: LazyLock = LazyLock::new(|| DbUser { - name: String::new(), - federation_id: "https://localhost/123".parse().unwrap(), - inbox: "https://localhost/123/inbox".parse().unwrap(), - public_key: DB_USER_KEYPAIR.public_key.clone(), - private_key: Some(DB_USER_KEYPAIR.private_key.clone()), - followers: vec![], - local: false, - }); - - #[async_trait] - impl Object for DbUser { - type DataType = DbConnection; - type Kind = Person; - type Error = Error; - - async fn read_from_id( - _object_id: Url, - _data: &Data, - ) -> Result, Self::Error> { - Ok(Some(DB_USER.clone())) - } - - async fn into_json(self, _data: &Data) -> Result { - Ok(Person { - preferred_username: self.name.clone(), - kind: Default::default(), - id: self.federation_id.clone().into(), - inbox: self.inbox.clone(), - public_key: self.public_key(), - }) - } - - async fn verify( - json: &Self::Kind, - expected_domain: &Url, - _data: &Data, - ) -> Result<(), Self::Error> { - verify_domains_match(json.id.inner(), expected_domain)?; - Ok(()) - } - - async fn from_json( - json: Self::Kind, - _data: &Data, - ) -> Result { - Ok(DbUser { - name: json.preferred_username, - federation_id: json.id.into(), - inbox: json.inbox, - public_key: json.public_key.public_key_pem, - private_key: None, - followers: vec![], - local: false, - }) - } - } - - impl Actor for DbUser { - fn id(&self) -> Url { - self.federation_id.clone() - } - - fn public_key_pem(&self) -> &str { - &self.public_key - } - - fn private_key_pem(&self) -> Option { - self.private_key.clone() - } - - fn inbox(&self) -> Url { - self.inbox.clone() - } - } - - #[derive(Deserialize, Serialize, Clone, Debug)] - #[serde(rename_all = "camelCase")] - pub struct Follow { - pub actor: ObjectId, - pub object: ObjectId, - #[serde(rename = "type")] - pub kind: FollowType, - pub id: Url, - } - - #[async_trait] - impl ActivityHandler for Follow { - type DataType = DbConnection; - type Error = Error; - - fn id(&self) -> &Url { - &self.id - } - - fn actor(&self) -> &Url { - self.actor.inner() - } - - async fn verify(&self, _: &Data) -> Result<(), Self::Error> { - Ok(()) - } - - async fn receive(self, _data: &Data) -> Result<(), Self::Error> { - Ok(()) - } - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - #[serde(rename_all = "camelCase")] - pub struct Note {} - #[derive(Debug, Clone)] - pub struct DbPost {} - - #[async_trait] - impl Object for DbPost { - type DataType = DbConnection; - type Kind = Note; - type Error = Error; - - async fn read_from_id( - _: Url, - _: &Data, - ) -> Result, Self::Error> { - todo!() - } - - async fn into_json(self, _: &Data) -> Result { - todo!() - } - - async fn verify( - _: &Self::Kind, - _: &Url, - _: &Data, - ) -> Result<(), Self::Error> { - todo!() - } - - async fn from_json(_: Self::Kind, _: &Data) -> Result { - todo!() - } - } -} diff --git a/src/traits/tests.rs b/src/traits/tests.rs new file mode 100644 index 0000000..ae2ee84 --- /dev/null +++ b/src/traits/tests.rs @@ -0,0 +1,201 @@ +#![doc(hidden)] +#![allow(clippy::unwrap_used)] +//! Some impls of these traits for use in tests. Dont use this from external crates. +//! +//! TODO: Should be using `cfg[doctest]` but blocked by + +use super::{async_trait, Activity, Actor, Data, Debug, Object, PublicKey, Url}; +use crate::{ + error::Error, + fetch::object_id::ObjectId, + http_signatures::{generate_actor_keypair, Keypair}, + protocol::verification::verify_domains_match, +}; +use activitystreams_kinds::{activity::FollowType, actor::PersonType}; +use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; + +#[derive(Clone)] +pub struct DbConnection; + +impl DbConnection { + pub async fn read_post_from_json_id(&self, _: Url) -> Result, Error> { + Ok(None) + } + pub async fn read_local_user(&self, _: &str) -> Result { + todo!() + } + pub async fn upsert(&self, _: &T) -> Result<(), Error> { + Ok(()) + } + pub async fn add_follower(&self, _: DbUser, _: DbUser) -> Result<(), Error> { + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Person { + #[serde(rename = "type")] + pub kind: PersonType, + pub preferred_username: String, + pub id: ObjectId, + pub inbox: Url, + pub public_key: PublicKey, +} +#[derive(Debug, Clone)] +pub struct DbUser { + pub name: String, + pub federation_id: Url, + pub inbox: Url, + pub public_key: String, + #[allow(dead_code)] + private_key: Option, + pub followers: Vec, + pub local: bool, +} + +pub static DB_USER_KEYPAIR: LazyLock = LazyLock::new(|| generate_actor_keypair().unwrap()); + +pub static DB_USER: LazyLock = LazyLock::new(|| DbUser { + name: String::new(), + federation_id: "https://localhost/123".parse().unwrap(), + inbox: "https://localhost/123/inbox".parse().unwrap(), + public_key: DB_USER_KEYPAIR.public_key.clone(), + private_key: Some(DB_USER_KEYPAIR.private_key.clone()), + followers: vec![], + local: false, +}); + +#[async_trait] +impl Object for DbUser { + type DataType = DbConnection; + type Kind = Person; + type Error = Error; + + fn id(&self) -> &Url { + &self.federation_id + } + + async fn read_from_id( + _object_id: Url, + _data: &Data, + ) -> Result, Self::Error> { + Ok(Some(DB_USER.clone())) + } + + async fn into_json(self, _data: &Data) -> Result { + Ok(Person { + preferred_username: self.name.clone(), + kind: Default::default(), + id: self.federation_id.clone().into(), + inbox: self.inbox.clone(), + public_key: self.public_key(), + }) + } + + async fn verify( + json: &Self::Kind, + expected_domain: &Url, + _data: &Data, + ) -> Result<(), Self::Error> { + verify_domains_match(json.id.inner(), expected_domain)?; + Ok(()) + } + + async fn from_json( + json: Self::Kind, + _data: &Data, + ) -> Result { + Ok(DbUser { + name: json.preferred_username, + federation_id: json.id.into(), + inbox: json.inbox, + public_key: json.public_key.public_key_pem, + private_key: None, + followers: vec![], + local: false, + }) + } +} + +impl Actor for DbUser { + fn public_key_pem(&self) -> &str { + &self.public_key + } + + fn private_key_pem(&self) -> Option { + self.private_key.clone() + } + + fn inbox(&self) -> Url { + self.inbox.clone() + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Follow { + pub actor: ObjectId, + pub object: ObjectId, + #[serde(rename = "type")] + pub kind: FollowType, + pub id: Url, +} + +#[async_trait] +impl Activity for Follow { + type DataType = DbConnection; + type Error = Error; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + async fn verify(&self, _: &Data) -> Result<(), Self::Error> { + Ok(()) + } + + async fn receive(self, _data: &Data) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Note {} +#[derive(Debug, Clone)] +pub struct DbPost { + pub federation_id: Url, +} + +#[async_trait] +impl Object for DbPost { + type DataType = DbConnection; + type Kind = Note; + type Error = Error; + + fn id(&self) -> &Url { + todo!() + } + + async fn read_from_id(_: Url, _: &Data) -> Result, Self::Error> { + todo!() + } + + async fn into_json(self, _: &Data) -> Result { + todo!() + } + + async fn verify(_: &Self::Kind, _: &Url, _: &Data) -> Result<(), Self::Error> { + todo!() + } + + async fn from_json(_: Self::Kind, _: &Data) -> Result { + todo!() + } +}