diff --git a/frontend/src/admin/settings/components/SettingsSlackForm.jsx b/frontend/src/admin/settings/components/SettingsSlackForm.jsx index c50c9dc1ea02bd78a2ac6dcdaa14c336742a3a8f..f418f769ec55a07f1106b1eac7b3324a22f8f6ca 100644 --- a/frontend/src/admin/settings/components/SettingsSlackForm.jsx +++ b/frontend/src/admin/settings/components/SettingsSlackForm.jsx @@ -3,6 +3,7 @@ import React, { Component, PropTypes } from "react"; import MetabaseAnalytics from "metabase/lib/analytics"; import MetabaseUtils from "metabase/lib/utils"; import SettingsEmailFormElement from "./SettingsEmailFormElement.jsx"; +import SettingsSetting from "./SettingsSetting.jsx"; import Icon from "metabase/components/Icon.jsx"; @@ -34,7 +35,7 @@ export default class SettingsSlackForm extends Component { // this gives us an opportunity to load up our formData with any existing values for elements let formData = {}; this.props.elements.forEach(function(element) { - formData[element.key] = element.value; + formData[element.key] = element.value || element.defaultValue; }); this.setState({formData}); @@ -156,10 +157,22 @@ export default class SettingsSlackForm extends Component { let errorMessage = (formErrors && formErrors.elements) ? formErrors.elements[element.key] : validationErrors[element.key], value = formData[element.key] || element.defaultValue; - return <SettingsEmailFormElement + if (element.key === "slack-token") { + return ( + <SettingsEmailFormElement key={element.key} element={_.extend(element, {value, errorMessage })} handleChangeEvent={this.handleChangeEvent.bind(this)} /> + ); + } else if (element.key === "metabot-enabled") { + return ( + <SettingsSetting + key={element.key} + setting={_.extend(element, {value, errorMessage })} + updateSetting={(setting, value) => this.handleChangeEvent(setting, value)} + /> + ); + } }); let saveSettingsButtonStates = { diff --git a/frontend/src/admin/settings/settings.controllers.js b/frontend/src/admin/settings/settings.controllers.js index e3bceddb7e0a39989a3e1d34049f9eba9d2e546f..31fd80ba337fcf258dbf12f6996d4daa163d25a0 100644 --- a/frontend/src/admin/settings/settings.controllers.js +++ b/frontend/src/admin/settings/settings.controllers.js @@ -105,7 +105,15 @@ const SECTIONS = [ type: "string", required: true, autoFocus: true - } + }, + { + key: "metabot-enabled", + display_name: "Metabot", + type: "boolean", + defaultValue: "true", + required: true, + autoFocus: false + }, ] } ]; diff --git a/src/metabase/api/slack.clj b/src/metabase/api/slack.clj index 006e971b2945c20944db1c5219282632d928d320..810b635a94f21fdcd290b0e3f3dc8433babba8ba 100644 --- a/src/metabase/api/slack.clj +++ b/src/metabase/api/slack.clj @@ -3,18 +3,20 @@ (:require [compojure.core :refer [PUT]] [metabase.api.common :refer :all] [metabase.config :as config] - [metabase.integrations.slack :as slack])) + [metabase.integrations.slack :as slack] + [metabase.models.setting :as setting])) (defendpoint PUT "/settings" - "Update the `slack-token`. You must be a superuser to do this." - [:as {{slack-token :slack-token} :body}] - {slack-token [Required NonEmptyString]} + "Update Slack related settings. You must be a superuser to do this." + [:as {{slack-token :slack-token, metabot-enabled :metabot-enabled, :as slack-settings} :body}] + {slack-token [Required NonEmptyString] + metabot-enabled [Required]} (check-superuser) (try - ;; just check that channels.list doesn't throw an exception (that the connection works) + ;; just check that channels.list doesn't throw an exception (a.k.a. that the token works) (when-not config/is-test? (slack/GET :channels.list, :exclude_archived 1, :token slack-token)) - (slack/slack-token slack-token) + (setting/set-all slack-settings) {:ok true} (catch clojure.lang.ExceptionInfo info {:status 400, :body (ex-data info)}))) diff --git a/src/metabase/events/metabot_lifecycle.clj b/src/metabase/events/metabot_lifecycle.clj new file mode 100644 index 0000000000000000000000000000000000000000..bc6406c01499c6322ae0ca1a55b81606441bc0ba --- /dev/null +++ b/src/metabase/events/metabot_lifecycle.clj @@ -0,0 +1,47 @@ +(ns metabase.events.metabot-lifecycle + (:require [clojure.core.async :as async] + [clojure.tools.logging :as log] + [metabase.db :as db] + [metabase.driver :as driver] + [metabase.events :as events] + [metabase.metabot :as metabot] + [metabase.models.database :refer [Database]])) + + +(def ^:const ^:private metabot-lifecycle-topics + "The `Set` of event topics which are subscribed to for use in metabot lifecycle." + #{:settings-update}) + +(def ^:private metabot-lifecycle-channel + "Channel for receiving event notifications we want to subscribe to for metabot lifecycle events." + (async/chan)) + + +;;; ## ---------------------------------------- EVENT PROCESSING ---------------------------------------- + + +(defn process-metabot-lifecycle-event + "Handle processing for a single event notification received on the metabot-lifecycle-channel" + [metabot-lifecycle-event] + ;; try/catch here to prevent individual topic processing exceptions from bubbling up. better to handle them here. + (when-let [{topic :topic object :item} metabot-lifecycle-event] + (try + ;; if someone updated our slack-token, or metabot was enabled/disabled then react accordingly + (let [{:keys [slack-token metabot-enabled]} object] + (cond + (and (contains? object :metabot-enabled) + (not (= "true" metabot-enabled))) (metabot/stop-metabot!) + (and (contains? object :slack-token) + (seq slack-token)) (metabot/start-metabot!))) + (catch Throwable e + (log/warn (format "Failed to process driver notifications event. %s" topic) e))))) + + + +;;; ## ---------------------------------------- LIFECYLE ---------------------------------------- + + +(defn events-init + "Automatically called during startup; start event listener for metabot lifecycle events." + [] + (events/start-event-listener metabot-lifecycle-topics metabot-lifecycle-channel process-metabot-lifecycle-event)) diff --git a/src/metabase/integrations/slack.clj b/src/metabase/integrations/slack.clj index b72d32181859a061d4bac11622a41aa6df9d2b1c..a27bb56a57a56781b7a006b9755d9231c32d10b3 100644 --- a/src/metabase/integrations/slack.clj +++ b/src/metabase/integrations/slack.clj @@ -7,13 +7,7 @@ ;; Define a setting which captures our Slack api token -(defsetting slack-token "Slack API bearer token obtained from https://api.slack.com/web#authentication" nil - :setter (fn [new-value] - (setting/set* :slack-token new-value) - (require 'metabase.metabot) - ((ns-resolve 'metabase.metabot (if (seq new-value) - 'start-metabot! - 'stop-metabot!))))) +(defsetting slack-token "Slack API bearer token obtained from https://api.slack.com/web#authentication" nil) (def ^:private ^:const ^String slack-api-base-url "https://slack.com/api") (def ^:private ^:const ^String files-channel-name "metabase_files") diff --git a/src/metabase/metabot.clj b/src/metabase/metabot.clj index c09ebaa2852a3f3d4019d5a4f390c0e87d373c33..339e275cd2ca8944d9790583461ace5d3f60c9b1 100644 --- a/src/metabase/metabot.clj +++ b/src/metabase/metabot.clj @@ -13,10 +13,12 @@ [metabase.api.common :refer [let-404]] [metabase.db :refer [sel]] [metabase.integrations.slack :as slack] + [metabase.models.setting :as setting] [metabase.task.send-pulses :as pulses] [metabase.util :as u] [metabase.util.urls :as urls])) +(setting/defsetting metabot-enabled "Enable Metabot, which lets you search for and view your saved questions directly via Slack." "true") ;;; # ------------------------------------------------------------ Metabot Command Handlers ------------------------------------------------------------ @@ -37,7 +39,6 @@ {(if (true? dispatch-token) (keyword symb) dispatch-token) varr}))] - (println (u/format-color 'cyan verb) fn-map) (fn dispatch* ([] (keys-description (format "Here's what I can %s:" verb) fn-map)) @@ -147,23 +148,20 @@ (when-let [tokens (seq (edn/read-string (str "(" (-> s (str/replace "“" "\"") ; replace smart quotes (str/replace "â€" "\"")) ")")))] - (println (u/format-color 'magenta tokens)) (apply apply-metabot-fn tokens)))) ;;; # ------------------------------------------------------------ Metabot Input Handling ------------------------------------------------------------ (defn- message->command-str [{:keys [text]}] - (u/prog1 (when (seq text) - (second (re-matches #"^mea?ta?boa?t\s+(.*)$" text))) - (println (u/format-color 'yellow <>)))) + (when (seq text) + (second (re-matches #"^mea?ta?boa?t\s+(.*)$" text)))) (defn- respond-to-message! [message response] (when response (let [response (if (coll? response) (str "```\n" (u/pprint-to-str response) "```") (str response))] (when (seq response) - (println (u/format-color 'green response)) (slack/post-chat-message! (:channel message) response))))) (defn- handle-slack-message [message] @@ -181,14 +179,13 @@ (defn- handle-slack-event [socket start-time event] (when-not (= socket @websocket) - (println "Go home websocket, you're drunk.") + (log/debug "Go home websocket, you're drunk.") (s/close! socket) (throw (Exception.))) (when-let [event (json/parse-string event keyword)] (when (and (human-message? event) (> (event-timestamp-ms event) start-time)) - (println (u/pprint-to-str 'cyan event)) (binding [*channel-id* (:channel event)] (do-async (handle-slack-message event)))))) @@ -197,10 +194,8 @@ (defn- connect-websocket! [] (when-let [websocket-url (slack/websocket-url)] - (log/info "Launching MetaBot... 🤖") (let [socket @(aleph/websocket-client websocket-url)] (reset! websocket socket) - (log/info "Connected to WebSocket.") (d/catch (s/consume (partial handle-slack-event socket (System/currentTimeMillis)) socket) (fn [error] @@ -221,7 +216,6 @@ (defn- start-websocket-monitor! [] (future (reset! websocket-monitor-thread-id (.getId (Thread/currentThread))) - (log/debug "Monitor thread ID ->" (.getId (Thread/currentThread))) ;; Every 2 seconds check to see if websocket connection is [still] open, [re-]open it if not (loop [] (Thread/sleep 500) @@ -229,7 +223,7 @@ (try (when (or (not @websocket) (s/closed? @websocket)) - (log/info "MetaBot WebSocket is closed. < Thread" (.getId (Thread/currentThread)) ">") + (log/debug "MetaBot WebSocket is closed. Reconnecting now.") (connect-websocket!)) (catch Throwable e (log/error "Error connecting websocket:" (.getMessage e)))) @@ -240,7 +234,8 @@ This will spin up a background thread that opens and maintains a Slack WebSocket connection." [] - (when (slack/slack-token) + (when (and (setting/get :slack-token) + (= "true" (setting/get :metabot-enabled))) (log/info "Starting MetaBot WebSocket monitor thread...") (start-websocket-monitor!))) diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj index 689c2b8b9bbce6f939dc6bd450fc946a9d296cf9..aa696d5ea94f7ad1b64caf492734a2f4f6ba6950 100644 --- a/src/metabase/models/setting.clj +++ b/src/metabase/models/setting.clj @@ -5,6 +5,7 @@ [korma.core :as k] [metabase.config :as config] [metabase.db :refer [exists? sel del]] + [metabase.events :as events] [metabase.models [common :as common] [interface :as i]] [metabase.setup :as setup] @@ -128,7 +129,7 @@ (if-let [v (clojure.core/get settings k)] (set k v) (delete k))) - settings) + (events/publish-event :settings-update settings)) (defn set* "Set the value of a `Setting`, deleting it if VALUE is `nil` or an empty string."