discordrb 3.3.0 → 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.

Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +126 -0
  3. data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
  6. data/.github/pull_request_template.md +37 -0
  7. data/.rubocop.yml +34 -37
  8. data/.travis.yml +5 -6
  9. data/CHANGELOG.md +472 -347
  10. data/Gemfile +2 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +61 -79
  13. data/Rakefile +2 -0
  14. data/bin/console +1 -0
  15. data/discordrb-webhooks.gemspec +6 -6
  16. data/discordrb.gemspec +17 -17
  17. data/lib/discordrb.rb +73 -0
  18. data/lib/discordrb/allowed_mentions.rb +36 -0
  19. data/lib/discordrb/api.rb +40 -15
  20. data/lib/discordrb/api/channel.rb +57 -39
  21. data/lib/discordrb/api/invite.rb +3 -3
  22. data/lib/discordrb/api/server.rb +55 -50
  23. data/lib/discordrb/api/user.rb +8 -8
  24. data/lib/discordrb/api/webhook.rb +6 -6
  25. data/lib/discordrb/await.rb +0 -1
  26. data/lib/discordrb/bot.rb +164 -72
  27. data/lib/discordrb/cache.rb +4 -2
  28. data/lib/discordrb/colour_rgb.rb +43 -0
  29. data/lib/discordrb/commands/command_bot.rb +22 -6
  30. data/lib/discordrb/commands/container.rb +20 -23
  31. data/lib/discordrb/commands/parser.rb +18 -18
  32. data/lib/discordrb/commands/rate_limiter.rb +3 -2
  33. data/lib/discordrb/container.rb +77 -17
  34. data/lib/discordrb/data.rb +25 -4180
  35. data/lib/discordrb/data/activity.rb +264 -0
  36. data/lib/discordrb/data/application.rb +50 -0
  37. data/lib/discordrb/data/attachment.rb +56 -0
  38. data/lib/discordrb/data/audit_logs.rb +345 -0
  39. data/lib/discordrb/data/channel.rb +849 -0
  40. data/lib/discordrb/data/embed.rb +251 -0
  41. data/lib/discordrb/data/emoji.rb +82 -0
  42. data/lib/discordrb/data/integration.rb +83 -0
  43. data/lib/discordrb/data/invite.rb +137 -0
  44. data/lib/discordrb/data/member.rb +297 -0
  45. data/lib/discordrb/data/message.rb +334 -0
  46. data/lib/discordrb/data/overwrite.rb +102 -0
  47. data/lib/discordrb/data/profile.rb +91 -0
  48. data/lib/discordrb/data/reaction.rb +33 -0
  49. data/lib/discordrb/data/recipient.rb +34 -0
  50. data/lib/discordrb/data/role.rb +191 -0
  51. data/lib/discordrb/data/server.rb +1002 -0
  52. data/lib/discordrb/data/user.rb +204 -0
  53. data/lib/discordrb/data/voice_region.rb +45 -0
  54. data/lib/discordrb/data/voice_state.rb +41 -0
  55. data/lib/discordrb/data/webhook.rb +145 -0
  56. data/lib/discordrb/errors.rb +2 -1
  57. data/lib/discordrb/events/bans.rb +7 -5
  58. data/lib/discordrb/events/channels.rb +2 -0
  59. data/lib/discordrb/events/guilds.rb +16 -9
  60. data/lib/discordrb/events/invites.rb +125 -0
  61. data/lib/discordrb/events/members.rb +6 -2
  62. data/lib/discordrb/events/message.rb +69 -27
  63. data/lib/discordrb/events/presence.rb +14 -4
  64. data/lib/discordrb/events/raw.rb +1 -3
  65. data/lib/discordrb/events/reactions.rb +49 -3
  66. data/lib/discordrb/events/typing.rb +6 -4
  67. data/lib/discordrb/events/voice_server_update.rb +47 -0
  68. data/lib/discordrb/events/voice_state_update.rb +15 -10
  69. data/lib/discordrb/events/webhooks.rb +9 -6
  70. data/lib/discordrb/gateway.rb +72 -57
  71. data/lib/discordrb/id_object.rb +39 -0
  72. data/lib/discordrb/light/integrations.rb +1 -1
  73. data/lib/discordrb/light/light_bot.rb +1 -1
  74. data/lib/discordrb/logger.rb +4 -4
  75. data/lib/discordrb/paginator.rb +57 -0
  76. data/lib/discordrb/permissions.rb +103 -8
  77. data/lib/discordrb/version.rb +1 -1
  78. data/lib/discordrb/voice/encoder.rb +3 -3
  79. data/lib/discordrb/voice/network.rb +84 -43
  80. data/lib/discordrb/voice/sodium.rb +96 -0
  81. data/lib/discordrb/voice/voice_bot.rb +34 -26
  82. metadata +93 -55
@@ -15,11 +15,16 @@ module Discordrb::Events
15
15
  # @return [Symbol] the new status.
16
16
  attr_reader :status
17
17
 
18
+ # @return [Hash<Symbol, Symbol>] the current online status (`:online`, `:idle` or `:dnd`) of the user
19
+ # on various device types (`:desktop`, `:mobile`, or `:web`). The value will be `nil` if the user is offline or invisible.
20
+ attr_reader :client_status
21
+
18
22
  def initialize(data, bot)
19
23
  @bot = bot
20
24
 
21
25
  @user = bot.user(data['user']['id'].to_i)
22
26
  @status = data['status'].to_sym
27
+ @client_status = user.client_status
23
28
  @server = bot.server(data['guild_id'].to_i)
24
29
  end
25
30
  end
@@ -32,9 +37,10 @@ module Discordrb::Events
32
37
 
33
38
  [
34
39
  matches_all(@attributes[:from], event.user) do |a, e|
35
- a == if a.is_a? String
40
+ a == case a
41
+ when String
36
42
  e.name
37
- elsif a.is_a? Integer
43
+ when Integer
38
44
  e.id
39
45
  else
40
46
  e
@@ -92,9 +98,10 @@ module Discordrb::Events
92
98
 
93
99
  [
94
100
  matches_all(@attributes[:from], event.user) do |a, e|
95
- a == if a.is_a? String
101
+ a == case a
102
+ when String
96
103
  e.name
97
- elsif a.is_a? Integer
104
+ when Integer
98
105
  e.id
99
106
  else
100
107
  e
@@ -105,6 +112,9 @@ module Discordrb::Events
105
112
  end,
106
113
  matches_all(@attributes[:type], event.type) do |a, e|
107
114
  a == e
115
+ end,
116
+ matches_all(@attributes[:client_status], event.client_status) do |a, e|
117
+ e.slice(a.keys) == a
108
118
  end
109
119
  ].reduce(true, &:&)
110
120
  end
@@ -30,9 +30,7 @@ module Discordrb::Events
30
30
  [
31
31
  matches_all(@attributes[:type] || @attributes[:t], event.type) do |a, e|
32
32
  if a.is_a? Regexp
33
- # 24: update to matches?
34
- match = a.match(e)
35
- match ? (e == match[0]) : false
33
+ a.match?(e)
36
34
  else
37
35
  e.to_s.casecmp(a.to_s).zero?
38
36
  end
@@ -11,6 +11,9 @@ module Discordrb::Events
11
11
  # @return [Emoji] the emoji that was reacted with.
12
12
  attr_reader :emoji
13
13
 
14
+ # @!visibility private
15
+ attr_reader :message_id
16
+
14
17
  def initialize(data, bot)
15
18
  @bot = bot
16
19
 
@@ -54,13 +57,38 @@ module Discordrb::Events
54
57
 
55
58
  [
56
59
  matches_all(@attributes[:emoji], event.emoji) do |a, e|
57
- if a.is_a? Integer
60
+ case a
61
+ when Integer
58
62
  e.id == a
59
- elsif a.is_a? String
63
+ when String
60
64
  e.name == a || e.name == a.delete(':') || e.id == a.resolve_id
61
65
  else
62
66
  e == a
63
67
  end
68
+ end,
69
+ matches_all(@attributes[:message], event.message_id) do |a, e|
70
+ a == e
71
+ end,
72
+ matches_all(@attributes[:in], event.channel) do |a, e|
73
+ case a
74
+ when String
75
+ # Make sure to remove the "#" from channel names in case it was specified
76
+ a.delete('#') == e.name
77
+ when Integer
78
+ a == e.id
79
+ else
80
+ a == e
81
+ end
82
+ end,
83
+ matches_all(@attributes[:from], event.user) do |a, e|
84
+ case a
85
+ when String
86
+ a == e.name
87
+ when :bot
88
+ e.current_bot?
89
+ else
90
+ a == e
91
+ end
64
92
  end
65
93
  ].reduce(true, &:&)
66
94
  end
@@ -82,6 +110,9 @@ module Discordrb::Events
82
110
  class ReactionRemoveAllEvent < Event
83
111
  include Respondable
84
112
 
113
+ # @!visibility private
114
+ attr_reader :message_id
115
+
85
116
  def initialize(data, bot)
86
117
  @bot = bot
87
118
 
@@ -107,7 +138,22 @@ module Discordrb::Events
107
138
  return false unless event.is_a? ReactionRemoveAllEvent
108
139
 
109
140
  # No attributes yet as there is no property available on the event that doesn't involve doing a resolution request
110
- [].reduce(true, &:&)
141
+ [
142
+ matches_all(@attributes[:message], event.message_id) do |a, e|
143
+ a == e
144
+ end,
145
+ matches_all(@attributes[:in], event.channel) do |a, e|
146
+ case a
147
+ when String
148
+ # Make sure to remove the "#" from channel names in case it was specified
149
+ a.delete('#') == e.name
150
+ when Integer
151
+ a == e.id
152
+ else
153
+ a == e
154
+ end
155
+ end
156
+ ].reduce(true, &:&)
111
157
  end
112
158
  end
113
159
  end
@@ -45,18 +45,20 @@ module Discordrb::Events
45
45
 
46
46
  [
47
47
  matches_all(@attributes[:in], event.channel) do |a, e|
48
- if a.is_a? String
48
+ case a
49
+ when String
49
50
  a.delete('#') == e.name
50
- elsif a.is_a? Integer
51
+ when Integer
51
52
  a == e.id
52
53
  else
53
54
  a == e
54
55
  end
55
56
  end,
56
57
  matches_all(@attributes[:from], event.user) do |a, e|
57
- a == if a.is_a? String
58
+ a == case a
59
+ when String
58
60
  e.name
59
- elsif a.is_a? Integer
61
+ when Integer
60
62
  e.id
61
63
  else
62
64
  e
@@ -0,0 +1,47 @@
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 server's voice server is updating.
8
+ # Sent when initially connecting to voice and when a voice instance fails
9
+ # over to a new server.
10
+ # This event is exposed for use with library agnostic interfaces like telecom and
11
+ # lavalink.
12
+ class VoiceServerUpdateEvent < Event
13
+ # @return [String] The voice connection token
14
+ attr_reader :token
15
+
16
+ # @return [Server] The server this update is for.
17
+ attr_reader :server
18
+
19
+ # @return [String] The voice server host.
20
+ attr_reader :endpoint
21
+
22
+ def initialize(data, bot)
23
+ @bot = bot
24
+
25
+ @token = data['token']
26
+ @endpoint = data['endpoint']
27
+ @server = bot.server(data['guild_id'])
28
+ end
29
+ end
30
+
31
+ # Event handler for VoiceServerUpdateEvent
32
+ class VoiceServerUpdateEventHandler < EventHandler
33
+ def matches?(event)
34
+ return false unless event.is_a? VoiceServerUpdateEvent
35
+
36
+ [
37
+ matches_all(@attributes[:from], event.server) do |a, e|
38
+ a == if a.is_a? String
39
+ e.name
40
+ else
41
+ e
42
+ end
43
+ end
44
+ ]
45
+ end
46
+ end
47
+ end
@@ -38,37 +38,38 @@ module Discordrb::Events
38
38
 
39
39
  [
40
40
  matches_all(@attributes[:from], event.user) do |a, e|
41
- a == if a.is_a? String
41
+ a == case a
42
+ when String
42
43
  e.name
43
- elsif a.is_a? Integer
44
+ when Integer
44
45
  e.id
45
46
  else
46
47
  e
47
48
  end
48
49
  end,
49
50
  matches_all(@attributes[:mute], event.mute) do |a, e|
50
- a == if a.is_a?(TrueClass) || a.is_a?(FalseClass)
51
+ a == if a.is_a? String
51
52
  e.to_s
52
53
  else
53
54
  e
54
55
  end
55
56
  end,
56
57
  matches_all(@attributes[:deaf], event.deaf) do |a, e|
57
- a == if a.is_a?(TrueClass) || a.is_a?(FalseClass)
58
+ a == if a.is_a? String
58
59
  e.to_s
59
60
  else
60
61
  e
61
62
  end
62
63
  end,
63
64
  matches_all(@attributes[:self_mute], event.self_mute) do |a, e|
64
- a == if a.is_a?(TrueClass) || a.is_a?(FalseClass)
65
+ a == if a.is_a? String
65
66
  e.to_s
66
67
  else
67
68
  e
68
69
  end
69
70
  end,
70
71
  matches_all(@attributes[:self_deaf], event.self_deaf) do |a, e|
71
- a == if a.is_a?(TrueClass) || a.is_a?(FalseClass)
72
+ a == if a.is_a? String
72
73
  e.to_s
73
74
  else
74
75
  e
@@ -76,9 +77,11 @@ module Discordrb::Events
76
77
  end,
77
78
  matches_all(@attributes[:channel], event.channel) do |a, e|
78
79
  next unless e # Don't bother if the channel is nil
79
- a == if a.is_a? String
80
+
81
+ a == case a
82
+ when String
80
83
  e.name
81
- elsif a.is_a? Integer
84
+ when Integer
82
85
  e.id
83
86
  else
84
87
  e
@@ -86,9 +89,11 @@ module Discordrb::Events
86
89
  end,
87
90
  matches_all(@attributes[:old_channel], event.old_channel) do |a, e|
88
91
  next unless e # Don't bother if the channel is nil
89
- a == if a.is_a? String
92
+
93
+ a == case a
94
+ when String
90
95
  e.name
91
- elsif a.is_a? Integer
96
+ when Integer
92
97
  e.id
93
98
  else
94
99
  e
@@ -28,28 +28,31 @@ module Discordrb::Events
28
28
 
29
29
  [
30
30
  matches_all(@attributes[:server], event.server) do |a, e|
31
- a == if a.is_a? String
31
+ a == case a
32
+ when String
32
33
  e.name
33
- elsif a.is_a? Integer
34
+ when Integer
34
35
  e.id
35
36
  else
36
37
  e
37
38
  end
38
39
  end,
39
40
  matches_all(@attributes[:channel], event.channel) do |a, e|
40
- if a.is_a? String
41
+ case a
42
+ when String
41
43
  # Make sure to remove the "#" from channel names in case it was specified
42
44
  a.delete('#') == e.name
43
- elsif a.is_a? Integer
45
+ when Integer
44
46
  a == e.id
45
47
  else
46
48
  a == e
47
49
  end
48
50
  end,
49
51
  matches_all(@attributes[:webhook], event) do |a, e|
50
- a == if a.is_a? String
52
+ a == case a
53
+ when String
51
54
  e.name
52
- elsif a.is_a? Integer
55
+ when Integer
53
56
  e.id
54
57
  else
55
58
  e
@@ -25,8 +25,6 @@
25
25
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
26
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
27
 
28
- require 'thread'
29
-
30
28
  module Discordrb
31
29
  # Gateway packet opcodes
32
30
  module Opcodes
@@ -145,20 +143,19 @@ module Discordrb
145
143
  # @return [true, false] whether or not this gateway should check for heartbeat ACKs.
146
144
  attr_accessor :check_heartbeat_acks
147
145
 
148
- def initialize(bot, token, shard_key = nil, compress_mode = :stream)
146
+ def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = nil)
149
147
  @token = token
150
148
  @bot = bot
151
149
 
152
150
  @shard_key = shard_key
153
151
 
154
- @getc_mutex = Mutex.new
155
-
156
152
  # Whether the connection to the gateway has succeeded yet
157
153
  @ws_success = false
158
154
 
159
155
  @check_heartbeat_acks = true
160
156
 
161
157
  @compress_mode = compress_mode
158
+ @intents = intents
162
159
  end
163
160
 
164
161
  # Connect to the gateway server in a separate thread
@@ -170,8 +167,19 @@ module Discordrb
170
167
  end
171
168
 
172
169
  LOGGER.debug('WS thread created! Now waiting for confirmation that everything worked')
173
- sleep(0.5) until @ws_success
174
- LOGGER.debug('Confirmation received! Exiting run.')
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
175
183
  end
176
184
 
177
185
  # Prevents all further execution until the websocket thread stops (e.g. through a closed connection).
@@ -181,16 +189,16 @@ module Discordrb
181
189
 
182
190
  # Whether the WebSocket connection to the gateway is currently open
183
191
  def open?
184
- @handshake && @handshake.finished? && !@closed
192
+ @handshake&.finished? && !@closed
185
193
  end
186
194
 
187
195
  # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
188
196
  # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
189
197
  #
190
198
  # If this method doesn't work or you're looking for something more drastic, use {#kill} instead.
191
- def stop(no_sync = false)
199
+ def stop
192
200
  @should_reconnect = false
193
- close(no_sync)
201
+ close
194
202
 
195
203
  # Return nil so command bots don't send a message
196
204
  nil
@@ -241,7 +249,7 @@ module Discordrb
241
249
  def heartbeat
242
250
  if check_heartbeat_acks
243
251
  unless @last_heartbeat_acked
244
- # We're in a bad situation - apparently the last heartbeat wasn't acked, which means the connection is likely
252
+ # We're in a bad situation - apparently the last heartbeat wasn't ACK'd, which means the connection is likely
245
253
  # a zombie. Reconnect
246
254
  LOGGER.warn('Last heartbeat was not acked, so this is a zombie connection! Reconnecting')
247
255
 
@@ -303,6 +311,7 @@ module Discordrb
303
311
  compress: compress,
304
312
  large_threshold: large_threshold
305
313
  }
314
+ data[:intents] = @intents unless @intents.nil?
306
315
 
307
316
  # Don't include the shard key at all if it is nil as Discord checks for its mere existence
308
317
  data[:shard] = shard_key if shard_key
@@ -312,7 +321,7 @@ module Discordrb
312
321
 
313
322
  # Sends a status update packet (op 3). This sets the bot user's status (online/idle/...) and game playing/streaming.
314
323
  # @param status [String] The status that should be set (`online`, `idle`, `dnd`, `invisible`).
315
- # @param since [Integer] The unix timestamp in milliseconds when the status was set. Should only be provided when
324
+ # @param since [Integer] The Unix timestamp in milliseconds when the status was set. Should only be provided when
316
325
  # `afk` is true.
317
326
  # @param game [Hash<Symbol => Object>, nil] `nil` if no game should be played, or a hash of `:game => "name"` if a
318
327
  # game should be played. The hash can also contain additional attributes for streaming statuses.
@@ -360,7 +369,7 @@ module Discordrb
360
369
  @instant_reconnect = true
361
370
  @should_reconnect = true
362
371
 
363
- close
372
+ close(4000)
364
373
  end
365
374
 
366
375
  # Sends a resume packet (op 6). This replays all events from a previous point specified by its packet sequence. This
@@ -402,12 +411,12 @@ module Discordrb
402
411
 
403
412
  # Sends a custom packet over the connection. This can be useful to implement future yet unimplemented functionality
404
413
  # or for testing. You probably shouldn't use this unless you know what you're doing.
405
- # @param op [Integer] The opcode the packet should be sent as. Can be one of {Opcodes} or a custom value if
414
+ # @param opcode [Integer] The opcode the packet should be sent as. Can be one of {Opcodes} or a custom value if
406
415
  # necessary.
407
- # @param packet [Object] Some arbitrary JSON-serialisable data that should be sent as the `d` field.
408
- def send_packet(op, packet)
416
+ # @param packet [Object] Some arbitrary JSON-serializable data that should be sent as the `d` field.
417
+ def send_packet(opcode, packet)
409
418
  data = {
410
- op: op,
419
+ op: opcode,
411
420
  d: packet
412
421
  }
413
422
 
@@ -436,20 +445,18 @@ module Discordrb
436
445
  @heartbeat_thread = Thread.new do
437
446
  Thread.current[:discordrb_name] = 'heartbeat'
438
447
  loop do
439
- begin
440
- # Send a heartbeat if heartbeats are active and either no session exists yet, or an existing session is
441
- # suspended (e.g. after op7)
442
- if (@session && !@session.suspended?) || !@session
443
- sleep @heartbeat_interval
444
- @bot.raise_heartbeat_event
445
- heartbeat
446
- else
447
- sleep 1
448
- end
449
- rescue => e
450
- LOGGER.error('An error occurred while heartbeating!')
451
- 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
452
456
  end
457
+ rescue StandardError => e
458
+ LOGGER.error('An error occurred while heartbeating!')
459
+ LOGGER.log_exception(e)
453
460
  end
454
461
  end
455
462
  end
@@ -502,7 +509,7 @@ module Discordrb
502
509
  cert_store.set_default_paths
503
510
  ctx.cert_store = cert_store
504
511
  else
505
- ctx.set_params ssl_version: :TLSv1_2
512
+ ctx.set_params ssl_version: :TLSv1_2 # rubocop:disable Naming/VariableNumber
506
513
  end
507
514
 
508
515
  socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
@@ -567,7 +574,7 @@ module Discordrb
567
574
 
568
575
  # We're done! Delegate to the websocket loop
569
576
  websocket_loop
570
- rescue => e
577
+ rescue StandardError => e
571
578
  LOGGER.error('An error occurred while connecting to the websocket!')
572
579
  LOGGER.log_exception(e)
573
580
  end
@@ -584,13 +591,17 @@ module Discordrb
584
591
  unless @socket
585
592
  LOGGER.warn('Socket is nil in websocket_loop! Reconnecting')
586
593
  handle_internal_close('Socket is nil in websocket_loop')
594
+ next
587
595
  end
588
596
 
589
- recv_data = nil
590
-
591
- # Get some data from the socket, synchronised so the socket can't be closed during this
592
- # 24: remove locking
593
- @getc_mutex.synchronize { recv_data = @socket.getc }
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
594
605
 
595
606
  # Check if we actually got data
596
607
  unless recv_data
@@ -621,7 +632,7 @@ module Discordrb
621
632
  # If the handshake hasn't finished, handle it
622
633
  handle_handshake_data(recv_data)
623
634
  end
624
- rescue => e
635
+ rescue StandardError => e
625
636
  handle_error(e)
626
637
  end
627
638
  end
@@ -645,12 +656,13 @@ module Discordrb
645
656
  ZLIB_SUFFIX = "\x00\x00\xFF\xFF".b.freeze
646
657
 
647
658
  def handle_message(msg)
648
- if @compress_mode == :large
659
+ case @compress_mode
660
+ when :large
649
661
  if msg.byteslice(0) == 'x'
650
662
  # The message is compressed, inflate it
651
663
  msg = Zlib::Inflate.inflate(msg)
652
664
  end
653
- elsif @compress_mode == :stream
665
+ when :stream
654
666
  # Write deflated string to buffer
655
667
  @zlib_reader << msg
656
668
 
@@ -703,6 +715,7 @@ module Discordrb
703
715
 
704
716
  @session = Session.new(data['session_id'])
705
717
  @session.sequence = 0
718
+ @bot.__send__(:notify_ready) if @intents && (@intents & INTENTS[:servers]).zero?
706
719
  when :RESUMED
707
720
  # The RESUMED event is received after a successful op 6 (resume). It does nothing except tell the bot the
708
721
  # connection is initiated (like READY would). Starting with v5, it doesn't set a new heartbeat interval anymore
@@ -750,7 +763,7 @@ module Discordrb
750
763
  LOGGER.debug("Trace: #{packet['d']['_trace']}")
751
764
  LOGGER.debug("Session: #{@session.inspect}")
752
765
 
753
- if @session && @session.should_resume?
766
+ if @session&.should_resume?
754
767
  # Make sure we're sending heartbeats again
755
768
  @session.resume
756
769
 
@@ -773,22 +786,31 @@ module Discordrb
773
786
  handle_close(e)
774
787
  end
775
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
+
776
795
  def handle_close(e)
796
+ @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
797
+
777
798
  if e.respond_to? :code
778
799
  # It is a proper close frame we're dealing with, print reason and message to console
779
800
  LOGGER.error('Websocket close frame received!')
780
801
  LOGGER.error("Code: #{e.code}")
781
802
  LOGGER.error("Message: #{e.data}")
803
+ @should_reconnect = false if FATAL_CLOSE_CODES.include?(e.code)
782
804
  elsif e.is_a? Exception
783
805
  # Log the exception
784
806
  LOGGER.error('The websocket connection has closed due to an error!')
785
807
  LOGGER.log_exception(e)
786
808
  else
787
- LOGGER.error("The websocket connection has closed: #{e.inspect}")
809
+ LOGGER.error("The websocket connection has closed: #{e&.inspect || '(no information)'}")
788
810
  end
789
811
  end
790
812
 
791
- def send(data, type = :text)
813
+ def send(data, type = :text, code = nil)
792
814
  LOGGER.out(data)
793
815
 
794
816
  unless @handshaked && !@closed
@@ -797,40 +819,33 @@ module Discordrb
797
819
  end
798
820
 
799
821
  # Create the frame we're going to send
800
- 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)
801
823
 
802
824
  # Try to send it
803
825
  begin
804
826
  @socket.write frame.to_s
805
- rescue => e
827
+ rescue StandardError => e
806
828
  # There has been an error!
807
829
  @pipe_broken = true
808
830
  handle_internal_close(e)
809
831
  end
810
832
  end
811
833
 
812
- def close(no_sync = false)
834
+ def close(code = 1000)
813
835
  # If we're already closed, there's no need to do anything - return
814
836
  return if @closed
815
837
 
816
838
  # Suspend the session so we don't send heartbeats
817
- @session.suspend if @session
839
+ @session&.suspend
818
840
 
819
841
  # Send a close frame (if we can)
820
- send nil, :close unless @pipe_broken
842
+ send nil, :close, code unless @pipe_broken
821
843
 
822
844
  # We're officially closed, notify the main loop.
823
- # This needs to be synchronised with the getc mutex, so the notification, and especially the actual
824
- # close afterwards, don't coincide with the main loop reading something from the SSL socket.
825
- # This would cause a segfault due to (I suspect) Ruby bug #12292: https://bugs.ruby-lang.org/issues/12292
826
- if no_sync
827
- @closed = true
828
- else
829
- @getc_mutex.synchronize { @closed = true }
830
- end
845
+ @closed = true
831
846
 
832
847
  # Close the socket if possible
833
- @socket.close if @socket
848
+ @socket&.close
834
849
  @socket = nil
835
850
 
836
851
  # Make sure we do necessary things as soon as we're closed