discordrb 3.3.0 → 3.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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