discordrb 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
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"