# frozen_string_literal: true class ChannelsCdrSignalController < ApplicationController prepend_before_action -> { authentication_check && authorize! }, except: [:webhook] skip_before_action :verify_csrf_token, only: [:webhook] include CreatesTicketArticles def index assets = {} channel_ids = [] Channel.where(area: 'Signal::Number').order(:id).each do |channel| assets = channel.assets(assets) channel_ids.push channel.id end render json: { assets: assets, channel_ids: channel_ids } end def add begin errors = {} errors['group_id'] = 'required' unless params[:group_id].present? if errors.present? render json: { errors: errors }, status: :bad_request return end channel = Channel.create( area: 'Signal::Number', options: { adapter: 'cdr_signal', phone_number: params[:phone_number], bot_token: params[:bot_token], bot_endpoint: params[:bot_endpoint], token: SecureRandom.urlsafe_base64(48), organization_id: params[:organization_id] }, group_id: params[:group_id], active: true ) rescue StandardError => e raise Exceptions::UnprocessableEntity, e.message end render json: channel end def update errors = {} errors['group_id'] = 'required' unless params[:group_id].present? if errors.present? render json: { errors: errors }, status: :bad_request return end channel = Channel.find_by(id: params[:id], area: 'Signal::Number') begin channel.options[:phone_number] = params[:phone_number] channel.options[:bot_token] = params[:bot_token] channel.options[:bot_endpoint] = params[:bot_endpoint] channel.options[:organization_id] = params[:organization_id] channel.group_id = params[:group_id] channel.save! rescue StandardError => e raise Exceptions::UnprocessableEntity, e.message end render json: channel end def rotate_token channel = Channel.find_by(id: params[:id], area: 'Signal::Number') channel.options[:token] = SecureRandom.urlsafe_base64(48) channel.save! render json: {} end def enable channel = Channel.find_by(id: params[:id], area: 'Signal::Number') channel.active = true channel.save! render json: {} end def disable channel = Channel.find_by(id: params[:id], area: 'Signal::Number') channel.active = false channel.save! render json: {} end def destroy channel = Channel.find_by(id: params[:id], area: 'Signal::Number') channel.destroy render json: {} end def channel_for_token(token) return false unless token Channel.where(area: 'Signal::Number').each do |channel| return channel if channel.options[:token] == token end false end def webhook token = params['token'] return render json: {}, status: 401 unless token channel = channel_for_token(token) return render json: {}, status: 401 if !channel || !channel.active return render json: {}, status: 401 if channel.options[:token] != token # Handle group creation events if params[:event] == 'group_created' return update_group end channel_id = channel.id # validate input errors = {} # %i[to # from # message_id # sent_at].each | field | # (errors[field] = 'required' if params[field].blank?) if errors.present? render json: { errors: errors }, status: :bad_request return end message_id = params[:message_id] return if Ticket::Article.exists?(message_id: "cdr_signal.#{message_id}") receiver_phone_number = params[:to].strip sender_phone_number = params[:from].strip # Check if this is a group message using the isGroup flag from bridge-worker # This flag is set when: # 1. The original message came from a Signal group # 2. Bridge-worker created a new group for the conversation # Note: We can't rely on UUID format alone because users with privacy settings # may have UUID identifiers instead of phone numbers is_group_message = params[:isGroup] == true || params[:is_group] == true # Group ID is now sent as a separate field to avoid 422 errors # when 'to' contains a UUID instead of a phone number group_id = params[:group_id] || (is_group_message ? params[:to] : nil) customer = User.find_by(phone: sender_phone_number) customer ||= User.find_by(mobile: sender_phone_number) unless customer role_ids = Role.signup_role_ids customer = User.create( firstname: '', lastname: '', email: '', password: '', phone: sender_phone_number, note: 'CDR Signal', active: true, role_ids: role_ids, updated_by_id: 1, created_by_id: 1 ) end # set current user UserInfo.current_user_id = customer.id current_user_set(customer, 'token_auth') group = Group.find_by(id: channel.group_id) if group.blank? Rails.logger.error "Signal channel #{channel_id} paired with Group #{channel.group_id}, but group does not exist!" return render json: { error: 'There was an error during Signal submission' }, status: 500 end organization_id = channel.options['organization_id'] if organization_id.present? organization = Organization.find_by(id: organization_id) unless organization.present? Rails.logger.error "Signal channel #{channel_id} paired with Organization #{organization_id}, but organization does not exist!" return render json: { error: 'There was an error during Signal submission' }, status: 500 end unless customer.organization_id.present? customer.organization_id = organization.id customer.save! end end message = params[:message] ||= 'No text content' sent_at = params[:sent_at] attachment_data_base64 = params[:attachment] attachment_filename = params[:filename] attachment_mimetype = params[:mime_type] title = "Message from #{sender_phone_number} at #{sent_at}" body = message # find ticket or create one state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) # For group messages, find ticket by group_id (either UUID or internal_id) # Signal uses two different IDs for the same group: # - UUID (management ID): returned when creating groups via API # - internal_id (base64): used in message envelopes if is_group_message && group_id Rails.logger.info "=== SIGNAL GROUP TICKET LOOKUP ===" Rails.logger.info "Looking for ticket with group_id: #{group_id}" Rails.logger.info "Customer ID: #{customer.id}" Rails.logger.info "Channel ID: #{channel.id}" begin # Find ticket using Ruby filtering to match group_id or internal_group_id all_tickets = Ticket.where(customer_id: customer.id) .where.not(state_id: state_ids) .order(:updated_at) Rails.logger.info "Found #{all_tickets.count} active tickets for customer" ticket = all_tickets.find do |t| begin has_preferences = t.preferences.is_a?(Hash) has_cdr_signal = has_preferences && t.preferences['cdr_signal'].is_a?(Hash) if has_cdr_signal stored_group_id = t.preferences['cdr_signal']['group_id'] stored_internal_id = t.preferences['cdr_signal']['internal_group_id'] stored_channel_id = t.preferences['channel_id'] Rails.logger.info "Ticket ##{t.number} (ID: #{t.id}):" Rails.logger.info " - stored_group_id: #{stored_group_id}" Rails.logger.info " - stored_internal_id: #{stored_internal_id}" Rails.logger.info " - stored_channel_id: #{stored_channel_id}" Rails.logger.info " - incoming_group_id: #{group_id}" # Match on either the UUID or the internal_id matches = group_id == stored_group_id || group_id == stored_internal_id Rails.logger.info " - MATCH: #{matches}" matches else Rails.logger.info "Ticket ##{t.number} has no cdr_signal preferences" false end rescue => e Rails.logger.error "Error checking ticket #{t.id}: #{e.message}" false end end if ticket Rails.logger.info "=== FOUND MATCHING TICKET: ##{ticket.number} ===" else Rails.logger.info "=== NO MATCHING TICKET FOUND - WILL CREATE NEW ===" end rescue ActiveRecord::StatementInvalid => e Rails.logger.error "Error finding ticket by group_id: #{e.message}" ticket = nil end else Rails.logger.info "Not a group message or no group_id, finding most recent ticket" ticket = Ticket.where(customer_id: customer.id).where.not(state_id: state_ids).order(:updated_at).first end if ticket # check if title need to be updated ticket.title = title if ticket.title == '-' new_state = Ticket::State.find_by(default_create: true) ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id else # Set up chat_id based on whether this is a group message chat_id = is_group_message ? group_id : sender_phone_number # Build preferences with group_id included if needed cdr_signal_prefs = { bot_token: channel.options[:bot_token], # change to bot id chat_id: chat_id } # Add group_id to preferences if this is a group message if is_group_message && group_id cdr_signal_prefs[:group_id] = group_id # Also store internal_group_id if provided cdr_signal_prefs[:internal_group_id] = params[:internal_group_id] if params[:internal_group_id].present? Rails.logger.info "Creating ticket with group preferences: group_id=#{group_id}, internal_group_id=#{params[:internal_group_id]}" end Rails.logger.info "=== CREATING NEW TICKET ===" Rails.logger.info "Preferences to be stored:" Rails.logger.info " - channel_id: #{channel.id}" Rails.logger.info " - cdr_signal: #{cdr_signal_prefs.inspect}" ticket = Ticket.new( group_id: channel.group_id, title: title, customer_id: customer.id, preferences: { channel_id: channel.id, cdr_signal: cdr_signal_prefs } ) end ticket.save! # Check if this is a system message is_system_message = params[:system_message] == true || params[:from] == 'system' article_params = { from: sender_phone_number, to: receiver_phone_number, sender_id: is_system_message ? Ticket::Article::Sender.find_by(name: 'System').id : Ticket::Article::Sender.find_by(name: 'Customer').id, subject: title, body: body, content_type: 'text/plain', message_id: "cdr_signal.#{message_id}", ticket_id: ticket.id, internal: params[:internal] == true || is_system_message, preferences: { cdr_signal: { timestamp: sent_at, message_id: message_id, from: sender_phone_number } } } if attachment_data_base64.present? article_params[:attachments] = [ # i don't even... # this is necessary because of what's going on in controllers/concerns/creates_ticket_articles.rb # we need help from the ruby gods { 'filename' => attachment_filename, :filename => attachment_filename, :data => attachment_data_base64, 'data' => attachment_data_base64, 'mime-type' => attachment_mimetype } ] end ticket.with_lock do ta = article_create(ticket, article_params) # Use 'note' type for system messages, 'cdr_signal' for regular messages article_type = is_system_message ? 'note' : 'cdr_signal' ta.update!(type_id: Ticket::Article::Type.find_by(name: article_type).id) end ticket.update!(create_article_type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id) result = { ticket: { id: ticket.id, number: ticket.number } } render json: result, status: :ok end # Webhook endpoint for receiving group creation notifications from bridge-worker # This is called when a Signal group is created for a conversation # Expected payload: # { # "event": "group_created", # "conversation_id": "ticket_id_or_number", # "original_recipient": "+1234567890", # "group_id": "uuid-of-signal-group", # "timestamp": "ISO8601 timestamp" # } def update_group # Validate required parameters errors = {} errors['event'] = 'required' unless params[:event].present? errors['conversation_id'] = 'required' unless params[:conversation_id].present? errors['group_id'] = 'required' unless params[:group_id].present? if errors.present? render json: { errors: errors }, status: :bad_request return end # Only handle group_created events for now unless params[:event] == 'group_created' render json: { error: 'Unsupported event type' }, status: :bad_request return end # Find the ticket by ID or number # Try to find by both ID and number since ticket numbers can be numeric ticket = Ticket.find_by(id: params[:conversation_id]) || Ticket.find_by(number: params[:conversation_id]) unless ticket Rails.logger.error "Signal group update: Ticket not found for conversation_id #{params[:conversation_id]}" render json: { error: 'Ticket not found' }, status: :not_found return end # Update ticket preferences with the group information ticket.preferences ||= {} ticket.preferences[:cdr_signal] ||= {} ticket.preferences[:cdr_signal][:group_id] = params[:group_id] ticket.preferences[:cdr_signal][:internal_group_id] = params[:internal_group_id] if params[:internal_group_id].present? ticket.preferences[:cdr_signal][:chat_id] = params[:group_id] 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? ticket.save! Rails.logger.info "Signal group #{params[:group_id]} associated with ticket #{ticket.id}" render json: { success: true, ticket_id: ticket.id, ticket_number: ticket.number }, status: :ok end end