Skip to content

Instantly share code, notes, and snippets.

@maxp
Created August 8, 2017 13:52
Show Gist options
  • Save maxp/6a4061b083c2295bffed90c0fc6af5e8 to your computer and use it in GitHub Desktop.
Save maxp/6a4061b083c2295bffed90c0fc6af5e8 to your computer and use it in GitHub Desktop.
Facebook API webhook implementation
(ns stg.fb.webhook
(:import
[javax.crypto Mac]
[javax.crypto.spec SecretKeySpec])
(:require
[clojure.string :as s]
[cheshire.core :as json]
[taoensso.timbre :refer [debug info warn]]
[ring.adapter.jetty :refer [run-jetty]]
[ring.middleware.params :refer [wrap-params]]
[compojure.core :refer [GET POST routes]]
[mount.core :refer [defstate]]
[stg.util.conf :refer [conf]]
[stg.util.core :refer [hexbyte]]
[stg.monitor.counters :refer [increment]]
[stg.bots.creds :refer [get-bot]]
[stg.bots.control :refer [setup-bots]]
[stg.fb.feeder :refer [emit]]))
;
(defn resp [status body]
{:status status :headers {"Content-Type" "text/plain"} :body body})
;
;;; ;;; ;;;
(defn process-post [{:keys [params bot]}]
(if (= "page" (:object params))
(doseq [entry (:entry params)
:let [page-id (:id entry) time (:time entry)]]
(doseq [m (:messaging entry)]
(increment :webhook.post)
(emit (assoc m :bot bot))))
(warn "unexpected object:" params))
;
(resp 200 "ok"))
;
(defn page-verify [{params :params} verify]
(increment :webhook.page-verify)
(if (and
(= (params "hub.mode") "subscribe")
(= (params "hub.verify_token") verify))
(resp 200 (params "hub.challenge"))
(resp 403 "hub.verify error")))
;
;;; ;;; ;;;
(defn hmac [key data]
(let [mac (Mac/getInstance "HmacSHA1")]
(.init mac (SecretKeySpec. (.getBytes key "UTF-8") "HmacSHA1"))
(->>
(.doFinal mac (.getBytes data "UTF-8"))
(map hexbyte)
(apply str))))
;
;; https://github.com/ring-clojure/ring/tree/1.5.0/ring-core/src/ring/middleware
(defn- keyword-syntax? [s]
(re-matches #"[A-Za-z*+!_?-][A-Za-z0-9*+!_?-]*" s))
(defn- keyify-params [target]
(cond
(map? target)
(into {}
(for [[k v] target]
[(if (and (string? k) (keyword-syntax? k))
(keyword k)
k)
(keyify-params v)]))
;
(vector? target)
(vec (map keyify-params target))
;
:else
target))
;
;;;;
(defn signed-json-body [handler req]
(let [[_ x-sig] (s/split (str ((:headers req) "x-hub-signature")) #"=")
key (-> req :bot :app_secret)
raw-body (slurp (:body req))]
(if-not (= x-sig (hmac key raw-body))
(resp 403 "x-hub-signature")
(let [data (json/parse-string raw-body true)
params (merge (:params req) data)]
(handler (assoc req :params (keyify-params params)))))))
;
(defn get-or-post [req]
(let [id (last (s/split (:uri req) #"/"))] ;; endpoint->app
(if-let [bot (get-bot id)]
(case (:request-method req)
:get (page-verify req (:verify bot))
:post (signed-json-body process-post (assoc req :bot bot))
(warn "webhook unexpected method:" req))
;
(do
(warn "webhook: bot not found - " id)
(resp 403 "wrong webhook")))))
;
;;; ;;; ;;;
(def slow-cnt (java.util.concurrent.atomic.AtomicLong. 0))
(defn wrap-slow-log [handler slow-ms]
(if slow-ms
(fn [req]
(let [t0 (System/nanoTime)
resp (handler req)
nano-delta (- (System/nanoTime) t0)
delay (/ nano-delta 1000000.)]
(increment :webhook.total-nanos nano-delta)
(when (> delay slow-ms)
(let [t (.incrementAndGet slow-cnt)]
(info "webhook-slow:" delay t)))
resp))
;;
handler))
;
(defn wrap-syserr [handler]
(fn [req]
(try
(handler req)
(catch Exception e
(do
(warn e)
(resp 500 "internal error"))))))
;
;;; ;;; ;;;
(defstate listener
:start
(let [cnf (-> conf :fb :listener)]
(run-jetty
(-> get-or-post
(wrap-params)
(wrap-slow-log
(-> conf :fb :slow-threshold))
(wrap-syserr))
cnf))
:stop
(do
(info "webhook.listener stopping")
(.stop listener)))
;
(defstate preload
:start
(setup-bots))
;
;;.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment