discordrb 3.3.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +152 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  5. data/.github/pull_request_template.md +37 -0
  6. data/.github/workflows/codeql.yml +65 -0
  7. data/.markdownlint.json +4 -0
  8. data/.rubocop.yml +39 -36
  9. data/CHANGELOG.md +874 -552
  10. data/Gemfile +2 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +80 -86
  13. data/Rakefile +2 -0
  14. data/bin/console +1 -0
  15. data/discordrb-webhooks.gemspec +9 -6
  16. data/discordrb.gemspec +21 -18
  17. data/lib/discordrb/allowed_mentions.rb +36 -0
  18. data/lib/discordrb/api/application.rb +202 -0
  19. data/lib/discordrb/api/channel.rb +236 -47
  20. data/lib/discordrb/api/interaction.rb +54 -0
  21. data/lib/discordrb/api/invite.rb +5 -5
  22. data/lib/discordrb/api/server.rb +94 -66
  23. data/lib/discordrb/api/user.rb +17 -11
  24. data/lib/discordrb/api/webhook.rb +63 -6
  25. data/lib/discordrb/api.rb +55 -16
  26. data/lib/discordrb/await.rb +0 -1
  27. data/lib/discordrb/bot.rb +480 -93
  28. data/lib/discordrb/cache.rb +31 -24
  29. data/lib/discordrb/colour_rgb.rb +43 -0
  30. data/lib/discordrb/commands/command_bot.rb +35 -12
  31. data/lib/discordrb/commands/container.rb +21 -24
  32. data/lib/discordrb/commands/parser.rb +20 -20
  33. data/lib/discordrb/commands/rate_limiter.rb +4 -3
  34. data/lib/discordrb/container.rb +209 -20
  35. data/lib/discordrb/data/activity.rb +271 -0
  36. data/lib/discordrb/data/application.rb +50 -0
  37. data/lib/discordrb/data/attachment.rb +71 -0
  38. data/lib/discordrb/data/audit_logs.rb +345 -0
  39. data/lib/discordrb/data/channel.rb +993 -0
  40. data/lib/discordrb/data/component.rb +229 -0
  41. data/lib/discordrb/data/embed.rb +251 -0
  42. data/lib/discordrb/data/emoji.rb +82 -0
  43. data/lib/discordrb/data/integration.rb +122 -0
  44. data/lib/discordrb/data/interaction.rb +800 -0
  45. data/lib/discordrb/data/invite.rb +137 -0
  46. data/lib/discordrb/data/member.rb +372 -0
  47. data/lib/discordrb/data/message.rb +414 -0
  48. data/lib/discordrb/data/overwrite.rb +108 -0
  49. data/lib/discordrb/data/profile.rb +91 -0
  50. data/lib/discordrb/data/reaction.rb +33 -0
  51. data/lib/discordrb/data/recipient.rb +34 -0
  52. data/lib/discordrb/data/role.rb +248 -0
  53. data/lib/discordrb/data/server.rb +1004 -0
  54. data/lib/discordrb/data/user.rb +264 -0
  55. data/lib/discordrb/data/voice_region.rb +45 -0
  56. data/lib/discordrb/data/voice_state.rb +41 -0
  57. data/lib/discordrb/data/webhook.rb +238 -0
  58. data/lib/discordrb/data.rb +28 -4180
  59. data/lib/discordrb/errors.rb +46 -4
  60. data/lib/discordrb/events/bans.rb +7 -5
  61. data/lib/discordrb/events/channels.rb +3 -1
  62. data/lib/discordrb/events/guilds.rb +16 -9
  63. data/lib/discordrb/events/interactions.rb +482 -0
  64. data/lib/discordrb/events/invites.rb +125 -0
  65. data/lib/discordrb/events/members.rb +6 -2
  66. data/lib/discordrb/events/message.rb +72 -27
  67. data/lib/discordrb/events/presence.rb +35 -18
  68. data/lib/discordrb/events/raw.rb +1 -3
  69. data/lib/discordrb/events/reactions.rb +49 -4
  70. data/lib/discordrb/events/threads.rb +96 -0
  71. data/lib/discordrb/events/typing.rb +6 -4
  72. data/lib/discordrb/events/voice_server_update.rb +47 -0
  73. data/lib/discordrb/events/voice_state_update.rb +15 -10
  74. data/lib/discordrb/events/webhooks.rb +9 -6
  75. data/lib/discordrb/gateway.rb +99 -71
  76. data/lib/discordrb/id_object.rb +39 -0
  77. data/lib/discordrb/light/integrations.rb +1 -1
  78. data/lib/discordrb/light/light_bot.rb +1 -1
  79. data/lib/discordrb/logger.rb +4 -4
  80. data/lib/discordrb/paginator.rb +57 -0
  81. data/lib/discordrb/permissions.rb +159 -39
  82. data/lib/discordrb/version.rb +1 -1
  83. data/lib/discordrb/voice/encoder.rb +16 -7
  84. data/lib/discordrb/voice/network.rb +99 -47
  85. data/lib/discordrb/voice/sodium.rb +98 -0
  86. data/lib/discordrb/voice/voice_bot.rb +33 -25
  87. data/lib/discordrb/webhooks.rb +2 -0
  88. data/lib/discordrb.rb +107 -1
  89. metadata +126 -54
  90. data/.codeclimate.yml +0 -16
  91. data/.travis.yml +0 -33
  92. data/bin/travis_build_docs.sh +0 -17
  93. /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
data/lib/discordrb/bot.rb CHANGED
@@ -18,11 +18,17 @@ 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'
22
+ require 'discordrb/events/interactions'
23
+ require 'discordrb/events/threads'
21
24
 
22
25
  require 'discordrb/api'
23
26
  require 'discordrb/api/channel'
24
27
  require 'discordrb/api/server'
25
28
  require 'discordrb/api/invite'
29
+ require 'discordrb/api/interaction'
30
+ require 'discordrb/api/application'
31
+
26
32
  require 'discordrb/errors'
27
33
  require 'discordrb/data'
28
34
  require 'discordrb/await'
@@ -92,21 +98,25 @@ module Discordrb
92
98
  # @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
93
99
  # unless you really need this so you don't inadvertently create infinite loops.
94
100
  # @param shard_id [Integer] The number of the shard this bot should handle. See
95
- # https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
101
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
96
102
  # @param num_shards [Integer] The total number of shards that should be running. See
97
- # https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
103
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
98
104
  # @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
99
105
  # @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
100
106
  # @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
101
107
  # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
102
108
  # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
103
109
  # that all data be received in a continuous compressed stream.
110
+ # @param intents [:all, :unprivileged, Array<Symbol>, :none] Gateway intents that this bot requires. `:all` will
111
+ # request all intents. `:unprivileged` will request only intents that are not defined as "Privileged". `:none`
112
+ # will request no intents. An array of symbols will request only those intents specified.
113
+ # @see Discordrb::INTENTS
104
114
  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
115
+ log_mode: :normal,
116
+ token: nil, client_id: nil,
117
+ type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
118
+ shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
119
+ compress_mode: :large, intents: :all
110
120
  )
111
121
  LOGGER.mode = log_mode
112
122
  LOGGER.token = token if redact_token
@@ -125,8 +135,21 @@ module Discordrb
125
135
 
126
136
  @compress_mode = compress_mode
127
137
 
138
+ raise 'Token string is empty or nil' if token.nil? || token.empty?
139
+
140
+ @intents = case intents
141
+ when :all
142
+ ALL_INTENTS
143
+ when :unprivileged
144
+ UNPRIVILEGED_INTENTS
145
+ when :none
146
+ NO_INTENTS
147
+ else
148
+ calculate_intents(intents)
149
+ end
150
+
128
151
  @token = process_token(@type, token)
129
- @gateway = Gateway.new(self, @token, @shard_key, @compress_mode)
152
+ @gateway = Gateway.new(self, @token, @shard_key, @compress_mode, @intents)
130
153
 
131
154
  init_cache
132
155
 
@@ -140,6 +163,8 @@ module Discordrb
140
163
  @current_thread = 0
141
164
 
142
165
  @status = :online
166
+
167
+ @application_commands = {}
143
168
  end
144
169
 
145
170
  # The list of users the bot shares a server with.
@@ -158,18 +183,23 @@ module Discordrb
158
183
  @servers
159
184
  end
160
185
 
186
+ # The list of members in threads the bot can see.
187
+ # @return [Hash<Integer => Hash<Integer => Hash<String => Object>>]
188
+ def thread_members
189
+ gateway_check
190
+ unavailable_servers_check
191
+ @thread_members
192
+ end
193
+
161
194
  # @overload emoji(id)
162
195
  # Return an emoji by its ID
163
- # @param id [Integer, #resolve_id] The emoji's ID.
196
+ # @param id [String, Integer] The emoji's ID.
164
197
  # @return [Emoji, nil] the emoji object. `nil` if the emoji was not found.
165
198
  # @overload emoji
166
199
  # The list of emoji the bot can use.
167
200
  # @return [Array<Emoji>] the emoji available.
168
201
  def emoji(id = nil)
169
- gateway_check
170
- unavailable_servers_check
171
-
172
- emoji_hash = @servers.values.map(&:emoji).reduce(&:merge)
202
+ emoji_hash = servers.values.map(&:emoji).reduce(&:merge)
173
203
  if id
174
204
  id = id.resolve_id
175
205
  emoji_hash[id]
@@ -193,8 +223,10 @@ module Discordrb
193
223
  # to edit user data like the current username (see {Profile#username=}).
194
224
  # @return [Profile] The bot's profile that can be used to edit data.
195
225
  def profile
196
- gateway_check
197
- @profile
226
+ return @profile if @profile
227
+
228
+ response = Discordrb::API::User.profile(@token)
229
+ @profile = Profile.new(JSON.parse(response), self)
198
230
  end
199
231
 
200
232
  alias_method :bot_user, :profile
@@ -203,6 +235,7 @@ module Discordrb
203
235
  # @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
204
236
  def bot_application
205
237
  return unless @type == :bot
238
+
206
239
  response = API.oauth_application(token)
207
240
  Application.new(JSON.parse(response), self)
208
241
  end
@@ -225,7 +258,7 @@ module Discordrb
225
258
 
226
259
  # Runs the bot, which logs into Discord and connects the WebSocket. This
227
260
  # prevents all further execution unless it is executed with
228
- # `backround` = `true`.
261
+ # `background` = `true`.
229
262
  # @param background [true, false] If it is `true`, then the bot will run in
230
263
  # another thread to allow further execution. If it is `false`, this method
231
264
  # will block until {#stop} is called. If the bot is run with `true`, make
@@ -254,9 +287,9 @@ module Discordrb
254
287
 
255
288
  # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
256
289
  # 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)
290
+ # @note This method no longer takes an argument as of 3.4.0
291
+ def stop(_no_sync = nil)
292
+ @gateway.stop
260
293
  end
261
294
 
262
295
  # @return [true, false] whether or not the bot is currently connected to Discord.
@@ -273,14 +306,14 @@ module Discordrb
273
306
 
274
307
  # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
275
308
  # @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.
309
+ # @param permission_bits [String, Integer] Permission bits that should be appended to invite url.
277
310
  # @return [String] the OAuth invite URL.
278
311
  def invite_url(server: nil, permission_bits: nil)
279
312
  @client_id ||= bot_application.id
280
313
 
281
314
  server_id_str = server ? "&guild_id=#{server.id}" : ''
282
315
  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"
316
+ "https://discord.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
284
317
  end
285
318
 
286
319
  # @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
@@ -299,22 +332,21 @@ module Discordrb
299
332
 
300
333
  server_id = channel.server.id
301
334
  return @voices[server_id] if @voices[server_id]
302
-
303
- nil
304
335
  end
305
336
 
306
337
  # Connects to a voice channel, initializes network connections and returns the {Voice::VoiceBot} over which audio
307
338
  # data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
308
339
  # connected to voice, the existing connection will be terminated - you don't have to call
309
340
  # {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
341
+ # @param chan [Channel, String, Integer] The voice channel, or its ID, to connect to.
342
+ # @param encrypted [true, false] Whether voice communication should be encrypted using
312
343
  # (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
313
344
  # @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
314
345
  def voice_connect(chan, encrypted = true)
346
+ raise ArgumentError, 'Unencrypted voice connections are no longer supported.' unless encrypted
347
+
315
348
  chan = channel(chan.resolve_id)
316
349
  server_id = chan.server.id
317
- @should_encrypt_voice = encrypted
318
350
 
319
351
  if @voices[chan.id]
320
352
  debug('Voice bot exists already! Destroying it')
@@ -336,7 +368,7 @@ module Discordrb
336
368
 
337
369
  # Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
338
370
  # {Discordrb::Voice::VoiceBot#destroy} rather than this.
339
- # @param server [Server, Integer, #resolve_id] The server the voice connection is on.
371
+ # @param server [Server, String, Integer] The server, or server ID, the voice connection is on.
340
372
  # @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
341
373
  # directly, you should leave it as true.
342
374
  def voice_destroy(server, destroy_vws = true)
@@ -355,31 +387,41 @@ module Discordrb
355
387
  end
356
388
 
357
389
  # 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.
390
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
359
391
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
360
392
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
361
- # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
393
+ # @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
394
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
395
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
396
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
362
397
  # @return [Message] The message that was sent.
363
- def send_message(channel, content, tts = false, embed = nil)
398
+ def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
364
399
  channel = channel.resolve_id
365
400
  debug("Sending message to #{channel} with content '#{content}'")
401
+ allowed_mentions = { parse: [] } if allowed_mentions == false
402
+ message_reference = { message_id: message_reference.id } if message_reference.respond_to?(:id)
403
+ embeds = (embeds.instance_of?(Array) ? embeds.map(&:to_hash) : [embeds&.to_hash]).compact
366
404
 
367
- response = API::Channel.create_message(token, channel, content, tts, embed ? embed.to_hash : nil)
405
+ response = API::Channel.create_message(token, channel, content, tts, embeds, nil, attachments, allowed_mentions&.to_hash, message_reference, components)
368
406
  Message.new(JSON.parse(response), self)
369
407
  end
370
408
 
371
409
  # Sends a text message to a channel given its ID and the message's content,
372
410
  # then deletes it after the specified timeout in seconds.
373
- # @param channel [Channel, Integer, #resolve_id] The channel to send something to.
411
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
374
412
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
375
413
  # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
376
414
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
377
- # @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)
415
+ # @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
416
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
417
+ # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
418
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
419
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
420
+ def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
379
421
  Thread.new do
380
422
  Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
381
423
 
382
- message = send_message(channel, content, tts, embed)
424
+ message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components)
383
425
  sleep(timeout)
384
426
  message.delete
385
427
  end
@@ -389,13 +431,25 @@ module Discordrb
389
431
 
390
432
  # Sends a file to a channel. If it is an image, it will automatically be embedded.
391
433
  # @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.
434
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
393
435
  # @param file [File] The file that should be sent.
394
436
  # @param caption [string] The caption for the file.
395
437
  # @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
438
+ # @param filename [String] Overrides the filename of the uploaded file
439
+ # @param spoiler [true, false] Whether or not this file should appear as a spoiler.
396
440
  # @example Send a file from disk
397
441
  # bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))
398
- def send_file(channel, file, caption: nil, tts: false)
442
+ def send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil)
443
+ if file.respond_to?(:read)
444
+ if spoiler
445
+ filename ||= File.basename(file.path)
446
+ filename = "SPOILER_#{filename}" unless filename.start_with? 'SPOILER_'
447
+ end
448
+ # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
449
+ file.define_singleton_method(:original_filename) { filename } if filename
450
+ file.define_singleton_method(:path) { filename } if filename
451
+ end
452
+
399
453
  channel = channel.resolve_id
400
454
  response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
401
455
  Message.new(JSON.parse(response), self)
@@ -410,14 +464,13 @@ module Discordrb
410
464
  def create_server(name, region = :'eu-central')
411
465
  response = API::Server.create(token, name, region)
412
466
  id = JSON.parse(response)['id'].to_i
413
- sleep 0.1 until @servers[id]
414
- server = @servers[id]
467
+ sleep 0.1 until (server = @servers[id])
415
468
  debug "Successfully created server #{server.id} with name #{server.name}"
416
469
  server
417
470
  end
418
471
 
419
472
  # 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
473
+ # Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
421
474
  # @param name [String] What your application should be called.
422
475
  # @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
423
476
  # @return [Array(String, String)] your applications' client ID and client secret to be used in OAuth authorization.
@@ -436,28 +489,46 @@ module Discordrb
436
489
  API.update_oauth_application(@token, name, redirect_uris, description, icon)
437
490
  end
438
491
 
439
- # Gets the user, channel, role or emoji from a mention of the user, channel, role or emoji.
492
+ # Gets the users, channels, roles and emoji from a string.
493
+ # @param mentions [String] The mentions, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
494
+ # @param server [Server, nil] The server of the associated mentions. (recommended for role parsing, to speed things up)
495
+ # @return [Array<User, Channel, Role, Emoji>] The array of users, channels, roles and emoji identified by the mentions, or `nil` if none exists.
496
+ def parse_mentions(mentions, server = nil)
497
+ array_to_return = []
498
+ # While possible mentions may be in message
499
+ while mentions.include?('<') && mentions.include?('>')
500
+ # Removing all content before the next possible mention
501
+ mentions = mentions.split('<', 2)[1]
502
+ # Locate the first valid mention enclosed in `<...>`, otherwise advance to the next open `<`
503
+ next unless mentions.split('>', 2).first.length < mentions.split('<', 2).first.length
504
+
505
+ # Store the possible mention value to be validated with RegEx
506
+ mention = mentions.split('>', 2).first
507
+ if /@!?(?<id>\d+)/ =~ mention
508
+ array_to_return << user(id) unless user(id).nil?
509
+ elsif /#(?<id>\d+)/ =~ mention
510
+ array_to_return << channel(id, server) unless channel(id, server).nil?
511
+ elsif /@&(?<id>\d+)/ =~ mention
512
+ if server
513
+ array_to_return << server.role(id) unless server.role(id).nil?
514
+ else
515
+ @servers.each_value do |element|
516
+ array_to_return << element.role(id) unless element.role(id).nil?
517
+ end
518
+ end
519
+ elsif /(?<animated>^a|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
520
+ array_to_return << (emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil))
521
+ end
522
+ end
523
+ array_to_return
524
+ end
525
+
526
+ # Gets the user, channel, role or emoji from a string.
440
527
  # @param mention [String] The mention, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
441
528
  # @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
442
529
  # @return [User, Channel, Role, Emoji] The user, channel, role or emoji identified by the mention, or `nil` if none exists.
443
530
  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
531
+ parse_mentions(mention, server).first
461
532
  end
462
533
 
463
534
  # Updates presence status.
@@ -466,7 +537,8 @@ module Discordrb
466
537
  # @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
467
538
  # @param since [Integer] When this status was set.
468
539
  # @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)
540
+ # @param activity_type [Integer] The type of activity status to display.
541
+ # Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching), or 5 (Competing).
470
542
  # @see Gateway#send_status_update
471
543
  def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
472
544
  gateway_check
@@ -480,7 +552,7 @@ module Discordrb
480
552
  @gateway.send_status_update(status, since, activity_obj, afk)
481
553
 
482
554
  # Update the status in the cache
483
- profile.update_presence('status' => status.to_s, 'game' => activity_obj)
555
+ profile.update_presence('status' => status.to_s, 'activities' => [activity_obj].compact)
484
556
  end
485
557
 
486
558
  # Sets the currently playing game to the specified game.
@@ -489,7 +561,6 @@ module Discordrb
489
561
  def game=(name)
490
562
  gateway_check
491
563
  update_status(@status, name, nil)
492
- name
493
564
  end
494
565
 
495
566
  alias_method :playing=, :game=
@@ -500,7 +571,6 @@ module Discordrb
500
571
  def listening=(name)
501
572
  gateway_check
502
573
  update_status(@status, name, nil, nil, nil, 2)
503
- name
504
574
  end
505
575
 
506
576
  # Sets the current watching status to the specified name.
@@ -509,7 +579,6 @@ module Discordrb
509
579
  def watching=(name)
510
580
  gateway_check
511
581
  update_status(@status, name, nil, nil, nil, 3)
512
- name
513
582
  end
514
583
 
515
584
  # Sets the currently online stream to the specified name and Twitch URL.
@@ -522,6 +591,14 @@ module Discordrb
522
591
  name
523
592
  end
524
593
 
594
+ # Sets the currently competing status to the specified name.
595
+ # @param name [String] The name of the game to be competing in.
596
+ # @return [String] The game that is being competed in now.
597
+ def competing=(name)
598
+ gateway_check
599
+ update_status(@status, name, nil, nil, nil, 5)
600
+ end
601
+
525
602
  # Sets status to online.
526
603
  def online
527
604
  gateway_check
@@ -550,6 +627,36 @@ module Discordrb
550
627
  update_status(:invisible, @activity, nil)
551
628
  end
552
629
 
630
+ # Join a thread
631
+ # @param channel [Channel, Integer, String]
632
+ def join_thread(channel)
633
+ API::Channel.join_thread(@token, channel.resolve_id)
634
+ nil
635
+ end
636
+
637
+ # Leave a thread
638
+ # @param channel [Channel, Integer, String]
639
+ def leave_thread(channel)
640
+ API::Channel.leave_thread(@token, channel.resolve_id)
641
+ nil
642
+ end
643
+
644
+ # Add a member to a thread
645
+ # @param channel [Channel, Integer, String]
646
+ # @param member [Member, Integer, String]
647
+ def add_thread_member(channel, member)
648
+ API::Channel.add_thread_member(@token, channel.resolve_id, member.resolve_id)
649
+ nil
650
+ end
651
+
652
+ # Remove a member from a thread
653
+ # @param channel [Channel, Integer, String]
654
+ # @param member [Member, Integer, String]
655
+ def remove_thread_member(channel, member)
656
+ API::Channel.remove_thread_member(@token, channel.resolve_id, member.resolve_id)
657
+ nil
658
+ end
659
+
553
660
  # Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
554
661
  def debug=(new_debug)
555
662
  LOGGER.debug = new_debug
@@ -576,6 +683,7 @@ module Discordrb
576
683
  # @deprecated Will be changed to blocking behavior in v4.0. Use {#add_await!} instead.
577
684
  def add_await(key, type, attributes = {}, &block)
578
685
  raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
686
+
579
687
  await = Await.new(self, key, type, attributes, block)
580
688
  @awaits ||= {}
581
689
  @awaits[key] = await
@@ -583,14 +691,17 @@ module Discordrb
583
691
 
584
692
  # Awaits an event, blocking the current thread until a response is received.
585
693
  # @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.
694
+ # @option attributes [Numeric] :timeout the amount of time (in seconds) to wait for a response before returning `nil`. Waits forever if omitted.
695
+ # @yield Executed when a matching event is received.
696
+ # @yieldparam event [Event] The event object that was triggered.
697
+ # @yieldreturn [true, false] Whether the event matches extra await criteria described by the block
587
698
  # @return [Event, nil] The event object that was triggered, or `nil` if a `timeout` was set and no event was raised in time.
588
699
  # @raise [ArgumentError] if `timeout` is given and is not a positive numeric value
589
700
  def add_await!(type, attributes = {})
590
701
  raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
591
702
 
592
703
  timeout = attributes[:timeout]
593
- raise ArgumentError, 'Timeout must be a number > 0' if timeout && timeout.is_a?(Numeric) && timeout <= 0
704
+ raise ArgumentError, 'Timeout must be a number > 0' if timeout.is_a?(Numeric) && !timeout.positive?
594
705
 
595
706
  mutex = Mutex.new
596
707
  cv = ConditionVariable.new
@@ -598,7 +709,12 @@ module Discordrb
598
709
  block = lambda do |event|
599
710
  mutex.synchronize do
600
711
  response = event
601
- cv.signal
712
+ if block_given?
713
+ result = yield(event)
714
+ cv.signal if result.is_a?(TrueClass)
715
+ else
716
+ cv.signal
717
+ end
602
718
  end
603
719
  end
604
720
 
@@ -615,25 +731,26 @@ module Discordrb
615
731
 
616
732
  remove_handler(handler)
617
733
  raise 'ConditionVariable was signaled without returning an event!' if response.nil? && timeout.nil?
734
+
618
735
  response
619
736
  end
620
737
 
621
738
  # Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.
622
739
  # @note Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and
623
740
  # presence and any other events will still be received.
624
- # @param user [User, Integer, #resolve_id] The user, or its ID, to be ignored.
741
+ # @param user [User, String, Integer] The user, or its ID, to be ignored.
625
742
  def ignore_user(user)
626
743
  @ignored_ids << user.resolve_id
627
744
  end
628
745
 
629
746
  # Remove a user from the ignore list.
630
- # @param user [User, Integer, #resolve_id] The user, or its ID, to be unignored.
747
+ # @param user [User, String, Integer] The user, or its ID, to be unignored.
631
748
  def unignore_user(user)
632
749
  @ignored_ids.delete(user.resolve_id)
633
750
  end
634
751
 
635
752
  # Checks whether a user is being ignored.
636
- # @param user [User, Integer, #resolve_id] The user, or its ID, to check.
753
+ # @param user [User, String, Integer] The user, or its ID, to check.
637
754
  # @return [true, false] whether or not the user is ignored.
638
755
  def ignored?(user)
639
756
  @ignored_ids.include?(user.resolve_id)
@@ -666,6 +783,118 @@ module Discordrb
666
783
  end
667
784
  end
668
785
 
786
+ # Get all application commands.
787
+ # @param server_id [String, Integer, nil] The ID of the server to get the commands from. Global if `nil`.
788
+ # @return [Array<ApplicationCommand>]
789
+ def get_application_commands(server_id: nil)
790
+ resp = if server_id
791
+ API::Application.get_guild_commands(@token, profile.id, server_id)
792
+ else
793
+ API::Application.get_global_commands(@token, profile.id)
794
+ end
795
+
796
+ JSON.parse(resp).map do |command_data|
797
+ ApplicationCommand.new(command_data, self, server_id)
798
+ end
799
+ end
800
+
801
+ # Get an application command by ID.
802
+ # @param command_id [String, Integer]
803
+ # @param server_id [String, Integer, nil] The ID of the server to get the command from. Global if `nil`.
804
+ def get_application_command(command_id, server_id: nil)
805
+ resp = if server_id
806
+ API::Application.get_guild_command(@token, profile.id, server_id, command_id)
807
+ else
808
+ API::Application.get_global_command(@token, profile.id, command_id)
809
+ end
810
+ ApplicationCommand.new(JSON.parse(resp), self, server_id)
811
+ end
812
+
813
+ # @yieldparam [OptionBuilder]
814
+ # @yieldparam [PermissionBuilder]
815
+ # @example
816
+ # bot.register_application_command(:reddit, 'Reddit Commands') do |cmd|
817
+ # cmd.subcommand_group(:subreddit, 'Subreddit Commands') do |group|
818
+ # group.subcommand(:hot, "What's trending") do |sub|
819
+ # sub.string(:subreddit, 'Subreddit to search')
820
+ # end
821
+ # group.subcommand(:new, "What's new") do |sub|
822
+ # sub.string(:since, 'How long ago', choices: ['this hour', 'today', 'this week', 'this month', 'this year', 'all time'])
823
+ # sub.string(:subreddit, 'Subreddit to search')
824
+ # end
825
+ # end
826
+ # end
827
+ def register_application_command(name, description, server_id: nil, default_permission: nil, type: :chat_input)
828
+ type = ApplicationCommand::TYPES[type] || type
829
+
830
+ builder = Interactions::OptionBuilder.new
831
+ permission_builder = Interactions::PermissionBuilder.new
832
+ yield(builder, permission_builder) if block_given?
833
+
834
+ resp = if server_id
835
+ API::Application.create_guild_command(@token, profile.id, server_id, name, description, builder.to_a, default_permission, type)
836
+ else
837
+ API::Application.create_global_command(@token, profile.id, name, description, builder.to_a, default_permission, type)
838
+ end
839
+ cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
840
+
841
+ if permission_builder.to_a.any?
842
+ raise ArgumentError, 'Permissions can only be set for guild commands' unless server_id
843
+
844
+ edit_application_command_permissions(cmd.id, server_id, permission_builder.to_a)
845
+ end
846
+
847
+ cmd
848
+ end
849
+
850
+ # @yieldparam [OptionBuilder]
851
+ # @yieldparam [PermissionBuilder]
852
+ def edit_application_command(command_id, server_id: nil, name: nil, description: nil, default_permission: nil, type: :chat_input)
853
+ type = ApplicationCommand::TYPES[type] || type
854
+
855
+ builder = Interactions::OptionBuilder.new
856
+ permission_builder = Interactions::PermissionBuilder.new
857
+
858
+ yield(builder, permission_builder) if block_given?
859
+
860
+ resp = if server_id
861
+ API::Application.edit_guild_command(@token, profile.id, server_id, command_id, name, description, builder.to_a, default_permission, type)
862
+ else
863
+ API::Application.edit_guild_command(@token, profile.id, command_id, name, description, builder.to_a, default_permission.type)
864
+ end
865
+ cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
866
+
867
+ if permission_builder.to_a.any?
868
+ raise ArgumentError, 'Permissions can only be set for guild commands' unless server_id
869
+
870
+ edit_application_command_permissions(cmd.id, server_id, permission_builder.to_a)
871
+ end
872
+
873
+ cmd
874
+ end
875
+
876
+ # Remove an application command from the commands registered with discord.
877
+ # @param command_id [String, Integer] The ID of the command to remove.
878
+ # @param server_id [String, Integer] The ID of the server to delete this command from, global if `nil`.
879
+ def delete_application_command(command_id, server_id: nil)
880
+ if server_id
881
+ API::Application.delete_guild_command(@token, profile.id, server_id, command_id)
882
+ else
883
+ API::Application.delete_global_command(@token, profile.id, command_id)
884
+ end
885
+ end
886
+
887
+ # @param command_id [Integer, String]
888
+ # @param server_id [Integer, String]
889
+ # @param permissions [Array<Hash>] An array of objects formatted as `{ id: ENTITY_ID, type: 1 or 2, permission: true or false }`
890
+ def edit_application_command_permissions(command_id, server_id, permissions = [])
891
+ builder = Interactions::PermissionBuilder.new
892
+ yield builder if block_given?
893
+
894
+ permissions += builder.to_a
895
+ API::Application.edit_guild_command_permissions(@token, profile.id, server_id, command_id, permissions)
896
+ end
897
+
669
898
  private
670
899
 
671
900
  # Throws a useful exception if there's currently no gateway connection.
@@ -677,7 +906,8 @@ module Discordrb
677
906
  # e.g. due to a Discord outage or because the servers are large and taking a while to load.
678
907
  def unavailable_servers_check
679
908
  # Return unless there are servers that are unavailable.
680
- return unless @unavailable_servers && @unavailable_servers > 0
909
+ return unless @unavailable_servers&.positive?
910
+
681
911
  LOGGER.warn("#{@unavailable_servers} servers haven't been cached yet.")
682
912
  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
913
  end
@@ -716,10 +946,16 @@ module Discordrb
716
946
 
717
947
  username = data['user']['username']
718
948
  if username && !member_is_new # Don't set the username for newly-cached members
719
- debug "Implicitly updating presence-obtained information for member #{user_id}"
949
+ debug "Implicitly updating presence-obtained information username for member #{user_id}"
720
950
  member.update_username(username)
721
951
  end
722
952
 
953
+ global_name = data['user']['global_name']
954
+ if global_name && !member_is_new # Don't set the global_name for newly-cached members
955
+ debug "Implicitly updating presence-obtained information global_name for member #{user_id}"
956
+ member.update_global_name(global_name)
957
+ end
958
+
723
959
  member.update_presence(data)
724
960
 
725
961
  member.avatar_id = data['user']['avatar'] if data['user']['avatar']
@@ -737,10 +973,21 @@ module Discordrb
737
973
 
738
974
  user_id = data['user_id'].to_i
739
975
  old_voice_state = server.voice_states[user_id]
740
- old_channel_id = old_voice_state.voice_channel.id if old_voice_state
976
+ old_channel_id = old_voice_state.voice_channel&.id if old_voice_state
741
977
 
742
978
  server.update_voice_state(data)
743
979
 
980
+ existing_voice = @voices[server_id]
981
+ if user_id == @profile.id && existing_voice
982
+ new_channel_id = data['channel_id']
983
+ if new_channel_id
984
+ new_channel = channel(new_channel_id)
985
+ existing_voice.channel = new_channel
986
+ else
987
+ voice_destroy(server_id)
988
+ end
989
+ end
990
+
744
991
  old_channel_id
745
992
  end
746
993
 
@@ -751,6 +998,7 @@ module Discordrb
751
998
 
752
999
  debug("Voice server update received! chan: #{channel.inspect}")
753
1000
  return unless channel
1001
+
754
1002
  @should_connect_to_voice.delete(server_id)
755
1003
  debug('Updating voice server!')
756
1004
 
@@ -763,19 +1011,19 @@ module Discordrb
763
1011
  end
764
1012
 
765
1013
  debug('Got data, now creating the bot.')
766
- @voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint, @should_encrypt_voice)
1014
+ @voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint)
767
1015
  end
768
1016
 
769
1017
  # Internal handler for CHANNEL_CREATE
770
1018
  def create_channel(data)
771
- channel = Channel.new(data, self)
1019
+ channel = data.is_a?(Discordrb::Channel) ? data : Channel.new(data, self)
772
1020
  server = channel.server
773
1021
 
774
1022
  # Handle normal and private channels separately
775
1023
  if server
776
1024
  server.add_channel(channel)
777
1025
  @channels[channel.id] = channel
778
- elsif channel.pm?
1026
+ elsif channel.private?
779
1027
  @pm_channels[channel.recipient.id] = channel
780
1028
  elsif channel.group?
781
1029
  @channels[channel.id] = channel
@@ -787,6 +1035,7 @@ module Discordrb
787
1035
  channel = Channel.new(data, self)
788
1036
  old_channel = @channels[channel.id]
789
1037
  return unless old_channel
1038
+
790
1039
  old_channel.update_from(channel)
791
1040
  end
792
1041
 
@@ -804,6 +1053,8 @@ module Discordrb
804
1053
  elsif channel.group?
805
1054
  @channels.delete(channel.id)
806
1055
  end
1056
+
1057
+ @thread_members.delete(channel.id) if channel.thread?
807
1058
  end
808
1059
 
809
1060
  # Internal handler for CHANNEL_RECIPIENT_ADD
@@ -843,12 +1094,16 @@ module Discordrb
843
1094
  member = server.member(data['user']['id'].to_i)
844
1095
  member.update_roles(data['roles'])
845
1096
  member.update_nick(data['nick'])
1097
+ member.update_global_name(data['user']['global_name']) if data['user']['global_name']
1098
+ member.update_boosting_since(data['premium_since'])
1099
+ member.update_communication_disabled_until(data['communication_disabled_until'])
846
1100
  end
847
1101
 
848
1102
  # Internal handler for GUILD_MEMBER_DELETE
849
1103
  def delete_guild_member(data)
850
1104
  server_id = data['guild_id'].to_i
851
1105
  server = self.server(server_id)
1106
+ return unless server
852
1107
 
853
1108
  user_id = data['user']['id'].to_i
854
1109
  server.delete_member(user_id)
@@ -858,7 +1113,7 @@ module Discordrb
858
1113
 
859
1114
  # Internal handler for GUILD_CREATE
860
1115
  def create_guild(data)
861
- ensure_server(data)
1116
+ ensure_server(data, true)
862
1117
  end
863
1118
 
864
1119
  # Internal handler for GUILD_UPDATE
@@ -949,15 +1204,15 @@ module Discordrb
949
1204
 
950
1205
  def process_token(type, token)
951
1206
  # Remove the "Bot " prefix if it exists
952
- token = token[4..-1] if token.start_with? 'Bot '
1207
+ token = token[4..] if token.start_with? 'Bot '
953
1208
 
954
- token = 'Bot ' + token unless type == :user
1209
+ token = "Bot #{token}" unless type == :user
955
1210
  token
956
1211
  end
957
1212
 
958
1213
  def handle_dispatch(type, data)
959
1214
  # 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
1215
+ if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !((@intents || 0) & INTENTS[:servers]).zero?
961
1216
  # The server streaming timed out!
962
1217
  LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
963
1218
  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.')
@@ -986,14 +1241,14 @@ module Discordrb
986
1241
  data['guilds'].each do |element|
987
1242
  # Check for true specifically because unavailable=false indicates that a previously unavailable server has
988
1243
  # come online
989
- if element['unavailable'].is_a? TrueClass
1244
+ if element['unavailable']
990
1245
  @unavailable_servers += 1
991
1246
 
992
1247
  # Ignore any unavailable servers
993
1248
  next
994
1249
  end
995
1250
 
996
- ensure_server(element)
1251
+ ensure_server(element, true)
997
1252
  end
998
1253
 
999
1254
  # Add PM and group channels
@@ -1018,9 +1273,14 @@ module Discordrb
1018
1273
  when :GUILD_MEMBERS_CHUNK
1019
1274
  id = data['guild_id'].to_i
1020
1275
  server = server(id)
1021
- server.process_chunk(data['members'])
1276
+ server.process_chunk(data['members'], data['chunk_index'], data['chunk_count'])
1277
+ when :INVITE_CREATE
1278
+ invite = Invite.new(data, self)
1279
+ raise_event(InviteCreateEvent.new(data, invite, self))
1280
+ when :INVITE_DELETE
1281
+ raise_event(InviteDeleteEvent.new(data, self))
1022
1282
  when :MESSAGE_CREATE
1023
- if ignored?(data['author']['id'].to_i)
1283
+ if ignored?(data['author']['id'])
1024
1284
  debug("Ignored author with ID #{data['author']['id']}")
1025
1285
  return
1026
1286
  end
@@ -1037,6 +1297,13 @@ module Discordrb
1037
1297
 
1038
1298
  return if message.from_bot? && !should_parse_self
1039
1299
 
1300
+ # Dispatch a ChannelCreateEvent for channels we don't have cached
1301
+ if message.channel.private? && @pm_channels[message.channel.recipient.id].nil?
1302
+ create_channel(message.channel)
1303
+
1304
+ raise_event(ChannelCreateEvent.new(message.channel, self))
1305
+ end
1306
+
1040
1307
  event = MessageEvent.new(message, self)
1041
1308
  raise_event(event)
1042
1309
 
@@ -1053,6 +1320,10 @@ module Discordrb
1053
1320
  update_message(data)
1054
1321
 
1055
1322
  message = Message.new(data, self)
1323
+
1324
+ event = MessageUpdateEvent.new(message, self)
1325
+ raise_event(event)
1326
+
1056
1327
  return if message.from_bot? && !should_parse_self
1057
1328
 
1058
1329
  unless message.author
@@ -1115,18 +1386,28 @@ module Discordrb
1115
1386
  # Ignore friends list presences
1116
1387
  return unless data['guild_id']
1117
1388
 
1118
- now_playing = data['game'].nil? ? nil : data['game']['name']
1389
+ new_activities = (data['activities'] || []).map { |act_data| Activity.new(act_data, self) }
1119
1390
  presence_user = @users[data['user']['id'].to_i]
1120
- played_before = presence_user.nil? ? nil : presence_user.game
1391
+ old_activities = (presence_user&.activities || [])
1121
1392
  update_presence(data)
1122
1393
 
1123
- event = if now_playing != played_before
1124
- PlayingEvent.new(data, self)
1125
- else
1126
- PresenceEvent.new(data, self)
1127
- end
1394
+ # Starting a new game
1395
+ playing_change = new_activities.reject do |act|
1396
+ old_activities.find { |old| old.name == act.name }
1397
+ end
1128
1398
 
1129
- raise_event(event)
1399
+ # Exiting an existing game
1400
+ playing_change += old_activities.reject do |old|
1401
+ new_activities.find { |act| act.name == old.name }
1402
+ end
1403
+
1404
+ if playing_change.any?
1405
+ playing_change.each do |act|
1406
+ raise_event(PlayingEvent.new(data, act, self))
1407
+ end
1408
+ else
1409
+ raise_event(PresenceEvent.new(data, self))
1410
+ end
1130
1411
  when :VOICE_STATE_UPDATE
1131
1412
  old_channel_id = update_voice_state(data)
1132
1413
 
@@ -1135,7 +1416,8 @@ module Discordrb
1135
1416
  when :VOICE_SERVER_UPDATE
1136
1417
  update_voice_server(data)
1137
1418
 
1138
- # no event as this is irrelevant to users
1419
+ event = VoiceServerUpdateEvent.new(data, self)
1420
+ raise_event(event)
1139
1421
  when :CHANNEL_CREATE
1140
1422
  create_channel(data)
1141
1423
 
@@ -1262,9 +1544,93 @@ module Discordrb
1262
1544
  event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1263
1545
  raise_event(event)
1264
1546
  end
1547
+ when :INTERACTION_CREATE
1548
+ event = InteractionCreateEvent.new(data, self)
1549
+ raise_event(event)
1550
+
1551
+ case data['type']
1552
+ when Interaction::TYPES[:command]
1553
+ event = ApplicationCommandEvent.new(data, self)
1554
+
1555
+ Thread.new do
1556
+ Thread.current[:discordrb_name] = "it-#{event.interaction.id}"
1557
+
1558
+ begin
1559
+ debug("Executing application command #{event.command_name}:#{event.command_id}")
1560
+
1561
+ @application_commands[event.command_name]&.call(event)
1562
+ rescue StandardError => e
1563
+ log_exception(e)
1564
+ end
1565
+ end
1566
+ when Interaction::TYPES[:component]
1567
+ case data['data']['component_type']
1568
+ when Webhooks::View::COMPONENT_TYPES[:button]
1569
+ event = ButtonEvent.new(data, self)
1570
+
1571
+ raise_event(event)
1572
+ when Webhooks::View::COMPONENT_TYPES[:string_select]
1573
+ event = StringSelectEvent.new(data, self)
1574
+
1575
+ raise_event(event)
1576
+ when Webhooks::View::COMPONENT_TYPES[:user_select]
1577
+ event = UserSelectEvent.new(data, self)
1578
+
1579
+ raise_event(event)
1580
+ when Webhooks::View::COMPONENT_TYPES[:role_select]
1581
+ event = RoleSelectEvent.new(data, self)
1582
+
1583
+ raise_event(event)
1584
+ when Webhooks::View::COMPONENT_TYPES[:mentionable_select]
1585
+ event = MentionableSelectEvent.new(data, self)
1586
+
1587
+ raise_event(event)
1588
+ when Webhooks::View::COMPONENT_TYPES[:channel_select]
1589
+ event = ChannelSelectEvent.new(data, self)
1590
+
1591
+ raise_event(event)
1592
+ end
1593
+ when Interaction::TYPES[:modal_submit]
1594
+
1595
+ event = ModalSubmitEvent.new(data, self)
1596
+ raise_event(event)
1597
+ end
1265
1598
  when :WEBHOOKS_UPDATE
1266
1599
  event = WebhookUpdateEvent.new(data, self)
1267
1600
  raise_event(event)
1601
+ when :THREAD_CREATE
1602
+ create_channel(data)
1603
+
1604
+ event = ThreadCreateEvent.new(data, self)
1605
+ raise_event(event)
1606
+ when :THREAD_UPDATE
1607
+ update_channel(data)
1608
+
1609
+ event = ThreadUpdateEvent.new(data, self)
1610
+ raise_event(event)
1611
+ when :THREAD_DELETE
1612
+ delete_channel(data)
1613
+ @thread_members.delete(data['id']&.resolve_id)
1614
+
1615
+ # raise ThreadDeleteEvent
1616
+ when :THREAD_LIST_SYNC
1617
+ data['members'].map { |member| ensure_thread_member(member) }
1618
+ data['threads'].map { |channel| ensure_channel(channel, data['guild_id']) }
1619
+
1620
+ # raise ThreadListSyncEvent?
1621
+ when :THREAD_MEMBER_UPDATE
1622
+ ensure_thread_member(data)
1623
+ when :THREAD_MEMBERS_UPDATE
1624
+ data['added_members']&.each do |added_member|
1625
+ ensure_thread_member(added_member) if added_member['user_id']
1626
+ end
1627
+
1628
+ data['removed_member_ids']&.each do |member_id|
1629
+ @thread_members[data['id']&.resolve_id]&.delete(member_id&.resolve_id)
1630
+ end
1631
+
1632
+ event = ThreadMembersUpdateEvent.new(data, self)
1633
+ raise_event(event)
1268
1634
  else
1269
1635
  # another event that we don't support yet
1270
1636
  debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
@@ -1300,6 +1666,7 @@ module Discordrb
1300
1666
  @event_handlers ||= {}
1301
1667
  handlers = @event_handlers[event.class]
1302
1668
  return unless handlers
1669
+
1303
1670
  handlers.dup.each do |handler|
1304
1671
  call_event(handler, event) if handler.matches?(event)
1305
1672
  end
@@ -1315,7 +1682,7 @@ module Discordrb
1315
1682
  begin
1316
1683
  handler.call(event)
1317
1684
  handler.after_call(event)
1318
- rescue => e
1685
+ rescue StandardError => e
1319
1686
  log_exception(e)
1320
1687
  ensure
1321
1688
  @event_threads.delete(t)
@@ -1328,6 +1695,7 @@ module Discordrb
1328
1695
  @awaits.each do |_, await|
1329
1696
  key, should_delete = await.match(event)
1330
1697
  next unless key
1698
+
1331
1699
  debug("should_delete: #{should_delete}")
1332
1700
  @awaits.delete(await.key) if should_delete
1333
1701
 
@@ -1335,5 +1703,24 @@ module Discordrb
1335
1703
  raise_event(await_event)
1336
1704
  end
1337
1705
  end
1706
+
1707
+ def calculate_intents(intents)
1708
+ intents.reduce(0) do |sum, intent|
1709
+ case intent
1710
+ when Symbol
1711
+ if INTENTS[intent]
1712
+ sum | INTENTS[intent]
1713
+ else
1714
+ LOGGER.warn("Unknown intent: #{intent}")
1715
+ sum
1716
+ end
1717
+ when Integer
1718
+ sum | intent
1719
+ else
1720
+ LOGGER.warn("Invalid intent: #{intent}")
1721
+ sum
1722
+ end
1723
+ end
1724
+ end
1338
1725
  end
1339
1726
  end