onyxcord 1.1.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/Gemfile +0 -1
- data/lib/onyxcord/api/channel.rb +1 -13
- data/lib/onyxcord/api/webhook.rb +1 -1
- data/lib/onyxcord/api.rb +90 -37
- data/lib/onyxcord/bot.rb +2 -5
- data/lib/onyxcord/cache.rb +10 -11
- data/lib/onyxcord/commands/command_bot.rb +11 -17
- data/lib/onyxcord/configuration.rb +44 -1
- data/lib/onyxcord/data/component.rb +14 -0
- data/lib/onyxcord/data/interaction.rb +11 -13
- data/lib/onyxcord/event_executor.rb +29 -10
- data/lib/onyxcord/gateway.rb +88 -499
- data/lib/onyxcord/rate_limiter/rest.rb +1 -1
- data/lib/onyxcord/version.rb +1 -1
- data/lib/onyxcord/voice/network.rb +2 -2
- data/lib/onyxcord/webhooks/builder.rb +150 -0
- data/lib/onyxcord/webhooks/client.rb +184 -0
- data/lib/onyxcord/webhooks/embeds.rb +250 -0
- data/lib/onyxcord/webhooks/modal.rb +284 -0
- data/lib/onyxcord/webhooks/version.rb +9 -0
- data/lib/onyxcord/webhooks/view.rb +578 -0
- data/lib/onyxcord/websocket.rb +59 -40
- data/lib/onyxcord.rb +5 -0
- data/onyxcord-webhooks.gemspec +14 -5
- data/onyxcord.gemspec +20 -7
- metadata +114 -28
data/lib/onyxcord/gateway.rb
CHANGED
|
@@ -1,107 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
#
|
|
7
|
-
# MIT License
|
|
8
|
-
#
|
|
9
|
-
# Permission is hereby granted, free of charge, to any person obtaining
|
|
10
|
-
# a copy of this software and associated documentation files (the
|
|
11
|
-
# "Software"), to deal in the Software without restriction, including
|
|
12
|
-
# without limitation the rights to use, copy, modify, merge, publish,
|
|
13
|
-
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
14
|
-
# permit persons to whom the Software is furnished to do so, subject to
|
|
15
|
-
# the following conditions:
|
|
16
|
-
#
|
|
17
|
-
# The above copyright notice and this permission notice shall be
|
|
18
|
-
# included in all copies or substantial portions of the Software.
|
|
19
|
-
#
|
|
20
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
21
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
22
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
23
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
24
|
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
25
|
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
26
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
27
|
-
|
|
3
|
+
require 'async'
|
|
4
|
+
require 'async/http/endpoint'
|
|
5
|
+
require 'async/websocket/client'
|
|
28
6
|
require 'onyxcord/rate_limiter/gateway'
|
|
29
7
|
|
|
30
8
|
module OnyxCord
|
|
31
9
|
# Gateway packet opcodes
|
|
32
10
|
module Opcodes
|
|
33
|
-
# **Received** when Discord dispatches an event to the gateway (like MESSAGE_CREATE, PRESENCE_UPDATE or whatever).
|
|
34
|
-
# The vast majority of received packets will have this opcode.
|
|
35
11
|
DISPATCH = 0
|
|
36
|
-
|
|
37
|
-
# **Two-way**: The client has to send a packet with this opcode every ~40 seconds (actual interval specified in
|
|
38
|
-
# READY or RESUMED) and the current sequence number, otherwise it will be disconnected from the gateway. In certain
|
|
39
|
-
# cases Discord may also send one, specifically if two clients are connected at once.
|
|
40
12
|
HEARTBEAT = 1
|
|
41
|
-
|
|
42
|
-
# **Sent**: This is one of the two possible ways to initiate a session after connecting to the gateway. It
|
|
43
|
-
# should contain the authentication token along with other stuff the server has to know right from the start, such
|
|
44
|
-
# as large_threshold and, for older gateway versions, the desired version.
|
|
45
13
|
IDENTIFY = 2
|
|
46
|
-
|
|
47
|
-
# **Sent**: Packets with this opcode are used to change the user's status and played game. (Sending this is never
|
|
48
|
-
# necessary for a gateway client to behave correctly)
|
|
49
14
|
PRESENCE = 3
|
|
50
|
-
|
|
51
|
-
# **Sent**: Packets with this opcode are used to change a user's voice state (mute/deaf/unmute/undeaf/etc.). It is
|
|
52
|
-
# also used to connect to a voice server in the first place. (Sending this is never necessary for a gateway client
|
|
53
|
-
# to behave correctly)
|
|
54
15
|
VOICE_STATE = 4
|
|
55
|
-
|
|
56
|
-
# **Sent**: This opcode is used to ping a voice server, whatever that means. The functionality of this opcode isn't
|
|
57
|
-
# known well but non-user clients should never send it.
|
|
58
16
|
VOICE_PING = 5
|
|
59
|
-
|
|
60
|
-
# **Sent**: This is the other of two possible ways to initiate a gateway session (other than {IDENTIFY}). Rather
|
|
61
|
-
# than starting an entirely new session, it resumes an existing session by replaying all events from a given
|
|
62
|
-
# sequence number. It should be used to recover from a connection error or anything like that when the session is
|
|
63
|
-
# still valid - sending this with an invalid session will cause an error to occur.
|
|
64
17
|
RESUME = 6
|
|
65
|
-
|
|
66
|
-
# **Received**: Discord sends this opcode to indicate that the client should reconnect to a different gateway
|
|
67
|
-
# server because the old one is currently being decommissioned. Counterintuitively, this opcode also invalidates the
|
|
68
|
-
# session - the client has to create an entirely new session with the new gateway instead of resuming the old one.
|
|
69
18
|
RECONNECT = 7
|
|
70
|
-
|
|
71
|
-
# **Sent**: This opcode identifies packets used to retrieve a list of members from a particular server. There is
|
|
72
|
-
# also a REST endpoint available for this, but it is inconvenient to use because the client has to implement
|
|
73
|
-
# pagination itself, whereas sending this opcode lets Discord handle the pagination and the client can just add
|
|
74
|
-
# members when it receives them. (Sending this is never necessary for a gateway client to behave correctly)
|
|
75
19
|
REQUEST_MEMBERS = 8
|
|
76
|
-
|
|
77
|
-
# **Received**: Sent by Discord when the session becomes invalid for any reason. This may include improperly
|
|
78
|
-
# resuming existing sessions, attempting to start sessions with invalid data, or something else entirely. The client
|
|
79
|
-
# should handle this by simply starting a new session.
|
|
80
20
|
INVALIDATE_SESSION = 9
|
|
81
|
-
|
|
82
|
-
# **Received**: Sent immediately for any opened connection; tells the client to start heartbeating early on, so the
|
|
83
|
-
# server can safely search for a session server to handle the connection without the connection being terminated.
|
|
84
|
-
# As a side-effect, large bots are less likely to disconnect because of very large READY parse times.
|
|
85
21
|
HELLO = 10
|
|
86
|
-
|
|
87
|
-
# **Received**: Returned after a heartbeat was sent to the server. This allows clients to identify and deal with
|
|
88
|
-
# zombie connections that don't dispatch any events anymore.
|
|
89
22
|
HEARTBEAT_ACK = 11
|
|
90
23
|
end
|
|
91
24
|
|
|
92
|
-
#
|
|
93
|
-
# there may be multiple sessions per connection or one session may persist over multiple connections.
|
|
25
|
+
# Stores data of an active gateway session.
|
|
94
26
|
class Session
|
|
95
|
-
|
|
96
|
-
attr_reader :session_id
|
|
97
|
-
|
|
98
|
-
# @return [Integer] Incrementing integer used to determine the most recent event reccived from Discord.
|
|
27
|
+
attr_reader :session_id, :resume_gateway_url
|
|
99
28
|
attr_accessor :sequence
|
|
100
29
|
|
|
101
|
-
# @return [String] Gateway URL used to reconnect to the gateway node that Discord wants this session to use.
|
|
102
|
-
attr_reader :resume_gateway_url
|
|
103
|
-
|
|
104
|
-
# @!visibility private
|
|
105
30
|
def initialize(session_id, resume_gateway_url)
|
|
106
31
|
@session_id = session_id
|
|
107
32
|
@sequence = 0
|
|
@@ -110,7 +35,6 @@ module OnyxCord
|
|
|
110
35
|
@resume_gateway_url = resume_gateway_url
|
|
111
36
|
end
|
|
112
37
|
|
|
113
|
-
# Flags this session as suspended, so we know not to try and send heartbeats, etc. to the gateway until we've reconnected
|
|
114
38
|
def suspend
|
|
115
39
|
@suspended = true
|
|
116
40
|
end
|
|
@@ -119,12 +43,10 @@ module OnyxCord
|
|
|
119
43
|
@suspended
|
|
120
44
|
end
|
|
121
45
|
|
|
122
|
-
# Flags this session as no longer being suspended, so we can resume
|
|
123
46
|
def resume
|
|
124
47
|
@suspended = false
|
|
125
48
|
end
|
|
126
49
|
|
|
127
|
-
# Flags this session as being invalid
|
|
128
50
|
def invalidate
|
|
129
51
|
@invalid = true
|
|
130
52
|
@resume_gateway_url = nil
|
|
@@ -139,163 +61,100 @@ module OnyxCord
|
|
|
139
61
|
end
|
|
140
62
|
end
|
|
141
63
|
|
|
142
|
-
# Client for the Discord gateway protocol
|
|
64
|
+
# Client for the Discord gateway protocol — fully async.
|
|
143
65
|
class Gateway
|
|
144
|
-
# How many members there need to be in a server for it to count as "large"
|
|
145
66
|
LARGE_THRESHOLD = 100
|
|
146
|
-
|
|
147
|
-
# The version of the gateway that's supposed to be used.
|
|
148
67
|
GATEWAY_VERSION = 9
|
|
149
|
-
|
|
150
|
-
# Close codes that are unrecoverable, after which we should not try to reconnect.
|
|
151
|
-
# - 4003: Not authenticated. How did this happen?
|
|
152
|
-
# - 4004: Authentication failed. Token was wrong, nothing we can do.
|
|
153
|
-
# - 4011: Sharding required. Currently requires developer intervention.
|
|
154
|
-
# - 4014: Use of disabled privileged intents.
|
|
155
68
|
FATAL_CLOSE_CODES = [4003, 4004, 4011, 4014].freeze
|
|
156
69
|
|
|
157
|
-
# Heartbeat ACKs are Discord's way of verifying on the client side whether the connection is still alive. If this is
|
|
158
|
-
# set to true (default value) the gateway client will use that functionality to detect zombie connections and
|
|
159
|
-
# reconnect in such a case; however it may lead to instability if there's some problem with the ACKs. If this occurs
|
|
160
|
-
# it can simply be set to false.
|
|
161
|
-
# @return [true, false] whether or not this gateway should check for heartbeat ACKs.
|
|
162
70
|
attr_accessor :check_heartbeat_acks
|
|
163
|
-
|
|
164
|
-
# @return [Integer] the intent parameter sent to the gateway server.
|
|
165
71
|
attr_reader :intents
|
|
166
72
|
|
|
167
73
|
def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = ALL_INTENTS)
|
|
168
74
|
@token = token
|
|
169
75
|
@bot = bot
|
|
170
|
-
|
|
171
76
|
@shard_key = shard_key
|
|
172
|
-
|
|
173
|
-
# Whether the connection to the gateway has succeeded yet
|
|
174
77
|
@ws_success = false
|
|
175
|
-
|
|
176
78
|
@check_heartbeat_acks = true
|
|
177
|
-
|
|
178
79
|
@compress_mode = compress_mode
|
|
179
80
|
@intents = intents
|
|
180
81
|
@send_limiter = OnyxCord::RateLimiter::Gateway.new
|
|
82
|
+
@connection = nil
|
|
83
|
+
@closed = true
|
|
84
|
+
@pipe_broken = false
|
|
181
85
|
end
|
|
182
86
|
|
|
183
|
-
# Connect to the gateway server
|
|
87
|
+
# Connect to the gateway server inside an Async reactor.
|
|
184
88
|
def run_async
|
|
185
89
|
@ws_thread = Thread.new do
|
|
186
|
-
Thread.current[:onyxcord_name] = '
|
|
187
|
-
|
|
188
|
-
|
|
90
|
+
Thread.current[:onyxcord_name] = 'gateway'
|
|
91
|
+
Async do |task|
|
|
92
|
+
@reactor_task = task
|
|
93
|
+
connect_loop
|
|
94
|
+
LOGGER.warn('The gateway loop exited!')
|
|
95
|
+
end
|
|
189
96
|
end
|
|
190
97
|
|
|
191
|
-
LOGGER.debug('
|
|
98
|
+
LOGGER.debug('Gateway thread created! Waiting for confirmation...')
|
|
192
99
|
loop do
|
|
193
100
|
sleep(0.5)
|
|
194
|
-
|
|
195
|
-
if @
|
|
196
|
-
LOGGER.debug('Confirmation received! Exiting run.')
|
|
197
|
-
break
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
if @should_reconnect == false
|
|
201
|
-
LOGGER.debug('Reconnection flag was unset. Exiting run.')
|
|
202
|
-
break
|
|
203
|
-
end
|
|
101
|
+
break if @ws_success
|
|
102
|
+
break if @should_reconnect == false
|
|
204
103
|
end
|
|
205
104
|
end
|
|
206
105
|
|
|
207
|
-
# Prevents all further execution until the websocket thread stops (e.g. through a closed connection).
|
|
208
106
|
def sync
|
|
209
|
-
@ws_thread
|
|
107
|
+
@ws_thread&.join
|
|
210
108
|
end
|
|
211
109
|
|
|
212
|
-
# Whether the WebSocket connection to the gateway is currently open
|
|
213
110
|
def open?
|
|
214
|
-
|
|
111
|
+
!@closed && @connection
|
|
215
112
|
end
|
|
216
113
|
|
|
217
|
-
# Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
|
|
218
|
-
# Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
|
|
219
|
-
#
|
|
220
|
-
# If this method doesn't work or you're looking for something more drastic, use {#kill} instead.
|
|
221
114
|
def stop
|
|
222
115
|
@should_reconnect = false
|
|
223
116
|
close
|
|
224
|
-
|
|
225
|
-
# Return nil so command bots don't send a message
|
|
226
117
|
nil
|
|
227
118
|
end
|
|
228
119
|
|
|
229
|
-
# Kills the websocket thread, stopping all connections to Discord.
|
|
230
120
|
def kill
|
|
231
|
-
@ws_thread
|
|
121
|
+
@ws_thread&.kill
|
|
232
122
|
end
|
|
233
123
|
|
|
234
|
-
# Notifies the {#run_async} method that everything is ready and the caller can now continue (i.e. with syncing,
|
|
235
|
-
# or with doing processing and then syncing)
|
|
236
124
|
def notify_ready
|
|
237
125
|
@ws_success = true
|
|
238
126
|
end
|
|
239
127
|
|
|
240
|
-
# Injects a reconnect event (op 7) into the event processor, causing Discord to reconnect to the given gateway URL.
|
|
241
|
-
# If the URL is set to nil, it will reconnect and get an entirely new gateway URL. This method has not much use
|
|
242
|
-
# outside of testing and implementing highly custom reconnect logic.
|
|
243
|
-
# @param url [String, nil] the URL to connect to or nil if one should be obtained from Discord.
|
|
244
128
|
def inject_reconnect(url = nil)
|
|
245
|
-
# When no URL is specified, the data should be nil, as is the case with Discord-sent packets.
|
|
246
129
|
data = url ? { url: url } : nil
|
|
247
|
-
|
|
248
|
-
handle_message({
|
|
249
|
-
op: Opcodes::RECONNECT,
|
|
250
|
-
d: data
|
|
251
|
-
}.to_json)
|
|
130
|
+
handle_message({ op: Opcodes::RECONNECT, d: data }.to_json)
|
|
252
131
|
end
|
|
253
132
|
|
|
254
|
-
# Injects a resume packet (op 6) into the gateway. If this is done with a running connection, it will cause an
|
|
255
|
-
# error. It has no use outside of testing stuff that I know of, but if you want to use it anyway for some reason,
|
|
256
|
-
# here it is.
|
|
257
|
-
# @param seq [Integer, nil] The sequence ID to inject, or nil if the currently tracked one should be used.
|
|
258
133
|
def inject_resume(seq)
|
|
259
134
|
send_resume(raw_token, @session_id, seq || @sequence)
|
|
260
135
|
end
|
|
261
136
|
|
|
262
|
-
# Injects a terminal gateway error into the handler. Useful for testing the reconnect logic.
|
|
263
|
-
# @param e [Exception] The exception object to inject.
|
|
264
137
|
def inject_error(e)
|
|
265
138
|
handle_internal_close(e)
|
|
266
139
|
end
|
|
267
140
|
|
|
268
|
-
# Sends a heartbeat with the last received packet's seq (to acknowledge that we have received it and all packets
|
|
269
|
-
# before it), or if none have been received yet, with 0.
|
|
270
|
-
# @see #send_heartbeat
|
|
271
141
|
def heartbeat
|
|
272
142
|
if check_heartbeat_acks
|
|
273
143
|
unless @last_heartbeat_acked
|
|
274
|
-
|
|
275
|
-
# a zombie. Reconnect
|
|
276
|
-
LOGGER.warn('Last heartbeat was not acked, so this is a zombie connection! Reconnecting')
|
|
277
|
-
|
|
278
|
-
# We can't send anything on zombie connections
|
|
144
|
+
LOGGER.warn('Last heartbeat was not acked — zombie connection! Reconnecting')
|
|
279
145
|
@pipe_broken = true
|
|
280
146
|
reconnect
|
|
281
147
|
return
|
|
282
148
|
end
|
|
283
|
-
|
|
284
149
|
@last_heartbeat_acked = false
|
|
285
150
|
end
|
|
286
|
-
|
|
287
151
|
send_heartbeat(@session ? @session.sequence : 0)
|
|
288
152
|
end
|
|
289
153
|
|
|
290
|
-
# Sends a heartbeat packet (op 1). This tells Discord that the current connection is still active and that the last
|
|
291
|
-
# packets until the given sequence have been processed (in case of a resume).
|
|
292
|
-
# @param sequence [Integer] The sequence number for which to send a heartbeat.
|
|
293
154
|
def send_heartbeat(sequence)
|
|
294
155
|
send_packet(Opcodes::HEARTBEAT, sequence)
|
|
295
156
|
end
|
|
296
157
|
|
|
297
|
-
# Identifies to Discord with the default parameters.
|
|
298
|
-
# @see #send_identify
|
|
299
158
|
def identify
|
|
300
159
|
compress = @compress_mode == :large
|
|
301
160
|
send_identify(@token, {
|
|
@@ -305,253 +164,105 @@ module OnyxCord
|
|
|
305
164
|
}, compress, LARGE_THRESHOLD, @shard_key, @intents)
|
|
306
165
|
end
|
|
307
166
|
|
|
308
|
-
# Sends an identify packet (op 2). This starts a new session on the current connection and tells Discord who we are.
|
|
309
|
-
# This can only be done once a connection.
|
|
310
|
-
# @param token [String] The token with which to authorise the session. If it belongs to a bot account, it must be
|
|
311
|
-
# prefixed with "Bot ".
|
|
312
|
-
# @param properties [Hash<Symbol => String>] A list of properties for Discord to use in analytics. The following
|
|
313
|
-
# keys are recognised:
|
|
314
|
-
#
|
|
315
|
-
# - "os" (recommended value: the operating system the bot is running on)
|
|
316
|
-
# - "browser" (recommended value: library name)
|
|
317
|
-
# - "device" (recommended value: library name)
|
|
318
|
-
# - "referrer" (recommended value: empty)
|
|
319
|
-
# - "referring_domain" (recommended value: empty)
|
|
320
|
-
#
|
|
321
|
-
# @param compress [true, false] Whether certain large packets should be compressed using zlib.
|
|
322
|
-
# @param large_threshold [Integer] The member threshold after which a server counts as large and will have to have
|
|
323
|
-
# its member list chunked.
|
|
324
|
-
# @param shard_key [Array(Integer, Integer), nil] The shard key to use for sharding, represented as
|
|
325
|
-
# [shard_id, num_shards], or nil if the bot should not be sharded.
|
|
326
167
|
def send_identify(token, properties, compress, large_threshold, shard_key = nil, intents = ALL_INTENTS)
|
|
327
168
|
data = {
|
|
328
|
-
# Don't send a v anymore as it's entirely determined by the URL now
|
|
329
169
|
token: token,
|
|
330
170
|
properties: properties,
|
|
331
171
|
compress: compress,
|
|
332
172
|
large_threshold: large_threshold,
|
|
333
173
|
intents: intents
|
|
334
174
|
}
|
|
335
|
-
|
|
336
|
-
# Don't include the shard key at all if it is nil as Discord checks for its mere existence
|
|
337
175
|
data[:shard] = shard_key if shard_key
|
|
338
|
-
|
|
339
176
|
send_packet(Opcodes::IDENTIFY, data)
|
|
340
177
|
end
|
|
341
178
|
|
|
342
|
-
# Sends a status update packet (op 3). This sets the bot user's status (online/idle/...) and game playing/streaming.
|
|
343
|
-
# @param status [String] The status that should be set (`online`, `idle`, `dnd`, `invisible`).
|
|
344
|
-
# @param since [Integer] The Unix timestamp in milliseconds when the status was set. Should only be provided when
|
|
345
|
-
# `afk` is true.
|
|
346
|
-
# @param game [Hash<Symbol => Object>, nil] `nil` if no game should be played, or a hash of `:game => "name"` if a
|
|
347
|
-
# game should be played. The hash can also contain additional attributes for streaming statuses.
|
|
348
|
-
# @param afk [true, false] Whether the status was set due to inactivity on the user's part.
|
|
349
179
|
def send_status_update(status, since, game, afk)
|
|
350
|
-
|
|
351
|
-
status: status,
|
|
352
|
-
since: since,
|
|
353
|
-
game: game,
|
|
354
|
-
afk: afk
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
send_packet(Opcodes::PRESENCE, data)
|
|
180
|
+
send_packet(Opcodes::PRESENCE, { status: status, since: since, game: game, afk: afk })
|
|
358
181
|
end
|
|
359
182
|
|
|
360
|
-
# Sends a voice state update packet (op 4). This packet can connect a user to a voice channel, update self mute/deaf
|
|
361
|
-
# status in an existing voice connection, move the user to a new voice channel on the same server or disconnect an
|
|
362
|
-
# existing voice connection.
|
|
363
|
-
# @param server_id [Integer] The ID of the server on which this action should occur.
|
|
364
|
-
# @param channel_id [Integer, nil] The channel ID to connect/move to, or `nil` to disconnect.
|
|
365
|
-
# @param self_mute [true, false] Whether the user should itself be muted to everyone else.
|
|
366
|
-
# @param self_deaf [true, false] Whether the user should be deaf towards other users.
|
|
367
183
|
def send_voice_state_update(server_id, channel_id, self_mute, self_deaf)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
self_deaf: self_deaf
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
send_packet(Opcodes::VOICE_STATE, data)
|
|
184
|
+
send_packet(Opcodes::VOICE_STATE, {
|
|
185
|
+
guild_id: server_id, channel_id: channel_id,
|
|
186
|
+
self_mute: self_mute, self_deaf: self_deaf
|
|
187
|
+
})
|
|
376
188
|
end
|
|
377
189
|
|
|
378
|
-
# Resumes the session from the last recorded point.
|
|
379
|
-
# @see #send_resume
|
|
380
190
|
def resume
|
|
381
191
|
send_resume(@token, @session.session_id, @session.sequence)
|
|
382
192
|
end
|
|
383
193
|
|
|
384
|
-
# Reconnects the gateway connection in a controlled manner.
|
|
385
|
-
# @param attempt_resume [true, false] Whether a resume should be attempted after the reconnection.
|
|
386
194
|
def reconnect(attempt_resume = true)
|
|
387
195
|
@session.suspend if @session && attempt_resume
|
|
388
|
-
|
|
389
196
|
@instant_reconnect = true
|
|
390
197
|
@should_reconnect = true
|
|
391
|
-
|
|
392
198
|
close(4000)
|
|
393
199
|
end
|
|
394
200
|
|
|
395
|
-
# Sends a resume packet (op 6). This replays all events from a previous point specified by its packet sequence. This
|
|
396
|
-
# will not work if the packet to resume from has already been acknowledged using a heartbeat, or if the session ID
|
|
397
|
-
# belongs to a now invalid session.
|
|
398
|
-
#
|
|
399
|
-
# If this packet is sent at the beginning of a connection, it will act similarly to an {#identify} in that it
|
|
400
|
-
# creates a session on the current connection. Unlike identify however, this packet can also be sent in an existing
|
|
401
|
-
# session and will just replay some of the events.
|
|
402
|
-
# @param token [String] The token that was used to identify the session to resume.
|
|
403
|
-
# @param session_id [String] The session ID of the session to resume.
|
|
404
|
-
# @param seq [Integer] The packet sequence of the packet after which the events should be replayed.
|
|
405
201
|
def send_resume(token, session_id, seq)
|
|
406
|
-
|
|
407
|
-
token: token,
|
|
408
|
-
session_id: session_id,
|
|
409
|
-
seq: seq
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
send_packet(Opcodes::RESUME, data)
|
|
202
|
+
send_packet(Opcodes::RESUME, { token: token, session_id: session_id, seq: seq })
|
|
413
203
|
end
|
|
414
204
|
|
|
415
|
-
# Sends a request members packet (op 8). This will order Discord to gradually sent all requested members as dispatch
|
|
416
|
-
# events with type `GUILD_MEMBERS_CHUNK`. It is necessary to use this method in order to get all members of a large
|
|
417
|
-
# server (see `large_threshold` in {#send_identify}), however it can also be used for other purposes.
|
|
418
|
-
# @param server_id [Integer] The ID of the server whose members to query.
|
|
419
|
-
# @param query [String] If this string is not empty, only members whose username starts with this string will be
|
|
420
|
-
# returned.
|
|
421
|
-
# @param limit [Integer] How many members to send at maximum, or `0` to send all members.
|
|
422
205
|
def send_request_members(server_id, query, limit)
|
|
423
|
-
|
|
424
|
-
guild_id: server_id,
|
|
425
|
-
query: query,
|
|
426
|
-
limit: limit
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
send_packet(Opcodes::REQUEST_MEMBERS, data)
|
|
206
|
+
send_packet(Opcodes::REQUEST_MEMBERS, { guild_id: server_id, query: query, limit: limit })
|
|
430
207
|
end
|
|
431
208
|
|
|
432
|
-
# Sends a custom packet over the connection. This can be useful to implement future yet unimplemented functionality
|
|
433
|
-
# or for testing. You probably shouldn't use this unless you know what you're doing.
|
|
434
|
-
# @param opcode [Integer] The opcode the packet should be sent as. Can be one of {Opcodes} or a custom value if
|
|
435
|
-
# necessary.
|
|
436
|
-
# @param packet [Object] Some arbitrary JSON-serializable data that should be sent as the `d` field.
|
|
437
209
|
def send_packet(opcode, packet)
|
|
438
|
-
|
|
439
|
-
op: opcode,
|
|
440
|
-
d: packet
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
send(data.to_json)
|
|
210
|
+
send({ op: opcode, d: packet }.to_json)
|
|
444
211
|
end
|
|
445
212
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
# @param data [String] The data to send.
|
|
449
|
-
# @param type [Symbol] The type the WebSocket frame should have; either `:text`, `:binary`, `:ping`, `:pong`, or
|
|
450
|
-
# `:close`.
|
|
451
|
-
def send_raw(data, type = :text)
|
|
452
|
-
send(data, type)
|
|
213
|
+
def send_raw(data, _type = :text)
|
|
214
|
+
send(data)
|
|
453
215
|
end
|
|
454
216
|
|
|
455
217
|
private
|
|
456
218
|
|
|
457
219
|
def setup_heartbeats(interval)
|
|
458
|
-
# Make sure to reset ACK handling, so we don't keep reconnecting
|
|
459
220
|
@last_heartbeat_acked = true
|
|
460
|
-
|
|
461
|
-
# We don't want to have redundant heartbeat threads, so if one already exists, don't start a new one
|
|
462
|
-
return if @heartbeat_thread
|
|
221
|
+
return if @heartbeat_task
|
|
463
222
|
|
|
464
223
|
@heartbeat_interval = interval
|
|
465
|
-
@
|
|
466
|
-
Thread.current[:onyxcord_name] = 'heartbeat'
|
|
224
|
+
@heartbeat_task = @reactor_task&.async do
|
|
467
225
|
loop do
|
|
468
|
-
# Send a heartbeat if heartbeats are active and either no session exists yet, or an existing session is
|
|
469
|
-
# suspended (e.g. after op7)
|
|
470
226
|
if (@session && !@session.suspended?) || !@session
|
|
471
227
|
sleep @heartbeat_interval
|
|
472
|
-
|
|
473
|
-
if @handshaked && !@closed
|
|
228
|
+
if !@closed && @connection
|
|
474
229
|
@bot.raise_heartbeat_event
|
|
475
230
|
heartbeat
|
|
476
231
|
else
|
|
477
|
-
LOGGER.debug('Tried to
|
|
232
|
+
LOGGER.debug('Tried to heartbeat without connection — skipping.')
|
|
478
233
|
end
|
|
479
234
|
else
|
|
480
235
|
sleep 1
|
|
481
236
|
end
|
|
482
237
|
rescue StandardError => e
|
|
483
|
-
LOGGER.error('
|
|
238
|
+
LOGGER.error('Error while heartbeating!')
|
|
484
239
|
LOGGER.log_exception(e)
|
|
485
240
|
end
|
|
486
241
|
end
|
|
487
242
|
end
|
|
488
243
|
|
|
489
244
|
def connect_loop
|
|
490
|
-
# Initialize falloff so we wait for more time before reconnecting each time
|
|
491
245
|
@falloff = 1.0
|
|
492
|
-
|
|
493
246
|
@should_reconnect = true
|
|
247
|
+
|
|
494
248
|
loop do
|
|
495
249
|
connect
|
|
496
|
-
|
|
497
250
|
break unless @should_reconnect
|
|
498
251
|
|
|
499
252
|
if @instant_reconnect
|
|
500
|
-
LOGGER.info('Instant reconnection
|
|
253
|
+
LOGGER.info('Instant reconnection — reconnecting now')
|
|
501
254
|
@instant_reconnect = false
|
|
502
255
|
else
|
|
503
256
|
wait_for_reconnect
|
|
504
257
|
end
|
|
505
|
-
|
|
506
|
-
# Restart the loop, i.e. reconnect
|
|
507
258
|
end
|
|
508
259
|
end
|
|
509
260
|
|
|
510
|
-
# Separate method to wait an ever-increasing amount of time before reconnecting after being disconnected in an
|
|
511
|
-
# unexpected way
|
|
512
261
|
def wait_for_reconnect
|
|
513
|
-
|
|
514
|
-
LOGGER.debug("Attempting to reconnect in #{@falloff} seconds.")
|
|
262
|
+
LOGGER.debug("Reconnecting in #{@falloff} seconds.")
|
|
515
263
|
sleep @falloff
|
|
516
|
-
|
|
517
|
-
# Calculate new falloff
|
|
518
264
|
@falloff *= 1.5
|
|
519
|
-
@falloff = 115 + (rand * 10) if @falloff > 120
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
# Create and connect a socket using a URI
|
|
523
|
-
def obtain_socket(uri)
|
|
524
|
-
socket = TCPSocket.new(uri.host, uri.port || socket_port(uri))
|
|
525
|
-
|
|
526
|
-
if secure_uri?(uri)
|
|
527
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
|
528
|
-
|
|
529
|
-
if ENV['ONYXCORD_SSL_VERIFY_NONE']
|
|
530
|
-
ctx.ssl_version = 'SSLv23'
|
|
531
|
-
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE # use VERIFY_PEER for verification
|
|
532
|
-
|
|
533
|
-
cert_store = OpenSSL::X509::Store.new
|
|
534
|
-
cert_store.set_default_paths
|
|
535
|
-
ctx.cert_store = cert_store
|
|
536
|
-
else
|
|
537
|
-
ctx.set_params ssl_version: :TLSv1_2 # rubocop:disable Naming/VariableNumber
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
|
541
|
-
socket.connect
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
socket
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
# Whether the URI is secure (connection should be encrypted)
|
|
548
|
-
def secure_uri?(uri)
|
|
549
|
-
%w[https wss].include? uri.scheme
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
# The port we should connect to, if the URI doesn't have one set.
|
|
553
|
-
def socket_port(uri)
|
|
554
|
-
secure_uri?(uri) ? 443 : 80
|
|
265
|
+
@falloff = 115 + (rand * 10) if @falloff > 120
|
|
555
266
|
end
|
|
556
267
|
|
|
557
268
|
def find_gateway
|
|
@@ -561,9 +272,7 @@ module OnyxCord
|
|
|
561
272
|
|
|
562
273
|
def process_gateway
|
|
563
274
|
raw_url = @session&.resume_gateway_url || find_gateway
|
|
564
|
-
|
|
565
|
-
# Append a slash in case it's not there (I'm not sure how well WSCS handles it otherwise)
|
|
566
|
-
raw_url += '/' unless raw_url.end_with? '/'
|
|
275
|
+
raw_url += '/' unless raw_url.end_with?('/')
|
|
567
276
|
|
|
568
277
|
query = if @compress_mode == :stream
|
|
569
278
|
"?encoding=json&v=#{GATEWAY_VERSION}&compress=zlib-stream"
|
|
@@ -577,104 +286,37 @@ module OnyxCord
|
|
|
577
286
|
def connect
|
|
578
287
|
LOGGER.debug('Connecting')
|
|
579
288
|
|
|
580
|
-
# Get the URI we should connect to
|
|
581
289
|
url = process_gateway
|
|
582
290
|
LOGGER.debug("Gateway URL: #{url}")
|
|
583
291
|
|
|
584
|
-
# Parse it
|
|
585
|
-
gateway_uri = URI.parse(url)
|
|
586
|
-
|
|
587
|
-
# Zlib context for this gateway connection
|
|
588
292
|
@zlib_reader = Zlib::Inflate.new
|
|
293
|
+
@pipe_broken = false
|
|
294
|
+
@closed = false
|
|
589
295
|
|
|
590
|
-
|
|
591
|
-
@socket = obtain_socket(gateway_uri)
|
|
592
|
-
LOGGER.debug('Obtained socket')
|
|
296
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
593
297
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
@pipe_broken = false # Whether we've received an EPIPE at any time
|
|
598
|
-
@closed = false # Whether the websocket is currently closed
|
|
298
|
+
Async::WebSocket::Client.connect(endpoint) do |connection|
|
|
299
|
+
@connection = connection
|
|
300
|
+
LOGGER.debug('WebSocket connected')
|
|
599
301
|
|
|
600
|
-
|
|
601
|
-
websocket_loop
|
|
602
|
-
rescue StandardError => e
|
|
603
|
-
LOGGER.error('An error occurred while connecting to the websocket!')
|
|
604
|
-
LOGGER.log_exception(e)
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
def websocket_loop
|
|
608
|
-
# Send the handshake data that we have so far
|
|
609
|
-
@socket.write(@handshake.to_s)
|
|
302
|
+
handle_open
|
|
610
303
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
until @closed
|
|
615
|
-
begin
|
|
616
|
-
unless @socket
|
|
617
|
-
LOGGER.warn('Socket is nil in websocket_loop! Reconnecting')
|
|
618
|
-
handle_internal_close('Socket is nil in websocket_loop')
|
|
619
|
-
next
|
|
620
|
-
end
|
|
621
|
-
|
|
622
|
-
# Get some data from the socket
|
|
623
|
-
begin
|
|
624
|
-
recv_data = @socket.readpartial(4096)
|
|
625
|
-
rescue EOFError
|
|
626
|
-
@pipe_broken = true
|
|
627
|
-
handle_internal_close('Socket EOF in websocket_loop')
|
|
628
|
-
next
|
|
629
|
-
end
|
|
630
|
-
|
|
631
|
-
# Check if we actually got data
|
|
632
|
-
unless recv_data
|
|
633
|
-
# If we didn't, wait
|
|
634
|
-
sleep 1
|
|
635
|
-
next
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
# Check whether the handshake has finished yet
|
|
639
|
-
if @handshaked
|
|
640
|
-
# If it hasn't, add the received data to the current frame
|
|
641
|
-
frame << recv_data
|
|
642
|
-
|
|
643
|
-
# Try to parse a message from the frame
|
|
644
|
-
msg = frame.next
|
|
645
|
-
while msg
|
|
646
|
-
# Check whether the message is a close frame, and if it is, handle accordingly
|
|
647
|
-
if msg.respond_to?(:code) && msg.code
|
|
648
|
-
handle_internal_close(msg)
|
|
649
|
-
break
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
# If there is one, handle it and try again
|
|
653
|
-
handle_message(msg.data)
|
|
654
|
-
msg = frame.next
|
|
655
|
-
end
|
|
656
|
-
else
|
|
657
|
-
# If the handshake hasn't finished, handle it
|
|
658
|
-
handle_handshake_data(recv_data)
|
|
659
|
-
end
|
|
660
|
-
rescue StandardError => e
|
|
661
|
-
handle_error(e)
|
|
304
|
+
while (message = connection.read)
|
|
305
|
+
handle_message(message.to_str)
|
|
662
306
|
end
|
|
663
307
|
end
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
@handshaked = true
|
|
671
|
-
handle_open
|
|
308
|
+
rescue StandardError => e
|
|
309
|
+
LOGGER.error('Error connecting to gateway!')
|
|
310
|
+
LOGGER.log_exception(e)
|
|
311
|
+
ensure
|
|
312
|
+
@closed = true
|
|
313
|
+
@connection = nil
|
|
672
314
|
end
|
|
673
315
|
|
|
674
316
|
def handle_open; end
|
|
675
317
|
|
|
676
318
|
def handle_error(e)
|
|
677
|
-
LOGGER.error('
|
|
319
|
+
LOGGER.error('Error in gateway loop!')
|
|
678
320
|
LOGGER.log_exception(e)
|
|
679
321
|
end
|
|
680
322
|
|
|
@@ -684,32 +326,19 @@ module OnyxCord
|
|
|
684
326
|
def handle_message(msg)
|
|
685
327
|
case @compress_mode
|
|
686
328
|
when :large
|
|
687
|
-
if msg.byteslice(0) == 'x'
|
|
688
|
-
# The message is compressed, inflate it
|
|
689
|
-
msg = Zlib::Inflate.inflate(msg)
|
|
690
|
-
end
|
|
329
|
+
msg = Zlib::Inflate.inflate(msg) if msg.byteslice(0) == 'x'
|
|
691
330
|
when :stream
|
|
692
|
-
# Write deflated string to buffer
|
|
693
331
|
@zlib_reader << msg
|
|
694
|
-
|
|
695
|
-
# Check if message ends in `ZLIB_SUFFIX`
|
|
696
332
|
return if msg.bytesize < 4 || msg.byteslice(-4, 4) != ZLIB_SUFFIX
|
|
697
333
|
|
|
698
|
-
# Inflate the deflated buffer
|
|
699
334
|
msg = @zlib_reader.inflate('')
|
|
700
335
|
end
|
|
701
336
|
|
|
702
|
-
# Parse packet
|
|
703
337
|
packet = JSON.parse(msg)
|
|
704
338
|
op = packet['op'].to_i
|
|
705
339
|
|
|
706
340
|
LOGGER.in(packet)
|
|
707
341
|
|
|
708
|
-
# If the packet has a sequence defined (all dispatch packets have one), make sure to update that in the
|
|
709
|
-
# session so it will be acknowledged next heartbeat.
|
|
710
|
-
# Only do this, of course, if a session has been created already; for a READY dispatch (which has s=0 set but is
|
|
711
|
-
# the packet that starts the session in the first place) we need not do any handling since initialising the
|
|
712
|
-
# session will set it to 0 by default.
|
|
713
342
|
@session.sequence = packet['s'] if packet['s'] && @session
|
|
714
343
|
|
|
715
344
|
case op
|
|
@@ -726,7 +355,7 @@ module OnyxCord
|
|
|
726
355
|
when Opcodes::HEARTBEAT
|
|
727
356
|
handle_heartbeat(packet)
|
|
728
357
|
else
|
|
729
|
-
LOGGER.warn("
|
|
358
|
+
LOGGER.warn("Invalid opcode #{op}: #{msg}")
|
|
730
359
|
end
|
|
731
360
|
end
|
|
732
361
|
|
|
@@ -737,15 +366,11 @@ module OnyxCord
|
|
|
737
366
|
|
|
738
367
|
case type
|
|
739
368
|
when :READY
|
|
740
|
-
LOGGER.info("Discord
|
|
741
|
-
|
|
369
|
+
LOGGER.info("Discord gateway v#{data['v']}, requested: #{GATEWAY_VERSION}")
|
|
742
370
|
@session = Session.new(data['session_id'], data['resume_gateway_url'])
|
|
743
371
|
@session.sequence = 0
|
|
744
372
|
@bot.__send__(:notify_ready) if @intents && @intents.nobits?(INTENTS[:servers])
|
|
745
373
|
when :RESUMED
|
|
746
|
-
# The RESUMED event is received after a successful op 6 (resume). It does nothing except tell the bot the
|
|
747
|
-
# connection is initiated (like READY would). Starting with v5, it doesn't set a new heartbeat interval anymore
|
|
748
|
-
# since that is handled by op 10 (HELLO).
|
|
749
374
|
LOGGER.info 'Resumed'
|
|
750
375
|
return
|
|
751
376
|
end
|
|
@@ -755,20 +380,18 @@ module OnyxCord
|
|
|
755
380
|
|
|
756
381
|
# Op 1
|
|
757
382
|
def handle_heartbeat(packet)
|
|
758
|
-
# If we receive a heartbeat, we have to resend one with the same sequence
|
|
759
383
|
send_heartbeat(packet['s'])
|
|
760
384
|
end
|
|
761
385
|
|
|
762
386
|
# Op 7
|
|
763
387
|
def handle_reconnect
|
|
764
|
-
LOGGER.debug('Received op 7, reconnecting
|
|
388
|
+
LOGGER.debug('Received op 7, reconnecting')
|
|
765
389
|
reconnect
|
|
766
390
|
end
|
|
767
391
|
|
|
768
392
|
# Op 9
|
|
769
393
|
def handle_invalidate_session(packet)
|
|
770
|
-
LOGGER.debug('Received op 9, invalidating session
|
|
771
|
-
|
|
394
|
+
LOGGER.debug('Received op 9, invalidating session')
|
|
772
395
|
if @session
|
|
773
396
|
if packet['d'] == true
|
|
774
397
|
reconnect
|
|
@@ -776,28 +399,21 @@ module OnyxCord
|
|
|
776
399
|
@session.invalidate
|
|
777
400
|
end
|
|
778
401
|
else
|
|
779
|
-
LOGGER.warn('
|
|
402
|
+
LOGGER.warn('Op 9 without session!')
|
|
780
403
|
end
|
|
781
|
-
|
|
782
404
|
identify
|
|
783
405
|
end
|
|
784
406
|
|
|
785
407
|
# Op 10
|
|
786
408
|
def handle_hello(packet)
|
|
787
409
|
LOGGER.debug('Hello!')
|
|
788
|
-
|
|
789
|
-
# The heartbeat interval is given in ms, so divide it by 1000 to get seconds
|
|
790
410
|
interval = packet['d']['heartbeat_interval'].to_f / 1000.0
|
|
791
411
|
setup_heartbeats(interval)
|
|
792
|
-
|
|
793
412
|
LOGGER.debug("Trace: #{packet['d']['_trace']}")
|
|
794
413
|
LOGGER.debug("Session: #{@session.inspect}")
|
|
795
414
|
|
|
796
415
|
if @session&.should_resume?
|
|
797
|
-
# Make sure we're sending heartbeats again
|
|
798
416
|
@session.resume
|
|
799
|
-
|
|
800
|
-
# Send the actual resume packet to get the missing events
|
|
801
417
|
resume
|
|
802
418
|
else
|
|
803
419
|
identify
|
|
@@ -806,11 +422,10 @@ module OnyxCord
|
|
|
806
422
|
|
|
807
423
|
# Op 11
|
|
808
424
|
def handle_heartbeat_ack(packet)
|
|
809
|
-
LOGGER.debug("
|
|
425
|
+
LOGGER.debug("Heartbeat ACK: #{packet.inspect}")
|
|
810
426
|
@last_heartbeat_acked = true if @check_heartbeat_acks
|
|
811
427
|
end
|
|
812
428
|
|
|
813
|
-
# Called when the websocket has been disconnected in some way - say due to a pipe error while sending
|
|
814
429
|
def handle_internal_close(e)
|
|
815
430
|
close
|
|
816
431
|
handle_close(e)
|
|
@@ -819,71 +434,45 @@ module OnyxCord
|
|
|
819
434
|
def handle_close(e)
|
|
820
435
|
@bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
|
|
821
436
|
|
|
822
|
-
if e.respond_to?
|
|
823
|
-
|
|
824
|
-
LOGGER.error('
|
|
825
|
-
LOGGER.error("Code: #{e.code}")
|
|
826
|
-
LOGGER.error("Message: #{e.data}")
|
|
827
|
-
|
|
828
|
-
if e.code == 4014
|
|
829
|
-
LOGGER.error(<<~ERROR)
|
|
830
|
-
You attempted to identify with privileged intents that your bot is not authorized to use
|
|
831
|
-
Please enable the privileged intents on the bot page of your application on the discord developer page.
|
|
832
|
-
Read more here https://discord.com/developers/docs/topics/gateway#privileged-intents
|
|
833
|
-
ERROR
|
|
834
|
-
end
|
|
835
|
-
|
|
437
|
+
if e.respond_to?(:code)
|
|
438
|
+
LOGGER.error("WebSocket close frame! Code: #{e.code}")
|
|
439
|
+
LOGGER.error('Privileged intents not authorized. Enable them in the Discord developer portal.') if e.code == 4014
|
|
836
440
|
@should_reconnect = false if FATAL_CLOSE_CODES.include?(e.code)
|
|
837
|
-
elsif e.is_a?
|
|
838
|
-
|
|
839
|
-
LOGGER.error('The websocket connection has closed due to an error!')
|
|
441
|
+
elsif e.is_a?(Exception)
|
|
442
|
+
LOGGER.error('WebSocket closed due to error!')
|
|
840
443
|
LOGGER.log_exception(e)
|
|
841
444
|
else
|
|
842
|
-
LOGGER.error("
|
|
445
|
+
LOGGER.error("WebSocket closed: #{e&.inspect || '(no info)'}")
|
|
843
446
|
end
|
|
844
447
|
end
|
|
845
448
|
|
|
846
|
-
def send(data,
|
|
449
|
+
def send(data, _type = :text, _code = nil)
|
|
847
450
|
LOGGER.out(data)
|
|
848
451
|
|
|
849
|
-
unless @
|
|
850
|
-
# If we're not handshaked or closed, it means there's no connection to send anything to
|
|
851
|
-
raise 'Tried to send something to the websocket while not being connected!'
|
|
852
|
-
end
|
|
853
|
-
|
|
854
|
-
@send_limiter.wait if %i[text binary].include?(type)
|
|
452
|
+
raise 'Tried to send to websocket while not connected!' unless @connection && !@closed
|
|
855
453
|
|
|
856
|
-
|
|
857
|
-
frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @handshake.version, code: code)
|
|
454
|
+
@send_limiter.wait
|
|
858
455
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
@pipe_broken = true
|
|
865
|
-
handle_internal_close(e)
|
|
866
|
-
end
|
|
456
|
+
@connection.write(Protocol::WebSocket::TextMessage.generate(data))
|
|
457
|
+
@connection.flush
|
|
458
|
+
rescue StandardError => e
|
|
459
|
+
@pipe_broken = true
|
|
460
|
+
handle_internal_close(e)
|
|
867
461
|
end
|
|
868
462
|
|
|
869
|
-
def close(
|
|
870
|
-
# If we're already closed, there's no need to do anything - return
|
|
463
|
+
def close(_code = 1000)
|
|
871
464
|
return if @closed
|
|
872
465
|
|
|
873
|
-
# Suspend the session so we don't send heartbeats
|
|
874
466
|
@session&.suspend
|
|
875
|
-
|
|
876
|
-
# Send a close frame (if we can)
|
|
877
|
-
send nil, :close, code unless @pipe_broken
|
|
878
|
-
|
|
879
|
-
# We're officially closed, notify the main loop.
|
|
880
467
|
@closed = true
|
|
881
468
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
469
|
+
begin
|
|
470
|
+
@connection&.close
|
|
471
|
+
rescue StandardError
|
|
472
|
+
# Ignore close errors
|
|
473
|
+
end
|
|
885
474
|
|
|
886
|
-
|
|
475
|
+
@connection = nil
|
|
887
476
|
handle_close(nil)
|
|
888
477
|
end
|
|
889
478
|
end
|