pager_tree-integrations 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|