-
Notifications
You must be signed in to change notification settings - Fork 14
/
gateway.clj
210 lines (182 loc) · 8.99 KB
/
gateway.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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
(ns discord.gateway
(:require
[clojure.core.async :refer [>! <! chan go go-loop] :as async]
[clojure.data.json :as json]
[discord.api.misc :as misc]
[discord.api.channels :as channels-api]
[discord.config :as config]
[discord.types.auth :as a]
[discord.types.messages :as messages]
[gniazdo.core :as ws]
[integrant.core :as ig]
[taoensso.timbre :as timbre]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Record Defining
;;;
;;; This is ultimately what this namespace is designed to construct and manage.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defrecord GatewayV2 [auth metadata websocket]
a/Authenticated
(token-type [_] (a/token-type auth))
(token [_] (a/token auth)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Helpers for messages that we will be sending *to* the gateway
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def gateway-intents
"This is the list of the valid intents that can be supplied to the Discord gateway. The
corresponding bit shift values are the indices of each intent. These intents and the information
they correspond to within Discord are documented here:
https://discord.com/developers/docs/topics/gateway#gateway-intents"
[:guilds :guild-members :guild-bans :guild-emojis :guild-integrations :guild-webhooks
:guild-invites :guild-voice-states :guild-presences :guild-messages :guild-message-reactions
:guild-message-typing :direct-messages :direct-message-reactions :direct-message-typing])
(defn intent-names->intent-value
"Given a list of intent names, specifically those from the gateway-intents set, calculates the
value that should be sent to the Discord gateway as a part of the Identify payload."
[desired-intents]
(let [intent-values (set (map (fn [intent-name] (.indexOf gateway-intents intent-name))
desired-intents))]
(if (contains? intent-values -1)
(throw (ex-info "Invalid intent supplied." {:valid-intents gateway-intents
:desired-intents desired-intents}))
(->> intent-values
(map (fn [iv] (bit-shift-left 1 iv)))
(reduce +)))))
(def gateway-event-types
[:dispatch :heartbeat :identify :presence :voice-state :voice-ping :resume :reconnect
:request-members :invalidate-session :hello :heartbeat-ack :guild-sync])
(defn format-gateway-message
[operation-name data]
(let [op-num (.indexOf gateway-event-types operation-name)]
(if (not (neg? op-num))
{:op op-num :d data}
(throw (ex-info "Invalid operation name!"
{:name operation-name :data data})))))
(defn send-gateway-message
[gateway message]
(let [formatted-message (json/write-str message)]
(ws/send-msg (:websocket gateway) formatted-message)))
(defn send-heartbeat [gateway]
(let [seq-num (get-in gateway [:metadata :seq-num])
heartbeat (format-gateway-message :heartbeat @seq-num)]
(timbre/debugf "Sending heartbeat for seq: %s" @seq-num)
(send-gateway-message gateway heartbeat)))
(defn send-identify
"Sends an identification message to the supplied Gateway. This tells the Discord gateway
information about ourselves."
[gateway intents]
(->> {:token (a/token gateway)
:properties {"$os" (System/getProperty "os.name")
"$browser" "discord.clj"
"$device" "discord.clj"}
:compress false
:large_threshold 250
:intents intents}
(format-gateway-message :identify)
(send-gateway-message gateway)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Gateway Control Management
;;;
;;; The Gateway Control events are primarily based on updating the state of the gateway connection
;;; session
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmulti handle-gateway-control-event
"Specifically handle events that are sent for the purpose of managing the gateway connection."
(fn [message metadata]
(gateway-event-types (:op message))))
(defmethod handle-gateway-control-event :hello
[message metadata]
(let [new-heartbeat (get-in message [:d :heartbeat_interval])]
(timbre/infof "Setting heartbeat interval to %d milliseconds" new-heartbeat)
(reset! (:heartbeat-interval metadata) new-heartbeat)))
;;; Since there is nothing to do regarding a heartback ACK message, we'll just ignore it.
(defmethod handle-gateway-control-event :heartbeat-ack [& _])
(defmethod handle-gateway-control-event :heartbeat
[message metadata])
(defmethod handle-gateway-control-event :default
[message metadata]
(timbre/infof "Unhandled gateway control event: %s" (gateway-event-types (:op message))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Gateway Message Events
;;;
;;; The gateway message events contain basically everything else that is transmitted over the
;;; gateway, including messages and updates about the state of users/guilds/channels that the bot is
;;; in. When a gateway event lacks a message type, that is when it is a control event and will be
;;; passed to the gateway control event handler.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmulti handle-gateway-event
"Handle messages from the Discord Gateway"
(fn handle-gateway-event-dispatch-fn [message auth metadata] (keyword (:t message))))
(defmethod handle-gateway-event nil
[message auth metadata]
(handle-gateway-control-event message metadata))
(defmethod handle-gateway-event :READY
[message auth metadata]
(reset! (:session-id metadata)
(get-in message [:d :session-id])))
(defmethod handle-gateway-event :HELLO
[message auth metadata]
(timbre/info "Received 'HELLO' Discord event."))
(defmethod handle-gateway-event :default
[message auth metadata]
(timbre/infof "Unknown message of type %s received." (keyword (:t message))))
(defmethod handle-gateway-event :MESSAGE_CREATE
[message auth metadata]
(timbre/infof "Received user message: %s" message)
(go (->> message :d messages/build-message (>! (:recv-chan metadata)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Connecting to the gateway and handling basic interactions with it
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key :discord/gateway-metadata
[_ {:keys [intents]}]
(let [intents-value (intent-names->intent-value intents)]
{:heartbeat-interval (atom nil)
:intents-value intents-value
:intent-names intents
:seq-num (atom 0)
:session-id (atom 0)
:stop-heartbeat-chan (async/chan)
:recv-chan (async/chan)
:send-chan (async/chan)}))
(defmethod ig/init-key :discord/message-handler-fn
[_ {:keys [auth config metadata]}]
(fn gateway-message-handler-fn [raw-message]
(let [message (json/read-str raw-message :key-fn keyword)
next-seq-num (:s message)]
;; Update the sequence number (if present)
(when (some? next-seq-num)
(swap! (:seq-num metadata) max next-seq-num))
;; Pass the message on to the handler
(handle-gateway-event message auth metadata))))
(defmethod ig/init-key :discord/websocket
[_ {:keys [auth message-handler-fn]}]
(let [gateway-url (misc/get-bot-gateway-url auth)]
(ws/connect
gateway-url
:on-receive message-handler-fn
:on-connect (fn [message] (timbre/infof "Connected to Discord Gateway (%s)" gateway-url))
:on-error (fn [message] (timbre/errorf "Error: %s" message)))))
(defmethod ig/halt-key! :discord/websocket
[_ websocket]
(timbre/infof "Closing websocket connection due to integrant halt!")
(ws/close websocket))
(defmethod ig/init-key :discord/gateway-connection
[_ {:keys [auth metadata websocket]}]
(let [gateway (->GatewayV2 auth metadata websocket)]
;; We'll send our initial heartbeat and identification events
(send-heartbeat gateway)
(send-identify gateway (:intents-value metadata))
;; Then we'll kick off a persistent loop in the background, which is sending the heartbeat.
(go-loop []
(when (-> metadata :heartbeat-interval deref some?)
(send-heartbeat gateway)
(async/alt!
(:stop-heartbeat-chan metadata) (timbre/warn "WebSocket closed! Stopping heartbeat")
(async/timeout @(:heartbeat-interval metadata)) (recur))))
;; We want to kick off a second loop in the background that is sending messages off send-chan
(go-loop []
(when-let [{:keys [channel-id content]} (<! (:send-chan metadata))]
(try (channels-api/send-message-to-channel auth channel-id content)
(catch Exception e (timbre/errorf "Error sending message: %s" e))))
(recur))
gateway))