Various additions and changes (#147)

* Add methods Object.id(), Object.deleted()

* Rename ActivityHandler to Activity

* comments

* fix

* comment
This commit is contained in:
Nutomic 2025-07-10 08:26:45 +00:00 committed by GitHub
parent e18b13e253
commit fa27a0c0b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 457 additions and 320 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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),
}

View file

@ -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<Self::DataType>,

View file

@ -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;

View file

@ -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<DateTime<Utc>> {
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
}

View file

@ -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<Self::DataType>,

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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};

View file

@ -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<Activity>(
pub(crate) async fn send<A>(
&self,
activity: Activity,
activity: A,
recipients: Vec<Url>,
use_queue: bool,
data: &Data<DatabaseHandle>,
) -> Result<(), Error>
where
Activity: ActivityHandler + Serialize + Debug + Send + Sync,
<Activity as ActivityHandler>::Error: From<anyhow::Error> + From<serde_json::Error>,
A: Activity + Serialize + Debug + Send + Sync,
<A as Activity>::Error: From<anyhow::Error> + From<serde_json::Error>,
{
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<DateTime<Utc>> {
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
}

View file

@ -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<Self::DataType>,

View file

@ -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, Datatype, ActorType>(
activity: &Activity,
pub async fn queue_activity<A, Datatype, ActorType>(
activity: &A,
actor: &ActorType,
inboxes: Vec<Url>,
data: &Data<Datatype>,
) -> Result<(), Error>
where
Activity: ActivityHandler + Serialize + Debug,
A: Activity + Serialize + Debug,
Datatype: Clone,
ActorType: Actor,
{

View file

@ -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, Datatype, ActorType>(
activity: &Activity,
pub async fn prepare<A, Datatype, ActorType>(
activity: &A,
actor: &ActorType,
inboxes: Vec<Url>,
data: &Data<Datatype>,
) -> Result<Vec<SendActivityTask>, 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, Datatype, ActorType>(
activity: &Activity,
pub(crate) async fn build_tasks<A, Datatype, ActorType>(
activity: &A,
actor: &ActorType,
inboxes: Vec<Url>,
data: &Data<Datatype>,
) -> Result<Vec<SendActivityTask>, 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"

View file

@ -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<Activity, ActorT, Datatype>(
/// After successful validation, activities are passed to respective [trait@Activity].
pub async fn receive_activity<A, ActorT, Datatype>(
request: HttpRequest,
body: Bytes,
data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
) -> Result<HttpResponse, <A as Activity>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>,
<A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>,
Datatype: Clone,
{
let (activity, _) = do_stuff::<Activity, ActorT, Datatype>(request, body, data).await?;
let (activity, _) = do_stuff::<A, ActorT, Datatype>(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<Activity, ActorT, Datatype>
pub trait ReceiveActivityHook<A, ActorT, Datatype>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + Clone + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>,
<A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>,
Datatype: Clone,
{
/// Called when a new activity is recived
fn hook(
self,
activity: &Activity,
activity: &A,
actor: &ActorT,
data: &Data<Datatype>,
) -> impl std::future::Future<Output = Result<(), <Activity as ActivityHandler>::Error>>;
) -> impl std::future::Future<Output = Result<(), <A as Activity>::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<Activity, ActorT, Datatype>(
pub async fn receive_activity_with_hook<A, ActorT, Datatype>(
request: HttpRequest,
body: Bytes,
hook: impl ReceiveActivityHook<Activity, ActorT, Datatype>,
hook: impl ReceiveActivityHook<A, ActorT, Datatype>,
data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
) -> Result<HttpResponse, <A as Activity>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + Clone + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>,
<A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>,
Datatype: Clone,
{
let (activity, actor) = do_stuff::<Activity, ActorT, Datatype>(request, body, data).await?;
let (activity, actor) = do_stuff::<A, ActorT, Datatype>(request, body, data).await?;
hook.hook(&activity, &actor, data).await?;
do_more_stuff(activity, data).await
}
async fn do_stuff<Activity, ActorT, Datatype>(
async fn do_stuff<A, ActorT, Datatype>(
request: HttpRequest,
body: Bytes,
data: &Data<Datatype>,
) -> Result<(Activity, ActorT), <Activity as ActivityHandler>::Error>
) -> Result<(A, ActorT), <A as Activity>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>,
<A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>,
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::<Activity, ActorT, _>(&body, data).await?;
let (activity, actor) = parse_received_activity::<A, ActorT, _>(&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, Datatype>(
activity: Activity,
async fn do_more_stuff<A, Datatype>(
activity: A,
data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
) -> Result<HttpResponse, <A as Activity>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
Datatype: Clone,
{
debug!("Receiving activity {}", activity.id().to_string());
@ -160,21 +160,21 @@ mod test {
struct Dummy;
impl<Activity, ActorT, Datatype> ReceiveActivityHook<Activity, ActorT, Datatype> for Dummy
impl<A, ActorT, Datatype> ReceiveActivityHook<A, ActorT, Datatype> for Dummy
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + Clone + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>,
<A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>,
Datatype: Clone,
{
async fn hook(
self,
_activity: &Activity,
_activity: &A,
_actor: &ActorT,
_data: &Data<Datatype>,
) -> Result<(), <Activity as ActivityHandler>::Error> {
) -> Result<(), <A as Activity>::Error> {
// ensure that hook gets called by returning this value
Err(Error::Other("test-error".to_string()).into())
}

View file

@ -4,6 +4,7 @@ mod http_compat;
pub mod inbox;
#[doc(hidden)]
pub mod middleware;
pub mod response;
use crate::{
config::Data,

50
src/actix_web/response.rs Normal file
View file

@ -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<T: Serialize>(
data: T,
federation_context: &Value,
) -> Result<HttpResponse, serde_json::Error> {
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<HttpResponse, serde_json::Error> {
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()
}

View file

@ -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<Activity, ActorT, Datatype>(
pub async fn receive_activity<A, ActorT, Datatype>(
activity_data: ActivityData,
data: &Data<Datatype>,
) -> Result<(), <Activity as ActivityHandler>::Error>
) -> Result<(), <A as Activity>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>,
<A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>,
Datatype: Clone,
{
let (activity, actor) =
parse_received_activity::<Activity, ActorT, _>(&activity_data.body, data).await?;
parse_received_activity::<A, ActorT, _>(&activity_data.body, data).await?;
verify_signature(
&activity_data.headers,

View file

@ -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<T: Clone> FederationConfig<T> {
FederationConfigBuilder::default()
}
pub(crate) async fn verify_url_and_domain<Activity, Datatype>(
&self,
activity: &Activity,
) -> Result<(), Error>
pub(crate) async fn verify_url_and_domain<A, Datatype>(&self, activity: &A) -> Result<(), Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
{
verify_domains_match(activity.id(), activity.actor())?;
self.verify_url_valid(activity.id()).await?;
@ -258,7 +255,7 @@ impl<T: Clone> FederationConfigBuilder<T> {
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
}

View file

@ -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<Activity, ActorT, Datatype>(
async fn parse_received_activity<A, ActorT, Datatype>(
body: &[u8],
data: &Data<Datatype>,
) -> Result<(Activity, ActorT), <Activity as ActivityHandler>::Error>
) -> Result<(A, ActorT), <A as Activity>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>,
<A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>,
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 }

View file

@ -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<T> WithContext<T> {
}
#[async_trait::async_trait]
impl<T> ActivityHandler for WithContext<T>
impl<T> Activity for WithContext<T>
where
T: ActivityHandler + Send + Sync,
T: Activity + Send + Sync,
{
type DataType = <T as ActivityHandler>::DataType;
type Error = <T as ActivityHandler>::Error;
type DataType = <T as Activity>::DataType;
type Error = <T as Activity>::Error;
fn id(&self) -> &Url {
self.inner.id()

View file

@ -3,5 +3,6 @@
pub mod context;
pub mod helpers;
pub mod public_key;
pub mod tombstone;
pub mod values;
pub mod verification;

27
src/protocol/tombstone.rs Normal file
View file

@ -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
///
/// <https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone>
#[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,
}
}
}

View file

@ -29,6 +29,14 @@ where
type Kind = UntaggedEither<T::Kind, R::Kind>;
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<DateTime<Utc>> {
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<Self::DataType>) -> Result<Self::Kind, Self::Error> {
Ok(match self {
Either::Left(l) => UntaggedEither::Left(l.into_json(data).await?),
@ -95,13 +110,6 @@ where
D: Sync + Send + Clone,
E: From<Error> + 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(),

View file

@ -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<Self::DataType>) -> Result<Option<Self>, Self::Error> {
/// // Attempt to read object from local database. Return Ok(None) if not found.
/// let post: Option<DbPost> = 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<Self::DataType>) -> Result<Self, Self::Error>;
/// 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<Self::DataType>,
) -> Result<actix_web::HttpResponse, Self::Error>
where
Self::Error: From<serde_json::Error>,
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<T> ActivityHandler for Box<T>
impl<T> Activity for Box<T>
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<Self::DataType>,
) -> Result<Self, Self::Error>;
}
/// Some impls of these traits for use in tests. Dont use this from external crates.
///
/// TODO: Should be using `cfg[doctest]` but blocked by <https://github.com/rust-lang/rust/issues/67295>
#[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<T>(&self, _: Url) -> Result<Option<T>, Error> {
Ok(None)
}
pub async fn read_local_user(&self, _: &str) -> Result<DbUser, Error> {
todo!()
}
pub async fn upsert<T>(&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<DbUser>,
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<String>,
pub followers: Vec<Url>,
pub local: bool,
}
pub static DB_USER_KEYPAIR: LazyLock<Keypair> =
LazyLock::new(|| generate_actor_keypair().unwrap());
pub static DB_USER: LazyLock<DbUser> = 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<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
Ok(Some(DB_USER.clone()))
}
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
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<Self::DataType>,
) -> Result<(), Self::Error> {
verify_domains_match(json.id.inner(), expected_domain)?;
Ok(())
}
async fn from_json(
json: Self::Kind,
_data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
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<String> {
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<DbUser>,
pub object: ObjectId<DbUser>,
#[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<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
async fn receive(self, _data: &Data<Self::DataType>) -> 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<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
todo!()
}
async fn into_json(self, _: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
todo!()
}
async fn verify(
_: &Self::Kind,
_: &Url,
_: &Data<Self::DataType>,
) -> Result<(), Self::Error> {
todo!()
}
async fn from_json(_: Self::Kind, _: &Data<Self::DataType>) -> Result<Self, Self::Error> {
todo!()
}
}
}

201
src/traits/tests.rs Normal file
View file

@ -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 <https://github.com/rust-lang/rust/issues/67295>
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<T>(&self, _: Url) -> Result<Option<T>, Error> {
Ok(None)
}
pub async fn read_local_user(&self, _: &str) -> Result<DbUser, Error> {
todo!()
}
pub async fn upsert<T>(&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<DbUser>,
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<String>,
pub followers: Vec<Url>,
pub local: bool,
}
pub static DB_USER_KEYPAIR: LazyLock<Keypair> = LazyLock::new(|| generate_actor_keypair().unwrap());
pub static DB_USER: LazyLock<DbUser> = 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<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
Ok(Some(DB_USER.clone()))
}
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
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<Self::DataType>,
) -> Result<(), Self::Error> {
verify_domains_match(json.id.inner(), expected_domain)?;
Ok(())
}
async fn from_json(
json: Self::Kind,
_data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
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<String> {
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<DbUser>,
pub object: ObjectId<DbUser>,
#[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<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
async fn receive(self, _data: &Data<Self::DataType>) -> 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<Self::DataType>) -> Result<Option<Self>, Self::Error> {
todo!()
}
async fn into_json(self, _: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
todo!()
}
async fn verify(_: &Self::Kind, _: &Url, _: &Data<Self::DataType>) -> Result<(), Self::Error> {
todo!()
}
async fn from_json(_: Self::Kind, _: &Data<Self::DataType>) -> Result<Self, Self::Error> {
todo!()
}
}