pager_tree-integrations 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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