discordrb 3.4.3 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +44 -18
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -1
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  5. data/.github/workflows/codeql.yml +65 -0
  6. data/.markdownlint.json +4 -0
  7. data/.rubocop.yml +8 -2
  8. data/CHANGELOG.md +390 -225
  9. data/LICENSE.txt +1 -1
  10. data/README.md +37 -25
  11. data/discordrb-webhooks.gemspec +4 -1
  12. data/discordrb.gemspec +9 -6
  13. data/lib/discordrb/api/application.rb +202 -0
  14. data/lib/discordrb/api/channel.rb +177 -11
  15. data/lib/discordrb/api/interaction.rb +54 -0
  16. data/lib/discordrb/api/invite.rb +2 -2
  17. data/lib/discordrb/api/server.rb +40 -19
  18. data/lib/discordrb/api/user.rb +8 -3
  19. data/lib/discordrb/api/webhook.rb +57 -0
  20. data/lib/discordrb/api.rb +19 -5
  21. data/lib/discordrb/bot.rb +317 -32
  22. data/lib/discordrb/cache.rb +27 -22
  23. data/lib/discordrb/commands/command_bot.rb +6 -4
  24. data/lib/discordrb/commands/container.rb +1 -1
  25. data/lib/discordrb/commands/parser.rb +2 -2
  26. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  27. data/lib/discordrb/container.rb +132 -3
  28. data/lib/discordrb/data/attachment.rb +15 -0
  29. data/lib/discordrb/data/audit_logs.rb +3 -3
  30. data/lib/discordrb/data/channel.rb +167 -23
  31. data/lib/discordrb/data/component.rb +229 -0
  32. data/lib/discordrb/data/integration.rb +42 -3
  33. data/lib/discordrb/data/interaction.rb +800 -0
  34. data/lib/discordrb/data/invite.rb +1 -1
  35. data/lib/discordrb/data/member.rb +108 -33
  36. data/lib/discordrb/data/message.rb +99 -19
  37. data/lib/discordrb/data/overwrite.rb +13 -7
  38. data/lib/discordrb/data/role.rb +58 -1
  39. data/lib/discordrb/data/server.rb +82 -80
  40. data/lib/discordrb/data/user.rb +69 -9
  41. data/lib/discordrb/data/webhook.rb +97 -4
  42. data/lib/discordrb/data.rb +3 -0
  43. data/lib/discordrb/errors.rb +44 -3
  44. data/lib/discordrb/events/channels.rb +1 -1
  45. data/lib/discordrb/events/interactions.rb +482 -0
  46. data/lib/discordrb/events/message.rb +9 -6
  47. data/lib/discordrb/events/presence.rb +21 -14
  48. data/lib/discordrb/events/reactions.rb +0 -1
  49. data/lib/discordrb/events/threads.rb +96 -0
  50. data/lib/discordrb/gateway.rb +30 -17
  51. data/lib/discordrb/permissions.rb +59 -34
  52. data/lib/discordrb/version.rb +1 -1
  53. data/lib/discordrb/voice/encoder.rb +2 -2
  54. data/lib/discordrb/voice/network.rb +18 -7
  55. data/lib/discordrb/voice/sodium.rb +3 -1
  56. data/lib/discordrb/voice/voice_bot.rb +3 -3
  57. data/lib/discordrb/webhooks.rb +2 -0
  58. data/lib/discordrb.rb +37 -4
  59. metadata +48 -14
  60. data/.codeclimate.yml +0 -16
  61. data/.travis.yml +0 -32
  62. data/bin/travis_build_docs.sh +0 -17
@@ -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