discordrb 3.4.3 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +44 -18
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -1
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  5. data/.github/workflows/codeql.yml +65 -0
  6. data/.markdownlint.json +4 -0
  7. data/.rubocop.yml +8 -2
  8. data/CHANGELOG.md +390 -225
  9. data/LICENSE.txt +1 -1
  10. data/README.md +37 -25
  11. data/discordrb-webhooks.gemspec +4 -1
  12. data/discordrb.gemspec +9 -6
  13. data/lib/discordrb/api/application.rb +202 -0
  14. data/lib/discordrb/api/channel.rb +177 -11
  15. data/lib/discordrb/api/interaction.rb +54 -0
  16. data/lib/discordrb/api/invite.rb +2 -2
  17. data/lib/discordrb/api/server.rb +40 -19
  18. data/lib/discordrb/api/user.rb +8 -3
  19. data/lib/discordrb/api/webhook.rb +57 -0
  20. data/lib/discordrb/api.rb +19 -5
  21. data/lib/discordrb/bot.rb +317 -32
  22. data/lib/discordrb/cache.rb +27 -22
  23. data/lib/discordrb/commands/command_bot.rb +6 -4
  24. data/lib/discordrb/commands/container.rb +1 -1
  25. data/lib/discordrb/commands/parser.rb +2 -2
  26. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  27. data/lib/discordrb/container.rb +132 -3
  28. data/lib/discordrb/data/attachment.rb +15 -0
  29. data/lib/discordrb/data/audit_logs.rb +3 -3
  30. data/lib/discordrb/data/channel.rb +167 -23
  31. data/lib/discordrb/data/component.rb +229 -0
  32. data/lib/discordrb/data/integration.rb +42 -3
  33. data/lib/discordrb/data/interaction.rb +800 -0
  34. data/lib/discordrb/data/invite.rb +1 -1
  35. data/lib/discordrb/data/member.rb +108 -33
  36. data/lib/discordrb/data/message.rb +99 -19
  37. data/lib/discordrb/data/overwrite.rb +13 -7
  38. data/lib/discordrb/data/role.rb +58 -1
  39. data/lib/discordrb/data/server.rb +82 -80
  40. data/lib/discordrb/data/user.rb +69 -9
  41. data/lib/discordrb/data/webhook.rb +97 -4
  42. data/lib/discordrb/data.rb +3 -0
  43. data/lib/discordrb/errors.rb +44 -3
  44. data/lib/discordrb/events/channels.rb +1 -1
  45. data/lib/discordrb/events/interactions.rb +482 -0
  46. data/lib/discordrb/events/message.rb +9 -6
  47. data/lib/discordrb/events/presence.rb +21 -14
  48. data/lib/discordrb/events/reactions.rb +0 -1
  49. data/lib/discordrb/events/threads.rb +96 -0
  50. data/lib/discordrb/gateway.rb +30 -17
  51. data/lib/discordrb/permissions.rb +59 -34
  52. data/lib/discordrb/version.rb +1 -1
  53. data/lib/discordrb/voice/encoder.rb +2 -2
  54. data/lib/discordrb/voice/network.rb +18 -7
  55. data/lib/discordrb/voice/sodium.rb +3 -1
  56. data/lib/discordrb/voice/voice_bot.rb +3 -3
  57. data/lib/discordrb/webhooks.rb +2 -0
  58. data/lib/discordrb.rb +37 -4
  59. metadata +48 -14
  60. data/.codeclimate.yml +0 -16
  61. data/.travis.yml +0 -32
  62. data/bin/travis_build_docs.sh +0 -17
@@ -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
@@ -17,9 +17,10 @@ module Discordrb::Events
17
17
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
18
18
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
19
19
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
20
+ # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
20
21
  # @return [Discordrb::Message] the message that was sent
21
- def send_message(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
22
- channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference)
22
+ def send_message(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
23
+ channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference, components)
23
24
  end
24
25
 
25
26
  # The same as {#send_message}, but yields a {Webhooks::Embed} for easy building of embedded content inside a block.
@@ -30,11 +31,12 @@ module Discordrb::Events
30
31
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
31
32
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
32
33
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
34
+ # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
33
35
  # @yield [embed] Yields the embed to allow for easy building inside a block.
34
36
  # @yieldparam embed [Discordrb::Webhooks::Embed] The embed from the parameters, or a new one.
35
37
  # @return [Message] The resulting message.
36
- def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowed_mentions = nil, message_reference = nil, &block)
37
- channel.send_embed(message, embed, attachments, tts, allowed_mentions, message_reference, &block)
38
+ def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowed_mentions = nil, message_reference = nil, components = nil, &block)
39
+ channel.send_embed(message, embed, attachments, tts, allowed_mentions, message_reference, components, &block)
38
40
  end
39
41
 
40
42
  # Sends a temporary message to the channel this message was sent in, right now.
@@ -44,8 +46,9 @@ module Discordrb::Events
44
46
  # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
45
47
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
46
48
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
47
- def send_temporary_message(content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil)
48
- channel.send_temporary_message(content, timeout, tts, embed, attachments, allowed_mentions)
49
+ # @param components [View, Array<Hash>, nil] A collection of components to attach to the message.
50
+ def send_temporary_message(content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, components = nil)
51
+ channel.send_temporary_message(content, timeout, tts, embed, attachments, allowed_mentions, components)
49
52
  end
50
53
 
51
54
  # Adds a string to be sent after the event has finished execution. Avoids problems with rate limiting because only
@@ -65,28 +65,35 @@ module Discordrb::Events
65
65
  # @return [User] the user whose status got updated.
66
66
  attr_reader :user
67
67
 
68
- # @return [String] the new game the user is playing.
69
- attr_reader :game
68
+ # @return [Discordrb::Activity] The new activity
69
+ attr_reader :activity
70
70
 
71
- # @return [String] the URL to the stream
72
- attr_reader :url
71
+ # @!attribute [r] url
72
+ # @return [String] the URL to the stream
73
73
 
74
- # @return [String] what the player is currently doing (ex. game being streamed)
75
- attr_reader :details
74
+ # @!attribute [r] details
75
+ # @return [String] what the player is currently doing (ex. game being streamed)
76
76
 
77
- # @return [Integer] the type of play. 0 = game, 1 = Twitch
78
- attr_reader :type
77
+ # @!attribute [r] type
78
+ # @return [Integer] the type of play. See {Discordrb::Activity}
79
+ delegate :url, :details, :type, to: :activity
79
80
 
80
- def initialize(data, bot)
81
+ # @return [Hash<Symbol, Symbol>] the current online status (`:online`, `:idle` or `:dnd`) of the user
82
+ # on various device types (`:desktop`, `:mobile`, or `:web`). The value will be `nil` if the user is offline or invisible.
83
+ attr_reader :client_status
84
+
85
+ def initialize(data, activity, bot)
81
86
  @bot = bot
87
+ @activity = activity
82
88
 
83
89
  @server = bot.server(data['guild_id'].to_i)
84
90
  @user = bot.user(data['user']['id'].to_i)
85
- @game = data['game'] ? data['game']['name'] : nil
86
- @type = data['game'] ? data['game']['type'].to_i : nil
87
- # Handle optional 'game' fields safely
88
- @url = data['game'] && data['game']['url'] ? data['game']['url'] : nil
89
- @details = data['game'] && data['game']['details'] ? data['game']['details'] : nil
91
+ @client_status = @user.client_status
92
+ end
93
+
94
+ # @return [String] the name of the new game the user is playing.
95
+ def game
96
+ @activity.name
90
97
  end
91
98
  end
92
99
 
@@ -137,7 +137,6 @@ module Discordrb::Events
137
137
  # Check for the proper event type
138
138
  return false unless event.is_a? ReactionRemoveAllEvent
139
139
 
140
- # No attributes yet as there is no property available on the event that doesn't involve doing a resolution request
141
140
  [
142
141
  matches_all(@attributes[:message], event.message_id) do |a, e|
143
142
  a == e
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb::Events
4
+ # Raised when a thread is created
5
+ class ThreadCreateEvent < Event
6
+ # @return [Channel] the thread in question.
7
+ attr_reader :thread
8
+
9
+ delegate :name, :server, :owner, :parent_channel, :thread_metadata, to: :thread
10
+
11
+ def initialize(data, bot)
12
+ @bot = bot
13
+ @thread = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
14
+ end
15
+ end
16
+
17
+ # Event handler for ChannelCreateEvent
18
+ class ThreadCreateEventHandler < EventHandler
19
+ def matches?(event)
20
+ # Check for the proper event type
21
+ return false unless event.is_a? ThreadCreateEvent
22
+
23
+ [
24
+ matches_all(@attributes[:name], event.name) do |a, e|
25
+ a == if a.is_a? String
26
+ e.to_s
27
+ else
28
+ e
29
+ end
30
+ end,
31
+ matches_all(@attributes[:server], event.server) do |a, e|
32
+ a.resolve_id == e.resolve_id
33
+ end,
34
+ matches_all(@attributes[:invitable], event.thread.invitable) do |a, e|
35
+ a == e
36
+ end,
37
+ matches_all(@attributes[:owner], event.thread.owner) do |a, e|
38
+ a.resolve_id == e.resolve_id
39
+ end,
40
+ matches_all(@attributes[:channel], event.thread.parent) do |a, e|
41
+ a.resolve_id == e.resolve_id
42
+ end
43
+ ].reduce(true, &:&)
44
+ end
45
+ end
46
+
47
+ # Raised when a thread is updated (e.g. name changes)
48
+ class ThreadUpdateEvent < ThreadCreateEvent; end
49
+
50
+ # Event handler for ThreadUpdateEvent
51
+ class ThreadUpdateEventHandler < ThreadCreateEventHandler
52
+ def matches?(event)
53
+ # Check for the proper event type
54
+ return false unless event.is_a? ThreadUpdateEvent
55
+
56
+ super
57
+ end
58
+ end
59
+
60
+ # Raised when members are added or removed from a thread.
61
+ class ThreadMembersUpdateEvent < Event
62
+ # @return [Channel]
63
+ attr_reader :thread
64
+
65
+ # @return [Array<Member, User>]
66
+ attr_reader :added_members
67
+
68
+ # @return [Array<Integer>]
69
+ attr_reader :removed_member_ids
70
+
71
+ # @return [Integer]
72
+ attr_reader :member_count
73
+
74
+ delegate :name, :server, :owner, :parent_channel, :thread_metadata, to: :thread
75
+
76
+ def initialize(data, bot)
77
+ @bot = bot
78
+ @thread = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
79
+ @added_members = data['added_members']&.map do |member|
80
+ data['guild_id'] ? bot.member(data['guild_id'], member['user_id']) : bot.user(member['user_id'])
81
+ end || []
82
+ @removed_member_ids = data['removed_member_ids']&.map(&:resolve_id) || []
83
+ @member_count = data['member_count']
84
+ end
85
+ end
86
+
87
+ # Event handler for ThreadMembersUpdateEvent
88
+ class ThreadMembersUpdateEventHandler < ThreadCreateEventHandler
89
+ def matches?(event)
90
+ # Check for the proper event type
91
+ return false unless event.is_a? ThreadMembersUpdateEvent
92
+
93
+ super
94
+ end
95
+ end
96
+ end