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