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
data/lib/discordrb/bot.rb CHANGED
@@ -18,6 +18,7 @@ require 'discordrb/events/bans'
18
18
  require 'discordrb/events/raw'
19
19
  require 'discordrb/events/reactions'
20
20
  require 'discordrb/events/webhooks'
21
+ require 'discordrb/events/invites'
21
22
 
22
23
  require 'discordrb/api'
23
24
  require 'discordrb/api/channel'
@@ -101,12 +102,14 @@ module Discordrb
101
102
  # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
102
103
  # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
103
104
  # that all data be received in a continuous compressed stream.
105
+ # @param intents [:all, Array<Symbol>, nil] Intents that this bot requires. See {Discordrb::INTENTS}. If `nil`, no intents
106
+ # field will be passed.
104
107
  def initialize(
105
- log_mode: :normal,
106
- token: nil, client_id: nil,
107
- type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
108
- shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
109
- compress_mode: :stream
108
+ log_mode: :normal,
109
+ token: nil, client_id: nil,
110
+ type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
111
+ shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
112
+ compress_mode: :large, intents: nil
110
113
  )
111
114
  LOGGER.mode = log_mode
112
115
  LOGGER.token = token if redact_token
@@ -125,8 +128,12 @@ module Discordrb
125
128
 
126
129
  @compress_mode = compress_mode
127
130
 
131
+ raise 'Token string is empty or nil' if token.nil? || token.empty?
132
+
133
+ @intents = intents == :all ? INTENTS.values.reduce(&:|) : calculate_intents(intents) if intents
134
+
128
135
  @token = process_token(@type, token)
129
- @gateway = Gateway.new(self, @token, @shard_key, @compress_mode)
136
+ @gateway = Gateway.new(self, @token, @shard_key, @compress_mode, @intents)
130
137
 
131
138
  init_cache
132
139
 
@@ -160,16 +167,13 @@ module Discordrb
160
167
 
161
168
  # @overload emoji(id)
162
169
  # Return an emoji by its ID
163
- # @param id [Integer, #resolve_id] The emoji's ID.
170
+ # @param id [String, Integer] The emoji's ID.
164
171
  # @return [Emoji, nil] the emoji object. `nil` if the emoji was not found.
165
172
  # @overload emoji
166
173
  # The list of emoji the bot can use.
167
174
  # @return [Array<Emoji>] the emoji available.
168
175
  def emoji(id = nil)
169
- gateway_check
170
- unavailable_servers_check
171
-
172
- emoji_hash = @servers.values.map(&:emoji).reduce(&:merge)
176
+ emoji_hash = servers.values.map(&:emoji).reduce(&:merge)
173
177
  if id
174
178
  id = id.resolve_id
175
179
  emoji_hash[id]
@@ -203,6 +207,7 @@ module Discordrb
203
207
  # @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
204
208
  def bot_application
205
209
  return unless @type == :bot
210
+
206
211
  response = API.oauth_application(token)
207
212
  Application.new(JSON.parse(response), self)
208
213
  end
@@ -225,7 +230,7 @@ module Discordrb
225
230
 
226
231
  # Runs the bot, which logs into Discord and connects the WebSocket. This
227
232
  # prevents all further execution unless it is executed with
228
- # `backround` = `true`.
233
+ # `background` = `true`.
229
234
  # @param background [true, false] If it is `true`, then the bot will run in
230
235
  # another thread to allow further execution. If it is `false`, this method
231
236
  # will block until {#stop} is called. If the bot is run with `true`, make
@@ -254,9 +259,9 @@ module Discordrb
254
259
 
255
260
  # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
256
261
  # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
257
- # @param no_sync [true, false] Whether or not to disable use of synchronize in the close method. This should be true if called from a trap context.
258
- def stop(no_sync = false)
259
- @gateway.stop(no_sync)
262
+ # @note This method no longer takes an argument as of 3.4.0
263
+ def stop(_no_sync = nil)
264
+ @gateway.stop
260
265
  end
261
266
 
262
267
  # @return [true, false] whether or not the bot is currently connected to Discord.
@@ -273,14 +278,14 @@ module Discordrb
273
278
 
274
279
  # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
275
280
  # @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
276
- # @param permission_bits [Integer, String] Permission bits that should be appended to invite url.
281
+ # @param permission_bits [String, Integer] Permission bits that should be appended to invite url.
277
282
  # @return [String] the OAuth invite URL.
278
283
  def invite_url(server: nil, permission_bits: nil)
279
284
  @client_id ||= bot_application.id
280
285
 
281
286
  server_id_str = server ? "&guild_id=#{server.id}" : ''
282
287
  permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
283
- "https://discordapp.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
288
+ "https://discord.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
284
289
  end
285
290
 
286
291
  # @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
@@ -299,22 +304,21 @@ module Discordrb
299
304
 
300
305
  server_id = channel.server.id
301
306
  return @voices[server_id] if @voices[server_id]
302
-
303
- nil
304
307
  end
305
308
 
306
309
  # Connects to a voice channel, initializes network connections and returns the {Voice::VoiceBot} over which audio
307
310
  # data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
308
311
  # connected to voice, the existing connection will be terminated - you don't have to call
309
312
  # {Discordrb::Voice::VoiceBot#destroy} before calling this method.
310
- # @param chan [Channel, Integer, #resolve_id] The voice channel to connect to.
311
- # @param encrypted [true, false] Whether voice communication should be encrypted using RbNaCl's SecretBox
313
+ # @param chan [Channel, String, Integer] The voice channel, or its ID, to connect to.
314
+ # @param encrypted [true, false] Whether voice communication should be encrypted using
312
315
  # (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
313
316
  # @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
314
317
  def voice_connect(chan, encrypted = true)
318
+ raise ArgumentError, 'Unencrypted voice connections are no longer supported.' unless encrypted
319
+
315
320
  chan = channel(chan.resolve_id)
316
321
  server_id = chan.server.id
317
- @should_encrypt_voice = encrypted
318
322
 
319
323
  if @voices[chan.id]
320
324
  debug('Voice bot exists already! Destroying it')
@@ -336,7 +340,7 @@ module Discordrb
336
340
 
337
341
  # Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
338
342
  # {Discordrb::Voice::VoiceBot#destroy} rather than this.
339
- # @param server [Server, Integer, #resolve_id] The server the voice connection is on.
343
+ # @param server [Server, String, Integer] The server, or server ID, the voice connection is on.
340
344
  # @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
341
345
  # directly, you should leave it as true.
342
346
  def voice_destroy(server, destroy_vws = true)
@@ -355,31 +359,38 @@ module Discordrb
355
359
  end
356
360
 
357
361
  # Sends a text message to a channel given its ID and the message's content.
358
- # @param channel [Channel, Integer, #resolve_id] The channel to send something to.
362
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
359
363
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
360
364
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
361
365
  # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
366
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
367
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
362
368
  # @return [Message] The message that was sent.
363
- def send_message(channel, content, tts = false, embed = nil)
369
+ def send_message(channel, content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
364
370
  channel = channel.resolve_id
365
371
  debug("Sending message to #{channel} with content '#{content}'")
372
+ allowed_mentions = { parse: [] } if allowed_mentions == false
373
+ message_reference = { message_id: message_reference.id } if message_reference
366
374
 
367
- response = API::Channel.create_message(token, channel, content, tts, embed ? embed.to_hash : nil)
375
+ response = API::Channel.create_message(token, channel, content, tts, embed&.to_hash, nil, attachments, allowed_mentions&.to_hash, message_reference)
368
376
  Message.new(JSON.parse(response), self)
369
377
  end
370
378
 
371
379
  # Sends a text message to a channel given its ID and the message's content,
372
380
  # then deletes it after the specified timeout in seconds.
373
- # @param channel [Channel, Integer, #resolve_id] The channel to send something to.
381
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
374
382
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
375
383
  # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
376
384
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
377
385
  # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
378
- def send_temporary_message(channel, content, timeout, tts = false, embed = nil)
386
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
387
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
388
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
389
+ def send_temporary_message(channel, content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
379
390
  Thread.new do
380
391
  Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
381
392
 
382
- message = send_message(channel, content, tts, embed)
393
+ message = send_message(channel, content, tts, embed, attachments, allowed_mentions, message_reference)
383
394
  sleep(timeout)
384
395
  message.delete
385
396
  end
@@ -389,13 +400,24 @@ module Discordrb
389
400
 
390
401
  # Sends a file to a channel. If it is an image, it will automatically be embedded.
391
402
  # @note This executes in a blocking way, so if you're sending long files, be wary of delays.
392
- # @param channel [Channel, Integer, #resolve_id] The channel to send something to.
403
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
393
404
  # @param file [File] The file that should be sent.
394
405
  # @param caption [string] The caption for the file.
395
406
  # @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
407
+ # @param filename [String] Overrides the filename of the uploaded file
408
+ # @param spoiler [true, false] Whether or not this file should appear as a spoiler.
396
409
  # @example Send a file from disk
397
410
  # bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))
398
- def send_file(channel, file, caption: nil, tts: false)
411
+ def send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil)
412
+ if file.respond_to?(:read)
413
+ if spoiler
414
+ filename ||= File.basename(file.path)
415
+ filename = "SPOILER_#{filename}" unless filename.start_with? 'SPOILER_'
416
+ end
417
+ # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
418
+ file.define_singleton_method(:original_filename) { filename } if filename
419
+ end
420
+
399
421
  channel = channel.resolve_id
400
422
  response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
401
423
  Message.new(JSON.parse(response), self)
@@ -410,14 +432,13 @@ module Discordrb
410
432
  def create_server(name, region = :'eu-central')
411
433
  response = API::Server.create(token, name, region)
412
434
  id = JSON.parse(response)['id'].to_i
413
- sleep 0.1 until @servers[id]
414
- server = @servers[id]
435
+ sleep 0.1 until (server = @servers[id])
415
436
  debug "Successfully created server #{server.id} with name #{server.name}"
416
437
  server
417
438
  end
418
439
 
419
440
  # Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using
420
- # Discord. For information how to use this, see the docs: https://discordapp.com/developers/docs/topics/oauth2
441
+ # Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
421
442
  # @param name [String] What your application should be called.
422
443
  # @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
423
444
  # @return [Array(String, String)] your applications' client ID and client secret to be used in OAuth authorization.
@@ -436,28 +457,46 @@ module Discordrb
436
457
  API.update_oauth_application(@token, name, redirect_uris, description, icon)
437
458
  end
438
459
 
439
- # Gets the user, channel, role or emoji from a mention of the user, channel, role or emoji.
460
+ # Gets the users, channels, roles and emoji from a string.
461
+ # @param mentions [String] The mentions, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
462
+ # @param server [Server, nil] The server of the associated mentions. (recommended for role parsing, to speed things up)
463
+ # @return [Array<User, Channel, Role, Emoji>] The array of users, channels, roles and emoji identified by the mentions, or `nil` if none exists.
464
+ def parse_mentions(mentions, server = nil)
465
+ array_to_return = []
466
+ # While possible mentions may be in message
467
+ while mentions.include?('<') && mentions.include?('>')
468
+ # Removing all content before the next possible mention
469
+ mentions = mentions.split('<', 2)[1]
470
+ # Locate the first valid mention enclosed in `<...>`, otherwise advance to the next open `<`
471
+ next unless mentions.split('>', 2).first.length < mentions.split('<', 2).first.length
472
+
473
+ # Store the possible mention value to be validated with RegEx
474
+ mention = mentions.split('>', 2).first
475
+ if /@!?(?<id>\d+)/ =~ mention
476
+ array_to_return << user(id) unless user(id).nil?
477
+ elsif /#(?<id>\d+)/ =~ mention
478
+ array_to_return << channel(id, server) unless channel(id, server).nil?
479
+ elsif /@&(?<id>\d+)/ =~ mention
480
+ if server
481
+ array_to_return << server.role(id) unless server.role(id).nil?
482
+ else
483
+ @servers.each_value do |element|
484
+ array_to_return << element.role(id) unless element.role(id).nil?
485
+ end
486
+ end
487
+ elsif /(?<animated>^a|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
488
+ array_to_return << (emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil))
489
+ end
490
+ end
491
+ array_to_return
492
+ end
493
+
494
+ # Gets the user, channel, role or emoji from a string.
440
495
  # @param mention [String] The mention, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
441
496
  # @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
442
497
  # @return [User, Channel, Role, Emoji] The user, channel, role or emoji identified by the mention, or `nil` if none exists.
443
498
  def parse_mention(mention, server = nil)
444
- # Mention format: <@id>
445
- if /<@!?(?<id>\d+)>/ =~ mention
446
- user(id)
447
- elsif /<#(?<id>\d+)>/ =~ mention
448
- channel(id, server)
449
- elsif /<@&(?<id>\d+)>/ =~ mention
450
- return server.role(id) if server
451
- @servers.values.each do |element|
452
- role = element.role(id)
453
- return role unless role.nil?
454
- end
455
-
456
- # Return nil if no role is found
457
- nil
458
- elsif /<(?<animated>a)?:(?<name>\w+):(?<id>\d+)>/ =~ mention
459
- emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil)
460
- end
499
+ parse_mentions(mention, server).first
461
500
  end
462
501
 
463
502
  # Updates presence status.
@@ -466,7 +505,8 @@ module Discordrb
466
505
  # @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
467
506
  # @param since [Integer] When this status was set.
468
507
  # @param afk [true, false] Whether the bot is AFK.
469
- # @param activity_type [Integer] The type of activity status to display. Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching)
508
+ # @param activity_type [Integer] The type of activity status to display.
509
+ # Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching), or 5 (Competing).
470
510
  # @see Gateway#send_status_update
471
511
  def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
472
512
  gateway_check
@@ -480,7 +520,7 @@ module Discordrb
480
520
  @gateway.send_status_update(status, since, activity_obj, afk)
481
521
 
482
522
  # Update the status in the cache
483
- profile.update_presence('status' => status.to_s, 'game' => activity_obj)
523
+ profile.update_presence('status' => status.to_s, 'activities' => [activity_obj].compact)
484
524
  end
485
525
 
486
526
  # Sets the currently playing game to the specified game.
@@ -489,7 +529,6 @@ module Discordrb
489
529
  def game=(name)
490
530
  gateway_check
491
531
  update_status(@status, name, nil)
492
- name
493
532
  end
494
533
 
495
534
  alias_method :playing=, :game=
@@ -500,7 +539,6 @@ module Discordrb
500
539
  def listening=(name)
501
540
  gateway_check
502
541
  update_status(@status, name, nil, nil, nil, 2)
503
- name
504
542
  end
505
543
 
506
544
  # Sets the current watching status to the specified name.
@@ -509,7 +547,6 @@ module Discordrb
509
547
  def watching=(name)
510
548
  gateway_check
511
549
  update_status(@status, name, nil, nil, nil, 3)
512
- name
513
550
  end
514
551
 
515
552
  # Sets the currently online stream to the specified name and Twitch URL.
@@ -522,6 +559,14 @@ module Discordrb
522
559
  name
523
560
  end
524
561
 
562
+ # Sets the currently competing status to the specified name.
563
+ # @param name [String] The name of the game to be competing in.
564
+ # @return [String] The game that is being competed in now.
565
+ def competing=(name)
566
+ gateway_check
567
+ update_status(@status, name, nil, nil, nil, 5)
568
+ end
569
+
525
570
  # Sets status to online.
526
571
  def online
527
572
  gateway_check
@@ -576,6 +621,7 @@ module Discordrb
576
621
  # @deprecated Will be changed to blocking behavior in v4.0. Use {#add_await!} instead.
577
622
  def add_await(key, type, attributes = {}, &block)
578
623
  raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
624
+
579
625
  await = Await.new(self, key, type, attributes, block)
580
626
  @awaits ||= {}
581
627
  @awaits[key] = await
@@ -583,14 +629,17 @@ module Discordrb
583
629
 
584
630
  # Awaits an event, blocking the current thread until a response is received.
585
631
  # @param type [Class] The event class that should be listened for.
586
- # @option attributes [Numeric] :timeout the amount of time to wait for a response before returning `nil`. Waits forever if omitted.
632
+ # @option attributes [Numeric] :timeout the amount of time (in seconds) to wait for a response before returning `nil`. Waits forever if omitted.
633
+ # @yield Executed when a matching event is received.
634
+ # @yieldparam event [Event] The event object that was triggered.
635
+ # @yieldreturn [true, false] Whether the event matches extra await criteria described by the block
587
636
  # @return [Event, nil] The event object that was triggered, or `nil` if a `timeout` was set and no event was raised in time.
588
637
  # @raise [ArgumentError] if `timeout` is given and is not a positive numeric value
589
638
  def add_await!(type, attributes = {})
590
639
  raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
591
640
 
592
641
  timeout = attributes[:timeout]
593
- raise ArgumentError, 'Timeout must be a number > 0' if timeout && timeout.is_a?(Numeric) && timeout <= 0
642
+ raise ArgumentError, 'Timeout must be a number > 0' if timeout.is_a?(Numeric) && !timeout.positive?
594
643
 
595
644
  mutex = Mutex.new
596
645
  cv = ConditionVariable.new
@@ -598,7 +647,12 @@ module Discordrb
598
647
  block = lambda do |event|
599
648
  mutex.synchronize do
600
649
  response = event
601
- cv.signal
650
+ if block_given?
651
+ result = yield(event)
652
+ cv.signal if result.is_a?(TrueClass)
653
+ else
654
+ cv.signal
655
+ end
602
656
  end
603
657
  end
604
658
 
@@ -615,25 +669,26 @@ module Discordrb
615
669
 
616
670
  remove_handler(handler)
617
671
  raise 'ConditionVariable was signaled without returning an event!' if response.nil? && timeout.nil?
672
+
618
673
  response
619
674
  end
620
675
 
621
676
  # Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.
622
677
  # @note Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and
623
678
  # presence and any other events will still be received.
624
- # @param user [User, Integer, #resolve_id] The user, or its ID, to be ignored.
679
+ # @param user [User, String, Integer] The user, or its ID, to be ignored.
625
680
  def ignore_user(user)
626
681
  @ignored_ids << user.resolve_id
627
682
  end
628
683
 
629
684
  # Remove a user from the ignore list.
630
- # @param user [User, Integer, #resolve_id] The user, or its ID, to be unignored.
685
+ # @param user [User, String, Integer] The user, or its ID, to be unignored.
631
686
  def unignore_user(user)
632
687
  @ignored_ids.delete(user.resolve_id)
633
688
  end
634
689
 
635
690
  # Checks whether a user is being ignored.
636
- # @param user [User, Integer, #resolve_id] The user, or its ID, to check.
691
+ # @param user [User, String, Integer] The user, or its ID, to check.
637
692
  # @return [true, false] whether or not the user is ignored.
638
693
  def ignored?(user)
639
694
  @ignored_ids.include?(user.resolve_id)
@@ -677,7 +732,8 @@ module Discordrb
677
732
  # e.g. due to a Discord outage or because the servers are large and taking a while to load.
678
733
  def unavailable_servers_check
679
734
  # Return unless there are servers that are unavailable.
680
- return unless @unavailable_servers && @unavailable_servers > 0
735
+ return unless @unavailable_servers&.positive?
736
+
681
737
  LOGGER.warn("#{@unavailable_servers} servers haven't been cached yet.")
682
738
  LOGGER.warn('Servers may be unavailable due to an outage, or your bot is on very large servers that are taking a while to load.')
683
739
  end
@@ -737,10 +793,21 @@ module Discordrb
737
793
 
738
794
  user_id = data['user_id'].to_i
739
795
  old_voice_state = server.voice_states[user_id]
740
- old_channel_id = old_voice_state.voice_channel.id if old_voice_state
796
+ old_channel_id = old_voice_state.voice_channel&.id if old_voice_state
741
797
 
742
798
  server.update_voice_state(data)
743
799
 
800
+ existing_voice = @voices[server_id]
801
+ if user_id == @profile.id && existing_voice
802
+ new_channel_id = data['channel_id']
803
+ if new_channel_id
804
+ new_channel = channel(new_channel_id)
805
+ existing_voice.channel = new_channel
806
+ else
807
+ voice_destroy(server_id)
808
+ end
809
+ end
810
+
744
811
  old_channel_id
745
812
  end
746
813
 
@@ -751,6 +818,7 @@ module Discordrb
751
818
 
752
819
  debug("Voice server update received! chan: #{channel.inspect}")
753
820
  return unless channel
821
+
754
822
  @should_connect_to_voice.delete(server_id)
755
823
  debug('Updating voice server!')
756
824
 
@@ -763,7 +831,7 @@ module Discordrb
763
831
  end
764
832
 
765
833
  debug('Got data, now creating the bot.')
766
- @voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint, @should_encrypt_voice)
834
+ @voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint)
767
835
  end
768
836
 
769
837
  # Internal handler for CHANNEL_CREATE
@@ -787,6 +855,7 @@ module Discordrb
787
855
  channel = Channel.new(data, self)
788
856
  old_channel = @channels[channel.id]
789
857
  return unless old_channel
858
+
790
859
  old_channel.update_from(channel)
791
860
  end
792
861
 
@@ -843,12 +912,14 @@ module Discordrb
843
912
  member = server.member(data['user']['id'].to_i)
844
913
  member.update_roles(data['roles'])
845
914
  member.update_nick(data['nick'])
915
+ member.update_boosting_since(data['premium_since'])
846
916
  end
847
917
 
848
918
  # Internal handler for GUILD_MEMBER_DELETE
849
919
  def delete_guild_member(data)
850
920
  server_id = data['guild_id'].to_i
851
921
  server = self.server(server_id)
922
+ return unless server
852
923
 
853
924
  user_id = data['user']['id'].to_i
854
925
  server.delete_member(user_id)
@@ -951,13 +1022,13 @@ module Discordrb
951
1022
  # Remove the "Bot " prefix if it exists
952
1023
  token = token[4..-1] if token.start_with? 'Bot '
953
1024
 
954
- token = 'Bot ' + token unless type == :user
1025
+ token = "Bot #{token}" unless type == :user
955
1026
  token
956
1027
  end
957
1028
 
958
1029
  def handle_dispatch(type, data)
959
1030
  # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
960
- if @unavailable_servers && @unavailable_servers > 0 && (Time.now - @unavailable_timeout_time) > 10
1031
+ if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !((@intents || 0) & INTENTS[:servers]).zero?
961
1032
  # The server streaming timed out!
962
1033
  LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
963
1034
  LOGGER.debug('Calling ready now because server loading is taking a long time. Servers may be unavailable due to an outage, or your bot is on very large servers.')
@@ -1019,6 +1090,11 @@ module Discordrb
1019
1090
  id = data['guild_id'].to_i
1020
1091
  server = server(id)
1021
1092
  server.process_chunk(data['members'])
1093
+ when :INVITE_CREATE
1094
+ invite = Invite.new(data, self)
1095
+ raise_event(InviteCreateEvent.new(data, invite, self))
1096
+ when :INVITE_DELETE
1097
+ raise_event(InviteDeleteEvent.new(data, self))
1022
1098
  when :MESSAGE_CREATE
1023
1099
  if ignored?(data['author']['id'].to_i)
1024
1100
  debug("Ignored author with ID #{data['author']['id']}")
@@ -1053,6 +1129,10 @@ module Discordrb
1053
1129
  update_message(data)
1054
1130
 
1055
1131
  message = Message.new(data, self)
1132
+
1133
+ event = MessageUpdateEvent.new(message, self)
1134
+ raise_event(event)
1135
+
1056
1136
  return if message.from_bot? && !should_parse_self
1057
1137
 
1058
1138
  unless message.author
@@ -1120,10 +1200,10 @@ module Discordrb
1120
1200
  played_before = presence_user.nil? ? nil : presence_user.game
1121
1201
  update_presence(data)
1122
1202
 
1123
- event = if now_playing != played_before
1124
- PlayingEvent.new(data, self)
1125
- else
1203
+ event = if now_playing == played_before
1126
1204
  PresenceEvent.new(data, self)
1205
+ else
1206
+ PlayingEvent.new(data, self)
1127
1207
  end
1128
1208
 
1129
1209
  raise_event(event)
@@ -1135,7 +1215,8 @@ module Discordrb
1135
1215
  when :VOICE_SERVER_UPDATE
1136
1216
  update_voice_server(data)
1137
1217
 
1138
- # no event as this is irrelevant to users
1218
+ event = VoiceServerUpdateEvent.new(data, self)
1219
+ raise_event(event)
1139
1220
  when :CHANNEL_CREATE
1140
1221
  create_channel(data)
1141
1222
 
@@ -1300,6 +1381,7 @@ module Discordrb
1300
1381
  @event_handlers ||= {}
1301
1382
  handlers = @event_handlers[event.class]
1302
1383
  return unless handlers
1384
+
1303
1385
  handlers.dup.each do |handler|
1304
1386
  call_event(handler, event) if handler.matches?(event)
1305
1387
  end
@@ -1315,7 +1397,7 @@ module Discordrb
1315
1397
  begin
1316
1398
  handler.call(event)
1317
1399
  handler.after_call(event)
1318
- rescue => e
1400
+ rescue StandardError => e
1319
1401
  log_exception(e)
1320
1402
  ensure
1321
1403
  @event_threads.delete(t)
@@ -1328,6 +1410,7 @@ module Discordrb
1328
1410
  @awaits.each do |_, await|
1329
1411
  key, should_delete = await.match(event)
1330
1412
  next unless key
1413
+
1331
1414
  debug("should_delete: #{should_delete}")
1332
1415
  @awaits.delete(await.key) if should_delete
1333
1416
 
@@ -1335,5 +1418,24 @@ module Discordrb
1335
1418
  raise_event(await_event)
1336
1419
  end
1337
1420
  end
1421
+
1422
+ def calculate_intents(intents)
1423
+ intents.reduce(0) do |sum, intent|
1424
+ case intent
1425
+ when Symbol
1426
+ if INTENTS[intent]
1427
+ sum | INTENTS[intent]
1428
+ else
1429
+ LOGGER.warn("Unknown intent: #{intent}")
1430
+ sum
1431
+ end
1432
+ when Integer
1433
+ sum | intent
1434
+ else
1435
+ LOGGER.warn("Invalid intent: #{intent}")
1436
+ sum
1437
+ end
1438
+ end
1439
+ end
1338
1440
  end
1339
1441
  end
@@ -134,6 +134,7 @@ module Discordrb
134
134
  def pm_channel(id)
135
135
  id = id.resolve_id
136
136
  return @pm_channels[id] if @pm_channels[id]
137
+
137
138
  debug("Creating pm channel with user id #{id}")
138
139
  response = API::User.create_pm(token, id)
139
140
  channel = Channel.new(JSON.parse(response), self)
@@ -187,7 +188,7 @@ module Discordrb
187
188
  #
188
189
  # * An {Invite} object
189
190
  # * The code for an invite
190
- # * A fully qualified invite URL (e.g. `https://discordapp.com/invite/0A37aN7fasF7n83q`)
191
+ # * A fully qualified invite URL (e.g. `https://discord.com/invite/0A37aN7fasF7n83q`)
191
192
  # * A short invite URL with protocol (e.g. `https://discord.gg/0A37aN7fasF7n83q`)
192
193
  # * A short invite URL without protocol (e.g. `discord.gg/0A37aN7fasF7n83q`)
193
194
  # @return [String] Only the code for the invite.
@@ -219,7 +220,7 @@ module Discordrb
219
220
  return [channel(id)]
220
221
  end
221
222
 
222
- @servers.values.each do |server|
223
+ @servers.each_value do |server|
223
224
  server.channels.each do |channel|
224
225
  results << channel if channel.name == channel_name && (server_name || server.name) == server.name && (!type || (channel.type == type))
225
226
  end
@@ -248,6 +249,7 @@ module Discordrb
248
249
  def find_user(username, discrim = nil)
249
250
  users = @users.values.find_all { |e| e.username == username }
250
251
  return users.find { |u| u.discrim == discrim } if discrim
252
+
251
253
  users
252
254
  end
253
255
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # A colour (red, green and blue values). Used for role colours. If you prefer the American spelling, the alias
5
+ # {ColorRGB} is also available.
6
+ class ColourRGB
7
+ # @return [Integer] the red part of this colour (0-255).
8
+ attr_reader :red
9
+
10
+ # @return [Integer] the green part of this colour (0-255).
11
+ attr_reader :green
12
+
13
+ # @return [Integer] the blue part of this colour (0-255).
14
+ attr_reader :blue
15
+
16
+ # @return [Integer] the colour's RGB values combined into one integer.
17
+ attr_reader :combined
18
+ alias_method :to_i, :combined
19
+
20
+ # Make a new colour from the combined value.
21
+ # @param combined [String, Integer] The colour's RGB values combined into one integer or a hexadecimal string
22
+ # @example Initialize a with a base 10 integer
23
+ # ColourRGB.new(7506394) #=> ColourRGB
24
+ # ColourRGB.new(0x7289da) #=> ColourRGB
25
+ # @example Initialize a with a hexadecimal string
26
+ # ColourRGB.new('7289da') #=> ColourRGB
27
+ def initialize(combined)
28
+ @combined = combined.is_a?(String) ? combined.to_i(16) : combined
29
+ @red = (@combined >> 16) & 0xFF
30
+ @green = (@combined >> 8) & 0xFF
31
+ @blue = @combined & 0xFF
32
+ end
33
+
34
+ # @return [String] the colour as a hexadecimal.
35
+ def hex
36
+ @combined.to_s(16)
37
+ end
38
+ alias_method :hexadecimal, :hex
39
+ end
40
+
41
+ # Alias for the class {ColourRGB}
42
+ ColorRGB = ColourRGB
43
+ end