onyxcord 3.2.8 → 4.0.0.beta.1

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/onyxcord/application_commands.rb +2 -0
  3. data/lib/onyxcord/bot.rb +12 -6
  4. data/lib/onyxcord/cache/manager.rb +9 -9
  5. data/lib/onyxcord/cogs/bot.rb +28 -0
  6. data/lib/onyxcord/cogs/cog.rb +264 -0
  7. data/lib/onyxcord/cogs/cog_meta.rb +61 -0
  8. data/lib/onyxcord/cogs/extensions.rb +109 -0
  9. data/lib/onyxcord/commands/bot/channels.rb +1 -0
  10. data/lib/onyxcord/commands/bot/execution.rb +1 -0
  11. data/lib/onyxcord/commands/container.rb +51 -0
  12. data/lib/onyxcord/commands/parser.rb +61 -7
  13. data/lib/onyxcord/commands/rate_limiter.rb +2 -2
  14. data/lib/onyxcord/core/bot/application_commands.rb +20 -16
  15. data/lib/onyxcord/core/bot/messaging.rb +2 -2
  16. data/lib/onyxcord/core/bot/oauth.rb +1 -1
  17. data/lib/onyxcord/core/version.rb +1 -1
  18. data/lib/onyxcord/events/interactions/autocomplete.rb +21 -6
  19. data/lib/onyxcord/gateway/client.rb +29 -14
  20. data/lib/onyxcord/interactions/command.rb +27 -2
  21. data/lib/onyxcord/interactions/internal/application_command.rb +1 -1
  22. data/lib/onyxcord/interactions/registry.rb +36 -0
  23. data/lib/onyxcord/internal/event_bus.rb +5 -3
  24. data/lib/onyxcord/internal/event_executor.rb +7 -0
  25. data/lib/onyxcord/internal/http.rb +18 -2
  26. data/lib/onyxcord/internal/json.rb +26 -22
  27. data/lib/onyxcord/internal/rate_limiter/async_rest.rb +4 -3
  28. data/lib/onyxcord/internal/rate_limiter/rest.rb +5 -3
  29. data/lib/onyxcord/light/light_bot.rb +3 -3
  30. data/lib/onyxcord/models/application.rb +1 -1
  31. data/lib/onyxcord/models/channel.rb +19 -17
  32. data/lib/onyxcord/models/component.rb +3 -3
  33. data/lib/onyxcord/models/interaction.rb +5 -5
  34. data/lib/onyxcord/models/member.rb +8 -8
  35. data/lib/onyxcord/models/message.rb +5 -5
  36. data/lib/onyxcord/models/poll.rb +2 -2
  37. data/lib/onyxcord/models/profile.rb +4 -4
  38. data/lib/onyxcord/models/role.rb +1 -1
  39. data/lib/onyxcord/models/scheduled_event.rb +3 -3
  40. data/lib/onyxcord/models/server.rb +46 -34
  41. data/lib/onyxcord/models/user.rb +1 -1
  42. data/lib/onyxcord/models/webhook.rb +7 -7
  43. data/lib/onyxcord/rest/client.rb +38 -6
  44. data/lib/onyxcord/voice/client.rb +2 -0
  45. data/lib/onyxcord/voice/network/udp.rb +4 -1
  46. data/lib/onyxcord/voice/network/websocket.rb +8 -3
  47. data/lib/onyxcord/webhooks/view/container_builder.rb +6 -1
  48. data/lib/onyxcord.rb +5 -1
  49. metadata +5 -1
@@ -83,6 +83,27 @@ module OnyxCord::Commands
83
83
  @commands.delete name
84
84
  end
85
85
 
86
+ # Creates a command group. Commands defined inside the block will be prefixed with the group name.
87
+ #
88
+ # @example
89
+ # group :admin do
90
+ # command(:ban) { |event, *args| "Banned #{args.first}" }
91
+ # command(:kick) { |event, *args| "Kicked #{args.first}" }
92
+ # end
93
+ # # Creates :admin_ban and :admin_kick commands
94
+ #
95
+ # @param group_name [Symbol] The group prefix.
96
+ # @param attributes [Hash] Shared attributes applied to all commands in the group.
97
+ # @yield The block defines commands within the group.
98
+ # @return [self]
99
+ def group(group_name, attributes = {}, &block)
100
+ raise ArgumentError, 'Block required for group' unless block_given?
101
+
102
+ proxy = GroupProxy.new(self, group_name, attributes)
103
+ proxy.instance_exec(&block)
104
+ self
105
+ end
106
+
86
107
  # Adds all commands from another container into this one. Existing commands will be overwritten.
87
108
  # @param container [Module] A module that `extend`s {CommandContainer} from which the commands will be added.
88
109
  def include_commands(container)
@@ -93,6 +114,21 @@ module OnyxCord::Commands
93
114
  @commands.merge! handlers
94
115
  end
95
116
 
117
+ # Yields each command in this container, flattening subcommands.
118
+ # @yieldparam name [Symbol] The command name.
119
+ # @yieldparam cmd [Command] The command object.
120
+ # @return [self]
121
+ def walk_commands
122
+ return self unless block_given?
123
+
124
+ @commands&.each do |name, cmd|
125
+ next if cmd.is_a?(CommandAlias)
126
+
127
+ yield name, cmd
128
+ end
129
+ self
130
+ end
131
+
96
132
  # Registers a before or after hook on an existing command.
97
133
  # @param command_name [Symbol] The name of the command to hook.
98
134
  # @param type [:before, :after] The hook type.
@@ -128,4 +164,19 @@ module OnyxCord::Commands
128
164
  end
129
165
  end
130
166
  end
167
+
168
+ # @private
169
+ # Proxy object used inside a {CommandContainer#group} block to prefix command names.
170
+ class GroupProxy
171
+ def initialize(container, group_name, shared_attributes)
172
+ @container = container
173
+ @group_name = group_name
174
+ @shared_attributes = shared_attributes
175
+ end
176
+
177
+ def command(name, attributes = {}, &block)
178
+ merged = @shared_attributes.merge(attributes)
179
+ @container.command(:"#{@group_name}_#{name}", merged, &block)
180
+ end
181
+ end
131
182
  end
@@ -12,6 +12,7 @@ module OnyxCord::Commands
12
12
  # @!visibility private
13
13
  def initialize(name, attributes = {}, &block)
14
14
  @name = name
15
+ @subcommands = {}
15
16
  @attributes = {
16
17
  # The lowest permission level that can use the command
17
18
  permission_level: attributes[:permission_level] || 0,
@@ -65,6 +66,9 @@ module OnyxCord::Commands
65
66
  # Block for handling internal exceptions, or a string to respond with
66
67
  rescue: attributes[:rescue],
67
68
 
69
+ # Whether the command executes even when a subcommand is provided
70
+ invoke_without_command: attributes[:invoke_without_command] || false,
71
+
68
72
  # A list of aliases that reference this command
69
73
  aliases: attributes[:aliases] || []
70
74
  }
@@ -94,6 +98,43 @@ module OnyxCord::Commands
94
98
  self
95
99
  end
96
100
 
101
+ # Registers a subcommand on this command.
102
+ # @param name [Symbol] The subcommand name.
103
+ # @param attributes [Hash] Attributes forwarded to the subcommand.
104
+ # @yieldparam event [CommandEvent] The event.
105
+ # @yieldparam args [Array<String>] Remaining arguments.
106
+ # @return [Command] The newly created subcommand.
107
+ def subcommand(name, attributes = {}, &block)
108
+ sub = self.class.new(name, attributes, &block)
109
+ @subcommands[name] = sub
110
+ sub
111
+ end
112
+
113
+ # @return [Hash{Symbol => Command}] subcommands registered on this command.
114
+ attr_reader :subcommands
115
+
116
+ # Finds a subcommand by name.
117
+ # @param name [Symbol, String] The subcommand name.
118
+ # @return [Command, nil]
119
+ def find_subcommand(name)
120
+ @subcommands[name.to_sym]
121
+ end
122
+
123
+ def copy_for(receiver = nil)
124
+ block = @block
125
+ copied = self.class.new(@name, @attributes.dup) do |event, *arguments|
126
+ receiver ? receiver.instance_exec(event, *arguments, &block) : block.call(event, *arguments)
127
+ end
128
+ @before_hooks.each { |hook| copied.before { |event, *args| receiver ? receiver.instance_exec(event, *args, &hook) : hook.call(event, *args) } }
129
+ @after_hooks.each { |hook| copied.after { |event, *args| receiver ? receiver.instance_exec(event, *args, &hook) : hook.call(event, *args) } }
130
+ @subcommands.each { |name, sub| copied.subcommand(name) { |event, *args| sub.call(event, args) } }
131
+ copied.instance_variable_set(:@check, proc { |event| receiver.__send__(:passed_cog_command_check?, event) }) if receiver
132
+ copied.instance_variable_set(:@error_handler, proc { |event, error| receiver.__send__(:handled_cog_command_error?, event, error) }) if receiver
133
+ copied.instance_variable_set(:@cog_before_invoke, proc { |event| receiver.__send__(:invoke_cog_before_invoke, event) }) if receiver
134
+ copied.instance_variable_set(:@cog_after_invoke, proc { |event, result| receiver.__send__(:invoke_cog_after_invoke, event, result) }) if receiver
135
+ copied
136
+ end
137
+
97
138
  # Calls this command and executes the code inside.
98
139
  # @param event [CommandEvent] The event to call the command with.
99
140
  # @param arguments [Array<String>] The attributes for the command.
@@ -127,6 +168,10 @@ module OnyxCord::Commands
127
168
  end
128
169
  end
129
170
 
171
+ return unless @check.nil? || @check.call(event)
172
+
173
+ @cog_before_invoke&.call(event)
174
+
130
175
  cancelled = @before_hooks.any? { |hook| hook.call(event, *arguments).is_a?(FalseClass) }
131
176
  return if cancelled
132
177
 
@@ -134,20 +179,29 @@ module OnyxCord::Commands
134
179
  event.drain_into(result)
135
180
 
136
181
  @after_hooks.each { |hook| hook.call(event, *arguments, result) }
182
+ @cog_after_invoke&.call(event, result)
137
183
 
138
184
  result
139
185
  rescue LocalJumpError => e # occurs when breaking
140
186
  result = e.exit_value
141
187
  event.drain_into(result)
142
188
  rescue StandardError => e # Something went wrong inside our @block!
143
- rescue_value = @attributes[:rescue] || event.bot.attributes[:rescue]
144
- if rescue_value
145
- event.respond(rescue_value.gsub('%exception%', e.message)) if rescue_value.is_a?(String)
146
- rescue_value.call(event, e) if rescue_value.respond_to?(:call)
147
- end
189
+ rescue_value = @attributes[:rescue]
190
+ return handle_rescue_value(rescue_value, event, e) if rescue_value
191
+ return if @error_handler&.call(event, e)
192
+
193
+ bot_rescue = event.bot.attributes[:rescue]
194
+ return handle_rescue_value(bot_rescue, event, e) if bot_rescue
148
195
 
149
196
  raise e
150
197
  end
198
+
199
+ private
200
+
201
+ def handle_rescue_value(rescue_value, event, error)
202
+ event.respond(rescue_value.gsub('%exception%', error.message)) if rescue_value.is_a?(String)
203
+ rescue_value.call(event, error) if rescue_value.respond_to?(:call)
204
+ end
151
205
  end
152
206
 
153
207
  # A command that references another command
@@ -190,10 +244,10 @@ module OnyxCord::Commands
190
244
 
191
245
  @chain.each_char.with_index do |char, index|
192
246
  # Escape character
193
- if char == '\\' && !escaped
247
+ if char == '\\' && !escaped && b_level <= 0
194
248
  escaped = true
195
249
  next
196
- elsif escaped && b_level <= 0
250
+ elsif escaped
197
251
  result += char
198
252
  escaped = false
199
253
  next
@@ -25,10 +25,10 @@ module OnyxCord::Commands
25
25
 
26
26
  @bucket.delete_if do |_, limit_hash|
27
27
  # Time limit has not run out
28
- return false if @time_span && rate_limit_time < (limit_hash[:set_time] + @time_span)
28
+ next false if @time_span && rate_limit_time < (limit_hash[:set_time] + @time_span)
29
29
 
30
30
  # Delay has not run out
31
- return false if @delay && rate_limit_time < (limit_hash[:last_time] + @delay)
31
+ next false if @delay && rate_limit_time < (limit_hash[:last_time] + @delay)
32
32
 
33
33
  true
34
34
  end
@@ -13,7 +13,7 @@ module OnyxCord
13
13
  REST::Application.get_global_commands(@token, profile.id)
14
14
  end
15
15
 
16
- JSON.parse(resp).map do |command_data|
16
+ OnyxCord::Internal::JSON.parse(resp).map do |command_data|
17
17
  ApplicationCommand.new(command_data, self, server_id)
18
18
  end
19
19
  end
@@ -27,38 +27,42 @@ module OnyxCord
27
27
  else
28
28
  REST::Application.get_global_command(@token, profile.id, command_id)
29
29
  end
30
- ApplicationCommand.new(JSON.parse(resp), self, server_id)
30
+ ApplicationCommand.new(OnyxCord::Internal::JSON.parse(resp), self, server_id)
31
31
  end
32
32
 
33
33
  # @return [OnyxCord::Interactions::Registry]
34
34
  def commands
35
- @commands ||= ApplicationCommands::Registry.new(self)
35
+ @commands ||= application_command_registry
36
+ end
37
+
38
+ def application_command_registry
39
+ @application_command_registry ||= ::OnyxCord::ApplicationCommands::Registry.new(self)
36
40
  end
37
41
 
38
42
  def slash(name, description: nil, **attributes, &block)
39
- commands.slash(name, description: description, **attributes, &block)
43
+ application_command_registry.slash(name, description: description, **attributes, &block)
40
44
  end
41
45
 
42
46
  def user_command(name, **attributes, &block)
43
- commands.user(name, **attributes, &block)
47
+ application_command_registry.user(name, **attributes, &block)
44
48
  end
45
49
 
46
50
  def message_command(name, **attributes, &block)
47
- commands.message(name, **attributes, &block)
51
+ application_command_registry.message(name, **attributes, &block)
48
52
  end
49
53
 
50
54
  def sync_application_commands!(server_id: nil, delete_unknown: false)
51
- commands.sync!(server_id: server_id, delete_unknown: delete_unknown)
55
+ application_command_registry.sync!(server_id: server_id, delete_unknown: delete_unknown)
52
56
  end
53
57
 
54
58
  def bulk_overwrite_global_application_commands(commands)
55
59
  response = REST::Application.bulk_overwrite_global_commands(@token, profile.id, commands)
56
- JSON.parse(response).map { |data| ApplicationCommand.new(data, self) }
60
+ OnyxCord::Internal::JSON.parse(response).map { |data| ApplicationCommand.new(data, self) }
57
61
  end
58
62
 
59
63
  def bulk_overwrite_guild_application_commands(server_id, commands)
60
64
  response = REST::Application.bulk_overwrite_guild_commands(@token, profile.id, server_id.resolve_id, commands)
61
- JSON.parse(response).map { |data| ApplicationCommand.new(data, self, server_id) }
65
+ OnyxCord::Internal::JSON.parse(response).map { |data| ApplicationCommand.new(data, self, server_id) }
62
66
  end
63
67
 
64
68
  # @yieldparam [OptionBuilder]
@@ -91,7 +95,7 @@ module OnyxCord
91
95
  else
92
96
  REST::Application.create_global_command(@token, profile.id, name, description, builder.to_a, default_permission, type, default_member_permissions&.to_s, contexts, nsfw, integration_types)
93
97
  end
94
- cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
98
+ cmd = ApplicationCommand.new(OnyxCord::Internal::JSON.parse(resp), self, server_id)
95
99
 
96
100
  if permission_builder.to_a.any?
97
101
  raise ArgumentError, 'Permissions can only be set for guild commands' unless server_id
@@ -121,7 +125,7 @@ module OnyxCord
121
125
  else
122
126
  REST::Application.edit_global_command(@token, profile.id, command_id, name, description, builder.to_a, default_permission, type, default_member_permissions&.to_s, contexts, nsfw, integration_types)
123
127
  end
124
- cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
128
+ cmd = ApplicationCommand.new(OnyxCord::Internal::JSON.parse(resp), self, server_id)
125
129
 
126
130
  if permission_builder.to_a.any?
127
131
  raise ArgumentError, 'Permissions can only be set for guild commands' unless server_id
@@ -163,14 +167,14 @@ module OnyxCord
163
167
  # @return [Array<ApplicationCommand::Permission>] The permissions for all of the application commands in the given server.
164
168
  def application_command_permissions(server_id:)
165
169
  response = REST::Application.get_guild_application_command_permissions(@token, profile.id, server_id.resolve_id)
166
- JSON.parse(response).flat_map { |data| data['permissions'].map { |inner| ApplicationCommand::Permission.new(inner, data, self) } }
170
+ OnyxCord::Internal::JSON.parse(response).flat_map { |data| data['permissions'].map { |inner| ApplicationCommand::Permission.new(inner, data, self) } }
167
171
  end
168
172
 
169
173
  # Fetches all the application emojis that the bot can use.
170
174
  # @return [Array<Emoji>] Returns an array of emoji objects.
171
175
  def application_emojis
172
176
  response = REST::Application.list_application_emojis(@token, profile.id)
173
- JSON.parse(response)['items'].map { |emoji| Emoji.new(emoji, self) }
177
+ OnyxCord::Internal::JSON.parse(response)['items'].map { |emoji| Emoji.new(emoji, self) }
174
178
  end
175
179
 
176
180
  # Fetches a single application emoji from its ID.
@@ -178,7 +182,7 @@ module OnyxCord
178
182
  # @return [Emoji] The application emoji.
179
183
  def application_emoji(emoji_id)
180
184
  response = REST::Application.get_application_emoji(@token, profile.id, emoji_id.resolve_id)
181
- Emoji.new(JSON.parse(response), self)
185
+ Emoji.new(OnyxCord::Internal::JSON.parse(response), self)
182
186
  end
183
187
 
184
188
  # Creates a new custom emoji that can be used by this application.
@@ -188,7 +192,7 @@ module OnyxCord
188
192
  def create_application_emoji(name:, image:)
189
193
  image = image.respond_to?(:read) ? OnyxCord.encode64(image) : image
190
194
  response = REST::Application.create_application_emoji(@token, profile.id, name, image)
191
- Emoji.new(JSON.parse(response), self)
195
+ Emoji.new(OnyxCord::Internal::JSON.parse(response), self)
192
196
  end
193
197
 
194
198
  # Edits an existing application emoji.
@@ -197,7 +201,7 @@ module OnyxCord
197
201
  # @return [Emoji] Returns the updated emoji object on success.
198
202
  def edit_application_emoji(emoji_id, name:)
199
203
  response = REST::Application.edit_application_emoji(@token, profile.id, emoji_id.resolve_id, name)
200
- Emoji.new(JSON.parse(response), self)
204
+ Emoji.new(OnyxCord::Internal::JSON.parse(response), self)
201
205
  end
202
206
 
203
207
  # Deletes an existing application emoji.
@@ -25,7 +25,7 @@ module OnyxCord
25
25
  flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
26
26
 
27
27
  response = REST::Channel.create_message(token, channel, content, tts, embeds, nonce, attachments, allowed_mentions&.to_hash, message_reference, components, flags, enforce_nonce, poll&.to_h)
28
- Message.new(JSON.parse(response), self)
28
+ Message.new(OnyxCord::Internal::JSON.parse(response), self)
29
29
  end
30
30
 
31
31
  # Sends a text message to a channel given its ID and the message's content,
@@ -75,7 +75,7 @@ module OnyxCord
75
75
 
76
76
  channel = channel.resolve_id
77
77
  response = REST::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
78
- Message.new(JSON.parse(response), self)
78
+ Message.new(OnyxCord::Internal::JSON.parse(response), self)
79
79
  end
80
80
 
81
81
  # Gets the users, channels, roles and emoji from a string.
@@ -9,7 +9,7 @@ module OnyxCord
9
9
  # @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
10
10
  # @return [Array(String, String)] your applications' client ID and client secret to be used in OAuth authorization.
11
11
  def create_oauth_application(name, redirect_uris)
12
- response = JSON.parse(REST.create_oauth_application(@token, name, redirect_uris))
12
+ response = OnyxCord::Internal::JSON.parse(REST.create_oauth_application(@token, name, redirect_uris))
13
13
  [response['id'], response['secret']]
14
14
  end
15
15
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnyxCord
4
- VERSION = '3.2.8'
4
+ VERSION = '4.0.0.beta.1'
5
5
  end
@@ -28,15 +28,13 @@ module OnyxCord::Events
28
28
  # @return [String] Name of the currently focused option.
29
29
  attr_reader :focused
30
30
 
31
- # @return [Hash] An empty hash that can be used to return choices by adding K/V pairs.
32
- attr_reader :choices
31
+ # @return [String, Integer, Float, nil] The current value typed by the user (if any).
32
+ attr_reader :value
33
33
 
34
34
  # @!visibility private
35
35
  def initialize(data, bot)
36
36
  super
37
37
 
38
- @choices = {}
39
-
40
38
  options = data['data']['options']
41
39
 
42
40
  options = case options[0]['type']
@@ -48,14 +46,31 @@ module OnyxCord::Events
48
46
  options
49
47
  end
50
48
 
51
- @focused = options.find { |opt| opt.key?('focused') }['name']
49
+ focused_option = options.find { |opt| opt.key?('focused') }
50
+ @focused = focused_option['name']
51
+ @value = focused_option['value']
52
52
  end
53
53
 
54
54
  # Respond to this interaction with autocomplete choices.
55
- # @param choices [Array<Hash>, Hash, nil] Autocomplete choices to return.
55
+ # @param choices [Array<Hash>, Hash] Autocomplete choices to return.
56
+ # When a Hash, keys are names and values are the choice values.
57
+ # When an Array, each element is a Hash with :name and :value keys.
56
58
  def respond(choices:)
57
59
  @interaction.show_autocomplete_choices(choices || [])
58
60
  end
61
+
62
+ # Convenience method to add a single choice and respond.
63
+ # @param name [String] The display name of the choice.
64
+ # @param value [String, Integer, Float] The value sent to the bot when selected.
65
+ def add_choice(name, value)
66
+ respond(choices: [{ name: name.to_s, value: value }])
67
+ end
68
+
69
+ # Convenience method to respond with multiple name/value pairs.
70
+ # @param pairs [Hash{String => String, Integer, Float}] Name => Value pairs.
71
+ def add_choices(pairs)
72
+ respond(choices: pairs.map { |n, v| { name: n.to_s, value: v } })
73
+ end
59
74
  end
60
75
 
61
76
  # An event for whenever an application command's permissions are updated.
@@ -28,6 +28,7 @@ module OnyxCord
28
28
  @compress_mode = compress_mode
29
29
  @intents = intents
30
30
  @send_limiter = Internal::RateLimiter::Gateway.new
31
+ @zlib_mutex = Mutex.new
31
32
  @connection = nil
32
33
  @closed = true
33
34
  @pipe_broken = false
@@ -62,6 +63,7 @@ module OnyxCord
62
63
 
63
64
  def stop
64
65
  @should_reconnect = false
66
+ @intentional_shutdown = true
65
67
  close
66
68
  nil
67
69
  end
@@ -75,8 +77,8 @@ module OnyxCord
75
77
  end
76
78
 
77
79
  def inject_reconnect(url = nil)
78
- data = url ? { url: url } : nil
79
- handle_message({ op: Internal::Gateway::Opcodes::RECONNECT, d: data }.to_json)
80
+ data = url ? { op: Internal::Gateway::Opcodes::RECONNECT, d: { url: url } } : { op: Internal::Gateway::Opcodes::RECONNECT, d: nil }
81
+ handle_message(data.to_json)
80
82
  end
81
83
 
82
84
  def inject_resume(seq)
@@ -153,6 +155,8 @@ module OnyxCord
153
155
  @session.invalidate if attempt_resume && @session && !@received_hello
154
156
  @instant_reconnect = true
155
157
  @should_reconnect = true
158
+ @heartbeat_task&.stop
159
+ @heartbeat_task = nil
156
160
  close(4000)
157
161
  end
158
162
 
@@ -226,7 +230,7 @@ module OnyxCord
226
230
 
227
231
  def find_gateway
228
232
  response = REST.gateway(@token)
229
- JSON.parse(response)['url']
233
+ OnyxCord::Internal::JSON.parse(response)['url']
230
234
  end
231
235
 
232
236
  def process_gateway
@@ -267,10 +271,12 @@ module OnyxCord
267
271
  end
268
272
  rescue Protocol::WebSocket::ClosedError, EOFError => e
269
273
  LOGGER.warn("Gateway websocket closed (#{e.class}); reconnecting.")
270
- begin
271
- @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
272
- rescue StandardError
273
- nil
274
+ unless @intentional_shutdown
275
+ begin
276
+ @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
277
+ rescue StandardError
278
+ nil
279
+ end
274
280
  end
275
281
  rescue StandardError => e
276
282
  @session&.invalidate unless @received_hello
@@ -303,15 +309,17 @@ module OnyxCord
303
309
  def handle_message(msg)
304
310
  case @compress_mode
305
311
  when :large
306
- msg = Zlib::Inflate.inflate(msg) if msg.byteslice(0) == 'x'
312
+ msg = @zlib_mutex.synchronize { Zlib::Inflate.inflate(msg) } if msg.byteslice(0) == 'x'
307
313
  when :stream
308
- @zlib_reader << msg
309
- return if msg.bytesize < 4 || msg.byteslice(-4, 4) != ZLIB_SUFFIX
314
+ @zlib_mutex.synchronize do
315
+ @zlib_reader << msg
316
+ return if msg.bytesize < 4 || msg.byteslice(-4, 4) != ZLIB_SUFFIX
310
317
 
311
- msg = @zlib_reader.inflate('')
318
+ msg = @zlib_reader.inflate('')
319
+ end
312
320
  end
313
321
 
314
- packet = JSON.parse(msg)
322
+ packet = OnyxCord::Internal::JSON.parse(msg)
315
323
  op = packet['op'].to_i
316
324
 
317
325
  LOGGER.in(packet)
@@ -374,11 +382,12 @@ module OnyxCord
374
382
  reconnect
375
383
  else
376
384
  @session.invalidate
385
+ identify
377
386
  end
378
387
  else
379
388
  LOGGER.warn('Op 9 without session!')
389
+ identify
380
390
  end
381
- identify
382
391
  end
383
392
 
384
393
  # Op 10
@@ -414,7 +423,7 @@ module OnyxCord
414
423
  end
415
424
 
416
425
  def handle_close(e)
417
- @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot))
426
+ @bot.__send__(:raise_event, Events::DisconnectEvent.new(@bot)) unless @intentional_shutdown
418
427
 
419
428
  if e.respond_to?(:code)
420
429
  LOGGER.error("WebSocket close frame! Code: #{e.code}")
@@ -454,6 +463,12 @@ module OnyxCord
454
463
  # Ignore close errors
455
464
  end
456
465
 
466
+ begin
467
+ @zlib_reader&.finish
468
+ rescue StandardError
469
+ nil
470
+ end
471
+ @zlib_reader = nil
457
472
  @connection = nil
458
473
  handle_close(nil)
459
474
  end
@@ -53,13 +53,38 @@ module OnyxCord
53
53
  end
54
54
 
55
55
  def execute(&block)
56
- @executor = block
56
+ @executor = @receiver ? proc { |context| @receiver.instance_exec(context, &block) } : block
57
57
  end
58
58
 
59
59
  def call(context)
60
60
  return unless @executor
61
+ return unless @check.nil? || @check.call(context, self)
61
62
 
62
- @executor.call(context)
63
+ @cog_before_invoke&.call(context, self)
64
+ result = @executor.call(context)
65
+ @cog_after_invoke&.call(context, result, self)
66
+ result
67
+ rescue StandardError => e
68
+ return if @error_handler&.call(context, e, self)
69
+
70
+ raise e
71
+ end
72
+
73
+ def copy_for(receiver = nil)
74
+ copied = self.class.new(@name, description: @description, type: @type, **@attributes, &@block)
75
+ copied.instance_variable_set(:@receiver, receiver)
76
+ copied.instance_variable_set(:@check, proc { |context, command| receiver.__send__(:passed_cog_application_command_check?, context, command) }) if receiver
77
+ copied.instance_variable_set(:@error_handler, proc { |context, error, command| receiver.__send__(:handled_cog_application_command_error?, context, error, command) }) if receiver
78
+ copied.instance_variable_set(:@cog_before_invoke, proc { |context, command| receiver.__send__(:invoke_cog_application_before_invoke, context, command) }) if receiver
79
+ copied.instance_variable_set(:@cog_after_invoke, proc { |context, result, command| receiver.__send__(:invoke_cog_application_after_invoke, context, result, command) }) if receiver
80
+ copied.instance_variable_set(:@options, @options.map(&:dup))
81
+
82
+ if @executor
83
+ executor = @executor
84
+ copied.execute { |context| receiver ? receiver.instance_exec(context, &executor) : executor.call(context) }
85
+ end
86
+
87
+ copied
63
88
  end
64
89
 
65
90
  def to_h
@@ -97,7 +97,7 @@ module OnyxCord
97
97
  def permissions(server_id: nil)
98
98
  raise ArgumentError, 'A server ID must be provided for global application commands' if @server_id.nil? && server_id.nil?
99
99
 
100
- response = JSON.parse(REST::Application.get_application_command_permissions(@bot.token, @bot.profile.id, @server_id || server_id&.resolve_id, @id))
100
+ response = OnyxCord::Internal::JSON.parse(REST::Application.get_application_command_permissions(@bot.token, @bot.profile.id, @server_id || server_id&.resolve_id, @id))
101
101
  response['permissions'].map { |permission| Permission.new(permission, response, @bot) }
102
102
  rescue OnyxCord::Errors::UnknownError
103
103
  # If there aren't any explicit overwrites configured for the command, the response is a 400.
@@ -8,6 +8,8 @@ module OnyxCord
8
8
  def initialize(bot)
9
9
  @bot = bot
10
10
  @commands = {}
11
+ @pending_sync = nil
12
+ @sync_mutex = Mutex.new
11
13
  end
12
14
 
13
15
  def slash(name, description:, **attributes, &block)
@@ -23,11 +25,16 @@ module OnyxCord
23
25
  end
24
26
 
25
27
  def register(command)
28
+ command.parse(&command.block) if command.block && command.options.empty?
26
29
  @commands[command.name] = command
27
30
  wire_handler(command)
28
31
  command
29
32
  end
30
33
 
34
+ # Immediately sync all commands to Discord.
35
+ # @param server_id [Integer, String, nil] Guild ID for guild commands, or nil for global.
36
+ # @param delete_unknown [Boolean] Whether to remove commands not in the registry.
37
+ # @return [void]
31
38
  def sync!(server_id: nil, delete_unknown: false) # rubocop:disable Lint/UnusedMethodArgument
32
39
  payload = @commands.values.map(&:to_h)
33
40
 
@@ -38,6 +45,35 @@ module OnyxCord
38
45
  end
39
46
  end
40
47
 
48
+ # Sync global application commands.
49
+ # @return [void]
50
+ def sync_global_commands
51
+ sync!(server_id: nil)
52
+ end
53
+
54
+ # Sync guild application commands.
55
+ # @param server_id [Integer, String] The guild ID.
56
+ # @return [void]
57
+ def sync_server_commands(server_id)
58
+ sync!(server_id: server_id)
59
+ end
60
+
61
+ # Schedule a debounced sync. Multiple calls within the delay window
62
+ # are collapsed into a single sync. Default delay is 2 seconds.
63
+ # @param delay [Numeric] Seconds to wait before syncing.
64
+ # @param server_id [Integer, String, nil] Guild ID, or nil for global.
65
+ # @return [Thread, nil] The timer thread, or nil if cancelled.
66
+ def schedule_delayed_command_sync(delay: 2, server_id: nil)
67
+ @sync_mutex.synchronize do
68
+ @pending_sync&.kill if @pending_sync.is_a?(Thread)
69
+ @pending_sync = Thread.new do
70
+ sleep(delay)
71
+ @sync_mutex.synchronize { @pending_sync = nil }
72
+ sync!(server_id: server_id)
73
+ end
74
+ end
75
+ end
76
+
41
77
  private
42
78
 
43
79
  def wire_handler(command)
@@ -7,7 +7,7 @@ module OnyxCord
7
7
 
8
8
  def handle_dispatch(type, data)
9
9
  # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
10
- if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !(@intents || 0).nobits?(INTENTS[:servers])
10
+ if @unavailable_servers&.positive? && @unavailable_timeout_time && (Time.now - @unavailable_timeout_time) > 10 && !(@intents || 0).nobits?(INTENTS[:servers])
11
11
  # The server streaming timed out!
12
12
  LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
13
13
  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.')
@@ -103,7 +103,7 @@ module OnyxCord
103
103
  raise_event(ChannelCreateEvent.new(message.channel, self))
104
104
  end
105
105
 
106
- message.channel.process_last_message_id(message.id)
106
+ message.channel.process_last_message_id(message.id) if message.id
107
107
 
108
108
  event = MessageEvent.new(message, self)
109
109
  raise_event(event)
@@ -535,7 +535,9 @@ module OnyxCord
535
535
  event = RawEvent.new(type, data, self)
536
536
  raise_event(event)
537
537
  end
538
- rescue Exception => e
538
+ rescue Interrupt, SystemExit
539
+ raise
540
+ rescue StandardError => e
539
541
  if defined?(Async::Cancel) && e.is_a?(Async::Cancel)
540
542
  LOGGER.debug('Gateway message handling was cancelled.')
541
543
  return