Skip to content

Commit

Permalink
Merge pull request #89 from jcpsantiago/join-slack-channel
Browse files Browse the repository at this point in the history
  • Loading branch information
jcpsantiago authored Nov 20, 2023
2 parents 91322be + dbca0b8 commit e837a67
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* Add `app.json` and define healthchecks for deployments with Dokku/Heroku
* Correctly set the logging as pretty EDN for dev, and JSON for prod
* Uninstall app from user's Slack if they uninstall from Confluence
* [#88](https://github.com/jcpsantiago/thearqivist/issues/88) Automatically join public channels

## 0.1.0 - 2023-04-20

Expand Down
3 changes: 2 additions & 1 deletion src/jcpsantiago/arqivist/api/slack/router.clj
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
{:description "Slack docs about Slash Commands"
:url "https://api.slack.com/interactivity/slash-commands"}}
:middleware [[wrap-verify-slack-request :verify-slack-request]
[wrap-add-slack-team-attributes :add-slack-team-attributes]]
[wrap-add-slack-team-attributes :add-slack-team-attributes]
[middleware-arqivist/wrap-join-slack-channel :join-slack-channel]]
:post
{:summary "Target for Slash Command interactions"
:description "This endpoint receives all interactions initiated by typing the `/arqive` slash command."
Expand Down
19 changes: 19 additions & 0 deletions src/jcpsantiago/arqivist/api/slack/specs.clj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
(spec/def ::token_type #{"bot" "user"})
(spec/def ::text string?)

(spec/def ::channel
(spec/keys
:req-un [::id ::name]))

;; Error response
(spec/def ::ok boolean?)
(spec/def ::error string?)
Expand Down Expand Up @@ -71,6 +75,21 @@
:good-response (spec/keys :req-un [::ok])
:error-response ::error-response))

;; User conversations API endpoint ------------------------------
(spec/def ::channels
(spec/coll-of ::channel))

(spec/def ::users-conversations
(spec/or
:good-response (spec/keys :req-un [::ok ::channels])
:error-response ::error-response))

;; Conversations join API endpoint ------------------------------
(spec/def ::conversations-join
(spec/or
:good-response (spec/keys :req-un [::ok ::channel])
:error-response ::error-response))

;; Slash commands are sent via POST requests with Content-type application/x-www-form-urlencoded.
;; See the docs in https://api.slack.com/interactivity/slash-commands#app_command_handling
;;
Expand Down
2 changes: 1 addition & 1 deletion src/jcpsantiago/arqivist/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
(mulog/set-global-context!
;; TODO: get the version from a file or config, issue #23
{:app-name "The Arqivist"
:version "2023-11-17.1"
:version "2023-11-19.1"
:service-profile (System/getenv "ARQIVIST_SERVICE_PROFILE")})
(mulog/log ::application-starup :arguments args)
(if team
Expand Down
105 changes: 103 additions & 2 deletions src/jcpsantiago/arqivist/middleware.clj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
[next.jdbc.sql :as sql]
[jcpsantiago.arqivist.api.confluence.utils :as utils]
[jcpsantiago.arqivist.api.slack.specs :as specs]
[ring.util.response :refer [bad-request]]
[jsonista.core :as json]))
[ring.util.response :refer [bad-request response]]
[clj-slack.conversations :as slack-convo]
[clj-slack.users :as slack-users]
[jsonista.core :as json]
[clojure.core :as c]))

;; Slack middleware ----------------------------------------------------------
(defn wrap-keep-raw-json-string
Expand Down Expand Up @@ -177,6 +180,104 @@
:error (.getMessage e)
:local-time (java.time.LocalDateTime/now))))))

;; Utils and handler for "join slack channel" middleware --- ↓

(defn conversation-member?
"
Util fn that checks if a channel-id is in the list of channels (aka conversations)
a user is in, as returned from the users.conversations Slack API method.
"
[channel-id user-conversations]
(let [member-channels (->> (:channels user-conversations)
(map :id)
(into #{}))]

(some member-channels [channel-id])))

(defn error-response-text
"
Returns a string with text for informing the user a generic error has happened.
Contains root-trace id from mulog.
"
[]
(str "Something didn't work 😣\n"
"I've alerted my supervisor, and a fix will be deployed ASAP so please try again later.\n"
"In case the error persists, please contact [email protected] directly and share the "
"error code: `" (:mulog/root-trace (mulog/local-context)) "`."))

(defn try-conversations-join
"
Helper function to the wrap-join-slack-channel middleware.
Tries to join a Slack channel. If the channel is private, informs the user of this fact, and
warns about the implications.
"
[slack-connection channel-id handler request]
(let [conversations-join-response (slack-convo/join slack-connection channel-id)
conversations-join (spec/conform ::specs/conversations-join conversations-join-response)]
(cond
(spec/valid? ::specs/conversations-join conversations-join)
(do
(mulog/log ::try-conversations-join
:success :true)
(handler request))

(some #{"method_not_supported_for_channel_type" "channel_not_found"}
[(:error conversations-join-response)])
(do
(mulog/log ::try-conversations-join
:success :false
:message "Possibly tried to join a private channel")
(response
(str "⚠️ It looks like you are trying to archive a *private channel*. "
"Archived channels do not carry over permissions, so assume the contents of the archive will be public.\n"
"If you are sure that is what you want, please invite me to this "
"channel first by mentioning me with <@the_arqivist>, "
"and then use the slash command again.")))

(not (spec/valid? ::specs/conversations-join conversations-join))
(do
(mulog/log ::try-conversations-join
:success :false
:message "conversations.join response did not conform to spec"
:explanation (spec/explain-data ::specs/conversations-join conversations-join-response))
(response (error-response-text))))))

(defn wrap-join-slack-channel
"
Ring middleware which checks if The Arqivist bot is a member of a channel,
then joins the channel, or asks the user to invite the bot in case it's a private channel.
* Must run _after_ the wrap-add-slack-team-attributes middleware to access slack-connection
* Used when interacting with the slash command endpoint.
"
[handler _]
(fn [request]
(let [channel_id (get-in request [:parameters :form :channel_id])
slack-connection (:slack-connection request)
users-conversations-response (slack-users/conversations slack-connection)
users-conversations (spec/conform ::specs/users-conversations users-conversations-response)
member? (conversation-member? channel_id users-conversations)
valid-spec? (spec/valid? ::specs/users-conversations users-conversations)]

(cond
(and valid-spec? member?)
(do
(mulog/log ::join-slack-channel
:success :true)
(handler request))

(and valid-spec? (not member?))
(try-conversations-join slack-connection channel_id handler request)

(not valid-spec?)
(do
(mulog/log ::join-slack-channel
:success :false
:message "Data does not conform to spec"
:slack-error (:error users-conversations-response)
:explanation (spec/explain-data ::specs/users-conversations users-conversations-response));
(response (error-response-text)))))))

;; Logging middleware -----------------------------------------------------
;; https://github.com/BrunoBonacci/mulog/blob/master/doc/ring-tracking.md
(defn wrap-trace-events
Expand Down

0 comments on commit e837a67

Please sign in to comment.