discordrb 3.3.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +152 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  5. data/.github/pull_request_template.md +37 -0
  6. data/.github/workflows/codeql.yml +65 -0
  7. data/.markdownlint.json +4 -0
  8. data/.rubocop.yml +39 -36
  9. data/CHANGELOG.md +874 -552
  10. data/Gemfile +2 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +80 -86
  13. data/Rakefile +2 -0
  14. data/bin/console +1 -0
  15. data/discordrb-webhooks.gemspec +9 -6
  16. data/discordrb.gemspec +21 -18
  17. data/lib/discordrb/allowed_mentions.rb +36 -0
  18. data/lib/discordrb/api/application.rb +202 -0
  19. data/lib/discordrb/api/channel.rb +236 -47
  20. data/lib/discordrb/api/interaction.rb +54 -0
  21. data/lib/discordrb/api/invite.rb +5 -5
  22. data/lib/discordrb/api/server.rb +94 -66
  23. data/lib/discordrb/api/user.rb +17 -11
  24. data/lib/discordrb/api/webhook.rb +63 -6
  25. data/lib/discordrb/api.rb +55 -16
  26. data/lib/discordrb/await.rb +0 -1
  27. data/lib/discordrb/bot.rb +480 -93
  28. data/lib/discordrb/cache.rb +31 -24
  29. data/lib/discordrb/colour_rgb.rb +43 -0
  30. data/lib/discordrb/commands/command_bot.rb +35 -12
  31. data/lib/discordrb/commands/container.rb +21 -24
  32. data/lib/discordrb/commands/parser.rb +20 -20
  33. data/lib/discordrb/commands/rate_limiter.rb +4 -3
  34. data/lib/discordrb/container.rb +209 -20
  35. data/lib/discordrb/data/activity.rb +271 -0
  36. data/lib/discordrb/data/application.rb +50 -0
  37. data/lib/discordrb/data/attachment.rb +71 -0
  38. data/lib/discordrb/data/audit_logs.rb +345 -0
  39. data/lib/discordrb/data/channel.rb +993 -0
  40. data/lib/discordrb/data/component.rb +229 -0
  41. data/lib/discordrb/data/embed.rb +251 -0
  42. data/lib/discordrb/data/emoji.rb +82 -0
  43. data/lib/discordrb/data/integration.rb +122 -0
  44. data/lib/discordrb/data/interaction.rb +800 -0
  45. data/lib/discordrb/data/invite.rb +137 -0
  46. data/lib/discordrb/data/member.rb +372 -0
  47. data/lib/discordrb/data/message.rb +414 -0
  48. data/lib/discordrb/data/overwrite.rb +108 -0
  49. data/lib/discordrb/data/profile.rb +91 -0
  50. data/lib/discordrb/data/reaction.rb +33 -0
  51. data/lib/discordrb/data/recipient.rb +34 -0
  52. data/lib/discordrb/data/role.rb +248 -0
  53. data/lib/discordrb/data/server.rb +1004 -0
  54. data/lib/discordrb/data/user.rb +264 -0
  55. data/lib/discordrb/data/voice_region.rb +45 -0
  56. data/lib/discordrb/data/voice_state.rb +41 -0
  57. data/lib/discordrb/data/webhook.rb +238 -0
  58. data/lib/discordrb/data.rb +28 -4180
  59. data/lib/discordrb/errors.rb +46 -4
  60. data/lib/discordrb/events/bans.rb +7 -5
  61. data/lib/discordrb/events/channels.rb +3 -1
  62. data/lib/discordrb/events/guilds.rb +16 -9
  63. data/lib/discordrb/events/interactions.rb +482 -0
  64. data/lib/discordrb/events/invites.rb +125 -0
  65. data/lib/discordrb/events/members.rb +6 -2
  66. data/lib/discordrb/events/message.rb +72 -27
  67. data/lib/discordrb/events/presence.rb +35 -18
  68. data/lib/discordrb/events/raw.rb +1 -3
  69. data/lib/discordrb/events/reactions.rb +49 -4
  70. data/lib/discordrb/events/threads.rb +96 -0
  71. data/lib/discordrb/events/typing.rb +6 -4
  72. data/lib/discordrb/events/voice_server_update.rb +47 -0
  73. data/lib/discordrb/events/voice_state_update.rb +15 -10
  74. data/lib/discordrb/events/webhooks.rb +9 -6
  75. data/lib/discordrb/gateway.rb +99 -71
  76. data/lib/discordrb/id_object.rb +39 -0
  77. data/lib/discordrb/light/integrations.rb +1 -1
  78. data/lib/discordrb/light/light_bot.rb +1 -1
  79. data/lib/discordrb/logger.rb +4 -4
  80. data/lib/discordrb/paginator.rb +57 -0
  81. data/lib/discordrb/permissions.rb +159 -39
  82. data/lib/discordrb/version.rb +1 -1
  83. data/lib/discordrb/voice/encoder.rb +16 -7
  84. data/lib/discordrb/voice/network.rb +99 -47
  85. data/lib/discordrb/voice/sodium.rb +98 -0
  86. data/lib/discordrb/voice/voice_bot.rb +33 -25
  87. data/lib/discordrb/webhooks.rb +2 -0
  88. data/lib/discordrb.rb +107 -1
  89. metadata +126 -54
  90. data/.codeclimate.yml +0 -16
  91. data/.travis.yml +0 -33
  92. data/bin/travis_build_docs.sh +0 -17
  93. /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
@@ -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