/api/slack endpoints

(ns metabase.api.slack
  (:require
   [clojure.java.io :as io]
   [clojure.string :as str]
   [compojure.core :refer [PUT]]
   [metabase.api.common :as api]
   [metabase.api.common.validation :as validation]
   [metabase.config :as config]
   [metabase.integrations.slack :as slack]
   [metabase.util.i18n :refer [tru]]
   [metabase.util.json :as json]
   [metabase.util.malli.schema :as ms]))
(set! *warn-on-reflection* true)

Create blocks for the Slack message with diagnostic information

(defn- create-slack-message-blocks
  [diagnostic-info file-info]
  (let [metabase-info (get-in diagnostic-info [:bugReportDetails :metabase-info])
        system-info (get-in diagnostic-info [:bugReportDetails :system-info])
        version-info (get-in diagnostic-info [:bugReportDetails :metabase-info :version])
        description (get diagnostic-info :description)
        file-url (if (string? file-info)
                   file-info
                   (:url file-info))]
    [{:type "section"
      :text {:type "mrkdwn"
             :text "A new bug report has been submitted. Please check it out!"}}
     {:type "section"
      :text {:type "mrkdwn"
             :text (str "*Description:*\n" (or description "N/A"))}}
     {:type "section"
      :fields [{:type "mrkdwn"
                :text (str "*URL:*\n" (get diagnostic-info :url "N/A"))}
               {:type "mrkdwn"
                :text (str "*App database:*\n"
                           (get metabase-info :application-database "N/A"))}
               {:type "mrkdwn"
                :text (str "*Java Runtime:*\n"
                           (get system-info :java.runtime.name "N/A"))}
               {:type "mrkdwn"
                :text (str "*Java Version:*\n"
                           (get system-info :java.runtime.version "N/A"))}
               {:type "mrkdwn"
                :text (str "*OS Name:*\n"
                           (get system-info :os.name "N/A"))}
               {:type "mrkdwn"
                :text (str "*OS Version:*\n"
                           (get system-info :os.version "N/A"))}
               {:type "mrkdwn"
                :text (str "*Version info:*\n```"
                           (json/encode version-info {:pretty true})
                           "```")}]}
     {:type "divider"}
     {:type "actions"
      :elements [{:type "button"
                  :text {:type "plain_text"
                         :text "Jump to debugger"
                         :emoji true}
                  :url (str "https://metabase-debugger.vercel.app/?fileId="
                            (if (string? file-info)
                              (last (str/split file-info #"/"))  ; Extract file ID from URL
                              (:id file-info)))
                  :style "primary"}
                 {:type "button"
                  :text {:type "plain_text"
                         :text "Download the report"
                         :emoji true}
                  :url file-url}]}]))

/settings

(api/defendpoint PUT 
  "Update Slack related settings. You must be a superuser to do this. Also updates the slack-cache.
  There are 3 cases where we alter the slack channel/user cache:
  1. falsy token           -> clear
  2. invalid token         -> clear
  3. truthy, valid token   -> refresh "
  [:as {{slack-app-token :slack-app-token, slack-files-channel :slack-files-channel, slack-bug-report-channel :slack-bug-report-channel} :body}]
  {slack-app-token     [:maybe ms/NonBlankString]
   slack-files-channel    [:maybe ms/NonBlankString]
   slack-bug-report-channel [:maybe :string]}
  (validation/check-has-application-permission :setting)
  (try
    ;; Clear settings if no values are provided
    (when (nil? slack-app-token)
      (slack/slack-app-token! nil)
      (slack/clear-channel-cache!))
    (when (nil? slack-files-channel)
      (slack/slack-files-channel! "metabase_files"))
    (when (and slack-app-token
               (not config/is-test?)
               (not (slack/valid-token? slack-app-token)))
      (slack/clear-channel-cache!)
      (throw (ex-info (tru "Invalid Slack token.")
                      {:errors {:slack-app-token (tru "invalid token")}})))
    (slack/slack-app-token! slack-app-token)
    (if slack-app-token
      (do (slack/slack-token-valid?! true)
          ;; Clear the deprecated `slack-token` when setting a new `slack-app-token`
          (slack/slack-token! nil)
          ;; refresh user/conversation cache when token is newly valid
          (slack/refresh-channels-and-usernames-when-needed!))
      ;; clear user/conversation cache when token is newly empty
      (slack/clear-channel-cache!))
    (when slack-files-channel
      (let [processed-files-channel (slack/process-files-channel-name slack-files-channel)]
        (when-not (slack/channel-exists? processed-files-channel)
          ;; Files channel could not be found; clear the token we had previously set since the integration should not be
          ;; enabled.
          (slack/slack-token-valid?! false)
          (slack/slack-app-token! nil)
          (slack/clear-channel-cache!)
          (throw (ex-info (tru "Slack channel not found.")
                          {:errors {:slack-files-channel (tru "channel not found")}})))
        (slack/slack-files-channel! processed-files-channel)))
    (when slack-bug-report-channel
      (let [processed-bug-channel (slack/process-files-channel-name slack-bug-report-channel)]
        (when (not (slack/channel-exists? processed-bug-channel))
          (throw (ex-info (tru "Slack channel not found.")
                          {:errors {:slack-bug-report-channel (tru "channel not found")}})))
        (slack/slack-bug-report-channel! processed-bug-channel)))
    {:ok true}
    (catch clojure.lang.ExceptionInfo info
      {:status 400, :body (ex-data info)})))
(def ^:private slack-manifest
  (delay (slurp (io/resource "slack-manifest.yaml"))))

/manifest

(api/defendpoint GET 
  "Returns the YAML manifest file that should be used to bootstrap new Slack apps"
  []
  (validation/check-has-application-permission :setting)
  @slack-manifest)

/bug-report

Handle bug report submissions to Slack

(api/defendpoint POST 
  "Send diagnostic information to the configured Slack channels."
  [:as {{:keys [diagnosticInfo]} :body}]
  {diagnosticInfo map?}
  (try
    (let [files-channel (slack/files-channel)
          bug-report-channel (slack/bug-report-channel)
          file-content (.getBytes (json/encode diagnosticInfo {:pretty true}))
          file-info (slack/upload-file! file-content
                                        "diagnostic-info.json"
                                        files-channel)]
      (let [blocks (create-slack-message-blocks diagnosticInfo file-info)]
        (slack/post-chat-message!
         bug-report-channel
         nil
         {:blocks blocks})
        {:success true
         :file-url (get file-info :permalink_public)}))
    (catch Exception e
      {:success false
       :error (.getMessage e)})))
(api/define-routes)