(Feat): Added a minimal pikanetwork client
This commit is contained in:
361
src/client/EllyClient.ts
Normal file
361
src/client/EllyClient.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Elly Discord Bot Client
|
||||
* Extended Discord.js Client with custom functionality
|
||||
*/
|
||||
|
||||
import {
|
||||
Client,
|
||||
Collection,
|
||||
GatewayIntentBits,
|
||||
Partials,
|
||||
ActivityType,
|
||||
type TextChannel,
|
||||
type Role,
|
||||
type Guild,
|
||||
} from 'discord.js';
|
||||
import type { Config } from '../config/types.ts';
|
||||
import type { Command } from '../types/index.ts';
|
||||
import { PikaNetworkAPI } from '../api/pika/index.ts';
|
||||
import { JsonDatabase } from '../database/connection.ts';
|
||||
import { DatabaseManager, createDatabaseManager } from '../database/DatabaseManager.ts';
|
||||
import { PermissionService } from '../services/PermissionService.ts';
|
||||
import { Logger, createLogger } from '../utils/logger.ts';
|
||||
import { getErrorHandler, type ErrorHandler } from '../utils/errors.ts';
|
||||
|
||||
/**
|
||||
* Extended Discord.js Client for Elly
|
||||
*/
|
||||
export class EllyClient extends Client {
|
||||
// Configuration
|
||||
public readonly config: Config;
|
||||
|
||||
// Services
|
||||
public readonly pikaAPI: PikaNetworkAPI;
|
||||
public readonly database: JsonDatabase; // Legacy JSON database
|
||||
public dbManager: DatabaseManager | null = null; // New SQLite database
|
||||
public readonly permissions: PermissionService;
|
||||
public readonly logger: Logger;
|
||||
public readonly errorHandler: ErrorHandler;
|
||||
|
||||
// Collections
|
||||
public readonly commands = new Collection<string, Command>();
|
||||
public readonly cooldowns = new Collection<string, Collection<string, number>>();
|
||||
|
||||
// Cached references
|
||||
public mainGuild: Guild | null = null;
|
||||
|
||||
// Cached roles
|
||||
public roles: {
|
||||
admin: Role | null;
|
||||
leader: Role | null;
|
||||
officer: Role | null;
|
||||
developer: Role | null;
|
||||
guildMember: Role | null;
|
||||
champion: Role | null;
|
||||
away: Role | null;
|
||||
applicationsBlacklisted: Role | null;
|
||||
suggestionsBlacklisted: Role | null;
|
||||
} = {
|
||||
admin: null,
|
||||
leader: null,
|
||||
officer: null,
|
||||
developer: null,
|
||||
guildMember: null,
|
||||
champion: null,
|
||||
away: null,
|
||||
applicationsBlacklisted: null,
|
||||
suggestionsBlacklisted: null,
|
||||
};
|
||||
|
||||
// Cached channels
|
||||
public channels_cache: {
|
||||
applications: TextChannel | null;
|
||||
applicationLogs: TextChannel | null;
|
||||
suggestions: TextChannel | null;
|
||||
suggestionLogs: TextChannel | null;
|
||||
guildUpdates: TextChannel | null;
|
||||
discordChangelog: TextChannel | null;
|
||||
inactivity: TextChannel | null;
|
||||
developmentLogs: TextChannel | null;
|
||||
donations: TextChannel | null;
|
||||
reminders: TextChannel | null;
|
||||
} = {
|
||||
applications: null,
|
||||
applicationLogs: null,
|
||||
suggestions: null,
|
||||
suggestionLogs: null,
|
||||
guildUpdates: null,
|
||||
discordChangelog: null,
|
||||
inactivity: null,
|
||||
developmentLogs: null,
|
||||
donations: null,
|
||||
reminders: null,
|
||||
};
|
||||
|
||||
// State
|
||||
private refreshInterval: number | undefined;
|
||||
|
||||
constructor(config: Config) {
|
||||
super({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
partials: [
|
||||
Partials.Message,
|
||||
Partials.Channel,
|
||||
Partials.Reaction,
|
||||
Partials.User,
|
||||
Partials.GuildMember,
|
||||
],
|
||||
});
|
||||
|
||||
this.config = config;
|
||||
|
||||
// Initialize services
|
||||
this.logger = createLogger('Elly', {
|
||||
level: config.logging.level,
|
||||
logFile: config.logging.file,
|
||||
});
|
||||
|
||||
this.pikaAPI = new PikaNetworkAPI({
|
||||
cacheTTL: config.api.pika_cache_ttl,
|
||||
timeout: config.api.pika_request_timeout,
|
||||
});
|
||||
|
||||
this.database = new JsonDatabase(config.database.path.replace('.db', '.json'));
|
||||
this.permissions = new PermissionService(config);
|
||||
this.errorHandler = getErrorHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the bot
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
this.logger.info('Initializing Elly...');
|
||||
|
||||
// Load legacy JSON database (for migration)
|
||||
await this.database.load();
|
||||
this.logger.info('Legacy JSON database loaded');
|
||||
|
||||
// Initialize SQLite database
|
||||
try {
|
||||
const sqlitePath = this.config.database.path.replace('.json', '.sqlite');
|
||||
this.dbManager = await createDatabaseManager(sqlitePath);
|
||||
this.logger.info('SQLite database initialized');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize SQLite database:', error);
|
||||
// Continue with JSON database as fallback
|
||||
}
|
||||
|
||||
// Set up event handlers
|
||||
this.setupEventHandlers();
|
||||
|
||||
// Start refresh interval
|
||||
this.startRefreshInterval();
|
||||
|
||||
this.logger.info('Initialization complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up core event handlers
|
||||
*/
|
||||
private setupEventHandlers(): void {
|
||||
this.once('ready', () => this.onReady());
|
||||
this.on('error', (error) => this.logger.error('Client error', error));
|
||||
this.on('warn', (warning) => this.logger.warn('Client warning', warning));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ready event
|
||||
*/
|
||||
private async onReady(): Promise<void> {
|
||||
this.logger.info(`Logged in as ${this.user?.tag}`);
|
||||
|
||||
// Set presence
|
||||
this.user?.setPresence({
|
||||
activities: [
|
||||
{
|
||||
name: this.config.bot.status,
|
||||
type: this.getActivityType(this.config.bot.activity_type),
|
||||
},
|
||||
],
|
||||
status: 'online',
|
||||
});
|
||||
|
||||
// Cache guild and roles
|
||||
await this.refreshCache();
|
||||
|
||||
this.logger.info('Elly is ready!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Discord.js ActivityType from config string
|
||||
*/
|
||||
private getActivityType(type: string): ActivityType {
|
||||
const types: Record<string, ActivityType> = {
|
||||
playing: ActivityType.Playing,
|
||||
streaming: ActivityType.Streaming,
|
||||
listening: ActivityType.Listening,
|
||||
watching: ActivityType.Watching,
|
||||
competing: ActivityType.Competing,
|
||||
};
|
||||
return types[type] ?? ActivityType.Watching;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cached guild, roles, and channels
|
||||
*/
|
||||
async refreshCache(): Promise<void> {
|
||||
try {
|
||||
// Get main guild
|
||||
this.mainGuild = await this.guilds.fetch(this.config.guild.id);
|
||||
|
||||
if (!this.mainGuild) {
|
||||
this.logger.error('Could not find main guild');
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache roles
|
||||
const roleConfig = this.config.roles;
|
||||
this.roles = {
|
||||
admin: this.findRole(roleConfig.admin),
|
||||
leader: this.findRole(roleConfig.leader),
|
||||
officer: this.findRole(roleConfig.officer),
|
||||
developer: this.findRole(roleConfig.developer),
|
||||
guildMember: this.findRole(roleConfig.guild_member),
|
||||
champion: this.findRole(roleConfig.champion),
|
||||
away: this.findRole(roleConfig.away),
|
||||
applicationsBlacklisted: this.findRole(roleConfig.applications_blacklisted),
|
||||
suggestionsBlacklisted: this.findRole(roleConfig.suggestions_blacklisted),
|
||||
};
|
||||
|
||||
// Cache channels
|
||||
const channelConfig = this.config.channels;
|
||||
this.channels_cache = {
|
||||
applications: this.findChannel(channelConfig.applications),
|
||||
applicationLogs: this.findChannel(channelConfig.application_logs),
|
||||
suggestions: this.findChannel(channelConfig.suggestions),
|
||||
suggestionLogs: this.findChannel(channelConfig.suggestion_logs),
|
||||
guildUpdates: this.findChannel(channelConfig.guild_updates),
|
||||
discordChangelog: this.findChannel(channelConfig.discord_changelog),
|
||||
inactivity: this.findChannel(channelConfig.inactivity),
|
||||
developmentLogs: this.findChannel(channelConfig.development_logs),
|
||||
donations: this.findChannel(channelConfig.donations),
|
||||
reminders: this.findChannel(channelConfig.reminders),
|
||||
};
|
||||
|
||||
this.logger.debug('Cache refreshed');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to refresh cache', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a role by name in the main guild
|
||||
*/
|
||||
private findRole(name: string): Role | null {
|
||||
if (!this.mainGuild) return null;
|
||||
return this.mainGuild.roles.cache.find(
|
||||
(role) => role.name.toLowerCase() === name.toLowerCase()
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a channel by name in the main guild
|
||||
*/
|
||||
private findChannel(name: string): TextChannel | null {
|
||||
if (!this.mainGuild) return null;
|
||||
const channel = this.mainGuild.channels.cache.find(
|
||||
(ch) => ch.name.toLowerCase() === name.toLowerCase()
|
||||
);
|
||||
return channel?.isTextBased() ? (channel as TextChannel) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the cache refresh interval
|
||||
*/
|
||||
private startRefreshInterval(): void {
|
||||
// Refresh cache every 10 minutes
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.refreshCache();
|
||||
}, 600000);
|
||||
|
||||
// Clear PikaNetwork API cache every hour
|
||||
setInterval(() => {
|
||||
this.pikaAPI.clearCache();
|
||||
this.logger.debug('PikaNetwork API cache cleared');
|
||||
}, 3600000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a command
|
||||
*/
|
||||
registerCommand(command: Command): void {
|
||||
this.commands.set(command.data.name, command);
|
||||
this.logger.debug(`Registered command: ${command.data.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is on cooldown for a command
|
||||
*/
|
||||
isOnCooldown(userId: string, commandName: string): number {
|
||||
const commandCooldowns = this.cooldowns.get(commandName);
|
||||
if (!commandCooldowns) return 0;
|
||||
|
||||
const expiresAt = commandCooldowns.get(userId);
|
||||
if (!expiresAt) return 0;
|
||||
|
||||
const now = Date.now();
|
||||
if (now < expiresAt) {
|
||||
return Math.ceil((expiresAt - now) / 1000);
|
||||
}
|
||||
|
||||
commandCooldowns.delete(userId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a cooldown for a user on a command
|
||||
*/
|
||||
setCooldown(userId: string, commandName: string, seconds: number): void {
|
||||
if (!this.cooldowns.has(commandName)) {
|
||||
this.cooldowns.set(commandName, new Collection());
|
||||
}
|
||||
|
||||
const commandCooldowns = this.cooldowns.get(commandName)!;
|
||||
commandCooldowns.set(userId, Date.now() + seconds * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
this.logger.info('Shutting down...');
|
||||
|
||||
// Clear intervals
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
|
||||
// Close SQLite database
|
||||
if (this.dbManager) {
|
||||
this.dbManager.close();
|
||||
}
|
||||
|
||||
// Save legacy JSON database
|
||||
await this.database.close();
|
||||
|
||||
// Destroy API client
|
||||
this.pikaAPI.destroy();
|
||||
|
||||
// Destroy Discord client
|
||||
this.destroy();
|
||||
|
||||
this.logger.info('Shutdown complete');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user