discordrb 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +152 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- data/.github/pull_request_template.md +37 -0
- data/.github/workflows/codeql.yml +65 -0
- data/.markdownlint.json +4 -0
- data/.rubocop.yml +39 -36
- data/CHANGELOG.md +874 -552
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +80 -86
- data/Rakefile +2 -0
- data/bin/console +1 -0
- data/discordrb-webhooks.gemspec +9 -6
- data/discordrb.gemspec +21 -18
- data/lib/discordrb/allowed_mentions.rb +36 -0
- data/lib/discordrb/api/application.rb +202 -0
- data/lib/discordrb/api/channel.rb +236 -47
- data/lib/discordrb/api/interaction.rb +54 -0
- data/lib/discordrb/api/invite.rb +5 -5
- data/lib/discordrb/api/server.rb +94 -66
- data/lib/discordrb/api/user.rb +17 -11
- data/lib/discordrb/api/webhook.rb +63 -6
- data/lib/discordrb/api.rb +55 -16
- data/lib/discordrb/await.rb +0 -1
- data/lib/discordrb/bot.rb +480 -93
- data/lib/discordrb/cache.rb +31 -24
- data/lib/discordrb/colour_rgb.rb +43 -0
- data/lib/discordrb/commands/command_bot.rb +35 -12
- data/lib/discordrb/commands/container.rb +21 -24
- data/lib/discordrb/commands/parser.rb +20 -20
- data/lib/discordrb/commands/rate_limiter.rb +4 -3
- data/lib/discordrb/container.rb +209 -20
- data/lib/discordrb/data/activity.rb +271 -0
- data/lib/discordrb/data/application.rb +50 -0
- data/lib/discordrb/data/attachment.rb +71 -0
- data/lib/discordrb/data/audit_logs.rb +345 -0
- data/lib/discordrb/data/channel.rb +993 -0
- data/lib/discordrb/data/component.rb +229 -0
- data/lib/discordrb/data/embed.rb +251 -0
- data/lib/discordrb/data/emoji.rb +82 -0
- data/lib/discordrb/data/integration.rb +122 -0
- data/lib/discordrb/data/interaction.rb +800 -0
- data/lib/discordrb/data/invite.rb +137 -0
- data/lib/discordrb/data/member.rb +372 -0
- data/lib/discordrb/data/message.rb +414 -0
- data/lib/discordrb/data/overwrite.rb +108 -0
- data/lib/discordrb/data/profile.rb +91 -0
- data/lib/discordrb/data/reaction.rb +33 -0
- data/lib/discordrb/data/recipient.rb +34 -0
- data/lib/discordrb/data/role.rb +248 -0
- data/lib/discordrb/data/server.rb +1004 -0
- data/lib/discordrb/data/user.rb +264 -0
- data/lib/discordrb/data/voice_region.rb +45 -0
- data/lib/discordrb/data/voice_state.rb +41 -0
- data/lib/discordrb/data/webhook.rb +238 -0
- data/lib/discordrb/data.rb +28 -4180
- data/lib/discordrb/errors.rb +46 -4
- data/lib/discordrb/events/bans.rb +7 -5
- data/lib/discordrb/events/channels.rb +3 -1
- data/lib/discordrb/events/guilds.rb +16 -9
- data/lib/discordrb/events/interactions.rb +482 -0
- data/lib/discordrb/events/invites.rb +125 -0
- data/lib/discordrb/events/members.rb +6 -2
- data/lib/discordrb/events/message.rb +72 -27
- data/lib/discordrb/events/presence.rb +35 -18
- data/lib/discordrb/events/raw.rb +1 -3
- data/lib/discordrb/events/reactions.rb +49 -4
- data/lib/discordrb/events/threads.rb +96 -0
- data/lib/discordrb/events/typing.rb +6 -4
- data/lib/discordrb/events/voice_server_update.rb +47 -0
- data/lib/discordrb/events/voice_state_update.rb +15 -10
- data/lib/discordrb/events/webhooks.rb +9 -6
- data/lib/discordrb/gateway.rb +99 -71
- data/lib/discordrb/id_object.rb +39 -0
- data/lib/discordrb/light/integrations.rb +1 -1
- data/lib/discordrb/light/light_bot.rb +1 -1
- data/lib/discordrb/logger.rb +4 -4
- data/lib/discordrb/paginator.rb +57 -0
- data/lib/discordrb/permissions.rb +159 -39
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/encoder.rb +16 -7
- data/lib/discordrb/voice/network.rb +99 -47
- data/lib/discordrb/voice/sodium.rb +98 -0
- data/lib/discordrb/voice/voice_bot.rb +33 -25
- data/lib/discordrb/webhooks.rb +2 -0
- data/lib/discordrb.rb +107 -1
- metadata +126 -54
- data/.codeclimate.yml +0 -16
- data/.travis.yml +0 -33
- data/bin/travis_build_docs.sh +0 -17
- /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
data/lib/discordrb/bot.rb
CHANGED
@@ -18,11 +18,17 @@ require 'discordrb/events/bans'
|
|
18
18
|
require 'discordrb/events/raw'
|
19
19
|
require 'discordrb/events/reactions'
|
20
20
|
require 'discordrb/events/webhooks'
|
21
|
+
require 'discordrb/events/invites'
|
22
|
+
require 'discordrb/events/interactions'
|
23
|
+
require 'discordrb/events/threads'
|
21
24
|
|
22
25
|
require 'discordrb/api'
|
23
26
|
require 'discordrb/api/channel'
|
24
27
|
require 'discordrb/api/server'
|
25
28
|
require 'discordrb/api/invite'
|
29
|
+
require 'discordrb/api/interaction'
|
30
|
+
require 'discordrb/api/application'
|
31
|
+
|
26
32
|
require 'discordrb/errors'
|
27
33
|
require 'discordrb/data'
|
28
34
|
require 'discordrb/await'
|
@@ -92,21 +98,25 @@ module Discordrb
|
|
92
98
|
# @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
|
93
99
|
# unless you really need this so you don't inadvertently create infinite loops.
|
94
100
|
# @param shard_id [Integer] The number of the shard this bot should handle. See
|
95
|
-
# https://github.com/
|
101
|
+
# https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
|
96
102
|
# @param num_shards [Integer] The total number of shards that should be running. See
|
97
|
-
# https://github.com/
|
103
|
+
# https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
|
98
104
|
# @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
|
99
105
|
# @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
|
100
106
|
# @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
|
101
107
|
# to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
|
102
108
|
# production bots). `:large` will request that large payloads are received compressed. `:stream` will request
|
103
109
|
# that all data be received in a continuous compressed stream.
|
110
|
+
# @param intents [:all, :unprivileged, Array<Symbol>, :none] Gateway intents that this bot requires. `:all` will
|
111
|
+
# request all intents. `:unprivileged` will request only intents that are not defined as "Privileged". `:none`
|
112
|
+
# will request no intents. An array of symbols will request only those intents specified.
|
113
|
+
# @see Discordrb::INTENTS
|
104
114
|
def initialize(
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
115
|
+
log_mode: :normal,
|
116
|
+
token: nil, client_id: nil,
|
117
|
+
type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
|
118
|
+
shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
|
119
|
+
compress_mode: :large, intents: :all
|
110
120
|
)
|
111
121
|
LOGGER.mode = log_mode
|
112
122
|
LOGGER.token = token if redact_token
|
@@ -125,8 +135,21 @@ module Discordrb
|
|
125
135
|
|
126
136
|
@compress_mode = compress_mode
|
127
137
|
|
138
|
+
raise 'Token string is empty or nil' if token.nil? || token.empty?
|
139
|
+
|
140
|
+
@intents = case intents
|
141
|
+
when :all
|
142
|
+
ALL_INTENTS
|
143
|
+
when :unprivileged
|
144
|
+
UNPRIVILEGED_INTENTS
|
145
|
+
when :none
|
146
|
+
NO_INTENTS
|
147
|
+
else
|
148
|
+
calculate_intents(intents)
|
149
|
+
end
|
150
|
+
|
128
151
|
@token = process_token(@type, token)
|
129
|
-
@gateway = Gateway.new(self, @token, @shard_key, @compress_mode)
|
152
|
+
@gateway = Gateway.new(self, @token, @shard_key, @compress_mode, @intents)
|
130
153
|
|
131
154
|
init_cache
|
132
155
|
|
@@ -140,6 +163,8 @@ module Discordrb
|
|
140
163
|
@current_thread = 0
|
141
164
|
|
142
165
|
@status = :online
|
166
|
+
|
167
|
+
@application_commands = {}
|
143
168
|
end
|
144
169
|
|
145
170
|
# The list of users the bot shares a server with.
|
@@ -158,18 +183,23 @@ module Discordrb
|
|
158
183
|
@servers
|
159
184
|
end
|
160
185
|
|
186
|
+
# The list of members in threads the bot can see.
|
187
|
+
# @return [Hash<Integer => Hash<Integer => Hash<String => Object>>]
|
188
|
+
def thread_members
|
189
|
+
gateway_check
|
190
|
+
unavailable_servers_check
|
191
|
+
@thread_members
|
192
|
+
end
|
193
|
+
|
161
194
|
# @overload emoji(id)
|
162
195
|
# Return an emoji by its ID
|
163
|
-
# @param id [
|
196
|
+
# @param id [String, Integer] The emoji's ID.
|
164
197
|
# @return [Emoji, nil] the emoji object. `nil` if the emoji was not found.
|
165
198
|
# @overload emoji
|
166
199
|
# The list of emoji the bot can use.
|
167
200
|
# @return [Array<Emoji>] the emoji available.
|
168
201
|
def emoji(id = nil)
|
169
|
-
|
170
|
-
unavailable_servers_check
|
171
|
-
|
172
|
-
emoji_hash = @servers.values.map(&:emoji).reduce(&:merge)
|
202
|
+
emoji_hash = servers.values.map(&:emoji).reduce(&:merge)
|
173
203
|
if id
|
174
204
|
id = id.resolve_id
|
175
205
|
emoji_hash[id]
|
@@ -193,8 +223,10 @@ module Discordrb
|
|
193
223
|
# to edit user data like the current username (see {Profile#username=}).
|
194
224
|
# @return [Profile] The bot's profile that can be used to edit data.
|
195
225
|
def profile
|
196
|
-
|
197
|
-
|
226
|
+
return @profile if @profile
|
227
|
+
|
228
|
+
response = Discordrb::API::User.profile(@token)
|
229
|
+
@profile = Profile.new(JSON.parse(response), self)
|
198
230
|
end
|
199
231
|
|
200
232
|
alias_method :bot_user, :profile
|
@@ -203,6 +235,7 @@ module Discordrb
|
|
203
235
|
# @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
|
204
236
|
def bot_application
|
205
237
|
return unless @type == :bot
|
238
|
+
|
206
239
|
response = API.oauth_application(token)
|
207
240
|
Application.new(JSON.parse(response), self)
|
208
241
|
end
|
@@ -225,7 +258,7 @@ module Discordrb
|
|
225
258
|
|
226
259
|
# Runs the bot, which logs into Discord and connects the WebSocket. This
|
227
260
|
# prevents all further execution unless it is executed with
|
228
|
-
# `
|
261
|
+
# `background` = `true`.
|
229
262
|
# @param background [true, false] If it is `true`, then the bot will run in
|
230
263
|
# another thread to allow further execution. If it is `false`, this method
|
231
264
|
# will block until {#stop} is called. If the bot is run with `true`, make
|
@@ -254,9 +287,9 @@ module Discordrb
|
|
254
287
|
|
255
288
|
# Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
|
256
289
|
# Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
|
257
|
-
# @
|
258
|
-
def stop(
|
259
|
-
@gateway.stop
|
290
|
+
# @note This method no longer takes an argument as of 3.4.0
|
291
|
+
def stop(_no_sync = nil)
|
292
|
+
@gateway.stop
|
260
293
|
end
|
261
294
|
|
262
295
|
# @return [true, false] whether or not the bot is currently connected to Discord.
|
@@ -273,14 +306,14 @@ module Discordrb
|
|
273
306
|
|
274
307
|
# Creates an OAuth invite URL that can be used to invite this bot to a particular server.
|
275
308
|
# @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
|
276
|
-
# @param permission_bits [
|
309
|
+
# @param permission_bits [String, Integer] Permission bits that should be appended to invite url.
|
277
310
|
# @return [String] the OAuth invite URL.
|
278
311
|
def invite_url(server: nil, permission_bits: nil)
|
279
312
|
@client_id ||= bot_application.id
|
280
313
|
|
281
314
|
server_id_str = server ? "&guild_id=#{server.id}" : ''
|
282
315
|
permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
|
283
|
-
"https://
|
316
|
+
"https://discord.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
|
284
317
|
end
|
285
318
|
|
286
319
|
# @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
|
@@ -299,22 +332,21 @@ module Discordrb
|
|
299
332
|
|
300
333
|
server_id = channel.server.id
|
301
334
|
return @voices[server_id] if @voices[server_id]
|
302
|
-
|
303
|
-
nil
|
304
335
|
end
|
305
336
|
|
306
337
|
# Connects to a voice channel, initializes network connections and returns the {Voice::VoiceBot} over which audio
|
307
338
|
# data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
|
308
339
|
# connected to voice, the existing connection will be terminated - you don't have to call
|
309
340
|
# {Discordrb::Voice::VoiceBot#destroy} before calling this method.
|
310
|
-
# @param chan [Channel,
|
311
|
-
# @param encrypted [true, false] Whether voice communication should be encrypted using
|
341
|
+
# @param chan [Channel, String, Integer] The voice channel, or its ID, to connect to.
|
342
|
+
# @param encrypted [true, false] Whether voice communication should be encrypted using
|
312
343
|
# (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
|
313
344
|
# @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
|
314
345
|
def voice_connect(chan, encrypted = true)
|
346
|
+
raise ArgumentError, 'Unencrypted voice connections are no longer supported.' unless encrypted
|
347
|
+
|
315
348
|
chan = channel(chan.resolve_id)
|
316
349
|
server_id = chan.server.id
|
317
|
-
@should_encrypt_voice = encrypted
|
318
350
|
|
319
351
|
if @voices[chan.id]
|
320
352
|
debug('Voice bot exists already! Destroying it')
|
@@ -336,7 +368,7 @@ module Discordrb
|
|
336
368
|
|
337
369
|
# Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
|
338
370
|
# {Discordrb::Voice::VoiceBot#destroy} rather than this.
|
339
|
-
# @param server [Server,
|
371
|
+
# @param server [Server, String, Integer] The server, or server ID, the voice connection is on.
|
340
372
|
# @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
|
341
373
|
# directly, you should leave it as true.
|
342
374
|
def voice_destroy(server, destroy_vws = true)
|
@@ -355,31 +387,41 @@ module Discordrb
|
|
355
387
|
end
|
356
388
|
|
357
389
|
# Sends a text message to a channel given its ID and the message's content.
|
358
|
-
# @param channel [Channel,
|
390
|
+
# @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
|
359
391
|
# @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
|
360
392
|
# @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
|
361
|
-
# @param
|
393
|
+
# @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
|
394
|
+
# @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
|
395
|
+
# @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
|
396
|
+
# @param components [View, Array<Hash>] Interaction components to associate with this message.
|
362
397
|
# @return [Message] The message that was sent.
|
363
|
-
def send_message(channel, content, tts = false,
|
398
|
+
def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
|
364
399
|
channel = channel.resolve_id
|
365
400
|
debug("Sending message to #{channel} with content '#{content}'")
|
401
|
+
allowed_mentions = { parse: [] } if allowed_mentions == false
|
402
|
+
message_reference = { message_id: message_reference.id } if message_reference.respond_to?(:id)
|
403
|
+
embeds = (embeds.instance_of?(Array) ? embeds.map(&:to_hash) : [embeds&.to_hash]).compact
|
366
404
|
|
367
|
-
response = API::Channel.create_message(token, channel, content, tts,
|
405
|
+
response = API::Channel.create_message(token, channel, content, tts, embeds, nil, attachments, allowed_mentions&.to_hash, message_reference, components)
|
368
406
|
Message.new(JSON.parse(response), self)
|
369
407
|
end
|
370
408
|
|
371
409
|
# Sends a text message to a channel given its ID and the message's content,
|
372
410
|
# then deletes it after the specified timeout in seconds.
|
373
|
-
# @param channel [Channel,
|
411
|
+
# @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
|
374
412
|
# @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
|
375
413
|
# @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
|
376
414
|
# @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
|
377
|
-
# @param
|
378
|
-
|
415
|
+
# @param embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed> nil] The rich embed(s) to append to this message.
|
416
|
+
# @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
|
417
|
+
# @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
|
418
|
+
# @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
|
419
|
+
# @param components [View, Array<Hash>] Interaction components to associate with this message.
|
420
|
+
def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil)
|
379
421
|
Thread.new do
|
380
422
|
Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg"
|
381
423
|
|
382
|
-
message = send_message(channel, content, tts,
|
424
|
+
message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components)
|
383
425
|
sleep(timeout)
|
384
426
|
message.delete
|
385
427
|
end
|
@@ -389,13 +431,25 @@ module Discordrb
|
|
389
431
|
|
390
432
|
# Sends a file to a channel. If it is an image, it will automatically be embedded.
|
391
433
|
# @note This executes in a blocking way, so if you're sending long files, be wary of delays.
|
392
|
-
# @param channel [Channel,
|
434
|
+
# @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
|
393
435
|
# @param file [File] The file that should be sent.
|
394
436
|
# @param caption [string] The caption for the file.
|
395
437
|
# @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
|
438
|
+
# @param filename [String] Overrides the filename of the uploaded file
|
439
|
+
# @param spoiler [true, false] Whether or not this file should appear as a spoiler.
|
396
440
|
# @example Send a file from disk
|
397
441
|
# bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))
|
398
|
-
def send_file(channel, file, caption: nil, tts: false)
|
442
|
+
def send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil)
|
443
|
+
if file.respond_to?(:read)
|
444
|
+
if spoiler
|
445
|
+
filename ||= File.basename(file.path)
|
446
|
+
filename = "SPOILER_#{filename}" unless filename.start_with? 'SPOILER_'
|
447
|
+
end
|
448
|
+
# https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
|
449
|
+
file.define_singleton_method(:original_filename) { filename } if filename
|
450
|
+
file.define_singleton_method(:path) { filename } if filename
|
451
|
+
end
|
452
|
+
|
399
453
|
channel = channel.resolve_id
|
400
454
|
response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
|
401
455
|
Message.new(JSON.parse(response), self)
|
@@ -410,14 +464,13 @@ module Discordrb
|
|
410
464
|
def create_server(name, region = :'eu-central')
|
411
465
|
response = API::Server.create(token, name, region)
|
412
466
|
id = JSON.parse(response)['id'].to_i
|
413
|
-
sleep 0.1 until @servers[id]
|
414
|
-
server = @servers[id]
|
467
|
+
sleep 0.1 until (server = @servers[id])
|
415
468
|
debug "Successfully created server #{server.id} with name #{server.name}"
|
416
469
|
server
|
417
470
|
end
|
418
471
|
|
419
472
|
# Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using
|
420
|
-
# Discord. For information how to use this, see the docs: https://
|
473
|
+
# Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
|
421
474
|
# @param name [String] What your application should be called.
|
422
475
|
# @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
|
423
476
|
# @return [Array(String, String)] your applications' client ID and client secret to be used in OAuth authorization.
|
@@ -436,28 +489,46 @@ module Discordrb
|
|
436
489
|
API.update_oauth_application(@token, name, redirect_uris, description, icon)
|
437
490
|
end
|
438
491
|
|
439
|
-
# Gets the
|
492
|
+
# Gets the users, channels, roles and emoji from a string.
|
493
|
+
# @param mentions [String] The mentions, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
|
494
|
+
# @param server [Server, nil] The server of the associated mentions. (recommended for role parsing, to speed things up)
|
495
|
+
# @return [Array<User, Channel, Role, Emoji>] The array of users, channels, roles and emoji identified by the mentions, or `nil` if none exists.
|
496
|
+
def parse_mentions(mentions, server = nil)
|
497
|
+
array_to_return = []
|
498
|
+
# While possible mentions may be in message
|
499
|
+
while mentions.include?('<') && mentions.include?('>')
|
500
|
+
# Removing all content before the next possible mention
|
501
|
+
mentions = mentions.split('<', 2)[1]
|
502
|
+
# Locate the first valid mention enclosed in `<...>`, otherwise advance to the next open `<`
|
503
|
+
next unless mentions.split('>', 2).first.length < mentions.split('<', 2).first.length
|
504
|
+
|
505
|
+
# Store the possible mention value to be validated with RegEx
|
506
|
+
mention = mentions.split('>', 2).first
|
507
|
+
if /@!?(?<id>\d+)/ =~ mention
|
508
|
+
array_to_return << user(id) unless user(id).nil?
|
509
|
+
elsif /#(?<id>\d+)/ =~ mention
|
510
|
+
array_to_return << channel(id, server) unless channel(id, server).nil?
|
511
|
+
elsif /@&(?<id>\d+)/ =~ mention
|
512
|
+
if server
|
513
|
+
array_to_return << server.role(id) unless server.role(id).nil?
|
514
|
+
else
|
515
|
+
@servers.each_value do |element|
|
516
|
+
array_to_return << element.role(id) unless element.role(id).nil?
|
517
|
+
end
|
518
|
+
end
|
519
|
+
elsif /(?<animated>^a|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
|
520
|
+
array_to_return << (emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil))
|
521
|
+
end
|
522
|
+
end
|
523
|
+
array_to_return
|
524
|
+
end
|
525
|
+
|
526
|
+
# Gets the user, channel, role or emoji from a string.
|
440
527
|
# @param mention [String] The mention, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
|
441
528
|
# @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
|
442
529
|
# @return [User, Channel, Role, Emoji] The user, channel, role or emoji identified by the mention, or `nil` if none exists.
|
443
530
|
def parse_mention(mention, server = nil)
|
444
|
-
|
445
|
-
if /<@!?(?<id>\d+)>/ =~ mention
|
446
|
-
user(id)
|
447
|
-
elsif /<#(?<id>\d+)>/ =~ mention
|
448
|
-
channel(id, server)
|
449
|
-
elsif /<@&(?<id>\d+)>/ =~ mention
|
450
|
-
return server.role(id) if server
|
451
|
-
@servers.values.each do |element|
|
452
|
-
role = element.role(id)
|
453
|
-
return role unless role.nil?
|
454
|
-
end
|
455
|
-
|
456
|
-
# Return nil if no role is found
|
457
|
-
nil
|
458
|
-
elsif /<(?<animated>a)?:(?<name>\w+):(?<id>\d+)>/ =~ mention
|
459
|
-
emoji(id) || Emoji.new({ 'animated' => !animated.nil?, 'name' => name, 'id' => id }, self, nil)
|
460
|
-
end
|
531
|
+
parse_mentions(mention, server).first
|
461
532
|
end
|
462
533
|
|
463
534
|
# Updates presence status.
|
@@ -466,7 +537,8 @@ module Discordrb
|
|
466
537
|
# @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
|
467
538
|
# @param since [Integer] When this status was set.
|
468
539
|
# @param afk [true, false] Whether the bot is AFK.
|
469
|
-
# @param activity_type [Integer] The type of activity status to display.
|
540
|
+
# @param activity_type [Integer] The type of activity status to display.
|
541
|
+
# Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching), or 5 (Competing).
|
470
542
|
# @see Gateway#send_status_update
|
471
543
|
def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
|
472
544
|
gateway_check
|
@@ -480,7 +552,7 @@ module Discordrb
|
|
480
552
|
@gateway.send_status_update(status, since, activity_obj, afk)
|
481
553
|
|
482
554
|
# Update the status in the cache
|
483
|
-
profile.update_presence('status' => status.to_s, '
|
555
|
+
profile.update_presence('status' => status.to_s, 'activities' => [activity_obj].compact)
|
484
556
|
end
|
485
557
|
|
486
558
|
# Sets the currently playing game to the specified game.
|
@@ -489,7 +561,6 @@ module Discordrb
|
|
489
561
|
def game=(name)
|
490
562
|
gateway_check
|
491
563
|
update_status(@status, name, nil)
|
492
|
-
name
|
493
564
|
end
|
494
565
|
|
495
566
|
alias_method :playing=, :game=
|
@@ -500,7 +571,6 @@ module Discordrb
|
|
500
571
|
def listening=(name)
|
501
572
|
gateway_check
|
502
573
|
update_status(@status, name, nil, nil, nil, 2)
|
503
|
-
name
|
504
574
|
end
|
505
575
|
|
506
576
|
# Sets the current watching status to the specified name.
|
@@ -509,7 +579,6 @@ module Discordrb
|
|
509
579
|
def watching=(name)
|
510
580
|
gateway_check
|
511
581
|
update_status(@status, name, nil, nil, nil, 3)
|
512
|
-
name
|
513
582
|
end
|
514
583
|
|
515
584
|
# Sets the currently online stream to the specified name and Twitch URL.
|
@@ -522,6 +591,14 @@ module Discordrb
|
|
522
591
|
name
|
523
592
|
end
|
524
593
|
|
594
|
+
# Sets the currently competing status to the specified name.
|
595
|
+
# @param name [String] The name of the game to be competing in.
|
596
|
+
# @return [String] The game that is being competed in now.
|
597
|
+
def competing=(name)
|
598
|
+
gateway_check
|
599
|
+
update_status(@status, name, nil, nil, nil, 5)
|
600
|
+
end
|
601
|
+
|
525
602
|
# Sets status to online.
|
526
603
|
def online
|
527
604
|
gateway_check
|
@@ -550,6 +627,36 @@ module Discordrb
|
|
550
627
|
update_status(:invisible, @activity, nil)
|
551
628
|
end
|
552
629
|
|
630
|
+
# Join a thread
|
631
|
+
# @param channel [Channel, Integer, String]
|
632
|
+
def join_thread(channel)
|
633
|
+
API::Channel.join_thread(@token, channel.resolve_id)
|
634
|
+
nil
|
635
|
+
end
|
636
|
+
|
637
|
+
# Leave a thread
|
638
|
+
# @param channel [Channel, Integer, String]
|
639
|
+
def leave_thread(channel)
|
640
|
+
API::Channel.leave_thread(@token, channel.resolve_id)
|
641
|
+
nil
|
642
|
+
end
|
643
|
+
|
644
|
+
# Add a member to a thread
|
645
|
+
# @param channel [Channel, Integer, String]
|
646
|
+
# @param member [Member, Integer, String]
|
647
|
+
def add_thread_member(channel, member)
|
648
|
+
API::Channel.add_thread_member(@token, channel.resolve_id, member.resolve_id)
|
649
|
+
nil
|
650
|
+
end
|
651
|
+
|
652
|
+
# Remove a member from a thread
|
653
|
+
# @param channel [Channel, Integer, String]
|
654
|
+
# @param member [Member, Integer, String]
|
655
|
+
def remove_thread_member(channel, member)
|
656
|
+
API::Channel.remove_thread_member(@token, channel.resolve_id, member.resolve_id)
|
657
|
+
nil
|
658
|
+
end
|
659
|
+
|
553
660
|
# Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
|
554
661
|
def debug=(new_debug)
|
555
662
|
LOGGER.debug = new_debug
|
@@ -576,6 +683,7 @@ module Discordrb
|
|
576
683
|
# @deprecated Will be changed to blocking behavior in v4.0. Use {#add_await!} instead.
|
577
684
|
def add_await(key, type, attributes = {}, &block)
|
578
685
|
raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
|
686
|
+
|
579
687
|
await = Await.new(self, key, type, attributes, block)
|
580
688
|
@awaits ||= {}
|
581
689
|
@awaits[key] = await
|
@@ -583,14 +691,17 @@ module Discordrb
|
|
583
691
|
|
584
692
|
# Awaits an event, blocking the current thread until a response is received.
|
585
693
|
# @param type [Class] The event class that should be listened for.
|
586
|
-
# @option attributes [Numeric] :timeout the amount of time to wait for a response before returning `nil`. Waits forever if omitted.
|
694
|
+
# @option attributes [Numeric] :timeout the amount of time (in seconds) to wait for a response before returning `nil`. Waits forever if omitted.
|
695
|
+
# @yield Executed when a matching event is received.
|
696
|
+
# @yieldparam event [Event] The event object that was triggered.
|
697
|
+
# @yieldreturn [true, false] Whether the event matches extra await criteria described by the block
|
587
698
|
# @return [Event, nil] The event object that was triggered, or `nil` if a `timeout` was set and no event was raised in time.
|
588
699
|
# @raise [ArgumentError] if `timeout` is given and is not a positive numeric value
|
589
700
|
def add_await!(type, attributes = {})
|
590
701
|
raise "You can't await an AwaitEvent!" if type == Discordrb::Events::AwaitEvent
|
591
702
|
|
592
703
|
timeout = attributes[:timeout]
|
593
|
-
raise ArgumentError, 'Timeout must be a number > 0' if timeout
|
704
|
+
raise ArgumentError, 'Timeout must be a number > 0' if timeout.is_a?(Numeric) && !timeout.positive?
|
594
705
|
|
595
706
|
mutex = Mutex.new
|
596
707
|
cv = ConditionVariable.new
|
@@ -598,7 +709,12 @@ module Discordrb
|
|
598
709
|
block = lambda do |event|
|
599
710
|
mutex.synchronize do
|
600
711
|
response = event
|
601
|
-
|
712
|
+
if block_given?
|
713
|
+
result = yield(event)
|
714
|
+
cv.signal if result.is_a?(TrueClass)
|
715
|
+
else
|
716
|
+
cv.signal
|
717
|
+
end
|
602
718
|
end
|
603
719
|
end
|
604
720
|
|
@@ -615,25 +731,26 @@ module Discordrb
|
|
615
731
|
|
616
732
|
remove_handler(handler)
|
617
733
|
raise 'ConditionVariable was signaled without returning an event!' if response.nil? && timeout.nil?
|
734
|
+
|
618
735
|
response
|
619
736
|
end
|
620
737
|
|
621
738
|
# Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.
|
622
739
|
# @note Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and
|
623
740
|
# presence and any other events will still be received.
|
624
|
-
# @param user [User,
|
741
|
+
# @param user [User, String, Integer] The user, or its ID, to be ignored.
|
625
742
|
def ignore_user(user)
|
626
743
|
@ignored_ids << user.resolve_id
|
627
744
|
end
|
628
745
|
|
629
746
|
# Remove a user from the ignore list.
|
630
|
-
# @param user [User,
|
747
|
+
# @param user [User, String, Integer] The user, or its ID, to be unignored.
|
631
748
|
def unignore_user(user)
|
632
749
|
@ignored_ids.delete(user.resolve_id)
|
633
750
|
end
|
634
751
|
|
635
752
|
# Checks whether a user is being ignored.
|
636
|
-
# @param user [User,
|
753
|
+
# @param user [User, String, Integer] The user, or its ID, to check.
|
637
754
|
# @return [true, false] whether or not the user is ignored.
|
638
755
|
def ignored?(user)
|
639
756
|
@ignored_ids.include?(user.resolve_id)
|
@@ -666,6 +783,118 @@ module Discordrb
|
|
666
783
|
end
|
667
784
|
end
|
668
785
|
|
786
|
+
# Get all application commands.
|
787
|
+
# @param server_id [String, Integer, nil] The ID of the server to get the commands from. Global if `nil`.
|
788
|
+
# @return [Array<ApplicationCommand>]
|
789
|
+
def get_application_commands(server_id: nil)
|
790
|
+
resp = if server_id
|
791
|
+
API::Application.get_guild_commands(@token, profile.id, server_id)
|
792
|
+
else
|
793
|
+
API::Application.get_global_commands(@token, profile.id)
|
794
|
+
end
|
795
|
+
|
796
|
+
JSON.parse(resp).map do |command_data|
|
797
|
+
ApplicationCommand.new(command_data, self, server_id)
|
798
|
+
end
|
799
|
+
end
|
800
|
+
|
801
|
+
# Get an application command by ID.
|
802
|
+
# @param command_id [String, Integer]
|
803
|
+
# @param server_id [String, Integer, nil] The ID of the server to get the command from. Global if `nil`.
|
804
|
+
def get_application_command(command_id, server_id: nil)
|
805
|
+
resp = if server_id
|
806
|
+
API::Application.get_guild_command(@token, profile.id, server_id, command_id)
|
807
|
+
else
|
808
|
+
API::Application.get_global_command(@token, profile.id, command_id)
|
809
|
+
end
|
810
|
+
ApplicationCommand.new(JSON.parse(resp), self, server_id)
|
811
|
+
end
|
812
|
+
|
813
|
+
# @yieldparam [OptionBuilder]
|
814
|
+
# @yieldparam [PermissionBuilder]
|
815
|
+
# @example
|
816
|
+
# bot.register_application_command(:reddit, 'Reddit Commands') do |cmd|
|
817
|
+
# cmd.subcommand_group(:subreddit, 'Subreddit Commands') do |group|
|
818
|
+
# group.subcommand(:hot, "What's trending") do |sub|
|
819
|
+
# sub.string(:subreddit, 'Subreddit to search')
|
820
|
+
# end
|
821
|
+
# group.subcommand(:new, "What's new") do |sub|
|
822
|
+
# sub.string(:since, 'How long ago', choices: ['this hour', 'today', 'this week', 'this month', 'this year', 'all time'])
|
823
|
+
# sub.string(:subreddit, 'Subreddit to search')
|
824
|
+
# end
|
825
|
+
# end
|
826
|
+
# end
|
827
|
+
def register_application_command(name, description, server_id: nil, default_permission: nil, type: :chat_input)
|
828
|
+
type = ApplicationCommand::TYPES[type] || type
|
829
|
+
|
830
|
+
builder = Interactions::OptionBuilder.new
|
831
|
+
permission_builder = Interactions::PermissionBuilder.new
|
832
|
+
yield(builder, permission_builder) if block_given?
|
833
|
+
|
834
|
+
resp = if server_id
|
835
|
+
API::Application.create_guild_command(@token, profile.id, server_id, name, description, builder.to_a, default_permission, type)
|
836
|
+
else
|
837
|
+
API::Application.create_global_command(@token, profile.id, name, description, builder.to_a, default_permission, type)
|
838
|
+
end
|
839
|
+
cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
|
840
|
+
|
841
|
+
if permission_builder.to_a.any?
|
842
|
+
raise ArgumentError, 'Permissions can only be set for guild commands' unless server_id
|
843
|
+
|
844
|
+
edit_application_command_permissions(cmd.id, server_id, permission_builder.to_a)
|
845
|
+
end
|
846
|
+
|
847
|
+
cmd
|
848
|
+
end
|
849
|
+
|
850
|
+
# @yieldparam [OptionBuilder]
|
851
|
+
# @yieldparam [PermissionBuilder]
|
852
|
+
def edit_application_command(command_id, server_id: nil, name: nil, description: nil, default_permission: nil, type: :chat_input)
|
853
|
+
type = ApplicationCommand::TYPES[type] || type
|
854
|
+
|
855
|
+
builder = Interactions::OptionBuilder.new
|
856
|
+
permission_builder = Interactions::PermissionBuilder.new
|
857
|
+
|
858
|
+
yield(builder, permission_builder) if block_given?
|
859
|
+
|
860
|
+
resp = if server_id
|
861
|
+
API::Application.edit_guild_command(@token, profile.id, server_id, command_id, name, description, builder.to_a, default_permission, type)
|
862
|
+
else
|
863
|
+
API::Application.edit_guild_command(@token, profile.id, command_id, name, description, builder.to_a, default_permission.type)
|
864
|
+
end
|
865
|
+
cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
|
866
|
+
|
867
|
+
if permission_builder.to_a.any?
|
868
|
+
raise ArgumentError, 'Permissions can only be set for guild commands' unless server_id
|
869
|
+
|
870
|
+
edit_application_command_permissions(cmd.id, server_id, permission_builder.to_a)
|
871
|
+
end
|
872
|
+
|
873
|
+
cmd
|
874
|
+
end
|
875
|
+
|
876
|
+
# Remove an application command from the commands registered with discord.
|
877
|
+
# @param command_id [String, Integer] The ID of the command to remove.
|
878
|
+
# @param server_id [String, Integer] The ID of the server to delete this command from, global if `nil`.
|
879
|
+
def delete_application_command(command_id, server_id: nil)
|
880
|
+
if server_id
|
881
|
+
API::Application.delete_guild_command(@token, profile.id, server_id, command_id)
|
882
|
+
else
|
883
|
+
API::Application.delete_global_command(@token, profile.id, command_id)
|
884
|
+
end
|
885
|
+
end
|
886
|
+
|
887
|
+
# @param command_id [Integer, String]
|
888
|
+
# @param server_id [Integer, String]
|
889
|
+
# @param permissions [Array<Hash>] An array of objects formatted as `{ id: ENTITY_ID, type: 1 or 2, permission: true or false }`
|
890
|
+
def edit_application_command_permissions(command_id, server_id, permissions = [])
|
891
|
+
builder = Interactions::PermissionBuilder.new
|
892
|
+
yield builder if block_given?
|
893
|
+
|
894
|
+
permissions += builder.to_a
|
895
|
+
API::Application.edit_guild_command_permissions(@token, profile.id, server_id, command_id, permissions)
|
896
|
+
end
|
897
|
+
|
669
898
|
private
|
670
899
|
|
671
900
|
# Throws a useful exception if there's currently no gateway connection.
|
@@ -677,7 +906,8 @@ module Discordrb
|
|
677
906
|
# e.g. due to a Discord outage or because the servers are large and taking a while to load.
|
678
907
|
def unavailable_servers_check
|
679
908
|
# Return unless there are servers that are unavailable.
|
680
|
-
return unless @unavailable_servers
|
909
|
+
return unless @unavailable_servers&.positive?
|
910
|
+
|
681
911
|
LOGGER.warn("#{@unavailable_servers} servers haven't been cached yet.")
|
682
912
|
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.')
|
683
913
|
end
|
@@ -716,10 +946,16 @@ module Discordrb
|
|
716
946
|
|
717
947
|
username = data['user']['username']
|
718
948
|
if username && !member_is_new # Don't set the username for newly-cached members
|
719
|
-
debug "Implicitly updating presence-obtained information for member #{user_id}"
|
949
|
+
debug "Implicitly updating presence-obtained information username for member #{user_id}"
|
720
950
|
member.update_username(username)
|
721
951
|
end
|
722
952
|
|
953
|
+
global_name = data['user']['global_name']
|
954
|
+
if global_name && !member_is_new # Don't set the global_name for newly-cached members
|
955
|
+
debug "Implicitly updating presence-obtained information global_name for member #{user_id}"
|
956
|
+
member.update_global_name(global_name)
|
957
|
+
end
|
958
|
+
|
723
959
|
member.update_presence(data)
|
724
960
|
|
725
961
|
member.avatar_id = data['user']['avatar'] if data['user']['avatar']
|
@@ -737,10 +973,21 @@ module Discordrb
|
|
737
973
|
|
738
974
|
user_id = data['user_id'].to_i
|
739
975
|
old_voice_state = server.voice_states[user_id]
|
740
|
-
old_channel_id = old_voice_state.voice_channel
|
976
|
+
old_channel_id = old_voice_state.voice_channel&.id if old_voice_state
|
741
977
|
|
742
978
|
server.update_voice_state(data)
|
743
979
|
|
980
|
+
existing_voice = @voices[server_id]
|
981
|
+
if user_id == @profile.id && existing_voice
|
982
|
+
new_channel_id = data['channel_id']
|
983
|
+
if new_channel_id
|
984
|
+
new_channel = channel(new_channel_id)
|
985
|
+
existing_voice.channel = new_channel
|
986
|
+
else
|
987
|
+
voice_destroy(server_id)
|
988
|
+
end
|
989
|
+
end
|
990
|
+
|
744
991
|
old_channel_id
|
745
992
|
end
|
746
993
|
|
@@ -751,6 +998,7 @@ module Discordrb
|
|
751
998
|
|
752
999
|
debug("Voice server update received! chan: #{channel.inspect}")
|
753
1000
|
return unless channel
|
1001
|
+
|
754
1002
|
@should_connect_to_voice.delete(server_id)
|
755
1003
|
debug('Updating voice server!')
|
756
1004
|
|
@@ -763,19 +1011,19 @@ module Discordrb
|
|
763
1011
|
end
|
764
1012
|
|
765
1013
|
debug('Got data, now creating the bot.')
|
766
|
-
@voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint
|
1014
|
+
@voices[server_id] = Discordrb::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint)
|
767
1015
|
end
|
768
1016
|
|
769
1017
|
# Internal handler for CHANNEL_CREATE
|
770
1018
|
def create_channel(data)
|
771
|
-
channel = Channel.new(data, self)
|
1019
|
+
channel = data.is_a?(Discordrb::Channel) ? data : Channel.new(data, self)
|
772
1020
|
server = channel.server
|
773
1021
|
|
774
1022
|
# Handle normal and private channels separately
|
775
1023
|
if server
|
776
1024
|
server.add_channel(channel)
|
777
1025
|
@channels[channel.id] = channel
|
778
|
-
elsif channel.
|
1026
|
+
elsif channel.private?
|
779
1027
|
@pm_channels[channel.recipient.id] = channel
|
780
1028
|
elsif channel.group?
|
781
1029
|
@channels[channel.id] = channel
|
@@ -787,6 +1035,7 @@ module Discordrb
|
|
787
1035
|
channel = Channel.new(data, self)
|
788
1036
|
old_channel = @channels[channel.id]
|
789
1037
|
return unless old_channel
|
1038
|
+
|
790
1039
|
old_channel.update_from(channel)
|
791
1040
|
end
|
792
1041
|
|
@@ -804,6 +1053,8 @@ module Discordrb
|
|
804
1053
|
elsif channel.group?
|
805
1054
|
@channels.delete(channel.id)
|
806
1055
|
end
|
1056
|
+
|
1057
|
+
@thread_members.delete(channel.id) if channel.thread?
|
807
1058
|
end
|
808
1059
|
|
809
1060
|
# Internal handler for CHANNEL_RECIPIENT_ADD
|
@@ -843,12 +1094,16 @@ module Discordrb
|
|
843
1094
|
member = server.member(data['user']['id'].to_i)
|
844
1095
|
member.update_roles(data['roles'])
|
845
1096
|
member.update_nick(data['nick'])
|
1097
|
+
member.update_global_name(data['user']['global_name']) if data['user']['global_name']
|
1098
|
+
member.update_boosting_since(data['premium_since'])
|
1099
|
+
member.update_communication_disabled_until(data['communication_disabled_until'])
|
846
1100
|
end
|
847
1101
|
|
848
1102
|
# Internal handler for GUILD_MEMBER_DELETE
|
849
1103
|
def delete_guild_member(data)
|
850
1104
|
server_id = data['guild_id'].to_i
|
851
1105
|
server = self.server(server_id)
|
1106
|
+
return unless server
|
852
1107
|
|
853
1108
|
user_id = data['user']['id'].to_i
|
854
1109
|
server.delete_member(user_id)
|
@@ -858,7 +1113,7 @@ module Discordrb
|
|
858
1113
|
|
859
1114
|
# Internal handler for GUILD_CREATE
|
860
1115
|
def create_guild(data)
|
861
|
-
ensure_server(data)
|
1116
|
+
ensure_server(data, true)
|
862
1117
|
end
|
863
1118
|
|
864
1119
|
# Internal handler for GUILD_UPDATE
|
@@ -949,15 +1204,15 @@ module Discordrb
|
|
949
1204
|
|
950
1205
|
def process_token(type, token)
|
951
1206
|
# Remove the "Bot " prefix if it exists
|
952
|
-
token = token[4
|
1207
|
+
token = token[4..] if token.start_with? 'Bot '
|
953
1208
|
|
954
|
-
token =
|
1209
|
+
token = "Bot #{token}" unless type == :user
|
955
1210
|
token
|
956
1211
|
end
|
957
1212
|
|
958
1213
|
def handle_dispatch(type, data)
|
959
1214
|
# Check whether there are still unavailable servers and there have been more than 10 seconds since READY
|
960
|
-
if @unavailable_servers &&
|
1215
|
+
if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !((@intents || 0) & INTENTS[:servers]).zero?
|
961
1216
|
# The server streaming timed out!
|
962
1217
|
LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
|
963
1218
|
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.')
|
@@ -986,14 +1241,14 @@ module Discordrb
|
|
986
1241
|
data['guilds'].each do |element|
|
987
1242
|
# Check for true specifically because unavailable=false indicates that a previously unavailable server has
|
988
1243
|
# come online
|
989
|
-
if element['unavailable']
|
1244
|
+
if element['unavailable']
|
990
1245
|
@unavailable_servers += 1
|
991
1246
|
|
992
1247
|
# Ignore any unavailable servers
|
993
1248
|
next
|
994
1249
|
end
|
995
1250
|
|
996
|
-
ensure_server(element)
|
1251
|
+
ensure_server(element, true)
|
997
1252
|
end
|
998
1253
|
|
999
1254
|
# Add PM and group channels
|
@@ -1018,9 +1273,14 @@ module Discordrb
|
|
1018
1273
|
when :GUILD_MEMBERS_CHUNK
|
1019
1274
|
id = data['guild_id'].to_i
|
1020
1275
|
server = server(id)
|
1021
|
-
server.process_chunk(data['members'])
|
1276
|
+
server.process_chunk(data['members'], data['chunk_index'], data['chunk_count'])
|
1277
|
+
when :INVITE_CREATE
|
1278
|
+
invite = Invite.new(data, self)
|
1279
|
+
raise_event(InviteCreateEvent.new(data, invite, self))
|
1280
|
+
when :INVITE_DELETE
|
1281
|
+
raise_event(InviteDeleteEvent.new(data, self))
|
1022
1282
|
when :MESSAGE_CREATE
|
1023
|
-
if ignored?(data['author']['id']
|
1283
|
+
if ignored?(data['author']['id'])
|
1024
1284
|
debug("Ignored author with ID #{data['author']['id']}")
|
1025
1285
|
return
|
1026
1286
|
end
|
@@ -1037,6 +1297,13 @@ module Discordrb
|
|
1037
1297
|
|
1038
1298
|
return if message.from_bot? && !should_parse_self
|
1039
1299
|
|
1300
|
+
# Dispatch a ChannelCreateEvent for channels we don't have cached
|
1301
|
+
if message.channel.private? && @pm_channels[message.channel.recipient.id].nil?
|
1302
|
+
create_channel(message.channel)
|
1303
|
+
|
1304
|
+
raise_event(ChannelCreateEvent.new(message.channel, self))
|
1305
|
+
end
|
1306
|
+
|
1040
1307
|
event = MessageEvent.new(message, self)
|
1041
1308
|
raise_event(event)
|
1042
1309
|
|
@@ -1053,6 +1320,10 @@ module Discordrb
|
|
1053
1320
|
update_message(data)
|
1054
1321
|
|
1055
1322
|
message = Message.new(data, self)
|
1323
|
+
|
1324
|
+
event = MessageUpdateEvent.new(message, self)
|
1325
|
+
raise_event(event)
|
1326
|
+
|
1056
1327
|
return if message.from_bot? && !should_parse_self
|
1057
1328
|
|
1058
1329
|
unless message.author
|
@@ -1115,18 +1386,28 @@ module Discordrb
|
|
1115
1386
|
# Ignore friends list presences
|
1116
1387
|
return unless data['guild_id']
|
1117
1388
|
|
1118
|
-
|
1389
|
+
new_activities = (data['activities'] || []).map { |act_data| Activity.new(act_data, self) }
|
1119
1390
|
presence_user = @users[data['user']['id'].to_i]
|
1120
|
-
|
1391
|
+
old_activities = (presence_user&.activities || [])
|
1121
1392
|
update_presence(data)
|
1122
1393
|
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
end
|
1394
|
+
# Starting a new game
|
1395
|
+
playing_change = new_activities.reject do |act|
|
1396
|
+
old_activities.find { |old| old.name == act.name }
|
1397
|
+
end
|
1128
1398
|
|
1129
|
-
|
1399
|
+
# Exiting an existing game
|
1400
|
+
playing_change += old_activities.reject do |old|
|
1401
|
+
new_activities.find { |act| act.name == old.name }
|
1402
|
+
end
|
1403
|
+
|
1404
|
+
if playing_change.any?
|
1405
|
+
playing_change.each do |act|
|
1406
|
+
raise_event(PlayingEvent.new(data, act, self))
|
1407
|
+
end
|
1408
|
+
else
|
1409
|
+
raise_event(PresenceEvent.new(data, self))
|
1410
|
+
end
|
1130
1411
|
when :VOICE_STATE_UPDATE
|
1131
1412
|
old_channel_id = update_voice_state(data)
|
1132
1413
|
|
@@ -1135,7 +1416,8 @@ module Discordrb
|
|
1135
1416
|
when :VOICE_SERVER_UPDATE
|
1136
1417
|
update_voice_server(data)
|
1137
1418
|
|
1138
|
-
|
1419
|
+
event = VoiceServerUpdateEvent.new(data, self)
|
1420
|
+
raise_event(event)
|
1139
1421
|
when :CHANNEL_CREATE
|
1140
1422
|
create_channel(data)
|
1141
1423
|
|
@@ -1262,9 +1544,93 @@ module Discordrb
|
|
1262
1544
|
event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
|
1263
1545
|
raise_event(event)
|
1264
1546
|
end
|
1547
|
+
when :INTERACTION_CREATE
|
1548
|
+
event = InteractionCreateEvent.new(data, self)
|
1549
|
+
raise_event(event)
|
1550
|
+
|
1551
|
+
case data['type']
|
1552
|
+
when Interaction::TYPES[:command]
|
1553
|
+
event = ApplicationCommandEvent.new(data, self)
|
1554
|
+
|
1555
|
+
Thread.new do
|
1556
|
+
Thread.current[:discordrb_name] = "it-#{event.interaction.id}"
|
1557
|
+
|
1558
|
+
begin
|
1559
|
+
debug("Executing application command #{event.command_name}:#{event.command_id}")
|
1560
|
+
|
1561
|
+
@application_commands[event.command_name]&.call(event)
|
1562
|
+
rescue StandardError => e
|
1563
|
+
log_exception(e)
|
1564
|
+
end
|
1565
|
+
end
|
1566
|
+
when Interaction::TYPES[:component]
|
1567
|
+
case data['data']['component_type']
|
1568
|
+
when Webhooks::View::COMPONENT_TYPES[:button]
|
1569
|
+
event = ButtonEvent.new(data, self)
|
1570
|
+
|
1571
|
+
raise_event(event)
|
1572
|
+
when Webhooks::View::COMPONENT_TYPES[:string_select]
|
1573
|
+
event = StringSelectEvent.new(data, self)
|
1574
|
+
|
1575
|
+
raise_event(event)
|
1576
|
+
when Webhooks::View::COMPONENT_TYPES[:user_select]
|
1577
|
+
event = UserSelectEvent.new(data, self)
|
1578
|
+
|
1579
|
+
raise_event(event)
|
1580
|
+
when Webhooks::View::COMPONENT_TYPES[:role_select]
|
1581
|
+
event = RoleSelectEvent.new(data, self)
|
1582
|
+
|
1583
|
+
raise_event(event)
|
1584
|
+
when Webhooks::View::COMPONENT_TYPES[:mentionable_select]
|
1585
|
+
event = MentionableSelectEvent.new(data, self)
|
1586
|
+
|
1587
|
+
raise_event(event)
|
1588
|
+
when Webhooks::View::COMPONENT_TYPES[:channel_select]
|
1589
|
+
event = ChannelSelectEvent.new(data, self)
|
1590
|
+
|
1591
|
+
raise_event(event)
|
1592
|
+
end
|
1593
|
+
when Interaction::TYPES[:modal_submit]
|
1594
|
+
|
1595
|
+
event = ModalSubmitEvent.new(data, self)
|
1596
|
+
raise_event(event)
|
1597
|
+
end
|
1265
1598
|
when :WEBHOOKS_UPDATE
|
1266
1599
|
event = WebhookUpdateEvent.new(data, self)
|
1267
1600
|
raise_event(event)
|
1601
|
+
when :THREAD_CREATE
|
1602
|
+
create_channel(data)
|
1603
|
+
|
1604
|
+
event = ThreadCreateEvent.new(data, self)
|
1605
|
+
raise_event(event)
|
1606
|
+
when :THREAD_UPDATE
|
1607
|
+
update_channel(data)
|
1608
|
+
|
1609
|
+
event = ThreadUpdateEvent.new(data, self)
|
1610
|
+
raise_event(event)
|
1611
|
+
when :THREAD_DELETE
|
1612
|
+
delete_channel(data)
|
1613
|
+
@thread_members.delete(data['id']&.resolve_id)
|
1614
|
+
|
1615
|
+
# raise ThreadDeleteEvent
|
1616
|
+
when :THREAD_LIST_SYNC
|
1617
|
+
data['members'].map { |member| ensure_thread_member(member) }
|
1618
|
+
data['threads'].map { |channel| ensure_channel(channel, data['guild_id']) }
|
1619
|
+
|
1620
|
+
# raise ThreadListSyncEvent?
|
1621
|
+
when :THREAD_MEMBER_UPDATE
|
1622
|
+
ensure_thread_member(data)
|
1623
|
+
when :THREAD_MEMBERS_UPDATE
|
1624
|
+
data['added_members']&.each do |added_member|
|
1625
|
+
ensure_thread_member(added_member) if added_member['user_id']
|
1626
|
+
end
|
1627
|
+
|
1628
|
+
data['removed_member_ids']&.each do |member_id|
|
1629
|
+
@thread_members[data['id']&.resolve_id]&.delete(member_id&.resolve_id)
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
event = ThreadMembersUpdateEvent.new(data, self)
|
1633
|
+
raise_event(event)
|
1268
1634
|
else
|
1269
1635
|
# another event that we don't support yet
|
1270
1636
|
debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
|
@@ -1300,6 +1666,7 @@ module Discordrb
|
|
1300
1666
|
@event_handlers ||= {}
|
1301
1667
|
handlers = @event_handlers[event.class]
|
1302
1668
|
return unless handlers
|
1669
|
+
|
1303
1670
|
handlers.dup.each do |handler|
|
1304
1671
|
call_event(handler, event) if handler.matches?(event)
|
1305
1672
|
end
|
@@ -1315,7 +1682,7 @@ module Discordrb
|
|
1315
1682
|
begin
|
1316
1683
|
handler.call(event)
|
1317
1684
|
handler.after_call(event)
|
1318
|
-
rescue => e
|
1685
|
+
rescue StandardError => e
|
1319
1686
|
log_exception(e)
|
1320
1687
|
ensure
|
1321
1688
|
@event_threads.delete(t)
|
@@ -1328,6 +1695,7 @@ module Discordrb
|
|
1328
1695
|
@awaits.each do |_, await|
|
1329
1696
|
key, should_delete = await.match(event)
|
1330
1697
|
next unless key
|
1698
|
+
|
1331
1699
|
debug("should_delete: #{should_delete}")
|
1332
1700
|
@awaits.delete(await.key) if should_delete
|
1333
1701
|
|
@@ -1335,5 +1703,24 @@ module Discordrb
|
|
1335
1703
|
raise_event(await_event)
|
1336
1704
|
end
|
1337
1705
|
end
|
1706
|
+
|
1707
|
+
def calculate_intents(intents)
|
1708
|
+
intents.reduce(0) do |sum, intent|
|
1709
|
+
case intent
|
1710
|
+
when Symbol
|
1711
|
+
if INTENTS[intent]
|
1712
|
+
sum | INTENTS[intent]
|
1713
|
+
else
|
1714
|
+
LOGGER.warn("Unknown intent: #{intent}")
|
1715
|
+
sum
|
1716
|
+
end
|
1717
|
+
when Integer
|
1718
|
+
sum | intent
|
1719
|
+
else
|
1720
|
+
LOGGER.warn("Invalid intent: #{intent}")
|
1721
|
+
sum
|
1722
|
+
end
|
1723
|
+
end
|
1724
|
+
end
|
1338
1725
|
end
|
1339
1726
|
end
|