Compare commits

...

2 commits

Author SHA1 Message Date
fef
055a97a8e1
ap: add base object type 2022-12-22 19:12:33 +01:00
fef
6a4e753069
util: fix type 2022-12-22 18:57:01 +01:00
3 changed files with 622 additions and 2 deletions

View file

@ -1 +1,2 @@
pub mod context;
pub mod object;

617
src/ap/object.rs Normal file
View 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,
],
);
}
}

View file

@ -137,7 +137,9 @@ impl<'de> de::Visitor<'de> for BearcapVisitor {
impl Validate for Bearcap {
fn validate(&self, builder: ResultBuilder) -> ResultBuilder {
builder.field(&self.url, "url", |f| {
f.check("URL must start with https://", |v| v.starts_with("https://"))
f.check("URL must start with https://", |v| {
v.starts_with("https://")
})
})
}
}
@ -148,7 +150,7 @@ mod tests {
use serde_test::{assert_tokens, Token};
#[test]
fn parse_vali() {
fn parse_valid() {
let bc = Bearcap::try_from("bear:?t=asdf&u=https://example.com/test")
.expect("Doesn't parse valid bearcap");
assert_eq!(bc.url, String::from("https://example.com/test"));