Class: SmsMessage
Overview
rubocop:disable Metrics/ClassLength
== Schema Information
Table name: sms_messages
Database name: primary
id :integer not null, primary key
autoreplied_at :datetime
body :text
direction :enum 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'].freeze
Models::Auditable::ALWAYS_IGNORED
Constants included
from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
#creator, #updater
Has and belongs to many
collapse
Delegated Instance Attributes
collapse
Class Method Summary
collapse
Instance Method Summary
collapse
#publish_event
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation
config
#after_commit
Instance Attribute Details
#body ⇒ Object
86
|
# File 'app/models/sms_message.rb', line 86
validates :body, length: { maximum: 1600 }, if: :outbound?
|
#direction ⇒ Object
85
|
# File 'app/models/sms_message.rb', line 85
validates :sender, :recipient, :direction, presence: true
|
#recipient ⇒ Object
85
|
# File 'app/models/sms_message.rb', line 85
validates :sender, :recipient, :direction, presence: true
|
#sender ⇒ Object
85
|
# File 'app/models/sms_message.rb', line 85
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
# File 'app/models/sms_message.rb', line 96
scope :between_participants, ->(*numbers) {
number1, number2 = [numbers].flatten.filter_map(&:presence).filter_map { |n| PhoneNumber.parse_and_format(n) }
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
88
89
90
91
92
|
# File 'app/models/sms_message.rb', line 88
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
93
94
95
|
# File 'app/models/sms_message.rb', line 93
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
319
320
321
|
# File 'app/models/sms_message.rb', line 319
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
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
|
# File 'app/models/sms_message.rb', line 467
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
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
|
# File 'app/models/sms_message.rb', line 486
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
461
462
463
464
|
# File 'app/models/sms_message.rb', line 461
def self.match_unknown(**args)
match_inbound_unknown_senders(**args)
match_outbound_unknown_recipient(**args)
end
|
.parties_matching_number(number) ⇒ Object
505
506
507
508
509
|
# File 'app/models/sms_message.rb', line 505
def self.parties_matching_number(number)
return Party.none if number.blank?
Party.joins(:contact_points).where(ContactPoint[:detail].eq(number))
end
|
.ransackable_scopes(_auth_object = nil) ⇒ Object
122
123
124
|
# File 'app/models/sms_message.rb', line 122
def self.ransackable_scopes(_auth_object = nil)
[:between_participants]
end
|
.unread ⇒ ActiveRecord::Relation<SmsMessage>
A relation of SmsMessages that are unread. Active Record Scope
114
|
# File 'app/models/sms_message.rb', line 114
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
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
|
# File 'app/models/sms_message.rb', line 392
def self.update_status(params)
return false if params.blank?
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
378
379
380
|
# File 'app/models/sms_message.rb', line 378
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
|
# File 'app/models/sms_message.rb', line 280
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
228
229
230
231
232
233
|
# File 'app/models/sms_message.rb', line 228
def block_sender
return false unless inbound?
return true if sender_sms_block
build_sender_sms_block.save
end
|
#conversation_id ⇒ Object
268
269
270
|
# File 'app/models/sms_message.rb', line 268
def conversation_id
[sender, recipient].sort
end
|
257
258
259
260
261
262
263
264
265
266
|
# File 'app/models/sms_message.rb', line 257
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
126
127
128
129
130
131
132
133
|
# File 'app/models/sms_message.rb', line 126
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
386
387
388
|
# File 'app/models/sms_message.rb', line 386
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.
518
519
520
521
522
523
524
525
526
527
528
|
# File 'app/models/sms_message.rb', line 518
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
|
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
|
# File 'app/models/sms_message.rb', line 339
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
335
336
337
|
# File 'app/models/sms_message.rb', line 335
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
551
552
553
|
# File 'app/models/sms_message.rb', line 551
def increment_publication_counters
publications.each { |p| p.increment!(:requested_counter) }
end
|
#literatures ⇒ Object
Alias for Uploads#literatures
544
|
# File 'app/models/sms_message.rb', line 544
delegate :literatures, to: :uploads
|
#mark_external_numbers_as_sms_enabled ⇒ Object
511
512
513
|
# File 'app/models/sms_message.rb', line 511
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
413
414
415
416
417
418
419
420
421
|
# File 'app/models/sms_message.rb', line 413
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
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
|
# File 'app/models/sms_message.rb', line 445
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
432
433
434
435
436
437
438
439
440
441
442
443
|
# File 'app/models/sms_message.rb', line 432
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
423
424
425
426
427
428
429
430
|
# File 'app/models/sms_message.rb', line 423
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
272
273
274
|
# File 'app/models/sms_message.rb', line 272
def messages_in_conversation
self.class.between_participants(sender, recipient)
end
|
409
410
411
|
# File 'app/models/sms_message.rb', line 409
def no_media?
uploads.blank?
end
|
#publications ⇒ Object
546
547
548
|
# File 'app/models/sms_message.rb', line 546
def publications
Item.joins(:literature).merge(literatures)
end
|
#receive_message ⇒ Object
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
|
# File 'app/models/sms_message.rb', line 302
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
276
277
278
|
# File 'app/models/sms_message.rb', line 276
def received_outside_business_hours?
!WorkingHours.in_working_hours?(created_at)
end
|
#recipient_do_not_text ⇒ DoNotCall
69
|
# File 'app/models/sms_message.rb', line 69
has_one :recipient_do_not_text, -> { where(do_not_text: true) }, class_name: 'DoNotCall', primary_key: :recipient, foreign_key: :number
|
#recipient_party ⇒ Party
66
|
# File 'app/models/sms_message.rb', line 66
belongs_to :recipient_party, class_name: 'Party', inverse_of: :recipient_sms_messages, optional: true
|
#recipient_sms_block ⇒ SmsBlock
68
|
# File 'app/models/sms_message.rb', line 68
has_one :recipient_sms_block, class_name: 'SmsBlock', primary_key: :recipient, foreign_key: :number
|
#recipient_to_s ⇒ Object
329
330
331
332
333
|
# File 'app/models/sms_message.rb', line 329
def recipient_to_s
r = PhoneNumber.parse_and_format(recipient).to_s
r << " (#{recipient_party.full_name})" if recipient_party
r
end
|
#send_message ⇒ Object
243
244
245
246
247
248
249
250
251
252
253
254
255
|
# File 'app/models/sms_message.rb', line 243
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
221
222
223
224
225
226
|
# File 'app/models/sms_message.rb', line 221
def sender_blocked?
return false unless inbound?
SmsBlock.where(number: sender).exists?
end
|
#sender_do_not_text ⇒ DoNotCall
70
|
# File 'app/models/sms_message.rb', line 70
has_one :sender_do_not_text, -> { where(do_not_text: true) }, class_name: 'DoNotCall', primary_key: :sender, foreign_key: :number
|
#sender_party ⇒ Party
65
|
# File 'app/models/sms_message.rb', line 65
belongs_to :sender_party, class_name: 'Party', inverse_of: :sender_sms_messages, optional: true
|
#sender_sms_block ⇒ SmsBlock
67
|
# File 'app/models/sms_message.rb', line 67
has_one :sender_sms_block, class_name: 'SmsBlock', primary_key: :sender, foreign_key: :number
|
#sender_to_s ⇒ Object
323
324
325
326
327
|
# File 'app/models/sms_message.rb', line 323
def sender_to_s
r = PhoneNumber.parse_and_format(sender).to_s
r << " (#{sender_party.full_name})" if sender_party
r
end
|
#set_defaults ⇒ Object
530
531
532
533
534
535
536
537
538
539
540
541
|
# File 'app/models/sms_message.rb', line 530
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_partial_path ⇒ Object
The partial for this model lives under app/views/crm/sms_messages/,
not the conventional app/views/sms_messages/. Without this override
render @sms_messages from any non-crm/sms_messages controller
(CustomersController#tab_sms, OrdersController#tab_sms, …) resolves
the derived sms_messages/sms_message path against the calling
controller's view dir and raises ActionView::MissingTemplate.
See AppSignal #5422 / #5532.
56
57
58
|
# File 'app/models/sms_message.rb', line 56
def to_partial_path
'crm/sms_messages/sms_message'
end
|
#to_s ⇒ Object
382
383
384
|
# File 'app/models/sms_message.rb', line 382
def to_s
"SMS #{id}"
end
|
#unblock_sender ⇒ Object
235
236
237
238
239
240
241
|
# File 'app/models/sms_message.rb', line 235
def unblock_sender
return false unless inbound?
return true unless sender_sms_block
sender_sms_block.destroy
true
end
|
#uploads ⇒ ActiveRecord::Relation<Upload>
72
|
# File 'app/models/sms_message.rb', line 72
has_and_belongs_to_many :uploads
|