onyxcord 2.0.0 → 2.0.6

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.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module ApplicationCommands
5
+ class Registry
6
+ attr_reader :bot, :commands
7
+
8
+ def initialize(bot)
9
+ @bot = bot
10
+ @commands = {}
11
+ end
12
+
13
+ def slash(name, description:, **attributes, &block)
14
+ register(Command.chat_input(name, description: description, **attributes, &block))
15
+ end
16
+
17
+ def user(name, **attributes, &block)
18
+ register(Command.user(name, **attributes, &block))
19
+ end
20
+
21
+ def message(name, **attributes, &block)
22
+ register(Command.message(name, **attributes, &block))
23
+ end
24
+
25
+ def register(command)
26
+ @commands[command.name] = command
27
+ wire_handler(command)
28
+ command
29
+ end
30
+
31
+ def sync!(server_id: nil, delete_unknown: false)
32
+ payload = @commands.values.map(&:to_h)
33
+
34
+ if server_id
35
+ @bot.bulk_overwrite_guild_application_commands(server_id, payload)
36
+ else
37
+ @bot.bulk_overwrite_global_application_commands(payload)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def wire_handler(command)
44
+ @bot.application_command(command.name) do |event|
45
+ command.call(Context.new(event, command))
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/application_commands/option'
4
+ require 'onyxcord/application_commands/command'
5
+ require 'onyxcord/application_commands/context'
6
+ require 'onyxcord/application_commands/registry'
7
+
8
+ module OnyxCord
9
+ module ApplicationCommands
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+
5
+ module OnyxCord
6
+ module AsyncRuntime
7
+ module_function
8
+
9
+ def run(&block)
10
+ current = Async::Task.current?
11
+ return yield current if current
12
+
13
+ Async(&block).wait
14
+ end
15
+
16
+ def async(&block)
17
+ current = Async::Task.current?
18
+ return current.async(&block) if current
19
+
20
+ Async(&block)
21
+ end
22
+
23
+ def sleep(duration)
24
+ task = Async::Task.current?
25
+ return task.sleep(duration) if task.respond_to?(:sleep)
26
+
27
+ Kernel.sleep(duration)
28
+ end
29
+ end
30
+ end
data/lib/onyxcord/bot.rb CHANGED
@@ -33,6 +33,7 @@ require 'onyxcord/api/invite'
33
33
  require 'onyxcord/api/interaction'
34
34
  require 'onyxcord/api/application'
35
35
 
36
+ require 'onyxcord/async/runtime'
36
37
  require 'onyxcord/errors'
37
38
  require 'onyxcord/message_components'
38
39
  require 'onyxcord/data'
@@ -42,6 +43,7 @@ require 'onyxcord/websocket'
42
43
  require 'onyxcord/cache'
43
44
  require 'onyxcord/gateway'
44
45
 
46
+ require 'onyxcord/application_commands'
45
47
  require 'onyxcord/voice/voice_bot'
46
48
 
47
49
  module OnyxCord
@@ -309,28 +311,27 @@ module OnyxCord
309
311
  # this. If you need a way to safely run code after the bot is fully
310
312
  # connected, use a {#ready} event handler instead.
311
313
  def run(background = false)
312
- @gateway.run_async
313
- return if background
314
+ if background
315
+ @run_task = OnyxCord::AsyncRuntime.async { run_forever }
316
+ return @run_task
317
+ end
318
+
319
+ OnyxCord::AsyncRuntime.run { run_forever }
320
+ end
314
321
 
315
- debug('Oh wait! Not exiting yet as run was run synchronously.')
316
- @gateway.sync
322
+ def run_forever
323
+ @gateway.run
317
324
  end
318
325
 
319
- # Joins the bot's connection thread with the current thread.
320
- # This blocks execution until the websocket stops, which should only happen
321
- # manually triggered. or due to an error. This is necessary to have a
322
- # continuously running bot.
323
326
  def join
324
- @gateway.sync
327
+ @run_task&.wait
325
328
  end
326
329
  alias_method :sync, :join
327
330
 
328
- # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
329
- # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
330
- # @note This method no longer takes an argument as of 3.4.0
331
331
  def stop(_no_sync = nil)
332
332
  @gateway.stop
333
333
  @event_executor.shutdown
334
+ @run_task&.stop if @run_task.respond_to?(:stop)
334
335
  nil
335
336
  end
336
337
 
@@ -411,7 +412,7 @@ module OnyxCord
411
412
 
412
413
  debug('Voice channel init packet sent! Now waiting.')
413
414
 
414
- sleep(0.05) until @voices[server_id]
415
+ OnyxCord::AsyncRuntime.sleep(0.05) until @voices[server_id]
415
416
  debug('Voice connect succeeded!')
416
417
  @voices[server_id]
417
418
  end
@@ -477,9 +478,9 @@ module OnyxCord
477
478
  # @param enforce_nonce [true, false] Whether the nonce should be enforced and used for message de-duplication.
478
479
  # @param poll [Hash, Poll::Builder, Poll, nil] The poll that should be attached to this message.
479
480
  def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil)
480
- Async do
481
+ OnyxCord::AsyncRuntime.async do
481
482
  message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce, poll)
482
- sleep(timeout)
483
+ OnyxCord::AsyncRuntime.sleep(timeout)
483
484
  message.delete
484
485
  end
485
486
 
@@ -864,6 +865,37 @@ module OnyxCord
864
865
  ApplicationCommand.new(JSON.parse(resp), self, server_id)
865
866
  end
866
867
 
868
+ # @return [OnyxCord::ApplicationCommands::Registry]
869
+ def commands
870
+ @modern_command_registry ||= OnyxCord::ApplicationCommands::Registry.new(self)
871
+ end
872
+
873
+ def slash(name, description: nil, **attributes, &block)
874
+ commands.slash(name, description: description, **attributes, &block)
875
+ end
876
+
877
+ def user_command(name, **attributes, &block)
878
+ commands.user(name, **attributes, &block)
879
+ end
880
+
881
+ def message_command(name, **attributes, &block)
882
+ commands.message(name, **attributes, &block)
883
+ end
884
+
885
+ def sync_application_commands!(server_id: nil, delete_unknown: false)
886
+ commands.sync!(server_id: server_id, delete_unknown: delete_unknown)
887
+ end
888
+
889
+ def bulk_overwrite_global_application_commands(commands)
890
+ response = API::Application.bulk_overwrite_global_commands(@token, profile.id, commands)
891
+ JSON.parse(response).map { |data| ApplicationCommand.new(data, self) }
892
+ end
893
+
894
+ def bulk_overwrite_guild_application_commands(server_id, commands)
895
+ response = API::Application.bulk_overwrite_guild_commands(@token, profile.id, server_id.resolve_id, commands)
896
+ JSON.parse(response).map { |data| ApplicationCommand.new(data, self, server_id) }
897
+ end
898
+
867
899
  # @yieldparam [OptionBuilder]
868
900
  # @yieldparam [PermissionBuilder]
869
901
  # @example
@@ -299,6 +299,10 @@ module OnyxCord
299
299
  Interactions::Message.new(JSON.parse(resp), @bot, self)
300
300
  end
301
301
 
302
+ alias edit_original edit_response
303
+ alias delete_original delete_response
304
+ alias followup send_message
305
+
302
306
  # @param message [String, Integer, InteractionMessage, Message] The message created by this interaction to be edited.
303
307
  # @param content [String] The message content.
304
308
  # @param embeds [Array<Hash, Webhooks::Embed>] The embeds for the message.
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'async'
3
+ require 'onyxcord/async/runtime'
4
4
 
5
5
  module OnyxCord
6
- # Event execution strategies used by bot dispatch.
7
6
  module EventExecutor
8
7
  STOP = Object.new.freeze
9
8
 
10
- # Deterministic executor useful for tests, benchmarks, and tiny bots.
11
9
  class Inline
12
10
  def post
13
11
  yield
@@ -20,8 +18,6 @@ module OnyxCord
20
18
  end
21
19
  end
22
20
 
23
- # Async-based worker pool for event handlers.
24
- # Uses Async tasks (fibers) instead of threads for lightweight concurrency.
25
21
  class Pool
26
22
  attr_reader :queue
27
23
 
@@ -47,7 +43,6 @@ module OnyxCord
47
43
  @queue.size
48
44
  end
49
45
 
50
- # Compatibility with code that checks worker threads.
51
46
  def threads
52
47
  @workers
53
48
  end
@@ -87,6 +82,59 @@ module OnyxCord
87
82
  end
88
83
  end
89
84
 
85
+ class AsyncPool
86
+ attr_reader :queue
87
+
88
+ def initialize(size:, queue_size: nil)
89
+ raise ArgumentError, 'Pool size must be greater than zero' unless size.positive?
90
+
91
+ @size = size
92
+ @queue = ::Async::Queue.new
93
+ @closed = false
94
+ @workers = []
95
+ start_workers
96
+ end
97
+
98
+ def post(&block)
99
+ raise ArgumentError, 'EventExecutor::AsyncPool#post requires a block' unless block
100
+ raise 'Event executor has been shut down' if @closed
101
+
102
+ @queue.enqueue(block)
103
+ end
104
+
105
+ def queue_size
106
+ @queue.size
107
+ end
108
+
109
+ def shutdown
110
+ return if @closed
111
+
112
+ @closed = true
113
+ @size.times { @queue.enqueue(STOP) }
114
+ end
115
+
116
+ def threads
117
+ []
118
+ end
119
+
120
+ private
121
+
122
+ def start_workers
123
+ @workers = Array.new(@size) do
124
+ OnyxCord::AsyncRuntime.async do
125
+ loop do
126
+ job = @queue.dequeue
127
+ break if job.equal?(STOP)
128
+
129
+ job.call
130
+ rescue StandardError => e
131
+ OnyxCord::LOGGER.log_exception(e) if defined?(OnyxCord::LOGGER)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
90
138
  module_function
91
139
 
92
140
  def build(type, workers:, queue_size: nil)
@@ -95,6 +143,8 @@ module OnyxCord
95
143
  Inline.new
96
144
  when :pool
97
145
  Pool.new(size: workers, queue_size: queue_size)
146
+ when :async_pool
147
+ AsyncPool.new(size: workers, queue_size: queue_size)
98
148
  else
99
149
  raise ArgumentError, "Unknown event executor: #{type.inspect}"
100
150
  end
@@ -3,6 +3,7 @@
3
3
  require 'async'
4
4
  require 'async/http/endpoint'
5
5
  require 'async/websocket/client'
6
+ require 'onyxcord/async/runtime'
6
7
  require 'onyxcord/rate_limiter/gateway'
7
8
 
8
9
  module OnyxCord
@@ -86,25 +87,24 @@ module OnyxCord
86
87
 
87
88
  # Connect to the gateway server inside an Async reactor.
88
89
  def run_async
89
- @ws_thread = Thread.new do
90
- Thread.current[:onyxcord_name] = 'gateway'
91
- Async do |task|
92
- @reactor_task = task
93
- connect_loop
94
- LOGGER.warn('The gateway loop exited!')
95
- end
96
- end
90
+ @task = OnyxCord::AsyncRuntime.async { run }
97
91
 
98
- LOGGER.debug('Gateway thread created! Waiting for confirmation...')
92
+ LOGGER.debug('Gateway task created! Waiting for confirmation...')
99
93
  loop do
100
- sleep(0.5)
94
+ OnyxCord::AsyncRuntime.sleep(0.5)
101
95
  break if @ws_success
102
96
  break if @should_reconnect == false
103
97
  end
104
98
  end
105
99
 
100
+ def run
101
+ @reactor_task = Async::Task.current
102
+ connect_loop
103
+ LOGGER.warn('The gateway loop exited!')
104
+ end
105
+
106
106
  def sync
107
- @ws_thread&.join
107
+ @task&.wait
108
108
  end
109
109
 
110
110
  def open?
@@ -118,7 +118,7 @@ module OnyxCord
118
118
  end
119
119
 
120
120
  def kill
121
- @ws_thread&.kill
121
+ @task&.stop
122
122
  end
123
123
 
124
124
  def notify_ready
@@ -224,7 +224,7 @@ module OnyxCord
224
224
  @heartbeat_task = @reactor_task&.async do
225
225
  loop do
226
226
  if (@session && !@session.suspended?) || !@session
227
- sleep @heartbeat_interval
227
+ OnyxCord::AsyncRuntime.sleep(@heartbeat_interval)
228
228
  if !@closed && @connection
229
229
  @bot.raise_heartbeat_event
230
230
  heartbeat
@@ -232,7 +232,7 @@ module OnyxCord
232
232
  LOGGER.debug('Tried to heartbeat without connection — skipping.')
233
233
  end
234
234
  else
235
- sleep 1
235
+ OnyxCord::AsyncRuntime.sleep(1)
236
236
  end
237
237
  rescue StandardError => e
238
238
  LOGGER.error('Error while heartbeating!')
@@ -260,7 +260,7 @@ module OnyxCord
260
260
 
261
261
  def wait_for_reconnect
262
262
  LOGGER.debug("Reconnecting in #{@falloff} seconds.")
263
- sleep @falloff
263
+ OnyxCord::AsyncRuntime.sleep(@falloff)
264
264
  @falloff *= 1.5
265
265
  @falloff = 115 + (rand * 10) if @falloff > 120
266
266
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httpx'
4
+ require 'onyxcord/json'
5
+
6
+ module OnyxCord
7
+ # Modern HTTP adapter wrapping HTTPX with persistent connections, automatic
8
+ # retries on 502, and a response interface compatible with the rest of OnyxCord.
9
+ module HTTP
10
+ # Lightweight response wrapper so call-sites that relied on RestClient's
11
+ # `.body`, `.headers`, `.code` interface keep working unchanged.
12
+ class Response
13
+ # @return [String] the response body
14
+ attr_reader :body
15
+
16
+ # @return [Integer] the HTTP status code
17
+ attr_reader :code
18
+
19
+ # @return [Hash] the response headers (symbol keys, underscored)
20
+ attr_reader :headers
21
+
22
+ def initialize(httpx_response)
23
+ @raw = httpx_response
24
+ @body = httpx_response.body.to_s
25
+ @code = httpx_response.status
26
+ @headers = normalize_headers(httpx_response.headers)
27
+ end
28
+
29
+ def to_s
30
+ @body
31
+ end
32
+
33
+ # RestClient compatibility — some code calls `response` directly as a
34
+ # string (implicit to_s).
35
+ alias_method :to_str, :to_s
36
+
37
+ private
38
+
39
+ def normalize_headers(headers)
40
+ headers.to_h.transform_keys do |key|
41
+ key.to_s.tr('-', '_').downcase.to_sym
42
+ end
43
+ end
44
+ end
45
+
46
+ module_function
47
+
48
+ # The shared HTTPX session with persistent connections.
49
+ def session
50
+ @session ||= HTTPX.plugin(:persistent)
51
+ .plugin(:follow_redirects)
52
+ end
53
+
54
+ # Reset the HTTP session (useful for tests).
55
+ def reset!
56
+ @session = nil
57
+ end
58
+
59
+ # Perform a raw HTTP request and return a {Response}.
60
+ # @param type [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
61
+ # @param url [String] The full URL.
62
+ # @param body [String, Hash, nil] The request body.
63
+ # @param headers [Hash] Request headers.
64
+ # @return [Response]
65
+ def request(type, url, body = nil, **headers)
66
+ http = session.with(headers: headers)
67
+
68
+ raw = case type
69
+ when :get
70
+ http.get(url)
71
+ when :post
72
+ if body.is_a?(Hash) && body.any? { |_, v| v.respond_to?(:read) || v.respond_to?(:path) }
73
+ # Multipart upload
74
+ http.plugin(:multipart).post(url, form: body)
75
+ else
76
+ http.post(url, body: body)
77
+ end
78
+ when :put
79
+ http.put(url, body: body)
80
+ when :patch
81
+ http.patch(url, body: body)
82
+ when :delete
83
+ http.delete(url)
84
+ else
85
+ raise ArgumentError, "Unknown HTTP method: #{type}"
86
+ end
87
+
88
+ # HTTPX returns an error response object on network failures
89
+ raise raw.error if raw.is_a?(HTTPX::ErrorResponse)
90
+
91
+ Response.new(raw)
92
+ end
93
+
94
+ # Convenience wrappers
95
+ def get(url, **headers)
96
+ request(:get, url, nil, **headers)
97
+ end
98
+
99
+ def post(url, body = nil, **headers)
100
+ request(:post, url, body, **headers)
101
+ end
102
+
103
+ def put(url, body = nil, **headers)
104
+ request(:put, url, body, **headers)
105
+ end
106
+
107
+ def patch(url, body = nil, **headers)
108
+ request(:patch, url, body, **headers)
109
+ end
110
+
111
+ def delete(url, **headers)
112
+ request(:delete, url, nil, **headers)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+
5
+ # Configure Oj in compat mode so that all stdlib JSON.parse / .to_json calls
6
+ # are transparently accelerated. This is loaded early by the main onyxcord.rb
7
+ # entry point so every module benefits automatically.
8
+ module OnyxCord
9
+ # Fast Oj-backed JSON wrapper providing compatibility with stdlib JSON methods.
10
+ module JSON
11
+ Oj.default_options = { mode: :compat, symbol_keys: false }
12
+
13
+ module_function
14
+
15
+ # Fast JSON decode using Oj via stdlib JSON compatibility.
16
+ # @param data [String] The JSON string to decode.
17
+ # @return [Hash, Array, String, Numeric, nil]
18
+ def decode(data, *args)
19
+ ::JSON.parse(data, *args)
20
+ end
21
+
22
+ # Fast JSON encode using Oj via stdlib JSON compatibility.
23
+ # @param data [Object] The object to encode as JSON.
24
+ # @return [String]
25
+ def encode(data, *args)
26
+ ::JSON.generate(data, *args)
27
+ end
28
+
29
+ # Alias parse to decode for standard library compatibility
30
+ def parse(data, *args)
31
+ ::JSON.parse(data, *args)
32
+ end
33
+
34
+ # Alias generate to encode for standard library compatibility
35
+ def generate(data, *args)
36
+ ::JSON.generate(data, *args)
37
+ end
38
+
39
+ # Alias load to decode for Oj/JSON compatibility
40
+ def load(data, *args)
41
+ ::JSON.parse(data, *args)
42
+ end
43
+
44
+ # Alias dump to encode for Oj/JSON compatibility
45
+ def dump(data, *args)
46
+ ::JSON.generate(data, *args)
47
+ end
48
+ end
49
+ end