onyxcord 1.1.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 (133) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/CONTRIBUTING.md +13 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  8. data/.github/pull_request_template.md +37 -0
  9. data/.github/workflows/ci.yml +78 -0
  10. data/.github/workflows/codeql.yml +65 -0
  11. data/.github/workflows/deploy.yml +54 -0
  12. data/.github/workflows/release.yml +51 -0
  13. data/.gitignore +16 -0
  14. data/.markdownlint.json +4 -0
  15. data/.overcommit.yml +7 -0
  16. data/.rspec +2 -0
  17. data/.rubocop.yml +129 -0
  18. data/.yardopts +1 -0
  19. data/CHANGELOG.md +0 -0
  20. data/Gemfile +7 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +305 -0
  23. data/Rakefile +17 -0
  24. data/bin/console +15 -0
  25. data/bin/setup +7 -0
  26. data/lib/onyxcord/allowed_mentions.rb +43 -0
  27. data/lib/onyxcord/api/application.rb +316 -0
  28. data/lib/onyxcord/api/channel.rb +700 -0
  29. data/lib/onyxcord/api/interaction.rb +67 -0
  30. data/lib/onyxcord/api/invite.rb +44 -0
  31. data/lib/onyxcord/api/server.rb +775 -0
  32. data/lib/onyxcord/api/user.rb +158 -0
  33. data/lib/onyxcord/api/webhook.rb +163 -0
  34. data/lib/onyxcord/api.rb +335 -0
  35. data/lib/onyxcord/await.rb +51 -0
  36. data/lib/onyxcord/bot.rb +1971 -0
  37. data/lib/onyxcord/cache.rb +326 -0
  38. data/lib/onyxcord/colour_rgb.rb +43 -0
  39. data/lib/onyxcord/commands/command_bot.rb +511 -0
  40. data/lib/onyxcord/commands/container.rb +112 -0
  41. data/lib/onyxcord/commands/events.rb +11 -0
  42. data/lib/onyxcord/commands/parser.rb +327 -0
  43. data/lib/onyxcord/commands/rate_limiter.rb +144 -0
  44. data/lib/onyxcord/configuration.rb +125 -0
  45. data/lib/onyxcord/container.rb +988 -0
  46. data/lib/onyxcord/data/activity.rb +271 -0
  47. data/lib/onyxcord/data/application.rb +341 -0
  48. data/lib/onyxcord/data/attachment.rb +91 -0
  49. data/lib/onyxcord/data/audit_logs.rb +438 -0
  50. data/lib/onyxcord/data/avatar_decoration.rb +26 -0
  51. data/lib/onyxcord/data/call.rb +22 -0
  52. data/lib/onyxcord/data/channel.rb +1355 -0
  53. data/lib/onyxcord/data/channel_tag.rb +69 -0
  54. data/lib/onyxcord/data/collectibles.rb +47 -0
  55. data/lib/onyxcord/data/component.rb +583 -0
  56. data/lib/onyxcord/data/embed.rb +258 -0
  57. data/lib/onyxcord/data/emoji.rb +123 -0
  58. data/lib/onyxcord/data/install_params.rb +24 -0
  59. data/lib/onyxcord/data/integration.rb +144 -0
  60. data/lib/onyxcord/data/interaction.rb +1141 -0
  61. data/lib/onyxcord/data/invite.rb +137 -0
  62. data/lib/onyxcord/data/member.rb +528 -0
  63. data/lib/onyxcord/data/message.rb +612 -0
  64. data/lib/onyxcord/data/message_activity.rb +41 -0
  65. data/lib/onyxcord/data/overwrite.rb +109 -0
  66. data/lib/onyxcord/data/poll.rb +365 -0
  67. data/lib/onyxcord/data/primary_server.rb +60 -0
  68. data/lib/onyxcord/data/profile.rb +79 -0
  69. data/lib/onyxcord/data/reaction.rb +64 -0
  70. data/lib/onyxcord/data/recipient.rb +34 -0
  71. data/lib/onyxcord/data/role.rb +449 -0
  72. data/lib/onyxcord/data/role_connection_data.rb +69 -0
  73. data/lib/onyxcord/data/role_subscription.rb +41 -0
  74. data/lib/onyxcord/data/scheduled_event.rb +513 -0
  75. data/lib/onyxcord/data/server.rb +1614 -0
  76. data/lib/onyxcord/data/server_preview.rb +68 -0
  77. data/lib/onyxcord/data/snapshot.rb +112 -0
  78. data/lib/onyxcord/data/team.rb +98 -0
  79. data/lib/onyxcord/data/timestamp.rb +69 -0
  80. data/lib/onyxcord/data/user.rb +324 -0
  81. data/lib/onyxcord/data/voice_region.rb +46 -0
  82. data/lib/onyxcord/data/voice_state.rb +41 -0
  83. data/lib/onyxcord/data/webhook.rb +238 -0
  84. data/lib/onyxcord/data.rb +57 -0
  85. data/lib/onyxcord/errors.rb +246 -0
  86. data/lib/onyxcord/event_executor.rb +80 -0
  87. data/lib/onyxcord/events/await.rb +48 -0
  88. data/lib/onyxcord/events/bans.rb +60 -0
  89. data/lib/onyxcord/events/channels.rb +225 -0
  90. data/lib/onyxcord/events/generic.rb +129 -0
  91. data/lib/onyxcord/events/guilds.rb +269 -0
  92. data/lib/onyxcord/events/integrations.rb +100 -0
  93. data/lib/onyxcord/events/interactions.rb +624 -0
  94. data/lib/onyxcord/events/invites.rb +127 -0
  95. data/lib/onyxcord/events/lifetime.rb +31 -0
  96. data/lib/onyxcord/events/members.rb +110 -0
  97. data/lib/onyxcord/events/message.rb +399 -0
  98. data/lib/onyxcord/events/polls.rb +118 -0
  99. data/lib/onyxcord/events/presence.rb +131 -0
  100. data/lib/onyxcord/events/raw.rb +74 -0
  101. data/lib/onyxcord/events/reactions.rb +218 -0
  102. data/lib/onyxcord/events/roles.rb +87 -0
  103. data/lib/onyxcord/events/scheduled_events.rb +171 -0
  104. data/lib/onyxcord/events/threads.rb +100 -0
  105. data/lib/onyxcord/events/typing.rb +73 -0
  106. data/lib/onyxcord/events/voice_server_update.rb +48 -0
  107. data/lib/onyxcord/events/voice_state_update.rb +106 -0
  108. data/lib/onyxcord/events/webhooks.rb +65 -0
  109. data/lib/onyxcord/gateway.rb +890 -0
  110. data/lib/onyxcord/id_object.rb +39 -0
  111. data/lib/onyxcord/light/data.rb +62 -0
  112. data/lib/onyxcord/light/integrations.rb +73 -0
  113. data/lib/onyxcord/light/light_bot.rb +58 -0
  114. data/lib/onyxcord/light.rb +8 -0
  115. data/lib/onyxcord/logger.rb +120 -0
  116. data/lib/onyxcord/message_components.rb +70 -0
  117. data/lib/onyxcord/paginator.rb +60 -0
  118. data/lib/onyxcord/permissions.rb +255 -0
  119. data/lib/onyxcord/rate_limiter/gateway.rb +42 -0
  120. data/lib/onyxcord/rate_limiter/rest.rb +89 -0
  121. data/lib/onyxcord/version.rb +7 -0
  122. data/lib/onyxcord/voice/encoder.rb +115 -0
  123. data/lib/onyxcord/voice/network.rb +380 -0
  124. data/lib/onyxcord/voice/opcodes.rb +29 -0
  125. data/lib/onyxcord/voice/sodium.rb +157 -0
  126. data/lib/onyxcord/voice/timer.rb +19 -0
  127. data/lib/onyxcord/voice/voice_bot.rb +386 -0
  128. data/lib/onyxcord/webhooks.rb +14 -0
  129. data/lib/onyxcord/websocket.rb +62 -0
  130. data/lib/onyxcord.rb +180 -0
  131. data/onyxcord-webhooks.gemspec +30 -0
  132. data/onyxcord.gemspec +50 -0
  133. metadata +421 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
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
+ OnyxCord.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
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/data'
4
+
5
+ module OnyxCord::Light
6
+ # Represents the bot account used for the light bot, but without any methods to change anything.
7
+ class LightProfile
8
+ include OnyxCord::IDObject
9
+ include OnyxCord::UserAttributes
10
+
11
+ # @!visibility private
12
+ def initialize(data, bot)
13
+ @bot = bot
14
+
15
+ @username = data['username']
16
+ @id = data['id'].to_i
17
+ @discriminator = data['discriminator']
18
+ @avatar_id = data['avatar']
19
+
20
+ @bot_account = false
21
+ @bot_account = true if data['bot']
22
+
23
+ @verified = data['verified']
24
+
25
+ @email = data['email']
26
+ end
27
+ end
28
+
29
+ # A server that only has an icon, a name, and an ID associated with it, like for example an integration's server.
30
+ class UltraLightServer
31
+ include OnyxCord::IDObject
32
+ include OnyxCord::ServerAttributes
33
+
34
+ # @!visibility private
35
+ def initialize(data, bot)
36
+ @bot = bot
37
+
38
+ @id = data['id'].to_i
39
+
40
+ @name = data['name']
41
+ @icon_id = data['icon']
42
+ end
43
+ end
44
+
45
+ # Represents a light server which only has a fraction of the properties of any other server.
46
+ class LightServer < UltraLightServer
47
+ # @return [true, false] whether or not the LightBot this server belongs to is the owner of the server.
48
+ attr_reader :bot_is_owner
49
+ alias_method :bot_is_owner?, :bot_is_owner
50
+
51
+ # @return [OnyxCord::Permissions] the permissions the LightBot has on this server
52
+ attr_reader :bot_permissions
53
+
54
+ # @!visibility private
55
+ def initialize(data, bot)
56
+ super(data, bot)
57
+
58
+ @bot_is_owner = data['owner']
59
+ @bot_permissions = OnyxCord::Permissions.new(data['permissions'])
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/data'
4
+ require 'onyxcord/light/data'
5
+
6
+ module OnyxCord::Light
7
+ # A connection of your Discord account to a particular other service (currently, Twitch and YouTube)
8
+ class Connection
9
+ # @return [Symbol] what type of connection this is (either :twitch or :youtube currently)
10
+ attr_reader :type
11
+
12
+ # @return [true, false] whether this connection is revoked
13
+ attr_reader :revoked
14
+ alias_method :revoked?, :revoked
15
+
16
+ # @return [String] the name of the connected account
17
+ attr_reader :name
18
+
19
+ # @return [String] the ID of the connected account
20
+ attr_reader :id
21
+
22
+ # @return [Array<Integration>] the integrations associated with this connection
23
+ attr_reader :integrations
24
+
25
+ # @!visibility private
26
+ def initialize(data, bot)
27
+ @bot = bot
28
+
29
+ @revoked = data['revoked']
30
+ @type = data['type'].to_sym
31
+ @name = data['name']
32
+ @id = data['id']
33
+
34
+ @integrations = data['integrations'].map { |e| Integration.new(e, self, bot) }
35
+ end
36
+ end
37
+
38
+ # An integration of a connection into a particular server, for example being a member of a subscriber-only Twitch
39
+ # server.
40
+ class Integration
41
+ include OnyxCord::IDObject
42
+
43
+ # @return [UltraLightServer] the server associated with this integration
44
+ attr_reader :server
45
+
46
+ # @note The connection returned by this method will have no integrations itself, as Discord doesn't provide that
47
+ # data. Also, it will always be considered not revoked.
48
+ # @return [Connection] the server's underlying connection (for a Twitch subscriber-only server, it would be the
49
+ # Twitch account connection of the server owner).
50
+ attr_reader :server_connection
51
+
52
+ # @return [Connection] the connection integrated with the server (i.e. your connection)
53
+ attr_reader :integrated_connection
54
+
55
+ # @!visibility private
56
+ def initialize(data, integrated, bot)
57
+ @bot = bot
58
+ @integrated_connection = integrated
59
+
60
+ @server = UltraLightServer.new(data['guild'], bot)
61
+
62
+ # Restructure the given data so we can reuse the Connection initializer
63
+ restructured = {}
64
+
65
+ restructured['type'] = data['type']
66
+ restructured['id'] = data['account']['id']
67
+ restructured['name'] = data['account']['name']
68
+ restructured['integrations'] = []
69
+
70
+ @server_connection = Connection.new(restructured, bot)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/api'
4
+ require 'onyxcord/api/invite'
5
+ require 'onyxcord/api/user'
6
+ require 'onyxcord/light/data'
7
+ require 'onyxcord/light/integrations'
8
+
9
+ # This module contains classes to allow connections to bots without a connection to the gateway socket, i.e. bots
10
+ # that only use the REST part of the API.
11
+ module OnyxCord::Light
12
+ # A bot that only uses the REST part of the API. Hierarchically unrelated to the regular {OnyxCord::Bot}. Useful to
13
+ # make applications integrated to Discord over OAuth, for example.
14
+ class LightBot
15
+ # Create a new LightBot. This does no networking yet, all networking is done by the methods on this class.
16
+ # @param token [String] The token that should be used to authenticate to Discord. Can be an OAuth token or a regular
17
+ # user account token.
18
+ def initialize(token)
19
+ if token.respond_to? :token
20
+ # Parse AccessTokens from the OAuth2 gem
21
+ token = token.token
22
+ end
23
+
24
+ unless token.include? '.'
25
+ # Discord user/bot tokens always contain two dots, so if there's none we can assume it's an OAuth token.
26
+ token = "Bearer #{token}" # OAuth tokens have to be prefixed with 'Bearer' for Discord to be able to use them
27
+ end
28
+
29
+ @token = token
30
+ end
31
+
32
+ # @return [LightProfile] the details of the user this bot is connected to.
33
+ def profile
34
+ response = OnyxCord::API::User.profile(@token)
35
+ LightProfile.new(JSON.parse(response), self)
36
+ end
37
+
38
+ # @return [Array<LightServer>] the servers this bot is connected to.
39
+ def servers
40
+ response = OnyxCord::API::User.servers(@token)
41
+ JSON.parse(response).map { |e| LightServer.new(e, self) }
42
+ end
43
+
44
+ # Joins a server using an instant invite.
45
+ # @param code [String] The code part of the invite (for example 0cDvIgU2voWn4BaD if the invite URL is
46
+ # https://discord.gg/0cDvIgU2voWn4BaD)
47
+ def join(code)
48
+ OnyxCord::API::Invite.accept(@token, code)
49
+ end
50
+
51
+ # Gets the connections associated with this account.
52
+ # @return [Array<Connection>] this account's connections.
53
+ def connections
54
+ response = OnyxCord::API::User.connections(@token)
55
+ JSON.parse(response).map { |e| Connection.new(e, self) }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/light/light_bot'
4
+
5
+ # This module contains classes to allow connections to bots without a connection to the gateway socket, i.e. bots
6
+ # that only use the REST part of the API.
7
+ module OnyxCord::Light
8
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # The format log timestamps should be in, in strftime format
5
+ LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'
6
+
7
+ # Logs debug messages
8
+ class Logger
9
+ # @return [true, false] whether this logger is in extra-fancy mode!
10
+ attr_writer :fancy
11
+
12
+ # @return [String, nil] The bot token to be redacted or nil if it shouldn't.
13
+ attr_writer :token
14
+
15
+ # @return [Array<IO>, Array<#puts & #flush>] the streams the logger should write to.
16
+ attr_accessor :streams
17
+
18
+ # Creates a new logger.
19
+ # @param fancy [true, false] Whether this logger uses fancy mode (ANSI escape codes to make the output colourful)
20
+ # @param streams [Array<IO>, Array<#puts & #flush>] the streams the logger should write to.
21
+ def initialize(fancy = false, streams = [$stdout])
22
+ @fancy = fancy
23
+ self.mode = :normal
24
+
25
+ @streams = streams
26
+ end
27
+
28
+ # The modes this logger can have. This is probably useless unless you want to write your own Logger
29
+ MODES = {
30
+ debug: { long: 'DEBUG', short: 'D', format_code: '' },
31
+ good: { long: 'GOOD', short: '✓', format_code: "\u001B[32m" }, # green
32
+ info: { long: 'INFO', short: 'i', format_code: '' },
33
+ warn: { long: 'WARN', short: '!', format_code: "\u001B[33m" }, # yellow
34
+ error: { long: 'ERROR', short: '✗', format_code: "\u001B[31m" }, # red
35
+ out: { long: 'OUT', short: '→', format_code: "\u001B[36m" }, # cyan
36
+ in: { long: 'IN', short: '←', format_code: "\u001B[35m" }, # purple
37
+ ratelimit: { long: 'RATELIMIT', short: 'R', format_code: "\u001B[41m" } # red background
38
+ }.freeze
39
+
40
+ # The ANSI format code that resets formatting
41
+ FORMAT_RESET = "\u001B[0m"
42
+
43
+ # The ANSI format code that makes something bold
44
+ FORMAT_BOLD = "\u001B[1m"
45
+
46
+ MODES.each do |mode, hash|
47
+ define_method(mode) do |message|
48
+ write(message.to_s, hash) if @enabled_modes.include? mode
49
+ end
50
+ end
51
+
52
+ # Sets the logging mode to :debug
53
+ # @param value [true, false] Whether debug mode should be on. If it is off the mode will be set to :normal.
54
+ def debug=(value)
55
+ self.mode = value ? :debug : :normal
56
+ end
57
+
58
+ # Sets the logging mode
59
+ # Possible modes are:
60
+ # * :debug logs everything
61
+ # * :verbose logs everything except for debug messages
62
+ # * :normal logs useful information, warnings and errors
63
+ # * :quiet only logs warnings and errors
64
+ # * :silent logs nothing
65
+ # @param value [Symbol] What logging mode to use
66
+ def mode=(value)
67
+ case value
68
+ when :debug
69
+ @enabled_modes = %i[debug good info warn error out in ratelimit]
70
+ when :verbose
71
+ @enabled_modes = %i[good info warn error out in ratelimit]
72
+ when :normal
73
+ @enabled_modes = %i[info warn error ratelimit]
74
+ when :quiet
75
+ @enabled_modes = %i[warn error]
76
+ when :silent
77
+ @enabled_modes = %i[]
78
+ end
79
+ end
80
+
81
+ # Logs an exception to the console.
82
+ # @param e [Exception] The exception to log.
83
+ def log_exception(e)
84
+ error("Exception: #{e.inspect}")
85
+ e.backtrace.each { |line| error(line) }
86
+ end
87
+
88
+ private
89
+
90
+ def write(message, mode)
91
+ thread_name = Thread.current[:onyxcord_name]
92
+ timestamp = Time.now.strftime(LOG_TIMESTAMP_FORMAT)
93
+
94
+ # Redact token if set
95
+ log = if @token && @token != ''
96
+ message.to_s.gsub(@token, 'REDACTED_TOKEN')
97
+ else
98
+ message.to_s
99
+ end
100
+
101
+ @streams.each do |stream|
102
+ if @fancy && !stream.is_a?(File)
103
+ fancy_write(stream, log, mode, thread_name, timestamp)
104
+ else
105
+ simple_write(stream, log, mode, thread_name, timestamp)
106
+ end
107
+ end
108
+ end
109
+
110
+ def fancy_write(stream, message, mode, thread_name, timestamp)
111
+ stream.puts "#{timestamp} #{FORMAT_BOLD}#{thread_name.ljust(16)}#{FORMAT_RESET} #{mode[:format_code]}#{mode[:short]}#{FORMAT_RESET} #{message}"
112
+ stream.flush
113
+ end
114
+
115
+ def simple_write(stream, message, mode, thread_name, timestamp)
116
+ stream.puts "[#{mode[:long]} : #{thread_name} @ #{timestamp}] #{message}"
117
+ stream.flush
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # Helpers for Discord message component payloads.
5
+ module MessageComponents
6
+ # Discord message flag for the Components V2 layout system.
7
+ IS_COMPONENTS_V2 = 1 << 15
8
+
9
+ # Component types that only exist in the Components V2 message system.
10
+ V2_COMPONENT_TYPES = [9, 10, 11, 12, 13, 14, 17].freeze
11
+
12
+ module_function
13
+
14
+ def payload(components)
15
+ case components
16
+ when nil
17
+ []
18
+ when Array
19
+ components.map { |component| component.respond_to?(:to_h) ? component.to_h : component }
20
+ when Hash
21
+ [components]
22
+ else
23
+ if components.respond_to?(:to_a)
24
+ components.to_a
25
+ elsif components.respond_to?(:to_h)
26
+ [components.to_h]
27
+ else
28
+ Array(components)
29
+ end
30
+ end
31
+ end
32
+
33
+ def components_v2?(components)
34
+ payload(components).any? { |component| component_v2?(component) }
35
+ end
36
+
37
+ def apply_v2_flag(flags, components, force: false)
38
+ return flags unless force || components_v2?(components)
39
+
40
+ flag_value(flags) | IS_COMPONENTS_V2
41
+ end
42
+
43
+ def component_v2?(component)
44
+ return false if component.nil?
45
+
46
+ component = component.to_h if component.respond_to?(:to_h)
47
+ return false unless component.is_a?(Hash)
48
+
49
+ type = component[:type] || component['type']
50
+ return true if V2_COMPONENT_TYPES.include?(type)
51
+
52
+ children = component[:components] || component['components']
53
+ return true if children && components_v2?(children)
54
+
55
+ accessory = component[:accessory] || component['accessory']
56
+ component_v2?(accessory)
57
+ end
58
+
59
+ def flag_value(flags)
60
+ case flags
61
+ when nil, :undef
62
+ 0
63
+ when Array
64
+ flags.map { |flag| flag.respond_to?(:to_i) ? flag.to_i : 0 }.reduce(0, &:|)
65
+ else
66
+ flags.respond_to?(:to_i) ? flags.to_i : 0
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
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
+ # @return [Integer] the total amount of elements that have been fetched so far.
11
+ attr_reader :amount_fetched
12
+
13
+ # Creates a new {Paginator}
14
+ # @param limit [Integer] the maximum number of items to request before stopping
15
+ # @param direction [:up, :down] the order in which results are returned in
16
+ # @yield [Array, nil] the last page of results, or nil if this is the first iteration.
17
+ # This should be used to request the next page of results.
18
+ # @yieldreturn [Array] the next page of results
19
+ def initialize(limit, direction, &block)
20
+ @amount_fetched = 0
21
+ @limit = limit
22
+ @direction = direction
23
+ @block = block
24
+ end
25
+
26
+ # Yields every item produced by the wrapped request, until it returns
27
+ # no more results or the configured `limit` is reached.
28
+ def each
29
+ last_page = nil
30
+ until limit_exceeded?
31
+ page = @block.call(last_page)
32
+ return if page.empty?
33
+
34
+ enumerator = case @direction
35
+ when :down
36
+ page.each
37
+ when :up
38
+ page.reverse_each
39
+ end
40
+
41
+ enumerator.each do |item|
42
+ yield item
43
+ @amount_fetched += 1
44
+ break if limit_exceeded?
45
+ end
46
+
47
+ last_page = page
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # Whether the paginator limit has been exceeded
54
+ def limit_exceeded?
55
+ return false if @limit.nil?
56
+
57
+ @amount_fetched >= @limit
58
+ end
59
+ end
60
+ end