discordrb 3.4.3 → 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 (62) 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 +390 -225
  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 +177 -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 +40 -19
  18. data/lib/discordrb/api/user.rb +8 -3
  19. data/lib/discordrb/api/webhook.rb +57 -0
  20. data/lib/discordrb/api.rb +19 -5
  21. data/lib/discordrb/bot.rb +317 -32
  22. data/lib/discordrb/cache.rb +27 -22
  23. data/lib/discordrb/commands/command_bot.rb +6 -4
  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/attachment.rb +15 -0
  29. data/lib/discordrb/data/audit_logs.rb +3 -3
  30. data/lib/discordrb/data/channel.rb +167 -23
  31. data/lib/discordrb/data/component.rb +229 -0
  32. data/lib/discordrb/data/integration.rb +42 -3
  33. data/lib/discordrb/data/interaction.rb +800 -0
  34. data/lib/discordrb/data/invite.rb +1 -1
  35. data/lib/discordrb/data/member.rb +108 -33
  36. data/lib/discordrb/data/message.rb +99 -19
  37. data/lib/discordrb/data/overwrite.rb +13 -7
  38. data/lib/discordrb/data/role.rb +58 -1
  39. data/lib/discordrb/data/server.rb +82 -80
  40. data/lib/discordrb/data/user.rb +69 -9
  41. data/lib/discordrb/data/webhook.rb +97 -4
  42. data/lib/discordrb/data.rb +3 -0
  43. data/lib/discordrb/errors.rb +44 -3
  44. data/lib/discordrb/events/channels.rb +1 -1
  45. data/lib/discordrb/events/interactions.rb +482 -0
  46. data/lib/discordrb/events/message.rb +9 -6
  47. data/lib/discordrb/events/presence.rb +21 -14
  48. data/lib/discordrb/events/reactions.rb +0 -1
  49. data/lib/discordrb/events/threads.rb +96 -0
  50. data/lib/discordrb/gateway.rb +30 -17
  51. data/lib/discordrb/permissions.rb +59 -34
  52. data/lib/discordrb/version.rb +1 -1
  53. data/lib/discordrb/voice/encoder.rb +2 -2
  54. data/lib/discordrb/voice/network.rb +18 -7
  55. data/lib/discordrb/voice/sodium.rb +3 -1
  56. data/lib/discordrb/voice/voice_bot.rb +3 -3
  57. data/lib/discordrb/webhooks.rb +2 -0
  58. data/lib/discordrb.rb +37 -4
  59. metadata +48 -14
  60. data/.codeclimate.yml +0 -16
  61. data/.travis.yml +0 -32
  62. data/bin/travis_build_docs.sh +0 -17
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,15 +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
418
  # @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)
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)
390
421
  Thread.new do
391
422
  Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
392
423
 
393
- message = send_message(channel, content, tts, embed, attachments, allowed_mentions, message_reference)
424
+ message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components)
394
425
  sleep(timeout)
395
426
  message.delete
396
427
  end
@@ -416,6 +447,7 @@ module Discordrb
416
447
  end
417
448
  # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
418
449
  file.define_singleton_method(:original_filename) { filename } if filename
450
+ file.define_singleton_method(:path) { filename } if filename
419
451
  end
420
452
 
421
453
  channel = channel.resolve_id
@@ -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,118 @@ 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)
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
+
724
898
  private
725
899
 
726
900
  # Throws a useful exception if there's currently no gateway connection.
@@ -772,10 +946,16 @@ module Discordrb
772
946
 
773
947
  username = data['user']['username']
774
948
  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}"
949
+ debug "Implicitly updating presence-obtained information username for member #{user_id}"
776
950
  member.update_username(username)
777
951
  end
778
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
+
779
959
  member.update_presence(data)
780
960
 
781
961
  member.avatar_id = data['user']['avatar'] if data['user']['avatar']
@@ -836,14 +1016,14 @@ module Discordrb
836
1016
 
837
1017
  # Internal handler for CHANNEL_CREATE
838
1018
  def create_channel(data)
839
- channel = Channel.new(data, self)
1019
+ channel = data.is_a?(Discordrb::Channel) ? data : Channel.new(data, self)
840
1020
  server = channel.server
841
1021
 
842
1022
  # Handle normal and private channels separately
843
1023
  if server
844
1024
  server.add_channel(channel)
845
1025
  @channels[channel.id] = channel
846
- elsif channel.pm?
1026
+ elsif channel.private?
847
1027
  @pm_channels[channel.recipient.id] = channel
848
1028
  elsif channel.group?
849
1029
  @channels[channel.id] = channel
@@ -873,6 +1053,8 @@ module Discordrb
873
1053
  elsif channel.group?
874
1054
  @channels.delete(channel.id)
875
1055
  end
1056
+
1057
+ @thread_members.delete(channel.id) if channel.thread?
876
1058
  end
877
1059
 
878
1060
  # Internal handler for CHANNEL_RECIPIENT_ADD
@@ -912,7 +1094,9 @@ module Discordrb
912
1094
  member = server.member(data['user']['id'].to_i)
913
1095
  member.update_roles(data['roles'])
914
1096
  member.update_nick(data['nick'])
1097
+ member.update_global_name(data['user']['global_name']) if data['user']['global_name']
915
1098
  member.update_boosting_since(data['premium_since'])
1099
+ member.update_communication_disabled_until(data['communication_disabled_until'])
916
1100
  end
917
1101
 
918
1102
  # Internal handler for GUILD_MEMBER_DELETE
@@ -929,7 +1113,7 @@ module Discordrb
929
1113
 
930
1114
  # Internal handler for GUILD_CREATE
931
1115
  def create_guild(data)
932
- ensure_server(data)
1116
+ ensure_server(data, true)
933
1117
  end
934
1118
 
935
1119
  # Internal handler for GUILD_UPDATE
@@ -1020,7 +1204,7 @@ module Discordrb
1020
1204
 
1021
1205
  def process_token(type, token)
1022
1206
  # Remove the "Bot " prefix if it exists
1023
- token = token[4..-1] if token.start_with? 'Bot '
1207
+ token = token[4..] if token.start_with? 'Bot '
1024
1208
 
1025
1209
  token = "Bot #{token}" unless type == :user
1026
1210
  token
@@ -1057,14 +1241,14 @@ module Discordrb
1057
1241
  data['guilds'].each do |element|
1058
1242
  # Check for true specifically because unavailable=false indicates that a previously unavailable server has
1059
1243
  # come online
1060
- if element['unavailable'].is_a? TrueClass
1244
+ if element['unavailable']
1061
1245
  @unavailable_servers += 1
1062
1246
 
1063
1247
  # Ignore any unavailable servers
1064
1248
  next
1065
1249
  end
1066
1250
 
1067
- ensure_server(element)
1251
+ ensure_server(element, true)
1068
1252
  end
1069
1253
 
1070
1254
  # Add PM and group channels
@@ -1089,14 +1273,14 @@ module Discordrb
1089
1273
  when :GUILD_MEMBERS_CHUNK
1090
1274
  id = data['guild_id'].to_i
1091
1275
  server = server(id)
1092
- server.process_chunk(data['members'])
1276
+ server.process_chunk(data['members'], data['chunk_index'], data['chunk_count'])
1093
1277
  when :INVITE_CREATE
1094
1278
  invite = Invite.new(data, self)
1095
1279
  raise_event(InviteCreateEvent.new(data, invite, self))
1096
1280
  when :INVITE_DELETE
1097
1281
  raise_event(InviteDeleteEvent.new(data, self))
1098
1282
  when :MESSAGE_CREATE
1099
- if ignored?(data['author']['id'].to_i)
1283
+ if ignored?(data['author']['id'])
1100
1284
  debug("Ignored author with ID #{data['author']['id']}")
1101
1285
  return
1102
1286
  end
@@ -1113,6 +1297,13 @@ module Discordrb
1113
1297
 
1114
1298
  return if message.from_bot? && !should_parse_self
1115
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
+
1116
1307
  event = MessageEvent.new(message, self)
1117
1308
  raise_event(event)
1118
1309
 
@@ -1195,18 +1386,28 @@ module Discordrb
1195
1386
  # Ignore friends list presences
1196
1387
  return unless data['guild_id']
1197
1388
 
1198
- now_playing = data['game'].nil? ? nil : data['game']['name']
1389
+ new_activities = (data['activities'] || []).map { |act_data| Activity.new(act_data, self) }
1199
1390
  presence_user = @users[data['user']['id'].to_i]
1200
- played_before = presence_user.nil? ? nil : presence_user.game
1391
+ old_activities = (presence_user&.activities || [])
1201
1392
  update_presence(data)
1202
1393
 
1203
- event = if now_playing == played_before
1204
- PresenceEvent.new(data, self)
1205
- else
1206
- PlayingEvent.new(data, self)
1207
- 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
1208
1398
 
1209
- 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
1210
1411
  when :VOICE_STATE_UPDATE
1211
1412
  old_channel_id = update_voice_state(data)
1212
1413
 
@@ -1343,9 +1544,93 @@ module Discordrb
1343
1544
  event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1344
1545
  raise_event(event)
1345
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
1346
1598
  when :WEBHOOKS_UPDATE
1347
1599
  event = WebhookUpdateEvent.new(data, self)
1348
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)
1349
1634
  else
1350
1635
  # another event that we don't support yet
1351
1636
  debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
@@ -21,8 +21,7 @@ module Discordrb
21
21
 
22
22
  @channels = {}
23
23
  @pm_channels = {}
24
-
25
- @restricted_channels = []
24
+ @thread_members = {}
26
25
  end
27
26
 
28
27
  # Returns or caches the available voice regions
@@ -42,28 +41,21 @@ module Discordrb
42
41
  # @param id [Integer] The channel ID for which to search for.
43
42
  # @param server [Server] The server for which to search the channel for. If this isn't specified, it will be
44
43
  # inferred using the API
45
- # @return [Channel] The channel identified by the ID.
44
+ # @return [Channel, nil] The channel identified by the ID.
45
+ # @raise Discordrb::Errors::NoPermission
46
46
  def channel(id, server = nil)
47
47
  id = id.resolve_id
48
48
 
49
- raise Discordrb::Errors::NoPermission if @restricted_channels.include? id
50
-
51
49
  debug("Obtaining data for channel with id #{id}")
52
50
  return @channels[id] if @channels[id]
53
51
 
54
52
  begin
55
- begin
56
- response = API::Channel.resolve(token, id)
57
- rescue RestClient::ResourceNotFound
58
- return nil
59
- end
60
- channel = Channel.new(JSON.parse(response), self, server)
61
- @channels[id] = channel
62
- rescue Discordrb::Errors::NoPermission
63
- debug "Tried to get access to restricted channel #{id}, blacklisting it"
64
- @restricted_channels << id
65
- raise
53
+ response = API::Channel.resolve(token, id)
54
+ rescue Discordrb::Errors::UnknownChannel
55
+ return nil
66
56
  end
57
+ channel = Channel.new(JSON.parse(response), self, server)
58
+ @channels[id] = channel
67
59
  end
68
60
 
69
61
  alias_method :group_channel, :channel
@@ -79,7 +71,7 @@ module Discordrb
79
71
  LOGGER.out("Resolving user #{id}")
80
72
  begin
81
73
  response = API::User.resolve(token, id)
82
- rescue RestClient::ResourceNotFound
74
+ rescue Discordrb::Errors::UnknownUser
83
75
  return nil
84
76
  end
85
77
  user = User.new(JSON.parse(response), self)
@@ -111,7 +103,6 @@ module Discordrb
111
103
  def member(server_or_id, user_id)
112
104
  server_id = server_or_id.resolve_id
113
105
  user_id = user_id.resolve_id
114
-
115
106
  server = server_or_id.is_a?(Server) ? server_or_id : self.server(server_id)
116
107
 
117
108
  return server.member(user_id) if server.member_cached?(user_id)
@@ -119,7 +110,7 @@ module Discordrb
119
110
  LOGGER.out("Resolving member #{server_id} on server #{user_id}")
120
111
  begin
121
112
  response = API::Server.resolve_member(token, server_id, user_id)
122
- rescue RestClient::ResourceNotFound
113
+ rescue Discordrb::Errors::UnknownUser, Discordrb::Errors::UnknownMember
123
114
  return nil
124
115
  end
125
116
  member = Member.new(JSON.parse(response), server, self)
@@ -156,10 +147,14 @@ module Discordrb
156
147
 
157
148
  # Ensures a given server object is cached and if not, cache it from the given data hash.
158
149
  # @param data [Hash] A data hash representing a server.
150
+ # @param force_cache [true, false] Whether the object in cache should be updated with the given
151
+ # data if it already exists.
159
152
  # @return [Server] the server represented by the data hash.
160
- def ensure_server(data)
153
+ def ensure_server(data, force_cache = false)
161
154
  if @servers.include?(data['id'].to_i)
162
- @servers[data['id'].to_i]
155
+ server = @servers[data['id'].to_i]
156
+ server.update_data(data) if force_cache
157
+ server
163
158
  else
164
159
  @servers[data['id'].to_i] = Server.new(data, self)
165
160
  end
@@ -177,6 +172,16 @@ module Discordrb
177
172
  end
178
173
  end
179
174
 
175
+ # Ensures a given thread member object is cached.
176
+ # @param data [Hash] Thread member data.
177
+ def ensure_thread_member(data)
178
+ thread_id = data['id'].to_i
179
+ user_id = data['user_id'].to_i
180
+
181
+ @thread_members[thread_id] ||= {}
182
+ @thread_members[thread_id][user_id] = data.slice('join_timestamp', 'flags')
183
+ end
184
+
180
185
  # Requests member chunks for a given server ID.
181
186
  # @param id [Integer] The server ID to request chunks for.
182
187
  def request_chunks(id)
@@ -194,7 +199,7 @@ module Discordrb
194
199
  # @return [String] Only the code for the invite.
195
200
  def resolve_invite_code(invite)
196
201
  invite = invite.code if invite.is_a? Discordrb::Invite
197
- invite = invite[invite.rindex('/') + 1..-1] if invite.start_with?('http', 'discord.gg')
202
+ invite = invite[invite.rindex('/') + 1..] if invite.start_with?('http', 'discord.gg')
198
203
  invite
199
204
  end
200
205