99
services/zammad-sync.js
Normal file
99
services/zammad-sync.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user