From 6a65fa7c98b66feb341775d124fa110bce947e95 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 16 Mar 2023 02:11:48 +0100 Subject: [PATCH] Changes to make Lemmy work with 0.4 (#29) * Make it work with Lemmy * working but needs cleanup * almost everything working * debug * stack overflow fix --- Cargo.lock | 2 +- docs/06_http_endpoints_axum.md | 10 +- docs/07_fetching_data.md | 2 +- docs/08_receiving_activities.md | 10 +- docs/09_sending_activities.md | 7 +- docs/10_fetching_objects_with_unknown_type.md | 12 +- .../live_federation/activities/create_post.rs | 18 +- examples/live_federation/http.rs | 8 +- examples/live_federation/objects/person.rs | 30 +-- examples/live_federation/objects/post.rs | 13 +- .../local_federation/activities/accept.rs | 6 +- .../activities/create_post.rs | 6 +- .../local_federation/activities/follow.rs | 6 +- examples/local_federation/actix_web/http.rs | 8 +- examples/local_federation/axum/http.rs | 8 +- examples/local_federation/objects/person.rs | 47 ++-- examples/local_federation/objects/post.rs | 13 +- src/activity_queue.rs | 14 +- src/actix_web/inbox.rs | 8 +- src/actix_web/middleware.rs | 6 +- src/axum/inbox.rs | 4 +- src/axum/json.rs | 4 +- src/axum/middleware.rs | 6 +- src/config.rs | 47 ++-- src/fetch/collection_id.rs | 97 ++++++++ src/fetch/mod.rs | 8 +- src/fetch/object_id.rs | 28 ++- src/fetch/webfinger.rs | 10 +- src/protocol/context.rs | 22 +- src/protocol/public_key.rs | 2 +- src/traits.rs | 234 +++++++++++------- 31 files changed, 427 insertions(+), 269 deletions(-) create mode 100644 src/fetch/collection_id.rs diff --git a/Cargo.lock b/Cargo.lock index c373b81..8a6fffb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "activitypub_federation" -version = "0.4.0-rc1" +version = "0.4.0-rc2" dependencies = [ "activitystreams-kinds", "actix-rt", diff --git a/docs/06_http_endpoints_axum.md b/docs/06_http_endpoints_axum.md index 50d18eb..f96b643 100644 --- a/docs/06_http_endpoints_axum.md +++ b/docs/06_http_endpoints_axum.md @@ -9,7 +9,7 @@ The next step is to allow other servers to fetch our actors and objects. For thi # use activitypub_federation::axum::json::ApubJson; # use anyhow::Error; # use activitypub_federation::traits::tests::Person; -# use activitypub_federation::config::RequestData; +# use activitypub_federation::config::Data; # use activitypub_federation::traits::tests::DbConnection; # use axum::extract::Path; # use activitypub_federation::config::ApubMiddleware; @@ -20,7 +20,7 @@ The next step is to allow other servers to fetch our actors and objects. For thi # use axum::TypedHeader; # use axum::response::IntoResponse; # use http::HeaderMap; -# async fn generate_user_html(_: String, _: RequestData) -> axum::response::Response { todo!() } +# async fn generate_user_html(_: String, _: Data) -> axum::response::Response { todo!() } #[actix_rt::main] async fn main() -> Result<(), Error> { @@ -44,7 +44,7 @@ async fn main() -> Result<(), Error> { async fn http_get_user( header_map: HeaderMap, Path(name): Path, - data: RequestData, + data: Data, ) -> impl IntoResponse { let accept = header_map.get("accept").map(|v| v.to_str().unwrap()); if accept == Some(APUB_JSON_CONTENT_TYPE) { @@ -71,7 +71,7 @@ To do this we can implement the following HTTP handler which must be bound to pa ```rust # use serde::Deserialize; # use axum::{extract::Query, Json}; -# use activitypub_federation::config::RequestData; +# use activitypub_federation::config::Data; # use activitypub_federation::fetch::webfinger::Webfinger; # use anyhow::Error; # use activitypub_federation::traits::tests::DbConnection; @@ -85,7 +85,7 @@ struct WebfingerQuery { async fn webfinger( Query(query): Query, - data: RequestData, + data: Data, ) -> Result, Error> { let name = extract_webfinger_name(&query.resource, &data)?; let db_user = data.read_local_user(name).await?; diff --git a/docs/07_fetching_data.md b/docs/07_fetching_data.md index 355b9c0..cf877dd 100644 --- a/docs/07_fetching_data.md +++ b/docs/07_fetching_data.md @@ -13,7 +13,7 @@ let config = FederationConfig::builder() .domain("example.com") .app_data(db_connection) .build()?; -let user_id = ObjectId::::new("https://mastodon.social/@LemmyDev")?; +let user_id = ObjectId::::parse("https://mastodon.social/@LemmyDev")?; let data = config.to_request_data(); let user = user_id.dereference(&data).await; assert!(user.is_ok()); diff --git a/docs/08_receiving_activities.md b/docs/08_receiving_activities.md index 47e5482..7e0cd03 100644 --- a/docs/08_receiving_activities.md +++ b/docs/08_receiving_activities.md @@ -11,7 +11,7 @@ Activitypub propagates actions across servers using `Activities`. For this each # use activitypub_federation::traits::tests::{DbConnection, DbUser}; # use activitystreams_kinds::activity::FollowType; # use activitypub_federation::traits::ActivityHandler; -# use activitypub_federation::config::RequestData; +# use activitypub_federation::config::Data; # async fn send_accept() -> Result<(), Error> { Ok(()) } #[derive(Deserialize, Serialize, Clone, Debug)] @@ -37,11 +37,11 @@ impl ActivityHandler for Follow { self.actor.inner() } - async fn verify(&self, _data: &RequestData) -> Result<(), Self::Error> { + async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { Ok(()) } - async fn receive(self, data: &RequestData) -> Result<(), Self::Error> { + async fn receive(self, data: &Data) -> Result<(), Self::Error> { let actor = self.actor.dereference(data).await?; let followed = self.object.dereference(data).await?; data.add_follower(followed, actor).await?; @@ -57,7 +57,7 @@ Next its time to setup the actual HTTP handler for the inbox. For this we first ``` # use axum::response::IntoResponse; # use activitypub_federation::axum::inbox::{ActivityData, receive_activity}; -# use activitypub_federation::config::RequestData; +# use activitypub_federation::config::Data; # use activitypub_federation::protocol::context::WithContext; # use activitypub_federation::traits::ActivityHandler; # use activitypub_federation::traits::tests::{DbConnection, DbUser, Follow}; @@ -72,7 +72,7 @@ pub enum PersonAcceptedActivities { } async fn http_post_user_inbox( - data: RequestData, + data: Data, activity_data: ActivityData, ) -> impl IntoResponse { receive_activity::, DbUser, DbConnection>( diff --git a/docs/09_sending_activities.md b/docs/09_sending_activities.md index 9ceaf5b..e971d05 100644 --- a/docs/09_sending_activities.md +++ b/docs/09_sending_activities.md @@ -17,17 +17,16 @@ To send an activity we need to initialize our previously defined struct, and pic # .app_data(db_connection) # .build()?; # let data = config.to_request_data(); +# let sender = DB_USER.clone(); # let recipient = DB_USER.clone(); -// Each actor has a keypair. Generate it on signup and store it in the database. -let keypair = generate_actor_keypair()?; let activity = Follow { - actor: ObjectId::new("https://lemmy.ml/u/nutomic")?, + actor: ObjectId::parse("https://lemmy.ml/u/nutomic")?, object: recipient.apub_id.clone().into(), kind: Default::default(), id: "https://lemmy.ml/activities/321".try_into()? }; let inboxes = vec![recipient.shared_inbox_or_inbox()]; -send_activity(activity, keypair.private_key, inboxes, &data).await?; +send_activity(activity, &sender, inboxes, &data).await?; # Ok::<(), anyhow::Error>(()) # }).unwrap() ``` diff --git a/docs/10_fetching_objects_with_unknown_type.md b/docs/10_fetching_objects_with_unknown_type.md index e5bf4d0..fba165e 100644 --- a/docs/10_fetching_objects_with_unknown_type.md +++ b/docs/10_fetching_objects_with_unknown_type.md @@ -9,7 +9,7 @@ It is sometimes necessary to fetch from a URL, but we don't know the exact type # use activitypub_federation::config::FederationConfig; # use serde::{Deserialize, Serialize}; # use activitypub_federation::traits::tests::DbConnection; -# use activitypub_federation::config::RequestData; +# use activitypub_federation::config::Data; # use url::Url; # use activitypub_federation::traits::tests::{Person, Note}; @@ -33,25 +33,25 @@ impl ApubObject for SearchableDbObjects { async fn read_from_apub_id( object_id: Url, - data: &RequestData, + data: &Data, ) -> Result, Self::Error> { Ok(None) } async fn into_apub( self, - data: &RequestData, + data: &Data, ) -> Result { unimplemented!(); } - async fn verify(apub: &Self::ApubType, expected_domain: &Url, _data: &RequestData) -> Result<(), Self::Error> { + async fn verify(apub: &Self::ApubType, expected_domain: &Url, _data: &Data) -> Result<(), Self::Error> { Ok(()) } async fn from_apub( apub: Self::ApubType, - data: &RequestData, + data: &Data, ) -> Result { use SearchableDbObjects::*; match apub { @@ -66,7 +66,7 @@ async fn main() -> Result<(), anyhow::Error> { # let config = FederationConfig::builder().domain("example.com").app_data(DbConnection).build().unwrap(); # let data = config.to_request_data(); let query = "https://example.com/id/413"; - let query_result = ObjectId::::new(query)? + let query_result = ObjectId::::parse(query)? .dereference(&data) .await?; match query_result { diff --git a/examples/live_federation/activities/create_post.rs b/examples/live_federation/activities/create_post.rs index 7bac286..965df33 100644 --- a/examples/live_federation/activities/create_post.rs +++ b/examples/live_federation/activities/create_post.rs @@ -7,7 +7,7 @@ use crate::{ }; use activitypub_federation::{ activity_queue::send_activity, - config::RequestData, + config::Data, fetch::object_id::ObjectId, kinds::activity::CreateType, protocol::{context::WithContext, helpers::deserialize_one_or_many}, @@ -29,11 +29,7 @@ pub struct CreatePost { } impl CreatePost { - pub async fn send( - note: Note, - inbox: Url, - data: &RequestData, - ) -> Result<(), Error> { + pub async fn send(note: Note, inbox: Url, data: &Data) -> Result<(), Error> { print!("Sending reply to {}", ¬e.attributed_to); let create = CreatePost { actor: note.attributed_to.clone(), @@ -43,11 +39,7 @@ impl CreatePost { id: generate_object_id(data.domain())?, }; let create_with_context = WithContext::new_default(create); - let private_key = data - .local_user() - .private_key - .expect("local user always has private key"); - send_activity(create_with_context, private_key, vec![inbox], data).await?; + send_activity(create_with_context, &data.local_user(), vec![inbox], data).await?; Ok(()) } } @@ -65,12 +57,12 @@ impl ActivityHandler for CreatePost { self.actor.inner() } - async fn verify(&self, data: &RequestData) -> Result<(), Self::Error> { + async fn verify(&self, data: &Data) -> Result<(), Self::Error> { DbPost::verify(&self.object, &self.id, data).await?; Ok(()) } - async fn receive(self, data: &RequestData) -> Result<(), Self::Error> { + async fn receive(self, data: &Data) -> Result<(), Self::Error> { DbPost::from_apub(self.object, data).await?; Ok(()) } diff --git a/examples/live_federation/http.rs b/examples/live_federation/http.rs index ba1ea68..bb96622 100644 --- a/examples/live_federation/http.rs +++ b/examples/live_federation/http.rs @@ -8,7 +8,7 @@ use activitypub_federation::{ inbox::{receive_activity, ActivityData}, json::ApubJson, }, - config::RequestData, + config::Data, fetch::webfinger::{build_webfinger_response, extract_webfinger_name, Webfinger}, protocol::context::WithContext, traits::ApubObject, @@ -31,7 +31,7 @@ impl IntoResponse for Error { #[debug_handler] pub async fn http_get_user( Path(name): Path, - data: RequestData, + data: Data, ) -> Result>, Error> { let db_user = data.read_user(&name)?; let apub_user = db_user.into_apub(&data).await?; @@ -40,7 +40,7 @@ pub async fn http_get_user( #[debug_handler] pub async fn http_post_user_inbox( - data: RequestData, + data: Data, activity_data: ActivityData, ) -> impl IntoResponse { receive_activity::, DbUser, DatabaseHandle>( @@ -58,7 +58,7 @@ pub struct WebfingerQuery { #[debug_handler] pub async fn webfinger( Query(query): Query, - data: RequestData, + data: Data, ) -> Result, Error> { let name = extract_webfinger_name(&query.resource, &data)?; let db_user = data.read_user(&name)?; diff --git a/examples/live_federation/objects/person.rs b/examples/live_federation/objects/person.rs index 786ce5d..0323d09 100644 --- a/examples/live_federation/objects/person.rs +++ b/examples/live_federation/objects/person.rs @@ -1,6 +1,6 @@ use crate::{activities::create_post::CreatePost, database::DatabaseHandle, error::Error}; use activitypub_federation::{ - config::RequestData, + config::Data, fetch::object_id::ObjectId, http_signatures::generate_actor_keypair, kinds::actor::PersonType, @@ -75,7 +75,7 @@ impl ApubObject for DbUser { async fn read_from_apub_id( object_id: Url, - data: &RequestData, + data: &Data, ) -> Result, Self::Error> { let users = data.users.lock().unwrap(); let res = users @@ -85,24 +85,20 @@ impl ApubObject for DbUser { Ok(res) } - async fn into_apub( - self, - _data: &RequestData, - ) -> Result { - let public_key = PublicKey::new(self.ap_id.clone().into_inner(), self.public_key.clone()); + async fn into_apub(self, _data: &Data) -> Result { Ok(Person { preferred_username: self.name.clone(), kind: Default::default(), id: self.ap_id.clone(), - inbox: self.inbox, - public_key, + inbox: self.inbox.clone(), + public_key: self.public_key(), }) } async fn verify( apub: &Self::ApubType, expected_domain: &Url, - _data: &RequestData, + _data: &Data, ) -> Result<(), Self::Error> { verify_domains_match(apub.id.inner(), expected_domain)?; Ok(()) @@ -110,7 +106,7 @@ impl ApubObject for DbUser { async fn from_apub( apub: Self::ApubType, - _data: &RequestData, + _data: &Data, ) -> Result { Ok(DbUser { name: apub.preferred_username, @@ -126,15 +122,19 @@ impl ApubObject for DbUser { } impl Actor for DbUser { + fn id(&self) -> Url { + self.ap_id.inner().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() } - - fn id(&self) -> &Url { - self.ap_id.inner() - } } diff --git a/examples/live_federation/objects/post.rs b/examples/live_federation/objects/post.rs index 825207d..2a84031 100644 --- a/examples/live_federation/objects/post.rs +++ b/examples/live_federation/objects/post.rs @@ -6,7 +6,7 @@ use crate::{ objects::person::DbUser, }; use activitypub_federation::{ - config::RequestData, + config::Data, fetch::object_id::ObjectId, kinds::{object::NoteType, public}, protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match}, @@ -53,22 +53,19 @@ impl ApubObject for DbPost { async fn read_from_apub_id( _object_id: Url, - _data: &RequestData, + _data: &Data, ) -> Result, Self::Error> { Ok(None) } - async fn into_apub( - self, - _data: &RequestData, - ) -> Result { + async fn into_apub(self, _data: &Data) -> Result { unimplemented!() } async fn verify( apub: &Self::ApubType, expected_domain: &Url, - _data: &RequestData, + _data: &Data, ) -> Result<(), Self::Error> { verify_domains_match(apub.id.inner(), expected_domain)?; Ok(()) @@ -76,7 +73,7 @@ impl ApubObject for DbPost { async fn from_apub( apub: Self::ApubType, - data: &RequestData, + data: &Data, ) -> Result { println!( "Received post with content {} and id {}", diff --git a/examples/local_federation/activities/accept.rs b/examples/local_federation/activities/accept.rs index 3e2add0..c18945f 100644 --- a/examples/local_federation/activities/accept.rs +++ b/examples/local_federation/activities/accept.rs @@ -1,6 +1,6 @@ use crate::{activities::follow::Follow, instance::DatabaseHandle, objects::person::DbUser}; use activitypub_federation::{ - config::RequestData, + config::Data, fetch::object_id::ObjectId, kinds::activity::AcceptType, traits::ActivityHandler, @@ -42,11 +42,11 @@ impl ActivityHandler for Accept { self.actor.inner() } - async fn verify(&self, _data: &RequestData) -> Result<(), Self::Error> { + async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { Ok(()) } - async fn receive(self, _data: &RequestData) -> Result<(), Self::Error> { + async fn receive(self, _data: &Data) -> Result<(), Self::Error> { Ok(()) } } diff --git a/examples/local_federation/activities/create_post.rs b/examples/local_federation/activities/create_post.rs index 5ca0a7f..661452b 100644 --- a/examples/local_federation/activities/create_post.rs +++ b/examples/local_federation/activities/create_post.rs @@ -4,7 +4,7 @@ use crate::{ DbPost, }; use activitypub_federation::{ - config::RequestData, + config::Data, fetch::object_id::ObjectId, kinds::activity::CreateType, protocol::helpers::deserialize_one_or_many, @@ -50,12 +50,12 @@ impl ActivityHandler for CreatePost { self.actor.inner() } - async fn verify(&self, data: &RequestData) -> Result<(), Self::Error> { + async fn verify(&self, data: &Data) -> Result<(), Self::Error> { DbPost::verify(&self.object, &self.id, data).await?; Ok(()) } - async fn receive(self, data: &RequestData) -> Result<(), Self::Error> { + async fn receive(self, data: &Data) -> Result<(), Self::Error> { DbPost::from_apub(self.object, data).await?; Ok(()) } diff --git a/examples/local_federation/activities/follow.rs b/examples/local_federation/activities/follow.rs index b5cd435..865a618 100644 --- a/examples/local_federation/activities/follow.rs +++ b/examples/local_federation/activities/follow.rs @@ -5,7 +5,7 @@ use crate::{ objects::person::DbUser, }; use activitypub_federation::{ - config::RequestData, + config::Data, fetch::object_id::ObjectId, kinds::activity::FollowType, traits::{ActivityHandler, Actor}, @@ -47,13 +47,13 @@ impl ActivityHandler for Follow { self.actor.inner() } - async fn verify(&self, _data: &RequestData) -> Result<(), Self::Error> { + async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { Ok(()) } // Ignore clippy false positive: https://github.com/rust-lang/rust-clippy/issues/6446 #[allow(clippy::await_holding_lock)] - async fn receive(self, data: &RequestData) -> Result<(), Self::Error> { + async fn receive(self, data: &Data) -> Result<(), Self::Error> { // add to followers let local_user = { let mut users = data.users.lock().unwrap(); diff --git a/examples/local_federation/actix_web/http.rs b/examples/local_federation/actix_web/http.rs index 0fd5037..238f85b 100644 --- a/examples/local_federation/actix_web/http.rs +++ b/examples/local_federation/actix_web/http.rs @@ -5,7 +5,7 @@ use crate::{ }; use activitypub_federation::{ actix_web::inbox::receive_activity, - config::{ApubMiddleware, FederationConfig, RequestData}, + config::{ApubMiddleware, Data, FederationConfig}, fetch::webfinger::{build_webfinger_response, extract_webfinger_name}, protocol::context::WithContext, traits::ApubObject, @@ -36,7 +36,7 @@ pub fn listen(config: &FederationConfig) -> Result<(), Error> { /// Handles requests to fetch user json over HTTP pub async fn http_get_user( user_name: web::Path, - data: RequestData, + data: Data, ) -> Result { let db_user = data.local_user(); if user_name.into_inner() == db_user.name { @@ -53,7 +53,7 @@ pub async fn http_get_user( pub async fn http_post_user_inbox( request: HttpRequest, body: Bytes, - data: RequestData, + data: Data, ) -> Result { receive_activity::, DbUser, DatabaseHandle>( request, body, &data, @@ -68,7 +68,7 @@ pub struct WebfingerQuery { pub async fn webfinger( query: web::Query, - data: RequestData, + data: Data, ) -> Result { let name = extract_webfinger_name(&query.resource, &data)?; let db_user = data.read_user(&name)?; diff --git a/examples/local_federation/axum/http.rs b/examples/local_federation/axum/http.rs index dfa8e7e..a14b86b 100644 --- a/examples/local_federation/axum/http.rs +++ b/examples/local_federation/axum/http.rs @@ -8,7 +8,7 @@ use activitypub_federation::{ inbox::{receive_activity, ActivityData}, json::ApubJson, }, - config::{ApubMiddleware, FederationConfig, RequestData}, + config::{ApubMiddleware, Data, FederationConfig}, fetch::webfinger::{build_webfinger_response, extract_webfinger_name, Webfinger}, protocol::context::WithContext, traits::ApubObject, @@ -48,7 +48,7 @@ pub fn listen(config: &FederationConfig) -> Result<(), Error> { #[debug_handler] async fn http_get_user( Path(name): Path, - data: RequestData, + data: Data, ) -> Result>, Error> { let db_user = data.read_user(&name)?; let apub_user = db_user.into_apub(&data).await?; @@ -57,7 +57,7 @@ async fn http_get_user( #[debug_handler] async fn http_post_user_inbox( - data: RequestData, + data: Data, activity_data: ActivityData, ) -> impl IntoResponse { receive_activity::, DbUser, DatabaseHandle>( @@ -75,7 +75,7 @@ struct WebfingerQuery { #[debug_handler] async fn webfinger( Query(query): Query, - data: RequestData, + data: Data, ) -> Result, Error> { let name = extract_webfinger_name(&query.resource, &data)?; let db_user = data.read_user(&name)?; diff --git a/examples/local_federation/objects/person.rs b/examples/local_federation/objects/person.rs index 2b13786..517f147 100644 --- a/examples/local_federation/objects/person.rs +++ b/examples/local_federation/objects/person.rs @@ -7,7 +7,7 @@ use crate::{ }; use activitypub_federation::{ activity_queue::send_activity, - config::RequestData, + config::Data, fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor}, http_signatures::generate_actor_keypair, kinds::actor::PersonType, @@ -81,15 +81,7 @@ impl DbUser { Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) } - fn public_key(&self) -> PublicKey { - PublicKey::new(self.ap_id.clone().into_inner(), self.public_key.clone()) - } - - pub async fn follow( - &self, - other: &str, - data: &RequestData, - ) -> Result<(), Error> { + pub async fn follow(&self, other: &str, data: &Data) -> Result<(), Error> { let other: DbUser = webfinger_resolve_actor(other, data).await?; let id = generate_object_id(data.domain())?; let follow = Follow::new(self.ap_id.clone(), other.ap_id.clone(), id.clone()); @@ -98,11 +90,7 @@ impl DbUser { Ok(()) } - pub async fn post( - &self, - post: DbPost, - data: &RequestData, - ) -> Result<(), Error> { + pub async fn post(&self, post: DbPost, data: &Data) -> Result<(), Error> { let id = generate_object_id(data.domain())?; let create = CreatePost::new(post.into_apub(data).await?, id.clone()); let mut inboxes = vec![]; @@ -118,20 +106,14 @@ impl DbUser { &self, activity: Activity, recipients: Vec, - data: &RequestData, + data: &Data, ) -> Result<(), ::Error> where Activity: ActivityHandler + Serialize + Debug + Send + Sync, ::Error: From + From, { let activity = WithContext::new_default(activity); - send_activity( - activity, - self.private_key.clone().unwrap(), - recipients, - data, - ) - .await?; + send_activity(activity, self, recipients, data).await?; Ok(()) } } @@ -148,7 +130,7 @@ impl ApubObject for DbUser { async fn read_from_apub_id( object_id: Url, - data: &RequestData, + data: &Data, ) -> Result, Self::Error> { let users = data.users.lock().unwrap(); let res = users @@ -158,10 +140,7 @@ impl ApubObject for DbUser { Ok(res) } - async fn into_apub( - self, - _data: &RequestData, - ) -> Result { + async fn into_apub(self, _data: &Data) -> Result { Ok(Person { preferred_username: self.name.clone(), kind: Default::default(), @@ -174,7 +153,7 @@ impl ApubObject for DbUser { async fn verify( apub: &Self::ApubType, expected_domain: &Url, - _data: &RequestData, + _data: &Data, ) -> Result<(), Self::Error> { verify_domains_match(apub.id.inner(), expected_domain)?; Ok(()) @@ -182,7 +161,7 @@ impl ApubObject for DbUser { async fn from_apub( apub: Self::ApubType, - data: &RequestData, + data: &Data, ) -> Result { let user = DbUser { name: apub.preferred_username, @@ -201,14 +180,18 @@ impl ApubObject for DbUser { } impl Actor for DbUser { - fn id(&self) -> &Url { - self.ap_id.inner() + fn id(&self) -> Url { + self.ap_id.inner().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() } diff --git a/examples/local_federation/objects/post.rs b/examples/local_federation/objects/post.rs index 07d7555..5e2f0a0 100644 --- a/examples/local_federation/objects/post.rs +++ b/examples/local_federation/objects/post.rs @@ -1,6 +1,6 @@ use crate::{error::Error, generate_object_id, instance::DatabaseHandle, objects::person::DbUser}; use activitypub_federation::{ - config::RequestData, + config::Data, fetch::object_id::ObjectId, kinds::{object::NoteType, public}, protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match}, @@ -49,7 +49,7 @@ impl ApubObject for DbPost { async fn read_from_apub_id( object_id: Url, - data: &RequestData, + data: &Data, ) -> Result, Self::Error> { let posts = data.posts.lock().unwrap(); let res = posts @@ -59,10 +59,7 @@ impl ApubObject for DbPost { Ok(res) } - async fn into_apub( - self, - data: &RequestData, - ) -> Result { + async fn into_apub(self, data: &Data) -> Result { let creator = self.creator.dereference_local(data).await?; Ok(Note { kind: Default::default(), @@ -76,7 +73,7 @@ impl ApubObject for DbPost { async fn verify( apub: &Self::ApubType, expected_domain: &Url, - _data: &RequestData, + _data: &Data, ) -> Result<(), Self::Error> { verify_domains_match(apub.id.inner(), expected_domain)?; Ok(()) @@ -84,7 +81,7 @@ impl ApubObject for DbPost { async fn from_apub( apub: Self::ApubType, - data: &RequestData, + data: &Data, ) -> Result { let post = DbPost { text: apub.content, diff --git a/src/activity_queue.rs b/src/activity_queue.rs index c2e8cbc..8f7a6b7 100644 --- a/src/activity_queue.rs +++ b/src/activity_queue.rs @@ -3,11 +3,11 @@ #![doc = include_str!("../docs/09_sending_activities.md")] use crate::{ - config::RequestData, + config::Data, error::Error, http_signatures::sign_request, reqwest_shim::ResponseExt, - traits::ActivityHandler, + traits::{ActivityHandler, Actor}, APUB_JSON_CONTENT_TYPE, }; use anyhow::anyhow; @@ -40,21 +40,25 @@ use url::Url; /// signature. Generated with [crate::http_signatures::generate_actor_keypair]. /// - `inboxes`: List of actor inboxes that should receive the activity. Should be built by calling /// [crate::traits::Actor::shared_inbox_or_inbox] for each target actor. -pub async fn send_activity( +pub async fn send_activity( activity: Activity, - private_key: String, + actor: &ActorType, inboxes: Vec, - data: &RequestData, + data: &Data, ) -> Result<(), ::Error> where Activity: ActivityHandler + Serialize, ::Error: From + From, Datatype: Clone, + ActorType: Actor, { let config = &data.config; let actor_id = activity.actor(); let activity_id = activity.id(); let activity_serialized = serde_json::to_string_pretty(&activity)?; + let private_key = actor + .private_key_pem() + .expect("Actor for sending activity has private key"); let inboxes: Vec = inboxes .into_iter() .unique() diff --git a/src/actix_web/inbox.rs b/src/actix_web/inbox.rs index fdcce81..d9cd4b1 100644 --- a/src/actix_web/inbox.rs +++ b/src/actix_web/inbox.rs @@ -1,7 +1,7 @@ //! Handles incoming activities, verifying HTTP signatures and other checks use crate::{ - config::RequestData, + config::Data, error::Error, fetch::object_id::ObjectId, http_signatures::{verify_inbox_hash, verify_signature}, @@ -17,7 +17,7 @@ use tracing::debug; pub async fn receive_activity( request: HttpRequest, body: Bytes, - data: &RequestData, + data: &Data, ) -> Result::Error> where Activity: ActivityHandler + DeserializeOwned + Send + 'static, @@ -112,8 +112,8 @@ mod test { let request_builder = ClientWithMiddleware::from(Client::default()).post("https://example.com/inbox"); let activity = Follow { - actor: ObjectId::new("http://localhost:123").unwrap(), - object: ObjectId::new("http://localhost:124").unwrap(), + actor: ObjectId::parse("http://localhost:123").unwrap(), + object: ObjectId::parse("http://localhost:124").unwrap(), kind: Default::default(), id: "http://localhost:123/1".try_into().unwrap(), }; diff --git a/src/actix_web/middleware.rs b/src/actix_web/middleware.rs index db8e0f5..1ca3c05 100644 --- a/src/actix_web/middleware.rs +++ b/src/actix_web/middleware.rs @@ -1,4 +1,4 @@ -use crate::config::{ApubMiddleware, FederationConfig, RequestData}; +use crate::config::{ApubMiddleware, Data, FederationConfig}; use actix_web::{ dev::{forward_ready, Payload, Service, ServiceRequest, ServiceResponse, Transform}, Error, @@ -29,7 +29,7 @@ where } } -/// Passes [FederationConfig] to HTTP handlers, converting it to [RequestData] in the process +/// Passes [FederationConfig] to HTTP handlers, converting it to [Data] in the process #[doc(hidden)] pub struct ApubService where @@ -61,7 +61,7 @@ where } } -impl FromRequest for RequestData { +impl FromRequest for Data { type Error = Error; type Future = Ready>; diff --git a/src/axum/inbox.rs b/src/axum/inbox.rs index 03362c0..3f761d0 100644 --- a/src/axum/inbox.rs +++ b/src/axum/inbox.rs @@ -3,7 +3,7 @@ #![doc = include_str!("../../docs/08_receiving_activities.md")] use crate::{ - config::RequestData, + config::Data, error::Error, fetch::object_id::ObjectId, http_signatures::{verify_inbox_hash, verify_signature}, @@ -23,7 +23,7 @@ use tracing::debug; /// Handles incoming activities, verifying HTTP signatures and other checks pub async fn receive_activity( activity_data: ActivityData, - data: &RequestData, + data: &Data, ) -> Result<(), ::Error> where Activity: ActivityHandler + DeserializeOwned + Send + 'static, diff --git a/src/axum/json.rs b/src/axum/json.rs index 18dfbe4..55604a2 100644 --- a/src/axum/json.rs +++ b/src/axum/json.rs @@ -5,10 +5,10 @@ //! # use axum::extract::Path; //! # use activitypub_federation::axum::json::ApubJson; //! # use activitypub_federation::protocol::context::WithContext; -//! # use activitypub_federation::config::RequestData; +//! # use activitypub_federation::config::Data; //! # use activitypub_federation::traits::ApubObject; //! # use activitypub_federation::traits::tests::{DbConnection, DbUser, Person}; -//! async fn http_get_user(Path(name): Path, data: RequestData) -> Result>, Error> { +//! async fn http_get_user(Path(name): Path, data: Data) -> Result>, Error> { //! let user: DbUser = data.read_local_user(name).await?; //! let person = user.into_apub(&data).await?; //! diff --git a/src/axum/middleware.rs b/src/axum/middleware.rs index cf4d473..11b6c4a 100644 --- a/src/axum/middleware.rs +++ b/src/axum/middleware.rs @@ -1,4 +1,4 @@ -use crate::config::{ApubMiddleware, FederationConfig, RequestData}; +use crate::config::{ApubMiddleware, Data, FederationConfig}; use axum::{async_trait, body::Body, extract::FromRequestParts, http::Request, response::Response}; use http::{request::Parts, StatusCode}; use std::task::{Context, Poll}; @@ -15,7 +15,7 @@ impl Layer for ApubMiddleware { } } -/// Passes [FederationConfig] to HTTP handlers, converting it to [RequestData] in the process +/// Passes [FederationConfig] to HTTP handlers, converting it to [Data] in the process #[doc(hidden)] #[derive(Clone)] pub struct ApubService { @@ -44,7 +44,7 @@ where } #[async_trait] -impl FromRequestParts for RequestData +impl FromRequestParts for Data where S: Send + Sync, T: Send + Sync, diff --git a/src/config.rs b/src/config.rs index ad0ed4e..b4b40d8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,7 +28,10 @@ use reqwest_middleware::ClientWithMiddleware; use serde::de::DeserializeOwned; use std::{ ops::Deref, - sync::{atomic::AtomicI32, Arc}, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, time::Duration, }; use url::Url; @@ -46,7 +49,7 @@ pub struct FederationConfig { /// Maximum number of outgoing HTTP requests per incoming HTTP request. See /// [crate::fetch::object_id::ObjectId] for more details. #[builder(default = "20")] - pub(crate) http_fetch_limit: i32, + pub(crate) http_fetch_limit: u32, #[builder(default = "reqwest::Client::default().into()")] /// HTTP client used for all outgoing requests. Middleware can be used to add functionality /// like log tracing or retry of failed requests. @@ -102,9 +105,9 @@ impl FederationConfig { Ok(()) } - /// Create new [RequestData] from this. You should prefer to use a middleware if possible. - pub fn to_request_data(&self) -> RequestData { - RequestData { + /// Create new [Data] from this. You should prefer to use a middleware if possible. + pub fn to_request_data(&self) -> Data { + Data { config: self.clone(), request_counter: Default::default(), } @@ -127,6 +130,11 @@ impl FederationConfig { _ => return Err(Error::UrlVerificationError("Invalid url scheme")), }; + // Urls which use our local domain are not a security risk, no further verification needed + if self.is_local_url(url) { + return Ok(()); + } + if url.domain().is_none() { return Err(Error::UrlVerificationError("Url must have a domain")); } @@ -137,11 +145,6 @@ impl FederationConfig { )); } - // Urls which use our local domain are not a security risk, no further verification needed - if self.is_local_url(url) { - return Ok(()); - } - self.url_verifier .verify(url) .await @@ -253,24 +256,36 @@ clone_trait_object!(UrlVerifier); /// prevent denial of service attacks, where an attacker triggers fetching of recursive objects. /// /// -pub struct RequestData { +pub struct Data { pub(crate) config: FederationConfig, - pub(crate) request_counter: AtomicI32, + pub(crate) request_counter: AtomicU32, } -impl RequestData { +impl Data { /// Returns the data which was stored in [FederationConfigBuilder::app_data] pub fn app_data(&self) -> &T { &self.config.app_data } - /// Returns the domain that was configured in [FederationConfig]. + /// The domain that was configured in [FederationConfig]. pub fn domain(&self) -> &str { &self.config.domain } + + /// Returns a new instance of `Data` with request counter set to 0. + pub fn reset_request_count(&self) -> Self { + Data { + config: self.config.clone(), + request_counter: Default::default(), + } + } + /// Total number of outgoing HTTP requests made with this data. + pub fn request_count(&self) -> u32 { + self.request_counter.load(Ordering::Relaxed) + } } -impl Deref for RequestData { +impl Deref for Data { type Target = T; fn deref(&self) -> &T { @@ -278,7 +293,7 @@ impl Deref for RequestData { } } -/// Middleware for HTTP handlers which provides access to [RequestData] +/// Middleware for HTTP handlers which provides access to [Data] #[derive(Clone)] pub struct ApubMiddleware(pub(crate) FederationConfig); diff --git a/src/fetch/collection_id.rs b/src/fetch/collection_id.rs new file mode 100644 index 0000000..4524e6f --- /dev/null +++ b/src/fetch/collection_id.rs @@ -0,0 +1,97 @@ +use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::ApubCollection}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{Debug, Display, Formatter}, + marker::PhantomData, +}; +use url::Url; + +/// Typed wrapper for Activitypub Collection ID which helps with dereferencing. +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct CollectionId(Box, PhantomData) +where + Kind: ApubCollection, + for<'de2> ::ApubType: Deserialize<'de2>; + +impl CollectionId +where + Kind: ApubCollection, + for<'de2> ::ApubType: Deserialize<'de2>, +{ + /// Construct a new CollectionId instance + pub fn parse(url: T) -> Result + where + T: TryInto, + url::ParseError: From<>::Error>, + { + Ok(Self(Box::new(url.try_into()?), PhantomData::)) + } + + /// Fetches collection over HTTP + /// + /// Unlike [ObjectId::fetch](crate::fetch::object_id::ObjectId::fetch) this method doesn't do + /// any caching. + pub async fn dereference( + &self, + owner: &::Owner, + data: &Data<::DataType>, + ) -> Result::Error> + where + ::Error: From, + { + let apub = fetch_object_http(&self.0, data).await?; + Kind::verify(&apub, &self.0, data).await?; + Kind::from_apub(apub, owner, data).await + } +} + +/// Need to implement clone manually, to avoid requiring Kind to be Clone +impl Clone for CollectionId +where + Kind: ApubCollection, + for<'de2> ::ApubType: serde::Deserialize<'de2>, +{ + fn clone(&self) -> Self { + CollectionId(self.0.clone(), self.1) + } +} + +impl Display for CollectionId +where + Kind: ApubCollection, + for<'de2> ::ApubType: serde::Deserialize<'de2>, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_str()) + } +} + +impl Debug for CollectionId +where + Kind: ApubCollection, + for<'de2> ::ApubType: serde::Deserialize<'de2>, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_str()) + } +} +impl From> for Url +where + Kind: ApubCollection, + for<'de2> ::ApubType: serde::Deserialize<'de2>, +{ + fn from(id: CollectionId) -> Self { + *id.0 + } +} + +impl From for CollectionId +where + Kind: ApubCollection + Send + 'static, + for<'de2> ::ApubType: serde::Deserialize<'de2>, +{ + fn from(url: Url) -> Self { + CollectionId(Box::new(url), PhantomData::) + } +} diff --git a/src/fetch/mod.rs b/src/fetch/mod.rs index fb55369..be76966 100644 --- a/src/fetch/mod.rs +++ b/src/fetch/mod.rs @@ -2,13 +2,15 @@ //! #![doc = include_str!("../../docs/07_fetching_data.md")] -use crate::{config::RequestData, error::Error, reqwest_shim::ResponseExt, APUB_JSON_CONTENT_TYPE}; +use crate::{config::Data, error::Error, reqwest_shim::ResponseExt, APUB_JSON_CONTENT_TYPE}; use http::StatusCode; use serde::de::DeserializeOwned; use std::sync::atomic::Ordering; use tracing::info; use url::Url; +/// Typed wrapper for collection IDs +pub mod collection_id; /// Typed wrapper for Activitypub Object ID which helps with dereferencing and caching pub mod object_id; /// Resolves identifiers of the form `name@example.com` @@ -24,9 +26,9 @@ pub mod webfinger; /// If the value exceeds [FederationSettings.http_fetch_limit], the request is aborted with /// [Error::RequestLimit]. This prevents denial of service attacks where an attack triggers /// infinite, recursive fetching of data. -async fn fetch_object_http( +pub async fn fetch_object_http( url: &Url, - data: &RequestData, + data: &Data, ) -> Result { let config = &data.config; // dont fetch local objects this way diff --git a/src/fetch/object_id.rs b/src/fetch/object_id.rs index af9b4fa..f0d2f76 100644 --- a/src/fetch/object_id.rs +++ b/src/fetch/object_id.rs @@ -1,13 +1,25 @@ -use crate::{config::RequestData, error::Error, fetch::fetch_object_http, traits::ApubObject}; +use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::ApubObject}; use anyhow::anyhow; use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; use std::{ fmt::{Debug, Display, Formatter}, marker::PhantomData, + str::FromStr, }; use url::Url; +impl FromStr for ObjectId +where + T: ApubObject + Send + 'static, + for<'de2> ::ApubType: Deserialize<'de2>, +{ + type Err = url::ParseError; + + fn from_str(s: &str) -> Result { + ObjectId::parse(s) + } +} /// Typed wrapper for Activitypub Object ID which helps with dereferencing and caching. /// /// It provides convenient methods for fetching the object from remote server or local database. @@ -32,7 +44,7 @@ use url::Url; /// .app_data(db_connection) /// .build()?; /// let request_data = config.to_request_data(); -/// let object_id = ObjectId::::new("https://lemmy.ml/u/nutomic")?; +/// let object_id = ObjectId::::parse("https://lemmy.ml/u/nutomic")?; /// // Attempt to fetch object from local database or fall back to remote server /// let user = object_id.dereference(&request_data).await; /// assert!(user.is_ok()); @@ -55,7 +67,7 @@ where for<'de2> ::ApubType: serde::Deserialize<'de2>, { /// Construct a new objectid instance - pub fn new(url: T) -> Result + pub fn parse(url: T) -> Result where T: TryInto, url::ParseError: From<>::Error>, @@ -76,7 +88,7 @@ where /// Fetches an activitypub object, either from local database (if possible), or over http. pub async fn dereference( &self, - data: &RequestData<::DataType>, + data: &Data<::DataType>, ) -> Result::Error> where ::Error: From + From, @@ -111,7 +123,7 @@ where /// the object is not found in the database. pub async fn dereference_local( &self, - data: &RequestData<::DataType>, + data: &Data<::DataType>, ) -> Result::Error> where ::Error: From, @@ -123,7 +135,7 @@ where /// returning none means the object was not found in local db async fn dereference_from_db( &self, - data: &RequestData<::DataType>, + data: &Data<::DataType>, ) -> Result, ::Error> { let id = self.0.clone(); ApubObject::read_from_apub_id(*id, data).await @@ -131,7 +143,7 @@ where async fn dereference_from_http( &self, - data: &RequestData<::DataType>, + data: &Data<::DataType>, db_object: Option, ) -> Result::Error> where @@ -238,7 +250,7 @@ pub mod tests { #[test] fn test_deserialize() { - let id = ObjectId::::new("http://test.com/").unwrap(); + let id = ObjectId::::parse("http://test.com/").unwrap(); let string = serde_json::to_string(&id).unwrap(); assert_eq!("\"http://test.com/\"", string); diff --git a/src/fetch/webfinger.rs b/src/fetch/webfinger.rs index 79a6d17..281b005 100644 --- a/src/fetch/webfinger.rs +++ b/src/fetch/webfinger.rs @@ -1,5 +1,5 @@ use crate::{ - config::RequestData, + config::Data, error::{Error, Error::WebfingerResolveFailed}, fetch::{fetch_object_http, object_id::ObjectId}, traits::{Actor, ApubObject}, @@ -19,7 +19,7 @@ use url::Url; /// is then fetched using [ObjectId::dereference], and the result returned. pub async fn webfinger_resolve_actor( identifier: &str, - data: &RequestData, + data: &Data, ) -> Result::Error> where Kind: ApubObject + Actor + Send + 'static + ApubObject, @@ -66,7 +66,7 @@ where /// request. For a parameter of the form `acct:gargron@mastodon.social` it returns `gargron`. /// /// Returns an error if query doesn't match local domain. -pub fn extract_webfinger_name(query: &str, data: &RequestData) -> Result +pub fn extract_webfinger_name(query: &str, data: &Data) -> Result where T: Clone, { @@ -118,7 +118,7 @@ pub fn build_webfinger_response(subject: String, url: Url) -> Webfinger { } /// A webfinger response with information about a `Person` or other type of actor. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] pub struct Webfinger { /// The actor which is described here, for example `acct:LemmyDev@mastodon.social` pub subject: String, @@ -133,7 +133,7 @@ pub struct Webfinger { } /// A single link included as part of a [Webfinger] response. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] pub struct WebfingerLink { /// Relationship of the link, such as `self` or `http://webfinger.net/rel/profile-page` pub rel: Option, diff --git a/src/protocol/context.rs b/src/protocol/context.rs index 692e7d0..433f8f3 100644 --- a/src/protocol/context.rs +++ b/src/protocol/context.rs @@ -19,11 +19,7 @@ //! Ok::<(), serde_json::error::Error>(()) //! ``` -use crate::{ - config::RequestData, - protocol::helpers::deserialize_one_or_many, - traits::ActivityHandler, -}; +use crate::{config::Data, protocol::helpers::deserialize_one_or_many, traits::ActivityHandler}; use serde::{Deserialize, Serialize}; use serde_json::Value; use url::Url; @@ -75,11 +71,23 @@ where self.inner.actor() } - async fn verify(&self, data: &RequestData) -> Result<(), Self::Error> { + async fn verify(&self, data: &Data) -> Result<(), Self::Error> { self.inner.verify(data).await } - async fn receive(self, data: &RequestData) -> Result<(), Self::Error> { + async fn receive(self, data: &Data) -> Result<(), Self::Error> { self.inner.receive(data).await } } + +impl Clone for WithContext +where + T: Clone, +{ + fn clone(&self) -> Self { + Self { + context: self.context.clone(), + inner: self.inner.clone(), + } + } +} diff --git a/src/protocol/public_key.rs b/src/protocol/public_key.rs index 27dd550..ecfcd3c 100644 --- a/src/protocol/public_key.rs +++ b/src/protocol/public_key.rs @@ -21,7 +21,7 @@ impl PublicKey { /// Create a new [PublicKey] struct for the `owner` with `public_key_pem`. /// /// It uses an standard key id of `{actor_id}#main-key` - pub fn new(owner: Url, public_key_pem: String) -> Self { + pub(crate) fn new(owner: Url, public_key_pem: String) -> Self { let id = main_key_id(&owner); PublicKey { id, diff --git a/src/traits.rs b/src/traits.rs index 4744632..6ec244d 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,86 +1,94 @@ //! Traits which need to be implemented for federated data types -use crate::{config::RequestData, protocol::public_key::PublicKey}; +use crate::{config::Data, protocol::public_key::PublicKey}; use async_trait::async_trait; use chrono::NaiveDateTime; -use std::ops::Deref; +use serde::Deserialize; +use std::{fmt::Debug, ops::Deref}; use url::Url; /// Helper for converting between database structs and federated protocol structs. /// /// ``` +/// # use activitystreams_kinds::{object::NoteType, public}; /// # use chrono::{Local, NaiveDateTime}; +/// # use serde::{Deserialize, Serialize}; /// # use url::Url; -/// # use activitypub_federation::protocol::public_key::PublicKey; -/// # use activitypub_federation::config::RequestData; -/// use activitypub_federation::protocol::verification::verify_domains_match; -/// # use activitypub_federation::traits::ApubObject; -/// # use activitypub_federation::traits::tests::{DbConnection, Person}; -/// # pub struct DbUser { -/// # pub name: String, -/// # pub ap_id: Url, -/// # pub inbox: Url, -/// # pub public_key: String, -/// # pub private_key: Option, -/// # pub local: bool, -/// # pub last_refreshed_at: NaiveDateTime, -/// # } -/// -/// #[async_trait::async_trait] -/// impl ApubObject for DbUser { -/// type DataType = DbConnection; -/// type ApubType = Person; -/// type Error = anyhow::Error; -/// -/// fn last_refreshed_at(&self) -> Option { -/// Some(self.last_refreshed_at) +/// # use activitypub_federation::protocol::{public_key::PublicKey, helpers::deserialize_one_or_many}; +/// # use activitypub_federation::config::Data; +/// # use activitypub_federation::fetch::object_id::ObjectId; +/// # use activitypub_federation::protocol::verification::verify_domains_match; +/// # use activitypub_federation::traits::{Actor, ApubObject}; +/// # use activitypub_federation::traits::tests::{DbConnection, DbUser}; +/// # +/// /// How the post is read/written in the local database +/// pub struct DbPost { +/// pub text: String, +/// pub ap_id: ObjectId, +/// pub creator: ObjectId, +/// pub local: bool, /// } /// -/// async fn read_from_apub_id(object_id: Url, data: &RequestData) -> Result, Self::Error> { +/// /// How the post is serialized and represented as Activitypub JSON +/// #[derive(Deserialize, Serialize, Debug)] +/// #[serde(rename_all = "camelCase")] +/// pub struct Note { +/// #[serde(rename = "type")] +/// kind: NoteType, +/// id: ObjectId, +/// pub(crate) attributed_to: ObjectId, +/// #[serde(deserialize_with = "deserialize_one_or_many")] +/// pub(crate) to: Vec, +/// content: String, +/// } +/// +/// #[async_trait::async_trait] +/// impl ApubObject for DbPost { +/// type DataType = DbConnection; +/// type ApubType = Note; +/// type Error = anyhow::Error; +/// +/// async fn read_from_apub_id(object_id: Url, data: &Data) -> Result, Self::Error> { /// // Attempt to read object from local database. Return Ok(None) if not found. -/// let user: Option = data.read_user_from_apub_id(object_id).await?; -/// Ok(user) +/// let post: Option = data.read_post_from_apub_id(object_id).await?; +/// Ok(post) /// } /// -/// async fn into_apub(self, data: &RequestData) -> Result { +/// async fn into_apub(self, data: &Data) -> Result { /// // Called when a local object gets sent out over Activitypub. Simply convert it to the /// // protocol struct -/// Ok(Person { +/// Ok(Note { /// kind: Default::default(), -/// preferred_username: self.name, /// id: self.ap_id.clone().into(), -/// inbox: self.inbox, -/// public_key: PublicKey::new(self.ap_id, self.public_key), +/// attributed_to: self.creator, +/// to: vec![public()], +/// content: self.text, /// }) /// } /// -/// async fn verify(apub: &Self::ApubType, expected_domain: &Url, data: &RequestData,) -> Result<(), Self::Error> { +/// async fn verify(apub: &Self::ApubType, expected_domain: &Url, data: &Data,) -> Result<(), Self::Error> { /// verify_domains_match(apub.id.inner(), expected_domain)?; /// // additional application specific checks /// Ok(()) /// } /// -/// async fn from_apub(apub: Self::ApubType, data: &RequestData) -> Result { +/// async fn from_apub(apub: Self::ApubType, data: &Data) -> Result { /// // Called when a remote object gets received over Activitypub. Validate and insert it /// // into the database. /// -/// let user = DbUser { -/// name: apub.preferred_username, -/// ap_id: apub.id.into_inner(), -/// inbox: apub.inbox, -/// public_key: apub.public_key.public_key_pem, -/// private_key: None, +/// let post = DbPost { +/// text: apub.content, +/// ap_id: apub.id, +/// creator: apub.attributed_to, /// local: false, -/// last_refreshed_at: Local::now().naive_local(), /// }; /// -/// // Make sure not to overwrite any local object -/// if data.domain() == user.ap_id.domain().unwrap() { -/// // Activitypub doesnt distinguish between creating and updating an object. Thats why we -/// // need to use upsert functionality here -/// data.upsert(&user).await?; -/// } -/// Ok(user) +/// // Here we need to persist the object in the local database. Note that Activitypub +/// // doesnt distinguish between creating and updating an object. Thats why we need to +/// // use upsert functionality. +/// data.upsert(&post).await?; +/// +/// Ok(post) /// } /// /// } @@ -112,13 +120,13 @@ pub trait ApubObject: Sized { /// Should return `Ok(None)` if not found. async fn read_from_apub_id( object_id: Url, - data: &RequestData, + data: &Data, ) -> Result, Self::Error>; /// Mark remote object as deleted in local database. /// /// Called when a `Delete` activity is received, or if fetch returns a `Tombstone` object. - async fn delete(self, _data: &RequestData) -> Result<(), Self::Error> { + async fn delete(self, _data: &Data) -> Result<(), Self::Error> { Ok(()) } @@ -126,10 +134,7 @@ pub trait ApubObject: Sized { /// /// Called when a local object gets fetched by another instance over HTTP, or when an object /// gets sent in an activity. - async fn into_apub( - self, - data: &RequestData, - ) -> Result; + async fn into_apub(self, data: &Data) -> Result; /// Verifies that the received object is valid. /// @@ -141,17 +146,17 @@ pub trait ApubObject: Sized { async fn verify( apub: &Self::ApubType, expected_domain: &Url, - data: &RequestData, + data: &Data, ) -> Result<(), Self::Error>; /// Convert object from ActivityPub type to database type. /// /// Called when an object is received from HTTP fetch or as part of an activity. This method - /// should do verification and write the received object to database. Note that there is no - /// distinction between create and update, so an `upsert` operation should be used. + /// 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_apub( apub: Self::ApubType, - data: &RequestData, + data: &Data, ) -> Result; } @@ -161,7 +166,7 @@ pub trait ApubObject: Sized { /// # use activitystreams_kinds::activity::FollowType; /// # use url::Url; /// # use activitypub_federation::fetch::object_id::ObjectId; -/// # use activitypub_federation::config::RequestData; +/// # use activitypub_federation::config::Data; /// # use activitypub_federation::traits::ActivityHandler; /// # use activitypub_federation::traits::tests::{DbConnection, DbUser}; /// #[derive(serde::Deserialize)] @@ -186,11 +191,11 @@ pub trait ApubObject: Sized { /// self.actor.inner() /// } /// -/// async fn verify(&self, data: &RequestData) -> Result<(), Self::Error> { +/// async fn verify(&self, data: &Data) -> Result<(), Self::Error> { /// Ok(()) /// } /// -/// async fn receive(self, data: &RequestData) -> Result<(), Self::Error> { +/// async fn receive(self, data: &Data) -> Result<(), Self::Error> { /// let local_user = self.object.dereference(data).await?; /// let follower = self.actor.dereference(data).await?; /// data.add_follower(local_user, follower).await?; @@ -217,19 +222,19 @@ pub trait ActivityHandler { /// /// This needs to be a separate method, because it might be used for activities /// like `Undo/Follow`, which shouldn't perform any database write for the inner `Follow`. - async fn verify(&self, data: &RequestData) -> Result<(), Self::Error>; + async fn verify(&self, data: &Data) -> Result<(), Self::Error>; /// Called when an activity is received. /// /// Should perform validation and possibly write action to the database. In case the activity /// has a nested `object` field, must call `object.from_apub` handler. - async fn receive(self, data: &RequestData) -> Result<(), Self::Error>; + async fn receive(self, data: &Data) -> Result<(), Self::Error>; } /// Trait to allow retrieving common Actor data. -pub trait Actor: ApubObject { +pub trait Actor: ApubObject + Send + 'static { /// `id` field of the actor - fn id(&self) -> &Url; + fn id(&self) -> Url; /// The actor's public key for verifying signatures of incoming activities. /// @@ -237,12 +242,18 @@ pub trait Actor: ApubObject { /// actor keypair. fn public_key_pem(&self) -> &str; + /// The actor's private key for signing outgoing activities. + /// + /// Use [generate_actor_keypair](crate::http_signatures::generate_actor_keypair) to create the + /// actor keypair. + fn private_key_pem(&self) -> Option; + /// The inbox where activities for this user should be sent to fn inbox(&self) -> Url; /// Generates a public key struct for use in the actor json representation fn public_key(&self) -> PublicKey { - PublicKey::new(self.id().clone(), self.public_key_pem().to_string()) + PublicKey::new(self.id(), self.public_key_pem().to_string()) } /// The actor's shared inbox, if any @@ -273,15 +284,56 @@ where self.deref().actor() } - async fn verify(&self, data: &RequestData) -> Result<(), Self::Error> { - (*self).verify(data).await + async fn verify(&self, data: &Data) -> Result<(), Self::Error> { + self.deref().verify(data).await } - async fn receive(self, data: &RequestData) -> Result<(), Self::Error> { + async fn receive(self, data: &Data) -> Result<(), Self::Error> { (*self).receive(data).await } } +/// Trait for federating collections +#[async_trait] +pub trait ApubCollection: Sized { + /// Actor or object that this collection belongs to + type Owner; + /// App data type passed to handlers. Must be identical to + /// [crate::config::FederationConfigBuilder::app_data] type. + type DataType: Clone + Send + Sync; + /// The type of protocol struct which gets sent over network to federate this database struct. + type ApubType: for<'de2> Deserialize<'de2>; + /// Error type returned by handler methods + type Error; + + /// Reads local collection from database and returns it as Activitypub JSON. + async fn read_local( + owner: &Self::Owner, + data: &Data, + ) -> Result; + + /// Verifies that the received object is valid. + /// + /// You should check here that the domain of id matches `expected_domain`. Additionally you + /// should perform any application specific checks. + async fn verify( + apub: &Self::ApubType, + expected_domain: &Url, + data: &Data, + ) -> Result<(), Self::Error>; + + /// Convert object from ActivityPub type to database type. + /// + /// Called when an object is received from HTTP fetch or as part of an activity. This method + /// should also 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_apub( + apub: Self::ApubType, + owner: &Self::Owner, + 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 @@ -303,7 +355,7 @@ pub mod tests { pub struct DbConnection; impl DbConnection { - pub async fn read_user_from_apub_id(&self, _: Url) -> Result, Error> { + pub async fn read_post_from_apub_id(&self, _: Url) -> Result, Error> { Ok(None) } pub async fn read_local_user(&self, _: String) -> Result { @@ -346,7 +398,7 @@ pub mod tests { apub_id: "https://localhost/123".parse().unwrap(), inbox: "https://localhost/123/inbox".parse().unwrap(), public_key: DB_USER_KEYPAIR.public_key.clone(), - private_key: None, + private_key: Some(DB_USER_KEYPAIR.private_key.clone()), followers: vec![], local: false, }); @@ -359,29 +411,28 @@ pub mod tests { async fn read_from_apub_id( _object_id: Url, - _data: &RequestData, + _data: &Data, ) -> Result, Self::Error> { Ok(Some(DB_USER.clone())) } async fn into_apub( self, - _data: &RequestData, + _data: &Data, ) -> Result { - let public_key = PublicKey::new(self.apub_id.clone(), self.public_key.clone()); Ok(Person { preferred_username: self.name.clone(), kind: Default::default(), - id: self.apub_id.into(), - inbox: self.inbox, - public_key, + id: self.apub_id.clone().into(), + inbox: self.inbox.clone(), + public_key: self.public_key(), }) } async fn verify( apub: &Self::ApubType, expected_domain: &Url, - _data: &RequestData, + _data: &Data, ) -> Result<(), Self::Error> { verify_domains_match(apub.id.inner(), expected_domain)?; Ok(()) @@ -389,7 +440,7 @@ pub mod tests { async fn from_apub( apub: Self::ApubType, - _data: &RequestData, + _data: &Data, ) -> Result { Ok(DbUser { name: apub.preferred_username, @@ -404,14 +455,18 @@ pub mod tests { } impl Actor for DbUser { - fn id(&self) -> &Url { - &self.apub_id + fn id(&self) -> Url { + self.apub_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() } @@ -440,11 +495,11 @@ pub mod tests { self.actor.inner() } - async fn verify(&self, _: &RequestData) -> Result<(), Self::Error> { + async fn verify(&self, _: &Data) -> Result<(), Self::Error> { Ok(()) } - async fn receive(self, _data: &RequestData) -> Result<(), Self::Error> { + async fn receive(self, _data: &Data) -> Result<(), Self::Error> { Ok(()) } } @@ -463,29 +518,26 @@ pub mod tests { async fn read_from_apub_id( _: Url, - _: &RequestData, + _: &Data, ) -> Result, Self::Error> { todo!() } - async fn into_apub( - self, - _: &RequestData, - ) -> Result { + async fn into_apub(self, _: &Data) -> Result { todo!() } async fn verify( _: &Self::ApubType, _: &Url, - _: &RequestData, + _: &Data, ) -> Result<(), Self::Error> { todo!() } async fn from_apub( _: Self::ApubType, - _: &RequestData, + _: &Data, ) -> Result { todo!() }