discordrb 3.4.3 → 3.6.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -1
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  7. data/.github/workflows/ci.yml +78 -0
  8. data/.github/workflows/codeql.yml +65 -0
  9. data/.github/workflows/deploy.yml +54 -0
  10. data/.github/workflows/release.yml +45 -0
  11. data/.markdownlint.json +4 -0
  12. data/.rubocop.yml +58 -2
  13. data/CHANGELOG.md +485 -225
  14. data/LICENSE.txt +1 -1
  15. data/README.md +38 -26
  16. data/discordrb-webhooks.gemspec +4 -1
  17. data/discordrb.gemspec +18 -10
  18. data/lib/discordrb/api/application.rb +278 -0
  19. data/lib/discordrb/api/channel.rb +222 -18
  20. data/lib/discordrb/api/interaction.rb +63 -0
  21. data/lib/discordrb/api/invite.rb +2 -2
  22. data/lib/discordrb/api/server.rb +123 -66
  23. data/lib/discordrb/api/user.rb +20 -5
  24. data/lib/discordrb/api/webhook.rb +72 -0
  25. data/lib/discordrb/api.rb +35 -25
  26. data/lib/discordrb/bot.rb +437 -66
  27. data/lib/discordrb/cache.rb +41 -22
  28. data/lib/discordrb/commands/command_bot.rb +13 -21
  29. data/lib/discordrb/commands/container.rb +1 -1
  30. data/lib/discordrb/commands/parser.rb +7 -7
  31. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  32. data/lib/discordrb/container.rb +178 -3
  33. data/lib/discordrb/data/activity.rb +1 -1
  34. data/lib/discordrb/data/application.rb +1 -0
  35. data/lib/discordrb/data/attachment.rb +38 -3
  36. data/lib/discordrb/data/audit_logs.rb +3 -3
  37. data/lib/discordrb/data/avatar_decoration.rb +26 -0
  38. data/lib/discordrb/data/call.rb +22 -0
  39. data/lib/discordrb/data/channel.rb +299 -30
  40. data/lib/discordrb/data/collectibles.rb +45 -0
  41. data/lib/discordrb/data/component.rb +229 -0
  42. data/lib/discordrb/data/embed.rb +10 -3
  43. data/lib/discordrb/data/emoji.rb +20 -1
  44. data/lib/discordrb/data/integration.rb +45 -3
  45. data/lib/discordrb/data/interaction.rb +937 -0
  46. data/lib/discordrb/data/invite.rb +1 -1
  47. data/lib/discordrb/data/member.rb +236 -44
  48. data/lib/discordrb/data/message.rb +278 -51
  49. data/lib/discordrb/data/overwrite.rb +15 -7
  50. data/lib/discordrb/data/primary_server.rb +60 -0
  51. data/lib/discordrb/data/profile.rb +2 -7
  52. data/lib/discordrb/data/reaction.rb +2 -1
  53. data/lib/discordrb/data/recipient.rb +1 -1
  54. data/lib/discordrb/data/role.rb +204 -18
  55. data/lib/discordrb/data/server.rb +194 -118
  56. data/lib/discordrb/data/server_preview.rb +68 -0
  57. data/lib/discordrb/data/snapshot.rb +110 -0
  58. data/lib/discordrb/data/user.rb +132 -12
  59. data/lib/discordrb/data/voice_region.rb +1 -0
  60. data/lib/discordrb/data/webhook.rb +99 -9
  61. data/lib/discordrb/data.rb +9 -0
  62. data/lib/discordrb/errors.rb +47 -3
  63. data/lib/discordrb/events/await.rb +1 -1
  64. data/lib/discordrb/events/channels.rb +38 -1
  65. data/lib/discordrb/events/generic.rb +2 -0
  66. data/lib/discordrb/events/guilds.rb +6 -1
  67. data/lib/discordrb/events/interactions.rb +575 -0
  68. data/lib/discordrb/events/invites.rb +2 -0
  69. data/lib/discordrb/events/members.rb +19 -2
  70. data/lib/discordrb/events/message.rb +42 -8
  71. data/lib/discordrb/events/presence.rb +23 -14
  72. data/lib/discordrb/events/raw.rb +1 -0
  73. data/lib/discordrb/events/reactions.rb +2 -1
  74. data/lib/discordrb/events/roles.rb +2 -0
  75. data/lib/discordrb/events/threads.rb +100 -0
  76. data/lib/discordrb/events/typing.rb +1 -0
  77. data/lib/discordrb/events/voice_server_update.rb +1 -0
  78. data/lib/discordrb/events/voice_state_update.rb +1 -0
  79. data/lib/discordrb/events/webhooks.rb +1 -0
  80. data/lib/discordrb/gateway.rb +57 -28
  81. data/lib/discordrb/paginator.rb +3 -3
  82. data/lib/discordrb/permissions.rb +71 -35
  83. data/lib/discordrb/version.rb +1 -1
  84. data/lib/discordrb/voice/encoder.rb +2 -2
  85. data/lib/discordrb/voice/network.rb +18 -7
  86. data/lib/discordrb/voice/sodium.rb +3 -1
  87. data/lib/discordrb/voice/voice_bot.rb +3 -3
  88. data/lib/discordrb/webhooks.rb +2 -0
  89. data/lib/discordrb/websocket.rb +0 -10
  90. data/lib/discordrb.rb +54 -5
  91. metadata +87 -25
  92. data/.circleci/config.yml +0 -126
  93. data/.codeclimate.yml +0 -16
  94. data/.travis.yml +0 -32
  95. data/bin/travis_build_docs.sh +0 -17
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # A decoration displayed on a user's avatar.
5
+ class AvatarDecoration
6
+ # @return [Integer] ID of the avatar decoration's SKU.
7
+ attr_reader :sku_id
8
+
9
+ # @return [String] ID that can be used to generate an avatar decoration URL.
10
+ # @see #url
11
+ attr_reader :decoration_id
12
+
13
+ # @!visibility private
14
+ def initialize(data, bot)
15
+ @bot = bot
16
+ @sku_id = data['sku_id']&.to_i
17
+ @decoration_id = data['asset']
18
+ end
19
+
20
+ # Utility method to get an avatar decoration URL.
21
+ # @return [String] the URL to the avatar decoration.
22
+ def url
23
+ API.avatar_decoration_url(@decoration_id)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # A call in a private channel.
5
+ class Call
6
+ # @return [Time, nil] the time at when the call ended.
7
+ attr_reader :ended_at
8
+
9
+ # @!visibility private
10
+ def initialize(data, bot)
11
+ @bot = bot
12
+ @participant_ids = data['participants'] || []
13
+ @ended_at = Time.iso8601(data['ended_timestamp']) if data['ended_timestamp']
14
+ end
15
+
16
+ # Get the users that participated in this call.
17
+ # @return [Array<User>] the participants of this call.
18
+ def participants
19
+ @participants ||= @participant_ids.map { |participant| @bot.user(participant) }
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'discordrb/webhooks/view'
4
+ require 'time'
5
+
3
6
  module Discordrb
4
7
  # A Discord channel, including data like the topic
5
8
  class Channel
@@ -13,23 +16,28 @@ module Discordrb
13
16
  group: 3,
14
17
  category: 4,
15
18
  news: 5,
16
- store: 6
19
+ store: 6,
20
+ news_thread: 10,
21
+ public_thread: 11,
22
+ private_thread: 12,
23
+ stage_voice: 13,
24
+ directory: 14,
25
+ forum: 15
17
26
  }.freeze
18
27
 
19
28
  # @return [String] this channel's name.
20
29
  attr_reader :name
21
30
 
22
- # @return [Server, nil] the server this channel is on. If this channel is a PM channel, it will be nil.
23
- attr_reader :server
24
-
25
- # @return [Integer, nil] the ID of the parent channel, if this channel is inside a category
31
+ # @return [Integer, nil] the ID of the parent channel, if this channel is inside a category. If this channel is a
32
+ # thread, this is the text channel it is a child to.
26
33
  attr_reader :parent_id
27
34
 
28
35
  # @return [Integer] the type of this channel
29
36
  # @see TYPES
30
37
  attr_reader :type
31
38
 
32
- # @return [Integer, nil] the ID of the owner of the group channel or nil if this is not a group channel.
39
+ # @return [Integer, nil] the ID of the owner of the group channel or nil if this is not a group channel. If this
40
+ # channel is a thread, this is the member that started the thread.
33
41
  attr_reader :owner_id
34
42
 
35
43
  # @return [Array<Recipient>, nil] the array of recipients of the private messages, or nil if this is not a Private channel
@@ -56,6 +64,40 @@ module Discordrb
56
64
  attr_reader :rate_limit_per_user
57
65
  alias_method :slowmode_rate, :rate_limit_per_user
58
66
 
67
+ # @return [Integer, nil] An approximate count of messages sent in a thread, excluding deleted messages.
68
+ attr_reader :message_count
69
+
70
+ # @return [Integer, nil] An approximate count of members in a thread. Stops counting at 50.
71
+ attr_reader :member_count
72
+
73
+ # @return [true, false, nil] Whether or not this thread is archived.
74
+ attr_reader :archived
75
+ alias_method :archived?, :archived
76
+
77
+ # @return [Integer, nil] How long after the last message before a thread is automatically archived.
78
+ attr_reader :auto_archive_duration
79
+
80
+ # @return [Time, nil] The timestamp of when this threads status last changed.
81
+ attr_reader :archive_timestamp
82
+
83
+ # @return [true, false, nil] Whether this thread is locked or not.
84
+ attr_reader :locked
85
+ alias_method :locked?, :locked
86
+
87
+ # @return [Time, nil] When the current user joined this thread.
88
+ attr_reader :join_timestamp
89
+
90
+ # @return [Integer, nil] Member flags for this thread, used for notifications.
91
+ attr_reader :member_flags
92
+
93
+ # @return [true, false, nil] For private threads, determines whether non-moderators can add other non-moderators to
94
+ # a thread.
95
+ attr_reader :invitable
96
+ alias_method :invitable?, :invitable
97
+
98
+ # @return [Time, nil] The time at when the last pinned message was pinned in this channel.
99
+ attr_reader :last_pin_timestamp
100
+
59
101
  # @return [true, false] whether or not this channel is a PM or group channel.
60
102
  def private?
61
103
  pm? || group?
@@ -99,15 +141,45 @@ module Discordrb
99
141
  end
100
142
  else
101
143
  @name = data['name']
102
- @server = server || bot.server(data['guild_id'].to_i)
144
+ @server_id = server&.id || data['guild_id'].to_i
145
+ @server = server
103
146
  end
104
147
 
105
148
  @nsfw = data['nsfw'] || false
106
149
  @rate_limit_per_user = data['rate_limit_per_user'] || 0
150
+ @message_count = data['message_count']
151
+ @member_count = data['member_count']
152
+
153
+ if (metadata = data['thread_metadata'])
154
+ @archived = metadata['archived']
155
+ @auto_archive_duration = metadata['auto_archive_duration']
156
+ @archive_timestamp = Time.iso8601(metadata['archive_timestamp'])
157
+ @locked = metadata['locked']
158
+ @invitable = metadata['invitable']
159
+ end
107
160
 
161
+ if (member = data['member'])
162
+ @member_join = Time.iso8601(member['join_timestamp'])
163
+ @member_flags = member['flags']
164
+ end
165
+
166
+ process_last_pin_timestamp(data['last_pin_timestamp'])
108
167
  process_permission_overwrites(data['permission_overwrites'])
109
168
  end
110
169
 
170
+ # @return [Server, nil] the server this channel is on. If this channel is a PM channel, it will be nil.
171
+ # @raise [Discordrb::Errors::NoPermission] This can happen when receiving interactions for servers in which the bot is not
172
+ # authorized with the `bot` scope.
173
+ def server
174
+ return @server if @server
175
+ return nil if private?
176
+
177
+ @server = @bot.server(@server_id)
178
+ raise Discordrb::Errors::NoPermission, 'The bot does not have access to this server' unless @server
179
+
180
+ @server
181
+ end
182
+
111
183
  # @return [true, false] whether or not this channel is a text channel
112
184
  def text?
113
185
  @type.zero?
@@ -143,6 +215,26 @@ module Discordrb
143
215
  @type == 6
144
216
  end
145
217
 
218
+ # @return [true, false] whether or not this channel is a news thread.
219
+ def news_thread?
220
+ @type == 10
221
+ end
222
+
223
+ # @return [true, false] whether or not this channel is a public thread.
224
+ def public_thread?
225
+ @type == 11
226
+ end
227
+
228
+ # @return [true, false] whether or not this channel is a private thread.
229
+ def private_thread?
230
+ @type == 12
231
+ end
232
+
233
+ # @return [true, false] whether or not this channel is a thread.
234
+ def thread?
235
+ news_thread? || public_thread? || private_thread?
236
+ end
237
+
146
238
  # @return [Channel, nil] the category channel, if this channel is in a category
147
239
  def category
148
240
  @bot.channel(@parent_id) if @parent_id
@@ -199,7 +291,7 @@ module Discordrb
199
291
  ids = if parent
200
292
  parent.children
201
293
  else
202
- @server.channels.reject(&:parent_id).select { |c| c.type == @type }
294
+ server.channels.reject(&:parent_id).select { |c| c.type == @type }
203
295
  end.sort_by(&:position).map(&:id)
204
296
 
205
297
  # Move our channel ID after the target ID by deleting it,
@@ -225,7 +317,7 @@ module Discordrb
225
317
  move_argument << hash
226
318
  end
227
319
 
228
- API::Server.update_channel_positions(@bot.token, @server.id, move_argument)
320
+ API::Server.update_channel_positions(@bot.token, @server_id, move_argument)
229
321
  end
230
322
 
231
323
  # Sets whether this channel is NSFW
@@ -262,9 +354,9 @@ module Discordrb
262
354
 
263
355
  # Sets the amount of time (in seconds) users have to wait in between sending messages.
264
356
  # @param rate [Integer]
265
- # @raise [ArgumentError] if value isn't between 0 and 120
357
+ # @raise [ArgumentError] if value isn't between 0 and 21600
266
358
  def rate_limit_per_user=(rate)
267
- raise ArgumentError, 'rate_limit_per_user must be between 0 and 120' unless rate.between?(0, 120)
359
+ raise ArgumentError, 'rate_limit_per_user must be between 0 and 21600' unless rate.between?(0, 21_600)
268
360
 
269
361
  update_channel_data(rate_limit_per_user: rate)
270
362
  end
@@ -341,9 +433,11 @@ module Discordrb
341
433
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
342
434
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
343
435
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
436
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
437
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) and SUPPRESS_NOTIFICATIONS (1 << 12) can be set.
344
438
  # @return [Message] the message that was sent.
345
- def send_message(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
346
- @bot.send_message(@id, content, tts, embed, attachments, allowed_mentions, message_reference)
439
+ def send_message(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0)
440
+ @bot.send_message(@id, content, tts, embed, attachments, allowed_mentions, message_reference, components, flags)
347
441
  end
348
442
 
349
443
  alias_method :send, :send_message
@@ -356,8 +450,10 @@ module Discordrb
356
450
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
357
451
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
358
452
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
359
- def send_temporary_message(content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
360
- @bot.send_temporary_message(@id, content, timeout, tts, embed, attachments, allowed_mentions, message_reference)
453
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
454
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) and SUPPRESS_NOTIFICATIONS (1 << 12) can be set.
455
+ def send_temporary_message(content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0)
456
+ @bot.send_temporary_message(@id, content, timeout, tts, embed, attachments, allowed_mentions, message_reference, components, flags)
361
457
  end
362
458
 
363
459
  # Convenience method to send a message with an embed.
@@ -372,19 +468,69 @@ module Discordrb
372
468
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
373
469
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
374
470
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
471
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
472
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) and SUPPRESS_NOTIFICATIONS (1 << 12) can be set.
375
473
  # @yield [embed] Yields the embed to allow for easy building inside a block.
376
474
  # @yieldparam embed [Discordrb::Webhooks::Embed] The embed from the parameters, or a new one.
377
475
  # @return [Message] The resulting message.
378
- def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowed_mentions = nil, message_reference = nil)
476
+ def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0)
379
477
  embed ||= Discordrb::Webhooks::Embed.new
380
- yield(embed) if block_given?
381
- send_message(message, tts, embed, attachments, allowed_mentions, message_reference)
478
+ view = Discordrb::Webhooks::View.new
479
+
480
+ yield(embed, view) if block_given?
481
+
482
+ send_message(message, tts, embed, attachments, allowed_mentions, message_reference, components || view.to_a, flags)
483
+ end
484
+
485
+ # Send a message to this channel.
486
+ # @example This sends a silent message with an embed.
487
+ # channel.send_message!(content: 'Hi <@171764626755813376>', flags: :suppress_notifications) do |builder|
488
+ # builder.add_embed do |embed|
489
+ # embed.title = 'The Ruby logo'
490
+ # embed.image = Discordrb::Webhooks::EmbedImage.new(url: 'https://www.ruby-lang.org/images/header-ruby-logo.png')
491
+ # end
492
+ # end
493
+ # @param content [String] The content of the message. Should not be longer than 2000 characters or it will result in an error.
494
+ # @param timeout [Float, nil] The amount of time in seconds after which the message sent will be deleted, or `nil` if the message should not be deleted.
495
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
496
+ # @param embeds [Array<Hash, Webhooks::Embed>] The embeds that should be attached to the message.
497
+ # @param attachments [Array<File>] Files that can be referenced in embeds and components via `attachment://file.png`.
498
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, nil] Mentions that are allowed to ping on this message.
499
+ # @param reference [Message, String, Integer, Hash, nil] The optional message, or message ID, to reply to or forward.
500
+ # @param components [View, Array<#to_h>] Interaction components to associate with this message.
501
+ # @param flags [Integer, Symbol, Array<Symbol, Integer>] Flags for this message. Currently only `:suppress_embeds` (1 << 2), `:suppress_notifications` (1 << 12), and `:uikit_components` (1 << 15) can be set.
502
+ # @param has_components [true, false] Whether this message includes any V2 components. Enabling this disables sending content and embeds.
503
+ # @param nonce [nil, String, Integer, false] The 25 character nonce that should be used when sending this message.
504
+ # @param enforce_nonce [true, false] Whether the provided nonce should be enforced and used for message de-duplication.
505
+ # @yieldparam builder [Webhooks::Builder] An optional message builder. Arguments passed to the builder overwrite method data.
506
+ # @yieldparam view [Webhooks::View] An optional component builder. Arguments passed to the builder overwrite method data.
507
+ # @return [Message, nil] The resulting message that was created, or `nil` if the `timeout` parameter was set to a non `nil` value.
508
+ def send_message!(content: '', timeout: nil, tts: false, embeds: [], attachments: nil, allowed_mentions: nil, reference: nil, components: nil, flags: 0, has_components: false, nonce: nil, enforce_nonce: false)
509
+ builder = Discordrb::Webhooks::Builder.new
510
+ view = Discordrb::Webhooks::View.new
511
+
512
+ builder.tts = tts
513
+ builder.content = content
514
+ embeds&.each { |embed| builder << embed }
515
+ builder.allowed_mentions = allowed_mentions
516
+
517
+ yield(builder, view) if block_given?
518
+
519
+ flags = Array(flags).map { |flag| Discordrb::Message::FLAGS[flag] || flag }.reduce(&:|)
520
+ flags |= (1 << 15) if has_components
521
+ builder = builder.to_json_hash
522
+
523
+ if timeout
524
+ @bot.send_temporary_message(@id, builder[:content], timeout, builder[:tts], builder[:embeds], attachments, builder[:allowed_mentions], reference, components&.to_a || view.to_a, flags, nonce, enforce_nonce)
525
+ else
526
+ @bot.send_message(@id, builder[:content], builder[:tts], builder[:embeds], attachments, builder[:allowed_mentions], reference, components&.to_a || view.to_a, flags, nonce, enforce_nonce)
527
+ end
382
528
  end
383
529
 
384
530
  # Sends multiple messages to a channel
385
531
  # @param content [Array<String>] The messages to send.
386
532
  def send_multiple(content)
387
- content.each { |e| send_message(e) }
533
+ content.each { |text| send_message!(content: text) }
388
534
  end
389
535
 
390
536
  # Splits a message into chunks whose length is at most the Discord character limit, then sends them individually.
@@ -502,6 +648,7 @@ module Discordrb
502
648
  # @!visibility private
503
649
  def update_from(other)
504
650
  @name = other.name
651
+ @type = other.type
505
652
  @position = other.position
506
653
  @topic = other.topic
507
654
  @recipients = other.recipients
@@ -511,6 +658,13 @@ module Discordrb
511
658
  @nsfw = other.nsfw
512
659
  @parent_id = other.parent_id
513
660
  @rate_limit_per_user = other.rate_limit_per_user
661
+ @archived = other.archived?
662
+ @auto_archive_duration = other.auto_archive_duration
663
+ @archive_timestamp = other.archive_timestamp
664
+ @locked = other.locked?
665
+ @invitable = other.invitable?
666
+ @message_count = other.message_count
667
+ @last_pin_timestamp = other.last_pin_timestamp
514
668
  end
515
669
 
516
670
  # The list of users currently in this channel. For a voice channel, it will return all the members currently
@@ -518,9 +672,9 @@ module Discordrb
518
672
  # @return [Array<Member>] the users in this channel
519
673
  def users
520
674
  if text?
521
- @server.online_members(include_idle: true).select { |u| u.can_read_messages? self }
675
+ server.online_members(include_idle: true).select { |u| u.can_read_messages? self }
522
676
  elsif voice?
523
- @server.voice_states.map { |id, voice_state| @server.member(id) if !voice_state.voice_channel.nil? && voice_state.voice_channel.id == @id }.compact
677
+ server.voice_states.filter_map { |id, voice_state| server.member(id) if !voice_state.voice_channel.nil? && voice_state.voice_channel.id == @id }
524
678
  end
525
679
  end
526
680
 
@@ -554,19 +708,37 @@ module Discordrb
554
708
  # @param message_id [Integer] The ID of the message to retrieve.
555
709
  # @return [Message, nil] the retrieved message, or `nil` if it couldn't be found.
556
710
  def load_message(message_id)
711
+ raise ArgumentError, 'message_id cannot be nil' if message_id.nil?
712
+
557
713
  response = API::Channel.message(@bot.token, @id, message_id)
558
714
  Message.new(JSON.parse(response), @bot)
559
- rescue RestClient::ResourceNotFound
715
+ rescue Discordrb::Errors::UnknownMessage
560
716
  nil
561
717
  end
562
718
 
563
719
  alias_method :message, :load_message
564
720
 
565
- # Requests all pinned messages in a channel.
566
- # @return [Array<Message>] the received messages.
567
- def pins
568
- msgs = API::Channel.pinned_messages(@bot.token, @id)
569
- JSON.parse(msgs).map { |msg| Message.new(msg, @bot) }
721
+ # Requests the pinned messages in a channel.
722
+ # @param limit [Integer, nil] the limit of how many pinned messages to retrieve. `nil` will return all the pinned messages.
723
+ # @return [Array<Message>] the messages pinned in the channel.
724
+ def pins(limit: 50)
725
+ get_pins = proc do |fetch_limit, before = nil|
726
+ resp = API::Channel.pinned_messages(@bot.token, @id, fetch_limit, before&.iso8601)
727
+ JSON.parse(resp)['items'].map { |pin| Message.new(pin['message'].merge({ 'pinned_at' => pin['pinned_at'] }), @bot) }
728
+ end
729
+
730
+ # Can be done without pagination.
731
+ return get_pins.call(limit) if limit && limit <= 50
732
+
733
+ paginator = Paginator.new(limit, :down) do |last_page|
734
+ if last_page && last_page.count < 50
735
+ []
736
+ else
737
+ get_pins.call(50, last_page&.last&.pinned_at)
738
+ end
739
+ end
740
+
741
+ paginator.to_a
570
742
  end
571
743
 
572
744
  # Delete the last N messages on this channel.
@@ -742,9 +914,96 @@ module Discordrb
742
914
  invites.map { |invite_data| Invite.new(invite_data, @bot) }
743
915
  end
744
916
 
917
+ # Start a thread.
918
+ # @param name [String] The name of the thread.
919
+ # @param auto_archive_duration [60, 1440, 4320, 10080] How long before a thread is automatically
920
+ # archived.
921
+ # @param message [Message, Integer, String] The message to reference when starting this thread.
922
+ # @param type [Symbol, Integer] The type of thread to create. Can be a key from {TYPES} or the value.
923
+ # @return [Channel]
924
+ def start_thread(name, auto_archive_duration, message: nil, type: 11)
925
+ message_id = message&.id || message
926
+ type = TYPES[type] || type
927
+
928
+ data = if message
929
+ API::Channel.start_thread_with_message(@bot.token, @id, message_id, name, auto_archive_duration)
930
+ else
931
+ API::Channel.start_thread_without_message(@bot.token, @id, name, auto_archive_duration, type)
932
+ end
933
+
934
+ Channel.new(JSON.parse(data), @bot, @server)
935
+ end
936
+
937
+ # Start a thread in a forum or media channel.
938
+ # @param name [String] The name of the forum post to create.
939
+ # @param auto_archive_duration [Integer, nil] How long before the post is automatically archived.
940
+ # @param rate_limit_per_user [Integer, nil] The slowmode rate of the forum post to create.
941
+ # @param tags [Array<#resolve_id>, nil] The tags of the forum channel to apply onto the forum post.
942
+ # @param content [String, nil] The content of the forum post's starter message.
943
+ # @param embeds [Array<Hash, Webhooks::Embed>, nil] The embeds that should be attached to the forum post's starter message.
944
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, nil] Mentions that are allowed to ping on this forum post's starter message.
945
+ # @param components [Webhooks::View, Array<#to_h>, nil] The interaction components to associate with this forum post's starter message.
946
+ # @param stickers [Array<#resolve_id>, nil] The stickers to include in the forum post's starter message.
947
+ # @param attachments [Array<File>, nil] Files that can be referenced in embeds and components via `attachment://file.png`.
948
+ # @param flags [Integer, Symbol, Array<Symbol, Integer>, nil] The flags to set on the forum post's starter message. Currently only `:suppress_embeds` (1 << 2), `:suppress_notifications` (1 << 12), and `:uikit_components` (1 << 15) can be set.
949
+ # @param has_components [true, false] Whether the starter message for this forum post includes any V2 components. Enabling this disables sending content and embeds.
950
+ # @param reason [String, nil] The reason for creating this forum post.
951
+ # @yieldparam builder [Webhooks::Builder] An optional message builder. Arguments passed to the builder overwrite method data.
952
+ # @yieldparam view [Webhooks::View] An optional component builder. Arguments passed to the builder overwrite method data.
953
+ # @return [Message] the starter message of the forum post. The forum post that was created can be accessed via {Message#thread}.
954
+ def start_forum_thread(name:, auto_archive_duration: nil, rate_limit_per_user: nil, tags: nil, content: nil, embeds: nil, allowed_mentions: nil, components: nil, stickers: nil, attachments: nil, flags: nil, has_components: false, reason: nil)
955
+ builder = Discordrb::Webhooks::Builder.new
956
+ view = Discordrb::Webhooks::View.new
957
+
958
+ builder.content = content
959
+ embeds&.each { |embed| builder << embed }
960
+ builder.allowed_mentions = allowed_mentions
961
+
962
+ yield(builder, view) if block_given?
963
+
964
+ flags = Array(flags).map { |flag| Discordrb::Message::FLAGS[flag] || flag }.reduce(&:|)
965
+ flags |= (1 << 15) if has_components
966
+ builder = builder.to_json_hash
967
+
968
+ message = { content: builder[:content], embeds: builder[:embeds], allowed_mentions: builder[:allowed_mentions], components: components&.to_a || view.to_a, sticker_ids: stickers&.map(&:resolve_id), flags: flags }
969
+ response = JSON.parse(API::Channel.start_thread_in_forum_or_media_channel(@bot.token, @id, name, message.compact, attachments, rate_limit_per_user, auto_archive_duration, tags&.map(&:resolve_id), reason))
970
+
971
+ Message.new(response['message'].merge!('channel_id' => response['id'], 'thread' => response), @bot)
972
+ end
973
+
974
+ # @!group Threads
975
+
976
+ # Join this thread.
977
+ def join_thread
978
+ @bot.join_thread(@id)
979
+ end
980
+
981
+ # Leave this thread
982
+ def leave_thread
983
+ @bot.leave_thread(@id)
984
+ end
985
+
986
+ # Members in the thread.
987
+ def members
988
+ @bot.thread_members[@id].collect { |id| @server_id ? @bot.member(@server_id, id) : @bot.user(id) }
989
+ end
990
+
991
+ # Add a member to the thread
992
+ # @param member [Member, Integer, String] The member, or ID of the member, to add to this thread.
993
+ def add_member(member)
994
+ @bot.add_thread_member(@id, member)
995
+ end
996
+
997
+ # @param member [Member, Integer, String] The member, or ID of the member, to remove from a thread.
998
+ def remove_member(member)
999
+ @bot.remove_thread_member(@id, member)
1000
+ end
1001
+
1002
+ # @!endgroup
1003
+
745
1004
  # The default `inspect` method is overwritten to give more useful output.
746
1005
  def inspect
747
- "<Channel name=#{@name} id=#{@id} topic=\"#{@topic}\" type=#{@type} position=#{@position} server=#{@server}>"
1006
+ "<Channel name=#{@name} id=#{@id} topic=\"#{@topic}\" type=#{@type} position=#{@position} server=#{@server || @server_id}>"
748
1007
  end
749
1008
 
750
1009
  # Adds a recipient to a group channel.
@@ -771,6 +1030,14 @@ module Discordrb
771
1030
  @recipients.delete(recipient)
772
1031
  end
773
1032
 
1033
+ # Set the last pin timestamp of a channel.
1034
+ # @param time [String, nil] the time of the last pinned message in the channel
1035
+ # @note For internal use only
1036
+ # @!visibility private
1037
+ def process_last_pin_timestamp(time)
1038
+ @last_pin_timestamp = time ? Time.parse(time) : time
1039
+ end
1040
+
774
1041
  # Updates the cached data with new data
775
1042
  # @note For internal use only
776
1043
  # @!visibility private
@@ -790,15 +1057,17 @@ module Discordrb
790
1057
 
791
1058
  # @return [String] a URL that a user can use to navigate to this channel in the client
792
1059
  def link
793
- "https://discord.com/channels/#{@server&.id || '@me'}/#{@channel.id}"
1060
+ "https://discord.com/channels/#{@server_id || '@me'}/#{@channel.id}"
794
1061
  end
795
1062
 
796
1063
  alias_method :jump_link, :link
797
1064
 
798
1065
  private
799
1066
 
1067
+ # rubocop:disable Lint/UselessConstantScoping
800
1068
  # For bulk_delete checking
801
1069
  TWO_WEEKS = 86_400 * 14
1070
+ # rubocop:enable Lint/UselessConstantScoping
802
1071
 
803
1072
  # Deletes a list of messages on this channel using bulk delete.
804
1073
  def bulk_delete(ids, strict = false, reason = nil)
@@ -821,7 +1090,7 @@ module Discordrb
821
1090
  def update_channel_data(new_data)
822
1091
  new_nsfw = new_data[:nsfw].is_a?(TrueClass) || new_data[:nsfw].is_a?(FalseClass) ? new_data[:nsfw] : @nsfw
823
1092
  # send permission_overwrite only when explicitly set
824
- overwrites = new_data[:permission_overwrites] ? new_data[:permission_overwrites].map { |_, v| v.to_hash } : nil
1093
+ overwrites = new_data[:permission_overwrites] ? new_data[:permission_overwrites].map(&:to_hash) : nil
825
1094
  response = JSON.parse(API::Channel.update(@bot.token, @id,
826
1095
  new_data[:name] || @name,
827
1096
  new_data[:topic] || @topic,
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # Collectibles are resources such as nameplates that can be collected by users.
5
+ class Collectibles
6
+ # @return [Nameplate, nil] the nameplate the user has collected or nil.
7
+ attr_reader :nameplate
8
+
9
+ # @!visibility private
10
+ def initialize(data, bot)
11
+ @bot = bot
12
+ @nameplate = Nameplate.new(data['nameplate'], bot) if data['nameplate']
13
+ end
14
+
15
+ # Collectable background images shown on a user's name in the member's tab.
16
+ class Nameplate
17
+ # @return [Integer] ID of the nameplate's SKU.
18
+ attr_reader :sku_id
19
+
20
+ # @return [String] the path to the nameplate asset.
21
+ attr_reader :asset
22
+
23
+ # @return [String] the label of the nameplate.
24
+ attr_reader :label
25
+
26
+ # @return [Symbol] the background color of the nameplate.
27
+ attr_reader :palette
28
+
29
+ # @!visibility private
30
+ def initialize(data, bot)
31
+ @bot = bot
32
+ @sku_id = data['sku_id']&.to_i
33
+ @asset = data['asset']
34
+ @label = data['label']
35
+ @palette = data['palette'].to_sym
36
+ end
37
+
38
+ # Utility method to get the URL of this nameplate.
39
+ # @return [String] CDN url of this nameplate.
40
+ def url
41
+ API.nameplate_url(@asset)
42
+ end
43
+ end
44
+ end
45
+ end