rubycord 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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