ap: add ActivityPub class hierarchy

words cannot describe the constant agonizing pain
of my miserable existence and why i haven't ended
it yet is beyond my comprehension
main
anna 9 months ago
parent dad5d07dd6
commit 891c6a9a39
Signed by: fef
GPG Key ID: 2585C2DC6D79B485

@ -1,357 +0,0 @@
use serde::de::{MapAccess, SeqAccess};
use serde::ser::{SerializeMap, SerializeSeq};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::fmt;
use crate::core::*;
/// The `@context` field in an ActivityPub document.
#[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct Context {
entries: Vec<Entry>,
}
pub mod uri {
pub static ACTIVITY_STREAMS: &'static str = "https://www.w3.org/ns/activitystreams";
pub static ACTIVITY_STREAMS_ALT: &'static str = "http://www.w3.org/ns/activitystreams";
pub static NYANO: &'static str = "https://nyanoblog.org/ns/activitystreams";
pub static TOOT: &'static str = "http://joinmastodon.org/ns#";
}
#[derive(Debug, Clone)]
pub enum Entry {
Anon(AnonEntry),
Named(NamedEntry),
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnonEntry(String);
#[derive(Debug, Clone, PartialEq)]
pub struct NamedEntry(String, String);
impl Entry {
pub fn anon(uri: &str) -> Entry {
Entry::Anon(AnonEntry(String::from(uri)))
}
pub fn named(name: &str, uri: &str) -> Entry {
Entry::Named(NamedEntry(String::from(name), String::from(uri)))
}
pub fn name(&self) -> Option<&str> {
match self {
Entry::Anon(_) => None,
Entry::Named(named) => Some(&named.0),
}
}
pub fn uri(&self) -> &str {
match self {
Entry::Anon(anon) => &anon.0,
Entry::Named(named) => &named.1,
}
}
}
impl PartialEq for Entry {
fn eq(&self, other: &Self) -> bool {
match self {
Entry::Anon(self_anon) => self_anon.0.eq(other.uri()),
Entry::Named(self_named) => match other {
Entry::Anon(other_anon) => self_named.1.eq(&other_anon.0),
Entry::Named(other_named) => self_named.eq(other_named),
},
}
}
}
impl Context {
pub fn new() -> Context {
Context {
entries: Vec::new(),
}
}
pub fn has_entry(&self, uri: &str) -> bool {
self.entries.iter().any(|e| e.uri().eq(uri))
}
pub fn add_entry(&mut self, mut entry: Entry) {
// As per <https://www.w3.org/TR/activitystreams-core/#jsonld>:
//
// > Implementations producing ActivityStreams 2.0. documents SHOULD include a
// > @context property with a value that includes a reference to the normative
// > Activity Streams 2.0 JSON-LD @context definition using the URL
// > "https://www.w3.org/ns/activitystreams". Implementations MAY use the
// > alternative URL "http://www.w3.org/ns/activitystreams" instead.
// > This can be done using a string, object, or array.
//
// We're just gonna silently rewrite the http:// to https:// and call it a day.
if entry.uri().eq(uri::ACTIVITY_STREAMS_ALT) {
entry = match entry {
Entry::Anon(_) => Entry::anon(uri::ACTIVITY_STREAMS),
Entry::Named(named) => Entry::named(&named.0, uri::ACTIVITY_STREAMS),
};
}
if !self.has_entry(entry.uri()) {
self.entries.push(entry);
}
}
/// Merge with a contained object's context, consuming that child's context.
///
/// To be used as:
///
/// ```
/// if let Some(other_context) = other_object.context.take() {
/// context.merge(other_context);
/// }
/// ```
pub fn merge(&mut self, other: Context) {
for e in other.entries {
self.add_entry(e);
}
}
}
impl Default for Context {
fn default() -> Self {
let mut ctx = Context::new();
ctx.add_entry(Entry::anon(uri::ACTIVITY_STREAMS));
ctx
}
}
impl Serialize for Context {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
// FIXME: omg this is just horrible
if self.entries.len() == 1 {
serializer.serialize_str(self.entries[0].uri())
} else {
let mut anons = Vec::new();
let mut nameds = Vec::new();
for e in &self.entries {
match e {
Entry::Anon(anon) => anons.push(anon),
Entry::Named(named) => nameds.push(named),
}
}
let (anons, nameds) = (anons, nameds);
let seq_len = anons.len() + if nameds.len() > 0 { 1 } else { 0 };
let mut seq = serializer.serialize_seq(Some(seq_len))?;
for a in anons {
seq.serialize_element(&a.0)?;
}
if nameds.len() > 0 {
seq.serialize_element(&NamedEntriesWrapper(nameds))?;
}
seq.end()
}
}
}
impl<'de> de::Deserialize<'de> for Context {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ContextVisitor)
}
}
struct ContextVisitor;
impl<'de> de::Visitor<'de> for ContextVisitor {
type Value = Context;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a valid JSON-LD context specification")
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(Context {
entries: vec![Entry::anon(value)],
})
}
fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(Context::new())
}
fn visit_seq<S>(self, mut seq: S) -> std::result::Result<Self::Value, S::Error>
where
S: SeqAccess<'de>,
{
let mut context = Context::new();
while let Some(wrapper) = seq.next_element::<StupidEntryWrapper>()? {
match wrapper {
StupidEntryWrapper::Single(entry) => context.add_entry(entry),
StupidEntryWrapper::Multi(entries) => {
for e in entries {
context.add_entry(e);
}
}
}
}
// As per <https://www.w3.org/TR/activitystreams-core/#jsonld>:
//
// "When a JSON-LD enabled Activity Streams 2.0 implementation encounters
// a JSON document identified using the "application/activity+json" MIME
// media type, and that document does not contain a @context property whose
// value includes a reference to the normative Activity Streams 2.0 JSON-LD
// @context definition, the implementation MUST assume that the normative
// @context definition still applies."
if !context.has_entry(uri::ACTIVITY_STREAMS) {
warn!(target: "ap", "@context does not have canonical ActivityStreams URI");
context.add_entry(Entry::anon(uri::ACTIVITY_STREAMS));
}
if context.entries.len() == 0 {
warn!(target: "ap", "Malformed @context: Arrays must always have elements, or be null");
context.add_entry(Entry::anon(uri::ACTIVITY_STREAMS));
}
Ok(context)
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut entries = Vec::new();
while let Some((k, v)) = map.next_entry::<&str, &str>()? {
entries.push(Entry::named(k, v));
}
Ok(Context { entries })
}
}
/// Stupid wrapper because if @context is an array, its elements are strings and/or maps.
enum StupidEntryWrapper {
/// Single (anonymous) URI
Single(Entry),
/// Map of (named) URIs
Multi(Vec<Entry>),
}
impl<'de> Deserialize<'de> for StupidEntryWrapper {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(StupidEntryWrapperVisitor)
}
}
struct StupidEntryWrapperVisitor;
impl<'de> de::Visitor<'de> for StupidEntryWrapperVisitor {
type Value = StupidEntryWrapper;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a context URI or map of named URIs")
}
fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(StupidEntryWrapper::Single(Entry::anon(s)))
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut entries = Vec::new();
while let Some((k, v)) = map.next_entry()? {
entries.push(Entry::Named(NamedEntry(k, v)));
}
Ok(StupidEntryWrapper::Multi(entries))
}
}
/// We can't use a HashMap to serialize named entries because that doesn't
/// produce deterministic results, so we have to use our own serializer
struct NamedEntriesWrapper<'a>(Vec<&'a NamedEntry>);
impl<'a> Serialize for NamedEntriesWrapper<'a> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for entry in &self.0 {
//let entry = *entry;
map.serialize_entry(&entry.0, &entry.1)?;
}
map.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_test::{assert_tokens, Token};
#[test]
fn default() {
let ctx = Context::default();
assert_tokens(&ctx, &[Token::String(uri::ACTIVITY_STREAMS)]);
}
#[test]
fn multi_anon() {
let mut ctx = Context::default();
ctx.add_entry(Entry::anon(uri::NYANO));
assert_tokens(
&ctx,
&[
Token::Seq { len: Some(2) },
Token::String(uri::ACTIVITY_STREAMS),
Token::String(uri::NYANO),
Token::SeqEnd,
],
);
}
#[test]
fn multi_named() {
let mut ctx = Context::default();
ctx.add_entry(Entry::named("nyano", uri::NYANO));
ctx.add_entry(Entry::named("toot", uri::TOOT));
assert_tokens(
&ctx,
&[
Token::Seq { len: Some(2) },
Token::String(uri::ACTIVITY_STREAMS),
Token::Map { len: Some(2) },
Token::String("nyano"),
Token::String(uri::NYANO),
Token::String("toot"),
Token::String(uri::TOOT),
Token::MapEnd,
Token::SeqEnd,
],
)
}
}

@ -9,27 +9,25 @@
// and work with the `application/activity+json` MIME type.
//
// FIXME: This file is, frankly, just pure chaos and desperately needs a refactor.
// Update 2023-07-29: Things have improved slightly, but it's still bad.
use crate::state::AppState;
use bytes::Bytes;
use futures::future::{BoxFuture, FutureExt};
use hashbrown::HashSet;
use iref::Iri;
use json_ld::{
syntax::{Parse, Value},
Loader, Profile, RemoteDocument,
};
use json_ld::{syntax::Value, Loader, Profile, RemoteDocument};
use locspan::{Meta, Span};
use rdf_types::vocabulary::IriIndex;
use rdf_types::{IriVocabulary, IriVocabularyMut};
use rdf_types::{vocabulary::IriIndex, IriVocabulary, IriVocabularyMut};
use reqwest::{
header::{ACCEPT, CONTENT_TYPE, LINK, LOCATION},
StatusCode,
};
use std::hash::Hash;
use std::ops::ControlFlow;
use crate::core::*;
use crate::headers;
use crate::state::AppState;
use crate::util::http::{
self,
header::{content_type::ContentType, link::Link},
@ -44,34 +42,26 @@ pub struct CachedLoader<I = IriIndex, M = Span, T = Value<M>> {
type DynParser<I, M, T> =
dyn 'static + Send + Sync + FnMut(&dyn IriVocabulary<Iri = I>, &I, Bytes) -> Result<Meta<T, M>>;
impl CachedLoader {
pub fn new(state: AppState) -> Self {
CachedLoader::new_with(state, move |vocab, file, bytes| {
let content = String::from_utf8(bytes.to_vec())
.map_err(|e| Error::MalformedApub(format!("Invalid encoding: {e}")))?;
json_ld::syntax::Value::parse_str(&content, |span| span)
.map_err(|e| Error::MalformedApub(format!("Syntax error in JSON document: {e}")))
})
}
}
impl<I, M, T> CachedLoader<I, M, T> {
pub fn new_with(
state: AppState,
parser: impl 'static
pub fn new_with<F>(state: AppState, parser: F) -> Self
where
F: 'static
+ Send
+ Sync
+ FnMut(&dyn IriVocabulary<Iri = I>, &I, Bytes) -> Result<Meta<T, M>>,
) -> Self {
CachedLoader {
{
Self {
state,
parser: Box::new(parser),
}
}
}
impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> Loader<I, M>
for CachedLoader<I, M, T>
impl<I, M, T> Loader<I, M> for CachedLoader<I, M, T>
where
I: Clone + Eq + Hash + Send + Sync,
M: Send,
T: Clone + Send,
{
type Output = T;
type Error = Error;
@ -88,27 +78,43 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> Loader<I, M>
}
}
impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<I, M, T> {
struct LoopState<'a, I, V> {
redirect_count: u32,
vocab: &'a mut V,
url: I,
}
impl<I, M, T> CachedLoader<I, M, T>
where
I: Clone + Eq + Hash + Send + Sync,
M: Send,
T: Clone + Send,
{
async fn load_chain(
&mut self,
vocabulary: &mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
mut url: I,
url: I,
) -> Result<RemoteDocument<I, M, T>> {
const MAX_REDIRECTS: u32 = 8;
const ACCEPT_ACTIVITY_PUB: &str =
"application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/json";
let mut redirect_count = 0;
let mut state = LoopState {
redirect_count: 0,
vocab: vocabulary,
url,
};
'next_url: loop {
if redirect_count > MAX_REDIRECTS {
return Err(Error::MalformedApub(format!(
loop {
if state.redirect_count > MAX_REDIRECTS {
return apub_err(format!(
"Refusing to follow more than {MAX_REDIRECTS} redirects"
)));
));
}
let iri = vocabulary
.iri(&url)
let iri = state
.vocab
.iri(&state.url)
.ok_or_else(|| Error::MalformedApub(String::from("Unresolved IRI")))?;
debug!(target: "ap", "downloading {iri}");
let response = http::get(
@ -121,76 +127,14 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
.await?;
match response.status() {
StatusCode::OK => {
let mut content_types = response
.headers()
.get_all(CONTENT_TYPE)
.into_iter()
.filter_map(ContentType::from_header);
match content_types.find(ContentType::is_json_ld) {
Some(content_type) => {
let mut context_url = None;
if !content_type.is_proper_json_ld() {
context_url = self.get_context_url(vocabulary, &url, &response)?;
}
let profile: HashSet<Profile<I>> = content_type
.profile()
.into_iter()
.flat_map(|profile| profile.split(' '))
.filter_map(|p| {
Iri::new(p).ok().map(|iri| Profile::new(iri, vocabulary))
})
.collect();
let bytes = response.bytes().await?;
let document = (*self.parser)(vocabulary, &url, bytes)
.map_err(|e| Error::MalformedApub(e.to_string()))?;
break Ok(RemoteDocument::new_full(
Some(url),
Some(content_type.into_mime()),
context_url,
profile,
document,
));
}
None => {
debug!(target: "ap", "no valid media type found");
for link in response.headers().get_all(LINK).into_iter() {
if let Some(link) = Link::from_header(link) {
if link.rel() == Some("alternate") && link.is_proper_json_ld() {
debug!(target: "ap", "link found");
let u = link.href().resolved(vocabulary.iri(&url).unwrap());
url = vocabulary.insert(u.as_iri());
redirect_count += 1;
continue 'next_url;
}
}
}
break Err(Error::MalformedApub(String::from("Invalid content type")));
}
}
}
StatusCode::OK => match self.handle_http_ok(&mut state, response).await {
ControlFlow::Break(result) => break result,
ControlFlow::Continue(_) => continue,
},
code if code.is_redirection() => {
if response.status() == StatusCode::SEE_OTHER {
break Err(Error::NotFound);
} else {
match response.headers().get(LOCATION) {
Some(location) => {
let u = Iri::new(location.as_bytes()).map_err(|_| {
Error::MalformedApub(String::from("Invalid redirect URL"))
})?;
url = vocabulary.insert(u);
}
None => {
break Err(Error::MalformedApub(String::from(
"Missing Location header in HTTP redirect",
)))
}
}
match self.handle_http_redirect(&mut state, response) {
ControlFlow::Break(result) => break result,
ControlFlow::Continue(_) => continue,
}
}
code => {
@ -203,12 +147,10 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
}
}
fn get_context_url(
&self,
vocabulary: &mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
url: &I,
response: &Response,
) -> Result<Option<I>> {
fn get_context_url<V>(&self, vocab: &mut V, url: &I, response: &Response) -> Result<Option<I>>
where
V: Send + Sync + IriVocabularyMut<Iri = I>,
{
let mut context_url = None;
for link in response.headers().get_all(LINK).into_iter() {
if let Some(link) = Link::from_header(link) {
@ -222,14 +164,116 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
}
let iri_buf = link.href().resolved(
vocabulary
vocab
.iri(url)
.ok_or_else(|| Error::MalformedApub(String::from("Unresolved IRI")))?,
);
context_url = Some(vocabulary.insert(iri_buf.as_iri()));
context_url = Some(vocab.insert(iri_buf.as_iri()));
}
}
}
Ok(context_url)
}
async fn handle_http_ok<V>(
&mut self,
state: &mut LoopState<'_, I, V>,
response: Response,
) -> ControlFlow<Result<RemoteDocument<I, M, T>>>
where
V: Send + Sync + IriVocabularyMut<Iri = I>,
{
let mut content_types = response
.headers()
.get_all(CONTENT_TYPE)
.into_iter()
.filter_map(ContentType::from_header);
match content_types.find(ContentType::is_json_ld) {
Some(content_type) => {
let mut context_url = None;
if !content_type.is_proper_json_ld() {
context_url = match self.get_context_url(state.vocab, &state.url, &response) {
Ok(context_url) => context_url,
Err(e) => return ControlFlow::Break(Err(e)),
};
}
let profile: HashSet<Profile<I>> = content_type
.profile()
.into_iter()
.flat_map(|profile| profile.split(' '))
.filter_map(|p| Iri::new(p).ok().map(|iri| Profile::new(iri, state.vocab)))
.collect();
let bytes = match response.bytes().await {
Ok(bytes) => bytes,
Err(e) => return ControlFlow::Break(Err(e)),
};
let document = match (*self.parser)(state.vocab, &state.url, bytes) {
Ok(document) => document,
Err(e) => return ControlFlow::Break(apub_err(e.to_string())),
};
ControlFlow::Break(Ok(RemoteDocument::new_full(
Some(state.url.clone()),
Some(content_type.into_mime()),
context_url,
profile,
document,
)))
}
None => {
debug!(target: "ap", "no valid media type found");
for link in response.headers().get_all(LINK).into_iter() {
if let Some(link) = Link::from_header(link) {
if link.rel() == Some("alternate") && link.is_proper_json_ld() {
debug!(target: "ap", "link found");
let u = link.href().resolved(state.vocab.iri(&state.url).unwrap());
state.url = state.vocab.insert(u.as_iri());
state.redirect_count += 1;
return ControlFlow::Continue(());
}
}
}
ControlFlow::Break(apub_err("Invalid content type"))
}
}
}
fn handle_http_redirect<V>(
&self,
state: &mut LoopState<'_, I, V>,
response: Response,
) -> ControlFlow<Result<RemoteDocument<I, M, T>>>
where
V: Send + Sync + IriVocabularyMut<Iri = I>,
{
state.redirect_count += 1;
if response.status() == StatusCode::SEE_OTHER {
return ControlFlow::Break(Err(Error::NotFound));
}
match response.headers().get(LOCATION) {
Some(location) => {
let u = match Iri::new(location.as_bytes()) {
Ok(u) => u,
Err(e) => {
return ControlFlow::Break(apub_err(format!("Invalid redirect URL: {e}")));
}
};
state.url = state.vocab.insert(u);
ControlFlow::Continue(())
}
None => ControlFlow::Break(Err(Error::MalformedApub(String::from(
"Missing Location header in HTTP redirect",
)))),
}
}
}
fn apub_err<T>(msg: impl Into<String>) -> Result<T> {
Err(Error::MalformedApub(msg.into()))
}

@ -1,3 +1,4 @@
pub mod loader;
pub mod processor;
pub mod trans;
pub mod vocab;

@ -1,617 +0,0 @@
use serde::de::{self, IgnoredAny, MapAccess, SeqAccess, Visitor};
use serde::ser::{SerializeMap, SerializeSeq};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use crate::ap::context::{uri, Context};
use crate::core::*;
/// Wrapper for [`BaseObject`] because ActivityPub allows
/// any object to be referenced indirectly through a URL.
#[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub enum Object {
Direct(BaseObject),
Indirect(String),
Multi(Vec<Object>),
}
/// Intermediate representation of actual AP objects.
/// Contains all possible fields for any type of object.
/// We "simplify" AP processing by treating anything as an object.
/// This struct is only used internally as a helper for (de)serializing objects
/// because doing that all in one step would, frankly, be insane.
#[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)]
pub struct BaseObject {
pub context: Option<Context>,
pub typ: Option<ObjectType>,
pub id: Option<ObjectId>,
pub name: Option<String>,
pub content: Option<String>,
pub subject: Option<Box<Object>>,
pub actor: Option<Box<Object>>,
pub object: Option<Box<Object>>,
pub attributed_to: Option<Box<Object>>,
}
#[derive(Debug, PartialEq)]
pub enum ObjectType {
Unknown(String),
// Object
Object,
Article,
Audio,
Document,
Event,
Image,
Note,
Page,
Place,
Profile,
Relationship,
Tombstone,
Video,
// Link
Link,
Mention,
// Activity
Activity,
Accept,
Add,
Announce,
Arrive,
Block,
Create,
Delete,
Dislike,
EmojiReact, // extension
Flag,
Follow,
Ignore,
Invite,
Join,
Leave,
Like,
Listen,
Move,
Offer,
Question,
Reject,
Read,
Remove,
TentativeReject,
TentativeAccept,
Travel,
Undo,
Update,
View,
// Actor
Actor,
Application,
Group,
Organization,
Person,
Service,
}
/// Globally unique object identifier.
#[derive(Debug, PartialEq)]
pub struct ObjectId(String);
pub trait GetObjectId {
/// Get this object's globally unique (ActivityPub) id.
/// The `base_url` parameter is for constructing a URI and takes
/// the form `https://domain.tld` (without a trailing slash).
/// Do not rely on the URL starting with `https` though, a future
/// version might support `http` URLs for Tor and such.
fn get_object_id(&self, base_url: &str) -> String;
}
impl<T> From<T> for ObjectId
where
T: GetObjectId,
{
fn from(t: T) -> ObjectId {
// TODO: there has to be a more elegant way for this
let base_domain = format!("https://{}", std::env::var("LOCAL_DOMAIN").unwrap());
ObjectId(t.get_object_id(&base_domain))
}
}
impl Serialize for Object {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Object::Direct(direct) => direct.serialize(serializer),
Object::Indirect(url) => serializer.serialize_str(url),
Object::Multi(objs) => {
let mut seq = serializer.serialize_seq(Some(objs.len()))?;
for obj in objs {
seq.serialize_element(obj)?;
}
seq.end()
}
}
}
}
impl Serialize for BaseObject {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(None)?;
if let Some(context) = &self.context {
map.serialize_entry("@context", context)?;
}
if let Some(typ) = &self.typ {
map.serialize_entry("type", typ)?;
}
if let Some(id) = &self.id {
map.serialize_entry("id", &id.0)?;
}
if let Some(name) = &self.name {
map.serialize_entry("name", name)?;
}
if let Some(content) = &self.content {
map.serialize_entry("content", content)?;
}
if let Some(subject) = &self.subject {
map.serialize_entry("subject", subject.as_ref())?;
}
if let Some(actor) = &self.actor {
map.serialize_entry("actor", actor.as_ref())?;
}
if let Some(object) = &self.object {
map.serialize_entry("object", object.as_ref())?;
}
if let Some(attributed_to) = &self.attributed_to {
map.serialize_entry("attributedTo", attributed_to.as_ref())?;
}
map.end()
}
}
impl Serialize for ObjectType {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Object {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ObjectVisitor)
}
}
struct ObjectVisitor;
impl<'de> Visitor<'de> for ObjectVisitor {
type Value = Object;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an Activity Streams 2.0 compliant JSON-LD object")
}
fn visit_string<E>(self, s: String) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(Object::Indirect(s))
}
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut objs = Vec::new();
while let Some(obj) = seq.next_element()? {
objs.push(obj);
}
Ok(Object::Multi(objs))
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut obj = BaseObject::default();
while let Some(k) = map.next_key::<String>()? {
match k.as_str() {
"@context" => obj.context = Some(map.next_value()?),
"type" => obj.typ = Some(map.next_value()?),
"id" => obj.id = Some(ObjectId(map.next_value()?)),
"name" => obj.name = Some(map.next_value()?),
"content" => obj.content = Some(map.next_value()?),
"subject" => obj.subject = Some(map.next_value()?),
"actor" => obj.actor = Some(Box::new(map.next_value()?)),
"object" => obj.object = Some(Box::new(map.next_value()?)),
"attributedTo" => obj.attributed_to = Some(Box::new(map.next_value()?)),
_ => {
map.next_value::<IgnoredAny>()?;
}
}
}
Ok(Object::Direct(obj))
}
}
impl<'de> Deserialize<'de> for ObjectType {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(ObjectTypeVisitor)
}
}
struct ObjectTypeVisitor;
impl<'de> Visitor<'de> for ObjectTypeVisitor {
type Value = ObjectType;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an Activity Streams 2.0 compliant object type name")
}
fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(ObjectType::from(s))
}
}
impl Default for BaseObject {
fn default() -> Self {
BaseObject {
context: None,
typ: None,
id: None,
name: None,
content: None,
subject: None,
actor: None,
object: None,
attributed_to: None,
}
}
}
////////// the rest is just boring ObjectType impls and tests //////////
impl ObjectType {
pub fn is_object(&self) -> bool {
match self {
ObjectType::Object => true,
ObjectType::Article => true,
ObjectType::Audio => true,
ObjectType::Document => true,
ObjectType::Event => true,
ObjectType::Image => true,
ObjectType::Note => true,
ObjectType::Page => true,
ObjectType::Place => true,
ObjectType::Profile => true,
ObjectType::Relationship => true,
ObjectType::Tombstone => true,
ObjectType::Video => true,
_ => false,
}
}
pub fn is_link(&self) -> bool {
match self {
ObjectType::Link => true,
ObjectType::Mention => true,
_ => false,
}
}
pub fn is_activity(&self) -> bool {
match self {
ObjectType::Activity => true,
ObjectType::Accept => true,
ObjectType::Add => true,
ObjectType::Announce => true,
ObjectType::Arrive => true,
ObjectType::Block => true,
ObjectType::Create => true,
ObjectType::Delete => true,
ObjectType::Dislike => true,
ObjectType::EmojiReact => true,
ObjectType::Flag => true,
ObjectType::Follow => true,
ObjectType::Ignore => true,
ObjectType::Invite => true,
ObjectType::Join => true,
ObjectType::Leave => true,
ObjectType::Like => true,
ObjectType::Listen => true,
ObjectType::Move => true,
ObjectType::Offer => true,
ObjectType::Question => true,
ObjectType::Reject => true,
ObjectType::Read => true,
ObjectType::Remove => true,
ObjectType::TentativeReject => true,
ObjectType::TentativeAccept => true,
ObjectType::Travel => true,
ObjectType::Undo => true,
ObjectType::Update => true,
ObjectType::View => true,
_ => false,
}
}
pub fn is_actor(&self) -> bool {
match self {
ObjectType::Actor => true,
ObjectType::Application => true,
ObjectType::Group => true,
ObjectType::Organization => true,
ObjectType::Person => true,
ObjectType::Service => true,
_ => false,
}
}
pub fn as_str(&self) -> &str {
match self {
ObjectType::Unknown(s) => s.as_str(),
// Object
ObjectType::Object => "Object",
ObjectType::Article => "Article",
ObjectType::Audio => "Audio",
ObjectType::Document => "Document",
ObjectType::Event => "Event",
ObjectType::Image => "Image",
ObjectType::Note => "Note",
ObjectType::Page => "Page",
ObjectType::Place => "Place",
ObjectType::Profile => "Profile",
ObjectType::Relationship => "Relationship",
ObjectType::Tombstone => "Tombstone",
ObjectType::Video => "Video",
// Link
ObjectType::Link => "Link",
ObjectType::Mention => "Mention",
// Activity
ObjectType::Activity => "Activity",
ObjectType::Accept => "Accept",
ObjectType::Add => "Add",
ObjectType::Announce => "Announce",
ObjectType::Arrive => "Arrive",
ObjectType::Block => "Block",
ObjectType::Create => "Create",
ObjectType::Delete => "Delete",
ObjectType::Dislike => "Dislike",
ObjectType::EmojiReact => "EmojiReact",
ObjectType::Flag => "Flag",
ObjectType::Follow => "Follow",
ObjectType::Ignore => "Ignore",
ObjectType::Invite => "Invite",
ObjectType::Join => "Join",
ObjectType::Leave => "Leave",
ObjectType::Like => "Like",
ObjectType::Listen => "Listen",
ObjectType::Move => "Move",
ObjectType::Offer => "Offer",
ObjectType::Question => "Question",
ObjectType::Reject => "Reject",
ObjectType::Read => "Read",
ObjectType::Remove => "Remove",
ObjectType::TentativeReject => "TentativeReject",
ObjectType::TentativeAccept => "TentativeAccept",
ObjectType::Travel => "Travel",
ObjectType::Undo => "Undo",
ObjectType::Update => "Update",
ObjectType::View => "View",
// Actor
ObjectType::Actor => "Actor",
ObjectType::Application => "Application",
ObjectType::Group => "Group",
ObjectType::Organization => "Organization",
ObjectType::Person => "Person",
ObjectType::Service => "Service",
}
}
}
impl From<&str> for ObjectType {
fn from(s: &str) -> ObjectType {
match s {
// Object
"Object" => ObjectType::Object,
"Article" => ObjectType::Article,
"Audio" => ObjectType::Audio,
"Document" => ObjectType::Document,
"Event" => ObjectType::Event,
"Image" => ObjectType::Image,
"Note" => ObjectType::Note,
"Page" => ObjectType::Page,
"Place" => ObjectType::Place,
"Profile" => ObjectType::Profile,
"Relationship" => ObjectType::Relationship,
"Tombstone" => ObjectType::Tombstone,
"Video" => ObjectType::Video,
// Link
"Link" => ObjectType::Link,
"Mention" => ObjectType::Mention,
// Activity
"Activity" => ObjectType::Activity,
"Accept" => ObjectType::Accept,
"Add" => ObjectType::Add,
"Announce" => ObjectType::Announce,
"Arrive" => ObjectType::Arrive,
"Block" => ObjectType::Block,
"Create" => ObjectType::Create,
"Delete" => ObjectType::Delete,
"Dislike" => ObjectType::Dislike,
"EmojiReact" => ObjectType::EmojiReact,
"Flag" => ObjectType::Flag,
"Follow" => ObjectType::Follow,
"Ignore" => ObjectType::Ignore,
"Invite" => ObjectType::Invite,
"Join" => ObjectType::Join,
"Leave" => ObjectType::Leave,
"Like" => ObjectType::Like,
"Listen" => ObjectType::Listen,
"Move" => ObjectType::Move,
"Offer" => ObjectType::Offer,
"Question" => ObjectType::Question,
"Reject" => ObjectType::Reject,
"Read" => ObjectType::Read,
"Remove" => ObjectType::Remove,
"TentativeReject" => ObjectType::TentativeReject,
"TentativeAccept" => ObjectType::TentativeAccept,
"Travel" => ObjectType::Travel,
"Undo" => ObjectType::Undo,
"Update" => ObjectType::Update,
"View" => ObjectType::View,
// Actor
"Actor" => ObjectType::Actor,
"Application" => ObjectType::Application,
"Group" => ObjectType::Group,
"Organization" => ObjectType::Organization,
"Person" => ObjectType::Person,
"Service" => ObjectType::Service,
// Unknown
_ => ObjectType::Unknown(String::from(s)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_test::{assert_tokens, Token};
#[test]
fn indirect() {
let obj = Object::Indirect(String::from("https://example.org/notes/1"));
assert_tokens(&obj, &[Token::String("https://example.org/notes/1")]);
}
#[test]
fn empty() {
let obj = Object::Direct(BaseObject::default());
assert_tokens(&obj, &[Token::Map { len: None }, Token::MapEnd]);
}
#[test]
fn basic() {
let obj = Object::Direct(BaseObject {
context: Some(Context::default()),
typ: Some(ObjectType::Object),
id: Some(ObjectId(String::from("https://www.example.com/object/1"))),
name: Some(String::from("A Simple, non-specific object")),
..Default::default()
});
assert_tokens(
&obj,
&[
Token::Map { len: None },
Token::String("@context"),
Token::String(uri::ACTIVITY_STREAMS),
Token::String("type"),
Token::String("Object"),
Token::String("id"),
Token::String("https://www.example.com/object/1"),
Token::String("name"),
Token::String("A Simple, non-specific object"),
Token::MapEnd,
],
);
}
#[test]
fn full() {
let obj = Object::Direct(BaseObject {
context: Some(Context::default()),
typ: Some(ObjectType::Create),
id: Some(ObjectId(String::from("https://www.example.org/object/2"))),
name: None,
content: None,
subject: Some(Box::new(Object::Indirect(String::from(
"https://www.example.org/actor/1",
)))),
actor: Some(Box::new(Object::Direct(BaseObject {
typ: Some(ObjectType::Person),
name: Some(String::from("Alice")),
..Default::default()
}))),
object: Some(Box::new(Object::Direct(BaseObject {
typ: Some(ObjectType::Note),
content: Some(String::from("hi i'm gay uwu")),
..Default::default()
}))),
attributed_to: Some(Box::new(Object::Multi(vec![
Object::Indirect(String::from("https://www.example.org/actor/2")),
Object::Direct(BaseObject {
typ: Some(ObjectType::Person),
name: Some(String::from("Bob")),
..Default::default()
}),
]))),
});
assert_tokens(
&obj,
&[
Token::Map { len: None },
Token::String("@context"),
Token::String(uri::ACTIVITY_STREAMS),
Token::String("type"),
Token::String("Create"),
Token::String("id"),
Token::String("https://www.example.org/object/2"),
Token::String("subject"),
Token::String("https://www.example.org/actor/1"),
Token::String("actor"),
Token::Map { len: None },
Token::String("type"),
Token::String("Person"),
Token::String("name"),
Token::String("Alice"),
Token::MapEnd,
Token::String("object"),
Token::Map { len: None },
Token::String("type"),
Token::String("Note"),
Token::String("content"),
Token::String("hi i'm gay uwu"),
Token::MapEnd,
Token::String("attributedTo"),
Token::Seq { len: Some(2) },
Token::String("https://www.example.org/actor/2"),
Token::Map { len: None },
Token::String("type"),
Token::String("Person"),
Token::String("name"),
Token::String("Bob"),
Token::MapEnd,
Token::SeqEnd,
Token::MapEnd,
],
);
}
}

@ -2,28 +2,32 @@ use json_ld::{
syntax::{Parse, Value},
Expand, IndexedObject, RemoteDocument,
};
use locspan::Span;
use locspan::{Meta, Span};
use mime::Mime;
use rdf_types::vocabulary::BlankIdIndex;
use rdf_types::{vocabulary::IriIndex, IndexVocabulary, Vocabulary};
use crate::ap::{loader::CachedLoader, vocab::Ids};
use crate::ap::{
loader::CachedLoader,
trans::{ApDocument, ParseApub},
vocab::Ids,
};
use crate::core::*;
use crate::state::AppState;
/// Main API for handling ActivityPub ingress, called by [`crate::job::inbox::InboxWorker`].
pub async fn process_document(state: &AppState, raw: &str) -> Result<()> {
pub async fn process_document(state: &AppState, raw: &str, mime: &Mime) -> Result<()> {
let mut vocab: IndexVocabulary = IndexVocabulary::new();
let indices = Ids::populate(&mut vocab);
let document = Value::parse_str(raw, |span| span)
.map_err(|e| Error::MalformedApub(format!("Could not parse document: {e}")))?;
let json = preprocess(raw)?;
let rd = RemoteDocument::new(None, Some(mime.clone()), json);
let rd = RemoteDocument::new(
None,
Some("application/activity+json".parse().unwrap()),
document,
);
let mut loader = CachedLoader::new(state.clone());
let mut loader = CachedLoader::new_with(state.clone(), move |_vocab, _iri, bytes| {
let content = std::str::from_utf8(bytes.as_ref())
.map_err(|e| Error::MalformedApub(format!("Invalid encoding: {e}")))?;
preprocess(&content)
});
let rd = rd.expand_with(&mut vocab, &mut loader).await.unwrap();
let vocab = vocab;
@ -37,42 +41,20 @@ pub async fn process_document(state: &AppState, raw: &str) -> Result<()> {
Ok(())
}
fn preprocess(raw: &str) -> Result<Meta<Value<Span>, Span>> {
json_ld::syntax::Value::parse_str(raw, |span| span)
.map_err(|e| Error::MalformedApub(format!("{e}")))
}
async fn process_object(
obj: IndexedObject<IriIndex, BlankIdIndex, Span>,
vocab: &impl Vocabulary<Iri = IriIndex>,
ids: &Ids,
) -> Result<()> {
let id = obj
.id()
.and_then(|id| id.as_iri())
.and_then(|index| vocab.iri(index))
.ok_or_else(apub_err("Object does not have an id"))?;
let typ = obj
.types()
.next()
.ok_or_else(apub_err("Object does not have a type"))?
.as_iri()
.and_then(|index| vocab.iri(index))
.ok_or_else(apub_err("Object has invalid type"))?;
// just for testing: extract and print `object.content`
// to see if the expansion actually works as expected
let properties = obj
.as_node()
.ok_or_else(apub_err("Object is not a node"))?
.properties();
let object = properties
.get_any(&ids.apub.property.object)
.and_then(|prop| prop.as_node())
.and_then(|node| node.properties().get_any(&ids.apub.property.content))
.and_then(|prop| prop.as_value())
.and_then(|val| val.as_str())
.unwrap();
debug!("content = \"{object}\"");
let document = ApDocument::parse_apub(&obj, vocab, ids);
if let Some(doc) = document {
debug!("\nParsed document:\n{doc:?}");
}
Ok(())
}
const fn apub_err(msg: impl Into<String>) -> impl FnOnce() -> Error {
move || Error::MalformedApub(msg.into())
}

@ -0,0 +1,303 @@
use chrono::NaiveDateTime;
use rdf_types::vocabulary::IriIndex;
use rdf_types::Vocabulary;
use std::fmt;
use crate::ap::trans::{
matches_type, AbstractObject, ApDocument, DebugApub, ParseApub, PropHelper, RawObject,
};
use crate::ap::vocab::Ids;
pub struct AbstractActivity {
_super: AbstractObject,
pub actor: Vec<ApDocument>,
pub object: Vec<ApDocument>,
pub target: Option<Box<ApDocument>>,
pub origin: Option<Box<ApDocument>>,
pub instrument: Vec<ApDocument>,
}
ap_extends!(AbstractActivity, AbstractObject);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for AbstractActivity {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.activity)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
AbstractObject::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
actor: ph.parse_prop_vec(&prop_ids.actor),
object: ph.parse_prop_vec(&prop_ids.object),
target: ph.parse_prop_box(&prop_ids.target),
origin: ph.parse_prop_box(&prop_ids.origin),
instrument: ph.parse_prop_vec(&prop_ids.instrument),
})
}
}
impl DebugApub for AbstractActivity {
fn apub_class_name(&self) -> &str {
"Activity"
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop_vec!(self, f, actor, depth)?;
ap_display_prop_vec!(self, f, object, depth)?;
ap_display_prop_box!(self, f, target, depth)?;
ap_display_prop_box!(self, f, origin, depth)?;
ap_display_prop_vec!(self, f, instrument, depth)
}
}
pub enum Activity {
Accept(Accept),
Add(Add),
Announce(Announce),
Arrive(Arrive),
Block(Block),
Create(Create),
Delete(Delete),
Dislike(Dislike),
Flag(Flag),
Follow(Follow),
Ignore(Ignore),
Invite(Invite),
Join(Join),
Leave(Leave),
Like(Like),
Listen(Listen),
Move(Move),
Offer(Offer),
Question(Question),
Reject(Reject),
Read(Read),
Remove(Remove),
TentativeReject(TentativeReject),
TentativeAccept(TentativeAccept),
Travel(Travel),
Undo(Undo),
Update(Update),
View(View),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Activity {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
Accept::parse_apub(obj, vocab, ids)
.map(Self::Accept)
.or_else(|| Add::parse_apub(obj, vocab, ids).map(Self::Add))
.or_else(|| Announce::parse_apub(obj, vocab, ids).map(Self::Announce))
.or_else(|| Arrive::parse_apub(obj, vocab, ids).map(Self::Arrive))
.or_else(|| Block::parse_apub(obj, vocab, ids).map(Self::Block))
.or_else(|| Create::parse_apub(obj, vocab, ids).map(Self::Create))
.or_else(|| Delete::parse_apub(obj, vocab, ids).map(Self::Delete))
.or_else(|| Dislike::parse_apub(obj, vocab, ids).map(Self::Dislike))
.or_else(|| Flag::parse_apub(obj, vocab, ids).map(Self::Flag))
.or_else(|| Follow::parse_apub(obj, vocab, ids).map(Self::Follow))
.or_else(|| Ignore::parse_apub(obj, vocab, ids).map(Self::Ignore))
.or_else(|| Invite::parse_apub(obj, vocab, ids).map(Self::Invite))
.or_else(|| Join::parse_apub(obj, vocab, ids).map(Self::Join))
.or_else(|| Leave::parse_apub(obj, vocab, ids).map(Self::Leave))
.or_else(|| Like::parse_apub(obj, vocab, ids).map(Self::Like))
.or_else(|| Listen::parse_apub(obj, vocab, ids).map(Self::Listen))
.or_else(|| Move::parse_apub(obj, vocab, ids).map(Self::Move))
.or_else(|| Offer::parse_apub(obj, vocab, ids).map(Self::Offer))
.or_else(|| Question::parse_apub(obj, vocab, ids).map(Self::Question))
.or_else(|| Reject::parse_apub(obj, vocab, ids).map(Self::Reject))
.or_else(|| Read::parse_apub(obj, vocab, ids).map(Self::Read))
.or_else(|| Remove::parse_apub(obj, vocab, ids).map(Self::Remove))
.or_else(|| TentativeReject::parse_apub(obj, vocab, ids).map(Self::TentativeReject))
.or_else(|| TentativeAccept::parse_apub(obj, vocab, ids).map(Self::TentativeAccept))
.or_else(|| Travel::parse_apub(obj, vocab, ids).map(Self::Travel))
.or_else(|| Undo::parse_apub(obj, vocab, ids).map(Self::Undo))
.or_else(|| Update::parse_apub(obj, vocab, ids).map(Self::Update))
.or_else(|| View::parse_apub(obj, vocab, ids).map(Self::View))
}
}
impl DebugApub for Activity {
fn apub_class_name(&self) -> &str {
use Activity::*;
match self {
Accept(a) => a.apub_class_name(),
Add(a) => a.apub_class_name(),
Announce(a) => a.apub_class_name(),
Arrive(a) => a.apub_class_name(),
Block(b) => b.apub_class_name(),
Create(c) => c.apub_class_name(),
Delete(d) => d.apub_class_name(),
Dislike(d) => d.apub_class_name(),
Flag(f) => f.apub_class_name(),
Follow(f) => f.apub_class_name(),
Ignore(i) => i.apub_class_name(),
Invite(i) => i.apub_class_name(),
Join(j) => j.apub_class_name(),
Leave(l) => l.apub_class_name(),
Like(l) => l.apub_class_name(),
Listen(l) => l.apub_class_name(),
Move(m) => m.apub_class_name(),
Offer(o) => o.apub_class_name(),
Question(q) => q.apub_class_name(),
Reject(r) => r.apub_class_name(),
Read(r) => r.apub_class_name(),
Remove(r) => r.apub_class_name(),
TentativeReject(t) => t.apub_class_name(),
TentativeAccept(t) => t.apub_class_name(),
Travel(t) => t.apub_class_name(),
Undo(u) => u.apub_class_name(),
Update(u) => u.apub_class_name(),
View(v) => v.apub_class_name(),
}
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use Activity::*;
match self {
Accept(a) => a.debug_apub(f, depth),
Add(a) => a.debug_apub(f, depth),
Announce(a) => a.debug_apub(f, depth),
Arrive(a) => a.debug_apub(f, depth),
Block(b) => b.debug_apub(f, depth),
Create(c) => c.debug_apub(f, depth),
Delete(d) => d.debug_apub(f, depth),
Dislike(d) => d.debug_apub(f, depth),
Flag(flag) => flag.debug_apub(f, depth),
Follow(follow) => follow.debug_apub(f, depth),
Ignore(i) => i.debug_apub(f, depth),
Invite(i) => i.debug_apub(f, depth),
Join(j) => j.debug_apub(f, depth),
Leave(l) => l.debug_apub(f, depth),
Like(l) => l.debug_apub(f, depth),
Listen(l) => l.debug_apub(f, depth),
Move(m) => m.debug_apub(f, depth),
Offer(o) => o.debug_apub(f, depth),
Question(q) => q.debug_apub(f, depth),
Reject(r) => r.debug_apub(f, depth),
Read(r) => r.debug_apub(f, depth),
Remove(r) => r.debug_apub(f, depth),
TentativeReject(t) => t.debug_apub(f, depth),
TentativeAccept(t) => t.debug_apub(f, depth),
Travel(t) => t.debug_apub(f, depth),
Undo(u) => u.debug_apub(f, depth),
Update(u) => u.debug_apub(f, depth),
View(v) => v.debug_apub(f, depth),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use Activity::*;
match self {
Accept(a) => a.debug_apub_members(f, depth),
Add(a) => a.debug_apub_members(f, depth),
Announce(a) => a.debug_apub_members(f, depth),
Arrive(a) => a.debug_apub_members(f, depth),
Block(b) => b.debug_apub_members(f, depth),
Create(c) => c.debug_apub_members(f, depth),
Delete(d) => d.debug_apub_members(f, depth),
Dislike(d) => d.debug_apub_members(f, depth),
Flag(flag) => flag.debug_apub_members(f, depth),
Follow(follow) => follow.debug_apub_members(f, depth),
Ignore(i) => i.debug_apub_members(f, depth),
Invite(i) => i.debug_apub_members(f, depth),
Join(j) => j.debug_apub_members(f, depth),
Leave(l) => l.debug_apub_members(f, depth),
Like(l) => l.debug_apub_members(f, depth),
Listen(l) => l.debug_apub_members(f, depth),
Move(m) => m.debug_apub_members(f, depth),
Offer(o) => o.debug_apub_members(f, depth),
Question(q) => q.debug_apub_members(f, depth),
Reject(r) => r.debug_apub_members(f, depth),
Read(r) => r.debug_apub_members(f, depth),
Remove(r) => r.debug_apub_members(f, depth),
TentativeReject(t) => t.debug_apub_members(f, depth),
TentativeAccept(t) => t.debug_apub_members(f, depth),
Travel(t) => t.debug_apub_members(f, depth),
Undo(u) => u.debug_apub_members(f, depth),
Update(u) => u.debug_apub_members(f, depth),
View(v) => v.debug_apub_members(f, depth),
}
}
}
pub struct Question {
_super: AbstractActivity,
pub one_of: Vec<ApDocument>,
pub any_of: Vec<ApDocument>,
pub closed: Option<NaiveDateTime>,
}
ap_extends!(Question, AbstractActivity);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Question {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.question)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
AbstractActivity::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
one_of: ph.parse_prop_vec(&prop_ids.one_of),
any_of: ph.parse_prop_vec(&prop_ids.any_of),
closed: ph.parse_prop(&prop_ids.closed),
})
}
}
impl DebugApub for Question {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop_vec!(self, f, one_of, depth)?;
ap_display_prop_vec!(self, f, any_of, depth)?;
ap_display_prop!(self, f, closed, depth)
}
}
ap_empty_child_impl!(Accept, AbstractActivity, apub, activity, accept);
ap_empty_child_impl!(Add, AbstractActivity, apub, activity, add);
ap_empty_child_impl!(Announce, AbstractActivity, apub, activity, announce);
ap_empty_child_impl!(Arrive, AbstractActivity, apub, activity, arrive);
ap_empty_child_impl!(Block, Ignore, apub, activity, block);
ap_empty_child_impl!(Create, AbstractActivity, apub, activity, create);
ap_empty_child_impl!(Delete, AbstractActivity, apub, activity, delete);
ap_empty_child_impl!(Dislike, AbstractActivity, apub, activity, dislike);
ap_empty_child_impl!(Flag, AbstractActivity, apub, activity, flag);
ap_empty_child_impl!(Follow, AbstractActivity, apub, activity, follow);
ap_empty_child_impl!(Ignore, AbstractActivity, apub, activity, ignore);
ap_empty_child_impl!(Invite, AbstractActivity, apub, activity, invite);
ap_empty_child_impl!(Join, AbstractActivity, apub, activity, join);
ap_empty_child_impl!(Leave, AbstractActivity, apub, activity, leave);
ap_empty_child_impl!(Like, AbstractActivity, apub, activity, like);
ap_empty_child_impl!(Listen, AbstractActivity, apub, activity, listen);
ap_empty_child_impl!(Move, AbstractActivity, apub, activity, mov);
ap_empty_child_impl!(Offer, AbstractActivity, apub, activity, offer);
ap_empty_child_impl!(Reject, AbstractActivity, apub, activity, reject);
ap_empty_child_impl!(Read, AbstractActivity, apub, activity, read);
ap_empty_child_impl!(Remove, AbstractActivity, apub, activity, remove);
ap_empty_child_impl!(
TentativeReject,
AbstractActivity,
apub,
activity,
tentative_reject
);
ap_empty_child_impl!(
TentativeAccept,
AbstractActivity,
apub,
activity,
tentative_accept
);
ap_empty_child_impl!(Travel, AbstractActivity, apub, activity, travel);
ap_empty_child_impl!(Undo, AbstractActivity, apub, activity, undo);
ap_empty_child_impl!(Update, AbstractActivity, apub, activity, update);
ap_empty_child_impl!(View, AbstractActivity, apub, activity, view);

@ -0,0 +1,124 @@
use iref::IriBuf;
use rdf_types::vocabulary::IriIndex;
use rdf_types::Vocabulary;
use std::fmt;
use crate::ap::trans::{AbstractObject, ApDocument, DebugApub, ParseApub, PropHelper, RawObject};
use crate::ap::vocab::Ids;
pub struct AbstractActor {
_super: AbstractObject,
inbox: Option<IriBuf>,
outbox: Option<IriBuf>,
following: Option<IriBuf>,
followers: Option<IriBuf>,
liked: Option<IriBuf>,
preferred_username: Option<String>,
// Mastodon extensions
featured: Option<Box<ApDocument>>,
featured_tags: Option<Box<ApDocument>>,
discoverable: Option<bool>,
suspended: Option<bool>,
}
ap_extends!(AbstractActor, AbstractObject);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for AbstractActor {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let result = unsafe { AbstractObject::_parse_apub_unchecked(obj, vocab, ids) };
result.map(|s| Self {
_super: s,
inbox: ph.parse_prop(&ids.apub.property.inbox),
outbox: ph.parse_prop(&ids.apub.property.outbox),
following: ph.parse_prop(&ids.apub.property.following),
followers: ph.parse_prop(&ids.apub.property.followers),
liked: ph.parse_prop(&ids.apub.property.liked),
preferred_username: ph.parse_prop(&ids.apub.property.preferred_username),
featured: ph.parse_prop_box(&ids.toot.props.featured),
featured_tags: ph.parse_prop_box(&ids.toot.props.featured_tags),
discoverable: ph.parse_prop(&ids.toot.props.discoverable),
suspended: ph.parse_prop(&ids.toot.props.suspended),
})
}
}
impl DebugApub for AbstractActor {
fn apub_class_name(&self) -> &str {
"Actor"
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop!(self, f, inbox, depth)?;
ap_display_prop!(self, f, outbox, depth)?;
ap_display_prop!(self, f, following, depth)?;
ap_display_prop!(self, f, followers, depth)?;
ap_display_prop!(self, f, liked, depth)?;
ap_display_prop!(self, f, preferred_username, depth)?;
ap_display_prop_box!(self, f, featured, depth)?;
ap_display_prop_box!(self, f, featured_tags, depth)?;
ap_display_prop!(self, f, discoverable, depth)?;
ap_display_prop!(self, f, suspended, depth)
}
}
pub enum Actor {
Application(Application),
Group(Group),
Organization(Organization),
Person(Person),
Service(Service),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Actor {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
Application::parse_apub(obj, vocab, ids)
.map(Self::Application)
.or_else(|| Group::parse_apub(obj, vocab, ids).map(Self::Group))
.or_else(|| Organization::parse_apub(obj, vocab, ids).map(Self::Organization))
.or_else(|| Person::parse_apub(obj, vocab, ids).map(Self::Person))
.or_else(|| Service::parse_apub(obj, vocab, ids).map(Self::Service))
}
}
impl DebugApub for Actor {
fn apub_class_name(&self) -> &str {
use Actor::*;
match self {
Application(a) => a.apub_class_name(),
Group(g) => g.apub_class_name(),
Organization(o) => o.apub_class_name(),
Person(p) => p.apub_class_name(),
Service(s) => s.apub_class_name(),
}
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use Actor::*;
match self {
Application(a) => a.debug_apub(f, depth),
Group(g) => g.debug_apub(f, depth),
Organization(o) => o.debug_apub(f, depth),
Person(p) => p.debug_apub(f, depth),
Service(s) => s.debug_apub(f, depth),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use Actor::*;
match self {
Application(a) => a.debug_apub_members(f, depth),
Group(g) => g.debug_apub_members(f, depth),
Organization(o) => o.debug_apub_members(f, depth),
Person(p) => p.debug_apub_members(f, depth),
Service(s) => s.debug_apub_members(f, depth),
}
}
}
ap_empty_child_impl!(Application, AbstractActor, apub, object, application);
ap_empty_child_impl!(Group, AbstractActor, apub, object, group);
ap_empty_child_impl!(Organization, AbstractActor, apub, object, organization);
ap_empty_child_impl!(Person, AbstractActor, apub, object, person);
ap_empty_child_impl!(Service, AbstractActor, apub, object, service);

@ -0,0 +1,103 @@
use iref::IriBuf;
use mime::Mime;
use rdf_types::vocabulary::IriIndex;
use rdf_types::Vocabulary;
use std::fmt;
use crate::ap::trans::{matches_type, ApDocument, DebugApub, ParseApub, PropHelper, RawObject};
use crate::ap::vocab::Ids;
pub enum Link {
Link(BaseLink),
Mention(Mention),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Link {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
BaseLink::parse_apub(obj, vocab, ids)
.map(Self::Link)
.or_else(|| Mention::parse_apub(obj, vocab, ids).map(Self::Mention))
}
}
impl DebugApub for Link {
fn apub_class_name(&self) -> &str {
match self {
Link::Link(link) => link.apub_class_name(),
Link::Mention(mention) => mention.apub_class_name(),
}
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
match self {
Link::Link(link) => link.debug_apub(f, depth),
Link::Mention(mention) => mention.debug_apub(f, depth),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
match self {
Link::Link(link) => link.debug_apub_members(f, depth),
Link::Mention(mention) => mention.debug_apub_members(f, depth),
}
}
}
pub struct BaseLink {
pub id: Option<IriBuf>,
pub href: Option<String>,
pub rel: Option<String>,
pub media_type: Option<Mime>,
pub name: Option<String>, // TODO: this could be a langString
pub hreflang: Option<String>,
pub height: Option<u32>,
pub width: Option<u32>,
pub preview: Option<Box<ApDocument>>,
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for BaseLink {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.link.link)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
Some(Self {
id: obj
.id()
.and_then(|id| id.as_iri())
.and_then(|idx| vocab.iri(idx))
.map(|iri| iri.to_owned()),
href: ph.parse_prop(&prop_ids.href),
rel: ph.parse_prop(&prop_ids.rel),
media_type: ph.parse_prop(&prop_ids.media_type),
name: ph.parse_prop(&prop_ids.name),
hreflang: ph.parse_prop(&prop_ids.hreflang),
height: ph.parse_prop(&prop_ids.height),
width: ph.parse_prop(&prop_ids.width),
preview: ph.parse_prop_box(&prop_ids.preview),
})
}
}
impl DebugApub for BaseLink {
fn apub_class_name(&self) -> &str {
"Link"
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
ap_display_prop!(self, f, href, depth)?;
ap_display_prop!(self, f, rel, depth)?;
ap_display_prop!(self, f, media_type, depth)?;
ap_display_prop!(self, f, name, depth)?;
ap_display_prop!(self, f, hreflang, depth)?;
ap_display_prop!(self, f, height, depth)?;
ap_display_prop!(self, f, width, depth)?;
ap_display_prop_box!(self, f, preview, depth)
}
}
ap_empty_child_impl!(Mention, BaseLink, apub, link, mention);

@ -0,0 +1,652 @@
//! A (nearly) complete model of ActivityPub's classes, with translators.
//! This is used as an intermediate representation between raw JSON-LD objects
//! as spit out by the loader, and our own internal representation of the data.
use chrono::NaiveDateTime;
use iref::IriBuf;
use json_ld::object::node::Properties;
use json_ld::IndexedObject;
use locspan::Span;
use mime::Mime;
use rdf_types::vocabulary::{BlankIdIndex, IriIndex};
use rdf_types::Vocabulary;
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use crate::ap::vocab::{Id, Ids};
use crate::core::*;
use crate::util::xsd;
pub type RawProps = Properties<IriIndex, BlankIdIndex, Span>;
pub type RawObject = IndexedObject<IriIndex, BlankIdIndex, Span>;
pub trait ParseApub<V: Vocabulary<Iri = IriIndex>>: Sized {
/// Attempt to translate a raw JSON-LD object (as in, from the `json-ld` crate)
/// to this type. Returns the parsed object on success, and the input on failure.
/// Unsupported properties SHOULD be logged but otherwise ignored.
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self>;
/// Only for internal use from subclasses, **DO NOT TOUCH**.
/// Can cause an infinite recursion loop that ends in a segfault.
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
Self::parse_apub(obj, vocab, ids)
}
}
trait DebugApub {
fn apub_class_name(&self) -> &str {
std::any::type_name::<Self>().split("::").last().unwrap()
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
f.write_str(self.apub_class_name())?;
f.write_str(" {\n")?;
self.debug_apub_members(f, depth + 1)?;
writeln!(f, "{}}}", indent(depth))
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result;
}
// just ... don't ask questions and be glad this, somehow, works
macro_rules! ap_extends {
($child:ident, $parent:ty) => {
impl ::std::ops::Deref for $child {
type Target = $parent;
fn deref(&self) -> &Self::Target {
&self._super
}
}
impl ::std::ops::DerefMut for $child {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self._super
}
}
};
}
macro_rules! ap_empty_child {
($child:ident, $parent:ty) => {
pub struct $child {
_super: $parent,
}
ap_extends!($child, $parent);
impl $crate::ap::trans::DebugApub for $child {
fn debug_apub_members(
&self,
f: &mut ::std::fmt::Formatter,
depth: usize,
) -> ::std::fmt::Result {
$crate::ap::trans::DebugApub::debug_apub_members(&self._super, f, depth)
}
}
};
}
macro_rules! ap_empty_child_impl {
($child:ident, $parent:ty, $id1:ident, $id2:ident, $id3:ident) => {
ap_empty_child!($child, $parent);
impl<V> $crate::ap::trans::ParseApub<V> for $child
where
V: ::rdf_types::Vocabulary<Iri = ::rdf_types::vocabulary::IriIndex>,
{
fn parse_apub(
obj: &$crate::ap::trans::RawObject,
vocab: &V,
ids: &$crate::ap::vocab::Ids,
) -> Option<Self> {
$crate::ap::trans::matches_type(obj, &ids.$id1.$id2.$id3)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(
obj: &$crate::ap::trans::RawObject,
vocab: &V,
ids: &$crate::ap::vocab::Ids,
) -> Option<Self> {
<$parent>::_parse_apub_unchecked(obj, vocab, ids).map(|p| Self { _super: p })
}
}
};
}
macro_rules! ap_display_prop {
($this:expr, $f:ident, $name:ident, $depth:expr) => {
$crate::ap::trans::display_prop($f, stringify!($name), $this.$name.as_ref(), $depth)
};
}
macro_rules! ap_display_prop_box {
($this:expr, $f:ident, $name:ident, $depth:expr) => {
$crate::ap::trans::display_prop($f, stringify!($name), $this.$name.as_deref(), $depth)
};
}
macro_rules! ap_display_prop_vec {
($this:expr, $f:ident, $name:ident, $depth:expr) => {
$crate::ap::trans::display_prop_vec($f, stringify!($name), $this.$name.as_slice(), $depth)
};
}
mod activity;
pub use activity::*;
mod actor;
pub use actor::*;
mod link;
pub use link::*;
mod object;
pub use object::*;
/// The top-level type for any kind of object or link
pub enum ApDocument {
Object(Object),
Link(Link),
Remote(String),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for ApDocument {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
if let Some(s) = obj.as_value().and_then(|v| v.as_str()) {
Some(Self::Remote(String::from(s)))
} else {
Object::parse_apub(obj, vocab, ids)
.map(Self::Object)
.or_else(|| Link::parse_apub(obj, vocab, ids).map(Self::Link))
}
}
}
impl DebugApub for ApDocument {
fn apub_class_name(&self) -> &str {
use ApDocument::*;
match self {
Object(o) => o.apub_class_name(),
Link(l) => l.apub_class_name(),
Remote(_) => "<remote>",
}
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use ApDocument::*;
match self {
Object(o) => o.debug_apub(f, depth),
Link(l) => l.debug_apub(f, depth),
Remote(url) => writeln!(f, "{}<remote> => {url}", indent(depth)),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use ApDocument::*;
match self {
Object(o) => o.debug_apub_members(f, depth),
Link(l) => l.debug_apub_members(f, depth),
Remote(url) => writeln!(f, "{}<remote> => {url}", indent(depth)),
}
}
}
impl fmt::Debug for ApDocument {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.debug_apub(f, 0)
}
}
pub enum ImageOrLink {
Link(Link),
Image(Image),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for ImageOrLink {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
Image::parse_apub(obj, vocab, ids)
.map(Self::Image)
.or_else(|| Link::parse_apub(obj, vocab, ids).map(Self::Link))
}
}
impl DebugApub for ImageOrLink {
fn apub_class_name(&self) -> &str {
match self {
ImageOrLink::Link(link) => link.apub_class_name(),
ImageOrLink::Image(image) => image.apub_class_name(),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
match self {
ImageOrLink::Link(link) => link.debug_apub_members(f, depth),
ImageOrLink::Image(image) => image.debug_apub_members(f, depth),
}
}
}
pub enum Collection {
Base(BaseCollection),
Ordered(OrderedCollection),
Page(CollectionPage),
OrderedPage(OrderedCollectionPage),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Collection {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
BaseCollection::parse_apub(obj, vocab, ids)
.map(Self::Base)
.or_else(|| OrderedCollection::parse_apub(obj, vocab, ids).map(Self::Ordered))
.or_else(|| CollectionPage::parse_apub(obj, vocab, ids).map(Self::Page))
.or_else(|| OrderedCollectionPage::parse_apub(obj, vocab, ids).map(Self::OrderedPage))
}
}
impl DebugApub for Collection {
fn apub_class_name(&self) -> &str {
use Collection::*;
match self {
Base(b) => b.apub_class_name(),
Ordered(o) => o.apub_class_name(),
Page(p) => p.apub_class_name(),
OrderedPage(o) => o.apub_class_name(),
}
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use Collection::*;
match self {
Base(b) => b.debug_apub(f, depth),
Ordered(o) => o.debug_apub(f, depth),
Page(p) => p.debug_apub(f, depth),
OrderedPage(o) => o.debug_apub(f, depth),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use Collection::*;
match self {
Base(b) => b.debug_apub_members(f, depth),
Ordered(o) => o.debug_apub_members(f, depth),
Page(p) => p.debug_apub_members(f, depth),
OrderedPage(o) => o.debug_apub_members(f, depth),
}
}
}
pub struct BaseCollection {
_super: AbstractObject,
pub total_items: Option<u32>,
pub current: Option<Box<CollectionPageOrLink>>,
pub first: Option<Box<CollectionPageOrLink>>,
pub last: Option<Box<CollectionPageOrLink>>,
pub items: Vec<ApDocument>,
}
ap_extends!(BaseCollection, AbstractObject);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for BaseCollection {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.collection)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
AbstractObject::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
total_items: ph.parse_prop(&prop_ids.total_items),
current: ph.parse_prop_box(&prop_ids.current),
first: ph.parse_prop_box(&prop_ids.first),
last: ph.parse_prop_box(&prop_ids.last),
items: ph.parse_prop_vec(&prop_ids.items),
})
}
}
impl DebugApub for BaseCollection {
fn apub_class_name(&self) -> &str {
"Collection"
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop!(self, f, total_items, depth)?;
ap_display_prop_box!(self, f, current, depth)?;
ap_display_prop_box!(self, f, first, depth)?;
ap_display_prop_box!(self, f, last, depth)?;
ap_display_prop_vec!(self, f, items, depth)
}
}
ap_empty_child_impl!(
OrderedCollection,
BaseCollection,
apub,
object,
ordered_collection
);
pub struct CollectionPage {
_super: BaseCollection,
pub part_of: Option<Box<CollectionOrLink>>,
pub next: Option<Box<CollectionPageOrLink>>,
pub prev: Option<Box<CollectionPageOrLink>>,
}
ap_extends!(CollectionPage, BaseCollection);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for CollectionPage {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.collection_page)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
BaseCollection::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
part_of: ph.parse_prop_box(&prop_ids.part_of),
next: ph.parse_prop_box(&prop_ids.next),
prev: ph.parse_prop_box(&prop_ids.prev),
})
}
}
impl DebugApub for CollectionPage {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop_box!(self, f, part_of, depth)?;
ap_display_prop_box!(self, f, next, depth)?;
ap_display_prop_box!(self, f, prev, depth)
}
}
pub struct OrderedCollectionPage {
_super: CollectionPage,
pub start_index: Option<u32>,
}
ap_extends!(OrderedCollectionPage, CollectionPage);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for OrderedCollectionPage {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.ordered_collection_page)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
CollectionPage::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
start_index: ph.parse_prop(&prop_ids.start_index),
})
}
}
impl DebugApub for OrderedCollectionPage {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop!(self, f, start_index, depth)
}
}
pub enum CollectionOrLink {
Collection(Collection),
Link(BaseLink),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for CollectionOrLink {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
Collection::parse_apub(obj, vocab, ids)
.map(Self::Collection)
.or_else(|| BaseLink::parse_apub(obj, vocab, ids).map(Self::Link))
}
}
impl DebugApub for CollectionOrLink {
fn apub_class_name(&self) -> &str {
use CollectionOrLink::*;
match self {
Collection(c) => c.apub_class_name(),
Link(l) => l.apub_class_name(),
}
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use CollectionOrLink::*;
match self {
Collection(c) => c.debug_apub(f, depth),
Link(l) => l.debug_apub(f, depth),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use CollectionOrLink::*;
match self {
Collection(c) => c.debug_apub_members(f, depth),
Link(l) => l.debug_apub_members(f, depth),
}
}
}
pub enum CollectionPageOrLink {
CollectionPage(CollectionPage),
Link(BaseLink),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for CollectionPageOrLink {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
CollectionPage::parse_apub(obj, vocab, ids)
.map(Self::CollectionPage)
.or_else(|| BaseLink::parse_apub(obj, vocab, ids).map(Self::Link))
}
}
impl DebugApub for CollectionPageOrLink {
fn apub_class_name(&self) -> &str {
use CollectionPageOrLink::*;
match self {
CollectionPage(c) => c.apub_class_name(),
Link(l) => l.apub_class_name(),
}
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use CollectionPageOrLink::*;
match self {
CollectionPage(c) => c.debug_apub(f, depth),
Link(l) => l.debug_apub(f, depth),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use CollectionPageOrLink::*;
match self {
CollectionPage(c) => c.debug_apub_members(f, depth),
Link(l) => l.debug_apub_members(f, depth),
}
}
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for IriBuf {
fn parse_apub(obj: &RawObject, vocab: &V, _ids: &Ids) -> Option<Self> {
vocab.iri(obj.as_iri()?).map(|iri| iri.to_owned())
}
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for xsd::Duration {
fn parse_apub(obj: &RawObject, _vocab: &V, _ids: &Ids) -> Option<Self> {
xsd::Duration::from_str(obj.as_value()?.as_str()?).ok()
}
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for NaiveDateTime {
fn parse_apub(obj: &RawObject, _vocab: &V, _ids: &Ids) -> Option<Self> {
use chrono::{DateTime, Utc};
// TODO: close enough for now, but this only supports UTC
let dt = DateTime::<Utc>::from_str(obj.as_value()?.as_str()?).ok()?;
Some(dt.naive_utc())
}
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Mime {
fn parse_apub(obj: &RawObject, _vocab: &V, _ids: &Ids) -> Option<Self> {
Mime::from_str(obj.as_value()?.as_str()?).ok()
}
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for String {
fn parse_apub(obj: &RawObject, _vocab: &V, _ids: &Ids) -> Option<Self> {
Some(obj.as_value()?.as_str()?.to_owned())
}
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for u32 {
fn parse_apub(obj: &RawObject, _vocab: &V, _ids: &Ids) -> Option<Self> {
obj.as_value()?.as_number()?.as_u32()
}
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for f32 {
fn parse_apub(obj: &RawObject, _vocab: &V, _ids: &Ids) -> Option<Self> {
Some(obj.as_value()?.as_number()?.as_f32_lossy())
}
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for bool {
fn parse_apub(obj: &RawObject, _vocab: &V, _ids: &Ids) -> Option<Self> {
obj.as_bool()
}
}
fn matches_type(obj: &RawObject, iri_id: &Id) -> Option<()> {
let iri = iri_id.as_iri().expect("IDs should only refer to IRIs");
let type_matches = obj
.types()
.any(|t| t.as_iri().is_some_and(|index| index == iri));
if type_matches {
Some(())
} else {
None
}
}
struct PropHelper<'a, V: Vocabulary<Iri = IriIndex>> {
props: &'a RawProps,
vocab: &'a V,
ids: &'a Ids,
}
impl<'a, V: Vocabulary<Iri = IriIndex>> PropHelper<'a, V> {
fn new(obj: &'a RawObject, vocab: &'a V, ids: &'a Ids) -> Option<Self> {
let props = obj.as_node()?.properties();
Some(Self { props, vocab, ids })
}
fn parse_prop<T: ParseApub<V>>(&self, prop_id: &Id) -> Option<T> {
T::parse_apub(self.props.get_any(prop_id)?, self.vocab, self.ids).or_else(|| {
let iri = prop_id
.as_iri()
.and_then(|index| self.vocab.iri(index))
.expect("predefined IRIs must always exist");
warn!("Ignoring unknown value for property {iri}");
None
})
}
fn parse_prop_box<T: ParseApub<V>>(&self, prop_id: &Id) -> Option<Box<T>> {
self.parse_prop(prop_id).map(Box::new)
}
fn parse_prop_vec<T: ParseApub<V>>(&self, prop_id: &Id) -> Vec<T> {
self.props
.get(prop_id)
.filter_map(|prop| T::parse_apub(prop, self.vocab, self.ids))
.collect()
}
}
impl DebugApub for IriBuf {
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self.debug_apub_members(f, depth)
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, _depth: usize) -> fmt::Result {
writeln!(f, "{}", self.as_str())
}
}
impl DebugApub for Mime {
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self.debug_apub_members(f, depth)
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, _depth: usize) -> fmt::Result {
writeln!(f, "{self}")
}
}
macro_rules! primitive_debug_apub_impl {
($t:ty) => {
impl DebugApub for $t {
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self.debug_apub_members(f, depth)
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, _depth: usize) -> fmt::Result {
writeln!(f, "{self:?}")
}
}
};
}
primitive_debug_apub_impl!(bool);
primitive_debug_apub_impl!(u32);
primitive_debug_apub_impl!(f32);
primitive_debug_apub_impl!(String);
primitive_debug_apub_impl!(NaiveDateTime);
primitive_debug_apub_impl!(xsd::Duration);
primitive_debug_apub_impl!(HashMap<String, String>);
fn display_prop<T>(f: &mut fmt::Formatter, name: &str, val: Option<&T>, depth: usize) -> fmt::Result
where
T: DebugApub,
{
match val {
Some(val) => {
write!(f, "{}{name}: ", indent(depth))?;
val.debug_apub(f, depth)
}
None => Ok(()),
}
}
fn display_prop_vec<T>(f: &mut fmt::Formatter, name: &str, val: &[T], depth: usize) -> fmt::Result
where
T: DebugApub,
{
match val.len() {
0 => Ok(()),
1 => display_prop(f, name, Some(&val[0]), depth),
_ => {
writeln!(f, "{}{name}: [", indent(depth))?;
for v in val {
write!(f, "{}", indent(depth))?;
v.debug_apub(f, depth + 1)?;
}
writeln!(f, "{}]", indent(depth))
}
}
}
fn indent(n: usize) -> impl fmt::Display {
" ".repeat(n)
}

@ -0,0 +1,428 @@
use chrono::NaiveDateTime;
use iref::IriBuf;
use mime::Mime;
use rdf_types::vocabulary::IriIndex;
use rdf_types::Vocabulary;
use std::fmt;
use crate::ap::trans::{
activity, actor, matches_type, ApDocument, BaseCollection, Collection, DebugApub, ImageOrLink,
ParseApub, PropHelper, RawObject,
};
use crate::ap::vocab::Ids;
use crate::util::xsd;
// The ActivityStreams vocabulary actually defines Image, Audio, Video, and Page
// as subclasses of Document. However, since Document is a child of Object and
// does not introduce any new properties, we simplify the class hierarchy by
// making all of the ones mentioned above direct subclasses of Object.
pub enum Object {
Activity(activity::Activity),
Actor(actor::Actor),
Article(Article),
Audio(Audio),
Document(Document),
Event(Event),
Image(Image),
Note(Note),
Page(Page),
Place(Place),
Profile(Profile),
Relationship(Relationship),
Tombstone(Tombstone),
Video(Video),
Collection(Collection),
// Mastodon extensions
Emoji(Emoji),
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Object {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
activity::Activity::parse_apub(obj, vocab, ids)
.map(Self::Activity)
.or_else(|| actor::Actor::parse_apub(obj, vocab, ids).map(Self::Actor))
.or_else(|| Article::parse_apub(obj, vocab, ids).map(Self::Article))
.or_else(|| Audio::parse_apub(obj, vocab, ids).map(Self::Audio))
.or_else(|| Document::parse_apub(obj, vocab, ids).map(Self::Document))
.or_else(|| Event::parse_apub(obj, vocab, ids).map(Self::Event))
.or_else(|| Image::parse_apub(obj, vocab, ids).map(Self::Image))
.or_else(|| Note::parse_apub(obj, vocab, ids).map(Self::Note))
.or_else(|| Page::parse_apub(obj, vocab, ids).map(Self::Page))
.or_else(|| Place::parse_apub(obj, vocab, ids).map(Self::Place))
.or_else(|| Profile::parse_apub(obj, vocab, ids).map(Self::Profile))
.or_else(|| Relationship::parse_apub(obj, vocab, ids).map(Self::Relationship))
.or_else(|| Tombstone::parse_apub(obj, vocab, ids).map(Self::Tombstone))
.or_else(|| Video::parse_apub(obj, vocab, ids).map(Self::Video))
.or_else(|| Collection::parse_apub(obj, vocab, ids).map(Self::Collection))
.or_else(|| Emoji::parse_apub(obj, vocab, ids).map(Self::Emoji))
}
}
impl DebugApub for Object {
fn apub_class_name(&self) -> &str {
use Object::*;
match self {
Activity(a) => a.apub_class_name(),
Actor(a) => a.apub_class_name(),
Article(a) => a.apub_class_name(),
Audio(a) => a.apub_class_name(),
Document(d) => d.apub_class_name(),
Event(e) => e.apub_class_name(),
Image(i) => i.apub_class_name(),
Note(n) => n.apub_class_name(),
Page(p) => p.apub_class_name(),
Place(p) => p.apub_class_name(),
Profile(p) => p.apub_class_name(),
Relationship(r) => r.apub_class_name(),
Tombstone(t) => t.apub_class_name(),
Video(v) => v.apub_class_name(),
Collection(c) => c.apub_class_name(),
Emoji(e) => e.apub_class_name(),
}
}
fn debug_apub(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use Object::*;
match self {
Activity(a) => a.debug_apub(f, depth),
Actor(a) => a.debug_apub(f, depth),
Article(a) => a.debug_apub(f, depth),
Audio(a) => a.debug_apub(f, depth),
Document(d) => d.debug_apub(f, depth),
Event(e) => e.debug_apub(f, depth),
Image(i) => i.debug_apub(f, depth),
Note(n) => n.debug_apub(f, depth),
Page(p) => p.debug_apub(f, depth),
Place(p) => p.debug_apub(f, depth),
Profile(p) => p.debug_apub(f, depth),
Relationship(r) => r.debug_apub(f, depth),
Tombstone(t) => t.debug_apub(f, depth),
Video(v) => v.debug_apub(f, depth),
Collection(c) => c.debug_apub(f, depth),
Emoji(e) => e.debug_apub(f, depth),
}
}
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
use Object::*;
match self {
Activity(a) => a.debug_apub_members(f, depth),
Actor(a) => a.debug_apub_members(f, depth),
Article(a) => a.debug_apub_members(f, depth),
Audio(a) => a.debug_apub_members(f, depth),
Document(d) => d.debug_apub_members(f, depth),
Event(e) => e.debug_apub_members(f, depth),
Image(i) => i.debug_apub_members(f, depth),
Note(n) => n.debug_apub_members(f, depth),
Page(p) => p.debug_apub_members(f, depth),
Place(p) => p.debug_apub_members(f, depth),
Profile(p) => p.debug_apub_members(f, depth),
Relationship(r) => r.debug_apub_members(f, depth),
Tombstone(t) => t.debug_apub_members(f, depth),
Video(v) => v.debug_apub_members(f, depth),
Collection(c) => c.debug_apub_members(f, depth),
Emoji(e) => e.debug_apub_members(f, depth),
}
}
}
pub struct AbstractObject {
pub id: Option<IriBuf>,
pub attachment: Vec<ApDocument>,
pub attributed_to: Vec<ApDocument>,
pub audience: Vec<ApDocument>,
pub content: Option<String>, // TODO: this could be a langString
pub context: Option<Box<ApDocument>>,
pub name: Option<String>, // TODO: this could be a langString
pub end_time: Option<NaiveDateTime>,
pub generator: Option<Box<ApDocument>>,
pub icon: Option<Box<ImageOrLink>>,
pub image: Option<Box<ImageOrLink>>,
pub in_reply_to: Vec<ApDocument>,
pub location: Option<Box<ApDocument>>,
pub preview: Option<Box<ApDocument>>,
pub published: Option<NaiveDateTime>,
pub replies: Option<Box<BaseCollection>>,
pub start_time: Option<NaiveDateTime>,
pub summary: Option<String>, // TODO: this could be a langString
pub tag: Option<Box<ApDocument>>,
pub updated: Option<NaiveDateTime>,
pub url: Option<IriBuf>,
pub to: Option<Box<ApDocument>>,
pub bto: Option<Box<ApDocument>>,
pub cc: Option<Box<ApDocument>>,
pub bcc: Option<Box<ApDocument>>,
pub media_type: Option<Mime>,
pub duration: Option<xsd::Duration>,
}
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for AbstractObject {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
Some(Self {
id: obj
.id()
.and_then(|id| id.as_iri())
.and_then(|idx| vocab.iri(idx))
.map(|iri| iri.to_owned()),
attachment: ph.parse_prop_vec(&prop_ids.attachment),
attributed_to: ph.parse_prop_vec(&prop_ids.attributed_to),
audience: ph.parse_prop_vec(&prop_ids.audience),
content: ph.parse_prop(&prop_ids.content),
context: ph.parse_prop_box(&prop_ids.context),
name: ph.parse_prop(&prop_ids.name),
end_time: ph.parse_prop(&prop_ids.end_time),
generator: ph.parse_prop_box(&prop_ids.generator),
icon: ph.parse_prop_box(&prop_ids.icon),
image: ph.parse_prop_box(&prop_ids.image),
in_reply_to: ph.parse_prop_vec(&prop_ids.in_reply_to),
location: ph.parse_prop_box(&prop_ids.location),
preview: ph.parse_prop_box(&prop_ids.preview),
published: ph.parse_prop(&prop_ids.published),
replies: ph.parse_prop_box(&prop_ids.replies),
start_time: ph.parse_prop(&prop_ids.start_time),
summary: ph.parse_prop(&prop_ids.summary),
tag: ph.parse_prop_box(&prop_ids.tag),
updated: ph.parse_prop(&prop_ids.updated),
url: ph.parse_prop(&prop_ids.url),
to: ph.parse_prop_box(&prop_ids.to),
bto: ph.parse_prop_box(&prop_ids.bto),
cc: ph.parse_prop_box(&prop_ids.cc),
bcc: ph.parse_prop_box(&prop_ids.bcc),
media_type: ph.parse_prop(&prop_ids.media_type),
duration: ph.parse_prop(&prop_ids.duration),
})
}
}
impl DebugApub for AbstractObject {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
ap_display_prop!(self, f, id, depth)?;
ap_display_prop_vec!(self, f, attachment, depth)?;
ap_display_prop_vec!(self, f, attributed_to, depth)?;
ap_display_prop_vec!(self, f, audience, depth)?;
ap_display_prop!(self, f, content, depth)?;
ap_display_prop_box!(self, f, context, depth)?;
ap_display_prop!(self, f, name, depth)?;
ap_display_prop!(self, f, end_time, depth)?;
ap_display_prop_box!(self, f, generator, depth)?;
ap_display_prop_box!(self, f, icon, depth)?;
ap_display_prop_box!(self, f, image, depth)?;
ap_display_prop_vec!(self, f, in_reply_to, depth)?;
ap_display_prop_box!(self, f, location, depth)?;
ap_display_prop_box!(self, f, preview, depth)?;
ap_display_prop!(self, f, published, depth)?;
ap_display_prop_box!(self, f, replies, depth)?;
ap_display_prop!(self, f, start_time, depth)?;
ap_display_prop!(self, f, summary, depth)?;
ap_display_prop_box!(self, f, tag, depth)?;
ap_display_prop!(self, f, updated, depth)?;
ap_display_prop!(self, f, url, depth)?;
ap_display_prop_box!(self, f, to, depth)?;
ap_display_prop_box!(self, f, bto, depth)?;
ap_display_prop_box!(self, f, cc, depth)?;
ap_display_prop_box!(self, f, bcc, depth)?;
ap_display_prop!(self, f, media_type, depth)?;
ap_display_prop!(self, f, duration, depth)
}
}
pub struct Relationship {
_super: AbstractObject,
pub subject: Option<Box<ApDocument>>,
pub object: Option<Box<ApDocument>>,
pub relationship: Option<Box<Object>>,
}
ap_extends!(Relationship, AbstractObject);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Relationship {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.relationship)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
AbstractObject::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
subject: ph.parse_prop_box(&prop_ids.subject),
object: ph.parse_prop_box(&prop_ids.object),
relationship: ph.parse_prop_box(&prop_ids.relationship),
})
}
}
impl DebugApub for Relationship {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop_box!(self, f, subject, depth)?;
ap_display_prop_box!(self, f, object, depth)?;
ap_display_prop_box!(self, f, relationship, depth)
}
}
pub struct Place {
_super: AbstractObject,
pub accuracy: Option<f32>,
pub altitude: Option<f32>,
pub latitude: Option<f32>,
pub longitude: Option<f32>,
pub radius: Option<f32>,
pub units: Option<String>,
}
ap_extends!(Place, AbstractObject);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Place {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.place)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
AbstractObject::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
accuracy: ph.parse_prop(&prop_ids.accuracy),
altitude: ph.parse_prop(&prop_ids.altitude),
latitude: ph.parse_prop(&prop_ids.latitude),
longitude: ph.parse_prop(&prop_ids.longitude),
radius: ph.parse_prop(&prop_ids.radius),
units: ph.parse_prop(&prop_ids.units),
})
}
}
impl DebugApub for Place {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop!(self, f, accuracy, depth)?;
ap_display_prop!(self, f, altitude, depth)?;
ap_display_prop!(self, f, latitude, depth)?;
ap_display_prop!(self, f, longitude, depth)?;
ap_display_prop!(self, f, radius, depth)?;
ap_display_prop!(self, f, units, depth)
}
}
pub struct Profile {
_super: AbstractObject,
pub describes: Option<Box<Object>>,
}
ap_extends!(Profile, AbstractObject);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Profile {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.profile)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
AbstractObject::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
describes: ph.parse_prop_box(&prop_ids.describes),
})
}
}
impl DebugApub for Profile {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop_box!(self, f, describes, depth)
}
}
pub struct Tombstone {
_super: AbstractObject,
pub former_type: Option<Mime>,
pub deleted: Option<NaiveDateTime>,
}
ap_extends!(Tombstone, AbstractObject);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Tombstone {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.tombstone)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let prop_ids = &ids.apub.property;
AbstractObject::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
former_type: ph.parse_prop(&prop_ids.former_type),
deleted: ph.parse_prop(&prop_ids.deleted),
})
}
}
impl DebugApub for Tombstone {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
ap_display_prop!(self, f, former_type, depth)?;
ap_display_prop!(self, f, deleted, depth)
}
}
pub struct Image {
_super: AbstractObject,
// Mastodon extensions
pub focal_point: Option<[f32; 2]>,
pub blurhash: Option<String>,
}
ap_extends!(Image, AbstractObject);
impl<V: Vocabulary<Iri = IriIndex>> ParseApub<V> for Image {
fn parse_apub(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
matches_type(obj, &ids.apub.object.image)?;
unsafe { Self::_parse_apub_unchecked(obj, vocab, ids) }
}
unsafe fn _parse_apub_unchecked(obj: &RawObject, vocab: &V, ids: &Ids) -> Option<Self> {
let ph = PropHelper::new(obj, vocab, ids)?;
let focal_point: Vec<f32> = ph.parse_prop_vec(&ids.toot.props.focal_point);
AbstractObject::_parse_apub_unchecked(obj, vocab, ids).map(|s| Self {
_super: s,
focal_point: (focal_point.len() >= 2).then(|| [focal_point[0], focal_point[1]]),
blurhash: ph.parse_prop(&ids.toot.props.blurhash),
})
}
}
impl DebugApub for Image {
fn debug_apub_members(&self, f: &mut fmt::Formatter, depth: usize) -> fmt::Result {
self._super.debug_apub_members(f, depth)?;
if let Some(focal_point) = self.focal_point {
writeln!(
f,
"{:>3$}: [{:.3}, {:.3}]",
"focal_point",
focal_point[0],
focal_point[1],
depth * 4
)?;
}
ap_display_prop!(self, f, blurhash, depth)
}
}
ap_empty_child_impl!(Article, AbstractObject, apub, object, article);
ap_empty_child_impl!(Document, AbstractObject, apub, object, document);
ap_empty_child_impl!(Audio, AbstractObject, apub, object, audio);
ap_empty_child_impl!(Video, AbstractObject, apub, object, video);
ap_empty_child_impl!(Note, AbstractObject, apub, object, note);
ap_empty_child_impl!(Page, AbstractObject, apub, object, page);
ap_empty_child_impl!(Event, AbstractObject, apub, object, event);
ap_empty_child_impl!(Emoji, AbstractObject, toot, class, emoji);

@ -143,6 +143,7 @@ pub struct ApubActivityIds {
pub mov: Id, // move
pub offer: Id,
pub read: Id,
pub remove: Id,
pub reject: Id,
pub tentative_accept: Id,
pub tentative_reject: Id,
@ -176,6 +177,7 @@ impl ApubActivityIds {
mov => iri!("https://www.w3.org/ns/activitystreams#Move"),
offer => iri!("https://www.w3.org/ns/activitystreams#Offer"),
read => iri!("https://www.w3.org/ns/activitystreams#Read"),
remove => iri!("https://www.w3.org/ns/activitystreams#Remove"),
reject => iri!("https://www.w3.org/ns/activitystreams#Reject"),
tentative_accept => iri!("https://www.w3.org/ns/activitystreams#TentativeAccept"),
tentative_reject => iri!("https://www.w3.org/ns/activitystreams#TentativeReject"),
@ -194,6 +196,8 @@ pub struct ApubLinkIds {
pub is_followed_by: Id,
pub is_contact: Id,
pub is_member: Id,
// Mastodon extension
pub hashtag: Id,
}
impl ApubLinkIds {
@ -207,6 +211,7 @@ impl ApubLinkIds {
is_followed_by => iri!("https://www.w3.org/ns/activitystreams#IsFollowedBy"),
is_contact => iri!("https://www.w3.org/ns/activitystreams#IsContact"),
is_member => iri!("https://www.w3.org/ns/activitystreams#IsMember"),
hashtag => iri!("https://www.w3.org/ns/activitystreams#Hashtag"),
}
}
}
@ -255,9 +260,7 @@ pub struct ApubPropertyIds {
pub url: Id,
pub altitude: Id,
pub content: Id,
pub content_map: Id,
pub name: Id,
pub name_map: Id,
pub downstream_duplicates: Id,
pub duration: Id,
pub end_time: Id,
@ -274,7 +277,6 @@ pub struct ApubPropertyIds {
pub start_index: Id,
pub start_time: Id,
pub summary: Id,
pub summary_map: Id,
pub total_items: Id,
pub units: Id,
pub updated: Id,
@ -357,9 +359,7 @@ impl ApubPropertyIds {
url => iri!("https://www.w3.org/ns/activitystreams#url"),
altitude => iri!("https://www.w3.org/ns/activitystreams#altitude"),
content => iri!("https://www.w3.org/ns/activitystreams#content"),
content_map => iri!("https://www.w3.org/ns/activitystreams#contentMap"),
name => iri!("https://www.w3.org/ns/activitystreams#name"),
name_map => iri!("https://www.w3.org/ns/activitystreams#nameMap"),
downstream_duplicates => iri!("https://www.w3.org/ns/activitystreams#downstreamDuplicates"),
duration => iri!("https://www.w3.org/ns/activitystreams#duration"),
end_time => iri!("https://www.w3.org/ns/activitystreams#endTime"),
@ -376,7 +376,6 @@ impl ApubPropertyIds {
start_index => iri!("https://www.w3.org/ns/activitystreams#startIndex"),
start_time => iri!("https://www.w3.org/ns/activitystreams#startTime"),
summary => iri!("https://www.w3.org/ns/activitystreams#summary"),
summary_map => iri!("https://www.w3.org/ns/activitystreams#summaryMap"),
total_items => iri!("https://www.w3.org/ns/activitystreams#totalItems"),
units => iri!("https://www.w3.org/ns/activitystreams#units"),
updated => iri!("https://www.w3.org/ns/activitystreams#updated"),
@ -391,7 +390,7 @@ impl ApubPropertyIds {
endpoints => iri!("https://www.w3.org/ns/activitystreams#endpoints"),
following => iri!("https://www.w3.org/ns/activitystreams#following"),
followers => iri!("https://www.w3.org/ns/activitystreams#followers"),
inbox => iri!("https://www.w3.org/ns/activitystreams#inbox"),
inbox => iri!("http://www.w3.org/ns/ldp#inbox"),
liked => iri!("https://www.w3.org/ns/activitystreams#liked"),
shares => iri!("https://www.w3.org/ns/activitystreams#shares"),
likes => iri!("https://www.w3.org/ns/activitystreams#likes"),

@ -1,7 +1,9 @@
use crate::ap::processor::process_document;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use mime::Mime;
use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
use super::Job;
use crate::core::*;
@ -10,18 +12,23 @@ use crate::state::AppState;
#[derive(Serialize, Deserialize)]
pub struct InboxWorker {
raw: String,
#[serde(
serialize_with = "serialize_mime",
deserialize_with = "deserialize_mime"
)]
mime: Mime,
}
impl InboxWorker {
pub fn new(raw: String) -> InboxWorker {
InboxWorker { raw }
pub fn new(raw: String, mime: Mime) -> InboxWorker {
InboxWorker { raw, mime }
}
}
#[async_trait]
impl Job for InboxWorker {
async fn perform(&self, state: &AppState) -> Result<()> {
process_document(state, &self.raw).await
process_document(state, &self.raw, &self.mime).await
}
fn name(&self) -> &'static str {
@ -34,3 +41,26 @@ impl fmt::Debug for InboxWorker {
write!(f, "ActivityPub ingress: {}", &self.raw)
}
}
fn serialize_mime<S: Serializer>(val: &Mime, ser: S) -> StdResult<S::Ok, S::Error> {
let string = format!("{val}");
ser.serialize_str(&string)
}
struct MimeVisitor;
impl<'de> Visitor<'de> for MimeVisitor {
type Value = Mime;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a valid MIME media type")
}
fn visit_str<E: de::Error>(self, val: &str) -> StdResult<Mime, E> {
Mime::from_str(val).map_err(|e| E::custom(e.to_string()))
}
}
fn deserialize_mime<'de, D: Deserializer<'de>>(de: D) -> StdResult<Mime, D::Error> {
de.deserialize_str(MimeVisitor)
}

@ -1,5 +1,7 @@
use actix_web::http::header::{ACCEPT, CONTENT_TYPE};
use actix_web::{post, web, HttpRequest, HttpResponse};
use mime::Mime;
use std::str::FromStr;
use crate::core::*;
use crate::job::inbox::InboxWorker;
@ -17,13 +19,25 @@ async fn post_inbox(body: String, request: HttpRequest, state: AppState) -> Resu
.get(CONTENT_TYPE)
.ok_or(Error::BadRequest)?
.to_str()?;
if CONTENT_TYPES.iter().all(|&typ| typ != content_type) {
let content_type = Mime::from_str(content_type).map_err(|_| Error::BadRequest)?;
let is_apub = if content_type.type_() == "application" {
match content_type.subtype().as_str() {
"activity" => content_type.suffix().map(|s| s.as_str()) == Some("json"),
"ld" => content_type.suffix().map(|s| s.as_str()) == Some("json"),
"json" => true,
_ => false,
}
} else {
false
};
if !is_apub {
return Ok(HttpResponse::UnsupportedMediaType()
.append_header((ACCEPT, "application/activity+json, application/ld+json"))
.finish());
}
state.sched.schedule(InboxWorker::new(body));
state.sched.schedule(InboxWorker::new(body, content_type));
Ok(HttpResponse::Accepted().finish())
}

Loading…
Cancel
Save