<commit_before>(ns metabase.metabot
  (:refer-clojure :exclude [list +])
  (:require (clojure [edn :as edn]
                     [string :as str])
            [clojure.java.io :as io]
            [clojure.tools.logging :as log]
            [aleph.http :as aleph]
            [cheshire.core :as json]
            [korma.core :as k]
            (manifold [bus :as bus]
                      [deferred :as d]
                      [stream :as s])
            [metabase.db :refer [sel]]
            [metabase.integrations.slack :as slack]
            [metabase.models.setting :as setting]
            [metabase.pulse :as pulse]
            [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 ------------------------------------------------------------

(def ^:private ^:dynamic *channel-id* nil)

(defn- keys-description
  ([message m]
   (str message " " (keys-description m)))
  ([m]
   (apply str (interpose ", " (sort (for [[k varr] m
                                          :when    (not (:unlisted (meta varr)))]
                                      (str \` (name k) \`)))))))

(defn- dispatch-fn [verb tag]
  (let [fn-map (into {} (for [[symb varr] (ns-interns *ns*)
                              :let        [dispatch-token (get (meta varr) tag)]
                              :when       dispatch-token]
                          {(if (true? dispatch-token)
                             (keyword symb)
                             dispatch-token) varr}))]
    (fn dispatch*
      ([]
       (keys-description (format "Here's what I can %s:" verb) fn-map))
      ([what & args]
       (if-let [f (fn-map (keyword what))]
         (apply f args)
         (format "I don't know how to %s `%s`.\n%s"
                 verb
                 (if (instance? clojure.lang.Named what)
                   (name what)
                   what)
                 (dispatch*)))))))

(defn- format-exception
  "Format a `Throwable` the way we'd like for posting it on slack."
  [^Throwable e]
  (str "Uh oh! :cry:\n>" (.getMessage e)))

(defmacro ^:private do-async {:style/indent 0} [& body]
  `(future (try ~@body
                (catch Throwable e#
                  (log/error (u/format-color '~'red (u/filtered-stacktrace e#)))
                  (slack/post-chat-message! *channel-id* (format-exception e#))))))

(defn- format-cards
  "Format a sequence of Cards as a nice multiline list for use in responses."
  [cards]
  (apply str (interpose "\n" (for [{id :id, card-name :name} cards]
                               (format "%d.  <%s|\"%s\">" id (urls/card-url id) card-name)))))


(defn ^:metabot list
  "Implementation of the `metabot list cards` command."
  [& _]
  (let [cards (sel :many :fields ['Card :id :name] (k/order :id :DESC) (k/limit 20))]
    (str "Here's your " (count cards) " most recent cards:\n" (format-cards cards))))

(defn- card-with-name [card-name]
  (first (u/prog1 (sel :many :fields ['Card :id :name], (k/where {(k/sqlfn :LOWER :name) [like (str \% (str/lower-case card-name) \%)]}))
           (when (> (count <>) 1)
             (throw (Exception. (str "Could you be a little more specific? I found these cards with names that matched:\n"
                                     (format-cards <>))))))))

(defn- id-or-name->card [card-id-or-name]
  (cond
    (integer? card-id-or-name)     (sel :one :fields ['Card :id :name], :id card-id-or-name)
    (or (string? card-id-or-name)
        (symbol? card-id-or-name)) (card-with-name card-id-or-name)
    :else                          (throw (Exception. (format "I don't know what Card `%s` is. Give me a Card ID or name.")))))

(defn ^:metabot show
  "Implementation of the `metabot show card <name-or-id>` command."
  ([]
   "Show which card? Give me a part of a card name or its ID and I can show it to you. If you don't know which card you want, try `metabot list`.")
  ([card-id-or-name & _]
   (if-let [{card-id :id} (id-or-name->card card-id-or-name)]
     (do
       (do-async (let [attachments (pulse/create-and-upload-slack-attachments! [(pulse/execute-card card-id)])]
                   (slack/post-chat-message! *channel-id*
                                             nil
                                             attachments)))
       "Ok, just a second...")
     (throw (Exception. "Not Found")))))
<commit_msg>Metabot support spaces in card names :robot_face:<commit_after>(ns metabase.metabot
  (:refer-clojure :exclude [list +])
  (:require (clojure [edn :as edn]
                     [string :as str])
            [clojure.java.io :as io]
            [clojure.tools.logging :as log]
            [aleph.http :as aleph]
            [cheshire.core :as json]
            [korma.core :as k]
            (manifold [bus :as bus]
                      [deferred :as d]
                      [stream :as s])
            [metabase.db :refer [sel]]
            [metabase.integrations.slack :as slack]
            [metabase.models.setting :as setting]
            [metabase.pulse :as pulse]
            [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 ------------------------------------------------------------

(def ^:private ^:dynamic *channel-id* nil)

(defn- keys-description
  ([message m]
   (str message " " (keys-description m)))
  ([m]
   (apply str (interpose ", " (sort (for [[k varr] m
                                          :when    (not (:unlisted (meta varr)))]
                                      (str \` (name k) \`)))))))

(defn- dispatch-fn [verb tag]
  (let [fn-map (into {} (for [[symb varr] (ns-interns *ns*)
                              :let        [dispatch-token (get (meta varr) tag)]
                              :when       dispatch-token]
                          {(if (true? dispatch-token)
                             (keyword symb)
                             dispatch-token) varr}))]
    (fn dispatch*
      ([]
       (keys-description (format "Here's what I can %s:" verb) fn-map))
      ([what & args]
       (if-let [f (fn-map (keyword what))]
         (apply f args)
         (format "I don't know how to %s `%s`.\n%s"
                 verb
                 (if (instance? clojure.lang.Named what)
                   (name what)
                   what)
                 (dispatch*)))))))

(defn- format-exception
  "Format a `Throwable` the way we'd like for posting it on slack."
  [^Throwable e]
  (str "Uh oh! :cry:\n>" (.getMessage e)))

(defmacro ^:private do-async {:style/indent 0} [& body]
  `(future (try ~@body
                (catch Throwable e#
                  (log/error (u/format-color '~'red (u/filtered-stacktrace e#)))
                  (slack/post-chat-message! *channel-id* (format-exception e#))))))

(defn- format-cards
  "Format a sequence of Cards as a nice multiline list for use in responses."
  [cards]
  (apply str (interpose "\n" (for [{id :id, card-name :name} cards]
                               (format "%d.  <%s|\"%s\">" id (urls/card-url id) card-name)))))


(defn ^:metabot list
  "Implementation of the `metabot list cards` command."
  [& _]
  (let [cards (sel :many :fields ['Card :id :name] (k/order :id :DESC) (k/limit 20))]
    (str "Here's your " (count cards) " most recent cards:\n" (format-cards cards))))

(defn- card-with-name [card-name]
  (first (u/prog1 (sel :many :fields ['Card :id :name], (k/where {(k/sqlfn :LOWER :name) [like (str \% (str/lower-case card-name) \%)]}))
           (when (> (count <>) 1)
             (throw (Exception. (str "Could you be a little more specific? I found these cards with names that matched:\n"
                                     (format-cards <>))))))))

(defn- id-or-name->card [card-id-or-name]
  (cond
    (integer? card-id-or-name)     (sel :one :fields ['Card :id :name], :id card-id-or-name)
    (or (string? card-id-or-name)
        (symbol? card-id-or-name)) (card-with-name card-id-or-name)
    :else                          (throw (Exception. (format "I don't know what Card `%s` is. Give me a Card ID or name.")))))

(defn ^:metabot show
  "Implementation of the `metabot show card <name-or-id>` command."
  ([]
   "Show which card? Give me a part of a card name or its ID and I can show it to you. If you don't know which card you want, try `metabot list`.")
  ([card-id-or-name]
   (if-let [{card-id :id} (id-or-name->card card-id-or-name)]
     (do
       (do-async (let [attachments (pulse/create-and-upload-slack-attachments! [(pulse/execute-card card-id)])]
                   (slack/post-chat-message! *channel-id*
                                             nil
                                             attachments)))
       "Ok, just a second...")
     (throw (Exception. "Not Found"))))
  ;; If the card name comes without spaces, e.g. (show 'my 'wacky 'card) turn it into a string an recur: (show "my wacky card")
  ([word & more]
   (show (apply str (interpose " " (cons word more))))))

{'fraction_non_alphanumeric': 0.17349887616397303, 'fraction_numerical': 0.0010703200256876805, 'mean_word_length': 1.6927953890489913, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4087516', 'n_tokens_mistral': 2974, 'n_tokens_neox': 2861, 'n_words': 898}