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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +61 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/pager_tree_integrations_manifest.js +1 -0
  6. data/app/assets/stylesheets/pager_tree/integrations/application.css +15 -0
  7. data/app/controllers/pager_tree/integrations/application_controller.rb +6 -0
  8. data/app/controllers/pager_tree/integrations/live_call_routing/twilio/v3_controller.rb +49 -0
  9. data/app/helpers/pager_tree/integrations/application_helper.rb +9 -0
  10. data/app/jobs/pager_tree/integrations/application_job.rb +6 -0
  11. data/app/jobs/pager_tree/integrations/outgoing_webhook_job.rb +12 -0
  12. data/app/mailers/pager_tree/integrations/application_mailer.rb +8 -0
  13. data/app/models/pager_tree/integrations/additional_datum.rb +41 -0
  14. data/app/models/pager_tree/integrations/alert.rb +60 -0
  15. data/app/models/pager_tree/integrations/apex_ping/v3.rb +69 -0
  16. data/app/models/pager_tree/integrations/application_record.rb +7 -0
  17. data/app/models/pager_tree/integrations/email/v3.rb +150 -0
  18. data/app/models/pager_tree/integrations/integration.rb +130 -0
  19. data/app/models/pager_tree/integrations/live_call_routing/twilio/v3.rb +332 -0
  20. data/app/models/pager_tree/integrations/outgoing_event.rb +25 -0
  21. data/app/models/pager_tree/integrations/outgoing_webhook/v3.rb +75 -0
  22. data/app/models/pager_tree/integrations/outgoing_webhook_delivery/hook_relay.rb +98 -0
  23. data/app/models/pager_tree/integrations/outgoing_webhook_delivery.rb +22 -0
  24. data/app/views/layouts/pager_tree/integrations/application.html.erb +15 -0
  25. data/app/views/pager_tree/integrations/apex_ping/v3/_form_options.html.erb +0 -0
  26. data/app/views/pager_tree/integrations/apex_ping/v3/_show_options.html.erb +0 -0
  27. data/app/views/pager_tree/integrations/email/v3/_form_options.html.erb +11 -0
  28. data/app/views/pager_tree/integrations/email/v3/_show_options.html.erb +17 -0
  29. data/app/views/pager_tree/integrations/live_call_routing/twilio/v3/_form_options.html.erb +80 -0
  30. data/app/views/pager_tree/integrations/live_call_routing/twilio/v3/_show_options.html.erb +181 -0
  31. data/app/views/pager_tree/integrations/outgoing_webhook/v3/_form_options.html.erb +50 -0
  32. data/app/views/pager_tree/integrations/outgoing_webhook/v3/_show_options.html.erb +61 -0
  33. data/app/views/shared/_password_visibility_button.html.erb +8 -0
  34. data/config/locales/en.yml +87 -0
  35. data/config/routes.rb +12 -0
  36. data/db/migrate/20220208195853_create_active_storage_tables.active_storage.rb +58 -0
  37. data/db/migrate/20220208195854_create_deferred_request_deferred_requests.deferred_request.rb +13 -0
  38. data/db/migrate/20220208195855_create_pager_tree_integrations_integrations.rb +10 -0
  39. data/db/migrate/20220215200426_create_pager_tree_integrations_outgoing_webhook_deliveries.rb +12 -0
  40. data/lib/generators/integration/USAGE +11 -0
  41. data/lib/generators/integration/integration_generator.rb +14 -0
  42. data/lib/generators/integration/templates/_form_options.html.erb.tt +8 -0
  43. data/lib/generators/integration/templates/_show_options.html.erb.tt +14 -0
  44. data/lib/generators/integration/templates/model.rb.tt +64 -0
  45. data/lib/generators/integration/templates/test.rb.tt +87 -0
  46. data/lib/pager_tree/integrations/engine.rb +11 -0
  47. data/lib/pager_tree/integrations/env.rb +36 -0
  48. data/lib/pager_tree/integrations/version.rb +5 -0
  49. data/lib/pager_tree/integrations.rb +29 -0
  50. data/lib/tasks/pager_tree/integrations_tasks.rake +4 -0
  51. 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