discordrb 3.4.3 → 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 (95) 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/ISSUE_TEMPLATE/bug_report.md +0 -1
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  7. data/.github/workflows/ci.yml +78 -0
  8. data/.github/workflows/codeql.yml +65 -0
  9. data/.github/workflows/deploy.yml +54 -0
  10. data/.github/workflows/release.yml +45 -0
  11. data/.markdownlint.json +4 -0
  12. data/.rubocop.yml +58 -2
  13. data/CHANGELOG.md +485 -225
  14. data/LICENSE.txt +1 -1
  15. data/README.md +38 -26
  16. data/discordrb-webhooks.gemspec +4 -1
  17. data/discordrb.gemspec +18 -10
  18. data/lib/discordrb/api/application.rb +278 -0
  19. data/lib/discordrb/api/channel.rb +222 -18
  20. data/lib/discordrb/api/interaction.rb +63 -0
  21. data/lib/discordrb/api/invite.rb +2 -2
  22. data/lib/discordrb/api/server.rb +123 -66
  23. data/lib/discordrb/api/user.rb +20 -5
  24. data/lib/discordrb/api/webhook.rb +72 -0
  25. data/lib/discordrb/api.rb +35 -25
  26. data/lib/discordrb/bot.rb +437 -66
  27. data/lib/discordrb/cache.rb +41 -22
  28. data/lib/discordrb/commands/command_bot.rb +13 -21
  29. data/lib/discordrb/commands/container.rb +1 -1
  30. data/lib/discordrb/commands/parser.rb +7 -7
  31. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  32. data/lib/discordrb/container.rb +178 -3
  33. data/lib/discordrb/data/activity.rb +1 -1
  34. data/lib/discordrb/data/application.rb +1 -0
  35. data/lib/discordrb/data/attachment.rb +38 -3
  36. data/lib/discordrb/data/audit_logs.rb +3 -3
  37. data/lib/discordrb/data/avatar_decoration.rb +26 -0
  38. data/lib/discordrb/data/call.rb +22 -0
  39. data/lib/discordrb/data/channel.rb +299 -30
  40. data/lib/discordrb/data/collectibles.rb +45 -0
  41. data/lib/discordrb/data/component.rb +229 -0
  42. data/lib/discordrb/data/embed.rb +10 -3
  43. data/lib/discordrb/data/emoji.rb +20 -1
  44. data/lib/discordrb/data/integration.rb +45 -3
  45. data/lib/discordrb/data/interaction.rb +937 -0
  46. data/lib/discordrb/data/invite.rb +1 -1
  47. data/lib/discordrb/data/member.rb +236 -44
  48. data/lib/discordrb/data/message.rb +278 -51
  49. data/lib/discordrb/data/overwrite.rb +15 -7
  50. data/lib/discordrb/data/primary_server.rb +60 -0
  51. data/lib/discordrb/data/profile.rb +2 -7
  52. data/lib/discordrb/data/reaction.rb +2 -1
  53. data/lib/discordrb/data/recipient.rb +1 -1
  54. data/lib/discordrb/data/role.rb +204 -18
  55. data/lib/discordrb/data/server.rb +194 -118
  56. data/lib/discordrb/data/server_preview.rb +68 -0
  57. data/lib/discordrb/data/snapshot.rb +110 -0
  58. data/lib/discordrb/data/user.rb +132 -12
  59. data/lib/discordrb/data/voice_region.rb +1 -0
  60. data/lib/discordrb/data/webhook.rb +99 -9
  61. data/lib/discordrb/data.rb +9 -0
  62. data/lib/discordrb/errors.rb +47 -3
  63. data/lib/discordrb/events/await.rb +1 -1
  64. data/lib/discordrb/events/channels.rb +38 -1
  65. data/lib/discordrb/events/generic.rb +2 -0
  66. data/lib/discordrb/events/guilds.rb +6 -1
  67. data/lib/discordrb/events/interactions.rb +575 -0
  68. data/lib/discordrb/events/invites.rb +2 -0
  69. data/lib/discordrb/events/members.rb +19 -2
  70. data/lib/discordrb/events/message.rb +42 -8
  71. data/lib/discordrb/events/presence.rb +23 -14
  72. data/lib/discordrb/events/raw.rb +1 -0
  73. data/lib/discordrb/events/reactions.rb +2 -1
  74. data/lib/discordrb/events/roles.rb +2 -0
  75. data/lib/discordrb/events/threads.rb +100 -0
  76. data/lib/discordrb/events/typing.rb +1 -0
  77. data/lib/discordrb/events/voice_server_update.rb +1 -0
  78. data/lib/discordrb/events/voice_state_update.rb +1 -0
  79. data/lib/discordrb/events/webhooks.rb +1 -0
  80. data/lib/discordrb/gateway.rb +57 -28
  81. data/lib/discordrb/paginator.rb +3 -3
  82. data/lib/discordrb/permissions.rb +71 -35
  83. data/lib/discordrb/version.rb +1 -1
  84. data/lib/discordrb/voice/encoder.rb +2 -2
  85. data/lib/discordrb/voice/network.rb +18 -7
  86. data/lib/discordrb/voice/sodium.rb +3 -1
  87. data/lib/discordrb/voice/voice_bot.rb +3 -3
  88. data/lib/discordrb/webhooks.rb +2 -0
  89. data/lib/discordrb/websocket.rb +0 -10
  90. data/lib/discordrb.rb +54 -5
  91. metadata +87 -25
  92. data/.circleci/config.yml +0 -126
  93. data/.codeclimate.yml +0 -16
  94. data/.travis.yml +0 -32
  95. data/bin/travis_build_docs.sh +0 -17
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'
@@ -19,11 +18,16 @@ require 'discordrb/events/raw'
19
18
  require 'discordrb/events/reactions'
20
19
  require 'discordrb/events/webhooks'
21
20
  require 'discordrb/events/invites'
21
+ require 'discordrb/events/interactions'
22
+ require 'discordrb/events/threads'
22
23
 
23
24
  require 'discordrb/api'
24
25
  require 'discordrb/api/channel'
25
26
  require 'discordrb/api/server'
26
27
  require 'discordrb/api/invite'
28
+ require 'discordrb/api/interaction'
29
+ require 'discordrb/api/application'
30
+
27
31
  require 'discordrb/errors'
28
32
  require 'discordrb/data'
29
33
  require 'discordrb/await'
@@ -93,23 +97,26 @@ module Discordrb
93
97
  # @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
94
98
  # unless you really need this so you don't inadvertently create infinite loops.
95
99
  # @param shard_id [Integer] The number of the shard this bot should handle. See
96
- # https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
100
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
97
101
  # @param num_shards [Integer] The total number of shards that should be running. See
98
- # https://github.com/discordapp/discord-api-docs/issues/17 for how to do sharding.
102
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
99
103
  # @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
100
104
  # @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
101
105
  # @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
102
106
  # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
103
107
  # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
104
108
  # that all data be received in a continuous compressed stream.
105
- # @param intents [:all, Array<Symbol>, nil] Intents that this bot requires. See {Discordrb::INTENTS}. If `nil`, no intents
106
- # field will be passed.
109
+ # @param intents [:all, :unprivileged, Array<Symbol>, :none, Integer] Gateway intents that this bot requires. `:all` will
110
+ # request all intents. `:unprivileged` will request only intents that are not defined as "Privileged". `:none`
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
+ # @see Discordrb::INTENTS
107
114
  def initialize(
108
115
  log_mode: :normal,
109
116
  token: nil, client_id: nil,
110
117
  type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
111
118
  shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
112
- compress_mode: :large, intents: nil
119
+ compress_mode: :large, intents: :all
113
120
  )
114
121
  LOGGER.mode = log_mode
115
122
  LOGGER.token = token if redact_token
@@ -130,7 +137,16 @@ module Discordrb
130
137
 
131
138
  raise 'Token string is empty or nil' if token.nil? || token.empty?
132
139
 
133
- @intents = intents == :all ? INTENTS.values.reduce(&:|) : calculate_intents(intents) if intents
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
134
150
 
135
151
  @token = process_token(@type, token)
136
152
  @gateway = Gateway.new(self, @token, @shard_key, @compress_mode, @intents)
@@ -147,6 +163,8 @@ module Discordrb
147
163
  @current_thread = 0
148
164
 
149
165
  @status = :online
166
+
167
+ @application_commands = {}
150
168
  end
151
169
 
152
170
  # The list of users the bot shares a server with.
@@ -165,6 +183,14 @@ module Discordrb
165
183
  @servers
166
184
  end
167
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
+
168
194
  # @overload emoji(id)
169
195
  # Return an emoji by its ID
170
196
  # @param id [String, Integer] The emoji's ID.
@@ -197,8 +223,10 @@ module Discordrb
197
223
  # to edit user data like the current username (see {Profile#username=}).
198
224
  # @return [Profile] The bot's profile that can be used to edit data.
199
225
  def profile
200
- gateway_check
201
- @profile
226
+ return @profile if @profile
227
+
228
+ response = Discordrb::API::User.profile(@token)
229
+ @profile = Profile.new(JSON.parse(response), self)
202
230
  end
203
231
 
204
232
  alias_method :bot_user, :profile
@@ -279,13 +307,21 @@ module Discordrb
279
307
  # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
280
308
  # @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
281
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.
282
312
  # @return [String] the OAuth invite URL.
283
- def invite_url(server: nil, permission_bits: nil)
313
+ def invite_url(server: nil, permission_bits: nil, redirect_uri: nil, scopes: ['bot'])
284
314
  @client_id ||= bot_application.id
285
315
 
286
- server_id_str = server ? "&guild_id=#{server.id}" : ''
287
- permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
288
- "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}"
289
325
  end
290
326
 
291
327
  # @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
@@ -362,17 +398,22 @@ module Discordrb
362
398
  # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
363
399
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
364
400
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
365
- # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
401
+ # @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
366
402
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
367
- # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
403
+ # @param message_reference [Message, String, Integer, Hash, nil] The message, or message ID, to reply to if any.
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.
368
408
  # @return [Message] The message that was sent.
369
- def send_message(channel, content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = 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)
370
410
  channel = channel.resolve_id
371
411
  debug("Sending message to #{channel} with content '#{content}'")
372
412
  allowed_mentions = { parse: [] } if allowed_mentions == false
373
- message_reference = { message_id: message_reference.id } if message_reference
413
+ message_reference = { message_id: message_reference.resolve_id } if message_reference.respond_to?(:resolve_id)
414
+ embeds = (embeds.instance_of?(Array) ? embeds.map(&:to_hash) : [embeds&.to_hash]).compact
374
415
 
375
- response = API::Channel.create_message(token, channel, content, tts, embed&.to_hash, nil, attachments, allowed_mentions&.to_hash, message_reference)
416
+ response = API::Channel.create_message(token, channel, content, tts, embeds, nonce, attachments, allowed_mentions&.to_hash, message_reference, components, flags, enforce_nonce)
376
417
  Message.new(JSON.parse(response), self)
377
418
  end
378
419
 
@@ -382,15 +423,19 @@ module Discordrb
382
423
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
383
424
  # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
384
425
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
385
- # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
426
+ # @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
386
427
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
387
428
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
388
429
  # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
389
- def send_temporary_message(channel, content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
430
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
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)
390
435
  Thread.new do
391
436
  Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
392
437
 
393
- message = send_message(channel, content, tts, embed, attachments, allowed_mentions, message_reference)
438
+ message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce)
394
439
  sleep(timeout)
395
440
  message.delete
396
441
  end
@@ -416,6 +461,7 @@ module Discordrb
416
461
  end
417
462
  # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
418
463
  file.define_singleton_method(:original_filename) { filename } if filename
464
+ file.define_singleton_method(:path) { filename } if filename
419
465
  end
420
466
 
421
467
  channel = channel.resolve_id
@@ -423,20 +469,6 @@ module Discordrb
423
469
  Message.new(JSON.parse(response), self)
424
470
  end
425
471
 
426
- # Creates a server on Discord with a specified name and a region.
427
- # @note Discord's API doesn't directly return the server when creating it, so this method
428
- # waits until the data has been received via the websocket. This may make the execution take a while.
429
- # @param name [String] The name the new server should have. Doesn't have to be alphanumeric.
430
- # @param region [Symbol] The region where the server should be created, for example 'eu-central' or 'hongkong'.
431
- # @return [Server] The server that was created.
432
- def create_server(name, region = :'eu-central')
433
- response = API::Server.create(token, name, region)
434
- id = JSON.parse(response)['id'].to_i
435
- sleep 0.1 until (server = @servers[id])
436
- debug "Successfully created server #{server.id} with name #{server.name}"
437
- server
438
- end
439
-
440
472
  # Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using
441
473
  # Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
442
474
  # @param name [String] What your application should be called.
@@ -485,7 +517,7 @@ module Discordrb
485
517
  end
486
518
  end
487
519
  elsif /(?<animated>^a|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
488
- array_to_return << (emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil))
520
+ array_to_return << (emoji(id) || Emoji.new({ 'animated' => animated != '', 'name' => name, 'id' => id }, self, nil))
489
521
  end
490
522
  end
491
523
  array_to_return
@@ -595,6 +627,36 @@ module Discordrb
595
627
  update_status(:invisible, @activity, nil)
596
628
  end
597
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
+
598
660
  # Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
599
661
  def debug=(new_debug)
600
662
  LOGGER.debug = new_debug
@@ -721,6 +783,158 @@ module Discordrb
721
783
  end
722
784
  end
723
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, default_member_permissions: nil, contexts: nil, nsfw: false)
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, default_member_permissions, contexts, nsfw)
836
+ else
837
+ API::Application.create_global_command(@token, profile.id, name, description, builder.to_a, default_permission, type, default_member_permissions, contexts, nsfw)
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, default_member_permissions: nil, contexts: nil, nsfw: nil)
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, default_member_permissions, contexts, nsfw)
862
+ else
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
+ 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
+
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
+
724
938
  private
725
939
 
726
940
  # Throws a useful exception if there's currently no gateway connection.
@@ -772,10 +986,16 @@ module Discordrb
772
986
 
773
987
  username = data['user']['username']
774
988
  if username && !member_is_new # Don't set the username for newly-cached members
775
- debug "Implicitly updating presence-obtained information for member #{user_id}"
989
+ debug "Implicitly updating presence-obtained information username for member #{user_id}"
776
990
  member.update_username(username)
777
991
  end
778
992
 
993
+ global_name = data['user']['global_name']
994
+ if global_name && !member_is_new # Don't set the global_name for newly-cached members
995
+ debug "Implicitly updating presence-obtained information global_name for member #{user_id}"
996
+ member.update_global_name(global_name)
997
+ end
998
+
779
999
  member.update_presence(data)
780
1000
 
781
1001
  member.avatar_id = data['user']['avatar'] if data['user']['avatar']
@@ -836,14 +1056,14 @@ module Discordrb
836
1056
 
837
1057
  # Internal handler for CHANNEL_CREATE
838
1058
  def create_channel(data)
839
- channel = Channel.new(data, self)
1059
+ channel = data.is_a?(Discordrb::Channel) ? data : Channel.new(data, self)
840
1060
  server = channel.server
841
1061
 
842
1062
  # Handle normal and private channels separately
843
1063
  if server
844
1064
  server.add_channel(channel)
845
1065
  @channels[channel.id] = channel
846
- elsif channel.pm?
1066
+ elsif channel.private?
847
1067
  @pm_channels[channel.recipient.id] = channel
848
1068
  elsif channel.group?
849
1069
  @channels[channel.id] = channel
@@ -873,6 +1093,8 @@ module Discordrb
873
1093
  elsif channel.group?
874
1094
  @channels.delete(channel.id)
875
1095
  end
1096
+
1097
+ @thread_members.delete(channel.id) if channel.thread?
876
1098
  end
877
1099
 
878
1100
  # Internal handler for CHANNEL_RECIPIENT_ADD
@@ -909,10 +1131,12 @@ module Discordrb
909
1131
  server_id = data['guild_id'].to_i
910
1132
  server = self.server(server_id)
911
1133
 
912
- member = server.member(data['user']['id'].to_i)
913
- member.update_roles(data['roles'])
914
- member.update_nick(data['nick'])
915
- member.update_boosting_since(data['premium_since'])
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
916
1140
  end
917
1141
 
918
1142
  # Internal handler for GUILD_MEMBER_DELETE
@@ -929,7 +1153,7 @@ module Discordrb
929
1153
 
930
1154
  # Internal handler for GUILD_CREATE
931
1155
  def create_guild(data)
932
- ensure_server(data)
1156
+ ensure_server(data, true)
933
1157
  end
934
1158
 
935
1159
  # Internal handler for GUILD_UPDATE
@@ -1020,7 +1244,7 @@ module Discordrb
1020
1244
 
1021
1245
  def process_token(type, token)
1022
1246
  # Remove the "Bot " prefix if it exists
1023
- token = token[4..-1] if token.start_with? 'Bot '
1247
+ token = token[4..] if token.start_with? 'Bot '
1024
1248
 
1025
1249
  token = "Bot #{token}" unless type == :user
1026
1250
  token
@@ -1028,7 +1252,7 @@ module Discordrb
1028
1252
 
1029
1253
  def handle_dispatch(type, data)
1030
1254
  # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
1031
- 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])
1032
1256
  # The server streaming timed out!
1033
1257
  LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
1034
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.')
@@ -1048,6 +1272,8 @@ module Discordrb
1048
1272
 
1049
1273
  @profile = Profile.new(data['user'], self)
1050
1274
 
1275
+ @client_id ||= data['application']['id']&.to_i
1276
+
1051
1277
  # Initialize servers
1052
1278
  @servers = {}
1053
1279
 
@@ -1057,14 +1283,14 @@ module Discordrb
1057
1283
  data['guilds'].each do |element|
1058
1284
  # Check for true specifically because unavailable=false indicates that a previously unavailable server has
1059
1285
  # come online
1060
- if element['unavailable'].is_a? TrueClass
1286
+ if element['unavailable']
1061
1287
  @unavailable_servers += 1
1062
1288
 
1063
1289
  # Ignore any unavailable servers
1064
1290
  next
1065
1291
  end
1066
1292
 
1067
- ensure_server(element)
1293
+ ensure_server(element, true)
1068
1294
  end
1069
1295
 
1070
1296
  # Add PM and group channels
@@ -1089,14 +1315,16 @@ module Discordrb
1089
1315
  when :GUILD_MEMBERS_CHUNK
1090
1316
  id = data['guild_id'].to_i
1091
1317
  server = server(id)
1092
- server.process_chunk(data['members'])
1318
+ server.process_chunk(data['members'], data['chunk_index'], data['chunk_count'])
1319
+ when :USER_UPDATE
1320
+ @profile = Profile.new(data, self)
1093
1321
  when :INVITE_CREATE
1094
1322
  invite = Invite.new(data, self)
1095
1323
  raise_event(InviteCreateEvent.new(data, invite, self))
1096
1324
  when :INVITE_DELETE
1097
1325
  raise_event(InviteDeleteEvent.new(data, self))
1098
1326
  when :MESSAGE_CREATE
1099
- if ignored?(data['author']['id'].to_i)
1327
+ if ignored?(data['author']['id'])
1100
1328
  debug("Ignored author with ID #{data['author']['id']}")
1101
1329
  return
1102
1330
  end
@@ -1106,12 +1334,29 @@ module Discordrb
1106
1334
  return
1107
1335
  end
1108
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
+
1109
1342
  # If create_message is overwritten with a method that returns the parsed message, use that instead, so we don't
1110
1343
  # parse the message twice (which is just thrown away performance)
1111
1344
  message = create_message(data)
1112
1345
  message = Message.new(data, self) unless message.is_a? Message
1113
1346
 
1114
- 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
1353
+
1354
+ # Dispatch a ChannelCreateEvent for channels we don't have cached
1355
+ if message.channel.private? && @pm_channels[message.channel.recipient.id].nil?
1356
+ create_channel(message.channel)
1357
+
1358
+ raise_event(ChannelCreateEvent.new(message.channel, self))
1359
+ end
1115
1360
 
1116
1361
  event = MessageEvent.new(message, self)
1117
1362
  raise_event(event)
@@ -1128,18 +1373,28 @@ module Discordrb
1128
1373
  when :MESSAGE_UPDATE
1129
1374
  update_message(data)
1130
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
+
1131
1381
  message = Message.new(data, self)
1132
1382
 
1133
1383
  event = MessageUpdateEvent.new(message, self)
1134
1384
  raise_event(event)
1135
1385
 
1136
- return if message.from_bot? && !should_parse_self
1137
-
1138
- unless message.author
1386
+ if data['author'].nil?
1139
1387
  LOGGER.debug("Edited a message with nil author! Content: #{message.content.inspect}, channel: #{message.channel.inspect}")
1140
1388
  return
1141
1389
  end
1142
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
+
1143
1398
  event = MessageEditEvent.new(message, self)
1144
1399
  raise_event(event)
1145
1400
  when :MESSAGE_DELETE
@@ -1177,6 +1432,12 @@ module Discordrb
1177
1432
 
1178
1433
  return if profile.id == data['user_id'].to_i && !should_parse_self
1179
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
+
1180
1441
  event = ReactionAddEvent.new(data, self)
1181
1442
  raise_event(event)
1182
1443
  when :MESSAGE_REACTION_REMOVE
@@ -1195,18 +1456,28 @@ module Discordrb
1195
1456
  # Ignore friends list presences
1196
1457
  return unless data['guild_id']
1197
1458
 
1198
- now_playing = data['game'].nil? ? nil : data['game']['name']
1459
+ new_activities = (data['activities'] || []).map { |act_data| Activity.new(act_data, self) }
1199
1460
  presence_user = @users[data['user']['id'].to_i]
1200
- played_before = presence_user.nil? ? nil : presence_user.game
1461
+ old_activities = (presence_user&.activities || [])
1201
1462
  update_presence(data)
1202
1463
 
1203
- event = if now_playing == played_before
1204
- PresenceEvent.new(data, self)
1205
- else
1206
- PlayingEvent.new(data, self)
1207
- end
1464
+ # Starting a new game
1465
+ playing_change = new_activities.reject do |act|
1466
+ old_activities.find { |old| old.name == act.name }
1467
+ end
1468
+
1469
+ # Exiting an existing game
1470
+ playing_change += old_activities.reject do |old|
1471
+ new_activities.find { |act| act.name == old.name }
1472
+ end
1208
1473
 
1209
- raise_event(event)
1474
+ if playing_change.any?
1475
+ playing_change.each do |act|
1476
+ raise_event(PlayingEvent.new(data, act, self))
1477
+ end
1478
+ else
1479
+ raise_event(PresenceEvent.new(data, self))
1480
+ end
1210
1481
  when :VOICE_STATE_UPDATE
1211
1482
  old_channel_id = update_voice_state(data)
1212
1483
 
@@ -1241,6 +1512,12 @@ module Discordrb
1241
1512
  remove_recipient(data)
1242
1513
 
1243
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
+
1244
1521
  raise_event(event)
1245
1522
  when :GUILD_MEMBER_ADD
1246
1523
  add_guild_member(data)
@@ -1343,9 +1620,101 @@ module Discordrb
1343
1620
  event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1344
1621
  raise_event(event)
1345
1622
  end
1623
+ when :APPLICATION_COMMAND_PERMISSIONS_UPDATE
1624
+ event = ApplicationCommandPermissionsUpdateEvent.new(data, self)
1625
+
1626
+ raise_event(event)
1627
+ when :INTERACTION_CREATE
1628
+ event = InteractionCreateEvent.new(data, self)
1629
+ raise_event(event)
1630
+
1631
+ case data['type']
1632
+ when Interaction::TYPES[:command]
1633
+ event = ApplicationCommandEvent.new(data, self)
1634
+
1635
+ Thread.new(event) do |evt|
1636
+ Thread.current[:discordrb_name] = "it-#{evt.interaction.id}"
1637
+
1638
+ begin
1639
+ debug("Executing application command #{evt.command_name}:#{evt.command_id}")
1640
+
1641
+ @application_commands[evt.command_name]&.call(evt)
1642
+ rescue StandardError => e
1643
+ log_exception(e)
1644
+ end
1645
+ end
1646
+ when Interaction::TYPES[:component]
1647
+ case data['data']['component_type']
1648
+ when Webhooks::View::COMPONENT_TYPES[:button]
1649
+ event = ButtonEvent.new(data, self)
1650
+
1651
+ raise_event(event)
1652
+ when Webhooks::View::COMPONENT_TYPES[:string_select]
1653
+ event = StringSelectEvent.new(data, self)
1654
+
1655
+ raise_event(event)
1656
+ when Webhooks::View::COMPONENT_TYPES[:user_select]
1657
+ event = UserSelectEvent.new(data, self)
1658
+
1659
+ raise_event(event)
1660
+ when Webhooks::View::COMPONENT_TYPES[:role_select]
1661
+ event = RoleSelectEvent.new(data, self)
1662
+
1663
+ raise_event(event)
1664
+ when Webhooks::View::COMPONENT_TYPES[:mentionable_select]
1665
+ event = MentionableSelectEvent.new(data, self)
1666
+
1667
+ raise_event(event)
1668
+ when Webhooks::View::COMPONENT_TYPES[:channel_select]
1669
+ event = ChannelSelectEvent.new(data, self)
1670
+
1671
+ raise_event(event)
1672
+ end
1673
+ when Interaction::TYPES[:modal_submit]
1674
+
1675
+ event = ModalSubmitEvent.new(data, self)
1676
+ raise_event(event)
1677
+ when Interaction::TYPES[:autocomplete]
1678
+
1679
+ event = AutocompleteEvent.new(data, self)
1680
+ raise_event(event)
1681
+ end
1346
1682
  when :WEBHOOKS_UPDATE
1347
1683
  event = WebhookUpdateEvent.new(data, self)
1348
1684
  raise_event(event)
1685
+ when :THREAD_CREATE
1686
+ create_channel(data)
1687
+
1688
+ event = ThreadCreateEvent.new(data, self)
1689
+ raise_event(event)
1690
+ when :THREAD_UPDATE
1691
+ update_channel(data)
1692
+
1693
+ event = ThreadUpdateEvent.new(data, self)
1694
+ raise_event(event)
1695
+ when :THREAD_DELETE
1696
+ delete_channel(data)
1697
+ @thread_members.delete(data['id']&.resolve_id)
1698
+
1699
+ # raise ThreadDeleteEvent
1700
+ when :THREAD_LIST_SYNC
1701
+ data['members'].map { |member| ensure_thread_member(member) }
1702
+ data['threads'].map { |channel| ensure_channel(channel, data['guild_id']) }
1703
+
1704
+ # raise ThreadListSyncEvent?
1705
+ when :THREAD_MEMBER_UPDATE
1706
+ ensure_thread_member(data)
1707
+ when :THREAD_MEMBERS_UPDATE
1708
+ data['added_members']&.each do |added_member|
1709
+ ensure_thread_member(added_member) if added_member['user_id']
1710
+ end
1711
+
1712
+ data['removed_member_ids']&.each do |member_id|
1713
+ @thread_members[data['id']&.resolve_id]&.delete(member_id&.resolve_id)
1714
+ end
1715
+
1716
+ event = ThreadMembersUpdateEvent.new(data, self)
1717
+ raise_event(event)
1349
1718
  else
1350
1719
  # another event that we don't support yet
1351
1720
  debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
@@ -1388,15 +1757,15 @@ module Discordrb
1388
1757
  end
1389
1758
 
1390
1759
  def call_event(handler, event)
1391
- t = Thread.new do
1760
+ t = Thread.new(event) do |evt|
1392
1761
  @event_threads ||= []
1393
1762
  @current_thread ||= 0
1394
1763
 
1395
1764
  @event_threads << t
1396
1765
  Thread.current[:discordrb_name] = "et-#{@current_thread += 1}"
1397
1766
  begin
1398
- handler.call(event)
1399
- handler.after_call(event)
1767
+ handler.call(evt)
1768
+ handler.after_call(evt)
1400
1769
  rescue StandardError => e
1401
1770
  log_exception(e)
1402
1771
  ensure
@@ -1407,7 +1776,7 @@ module Discordrb
1407
1776
 
1408
1777
  def handle_awaits(event)
1409
1778
  @awaits ||= {}
1410
- @awaits.each do |_, await|
1779
+ @awaits.each_value do |await|
1411
1780
  key, should_delete = await.match(event)
1412
1781
  next unless key
1413
1782
 
@@ -1420,6 +1789,8 @@ module Discordrb
1420
1789
  end
1421
1790
 
1422
1791
  def calculate_intents(intents)
1792
+ intents = [intents] unless intents.is_a? Array
1793
+
1423
1794
  intents.reduce(0) do |sum, intent|
1424
1795
  case intent
1425
1796
  when Symbol