discordrb 3.3.0 → 3.5.0

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 (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