diff --git a/README.md b/README.md index 7952b24..97d9ad0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,11 @@ Good. Let us proceed. -**The Old Dog and Duck** is intended to be a set of libraries to enable people to build stuff which interacts with ActivityPub. It isn't intended to be a replacement for, or clone of, Mastodon. I do think I might implement my own ActivityPub server on top of The Old Dog and Duck, that specifically might allow for user-pluggable feed-sorting algorithms and with my own user interface/user experience take, but that project is not this project. +**The Old Dog and Duck** is intended to be a set of libraries to enable people to build stuff which interacts with ActivityPub. It isn't intended to be a replacement for, or clone of, Mastodon. I do think I might implement my own ActivityPub server on top of The Old Dog and Duck, that specifically might allow for user-pluggable feed-sorting algorithms and with my own user interface/user experience take, but that project is not (yet, at any rate) this project. + +## Status + +This is a long way pre-alpha. Everything will change. Feel free to play, but do so at your own risk. Contributions welcome. ## Architecture @@ -47,17 +51,31 @@ Where deliveries are ordered and arrive; and from where deliveries onwards are d Duck-typing for ActivityStreams objects. +As of version 0.1.0, this is substantially the only part that is yet at all useful, and it is still a long way from finished or robust. + ### Scratch What the dog does when bored. Essentially, a place where I can learn how to make this stuff work, but perhaps eventually an ActivityPub server in its own right. ## Usage -FIXME +At present, only the duck-typing functions work. To play with them, use + +```clojure +(require '[dog-and-duck.quack.quack :as q]) +``` + +## Testing + +Prior to testing, you should clone [activitystreams-test-documents](https://github.com/w3c-social/activitystreams-test-documents) into the `resources` directory. You can then test with + +```bash +lein test +``` ## License -Copyright © Simon Brooke, 2022 +Copyright © Simon Brooke, 2022. This program and the accompanying materials are made available under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your diff --git a/src/dog_and_duck/quack/quack.clj b/src/dog_and_duck/quack/quack.clj index 1790d04..7ba0ed6 100644 --- a/src/dog_and_duck/quack/quack.clj +++ b/src/dog_and_duck/quack/quack.clj @@ -20,7 +20,7 @@ ;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. (defn object? - "Return `true` iff `x` is recognisably an ActivityStreams object. + "Returns `true` iff `x` is recognisably an ActivityStreams object. **NOTE THAT** The ActivityStreams spec [says](https://www.w3.org/TR/activitystreams-core/#object): @@ -34,9 +34,6 @@ [x] (and (map? x) (:type x) true)) -(object? nil) -(object? {:type "test"}) - (defn persistent-object? "`true` iff `x` is a persistent object. @@ -49,31 +46,142 @@ (persistent-object? {:type "test" :id "https://mastodon.scot/@barfilfarm"}) -(defn actor? - "TODO!" - [x] - true) +(def ^:const actor-types + "The set of types we will accept as actors. + + There's an [explicit set of allowed actor types] + (https://www.w3.org/TR/activitystreams-vocabulary/#actor-types)." + #{"Application" + "Group" + "Organization" + "Person" + "Service"}) -(def verb? +(defn actor-type? + ;; TODO: better as a macro + [x] + (if (actor-types x) true false)) + +(def ^:const verb-types "The set of types we will accept as verbs. - There's an [explicit set of allowed verbs] + There's an [explicit set of allowed verb types] (https://www.w3.org/TR/activitystreams-vocabulary/#activity-types)." #{"Accept" "Add" "Announce" "Arrive" "Block" "Create" "Delete" "Dislike" "Flag" "Follow" "Ignore" "Invite" "Join" "Leave" "Like" "Listen" "Move" "Offer" "Question" "Reject" "Read" "Remove" "TentativeAccept" "TentativeReject" "Travel" "Undo" "Update" "View"}) -(defn activity? - "`true` iff `x` is an activity, else false. +(defn verb-type? + ;; TODO: better as a macro + [x] + (if (verb-types x) true false)) - see " +(def ^:const activitystreams-context-uri + "The URI of the context of an ActivityStreams object is expected to be this + literal string." + "https://www.w3.org/ns/activitystreams") + +(defn context? + "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." + [x] + (cond + (nil? x) false + (string? x) (and (= x activitystreams-context-uri) true) + (coll? x) (and (context? (first (remove map? x))) + (= (count x) 2) + true) + :else false)) + +(defmacro has-context? [x] + `(context? ((keyword "@context") ~x))) + +(defn actor? + "Returns `true` if `x` quacks like an actor, else false." + [x] + (and + (object? x) + (has-context? x) + (uri? (URI. (:inbox x))) + (uri? (URI. (:outbox x))) + (actor-type? (:type x)) + true)) + +(defn activity? + "`true` iff `x` quacks like an activity, else false." [x] (try (and (object? x) - (uri? (URI. ((keyword "@context") x))) + (has-context? x) (string? (:summary x)) (actor? (:actor x)) - (verb? (:type x)) - (or (object? (:object x)) (uri? (URI. x)))) - (catch URISyntaxException _ false))) \ No newline at end of file + (verb-type? (:type x)) + (or (object? (:object x)) (uri? (URI. (:object x)))) + true) + (catch URISyntaxException _ false))) + +(defn link? + "`true` iff `x` quacks like a link, else false." + [x] + (and (object? x) + (= (:type x) "Link") + (uri? (URI. (:href x))) + true)) + +(defn link-or-uri? + "`true` iff `x` is either a URI or a link, else false. + + There are several points in the specification where e.g. the `:image` + property (if present) may be either a link or a URI." + [x] + (and + (cond (string? x) (uri? (URI. x)) + :else (link? x)) + true)) + +(defn collection? + "`true` iff `x` quacks like a collection of type `type`, else `false`. + + With one argument, will recognise plain collections and ordered collections, + but (currently) not collection pages." + ([x type] + (let [items (or (:items x) (:orderedItems x))] + (and + (cond + (:items x) (nil? (:orderedItems x)) + (:orderedItems x) (nil? (:items x))) ;; can't have both properties + (object? x) + (= (:type x) type) + (coll? items) + (every? object? items) + (integer? (:totalItems x)) + true))) + ([x] + (or (collection? x "Collection") + (collection? x "OrderedCollection")))) + +(defn unordered-collection? + "`true` iff `x` quacks like an unordered collection, else `false`." + [x] + (collection? x "Collection")) + +(defn ordered-collection? + "`true` iff `x` quacks like an ordered collection, else `false`." + [x] + (collection? x "OrderedCollection")) + +(defn collection-page? + "`true` iff `x` quacks like a page in a paged collection, else `false`." + [x] + (collection? x "CollectionPage")) + +(defn ordered-collection-page? + "`true` iff `x` quacks like a page in an ordered paged collection, else `false`." + [x] + (collection? x "OrderedCollectionPage")) + + diff --git a/test/dog_and_duck/quack/quack_test.clj b/test/dog_and_duck/quack/quack_test.clj new file mode 100644 index 0000000..cf699ae --- /dev/null +++ b/test/dog_and_duck/quack/quack_test.clj @@ -0,0 +1,129 @@ +(ns dog-and-duck.quack.quack-test + (:require [clojure.test :refer [deftest is testing]] + [dog-and-duck.quack.quack :refer [activitystreams-context-uri + actor? actor-type? context? + object? persistent-object? + verb-type?]] + [dog-and-duck.scratch.parser :refer [clean]])) + +;;; Copyright (C) Simon Brooke, 2022 + +;;; This program is free software; you can redistribute it and/or +;;; modify it under the terms of the GNU General Public License +;;; as published by the Free Software Foundation; either version 2 +;;; of the License, or (at your option) any later version. + +;;; This program is distributed in the hope that it will be useful, +;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. + +;;; You should have received a copy of the GNU General Public License +;;; along with this program; if not, write to the Free Software +;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(deftest object-test + (testing "object recognition" + (let [expected false + actual (object? nil)] + (is (= actual expected))) + (let [expected true + actual (object? {:type "Test"})] + (is (= actual expected))) + (let [expected false + actual (object? + (first + (clean + (slurp "resources/activitystreams-test-documents/empty.json"))))] + (is (= actual expected))) + (let [expected true + actual (object? + (first + (clean + (slurp "resources/activitystreams-test-documents/core-ex1-jsonld.json"))))] + (is (= actual expected))))) + +(deftest persistent-object-test + (testing "persistent object recognition" + (let [expected false + actual (persistent-object? nil)] + (is (= actual expected) "Not persistent: not an object.")) + (let [expected true + actual (persistent-object? {:type "Test" :id "https://foo.bar/@ban"})] + (is (= actual expected) "Is persistent: has both id and type.")) + (let [expected false + actual (persistent-object? + (first + (clean + (slurp "resources/activitystreams-test-documents/simple0001.json"))))] + (is (= actual expected) "Not persistent: has no id.")) + (let [expected true + actual (persistent-object? + (first + (clean + (slurp "resources/activitystreams-test-documents/simple0008.json"))))] + (is (= actual expected) "Is persistent: has both id and type.")))) + +(deftest actor-type-test + (testing "identification of actor types" + (let [expected false + actual (actor-type? nil)] + (is (= actual expected) "nil is not an actor")) + (let [expected false + actual (actor-type? "Duck")] + (is (= actual expected) "A duck is not an actor")) + (let [expected true + actual (actor-type? "Person")] + (is (= actual expected) "A person is an actor")) + (let [expected true + actual (actor-type? "Service")] + (is (= actual expected) "A service is an actor")))) + +(deftest verb-type-test + (testing "identification of verb types" + (let [expected false + actual (verb-type? nil)] + (is (= actual expected) "nil is not a verb")) + (let [expected false + actual (verb-type? "Quack")] + (is (= actual expected) "Quack is not a verb")) + (let [expected true + actual (verb-type? "Create")] + (is (= actual expected) "Create is a verb")) + (let [expected true + actual (verb-type? "Reject")] + (is (= actual expected) "Reject is a verb")))) + +(deftest context-test + (testing "identification of valid contexts" + (let [expected false + actual (context? "https://foo.bar/ban/")] + (is (= actual expected) + "Only `activitystreams-context-uri` is valid as a context on its own")) + (let [expected true + actual (context? activitystreams-context-uri)] + (is (= actual expected) + "`activitystreams-context-uri` is valid as a context on its own")) + (let [expected false + actual (context? [{:foo "bar"} "https://foo.bar/ban/"])] + (is (= actual expected) + "Only `activitystreams-context-uri` is valid as a context uri")) + (let [expected true + actual (context? [{:foo "bar"} activitystreams-context-uri])] + (is (= actual expected) + "`activitystreams-context-uri` is valid as a context uri")) + (let [expected true + actual (context? [activitystreams-context-uri {:foo "bar"}])] + (is (= actual expected) + "order of elements within a context should not matter")) + )) + +(deftest actor-test + (testing "identification of actors" + (let [expected false + actual (actor? (-> "resources/activitystreams-test-documents/simple0008.json" slurp clean first))] + (is (= actual expected) "A Note is not an actor")) + (let [expected true + actual (actor? (-> "resources/activitystreams-test-documents/simple0020.json" slurp clean first :actor))] + (is (= actual expected) "A Person is an actor")) + )) \ No newline at end of file