Signal group and Formstack fixes

This commit is contained in:
Darren Clarke 2025-11-13 10:42:16 +01:00
parent 00d1fe5eef
commit 0e8c9be247
16 changed files with 692 additions and 142 deletions

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class CdrSignalChannelsController < ApplicationController
prepend_before_action -> { authentication_check && authorize! }
def index
channels = Channel.where(area: 'Signal::Number', active: true).map do |channel|
{
id: channel.id,
phone_number: channel.options['phone_number'],
bot_token: channel.options['bot_token'],
bot_endpoint: channel.options['bot_endpoint']
}
end
render json: channels
end
end

View file

@ -122,6 +122,11 @@ class ChannelsCdrSignalController < ApplicationController
return update_group
end
# Handle group member joined events
if params[:event] == 'group_member_joined'
return handle_group_member_joined
end
channel_id = channel.id
# validate input
@ -397,6 +402,10 @@ class ChannelsCdrSignalController < ApplicationController
ticket.preferences[:cdr_signal][:original_recipient] = params[:original_recipient] if params[:original_recipient].present?
ticket.preferences[:cdr_signal][:group_created_at] = params[:timestamp] if params[:timestamp].present?
# Track whether user has joined the group (initially false)
# This will be updated to true when we receive a group join event from Signal
ticket.preferences[:cdr_signal][:group_joined] = params[:group_joined] if params.key?(:group_joined)
ticket.save!
Rails.logger.info "Signal group #{params[:group_id]} associated with ticket #{ticket.id}"
@ -407,4 +416,64 @@ class ChannelsCdrSignalController < ApplicationController
ticket_number: ticket.number
}, status: :ok
end
# Webhook endpoint for receiving group member joined notifications from bridge-worker
# This is called when a user accepts the Signal group invitation
# Expected payload:
# {
# "event": "group_member_joined",
# "group_id": "group.base64encodedid",
# "member_phone": "+1234567890",
# "timestamp": "ISO8601 timestamp"
# }
def handle_group_member_joined
# Validate required parameters
errors = {}
errors['event'] = 'required' unless params[:event].present?
errors['group_id'] = 'required' unless params[:group_id].present?
errors['member_phone'] = 'required' unless params[:member_phone].present?
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
# Find ticket(s) with this group_id in preferences
# Search all active tickets for matching group
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
ticket = Ticket.where.not(state_id: state_ids).find do |t|
t.preferences.is_a?(Hash) &&
t.preferences['cdr_signal'].is_a?(Hash) &&
t.preferences['cdr_signal']['chat_id'] == params[:group_id]
end
unless ticket
Rails.logger.warn "Signal group member joined: Ticket not found for group_id #{params[:group_id]}"
render json: { error: 'Ticket not found for this group' }, status: :not_found
return
end
# Check if the member who joined matches the original recipient
original_recipient = ticket.preferences.dig('cdr_signal', 'original_recipient')
member_phone = params[:member_phone]
# Update group_joined flag
ticket.preferences[:cdr_signal][:group_joined] = true
ticket.preferences[:cdr_signal][:group_joined_at] = params[:timestamp] if params[:timestamp].present?
ticket.preferences[:cdr_signal][:group_joined_by] = member_phone
ticket.save!
Rails.logger.info "Signal group member #{member_phone} joined group #{params[:group_id]} for ticket #{ticket.id}"
render json: {
success: true,
ticket_id: ticket.id,
ticket_number: ticket.number,
group_joined: true
}, status: :ok
end
end

View file

@ -30,6 +30,25 @@ class CommunicateCdrSignalJob < ApplicationJob
log_error(article,
"Can't find ticket.preferences['cdr_signal']['chat_id'] for Ticket.find(#{article.ticket_id})")
end
# Check if this is a group chat and if the user has joined
chat_id = ticket.preferences['cdr_signal']['chat_id']
is_group_chat = chat_id&.start_with?('group.')
group_joined = ticket.preferences.dig('cdr_signal', 'group_joined')
# If this is a group chat and user hasn't joined yet, don't send the message
if is_group_chat && group_joined == false
Rails.logger.info "Ticket ##{ticket.number}: User hasn't joined Signal group yet, skipping message delivery"
# 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
article.save!
# Retry later when user might have joined
raise 'User has not joined Signal group yet'
end
channel = ::CdrSignal.bot_by_bot_token(ticket.preferences['cdr_signal']['bot_token'])
channel ||= ::Channel.lookup(id: ticket.preferences['channel_id'])
unless channel

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Controllers
class CdrSignalChannelsControllerPolicy < Controllers::ApplicationControllerPolicy
def index?
user.permissions?('admin.channel')
end
end
end

View file

@ -0,0 +1,5 @@
Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
match api_path + '/cdr_signal_channels', to: 'cdr_signal_channels#index', via: :get
end

View file

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

View file

@ -0,0 +1,79 @@
# frozen_string_literal: true
# Monkey patch Transaction::Notification to prevent attachments from being
# included in ticket notification emails for security/privacy reasons.
#
# This overrides the send_notification_email method to always pass an empty
# attachments array instead of article.attachments_inline.
module TransactionNotificationNoAttachments
def send_notification_email(user:, ticket:, article:, changes:, current_user:, recipients_reason:)
template = case @item[:type]
when 'create'
'ticket_create'
when 'update'
'ticket_update'
when 'reminder_reached'
'ticket_reminder_reached'
when 'escalation'
'ticket_escalation'
when 'escalation_warning'
'ticket_escalation_warning'
when 'update.merged_into', 'update.received_merge'
'ticket_update_merged'
when 'update.reaction'
'ticket_article_update_reaction'
else
raise "unknown type for notification #{@item[:type]}"
end
# HARDENING: Always use empty attachments array to prevent leaking sensitive files
original_attachment_count = article&.attachments_inline&.count || 0
attachments = []
if original_attachment_count > 0
Rails.logger.info "[HARDENING] Stripped #{original_attachment_count} attachment(s) from notification email for ticket ##{ticket.id}"
end
NotificationFactory::Mailer.notification(
template: template,
user: user,
objects: {
ticket: ticket,
article: article,
recipient: user,
current_user: current_user,
changes: changes,
reason: recipients_reason[user.id],
},
message_id: "<notification.#{DateTime.current.to_fs(:number)}.#{ticket.id}.#{user.id}.#{SecureRandom.uuid}@#{Setting.get('fqdn')}>",
references: ticket.get_references,
main_object: ticket,
attachments: attachments,
)
Rails.logger.debug { "sent ticket email notification to agent (#{@item[:type]}/#{ticket.id}/#{user.email})" }
rescue Channel::DeliveryError => e
status_code = begin
e.original_error.response.status.to_i
rescue
raise e
end
if Transaction::Notification::SILENCABLE_SMTP_ERROR_CODES.any? { |elem| elem.include? status_code }
Rails.logger.info do
"could not send ticket email notification to agent (#{@item[:type]}/#{ticket.id}/#{user.email}) #{e.original_error}"
end
return
end
raise e
end
end
# Apply the monkey patch after Rails initialization when all classes are loaded
Rails.application.config.after_initialize do
Rails.logger.info '[HARDENING] Loading TransactionNotificationNoAttachments monkey patch...'
Transaction::Notification.prepend(TransactionNotificationNoAttachments)
Rails.logger.info '[HARDENING] TransactionNotificationNoAttachments monkey patch successfully applied - email attachments will be stripped from notifications'
end