Class: InstagramConnect::Conversation

Inherits:
ApplicationRecord show all
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

Instance Method Summary collapse

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: .id, igsid: igsid)
rescue ActiveRecord::RecordNotUnique
  find_by!(account_id: .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.

Returns:

  • (Boolean)


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 register_message(message)
  stamp = message.sent_at || message.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, message.preview, stamp, stamp ]

  if message.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