twilio-rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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