/** * Syncs Zammad agent replies to Discord (for Discord tickets) and Gmail (for email tickets). * Polls Zammad ticket articles and pushes new customer-visible Agent replies to the right channel. */ const { mongoose } = require('../db-connection'); const { getZammadTicketArticles } = require('./zammad'); const { sendGmailReply } = require('./gmail'); const { htmlToTextWithBlocks } = require('../utils'); const Ticket = mongoose.model('Ticket'); function bodyToText(body, contentType) { if (!body) return ''; const isHtml = (contentType || '').toLowerCase().includes('html'); return isHtml ? htmlToTextWithBlocks(body).trim() : String(body).trim(); } /** * Run once: find open tickets with Zammad ID, fetch new agent (customer-visible) articles, * post to Discord or send via Gmail, then update lastSyncedZammadArticleId. * @param {import('discord.js').Client} client - Discord client (for posting to ticket channels) */ async function syncZammadReplies(client) { if (!client?.channels) return; const tickets = await Ticket.find({ zammadTicketId: { $exists: true, $ne: null }, status: 'open' }) .select('gmailThreadId discordThreadId zammadTicketId lastSyncedZammadArticleId senderEmail subject') .lean(); for (const ticket of tickets) { try { const articles = await getZammadTicketArticles(ticket.zammadTicketId); // Only agent replies that are customer-visible (not internal notes) const agentReplies = articles.filter( (a) => a.sender === 'Agent' && a.internal === false && a.body ); if (agentReplies.length === 0) continue; const lastSynced = ticket.lastSyncedZammadArticleId || 0; const newReplies = agentReplies.filter((a) => a.id > lastSynced); const maxId = Math.max(lastSynced, ...agentReplies.map((a) => a.id)); // First run: just advance cursor so we don't resend existing articles if (newReplies.length === 0) { if (maxId > lastSynced) { await Ticket.updateOne( { gmailThreadId: ticket.gmailThreadId }, { $set: { lastSyncedZammadArticleId: maxId } } ); } continue; } const isDiscordTicket = ticket.gmailThreadId.startsWith('discord-'); for (const article of newReplies) { const text = bodyToText(article.body, article.content_type); if (!text) continue; const fromLabel = article.created_by || 'Support'; if (isDiscordTicket && ticket.discordThreadId) { const channel = await client.channels.fetch(ticket.discordThreadId).catch(() => null); if (channel) { await channel.send(`**${fromLabel}** (via Zammad):\n${text}`).catch((err) => { console.error('Zammad sync: Discord send failed:', err.message); }); } } else { // Email ticket: send reply via Gmail try { await sendGmailReply( ticket.gmailThreadId, text, ticket.senderEmail, ticket.subject || 'Support', fromLabel, null ); } catch (err) { console.error('Zammad sync: Gmail send failed:', err.message); } } } await Ticket.updateOne( { gmailThreadId: ticket.gmailThreadId }, { $set: { lastSyncedZammadArticleId: maxId } } ); } catch (err) { console.error('Zammad sync error for ticket', ticket.gmailThreadId, err.message); } } } module.exports = { syncZammadReplies };