discordrb 3.3.0 → 3.5.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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +152 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  5. data/.github/pull_request_template.md +37 -0
  6. data/.github/workflows/codeql.yml +65 -0
  7. data/.markdownlint.json +4 -0
  8. data/.rubocop.yml +39 -36
  9. data/CHANGELOG.md +874 -552
  10. data/Gemfile +2 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +80 -86
  13. data/Rakefile +2 -0
  14. data/bin/console +1 -0
  15. data/discordrb-webhooks.gemspec +9 -6
  16. data/discordrb.gemspec +21 -18
  17. data/lib/discordrb/allowed_mentions.rb +36 -0
  18. data/lib/discordrb/api/application.rb +202 -0
  19. data/lib/discordrb/api/channel.rb +236 -47
  20. data/lib/discordrb/api/interaction.rb +54 -0
  21. data/lib/discordrb/api/invite.rb +5 -5
  22. data/lib/discordrb/api/server.rb +94 -66
  23. data/lib/discordrb/api/user.rb +17 -11
  24. data/lib/discordrb/api/webhook.rb +63 -6
  25. data/lib/discordrb/api.rb +55 -16
  26. data/lib/discordrb/await.rb +0 -1
  27. data/lib/discordrb/bot.rb +480 -93
  28. data/lib/discordrb/cache.rb +31 -24
  29. data/lib/discordrb/colour_rgb.rb +43 -0
  30. data/lib/discordrb/commands/command_bot.rb +35 -12
  31. data/lib/discordrb/commands/container.rb +21 -24
  32. data/lib/discordrb/commands/parser.rb +20 -20
  33. data/lib/discordrb/commands/rate_limiter.rb +4 -3
  34. data/lib/discordrb/container.rb +209 -20
  35. data/lib/discordrb/data/activity.rb +271 -0
  36. data/lib/discordrb/data/application.rb +50 -0
  37. data/lib/discordrb/data/attachment.rb +71 -0
  38. data/lib/discordrb/data/audit_logs.rb +345 -0
  39. data/lib/discordrb/data/channel.rb +993 -0
  40. data/lib/discordrb/data/component.rb +229 -0
  41. data/lib/discordrb/data/embed.rb +251 -0
  42. data/lib/discordrb/data/emoji.rb +82 -0
  43. data/lib/discordrb/data/integration.rb +122 -0
  44. data/lib/discordrb/data/interaction.rb +800 -0
  45. data/lib/discordrb/data/invite.rb +137 -0
  46. data/lib/discordrb/data/member.rb +372 -0
  47. data/lib/discordrb/data/message.rb +414 -0
  48. data/lib/discordrb/data/overwrite.rb +108 -0
  49. data/lib/discordrb/data/profile.rb +91 -0
  50. data/lib/discordrb/data/reaction.rb +33 -0
  51. data/lib/discordrb/data/recipient.rb +34 -0
  52. data/lib/discordrb/data/role.rb +248 -0
  53. data/lib/discordrb/data/server.rb +1004 -0
  54. data/lib/discordrb/data/user.rb +264 -0
  55. data/lib/discordrb/data/voice_region.rb +45 -0
  56. data/lib/discordrb/data/voice_state.rb +41 -0
  57. data/lib/discordrb/data/webhook.rb +238 -0
  58. data/lib/discordrb/data.rb +28 -4180
  59. data/lib/discordrb/errors.rb +46 -4
  60. data/lib/discordrb/events/bans.rb +7 -5
  61. data/lib/discordrb/events/channels.rb +3 -1
  62. data/lib/discordrb/events/guilds.rb +16 -9
  63. data/lib/discordrb/events/interactions.rb +482 -0
  64. data/lib/discordrb/events/invites.rb +125 -0
  65. data/lib/discordrb/events/members.rb +6 -2
  66. data/lib/discordrb/events/message.rb +72 -27
  67. data/lib/discordrb/events/presence.rb +35 -18
  68. data/lib/discordrb/events/raw.rb +1 -3
  69. data/lib/discordrb/events/reactions.rb +49 -4
  70. data/lib/discordrb/events/threads.rb +96 -0
  71. data/lib/discordrb/events/typing.rb +6 -4
  72. data/lib/discordrb/events/voice_server_update.rb +47 -0
  73. data/lib/discordrb/events/voice_state_update.rb +15 -10
  74. data/lib/discordrb/events/webhooks.rb +9 -6
  75. data/lib/discordrb/gateway.rb +99 -71
  76. data/lib/discordrb/id_object.rb +39 -0
  77. data/lib/discordrb/light/integrations.rb +1 -1
  78. data/lib/discordrb/light/light_bot.rb +1 -1
  79. data/lib/discordrb/logger.rb +4 -4
  80. data/lib/discordrb/paginator.rb +57 -0
  81. data/lib/discordrb/permissions.rb +159 -39
  82. data/lib/discordrb/version.rb +1 -1
  83. data/lib/discordrb/voice/encoder.rb +16 -7
  84. data/lib/discordrb/voice/network.rb +99 -47
  85. data/lib/discordrb/voice/sodium.rb +98 -0
  86. data/lib/discordrb/voice/voice_bot.rb +33 -25
  87. data/lib/discordrb/webhooks.rb +2 -0
  88. data/lib/discordrb.rb +107 -1
  89. metadata +126 -54
  90. data/.codeclimate.yml +0 -16
  91. data/.travis.yml +0 -33
  92. data/bin/travis_build_docs.sh +0 -17
  93. /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
@@ -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
@@ -136,7 +134,7 @@ module Discordrb
136
134
  LARGE_THRESHOLD = 100
137
135
 
138
136
  # The version of the gateway that's supposed to be used.
139
- GATEWAY_VERSION = 6
137
+ GATEWAY_VERSION = 9
140
138
 
141
139
  # Heartbeat ACKs are Discord's way of verifying on the client side whether the connection is still alive. If this is
142
140
  # set to true (default value) the gateway client will use that functionality to detect zombie connections and
@@ -145,20 +143,22 @@ 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
+ # @return [Integer] the intent parameter sent to the gateway server.
147
+ attr_reader :intents
148
+
149
+ def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = ALL_INTENTS)
149
150
  @token = token
150
151
  @bot = bot
151
152
 
152
153
  @shard_key = shard_key
153
154
 
154
- @getc_mutex = Mutex.new
155
-
156
155
  # Whether the connection to the gateway has succeeded yet
157
156
  @ws_success = false
158
157
 
159
158
  @check_heartbeat_acks = true
160
159
 
161
160
  @compress_mode = compress_mode
161
+ @intents = intents
162
162
  end
163
163
 
164
164
  # Connect to the gateway server in a separate thread
@@ -170,8 +170,19 @@ module Discordrb
170
170
  end
171
171
 
172
172
  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.')
173
+ loop do
174
+ sleep(0.5)
175
+
176
+ if @ws_success
177
+ LOGGER.debug('Confirmation received! Exiting run.')
178
+ break
179
+ end
180
+
181
+ if @should_reconnect == false
182
+ LOGGER.debug('Reconnection flag was unset. Exiting run.')
183
+ break
184
+ end
185
+ end
175
186
  end
176
187
 
177
188
  # Prevents all further execution until the websocket thread stops (e.g. through a closed connection).
@@ -181,16 +192,16 @@ module Discordrb
181
192
 
182
193
  # Whether the WebSocket connection to the gateway is currently open
183
194
  def open?
184
- @handshake && @handshake.finished? && !@closed
195
+ @handshake&.finished? && !@closed
185
196
  end
186
197
 
187
198
  # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
188
199
  # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
189
200
  #
190
201
  # If this method doesn't work or you're looking for something more drastic, use {#kill} instead.
191
- def stop(no_sync = false)
202
+ def stop
192
203
  @should_reconnect = false
193
- close(no_sync)
204
+ close
194
205
 
195
206
  # Return nil so command bots don't send a message
196
207
  nil
@@ -241,7 +252,7 @@ module Discordrb
241
252
  def heartbeat
242
253
  if check_heartbeat_acks
243
254
  unless @last_heartbeat_acked
244
- # We're in a bad situation - apparently the last heartbeat wasn't acked, which means the connection is likely
255
+ # We're in a bad situation - apparently the last heartbeat wasn't ACK'd, which means the connection is likely
245
256
  # a zombie. Reconnect
246
257
  LOGGER.warn('Last heartbeat was not acked, so this is a zombie connection! Reconnecting')
247
258
 
@@ -269,12 +280,12 @@ module Discordrb
269
280
  def identify
270
281
  compress = @compress_mode == :large
271
282
  send_identify(@token, {
272
- '$os': RUBY_PLATFORM,
273
- '$browser': 'discordrb',
274
- '$device': 'discordrb',
275
- '$referrer': '',
276
- '$referring_domain': ''
277
- }, compress, 100, @shard_key)
283
+ os: RUBY_PLATFORM,
284
+ browser: 'discordrb',
285
+ device: 'discordrb',
286
+ referrer: '',
287
+ referring_domain: ''
288
+ }, compress, LARGE_THRESHOLD, @shard_key, @intents)
278
289
  end
279
290
 
280
291
  # Sends an identify packet (op 2). This starts a new session on the current connection and tells Discord who we are.
@@ -284,24 +295,25 @@ module Discordrb
284
295
  # @param properties [Hash<Symbol => String>] A list of properties for Discord to use in analytics. The following
285
296
  # keys are recognised:
286
297
  #
287
- # - "$os" (recommended value: the operating system the bot is running on)
288
- # - "$browser" (recommended value: library name)
289
- # - "$device" (recommended value: library name)
290
- # - "$referrer" (recommended value: empty)
291
- # - "$referring_domain" (recommended value: empty)
298
+ # - "os" (recommended value: the operating system the bot is running on)
299
+ # - "browser" (recommended value: library name)
300
+ # - "device" (recommended value: library name)
301
+ # - "referrer" (recommended value: empty)
302
+ # - "referring_domain" (recommended value: empty)
292
303
  #
293
304
  # @param compress [true, false] Whether certain large packets should be compressed using zlib.
294
305
  # @param large_threshold [Integer] The member threshold after which a server counts as large and will have to have
295
306
  # its member list chunked.
296
307
  # @param shard_key [Array(Integer, Integer), nil] The shard key to use for sharding, represented as
297
308
  # [shard_id, num_shards], or nil if the bot should not be sharded.
298
- def send_identify(token, properties, compress, large_threshold, shard_key = nil)
309
+ def send_identify(token, properties, compress, large_threshold, shard_key = nil, intents = ALL_INTENTS)
299
310
  data = {
300
311
  # Don't send a v anymore as it's entirely determined by the URL now
301
312
  token: token,
302
313
  properties: properties,
303
314
  compress: compress,
304
- large_threshold: large_threshold
315
+ large_threshold: large_threshold,
316
+ intents: intents
305
317
  }
306
318
 
307
319
  # Don't include the shard key at all if it is nil as Discord checks for its mere existence
@@ -312,7 +324,7 @@ module Discordrb
312
324
 
313
325
  # Sends a status update packet (op 3). This sets the bot user's status (online/idle/...) and game playing/streaming.
314
326
  # @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
327
+ # @param since [Integer] The Unix timestamp in milliseconds when the status was set. Should only be provided when
316
328
  # `afk` is true.
317
329
  # @param game [Hash<Symbol => Object>, nil] `nil` if no game should be played, or a hash of `:game => "name"` if a
318
330
  # game should be played. The hash can also contain additional attributes for streaming statuses.
@@ -360,7 +372,7 @@ module Discordrb
360
372
  @instant_reconnect = true
361
373
  @should_reconnect = true
362
374
 
363
- close
375
+ close(4000)
364
376
  end
365
377
 
366
378
  # Sends a resume packet (op 6). This replays all events from a previous point specified by its packet sequence. This
@@ -402,12 +414,12 @@ module Discordrb
402
414
 
403
415
  # Sends a custom packet over the connection. This can be useful to implement future yet unimplemented functionality
404
416
  # 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
417
+ # @param opcode [Integer] The opcode the packet should be sent as. Can be one of {Opcodes} or a custom value if
406
418
  # necessary.
407
- # @param packet [Object] Some arbitrary JSON-serialisable data that should be sent as the `d` field.
408
- def send_packet(op, packet)
419
+ # @param packet [Object] Some arbitrary JSON-serializable data that should be sent as the `d` field.
420
+ def send_packet(opcode, packet)
409
421
  data = {
410
- op: op,
422
+ op: opcode,
411
423
  d: packet
412
424
  }
413
425
 
@@ -436,20 +448,18 @@ module Discordrb
436
448
  @heartbeat_thread = Thread.new do
437
449
  Thread.current[:discordrb_name] = 'heartbeat'
438
450
  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)
451
+ # Send a heartbeat if heartbeats are active and either no session exists yet, or an existing session is
452
+ # suspended (e.g. after op7)
453
+ if (@session && !@session.suspended?) || !@session
454
+ sleep @heartbeat_interval
455
+ @bot.raise_heartbeat_event
456
+ heartbeat
457
+ else
458
+ sleep 1
452
459
  end
460
+ rescue StandardError => e
461
+ LOGGER.error('An error occurred while heartbeating!')
462
+ LOGGER.log_exception(e)
453
463
  end
454
464
  end
455
465
  end
@@ -502,7 +512,7 @@ module Discordrb
502
512
  cert_store.set_default_paths
503
513
  ctx.cert_store = cert_store
504
514
  else
505
- ctx.set_params ssl_version: :TLSv1_2
515
+ ctx.set_params ssl_version: :TLSv1_2 # rubocop:disable Naming/VariableNumber
506
516
  end
507
517
 
508
518
  socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
@@ -567,7 +577,7 @@ module Discordrb
567
577
 
568
578
  # We're done! Delegate to the websocket loop
569
579
  websocket_loop
570
- rescue => e
580
+ rescue StandardError => e
571
581
  LOGGER.error('An error occurred while connecting to the websocket!')
572
582
  LOGGER.log_exception(e)
573
583
  end
@@ -584,13 +594,17 @@ module Discordrb
584
594
  unless @socket
585
595
  LOGGER.warn('Socket is nil in websocket_loop! Reconnecting')
586
596
  handle_internal_close('Socket is nil in websocket_loop')
597
+ next
587
598
  end
588
599
 
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 }
600
+ # Get some data from the socket
601
+ begin
602
+ recv_data = @socket.readpartial(4096)
603
+ rescue EOFError
604
+ @pipe_broken = true
605
+ handle_internal_close('Socket EOF in websocket_loop')
606
+ next
607
+ end
594
608
 
595
609
  # Check if we actually got data
596
610
  unless recv_data
@@ -621,7 +635,7 @@ module Discordrb
621
635
  # If the handshake hasn't finished, handle it
622
636
  handle_handshake_data(recv_data)
623
637
  end
624
- rescue => e
638
+ rescue StandardError => e
625
639
  handle_error(e)
626
640
  end
627
641
  end
@@ -645,12 +659,13 @@ module Discordrb
645
659
  ZLIB_SUFFIX = "\x00\x00\xFF\xFF".b.freeze
646
660
 
647
661
  def handle_message(msg)
648
- if @compress_mode == :large
662
+ case @compress_mode
663
+ when :large
649
664
  if msg.byteslice(0) == 'x'
650
665
  # The message is compressed, inflate it
651
666
  msg = Zlib::Inflate.inflate(msg)
652
667
  end
653
- elsif @compress_mode == :stream
668
+ when :stream
654
669
  # Write deflated string to buffer
655
670
  @zlib_reader << msg
656
671
 
@@ -703,6 +718,7 @@ module Discordrb
703
718
 
704
719
  @session = Session.new(data['session_id'])
705
720
  @session.sequence = 0
721
+ @bot.__send__(:notify_ready) if @intents && (@intents & INTENTS[:servers]).zero?
706
722
  when :RESUMED
707
723
  # The RESUMED event is received after a successful op 6 (resume). It does nothing except tell the bot the
708
724
  # connection is initiated (like READY would). Starting with v5, it doesn't set a new heartbeat interval anymore
@@ -750,7 +766,7 @@ module Discordrb
750
766
  LOGGER.debug("Trace: #{packet['d']['_trace']}")
751
767
  LOGGER.debug("Session: #{@session.inspect}")
752
768
 
753
- if @session && @session.should_resume?
769
+ if @session&.should_resume?
754
770
  # Make sure we're sending heartbeats again
755
771
  @session.resume
756
772
 
@@ -773,22 +789,41 @@ module Discordrb
773
789
  handle_close(e)
774
790
  end
775
791
 
792
+ # Close codes that are unrecoverable, after which we should not try to reconnect.
793
+ # - 4003: Not authenticated. How did this happen?
794
+ # - 4004: Authentication failed. Token was wrong, nothing we can do.
795
+ # - 4011: Sharding required. Currently requires developer intervention.
796
+ # - 4014: Use of disabled privileged intents.
797
+ FATAL_CLOSE_CODES = [4003, 4004, 4011, 4014].freeze
798
+
776
799
  def handle_close(e)
800
+ @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
801
+
777
802
  if e.respond_to? :code
778
803
  # It is a proper close frame we're dealing with, print reason and message to console
779
804
  LOGGER.error('Websocket close frame received!')
780
805
  LOGGER.error("Code: #{e.code}")
781
806
  LOGGER.error("Message: #{e.data}")
807
+
808
+ if e.code == 4014
809
+ LOGGER.error(<<~ERROR)
810
+ You attempted to identify with privileged intents that your bot is not authorized to use
811
+ Please enable the privileged intents on the bot page of your application on the discord developer page.
812
+ Read more here https://discord.com/developers/docs/topics/gateway#privileged-intents
813
+ ERROR
814
+ end
815
+
816
+ @should_reconnect = false if FATAL_CLOSE_CODES.include?(e.code)
782
817
  elsif e.is_a? Exception
783
818
  # Log the exception
784
819
  LOGGER.error('The websocket connection has closed due to an error!')
785
820
  LOGGER.log_exception(e)
786
821
  else
787
- LOGGER.error("The websocket connection has closed: #{e.inspect}")
822
+ LOGGER.error("The websocket connection has closed: #{e&.inspect || '(no information)'}")
788
823
  end
789
824
  end
790
825
 
791
- def send(data, type = :text)
826
+ def send(data, type = :text, code = nil)
792
827
  LOGGER.out(data)
793
828
 
794
829
  unless @handshaked && !@closed
@@ -797,40 +832,33 @@ module Discordrb
797
832
  end
798
833
 
799
834
  # Create the frame we're going to send
800
- frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @handshake.version)
835
+ frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @handshake.version, code: code)
801
836
 
802
837
  # Try to send it
803
838
  begin
804
839
  @socket.write frame.to_s
805
- rescue => e
840
+ rescue StandardError => e
806
841
  # There has been an error!
807
842
  @pipe_broken = true
808
843
  handle_internal_close(e)
809
844
  end
810
845
  end
811
846
 
812
- def close(no_sync = false)
847
+ def close(code = 1000)
813
848
  # If we're already closed, there's no need to do anything - return
814
849
  return if @closed
815
850
 
816
851
  # Suspend the session so we don't send heartbeats
817
- @session.suspend if @session
852
+ @session&.suspend
818
853
 
819
854
  # Send a close frame (if we can)
820
- send nil, :close unless @pipe_broken
855
+ send nil, :close, code unless @pipe_broken
821
856
 
822
857
  # 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
858
+ @closed = true
831
859
 
832
860
  # Close the socket if possible
833
- @socket.close if @socket
861
+ @socket&.close
834
862
  @socket = nil
835
863
 
836
864
  # Make sure we do necessary things as soon as we're closed
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # Mixin for objects that have IDs
5
+ module IDObject
6
+ # @return [Integer] the ID which uniquely identifies this object across Discord.
7
+ attr_reader :id
8
+ alias_method :resolve_id, :id
9
+ alias_method :hash, :id
10
+
11
+ # ID based comparison
12
+ def ==(other)
13
+ Discordrb.id_compare(@id, other)
14
+ end
15
+
16
+ alias_method :eql?, :==
17
+
18
+ # Estimates the time this object was generated on based on the beginning of the ID. This is fairly accurate but
19
+ # shouldn't be relied on as Discord might change its algorithm at any time
20
+ # @return [Time] when this object was created at
21
+ def creation_time
22
+ # Milliseconds
23
+ ms = (@id >> 22) + DISCORD_EPOCH
24
+ Time.at(ms / 1000.0)
25
+ end
26
+
27
+ # Creates an artificial snowflake at the given point in time. Useful for comparing against.
28
+ # @param time [Time] The time the snowflake should represent.
29
+ # @return [Integer] a snowflake with the timestamp data as the given time
30
+ def self.synthesise(time)
31
+ ms = (time.to_f * 1000).to_i
32
+ (ms - DISCORD_EPOCH) << 22
33
+ end
34
+
35
+ class << self
36
+ alias_method :synthesize, :synthesise
37
+ end
38
+ end
39
+ end
@@ -49,7 +49,7 @@ module Discordrb::Light
49
49
  # Twitch account connection of the server owner).
50
50
  attr_reader :server_connection
51
51
 
52
- # @return [Connection] the connection integrated with the server (i. e. your connection)
52
+ # @return [Connection] the connection integrated with the server (i.e. your connection)
53
53
  attr_reader :integrated_connection
54
54
 
55
55
  # @!visibility private
@@ -6,7 +6,7 @@ require 'discordrb/api/user'
6
6
  require 'discordrb/light/data'
7
7
  require 'discordrb/light/integrations'
8
8
 
9
- # This module contains classes to allow connections to bots without a connection to the gateway socket, i. e. bots
9
+ # This module contains classes to allow connections to bots without a connection to the gateway socket, i.e. bots
10
10
  # that only use the REST part of the API.
11
11
  module Discordrb::Light
12
12
  # A bot that only uses the REST part of the API. Hierarchically unrelated to the regular {Discordrb::Bot}. Useful to
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Discordrb
4
4
  # The format log timestamps should be in, in strftime format
5
- LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'.freeze
5
+ LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'
6
6
 
7
7
  # Logs debug messages
8
8
  class Logger
@@ -18,7 +18,7 @@ module Discordrb
18
18
  # Creates a new logger.
19
19
  # @param fancy [true, false] Whether this logger uses fancy mode (ANSI escape codes to make the output colourful)
20
20
  # @param streams [Array<IO>, Array<#puts & #flush>] the streams the logger should write to.
21
- def initialize(fancy = false, streams = [STDOUT])
21
+ def initialize(fancy = false, streams = [$stdout])
22
22
  @fancy = fancy
23
23
  self.mode = :normal
24
24
 
@@ -38,10 +38,10 @@ module Discordrb
38
38
  }.freeze
39
39
 
40
40
  # The ANSI format code that resets formatting
41
- FORMAT_RESET = "\u001B[0m".freeze
41
+ FORMAT_RESET = "\u001B[0m"
42
42
 
43
43
  # The ANSI format code that makes something bold
44
- FORMAT_BOLD = "\u001B[1m".freeze
44
+ FORMAT_BOLD = "\u001B[1m"
45
45
 
46
46
  MODES.each do |mode, hash|
47
47
  define_method(mode) do |message|