adding fedration
This commit is contained in:
parent
85de24f178
commit
bef53cc709
11 changed files with 445 additions and 0 deletions
16
packages/backend/crates/activitypub/Cargo.toml
Normal file
16
packages/backend/crates/activitypub/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "activitypub"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.71"
|
||||||
|
async-trait = "0.1.68"
|
||||||
|
displaydoc = "0.2.4"
|
||||||
|
parse-display = "0.8.0"
|
||||||
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0.96", features = ["preserve_order"] }
|
||||||
|
thiserror = "1.0.40"
|
||||||
|
url = { version = "2.3.1", features = ["serde"] }
|
42
packages/backend/crates/activitypub/src/federation/error.rs
Normal file
42
packages/backend/crates/activitypub/src/federation/error.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
//! Error messages returned by this library
|
||||||
|
|
||||||
|
use displaydoc::Display;
|
||||||
|
|
||||||
|
/// Error messages returned by this library
|
||||||
|
#[derive(thiserror::Error, Debug, Display)]
|
||||||
|
pub(crate) enum Error {
|
||||||
|
/// Requested object was not found in local database
|
||||||
|
NotFound,
|
||||||
|
/// Request limit was reached during fetch
|
||||||
|
RequestLimit,
|
||||||
|
/// Response body limit was reached during fetch
|
||||||
|
ResponseBodyLimit,
|
||||||
|
/// Object to be fetched was deleted
|
||||||
|
ObjectDeleted,
|
||||||
|
/// Url in object was invalid
|
||||||
|
UrlVerificationError,
|
||||||
|
/// Incoming activity has invalid digest for body
|
||||||
|
ActivityBodyDigestInvalid,
|
||||||
|
/// Incoming activity has invalid signature
|
||||||
|
ActivitySignatureInvalid,
|
||||||
|
/// Failed to resolve actor via webfinger
|
||||||
|
WebfingerResolveFailed,
|
||||||
|
/// Other errors which are not explicitly handled
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub(crate) fn other<T>(error: T) -> Self
|
||||||
|
where
|
||||||
|
T: Into<anyhow::Error>,
|
||||||
|
{
|
||||||
|
Error::Other(error.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Error {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
std::mem::discriminant(self) == std::mem::discriminant(other)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod error;
|
||||||
|
mod protocol;
|
|
@ -0,0 +1,97 @@
|
||||||
|
// GNU Affero General Public License v3.0
|
||||||
|
// https://github.com/LemmyNet/activitypub-federation-rust
|
||||||
|
|
||||||
|
//! Wrapper for federated structs which handles `@context` field.
|
||||||
|
//!
|
||||||
|
//! This wrapper can be used when sending Activitypub data, to automatically add `@context`. It
|
||||||
|
//! avoids having to repeat the `@context` property on every struct, and getting multiple contexts
|
||||||
|
//! in nested structs.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use activitypub_federation::protocol::context::WithContext;
|
||||||
|
//! #[derive(serde::Serialize)]
|
||||||
|
//! struct Note {
|
||||||
|
//! content: String
|
||||||
|
//! }
|
||||||
|
//! let note = Note {
|
||||||
|
//! content: "Hello world".to_string()
|
||||||
|
//! };
|
||||||
|
//! let note_with_context = WithContext::new_default(note);
|
||||||
|
//! let serialized = serde_json::to_string(¬e_with_context)?;
|
||||||
|
//! assert_eq!(serialized, r#"{"@context":["https://www.w3.org/ns/activitystreams"],"content":"Hello world"}"#);
|
||||||
|
//! Ok::<(), serde_json::error::Error>(())
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::federation::protocol::helper::deserialize_one_or_many;
|
||||||
|
use crate::{config::Data, traits::ActivityHandler};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Default context used in Activitypub
|
||||||
|
const DEFAULT_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
|
||||||
|
|
||||||
|
/// Wrapper for federated structs which handles `@context` field.
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct WithContext<T> {
|
||||||
|
#[serde(rename = "@context")]
|
||||||
|
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||||
|
context: Vec<Value>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> WithContext<T> {
|
||||||
|
/// Create a new wrapper with the default Activitypub context.
|
||||||
|
pub fn new_default(inner: T) -> WithContext<T> {
|
||||||
|
let context = vec![Value::String(DEFAULT_CONTEXT.to_string())];
|
||||||
|
WithContext::new(inner, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new wrapper with custom context. Use this in case you are implementing extensions.
|
||||||
|
pub fn new(inner: T, context: Vec<Value>) -> WithContext<T> {
|
||||||
|
WithContext { context, inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner `T` object which this `WithContext` object is wrapping
|
||||||
|
pub fn inner(&self) -> &T {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T> ActivityHandler for WithContext<T>
|
||||||
|
where
|
||||||
|
T: ActivityHandler + Send + Sync,
|
||||||
|
{
|
||||||
|
type DataType = <T as ActivityHandler>::DataType;
|
||||||
|
type Error = <T as ActivityHandler>::Error;
|
||||||
|
|
||||||
|
fn id(&self) -> &Url {
|
||||||
|
self.inner.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actor(&self) -> &Url {
|
||||||
|
self.inner.actor()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||||
|
self.inner.verify(data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||||
|
self.inner.receive(data).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for WithContext<T>
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
context: self.context.clone(),
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
// GNU Affero General Public License v3.0
|
||||||
|
// https://github.com/LemmyNet/activitypub-federation-rust
|
||||||
|
|
||||||
|
//! Serde deserialization functions which help to receive differently shaped data
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
/// Deserialize JSON single value or array into Vec.
|
||||||
|
///
|
||||||
|
/// Useful if your application can handle multiple values for a field, but another federated
|
||||||
|
/// platform only sends a single one.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use activitypub_federation::protocol::helpers::deserialize_one_or_many;
|
||||||
|
/// # use url::Url;
|
||||||
|
/// #[derive(serde::Deserialize)]
|
||||||
|
/// struct Note {
|
||||||
|
/// #[serde(deserialize_with = "deserialize_one_or_many")]
|
||||||
|
/// to: Vec<Url>
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let single: Note = serde_json::from_str(r#"{"to": "https://example.com/u/alice" }"#)?;
|
||||||
|
/// assert_eq!(single.to.len(), 1);
|
||||||
|
///
|
||||||
|
/// let multiple: Note = serde_json::from_str(
|
||||||
|
/// r#"{"to": [
|
||||||
|
/// "https://example.com/u/alice",
|
||||||
|
/// "https://lemmy.ml/u/bob"
|
||||||
|
/// ]}"#)?;
|
||||||
|
/// assert_eq!(multiple.to.len(), 2);
|
||||||
|
/// Ok::<(), anyhow::Error>(())
|
||||||
|
pub(crate) fn deserialize_one_or_many<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
|
||||||
|
where
|
||||||
|
T: Deserialize<'de>,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum OneOrMany<T> {
|
||||||
|
One(T),
|
||||||
|
Many(Vec<T>),
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: OneOrMany<T> = Deserialize::deserialize(deserializer)?;
|
||||||
|
Ok(match result {
|
||||||
|
OneOrMany::Many(list) => list,
|
||||||
|
OneOrMany::One(value) => vec![value],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize JSON single value or single element array into single value.
|
||||||
|
///
|
||||||
|
/// Useful if your application can only handle a single value for a field, but another federated
|
||||||
|
/// platform sends single value wrapped in array. Fails if array contains multiple items.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use activitypub_federation::protocol::helpers::deserialize_one;
|
||||||
|
/// # use url::Url;
|
||||||
|
/// #[derive(serde::Deserialize)]
|
||||||
|
/// struct Note {
|
||||||
|
/// #[serde(deserialize_with = "deserialize_one")]
|
||||||
|
/// to: Url
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let note = serde_json::from_str::<Note>(r#"{"to": ["https://example.com/u/alice"] }"#);
|
||||||
|
/// assert!(note.is_ok());
|
||||||
|
pub(crate) fn deserialize_one<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
T: Deserialize<'de>,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum MaybeArray<T> {
|
||||||
|
Simple(T),
|
||||||
|
Array([T; 1]),
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: MaybeArray<T> = Deserialize::deserialize(deserializer)?;
|
||||||
|
Ok(match result {
|
||||||
|
MaybeArray::Simple(value) => value,
|
||||||
|
MaybeArray::Array([value]) => value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to deserialize item, in case of error falls back to the type's default value.
|
||||||
|
///
|
||||||
|
/// Useful for optional fields which are sent with a different type from another platform,
|
||||||
|
/// eg object instead of array. Should always be used together with `#[serde(default)]`, so
|
||||||
|
/// that a mssing value doesn't cause an error.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use activitypub_federation::protocol::helpers::deserialize_skip_error;
|
||||||
|
/// # use url::Url;
|
||||||
|
/// #[derive(serde::Deserialize)]
|
||||||
|
/// struct Note {
|
||||||
|
/// content: String,
|
||||||
|
/// #[serde(deserialize_with = "deserialize_skip_error", default)]
|
||||||
|
/// source: Option<String>
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let note = serde_json::from_str::<Note>(
|
||||||
|
/// r#"{
|
||||||
|
/// "content": "How are you?",
|
||||||
|
/// "source": {
|
||||||
|
/// "content": "How are you?",
|
||||||
|
/// "mediaType": "text/markdown"
|
||||||
|
/// }
|
||||||
|
/// }"#);
|
||||||
|
/// assert_eq!(note.unwrap().source, None);
|
||||||
|
/// # Ok::<(), anyhow::Error>(())
|
||||||
|
pub(crate) fn deserialize_skip_error<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
T: Deserialize<'de> + Default,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
|
let inner = T::deserialize(value).unwrap_or_default();
|
||||||
|
Ok(inner)
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
//! Types of Activity, Actor, Collection, Link, and Object
|
||||||
|
|
||||||
|
use parse_display::{Display, FromStr};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub(crate) enum ActivityType {
|
||||||
|
Activity,
|
||||||
|
Accept,
|
||||||
|
Add,
|
||||||
|
Announce,
|
||||||
|
Arrive,
|
||||||
|
Block,
|
||||||
|
#[default]
|
||||||
|
Create,
|
||||||
|
Delete,
|
||||||
|
Dislike,
|
||||||
|
Flag,
|
||||||
|
Follow,
|
||||||
|
Ignore,
|
||||||
|
Invite,
|
||||||
|
Join,
|
||||||
|
Leave,
|
||||||
|
Like,
|
||||||
|
Listen,
|
||||||
|
Move,
|
||||||
|
Offer,
|
||||||
|
Question,
|
||||||
|
Read,
|
||||||
|
Reject,
|
||||||
|
Remove,
|
||||||
|
TentativeAccept,
|
||||||
|
TentativeReject,
|
||||||
|
Travel,
|
||||||
|
Undo,
|
||||||
|
Update,
|
||||||
|
View,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub(crate) enum ActorType {
|
||||||
|
Application,
|
||||||
|
Group,
|
||||||
|
Organization,
|
||||||
|
#[default]
|
||||||
|
Person,
|
||||||
|
Service,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub(crate) enum CollectionType {
|
||||||
|
Collection,
|
||||||
|
#[default]
|
||||||
|
OrderedCollection,
|
||||||
|
CollectionPage,
|
||||||
|
OrderedCollectionPage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub(crate) enum LinkType {
|
||||||
|
#[default]
|
||||||
|
Link,
|
||||||
|
Mention,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub(crate) enum ObjectType {
|
||||||
|
Object,
|
||||||
|
Article,
|
||||||
|
Audio,
|
||||||
|
Document,
|
||||||
|
Event,
|
||||||
|
Image,
|
||||||
|
#[default]
|
||||||
|
Note,
|
||||||
|
Page,
|
||||||
|
Place,
|
||||||
|
Profile,
|
||||||
|
Relationship,
|
||||||
|
Tombstone,
|
||||||
|
Video,
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
mod context;
|
||||||
|
mod helper;
|
||||||
|
mod kind;
|
||||||
|
mod public_key;
|
||||||
|
mod verification;
|
|
@ -0,0 +1,39 @@
|
||||||
|
// GNU Affero General Public License v3.0
|
||||||
|
// https://github.com/LemmyNet/activitypub-federation-rust
|
||||||
|
|
||||||
|
//! Struct which is used to federate actor key for HTTP signatures
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Public key of actors which is used for HTTP signatures.
|
||||||
|
///
|
||||||
|
/// This needs to be federated in the `public_key` field of all actors.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct PublicKey {
|
||||||
|
/// Id of this private key.
|
||||||
|
pub id: String,
|
||||||
|
/// ID of the actor that this public key belongs to
|
||||||
|
pub owner: Url,
|
||||||
|
/// The actual public key in PEM format
|
||||||
|
pub public_key_pem: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(crate) fn new(owner: Url, public_key_pem: String) -> Self {
|
||||||
|
let id = main_key_id(&owner);
|
||||||
|
PublicKey {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
public_key_pem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn main_key_id(owner: &Url) -> String {
|
||||||
|
format!("{}#main-key", &owner)
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
// GNU Affero General Public License v3.0
|
||||||
|
// https://github.com/LemmyNet/activitypub-federation-rust
|
||||||
|
|
||||||
|
//! Verify that received data is valid
|
||||||
|
|
||||||
|
use crate::federation::error::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Check that both urls have the same domain. If not, return UrlVerificationError.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use url::Url;
|
||||||
|
/// # use activitypub_federation::protocol::verification::verify_domains_match;
|
||||||
|
/// let a = Url::parse("https://example.com/abc")?;
|
||||||
|
/// let b = Url::parse("https://sample.net/abc")?;
|
||||||
|
/// assert!(verify_domains_match(&a, &b).is_err());
|
||||||
|
/// # Ok::<(), url::ParseError>(())
|
||||||
|
/// ```
|
||||||
|
pub fn verify_domains_match(a: &Url, b: &Url) -> Result<(), Error> {
|
||||||
|
if a.domain() != b.domain() {
|
||||||
|
return Err(Error::UrlVerificationError);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that both urls are identical. If not, return UrlVerificationError.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use url::Url;
|
||||||
|
/// # use activitypub_federation::protocol::verification::verify_urls_match;
|
||||||
|
/// let a = Url::parse("https://example.com/abc")?;
|
||||||
|
/// let b = Url::parse("https://example.com/123")?;
|
||||||
|
/// assert!(verify_urls_match(&a, &b).is_err());
|
||||||
|
/// # Ok::<(), url::ParseError>(())
|
||||||
|
/// ```
|
||||||
|
pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), Error> {
|
||||||
|
if a != b {
|
||||||
|
return Err(Error::UrlVerificationError);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
1
packages/backend/crates/activitypub/src/lib.rs
Normal file
1
packages/backend/crates/activitypub/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mod federation;
|
Loading…
Reference in a new issue