diff --git a/Cargo.lock b/Cargo.lock index b9d58ac..4f07ecc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,6 +584,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -742,6 +752,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -818,6 +837,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1188,6 +1222,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1284,6 +1331,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + [[package]] name = "is-terminal" version = "0.4.9" @@ -1377,13 +1430,16 @@ checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" name = "magnetar" version = "0.2.0" dependencies = [ + "async-trait", "axum", "cached", "cfg-if", "chrono", "dotenvy", + "futures-util", "headers", "hyper", + "magnetar_activity_pub", "magnetar_calckey_model", "magnetar_common", "magnetar_core", @@ -1392,6 +1448,7 @@ dependencies = [ "magnetar_webfinger", "miette", "percent-encoding", + "reqwest", "serde", "serde_json", "strum", @@ -1403,6 +1460,20 @@ dependencies = [ "tracing", "tracing-subscriber", "unicode-segmentation", + "url", +] + +[[package]] +name = "magnetar_activity_pub" +version = "0.2.0" +dependencies = [ + "async-trait", + "chrono", + "serde", + "serde_json", + "thiserror", + "tokio", + "url", ] [[package]] @@ -1603,6 +1674,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1707,6 +1796,50 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +dependencies = [ + "bitflags 2.3.3", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-float" version = "3.7.0" @@ -2139,6 +2272,43 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -2291,6 +2461,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2472,6 +2651,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.180" @@ -3132,6 +3334,16 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -3480,6 +3692,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -3571,6 +3784,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.87" @@ -3740,6 +3965,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 704be05..adaca3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ license = "AGPL-3.0-only" [workspace] members = [ ".", + "ext_activity_pub", "ext_nodeinfo", "ext_webfinger", "ext_calckey_model", @@ -62,6 +63,7 @@ wasm-bindgen-futures = "0.4" web-sys = "0.3" [dependencies] +magnetar_activity_pub = { path = "./ext_activity_pub" } magnetar_core = { path = "./core" } magnetar_common = { path = "./magnetar_common" } magnetar_webfinger = { path = "./ext_webfinger" } @@ -73,12 +75,17 @@ cached = { workspace = true } chrono = { workspace = true } dotenvy = { workspace = true } +async-trait = { workspace = true } axum = { workspace = true, features = ["macros", "headers"] } +futures-util = { workspace = true } headers = { workspace = true } hyper = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] } tower = { workspace = true } tower-http = { workspace = true, features = ["cors", "trace", "fs"] } +url = { workspace = true } + +reqwest = { workspace = true, features = ["json"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing = { workspace = true } diff --git a/ext_activity_pub/Cargo.toml b/ext_activity_pub/Cargo.toml new file mode 100644 index 0000000..66bf8c2 --- /dev/null +++ b/ext_activity_pub/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "magnetar_activity_pub" +version.workspace = true +edition.workspace = true +license = "MIT OR Apache-2.0" + +[lib] +crate-type = ["rlib"] + +[dependencies] +async-trait = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +thiserror = { workspace = true } +url = { workspace = true, features = ["serde"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } \ No newline at end of file diff --git a/ext_activity_pub/src/lib.rs b/ext_activity_pub/src/lib.rs new file mode 100644 index 0000000..3cb4a8b --- /dev/null +++ b/ext_activity_pub/src/lib.rs @@ -0,0 +1,237 @@ +pub mod link; +pub mod object; +pub mod recipe; + +use crate::link::Link; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::borrow::Cow; +use std::fmt::Debug; +use std::ops::Deref; +use url::Url; + +pub trait ObjectSingle: Clone + Debug + Sized + 'static { + fn get_type() -> &'static str; +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ObjectRaw { + #[serde(flatten)] + data: Box, + #[serde(flatten)] + raw: Value, +} + +impl Deref for ObjectRaw { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl ObjectRaw { + pub fn into_inner(self) -> Box { + self.data + } +} + +impl AsRef for ObjectRaw { + fn as_ref(&self) -> &T { + self.as_data() + } +} + +impl ObjectRaw { + pub fn as_data(&self) -> &T { + &self.data + } + + pub fn as_json(&self) -> &Value { + &self.raw + } +} + +#[async_trait::async_trait] +pub trait Resolver: Send + Sync { + type Error: Send + Sync; + + async fn resolve Deserialize<'a>>(&self, id: &str) -> Result; +} + +#[async_trait::async_trait] +pub trait Resolvable { + type Output: Clone + Send + Sync; + + async fn resolve(&self, resolver: &R) -> Result, R::Error>; + + fn unresolve(&self) -> Option<&str>; +} + +#[macro_export] +macro_rules! def_ld { + ($obj_type: expr, $x: ty) => { + impl $crate::ObjectSingle for $x { + fn get_type() -> &'static str { + $obj_type + } + } + }; +} + +#[macro_export] +macro_rules! ld_document { + ($y: ident, $x: ty) => { + impl $y { + pub async fn resolve( + &self, + resolver: &R, + ) -> Result>, R::Error> { + match self { + $y::Object(obj) => Ok(std::borrow::Cow::Borrowed(obj)), + $y::Remote(Id(link)) => { + Ok(std::borrow::Cow::Owned(resolver.resolve(link).await?)) + } + } + } + + pub fn unresolve(&self) -> Option<&str> { + match self { + $y::Object(obj) => obj.as_data().as_id.as_deref(), + $y::Remote(Id(id)) => Some(&id), + } + } + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + #[serde(untagged)] + pub enum $y { + Remote(Id), + Object(Box>), + } + + #[async_trait::async_trait] + impl $crate::Resolvable for $y { + type Output = $crate::ObjectRaw<$x>; + + async fn resolve( + &self, + resolver: &R, + ) -> Result, R::Error> { + self.resolve(resolver).await + } + + fn unresolve(&self) -> Option<&str> { + self.unresolve() + } + } + + /* + impl ObjectRaw<$x> { + pub fn try_cast() -> T {} + } + + */ + }; +} + +#[macro_export] +macro_rules! ld_union { + ($y: ident, $z: ident, $($item_name: ident as $item_type: ty),+) => { + impl $y { + pub async fn resolve( + &self, + resolver: &R, + ) -> Result>, R::Error> { + match self { + $y::Object(obj) => Ok(std::borrow::Cow::Borrowed(obj)), + $y::Remote(Id(link)) => { + Ok(std::borrow::Cow::Owned(resolver.resolve(link).await?)) + } + } + } + + pub fn unresolve(&self) -> Option<&str> { + match self { + $y::Object(obj) => obj.as_data().base.as_id.as_deref(), + $y::Remote(Id(id)) => Some(&id), + } + } + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct $z { + #[serde(flatten)] + pub base: $crate::Base, + $( + #[serde(flatten)] + $item_name: Box<$item_type>, + )+ + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + #[serde(untagged)] + pub enum $y { + Remote(Id), + Object(ObjectRaw<$z>), + } + + #[async_trait::async_trait] + impl $crate::Resolvable for $y { + type Output = $crate::ObjectRaw<$z>; + + async fn resolve( + &self, + resolver: &R, + ) -> Result, R::Error> { + self.resolve(resolver).await + } + + fn unresolve(&self) -> Option<&str> { + self.unresolve() + } + } + }; +} + +#[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 { + Url(Url), + Link(Box), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[repr(transparent)] +pub struct Id(pub String); diff --git a/ext_activity_pub/src/link/link_types.rs b/ext_activity_pub/src/link/link_types.rs new file mode 100644 index 0000000..b8a5cd7 --- /dev/null +++ b/ext_activity_pub/src/link/link_types.rs @@ -0,0 +1,8 @@ +use crate::{def_ld, ld_document, Id, ObjectRaw}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct LinkMention; + +def_ld!("https://www.w3.org/ns/activitystreams#Mention", LinkMention); +ld_document!(RefLinkMention, LinkMention); diff --git a/ext_activity_pub/src/link/mod.rs b/ext_activity_pub/src/link/mod.rs new file mode 100644 index 0000000..085d944 --- /dev/null +++ b/ext_activity_pub/src/link/mod.rs @@ -0,0 +1,44 @@ +use crate::object::RefObjectLinkUnion; +use crate::OneOrMore; +use crate::{def_ld, ld_document, Id, ObjectRaw}; +use serde::{Deserialize, Serialize}; + +pub mod link_types; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct Link { + #[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); +ld_document!(RefLink, Link); diff --git a/ext_activity_pub/src/object/activity.rs b/ext_activity_pub/src/object/activity.rs new file mode 100644 index 0000000..fc60089 --- /dev/null +++ b/ext_activity_pub/src/object/activity.rs @@ -0,0 +1,240 @@ +use crate::object::{Object, RefObjectLinkUnion}; +use crate::{def_ld, ld_document, Id, ObjectRaw, ObjectSingle, OneOrMore}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct Activity { + #[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!("Activity", Activity, base as Object); +ld_document!(RefActivity, Activity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct IntransitiveActivity; + +def_ld!( + "IntransitiveActivity", + IntransitiveActivity, + base as Activity +); +ld_document!(RefIntransitiveActivity, IntransitiveActivity); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityAccept; + +def_ld!("Accept", ActivityAccept, base as Activity); +ld_document!(RefActivityAccept, ActivityAccept); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityTentativeAccept; + +def_ld!( + "TentativeAccept", + ActivityTentativeAccept, + base as ActivityAccept +); +ld_document!(RefActivityTentativeAccept, ActivityTentativeAccept); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityAdd; + +def_ld!("Add", ActivityAdd, base as Activity); +ld_document!(RefActivityAdd, ActivityAdd); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityArrive; + +def_ld!("Arrive", ActivityArrive, base as Activity); +ld_document!(RefActivityArrive, ActivityArrive); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityCreate; + +def_ld!("Create", ActivityCreate, base as Activity); +ld_document!(RefActivityCreate, ActivityCreate); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityDelete; + +def_ld!("Delete", ActivityDelete, base as Activity); +ld_document!(RefActivityDelete, ActivityDelete); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityFollow; + +def_ld!("Follow", ActivityFollow, base as Activity); +ld_document!(RefActivityFollow, ActivityFollow); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityIgnore; + +def_ld!("Ignore", ActivityIgnore, base as Activity); +ld_document!(RefActivityIgnore, ActivityIgnore); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityJoin; + +def_ld!("Join", ActivityJoin, base as Activity); +ld_document!(RefActivityJoin, ActivityJoin); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityLeave; + +def_ld!("Leave", ActivityLeave, base as Activity); +ld_document!(RefActivityLeave, ActivityLeave); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityLike; + +def_ld!("Like", ActivityLike, base as Activity); +ld_document!(RefActivityLike, ActivityLike); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityOffer; + +def_ld!("Offer", ActivityOffer, base as Activity); +ld_document!(RefActivityOffer, ActivityOffer); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityInvite; + +def_ld!("Invite", ActivityInvite, base as ActivityOffer); +ld_document!(RefActivityInvite, ActivityInvite); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityReject; + +def_ld!("Reject", ActivityReject, base as Activity); +ld_document!(RefActivityReject, ActivityReject); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityTentativeReject; + +def_ld!( + "TentativeReject", + ActivityTentativeReject, + base as ActivityReject +); +ld_document!(RefActivityTentativeReject, ActivityTentativeReject); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityRemove; + +def_ld!("Remove", ActivityRemove, base as Activity); +ld_document!(RefActivityRemove, ActivityRemove); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityUndo; + +def_ld!("Undo", ActivityUndo, base as Activity); +ld_document!(RefActivityUndo, ActivityUndo); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityUpdate; + +def_ld!("Update", ActivityUpdate, base as Activity); +ld_document!(RefActivityUpdate, ActivityUpdate); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityView; + +def_ld!("View", ActivityView, base as Activity); +ld_document!(RefActivityView, ActivityView); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityListen; + +def_ld!("Listen", ActivityListen, base as Activity); +ld_document!(RefActivityListen, ActivityListen); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityRead; + +def_ld!("Read", ActivityRead, base as Activity); +ld_document!(RefActivityRead, ActivityRead); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityMove; + +def_ld!("Move", ActivityMove, base as Activity); +ld_document!(RefActivityMove, ActivityMove); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityTravel; + +def_ld!("Travel", ActivityTravel, base as IntransitiveActivity); +ld_document!(RefActivityTravel, ActivityTravel); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityAnnounce; + +def_ld!("Announce", ActivityAnnounce, base as Activity); +ld_document!(RefActivityAnnounce, ActivityAnnounce); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityBlock; + +def_ld!("Block", ActivityBlock, base as ActivityIgnore); +ld_document!(RefActivityBlock, ActivityBlock); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityFlag; + +def_ld!("Flag", ActivityFlag, base as Activity); +ld_document!(RefActivityFlag, ActivityFlag); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityDislike; + +def_ld!("Dislike", ActivityDislike, base as Activity); +ld_document!(RefActivityDislike, ActivityDislike); + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ActivityClosedStatus { + Date(DateTime), + Bool(bool), + Other(RefObjectLinkUnion), +} + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActivityQuestion { + #[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!("Question", ActivityQuestion, base as IntransitiveActivity); +ld_document!(RefActivityQuestion, ActivityQuestion); diff --git a/ext_activity_pub/src/object/actor.rs b/ext_activity_pub/src/object/actor.rs new file mode 100644 index 0000000..cf7d46e --- /dev/null +++ b/ext_activity_pub/src/object/actor.rs @@ -0,0 +1,68 @@ +use crate::object::{RefCollection, RefOrderedCollection}; +use crate::{def_ld, ld_document, Id, ObjectRaw}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorActivityPubProps { + #[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!("Application", ActorApplication, base as Object); +ld_document!(RefActorApplication, ActorApplication); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorGroup; + +def_ld!("Group", ActorGroup, base as Object); +ld_document!(RefActorGroup, ActorGroup); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorOrganization; + +def_ld!("Organization", ActorOrganization, base as Object); +ld_document!(RefActorOrganization, ActorOrganization); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorPerson; + +def_ld!("Person", ActorPerson, base as Object); +ld_document!(RefActorPerson, ActorPerson); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ActorService; + +def_ld!("Service", ActorService, base as Object); +ld_document!(RefActorService, ActorService); diff --git a/ext_activity_pub/src/object/mod.rs b/ext_activity_pub/src/object/mod.rs new file mode 100644 index 0000000..a48f02c --- /dev/null +++ b/ext_activity_pub/src/object/mod.rs @@ -0,0 +1,233 @@ +use crate::link::Link; +use crate::object::object_types::RefObjectImageLinkUnion; +use crate::{def_ld, Id}; +use crate::{ld_document, ld_union, Base, LinkOrUrl, ObjectRaw, ObjectSingle, OneOrMore}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub mod activity; +pub mod actor; +pub mod object_types; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct Object { + #[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_document!(RefObject, Object); +ld_union!( + RefObjectLinkUnion, + 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_document!(RefCollection, Collection); +ld_union!( + RefCollectionLinkUnion, + 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 +); +ld_document!(RefOrderedCollection, 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_document!(RefCollectionPage, CollectionPage); + +ld_union!( + RefCollectionPageLinkUnion, + 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 +); +ld_document!(RefOrderedCollectionPage, OrderedCollectionPage); diff --git a/ext_activity_pub/src/object/object_types.rs b/ext_activity_pub/src/object/object_types.rs new file mode 100644 index 0000000..36d1fc4 --- /dev/null +++ b/ext_activity_pub/src/object/object_types.rs @@ -0,0 +1,131 @@ +use crate::link::Link; +use crate::object::{Object, RefObject, RefObjectLinkUnion}; +use crate::{def_ld, ld_document, ld_union, Id, ObjectRaw, ObjectSingle, OneOrMore}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectRelationship { + #[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!("Relationship", ObjectRelationship, base as Object); +ld_document!(RefObjectRelationship, ObjectRelationship); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectArticle; + +def_ld!("Article", ObjectArticle, base as Object); +ld_document!(RefObjectArticle, ObjectArticle); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectDocument; + +def_ld!("Document", ObjectDocument, base as Object); +ld_document!(RefObjectDocument, ObjectDocument); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectAudio; + +def_ld!("Audio", ObjectAudio, base as ObjectDocument); +ld_document!(RefObjectAudio, ObjectAudio); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectImage; + +def_ld!("Image", ObjectImage, base as ObjectDocument); +ld_document!(RefObjectImage, ObjectImage); +ld_union!( + RefObjectImageLinkUnion, + ObjectImageLinkUnion, + image_props as ObjectImage, + link_props as Link +); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectVideo; + +def_ld!("Video", ObjectVideo, base as ObjectDocument); +ld_document!(RefObjectVideo, ObjectVideo); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectNote; + +def_ld!("Note", ObjectNote, base as Object); +ld_document!(RefObjectNote, ObjectNote); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectPage; + +def_ld!("Page", ObjectPage, base as ObjectDocument); +ld_document!(RefObjectPage, ObjectPage); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectEvent; + +def_ld!("Event", ObjectEvent, base as Object); +ld_document!(RefObjectEvent, ObjectEvent); + +#[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!("Place", ObjectPlace, base as Object); +ld_document!(RefObjectPlace, ObjectPlace); + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct ObjectProfile { + #[serde(rename = "https://www.w3.org/ns/activitystreams#describes")] + #[serde(skip_serializing_if = "Option::is_none")] + pub describes: Option, +} + +def_ld!("Profile", ObjectProfile, base as Object); +ld_document!(RefObjectProfile, ObjectProfile); + +#[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!("Tombstone", ObjectTombstone, base as Object); +ld_document!(RefObjectTombstone, ObjectTombstone); diff --git a/ext_activity_pub/src/recipe/account_move.rs b/ext_activity_pub/src/recipe/account_move.rs new file mode 100644 index 0000000..e69de29 diff --git a/ext_activity_pub/src/recipe/block.rs b/ext_activity_pub/src/recipe/block.rs new file mode 100644 index 0000000..1193c8b --- /dev/null +++ b/ext_activity_pub/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_pub/src/recipe/flag.rs b/ext_activity_pub/src/recipe/flag.rs new file mode 100644 index 0000000..e69de29 diff --git a/ext_activity_pub/src/recipe/mod.rs b/ext_activity_pub/src/recipe/mod.rs new file mode 100644 index 0000000..f5a782b --- /dev/null +++ b/ext_activity_pub/src/recipe/mod.rs @@ -0,0 +1,97 @@ +use crate::{OneOrMore, Resolvable}; +use thiserror::Error; + +#[macro_use] +pub mod account_move; +#[macro_use] +pub mod block; +#[macro_use] +pub mod flag; +#[macro_use] +pub mod poll; +#[macro_use] +pub mod tag; + +#[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/ext_activity_pub/src/recipe/poll.rs b/ext_activity_pub/src/recipe/poll.rs new file mode 100644 index 0000000..e69de29 diff --git a/ext_activity_pub/src/recipe/tag.rs b/ext_activity_pub/src/recipe/tag.rs new file mode 100644 index 0000000..e69de29 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; diff --git a/src/main.rs b/src/main.rs index e802e89..6044d58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod api_v1; +pub mod client; pub mod model; pub mod nodeinfo; pub mod service;