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
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ffi'
|
4
|
+
|
5
|
+
module Discordrb::Voice
|
6
|
+
# @!visibility private
|
7
|
+
module Sodium
|
8
|
+
extend ::FFI::Library
|
9
|
+
|
10
|
+
ffi_lib(['sodium', 'libsodium.so.18', 'libsodium.so.23'])
|
11
|
+
|
12
|
+
# Encryption & decryption
|
13
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305, %i[pointer pointer ulong_long pointer pointer], :int)
|
14
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_open, %i[pointer pointer ulong_long pointer pointer], :int)
|
15
|
+
|
16
|
+
# Constants
|
17
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_keybytes, [], :size_t)
|
18
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_noncebytes, [], :size_t)
|
19
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_zerobytes, [], :size_t)
|
20
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_boxzerobytes, [], :size_t)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Utility class for interacting with required `xsalsa20poly1305` functions for voice transmission
|
24
|
+
# @!visibility private
|
25
|
+
class SecretBox
|
26
|
+
# Exception raised when a key or nonce with invalid length is used
|
27
|
+
class LengthError < RuntimeError
|
28
|
+
end
|
29
|
+
|
30
|
+
# Exception raised when encryption or decryption fails
|
31
|
+
class CryptoError < RuntimeError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Required key length
|
35
|
+
KEY_LENGTH = Sodium.crypto_secretbox_xsalsa20poly1305_keybytes
|
36
|
+
|
37
|
+
# Required nonce length
|
38
|
+
NONCE_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_noncebytes
|
39
|
+
|
40
|
+
# Zero byte padding for encryption
|
41
|
+
ZERO_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_zerobytes
|
42
|
+
|
43
|
+
# Zero byte padding for decryption
|
44
|
+
BOX_ZERO_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_boxzerobytes
|
45
|
+
|
46
|
+
# @param key [String] Crypto key of length {KEY_LENGTH}
|
47
|
+
def initialize(key)
|
48
|
+
raise(LengthError, 'Key length') if key.bytesize != KEY_LENGTH
|
49
|
+
|
50
|
+
@key = key
|
51
|
+
end
|
52
|
+
|
53
|
+
# Encrypts a message using this box's key
|
54
|
+
# @param nonce [String] encryption nonce for this message
|
55
|
+
# @param message [String] message to be encrypted
|
56
|
+
def box(nonce, message)
|
57
|
+
raise(LengthError, 'Nonce length') if nonce.bytesize != NONCE_BYTES
|
58
|
+
|
59
|
+
message_padded = prepend_zeroes(ZERO_BYTES, message)
|
60
|
+
buffer = zero_string(message_padded.bytesize)
|
61
|
+
|
62
|
+
success = Sodium.crypto_secretbox_xsalsa20poly1305(buffer, message_padded, message_padded.bytesize, nonce, @key)
|
63
|
+
raise(CryptoError, "Encryption failed (#{success})") unless success.zero?
|
64
|
+
|
65
|
+
remove_zeroes(BOX_ZERO_BYTES, buffer)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Decrypts the given ciphertext using this box's key
|
69
|
+
# @param nonce [String] encryption nonce for this ciphertext
|
70
|
+
# @param ciphertext [String] ciphertext to decrypt
|
71
|
+
def open(nonce, ciphertext)
|
72
|
+
raise(LengthError, 'Nonce length') if nonce.bytesize != NONCE_BYTES
|
73
|
+
|
74
|
+
ct_padded = prepend_zeroes(BOX_ZERO_BYTES, ciphertext)
|
75
|
+
buffer = zero_string(ct_padded.bytesize)
|
76
|
+
|
77
|
+
success = Sodium.crypto_secretbox_xsalsa20poly1305_open(buffer, ct_padded, ct_padded.bytesize, nonce, @key)
|
78
|
+
raise(CryptoError, "Decryption failed (#{success})") unless success.zero?
|
79
|
+
|
80
|
+
remove_zeroes(ZERO_BYTES, buffer)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def zero_string(size)
|
86
|
+
str = "\0" * size
|
87
|
+
str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding)
|
88
|
+
end
|
89
|
+
|
90
|
+
def prepend_zeroes(size, string)
|
91
|
+
zero_string(size) + string
|
92
|
+
end
|
93
|
+
|
94
|
+
def remove_zeroes(size, string)
|
95
|
+
string.slice!(size, string.bytesize - size)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -6,7 +6,7 @@ require 'discordrb/logger'
|
|
6
6
|
|
7
7
|
# Voice support
|
8
8
|
module Discordrb::Voice
|
9
|
-
# How long one voice packet should ideally be (
|
9
|
+
# How long one voice packet should ideally be (20ms as defined by Discord)
|
10
10
|
IDEAL_LENGTH = 20.0
|
11
11
|
|
12
12
|
# How many bytes of data to read (1920 bytes * 2 channels) from audio PCM data
|
@@ -24,6 +24,9 @@ module Discordrb::Voice
|
|
24
24
|
# @return [Channel] the current voice channel
|
25
25
|
attr_reader :channel
|
26
26
|
|
27
|
+
# @!visibility private
|
28
|
+
attr_writer :channel
|
29
|
+
|
27
30
|
# @return [Integer, nil] the amount of time the stream has been playing, or `nil` if nothing has been played yet.
|
28
31
|
attr_reader :stream_time
|
29
32
|
|
@@ -36,7 +39,7 @@ module Discordrb::Voice
|
|
36
39
|
# play parts back too fast. How often these measurements should be done depends a lot on the system, and if it's
|
37
40
|
# done too quickly, especially on slow connections, the playback speed will vary wildly; if it's done too slowly
|
38
41
|
# however, small errors will cause quality problems for a longer time.
|
39
|
-
# @return [Integer] how frequently audio length adjustments should be done, in ideal packets (
|
42
|
+
# @return [Integer] how frequently audio length adjustments should be done, in ideal packets (20ms).
|
40
43
|
attr_accessor :adjust_interval
|
41
44
|
|
42
45
|
# This particular value is also important because ffmpeg may take longer to process the first few packets. It is
|
@@ -44,7 +47,7 @@ module Discordrb::Voice
|
|
44
47
|
# shouldn't be any higher than {#adjust_interval}, otherwise no adjustments will take place. If {#adjust_interval}
|
45
48
|
# is at a value higher than 10, this value should not be changed at all.
|
46
49
|
# @see #adjust_interval
|
47
|
-
# @return [Integer] the packet number (1 packet =
|
50
|
+
# @return [Integer] the packet number (1 packet = 20ms) at which length adjustments should start.
|
48
51
|
attr_accessor :adjust_offset
|
49
52
|
|
50
53
|
# This value determines whether or not the adjustment length should be averaged with the previous value. This may
|
@@ -60,8 +63,8 @@ module Discordrb::Voice
|
|
60
63
|
attr_accessor :adjust_debug
|
61
64
|
|
62
65
|
# If this value is set, no length adjustments will ever be done and this value will always be used as the length
|
63
|
-
# (i.
|
64
|
-
# The ideal length is
|
66
|
+
# (i.e. packets will be sent every N seconds). Be careful not to set it too low as to not spam Discord's servers.
|
67
|
+
# The ideal length is 20ms (accessible by the {Discordrb::Voice::IDEAL_LENGTH} constant), this value should be
|
65
68
|
# slightly lower than that because encoding + sending takes time. Note that sending DCA files is significantly
|
66
69
|
# faster than sending regular audio files (usually about four times as fast), so you might want to set this value
|
67
70
|
# to something else if you're sending a DCA file.
|
@@ -74,13 +77,12 @@ module Discordrb::Voice
|
|
74
77
|
attr_accessor :volume
|
75
78
|
|
76
79
|
# @!visibility private
|
77
|
-
def initialize(channel, bot, token, session, endpoint
|
80
|
+
def initialize(channel, bot, token, session, endpoint)
|
78
81
|
@bot = bot
|
79
82
|
@channel = channel
|
80
83
|
|
81
84
|
@ws = VoiceWS.new(channel, bot, token, session, endpoint)
|
82
85
|
@udp = @ws.udp
|
83
|
-
@udp.encrypted = encrypted
|
84
86
|
|
85
87
|
@sequence = @time = 0
|
86
88
|
@skips = 0
|
@@ -95,14 +97,15 @@ module Discordrb::Voice
|
|
95
97
|
|
96
98
|
@encoder = Encoder.new
|
97
99
|
@ws.connect
|
98
|
-
rescue => e
|
100
|
+
rescue StandardError => e
|
99
101
|
Discordrb::LOGGER.log_exception(e)
|
100
102
|
raise
|
101
103
|
end
|
102
104
|
|
103
105
|
# @return [true, false] whether audio data sent will be encrypted.
|
106
|
+
# @deprecated Discord no longer supports unencrypted voice communication.
|
104
107
|
def encrypted?
|
105
|
-
|
108
|
+
true
|
106
109
|
end
|
107
110
|
|
108
111
|
# Set the filter volume. This volume is applied as a filter for decoded audio data. It has the advantage that using
|
@@ -132,7 +135,7 @@ module Discordrb::Voice
|
|
132
135
|
|
133
136
|
alias_method :isplaying?, :playing?
|
134
137
|
|
135
|
-
# Continue playback. This change may take up to
|
138
|
+
# Continue playback. This change may take up to 100ms to take effect, which is usually negligible.
|
136
139
|
def continue
|
137
140
|
@paused = false
|
138
141
|
end
|
@@ -145,7 +148,8 @@ module Discordrb::Voice
|
|
145
148
|
end
|
146
149
|
|
147
150
|
# Sets whether or not the bot is speaking (green circle around user).
|
148
|
-
# @param value [true, false] whether or not the bot should be speaking
|
151
|
+
# @param value [true, false, Integer] whether or not the bot should be speaking, or a bitmask denoting the audio type
|
152
|
+
# @note https://discord.com/developers/docs/topics/voice-connections#speaking for information on the speaking bitmask
|
149
153
|
def speaking=(value)
|
150
154
|
@playing = value
|
151
155
|
@ws.send_speaking(value)
|
@@ -161,6 +165,7 @@ module Discordrb::Voice
|
|
161
165
|
sleep IDEAL_LENGTH / 1000.0 if @was_playing_before
|
162
166
|
|
163
167
|
return unless wait_for_confirmation
|
168
|
+
|
164
169
|
@has_stopped_playing = false
|
165
170
|
sleep IDEAL_LENGTH / 1000.0 until @has_stopped_playing
|
166
171
|
@has_stopped_playing = false
|
@@ -173,7 +178,7 @@ module Discordrb::Voice
|
|
173
178
|
@ws.destroy
|
174
179
|
end
|
175
180
|
|
176
|
-
# Plays a stream of raw data to the channel. All playback methods are blocking, i.
|
181
|
+
# Plays a stream of raw data to the channel. All playback methods are blocking, i.e. they wait for the playback to
|
177
182
|
# finish before exiting the method. This doesn't cause a problem if you just use discordrb events/commands to
|
178
183
|
# play stuff, as these are fully threaded, but if you don't want this behaviour anyway, be sure to call these
|
179
184
|
# methods in separate threads.
|
@@ -206,7 +211,7 @@ module Discordrb::Voice
|
|
206
211
|
end
|
207
212
|
|
208
213
|
# Adjust volume
|
209
|
-
buf = @encoder.adjust_volume(buf, @volume) if @volume != 1.0
|
214
|
+
buf = @encoder.adjust_volume(buf, @volume) if @volume != 1.0 # rubocop:disable Lint/FloatComparison
|
210
215
|
|
211
216
|
@first_packet = false
|
212
217
|
|
@@ -215,12 +220,15 @@ module Discordrb::Voice
|
|
215
220
|
end
|
216
221
|
|
217
222
|
# If the stream is a process, kill it
|
218
|
-
if encoded_io
|
223
|
+
if encoded_io&.pid
|
219
224
|
Discordrb::LOGGER.debug("Killing ffmpeg process with pid #{encoded_io.pid.inspect}")
|
220
225
|
|
221
226
|
begin
|
222
|
-
|
223
|
-
|
227
|
+
pid = encoded_io.pid
|
228
|
+
# Windows does not support TERM as a kill signal, so we use KILL. `Process.waitpid` verifies that our
|
229
|
+
# child process has not already completed.
|
230
|
+
Process.kill(Gem.win_platform? ? 'KILL' : 'TERM', pid) if Process.waitpid(pid, Process::WNOHANG).nil?
|
231
|
+
rescue StandardError => e
|
224
232
|
Discordrb::LOGGER.warn('Failed to kill ffmpeg process! You *might* have a process leak now.')
|
225
233
|
Discordrb::LOGGER.warn("Reason: #{e}")
|
226
234
|
end
|
@@ -255,13 +263,13 @@ module Discordrb::Voice
|
|
255
263
|
stop_playing(true) if @playing
|
256
264
|
|
257
265
|
@bot.debug "Reading DCA file #{file}"
|
258
|
-
input_stream = open(file)
|
266
|
+
input_stream = File.open(file)
|
259
267
|
|
260
268
|
magic = input_stream.read(4)
|
261
269
|
raise ArgumentError, 'Not a DCA1 file! The file might have been corrupted, please recreate it.' unless magic == 'DCA1'
|
262
270
|
|
263
271
|
# Read the metadata header, then read the metadata and discard it as we don't care about it
|
264
|
-
metadata_header = input_stream.read(4).
|
272
|
+
metadata_header = input_stream.read(4).unpack1('l<')
|
265
273
|
input_stream.read(metadata_header)
|
266
274
|
|
267
275
|
# Play the data, without re-encoding it to opus
|
@@ -275,9 +283,9 @@ module Discordrb::Voice
|
|
275
283
|
next :stop
|
276
284
|
end
|
277
285
|
|
278
|
-
header = header_str.
|
286
|
+
header = header_str.unpack1('s<')
|
279
287
|
|
280
|
-
raise 'Negative header in DCA file! Your file is likely corrupted.' if header
|
288
|
+
raise 'Negative header in DCA file! Your file is likely corrupted.' if header.negative?
|
281
289
|
rescue EOFError
|
282
290
|
@bot.debug 'Finished DCA parsing (EOFError)'
|
283
291
|
next :stop
|
@@ -292,7 +300,7 @@ module Discordrb::Voice
|
|
292
300
|
|
293
301
|
private
|
294
302
|
|
295
|
-
# Plays the data from the
|
303
|
+
# Plays the data from the IO stream as Discord requires it
|
296
304
|
def play_internal
|
297
305
|
count = 0
|
298
306
|
@playing = true
|
@@ -311,7 +319,7 @@ module Discordrb::Voice
|
|
311
319
|
break unless @playing
|
312
320
|
|
313
321
|
# If we should skip, get some data, discard it and go to the next iteration
|
314
|
-
if @skips
|
322
|
+
if @skips.positive?
|
315
323
|
@skips -= 1
|
316
324
|
yield
|
317
325
|
next
|
@@ -364,7 +372,7 @@ module Discordrb::Voice
|
|
364
372
|
# If paused, wait
|
365
373
|
sleep 0.1 while @paused
|
366
374
|
|
367
|
-
if @length
|
375
|
+
if @length.positive?
|
368
376
|
# Wait `length` ms, then send the next packet
|
369
377
|
sleep @length / 1000.0
|
370
378
|
else
|
@@ -378,13 +386,13 @@ module Discordrb::Voice
|
|
378
386
|
increment_packet_headers
|
379
387
|
@udp.send_audio(Encoder::OPUS_SILENCE, @sequence, @time)
|
380
388
|
|
381
|
-
# Length adjustments don't matter here, we can just wait
|
389
|
+
# Length adjustments don't matter here, we can just wait 20ms since nobody is going to hear it anyway
|
382
390
|
sleep IDEAL_LENGTH / 1000.0
|
383
391
|
end
|
384
392
|
|
385
393
|
@bot.debug('Performing final cleanup after stream ended')
|
386
394
|
|
387
|
-
# Final
|
395
|
+
# Final clean-up
|
388
396
|
stop_playing
|
389
397
|
|
390
398
|
# Notify any stop_playing methods running right now that we have actually stopped
|
data/lib/discordrb/webhooks.rb
CHANGED
data/lib/discordrb.rb
CHANGED
@@ -10,7 +10,113 @@ module Discordrb
|
|
10
10
|
Thread.current[:discordrb_name] = 'main'
|
11
11
|
|
12
12
|
# The default debug logger used by discordrb.
|
13
|
-
LOGGER = Logger.new(ENV
|
13
|
+
LOGGER = Logger.new(ENV.fetch('DISCORDRB_FANCY_LOG', false))
|
14
|
+
|
15
|
+
# The Unix timestamp Discord IDs are based on
|
16
|
+
DISCORD_EPOCH = 1_420_070_400_000
|
17
|
+
|
18
|
+
# Used to declare what events you wish to recieve from Discord.
|
19
|
+
# @see https://discord.com/developers/docs/topics/gateway#gateway-intents
|
20
|
+
INTENTS = {
|
21
|
+
servers: 1 << 0,
|
22
|
+
server_members: 1 << 1,
|
23
|
+
server_bans: 1 << 2,
|
24
|
+
server_emojis: 1 << 3,
|
25
|
+
server_integrations: 1 << 4,
|
26
|
+
server_webhooks: 1 << 5,
|
27
|
+
server_invites: 1 << 6,
|
28
|
+
server_voice_states: 1 << 7,
|
29
|
+
server_presences: 1 << 8,
|
30
|
+
server_messages: 1 << 9,
|
31
|
+
server_message_reactions: 1 << 10,
|
32
|
+
server_message_typing: 1 << 11,
|
33
|
+
direct_messages: 1 << 12,
|
34
|
+
direct_message_reactions: 1 << 13,
|
35
|
+
direct_message_typing: 1 << 14
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
# All available intents
|
39
|
+
ALL_INTENTS = INTENTS.values.reduce(&:|)
|
40
|
+
|
41
|
+
# All unprivileged intents
|
42
|
+
# @see https://discord.com/developers/docs/topics/gateway#privileged-intents
|
43
|
+
UNPRIVILEGED_INTENTS = ALL_INTENTS & ~(INTENTS[:server_members] | INTENTS[:server_presences])
|
44
|
+
|
45
|
+
# No intents
|
46
|
+
NO_INTENTS = 0
|
47
|
+
|
48
|
+
# Compares two objects based on IDs - either the objects' IDs are equal, or one object is equal to the other's ID.
|
49
|
+
def self.id_compare(one_id, other)
|
50
|
+
other.respond_to?(:resolve_id) ? (one_id.resolve_id == other.resolve_id) : (one_id == other)
|
51
|
+
end
|
52
|
+
|
53
|
+
# The maximum length a Discord message can have
|
54
|
+
CHARACTER_LIMIT = 2000
|
55
|
+
|
56
|
+
# For creating timestamps with {timestamp}
|
57
|
+
# @see https://discord.com/developers/docs/reference#message-formatting-timestamp-styles
|
58
|
+
TIMESTAMP_STYLES = {
|
59
|
+
short_time: 't', # 16:20
|
60
|
+
long_time: 'T', # 16:20:30
|
61
|
+
short_date: 'd', # 20/04/2021
|
62
|
+
long_date: 'D', # 20 April 2021
|
63
|
+
short_datetime: 'f', # 20 April 2021 16:20
|
64
|
+
long_datetime: 'F', # Tuesday, 20 April 2021 16:20
|
65
|
+
relative: 'R' # 2 months ago
|
66
|
+
}.freeze
|
67
|
+
|
68
|
+
# Splits a message into chunks of 2000 characters. Attempts to split by lines if possible.
|
69
|
+
# @param msg [String] The message to split.
|
70
|
+
# @return [Array<String>] the message split into chunks
|
71
|
+
def self.split_message(msg)
|
72
|
+
# If the messages is empty, return an empty array
|
73
|
+
return [] if msg.empty?
|
74
|
+
|
75
|
+
# Split the message into lines
|
76
|
+
lines = msg.lines
|
77
|
+
|
78
|
+
# Turn the message into a "triangle" of consecutively longer slices, for example the array [1,2,3,4] would become
|
79
|
+
# [
|
80
|
+
# [1],
|
81
|
+
# [1, 2],
|
82
|
+
# [1, 2, 3],
|
83
|
+
# [1, 2, 3, 4]
|
84
|
+
# ]
|
85
|
+
tri = (0...lines.length).map { |i| lines.combination(i + 1).first }
|
86
|
+
|
87
|
+
# Join the individual elements together to get an array of strings with consecutively more lines
|
88
|
+
joined = tri.map(&:join)
|
89
|
+
|
90
|
+
# Find the largest element that is still below the character limit, or if none such element exists return the first
|
91
|
+
ideal = joined.max_by { |e| e.length > CHARACTER_LIMIT ? -1 : e.length }
|
92
|
+
|
93
|
+
# If it's still larger than the character limit (none was smaller than it) split it into the largest chunk without
|
94
|
+
# cutting words apart, breaking on the nearest space within character limit, otherwise just return an array with one element
|
95
|
+
ideal_ary = ideal.length > CHARACTER_LIMIT ? ideal.split(/(.{1,#{CHARACTER_LIMIT}}\b|.{1,#{CHARACTER_LIMIT}})/o).reject(&:empty?) : [ideal]
|
96
|
+
|
97
|
+
# Slice off the ideal part and strip newlines
|
98
|
+
rest = msg[ideal.length..].strip
|
99
|
+
|
100
|
+
# If none remains, return an empty array -> we're done
|
101
|
+
return [] unless rest
|
102
|
+
|
103
|
+
# Otherwise, call the method recursively to split the rest of the string and add it onto the ideal array
|
104
|
+
ideal_ary + split_message(rest)
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param time [Time, Integer] The time to create the timestamp from, or a unix timestamp integer.
|
108
|
+
# @param style [Symbol, String] One of the keys from {TIMESTAMP_STYLES} or a string with the style.
|
109
|
+
# @return [String]
|
110
|
+
# @example
|
111
|
+
# Discordrb.timestamp(Time.now, :short_time)
|
112
|
+
# # => "<t:1632146954:t>"
|
113
|
+
def self.timestamp(time, style = nil)
|
114
|
+
if style.nil?
|
115
|
+
"<t:#{time.to_i}>"
|
116
|
+
else
|
117
|
+
"<t:#{time.to_i}:#{TIMESTAMP_STYLES[style] || style}>"
|
118
|
+
end
|
119
|
+
end
|
14
120
|
end
|
15
121
|
|
16
122
|
# In discordrb, Integer and {String} are monkey-patched to allow for easy resolution of IDs
|