Class: SmsMessage
Overview
== Schema Information
Table name: sms_messages
Database name: primary
id :integer not null, primary key
autoreplied_at :datetime
body :text
direction :integer default("inbound"), not null
num_media :integer default(0), not null
raw_data :jsonb
recipient :string not null
sender :string not null
state :string default("draft"), not null
status_message :string
transmit_at :datetime
unread :boolean default(TRUE), not null
created_at :datetime not null
updated_at :datetime not null
creator_id :integer
recipient_party_id :integer
sender_party_id :integer
unique_id :string
updater_id :integer
Indexes
idx_direction_recipient (direction,recipient)
index_sms_messages_on_recipient (recipient)
index_sms_messages_on_recipient_party_id (recipient_party_id)
index_sms_messages_on_sender (sender)
index_sms_messages_on_sender_party_id (sender_party_id)
index_sms_messages_on_state (state)
index_sms_messages_on_transmit_at (transmit_at)
sms_messages_unique_id (unique_id) UNIQUE
Foreign Keys
fk_rails_... (recipient_party_id => parties.id)
fk_rails_... (sender_party_id => parties.id)
Constant Summary
collapse
- SMS_GLOBAL_NUMBERS =
['+18008755285', '+18475407775', '+18473183355', '+18473501641',
'+18473501655', '+18473489826', '+18473507429', '+18473501980',
'+18472629814', '+18473507262', '+18473507212']
Models::Auditable::ALWAYS_IGNORED
Instance Attribute Summary collapse
#creator, #updater
Has and belongs to many
collapse
Delegated Instance Attributes
collapse
Class Method Summary
collapse
Instance Method Summary
collapse
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation
#publish_event
Instance Attribute Details
#body ⇒ Object
69
|
# File 'app/models/sms_message.rb', line 69
validates :body, length: { maximum: 1600 }, if: :outbound?
|
#direction ⇒ Object
68
|
# File 'app/models/sms_message.rb', line 68
validates :sender, :recipient, :direction, presence: true
|
#recipient ⇒ Object
68
|
# File 'app/models/sms_message.rb', line 68
validates :sender, :recipient, :direction, presence: true
|
#sender ⇒ Object
68
|
# File 'app/models/sms_message.rb', line 68
validates :sender, :recipient, :direction, presence: true
|
Class Method Details
.between_participants ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are between participants. Active Record Scope
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
# File 'app/models/sms_message.rb', line 79
scope :between_participants, ->(*numbers) {
number1, number2 = [numbers].flatten.map(&:presence).compact.map { |n| PhoneNumber.parse_and_format(n) }.compact
if number1 && number2
where('
(sms_messages.sender = :number1 OR sms_messages.recipient = :number1)
AND
(sms_messages.sender = :number2 OR sms_messages.recipient = :number2)
', number1:, number2:)
elsif number = number1 || number2
for_numbers(number)
else
SmsMessage.none
end
}
|
.for_employee_ids ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are for employee ids. Active Record Scope
71
72
73
74
75
|
# File 'app/models/sms_message.rb', line 71
scope :for_employee_ids, ->(*rep_ids, include_global: true) {
sms_numbers = Employee.all_active_employees_sms_numbers(rep_ids:)
sms_numbers += SMS_GLOBAL_NUMBERS if include_global
for_numbers(sms_numbers)
}
|
.for_numbers ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are for numbers. Active Record Scope
76
77
78
|
# File 'app/models/sms_message.rb', line 76
scope :for_numbers, ->(*numbers) {
where('sms_messages.sender IN (:numbers) OR sms_messages.recipient IN (:numbers)', numbers: [numbers].flatten.compact.uniq)
}
|
.global_numbers_for_select ⇒ Object
285
286
287
|
# File 'app/models/sms_message.rb', line 285
def self.global_numbers_for_select
SMS_GLOBAL_NUMBERS.map { |n| [n, n] }
end
|
.match_inbound_unknown_senders(numbers: nil, party: nil, date_range: nil) ⇒ Object
Bulk inbound sender match
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
|
# File 'app/models/sms_message.rb', line 433
def self.match_inbound_unknown_senders(numbers: nil, party: nil, date_range: nil)
messages = SmsMessage.inbound.where(sender_party_id: nil)
messages = messages.where(created_at: date_range) if date_range
messages = messages.where(sender: numbers) if numbers.present?
messages.each do |m|
m.sender_party = party
m.sender_party ||= parties_matching_number(m.sender).active.find_by(type: 'Contact')
m.sender_party ||= parties_matching_number(m.sender).active.find_by(type: 'Customer')
m.sender_party ||= parties_matching_number(m.sender).inactive.find_by(type: 'Contact')
m.sender_party ||= parties_matching_number(m.sender).inactive.find_by(type: 'Customer')
m.save if m.sender_party
end
end
|
.match_outbound_unknown_recipient(numbers: nil, party: nil, date_range: nil) ⇒ Object
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
|
# File 'app/models/sms_message.rb', line 452
def self.match_outbound_unknown_recipient(numbers: nil, party: nil, date_range: nil)
messages = SmsMessage.outbound.where(recipient_party_id: nil)
messages = messages.where(created_at: date_range) if date_range
messages = messages.where(recipient: numbers) if numbers.present?
messages.each do |m|
m.recipient_party = party
m.recipient_party ||= parties_matching_number(m.recipient).active.where(type: 'Contact').first
m.recipient_party ||= parties_matching_number(m.recipient).active.where(type: 'Customer').first
m.recipient_party ||= parties_matching_number(m.recipient).inactive.where(type: 'Contact').first
m.recipient_party ||= parties_matching_number(m.recipient).inactive.where(type: 'Customer').first
m.save if m.recipient_party
end
end
|
.match_unknown(**args) ⇒ Object
427
428
429
430
|
# File 'app/models/sms_message.rb', line 427
def self.match_unknown(**args)
match_inbound_unknown_senders(**args)
match_outbound_unknown_recipient(**args)
end
|
.parties_matching_number(number) ⇒ Object
471
472
473
474
475
|
# File 'app/models/sms_message.rb', line 471
def self.parties_matching_number(number)
return Party.none unless number.present?
Party.joins(:contact_points).where(ContactPoint[:detail].eq(number))
end
|
.ransackable_scopes(auth_object = nil) ⇒ Object
105
106
107
|
# File 'app/models/sms_message.rb', line 105
def self.ransackable_scopes(auth_object = nil)
[:between_participants]
end
|
.unread ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are unread. Active Record Scope
97
|
# File 'app/models/sms_message.rb', line 97
scope :unread, -> { where(unread: true) }
|
.update_status(params) ⇒ Object
Processes events coming in from the twilio gateway basically discover
the message that was sent and update its status
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
|
# File 'app/models/sms_message.rb', line 358
def self.update_status(params)
return false unless params.present?
return false unless sid = params['SmsSid'].presence
return false unless msg = SmsMessage.outbound.where(unique_id: sid).first
msg.raw_data = params.except(:action, :controller)
case params['SmsStatus']
when 'delivered'
msg.trigger_delivered
when 'failed', 'undelivered'
msg.trigger_exception
else
msg.save
end
true
end
|
Instance Method Details
344
345
346
|
# File 'app/models/sms_message.rb', line 344
def all_related_sms_messages
SmsMessage.where('sms_messages.sender IN (:participants) OR sms_messages.recipient IN (:participants)', participants: [sender, recipient]).order('sms_messages.created_at DESC')
end
|
#auto_reply_outside_business_hours ⇒ Object
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
|
# File 'app/models/sms_message.rb', line 246
def auto_reply_outside_business_hours
return unless received_outside_business_hours? && autoreplied_at.nil? && !messages_in_conversation.where(transmit_at: 1.hour.ago..).exists?
return if PhoneNumber.shortcode?(sender)
body = 'Thank you for your message. We will get back to you during business hours, Mon-Fri from 7:30am to 5:30pm CST'
begin
TwilioClient.send_message(from: recipient, to: sender, body:)
update(autoreplied_at: Time.current)
rescue Twilio::REST::RestError => e
raise unless e.code == 21_610
Rails.logger.info "[SmsMessage] Auto-reply skipped - recipient #{sender} has unsubscribed from SMS"
end
end
|
#block_sender ⇒ Object
194
195
196
197
198
199
|
# File 'app/models/sms_message.rb', line 194
def block_sender
return false unless inbound?
return true if sender_sms_block
build_sender_sms_block.save
end
|
#conversation_id ⇒ Object
234
235
236
|
# File 'app/models/sms_message.rb', line 234
def conversation_id
[sender, recipient].sort
end
|
223
224
225
226
227
228
229
230
231
232
|
# File 'app/models/sms_message.rb', line 223
def create_media_urls
twilio_config = Heatwave::Configuration.fetch(:twilio)
base_url = twilio_config[:sms_base_wehook_url]
raise 'Unable to configure base URL for media download' unless base_url
uploads.map do |upload|
token = upload.active_download_token
"#{base_url}/media_download/#{token}"
end.presence
end
|
#deep_dup ⇒ Object
109
110
111
112
113
114
115
116
|
# File 'app/models/sms_message.rb', line 109
def deep_dup
deep_clone(
include: :uploads,
except: %i[unique_id transmit_at created_at updated_at]
) do |_original, copy|
copy.state = 'draft' if copy.is_a?(SmsMessage)
end
end
|
#editable? ⇒ Boolean
352
353
354
|
# File 'app/models/sms_message.rb', line 352
def editable?
draft? && outbound?
end
|
#enqueue_crm_navbar_refresh ⇒ Object
Only enqueue when the change can affect the navbar badge — i.e. the
inbound/unread tuple. Skips chatty outbound updates (state machine churn,
delivery callbacks) and any inbound update that doesn't move unread.
484
485
486
487
488
489
490
491
492
493
494
|
# File 'app/models/sms_message.rb', line 484
def enqueue_crm_navbar_refresh
return unless inbound?
if previously_new_record?
return unless unread?
elsif !saved_change_to_unread?
return
end
CrmNavbarFanoutWorker.perform_async('sms')
end
|
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
|
# File 'app/models/sms_message.rb', line 305
def fetch_media
return true unless num_media.positive?
begin
self.raw_data ||= {}
(0..(num_media - 1)).to_a.each do |i|
media_content_type = raw_data["MediaContentType#{i}"]
media_url = raw_data["MediaUrl#{i}"]
next unless media_url
uri = Addressable::URI.parse(media_url)
uri.user = Heatwave::Configuration.fetch(:twilio, :account_sid)
uri.password = Heatwave::Configuration.fetch(:twilio, :auth_token)
ext = Mime::Type.lookup(media_content_type).symbol.to_s
upload = Upload.uploadify_from_url(file_name: "attachment_#{i}.#{ext}",
url: uri.to_s,
category: nil,
resource: self)
uploads << upload
end
true
rescue Down::NotFound => e
self.status_message = e.to_s
ErrorReporting.warning(e, reason: 'twilio_media_expired', sms_message_id: id)
false
rescue Down::ClientError => e
self.status_message = e.to_s
ErrorReporting.warning(e, reason: 'twilio_media_access_denied', sms_message_id: id)
false
rescue StandardError => e
self.status_message = e.to_s
ErrorReporting.error(e)
false
end
end
|
#from_country_iso ⇒ Object
301
302
303
|
# File 'app/models/sms_message.rb', line 301
def from_country_iso
raw_data['FromCountry'].presence || PhoneNumber.parse(sender).country
end
|
#increment_publication_counters ⇒ Object
Increment the counter of our publications, sort of a way to keep track of popularity
517
518
519
|
# File 'app/models/sms_message.rb', line 517
def increment_publication_counters
publications.each { |p| p.increment!(:requested_counter) }
end
|
#literatures ⇒ Object
Alias for Uploads#literatures
510
|
# File 'app/models/sms_message.rb', line 510
delegate :literatures, to: :uploads
|
#mark_external_numbers_as_sms_enabled ⇒ Object
477
478
479
|
# File 'app/models/sms_message.rb', line 477
def mark_external_numbers_as_sms_enabled
ContactPoint.dialable.where('contact_points.detail = :sender OR contact_points.detail = :recipient', sender:, recipient:).sms_none.each(&:sms_enabled!)
end
|
#match_inbound_recipient ⇒ Object
379
380
381
382
383
384
385
386
387
|
# File 'app/models/sms_message.rb', line 379
def match_inbound_recipient
return unless inbound? && recipient_party.nil?
employee = Employee.joins(:contact_points, :employee_phone_status).active_employees.find_by(ContactPoint[:detail].eq(recipient))
return unless employee
self.recipient_party = employee
end
|
#match_inbound_sender ⇒ Object
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
|
# File 'app/models/sms_message.rb', line 411
def match_inbound_sender
return unless inbound?
prev_message = SmsMessage.outbound.order(created_at: :desc).where.not(recipient_party_id: nil).find_by(recipient: sender)
self.sender_party ||= prev_message.recipient_party if prev_message
self.sender_party ||= self.class.parties_matching_number(sender).active.order(created_at: :desc).find_by(type: 'Contact')
self.sender_party ||= self.class.parties_matching_number(sender).active.order(created_at: :desc).find_by(type: 'Customer')
self.sender_party ||= self.class.parties_matching_number(sender).inactive.order(created_at: :desc).find_by(type: 'Contact')
self.sender_party ||= self.class.parties_matching_number(sender).inactive.order(created_at: :desc).find_by(type: 'Customer')
end
|
#match_outbound_recipient ⇒ Object
398
399
400
401
402
403
404
405
406
407
408
409
|
# File 'app/models/sms_message.rb', line 398
def match_outbound_recipient
return unless outbound?
self.recipient_party ||= self.class.parties_matching_number(recipient).order(created_at: :desc).active.find_by(type: 'Contact')
self.recipient_party ||= self.class.parties_matching_number(recipient).order(created_at: :desc).active.find_by(type: 'Customer')
self.recipient_party ||= self.class.parties_matching_number(recipient).order(created_at: :desc).inactive.find_by(type: 'Contact')
self.recipient_party ||= self.class.parties_matching_number(recipient).order(created_at: :desc).inactive.find_by(type: 'Customer')
end
|
#match_outbound_sender ⇒ Object
389
390
391
392
393
394
395
396
|
# File 'app/models/sms_message.rb', line 389
def match_outbound_sender
return unless outbound? && sender_party.nil?
employee = Employee.joins(:contact_points, :employee_phone_status).active_employees.find_by(ContactPoint[:detail].eq(sender))
return unless employee
self.sender_party = employee
end
|
#messages_in_conversation ⇒ Object
238
239
240
|
# File 'app/models/sms_message.rb', line 238
def messages_in_conversation
self.class.between_participants(sender, recipient)
end
|
375
376
377
|
# File 'app/models/sms_message.rb', line 375
def no_media?
uploads.blank?
end
|
#publications ⇒ Object
512
513
514
|
# File 'app/models/sms_message.rb', line 512
def publications
Item.joins(:literature).merge(literatures)
end
|
#receive_message ⇒ Object
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
# File 'app/models/sms_message.rb', line 268
def receive_message
return false unless can_trigger_received?
mark_external_numbers_as_sms_enabled
auto_reply_outside_business_hours
match_inbound_recipient
match_inbound_sender
if fetch_media
trigger_received
else
trigger_exception
end
end
|
#received_outside_business_hours? ⇒ Boolean
242
243
244
|
# File 'app/models/sms_message.rb', line 242
def received_outside_business_hours?
!WorkingHours.in_working_hours?(created_at)
end
|
#recipient_do_not_text ⇒ DoNotCall
53
|
# File 'app/models/sms_message.rb', line 53
has_one :recipient_do_not_text, -> { where(do_not_text: true) }, class_name: 'DoNotCall', primary_key: :recipient, foreign_key: :number
|
#recipient_party ⇒ Party
50
|
# File 'app/models/sms_message.rb', line 50
belongs_to :recipient_party, class_name: 'Party', inverse_of: :recipient_sms_messages, optional: true
|
#recipient_sms_block ⇒ SmsBlock
52
|
# File 'app/models/sms_message.rb', line 52
has_one :recipient_sms_block, class_name: 'SmsBlock', primary_key: :recipient, foreign_key: :number
|
#recipient_to_s ⇒ Object
295
296
297
298
299
|
# File 'app/models/sms_message.rb', line 295
def recipient_to_s
r = "#{PhoneNumber.parse_and_format(recipient)}"
r << " (#{recipient_party.full_name})" if recipient_party
r
end
|
#send_message ⇒ Object
209
210
211
212
213
214
215
216
217
218
219
220
221
|
# File 'app/models/sms_message.rb', line 209
def send_message
return false unless valid? && can_trigger_send?
match_outbound_sender
match_outbound_recipient
mark_external_numbers_as_sms_enabled
media_urls = create_media_urls
res = TwilioClient.send_message(from: sender, to: recipient, body:, media_urls:)
self.unique_id = res.sid
trigger_send
end
|
#sender_blocked? ⇒ Boolean
187
188
189
190
191
192
|
# File 'app/models/sms_message.rb', line 187
def sender_blocked?
return false unless inbound?
SmsBlock.where(number: sender).exists?
end
|
#sender_do_not_text ⇒ DoNotCall
54
|
# File 'app/models/sms_message.rb', line 54
has_one :sender_do_not_text, -> { where(do_not_text: true) }, class_name: 'DoNotCall', primary_key: :sender, foreign_key: :number
|
#sender_party ⇒ Party
49
|
# File 'app/models/sms_message.rb', line 49
belongs_to :sender_party, class_name: 'Party', inverse_of: :sender_sms_messages, optional: true
|
#sender_sms_block ⇒ SmsBlock
51
|
# File 'app/models/sms_message.rb', line 51
has_one :sender_sms_block, class_name: 'SmsBlock', primary_key: :sender, foreign_key: :number
|
#sender_to_s ⇒ Object
289
290
291
292
293
|
# File 'app/models/sms_message.rb', line 289
def sender_to_s
r = "#{PhoneNumber.parse_and_format(sender)}"
r << " (#{sender_party.full_name})" if sender_party
r
end
|
#set_defaults ⇒ Object
496
497
498
499
500
501
502
503
504
505
506
507
|
# File 'app/models/sms_message.rb', line 496
def set_defaults
self.unread = false if outbound?
if (sn = PhoneNumber.parse_and_format(sender)).present?
self.sender = sn
end
if (rn = PhoneNumber.parse_and_format(recipient)).present?
self.recipient = rn
end
:continue
end
|
#to_s ⇒ Object
348
349
350
|
# File 'app/models/sms_message.rb', line 348
def to_s
"SMS #{id}"
end
|
#unblock_sender ⇒ Object
201
202
203
204
205
206
207
|
# File 'app/models/sms_message.rb', line 201
def unblock_sender
return false unless inbound?
return true unless sender_sms_block
sender_sms_block.destroy
true
end
|
#uploads ⇒ ActiveRecord::Relation<Upload>
56
|
# File 'app/models/sms_message.rb', line 56
has_and_belongs_to_many :uploads
|