228 lines
7.6 KiB
Clojure
228 lines
7.6 KiB
Clojure
(ns dog-and-duck.quack.quack
|
|
"Validator for ActivityPub objects: if it walks like a duck, and it quacks
|
|
like a duck...
|
|
|
|
**NOTE THAT the ActivityPub spec
|
|
[says](https://www.w3.org/TR/activitypub/#obj)
|
|
|
|
> Servers SHOULD validate the content they receive to avoid content
|
|
> spoofing attacks
|
|
|
|
but in practice ActivityPub content collected in the wild bears only
|
|
a hazy relationship to the spec, so this is difficult. I suspect that
|
|
I may have to implement a `*strict*` dynamic variable, so that users can
|
|
toggle some checks off."
|
|
|
|
;;(:require [clojure.spec.alpha as s])
|
|
(:require [dog-and-duck.quack.picky :refer [filter-severity has-context?
|
|
object-faults]])
|
|
(:import [java.net URI URISyntaxException]))
|
|
|
|
;;; Copyright (C) Simon Brooke, 2022
|
|
|
|
;;; This program is free software; you can redistribute it and/or
|
|
;;; modify it under the terms of the GNU General Public License
|
|
;;; as published by the Free Software Foundation; either version 2
|
|
;;; of the License, or (at your option) any later version.
|
|
|
|
;;; This program is distributed in the hope that it will be useful,
|
|
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;;; GNU General Public License for more details.
|
|
|
|
;;; You should have received a copy of the GNU General Public License
|
|
;;; along with this program; if not, write to the Free Software
|
|
;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
(defn object?
|
|
"Returns `true` iff `x` is recognisably an ActivityStreams object.
|
|
|
|
**NOTE THAT** The ActivityStreams spec
|
|
[says](https://www.w3.org/TR/activitystreams-core/#object):
|
|
|
|
> All properties are optional (including the id and type)
|
|
|
|
But we are *just not having that*, because otherwise we're flying blind.
|
|
We *shall* reject objects lacking at least `:type`. Missing `:id` keys are
|
|
tolerable because they represent transient objects, which we expect to
|
|
handle.
|
|
|
|
**NOTE THAT** The ActivityPub spec [says](https://www.w3.org/TR/activitypub/#obj)
|
|
|
|
> Implementers SHOULD include the ActivityPub context in their object
|
|
> definitions
|
|
|
|
but in samples found in the wild they typically don't."
|
|
([x]
|
|
(and (map? x) (:type x) true))
|
|
([x severity]
|
|
(empty? (filter-severity (object-faults x) severity))))
|
|
|
|
(defn persistent-object?
|
|
"`true` iff `x` is a persistent object.
|
|
|
|
Transient objects in ActivityPub are not required to have an `id` key, but persistent
|
|
ones must have a key, and it must be an IRI (but normally a URI)."
|
|
[x]
|
|
(try
|
|
(and (object? x) (uri? (URI. (:id x))))
|
|
(catch URISyntaxException _ false)))
|
|
|
|
;; (persistent-object? {:type "test" :id "https://mastodon.scot/@barfilfarm"})
|
|
|
|
(def ^:const actor-types
|
|
"The set of types we will accept as actors.
|
|
|
|
There's an [explicit set of allowed actor types]
|
|
(https://www.w3.org/TR/activitystreams-vocabulary/#actor-types)."
|
|
#{"Application"
|
|
"Group"
|
|
"Organization"
|
|
"Person"
|
|
"Service"})
|
|
|
|
(defmacro actor-type?
|
|
"Return `true` iff the `x` is a recognised actor type, else `false`."
|
|
[^String x]
|
|
`(if (actor-types ~x) true false))
|
|
|
|
;; (actor-type? "Group")
|
|
|
|
(def ^:const verb-types
|
|
"The set of types we will accept as verbs.
|
|
|
|
There's an [explicit set of allowed verb types]
|
|
(https://www.w3.org/TR/activitystreams-vocabulary/#activity-types)."
|
|
#{"Accept" "Add" "Announce" "Arrive" "Block" "Create" "Delete" "Dislike"
|
|
"Flag" "Follow" "Ignore" "Invite" "Join" "Leave" "Like" "Listen" "Move"
|
|
"Offer" "Question" "Reject" "Read" "Remove" "TentativeAccept"
|
|
"TentativeReject" "Travel" "Undo" "Update" "View"})
|
|
|
|
(defmacro verb-type?
|
|
;; TODO: better as a macro
|
|
[^String x]
|
|
`(if (verb-types ~x) true false))
|
|
|
|
|
|
(defn actor?
|
|
"Returns `true` if `x` quacks like an actor, else false.
|
|
|
|
**NOTE THAT** [Section 4.1 of the spec]
|
|
(https://www.w3.org/TR/activitypub/#actor-objects) says explicitly that
|
|
|
|
> Actor objects MUST have, in addition to the properties mandated by 3.1 Object Identifiers, the following properties:
|
|
>
|
|
> inbox
|
|
> A reference to an [ActivityStreams] OrderedCollection comprised of all the messages received by the actor; see 5.2 Inbox.
|
|
> outbox
|
|
> An [ActivityStreams] OrderedCollection comprised of all the messages produced by the actor; see 5.1 Outbox.
|
|
|
|
However, none of the provided examples in the [activitystreams-test-documents repository]() does in fact have these properties"
|
|
[x]
|
|
(and
|
|
(object? x)
|
|
(has-context? x)
|
|
(uri? (URI. (:inbox x)))
|
|
(uri? (URI. (:outbox x)))
|
|
(actor-type? (:type x))
|
|
true))
|
|
|
|
(defn actor-or-uri?
|
|
"`true` if `x` is either a URI or an actor.
|
|
|
|
**TODO**: I need to decide about whether to reify referenced objects
|
|
before validation or after. After reification, every reference to an actor
|
|
*must be* to an actor object, but before, may only be to a URI pointing to
|
|
one."
|
|
[x]
|
|
(and
|
|
(cond (string? x) (uri? (URI. x))
|
|
:else (actor? x))
|
|
true))
|
|
|
|
(defn activity?
|
|
"`true` iff `x` quacks like an activity, else false."
|
|
[x]
|
|
(try
|
|
(and (object? x)
|
|
(has-context? x)
|
|
(string? (:summary x))
|
|
(actor-or-uri? (:actor x))
|
|
(verb-type? (:type x))
|
|
(or (object? (:object x)) (uri? (URI. (:object x))))
|
|
true)
|
|
(catch URISyntaxException _ false)))
|
|
|
|
(defn link?
|
|
"`true` iff `x` quacks like a link, else false."
|
|
[x]
|
|
(and (object? x)
|
|
(= (:type x) "Link")
|
|
(uri? (URI. (:href x)))
|
|
true))
|
|
|
|
(defn link-or-uri?
|
|
"`true` iff `x` is either a URI or a link, else false.
|
|
|
|
There are several points in the specification where e.g. the `:image`
|
|
property (if present) may be either a link or a URI."
|
|
[x]
|
|
(and
|
|
(cond (string? x) (uri? (URI. x))
|
|
:else (link? x))
|
|
true))
|
|
|
|
(defn collection?
|
|
"`true` iff `x` quacks like a collection of type `object-type`, else `false`.
|
|
|
|
With one argument, will recognise plain collections and ordered collections,
|
|
but (currently) not collection pages."
|
|
([x ^String object-type]
|
|
(let [items (or (:items x) (:orderedItems x))]
|
|
(and
|
|
(cond
|
|
(:items x) (nil? (:orderedItems x))
|
|
(:orderedItems x) (nil? (:items x)) ;; can't have both properties
|
|
(integer? (:totalItems x)) true ;; can have neither, provided it has totalItems.
|
|
:else false)
|
|
(object? x)
|
|
(= (:type x) object-type)
|
|
(if items
|
|
(and (coll? items)
|
|
(every? object? items) ;; if there are items, they must form a
|
|
;; collection of objects.
|
|
true)
|
|
true) ;; but it's OK if there aren't.
|
|
true)
|
|
;; test for totalItems not done here, because collection pages don't
|
|
;; have it.
|
|
))
|
|
([x]
|
|
(and
|
|
(or (collection? x "Collection")
|
|
(collection? x "OrderedCollection"))
|
|
(integer? (:totalItems x))
|
|
true)))
|
|
|
|
(defn unordered-collection?
|
|
"`true` iff `x` quacks like an unordered collection, else `false`."
|
|
[x]
|
|
(and (collection? x "Collection") (integer? (:totalItems x)) true))
|
|
|
|
(defn ordered-collection?
|
|
"`true` iff `x` quacks like an ordered collection, else `false`."
|
|
[x]
|
|
(and (collection? x "OrderedCollection") (integer? (:totalItems x)) true))
|
|
|
|
(defn collection-page?
|
|
"`true` iff `x` quacks like a page in a paged collection, else `false`."
|
|
[x]
|
|
(collection? x "CollectionPage"))
|
|
|
|
(defn ordered-collection-page?
|
|
"`true` iff `x` quacks like a page in an ordered paged collection, else `false`."
|
|
[x]
|
|
(collection? x "OrderedCollectionPage"))
|
|
|
|
|