discordrb 3.3.0 → 3.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +126 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
  5. data/.github/pull_request_template.md +37 -0
  6. data/.rubocop.yml +34 -37
  7. data/.travis.yml +5 -6
  8. data/CHANGELOG.md +504 -347
  9. data/Gemfile +2 -0
  10. data/LICENSE.txt +1 -1
  11. data/README.md +61 -79
  12. data/Rakefile +2 -0
  13. data/bin/console +1 -0
  14. data/discordrb-webhooks.gemspec +6 -6
  15. data/discordrb.gemspec +18 -18
  16. data/lib/discordrb/allowed_mentions.rb +36 -0
  17. data/lib/discordrb/api/channel.rb +62 -39
  18. data/lib/discordrb/api/invite.rb +3 -3
  19. data/lib/discordrb/api/server.rb +57 -50
  20. data/lib/discordrb/api/user.rb +9 -8
  21. data/lib/discordrb/api/webhook.rb +6 -6
  22. data/lib/discordrb/api.rb +40 -15
  23. data/lib/discordrb/await.rb +0 -1
  24. data/lib/discordrb/bot.rb +175 -73
  25. data/lib/discordrb/cache.rb +4 -2
  26. data/lib/discordrb/colour_rgb.rb +43 -0
  27. data/lib/discordrb/commands/command_bot.rb +30 -9
  28. data/lib/discordrb/commands/container.rb +20 -23
  29. data/lib/discordrb/commands/parser.rb +18 -18
  30. data/lib/discordrb/commands/rate_limiter.rb +3 -2
  31. data/lib/discordrb/container.rb +77 -17
  32. data/lib/discordrb/data/activity.rb +271 -0
  33. data/lib/discordrb/data/application.rb +50 -0
  34. data/lib/discordrb/data/attachment.rb +56 -0
  35. data/lib/discordrb/data/audit_logs.rb +345 -0
  36. data/lib/discordrb/data/channel.rb +849 -0
  37. data/lib/discordrb/data/embed.rb +251 -0
  38. data/lib/discordrb/data/emoji.rb +82 -0
  39. data/lib/discordrb/data/integration.rb +83 -0
  40. data/lib/discordrb/data/invite.rb +137 -0
  41. data/lib/discordrb/data/member.rb +297 -0
  42. data/lib/discordrb/data/message.rb +334 -0
  43. data/lib/discordrb/data/overwrite.rb +102 -0
  44. data/lib/discordrb/data/profile.rb +91 -0
  45. data/lib/discordrb/data/reaction.rb +33 -0
  46. data/lib/discordrb/data/recipient.rb +34 -0
  47. data/lib/discordrb/data/role.rb +191 -0
  48. data/lib/discordrb/data/server.rb +1002 -0
  49. data/lib/discordrb/data/user.rb +204 -0
  50. data/lib/discordrb/data/voice_region.rb +45 -0
  51. data/lib/discordrb/data/voice_state.rb +41 -0
  52. data/lib/discordrb/data/webhook.rb +145 -0
  53. data/lib/discordrb/data.rb +25 -4180
  54. data/lib/discordrb/errors.rb +2 -1
  55. data/lib/discordrb/events/bans.rb +7 -5
  56. data/lib/discordrb/events/channels.rb +2 -0
  57. data/lib/discordrb/events/guilds.rb +16 -9
  58. data/lib/discordrb/events/invites.rb +125 -0
  59. data/lib/discordrb/events/members.rb +6 -2
  60. data/lib/discordrb/events/message.rb +69 -27
  61. data/lib/discordrb/events/presence.rb +14 -4
  62. data/lib/discordrb/events/raw.rb +1 -3
  63. data/lib/discordrb/events/reactions.rb +49 -3
  64. data/lib/discordrb/events/typing.rb +6 -4
  65. data/lib/discordrb/events/voice_server_update.rb +47 -0
  66. data/lib/discordrb/events/voice_state_update.rb +15 -10
  67. data/lib/discordrb/events/webhooks.rb +9 -6
  68. data/lib/discordrb/gateway.rb +72 -57
  69. data/lib/discordrb/id_object.rb +39 -0
  70. data/lib/discordrb/light/integrations.rb +1 -1
  71. data/lib/discordrb/light/light_bot.rb +1 -1
  72. data/lib/discordrb/logger.rb +4 -4
  73. data/lib/discordrb/paginator.rb +57 -0
  74. data/lib/discordrb/permissions.rb +103 -8
  75. data/lib/discordrb/version.rb +1 -1
  76. data/lib/discordrb/voice/encoder.rb +16 -7
  77. data/lib/discordrb/voice/network.rb +84 -43
  78. data/lib/discordrb/voice/sodium.rb +96 -0
  79. data/lib/discordrb/voice/voice_bot.rb +34 -26
  80. data/lib/discordrb.rb +73 -0
  81. metadata +98 -60
  82. /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
@@ -0,0 +1,849 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # A Discord channel, including data like the topic
5
+ class Channel
6
+ include IDObject
7
+
8
+ # Map of channel types
9
+ TYPES = {
10
+ text: 0,
11
+ dm: 1,
12
+ voice: 2,
13
+ group: 3,
14
+ category: 4,
15
+ news: 5,
16
+ store: 6
17
+ }.freeze
18
+
19
+ # @return [String] this channel's name.
20
+ attr_reader :name
21
+
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
26
+ attr_reader :parent_id
27
+
28
+ # @return [Integer] the type of this channel
29
+ # @see TYPES
30
+ attr_reader :type
31
+
32
+ # @return [Integer, nil] the ID of the owner of the group channel or nil if this is not a group channel.
33
+ attr_reader :owner_id
34
+
35
+ # @return [Array<Recipient>, nil] the array of recipients of the private messages, or nil if this is not a Private channel
36
+ attr_reader :recipients
37
+
38
+ # @return [String] the channel's topic
39
+ attr_reader :topic
40
+
41
+ # @return [Integer] the bitrate (in bps) of the channel
42
+ attr_reader :bitrate
43
+
44
+ # @return [Integer] the amount of users that can be in the channel. `0` means it is unlimited.
45
+ attr_reader :user_limit
46
+ alias_method :limit, :user_limit
47
+
48
+ # @return [Integer] the channel's position on the channel list
49
+ attr_reader :position
50
+
51
+ # @return [true, false] if this channel is marked as nsfw
52
+ attr_reader :nsfw
53
+ alias_method :nsfw?, :nsfw
54
+
55
+ # @return [Integer] the amount of time (in seconds) users need to wait to send in between messages.
56
+ attr_reader :rate_limit_per_user
57
+ alias_method :slowmode_rate, :rate_limit_per_user
58
+
59
+ # @return [true, false] whether or not this channel is a PM or group channel.
60
+ def private?
61
+ pm? || group?
62
+ end
63
+
64
+ # @return [String] a string that will mention the channel as a clickable link on Discord.
65
+ def mention
66
+ "<##{@id}>"
67
+ end
68
+
69
+ # @return [Recipient, nil] the recipient of the private messages, or nil if this is not a PM channel
70
+ def recipient
71
+ @recipients.first if pm?
72
+ end
73
+
74
+ # @!visibility private
75
+ def initialize(data, bot, server = nil)
76
+ @bot = bot
77
+ # data is sometimes a Hash and other times an array of Hashes, you only want the last one if it's an array
78
+ data = data[-1] if data.is_a?(Array)
79
+
80
+ @id = data['id'].to_i
81
+ @type = data['type'] || 0
82
+ @topic = data['topic']
83
+ @bitrate = data['bitrate']
84
+ @user_limit = data['user_limit']
85
+ @position = data['position']
86
+ @parent_id = data['parent_id'].to_i if data['parent_id']
87
+
88
+ if private?
89
+ @recipients = []
90
+ data['recipients']&.each do |recipient|
91
+ recipient_user = bot.ensure_user(recipient)
92
+ @recipients << Recipient.new(recipient_user, self, bot)
93
+ end
94
+ if pm?
95
+ @name = @recipients.first.username
96
+ else
97
+ @name = data['name']
98
+ @owner_id = data['owner_id']
99
+ end
100
+ else
101
+ @name = data['name']
102
+ @server = server || bot.server(data['guild_id'].to_i)
103
+ end
104
+
105
+ @nsfw = data['nsfw'] || false
106
+ @rate_limit_per_user = data['rate_limit_per_user'] || 0
107
+
108
+ process_permission_overwrites(data['permission_overwrites'])
109
+ end
110
+
111
+ # @return [true, false] whether or not this channel is a text channel
112
+ def text?
113
+ @type.zero?
114
+ end
115
+
116
+ # @return [true, false] whether or not this channel is a PM channel.
117
+ def pm?
118
+ @type == 1
119
+ end
120
+
121
+ # @return [true, false] whether or not this channel is a voice channel.
122
+ def voice?
123
+ @type == 2
124
+ end
125
+
126
+ # @return [true, false] whether or not this channel is a group channel.
127
+ def group?
128
+ @type == 3
129
+ end
130
+
131
+ # @return [true, false] whether or not this channel is a category channel.
132
+ def category?
133
+ @type == 4
134
+ end
135
+
136
+ # @return [true, false] whether or not this channel is a news channel.
137
+ def news?
138
+ @type == 5
139
+ end
140
+
141
+ # @return [true, false] whether or not this channel is a store channel.
142
+ def store?
143
+ @type == 6
144
+ end
145
+
146
+ # @return [Channel, nil] the category channel, if this channel is in a category
147
+ def category
148
+ @bot.channel(@parent_id) if @parent_id
149
+ end
150
+
151
+ alias_method :parent, :category
152
+
153
+ # Sets this channels parent category
154
+ # @param channel [Channel, String, Integer] the target category channel, or its ID
155
+ # @raise [ArgumentError] if the target channel isn't a category
156
+ def category=(channel)
157
+ channel = @bot.channel(channel)
158
+ raise ArgumentError, 'Cannot set parent category to a channel that isn\'t a category' unless channel.category?
159
+
160
+ update_channel_data(parent_id: channel.id)
161
+ end
162
+
163
+ alias_method :parent=, :category=
164
+
165
+ # Sorts this channel's position to follow another channel.
166
+ # @param other [Channel, String, Integer, nil] The channel, or its ID, below which this channel should be sorted. If the given
167
+ # channel is a category, this channel will be sorted at the top of that category. If it is `nil`, the channel will
168
+ # be sorted at the top of the channel list.
169
+ # @param lock_permissions [true, false] Whether the channel's permissions should be synced to the category's
170
+ def sort_after(other = nil, lock_permissions = false)
171
+ raise TypeError, 'other must be one of Channel, NilClass, String, or Integer' unless other.is_a?(Channel) || other.nil? || other.respond_to?(:resolve_id)
172
+
173
+ other = @bot.channel(other.resolve_id) if other
174
+
175
+ # Container for the API request payload
176
+ move_argument = []
177
+
178
+ if other
179
+ raise ArgumentError, 'Can only sort a channel after a channel of the same type!' unless other.category? || (@type == other.type)
180
+
181
+ raise ArgumentError, 'Can only sort a channel after a channel in the same server!' unless other.server == server
182
+
183
+ # Store `others` parent (or if `other` is a category itself)
184
+ parent = if category? && other.category?
185
+ # If we're sorting two categories, there is no new parent
186
+ nil
187
+ elsif other.category?
188
+ # `other` is the category this channel will be moved into
189
+ other
190
+ else
191
+ # `other`'s parent is the category this channel will be
192
+ # moved into (if it exists)
193
+ other.parent
194
+ end
195
+ end
196
+
197
+ # Collect and sort the IDs within the context (category or not) that we
198
+ # need to form our payload with
199
+ ids = if parent
200
+ parent.children
201
+ else
202
+ @server.channels.reject(&:parent_id).select { |c| c.type == @type }
203
+ end.sort_by(&:position).map(&:id)
204
+
205
+ # Move our channel ID after the target ID by deleting it,
206
+ # getting the index of `other`, and inserting it after.
207
+ ids.delete(@id) if ids.include?(@id)
208
+ index = other ? (ids.index { |c| c == other.id } || -1) + 1 : 0
209
+ ids.insert(index, @id)
210
+
211
+ # Generate `move_argument`, making the positions in order from how
212
+ # we have sorted them in the above logic
213
+ ids.each_with_index do |id, pos|
214
+ # These keys are present in each element
215
+ hash = { id: id, position: pos }
216
+
217
+ # Conditionally add `lock_permissions` and `parent_id` if we're
218
+ # iterating past ourselves
219
+ if id == @id
220
+ hash[:lock_permissions] = true if lock_permissions
221
+ hash[:parent_id] = parent.nil? ? nil : parent.id
222
+ end
223
+
224
+ # Add it to the stack
225
+ move_argument << hash
226
+ end
227
+
228
+ API::Server.update_channel_positions(@bot.token, @server.id, move_argument)
229
+ end
230
+
231
+ # Sets whether this channel is NSFW
232
+ # @param nsfw [true, false]
233
+ # @raise [ArgumentError] if value isn't one of true, false
234
+ def nsfw=(nsfw)
235
+ raise ArgumentError, 'nsfw value must be true or false' unless nsfw.is_a?(TrueClass) || nsfw.is_a?(FalseClass)
236
+
237
+ update_channel_data(nsfw: nsfw)
238
+ end
239
+
240
+ # This channel's permission overwrites
241
+ # @overload permission_overwrites
242
+ # The overwrites represented as a hash of role/user ID
243
+ # to an Overwrite object
244
+ # @return [Hash<Integer => Overwrite>] the channel's permission overwrites
245
+ # @overload permission_overwrites(type)
246
+ # Return an array of a certain type of overwrite
247
+ # @param type [Symbol] the kind of overwrite to return
248
+ # @return [Array<Overwrite>]
249
+ def permission_overwrites(type = nil)
250
+ return @permission_overwrites unless type
251
+
252
+ @permission_overwrites.values.select { |e| e.type == type }
253
+ end
254
+
255
+ alias_method :overwrites, :permission_overwrites
256
+
257
+ # Bulk sets this channels permission overwrites
258
+ # @param overwrites [Array<Overwrite>]
259
+ def permission_overwrites=(overwrites)
260
+ update_channel_data(permission_overwrites: overwrites)
261
+ end
262
+
263
+ # Sets the amount of time (in seconds) users have to wait in between sending messages.
264
+ # @param rate [Integer]
265
+ # @raise [ArgumentError] if value isn't between 0 and 120
266
+ def rate_limit_per_user=(rate)
267
+ raise ArgumentError, 'rate_limit_per_user must be between 0 and 120' unless rate.between?(0, 120)
268
+
269
+ update_channel_data(rate_limit_per_user: rate)
270
+ end
271
+
272
+ alias_method :slowmode_rate=, :rate_limit_per_user=
273
+
274
+ # Syncs this channels overwrites with its parent category
275
+ # @raise [RuntimeError] if this channel is not in a category
276
+ def sync_overwrites
277
+ raise 'Cannot sync overwrites on a channel with no parent category' unless parent
278
+
279
+ self.permission_overwrites = parent.permission_overwrites
280
+ end
281
+
282
+ alias_method :sync, :sync_overwrites
283
+
284
+ # @return [true, false, nil] whether this channels permissions match the permission overwrites of the category that it's in, or nil if it is not in a category
285
+ def synchronized?
286
+ return unless parent
287
+
288
+ permission_overwrites == parent.permission_overwrites
289
+ end
290
+
291
+ alias_method :synced?, :synchronized?
292
+
293
+ # Returns the children of this channel, if it is a category. Otherwise returns an empty array.
294
+ # @return [Array<Channel>]
295
+ def children
296
+ return [] unless category?
297
+
298
+ server.channels.select { |c| c.parent_id == id }
299
+ end
300
+
301
+ alias_method :channels, :children
302
+
303
+ # Returns the text channels in this category, if it is a category channel. Otherwise returns an empty array.
304
+ # @return [Array<Channel>]
305
+ def text_channels
306
+ children.select(&:text?)
307
+ end
308
+
309
+ # Returns the voice channels in this category, if it is a category channel. Otherwise returns an empty array.
310
+ # @return [Array<Channel>]
311
+ def voice_channels
312
+ children.select(&:voice?)
313
+ end
314
+
315
+ # @return [Overwrite] any member-type permission overwrites on this channel
316
+ def member_overwrites
317
+ permission_overwrites :member
318
+ end
319
+
320
+ # @return [Overwrite] any role-type permission overwrites on this channel
321
+ def role_overwrites
322
+ permission_overwrites :role
323
+ end
324
+
325
+ # @return [true, false] whether or not this channel is the default channel
326
+ def default_channel?
327
+ server.default_channel == self
328
+ end
329
+
330
+ alias_method :default?, :default_channel?
331
+
332
+ # @return [true, false] whether or not this channel has slowmode enabled
333
+ def slowmode?
334
+ @rate_limit_per_user != 0
335
+ end
336
+
337
+ # Sends a message to this channel.
338
+ # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
339
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
340
+ # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
341
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
342
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
343
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
344
+ # @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)
347
+ end
348
+
349
+ alias_method :send, :send_message
350
+
351
+ # Sends a temporary message to this channel.
352
+ # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
353
+ # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
354
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
355
+ # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
356
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
357
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
358
+ # @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)
361
+ end
362
+
363
+ # Convenience method to send a message with an embed.
364
+ # @example Send a message with an embed
365
+ # channel.send_embed do |embed|
366
+ # embed.title = 'The Ruby logo'
367
+ # embed.image = Discordrb::Webhooks::EmbedImage.new(url: 'https://www.ruby-lang.org/images/header-ruby-logo.png')
368
+ # end
369
+ # @param message [String] The message that should be sent along with the embed. If this is the empty string, only the embed will be shown.
370
+ # @param embed [Discordrb::Webhooks::Embed, nil] The embed to start the building process with, or nil if one should be created anew.
371
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
372
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
373
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
374
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
375
+ # @yield [embed] Yields the embed to allow for easy building inside a block.
376
+ # @yieldparam embed [Discordrb::Webhooks::Embed] The embed from the parameters, or a new one.
377
+ # @return [Message] The resulting message.
378
+ def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowed_mentions = nil, message_reference = nil)
379
+ embed ||= Discordrb::Webhooks::Embed.new
380
+ yield(embed) if block_given?
381
+ send_message(message, tts, embed, attachments, allowed_mentions, message_reference)
382
+ end
383
+
384
+ # Sends multiple messages to a channel
385
+ # @param content [Array<String>] The messages to send.
386
+ def send_multiple(content)
387
+ content.each { |e| send_message(e) }
388
+ end
389
+
390
+ # Splits a message into chunks whose length is at most the Discord character limit, then sends them individually.
391
+ # Useful for sending long messages, but be wary of rate limits!
392
+ def split_send(content)
393
+ send_multiple(Discordrb.split_message(content))
394
+ nil
395
+ end
396
+
397
+ # Sends a file to this channel. If it is an image, it will be embedded.
398
+ # @param file [File] The file to send. There's no clear size limit for this, you'll have to attempt it for yourself (most non-image files are fine, large images may fail to embed)
399
+ # @param caption [string] The caption for the file.
400
+ # @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
401
+ # @param filename [String] Overrides the filename of the uploaded file
402
+ # @param spoiler [true, false] Whether or not this file should appear as a spoiler.
403
+ # @example Send a file from disk
404
+ # channel.send_file(File.open('rubytaco.png', 'r'))
405
+ def send_file(file, caption: nil, tts: false, filename: nil, spoiler: nil)
406
+ @bot.send_file(@id, file, caption: caption, tts: tts, filename: filename, spoiler: spoiler)
407
+ end
408
+
409
+ # Deletes a message on this channel. Mostly useful in case a message needs to be deleted when only the ID is known
410
+ # @param message [Message, String, Integer, String, Integer] The message, or its ID, that should be deleted.
411
+ def delete_message(message)
412
+ API::Channel.delete_message(@bot.token, @id, message.resolve_id)
413
+ end
414
+
415
+ # Permanently deletes this channel
416
+ # @param reason [String] The reason the for the channel deletion.
417
+ def delete(reason = nil)
418
+ API::Channel.delete(@bot.token, @id, reason)
419
+ end
420
+
421
+ # Sets this channel's name. The name must be alphanumeric with dashes, unless this is a voice channel (then there are no limitations)
422
+ # @param name [String] The new name.
423
+ def name=(name)
424
+ update_channel_data(name: name)
425
+ end
426
+
427
+ # Sets this channel's topic.
428
+ # @param topic [String] The new topic.
429
+ def topic=(topic)
430
+ raise 'Tried to set topic on voice channel' if voice?
431
+
432
+ update_channel_data(topic: topic)
433
+ end
434
+
435
+ # Sets this channel's bitrate.
436
+ # @param bitrate [Integer] The new bitrate (in bps). Number has to be between 8000-96000 (128000 for VIP servers)
437
+ def bitrate=(bitrate)
438
+ raise 'Tried to set bitrate on text channel' if text?
439
+
440
+ update_channel_data(bitrate: bitrate)
441
+ end
442
+
443
+ # Sets this channel's user limit.
444
+ # @param limit [Integer] The new user limit. `0` for unlimited, has to be a number between 0-99
445
+ def user_limit=(limit)
446
+ raise 'Tried to set user_limit on text channel' if text?
447
+
448
+ update_channel_data(user_limit: limit)
449
+ end
450
+
451
+ alias_method :limit=, :user_limit=
452
+
453
+ # Sets this channel's position in the list.
454
+ # @param position [Integer] The new position.
455
+ def position=(position)
456
+ update_channel_data(position: position)
457
+ end
458
+
459
+ # Defines a permission overwrite for this channel that sets the specified thing to the specified allow and deny
460
+ # permission sets, or change an existing one.
461
+ # @overload define_overwrite(overwrite)
462
+ # @param thing [Overwrite] an Overwrite object to apply to this channel
463
+ # @param reason [String] The reason the for defining the overwrite.
464
+ # @overload define_overwrite(thing, allow, deny)
465
+ # @param thing [User, Role] What to define an overwrite for.
466
+ # @param allow [#bits, Permissions, Integer] The permission sets that should receive an `allow` override (i.e. a
467
+ # green checkmark on Discord)
468
+ # @param deny [#bits, Permissions, Integer] The permission sets that should receive a `deny` override (i.e. a red
469
+ # cross on Discord)
470
+ # @param reason [String] The reason the for defining the overwrite.
471
+ # @example Define a permission overwrite for a user that can then mention everyone and use TTS, but not create any invites
472
+ # allow = Discordrb::Permissions.new
473
+ # allow.can_mention_everyone = true
474
+ # allow.can_send_tts_messages = true
475
+ #
476
+ # deny = Discordrb::Permissions.new
477
+ # deny.can_create_instant_invite = true
478
+ #
479
+ # channel.define_overwrite(user, allow, deny)
480
+ def define_overwrite(thing, allow = 0, deny = 0, reason: nil)
481
+ unless thing.is_a? Overwrite
482
+ allow_bits = allow.respond_to?(:bits) ? allow.bits : allow
483
+ deny_bits = deny.respond_to?(:bits) ? deny.bits : deny
484
+
485
+ thing = Overwrite.new thing, allow: allow_bits, deny: deny_bits
486
+ end
487
+
488
+ API::Channel.update_permission(@bot.token, @id, thing.id, thing.allow.bits, thing.deny.bits, thing.type, reason)
489
+ end
490
+
491
+ # Deletes a permission overwrite for this channel
492
+ # @param target [Member, User, Role, Profile, Recipient, String, Integer] What permission overwrite to delete
493
+ # @param reason [String] The reason the for the overwrite deletion.
494
+ def delete_overwrite(target, reason = nil)
495
+ raise 'Tried deleting a overwrite for an invalid target' unless target.is_a?(Member) || target.is_a?(User) || target.is_a?(Role) || target.is_a?(Profile) || target.is_a?(Recipient) || target.respond_to?(:resolve_id)
496
+
497
+ API::Channel.delete_permission(@bot.token, @id, target.resolve_id, reason)
498
+ end
499
+
500
+ # Updates the cached data from another channel.
501
+ # @note For internal use only
502
+ # @!visibility private
503
+ def update_from(other)
504
+ @name = other.name
505
+ @position = other.position
506
+ @topic = other.topic
507
+ @recipients = other.recipients
508
+ @bitrate = other.bitrate
509
+ @user_limit = other.user_limit
510
+ @permission_overwrites = other.permission_overwrites
511
+ @nsfw = other.nsfw
512
+ @parent_id = other.parent_id
513
+ @rate_limit_per_user = other.rate_limit_per_user
514
+ end
515
+
516
+ # The list of users currently in this channel. For a voice channel, it will return all the members currently
517
+ # in that channel. For a text channel, it will return all online members that have permission to read it.
518
+ # @return [Array<Member>] the users in this channel
519
+ def users
520
+ if text?
521
+ @server.online_members(include_idle: true).select { |u| u.can_read_messages? self }
522
+ 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
524
+ end
525
+ end
526
+
527
+ # Retrieves some of this channel's message history.
528
+ # @param amount [Integer] How many messages to retrieve. This must be less than or equal to 100, if it is higher
529
+ # than 100 it will be treated as 100 on Discord's side.
530
+ # @param before_id [Integer] The ID of the most recent message the retrieval should start at, or nil if it should
531
+ # start at the current message.
532
+ # @param after_id [Integer] The ID of the oldest message the retrieval should start at, or nil if it should start
533
+ # as soon as possible with the specified amount.
534
+ # @param around_id [Integer] The ID of the message retrieval should start from, reading in both directions
535
+ # @example Count the number of messages in the last 50 messages that contain the letter 'e'.
536
+ # message_count = channel.history(50).count {|message| message.content.include? "e"}
537
+ # @example Get the last 10 messages before the provided message.
538
+ # last_ten_messages = channel.history(10, message.id)
539
+ # @return [Array<Message>] the retrieved messages.
540
+ def history(amount, before_id = nil, after_id = nil, around_id = nil)
541
+ logs = API::Channel.messages(@bot.token, @id, amount, before_id, after_id, around_id)
542
+ JSON.parse(logs).map { |message| Message.new(message, @bot) }
543
+ end
544
+
545
+ # Retrieves message history, but only message IDs for use with prune.
546
+ # @note For internal use only
547
+ # @!visibility private
548
+ def history_ids(amount, before_id = nil, after_id = nil, around_id = nil)
549
+ logs = API::Channel.messages(@bot.token, @id, amount, before_id, after_id, around_id)
550
+ JSON.parse(logs).map { |message| message['id'].to_i }
551
+ end
552
+
553
+ # Returns a single message from this channel's history by ID.
554
+ # @param message_id [Integer] The ID of the message to retrieve.
555
+ # @return [Message, nil] the retrieved message, or `nil` if it couldn't be found.
556
+ def load_message(message_id)
557
+ response = API::Channel.message(@bot.token, @id, message_id)
558
+ Message.new(JSON.parse(response), @bot)
559
+ rescue RestClient::ResourceNotFound
560
+ nil
561
+ end
562
+
563
+ alias_method :message, :load_message
564
+
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) }
570
+ end
571
+
572
+ # Delete the last N messages on this channel.
573
+ # @param amount [Integer] The amount of message history to consider for pruning. Must be a value between 2 and 100 (Discord limitation)
574
+ # @param strict [true, false] Whether an error should be raised when a message is reached that is too old to be bulk
575
+ # deleted. If this is false only a warning message will be output to the console.
576
+ # @param reason [String, nil] The reason for pruning
577
+ # @raise [ArgumentError] if the amount of messages is not a value between 2 and 100
578
+ # @yield [message] Yields each message in this channels history for filtering the messages to delete
579
+ # @example Pruning messages from a specific user ID
580
+ # channel.prune(100) { |m| m.author.id == 83283213010599936 }
581
+ # @return [Integer] The amount of messages that were successfully deleted
582
+ def prune(amount, strict = false, reason = nil, &block)
583
+ raise ArgumentError, 'Can only delete between 1 and 100 messages!' unless amount.between?(1, 100)
584
+
585
+ messages =
586
+ if block
587
+ history(amount).select(&block).map(&:id)
588
+ else
589
+ history_ids(amount)
590
+ end
591
+
592
+ case messages.size
593
+ when 0
594
+ 0
595
+ when 1
596
+ API::Channel.delete_message(@bot.token, @id, messages.first, reason)
597
+ 1
598
+ else
599
+ bulk_delete(messages, strict, reason)
600
+ end
601
+ end
602
+
603
+ # Deletes a collection of messages
604
+ # @param messages [Array<Message, String, Integer>] the messages (or message IDs) to delete. Total must be an amount between 2 and 100 (Discord limitation)
605
+ # @param strict [true, false] Whether an error should be raised when a message is reached that is too old to be bulk
606
+ # deleted. If this is false only a warning message will be output to the console.
607
+ # @param reason [String, nil] The reason for deleting the messages
608
+ # @raise [ArgumentError] if the amount of messages is not a value between 2 and 100
609
+ # @return [Integer] The amount of messages that were successfully deleted
610
+ def delete_messages(messages, strict = false, reason = nil)
611
+ raise ArgumentError, 'Can only delete between 2 and 100 messages!' unless messages.count.between?(2, 100)
612
+
613
+ messages.map!(&:resolve_id)
614
+ bulk_delete(messages, strict, reason)
615
+ end
616
+
617
+ # Updates the cached permission overwrites
618
+ # @note For internal use only
619
+ # @!visibility private
620
+ def update_overwrites(overwrites)
621
+ @permission_overwrites = overwrites
622
+ end
623
+
624
+ # Add an {Await} for a message in this channel. This is identical in functionality to adding a
625
+ # {Discordrb::Events::MessageEvent} await with the `in` attribute as this channel.
626
+ # @see Bot#add_await
627
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#await!} instead.
628
+ def await(key, attributes = {}, &block)
629
+ @bot.add_await(key, Discordrb::Events::MessageEvent, { in: @id }.merge(attributes), &block)
630
+ end
631
+
632
+ # Add a blocking {Await} for a message in this channel. This is identical in functionality to adding a
633
+ # {Discordrb::Events::MessageEvent} await with the `in` attribute as this channel.
634
+ # @see Bot#add_await!
635
+ def await!(attributes = {}, &block)
636
+ @bot.add_await!(Discordrb::Events::MessageEvent, { in: @id }.merge(attributes), &block)
637
+ end
638
+
639
+ # Creates a new invite to this channel.
640
+ # @param max_age [Integer] How many seconds this invite should last.
641
+ # @param max_uses [Integer] How many times this invite should be able to be used.
642
+ # @param temporary [true, false] Whether membership should be temporary (kicked after going offline).
643
+ # @param unique [true, false] If true, Discord will always send a unique invite instead of possibly re-using a similar one
644
+ # @param reason [String] The reason the for the creation of this invite.
645
+ # @return [Invite] the created invite.
646
+ def make_invite(max_age = 0, max_uses = 0, temporary = false, unique = false, reason = nil)
647
+ response = API::Channel.create_invite(@bot.token, @id, max_age, max_uses, temporary, unique, reason)
648
+ Invite.new(JSON.parse(response), @bot)
649
+ end
650
+
651
+ alias_method :invite, :make_invite
652
+
653
+ # Starts typing, which displays the typing indicator on the client for five seconds.
654
+ # If you want to keep typing you'll have to resend this every five seconds. (An abstraction
655
+ # for this will eventually be coming)
656
+ # @example Send a typing indicator for the bot in a given channel.
657
+ # channel.start_typing()
658
+ def start_typing
659
+ API::Channel.start_typing(@bot.token, @id)
660
+ end
661
+
662
+ # Creates a Group channel
663
+ # @param user_ids [Array<Integer>] Array of user IDs to add to the new group channel (Excluding
664
+ # the recipient of the PM channel).
665
+ # @return [Channel] the created channel.
666
+ def create_group(user_ids)
667
+ raise 'Attempted to create group channel on a non-pm channel!' unless pm?
668
+
669
+ response = API::Channel.create_group(@bot.token, @id, user_ids.shift)
670
+ channel = Channel.new(JSON.parse(response), @bot)
671
+ channel.add_group_users(user_ids)
672
+ end
673
+
674
+ # Adds a user to a group channel.
675
+ # @param user_ids [Array<String, Integer>, String, Integer] User ID or array of user IDs to add to the group channel.
676
+ # @return [Channel] the group channel.
677
+ def add_group_users(user_ids)
678
+ raise 'Attempted to add a user to a non-group channel!' unless group?
679
+
680
+ user_ids = [user_ids] unless user_ids.is_a? Array
681
+ user_ids.each do |user_id|
682
+ API::Channel.add_group_user(@bot.token, @id, user_id.resolve_id)
683
+ end
684
+ self
685
+ end
686
+
687
+ alias_method :add_group_user, :add_group_users
688
+
689
+ # Removes a user from a group channel.
690
+ # @param user_ids [Array<String, Integer>, String, Integer] User ID or array of user IDs to remove from the group channel.
691
+ # @return [Channel] the group channel.
692
+ def remove_group_users(user_ids)
693
+ raise 'Attempted to remove a user from a non-group channel!' unless group?
694
+
695
+ user_ids = [user_ids] unless user_ids.is_a? Array
696
+ user_ids.each do |user_id|
697
+ API::Channel.remove_group_user(@bot.token, @id, user_id.resolve_id)
698
+ end
699
+ self
700
+ end
701
+
702
+ alias_method :remove_group_user, :remove_group_users
703
+
704
+ # Leaves the group.
705
+ def leave_group
706
+ raise 'Attempted to leave a non-group channel!' unless group?
707
+
708
+ API::Channel.leave_group(@bot.token, @id)
709
+ end
710
+
711
+ alias_method :leave, :leave_group
712
+
713
+ # Creates a webhook in this channel
714
+ # @param name [String] the default name of this webhook.
715
+ # @param avatar [String] the default avatar URL to give this webhook.
716
+ # @param reason [String] the reason for the webhook creation.
717
+ # @raise [ArgumentError] if the channel isn't a text channel in a server.
718
+ # @return [Webhook] the created webhook.
719
+ def create_webhook(name, avatar = nil, reason = nil)
720
+ raise ArgumentError, 'Tried to create a webhook in a non-server channel' unless server
721
+ raise ArgumentError, 'Tried to create a webhook in a non-text channel' unless text?
722
+
723
+ response = API::Channel.create_webhook(@bot.token, @id, name, avatar, reason)
724
+ Webhook.new(JSON.parse(response), @bot)
725
+ end
726
+
727
+ # Requests a list of Webhooks on the channel.
728
+ # @return [Array<Webhook>] webhooks on the channel.
729
+ def webhooks
730
+ raise 'Tried to request webhooks from a non-server channel' unless server
731
+
732
+ webhooks = JSON.parse(API::Channel.webhooks(@bot.token, @id))
733
+ webhooks.map { |webhook_data| Webhook.new(webhook_data, @bot) }
734
+ end
735
+
736
+ # Requests a list of Invites to the channel.
737
+ # @return [Array<Invite>] invites to the channel.
738
+ def invites
739
+ raise 'Tried to request invites from a non-server channel' unless server
740
+
741
+ invites = JSON.parse(API::Channel.invites(@bot.token, @id))
742
+ invites.map { |invite_data| Invite.new(invite_data, @bot) }
743
+ end
744
+
745
+ # The default `inspect` method is overwritten to give more useful output.
746
+ def inspect
747
+ "<Channel name=#{@name} id=#{@id} topic=\"#{@topic}\" type=#{@type} position=#{@position} server=#{@server}>"
748
+ end
749
+
750
+ # Adds a recipient to a group channel.
751
+ # @param recipient [Recipient] the recipient to add to the group
752
+ # @raise [ArgumentError] if tried to add a non-recipient
753
+ # @note For internal use only
754
+ # @!visibility private
755
+ def add_recipient(recipient)
756
+ raise 'Tried to add recipient to a non-group channel' unless group?
757
+ raise ArgumentError, 'Tried to add a non-recipient to a group' unless recipient.is_a?(Recipient)
758
+
759
+ @recipients << recipient
760
+ end
761
+
762
+ # Removes a recipient from a group channel.
763
+ # @param recipient [Recipient] the recipient to remove from the group
764
+ # @raise [ArgumentError] if tried to remove a non-recipient
765
+ # @note For internal use only
766
+ # @!visibility private
767
+ def remove_recipient(recipient)
768
+ raise 'Tried to remove recipient from a non-group channel' unless group?
769
+ raise ArgumentError, 'Tried to remove a non-recipient from a group' unless recipient.is_a?(Recipient)
770
+
771
+ @recipients.delete(recipient)
772
+ end
773
+
774
+ # Updates the cached data with new data
775
+ # @note For internal use only
776
+ # @!visibility private
777
+ def update_data(new_data = nil)
778
+ new_data ||= JSON.parse(API::Channel.resolve(@bot.token, @id))
779
+ @name = new_data[:name] || new_data['name'] || @name
780
+ @topic = new_data[:topic] || new_data['topic'] || @topic
781
+ @position = new_data[:position] || new_data['position'] || @position
782
+ @bitrate = new_data[:bitrate] || new_data['bitrate'] || @bitrate
783
+ @user_limit = new_data[:user_limit] || new_data['user_limit'] || @user_limit
784
+ new_nsfw = new_data.key?(:nsfw) ? new_data[:nsfw] : new_data['nsfw']
785
+ @nsfw = new_nsfw.nil? ? @nsfw : new_nsfw
786
+ @parent_id = new_data[:parent_id] || new_data['parent_id'] || @parent_id
787
+ process_permission_overwrites(new_data[:permission_overwrites] || new_data['permission_overwrites'])
788
+ @rate_limit_per_user = new_data[:rate_limit_per_user] || new_data['rate_limit_per_user'] || @rate_limit_per_user
789
+ end
790
+
791
+ # @return [String] a URL that a user can use to navigate to this channel in the client
792
+ def link
793
+ "https://discord.com/channels/#{@server&.id || '@me'}/#{@channel.id}"
794
+ end
795
+
796
+ alias_method :jump_link, :link
797
+
798
+ private
799
+
800
+ # For bulk_delete checking
801
+ TWO_WEEKS = 86_400 * 14
802
+
803
+ # Deletes a list of messages on this channel using bulk delete.
804
+ def bulk_delete(ids, strict = false, reason = nil)
805
+ min_snowflake = IDObject.synthesise(Time.now - TWO_WEEKS)
806
+
807
+ ids.reject! do |e|
808
+ next unless e < min_snowflake
809
+
810
+ message = "Attempted to bulk_delete message #{e} which is too old (min = #{min_snowflake})"
811
+ raise ArgumentError, message if strict
812
+
813
+ Discordrb::LOGGER.warn(message)
814
+ true
815
+ end
816
+
817
+ API::Channel.bulk_delete_messages(@bot.token, @id, ids, reason)
818
+ ids.size
819
+ end
820
+
821
+ def update_channel_data(new_data)
822
+ new_nsfw = new_data[:nsfw].is_a?(TrueClass) || new_data[:nsfw].is_a?(FalseClass) ? new_data[:nsfw] : @nsfw
823
+ # send permission_overwrite only when explicitly set
824
+ overwrites = new_data[:permission_overwrites] ? new_data[:permission_overwrites].map { |_, v| v.to_hash } : nil
825
+ response = JSON.parse(API::Channel.update(@bot.token, @id,
826
+ new_data[:name] || @name,
827
+ new_data[:topic] || @topic,
828
+ new_data[:position] || @position,
829
+ new_data[:bitrate] || @bitrate,
830
+ new_data[:user_limit] || @user_limit,
831
+ new_nsfw,
832
+ overwrites,
833
+ new_data[:parent_id] || @parent_id,
834
+ new_data[:rate_limit_per_user] || @rate_limit_per_user))
835
+ update_data(response)
836
+ end
837
+
838
+ def process_permission_overwrites(overwrites)
839
+ # Populate permission overwrites
840
+ @permission_overwrites = {}
841
+ return unless overwrites
842
+
843
+ overwrites.each do |element|
844
+ id = element['id'].to_i
845
+ @permission_overwrites[id] = Overwrite.from_hash(element)
846
+ end
847
+ end
848
+ end
849
+ end