onyxcord 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/CONTRIBUTING.md +13 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  8. data/.github/pull_request_template.md +37 -0
  9. data/.github/workflows/ci.yml +78 -0
  10. data/.github/workflows/codeql.yml +65 -0
  11. data/.github/workflows/deploy.yml +54 -0
  12. data/.github/workflows/release.yml +51 -0
  13. data/.gitignore +16 -0
  14. data/.markdownlint.json +4 -0
  15. data/.overcommit.yml +7 -0
  16. data/.rspec +2 -0
  17. data/.rubocop.yml +129 -0
  18. data/.yardopts +1 -0
  19. data/CHANGELOG.md +0 -0
  20. data/Gemfile +7 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +305 -0
  23. data/Rakefile +17 -0
  24. data/bin/console +15 -0
  25. data/bin/setup +7 -0
  26. data/lib/onyxcord/allowed_mentions.rb +43 -0
  27. data/lib/onyxcord/api/application.rb +316 -0
  28. data/lib/onyxcord/api/channel.rb +700 -0
  29. data/lib/onyxcord/api/interaction.rb +67 -0
  30. data/lib/onyxcord/api/invite.rb +44 -0
  31. data/lib/onyxcord/api/server.rb +775 -0
  32. data/lib/onyxcord/api/user.rb +158 -0
  33. data/lib/onyxcord/api/webhook.rb +163 -0
  34. data/lib/onyxcord/api.rb +335 -0
  35. data/lib/onyxcord/await.rb +51 -0
  36. data/lib/onyxcord/bot.rb +1971 -0
  37. data/lib/onyxcord/cache.rb +326 -0
  38. data/lib/onyxcord/colour_rgb.rb +43 -0
  39. data/lib/onyxcord/commands/command_bot.rb +511 -0
  40. data/lib/onyxcord/commands/container.rb +112 -0
  41. data/lib/onyxcord/commands/events.rb +11 -0
  42. data/lib/onyxcord/commands/parser.rb +327 -0
  43. data/lib/onyxcord/commands/rate_limiter.rb +144 -0
  44. data/lib/onyxcord/configuration.rb +125 -0
  45. data/lib/onyxcord/container.rb +988 -0
  46. data/lib/onyxcord/data/activity.rb +271 -0
  47. data/lib/onyxcord/data/application.rb +341 -0
  48. data/lib/onyxcord/data/attachment.rb +91 -0
  49. data/lib/onyxcord/data/audit_logs.rb +438 -0
  50. data/lib/onyxcord/data/avatar_decoration.rb +26 -0
  51. data/lib/onyxcord/data/call.rb +22 -0
  52. data/lib/onyxcord/data/channel.rb +1355 -0
  53. data/lib/onyxcord/data/channel_tag.rb +69 -0
  54. data/lib/onyxcord/data/collectibles.rb +47 -0
  55. data/lib/onyxcord/data/component.rb +583 -0
  56. data/lib/onyxcord/data/embed.rb +258 -0
  57. data/lib/onyxcord/data/emoji.rb +123 -0
  58. data/lib/onyxcord/data/install_params.rb +24 -0
  59. data/lib/onyxcord/data/integration.rb +144 -0
  60. data/lib/onyxcord/data/interaction.rb +1141 -0
  61. data/lib/onyxcord/data/invite.rb +137 -0
  62. data/lib/onyxcord/data/member.rb +528 -0
  63. data/lib/onyxcord/data/message.rb +612 -0
  64. data/lib/onyxcord/data/message_activity.rb +41 -0
  65. data/lib/onyxcord/data/overwrite.rb +109 -0
  66. data/lib/onyxcord/data/poll.rb +365 -0
  67. data/lib/onyxcord/data/primary_server.rb +60 -0
  68. data/lib/onyxcord/data/profile.rb +79 -0
  69. data/lib/onyxcord/data/reaction.rb +64 -0
  70. data/lib/onyxcord/data/recipient.rb +34 -0
  71. data/lib/onyxcord/data/role.rb +449 -0
  72. data/lib/onyxcord/data/role_connection_data.rb +69 -0
  73. data/lib/onyxcord/data/role_subscription.rb +41 -0
  74. data/lib/onyxcord/data/scheduled_event.rb +513 -0
  75. data/lib/onyxcord/data/server.rb +1614 -0
  76. data/lib/onyxcord/data/server_preview.rb +68 -0
  77. data/lib/onyxcord/data/snapshot.rb +112 -0
  78. data/lib/onyxcord/data/team.rb +98 -0
  79. data/lib/onyxcord/data/timestamp.rb +69 -0
  80. data/lib/onyxcord/data/user.rb +324 -0
  81. data/lib/onyxcord/data/voice_region.rb +46 -0
  82. data/lib/onyxcord/data/voice_state.rb +41 -0
  83. data/lib/onyxcord/data/webhook.rb +238 -0
  84. data/lib/onyxcord/data.rb +57 -0
  85. data/lib/onyxcord/errors.rb +246 -0
  86. data/lib/onyxcord/event_executor.rb +80 -0
  87. data/lib/onyxcord/events/await.rb +48 -0
  88. data/lib/onyxcord/events/bans.rb +60 -0
  89. data/lib/onyxcord/events/channels.rb +225 -0
  90. data/lib/onyxcord/events/generic.rb +129 -0
  91. data/lib/onyxcord/events/guilds.rb +269 -0
  92. data/lib/onyxcord/events/integrations.rb +100 -0
  93. data/lib/onyxcord/events/interactions.rb +624 -0
  94. data/lib/onyxcord/events/invites.rb +127 -0
  95. data/lib/onyxcord/events/lifetime.rb +31 -0
  96. data/lib/onyxcord/events/members.rb +110 -0
  97. data/lib/onyxcord/events/message.rb +399 -0
  98. data/lib/onyxcord/events/polls.rb +118 -0
  99. data/lib/onyxcord/events/presence.rb +131 -0
  100. data/lib/onyxcord/events/raw.rb +74 -0
  101. data/lib/onyxcord/events/reactions.rb +218 -0
  102. data/lib/onyxcord/events/roles.rb +87 -0
  103. data/lib/onyxcord/events/scheduled_events.rb +171 -0
  104. data/lib/onyxcord/events/threads.rb +100 -0
  105. data/lib/onyxcord/events/typing.rb +73 -0
  106. data/lib/onyxcord/events/voice_server_update.rb +48 -0
  107. data/lib/onyxcord/events/voice_state_update.rb +106 -0
  108. data/lib/onyxcord/events/webhooks.rb +65 -0
  109. data/lib/onyxcord/gateway.rb +890 -0
  110. data/lib/onyxcord/id_object.rb +39 -0
  111. data/lib/onyxcord/light/data.rb +62 -0
  112. data/lib/onyxcord/light/integrations.rb +73 -0
  113. data/lib/onyxcord/light/light_bot.rb +58 -0
  114. data/lib/onyxcord/light.rb +8 -0
  115. data/lib/onyxcord/logger.rb +120 -0
  116. data/lib/onyxcord/message_components.rb +70 -0
  117. data/lib/onyxcord/paginator.rb +60 -0
  118. data/lib/onyxcord/permissions.rb +255 -0
  119. data/lib/onyxcord/rate_limiter/gateway.rb +42 -0
  120. data/lib/onyxcord/rate_limiter/rest.rb +89 -0
  121. data/lib/onyxcord/version.rb +7 -0
  122. data/lib/onyxcord/voice/encoder.rb +115 -0
  123. data/lib/onyxcord/voice/network.rb +380 -0
  124. data/lib/onyxcord/voice/opcodes.rb +29 -0
  125. data/lib/onyxcord/voice/sodium.rb +157 -0
  126. data/lib/onyxcord/voice/timer.rb +19 -0
  127. data/lib/onyxcord/voice/voice_bot.rb +386 -0
  128. data/lib/onyxcord/webhooks.rb +14 -0
  129. data/lib/onyxcord/websocket.rb +62 -0
  130. data/lib/onyxcord.rb +180 -0
  131. data/onyxcord-webhooks.gemspec +30 -0
  132. data/onyxcord.gemspec +50 -0
  133. metadata +421 -0
@@ -0,0 +1,386 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/voice/encoder'
4
+ require 'onyxcord/voice/network'
5
+ require 'onyxcord/voice/timer'
6
+ require 'onyxcord/logger'
7
+ require 'ffi'
8
+
9
+ # Voice support
10
+ module OnyxCord::Voice
11
+ # How long one voice packet should ideally be (20ms as defined by Discord)
12
+ IDEAL_LENGTH = 20.0
13
+
14
+ # How many bytes of data to read (1920 bytes * 2 channels) from audio PCM data
15
+ DATA_LENGTH = 1920 * 2
16
+
17
+ # This class represents a connection to a Discord voice server and channel. It can be used to play audio files and
18
+ # streams and to control playback on currently playing tracks. The method {Bot#voice_connect} can be used to connect
19
+ # to a voice channel.
20
+ #
21
+ # onyxcord does latency adjustments every now and then to improve playback quality. I made sure to put useful
22
+ # defaults for the adjustment parameters, but if the sound is patchy or too fast (or the speed varies a lot) you
23
+ # should check the parameters and adjust them to your connection: {VoiceBot#adjust_interval},
24
+ # {VoiceBot#adjust_offset}, and {VoiceBot#adjust_average}.
25
+ class VoiceBot
26
+ # @return [Channel] the current voice channel
27
+ attr_reader :channel
28
+
29
+ # @!visibility private
30
+ attr_writer :channel
31
+
32
+ # @return [Integer, nil] the amount of time the stream has been playing, or `nil` if nothing has been played yet.
33
+ attr_reader :stream_time
34
+
35
+ # @return [Encoder] the encoder used to encode audio files into the format required by Discord.
36
+ attr_reader :encoder
37
+
38
+ # onyxcord will occasionally measure the time it takes to send a packet, and adjust future delay times based
39
+ # on that data. This makes voice playback more smooth, because if packets are sent too slowly, the audio will
40
+ # sound patchy, and if they're sent too quickly, packets will "pile up" and occasionally skip some data or
41
+ # play parts back too fast. How often these measurements should be done depends a lot on the system, and if it's
42
+ # done too quickly, especially on slow connections, the playback speed will vary wildly; if it's done too slowly
43
+ # however, small errors will cause quality problems for a longer time.
44
+ # @return [Integer] how frequently audio length adjustments should be done, in ideal packets (20ms).
45
+ attr_accessor :adjust_interval
46
+
47
+ # This particular value is also important because ffmpeg may take longer to process the first few packets. It is
48
+ # recommended to set this to 10 at maximum, otherwise it will take too long to make the first adjustment, but it
49
+ # shouldn't be any higher than {#adjust_interval}, otherwise no adjustments will take place. If {#adjust_interval}
50
+ # is at a value higher than 10, this value should not be changed at all.
51
+ # @see #adjust_interval
52
+ # @return [Integer] the packet number (1 packet = 20ms) at which length adjustments should start.
53
+ attr_accessor :adjust_offset
54
+
55
+ # This value determines whether or not the adjustment length should be averaged with the previous value. This may
56
+ # be useful on slower connections where latencies vary a lot. In general, it will make adjustments more smooth,
57
+ # but whether that is desired behaviour should be tried on a case-by-case basis.
58
+ # @see #adjust_interval
59
+ # @return [true, false] whether adjustment lengths should be averaged with the respective previous value.
60
+ attr_accessor :adjust_average
61
+
62
+ # Disable the debug message for length adjustment specifically, as it can get quite spammy with very low intervals
63
+ # @see #adjust_interval
64
+ # @return [true, false] whether length adjustment debug messages should be printed
65
+ attr_accessor :adjust_debug
66
+
67
+ # If this value is set, no length adjustments will ever be done and this value will always be used as the length
68
+ # (i.e. packets will be sent every N seconds). Be careful not to set it too low as to not spam Discord's servers.
69
+ # The ideal length is 20ms (accessible by the {OnyxCord::Voice::IDEAL_LENGTH} constant), this value should be
70
+ # slightly lower than that because encoding + sending takes time. Note that sending DCA files is significantly
71
+ # faster than sending regular audio files (usually about four times as fast), so you might want to set this value
72
+ # to something else if you're sending a DCA file.
73
+ # @return [Float] the packet length that should be used instead of calculating it during the adjustments, in ms.
74
+ attr_accessor :length_override
75
+
76
+ # The factor the audio's volume should be multiplied with. `1` is no change in volume, `0` is completely silent,
77
+ # `0.5` is half the default volume and `2` is twice the default.
78
+ # @return [Float] the volume for audio playback, `1.0` by default.
79
+ attr_accessor :volume
80
+
81
+ # @!visibility private
82
+ def initialize(channel, bot, token, session, endpoint)
83
+ @bot = bot
84
+ @channel = channel
85
+
86
+ @ws = VoiceWS.new(channel, bot, token, session, endpoint)
87
+ @udp = @ws.udp
88
+
89
+ @sequence = @time = 0
90
+ @skips = 0
91
+
92
+ @adjust_interval = 100
93
+ @adjust_offset = 10
94
+ @adjust_average = false
95
+ @adjust_debug = true
96
+
97
+ @volume = 1.0
98
+ @playing = false
99
+
100
+ @encoder = Encoder.new
101
+ @ws.connect
102
+ rescue StandardError => e
103
+ OnyxCord::LOGGER.log_exception(e)
104
+ raise
105
+ end
106
+
107
+ # @return [true, false] whether audio data sent will be encrypted.
108
+ # @deprecated Discord no longer supports unencrypted voice communication.
109
+ def encrypted?
110
+ true
111
+ end
112
+
113
+ # Set the filter volume. This volume is applied as a filter for decoded audio data. It has the advantage that using
114
+ # it is much faster than regular volume, but it can only be changed before starting to play something.
115
+ # @param value [Integer] The value to set the volume to. For possible values, see {#volume}
116
+ def filter_volume=(value)
117
+ @encoder.filter_volume = value
118
+ end
119
+
120
+ # @see #filter_volume=
121
+ # @return [Integer] the volume used as a filter for ffmpeg/avconv.
122
+ def filter_volume
123
+ @encoder.filter_volume
124
+ end
125
+
126
+ # Pause playback. This is not instant; it may take up to 20 ms for this change to take effect. (This is usually
127
+ # negligible.)
128
+ def pause
129
+ @paused = true
130
+ end
131
+
132
+ # @see #play
133
+ # @return [true, false] Whether it is playing sound or not.
134
+ def playing?
135
+ @playing
136
+ end
137
+
138
+ alias_method :isplaying?, :playing?
139
+
140
+ # Continue playback. This change may take up to 100ms to take effect, which is usually negligible.
141
+ def continue
142
+ @paused = false
143
+ end
144
+
145
+ # Skips to a later time in the song. It's impossible to go back without replaying the song.
146
+ # @param secs [Float] How many seconds to skip forwards. Skipping will always be done in discrete intervals of
147
+ # 0.05 seconds, so if the given amount is smaller than that, it will be rounded up.
148
+ def skip(secs)
149
+ @skips += (secs * (1000 / IDEAL_LENGTH)).ceil
150
+ end
151
+
152
+ # Sets whether or not the bot is speaking (green circle around user).
153
+ # @param value [true, false, Integer] whether or not the bot should be speaking, or a bitmask denoting the audio type
154
+ # @note https://discord.com/developers/docs/topics/voice-connections#speaking for information on the speaking bitmask
155
+ def speaking=(value)
156
+ @playing = value
157
+ @ws.send_speaking(value)
158
+ end
159
+
160
+ # Stops the current playback entirely.
161
+ # @param wait_for_confirmation [true, false] Whether the method should wait for confirmation from the playback
162
+ # method that the playback has actually stopped.
163
+ def stop_playing(wait_for_confirmation = false)
164
+ @was_playing_before = @playing
165
+ @speaking = false
166
+ @playing = false
167
+ sleep IDEAL_LENGTH / 1000.0 if @was_playing_before
168
+
169
+ return unless wait_for_confirmation
170
+
171
+ @has_stopped_playing = false
172
+ sleep IDEAL_LENGTH / 1000.0 until @has_stopped_playing
173
+ @has_stopped_playing = false
174
+ end
175
+
176
+ # Permanently disconnects from the voice channel; to reconnect you will have to call {Bot#voice_connect} again.
177
+ def destroy
178
+ stop_playing
179
+ @bot.voice_destroy(@channel.server.id, false)
180
+ @ws.destroy
181
+ end
182
+
183
+ # Plays a stream of raw data to the channel. All playback methods are blocking, i.e. they wait for the playback to
184
+ # finish before exiting the method. This doesn't cause a problem if you just use onyxcord events/commands to
185
+ # play stuff, as these are fully threaded, but if you don't want this behaviour anyway, be sure to call these
186
+ # methods in separate threads.
187
+ # @param encoded_io [IO] A stream of raw PCM data (s16le)
188
+ def play(encoded_io)
189
+ stop_playing(true) if @playing
190
+ @retry_attempts = 3
191
+ @first_packet = true
192
+
193
+ play_internal do
194
+ buf = nil
195
+
196
+ # Read some data from the buffer
197
+ begin
198
+ buf = encoded_io.readpartial(DATA_LENGTH) if encoded_io
199
+ rescue EOFError
200
+ raise IOError, 'File or stream not found!' if @first_packet
201
+
202
+ @bot.debug('EOF while reading, breaking immediately')
203
+ next :stop
204
+ end
205
+
206
+ # Check whether the buffer has enough data
207
+ if !buf || buf.length != DATA_LENGTH
208
+ @bot.debug("No data is available! Retrying #{@retry_attempts} more times")
209
+ next :stop if @retry_attempts.zero?
210
+
211
+ @retry_attempts -= 1
212
+ next
213
+ end
214
+
215
+ # Adjust volume
216
+ buf = @encoder.adjust_volume(buf, @volume) if @volume != 1.0 # rubocop:disable Lint/FloatComparison
217
+
218
+ @first_packet = false
219
+
220
+ # Encode data
221
+ @encoder.encode(buf)
222
+ end
223
+
224
+ # If the stream is a process, kill it
225
+ if encoded_io&.pid
226
+ OnyxCord::LOGGER.debug("Killing ffmpeg process with pid #{encoded_io.pid.inspect}")
227
+
228
+ begin
229
+ pid = encoded_io.pid
230
+ # Windows does not support TERM as a kill signal, so we use KILL. `Process.waitpid` verifies that our
231
+ # child process has not already completed.
232
+ Process.kill(Gem.win_platform? ? 'KILL' : 'TERM', pid) if Process.waitpid(pid, Process::WNOHANG).nil?
233
+ rescue StandardError => e
234
+ OnyxCord::LOGGER.warn('Failed to kill ffmpeg process! You *might* have a process leak now.')
235
+ OnyxCord::LOGGER.warn("Reason: #{e}")
236
+ end
237
+ end
238
+
239
+ # Close the stream
240
+ encoded_io.close
241
+ end
242
+
243
+ # Plays an encoded audio file of arbitrary format to the channel.
244
+ # @see Encoder#encode_file
245
+ # @see #play
246
+ def play_file(file, options = '')
247
+ play @encoder.encode_file(file, options)
248
+ end
249
+
250
+ # Plays a stream of encoded audio data of arbitrary format to the channel.
251
+ # @see Encoder#encode_io
252
+ # @see #play
253
+ def play_io(io, options = '')
254
+ play @encoder.encode_io(io, options)
255
+ end
256
+
257
+ # Plays a stream of audio data in the DCA format. This format has the advantage that no recoding has to be
258
+ # done - the file contains the data exactly as Discord needs it.
259
+ # @note DCA playback will not be affected by the volume modifier ({#volume}) because the modifier operates on raw
260
+ # PCM, not opus data. Modifying the volume of DCA data would involve decoding it, multiplying the samples and
261
+ # re-encoding it, which defeats its entire purpose (no recoding).
262
+ # @see https://github.com/bwmarrin/dca
263
+ # @see #play
264
+ def play_dca(file)
265
+ stop_playing(true) if @playing
266
+
267
+ @bot.debug "Reading DCA file #{file}"
268
+ input_stream = File.open(file)
269
+
270
+ magic = input_stream.read(4)
271
+ raise ArgumentError, 'Not a DCA1 file! The file might have been corrupted, please recreate it.' unless magic == 'DCA1'
272
+
273
+ # Read the metadata header, then read the metadata and discard it as we don't care about it
274
+ metadata_header = input_stream.read(4).unpack1('l<')
275
+ input_stream.read(metadata_header)
276
+
277
+ # Play the data, without re-encoding it to opus
278
+ play_internal do
279
+ begin
280
+ # Read header
281
+ header_str = input_stream.read(2)
282
+
283
+ unless header_str
284
+ @bot.debug 'Finished DCA parsing (header is nil)'
285
+ next :stop
286
+ end
287
+
288
+ header = header_str.unpack1('s<')
289
+
290
+ raise 'Negative header in DCA file! Your file is likely corrupted.' if header.negative?
291
+ rescue EOFError
292
+ @bot.debug 'Finished DCA parsing (EOFError)'
293
+ next :stop
294
+ end
295
+
296
+ # Read bytes
297
+ input_stream.read(header)
298
+ end
299
+ end
300
+
301
+ alias_method :play_stream, :play_io
302
+
303
+ private
304
+
305
+ # Plays the data from the IO stream as Discord requires it
306
+ def play_internal
307
+ count = 0
308
+ @playing = true
309
+ self.speaking = true
310
+
311
+ last_sent = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
312
+
313
+ loop do
314
+ # If paused, wait
315
+ sleep 0.1 while @paused
316
+
317
+ break unless @playing
318
+
319
+ # Get timestamp before encoding
320
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
321
+
322
+ # If we should skip, get some data, discard it and go to the next iteration
323
+ if @skips.positive?
324
+ @skips -= 1
325
+ yield
326
+ next
327
+ end
328
+
329
+ # Track packet count, sequence and time (Discord requires this)
330
+ count += 1
331
+ increment_packet_headers
332
+
333
+ # Get packet data
334
+ buf = yield
335
+
336
+ # Stop doing anything if the stop signal was sent
337
+ break if buf == :stop
338
+
339
+ # Proceed to the next packet if we got nil
340
+ next unless buf
341
+
342
+ # Track intermediate adjustment so we can measure how much encoding contributes to the total time
343
+ intermediate_adjust = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
344
+
345
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
346
+
347
+ if (last_sent + IDEAL_LENGTH) > Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
348
+ sleep_duration = (last_sent + IDEAL_LENGTH - Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)) / 1000.0
349
+ @bot.debug("Waiting for next frame: #{sleep_duration * 1000}ms (encoding #{intermediate_adjust - start_time}ms)") if @adjust_debug
350
+ sleep sleep_duration if sleep_duration.positive?
351
+ end
352
+
353
+ # Send the packet
354
+ @udp.send_audio(buf, @sequence, @time)
355
+
356
+ # Set the stream time (for tracking how long we've been playing)
357
+ @stream_time = count * IDEAL_LENGTH / 1000
358
+ last_sent = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
359
+ end
360
+
361
+ @bot.debug('Sending five silent frames to clear out buffers')
362
+
363
+ 5.times do
364
+ increment_packet_headers
365
+ @udp.send_audio(Encoder::OPUS_SILENCE, @sequence, @time)
366
+
367
+ # Length adjustments don't matter here, we can just wait 20ms since nobody is going to hear it anyway
368
+ sleep IDEAL_LENGTH / 1000.0
369
+ end
370
+
371
+ @bot.debug('Performing final cleanup after stream ended')
372
+
373
+ # Final clean-up
374
+ stop_playing
375
+
376
+ # Notify any stop_playing methods running right now that we have actually stopped
377
+ @has_stopped_playing = true
378
+ end
379
+
380
+ # Increment sequence and time
381
+ def increment_packet_headers
382
+ @sequence + 10 < 65_535 ? @sequence += 1 : @sequence = 0
383
+ @time + 9600 < 4_294_967_295 ? @time += 960 : @time = 0
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/webhooks/version'
4
+ require 'onyxcord/webhooks/embeds'
5
+ require 'onyxcord/webhooks/client'
6
+ require 'onyxcord/webhooks/builder'
7
+ require 'onyxcord/webhooks/view'
8
+ require 'onyxcord/webhooks/modal'
9
+
10
+ module OnyxCord
11
+ # Webhook client
12
+ module Webhooks
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'websocket-client-simple'
4
+
5
+ module OnyxCord
6
+ # Utility wrapper class that abstracts an instance of WSCS. Useful should we decide that WSCS isn't good either -
7
+ # in that case we can just switch to something else
8
+ class WebSocket
9
+ attr_reader :open_handler, :message_handler, :close_handler, :error_handler
10
+
11
+ # Create a new WebSocket and connect to the given endpoint.
12
+ # @param endpoint [String] Where to connect to.
13
+ # @param open_handler [#call] The handler that should be called when the websocket has opened successfully.
14
+ # @param message_handler [#call] The handler that should be called when the websocket receives a message. The
15
+ # handler can take one parameter which will have a `data` attribute for normal messages and `code` and `data` for
16
+ # close frames.
17
+ # @param close_handler [#call] The handler that should be called when the websocket is closed due to an internal
18
+ # error. The error will be passed as the first parameter to the handler.
19
+ # @param error_handler [#call] The handler that should be called when an error occurs in another handler. The error
20
+ # will be passed as the first parameter to the handler.
21
+ def initialize(endpoint, open_handler, message_handler, close_handler, error_handler)
22
+ OnyxCord::LOGGER.debug "Using WSCS version: #{::WebSocket::Client::Simple::VERSION}"
23
+
24
+ @open_handler = open_handler
25
+ @message_handler = message_handler
26
+ @close_handler = close_handler
27
+ @error_handler = error_handler
28
+
29
+ instance = self # to work around WSCS's weird way of handling blocks
30
+
31
+ @client = ::WebSocket::Client::Simple.connect(endpoint) do |ws|
32
+ ws.on(:open) { instance.open_handler.call }
33
+ ws.on(:message) do |msg|
34
+ # If the message has a code attribute, it is in reality a close message
35
+ if msg.code
36
+ instance.close_handler.call(msg)
37
+ else
38
+ instance.message_handler.call(msg.data)
39
+ end
40
+ end
41
+ ws.on(:close) { |err| instance.close_handler.call(err) }
42
+ ws.on(:error) { |err| instance.error_handler.call(err) }
43
+ end
44
+ end
45
+
46
+ # Send data over this WebSocket
47
+ # @param data [String] What to send
48
+ def send(data)
49
+ @client.send(data)
50
+ end
51
+
52
+ # Close the WebSocket connection
53
+ def close
54
+ @client.close
55
+ end
56
+
57
+ # @return [Thread] the internal WSCS thread
58
+ def thread
59
+ @client.thread
60
+ end
61
+ end
62
+ end
data/lib/onyxcord.rb ADDED
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/version'
4
+ require 'onyxcord/configuration'
5
+ require 'onyxcord/event_executor'
6
+ require 'onyxcord/message_components'
7
+ require 'onyxcord/bot'
8
+ require 'onyxcord/commands/command_bot'
9
+ require 'onyxcord/logger'
10
+
11
+ # All onyxcord functionality, to be extended by other files
12
+ module OnyxCord
13
+ Thread.current[:onyxcord_name] = 'main'
14
+
15
+ # The default debug logger used by onyxcord.
16
+ LOGGER = Logger.new(ENV.fetch('ONYXCORD_FANCY_LOG', false))
17
+
18
+ # The Unix timestamp Discord IDs are based on
19
+ DISCORD_EPOCH = 1_420_070_400_000
20
+
21
+ # Used to declare what events you wish to recieve from Discord.
22
+ # @see https://discord.com/developers/docs/topics/gateway#gateway-intents
23
+ INTENTS = {
24
+ servers: 1 << 0,
25
+ server_members: 1 << 1,
26
+ server_bans: 1 << 2,
27
+ server_emojis: 1 << 3,
28
+ server_integrations: 1 << 4,
29
+ server_webhooks: 1 << 5,
30
+ server_invites: 1 << 6,
31
+ server_voice_states: 1 << 7,
32
+ server_presences: 1 << 8,
33
+ server_messages: 1 << 9,
34
+ server_message_reactions: 1 << 10,
35
+ server_message_typing: 1 << 11,
36
+ direct_messages: 1 << 12,
37
+ direct_message_reactions: 1 << 13,
38
+ direct_message_typing: 1 << 14,
39
+ message_content: 1 << 15,
40
+ server_scheduled_events: 1 << 16,
41
+ server_message_polls: 1 << 24,
42
+ direct_message_polls: 1 << 25
43
+ }.freeze
44
+
45
+ INTENT_ALIASES = {
46
+ guilds: :servers,
47
+ guild_members: :server_members,
48
+ guild_bans: :server_bans,
49
+ guild_emojis: :server_emojis,
50
+ guild_integrations: :server_integrations,
51
+ guild_webhooks: :server_webhooks,
52
+ guild_invites: :server_invites,
53
+ guild_voice_states: :server_voice_states,
54
+ guild_presences: :server_presences,
55
+ guild_messages: :server_messages,
56
+ guild_message_reactions: :server_message_reactions,
57
+ guild_message_typing: :server_message_typing,
58
+ guild_scheduled_events: :server_scheduled_events,
59
+ guild_message_polls: :server_message_polls
60
+ }.freeze
61
+
62
+ MINIMAL_INTENTS = INTENTS.values_at(:servers, :server_messages, :direct_messages).reduce(&:|)
63
+
64
+ # All available intents
65
+ ALL_INTENTS = INTENTS.values.reduce(&:|)
66
+
67
+ # All unprivileged intents
68
+ # @see https://discord.com/developers/docs/topics/gateway#privileged-intents
69
+ UNPRIVILEGED_INTENTS = ALL_INTENTS & ~(INTENTS[:server_members] | INTENTS[:server_presences] | INTENTS[:message_content])
70
+
71
+ # No intents
72
+ NO_INTENTS = 0
73
+
74
+ # Compares two objects based on IDs - either the objects' IDs are equal, or one object is equal to the other's ID.
75
+ def self.id_compare?(one_id, other)
76
+ other.respond_to?(:resolve_id) ? (one_id.resolve_id == other.resolve_id) : (one_id == other)
77
+ end
78
+
79
+ # @deprecated Please use {OnyxCord.id_compare?}
80
+ singleton_class.alias_method :id_compare, :id_compare?
81
+
82
+ # The maximum length a Discord message can have
83
+ CHARACTER_LIMIT = 2000
84
+
85
+ # For creating timestamps with {timestamp}
86
+ # @see https://discord.com/developers/docs/reference#message-formatting-timestamp-styles
87
+ TIMESTAMP_STYLES = {
88
+ short_time: 't', # 16:20
89
+ long_time: 'T', # 16:20:30
90
+ short_date: 'd', # 20/04/2021
91
+ long_date: 'D', # 20 April 2021
92
+ short_datetime: 'f', # 20 April 2021 16:20
93
+ long_datetime: 'F', # Tuesday, 20 April 2021 16:20
94
+ relative: 'R', # 2 months ago
95
+ simple_datetime: 's', # 20/04/2021, 16:20
96
+ medium_datetime: 'S' # 20/04/2021, 16:20:30
97
+ }.freeze
98
+
99
+ # Splits a message into chunks of 2000 characters. Attempts to split by lines if possible.
100
+ # @param msg [String] The message to split.
101
+ # @return [Array<String>] the message split into chunks
102
+ def self.split_message(msg)
103
+ # If the messages is empty, return an empty array
104
+ return [] if msg.empty?
105
+
106
+ # Split the message into lines
107
+ lines = msg.lines
108
+
109
+ # Turn the message into a "triangle" of consecutively longer slices, for example the array [1,2,3,4] would become
110
+ # [
111
+ # [1],
112
+ # [1, 2],
113
+ # [1, 2, 3],
114
+ # [1, 2, 3, 4]
115
+ # ]
116
+ tri = (0...lines.length).map { |i| lines.combination(i + 1).first }
117
+
118
+ # Join the individual elements together to get an array of strings with consecutively more lines
119
+ joined = tri.map(&:join)
120
+
121
+ # Find the largest element that is still below the character limit, or if none such element exists return the first
122
+ ideal = joined.max_by { |e| e.length > CHARACTER_LIMIT ? -1 : e.length }
123
+
124
+ # If it's still larger than the character limit (none was smaller than it) split it into the largest chunk without
125
+ # cutting words apart, breaking on the nearest space within character limit, otherwise just return an array with one element
126
+ ideal_ary = ideal.length > CHARACTER_LIMIT ? ideal.split(/(.{1,#{CHARACTER_LIMIT}}\b|.{1,#{CHARACTER_LIMIT}})/o).reject(&:empty?) : [ideal]
127
+
128
+ # Slice off the ideal part and strip newlines
129
+ rest = msg[ideal.length..].strip
130
+
131
+ # If none remains, return an empty array -> we're done
132
+ return [] unless rest
133
+
134
+ # Otherwise, call the method recursively to split the rest of the string and add it onto the ideal array
135
+ ideal_ary + split_message(rest)
136
+ end
137
+
138
+ # @param time [Time, Integer] The time to create the timestamp from, or a unix timestamp integer.
139
+ # @param style [Symbol, String] One of the keys from {TIMESTAMP_STYLES} or a string with the style.
140
+ # @return [String]
141
+ # @example
142
+ # OnyxCord.timestamp(Time.now, :short_time)
143
+ # # => "<t:1632146954:t>"
144
+ def self.timestamp(time, style = nil)
145
+ if style.nil?
146
+ "<t:#{time.to_i}>"
147
+ else
148
+ "<t:#{time.to_i}:#{TIMESTAMP_STYLES[style] || style}>"
149
+ end
150
+ end
151
+
152
+ # A utility method to base64 encode a file like object using its mime type.
153
+ # @param file [File, #read] A file like object that responds to #read.
154
+ # @return [String] The file object encoded as base64 image data.
155
+ def self.encode64(file)
156
+ path_method = %i[original_filename path local_path].find { |method| file.respond_to?(method) }
157
+
158
+ raise ArgumentError, 'File object must respond to original_filename, path, or local path.' unless path_method
159
+ raise ArgumentError, 'File object must respond to read.' unless file.respond_to?(:read)
160
+
161
+ mime_type = MIME::Types.type_for(file.__send__(path_method)).first&.to_s || 'image/jpeg'
162
+ "data:#{mime_type};base64,#{Base64.encode64(file.read).strip}"
163
+ end
164
+ end
165
+
166
+ # In onyxcord, Integer and {String} are monkey-patched to allow for easy resolution of IDs
167
+ class Integer
168
+ # @return [Integer] The Discord ID represented by this integer, i.e. the integer itself
169
+ def resolve_id
170
+ self
171
+ end
172
+ end
173
+
174
+ # In onyxcord, {Integer} and String are monkey-patched to allow for easy resolution of IDs
175
+ class String
176
+ # @return [Integer] The Discord ID represented by this string, i.e. the string converted to an integer
177
+ def resolve_id
178
+ to_i
179
+ end
180
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/onyxcord/webhooks/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'onyxcord-webhooks'
7
+ spec.version = OnyxCord::Webhooks::VERSION
8
+ spec.authors = ['Gustavo Silva']
9
+ spec.email = ['gustavosilva8kt@gmail.com']
10
+
11
+ spec.summary = 'Webhook client for onyxcord'
12
+ spec.description = "A webhook client for OnyxCord, a Ruby Discord library based on discordrb and updated with Components V2 support."
13
+ spec.homepage = 'https://github.com/kruldevb/OnyxCord'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z lib/onyxcord/webhooks/`.split("\x0") + ['lib/onyxcord/webhooks.rb']
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'rest-client', '>= 2.0.0'
22
+
23
+ spec.required_ruby_version = '>= 3.3'
24
+ spec.metadata = {
25
+ 'bug_tracker_uri' => 'https://github.com/kruldevb/OnyxCord/issues',
26
+ 'documentation_uri' => 'https://github.com/kruldevb/OnyxCord#readme',
27
+ 'source_code_uri' => 'https://github.com/kruldevb/OnyxCord',
28
+ 'rubygems_mfa_required' => 'true'
29
+ }
30
+ end