discordrb 3.4.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +44 -18
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -1
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  5. data/.github/workflows/codeql.yml +65 -0
  6. data/.markdownlint.json +4 -0
  7. data/.rubocop.yml +8 -2
  8. data/CHANGELOG.md +419 -222
  9. data/LICENSE.txt +1 -1
  10. data/README.md +37 -25
  11. data/discordrb-webhooks.gemspec +4 -1
  12. data/discordrb.gemspec +9 -6
  13. data/lib/discordrb/api/application.rb +202 -0
  14. data/lib/discordrb/api/channel.rb +182 -11
  15. data/lib/discordrb/api/interaction.rb +54 -0
  16. data/lib/discordrb/api/invite.rb +2 -2
  17. data/lib/discordrb/api/server.rb +42 -19
  18. data/lib/discordrb/api/user.rb +9 -3
  19. data/lib/discordrb/api/webhook.rb +57 -0
  20. data/lib/discordrb/api.rb +19 -5
  21. data/lib/discordrb/bot.rb +328 -33
  22. data/lib/discordrb/cache.rb +27 -22
  23. data/lib/discordrb/commands/command_bot.rb +14 -7
  24. data/lib/discordrb/commands/container.rb +1 -1
  25. data/lib/discordrb/commands/parser.rb +2 -2
  26. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  27. data/lib/discordrb/container.rb +132 -3
  28. data/lib/discordrb/data/activity.rb +8 -1
  29. data/lib/discordrb/data/attachment.rb +15 -0
  30. data/lib/discordrb/data/audit_logs.rb +3 -3
  31. data/lib/discordrb/data/channel.rb +167 -23
  32. data/lib/discordrb/data/component.rb +229 -0
  33. data/lib/discordrb/data/integration.rb +42 -3
  34. data/lib/discordrb/data/interaction.rb +800 -0
  35. data/lib/discordrb/data/invite.rb +2 -2
  36. data/lib/discordrb/data/member.rb +108 -33
  37. data/lib/discordrb/data/message.rb +100 -20
  38. data/lib/discordrb/data/overwrite.rb +13 -7
  39. data/lib/discordrb/data/role.rb +58 -1
  40. data/lib/discordrb/data/server.rb +82 -80
  41. data/lib/discordrb/data/user.rb +69 -9
  42. data/lib/discordrb/data/webhook.rb +97 -4
  43. data/lib/discordrb/data.rb +3 -0
  44. data/lib/discordrb/errors.rb +44 -3
  45. data/lib/discordrb/events/channels.rb +1 -1
  46. data/lib/discordrb/events/interactions.rb +482 -0
  47. data/lib/discordrb/events/message.rb +9 -6
  48. data/lib/discordrb/events/presence.rb +21 -14
  49. data/lib/discordrb/events/reactions.rb +0 -1
  50. data/lib/discordrb/events/threads.rb +96 -0
  51. data/lib/discordrb/gateway.rb +30 -17
  52. data/lib/discordrb/permissions.rb +59 -34
  53. data/lib/discordrb/version.rb +1 -1
  54. data/lib/discordrb/voice/encoder.rb +13 -4
  55. data/lib/discordrb/voice/network.rb +18 -7
  56. data/lib/discordrb/voice/sodium.rb +3 -1
  57. data/lib/discordrb/voice/voice_bot.rb +3 -3
  58. data/lib/discordrb/webhooks.rb +2 -0
  59. data/lib/discordrb.rb +37 -4
  60. metadata +53 -19
  61. data/.codeclimate.yml +0 -16
  62. data/.travis.yml +0 -32
  63. data/bin/travis_build_docs.sh +0 -17
data/lib/discordrb/api.rb CHANGED
@@ -9,7 +9,7 @@ require 'discordrb/errors'
9
9
  # List of methods representing endpoints in Discord's API
10
10
  module Discordrb::API
11
11
  # The base URL of the Discord REST API.
12
- APIBASE = 'https://discord.com/api/v6'
12
+ APIBASE = 'https://discord.com/api/v9'
13
13
 
14
14
  # The URL of Discord's CDN
15
15
  CDN_URL = 'https://cdn.discordapp.com'
@@ -94,9 +94,6 @@ module Discordrb::API
94
94
  # Add a custom user agent
95
95
  attributes.last[:user_agent] = user_agent if attributes.last.is_a? Hash
96
96
 
97
- # Specify RateLimit precision
98
- attributes.last[:x_ratelimit_precision] = 'millisecond'
99
-
100
97
  # The most recent Discord rate limit requirements require the support of major parameters, where a particular route
101
98
  # and major parameter combination (*not* the HTTP method) uniquely identifies a RL bucket.
102
99
  key = [key, major_parameter].freeze
@@ -115,6 +112,15 @@ module Discordrb::API
115
112
  response = raw_request(type, attributes)
116
113
  rescue RestClient::Exception => e
117
114
  response = e.response
115
+
116
+ if response.body && !e.is_a?(RestClient::TooManyRequests)
117
+ data = JSON.parse(response.body)
118
+ err_klass = Discordrb::Errors.error_class_for(data['code'] || 0)
119
+ e = err_klass.new(data['message'], data['errors'])
120
+
121
+ Discordrb::LOGGER.error(e.full_message)
122
+ end
123
+
118
124
  raise e
119
125
  rescue Discordrb::Errors::NoPermission => e
120
126
  if e.respond_to?(:_rc_response)
@@ -137,7 +143,7 @@ module Discordrb::API
137
143
 
138
144
  unless mutex.locked?
139
145
  response = JSON.parse(e.response)
140
- wait_seconds = response['retry_after'].to_i / 1000.0
146
+ wait_seconds = response['retry_after'] ? response['retry_after'].to_f : e.response.headers[:retry_after].to_i
141
147
  Discordrb::LOGGER.ratelimit("Locking RL mutex (key: #{key}) for #{wait_seconds} seconds due to Discord rate limiting")
142
148
  trace("429 #{key.join(' ')}")
143
149
 
@@ -217,6 +223,14 @@ module Discordrb::API
217
223
  "#{cdn_url}/app-assets/#{application_id}/achievements/#{achievement_id}/icons/#{icon_hash}.#{format}"
218
224
  end
219
225
 
226
+ # @param role_id [String, Integer]
227
+ # @param icon_hash [String]
228
+ # @param format ['webp', 'png', 'jpeg']
229
+ # @return [String]
230
+ def role_icon_url(role_id, icon_hash, format = 'webp')
231
+ "#{cdn_url}/role-icons/#{role_id}/#{icon_hash}.#{format}"
232
+ end
233
+
220
234
  # Login to the server
221
235
  def login(email, password)
222
236
  request(
data/lib/discordrb/bot.rb CHANGED
@@ -19,11 +19,16 @@ require 'discordrb/events/raw'
19
19
  require 'discordrb/events/reactions'
20
20
  require 'discordrb/events/webhooks'
21
21
  require 'discordrb/events/invites'
22
+ require 'discordrb/events/interactions'
23
+ require 'discordrb/events/threads'
22
24
 
23
25
  require 'discordrb/api'
24
26
  require 'discordrb/api/channel'
25
27
  require 'discordrb/api/server'
26
28
  require 'discordrb/api/invite'
29
+ require 'discordrb/api/interaction'
30
+ require 'discordrb/api/application'
31
+
27
32
  require 'discordrb/errors'
28
33
  require 'discordrb/data'
29
34
  require 'discordrb/await'
@@ -93,23 +98,25 @@ module Discordrb
93
98
  # @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
94
99
  # unless you really need this so you don't inadvertently create infinite loops.
95
100
  # @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.
101
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
97
102
  # @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.
103
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
99
104
  # @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
100
105
  # @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
101
106
  # @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
102
107
  # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
103
108
  # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
104
109
  # 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.
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
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
@@ -362,17 +390,19 @@ module Discordrb
362
390
  # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
363
391
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
364
392
  # @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.
393
+ # @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
366
394
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
367
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.
368
397
  # @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)
398
+ def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
370
399
  channel = channel.resolve_id
371
400
  debug("Sending message to #{channel} with content '#{content}'")
372
401
  allowed_mentions = { parse: [] } if allowed_mentions == false
373
- message_reference = { message_id: message_reference.id } if message_reference
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
374
404
 
375
- response = API::Channel.create_message(token, channel, content, tts, embed&.to_hash, nil, attachments, allowed_mentions&.to_hash, message_reference)
405
+ response = API::Channel.create_message(token, channel, content, tts, embeds, nil, attachments, allowed_mentions&.to_hash, message_reference, components)
376
406
  Message.new(JSON.parse(response), self)
377
407
  end
378
408
 
@@ -382,14 +412,16 @@ module Discordrb
382
412
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
383
413
  # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
384
414
  # @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.
415
+ # @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
386
416
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
387
417
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
388
- def send_temporary_message(channel, content, timeout, tts = false, embed = nil, attachments = nil, allowed_mentions = nil)
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)
389
421
  Thread.new do
390
422
  Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
391
423
 
392
- message = send_message(channel, content, tts, embed, attachments, allowed_mentions)
424
+ message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components)
393
425
  sleep(timeout)
394
426
  message.delete
395
427
  end
@@ -415,6 +447,7 @@ module Discordrb
415
447
  end
416
448
  # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
417
449
  file.define_singleton_method(:original_filename) { filename } if filename
450
+ file.define_singleton_method(:path) { filename } if filename
418
451
  end
419
452
 
420
453
  channel = channel.resolve_id
@@ -504,7 +537,8 @@ module Discordrb
504
537
  # @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
505
538
  # @param since [Integer] When this status was set.
506
539
  # @param afk [true, false] Whether the bot is AFK.
507
- # @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).
508
542
  # @see Gateway#send_status_update
509
543
  def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
510
544
  gateway_check
@@ -557,6 +591,14 @@ module Discordrb
557
591
  name
558
592
  end
559
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
+
560
602
  # Sets status to online.
561
603
  def online
562
604
  gateway_check
@@ -585,6 +627,36 @@ module Discordrb
585
627
  update_status(:invisible, @activity, nil)
586
628
  end
587
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
+
588
660
  # Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
589
661
  def debug=(new_debug)
590
662
  LOGGER.debug = new_debug
@@ -711,6 +783,118 @@ module Discordrb
711
783
  end
712
784
  end
713
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
+
714
898
  private
715
899
 
716
900
  # Throws a useful exception if there's currently no gateway connection.
@@ -762,10 +946,16 @@ module Discordrb
762
946
 
763
947
  username = data['user']['username']
764
948
  if username && !member_is_new # Don't set the username for newly-cached members
765
- debug "Implicitly updating presence-obtained information for member #{user_id}"
949
+ debug "Implicitly updating presence-obtained information username for member #{user_id}"
766
950
  member.update_username(username)
767
951
  end
768
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
+
769
959
  member.update_presence(data)
770
960
 
771
961
  member.avatar_id = data['user']['avatar'] if data['user']['avatar']
@@ -826,14 +1016,14 @@ module Discordrb
826
1016
 
827
1017
  # Internal handler for CHANNEL_CREATE
828
1018
  def create_channel(data)
829
- channel = Channel.new(data, self)
1019
+ channel = data.is_a?(Discordrb::Channel) ? data : Channel.new(data, self)
830
1020
  server = channel.server
831
1021
 
832
1022
  # Handle normal and private channels separately
833
1023
  if server
834
1024
  server.add_channel(channel)
835
1025
  @channels[channel.id] = channel
836
- elsif channel.pm?
1026
+ elsif channel.private?
837
1027
  @pm_channels[channel.recipient.id] = channel
838
1028
  elsif channel.group?
839
1029
  @channels[channel.id] = channel
@@ -863,6 +1053,8 @@ module Discordrb
863
1053
  elsif channel.group?
864
1054
  @channels.delete(channel.id)
865
1055
  end
1056
+
1057
+ @thread_members.delete(channel.id) if channel.thread?
866
1058
  end
867
1059
 
868
1060
  # Internal handler for CHANNEL_RECIPIENT_ADD
@@ -902,7 +1094,9 @@ module Discordrb
902
1094
  member = server.member(data['user']['id'].to_i)
903
1095
  member.update_roles(data['roles'])
904
1096
  member.update_nick(data['nick'])
1097
+ member.update_global_name(data['user']['global_name']) if data['user']['global_name']
905
1098
  member.update_boosting_since(data['premium_since'])
1099
+ member.update_communication_disabled_until(data['communication_disabled_until'])
906
1100
  end
907
1101
 
908
1102
  # Internal handler for GUILD_MEMBER_DELETE
@@ -919,7 +1113,7 @@ module Discordrb
919
1113
 
920
1114
  # Internal handler for GUILD_CREATE
921
1115
  def create_guild(data)
922
- ensure_server(data)
1116
+ ensure_server(data, true)
923
1117
  end
924
1118
 
925
1119
  # Internal handler for GUILD_UPDATE
@@ -1010,7 +1204,7 @@ module Discordrb
1010
1204
 
1011
1205
  def process_token(type, token)
1012
1206
  # Remove the "Bot " prefix if it exists
1013
- token = token[4..-1] if token.start_with? 'Bot '
1207
+ token = token[4..] if token.start_with? 'Bot '
1014
1208
 
1015
1209
  token = "Bot #{token}" unless type == :user
1016
1210
  token
@@ -1047,14 +1241,14 @@ module Discordrb
1047
1241
  data['guilds'].each do |element|
1048
1242
  # Check for true specifically because unavailable=false indicates that a previously unavailable server has
1049
1243
  # come online
1050
- if element['unavailable'].is_a? TrueClass
1244
+ if element['unavailable']
1051
1245
  @unavailable_servers += 1
1052
1246
 
1053
1247
  # Ignore any unavailable servers
1054
1248
  next
1055
1249
  end
1056
1250
 
1057
- ensure_server(element)
1251
+ ensure_server(element, true)
1058
1252
  end
1059
1253
 
1060
1254
  # Add PM and group channels
@@ -1079,14 +1273,14 @@ module Discordrb
1079
1273
  when :GUILD_MEMBERS_CHUNK
1080
1274
  id = data['guild_id'].to_i
1081
1275
  server = server(id)
1082
- server.process_chunk(data['members'])
1276
+ server.process_chunk(data['members'], data['chunk_index'], data['chunk_count'])
1083
1277
  when :INVITE_CREATE
1084
1278
  invite = Invite.new(data, self)
1085
1279
  raise_event(InviteCreateEvent.new(data, invite, self))
1086
1280
  when :INVITE_DELETE
1087
1281
  raise_event(InviteDeleteEvent.new(data, self))
1088
1282
  when :MESSAGE_CREATE
1089
- if ignored?(data['author']['id'].to_i)
1283
+ if ignored?(data['author']['id'])
1090
1284
  debug("Ignored author with ID #{data['author']['id']}")
1091
1285
  return
1092
1286
  end
@@ -1103,6 +1297,13 @@ module Discordrb
1103
1297
 
1104
1298
  return if message.from_bot? && !should_parse_self
1105
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
+
1106
1307
  event = MessageEvent.new(message, self)
1107
1308
  raise_event(event)
1108
1309
 
@@ -1185,18 +1386,28 @@ module Discordrb
1185
1386
  # Ignore friends list presences
1186
1387
  return unless data['guild_id']
1187
1388
 
1188
- now_playing = data['game'].nil? ? nil : data['game']['name']
1389
+ new_activities = (data['activities'] || []).map { |act_data| Activity.new(act_data, self) }
1189
1390
  presence_user = @users[data['user']['id'].to_i]
1190
- played_before = presence_user.nil? ? nil : presence_user.game
1391
+ old_activities = (presence_user&.activities || [])
1191
1392
  update_presence(data)
1192
1393
 
1193
- event = if now_playing == played_before
1194
- PresenceEvent.new(data, self)
1195
- else
1196
- PlayingEvent.new(data, self)
1197
- 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
1198
1398
 
1199
- 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
1200
1411
  when :VOICE_STATE_UPDATE
1201
1412
  old_channel_id = update_voice_state(data)
1202
1413
 
@@ -1333,9 +1544,93 @@ module Discordrb
1333
1544
  event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1334
1545
  raise_event(event)
1335
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
1336
1598
  when :WEBHOOKS_UPDATE
1337
1599
  event = WebhookUpdateEvent.new(data, self)
1338
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)
1339
1634
  else
1340
1635
  # another event that we don't support yet
1341
1636
  debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"