instagram_connect 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 610d72aa6d017bd24db1f7b670ae377a9d6375c74195f21d2992a4249daadc91
|
|
4
|
+
data.tar.gz: 453de0f0a35ee225997a54292260e2b40dcf302b32242fe05f108ef3a702917c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 833aa49017299e66d1b2b6470ed32299ade4863cd8fd3393aa2790231af47e98c1e33412c200f6e7f5eec319d842b6cf4d5a27951cdf137223731caaa5ae435e
|
|
7
|
+
data.tar.gz: 62bf43bce6b59381057c3e26b0b201f529cac269eaadaf5497a61f5217d60858f8b2d6c223912aa0703fec3d415c011ba8a8bf4ac6edfd60fdb5927da3fa055e
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.3] - 2026-07-26
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Conversation sync imported nothing on the Facebook-Login path.** The conversations edge belongs
|
|
14
|
+
to the PAGE, not the Instagram user — `/{ig-user-id}/conversations` answers with a capability
|
|
15
|
+
error even when holding a valid Page token. The sync now targets the Page and falls back to the
|
|
16
|
+
Instagram user id only when there is no Page (the Instagram-Login path). Found in production,
|
|
17
|
+
where the job "succeeded" in 600ms twice while importing zero threads.
|
|
18
|
+
- **That failure was silent.** The job returned quietly when the listing call failed, so nothing —
|
|
19
|
+
logs, health, operator — ever learned why. It now logs the account, the node it asked, and Meta's
|
|
20
|
+
own error message.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## [0.3.2] - 2026-07-26
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Existing conversations are now imported.** The gem only ever created threads from inbound
|
|
28
|
+
webhooks, and webhooks only announce NEW activity — so a freshly connected account showed an
|
|
29
|
+
empty inbox beside an Instagram account full of conversations, and stayed empty until somebody
|
|
30
|
+
happened to message in. `SyncConversationsJob` pulls every thread and the 20 most recent messages
|
|
31
|
+
in each, which is the whole history Meta exposes to any app. It runs on connect and is safe to
|
|
32
|
+
re-run: threads are located by igsid and messages dedupe on `ig_message_id`.
|
|
33
|
+
- `Client#list_conversations` and `Client#conversation_messages`.
|
|
34
|
+
|
|
35
|
+
### Note
|
|
36
|
+
|
|
37
|
+
The Conversations API is a **Page-token** operation. An account still holding the OAuth user token
|
|
38
|
+
gets `(#3) Application does not have the capability to make this API call` — verified against a live
|
|
39
|
+
account. `AccountReadinessJob` performs that swap, so it must have run before a sync can succeed.
|
|
40
|
+
|
|
41
|
+
|
|
9
42
|
## [0.3.1] - 2026-07-26
|
|
10
43
|
|
|
11
44
|
### Fixed
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module InstagramConnect
|
|
2
|
+
# Pulls the threads that already exist on the account.
|
|
3
|
+
#
|
|
4
|
+
# Webhooks only ever announce NEW activity, so a freshly connected account
|
|
5
|
+
# shows an empty inbox until somebody happens to message in — even though the
|
|
6
|
+
# operator has years of conversations sitting in Instagram. This fetches what
|
|
7
|
+
# Meta will give us: every thread, and the 20 most recent messages in each,
|
|
8
|
+
# which is the entire history the API exposes to any app.
|
|
9
|
+
#
|
|
10
|
+
# Safe to re-run. Threads are located by igsid and messages are deduped on
|
|
11
|
+
# ig_message_id, so a second pass updates rather than duplicates.
|
|
12
|
+
class SyncConversationsJob < ApplicationJob
|
|
13
|
+
queue_as :default
|
|
14
|
+
|
|
15
|
+
def perform(account_id)
|
|
16
|
+
account = Account.find_by(id: account_id)
|
|
17
|
+
return if account.nil? || !account.active?
|
|
18
|
+
|
|
19
|
+
# The conversations edge lives on the Page for Facebook-Login accounts and
|
|
20
|
+
# on the Instagram user for Instagram-Login ones.
|
|
21
|
+
node = account.page_id.presence || account.ig_user_id
|
|
22
|
+
threads = account.client.list_conversations(node_id: node)
|
|
23
|
+
unless threads.success?
|
|
24
|
+
# A sync that fails must say so. Swallowing this once cost a production
|
|
25
|
+
# afternoon: the job "succeeded" in 600ms and imported nothing.
|
|
26
|
+
logger.error("[instagram_connect] conversation sync failed for " \
|
|
27
|
+
"account=#{account.id} node=#{node}: #{threads.error_message}")
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Array(threads.data["data"]).each { |thread| sync_thread(account, thread["id"]) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def sync_thread(account, conversation_id)
|
|
37
|
+
result = account.client.conversation_messages(conversation_id: conversation_id)
|
|
38
|
+
return unless result.success?
|
|
39
|
+
|
|
40
|
+
messages = Array(result.data.dig("messages", "data"))
|
|
41
|
+
# A thread with no readable messages tells us nothing and would show as an
|
|
42
|
+
# empty row in the inbox, so it is skipped rather than created.
|
|
43
|
+
return if messages.empty?
|
|
44
|
+
|
|
45
|
+
conversation = Conversation.locate(account: account, igsid: counterpart(messages, account))
|
|
46
|
+
messages.reverse_each { |message| import(conversation, account, message) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Meta reports both ends of every message; the thread belongs to whichever
|
|
50
|
+
# participant is not us.
|
|
51
|
+
def counterpart(messages, account)
|
|
52
|
+
messages.each do |message|
|
|
53
|
+
[ message.dig("from", "id"), *Array(message.dig("to", "data")).map { |t| t["id"] } ]
|
|
54
|
+
.compact.each { |id| return id.to_s if id.to_s != account.ig_user_id.to_s }
|
|
55
|
+
end
|
|
56
|
+
account.ig_user_id.to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def import(conversation, account, payload)
|
|
60
|
+
mid = payload["id"].to_s
|
|
61
|
+
return if mid.blank?
|
|
62
|
+
|
|
63
|
+
message = Message.find_or_initialize_by(conversation_id: conversation.id, ig_message_id: mid)
|
|
64
|
+
return unless message.new_record?
|
|
65
|
+
|
|
66
|
+
outbound = payload.dig("from", "id").to_s == account.ig_user_id.to_s
|
|
67
|
+
message.assign_attributes(
|
|
68
|
+
direction: outbound ? "outbound" : "inbound",
|
|
69
|
+
# Historical rows: an outbound message we are reading back has already
|
|
70
|
+
# been delivered, and an inbound one has already arrived.
|
|
71
|
+
status: outbound ? "sent" : "received",
|
|
72
|
+
body: payload["message"],
|
|
73
|
+
source: "sync",
|
|
74
|
+
sent_at: parse_time(payload["created_time"])
|
|
75
|
+
)
|
|
76
|
+
message.account_id = account.id
|
|
77
|
+
message.save!
|
|
78
|
+
conversation.register_message(message)
|
|
79
|
+
rescue ActiveRecord::RecordNotUnique
|
|
80
|
+
# Raced a webhook for the same message. The webhook's copy is the one that
|
|
81
|
+
# matters; ours would have been identical.
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def parse_time(value)
|
|
85
|
+
Time.zone.parse(value.to_s)
|
|
86
|
+
rescue ArgumentError, TypeError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -157,6 +157,25 @@ module InstagramConnect
|
|
|
157
157
|
get("/me/accounts", { fields: "id,name,access_token,instagram_business_account" })
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
+
# Threads that already exist on the account. Webhooks only ever tell us about
|
|
161
|
+
# NEW activity, so without this an inbox starts empty and stays empty until
|
|
162
|
+
# somebody happens to message in. Meta returns the 20 most recent messages
|
|
163
|
+
# per thread and no more — that is the whole history available to any app.
|
|
164
|
+
#
|
|
165
|
+
# node_id is the object that OWNS the conversations edge, and on the
|
|
166
|
+
# Facebook-Login path that is the PAGE, not the Instagram user — verified
|
|
167
|
+
# in production, where the Instagram user id answered with a capability
|
|
168
|
+
# error while holding a perfectly good Page token.
|
|
169
|
+
def list_conversations(node_id: @ig_user_id, limit: 50)
|
|
170
|
+
collect("/#{require_ig_user_id(node_id)}/conversations",
|
|
171
|
+
{ platform: "instagram", fields: "id,updated_time", limit: limit })
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def conversation_messages(conversation_id:, limit: 20)
|
|
175
|
+
get("/#{conversation_id}",
|
|
176
|
+
{ fields: "messages.limit(#{limit}){id,created_time,from,to,message}" })
|
|
177
|
+
end
|
|
178
|
+
|
|
160
179
|
# One Page by id. /me/accounts answers for the Pages a token may *enumerate*;
|
|
161
180
|
# this answers for a Page it may *read*. Those are not the same set — a Page
|
|
162
181
|
# granted through the Login-for-Business asset picker is readable here while
|
|
@@ -25,6 +25,10 @@ module InstagramConnect
|
|
|
25
25
|
)
|
|
26
26
|
account.connected_by_id = connected_by_id unless connected_by_id.nil?
|
|
27
27
|
account.save!
|
|
28
|
+
# Webhooks only announce new activity, so without this the operator
|
|
29
|
+
# connects and stares at an empty inbox beside an Instagram account full
|
|
30
|
+
# of conversations.
|
|
31
|
+
SyncConversationsJob.perform_later(account.id)
|
|
28
32
|
account
|
|
29
33
|
end
|
|
30
34
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: instagram_connect
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kshitiz Sinha
|
|
@@ -100,6 +100,7 @@ files:
|
|
|
100
100
|
- app/jobs/instagram_connect/refresh_tokens_job.rb
|
|
101
101
|
- app/jobs/instagram_connect/replay_events_job.rb
|
|
102
102
|
- app/jobs/instagram_connect/send_message_job.rb
|
|
103
|
+
- app/jobs/instagram_connect/sync_conversations_job.rb
|
|
103
104
|
- app/models/instagram_connect/account.rb
|
|
104
105
|
- app/models/instagram_connect/api_budget.rb
|
|
105
106
|
- app/models/instagram_connect/application_record.rb
|