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
@@ -100,7 +100,7 @@ module Discordrb
100
100
  end
101
101
 
102
102
  @uses = data['uses']
103
- @inviter = data['inviter'] ? (@bot.user(data['inviter']['id'].to_i) || User.new(data['inviter'], bot)) : nil
103
+ @inviter = data['inviter'] ? bot.ensure_user(data['inviter']) : nil
104
104
  @temporary = data['temporary']
105
105
  @revoked = data['revoked']
106
106
  @online_member_count = data['approximate_presence_count']
@@ -18,6 +18,10 @@ module Discordrb
18
18
 
19
19
  # @return [Server] the server this member is on.
20
20
  attr_reader :server
21
+
22
+ # @return [Time] When the user's timeout will expire.
23
+ attr_reader :communication_disabled_until
24
+ alias_method :timeout, :communication_disabled_until
21
25
  end
22
26
 
23
27
  # A member is a user on a server. It differs from regular users in that it has roles, voice statuses and things like
@@ -62,17 +66,39 @@ module Discordrb
62
66
  @user = bot.ensure_user(data['user'])
63
67
  super @user # Initialize the delegate class
64
68
 
65
- # Somehow, Discord doesn't send the server ID in the standard member format...
66
- raise ArgumentError, 'Cannot create a member without any information about the server!' if server.nil? && data['guild_id'].nil?
67
-
68
- @server = server || bot.server(data['guild_id'].to_i)
69
+ @server = server
70
+ @server_id = server&.id || data['guild_id'].to_i
69
71
 
70
- # Initialize the roles by getting the roles from the server one-by-one
71
- update_roles(data['roles'])
72
+ @role_ids = data['roles']&.map(&:to_i) || []
72
73
 
73
74
  @nick = data['nick']
74
75
  @joined_at = data['joined_at'] ? Time.parse(data['joined_at']) : nil
75
76
  @boosting_since = data['premium_since'] ? Time.parse(data['premium_since']) : nil
77
+ timeout_until = data['communication_disabled_until']
78
+ @communication_disabled_until = timeout_until ? Time.parse(timeout_until) : nil
79
+ @permissions = Permissions.new(data['permissions']) if data['permissions']
80
+ end
81
+
82
+ # @return [Server] the server this member is on.
83
+ # @raise [Discordrb::Errors::NoPermission] This can happen when receiving interactions for servers in which the bot is not
84
+ # authorized with the `bot` scope.
85
+ def server
86
+ return @server if @server
87
+
88
+ @server = @bot.server(@server_id)
89
+ raise Discordrb::Errors::NoPermission, 'The bot does not have access to this server' unless @server
90
+
91
+ @server
92
+ end
93
+
94
+ # @return [Array<Role>] the roles this member has.
95
+ # @raise [Discordrb::Errors::NoPermission] This can happen when receiving interactions for servers in which the bot is not
96
+ # authorized with the `bot` scope.
97
+ def roles
98
+ return @roles if @roles
99
+
100
+ update_roles(@role_ids)
101
+ @roles
76
102
  end
77
103
 
78
104
  # @return [true, false] if this user is a Nitro Booster of this server.
@@ -82,14 +108,14 @@ module Discordrb
82
108
 
83
109
  # @return [true, false] whether this member is the server owner.
84
110
  def owner?
85
- @server.owner == self
111
+ server.owner == self
86
112
  end
87
113
 
88
114
  # @param role [Role, String, Integer] the role to check or its ID.
89
115
  # @return [true, false] whether this member has the specified role.
90
116
  def role?(role)
91
117
  role = role.resolve_id
92
- @roles.any? { |e| e.id == role }
118
+ roles.any?(role)
93
119
  end
94
120
 
95
121
  # @see Member#set_roles
@@ -97,12 +123,30 @@ module Discordrb
97
123
  set_roles(role)
98
124
  end
99
125
 
126
+ # Check if the current user has communication disabled.
127
+ # @return [true, false]
128
+ def communication_disabled?
129
+ !@communication_disabled_until.nil? && @communication_disabled_until > Time.now
130
+ end
131
+
132
+ alias_method :timeout?, :communication_disabled?
133
+
134
+ # Set a user's timeout duration, or remove it by setting the timeout to `nil`.
135
+ # @param timeout_until [Time, nil] When the timeout will end.
136
+ def communication_disabled_until=(timeout_until)
137
+ raise ArgumentError, 'A time out cannot exceed 28 days' if timeout_until && timeout_until > (Time.now + 2_419_200)
138
+
139
+ API::Server.update_member(@bot.token, @server_id, @user.id, communication_disabled_until: timeout_until.iso8601)
140
+ end
141
+
142
+ alias_method :timeout=, :communication_disabled_until=
143
+
100
144
  # Bulk sets a member's roles.
101
145
  # @param role [Role, Array<Role>] The role(s) to set.
102
146
  # @param reason [String] The reason the user's roles are being changed.
103
147
  def set_roles(role, reason = nil)
104
148
  role_ids = role_id_array(role)
105
- API::Server.update_member(@bot.token, @server.id, @user.id, roles: role_ids, reason: reason)
149
+ API::Server.update_member(@bot.token, @server_id, @user.id, roles: role_ids, reason: reason)
106
150
  end
107
151
 
108
152
  # Adds and removes roles from a member.
@@ -116,10 +160,10 @@ module Discordrb
116
160
  def modify_roles(add, remove, reason = nil)
117
161
  add_role_ids = role_id_array(add)
118
162
  remove_role_ids = role_id_array(remove)
119
- old_role_ids = @roles.map(&:id)
163
+ old_role_ids = resolve_role_ids
120
164
  new_role_ids = (old_role_ids - remove_role_ids + add_role_ids).uniq
121
165
 
122
- API::Server.update_member(@bot.token, @server.id, @user.id, roles: new_role_ids, reason: reason)
166
+ API::Server.update_member(@bot.token, @server_id, @user.id, roles: new_role_ids, reason: reason)
123
167
  end
124
168
 
125
169
  # Adds one or more roles to this member.
@@ -129,11 +173,11 @@ module Discordrb
129
173
  role_ids = role_id_array(role)
130
174
 
131
175
  if role_ids.count == 1
132
- API::Server.add_member_role(@bot.token, @server.id, @user.id, role_ids[0], reason)
176
+ API::Server.add_member_role(@bot.token, @server_id, @user.id, role_ids[0], reason)
133
177
  else
134
- old_role_ids = @roles.map(&:id)
178
+ old_role_ids = resolve_role_ids
135
179
  new_role_ids = (old_role_ids + role_ids).uniq
136
- API::Server.update_member(@bot.token, @server.id, @user.id, roles: new_role_ids, reason: reason)
180
+ API::Server.update_member(@bot.token, @server_id, @user.id, roles: new_role_ids, reason: reason)
137
181
  end
138
182
  end
139
183
 
@@ -144,22 +188,22 @@ module Discordrb
144
188
  role_ids = role_id_array(role)
145
189
 
146
190
  if role_ids.count == 1
147
- API::Server.remove_member_role(@bot.token, @server.id, @user.id, role_ids[0], reason)
191
+ API::Server.remove_member_role(@bot.token, @server_id, @user.id, role_ids[0], reason)
148
192
  else
149
- old_role_ids = @roles.map(&:id)
193
+ old_role_ids = resolve_role_ids
150
194
  new_role_ids = old_role_ids.reject { |i| role_ids.include?(i) }
151
- API::Server.update_member(@bot.token, @server.id, @user.id, roles: new_role_ids, reason: reason)
195
+ API::Server.update_member(@bot.token, @server_id, @user.id, roles: new_role_ids, reason: reason)
152
196
  end
153
197
  end
154
198
 
155
199
  # @return [Role] the highest role this member has.
156
200
  def highest_role
157
- @roles.max_by(&:position)
201
+ roles.max_by(&:position)
158
202
  end
159
203
 
160
204
  # @return [Role, nil] the role this member is being hoisted with.
161
205
  def hoist_role
162
- hoisted_roles = @roles.select(&:hoist)
206
+ hoisted_roles = roles.select(&:hoist)
163
207
  return nil if hoisted_roles.empty?
164
208
 
165
209
  hoisted_roles.max_by(&:position)
@@ -167,7 +211,7 @@ module Discordrb
167
211
 
168
212
  # @return [Role, nil] the role this member is basing their colour on.
169
213
  def colour_role
170
- coloured_roles = @roles.select { |v| v.colour.combined.nonzero? }
214
+ coloured_roles = roles.select { |v| v.colour.combined.nonzero? }
171
215
  return nil if coloured_roles.empty?
172
216
 
173
217
  coloured_roles.max_by(&:position)
@@ -184,22 +228,41 @@ module Discordrb
184
228
 
185
229
  # Server deafens this member.
186
230
  def server_deafen
187
- API::Server.update_member(@bot.token, @server.id, @user.id, deaf: true)
231
+ API::Server.update_member(@bot.token, @server_id, @user.id, deaf: true)
188
232
  end
189
233
 
190
234
  # Server undeafens this member.
191
235
  def server_undeafen
192
- API::Server.update_member(@bot.token, @server.id, @user.id, deaf: false)
236
+ API::Server.update_member(@bot.token, @server_id, @user.id, deaf: false)
193
237
  end
194
238
 
195
239
  # Server mutes this member.
196
240
  def server_mute
197
- API::Server.update_member(@bot.token, @server.id, @user.id, mute: true)
241
+ API::Server.update_member(@bot.token, @server_id, @user.id, mute: true)
198
242
  end
199
243
 
200
244
  # Server unmutes this member.
201
245
  def server_unmute
202
- API::Server.update_member(@bot.token, @server.id, @user.id, mute: false)
246
+ API::Server.update_member(@bot.token, @server_id, @user.id, mute: false)
247
+ end
248
+
249
+ # Bans this member from the server.
250
+ # @param message_days [Integer] How many days worth of messages sent by the member should be deleted.
251
+ # @param reason [String] The reason this member is being banned.
252
+ def ban(message_days = 0, reason: nil)
253
+ server.ban(@user, message_days, reason: reason)
254
+ end
255
+
256
+ # Unbans this member from the server.
257
+ # @param reason [String] The reason this member is being unbanned.
258
+ def unban(reason = nil)
259
+ server.unban(@user, reason)
260
+ end
261
+
262
+ # Kicks this member from the server.
263
+ # @param reason [String] The reason this member is being kicked.
264
+ def kick(reason = nil)
265
+ server.kick(@user, reason)
203
266
  end
204
267
 
205
268
  # @see Member#set_nick
@@ -218,28 +281,28 @@ module Discordrb
218
281
  nick ||= ''
219
282
 
220
283
  if @user.current_bot?
221
- API::User.change_own_nickname(@bot.token, @server.id, nick, reason)
284
+ API::User.change_own_nickname(@bot.token, @server_id, nick, reason)
222
285
  else
223
- API::Server.update_member(@bot.token, @server.id, @user.id, nick: nick, reason: nil)
286
+ API::Server.update_member(@bot.token, @server_id, @user.id, nick: nick, reason: nil)
224
287
  end
225
288
  end
226
289
 
227
290
  alias_method :set_nickname, :set_nick
228
291
 
229
- # @return [String] the name the user displays as (nickname if they have one, username otherwise)
292
+ # @return [String] the name the user displays as (nickname if they have one, global_name if they have one, username otherwise)
230
293
  def display_name
231
- nickname || username
294
+ nickname || global_name || username
232
295
  end
233
296
 
234
297
  # Update this member's roles
235
298
  # @note For internal use only.
236
299
  # @!visibility private
237
300
  def update_roles(role_ids)
238
- @roles = [@server.role(@server.id)]
301
+ @roles = [server.role(@server_id)]
239
302
  role_ids.each do |id|
240
303
  # It is possible for members to have roles that do not exist
241
- # on the server any longer. See https://github.com/shardlab/discordrb/issues/371
242
- role = @server.role(id)
304
+ # on the server any longer. See https://github.com/discordrb/discordrb/issues/371
305
+ role = server.role(id)
243
306
  @roles << role if role
244
307
  end
245
308
  end
@@ -258,6 +321,12 @@ module Discordrb
258
321
  @boosting_since = time
259
322
  end
260
323
 
324
+ # @!visibility private
325
+ def update_communication_disabled_until(time)
326
+ time = time ? Time.parse(time) : nil
327
+ @communication_disabled_until = time
328
+ end
329
+
261
330
  # Update this member
262
331
  # @note For internal use only.
263
332
  # @!visibility private
@@ -268,13 +337,15 @@ module Discordrb
268
337
  @deaf = data['deaf'] if data.key?('deaf')
269
338
 
270
339
  @joined_at = Time.parse(data['joined_at']) if data['joined_at']
340
+ timeout_until = data['communication_disabled_until']
341
+ @communication_disabled_until = timeout_until ? Time.parse(timeout_until) : nil
271
342
  end
272
343
 
273
344
  include PermissionCalculator
274
345
 
275
346
  # Overwriting inspect for debug purposes
276
347
  def inspect
277
- "<Member user=#{@user.inspect} server=#{@server.inspect} joined_at=#{@joined_at} roles=#{@roles.inspect} voice_channel=#{@voice_channel.inspect} mute=#{@mute} deaf=#{@deaf} self_mute=#{@self_mute} self_deaf=#{@self_deaf}>"
348
+ "<Member user=#{@user.inspect} server=#{@server&.inspect || @server_id} joined_at=#{@joined_at} roles=#{@roles&.inspect || @role_ids} voice_channel=#{@voice_channel.inspect} mute=#{@mute} deaf=#{@deaf} self_mute=#{@self_mute} self_deaf=#{@self_deaf}>"
278
349
  end
279
350
 
280
351
  private
@@ -290,8 +361,12 @@ module Discordrb
290
361
 
291
362
  # Utility method to get data out of this member's voice state
292
363
  def voice_state_attribute(name)
293
- voice_state = @server.voice_states[@user.id]
364
+ voice_state = server.voice_states[@user.id]
294
365
  voice_state&.send name
295
366
  end
367
+
368
+ def resolve_role_ids
369
+ @roles ? @roles.collect(&:id) : @role_ids
370
+ end
296
371
  end
297
372
  end
@@ -61,14 +61,17 @@ module Discordrb
61
61
  attr_reader :pinned
62
62
  alias_method :pinned?, :pinned
63
63
 
64
+ # @return [Integer] what the type of the message is
65
+ attr_reader :type
66
+
64
67
  # @return [Server, nil] the server in which this message was sent.
65
68
  attr_reader :server
66
69
 
67
70
  # @return [Integer, nil] the webhook ID that sent this message, or `nil` if it wasn't sent through a webhook.
68
71
  attr_reader :webhook_id
69
72
 
70
- # The discriminator that webhook user accounts have.
71
- ZERO_DISCRIM = '0000'
73
+ # @return [Array<Component>]
74
+ attr_reader :components
72
75
 
73
76
  # @!visibility private
74
77
  def initialize(data, bot)
@@ -76,6 +79,7 @@ module Discordrb
76
79
  @content = data['content']
77
80
  @channel = bot.channel(data['channel_id'].to_i)
78
81
  @pinned = data['pinned']
82
+ @type = data['type']
79
83
  @tts = data['tts']
80
84
  @nonce = data['nonce']
81
85
  @mention_everyone = data['mention_everyone']
@@ -85,12 +89,14 @@ module Discordrb
85
89
 
86
90
  @server = @channel.server
87
91
 
92
+ @webhook_id = data['webhook_id']&.to_i
93
+
88
94
  @author = if data['author']
89
- if data['author']['discriminator'] == ZERO_DISCRIM
95
+ if @webhook_id
90
96
  # This is a webhook user! It would be pointless to try to resolve a member here, so we just create
91
97
  # a User and return that instead.
92
98
  Discordrb::LOGGER.debug("Webhook user: #{data['author']['id']}")
93
- User.new(data['author'], @bot)
99
+ User.new(data['author'].merge({ '_webhook' => true }), @bot)
94
100
  elsif @channel.private?
95
101
  # Turn the message user into a recipient - we can't use the channel recipient
96
102
  # directly because the bot may also send messages to the channel
@@ -100,11 +106,12 @@ module Discordrb
100
106
 
101
107
  if member
102
108
  member.update_data(data['member']) if data['member']
109
+ member.update_global_name(data['author']['global_name']) if data['author']['global_name']
103
110
  else
104
111
  Discordrb::LOGGER.debug("Member with ID #{data['author']['id']} not cached (possibly left the server).")
105
112
  member = if data['member']
106
113
  member_data = data['author'].merge(data['member'])
107
- Member.new(member_data, bot)
114
+ Member.new(member_data, @server, bot)
108
115
  else
109
116
  @bot.ensure_user(data['author'])
110
117
  end
@@ -114,8 +121,6 @@ module Discordrb
114
121
  end
115
122
  end
116
123
 
117
- @webhook_id = data['webhook_id'].to_i if data['webhook_id']
118
-
119
124
  @timestamp = Time.parse(data['timestamp']) if data['timestamp']
120
125
  @edited_timestamp = data['edited_timestamp'].nil? ? nil : Time.parse(data['edited_timestamp'])
121
126
  @edited = !@edited_timestamp.nil?
@@ -149,43 +154,53 @@ module Discordrb
149
154
 
150
155
  @embeds = []
151
156
  @embeds = data['embeds'].map { |e| Embed.new(e, self) } if data['embeds']
157
+
158
+ @components = []
159
+ @components = data['components'].map { |component_data| Components.from_data(component_data, @bot) } if data['components']
152
160
  end
153
161
 
154
162
  # Replies to this message with the specified content.
155
163
  # @deprecated Please use {#respond}.
164
+ # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
165
+ # @return (see #respond)
156
166
  # @see Channel#send_message
157
167
  def reply(content)
158
168
  @channel.send_message(content)
159
169
  end
160
170
 
161
- # Sends a message to this channel.
171
+ # Responds to this message as an inline reply.
162
172
  # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
163
173
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
164
174
  # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
165
175
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
166
176
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
167
177
  # @param mention_user [true, false] Whether the user that is being replied to should be pinged by the reply.
168
- # @return [Message] the message that was sent.
169
- def reply!(content, tts: false, embed: nil, attachments: nil, allowed_mentions: {}, mention_user: false)
178
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
179
+ # @return (see #respond)
180
+ def reply!(content, tts: false, embed: nil, attachments: nil, allowed_mentions: {}, mention_user: false, components: nil)
170
181
  allowed_mentions = { parse: [] } if allowed_mentions == false
171
182
  allowed_mentions = allowed_mentions.to_hash.transform_keys(&:to_sym)
172
183
  allowed_mentions[:replied_user] = mention_user
173
184
 
174
- respond(content, tts, embed, attachments, allowed_mentions, self)
185
+ respond(content, tts, embed, attachments, allowed_mentions, self, components)
175
186
  end
176
187
 
177
188
  # (see Channel#send_message)
178
- def respond(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
179
- @channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference)
189
+ def respond(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
190
+ @channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference, components)
180
191
  end
181
192
 
182
193
  # Edits this message to have the specified content instead.
183
194
  # You can only edit your own messages.
184
195
  # @param new_content [String] the new content the message should have.
185
- # @param new_embed [Hash, Discordrb::Webhooks::Embed, nil] The new embed the message should have. If `nil` the message will be changed to have no embed.
196
+ # @param new_embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed>, nil] The new embeds the message should have. If `nil` the message will be changed to have no embeds.
197
+ # @param new_components [View, Array<Hash>] The new components the message should have. If `nil` the message will be changed to have no components.
186
198
  # @return [Message] the resulting message.
187
- def edit(new_content, new_embed = nil)
188
- response = API::Channel.edit_message(@bot.token, @channel.id, @id, new_content, [], new_embed ? new_embed.to_hash : nil)
199
+ def edit(new_content, new_embeds = nil, new_components = nil)
200
+ new_embeds = (new_embeds.instance_of?(Array) ? new_embeds.map(&:to_hash) : [new_embeds&.to_hash]).compact
201
+ new_components = new_components&.to_a || []
202
+
203
+ response = API::Channel.edit_message(@bot.token, @channel.id, @id, new_content, [], new_embeds, new_components)
189
204
  Message.new(JSON.parse(response), @bot)
190
205
  end
191
206
 
@@ -222,6 +237,19 @@ module Discordrb
222
237
  @bot.add_await!(Discordrb::Events::MessageEvent, { from: @author.id, in: @channel.id }.merge(attributes), &block)
223
238
  end
224
239
 
240
+ # Add an {Await} for a reaction to be added on this message.
241
+ # @see Bot#add_await
242
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#await_reaction!} instead.
243
+ def await_reaction(key, attributes = {}, &block)
244
+ @bot.add_await(key, Discordrb::Events::ReactionAddEvent, { message: @id }.merge(attributes), &block)
245
+ end
246
+
247
+ # Add a blocking {Await} for a reaction to be added on this message.
248
+ # @see Bot#add_await!
249
+ def await_reaction!(attributes = {}, &block)
250
+ @bot.add_await!(Discordrb::Events::ReactionAddEvent, { message: @id }.merge(attributes), &block)
251
+ end
252
+
225
253
  # @return [true, false] whether this message was sent by the current {Bot}.
226
254
  def from_bot?
227
255
  @author&.current_bot?
@@ -276,14 +304,38 @@ module Discordrb
276
304
  # @return [Array<User>] the users who used this reaction
277
305
  def reacted_with(reaction, limit: 100)
278
306
  reaction = reaction.to_reaction if reaction.respond_to?(:to_reaction)
307
+ reaction = reaction.to_s if reaction.respond_to?(:to_s)
308
+
309
+ get_reactions = proc do |fetch_limit, after_id = nil|
310
+ resp = API::Channel.get_reactions(@bot.token, @channel.id, @id, reaction, nil, after_id, fetch_limit)
311
+ return JSON.parse(resp).map { |d| User.new(d, @bot) }
312
+ end
313
+
314
+ # Can be done without pagination
315
+ return get_reactions.call(limit) if limit && limit <= 100
316
+
279
317
  paginator = Paginator.new(limit, :down) do |last_page|
280
- after_id = last_page.last.id if last_page
281
- last_page = JSON.parse(API::Channel.get_reactions(@bot.token, @channel.id, @id, reaction, nil, after_id, limit))
282
- last_page.map { |d| User.new(d, @bot) }
318
+ if last_page && last_page.count < 100
319
+ []
320
+ else
321
+ get_reactions.call(100, last_page&.last&.id)
322
+ end
283
323
  end
324
+
284
325
  paginator.to_a
285
326
  end
286
327
 
328
+ # Returns a hash of all reactions to a message as keys and the users that reacted to it as values.
329
+ # @param limit [Integer] the limit of how many users to retrieve per distinct reaction emoji. `nil` will return all users
330
+ # @example Get all the users that reacted to a message for a giveaway.
331
+ # giveaway_participants = message.all_reaction_users
332
+ # @return [Hash<String => Array<User>>] A hash mapping the string representation of a
333
+ # reaction to an array of users.
334
+ def all_reaction_users(limit: 100)
335
+ all_reactions = @reactions.map { |r| { r.to_s => reacted_with(r, limit: limit) } }
336
+ all_reactions.reduce({}, :merge)
337
+ end
338
+
287
339
  # Deletes a reaction made by a user on this message.
288
340
  # @param user [User, String, Integer] the user or user ID who used this reaction
289
341
  # @param reaction [String, #to_reaction] the reaction to remove
@@ -322,6 +374,12 @@ module Discordrb
322
374
  !@referenced_message.nil?
323
375
  end
324
376
 
377
+ # Whether or not this message was of type "CHAT_INPUT_COMMAND"
378
+ # @return [true, false]
379
+ def chat_input_command?
380
+ @type == 20
381
+ end
382
+
325
383
  # @return [Message, nil] the Message this Message was sent in reply to.
326
384
  def referenced_message
327
385
  return @referenced_message if @referenced_message
@@ -330,5 +388,27 @@ module Discordrb
330
388
  referenced_channel = @bot.channel(@message_reference['channel_id'])
331
389
  @referenced_message = referenced_channel.message(@message_reference['message_id'])
332
390
  end
391
+
392
+ # @return [Array<Components::Button>]
393
+ def buttons
394
+ results = @components.collect do |component|
395
+ case component
396
+ when Components::Button
397
+ component
398
+ when Components::ActionRow
399
+ component.buttons
400
+ end
401
+ end
402
+
403
+ results.flatten.compact
404
+ end
405
+
406
+ # to_message -> self or message
407
+ # @return [Discordrb::Message]
408
+ def to_message
409
+ self
410
+ end
411
+
412
+ alias_method :message, :to_message
333
413
  end
334
414
  end
@@ -4,6 +4,12 @@ module Discordrb
4
4
  # A permissions overwrite, when applied to channels describes additional
5
5
  # permissions a member needs to perform certain actions in context.
6
6
  class Overwrite
7
+ # Types of overwrites mapped to their API value.
8
+ TYPES = {
9
+ role: 0,
10
+ member: 1
11
+ }.freeze
12
+
7
13
  # @return [Integer] ID of the thing associated with this overwrite type
8
14
  attr_accessor :id
9
15
 
@@ -32,14 +38,14 @@ module Discordrb
32
38
  # @example Create an overwrite by ID and permissions bits
33
39
  # Overwrite.new(120571255635181568, type: 'member', allow: 1024, deny: 0)
34
40
  # @param object [Integer, #id] the ID or object this overwrite is for
35
- # @param type [String] the type of object this overwrite is for (only required if object is an Integer)
36
- # @param allow [Integer, Permissions] allowed permissions for this overwrite, by bits or a Permissions object
37
- # @param deny [Integer, Permissions] denied permissions for this overwrite, by bits or a Permissions object
41
+ # @param type [String, Symbol, Integer] the type of object this overwrite is for (only required if object is an Integer)
42
+ # @param allow [String, Integer, Permissions] allowed permissions for this overwrite, by bits or a Permissions object
43
+ # @param deny [String, Integer, Permissions] denied permissions for this overwrite, by bits or a Permissions object
38
44
  # @raise [ArgumentError] if type is not :member or :role
39
45
  def initialize(object = nil, type: nil, allow: 0, deny: 0)
40
46
  if type
41
- type = type.to_sym
42
- raise ArgumentError, 'Overwrite type must be :member or :role' unless (type != :member) || (type != :role)
47
+ type = TYPES.value?(type) ? TYPES.key(type) : type.to_sym
48
+ raise ArgumentError, 'Overwrite type must be :member or :role' unless type
43
49
  end
44
50
 
45
51
  @id = object.respond_to?(:id) ? object.id : object
@@ -71,7 +77,7 @@ module Discordrb
71
77
  def self.from_hash(data)
72
78
  new(
73
79
  data['id'].to_i,
74
- type: data['type'],
80
+ type: TYPES.key(data['type']),
75
81
  allow: Permissions.new(data['allow']),
76
82
  deny: Permissions.new(data['deny'])
77
83
  )
@@ -93,7 +99,7 @@ module Discordrb
93
99
  def to_hash
94
100
  {
95
101
  id: id,
96
- type: type,
102
+ type: TYPES[type],
97
103
  allow: allow.bits,
98
104
  deny: deny.bits
99
105
  }
@@ -32,6 +32,42 @@ module Discordrb
32
32
  # @return [Integer] the position of this role in the hierarchy
33
33
  attr_reader :position
34
34
 
35
+ # @return [String, nil] The icon hash for this role.
36
+ attr_reader :icon
37
+
38
+ # @return [Tags, nil] The role tags
39
+ attr_reader :tags
40
+
41
+ # Wrapper for the role tags
42
+ class Tags
43
+ # @return [Integer, nil] The ID of the bot this role belongs to
44
+ attr_reader :bot_id
45
+
46
+ # @return [Integer, nil] The ID of the integration this role belongs to
47
+ attr_reader :integration_id
48
+
49
+ # @return [true, false] Whether this is the guild's Booster role
50
+ attr_reader :premium_subscriber
51
+
52
+ # @return [Integer, nil] The id of this role's subscription sku and listing
53
+ attr_reader :subscription_listing_id
54
+
55
+ # @return [true, false] Whether this role is available for purchase
56
+ attr_reader :available_for_purchase
57
+
58
+ # @return [true, false] Whether this role is a guild's linked role
59
+ attr_reader :guild_connections
60
+
61
+ def initialize(data)
62
+ @bot_id = data['bot_id']&.resolve_id
63
+ @integration_id = data['integration_id']&.resolve_id
64
+ @premium_subscriber = data.key?('premium_subscriber')
65
+ @subscription_listing_id = data['subscription_listing_id']&.resolve_id
66
+ @available_for_purchase = data.key?('available_for_purchase')
67
+ @guild_connections = data.key?('guild_connections')
68
+ end
69
+ end
70
+
35
71
  # This class is used internally as a wrapper to a Role object that allows easy writing of permission data.
36
72
  class RoleWriter
37
73
  # @!visibility private
@@ -67,6 +103,10 @@ module Discordrb
67
103
  @managed = data['managed']
68
104
 
69
105
  @colour = ColourRGB.new(data['color'])
106
+
107
+ @icon = data['icon']
108
+
109
+ @tags = Tags.new(data['tags']) if data['tags']
70
110
  end
71
111
 
72
112
  # @return [String] a string that will mention this role, if it is mentionable.
@@ -92,6 +132,7 @@ module Discordrb
92
132
  @colour = other.colour
93
133
  @position = other.position
94
134
  @managed = other.managed
135
+ @icon = other.icon
95
136
  end
96
137
 
97
138
  # Updates the data cache from a hash containing data
@@ -128,6 +169,20 @@ module Discordrb
128
169
  update_role_data(colour: colour)
129
170
  end
130
171
 
172
+ # Upload a role icon for servers with the ROLE_ICONS feature.
173
+ # @param file [File]
174
+ def icon=(file)
175
+ update_role_data(icon: file)
176
+ end
177
+
178
+ # @param format ['webp', 'png', 'jpeg']
179
+ # @return [String] URL to the icon on Discord's CDN.
180
+ def icon_url(format = 'webp')
181
+ return nil unless @icon
182
+
183
+ Discordrb::API.role_icon_url(@id, @icon, format)
184
+ end
185
+
131
186
  alias_method :color=, :colour=
132
187
 
133
188
  # Changes this role's permissions to a fixed bitfield. This allows setting multiple permissions at once with just
@@ -184,7 +239,9 @@ module Discordrb
184
239
  (new_data[:colour] || @colour).combined,
185
240
  new_data[:hoist].nil? ? @hoist : new_data[:hoist],
186
241
  new_data[:mentionable].nil? ? @mentionable : new_data[:mentionable],
187
- new_data[:permissions] || @permissions.bits)
242
+ new_data[:permissions] || @permissions.bits,
243
+ nil,
244
+ new_data.key?(:icon) ? new_data[:icon] : :undef)
188
245
  update_data(new_data)
189
246
  end
190
247
  end