(Feat): Added a minimal pikanetwork client
This commit is contained in:
212
src/events/interactionCreate.ts
Normal file
212
src/events/interactionCreate.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Interaction Create Event Handler
|
||||
* Handles all Discord interactions (commands, buttons, modals, etc.)
|
||||
*/
|
||||
|
||||
import { Events, type Interaction } from 'discord.js';
|
||||
import type { EllyClient } from '../client/EllyClient.ts';
|
||||
import type { BotEvent } from '../types/index.ts';
|
||||
|
||||
export const interactionCreateEvent: BotEvent<Interaction> = {
|
||||
name: Events.InteractionCreate,
|
||||
|
||||
async execute(interaction: Interaction): Promise<void> {
|
||||
const client = interaction.client as EllyClient;
|
||||
|
||||
// Handle slash commands
|
||||
if (interaction.isChatInputCommand()) {
|
||||
await handleCommand(client, interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle autocomplete
|
||||
if (interaction.isAutocomplete()) {
|
||||
await handleAutocomplete(client, interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle button interactions
|
||||
if (interaction.isButton()) {
|
||||
await handleButton(client, interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle modal submissions
|
||||
if (interaction.isModalSubmit()) {
|
||||
await handleModal(client, interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle select menu interactions
|
||||
if (interaction.isStringSelectMenu()) {
|
||||
await handleSelectMenu(client, interaction);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle slash command interactions
|
||||
*/
|
||||
async function handleCommand(
|
||||
client: EllyClient,
|
||||
interaction: Interaction & { isChatInputCommand(): true }
|
||||
): Promise<void> {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
if (!command) {
|
||||
client.logger.warn(`Unknown command: ${interaction.commandName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
const cooldownRemaining = client.isOnCooldown(interaction.user.id, interaction.commandName);
|
||||
if (cooldownRemaining > 0) {
|
||||
await interaction.reply({
|
||||
content: `⏳ Please wait **${cooldownRemaining}** seconds before using this command again.`,
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if (interaction.guild && interaction.member) {
|
||||
const member = interaction.guild.members.cache.get(interaction.user.id);
|
||||
if (member && !client.permissions.hasPermission(member, command.permission)) {
|
||||
await interaction.reply({
|
||||
content: client.permissions.formatDeniedMessage(command.permission),
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute command
|
||||
try {
|
||||
client.logger.debug(`Executing command: ${interaction.commandName}`, {
|
||||
user: interaction.user.tag,
|
||||
guild: interaction.guild?.name,
|
||||
});
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
// Set cooldown
|
||||
if (command.cooldown) {
|
||||
client.setCooldown(interaction.user.id, interaction.commandName, command.cooldown);
|
||||
}
|
||||
} catch (error) {
|
||||
client.logger.error(`Error executing command ${interaction.commandName}`, error);
|
||||
|
||||
const errorMessage = '❌ An error occurred while executing this command.';
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: errorMessage, ephemeral: true });
|
||||
} else {
|
||||
await interaction.reply({ content: errorMessage, ephemeral: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle autocomplete interactions
|
||||
*/
|
||||
async function handleAutocomplete(
|
||||
client: EllyClient,
|
||||
interaction: Interaction & { isAutocomplete(): true }
|
||||
): Promise<void> {
|
||||
if (!interaction.isAutocomplete()) return;
|
||||
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
if (!command?.autocomplete) return;
|
||||
|
||||
try {
|
||||
await command.autocomplete(interaction);
|
||||
} catch (error) {
|
||||
client.logger.error(`Error handling autocomplete for ${interaction.commandName}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle button interactions
|
||||
*/
|
||||
async function handleButton(
|
||||
client: EllyClient,
|
||||
interaction: Interaction & { isButton(): true }
|
||||
): Promise<void> {
|
||||
if (!interaction.isButton()) return;
|
||||
|
||||
client.logger.debug(`Button interaction: ${interaction.customId}`, {
|
||||
user: interaction.user.tag,
|
||||
});
|
||||
|
||||
// Handle paginator buttons
|
||||
if (interaction.customId.startsWith('paginator:')) {
|
||||
// Paginator handles its own interactions
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle application buttons
|
||||
if (interaction.customId.startsWith('application:')) {
|
||||
// TODO: Implement application button handler
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle suggestion buttons
|
||||
if (interaction.customId.startsWith('suggestion:')) {
|
||||
// TODO: Implement suggestion button handler
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle family buttons
|
||||
if (interaction.customId.startsWith('family:')) {
|
||||
// TODO: Implement family button handler
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle modal submissions
|
||||
*/
|
||||
async function handleModal(
|
||||
client: EllyClient,
|
||||
interaction: Interaction & { isModalSubmit(): true }
|
||||
): Promise<void> {
|
||||
if (!interaction.isModalSubmit()) return;
|
||||
|
||||
client.logger.debug(`Modal submission: ${interaction.customId}`, {
|
||||
user: interaction.user.tag,
|
||||
});
|
||||
|
||||
// Handle application modal
|
||||
if (interaction.customId.startsWith('application:')) {
|
||||
// TODO: Implement application modal handler
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle feedback modal
|
||||
if (interaction.customId.startsWith('feedback:')) {
|
||||
// TODO: Implement feedback modal handler
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle select menu interactions
|
||||
*/
|
||||
async function handleSelectMenu(
|
||||
client: EllyClient,
|
||||
interaction: Interaction & { isStringSelectMenu(): true }
|
||||
): Promise<void> {
|
||||
if (!interaction.isStringSelectMenu()) return;
|
||||
|
||||
client.logger.debug(`Select menu interaction: ${interaction.customId}`, {
|
||||
user: interaction.user.tag,
|
||||
values: interaction.values,
|
||||
});
|
||||
|
||||
// Handle stats select menu
|
||||
if (interaction.customId.startsWith('stats:')) {
|
||||
// TODO: Implement stats select menu handler
|
||||
return;
|
||||
}
|
||||
}
|
||||
167
src/events/messageCreate.ts
Normal file
167
src/events/messageCreate.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* MessageCreate Event
|
||||
* Handles message filtering and auto-moderation
|
||||
*/
|
||||
|
||||
import { Events, type Message, EmbedBuilder, ChannelType } from 'discord.js';
|
||||
import type { BotEvent } from '../types/index.ts';
|
||||
import type { EllyClient } from '../client/EllyClient.ts';
|
||||
import { FilterRepository } from '../database/repositories/FilterRepository.ts';
|
||||
|
||||
export const messageCreateEvent: BotEvent = {
|
||||
name: Events.MessageCreate,
|
||||
once: false,
|
||||
|
||||
async execute(message: Message): Promise<void> {
|
||||
// Ignore bots and DMs
|
||||
if (message.author.bot) return;
|
||||
if (!message.guild) return;
|
||||
if (!message.member) return;
|
||||
|
||||
const client = message.client as EllyClient;
|
||||
|
||||
// Check if filtering is enabled
|
||||
if (!client.config.features.channelFiltering) return;
|
||||
|
||||
const repo = new FilterRepository(client.database);
|
||||
|
||||
// Get user's role IDs
|
||||
const userRoles = message.member.roles.cache.map((r) => r.id);
|
||||
|
||||
// Check message against filters
|
||||
const matchedFilter = await repo.checkMessage(
|
||||
message.channel.id,
|
||||
message.content,
|
||||
userRoles
|
||||
);
|
||||
|
||||
// Also check for attachments if there's an attachment filter
|
||||
if (!matchedFilter && message.attachments.size > 0) {
|
||||
const filters = await repo.getChannelFilters(message.channel.id);
|
||||
const attachmentFilter = filters.find((f) => f.filterType === 'attachments');
|
||||
|
||||
if (attachmentFilter && !attachmentFilter.allowedRoles.some((r) => userRoles.includes(r))) {
|
||||
await handleFilterMatch(message, client, repo, attachmentFilter.id, 'attachments');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedFilter) {
|
||||
await handleFilterMatch(message, client, repo, matchedFilter.id, matchedFilter.filterType);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a filter match
|
||||
*/
|
||||
async function handleFilterMatch(
|
||||
message: Message,
|
||||
client: EllyClient,
|
||||
repo: FilterRepository,
|
||||
filterId: string,
|
||||
filterType: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Delete the message
|
||||
await message.delete();
|
||||
|
||||
// Log the action
|
||||
await repo.logAction({
|
||||
filterId,
|
||||
channelId: message.channel.id,
|
||||
userId: message.author.id,
|
||||
messageContent: message.content.substring(0, 500), // Truncate for storage
|
||||
action: 'deleted',
|
||||
});
|
||||
|
||||
// Send warning to user
|
||||
try {
|
||||
const warningEmbed = new EmbedBuilder()
|
||||
.setColor(client.config.colors.warning)
|
||||
.setTitle('⚠️ Message Removed')
|
||||
.setDescription(
|
||||
`Your message in ${message.channel} was removed because it violated the channel rules.`
|
||||
)
|
||||
.addFields({ name: 'Reason', value: getFilterReason(filterType) })
|
||||
.setFooter({ text: 'Please follow the channel guidelines.' })
|
||||
.setTimestamp();
|
||||
|
||||
await message.author.send({ embeds: [warningEmbed] });
|
||||
} catch {
|
||||
// User might have DMs disabled
|
||||
}
|
||||
|
||||
// Log to development channel
|
||||
await logToDevChannel(message, client, filterType);
|
||||
|
||||
} catch (error) {
|
||||
// Message might have already been deleted
|
||||
console.error('[Filter] Error handling filter match:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable filter reason
|
||||
*/
|
||||
function getFilterReason(filterType: string): string {
|
||||
switch (filterType) {
|
||||
case 'links':
|
||||
return 'Links are not allowed in this channel.';
|
||||
case 'images':
|
||||
return 'Image links are not allowed in this channel.';
|
||||
case 'attachments':
|
||||
return 'Attachments are not allowed in this channel.';
|
||||
case 'invites':
|
||||
return 'Discord invites are not allowed in this channel.';
|
||||
case 'custom':
|
||||
return 'Your message matched a blocked pattern.';
|
||||
default:
|
||||
return 'Your message violated channel rules.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log filter action to development channel
|
||||
*/
|
||||
async function logToDevChannel(
|
||||
message: Message,
|
||||
client: EllyClient,
|
||||
filterType: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const logChannelName = client.config.channels.developmentLogs;
|
||||
const logChannel = message.guild?.channels.cache.find(
|
||||
(c) => c.name === logChannelName && c.type === ChannelType.GuildText
|
||||
);
|
||||
|
||||
if (!logChannel || logChannel.type !== ChannelType.GuildText) return;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(client.config.colors.warning)
|
||||
.setTitle('🛡️ Message Filtered')
|
||||
.addFields(
|
||||
{ name: 'User', value: `${message.author.tag} (${message.author.id})`, inline: true },
|
||||
{ name: 'Channel', value: `${message.channel}`, inline: true },
|
||||
{ name: 'Filter Type', value: filterType, inline: true },
|
||||
{
|
||||
name: 'Content',
|
||||
value: message.content.length > 0
|
||||
? `\`\`\`${message.content.substring(0, 500)}\`\`\``
|
||||
: '*No text content*'
|
||||
}
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (message.attachments.size > 0) {
|
||||
embed.addFields({
|
||||
name: 'Attachments',
|
||||
value: message.attachments.map((a) => a.name).join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
await logChannel.send({ embeds: [embed] });
|
||||
} catch {
|
||||
// Ignore logging errors
|
||||
}
|
||||
}
|
||||
119
src/events/ready.ts
Normal file
119
src/events/ready.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Ready Event Handler
|
||||
* Handles bot initialization when connected to Discord
|
||||
*/
|
||||
|
||||
import { Events, REST, Routes } from 'discord.js';
|
||||
import type { EllyClient } from '../client/EllyClient.ts';
|
||||
import type { BotEvent } from '../types/index.ts';
|
||||
|
||||
export const readyEvent: BotEvent = {
|
||||
name: Events.ClientReady,
|
||||
once: true,
|
||||
|
||||
async execute(client: EllyClient): Promise<void> {
|
||||
console.log('');
|
||||
client.logger.info('═══════════════════════════════════════════════════════');
|
||||
client.logger.info(' BOT CONNECTED ');
|
||||
client.logger.info('═══════════════════════════════════════════════════════');
|
||||
client.logger.info(`✓ Logged in as: ${client.user?.tag}`);
|
||||
client.logger.info(` ├─ User ID: ${client.user?.id}`);
|
||||
client.logger.info(` ├─ Guilds: ${client.guilds.cache.size}`);
|
||||
client.logger.info(` └─ Users: ${client.users.cache.size} cached`);
|
||||
|
||||
// Register slash commands
|
||||
await registerCommands(client);
|
||||
|
||||
console.log('');
|
||||
client.logger.info('═══════════════════════════════════════════════════════');
|
||||
client.logger.info(' ELLY IS NOW ONLINE ');
|
||||
client.logger.info('═══════════════════════════════════════════════════════');
|
||||
console.log('');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Register slash commands with Discord
|
||||
*/
|
||||
async function registerCommands(client: EllyClient): Promise<void> {
|
||||
const token = Deno.env.get('DISCORD_TOKEN');
|
||||
if (!token || !client.user) {
|
||||
client.logger.error('✗ Cannot register commands: missing token or client user');
|
||||
return;
|
||||
}
|
||||
|
||||
const rest = new REST({ version: '10' }).setToken(token);
|
||||
const commands = client.commands.map((cmd) => cmd.data.toJSON());
|
||||
|
||||
console.log('');
|
||||
client.logger.info('───────────────────────────────────────────────────────');
|
||||
client.logger.info(' SLASH COMMAND REGISTRATION ');
|
||||
client.logger.info('───────────────────────────────────────────────────────');
|
||||
|
||||
try {
|
||||
client.logger.info(`Preparing to sync ${commands.length} slash commands...`);
|
||||
|
||||
// List all commands being registered
|
||||
const commandsByCategory: Record<string, string[]> = {};
|
||||
for (const cmd of client.commands.values()) {
|
||||
const category = cmd.data.name.includes('bedwars') || cmd.data.name.includes('skywars') || cmd.data.name === 'guild'
|
||||
? 'Statistics'
|
||||
: cmd.data.name.includes('marry') || cmd.data.name.includes('divorce') || cmd.data.name === 'relationship'
|
||||
? 'Family'
|
||||
: cmd.data.name === 'remind' || cmd.data.name === 'away'
|
||||
? 'Utility'
|
||||
: cmd.data.name === 'purge'
|
||||
? 'Moderation'
|
||||
: 'Developer';
|
||||
|
||||
if (!commandsByCategory[category]) {
|
||||
commandsByCategory[category] = [];
|
||||
}
|
||||
commandsByCategory[category].push(`/${cmd.data.name}`);
|
||||
}
|
||||
|
||||
for (const [category, cmds] of Object.entries(commandsByCategory)) {
|
||||
client.logger.info(` 📁 ${category}: ${cmds.join(', ')}`);
|
||||
}
|
||||
|
||||
// Register to specific guild for faster updates during development
|
||||
if (client.config.guild.id) {
|
||||
client.logger.info('');
|
||||
client.logger.info(`Syncing to guild: ${client.config.guild.name}`);
|
||||
client.logger.info(` ├─ Guild ID: ${client.config.guild.id}`);
|
||||
client.logger.info(` ├─ Mode: Guild-specific (instant updates)`);
|
||||
client.logger.info(` └─ Syncing...`);
|
||||
|
||||
const startTime = Date.now();
|
||||
await rest.put(Routes.applicationGuildCommands(client.user.id, client.config.guild.id), {
|
||||
body: commands,
|
||||
});
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
client.logger.info('');
|
||||
client.logger.info(`✓ Successfully synced ${commands.length} commands to guild!`);
|
||||
client.logger.info(` ├─ Time: ${elapsed}ms`);
|
||||
client.logger.info(` ├─ Guild: ${client.config.guild.name}`);
|
||||
client.logger.info(` └─ Commands are available immediately`);
|
||||
} else {
|
||||
client.logger.info('');
|
||||
client.logger.info('Syncing globally (no guild ID configured)');
|
||||
client.logger.info(' ├─ Mode: Global (may take up to 1 hour)');
|
||||
client.logger.info(' └─ Syncing...');
|
||||
|
||||
const startTime = Date.now();
|
||||
await rest.put(Routes.applicationCommands(client.user.id), {
|
||||
body: commands,
|
||||
});
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
client.logger.info('');
|
||||
client.logger.info(`✓ Successfully synced ${commands.length} commands globally!`);
|
||||
client.logger.info(` ├─ Time: ${elapsed}ms`);
|
||||
client.logger.info(` └─ Note: Global commands may take up to 1 hour to appear`);
|
||||
}
|
||||
} catch (error) {
|
||||
client.logger.error('✗ Failed to register commands');
|
||||
client.logger.error(` └─ Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user