Add support for signal usernames

This commit is contained in:
Darren Clarke 2026-02-13 11:14:04 +01:00
parent e9afa065b5
commit 1b5f85627c
5 changed files with 88 additions and 17 deletions

View file

@ -10,10 +10,10 @@
<% if @signal_notification_enabled: %> <% if @signal_notification_enabled: %>
<div class="js-signal-phone-container" style="<% if !@signal_has_checked: %>display: none;<% end %>"> <div class="js-signal-phone-container" style="<% if !@signal_has_checked: %>display: none;<% end %>">
<h2><%- @T('Signal Phone Number') %></h2> <h2><%- @T('Signal Recipient') %></h2>
<div class="form-group"> <div class="form-group">
<input type="text" name="signal_uid" class="form-control" value="<%= @signal_uid %>" placeholder="+1234567890"> <input type="text" name="signal_uid" class="form-control" value="<%= @signal_uid %>" placeholder="+1234567890 or u:username.42">
<p class="help-block"><%- @T('Use international format with country code (e.g., +1234567890)') %></p> <p class="help-block"><%- @T('Phone number (+1234567890) or username with u: prefix (u:john.42)') %></p>
</div> </div>
</div> </div>
<% end %> <% end %>

View file

@ -97,16 +97,16 @@ const schema = computed(() => {
}, },
] ]
// CDR Link: Add Signal phone number field only when Signal notifications are enabled // CDR Link: Add Signal recipient field only when Signal notifications are enabled
// AND at least one Signal notification checkbox is selected // AND at least one Signal notification checkbox is selected
if (signalNotificationEnabled.value && showSignalPhoneField.value) { if (signalNotificationEnabled.value && showSignalPhoneField.value) {
baseSchema.push({ baseSchema.push({
type: 'text', type: 'text',
name: 'signal_uid', name: 'signal_uid',
label: __('Signal Phone Number'), label: __('Signal Recipient'),
help: __('Use international format with country code (e.g., +1234567890). Required when Signal notifications are enabled in the matrix.'), help: __('Phone number (+1234567890) or username with u: prefix (u:john.42)'),
props: { props: {
placeholder: '+1234567890', placeholder: '+1234567890 or u:username.42',
}, },
} as any) } as any)
} }

View file

@ -32,6 +32,7 @@ class CreateTicketFromFormJob < ApplicationJob
email = get_field_value(form_data, 'email', mapping) email = get_field_value(form_data, 'email', mapping)
raw_phone = get_field_value(form_data, 'phone', mapping) raw_phone = get_field_value(form_data, 'phone', mapping)
raw_signal_account = get_field_value(form_data, 'signalAccount', mapping) raw_signal_account = get_field_value(form_data, 'signalAccount', mapping)
raw_signal_username = get_field_value(form_data, 'signalUsername', mapping)
organization = get_field_value(form_data, 'organization', mapping) organization = get_field_value(form_data, 'organization', mapping)
type_of_support = get_field_value(form_data, 'typeOfSupport', mapping) type_of_support = get_field_value(form_data, 'typeOfSupport', mapping)
description_of_issue = get_field_value(form_data, 'descriptionOfIssue', mapping) description_of_issue = get_field_value(form_data, 'descriptionOfIssue', mapping)
@ -39,10 +40,11 @@ class CreateTicketFromFormJob < ApplicationJob
# Sanitize phone numbers # Sanitize phone numbers
phone = sanitize_phone_number(raw_phone) phone = sanitize_phone_number(raw_phone)
signal_account = sanitize_phone_number(raw_signal_account) signal_account = sanitize_phone_number(raw_signal_account)
signal_username = normalize_signal_username(raw_signal_username)
# Validate contact info # Validate contact info
unless email.present? || phone.present? || signal_account.present? unless email.present? || phone.present? || signal_account.present? || signal_username.present?
raise 'At least one contact method (email, phone, or signalAccount) is required' raise 'At least one contact method (email, phone, signalAccount, or signalUsername) is required'
end end
# Build ticket title # Build ticket title
@ -60,7 +62,8 @@ class CreateTicketFromFormJob < ApplicationJob
phone: phone, phone: phone,
email: email, email: email,
first_name: first_name, first_name: first_name,
last_name: last_name last_name: last_name,
signal_username: signal_username
) )
Rails.logger.info "Formstack: Using customer #{customer.id} for ticket" Rails.logger.info "Formstack: Using customer #{customer.id} for ticket"
@ -75,18 +78,20 @@ class CreateTicketFromFormJob < ApplicationJob
article_type = Ticket::Article::Type.find_by(name: article_type_name) article_type = Ticket::Article::Type.find_by(name: article_type_name)
# Check for Signal integration # Check for Signal integration
# Use phone number if available, otherwise fall back to username
signal_recipient = signal_account.presence || signal_username
signal_article_type = nil signal_article_type = nil
signal_channel_id = nil signal_channel_id = nil
signal_bot_token = nil signal_bot_token = nil
if signal_account.present? if signal_recipient.present?
signal_channel = Channel.where(area: 'Signal::Number', active: true).first signal_channel = Channel.where(area: 'Signal::Number', active: true).first
if signal_channel if signal_channel
signal_channel_id = signal_channel.id signal_channel_id = signal_channel.id
signal_bot_token = signal_channel.options[:bot_token] signal_bot_token = signal_channel.options[:bot_token]
signal_article_type = Ticket::Article::Type.find_by(name: 'cdr_signal') signal_article_type = Ticket::Article::Type.find_by(name: 'cdr_signal')
Rails.logger.info "Formstack: Found Signal channel #{signal_channel_id} for Signal ticket" Rails.logger.info "Formstack: Found Signal channel #{signal_channel_id} for Signal ticket (recipient: #{signal_recipient})"
end end
end end
@ -102,15 +107,15 @@ class CreateTicketFromFormJob < ApplicationJob
ticket_data.merge!(zammad_fields) ticket_data.merge!(zammad_fields)
# Add Signal preferences if applicable # Add Signal preferences if applicable
if signal_channel_id && signal_bot_token && signal_article_type && signal_account if signal_channel_id && signal_bot_token && signal_article_type && signal_recipient
ticket_data[:preferences] = { ticket_data[:preferences] = {
channel_id: signal_channel_id, channel_id: signal_channel_id,
cdr_signal: { cdr_signal: {
bot_token: signal_bot_token, bot_token: signal_bot_token,
chat_id: signal_account chat_id: signal_recipient
} }
} }
Rails.logger.info "Formstack: Adding Signal preferences to ticket" Rails.logger.info "Formstack: Adding Signal preferences to ticket (chat_id: #{signal_recipient})"
end end
# Create ticket # Create ticket
@ -267,7 +272,7 @@ class CreateTicketFromFormJob < ApplicationJob
nil nil
end end
def find_or_create_customer(phone:, email:, first_name:, last_name:) def find_or_create_customer(phone:, email:, first_name:, last_name:, signal_username: nil)
customer = nil customer = nil
# Try phone first # Try phone first
@ -284,6 +289,11 @@ class CreateTicketFromFormJob < ApplicationJob
end end
end end
# Try Signal username
if customer.nil? && signal_username.present?
customer = User.find_by(signal_username: signal_username)
end
# Create new customer if not found # Create new customer if not found
unless customer unless customer
user_data = { user_data = {
@ -296,14 +306,31 @@ class CreateTicketFromFormJob < ApplicationJob
} }
user_data[:email] = email if email.present? user_data[:email] = email if email.present?
user_data[:phone] = phone if phone.present? user_data[:phone] = phone if phone.present?
user_data[:signal_username] = signal_username if signal_username.present?
customer = User.create!(user_data) customer = User.create!(user_data)
Rails.logger.info "Formstack: Created new customer #{customer.id}" Rails.logger.info "Formstack: Created new customer #{customer.id}"
end end
# Update existing customer if they don't have username
if customer && signal_username.present? && customer.signal_username.blank?
customer.update!(signal_username: signal_username)
Rails.logger.info "Formstack: Updated customer #{customer.id} with signal_username"
end
customer customer
end end
def normalize_signal_username(raw_username)
return nil unless raw_username.present?
username = raw_username.to_s.strip
return nil if username.empty?
# Add u: prefix if not present
username.start_with?('u:') ? username : "u:#{username}"
end
def get_zammad_field_values(form_data, mapping) def get_zammad_field_values(form_data, mapping)
result = {} result = {}
zammad_fields = mapping['zammadFields'] || {} zammad_fields = mapping['zammadFields'] || {}

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
class AddSignalUsername < ActiveRecord::Migration[5.2]
def self.up
add_column :users, :signal_username, :string, limit: 50
add_index :users, :signal_username
User.reset_column_information
ObjectManager::Attribute.add(
force: true,
object: 'User',
name: 'signal_username',
display: 'Signal Username',
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: 721,
created_by_id: 1,
updated_by_id: 1
)
end
def self.down
ObjectManager::Attribute.find_by(name: 'signal_username', object: ObjectLookup.find_by(name: 'User'))&.destroy
remove_column :users, :signal_username
end
end

View file

@ -333,7 +333,7 @@ class CdrSignal
# Use Signal CLI API # Use Signal CLI API
api = CdrSignalApi.new api = CdrSignalApi.new
# Check if we need to create a group (auto-groups enabled, recipient is a phone number) # Check if we need to create a group (auto-groups enabled, recipient is not already a group)
is_group_id = recipient.start_with?('group.') is_group_id = recipient.start_with?('group.')
final_recipient = recipient final_recipient = recipient