2024-06-05 15:12:48 +02:00
import { db , getWorkerUtils } from "@link-stack/bridge-common" ;
2025-08-20 11:37:39 +02:00
import { createLogger } from "@link-stack/logger" ;
2025-06-10 14:02:21 +02:00
import * as signalApi from "@link-stack/signal-api" ;
const { Configuration , GroupsApi } = signalApi ;
2024-04-30 13:13:49 +02:00
2025-08-20 11:37:39 +02:00
const logger = createLogger ( 'bridge-worker-receive-signal-message' ) ;
2024-04-30 13:13:49 +02:00
interface ReceiveSignalMessageTaskOptions {
2024-06-05 15:12:48 +02:00
token : string ;
2024-07-18 11:08:01 +02:00
to : string ;
from : string ;
messageId : string ;
sentAt : string ;
2024-06-05 15:12:48 +02:00
message : string ;
2024-07-18 11:08:01 +02:00
attachment? : string ;
filename? : string ;
mimeType? : string ;
2025-06-10 14:02:21 +02:00
isGroup? : boolean ;
2024-04-30 13:13:49 +02:00
}
const receiveSignalMessageTask = async ( {
2024-06-05 15:12:48 +02:00
token ,
2024-07-18 11:08:01 +02:00
to ,
from ,
messageId ,
sentAt ,
2024-04-30 13:13:49 +02:00
message ,
2024-07-18 11:08:01 +02:00
attachment ,
filename ,
mimeType ,
2025-06-10 14:02:21 +02:00
isGroup ,
2024-06-05 15:12:48 +02:00
} : ReceiveSignalMessageTaskOptions ) : Promise < void > = > {
2025-08-20 11:37:39 +02:00
logger . debug ( {
2025-07-07 20:02:54 +02:00
messageId ,
from ,
to ,
isGroup ,
hasMessage : ! ! message ,
hasAttachment : ! ! attachment ,
token ,
2025-08-20 11:37:39 +02:00
} , 'Processing incoming message' ) ;
2024-06-05 15:12:48 +02:00
const worker = await getWorkerUtils ( ) ;
const row = await db
. selectFrom ( "SignalBot" )
. selectAll ( )
. where ( "id" , "=" , token )
. executeTakeFirstOrThrow ( ) ;
const backendId = row . id ;
2025-06-10 14:02:21 +02:00
let finalTo = to ;
2025-07-07 20:02:54 +02:00
let createdInternalId : string | undefined ;
2025-06-10 14:02:21 +02:00
// Check if auto-group creation is enabled and this is NOT already a group message
const enableAutoGroups = process . env . BRIDGE_SIGNAL_AUTO_GROUPS === "true" ;
2025-07-08 18:03:01 +02:00
2025-08-20 11:37:39 +02:00
logger . debug ( {
2025-07-07 20:02:54 +02:00
enableAutoGroups ,
isGroup ,
shouldCreateGroup : enableAutoGroups && ! isGroup && from && to ,
2025-08-20 11:37:39 +02:00
} , 'Auto-groups config' ) ;
2025-06-10 14:02:21 +02:00
2025-07-07 20:02:54 +02:00
// If this is already a group message and auto-groups is enabled,
2025-07-08 18:03:01 +02:00
// 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
2025-08-20 11:37:39 +02:00
logger . debug ( 'Message is from existing group with internal ID' ) ;
2025-07-08 18:03:01 +02:00
finalTo = to ;
2025-07-07 20:02:54 +02:00
} else if ( enableAutoGroups && ! isGroup && from && to ) {
2025-06-10 14:02:21 +02:00
try {
const config = new Configuration ( {
basePath : process.env.BRIDGE_SIGNAL_URL ,
} ) ;
const groupsClient = new GroupsApi ( config ) ;
2025-07-08 18:03:01 +02:00
// Always create a new group for direct messages to the helpdesk
// This ensures each conversation gets its own group/ticket
2025-08-20 11:37:39 +02:00
logger . info ( { from } , 'Creating new group for user' ) ;
2025-07-08 18:03:01 +02:00
// Include timestamp to make each group unique
const timestamp = new Date ( )
. toISOString ( )
. replace ( /[:.]/g , "-" )
. substring ( 0 , 19 ) ;
const groupName = ` Support: ${ from } ( ${ timestamp } ) ` ;
2025-06-10 14:02:21 +02:00
// Create new group for this conversation
const createGroupResponse = await groupsClient . v1GroupsNumberPost ( {
number : row . phoneNumber ,
data : {
name : groupName ,
members : [ from ] ,
description : "Private support conversation" ,
} ,
} ) ;
2025-08-20 11:37:39 +02:00
logger . debug ( { createGroupResponse } , 'Group creation response from Signal API' ) ;
2025-07-08 18:03:01 +02:00
2025-06-10 14:02:21 +02:00
if ( createGroupResponse . id ) {
2025-07-08 18:03:01 +02:00
// 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 {
2025-08-20 11:37:39 +02:00
logger . debug ( 'Fetching group details to get internalId' ) ;
2025-07-08 18:03:01 +02:00
const groups = await groupsClient . v1GroupsNumberGet ( {
number : row . phoneNumber ,
} ) ;
2025-08-20 11:37:39 +02:00
logger . debug ( { groupsSample : groups.slice ( 0 , 3 ) } , 'Groups for bot' ) ;
2025-07-07 20:02:54 +02:00
2025-07-08 18:03:01 +02:00
const createdGroup = groups . find ( ( g ) = > g . id === finalTo ) ;
if ( createdGroup ) {
2025-08-20 11:37:39 +02:00
logger . debug ( { createdGroup } , 'Found created group details' ) ;
2025-07-08 18:03:01 +02:00
}
if ( createdGroup && createdGroup . internalId ) {
createdInternalId = createdGroup . internalId ;
2025-08-20 11:37:39 +02:00
logger . debug ( { createdInternalId } , 'Got actual internalId' ) ;
2025-07-08 18:03:01 +02:00
} else {
// Fallback: extract base64 part from ID
if ( finalTo . startsWith ( "group." ) ) {
createdInternalId = finalTo . substring ( 6 ) ;
2025-07-07 20:02:54 +02:00
}
2025-07-08 18:03:01 +02:00
}
} catch ( fetchError ) {
2025-08-20 11:37:39 +02:00
logger . debug ( 'Could not fetch group details, using ID base64 part' ) ;
2025-07-08 18:03:01 +02:00
// Fallback: extract base64 part from ID
if ( finalTo . startsWith ( "group." ) ) {
createdInternalId = finalTo . substring ( 6 ) ;
}
2025-07-07 20:02:54 +02:00
}
2025-08-20 11:37:39 +02:00
logger . debug ( {
2025-07-08 18:03:01 +02:00
fullGroupId : finalTo ,
internalId : createdInternalId ,
2025-08-20 11:37:39 +02:00
} , 'Group created successfully' ) ;
logger . debug ( {
2025-07-08 18:03:01 +02:00
groupId : finalTo ,
internalId : createdInternalId ,
groupName ,
forPhoneNumber : from ,
botNumber : row.phoneNumber ,
response : createGroupResponse ,
2025-08-20 11:37:39 +02:00
} , 'Created new Signal group' ) ;
2025-07-08 18:03:01 +02:00
}
// Now handle notifications and message forwarding for both new and existing groups
if ( finalTo && finalTo . startsWith ( "group." ) ) {
2025-07-07 20:02:54 +02:00
// Forward the user's initial message to the group using quote feature
try {
2025-08-20 11:37:39 +02:00
logger . debug ( 'Forwarding initial message to group using quote feature' ) ;
2025-07-08 18:03:01 +02:00
2025-07-07 20:02:54 +02:00
const attributionMessage = ` Message from ${ from } : \ n" ${ message } " \ n \ n--- \ nSupport team: Your request has been received. An agent will respond shortly. ` ;
2025-07-08 18:03:01 +02:00
2025-07-07 20:02:54 +02:00
await worker . addJob ( "signal/send-signal-message" , {
2025-07-08 18:03:01 +02:00
token : row.token ,
to : finalTo ,
2025-07-07 20:02:54 +02:00
message : attributionMessage ,
2025-07-08 18:03:01 +02:00
conversationId : null ,
2025-07-07 20:02:54 +02:00
quoteMessage : message ,
quoteAuthor : from ,
2025-07-08 18:03:01 +02:00
quoteTimestamp : Date.parse ( sentAt ) ,
2025-07-07 20:02:54 +02:00
} ) ;
2025-07-08 18:03:01 +02:00
2025-08-20 11:37:39 +02:00
logger . debug ( { finalTo } , 'Successfully forwarded initial message to group' ) ;
2025-07-07 20:02:54 +02:00
} catch ( forwardError ) {
2025-08-20 11:37:39 +02:00
logger . error ( { error : forwardError } , 'Error forwarding message to group' ) ;
2025-07-08 18:03:01 +02:00
}
// Send a response to the original DM informing about the group
try {
2025-08-20 11:37:39 +02:00
logger . debug ( 'Sending group notification to original DM' ) ;
2025-07-08 18:03:01 +02:00
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 ,
} ) ;
2025-08-20 11:37:39 +02:00
logger . debug ( 'Successfully sent group notification to user DM' ) ;
2025-07-08 18:03:01 +02:00
} catch ( dmError ) {
2025-08-20 11:37:39 +02:00
logger . error ( { error : dmError } , 'Error sending DM notification' ) ;
2025-07-07 20:02:54 +02:00
}
}
} catch ( error : any ) {
// Check if error is because group already exists
2025-07-08 18:03:01 +02:00
const errorMessage =
error ? . response ? . data ? . error || error ? . message || error ;
const isAlreadyExists =
errorMessage ? . toString ( ) . toLowerCase ( ) . includes ( "already" ) ||
errorMessage ? . toString ( ) . toLowerCase ( ) . includes ( "exists" ) ;
2025-07-07 20:02:54 +02:00
if ( isAlreadyExists ) {
2025-08-20 11:37:39 +02:00
logger . debug ( { from } , 'Group might already exist, continuing with original recipient' ) ;
2025-07-07 20:02:54 +02:00
} else {
2025-08-20 11:37:39 +02:00
logger . error ( {
2025-07-07 20:02:54 +02:00
error : errorMessage ,
from ,
to ,
botNumber : row.phoneNumber ,
2025-08-20 11:37:39 +02:00
} , 'Error creating Signal group' ) ;
2025-06-10 14:02:21 +02:00
}
}
}
2024-06-05 15:12:48 +02:00
const payload = {
2025-07-08 18:03:01 +02:00
to : finalTo ,
2024-07-18 11:08:01 +02:00
from ,
message_id : messageId ,
sent_at : sentAt ,
2024-06-05 15:12:48 +02:00
message ,
2024-07-18 11:08:01 +02:00
attachment ,
filename ,
mime_type : mimeType ,
2025-07-08 18:03:01 +02:00
is_group : finalTo.startsWith ( "group" ) ,
2024-06-05 15:12:48 +02:00
} ;
await worker . addJob ( "common/notify-webhooks" , { backendId , payload } ) ;
} ;
2024-04-30 13:13:49 +02:00
export default receiveSignalMessageTask ;