adding fedration

This commit is contained in:
Namekuji 2023-05-16 14:13:57 -04:00
parent 85de24f178
commit bef53cc709
No known key found for this signature in database
GPG key ID: B541BD6E646CABC7
11 changed files with 445 additions and 0 deletions

View 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"] }

View 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)
}
}

View file

@ -0,0 +1,2 @@
mod error;
mod protocol;

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
mod context;
mod helper;
mod kind;
mod public_key;
mod verification;

View file

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

View file

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

View file

@ -0,0 +1 @@
mod federation;