Created
August 8, 2017 13:52
-
-
Save maxp/6a4061b083c2295bffed90c0fc6af5e8 to your computer and use it in GitHub Desktop.
Facebook API webhook implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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