Merge feature/split-signal-improvements into combined branch
Combines Signal split/merge improvements with keycloak auth, baileys-7 updates, and signal notifications support. Resolved conflicts: - Kept LID user ID support in bridge-whatsapp - Kept bridge-dev.yml docker compose addition - Used 3.5.0-beta.1 version from split-signal-improvements
This commit is contained in:
commit
38efae02d4
26 changed files with 1604 additions and 24 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/bridge-common",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"main": "build/main/index.js",
|
||||
"type": "module",
|
||||
"author": "Darren Clarke <darren@redaranj.com>",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/bridge-ui",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/eslint-config",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "amigo's eslint config",
|
||||
"main": "index.js",
|
||||
"author": "Abel Luck <abel@guardianproject.info>",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/jest-config",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"author": "Abel Luck <abel@guardianproject.info>",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/logger",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "Shared logging utility for Link Stack monorepo",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/signal-api",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"type": "module",
|
||||
"main": "build/index.js",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/typescript-config",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "Shared TypeScript config",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Abel Luck <abel@guardianproject.info>",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/ui",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@link-stack/zammad-addon-bridge",
|
||||
"displayName": "Bridge",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "An addon that adds CDR Bridge channels to Zammad.",
|
||||
"scripts": {
|
||||
"build": "node '../zammad-addon-common/dist/build.js'",
|
||||
|
|
|
|||
|
|
@ -292,6 +292,11 @@ class ChannelsCdrSignalController < ApplicationController
|
|||
user_id: sender_user_id
|
||||
}
|
||||
|
||||
# Store original recipient phone for group tickets to enable ticket splitting
|
||||
if is_group_message
|
||||
cdr_signal_prefs[:original_recipient] = sender_phone_number
|
||||
end
|
||||
|
||||
Rails.logger.info "=== CREATING NEW TICKET ==="
|
||||
Rails.logger.info "Preferences to be stored:"
|
||||
Rails.logger.info " - channel_id: #{channel.id}"
|
||||
|
|
@ -403,6 +408,24 @@ class ChannelsCdrSignalController < ApplicationController
|
|||
return
|
||||
end
|
||||
|
||||
# Idempotency check: if chat_id is already a group ID, don't overwrite it
|
||||
# This prevents race conditions where multiple group_created webhooks arrive
|
||||
# (e.g., due to retries after API timeouts during group creation)
|
||||
existing_chat_id = ticket.preferences&.dig(:cdr_signal, :chat_id) ||
|
||||
ticket.preferences&.dig('cdr_signal', 'chat_id')
|
||||
if existing_chat_id&.start_with?('group.')
|
||||
Rails.logger.info "Signal group update: Ticket #{ticket.id} already has group #{existing_chat_id}, ignoring new group #{params[:group_id]}"
|
||||
render json: {
|
||||
success: true,
|
||||
skipped: true,
|
||||
reason: 'Ticket already has a group assigned',
|
||||
existing_group_id: existing_chat_id,
|
||||
ticket_id: ticket.id,
|
||||
ticket_number: ticket.number
|
||||
}, status: :ok
|
||||
return
|
||||
end
|
||||
|
||||
# Update ticket preferences with the group information
|
||||
ticket.preferences ||= {}
|
||||
ticket.preferences[:cdr_signal] ||= {}
|
||||
|
|
@ -487,6 +510,36 @@ class ChannelsCdrSignalController < ApplicationController
|
|||
|
||||
Rails.logger.info "Signal group member #{member_phone} joined group #{params[:group_id]} for ticket #{ticket.id}"
|
||||
|
||||
# Check if any articles had a group_not_joined notification and add resolution note
|
||||
# Only add resolution note if we previously notified about the delivery issue
|
||||
articles_with_pending_notification = Ticket::Article.where(ticket_id: ticket.id)
|
||||
.where("preferences LIKE ?", "%group_not_joined_note_added: true%")
|
||||
|
||||
if articles_with_pending_notification.exists?
|
||||
# Check if we already added a resolution note for this ticket
|
||||
resolution_note_exists = Ticket::Article.where(ticket_id: ticket.id)
|
||||
.where("preferences LIKE ?", "%group_joined_resolution: true%")
|
||||
.exists?
|
||||
|
||||
unless resolution_note_exists
|
||||
Ticket::Article.create(
|
||||
ticket_id: ticket.id,
|
||||
content_type: 'text/plain',
|
||||
body: 'Recipient has now joined the Signal group. Pending messages will be delivered shortly.',
|
||||
internal: true,
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'note'),
|
||||
preferences: {
|
||||
delivery_message: true,
|
||||
group_joined_resolution: true,
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
Rails.logger.info "Ticket ##{ticket.number}: Added resolution note about customer joining Signal group"
|
||||
end
|
||||
end
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
ticket_id: ticket.id,
|
||||
|
|
|
|||
|
|
@ -40,10 +40,37 @@ class CommunicateCdrSignalJob < ApplicationJob
|
|||
if is_group_chat && group_joined == false
|
||||
Rails.logger.info "Ticket ##{ticket.number}: User hasn't joined Signal group yet, skipping message delivery"
|
||||
|
||||
# Track group_not_joined retry attempts separately
|
||||
article.preferences['group_not_joined_retry'] ||= 0
|
||||
article.preferences['group_not_joined_retry'] += 1
|
||||
|
||||
# Mark article as pending delivery
|
||||
article.preferences['delivery_status'] = 'pending'
|
||||
article.preferences['delivery_status_message'] = 'Waiting for user to join Signal group'
|
||||
article.preferences['delivery_status_date'] = Time.zone.now
|
||||
|
||||
# After 3 failed attempts, add a note to inform the agent (only once)
|
||||
if article.preferences['group_not_joined_retry'] == 3 && !article.preferences['group_not_joined_note_added']
|
||||
Ticket::Article.create(
|
||||
ticket_id: ticket.id,
|
||||
content_type: 'text/plain',
|
||||
body: 'Unable to send Signal message: Recipient has not yet joined the Signal group. ' \
|
||||
'The message will be delivered automatically once they accept the group invitation.',
|
||||
internal: true,
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'note'),
|
||||
preferences: {
|
||||
delivery_article_id_related: article.id,
|
||||
delivery_message: true,
|
||||
group_not_joined_notification: true,
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
article.preferences['group_not_joined_note_added'] = true
|
||||
Rails.logger.info "Ticket ##{ticket.number}: Added notification note about pending group join"
|
||||
end
|
||||
|
||||
article.save!
|
||||
|
||||
# Retry later when user might have joined
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Link::SetupSplitSignalGroup
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_create :setup_signal_group_for_split_ticket
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_signal_group_for_split_ticket
|
||||
# Only if auto-groups enabled
|
||||
return unless ENV['BRIDGE_SIGNAL_AUTO_GROUPS'].to_s.downcase == 'true'
|
||||
|
||||
# Only child links (splits create child->parent links)
|
||||
return unless link_type_id == Link::Type.find_by(name: 'child')&.id
|
||||
|
||||
# Only Ticket-to-Ticket links
|
||||
ticket_object_id = Link::Object.find_by(name: 'Ticket')&.id
|
||||
return unless link_object_source_id == ticket_object_id
|
||||
return unless link_object_target_id == ticket_object_id
|
||||
|
||||
child_ticket = Ticket.find_by(id: link_object_source_value)
|
||||
parent_ticket = Ticket.find_by(id: link_object_target_value)
|
||||
return unless child_ticket && parent_ticket
|
||||
|
||||
# Only if parent has Signal group (chat_id starts with "group.")
|
||||
parent_signal_prefs = parent_ticket.preferences&.dig('cdr_signal')
|
||||
return unless parent_signal_prefs.present?
|
||||
return unless parent_signal_prefs['chat_id']&.start_with?('group.')
|
||||
|
||||
original_recipient = parent_signal_prefs['original_recipient']
|
||||
return unless original_recipient.present?
|
||||
|
||||
# Set up child for lazy group creation:
|
||||
# chat_id = phone number triggers new group on first message
|
||||
child_ticket.preferences ||= {}
|
||||
child_ticket.preferences['channel_id'] = parent_ticket.preferences['channel_id']
|
||||
child_ticket.preferences['cdr_signal'] = {
|
||||
'bot_token' => parent_signal_prefs['bot_token'],
|
||||
'chat_id' => original_recipient, # Phone number, NOT group ID
|
||||
'original_recipient' => original_recipient
|
||||
}
|
||||
# Set article type so Zammad shows Signal reply option
|
||||
child_ticket.create_article_type_id = Ticket::Article::Type.find_by(name: 'cdr_signal')&.id
|
||||
child_ticket.save!
|
||||
|
||||
Rails.logger.info "Signal split: Ticket ##{child_ticket.number} set up for new group (recipient: #{original_recipient})"
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.config.after_initialize do
|
||||
Rails.application.config.after_initialize do
|
||||
class Ticket::Article
|
||||
include Ticket::Article::EnqueueCommunicateCdrSignalJob
|
||||
end
|
||||
|
||||
# Handle Signal group setup for split tickets
|
||||
class Link
|
||||
include Link::SetupSplitSignalGroup
|
||||
end
|
||||
|
||||
icon = File.read('public/assets/images/icons/cdr_signal.svg')
|
||||
doc = File.open('public/assets/images/icons.svg') { |f| Nokogiri::XML(f) }
|
||||
if !doc.at_css('#icon-cdr-signal')
|
||||
|
|
@ -15,4 +20,3 @@ Rails.application.config.after_initialize do
|
|||
end
|
||||
File.write('public/assets/images/icons.svg', doc.to_xml)
|
||||
end
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@link-stack/zammad-addon-common",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "",
|
||||
"bin": {
|
||||
"zpm-build": "./dist/build.js",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@link-stack/zammad-addon-hardening",
|
||||
"displayName": "Hardening",
|
||||
"version": "3.4.0-beta.7",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "A Zammad addon that hardens a Zammad instance according to CDR's needs.",
|
||||
"scripts": {
|
||||
"build": "node '../zammad-addon-common/dist/build.js'",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue