discord-starboard-plus
Version:
Discord Starboard Plus: A clean, maintainable starboard system for Discord.js bots. Features per-guild configuration, TypeScript support. Highlight your community's favorite messages with customizable starboards.
237 lines • 8.64 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StarboardManager = void 0;
const ConfigValidator_1 = require("../utils/ConfigValidator");
const Logger_1 = require("../utils/Logger");
const GuildConfigManager_1 = require("./GuildConfigManager");
const ValidationService_1 = require("../services/ValidationService");
const MessageSearchService_1 = require("../services/MessageSearchService");
const EmbedBuilderService_1 = require("../services/EmbedBuilderService");
const ReactionAddHandler_1 = require("../handlers/ReactionAddHandler");
const ReactionRemoveHandler_1 = require("../handlers/ReactionRemoveHandler");
const MessageDeleteHandler_1 = require("../handlers/MessageDeleteHandler");
/**
* Main orchestrator for the Starboard system.
*
* Implements clean architecture principles:
* - Dependency injection for all services
* - Proper event listener management (no memory leaks)
* - Per-guild configuration support
*/
class StarboardManager {
client;
logger;
guildConfig;
validation;
messageSearch;
embedBuilder;
handlers;
/**
* Bound event handlers stored for proper cleanup.
*/
boundHandlers;
isDestroyed = false;
/**
* Create a new StarboardManager instance.
*
* @param client - Discord.js Client instance
* @param options - Starboard configuration options
* @throws Error if client is missing or options are invalid
*/
constructor(client, options) {
if (!client) {
throw new Error('Discord client is required');
}
// Validate and normalize options
const normalizedOptions = ConfigValidator_1.ConfigValidator.validateAndNormalize(options);
this.client = client;
// Initialize services
this.logger = new Logger_1.Logger({
enabled: normalizedOptions.logActions,
prefix: 'Starboard'
});
this.guildConfig = new GuildConfigManager_1.GuildConfigManager(normalizedOptions, this.logger);
this.validation = new ValidationService_1.ValidationService(this.logger);
this.messageSearch = new MessageSearchService_1.MessageSearchService(this.logger);
this.embedBuilder = new EmbedBuilderService_1.EmbedBuilderService();
// Initialize handlers
this.handlers = this.createHandlers();
// Create bound handlers for proper cleanup
// Note: Discord.js event handlers receive partial types that we need to handle
this.boundHandlers = {
reactionAdd: (r, u) => this.handleReactionAdd(r, u),
reactionRemove: (r, u) => this.handleReactionRemove(r, u),
messageDelete: (m) => this.handleMessageDelete(m)
};
// Attach event listeners
this.attachListeners();
this.logger.success('Starboard system initialized');
}
/**
* Create handler instances with injected dependencies.
*/
createHandlers() {
const dependencies = {
client: this.client,
logger: this.logger,
guildConfig: this.guildConfig,
validation: this.validation,
messageSearch: this.messageSearch,
embedBuilder: this.embedBuilder
};
return {
reactionAdd: new ReactionAddHandler_1.ReactionAddHandler(dependencies),
reactionRemove: new ReactionRemoveHandler_1.ReactionRemoveHandler(dependencies),
messageDelete: new MessageDeleteHandler_1.MessageDeleteHandler(dependencies)
};
}
/**
* Attach event listeners to the client.
*/
attachListeners() {
this.logger.info('Initializing event listeners...');
this.client.on('messageReactionAdd', this.boundHandlers.reactionAdd);
this.client.on('messageReactionRemove', this.boundHandlers.reactionRemove);
this.client.on('messageDelete', this.boundHandlers.messageDelete);
this.logger.success('Event listeners attached');
}
/**
* Wrapper for reaction add handler.
* Handles partial reactions by fetching the full reaction if needed.
*/
async handleReactionAdd(reaction, user) {
if (this.isDestroyed)
return;
const fullReaction = reaction.partial
? await reaction.fetch().catch(() => null)
: reaction;
if (!fullReaction)
return;
const fullUser = user.partial
? await user.fetch().catch(() => null)
: user;
if (!fullUser)
return;
return this.handlers.reactionAdd.handle(fullReaction, fullUser);
}
/**
* Wrapper for reaction remove handler.
* Handles partial reactions by fetching the full reaction if needed.
*/
async handleReactionRemove(reaction, user) {
if (this.isDestroyed)
return;
// Fetch partial reaction if needed
const fullReaction = reaction.partial
? await reaction.fetch().catch(() => null)
: reaction;
if (!fullReaction)
return;
// Fetch partial user if needed
const fullUser = user.partial
? await user.fetch().catch(() => null)
: user;
if (!fullUser)
return;
return this.handlers.reactionRemove.handle(fullReaction, fullUser);
}
/**
* Wrapper for message delete handler.
* Handles partial messages - note: deleted messages may not be fetchable.
*/
async handleMessageDelete(message) {
if (this.isDestroyed)
return;
// For deleted messages, we may not be able to fetch the full message
// But we still have the message ID which is what we need
return this.handlers.messageDelete.handle(message);
}
/**
* Clean up event listeners and destroy the instance.
* FIX: This method properly removes event listeners, fixing the memory leak.
*/
destroy() {
if (this.isDestroyed) {
this.logger.warn('StarboardManager already destroyed');
return;
}
this.client.off('messageReactionAdd', this.boundHandlers.reactionAdd);
this.client.off('messageReactionRemove', this.boundHandlers.reactionRemove);
this.client.off('messageDelete', this.boundHandlers.messageDelete);
this.isDestroyed = true;
this.logger.info('Starboard system destroyed');
}
/**
* Check if the manager has been destroyed.
*/
isActive() {
return !this.isDestroyed;
}
// =========================================================================
// Public API for per-guild configuration
// =========================================================================
/**
* Set configuration for a specific guild.
*
* @param guildId - The guild ID
* @param options - Partial options to merge with defaults
*/
setGuildConfig(guildId, options) {
this.guildConfig.setGuildConfig(guildId, options);
}
/**
* Get configuration for a specific guild.
*
* @param guildId - The guild ID
* @returns The guild's configuration (or defaults if not set)
*/
getGuildConfig(guildId) {
return this.guildConfig.getGuildConfig(guildId);
}
/**
* Check if a guild has custom configuration.
*
* @param guildId - The guild ID
*/
hasGuildConfig(guildId) {
return this.guildConfig.hasGuildConfig(guildId);
}
/**
* Remove custom configuration for a guild.
*
* @param guildId - The guild ID
* @returns true if config was removed, false if it didn't exist
*/
removeGuildConfig(guildId) {
return this.guildConfig.removeGuildConfig(guildId);
}
/**
* Get the default options used for guilds without custom config.
*/
getDefaultOptions() {
return this.guildConfig.getDefaultOptions();
}
/**
* Update the default options.
* Affects all guilds without custom configuration.
*
* @param options - Partial options to merge with current defaults
*/
updateDefaultOptions(options) {
this.guildConfig.setDefaultOptions(options);
}
/**
* Get list of guilds with custom configuration.
*/
getConfiguredGuilds() {
return this.guildConfig.getConfiguredGuilds();
}
/**
* Get number of guilds with custom configuration.
*/
getConfiguredGuildCount() {
return this.guildConfig.getConfiguredGuildCount();
}
}
exports.StarboardManager = StarboardManager;
//# sourceMappingURL=StarboardManager.js.map