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