/api/notification endpoints | (ns metabase.api.notification (:require [clojure.data :refer [diff]] [honey.sql.helpers :as sql.helpers] [medley.core :as m] [metabase.api.common :as api] [metabase.api.macros :as api.macros] [metabase.channel.email :as email] [metabase.channel.email.messages :as messages] [metabase.events :as events] [metabase.models.interface :as mi] [metabase.models.notification :as models.notification] [metabase.notification.core :as notification] [metabase.util :as u] [metabase.util.malli.schema :as ms] [toucan2.core :as t2] [toucan2.realize :as t2.realize])) |
(set! *warn-on-reflection* true) | |
Get a notification by id. | (defn get-notification [id] (-> (t2/select-one :model/Notification id) api/check-404 models.notification/hydrate-notification)) |
(defn- card-notification? [notification] (= :notification/card (:payload_type notification))) | |
List notifications. See | (defn list-notifications [{:keys [creator_id creator_or_recipient_id recipient_id card_id payload_type include_inactive legacy-active legacy-user-id]}] (->> (t2/reducible-select :model/Notification (cond-> {:select-distinct [:notification.*]} creator_id (sql.helpers/where [:= :notification.creator_id creator_id]) recipient_id (-> (sql.helpers/left-join :notification_handler [:= :notification_handler.notification_id :notification.id]) (sql.helpers/left-join :notification_recipient [:= :notification_recipient.notification_handler_id :notification_handler.id]) (sql.helpers/where [:= :notification_recipient.user_id recipient_id])) creator_or_recipient_id (-> (sql.helpers/left-join :notification_handler [:= :notification_handler.notification_id :notification.id]) (sql.helpers/left-join :notification_recipient [:= :notification_recipient.notification_handler_id :notification_handler.id]) (sql.helpers/where [:or [:= :notification_recipient.user_id creator_or_recipient_id] [:= :notification.creator_id creator_or_recipient_id]])) card_id (-> (sql.helpers/left-join :notification_card [:and [:= :notification_card.id :notification.payload_id] [:= :notification.payload_type "notification/card"]]) (sql.helpers/where [:= :notification_card.card_id card_id])) (and (nil? legacy-active) (not (true? include_inactive))) (sql.helpers/where [:= :notification.active true]) payload_type (sql.helpers/where [:= :notification.payload_type (u/qualified-name payload_type)]) ;; legacy-active and legacy-user-id only used by alert api, will be removed soon (some? legacy-active) (sql.helpers/where [:= :notification.active legacy-active]) legacy-user-id (-> (sql.helpers/left-join :notification_handler [:= :notification_handler.notification_id :notification.id]) (sql.helpers/left-join :notification_recipient [:= :notification_recipient.notification_handler_id :notification_handler.id]) (sql.helpers/where [:or [:= :notification_recipient.user_id legacy-user-id] [:= :notification.creator_id legacy-user-id]])))) (into [] (comp (map t2.realize/realize) (filter mi/can-read?))) models.notification/hydrate-notification)) |
(api.macros/defendpoint :get "/" "List notifications. - `creator_id`: if provided returns only notifications created by this user - `recipient_id`: if provided returns only notification that has recipient_id as a recipient - `creator_or_recipient_id`: if provided returns only notification that has user_id as creator or recipient - `card_id`: if provided returns only notification that has card_id as payload" [_route-params {:keys [creator_id creator_or_recipient_id recipient_id card_id include_inactive payload_type]} :- [:map [:creator_id {:optional true} ms/PositiveInt] [:recipient_id {:optional true} ms/PositiveInt] [:creator_or_recipient_id {:optional true} ms/PositiveInt] [:card_id {:optional true} ms/PositiveInt] [:include_inactive {:optional true} ms/BooleanValue] [:pyaload_type {:optional true} [:maybe (into [:enum] models.notification/notification-types)]]]] (list-notifications {:creator_id creator_id :recipient_id recipient_id :creator_or_recipient_id creator_or_recipient_id :card_id card_id :include_inactive include_inactive :payload_type payload_type})) | |
(api.macros/defendpoint :get "/:id" "Get a notification by id." [{:keys [id]} :- [:map [:id ms/PositiveInt]]] (-> (get-notification id) api/read-check)) | |
(defn- all-email-recipients [notification] (->> (:handlers notification) (filter #(= :channel/email ((comp keyword :channel_type) %))) (mapcat :recipients) (filter #(#{:notification-recipient/user :notification-recipient/raw-value} ((comp keyword :type) %))) (map (fn [recipient] (if (= :notification-recipient/user ((comp keyword :type) recipient)) (or (-> recipient :user :email) (t2/select-one-fn :email :model/User (:user_id recipient))) (-> recipient :details :value)))) (remove nil?) set)) | |
(defn- send-you-were-added-card-notification-email! [notification] (when (email/email-configured?) (let [current-user? #{(:email @api/*current-user*)}] (when-let [recipients-except-creator (->> (all-email-recipients notification) (remove current-user?) seq)] (messages/send-you-were-added-card-notification-email! (update notification :payload t2/hydrate :card) recipients-except-creator @api/*current-user*))))) | |
(api.macros/defendpoint :post "/" "Create a new notification, return the created notification." [_route _query body :- ::models.notification/FullyHydratedNotification] (api/create-check :model/Notification body) (let [notification (models.notification/hydrate-notification (models.notification/create-notification! (-> body (assoc :creator_id api/*current-user-id*) (dissoc :handlers :subscriptions)) (:subscriptions body) (:handlers body)))] (when (card-notification? notification) (send-you-were-added-card-notification-email! notification)) (events/publish-event! :event/notification-create {:object notification :user-id api/*current-user-id*}) notification)) | |
Send notification emails based on changes between updated and existing notification | (defn- notify-notification-updates! [updated-notification existing-notification] (when (email/email-configured?) (let [was-active? (:active existing-notification) is-active? (:active updated-notification) current-user @api/*current-user* old-emails (all-email-recipients existing-notification) new-emails (all-email-recipients updated-notification) notification (update existing-notification :payload t2/hydrate :card)] (cond ;; Notification was just archived - notify all users they were unsubscribed (and was-active? (not is-active?)) (messages/send-you-were-removed-notification-card-email! notification old-emails current-user) ;; Notification was just unarchived - notify all users they were added (and (not was-active?) is-active?) (messages/send-you-were-added-card-notification-email! notification new-emails @api/*current-user*) (not= old-emails new-emails) (let [[removed-recipients added-recipients _] (diff old-emails new-emails)] (when (seq removed-recipients) (messages/send-you-were-removed-notification-card-email! notification removed-recipients current-user)) (when (seq added-recipients) (messages/send-you-were-added-card-notification-email! notification added-recipients @api/*current-user*))))))) |
(api.macros/defendpoint :put "/:id" "Update a notification, can also update its subscriptions, handlers. Return the updated notification." [{:keys [id]} :- [:map [:id ms/PositiveInt]] _query body :- ::models.notification/FullyHydratedNotification] (let [existing-notification (get-notification id)] (api/update-check existing-notification body) (models.notification/update-notification! existing-notification body) (when (card-notification? existing-notification) (notify-notification-updates! body existing-notification)) (u/prog1 (get-notification id) (events/publish-event! :event/notification-update {:object <> :previous-object existing-notification :user-id api/*current-user-id*})))) | |
(api.macros/defendpoint :post "/:id/send" "Send a notification by id." [{:keys [id]} :- [:map [:id ms/PositiveInt]] _query {:keys [handler_ids]} :- [:map [:handler_ids {:optional true} [:sequential ms/PositiveInt]]]] (let [notification (get-notification id)] (api/read-check notification) (cond-> notification (seq handler_ids) (update :handlers (fn [handlers] (filter (comp (set handler_ids) :id) handlers))) true (notification/send-notification! :notification/sync? true)))) | |
(defn- promote-to-t2-instance [notification] (-> (t2/instance :model/Notification notification) (m/update-existing :handlers #(map (fn [x] (-> (t2/instance :model/NotificationHandler x) (m/update-existing :channel (fn [c] (t2/instance :model/Channel) c)) (m/update-existing :template (fn [t] (t2/instance :model/ChannelTemplate) t)) (m/update-existing :recipients (fn [recipients] (map (fn [r] (t2/instance :model/NotificationRecipient r)) recipients))))) %)) (m/update-existing :subscriptions #(map (fn [x] (t2/instance :model/NotificationSubscription x)) %)))) | |
(api.macros/defendpoint :post "/send" "Send an unsaved notification." [_route _query body :- ::models.notification/FullyHydratedNotification] (api/create-check :model/Notification body) (-> body (assoc :creator_id api/*current-user-id*) promote-to-t2-instance (notification/send-notification! :notification/sync? true))) | |
Unsubscribe a user from a notification. | (defn unsubscribe-user! [notification-id user-id] (let [notification (get-notification notification-id)] (api/check-403 (models.notification/current-user-is-recipient? notification)) (models.notification/unsubscribe-user! notification-id user-id) (u/prog1 (get-notification notification-id) (when (card-notification? <>) (u/ignore-exceptions (messages/send-you-unsubscribed-notification-card-email! (update <> :payload t2/hydrate :card) [(:email @api/*current-user*)]))) (events/publish-event! :event/notification-unsubscribe {:object {:id notification-id} :user-id api/*current-user-id*})))) |
(api.macros/defendpoint :post "/:id/unsubscribe" "Unsubscribe current user from a notification." [{:keys [id]} :- [:map [:id ms/PositiveInt]]] (unsubscribe-user! id api/*current-user-id*) api/generic-204-no-content) | |