diff --git a/.gitignore b/.gitignore index 4011990..9b270af 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ project.org apps/bridge-worker/scripts/* ENVIRONMENT_VARIABLES_MIGRATION.md local-scripts/* +docs/ diff --git a/apps/bridge-whatsapp/src/service.ts b/apps/bridge-whatsapp/src/service.ts index 41450cc..43bc205 100644 --- a/apps/bridge-whatsapp/src/service.ts +++ b/apps/bridge-whatsapp/src/service.ts @@ -193,13 +193,14 @@ export default class WhatsappService extends Service { return; } const { id, fromMe, remoteJid } = key; - logger.info("Message type debug"); - for (const key in message) { - logger.info( - { key, exists: !!message[key as keyof proto.IMessage] }, - "Message field", - ); - } + // Baileys 7 uses LIDs (Linked IDs) instead of phone numbers in some cases. + // senderPn contains the actual phone number when available. + const senderPn = (key as any).senderPn as string | undefined; + const participantPn = (key as any).participantPn as string | undefined; + logger.info( + { remoteJid, senderPn, participantPn, fromMe }, + "Processing incoming message", + ); const isValidMessage = message && remoteJid !== "status@broadcast" && !fromMe; if (isValidMessage) { const { audioMessage, documentMessage, imageMessage, videoMessage } = message; @@ -257,9 +258,27 @@ export default class WhatsappService extends Service { videoMessage, ].find((text) => text && text !== ""); + // Extract phone number and user ID (LID) separately + // remoteJid may contain LIDs (Baileys 7+) which are not phone numbers + const jidValue = remoteJid?.split("@")[0]; + const isLidJid = remoteJid?.endsWith("@lid"); + + // Phone number: prefer senderPn/participantPn, fall back to remoteJid only if it's not a LID + const senderPhone = senderPn?.split("@")[0] || participantPn?.split("@")[0] || (!isLidJid ? jidValue : undefined); + + // User ID (LID): extract from remoteJid if it's a LID format + const senderUserId = isLidJid ? jidValue : undefined; + + // Must have at least one identifier + if (!senderPhone && !senderUserId) { + logger.warn({ remoteJid, senderPn, participantPn }, "Could not determine sender identity, skipping message"); + return; + } + const payload = { to: botID, - from: remoteJid?.split("@")[0], + from: senderPhone, + userId: senderUserId, messageId: id, sentAt: new Date((messageTimestamp as number) * 1000).toISOString(), message: messageText, @@ -423,12 +442,17 @@ export default class WhatsappService extends Service { } async receive( - botID: string, + _botID: string, _lastReceivedDate: Date, ): Promise { - const connection = this.connections[botID]?.socket; - const messages = await connection.loadAllUnreadMessages(); - - return messages; + // loadAllUnreadMessages() was removed in Baileys 7.x + // Messages are now delivered via events (messages.upsert, messaging-history.set) + // and forwarded to webhooks automatically. + // See: https://baileys.wiki/docs/migration/to-v7.0.0/ + throw new Error( + "Message polling is no longer supported in Baileys 7.x. " + + "Please configure a webhook to receive messages instead. " + + "Messages are automatically forwarded to BRIDGE_FRONTEND_URL/api/whatsapp/bots/{id}/receive" + ); } } diff --git a/apps/bridge-worker/tasks/fetch-signal-messages.ts b/apps/bridge-worker/tasks/fetch-signal-messages.ts index f4bac04..6769a81 100644 --- a/apps/bridge-worker/tasks/fetch-signal-messages.ts +++ b/apps/bridge-worker/tasks/fetch-signal-messages.ts @@ -168,6 +168,7 @@ const processMessage = async ({ token: id, to: toRecipient, from: source, + userId: sourceUuid, // Signal user UUID for user identification messageId: `${sourceUuid}-${rawTimestamp}`, message: dataMessage?.message, sentAt: timestamp.toISOString(), diff --git a/apps/bridge-worker/tasks/signal/receive-signal-message.ts b/apps/bridge-worker/tasks/signal/receive-signal-message.ts index a504b3c..331f41a 100644 --- a/apps/bridge-worker/tasks/signal/receive-signal-message.ts +++ b/apps/bridge-worker/tasks/signal/receive-signal-message.ts @@ -9,6 +9,7 @@ interface ReceiveSignalMessageTaskOptions { token: string; to: string; from: string; + userId?: string; // Signal user UUID for user identification messageId: string; sentAt: string; message: string; @@ -22,6 +23,7 @@ const receiveSignalMessageTask = async ({ token, to, from, + userId, messageId, sentAt, message, @@ -212,6 +214,7 @@ const receiveSignalMessageTask = async ({ const payload = { to: finalTo, from, + user_id: userId, // Signal user UUID for user identification message_id: messageId, sent_at: sentAt, message, diff --git a/apps/bridge-worker/tasks/whatsapp/receive-whatsapp-message.ts b/apps/bridge-worker/tasks/whatsapp/receive-whatsapp-message.ts index 48adc6f..94cee82 100644 --- a/apps/bridge-worker/tasks/whatsapp/receive-whatsapp-message.ts +++ b/apps/bridge-worker/tasks/whatsapp/receive-whatsapp-message.ts @@ -3,7 +3,8 @@ import { db, getWorkerUtils } from "@link-stack/bridge-common"; interface ReceiveWhatsappMessageTaskOptions { token: string; to: string; - from: string; + from?: string; + userId?: string; messageId: string; sentAt: string; message: string; @@ -16,6 +17,7 @@ const receiveWhatsappMessageTask = async ({ token, to, from, + userId, messageId, sentAt, message, @@ -33,6 +35,7 @@ const receiveWhatsappMessageTask = async ({ const payload = { to, from, + user_id: userId, message_id: messageId, sent_at: sentAt, message, diff --git a/packages/zammad-addon-bridge/src/app/controllers/channels_cdr_signal_controller.rb b/packages/zammad-addon-bridge/src/app/controllers/channels_cdr_signal_controller.rb index f09c458..bd22b09 100644 --- a/packages/zammad-addon-bridge/src/app/controllers/channels_cdr_signal_controller.rb +++ b/packages/zammad-addon-bridge/src/app/controllers/channels_cdr_signal_controller.rb @@ -154,16 +154,31 @@ class ChannelsCdrSignalController < ApplicationController return if Ticket::Article.exists?(message_id: "cdr_signal.#{message_id}") receiver_phone_number = params[:to].strip - sender_phone_number = params[:from].strip + sender_phone_number = params[:from].present? ? params[:from].strip : nil + sender_user_id = params[:user_id].present? ? params[:user_id].strip : nil # Check if this is a group message using the is_group flag from bridge-worker # This flag is set when: # 1. The original message came from a Signal group # 2. Bridge-worker created a new group for the conversation - is_group_message = params[:is_group].to_s == 'true' || params[:is_group].to_s == 'true' + is_group_message = params[:is_group].to_s == 'true' + + # Lookup customer with fallback chain: + # 1. Phone number in phone/mobile fields (preferred) + # 2. Signal user ID in signal_user_id field + # 3. User ID in phone/mobile fields (legacy - we used to store UUIDs there) + customer = nil + if sender_phone_number.present? + customer = User.find_by(phone: sender_phone_number) + customer ||= User.find_by(mobile: sender_phone_number) + end + if customer.nil? && sender_user_id.present? + customer = User.find_by(signal_user_id: sender_user_id) + # Legacy fallback: user ID might be stored in phone field + customer ||= User.find_by(phone: sender_user_id) + customer ||= User.find_by(mobile: sender_user_id) + end - customer = User.find_by(phone: sender_phone_number) - customer ||= User.find_by(mobile: sender_phone_number) unless customer role_ids = Role.signup_role_ids customer = User.create( @@ -171,7 +186,8 @@ class ChannelsCdrSignalController < ApplicationController lastname: '', email: '', password: '', - phone: sender_phone_number, + phone: sender_phone_number.presence || sender_user_id, + signal_user_id: sender_user_id, note: 'CDR Signal', active: true, role_ids: role_ids, @@ -180,6 +196,15 @@ class ChannelsCdrSignalController < ApplicationController ) end + # Update signal_user_id if we have it and customer doesn't + if sender_user_id.present? && customer.signal_user_id.blank? + customer.update(signal_user_id: sender_user_id) + end + # Update phone if we have it and customer only has user_id in phone field + if sender_phone_number.present? && customer.phone == sender_user_id + customer.update(phone: sender_phone_number) + end + # set current user UserInfo.current_user_id = customer.id current_user_set(customer, 'token_auth') @@ -208,7 +233,8 @@ class ChannelsCdrSignalController < ApplicationController attachment_data_base64 = params[:attachment] attachment_filename = params[:filename] attachment_mimetype = params[:mime_type] - title = "Message from #{sender_phone_number} at #{sent_at}" + sender_display = sender_phone_number.presence || sender_user_id + title = "Message from #{sender_display} at #{sent_at}" body = message # find ticket or create one @@ -218,7 +244,7 @@ class ChannelsCdrSignalController < ApplicationController Rails.logger.info "=== SIGNAL GROUP TICKET LOOKUP ===" Rails.logger.info "Looking for ticket with group_id: #{receiver_phone_number}" Rails.logger.info "Customer ID: #{customer.id}" - Rails.logger.info "Customer Phone: #{sender_phone_number}" + Rails.logger.info "Customer Phone: #{sender_display}" Rails.logger.info "Channel ID: #{channel.id}" begin @@ -256,12 +282,13 @@ class ChannelsCdrSignalController < ApplicationController ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id else # Set up chat_id based on whether this is a group message - chat_id = is_group_message ? receiver_phone_number : sender_phone_number + chat_id = is_group_message ? receiver_phone_number : (sender_phone_number.presence || sender_user_id) # Build preferences with group_id included if needed cdr_signal_prefs = { - bot_token: channel.options[:bot_token], # change to bot id - chat_id: chat_id + bot_token: channel.options[:bot_token], + chat_id: chat_id, + user_id: sender_user_id } Rails.logger.info "=== CREATING NEW TICKET ===" @@ -283,7 +310,7 @@ class ChannelsCdrSignalController < ApplicationController ticket.save! article_params = { - from: sender_phone_number, + from: sender_display, to: receiver_phone_number, sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id, subject: title, @@ -296,7 +323,8 @@ class ChannelsCdrSignalController < ApplicationController cdr_signal: { timestamp: sent_at, message_id: message_id, - from: sender_phone_number + from: sender_phone_number, + user_id: sender_user_id } } } diff --git a/packages/zammad-addon-bridge/src/app/controllers/channels_cdr_whatsapp_controller.rb b/packages/zammad-addon-bridge/src/app/controllers/channels_cdr_whatsapp_controller.rb index c30d5ee..bc91676 100644 --- a/packages/zammad-addon-bridge/src/app/controllers/channels_cdr_whatsapp_controller.rb +++ b/packages/zammad-addon-bridge/src/app/controllers/channels_cdr_whatsapp_controller.rb @@ -123,12 +123,16 @@ class ChannelsCdrWhatsappController < ApplicationController errors = {} %i[to - from message_id sent_at].each do |field| errors[field] = 'required' if params[field].blank? end + # At least one of from (phone) or user_id must be present + if params[:from].blank? && params[:user_id].blank? + errors[:from] = 'required (or user_id)' + end + if errors.present? render json: { errors: errors @@ -141,9 +145,25 @@ class ChannelsCdrWhatsappController < ApplicationController return if Ticket::Article.exists?(message_id: "cdr_whatsapp.#{message_id}") receiver_phone_number = params[:to].strip - sender_phone_number = params[:from].strip - customer = User.find_by(phone: sender_phone_number) - customer ||= User.find_by(mobile: sender_phone_number) + sender_phone_number = params[:from].present? ? params[:from].strip : nil + sender_user_id = params[:user_id].present? ? params[:user_id].strip : nil + + # Lookup customer with fallback chain: + # 1. Phone number in phone/mobile fields (preferred) + # 2. WhatsApp user ID in whatsapp_user_id field + # 3. User ID in phone/mobile fields (legacy - we used to store LIDs there) + customer = nil + if sender_phone_number.present? + customer = User.find_by(phone: sender_phone_number) + customer ||= User.find_by(mobile: sender_phone_number) + end + if customer.nil? && sender_user_id.present? + customer = User.find_by(whatsapp_user_id: sender_user_id) + # Legacy fallback: user ID might be stored in phone field + customer ||= User.find_by(phone: sender_user_id) + customer ||= User.find_by(mobile: sender_user_id) + end + unless customer role_ids = Role.signup_role_ids customer = User.create( @@ -151,7 +171,8 @@ class ChannelsCdrWhatsappController < ApplicationController lastname: '', email: '', password: '', - phone: sender_phone_number, + phone: sender_phone_number.presence || sender_user_id, + whatsapp_user_id: sender_user_id, note: 'CDR Whatsapp', active: true, role_ids: role_ids, @@ -160,6 +181,15 @@ class ChannelsCdrWhatsappController < ApplicationController ) end + # Update whatsapp_user_id if we have it and customer doesn't + if sender_user_id.present? && customer.whatsapp_user_id.blank? + customer.update(whatsapp_user_id: sender_user_id) + end + # Update phone if we have it and customer only has user_id in phone field + if sender_phone_number.present? && customer.phone == sender_user_id + customer.update(phone: sender_phone_number) + end + # set current user UserInfo.current_user_id = customer.id current_user_set(customer, 'token_auth') @@ -188,7 +218,8 @@ class ChannelsCdrWhatsappController < ApplicationController attachment_data_base64 = params[:attachment] attachment_filename = params[:filename] attachment_mimetype = params[:mime_type] - title = "Message from #{sender_phone_number} at #{sent_at}" + sender_display = sender_phone_number.presence || sender_user_id + title = "Message from #{sender_display} at #{sent_at}" body = message # find ticket or create one @@ -207,8 +238,9 @@ class ChannelsCdrWhatsappController < ApplicationController preferences: { channel_id: channel.id, cdr_whatsapp: { - bot_token: channel.options[:bot_token], # change to bot id - chat_id: sender_phone_number + bot_token: channel.options[:bot_token], + chat_id: sender_phone_number.presence || sender_user_id, + user_id: sender_user_id } } ) @@ -217,7 +249,7 @@ class ChannelsCdrWhatsappController < ApplicationController ticket.save! article_params = { - from: sender_phone_number, + from: sender_display, to: receiver_phone_number, sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id, subject: title, @@ -230,7 +262,8 @@ class ChannelsCdrWhatsappController < ApplicationController cdr_whatsapp: { timestamp: sent_at, message_id: message_id, - from: sender_phone_number + from: sender_phone_number, + user_id: sender_user_id } } } diff --git a/packages/zammad-addon-bridge/src/db/addon/bridge/20260115000001_add_messaging_user_ids.rb b/packages/zammad-addon-bridge/src/db/addon/bridge/20260115000001_add_messaging_user_ids.rb new file mode 100644 index 0000000..817f9c2 --- /dev/null +++ b/packages/zammad-addon-bridge/src/db/addon/bridge/20260115000001_add_messaging_user_ids.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class AddMessagingUserIds < ActiveRecord::Migration[5.2] + def self.up + # Add WhatsApp user ID field (LID - Linked ID in Baileys 7+) + unless column_exists?(:users, :whatsapp_user_id) + add_column :users, :whatsapp_user_id, :string, limit: 50 + add_index :users, :whatsapp_user_id + end + + # Add Signal user ID field (UUID) + unless column_exists?(:users, :signal_user_id) + add_column :users, :signal_user_id, :string, limit: 50 + add_index :users, :signal_user_id + end + end + + def self.down + remove_index :users, :whatsapp_user_id if index_exists?(:users, :whatsapp_user_id) + remove_column :users, :whatsapp_user_id if column_exists?(:users, :whatsapp_user_id) + + remove_index :users, :signal_user_id if index_exists?(:users, :signal_user_id) + remove_column :users, :signal_user_id if column_exists?(:users, :signal_user_id) + end +end