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,89 @@
1
+ class InstallTwilioRails < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table "messages", force: :cascade do |t|
4
+ t.bigint "sms_conversation_id", null: false
5
+ t.string "sid"
6
+ t.text "body"
7
+ t.string "status"
8
+ t.string "direction"
9
+ t.datetime "created_at", null: false
10
+ t.datetime "updated_at", null: false
11
+ t.index ["created_at"], name: "index_messages_on_created_at"
12
+ t.index ["direction"], name: "index_messages_on_direction"
13
+ t.index ["sms_conversation_id"], name: "index_messages_on_sms_conversation_id"
14
+ end
15
+
16
+ create_table "phone_callers", force: :cascade do |t|
17
+ t.string "phone_number"
18
+ t.datetime "created_at", null: false
19
+ t.datetime "updated_at", null: false
20
+ t.index ["phone_number"], name: "index_phone_callers_on_phone_number"
21
+ end
22
+
23
+ create_table "phone_calls", force: :cascade do |t|
24
+ t.integer "phone_caller_id"
25
+ t.string "sid"
26
+ t.string "number"
27
+ t.string "from_number"
28
+ t.string "from_city"
29
+ t.string "from_province"
30
+ t.string "from_country"
31
+ t.string "tree_name"
32
+ t.string "direction"
33
+ t.string "answered_by"
34
+ t.boolean "unanswered", default: false
35
+ t.boolean "finished", default: false
36
+ t.string "call_status"
37
+ t.integer "length_seconds"
38
+ t.datetime "created_at", null: false
39
+ t.datetime "updated_at", null: false
40
+ t.index ["created_at"], name: "index_phone_calls_on_created_at"
41
+ t.index ["direction"], name: "index_phone_calls_on_direction"
42
+ t.index ["phone_caller_id"], name: "index_phone_calls_on_phone_caller_id"
43
+ t.index ["sid"], name: "index_calls_on_sid"
44
+ t.index ["tree_name"], name: "index_phone_calls_on_tree_name"
45
+ end
46
+
47
+ create_table "recordings", force: :cascade do |t|
48
+ t.bigint "phone_call_id", null: false
49
+ t.string "recording_sid"
50
+ t.string "duration"
51
+ t.string "url"
52
+ t.datetime "created_at", null: false
53
+ t.datetime "updated_at", null: false
54
+ t.index ["phone_call_id"], name: "index_recordings_on_call_id"
55
+ end
56
+
57
+ create_table "responses", force: :cascade do |t|
58
+ t.bigint "phone_call_id"
59
+ t.bigint "recording_id"
60
+ t.string "prompt_handle"
61
+ t.string "digits"
62
+ t.text "transcription"
63
+ t.boolean "transcribed", default: false
64
+ t.boolean "timeout", default: false
65
+ t.datetime "created_at", null: false
66
+ t.datetime "updated_at", null: false
67
+ t.index ["created_at"], name: "index_responses_on_created_at"
68
+ t.index ["digits"], name: "index_responses_on_digits"
69
+ t.index ["phone_call_id", "prompt_handle"], name: "index_responses_on_phone_call_id_and_prompt_handle"
70
+ t.index ["prompt_handle"], name: "index_responses_on_prompt_handle"
71
+ t.index ["recording_id"], name: "index_responses_on_recording_id"
72
+ t.index ["timeout"], name: "index_responses_on_timeout"
73
+ t.index ["transcribed"], name: "index_responses_on_transcribed"
74
+ end
75
+
76
+ create_table "sms_conversations", force: :cascade do |t|
77
+ t.string "number"
78
+ t.string "from_number"
79
+ t.string "from_city"
80
+ t.string "from_province"
81
+ t.string "from_country"
82
+ t.datetime "created_at", null: false
83
+ t.datetime "updated_at", null: false
84
+ t.index ["created_at"], name: "index_sms_conversations_on_created_at"
85
+ t.index ["from_number"], name: "index_sms_conversations_on_from_number"
86
+ t.index ["number"], name: "index_sms_conversations_on_number"
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class PhoneCall < ApplicationRecord
3
+ include Twilio::Rails::Models::PhoneCall
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class PhoneCaller < ApplicationRecord
3
+ include Twilio::Rails::Models::PhoneCaller
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class Recording < ApplicationRecord
3
+ include Twilio::Rails::Models::Recording
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class Response < ApplicationRecord
3
+ include Twilio::Rails::Models::Response
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class SMSConversation < ApplicationRecord
3
+ include Twilio::Rails::Models::SMSConversation
4
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a new phone tree and registers it.
3
+
4
+ Example:
5
+ bin/rails generate twilio:rails:phone_tree SomeExample
6
+
7
+ This will create:
8
+ app/phone_trees/some_example_tree.rb
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ class Twilio::Rails::PhoneTreeGenerator < Rails::Generators::NamedBase
3
+ source_root File.expand_path("templates", __dir__)
4
+
5
+ def copy_template_tree
6
+ template "tree.rb.erb", "app/phone_trees/#{file_name}_tree.rb"
7
+ end
8
+
9
+ def register_tree
10
+ insert_into_file "config/initializers/twilio_rails.rb", "\n config.phone_trees.register { #{class_name}Tree }", before: "\nend\n"
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ class <%= class_name %>Tree < Twilio::Rails::Phone::BaseTree
3
+ # voice "male"
4
+ # final_timeout_message("Goodbye.")
5
+ # invalid_phone_number("Sorry, we cannot accept your call.")
6
+
7
+ greeting message: "Hello!",
8
+ prompt: :thanks_and_hangup
9
+
10
+ prompt :thanks_and_hangup,
11
+ message: "Thank you for calling!",
12
+ after: { hangup: true }
13
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a new SMS responder and registers it.
3
+
4
+ Example:
5
+ bin/rails generate twilio:rails:sms_responder SomeExample
6
+
7
+ This will create:
8
+ app/sms_responders/some_example_responder.rb
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ class Twilio::Rails::SmsResponderGenerator < Rails::Generators::NamedBase
3
+ source_root File.expand_path("templates", __dir__)
4
+
5
+ def copy_template_responder
6
+ template "responder.rb.erb", "app/sms_responders/#{file_name}_responder.rb"
7
+ end
8
+
9
+ def register_responder
10
+ insert_into_file "config/initializers/twilio_rails.rb", "\n config.sms_responders.register { #{class_name}Responder }", before: "\nend\n"
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ class <%= class_name %>Responder < Twilio::Rails::SMS::DelegatedResponder
3
+ def handle?
4
+ raise NotImplementedError, "Implement #{ self.class }#handle?"
5
+ end
6
+
7
+ def reply
8
+ raise NotImplementedError, "Implement #{ self.class }#reply"
9
+ end
10
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ namespace :twilio do
3
+ namespace :rails do
4
+ desc "Show the available values to config in Twilio"
5
+ task config: :environment do
6
+ if Twilio::Rails.config.host.blank?
7
+ puts "Twilio::Rails.config.host is not set. Set it in the `config/initializers/twilio_rails.rb` file."
8
+ elsif Twilio::Rails.config.host == "https://example.com"
9
+ puts "Twilio::Rails.config.host is set to a test value. Set it in the `config/initializers/twilio_rails.rb` file."
10
+ else
11
+ http_methods = if Twilio::Rails.config.controller_http_methods.length == 1
12
+ "HTTP #{ Twilio::Rails.config.controller_http_methods.first.to_s.upcase }"
13
+ else
14
+ "HTTP POST or HTTP GET"
15
+ end
16
+
17
+ puts "Log into the Twilio web console: https://console.twilio.com"
18
+ puts "Navigate to Phone Numbers -> Manage -> Active Numbers and find the phone number #{Twilio::Rails.config.default_outgoing_phone_number}."
19
+ puts ""
20
+
21
+ if Twilio::Rails.config.phone_trees.all.length == 0
22
+ puts "You cannot yet configure `Voice & Fax' because There are no phone trees registered in this application."
23
+ puts "Register them in the `config/initializers/twilio_rails.rb` file if you want to handle phone calls, and run this task again to help configure Twilio."
24
+ else
25
+ puts "Under 'Voice & Fax' set 'A CALL COMES IN' to 'Webhook' with #{ http_methods } and one of the following URLs:"
26
+ Twilio::Rails.config.phone_trees.all.each do |name, tree|
27
+ puts " #{tree.inbound_url}"
28
+ end
29
+ puts ""
30
+ puts "Under 'Voice & Fax' set 'CALL STATUS CHANGES' to following URL:"
31
+ puts " #{ ::Twilio::Rails.config.host }#{ ::Twilio::Rails::Engine.routes.url_helpers.phone_status_path(format: :xml) }"
32
+ end
33
+
34
+ puts ""
35
+ puts "Under 'Messaging' set 'A MESSAGE COMES IN' to 'Webhook' with #{ http_methods } and the following URL:"
36
+ puts " #{ ::Twilio::Rails.config.host }#{ ::Twilio::Rails::Engine.routes.url_helpers.sms_message_path(format: :xml) }"
37
+
38
+ if Twilio::Rails.config.sms_responders.all.length == 0
39
+ puts "There are no SMS responders registered so they will not be handled."
40
+ puts "Register them in the `config/initializers/twilio_rails.rb` file if you want to handle SMS messages."
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+ module Twilio
3
+ module Rails
4
+ # An abstraction over top of the `Twilio::REST` API client. Used to send SMS messages and start calls, as well as
5
+ # return an initialized client if needed.
6
+ module Client
7
+ extend self
8
+
9
+ # @return [Twilio::REST::Client] Twilio client initialized with `account_sid` and `auth_token` from the config.
10
+ def client
11
+ @twilio_client ||= Twilio::REST::Client.new(
12
+ Twilio::Rails.config.account_sid,
13
+ Twilio::Rails.config.auth_token,
14
+ )
15
+ end
16
+
17
+ # Do not call this directly, instead see {Twilio::Rails::SMS::SendOperation}. Send an SMS message to and from the
18
+ # given phone numbers using the Twilio REST API directly. This does not store or manage any interactions in the
19
+ # database.
20
+ #
21
+ # @param message [String] the message to send.
22
+ # @param to [String] the phone number to send the message to.
23
+ # @param from [String] the phone number to send the message from.
24
+ # @return [String] the SID returned from Twilio for the sent SMS message.
25
+ def send_message(message:, to:, from:)
26
+ Twilio::Rails.config.logger.tagged(self) { |l| l.info("[send_message] to=#{ to } from=#{ from } body='#{ message }'") }
27
+ client.messages.create(
28
+ from: from,
29
+ to: to,
30
+ body: message,
31
+ status_callback: "#{ Twilio::Rails.config.host }#{ ::Twilio::Rails::Engine.routes.url_helpers.sms_status_path(format: :xml) }",
32
+ ).sid
33
+ end
34
+
35
+ # Do not call this directly, instead see {Twilio::Rails::SMS::SendOperation}. Sends multiple SMS messages to and
36
+ # from the given phone numbers using the Twilio REST API directly. This does not store or manage any interactions
37
+ # in the database. If a message is blank it will be ignored.
38
+ #
39
+ # @param messages [Array<String>] the messages to send.
40
+ # @param to [String] the phone number to send the messages to.
41
+ # @param from [String] the phone number to send the messages from.
42
+ # @return [Array<String>] the SIDs returned from Twilio for the sent SMS messages.
43
+ def send_messages(messages:, to:, from:)
44
+ Twilio::Rails.config.logger.tagged(self) { |l| l.info("[send_messages] to blank messages") } if messages.blank?
45
+ messages.map { |m| send_message(message: m, to: to, from: from) }
46
+ end
47
+
48
+ # Do not call this directly, instead see {Twilio::Rails::Phone::StartCallOperation}. Starts a phone call to and
49
+ # from the given phone numbers using the Twilio REST API directly. This does not store or manage any interactions
50
+ # in the database. The URL should come from the {Twilio::Rails::Phone::Tree} that is being used to start the call.
51
+ #
52
+ # @param url [String] the URL to use for the Twilio REST API call, probably from the {Twilio::Rails::Phone::Tree}.
53
+ # @param to [String] the phone number to make the call to.
54
+ # @param from [String] the phone number to make the call from.
55
+ # @param answering_machine_detection [true, false] whether or not to enable answering machine detection.
56
+ # @return [String] the SID returned from Twilio for the started call.
57
+ def start_call(url:, to:, from:, answering_machine_detection: true)
58
+ Twilio::Rails.config.logger.tagged(self) { |l| l.info("[start_call] to=#{ to } from=#{ from } url=#{ url } answering_machine_detection=#{ !!answering_machine_detection }") }
59
+ client.calls.create(
60
+ from: from,
61
+ to: to,
62
+ url: url,
63
+ machine_detection: ( answering_machine_detection ? "Enable" : "Disable" ),
64
+ async_amd: true,
65
+ async_amd_status_callback: "#{ Twilio::Rails.config.host }#{ ::Twilio::Rails::Engine.routes.url_helpers.phone_status_path(format: :xml, async_amd: "true") }",
66
+ async_amd_status_callback_method: "POST",
67
+ status_callback: "#{ Twilio::Rails.config.host }#{ ::Twilio::Rails::Engine.routes.url_helpers.phone_status_path(format: :xml) }",
68
+ status_callback_method: "POST",
69
+ status_callback_event: ["completed", "no-answer"],
70
+ # timeout: 30,
71
+ ).sid
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module Twilio
3
+ module Rails
4
+ # Provides scopes, validations, and convenience methods for a model that has an attribute `direction` with
5
+ # values "inbound" or "outbound".
6
+ module HasDirection
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ validates :direction, inclusion: { in: ["outbound", "inbound"] }
11
+
12
+ scope :outbound, -> { where(direction: "outbound") }
13
+ scope :inbound, -> { where(direction: "inbound") }
14
+ end
15
+
16
+ def inbound?
17
+ direction == "inbound"
18
+ end
19
+
20
+ def outbound?
21
+ direction == "outbound"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ module Twilio
3
+ module Rails
4
+ # Provides validations and reformatting on validation for a model that has an attribute `phone_number` that is
5
+ # that is unique and that matches {Twilio::Rails::Formatter.coerce_to_valid_phone_number}.
6
+ module HasPhoneNumber
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ validates :phone_number, uniqueness: { allow_blank: true, message: "already exists" }
11
+
12
+ before_validation :reformat_phone_number
13
+ end
14
+
15
+ def reformat_phone_number
16
+ current = Twilio::Rails::Formatter.coerce_to_valid_phone_number(phone_number)
17
+ self.phone_number = current if current
18
+
19
+ true
20
+ end
21
+
22
+ def valid_north_american_phone_number?
23
+ Twilio::Rails::Formatter.valid_north_american_phone_number?(phone_number)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ module Twilio
3
+ module Rails
4
+ # Provides convenience scopes for a model that has a `created_at` attribute.
5
+ module HasTimeScopes
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ scope :in_last, ->(time) { where(created_at: (Time.now - time)..(Time.now)) }
10
+ scope :in_previous, ->(time) { where(created_at: (Time.now - time - time)..(Time.now - time)) }
11
+
12
+ scope :in_last_24_hours, -> { in_last(1.day) }
13
+ scope :in_last_2_days, -> { in_last(2.days) }
14
+ scope :in_last_4_hours, -> { in_last(4.hours) }
15
+ scope :in_previous_24_hours, -> { in_previous(1.day) }
16
+ end
17
+ end
18
+ end
19
+ end