(Feat): Added a minimal pikanetwork client

This commit is contained in:
2025-12-01 13:08:01 +00:00
commit 101d093965
68 changed files with 18007 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
/**
* Application Admin Handlers (Export/Purge)
*/
import {
EmbedBuilder,
AttachmentBuilder,
type ChatInputCommandInteraction,
} from 'discord.js';
import type { EllyClient } from '../../../client/EllyClient.ts';
import { ApplicationRepository, type Application } from '../../../database/repositories/ApplicationRepository.ts';
import { PermissionLevel } from '../../../types/index.ts';
/**
* Handle exporting applications to CSV
*/
export async function handleExport(
interaction: ChatInputCommandInteraction,
client: EllyClient,
repo: ApplicationRepository
): Promise<void> {
const member = interaction.guild?.members.cache.get(interaction.user.id);
if (!member || !client.permissions.hasPermission(member, PermissionLevel.Admin)) {
await interaction.reply({
content: '❌ You need Admin permission to export applications.',
ephemeral: true,
});
return;
}
const statusFilter = interaction.options.getString('status') ?? 'all';
await interaction.deferReply({ ephemeral: true });
// Get applications
let applications: Application[];
if (statusFilter === 'all') {
applications = await repo.getAll();
} else {
applications = await repo.getByStatus(statusFilter as Application['status']);
}
if (applications.length === 0) {
await interaction.editReply({
content: '📭 No applications to export.',
});
return;
}
// Create CSV content
const headers = [
'ID',
'User ID',
'MC Username',
'Status',
'Discord Age',
'Timezone',
'Activity',
'Why Join',
'Experience',
'Reviewed By',
'Created At',
'Reviewed At',
];
const rows = applications.map((app) => [
app.id,
app.userId,
app.minecraftUsername,
app.status,
app.discordAge ?? '',
app.timezone ?? '',
app.activity ?? '',
`"${(app.whyJoin ?? '').replace(/"/g, '""')}"`,
`"${(app.experience ?? '').replace(/"/g, '""')}"`,
app.reviewedBy ?? '',
new Date(app.createdAt).toISOString(),
app.reviewedAt ? new Date(app.reviewedAt).toISOString() : '',
]);
const csv = [headers.join(','), ...rows.map((r) => r.join(','))].join('\n');
// Create attachment
const buffer = new TextEncoder().encode(csv);
const attachment = new AttachmentBuilder(buffer, {
name: `applications_${statusFilter}_${Date.now()}.csv`,
});
const embed = new EmbedBuilder()
.setColor(0x57f287)
.setTitle('📤 Applications Exported')
.addFields(
{ name: 'Filter', value: statusFilter === 'all' ? 'All' : statusFilter, inline: true },
{ name: 'Count', value: String(applications.length), inline: true },
{ name: 'Format', value: 'CSV', inline: true }
)
.setFooter({ text: `Exported by ${interaction.user.tag}` })
.setTimestamp();
await interaction.editReply({
embeds: [embed],
files: [attachment],
});
// Log export
const logChannel = client.channels_cache.applicationLogs;
if (logChannel) {
await logChannel.send({
embeds: [
new EmbedBuilder()
.setColor(0x3498db)
.setTitle('📤 Applications Exported')
.addFields(
{ name: 'Exported By', value: interaction.user.tag, inline: true },
{ name: 'Filter', value: statusFilter, inline: true },
{ name: 'Count', value: String(applications.length), inline: true }
)
.setTimestamp(),
],
});
}
}
/**
* Handle purging old applications
*/
export async function handlePurge(
interaction: ChatInputCommandInteraction,
client: EllyClient,
repo: ApplicationRepository
): Promise<void> {
const member = interaction.guild?.members.cache.get(interaction.user.id);
if (!member || !client.permissions.hasPermission(member, PermissionLevel.Admin)) {
await interaction.reply({
content: '❌ You need Admin permission to purge applications.',
ephemeral: true,
});
return;
}
const days = interaction.options.getInteger('days', true);
const statusFilter = interaction.options.getString('status');
await interaction.deferReply({ ephemeral: true });
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
// Get applications to purge
const allApplications = await repo.getAll();
let toPurge = allApplications.filter((a) => a.createdAt < cutoff);
// Apply status filter
if (statusFilter === 'denied') {
toPurge = toPurge.filter((a) => a.status === 'denied');
} else if (statusFilter === 'reviewed') {
toPurge = toPurge.filter((a) => a.status !== 'pending');
}
if (toPurge.length === 0) {
await interaction.editReply({
content: '📭 No applications match the purge criteria.',
});
return;
}
// Confirm purge
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = await import('discord.js');
const confirmEmbed = new EmbedBuilder()
.setColor(0xed4245)
.setTitle('⚠️ Confirm Purge')
.setDescription(
`You are about to permanently delete **${toPurge.length}** applications.\n\n` +
`**Criteria:**\n` +
`• Older than: ${days} days\n` +
`• Status filter: ${statusFilter ?? 'All reviewed'}\n\n` +
`This action cannot be undone!`
)
.setTimestamp();
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('purge:confirm')
.setLabel(`Delete ${toPurge.length} Applications`)
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId('purge:cancel')
.setLabel('Cancel')
.setStyle(ButtonStyle.Secondary)
);
const response = await interaction.editReply({
embeds: [confirmEmbed],
components: [row],
});
try {
const buttonInteraction = await response.awaitMessageComponent({
componentType: ComponentType.Button,
filter: (i) => i.user.id === interaction.user.id,
time: 30000,
});
if (buttonInteraction.customId === 'purge:cancel') {
await buttonInteraction.update({
embeds: [
new EmbedBuilder()
.setColor(0x3498db)
.setTitle('❌ Purge Cancelled')
.setDescription('No applications were deleted.')
.setTimestamp(),
],
components: [],
});
return;
}
// Perform purge
let deleted = 0;
for (const app of toPurge) {
const success = await repo.delete(app.id);
if (success) deleted++;
}
// Also delete associated notes
const notes = client.database.get<Array<{ appId: string }>>('application_notes') ?? [];
const purgedIds = new Set(toPurge.map((a) => a.id));
const remainingNotes = notes.filter((n) => !purgedIds.has(n.appId));
client.database.set('application_notes', remainingNotes);
const resultEmbed = new EmbedBuilder()
.setColor(0x57f287)
.setTitle('🗑️ Purge Complete')
.addFields(
{ name: 'Deleted', value: String(deleted), inline: true },
{ name: 'Notes Removed', value: String(notes.length - remainingNotes.length), inline: true },
{ name: 'Criteria', value: `Older than ${days} days`, inline: true }
)
.setFooter({ text: `Purged by ${interaction.user.tag}` })
.setTimestamp();
await buttonInteraction.update({
embeds: [resultEmbed],
components: [],
});
// Log purge
const logChannel = client.channels_cache.applicationLogs;
if (logChannel) {
await logChannel.send({
embeds: [
new EmbedBuilder()
.setColor(0xed4245)
.setTitle('🗑️ Applications Purged')
.addFields(
{ name: 'Purged By', value: interaction.user.tag, inline: true },
{ name: 'Count', value: String(deleted), inline: true },
{ name: 'Criteria', value: `Older than ${days} days, ${statusFilter ?? 'all reviewed'}`, inline: true }
)
.setTimestamp(),
],
});
}
} catch {
await interaction.editReply({
embeds: [
new EmbedBuilder()
.setColor(0xed4245)
.setTitle('⏰ Timed Out')
.setDescription('Purge cancelled due to timeout.')
.setTimestamp(),
],
components: [],
});
}
}