discordrb 3.4.3 → 3.5.0

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