Compare commits

...

9 commits

Author SHA1 Message Date
Darren Clarke
87bb05fdd5 Bump version to 3.4.0-beta.5 2026-01-15 16:51:20 +01:00
Darren Clarke
3d8f794cab Add user ID support for Baileys 7 LIDs and Signal UUIDs
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
2026-01-15 13:08:56 +01:00
Darren Clarke
57d7173485 Bump version to 3.4.0-beta.4 2026-01-14 11:33:11 +01:00
Darren Clarke
8688efc5af Regenerate pnpm-lock.yaml after rebase 2026-01-14 11:02:11 +01:00
Darren Clarke
d6dab5fb1f Build updates 2026-01-14 11:01:31 +01:00
Darren Clarke
7a6e7d0748 Update docker.js 2026-01-14 11:00:48 +01:00
Darren Clarke
57f3ccbaeb Fetch message history at startup 2026-01-14 11:00:41 +01:00
Darren Clarke
e202eeb9d2 Remove deprecated property 2026-01-14 11:00:41 +01:00
Darren Clarke
e952973f7f Update Baileys to 7RC 2026-01-14 11:00:41 +01:00
33 changed files with 1091 additions and 894 deletions

1
.gitignore vendored
View file

@ -31,3 +31,4 @@ project.org
apps/bridge-worker/scripts/*
ENVIRONMENT_VARIABLES_MIGRATION.md
local-scripts/*
docs/

2
.nvmrc
View file

@ -1 +1 @@
v22.18.0
v24

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/bridge-frontend",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"type": "module",
"scripts": {
"dev": "next dev",

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/bridge-migrations",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"type": "module",
"scripts": {
"migrate:up:all": "tsx migrate.ts up:all",

View file

@ -1,17 +1,17 @@
{
"name": "@link-stack/bridge-whatsapp",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"type": "module",
"main": "build/main/index.js",
"author": "Darren Clarke <darren@redaranj.com>",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@adiwajshing/keyed-db": "0.2.4",
"@hapi/hapi": "^21.4.3",
"@hapipal/schmervice": "^3.0.0",
"@hapipal/toys": "^4.0.0",
"@link-stack/bridge-common": "workspace:*",
"@link-stack/logger": "workspace:*",
"@whiskeysockets/baileys": "6.7.21",
"@whiskeysockets/baileys": "7.0.0-rc.9",
"hapi-pino": "^13.0.0",
"link-preview-js": "^3.1.0"
},
@ -19,15 +19,12 @@
"@link-stack/eslint-config": "workspace:*",
"@link-stack/jest-config": "workspace:*",
"@link-stack/typescript-config": "workspace:*",
"@types/long": "^5",
"@types/node": "*",
"dotenv-cli": "^10.0.0",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "dotenv -- tsx src/index.ts",
"dev": "node --env-file=.env --experimental-transform-types src/index.ts",
"start": "node build/main/index.js"
}
}

View file

@ -1,17 +1,17 @@
import * as Hapi from "@hapi/hapi";
import hapiPino from "hapi-pino";
import Schmervice from "@hapipal/schmervice";
import WhatsappService from "./service.js";
import WhatsappService from "./service.ts";
import {
RegisterBotRoute,
UnverifyBotRoute,
GetBotRoute,
SendMessageRoute,
ReceiveMessageRoute,
} from "./routes.js";
} from "./routes.ts";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-whatsapp-index');
const logger = createLogger("bridge-whatsapp-index");
const server = Hapi.server({ port: 5000 });

View file

@ -1,6 +1,6 @@
import * as Hapi from "@hapi/hapi";
import Toys from "@hapipal/toys";
import WhatsappService from "./service";
import WhatsappService from "./service.ts";
const withDefaults = Toys.withRouteDefaults({
options: {
@ -27,15 +27,9 @@ export const SendMessageRoute = withDefaults({
description: "Send a message",
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
const { id } = request.params;
const { phoneNumber, message, attachments } =
request.payload as MessageRequest;
const { phoneNumber, message, attachments } = request.payload as MessageRequest;
const whatsappService = getService(request);
await whatsappService.send(
id,
phoneNumber,
message as string,
attachments,
);
await whatsappService.send(id, phoneNumber, message as string, attachments);
request.logger.info(
{
id,

View file

@ -4,12 +4,13 @@ import makeWASocket, {
DisconnectReason,
proto,
downloadContentFromMessage,
MediaType,
fetchLatestBaileysVersion,
isJidBroadcast,
isJidStatusBroadcast,
useMultiFileAuthState,
} from "@whiskeysockets/baileys";
type MediaType = "audio" | "document" | "image" | "video" | "sticker";
import fs from "fs";
import { createLogger } from "@link-stack/logger";
import {
@ -97,6 +98,7 @@ export default class WhatsappService extends Service {
...options,
auth: state,
generateHighQualityLinkPreview: false,
syncFullHistory: true,
msgRetryCounterMap,
shouldIgnoreJid: (jid) => isJidBroadcast(jid) || isJidStatusBroadcast(jid),
});
@ -147,6 +149,17 @@ export default class WhatsappService extends Service {
await this.queueUnreadMessages(botID, messages);
}
}
if (events["messaging-history.set"]) {
const { messages, isLatest } = events["messaging-history.set"];
logger.info(
{ messageCount: messages.length, isLatest },
"received message history on connection",
);
if (messages.length > 0) {
await this.queueUnreadMessages(botID, messages);
}
}
});
this.connections[botID] = { socket, msgRetryCounterMap };
@ -167,7 +180,6 @@ export default class WhatsappService extends Service {
await this.createConnection(botID, this.server, {
browser: WhatsappService.browserDescription,
printQRInTerminal: true,
version,
});
}
@ -175,18 +187,20 @@ export default class WhatsappService extends Service {
}
private async queueMessage(botID: string, webMessageInfo: proto.IWebMessageInfo) {
const {
key: { id, fromMe, remoteJid },
message,
messageTimestamp,
} = webMessageInfo;
logger.info("Message type debug");
for (const key in message) {
logger.info(
{ key, exists: !!message[key as keyof proto.IMessage] },
"Message field",
);
const { key, message, messageTimestamp } = webMessageInfo;
if (!key) {
logger.warn("Message missing key, skipping");
return;
}
const { id, fromMe, remoteJid } = key;
// 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;
@ -244,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,
@ -410,12 +442,17 @@ export default class WhatsappService extends Service {
}
async receive(
botID: string,
_botID: string,
_lastReceivedDate: Date,
): Promise<proto.IWebMessageInfo[]> {
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"
);
}
}

View file

@ -1,4 +1,4 @@
import type WhatsappService from "./service.js";
import type WhatsappService from "./service.ts";
declare module "@hapipal/schmervice" {
interface SchmerviceDecorator {

View file

@ -1,16 +1,17 @@
{
"extends": "@link-stack/typescript-config/tsconfig.node.json",
"compilerOptions": {
"module": "commonjs",
"target": "es2018",
"module": "NodeNext",
"target": "es2022",
"esModuleInterop": true,
"moduleResolution": "node",
"moduleResolution": "NodeNext",
"outDir": "build/main",
"rootDir": "src",
"skipLibCheck": true,
"types": ["node"],
"lib": ["es2020", "DOM"],
"composite": true
"lib": ["es2022", "DOM"],
"composite": true,
"rewriteRelativeImportExtensions": true
},
"include": ["src/**/*.ts", "src/**/.*.ts"],
"exclude": ["node_modules/**"]

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/bridge-worker",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"type": "module",
"main": "build/main/index.js",
"author": "Darren Clarke <darren@redaranj.com>",

View file

@ -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(),

View file

@ -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,

View file

@ -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,

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/link",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"type": "module",
"scripts": {
"dev": "next dev -H 0.0.0.0",

View file

@ -15,7 +15,7 @@ COPY --from=node /usr/local/lib /usr/local/lib
COPY --from=node /usr/local/include /usr/local/include
COPY --from=node /usr/local/bin /usr/local/bin
# Prepare pnpm (corepack is already enabled in node:22-alpine)
# Prepare pnpm (corepack symlinks already copied from node image)
RUN corepack prepare pnpm@9.15.4 --activate
# Set up pnpm home

View file

@ -5,13 +5,13 @@ const app = process.argv[2];
const command = process.argv[3];
const files = {
all: ["zammad", "postgresql", "bridge", "opensearch", "link", "signal-cli-rest-api"],
all: ["zammad", "postgresql", "bridge", "opensearch", "link", "signal-cli-rest-api", "bridge-whatsapp"],
linkDev: ["zammad", "postgresql", "opensearch"],
link: ["zammad", "postgresql", "opensearch", "link"],
linkOnly: ["link"],
opensearch: ["opensearch"],
bridgeDev: ["zammad", "postgresql", "signal-cli-rest-api"],
bridge: ["zammad", "postgresql", "bridge", "signal-cli-rest-api"],
bridgeDev: ["zammad", "postgresql", "signal-cli-rest-api", "bridge-whatsapp"],
bridge: ["zammad", "postgresql", "bridge", "signal-cli-rest-api", "bridge-whatsapp"],
zammad: ["zammad", "postgresql", "opensearch"],
};
@ -21,7 +21,7 @@ const finalFiles = files[app]
.map((file) => ['-f', `docker/compose/${file}.yml`]).flat();
// Add bridge-dev.yml for dev commands that include zammad
const devAppsWithZammad = ['linkDev', 'bridgeDev', 'all'];
const devAppsWithZammad = ['linkDev', 'bridgeDev'];
if (devAppsWithZammad.includes(app) && files[app].includes('zammad')) {
finalFiles.push('-f', 'docker-compose.bridge-dev.yml');
}

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "Link from the Center for Digital Resilience",
"scripts": {
"dev": "dotenv -- turbo dev",
@ -49,7 +49,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"ts-node": "^10.9.2",
"turbo": "^2.5.8",
"turbo": "^2.6.0",
"typescript": "latest"
},
"pnpm": {

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/bridge-common",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"main": "build/main/index.js",
"type": "module",
"author": "Darren Clarke <darren@redaranj.com>",

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/bridge-ui",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"scripts": {
"build": "tsc -p tsconfig.json"
},

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/eslint-config",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "amigo's eslint config",
"main": "index.js",
"author": "Abel Luck <abel@guardianproject.info>",

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/jest-config",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "",
"main": "index.js",
"author": "Abel Luck <abel@guardianproject.info>",

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/logger",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "Shared logging utility for Link Stack monorepo",
"main": "./dist/index.js",
"module": "./dist/index.mjs",

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/signal-api",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"type": "module",
"main": "build/index.js",
"exports": {

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/typescript-config",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "Shared TypeScript config",
"license": "AGPL-3.0-or-later",
"author": "Abel Luck <abel@guardianproject.info>",

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/ui",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "",
"scripts": {
"build": "tsc -p tsconfig.json"

View file

@ -1,7 +1,7 @@
{
"name": "@link-stack/zammad-addon-bridge",
"displayName": "Bridge",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "An addon that adds CDR Bridge channels to Zammad.",
"scripts": {
"build": "node '../zammad-addon-common/dist/build.js'",

View file

@ -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
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
}
}
}

View file

@ -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
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
}
}
}

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/zammad-addon-common",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "",
"bin": {
"zpm-build": "./dist/build.js",

View file

@ -1,7 +1,7 @@
{
"name": "@link-stack/zammad-addon-hardening",
"displayName": "Hardening",
"version": "3.3.5",
"version": "3.4.0-beta.5",
"description": "A Zammad addon that hardens a Zammad instance according to CDR's needs.",
"scripts": {
"build": "node '../zammad-addon-common/dist/build.js'",

1682
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff