Class: InstagramConnect::Conversation
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- InstagramConnect::Conversation
- Defined in:
- app/models/instagram_connect/conversation.rb
Overview
One DM thread — a connected account talking to one Instagram user (igsid). Threads are shared across the host's operators; ownership lives on the account, not per-operator.
Every writer here is SQL-side and monotonic. Meta guarantees no ordering across webhook batches, so a late-arriving older event must not roll a thread backwards, and concurrent inbound writes must not lose an unread increment.
Constant Summary collapse
- FOLDERS =
%w[primary general requests].freeze
Class Method Summary collapse
-
.locate(account:, igsid:) ⇒ Object
Race-safe find-or-create on the unique [account_id, igsid] pair.
Instance Method Summary collapse
-
#controlled_elsewhere? ⇒ Boolean
True when another app (typically the Page Inbox) holds thread control, in which case the Send API rejects our messages.
-
#mark_read_by_customer(at:, mid: nil) ⇒ Object
A read receipt only ever moves forward.
-
#register_event(at) ⇒ Object
Any activity that is not a message — a reaction, an opt-in — still counts as the thread being alive, and still reopens Meta's 24-hour window.
-
#register_message(message) ⇒ Object
Denormalizes the thread summary + unread count atomically (SQL-side) so concurrent inbound writes can't lose an unread increment.
- #register_referral(ref:, at:) ⇒ Object
- #register_thread_control(owner_app_id:, at:) ⇒ Object
Class Method Details
.locate(account:, igsid:) ⇒ Object
Race-safe find-or-create on the unique [account_id, igsid] pair.
27 28 29 30 31 |
# File 'app/models/instagram_connect/conversation.rb', line 27 def self.locate(account:, igsid:) find_or_create_by!(account_id: account.id, igsid: igsid) rescue ActiveRecord::RecordNotUnique find_by!(account_id: account.id, igsid: igsid) end |
Instance Method Details
#controlled_elsewhere? ⇒ Boolean
True when another app (typically the Page Inbox) holds thread control, in which case the Send API rejects our messages.
104 105 106 |
# File 'app/models/instagram_connect/conversation.rb', line 104 def controlled_elsewhere? thread_control_at.present? && thread_owner_app_id.present? end |
#mark_read_by_customer(at:, mid: nil) ⇒ Object
A read receipt only ever moves forward. A stale one redelivered after a newer message must not mark the thread read again.
59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'app/models/instagram_connect/conversation.rb', line 59 def mark_read_by_customer(at:, mid: nil) apply( [ "customer_last_read_mid = CASE WHEN customer_last_read_at IS NULL " \ "OR customer_last_read_at < ? THEN ? ELSE customer_last_read_mid END", "customer_last_read_at = #{monotonic('customer_last_read_at')}", "last_event_at = #{monotonic('last_event_at')}" ], [ at, mid, at, at, at, at ] ) mark_outbound_seen(at) self end |
#register_event(at) ⇒ Object
Any activity that is not a message — a reaction, an opt-in — still counts as the thread being alive, and still reopens Meta's 24-hour window.
98 99 100 |
# File 'app/models/instagram_connect/conversation.rb', line 98 def register_event(at) apply([ "last_event_at = #{monotonic('last_event_at')}" ], [ at, at ]) end |
#register_message(message) ⇒ Object
Denormalizes the thread summary + unread count atomically (SQL-side) so concurrent inbound writes can't lose an unread increment.
Keyed on the message's wire time, not our created_at: a webhook batch delayed by a redeploy would otherwise stamp old messages as the newest and reorder the inbox.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'app/models/instagram_connect/conversation.rb', line 39 def () stamp = .sent_at || .created_at || Time.current assignments = [ "last_message_at = #{monotonic('last_message_at')}", "last_message_preview = ?", "last_event_at = #{monotonic('last_event_at')}" ] binds = [ stamp, stamp, .preview, stamp, stamp ] if .inbound? assignments << "last_inbound_at = #{monotonic('last_inbound_at')}" assignments << "unread_count = unread_count + 1" binds += [ stamp, stamp ] end apply(assignments, binds) end |
#register_referral(ref:, at:) ⇒ Object
73 74 75 76 77 78 79 80 81 82 |
# File 'app/models/instagram_connect/conversation.rb', line 73 def register_referral(ref:, at:) apply( [ "last_referral_ref = CASE WHEN last_event_at IS NULL OR last_event_at < ? " \ "THEN ? ELSE last_referral_ref END", "last_event_at = #{monotonic('last_event_at')}" ], [ at, ref, at, at ] ) end |
#register_thread_control(owner_app_id:, at:) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 |
# File 'app/models/instagram_connect/conversation.rb', line 84 def register_thread_control(owner_app_id:, at:) apply( [ "thread_owner_app_id = CASE WHEN thread_control_at IS NULL OR thread_control_at < ? " \ "THEN ? ELSE thread_owner_app_id END", "thread_control_at = #{monotonic('thread_control_at')}", "last_event_at = #{monotonic('last_event_at')}" ], [ at, owner_app_id, at, at, at, at ] ) end |