Use _uid instead of _id to please Rails

This commit is contained in:
Darren Clarke 2026-01-19 16:51:51 +01:00
parent 87bb05fdd5
commit ac42d7df78
22 changed files with 151 additions and 50 deletions

1
.gitignore vendored
View file

@ -32,3 +32,4 @@ apps/bridge-worker/scripts/*
ENVIRONMENT_VARIABLES_MIGRATION.md ENVIRONMENT_VARIABLES_MIGRATION.md
local-scripts/* local-scripts/*
docs/ docs/
packages/zammad-addon-bridge/test/

View file

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

View file

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

View file

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

View file

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

View file

@ -64,13 +64,14 @@ const sendSignalMessageTask = async ({
let groupCreated = false; let groupCreated = false;
try { try {
// Check if 'to' is a group ID (UUID format, group.base64 format, or base64) vs phone number // Check if 'to' is a group ID (group.base64 format or base64 internal ID) vs individual recipient
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( // Signal group IDs are 32 bytes = 44 chars base64 (or 43 without padding)
to, // Signal user UUIDs (ACIs) are 36 chars with hyphens: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
); // Phone numbers start with +, usernames with u:, PNIs with PNI:
const isGroupPrefix = to.startsWith("group."); const isGroupPrefix = to.startsWith("group.");
const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(to) && to.length > 20; // Base64 internal_id const isBase64GroupId =
const isGroupId = isUUID || isGroupPrefix || isBase64; /^[A-Za-z0-9+/]+=*$/.test(to) && to.length >= 43 && to.length <= 44;
const isGroupId = isGroupPrefix || isBase64GroupId;
const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true"; const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true";
logger.debug( logger.debug(

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "@link-stack", "name": "@link-stack",
"version": "3.4.0-beta.5", "version": "3.4.0-beta.6",
"description": "Link from the Center for Digital Resilience", "description": "Link from the Center for Digital Resilience",
"scripts": { "scripts": {
"dev": "dotenv -- turbo dev", "dev": "dotenv -- turbo dev",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -165,7 +165,7 @@ class ChannelsCdrSignalController < ApplicationController
# Lookup customer with fallback chain: # Lookup customer with fallback chain:
# 1. Phone number in phone/mobile fields (preferred) # 1. Phone number in phone/mobile fields (preferred)
# 2. Signal user ID in signal_user_id field # 2. Signal user ID in signal_uid field
# 3. User ID in phone/mobile fields (legacy - we used to store UUIDs there) # 3. User ID in phone/mobile fields (legacy - we used to store UUIDs there)
customer = nil customer = nil
if sender_phone_number.present? if sender_phone_number.present?
@ -173,7 +173,7 @@ class ChannelsCdrSignalController < ApplicationController
customer ||= User.find_by(mobile: sender_phone_number) customer ||= User.find_by(mobile: sender_phone_number)
end end
if customer.nil? && sender_user_id.present? if customer.nil? && sender_user_id.present?
customer = User.find_by(signal_user_id: sender_user_id) customer = User.find_by(signal_uid: sender_user_id)
# Legacy fallback: user ID might be stored in phone field # Legacy fallback: user ID might be stored in phone field
customer ||= User.find_by(phone: sender_user_id) customer ||= User.find_by(phone: sender_user_id)
customer ||= User.find_by(mobile: sender_user_id) customer ||= User.find_by(mobile: sender_user_id)
@ -187,7 +187,7 @@ class ChannelsCdrSignalController < ApplicationController
email: '', email: '',
password: '', password: '',
phone: sender_phone_number.presence || sender_user_id, phone: sender_phone_number.presence || sender_user_id,
signal_user_id: sender_user_id, signal_uid: sender_user_id,
note: 'CDR Signal', note: 'CDR Signal',
active: true, active: true,
role_ids: role_ids, role_ids: role_ids,
@ -196,9 +196,9 @@ class ChannelsCdrSignalController < ApplicationController
) )
end end
# Update signal_user_id if we have it and customer doesn't # Update signal_uid if we have it and customer doesn't
if sender_user_id.present? && customer.signal_user_id.blank? if sender_user_id.present? && customer.signal_uid.blank?
customer.update(signal_user_id: sender_user_id) customer.update(signal_uid: sender_user_id)
end end
# Update phone if we have it and customer only has user_id in phone field # 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 if sender_phone_number.present? && customer.phone == sender_user_id
@ -282,7 +282,8 @@ class ChannelsCdrSignalController < ApplicationController
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
else else
# Set up chat_id based on whether this is a group message # Set up chat_id based on whether this is a group message
chat_id = is_group_message ? receiver_phone_number : (sender_phone_number.presence || sender_user_id) # For direct messages, prefer UUID (more stable than phone numbers which can change)
chat_id = is_group_message ? receiver_phone_number : (sender_user_id.presence || sender_phone_number)
# Build preferences with group_id included if needed # Build preferences with group_id included if needed
cdr_signal_prefs = { cdr_signal_prefs = {

View file

@ -150,7 +150,7 @@ class ChannelsCdrWhatsappController < ApplicationController
# Lookup customer with fallback chain: # Lookup customer with fallback chain:
# 1. Phone number in phone/mobile fields (preferred) # 1. Phone number in phone/mobile fields (preferred)
# 2. WhatsApp user ID in whatsapp_user_id field # 2. WhatsApp user ID in whatsapp_uid field
# 3. User ID in phone/mobile fields (legacy - we used to store LIDs there) # 3. User ID in phone/mobile fields (legacy - we used to store LIDs there)
customer = nil customer = nil
if sender_phone_number.present? if sender_phone_number.present?
@ -158,7 +158,7 @@ class ChannelsCdrWhatsappController < ApplicationController
customer ||= User.find_by(mobile: sender_phone_number) customer ||= User.find_by(mobile: sender_phone_number)
end end
if customer.nil? && sender_user_id.present? if customer.nil? && sender_user_id.present?
customer = User.find_by(whatsapp_user_id: sender_user_id) customer = User.find_by(whatsapp_uid: sender_user_id)
# Legacy fallback: user ID might be stored in phone field # Legacy fallback: user ID might be stored in phone field
customer ||= User.find_by(phone: sender_user_id) customer ||= User.find_by(phone: sender_user_id)
customer ||= User.find_by(mobile: sender_user_id) customer ||= User.find_by(mobile: sender_user_id)
@ -172,7 +172,7 @@ class ChannelsCdrWhatsappController < ApplicationController
email: '', email: '',
password: '', password: '',
phone: sender_phone_number.presence || sender_user_id, phone: sender_phone_number.presence || sender_user_id,
whatsapp_user_id: sender_user_id, whatsapp_uid: sender_user_id,
note: 'CDR Whatsapp', note: 'CDR Whatsapp',
active: true, active: true,
role_ids: role_ids, role_ids: role_ids,
@ -181,9 +181,9 @@ class ChannelsCdrWhatsappController < ApplicationController
) )
end end
# Update whatsapp_user_id if we have it and customer doesn't # Update whatsapp_uid if we have it and customer doesn't
if sender_user_id.present? && customer.whatsapp_user_id.blank? if sender_user_id.present? && customer.whatsapp_uid.blank?
customer.update(whatsapp_user_id: sender_user_id) customer.update(whatsapp_uid: sender_user_id)
end end
# Update phone if we have it and customer only has user_id in phone field # 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 if sender_phone_number.present? && customer.phone == sender_user_id

View file

@ -1,25 +1,123 @@
# frozen_string_literal: true
class AddMessagingUserIds < ActiveRecord::Migration[5.2] class AddMessagingUserIds < ActiveRecord::Migration[5.2]
def self.up def self.up
# Add WhatsApp user ID field (LID - Linked ID in Baileys 7+) # Add WhatsApp UID column
unless column_exists?(:users, :whatsapp_user_id) unless column_exists?(:users, :whatsapp_uid)
add_column :users, :whatsapp_user_id, :string, limit: 50 add_column :users, :whatsapp_uid, :string, limit: 50
add_index :users, :whatsapp_user_id add_index :users, :whatsapp_uid
end end
User.reset_column_information
# Add Signal user ID field (UUID) # Add Signal UID column
unless column_exists?(:users, :signal_user_id) unless column_exists?(:users, :signal_uid)
add_column :users, :signal_user_id, :string, limit: 50 add_column :users, :signal_uid, :string, limit: 50
add_index :users, :signal_user_id add_index :users, :signal_uid
end end
User.reset_column_information
# Register WhatsApp UID with ObjectManager for UI
# Column name: whatsapp_uid, Display name: "WhatsApp User ID"
ObjectManager::Attribute.add(
force: true,
object: 'User',
name: 'whatsapp_uid',
display: 'WhatsApp User ID',
data_type: 'input',
data_option: {
type: 'text',
maxlength: 50,
null: true,
item_class: 'formGroup--halfSize',
},
editable: false,
active: true,
screens: {
signup: {},
invite_agent: {},
invite_customer: {},
edit: {
'-all-' => {
null: true,
},
},
create: {
'-all-' => {
null: true,
},
},
view: {
'-all-' => {
shown: true,
},
},
},
to_create: false,
to_migrate: false,
to_delete: false,
position: 710,
created_by_id: 1,
updated_by_id: 1,
)
# Register Signal UID with ObjectManager for UI
# Column name: signal_uid, Display name: "Signal User ID"
ObjectManager::Attribute.add(
force: true,
object: 'User',
name: 'signal_uid',
display: 'Signal User ID',
data_type: 'input',
data_option: {
type: 'text',
maxlength: 50,
null: true,
item_class: 'formGroup--halfSize',
},
editable: false,
active: true,
screens: {
signup: {},
invite_agent: {},
invite_customer: {},
edit: {
'-all-' => {
null: true,
},
},
create: {
'-all-' => {
null: true,
},
},
view: {
'-all-' => {
shown: true,
},
},
},
to_create: false,
to_migrate: false,
to_delete: false,
position: 720,
created_by_id: 1,
updated_by_id: 1,
)
end end
def self.down def self.down
remove_index :users, :whatsapp_user_id if index_exists?(:users, :whatsapp_user_id) ObjectManager::Attribute.remove(
remove_column :users, :whatsapp_user_id if column_exists?(:users, :whatsapp_user_id) object: 'User',
name: 'whatsapp_uid',
)
remove_index :users, :signal_user_id if index_exists?(:users, :signal_user_id) ObjectManager::Attribute.remove(
remove_column :users, :signal_user_id if column_exists?(:users, :signal_user_id) object: 'User',
name: 'signal_uid',
)
remove_index :users, :whatsapp_uid if index_exists?(:users, :whatsapp_uid)
remove_column :users, :whatsapp_uid if column_exists?(:users, :whatsapp_uid)
remove_index :users, :signal_uid if index_exists?(:users, :signal_uid)
remove_column :users, :signal_uid if column_exists?(:users, :signal_uid)
end end
end end

View file

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

View file

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