discordrb 3.3.0 → 3.4.3

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +126 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
  5. data/.github/pull_request_template.md +37 -0
  6. data/.rubocop.yml +34 -37
  7. data/.travis.yml +5 -6
  8. data/CHANGELOG.md +504 -347
  9. data/Gemfile +2 -0
  10. data/LICENSE.txt +1 -1
  11. data/README.md +61 -79
  12. data/Rakefile +2 -0
  13. data/bin/console +1 -0
  14. data/discordrb-webhooks.gemspec +6 -6
  15. data/discordrb.gemspec +18 -18
  16. data/lib/discordrb/allowed_mentions.rb +36 -0
  17. data/lib/discordrb/api/channel.rb +62 -39
  18. data/lib/discordrb/api/invite.rb +3 -3
  19. data/lib/discordrb/api/server.rb +57 -50
  20. data/lib/discordrb/api/user.rb +9 -8
  21. data/lib/discordrb/api/webhook.rb +6 -6
  22. data/lib/discordrb/api.rb +40 -15
  23. data/lib/discordrb/await.rb +0 -1
  24. data/lib/discordrb/bot.rb +175 -73
  25. data/lib/discordrb/cache.rb +4 -2
  26. data/lib/discordrb/colour_rgb.rb +43 -0
  27. data/lib/discordrb/commands/command_bot.rb +30 -9
  28. data/lib/discordrb/commands/container.rb +20 -23
  29. data/lib/discordrb/commands/parser.rb +18 -18
  30. data/lib/discordrb/commands/rate_limiter.rb +3 -2
  31. data/lib/discordrb/container.rb +77 -17
  32. data/lib/discordrb/data/activity.rb +271 -0
  33. data/lib/discordrb/data/application.rb +50 -0
  34. data/lib/discordrb/data/attachment.rb +56 -0
  35. data/lib/discordrb/data/audit_logs.rb +345 -0
  36. data/lib/discordrb/data/channel.rb +849 -0
  37. data/lib/discordrb/data/embed.rb +251 -0
  38. data/lib/discordrb/data/emoji.rb +82 -0
  39. data/lib/discordrb/data/integration.rb +83 -0
  40. data/lib/discordrb/data/invite.rb +137 -0
  41. data/lib/discordrb/data/member.rb +297 -0
  42. data/lib/discordrb/data/message.rb +334 -0
  43. data/lib/discordrb/data/overwrite.rb +102 -0
  44. data/lib/discordrb/data/profile.rb +91 -0
  45. data/lib/discordrb/data/reaction.rb +33 -0
  46. data/lib/discordrb/data/recipient.rb +34 -0
  47. data/lib/discordrb/data/role.rb +191 -0
  48. data/lib/discordrb/data/server.rb +1002 -0
  49. data/lib/discordrb/data/user.rb +204 -0
  50. data/lib/discordrb/data/voice_region.rb +45 -0
  51. data/lib/discordrb/data/voice_state.rb +41 -0
  52. data/lib/discordrb/data/webhook.rb +145 -0
  53. data/lib/discordrb/data.rb +25 -4180
  54. data/lib/discordrb/errors.rb +2 -1
  55. data/lib/discordrb/events/bans.rb +7 -5
  56. data/lib/discordrb/events/channels.rb +2 -0
  57. data/lib/discordrb/events/guilds.rb +16 -9
  58. data/lib/discordrb/events/invites.rb +125 -0
  59. data/lib/discordrb/events/members.rb +6 -2
  60. data/lib/discordrb/events/message.rb +69 -27
  61. data/lib/discordrb/events/presence.rb +14 -4
  62. data/lib/discordrb/events/raw.rb +1 -3
  63. data/lib/discordrb/events/reactions.rb +49 -3
  64. data/lib/discordrb/events/typing.rb +6 -4
  65. data/lib/discordrb/events/voice_server_update.rb +47 -0
  66. data/lib/discordrb/events/voice_state_update.rb +15 -10
  67. data/lib/discordrb/events/webhooks.rb +9 -6
  68. data/lib/discordrb/gateway.rb +72 -57
  69. data/lib/discordrb/id_object.rb +39 -0
  70. data/lib/discordrb/light/integrations.rb +1 -1
  71. data/lib/discordrb/light/light_bot.rb +1 -1
  72. data/lib/discordrb/logger.rb +4 -4
  73. data/lib/discordrb/paginator.rb +57 -0
  74. data/lib/discordrb/permissions.rb +103 -8
  75. data/lib/discordrb/version.rb +1 -1
  76. data/lib/discordrb/voice/encoder.rb +16 -7
  77. data/lib/discordrb/voice/network.rb +84 -43
  78. data/lib/discordrb/voice/sodium.rb +96 -0
  79. data/lib/discordrb/voice/voice_bot.rb +34 -26
  80. data/lib/discordrb.rb +73 -0
  81. metadata +98 -60
  82. /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
@@ -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
@@ -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|
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # Utility class for wrapping paginated endpoints. It is [Enumerable](https://ruby-doc.org/core-2.5.1/Enumerable.html),
5
+ # similar to an `Array`, so most of the same methods can be used to filter the results of the request
6
+ # that it wraps. If you simply want an array of all of the results, `#to_a` can be called.
7
+ class Paginator
8
+ include Enumerable
9
+
10
+ # Creates a new {Paginator}
11
+ # @param limit [Integer] the maximum number of items to request before stopping
12
+ # @param direction [:up, :down] the order in which results are returned in
13
+ # @yield [Array, nil] the last page of results, or nil if this is the first iteration.
14
+ # This should be used to request the next page of results.
15
+ # @yieldreturn [Array] the next page of results
16
+ def initialize(limit, direction, &block)
17
+ @count = 0
18
+ @limit = limit
19
+ @direction = direction
20
+ @block = block
21
+ end
22
+
23
+ # Yields every item produced by the wrapped request, until it returns
24
+ # no more results or the configured `limit` is reached.
25
+ def each
26
+ last_page = nil
27
+ until limit_check
28
+ page = @block.call(last_page)
29
+ return if page.empty?
30
+
31
+ enumerator = case @direction
32
+ when :down
33
+ page.each
34
+ when :up
35
+ page.reverse_each
36
+ end
37
+
38
+ enumerator.each do |item|
39
+ yield item
40
+ @count += 1
41
+ break if limit_check
42
+ end
43
+
44
+ last_page = page
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # Whether the paginator limit has been exceeded
51
+ def limit_check
52
+ return false if @limit.nil?
53
+
54
+ @count >= @limit
55
+ end
56
+ end
57
+ end