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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cf947f0ddeb9ea673be2d9ce93cfcdff8e96290c231655c1ae724343bd699bff
4
+ data.tar.gz: 701c404e5d513947077e189797643694cddf2db51c87356095330295fbda5871
5
+ SHA512:
6
+ metadata.gz: 5259e6b131007ef2079977e1dc701c0b1636176242f3c935e1c95feee6dfa610890d8793eb0539eccdaad466a032ee8dab067bdccdbbee9ca6f3f22930e8bcf2
7
+ data.tar.gz: 5d798cd90d9575e25cbe2fcf67c79c57ad6154f83a834b90fb24daa329f033061305b195af6696f7b2b8d1b0d2c7488eb9151ede45de083cce42949e879bd26a
data/CHANGELOG.md ADDED
File without changes
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Kevin McPhillips
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # Twilio Rails
2
+
3
+ [![RSpec Tests](https://github.com/kmcphillips/twilio-rails/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/kmcphillips/twilio-rails/actions/workflows/ci.yml)
4
+
5
+ The `twilio-rails` gem is an opinionated Rails engine and a framework for building complex, realtime, stateful phone interactions in Rails without needing to directly interact with the Twilio API or use TwiML. It is not a replacement for the [`twilio-ruby` gem](https://github.com/twilio/twilio-ruby), but is rather built on top of it.
6
+
7
+ The most powerful ability of this engine is to build phone trees (think of calling customer service and pressing 2 for account information or whatever) using a simple Ruby DSL.
8
+
9
+ What does this mean in practice? **Call and find out!**
10
+ * In Canada: 📞 **(204) 800-7772**
11
+ * In the US: 📞 **(631) 800-7772**
12
+
13
+ ## Documentation
14
+
15
+ On [RubyDoc](https://rubydoc.info/github/kmcphillips/twilio-rails/main) or [Github pages](https://twilio-rails.kev.cool/).
16
+
17
+
18
+ ## Getting started
19
+
20
+ ### Installation
21
+
22
+ This Engine assumes it is running in a Rails app with a configured database, an ActiveJob provider, a configured ActiveStorage store, and controller sessions enabled.
23
+
24
+ Begin by adding this line to your Rails application's Gemfile:
25
+
26
+ ```ruby
27
+ gem "twilio-rails"
28
+ ```
29
+
30
+ After running `bundle`, run the installer:
31
+
32
+ ```sh
33
+ bin/rails generate twilio:rails:install
34
+ ```
35
+
36
+ There is now a pending migration to create the tables needed for the framework. But before running `bin/rails db:migrate` a development domain needs to be setup and the initializer needs to be configured with values from your Twilio account.
37
+
38
+
39
+ ### Local development
40
+
41
+ Twilio requires a publicly accessible URL to make requests to. When developing locally a tool such as [ngrok](https://ngrok.com/) can expose a local dev server via a publicly available SSL URL. Ngrok has a free tier and is easy to use. [See the install instructions for more information](https://ngrok.com/download). Other forwarding services exist and will work fine as well.
42
+
43
+ Whatever service, the public URL must be set in the `config/initializers/twilio_rails.rb` file as the `host` value. If this value is not set it will be inferred from `action_controller.default_url_options` if possible. Rails also requires the host to be added to the `config.hosts` list in `application.rb` or `development.rb`:
44
+
45
+ ```ruby
46
+ # config/application.rb
47
+ config.hosts << "my-ngrok-url.ngrok.io"
48
+ ```
49
+
50
+
51
+ ### Twilio configuration
52
+
53
+ Twilio will provide the phone number(s) you will use for your phone trees and SMS responders. Begin by creating an account and logging in at [https://console.twilio.com](https://console.twilio.com).
54
+
55
+ From the dashboard, find the "Account SID" and "Auth token" and copy them into the `config/initializers/twilio_rails.rb` file. Or better yet, use an environment variable or a secrets file to store them.
56
+
57
+ Next, go to "Phone Numbers -> Manage -> Buy a Number" and buy a phone number. Enter this number into the `config/initializers/twilio_rails.rb` file as well as the `default_phone_number` option.
58
+
59
+ You can get instructions on configuring the Twilio dashboard for your app by running:
60
+
61
+ ```sh
62
+ bin/rails twilio:rails:config
63
+ ```
64
+
65
+ This command will give you output tailored to the configuration and handlers in your app.
66
+
67
+ Phone call handerls should be configured something like:
68
+ ![Twilio phone tree config](https://user-images.githubusercontent.com/84159/233141680-78fde504-583c-44d1-bf42-bb4058e0e523.png)
69
+
70
+ And SMS handlers something like:
71
+ ![Twilio sms config](https://user-images.githubusercontent.com/84159/217126828-9c77ab34-9826-4e7c-bac3-2b893b08d39d.png)
72
+
73
+
74
+ ### `twilio-rails` configuration
75
+
76
+ The install generator will create a `config/initializers/twilio_rails.rb` file with reasonable default values and good documentation of each value and its use. Some are required for the engine to function and are provided by Twilio (`account_sid`, `auth_token`, and `default_outgoing_phone_number`).
77
+
78
+ The config options are documented inline and can be found:
79
+ * [In the initializer `lib/generators/twilio/rails/install/templates/twilio_rails.rb`](lib/generators/twilio/rails/install/templates/twilio_rails.rb)
80
+ * [In the `Configuration` class](lib/twilio/rails/configuration.rb)
81
+
82
+
83
+ ### Generators
84
+
85
+ There are generators to produce any required boilerplate. As described in the install steps, there is the installation generator:
86
+
87
+ ```sh
88
+ bin/rails generate twilio:rails:install
89
+ ```
90
+
91
+ And then there are generators to create phone trees and SMS responders:
92
+
93
+ ```sh
94
+ bin/rails generate twilio:rails:phone_tree
95
+ ```
96
+ ```sh
97
+ bin/rails generate twilio:rails:sms_responder
98
+ ```
99
+
100
+ Both are explained in detail below.
101
+
102
+
103
+ ### Example app
104
+
105
+ An example Rails app demonstrating the framework is available at [`twilio-rails-example`](https://github.com/kmcphillips/twilio-rails-example). It can be run locally with some minimal configuration, or can be reached as a working Twilio app by calling:
106
+
107
+ * In Canada: 📞 **(204) 800-7772**
108
+ * In the US: 📞 **(631) 800-7772**
109
+
110
+
111
+ ## How it works
112
+
113
+ This gem provides the persistence layer, lifecycle management and events, and a DSL for building phone trees and SMS responders. Twilio provides a [`twilio-ruby` gem](https://github.com/twilio/twilio-ruby) for their API and [TwiML](https://www.twilio.com/docs/voice/twiml) to define complex phone and SMS interactions. This gem uses both of these but the user does not need to understand or use either of them directly.
114
+
115
+ ### Models
116
+
117
+ After running the install generator, it generates five Active Record models with the following relationships:
118
+
119
+ ![model classes](https://user-images.githubusercontent.com/84159/217126823-36a8a8c5-3b4e-4d76-987b-f4c237d6ae2e.png)
120
+
121
+ The `PhoneCaller` is the individual making a phone call, uniquely identified by their phone number.
122
+
123
+ The `PhoneCall` is the record of a single phone call, either inbound or outbound. It is mutable and lifecycle callbacks handle state changes such as call length, call status, if it was answered or not, answering machine detection, etc.. Every phone call is mapped to exactly one phone tree, discussed in detail below, which directs using ruby how each interaction with the caller is handled.
124
+
125
+ A phone call has many `Response` records. Each interaction with the caller is a response, which is also mutable and lifecycle managed. Responses are stored in order and are the log of every step of the phone call. Responses contain user input, if any was asked for, such as digit presses, voice input, and transcriptions.
126
+
127
+ An `SMSConversation` is the record of a series of SMS messages exchanged with a phone caller. Each conversation has many `Message` records, flagged as either inbound or outbound. The full contents of the messages are stored in in the DB. Messages are handled based on responders, discussed in detail below.
128
+
129
+ Any and all of these models can be extended with extra fields and any logic required by the implementing application. They can also be named differently and configured in the initializer.
130
+
131
+
132
+ ### Phone trees
133
+
134
+ A phone tree is a subclass of [`Twilio::Rails::Phone::BaseTree`](lib/twilio/rails/phone/base_tree.rb) and provides a ruby DSL for defining how a phone call will be handled. See the documentation for full details.
135
+
136
+ Start by running the generator to create a new phone tree in `app/twilio/phone_trees/documentation_example_tree.rb`:
137
+
138
+ ```sh
139
+ bin/rails generate twilio:rails:phone_tree DocumentationExampleTree
140
+ ```
141
+
142
+ Regardless of inbound or outbound call, the entrypoint of a phone tree is the `greeting`:
143
+
144
+ ```ruby
145
+ class DocumentationExampleTree < Twilio::Rails::Phone::BaseTree
146
+ greeting message: "Hello!",
147
+ prompt: :thank_you_for_calling
148
+ ```
149
+
150
+ A `greeting` can provide some kind of `message:` and must provide a `prompt:`. A phone tree is a series of named `prompt`s that are jumped to by name to control the flow of the call. In this example, following the greeting control of the call moves to the `thank_you_for_calling` prompt:
151
+
152
+ ```ruby
153
+ prompt :thank_you_for_calling,
154
+ message: "Thank you for calling.",
155
+ after: :hold_music
156
+ ```
157
+
158
+ Any `message:` string as text will be read to the caller using Twilio's [Text-To-Speech voice synthesis](https://www.twilio.com/docs/voice/twiml/say/text-speech). Twilio allows the choice between several voices, including Amazon Polly voices. The voice can be set for the entire tree or for individual prompts.
159
+
160
+ ```ruby
161
+ voice "man"
162
+
163
+ prompt :polly_demo,
164
+ message: { say: "I am a Polly voice.", voice: "Polly.Matthew-Neural" },
165
+ after: :hold_music
166
+ ```
167
+
168
+ Any `message:` can also accept a `Hash` instead of a `String`:
169
+
170
+ * `{ say: "Hello" }` - Text-to-speech using the default or globally configured voice. Equivalent to just passing `"Hello"`.
171
+ * `{ say: "Hello", voice: "man" }` - Text-to-speech using the specified voice.
172
+ * `{ play: "https://example.com/audio.mp3" }` - Play a `wav` or `mp3` audio file from a URL.
173
+ * `{ pause: 1 }` - Pause silently for the specified number of seconds.
174
+
175
+ A `message:` can also be an `Array` which contains any number of the above hashes and strings, which will be passed to Twilio in order.
176
+
177
+ ```ruby
178
+ prompt :musical_interlude,
179
+ message: [
180
+ { say: "Please listen to this music.", voice: "Polly.Salli" },
181
+ { play: "https://example.com/musical_interlude.mp3" },
182
+ { pause: 1 }
183
+ "We hope you enjoyed this music.",
184
+ ],
185
+ after: :time_of_day
186
+ ```
187
+
188
+ And finally, a `message:` can be a `Proc` which will be called with the previous `Response` object and can return any of the above. Nearly any part of a phone tree can be a `Proc` which can be used to make the tree dynamic and interactive.
189
+
190
+ ```ruby
191
+ prompt :time_of_day,
192
+ message: ->(response) { "The time is #{Time.now.strftime("%l:%M %p")}." },
193
+ after: {
194
+ prompt: :last_prompt,
195
+ message: ->(response) { "All is well." }
196
+ }
197
+ ```
198
+
199
+ The `after:` option can be a `Hash` rather than just a symbol. If it is a hash it accepts a `prompt:` key which is the same as just passing a symbol. It also accepts a `message:` which supports all of the above options, including a `Proc`. Though in this case it will be called with the current `Response` object, not the previous one.
200
+
201
+ ```ruby
202
+ prompt :last_prompt,
203
+ after: {
204
+ message: "Have a good day. Goodbye.",
205
+ hangup: true
206
+ }
207
+ ```
208
+
209
+ The `after:` option can also accept a `hangup:` key which will hang up the call after the message is read. This is useful for the last prompt in a tree. The `after:` must provide either the next prompt or a hangup, not both. The entire `after:` can also be a `Proc` with the current response object.
210
+
211
+ ```ruby
212
+ prompt :maybe_last_prompt,
213
+ after: ->(response) {
214
+ if MyServiceObject.should_hangup?(response)
215
+ { message: "Sorry, this call must now end.", hangup: true }
216
+ else
217
+ :main_menu
218
+ end
219
+ }
220
+ ```
221
+
222
+ This is starting to show how using `Proc`s a phone tree can be highly dynamic and interactive. Each `Response` is saved to the database automatically, and associated to the `PhoneCall` in order. The `Proc` can make calls into the Rails app and do any kind of complex logic to determine the next step in the call. Just be aware that raising an exception or returning an invalid value will cause Twilio to error and for the call to end. Also keep in mind that Twilio will end the call if the response takes too long.
223
+
224
+ The final key that `prompt` accepts is `gather:`. This is used to collect digits from the keypad, or speech/voice audio. A `gather:` is optional and will be inserted between the `message:` if any and the `after:`.
225
+
226
+ ```ruby
227
+ prompt :rate_your_experience,
228
+ message: "Please rate your experience on a scale of 1 to 5."
229
+ gather: {
230
+ type: :digits,
231
+ timeout: 5,
232
+ number: 1,
233
+ interrupt: true
234
+ },
235
+ after: ->(response) {
236
+ if response.integer_digits.blank?
237
+ {
238
+ message: "Sorry, we did not get your rating. You can enter using the number keys on your phone",
239
+ prompt: :rate_your_experience
240
+ }
241
+ elsif response.integer_digits < 1 || response.integer_digits > 5
242
+ {
243
+ message: "Sorry, your rating must be between 1 and 5.",
244
+ prompt: :rate_your_experience
245
+ }
246
+ else
247
+ {
248
+ message: "You have given a rating of #{ response.integer_digits }. Thank you. Goodbye.",
249
+ hangup: true
250
+ }
251
+ end
252
+ }
253
+ ```
254
+
255
+ The `gather:` for `type: :digits` pauses after the message for `timeout:` number of seconds, defaulting to 5, and waits for the caller to type in `number:` number of digits, defaulting to 1. The digits, if any pressed, will be stored on the `Response` and this can be used in the `after:` to determine the next step in the call. The `interrupt:` boolean option, default `false`, dictates if pressing a digit will interrupt the message being played, or if the gather will not gather until the message has completed playing. To tie it together, the above example uses an [accessor method on `Response`](lib/twilio/rails/models/response.rb) to get the digits as an integer, and takes an action based on some basic data validation. If the response is not valid, the same prompt is repeated to the caller. If a digit is pressed before the message is finished playing, the message will stop, the digit will be stored, and move right to the `after`.
256
+
257
+ The `gather:` can also accept `type: :voice` which will record the caller's voice for `length:` number of seconds, defaulting to 10.
258
+
259
+ ```ruby
260
+ prompt :record_your_feedback,
261
+ message: "Please leave us a message with your feedback, and press the pound key when you are done.",
262
+ gather: {
263
+ type: :voice,
264
+ length: 30,
265
+ beep: true,
266
+ transcribe: true,
267
+ profanity_filter: true
268
+ },
269
+ after: {
270
+ message: "Thank you for your feedback. Goodbye.",
271
+ hangup: true
272
+ }
273
+ ```
274
+
275
+ The above `gather:` with `type: :voice` example will finish reading the message, play a beep, and then record the phone caller's speech for 30 seconds or until they press the `#` pound key. The phone tree will then immediately execute the `after:`, while the framework continues to handle the audio recording asynchronously. When Twilio makes it available, the audio file of the recording will be downloaded and stored as an ActiveStorage attachment in a `Recording` model as `response.recording`. If the `transcribe:` option is set to `true`, the voice in the recording will also attempt to be transcribed as text and stored as `response.transcription`. Importantly though, **neither are guaranteed to arrive or will arrive immediately**. In practice they both usually arrive within a few seconds, but can sometimes be blank or missing if the caller is silent or garbled. There is a cost to transcription so it can be disabled, and the `profanity_filter:` defaults to false and will just *** out any profanity in the transcription.
276
+
277
+ Finally, the `gather:` can also accept `type: :speech` which is a specialzed model designed to identify voice in realtime. It will provide the `response.transcription` field immediately, making it available in the `after:` proc or in the next prompt. But the tradeoffs are that it does not provide a recording, there is a time gap of a few seconds between prompts, and it is more expensive. See the [Twilio documentation for specifics](https://www.twilio.com/docs/voice/twiml/gather#speechmodel). The keys it expects match the documentation, `speech_model:`, `speech_timeout:`, `language:` (defaults to "en-US"), and `encanced:` (defaults to false).
278
+
279
+ ```ruby
280
+ prompt :what_direction_should_we_go,
281
+ message: "Which cardinal direction should we go?",
282
+ gather: {
283
+ type: :speech,
284
+ language: "en-US",
285
+ enhanced: true,
286
+ speech_model: "numbers_and_commands",
287
+ speech_timeout: "auto",
288
+ },
289
+ after: ->(response) {
290
+ if response.transcription.blank?
291
+ {
292
+ message: "Sorry, we did not get your response. Please try again.",
293
+ prompt: :what_direction_should_we_go,
294
+ }
295
+ elsif response.transcription_matches?("north", "south", "east", "west")
296
+ MyCommandObject.move(response.transcription)
297
+
298
+ {
299
+ message: "Moving #{ response.transcription }.",
300
+ hangup: true
301
+ }
302
+ else
303
+ {
304
+ message: "Sorry, we did not understand your response.",
305
+ prompt: :what_direction_should_we_go,
306
+ }
307
+ end
308
+ }
309
+ ```
310
+
311
+ To inspect the implementation and get further detail, most of the magic happens in [`Twilio::Rails::Phone::Tree`](lib/twilio/rails/phone/tree.rb) and the operations under [`Twilio::Rails::Phone::Twiml`](app/operations/twilio/rails/phone/twiml/) where the DSL is defined and then converted inbot [TwiML](https://www.twilio.com/docs/voice/twiml).
312
+
313
+
314
+ ### Make an outgoing phone call
315
+
316
+ An outgoing phone call may be started from any valid phone tree and any configured Twilio phone number via the [`Twilio::Rails::Phone::StartCallOperation`](app/operations/twilio/rails/phone/start_call_operation.rb). This starts the asynchronous process of making the call. It will return the DB phone call instance which will be updated with the status of the call.
317
+
318
+ ```ruby
319
+ Twilio::Phone::StartCallOperation.call(
320
+ tree: Twilio::Rails.config.phone_trees.for("your_tree_name"),
321
+ to: "+155566677777", # or an instance of Twilio::Rails::PhoneNumber
322
+ from: Twilio::Rails.config.default_outgoing_phone_number # optional and defaults to this value
323
+ )
324
+ ```
325
+
326
+
327
+ ### SMS responders
328
+
329
+ > **Warning**
330
+ > Due to how Twilio makes API calls into the application for SMS messages, SMS responders require Rails sessions to be enabled and setup in order to handle SMS messages.
331
+
332
+ Twilio provides a hook for incoming SMS messages and can send SMS messages to any phone number. This gem provides a simple method for handling SMS conversations, though it does not provide a full stateful tree structure.
333
+
334
+ An SMS responder is a subclass of [`Twilio::Rails::SMS::DelegatedResponder`](lib/twilio/rails/sms/delegated_responder.rb). Any number of responders may be added to the app provided they are registered in the initializer with `config.sms_responders.register { MyResponderClass }`.
335
+
336
+ The responder class will be initialized with the `message` and `sms_conversation` local variables set, and must implement two methods:
337
+ * `handle?`: Return true if this handler handles the given message, false if it does not.
338
+ * `reply`: A string to reply to the message with, or `nil` if the message is handled and no response should be sent.
339
+
340
+ All registered responders will be visited in order and the first one to return a truthy value from `#handle?` will handle the message and no further responders will be called. If all `#handle?` methods return false than the incoming message is ignored.
341
+
342
+ The `sms_conversation` variable is an instance of the implementor of `Twilio::Rails::Models::SMSConversation` and contains the full history of the conversation with this phone caller, and can be used to determine the next step in the conversation. These models can also be extended to add any required application level fields and logic.
343
+
344
+
345
+ ### Send an outgoing SMS message
346
+
347
+ An out going SMS message may be sent via the [`Twilio::Rails::SMS::SendOperation`](app/operations/twilio/rails/sms/send_operation.rb). This will send the message and start a conversation, storing all messages and replies in the DB:
348
+
349
+ ```ruby
350
+ Twilio::Rails::SMS::SendOperation.call(
351
+ phone_caller_id: phone_caller.id,
352
+ messages: ["Hello world!"], # an array of strings, each one will be sent as a separate message in sequence
353
+ from_number: Twilio::Rails.config.default_outgoing_phone_number # optional and defaults to this value
354
+ )
355
+ ```
356
+
357
+ Since the operation assumes a phone caller, it can first be created and/or retrieved by calling:
358
+
359
+ ```ruby
360
+ phone_caller = Twilio::Rails::FindOrCreatePhoneCallerOperation.call(phone_number: "+155566677777")
361
+ ```
362
+
363
+
364
+ ### Errors
365
+
366
+ All errors are subclasses of [`Twilio::Rails::Error`](lib/twilio/rails.rb). They are grouped under [`Twilio::Rails::Phone::Error`](lib/twilio/rails/phone.rb) and [`Twilio::Rails::SMS::Error`](lib/twilio/rails/sms.rb), and then further specialized from there.
367
+
368
+ There is a configuration option to add an exception notifier in some important places in the framework. It will never catch or handle exceptions.
369
+
370
+ ```ruby
371
+ config.exception_notifier = ->(exception, message, context, exception_binding) {
372
+ # Send an email or use some kind of service etc.
373
+ }
374
+ ```
375
+
376
+ ### The rest of the documentation
377
+
378
+ Anything not covered in this documentation is probably documented on the classes and method calls in the application. Probably the most interesting and useful places to look are:
379
+
380
+ * [lib/twilio/rails/models](lib/twilio/rails/models)
381
+ * [app/operations](app/operations)
382
+
383
+
384
+ ## Limitations and known issues
385
+
386
+ This framework was extracted from a larger project. There are some assumptions built in that are limitations of the current implementation. Please feel free to PR improvements! But for now, known limitations are:
387
+
388
+ * Only North American phone numbers are supported, 1 plus 10 digits (`+155566677777`).
389
+ * If a phone call whose number is not of the above format is received it is not even persisted or handled.
390
+ * Some North American assumptions of "day" are probably hidden in a couple places.
391
+ * Only production tested with MySQL and SQLite, but should work with Postgres. Assumes `utf8mb4` encoding in MySQL, but the migration does not specify it in order to support other DBs.
392
+ * Only production tested with Sidekiq, but any ActiveJob provider should work.
393
+ * There is no support for domain level events or observers. This means hooks need to be implemented using active record model callbacks, which is opaque, fragile, and confusing. In future the framework could define and trigger named events based on lifecycle.
394
+ * SMS handling is pretty simple and pattern matching based. This is not an implementation of a full chat bot. Other better frameworks exist for that. This could probably be completely rebuilt to work in a similar way where a phone number is bound to a responder by name, rather than each one implementing `handle?`.
395
+ * The `DelegatedResponder#reply` method assumes a single `String` message, but probably should also or by default support an array of strings.
396
+ * Generators do not generate tests, but should look at the generator `test_framework` config and produce tests or specs for the created classes.
397
+ * Not all Twilio TwiML features are supported. Many though are easy to add flags that are just passed through, and are easy to add.
398
+ * The `gather:` should support `hints:` and some other config options.
399
+ * Some documentation is missing in:
400
+ * Controller actions.
401
+
402
+
403
+ ## Contributing
404
+
405
+ PRs welcome! I will help you. Please do not hesitate to open PRs or issues if a feature is missing or if you encounter a bug.
406
+
407
+ To get started, fork the repo and clone it. The `.ruby-version` assumes Ruby 3.2.0 but this can easily be changed.
408
+
409
+ Run `bundle install` to install dependencies. A console can be started with `bin/rails c`. The tests can be run with `bundle exec rspec`.
410
+
411
+ No PR will be accepted without test coverage. Please add tests for any new features or bug fixes.
412
+
413
+ Any change must also be tested against [the example app](https://github.com/kmcphillips/twilio-rails-example), but this is not an automated process. See the documentation in the example app for more information.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/twilio/rails .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,6 @@
1
+ module Twilio
2
+ module Rails
3
+ class ApplicationController < ActionController::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+ module Twilio
3
+ module Rails
4
+ class PhoneController < ApplicationController
5
+ skip_before_action :verify_authenticity_token
6
+
7
+ before_action :validate_webhook
8
+
9
+ def inbound
10
+ respond_to do |format|
11
+ format.xml do
12
+ phone_call = Twilio::Rails::Phone::CreateOperation.call(params: params_hash, tree: tree)
13
+ render xml: Twilio::Rails::Phone::Twiml::GreetingOperation.call(phone_call_id: phone_call.id, tree: tree)
14
+ end
15
+ end
16
+ end
17
+
18
+ def outbound
19
+ respond_to do |format|
20
+ format.xml do
21
+ phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
22
+ render xml: Twilio::Rails::Phone::Twiml::GreetingOperation.call(phone_call_id: phone_call.id, tree: tree)
23
+ end
24
+ end
25
+ end
26
+
27
+ def prompt
28
+ respond_to do |format|
29
+ format.xml do
30
+ phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
31
+ phone_call = Twilio::Rails::Phone::UpdateOperation.call(phone_call_id: phone_call.id, params: params_hash)
32
+ render xml: Twilio::Rails::Phone::Twiml::PromptOperation.call(phone_call_id: phone_call.id, tree: tree, response_id: params[:response_id].to_i)
33
+ end
34
+ end
35
+ end
36
+
37
+ def prompt_response
38
+ respond_to do |format|
39
+ format.xml do
40
+ phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
41
+ phone_call = Twilio::Rails::Phone::UpdateOperation.call(phone_call_id: phone_call.id, params: params_hash)
42
+ response = Twilio::Rails::Phone::UpdateResponseOperation.call(phone_call_id: phone_call.id, response_id: params[:response_id].to_i, params: params_hash)
43
+ render xml: Twilio::Rails::Phone::Twiml::PromptResponseOperation.call(phone_call_id: phone_call.id, tree: tree, response_id: params[:response_id].to_i, params: params_hash)
44
+ end
45
+ end
46
+ end
47
+
48
+ def timeout
49
+ respond_to do |format|
50
+ format.xml do
51
+ phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
52
+ phone_call = Twilio::Rails::Phone::UpdateOperation.call(phone_call_id: phone_call.id, params: params_hash)
53
+ render xml: Twilio::Rails::Phone::Twiml::TimeoutOperation.call(phone_call_id: phone_call.id, tree: tree, response_id: params[:response_id].to_i)
54
+ end
55
+ end
56
+ end
57
+
58
+ def transcribe
59
+ respond_to do |format|
60
+ format.xml do
61
+ phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
62
+ Twilio::Rails::Phone::UpdateResponseOperation.call(phone_call_id: phone_call.id, response_id: params[:response_id].to_i, params: params_hash)
63
+
64
+ head :ok
65
+ end
66
+ end
67
+ end
68
+
69
+ def status
70
+ respond_to do |format|
71
+ format.xml do
72
+ phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
73
+ phone_call = Twilio::Rails::Phone::UpdateOperation.call(phone_call_id: phone_call.id, params: params_hash)
74
+
75
+ head :ok
76
+ end
77
+ end
78
+ end
79
+
80
+ def receive_response_recording
81
+ respond_to do |format|
82
+ format.xml do
83
+ phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
84
+ Twilio::Rails::Phone::ReceiveRecordingOperation.call(phone_call_id: phone_call.id, response_id: params[:response_id].to_i, params: params_hash)
85
+
86
+ head :ok
87
+ end
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def validate_webhook
94
+ if params["AccountSid"] != Twilio::Rails.config.account_sid
95
+ respond_to do |format|
96
+ format.xml do
97
+ render xml: Twilio::Rails::Phone::Twiml::RequestValidationFailureOperation.call
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def tree
104
+ @tree ||= Twilio::Rails.config.phone_trees.for(params[:tree_name])
105
+ end
106
+
107
+ def params_hash
108
+ params.permit!.to_h.except("controller", "action", "format", "response_id", "tree_name")
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ module Twilio
3
+ module Rails
4
+ class SMSController < ::Twilio::Rails::ApplicationController
5
+ skip_before_action :verify_authenticity_token
6
+
7
+ before_action :validate_webhook
8
+
9
+ def message
10
+ respond_to do |format|
11
+ format.xml do
12
+ if spam?
13
+ render xml: Twilio::Rails::SMS::Twiml::ErrorOperation.call()
14
+ else
15
+ if session[:sms_conversation_id].present?
16
+ conversation = Twilio::Rails::SMS::FindOperation.call(sms_conversation_id: session[:sms_conversation_id])
17
+ else
18
+ conversation = Twilio::Rails::SMS::CreateOperation.call(params: params_hash)
19
+ session[:sms_conversation_id] = conversation.id
20
+ end
21
+
22
+ render xml: Twilio::Rails::SMS::Twiml::MessageOperation.call(sms_conversation_id: conversation.id, params: params_hash)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def status
29
+ respond_to do |format|
30
+ format.xml do
31
+ if params[:message_id].present?
32
+ message = Twilio::Rails::SMS::UpdateMessageOperation.call(message_id: params[:message_id].to_i, params: params_hash)
33
+ else
34
+ message = Twilio::Rails::SMS::FindMessageOperation.call(params: params_hash)
35
+ message = Twilio::Rails::SMS::UpdateMessageOperation.call(message_id: message.id, params: params_hash)
36
+ end
37
+
38
+ head :ok
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def validate_webhook
46
+ if params["AccountSid"] != Twilio::Rails.config.account_sid
47
+ respond_to do |format|
48
+ format.xml do
49
+ render xml: Twilio::Rails::SMS::Twiml::ErrorOperation.call()
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def spam?
56
+ Twilio::Rails.config.spam_filter && Twilio::Rails.config.spam_filter.call(params)
57
+ end
58
+
59
+ def params_hash
60
+ params.permit!.to_h.except("controller", "action", "format", "message_id", "tree_name")
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,6 @@
1
+ module Twilio
2
+ module Rails
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end