Last active
April 26, 2019 14:10
-
-
Save joebrislin/ef8a4acb2345ef69c5b3 to your computer and use it in GitHub Desktop.
Code Samples - Delayed Job Queue to send out Appointment Reminders (Twilio)
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 < 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 |
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 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 |
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 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