ap: add base object type
This commit is contained in:
parent
6a4e753069
commit
055a97a8e1
2 changed files with 618 additions and 0 deletions
|
@ -1 +1,2 @@
|
|||
pub mod context;
|
||||
pub mod object;
|
||||
|
|
617
src/ap/object.rs
Normal file
617
src/ap/object.rs
Normal file
|
@ -0,0 +1,617 @@
|
|||
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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue