This commit is contained in:
2026-04-20 18:05:36 +00:00
parent d73422555d
commit 33b1f276c6
26 changed files with 598 additions and 183 deletions

View File

@@ -440,6 +440,7 @@ async function handleCommand(interaction) {
}
try {
// TODO(queue-migrate): permissionOverwrites mutation bypasses channelQueue — could race a pending rename/send on the same channel.
await interaction.channel.permissionOverwrites.create(user.id, {
ViewChannel: true,
SendMessages: true,
@@ -462,6 +463,7 @@ async function handleCommand(interaction) {
}
try {
// TODO(queue-migrate): permissionOverwrites mutation bypasses channelQueue — could race a pending rename/send on the same channel.
await interaction.channel.permissionOverwrites.delete(user.id);
await interaction.reply({ content: `Removed ${user} from this ticket.`, allowedMentions: { parse: ['users'] } });
} catch (err) {
@@ -524,6 +526,7 @@ async function handleCommand(interaction) {
}
try {
// TODO(queue-migrate): setParent bypasses channelQueue (enqueueMove) — use enqueueMove so moves serialize with pending renames/sends.
await interaction.channel.setParent(category.id, { lockPermissions: true });
await interaction.reply(`Moved ticket to **${category.name}**.`);
@@ -711,6 +714,7 @@ async function handleCommand(interaction) {
}
try {
// TODO(queue-migrate): setTopic bypasses channelQueue — could race a pending rename/send on the same channel.
await interaction.channel.setTopic(text);
await interaction.reply('Topic updated successfully.');
} catch (err) {
@@ -1085,19 +1089,46 @@ async function handleCommand(interaction) {
return interaction.editReply('BACKUP_EXPORT_CHANNEL_ID is not set in .env.');
}
try {
const tickets = await Ticket.find().sort({ ticketNumber: 1 }).lean();
const lines = ['# Ticket backup ' + new Date().toISOString(), 'ticketNumber\tstatus\tsenderEmail\tsubject\tcreatedAt\tclaimedBy\tpriority\tescalationTier'];
for (const t of tickets) {
// Stream every ticket through a Mongoose cursor to a tmp file so peak RSS
// stays bounded regardless of collection size; attach the file, then unlink.
const fs = require('fs');
const os = require('os');
const path = require('path');
const tmpName = `ticket-backup-${Date.now()}-${process.pid}.txt`;
const tmpPath = path.join(os.tmpdir(), tmpName);
const ws = fs.createWriteStream(tmpPath, { encoding: 'utf8' });
ws.write('# Ticket backup ' + new Date().toISOString() + '\n');
ws.write('ticketNumber\tstatus\tsenderEmail\tsubject\tcreatedAt\tclaimedBy\tpriority\tescalationTier\n');
let count = 0;
const cursor = Ticket.find().sort({ ticketNumber: 1 }).lean().cursor();
for await (const t of cursor) {
const created = t.createdAt ? new Date(t.createdAt).toISOString() : '';
lines.push([t.ticketNumber, t.status || '', (t.senderEmail || '').replace(/\t/g, ' '), (t.subject || '').replace(/\t/g, ' ').slice(0, 200), created, (t.claimedBy || '').replace(/\t/g, ' '), t.priority || '', t.escalationTier ?? ''].join('\t'));
ws.write([
t.ticketNumber,
t.status || '',
(t.senderEmail || '').replace(/\t/g, ' '),
(t.subject || '').replace(/\t/g, ' ').slice(0, 200),
created,
(t.claimedBy || '').replace(/\t/g, ' '),
t.priority || '',
t.escalationTier ?? ''
].join('\t') + '\n');
count++;
}
await new Promise((resolve, reject) => ws.end(err => err ? reject(err) : resolve()));
try {
const channel = await interaction.client.channels.fetch(CONFIG.BACKUP_EXPORT_CHANNEL_ID);
await enqueueSend(channel, {
content: `Ticket backup by ${interaction.user.tag} (${count} tickets)`,
files: [new AttachmentBuilder(tmpPath, { name: tmpName })]
});
await interaction.editReply(`Backup complete. ${count} tickets sent to the backup channel.`);
} finally {
fs.promises.unlink(tmpPath).catch(() => {});
}
const buf = Buffer.from(lines.join('\n'), 'utf8');
const channel = await interaction.client.channels.fetch(CONFIG.BACKUP_EXPORT_CHANNEL_ID);
await enqueueSend(channel, {
content: `Ticket backup by ${interaction.user.tag} (${tickets.length} tickets)`,
files: [new AttachmentBuilder(buf, { name: `ticket-backup-${Date.now()}.txt` })]
});
await interaction.editReply(`Backup complete. ${tickets.length} tickets sent to the backup channel.`);
} catch (err) {
trackError('backup-command', err, interaction);
await interaction.editReply('Failed to create backup: ' + (err.message || err));