Baileys 7 uses LIDs (Linked IDs) instead of phone numbers in remoteJid for some messages. This caused messages to be matched to wrong tickets because the LID was used as the sender identifier. This commit adds proper support for both phone numbers and user IDs across WhatsApp and Signal channels. Changes: Database: - Add migration for whatsapp_user_id and signal_user_id fields on users table Zammad controllers: - Update user lookup with 3-step fallback: phone → dedicated user_id field → user_id in phone field (legacy) - Store user IDs in dedicated fields when available - Update phone field when we receive actual phone number for legacy records - Fix redundant condition in Signal controller Bridge services: - Extract both phone (from senderPn/participantPn) and LID (from remoteJid) - Send both identifiers to Zammad via webhooks - Use camelCase (userId) in bridge-whatsapp, convert to snake_case (user_id) in bridge-worker for Zammad compatibility Baileys 7 compliance: - Remove broken loadAllUnreadMessages() call (removed in Baileys 7) - Return descriptive error directing users to use webhooks instead Misc: - Add docs/ to .gitignore
230 lines
7.6 KiB
TypeScript
230 lines
7.6 KiB
TypeScript
import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
|
import { createLogger } from "@link-stack/logger";
|
|
import * as signalApi from "@link-stack/signal-api";
|
|
const { Configuration, GroupsApi } = signalApi;
|
|
|
|
const logger = createLogger('bridge-worker-receive-signal-message');
|
|
|
|
interface ReceiveSignalMessageTaskOptions {
|
|
token: string;
|
|
to: string;
|
|
from: string;
|
|
userId?: string; // Signal user UUID for user identification
|
|
messageId: string;
|
|
sentAt: string;
|
|
message: string;
|
|
attachment?: string;
|
|
filename?: string;
|
|
mimeType?: string;
|
|
isGroup?: boolean;
|
|
}
|
|
|
|
const receiveSignalMessageTask = async ({
|
|
token,
|
|
to,
|
|
from,
|
|
userId,
|
|
messageId,
|
|
sentAt,
|
|
message,
|
|
attachment,
|
|
filename,
|
|
mimeType,
|
|
isGroup,
|
|
}: ReceiveSignalMessageTaskOptions): Promise<void> => {
|
|
logger.debug({
|
|
messageId,
|
|
from,
|
|
to,
|
|
isGroup,
|
|
hasMessage: !!message,
|
|
hasAttachment: !!attachment,
|
|
token,
|
|
}, 'Processing incoming message');
|
|
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";
|
|
|
|
logger.debug({
|
|
enableAutoGroups,
|
|
isGroup,
|
|
shouldCreateGroup: enableAutoGroups && !isGroup && from && to,
|
|
}, 'Auto-groups config');
|
|
|
|
// 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
|
|
logger.debug('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
|
|
logger.info({ from }, 'Creating new group for user');
|
|
|
|
// 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",
|
|
},
|
|
});
|
|
|
|
logger.debug({ createGroupResponse }, 'Group creation response from Signal API');
|
|
|
|
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 {
|
|
logger.debug('Fetching group details to get internalId');
|
|
const groups = await groupsClient.v1GroupsNumberGet({
|
|
number: row.phoneNumber,
|
|
});
|
|
|
|
logger.debug({ groupsSample: groups.slice(0, 3) }, 'Groups for bot');
|
|
|
|
const createdGroup = groups.find((g) => g.id === finalTo);
|
|
if (createdGroup) {
|
|
logger.debug({ createdGroup }, 'Found created group details');
|
|
}
|
|
|
|
if (createdGroup && createdGroup.internalId) {
|
|
createdInternalId = createdGroup.internalId;
|
|
logger.debug({ createdInternalId }, 'Got actual internalId');
|
|
} else {
|
|
// Fallback: extract base64 part from ID
|
|
if (finalTo.startsWith("group.")) {
|
|
createdInternalId = finalTo.substring(6);
|
|
}
|
|
}
|
|
} catch (fetchError) {
|
|
logger.debug('Could not fetch group details, using ID base64 part');
|
|
// Fallback: extract base64 part from ID
|
|
if (finalTo.startsWith("group.")) {
|
|
createdInternalId = finalTo.substring(6);
|
|
}
|
|
}
|
|
|
|
logger.debug({
|
|
fullGroupId: finalTo,
|
|
internalId: createdInternalId,
|
|
}, 'Group created successfully');
|
|
logger.debug({
|
|
groupId: finalTo,
|
|
internalId: createdInternalId,
|
|
groupName,
|
|
forPhoneNumber: from,
|
|
botNumber: row.phoneNumber,
|
|
response: createGroupResponse,
|
|
}, 'Created new Signal group');
|
|
}
|
|
|
|
// 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 {
|
|
logger.debug('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),
|
|
});
|
|
|
|
logger.debug({ finalTo }, 'Successfully forwarded initial message to group');
|
|
} catch (forwardError) {
|
|
logger.error({ error: forwardError }, 'Error forwarding message to group');
|
|
}
|
|
|
|
// Send a response to the original DM informing about the group
|
|
try {
|
|
logger.debug('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,
|
|
});
|
|
|
|
logger.debug('Successfully sent group notification to user DM');
|
|
} catch (dmError) {
|
|
logger.error({ error: dmError }, 'Error sending DM notification');
|
|
}
|
|
}
|
|
} 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) {
|
|
logger.debug({ from }, 'Group might already exist, continuing with original recipient');
|
|
} else {
|
|
logger.error({
|
|
error: errorMessage,
|
|
from,
|
|
to,
|
|
botNumber: row.phoneNumber,
|
|
}, 'Error creating Signal group');
|
|
}
|
|
}
|
|
}
|
|
|
|
const payload = {
|
|
to: finalTo,
|
|
from,
|
|
user_id: userId, // Signal user UUID for user identification
|
|
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;
|