discordrb 3.3.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
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|