diff --git a/docs/cloverage/codecov.json b/docs/cloverage/codecov.json index 241fe56..8a65628 100644 --- a/docs/cloverage/codecov.json +++ b/docs/cloverage/codecov.json @@ -3,24 +3,23 @@ [null, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, 1, null, null, null, null, null, null, null, null, + null, null, null, null, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - 40, null, 0, null, 1, null, null, null, null, null, true, 4, null, - null, null, null, 1, null, null, null, null, 1, null, null, null, - null, null, true, null, null, 5, null, null, null, 1, null, true, - null, null, 1, null, null, null, null, true, null, null, 5, null, - null, 1, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, true, 2, 2, 0, 0, 0, null, null, 1, null, - null, null, null, null, null, null, 0, 0, 0, null, null, 1, null, - null, 0, 0, 0, 0, 0, 0, 0, null, null, null, 1, null, null, 0, 0, 0, - null, null, 1, null, null, null, null, null, 0, 0, 0, null, null, 1, - null, null, null, null, null, true, true, true, true, 2, 1, null, 1, - 1, true, true, 1, null, null, null, null, null, null, null, null, 0, - 0, 0, 0, null, null, 1, null, null, 0, null, 1, null, null, 0, null, - 1, null, null, 0, null, 1, null, null, 2, null, null], + null, 35, null, 35, null, 1, null, null, null, null, null, 4, null, + 4, null, 1, null, 3, null, 3, null, 1, null, null, null, null, null, + null, null, 0, 0, 0, 0, null, null, null, null, 1, null, 0, null, 0, + null, 1, null, 0, null, 0, null, 1, null, null, null, null, null, 0, + 0, 0, null, null, 1, null, null, null, null, null, true, true, true, + true, 2, 1, null, 1, 1, true, true, 1, null, null, null, null, null, + null, null, null, 0, 0, 0, 0, null, null, 1, null, null, 0, null, 1, + null, null, 0, null, 1, null, null, 0, null, 1, null, null, 2, null, + null], "dog_and_duck/utils/process.clj": - [null, 1, null, null, 1, null, null, null, null, 1, 1, 1, null, true, - null, null, null, null, null, null, null, null, null, 1], + [null, 1, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, 1, null, null, null, + null, null, null, 1, 1, 1, 1, 1, null, true, null, null, null, null, + null, null, null, null, null, 1, null, 1, null, null, null, null, + 1], "clj_activitypub/internal/http_util.clj": [null, 1, null, null, null, null, null, null, null, null, 1, 1, 1, 1, 2, 2, 1, 1, 1, null, 1, 0, 0, null, 1, null, null, null, 0], @@ -31,7 +30,7 @@ "dog_and_duck/scratch/parser.clj": [null, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, 1, null, null, null, 10, 10, 10, 10, 10, true, 0, null, 1, + null, 1, null, null, null, 11, 11, 11, 11, 11, true, 0, null, 1, null, 1, 0, 0, 0, 0, 0, 0, 1, null, 1], "clj_activitypub/core.clj": [null, 1, null, null, null, null, null, null, null, null, null, 1, @@ -51,21 +50,50 @@ "dog_and_duck/quack/picky.clj": [null, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, 1, + null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, + null, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 1, null, - 1, null, 1, 1, 1, 1, 1, null, 1, null, null, null, true, true, 7, 7, - 7, 7, 12, 7, null, 1, null, null, null, null, 1, null, null, null, - null, 1, null, null, null, null, null, null, true, 22, 18, true, 2, - null, null, null, 12, null, null, 3, null, 1, null, null, null, - null, null, null, null, null, 14, 14, 14, 14, null, 14, null, 14, - null, 14, 14, 14, null, 1, null, null, 12, 12, 12, 12, 1, null, - null, 12, 12, 3, null, null, 12, 3, null, null, 12, 4, null, null, - 12, null, 1, null, null, 5, 5, 5, 5, 5, 4, 3, 1, null, 1, null, 0, - 1, 5, null], + null, null, null, null, null, null, null, null, 1, null, null, null, + 1, null, 1, null, null, null, null, null, null, null, null, null, + null, null, 1, null, 1, null, 1, 1, 1, 1, 1, 1, null, 1, null, null, + 0, null, 1, null, null, null, null, null, 0, 0, 0, 0, 0, null, 1, + null, null, null, true, true, 46, 46, 46, 72, 46, null, 0, 0, null, + 0, 0, null, 1, null, null, null, null, 1, null, null, null, null, 1, + null, null, null, null, null, null, true, 261, 123, true, 6, null, + null, null, 89, null, null, 3, null, 1, null, null, null, null, + null, null, null, null, 372, 372, 372, 372, null, 372, null, 372, + null, 372, 372, true, null, 0, 0, null, true, null, null, 6, 6, + null, 1, null, null, null, null, null, null, 0, 0, 0, 0, 0, 0, 0, 0, + 0, null, 0, 0, 0, 0, 0, 0, null, 1, null, null, null, null, null, + true, 89, 89, 89, 5, 89, 89, 66, 89, 13, 89, 23, null, 0, 0, 0, 0, + 0, null, null, 0, null, 1, null, null, null, null, 0, null, 16, 16, + null, 0, null, 0, null, 12, null, 1, null, null, true, 17, 17, 17, + 17, 17, 14, 13, 8, null, 1, null, 0, 3, null, 1, null, null, null, + null, 1, null, null, null, null, null, 1, null, null, 12, null, 1, + null, null, null, 8, 8, true, 8, null, 1, null, null, true, 3, 3, 3, + 3, 1, 3, 3, 3, 3, null, 1, null, null, null, null, 1, null, null, + null, null, 1, null, null, null, 4, null, 1, null, null, null, 0, 0, + 0, 0, null, 1, null, null, null, null, null, 0, null, 0, 0, null, 1, + null, null, null, null, 0, 0, 0, 0, null, null, null, 1, null, null, + null, null, null, null, null, null, null, null, null, null, null, + null, null, null, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, null, null, 0, 0, + null, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, null, 1, null, null, 0, + null, 1, null, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, + 0, 0, 0, null, 1, null, null, null, true, 0, true, true, null, 1, + null, null, null, 1, null, 1, null, null, 1, null, 1, 0, null, null, + null, 1, null, null, 1, 1, 1, 1, null, 1, 1, 1, 1, 1, 1, null, 1, 1, + 1, 0, null, null, null, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, null, 1, null, null, null, null, null, null, null, 0, 0, 0, null, + 0, 0, 0, 0, null, 1, null, 0, 0, 0, 0, 0, 0, 0, 0, 0], "dog_and_duck/quack/fault_messages.clj": - [null, 1, null, 1, null, 1, null, null, null, null, null, null, - null], - "dog_and_duck/scratch/core.clj":[null, 1, null, 1, null, null, 0], + [null, 1, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, 1, null, 1, null, null, + null, null, null, null, null, null, null, null], + "dog_and_duck/scratch/core.clj": + [null, 1, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, 1, null, null, 0], "clj_activitypub/webfinger.clj": [null, 1, null, null, null, null, null, null, null, 1, null, 1, 1, null, 1, null, null, 1, 1, 1, null, 1, 1, null, 1, null, null, null, @@ -73,6 +101,6 @@ "dog_and_duck/scratch/scratch.clj": [null, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, 1, null, 1, 1, 1, null, null, null, - null, 1, null, 1, 1, 1, 1, null, null, 1, null, null, 1, 1, null, - null, null, null, 1, 1, 1, 1, 1, 1, null, null]}} + null, null, 1, null, 1, 1, 1, null, null, null, null, 1, null, 1, 1, + 1, 1, null, null, 1, null, null, null, null, null, null, null, 1, 1, + 1, 1, 1, 1, null, null]}} diff --git a/docs/cloverage/coverage.xml b/docs/cloverage/coverage.xml index 53acaeb..6417165 100644 --- a/docs/cloverage/coverage.xml +++ b/docs/cloverage/coverage.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/cloverage/dog_and_duck/quack/fault_messages.clj.html b/docs/cloverage/dog_and_duck/quack/fault_messages.clj.html index 36cfe2a..ebc8537 100644 --- a/docs/cloverage/dog_and_duck/quack/fault_messages.clj.html +++ b/docs/cloverage/dog_and_duck/quack/fault_messages.clj.html @@ -10,35 +10,92 @@ 002  
+ + 003  ;;;     Copyright (C) Simon Brooke, 2022 +
+ + 004   +
+ + 005  ;;;     This program is free software; you can redistribute it and/or +
+ + 006  ;;;     modify it under the terms of the GNU General Public License +
+ + 007  ;;;     as published by the Free Software Foundation; either version 2 +
+ + 008  ;;;     of the License, or (at your option) any later version. +
+ + 009   +
+ + 010  ;;;     This program is distributed in the hope that it will be useful, +
+ + 011  ;;;     but WITHOUT ANY WARRANTY; without even the implied warranty of +
+ + 012  ;;;     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +
+ + 013  ;;;     GNU General Public License for more details. +
+ + 014   +
+ + 015  ;;;     You should have received a copy of the GNU General Public License +
+ + 016  ;;;     along with this program; if not, write to the Free Software +
+ + 017  ;;;     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. +
+ + 018   +
- 003  (def messages + 019  (def messages
- 004    "Actual fault messages to which fault codes resolve." + 020    "Actual fault messages to which fault codes resolve."
- - 005    {:id-not-https "Publicly facing content SHOULD use HTTPS URIs" + + 021    {:id-not-https "Publicly facing content SHOULD use HTTPS URIs"
- 006     :id-not-uri "identifiers must be publicly dereferencable URIs" + 022     :id-not-uri "identifiers must be publicly dereferencable URIs"
- 007     :no-context "Section 3 of the ActivityPub specification states Implementers SHOULD include the ActivityPub context in their object definitions`." + 023     :no-context "Section 3 of the ActivityPub specification states Implementers SHOULD include the ActivityPub context in their object definitions`."
- 008     :no-id-persistent "Persistent objects MUST have unique global identifiers." + 024     :no-id-persistent "Persistent objects MUST have unique global identifiers."
- 009     :no-id-transient "The ActivityPub specification allows objects without `id` fields only if they are intentionally transient; even so it is preferred that the object should have an explicit null id." + 025     :no-id-transient "The ActivityPub specification allows objects without `id` fields only if they are intentionally transient; even so it is preferred that the object should have an explicit null id."
- 010     :null-id-persistent "Persistent objects MUST have non-null identifiers." + 026     :no-inbox "Actor objects MUST have an `inbox` property, whose value MUST be a reference to an ordered collection."
- 011     :no-type "The ActivityPub specification states that the `type` field is optional, but it is hard to process objects with no known type." + 027     :no-outbox "Actor objects MUST have an `outbox` property, whose value MUST be a reference to an ordered collection."
- 012     :not-an-object "ActivityStreams object must be JSON objects."}) + 028     :no-type "The ActivityPub specification states that the `type` field is optional, but it is hard to process objects with no known type." +
+ + 029     :not-actor-type "The `type` value of the object was not a recognised actor type." +
+ + 030     :null-id-persistent "Persistent objects MUST have non-null identifiers." +
+ + 031     :not-an-object "ActivityStreams object must be JSON objects."})
diff --git a/docs/cloverage/dog_and_duck/quack/picky.clj.html b/docs/cloverage/dog_and_duck/quack/picky.clj.html index ba4a0b1..18b3ae0 100644 --- a/docs/cloverage/dog_and_duck/quack/picky.clj.html +++ b/docs/cloverage/dog_and_duck/quack/picky.clj.html @@ -44,7 +44,7 @@ 013                                3. `:severity` whose value shall be one of 

- 014                                   `minor`, `should`, `must` or `critical`; + 014                                   `info`, `minor`, `should`, `must` or `critical`;
015                                4. `:fault` whose value shall be a unique token @@ -92,403 +92,1558 @@ 029                                ActivityStreams spec."
- 030      (:require [dog-and-duck.quack.fault-messages :refer [messages]] + 030      (:require [clojure.set :refer [intersection]]
- 031                [dog-and-duck.utils.process :refer [pid]]) + 031                [dog-and-duck.quack.fault-messages :refer [messages]]
- 032      (:import [java.net URI URISyntaxException])) + 032                [dog-and-duck.utils.process :refer [get-hostname get-pid]] +
+ + 033                [taoensso.timbre :as timbre +
+ + 034        ;; Optional, just refer what you like: +
+ + 035                 :refer [warn]] +
+ + 036                [clojure.data.json :as json]) +
+ + 037      (:import [java.net URI URISyntaxException]))
- 033   -
- - 034  (def ^:const severity + 038  
- 035    "Severity of faults found, as follows: -
- - 036      -
- - 037     1. `:minor` things which I consider to be faults, but which  -
- - 038        don't actually breach the spec; -
- - 039     2. `:should` instances where the spec says something SHOULD -
- - 040        be done, which isn't; -
- - 041     3. `:must` instances where the spec says something MUST -
- - 042        be done, which isn't; -
- - 043     4. `:critical` instances where I believe the fault means that -
- - 044        the object cannot be meaningfully processed." -
- - 045    #{:minor :should :must :critical}) + 039  ;;;     Copyright (C) Simon Brooke, 2022
- 046   -
- - 047  (def ^:const severity-filters + 040  
- 048    "Hack for implementing a severity hierarchy" + 041  ;;;     This program is free software; you can redistribute it and/or
- - 049    {:all #{} + + 042  ;;;     modify it under the terms of the GNU General Public License
- - 050     :minor #{:minor} + + 043  ;;;     as published by the Free Software Foundation; either version 2
- - 051     :should #{:minor :should} + + 044  ;;;     of the License, or (at your option) any later version.
- - 052     :must #{:minor :should :must} + + 045  
- - 053     :critical severity}) + + 046  ;;;     This program is distributed in the hope that it will be useful, +
+ + 047  ;;;     but WITHOUT ANY WARRANTY; without even the implied warranty of +
+ + 048  ;;;     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +
+ + 049  ;;;     GNU General Public License for more details. +
+ + 050   +
+ + 051  ;;;     You should have received a copy of the GNU General Public License +
+ + 052  ;;;     along with this program; if not, write to the Free Software +
+ + 053  ;;;     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
054  
- - 055  (defn filter-severity -
- 056    "Return a list of reports taken from these `reports` where the severity -
- - 057     of the report is greater than this `severity`." -
- - 058    [reports severity] -
- - 059    (assert -
- - 060     (and -
- - 061      (coll? reports) -
- - 062      (every? map? reports) -
- - 063      (every? :severity reports))) -
- - 064    (remove -
- - 065     #((severity-filters severity) (:severity %)) -
- - 066     reports)) + 055  ;; ERRATA
- 067   + 056  
- 068  (def ^:const activitystreams-context-uri + 057  (def ^:dynamic *reify-refs*
- 069    "The URI of the context of an ActivityStreams object is expected to be this + 058    "If `true`, references to objects in fields will be reified and validated. 
- 070     literal string." + 059     If `false`, they won't, but an `:info` level fault report will be generated.
- 071    "https://www.w3.org/ns/activitystreams") -
- - 072   -
- - 073  (def ^:const validation-fault-context-uri + 060     
- 074    "The URI of the context of a validation fault report object shall be this + 061     There are several things in the spec which, in a document, may correctly be
- 075     literal string." + 062     either
- 076    "https://simon-brooke.github.io/dog-and-duck/codox/Validation_Faults.html") + 063      +
+ + 064     1. a fully fleshed out object, or +
+ + 065     2. a URI pointing to such an object. +
+ + 066      +
+ + 067     Obviously to fully validate a document we ought to reify all the refs and  +
+ + 068     check that they are themselves valid, but +
+ + 069      +
+ + 070     a. in some of the published test documents the URIs do not reference a +
+ + 071        valid document; +
+ + 072     b. there will be performance costs to reifying all the refs; +
+ + 073     c. in perverse cases, reifying refs might result in runaway recursion. +
+ + 074      +
+ + 075     TODO: I think that in production this should default to `true`." +
+ + 076    false)
077  
- - 078  (defn context? + + 078  (def ^:dynamic *reject-severity*
- 079    "Returns `true` iff `x` quacks like an ActivityStreams context, else false. + 079    "The severity at which the binary validator will return `false`.
080     
- 081     A context is either + 081     In practice documents seen in the wild do not typically appear to be 
- 082     1. the URI (actually an IRI) `activitystreams-context-uri`, or + 082     fully valid, and this does not matter. This allows the sensitivity of
- 083     2. a collection comprising that URI and a map." + 083     the binary validator (`dog-and-duck.quack.quack`) to be tuned. It's in
- 084    [x] + 084     this (`dog-and-duck.quack.picky`) namespace, not that one, because this
- - 085    (cond + + 085     namespace is where concerns about severity are handled." +
+ + 086    :must) +
+ + 087   +
+ + 088  (def ^:const context-key +
+ + 089    "The Clojure reader barfs on `:@context`, although it is in principle a valid  +
+ + 090     keyword. So we'll make it once, here, to make the code more performant and +
+ + 091     easier to read."
- 086      (nil? x) false -
- - 087      (string? x) (and (= x activitystreams-context-uri) true) -
- - 088      (coll? x) (and (context? (first (remove map? x))) -
- - 089                     (= (count x) 2) -
- - 090                     true) -
- - 091      :else false)) + 092    (keyword "@context"))
- 092   -
- - 093  (defmacro has-context? -
- - 094    "True if `x` is an ActivityStreams object with a valid context, else `false`." -
- - 095    [x] + 093  
- 096    `(context? ((keyword "@context") ~x))) + 094  (def ^:const severity +
+ + 095    "Severity of faults found, as follows: +
+ + 096      +
+ + 097     0. `:info` not actually a fault, but an issue noted during validation; +
+ + 098     1. `:minor` things which I consider to be faults, but which  +
+ + 099        don't actually breach the spec; +
+ + 100     2. `:should` instances where the spec says something SHOULD +
+ + 101        be done, which isn't; +
+ + 102     3. `:must` instances where the spec says something MUST +
+ + 103        be done, which isn't; +
+ + 104     4. `:critical` instances where I believe the fault means that +
+ + 105        the object cannot be meaningfully processed." +
+ + 106    #{:info :minor :should :must :critical})
- 097   + 107  
- 098  (defn make-fault-object + 108  (def ^:const severity-filters
- 099    "Return a fault object with these `severity`, `fault` and `narrative` values. + 109    "Hack for implementing a severity hierarchy"
- - 100      + + 110    {:all #{}
- - 101     An ActivityPub object MUST have a globally unique ID. Whether this is  -
- - 102     meaningful depends on whether we persist fault report objects and serve -
- - 103     them, which at present I have no plans to do." -
- - 104    ;; TODO: should not pass in the narrative; instead should use the :fault value -
- - 105    ;; to look up the narrative in a resource file. -
- - 106    [severity fault] -
- - 107    (assoc {} -
- - 108           (keyword "@context") validation-fault-context-uri -
- - 109           :id (str "https://" + + 111     :info #{}
- 110                    (.. java.net.InetAddress getLocalHost getHostName) -
- - 111                    "/fault/" -
- - 112                    pid -
- - 113                    ":" + 112     :minor #{:info}
- 114                    (inst-ms (java.util.Date.))) + 113     :should #{:info :minor}
- - 115           :type "Fault" + + 114     :must #{:info :minor :should}
- 116           :severity severity -
- - 117           :fault fault -
- - 118           :narrative (messages fault))) + 115     :critical severity})
- 119   + 116  
- 120  (defn object-faults + 117  (defn truthy?
- 121    "Return a list of faults found in object `x`, or `nil` if none are." + 118    "Return `true` if `x` is truthy, else `false`."
- 122    [x] -
- - 123    (let [faults (remove -
- - 124                  empty? -
- - 125                  (list -
- - 126                   (when-not (map? x) -
- - 127                     (make-fault-object -
- - 128                      :critical -
- - 129                      :not-an-object)) -
- - 130                   (when-not -
- - 131                    (has-context? x) -
- - 132                     (make-fault-object -
- - 133                      :should -
- - 134                      :no-context)) -
- - 135                   (when-not (:type x) -
- - 136                     (make-fault-object -
- - 137                      :minor -
- - 138                      :no-type)) -
- - 139                   (when-not (and (map? x) (contains? x :id)) -
- - 140                     (make-fault-object -
- - 141                      :minor -
- - 142                      :no-id-transient))))] -
- - 143      (if (empty? faults) nil faults))) -
- - 144   -
- - 145  (defn persistent-object-faults -
- - 146    "Return a list of faults found in persistent object `x`, or `nil` if none are." -
- - 147    [x] -
- - 148    (let [faults (concat -
- - 149                  (object-faults x) -
- - 150                  (remove empty? -
- - 151                          (list -
- - 152                           (if (contains? x :id) -
- - 153                             (try (let [id (URI. (:id x))] -
- - 154                                    (when-not (= (.getScheme id) "https") -
- - 155                                      (make-fault-object :should :id-not-https))) -
- - 156                                  (catch URISyntaxException _ -
- - 157                                    (make-fault-object :must :id-not-uri)) -
- - 158                                  (catch NullPointerException _ + 119    [x]
- 159                                    (make-fault-object :must :null-id-persistent))) -
- - 160                             (make-fault-object :must :no-id-persistent)))))] -
- - 161      (if (empty? faults) nil faults))) + 120    (if x true false))
- 162   + 121   +
+ + 122  (defn has-type? +
+ + 123    "Return `true` if object `x` has type `type`, else `false`. +
+ + 124      +
+ + 125     The values of `type` fields of ActivityStreams objects may be lists; they +
+ + 126     are considered to have a type if the type token is a member of the list." +
+ + 127    [x type] +
+ + 128    (assert (map? x) (string? type)) +
+ + 129    (let [tv (:type x)] +
+ + 130      (cond +
+ + 131        (coll? tv) (truthy? (not-empty (filter #(= % type) tv))) +
+ + 132        :else (= tv type)))) +
+ + 133   +
+ + 134  (defn filter-severity +
+ + 135    "Return a list of reports taken from these `reports` where the severity +
+ + 136     of the report is greater than this or equal to this `severity`." +
+ + 137    [reports severity] +
+ + 138    (cond (nil? reports) nil +
+ + 139          (and +
+ + 140           (coll? reports) +
+ + 141           (every? map? reports) +
+ + 142           (every? :severity reports)) (remove +
+ + 143                                        #((severity-filters severity) (:severity %)) +
+ + 144                                        reports) +
+ + 145          :else +
+ + 146          (throw +
+ + 147           (ex-info +
+ + 148            "Argument `reports` was not a collection of fault reports" +
+ + 149            {:arguments {:reports reports +
+ + 150                         :severity severity}})))) +
+ + 151   +
+ + 152  (def ^:const activitystreams-context-uri +
+ + 153    "The URI of the context of an ActivityStreams object is expected to be this +
+ + 154     literal string." +
+ + 155    "https://www.w3.org/ns/activitystreams") +
+ + 156   +
+ + 157  (def ^:const validation-fault-context-uri +
+ + 158    "The URI of the context of a validation fault report object shall be this +
+ + 159     literal string." +
+ + 160    "https://simon-brooke.github.io/dog-and-duck/codox/Validation_Faults.html") +
+ + 161   +
+ + 162  (defn context? +
+ + 163    "Returns `true` iff `x` quacks like an ActivityStreams context, else false. +
+ + 164      +
+ + 165     A context is either +
+ + 166     1. the URI (actually an IRI) `activitystreams-context-uri`, or +
+ + 167     2. a collection comprising that URI and a map." +
+ + 168    [x] +
+ + 169    (cond +
+ + 170      (nil? x) false +
+ + 171      (string? x) (and (= x activitystreams-context-uri) true) +
+ + 172      (coll? x) (and (context? (first (remove map? x))) +
+ + 173                     (= (count x) 2) +
+ + 174                     true) +
+ + 175      :else false)) +
+ + 176   +
+ + 177  (defmacro has-context? +
+ + 178    "True if `x` is an ActivityStreams object with a valid context, else `false`." +
+ + 179    [x] +
+ + 180    `(context? (context-key ~x))) +
+ + 181   +
+ + 182  (defn make-fault-object +
+ + 183    "Return a fault object with these `severity`, `fault` and `narrative` values. +
+ + 184      +
+ + 185     An ActivityPub object MUST have a globally unique ID. Whether this is  +
+ + 186     meaningful depends on whether we persist fault report objects and serve +
+ + 187     them, which at present I have no plans to do." +
+ + 188    ;; TODO: should not pass in the narrative; instead should use the :fault value +
+ + 189    ;; to look up the narrative in a resource file. +
+ + 190    [severity fault] +
+ + 191    (assoc {} +
+ + 192           context-key validation-fault-context-uri +
+ + 193           :id (str "https://" +
+ + 194                    (get-hostname) +
+ + 195                    "/fault/" +
+ + 196                    (get-pid) +
+ + 197                    ":" +
+ + 198                    (inst-ms (java.util.Date.))) +
+ + 199           :type "Fault" +
+ + 200           :severity severity +
+ + 201           :fault fault +
+ + 202           :narrative (or (messages fault) +
+ + 203                          (do +
+ + 204                            (warn "No narrative provided for fault token " fault) +
+ + 205                            (str fault))))) +
+ + 206   +
+ + 207  (defmacro nil-if-empty +
+ + 208    "if `x` is an empty collection, return `nil`; else return `x`." +
+ + 209    [x] +
+ + 210    `(if (and (coll? ~x) (empty? ~x)) nil +
+ + 211         ~x)) +
+ + 212   +
+ + 213  (defn has-type-or-fault +
+ + 214    "If object `x` has a `:type` value which is `acceptable`, return `nil`; +
+ + 215     else return a fault object with this `severity` and `token`. +
+ + 216      +
+ + 217     `acceptable` may be passed as either nil, a string, or a set of strings. +
+ + 218     If `acceptable` is `nil`, no type specific tests will be performed." +
+ + 219    [x acceptable severity token] +
+ + 220    (when acceptable +
+ + 221      (let [tv (:type x)] +
+ + 222        (when-not +
+ + 223         (cond +
+ + 224           (and (string? tv) (string? acceptable)) (= tv acceptable) +
+ + 225           (and (string? tv) (set? acceptable)) (acceptable tv) +
+ + 226           (and (coll? tv) (string? acceptable)) ((set tv) acceptable) +
+ + 227           (and (coll? tv) (set? acceptable)) (not-empty +
+ + 228                                               (intersection (set tv) acceptable)) +
+ + 229           :else +
+ + 230           (throw (ex-info "Type value or `acceptable` argument not as expected." +
+ + 231                           {:arguments {:x x +
+ + 232                                        :acceptable acceptable +
+ + 233                                        :severity severity +
+ + 234                                        :token token}}))) +
+ + 235          (make-fault-object severity token))))) +
+ + 236   +
+ + 237  (defn object-faults +
+ + 238    "Return a list of faults found in object `x`, or `nil` if none are. +
+ + 239      +
+ + 240     If `expected-type` is also passed, verify that `x` has `expected-type`. +
+ + 241     `expected-type` may be passed as a string or as a set of strings." +
+ + 242    ([x] +
+ + 243     (nil-if-empty +
+ + 244      (remove empty? +
+ + 245              (list +
+ + 246               (when-not (map? x) +
+ + 247                 (make-fault-object :critical :not-an-object)) +
+ + 248               (when-not +
+ + 249                (has-context? x) +
+ + 250                 (make-fault-object :should :no-context)) +
+ + 251               (when-not (:type x) +
+ + 252                 (make-fault-object :minor :no-type)) +
+ + 253               (when-not (and (map? x) (contains? x :id)) +
+ + 254                 (make-fault-object :minor :no-id-transient)))))) +
+ + 255    ([x expected-type] +
+ + 256     (nil-if-empty +
+ + 257      (remove empty? +
+ + 258              (concat +
+ + 259               (object-faults x) +
+ + 260               (list +
+ + 261                ;; TODO: should resolve the correct `-faults`function for the +
+ + 262                ;; `expected-type` and call that; but that's for later. +
+ + 263                (has-type-or-fault x expected-type :critical :unexpected-type))))))) +
+ + 264   +
+ + 265  (defn uri-or-fault +
+ + 266    "If `u` is not a valid URI, return a fault object with this `severity` and  +
+ + 267     `if-invalid-token`. If it's `nil`, return a fault object with this `severity` +
+ + 268     and `if-missing-token`. Otherwise return nil." +
+ + 269    ([u severity if-missing-token] +
+ + 270     (uri-or-fault u severity if-missing-token if-missing-token)) +
+ + 271    ([u severity if-missing-token if-invalid-token] +
+ + 272     (try +
+ + 273       (if (uri? (URI. u)) +
+ + 274         nil +
+ + 275         (make-fault-object severity if-invalid-token)) +
+ + 276       (catch URISyntaxException _ +
+ + 277         (make-fault-object severity if-invalid-token)) +
+ + 278       (catch NullPointerException _ +
+ + 279         (make-fault-object severity if-missing-token))))) +
+ + 280   +
+ + 281  (defn persistent-object-faults +
+ + 282    "Return a list of faults found in persistent object `x`, or `nil` if none are." +
+ + 283    [x] +
+ + 284    (nil-if-empty +
+ + 285     (remove empty? +
+ + 286             (concat +
+ + 287              (object-faults x) +
+ + 288              (list +
+ + 289               (if (contains? x :id) +
+ + 290                 (try (let [id (URI. (:id x))] +
+ + 291                        (when-not (= (.getScheme id) "https") +
+ + 292                          (make-fault-object :should :id-not-https))) +
+ + 293                      (catch URISyntaxException _ +
+ + 294                        (make-fault-object :must :id-not-uri)) +
+ + 295                      (catch NullPointerException _ +
+ + 296                        (make-fault-object :must :null-id-persistent))) +
+ + 297                 (make-fault-object :must :no-id-persistent))))))) +
+ + 298   +
+ + 299  (def ^:const actor-types +
+ + 300    "The set of types we will accept as actors. +
+ + 301      +
+ + 302     There's an [explicit set of allowed actor types] +
+ + 303     (https://www.w3.org/TR/activitystreams-vocabulary/#actor-types)." +
+ + 304    #{"Application" +
+ + 305      "Group" +
+ + 306      "Organization" +
+ + 307      "Person" +
+ + 308      "Service"}) +
+ + 309   +
+ + 310  (defn actor-type? +
+ + 311    "Return `true` if the `x` is a recognised actor type, else `false`." +
+ + 312    [^String x] +
+ + 313    (if (actor-types x) true false)) +
+ + 314   +
+ + 315  (defn has-actor-type? +
+ + 316    "Return `true` if the object `x` has a type which is an actor type, else  +
+ + 317     `false`." +
+ + 318    [x] +
+ + 319    (let [tv (:type x)] +
+ + 320      (cond +
+ + 321        (coll? tv) (truthy? (not-empty (filter actor-type? tv))) +
+ + 322        :else (actor-type? tv)))) +
+ + 323   +
+ + 324  (defn actor-faults +
+ + 325    "Return a list of faults found in actor `x`, or `nil` if none are." +
+ + 326    [x] +
+ + 327    (nil-if-empty +
+ + 328     (remove empty? +
+ + 329             (concat (persistent-object-faults x) +
+ + 330                     (list +
+ + 331                      (when-not (has-actor-type? x) +
+ + 332                        (make-fault-object :must :not-actor-type)) +
+ + 333                      (uri-or-fault +
+ + 334                       (:inbox x) :must :no-inbox :invalid-inbox-uri) +
+ + 335                      (uri-or-fault +
+ + 336                       (:outbox x) :must :no-outbox :invalid-outbox-uri)))))) +
+ + 337   +
+ + 338  (def ^:const verb-types +
+ + 339    "The set of types we will accept as verbs. +
+ + 340      +
+ + 341     There's an [explicit set of allowed verb types] +
+ + 342     (https://www.w3.org/TR/activitystreams-vocabulary/#activity-types)." +
+ + 343    #{"Accept" "Add" "Announce" "Arrive" "Block" "Create" "Delete" "Dislike" +
+ + 344      "Flag" "Follow" "Ignore" "Invite" "Join" "Leave" "Like" "Listen" "Move" +
+ + 345      "Offer" "Question" "Reject" "Read" "Remove" "TentativeAccept" +
+ + 346      "TentativeReject" "Travel" "Undo" "Update" "View"}) +
+ + 347   +
+ + 348  (defn verb-type? +
+ + 349    "`true` if `x`, a string, represents a recognised ActivityStreams activity +
+ + 350     type." +
+ + 351    [^String x] +
+ + 352    (if (verb-types x) true false)) +
+ + 353   +
+ + 354  (defn has-activity-type? +
+ + 355    "Return `true` if the object `x` has a type which is an activity type, else  +
+ + 356     `false`." +
+ + 357    [x] +
+ + 358    (let [tv (:type x)] +
+ + 359      (cond +
+ + 360        (coll? tv) (truthy? (not-empty (filter verb-type? tv))) +
+ + 361        :else (actor-type? tv)))) +
+ + 362   +
+ + 363  (defn string-or-fault +
+ + 364    "If this `value` is not a string, return a fault object with this `severity`  +
+ + 365     and `token`, else `nil`. If `pattern` is also passed, it is expected to be +
+ + 366     a Regex, and the fault object will be returned unless `value` matches the  +
+ + 367     `pattern`." +
+ + 368    ([value severity token] +
+ + 369     (when-not (string? value) (make-fault-object severity token))) +
+ + 370    ([value severity token pattern] +
+ + 371     (when not (and (string? value) (re-matches pattern value)) +
+ + 372           (make-fault-object severity token)))) +
+ + 373   +
+ + 374  (defn link-faults +
+ + 375    "A link object is required to have an `href` property. It may have all of +
+ + 376     `rel` | `mediaType` | `name` | `hreflang` | `height` | `width` | `preview` +
+ + 377     but I *think* they're all optional." +
+ + 378    [x] +
+ + 379    (list +
+ + 380     (uri-or-fault +
+ + 381      (:href x) :must :no-href-uri :invalid-href-uri) +
+ + 382     (string-or-fault (:mediaType x) :minor :no-media-type #"\w+\/[-+.\w]+") +
+ + 383     ;; TODO: possibly more here. Audit against the specs +
+ + 384     )) +
+ + 385   +
+ + 386  (defn object-reference-or-faults +
+ + 387    "If this `value` is either  +
+ + 388      +
+ + 389     1. an object of `expected-type`; +
+ + 390     2. a URI referencing an object of  `expected-type`; or +
+ + 391     3. a link object referencing an object of  `expected-type` +
+ + 392      +
+ + 393     and no faults are returned from validating the linked object, then return +
+ + 394     `nil`; else return a sequence comprising a fault object with this `severity` +
+ + 395     and `token`, prepended to the faults returned. +
+ + 396      +
+ + 397     As with `has-type-or-fault` (q.v.), `expected-type` may be passed as a +
+ + 398     string or as a set of strings. +
+ + 399      +
+ + 400     **NOTE THAT** if `*reify-refs*` is `false`, referenced objects will not +
+ + 401     actually be checked." +
+ + 402    [value expected-type severity token] +
+ + 403    (let [faults (cond +
+ + 404                   (string? value) (try (let [uri (URI. value) +
+ + 405                                              object (when *reify-refs* +
+ + 406                                                       (json/read-str (slurp uri)))] +
+ + 407                                          (when object +
+ + 408                                            (object-faults object expected-type))) +
+ + 409                                        (catch URISyntaxException _ +
+ + 410                                          (make-fault-object severity token))) +
+ + 411                   (map? value) (if (has-type? value "Link") +
+ + 412                                  (cond +
+ + 413                                    ;; if we were looking for a link and we've  +
+ + 414                                    ;; found a link, that's OK. +
+ + 415                                    (= expected-type "Link") nil +
+ + 416                                    (and (set? expected-type) (expected-type "Link")) nil +
+ + 417                                    :else +
+ + 418                                    (object-reference-or-faults +
+ + 419                                     (:href value) expected-type severity token)) +
+ + 420                                  (object-faults value expected-type)) +
+ + 421                   :else (throw +
+ + 422                          (ex-info +
+ + 423                           "Argument `value` was not an object or a link to an object" +
+ + 424                           {:arguments {:value value} +
+ + 425                            :expected-type expected-type +
+ + 426                            :severity severity +
+ + 427                            :token token})))] +
+ + 428      (when faults (cons (make-fault-object severity token) faults)))) +
+ + 429   +
+ + 430  (defn link-faults +
+ + 431    "Return a list of faults found in the link `x`, or `nil` if none are found." +
+ + 432    [x] +
+ + 433    (object-reference-or-faults x "Link" :critical :expected-link)) +
+ + 434   +
+ + 435  (defn coll-object-reference-or-fault +
+ + 436    "As object-reference-or-fault, except `value` argument may also be a list of +
+ + 437     objects and/or object references." +
+ + 438    [value expected-type severity token] +
+ + 439    (cond +
+ + 440      (map? value) (object-reference-or-faults value expected-type severity token) +
+ + 441      (coll? value) (nil-if-empty +
+ + 442                     (remove nil? +
+ + 443                             (reduce concat +
+ + 444                                     (map +
+ + 445                                      #(object-reference-or-faults +
+ + 446                                        % expected-type severity token) +
+ + 447                                      value)))) +
+ + 448      :else (throw +
+ + 449             (ex-info +
+ + 450              "Argument `value` was not an object, a link to an object, nor a list of these." +
+ + 451              {:arguments {:value value} +
+ + 452               :expected-type expected-type +
+ + 453               :severity severity +
+ + 454               :token token})))) +
+ + 455   +
+ + 456  (def ^:const base-activity-required-properties +
+ + 457    "Properties most activities should have. Values are validating functions, each. +
+ + 458      +
+ + 459     See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-activity" +
+ + 460    {:summary (fn [v] (when-not (string? v) +
+ + 461                        (list (make-fault-object :should :no-summary)))) +
+ + 462     :actor (fn [v] (object-reference-or-faults v actor-types :must :no-actor)) +
+ + 463     :object (fn [v] (object-reference-or-faults v nil :must :no-object))}) +
+ + 464   +
+ + 465  (def ^:const intransitive-activity-required-properties +
+ + 466    "Properties intransitive activities should have. +
+ + 467      +
+ + 468     See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-intransitiveactivity" +
+ + 469    (dissoc base-activity-required-properties :object)) +
+ + 470   +
+ + 471  (def ^:const accept-required-properties +
+ + 472    "As base-activity-required-properties, except that the type of the object +
+ + 473     is restricted." +
+ + 474    (assoc base-activity-required-properties +
+ + 475           :object +
+ + 476           (fn [v] +
+ + 477             (object-reference-or-faults v #{"Invite" "Person"} +
+ + 478                                         :must +
+ + 479                                         :bad-accept-target)))) +
+ + 480   +
+ + 481  (def ^:const activity-required-properties +
+ + 482    "Properties activities should have, keyed by activity type. Values are maps  +
+ + 483     of the format of `base-activity-required-properties`, q.v." +
+ + 484    {"Accept" accept-required-properties +
+ + 485     "Add" base-activity-required-properties +
+ + 486     "Announce" base-activity-required-properties +
+ + 487     "Arrive" intransitive-activity-required-properties +
+ + 488     ;; TODO: is `:location` required for arrive? +
+ + 489     "Block" base-activity-required-properties +
+ + 490     "Create" base-activity-required-properties +
+ + 491     "Delete" base-activity-required-properties +
+ + 492     "Dislike" base-activity-required-properties +
+ + 493     "Flag" base-activity-required-properties +
+ + 494     "Follow" base-activity-required-properties +
+ + 495     ;; TODO: is `:object` required to be an actor? +
+ + 496     "Ignore" base-activity-required-properties +
+ + 497     "Invite" (assoc base-activity-required-properties :target +
+ + 498                     (fn [v] +
+ + 499                       (coll-object-reference-or-fault v #{"Event" "Group"} +
+ + 500                                                       :must +
+ + 501                                                       :bad-accept-target))) +
+ + 502     ;; TODO: are here other things one could meaningfully be invited to? +
+ + 503     "Join" base-activity-required-properties +
+ + 504     "Leave" base-activity-required-properties +
+ + 505     "Like" base-activity-required-properties +
+ + 506     "Listen" base-activity-required-properties +
+ + 507     "Move" base-activity-required-properties +
+ + 508     "Offer" base-activity-required-properties +
+ + 509     "Question" intransitive-activity-required-properties +
+ + 510     "Reject" base-activity-required-properties +
+ + 511     "Read" base-activity-required-properties +
+ + 512     "Remove" base-activity-required-properties +
+ + 513     "TentativeReject" base-activity-required-properties +
+ + 514     "TentativeAccept" accept-required-properties +
+ + 515     "Travel" base-activity-required-properties +
+ + 516     "Undo" base-activity-required-properties +
+ + 517     "Update" base-activity-required-properties +
+ + 518     "View" base-activity-required-properties}) +
+ + 519   +
+ + 520  (defn activity-type-faults +
+ + 521    "Return a list of faults found in the activity `x`; if `type` is also  +
+ + 522     specified, it should be a string naming a specific activity type for +
+ + 523     which checks should be performed. +
+ + 524      +
+ + 525     Some specific activity types have specific requirements which are not +
+ + 526     requirements." +
+ + 527    ([x] +
+ + 528     (if (coll? (:type x)) +
+ + 529       (map #(activity-type-faults x %) (:type x)) +
+ + 530       (activity-type-faults x (:type x)))) +
+ + 531    ([x type] +
+ + 532     (let [checks (activity-required-properties type)] +
+ + 533       (map +
+ + 534        #(apply (checks %) (x %)) +
+ + 535        (keys checks))))) +
+ + 536   +
+ + 537  (defn activity-faults +
+ + 538    [x] +
+ + 539    (nil-if-empty +
+ + 540     (remove empty? +
+ + 541             (concat (persistent-object-faults x) +
+ + 542                     (activity-type-faults x) +
+ + 543                     (list +
+ + 544                      (when-not +
+ + 545                       (has-activity-type? x) +
+ + 546                        (make-fault-object :must :not-activity-type)) +
+ + 547                      (when-not (string? (:summary x)) (make-fault-object :should :no-summary)))))))
diff --git a/docs/cloverage/dog_and_duck/quack/quack.clj.html b/docs/cloverage/dog_and_duck/quack/quack.clj.html index 4318f94..3c5e951 100644 --- a/docs/cloverage/dog_and_duck/quack/quack.clj.html +++ b/docs/cloverage/dog_and_duck/quack/quack.clj.html @@ -46,644 +46,470 @@ 014     toggle some checks off."
- - 015     + + 015  
- 016    ;;(:require [clojure.spec.alpha as s]) + 016    (:require [dog-and-duck.quack.picky :refer [*reject-severity* activity-faults
- 017    (:require [dog-and-duck.quack.picky :refer [filter-severity has-context?  + 017                                                actor-faults filter-severity link-faults
- 018                                                object-faults]]) -
- - 019    (:import [java.net URI URISyntaxException])) + 018                                                object-faults persistent-object-faults]])
- 020   + 019  
- 021  ;;;     Copyright (C) Simon Brooke, 2022 + 020    (:import [java.net URI URISyntaxException]))
- 022   + 021  
- 023  ;;;     This program is free software; you can redistribute it and/or -
- - 024  ;;;     modify it under the terms of the GNU General Public License -
- - 025  ;;;     as published by the Free Software Foundation; either version 2 -
- - 026  ;;;     of the License, or (at your option) any later version. + 022  ;;;     Copyright (C) Simon Brooke, 2022
- 027   + 023  
- 028  ;;;     This program is distributed in the hope that it will be useful, + 024  ;;;     This program is free software; you can redistribute it and/or
- 029  ;;;     but WITHOUT ANY WARRANTY; without even the implied warranty of + 025  ;;;     modify it under the terms of the GNU General Public License
- 030  ;;;     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + 026  ;;;     as published by the Free Software Foundation; either version 2
- 031  ;;;     GNU General Public License for more details. + 027  ;;;     of the License, or (at your option) any later version.
- 032   + 028  
- 033  ;;;     You should have received a copy of the GNU General Public License + 029  ;;;     This program is distributed in the hope that it will be useful,
- 034  ;;;     along with this program; if not, write to the Free Software + 030  ;;;     but WITHOUT ANY WARRANTY; without even the implied warranty of
- 035  ;;;     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. + 031  ;;;     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +
+ + 032  ;;;     GNU General Public License for more details.
- 036   + 033   +
+ + 034  ;;;     You should have received a copy of the GNU General Public License +
+ + 035  ;;;     along with this program; if not, write to the Free Software +
+ + 036  ;;;     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. +
+ + 037  
- 037  (defn object? + 038  (defn object?
- 038    "Returns `true` iff `x` is recognisably an ActivityStreams object. + 039    "Returns `true` iff `x` is recognisably an ActivityStreams object.
- 039      + 040     
- 040     **NOTE THAT** The ActivityStreams spec  + 041     **NOTE THAT** The ActivityStreams spec 
- 041     [says](https://www.w3.org/TR/activitystreams-core/#object): + 042     [says](https://www.w3.org/TR/activitystreams-core/#object):
- 042      + 043     
- 043     > All properties are optional (including the id and type) + 044     > All properties are optional (including the id and type)
- 044      + 045     
- 045     But we are *just not having that*, because otherwise we're flying blind. + 046     But we are *just not having that*, because otherwise we're flying blind.
- 046     We *shall* reject objects lacking at least `:type`. Missing `:id` keys are + 047     We *shall* reject objects lacking at least `:type`. Missing `:id` keys are
- 047     tolerable because they represent transient objects, which we expect to  + 048     tolerable because they represent transient objects, which we expect to 
- 048     handle. + 049     handle.
- 049      + 050     
- 050     **NOTE THAT** The ActivityPub spec [says](https://www.w3.org/TR/activitypub/#obj) + 051     **NOTE THAT** The ActivityPub spec [says](https://www.w3.org/TR/activitypub/#obj)
- 051      + 052     
- 052     > Implementers SHOULD include the ActivityPub context in their object  + 053     > Implementers SHOULD include the ActivityPub context in their object 
- 053     > definitions + 054     > definitions
- 054      + 055     
- 055     but in samples found in the wild they typically don't." + 056     but in samples found in the wild they typically don't."
- 056    ([x] + 057    ([x]
- - 057    (and (map? x) (:type x) true)) + + 058     (object? x *reject-severity*))
- 058    ([x severity] + 059    ([x severity] +
+ + 060     (empty? (filter-severity (object-faults x) severity)))) +
+ + 061   +
+ + 062  (defn persistent-object? +
+ + 063    "`true` iff `x` is a persistent object. +
+ + 064   +
+ + 065     Transient objects in ActivityPub are not required to have an `id` key, but persistent +
+ + 066     ones must have a key, and it must be an IRI (but normally a URI)." +
+ + 067    ([x] +
+ + 068     (persistent-object? x *reject-severity*)) +
+ + 069    ([x severity] +
+ + 070     (empty? (filter-severity (persistent-object-faults x) severity)))) +
+ + 071   +
+ + 072  (defn actor? +
+ + 073    "Returns `true` if `x` quacks like an actor, else false." +
+ + 074    ([x] (actor? x *reject-severity*)) +
+ + 075    ([x severity] +
+ + 076     (empty? (filter-severity (actor-faults x) severity)))) +
+ + 077   +
+ + 078  (defn actor-or-uri? +
+ + 079    "`true` if `x` is either a URI or an actor. +
+ + 080      +
+ + 081     **TODO**: I need to decide about whether to reify referenced objects +
+ + 082     before validation or after. After reification, every reference to an actor +
+ + 083     *must be* to an actor object, but before, may only be to a URI pointing to  +
+ + 084     one." +
+ + 085    [x] +
+ + 086    (try +
+ + 087      (and +
+ + 088       (cond (string? x) (uri? (URI. x)) +
+ + 089             :else (actor? x)) +
+ + 090       true) +
+ + 091      (catch URISyntaxException _ false) +
+ + 092      (catch NullPointerException _ false))) +
+ + 093   +
+ + 094  (defn activity? +
+ + 095    "`true` iff `x` quacks like an activity, else false." +
+ + 096    ([x] (activity? x *reject-severity*)) +
+ + 097    ([x severity]
- 059     (empty? (filter-severity (object-faults x) severity)))) + 098     (empty? (filter-severity (activity-faults x) severity))))
- 060   + 099  
- 061  (defn persistent-object? + 100  (defn link?
- 062    "`true` iff `x` is a persistent object. + 101    "`true` iff `x` quacks like a link, else false."
- - 063   + + 102    ([x] (link? x *reject-severity*))
- 064     Transient objects in ActivityPub are not required to have an `id` key, but persistent + 103    ([x severity]
- - 065     ones must have a key, and it must be an IRI (but normally a URI)." -
- - 066    [x] -
- - 067    (try -
- - 068      (and (object? x) (uri? (URI. (:id x)))) -
- - 069      (catch URISyntaxException _ false))) -
- - 070   -
- - 071  ;; (persistent-object? {:type "test" :id "https://mastodon.scot/@barfilfarm"}) -
- - 072   -
- - 073  (def ^:const actor-types -
- - 074    "The set of types we will accept as actors. -
- - 075      -
- - 076     There's an [explicit set of allowed actor types] -
- - 077     (https://www.w3.org/TR/activitystreams-vocabulary/#actor-types)." -
- - 078    #{"Application" -
- - 079      "Group" -
- - 080      "Organization" -
- - 081      "Person" -
- - 082      "Service"}) -
- - 083   -
- - 084  (defmacro actor-type? -
- - 085    "Return `true` iff the `x` is a recognised actor type, else `false`." -
- - 086    [^String x] -
- - 087    `(if (actor-types ~x) true false)) -
- - 088   -
- - 089  ;; (actor-type? "Group") -
- - 090   -
- - 091  (def ^:const verb-types -
- - 092    "The set of types we will accept as verbs. -
- - 093      -
- - 094     There's an [explicit set of allowed verb types] -
- - 095     (https://www.w3.org/TR/activitystreams-vocabulary/#activity-types)." -
- - 096    #{"Accept" "Add" "Announce" "Arrive" "Block" "Create" "Delete" "Dislike" -
- - 097      "Flag" "Follow" "Ignore" "Invite" "Join" "Leave" "Like" "Listen" "Move" -
- - 098      "Offer" "Question" "Reject" "Read" "Remove" "TentativeAccept" -
- - 099      "TentativeReject" "Travel" "Undo" "Update" "View"}) -
- - 100   -
- - 101  (defmacro verb-type? -
- - 102    ;; TODO: better as a macro -
- - 103    [^String x] -
- - 104    `(if (verb-types ~x) true false)) + + 104     (empty? (filter-severity (link-faults x) severity))))
105  
+ + 106  (defn link-or-uri? +
+ + 107    "`true` iff `x` is either a URI or a link, else false. +
+ + 108      +
+ + 109     There are several points in the specification where e.g. the `:image` +
+ + 110     property (if present) may be either a link or a URI." +
+ + 111    [x] +
+ + 112    (and +
+ + 113     (cond (string? x) (uri? (URI. x)) +
+ + 114           :else (link? x)) +
+ + 115     true)) +
- 106   + 116  
- 107  (defn actor? + 117  (defn collection?
- 108    "Returns `true` if `x` quacks like an actor, else false. -
- - 109      -
- - 110     **NOTE THAT** [Section 4.1 of the spec] -
- - 111     (https://www.w3.org/TR/activitypub/#actor-objects) says explicitly that -
- - 112      -
- - 113     >  Actor objects MUST have, in addition to the properties mandated by 3.1 Object Identifiers, the following properties: -
- - 114     > -
- - 115     >  inbox -
- - 116     >    A reference to an [ActivityStreams] OrderedCollection comprised of all the messages received by the actor; see 5.2 Inbox.  -
- - 117     > outbox -
- - 118     >    An [ActivityStreams] OrderedCollection comprised of all the messages produced by the actor; see 5.1 Outbox.  + 118    "`true` iff `x` quacks like a collection of type `object-type`, else `false`.
119     
- 120     However, none of the provided examples in the [activitystreams-test-documents repository]() does in fact have these properties" + 120     With one argument, will recognise plain collections and ordered collections,
- 121    [x] + 121     but (currently) not collection pages."
- - 122    (and + + 122    ([x ^String object-type]
- - 123     (object? x) + + 123     (let [items (or (:items x) (:orderedItems x))] +
+ + 124       (and +
+ + 125        (cond +
+ + 126          (:items x) (nil? (:orderedItems x)) +
+ + 127          (:orderedItems x) (nil? (:items x)) ;; can't have both properties
- 124     (has-context? x) -
- - 125     (uri? (URI. (:inbox x))) -
- - 126     (uri? (URI. (:outbox x))) -
- - 127     (actor-type? (:type x)) + 128          (integer? (:totalItems x)) true ;; can have neither, provided it has totalItems.
- 128     true)) + 129          :else false)
- - 129   + + 130        (object? x)
- - 130  (defn actor-or-uri? + + 131        (= (:type x) object-type) +
+ + 132        (if items +
+ + 133          (and (coll? items) +
+ + 134               (every? object? items) ;; if there are items, they must form a
- 131    "`true` if `x` is either a URI or an actor. + 135                                      ;; collection of objects.
- 132      + 136               true)
- 133     **TODO**: I need to decide about whether to reify referenced objects + 137          true) ;; but it's OK if there aren't.
- 134     before validation or after. After reification, every reference to an actor + 138        true)
- 135     *must be* to an actor object, but before, may only be to a URI pointing to  + 139       ;; test for totalItems not done here, because collection pages don't
- 136     one." + 140       ;; have it.
- 137    [x] + 141       ))
- - 138    (and  + + 142    ([x]
- 139     (cond (string? x) (uri? (URI. x)) + 143     (and
- - 140          :else (actor? x))  -
- - 141         true)) -
- - 142   -
- - 143  (defn activity? -
- - 144    "`true` iff `x` quacks like an activity, else false." -
- - 145    [x] + + 144      (or (collection? x "Collection")
- 146    (try -
- - 147      (and (object? x) + 145          (collection? x "OrderedCollection"))
- 148           (has-context? x) -
- - 149           (string? (:summary x)) -
- - 150           (actor-or-uri? (:actor x)) -
- - 151           (verb-type? (:type x)) -
- - 152           (or (object? (:object x)) (uri? (URI. (:object x)))) + 146      (integer? (:totalItems x))
- 153           true) -
- - 154      (catch URISyntaxException _ false))) + 147      true)))
- 155   + 148  
- 156  (defn link? + 149  (defn unordered-collection?
- 157    "`true` iff `x` quacks like a link, else false." + 150    "`true` iff `x` quacks like an unordered collection, else `false`."
- 158    [x] + 151    [x]
- - 159    (and (object? x) + + 152    (and (collection? x "Collection") (integer? (:totalItems x)) true))
- - 160         (= (:type x) "Link") + + 153  
- - 161         (uri? (URI. (:href x))) + + 154  (defn ordered-collection?
- 162         true)) + 155    "`true` iff `x` quacks like an ordered collection, else `false`." +
+ + 156    [x] +
+ + 157    (and (collection? x "OrderedCollection") (integer? (:totalItems x)) true)) +
+ + 158   +
+ + 159  (defn collection-page? +
+ + 160    "`true` iff `x` quacks like a page in a paged collection, else `false`." +
+ + 161    [x] +
+ + 162    (collection? x "CollectionPage"))
163  
- 164  (defn link-or-uri? + 164  (defn ordered-collection-page?
- 165    "`true` iff `x` is either a URI or a link, else false. + 165    "`true` iff `x` quacks like a page in an ordered paged collection, else `false`."
- 166      -
- - 167     There are several points in the specification where e.g. the `:image` -
- - 168     property (if present) may be either a link or a URI." -
- - 169    [x] -
- - 170    (and -
- - 171     (cond (string? x) (uri? (URI. x)) -
- - 172           :else (link? x)) -
- - 173     true)) -
- - 174   -
- - 175  (defn collection? -
- - 176    "`true` iff `x` quacks like a collection of type `object-type`, else `false`. -
- - 177      -
- - 178     With one argument, will recognise plain collections and ordered collections, -
- - 179     but (currently) not collection pages." -
- - 180    ([x ^String object-type] -
- - 181     (let [items (or (:items x) (:orderedItems x))] -
- - 182       (and -
- - 183        (cond -
- - 184          (:items x) (nil? (:orderedItems x)) -
- - 185          (:orderedItems x) (nil? (:items x)) ;; can't have both properties -
- - 186          (integer? (:totalItems x)) true ;; can have neither, provided it has totalItems. -
- - 187          :else false)  -
- - 188        (object? x) -
- - 189        (= (:type x) object-type) -
- - 190        (if items -
- - 191          (and (coll? items) + 166    [x]
- 192               (every? object? items) ;; if there are items, they must form a -
- - 193                                      ;; collection of objects. -
- - 194               true) -
- - 195          true) ;; but it's OK if there aren't. -
- - 196        true) -
- - 197       ;; test for totalItems not done here, because collection pages don't -
- - 198       ;; have it. -
- - 199       )) -
- - 200    ([x] -
- - 201     (and -
- - 202      (or (collection? x "Collection") -
- - 203          (collection? x "OrderedCollection")) -
- - 204      (integer? (:totalItems x)) -
- - 205      true))) + 167    (collection? x "OrderedCollectionPage"))
- 206   -
- - 207  (defn unordered-collection? -
- - 208    "`true` iff `x` quacks like an unordered collection, else `false`." -
- - 209    [x] -
- - 210    (and (collection? x "Collection") (integer? (:totalItems x)) true)) + 168  
- 211   -
- - 212  (defn ordered-collection? -
- - 213    "`true` iff `x` quacks like an ordered collection, else `false`." -
- - 214    [x] -
- - 215    (and (collection? x "OrderedCollection") (integer? (:totalItems x)) true)) -
- - 216   -
- - 217  (defn collection-page? -
- - 218    "`true` iff `x` quacks like a page in a paged collection, else `false`." -
- - 219    [x] -
- - 220    (collection? x "CollectionPage")) -
- - 221   -
- - 222  (defn ordered-collection-page? -
- - 223    "`true` iff `x` quacks like a page in an ordered paged collection, else `false`." -
- - 224    [x] -
- - 225    (collection? x "OrderedCollectionPage")) -
- - 226   -
- - 227   + 169  
diff --git a/docs/cloverage/dog_and_duck/scratch/core.clj.html b/docs/cloverage/dog_and_duck/scratch/core.clj.html index eb11848..d9475e2 100644 --- a/docs/cloverage/dog_and_duck/scratch/core.clj.html +++ b/docs/cloverage/dog_and_duck/scratch/core.clj.html @@ -10,17 +10,65 @@ 002  
+ + 003  ;;;     Copyright (C) Simon Brooke, 2022 +
+ + 004   +
+ + 005  ;;;     This program is free software; you can redistribute it and/or +
+ + 006  ;;;     modify it under the terms of the GNU General Public License +
+ + 007  ;;;     as published by the Free Software Foundation; either version 2 +
+ + 008  ;;;     of the License, or (at your option) any later version. +
+ + 009   +
+ + 010  ;;;     This program is distributed in the hope that it will be useful, +
+ + 011  ;;;     but WITHOUT ANY WARRANTY; without even the implied warranty of +
+ + 012  ;;;     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +
+ + 013  ;;;     GNU General Public License for more details. +
+ + 014   +
+ + 015  ;;;     You should have received a copy of the GNU General Public License +
+ + 016  ;;;     along with this program; if not, write to the Free Software +
+ + 017  ;;;     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. +
+ + 018   +
- 003  (defn foo + 019  (defn foo
- 004    "I don't do a whole lot." + 020    "I don't do a whole lot."
- 005    [x] + 021    [x]
- 006    (println x "Hello, World!")) + 022    (println x "Hello, World!"))
diff --git a/docs/cloverage/dog_and_duck/scratch/scratch.clj.html b/docs/cloverage/dog_and_duck/scratch/scratch.clj.html index 6ed1ed5..4452f7b 100644 --- a/docs/cloverage/dog_and_duck/scratch/scratch.clj.html +++ b/docs/cloverage/dog_and_duck/scratch/scratch.clj.html @@ -17,175 +17,163 @@ 004              [clj-activitypub.webfinger :as webfinger]

- 005              [clj-pgp.core :as pgp] + 005              [clj-pgp.generate :as pgp-gen]
- 006              [clj-pgp.keyring :as keyring] -
- - 007              [clj-pgp.generate :as pgp-gen] -
- - 008              [clojure.walk :refer [keywordize-keys]] -
- - 009              [clojure.pprint :refer [pprint]])) + 006              [clojure.walk :refer [keywordize-keys]]))
- 010   + 007  
- 011  ;;;     Copyright (C) Simon Brooke, 2022 + 008  ;;;     Copyright (C) Simon Brooke, 2022
- 012   + 009  
- 013  ;;;     This program is free software; you can redistribute it and/or + 010  ;;;     This program is free software; you can redistribute it and/or
- 014  ;;;     modify it under the terms of the GNU General Public License + 011  ;;;     modify it under the terms of the GNU General Public License
- 015  ;;;     as published by the Free Software Foundation; either version 2 + 012  ;;;     as published by the Free Software Foundation; either version 2
- 016  ;;;     of the License, or (at your option) any later version. + 013  ;;;     of the License, or (at your option) any later version.
- 017       + 014      
- 018  ;;;     This program is distributed in the hope that it will be useful, + 015  ;;;     This program is distributed in the hope that it will be useful,
- 019  ;;;     but WITHOUT ANY WARRANTY; without even the implied warranty of + 016  ;;;     but WITHOUT ANY WARRANTY; without even the implied warranty of
- 020  ;;;     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + 017  ;;;     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- 021  ;;;     GNU General Public License for more details. + 018  ;;;     GNU General Public License for more details.
- 022       + 019      
- 023  ;;;     You should have received a copy of the GNU General Public License + 020  ;;;     You should have received a copy of the GNU General Public License
- 024  ;;;     along with this program; if not, write to the Free Software + 021  ;;;     along with this program; if not, write to the Free Software
- 025  ;;;     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. + 022  ;;;     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. +
+ + 023   +
+ + 024  ;;; Use any ActivityPub account handle you like - for example, your own +
+ + 025  (def account-handle "@simon_brooke@mastodon.scot")
026  
- - 027  ;;; Use any ActivityPub account handle you like - for example, your own -
- - 028  (def account-handle "@simon_brooke@mastodon.scot") -
- - 029   + + 027  (def handle (activitypub/parse-account account-handle))
- 030  (def handle (activitypub/parse-account account-handle)) -
- - 031  (webfinger/fetch-user-id "mastodon.scot" "simon_brooke") + 028  (webfinger/fetch-user-id "mastodon.scot" "simon_brooke")
- 032  (apply webfinger/fetch-user-id (map handle [:domain :username])) + 029  (apply webfinger/fetch-user-id (map handle [:domain :username]))
- 033   + 030  
- 034  ;;; Retrieve the account details from its home server + 031  ;;; Retrieve the account details from its home server
- 035  ;;; (`keywordize-keys` is not necessary here but produces a more idiomatic clojure + 032  ;;; (`keywordize-keys` is not necessary here but produces a more idiomatic clojure
- 036  ;;; data structure) + 033  ;;; data structure)
- 037  (def account + 034  (def account
- 038    "Fetch my account to mess with" + 035    "Fetch my account to mess with"
- 039    (let [handle (activitypub/parse-account account-handle)] + 036    (let [handle (activitypub/parse-account account-handle)]
- 040      (keywordize-keys + 037      (keywordize-keys
- 041       (activitypub/fetch-user + 038       (activitypub/fetch-user
- 042        (apply webfinger/fetch-user-id (map handle [:domain :username])))))) + 039        (apply webfinger/fetch-user-id (map handle [:domain :username])))))) +
+ + 040   +
+ + 041  ;;; examine what you got back! +
+ + 042  (:inbox account)
043  
- 044  ;;; examine what you got back! + 044  ;; (def rsa (pgp-gen/rsa-keypair-generator 2048))
- - 045  (:inbox account) + + 045  ;; (def kp (pgp-gen/generate-keypair rsa :rsa-general))
046  
- - 047   -
- - 048  (def rsa (pgp-gen/rsa-keypair-generator 2048)) -
- - 049  (def kp (pgp-gen/generate-keypair rsa :rsa-general)) -
- - 050   + + 047  ;; how we make a public/private key pair. But this key pair is not the one 
- 051  ;; how we make a public/private key pair. But this key pair is not the one  + 048  ;; known to mastodon.scot as my key pair, so that doesn't get us very far...
- 052  ;; known to mastodon.scot as my key pair, so that doesn't get us very far... -
- - 053  ;; I think. + 049  ;; I think.
- 054  (let [rsa (pgp-gen/rsa-keypair-generator 2048) + 050  (let [rsa (pgp-gen/rsa-keypair-generator 2048)
- 055        kp (pgp-gen/generate-keypair rsa :rsa-general) + 051        kp (pgp-gen/generate-keypair rsa :rsa-general)
- 056        public (-> kp .getPublicKey .getEncoded) + 052        public (-> kp .getPublicKey .getEncoded)
- 057        private (-> kp .getPrivateKey .getPrivateKeyDataPacket .getEncoded)] + 053        private (-> kp .getPrivateKey .getPrivateKeyDataPacket .getEncoded)]
- 058    (println (str "Public key:  " public)) + 054    (println (str "Public key:  " public))
- 059    (println (str "Private key: " private)) + 055    (println (str "Private key: " private))
- 060    ) + 056    )
- 061   + 057  
diff --git a/docs/cloverage/dog_and_duck/utils/process.clj.html b/docs/cloverage/dog_and_duck/utils/process.clj.html index 51feb91..cd06dd2 100644 --- a/docs/cloverage/dog_and_duck/utils/process.clj.html +++ b/docs/cloverage/dog_and_duck/utils/process.clj.html @@ -13,65 +13,146 @@ 003  
+ + 004  ;;;     Copyright (C) Simon Brooke, 2022 +
+ + 005   +
+ + 006  ;;;     This program is free software; you can redistribute it and/or +
+ + 007  ;;;     modify it under the terms of the GNU General Public License +
+ + 008  ;;;     as published by the Free Software Foundation; either version 2 +
+ + 009  ;;;     of the License, or (at your option) any later version. +
+ + 010   +
+ + 011  ;;;     This program is distributed in the hope that it will be useful, +
+ + 012  ;;;     but WITHOUT ANY WARRANTY; without even the implied warranty of +
+ + 013  ;;;     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +
+ + 014  ;;;     GNU General Public License for more details. +
+ + 015   +
+ + 016  ;;;     You should have received a copy of the GNU General Public License +
+ + 017  ;;;     along with this program; if not, write to the Free Software +
+ + 018  ;;;     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. +
+ + 019   +
- 004  (def pid + 020  (def get-pid
- 005    "OK, this is hacky as fuck, but I hope it works. The problem is that the + 021    "Get the process id of the current process.
- 006     way to get the process id has changed several times during the history + 022     
- 007     of Java development, and the code for one version of Java won't even compile + 023     OK, this is hacky as fuck, but I hope it works. The problem is that the
- 008     in a different version." + 024     way to get the process id has changed several times during the history
- - 009    (let [java-version (read-string (apply str (take 2 + + 025     of Java development, and the code for one version of Java won't even compile
- - 010                                                     (split + + 026     in a different version."
- 011                                                      (System/getProperty "java.version") + 027    (memoize +
+ + 028     (fn [] +
+ + 029       (let [java-version (read-string (apply str (take 2 +
+ + 030                                                     (split +
+ + 031                                                      (System/getProperty "java.version")
- 012                                                      #"[_\.]")))) + 032                                                      #"[_\.]"))))
- 013          cmd (case java-version + 033          cmd (case java-version
- 014                18 "(let [[_ pid hostname] + 034                18 "(let [[_ pid hostname]
- 015                      (re-find + 035                      (re-find
- 016                        #\"^(\\d+)@(.*)\" + 036                        #\"^(\\d+)@(.*)\"
- 017                        (.getName + 037                        (.getName
- 018                          (java.lang.management.ManagementFactory/getRuntimeMXBean)))] + 038                          (java.lang.management.ManagementFactory/getRuntimeMXBean)))]
- 019                      pid)" + 039                      pid)"
- 020                (19 110) "(.pid (java.lang.ProcessHandle/current))" + 040                (19 110) "(.pid (java.lang.ProcessHandle/current))"
- 021                111 "(.getPid (java.lang.management.ManagementFactory/getRuntimeMXBean))" + 041                111 "(.getPid (java.lang.management.ManagementFactory/getRuntimeMXBean))"
- 022                ":default")] + 042                ":default")]
- 023      (eval (read-string cmd)))) + 043      (eval (read-string cmd)))))) +
+ + 044   +
+ + 045  (def get-hostname  +
+ + 046    "return the hostname of the current host. +
+ + 047      +
+ + 048     Java's methods for getting the hostname are quite startlingly slow, we +
+ + 049     do not want todo this repeatedly!" +
+ + 050    (memoize (fn [] (.. java.net.InetAddress getLocalHost getHostName))))
diff --git a/docs/cloverage/index.html b/docs/cloverage/index.html index 6f54e71..71a7ff8 100644 --- a/docs/cloverage/index.html +++ b/docs/cloverage/index.html @@ -96,47 +96,47 @@ dog-and-duck.quack.fault-messages
19
+ float:left;"> 25 100.00 %
3
100.00 % -1213 +3153 dog-and-duck.quack.picky
283
18
-94.02 % + style="width:53.432642487046635%; + float:left;"> 825
719
+53.43 %
60
4
1
-98.46 % -1621165 + style="width:53.57142857142857%; + float:left;"> 135
13
104
+58.73 % +54741252 dog-and-duck.quack.quack
237
253
-48.37 % + style="width:46.56488549618321%; + float:left;"> 122
140
+46.56 %
29
11
27
-59.70 % -2272667 + style="width:51.02040816326531%; + float:left;"> 25
6
18
+63.27 % +1692149 dog-and-duck.scratch.core
1
66.67 % -613 +2253 dog-and-duck.scratch.parser
dog-and-duck.scratch.scratch
75
+ float:left;"> 66
100.00 %
19
+ float:left;"> 17 100.00 % -611019 +57917 dog-and-duck.utils.process
25
34
4
-86.21 % +89.47 %
6
10
1
100.00 % -2317 +50611 Totals: -61.92 % +54.39 % -67.69 % +60.38 % diff --git a/docs/codox/Using_ActivityPub.html b/docs/codox/Using_ActivityPub.html index 7c66e9d..ea14e0d 100644 --- a/docs/codox/Using_ActivityPub.html +++ b/docs/codox/Using_ActivityPub.html @@ -2,4 +2,37 @@ ""> Using ActivityPub

Using ActivityPub

Introduction

-

I do not know what I am doing; I am learning, and playing. Nothing in this document should be treated as good advice; it simply relates to the current state of my knowledge.

\ No newline at end of file +

I do not know what I am doing; I am learning, and playing. Nothing in this document should be treated as good advice; it simply relates to the current state of my knowledge.

+

What happens when you post a new item to an ActivityPub server

+

Your client issues a POST request to your outbox URI, with the object you’re posting as payload. It should be wrapped in a Create activity, but the spec makes clear that:

+
+

The server MUST accept a valid [ActivityStreams] object that isn’t a subtype of Activity in the POST request to the outbox

+
+

If no Create wrapper is present, the server creates one before further processing the request.

+

The Create wrapper, or, if that is missing, the object itself, may have properties to, bto, cc, bcc, and audience.

+

The to, bto, cc, and bcc properties are all expected to be either URIs of actors, or URIs of collections of actors. The behaviour of your local ActivityPub server in response to these properties is similar: it sends a POST request to the inbox URI associated with each actor URI.

+

There’s one ‘magic’ URI, "https://www.w3.org/ns/activitystreams#Public". If this is specified in any of the above fields (including audience), then the wrapped object is sent to the outboxes of each actor on your followers list; but also, it is recognised by other ActivityPub servers as a public post, and will thus appear in their ‘Federated’ feed as well as in the individual feeds your followers and of people directly addressed.

+

Things I don’t yet understand about Create

+
    +
  1. Is the entire new object transmitted in the Create transmission to each addressee, or only its (URI) id value?
  2. +
  3. What happens if an addressee’s home server is down at the time the object is posted? Is it queued for them and subsequently retried until it is delivered, or is it dropped?
  4. +
  5. When multiple actors on one host server are addressed in a Create request, is the inbox URI for each actor individually posted to, or is there one ‘postmaster’ endpoint on the server which can be addressed, from which the post can then be distributed to the particular actors’ inboxes?
  6. +
  7. What is the response your local server makes to your client? Does it contain the id of the object created, or is that id generated by the client in the first place?
  8. +
  9. If an item doesn’t have the magic public URL among its addressees, is an attempt to GET that item checked for whether the originator of the request is one of the explicit addressees? Or is such a request simply refused 401 not authorised?
  10. +
+

Obviously if the outbox being posted to is not the outbox of the duly authenticated and authorised logged in user, the attempt to post to an outbox must fail with a 401 not authorised response.

+

What happens when you post an Update activity regarding an existing item

+

Your client issues a POST request to your outbox URI, with the Update activity object as payload. The Update activity will have in its object field a partially specified copy of the object to be updated, containing only the id value and the values of those fields to be changed. Your local server will update its stored representation of the object.

+

I am not clear whether it will retransmit the Update to users addressed in the Create object. I see messages of the form ‘[user] edited a post’ in my Notifications feed on Mastodon, so it seems so.

+

What happens when you post a Delete activity regarding an existing item

+

Your client issues a POST request to your outbox URI, with the Delete activity object as payload. The Delete activity object has in its object value (probably?) only the id value of the object to be deleted, or, at most, a Link object having that has that id value as its href value.

+

The Delete object is NOT retransmitted to the addressees of the original create request. Instead, your server will either:

+
    +
  1. Return a 404 reponse to all subsequent requests for the object, or
  2. +
  3. Return a 410 ‘Gone’ response, having as payload a Tombstone object.
  4. +
+

The Tombstone object

+

The Tombstone object is a means of acknowledging that the requested object did once exist. It is an ActivityStreams object with the same id value as the original (deleted) object,the type value Tombstone, and the following fields: published, deleted, and (presumably optionally) updated. The values of these fields are timestamps (? or in the case of updated, perhaps lists of timestamps?)

+

What I don’t yet understand about this whole lifecycle

+

If we’re pushing entire objects – which may include media attachments – to the inboxes of many recipients who may never choose to read them, that feels like a lot of wasted bandwidth. However, if we’re pushing only ids or links of posts which are not public, that feels like a major security headache in verifying that the requestors are indeed verified recipients.

+

Again, the fact that Mastodon is able to show me ‘[user] edited a post’ items in my notifications seems to imply that updates of the complete edited object are being pushed out to all recipients’ inboxes, and again that seems expensive.

\ No newline at end of file diff --git a/docs/codox/Validation_Faults.html b/docs/codox/Validation_Faults.html index 456b40f..712e88d 100644 --- a/docs/codox/Validation_Faults.html +++ b/docs/codox/Validation_Faults.html @@ -27,6 +27,7 @@

Severity

Each fault report object MUST have a severity field whose value MUST be one of

    +
  1. :info things which are not actuallys fault, but issues noted during validation;
  2. :minor things which I consider to be faults, but which don’t actually breach the spec;
  3. :should instances where the spec says something SHOULD be done, which isn’t;
  4. :must instances where the spec says something MUST be done, which isn’t;
  5. diff --git a/docs/codox/dog-and-duck.quack.fault-messages.html b/docs/codox/dog-and-duck.quack.fault-messages.html index fbb29ad..f548cda 100644 --- a/docs/codox/dog-and-duck.quack.fault-messages.html +++ b/docs/codox/dog-and-duck.quack.fault-messages.html @@ -1,3 +1,3 @@ -dog-and-duck.quack.fault-messages documentation

    dog-and-duck.quack.fault-messages

    TODO: write docs

    messages

    Actual fault messages to which fault codes resolve.

    \ No newline at end of file +dog-and-duck.quack.fault-messages documentation

    dog-and-duck.quack.fault-messages

    TODO: write docs

    messages

    Actual fault messages to which fault codes resolve.

    \ No newline at end of file diff --git a/docs/codox/dog-and-duck.quack.picky.html b/docs/codox/dog-and-duck.quack.picky.html index 067ac88..c98eff0 100644 --- a/docs/codox/dog-and-duck.quack.picky.html +++ b/docs/codox/dog-and-duck.quack.picky.html @@ -1,6 +1,6 @@ -dog-and-duck.quack.picky documentation

    dog-and-duck.quack.picky

    Fault-finder for ActivityPub documents.

    +dog-and-duck.quack.picky documentation

    dog-and-duck.quack.picky

    Fault-finder for ActivityPub documents.

    Generally, each -faults function will return:

    1. nil if no faults were found;
    2. @@ -10,17 +10,43 @@
      1. :@context whose value shall be the URL of a document specifying this vocabulary;
      2. :type whose value shall be Fault;
      3. -
      4. :severity whose value shall be one of minor, should, must or critical;
      5. +
      6. :severity whose value shall be one of info, minor, should, must or critical;
      7. :fault whose value shall be a unique token representing the particular fault type;
      8. :narrative whose value shall be a natural language description of the fault type.

      Note that the reason for the :fault property is to be able to have a well known place, linked to from the @context URL, which allows narratives for each fault type to be served in as many natural languages as possible.

      -

      The idea further is that it should ultimately be possible to serialise a fault report as a document which in its own right conforms to the ActivityStreams spec.

    activitystreams-context-uri

    The URI of the context of an ActivityStreams object is expected to be this literal string.

    context?

    (context? x)

    Returns true iff x quacks like an ActivityStreams context, else false.

    -

    A context is either 1. the URI (actually an IRI) activitystreams-context-uri, or 2. a collection comprising that URI and a map.

    filter-severity

    (filter-severity reports severity)

    Return a list of reports taken from these reports where the severity of the report is greater than this severity.

    has-context?

    macro

    (has-context? x)

    True if x is an ActivityStreams object with a valid context, else false.

    make-fault-object

    (make-fault-object severity fault)

    Return a fault object with these severity, fault and narrative values.

    -

    An ActivityPub object MUST have a globally unique ID. Whether this is meaningful depends on whether we persist fault report objects and serve them, which at present I have no plans to do.

    object-faults

    (object-faults x)

    Return a list of faults found in object x, or nil if none are.

    persistent-object-faults

    (persistent-object-faults x)

    Return a list of faults found in persistent object x, or nil if none are.

    severity

    Severity of faults found, as follows:

    +

    The idea further is that it should ultimately be possible to serialise a fault report as a document which in its own right conforms to the ActivityStreams spec.

    *reify-refs*

    dynamic

    If true, references to objects in fields will be reified and validated. If false, they won’t, but an :info level fault report will be generated.

    +

    There are several things in the spec which, in a document, may correctly be either

      +
    1. a fully fleshed out object, or
    2. +
    3. a URI pointing to such an object.
    4. +
    +

    Obviously to fully validate a document we ought to reify all the refs and check that they are themselves valid, but

    +

    a. in some of the published test documents the URIs do not reference a valid document; b. there will be performance costs to reifying all the refs; c. in perverse cases, reifying refs might result in runaway recursion.

    +

    TODO: I think that in production this should default to true.

    *reject-severity*

    dynamic

    The severity at which the binary validator will return false.

    +

    In practice documents seen in the wild do not typically appear to be fully valid, and this does not matter. This allows the sensitivity of the binary validator (dog-and-duck.quack.quack) to be tuned. It’s in this (dog-and-duck.quack.picky) namespace, not that one, because this namespace is where concerns about severity are handled.

    accept-required-properties

    As base-activity-required-properties, except that the type of the object is restricted.

    activity-faults

    (activity-faults x)

    TODO: write docs

    activity-required-properties

    Properties activities should have, keyed by activity type. Values are maps of the format of base-activity-required-properties, q.v.

    activity-type-faults

    (activity-type-faults x)(activity-type-faults x type)

    Return a list of faults found in the activity x; if type is also specified, it should be a string naming a specific activity type for which checks should be performed.

    +

    Some specific activity types have specific requirements which are not requirements.

    activitystreams-context-uri

    The URI of the context of an ActivityStreams object is expected to be this literal string.

    actor-faults

    (actor-faults x)

    Return a list of faults found in actor x, or nil if none are.

    actor-type?

    (actor-type? x)

    Return true if the x is a recognised actor type, else false.

    actor-types

    The set of types we will accept as actors.

    +

    There’s an explicit set of allowed actor types.

    base-activity-required-properties

    Properties most activities should have. Values are validating functions, each.

    +

    See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-activity

    coll-object-reference-or-fault

    (coll-object-reference-or-fault value expected-type severity token)

    As object-reference-or-fault, except value argument may also be a list of objects and/or object references.

    context-key

    The Clojure reader barfs on :@context, although it is in principle a valid keyword. So we’ll make it once, here, to make the code more performant and easier to read.

    context?

    (context? x)

    Returns true iff x quacks like an ActivityStreams context, else false.

    +

    A context is either 1. the URI (actually an IRI) activitystreams-context-uri, or 2. a collection comprising that URI and a map.

    filter-severity

    (filter-severity reports severity)

    Return a list of reports taken from these reports where the severity of the report is greater than this or equal to this severity.

    has-activity-type?

    (has-activity-type? x)

    Return true if the object x has a type which is an activity type, else false.

    has-actor-type?

    (has-actor-type? x)

    Return true if the object x has a type which is an actor type, else false.

    has-context?

    macro

    (has-context? x)

    True if x is an ActivityStreams object with a valid context, else false.

    has-type-or-fault

    (has-type-or-fault x acceptable severity token)

    If object x has a :type value which is acceptable, return nil; else return a fault object with this severity and token.

    +

    acceptable may be passed as either nil, a string, or a set of strings. If acceptable is nil, no type specific tests will be performed.

    has-type?

    (has-type? x type)

    Return true if object x has type type, else false.

    +

    The values of type fields of ActivityStreams objects may be lists; they are considered to have a type if the type token is a member of the list.

    intransitive-activity-required-properties

    Properties intransitive activities should have.

    +

    See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-intransitiveactivity

    make-fault-object

    (make-fault-object severity fault)

    Return a fault object with these severity, fault and narrative values.

    +

    An ActivityPub object MUST have a globally unique ID. Whether this is meaningful depends on whether we persist fault report objects and serve them, which at present I have no plans to do.

    nil-if-empty

    macro

    (nil-if-empty x)

    if x is an empty collection, return nil; else return x.

    object-faults

    (object-faults x)(object-faults x expected-type)

    Return a list of faults found in object x, or nil if none are.

    +

    If expected-type is also passed, verify that x has expected-type. expected-type may be passed as a string or as a set of strings.

    object-reference-or-faults

    (object-reference-or-faults value expected-type severity token)

    If this value is either

    +
      +
    1. an object of expected-type;
    2. +
    3. a URI referencing an object of expected-type; or
    4. +
    5. a link object referencing an object of expected-type
    6. +
    +

    and no faults are returned from validating the linked object, then return nil; else return a sequence comprising a fault object with this severity and token, prepended to the faults returned.

    +

    As with has-type-or-fault (q.v.), expected-type may be passed as a string or as a set of strings.

    +

    NOTE THAT if *reify-refs* is false, referenced objects will not actually be checked.

    persistent-object-faults

    (persistent-object-faults x)

    Return a list of faults found in persistent object x, or nil if none are.

    severity

    Severity of faults found, as follows:

    +
      +
    1. :info not actually a fault, but an issue noted during validation;
    2. :minor things which I consider to be faults, but which don’t actually breach the spec;
    3. :should instances where the spec says something SHOULD be done, which isn’t;
    4. :must instances where the spec says something MUST be done, which isn’t;
    5. :critical instances where I believe the fault means that the object cannot be meaningfully processed.
    6. -

    severity-filters

    Hack for implementing a severity hierarchy

    validation-fault-context-uri

    The URI of the context of a validation fault report object shall be this literal string.

    \ No newline at end of file +

severity-filters

Hack for implementing a severity hierarchy

string-or-fault

(string-or-fault value severity token)(string-or-fault value severity token pattern)

If this value is not a string, return a fault object with this severity and token, else nil. If pattern is also passed, it is expected to be a Regex, and the fault object will be returned unless value matches the pattern.

truthy?

(truthy? x)

Return true if x is truthy, else false.

uri-or-fault

(uri-or-fault u severity if-missing-token)(uri-or-fault u severity if-missing-token if-invalid-token)

If u is not a valid URI, return a fault object with this severity and if-invalid-token. If it’s nil, return a fault object with this severity and if-missing-token. Otherwise return nil.

validation-fault-context-uri

The URI of the context of a validation fault report object shall be this literal string.

verb-type?

(verb-type? x)

true if x, a string, represents a recognised ActivityStreams activity type.

verb-types

The set of types we will accept as verbs.

+

There’s an explicit set of allowed verb types.

\ No newline at end of file diff --git a/docs/codox/dog-and-duck.quack.quack.html b/docs/codox/dog-and-duck.quack.quack.html index f2f9782..108053c 100644 --- a/docs/codox/dog-and-duck.quack.quack.html +++ b/docs/codox/dog-and-duck.quack.quack.html @@ -1,21 +1,14 @@ -dog-and-duck.quack.quack documentation

dog-and-duck.quack.quack

Validator for ActivityPub objects: if it walks like a duck, and it quacks like a duck…

+dog-and-duck.quack.quack documentation

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

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.

activity?

(activity? x)

true iff x quacks like an activity, else false.

actor-or-uri?

(actor-or-uri? x)

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.

actor-type?

macro

(actor-type? x)

Return true iff the x is a recognised actor type, else false.

actor-types

The set of types we will accept as actors.

-

There’s an explicit set of allowed actor types.

actor?

(actor? x)

Returns true if x quacks like an actor, else false.

-

NOTE THAT Section 4.1 of the spec 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

collection-page?

(collection-page? x)

true iff x quacks like a page in a paged collection, else false.

collection?

(collection? x object-type)(collection? x)

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.

link?

(link? x)

true iff x quacks like a link, else false.

object?

(object? x)(object? x severity)

Returns true iff x is recognisably an ActivityStreams object.

+

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.

activity?

(activity? x)(activity? x severity)

true iff x quacks like an activity, else false.

actor-or-uri?

(actor-or-uri? x)

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.

actor?

(actor? x)(actor? x severity)

Returns true if x quacks like an actor, else false.

collection-page?

(collection-page? x)

true iff x quacks like a page in a paged collection, else false.

collection?

(collection? x object-type)(collection? x)

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.

link?

(link? x)(link? x severity)

true iff x quacks like a link, else false.

object?

(object? x)(object? x severity)

Returns true iff x is recognisably an ActivityStreams object.

NOTE THAT The ActivityStreams spec says:

All properties are optional (including the id and type)

@@ -25,6 +18,5 @@

Implementers SHOULD include the ActivityPub context in their object definitions

-

but in samples found in the wild they typically don’t.

ordered-collection-page?

(ordered-collection-page? x)

true iff x quacks like a page in an ordered paged collection, else false.

ordered-collection?

(ordered-collection? x)

true iff x quacks like an ordered collection, else false.

persistent-object?

(persistent-object? x)

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).

unordered-collection?

(unordered-collection? x)

true iff x quacks like an unordered collection, else false.

verb-type?

macro

(verb-type? x)

TODO: write docs

verb-types

The set of types we will accept as verbs.

-

There’s an explicit set of allowed verb types.

\ No newline at end of file +

but in samples found in the wild they typically don’t.

ordered-collection-page?

(ordered-collection-page? x)

true iff x quacks like a page in an ordered paged collection, else false.

ordered-collection?

(ordered-collection? x)

true iff x quacks like an ordered collection, else false.

persistent-object?

(persistent-object? x)(persistent-object? x severity)

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).

unordered-collection?

(unordered-collection? x)

true iff x quacks like an unordered collection, else false.

\ No newline at end of file diff --git a/docs/codox/dog-and-duck.scratch.core.html b/docs/codox/dog-and-duck.scratch.core.html index 552db14..1a895c3 100644 --- a/docs/codox/dog-and-duck.scratch.core.html +++ b/docs/codox/dog-and-duck.scratch.core.html @@ -1,3 +1,3 @@ -dog-and-duck.scratch.core documentation

dog-and-duck.scratch.core

TODO: write docs

foo

(foo x)

I don’t do a whole lot.

\ No newline at end of file +dog-and-duck.scratch.core documentation

dog-and-duck.scratch.core

TODO: write docs

foo

(foo x)

I don’t do a whole lot.

\ No newline at end of file diff --git a/docs/codox/dog-and-duck.scratch.scratch.html b/docs/codox/dog-and-duck.scratch.scratch.html index 586cc4d..c0eae90 100644 --- a/docs/codox/dog-and-duck.scratch.scratch.html +++ b/docs/codox/dog-and-duck.scratch.scratch.html @@ -1,3 +1,3 @@ -dog-and-duck.scratch.scratch documentation

dog-and-duck.scratch.scratch

Scratchpad where I try to understand how to do this stuff.

account

Fetch my account to mess with

account-handle

TODO: write docs

handle

TODO: write docs

kp

TODO: write docs

rsa

TODO: write docs

\ No newline at end of file +dog-and-duck.scratch.scratch documentation

dog-and-duck.scratch.scratch

Scratchpad where I try to understand how to do this stuff.

account

Fetch my account to mess with

account-handle

TODO: write docs

handle

TODO: write docs

\ No newline at end of file diff --git a/docs/codox/dog-and-duck.utils.process.html b/docs/codox/dog-and-duck.utils.process.html index 5d78380..4390b03 100644 --- a/docs/codox/dog-and-duck.utils.process.html +++ b/docs/codox/dog-and-duck.utils.process.html @@ -1,3 +1,5 @@ -dog-and-duck.utils.process documentation

dog-and-duck.utils.process

TODO: write docs

pid

OK, this is hacky as fuck, but I hope it works. The problem is that the way to get the process id has changed several times during the history of Java development, and the code for one version of Java won’t even compile in a different version.

\ No newline at end of file +dog-and-duck.utils.process documentation

dog-and-duck.utils.process

TODO: write docs

get-hostname

return the hostname of the current host.

+

Java’s methods for getting the hostname are quite startlingly slow, we do not want todo this repeatedly!

get-pid

Get the process id of the current process.

+

OK, this is hacky as fuck, but I hope it works. The problem is that the way to get the process id has changed several times during the history of Java development, and the code for one version of Java won’t even compile in a different version.

\ No newline at end of file diff --git a/docs/codox/index.html b/docs/codox/index.html index c51768e..0aa65a0 100644 --- a/docs/codox/index.html +++ b/docs/codox/index.html @@ -1,3 +1,3 @@ -Dog-and-duck 0.1.0-SNAPSHOT

Dog-and-duck 0.1.0-SNAPSHOT

Released under the GPL-2.0-or-later

A playground for hacking ActivityPub stuff.

Installation

To install, add the following dependency to your project or build file:

[dog-and-duck "0.1.0-SNAPSHOT"]

Topics

Namespaces

clj-activitypub.core

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

clj-activitypub.internal.crypto

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

Public variables and functions:

clj-activitypub.internal.http-util

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

Public variables and functions:

clj-activitypub.internal.thread-cache

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

Public variables and functions:

clj-activitypub.webfinger

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

Public variables and functions:

dog-and-duck.quack.fault-messages

TODO: write docs

Public variables and functions:

dog-and-duck.scratch.core

TODO: write docs

Public variables and functions:

dog-and-duck.scratch.parser

TODO: write docs

Public variables and functions:

dog-and-duck.scratch.scratch

Scratchpad where I try to understand how to do this stuff.

Public variables and functions:

dog-and-duck.utils.process

TODO: write docs

Public variables and functions:

\ No newline at end of file +Dog-and-duck 0.1.0-SNAPSHOT

Dog-and-duck 0.1.0-SNAPSHOT

Released under the GPL-2.0-or-later

A playground for hacking ActivityPub stuff.

Installation

To install, add the following dependency to your project or build file:

[dog-and-duck "0.1.0-SNAPSHOT"]

Topics

Namespaces

clj-activitypub.core

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

clj-activitypub.internal.crypto

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

Public variables and functions:

clj-activitypub.internal.http-util

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

Public variables and functions:

clj-activitypub.internal.thread-cache

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

Public variables and functions:

clj-activitypub.webfinger

copied from Jahfer’s clj-activitypub library. If and when Jahfer issues a release of that library, this directory will be deleted and a dependency on that library will be added to the project.

Public variables and functions:

dog-and-duck.quack.fault-messages

TODO: write docs

Public variables and functions:

dog-and-duck.quack.quack

Validator for ActivityPub objects: if it walks like a duck, and it quacks like a duck…

dog-and-duck.scratch.core

TODO: write docs

Public variables and functions:

dog-and-duck.scratch.parser

TODO: write docs

Public variables and functions:

dog-and-duck.scratch.scratch

Scratchpad where I try to understand how to do this stuff.

Public variables and functions:

dog-and-duck.utils.process

TODO: write docs

Public variables and functions:

\ No newline at end of file