pager_tree-integrations 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/LICENSE +201 -0
- data/README.md +61 -0
- data/Rakefile +8 -0
- data/app/assets/config/pager_tree_integrations_manifest.js +1 -0
- data/app/assets/stylesheets/pager_tree/integrations/application.css +15 -0
- data/app/controllers/pager_tree/integrations/application_controller.rb +6 -0
- data/app/controllers/pager_tree/integrations/live_call_routing/twilio/v3_controller.rb +49 -0
- data/app/helpers/pager_tree/integrations/application_helper.rb +9 -0
- data/app/jobs/pager_tree/integrations/application_job.rb +6 -0
- data/app/jobs/pager_tree/integrations/outgoing_webhook_job.rb +12 -0
- data/app/mailers/pager_tree/integrations/application_mailer.rb +8 -0
- data/app/models/pager_tree/integrations/additional_datum.rb +41 -0
- data/app/models/pager_tree/integrations/alert.rb +60 -0
- data/app/models/pager_tree/integrations/apex_ping/v3.rb +69 -0
- data/app/models/pager_tree/integrations/application_record.rb +7 -0
- data/app/models/pager_tree/integrations/email/v3.rb +150 -0
- data/app/models/pager_tree/integrations/integration.rb +130 -0
- data/app/models/pager_tree/integrations/live_call_routing/twilio/v3.rb +332 -0
- data/app/models/pager_tree/integrations/outgoing_event.rb +25 -0
- data/app/models/pager_tree/integrations/outgoing_webhook/v3.rb +75 -0
- data/app/models/pager_tree/integrations/outgoing_webhook_delivery/hook_relay.rb +98 -0
- data/app/models/pager_tree/integrations/outgoing_webhook_delivery.rb +22 -0
- data/app/views/layouts/pager_tree/integrations/application.html.erb +15 -0
- data/app/views/pager_tree/integrations/apex_ping/v3/_form_options.html.erb +0 -0
- data/app/views/pager_tree/integrations/apex_ping/v3/_show_options.html.erb +0 -0
- data/app/views/pager_tree/integrations/email/v3/_form_options.html.erb +11 -0
- data/app/views/pager_tree/integrations/email/v3/_show_options.html.erb +17 -0
- data/app/views/pager_tree/integrations/live_call_routing/twilio/v3/_form_options.html.erb +80 -0
- data/app/views/pager_tree/integrations/live_call_routing/twilio/v3/_show_options.html.erb +181 -0
- data/app/views/pager_tree/integrations/outgoing_webhook/v3/_form_options.html.erb +50 -0
- data/app/views/pager_tree/integrations/outgoing_webhook/v3/_show_options.html.erb +61 -0
- data/app/views/shared/_password_visibility_button.html.erb +8 -0
- data/config/locales/en.yml +87 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20220208195853_create_active_storage_tables.active_storage.rb +58 -0
- data/db/migrate/20220208195854_create_deferred_request_deferred_requests.deferred_request.rb +13 -0
- data/db/migrate/20220208195855_create_pager_tree_integrations_integrations.rb +10 -0
- data/db/migrate/20220215200426_create_pager_tree_integrations_outgoing_webhook_deliveries.rb +12 -0
- data/lib/generators/integration/USAGE +11 -0
- data/lib/generators/integration/integration_generator.rb +14 -0
- data/lib/generators/integration/templates/_form_options.html.erb.tt +8 -0
- data/lib/generators/integration/templates/_show_options.html.erb.tt +14 -0
- data/lib/generators/integration/templates/model.rb.tt +64 -0
- data/lib/generators/integration/templates/test.rb.tt +87 -0
- data/lib/pager_tree/integrations/engine.rb +11 -0
- data/lib/pager_tree/integrations/env.rb +36 -0
- data/lib/pager_tree/integrations/version.rb +5 -0
- data/lib/pager_tree/integrations.rb +29 -0
- data/lib/tasks/pager_tree/integrations_tasks.rake +4 -0
- metadata +152 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
module PagerTree::Integrations
|
2
|
+
class Email::V3 < Integration
|
3
|
+
OPTIONS = [
|
4
|
+
{key: :allow_spam, type: :boolean, default: false},
|
5
|
+
{key: :dedup_threads, type: :boolean, default: true}
|
6
|
+
]
|
7
|
+
store_accessor :options, *OPTIONS.map { |x| x[:key] }.map(&:to_s), prefix: "option"
|
8
|
+
|
9
|
+
validates :option_allow_spam, inclusion: {in: [true, false]}
|
10
|
+
validates :option_dedup_threads, inclusion: {in: [true, false]}
|
11
|
+
|
12
|
+
after_initialize do
|
13
|
+
self.option_allow_spam ||= true
|
14
|
+
self.option_dedup_threads ||= false
|
15
|
+
end
|
16
|
+
|
17
|
+
# SPECIAL: override integration endpoint
|
18
|
+
def endpoint
|
19
|
+
domain = ::PagerTree::Integrations.integration_email_v3_domain
|
20
|
+
inbox = ::PagerTree::Integrations.integration_email_v3_inbox
|
21
|
+
postfix = ""
|
22
|
+
postfix = "_stg" if Rails.env.staging?
|
23
|
+
postfix = "_tst" if Rails.env.test?
|
24
|
+
postfix = "_dev" if Rails.env.development?
|
25
|
+
|
26
|
+
"#{inbox}#{postfix}+#{id}@#{domain}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def adapter_should_block?
|
30
|
+
return false if option_allow_spam == true
|
31
|
+
|
32
|
+
ses_spam_verdict = _get_header("X-SES-Spam-Verdict")&.value
|
33
|
+
|
34
|
+
if ses_spam_verdict.present?
|
35
|
+
return ses_spam_verdict != "PASS"
|
36
|
+
end
|
37
|
+
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def adapter_supports_incoming?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def adapter_thirdparty_id
|
46
|
+
_thirdparty_id
|
47
|
+
end
|
48
|
+
|
49
|
+
def adapter_action
|
50
|
+
:create
|
51
|
+
end
|
52
|
+
|
53
|
+
def adapter_process_create
|
54
|
+
Alert.new(
|
55
|
+
title: _title,
|
56
|
+
description: _description,
|
57
|
+
urgency: urgency,
|
58
|
+
thirdparty_id: _thirdparty_id,
|
59
|
+
dedup_keys: _dedup_keys,
|
60
|
+
additional_data: _additional_datums,
|
61
|
+
attachments: _attachments
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def _mail
|
68
|
+
@_mail ||= adapter_incoming_request_params.dig("mail")
|
69
|
+
end
|
70
|
+
|
71
|
+
def _inbound_email
|
72
|
+
@_inbound_email ||= adapter_incoming_request_params.dig("inbound_email")
|
73
|
+
end
|
74
|
+
|
75
|
+
def _thirdparty_id
|
76
|
+
_mail.message_id
|
77
|
+
end
|
78
|
+
|
79
|
+
def _dedup_keys
|
80
|
+
keys = [_thirdparty_id]
|
81
|
+
keys.concat(Array(_mail.references)) if option_dedup_threads
|
82
|
+
keys
|
83
|
+
end
|
84
|
+
|
85
|
+
def _title
|
86
|
+
_mail.subject
|
87
|
+
end
|
88
|
+
|
89
|
+
def _description
|
90
|
+
_body
|
91
|
+
end
|
92
|
+
|
93
|
+
def _body
|
94
|
+
return @_body if @_body
|
95
|
+
|
96
|
+
if _mail.multipart? && _mail.html_part
|
97
|
+
document = Nokogiri::HTML(_mail.html_part.body.decoded)
|
98
|
+
|
99
|
+
_attachments_hash.map do |attachment_hash|
|
100
|
+
attachment = attachment_hash[:original]
|
101
|
+
blob = attachment_hash[:blob]
|
102
|
+
|
103
|
+
if attachment.content_id.present?
|
104
|
+
# Remove the beginning and end < >
|
105
|
+
content_id = attachment.content_id[1...-1]
|
106
|
+
element = document.at_css "img[src='cid:#{content_id}']"
|
107
|
+
|
108
|
+
element&.replace "<action-text-attachment sgid=\"#{blob.attachable_sgid}\" content-type=\"#{attachment.content_type}\" filename=\"#{attachment.filename}\"></action-text-attachment>"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
@_body = ::Sanitize.document(document, Sanitize::Config::RELAXED)
|
113
|
+
elsif _mail.multipart? && _mail.text_part
|
114
|
+
@_body = _mail.text_part.body.decoded
|
115
|
+
else
|
116
|
+
@_body = _mail.decoded
|
117
|
+
end
|
118
|
+
|
119
|
+
@_body
|
120
|
+
end
|
121
|
+
|
122
|
+
def _attachments_hash
|
123
|
+
@_attachments_hash ||= _mail.attachments.map do |attachment|
|
124
|
+
blob = ActiveStorage::Blob.create_and_upload!(
|
125
|
+
io: StringIO.new(attachment.body.to_s),
|
126
|
+
filename: attachment.filename,
|
127
|
+
content_type: attachment.content_type
|
128
|
+
)
|
129
|
+
{original: attachment, blob: blob}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def _attachments
|
134
|
+
_attachments_hash.map { |attachment_hash| attachment_hash[:blob] }
|
135
|
+
end
|
136
|
+
|
137
|
+
# TODO: Implement any additional data that should be shown in the alert with high priority (be picky as to 'very important' information)
|
138
|
+
def _additional_datums
|
139
|
+
[
|
140
|
+
AdditionalDatum.new(format: "email", label: "From", value: _mail.from),
|
141
|
+
AdditionalDatum.new(format: "email", label: "To", value: _mail.to),
|
142
|
+
AdditionalDatum.new(format: "email", label: "CCs", value: _mail.cc)
|
143
|
+
]
|
144
|
+
end
|
145
|
+
|
146
|
+
def _get_header(name)
|
147
|
+
_mail.header_fields.find { |x| x.name == name }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module PagerTree::Integrations
|
2
|
+
class Integration < PagerTree::Integrations.integration_parent_class.constantize
|
3
|
+
serialize :options, JSON
|
4
|
+
encrypts :options
|
5
|
+
|
6
|
+
GENERIC_OPTIONS = [
|
7
|
+
{key: :title_template, type: :string, default: nil},
|
8
|
+
{key: :title_template_enabled, type: :boolean, default: false},
|
9
|
+
{key: :description_template, type: :string, default: nil},
|
10
|
+
{key: :description_template_enabled, type: :string, default: false}
|
11
|
+
]
|
12
|
+
store_accessor :options, *GENERIC_OPTIONS.map { |x| x[:key] }.map(&:to_s), prefix: "option"
|
13
|
+
|
14
|
+
before_validation :cast_types
|
15
|
+
|
16
|
+
attribute :option_title_template_enabled, :boolean, default: false
|
17
|
+
attribute :option_description_template_enabled, :boolean, default: false
|
18
|
+
attribute :option_title_template, :string, default: nil
|
19
|
+
attribute :option_description_template, :string, default: nil
|
20
|
+
|
21
|
+
# careful controller is not always guaranteed
|
22
|
+
attribute :adapter_controller
|
23
|
+
# for handling incoming requests
|
24
|
+
attribute :adapter_incoming_request_params
|
25
|
+
# for getting the most data, but not always guaranteed
|
26
|
+
attribute :adapter_incoming_deferred_request
|
27
|
+
# alert if found (by thirdparty id)
|
28
|
+
attribute :adapter_alert
|
29
|
+
# the outgoing event
|
30
|
+
attribute :adapter_outgoing_event
|
31
|
+
|
32
|
+
# START basic incoming functions
|
33
|
+
def adapter_supports_incoming?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
# A unique identifier for this integration/alert
|
38
|
+
def adapter_thirdparty_id
|
39
|
+
ULID.generate
|
40
|
+
end
|
41
|
+
|
42
|
+
def adapter_incoming_can_defer?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns :create, :acknowledge, :resolve, or :other
|
47
|
+
def adapter_action
|
48
|
+
:other
|
49
|
+
end
|
50
|
+
|
51
|
+
def adapter_process_create
|
52
|
+
end
|
53
|
+
# END basic incoming functions
|
54
|
+
|
55
|
+
# START basic outgoing functions
|
56
|
+
def adapter_supports_outgoing?
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def adapter_outgoing_interest?(event_name)
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
def adapter_process_outgoing
|
65
|
+
end
|
66
|
+
# END basic outgoing functions
|
67
|
+
|
68
|
+
# START basic show functions
|
69
|
+
def adapter_show_alerts?
|
70
|
+
adapter_supports_incoming?
|
71
|
+
end
|
72
|
+
|
73
|
+
def adapter_show_logs?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
def adapter_show_outgoing_webhook_delivery?
|
78
|
+
false
|
79
|
+
end
|
80
|
+
# END basic show functions
|
81
|
+
|
82
|
+
def adapter_response_rate_limit
|
83
|
+
adapter_controller&.head(:not_found)
|
84
|
+
end
|
85
|
+
|
86
|
+
def adapter_response_disabled
|
87
|
+
adapter_controller&.head(:method_not_allowed)
|
88
|
+
end
|
89
|
+
|
90
|
+
def adapter_response_inactive_subscription
|
91
|
+
adapter_controller&.head(:payment_required)
|
92
|
+
end
|
93
|
+
|
94
|
+
def adapter_response_upgrade
|
95
|
+
adapter_controller&.head(:payment_required)
|
96
|
+
end
|
97
|
+
|
98
|
+
def adapter_response_maintenance_mode
|
99
|
+
adapter_controller&.head(:ok)
|
100
|
+
end
|
101
|
+
|
102
|
+
def adapter_response_blocked
|
103
|
+
adapter_controller&.head(:bad_request)
|
104
|
+
end
|
105
|
+
|
106
|
+
def adapter_response_deferred
|
107
|
+
adapter_controller&.head(:ok)
|
108
|
+
end
|
109
|
+
|
110
|
+
def adapter_response_incoming
|
111
|
+
adapter_controller&.head(:ok)
|
112
|
+
end
|
113
|
+
|
114
|
+
def cast_types
|
115
|
+
(self.class.const_get(:GENERIC_OPTIONS) + self.class.const_get(:OPTIONS)).each do |option|
|
116
|
+
key = option[:key]
|
117
|
+
value = send("option_#{key}")
|
118
|
+
type = option[:type]
|
119
|
+
|
120
|
+
value = ActiveModel::Type::Boolean.new.cast(value) if type == :boolean
|
121
|
+
value = ActiveModel::Type::String.new.cast(value) if type == :string
|
122
|
+
value = ActiveModel::Type::Integer.new.cast(value) if type == :integer
|
123
|
+
|
124
|
+
value = option[:default] if value.nil? && option.has_key?(:default)
|
125
|
+
|
126
|
+
send("option_#{key}=", value)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,332 @@
|
|
1
|
+
module PagerTree::Integrations
|
2
|
+
class LiveCallRouting::Twilio::V3 < Integration
|
3
|
+
OPTIONS = [
|
4
|
+
{key: :account_sid, type: :string, default: nil},
|
5
|
+
{key: :api_key, type: :string, default: nil},
|
6
|
+
{key: :api_secret, type: :string, default: nil},
|
7
|
+
{key: :force_input, type: :boolean, default: false},
|
8
|
+
{key: :record, type: :boolean, default: false},
|
9
|
+
{key: :record_email, type: :string, default: ""}
|
10
|
+
]
|
11
|
+
store_accessor :options, *OPTIONS.map { |x| x[:key] }.map(&:to_s), prefix: "option"
|
12
|
+
|
13
|
+
has_one_attached :option_connect_now_media
|
14
|
+
has_one_attached :option_music_media
|
15
|
+
has_one_attached :option_no_answer_media
|
16
|
+
has_one_attached :option_no_answer_thank_you_media
|
17
|
+
has_one_attached :option_please_wait_media
|
18
|
+
has_one_attached :option_welcome_media
|
19
|
+
|
20
|
+
validates :option_account_sid, presence: true
|
21
|
+
validates :option_api_key, presence: true
|
22
|
+
validates :option_api_secret, presence: true
|
23
|
+
validates :option_force_input, inclusion: {in: [true, false]}
|
24
|
+
validates :option_record, inclusion: {in: [true, false]}
|
25
|
+
validate :validate_record_emails
|
26
|
+
|
27
|
+
after_initialize do
|
28
|
+
self.option_account_sid ||= nil
|
29
|
+
self.option_api_key ||= nil
|
30
|
+
self.option_api_secret ||= nil
|
31
|
+
self.option_force_input ||= false
|
32
|
+
self.option_record ||= false
|
33
|
+
self.option_record_email ||= ""
|
34
|
+
end
|
35
|
+
|
36
|
+
SPEAK_OPTIONS = {
|
37
|
+
language: "en",
|
38
|
+
voice: "man"
|
39
|
+
}
|
40
|
+
|
41
|
+
TWILIO_LIVECALL_CONNECT_NOW = "https://app.pagertree.com/assets/sounds/you-are-now-being-connected.mp3"
|
42
|
+
TWILIO_LIVECALL_MUSIC = "http://com.twilio.sounds.music.s3.amazonaws.com/oldDog_-_endless_goodbye_%28instr.%29.mp3"
|
43
|
+
TWILIO_LIVECALL_PLEASE_WAIT = "https://app.pagertree.com/assets/sounds/please-wait.mp3"
|
44
|
+
|
45
|
+
def option_connect_now_media_url
|
46
|
+
option_connect_now_media&.url || TWILIO_LIVECALL_CONNECT_NOW
|
47
|
+
end
|
48
|
+
|
49
|
+
def option_music_media_url
|
50
|
+
option_music_media&.url || TWILIO_LIVECALL_MUSIC
|
51
|
+
end
|
52
|
+
|
53
|
+
def option_please_wait_media_url
|
54
|
+
option_please_wait_media&.url || TWILIO_LIVECALL_PLEASE_WAIT
|
55
|
+
end
|
56
|
+
|
57
|
+
def option_record_emails=(x)
|
58
|
+
self.option_record_email = Array(x).join(",")
|
59
|
+
end
|
60
|
+
|
61
|
+
def option_record_emails
|
62
|
+
self.option_record_email.split(",")
|
63
|
+
end
|
64
|
+
|
65
|
+
def option_record_emails_list=(x)
|
66
|
+
# what comes in as json, via tagify
|
67
|
+
uniq_array = []
|
68
|
+
begin
|
69
|
+
uniq_array = JSON.parse(x).map { |y| y["value"] }.uniq
|
70
|
+
rescue JSON::ParserError => exception
|
71
|
+
Rails.logger.debug(exception)
|
72
|
+
end
|
73
|
+
|
74
|
+
self.option_record_emails = uniq_array
|
75
|
+
end
|
76
|
+
|
77
|
+
def option_record_emails_list
|
78
|
+
option_record_emails
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_record_emails
|
82
|
+
errors.add(:record_emails, "must be a valid email") if option_record_emails.any? { |x| !x.match(URI::MailTo::EMAIL_REGEXP) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def adapter_supports_incoming?
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
def adapter_supports_outgoing?
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
def adapter_incoming_can_defer?
|
94
|
+
false
|
95
|
+
end
|
96
|
+
|
97
|
+
def adapter_action
|
98
|
+
:create
|
99
|
+
end
|
100
|
+
|
101
|
+
def adapter_thirdparty_id
|
102
|
+
_thirdparty_id
|
103
|
+
end
|
104
|
+
|
105
|
+
def adapter_process_create
|
106
|
+
Alert.new(
|
107
|
+
title: _title,
|
108
|
+
urgency: urgency,
|
109
|
+
|
110
|
+
thirdparty_id: _thirdparty_id,
|
111
|
+
dedup_keys: [_thirdparty_id],
|
112
|
+
additional_data: _additional_datums
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
def adapter_response_incoming
|
117
|
+
if _teams_size == 0
|
118
|
+
_twiml.say(message: "This integration is not configured to route to any teams. Goodbye", **SPEAK_OPTIONS)
|
119
|
+
_twiml.hangup
|
120
|
+
return adapter_controller&.render(xml: _twiml.to_xml)
|
121
|
+
end
|
122
|
+
|
123
|
+
if !adapter_alert.meta["live_call_welcome"] && option_welcome_media.present?
|
124
|
+
adapter_alert.logs.create!(message: "Play welcome media to caller.")
|
125
|
+
_twiml.play(url: option_welcome_media.url)
|
126
|
+
adapter_alert.meta["live_call_welcome"] = true
|
127
|
+
adapter_alert.save!
|
128
|
+
end
|
129
|
+
|
130
|
+
if selected_team
|
131
|
+
adapter_alert.logs.create!(message: "Caller selected team '#{selected_team.name}'. Playing please wait media.")
|
132
|
+
_twiml.play(url: option_please_wait_media&.url || TWILIO_LIVECALL_PLEASE_WAIT)
|
133
|
+
friendly_name = adapter_alert.id
|
134
|
+
|
135
|
+
# create the queue and save it off
|
136
|
+
queue = _client.queues.create(friendly_name: friendly_name)
|
137
|
+
adapter_alert.meta["live_call_queue_sid"] = queue.sid
|
138
|
+
adapter_alert.save!
|
139
|
+
|
140
|
+
_twiml.enqueue(
|
141
|
+
name: friendly_name,
|
142
|
+
action: PagerTree::Integrations::Engine.routes.url_helpers.queue_status_live_call_routing_twilio_v3_path(id, thirdparty_id: _thirdparty_id),
|
143
|
+
method: "POST",
|
144
|
+
wait_url: PagerTree::Integrations::Engine.routes.url_helpers.music_live_call_routing_twilio_v3_path(id, thirdparty_id: _thirdparty_id),
|
145
|
+
wait_url_method: "GET"
|
146
|
+
)
|
147
|
+
else
|
148
|
+
adapter_alert.meta["live_call_repeat_count"] ||= 0
|
149
|
+
adapter_alert.meta["live_call_repeat_count"] += 1
|
150
|
+
adapter_alert.save!
|
151
|
+
|
152
|
+
if adapter_alert.meta["live_call_repeat_count"] <= 3
|
153
|
+
adapter_alert.logs.create!(message: "Caller has not selected a team. Playing team options.")
|
154
|
+
_twiml.gather numDigits: _teams_size.to_s.size, timeout: 30 do |g|
|
155
|
+
3.times do
|
156
|
+
g.say(message: _teams_message, **SPEAK_OPTIONS)
|
157
|
+
g.pause(length: 1)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
else
|
161
|
+
adapter_alert.logs.create!(message: "Caller input bad input (too many times). Hangup.")
|
162
|
+
_twiml.say(message: "Too much invalid input. Goodbye.", **SPEAK_OPTIONS)
|
163
|
+
_twiml.hangup
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
adapter_controller&.render(xml: _twiml.to_xml)
|
168
|
+
end
|
169
|
+
|
170
|
+
def adapter_response_disabled
|
171
|
+
_twiml.say(message: "This integration is currently disabled. Goodbye!", **SPEAK_OPTIONS)
|
172
|
+
_twiml.hangup
|
173
|
+
|
174
|
+
adapter_controller&.render(xml: _twiml.to_xml)
|
175
|
+
end
|
176
|
+
|
177
|
+
def adapter_response_upgrade
|
178
|
+
_twiml.say(message: "This account must be upgraded to use live call routing. Goodbye!", **SPEAK_OPTIONS)
|
179
|
+
_twiml.hangup
|
180
|
+
|
181
|
+
adapter_controller&.render(xml: _twiml.to_xml)
|
182
|
+
end
|
183
|
+
|
184
|
+
def adapter_response_maintenance_mode
|
185
|
+
_twiml.say(message: "This integration is currently in maintenance mode. Goodbye!", **SPEAK_OPTIONS)
|
186
|
+
_twiml.hangup
|
187
|
+
|
188
|
+
adapter_controller&.render(xml: _twiml.to_xml)
|
189
|
+
end
|
190
|
+
|
191
|
+
def adapter_response_music
|
192
|
+
_twiml.play(url: option_music_media_url, loop: 0)
|
193
|
+
adapter_controller&.render(xml: _twiml.to_xml)
|
194
|
+
end
|
195
|
+
|
196
|
+
def response_dropped
|
197
|
+
recording_url = adapter_incoming_request_params.dig("RecordingUrl")
|
198
|
+
|
199
|
+
if recording_url
|
200
|
+
if option_no_answer_thank_you_media.present?
|
201
|
+
_twiml.play(url: option_no_answer_thank_you_media.url)
|
202
|
+
else
|
203
|
+
_twiml.say(message: "Thank you for your message. Goodbye.")
|
204
|
+
end
|
205
|
+
_twiml.hangup
|
206
|
+
|
207
|
+
adapter_alert.additional_data.push(AdditionalDatum.new(format: "link", label: "Voicemail", value: recording_url).to_h)
|
208
|
+
adapter_alert.save!
|
209
|
+
|
210
|
+
adapter_alert.logs.create!(message: "Caller left a <a href='#{recording_url}' target='_blank'>voicemail</a>.")
|
211
|
+
|
212
|
+
adapter_record_emails.each do |email|
|
213
|
+
TwilioLiveCallRouting::V3Mailer.with(email: email, alert: alert).call_recording.deliver_later
|
214
|
+
end
|
215
|
+
elsif record
|
216
|
+
_twiml.play(url: option_no_answer_media_url)
|
217
|
+
_twiml.record(max_length: 60)
|
218
|
+
else
|
219
|
+
_twiml.say(message: "No one is available to answer this call. Goodbye.")
|
220
|
+
_twiml.hangup
|
221
|
+
end
|
222
|
+
|
223
|
+
controller.render(xml: _twiml.to_xml)
|
224
|
+
end
|
225
|
+
|
226
|
+
def adapter_process_queue_status_deferred
|
227
|
+
queue_result = adapter_incoming_request_params.dig("QueueResult")
|
228
|
+
adapter_source_log&.sublog("Processing queus status #{queue_result}")
|
229
|
+
|
230
|
+
if queue_result == "hangup"
|
231
|
+
self.adapter_alert = alerts.find_by(thirdparty_id: _thirdparty_id)
|
232
|
+
queue_destroy
|
233
|
+
end
|
234
|
+
|
235
|
+
adapter_source_log&.save!
|
236
|
+
end
|
237
|
+
|
238
|
+
def perform_outgoing(**params)
|
239
|
+
event = params[:event]
|
240
|
+
if event == "alert.acknowledged"
|
241
|
+
on_acknowledge
|
242
|
+
elsif event == "alert.dropped"
|
243
|
+
on_drop
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
def _thirdparty_id
|
250
|
+
adapter_incoming_request_params.dig("CallSid")
|
251
|
+
end
|
252
|
+
|
253
|
+
def _title
|
254
|
+
"Incoming call from #{adapter_incoming_request_params.dig("From")}"
|
255
|
+
end
|
256
|
+
|
257
|
+
def _additional_datums
|
258
|
+
[
|
259
|
+
AdditionalDatum.new(format: "phone", label: "Caller Phone", value: adapter_incoming_request_params.dig("From")),
|
260
|
+
AdditionalDatum.new(format: "text", label: "Caller City", value: adapter_incoming_request_params.dig("CallerCity")),
|
261
|
+
AdditionalDatum.new(format: "text", label: "Caller State", value: adapter_incoming_request_params.dig("CallerState")),
|
262
|
+
AdditionalDatum.new(format: "text", label: "Caller Zipcode", value: adapter_incoming_request_params.dig("CallerZipcode")),
|
263
|
+
AdditionalDatum.new(format: "text", label: "Caller Country", value: adapter_incoming_request_params.dig("CallerCountry"))
|
264
|
+
]
|
265
|
+
end
|
266
|
+
|
267
|
+
def _client
|
268
|
+
@_client ||= ::Twilio::REST::Client.new(self.option_api_key, self.option_api_secret, self.option_account_sid)
|
269
|
+
end
|
270
|
+
|
271
|
+
def _call
|
272
|
+
@_call ||= _client.calls(call_sid).fetch
|
273
|
+
end
|
274
|
+
|
275
|
+
def _twiml
|
276
|
+
@_twiml ||= ::Twilio::TwiML::VoiceResponse.new
|
277
|
+
end
|
278
|
+
|
279
|
+
def _teams_size
|
280
|
+
@_teams_size ||= teams.size
|
281
|
+
end
|
282
|
+
|
283
|
+
def _teams_sorted
|
284
|
+
@_teams_sorted ||= teams.order(name: :desc)
|
285
|
+
end
|
286
|
+
|
287
|
+
def _teams_message
|
288
|
+
"Make a selection followed by the pound sign. " + _teams_sorted.each_with_index.map { |team, index| "#{index + 1} for #{team.name}." }.join(" ")
|
289
|
+
end
|
290
|
+
|
291
|
+
def _digits
|
292
|
+
@_digits ||= adapter_incoming_request_params.dig("Digits")&.to_i
|
293
|
+
end
|
294
|
+
|
295
|
+
def selected_team
|
296
|
+
return nil if _teams_size == 0
|
297
|
+
return _teams_sorted.first if _teams_size == 1 && option_force_input == false
|
298
|
+
return _teams_sorted[_digits - 1] if _digits.present?
|
299
|
+
nil
|
300
|
+
end
|
301
|
+
|
302
|
+
def on_acknowledge
|
303
|
+
# log that we are going to transfer
|
304
|
+
adapter_alert.logs.create!(message: "Attempting to transfer the call...")
|
305
|
+
|
306
|
+
# try to transfer the caller
|
307
|
+
number = "+19402733696"
|
308
|
+
_twiml.play(url: option_connect_now_media_url)
|
309
|
+
_twiml.pause(length: 1)
|
310
|
+
_twiml.dial(number: number, caller_id: _call.to, answer_on_bridge: true)
|
311
|
+
_call.update(twiml: _twiml.to_xml)
|
312
|
+
|
313
|
+
# log if we successfully transfered or failed
|
314
|
+
adapter_alert.logs.create!(message: "Tranferring the call succeeded.")
|
315
|
+
end
|
316
|
+
|
317
|
+
def on_drop
|
318
|
+
_call.update(url: PagerTree::Integrations::Engine.routes.url_helpers.dropped_twilio_live_call_routing_v3_url(id, thirdparty_id: thirdparty_id))
|
319
|
+
end
|
320
|
+
|
321
|
+
def queue_destroy
|
322
|
+
if (queue_sid = adapter_alert&.meta&.fetch("live_call_queue_sid", nil))
|
323
|
+
begin
|
324
|
+
_client.queues(queue_sid).delete
|
325
|
+
adapter_source_log&.sublog("Successfully destroyed queue")
|
326
|
+
rescue => exception
|
327
|
+
adapter_source_log&.sublog("Failed to destroy queue - #{exception.message}")
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module PagerTree::Integrations
|
2
|
+
class OutgoingEvent
|
3
|
+
include ActiveModel::Model
|
4
|
+
include ActiveModel::API
|
5
|
+
extend ActiveModel::Callbacks
|
6
|
+
|
7
|
+
attr_accessor :event_name
|
8
|
+
attr_accessor :item
|
9
|
+
attr_accessor :changes
|
10
|
+
|
11
|
+
define_model_callbacks :initialize
|
12
|
+
|
13
|
+
def initialize(params = {})
|
14
|
+
run_callbacks :initialize do
|
15
|
+
super(params)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
after_initialize do
|
20
|
+
self.event_name ||= nil
|
21
|
+
self.item ||= nil
|
22
|
+
self.changes ||= nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module PagerTree::Integrations
|
2
|
+
class OutgoingWebhook::V3 < Integration
|
3
|
+
OPTIONS = [
|
4
|
+
{key: :webhook_url, type: :string, default: nil},
|
5
|
+
{key: :username, type: :string, default: nil},
|
6
|
+
{key: :password, type: :string, default: nil},
|
7
|
+
{key: :alert_created, type: :boolean, default: false},
|
8
|
+
{key: :alert_open, type: :boolean, default: false},
|
9
|
+
{key: :alert_acknowledged, type: :boolean, default: false},
|
10
|
+
{key: :alert_rejected, type: :boolean, default: false},
|
11
|
+
{key: :alert_timeout, type: :boolean, default: false},
|
12
|
+
{key: :alert_resolved, type: :boolean, default: false},
|
13
|
+
{key: :alert_dropped, type: :boolean, default: false},
|
14
|
+
{key: :alert_handoff, type: :boolean, default: false},
|
15
|
+
{key: :template, type: :string, default: nil},
|
16
|
+
{key: :send_linked, type: :boolean, default: false}
|
17
|
+
]
|
18
|
+
store_accessor :options, *OPTIONS.map { |x| x[:key] }.map(&:to_s), prefix: "option"
|
19
|
+
|
20
|
+
validates :option_webhook_url, presence: true, url: {no_local: true}
|
21
|
+
validates :option_alert_created, inclusion: {in: [true, false]}
|
22
|
+
validates :option_alert_open, inclusion: {in: [true, false]}
|
23
|
+
validates :option_alert_acknowledged, inclusion: {in: [true, false]}
|
24
|
+
validates :option_alert_rejected, inclusion: {in: [true, false]}
|
25
|
+
validates :option_alert_timeout, inclusion: {in: [true, false]}
|
26
|
+
validates :option_alert_resolved, inclusion: {in: [true, false]}
|
27
|
+
validates :option_alert_dropped, inclusion: {in: [true, false]}
|
28
|
+
validates :option_alert_handoff, inclusion: {in: [true, false]}
|
29
|
+
validates :option_send_linked, inclusion: {in: [true, false]}
|
30
|
+
|
31
|
+
after_initialize do
|
32
|
+
self.option_alert_created ||= false
|
33
|
+
self.option_alert_open ||= false
|
34
|
+
self.option_alert_acknowledged ||= false
|
35
|
+
self.option_alert_rejected ||= false
|
36
|
+
self.option_alert_timeout ||= false
|
37
|
+
self.option_alert_resolved ||= false
|
38
|
+
self.option_alert_dropped ||= false
|
39
|
+
self.option_alert_handoff ||= false
|
40
|
+
self.option_send_linked ||= false
|
41
|
+
self.option_template ||= ""
|
42
|
+
end
|
43
|
+
|
44
|
+
def adapter_supports_outgoing?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def adapter_show_outgoing_webhook_delivery?
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def adapter_outgoing_interest?(event_name)
|
53
|
+
try("option_#{event_name}") || false
|
54
|
+
end
|
55
|
+
|
56
|
+
def adapter_process_outgoing
|
57
|
+
body = {
|
58
|
+
data: adapter_outgoing_event.item,
|
59
|
+
type: adapter_outgoing_event.event_name
|
60
|
+
}
|
61
|
+
|
62
|
+
# create the delivery, save it, and send it later
|
63
|
+
outgoing_webhook_delivery = OutgoingWebhookDelivery.factory(
|
64
|
+
resource: self,
|
65
|
+
url: option_webhook_url,
|
66
|
+
auth: {username: option_username, password: option_password},
|
67
|
+
body: body
|
68
|
+
)
|
69
|
+
outgoing_webhook_delivery.save!
|
70
|
+
outgoing_webhook_delivery.deliver_later
|
71
|
+
|
72
|
+
outgoing_webhook_delivery
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|