-
Notifications
You must be signed in to change notification settings - Fork 2
/
client.clj
150 lines (131 loc) · 5.37 KB
/
client.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
(ns slacker.client
"A simple Slack bot following an emit/handle flow. Read more about it in the
README."
(:require
[clojure.core.async :refer [<! <!! >! chan go go-loop pub sub]]
[clojure.data.json :refer [read-str]]
[clojure.stacktrace :refer [print-stack-trace]]
[clojure.string :refer [lower-case]]
[clojure.tools.logging :as log]
[org.httpkit.client :as http]
[gniazdo.core :refer [connect send-msg]]
[slacker.converters :refer [string->keyword string->slack-json]]))
(def ^:private publisher (chan))
(def ^:private publication (pub publisher first))
(def ^:private connection (atom nil))
(defn await!
"Blocks the thread, awaiting the occurrence of the given topic on the event
channel. This is very handy for awaiting :slacker.client/bot-disconnected in
the main function, which essentially blocks until the bot dies."
[topic]
(let [c (chan)]
(sub publication topic c)
(<!! c)))
(defn- emit!-template
[return-chan topic args]
(log/debugf "Emit: topic=[%s], ns=[%s], msg=[%s]" topic *ns* args)
(go (>! publisher (apply vector topic return-chan args))))
(defn emit!
"Emits an event to handlers. It will find all handlers registered for the
topic and call them with the additional arguments if any."
[topic & args]
(emit!-template nil topic args)
nil)
(defn emit-with-feedback!
"Emits an event to handlers. It will find all handlers registered for the
topic and call them with the additional arguments if any."
[topic & args]
(let [return-chan (chan)]
(emit!-template return-chan topic args)
return-chan))
(defmacro with-stacktrace-log
"Attempts to evaluate body and logs the stacktrace of any thrown throwable
as they would otherwise be difficult to notice given the asynchronous nature
of everything.
Wrap any expression for which error logging is desired in this macro."
[& body]
`(try
~@body
(catch Throwable t#
(->> t#
print-stack-trace
with-out-str
(log/errorf "Error during 'handle' in ns=[%s]:\n%s" *ns*)))))
(defn handle
"Subscribes an event handler for the given topic. The handler will be called
whenever an event is emitted for the given topic, and any optional args from
emit! will be passed as arguments to the handler-fn."
[topic handler-fn]
(let [c (chan)]
(sub publication topic c)
(go-loop []
(when-let [[topic return-chan & msg] (<! c)]
(go (with-stacktrace-log
(when-let [result (apply handler-fn msg)]
(when return-chan
(>! return-chan result)))))
(recur)))
nil))
;; +--------------------------------------------------------------------------+
;; | Websockets |
;; +--------------------------------------------------------------------------+
(defn connect-bot
"Retrieves the websocket URL for a given bot token and emits this token in a
command called ::connect-websocket commanding that a connection be made to
the URL."
[token]
(let [url (some-> (format "https://slack.com/api/rtm.start?token=%s" token)
(http/get)
(deref)
(:body)
(read-str :key-fn string->keyword)
(:url))]
(if url
(emit! ::connect-websocket token url)
(log/errorf "Failed connecting bot with token '%s'." token))))
(handle ::connect-bot connect-bot)
(defn connect-websocket
"Connects to the websocket temporarily available at `url` and emits the
following two events:
[::websocket-connected url socket] for handlers interested in the socket.
[::bot-connected token] for handlers interested in the token."
[token url]
(let [socket (connect url
:on-receive
(fn [raw]
(emit! ::receive-message
(read-str raw :key-fn string->keyword)))
:on-error
(fn [& args]
(log/error "Error in websocket.")
(emit! ::websocket-erred args)
(emit! ::bot-disconnected))
:on-close
(fn [& args]
(log/warn "Closed websocket.")
(emit! ::websocket-closed args)
(emit! ::bot-disconnected)))]
(reset! connection socket)
(emit! ::websocket-connected url socket)
(emit! ::bot-connected token)))
(handle ::connect-websocket connect-websocket)
;; +--------------------------------------------------------------------------+
;; | Message handling |
;; +--------------------------------------------------------------------------+
(defn receive-message
"Handles the reception of a message by republishing it with a keywordized
topic matching those described in the Slack API. This makes it easy to only
subscribe to the kind of events you want, such as :message, :presence_change,
etc."
[msg]
(let [topic (cond (:type msg) (:type msg)
(:reply_to msg) "reply"
:else "unknown")]
(go (>! publisher [(string->keyword topic) msg]))))
(handle ::receive-message receive-message)
(defn send-message
"Sends a message to the currently open websocket. Takes a channel ID and a
message in the form of a string."
[receiver msg]
(send-msg @connection (string->slack-json receiver msg)))
(handle ::send-message send-message)