Skip to content

Instantly share code, notes, and snippets.

@joebrislin
Last active April 26, 2019 14:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joebrislin/ef8a4acb2345ef69c5b3 to your computer and use it in GitHub Desktop.
Save joebrislin/ef8a4acb2345ef69c5b3 to your computer and use it in GitHub Desktop.
Code Samples - Delayed Job Queue to send out Appointment Reminders (Twilio)
class Appointment < ActiveRecord::Base
include Rails.application.routes.url_helpers
validates :name, presence: true
validates :phone_number, presence: true
validates :time, presence: true
# After initialization, set default values
after_initialize :set_default_values
before_create :confirmation_text
after_create :reminder
belongs_to :user
@@REMINDER_TIME = 5.minutes # delay before sending out reminder
# Add repsonse text if a confirmation is needed
def confirmation_text
if(self.confirm && !self.voice)
self.message = "#{self.message}\n\nPlease reply YES to confirm."
end
end
def set_default_values
self.is_confirmed = 0
self.time = Time.now + @@REMINDER_TIME
end
# Notify our appointment attendee X minutes before the appointment time
def reminder
@client = Twilio::REST::Client.new self.user.twilio_account_sid, self.user.twilio_auth_token
@twilio_from_phone = self.user.twilio_number
@validate_phone = self.from_phone_number.to_s().gsub(' ','').gsub('-', '').gsub('(', '').gsub(')', '').last(10)
@validate_phone.prepend('+1')
if( self.from_phone_number.present? )
# confirm that from_phone_number is valid for twilio account - if not send with default phone number
@client.account.incoming_phone_numbers.list({
:phone_number => @validate_phone }).each do |incoming|
@twilio_from_phone = incoming.phone_number
end
end
if( self.voice ) # Send voice reponse if true
call_send = @client.account.calls.create(
:from => @twilio_from_phone, # From your Twilio number
:to => self.phone_number, # To any number
:IfMachine => "Continue",
# Fetch instructions from this URL when the call connects
:url => root_url + 'api/v1/voice/request/' + self.id.to_s() + '/'
)
update_attribute( :twilio_messagesid, call_send.sid )
puts call_send.to
else # Otherwise send as SMS
message_send = @client.messages.create(
:from => @twilio_from_phone,
:to => self.phone_number,
:body => self.message,
)
update_attribute( :twilio_messagesid, message_send.sid )
puts message_send.to
end
end
def request_voice
reply_url = root_url + api_v1_voice_reply_path[1..-1] + '/'
call_twiml = Twilio::TwiML::Response.new do |r|
r.Say self.message, :voice => 'alice'
if( self.confirm )
r.Gather :numDigits => '1', :method => 'POST', :action => reply_url do |g|
g.Say 'Please press 1 to confirm your appointment.', :voice => 'alice'
end
end
end
call_twiml.text
end
def when_to_run
self.time
end
handle_asynchronously :reminder, :run_at => Proc.new { |i| i.when_to_run }
end
class Api::V1::SmsController < Api::V1::BaseController
include ApplicationHelper
before_filter :find_reminder, only: [:show, :update]
before_filter only: :update do |c|
meth = c.method(:validate_json)
# meth.call (@json.has_key?('reminder') && @json['reminder'].responds_to?(:[]) && @json['reminder']['id'])
meth.call (@json.has_key?('reminder') && @json['reminder']['id'])
end
before_filter only: :create do |c|
meth = c.method(:validate_json)
meth.call (@json.has_key?('reminder'))
end
before_filter only: :create do |c|
meth = c.method(:check_existence)
meth.call(@reminder, "Appointment", "find(@json['reminder']['id'])")
end
before_filter only: :reply do |c|
meth = c.method(:validate_json)
meth.call (params.has_key?('From') && params.has_key?('Body'))
end
def index
render json: reminder.where('owner_id = ?', current_user.id)
end
def show
render json: @reminder
end
def create
if @reminder.present?
render nothing: true, status: :conflict
else
@reminder = Appointment.new
@reminder['voice'] = 0
@reminder['user_id'] = current_user.id
update_values :@reminder, @json['reminder']
end
end
def update
update_values :@reminder, @json['reminder']
end
def reply
begin
# Twilio does not respond with same message id as what was originally sent.
# So we have to assume that the last message with a confirmation sent out with a correlating phone number is the correct record
from_phone = stripPhoneNumber(params[:From])
to_phone = stripPhoneNumber(params[:To])
@reply_reminder = Appointment
.where("replace(replace(replace(replace(phone_number, '-', '') ,'+1','') ,')','') ,'(','') = '#{from_phone}' AND replace(replace(replace(replace(from_phone_number, '-', '') ,'+1','') ,')','') ,'(','')= '#{to_phone}'")
# .where( confirm: true )
.where( voice: false )
.order('id DESC')
.first()
# Send callback url regardless of confirmation
if @reply_reminder.present? && @reply_reminder.callback_url.present?
# Handle whether url already contains query params
if @reply_reminder.callback_url.include? "?"
url_request = @reply_reminder.callback_url + '&reminder_id=' + @reply_reminder.id.to_s() + '&body=' + Rack::Utils.escape(params[:Body])
else
url_request = @reply_reminder.callback_url + '?reminder_id=' + @reply_reminder.id.to_s() + '&body=' + Rack::Utils.escape(params[:Body])
end
# response = HTTParty.get(url_request)
# puts response.body, response.code, response.message
HTTParty.get(url_request)
end
if @reply_reminder.present? && params[:Body].downcase == 'yes'
@reply_reminder.is_confirmed = 1
@reply_reminder.confirmed_at = Time.now
if @reply_reminder.save
render :nothing => true, status: :ok
else
render :nothing => true, status: :unprocessable_entity
end
elsif !@reply_reminder.present?
render nothing: true, status: :bad_request # no record present return bad request
else
render :nothing => true, :status => 200, :content_type => 'text/html'
end
rescue ActiveRecord::RecordNotFound
render nothing: true, status: :bad_request
end
end
private
def find_reminder
@reminder = Appointment.find(params[:id])
render nothing: true, status: :not_found unless @reminder.present? && @reminder.user == current_user
end
end
class Api::V1::VoiceController < Api::V1::BaseController
before_filter :find_reminder, only: [:show, :update]
before_filter only: :update do |c|
meth = c.method(:validate_json)
# meth.call (@json.has_key?('reminder') && @json['reminder'].responds_to?(:[]) && @json['reminder']['id'])
meth.call (@json.has_key?('reminder') && @json['reminder']['id'])
end
before_filter only: :create do |c|
meth = c.method(:validate_json)
meth.call (@json.has_key?('reminder'))
end
before_filter only: :create do |c|
meth = c.method(:check_existence)
meth.call(@reminder, "Appointment", "find(@json['reminder']['id'])")
end
before_filter only: :reply do |c|
meth = c.method(:validate_json)
meth.call (params.has_key?('CallSid') && params.has_key?('Digits'))
end
def index
render json: reminder.where('owner_id = ?', current_user.id)
end
def show
render json: @reminder
end
def create
if @reminder.present?
render nothing: true, status: :conflict
else
@reminder = Appointment.new
@reminder['voice'] = 1
@reminder['user_id'] = current_user.id
update_values :@reminder, @json['reminder']
end
end
def update
update_values :@reminder, @json['reminder']
end
def reply
begin
@reply_reminder = Appointment.find_by(twilio_messagesid: params[:CallSid])
if @reply_reminder.present?
@reply_reminder.assign_attributes( { :is_confirmed => params[:Digits].to_i == 1 ? true : false, :confirmed_at => Time.now } )
if @reply_reminder.save
if @reply_reminder.callback_url.present?
# Handle whether url already contains query params
if @reply_reminder.callback_url.include? "?"
url_request = @reply_reminder.callback_url + '&reminder_id=' + @reply_reminder.id.to_s() + '&body=' + params[:Digits]
else
url_request = @reply_reminder.callback_url + '?reminder_id=' + @reply_reminder.id.to_s() + '&body=' + params[:Digits]
end
response = HTTParty.get(url_request)
puts response.body, response.code, response.message
end
reply_twiml = Twilio::TwiML::Response.new do |r|
r.Say 'Thank you for your response. Goodbye.', :voice => 'alice'
end
render xml: reply_twiml.text, status: :ok
end
end
rescue ActiveRecord::RecordNotFound
render nothing: true, status: :bad_request
end
end
def twiml_request
@twiml_reminder = Appointment.find(params[:id])
if @twiml_reminder.present?
render xml: @twiml_reminder.request_voice, status: :ok
else
render nothing: true, status: :bad_request
end
end
private
def find_reminder
@reminder = Appointment.find(params[:id])
render nothing: true, status: :not_found unless @reminder.present? && @reminder.user == current_user
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment