twilio-rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +21 -0
- data/README.md +413 -0
- data/Rakefile +8 -0
- data/app/assets/config/twilio_rails_manifest.js +1 -0
- data/app/assets/stylesheets/twilio/rails/application.css +15 -0
- data/app/controllers/twilio/rails/application_controller.rb +6 -0
- data/app/controllers/twilio/rails/phone_controller.rb +112 -0
- data/app/controllers/twilio/rails/sms_controller.rb +64 -0
- data/app/helpers/twilio/rails/application_helper.rb +6 -0
- data/app/jobs/twilio/rails/application_job.rb +6 -0
- data/app/jobs/twilio/rails/phone/attach_recording_job.rb +15 -0
- data/app/jobs/twilio/rails/phone/finished_call_job.rb +15 -0
- data/app/jobs/twilio/rails/phone/unanswered_call_job.rb +15 -0
- data/app/mailers/twilio/rails/application_mailer.rb +8 -0
- data/app/models/twilio/rails/application_record.rb +7 -0
- data/app/operations/twilio/rails/application_operation.rb +21 -0
- data/app/operations/twilio/rails/find_or_create_phone_caller_operation.rb +29 -0
- data/app/operations/twilio/rails/phone/attach_recording_operation.rb +31 -0
- data/app/operations/twilio/rails/phone/base_operation.rb +21 -0
- data/app/operations/twilio/rails/phone/create_operation.rb +49 -0
- data/app/operations/twilio/rails/phone/find_operation.rb +14 -0
- data/app/operations/twilio/rails/phone/finished_call_operation.rb +17 -0
- data/app/operations/twilio/rails/phone/receive_recording_operation.rb +35 -0
- data/app/operations/twilio/rails/phone/start_call_operation.rb +53 -0
- data/app/operations/twilio/rails/phone/twiml/after_operation.rb +37 -0
- data/app/operations/twilio/rails/phone/twiml/base_operation.rb +50 -0
- data/app/operations/twilio/rails/phone/twiml/error_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/twiml/greeting_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/twiml/prompt_operation.rb +109 -0
- data/app/operations/twilio/rails/phone/twiml/prompt_response_operation.rb +29 -0
- data/app/operations/twilio/rails/phone/twiml/request_validation_failure_operation.rb +16 -0
- data/app/operations/twilio/rails/phone/twiml/timeout_operation.rb +48 -0
- data/app/operations/twilio/rails/phone/unanswered_call_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/update_operation.rb +26 -0
- data/app/operations/twilio/rails/phone/update_response_operation.rb +38 -0
- data/app/operations/twilio/rails/sms/base_operation.rb +17 -0
- data/app/operations/twilio/rails/sms/create_operation.rb +23 -0
- data/app/operations/twilio/rails/sms/find_message_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/find_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/send_operation.rb +102 -0
- data/app/operations/twilio/rails/sms/twiml/base_operation.rb +11 -0
- data/app/operations/twilio/rails/sms/twiml/error_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/twiml/message_operation.rb +49 -0
- data/app/operations/twilio/rails/sms/update_message_operation.rb +27 -0
- data/app/views/layouts/twilio/rails/application.html.erb +15 -0
- data/config/routes.rb +16 -0
- data/lib/generators/twilio/rails/install/USAGE +15 -0
- data/lib/generators/twilio/rails/install/install_generator.rb +34 -0
- data/lib/generators/twilio/rails/install/templates/initializer.rb +83 -0
- data/lib/generators/twilio/rails/install/templates/message.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/migration.rb +89 -0
- data/lib/generators/twilio/rails/install/templates/phone_call.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/phone_caller.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/recording.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/response.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/sms_conversation.rb +4 -0
- data/lib/generators/twilio/rails/phone_tree/USAGE +8 -0
- data/lib/generators/twilio/rails/phone_tree/phone_tree_generator.rb +12 -0
- data/lib/generators/twilio/rails/phone_tree/templates/tree.rb.erb +13 -0
- data/lib/generators/twilio/rails/sms_responder/USAGE +8 -0
- data/lib/generators/twilio/rails/sms_responder/sms_responder_generator.rb +12 -0
- data/lib/generators/twilio/rails/sms_responder/templates/responder.rb.erb +10 -0
- data/lib/tasks/rails_tasks.rake +45 -0
- data/lib/twilio/rails/client.rb +75 -0
- data/lib/twilio/rails/concerns/has_direction.rb +25 -0
- data/lib/twilio/rails/concerns/has_phone_number.rb +27 -0
- data/lib/twilio/rails/concerns/has_time_scopes.rb +19 -0
- data/lib/twilio/rails/configuration.rb +380 -0
- data/lib/twilio/rails/engine.rb +11 -0
- data/lib/twilio/rails/formatter.rb +93 -0
- data/lib/twilio/rails/models/message.rb +21 -0
- data/lib/twilio/rails/models/phone_call.rb +132 -0
- data/lib/twilio/rails/models/phone_caller.rb +100 -0
- data/lib/twilio/rails/models/recording.rb +27 -0
- data/lib/twilio/rails/models/response.rb +153 -0
- data/lib/twilio/rails/models/sms_conversation.rb +29 -0
- data/lib/twilio/rails/phone/base_tree.rb +229 -0
- data/lib/twilio/rails/phone/tree.rb +229 -0
- data/lib/twilio/rails/phone/tree_macros.rb +147 -0
- data/lib/twilio/rails/phone.rb +12 -0
- data/lib/twilio/rails/phone_number.rb +29 -0
- data/lib/twilio/rails/railtie.rb +17 -0
- data/lib/twilio/rails/sms/delegated_responder.rb +97 -0
- data/lib/twilio/rails/sms/responder.rb +33 -0
- data/lib/twilio/rails/sms.rb +12 -0
- data/lib/twilio/rails/version.rb +5 -0
- data/lib/twilio/rails.rb +89 -0
- metadata +289 -0
@@ -0,0 +1,380 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
class Configuration
|
5
|
+
# Raised in initialization if the configuration is invalid.
|
6
|
+
class Error < StandardError ; end
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@finalized = false
|
10
|
+
@setup = false
|
11
|
+
|
12
|
+
@default_outgoing_phone_number = nil
|
13
|
+
@logger = ::Rails.logger
|
14
|
+
@account_sid = nil
|
15
|
+
@auth_token = nil
|
16
|
+
@spam_filter = nil
|
17
|
+
@exception_notifier = nil
|
18
|
+
@attach_recordings = true
|
19
|
+
@yes_responses = [ "yes", "accept", "ya", "yeah", "true", "ok", "okay" ]
|
20
|
+
@no_responses = [ "no", "naw", "nah", "reject", "decline", "negative", "not", "false" ]
|
21
|
+
@message_class_name = "Message"
|
22
|
+
@message_class = nil
|
23
|
+
@phone_call_class_name = "PhoneCall"
|
24
|
+
@phone_call_class = nil
|
25
|
+
@phone_caller_class_name = "PhoneCaller"
|
26
|
+
@phone_caller_class = nil
|
27
|
+
@sms_conversation_class_name = "SMSConversation"
|
28
|
+
@sms_conversation_class = nil
|
29
|
+
@response_class_name = "Response"
|
30
|
+
@response_class = nil
|
31
|
+
@recording_class_name = "Recording"
|
32
|
+
@recording_class = nil
|
33
|
+
@phone_trees = PhoneTreeRegistry.new
|
34
|
+
@sms_responders = SMSResponderRegistry.new
|
35
|
+
@host = if ::Rails.configuration&.action_controller&.default_url_options
|
36
|
+
"#{ ::Rails.configuration.action_controller.default_url_options[:protocol] }://#{ ::Rails.configuration.action_controller.default_url_options[:host] }"
|
37
|
+
else
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
@controller_http_methods = [:get, :post]
|
41
|
+
@include_phone_macros = []
|
42
|
+
end
|
43
|
+
|
44
|
+
# This is the phone number that will be used to send SMS messages or start Phone Calls. It must be first configured
|
45
|
+
# and purchased in the Twilio dashboard, then entered here. The format must be "+15556667777". In most applications it
|
46
|
+
# is probably the only number, but in more complex applications it is the "main" or default number. It is used when
|
47
|
+
# the phone number is not specified and the number otherwise cannot be intelligently guessed or inferred.
|
48
|
+
#
|
49
|
+
# @return [String] the default outgoing phone number formatted as "+15555555555"
|
50
|
+
attr_accessor :default_outgoing_phone_number
|
51
|
+
|
52
|
+
# The logger used by the framework. Defaults to `Rails.logger`. It cannot be `nil`, so to disable framework
|
53
|
+
# logging explicitly set it to `Logger.new(nil)`.
|
54
|
+
#
|
55
|
+
# @return [Logger] the logger used by the framework.
|
56
|
+
attr_accessor :logger
|
57
|
+
|
58
|
+
# The account SID used to authenticate with Twilio. This should be set from an environment variable or from
|
59
|
+
# somewhere like `Rails.credentials`.
|
60
|
+
#
|
61
|
+
# @return [String] the account SID used to authenticate with Twilio.
|
62
|
+
attr_accessor :account_sid
|
63
|
+
|
64
|
+
# The account auth token used to authenticate with Twilio. his should be set from an environment variable or from
|
65
|
+
# somewhere like `Rails.credentials`.
|
66
|
+
#
|
67
|
+
# @return [String] the account auth token used to authenticate with Twilio.
|
68
|
+
attr_accessor :auth_token
|
69
|
+
|
70
|
+
# Allows SMS messages to be filtered at source if they appear to be spam. This is an optional callable that is run
|
71
|
+
# with raw params from Twilio on each request. If the callable returns `true` it will prevent the message from
|
72
|
+
# being processed. This is useful for filtering out messages that are obviously spam. Setting this to `nil` will
|
73
|
+
# disable the filter and is the default.
|
74
|
+
#
|
75
|
+
# @return [Proc] a proc that will be called to filter messages, or `nil` if no filter is set.
|
76
|
+
attr_accessor :spam_filter
|
77
|
+
|
78
|
+
# A proc that will be called when an exception is raised in certain key points in the framework. This will never
|
79
|
+
# capture the exception, it will raise regardless, but it is a good spot to send an email or notify in chat
|
80
|
+
# if desired. The proc needs to accept `(exception, message, context, exception_binding)` as arguments. The
|
81
|
+
# default is `nil`, which means no action will be taken.
|
82
|
+
#
|
83
|
+
# @return [Proc] a proc that will be called when an exception is raised in certain key points in the framework.
|
84
|
+
attr_accessor :exception_notifier
|
85
|
+
|
86
|
+
# Controls if recordings will be downloaded and attached to the `Recording` model in an ActiveStorage attachment.
|
87
|
+
# This is `true` by default, but can be set to `false` to disable all downloads. It can also be set to a `Proc` or
|
88
|
+
# callable that will receive the `Recording` instance and return a boolean for this specific instance. A typical
|
89
|
+
# usage would be to delegate to the model or a business logic process to determine if the recording should be
|
90
|
+
# downloaded.
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# Twilio::Rails.config.attach_recordings = ->(recording) { recording.should_attach_audio? }
|
94
|
+
#
|
95
|
+
# @return [true, false, Proc] a boolean or a proc that will be called to return a boolean to determine if reordings will be downloaded.
|
96
|
+
attr_accessor :attach_recordings
|
97
|
+
|
98
|
+
# A list of strings to be interpreted as yes or acceptance to a question.
|
99
|
+
#
|
100
|
+
# @return [Array<String>] a list of strings to be interpreted as yes or acceptance to a question.
|
101
|
+
attr_accessor :yes_responses
|
102
|
+
|
103
|
+
# A list of strings to be interpreted as no or rejection to a question.
|
104
|
+
#
|
105
|
+
# @return [Array<String>] a list of strings to be interpreted as no or rejection to a question.
|
106
|
+
attr_accessor :no_responses
|
107
|
+
|
108
|
+
# The name of the model classes, as strings, that this application uses to represent the concepts stored in the DB.
|
109
|
+
# The generators will generate the models with the default names below, but they can be changed as the application
|
110
|
+
# may need.
|
111
|
+
#
|
112
|
+
# @return [String] the name of the model class defined in the Rails application.
|
113
|
+
attr_accessor :phone_caller_class_name, :phone_call_class_name, :response_class_name,
|
114
|
+
:sms_conversation_class_name, :message_class_name, :recording_class_name
|
115
|
+
# @return [Class] the class of the model defined in the Rails application constantized from the string name.
|
116
|
+
attr_reader :phone_caller_class, :phone_call_class, :response_class,
|
117
|
+
:sms_conversation_class, :message_class, :recording_class
|
118
|
+
|
119
|
+
# A registry of phone tree classes that are used to handle incoming phone calls. Calling `register` will add
|
120
|
+
# a responder, and they can be accessed via `all` or `for(name)`. The tree is built by subclassing
|
121
|
+
# `Twilio::Rails::Phone::BaseTree` and defining the tree as described in the documentation.
|
122
|
+
#
|
123
|
+
# @return [PhoneTreeRegistry] a registry of phone tree classes that are used to handle incoming phone calls.
|
124
|
+
attr_reader :phone_trees
|
125
|
+
|
126
|
+
# A registry of SMS responder classes that are used to handle incoming SMS messages. Calling `register` will add
|
127
|
+
# a responder, and they can be accessed via `all` or `for(name)`. The class must either be a subclass
|
128
|
+
# of `Twilio::Rails::SMS::DelegatedResponder` or implement the same interface. Responders are evaluated in the
|
129
|
+
# order they are registered.
|
130
|
+
#
|
131
|
+
# @return [SMSResponderRegistry] a registry of SMS responder classes that are used to handle incoming messages.
|
132
|
+
attr_reader :sms_responders
|
133
|
+
|
134
|
+
# The default protocol and host used to generate URLs for Twilio to call back to. Defaults to what is defined
|
135
|
+
# by `Rails` using `default_url_options`.
|
136
|
+
#
|
137
|
+
# @return [String] the host and protocol where Twilio can reach the application, formatted "https://example.com".
|
138
|
+
attr_reader :host
|
139
|
+
# Sets the host and protocol where Twilio can reach the application, formatted "https://example.com".
|
140
|
+
#
|
141
|
+
# @param value [String] the host and protocol where Twilio can reach the application, formatted "https://example.com".
|
142
|
+
def host=(value)
|
143
|
+
@host = if value.is_a?(String)
|
144
|
+
value.gsub(/\/$/, "")
|
145
|
+
else
|
146
|
+
value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# The {#host} domain name with the protocol stripped, if the host is set.
|
151
|
+
#
|
152
|
+
# @return [String] the {#host} domain name.
|
153
|
+
def host_domain
|
154
|
+
return nil unless host.present?
|
155
|
+
value = host.gsub(/\Ahttps?:\/\//, "")
|
156
|
+
value = value.gsub(/:\d+\z/, "")
|
157
|
+
value
|
158
|
+
end
|
159
|
+
|
160
|
+
# The HTTP methods that Twilio will use to call into the app. Defaults to `[:get, :post]` but can be restricted
|
161
|
+
# to just `[:get]` or `[:post]`. This must match the configuration in the Twilio dashboard.
|
162
|
+
#
|
163
|
+
# @return [Array<Symbol>] the HTTP methods used for the routes that Twilio will use to call into the app.
|
164
|
+
attr_accessor :controller_http_methods
|
165
|
+
|
166
|
+
# Allows adding a module to be included into the `macros` in the phone tree DSL. This is useful for adding
|
167
|
+
# convenience methods specific to the application. It can be called multiple times to add multiple modules.
|
168
|
+
# Built in macros can be seen in {Twilio::Rails::Phone::TreeMacros}.
|
169
|
+
#
|
170
|
+
# @param [Module] mod a module to be included into the `macros` module use in the phone tree DSL.
|
171
|
+
# @return [nil]
|
172
|
+
def include_phone_macros(mod)
|
173
|
+
@include_phone_macros << mod
|
174
|
+
|
175
|
+
if @finalized
|
176
|
+
validate!
|
177
|
+
until @include_phone_macros.empty?
|
178
|
+
Twilio::Rails::Phone::TreeMacros.include(@include_phone_macros.pop)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
nil
|
183
|
+
end
|
184
|
+
|
185
|
+
# Uses the {#attach_recordings} configuration to determine if the recording should be downloaded and attached.
|
186
|
+
#
|
187
|
+
# @return [true, false] If this recording should be downloaded and attached.
|
188
|
+
def attach_recording?(recording)
|
189
|
+
if attach_recordings.is_a?(Proc) || attach_recordings.respond_to?(:call)
|
190
|
+
!!attach_recordings.call(recording)
|
191
|
+
else
|
192
|
+
!!attach_recordings
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Flags that the configuration has been setup and should be validated and finalized.
|
197
|
+
# If this is not called, the framework will not work, but the Railtie will not prevent
|
198
|
+
# the application from starting.
|
199
|
+
#
|
200
|
+
# @return [nil]
|
201
|
+
def setup!
|
202
|
+
@setup = true
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
|
206
|
+
# Validates the configuration and raises an error if it is invalid. This is called after initialization, but is
|
207
|
+
# not the finalized configuration. See {.finalize!} for the last step.
|
208
|
+
#
|
209
|
+
# @return [nil]
|
210
|
+
def validate!
|
211
|
+
return nil unless @setup
|
212
|
+
raise Error, "`default_outgoing_phone_number` must be set" if @default_outgoing_phone_number.blank?
|
213
|
+
raise Error, "`default_outgoing_phone_number` must be a String of the format `\"+12223334444\"`" unless @default_outgoing_phone_number.is_a?(String) && @default_outgoing_phone_number.match?(/\A\+1[0-9]{10}\Z/)
|
214
|
+
raise Error, "`account_sid` must be set" if @account_sid.blank?
|
215
|
+
raise Error, "`auth_token` must be set" if @auth_token.blank?
|
216
|
+
raise Error, "`logger` must be set" if @logger.blank?
|
217
|
+
raise Error, "`spam_filter` must be callable" if @spam_filter && !@spam_filter.respond_to?(:call)
|
218
|
+
raise Error, "`exception_notifier` must be callable" if @exception_notifier && !@exception_notifier.respond_to?(:call)
|
219
|
+
raise Error, '`yes_responses` must be an array' unless @yes_responses.is_a?(Array)
|
220
|
+
raise Error, '`no_responses` must be an array' unless @no_responses.is_a?(Array)
|
221
|
+
raise Error, "`host` #{ @host.inspect } is not a valid URL of the format https://example.com without the trailing slash" unless @host =~ /\Ahttps?:\/\/[a-z0-9\-\.:]+\Z/i
|
222
|
+
raise Error, "`controller_http_methods` must be an array containing one or both of `:get` and `:post` but was #{ @controller_http_methods.inspect }" unless @controller_http_methods.is_a?(Array) && @controller_http_methods.sort == [:get, :post].sort || @controller_http_methods == [:get] || @controller_http_methods == [:post]
|
223
|
+
raise Error, "`include_phone_macros` must be a module, but received #{ @include_phone_macros.inspect }" unless @include_phone_macros.all? { |mod| mod.is_a?(Module) }
|
224
|
+
nil
|
225
|
+
end
|
226
|
+
|
227
|
+
# Finalizes the configuration and makes it ready for use. This is called by the railtie after initialization. It
|
228
|
+
# constantizes and performs the final steps that assumes the whole app has been initalized. Called in `to_prepare`
|
229
|
+
# in the engine, so this is called on every code reload in development mode.
|
230
|
+
#
|
231
|
+
# @return [true]
|
232
|
+
def finalize!
|
233
|
+
return nil unless @setup
|
234
|
+
validate!
|
235
|
+
|
236
|
+
[
|
237
|
+
:phone_caller_class_name,
|
238
|
+
:phone_call_class_name,
|
239
|
+
:response_class_name,
|
240
|
+
:sms_conversation_class_name,
|
241
|
+
:message_class_name,
|
242
|
+
:recording_class_name,
|
243
|
+
].each do |attribute|
|
244
|
+
value = self.send(attribute)
|
245
|
+
raise Error, "`#{attribute}` must be set to a string name" if value.blank? || !value.is_a?(String)
|
246
|
+
begin
|
247
|
+
klass = value.constantize
|
248
|
+
instance_variable_set("@#{ attribute.to_s.gsub("_name", "") }", klass)
|
249
|
+
rescue NameError
|
250
|
+
raise Error, "`#{attribute}` must be a valid class name but could not be found or constantized"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
until @include_phone_macros.empty?
|
255
|
+
Twilio::Rails::Phone::TreeMacros.include(@include_phone_macros.pop)
|
256
|
+
end
|
257
|
+
|
258
|
+
@phone_trees.finalize!
|
259
|
+
@sms_responders.finalize!
|
260
|
+
|
261
|
+
@finalized = true
|
262
|
+
end
|
263
|
+
|
264
|
+
# Base abstract registry class for configuration both phone trees and SMS responders.
|
265
|
+
# @abstract
|
266
|
+
class Registry
|
267
|
+
def initialize
|
268
|
+
@finalized = false
|
269
|
+
@registry = {}.with_indifferent_access
|
270
|
+
@values = []
|
271
|
+
end
|
272
|
+
|
273
|
+
# Finalizes the registry and makes it ready for use. It evaluates the blocks and constantizes the class names.
|
274
|
+
# Looks up the constants each time `to_prepare` is called, so frequently in dev but only once in production.
|
275
|
+
#
|
276
|
+
# @return [true]
|
277
|
+
def finalize!
|
278
|
+
@registry = {}.with_indifferent_access
|
279
|
+
@values.each { |value| add_to_registry(value) }
|
280
|
+
@finalized = true
|
281
|
+
end
|
282
|
+
|
283
|
+
# Registers a phone tree or SMS responder. It accepts a callable, a Class, a String, or a block which returns
|
284
|
+
# any of the aforementioned. The result will all be turned into a class when {#finalize!} is called. This can be
|
285
|
+
# called multiple times.
|
286
|
+
#
|
287
|
+
# @param klass_or_proc [Class, String, Proc] value containing the Class to be lazily initialized when {#finalize!} is called.
|
288
|
+
# @yield [nil] if a block is passed, it will be called and the result will be used as the value.
|
289
|
+
# @yieldreturn [Class, String, Proc] containing the Class to be lazily initialized when {#finalize!} is called.
|
290
|
+
# @return [nil]
|
291
|
+
def register(klass_or_proc=nil, &block)
|
292
|
+
raise Error, "Must pass either a param or a block" unless klass_or_proc.present? ^ block.present?
|
293
|
+
value = klass_or_proc || block
|
294
|
+
|
295
|
+
@values << value
|
296
|
+
add_to_registry(value) if @finalized
|
297
|
+
|
298
|
+
nil
|
299
|
+
end
|
300
|
+
|
301
|
+
# Returns the phone tree or SMS responder for the given name, or raises an error if it is not found.
|
302
|
+
#
|
303
|
+
# @param [String, Symbol] name of the phone tree or SMS responder to find.
|
304
|
+
# @return [Class] the phone tree or SMS responder class.
|
305
|
+
def for(name)
|
306
|
+
@registry[name.to_s] || raise(error_class, "No responder registered for '#{ name }'")
|
307
|
+
end
|
308
|
+
|
309
|
+
# Returns all the phone trees or SMS responders as a read-only hash, keyed by name.
|
310
|
+
#
|
311
|
+
# @return [Hash] all the phone trees or SMS responders.
|
312
|
+
def all
|
313
|
+
@registry.dup.freeze
|
314
|
+
end
|
315
|
+
|
316
|
+
private
|
317
|
+
|
318
|
+
def add_to_registry(value)
|
319
|
+
raise NotImplementedError
|
320
|
+
end
|
321
|
+
|
322
|
+
def error_class
|
323
|
+
StandardError
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Registry class used to store and query SMS responders in the configuration. It is the value
|
328
|
+
# of {Twilio::Rails::Configuration#sms_responders}.
|
329
|
+
class SMSResponderRegistry < Registry
|
330
|
+
private
|
331
|
+
|
332
|
+
def add_to_registry(value)
|
333
|
+
value = value.call if value.respond_to?(:call)
|
334
|
+
begin
|
335
|
+
value = value.constantize if value.is_a?(String)
|
336
|
+
rescue NameError => e
|
337
|
+
raise(error_class, "Responder class '#{ value }' could not be constantized")
|
338
|
+
end
|
339
|
+
raise(error_class, "Responder cannot be blank") unless value.present?
|
340
|
+
raise(error_class, "Responder must be a class but got #{ value.inspect }") unless value.is_a?(Class)
|
341
|
+
name = value.responder_name
|
342
|
+
raise(error_class, "Responder name cannot be blank") unless name.present?
|
343
|
+
raise(error_class, "Responder name '#{ name }' is already registered") if @registry[name]
|
344
|
+
@registry[name] = value
|
345
|
+
end
|
346
|
+
|
347
|
+
def error_class
|
348
|
+
Twilio::Rails::SMS::InvalidResponderError
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Registry class used to store and query phone trees in the configuration. It is the value
|
353
|
+
# of {Twilio::Rails::Configuration#phone_trees}.
|
354
|
+
class PhoneTreeRegistry < Registry
|
355
|
+
private
|
356
|
+
|
357
|
+
def add_to_registry(value)
|
358
|
+
value = value.call if value.respond_to?(:call)
|
359
|
+
begin
|
360
|
+
value = value.constantize if value.is_a?(String)
|
361
|
+
rescue NameError => e
|
362
|
+
raise(error_class, "Tree class '#{ value }' could not be constantized")
|
363
|
+
end
|
364
|
+
raise(error_class, "Tree cannot be blank #{ value }") unless value.present?
|
365
|
+
raise(error_class, "Tree is not a Twilio::Rails::Phone::BaseTree class #{ value }") unless value.is_a?(Class)
|
366
|
+
raise(error_class, "Tree is not a Twilio::Rails::Phone::BaseTree #{ value }") unless value.ancestors.include?(Twilio::Rails::Phone::BaseTree)
|
367
|
+
name = value.tree_name
|
368
|
+
raise(error_class, "Tree name cannot be blank") unless name.present?
|
369
|
+
raise(error_class, "Tree name '#{ name }' is already registered") if @registry[name]
|
370
|
+
klass = klass.constantize if klass.is_a?(String)
|
371
|
+
@registry[name] = value.tree
|
372
|
+
end
|
373
|
+
|
374
|
+
def error_class
|
375
|
+
Twilio::Rails::Phone::InvalidTreeError
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Formatter
|
5
|
+
extend self
|
6
|
+
|
7
|
+
PHONE_NUMBER_REGEX = /\A\+1[0-9]{10}\Z/
|
8
|
+
PHONE_NUMBER_SEGMENTS_REGEX = /\A\+1([0-9]{3})([0-9]{3})([0-9]{4})\Z/
|
9
|
+
|
10
|
+
# Takes in a string or a {Twilio::Rails::PhoneNumber} or something that responds to `to_s` and turns it into a
|
11
|
+
# consistently formatted valid north american 10 digit phone number prefixed with 1 and plus. It uses the format
|
12
|
+
# Twilio expects which is "+15555555555" or returns `nil` if it cannot be coerced.
|
13
|
+
#
|
14
|
+
# @param string [String, Twilio::Rails::PhoneNumber, nil, Object] the input to turn into a phone number string.
|
15
|
+
# @return [String, nil] the phone number string or nil.
|
16
|
+
def coerce_to_valid_phone_number(string)
|
17
|
+
string = string.number if string.is_a?(Twilio::Rails::PhoneNumber)
|
18
|
+
string = string.to_s.presence
|
19
|
+
|
20
|
+
if string
|
21
|
+
string = string.gsub(/[^0-9]/, "")
|
22
|
+
string = "1#{ string }" unless string.starts_with?("1")
|
23
|
+
string = "+#{ string }"
|
24
|
+
string = nil unless valid_north_american_phone_number?(string)
|
25
|
+
end
|
26
|
+
|
27
|
+
string
|
28
|
+
end
|
29
|
+
|
30
|
+
# Takes in a string or a {Twilio::Rails::PhoneNumber} or something that responds to `to_s` and validates it
|
31
|
+
# matches the expected format "+15555555555" of a north american phone number.
|
32
|
+
#
|
33
|
+
# @param phone_number [String, Twilio::Rails::PhoneNumber, nil] the input to validate as a phone number.
|
34
|
+
# @return [true, false]
|
35
|
+
def valid_north_american_phone_number?(phone_number)
|
36
|
+
phone_number = phone_number.number if phone_number.is_a?(Twilio::Rails::PhoneNumber)
|
37
|
+
!!phone_number&.match?(PHONE_NUMBER_REGEX)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Takes in a string or a {Twilio::Rails::PhoneNumber} or something that responds to `to_s` and turns it into
|
41
|
+
# a phone number formatted for URLs. Appropriate to use for `#to_param` in Rails or other controller concerns
|
42
|
+
# where a phone number or phone caller can be passed around as a URL parameter.
|
43
|
+
#
|
44
|
+
# @param phone_number [String, Twilio::Rails::PhoneNumber, nil] the input to turn into a phone number string.
|
45
|
+
# @return [String] the phone number string or empty string if invalid.
|
46
|
+
def to_phone_number_url_param(phone_number)
|
47
|
+
phone_number = coerce_to_valid_phone_number(phone_number)
|
48
|
+
return "" unless phone_number
|
49
|
+
matches = phone_number.match(PHONE_NUMBER_SEGMENTS_REGEX)
|
50
|
+
raise Twilio::Rails::Error, "[to_phone_number_url_param] Phone number marked as valid but could not capture. I made a bad regex: #{ phone_number }" unless matches
|
51
|
+
matches.captures.join("-")
|
52
|
+
end
|
53
|
+
|
54
|
+
# Takes in a string or a {Twilio::Rails::PhoneNumber} or something that responds to `to_s` and turns it into a
|
55
|
+
# phone number string formatted for display. If the number cannot be coerced to a valid phone number it will be
|
56
|
+
# passed through.
|
57
|
+
#
|
58
|
+
# @param phone_number [String, Twilio::Rails::PhoneNumber, nil] the input to turn into a phone number string.
|
59
|
+
# @return [String, Object] the phone number string or the original object if invalid.
|
60
|
+
def display_phone_number(phone_number)
|
61
|
+
coerced_phone_number = coerce_to_valid_phone_number(phone_number)
|
62
|
+
if coerced_phone_number
|
63
|
+
matches = coerced_phone_number.match(PHONE_NUMBER_SEGMENTS_REGEX)
|
64
|
+
raise Twilio::Rails::Error, "[display_phone_number] Phone number marked as valid but could not capture. I made a bad regex: #{ phone_number }" unless matches
|
65
|
+
"(#{ matches.captures[0] }) #{ matches.captures[1] } #{ matches.captures[2] }"
|
66
|
+
else
|
67
|
+
phone_number
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Formats a city, province, and country into a single string, correctly handling blanks, and formatting countries.
|
72
|
+
#
|
73
|
+
# @param city [String, nil] the city name.
|
74
|
+
# @param province [String, nil] the province name.
|
75
|
+
# @param country [String, nil] the country code.
|
76
|
+
# @return [String] the formatted location string.
|
77
|
+
def location(city: nil, country: nil, province: nil)
|
78
|
+
country_name = case country
|
79
|
+
when "CA" then "Canada"
|
80
|
+
when "US" then "USA"
|
81
|
+
else
|
82
|
+
country
|
83
|
+
end
|
84
|
+
|
85
|
+
[
|
86
|
+
city.presence&.titleize,
|
87
|
+
province,
|
88
|
+
country_name,
|
89
|
+
].reject(&:blank?).join(", ")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Models
|
5
|
+
# A message sent or received via SMS. Belongs to a {Twilio::Rails::Models::SmsConversation}. Has a direction to
|
6
|
+
# indicate whether it was sent or received.
|
7
|
+
module Message
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
include Twilio::Rails::HasDirection
|
12
|
+
include Twilio::Rails::HasTimeScopes
|
13
|
+
|
14
|
+
belongs_to :sms_conversation, class_name: Twilio::Rails.config.sms_conversation_class_name
|
15
|
+
|
16
|
+
scope :in_order, -> { reorder(created_at: :asc) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Models
|
5
|
+
# The record of a phone call. Can be inbound or outbound. The associated {Twilio::Rails::Models::Response} objects
|
6
|
+
# in order track the progress of the call.
|
7
|
+
module PhoneCall
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
include Twilio::Rails::HasDirection
|
12
|
+
include Twilio::Rails::HasTimeScopes
|
13
|
+
|
14
|
+
validates :sid, presence: true
|
15
|
+
validates_associated :phone_caller
|
16
|
+
|
17
|
+
belongs_to :phone_caller, class_name: Twilio::Rails.config.phone_caller_class_name
|
18
|
+
|
19
|
+
has_many :responses, -> { order(created_at: :asc) }, dependent: :destroy, class_name: Twilio::Rails.config.response_class_name
|
20
|
+
has_many :recordings, -> { order(created_at: :asc) }, dependent: :destroy, class_name: Twilio::Rails.config.recording_class_name
|
21
|
+
|
22
|
+
scope :recent, -> { reorder(created_at: :desc).limit(10) }
|
23
|
+
scope :tree, ->(name) { where(tree_name: name) }
|
24
|
+
scope :called_today, -> { where("created_at > ?", Time.now - 1.day).includes(:phone_caller).order(created_at: :asc) }
|
25
|
+
scope :in_progress, -> { where(call_status: "in-progress") }
|
26
|
+
scope :finished, -> { where(finished: true) }
|
27
|
+
scope :unfinished, -> { where(finished: false) }
|
28
|
+
|
29
|
+
after_save :status_callback
|
30
|
+
end
|
31
|
+
|
32
|
+
# All possible call statuses:
|
33
|
+
# "queued", "initiated", "ringing", "in-progress", "completed", "canceled", "busy", "no-answer", "failed"
|
34
|
+
|
35
|
+
class_methods do
|
36
|
+
# Returns the number of unique callers for a given tree.
|
37
|
+
#
|
38
|
+
# @param tree [String, Twilio::Rails::Phone::Tree, String, Symbol] The tree or name of the tree.
|
39
|
+
# @return [Integer] The number of unique callers for the tree.
|
40
|
+
def caller_count_for_tree(tree)
|
41
|
+
tree = tree.is_a?(Twilio::Rails::Phone::Tree) ? tree.name : tree
|
42
|
+
tree(tree).pluck(:phone_caller_id).uniq.count
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Indicates if the call was answered by an answering machine. Only will return true if answering machine
|
47
|
+
# detection is enabled. Is always false for inbound calls.
|
48
|
+
#
|
49
|
+
# @return [true, false] true if the call was answered by an answering machine.
|
50
|
+
def answering_machine?
|
51
|
+
outbound? && answered_by == "machine_start"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Indicates if the call was not answered, busy, or failed. Is always false for inbound calls.
|
55
|
+
#
|
56
|
+
# @return [true, false] true if the call was not answered by a person.
|
57
|
+
def no_answer?
|
58
|
+
outbound? && call_status.in?(["busy", "failed", "no-answer"])
|
59
|
+
end
|
60
|
+
|
61
|
+
# Checks if the call is in the completed state. This does not cover all possible states for a call that is not
|
62
|
+
# in progress. See {#in_progress?} to check if a call is finished or not.
|
63
|
+
#
|
64
|
+
# @return [true, false] true if the call is in the completed state.
|
65
|
+
def completed?
|
66
|
+
call_status.in?(["completed"])
|
67
|
+
end
|
68
|
+
|
69
|
+
# Checks if that call is in a state where it is currently in progress with the caller. This includes ringing,
|
70
|
+
# queued, initiated, or in progress. Use this method to check if the call has finished or not.
|
71
|
+
#
|
72
|
+
# @return [true, false] true if the call is currently ringing, queued, or in progress.
|
73
|
+
def in_progress?
|
74
|
+
call_status.blank? || call_status.in?(["queued", "initiated", "ringing", "in-progress"])
|
75
|
+
end
|
76
|
+
|
77
|
+
# A formatted string for the location data of the caller provided by Twilio, if any is available.
|
78
|
+
#
|
79
|
+
# @return [String] The location of the caller.
|
80
|
+
def location
|
81
|
+
Twilio::Rails::Formatter.location(city: from_city, country: from_country, province: from_province)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [String] The {Twilio::Rails::Phone::Tree} for the call.
|
85
|
+
def tree
|
86
|
+
@tree ||= Twilio::Rails.config.phone_trees.for(tree_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Checks if the call is for a given tree or trees, by class or by name.
|
90
|
+
#
|
91
|
+
# @param tree [Twilio::Rails::Phone::Tree, String, Symbol, Array] The tree or name of the tree, or an array of either.
|
92
|
+
# @return [true, false] true if the call is for the given tree or trees.
|
93
|
+
def for?(tree:)
|
94
|
+
trees = Array(tree).map { |t| t.is_a?(Twilio::Rails::Phone::Tree) ? t.name : t.to_s }
|
95
|
+
|
96
|
+
trees.include?(tree_name)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Updates the `length_seconds` attribute based on the time difference between the first and most recent
|
100
|
+
# responses in the phone call. Called by {Twilio::Rails::Phone::Response} when it is updated.
|
101
|
+
#
|
102
|
+
# @return [Integer] The length of the call in seconds.
|
103
|
+
def recalculate_length
|
104
|
+
first_response = responses.in_order.first
|
105
|
+
last_response = responses.in_order.last
|
106
|
+
result = 0
|
107
|
+
estimated_length_seconds = 5 # scientifically determined to be extremely accurate
|
108
|
+
|
109
|
+
result = last_response.created_at.to_i - first_response.created_at.to_i + estimated_length_seconds if first_response
|
110
|
+
update!(length_seconds: result)
|
111
|
+
result
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def status_callback
|
117
|
+
if saved_changes.key?("call_status") && no_answer?
|
118
|
+
Twilio::Rails::Phone::UnansweredCallJob.perform_later(phone_call_id: id)
|
119
|
+
end
|
120
|
+
|
121
|
+
if saved_changes.key?("call_status") && !in_progress?
|
122
|
+
Twilio::Rails::Phone::FinishedCallJob.perform_later(phone_call_id: id)
|
123
|
+
end
|
124
|
+
|
125
|
+
if saved_changes.key?("answered_by") && answering_machine?
|
126
|
+
Twilio::Rails::Phone::UnansweredCallJob.perform_later(phone_call_id: id)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|