378 lines
14 KiB
Rust
378 lines
14 KiB
Rust
//! Traits which need to be implemented for federated data types
|
|
|
|
use crate::{config::Data, protocol::public_key::PublicKey};
|
|
use async_trait::async_trait;
|
|
use chrono::{DateTime, Utc};
|
|
use serde::Deserialize;
|
|
use std::{fmt::Debug, ops::Deref};
|
|
use url::Url;
|
|
|
|
/// `Either` implementations for traits
|
|
pub mod either;
|
|
pub mod tests;
|
|
|
|
/// Helper for converting between database structs and federated protocol structs.
|
|
///
|
|
/// ```
|
|
/// # use activitystreams_kinds::{object::NoteType, public};
|
|
/// # use chrono::{Local, DateTime, Utc};
|
|
/// # use serde::{Deserialize, Serialize};
|
|
/// # use url::Url;
|
|
/// # 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, Object};
|
|
/// # use activitypub_federation::traits::tests::{DbConnection, DbUser};
|
|
/// #
|
|
/// /// How the post is read/written in the local database
|
|
/// #[derive(Debug)]
|
|
/// pub struct DbPost {
|
|
/// pub text: String,
|
|
/// pub ap_id: ObjectId<DbPost>,
|
|
/// pub creator: ObjectId<DbUser>,
|
|
/// pub local: bool,
|
|
/// }
|
|
///
|
|
/// /// 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<DbPost>,
|
|
/// pub(crate) attributed_to: ObjectId<DbUser>,
|
|
/// #[serde(deserialize_with = "deserialize_one_or_many")]
|
|
/// pub(crate) to: Vec<Url>,
|
|
/// content: String,
|
|
/// }
|
|
///
|
|
/// #[async_trait::async_trait]
|
|
/// impl Object for DbPost {
|
|
/// type DataType = DbConnection;
|
|
/// 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?;
|
|
/// Ok(post)
|
|
/// }
|
|
///
|
|
/// async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
|
/// // Called when a local object gets sent out over Activitypub. Simply convert it to the
|
|
/// // protocol struct
|
|
/// Ok(Note {
|
|
/// kind: Default::default(),
|
|
/// id: self.ap_id.clone().into(),
|
|
/// attributed_to: self.creator,
|
|
/// to: vec![public()],
|
|
/// content: self.text,
|
|
/// })
|
|
/// }
|
|
///
|
|
/// async fn verify(json: &Self::Kind, expected_domain: &Url, data: &Data<Self::DataType>,) -> Result<(), Self::Error> {
|
|
/// verify_domains_match(json.id.inner(), expected_domain)?;
|
|
/// // additional application specific checks
|
|
/// Ok(())
|
|
/// }
|
|
///
|
|
/// async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
|
/// // Called when a remote object gets received over Activitypub. Validate and insert it
|
|
/// // into the database.
|
|
///
|
|
/// let post = DbPost {
|
|
/// text: json.content,
|
|
/// ap_id: json.id,
|
|
/// creator: json.attributed_to,
|
|
/// local: false,
|
|
/// };
|
|
///
|
|
/// // 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)
|
|
/// }
|
|
///
|
|
/// }
|
|
#[async_trait]
|
|
pub trait Object: Sized + Debug {
|
|
/// 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 Kind;
|
|
/// 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
|
|
/// original instance. This should always be implemented for actors, because there is no active
|
|
/// update mechanism prescribed. It is possible to send `Update/Person` activities for profile
|
|
/// changes, but not all implementations do this, so `last_refreshed_at` is still necessary.
|
|
///
|
|
/// The object is refetched if `last_refreshed_at` value is more than 24 hours ago. In debug
|
|
/// mode this is reduced to 20 seconds.
|
|
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
|
None
|
|
}
|
|
|
|
/// Try to read the object with given `id` from local database.
|
|
///
|
|
/// Should return `Ok(None)` if not found.
|
|
async fn read_from_id(
|
|
object_id: Url,
|
|
data: &Data<Self::DataType>,
|
|
) -> Result<Option<Self>, 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: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
|
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
|
|
/// gets sent in an activity.
|
|
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error>;
|
|
|
|
/// 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.
|
|
///
|
|
/// It is necessary to use a separate method for this, because it might be used for activities
|
|
/// like `Delete/Note`, which shouldn't perform any database write for the inner `Note`.
|
|
async fn verify(
|
|
json: &Self::Kind,
|
|
expected_domain: &Url,
|
|
data: &Data<Self::DataType>,
|
|
) -> 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 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.
|
|
///
|
|
/// ```
|
|
/// # use activitystreams_kinds::activity::FollowType;
|
|
/// # use url::Url;
|
|
/// # use activitypub_federation::fetch::object_id::ObjectId;
|
|
/// # use activitypub_federation::config::Data;
|
|
/// # use activitypub_federation::traits::Activity;
|
|
/// # use activitypub_federation::traits::tests::{DbConnection, DbUser};
|
|
/// #[derive(serde::Deserialize)]
|
|
/// struct Follow {
|
|
/// actor: ObjectId<DbUser>,
|
|
/// object: ObjectId<DbUser>,
|
|
/// #[serde(rename = "type")]
|
|
/// kind: FollowType,
|
|
/// id: Url,
|
|
/// }
|
|
///
|
|
/// #[async_trait::async_trait]
|
|
/// impl Activity for Follow {
|
|
/// type DataType = DbConnection;
|
|
/// type Error = anyhow::Error;
|
|
///
|
|
/// fn id(&self) -> &Url {
|
|
/// &self.id
|
|
/// }
|
|
///
|
|
/// fn actor(&self) -> &Url {
|
|
/// self.actor.inner()
|
|
/// }
|
|
///
|
|
/// async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
|
/// Ok(())
|
|
/// }
|
|
///
|
|
/// async fn receive(self, data: &Data<Self::DataType>) -> 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?;
|
|
/// Ok(())
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
#[async_trait]
|
|
#[enum_delegate::register]
|
|
pub trait Activity {
|
|
/// App data type passed to handlers. Must be identical to
|
|
/// [crate::config::FederationConfigBuilder::app_data] type.
|
|
type DataType: Clone + Send + Sync;
|
|
/// Error type returned by handler methods
|
|
type Error;
|
|
|
|
/// `id` field of the activity
|
|
fn id(&self) -> &Url;
|
|
|
|
/// `actor` field of activity
|
|
fn actor(&self) -> &Url;
|
|
|
|
/// Verifies that the received activity is valid.
|
|
///
|
|
/// 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: &Data<Self::DataType>) -> 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_json` handler.
|
|
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error>;
|
|
}
|
|
|
|
/// Trait to allow retrieving common Actor data.
|
|
pub trait Actor: Object + Send + 'static {
|
|
/// The actor's public key for verifying signatures of incoming activities.
|
|
///
|
|
/// Use [generate_actor_keypair](crate::http_signatures::generate_actor_keypair) to create the
|
|
/// 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<String>;
|
|
|
|
/// 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())
|
|
}
|
|
|
|
/// The actor's shared inbox, if any
|
|
fn shared_inbox(&self) -> Option<Url> {
|
|
None
|
|
}
|
|
|
|
/// Returns shared inbox if it exists, normal inbox otherwise.
|
|
fn shared_inbox_or_inbox(&self) -> Url {
|
|
self.shared_inbox().unwrap_or_else(|| self.inbox())
|
|
}
|
|
}
|
|
|
|
/// Allow for boxing of enum variants
|
|
#[async_trait]
|
|
impl<T> Activity for Box<T>
|
|
where
|
|
T: Activity + Send + Sync,
|
|
{
|
|
type DataType = T::DataType;
|
|
type Error = T::Error;
|
|
|
|
fn id(&self) -> &Url {
|
|
self.deref().id()
|
|
}
|
|
|
|
fn actor(&self) -> &Url {
|
|
self.deref().actor()
|
|
}
|
|
|
|
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
|
self.deref().verify(data).await
|
|
}
|
|
|
|
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
|
(*self).receive(data).await
|
|
}
|
|
}
|
|
|
|
/// Trait for federating collections
|
|
#[async_trait]
|
|
pub trait Collection: 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 Kind: 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<Self::DataType>,
|
|
) -> Result<Self::Kind, Self::Error>;
|
|
|
|
/// 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(
|
|
json: &Self::Kind,
|
|
expected_domain: &Url,
|
|
data: &Data<Self::DataType>,
|
|
) -> 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_json(
|
|
json: Self::Kind,
|
|
owner: &Self::Owner,
|
|
data: &Data<Self::DataType>,
|
|
) -> Result<Self, Self::Error>;
|
|
}
|