Created
January 11, 2019 01:50
-
-
Save sabril/1ee96091d1bd9df19c8e32d2b3c1aeb1 to your computer and use it in GitHub Desktop.
Twilio Phone Number Provision and Release
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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