Signal group and Formstack fixes

This commit is contained in:
Darren Clarke 2025-11-13 10:42:16 +01:00
parent 00d1fe5eef
commit 0e8c9be247
16 changed files with 692 additions and 142 deletions

View file

@ -1 +1,2 @@
*/1 * * * * fetch-signal-messages ?max=1&id=fetchSignalMessagesCron {"scheduleTasks": "true"}
*/1 * * * * signal:fetch-signal-messages ?max=1&id=fetchSignalMessagesCron {"scheduleTasks": "true"}
*/2 * * * * signal:check-group-membership ?max=1&id=checkGroupMembershipCron {}

View file

@ -3,7 +3,7 @@ import { createLogger } from "@link-stack/logger";
import * as path from "path";
import { fileURLToPath } from "url";
const logger = createLogger('bridge-worker');
const logger = createLogger("bridge-worker");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -32,6 +32,15 @@ const main = async () => {
};
main().catch((err) => {
logger.error({ error: err }, 'Worker failed to start');
logger.error(
{
error: err,
message: err.message,
stack: err.stack,
name: err.name,
},
"Worker failed to start",
);
console.error("Full error:", err);
process.exit(1);
});

View file

@ -10,7 +10,7 @@ import {
type FieldMappingConfig,
} from "../../lib/formstack-field-mapping.js";
const logger = createLogger('create-ticket-from-form');
const logger = createLogger("create-ticket-from-form");
export interface CreateTicketFromFormOptions {
formData: any;
@ -26,40 +26,49 @@ const createTicketFromFormTask = async (
const mapping = loadFieldMapping();
// Log only non-PII metadata using configured field names
const formId = getFieldValue(formData, 'formId', mapping);
const uniqueId = getFieldValue(formData, 'uniqueId', mapping);
const formId = getFieldValue(formData, "formId", mapping);
const uniqueId = getFieldValue(formData, "uniqueId", mapping);
logger.info({
formId,
uniqueId,
receivedAt,
fieldCount: Object.keys(formData).length
}, 'Processing Formstack form submission');
logger.info(
{
formId,
uniqueId,
receivedAt,
fieldCount: Object.keys(formData).length,
},
"Processing Formstack form submission",
);
// Extract fields using dynamic mapping
const nameField = getFieldValue(formData, 'name', mapping);
const nameField = getFieldValue(formData, "name", mapping);
const firstName = mapping.nestedFields?.name?.firstNamePath
? getNestedFieldValue(nameField, mapping.nestedFields.name.firstNamePath) || ''
: '';
? getNestedFieldValue(nameField, mapping.nestedFields.name.firstNamePath) || ""
: "";
const lastName = mapping.nestedFields?.name?.lastNamePath
? getNestedFieldValue(nameField, mapping.nestedFields.name.lastNamePath) || ''
: '';
const fullName = (firstName && lastName)
? `${firstName} ${lastName}`.trim()
: firstName || lastName || 'Unknown';
? getNestedFieldValue(nameField, mapping.nestedFields.name.lastNamePath) || ""
: "";
const fullName =
firstName && lastName
? `${firstName} ${lastName}`.trim()
: firstName || lastName || "Unknown";
// Extract well-known fields used for special logic (all optional)
const email = getFieldValue(formData, 'email', mapping);
const phone = getFieldValue(formData, 'phone', mapping);
const signalAccount = getFieldValue(formData, 'signalAccount', mapping);
const organization = getFieldValue(formData, 'organization', mapping);
const typeOfSupport = getFieldValue(formData, 'typeOfSupport', mapping);
const descriptionOfIssue = getFieldValue(formData, 'descriptionOfIssue', mapping);
const email = getFieldValue(formData, "email", mapping);
const phone = getFieldValue(formData, "phone", mapping);
const signalAccount = getFieldValue(formData, "signalAccount", mapping);
const organization = getFieldValue(formData, "organization", mapping);
const typeOfSupport = getFieldValue(formData, "typeOfSupport", mapping);
const descriptionOfIssue = getFieldValue(formData, "descriptionOfIssue", mapping);
// Validate that at least one contact method is provided
if (!email && !phone && !signalAccount) {
logger.error({ formId, uniqueId }, 'No contact information provided - at least one of email, phone, or signalAccount is required');
throw new Error('At least one contact method (email, phone, or signalAccount) is required for ticket creation');
logger.error(
{ formId, uniqueId },
"No contact information provided - at least one of email, phone, or signalAccount is required",
);
throw new Error(
"At least one contact method (email, phone, or signalAccount) is required for ticket creation",
);
}
// Build ticket title using configured template
@ -72,10 +81,10 @@ const createTicketFromFormTask = async (
// Build article body - format all fields as HTML
const formatAllFields = (data: any): string => {
let html = '';
let html = "";
// Add formatted name field first if we have it
if (fullName && fullName !== 'Unknown') {
if (fullName && fullName !== "Unknown") {
html += `<strong>Name:</strong><br>${fullName}<br>`;
}
@ -84,15 +93,18 @@ const createTicketFromFormTask = async (
const skipFields = [
mapping.sourceFields.formId,
mapping.sourceFields.uniqueId,
mapping.sourceFields.name, // Skip raw name field
'HandshakeKey',
mapping.sourceFields.name, // Skip raw name field
"HandshakeKey",
].filter(Boolean);
if (skipFields.includes(key)) continue;
if (value === null || value === undefined || value === '') continue;
if (value === null || value === undefined || value === "") continue;
const displayValue = Array.isArray(value) ? value.join(', ') :
typeof value === 'object' ? JSON.stringify(value) : value;
const displayValue = Array.isArray(value)
? value.join(", ")
: typeof value === "object"
? JSON.stringify(value)
: value;
html += `<strong>${key}:</strong><br>${displayValue}<br>`;
}
return html;
@ -101,12 +113,12 @@ const createTicketFromFormTask = async (
const body = formatAllFields(formData);
// Get Zammad configuration from environment
const zammadUrl = process.env.ZAMMAD_URL || 'http://zammad-nginx:8080';
const zammadUrl = process.env.ZAMMAD_URL || "http://zammad-nginx:8080";
const zammadToken = process.env.ZAMMAD_API_TOKEN;
if (!zammadToken) {
logger.error('ZAMMAD_API_TOKEN environment variable is not configured');
throw new Error('ZAMMAD_API_TOKEN is required');
logger.error("ZAMMAD_API_TOKEN environment variable is not configured");
throw new Error("ZAMMAD_API_TOKEN is required");
}
const zammad = Zammad({ token: zammadToken }, zammadUrl);
@ -115,35 +127,39 @@ const createTicketFromFormTask = async (
// Look up the configured article type
let articleTypeId: number | undefined;
try {
const articleTypes = await zammad.get('ticket_article_types');
const configuredType = articleTypes.find((t: any) => t.name === mapping.ticket.defaultArticleType);
const articleTypes = await zammad.get("ticket_article_types");
const configuredType = articleTypes.find(
(t: any) => t.name === mapping.ticket.defaultArticleType,
);
articleTypeId = configuredType?.id;
if (articleTypeId) {
logger.info({ articleTypeId, typeName: mapping.ticket.defaultArticleType }, 'Found configured article type');
logger.info(
{ articleTypeId, typeName: mapping.ticket.defaultArticleType },
"Found configured article type",
);
} else {
logger.warn({ typeName: mapping.ticket.defaultArticleType }, 'Configured article type not found, ticket will use default type');
logger.warn(
{ typeName: mapping.ticket.defaultArticleType },
"Configured article type not found, ticket will use default type",
);
}
} catch (error: any) {
logger.warn({ error: error.message }, 'Failed to look up article type');
logger.warn({ error: error.message }, "Failed to look up article type");
}
// Get or create user
// Try to find existing user by: signalAccount -> phone -> email
// Try to find existing user by: phone -> email
// Note: We can't search by Signal account since Signal group IDs aren't phone numbers
let customer;
// Try Signal account first if provided
if (signalAccount) {
customer = await getUser(zammad, signalAccount);
if (customer) {
logger.info({ customerId: customer.id, method: 'signal' }, 'Found existing user by Signal account');
}
}
// Fall back to phone if no customer found yet
if (!customer && phone) {
// Try phone if provided
if (phone) {
customer = await getUser(zammad, phone);
if (customer) {
logger.info({ customerId: customer.id, method: 'phone' }, 'Found existing user by phone');
logger.info(
{ customerId: customer.id, method: "phone" },
"Found existing user by phone",
);
}
}
@ -155,22 +171,25 @@ const createTicketFromFormTask = async (
const emailResults = await zammad.user.search(`email:${email}`);
if (emailResults.length > 0) {
customer = emailResults[0];
logger.info({ customerId: customer.id, method: 'email' }, 'Found existing user by email');
logger.info(
{ customerId: customer.id, method: "email" },
"Found existing user by email",
);
}
} else {
logger.warn({ email }, 'Invalid email format provided, skipping email search');
logger.warn({ email }, "Invalid email format provided, skipping email search");
}
}
if (!customer) {
// Create new user
logger.info('Creating new user from form submission');
logger.info("Creating new user from form submission");
// Build user data with whatever contact info we have
const userData: any = {
firstname: firstName,
lastname: lastName,
roles: ['Customer'],
roles: ["Customer"],
};
// Add contact info only if provided
@ -178,47 +197,105 @@ const createTicketFromFormTask = async (
userData.email = email;
}
const userPhone = signalAccount || phone;
if (userPhone) {
userData.phone = userPhone;
// Use phone number if provided (don't use Signal group ID as phone)
if (phone) {
userData.phone = phone;
}
customer = await zammad.user.create(userData);
}
logger.info({
customerId: customer.id,
email: customer.email,
}, 'Using customer for ticket');
logger.info(
{
customerId: customer.id,
email: customer.email,
},
"Using customer for ticket",
);
// Look up the configured group
const groups = await zammad.get('groups');
const groups = await zammad.get("groups");
const targetGroup = groups.find((g: any) => g.name === mapping.ticket.group);
if (!targetGroup) {
logger.error({ groupName: mapping.ticket.group }, 'Configured group not found');
logger.error({ groupName: mapping.ticket.group }, "Configured group not found");
throw new Error(`Zammad group "${mapping.ticket.group}" not found`);
}
logger.info({ groupId: targetGroup.id, groupName: targetGroup.name }, 'Using configured group');
logger.info(
{ groupId: targetGroup.id, groupName: targetGroup.name },
"Using configured group",
);
// Build custom fields using Zammad field mapping
// This dynamically maps all configured fields without hardcoding
const customFields = getZammadFieldValues(formData, mapping);
// Check if this is a Signal ticket
let signalArticleType = null;
let signalChannel = null;
if (signalAccount) {
try {
logger.info({ signalAccount }, "Looking up Signal channel and article type");
// Look up Signal channels (admin-only endpoint)
const channels = await zammad.get("cdr_signal_channels");
if (channels.length > 0) {
signalChannel = channels[0]; // Use first active Signal channel
logger.info(
{
channelId: signalChannel.id,
phoneNumber: signalChannel.phone_number,
},
"Found active Signal channel",
);
} else {
logger.warn("No active Signal channels found");
}
// Look up cdr_signal article type
const articleTypes = await zammad.get("ticket_article_types");
signalArticleType = articleTypes.find((t: any) => t.name === "cdr_signal");
if (!signalArticleType) {
logger.warn("Signal article type (cdr_signal) not found, using default type");
} else {
logger.info(
{ articleTypeId: signalArticleType.id },
"Found Signal article type",
);
}
} catch (error: any) {
logger.warn(
{ error: error.message },
"Failed to look up Signal article type, creating regular ticket",
);
}
}
// Create the ticket
const articleData: any = {
subject: descriptionOfIssue || 'Support Request',
subject: descriptionOfIssue || "Support Request",
body,
content_type: 'text/html',
content_type: "text/html",
internal: false,
};
if (articleTypeId) {
// Use Signal article type if available, otherwise use configured default
if (signalArticleType) {
articleData.type_id = signalArticleType.id;
logger.info({ typeId: signalArticleType.id }, "Using Signal article type");
// IMPORTANT: Set sender to "Customer" for Signal tickets created from Formstack
// This prevents the article from being echoed back to the user via Signal
// (enqueue_communicate_cdr_signal_job only sends if sender != 'Customer')
articleData.sender = "Customer";
} else if (articleTypeId) {
articleData.type_id = articleTypeId;
}
const ticketData = {
const ticketData: any = {
title,
group_id: targetGroup.id,
customer_id: customer.id,
@ -226,29 +303,84 @@ const createTicketFromFormTask = async (
...customFields,
};
logger.info({
title,
groupId: targetGroup.id,
customerId: customer.id,
hasArticleType: !!articleTypeId,
customFieldCount: Object.keys(customFields).length,
}, 'Creating ticket');
// Add Signal preferences if we have Signal channel and article type
// Note: signalAccount from Formstack is the phone number the user typed in
// Groups are added later via update_group webhook from bridge-worker
if (signalChannel && signalArticleType && signalAccount) {
ticketData.preferences = {
channel_id: signalChannel.id,
cdr_signal: {
bot_token: signalChannel.bot_token,
chat_id: signalAccount, // Use Signal phone number as chat_id
},
};
logger.info(
{
channelId: signalChannel.id,
chatId: signalAccount,
},
"Adding Signal preferences to ticket",
);
}
logger.info(
{
title,
groupId: targetGroup.id,
customerId: customer.id,
hasArticleType: !!articleTypeId || !!signalArticleType,
isSignalTicket: !!signalArticleType && !!signalAccount,
customFieldCount: Object.keys(customFields).length,
},
"Creating ticket",
);
const ticket = await zammad.ticket.create(ticketData);
logger.info({
ticketId: ticket.id,
ticketNumber: ticket.id,
title,
}, 'Successfully created ticket from Formstack submission');
// Set create_article_type_id for Signal tickets to enable proper replies
if (signalArticleType && signalChannel) {
try {
await zammad.ticket.update(ticket.id, {
create_article_type_id: signalArticleType.id,
});
logger.info(
{
ticketId: ticket.id,
articleTypeId: signalArticleType.id,
},
"Set create_article_type_id for Signal ticket",
);
} catch (error: any) {
logger.warn(
{
error: error.message,
ticketId: ticket.id,
},
"Failed to set create_article_type_id, ticket may not support Signal replies",
);
}
}
logger.info(
{
ticketId: ticket.id,
ticketNumber: ticket.id,
title,
isSignalTicket: !!signalChannel,
},
"Successfully created ticket from Formstack submission",
);
} catch (error: any) {
logger.error({
error: error.message,
stack: error.stack,
formId,
uniqueId,
}, 'Failed to create ticket from Formstack submission');
logger.error(
{
error: error.message,
stack: error.stack,
formId,
uniqueId,
},
"Failed to create ticket from Formstack submission",
);
throw error;
}
};

View file

@ -0,0 +1,117 @@
#!/usr/bin/env node
/**
* Check Signal group membership status and update Zammad tickets
*
* This task queries the Signal CLI API to check if users have joined
* their assigned groups. When a user joins (moves from pendingInvites to members),
* it updates the ticket's group_joined flag in Zammad.
*/
import { db, getWorkerUtils } from "@link-stack/bridge-common";
import { createLogger } from "@link-stack/logger";
import * as signalApi from "@link-stack/signal-api";
const logger = createLogger("check-group-membership");
const { Configuration, GroupsApi } = signalApi;
interface CheckGroupMembershipTaskOptions {
// Optional: Check specific group. If not provided, checks all groups with group_joined=false
groupId?: string;
botToken?: string;
}
const checkGroupMembershipTask = async (
options: CheckGroupMembershipTaskOptions = {},
): Promise<void> => {
const config = new Configuration({
basePath: process.env.BRIDGE_SIGNAL_URL,
});
const groupsClient = new GroupsApi(config);
const worker = await getWorkerUtils();
// Get all Signal bots
const bots = await db.selectFrom("SignalBot").selectAll().execute();
for (const bot of bots) {
try {
logger.debug(
{ botId: bot.id, phoneNumber: bot.phoneNumber },
"Checking groups for bot",
);
// Get all groups for this bot
const groups = await groupsClient.v1GroupsNumberGet({
number: bot.phoneNumber,
});
logger.debug(
{ botId: bot.id, groupCount: groups.length },
"Retrieved groups from Signal CLI",
);
// For each group, check if we have tickets waiting for members to join
for (const group of groups) {
if (!group.id || !group.internalId) {
logger.debug({ groupName: group.name }, "Skipping group without ID");
continue;
}
// Log info about each group temporarily for debugging
logger.info(
{
groupId: group.id,
groupName: group.name,
membersCount: group.members?.length || 0,
members: group.members,
pendingInvitesCount: group.pendingInvites?.length || 0,
pendingInvites: group.pendingInvites,
pendingRequestsCount: group.pendingRequests?.length || 0,
},
"Checking group membership",
);
// Notify Zammad about each member who has joined
// This handles both cases:
// 1. New contacts who must accept invite (they move from pendingInvites to members)
// 2. Existing contacts who are auto-added (they appear directly in members)
if (group.members && group.members.length > 0) {
for (const memberPhone of group.members) {
// Check if this member was previously pending
// We'll send the webhook and let Zammad decide if it needs to update
await worker.addJob("common/notify-webhooks", {
backendId: bot.id,
payload: {
event: "group_member_joined",
group_id: group.id,
member_phone: memberPhone,
timestamp: new Date().toISOString(),
},
});
logger.info(
{
groupId: group.id,
memberPhone,
},
"Notified Zammad about group member",
);
}
}
}
} catch (error: any) {
logger.error(
{
botId: bot.id,
error: error.message,
stack: error.stack,
},
"Error checking group membership for bot",
);
}
}
logger.info("Completed group membership check");
};
export default checkGroupMembershipTask;

View file

@ -2,7 +2,7 @@ import { db, getWorkerUtils } from "@link-stack/bridge-common";
import { createLogger } from "@link-stack/logger";
import * as signalApi from "@link-stack/signal-api";
const logger = createLogger('fetch-signal-messages');
const logger = createLogger("fetch-signal-messages");
const { Configuration, MessagesApi, AttachmentsApi } = signalApi;
const config = new Configuration({
@ -28,13 +28,13 @@ const fetchAttachments = async (attachments: any[] | undefined) => {
let defaultFilename = name;
if (!defaultFilename) {
// Check if id already has an extension
const hasExtension = id.includes('.');
const hasExtension = id.includes(".");
if (hasExtension) {
// ID already includes extension
defaultFilename = id;
} else {
// Add extension based on content type
const extension = contentType?.split('/')[1] || 'bin';
const extension = contentType?.split("/")[1] || "bin";
defaultFilename = `${id}.${extension}`;
}
}
@ -64,7 +64,22 @@ const processMessage = async ({
message: msg,
}: ProcessMessageArgs): Promise<Record<string, any>[]> => {
const { envelope } = msg;
const { source, sourceUuid, dataMessage } = envelope;
const { source, sourceUuid, dataMessage, syncMessage, receiptMessage, typingMessage } =
envelope;
// Log all envelope types to understand what events we're receiving
logger.info(
{
source,
sourceUuid,
hasDataMessage: !!dataMessage,
hasSyncMessage: !!syncMessage,
hasReceiptMessage: !!receiptMessage,
hasTypingMessage: !!typingMessage,
envelopeKeys: Object.keys(envelope),
},
"Received Signal envelope",
);
const isGroup = !!(
dataMessage?.groupV2 ||
@ -72,23 +87,69 @@ const processMessage = async ({
dataMessage?.groupInfo
);
// Check if this is a group membership change event
const groupInfo = dataMessage?.groupInfo;
if (groupInfo) {
logger.info(
{
type: groupInfo.type,
groupId: groupInfo.groupId,
source,
groupInfoKeys: Object.keys(groupInfo),
fullGroupInfo: groupInfo,
},
"Received group info event",
);
// If user joined the group, notify Zammad
if (groupInfo.type === "JOIN" || groupInfo.type === "JOINED") {
const worker = await getWorkerUtils();
const groupId = groupInfo.groupId
? `group.${Buffer.from(groupInfo.groupId).toString("base64")}`
: null;
if (groupId) {
await worker.addJob("common/notify-webhooks", {
backendId: id,
payload: {
event: "group_member_joined",
group_id: groupId,
member_phone: source,
timestamp: new Date().toISOString(),
},
});
logger.info(
{
groupId,
memberPhone: source,
},
"User joined Signal group, notifying Zammad",
);
}
}
}
if (!dataMessage) return [];
const { attachments } = dataMessage;
const rawTimestamp = dataMessage?.timestamp;
logger.debug({
sourceUuid,
source,
rawTimestamp,
hasGroupV2: !!dataMessage?.groupV2,
hasGroupContext: !!dataMessage?.groupContext,
hasGroupInfo: !!dataMessage?.groupInfo,
isGroup,
groupV2Id: dataMessage?.groupV2?.id,
groupContextType: dataMessage?.groupContext?.type,
groupInfoType: dataMessage?.groupInfo?.type,
}, 'Processing message');
logger.debug(
{
sourceUuid,
source,
rawTimestamp,
hasGroupV2: !!dataMessage?.groupV2,
hasGroupContext: !!dataMessage?.groupContext,
hasGroupInfo: !!dataMessage?.groupInfo,
isGroup,
groupV2Id: dataMessage?.groupV2?.id,
groupContextType: dataMessage?.groupContext?.type,
groupInfoType: dataMessage?.groupInfo?.type,
},
"Processing message",
);
const timestamp = new Date(rawTimestamp);
const formattedAttachments = await fetchAttachments(attachments);
@ -165,7 +226,7 @@ const fetchSignalMessagesTask = async ({
number: phoneNumber,
});
logger.debug({ botId: id, phoneNumber }, 'Fetching messages for bot');
logger.debug({ botId: id, phoneNumber }, "Fetching messages for bot");
for (const message of messages) {
const formattedMessages = await processMessage({
@ -175,19 +236,19 @@ const fetchSignalMessagesTask = async ({
});
for (const formattedMessage of formattedMessages) {
if (formattedMessage.to !== formattedMessage.from) {
logger.debug({
messageId: formattedMessage.messageId,
from: formattedMessage.from,
to: formattedMessage.to,
isGroup: formattedMessage.isGroup,
hasMessage: !!formattedMessage.message,
hasAttachment: !!formattedMessage.attachment,
}, 'Creating job for message');
await worker.addJob(
"signal/receive-signal-message",
formattedMessage,
logger.debug(
{
messageId: formattedMessage.messageId,
from: formattedMessage.from,
to: formattedMessage.to,
isGroup: formattedMessage.isGroup,
hasMessage: !!formattedMessage.message,
hasAttachment: !!formattedMessage.attachment,
},
"Creating job for message",
);
await worker.addJob("signal/receive-signal-message", formattedMessage);
}
}
}

View file

@ -65,10 +65,9 @@ const sendSignalMessageTask = async ({
try {
// Check if 'to' is a group ID (UUID format, group.base64 format, or base64) vs phone number
const isUUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
to,
);
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
to,
);
const isGroupPrefix = to.startsWith("group.");
const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(to) && to.length > 20; // Base64 internal_id
const isGroupId = isUUID || isGroupPrefix || isBase64;
@ -79,8 +78,7 @@ const sendSignalMessageTask = async ({
to,
isGroupId,
enableAutoGroups,
shouldCreateGroup:
enableAutoGroups && !isGroupId && to && conversationId,
shouldCreateGroup: enableAutoGroups && !isGroupId && to && conversationId,
},
"Recipient analysis",
);
@ -140,6 +138,7 @@ const sendSignalMessageTask = async ({
);
// Notify Zammad about the new group ID via webhook
// Set group_joined: false initially - will be updated when user accepts invitation
await worker.addJob("common/notify-webhooks", {
backendId: bot.id,
payload: {
@ -148,6 +147,7 @@ const sendSignalMessageTask = async ({
original_recipient: to,
group_id: finalTo,
internal_group_id: internalId,
group_joined: false,
timestamp: new Date().toISOString(),
},
});
@ -155,8 +155,7 @@ const sendSignalMessageTask = async ({
} catch (groupError) {
logger.error(
{
error:
groupError instanceof Error ? groupError.message : groupError,
error: groupError instanceof Error ? groupError.message : groupError,
to,
conversationId,
},
@ -217,7 +216,9 @@ const sendSignalMessageTask = async ({
const MAX_TOTAL_SIZE = getMaxTotalAttachmentSize();
if (attachments.length > MAX_ATTACHMENTS) {
throw new Error(`Too many attachments: ${attachments.length} (max ${MAX_ATTACHMENTS})`);
throw new Error(
`Too many attachments: ${attachments.length} (max ${MAX_ATTACHMENTS})`,
);
}
let totalSize = 0;
@ -228,20 +229,26 @@ const sendSignalMessageTask = async ({
const estimatedSize = (attachment.data.length * 3) / 4;
if (estimatedSize > MAX_ATTACHMENT_SIZE) {
logger.warn({
filename: attachment.filename,
size: estimatedSize,
maxSize: MAX_ATTACHMENT_SIZE
}, 'Attachment exceeds size limit, skipping');
logger.warn(
{
filename: attachment.filename,
size: estimatedSize,
maxSize: MAX_ATTACHMENT_SIZE,
},
"Attachment exceeds size limit, skipping",
);
continue;
}
totalSize += estimatedSize;
if (totalSize > MAX_TOTAL_SIZE) {
logger.warn({
totalSize,
maxTotalSize: MAX_TOTAL_SIZE
}, 'Total attachment size exceeds limit, skipping remaining');
logger.warn(
{
totalSize,
maxTotalSize: MAX_TOTAL_SIZE,
},
"Total attachment size exceeds limit, skipping remaining",
);
break;
}
@ -253,8 +260,10 @@ const sendSignalMessageTask = async ({
logger.debug(
{
attachmentCount: validatedAttachments.length,
attachmentNames: attachments.slice(0, validatedAttachments.length).map((att) => att.filename),
totalSizeBytes: totalSize
attachmentNames: attachments
.slice(0, validatedAttachments.length)
.map((att) => att.filename),
totalSizeBytes: totalSize,
},
"Including attachments in message",
);