discordrb 3.3.0 → 3.4.3

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