onyxcord 3.2.8 → 4.0.0.beta.2

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 +6 -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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 519bb339e62e93fa61c6a48e004937ad6e7b7e3645abc6be14709edafb4c05cc
4
- data.tar.gz: efb25e85bdd412727c3f46a69c1b92ebe02a557cb67e878e9040616a25abfa02
3
+ metadata.gz: 4fac344c79bce79ade61b9d7e1d40a756581380e31945ffb93470b5df4ced0c8
4
+ data.tar.gz: c74bbc605deb09a8e19d2a904a4b10e11b27b2a1909403113a2c589ff04de3fc
5
5
  SHA512:
6
- metadata.gz: 4313a469aa21679b5b6522f954a25a16e9ef07071a31b09c61fdd5a23fe9dc3bd9c8965a0bab94ae758450140501d619acb107ebb31ac4e7f98779745afa37c9
7
- data.tar.gz: f1accfb656c81f934c96b46af5521c222bc7995eed2445377ef2acf4c868e1ebbcc6840b814b395b0476f0f27d66643c9687899d61ad06c2bbee89c22a8d117a
6
+ metadata.gz: d28b1b62c8e2b5bba98094e9f43d25feb77afbe9b004f3c642be4efe2cd03e487ce0a42d14e413bc840c62aee9ebced716107ef01f01eea530900acfa66d2508
7
+ data.tar.gz: db0532185853f82710423f20df5b1136f22c8d325782da62642c0855a0398e5585aa15ecc8ab34fd1ad7d7d2a88555c588b7468735b088d73b36c748462c8f56
@@ -3,6 +3,8 @@
3
3
  require 'onyxcord/interactions'
4
4
 
5
5
  module OnyxCord
6
+ # @see OnyxCord::Interactions
7
+ # @deprecated Use {OnyxCord::Interactions} directly.
6
8
  module ApplicationCommands
7
9
  Command = Interactions::Command
8
10
  Option = Interactions::Option
data/lib/onyxcord/bot.rb CHANGED
@@ -9,6 +9,8 @@ require_relative 'core/bot/oauth'
9
9
  require_relative 'core/bot/presence'
10
10
  require_relative 'core/bot/awaits'
11
11
  require_relative 'core/bot/application_commands'
12
+ require_relative 'cogs/bot'
13
+ require_relative 'cogs/extensions'
12
14
 
13
15
  module OnyxCord
14
16
  # Represents a Discord bot, including servers, users, etc.
@@ -59,6 +61,8 @@ module OnyxCord
59
61
  include Presence
60
62
  include Awaits
61
63
  include ApplicationCommands
64
+ include Cogs
65
+ include Extensions
62
66
 
63
67
  # Makes a new bot with the given authentication data. It will be ready to be added event handlers to and can
64
68
  # eventually be run with {#run}.
@@ -117,7 +121,8 @@ module OnyxCord
117
121
  executor_queue_size = config.normalize_event_queue_size(event_queue_size)
118
122
 
119
123
  LOGGER.mode = log_mode
120
- LOGGER.token = token if redact_token
124
+ @redact_token = redact_token
125
+ LOGGER.token = token if @redact_token
121
126
 
122
127
  @should_parse_self = parse_self
123
128
 
@@ -168,6 +173,7 @@ module OnyxCord
168
173
 
169
174
  @application_commands = {}
170
175
  @request_members_rl = {}
176
+ @cogs = {}
171
177
  end
172
178
 
173
179
  # The list of users the bot shares a server with.
@@ -225,7 +231,7 @@ module OnyxCord
225
231
  # @return [GlobalEmoji, nil] the emoji identified by the name, or `nil` if it couldn't be found.
226
232
  def find_emoji(name)
227
233
  LOGGER.out("Resolving emoji #{name}")
228
- emoji.find { |element| element.name == name }
234
+ emoji.values.find { |element| element.name == name }
229
235
  end
230
236
 
231
237
  # The bot's user profile. This special user object can be used
@@ -235,7 +241,7 @@ module OnyxCord
235
241
  return @profile if @profile
236
242
 
237
243
  response = OnyxCord::REST::User.profile(@token)
238
- @profile = Profile.new(JSON.parse(response), self)
244
+ @profile = Profile.new(OnyxCord::Internal::JSON.parse(response), self)
239
245
  end
240
246
 
241
247
  alias_method :bot_user, :profile
@@ -244,7 +250,7 @@ module OnyxCord
244
250
  # @return [Application] The bot's application info.
245
251
  def bot_application
246
252
  response = REST.oauth_application(token)
247
- Application.new(JSON.parse(response), self)
253
+ Application.new(OnyxCord::Internal::JSON.parse(response), self)
248
254
  end
249
255
 
250
256
  alias_method :bot_app, :bot_application
@@ -253,8 +259,8 @@ module OnyxCord
253
259
  # Get the role connection metadata records associated with this application.
254
260
  # @return [Array<RoleConnectionMetadata>] the role connection metadata records associated with this application.
255
261
  def role_connection_metadata_records
256
- response = REST::Application.get_application_role_connection_metadata_records(@bot.token, @id)
257
- JSON.parse(response).map { |role_connection| RoleConnectionMetadata.new(role_connection, @bot) }
262
+ response = REST::Application.get_application_role_connection_metadata_records(token, @client_id)
263
+ OnyxCord::Internal::JSON.parse(response).map { |role_connection| RoleConnectionMetadata.new(role_connection, self) }
258
264
  end
259
265
 
260
266
  # The Discord API token received when logging in. Useful to explicitly call
@@ -26,7 +26,7 @@ module OnyxCord
26
26
 
27
27
  # Initializes this cache
28
28
  def init_cache
29
- @cache_policy ||= OnyxCord.configuration.normalize_cache(:full)
29
+ @cache_policy ||= OnyxCord.configuration.normalize_cache(OnyxCord.configuration.cache)
30
30
  sizes = OnyxCord.configuration.cache_sizes
31
31
 
32
32
  @users = cache_enabled?(:users) ? LruRedux::ThreadSafeCache.new(sizes.users) : nil
@@ -73,7 +73,7 @@ module OnyxCord
73
73
  def fetch_voice_regions
74
74
  regions_by_id = {}
75
75
 
76
- regions = JSON.parse REST.voice_regions(token)
76
+ regions = OnyxCord::Internal::JSON.parse REST.voice_regions(token)
77
77
  regions.each do |data|
78
78
  regions_by_id[data['id']] = VoiceRegion.new(data)
79
79
  end
@@ -99,7 +99,7 @@ module OnyxCord
99
99
  rescue OnyxCord::Errors::UnknownChannel
100
100
  return nil
101
101
  end
102
- channel = Channel.new(JSON.parse(response), self, server)
102
+ channel = Channel.new(OnyxCord::Internal::JSON.parse(response), self, server)
103
103
  @channels[id] = channel if cache_enabled?(:channels)
104
104
  channel
105
105
  end
@@ -120,7 +120,7 @@ module OnyxCord
120
120
  rescue OnyxCord::Errors::UnknownUser
121
121
  return nil
122
122
  end
123
- user = User.new(JSON.parse(response), self)
123
+ user = User.new(OnyxCord::Internal::JSON.parse(response), self)
124
124
  @users[id] = user if cache_enabled?(:users)
125
125
  user
126
126
  end
@@ -139,7 +139,7 @@ module OnyxCord
139
139
  rescue OnyxCord::Errors::NoPermission
140
140
  return nil
141
141
  end
142
- server = Server.new(JSON.parse(response), self)
142
+ server = Server.new(OnyxCord::Internal::JSON.parse(response), self)
143
143
  @servers[id] = server if cache_enabled?(:servers)
144
144
  server
145
145
  end
@@ -161,7 +161,7 @@ module OnyxCord
161
161
  rescue OnyxCord::Errors::UnknownUser, OnyxCord::Errors::UnknownMember
162
162
  return nil
163
163
  end
164
- member = Member.new(JSON.parse(response), server, self)
164
+ member = Member.new(OnyxCord::Internal::JSON.parse(response), server, self)
165
165
  server.cache_member(member)
166
166
  end
167
167
 
@@ -176,7 +176,7 @@ module OnyxCord
176
176
 
177
177
  debug("Creating pm channel with user id #{id}")
178
178
  response = REST::User.create_pm(token, id)
179
- channel = Channel.new(JSON.parse(response), self)
179
+ channel = Channel.new(OnyxCord::Internal::JSON.parse(response), self)
180
180
  @pm_channels[id] = channel if cache_enabled?(:pm_channels)
181
181
  channel
182
182
  end
@@ -190,7 +190,7 @@ module OnyxCord
190
190
  id = id.resolve_id
191
191
  return @server_previews[id] if @server_previews&.[](id)
192
192
 
193
- response = JSON.parse(REST::Server.preview(token, id))
193
+ response = OnyxCord::Internal::JSON.parse(REST::Server.preview(token, id))
194
194
  preview = ServerPreview.new(response, self)
195
195
  @server_previews[id] = preview if cache_enabled?(:server_previews)
196
196
  preview
@@ -301,7 +301,7 @@ module OnyxCord
301
301
  # @return [Invite] The invite with information about the given invite URL.
302
302
  def invite(invite)
303
303
  code = resolve_invite_code(invite)
304
- Invite.new(JSON.parse(REST::Invite.resolve(token, code)), self)
304
+ Invite.new(OnyxCord::Internal::JSON.parse(REST::Invite.resolve(token, code)), self)
305
305
  end
306
306
 
307
307
  # Finds a channel given its name and optionally the name of the server it is in.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ class Bot
5
+ module Cogs
6
+ attr_reader :cogs
7
+
8
+ def add_cog(cog)
9
+ @cogs ||= {}
10
+ raise ArgumentError, "Cog #{cog.qualified_name} is already loaded" if @cogs.key?(cog.qualified_name)
11
+
12
+ cog._inject(self)
13
+ @cogs[cog.qualified_name] = cog
14
+ end
15
+
16
+ def remove_cog(name)
17
+ @cogs ||= {}
18
+ cog = @cogs.delete(name.to_s)
19
+ cog&._eject(self)
20
+ end
21
+
22
+ def get_cog(name)
23
+ @cogs ||= {}
24
+ @cogs[name.to_s]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord; end
4
+
5
+ require 'onyxcord/commands/container'
6
+ require 'onyxcord/cogs/cog_meta'
7
+
8
+ module OnyxCord
9
+ class Cog
10
+ include CogMeta
11
+
12
+ attr_reader :bot, :commands, :application_commands
13
+
14
+ def self.inherited(child)
15
+ child.include CogMeta
16
+ super
17
+ end
18
+
19
+ def initialize(bot)
20
+ @bot = bot
21
+ _update_copy
22
+ end
23
+
24
+ def cog_load; end
25
+
26
+ def cog_unload; end
27
+
28
+ def cog_command_error(_event, _error); end
29
+
30
+ def cog_slash_command_error(_context, _error); end
31
+
32
+ def cog_user_command_error(_context, _error); end
33
+
34
+ def cog_message_command_error(_context, _error); end
35
+
36
+ def cog_check(_event)
37
+ true
38
+ end
39
+
40
+ def cog_slash_command_check(_context)
41
+ true
42
+ end
43
+
44
+ def cog_user_command_check(_context)
45
+ true
46
+ end
47
+
48
+ def cog_message_command_check(_context)
49
+ true
50
+ end
51
+
52
+ def cog_before_invoke(_event); end
53
+
54
+ def cog_after_invoke(_event, _result); end
55
+
56
+ def cog_slash_before_invoke(_context); end
57
+
58
+ def cog_slash_after_invoke(_context, _result); end
59
+
60
+ def cog_user_before_invoke(_context); end
61
+
62
+ def cog_user_after_invoke(_context, _result); end
63
+
64
+ def cog_message_before_invoke(_context); end
65
+
66
+ def cog_message_after_invoke(_context, _result); end
67
+
68
+ def _inject(bot)
69
+ @bot = bot
70
+ old_commands = (bot.instance_variable_get(:@commands) || {}).dup
71
+ old_application_commands = (bot.application_command_registry.commands || {}).dup
72
+ old_application_handlers = (bot.instance_variable_get(:@application_commands) || {}).dup
73
+ old_event_handlers = clone_event_handlers(bot.instance_variable_get(:@event_handlers))
74
+ old_raw_handlers = (bot.instance_variable_get(:@raw_handlers) || []).dup
75
+
76
+ inject_commands(bot)
77
+ inject_listeners(bot)
78
+ cog_load
79
+ self
80
+ rescue StandardError
81
+ bot.instance_variable_set(:@commands, old_commands)
82
+ bot.application_command_registry.commands.replace(old_application_commands)
83
+ bot.instance_variable_set(:@application_commands, old_application_handlers)
84
+ bot.instance_variable_set(:@event_handlers, old_event_handlers)
85
+ bot.instance_variable_set(:@raw_handlers, old_raw_handlers)
86
+ raise
87
+ end
88
+
89
+ def _eject(bot)
90
+ eject_commands(bot)
91
+ eject_listeners(bot)
92
+ cog_unload
93
+ self
94
+ end
95
+
96
+ def _update_copy
97
+ @commands = copy_prefix_commands
98
+ @application_commands = self.class.application_commands.transform_values do |command|
99
+ copy = command.copy_for(self)
100
+ copy.parse(&copy.block) if copy.block && copy.options.empty?
101
+ copy
102
+ end
103
+ @listeners = self.class.listeners.map { |listener| listener.merge(names: listener[:names].dup) }
104
+ end
105
+
106
+ # rubocop:disable Naming/AccessorMethodName
107
+ def get_commands
108
+ @commands.values.grep(Commands::Command)
109
+ end
110
+
111
+ def get_application_commands
112
+ @application_commands.values
113
+ end
114
+
115
+ def get_listeners
116
+ @listeners
117
+ end
118
+ # rubocop:enable Naming/AccessorMethodName
119
+
120
+ def walk_commands
121
+ get_commands + get_application_commands.flat_map { |command| [command, *walk_options(command.options)] }
122
+ end
123
+
124
+ def handled_cog_command_error?(event, error)
125
+ return false unless overrides?(:cog_command_error)
126
+
127
+ cog_command_error(event, error)
128
+ true
129
+ end
130
+
131
+ def handled_cog_application_command_error?(context, error, command)
132
+ method_name = case command.type
133
+ when :chat_input, :primary_entry_point then :cog_slash_command_error
134
+ when :user then :cog_user_command_error
135
+ when :message then :cog_message_command_error
136
+ end
137
+ return false unless method_name && overrides?(method_name)
138
+
139
+ public_send(method_name, context, error)
140
+ true
141
+ end
142
+
143
+ def passed_cog_command_check?(event)
144
+ return true unless overrides?(:cog_check)
145
+
146
+ cog_check(event)
147
+ end
148
+
149
+ def passed_cog_application_command_check?(context, command)
150
+ method_name = case command.type
151
+ when :chat_input, :primary_entry_point then :cog_slash_command_check
152
+ when :user then :cog_user_command_check
153
+ when :message then :cog_message_command_check
154
+ end
155
+ return true unless method_name && overrides?(method_name)
156
+
157
+ public_send(method_name, context)
158
+ end
159
+
160
+ def invoke_cog_before_invoke(event)
161
+ return unless overrides?(:cog_before_invoke)
162
+
163
+ cog_before_invoke(event)
164
+ end
165
+
166
+ def invoke_cog_after_invoke(event, result)
167
+ return unless overrides?(:cog_after_invoke)
168
+
169
+ cog_after_invoke(event, result)
170
+ end
171
+
172
+ def invoke_cog_application_before_invoke(context, command)
173
+ method_name = case command.type
174
+ when :chat_input, :primary_entry_point then :cog_slash_before_invoke
175
+ when :user then :cog_user_before_invoke
176
+ when :message then :cog_message_before_invoke
177
+ end
178
+ return unless method_name && overrides?(method_name)
179
+
180
+ public_send(method_name, context)
181
+ end
182
+
183
+ def invoke_cog_application_after_invoke(context, result, command)
184
+ method_name = case command.type
185
+ when :chat_input, :primary_entry_point then :cog_slash_after_invoke
186
+ when :user then :cog_user_after_invoke
187
+ when :message then :cog_message_after_invoke
188
+ end
189
+ return unless method_name && overrides?(method_name)
190
+
191
+ public_send(method_name, context, result)
192
+ end
193
+
194
+ private
195
+
196
+ def overrides?(method_name)
197
+ method(method_name).owner != OnyxCord::Cog
198
+ end
199
+
200
+ def copy_prefix_commands
201
+ copied = {}
202
+
203
+ (self.class.commands || {}).each_value do |command|
204
+ next unless command.is_a?(Commands::Command)
205
+
206
+ copy = command.copy_for(self)
207
+ copied[copy.name] = copy
208
+ copy.attributes[:aliases].each { |name| copied[name] = Commands::CommandAlias.new(name, copy) }
209
+ end
210
+
211
+ copied
212
+ end
213
+
214
+ def walk_options(options)
215
+ options.flat_map { |option| [option, *walk_options(option.options)] }
216
+ end
217
+
218
+ def inject_commands(bot)
219
+ @__onyxcord_injected_commands = @commands.keys
220
+ bot_commands = bot.instance_variable_get(:@commands) || {}
221
+ bot.instance_variable_set(:@commands, bot_commands.merge(@commands))
222
+
223
+ @__onyxcord_injected_application_commands = @application_commands.keys
224
+ @application_commands.each_value { |command| bot.application_command_registry.register(command) }
225
+ end
226
+
227
+ def inject_listeners(bot)
228
+ @__onyxcord_injected_listeners = []
229
+ @listeners.each do |listener|
230
+ listener[:names].each do |name|
231
+ raise ArgumentError, "Unknown listener event #{name}" unless bot.respond_to?(name)
232
+
233
+ handler = bot.public_send(name, listener[:attributes]) { |event| instance_exec(event, &listener[:block]) }
234
+ @__onyxcord_injected_listeners << handler
235
+ end
236
+ end
237
+ end
238
+
239
+ def eject_commands(bot)
240
+ bot_commands = bot.instance_variable_get(:@commands)
241
+ @__onyxcord_injected_commands&.each do |name|
242
+ bot_commands&.delete(name) if bot_commands[name].equal?(@commands[name])
243
+ end
244
+
245
+ registry_commands = bot.application_command_registry.commands
246
+ application_handlers = bot.instance_variable_get(:@application_commands)
247
+ @__onyxcord_injected_application_commands&.each do |name|
248
+ registry_commands.delete(name) if registry_commands[name].equal?(@application_commands[name])
249
+ application_handlers&.delete(name.to_sym)
250
+ end
251
+ end
252
+
253
+ def eject_listeners(bot)
254
+ @__onyxcord_injected_listeners&.each { |handler| bot.remove_handler(handler) }
255
+ @__onyxcord_injected_listeners = []
256
+ end
257
+
258
+ def clone_event_handlers(handlers)
259
+ return {} unless handlers
260
+
261
+ handlers.transform_values(&:dup)
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module CogMeta
5
+ def self.included(base)
6
+ base.extend Commands::CommandContainer
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ def qualified_name
11
+ self.class.name || self.class.to_s
12
+ end
13
+
14
+ def description
15
+ self.class.instance_variable_get(:@description)
16
+ end
17
+
18
+ module ClassMethods
19
+ def description(value = nil)
20
+ @description = value unless value.nil?
21
+ @description
22
+ end
23
+
24
+ def command_attrs(attributes = nil)
25
+ @command_attrs = attributes.dup if attributes
26
+ @command_attrs ||= {}
27
+ end
28
+
29
+ def command(name, attributes = {}, &block)
30
+ super(name, command_attrs.merge(attributes), &block)
31
+ end
32
+
33
+ def slash(name, description:, **attributes, &block)
34
+ application_commands[name.to_s] = Interactions::Command.chat_input(name, description: description, **attributes, &block)
35
+ end
36
+
37
+ def user_command(name, **attributes, &block)
38
+ application_commands[name.to_s] = Interactions::Command.user(name, **attributes, &block)
39
+ end
40
+
41
+ def message_command(name, **attributes, &block)
42
+ application_commands[name.to_s] = Interactions::Command.message(name, **attributes, &block)
43
+ end
44
+
45
+ def application_commands
46
+ @application_commands ||= {}
47
+ end
48
+
49
+ def listener(*names, **attributes, &block)
50
+ raise ArgumentError, 'Listener requires at least one event name' if names.empty?
51
+ raise ArgumentError, 'Listener requires a block' unless block
52
+
53
+ listeners << { names: names.map(&:to_sym), attributes: attributes, block: block }
54
+ end
55
+
56
+ def listeners
57
+ @listeners ||= []
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ class Bot
5
+ # Extension management: load, unload, and reload cog modules from files.
6
+ module Extensions
7
+ # @return [Hash{String => Hash}] loaded extensions keyed by name
8
+ attr_reader :extensions
9
+
10
+ # Load an extension from a file. The file must define a +setup(bot)+ method
11
+ # that receives the bot and returns a cog instance (or an array of cog instances).
12
+ # Optionally, the file can define a +teardown(bot)+ method for cleanup.
13
+ #
14
+ # @param name [String, Symbol] The extension name (used as key and to resolve file path).
15
+ # @param path [String, nil] Optional explicit file path. When nil, resolves +extensions/<name>.rb+.
16
+ # @return [Array<Cog>] the cogs created by the extension.
17
+ # @raise [ArgumentError] if the extension is already loaded.
18
+ # @raise [LoadError] if the file cannot be found or does not define +setup+.
19
+ def load_extension(name, path: nil)
20
+ @extensions ||= {}
21
+ name = name.to_s
22
+ raise ArgumentError, "Extension '#{name}' is already loaded" if @extensions.key?(name)
23
+
24
+ path ||= resolve_extension_path(name)
25
+ raise LoadError, "Extension file not found: #{path}" unless File.exist?(path)
26
+
27
+ mod = load_extension_module(path, name)
28
+
29
+ raise LoadError, "Extension '#{name}' must define a self.setup(bot) method" unless mod.respond_to?(:setup)
30
+
31
+ result = mod.setup(self)
32
+ cogs = Array(result).select { |obj| obj.is_a?(Cog) }
33
+
34
+ @extensions[name] = {
35
+ path: path,
36
+ module: mod,
37
+ cogs: cogs.map { |c| c.respond_to?(:qualified_name) ? c.qualified_name : c.class.name }
38
+ }
39
+
40
+ cogs
41
+ end
42
+
43
+ # Unload an extension by name. Calls +teardown(bot)+ if defined.
44
+ #
45
+ # @param name [String, Symbol] The extension name.
46
+ # @return [true, false] whether the extension was found and unloaded.
47
+ def unload_extension(name) # rubocop:disable Naming/PredicateMethod
48
+ @extensions ||= {}
49
+ name = name.to_s
50
+ ext = @extensions.delete(name)
51
+ return false unless ext
52
+
53
+ mod = ext[:module]
54
+ mod.teardown(self) if mod.respond_to?(:teardown)
55
+
56
+ ext[:cogs].each { |cog_name| remove_cog(cog_name) }
57
+
58
+ true
59
+ end
60
+
61
+ # Reload an extension: unload then load.
62
+ #
63
+ # @param name [String, Symbol] The extension name.
64
+ # @return [Array<Cog>] the cogs created by the reloaded extension.
65
+ # @raise [ArgumentError] if the extension was not previously loaded.
66
+ def reload_extension(name)
67
+ @extensions ||= {}
68
+ name = name.to_s
69
+ ext = @extensions[name]
70
+ raise ArgumentError, "Extension '#{name}' is not loaded" unless ext
71
+
72
+ path = ext[:path]
73
+ unload_extension(name)
74
+ load_extension(name, path: path)
75
+ end
76
+
77
+ # Load all +.rb+ files from a directory as extensions.
78
+ #
79
+ # @param dir [String] The directory path containing extension files.
80
+ # @return [Array<Cog>] all cogs created from the loaded extensions.
81
+ def load_extensions(dir)
82
+ raise ArgumentError, "Extensions directory not found: #{dir}" unless Dir.exist?(dir)
83
+
84
+ cogs = []
85
+ Dir.glob(File.join(dir, '*.rb')).each do |file|
86
+ name = File.basename(file, '.rb')
87
+ cogs.concat(load_extension(name, path: file))
88
+ rescue LoadError, StandardError => e
89
+ OnyxCord::LOGGER.warn("Failed to load extension '#{name}': #{e.message}") if defined?(OnyxCord::LOGGER)
90
+ end
91
+ cogs
92
+ end
93
+
94
+ private
95
+
96
+ def resolve_extension_path(name)
97
+ File.join('extensions', "#{name}.rb")
98
+ end
99
+
100
+ def load_extension_module(path, name)
101
+ # Create a fresh module for the extension to avoid constant collisions
102
+ mod = Module.new
103
+ mod.define_singleton_method(:name) { "OnyxCord::Extensions::#{name.capitalize}" }
104
+ mod.module_eval(File.read(path), path)
105
+ mod
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OnyxCord::Commands::Bot
4
+ # Channel restriction logic for command bots.
4
5
  module Channels
5
6
  # @see Commands::Bot#update_channels
6
7
  def channels=(channels)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OnyxCord::Commands::Bot
4
+ # Command execution and dispatch logic for the command bot.
4
5
  module Execution
5
6
  # Returns all aliases for the command with the given name
6
7
  # @param name [Symbol] the name of the `Command`