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