import { db, getWorkerUtils } from "@link-stack/bridge-common"; import * as signalApi from "@link-stack/signal-api"; const { Configuration, GroupsApi } = signalApi; interface ReceiveSignalMessageTaskOptions { token: string; to: string; from: string; messageId: string; sentAt: string; message: string; attachment?: string; filename?: string; mimeType?: string; isGroup?: boolean; } const receiveSignalMessageTask = async ({ token, to, from, messageId, sentAt, message, attachment, filename, mimeType, isGroup, }: ReceiveSignalMessageTaskOptions): Promise => { console.log(`[receive-signal-message] Processing incoming message:`, { messageId, from, to, isGroup, hasMessage: !!message, hasAttachment: !!attachment, token, }); const worker = await getWorkerUtils(); const row = await db .selectFrom("SignalBot") .selectAll() .where("id", "=", token) .executeTakeFirstOrThrow(); const backendId = row.id; let finalTo = to; let createdInternalId: string | undefined; // Check if auto-group creation is enabled and this is NOT already a group message const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true"; console.log(`[receive-signal-message] Auto-groups config:`, { enableAutoGroups, isGroup, shouldCreateGroup: enableAutoGroups && !isGroup && from && to, }); // If this is already a group message and auto-groups is enabled, // use group provided in 'to' if (enableAutoGroups && isGroup && to) { // Signal sends the internal ID (base64) in group messages // We should NOT add "group." prefix - that's for sending messages, not receiving console.log( `[receive-signal-message] Message is from existing group with internal ID`, ); finalTo = to; } else if (enableAutoGroups && !isGroup && from && to) { try { const config = new Configuration({ basePath: process.env.BRIDGE_SIGNAL_URL, }); const groupsClient = new GroupsApi(config); // Always create a new group for direct messages to the helpdesk // This ensures each conversation gets its own group/ticket console.log( `[receive-signal-message] Creating new group for user ${from}`, ); // Include timestamp to make each group unique const timestamp = new Date() .toISOString() .replace(/[:.]/g, "-") .substring(0, 19); const groupName = `Support: ${from} (${timestamp})`; // Create new group for this conversation const createGroupResponse = await groupsClient.v1GroupsNumberPost({ number: row.phoneNumber, data: { name: groupName, members: [from], description: "Private support conversation", }, }); console.log( `[receive-signal-message] Full group creation response from Signal API:`, JSON.stringify(createGroupResponse, null, 2), ); if (createGroupResponse.id) { // The createGroupResponse.id already contains the full group identifier (group.BASE64) finalTo = createGroupResponse.id; // Fetch the group details to get the actual internalId // The base64 part of the ID is NOT the same as the internalId! try { console.log( `[receive-signal-message] Fetching group details to get internalId...`, ); const groups = await groupsClient.v1GroupsNumberGet({ number: row.phoneNumber, }); console.log( `[receive-signal-message] All groups for bot:`, JSON.stringify(groups.slice(0, 3), null, 2), // Show first 3 to avoid too much output ); const createdGroup = groups.find((g) => g.id === finalTo); if (createdGroup) { console.log( `[receive-signal-message] Found created group details:`, JSON.stringify(createdGroup, null, 2), ); } if (createdGroup && createdGroup.internalId) { createdInternalId = createdGroup.internalId; console.log( `[receive-signal-message] Got actual internalId: ${createdInternalId}`, ); } else { // Fallback: extract base64 part from ID if (finalTo.startsWith("group.")) { createdInternalId = finalTo.substring(6); } } } catch (fetchError) { console.log( `[receive-signal-message] Could not fetch group details, using ID base64 part`, ); // Fallback: extract base64 part from ID if (finalTo.startsWith("group.")) { createdInternalId = finalTo.substring(6); } } console.log(`[receive-signal-message] Group created successfully:`, { fullGroupId: finalTo, internalId: createdInternalId, }); console.log(`[receive-signal-message] Created new Signal group:`, { groupId: finalTo, internalId: createdInternalId, groupName, forPhoneNumber: from, botNumber: row.phoneNumber, response: createGroupResponse, }); } // Now handle notifications and message forwarding for both new and existing groups if (finalTo && finalTo.startsWith("group.")) { // Forward the user's initial message to the group using quote feature try { console.log( `[receive-signal-message] Forwarding initial message to group using quote feature`, ); const attributionMessage = `Message from ${from}:\n"${message}"\n\n---\nSupport team: Your request has been received. An agent will respond shortly.`; await worker.addJob("signal/send-signal-message", { token: row.token, to: finalTo, message: attributionMessage, conversationId: null, quoteMessage: message, quoteAuthor: from, quoteTimestamp: Date.parse(sentAt), }); console.log( `[receive-signal-message] Successfully forwarded initial message to group ${finalTo}`, ); } catch (forwardError) { console.error( "[receive-signal-message] Error forwarding message to group:", forwardError, ); } // Send a response to the original DM informing about the group try { console.log( `[receive-signal-message] Sending group notification to original DM`, ); const dmNotification = `Hello! A private support group has been created for your conversation.\n\nGroup name: ${groupName}\n\nPlease look for the new group in your Signal app to continue the conversation. Our support team will respond there shortly.\n\nThank you for contacting support!`; await worker.addJob("signal/send-signal-message", { token: row.token, to: from, message: dmNotification, conversationId: null, }); console.log( `[receive-signal-message] Successfully sent group notification to user DM`, ); } catch (dmError) { console.error( "[receive-signal-message] Error sending DM notification:", dmError, ); } } } catch (error: any) { // Check if error is because group already exists const errorMessage = error?.response?.data?.error || error?.message || error; const isAlreadyExists = errorMessage?.toString().toLowerCase().includes("already") || errorMessage?.toString().toLowerCase().includes("exists"); if (isAlreadyExists) { console.log( `[receive-signal-message] Group might already exist for ${from}, continuing with original recipient`, ); } else { console.error("[receive-signal-message] Error creating Signal group:", { error: errorMessage, from, to, botNumber: row.phoneNumber, }); } } } const payload = { to: finalTo, from, message_id: messageId, sent_at: sentAt, message, attachment, filename, mime_type: mimeType, is_group: finalTo.startsWith("group"), }; await worker.addJob("common/notify-webhooks", { backendId, payload }); }; export default receiveSignalMessageTask;