diff --git a/.env.example b/.env.example index 7378ebb..2f5035a 100644 --- a/.env.example +++ b/.env.example @@ -117,6 +117,8 @@ CLAIM_TIMEOUT_HOURS=48 AUTO_UNCLAIM_ENABLED=false AUTO_UNCLAIM_AFTER_HOURS=24 ALLOW_CLAIM_OVERWRITE=false +STAFF_EMOJIS=224692549225283584:🍅 # userId:emoji pairs, comma-separated +CLAIMER_EMOJI_FALLBACK=🎫 # fallback if claimer has no entry in STAFF_EMOJIS # --- Thread-style tickets (legacy) --- USE_THREADS=false diff --git a/config.js b/config.js index eacd3b2..f4822f2 100644 --- a/config.js +++ b/config.js @@ -134,6 +134,22 @@ const CONFIG = { } return map; })(), + STAFF_EMOJIS: (() => { + const raw = process.env.STAFF_EMOJIS; + const map = new Map(); + if (!raw || !String(raw).trim()) return map; + for (const part of String(raw).split(',')) { + const seg = part.trim(); + if (!seg) continue; + const idx = seg.indexOf(':'); + if (idx === -1) continue; + const userId = seg.slice(0, idx).trim(); + const emoji = seg.slice(idx + 1).trim(); + if (userId && emoji) map.set(userId, emoji); + } + return map; + })(), + CLAIMER_EMOJI_FALLBACK: process.env.CLAIMER_EMOJI_FALLBACK || '🎫', STAFF_T1_CATEGORY: process.env.STAFF_T1_CATEGORY || null, STAFF_T2_CATEGORY: process.env.STAFF_T2_CATEGORY || null, STAFF_T3_CATEGORY: process.env.STAFF_T3_CATEGORY || null, diff --git a/handlers/buttons.js b/handlers/buttons.js index 6bb4939..54d88c1 100644 --- a/handlers/buttons.js +++ b/handlers/buttons.js @@ -16,12 +16,11 @@ const { } = require('discord.js'); const { mongoose } = require('../db-connection'); const { CONFIG } = require('../config'); -const { canRename, makeTicketName, minutesFromMs, getOrCreateTicketCategory, cleanupEmptyOverflowCategory, createDiscordTicketAsThread, checkTicketCreationRateLimit } = require('../services/tickets'); +const { canRename, makeTicketName, minutesFromMs, getOrCreateTicketCategory, cleanupEmptyOverflowCategory, createDiscordTicketAsThread, checkTicketCreationRateLimit, getSenderLocal } = require('../services/tickets'); const { sendTicketClosedEmail } = require('../services/gmail'); const { getTicketActionRow } = require('../utils/ticketComponents'); const { setEmailRouting } = require('../services/guildSettings'); -const { enqueueRename, enqueueMove } = require('../services/channelQueue'); -const { createStaffChannel } = require('../services/staffChannel'); +const { enqueueRename } = require('../services/channelQueue'); const { runEscalation, runDeescalation } = require('./commands'); const { trackInteraction, trackError } = require('./analytics'); @@ -301,12 +300,30 @@ async function handleClaim(interaction, ticket) { freshTicket.claimedBy = claimerLabel; freshTicket.claimerId = interaction.user.id; + // Resolve claimerEmoji from STAFF_EMOJIS map (fallback to CLAIMER_EMOJI_FALLBACK) + const claimerEmoji = CONFIG.STAFF_EMOJIS.get(interaction.user.id) || CONFIG.CLAIMER_EMOJI_FALLBACK; + // Resolve creatorNickname: displayName for Discord tickets, senderLocal for email tickets + let creatorNickname; + if (freshTicket.gmailThreadId.startsWith('discord-')) { + const creatorUserId = freshTicket.gmailThreadId.split('-').pop(); + try { + const creatorMember = await guild.members.fetch(creatorUserId); + creatorNickname = creatorMember.displayName; + } catch { + creatorNickname = freshTicket.senderEmail; + } + } else { + creatorNickname = getSenderLocal(freshTicket.senderEmail); + } + const renameInfo = await canRename(freshTicket); if (renameInfo.ok) { const newName = makeTicketName( { escalated: !!freshTicket.escalated, claimed: true }, freshTicket, - guild + guild, + claimerEmoji, + creatorNickname ); try { await enqueueRename(interaction.channel, newName); @@ -321,25 +338,6 @@ async function handleClaim(interaction, ticket) { ); } - try { - const staffCategoryId = CONFIG.STAFF_CATEGORIES.get(interaction.user.id); - if (staffCategoryId) await enqueueMove(interaction.channel, staffCategoryId); - const staffChan = await createStaffChannel( - interaction.guild, - freshTicket, - interaction.user.id, - interaction.channel.name - ); - if (staffChan) { - await Ticket.updateOne( - { gmailThreadId: freshTicket.gmailThreadId }, - { $set: { staffChannelId: staffChan.id } } - ); - } - } catch (e) { - console.error('Staff channel / category (claim):', e); - } - const baseLabel = `Unclaim (${claimerLabel})`; const label = renameInfo.ok ? baseLabel @@ -371,15 +369,6 @@ async function handleClaim(interaction, ticket) { await interaction.followUp({ embeds: [claimEmbed] }); } else { // Unclaim - try { - if (freshTicket.staffChannelId) { - const { deleteStaffChannel } = require('../services/staffChannel'); - await deleteStaffChannel(interaction.guild, freshTicket.staffChannelId); - } - } catch (e) { - console.error('Delete staff channel (unclaim):', e); - } - await Ticket.updateOne( { gmailThreadId: freshTicket.gmailThreadId }, { $set: { claimedBy: null, claimerId: null, staffChannelId: null } } @@ -390,17 +379,11 @@ async function handleClaim(interaction, ticket) { const renameInfo = await canRename(freshTicket); if (renameInfo.ok) { - const currentName = interaction.channel.name.replace(/^[🟢🟡🔴]/, ''); try { - await enqueueRename(interaction.channel, `🟢${currentName}`); + await enqueueRename(interaction.channel, `ticket-${freshTicket.ticketNumber}`); } catch (e) { console.error('Rename error (unclaim):', e); } - try { - if (CONFIG.STAFF_T1_CATEGORY) await enqueueMove(interaction.channel, CONFIG.STAFF_T1_CATEGORY); - } catch (e) { - console.error('Move error (unclaim):', e); - } } else { const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtUnix = Math.floor(unlockAtMs / 1000); diff --git a/services/tickets.js b/services/tickets.js index f7a6d20..313c0cd 100644 --- a/services/tickets.js +++ b/services/tickets.js @@ -33,14 +33,29 @@ function getSenderLocal(senderEmail) { return (senderEmail || 'unknown').split('@')[0].toLowerCase(); } -function makeTicketName({ escalated, claimed }, ticket, guild) { +function toDiscordSafeName(str) { + return str + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^\p{L}\p{N}\p{Emoji_Presentation}-]/gu, '') + .replace(/-{2,}/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 100); +} + +// claimerEmoji and creatorNickname are only used in the claimed branch. +// Callers that do not pass them (e.g. escalation rename) get the unclaimed name as before. +function makeTicketName({ escalated, claimed }, ticket, guild, claimerEmoji, creatorNickname) { const senderLocal = getSenderLocal(ticket.senderEmail); const num = ticket.ticketNumber || 1; if (escalated) { - return claimed - ? `e-ticket-${senderLocal}-${num}` + return (claimed && claimerEmoji && creatorNickname) + ? toDiscordSafeName(`e-${claimerEmoji}-${creatorNickname}-${num}`) : `escalated-ticket-${senderLocal}-${num}`; } + if (claimed && claimerEmoji && creatorNickname) { + return toDiscordSafeName(`${claimerEmoji}-${creatorNickname}-${num}`); + } return `ticket-${senderLocal}-${num}`; }