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: %>
<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">
<input type="text" name="signal_uid" class="form-control" value="<%= @signal_uid %>" placeholder="+1234567890">
<p class="help-block"><%- @T('Use international format with country code (e.g., +1234567890)') %></p>
<input type="text" name="signal_uid" class="form-control" value="<%= @signal_uid %>" placeholder="+1234567890 or u:username.42">
<p class="help-block"><%- @T('Phone number (+1234567890) or username with u: prefix (u:john.42)') %></p>
</div>
</div>
<% 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
if (signalNotificationEnabled.value && showSignalPhoneField.value) {
baseSchema.push({
type: 'text',
name: 'signal_uid',
label: __('Signal Phone Number'),
help: __('Use international format with country code (e.g., +1234567890). Required when Signal notifications are enabled in the matrix.'),
label: __('Signal Recipient'),
help: __('Phone number (+1234567890) or username with u: prefix (u:john.42)'),
props: {
placeholder: '+1234567890',
placeholder: '+1234567890 or u:username.42',
},
} as any)
}

View file

@ -32,6 +32,7 @@ class CreateTicketFromFormJob < ApplicationJob
email = get_field_value(form_data, 'email', mapping)
raw_phone = get_field_value(form_data, 'phone', 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)
type_of_support = get_field_value(form_data, 'typeOfSupport', mapping)
description_of_issue = get_field_value(form_data, 'descriptionOfIssue', mapping)
@ -39,10 +40,11 @@ class CreateTicketFromFormJob < ApplicationJob
# Sanitize phone numbers
phone = sanitize_phone_number(raw_phone)
signal_account = sanitize_phone_number(raw_signal_account)
signal_username = normalize_signal_username(raw_signal_username)
# Validate contact info
unless email.present? || phone.present? || signal_account.present?
raise 'At least one contact method (email, phone, or signalAccount) is required'
unless email.present? || phone.present? || signal_account.present? || signal_username.present?
raise 'At least one contact method (email, phone, signalAccount, or signalUsername) is required'
end
# Build ticket title
@ -60,7 +62,8 @@ class CreateTicketFromFormJob < ApplicationJob
phone: phone,
email: email,
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"
@ -75,18 +78,20 @@ class CreateTicketFromFormJob < ApplicationJob
article_type = Ticket::Article::Type.find_by(name: article_type_name)
# 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_channel_id = nil
signal_bot_token = nil
if signal_account.present?
if signal_recipient.present?
signal_channel = Channel.where(area: 'Signal::Number', active: true).first
if signal_channel
signal_channel_id = signal_channel.id
signal_bot_token = signal_channel.options[:bot_token]
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
@ -102,15 +107,15 @@ class CreateTicketFromFormJob < ApplicationJob
ticket_data.merge!(zammad_fields)
# 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] = {
channel_id: signal_channel_id,
cdr_signal: {
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
# Create ticket
@ -267,7 +272,7 @@ class CreateTicketFromFormJob < ApplicationJob
nil
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
# Try phone first
@ -284,6 +289,11 @@ class CreateTicketFromFormJob < ApplicationJob
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
unless customer
user_data = {
@ -296,14 +306,31 @@ class CreateTicketFromFormJob < ApplicationJob
}
user_data[:email] = email if email.present?
user_data[:phone] = phone if phone.present?
user_data[:signal_username] = signal_username if signal_username.present?
customer = User.create!(user_data)
Rails.logger.info "Formstack: Created new customer #{customer.id}"
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
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)
result = {}
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
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.')
final_recipient = recipient