mirror of
https://github.com/penpot/penpot.git
synced 2025-05-20 08:16:12 +02:00
771 lines
27 KiB
Clojure
771 lines
27 KiB
Clojure
;; 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.rpc.commands.comments
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.schema :as sm]
|
|
[app.common.uri :as uri]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.db :as db]
|
|
[app.db.sql :as sql]
|
|
[app.email :as eml]
|
|
[app.features.fdata :as feat.fdata]
|
|
[app.loggers.audit :as-alias audit]
|
|
[app.loggers.webhooks :as-alias webhooks]
|
|
[app.rpc :as-alias rpc]
|
|
[app.rpc.commands.files :as files]
|
|
[app.rpc.commands.profile :as profile]
|
|
[app.rpc.commands.teams :as teams]
|
|
[app.rpc.doc :as-alias doc]
|
|
[app.rpc.quotes :as quotes]
|
|
[app.rpc.retry :as rtry]
|
|
[app.util.pointer-map :as pmap]
|
|
[app.util.services :as sv]
|
|
[app.util.time :as dt]
|
|
[clojure.set :as set]
|
|
[cuerdas.core :as str]))
|
|
|
|
;; --- GENERAL PURPOSE INTERNAL HELPERS
|
|
|
|
(def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)")
|
|
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
|
|
|
|
(defn- format-comment
|
|
[{:keys [content]}]
|
|
(->> (d/interleave-all
|
|
(str/split content r-mentions-split)
|
|
(->> (re-seq r-mentions content)
|
|
(map (fn [[_ user _]] user))))
|
|
(str/join "")))
|
|
|
|
(defn- format-comment-url
|
|
[{:keys [team-id file-id page-id]}]
|
|
(str/ffmt "%/#/workspace?%"
|
|
(cf/get :public-uri)
|
|
(uri/map->query-string
|
|
{:file-id file-id
|
|
:page-id page-id
|
|
:team-id team-id})))
|
|
|
|
(defn- format-comment-ref
|
|
[{:keys [seqn]} {:keys [file-name page-name]}]
|
|
(str/ffmt "#%, %, %" seqn file-name page-name))
|
|
|
|
(defn get-team-users
|
|
[conn team-id]
|
|
(->> (teams/get-users+props conn team-id)
|
|
(map profile/decode-row)
|
|
(d/index-by :id)))
|
|
|
|
(defn- resolve-profile-name
|
|
[conn profile-id]
|
|
(-> (db/get conn :profile {:id profile-id}
|
|
{::sql/columns [:fullname]})
|
|
(get :fullname)))
|
|
|
|
(defn- notification-email?
|
|
[profile-id owner-id props]
|
|
(if (= profile-id owner-id)
|
|
(not= :none (-> props :notifications :email-comments))
|
|
(= :all (-> props :notifications :email-comments))))
|
|
|
|
(defn- mention-email?
|
|
[props]
|
|
(not= :none (-> props :notifications :email-comments)))
|
|
|
|
(defn send-comment-emails!
|
|
[conn {:keys [profile-id team-id] :as params} comment thread]
|
|
|
|
(let [team-users (get-team-users conn team-id)
|
|
source-user (resolve-profile-name conn profile-id)
|
|
|
|
comment-reference (format-comment-ref thread params)
|
|
comment-content (format-comment comment)
|
|
comment-url (format-comment-url params)
|
|
|
|
;; Users mentioned in this comment
|
|
comment-mentions
|
|
(-> (:mentions comment)
|
|
(set/difference #{profile-id}))
|
|
|
|
;; Users mentioned in this thread
|
|
thread-mentions
|
|
(-> (:mentions thread)
|
|
;; Remove the mentions in the thread because we're already sending a
|
|
;; notification
|
|
(set/difference comment-mentions)
|
|
(disj profile-id))
|
|
|
|
;; All users
|
|
notificate-users-ids
|
|
(-> (set (keys team-users))
|
|
(set/difference comment-mentions)
|
|
(set/difference thread-mentions)
|
|
(disj profile-id))]
|
|
|
|
(doseq [mention comment-mentions]
|
|
(let [{:keys [fullname email props]} (get team-users mention)]
|
|
(when (mention-email? props)
|
|
(eml/send!
|
|
{::eml/conn conn
|
|
::eml/factory eml/comment-mention
|
|
:to email
|
|
:name fullname
|
|
:source-user source-user
|
|
:comment-reference comment-reference
|
|
:comment-content comment-content
|
|
:comment-url comment-url}))))
|
|
|
|
;; Send to the thread users
|
|
(doseq [mention thread-mentions]
|
|
(let [{:keys [fullname email props]} (get team-users mention)]
|
|
(when (mention-email? props)
|
|
(eml/send!
|
|
{::eml/conn conn
|
|
::eml/factory eml/comment-thread
|
|
:to email
|
|
:name fullname
|
|
:source-user source-user
|
|
:comment-reference comment-reference
|
|
:comment-content comment-content
|
|
:comment-url comment-url}))))
|
|
|
|
;; Send to users with the "all" flag activated
|
|
(doseq [user-id notificate-users-ids]
|
|
(let [{:keys [id fullname email props]} (get team-users user-id)]
|
|
(when (notification-email? id (:owner-id thread) props)
|
|
(eml/send!
|
|
{::eml/conn conn
|
|
::eml/factory eml/comment-notification
|
|
:to email
|
|
:name fullname
|
|
:source-user source-user
|
|
:comment-reference comment-reference
|
|
:comment-content comment-content
|
|
:comment-url comment-url}))))))
|
|
|
|
(defn- decode-row
|
|
[{:keys [participants position mentions] :as row}]
|
|
(cond-> row
|
|
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
|
|
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))
|
|
(db/pgarray? mentions) (assoc :mentions (db/decode-pgarray mentions #{}))))
|
|
|
|
(def xf-decode-row
|
|
(map decode-row))
|
|
|
|
(def ^:private
|
|
sql:get-file
|
|
"select f.id, f.modified_at, f.revn, f.features, f.name,
|
|
f.project_id, p.team_id, f.data
|
|
from file as f
|
|
join project as p on (p.id = f.project_id)
|
|
where f.id = ?
|
|
and f.deleted_at is null")
|
|
|
|
(defn- get-file
|
|
"A specialized version of get-file for comments module."
|
|
[cfg file-id page-id]
|
|
(let [file (db/exec-one! cfg [sql:get-file file-id])]
|
|
(when-not file
|
|
(ex/raise :type :not-found
|
|
:code :object-not-found
|
|
:hint "file not found"))
|
|
|
|
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
|
(let [{:keys [data] :as file} (files/decode-row file)]
|
|
(-> file
|
|
(assoc :page-name (dm/get-in data [:pages-index page-id :name]))
|
|
(assoc :page-id page-id)
|
|
(dissoc :data))))))
|
|
|
|
(defn- get-comment-thread
|
|
[conn thread-id & {:as opts}]
|
|
(-> (db/get-by-id conn :comment-thread thread-id opts)
|
|
(decode-row)))
|
|
|
|
(defn- get-comment
|
|
[conn comment-id & {:as opts}]
|
|
(db/get-by-id conn :comment comment-id opts))
|
|
|
|
(def ^:private sql:get-next-seqn
|
|
"SELECT (f.comment_thread_seqn + 1) AS next_seqn
|
|
FROM file AS f
|
|
WHERE f.id = ?
|
|
FOR UPDATE")
|
|
|
|
(defn- get-next-seqn
|
|
[conn file-id]
|
|
(let [res (db/exec-one! conn [sql:get-next-seqn file-id])]
|
|
(:next-seqn res)))
|
|
|
|
(def sql:upsert-comment-thread-status
|
|
"insert into comment_thread_status (thread_id, profile_id, modified_at)
|
|
values (?, ?, ?)
|
|
on conflict (thread_id, profile_id)
|
|
do update set modified_at = ?
|
|
returning modified_at;")
|
|
|
|
(defn upsert-comment-thread-status!
|
|
([conn profile-id thread-id]
|
|
(upsert-comment-thread-status! conn profile-id thread-id (dt/in-future "1s")))
|
|
([conn profile-id thread-id mod-at]
|
|
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id mod-at mod-at])))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; QUERY COMMANDS
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; --- COMMAND: Get Comment Threads
|
|
|
|
(declare ^:private get-comment-threads)
|
|
|
|
(def ^:private
|
|
schema:get-comment-threads
|
|
[:and
|
|
[:map {:title "get-comment-threads"}
|
|
[:file-id {:optional true} ::sm/uuid]
|
|
[:team-id {:optional true} ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]]
|
|
[::sm/contains-any #{:file-id :team-id}]])
|
|
|
|
(sv/defmethod ::get-comment-threads
|
|
{::doc/added "1.15"
|
|
::sm/params schema:get-comment-threads}
|
|
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
|
|
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(get-comment-threads conn profile-id file-id))))
|
|
|
|
(def ^:private sql:comment-threads
|
|
"SELECT DISTINCT ON (ct.id)
|
|
ct.*,
|
|
p.team_id AS team_id,
|
|
f.name AS file_name,
|
|
f.project_id AS project_id,
|
|
first_value(c.content) OVER w AS content,
|
|
(SELECT count(1)
|
|
FROM comment AS c
|
|
WHERE c.thread_id = ct.id) AS count_comments,
|
|
(SELECT count(1)
|
|
FROM comment AS c
|
|
WHERE c.thread_id = ct.id
|
|
AND c.created_at >= coalesce(cts.modified_at, ct.created_at)) AS count_unread_comments
|
|
FROM comment_thread AS ct
|
|
INNER JOIN comment AS c ON (c.thread_id = ct.id)
|
|
INNER JOIN file AS f ON (f.id = ct.file_id)
|
|
INNER JOIN project AS p ON (p.id = f.project_id)
|
|
LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?)
|
|
WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)")
|
|
|
|
(def ^:private sql:comment-threads-by-file-id
|
|
(str "WITH threads AS (" sql:comment-threads ")"
|
|
"SELECT * FROM threads WHERE file_id = ?"))
|
|
|
|
(defn- get-comment-threads
|
|
[conn profile-id file-id]
|
|
(->> (db/exec! conn [sql:comment-threads-by-file-id profile-id file-id])
|
|
(into [] xf-decode-row)))
|
|
|
|
;; --- COMMAND: Get Unread Comment Threads
|
|
|
|
(declare ^:private get-unread-comment-threads)
|
|
|
|
(def ^:private
|
|
schema:get-unread-comment-threads
|
|
[:map {:title "get-unread-comment-threads"}
|
|
[:team-id ::sm/uuid]])
|
|
|
|
(sv/defmethod ::get-unread-comment-threads
|
|
{::doc/added "1.15"
|
|
::sm/params schema:get-unread-comment-threads}
|
|
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
|
(db/run!
|
|
cfg
|
|
(fn [{:keys [::db/conn]}]
|
|
(teams/check-read-permissions! conn profile-id team-id)
|
|
(get-unread-comment-threads conn profile-id team-id))))
|
|
|
|
(def sql:unread-all-comment-threads-by-team
|
|
(str "WITH threads AS (" sql:comment-threads ")"
|
|
"SELECT * FROM threads WHERE count_unread_comments > 0 AND team_id = ?"))
|
|
|
|
;; The partial configuration will retrieve only comments created by the user and
|
|
;; threads that have a mention to the user.
|
|
(def sql:unread-partial-comment-threads-by-team
|
|
(str "WITH threads AS (" sql:comment-threads ")"
|
|
"SELECT * FROM threads
|
|
WHERE count_unread_comments > 0
|
|
AND team_id = ?
|
|
AND (owner_id = ? OR ? = ANY(mentions))"))
|
|
|
|
(defn- get-unread-comment-threads
|
|
[conn profile-id team-id]
|
|
(let [profile (-> (db/get conn :profile {:id profile-id})
|
|
(profile/decode-row))
|
|
notify (or (-> profile :props :notifications :dashboard-comments) :all)]
|
|
|
|
(case notify
|
|
:all
|
|
(->> (db/exec! conn [sql:unread-all-comment-threads-by-team profile-id team-id])
|
|
(into [] xf-decode-row))
|
|
|
|
:partial
|
|
(->> (db/exec! conn [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id])
|
|
(into [] xf-decode-row))
|
|
|
|
[])))
|
|
|
|
;; --- COMMAND: Get Single Comment Thread
|
|
|
|
(def ^:private
|
|
schema:get-comment-thread
|
|
[:map {:title "get-comment-thread"}
|
|
[:file-id ::sm/uuid]
|
|
[:id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::get-comment-thread
|
|
{::doc/added "1.15"
|
|
::sm/params schema:get-comment-thread}
|
|
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
|
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(let [sql (str "WITH threads AS (" sql:comment-threads ")"
|
|
"SELECT * FROM threads WHERE id = ? AND file_id = ?")]
|
|
(-> (db/exec-one! conn [sql profile-id id file-id])
|
|
(decode-row))))))
|
|
|
|
;; --- COMMAND: Retrieve Comments
|
|
|
|
(declare ^:private get-comments)
|
|
|
|
(def ^:private
|
|
schema:get-comments
|
|
[:map {:title "get-comments"}
|
|
[:thread-id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::get-comments
|
|
{::doc/added "1.15"
|
|
::sm/params schema:get-comments}
|
|
[cfg {:keys [::rpc/profile-id thread-id share-id]}]
|
|
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(get-comments conn thread-id)))))
|
|
|
|
(defn- get-comments
|
|
[conn thread-id]
|
|
(->> (db/query conn :comment
|
|
{:thread-id thread-id}
|
|
{:order-by [[:created-at :asc]]})
|
|
(into [] xf-decode-row)))
|
|
|
|
;; --- COMMAND: Get file comments users
|
|
|
|
;; All the profiles that had comment the file, plus the current
|
|
;; profile.
|
|
|
|
(def ^:private sql:file-comment-users
|
|
"WITH available_profiles AS (
|
|
SELECT DISTINCT owner_id AS id
|
|
FROM comment
|
|
WHERE thread_id IN (SELECT id FROM comment_thread WHERE file_id=?)
|
|
)
|
|
SELECT p.id,
|
|
p.email,
|
|
p.fullname AS name,
|
|
p.fullname AS fullname,
|
|
p.photo_id,
|
|
p.is_active
|
|
FROM profile AS p
|
|
WHERE p.id IN (SELECT id FROM available_profiles) OR p.id=?")
|
|
|
|
(defn get-file-comments-users
|
|
[conn file-id profile-id]
|
|
(db/exec! conn [sql:file-comment-users file-id profile-id]))
|
|
|
|
(def ^:private
|
|
schema:get-profiles-for-file-comments
|
|
[:map {:title "get-profiles-for-file-comments"}
|
|
[:file-id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::get-profiles-for-file-comments
|
|
"Retrieves a list of profiles with limited set of properties of all
|
|
participants on comment threads of the file."
|
|
{::doc/added "1.15"
|
|
::doc/changes ["1.15" "Imported from queries and renamed."]
|
|
::sm/params schema:get-profiles-for-file-comments}
|
|
[cfg {:keys [::rpc/profile-id file-id share-id]}]
|
|
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(get-file-comments-users conn file-id profile-id))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; MUTATION COMMANDS
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(declare ^:private create-comment-thread)
|
|
|
|
;; --- COMMAND: Create Comment Thread
|
|
|
|
(def ^:private
|
|
schema:create-comment-thread
|
|
[:map {:title "create-comment-thread"}
|
|
[:file-id ::sm/uuid]
|
|
[:position ::gpt/point]
|
|
[:content [:string {:max 750}]]
|
|
[:page-id ::sm/uuid]
|
|
[:frame-id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]
|
|
[:mentions {:optional true} [:vector ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::create-comment-thread
|
|
{::doc/added "1.15"
|
|
::webhooks/event? true
|
|
::rtry/enabled true
|
|
::rtry/when rtry/conflict-exception?
|
|
::sm/params schema:create-comment-thread}
|
|
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id mentions position content frame-id]}]
|
|
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
|
|
|
(let [{:keys [team-id project-id page-name name]} (get-file cfg file-id page-id)]
|
|
(-> cfg
|
|
(assoc ::quotes/profile-id profile-id)
|
|
(assoc ::quotes/team-id team-id)
|
|
(assoc ::quotes/project-id project-id)
|
|
(assoc ::quotes/file-id file-id)
|
|
(quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
|
|
{::quotes/id ::quotes/comments-per-file}))
|
|
|
|
(let [params {:created-at request-at
|
|
:profile-id profile-id
|
|
:file-id file-id
|
|
:file-name name
|
|
:page-id page-id
|
|
:page-name page-name
|
|
:position position
|
|
:content content
|
|
:frame-id frame-id
|
|
:team-id team-id
|
|
:project-id project-id
|
|
:mentions mentions}
|
|
thread (-> (db/tx-run! cfg create-comment-thread params)
|
|
(decode-row))]
|
|
|
|
(vary-meta thread assoc ::audit/props thread))))
|
|
|
|
(defn- create-comment-thread
|
|
[{:keys [::db/conn] :as cfg}
|
|
{:keys [profile-id file-id page-id page-name created-at position content mentions frame-id] :as params}]
|
|
|
|
(let [;; NOTE: we take the next seq number from a separate query
|
|
;; because we need to lock the file for avoid race conditions
|
|
|
|
;; FIXME: this method touches and locks the file table,which
|
|
;; is already heavy-update tablel; we need to think on move
|
|
;; the sequence state management to a different table or
|
|
;; different storage (example: redis) for alivate the update
|
|
;; pression on the file table
|
|
|
|
seqn (get-next-seqn conn file-id)
|
|
thread-id (uuid/next)
|
|
thread (-> (db/insert! conn :comment-thread
|
|
{:id thread-id
|
|
:file-id file-id
|
|
:owner-id profile-id
|
|
:participants (db/tjson #{profile-id})
|
|
:page-name page-name
|
|
:page-id page-id
|
|
:created-at created-at
|
|
:modified-at created-at
|
|
:seqn seqn
|
|
:position (db/pgpoint position)
|
|
:frame-id frame-id
|
|
:mentions (db/encode-pgarray mentions conn "uuid")})
|
|
(decode-row))
|
|
comment (-> (db/insert! conn :comment
|
|
{:id (uuid/next)
|
|
:thread-id thread-id
|
|
:owner-id profile-id
|
|
:created-at created-at
|
|
:modified-at created-at
|
|
:mentions (db/encode-pgarray mentions conn "uuid")
|
|
:content content})
|
|
(decode-row))]
|
|
|
|
;; Make the current thread as read.
|
|
(upsert-comment-thread-status! conn profile-id thread-id created-at)
|
|
|
|
;; Optimistic update of current seq number on file.
|
|
(db/update! conn :file
|
|
{:comment-thread-seqn seqn}
|
|
{:id file-id}
|
|
{::db/return-keys false})
|
|
|
|
;; Send mentions emails
|
|
(send-comment-emails! conn params comment thread)
|
|
|
|
(-> thread
|
|
(select-keys [:id :file-id :page-id :mentions])
|
|
(assoc :comment-id (:id comment)))))
|
|
|
|
;; --- COMMAND: Update Comment Thread Status
|
|
|
|
(def ^:private
|
|
schema:update-comment-thread-status
|
|
[:map {:title "update-comment-thread-status"}
|
|
[:id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::update-comment-thread-status
|
|
{::doc/added "1.15"
|
|
::sm/params schema:update-comment-thread-status
|
|
::db/transaction true}
|
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(upsert-comment-thread-status! conn profile-id id)))
|
|
|
|
;; --- COMMAND: Update Comment Thread
|
|
|
|
(def ^:private
|
|
schema:update-comment-thread
|
|
[:map {:title "update-comment-thread"}
|
|
[:id ::sm/uuid]
|
|
[:is-resolved :boolean]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::update-comment-thread
|
|
{::doc/added "1.15"
|
|
::sm/params schema:update-comment-thread
|
|
::db/transaction true}
|
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id id is-resolved share-id]}]
|
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(db/update! conn :comment-thread
|
|
{:is-resolved is-resolved}
|
|
{:id id})
|
|
nil))
|
|
|
|
;; --- COMMAND: Add Comment
|
|
|
|
(declare ^:private get-comment-thread)
|
|
|
|
(def ^:private
|
|
schema:create-comment
|
|
[:map {:title "create-comment"}
|
|
[:thread-id ::sm/uuid]
|
|
[:content [:string {:max 250}]]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]
|
|
[:mentions {:optional true} [:vector ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::create-comment
|
|
{::doc/added "1.15"
|
|
::webhooks/event? true
|
|
::sm/params schema:create-comment
|
|
::db/transaction true}
|
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content mentions]}]
|
|
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)
|
|
{file-name :name :keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
|
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
|
|
::quotes/profile-id profile-id
|
|
::quotes/team-id team-id
|
|
::quotes/project-id project-id
|
|
::quotes/file-id file-id})
|
|
|
|
;; Update the page-name cached attribute on comment thread table.
|
|
(when (not= page-name (:page-name thread))
|
|
(db/update! conn :comment-thread
|
|
{:page-name page-name}
|
|
{:id thread-id}))
|
|
|
|
(let [comment (-> (db/insert!
|
|
conn :comment
|
|
{:id (uuid/next)
|
|
:created-at request-at
|
|
:modified-at request-at
|
|
:thread-id thread-id
|
|
:owner-id profile-id
|
|
:content content
|
|
:mentions
|
|
(-> mentions
|
|
(set)
|
|
(db/encode-pgarray conn "uuid"))})
|
|
(decode-row))
|
|
props {:file-id file-id
|
|
:share-id nil}]
|
|
|
|
;; Update thread modified-at attribute and assoc the current
|
|
;; profile to the participant set.
|
|
(db/update! conn :comment-thread
|
|
{:modified-at request-at
|
|
:participants (-> (:participants thread #{})
|
|
(conj profile-id)
|
|
(db/tjson))
|
|
:mentions (-> (:mentions thread)
|
|
(set)
|
|
(into mentions)
|
|
(db/encode-pgarray conn "uuid"))}
|
|
{:id thread-id})
|
|
|
|
;; Update the current profile status in relation to the
|
|
;; current thread.
|
|
(upsert-comment-thread-status! conn profile-id thread-id)
|
|
|
|
(let [params {:project-id project-id
|
|
:profile-id profile-id
|
|
:team-id team-id
|
|
:file-id (:file-id thread)
|
|
:page-id (:page-id thread)
|
|
:file-name file-name
|
|
:page-name page-name}]
|
|
(send-comment-emails! conn params comment thread))
|
|
|
|
(vary-meta comment assoc ::audit/props props))))
|
|
|
|
;; --- COMMAND: Update Comment
|
|
|
|
(def ^:private
|
|
schema:update-comment
|
|
[:map {:title "update-comment"}
|
|
[:id ::sm/uuid]
|
|
[:content [:string {:max 250}]]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]
|
|
[:mentions {:optional true} [:vector ::sm/uuid]]])
|
|
|
|
;; TODO Check if there are new mentions, if there are send the new emails.
|
|
(sv/defmethod ::update-comment
|
|
{::doc/added "1.15"
|
|
::sm/params schema:update-comment
|
|
::db/transaction true}
|
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content mentions]}]
|
|
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::sql/for-update true)
|
|
{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::sql/for-update true)]
|
|
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
|
|
;; Don't allow edit comments to not owners
|
|
(when-not (= owner-id profile-id)
|
|
(ex/raise :type :validation
|
|
:code :not-allowed))
|
|
|
|
(let [{:keys [page-name]} (get-file cfg file-id page-id)]
|
|
(db/update! conn :comment
|
|
{:content content
|
|
:modified-at request-at
|
|
:mentions (db/encode-pgarray mentions conn "uuid")}
|
|
{:id id})
|
|
|
|
(db/update! conn :comment-thread
|
|
{:modified-at request-at
|
|
:page-name page-name
|
|
:mentions
|
|
(-> (:mentions thread)
|
|
(set)
|
|
(into mentions)
|
|
(db/encode-pgarray conn "uuid"))}
|
|
{:id thread-id})
|
|
nil)))
|
|
|
|
;; --- COMMAND: Delete Comment Thread
|
|
|
|
(def ^:private
|
|
schema:delete-comment-thread
|
|
[:map {:title "delete-comment-thread"}
|
|
[:id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::delete-comment-thread
|
|
{::doc/added "1.15"
|
|
::sm/params schema:delete-comment-thread
|
|
::db/transaction true}
|
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
|
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(when-not (= owner-id profile-id)
|
|
(ex/raise :type :validation
|
|
:code :not-allowed))
|
|
|
|
(db/delete! conn :comment-thread {:id id})
|
|
nil))
|
|
|
|
;; --- COMMAND: Delete comment
|
|
|
|
(def ^:private
|
|
schema:delete-comment
|
|
[:map {:title "delete-comment"}
|
|
[:id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::delete-comment
|
|
{::doc/added "1.15"
|
|
::sm/params schema:delete-comment
|
|
::db/transaction true}
|
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
|
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::sql/for-update true)
|
|
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(when-not (= owner-id profile-id)
|
|
(ex/raise :type :validation
|
|
:code :not-allowed))
|
|
(db/delete! conn :comment {:id id})
|
|
nil))
|
|
|
|
;; --- COMMAND: Update comment thread position
|
|
|
|
(def ^:private
|
|
schema:update-comment-thread-position
|
|
[:map {:title "update-comment-thread-position"}
|
|
[:id ::sm/uuid]
|
|
[:position ::gpt/point]
|
|
[:frame-id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::update-comment-thread-position
|
|
{::doc/added "1.15"
|
|
::sm/params schema:update-comment-thread-position
|
|
::db/transaction true}
|
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}]
|
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(db/update! conn :comment-thread
|
|
{:modified-at request-at
|
|
:position (db/pgpoint position)
|
|
:frame-id frame-id}
|
|
{:id (:id thread)})
|
|
nil))
|
|
|
|
;; --- COMMAND: Update comment frame
|
|
|
|
(def ^:private
|
|
schema:update-comment-thread-frame
|
|
[:map {:title "update-comment-thread-frame"}
|
|
[:id ::sm/uuid]
|
|
[:frame-id ::sm/uuid]
|
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
|
|
|
(sv/defmethod ::update-comment-thread-frame
|
|
{::doc/added "1.15"
|
|
::sm/params schema:update-comment-thread-frame
|
|
::db/transaction true}
|
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}]
|
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
|
(db/update! conn :comment-thread
|
|
{:modified-at request-at
|
|
:frame-id frame-id}
|
|
{:id id})
|
|
nil))
|