discordrb 3.4.3 → 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 (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