|
3 | 3 | [org.httpkit.server :as srv]
|
4 | 4 | [org.httpkit.client :as clnt]
|
5 | 5 | [clojure.core.match :refer [match]]
|
6 |
| - [clojure.tools.logging :as log] |
| 6 | + [taoensso.timbre :as log] |
7 | 7 | [clojure.string :as str]
|
8 | 8 | [cheshire.core :as json]
|
9 | 9 | [twitter.oauth :as oauth]
|
10 |
| - [twitter.api.restful :as r])) |
| 10 | + [twitter.api.restful :as r] |
| 11 | + [failjure.core :as f]) |
| 12 | + (:import (javax.crypto Mac) |
| 13 | + (javax.crypto.spec SecretKeySpec))) |
11 | 14 |
|
12 | 15 | (def port 3000)
|
13 | 16 |
|
| 17 | +(def github-token (System/getenv "GITHUB_TOKEN")) |
| 18 | + |
14 | 19 | (def slack-hook-url (System/getenv "SLACK_HOOK_URL"))
|
15 | 20 | (def slack-channel "#datahike")
|
16 | 21 |
|
|
28 | 33 | (log/info "Received webhook" d)
|
29 | 34 | d)
|
30 | 35 |
|
| 36 | +(defn hmac-sha-256 |
| 37 | + "Takes the webhook body as string |
| 38 | + Takes the secret webhook token as string |
| 39 | + Computes an HMAC |
| 40 | + Returns HMAC as string |
| 41 | + https://stackoverflow.com/a/15444056" |
| 42 | + [^String body-str ^String key-str] |
| 43 | + (let [key-spec (SecretKeySpec. (.getBytes key-str) "HmacSHA256") |
| 44 | + hmac (doto (Mac/getInstance "HmacSHA256") (.init key-spec)) |
| 45 | + result (.doFinal hmac (.getBytes body-str))] |
| 46 | + (apply str (map #(format "%02x" %) result)))) |
| 47 | + |
| 48 | +(defn check-hmac [body-str {:keys [headers]}] |
| 49 | + (let [hmac (str "sha256=" (hmac-sha-256 body-str github-token)) |
| 50 | + header (get headers "x-hub-signature-256")] |
| 51 | + (if (not= hmac header) |
| 52 | + (throw (ex-info "HMAC does not match" {:hmac hmac |
| 53 | + :header header})) |
| 54 | + body-str))) |
| 55 | + |
31 | 56 | (defn trigger-slack-reminder [_]
|
32 | 57 | (if slack-hook-url
|
33 | 58 | (let [message "In a quarter hour we will have our weekly open-source meeting
|
|
41 | 66 | (log/debug "Weekly OSS meeting announcement triggered"))
|
42 | 67 | (log/error "Slack Hook URL not provided")))
|
43 | 68 |
|
44 |
| -(defn trigger-slack-announcement [[release repository :as d]] |
| 69 | +(defn trigger-slack-announcement [body] |
45 | 70 | (if slack-hook-url
|
46 |
| - (let [message (format "Version %s of %s was just released. Find the changelog or get in contact with us <%s|over on GitHub.>" |
| 71 | + (let [[release repository] (juxt body :release :repository) |
| 72 | + message (format "Version %s of %s was just released. Find the changelog or get in contact with us <%s|over on GitHub.>" |
47 | 73 | (:tag_name release)
|
48 | 74 | (:name repository)
|
49 | 75 | (:html_url release))
|
|
53 | 79 | (clnt/post slack-hook-url {:headers {"content-type" "application/json"}
|
54 | 80 | :body json}))
|
55 | 81 | (log/error "Slack Hook URL not provided"))
|
56 |
| - d) |
| 82 | + body) |
57 | 83 |
|
58 |
| -(defn trigger-twitter-announcement [[release repository :as d]] |
59 |
| - (let [message (format "Version %s of %s was just released. Take a look at the changelog over on GitHub: %s" |
| 84 | +(defn trigger-twitter-announcement [body] |
| 85 | + (let [[release repository] (juxt body :release :repository) |
| 86 | + message (format "Version %s of %s was just released. Take a look at the changelog over on GitHub: %s" |
60 | 87 | (:tag_name release)
|
61 | 88 | (:name repository)
|
62 | 89 | (:html_url release))]
|
63 | 90 | (if (not-any? nil? [twitter-api-key twitter-api-secret twitter-access-token twitter-access-token-secret])
|
64 | 91 | (r/statuses-update :oauth-creds twitter-creds
|
65 | 92 | :params {:status message})
|
66 | 93 | (log/error "Twitter secrets not provided")))
|
67 |
| - d) |
| 94 | + body) |
68 | 95 |
|
69 | 96 | ;;;;;;;;;;;;;;;;;;;;;;;;;;
|
70 | 97 | ;; Handlers
|
71 | 98 | ;;;;;;;;;;;;;;;;;;;;;;;;;;
|
72 | 99 |
|
73 |
| -(defn github-hook [{body :body :as req}] |
74 |
| - (-> (slurp body) |
75 |
| - (json/parse-string true) |
76 |
| - log-request |
77 |
| - ((juxt :release :repository)) |
78 |
| - trigger-slack-announcement |
79 |
| - trigger-twitter-announcement)) |
| 100 | +(defn github-hook [{:keys [body headers] :as req}] |
| 101 | + (let [result (f/ok-> (slurp body) |
| 102 | + (check-hmac req) |
| 103 | + (json/parse-string true) |
| 104 | + log-request |
| 105 | + trigger-slack-announcement |
| 106 | + trigger-twitter-announcement)] |
| 107 | + (if (f/failed? result) |
| 108 | + (do (log/error (f/message result) {:delivery (get headers "X-GitHub-Delivery") |
| 109 | + :event (get headers "X-GitHub-Event")}) |
| 110 | + {:status 500 :body (f/message result)}) |
| 111 | + {:status 201}))) |
80 | 112 |
|
81 | 113 | ;;;;;;;;;;;;;;;;;;;;;;;;;;
|
82 | 114 | ;; Routes
|
|
85 | 117 | (defn routes [{:keys [request-method uri] :as req}]
|
86 | 118 | (let [path (vec (rest (str/split uri #"/")))]
|
87 | 119 | (match [request-method path]
|
88 |
| - [:post ["github" "release"]] {:body (github-hook req)} |
| 120 | + [:post ["github" "release"]] (github-hook req) |
89 | 121 | :else {:status 404 :body "Error 404: Page not found"})))
|
90 | 122 |
|
91 | 123 | ;;;;;;;;;;;;;;;;;;;;;;;;;;
|
92 | 124 | ;; Server
|
93 | 125 | ;;;;;;;;;;;;;;;;;;;;;;;;;;
|
94 | 126 |
|
95 | 127 | (def server (let [url (str "http://localhost:" port "/")]
|
96 |
| - (srv/run-server #'routes {:port port}) |
97 |
| - (println "serving" url))) |
| 128 | + (println "serving" url) |
| 129 | + (srv/run-server #'routes {:port port}))) |
98 | 130 |
|
99 | 131 | (def scheduler (s/start-scheduler trigger-slack-reminder))
|
100 | 132 |
|
101 | 133 | (comment
|
102 | 134 | (def payload (json/parse-string (slurp "test/payload.sample.json") true))
|
103 | 135 |
|
104 |
| - (defonce server (atom nil)) |
105 |
| - (reset! server (srv/run-server #'routes {:port port})) |
| 136 | + (srv/server-stop! @server) |
106 | 137 |
|
107 | 138 | @(clnt/post "http://localhost:3000/github/release" {:headers {"Content-Type" "application/json"} :body (slurp "test/payload.sample.json")}))
|
0 commit comments