twilio-rails 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|