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.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE +21 -0
  4. data/README.md +413 -0
  5. data/Rakefile +8 -0
  6. data/app/assets/config/twilio_rails_manifest.js +1 -0
  7. data/app/assets/stylesheets/twilio/rails/application.css +15 -0
  8. data/app/controllers/twilio/rails/application_controller.rb +6 -0
  9. data/app/controllers/twilio/rails/phone_controller.rb +112 -0
  10. data/app/controllers/twilio/rails/sms_controller.rb +64 -0
  11. data/app/helpers/twilio/rails/application_helper.rb +6 -0
  12. data/app/jobs/twilio/rails/application_job.rb +6 -0
  13. data/app/jobs/twilio/rails/phone/attach_recording_job.rb +15 -0
  14. data/app/jobs/twilio/rails/phone/finished_call_job.rb +15 -0
  15. data/app/jobs/twilio/rails/phone/unanswered_call_job.rb +15 -0
  16. data/app/mailers/twilio/rails/application_mailer.rb +8 -0
  17. data/app/models/twilio/rails/application_record.rb +7 -0
  18. data/app/operations/twilio/rails/application_operation.rb +21 -0
  19. data/app/operations/twilio/rails/find_or_create_phone_caller_operation.rb +29 -0
  20. data/app/operations/twilio/rails/phone/attach_recording_operation.rb +31 -0
  21. data/app/operations/twilio/rails/phone/base_operation.rb +21 -0
  22. data/app/operations/twilio/rails/phone/create_operation.rb +49 -0
  23. data/app/operations/twilio/rails/phone/find_operation.rb +14 -0
  24. data/app/operations/twilio/rails/phone/finished_call_operation.rb +17 -0
  25. data/app/operations/twilio/rails/phone/receive_recording_operation.rb +35 -0
  26. data/app/operations/twilio/rails/phone/start_call_operation.rb +53 -0
  27. data/app/operations/twilio/rails/phone/twiml/after_operation.rb +37 -0
  28. data/app/operations/twilio/rails/phone/twiml/base_operation.rb +50 -0
  29. data/app/operations/twilio/rails/phone/twiml/error_operation.rb +22 -0
  30. data/app/operations/twilio/rails/phone/twiml/greeting_operation.rb +22 -0
  31. data/app/operations/twilio/rails/phone/twiml/prompt_operation.rb +109 -0
  32. data/app/operations/twilio/rails/phone/twiml/prompt_response_operation.rb +29 -0
  33. data/app/operations/twilio/rails/phone/twiml/request_validation_failure_operation.rb +16 -0
  34. data/app/operations/twilio/rails/phone/twiml/timeout_operation.rb +48 -0
  35. data/app/operations/twilio/rails/phone/unanswered_call_operation.rb +22 -0
  36. data/app/operations/twilio/rails/phone/update_operation.rb +26 -0
  37. data/app/operations/twilio/rails/phone/update_response_operation.rb +38 -0
  38. data/app/operations/twilio/rails/sms/base_operation.rb +17 -0
  39. data/app/operations/twilio/rails/sms/create_operation.rb +23 -0
  40. data/app/operations/twilio/rails/sms/find_message_operation.rb +15 -0
  41. data/app/operations/twilio/rails/sms/find_operation.rb +15 -0
  42. data/app/operations/twilio/rails/sms/send_operation.rb +102 -0
  43. data/app/operations/twilio/rails/sms/twiml/base_operation.rb +11 -0
  44. data/app/operations/twilio/rails/sms/twiml/error_operation.rb +15 -0
  45. data/app/operations/twilio/rails/sms/twiml/message_operation.rb +49 -0
  46. data/app/operations/twilio/rails/sms/update_message_operation.rb +27 -0
  47. data/app/views/layouts/twilio/rails/application.html.erb +15 -0
  48. data/config/routes.rb +16 -0
  49. data/lib/generators/twilio/rails/install/USAGE +15 -0
  50. data/lib/generators/twilio/rails/install/install_generator.rb +34 -0
  51. data/lib/generators/twilio/rails/install/templates/initializer.rb +83 -0
  52. data/lib/generators/twilio/rails/install/templates/message.rb +4 -0
  53. data/lib/generators/twilio/rails/install/templates/migration.rb +89 -0
  54. data/lib/generators/twilio/rails/install/templates/phone_call.rb +4 -0
  55. data/lib/generators/twilio/rails/install/templates/phone_caller.rb +4 -0
  56. data/lib/generators/twilio/rails/install/templates/recording.rb +4 -0
  57. data/lib/generators/twilio/rails/install/templates/response.rb +4 -0
  58. data/lib/generators/twilio/rails/install/templates/sms_conversation.rb +4 -0
  59. data/lib/generators/twilio/rails/phone_tree/USAGE +8 -0
  60. data/lib/generators/twilio/rails/phone_tree/phone_tree_generator.rb +12 -0
  61. data/lib/generators/twilio/rails/phone_tree/templates/tree.rb.erb +13 -0
  62. data/lib/generators/twilio/rails/sms_responder/USAGE +8 -0
  63. data/lib/generators/twilio/rails/sms_responder/sms_responder_generator.rb +12 -0
  64. data/lib/generators/twilio/rails/sms_responder/templates/responder.rb.erb +10 -0
  65. data/lib/tasks/rails_tasks.rake +45 -0
  66. data/lib/twilio/rails/client.rb +75 -0
  67. data/lib/twilio/rails/concerns/has_direction.rb +25 -0
  68. data/lib/twilio/rails/concerns/has_phone_number.rb +27 -0
  69. data/lib/twilio/rails/concerns/has_time_scopes.rb +19 -0
  70. data/lib/twilio/rails/configuration.rb +380 -0
  71. data/lib/twilio/rails/engine.rb +11 -0
  72. data/lib/twilio/rails/formatter.rb +93 -0
  73. data/lib/twilio/rails/models/message.rb +21 -0
  74. data/lib/twilio/rails/models/phone_call.rb +132 -0
  75. data/lib/twilio/rails/models/phone_caller.rb +100 -0
  76. data/lib/twilio/rails/models/recording.rb +27 -0
  77. data/lib/twilio/rails/models/response.rb +153 -0
  78. data/lib/twilio/rails/models/sms_conversation.rb +29 -0
  79. data/lib/twilio/rails/phone/base_tree.rb +229 -0
  80. data/lib/twilio/rails/phone/tree.rb +229 -0
  81. data/lib/twilio/rails/phone/tree_macros.rb +147 -0
  82. data/lib/twilio/rails/phone.rb +12 -0
  83. data/lib/twilio/rails/phone_number.rb +29 -0
  84. data/lib/twilio/rails/railtie.rb +17 -0
  85. data/lib/twilio/rails/sms/delegated_responder.rb +97 -0
  86. data/lib/twilio/rails/sms/responder.rb +33 -0
  87. data/lib/twilio/rails/sms.rb +12 -0
  88. data/lib/twilio/rails/version.rb +5 -0
  89. data/lib/twilio/rails.rb +89 -0
  90. 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,11 @@
1
+ module Twilio
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Twilio::Rails
5
+
6
+ config.to_prepare do
7
+ Twilio::Rails.config.finalize!
8
+ end
9
+ end
10
+ end
11
+ 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