# frozen_string_literal: true class ChannelsCdrWhatsappController < 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: 'Whatsapp::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' if params[:group_id].blank? if errors.present? render json: { errors: errors }, status: :bad_request return end channel = Channel.create( area: 'Whatsapp::Number', options: { adapter: 'cdr_whatsapp', 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' if params[:group_id].blank? if errors.present? render json: { errors: errors }, status: :bad_request return end channel = Channel.find_by(id: params[:id], area: 'Whatsapp::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: 'Whatsapp::Number') channel.options[:token] = SecureRandom.urlsafe_base64(48) channel.save! render json: {} end def enable channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number') channel.active = true channel.save! render json: {} end def disable channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number') channel.active = false channel.save! render json: {} end def destroy channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number') channel.destroy render json: {} end def channel_for_token(token) return false unless token Channel.where(area: 'Whatsapp::Number').each do |channel| return channel if channel.options[:token] == token end false end def webhook token = params['token'] return render json: {}, status: :unauthorized unless token channel = channel_for_token(token) return render json: {}, status: :unauthorized if !channel || !channel.active return render json: {}, status: :unauthorized if channel.options[:token] != token channel_id = channel.id # validate input errors = {} %i[to message_id sent_at].each do |field| errors[field] = 'required' if params[field].blank? end # At least one of from (phone) or user_id must be present if params[:from].blank? && params[:user_id].blank? errors[:from] = 'required (or user_id)' end 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_whatsapp.#{message_id}") receiver_phone_number = params[:to].strip sender_phone_number = params[:from].present? ? params[:from].strip : nil sender_user_id = params[:user_id].present? ? params[:user_id].strip : nil # Lookup customer with fallback chain: # 1. Phone number in phone/mobile fields (preferred) # 2. WhatsApp user ID in whatsapp_user_id field # 3. User ID in phone/mobile fields (legacy - we used to store LIDs there) customer = nil if sender_phone_number.present? customer = User.find_by(phone: sender_phone_number) customer ||= User.find_by(mobile: sender_phone_number) end if customer.nil? && sender_user_id.present? customer = User.find_by(whatsapp_user_id: sender_user_id) # Legacy fallback: user ID might be stored in phone field customer ||= User.find_by(phone: sender_user_id) customer ||= User.find_by(mobile: sender_user_id) end unless customer role_ids = Role.signup_role_ids customer = User.create( firstname: '', lastname: '', email: '', password: '', phone: sender_phone_number.presence || sender_user_id, whatsapp_user_id: sender_user_id, note: 'CDR Whatsapp', active: true, role_ids: role_ids, updated_by_id: 1, created_by_id: 1 ) end # Update whatsapp_user_id if we have it and customer doesn't if sender_user_id.present? && customer.whatsapp_user_id.blank? customer.update(whatsapp_user_id: sender_user_id) end # Update phone if we have it and customer only has user_id in phone field if sender_phone_number.present? && customer.phone == sender_user_id customer.update(phone: sender_phone_number) 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 "Whatsapp channel #{channel_id} paired with Group #{channel.group_id}, but group does not exist!" return render json: { error: 'There was an error during Whatsapp submission' }, status: :internal_server_error end organization_id = channel.options['organization_id'] if organization_id.present? organization = Organization.find_by(id: organization_id) if organization.blank? Rails.logger.error "Whatsapp channel #{channel_id} paired with Organization #{organization_id}, but organization does not exist!" return render json: { error: 'There was an error during Whatsapp submission' }, status: :internal_server_error end if customer.organization_id.blank? 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] sender_display = sender_phone_number.presence || sender_user_id title = "Message from #{sender_display} at #{sent_at}" body = message # find ticket or create one state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id) ticket = Ticket.where(customer_id: customer.id).where.not(state_id: state_ids).order(:updated_at).first 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 ticket = Ticket.new( group_id: channel.group_id, title: title, customer_id: customer.id, preferences: { channel_id: channel.id, cdr_whatsapp: { bot_token: channel.options[:bot_token], chat_id: sender_phone_number.presence || sender_user_id, user_id: sender_user_id } } ) end ticket.save! article_params = { from: sender_display, to: receiver_phone_number, sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id, subject: title, body: body, content_type: 'text/plain', message_id: "cdr_whatsapp.#{message_id}", ticket_id: ticket.id, internal: false, preferences: { cdr_whatsapp: { timestamp: sent_at, message_id: message_id, from: sender_phone_number, user_id: sender_user_id } } } 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 # setting the article type after saving seems to be the only way to get it to stick ticket.with_lock do ta = article_create(ticket, article_params) ta.update!(type_id: Ticket::Article::Type.find_by(name: 'cdr_whatsapp').id) end ticket.update!(create_article_type_id: Ticket::Article::Type.find_by(name: 'cdr_whatsapp').id) result = { ticket: { id: ticket.id, number: ticket.number } } render json: result, status: :ok end end