discordrb 3.5.0 → 3.6.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/workflows/ci.yml +78 -0
  6. data/.github/workflows/codeql.yml +3 -3
  7. data/.github/workflows/deploy.yml +54 -0
  8. data/.github/workflows/release.yml +45 -0
  9. data/.rubocop.yml +52 -2
  10. data/CHANGELOG.md +95 -0
  11. data/README.md +5 -5
  12. data/discordrb-webhooks.gemspec +1 -1
  13. data/discordrb.gemspec +16 -11
  14. data/lib/discordrb/api/application.rb +84 -8
  15. data/lib/discordrb/api/channel.rb +51 -13
  16. data/lib/discordrb/api/interaction.rb +15 -6
  17. data/lib/discordrb/api/invite.rb +1 -1
  18. data/lib/discordrb/api/server.rb +96 -60
  19. data/lib/discordrb/api/user.rb +12 -2
  20. data/lib/discordrb/api/webhook.rb +20 -5
  21. data/lib/discordrb/api.rb +16 -20
  22. data/lib/discordrb/bot.rb +139 -53
  23. data/lib/discordrb/cache.rb +15 -1
  24. data/lib/discordrb/commands/command_bot.rb +7 -17
  25. data/lib/discordrb/commands/parser.rb +7 -7
  26. data/lib/discordrb/container.rb +46 -0
  27. data/lib/discordrb/data/activity.rb +1 -1
  28. data/lib/discordrb/data/application.rb +1 -0
  29. data/lib/discordrb/data/attachment.rb +23 -3
  30. data/lib/discordrb/data/avatar_decoration.rb +26 -0
  31. data/lib/discordrb/data/call.rb +22 -0
  32. data/lib/discordrb/data/channel.rb +140 -15
  33. data/lib/discordrb/data/collectibles.rb +45 -0
  34. data/lib/discordrb/data/embed.rb +10 -3
  35. data/lib/discordrb/data/emoji.rb +20 -1
  36. data/lib/discordrb/data/integration.rb +3 -0
  37. data/lib/discordrb/data/interaction.rb +164 -27
  38. data/lib/discordrb/data/member.rb +145 -28
  39. data/lib/discordrb/data/message.rb +198 -51
  40. data/lib/discordrb/data/overwrite.rb +2 -0
  41. data/lib/discordrb/data/primary_server.rb +60 -0
  42. data/lib/discordrb/data/profile.rb +2 -7
  43. data/lib/discordrb/data/reaction.rb +2 -1
  44. data/lib/discordrb/data/recipient.rb +1 -1
  45. data/lib/discordrb/data/role.rb +151 -22
  46. data/lib/discordrb/data/server.rb +115 -41
  47. data/lib/discordrb/data/server_preview.rb +68 -0
  48. data/lib/discordrb/data/snapshot.rb +110 -0
  49. data/lib/discordrb/data/user.rb +68 -8
  50. data/lib/discordrb/data/voice_region.rb +1 -0
  51. data/lib/discordrb/data/webhook.rb +2 -5
  52. data/lib/discordrb/data.rb +6 -0
  53. data/lib/discordrb/errors.rb +5 -2
  54. data/lib/discordrb/events/await.rb +1 -1
  55. data/lib/discordrb/events/channels.rb +37 -0
  56. data/lib/discordrb/events/generic.rb +2 -0
  57. data/lib/discordrb/events/guilds.rb +6 -1
  58. data/lib/discordrb/events/interactions.rb +135 -42
  59. data/lib/discordrb/events/invites.rb +2 -0
  60. data/lib/discordrb/events/members.rb +19 -2
  61. data/lib/discordrb/events/message.rb +39 -8
  62. data/lib/discordrb/events/presence.rb +2 -0
  63. data/lib/discordrb/events/raw.rb +1 -0
  64. data/lib/discordrb/events/reactions.rb +2 -0
  65. data/lib/discordrb/events/roles.rb +2 -0
  66. data/lib/discordrb/events/threads.rb +10 -6
  67. data/lib/discordrb/events/typing.rb +1 -0
  68. data/lib/discordrb/events/voice_server_update.rb +1 -0
  69. data/lib/discordrb/events/voice_state_update.rb +1 -0
  70. data/lib/discordrb/events/webhooks.rb +1 -0
  71. data/lib/discordrb/gateway.rb +29 -13
  72. data/lib/discordrb/paginator.rb +3 -3
  73. data/lib/discordrb/permissions.rb +54 -43
  74. data/lib/discordrb/version.rb +1 -1
  75. data/lib/discordrb/websocket.rb +0 -10
  76. data/lib/discordrb.rb +17 -1
  77. metadata +53 -25
  78. data/.circleci/config.yml +0 -152
data/lib/discordrb/api.rb CHANGED
@@ -203,6 +203,11 @@ module Discordrb::API
203
203
  "#{cdn_url}/splashes/#{server_id}/#{splash_id}.#{format}"
204
204
  end
205
205
 
206
+ # Make a discovery splash URL from server and splash IDs
207
+ def discovery_splash_url(server_id, splash_id, format = 'webp')
208
+ "#{cdn_url}/discovery-splashes/#{server_id}/#{splash_id}.#{format}"
209
+ end
210
+
206
211
  # Make a banner URL from server and banner IDs
207
212
  def banner_url(server_id, banner_id, format = 'webp')
208
213
  "#{cdn_url}/banners/#{server_id}/#{banner_id}.#{format}"
@@ -231,28 +236,19 @@ module Discordrb::API
231
236
  "#{cdn_url}/role-icons/#{role_id}/#{icon_hash}.#{format}"
232
237
  end
233
238
 
234
- # Login to the server
235
- def login(email, password)
236
- request(
237
- :auth_login,
238
- nil,
239
- :post,
240
- "#{api_base}/auth/login",
241
- email: email,
242
- password: password
243
- )
239
+ # make an avatar decoration URL from an avatar decoration ID.
240
+ def avatar_decoration_url(avatar_decoration_id, format = 'png')
241
+ "#{cdn_url}/avatar-decoration-presets/#{avatar_decoration_id}.#{format}"
244
242
  end
245
243
 
246
- # Logout from the server
247
- def logout(token)
248
- request(
249
- :auth_logout,
250
- nil,
251
- :post,
252
- "#{api_base}/auth/logout",
253
- nil,
254
- Authorization: token
255
- )
244
+ # make a nameplate URL from the nameplate asset.
245
+ def nameplate_url(nameplate_asset, format = 'webm')
246
+ "#{cdn_url}/assets/collectibles/#{nameplate_asset.delete_suffix('/')}/asset.#{format}"
247
+ end
248
+
249
+ # make a server tag badge URL from a server ID and badge ID.
250
+ def server_tag_badge_url(server_id, badge_id, format = 'webp')
251
+ "#{cdn_url}/guild-tag-badges/#{server_id}/#{badge_id}.#{format}"
256
252
  end
257
253
 
258
254
  # Create an OAuth application
data/lib/discordrb/bot.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'rest-client'
4
4
  require 'zlib'
5
- require 'set'
6
5
 
7
6
  require 'discordrb/events/message'
8
7
  require 'discordrb/events/typing'
@@ -107,9 +106,10 @@ module Discordrb
107
106
  # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
108
107
  # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
109
108
  # 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
109
+ # @param intents [:all, :unprivileged, Array<Symbol>, :none, Integer] Gateway intents that this bot requires. `:all` will
111
110
  # 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.
111
+ # will request no intents. An array of symbols will request only those intents specified. An integer value will request
112
+ # exactly all the intents specified in the bitwise value.
113
113
  # @see Discordrb::INTENTS
114
114
  def initialize(
115
115
  log_mode: :normal,
@@ -307,13 +307,21 @@ module Discordrb
307
307
  # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
308
308
  # @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
309
309
  # @param permission_bits [String, Integer] Permission bits that should be appended to invite url.
310
+ # @param redirect_uri [String] Redirect URI that should be appended to invite url.
311
+ # @param scopes [Array<String>] Scopes that should be appended to invite url.
310
312
  # @return [String] the OAuth invite URL.
311
- def invite_url(server: nil, permission_bits: nil)
313
+ def invite_url(server: nil, permission_bits: nil, redirect_uri: nil, scopes: ['bot'])
312
314
  @client_id ||= bot_application.id
313
315
 
314
- server_id_str = server ? "&guild_id=#{server.id}" : ''
315
- permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
316
- "https://discord.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
316
+ query = URI.encode_www_form({
317
+ client_id: @client_id,
318
+ guild_id: server&.id,
319
+ permissions: permission_bits,
320
+ redirect_uri: redirect_uri,
321
+ scope: scopes.join(' ')
322
+ }.compact)
323
+
324
+ "https://discord.com/oauth2/authorize?#{query}"
317
325
  end
318
326
 
319
327
  # @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
@@ -392,17 +400,20 @@ module Discordrb
392
400
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
393
401
  # @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
394
402
  # @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.
403
+ # @param message_reference [Message, String, Integer, Hash, nil] The message, or message ID, to reply to if any.
396
404
  # @param components [View, Array<Hash>] Interaction components to associate with this message.
405
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2), SUPPRESS_NOTIFICATIONS (1 << 12), and IS_COMPONENTS_V2 (1 << 15) can be set.
406
+ # @param nonce [String, nil] A optional nonce in order to verify that a message was sent. Maximum of twenty-five characters.
407
+ # @param enforce_nonce [true, false] whether the nonce should be enforced and used for message de-duplication.
397
408
  # @return [Message] The message that was sent.
398
- def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
409
+ def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false)
399
410
  channel = channel.resolve_id
400
411
  debug("Sending message to #{channel} with content '#{content}'")
401
412
  allowed_mentions = { parse: [] } if allowed_mentions == false
402
- message_reference = { message_id: message_reference.id } if message_reference.respond_to?(:id)
413
+ message_reference = { message_id: message_reference.resolve_id } if message_reference.respond_to?(:resolve_id)
403
414
  embeds = (embeds.instance_of?(Array) ? embeds.map(&:to_hash) : [embeds&.to_hash]).compact
404
415
 
405
- response = API::Channel.create_message(token, channel, content, tts, embeds, nil, attachments, allowed_mentions&.to_hash, message_reference, components)
416
+ response = API::Channel.create_message(token, channel, content, tts, embeds, nonce, attachments, allowed_mentions&.to_hash, message_reference, components, flags, enforce_nonce)
406
417
  Message.new(JSON.parse(response), self)
407
418
  end
408
419
 
@@ -417,11 +428,14 @@ module Discordrb
417
428
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
418
429
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
419
430
  # @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)
431
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2), SUPPRESS_NOTIFICATIONS (1 << 12), and IS_COMPONENTS_V2 (1 << 15) can be set.
432
+ # @param nonce [String, nil] A optional nonce in order to verify that a message was sent. Maximum of twenty-five characters.
433
+ # @param enforce_nonce [true, false] whether the nonce should be enforced and used for message de-duplication.
434
+ def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false)
421
435
  Thread.new do
422
436
  Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
423
437
 
424
- message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components)
438
+ message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce)
425
439
  sleep(timeout)
426
440
  message.delete
427
441
  end
@@ -455,20 +469,6 @@ module Discordrb
455
469
  Message.new(JSON.parse(response), self)
456
470
  end
457
471
 
458
- # Creates a server on Discord with a specified name and a region.
459
- # @note Discord's API doesn't directly return the server when creating it, so this method
460
- # waits until the data has been received via the websocket. This may make the execution take a while.
461
- # @param name [String] The name the new server should have. Doesn't have to be alphanumeric.
462
- # @param region [Symbol] The region where the server should be created, for example 'eu-central' or 'hongkong'.
463
- # @return [Server] The server that was created.
464
- def create_server(name, region = :'eu-central')
465
- response = API::Server.create(token, name, region)
466
- id = JSON.parse(response)['id'].to_i
467
- sleep 0.1 until (server = @servers[id])
468
- debug "Successfully created server #{server.id} with name #{server.name}"
469
- server
470
- end
471
-
472
472
  # Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using
473
473
  # Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
474
474
  # @param name [String] What your application should be called.
@@ -517,7 +517,7 @@ module Discordrb
517
517
  end
518
518
  end
519
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))
520
+ array_to_return << (emoji(id) || Emoji.new({ 'animated' => animated != '', 'name' => name, 'id' => id }, self, nil))
521
521
  end
522
522
  end
523
523
  array_to_return
@@ -824,7 +824,7 @@ module Discordrb
824
824
  # end
825
825
  # end
826
826
  # end
827
- def register_application_command(name, description, server_id: nil, default_permission: nil, type: :chat_input)
827
+ def register_application_command(name, description, server_id: nil, default_permission: nil, type: :chat_input, default_member_permissions: nil, contexts: nil, nsfw: false)
828
828
  type = ApplicationCommand::TYPES[type] || type
829
829
 
830
830
  builder = Interactions::OptionBuilder.new
@@ -832,9 +832,9 @@ module Discordrb
832
832
  yield(builder, permission_builder) if block_given?
833
833
 
834
834
  resp = if server_id
835
- API::Application.create_guild_command(@token, profile.id, server_id, name, description, builder.to_a, default_permission, type)
835
+ API::Application.create_guild_command(@token, profile.id, server_id, name, description, builder.to_a, default_permission, type, default_member_permissions, contexts, nsfw)
836
836
  else
837
- API::Application.create_global_command(@token, profile.id, name, description, builder.to_a, default_permission, type)
837
+ API::Application.create_global_command(@token, profile.id, name, description, builder.to_a, default_permission, type, default_member_permissions, contexts, nsfw)
838
838
  end
839
839
  cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
840
840
 
@@ -849,7 +849,7 @@ module Discordrb
849
849
 
850
850
  # @yieldparam [OptionBuilder]
851
851
  # @yieldparam [PermissionBuilder]
852
- def edit_application_command(command_id, server_id: nil, name: nil, description: nil, default_permission: nil, type: :chat_input)
852
+ def edit_application_command(command_id, server_id: nil, name: nil, description: nil, default_permission: nil, type: :chat_input, default_member_permissions: nil, contexts: nil, nsfw: nil)
853
853
  type = ApplicationCommand::TYPES[type] || type
854
854
 
855
855
  builder = Interactions::OptionBuilder.new
@@ -858,9 +858,9 @@ module Discordrb
858
858
  yield(builder, permission_builder) if block_given?
859
859
 
860
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)
861
+ API::Application.edit_guild_command(@token, profile.id, server_id, command_id, name, description, builder.to_a, default_permission, type, default_member_permissions, contexts, nsfw)
862
862
  else
863
- API::Application.edit_guild_command(@token, profile.id, command_id, name, description, builder.to_a, default_permission.type)
863
+ API::Application.edit_global_command(@token, profile.id, command_id, name, description, builder.to_a, default_permission, type, default_member_permissions, contexts, nsfw)
864
864
  end
865
865
  cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
866
866
 
@@ -895,6 +895,46 @@ module Discordrb
895
895
  API::Application.edit_guild_command_permissions(@token, profile.id, server_id, command_id, permissions)
896
896
  end
897
897
 
898
+ # Fetches all the application emojis that the bot can use.
899
+ # @return [Array<Emoji>] Returns an array of emoji objects.
900
+ def application_emojis
901
+ response = API::Application.list_application_emojis(@token, profile.id)
902
+ JSON.parse(response)['items'].map { |emoji| Emoji.new(emoji, self) }
903
+ end
904
+
905
+ # Fetches a single application emoji from its ID.
906
+ # @param emoji_id [Integer, String] ID of the application emoji.
907
+ # @return [Emoji] The application emoji.
908
+ def application_emoji(emoji_id)
909
+ response = API::Application.get_application_emoji(@token, profile.id, emoji_id.resolve_id)
910
+ Emoji.new(JSON.parse(response), self)
911
+ end
912
+
913
+ # Creates a new custom emoji that can be used by this application.
914
+ # @param name [String] The name of emoji to create.
915
+ # @param image [String, #read] Base64 string with the image data, or an object that responds to #read.
916
+ # @return [Emoji] The emoji that has been created.
917
+ def create_application_emoji(name:, image:)
918
+ image = image.respond_to?(:read) ? Discordrb.encode64(image) : image
919
+ response = API::Application.create_application_emoji(@token, profile.id, name, image)
920
+ Emoji.new(JSON.parse(response), self)
921
+ end
922
+
923
+ # Edits an existing application emoji.
924
+ # @param emoji_id [Integer, String, Emoji] ID of the application emoji to edit.
925
+ # @param name [String] The new name of the emoji.
926
+ # @return [Emoji] Returns the updated emoji object on success.
927
+ def edit_application_emoji(emoji_id, name:)
928
+ response = API::Application.edit_application_emoji(@token, profile.id, emoji_id.resolve_id, name)
929
+ Emoji.new(JSON.parse(response), self)
930
+ end
931
+
932
+ # Deletes an existing application emoji.
933
+ # @param emoji_id [Integer, String, Emoji] ID of the application emoji to delete.
934
+ def delete_application_emoji(emoji_id)
935
+ API::Application.delete_application_emoji(@token, profile.id, emoji_id.resolve_id)
936
+ end
937
+
898
938
  private
899
939
 
900
940
  # Throws a useful exception if there's currently no gateway connection.
@@ -1091,12 +1131,12 @@ module Discordrb
1091
1131
  server_id = data['guild_id'].to_i
1092
1132
  server = self.server(server_id)
1093
1133
 
1094
- member = server.member(data['user']['id'].to_i)
1095
- member.update_roles(data['roles'])
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'])
1134
+ # Only attempt to update members that're already cached
1135
+ if (member = server.member(data['user']['id'].to_i, false))
1136
+ member.update_data(data)
1137
+ else
1138
+ ensure_user(data['user'])
1139
+ end
1100
1140
  end
1101
1141
 
1102
1142
  # Internal handler for GUILD_MEMBER_DELETE
@@ -1212,7 +1252,7 @@ module Discordrb
1212
1252
 
1213
1253
  def handle_dispatch(type, data)
1214
1254
  # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
1215
- if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !((@intents || 0) & INTENTS[:servers]).zero?
1255
+ if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !(@intents || 0).nobits?(INTENTS[:servers])
1216
1256
  # The server streaming timed out!
1217
1257
  LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
1218
1258
  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.')
@@ -1232,6 +1272,8 @@ module Discordrb
1232
1272
 
1233
1273
  @profile = Profile.new(data['user'], self)
1234
1274
 
1275
+ @client_id ||= data['application']['id']&.to_i
1276
+
1235
1277
  # Initialize servers
1236
1278
  @servers = {}
1237
1279
 
@@ -1274,6 +1316,8 @@ module Discordrb
1274
1316
  id = data['guild_id'].to_i
1275
1317
  server = server(id)
1276
1318
  server.process_chunk(data['members'], data['chunk_index'], data['chunk_count'])
1319
+ when :USER_UPDATE
1320
+ @profile = Profile.new(data, self)
1277
1321
  when :INVITE_CREATE
1278
1322
  invite = Invite.new(data, self)
1279
1323
  raise_event(InviteCreateEvent.new(data, invite, self))
@@ -1290,12 +1334,22 @@ module Discordrb
1290
1334
  return
1291
1335
  end
1292
1336
 
1337
+ if !should_parse_self && profile.id == data['author']['id'].to_i
1338
+ debug('Ignored message from the current bot')
1339
+ return
1340
+ end
1341
+
1293
1342
  # If create_message is overwritten with a method that returns the parsed message, use that instead, so we don't
1294
1343
  # parse the message twice (which is just thrown away performance)
1295
1344
  message = create_message(data)
1296
1345
  message = Message.new(data, self) unless message.is_a? Message
1297
1346
 
1298
- return if message.from_bot? && !should_parse_self
1347
+ # Update the existing member if it exists in the cache.
1348
+ if data['member']
1349
+ member = message.channel.server&.member(data['author']['id'].to_i, false)
1350
+ data['member']['user'] = data['author']
1351
+ member&.update_data(data['member'])
1352
+ end
1299
1353
 
1300
1354
  # Dispatch a ChannelCreateEvent for channels we don't have cached
1301
1355
  if message.channel.private? && @pm_channels[message.channel.recipient.id].nil?
@@ -1319,18 +1373,28 @@ module Discordrb
1319
1373
  when :MESSAGE_UPDATE
1320
1374
  update_message(data)
1321
1375
 
1376
+ if !should_parse_self && profile.id == data['author']['id'].to_i
1377
+ debug('Ignored message from the current bot')
1378
+ return
1379
+ end
1380
+
1322
1381
  message = Message.new(data, self)
1323
1382
 
1324
1383
  event = MessageUpdateEvent.new(message, self)
1325
1384
  raise_event(event)
1326
1385
 
1327
- return if message.from_bot? && !should_parse_self
1328
-
1329
- unless message.author
1386
+ if data['author'].nil?
1330
1387
  LOGGER.debug("Edited a message with nil author! Content: #{message.content.inspect}, channel: #{message.channel.inspect}")
1331
1388
  return
1332
1389
  end
1333
1390
 
1391
+ # Update the existing member if it exists in the cache.
1392
+ if data['member']
1393
+ member = message.channel.server&.member(data['author']['id'].to_i, false)
1394
+ data['member']['user'] = data['author']
1395
+ member&.update_data(data['member'])
1396
+ end
1397
+
1334
1398
  event = MessageEditEvent.new(message, self)
1335
1399
  raise_event(event)
1336
1400
  when :MESSAGE_DELETE
@@ -1368,6 +1432,12 @@ module Discordrb
1368
1432
 
1369
1433
  return if profile.id == data['user_id'].to_i && !should_parse_self
1370
1434
 
1435
+ if data['member']
1436
+ server = self.server(data['guild_id'].to_i)
1437
+
1438
+ server&.cache_member(Member.new(data['member'], server, self))
1439
+ end
1440
+
1371
1441
  event = ReactionAddEvent.new(data, self)
1372
1442
  raise_event(event)
1373
1443
  when :MESSAGE_REACTION_REMOVE
@@ -1442,6 +1512,12 @@ module Discordrb
1442
1512
  remove_recipient(data)
1443
1513
 
1444
1514
  event = ChannelRecipientRemoveEvent.new(data, self)
1515
+ raise_event(event)
1516
+ when :CHANNEL_PINS_UPDATE
1517
+ event = ChannelPinsUpdateEvent.new(data, self)
1518
+
1519
+ event.channel.process_last_pin_timestamp(data['last_pin_timestamp']) if data.key?('last_pin_timestamp')
1520
+
1445
1521
  raise_event(event)
1446
1522
  when :GUILD_MEMBER_ADD
1447
1523
  add_guild_member(data)
@@ -1544,6 +1620,10 @@ module Discordrb
1544
1620
  event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1545
1621
  raise_event(event)
1546
1622
  end
1623
+ when :APPLICATION_COMMAND_PERMISSIONS_UPDATE
1624
+ event = ApplicationCommandPermissionsUpdateEvent.new(data, self)
1625
+
1626
+ raise_event(event)
1547
1627
  when :INTERACTION_CREATE
1548
1628
  event = InteractionCreateEvent.new(data, self)
1549
1629
  raise_event(event)
@@ -1552,13 +1632,13 @@ module Discordrb
1552
1632
  when Interaction::TYPES[:command]
1553
1633
  event = ApplicationCommandEvent.new(data, self)
1554
1634
 
1555
- Thread.new do
1556
- Thread.current[:discordrb_name] = "it-#{event.interaction.id}"
1635
+ Thread.new(event) do |evt|
1636
+ Thread.current[:discordrb_name] = "it-#{evt.interaction.id}"
1557
1637
 
1558
1638
  begin
1559
- debug("Executing application command #{event.command_name}:#{event.command_id}")
1639
+ debug("Executing application command #{evt.command_name}:#{evt.command_id}")
1560
1640
 
1561
- @application_commands[event.command_name]&.call(event)
1641
+ @application_commands[evt.command_name]&.call(evt)
1562
1642
  rescue StandardError => e
1563
1643
  log_exception(e)
1564
1644
  end
@@ -1594,6 +1674,10 @@ module Discordrb
1594
1674
 
1595
1675
  event = ModalSubmitEvent.new(data, self)
1596
1676
  raise_event(event)
1677
+ when Interaction::TYPES[:autocomplete]
1678
+
1679
+ event = AutocompleteEvent.new(data, self)
1680
+ raise_event(event)
1597
1681
  end
1598
1682
  when :WEBHOOKS_UPDATE
1599
1683
  event = WebhookUpdateEvent.new(data, self)
@@ -1673,15 +1757,15 @@ module Discordrb
1673
1757
  end
1674
1758
 
1675
1759
  def call_event(handler, event)
1676
- t = Thread.new do
1760
+ t = Thread.new(event) do |evt|
1677
1761
  @event_threads ||= []
1678
1762
  @current_thread ||= 0
1679
1763
 
1680
1764
  @event_threads << t
1681
1765
  Thread.current[:discordrb_name] = "et-#{@current_thread += 1}"
1682
1766
  begin
1683
- handler.call(event)
1684
- handler.after_call(event)
1767
+ handler.call(evt)
1768
+ handler.after_call(evt)
1685
1769
  rescue StandardError => e
1686
1770
  log_exception(e)
1687
1771
  ensure
@@ -1692,7 +1776,7 @@ module Discordrb
1692
1776
 
1693
1777
  def handle_awaits(event)
1694
1778
  @awaits ||= {}
1695
- @awaits.each do |_, await|
1779
+ @awaits.each_value do |await|
1696
1780
  key, should_delete = await.match(event)
1697
1781
  next unless key
1698
1782
 
@@ -1705,6 +1789,8 @@ module Discordrb
1705
1789
  end
1706
1790
 
1707
1791
  def calculate_intents(intents)
1792
+ intents = [intents] unless intents.is_a? Array
1793
+
1708
1794
  intents.reduce(0) do |sum, intent|
1709
1795
  case intent
1710
1796
  when Symbol
@@ -22,6 +22,7 @@ module Discordrb
22
22
  @channels = {}
23
23
  @pm_channels = {}
24
24
  @thread_members = {}
25
+ @server_previews = {}
25
26
  end
26
27
 
27
28
  # Returns or caches the available voice regions
@@ -134,6 +135,19 @@ module Discordrb
134
135
 
135
136
  alias_method :private_channel, :pm_channel
136
137
 
138
+ # Get a server preview. If the bot isn't a member of the server, the server must be discoverable.
139
+ # @param id [Integer, String, Server] the ID of the server preview to get.
140
+ # @return [ServerPreview, nil] the server preview, or `nil` if the server isn't accessible.
141
+ def server_preview(id)
142
+ id = id.resolve_id
143
+ return @server_previews[id] if @server_previews[id]
144
+
145
+ response = JSON.parse(API::Server.preview(token, id))
146
+ @server_previews[id] = ServerPreview.new(response, self)
147
+ rescue StandardError
148
+ nil
149
+ end
150
+
137
151
  # Ensures a given user object is cached and if not, cache it from the given data hash.
138
152
  # @param data [Hash] A data hash representing a user.
139
153
  # @return [User] the user represented by the data hash.
@@ -199,7 +213,7 @@ module Discordrb
199
213
  # @return [String] Only the code for the invite.
200
214
  def resolve_invite_code(invite)
201
215
  invite = invite.code if invite.is_a? Discordrb::Invite
202
- invite = invite[invite.rindex('/') + 1..] if invite.start_with?('http', 'discord.gg')
216
+ invite = invite[(invite.rindex('/') + 1)..] if invite.start_with?('http', 'discord.gg')
203
217
  invite
204
218
  end
205
219
 
@@ -109,7 +109,7 @@ module Discordrb::Commands
109
109
  spaces_allowed: attributes[:spaces_allowed].nil? ? false : attributes[:spaces_allowed],
110
110
 
111
111
  # Webhooks allowed to trigger commands
112
- webhook_commands: attributes[:webhook_commands].nil? ? true : attributes[:webhook_commands],
112
+ webhook_commands: attributes[:webhook_commands].nil? || attributes[:webhook_commands],
113
113
 
114
114
  channels: attributes[:channels] || [],
115
115
 
@@ -153,7 +153,9 @@ module Discordrb::Commands
153
153
  command = command.aliased_command
154
154
  command_name = command.name
155
155
  end
156
+ # rubocop:disable Lint/ReturnInVoidContext
156
157
  return "The command `#{command_name}` does not exist!" unless command
158
+ # rubocop:enable Lint/ReturnInVoidContext
157
159
 
158
160
  desc = command.attributes[:description] || '*No description available*'
159
161
  usage = command.attributes[:usage]
@@ -257,24 +259,16 @@ module Discordrb::Commands
257
259
  next arg if types[i].nil? || types[i] == String
258
260
 
259
261
  if types[i] == Integer
260
- begin
261
- Integer(arg, 10)
262
- rescue ArgumentError
263
- nil
264
- end
262
+ Integer(arg, 10, exception: false)
265
263
  elsif types[i] == Float
266
- begin
267
- Float(arg)
268
- rescue ArgumentError
269
- nil
270
- end
264
+ Float(arg, exception: false)
271
265
  elsif types[i] == Time
272
266
  begin
273
267
  Time.parse arg
274
268
  rescue ArgumentError
275
269
  nil
276
270
  end
277
- elsif types[i] == TrueClass || types[i] == FalseClass
271
+ elsif [TrueClass, FalseClass].include?(types[i])
278
272
  if arg.casecmp('true').zero? || arg.downcase.start_with?('y')
279
273
  true
280
274
  elsif arg.casecmp('false').zero? || arg.downcase.start_with?('n')
@@ -295,11 +289,7 @@ module Discordrb::Commands
295
289
  nil
296
290
  end
297
291
  elsif types[i] == Rational
298
- begin
299
- Rational(arg)
300
- rescue ArgumentError
301
- nil
302
- end
292
+ Rational(arg, exception: false)
303
293
  elsif types[i] == Range
304
294
  begin
305
295
  if arg.include? '...'
@@ -32,10 +32,10 @@ module Discordrb::Commands
32
32
  channels: attributes[:channels] || nil,
33
33
 
34
34
  # Whether this command is usable in a command chain
35
- chain_usable: attributes[:chain_usable].nil? ? true : attributes[:chain_usable],
35
+ chain_usable: attributes[:chain_usable].nil? || attributes[:chain_usable],
36
36
 
37
37
  # Whether this command should show up in the help command
38
- help_available: attributes[:help_available].nil? ? true : attributes[:help_available],
38
+ help_available: attributes[:help_available].nil? || attributes[:help_available],
39
39
 
40
40
  # Description (for help command)
41
41
  description: attributes[:description] || nil,
@@ -159,7 +159,7 @@ module Discordrb::Commands
159
159
  escaped = false
160
160
  hacky_delim, hacky_space, hacky_prev, hacky_newline = [0xe001, 0xe002, 0xe003, 0xe004].pack('U*').chars
161
161
 
162
- @chain.each_char.each_with_index do |char, index|
162
+ @chain.each_char.with_index do |char, index|
163
163
  # Escape character
164
164
  if char == '\\' && !escaped
165
165
  escaped = true
@@ -211,7 +211,7 @@ module Discordrb::Commands
211
211
  b_level -= 1
212
212
  next unless b_level.zero?
213
213
 
214
- nested = @chain[b_start + 1..index - 1]
214
+ nested = @chain[(b_start + 1)..(index - 1)]
215
215
  subchain = CommandChain.new(nested, @bot, true)
216
216
  result += subchain.execute(event)
217
217
  end
@@ -245,8 +245,8 @@ module Discordrb::Commands
245
245
  command = command.gsub hacky_delim, @attributes[:chain_delimiter]
246
246
 
247
247
  first_space = command.index ' '
248
- command_name = first_space ? command[0..first_space - 1] : command
249
- arguments = first_space ? command[first_space + 1..] : ''
248
+ command_name = first_space ? command[0..(first_space - 1)] : command
249
+ arguments = first_space ? command[(first_space + 1)..] : ''
250
250
 
251
251
  # Append a previous sign if none is present
252
252
  arguments += @attributes[:previous] unless arguments.include? @attributes[:previous]
@@ -318,7 +318,7 @@ module Discordrb::Commands
318
318
  arg.split ' '
319
319
  end
320
320
 
321
- chain = chain[chain_args_index + 1..]
321
+ chain = chain[(chain_args_index + 1)..]
322
322
  end
323
323
 
324
324
  [chain_args, chain]