discordrb 3.1.1 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of discordrb might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.circleci/config.yml +126 -0
- data/.codeclimate.yml +16 -0
- data/.github/CONTRIBUTING.md +13 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
- data/.github/pull_request_template.md +37 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +39 -33
- data/.travis.yml +27 -2
- data/.yardopts +1 -1
- data/CHANGELOG.md +808 -208
- data/Gemfile +4 -1
- data/LICENSE.txt +1 -1
- data/README.md +108 -53
- data/Rakefile +14 -1
- data/bin/console +1 -0
- data/bin/travis_build_docs.sh +17 -0
- data/discordrb-webhooks.gemspec +26 -0
- data/discordrb.gemspec +24 -15
- data/lib/discordrb.rb +75 -2
- data/lib/discordrb/allowed_mentions.rb +36 -0
- data/lib/discordrb/api.rb +126 -27
- data/lib/discordrb/api/channel.rb +165 -43
- data/lib/discordrb/api/invite.rb +10 -7
- data/lib/discordrb/api/server.rb +240 -61
- data/lib/discordrb/api/user.rb +26 -24
- data/lib/discordrb/api/webhook.rb +83 -0
- data/lib/discordrb/await.rb +1 -2
- data/lib/discordrb/bot.rb +417 -149
- data/lib/discordrb/cache.rb +42 -10
- data/lib/discordrb/colour_rgb.rb +43 -0
- data/lib/discordrb/commands/command_bot.rb +186 -31
- data/lib/discordrb/commands/container.rb +30 -16
- data/lib/discordrb/commands/parser.rb +102 -47
- data/lib/discordrb/commands/rate_limiter.rb +18 -17
- data/lib/discordrb/container.rb +245 -41
- data/lib/discordrb/data.rb +27 -2511
- data/lib/discordrb/data/activity.rb +264 -0
- data/lib/discordrb/data/application.rb +50 -0
- data/lib/discordrb/data/attachment.rb +56 -0
- data/lib/discordrb/data/audit_logs.rb +345 -0
- data/lib/discordrb/data/channel.rb +849 -0
- data/lib/discordrb/data/embed.rb +251 -0
- data/lib/discordrb/data/emoji.rb +82 -0
- data/lib/discordrb/data/integration.rb +83 -0
- data/lib/discordrb/data/invite.rb +137 -0
- data/lib/discordrb/data/member.rb +297 -0
- data/lib/discordrb/data/message.rb +334 -0
- data/lib/discordrb/data/overwrite.rb +102 -0
- data/lib/discordrb/data/profile.rb +91 -0
- data/lib/discordrb/data/reaction.rb +33 -0
- data/lib/discordrb/data/recipient.rb +34 -0
- data/lib/discordrb/data/role.rb +191 -0
- data/lib/discordrb/data/server.rb +1002 -0
- data/lib/discordrb/data/user.rb +204 -0
- data/lib/discordrb/data/voice_region.rb +45 -0
- data/lib/discordrb/data/voice_state.rb +41 -0
- data/lib/discordrb/data/webhook.rb +145 -0
- data/lib/discordrb/errors.rb +36 -2
- data/lib/discordrb/events/bans.rb +7 -5
- data/lib/discordrb/events/channels.rb +2 -0
- data/lib/discordrb/events/generic.rb +19 -3
- data/lib/discordrb/events/guilds.rb +129 -6
- data/lib/discordrb/events/invites.rb +125 -0
- data/lib/discordrb/events/members.rb +6 -2
- data/lib/discordrb/events/message.rb +86 -36
- data/lib/discordrb/events/presence.rb +23 -16
- data/lib/discordrb/events/raw.rb +47 -0
- data/lib/discordrb/events/reactions.rb +159 -0
- data/lib/discordrb/events/roles.rb +7 -6
- data/lib/discordrb/events/typing.rb +9 -5
- data/lib/discordrb/events/voice_server_update.rb +47 -0
- data/lib/discordrb/events/voice_state_update.rb +29 -9
- data/lib/discordrb/events/webhooks.rb +64 -0
- data/lib/discordrb/gateway.rb +219 -88
- data/lib/discordrb/id_object.rb +39 -0
- data/lib/discordrb/light.rb +1 -1
- data/lib/discordrb/light/integrations.rb +1 -1
- data/lib/discordrb/light/light_bot.rb +1 -1
- data/lib/discordrb/logger.rb +12 -11
- data/lib/discordrb/paginator.rb +57 -0
- data/lib/discordrb/permissions.rb +148 -14
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/encoder.rb +14 -15
- data/lib/discordrb/voice/network.rb +86 -45
- data/lib/discordrb/voice/sodium.rb +96 -0
- data/lib/discordrb/voice/voice_bot.rb +52 -40
- data/lib/discordrb/webhooks.rb +12 -0
- data/lib/discordrb/websocket.rb +2 -2
- metadata +137 -34
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'discordrb/events/generic'
|
4
|
+
require 'discordrb/data'
|
5
|
+
|
6
|
+
module Discordrb::Events
|
7
|
+
# Event raised when a webhook is updated
|
8
|
+
class WebhookUpdateEvent < Event
|
9
|
+
# @return [Server] the server where the webhook updated
|
10
|
+
attr_reader :server
|
11
|
+
|
12
|
+
# @return [Channel] the channel the webhook is associated to
|
13
|
+
attr_reader :channel
|
14
|
+
|
15
|
+
def initialize(data, bot)
|
16
|
+
@bot = bot
|
17
|
+
|
18
|
+
@server = bot.server(data['guild_id'].to_i)
|
19
|
+
@channel = bot.channel(data['channel_id'].to_i)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Event handler for {WebhookUpdateEvent}
|
24
|
+
class WebhookUpdateEventHandler < EventHandler
|
25
|
+
def matches?(event)
|
26
|
+
# Check for the proper event type
|
27
|
+
return false unless event.is_a? WebhookUpdateEvent
|
28
|
+
|
29
|
+
[
|
30
|
+
matches_all(@attributes[:server], event.server) do |a, e|
|
31
|
+
a == case a
|
32
|
+
when String
|
33
|
+
e.name
|
34
|
+
when Integer
|
35
|
+
e.id
|
36
|
+
else
|
37
|
+
e
|
38
|
+
end
|
39
|
+
end,
|
40
|
+
matches_all(@attributes[:channel], event.channel) do |a, e|
|
41
|
+
case a
|
42
|
+
when String
|
43
|
+
# Make sure to remove the "#" from channel names in case it was specified
|
44
|
+
a.delete('#') == e.name
|
45
|
+
when Integer
|
46
|
+
a == e.id
|
47
|
+
else
|
48
|
+
a == e
|
49
|
+
end
|
50
|
+
end,
|
51
|
+
matches_all(@attributes[:webhook], event) do |a, e|
|
52
|
+
a == case a
|
53
|
+
when String
|
54
|
+
e.name
|
55
|
+
when Integer
|
56
|
+
e.id
|
57
|
+
else
|
58
|
+
e
|
59
|
+
end
|
60
|
+
end
|
61
|
+
].reduce(true, &:&)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/discordrb/gateway.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This file uses code from Websocket::Client::Simple, licensed under the following license:
|
2
4
|
#
|
3
5
|
# Copyright (c) 2013-2014 Sho Hashimoto
|
@@ -23,8 +25,6 @@
|
|
23
25
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
26
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
27
|
|
26
|
-
require 'thread'
|
27
|
-
|
28
28
|
module Discordrb
|
29
29
|
# Gateway packet opcodes
|
30
30
|
module Opcodes
|
@@ -72,8 +72,9 @@ module Discordrb
|
|
72
72
|
# members when it receives them. (Sending this is never necessary for a gateway client to behave correctly)
|
73
73
|
REQUEST_MEMBERS = 8
|
74
74
|
|
75
|
-
# **Received**:
|
76
|
-
#
|
75
|
+
# **Received**: Sent by Discord when the session becomes invalid for any reason. This may include improperly
|
76
|
+
# resuming existing sessions, attempting to start sessions with invalid data, or something else entirely. The client
|
77
|
+
# should handle this by simply starting a new session.
|
77
78
|
INVALIDATE_SESSION = 9
|
78
79
|
|
79
80
|
# **Received**: Sent immediately for any opened connection; tells the client to start heartbeating early on, so the
|
@@ -93,12 +94,13 @@ module Discordrb
|
|
93
94
|
attr_accessor :sequence
|
94
95
|
|
95
96
|
def initialize(session_id)
|
96
|
-
@
|
97
|
+
@session_id = session_id
|
97
98
|
@sequence = 0
|
98
99
|
@suspended = false
|
99
100
|
@invalid = false
|
100
101
|
end
|
101
102
|
|
103
|
+
# Flags this session as suspended, so we know not to try and send heartbeats, etc. to the gateway until we've reconnected
|
102
104
|
def suspend
|
103
105
|
@suspended = true
|
104
106
|
end
|
@@ -107,6 +109,12 @@ module Discordrb
|
|
107
109
|
@suspended
|
108
110
|
end
|
109
111
|
|
112
|
+
# Flags this session as no longer being suspended, so we can resume
|
113
|
+
def resume
|
114
|
+
@suspended = false
|
115
|
+
end
|
116
|
+
|
117
|
+
# Flags this session as being invalid
|
110
118
|
def invalidate
|
111
119
|
@invalid = true
|
112
120
|
end
|
@@ -128,14 +136,26 @@ module Discordrb
|
|
128
136
|
# The version of the gateway that's supposed to be used.
|
129
137
|
GATEWAY_VERSION = 6
|
130
138
|
|
131
|
-
|
139
|
+
# Heartbeat ACKs are Discord's way of verifying on the client side whether the connection is still alive. If this is
|
140
|
+
# set to true (default value) the gateway client will use that functionality to detect zombie connections and
|
141
|
+
# reconnect in such a case; however it may lead to instability if there's some problem with the ACKs. If this occurs
|
142
|
+
# it can simply be set to false.
|
143
|
+
# @return [true, false] whether or not this gateway should check for heartbeat ACKs.
|
144
|
+
attr_accessor :check_heartbeat_acks
|
145
|
+
|
146
|
+
def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = nil)
|
132
147
|
@token = token
|
133
148
|
@bot = bot
|
134
149
|
|
135
|
-
@
|
150
|
+
@shard_key = shard_key
|
136
151
|
|
137
152
|
# Whether the connection to the gateway has succeeded yet
|
138
153
|
@ws_success = false
|
154
|
+
|
155
|
+
@check_heartbeat_acks = true
|
156
|
+
|
157
|
+
@compress_mode = compress_mode
|
158
|
+
@intents = intents
|
139
159
|
end
|
140
160
|
|
141
161
|
# Connect to the gateway server in a separate thread
|
@@ -147,27 +167,41 @@ module Discordrb
|
|
147
167
|
end
|
148
168
|
|
149
169
|
LOGGER.debug('WS thread created! Now waiting for confirmation that everything worked')
|
150
|
-
|
151
|
-
|
170
|
+
loop do
|
171
|
+
sleep(0.5)
|
172
|
+
|
173
|
+
if @ws_success
|
174
|
+
LOGGER.debug('Confirmation received! Exiting run.')
|
175
|
+
break
|
176
|
+
end
|
177
|
+
|
178
|
+
if @should_reconnect == false
|
179
|
+
LOGGER.debug('Reconnection flag was unset. Exiting run.')
|
180
|
+
break
|
181
|
+
end
|
182
|
+
end
|
152
183
|
end
|
153
184
|
|
154
|
-
# Prevents all further execution until the websocket thread stops (e.
|
185
|
+
# Prevents all further execution until the websocket thread stops (e.g. through a closed connection).
|
155
186
|
def sync
|
156
187
|
@ws_thread.join
|
157
188
|
end
|
158
189
|
|
159
190
|
# Whether the WebSocket connection to the gateway is currently open
|
160
191
|
def open?
|
161
|
-
@handshake
|
192
|
+
@handshake&.finished? && !@closed
|
162
193
|
end
|
163
194
|
|
164
195
|
# Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
|
165
196
|
# Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
|
166
197
|
#
|
167
198
|
# If this method doesn't work or you're looking for something more drastic, use {#kill} instead.
|
168
|
-
def stop
|
199
|
+
def stop
|
169
200
|
@should_reconnect = false
|
170
|
-
close
|
201
|
+
close
|
202
|
+
|
203
|
+
# Return nil so command bots don't send a message
|
204
|
+
nil
|
171
205
|
end
|
172
206
|
|
173
207
|
# Kills the websocket thread, stopping all connections to Discord.
|
@@ -213,6 +247,21 @@ module Discordrb
|
|
213
247
|
# before it), or if none have been received yet, with 0.
|
214
248
|
# @see #send_heartbeat
|
215
249
|
def heartbeat
|
250
|
+
if check_heartbeat_acks
|
251
|
+
unless @last_heartbeat_acked
|
252
|
+
# We're in a bad situation - apparently the last heartbeat wasn't ACK'd, which means the connection is likely
|
253
|
+
# a zombie. Reconnect
|
254
|
+
LOGGER.warn('Last heartbeat was not acked, so this is a zombie connection! Reconnecting')
|
255
|
+
|
256
|
+
# We can't send anything on zombie connections
|
257
|
+
@pipe_broken = true
|
258
|
+
reconnect
|
259
|
+
return
|
260
|
+
end
|
261
|
+
|
262
|
+
@last_heartbeat_acked = false
|
263
|
+
end
|
264
|
+
|
216
265
|
send_heartbeat(@session ? @session.sequence : 0)
|
217
266
|
end
|
218
267
|
|
@@ -226,13 +275,14 @@ module Discordrb
|
|
226
275
|
# Identifies to Discord with the default parameters.
|
227
276
|
# @see #send_identify
|
228
277
|
def identify
|
278
|
+
compress = @compress_mode == :large
|
229
279
|
send_identify(@token, {
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
},
|
280
|
+
'$os': RUBY_PLATFORM,
|
281
|
+
'$browser': 'discordrb',
|
282
|
+
'$device': 'discordrb',
|
283
|
+
'$referrer': '',
|
284
|
+
'$referring_domain': ''
|
285
|
+
}, compress, 100, @shard_key)
|
236
286
|
end
|
237
287
|
|
238
288
|
# Sends an identify packet (op 2). This starts a new session on the current connection and tells Discord who we are.
|
@@ -251,7 +301,9 @@ module Discordrb
|
|
251
301
|
# @param compress [true, false] Whether certain large packets should be compressed using zlib.
|
252
302
|
# @param large_threshold [Integer] The member threshold after which a server counts as large and will have to have
|
253
303
|
# its member list chunked.
|
254
|
-
|
304
|
+
# @param shard_key [Array(Integer, Integer), nil] The shard key to use for sharding, represented as
|
305
|
+
# [shard_id, num_shards], or nil if the bot should not be sharded.
|
306
|
+
def send_identify(token, properties, compress, large_threshold, shard_key = nil)
|
255
307
|
data = {
|
256
308
|
# Don't send a v anymore as it's entirely determined by the URL now
|
257
309
|
token: token,
|
@@ -259,13 +311,17 @@ module Discordrb
|
|
259
311
|
compress: compress,
|
260
312
|
large_threshold: large_threshold
|
261
313
|
}
|
314
|
+
data[:intents] = @intents unless @intents.nil?
|
315
|
+
|
316
|
+
# Don't include the shard key at all if it is nil as Discord checks for its mere existence
|
317
|
+
data[:shard] = shard_key if shard_key
|
262
318
|
|
263
319
|
send_packet(Opcodes::IDENTIFY, data)
|
264
320
|
end
|
265
321
|
|
266
322
|
# Sends a status update packet (op 3). This sets the bot user's status (online/idle/...) and game playing/streaming.
|
267
323
|
# @param status [String] The status that should be set (`online`, `idle`, `dnd`, `invisible`).
|
268
|
-
# @param since [Integer] The
|
324
|
+
# @param since [Integer] The Unix timestamp in milliseconds when the status was set. Should only be provided when
|
269
325
|
# `afk` is true.
|
270
326
|
# @param game [Hash<Symbol => Object>, nil] `nil` if no game should be played, or a hash of `:game => "name"` if a
|
271
327
|
# game should be played. The hash can also contain additional attributes for streaming statuses.
|
@@ -305,6 +361,17 @@ module Discordrb
|
|
305
361
|
send_resume(@token, @session.session_id, @session.sequence)
|
306
362
|
end
|
307
363
|
|
364
|
+
# Reconnects the gateway connection in a controlled manner.
|
365
|
+
# @param attempt_resume [true, false] Whether a resume should be attempted after the reconnection.
|
366
|
+
def reconnect(attempt_resume = true)
|
367
|
+
@session.suspend if @session && attempt_resume
|
368
|
+
|
369
|
+
@instant_reconnect = true
|
370
|
+
@should_reconnect = true
|
371
|
+
|
372
|
+
close(4000)
|
373
|
+
end
|
374
|
+
|
308
375
|
# Sends a resume packet (op 6). This replays all events from a previous point specified by its packet sequence. This
|
309
376
|
# will not work if the packet to resume from has already been acknowledged using a heartbeat, or if the session ID
|
310
377
|
# belongs to a now invalid session.
|
@@ -342,9 +409,35 @@ module Discordrb
|
|
342
409
|
send_packet(Opcodes::REQUEST_MEMBERS, data)
|
343
410
|
end
|
344
411
|
|
412
|
+
# Sends a custom packet over the connection. This can be useful to implement future yet unimplemented functionality
|
413
|
+
# or for testing. You probably shouldn't use this unless you know what you're doing.
|
414
|
+
# @param opcode [Integer] The opcode the packet should be sent as. Can be one of {Opcodes} or a custom value if
|
415
|
+
# necessary.
|
416
|
+
# @param packet [Object] Some arbitrary JSON-serializable data that should be sent as the `d` field.
|
417
|
+
def send_packet(opcode, packet)
|
418
|
+
data = {
|
419
|
+
op: opcode,
|
420
|
+
d: packet
|
421
|
+
}
|
422
|
+
|
423
|
+
send(data.to_json)
|
424
|
+
end
|
425
|
+
|
426
|
+
# Sends custom raw data over the connection. Only useful for testing; even if you know what you're doing you
|
427
|
+
# probably want to use {#send_packet} instead.
|
428
|
+
# @param data [String] The data to send.
|
429
|
+
# @param type [Symbol] The type the WebSocket frame should have; either `:text`, `:binary`, `:ping`, `:pong`, or
|
430
|
+
# `:close`.
|
431
|
+
def send_raw(data, type = :text)
|
432
|
+
send(data, type)
|
433
|
+
end
|
434
|
+
|
345
435
|
private
|
346
436
|
|
347
437
|
def setup_heartbeats(interval)
|
438
|
+
# Make sure to reset ACK handling, so we don't keep reconnecting
|
439
|
+
@last_heartbeat_acked = true
|
440
|
+
|
348
441
|
# We don't want to have redundant heartbeat threads, so if one already exists, don't start a new one
|
349
442
|
return if @heartbeat_thread
|
350
443
|
|
@@ -352,20 +445,18 @@ module Discordrb
|
|
352
445
|
@heartbeat_thread = Thread.new do
|
353
446
|
Thread.current[:discordrb_name] = 'heartbeat'
|
354
447
|
loop do
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
sleep 1
|
364
|
-
end
|
365
|
-
rescue => e
|
366
|
-
LOGGER.error('An error occurred while heartbeating!')
|
367
|
-
LOGGER.log_exception(e)
|
448
|
+
# Send a heartbeat if heartbeats are active and either no session exists yet, or an existing session is
|
449
|
+
# suspended (e.g. after op7)
|
450
|
+
if (@session && !@session.suspended?) || !@session
|
451
|
+
sleep @heartbeat_interval
|
452
|
+
@bot.raise_heartbeat_event
|
453
|
+
heartbeat
|
454
|
+
else
|
455
|
+
sleep 1
|
368
456
|
end
|
457
|
+
rescue StandardError => e
|
458
|
+
LOGGER.error('An error occurred while heartbeating!')
|
459
|
+
LOGGER.log_exception(e)
|
369
460
|
end
|
370
461
|
end
|
371
462
|
end
|
@@ -381,14 +472,13 @@ module Discordrb
|
|
381
472
|
break unless @should_reconnect
|
382
473
|
|
383
474
|
if @instant_reconnect
|
384
|
-
|
385
|
-
LOGGER.info('Got an op 7, reconnecting right away')
|
475
|
+
LOGGER.info('Instant reconnection flag was set - reconnecting right away')
|
386
476
|
@instant_reconnect = false
|
387
477
|
else
|
388
478
|
wait_for_reconnect
|
389
479
|
end
|
390
480
|
|
391
|
-
# Restart the loop, i.
|
481
|
+
# Restart the loop, i.e. reconnect
|
392
482
|
end
|
393
483
|
end
|
394
484
|
|
@@ -410,14 +500,19 @@ module Discordrb
|
|
410
500
|
|
411
501
|
if secure_uri?(uri)
|
412
502
|
ctx = OpenSSL::SSL::SSLContext.new
|
413
|
-
ctx.ssl_version = 'SSLv23'
|
414
|
-
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE # use VERIFY_PEER for verification
|
415
503
|
|
416
|
-
|
417
|
-
|
418
|
-
|
504
|
+
if ENV['DISCORDRB_SSL_VERIFY_NONE']
|
505
|
+
ctx.ssl_version = 'SSLv23'
|
506
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE # use VERIFY_PEER for verification
|
507
|
+
|
508
|
+
cert_store = OpenSSL::X509::Store.new
|
509
|
+
cert_store.set_default_paths
|
510
|
+
ctx.cert_store = cert_store
|
511
|
+
else
|
512
|
+
ctx.set_params ssl_version: :TLSv1_2 # rubocop:disable Naming/VariableNumber
|
513
|
+
end
|
419
514
|
|
420
|
-
socket =
|
515
|
+
socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
421
516
|
socket.connect
|
422
517
|
end
|
423
518
|
|
@@ -426,7 +521,7 @@ module Discordrb
|
|
426
521
|
|
427
522
|
# Whether the URI is secure (connection should be encrypted)
|
428
523
|
def secure_uri?(uri)
|
429
|
-
%w
|
524
|
+
%w[https wss].include? uri.scheme
|
430
525
|
end
|
431
526
|
|
432
527
|
# The port we should connect to, if the URI doesn't have one set.
|
@@ -445,8 +540,13 @@ module Discordrb
|
|
445
540
|
# Append a slash in case it's not there (I'm not sure how well WSCS handles it otherwise)
|
446
541
|
raw_url += '/' unless raw_url.end_with? '/'
|
447
542
|
|
448
|
-
|
449
|
-
|
543
|
+
query = if @compress_mode == :stream
|
544
|
+
"?encoding=json&v=#{GATEWAY_VERSION}&compress=zlib-stream"
|
545
|
+
else
|
546
|
+
"?encoding=json&v=#{GATEWAY_VERSION}"
|
547
|
+
end
|
548
|
+
|
549
|
+
raw_url + query
|
450
550
|
end
|
451
551
|
|
452
552
|
def connect
|
@@ -459,6 +559,9 @@ module Discordrb
|
|
459
559
|
# Parse it
|
460
560
|
gateway_uri = URI.parse(url)
|
461
561
|
|
562
|
+
# Zlib context for this gateway connection
|
563
|
+
@zlib_reader = Zlib::Inflate.new
|
564
|
+
|
462
565
|
# Connect to the obtained URI with a socket
|
463
566
|
@socket = obtain_socket(gateway_uri)
|
464
567
|
LOGGER.debug('Obtained socket')
|
@@ -471,7 +574,7 @@ module Discordrb
|
|
471
574
|
|
472
575
|
# We're done! Delegate to the websocket loop
|
473
576
|
websocket_loop
|
474
|
-
rescue => e
|
577
|
+
rescue StandardError => e
|
475
578
|
LOGGER.error('An error occurred while connecting to the websocket!')
|
476
579
|
LOGGER.log_exception(e)
|
477
580
|
end
|
@@ -485,10 +588,20 @@ module Discordrb
|
|
485
588
|
|
486
589
|
until @closed
|
487
590
|
begin
|
488
|
-
|
591
|
+
unless @socket
|
592
|
+
LOGGER.warn('Socket is nil in websocket_loop! Reconnecting')
|
593
|
+
handle_internal_close('Socket is nil in websocket_loop')
|
594
|
+
next
|
595
|
+
end
|
489
596
|
|
490
|
-
# Get some data from the socket
|
491
|
-
|
597
|
+
# Get some data from the socket
|
598
|
+
begin
|
599
|
+
recv_data = @socket.readpartial(4096)
|
600
|
+
rescue EOFError
|
601
|
+
@pipe_broken = true
|
602
|
+
handle_internal_close('Socket EOF in websocket_loop')
|
603
|
+
next
|
604
|
+
end
|
492
605
|
|
493
606
|
# Check if we actually got data
|
494
607
|
unless recv_data
|
@@ -519,7 +632,7 @@ module Discordrb
|
|
519
632
|
# If the handshake hasn't finished, handle it
|
520
633
|
handle_handshake_data(recv_data)
|
521
634
|
end
|
522
|
-
rescue => e
|
635
|
+
rescue StandardError => e
|
523
636
|
handle_error(e)
|
524
637
|
end
|
525
638
|
end
|
@@ -533,18 +646,31 @@ module Discordrb
|
|
533
646
|
handle_open
|
534
647
|
end
|
535
648
|
|
536
|
-
def handle_open
|
537
|
-
end
|
649
|
+
def handle_open; end
|
538
650
|
|
539
651
|
def handle_error(e)
|
540
652
|
LOGGER.error('An error occurred in the main websocket loop!')
|
541
653
|
LOGGER.log_exception(e)
|
542
654
|
end
|
543
655
|
|
656
|
+
ZLIB_SUFFIX = "\x00\x00\xFF\xFF".b.freeze
|
657
|
+
|
544
658
|
def handle_message(msg)
|
545
|
-
|
546
|
-
|
547
|
-
msg
|
659
|
+
case @compress_mode
|
660
|
+
when :large
|
661
|
+
if msg.byteslice(0) == 'x'
|
662
|
+
# The message is compressed, inflate it
|
663
|
+
msg = Zlib::Inflate.inflate(msg)
|
664
|
+
end
|
665
|
+
when :stream
|
666
|
+
# Write deflated string to buffer
|
667
|
+
@zlib_reader << msg
|
668
|
+
|
669
|
+
# Check if message ends in `ZLIB_SUFFIX`
|
670
|
+
return if msg.bytesize < 4 || msg.byteslice(-4, 4) != ZLIB_SUFFIX
|
671
|
+
|
672
|
+
# Inflate the deflated buffer
|
673
|
+
msg = @zlib_reader.inflate('')
|
548
674
|
end
|
549
675
|
|
550
676
|
# Parse packet
|
@@ -571,6 +697,8 @@ module Discordrb
|
|
571
697
|
handle_invalidate_session
|
572
698
|
when Opcodes::HEARTBEAT_ACK
|
573
699
|
handle_heartbeat_ack(packet)
|
700
|
+
when Opcodes::HEARTBEAT
|
701
|
+
handle_heartbeat(packet)
|
574
702
|
else
|
575
703
|
LOGGER.warn("Received invalid opcode #{op} - please report with this information: #{msg}")
|
576
704
|
end
|
@@ -587,24 +715,28 @@ module Discordrb
|
|
587
715
|
|
588
716
|
@session = Session.new(data['session_id'])
|
589
717
|
@session.sequence = 0
|
718
|
+
@bot.__send__(:notify_ready) if @intents && (@intents & INTENTS[:servers]).zero?
|
590
719
|
when :RESUMED
|
591
720
|
# The RESUMED event is received after a successful op 6 (resume). It does nothing except tell the bot the
|
592
721
|
# connection is initiated (like READY would). Starting with v5, it doesn't set a new heartbeat interval anymore
|
593
722
|
# since that is handled by op 10 (HELLO).
|
594
|
-
LOGGER.
|
723
|
+
LOGGER.info 'Resumed'
|
595
724
|
return
|
596
725
|
end
|
597
726
|
|
598
727
|
@bot.dispatch(type, data)
|
599
728
|
end
|
600
729
|
|
730
|
+
# Op 1
|
731
|
+
def handle_heartbeat(packet)
|
732
|
+
# If we receive a heartbeat, we have to resend one with the same sequence
|
733
|
+
send_heartbeat(packet['s'])
|
734
|
+
end
|
735
|
+
|
601
736
|
# Op 7
|
602
737
|
def handle_reconnect
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
# Suspend session so we resume afterwards
|
607
|
-
@session.suspend
|
738
|
+
LOGGER.debug('Received op 7, reconnecting and attempting resume')
|
739
|
+
reconnect
|
608
740
|
end
|
609
741
|
|
610
742
|
# Op 9
|
@@ -629,8 +761,13 @@ module Discordrb
|
|
629
761
|
setup_heartbeats(interval)
|
630
762
|
|
631
763
|
LOGGER.debug("Trace: #{packet['d']['_trace']}")
|
764
|
+
LOGGER.debug("Session: #{@session.inspect}")
|
765
|
+
|
766
|
+
if @session&.should_resume?
|
767
|
+
# Make sure we're sending heartbeats again
|
768
|
+
@session.resume
|
632
769
|
|
633
|
-
|
770
|
+
# Send the actual resume packet to get the missing events
|
634
771
|
resume
|
635
772
|
else
|
636
773
|
identify
|
@@ -640,6 +777,7 @@ module Discordrb
|
|
640
777
|
# Op 11
|
641
778
|
def handle_heartbeat_ack(packet)
|
642
779
|
LOGGER.debug("Received heartbeat ack for packet: #{packet.inspect}")
|
780
|
+
@last_heartbeat_acked = true if @check_heartbeat_acks
|
643
781
|
end
|
644
782
|
|
645
783
|
# Called when the websocket has been disconnected in some way - say due to a pipe error while sending
|
@@ -648,31 +786,31 @@ module Discordrb
|
|
648
786
|
handle_close(e)
|
649
787
|
end
|
650
788
|
|
789
|
+
# Close codes that are unrecoverable, after which we should not try to reconnect.
|
790
|
+
# - 4003: Not authenticated. How did this happen?
|
791
|
+
# - 4004: Authentication failed. Token was wrong, nothing we can do.
|
792
|
+
# - 4011: Sharding required. Currently requires developer intervention.
|
793
|
+
FATAL_CLOSE_CODES = [4003, 4004, 4011].freeze
|
794
|
+
|
651
795
|
def handle_close(e)
|
796
|
+
@bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
|
797
|
+
|
652
798
|
if e.respond_to? :code
|
653
799
|
# It is a proper close frame we're dealing with, print reason and message to console
|
654
800
|
LOGGER.error('Websocket close frame received!')
|
655
801
|
LOGGER.error("Code: #{e.code}")
|
656
802
|
LOGGER.error("Message: #{e.data}")
|
803
|
+
@should_reconnect = false if FATAL_CLOSE_CODES.include?(e.code)
|
657
804
|
elsif e.is_a? Exception
|
658
805
|
# Log the exception
|
659
806
|
LOGGER.error('The websocket connection has closed due to an error!')
|
660
807
|
LOGGER.log_exception(e)
|
661
808
|
else
|
662
|
-
LOGGER.error("The websocket connection has closed: #{e
|
809
|
+
LOGGER.error("The websocket connection has closed: #{e&.inspect || '(no information)'}")
|
663
810
|
end
|
664
811
|
end
|
665
812
|
|
666
|
-
def
|
667
|
-
data = {
|
668
|
-
op: op,
|
669
|
-
d: packet
|
670
|
-
}
|
671
|
-
|
672
|
-
send(data.to_json)
|
673
|
-
end
|
674
|
-
|
675
|
-
def send(data, type = :text)
|
813
|
+
def send(data, type = :text, code = nil)
|
676
814
|
LOGGER.out(data)
|
677
815
|
|
678
816
|
unless @handshaked && !@closed
|
@@ -681,40 +819,33 @@ module Discordrb
|
|
681
819
|
end
|
682
820
|
|
683
821
|
# Create the frame we're going to send
|
684
|
-
frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @handshake.version)
|
822
|
+
frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @handshake.version, code: code)
|
685
823
|
|
686
824
|
# Try to send it
|
687
825
|
begin
|
688
826
|
@socket.write frame.to_s
|
689
|
-
rescue
|
827
|
+
rescue StandardError => e
|
690
828
|
# There has been an error!
|
691
829
|
@pipe_broken = true
|
692
830
|
handle_internal_close(e)
|
693
831
|
end
|
694
832
|
end
|
695
833
|
|
696
|
-
def close(
|
834
|
+
def close(code = 1000)
|
697
835
|
# If we're already closed, there's no need to do anything - return
|
698
836
|
return if @closed
|
699
837
|
|
700
838
|
# Suspend the session so we don't send heartbeats
|
701
|
-
@session
|
839
|
+
@session&.suspend
|
702
840
|
|
703
841
|
# Send a close frame (if we can)
|
704
|
-
send nil, :close unless @pipe_broken
|
842
|
+
send nil, :close, code unless @pipe_broken
|
705
843
|
|
706
844
|
# We're officially closed, notify the main loop.
|
707
|
-
|
708
|
-
# close afterwards, don't coincide with the main loop reading something from the SSL socket.
|
709
|
-
# This would cause a segfault due to (I suspect) Ruby bug #12292: https://bugs.ruby-lang.org/issues/12292
|
710
|
-
if no_sync
|
711
|
-
@closed = true
|
712
|
-
else
|
713
|
-
@getc_mutex.synchronize { @closed = true }
|
714
|
-
end
|
845
|
+
@closed = true
|
715
846
|
|
716
847
|
# Close the socket if possible
|
717
|
-
@socket
|
848
|
+
@socket&.close
|
718
849
|
@socket = nil
|
719
850
|
|
720
851
|
# Make sure we do necessary things as soon as we're closed
|