Skip to content

Instantly share code, notes, and snippets.

@sabril
Created January 11, 2019 01:50
Show Gist options
  • Save sabril/1ee96091d1bd9df19c8e32d2b3c1aeb1 to your computer and use it in GitHub Desktop.
Save sabril/1ee96091d1bd9df19c8e32d2b3c1aeb1 to your computer and use it in GitHub Desktop.
Twilio Phone Number Provision and Release
class Appointment < ApplicationRecord
# .. this model is long and has many lines of code, so we cut it where it's important :-D
phony_normalize :phone, :default_country_code => 'US', :add_plus => true
# Associations
has_many :appointment_call_logs, dependent: :destroy
has_many :call_logs, through: :appointment_call_logs, source: :call_log
has_one :masked_number, as: :callable
#callbacks
after_create :create_masked_phone_number
def phone_formatted
phone.phony_formatted(format: :international, spaces: '-') if phone.present?
end
def masked_phone_number_formatted
masked_phone_number.phony_formatted(format: :international, spaces: '-') if masked_phone_number.present?
end
def needs_masked_number?
phone.present? && appointment_type.present? && appointment_type.requires_phone
end
def create_masked_phone_number
return unless needs_masked_number?
self.masked_phone_number = get_masked_number
self.stored_masked_phone_number = self.masked_phone_number
save
self.create_intro_message
end
def get_masked_number
if is_subscription_appointment?
# Subscription appointment should find existing number assigned to user
masked_number = MaskedNumber.where(user: self.user).first
if masked_number.nil?
# or create new masked number assigned to user
masked_number = MaskedNumber.create(callable: self, active: true, number: TwilioService.provision_phone_number, user: self.user)
end
else
# Regular appointments always get new number
masked_number = MaskedNumber.create(callable: self, active: true, number: TwilioService.provision_phone_number)
end
return masked_number.number
end
def get_user_number(from_user)
if from_user == self.user && self.phone.present?
return self.phone
end
return from_user.get_phone_number
end
def get_user_from_number(number)
return nil if number.blank?
Rails.logger.info(puts "get_user_from_number: #{number}")
number_formatted = Phony.normalize(number)
Rails.logger.info(puts "number_formatted: #{number_formatted}")
return self.user if number_formatted == Phony.normalize(self.phone)
return self.assigned_user if assigned_user.present? && number_formatted == Phony.normalize(get_assigned_user_number)
users.each do |found_user|
number = found_user.try(:phone_number).try(:number)
next if number.blank?
found_number = Phony.normalize(number)
Rails.logger.info(puts "matches? #{found_number}")
if number_formatted == found_number
return found_user
end
end
return nil
end
def forward_sms(incoming_phone, message)
Rails.logger.info(puts "forward_sms #{incoming_phone}, #{message}")
found_user = get_user_from_number(incoming_phone)
Rails.logger.info(puts "found_user #{found_user.inspect}")
appointment_message = self.appointment_messages.build(user: found_user, text: message)
appointment_message.save
Rails.logger.info(puts appointment_message.inspect)
response = Twilio::TwiML::MessagingResponse.new
end
def forward_voice(incoming_number, message)
outgoing_number = outgoing_number(incoming_number)
response = Twilio::TwiML::VoiceResponse.new
response.dial(caller_id: masked_phone_number) do |dial|
dial.number(outgoing_number)
end
end
def outgoing_number(incoming_number)
if incoming_number == self.phone
# The call is coming from the appointment user's number
outgoing_number = get_assigned_user_number
else
# Call is coming from any other number
outgoing_number = phone
end
outgoing_number
end
def get_assigned_user_number
helper = assigned_user
return helper.get_phone_number if helper.get_phone_number.present?
return helper.provider.phone
end
end
class TwilioService
def self.phone_number
Rails.application.secrets.twilio_number
end
def self.client
@@client ||= Twilio::REST::Client.new Rails.application.secrets.twilio_account_sid, Rails.application.secrets.twilio_auth_token
end
def self.send_sms(to_number, body, from_number = nil)
return unless Rails.application.secrets.twilio_enabled
return unless to_number.present? && body.present?
from_number = self.phone_number if from_number.nil?
Rails.logger.info(puts("send_sms to_number: #{to_number}, body: #{body}, from_number: #{from_number}"))
begin
self.client.api.account.messages.create(
from: self.format_number(from_number),
to: self.format_number(to_number),
body: body
)
rescue
Rails.logger.error(" could not send_sms")
end
end
def self.format_number(input)
input.to_s.phony_normalized
end
def self.provision_phone_number
return unless Rails.application.secrets.twilio_enabled
# Twilio test credentials doesn't support phone lookup
if Rails.env.development? || Rails.env.test?
@number = FactoryBot.generate(:phone)
else
# Lookup numbers in host area code, if none than lookup from anywhere
@numbers = self.client.api.available_phone_numbers('US').local.list()
# Purchase the number & set the application_sid for voice and sms, will
# tell the number where to route calls/sms
@number = @numbers.first.phone_number
self.client.api.incoming_phone_numbers.create(
phone_number: @number,
voice_application_sid: Rails.application.secrets.twilio_app_sid,
sms_application_sid: Rails.application.secrets.twilio_app_sid
)
end
@number
end
def self.release_phone_number(phone_number)
return unless Rails.application.secrets.twilio_enabled
Rails.logger.info "TwilioService.release_phone_number: #{phone_number}"
self.client.incoming_phone_numbers.list(phone_number: phone_number).each do |number|
number.delete
end
end
def self.get_call_logs
self.client.calls.list
end
end
User Story:
As a consumer, I want to be able to send and recevie text messages to/from my coach. I don't want to reveal my real phone number and my coach also want that.
Task:
Design the service for above user stories.
Approach:
Each conversation/appointment between user and their coach will have it's own masked number.
This number will be purchased from twilio.
This number will know how to route a message. If user send a message to this number, it will forward the message to their coach. If coach reply to this number, it will forward the reply to user.
Gotchas:
A Number in twilio cost $1 on purchase and $0.5 monthly. That costs will rise if we have big number of users and don't manage it well.
We need to manage the iddle numbers, either we release them or assign to other conversation/appointment if not being used anymore.
The trigger is if conversation/appointment completed.
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
# when receiving call
def appointment_voice
set_twilio_params
response = @appointment&.forward_voice(@incoming_number, @message)
render xml: response.to_s
end
# when receiving sms
def appointment_sms
set_twilio_params
Rails.logger.info(puts "appointment_sms for appointment: #{@appointment.inspect}")
response = @appointment&.forward_sms(@incoming_number, @message)
render xml: response.to_s
end
private
# Load up Twilio parameters
def set_twilio_params
@incoming_number = params[:From]
@message = params[:Body]
masked_phone_number = params[:To].gsub("+", "")
@appointment = Appointment.where("masked_phone_number LIKE ?", "%#{masked_phone_number}%").first
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment