Gmail folder auto-advance + /forward command

- Reply-cycle auto-advance: staff reply files the thread to "Awaiting Reply",
  a customer response files it to "Needs Response" (new GMAIL_LABEL_AWAITING_REPLY
  / GMAIL_LABEL_NEEDS_RESPONSE labels + autoAdvanceFolder, which only moves
  threads still in the auto-cycle and leaves hand-filed folders alone)
- /forward: forward a ticket's email to another address (handlers/commands/forward.js
  + forward composition in services/gmail.js)
- Tests for the auto-advance cycle; label fixtures updated for the new labels
This commit is contained in:
2026-06-05 02:46:50 +00:00
parent 0fcffe8d33
commit 6bae3e79b1
10 changed files with 410 additions and 10 deletions

View File

@@ -0,0 +1,59 @@
/**
* /forward — forward this ticket's email thread to a third-party address.
*
* Builds a fresh outbound email to the target only; the original customer is
* never looped in (see services/gmail.js forwardThread).
*/
const { MessageFlags } = require('discord.js');
const { findTicketForChannel } = require('../sharedHelpers');
const { forwardThread } = require('../../services/gmail');
const { logError, logTicketEvent } = require('../../services/debugLog');
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
async function handleForward(interaction) {
const ticket = await findTicketForChannel(interaction);
if (!ticket) return;
// Discord-origin tickets have no Gmail thread to forward.
if (ticket.gmailThreadId.startsWith('discord-')) {
return interaction.reply({
content: "This ticket has no email thread, so there's nothing to forward.",
flags: MessageFlags.Ephemeral
});
}
const target = interaction.options.getString('email');
const note = interaction.options.getString('note') || '';
// Defer: fetching the thread + downloading attachments can exceed the 3s window.
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const { messageCount, attachmentCount, skipped } = await forwardThread(
ticket.gmailThreadId, target, note
);
logTicketEvent('Email thread forwarded', [
{ name: 'To', value: target },
{ name: 'Messages', value: String(messageCount) },
{ name: 'Forwarded by', value: interaction.user.tag }
], interaction).catch(() => {});
const skippedNote = skipped ? ` (${plural(skipped, 'attachment')} skipped — over the size limit)` : '';
return interaction.editReply({
content: `Forwarded ${plural(messageCount, 'message')} (${plural(attachmentCount, 'attachment')}) to **${target}**.${skippedNote}`
});
} catch (err) {
if (err.code === 'EBADRECIPIENT') {
return interaction.editReply({ content: "That doesn't look like a valid email address." });
}
if (err.code === 'EEMPTY') {
return interaction.editReply({ content: 'This thread has no messages to forward.' });
}
logError('handleForward', err, interaction).catch(() => {});
return interaction.editReply({ content: `Failed to forward: ${err.message}` });
}
}
module.exports = { handleForward };

View File

@@ -31,6 +31,7 @@ const { runEscalation, runDeescalation, handleEscalate, handleDeescalate, resolv
const { handleCloseTimer, handleCancelClose, handleForceClose } = require('./close');
const { handleResponse, handleAutocomplete } = require('./response');
const { handlePanel, handleSignature } = require('./panel');
const { handleForward } = require('./forward');
const { handleCreateTicketFromMessage, handleViewUserTickets } = require('./contextMenu');
const Ticket = mongoose.model('Ticket');
@@ -355,7 +356,7 @@ async function handleHelp(interaction) {
},
{
name: 'Ticket Management',
value: '`/transfer @staff [reason]` - Transfer ticket to another staff member\n`/move #category` - Move ticket to another category\n`/force-close` - Force close ticket without confirmation\n`/cancel-close` - Abort a pending force-close countdown\n`/topic <text>` - Set ticket topic/description\n`/folder <destination>` - File this ticket\'s email into a Gmail folder'
value: '`/transfer @staff [reason]` - Transfer ticket to another staff member\n`/move #category` - Move ticket to another category\n`/force-close` - Force close ticket without confirmation\n`/cancel-close` - Abort a pending force-close countdown\n`/topic <text>` - Set ticket topic/description\n`/folder <destination>` - File this ticket\'s email into a Gmail folder\n`/forward <email> [note]` - Forward this ticket\'s email thread to another address'
},
{
name: 'Saved Responses',
@@ -405,6 +406,7 @@ const COMMAND_HANDLERS = {
email: handleEmail,
folder: handleFolder,
closetimer: handleCloseTimer,
forward: handleForward,
'cancel-close': handleCancelClose,
'force-close': handleForceClose,
topic: handleTopic,