diff --git a/Cargo.lock b/Cargo.lock index e163ec3..99081a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1640,6 +1640,20 @@ dependencies = [ "url", ] +[[package]] +name = "magnetar_activity_streams" +version = "0.3.0-alpha" +dependencies = [ + "async-trait", + "chrono", + "either", + "serde", + "serde_json", + "thiserror", + "tokio", + "url", +] + [[package]] name = "magnetar_calckey_fe" version = "0.3.0-alpha" diff --git a/Cargo.toml b/Cargo.toml index 7cb9a8a..8d643d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ license = "AGPL-3.0-only" [workspace] members = [ ".", + "ext_activity_streams", "ext_federation", "ext_nodeinfo", "ext_webfinger", diff --git a/ext_activity_streams/Cargo.toml b/ext_activity_streams/Cargo.toml index 1d71e4c..6d6312e 100644 --- a/ext_activity_streams/Cargo.toml +++ b/ext_activity_streams/Cargo.toml @@ -13,6 +13,7 @@ async-trait = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +either = { workspace = true } chrono = { workspace = true, features = ["serde"] } thiserror = { workspace = true } url = { workspace = true, features = ["serde"] } diff --git a/ext_activity_streams/src/lib.rs b/ext_activity_streams/src/lib.rs index 8b13789..fce2f60 100644 --- a/ext_activity_streams/src/lib.rs +++ b/ext_activity_streams/src/lib.rs @@ -1 +1,88 @@ +use std::borrow::Cow; +use std::fmt::Debug; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::link::Link; + +pub mod link; +pub mod object; +pub mod recipe; + +pub trait ObjectTyped: Clone + Debug + Sized + 'static { + fn get_type() -> &'static str; +} + +#[macro_export] +macro_rules! def_ld { + ($obj_type: expr, $x: ty) => { + impl $crate::ObjectTyped for $x { + fn get_type() -> &'static str { + $obj_type + } + } + }; +} + +#[macro_export] +macro_rules! ld_union { + ($z: ident, $($item_name: ident as $item_type: ty),+) => { + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct $z { + $( + #[serde(flatten)] + $item_name: Box<$item_type>, + )+ + } + }; +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OneOrMore { + One(T), + Many(Vec), +} + +impl OneOrMore { + pub fn into_vec(self) -> Vec { + match self { + OneOrMore::One(x) => vec![x], + OneOrMore::Many(x) => x, + } + } + + pub fn iter(&self) -> std::slice::Iter<'_, T> { + match self { + OneOrMore::One(x) => std::slice::from_ref(x).iter(), + OneOrMore::Many(x) => x.iter(), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Base { + #[serde(rename = "@type")] + as_type: Option>, + #[serde(rename = "@id")] + as_id: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum LinkOrUrl<'a> { + Url(Url), + Link(Box>), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[repr(transparent)] +pub struct Id(pub String); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ObjectSlot<'a, T: ObjectTyped> { + Id(Id), + Value(Cow<'a, T>), +} diff --git a/ext_activity_streams/src/link/link_types.rs b/ext_activity_streams/src/link/link_types.rs new file mode 100644 index 0000000..2c2a419 --- /dev/null +++ b/ext_activity_streams/src/link/link_types.rs @@ -0,0 +1,7 @@ +use crate::def_ld; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct LinkMention {} + +def_ld!("https://www.w3.org/ns/activitystreams#Mention", LinkMention); diff --git a/ext_activity_streams/src/link/mod.rs b/ext_activity_streams/src/link/mod.rs new file mode 100644 index 0000000..2658386 --- /dev/null +++ b/ext_activity_streams/src/link/mod.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; + +use crate::{def_ld, ObjectSlot}; +use crate::object::ObjectLinkUnion; +use crate::OneOrMore; + +pub mod link_types; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct Link<'a> { + #[serde(rename = "https://www.w3.org/ns/activitystreams#href")] + #[serde(skip_serializing_if = "Option::is_none")] + pub href: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#hreflang")] + #[serde(skip_serializing_if = "Option::is_none")] + pub href_lang: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#mediaType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub media_type: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#rel")] + #[serde(skip_serializing_if = "Option::is_none")] + pub rel: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#width")] + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#height")] + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#preview")] + #[serde(skip_serializing_if = "Option::is_none")] + pub preview: Option>>, +} + +def_ld!("https://www.w3.org/ns/activitystreams#Link", Link<'_>); diff --git a/ext_activity_streams/src/object/activity.rs b/ext_activity_streams/src/object/activity.rs new file mode 100644 index 0000000..33b3b5a --- /dev/null +++ b/ext_activity_streams/src/object/activity.rs @@ -0,0 +1,211 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::{def_ld, ObjectSlot, ObjectTyped, OneOrMore}; +use crate::object::ObjectLinkUnion; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct Activity<'a> { + #[serde(rename = "https://www.w3.org/ns/activitystreams#actor")] + #[serde(skip_serializing_if = "Option::is_none")] + pub actor: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#object")] + #[serde(skip_serializing_if = "Option::is_none")] + pub object: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#target")] + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#result")] + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#origin")] + #[serde(skip_serializing_if = "Option::is_none")] + pub origin: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#instrument")] + #[serde(skip_serializing_if = "Option::is_none")] + pub instrument: Option>>, +} + +def_ld!("https://www.w3.org/ns/activitystreams#Activity", Activity<'_>, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct IntransitiveActivity {} + +def_ld!( + "https://www.w3.org/ns/activitystreams#IntransitiveActivity", + IntransitiveActivity, + base as Activity +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityAccept {} + +def_ld!("https://www.w3.org/ns/activitystreams#Accept", ActivityAccept, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityTentativeAccept {} + +def_ld!( + "https://www.w3.org/ns/activitystreams#TentativeAccept", + ActivityTentativeAccept, + base as ActivityAccept +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityAdd; + +def_ld!("https://www.w3.org/ns/activitystreams#Add", ActivityAdd, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityArrive; + +def_ld!("https://www.w3.org/ns/activitystreams#Arrive", ActivityArrive, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityCreate; + +def_ld!("https://www.w3.org/ns/activitystreams#Create", ActivityCreate, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityDelete {} + +def_ld!("https://www.w3.org/ns/activitystreams#Delete", ActivityDelete, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityFollow {} + +def_ld!("https://www.w3.org/ns/activitystreams#Follow", ActivityFollow, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityIgnore {} + +def_ld!("https://www.w3.org/ns/activitystreams#Ignore", ActivityIgnore, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityJoin {} + +def_ld!("https://www.w3.org/ns/activitystreams#Join", ActivityJoin, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityLeave {} + +def_ld!("https://www.w3.org/ns/activitystreams#Leave", ActivityLeave, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityLike {} + +def_ld!("https://www.w3.org/ns/activitystreams#Like", ActivityLike, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityOffer {} + +def_ld!("https://www.w3.org/ns/activitystreams#Offer", ActivityOffer, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityInvite {} + +def_ld!("https://www.w3.org/ns/activitystreams#Invite", ActivityInvite, base as ActivityOffer); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityReject {} + +def_ld!("https://www.w3.org/ns/activitystreams#Reject", ActivityReject, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityTentativeReject {} + +def_ld!( + "https://www.w3.org/ns/activitystreams#TentativeReject", + ActivityTentativeReject, + base as ActivityReject +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityRemove {} + +def_ld!("https://www.w3.org/ns/activitystreams#Remove", ActivityRemove, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityUndo {} + +def_ld!("https://www.w3.org/ns/activitystreams#Undo", ActivityUndo, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityUpdate {} + +def_ld!("https://www.w3.org/ns/activitystreams#Update", ActivityUpdate, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityView {} + +def_ld!("https://www.w3.org/ns/activitystreams#View", ActivityView, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityListen {} + +def_ld!("https://www.w3.org/ns/activitystreams#Listen", ActivityListen, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityRead {} + +def_ld!("https://www.w3.org/ns/activitystreams#Read", ActivityRead, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityMove {} + +def_ld!("https://www.w3.org/ns/activitystreams#Move", ActivityMove, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityTravel {} + +def_ld!("https://www.w3.org/ns/activitystreams#Travel", ActivityTravel, base as IntransitiveActivity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityAnnounce {} + +def_ld!("https://www.w3.org/ns/activitystreams#Announce", ActivityAnnounce, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityBlock {} + +def_ld!("https://www.w3.org/ns/activitystreams#Block", ActivityBlock, base as ActivityIgnore); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityFlag {} + +def_ld!("https://www.w3.org/ns/activitystreams#Flag", ActivityFlag, base as Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityDislike {} + +def_ld!("https://www.w3.org/ns/activitystreams#Dislike", ActivityDislike, base as Activity); + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ActivityClosedStatus<'a> { + Date(DateTime), + Bool(bool), + Other(ObjectSlot<'a, ObjectLinkUnion>), +} + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityQuestion<'a> { + #[serde(rename = "https://www.w3.org/ns/activitystreams#oneOf")] + #[serde(skip_serializing_if = "Option::is_none")] + pub one_of: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#anyOf")] + #[serde(skip_serializing_if = "Option::is_none")] + pub any_of: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#closed")] + #[serde(skip_serializing_if = "Option::is_none")] + pub closed: Option>>, +} + +def_ld!("https://www.w3.org/ns/activitystreams#Question", ActivityQuestion<'_>, base as IntransitiveActivity); diff --git a/ext_activity_streams/src/object/actor.rs b/ext_activity_streams/src/object/actor.rs new file mode 100644 index 0000000..f9f4fec --- /dev/null +++ b/ext_activity_streams/src/object/actor.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{def_ld, ObjectSlot}; +use crate::object::{Collection, ObjectLinkUnion, OrderedCollection}; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorActivityPubProps<'a> { + #[serde(rename = "https://www.w3.org/ns/activitystreams#inbox")] + #[serde(skip_serializing_if = "Option::is_none")] + pub inbox: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#outbox")] + #[serde(skip_serializing_if = "Option::is_none")] + pub outbox: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#following")] + #[serde(skip_serializing_if = "Option::is_none")] + pub following: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#followers")] + #[serde(skip_serializing_if = "Option::is_none")] + pub followers: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#liked")] + #[serde(skip_serializing_if = "Option::is_none")] + pub liked: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#shares")] + #[serde(skip_serializing_if = "Option::is_none")] + pub shares: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#preferredUsername")] + #[serde(skip_serializing_if = "Option::is_none")] + pub preferred_username: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#endpoints")] + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoints: Option, +} + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorApplication {} + +def_ld!("https://www.w3.org/ns/activitystreams#Application", ActorApplication, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorGroup {} + +def_ld!("https://www.w3.org/ns/activitystreams#Group", ActorGroup, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorOrganization {} + +def_ld!("https://www.w3.org/ns/activitystreams#Organization", ActorOrganization, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorPerson {} + +def_ld!("https://www.w3.org/ns/activitystreams#Person", ActorPerson, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorService {} + +def_ld!("https://www.w3.org/ns/activitystreams#Service", ActorService, base as Object); diff --git a/ext_activity_streams/src/object/mod.rs b/ext_activity_streams/src/object/mod.rs new file mode 100644 index 0000000..688b222 --- /dev/null +++ b/ext_activity_streams/src/object/mod.rs @@ -0,0 +1,226 @@ +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::{ld_union, LinkOrUrl, ObjectSlot, OneOrMore}; +use crate::def_ld; +use crate::link::Link; +use crate::object::object_types::ObjectImageLinkUnion; + +pub mod activity; +pub mod actor; +pub mod object_types; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct Object<'a> { + #[serde(rename = "https://www.w3.org/ns/activitystreams#attachment")] + #[serde(skip_serializing_if = "Option::is_none")] + pub attachment: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#attributedTo")] + #[serde(skip_serializing_if = "Option::is_none")] + pub attributed_to: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#audience")] + #[serde(skip_serializing_if = "Option::is_none")] + pub audience: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#content")] + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#contentMap")] + #[serde(skip_serializing_if = "Option::is_none")] + pub content_map: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#context")] + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#name")] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#nameMap")] + #[serde(skip_serializing_if = "Option::is_none")] + pub name_map: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#endTime")] + #[serde(skip_serializing_if = "Option::is_none")] + pub end_time: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#generator")] + #[serde(skip_serializing_if = "Option::is_none")] + pub generator: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#icon")] + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#image")] + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#inReplyTo")] + #[serde(skip_serializing_if = "Option::is_none")] + pub in_reply_to: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#location")] + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#preview")] + #[serde(skip_serializing_if = "Option::is_none")] + pub preview: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#published")] + #[serde(skip_serializing_if = "Option::is_none")] + pub published: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#replies")] + #[serde(skip_serializing_if = "Option::is_none")] + pub replies: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#startTime")] + #[serde(skip_serializing_if = "Option::is_none")] + pub start_time: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#summary")] + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#summaryMap")] + #[serde(skip_serializing_if = "Option::is_none")] + pub summary_map: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#tag")] + #[serde(skip_serializing_if = "Option::is_none")] + pub tag: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#updated")] + #[serde(skip_serializing_if = "Option::is_none")] + pub updated: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#url")] + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#to")] + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#bto")] + #[serde(skip_serializing_if = "Option::is_none")] + pub bto: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#cc")] + #[serde(skip_serializing_if = "Option::is_none")] + pub cc: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#bcc")] + #[serde(skip_serializing_if = "Option::is_none")] + pub bcc: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#mediaType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub media_type: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#duration")] + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + // ActivityPub + #[serde(rename = "https://www.w3.org/ns/activitystreams#source")] + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option>>, +} + +def_ld!("https://www.w3.org/ns/activitystreams#Object", Object<'_>); +ld_union!( + ObjectLinkUnion, + object_props as Object, + link_props as Link +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct Collection { + #[serde(rename = "https://www.w3.org/ns/activitystreams#totalItems")] + #[serde(skip_serializing_if = "Option::is_none")] + pub total_items: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#current")] + #[serde(skip_serializing_if = "Option::is_none")] + pub current: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#first")] + #[serde(skip_serializing_if = "Option::is_none")] + pub first: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#last")] + #[serde(skip_serializing_if = "Option::is_none")] + pub last: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#items")] + #[serde(skip_serializing_if = "Option::is_none")] + pub items: Option>, +} + +def_ld!( + "https://www.w3.org/ns/activitystreams#Collection", + Collection +); +ld_union!( + CollectionLinkUnion, + collection_props as Collection, + link_props as Link +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct OrderedCollection { + #[serde(rename = "https://www.w3.org/ns/activitystreams#orderedItems")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ordered_items: Option>, +} + +def_ld!( + "https://www.w3.org/ns/activitystreams#OrderedCollection", + OrderedCollection +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct CollectionPage { + #[serde(rename = "https://www.w3.org/ns/activitystreams#next")] + #[serde(skip_serializing_if = "Option::is_none")] + pub next: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#prev")] + #[serde(skip_serializing_if = "Option::is_none")] + pub prev: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#partOf")] + #[serde(skip_serializing_if = "Option::is_none")] + pub part_of: Option, +} + +def_ld!( + "https://www.w3.org/ns/activitystreams#CollectionPage", + CollectionPage +); +ld_union!( + CollectionPageLinkUnion, + page_props as CollectionPage, + link_props as Link +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct OrderedCollectionPage { + #[serde(rename = "https://www.w3.org/ns/activitystreams#startIndex")] + #[serde(skip_serializing_if = "Option::is_none")] + pub start_index: Option, +} + +def_ld!( + "https://www.w3.org/ns/activitystreams#OrderedCollectionPage", + OrderedCollectionPage +); diff --git a/ext_activity_streams/src/object/object_types.rs b/ext_activity_streams/src/object/object_types.rs new file mode 100644 index 0000000..2e64ee8 --- /dev/null +++ b/ext_activity_streams/src/object/object_types.rs @@ -0,0 +1,119 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::{def_ld, ld_union, ObjectSlot, ObjectTyped, OneOrMore}; +use crate::link::Link; +use crate::object::{Object, ObjectLinkUnion}; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectRelationship<'a> { + #[serde(rename = "https://www.w3.org/ns/activitystreams#object")] + #[serde(skip_serializing_if = "Option::is_none")] + pub object: Option>>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#subject")] + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#relationship")] + #[serde(skip_serializing_if = "Option::is_none")] + pub relationship: Option>>>, +} + +def_ld!("https://www.w3.org/ns/activitystreams#Relationship", ObjectRelationship, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectArticle {} + +def_ld!("https://www.w3.org/ns/activitystreams#Article", ObjectArticle, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectDocument {} + +def_ld!("https://www.w3.org/ns/activitystreams#Document", ObjectDocument, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectAudio {} + +def_ld!("https://www.w3.org/ns/activitystreams#Audio", ObjectAudio, base as ObjectDocument); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectImage {} + +def_ld!("https://www.w3.org/ns/activitystreams#Image", ObjectImage, base as ObjectDocument); +ld_union!( + ObjectImageLinkUnion, + image_props as ObjectImage, + link_props as Link +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectVideo {} + +def_ld!("https://www.w3.org/ns/activitystreams#Video", ObjectVideo, base as ObjectDocument); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectNote {} + +def_ld!("https://www.w3.org/ns/activitystreams#Note", ObjectNote, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectPage {} + +def_ld!("https://www.w3.org/ns/activitystreams#Page", ObjectPage, base as ObjectDocument); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectEvent {} + +def_ld!("https://www.w3.org/ns/activitystreams#Event", ObjectEvent, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectPlace { + #[serde(rename = "https://www.w3.org/ns/activitystreams#accuracy")] + #[serde(skip_serializing_if = "Option::is_none")] + pub accuracy: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#altitude")] + #[serde(skip_serializing_if = "Option::is_none")] + pub altitude: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#latitude")] + #[serde(skip_serializing_if = "Option::is_none")] + pub latitude: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#longitude")] + #[serde(skip_serializing_if = "Option::is_none")] + pub longitude: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#radius")] + #[serde(skip_serializing_if = "Option::is_none")] + pub radius: Option, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#units")] + #[serde(skip_serializing_if = "Option::is_none")] + pub units: Option, +} + +def_ld!("https://www.w3.org/ns/activitystreams#Place", ObjectPlace, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectProfile<'a> { + #[serde(rename = "https://www.w3.org/ns/activitystreams#describes")] + #[serde(skip_serializing_if = "Option::is_none")] + pub describes: Option>>, +} + +def_ld!("https://www.w3.org/ns/activitystreams#Profile", ObjectProfile, base as Object); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectTombstone { + #[serde(rename = "https://www.w3.org/ns/activitystreams#formerType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub former_type: Option>, + + #[serde(rename = "https://www.w3.org/ns/activitystreams#deleted")] + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted: Option>, +} + +def_ld!("https://www.w3.org/ns/activitystreams#Tombstone", ObjectTombstone, base as Object); diff --git a/ext_activity_streams/src/recipe/block.rs b/ext_activity_streams/src/recipe/block.rs new file mode 100644 index 0000000..1193c8b --- /dev/null +++ b/ext_activity_streams/src/recipe/block.rs @@ -0,0 +1,87 @@ +use crate::object::activity::ActivityBlock; + +use crate::object::Object; +use crate::recipe::RecipeError; +use crate::{extract_field, Id, ObjectRaw}; +use serde::Deserialize; + +#[derive(Clone, Debug)] +pub struct Block { + pub id: Id, + pub actor: Id, + pub object: Id, + pub to: Id, +} + +impl Block { + fn parse(object: &ObjectRaw>) -> Result { + let json = object.as_json(); + let data: &Object = (*object.data).as_ref(); + + let ap_type = extract_field!(data, as_type, unwrap); + if ap_type.iter().all(|t| t != "Block") { + return Err(RecipeError::UnexpectedType(format!("{:?}", ap_type))); + } + + let block = ActivityBlock::deserialize(json)?; + Ok(Block { + id: Id(extract_field!(block, as_id, unwrap | owned)), + actor: Id(extract_field!( + block, + actor, + unwrap | single | as_id | owned + )), + object: Id(extract_field!( + block, + object, + unwrap | single | as_id | owned + )), + to: Id(extract_field!(block, to, unwrap | single | as_id | owned)), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::object::activity::{Activity, ActivityBlock, ActivityIgnore}; + use crate::object::Object; + use crate::ObjectRaw; + use serde::Deserialize; + use serde_json::json; + + #[test] + fn parse_json() { + let json = json!({ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://a.example.com/blocks/1", + "type": "Block", + "actor": "https://a.example.com/users/1", + "object": "https://b.example.com/users/2", + "to": "https://b.example.com/users/2", + }); + + let object = ObjectRaw::::deserialize(json).unwrap(); + + let block = super::Block::parse(&object).unwrap(); + + println!("{:#?}", block); + } + + #[test] + fn parse_json_with_multiple_types() { + let json = json!({ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://a.example.com/blocks/1", + "type": ["Block", "https://example.com/SomeOtherType"], + "actor": "https://a.example.com/users/1", + "object": "https://b.example.com/users/2", + "to": "https://b.example.com/users/2", + }); + + let object = ObjectRaw::::deserialize(json).unwrap(); + + let block = super::Block::parse(&object).unwrap(); + + println!("{:#?}", block); + } +} diff --git a/ext_activity_streams/src/recipe/mod.rs b/ext_activity_streams/src/recipe/mod.rs new file mode 100644 index 0000000..0d62ab0 --- /dev/null +++ b/ext_activity_streams/src/recipe/mod.rs @@ -0,0 +1,90 @@ +use thiserror::Error; + +use crate::OneOrMore; + +#[macro_use] +pub mod block; + +#[derive(Debug, Error)] +pub enum RecipeError { + #[error("Serde error")] + DeserializationError(#[from] serde_json::Error), + #[error("Unexpected type: {0}")] + UnexpectedType(String), + #[error("Missing field: {0}")] + MissingField(&'static str), + #[error("Single item expected: {0}")] + ExpectedSingleField(&'static str), + #[error("Missing ID")] + MissingId, +} + +#[derive(Debug, Error)] +pub struct FieldPipeline { + value: T, + field_name: &'static str, +} + +#[macro_export] +macro_rules! extract_field { + ($obj: expr, $field: ident, $($step:ident)|+ $(|)?) => {{ + let pipeline = $crate::recipe::FieldPipeline { + value: $obj.$field.as_ref(), + field_name: stringify!($field), + }; + + $( + let pipeline = pipeline.$step()?; + )+ + + pipeline.into_inner() + }}; +} + +impl FieldPipeline> { + fn unwrap(self) -> Result, RecipeError> { + Ok(FieldPipeline { + value: self + .value + .ok_or(RecipeError::MissingField(self.field_name))?, + field_name: self.field_name, + }) + } +} + +impl FieldPipeline<&T> { + fn as_id(&self) -> Result, RecipeError> { + Ok(FieldPipeline { + value: self.value.unresolve().ok_or(RecipeError::MissingId)?, + field_name: self.field_name, + }) + } +} + +impl<'a, T> FieldPipeline<&'a OneOrMore> { + fn single(self) -> Result, RecipeError> { + Ok(FieldPipeline { + value: match self.value { + OneOrMore::One(x) => Ok(x), + OneOrMore::Many(vec) if vec.len() == 1 => Ok(vec.first().unwrap()), + OneOrMore::Many(_) => Err(RecipeError::ExpectedSingleField(self.field_name)), + }?, + field_name: self.field_name, + }) + } +} + +impl FieldPipeline<&T> { + fn owned(&self) -> Result, RecipeError> { + Ok(FieldPipeline { + value: self.value.to_owned(), + field_name: self.field_name, + }) + } +} + +impl FieldPipeline { + fn into_inner(self) -> T { + self.value + } +} diff --git a/src/client/dereferencing_client.rs b/src/client/dereferencing_client.rs new file mode 100644 index 0000000..c09508d --- /dev/null +++ b/src/client/dereferencing_client.rs @@ -0,0 +1,94 @@ +use futures_util::stream::StreamExt; +use magnetar_activity_pub::Resolver; +use magnetar_core::web_model::content_type::ContentActivityStreams; +use reqwest::Client; +use serde::Deserialize; +use serde_json::Value; +use thiserror::Error; +use url::Url; + +#[derive(Debug, Clone)] +pub struct DereferencingClient { + pub client: Client, + pub body_limit: usize, +} + +#[derive(Debug, Error)] +pub enum DereferencingClientBuilderError { + #[error("Reqwest error: {0}")] + ReqwestError(#[from] reqwest::Error), +} + +#[derive(Debug, Error)] +pub enum DereferencingClientError { + #[error("Reqwest error: {0}")] + ReqwestError(#[from] reqwest::Error), + #[error("JSON error: {0}")] + JsonError(#[from] serde_json::Error), + #[error("Body limit exceeded error")] + BodyLimitExceededError, + #[error("Invalid URL: {0}")] + InvalidUrl(#[from] url::ParseError), + #[error("Client error: {0}")] + Other(String), +} + +impl DereferencingClient { + pub fn new( + force_https: bool, + body_limit: usize, + ) -> Result { + let client = Client::builder().https_only(force_https).build()?; + + Ok(DereferencingClient { client, body_limit }) + } + + pub async fn dereference(&self, url: Url) -> Result { + let mut headers = reqwest::header::HeaderMap::new(); + + headers.insert( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static(ContentActivityStreams.as_ref()), + ); + + let response = self + .client + .get(url) + .headers(headers) + .send() + .await + .map_err(DereferencingClientError::ReqwestError)?; + + let mut body = response.bytes_stream(); + + let mut data = Vec::with_capacity(4096); + + while let Some(buf) = body.next().await { + let chunk = buf.map_err(DereferencingClientError::ReqwestError)?; + + if data.len() + chunk.len() > self.body_limit { + return Err(DereferencingClientError::BodyLimitExceededError); + } + + data.extend_from_slice(&chunk); + } + + let json = + serde_json::from_slice::(&data).map_err(DereferencingClientError::JsonError)?; + + Ok(json) + } +} + +#[async_trait::async_trait] +impl Resolver for DereferencingClient { + type Error = DereferencingClientError; + + async fn resolve Deserialize<'a>>(&self, id: &str) -> Result { + let url = id.parse().map_err(DereferencingClientError::InvalidUrl)?; + + let json = self.dereference(url).await?; + + serde_json::from_value(json).map_err(DereferencingClientError::JsonError) + } +} diff --git a/src/client/forwarding_client.rs b/src/client/forwarding_client.rs new file mode 100644 index 0000000..5ae1ffe --- /dev/null +++ b/src/client/forwarding_client.rs @@ -0,0 +1,72 @@ +use axum::http::{HeaderMap, Method}; +use hyper::client::HttpConnector; +use hyper::http::uri; +use hyper::{header, Body, Client, Request, Response, Uri}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ProxyClientError { + #[error("Client error: {0}")] + ClientError(String), + #[error("URL error: {0}")] + UriError(#[from] uri::InvalidUri), + #[error("HTTP error: {0}")] + HttpError(#[from] axum::http::Error), + #[error("Hyper error: {0}")] + HyperError(#[from] hyper::Error), +} + +pub struct ProxyClient { + pub client: Client, + pub upstream: String, +} + +impl ProxyClient { + pub fn new(upstream: String) -> ProxyClient { + let client = Client::builder().set_host(false).build_http(); + + ProxyClient { client, upstream } + } + + pub async fn send( + &self, + host: &str, + path: &str, + method: Method, + body: Body, + headers_in: &HeaderMap, + ) -> Result, ProxyClientError> { + let mut builder = Request::builder(); + + let Some(headers) = builder.headers_mut() else { + return Err(ProxyClientError::ClientError("No headers".to_owned())); + }; + + *headers = headers_in.clone(); + + headers.insert( + header::HOST, + host.parse().map_err(|e| { + ProxyClientError::ClientError(format!("Invalid header value: {e:?}")) + })?, + ); + + let uri = format!("{}/{}", self.upstream, path) + .parse::() + .map_err(ProxyClientError::UriError)?; + + let request = builder + .method(method) + .uri(uri) + .body(body) + .map_err(ProxyClientError::HttpError)?; + + let response = self + .client + .request(request) + .await + .map_err(ProxyClientError::HyperError)?; + + Ok(response) + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..241bcaf --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,2 @@ +pub mod dereferencing_client; +pub mod forwarding_client;