diff --git a/backend/resources/gpml/config.edn b/backend/resources/gpml/config.edn index dde570e77..c7c61ed77 100644 --- a/backend/resources/gpml/config.edn +++ b/backend/resources/gpml/config.edn @@ -615,17 +615,25 @@ :handler #ig/ref :gpml.handler.chat/get-all-channels :parameters #ig/ref :gpml.handler.chat/get-all-channels-params}}] ["/private" - {:get {:summary "Get all private channels in the server" - :middleware [#ig/ref :gpml.auth/auth-middleware - #ig/ref :gpml.auth/auth-required] - :swagger {:tags ["chat"]} - :handler #ig/ref :gpml.handler.chat/get-private-channels} - :post {:summary "Send private channel invitation request" + ["" + {:get {:summary "Get all private channels in the server" :middleware [#ig/ref :gpml.auth/auth-middleware #ig/ref :gpml.auth/auth-required] :swagger {:tags ["chat"]} - :handler #ig/ref :gpml.handler.chat/send-private-channel-invitation-request - :parameters #ig/ref :gpml.handler.chat/send-private-channel-invitation-request-params}}] + :handler #ig/ref :gpml.handler.chat/get-private-channels} + :post {:summary "Send private channel invitation request" + :middleware [#ig/ref :gpml.auth/auth-middleware + #ig/ref :gpml.auth/auth-required] + :swagger {:tags ["chat"]} + :handler #ig/ref :gpml.handler.chat/send-private-channel-invitation-request + :parameters #ig/ref :gpml.handler.chat/send-private-channel-invitation-request-params}}] + ["/add-user" + {:post {:summary "Adds user to private channel" + :middleware [#ig/ref :gpml.auth/auth-middleware + #ig/ref :gpml.auth/auth-required] + :swagger {:tags ["chat"]} + :handler #ig/ref :gpml.handler.chat/add-user-to-private-channel + :parameters #ig/ref :gpml.handler.chat/add-user-to-private-channel-params}}]] ["/public" {:get {:summary "Get all public channels in the server" :middleware [#ig/ref :gpml.auth/auth-middleware @@ -899,6 +907,8 @@ :gpml.handler.chat/get-user-joined-channels #ig/ref :gpml.config/common :gpml.handler.chat/send-private-channel-invitation-request #ig/ref :gpml.config/common :gpml.handler.chat/send-private-channel-invitation-request-params {} + :gpml.handler.chat/add-user-to-private-channel #ig/ref :gpml.config/common + :gpml.handler.chat/add-user-to-private-channel-params {} :gpml.handler.chat/remove-user-from-channel #ig/ref :gpml.config/common :gpml.handler.chat/remove-user-from-channel-params {} diff --git a/backend/src/gpml/boundary/adapter/chat/rocket_chat/core.clj b/backend/src/gpml/boundary/adapter/chat/rocket_chat/core.clj index 65f0ed922..8c18371e3 100644 --- a/backend/src/gpml/boundary/adapter/chat/rocket_chat/core.clj +++ b/backend/src/gpml/boundary/adapter/chat/rocket_chat/core.clj @@ -387,6 +387,28 @@ :reason :exception :error-details {:msg (ex-message t)}}))) +(defn- add-user-to-private-channel* + [{:keys [logger api-key api-user-id] :as adapter} user-id channel-id] + (try + (let [{:keys [status body]} + (http-client/do-request logger + {:url (build-api-endpoint-url adapter "/groups.invite") + :method :post + :body (json/->json {:roomId channel-id :userId user-id}) + :headers (get-auth-headers api-key api-user-id) + :as :json-keyword-keys})] + (if (<= 200 status 299) + {:success? true} + {:success? false + :reason :failed-to-add-user-to-private-channel + :error-details body})) + (catch Throwable t + (log logger :error :failed-to-add-user-to-private-channel {:exception-message (ex-message t) + :stack-trace (map str (.getStackTrace t))}) + {:success? false + :reason :exception + :error-details {:msg (ex-message t)}}))) + (defrecord RocketChat [api-domain-url api-url-path api-key api-user-id logger] port/Chat (create-user-account [this user] @@ -414,4 +436,6 @@ (get-user-joined-channels [this user-id] (get-user-joined-channels* this user-id)) (remove-user-from-channel [this user-id channel-id channel-type] - (remove-user-from-channel* this user-id channel-id channel-type))) + (remove-user-from-channel* this user-id channel-id channel-type)) + (add-user-to-private-channel [this user-id channel-id] + (add-user-to-private-channel* this user-id channel-id))) diff --git a/backend/src/gpml/boundary/port/chat.clj b/backend/src/gpml/boundary/port/chat.clj index db15e8cd8..4599f3bbf 100644 --- a/backend/src/gpml/boundary/port/chat.clj +++ b/backend/src/gpml/boundary/port/chat.clj @@ -16,4 +16,5 @@ [this user-id] [this user-id opts]) (get-user-joined-channels [this user-id]) - (remove-user-from-channel [this user-id channel-id channel-type])) + (remove-user-from-channel [this user-id channel-id channel-type]) + (add-user-to-private-channel [this user-id channel-id])) diff --git a/backend/src/gpml/handler/chat.clj b/backend/src/gpml/handler/chat.clj index cc0884eb3..9b84bbc11 100644 --- a/backend/src/gpml/handler/chat.clj +++ b/backend/src/gpml/handler/chat.clj @@ -1,10 +1,13 @@ (ns gpml.handler.chat (:require [camel-snake-kebab.core :refer [->snake_case]] [camel-snake-kebab.extras :as cske] + [duct.logger :refer [log]] + [gpml.db.stakeholder :as db.stakeholder] [gpml.domain.types :as dom.types] [gpml.handler.resource.permission :as h.r.permission] [gpml.handler.responses :as r] [gpml.service.chat :as srv.chat] + [gpml.util.email :as email] [integrant.core :as ig])) (def ^:private channel-types @@ -21,6 +24,12 @@ (def ^:private send-private-channel-invitation-request-params-schema [:map + [:channel_id + {:optional false + :swagger {:description "The channel id" + :type "string" + :allowEmptyValue false}} + [:string {:min 1}]] [:channel_name {:optional false :swagger {:description "The channel name" @@ -28,6 +37,29 @@ :allowEmptyValue false}} [:string {:min 1}]]]) +(def ^:private add-user-to-private-channel-params-schema + [:map + [:channel_id + {:optional false + :swagger {:description "The channel id" + :type "string" + :allowEmptyValue false}} + [:string {:min 1}]] + [:channel_name + {:optional false + :swagger {:description "The channel name" + :type "string" + :allowEmptyValue false}} + [:string {:min 1}]] + [:user_id + {:optional false + :swagger {:description "The user's identifier in GPML" + :type "integer" + :allowEmptyValue false}} + [:fn + {:error/message "Not a valid user identifier. It should be a positive integer."} + pos-int?]]]) + (def ^:private get-all-channels-params-schema [:map [:name @@ -131,9 +163,11 @@ :root-context? true}) (r/forbidden {:message "Unauthorized"}) (let [channel-name (get-in parameters [:body :channel_name]) + channel-id (get-in parameters [:body :channel_id]) result (srv.chat/send-private-channel-invitation-request config user + channel-id channel-name)] (if (:success? result) (r/ok {}) @@ -150,6 +184,24 @@ (r/ok {}) (r/server-error (dissoc result :success?))))) +(defn- add-user-to-private-channel + [{:keys [db mailjet-config] :as config} parameters] + (let [{:keys [channel_id channel_name user_id]} (:body parameters) + target-user (db.stakeholder/get-stakeholder-by-id (:spec db) {:id user_id})] + (if (seq target-user) + (let [result (srv.chat/add-user-to-private-channel config + (:chat_account_id target-user) + channel_id)] + (if (:success? result) + (do + (email/notify-user-about-chat-private-channel-invitation-request-accepted + mailjet-config + target-user + channel_name) + (r/ok {})) + (r/server-error (dissoc result :success?)))) + (r/server-error {:reason :user-not-found})))) + (defmethod ig/init-key :gpml.handler.chat/post [_ config] (fn [req] @@ -197,6 +249,23 @@ [_ _] {:body send-private-channel-invitation-request-params-schema}) +(defmethod ig/init-key :gpml.handler.chat/add-user-to-private-channel-params + [_ _] + {:body add-user-to-private-channel-params-schema}) + +(defmethod ig/init-key :gpml.handler.chat/add-user-to-private-channel + [_ {:keys [logger] :as config}] + (fn [{parameters :parameters user :user}] + (if (h.r.permission/super-admin? config (:id user)) + (try + (add-user-to-private-channel config parameters) + (catch Throwable t + (log logger :error ::failed-to-add-user-to-private-channel {:exception-message (ex-message t)}) + (let [response {:success? false + :reason :could-not-add-user-to-private-channel}] + (r/server-error (assoc-in response [:error-details :error] (ex-message t)))))) + (r/forbidden {:message "Unauthorized"})))) + (defmethod ig/init-key :gpml.handler.chat/remove-user-from-channel [_ config] (fn [req] diff --git a/backend/src/gpml/service/chat.clj b/backend/src/gpml/service/chat.clj index 9f232e426..19faa318f 100644 --- a/backend/src/gpml/service/chat.clj +++ b/backend/src/gpml/service/chat.clj @@ -210,11 +210,18 @@ channel-id channel-type)) +(defn add-user-to-private-channel + [{:keys [chat-adapter]} chat-account-id channel-id] + (chat/add-user-to-private-channel chat-adapter + chat-account-id + channel-id)) + (defn send-private-channel-invitation-request - [{:keys [db mailjet-config]} user channel-name] + [{:keys [db mailjet-config]} user channel-id channel-name] (let [super-admins (db.rbac-util/get-super-admins-details (:spec db) {})] (util.email/notify-admins-new-chat-private-channel-invitation-request mailjet-config super-admins user + channel-id channel-name))) diff --git a/backend/src/gpml/util.clj b/backend/src/gpml/util.clj index f7049fede..1eb3cfe91 100644 --- a/backend/src/gpml/util.clj +++ b/backend/src/gpml/util.clj @@ -1,9 +1,9 @@ (ns gpml.util (:require [clojure.string :as str] [clojure.walk :as w] - [gpml.util.regular-expressions :as util.regex]) + [gpml.util.regular-expressions]) (:import [java.io File] - [java.net URL] + [java.net URL URLEncoder] [java.util Base64] [java.util UUID])) @@ -182,3 +182,7 @@ [email] (and string? (re-matches gpml.util.regular-expressions/email-re email))) + +(defn encode-url-param + [^String param] + (URLEncoder/encode param "utf-8")) diff --git a/backend/src/gpml/util/email.clj b/backend/src/gpml/util/email.clj index 086d9102b..02e45975a 100644 --- a/backend/src/gpml/util/email.clj +++ b/backend/src/gpml/util/email.clj @@ -1,9 +1,10 @@ (ns gpml.util.email (:require [clj-http.client :as client] - [clojure.string :as str] - [gpml.db.stakeholder :as db.stakeholder] - [gpml.handler.util :as util] - [jsonista.core :as j])) + [clojure.string :as str] + [gpml.db.stakeholder :as db.stakeholder] + [gpml.handler.util :as h.util] + [gpml.util :as util] + [jsonista.core :as j])) (defn make-message [sender receiver subject text html] {:From sender :To [receiver] :Subject subject :TextPart text :HTMLPart html}) @@ -16,10 +17,10 @@ (defn send-email [{:keys [api-key secret-key]} sender subject receivers texts htmls] (let [messages (map make-message (repeat sender) receivers (repeat subject) texts htmls)] (client/post "https://api.mailjet.com/v3.1/send" - {:basic-auth [api-key secret-key] - :content-type :json - :throw-exceptions false - :body (j/write-value-as-string {:Messages messages})}))) + {:basic-auth [api-key secret-key] + :content-type :json + :throw-exceptions false + :body (j/write-value-as-string {:Messages messages})}))) ;; FIXME: this shouldn't be hardcoded here. We'll be moving to ;; mailchimp soon so we'll refactor everything here. @@ -51,7 +52,7 @@ A new subscription request has arrived from %s. (defn notify-expert-invitation-text [first-name last-name invitation-id app-domain] (let [platform-link (str app-domain "/login?invite=" invitation-id) - user-full-name (get-user-full-name {:first_name first-name :last_name last-name})] + user-full-name (get-user-full-name {:first_name first-name :last_name last-name})] (format "Dear %s, You have been invited to join the UNEP GPML Digital Platform as an expert. @@ -86,9 +87,9 @@ Your submission has been published to %s/%s/%s. - UNEP GPML Digital Platform " - (:app-domain mailjet-config) - (util/get-api-topic-type topic-type topic-item) - (:id topic-item))) + (:app-domain mailjet-config) + (h.util/get-api-topic-type topic-type topic-item) + (:id topic-item))) (defn notify-user-review-rejected-text [mailjet-config topic-type topic-item] (format "Dear user, @@ -100,33 +101,50 @@ again, please visit this URL: %s/edit-%s/%s - UNEP GPML Digital Platform " - (util/get-title topic-type topic-item) - (:app-domain mailjet-config) - (-> (util/get-api-topic-type topic-type topic-item) - (str/replace "_" "-")) - (:id topic-item))) + (h.util/get-title topic-type topic-item) + (:app-domain mailjet-config) + (-> (h.util/get-api-topic-type topic-type topic-item) + (str/replace "_" "-")) + (:id topic-item))) (defn notify-user-review-subject [mailjet-config review-status topic-type topic-item] (format "[%s] %s %s" - (:app-name mailjet-config) - (util/get-display-topic-type topic-type topic-item) - (str/lower-case review-status))) + (:app-name mailjet-config) + (h.util/get-display-topic-type topic-type topic-item) + (str/lower-case review-status))) (defn notify-private-channel-invitation-request-subject [app-name channel-name] - (format "[%s] Invitation request for private channel %s" app-name channel-name)) + (format "[%s] Request to Join %s" app-name channel-name)) + +(defn notify-user-about-chat-private-channel-invitation-request-accepted-subject + [app-name channel-name] + (format "[%s] You've joined %s" app-name channel-name)) (defn notify-private-channel-invitation-request-text - [admin-name user-name user-email channel-name] - (format "Dear %s + [user-name channel-name review-request-link] + (format "%s wants to join %s + +Visit the link below to review the request: + +%s + +- UNEP GPML Digital Platform" + user-name + channel-name + review-request-link)) + +(defn notify-user-about-chat-private-channel-invitation-request-accepted-text + [channel-name base-url] + (format "Your request to join %s channel on the GPML platform was approved. + +View the forums in your GPML workspace: -%s user with email %s, is requesting access to the private channel %s. +%s/forum - UNEP GPML Digital Platform" - admin-name - user-name - user-email - channel-name)) + channel-name + base-url)) (defn notify-user-invitation-text [inviter-name app-domain entity-name] (format "Dear user, @@ -144,18 +162,18 @@ again, please visit this URL: %s/edit-%s/%s (defn notify-admins-pending-approval [db mailjet-config new-item] (let [admins (db.stakeholder/get-admins db) - item-type (:type new-item) - item-title (if (= item-type "stakeholder") - (get-user-full-name new-item) - (or (:title new-item) (:name new-item) (:tag new-item))) - subject (format "[%s] New %s needs approval" (:app-name mailjet-config) item-type) - sender unep-sender - names (map get-user-full-name admins) - receivers (map #(assoc {} :Name %1 :Email (:email %2)) names admins) - texts (->> names (map #(format notify-admins-pending-approval-text - %1 item-type item-title - (:app-domain mailjet-config)))) - htmls (repeat nil)] + item-type (:type new-item) + item-title (if (= item-type "stakeholder") + (get-user-full-name new-item) + (or (:title new-item) (:name new-item) (:tag new-item))) + subject (format "[%s] New %s needs approval" (:app-name mailjet-config) item-type) + sender unep-sender + names (map get-user-full-name admins) + receivers (map #(assoc {} :Name %1 :Email (:email %2)) names admins) + texts (->> names (map #(format notify-admins-pending-approval-text + %1 item-type item-title + (:app-domain mailjet-config)))) + htmls (repeat nil)] (when (> (count receivers) 0) (send-email mailjet-config sender subject receivers texts htmls)))) @@ -166,11 +184,11 @@ again, please visit this URL: %s/edit-%s/%s `texts` a collection of a single element, since in this case we are sending a single message." [mailjet-config dest-email req-email] (let [subject (format "[%s] New subscription request" (:app-name mailjet-config)) - sender unep-sender - receivers [{:Name "GPML Secretariat" - :Email dest-email}] - texts [(format notify-secretariat-new-subscription-text req-email)] - htmls (repeat nil)] + sender unep-sender + receivers [{:Name "GPML Secretariat" + :Email dest-email}] + texts [(format notify-secretariat-new-subscription-text req-email)] + htmls (repeat nil)] (send-email mailjet-config sender subject receivers texts htmls))) (defn notify-about-new-contact @@ -179,41 +197,65 @@ again, please visit this URL: %s/edit-%s/%s to be sent. That is why we provide an infinite sequence for non-used `htmls` option. Besides, we make `sender` and `texts` a collection of a single element, since in this case we are sending a single message." [mailjet-config {dest-email :dest-email - req-email :email - name :name - organization :organization - msg :message - subject :subject}] + req-email :email + name :name + organization :organization + msg :message + subject :subject}] (let [msg-body (format "Name: %s\nEmail: %s\nOrganization: %s\nMessage: \n%s" - name - req-email - organization - msg) - sender unep-sender - receivers [{:Name "Contact Management" - :Email dest-email}] - texts [msg-body] - htmls (repeat nil)] + name + req-email + organization + msg) + sender unep-sender + receivers [{:Name "Contact Management" + :Email dest-email}] + texts [msg-body] + htmls (repeat nil)] (send-email mailjet-config sender subject receivers texts htmls))) (defn notify-admins-new-chat-private-channel-invitation-request - [mailjet-config admins user channel-name] + [mailjet-config admins user channel-id channel-name] + (let [sender unep-sender + subject (notify-private-channel-invitation-request-subject + (:app-name mailjet-config) + channel-name) + receivers (map + (fn [admin] {:Name (get-user-full-name admin) + :Email (:email admin)}) + admins) + texts (map (fn [_receiver] + (notify-private-channel-invitation-request-text + (get-user-full-name user) + channel-name + (format "%s/admin/forum/add-user?user_id=%s&channel_id=%s&email=%s&channel_name=%s" + (:app-domain mailjet-config) + (:id user) + (util/encode-url-param channel-id) + (util/encode-url-param (:email user)) + (util/encode-url-param channel-name)))) + receivers) + htmls (repeat nil) + {:keys [status body]} (send-email mailjet-config sender subject receivers texts htmls)] + (if (<= 200 status 299) + {:success? true} + {:success? false + :reason :failed-to-send-email + :error-details body}))) + +(defn notify-user-about-chat-private-channel-invitation-request-accepted + [mailjet-config user channel-name] (let [sender unep-sender - subject (notify-private-channel-invitation-request-subject - (:app-name mailjet-config) - channel-name) - receivers (map - (fn [admin] {:Name (get-user-full-name admin) - :Email (:email admin)}) - admins) - texts (map (fn [receiver] - (notify-private-channel-invitation-request-text (:Name receiver) - (get-user-full-name user) - (:email user) - channel-name)) - receivers) - htmls (repeat nil) - {:keys [status body]} (send-email mailjet-config sender subject receivers texts htmls)] + subject (notify-user-about-chat-private-channel-invitation-request-accepted-subject + (:app-name mailjet-config) + channel-name) + receivers [{:Name (get-user-full-name user) + :Email (:email user)}] + texts [(notify-user-about-chat-private-channel-invitation-request-accepted-text + channel-name + (:app-domain mailjet-config))] + htmls (repeat nil) + {:keys [status body]} (send-email mailjet-config sender subject receivers texts htmls)] (if (<= 200 status 299) {:success? true} {:success? false @@ -223,8 +265,8 @@ again, please visit this URL: %s/edit-%s/%s (comment (require 'dev) (let [db (dev/db-conn) - config {:api-key (System/getenv "MAILJET_API_KEY") - :secret-key (System/getenv "MAILJET_SECRET_KEY") - :app-name (System/getenv "APP_NAME") - :app-domain (System/getenv "APP_DOMAIN")}] + config {:api-key (System/getenv "MAILJET_API_KEY") + :secret-key (System/getenv "MAILJET_SECRET_KEY") + :app-name (System/getenv "APP_NAME") + :app-domain (System/getenv "APP_DOMAIN")}] (notify-admins-pending-approval db config {:type "stakeholder" :title "Mr" :first_name "Puneeth" :last_name "Chaganti"})))