001  (ns clj-activitypub.core
002    "copied from [Jahfer's clj-activitypub library](https://github.com/jahfer/clj-activitypub). 
003     If and when Jahfer issues a release of that library, this directory will be deleted and a 
004     dependency on that library will be added to the project."
005    (:require [clj-activitypub.internal.crypto :as crypto]
006              [clj-activitypub.internal.thread-cache :as thread-cache]
007              [clj-activitypub.internal.http-util :as http]
008              [clj-http.client :as client]
009              [clojure.string :as str]))
010  
011  (defn config
012    "Creates hash of computed data relevant for most ActivityPub utilities."
013    [{:keys [domain username username-route public-key private-key]
014      :or {username-route "/users/"
015           public-key nil
016           private-key nil}}]
017    (let [base-url (str "https://" domain)]
018      {:domain domain
019       :base-url base-url
020       :username username
021       :user-id (str base-url username-route username)
022       :public-key public-key
023       :private-key (when private-key
024                      (crypto/private-key private-key))}))
025  
026  (defn parse-account
027    "Given an ActivityPub handle (e.g. @jahfer@mastodon.social), produces
028     a map containing {:domain ... :username ...}."
029    [handle]
030    (let [[username domain] (filter #(not (str/blank? %))
031                                    (str/split handle #"@"))]
032      {:domain domain :username username}))
033  
034  (def ^:private user-cache (thread-cache/make))
035  (defn fetch-user
036    "Fetches the customer account details located at user-id from a remote
037     server. Will return cached results if they exist in memory."
038    [user-id]
039    ((:get-v user-cache)
040     user-id
041     #(:body
042       (client/get user-id {:as :json-string-keys
043                            :throw-exceptions false
044                            :ignore-unknown-host? true
045                            :headers {"Accept" "application/activity+json"}}))))
046  
047  (defn actor
048    "Accepts a config, and returns a map in the form expected by the ActivityPub
049     spec. See https://www.w3.org/TR/activitypub/#actor-objects for reference."
050    [{:keys [user-id username public-key]}]
051    {"@context" ["https://www.w3.org/ns/activitystreams"
052                 "https://w3id.org/security/v1"]
053     :id user-id
054     :type "Person"
055     :preferredUsername username
056     :inbox (str user-id "/inbox")
057     :outbox (str user-id "/outbox")
058     :publicKey {:id (str user-id "#main-key")
059                 :owner user-id
060                 :publicKeyPem (or public-key "")}})
061  
062  (def signature-headers ["(request-target)" "host" "date" "digest"])
063  
064  (defn- str-for-signature [headers]
065    (let [headers-xf (reduce-kv
066                      (fn [m k v]
067                        (assoc m (str/lower-case k) v)) {} headers)]
068      (->> signature-headers
069           (select-keys headers-xf)
070           (reduce-kv (fn [coll k v] (conj coll (str k ": " v))) [])
071           (interpose "\n")
072           (apply str))))
073  
074  (defn gen-signature-header
075    "Generates a HTTP Signature string based on the provided map of headers."
076    [config headers]
077    (let [{:keys [user-id private-key]} config
078          string-to-sign (str-for-signature headers)
079          signature (crypto/base64-encode (crypto/sign string-to-sign private-key))
080          sig-header-keys {"keyId" user-id
081                           "headers" (str/join " " signature-headers)
082                           "signature" signature}]
083      (->> sig-header-keys
084           (reduce-kv (fn [m k v]
085                        (conj m (str k "=" "\"" v "\""))) [])
086           (interpose ",")
087           (apply str))))
088  
089  (defn auth-headers
090    "Given a config and request map of {:body ... :headers ...}, returns the
091     original set of headers with Signature and Digest attributes appended."
092    [config {:keys [body headers]}]
093    (let [digest (http/digest body)
094          h (-> headers
095                (assoc "Digest" digest)
096                (assoc "(request-target)" "post /inbox"))]
097      (assoc headers
098             "Signature" (gen-signature-header config h)
099             "Digest" digest)))
100  
101  (defmulti obj
102    "Produces a map representing an ActivityPub object which can be serialized
103     directly to JSON in the form expected by the ActivityStreams 2.0 spec.
104     See https://www.w3.org/TR/activitystreams-vocabulary/ for reference."
105    (fn [_config object-data] (:type object-data)))
106  
107  (defmethod obj :note
108    [{:keys [user-id]}
109     {:keys [id published inReplyTo content to]
110      :or {published (http/date)
111           inReplyTo ""
112           to "https://www.w3.org/ns/activitystreams#Public"}}]
113    {"id" (str user-id "/notes/" id)
114     "type" "Note"
115     "published" published
116     "attributedTo" user-id
117     "inReplyTo" inReplyTo
118     "content" content
119     "to" to})
120  
121  (defmulti activity
122    "Produces a map representing an ActivityPub activity which can be serialized
123     directly to JSON in the form expected by the ActivityStreams 2.0 spec.
124     See https://www.w3.org/TR/activitystreams-vocabulary/ for reference."
125    (fn [_config activity-type _data] activity-type))
126  
127  (defmethod activity :create [{:keys [user-id]} _ data]
128    {"@context" ["https://www.w3.org/ns/activitystreams"
129                 "https://w3id.org/security/v1"]
130     "type" "Create"
131     "actor" user-id
132     "object" data})
133  
134  (defmethod activity :delete [{:keys [user-id]} _ data]
135    {"@context" ["https://www.w3.org/ns/activitystreams"
136                 "https://w3id.org/security/v1"]
137     "type" "Delete"
138     "actor" user-id
139     "object" data})
140  
141  (defn with-config
142    "Returns curried forms of the #activity and #obj multimethods in the form
143     {:activity ... :obj ...}, with the initial parameter set to config."
144    [config]
145    (let [f (juxt
146             #(partial activity %)
147             #(partial obj %))
148          [activity-fn obj-fn] (f config)]
149      {:activity activity-fn
150       :obj obj-fn}))