;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC (ns app.loggers.webhooks "A mattermost integration for error reporting." (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.logging :as l] [app.common.transit :as t] [app.common.uri :as uri] [app.config :as cf] [app.db :as db] [app.http.client :as http] [app.util.time :as dt] [app.worker :as wrk] [clojure.data.json :as json] [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig])) ;; --- HELPERS (defn key-fn [k & keys] (fn [params] (reduce #(dm/str %1 ":" (get params %2)) (dm/str (get params k)) keys))) ;; --- PROC (defn- lookup-webhooks-by-team [pool team-id] (db/exec! pool ["select w.* from webhook as w where team_id=? and is_active=true" team-id])) (defn- lookup-webhooks-by-project [pool project-id] (let [sql [(str "select w.* from webhook as w" " join project as p on (p.team_id = w.team_id)" " where p.id = ? and w.is_active = true") project-id]] (db/exec! pool sql))) (defn- lookup-webhooks-by-file [pool file-id] (let [sql [(str "select w.* from webhook as w" " join project as p on (p.team_id = w.team_id)" " join file as f on (f.project_id = p.id)" " where f.id = ? and w.is_active = true") file-id]] (db/exec! pool sql))) (defn- lookup-webhooks [{:keys [::db/pool]} {:keys [props] :as event}] (or (some->> (:team-id props) (lookup-webhooks-by-team pool)) (some->> (:project-id props) (lookup-webhooks-by-project pool)) (some->> (:file-id props) (lookup-webhooks-by-file pool)))) (defmethod ig/pre-init-spec ::process-event-handler [_] (s/keys :req [::db/pool])) (defmethod ig/init-key ::process-event-handler [_ {:keys [::db/pool] :as cfg}] (fn [{:keys [props] :as task}] (let [event (::event props)] (l/dbg :hint "process webhook event" :name (:name event)) (when-let [items (lookup-webhooks cfg event)] (l/trc :hint "webhooks found for event" :total (count items)) (db/with-atomic [conn pool] (doseq [item items] (wrk/submit! ::wrk/conn conn ::wrk/task :run-webhook ::wrk/queue :webhooks ::wrk/max-retries 3 ::event event ::config item))))))) ;; --- RUN (declare interpret-exception) (declare interpret-response) (def json-write-opts {:key-fn str/camel :indent true}) (defmethod ig/pre-init-spec ::run-webhook-handler [_] (s/keys :req [::http/client ::db/pool])) (defmethod ig/prep-key ::run-webhook-handler [_ cfg] (merge {::max-errors 3} (d/without-nils cfg))) (defmethod ig/init-key ::run-webhook-handler [_ {:keys [::db/pool ::max-errors] :as cfg}] (letfn [(update-webhook! [whook err] (if err (let [sql [(str "update webhook " " set error_code=?, " " error_count=error_count+1 " " where id=?") err (:id whook)] res (db/exec-one! pool sql {::db/return-keys true})] (when (>= (:error-count res) max-errors) (db/update! pool :webhook {:is-active false} {:id (:id whook)}))) (db/update! pool :webhook {:updated-at (dt/now) :error-code nil :error-count 0} {:id (:id whook)}))) (report-delivery! [whook req rsp err] (db/insert! pool :webhook-delivery {:webhook-id (:id whook) :created-at (dt/now) :error-code err :req-data (db/tjson req) :rsp-data (db/tjson rsp)}))] (fn [{:keys [props] :as task}] (let [event (::event props) whook (::config props) body (case (:mtype whook) "application/json" (json/write-str event json-write-opts) "application/transit+json" (t/encode-str event) "application/x-www-form-urlencoded" (uri/map->query-string event))] (l/dbg :hint "run webhook" :event-name (:name event) :webhook-id (:id whook) :webhook-uri (:uri whook) :webhook-mtype (:mtype whook)) (let [req {:uri (:uri whook) :headers {"content-type" (:mtype whook) "user-agent" (str/ffmt "penpot/%" (:main cf/version))} :timeout (dt/duration "4s") :method :post :body body}] (try (let [rsp (http/req! cfg req {:response-type :input-stream :sync? true}) err (interpret-response rsp)] (report-delivery! whook req rsp err) (update-webhook! whook err)) (catch Throwable cause (let [err (interpret-exception cause)] (report-delivery! whook req nil err) (update-webhook! whook err) (when (= err "unknown") (l/err :hint "unknown error on webhook request" :cause cause)))))))))) (defn interpret-response [{:keys [status] :as response}] (when-not (or (= 200 status) (= 204 status)) (str/ffmt "unexpected-status:%" status))) (defn interpret-exception [cause] (cond (instance? javax.net.ssl.SSLHandshakeException cause) "ssl-validation-error" (instance? java.net.ConnectException cause) "connection-error" (instance? java.lang.IllegalArgumentException cause) "invalid-uri" (instance? java.net.http.HttpConnectTimeoutException cause) "timeout"))