rubycord 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/lib/rubycord/allowed_mentions.rb +34 -0
  3. data/lib/rubycord/api/application.rb +200 -0
  4. data/lib/rubycord/api/channel.rb +597 -0
  5. data/lib/rubycord/api/interaction.rb +52 -0
  6. data/lib/rubycord/api/invite.rb +42 -0
  7. data/lib/rubycord/api/server.rb +557 -0
  8. data/lib/rubycord/api/user.rb +153 -0
  9. data/lib/rubycord/api/webhook.rb +138 -0
  10. data/lib/rubycord/api.rb +356 -0
  11. data/lib/rubycord/await.rb +49 -0
  12. data/lib/rubycord/bot.rb +1757 -0
  13. data/lib/rubycord/cache.rb +259 -0
  14. data/lib/rubycord/colour_rgb.rb +41 -0
  15. data/lib/rubycord/commands/command_bot.rb +519 -0
  16. data/lib/rubycord/commands/container.rb +110 -0
  17. data/lib/rubycord/commands/events.rb +9 -0
  18. data/lib/rubycord/commands/parser.rb +325 -0
  19. data/lib/rubycord/commands/rate_limiter.rb +142 -0
  20. data/lib/rubycord/container.rb +753 -0
  21. data/lib/rubycord/data/activity.rb +269 -0
  22. data/lib/rubycord/data/application.rb +48 -0
  23. data/lib/rubycord/data/attachment.rb +109 -0
  24. data/lib/rubycord/data/audit_logs.rb +343 -0
  25. data/lib/rubycord/data/channel.rb +996 -0
  26. data/lib/rubycord/data/component.rb +227 -0
  27. data/lib/rubycord/data/embed.rb +249 -0
  28. data/lib/rubycord/data/emoji.rb +80 -0
  29. data/lib/rubycord/data/integration.rb +120 -0
  30. data/lib/rubycord/data/interaction.rb +798 -0
  31. data/lib/rubycord/data/invite.rb +135 -0
  32. data/lib/rubycord/data/member.rb +370 -0
  33. data/lib/rubycord/data/message.rb +412 -0
  34. data/lib/rubycord/data/overwrite.rb +106 -0
  35. data/lib/rubycord/data/profile.rb +89 -0
  36. data/lib/rubycord/data/reaction.rb +31 -0
  37. data/lib/rubycord/data/recipient.rb +32 -0
  38. data/lib/rubycord/data/role.rb +246 -0
  39. data/lib/rubycord/data/server.rb +1002 -0
  40. data/lib/rubycord/data/user.rb +261 -0
  41. data/lib/rubycord/data/voice_region.rb +43 -0
  42. data/lib/rubycord/data/voice_state.rb +39 -0
  43. data/lib/rubycord/data/webhook.rb +232 -0
  44. data/lib/rubycord/data.rb +40 -0
  45. data/lib/rubycord/errors.rb +737 -0
  46. data/lib/rubycord/events/await.rb +46 -0
  47. data/lib/rubycord/events/bans.rb +58 -0
  48. data/lib/rubycord/events/channels.rb +186 -0
  49. data/lib/rubycord/events/generic.rb +126 -0
  50. data/lib/rubycord/events/guilds.rb +191 -0
  51. data/lib/rubycord/events/interactions.rb +480 -0
  52. data/lib/rubycord/events/invites.rb +123 -0
  53. data/lib/rubycord/events/lifetime.rb +29 -0
  54. data/lib/rubycord/events/members.rb +91 -0
  55. data/lib/rubycord/events/message.rb +337 -0
  56. data/lib/rubycord/events/presence.rb +127 -0
  57. data/lib/rubycord/events/raw.rb +45 -0
  58. data/lib/rubycord/events/reactions.rb +156 -0
  59. data/lib/rubycord/events/roles.rb +86 -0
  60. data/lib/rubycord/events/threads.rb +94 -0
  61. data/lib/rubycord/events/typing.rb +70 -0
  62. data/lib/rubycord/events/voice_server_update.rb +45 -0
  63. data/lib/rubycord/events/voice_state_update.rb +103 -0
  64. data/lib/rubycord/events/webhooks.rb +62 -0
  65. data/lib/rubycord/gateway.rb +867 -0
  66. data/lib/rubycord/id_object.rb +37 -0
  67. data/lib/rubycord/light/data.rb +60 -0
  68. data/lib/rubycord/light/integrations.rb +71 -0
  69. data/lib/rubycord/light/light_bot.rb +56 -0
  70. data/lib/rubycord/light.rb +6 -0
  71. data/lib/rubycord/logger.rb +118 -0
  72. data/lib/rubycord/paginator.rb +55 -0
  73. data/lib/rubycord/permissions.rb +251 -0
  74. data/lib/rubycord/version.rb +5 -0
  75. data/lib/rubycord/voice/encoder.rb +113 -0
  76. data/lib/rubycord/voice/network.rb +366 -0
  77. data/lib/rubycord/voice/sodium.rb +96 -0
  78. data/lib/rubycord/voice/voice_bot.rb +408 -0
  79. data/lib/rubycord/webhooks/builder.rb +100 -0
  80. data/lib/rubycord/webhooks/client.rb +132 -0
  81. data/lib/rubycord/webhooks/embeds.rb +248 -0
  82. data/lib/rubycord/webhooks/modal.rb +78 -0
  83. data/lib/rubycord/webhooks/version.rb +7 -0
  84. data/lib/rubycord/webhooks/view.rb +192 -0
  85. data/lib/rubycord/webhooks.rb +12 -0
  86. data/lib/rubycord/websocket.rb +70 -0
  87. data/lib/rubycord.rb +140 -0
  88. metadata +231 -0
@@ -0,0 +1,1757 @@
1
+ require "rest-client"
2
+ require "zlib"
3
+
4
+ require "rubycord/events/message"
5
+ require "rubycord/events/typing"
6
+ require "rubycord/events/lifetime"
7
+ require "rubycord/events/presence"
8
+ require "rubycord/events/voice_state_update"
9
+ require "rubycord/events/channels"
10
+ require "rubycord/events/members"
11
+ require "rubycord/events/roles"
12
+ require "rubycord/events/guilds"
13
+ require "rubycord/events/await"
14
+ require "rubycord/events/bans"
15
+ require "rubycord/events/raw"
16
+ require "rubycord/events/reactions"
17
+ require "rubycord/events/webhooks"
18
+ require "rubycord/events/invites"
19
+ require "rubycord/events/interactions"
20
+ require "rubycord/events/threads"
21
+
22
+ require "rubycord/api"
23
+ require "rubycord/api/channel"
24
+ require "rubycord/api/server"
25
+ require "rubycord/api/invite"
26
+ require "rubycord/api/interaction"
27
+ require "rubycord/api/application"
28
+
29
+ require "rubycord/errors"
30
+ require "rubycord/data"
31
+ require "rubycord/await"
32
+ require "rubycord/container"
33
+ require "rubycord/websocket"
34
+ require "rubycord/cache"
35
+ require "rubycord/gateway"
36
+
37
+ require "rubycord/voice/voice_bot"
38
+
39
+ module Rubycord
40
+ # Represents a Discord bot, including servers, users, etc.
41
+ class Bot
42
+ # The list of currently running threads used to parse and call events.
43
+ # The threads will have a local variable `:rubycord_name` in the format of `et-1234`, where
44
+ # "et" stands for "event thread" and the number is a continually incrementing number representing
45
+ # how many events were executed before.
46
+ # @return [Array<Thread>] The threads.
47
+ attr_reader :event_threads
48
+
49
+ # @return [true, false] whether or not the bot should parse its own messages. Off by default.
50
+ attr_accessor :should_parse_self
51
+
52
+ # The bot's name which rubycord sends to Discord when making any request, so Discord can identify bots with the
53
+ # same codebase. Not required but I recommend setting it anyway.
54
+ # @return [String] The bot's name.
55
+ attr_accessor :name
56
+
57
+ # @return [Array(Integer, Integer)] the current shard key
58
+ attr_reader :shard_key
59
+
60
+ # @return [Hash<Symbol => Await>] the list of registered {Await}s.
61
+ attr_reader :awaits
62
+
63
+ # The gateway connection is an internal detail that is useless to most people. It is however essential while
64
+ # debugging or developing rubycord itself, or while writing very custom bots.
65
+ # @return [Gateway] the underlying {Gateway} object.
66
+ attr_reader :gateway
67
+
68
+ include EventContainer
69
+ include Cache
70
+
71
+ # Makes a new bot with the given authentication data. It will be ready to be added event handlers to and can
72
+ # eventually be run with {#run}.
73
+ #
74
+ # As support for logging in using username and password has been removed in version 3.0.0, only a token login is
75
+ # possible. Be sure to specify the `type` parameter as `:user` if you're logging in as a user.
76
+ #
77
+ # Simply creating a bot won't be enough to start sending messages etc. with, only a limited set of methods can
78
+ # be used after logging in. If you want to do something when the bot has connected successfully, either do it in the
79
+ # {#ready} event, or use the {#run} method with the :async parameter and do the processing after that.
80
+ # @param log_mode [Symbol] The mode this bot should use for logging. See {Logger#mode=} for a list of modes.
81
+ # @param token [String] The token that should be used to log in. If your bot is a bot account, you have to specify
82
+ # this. If you're logging in as a user, make sure to also set the account type to :user so rubycord doesn't think
83
+ # you're trying to log in as a bot.
84
+ # @param client_id [Integer] If you're logging in as a bot, the bot's client ID. This is optional, and may be fetched
85
+ # from the API by calling {Bot#bot_application} (see {Application}).
86
+ # @param type [Symbol] This parameter lets you manually overwrite the account type. This needs to be set when
87
+ # logging in as a user, otherwise rubycord will treat you as a bot account. Valid values are `:user` and `:bot`.
88
+ # @param name [String] Your bot's name. This will be sent to Discord with any API requests, who will use this to
89
+ # trace the source of excessive API requests; it's recommended to set this to something if you make bots that many
90
+ # people will host on their servers separately.
91
+ # @param fancy_log [true, false] Whether the output log should be made extra fancy using ANSI escape codes. (Your
92
+ # terminal may not support this.)
93
+ # @param suppress_ready [true, false] Whether the READY packet should be exempt from being printed to console.
94
+ # Useful for very large bots running in debug or verbose log_mode.
95
+ # @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
96
+ # unless you really need this so you don't inadvertently create infinite loops.
97
+ # @param shard_id [Integer] The number of the shard this bot should handle. See
98
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
99
+ # @param num_shards [Integer] The total number of shards that should be running. See
100
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
101
+ # @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
102
+ # @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
103
+ # @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
104
+ # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
105
+ # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
106
+ # that all data be received in a continuous compressed stream.
107
+ # @param intents [:all, :unprivileged, Array<Symbol>, :none] Gateway intents that this bot requires. `:all` will
108
+ # request all intents. `:unprivileged` will request only intents that are not defined as "Privileged". `:none`
109
+ # will request no intents. An array of symbols will request only those intents specified.
110
+ # @see Rubycord::INTENTS
111
+ def initialize(
112
+ log_mode: :normal,
113
+ token: nil, client_id: nil,
114
+ type: nil, name: "", fancy_log: false, suppress_ready: false, parse_self: false,
115
+ shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
116
+ compress_mode: :large, intents: :all
117
+ )
118
+ LOGGER.mode = log_mode
119
+ LOGGER.token = token if redact_token
120
+
121
+ @should_parse_self = parse_self
122
+
123
+ @client_id = client_id
124
+
125
+ @type = type || :bot
126
+ @name = name
127
+
128
+ @shard_key = num_shards ? [shard_id, num_shards] : nil
129
+
130
+ LOGGER.fancy = fancy_log
131
+ @prevent_ready = suppress_ready
132
+
133
+ @compress_mode = compress_mode
134
+
135
+ raise "Token string is empty or nil" if token.nil? || token.empty?
136
+
137
+ @intents = case intents
138
+ when :all
139
+ ALL_INTENTS
140
+ when :unprivileged
141
+ UNPRIVILEGED_INTENTS
142
+ when :none
143
+ NO_INTENTS
144
+ else
145
+ calculate_intents(intents)
146
+ end
147
+
148
+ @token = process_token(@type, token)
149
+ @gateway = Gateway.new(self, @token, @shard_key, @compress_mode, @intents)
150
+
151
+ init_cache
152
+
153
+ @voices = {}
154
+ @should_connect_to_voice = {}
155
+
156
+ @ignored_ids = Set.new
157
+ @ignore_bots = ignore_bots
158
+
159
+ @event_threads = []
160
+ @current_thread = 0
161
+
162
+ @status = :online
163
+
164
+ @application_commands = {}
165
+ end
166
+
167
+ # The list of users the bot shares a server with.
168
+ # @return [Hash<Integer => User>] The users by ID.
169
+ def users
170
+ gateway_check
171
+ unavailable_servers_check
172
+ @users
173
+ end
174
+
175
+ # The list of servers the bot is currently in.
176
+ # @return [Hash<Integer => Server>] The servers by ID.
177
+ def servers
178
+ gateway_check
179
+ unavailable_servers_check
180
+ @servers
181
+ end
182
+
183
+ # The list of members in threads the bot can see.
184
+ # @return [Hash<Integer => Hash<Integer => Hash<String => Object>>]
185
+ def thread_members
186
+ gateway_check
187
+ unavailable_servers_check
188
+ @thread_members
189
+ end
190
+
191
+ # @overload emoji(id)
192
+ # Return an emoji by its ID
193
+ # @param id [String, Integer] The emoji's ID.
194
+ # @return [Emoji, nil] the emoji object. `nil` if the emoji was not found.
195
+ # @overload emoji
196
+ # The list of emoji the bot can use.
197
+ # @return [Array<Emoji>] the emoji available.
198
+ def emoji(id = nil)
199
+ emoji_hash = servers.values.map(&:emoji).reduce(&:merge)
200
+ if id
201
+ id = id.resolve_id
202
+ emoji_hash[id]
203
+ else
204
+ emoji_hash.values
205
+ end
206
+ end
207
+
208
+ alias_method :emojis, :emoji
209
+ alias_method :all_emoji, :emoji
210
+
211
+ # Finds an emoji by its name.
212
+ # @param name [String] The emoji name that should be resolved.
213
+ # @return [GlobalEmoji, nil] the emoji identified by the name, or `nil` if it couldn't be found.
214
+ def find_emoji(name)
215
+ LOGGER.out("Resolving emoji #{name}")
216
+ emoji.find { |element| element.name == name }
217
+ end
218
+
219
+ # The bot's user profile. This special user object can be used
220
+ # to edit user data like the current username (see {Profile#username=}).
221
+ # @return [Profile] The bot's profile that can be used to edit data.
222
+ def profile
223
+ return @profile if @profile
224
+
225
+ response = Rubycord::API::User.profile(@token)
226
+ @profile = Profile.new(JSON.parse(response), self)
227
+ end
228
+
229
+ alias_method :bot_user, :profile
230
+
231
+ # The bot's OAuth application.
232
+ # @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
233
+ def bot_application
234
+ return unless @type == :bot
235
+
236
+ response = API.oauth_application(token)
237
+ Application.new(JSON.parse(response), self)
238
+ end
239
+
240
+ alias_method :bot_app, :bot_application
241
+
242
+ # The Discord API token received when logging in. Useful to explicitly call
243
+ # {API} methods.
244
+ # @return [String] The API token.
245
+ def token
246
+ API.bot_name = @name
247
+ @token
248
+ end
249
+
250
+ # @return [String] the raw token, without any prefix
251
+ # @see #token
252
+ def raw_token
253
+ @token.split(" ").last
254
+ end
255
+
256
+ # Runs the bot, which logs into Discord and connects the WebSocket. This
257
+ # prevents all further execution unless it is executed with
258
+ # `background` = `true`.
259
+ # @param background [true, false] If it is `true`, then the bot will run in
260
+ # another thread to allow further execution. If it is `false`, this method
261
+ # will block until {#stop} is called. If the bot is run with `true`, make
262
+ # sure to eventually call {#join} so the script doesn't stop prematurely.
263
+ # @note Running the bot in the background means that you can call some
264
+ # methods that require a gateway connection *before* that connection is
265
+ # established. In most cases an exception will be raised if you try to do
266
+ # this. If you need a way to safely run code after the bot is fully
267
+ # connected, use a {#ready} event handler instead.
268
+ def run(background = false)
269
+ @gateway.run_async
270
+ return if background
271
+
272
+ debug("Oh wait! Not exiting yet as run was run synchronously.")
273
+ @gateway.sync
274
+ end
275
+
276
+ # Joins the bot's connection thread with the current thread.
277
+ # This blocks execution until the websocket stops, which should only happen
278
+ # manually triggered. or due to an error. This is necessary to have a
279
+ # continuously running bot.
280
+ def join
281
+ @gateway.sync
282
+ end
283
+ alias_method :sync, :join
284
+
285
+ # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
286
+ # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
287
+ # @note This method no longer takes an argument as of 3.4.0
288
+ def stop(_no_sync = nil)
289
+ @gateway.stop
290
+ end
291
+
292
+ # @return [true, false] whether or not the bot is currently connected to Discord.
293
+ def connected?
294
+ @gateway.open?
295
+ end
296
+
297
+ # Makes the bot join an invite to a server.
298
+ # @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
299
+ def accept_invite(invite)
300
+ resolved = invite(invite).code
301
+ API::Invite.accept(token, resolved)
302
+ end
303
+
304
+ # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
305
+ # @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
306
+ # @param permission_bits [String, Integer] Permission bits that should be appended to invite url.
307
+ # @param redirect_uri [String] Redirect URI that should be appended to invite url.
308
+ # @param scopes [Array<String>] Scopes that should be appended to invite url.
309
+ # @return [String] the OAuth invite URL.
310
+ def invite_url(server: nil, permission_bits: nil, redirect_uri: nil, scopes: ["bot"])
311
+ @client_id ||= bot_application.id
312
+
313
+ query = URI.encode_www_form({
314
+ client_id: @client_id,
315
+ guild_id: server&.id,
316
+ permissions: permission_bits,
317
+ redirect_uri: redirect_uri,
318
+ scope: scopes.join(" ")
319
+ }.compact)
320
+
321
+ "https://discord.com/oauth2/authorize?#{query}"
322
+ end
323
+
324
+ # @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
325
+ attr_reader :voices
326
+
327
+ # Gets the voice bot for a particular server or channel. You can connect to a new channel using the {#voice_connect}
328
+ # method.
329
+ # @param thing [Channel, Server, Integer] the server or channel you want to get the voice bot for, or its ID.
330
+ # @return [Voice::VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
331
+ def voice(thing)
332
+ id = thing.resolve_id
333
+ return @voices[id] if @voices[id]
334
+
335
+ channel = channel(id)
336
+ return nil unless channel
337
+
338
+ server_id = channel.server.id
339
+ @voices[server_id]
340
+ end
341
+
342
+ # Connects to a voice channel, initializes network connections and returns the {Voice::VoiceBot} over which audio
343
+ # data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
344
+ # connected to voice, the existing connection will be terminated - you don't have to call
345
+ # {Rubycord::Voice::VoiceBot#destroy} before calling this method.
346
+ # @param chan [Channel, String, Integer] The voice channel, or its ID, to connect to.
347
+ # @param encrypted [true, false] Whether voice communication should be encrypted using
348
+ # (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
349
+ # @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
350
+ def voice_connect(chan, encrypted = true)
351
+ raise ArgumentError, "Unencrypted voice connections are no longer supported." unless encrypted
352
+
353
+ chan = channel(chan.resolve_id)
354
+ server_id = chan.server.id
355
+
356
+ if @voices[chan.id]
357
+ debug("Voice bot exists already! Destroying it")
358
+ @voices[chan.id].destroy
359
+ @voices.delete(chan.id)
360
+ end
361
+
362
+ debug("Got voice channel: #{chan}")
363
+
364
+ @should_connect_to_voice[server_id] = chan
365
+ @gateway.send_voice_state_update(server_id.to_s, chan.id.to_s, false, false)
366
+
367
+ debug("Voice channel init packet sent! Now waiting.")
368
+
369
+ sleep(0.05) until @voices[server_id]
370
+ debug("Voice connect succeeded!")
371
+ @voices[server_id]
372
+ end
373
+
374
+ # Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
375
+ # {Rubycord::Voice::VoiceBot#destroy} rather than this.
376
+ # @param server [Server, String, Integer] The server, or server ID, the voice connection is on.
377
+ # @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
378
+ # directly, you should leave it as true.
379
+ def voice_destroy(server, destroy_vws = true)
380
+ server = server.resolve_id
381
+ @gateway.send_voice_state_update(server.to_s, nil, false, false)
382
+ @voices[server].destroy if @voices[server] && destroy_vws
383
+ @voices.delete(server)
384
+ end
385
+
386
+ # Revokes an invite to a server. Will fail unless you have the *Manage Server* permission.
387
+ # It is recommended that you use {Invite#delete} instead.
388
+ # @param code [String, Invite] The invite to revoke. For possible formats see {#resolve_invite_code}.
389
+ def delete_invite(code)
390
+ invite = resolve_invite_code(code)
391
+ API::Invite.delete(token, invite)
392
+ end
393
+
394
+ # Sends a text message to a channel given its ID and the message's content.
395
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
396
+ # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
397
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
398
+ # @param embeds [Hash, Rubycord::Webhooks::Embed, Array<Hash>, Array<Rubycord::Webhooks::Embed> nil] The rich embed(s) to append to this message.
399
+ # @param allowed_mentions [Hash, Rubycord::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
400
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
401
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
402
+ # @return [Message] The message that was sent.
403
+ def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
404
+ channel = channel.resolve_id
405
+ debug("Sending message to #{channel} with content '#{content}'")
406
+ allowed_mentions = {parse: []} if allowed_mentions == false
407
+ message_reference = {message_id: message_reference.id} if message_reference.respond_to?(:id)
408
+ embeds = (embeds.instance_of?(Array) ? embeds.map(&:to_hash) : [embeds&.to_hash]).compact
409
+
410
+ response = API::Channel.create_message(token, channel, content, tts, embeds, nil, attachments, allowed_mentions&.to_hash, message_reference, components)
411
+ Message.new(JSON.parse(response), self)
412
+ end
413
+
414
+ # Sends a text message to a channel given its ID and the message's content,
415
+ # then deletes it after the specified timeout in seconds.
416
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
417
+ # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
418
+ # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
419
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
420
+ # @param embeds [Hash, Rubycord::Webhooks::Embed, Array<Hash>, Array<Rubycord::Webhooks::Embed> nil] The rich embed(s) to append to this message.
421
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
422
+ # @param allowed_mentions [Hash, Rubycord::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
423
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
424
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
425
+ def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
426
+ Thread.new do
427
+ Thread.current[:rubycord_name] = "#{@current_thread}-temp-msg"
428
+
429
+ message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components)
430
+ sleep(timeout)
431
+ message.delete
432
+ end
433
+
434
+ nil
435
+ end
436
+
437
+ # Sends a file to a channel. If it is an image, it will automatically be embedded.
438
+ # @note This executes in a blocking way, so if you're sending long files, be wary of delays.
439
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
440
+ # @param file [File] The file that should be sent.
441
+ # @param caption [string] The caption for the file.
442
+ # @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
443
+ # @param filename [String] Overrides the filename of the uploaded file
444
+ # @param spoiler [true, false] Whether or not this file should appear as a spoiler.
445
+ # @example Send a file from disk
446
+ # bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))
447
+ def send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil)
448
+ if file.respond_to?(:read)
449
+ if spoiler
450
+ filename ||= File.basename(file.path)
451
+ filename = "SPOILER_#{filename}" unless filename.start_with? "SPOILER_"
452
+ end
453
+ # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
454
+ file.define_singleton_method(:original_filename) { filename } if filename
455
+ file.define_singleton_method(:path) { filename } if filename
456
+ end
457
+
458
+ channel = channel.resolve_id
459
+ response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
460
+ Message.new(JSON.parse(response), self)
461
+ end
462
+
463
+ # Creates a server on Discord with a specified name and a region.
464
+ # @note Discord's API doesn't directly return the server when creating it, so this method
465
+ # waits until the data has been received via the websocket. This may make the execution take a while.
466
+ # @param name [String] The name the new server should have. Doesn't have to be alphanumeric.
467
+ # @param region [Symbol] The region where the server should be created, for example 'eu-central' or 'hongkong'.
468
+ # @return [Server] The server that was created.
469
+ def create_server(name, region = :"eu-central")
470
+ response = API::Server.create(token, name, region)
471
+ id = JSON.parse(response)["id"].to_i
472
+ sleep 0.1 until (server = @servers[id])
473
+ debug "Successfully created server #{server.id} with name #{server.name}"
474
+ server
475
+ end
476
+
477
+ # Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using
478
+ # Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
479
+ # @param name [String] What your application should be called.
480
+ # @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
481
+ # @return [Array(String, String)] your applications' client ID and client secret to be used in OAuth authorization.
482
+ def create_oauth_application(name, redirect_uris)
483
+ response = JSON.parse(API.create_oauth_application(@token, name, redirect_uris))
484
+ [response["id"], response["secret"]]
485
+ end
486
+
487
+ # Changes information about your OAuth application
488
+ # @param name [String] What your application should be called.
489
+ # @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
490
+ # @param description [String] A string that describes what your application does.
491
+ # @param icon [String, nil] A data URI for your icon image (for example a base 64 encoded image), or nil if no icon
492
+ # should be set or changed.
493
+ def update_oauth_application(name, redirect_uris, description = "", icon = nil)
494
+ API.update_oauth_application(@token, name, redirect_uris, description, icon)
495
+ end
496
+
497
+ # Gets the users, channels, roles and emoji from a string.
498
+ # @param mentions [String] The mentions, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
499
+ # @param server [Server, nil] The server of the associated mentions. (recommended for role parsing, to speed things up)
500
+ # @return [Array<User, Channel, Role, Emoji>] The array of users, channels, roles and emoji identified by the mentions, or `nil` if none exists.
501
+ def parse_mentions(mentions, server = nil)
502
+ array_to_return = []
503
+ # While possible mentions may be in message
504
+ while mentions.include?("<") && mentions.include?(">")
505
+ # Removing all content before the next possible mention
506
+ mentions = mentions.split("<", 2)[1]
507
+ # Locate the first valid mention enclosed in `<...>`, otherwise advance to the next open `<`
508
+ next unless mentions.split(">", 2).first.length < mentions.split("<", 2).first.length
509
+
510
+ # Store the possible mention value to be validated with RegEx
511
+ mention = mentions.split(">", 2).first
512
+ if /@!?(?<id>\d+)/ =~ mention
513
+ array_to_return << user(id) unless user(id).nil?
514
+ elsif /#(?<id>\d+)/ =~ mention
515
+ array_to_return << channel(id, server) unless channel(id, server).nil?
516
+ elsif /@&(?<id>\d+)/ =~ mention
517
+ if server
518
+ array_to_return << server.role(id) unless server.role(id).nil?
519
+ else
520
+ @servers.each_value do |element|
521
+ array_to_return << element.role(id) unless element.role(id).nil?
522
+ end
523
+ end
524
+ elsif /(?<animated>^a|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
525
+ array_to_return << (emoji(id) || Emoji.new({"animated" => !animated.nil?, "name" => name, "id" => id}, self, nil))
526
+ end
527
+ end
528
+ array_to_return
529
+ end
530
+
531
+ # Gets the user, channel, role or emoji from a string.
532
+ # @param mention [String] The mention, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
533
+ # @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
534
+ # @return [User, Channel, Role, Emoji] The user, channel, role or emoji identified by the mention, or `nil` if none exists.
535
+ def parse_mention(mention, server = nil)
536
+ parse_mentions(mention, server).first
537
+ end
538
+
539
+ # Updates presence status.
540
+ # @param status [String] The status the bot should show up as. Can be `online`, `dnd`, `idle`, or `invisible`
541
+ # @param activity [String, nil] The name of the activity to be played/watched/listened to/stream name on the stream.
542
+ # @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
543
+ # @param since [Integer] When this status was set.
544
+ # @param afk [true, false] Whether the bot is AFK.
545
+ # @param activity_type [Integer] The type of activity status to display.
546
+ # Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching), 4 (Custom), or 5 (Competing).
547
+ # @see Gateway#send_status_update
548
+ def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
549
+ gateway_check
550
+
551
+ @activity = activity
552
+ @status = status
553
+ @streamurl = url
554
+ type = url ? 1 : activity_type
555
+
556
+ activity_obj = if type == 4
557
+ {"name" => activity, "type" => type, "state" => activity}
558
+ else
559
+ (activity || url) ? {"name" => activity, "url" => url, "type" => type} : nil
560
+ end
561
+ @gateway.send_status_update(status, since, activity_obj, afk)
562
+
563
+ # Update the status in the cache
564
+ profile.update_presence("status" => status.to_s, "activities" => [activity_obj].compact)
565
+ end
566
+
567
+ # Sets the currently playing game to the specified game.
568
+ # @param name [String] The name of the game to be played.
569
+ # @return [String] The game that is being played now.
570
+ def game=(name)
571
+ gateway_check
572
+ update_status(@status, name, nil)
573
+ end
574
+
575
+ alias_method :playing=, :game=
576
+
577
+ # Sets the current listening status to the specified name.
578
+ # @param name [String] The thing to be listened to.
579
+ # @return [String] The thing that is now being listened to.
580
+ def listening=(name)
581
+ gateway_check
582
+ update_status(@status, name, nil, nil, nil, 2)
583
+ end
584
+
585
+ # Sets the current watching status to the specified name.
586
+ # @param name [String] The thing to be watched.
587
+ # @return [String] The thing that is now being watched.
588
+ def watching=(name)
589
+ gateway_check
590
+ update_status(@status, name, nil, nil, nil, 3)
591
+ end
592
+
593
+ # Sets the currently online stream to the specified name and Twitch URL.
594
+ # @param name [String] The name of the stream to display.
595
+ # @param url [String] The url of the current Twitch stream.
596
+ # @return [String] The stream name that is being displayed now.
597
+ def stream(name, url)
598
+ gateway_check
599
+ update_status(@status, name, url)
600
+ name
601
+ end
602
+
603
+ # Sets the currently competing status to the specified name.
604
+ # @param name [String] The name of the game to be competing in.
605
+ # @return [String] The game that is being competed in now.
606
+ def competing=(name)
607
+ gateway_check
608
+ update_status(@status, name, nil, nil, nil, 5)
609
+ end
610
+
611
+ # Sets the currently custom status to the specified name.
612
+ # @param name [String] The custom status.
613
+ # @return [String] The custom status that is being used now.
614
+ def custom_status=(name)
615
+ gateway_check
616
+ update_status(@status, name, nil, nil, nil, 4)
617
+ end
618
+
619
+ # Sets status to online.
620
+ def online
621
+ gateway_check
622
+ update_status(:online, @activity, @streamurl)
623
+ end
624
+
625
+ alias_method :on, :online
626
+
627
+ # Sets status to idle.
628
+ def idle
629
+ gateway_check
630
+ update_status(:idle, @activity, nil)
631
+ end
632
+
633
+ alias_method :away, :idle
634
+
635
+ # Sets the bot's status to DnD (red icon).
636
+ def dnd
637
+ gateway_check
638
+ update_status(:dnd, @activity, nil)
639
+ end
640
+
641
+ # Sets the bot's status to invisible (appears offline).
642
+ def invisible
643
+ gateway_check
644
+ update_status(:invisible, @activity, nil)
645
+ end
646
+
647
+ # Join a thread
648
+ # @param channel [Channel, Integer, String]
649
+ def join_thread(channel)
650
+ API::Channel.join_thread(@token, channel.resolve_id)
651
+ nil
652
+ end
653
+
654
+ # Leave a thread
655
+ # @param channel [Channel, Integer, String]
656
+ def leave_thread(channel)
657
+ API::Channel.leave_thread(@token, channel.resolve_id)
658
+ nil
659
+ end
660
+
661
+ # Add a member to a thread
662
+ # @param channel [Channel, Integer, String]
663
+ # @param member [Member, Integer, String]
664
+ def add_thread_member(channel, member)
665
+ API::Channel.add_thread_member(@token, channel.resolve_id, member.resolve_id)
666
+ nil
667
+ end
668
+
669
+ # Remove a member from a thread
670
+ # @param channel [Channel, Integer, String]
671
+ # @param member [Member, Integer, String]
672
+ def remove_thread_member(channel, member)
673
+ API::Channel.remove_thread_member(@token, channel.resolve_id, member.resolve_id)
674
+ nil
675
+ end
676
+
677
+ # Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
678
+ def debug=(new_debug)
679
+ LOGGER.debug = new_debug
680
+ end
681
+
682
+ # Sets the logging mode
683
+ # @see Logger#mode=
684
+ def mode=(new_mode)
685
+ LOGGER.mode = new_mode
686
+ end
687
+
688
+ # Prevents the READY packet from being printed regardless of debug mode.
689
+ def suppress_ready_debug
690
+ @prevent_ready = true
691
+ end
692
+
693
+ # Add an await the bot should listen to. For information on awaits, see {Await}.
694
+ # @param key [Symbol] The key that uniquely identifies the await for {AwaitEvent}s to listen to (see {#await}).
695
+ # @param type [Class] The event class that should be listened for.
696
+ # @param attributes [Hash] The attributes the event should check for. The block will only be executed if all attributes match.
697
+ # @yield Is executed when the await is triggered.
698
+ # @yieldparam event [Event] The event object that was triggered.
699
+ # @return [Await] The await that was created.
700
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#add_await!} instead.
701
+ def add_await(key, type, attributes = {}, &block)
702
+ raise "You can't await an AwaitEvent!" if type == Rubycord::Events::AwaitEvent
703
+
704
+ await = Await.new(self, key, type, attributes, block)
705
+ @awaits ||= {}
706
+ @awaits[key] = await
707
+ end
708
+
709
+ # Awaits an event, blocking the current thread until a response is received.
710
+ # @param type [Class] The event class that should be listened for.
711
+ # @option attributes [Numeric] :timeout the amount of time (in seconds) to wait for a response before returning `nil`. Waits forever if omitted.
712
+ # @yield Executed when a matching event is received.
713
+ # @yieldparam event [Event] The event object that was triggered.
714
+ # @yieldreturn [true, false] Whether the event matches extra await criteria described by the block
715
+ # @return [Event, nil] The event object that was triggered, or `nil` if a `timeout` was set and no event was raised in time.
716
+ # @raise [ArgumentError] if `timeout` is given and is not a positive numeric value
717
+ def add_await!(type, attributes = {})
718
+ raise "You can't await an AwaitEvent!" if type == Rubycord::Events::AwaitEvent
719
+
720
+ timeout = attributes[:timeout]
721
+ raise ArgumentError, "Timeout must be a number > 0" if timeout.is_a?(Numeric) && !timeout.positive?
722
+
723
+ mutex = Mutex.new
724
+ cv = ConditionVariable.new
725
+ response = nil
726
+ block = lambda do |event|
727
+ mutex.synchronize do
728
+ response = event
729
+ if block_given?
730
+ result = yield(event)
731
+ cv.signal if result.is_a?(TrueClass)
732
+ else
733
+ cv.signal
734
+ end
735
+ end
736
+ end
737
+
738
+ handler = register_event(type, attributes, block)
739
+
740
+ if timeout
741
+ Thread.new do
742
+ sleep timeout
743
+ mutex.synchronize { cv.signal }
744
+ end
745
+ end
746
+
747
+ mutex.synchronize { cv.wait(mutex) }
748
+
749
+ remove_handler(handler)
750
+ raise "ConditionVariable was signaled without returning an event!" if response.nil? && timeout.nil?
751
+
752
+ response
753
+ end
754
+
755
+ # Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.
756
+ # @note Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and
757
+ # presence and any other events will still be received.
758
+ # @param user [User, String, Integer] The user, or its ID, to be ignored.
759
+ def ignore_user(user)
760
+ @ignored_ids << user.resolve_id
761
+ end
762
+
763
+ # Remove a user from the ignore list.
764
+ # @param user [User, String, Integer] The user, or its ID, to be unignored.
765
+ def unignore_user(user)
766
+ @ignored_ids.delete(user.resolve_id)
767
+ end
768
+
769
+ # Checks whether a user is being ignored.
770
+ # @param user [User, String, Integer] The user, or its ID, to check.
771
+ # @return [true, false] whether or not the user is ignored.
772
+ def ignored?(user)
773
+ @ignored_ids.include?(user.resolve_id)
774
+ end
775
+
776
+ # @see Logger#debug
777
+ def debug(message)
778
+ LOGGER.debug(message)
779
+ end
780
+
781
+ # @see Logger#log_exception
782
+ def log_exception(e)
783
+ LOGGER.log_exception(e)
784
+ end
785
+
786
+ # Dispatches an event to this bot. Called by the gateway connection handler used internally.
787
+ def dispatch(type, data)
788
+ handle_dispatch(type, data)
789
+ end
790
+
791
+ # Raises a heartbeat event. Called by the gateway connection handler used internally.
792
+ def raise_heartbeat_event
793
+ raise_event(HeartbeatEvent.new(self))
794
+ end
795
+
796
+ # Makes the bot leave any groups with no recipients remaining
797
+ def prune_empty_groups
798
+ @channels.each_value do |channel|
799
+ channel.leave_group if channel.group? && channel.recipients.empty?
800
+ end
801
+ end
802
+
803
+ # Get all application commands.
804
+ # @param server_id [String, Integer, nil] The ID of the server to get the commands from. Global if `nil`.
805
+ # @return [Array<ApplicationCommand>]
806
+ def get_application_commands(server_id: nil)
807
+ resp = if server_id
808
+ API::Application.get_guild_commands(@token, profile.id, server_id)
809
+ else
810
+ API::Application.get_global_commands(@token, profile.id)
811
+ end
812
+
813
+ JSON.parse(resp).map do |command_data|
814
+ ApplicationCommand.new(command_data, self, server_id)
815
+ end
816
+ end
817
+
818
+ # Get an application command by ID.
819
+ # @param command_id [String, Integer]
820
+ # @param server_id [String, Integer, nil] The ID of the server to get the command from. Global if `nil`.
821
+ def get_application_command(command_id, server_id: nil)
822
+ resp = if server_id
823
+ API::Application.get_guild_command(@token, profile.id, server_id, command_id)
824
+ else
825
+ API::Application.get_global_command(@token, profile.id, command_id)
826
+ end
827
+ ApplicationCommand.new(JSON.parse(resp), self, server_id)
828
+ end
829
+
830
+ # @yieldparam [OptionBuilder]
831
+ # @yieldparam [PermissionBuilder]
832
+ # @example
833
+ # bot.register_application_command(:reddit, 'Reddit Commands') do |cmd|
834
+ # cmd.subcommand_group(:subreddit, 'Subreddit Commands') do |group|
835
+ # group.subcommand(:hot, "What's trending") do |sub|
836
+ # sub.string(:subreddit, 'Subreddit to search')
837
+ # end
838
+ # group.subcommand(:new, "What's new") do |sub|
839
+ # sub.string(:since, 'How long ago', choices: ['this hour', 'today', 'this week', 'this month', 'this year', 'all time'])
840
+ # sub.string(:subreddit, 'Subreddit to search')
841
+ # end
842
+ # end
843
+ # end
844
+ def register_application_command(name, description, server_id: nil, default_permission: nil, type: :chat_input)
845
+ type = ApplicationCommand::TYPES[type] || type
846
+
847
+ builder = Interactions::OptionBuilder.new
848
+ permission_builder = Interactions::PermissionBuilder.new
849
+ yield(builder, permission_builder) if block_given?
850
+
851
+ resp = if server_id
852
+ API::Application.create_guild_command(@token, profile.id, server_id, name, description, builder.to_a, default_permission, type)
853
+ else
854
+ API::Application.create_global_command(@token, profile.id, name, description, builder.to_a, default_permission, type)
855
+ end
856
+ cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
857
+
858
+ if permission_builder.to_a.any?
859
+ raise ArgumentError, "Permissions can only be set for guild commands" unless server_id
860
+
861
+ edit_application_command_permissions(cmd.id, server_id, permission_builder.to_a)
862
+ end
863
+
864
+ cmd
865
+ end
866
+
867
+ # @yieldparam [OptionBuilder]
868
+ # @yieldparam [PermissionBuilder]
869
+ def edit_application_command(command_id, server_id: nil, name: nil, description: nil, default_permission: nil, type: :chat_input)
870
+ type = ApplicationCommand::TYPES[type] || type
871
+
872
+ builder = Interactions::OptionBuilder.new
873
+ permission_builder = Interactions::PermissionBuilder.new
874
+
875
+ yield(builder, permission_builder) if block_given?
876
+
877
+ resp = if server_id
878
+ API::Application.edit_guild_command(@token, profile.id, server_id, command_id, name, description, builder.to_a, default_permission, type)
879
+ else
880
+ API::Application.edit_guild_command(@token, profile.id, command_id, name, description, builder.to_a, default_permission.type)
881
+ end
882
+ cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
883
+
884
+ if permission_builder.to_a.any?
885
+ raise ArgumentError, "Permissions can only be set for guild commands" unless server_id
886
+
887
+ edit_application_command_permissions(cmd.id, server_id, permission_builder.to_a)
888
+ end
889
+
890
+ cmd
891
+ end
892
+
893
+ # Remove an application command from the commands registered with discord.
894
+ # @param command_id [String, Integer] The ID of the command to remove.
895
+ # @param server_id [String, Integer] The ID of the server to delete this command from, global if `nil`.
896
+ def delete_application_command(command_id, server_id: nil)
897
+ if server_id
898
+ API::Application.delete_guild_command(@token, profile.id, server_id, command_id)
899
+ else
900
+ API::Application.delete_global_command(@token, profile.id, command_id)
901
+ end
902
+ end
903
+
904
+ # @param command_id [Integer, String]
905
+ # @param server_id [Integer, String]
906
+ # @param permissions [Array<Hash>] An array of objects formatted as `{ id: ENTITY_ID, type: 1 or 2, permission: true or false }`
907
+ def edit_application_command_permissions(command_id, server_id, permissions = [])
908
+ builder = Interactions::PermissionBuilder.new
909
+ yield builder if block_given?
910
+
911
+ permissions += builder.to_a
912
+ API::Application.edit_guild_command_permissions(@token, profile.id, server_id, command_id, permissions)
913
+ end
914
+
915
+ # The inspect method is overwritten to reduce superfluous data
916
+ def inspect
917
+ "#<Bot @name=#{@name.inspect} @intents=#{@intents.inspect} @shard_key=#{@shard_key.inspect}>"
918
+ end
919
+
920
+ private
921
+
922
+ # Throws a useful exception if there's currently no gateway connection.
923
+ def gateway_check
924
+ raise "A gateway connection is necessary to call this method! You'll have to do it inside any event (e.g. `ready`) or after `bot.run :async`." unless connected?
925
+ end
926
+
927
+ # Logs a warning if there are servers which are still unavailable.
928
+ # e.g. due to a Discord outage or because the servers are large and taking a while to load.
929
+ def unavailable_servers_check
930
+ # Return unless there are servers that are unavailable.
931
+ return unless @unavailable_servers&.positive?
932
+
933
+ LOGGER.warn("#{@unavailable_servers} servers haven't been cached yet.")
934
+ LOGGER.warn("Servers may be unavailable due to an outage, or your bot is on very large servers that are taking a while to load.")
935
+ end
936
+
937
+ ### ## ## ######## ######## ######## ## ## ### ## ######
938
+ ## ### ## ## ## ## ## ### ## ## ## ## ## ##
939
+ ## #### ## ## ## ## ## #### ## ## ## ## ##
940
+ ## ## ## ## ## ###### ######## ## ## ## ## ## ## ######
941
+ ## ## #### ## ## ## ## ## #### ######### ## ##
942
+ ## ## ### ## ## ## ## ## ### ## ## ## ## ##
943
+ ### ## ## ## ######## ## ## ## ## ## ## ######## ######
944
+
945
+ # Internal handler for PRESENCE_UPDATE
946
+ def update_presence(data)
947
+ # Friends list presences have no server ID so ignore these to not cause an error
948
+ return unless data["guild_id"]
949
+
950
+ user_id = data["user"]["id"].to_i
951
+ server_id = data["guild_id"].to_i
952
+ server = server(server_id)
953
+ return unless server
954
+
955
+ member_is_new = false
956
+
957
+ if server.member_cached?(user_id)
958
+ member = server.member(user_id)
959
+ else
960
+ # If the member is not cached yet, it means that it just came online from not being cached at all
961
+ # due to large_threshold. Fortunately, Discord sends the entire member object in this case, and
962
+ # not just a part of it - we can just cache this member directly
963
+ member = Member.new(data, server, self)
964
+ debug("Implicitly adding presence-obtained member #{user_id} to #{server_id} cache")
965
+
966
+ member_is_new = true
967
+ end
968
+
969
+ username = data["user"]["username"]
970
+ if username && !member_is_new # Don't set the username for newly-cached members
971
+ debug "Implicitly updating presence-obtained information username for member #{user_id}"
972
+ member.update_username(username)
973
+ end
974
+
975
+ global_name = data["user"]["global_name"]
976
+ if global_name && !member_is_new # Don't set the global_name for newly-cached members
977
+ debug "Implicitly updating presence-obtained information global_name for member #{user_id}"
978
+ member.update_global_name(global_name)
979
+ end
980
+
981
+ member.update_presence(data)
982
+
983
+ member.avatar_id = data["user"]["avatar"] if data["user"]["avatar"]
984
+
985
+ server.cache_member(member)
986
+ end
987
+
988
+ # Internal handler for VOICE_STATE_UPDATE
989
+ def update_voice_state(data)
990
+ @session_id = data["session_id"]
991
+
992
+ server_id = data["guild_id"].to_i
993
+ server = server(server_id)
994
+ return unless server
995
+
996
+ user_id = data["user_id"].to_i
997
+ old_voice_state = server.voice_states[user_id]
998
+ old_channel_id = old_voice_state.voice_channel&.id if old_voice_state
999
+
1000
+ server.update_voice_state(data)
1001
+
1002
+ existing_voice = @voices[server_id]
1003
+ if user_id == @profile.id && existing_voice
1004
+ new_channel_id = data["channel_id"]
1005
+ if new_channel_id
1006
+ new_channel = channel(new_channel_id)
1007
+ existing_voice.channel = new_channel
1008
+ else
1009
+ voice_destroy(server_id)
1010
+ end
1011
+ end
1012
+
1013
+ old_channel_id
1014
+ end
1015
+
1016
+ # Internal handler for VOICE_SERVER_UPDATE
1017
+ def update_voice_server(data)
1018
+ server_id = data["guild_id"].to_i
1019
+ channel = @should_connect_to_voice[server_id]
1020
+
1021
+ debug("Voice server update received! chan: #{channel.inspect}")
1022
+ return unless channel
1023
+
1024
+ @should_connect_to_voice.delete(server_id)
1025
+ debug("Updating voice server!")
1026
+
1027
+ token = data["token"]
1028
+ endpoint = data["endpoint"]
1029
+
1030
+ unless endpoint
1031
+ debug("VOICE_SERVER_UPDATE sent with nil endpoint! Ignoring")
1032
+ return
1033
+ end
1034
+
1035
+ debug("Got data, now creating the bot.")
1036
+ @voices[server_id] = Rubycord::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint)
1037
+ end
1038
+
1039
+ # Internal handler for CHANNEL_CREATE
1040
+ def create_channel(data)
1041
+ channel = data.is_a?(Rubycord::Channel) ? data : Channel.new(data, self)
1042
+ server = channel.server
1043
+
1044
+ # Handle normal and private channels separately
1045
+ if server
1046
+ server.add_channel(channel)
1047
+ @channels[channel.id] = channel
1048
+ elsif channel.private?
1049
+ @pm_channels[channel.recipient.id] = channel
1050
+ elsif channel.group?
1051
+ @channels[channel.id] = channel
1052
+ end
1053
+ end
1054
+
1055
+ # Internal handler for CHANNEL_UPDATE
1056
+ def update_channel(data)
1057
+ channel = Channel.new(data, self)
1058
+ old_channel = @channels[channel.id]
1059
+ return unless old_channel
1060
+
1061
+ old_channel.update_from(channel)
1062
+ end
1063
+
1064
+ # Internal handler for CHANNEL_DELETE
1065
+ def delete_channel(data)
1066
+ channel = Channel.new(data, self)
1067
+ server = channel.server
1068
+
1069
+ # Handle normal and private channels separately
1070
+ if server
1071
+ @channels.delete(channel.id)
1072
+ server.delete_channel(channel.id)
1073
+ elsif channel.pm?
1074
+ @pm_channels.delete(channel.recipient.id)
1075
+ elsif channel.group?
1076
+ @channels.delete(channel.id)
1077
+ end
1078
+
1079
+ @thread_members.delete(channel.id) if channel.thread?
1080
+ end
1081
+
1082
+ # Internal handler for CHANNEL_RECIPIENT_ADD
1083
+ def add_recipient(data)
1084
+ channel_id = data["channel_id"].to_i
1085
+ channel = self.channel(channel_id)
1086
+
1087
+ recipient_user = ensure_user(data["user"])
1088
+ recipient = Recipient.new(recipient_user, channel, self)
1089
+ channel.add_recipient(recipient)
1090
+ end
1091
+
1092
+ # Internal handler for CHANNEL_RECIPIENT_REMOVE
1093
+ def remove_recipient(data)
1094
+ channel_id = data["channel_id"].to_i
1095
+ channel = self.channel(channel_id)
1096
+
1097
+ recipient_user = ensure_user(data["user"])
1098
+ recipient = Recipient.new(recipient_user, channel, self)
1099
+ channel.remove_recipient(recipient)
1100
+ end
1101
+
1102
+ # Internal handler for GUILD_MEMBER_ADD
1103
+ def add_guild_member(data)
1104
+ server_id = data["guild_id"].to_i
1105
+ server = self.server(server_id)
1106
+
1107
+ member = Member.new(data, server, self)
1108
+ server.add_member(member)
1109
+ end
1110
+
1111
+ # Internal handler for GUILD_MEMBER_UPDATE
1112
+ def update_guild_member(data)
1113
+ server_id = data["guild_id"].to_i
1114
+ server = self.server(server_id)
1115
+
1116
+ member = server.member(data["user"]["id"].to_i)
1117
+ member.update_roles(data["roles"])
1118
+ member.update_nick(data["nick"])
1119
+ member.update_global_name(data["user"]["global_name"]) if data["user"]["global_name"]
1120
+ member.update_boosting_since(data["premium_since"])
1121
+ member.update_communication_disabled_until(data["communication_disabled_until"])
1122
+ end
1123
+
1124
+ # Internal handler for GUILD_MEMBER_DELETE
1125
+ def delete_guild_member(data)
1126
+ server_id = data["guild_id"].to_i
1127
+ server = self.server(server_id)
1128
+ return unless server
1129
+
1130
+ user_id = data["user"]["id"].to_i
1131
+ server.delete_member(user_id)
1132
+ rescue Rubycord::Errors::NoPermission
1133
+ Rubycord::LOGGER.warn("delete_guild_member attempted to access a server for which the bot doesn't have permission! Not sure what happened here, ignoring")
1134
+ end
1135
+
1136
+ # Internal handler for GUILD_CREATE
1137
+ def create_guild(data)
1138
+ ensure_server(data, true)
1139
+ end
1140
+
1141
+ # Internal handler for GUILD_UPDATE
1142
+ def update_guild(data)
1143
+ @servers[data["id"].to_i].update_data(data)
1144
+ end
1145
+
1146
+ # Internal handler for GUILD_DELETE
1147
+ def delete_guild(data)
1148
+ id = data["id"].to_i
1149
+ @servers.delete(id)
1150
+ end
1151
+
1152
+ # Internal handler for GUILD_ROLE_UPDATE
1153
+ def update_guild_role(data)
1154
+ role_data = data["role"]
1155
+ server_id = data["guild_id"].to_i
1156
+ server = @servers[server_id]
1157
+ new_role = Role.new(role_data, self, server)
1158
+ role_id = role_data["id"].to_i
1159
+ old_role = server.roles.find { |r| r.id == role_id }
1160
+ old_role.update_from(new_role)
1161
+ end
1162
+
1163
+ # Internal handler for GUILD_ROLE_CREATE
1164
+ def create_guild_role(data)
1165
+ role_data = data["role"]
1166
+ server_id = data["guild_id"].to_i
1167
+ server = @servers[server_id]
1168
+ new_role = Role.new(role_data, self, server)
1169
+ existing_role = server.role(new_role.id)
1170
+ if existing_role
1171
+ existing_role.update_from(new_role)
1172
+ else
1173
+ server.add_role(new_role)
1174
+ end
1175
+ end
1176
+
1177
+ # Internal handler for GUILD_ROLE_DELETE
1178
+ def delete_guild_role(data)
1179
+ role_id = data["role_id"].to_i
1180
+ server_id = data["guild_id"].to_i
1181
+ server = @servers[server_id]
1182
+ server.delete_role(role_id)
1183
+ end
1184
+
1185
+ # Internal handler for GUILD_EMOJIS_UPDATE
1186
+ def update_guild_emoji(data)
1187
+ server_id = data["guild_id"].to_i
1188
+ server = @servers[server_id]
1189
+ server.update_emoji_data(data)
1190
+ end
1191
+
1192
+ # Internal handler for MESSAGE_CREATE
1193
+ def create_message(data)
1194
+ end
1195
+
1196
+ # Internal handler for TYPING_START
1197
+ def start_typing(data)
1198
+ end
1199
+
1200
+ # Internal handler for MESSAGE_UPDATE
1201
+ def update_message(data)
1202
+ end
1203
+
1204
+ # Internal handler for MESSAGE_DELETE
1205
+ def delete_message(data)
1206
+ end
1207
+
1208
+ # Internal handler for MESSAGE_REACTION_ADD
1209
+ def add_message_reaction(data)
1210
+ end
1211
+
1212
+ # Internal handler for MESSAGE_REACTION_REMOVE
1213
+ def remove_message_reaction(data)
1214
+ end
1215
+
1216
+ # Internal handler for MESSAGE_REACTION_REMOVE_ALL
1217
+ def remove_all_message_reactions(data)
1218
+ end
1219
+
1220
+ # Internal handler for GUILD_BAN_ADD
1221
+ def add_user_ban(data)
1222
+ end
1223
+
1224
+ # Internal handler for GUILD_BAN_REMOVE
1225
+ def remove_user_ban(data)
1226
+ end
1227
+
1228
+ ## ####### ###### #### ## ##
1229
+ ## ## ## ## ## ## ### ##
1230
+ ## ## ## ## ## #### ##
1231
+ ## ## ## ## #### ## ## ## ##
1232
+ ## ## ## ## ## ## ## ####
1233
+ ## ## ## ## ## ## ## ###
1234
+ ######## ####### ###### #### ## ##
1235
+
1236
+ def process_token(type, token)
1237
+ # Remove the "Bot " prefix if it exists
1238
+ token = token[4..] if token.start_with? "Bot "
1239
+
1240
+ token = "Bot #{token}" unless type == :user
1241
+ token
1242
+ end
1243
+
1244
+ def handle_dispatch(type, data)
1245
+ # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
1246
+ if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !((@intents || 0) & INTENTS[:servers]).zero?
1247
+ # The server streaming timed out!
1248
+ LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
1249
+ LOGGER.debug("Calling ready now because server loading is taking a long time. Servers may be unavailable due to an outage, or your bot is on very large servers.")
1250
+
1251
+ # Unset the unavailable server count so this doesn't get triggered again
1252
+ @unavailable_servers = 0
1253
+
1254
+ notify_ready
1255
+ end
1256
+
1257
+ case type
1258
+ when :READY
1259
+ # As READY may be called multiple times over a single process lifetime, we here need to reset the cache entirely
1260
+ # to prevent possible inconsistencies, like objects referencing old versions of other objects which have been
1261
+ # replaced.
1262
+ init_cache
1263
+
1264
+ @profile = Profile.new(data["user"], self)
1265
+
1266
+ # Initialize servers
1267
+ @servers = {}
1268
+
1269
+ # Count unavailable servers
1270
+ @unavailable_servers = 0
1271
+
1272
+ data["guilds"].each do |element|
1273
+ # Check for true specifically because unavailable=false indicates that a previously unavailable server has
1274
+ # come online
1275
+ if element["unavailable"]
1276
+ @unavailable_servers += 1
1277
+
1278
+ # Ignore any unavailable servers
1279
+ next
1280
+ end
1281
+
1282
+ ensure_server(element, true)
1283
+ end
1284
+
1285
+ # Add PM and group channels
1286
+ data["private_channels"].each do |element|
1287
+ channel = ensure_channel(element)
1288
+ if channel.pm?
1289
+ @pm_channels[channel.recipient.id] = channel
1290
+ else
1291
+ @channels[channel.id] = channel
1292
+ end
1293
+ end
1294
+
1295
+ # Don't notify yet if there are unavailable servers because they need to get available before the bot truly has
1296
+ # all the data
1297
+ if @unavailable_servers.zero?
1298
+ # No unavailable servers - we're ready!
1299
+ notify_ready
1300
+ end
1301
+
1302
+ @ready_time = Time.now
1303
+ @unavailable_timeout_time = Time.now
1304
+ when :GUILD_MEMBERS_CHUNK
1305
+ id = data["guild_id"].to_i
1306
+ server = server(id)
1307
+ server.process_chunk(data["members"], data["chunk_index"], data["chunk_count"])
1308
+ when :INVITE_CREATE
1309
+ invite = Invite.new(data, self)
1310
+ raise_event(InviteCreateEvent.new(data, invite, self))
1311
+ when :INVITE_DELETE
1312
+ raise_event(InviteDeleteEvent.new(data, self))
1313
+ when :MESSAGE_CREATE
1314
+ if ignored?(data["author"]["id"])
1315
+ debug("Ignored author with ID #{data["author"]["id"]}")
1316
+ return
1317
+ end
1318
+
1319
+ if @ignore_bots && data["author"]["bot"]
1320
+ debug("Ignored Bot account with ID #{data["author"]["id"]}")
1321
+ return
1322
+ end
1323
+
1324
+ # If create_message is overwritten with a method that returns the parsed message, use that instead, so we don't
1325
+ # parse the message twice (which is just thrown away performance)
1326
+ message = create_message(data)
1327
+ message = Message.new(data, self) unless message.is_a? Message
1328
+
1329
+ return if message.from_bot? && !should_parse_self
1330
+
1331
+ # Dispatch a ChannelCreateEvent for channels we don't have cached
1332
+ if message.channel.private? && @pm_channels[message.channel.recipient.id].nil?
1333
+ create_channel(message.channel)
1334
+
1335
+ raise_event(ChannelCreateEvent.new(message.channel, self))
1336
+ end
1337
+
1338
+ event = MessageEvent.new(message, self)
1339
+ raise_event(event)
1340
+
1341
+ if message.mentions.any? { |user| user.id == @profile.id }
1342
+ event = MentionEvent.new(message, self)
1343
+ raise_event(event)
1344
+ end
1345
+
1346
+ if message.channel.private?
1347
+ event = PrivateMessageEvent.new(message, self)
1348
+ raise_event(event)
1349
+ end
1350
+ when :MESSAGE_UPDATE
1351
+ update_message(data)
1352
+
1353
+ message = Message.new(data, self)
1354
+
1355
+ event = MessageUpdateEvent.new(message, self)
1356
+ raise_event(event)
1357
+
1358
+ return if message.from_bot? && !should_parse_self
1359
+
1360
+ unless message.author
1361
+ LOGGER.debug("Edited a message with nil author! Content: #{message.content.inspect}, channel: #{message.channel.inspect}")
1362
+ return
1363
+ end
1364
+
1365
+ event = MessageEditEvent.new(message, self)
1366
+ raise_event(event)
1367
+ when :MESSAGE_DELETE
1368
+ delete_message(data)
1369
+
1370
+ event = MessageDeleteEvent.new(data, self)
1371
+ raise_event(event)
1372
+ when :MESSAGE_DELETE_BULK
1373
+ debug("MESSAGE_DELETE_BULK will raise #{data["ids"].length} events")
1374
+
1375
+ data["ids"].each do |single_id|
1376
+ # Form a data hash for a single ID so the methods get what they want
1377
+ single_data = {
1378
+ "id" => single_id,
1379
+ "channel_id" => data["channel_id"]
1380
+ }
1381
+
1382
+ # Raise as normal
1383
+ delete_message(single_data)
1384
+
1385
+ event = MessageDeleteEvent.new(single_data, self)
1386
+ raise_event(event)
1387
+ end
1388
+ when :TYPING_START
1389
+ start_typing(data)
1390
+
1391
+ begin
1392
+ event = TypingEvent.new(data, self)
1393
+ raise_event(event)
1394
+ rescue Rubycord::Errors::NoPermission
1395
+ debug "Typing started in channel the bot has no access to, ignoring"
1396
+ end
1397
+ when :MESSAGE_REACTION_ADD
1398
+ add_message_reaction(data)
1399
+
1400
+ return if profile.id == data["user_id"].to_i && !should_parse_self
1401
+
1402
+ event = ReactionAddEvent.new(data, self)
1403
+ raise_event(event)
1404
+ when :MESSAGE_REACTION_REMOVE
1405
+ remove_message_reaction(data)
1406
+
1407
+ return if profile.id == data["user_id"].to_i && !should_parse_self
1408
+
1409
+ event = ReactionRemoveEvent.new(data, self)
1410
+ raise_event(event)
1411
+ when :MESSAGE_REACTION_REMOVE_ALL
1412
+ remove_all_message_reactions(data)
1413
+
1414
+ event = ReactionRemoveAllEvent.new(data, self)
1415
+ raise_event(event)
1416
+ when :PRESENCE_UPDATE
1417
+ # Ignore friends list presences
1418
+ return unless data["guild_id"]
1419
+
1420
+ new_activities = (data["activities"] || []).map { |act_data| Activity.new(act_data, self) }
1421
+ presence_user = @users[data["user"]["id"].to_i]
1422
+ old_activities = presence_user&.activities || []
1423
+ update_presence(data)
1424
+
1425
+ # Starting a new game
1426
+ playing_change = new_activities.reject do |act|
1427
+ old_activities.find { |old| old.name == act.name }
1428
+ end
1429
+
1430
+ # Exiting an existing game
1431
+ playing_change += old_activities.reject do |old|
1432
+ new_activities.find { |act| act.name == old.name }
1433
+ end
1434
+
1435
+ if playing_change.any?
1436
+ playing_change.each do |act|
1437
+ raise_event(PlayingEvent.new(data, act, self))
1438
+ end
1439
+ else
1440
+ raise_event(PresenceEvent.new(data, self))
1441
+ end
1442
+ when :VOICE_STATE_UPDATE
1443
+ old_channel_id = update_voice_state(data)
1444
+
1445
+ event = VoiceStateUpdateEvent.new(data, old_channel_id, self)
1446
+ raise_event(event)
1447
+ when :VOICE_SERVER_UPDATE
1448
+ update_voice_server(data)
1449
+
1450
+ event = VoiceServerUpdateEvent.new(data, self)
1451
+ raise_event(event)
1452
+ when :CHANNEL_CREATE
1453
+ create_channel(data)
1454
+
1455
+ event = ChannelCreateEvent.new(data, self)
1456
+ raise_event(event)
1457
+ when :CHANNEL_UPDATE
1458
+ update_channel(data)
1459
+
1460
+ event = ChannelUpdateEvent.new(data, self)
1461
+ raise_event(event)
1462
+ when :CHANNEL_DELETE
1463
+ delete_channel(data)
1464
+
1465
+ event = ChannelDeleteEvent.new(data, self)
1466
+ raise_event(event)
1467
+ when :CHANNEL_RECIPIENT_ADD
1468
+ add_recipient(data)
1469
+
1470
+ event = ChannelRecipientAddEvent.new(data, self)
1471
+ raise_event(event)
1472
+ when :CHANNEL_RECIPIENT_REMOVE
1473
+ remove_recipient(data)
1474
+
1475
+ event = ChannelRecipientRemoveEvent.new(data, self)
1476
+ raise_event(event)
1477
+ when :GUILD_MEMBER_ADD
1478
+ add_guild_member(data)
1479
+
1480
+ event = ServerMemberAddEvent.new(data, self)
1481
+ raise_event(event)
1482
+ when :GUILD_MEMBER_UPDATE
1483
+ update_guild_member(data)
1484
+
1485
+ event = ServerMemberUpdateEvent.new(data, self)
1486
+ raise_event(event)
1487
+ when :GUILD_MEMBER_REMOVE
1488
+ delete_guild_member(data)
1489
+
1490
+ event = ServerMemberDeleteEvent.new(data, self)
1491
+ raise_event(event)
1492
+ when :GUILD_BAN_ADD
1493
+ add_user_ban(data)
1494
+
1495
+ event = UserBanEvent.new(data, self)
1496
+ raise_event(event)
1497
+ when :GUILD_BAN_REMOVE
1498
+ remove_user_ban(data)
1499
+
1500
+ event = UserUnbanEvent.new(data, self)
1501
+ raise_event(event)
1502
+ when :GUILD_ROLE_UPDATE
1503
+ update_guild_role(data)
1504
+
1505
+ event = ServerRoleUpdateEvent.new(data, self)
1506
+ raise_event(event)
1507
+ when :GUILD_ROLE_CREATE
1508
+ create_guild_role(data)
1509
+
1510
+ event = ServerRoleCreateEvent.new(data, self)
1511
+ raise_event(event)
1512
+ when :GUILD_ROLE_DELETE
1513
+ delete_guild_role(data)
1514
+
1515
+ event = ServerRoleDeleteEvent.new(data, self)
1516
+ raise_event(event)
1517
+ when :GUILD_CREATE
1518
+ create_guild(data)
1519
+
1520
+ # Check for false specifically (no data means the server has never been unavailable)
1521
+ if data["unavailable"].is_a? FalseClass
1522
+ @unavailable_servers -= 1 if @unavailable_servers
1523
+ @unavailable_timeout_time = Time.now
1524
+
1525
+ notify_ready if @unavailable_servers.zero?
1526
+
1527
+ # Return here so the event doesn't get triggered
1528
+ return
1529
+ end
1530
+
1531
+ event = ServerCreateEvent.new(data, self)
1532
+ raise_event(event)
1533
+ when :GUILD_UPDATE
1534
+ update_guild(data)
1535
+
1536
+ event = ServerUpdateEvent.new(data, self)
1537
+ raise_event(event)
1538
+ when :GUILD_DELETE
1539
+ delete_guild(data)
1540
+
1541
+ if data["unavailable"].is_a? TrueClass
1542
+ LOGGER.warn("Server #{data["id"]} is unavailable due to an outage!")
1543
+ return # Don't raise an event
1544
+ end
1545
+
1546
+ event = ServerDeleteEvent.new(data, self)
1547
+ raise_event(event)
1548
+ when :GUILD_EMOJIS_UPDATE
1549
+ server_id = data["guild_id"].to_i
1550
+ server = @servers[server_id]
1551
+ old_emoji_data = server.emoji.clone
1552
+ update_guild_emoji(data)
1553
+ new_emoji_data = server.emoji
1554
+
1555
+ created_ids = new_emoji_data.keys - old_emoji_data.keys
1556
+ deleted_ids = old_emoji_data.keys - new_emoji_data.keys
1557
+ updated_ids = old_emoji_data.select do |k, v|
1558
+ new_emoji_data[k] && (v.name != new_emoji_data[k].name || v.roles != new_emoji_data[k].roles)
1559
+ end.keys
1560
+
1561
+ event = ServerEmojiChangeEvent.new(server, data, self)
1562
+ raise_event(event)
1563
+
1564
+ created_ids.each do |e|
1565
+ event = ServerEmojiCreateEvent.new(server, new_emoji_data[e], self)
1566
+ raise_event(event)
1567
+ end
1568
+
1569
+ deleted_ids.each do |e|
1570
+ event = ServerEmojiDeleteEvent.new(server, old_emoji_data[e], self)
1571
+ raise_event(event)
1572
+ end
1573
+
1574
+ updated_ids.each do |e|
1575
+ event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1576
+ raise_event(event)
1577
+ end
1578
+ when :INTERACTION_CREATE
1579
+ event = InteractionCreateEvent.new(data, self)
1580
+ raise_event(event)
1581
+
1582
+ case data["type"]
1583
+ when Interaction::TYPES[:command]
1584
+ event = ApplicationCommandEvent.new(data, self)
1585
+
1586
+ Thread.new do
1587
+ Thread.current[:rubycord_name] = "it-#{event.interaction.id}"
1588
+
1589
+ begin
1590
+ debug("Executing application command #{event.command_name}:#{event.command_id}")
1591
+
1592
+ @application_commands[event.command_name]&.call(event)
1593
+ rescue => e
1594
+ log_exception(e)
1595
+ end
1596
+ end
1597
+ when Interaction::TYPES[:component]
1598
+ case data["data"]["component_type"]
1599
+ when Webhooks::View::COMPONENT_TYPES[:button]
1600
+ event = ButtonEvent.new(data, self)
1601
+
1602
+ raise_event(event)
1603
+ when Webhooks::View::COMPONENT_TYPES[:string_select]
1604
+ event = StringSelectEvent.new(data, self)
1605
+
1606
+ raise_event(event)
1607
+ when Webhooks::View::COMPONENT_TYPES[:user_select]
1608
+ event = UserSelectEvent.new(data, self)
1609
+
1610
+ raise_event(event)
1611
+ when Webhooks::View::COMPONENT_TYPES[:role_select]
1612
+ event = RoleSelectEvent.new(data, self)
1613
+
1614
+ raise_event(event)
1615
+ when Webhooks::View::COMPONENT_TYPES[:mentionable_select]
1616
+ event = MentionableSelectEvent.new(data, self)
1617
+
1618
+ raise_event(event)
1619
+ when Webhooks::View::COMPONENT_TYPES[:channel_select]
1620
+ event = ChannelSelectEvent.new(data, self)
1621
+
1622
+ raise_event(event)
1623
+ end
1624
+ when Interaction::TYPES[:modal_submit]
1625
+
1626
+ event = ModalSubmitEvent.new(data, self)
1627
+ raise_event(event)
1628
+ end
1629
+ when :WEBHOOKS_UPDATE
1630
+ event = WebhookUpdateEvent.new(data, self)
1631
+ raise_event(event)
1632
+ when :THREAD_CREATE
1633
+ create_channel(data)
1634
+
1635
+ event = ThreadCreateEvent.new(data, self)
1636
+ raise_event(event)
1637
+ when :THREAD_UPDATE
1638
+ update_channel(data)
1639
+
1640
+ event = ThreadUpdateEvent.new(data, self)
1641
+ raise_event(event)
1642
+ when :THREAD_DELETE
1643
+ delete_channel(data)
1644
+ @thread_members.delete(data["id"]&.resolve_id)
1645
+
1646
+ # raise ThreadDeleteEvent
1647
+ when :THREAD_LIST_SYNC
1648
+ data["members"].map { |member| ensure_thread_member(member) }
1649
+ data["threads"].map { |channel| ensure_channel(channel, data["guild_id"]) }
1650
+
1651
+ # raise ThreadListSyncEvent?
1652
+ when :THREAD_MEMBER_UPDATE
1653
+ ensure_thread_member(data)
1654
+ when :THREAD_MEMBERS_UPDATE
1655
+ data["added_members"]&.each do |added_member|
1656
+ ensure_thread_member(added_member) if added_member["user_id"]
1657
+ end
1658
+
1659
+ data["removed_member_ids"]&.each do |member_id|
1660
+ @thread_members[data["id"]&.resolve_id]&.delete(member_id&.resolve_id)
1661
+ end
1662
+
1663
+ event = ThreadMembersUpdateEvent.new(data, self)
1664
+ raise_event(event)
1665
+ else
1666
+ # another event that we don't support yet
1667
+ debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
1668
+
1669
+ event = UnknownEvent.new(type, data, self)
1670
+ raise_event(event)
1671
+ end
1672
+
1673
+ # The existence of this array is checked before for performance reasons, since this has to be done for *every*
1674
+ # dispatch.
1675
+ if @event_handlers && @event_handlers[RawEvent]
1676
+ event = RawEvent.new(type, data, self)
1677
+ raise_event(event)
1678
+ end
1679
+ rescue => e
1680
+ LOGGER.error("Gateway message error!")
1681
+ log_exception(e)
1682
+ end
1683
+
1684
+ # Notifies everything there is to be notified that the connection is now ready
1685
+ def notify_ready
1686
+ # Make sure to raise the event
1687
+ raise_event(ReadyEvent.new(self))
1688
+ LOGGER.good "Ready"
1689
+
1690
+ @gateway.notify_ready
1691
+ end
1692
+
1693
+ def raise_event(event)
1694
+ debug("Raised a #{event.class}")
1695
+ handle_awaits(event)
1696
+
1697
+ @event_handlers ||= {}
1698
+ handlers = @event_handlers[event.class]
1699
+ return unless handlers
1700
+
1701
+ handlers.dup.each do |handler|
1702
+ call_event(handler, event) if handler.matches?(event)
1703
+ end
1704
+ end
1705
+
1706
+ def call_event(handler, event)
1707
+ t = Thread.new do
1708
+ @event_threads ||= []
1709
+ @current_thread ||= 0
1710
+
1711
+ @event_threads << t
1712
+ Thread.current[:rubycord_name] = "et-#{@current_thread += 1}"
1713
+ begin
1714
+ handler.call(event)
1715
+ handler.after_call(event)
1716
+ rescue => e
1717
+ log_exception(e)
1718
+ ensure
1719
+ @event_threads.delete(t)
1720
+ end
1721
+ end
1722
+ end
1723
+
1724
+ def handle_awaits(event)
1725
+ @awaits ||= {}
1726
+ @awaits.each_value do |await|
1727
+ key, should_delete = await.match(event)
1728
+ next unless key
1729
+
1730
+ debug("should_delete: #{should_delete}")
1731
+ @awaits.delete(await.key) if should_delete
1732
+
1733
+ await_event = Rubycord::Events::AwaitEvent.new(await, event, self)
1734
+ raise_event(await_event)
1735
+ end
1736
+ end
1737
+
1738
+ def calculate_intents(intents)
1739
+ intents.reduce(0) do |sum, intent|
1740
+ case intent
1741
+ when Symbol
1742
+ if INTENTS[intent]
1743
+ sum | INTENTS[intent]
1744
+ else
1745
+ LOGGER.warn("Unknown intent: #{intent}")
1746
+ sum
1747
+ end
1748
+ when Integer
1749
+ sum | intent
1750
+ else
1751
+ LOGGER.warn("Invalid intent: #{intent}")
1752
+ sum
1753
+ end
1754
+ end
1755
+ end
1756
+ end
1757
+ end