twilio-rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +21 -0
- data/README.md +413 -0
- data/Rakefile +8 -0
- data/app/assets/config/twilio_rails_manifest.js +1 -0
- data/app/assets/stylesheets/twilio/rails/application.css +15 -0
- data/app/controllers/twilio/rails/application_controller.rb +6 -0
- data/app/controllers/twilio/rails/phone_controller.rb +112 -0
- data/app/controllers/twilio/rails/sms_controller.rb +64 -0
- data/app/helpers/twilio/rails/application_helper.rb +6 -0
- data/app/jobs/twilio/rails/application_job.rb +6 -0
- data/app/jobs/twilio/rails/phone/attach_recording_job.rb +15 -0
- data/app/jobs/twilio/rails/phone/finished_call_job.rb +15 -0
- data/app/jobs/twilio/rails/phone/unanswered_call_job.rb +15 -0
- data/app/mailers/twilio/rails/application_mailer.rb +8 -0
- data/app/models/twilio/rails/application_record.rb +7 -0
- data/app/operations/twilio/rails/application_operation.rb +21 -0
- data/app/operations/twilio/rails/find_or_create_phone_caller_operation.rb +29 -0
- data/app/operations/twilio/rails/phone/attach_recording_operation.rb +31 -0
- data/app/operations/twilio/rails/phone/base_operation.rb +21 -0
- data/app/operations/twilio/rails/phone/create_operation.rb +49 -0
- data/app/operations/twilio/rails/phone/find_operation.rb +14 -0
- data/app/operations/twilio/rails/phone/finished_call_operation.rb +17 -0
- data/app/operations/twilio/rails/phone/receive_recording_operation.rb +35 -0
- data/app/operations/twilio/rails/phone/start_call_operation.rb +53 -0
- data/app/operations/twilio/rails/phone/twiml/after_operation.rb +37 -0
- data/app/operations/twilio/rails/phone/twiml/base_operation.rb +50 -0
- data/app/operations/twilio/rails/phone/twiml/error_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/twiml/greeting_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/twiml/prompt_operation.rb +109 -0
- data/app/operations/twilio/rails/phone/twiml/prompt_response_operation.rb +29 -0
- data/app/operations/twilio/rails/phone/twiml/request_validation_failure_operation.rb +16 -0
- data/app/operations/twilio/rails/phone/twiml/timeout_operation.rb +48 -0
- data/app/operations/twilio/rails/phone/unanswered_call_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/update_operation.rb +26 -0
- data/app/operations/twilio/rails/phone/update_response_operation.rb +38 -0
- data/app/operations/twilio/rails/sms/base_operation.rb +17 -0
- data/app/operations/twilio/rails/sms/create_operation.rb +23 -0
- data/app/operations/twilio/rails/sms/find_message_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/find_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/send_operation.rb +102 -0
- data/app/operations/twilio/rails/sms/twiml/base_operation.rb +11 -0
- data/app/operations/twilio/rails/sms/twiml/error_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/twiml/message_operation.rb +49 -0
- data/app/operations/twilio/rails/sms/update_message_operation.rb +27 -0
- data/app/views/layouts/twilio/rails/application.html.erb +15 -0
- data/config/routes.rb +16 -0
- data/lib/generators/twilio/rails/install/USAGE +15 -0
- data/lib/generators/twilio/rails/install/install_generator.rb +34 -0
- data/lib/generators/twilio/rails/install/templates/initializer.rb +83 -0
- data/lib/generators/twilio/rails/install/templates/message.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/migration.rb +89 -0
- data/lib/generators/twilio/rails/install/templates/phone_call.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/phone_caller.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/recording.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/response.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/sms_conversation.rb +4 -0
- data/lib/generators/twilio/rails/phone_tree/USAGE +8 -0
- data/lib/generators/twilio/rails/phone_tree/phone_tree_generator.rb +12 -0
- data/lib/generators/twilio/rails/phone_tree/templates/tree.rb.erb +13 -0
- data/lib/generators/twilio/rails/sms_responder/USAGE +8 -0
- data/lib/generators/twilio/rails/sms_responder/sms_responder_generator.rb +12 -0
- data/lib/generators/twilio/rails/sms_responder/templates/responder.rb.erb +10 -0
- data/lib/tasks/rails_tasks.rake +45 -0
- data/lib/twilio/rails/client.rb +75 -0
- data/lib/twilio/rails/concerns/has_direction.rb +25 -0
- data/lib/twilio/rails/concerns/has_phone_number.rb +27 -0
- data/lib/twilio/rails/concerns/has_time_scopes.rb +19 -0
- data/lib/twilio/rails/configuration.rb +380 -0
- data/lib/twilio/rails/engine.rb +11 -0
- data/lib/twilio/rails/formatter.rb +93 -0
- data/lib/twilio/rails/models/message.rb +21 -0
- data/lib/twilio/rails/models/phone_call.rb +132 -0
- data/lib/twilio/rails/models/phone_caller.rb +100 -0
- data/lib/twilio/rails/models/recording.rb +27 -0
- data/lib/twilio/rails/models/response.rb +153 -0
- data/lib/twilio/rails/models/sms_conversation.rb +29 -0
- data/lib/twilio/rails/phone/base_tree.rb +229 -0
- data/lib/twilio/rails/phone/tree.rb +229 -0
- data/lib/twilio/rails/phone/tree_macros.rb +147 -0
- data/lib/twilio/rails/phone.rb +12 -0
- data/lib/twilio/rails/phone_number.rb +29 -0
- data/lib/twilio/rails/railtie.rb +17 -0
- data/lib/twilio/rails/sms/delegated_responder.rb +97 -0
- data/lib/twilio/rails/sms/responder.rb +33 -0
- data/lib/twilio/rails/sms.rb +12 -0
- data/lib/twilio/rails/version.rb +5 -0
- data/lib/twilio/rails.rb +89 -0
- metadata +289 -0
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 @@
|
|
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,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
|