discordrb 3.3.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +152 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  5. data/.github/pull_request_template.md +37 -0
  6. data/.github/workflows/codeql.yml +65 -0
  7. data/.markdownlint.json +4 -0
  8. data/.rubocop.yml +39 -36
  9. data/CHANGELOG.md +874 -552
  10. data/Gemfile +2 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +80 -86
  13. data/Rakefile +2 -0
  14. data/bin/console +1 -0
  15. data/discordrb-webhooks.gemspec +9 -6
  16. data/discordrb.gemspec +21 -18
  17. data/lib/discordrb/allowed_mentions.rb +36 -0
  18. data/lib/discordrb/api/application.rb +202 -0
  19. data/lib/discordrb/api/channel.rb +236 -47
  20. data/lib/discordrb/api/interaction.rb +54 -0
  21. data/lib/discordrb/api/invite.rb +5 -5
  22. data/lib/discordrb/api/server.rb +94 -66
  23. data/lib/discordrb/api/user.rb +17 -11
  24. data/lib/discordrb/api/webhook.rb +63 -6
  25. data/lib/discordrb/api.rb +55 -16
  26. data/lib/discordrb/await.rb +0 -1
  27. data/lib/discordrb/bot.rb +480 -93
  28. data/lib/discordrb/cache.rb +31 -24
  29. data/lib/discordrb/colour_rgb.rb +43 -0
  30. data/lib/discordrb/commands/command_bot.rb +35 -12
  31. data/lib/discordrb/commands/container.rb +21 -24
  32. data/lib/discordrb/commands/parser.rb +20 -20
  33. data/lib/discordrb/commands/rate_limiter.rb +4 -3
  34. data/lib/discordrb/container.rb +209 -20
  35. data/lib/discordrb/data/activity.rb +271 -0
  36. data/lib/discordrb/data/application.rb +50 -0
  37. data/lib/discordrb/data/attachment.rb +71 -0
  38. data/lib/discordrb/data/audit_logs.rb +345 -0
  39. data/lib/discordrb/data/channel.rb +993 -0
  40. data/lib/discordrb/data/component.rb +229 -0
  41. data/lib/discordrb/data/embed.rb +251 -0
  42. data/lib/discordrb/data/emoji.rb +82 -0
  43. data/lib/discordrb/data/integration.rb +122 -0
  44. data/lib/discordrb/data/interaction.rb +800 -0
  45. data/lib/discordrb/data/invite.rb +137 -0
  46. data/lib/discordrb/data/member.rb +372 -0
  47. data/lib/discordrb/data/message.rb +414 -0
  48. data/lib/discordrb/data/overwrite.rb +108 -0
  49. data/lib/discordrb/data/profile.rb +91 -0
  50. data/lib/discordrb/data/reaction.rb +33 -0
  51. data/lib/discordrb/data/recipient.rb +34 -0
  52. data/lib/discordrb/data/role.rb +248 -0
  53. data/lib/discordrb/data/server.rb +1004 -0
  54. data/lib/discordrb/data/user.rb +264 -0
  55. data/lib/discordrb/data/voice_region.rb +45 -0
  56. data/lib/discordrb/data/voice_state.rb +41 -0
  57. data/lib/discordrb/data/webhook.rb +238 -0
  58. data/lib/discordrb/data.rb +28 -4180
  59. data/lib/discordrb/errors.rb +46 -4
  60. data/lib/discordrb/events/bans.rb +7 -5
  61. data/lib/discordrb/events/channels.rb +3 -1
  62. data/lib/discordrb/events/guilds.rb +16 -9
  63. data/lib/discordrb/events/interactions.rb +482 -0
  64. data/lib/discordrb/events/invites.rb +125 -0
  65. data/lib/discordrb/events/members.rb +6 -2
  66. data/lib/discordrb/events/message.rb +72 -27
  67. data/lib/discordrb/events/presence.rb +35 -18
  68. data/lib/discordrb/events/raw.rb +1 -3
  69. data/lib/discordrb/events/reactions.rb +49 -4
  70. data/lib/discordrb/events/threads.rb +96 -0
  71. data/lib/discordrb/events/typing.rb +6 -4
  72. data/lib/discordrb/events/voice_server_update.rb +47 -0
  73. data/lib/discordrb/events/voice_state_update.rb +15 -10
  74. data/lib/discordrb/events/webhooks.rb +9 -6
  75. data/lib/discordrb/gateway.rb +99 -71
  76. data/lib/discordrb/id_object.rb +39 -0
  77. data/lib/discordrb/light/integrations.rb +1 -1
  78. data/lib/discordrb/light/light_bot.rb +1 -1
  79. data/lib/discordrb/logger.rb +4 -4
  80. data/lib/discordrb/paginator.rb +57 -0
  81. data/lib/discordrb/permissions.rb +159 -39
  82. data/lib/discordrb/version.rb +1 -1
  83. data/lib/discordrb/voice/encoder.rb +16 -7
  84. data/lib/discordrb/voice/network.rb +99 -47
  85. data/lib/discordrb/voice/sodium.rb +98 -0
  86. data/lib/discordrb/voice/voice_bot.rb +33 -25
  87. data/lib/discordrb/webhooks.rb +2 -0
  88. data/lib/discordrb.rb +107 -1
  89. metadata +126 -54
  90. data/.codeclimate.yml +0 -16
  91. data/.travis.yml +0 -33
  92. data/bin/travis_build_docs.sh +0 -17
  93. /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
@@ -20,6 +20,9 @@ module Discordrb
20
20
  # Raised when the bot gets a HTTP 502 error, which is usually caused by Cloudflare.
21
21
  class CloudflareError < RuntimeError; end
22
22
 
23
+ # Raised when using a webhook method without an associated token.
24
+ class UnauthorizedWebhook < RuntimeError; end
25
+
23
26
  # Generic class for errors denoted by API error codes
24
27
  class CodeError < RuntimeError
25
28
  class << self
@@ -29,8 +32,11 @@ module Discordrb
29
32
 
30
33
  # Create a new error with a particular message (the code should be defined by the class instance variable)
31
34
  # @param message [String] the message to use
32
- def initialize(message)
35
+ # @param errors [Hash] API errors
36
+ def initialize(message, errors = nil)
33
37
  @message = message
38
+
39
+ @errors = errors ? flatten_errors(errors) : []
34
40
  end
35
41
 
36
42
  # @return [Integer] The error code represented by this error.
@@ -38,26 +44,62 @@ module Discordrb
38
44
  self.class.code
39
45
  end
40
46
 
47
+ # @return [String] A message including the message and flattened errors.
48
+ def full_message(*)
49
+ error_list = @errors.collect { |err| "\t- #{err}" }
50
+
51
+ "#{@message}\n#{error_list.join("\n")}"
52
+ end
53
+
41
54
  # @return [String] This error's represented message
42
55
  attr_reader :message
56
+
57
+ # @return [Hash] More precise errors
58
+ attr_reader :errors
59
+
60
+ private
61
+
62
+ # @!visibility hidden
63
+ # Flattens errors into a more easily read format.
64
+ # @example Flattening errors of a bad field
65
+ # flatten_errors(data['errors'])
66
+ # # => ["embed.fields[0].name: This field is required", "embed.fields[0].value: This field is required"]
67
+ def flatten_errors(err, prev_key = nil)
68
+ err.collect do |key, sub_err|
69
+ if prev_key
70
+ key = /\A\d+\Z/.match?(key) ? "#{prev_key}[#{key}]" : "#{prev_key}.#{key}"
71
+ end
72
+
73
+ if (errs = sub_err['_errors'])
74
+ "#{key}: #{errs.map { |e| e['message'] }.join(' ')}"
75
+ elsif sub_err['message'] || sub_err['code']
76
+ "#{sub_err['code'] ? "#{sub_err['code']}: " : nil}#{err_msg}"
77
+ elsif sub_err.is_a? String
78
+ sub_err
79
+ else
80
+ flatten_errors(sub_err, key)
81
+ end
82
+ end.flatten
83
+ end
43
84
  end
44
85
 
45
86
  # Create a new code error class
46
- # rubocop:disable Style/MethodName
87
+ # rubocop:disable Naming/MethodName
47
88
  def self.Code(code)
48
89
  classy = Class.new(CodeError)
49
- classy.instance_variable_set('@code', code)
90
+ classy.instance_variable_set(:@code, code)
50
91
 
51
92
  @code_classes ||= {}
52
93
  @code_classes[code] = classy
53
94
 
54
95
  classy
55
96
  end
97
+ # rubocop:enable Naming/MethodName
56
98
 
57
99
  # @param code [Integer] The code to check
58
100
  # @return [Class] the error class for the given code
59
101
  def self.error_class_for(code)
60
- @code_classes[code]
102
+ @code_classes[code] || UnknownError
61
103
  end
62
104
 
63
105
  # Used when Discord doesn't provide a more specific code
@@ -27,20 +27,22 @@ module Discordrb::Events
27
27
 
28
28
  [
29
29
  matches_all(@attributes[:user], event.user) do |a, e|
30
- if a.is_a? String
30
+ case a
31
+ when String
31
32
  a == e.name
32
- elsif a.is_a? Integer
33
+ when Integer
33
34
  a == e.id
34
- elsif a == :bot
35
+ when :bot
35
36
  e.current_bot?
36
37
  else
37
38
  a == e
38
39
  end
39
40
  end,
40
41
  matches_all(@attributes[:server], event.server) do |a, e|
41
- a == if a.is_a? String
42
+ a == case a
43
+ when String
42
44
  e.name
43
- elsif a.is_a? Integer
45
+ when Integer
44
46
  e.id
45
47
  else
46
48
  e
@@ -31,7 +31,7 @@ module Discordrb::Events
31
31
 
32
32
  def initialize(data, bot)
33
33
  @bot = bot
34
- @channel = bot.channel(data['id'].to_i)
34
+ @channel = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
35
35
  end
36
36
  end
37
37
 
@@ -126,10 +126,12 @@ module Discordrb::Events
126
126
  class ChannelRecipientEvent < Event
127
127
  # @return [Channel] the channel in question.
128
128
  attr_reader :channel
129
+
129
130
  delegate :name, :server, :type, :owner_id, :recipients, :topic, :user_limit, :position, :permission_overwrites, to: :channel
130
131
 
131
132
  # @return [Recipient] the recipient that was added/removed from the group
132
133
  attr_reader :recipient
134
+
133
135
  delegate :id, to: :recipient
134
136
 
135
137
  def initialize(data, bot)
@@ -30,9 +30,10 @@ module Discordrb::Events
30
30
 
31
31
  [
32
32
  matches_all(@attributes[:server], event.server) do |a, e|
33
- a == if a.is_a? String
33
+ a == case a
34
+ when String
34
35
  e.name
35
- elsif a.is_a? Integer
36
+ when Integer
36
37
  e.id
37
38
  else
38
39
  e
@@ -56,12 +57,16 @@ module Discordrb::Events
56
57
  # Event handler for {ServerUpdateEvent}
57
58
  class ServerUpdateEventHandler < ServerEventHandler; end
58
59
 
59
- # Server is deleted
60
+ # Server is deleted, the server was left because the bot was kicked, or the
61
+ # bot made itself leave the server.
60
62
  # @see Discordrb::EventContainer#server_delete
61
63
  class ServerDeleteEvent < ServerEvent
64
+ # @return [Integer] The ID of the server that was left.
65
+ attr_reader :server
66
+
62
67
  # Override init_server to account for the deleted server
63
- def init_server(data, bot)
64
- @server = Discordrb::Server.new(data, bot, false)
68
+ def init_server(data, _bot)
69
+ @server = data['id'].to_i
65
70
  end
66
71
  end
67
72
 
@@ -141,9 +146,10 @@ module Discordrb::Events
141
146
 
142
147
  [
143
148
  matches_all(@attributes[:server], event.server) do |a, e|
144
- a == if a.is_a? String
149
+ a == case a
150
+ when String
145
151
  e.name
146
- elsif a.is_a? Integer
152
+ when Integer
147
153
  e.id
148
154
  else
149
155
  e
@@ -169,9 +175,10 @@ module Discordrb::Events
169
175
 
170
176
  [
171
177
  matches_all(@attributes[:server], event.server) do |a, e|
172
- a == if a.is_a? String
178
+ a == case a
179
+ when String
173
180
  e.name
174
- elsif a.is_a? Integer
181
+ when Integer
175
182
  e.id
176
183
  else
177
184
  e
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'discordrb/events/generic'
4
+ require 'discordrb/data'
5
+
6
+ module Discordrb::Events
7
+ # Generic subclass for interaction events
8
+ class InteractionCreateEvent < Event
9
+ # @return [Interaction] The interaction for this event.
10
+ attr_reader :interaction
11
+
12
+ # @!attribute [r] type
13
+ # @return [Integer]
14
+ # @see Interaction#type
15
+ # @!attribute [r] server
16
+ # @return [Server, nil]
17
+ # @see Interaction#server
18
+ # @!attribute [r] server_id
19
+ # @return [Integer]
20
+ # @see Interaction#server_id
21
+ # @!attribute [r] channel
22
+ # @return [Channel]
23
+ # @see Interaction#channel
24
+ # @!attribute [r] channel_id
25
+ # @return [Integer]
26
+ # @see Interaction#channel_id
27
+ # @!attribute [r] user
28
+ # @return [User]
29
+ # @see Interaction#user
30
+ delegate :type, :server, :server_id, :channel, :channel_id, :user, to: :interaction
31
+
32
+ def initialize(data, bot)
33
+ @interaction = Discordrb::Interaction.new(data, bot)
34
+ @bot = bot
35
+ end
36
+
37
+ # (see Interaction#respond)
38
+ def respond(content: nil, tts: nil, embeds: nil, allowed_mentions: nil, flags: 0, ephemeral: nil, wait: false, components: nil, &block)
39
+ @interaction.respond(
40
+ content: content, tts: tts, embeds: embeds, allowed_mentions: allowed_mentions,
41
+ flags: flags, ephemeral: ephemeral, wait: wait, components: components, &block
42
+ )
43
+ end
44
+
45
+ # (see Interaction#defer)
46
+ def defer(flags: 0, ephemeral: true)
47
+ @interaction.defer(flags: flags, ephemeral: ephemeral)
48
+ end
49
+
50
+ # (see Interaction#update_message)
51
+ def update_message(content: nil, tts: nil, embeds: nil, allowed_mentions: nil, flags: 0, ephemeral: nil, wait: false, components: nil, &block)
52
+ @interaction.update_message(
53
+ content: content, tts: tts, embeds: embeds, allowed_mentions: allowed_mentions,
54
+ flags: flags, ephemeral: ephemeral, wait: wait, components: components, &block
55
+ )
56
+ end
57
+
58
+ # (see Interaction#show_modal)
59
+ def show_modal(title:, custom_id:, components: nil, &block)
60
+ @interaction.show_modal(title: title, custom_id: custom_id, components: components, &block)
61
+ end
62
+
63
+ # (see Interaction#edit_response)
64
+ def edit_response(content: nil, embeds: nil, allowed_mentions: nil, components: nil, &block)
65
+ @interaction.edit_response(content: content, embeds: embeds, allowed_mentions: allowed_mentions, components: components, &block)
66
+ end
67
+
68
+ # (see Interaction#delete_response)
69
+ def delete_response
70
+ @interaction.delete_response
71
+ end
72
+
73
+ # (see Interaction#send_message)
74
+ def send_message(content: nil, embeds: nil, tts: false, allowed_mentions: nil, flags: 0, ephemeral: nil, components: nil, &block)
75
+ @interaction.send_message(content: content, embeds: embeds, tts: tts, allowed_mentions: allowed_mentions, flags: flags, ephemeral: ephemeral, components: components, &block)
76
+ end
77
+
78
+ # (see Interaction#edit_message)
79
+ def edit_message(message, content: nil, embeds: nil, allowed_mentions: nil, &block)
80
+ @interaction.edit_message(message, content: content, embeds: embeds, allowed_mentions: allowed_mentions, &block)
81
+ end
82
+
83
+ # (see Interaction#delete_message)
84
+ def delete_message(message)
85
+ @interaction.delete_message(message)
86
+ end
87
+
88
+ # (see Interaction#defer_update)
89
+ def defer_update
90
+ @interaction.defer_update
91
+ end
92
+
93
+ # (see Interaction#get_component)
94
+ def get_component(custom_id)
95
+ @interaction.get_component(custom_id)
96
+ end
97
+ end
98
+
99
+ # Event handler for INTERACTION_CREATE events.
100
+ class InteractionCreateEventHandler < EventHandler
101
+ # @!visibility private
102
+ def matches?(event)
103
+ return false unless event.is_a? InteractionCreateEvent
104
+
105
+ [
106
+ matches_all(@attributes[:type], event.type) do |a, e|
107
+ a == case a
108
+ when String, Symbol
109
+ Discordrb::Interactions::TYPES[e.to_sym]
110
+ else
111
+ e
112
+ end
113
+ end,
114
+
115
+ matches_all(@attributes[:server], event.interaction) do |a, e|
116
+ a.resolve_id == e.server_id
117
+ end,
118
+
119
+ matches_all(@attributes[:channel], event.interaction) do |a, e|
120
+ a.resolve_id == e.channel_id
121
+ end,
122
+
123
+ matches_all(@attributes[:user], event.user) do |a, e|
124
+ a.resolve_id == e.id
125
+ end
126
+ ].reduce(true, &:&)
127
+ end
128
+ end
129
+
130
+ # Event for ApplicationCommand interactions.
131
+ class ApplicationCommandEvent < InteractionCreateEvent
132
+ # Struct to allow accessing data via [] or methods.
133
+ Resolved = Struct.new('Resolved', :channels, :members, :messages, :roles, :users, :attachments) # rubocop:disable Lint/StructNewOverride
134
+
135
+ # @return [String] The name of the command.
136
+ attr_reader :command_name
137
+
138
+ # @return [Integer] The ID of the command.
139
+ attr_reader :command_id
140
+
141
+ # @return [String, nil] The name of the subcommand group relevant to this event.
142
+ attr_reader :subcommand_group
143
+
144
+ # @return [String, nil] The name of the subcommand relevant to this event.
145
+ attr_reader :subcommand
146
+
147
+ # @return [Resolved]
148
+ attr_reader :resolved
149
+
150
+ # @return [Hash<Symbol, Object>] Arguments provided to the command, mapped as `Name => Value`.
151
+ attr_reader :options
152
+
153
+ # @return [Integer, nil] The target of this command when it is a context command.
154
+ attr_reader :target_id
155
+
156
+ def initialize(data, bot)
157
+ super
158
+
159
+ command_data = data['data']
160
+
161
+ @command_id = command_data['id']
162
+ @command_name = command_data['name'].to_sym
163
+
164
+ @target_id = command_data['target_id']&.to_i
165
+ @resolved = Resolved.new({}, {}, {}, {}, {}, {})
166
+ process_resolved(command_data['resolved']) if command_data['resolved']
167
+
168
+ options = command_data['options'] || []
169
+
170
+ if options.empty?
171
+ @options = {}
172
+ return
173
+ end
174
+
175
+ case options[0]['type']
176
+ when 2
177
+ options = options[0]
178
+ @subcommand_group = options['name'].to_sym
179
+ @subcommand = options['options'][0]['name'].to_sym
180
+ options = options['options'][0]['options']
181
+ when 1
182
+ options = options[0]
183
+ @subcommand = options['name'].to_sym
184
+ options = options['options']
185
+ end
186
+
187
+ @options = transform_options_hash(options || {})
188
+ end
189
+
190
+ # @return [Message, User, nil] The target of this command, for context commands.
191
+ def target
192
+ return nil unless @target_id
193
+
194
+ @resolved.find { |data| data.key?(@target_id) }[@target_id]
195
+ end
196
+
197
+ private
198
+
199
+ def process_resolved(resolved_data)
200
+ resolved_data['users']&.each do |id, data|
201
+ @resolved[:users][id.to_i] = @bot.ensure_user(data)
202
+ end
203
+
204
+ resolved_data['roles']&.each do |id, data|
205
+ @resolved[:roles][id.to_i] = Discordrb::Role.new(data, @bot)
206
+ end
207
+
208
+ resolved_data['channels']&.each do |id, data|
209
+ data['guild_id'] = @interaction.server_id
210
+ @resolved[:channels][id.to_i] = Discordrb::Channel.new(data, @bot)
211
+ end
212
+
213
+ resolved_data['members']&.each do |id, data|
214
+ data['user'] = resolved_data['users'][id]
215
+ data['guild_id'] = @interaction.server_id
216
+ @resolved[:members][id.to_i] = Discordrb::Member.new(data, nil, @bot)
217
+ end
218
+
219
+ resolved_data['messages']&.each do |id, data|
220
+ @resolved[:messages][id.to_i] = Discordrb::Message.new(data, @bot)
221
+ end
222
+
223
+ resolved_data['attachments']&.each do |id, data|
224
+ @resolved[:attachments][id.to_i] = Discordrb::Attachment.new(data, nil, @bot)
225
+ end
226
+ end
227
+
228
+ def transform_options_hash(hash)
229
+ hash.to_h { |opt| [opt['name'], opt['options'] || opt['value']] }
230
+ end
231
+ end
232
+
233
+ # Event handler for ApplicationCommandEvents.
234
+ class ApplicationCommandEventHandler < EventHandler
235
+ # @return [Hash]
236
+ attr_reader :subcommands
237
+
238
+ # @!visibility private
239
+ def initialize(attributes, block)
240
+ super
241
+
242
+ @subcommands = {}
243
+ end
244
+
245
+ # @param name [Symbol, String]
246
+ # @yieldparam [SubcommandBuilder]
247
+ # @return [ApplicationCommandEventHandler]
248
+ def group(name)
249
+ raise ArgumentError, 'Unable to mix subcommands and groups' if @subcommands.any? { |_, v| v.is_a? Proc }
250
+
251
+ builder = SubcommandBuilder.new(name)
252
+ yield builder
253
+
254
+ @subcommands.merge!(builder.to_h)
255
+ self
256
+ end
257
+
258
+ # @param name [String, Symbol]
259
+ # @yieldparam [SubcommandBuilder]
260
+ # @return [ApplicationCommandEventHandler]
261
+ def subcommand(name, &block)
262
+ raise ArgumentError, 'Unable to mix subcommands and groups' if @subcommands.any? { |_, v| v.is_a? Hash }
263
+
264
+ @subcommands[name.to_sym] = block
265
+
266
+ self
267
+ end
268
+
269
+ # @!visibility private
270
+ # @param event [Event]
271
+ def call(event)
272
+ return unless matches?(event)
273
+
274
+ if event.subcommand_group
275
+ unless (cmd = @subcommands.dig(event.subcommand_group, event.subcommand))
276
+ Discordrb::LOGGER.debug("Received an event for an unhandled subcommand `#{event.command_name} #{event.subcommand_group} #{event.subcommand}'")
277
+ return
278
+ end
279
+
280
+ cmd.call(event)
281
+ elsif event.subcommand
282
+ unless (cmd = @subcommands[event.subcommand])
283
+ Discordrb::LOGGER.debug("Received an event for an unhandled subcommand `#{event.command_name} #{event.subcommand}'")
284
+ return
285
+ end
286
+
287
+ cmd.call(event)
288
+ else
289
+ @block.call(event)
290
+ end
291
+ end
292
+
293
+ # @!visibility private
294
+ def matches?(event)
295
+ return false unless event.is_a? ApplicationCommandEvent
296
+
297
+ [
298
+ matches_all(@attributes[:name], event.command_name) do |a, e|
299
+ a.to_sym == e.to_sym
300
+ end
301
+ ].reduce(true, &:&)
302
+ end
303
+ end
304
+
305
+ # Builder for adding subcommands to an ApplicationCommandHandler
306
+ class SubcommandBuilder
307
+ # @!visibility private
308
+ # @param group [String, Symbol, nil]
309
+ def initialize(group = nil)
310
+ @group = group&.to_sym
311
+ @subcommands = {}
312
+ end
313
+
314
+ # @param name [Symbol, String]
315
+ # @yieldparam [ApplicationCommandEvent]
316
+ def subcommand(name, &block)
317
+ @subcommands[name.to_sym] = block
318
+ end
319
+
320
+ # @!visibility private
321
+ def to_h
322
+ @group ? { @group => @subcommands } : @subcommands
323
+ end
324
+ end
325
+
326
+ # An event for when a user interacts with a component.
327
+ class ComponentEvent < InteractionCreateEvent
328
+ # @return [String] User provided data for this button.
329
+ attr_reader :custom_id
330
+
331
+ # @return [Interactions::Message, nil] The message the button originates from.
332
+ attr_reader :message
333
+
334
+ # @!visibility private
335
+ def initialize(data, bot)
336
+ super
337
+
338
+ @message = Discordrb::Interactions::Message.new(data['message'], bot, @interaction) if data['message']
339
+ @custom_id = data['data']['custom_id']
340
+ end
341
+ end
342
+
343
+ # Generic handler for component events.
344
+ class ComponentEventHandler < InteractionCreateEventHandler
345
+ def matches?(event)
346
+ return false unless super
347
+ return false unless event.is_a? ComponentEvent
348
+
349
+ [
350
+ matches_all(@attributes[:custom_id], event.custom_id) do |a, e|
351
+ # Match regexp and strings
352
+ case a
353
+ when Regexp
354
+ a.match?(e)
355
+ else
356
+ a == e
357
+ end
358
+ end,
359
+ matches_all(@attributes[:message], event.message) do |a, e|
360
+ case a
361
+ when String, Integer
362
+ a.resolve_id == e.id
363
+ else
364
+ a.id == e.id
365
+ end
366
+ end
367
+ ].reduce(&:&)
368
+ end
369
+ end
370
+
371
+ # An event for when a user interacts with a button component.
372
+ class ButtonEvent < ComponentEvent
373
+ end
374
+
375
+ # Event handler for a Button interaction event.
376
+ class ButtonEventHandler < ComponentEventHandler
377
+ end
378
+
379
+ # Event for when a user interacts with a select string component.
380
+ class StringSelectEvent < ComponentEvent
381
+ # @return [Array<String>] Selected values.
382
+ attr_reader :values
383
+
384
+ # @!visibility private
385
+ def initialize(data, bot)
386
+ super
387
+
388
+ @values = data['data']['values']
389
+ end
390
+ end
391
+
392
+ # Event handler for a select string component.
393
+ class StringSelectEventHandler < ComponentEventHandler
394
+ end
395
+
396
+ # An event for when a user submits a modal.
397
+ class ModalSubmitEvent < ComponentEvent
398
+ # @return [Array<TextInputComponent>]
399
+ attr_reader :components
400
+
401
+ # Get the value of an input passed to the modal.
402
+ # @param custom_id [String] The custom ID of the component to look for.
403
+ # @return [String, nil]
404
+ def value(custom_id)
405
+ get_component(custom_id)&.value
406
+ end
407
+ end
408
+
409
+ # Event handler for a modal submission.
410
+ class ModalSubmitEventHandler < ComponentEventHandler
411
+ end
412
+
413
+ # Event for when a user interacts with a select user component.
414
+ class UserSelectEvent < ComponentEvent
415
+ # @return [Array<User>] Selected values.
416
+ attr_reader :values
417
+
418
+ # @!visibility private
419
+ def initialize(data, bot)
420
+ super
421
+
422
+ @values = data['data']['values'].map { |e| bot.user(e) }
423
+ end
424
+ end
425
+
426
+ # Event handler for a select user component.
427
+ class UserSelectEventHandler < ComponentEventHandler
428
+ end
429
+
430
+ # Event for when a user interacts with a select role component.
431
+ class RoleSelectEvent < ComponentEvent
432
+ # @return [Array<Role>] Selected values.
433
+ attr_reader :values
434
+
435
+ # @!visibility private
436
+ def initialize(data, bot)
437
+ super
438
+
439
+ @values = data['data']['values'].map { |e| bot.server(data['guild_id']).role(e) }
440
+ end
441
+ end
442
+
443
+ # Event handler for a select role component.
444
+ class RoleSelectEventHandler < ComponentEventHandler
445
+ end
446
+
447
+ # Event for when a user interacts with a select mentionable component.
448
+ class MentionableSelectEvent < ComponentEvent
449
+ # @return [Hash<Symbol => Array<User>, Symbol => Array<Role>>] Selected values.
450
+ attr_reader :values
451
+
452
+ # @!visibility private
453
+ def initialize(data, bot)
454
+ super
455
+
456
+ users = data['data']['resolved']['users'].keys.map { |e| bot.user(e) }
457
+ roles = data['data']['resolved']['roles'] ? data['data']['resolved']['roles'].keys.map { |e| bot.server(data['guild_id']).role(e) } : []
458
+ @values = { users: users, roles: roles }
459
+ end
460
+ end
461
+
462
+ # Event handler for a select mentionable component.
463
+ class MentionableSelectEventHandler < ComponentEventHandler
464
+ end
465
+
466
+ # Event for when a user interacts with a select channel component.
467
+ class ChannelSelectEvent < ComponentEvent
468
+ # @return [Array<Channel>] Selected values.
469
+ attr_reader :values
470
+
471
+ # @!visibility private
472
+ def initialize(data, bot)
473
+ super
474
+
475
+ @values = data['data']['values'].map { |e| bot.channel(e, bot.server(data['guild_id'])) }
476
+ end
477
+ end
478
+
479
+ # Event handler for a select channel component.
480
+ class ChannelSelectEventHandler < ComponentEventHandler
481
+ end
482
+ end