Everything compiles, tests pass.

This commit is contained in:
Simon Brooke 2022-12-24 22:09:08 +00:00
parent 629f73ab4d
commit a60187eb2a
5 changed files with 86 additions and 24 deletions

View file

@ -3,3 +3,53 @@
## Introduction ## 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. 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*](https://www.w3.org/TR/activitypub/#create-activity-outbox) be wrapped in a `Create` activity, but the spec [makes clear that](https://www.w3.org/TR/activitypub/#object-without-create):
> 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. 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?
3. 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?
4. 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?
5. 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`?
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. Return a 410 'Gone' response, having as payload a `Tombstone` object.
### 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.

View file

@ -23,6 +23,9 @@
:no-context "Section 3 of the ActivityPub specification states Implementers SHOULD include the ActivityPub context in their object definitions`." :no-context "Section 3 of the ActivityPub specification states Implementers SHOULD include the ActivityPub context in their object definitions`."
:no-id-persistent "Persistent objects MUST have unique global identifiers." :no-id-persistent "Persistent objects MUST have unique global identifiers."
: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." :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."
:null-id-persistent "Persistent objects MUST have non-null identifiers." :no-inbox "Actor objects MUST have an `inbox` property, whose value MUST be a reference to an ordered collection."
:no-outbox "Actor objects MUST have an `outbox` property, whose value MUST be a reference to an ordered collection."
:no-type "The ActivityPub specification states that the `type` field is optional, but it is hard to process objects with no known type." :no-type "The ActivityPub specification states that the `type` field is optional, but it is hard to process objects with no known type."
:not-actor-type "The `type` value of the object was not a recognised actor type."
:null-id-persistent "Persistent objects MUST have non-null identifiers."
:not-an-object "ActivityStreams object must be JSON objects."}) :not-an-object "ActivityStreams object must be JSON objects."})

View file

@ -207,9 +207,8 @@
(defmacro nil-if-empty (defmacro nil-if-empty
"if `x` is an empty collection, return `nil`; else return `x`." "if `x` is an empty collection, return `nil`; else return `x`."
[x] [x]
`(if (coll? ~x) `(if (and (coll? ~x) (empty? ~x)) nil
(nil-if-empty ~x) ~x))
~x))
(defn has-type-or-fault (defn has-type-or-fault
"If object `x` has a `:type` value which is `acceptable`, return `nil`; "If object `x` has a `:type` value which is `acceptable`, return `nil`;
@ -271,7 +270,9 @@
(uri-or-fault u severity if-missing-token if-missing-token)) (uri-or-fault u severity if-missing-token if-missing-token))
([u severity if-missing-token if-invalid-token] ([u severity if-missing-token if-invalid-token]
(try (try
(uri? (URI. u)) (if (uri? (URI. u))
nil
(make-fault-object severity if-invalid-token))
(catch URISyntaxException _ (catch URISyntaxException _
(make-fault-object severity if-invalid-token)) (make-fault-object severity if-invalid-token))
(catch NullPointerException _ (catch NullPointerException _
@ -306,10 +307,10 @@
"Person" "Person"
"Service"}) "Service"})
(defmacro actor-type? (defn actor-type?
"Return `true` if the `x` is a recognised actor type, else `false`." "Return `true` if the `x` is a recognised actor type, else `false`."
[^String x] [^String x]
`(if (actor-types ~x) true false)) (if (actor-types x) true false))
(defn has-actor-type? (defn has-actor-type?
"Return `true` if the object `x` has a type which is an actor type, else "Return `true` if the object `x` has a type which is an actor type, else
@ -344,11 +345,11 @@
"Offer" "Question" "Reject" "Read" "Remove" "TentativeAccept" "Offer" "Question" "Reject" "Read" "Remove" "TentativeAccept"
"TentativeReject" "Travel" "Undo" "Update" "View"}) "TentativeReject" "Travel" "Undo" "Update" "View"})
(defmacro verb-type? (defn verb-type?
"`true` if `x`, a string, represents a recognised ActivityStreams activity "`true` if `x`, a string, represents a recognised ActivityStreams activity
type." type."
[^String x] [^String x]
`(if (verb-types ~x) true false)) (if (verb-types x) true false))
(defn has-activity-type? (defn has-activity-type?
"Return `true` if the object `x` has a type which is an activity type, else "Return `true` if the object `x` has a type which is an activity type, else
@ -474,8 +475,8 @@
:object :object
(fn [v] (fn [v]
(object-reference-or-faults v #{"Invite" "Person"} (object-reference-or-faults v #{"Invite" "Person"}
:must :must
:bad-accept-target)))) :bad-accept-target))))
(def ^:const activity-required-properties (def ^:const activity-required-properties
"Properties activities should have, keyed by activity type. Values are maps "Properties activities should have, keyed by activity type. Values are maps

View file

@ -15,7 +15,9 @@
(:require [dog-and-duck.quack.picky :refer [*reject-severity* activity-faults (:require [dog-and-duck.quack.picky :refer [*reject-severity* activity-faults
actor-faults filter-severity link-faults actor-faults filter-severity link-faults
object-faults persistent-object-faults]])) object-faults persistent-object-faults]])
(:import [java.net URI URISyntaxException]))
;;; Copyright (C) Simon Brooke, 2022 ;;; Copyright (C) Simon Brooke, 2022
@ -81,10 +83,13 @@
*must be* to an actor object, but before, may only be to a URI pointing to *must be* to an actor object, but before, may only be to a URI pointing to
one." one."
[x] [x]
(and (try
(cond (string? x) (uri? (URI. x)) (and
:else (actor? x)) (cond (string? x) (uri? (URI. x))
true)) :else (actor? x))
true)
(catch URISyntaxException _ false)
(catch NullPointerException _ false)))
(defn activity? (defn activity?
"`true` iff `x` quacks like an activity, else false." "`true` iff `x` quacks like an activity, else false."

View file

@ -1,11 +1,11 @@
(ns dog-and-duck.quack.quack-test (ns dog-and-duck.quack.quack-test
(:require [clojure.test :refer [deftest is testing]] (:require [clojure.test :refer [deftest is testing]]
[dog-and-duck.quack.picky :refer [activitystreams-context-uri [dog-and-duck.quack.picky :refer [activitystreams-context-uri
context? context-key]] actor-type? context? context-key
[dog-and-duck.quack.quack :refer [actor? actor-type?
object? ordered-collection-page?
persistent-object?
verb-type?]] verb-type?]]
[dog-and-duck.quack.quack :refer [actor?
object? ordered-collection-page?
persistent-object?]]
[dog-and-duck.scratch.parser :refer [clean]])) [dog-and-duck.scratch.parser :refer [clean]]))
;;; Copyright (C) Simon Brooke, 2022 ;;; Copyright (C) Simon Brooke, 2022
@ -126,13 +126,16 @@
(is (= actual expected) "A Note is not an actor")) (is (= actual expected) "A Note is not an actor"))
(let [expected false (let [expected false
actual (actor? (-> "resources/activitystreams-test-documents/simple0020.json" slurp clean first :actor))] actual (actor? (-> "resources/activitystreams-test-documents/simple0020.json" slurp clean first :actor))]
(is (= actual expected) "The Person in this file is not valid as an actor, because it lacks a context.")) (is (= actual expected) "The Person in this file is not valid as an actor, because it lacks a context, https id, and outbox."))
(let [o (assoc (-> "resources/activitystreams-test-documents/simple0020.json" (let [o (assoc (-> "resources/activitystreams-test-documents/simple0020.json"
slurp slurp
clean clean
first first
:actor) :actor)
context-key activitystreams-context-uri) context-key activitystreams-context-uri
:id "https://example.org/@sally"
:inbox "https://example.org/@sally/inbox"
:outbox "https://example.org/@sally/outbox")
expected true expected true
actual (actor? o)] actual (actor? o)]
(is (= actual expected) (str "The Person from this file is now valid as an actor, because it has a context." o))))) (is (= actual expected) (str "The Person from this file is now valid as an actor, because it has a context." o)))))