instagram_connect 0.3.1 → 0.3.2

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: 1d1a0dd271f5e3f830351ddd25d62794b512c7786f125b6aa2f5ea8167db1423
4
- data.tar.gz: 0e211865a1e788987b1fdb64f739bb2cde4c08ef57821b6b7602e23b22689cf3
3
+ metadata.gz: c6cdab9c7a77b05738f216055248fb49663ce358ab496612c37be3b065b329dc
4
+ data.tar.gz: 2ff8e7866abc44e383509240659cb2403f1e1ce803b55d3cbbf6d28fc7fe394c
5
5
  SHA512:
6
- metadata.gz: 8df4d4149716e96900c767391f73c820b6777f0962c0f7e1d3c4e2522984f7af27da083ce75ada8092bd188dd9897053767e6c1fb2e7654291f6bff7a0c0ce66
7
- data.tar.gz: 97a67eb17002fd5611fb76f53fc6e5a852ed8d8a62875f07a367258fe16758fa16556194b82fa61eea270ca131649df78027821618f3aadfb6c9fa92e5f0d269
6
+ metadata.gz: d2dc22ae8f6c88e3a7f3ab210f3211a884c65fc0ea688e10dd9da63377b8fe91b21bb419420e875634783844e0daa84c9c6e458d0344fba6de4d11f35c1eaba3
7
+ data.tar.gz: 8dc40c7345f8a292e4370977c1233f46a77f7a551c0ebfd55db89fd30a769fd453023a5229bfaf9df43a47ae788202b5a0a9cab7bc62d52916604a6410b1b675
data/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.2] - 2026-07-26
10
+
11
+ ### Added
12
+
13
+ - **Existing conversations are now imported.** The gem only ever created threads from inbound
14
+ webhooks, and webhooks only announce NEW activity — so a freshly connected account showed an
15
+ empty inbox beside an Instagram account full of conversations, and stayed empty until somebody
16
+ happened to message in. `SyncConversationsJob` pulls every thread and the 20 most recent messages
17
+ in each, which is the whole history Meta exposes to any app. It runs on connect and is safe to
18
+ re-run: threads are located by igsid and messages dedupe on `ig_message_id`.
19
+ - `Client#list_conversations` and `Client#conversation_messages`.
20
+
21
+ ### Note
22
+
23
+ The Conversations API is a **Page-token** operation. An account still holding the OAuth user token
24
+ gets `(#3) Application does not have the capability to make this API call` — verified against a live
25
+ account. `AccountReadinessJob` performs that swap, so it must have run before a sync can succeed.
26
+
27
+
9
28
  ## [0.3.1] - 2026-07-26
10
29
 
11
30
  ### Fixed
@@ -0,0 +1,81 @@
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
+ threads = account.client.list_conversations
20
+ return unless threads.success?
21
+
22
+ Array(threads.data["data"]).each { |thread| sync_thread(account, thread["id"]) }
23
+ end
24
+
25
+ private
26
+
27
+ def sync_thread(account, conversation_id)
28
+ result = account.client.conversation_messages(conversation_id: conversation_id)
29
+ return unless result.success?
30
+
31
+ messages = Array(result.data.dig("messages", "data"))
32
+ # A thread with no readable messages tells us nothing and would show as an
33
+ # empty row in the inbox, so it is skipped rather than created.
34
+ return if messages.empty?
35
+
36
+ conversation = Conversation.locate(account: account, igsid: counterpart(messages, account))
37
+ messages.reverse_each { |message| import(conversation, account, message) }
38
+ end
39
+
40
+ # Meta reports both ends of every message; the thread belongs to whichever
41
+ # participant is not us.
42
+ def counterpart(messages, account)
43
+ messages.each do |message|
44
+ [ message.dig("from", "id"), *Array(message.dig("to", "data")).map { |t| t["id"] } ]
45
+ .compact.each { |id| return id.to_s if id.to_s != account.ig_user_id.to_s }
46
+ end
47
+ account.ig_user_id.to_s
48
+ end
49
+
50
+ def import(conversation, account, payload)
51
+ mid = payload["id"].to_s
52
+ return if mid.blank?
53
+
54
+ message = Message.find_or_initialize_by(conversation_id: conversation.id, ig_message_id: mid)
55
+ return unless message.new_record?
56
+
57
+ outbound = payload.dig("from", "id").to_s == account.ig_user_id.to_s
58
+ message.assign_attributes(
59
+ direction: outbound ? "outbound" : "inbound",
60
+ # Historical rows: an outbound message we are reading back has already
61
+ # been delivered, and an inbound one has already arrived.
62
+ status: outbound ? "sent" : "received",
63
+ body: payload["message"],
64
+ source: "sync",
65
+ sent_at: parse_time(payload["created_time"])
66
+ )
67
+ message.account_id = account.id
68
+ message.save!
69
+ conversation.register_message(message)
70
+ rescue ActiveRecord::RecordNotUnique
71
+ # Raced a webhook for the same message. The webhook's copy is the one that
72
+ # matters; ours would have been identical.
73
+ end
74
+
75
+ def parse_time(value)
76
+ Time.zone.parse(value.to_s)
77
+ rescue ArgumentError, TypeError
78
+ nil
79
+ end
80
+ end
81
+ end
@@ -157,6 +157,20 @@ 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
+ def list_conversations(ig_user_id: @ig_user_id, limit: 50)
165
+ collect("/#{require_ig_user_id(ig_user_id)}/conversations",
166
+ { platform: "instagram", fields: "id,updated_time", limit: limit })
167
+ end
168
+
169
+ def conversation_messages(conversation_id:, limit: 20)
170
+ get("/#{conversation_id}",
171
+ { fields: "messages.limit(#{limit}){id,created_time,from,to,message}" })
172
+ end
173
+
160
174
  # One Page by id. /me/accounts answers for the Pages a token may *enumerate*;
161
175
  # this answers for a Page it may *read*. Those are not the same set — a Page
162
176
  # 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
 
@@ -1,3 +1,3 @@
1
1
  module InstagramConnect
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.2"
3
3
  end
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.1
4
+ version: 0.3.2
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