- Create new @link-stack/logger package wrapping Pino for structured logging - Replace all console.log/error/warn statements across the monorepo - Configure environment-aware logging (pretty-print in dev, JSON in prod) - Add automatic redaction of sensitive fields (passwords, tokens, etc.) - Remove dead commented-out logger file from bridge-worker - Follow Pino's standard argument order (context object first, message second) - Support log levels via LOG_LEVEL environment variable - Export TypeScript types for better IDE support This provides consistent, structured logging across all applications and packages, making debugging easier and production logs more parseable.
212 lines
6.7 KiB
TypeScript
212 lines
6.7 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, MessagesApi, GroupsApi } = signalApi;
|
|
|
|
const logger = createLogger('bridge-worker-send-signal-message');
|
|
|
|
interface SendSignalMessageTaskOptions {
|
|
token: string;
|
|
to: string;
|
|
message: any;
|
|
conversationId?: string; // Zammad ticket/conversation ID for callback
|
|
quoteMessage?: string; // Optional: message text to quote
|
|
quoteAuthor?: string; // Optional: author of quoted message (phone number)
|
|
quoteTimestamp?: number; // Optional: timestamp of quoted message in milliseconds
|
|
}
|
|
|
|
const sendSignalMessageTask = async ({
|
|
token,
|
|
to,
|
|
message,
|
|
conversationId,
|
|
quoteMessage,
|
|
quoteAuthor,
|
|
quoteTimestamp,
|
|
}: SendSignalMessageTaskOptions): Promise<void> => {
|
|
logger.debug({
|
|
token,
|
|
to,
|
|
conversationId,
|
|
messageLength: message?.length,
|
|
}, 'Processing outgoing message');
|
|
const bot = await db
|
|
.selectFrom("SignalBot")
|
|
.selectAll()
|
|
.where("token", "=", token)
|
|
.executeTakeFirstOrThrow();
|
|
|
|
const { phoneNumber: number } = bot;
|
|
const config = new Configuration({
|
|
basePath: process.env.BRIDGE_SIGNAL_URL,
|
|
});
|
|
const messagesClient = new MessagesApi(config);
|
|
const groupsClient = new GroupsApi(config);
|
|
const worker = await getWorkerUtils();
|
|
|
|
let finalTo = to;
|
|
let groupCreated = false;
|
|
|
|
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 isGroupPrefix = to.startsWith("group.");
|
|
const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(to) && to.length > 20; // Base64 internal_id
|
|
const isGroupId = isUUID || isGroupPrefix || isBase64;
|
|
const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true";
|
|
|
|
logger.debug({
|
|
to,
|
|
isGroupId,
|
|
enableAutoGroups,
|
|
shouldCreateGroup: enableAutoGroups && !isGroupId && to && conversationId,
|
|
}, 'Recipient analysis');
|
|
|
|
// If sending to a phone number and auto-groups is enabled, create a group first
|
|
if (enableAutoGroups && !isGroupId && to && conversationId) {
|
|
try {
|
|
const groupName = `DPN Support Request: ${conversationId}`;
|
|
const createGroupResponse = await groupsClient.v1GroupsNumberPost({
|
|
number: bot.phoneNumber,
|
|
data: {
|
|
name: groupName,
|
|
members: [to],
|
|
description: "Private support conversation",
|
|
},
|
|
});
|
|
|
|
if (createGroupResponse.id) {
|
|
// The createGroupResponse.id already contains the full group identifier (group.BASE64)
|
|
finalTo = createGroupResponse.id;
|
|
groupCreated = true;
|
|
|
|
// Fetch the group details to get the actual internalId
|
|
let internalId: string | undefined;
|
|
try {
|
|
const groups = await groupsClient.v1GroupsNumberGet({
|
|
number: bot.phoneNumber,
|
|
});
|
|
|
|
const createdGroup = groups.find((g) => g.id === finalTo);
|
|
if (createdGroup && createdGroup.internalId) {
|
|
internalId = createdGroup.internalId;
|
|
logger.debug({ internalId }, 'Got actual internalId');
|
|
} else {
|
|
// Fallback: extract base64 part from ID
|
|
if (finalTo.startsWith("group.")) {
|
|
internalId = 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.")) {
|
|
internalId = finalTo.substring(6);
|
|
}
|
|
}
|
|
logger.debug({
|
|
groupId: finalTo,
|
|
internalId,
|
|
groupName,
|
|
conversationId,
|
|
originalRecipient: to,
|
|
botNumber: bot.phoneNumber,
|
|
}, 'Created new Signal group');
|
|
|
|
// Notify Zammad about the new group ID via webhook
|
|
await worker.addJob("common/notify-webhooks", {
|
|
backendId: bot.id,
|
|
payload: {
|
|
event: "group_created",
|
|
conversation_id: conversationId,
|
|
original_recipient: to,
|
|
group_id: finalTo,
|
|
internal_group_id: internalId,
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
});
|
|
}
|
|
} catch (groupError) {
|
|
logger.error({
|
|
error: groupError instanceof Error ? groupError.message : groupError,
|
|
to,
|
|
conversationId,
|
|
}, 'Error creating Signal group');
|
|
// Continue with original recipient if group creation fails
|
|
}
|
|
}
|
|
|
|
logger.debug({
|
|
fromNumber: number,
|
|
toRecipient: finalTo,
|
|
originalTo: to,
|
|
recipientChanged: to !== finalTo,
|
|
groupCreated,
|
|
isGroupRecipient: finalTo.startsWith("group."),
|
|
}, 'Sending message via API');
|
|
|
|
// Build the message data with optional quote parameters
|
|
const messageData: signalApi.ApiSendMessageV2 = {
|
|
number,
|
|
recipients: [finalTo],
|
|
message,
|
|
};
|
|
|
|
logger.debug({
|
|
number,
|
|
recipients: [finalTo],
|
|
message: message.substring(0, 50) + "...",
|
|
hasQuoteParams: !!(quoteMessage && quoteAuthor && quoteTimestamp),
|
|
}, 'Message data being sent');
|
|
|
|
// Add quote parameters if all are provided
|
|
if (quoteMessage && quoteAuthor && quoteTimestamp) {
|
|
messageData.quoteTimestamp = quoteTimestamp;
|
|
messageData.quoteAuthor = quoteAuthor;
|
|
messageData.quoteMessage = quoteMessage;
|
|
|
|
logger.debug({
|
|
quoteAuthor,
|
|
quoteMessage: quoteMessage.substring(0, 50) + "...",
|
|
quoteTimestamp,
|
|
}, 'Including quote in message');
|
|
}
|
|
|
|
const response = await messagesClient.v2SendPost({
|
|
data: messageData,
|
|
});
|
|
|
|
logger.debug({
|
|
to: finalTo,
|
|
groupCreated,
|
|
response: response?.timestamp || "no timestamp",
|
|
}, 'Message sent successfully');
|
|
} catch (error: any) {
|
|
// Try to get the actual error message from the response
|
|
if (error.response) {
|
|
try {
|
|
const errorBody = await error.response.text();
|
|
logger.error({
|
|
status: error.response.status,
|
|
statusText: error.response.statusText,
|
|
body: errorBody,
|
|
sentTo: finalTo,
|
|
messageDetails: {
|
|
fromNumber: number,
|
|
toRecipients: [finalTo],
|
|
hasQuote: !!quoteMessage,
|
|
},
|
|
}, 'Signal API error');
|
|
} catch (e) {
|
|
logger.error('Could not parse error response');
|
|
}
|
|
}
|
|
logger.error({ error }, 'Full error details');
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export default sendSignalMessageTask;
|