discord_rda 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +398 -0
- data/lib/discord_rda/bot.rb +842 -0
- data/lib/discord_rda/cache/configurable_cache.rb +283 -0
- data/lib/discord_rda/cache/entity_cache.rb +184 -0
- data/lib/discord_rda/cache/memory_store.rb +143 -0
- data/lib/discord_rda/cache/redis_store.rb +136 -0
- data/lib/discord_rda/cache/store.rb +56 -0
- data/lib/discord_rda/connection/gateway_client.rb +383 -0
- data/lib/discord_rda/connection/invalid_bucket.rb +205 -0
- data/lib/discord_rda/connection/rate_limiter.rb +280 -0
- data/lib/discord_rda/connection/request_queue.rb +340 -0
- data/lib/discord_rda/connection/reshard_manager.rb +328 -0
- data/lib/discord_rda/connection/rest_client.rb +316 -0
- data/lib/discord_rda/connection/rest_proxy.rb +165 -0
- data/lib/discord_rda/connection/scalable_rest_client.rb +526 -0
- data/lib/discord_rda/connection/shard_manager.rb +223 -0
- data/lib/discord_rda/core/async_runtime.rb +108 -0
- data/lib/discord_rda/core/configuration.rb +194 -0
- data/lib/discord_rda/core/logger.rb +188 -0
- data/lib/discord_rda/core/snowflake.rb +121 -0
- data/lib/discord_rda/entity/attachment.rb +88 -0
- data/lib/discord_rda/entity/base.rb +103 -0
- data/lib/discord_rda/entity/channel.rb +446 -0
- data/lib/discord_rda/entity/channel_builder.rb +280 -0
- data/lib/discord_rda/entity/color.rb +253 -0
- data/lib/discord_rda/entity/embed.rb +221 -0
- data/lib/discord_rda/entity/emoji.rb +89 -0
- data/lib/discord_rda/entity/factory.rb +99 -0
- data/lib/discord_rda/entity/guild.rb +619 -0
- data/lib/discord_rda/entity/member.rb +263 -0
- data/lib/discord_rda/entity/message.rb +405 -0
- data/lib/discord_rda/entity/message_builder.rb +369 -0
- data/lib/discord_rda/entity/role.rb +157 -0
- data/lib/discord_rda/entity/support.rb +294 -0
- data/lib/discord_rda/entity/user.rb +231 -0
- data/lib/discord_rda/entity/value_objects.rb +263 -0
- data/lib/discord_rda/event/auto_moderation.rb +294 -0
- data/lib/discord_rda/event/base.rb +986 -0
- data/lib/discord_rda/event/bus.rb +225 -0
- data/lib/discord_rda/event/scheduled_event.rb +257 -0
- data/lib/discord_rda/hot_reload_manager.rb +303 -0
- data/lib/discord_rda/interactions/application_command.rb +436 -0
- data/lib/discord_rda/interactions/command_system.rb +484 -0
- data/lib/discord_rda/interactions/components.rb +464 -0
- data/lib/discord_rda/interactions/interaction.rb +553 -0
- data/lib/discord_rda/plugin/analytics_plugin.rb +528 -0
- data/lib/discord_rda/plugin/base.rb +190 -0
- data/lib/discord_rda/plugin/registry.rb +126 -0
- data/lib/discord_rda/version.rb +5 -0
- data/lib/discord_rda.rb +70 -0
- metadata +302 -0
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiscordRDA
|
|
4
|
+
# Main Bot class for DiscordRDA.
|
|
5
|
+
# Entry point for building Discord bots.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic bot
|
|
8
|
+
# bot = DiscordRDA::Bot.new(token: ENV['DISCORD_TOKEN'])
|
|
9
|
+
# bot.on(:message_create) { |e| puts e.content }
|
|
10
|
+
# bot.run
|
|
11
|
+
#
|
|
12
|
+
class Bot
|
|
13
|
+
# @return [Configuration] Bot configuration
|
|
14
|
+
attr_reader :config
|
|
15
|
+
|
|
16
|
+
# @return [Logger] Logger instance
|
|
17
|
+
attr_reader :logger
|
|
18
|
+
|
|
19
|
+
# @return [EventBus] Event bus
|
|
20
|
+
attr_reader :event_bus
|
|
21
|
+
|
|
22
|
+
# @return [EntityCache] Entity cache
|
|
23
|
+
attr_reader :cache
|
|
24
|
+
|
|
25
|
+
# @return [ShardManager] Shard manager
|
|
26
|
+
attr_reader :shard_manager
|
|
27
|
+
|
|
28
|
+
# @return [RestClient] REST client
|
|
29
|
+
attr_reader :rest
|
|
30
|
+
|
|
31
|
+
# @return [ScalableRestClient] Scalable REST client (if enabled)
|
|
32
|
+
attr_reader :scalable_rest
|
|
33
|
+
|
|
34
|
+
# @return [ReshardManager] Reshard manager
|
|
35
|
+
attr_reader :reshard_manager
|
|
36
|
+
|
|
37
|
+
# @return [HotReloadManager] Hot reload manager
|
|
38
|
+
attr_reader :hot_reload_manager
|
|
39
|
+
|
|
40
|
+
# @return [PluginRegistry] Plugin registry
|
|
41
|
+
attr_reader :plugins
|
|
42
|
+
|
|
43
|
+
# @return [Boolean] Whether bot is running
|
|
44
|
+
attr_reader :running
|
|
45
|
+
|
|
46
|
+
# @return [Hash] Registered slash commands
|
|
47
|
+
attr_reader :slash_commands
|
|
48
|
+
|
|
49
|
+
# Initialize a new bot
|
|
50
|
+
# @param token [String] Bot token
|
|
51
|
+
# @param options [Hash] Configuration options
|
|
52
|
+
def initialize(token:, **options)
|
|
53
|
+
@config = Configuration.new(options.merge(token: token))
|
|
54
|
+
@logger = Logger.new(level: @config.log_level, format: @config.log_format)
|
|
55
|
+
@event_bus = EventBus.new(logger: @logger)
|
|
56
|
+
@cache = build_cache
|
|
57
|
+
@shard_manager = ShardManager.new(@config, @event_bus, @logger)
|
|
58
|
+
@rest = RestClient.new(@config, @logger)
|
|
59
|
+
|
|
60
|
+
# Configure entity API clients
|
|
61
|
+
Message.api = @rest
|
|
62
|
+
Interaction.api = @rest
|
|
63
|
+
|
|
64
|
+
setup_event_handlers
|
|
65
|
+
setup_interaction_handlers
|
|
66
|
+
|
|
67
|
+
# Initialize scalable components
|
|
68
|
+
@scalable_rest = nil
|
|
69
|
+
@reshard_manager = ReshardManager.new(self, @shard_manager, @logger)
|
|
70
|
+
@hot_reload_manager = HotReloadManager.new(self, @logger)
|
|
71
|
+
@plugins = PluginRegistry.new(logger: @logger)
|
|
72
|
+
@slash_commands = {}
|
|
73
|
+
@running = false
|
|
74
|
+
@commands = {}
|
|
75
|
+
|
|
76
|
+
setup_event_handlers
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Register a slash command (global or guild-specific)
|
|
80
|
+
# @param name [String] Command name
|
|
81
|
+
# @param description [String] Command description
|
|
82
|
+
# @param options [Hash] Command options
|
|
83
|
+
# @option options [String] :guild_id Guild-specific command (nil for global)
|
|
84
|
+
# @option options [Array<Hash>] :options Command options
|
|
85
|
+
# @option options [Integer] :default_member_permissions Default required permissions
|
|
86
|
+
# @option options [Boolean] :dm_permission Whether works in DMs
|
|
87
|
+
# @yield [CommandBuilder] DSL block for building command
|
|
88
|
+
# @return [ApplicationCommand] Registered command
|
|
89
|
+
def slash(name, description, **options, &block)
|
|
90
|
+
builder = CommandBuilder.new(name, description)
|
|
91
|
+
builder.dm_allowed(options[:dm_permission]) if options.key?(:dm_permission)
|
|
92
|
+
builder.default_permissions(options[:default_member_permissions]) if options[:default_member_permissions]
|
|
93
|
+
builder.nsfw(options[:nsfw]) if options[:nsfw]
|
|
94
|
+
|
|
95
|
+
block.call(builder) if block
|
|
96
|
+
|
|
97
|
+
cmd = builder.build
|
|
98
|
+
cmd.instance_variable_set(:@application_id, me.id.to_s) rescue nil
|
|
99
|
+
cmd.instance_variable_set(:@guild_id, options[:guild_id].to_s) if options[:guild_id]
|
|
100
|
+
|
|
101
|
+
key = options[:guild_id] ? "#{name}:#{options[:guild_id]}" : name
|
|
102
|
+
@slash_commands[key] = cmd
|
|
103
|
+
|
|
104
|
+
# Register with Discord if we have application ID
|
|
105
|
+
if cmd.application_id
|
|
106
|
+
if options[:guild_id]
|
|
107
|
+
cmd.create_guild(self, options[:guild_id])
|
|
108
|
+
else
|
|
109
|
+
cmd.create_global(self)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
@logger.info('Registered slash command', name: name, guild: options[:guild_id] || 'global')
|
|
114
|
+
cmd
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Register a context menu command (user or message)
|
|
118
|
+
# @param type [Symbol] :user or :message
|
|
119
|
+
# @param name [String] Command name
|
|
120
|
+
# @param options [Hash] Command options
|
|
121
|
+
# @yield [Interaction] Handler block
|
|
122
|
+
# @return [ApplicationCommand] Registered command
|
|
123
|
+
def context_menu(type:, name:, **options, &block)
|
|
124
|
+
cmd_type = type == :user ? 2 : 3
|
|
125
|
+
options[:type] = cmd_type
|
|
126
|
+
options[:description] = '' # Context menus don't have descriptions
|
|
127
|
+
|
|
128
|
+
slash(name, '', **options, &block)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Bulk register global commands (replaces existing)
|
|
132
|
+
# @param commands [Array<CommandBuilder>] Commands to register
|
|
133
|
+
# @return [Array<ApplicationCommand>] Registered commands
|
|
134
|
+
def bulk_register_commands(commands)
|
|
135
|
+
return [] unless me
|
|
136
|
+
|
|
137
|
+
app_id = me.id.to_s
|
|
138
|
+
payload = commands.map(&:to_h)
|
|
139
|
+
|
|
140
|
+
data = @rest.put("/applications/#{app_id}/commands", body: payload)
|
|
141
|
+
data.map { |cmd| ApplicationCommand.new(cmd) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Delete a global command
|
|
145
|
+
# @param command_id [String] Command ID
|
|
146
|
+
# @return [void]
|
|
147
|
+
def delete_global_command(command_id)
|
|
148
|
+
@rest.delete("/applications/#{me.id}/commands/#{command_id}") if me
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Delete a guild command
|
|
152
|
+
# @param guild_id [String] Guild ID
|
|
153
|
+
# @param command_id [String] Command ID
|
|
154
|
+
# @return [void]
|
|
155
|
+
def delete_guild_command(guild_id, command_id)
|
|
156
|
+
@rest.delete("/applications/#{me.id}/guilds/#{guild_id}/commands/#{command_id}") if me
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Register an event handler
|
|
160
|
+
# @param event [String, Symbol] Event type
|
|
161
|
+
# @yield Event handler block
|
|
162
|
+
# @return [Subscription] Subscription object
|
|
163
|
+
def on(event, &block)
|
|
164
|
+
@event_bus.on(event, &block)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Register a one-time event handler
|
|
168
|
+
# @param event [String, Symbol] Event type
|
|
169
|
+
# @yield Event handler block
|
|
170
|
+
# @return [Subscription] Subscription object
|
|
171
|
+
def once(event, &block)
|
|
172
|
+
@event_bus.once(event, &block)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Wait for an event
|
|
176
|
+
# @param event [String, Symbol] Event type
|
|
177
|
+
# @param timeout [Float] Timeout in seconds
|
|
178
|
+
# @yield Block to match event
|
|
179
|
+
# @return [Event, nil] Event or nil if timeout
|
|
180
|
+
def wait_for(event, timeout: nil, &block)
|
|
181
|
+
@event_bus.wait_for(event, timeout: timeout, &block)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Register a command
|
|
185
|
+
# @param name [String] Command name
|
|
186
|
+
# @param description [String] Command description
|
|
187
|
+
# @param options [Array<Hash>] Command options
|
|
188
|
+
# @yield Command handler
|
|
189
|
+
# @return [void]
|
|
190
|
+
def register_command(name, description = '', options = [], &block)
|
|
191
|
+
@commands[name.to_s] = {
|
|
192
|
+
description: description,
|
|
193
|
+
options: options,
|
|
194
|
+
handler: block
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
alias command register_command
|
|
198
|
+
|
|
199
|
+
# Register a plugin
|
|
200
|
+
# @param plugin [Plugin] Plugin to register
|
|
201
|
+
# @return [Boolean] True if registered
|
|
202
|
+
def register_plugin(plugin)
|
|
203
|
+
@plugins.register(plugin, self)
|
|
204
|
+
end
|
|
205
|
+
alias plugin register_plugin
|
|
206
|
+
|
|
207
|
+
# Use middleware
|
|
208
|
+
# @param middleware [Middleware] Middleware to use
|
|
209
|
+
# @return [void]
|
|
210
|
+
def use(middleware)
|
|
211
|
+
@event_bus.use(middleware)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Run the bot
|
|
215
|
+
# @param async [Boolean] Run asynchronously
|
|
216
|
+
# @return [void]
|
|
217
|
+
def run(async: false)
|
|
218
|
+
@running = true
|
|
219
|
+
|
|
220
|
+
@logger.info('Starting DiscordRDA bot', version: VERSION, shards: @config.shards.length)
|
|
221
|
+
|
|
222
|
+
# Start REST client
|
|
223
|
+
@rest.start
|
|
224
|
+
|
|
225
|
+
# Calculate shard count if auto
|
|
226
|
+
shard_count = if @config.shards == [:auto]
|
|
227
|
+
@shard_manager.calculate_shard_count(:auto, @rest)
|
|
228
|
+
else
|
|
229
|
+
@config.shards.length
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
@shard_manager.instance_variable_set(:@shard_count, shard_count)
|
|
233
|
+
|
|
234
|
+
# Start shards
|
|
235
|
+
if async
|
|
236
|
+
Async { start_shards }
|
|
237
|
+
else
|
|
238
|
+
start_shards
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Stop the bot
|
|
243
|
+
# @return [void]
|
|
244
|
+
def stop
|
|
245
|
+
@logger.info('Stopping bot')
|
|
246
|
+
@running = false
|
|
247
|
+
@shard_manager.stop
|
|
248
|
+
@rest.stop
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Update bot presence
|
|
252
|
+
# @param status [String] online, idle, dnd, invisible
|
|
253
|
+
# @param activity [Hash] Activity data
|
|
254
|
+
# @return [void]
|
|
255
|
+
def update_presence(status: 'online', activity: nil)
|
|
256
|
+
@shard_manager.shards.each do |shard|
|
|
257
|
+
shard.update_presence(status: status, activity: activity)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Get bot status
|
|
262
|
+
# @return [Hash] Status information
|
|
263
|
+
def status
|
|
264
|
+
{
|
|
265
|
+
running: @running,
|
|
266
|
+
shards: @shard_manager.status,
|
|
267
|
+
cache: @cache.stats,
|
|
268
|
+
plugins: @plugins.stats
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Fetch current user
|
|
273
|
+
# @return [User] Bot user
|
|
274
|
+
def me
|
|
275
|
+
data = @rest.get('/users/@me')
|
|
276
|
+
User.new(data)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Get a guild by ID
|
|
280
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
281
|
+
# @return [Guild, nil] Guild or nil
|
|
282
|
+
def guild(guild_id)
|
|
283
|
+
cached = @cache.guild(guild_id)
|
|
284
|
+
return cached if cached
|
|
285
|
+
|
|
286
|
+
data = @rest.get("/guilds/#{guild_id}")
|
|
287
|
+
guild = Guild.new(data)
|
|
288
|
+
@cache.cache_guild(guild)
|
|
289
|
+
guild
|
|
290
|
+
rescue RestClient::NotFoundError
|
|
291
|
+
nil
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Get a channel by ID
|
|
295
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
296
|
+
# @return [Channel, nil] Channel or nil
|
|
297
|
+
def channel(channel_id)
|
|
298
|
+
cached = @cache.channel(channel_id)
|
|
299
|
+
return cached if cached
|
|
300
|
+
|
|
301
|
+
data = @rest.get("/channels/#{channel_id}")
|
|
302
|
+
channel = Channel.new(data)
|
|
303
|
+
@cache.cache_channel(channel)
|
|
304
|
+
channel
|
|
305
|
+
rescue RestClient::NotFoundError
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Send a message to a channel
|
|
310
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
311
|
+
# @param content [String] Message content
|
|
312
|
+
# @param options [Hash] Message options
|
|
313
|
+
# @return [Message] Sent message
|
|
314
|
+
def send_message(channel_id, content = nil, **options)
|
|
315
|
+
payload = { content: content }.merge(options).compact
|
|
316
|
+
data = @rest.post("/channels/#{channel_id}/messages", body: payload)
|
|
317
|
+
Message.new(data)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Get messages from a channel with pagination (simplified)
|
|
321
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
322
|
+
# @param limit [Integer] Max messages to fetch (1-100, default 50)
|
|
323
|
+
# @param before [String, Snowflake] Get messages before this ID
|
|
324
|
+
# @param after [String, Snowflake] Get messages after this ID
|
|
325
|
+
# @param around [String, Snowflake] Get messages around this ID
|
|
326
|
+
# @return [Array<Message>] Messages
|
|
327
|
+
def channel_messages(channel_id, limit: 50, before: nil, after: nil, around: nil)
|
|
328
|
+
params = { limit: limit }
|
|
329
|
+
params[:before] = before.to_s if before
|
|
330
|
+
params[:after] = after.to_s if after
|
|
331
|
+
params[:around] = around.to_s if around
|
|
332
|
+
|
|
333
|
+
data = @rest.get("/channels/#{channel_id}/messages", params: params)
|
|
334
|
+
data.map { |msg| Message.new(msg) }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Get a single message from a channel
|
|
338
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
339
|
+
# @param message_id [String, Snowflake] Message ID
|
|
340
|
+
# @return [Message, nil] Message or nil
|
|
341
|
+
def channel_message(channel_id, message_id)
|
|
342
|
+
data = @rest.get("/channels/#{channel_id}/messages/#{message_id}")
|
|
343
|
+
Message.new(data)
|
|
344
|
+
rescue RestClient::NotFoundError
|
|
345
|
+
nil
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Enable scalable REST client (queue-based rate limiting)
|
|
349
|
+
# @param proxy [Hash] Optional proxy configuration
|
|
350
|
+
# @return [void]
|
|
351
|
+
def enable_scalable_rest(proxy: nil)
|
|
352
|
+
@logger.info('Enabling scalable REST client')
|
|
353
|
+
@scalable_rest = ScalableRestClient.new(@config, @logger, proxy: proxy)
|
|
354
|
+
@scalable_rest.start
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Enable hot reload for development
|
|
358
|
+
# @param watch_dir [String] Directory to watch
|
|
359
|
+
# @return [void]
|
|
360
|
+
def enable_hot_reload(watch_dir: 'lib')
|
|
361
|
+
@logger.info('Enabling hot reload', watch_dir: watch_dir)
|
|
362
|
+
@hot_reload_manager = HotReloadManager.new(self, @logger, watch_dir: watch_dir)
|
|
363
|
+
@hot_reload_manager.enable
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Trigger zero-downtime resharding
|
|
367
|
+
# @param new_shard_count [Integer] New shard count
|
|
368
|
+
# @return [void]
|
|
369
|
+
def reshard_to(new_shard_count)
|
|
370
|
+
@logger.info('Triggering resharding', new_count: new_shard_count)
|
|
371
|
+
@reshard_manager.reshard_to(new_shard_count)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Enable auto-resharding based on guild count
|
|
375
|
+
# @param max_guilds_per_shard [Integer] Max guilds per shard
|
|
376
|
+
# @return [void]
|
|
377
|
+
def enable_auto_reshard(max_guilds_per_shard: 1000)
|
|
378
|
+
@event_bus.on(:guild_create) do |_event|
|
|
379
|
+
guild_count = @shard_manager.total_guilds || 0
|
|
380
|
+
@reshard_manager.auto_reshard_if_needed(guild_count, max_guilds_per_shard: max_guilds_per_shard)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Get invalid request bucket status
|
|
385
|
+
# @return [Hash, nil] Invalid bucket status
|
|
386
|
+
def invalid_bucket_status
|
|
387
|
+
@scalable_rest&.invalid_bucket&.status
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Get analytics data (if analytics plugin registered)
|
|
391
|
+
# @return [Hash] Analytics data
|
|
392
|
+
def analytics
|
|
393
|
+
analytics_plugin = @plugins.get(:Analytics)
|
|
394
|
+
analytics_plugin&.summary || {}
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# === Message Reactions (Simplified) ===
|
|
398
|
+
|
|
399
|
+
# Add a reaction to a message
|
|
400
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
401
|
+
# @param message_id [String, Snowflake] Message ID
|
|
402
|
+
# @param emoji [String, Emoji] Emoji (unicode or name:id format)
|
|
403
|
+
# @return [void]
|
|
404
|
+
def add_reaction(channel_id, message_id, emoji)
|
|
405
|
+
emoji_str = emoji.respond_to?(:id) ? "#{emoji.name}:#{emoji.id}" : emoji.to_s
|
|
406
|
+
@rest.put("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}/@me")
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Remove a reaction from a message
|
|
410
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
411
|
+
# @param message_id [String, Snowflake] Message ID
|
|
412
|
+
# @param emoji [String, Emoji] Emoji
|
|
413
|
+
# @param user_id [String, Snowflake] User ID (default: @me)
|
|
414
|
+
# @return [void]
|
|
415
|
+
def remove_reaction(channel_id, message_id, emoji, user_id: '@me')
|
|
416
|
+
emoji_str = emoji.respond_to?(:id) ? "#{emoji.name}:#{emoji.id}" : emoji.to_s
|
|
417
|
+
@rest.delete("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}/#{user_id}")
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Get reactions for a message (simplified - no pagination)
|
|
421
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
422
|
+
# @param message_id [String, Snowflake] Message ID
|
|
423
|
+
# @param emoji [String, Emoji] Emoji filter
|
|
424
|
+
# @param limit [Integer] Max users to return (1-100, default 25)
|
|
425
|
+
# @return [Array<User>] Users who reacted
|
|
426
|
+
def get_reactions(channel_id, message_id, emoji, limit: 25)
|
|
427
|
+
emoji_str = emoji.respond_to?(:id) ? "#{emoji.name}:#{emoji.id}" : emoji.to_s
|
|
428
|
+
data = @rest.get("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}", params: { limit: limit })
|
|
429
|
+
data.map { |u| User.new(u) }
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Remove all reactions from a message
|
|
433
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
434
|
+
# @param message_id [String, Snowflake] Message ID
|
|
435
|
+
# @return [void]
|
|
436
|
+
def remove_all_reactions(channel_id, message_id)
|
|
437
|
+
@rest.delete("/channels/#{channel_id}/messages/#{message_id}/reactions")
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# === Guild Members (Simplified) ===
|
|
441
|
+
|
|
442
|
+
# Get a guild member
|
|
443
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
444
|
+
# @param user_id [String, Snowflake] User ID
|
|
445
|
+
# @return [Member, nil] Member or nil
|
|
446
|
+
def guild_member(guild_id, user_id)
|
|
447
|
+
data = @rest.get("/guilds/#{guild_id}/members/#{user_id}")
|
|
448
|
+
Member.new(data.merge('guild_id' => guild_id.to_s))
|
|
449
|
+
rescue RestClient::NotFoundError
|
|
450
|
+
nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# List guild members (simplified - basic pagination)
|
|
454
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
455
|
+
# @param limit [Integer] Max members (1-1000, default 100)
|
|
456
|
+
# @param after [String, Snowflake] Get members after this user ID
|
|
457
|
+
# @return [Array<Member>] Members
|
|
458
|
+
def guild_members(guild_id, limit: 100, after: nil)
|
|
459
|
+
params = { limit: limit }
|
|
460
|
+
params[:after] = after.to_s if after
|
|
461
|
+
data = @rest.get("/guilds/#{guild_id}/members", params: params)
|
|
462
|
+
data.map { |m| Member.new(m.merge('guild_id' => guild_id.to_s)) }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Search guild members by query (simplified)
|
|
466
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
467
|
+
# @param query [String] Search query (username/nickname prefix)
|
|
468
|
+
# @param limit [Integer] Max results (1-100, default 25)
|
|
469
|
+
# @return [Array<Member>] Matching members
|
|
470
|
+
def search_guild_members(guild_id, query, limit: 25)
|
|
471
|
+
params = { query: query, limit: limit }
|
|
472
|
+
data = @rest.get("/guilds/#{guild_id}/members/search", params: params)
|
|
473
|
+
data.map { |m| Member.new(m.merge('guild_id' => guild_id.to_s)) }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Modify a guild member (simplified)
|
|
477
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
478
|
+
# @param user_id [String, Snowflake] User ID
|
|
479
|
+
# @param options [Hash] Options to modify (nick, roles, mute, deaf, channel_id)
|
|
480
|
+
# @return [Member] Updated member
|
|
481
|
+
def modify_guild_member(guild_id, user_id, **options)
|
|
482
|
+
payload = options.slice(:nick, :roles, :mute, :deaf, :channel_id, :communication_disabled_until)
|
|
483
|
+
data = @rest.patch("/guilds/#{guild_id}/members/#{user_id}", body: payload)
|
|
484
|
+
Member.new(data.merge('guild_id' => guild_id.to_s))
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Add role to guild member
|
|
488
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
489
|
+
# @param user_id [String, Snowflake] User ID
|
|
490
|
+
# @param role_id [String, Snowflake] Role ID
|
|
491
|
+
# @param reason [String] Audit log reason
|
|
492
|
+
# @return [void]
|
|
493
|
+
def add_guild_member_role(guild_id, user_id, role_id, reason: nil)
|
|
494
|
+
headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
|
|
495
|
+
@rest.put("/guilds/#{guild_id}/members/#{user_id}/roles/#{role_id}", headers: headers)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Remove role from guild member
|
|
499
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
500
|
+
# @param user_id [String, Snowflake] User ID
|
|
501
|
+
# @param role_id [String, Snowflake] Role ID
|
|
502
|
+
# @param reason [String] Audit log reason
|
|
503
|
+
# @return [void]
|
|
504
|
+
def remove_guild_member_role(guild_id, user_id, role_id, reason: nil)
|
|
505
|
+
headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
|
|
506
|
+
@rest.delete("/guilds/#{guild_id}/members/#{user_id}/roles/#{role_id}", headers: headers)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Remove guild member (kick)
|
|
510
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
511
|
+
# @param user_id [String, Snowflake] User ID
|
|
512
|
+
# @param reason [String] Audit log reason
|
|
513
|
+
# @return [void]
|
|
514
|
+
def remove_guild_member(guild_id, user_id, reason: nil)
|
|
515
|
+
headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
|
|
516
|
+
@rest.delete("/guilds/#{guild_id}/members/#{user_id}", headers: headers)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# === Guild Roles (Simplified) ===
|
|
520
|
+
|
|
521
|
+
# Get guild roles
|
|
522
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
523
|
+
# @return [Array<Role>] Roles
|
|
524
|
+
def guild_roles(guild_id)
|
|
525
|
+
data = @rest.get("/guilds/#{guild_id}/roles")
|
|
526
|
+
data.map { |r| Role.new(r.merge('guild_id' => guild_id.to_s)) }
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Create guild role (simplified)
|
|
530
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
531
|
+
# @param name [String] Role name
|
|
532
|
+
# @param options [Hash] Optional settings (permissions, color, hoist, mentionable)
|
|
533
|
+
# @return [Role] Created role
|
|
534
|
+
def create_guild_role(guild_id, name:, **options)
|
|
535
|
+
payload = { name: name }.merge(options.slice(:permissions, :color, :hoist, :mentionable, :icon, :unicode_emoji))
|
|
536
|
+
data = @rest.post("/guilds/#{guild_id}/roles", body: payload)
|
|
537
|
+
Role.new(data.merge('guild_id' => guild_id.to_s))
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Modify guild role
|
|
541
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
542
|
+
# @param role_id [String, Snowflake] Role ID
|
|
543
|
+
# @param options [Hash] Settings to modify
|
|
544
|
+
# @return [Role] Updated role
|
|
545
|
+
def modify_guild_role(guild_id, role_id, **options)
|
|
546
|
+
payload = options.slice(:name, :permissions, :color, :hoist, :mentionable, :icon, :unicode_emoji)
|
|
547
|
+
data = @rest.patch("/guilds/#{guild_id}/roles/#{role_id}", body: payload)
|
|
548
|
+
Role.new(data.merge('guild_id' => guild_id.to_s))
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Delete guild role
|
|
552
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
553
|
+
# @param role_id [String, Snowflake] Role ID
|
|
554
|
+
# @param reason [String] Audit log reason
|
|
555
|
+
# @return [void]
|
|
556
|
+
def delete_guild_role(guild_id, role_id, reason: nil)
|
|
557
|
+
headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
|
|
558
|
+
@rest.delete("/guilds/#{guild_id}/roles/#{role_id}", headers: headers)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# === Guild Bans (Simplified) ===
|
|
562
|
+
|
|
563
|
+
# Get guild bans (simplified - no pagination)
|
|
564
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
565
|
+
# @param limit [Integer] Max bans (1-1000, default 100)
|
|
566
|
+
# @return [Array<Hash>] Bans (user + reason data)
|
|
567
|
+
def guild_bans(guild_id, limit: 100)
|
|
568
|
+
data = @rest.get("/guilds/#{guild_id}/bans", params: { limit: limit })
|
|
569
|
+
data.map { |b| { user: User.new(b['user']), reason: b['reason'] } }
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Get a specific guild ban
|
|
573
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
574
|
+
# @param user_id [String, Snowflake] User ID
|
|
575
|
+
# @return [Hash, nil] Ban data or nil
|
|
576
|
+
def guild_ban(guild_id, user_id)
|
|
577
|
+
data = @rest.get("/guilds/#{guild_id}/bans/#{user_id}")
|
|
578
|
+
{ user: User.new(data['user']), reason: data['reason'] }
|
|
579
|
+
rescue RestClient::NotFoundError
|
|
580
|
+
nil
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Create guild ban
|
|
584
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
585
|
+
# @param user_id [String, Snowflake] User ID
|
|
586
|
+
# @param delete_message_days [Integer] Days of messages to delete (0-7)
|
|
587
|
+
# @param reason [String] Audit log reason
|
|
588
|
+
# @return [void]
|
|
589
|
+
def create_guild_ban(guild_id, user_id, delete_message_days: nil, reason: nil)
|
|
590
|
+
payload = {}
|
|
591
|
+
payload[:delete_message_days] = delete_message_days if delete_message_days
|
|
592
|
+
headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
|
|
593
|
+
@rest.put("/guilds/#{guild_id}/bans/#{user_id}", body: payload, headers: headers)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Remove guild ban (unban)
|
|
597
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
598
|
+
# @param user_id [String, Snowflake] User ID
|
|
599
|
+
# @param reason [String] Audit log reason
|
|
600
|
+
# @return [void]
|
|
601
|
+
def remove_guild_ban(guild_id, user_id, reason: nil)
|
|
602
|
+
headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
|
|
603
|
+
@rest.delete("/guilds/#{guild_id}/bans/#{user_id}", headers: headers)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# === Webhooks (Simplified) ===
|
|
607
|
+
|
|
608
|
+
# Create a webhook
|
|
609
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
610
|
+
# @param name [String] Webhook name
|
|
611
|
+
# @param avatar [String] Base64-encoded avatar image (optional)
|
|
612
|
+
# @return [Hash] Webhook data
|
|
613
|
+
def create_webhook(channel_id, name:, avatar: nil)
|
|
614
|
+
payload = { name: name }
|
|
615
|
+
payload[:avatar] = avatar if avatar
|
|
616
|
+
@rest.post("/channels/#{channel_id}/webhooks", body: payload)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Get channel webhooks
|
|
620
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
621
|
+
# @return [Array<Hash>] Webhooks
|
|
622
|
+
def channel_webhooks(channel_id)
|
|
623
|
+
@rest.get("/channels/#{channel_id}/webhooks")
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Get guild webhooks
|
|
627
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
628
|
+
# @return [Array<Hash>] Webhooks
|
|
629
|
+
def guild_webhooks(guild_id)
|
|
630
|
+
@rest.get("/guilds/#{guild_id}/webhooks")
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Execute webhook (simplified)
|
|
634
|
+
# @param webhook_id [String, Snowflake] Webhook ID
|
|
635
|
+
# @param token [String] Webhook token
|
|
636
|
+
# @param content [String] Message content
|
|
637
|
+
# @param options [Hash] Options (username, avatar_url, embeds, etc.)
|
|
638
|
+
# @return [void]
|
|
639
|
+
def execute_webhook(webhook_id, token, content = nil, **options)
|
|
640
|
+
payload = { content: content }.merge(options.slice(:username, :avatar_url, :embeds, :components, :allowed_mentions))
|
|
641
|
+
@rest.post("/webhooks/#{webhook_id}/#{token}", body: payload)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Delete a webhook
|
|
645
|
+
# @param webhook_id [String, Snowflake] Webhook ID
|
|
646
|
+
# @param token [String] Webhook token (optional, for webhook-owned deletes)
|
|
647
|
+
# @return [void]
|
|
648
|
+
def delete_webhook(webhook_id, token: nil)
|
|
649
|
+
path = token ? "/webhooks/#{webhook_id}/#{token}" : "/webhooks/#{webhook_id}"
|
|
650
|
+
@rest.delete(path)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# === Channel Management (Simplified) ===
|
|
654
|
+
|
|
655
|
+
# Get guild channels
|
|
656
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
657
|
+
# @return [Array<Channel>] Channels
|
|
658
|
+
def guild_channels(guild_id)
|
|
659
|
+
data = @rest.get("/guilds/#{guild_id}/channels")
|
|
660
|
+
data.map { |c| Channel.new(c) }
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Create guild channel (simplified)
|
|
664
|
+
# @param guild_id [String, Snowflake] Guild ID
|
|
665
|
+
# @param name [String] Channel name
|
|
666
|
+
# @param type [Integer] Channel type (0=text, 2=voice, 4=category, etc.)
|
|
667
|
+
# @param options [Hash] Optional settings
|
|
668
|
+
# @return [Channel] Created channel
|
|
669
|
+
def create_guild_channel(guild_id, name:, type: 0, **options)
|
|
670
|
+
payload = { name: name, type: type }.merge(options.slice(:topic, :bitrate, :user_limit, :parent_id, :nsfw, :permission_overwrites, :rate_limit_per_user))
|
|
671
|
+
data = @rest.post("/guilds/#{guild_id}/channels", body: payload)
|
|
672
|
+
Channel.new(data)
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Modify channel
|
|
676
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
677
|
+
# @param options [Hash] Settings to modify
|
|
678
|
+
# @return [Channel] Updated channel
|
|
679
|
+
def modify_channel(channel_id, **options)
|
|
680
|
+
payload = options.slice(:name, :type, :position, :topic, :nsfw, :rate_limit_per_user, :bitrate, :user_limit, :parent_id, :default_auto_archive_duration)
|
|
681
|
+
data = @rest.patch("/channels/#{channel_id}", body: payload)
|
|
682
|
+
Channel.new(data)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Delete channel
|
|
686
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
687
|
+
# @param reason [String] Audit log reason
|
|
688
|
+
# @return [Channel] Deleted channel
|
|
689
|
+
def delete_channel(channel_id, reason: nil)
|
|
690
|
+
headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
|
|
691
|
+
data = @rest.delete("/channels/#{channel_id}", headers: headers)
|
|
692
|
+
Channel.new(data)
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Bulk delete messages
|
|
696
|
+
# @param channel_id [String, Snowflake] Channel ID
|
|
697
|
+
# @param message_ids [Array<String, Snowflake>] Message IDs to delete (2-100)
|
|
698
|
+
# @param reason [String] Audit log reason
|
|
699
|
+
# @return [void]
|
|
700
|
+
def bulk_delete_messages(channel_id, message_ids, reason: nil)
|
|
701
|
+
headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
|
|
702
|
+
@rest.post("/channels/#{channel_id}/messages/bulk-delete", body: { messages: message_ids.map(&:to_s) }, headers: headers)
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def setup_interaction_handlers
|
|
706
|
+
# Handle slash commands
|
|
707
|
+
@event_bus.on(:interaction_create) do |event|
|
|
708
|
+
interaction = event.interaction
|
|
709
|
+
|
|
710
|
+
if interaction.command?
|
|
711
|
+
handle_slash_command(interaction)
|
|
712
|
+
elsif interaction.component?
|
|
713
|
+
handle_component(interaction)
|
|
714
|
+
elsif interaction.autocomplete?
|
|
715
|
+
handle_autocomplete(interaction)
|
|
716
|
+
elsif interaction.modal_submit?
|
|
717
|
+
handle_modal_submit(interaction)
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def handle_slash_command(interaction)
|
|
723
|
+
cmd_name = interaction.command_name
|
|
724
|
+
guild_id = interaction.guild_id
|
|
725
|
+
|
|
726
|
+
# Try guild-specific command first, then global
|
|
727
|
+
key = guild_id ? "#{cmd_name}:#{guild_id}" : cmd_name
|
|
728
|
+
cmd = @slash_commands[key] || @slash_commands[cmd_name]
|
|
729
|
+
|
|
730
|
+
if cmd && cmd.handler
|
|
731
|
+
@logger.debug('Executing slash command', name: cmd_name, user: interaction.user&.id)
|
|
732
|
+
begin
|
|
733
|
+
cmd.handler.call(interaction)
|
|
734
|
+
rescue => e
|
|
735
|
+
@logger.error('Slash command error', command: cmd_name, error: e)
|
|
736
|
+
# Send error response
|
|
737
|
+
interaction.respond(content: "An error occurred while executing this command.", ephemeral: true) rescue nil
|
|
738
|
+
end
|
|
739
|
+
else
|
|
740
|
+
@logger.warn('Unknown slash command', name: cmd_name)
|
|
741
|
+
interaction.respond(content: "Unknown command: #{cmd_name}", ephemeral: true) rescue nil
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def handle_component(interaction)
|
|
746
|
+
# Component interactions are handled by custom_id patterns or specific handlers
|
|
747
|
+
custom_id = interaction.custom_id
|
|
748
|
+
@logger.debug('Component interaction', custom_id: custom_id, user: interaction.user&.id)
|
|
749
|
+
|
|
750
|
+
# Emit specific event for this component type
|
|
751
|
+
event_type = case interaction.component_type
|
|
752
|
+
when 2 then :button_click
|
|
753
|
+
when 3 then :string_select
|
|
754
|
+
when 5 then :user_select
|
|
755
|
+
when 6 then :role_select
|
|
756
|
+
when 7 then :mentionable_select
|
|
757
|
+
when 8 then :channel_select
|
|
758
|
+
else :component_interaction
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
@event_bus.emit(event_type, interaction)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def handle_autocomplete(interaction)
|
|
765
|
+
# Autocomplete needs to be handled by the command that registered it
|
|
766
|
+
cmd_name = interaction.command_name
|
|
767
|
+
focused = interaction.focused_option
|
|
768
|
+
|
|
769
|
+
@logger.debug('Autocomplete', command: cmd_name, option: focused&.dig('name'))
|
|
770
|
+
|
|
771
|
+
# Emit autocomplete event
|
|
772
|
+
@event_bus.emit(:autocomplete, interaction)
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def handle_modal_submit(interaction)
|
|
776
|
+
modal_id = interaction.custom_id
|
|
777
|
+
values = interaction.modal_values
|
|
778
|
+
|
|
779
|
+
@logger.debug('Modal submit', modal_id: modal_id, values: values.keys)
|
|
780
|
+
|
|
781
|
+
# Emit modal submit event
|
|
782
|
+
@event_bus.emit(:modal_submit, interaction)
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
private
|
|
786
|
+
|
|
787
|
+
def build_cache
|
|
788
|
+
store = case @config.cache
|
|
789
|
+
when :redis
|
|
790
|
+
RedisStore.new
|
|
791
|
+
else
|
|
792
|
+
MemoryStore.new
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
EntityCache.new(store, logger: @logger)
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def setup_event_handlers
|
|
799
|
+
# Cache entities on relevant events
|
|
800
|
+
@event_bus.on(:guild_create) do |event|
|
|
801
|
+
@cache.cache_guild(event.guild) if event.available?
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
@event_bus.on(:guild_update) do |event|
|
|
805
|
+
@cache.cache_guild(event.guild)
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
@event_bus.on(:channel_create) do |event|
|
|
809
|
+
@cache.cache_channel(event.channel)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
@event_bus.on(:channel_update) do |event|
|
|
813
|
+
@cache.cache_channel(event.channel)
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
@event_bus.on(:message_create) do |event|
|
|
817
|
+
@cache.cache_message(event.message)
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# Track ready state
|
|
821
|
+
@event_bus.on(:ready) do |event|
|
|
822
|
+
@logger.info('Bot ready', user: event.user&.username, guilds: event.guilds.length)
|
|
823
|
+
@plugins.all.each { |p| p.ready(self) if p.enabled? }
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def start_shards
|
|
828
|
+
if @config.shards == [:auto]
|
|
829
|
+
@shard_manager.start
|
|
830
|
+
else
|
|
831
|
+
shard_ids = @config.shards.map(&:first)
|
|
832
|
+
@shard_manager.start(shard_ids)
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Keep running
|
|
836
|
+
sleep(1) while @running
|
|
837
|
+
rescue Interrupt
|
|
838
|
+
@logger.info('Interrupted')
|
|
839
|
+
stop
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
end
|