1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
//! This module contains types and functions for handling ActivityPub and
//! Activity Streams 2.0 objects. Everything here centers around [`Object`], so I
//! would suggest starting there.
//!
//! ActivityPub and Activity Streams 2.0 are available under the W3C document
//! license, which requires the full text of the following notice to be viewable
//! in derivative works, so here it is:
//!
//! ## W3C Software and Document Notice and License
//!
//! This work is being provided by the copyright holders under the following
//! license.
//!
//! ### License
//!
//! By obtaining and/or copying this work, you (the licensee) agree that you
//! have read, understood, and will comply with the following terms and
//! conditions.
//!
//! Permission to copy, modify, and distribute this work, with or without
//! modification, for any purpose and without fee or royalty is hereby granted,
//! provided that you include the following on ALL copies of the work or
//! portions thereof, including modifications:
//!
//! - The full text of this NOTICE in a location viewable to users of the
//!   redistributed or derivative work.
//! - Any pre-existing intellectual property disclaimers, notices, or terms and
//!   conditions. If none exist, the W3C Software and Document Short Notice
//!   should be included.
//! - Notice of any changes or modifications, through a copyright statement on
//!   the new code or document such as "This software or document includes
//!   material copied from or derived from [title and URI of the W3C document].
//!   Copyright © \[YEAR\] W3C® (MIT, ERCIM, Keio, Beihang)."
//!
//! ### Disclaimers
//!
//! THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS
//! OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES
//! OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF
//! THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS,
//! COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
//!
//! COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR
//! CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT.
//!
//! The name and trademarks of copyright holders may NOT be used in advertising
//! or publicity pertaining to the work without specific, written prior
//! permission. Title to copyright in this work will at all times remain with
//! copyright holders.
//!
//! ### Notes
//!
//! This version:
//! <http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document>
//!
//! Previous version:
//! <http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231>
//!
//! This version makes clear that the license is applicable to both software and
//! text, by changing the name and substituting "work" for instances of
//! "software and its documentation." It moves "notice of changes or
//! modifications to the files" to the copyright notice, to make clear that the
//! license is compatible with other liberal licenses.

use once_cell::sync::OnceCell;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use time::OffsetDateTime;

pub mod activity_broker;
pub mod inbox_queue;

/// A general struct describing any\* [Activity Streams 2.0] object. See also:
/// [Activity Vocabulary].
///
/// This is simply a strongly typed version of the JSON used to describe the
/// objects specified in the standard. Extensions are not supported by design.
///
/// Most of the fields contain [`ObjectsOrLinks`] as the standard is quite loose
/// with specifying what fits in which property: almost all of them can have
/// none, one, or many values, and those values can simply refer to an object by
/// id ("links") or be the entire object, hence the very nested and recursive
/// structure of this object. In practice, these objects aren't *that*
/// sprawling, mostly recursing one or two levels deep.
///
/// All the properties are converted from snake_case to camelCase. See the [4.
/// Properties] chapter of the Activity Vocabulary for descriptions of all the
/// fields.
///
/// \* Currently not all properties of Activity Streams 2.0 objects are
/// included, as this isn't a full implementation.
///
/// [Activity Streams 2.0]: https://www.w3.org/TR/activitystreams-core/
/// [Activity Vocabulary]: https://www.w3.org/TR/activitystreams-vocabulary/
/// [4. Properties]: https://www.w3.org/TR/activitystreams-vocabulary/#properties
// TODO: Ensure all AS2 and AP properties are included.
// (And remove the above "*" once this is done.)
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Object<'a> {
    /// Serializes and deserializes to and from "@context". Renamed for Rust
    /// variable naming rules.
    #[serde(rename = "@context")]
    pub context: Option<ObjectsOrLinks<'a>>,
    /// Serializes and deserializes to and from "type". Renamed for Rust
    /// variable naming rules.
    #[serde(rename = "type")]
    pub type_: Option<Links<'a>>,
    /// This is a [`Some`] in almost all cases: only anonymous objects, such as
    /// those submitted to the server during a C2S interaction where the Object
    /// hasn't yet been properly finalized.
    pub id: Option<AnyUri<'a>>,

    // Object | Link
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attributed_to: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<LangString<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name_map: Option<LangStringMap<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<LangString<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary_map: Option<LangStringMap<'a>>,

    // Object fields
    #[serde(skip_serializing_if = "Option::is_none")]
    pub audience: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<LangString<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_map: Option<LangStringMap<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bto: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cc: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bcc: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub in_reply_to: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub published: Option<Date>,

    // Activity fields
    #[serde(skip_serializing_if = "Option::is_none")]
    pub actor: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub object: Option<ObjectOrLink<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target: Option<ObjectOrLink<'a>>,

    // Collection fields
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_items: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub current: Option<ObjectOrLink<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub first: Option<ObjectOrLink<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last: Option<ObjectOrLink<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ordered_items: Option<ObjectsOrLinks<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prev: Option<ObjectOrLink<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next: Option<ObjectOrLink<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub part_of: Option<ObjectOrLink<'a>>,

    // (ActivityPub) Actor fields
    #[serde(skip_serializing_if = "Option::is_none")]
    pub inbox: Option<Link<'a>>, // Required for Actors
    #[serde(skip_serializing_if = "Option::is_none")]
    pub outbox: Option<Link<'a>>, // Required for Actors
    #[serde(skip_serializing_if = "Option::is_none")]
    pub following: Option<Link<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub followers: Option<Link<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub liked: Option<Link<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub streams: Option<Links<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub preferred_username: Option<StringProp<'a>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub endpoints: Option<ApEndpoints<'a>>,
}

/// A serialization-wise transparent struct for holding date times, just to
/// specify that it should serialize and deserialize to and from RFC 3339 dates.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(transparent)]
pub struct Date(#[serde(with = "time::serde::rfc3339")] pub OffsetDateTime);

/// ActivityPub-defined endpoints-structure for Actors. See the "endpoints"
/// field under [4.1 Actor
/// objects](https://www.w3.org/TR/activitypub/#actor-objects).
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ApEndpoints<'a> {
    pub proxy_url: Option<AnyUri<'a>>,
    pub oauth_authorization_endpoint: Option<AnyUri<'a>>,
    pub oauth_token_endpoint: Option<AnyUri<'a>>,
    pub provide_client_key: Option<AnyUri<'a>>,
    pub sign_client_key: Option<AnyUri<'a>>,
    pub shared_inbox: Option<AnyUri<'a>>,
}

impl Object<'_> {
    /// Returns true if the `type_` field of the object is or contains the given
    /// link. Generally these are from the Activity Streams spec, so they're not
    /// fully qualified urls, e.g. `"Note"` or `"Follow"`.
    pub fn has_type(&self, link: &str) -> bool {
        match &self.type_ {
            Some(Links::Single(link_)) => link == link_,
            Some(Links::Multiple(links)) => links.iter().any(|link_| link_ == link),
            None => false,
        }
    }

    /// Returns true if the type of this object is one of the Actor types. See
    /// [3.2 Actor Types] in the Activity Vocabulary.
    ///
    /// [3.2 Actor Types]:
    ///     https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
    pub fn is_actor(&self) -> bool {
        static ACTOR_TYPE_REGEX: OnceCell<Regex> = OnceCell::new();
        let regex =
            ACTOR_TYPE_REGEX.get_or_init(|| Regex::new("^(Application|Group|Organization|Person|Service)$").unwrap());
        match &self.type_ {
            Some(Links::Single(link)) => regex.is_match(link),
            Some(Links::Multiple(links)) => links.iter().any(|link| regex.is_match(link)),
            None => false,
        }
    }

    /// Recursively calls [`ObjectOrLink::compact`] for all the [`ObjectOrLink`]
    /// and [`ObjectsOrLinks`] properties in this object.
    pub fn compact(&mut self) {
        for objs_or_links in (self.attributed_to.iter_mut())
            .chain(self.audience.iter_mut())
            .chain(self.to.iter_mut())
            .chain(self.cc.iter_mut())
            .chain(self.in_reply_to.iter_mut())
            .chain(self.actor.iter_mut())
        {
            objs_or_links.compact();
        }
        if let Some(obj_or_link) = &mut self.object {
            obj_or_link.compact();
        }
    }
}

/// Convenience function for creating the string
/// `"https://www.w3.org/ns/activitystreams"` wrapped in the necessary types for
/// the `context` field of an [`Object`].
pub fn activitystreams_context() -> Option<ObjectsOrLinks<'static>> {
    Some(ObjectsOrLinks::Single(ObjectOrLink::Link(
        "https://www.w3.org/ns/activitystreams".into(),
    )))
}

/// Convenience macro for defining functions that return a string wrapped in the
/// necessary types to fit the [`Object::type_`] field.
macro_rules! define_link {
    ($name:ident, $link:literal) => {
        /// Returns the string
        #[doc = concat!("`\"", $link, "\"`")]
        /// wrapped by [define_link].
        pub fn $name() -> Option<Links<'static>> {
            Some(Links::Single($link.into()))
        }
    };
}

define_link!(type_create, "Create");
define_link!(type_person, "Person");
define_link!(type_ordered_collection, "OrderedCollection");
define_link!(type_collection_page, "CollectionPage");

/// One or more [`Link`]s.
#[derive(Clone, Serialize, Deserialize, Debug, derive_more::Display)]
#[serde(untagged)]
pub enum Links<'a> {
    #[display(fmt = "{:?}", _0)]
    Single(Link<'a>),
    #[display(fmt = "{:?}", _0)]
    Multiple(Vec<Link<'a>>),
}

/// A string that is written in the language of the context, which may not be
/// defined. See [4.7 Natural Language
/// Values](https://www.w3.org/TR/activitystreams-core/#naturalLanguageValues)
/// in the Activity Streams 2.0 spec for more info.
pub type LangString<'a> = StringProp<'a>;
/// A map of translated strings, each one translated to the language indicated
/// in the key. See [4.7 Natural Language
/// Values](https://www.w3.org/TR/activitystreams-core/#naturalLanguageValues)
/// in the Activity Streams 2.0 spec for more info.
pub type LangStringMap<'a> = HashMap<StringProp<'a>, LangString<'a>>;
/// A string representing a resource or other relevant information. These can be
/// URLs, but sometimes just specific strings as defined in the Activity
/// Vocabulary, such as "Follow" (which refers to the Follow activity).
pub type Link<'a> = AnyUri<'a>;
/// A string containing any URI.
pub type AnyUri<'a> = StringProp<'a>;
/// A string used as the type for [`Object`] properties that do not have further
/// meaning aside from just being a string as far as these types go.
pub type StringProp<'a> = Cow<'a, str>;

/// One or more [`ObjectOrLink`]s.
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum ObjectsOrLinks<'a> {
    Single(ObjectOrLink<'a>),
    Multiple(Vec<ObjectOrLink<'a>>),
}
impl<'a> ObjectsOrLinks<'a> {
    /// Makes an ObjectOrLinks::Single that contains the given Link.
    pub fn link(link: Link<'_>) -> ObjectsOrLinks {
        ObjectsOrLinks::Single(ObjectOrLink::Link(link))
    }

    /// Returns true if this either represents an Object with the given id, or
    /// contains an Object that has the given id.
    pub fn has_id(&self, id: &str) -> bool {
        match self {
            ObjectsOrLinks::Single(o) => o.has_id(id),
            ObjectsOrLinks::Multiple(v) => v.iter().any(|o| o.has_id(id)),
        }
    }

    /// Recursively calls [`ObjectOrLink::compact`] for all the inner values of
    /// this object.
    pub fn compact(&mut self) {
        match self {
            ObjectsOrLinks::Single(obj_or_link) => obj_or_link.compact(),
            ObjectsOrLinks::Multiple(v) => {
                for obj_or_link in v {
                    obj_or_link.compact();
                }
            }
        }
    }

    /// Recursively runs the given function `f` for each `Link` contained in
    /// this.
    pub fn for_links<F: FnMut(&AnyUri<'a>)>(&self, mut f: F) {
        match self {
            ObjectsOrLinks::Single(obj_or_link) => obj_or_link.for_link(f),
            ObjectsOrLinks::Multiple(objs_or_links) => objs_or_links.iter().for_each(|obj_or_link| {
                obj_or_link.for_link(&mut f);
            }),
        }
    }

    /// Recursively runs the given function `f` for each `Link` contained in
    /// this, with mutable access to the specific [`ObjectOrLink`].
    pub fn for_objects_or_links_mut<F: FnMut(&mut ObjectOrLink)>(&mut self, mut f: F) {
        match self {
            ObjectsOrLinks::Single(obj_or_link) => f(obj_or_link),
            ObjectsOrLinks::Multiple(objs_or_links) => objs_or_links.iter_mut().for_each(|obj_or_link| {
                f(obj_or_link);
            }),
        }
    }
}

/// Either an [`Object`] or a [`Link`] which should refer to a (possibly remote)
/// [`Object`].
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum ObjectOrLink<'a> {
    Object(Box<Object<'a>>),
    Link(AnyUri<'a>),
}

impl<'a> ObjectOrLink<'a> {
    /// Compacts this to be an [`ObjectOrLink::Link`]. If this is already a
    /// Link, nothing happens, if not, the Object's id is used as the link.
    pub fn compact(&mut self) {
        if let ObjectOrLink::Object(obj) = self {
            *self = ObjectOrLink::Link(obj.id.take().unwrap())
        }
    }

    /// Returns true if this either is a link that is equal to the given id, or
    /// if this is an Object with the given id.
    pub fn has_id(&self, id: &str) -> bool {
        match self {
            ObjectOrLink::Link(link) => link == id,
            ObjectOrLink::Object(obj) => {
                if let Some(id_) = &obj.id {
                    id == id_
                } else {
                    false
                }
            }
        }
    }

    /// If this is an [`ObjectOrLink::Link`], runs the given function `f` and
    /// passes the inner link to it as a parameter.
    pub fn for_link<F: FnMut(&AnyUri<'a>)>(&self, mut f: F) {
        if let ObjectOrLink::Link(link) = self {
            f(link);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basic_note() {
        let json = r#"{"@context": "https://www.w3.org/ns/activitystreams",
        "type": "Note",
        "content": "Foo bar :)"}"#;
        serde_json::from_str::<Object>(json).unwrap();
    }

    #[test]
    fn test_example_1_actor() {
        let json = r#"{"@context": "https://www.w3.org/ns/activitystreams",
 "type": "Person",
 "id": "https://social.example/alyssa/",
 "name": "Alyssa P. Hacker",
 "preferredUsername": "alyssa",
 "summary": "Lisp enthusiast hailing from MIT",
 "inbox": "https://social.example/alyssa/inbox/",
 "outbox": "https://social.example/alyssa/outbox/",
 "followers": "https://social.example/alyssa/followers/",
 "following": "https://social.example/alyssa/following/",
 "liked": "https://social.example/alyssa/liked/"}"#;
        serde_json::from_str::<Object>(json).unwrap();
    }

    #[test]
    fn test_example_2_note() {
        let json = r#"{"@context": "https://www.w3.org/ns/activitystreams",
 "type": "Note",
 "to": ["https://chatty.example/ben/"],
 "attributedTo": "https://social.example/alyssa/",
 "content": "Say, did you finish reading that book I lent you?"}"#;
        serde_json::from_str::<Object>(json).unwrap();
    }

    #[test]
    fn test_example_3_create() {
        let json = r#"{"@context": "https://www.w3.org/ns/activitystreams",
 "type": "Create",
 "id": "https://social.example/alyssa/posts/a29a6843-9feb-4c74-a7f7-081b9c9201d3",
 "to": ["https://chatty.example/ben/"],
 "actor": "https://social.example/alyssa/",
 "object": {"type": "Note",
            "id": "https://social.example/alyssa/posts/49e2d03d-b53a-4c4c-a95c-94a6abf45a19",
            "attributedTo": "https://social.example/alyssa/",
            "to": ["https://chatty.example/ben/"],
            "content": "Say, did you finish reading that book I lent you?"}}"#;
        serde_json::from_str::<Object>(json).unwrap();
    }

    #[test]
    fn test_example_4_create() {
        let json = r#"{"@context": "https://www.w3.org/ns/activitystreams",
 "type": "Create",
 "id": "https://chatty.example/ben/p/51086",
 "to": ["https://social.example/alyssa/"],
 "actor": "https://chatty.example/ben/",
 "object": {"type": "Note",
            "id": "https://chatty.example/ben/p/51085",
            "attributedTo": "https://chatty.example/ben/",
            "to": ["https://social.example/alyssa/"],
            "inReplyTo": "https://social.example/alyssa/posts/49e2d03d-b53a-4c4c-a95c-94a6abf45a19",
            "content": "<p>Argh, yeah, sorry, I'll get it back to you tomorrow.</p>\n<p>I was reviewing the section on register machines,\nsince it's been a while since I wrote one.</p>"}}"#;
        serde_json::from_str::<Object>(json).unwrap();
    }

    #[test]
    fn test_example_5_like() {
        let json = r#"{"@context": "https://www.w3.org/ns/activitystreams",
 "type": "Like",
 "id": "https://social.example/alyssa/posts/5312e10e-5110-42e5-a09b-934882b3ecec",
 "to": ["https://chatty.example/ben/"],
 "actor": "https://social.example/alyssa/",
 "object": "https://chatty.example/ben/p/51086"}"#;
        serde_json::from_str::<Object>(json).unwrap();
    }

    #[test]
    fn test_example_6_create() {
        let json = r#"{"@context": "https://www.w3.org/ns/activitystreams",
 "type": "Create",
 "id": "https://social.example/alyssa/posts/9282e9cc-14d0-42b3-a758-d6aeca6c876b",
 "to": ["https://social.example/alyssa/followers/",
        "https://www.w3.org/ns/activitystreams#Public"],
 "actor": "https://social.example/alyssa/",
 "object": {"type": "Note",
            "id": "https://social.example/alyssa/posts/d18c55d4-8a63-4181-9745-4e6cf7938fa1",
            "attributedTo": "https://social.example/alyssa/",
            "to": ["https://social.example/alyssa/followers/",
                   "https://www.w3.org/ns/activitystreams#Public"],
            "content": "Lending books to friends is nice.  Getting them back is even nicer! :)"}}"#;
        serde_json::from_str::<Object>(json).unwrap();
    }
}