p-queue
This commit is contained in:
@@ -25,8 +25,8 @@ DISCORD_ESCALATED_CATEGORY_ID= # Fallback escalation category (Discord)
|
|||||||
EMAIL_ESCALATED_CATEGORY_ID= # Fallback escalation category (email); legacy alias: ESCALATED_CATEGORY_ID
|
EMAIL_ESCALATED_CATEGORY_ID= # Fallback escalation category (email); legacy alias: ESCALATED_CATEGORY_ID
|
||||||
DISCORD_ESCALATED2_CHANNEL_ID= # Tier 2 escalation category/channel (Discord)
|
DISCORD_ESCALATED2_CHANNEL_ID= # Tier 2 escalation category/channel (Discord)
|
||||||
DISCORD_ESCALATED3_CHANNEL_ID= # Tier 3 escalation category/channel (Discord)
|
DISCORD_ESCALATED3_CHANNEL_ID= # Tier 3 escalation category/channel (Discord)
|
||||||
EMAIL_ESCALATED2_CHANNEL_ID= # Tier 2 escalation category/channel (email)
|
EMAIL_ESCALATED2_CHANNEL_ID= # Tier 2 escalation category ID (email tickets; env name says CHANNEL for legacy reasons)
|
||||||
EMAIL_ESCALATED3_CHANNEL_ID= # Tier 3 escalation category/channel (email)
|
EMAIL_ESCALATED3_CHANNEL_ID= # Tier 3 escalation category ID (email tickets; same naming note as tier 2)
|
||||||
|
|
||||||
# Logging, transcripts, and utility
|
# Logging, transcripts, and utility
|
||||||
ROLE_ID_TO_PING= # Role ID to ping on new tickets (config also accepts ROLE_TO_PING_ID as alias)
|
ROLE_ID_TO_PING= # Role ID to ping on new tickets (config also accepts ROLE_TO_PING_ID as alias)
|
||||||
|
|||||||
@@ -2,29 +2,34 @@
|
|||||||
# Broccolini Bot – Test environment template (no secrets)
|
# Broccolini Bot – Test environment template (no secrets)
|
||||||
# Copy to .env.test and fill with TEST-only values. Run with ENV_FILE=.env.test
|
# Copy to .env.test and fill with TEST-only values. Run with ENV_FILE=.env.test
|
||||||
# so changes are tried here first, then migrated to .env after confirmation.
|
# so changes are tried here first, then migrated to .env after confirmation.
|
||||||
# See ENV_AND_SECURITY.md. Never commit .env or .env.test.
|
# See docs/setup/ENV_AND_SECURITY.md. Never commit .env or .env.test.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# --- Discord: Core (use a test guild / bot if possible) ---
|
# --- Discord: Core (use a test guild / bot if possible) ---
|
||||||
DISCORD_TOKEN=
|
DISCORD_TOKEN= # Bot token (test bot)
|
||||||
DISCORD_APPLICATION_ID=
|
DISCORD_APPLICATION_ID= # Application (client) ID
|
||||||
DISCORD_GUILD_ID=
|
DISCORD_GUILD_ID= # Test server ID
|
||||||
|
|
||||||
# --- Discord: Channel & category IDs (test server) ---
|
# --- Discord: Channel & category IDs (test server) ---
|
||||||
|
# Ticket creation: set one or both; /panel and /email-routing choose behavior
|
||||||
DISCORD_TICKET_CATEGORY_ID=
|
DISCORD_TICKET_CATEGORY_ID=
|
||||||
TICKET_CATEGORY_ID=
|
TICKET_CATEGORY_ID=
|
||||||
DISCORD_THREAD_CHANNEL_ID=
|
# DISCORD_THREAD_CHANNEL_ID= # Text channel for Discord ticket threads (optional)
|
||||||
EMAIL_THREAD_CHANNEL_ID=
|
# EMAIL_THREAD_CHANNEL_ID= # Text channel for email ticket threads (optional)
|
||||||
|
|
||||||
# --- Escalation (optional for test) ---
|
# Overflow categories when main hits 50 channels (comma-separated, optional)
|
||||||
DISCORD_ESCALATED_CATEGORY_ID=
|
# EMAIL_TICKET_OVERFLOW_CATEGORY_IDS=
|
||||||
EMAIL_ESCALATED_CATEGORY_ID=
|
# DISCORD_TICKET_OVERFLOW_CATEGORY_IDS=
|
||||||
|
|
||||||
|
# Escalation (optional for test)
|
||||||
|
# DISCORD_ESCALATED_CATEGORY_ID=
|
||||||
|
# EMAIL_ESCALATED_CATEGORY_ID= # legacy alias: ESCALATED_CATEGORY_ID
|
||||||
DISCORD_ESCALATED2_CHANNEL_ID=
|
DISCORD_ESCALATED2_CHANNEL_ID=
|
||||||
DISCORD_ESCALATED3_CHANNEL_ID=
|
DISCORD_ESCALATED3_CHANNEL_ID=
|
||||||
EMAIL_ESCALATED2_CHANNEL_ID=
|
EMAIL_ESCALATED2_CHANNEL_ID= # Tier 2 category ID (email); env name *_CHANNEL_* is legacy
|
||||||
EMAIL_ESCALATED3_CHANNEL_ID=
|
EMAIL_ESCALATED3_CHANNEL_ID=
|
||||||
|
|
||||||
# --- Logging & utility ---
|
# --- Logging, transcripts, and utility ---
|
||||||
ROLE_ID_TO_PING=
|
ROLE_ID_TO_PING=
|
||||||
TRANSCRIPT_CHANNEL_ID=
|
TRANSCRIPT_CHANNEL_ID=
|
||||||
LOGGING_CHANNEL_ID=
|
LOGGING_CHANNEL_ID=
|
||||||
@@ -33,7 +38,7 @@ BACKUP_EXPORT_CHANNEL_ID=
|
|||||||
ACCOUNT_INFO_CHANNEL_ID=
|
ACCOUNT_INFO_CHANNEL_ID=
|
||||||
DISCORD_CHANNEL_ID=
|
DISCORD_CHANNEL_ID=
|
||||||
|
|
||||||
# --- Buttons / copy ---
|
# --- Discord: Ticket copy & buttons ---
|
||||||
ESCALATION_MESSAGE=
|
ESCALATION_MESSAGE=
|
||||||
BUTTON_LABEL_CLOSE=Close Ticket
|
BUTTON_LABEL_CLOSE=Close Ticket
|
||||||
BUTTON_LABEL_CLAIM=Claim
|
BUTTON_LABEL_CLAIM=Claim
|
||||||
@@ -42,21 +47,27 @@ BUTTON_EMOJI_CLOSE=🔒
|
|||||||
BUTTON_EMOJI_CLAIM=📌
|
BUTTON_EMOJI_CLAIM=📌
|
||||||
BUTTON_EMOJI_UNCLAIM=🔓
|
BUTTON_EMOJI_UNCLAIM=🔓
|
||||||
|
|
||||||
# --- Google / Gmail (test inbox or same as prod – your choice) ---
|
# --- Google / Gmail (test inbox / separate OAuth client optional) ---
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
REFRESH_TOKEN=
|
REFRESH_TOKEN=
|
||||||
MY_EMAIL=
|
MY_EMAIL=
|
||||||
|
|
||||||
# --- Server ---
|
# --- Server & URLs ---
|
||||||
NGROK_URL=
|
# NGROK_URL= # Optional; public URL if you use ngrok for webhooks
|
||||||
DISCORD_ONLY_PORT=5001
|
DISCORD_ONLY_PORT=5000
|
||||||
|
# HEALTHCHECK_HOST=
|
||||||
|
|
||||||
# --- Database (use a separate test DB or db name to avoid data loss) ---
|
# --- bOSScord (support cockpit) ---
|
||||||
|
# BOSSCORD_API_KEY=
|
||||||
|
# BOSSCORD_CORS_ORIGIN=*
|
||||||
|
|
||||||
|
# --- Database (test cluster or local) ---
|
||||||
MONGODB_URI=
|
MONGODB_URI=
|
||||||
|
# MONGODB_DATABASE=
|
||||||
|
|
||||||
# --- Branding & copy ---
|
# --- Branding & copy ---
|
||||||
SUPPORT_NAME=Support (Test)
|
SUPPORT_NAME=Support
|
||||||
LOGO_URL=
|
LOGO_URL=
|
||||||
EMAIL_SIGNATURE=
|
EMAIL_SIGNATURE=
|
||||||
TICKET_CLOSE_SUBJECT_PREFIX=[Resolved]
|
TICKET_CLOSE_SUBJECT_PREFIX=[Resolved]
|
||||||
@@ -66,7 +77,7 @@ DISCORD_CLOSE_MESSAGE=
|
|||||||
DISCORD_TRANSCRIPT_MESSAGE=
|
DISCORD_TRANSCRIPT_MESSAGE=
|
||||||
DISCORD_AUTO_CLOSE_MESSAGE=
|
DISCORD_AUTO_CLOSE_MESSAGE=
|
||||||
|
|
||||||
# --- Limits & permissions ---
|
# --- Ticket limits & permissions ---
|
||||||
GLOBAL_TICKET_LIMIT=5
|
GLOBAL_TICKET_LIMIT=5
|
||||||
TICKET_LIMIT_PER_CATEGORY=3
|
TICKET_LIMIT_PER_CATEGORY=3
|
||||||
RATE_LIMIT_TICKETS_PER_USER=0
|
RATE_LIMIT_TICKETS_PER_USER=0
|
||||||
@@ -74,10 +85,12 @@ RATE_LIMIT_WINDOW_MINUTES=60
|
|||||||
BLACKLISTED_ROLES=
|
BLACKLISTED_ROLES=
|
||||||
ADDITIONAL_STAFF_ROLES=
|
ADDITIONAL_STAFF_ROLES=
|
||||||
|
|
||||||
# --- Auto-close / reminders ---
|
# --- Auto-close ---
|
||||||
AUTO_CLOSE_ENABLED=false
|
AUTO_CLOSE_ENABLED=false
|
||||||
AUTO_CLOSE_AFTER_HOURS=72
|
AUTO_CLOSE_AFTER_HOURS=72
|
||||||
AUTO_CLOSE_MESSAGE=
|
AUTO_CLOSE_MESSAGE=
|
||||||
|
|
||||||
|
# --- Reminders ---
|
||||||
REMINDER_ENABLED=false
|
REMINDER_ENABLED=false
|
||||||
REMINDER_AFTER_HOURS=24
|
REMINDER_AFTER_HOURS=24
|
||||||
REMINDER_MESSAGE=
|
REMINDER_MESSAGE=
|
||||||
@@ -99,16 +112,16 @@ AUTO_UNCLAIM_ENABLED=false
|
|||||||
AUTO_UNCLAIM_AFTER_HOURS=24
|
AUTO_UNCLAIM_AFTER_HOURS=24
|
||||||
ALLOW_CLAIM_OVERWRITE=false
|
ALLOW_CLAIM_OVERWRITE=false
|
||||||
|
|
||||||
# --- Thread (legacy) ---
|
# --- Thread-style tickets (legacy) ---
|
||||||
USE_THREADS=false
|
USE_THREADS=false
|
||||||
THREAD_PARENT_CHANNEL=
|
THREAD_PARENT_CHANNEL=
|
||||||
|
|
||||||
# --- Game list ---
|
# --- Embed colors (hex with 0x prefix) ---
|
||||||
GAME_LIST=Project Zomboid, Minecraft, ...
|
|
||||||
|
|
||||||
# --- Embed colors ---
|
|
||||||
EMBED_COLOR_OPEN=0x00FF00
|
EMBED_COLOR_OPEN=0x00FF00
|
||||||
EMBED_COLOR_CLOSED=0xFF0000
|
EMBED_COLOR_CLOSED=0xFF0000
|
||||||
EMBED_COLOR_CLAIMED=0xFFFF00
|
EMBED_COLOR_CLAIMED=0xFFFF00
|
||||||
EMBED_COLOR_ESCALATED=0xFF6600
|
EMBED_COLOR_ESCALATED=0xFF6600
|
||||||
EMBED_COLOR_INFO=0x1e2124
|
EMBED_COLOR_INFO=0x1e2124
|
||||||
|
|
||||||
|
# --- Game list (comma-separated; used for detection and tags) ---
|
||||||
|
GAME_LIST=Project Zomboid, Minecraft
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const CONFIG = {
|
|||||||
EMAIL_THREAD_CHANNEL_ID: process.env.EMAIL_THREAD_CHANNEL_ID || null,
|
EMAIL_THREAD_CHANNEL_ID: process.env.EMAIL_THREAD_CHANNEL_ID || null,
|
||||||
EMAIL_ESCALATED_CATEGORY_ID: process.env.EMAIL_ESCALATED_CATEGORY_ID || process.env.ESCALATED_CATEGORY_ID,
|
EMAIL_ESCALATED_CATEGORY_ID: process.env.EMAIL_ESCALATED_CATEGORY_ID || process.env.ESCALATED_CATEGORY_ID,
|
||||||
DISCORD_ESCALATED_CATEGORY_ID: process.env.DISCORD_ESCALATED_CATEGORY_ID,
|
DISCORD_ESCALATED_CATEGORY_ID: process.env.DISCORD_ESCALATED_CATEGORY_ID,
|
||||||
|
// Tier 2/3 email escalation: category IDs where ticket channels are placed (env uses *_CHANNEL_* for legacy naming).
|
||||||
EMAIL_ESCALATED2_CHANNEL_ID: process.env.EMAIL_ESCALATED2_CHANNEL_ID || null,
|
EMAIL_ESCALATED2_CHANNEL_ID: process.env.EMAIL_ESCALATED2_CHANNEL_ID || null,
|
||||||
DISCORD_ESCALATED2_CHANNEL_ID: process.env.DISCORD_ESCALATED2_CHANNEL_ID || null,
|
DISCORD_ESCALATED2_CHANNEL_ID: process.env.DISCORD_ESCALATED2_CHANNEL_ID || null,
|
||||||
EMAIL_ESCALATED3_CHANNEL_ID: process.env.EMAIL_ESCALATED3_CHANNEL_ID || null,
|
EMAIL_ESCALATED3_CHANNEL_ID: process.env.EMAIL_ESCALATED3_CHANNEL_ID || null,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const { canRename, makeTicketName, minutesFromMs, pickTicketCategoryId, createDi
|
|||||||
const { sendTicketClosedEmail } = require('../services/gmail');
|
const { sendTicketClosedEmail } = require('../services/gmail');
|
||||||
const { getTicketActionRow } = require('../utils/ticketComponents');
|
const { getTicketActionRow } = require('../utils/ticketComponents');
|
||||||
const { setEmailRouting } = require('../services/guildSettings');
|
const { setEmailRouting } = require('../services/guildSettings');
|
||||||
|
const { enqueueRename } = require('../services/channelQueue');
|
||||||
const { runEscalation, runDeescalation } = require('./commands');
|
const { runEscalation, runDeescalation } = require('./commands');
|
||||||
const { trackInteraction, trackError } = require('./analytics');
|
const { trackInteraction, trackError } = require('./analytics');
|
||||||
|
|
||||||
@@ -297,7 +298,7 @@ async function handleClaim(interaction, ticket) {
|
|||||||
guild
|
guild
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await interaction.channel.setName(newName);
|
await enqueueRename(interaction.channel, newName);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Rename error (claim):', e);
|
console.error('Rename error (claim):', e);
|
||||||
}
|
}
|
||||||
@@ -333,8 +334,10 @@ async function handleClaim(interaction, ticket) {
|
|||||||
.replace(/\{staff_mention\}/g, interaction.user.toString())
|
.replace(/\{staff_mention\}/g, interaction.user.toString())
|
||||||
.replace(/\{staff_name\}/g, interaction.member?.displayName || interaction.user.username);
|
.replace(/\{staff_name\}/g, interaction.member?.displayName || interaction.user.username);
|
||||||
const claimEmbed = new EmbedBuilder()
|
const claimEmbed = new EmbedBuilder()
|
||||||
|
.setTitle('✅ Ticket Claimed')
|
||||||
.setDescription(claimText)
|
.setDescription(claimText)
|
||||||
.setColor(CONFIG.EMBED_COLOR_INFO);
|
.setColor(CONFIG.EMBED_COLOR_CLAIMED)
|
||||||
|
.setFooter({ text: `Claimed by ${claimerLabel}` });
|
||||||
await interaction.followUp({ embeds: [claimEmbed] });
|
await interaction.followUp({ embeds: [claimEmbed] });
|
||||||
} else {
|
} else {
|
||||||
// Unclaim
|
// Unclaim
|
||||||
@@ -352,7 +355,7 @@ async function handleClaim(interaction, ticket) {
|
|||||||
guild
|
guild
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await interaction.channel.setName(newName);
|
await enqueueRename(interaction.channel, newName);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Rename error (unclaim):', e);
|
console.error('Rename error (unclaim):', e);
|
||||||
}
|
}
|
||||||
@@ -383,8 +386,10 @@ async function handleClaim(interaction, ticket) {
|
|||||||
.replace(/\{staff_mention\}/g, interaction.user.toString())
|
.replace(/\{staff_mention\}/g, interaction.user.toString())
|
||||||
.replace(/\{staff_name\}/g, interaction.member?.displayName || interaction.user.username);
|
.replace(/\{staff_name\}/g, interaction.member?.displayName || interaction.user.username);
|
||||||
const unclaimEmbed = new EmbedBuilder()
|
const unclaimEmbed = new EmbedBuilder()
|
||||||
|
.setTitle('🔓 Ticket Unclaimed')
|
||||||
.setDescription(unclaimText)
|
.setDescription(unclaimText)
|
||||||
.setColor(CONFIG.EMBED_COLOR_INFO);
|
.setColor(0x808080)
|
||||||
|
.setFooter({ text: `Unclaimed by ${claimerLabel}` });
|
||||||
await interaction.followUp({ embeds: [unclaimEmbed] });
|
await interaction.followUp({ embeds: [unclaimEmbed] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ const {
|
|||||||
} = require('discord.js');
|
} = require('discord.js');
|
||||||
const { mongoose } = require('../db-connection');
|
const { mongoose } = require('../db-connection');
|
||||||
const { CONFIG, TICKET_TAGS } = require('../config');
|
const { CONFIG, TICKET_TAGS } = require('../config');
|
||||||
const { getPriorityEmoji, replaceVariables, escapeRegex } = require('../utils');
|
const { getPriorityEmoji, getPriorityColor, replaceVariables, escapeRegex } = require('../utils');
|
||||||
const { canRename, makeTicketName, pickTicketCategoryId, createDiscordTicketAsThread, checkTicketCreationRateLimit } = require('../services/tickets');
|
const { canRename, makeTicketName, pickTicketCategoryId, createDiscordTicketAsThread, checkTicketCreationRateLimit } = require('../services/tickets');
|
||||||
const { sendTicketNotificationEmail } = require('../services/gmail');
|
const { sendTicketNotificationEmail } = require('../services/gmail');
|
||||||
const { getTicketActionRow } = require('../utils/ticketComponents');
|
const { getTicketActionRow } = require('../utils/ticketComponents');
|
||||||
const { getEmailRouting } = require('../services/guildSettings');
|
const { getEmailRouting } = require('../services/guildSettings');
|
||||||
|
const { enqueueRename, enqueueMove } = require('../services/channelQueue');
|
||||||
const { trackInteraction, trackError, getAnalyticsSummary } = require('./analytics');
|
const { trackInteraction, trackError, getAnalyticsSummary } = require('./analytics');
|
||||||
const { handleAccountInfoCommand } = require('./accountinfo');
|
const { handleAccountInfoCommand } = require('./accountinfo');
|
||||||
const { handleSetupCommand } = require('./setup');
|
const { handleSetupCommand } = require('./setup');
|
||||||
@@ -80,7 +81,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
|
|||||||
interaction.guild
|
interaction.guild
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await interaction.channel.setName(newName);
|
await enqueueRename(interaction.channel, newName);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Rename error (escalate):', e);
|
console.error('Rename error (escalate):', e);
|
||||||
}
|
}
|
||||||
@@ -93,7 +94,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!interaction.channel.isThread() && categoryId) {
|
if (!interaction.channel.isThread() && categoryId) {
|
||||||
await interaction.channel.setParent(categoryId, { lockPermissions: true });
|
await enqueueMove(interaction.channel, categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingEmbed = new EmbedBuilder()
|
const pendingEmbed = new EmbedBuilder()
|
||||||
@@ -117,8 +118,10 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
|
|||||||
.replace(/\{support_name\}/g, CONFIG.SUPPORT_NAME)
|
.replace(/\{support_name\}/g, CONFIG.SUPPORT_NAME)
|
||||||
+ (reason ? `\n\n**Reason:** ${reason}` : '');
|
+ (reason ? `\n\n**Reason:** ${reason}` : '');
|
||||||
const escalatedEmbed = new EmbedBuilder()
|
const escalatedEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(`🚨 Escalated to ${nextTier === 1 ? 'Tier 2' : 'Tier 3'} Support`)
|
||||||
.setDescription(escalationBody)
|
.setDescription(escalationBody)
|
||||||
.setColor(CONFIG.EMBED_COLOR_INFO);
|
.setColor(CONFIG.EMBED_COLOR_ESCALATED)
|
||||||
|
.setFooter({ text: `Escalated by ${interaction.member?.displayName || interaction.user.username}` });
|
||||||
const updatedTicketForRow = { ...ticket, escalationTier: nextTier, escalated: true };
|
const updatedTicketForRow = { ...ticket, escalationTier: nextTier, escalated: true };
|
||||||
const escalationRow = getTicketActionRow(updatedTicketForRow);
|
const escalationRow = getTicketActionRow(updatedTicketForRow);
|
||||||
await interaction.channel.send({
|
await interaction.channel.send({
|
||||||
@@ -193,7 +196,7 @@ async function runDeescalation(interaction, ticket) {
|
|||||||
interaction.guild
|
interaction.guild
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await interaction.channel.setName(newName);
|
await enqueueRename(interaction.channel, newName);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Rename error (deescalate):', e);
|
console.error('Rename error (deescalate):', e);
|
||||||
}
|
}
|
||||||
@@ -206,12 +209,16 @@ async function runDeescalation(interaction, ticket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!interaction.channel.isThread() && categoryId) {
|
if (!interaction.channel.isThread() && categoryId) {
|
||||||
await interaction.channel.setParent(categoryId, { lockPermissions: true });
|
await enqueueMove(interaction.channel, categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tierLabel = newTier === 0 ? 'normal' : newTier === 1 ? 'tier 2' : 'tier 3';
|
const tierLabel = newTier === 0 ? 'normal' : newTier === 1 ? 'tier 2' : 'tier 3';
|
||||||
|
const deescalateEmbed = new EmbedBuilder()
|
||||||
|
.setColor(0x00BFFF)
|
||||||
|
.setTitle(`✅ De-escalated to ${tierLabel} Support`)
|
||||||
|
.setFooter({ text: interaction.member?.displayName || interaction.user.username });
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: `Ticket de‑escalated to **${tierLabel}** support.`,
|
embeds: [deescalateEmbed],
|
||||||
ephemeral: true
|
ephemeral: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -737,7 +744,16 @@ async function handleCommand(interaction) {
|
|||||||
{ $set: { priority: level } }
|
{ $set: { priority: level } }
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.reply(channelMessage);
|
const priorityTitle =
|
||||||
|
newIdx === oldIdx
|
||||||
|
? 'Priority Set'
|
||||||
|
: `Priority ${newIdx > oldIdx ? 'Upgraded' : 'Downgraded'} → ${levelLabel}`;
|
||||||
|
const priorityEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(priorityTitle)
|
||||||
|
.setDescription(channelMessage)
|
||||||
|
.setColor(getPriorityColor(level))
|
||||||
|
.setFooter({ text: interaction.member?.displayName || interaction.user.username });
|
||||||
|
await interaction.reply({ embeds: [priorityEmbed] });
|
||||||
|
|
||||||
if (level === 'high' && ticket.gmailThreadId && !ticket.gmailThreadId.startsWith('discord-')) {
|
if (level === 'high' && ticket.gmailThreadId && !ticket.gmailThreadId.startsWith('discord-')) {
|
||||||
await sendTicketNotificationEmail(
|
await sendTicketNotificationEmail(
|
||||||
@@ -1139,11 +1155,4 @@ async function handleAutocomplete(interaction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = { handleCommand, handleContextMenu, handleAutocomplete, runEscalation, runDeescalation };
|
||||||
handleCommand,
|
|
||||||
handleContextMenu,
|
|
||||||
handleAutocomplete,
|
|
||||||
runEscalation,
|
|
||||||
runDeescalation,
|
|
||||||
hasStaffRole
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,91 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Discord messageCreate handler – prefix commands and forwards staff replies to Gmail.
|
* Discord messageCreate handler – forwards staff replies to Gmail.
|
||||||
*/
|
*/
|
||||||
const { ChannelType, AttachmentBuilder } = require('discord.js');
|
|
||||||
const { mongoose } = require('../db-connection');
|
const { mongoose } = require('../db-connection');
|
||||||
const { CONFIG } = require('../config');
|
const { CONFIG } = require('../config');
|
||||||
const { extractRawEmail } = require('../utils');
|
const { extractRawEmail } = require('../utils');
|
||||||
const { getGmailClient, sendGmailReply } = require('../services/gmail');
|
const { getGmailClient, sendGmailReply } = require('../services/gmail');
|
||||||
const { updateTicketActivity } = require('../services/tickets');
|
const { updateTicketActivity } = require('../services/tickets');
|
||||||
const { hasStaffRole } = require('./commands');
|
|
||||||
const { trackInteraction, trackError } = require('./analytics');
|
|
||||||
|
|
||||||
const Ticket = mongoose.model('Ticket');
|
const Ticket = mongoose.model('Ticket');
|
||||||
|
|
||||||
/**
|
|
||||||
* `!ids` — list channel and role IDs (same rules as slash staff-gated commands).
|
|
||||||
* @returns {Promise<boolean>} true if the message was consumed
|
|
||||||
*/
|
|
||||||
async function tryHandleIdsCommand(m) {
|
|
||||||
if (m.content?.trim().toLowerCase() !== '!ids') return false;
|
|
||||||
|
|
||||||
if (!m.guild) {
|
|
||||||
await m.reply('This command can only be used in a server.').catch(() => {});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const staffConfigured =
|
|
||||||
CONFIG.ROLE_ID_TO_PING || (CONFIG.ADDITIONAL_STAFF_ROLES && CONFIG.ADDITIONAL_STAFF_ROLES.length > 0);
|
|
||||||
if (staffConfigured && !hasStaffRole(m.member)) {
|
|
||||||
const roleMention = CONFIG.ROLE_ID_TO_PING ? `<@&${CONFIG.ROLE_ID_TO_PING}>` : 'support';
|
|
||||||
await m.reply({
|
|
||||||
content: `This command is only available to the support team (${roleMention}).`,
|
|
||||||
allowedMentions: { users: [], roles: [], repliedUser: false }
|
|
||||||
}).catch(() => {});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
trackInteraction('commands', 'ids-prefix', m.author.tag);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await m.guild.channels.fetch().catch(() => {});
|
|
||||||
|
|
||||||
const channelTypeName = type =>
|
|
||||||
Object.entries(ChannelType).find(([, v]) => v === type)?.[0] ?? String(type);
|
|
||||||
|
|
||||||
const lines = ['**Channels:**'];
|
|
||||||
const channels = [...m.guild.channels.cache.values()].sort((a, b) => a.rawPosition - b.rawPosition);
|
|
||||||
for (const ch of channels) {
|
|
||||||
lines.push(`\`${ch.id}\` — ${ch.name} (${channelTypeName(ch.type)})`);
|
|
||||||
}
|
|
||||||
lines.push('');
|
|
||||||
lines.push('**Roles:**');
|
|
||||||
const roles = [...m.guild.roles.cache.values()].sort((a, b) => b.position - a.position);
|
|
||||||
for (const role of roles) {
|
|
||||||
lines.push(`\`${role.id}\` — ${role.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = lines.join('\n');
|
|
||||||
const baseReply = { allowedMentions: { users: [], roles: [], repliedUser: false } };
|
|
||||||
if (text.length <= 1900) {
|
|
||||||
await m.reply({ content: text, ...baseReply }).catch(() => {});
|
|
||||||
} else {
|
|
||||||
const buf = Buffer.from(text, 'utf8');
|
|
||||||
await m.reply({
|
|
||||||
content: 'List is long; sent as a file.',
|
|
||||||
files: [new AttachmentBuilder(buf, { name: `guild-ids-${m.guild.id}.txt` })],
|
|
||||||
...baseReply
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
trackError('ids-prefix-command', err, null);
|
|
||||||
await m.reply({
|
|
||||||
content: 'Failed to build ID list.',
|
|
||||||
allowedMentions: { repliedUser: false }
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a Discord message in a ticket channel → relay to Gmail (email tickets only).
|
* Handle a Discord message in a ticket channel → relay to Gmail (email tickets only).
|
||||||
*/
|
*/
|
||||||
async function handleDiscordReply(m) {
|
async function handleDiscordReply(m) {
|
||||||
if (m.author.bot || m.interaction) return;
|
if (m.author.bot || m.interaction) return;
|
||||||
|
|
||||||
if (await tryHandleIdsCommand(m)) return;
|
|
||||||
|
|
||||||
const ticket = await Ticket.findOne({ discordThreadId: m.channel.id }).lean();
|
const ticket = await Ticket.findOne({ discordThreadId: m.channel.id }).lean();
|
||||||
if (!ticket) return;
|
if (!ticket) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"p-queue": "^6.6.2",
|
||||||
"discord.js": "^14.25.1",
|
"discord.js": "^14.25.1",
|
||||||
"dotenv": "^17.2.4",
|
"dotenv": "^17.2.4",
|
||||||
"dotenv-expand": "^11.0.6",
|
"dotenv-expand": "^11.0.6",
|
||||||
|
|||||||
20
services/channelQueue.js
Normal file
20
services/channelQueue.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Serialized channel renames/moves to avoid Discord rate limits (e.g. 2 renames / 10 min per channel).
|
||||||
|
*/
|
||||||
|
const PQueue = require('p-queue').default;
|
||||||
|
|
||||||
|
const channelQueue = new PQueue({
|
||||||
|
concurrency: 1,
|
||||||
|
intervalCap: 2,
|
||||||
|
interval: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
function enqueueRename(channel, newName) {
|
||||||
|
return channelQueue.add(() => channel.setName(newName));
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueMove(channel, categoryId) {
|
||||||
|
return channelQueue.add(() => channel.setParent(categoryId, { lockPermissions: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { channelQueue, enqueueRename, enqueueMove };
|
||||||
@@ -2,20 +2,19 @@
|
|||||||
* Guild-specific settings (e.g. email ticket routing).
|
* Guild-specific settings (e.g. email ticket routing).
|
||||||
*/
|
*/
|
||||||
const { mongoose } = require('../db-connection');
|
const { mongoose } = require('../db-connection');
|
||||||
const { CONFIG } = require('../config');
|
|
||||||
|
|
||||||
const GuildSettings = mongoose.model('GuildSettings');
|
const GuildSettings = mongoose.model('GuildSettings');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get email ticket routing for a guild. Returns 'thread' or 'category'.
|
* Get email ticket routing for a guild. Returns 'thread' or 'category'.
|
||||||
* If not set, defaults from CONFIG: thread if EMAIL_THREAD_CHANNEL_ID is set, else category.
|
* If not set, defaults to 'category'.
|
||||||
* @param {string} guildId
|
* @param {string} guildId
|
||||||
* @returns {Promise<'thread'|'category'>}
|
* @returns {Promise<'thread'|'category'>}
|
||||||
*/
|
*/
|
||||||
async function getEmailRouting(guildId) {
|
async function getEmailRouting(guildId) {
|
||||||
const doc = await GuildSettings.findOne({ guildId }).select('emailRouting').lean();
|
const doc = await GuildSettings.findOne({ guildId }).select('emailRouting').lean();
|
||||||
if (doc && doc.emailRouting) return doc.emailRouting;
|
if (doc && doc.emailRouting) return doc.emailRouting;
|
||||||
return CONFIG.EMAIL_THREAD_CHANNEL_ID ? 'thread' : 'category';
|
return 'category';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user