You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

653 lines
20 KiB
Rust

//! 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)
}