Add generic reply context fields to NewMessage (reply_to_message_id, reply_to_message_content, reply_to_sender_name) so any channel can pass quoted message context to the agent. - Add thread_id and reply_to_* fields to NewMessage interface - Add DB migration for reply context columns on messages table - Update storeMessage/getMessagesSince/getNewMessages to persist and retrieve reply fields - Render reply context as <quoted_message> XML in formatMessages - Add DB and formatting tests Co-Authored-By: Alfred-the-buttler <leon.alfred.bot@gmail.com> Co-Authored-By: moktamd <moktamd@users.noreply.github.com> Co-Authored-By: gurixs-carson <gurixs-carson@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
60 lines
1.8 KiB
TypeScript
60 lines
1.8 KiB
TypeScript
import { Channel, NewMessage } from './types.js';
|
|
import { formatLocalTime } from './timezone.js';
|
|
|
|
export function escapeXml(s: string): string {
|
|
if (!s) return '';
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
export function formatMessages(
|
|
messages: NewMessage[],
|
|
timezone: string,
|
|
): string {
|
|
const lines = messages.map((m) => {
|
|
const displayTime = formatLocalTime(m.timestamp, timezone);
|
|
const replyAttr = m.reply_to_message_id
|
|
? ` reply_to="${escapeXml(m.reply_to_message_id)}"`
|
|
: '';
|
|
const replySnippet =
|
|
m.reply_to_message_content && m.reply_to_sender_name
|
|
? `\n <quoted_message from="${escapeXml(m.reply_to_sender_name)}">${escapeXml(m.reply_to_message_content)}</quoted_message>`
|
|
: '';
|
|
return `<message sender="${escapeXml(m.sender_name)}" time="${escapeXml(displayTime)}"${replyAttr}>${replySnippet}${escapeXml(m.content)}</message>`;
|
|
});
|
|
|
|
const header = `<context timezone="${escapeXml(timezone)}" />\n`;
|
|
|
|
return `${header}<messages>\n${lines.join('\n')}\n</messages>`;
|
|
}
|
|
|
|
export function stripInternalTags(text: string): string {
|
|
return text.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
|
|
}
|
|
|
|
export function formatOutbound(rawText: string): string {
|
|
const text = stripInternalTags(rawText);
|
|
if (!text) return '';
|
|
return text;
|
|
}
|
|
|
|
export function routeOutbound(
|
|
channels: Channel[],
|
|
jid: string,
|
|
text: string,
|
|
): Promise<void> {
|
|
const channel = channels.find((c) => c.ownsJid(jid) && c.isConnected());
|
|
if (!channel) throw new Error(`No channel for JID: ${jid}`);
|
|
return channel.sendMessage(jid, text);
|
|
}
|
|
|
|
export function findChannel(
|
|
channels: Channel[],
|
|
jid: string,
|
|
): Channel | undefined {
|
|
return channels.find((c) => c.ownsJid(jid));
|
|
}
|