instagram_connect 0.3.13 → 0.3.14
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 +4 -4
- data/CHANGELOG.md +31 -0
- data/app/jobs/instagram_connect/send_message_job.rb +20 -1
- data/app/jobs/instagram_connect/upload_attachment_job.rb +66 -0
- data/app/models/instagram_connect/message.rb +6 -0
- data/db/migrate/20260728020000_add_attachment_delivered_at_to_messages.rb +9 -0
- data/lib/instagram_connect/ingest/handlers/messages.rb +33 -0
- data/lib/instagram_connect/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d434793fea3922a883a5c471a9b8ac915fd9495e1d33249c0e8eaaa3a0ad5a57
|
|
4
|
+
data.tar.gz: ce5ba440af874f60ffd385c0f1bb9ec9163e133ad9ae82fee572f9a4a31fdd34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a891502b9568dabd9836bc7b33e37823b6552b390b89ded7391d80af5a1c5799ac8486ff8424096f4bedc4497d0f1f5027845f46a7ee169224d19291bb87b00f
|
|
7
|
+
data.tar.gz: decbeeebb16e42c4e08213276bc910c3f1930ada5dffcf369a4e93f689afe8a94efed8a0964e9d79a5173616b3664ae60751c33bb997669de06d8165c22bfbc2
|
data/CHANGELOG.md
CHANGED
|
@@ -6,10 +6,41 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.14] - 2026-07-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Outbound attachments.** `UploadAttachmentJob` uploads a message's file to Meta and records the
|
|
14
|
+
reusable `attachment_upload_id`; `SendMessageJob` then sends the attachment BEFORE the words
|
|
15
|
+
about it, so the customer sees the photo and then the caption in the order the operator wrote
|
|
16
|
+
them. Two steps rather than one because a file Meta rejects (too large, wrong type) must fail
|
|
17
|
+
the message with THAT reason rather than the send's. Size ceilings are checked before an upload
|
|
18
|
+
is spent, and `attachment_delivered_at` is the resume cursor so a worker killed between the two
|
|
19
|
+
halves never shows the customer the photo twice. `upload_attachment`/`send_attachment` had
|
|
20
|
+
existed with zero callers since 0.1.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- An attachment-only message no longer posts an empty text alongside it: splitting `""` yields one
|
|
25
|
+
empty part, which was sent as a message.
|
|
26
|
+
|
|
27
|
+
### Migration
|
|
28
|
+
|
|
29
|
+
- `add_column :instagram_connect_messages, :attachment_delivered_at, :datetime` (`if_not_exists`).
|
|
30
|
+
|
|
31
|
+
|
|
9
32
|
## [0.3.13] - 2026-07-28
|
|
10
33
|
|
|
11
34
|
### Fixed
|
|
12
35
|
|
|
36
|
+
- **An echo of a message this system sent no longer collides with it.** Every outbound send stores
|
|
37
|
+
Meta's mid; the echo of that same message then arrived and the handler called `create!` on the
|
|
38
|
+
same mid, so the event was banked `failed` and every replay failed again — forever
|
|
39
|
+
(`PG::UniqueViolation` on `index_instagram_connect_messages_on_account_and_mid`, hourly, in
|
|
40
|
+
production). The echo now ENRICHES the row it is echoing — filling only what the original write
|
|
41
|
+
could not know — and never rewrites direction, status, source or body, so a CMS-sent message
|
|
42
|
+
cannot be turned into one that looks sent from the phone.
|
|
43
|
+
|
|
13
44
|
- **The readiness pass was the one path still shipping an unreadable token to Meta.** It builds its
|
|
14
45
|
own `Authorization` header rather than going through `Account#client`, so 0.3.12's guard never
|
|
15
46
|
covered it: Meta answered "Cannot parse access token" and the failure was recorded as if the
|
|
@@ -41,7 +41,26 @@ module InstagramConnect
|
|
|
41
41
|
|
|
42
42
|
def deliver(message, conversation, tag)
|
|
43
43
|
client = conversation.account.client
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
# The attachment goes first, so the customer sees the photo and then the
|
|
46
|
+
# words about it — the order the operator wrote them in. Sent by
|
|
47
|
+
# reference: attachment_upload_id is Meta's reusable id, so the same file
|
|
48
|
+
# is uploaded once however many threads it is sent to.
|
|
49
|
+
if message.attachment_upload_id.present? && !message.attachment_delivered?
|
|
50
|
+
result = client.send_attachment(recipient_id: conversation.igsid,
|
|
51
|
+
attachment_id: message.attachment_upload_id, tag: tag)
|
|
52
|
+
unless result.success?
|
|
53
|
+
return fail_message(message, result.error_code&.to_s, result.error_message)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
message.update!(attachment_delivered_at: Time.current,
|
|
57
|
+
ig_message_id: message.ig_message_id.presence || result.id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# reject(&:blank?): an attachment-only message has no text, and splitting
|
|
61
|
+
# "" yields one empty part — which would post an empty message Meta has
|
|
62
|
+
# no reason to accept.
|
|
63
|
+
parts = TextSplitter.split(message.body.to_s).reject(&:blank?)
|
|
45
64
|
|
|
46
65
|
# Resume rather than restart. A worker killed mid-fan-out has already
|
|
47
66
|
# delivered the earlier parts, and re-sending them repeats text the
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module InstagramConnect
|
|
2
|
+
# Uploads a message's file to Meta and records the reusable attachment id,
|
|
3
|
+
# then hands the message to the sender.
|
|
4
|
+
#
|
|
5
|
+
# Two steps rather than one because Meta's upload is a separate call with its
|
|
6
|
+
# own failure mode: a file it rejects (too large, wrong type) must fail the
|
|
7
|
+
# message with THAT reason, not with whatever the send would have said
|
|
8
|
+
# afterwards. The id is reusable, so the same file sent to a second thread
|
|
9
|
+
# costs no upload at all.
|
|
10
|
+
#
|
|
11
|
+
# The host attaches the file however it likes (Active Storage, a Tempfile);
|
|
12
|
+
# this job only needs something that responds to #open or #path.
|
|
13
|
+
class UploadAttachmentJob < ApplicationJob
|
|
14
|
+
queue_as :default
|
|
15
|
+
|
|
16
|
+
# Meta's own ceilings, stated here so a file that cannot possibly work is
|
|
17
|
+
# refused before it costs an upload.
|
|
18
|
+
SIZE_LIMITS = { "image" => 8.megabytes, "video" => 25.megabytes,
|
|
19
|
+
"audio" => 25.megabytes, "file" => 25.megabytes }.freeze
|
|
20
|
+
|
|
21
|
+
def perform(message_id, file_path, type: "image")
|
|
22
|
+
message = Message.find_by(id: message_id)
|
|
23
|
+
return if message.nil? || message.attachment_upload_id.present?
|
|
24
|
+
|
|
25
|
+
unless File.exist?(file_path)
|
|
26
|
+
return fail_message(message, "attachment_missing",
|
|
27
|
+
"The file to send could not be read.")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
limit = SIZE_LIMITS.fetch(type, SIZE_LIMITS["file"])
|
|
31
|
+
if File.size(file_path) > limit
|
|
32
|
+
return fail_message(message, "attachment_too_large",
|
|
33
|
+
"Instagram refuses #{type}s over #{limit / 1.megabyte}MB.")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
upload(message, file_path, type)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def upload(message, file_path, type)
|
|
42
|
+
result = File.open(file_path) do |file|
|
|
43
|
+
message.conversation.account.client.upload_attachment(file: file, type: type)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless result.success?
|
|
47
|
+
return fail_message(message, result.error_code&.to_s, result.error_message)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attachment_id = result.data["attachment_id"].presence || result.id
|
|
51
|
+
if attachment_id.blank?
|
|
52
|
+
return fail_message(message, "attachment_upload_empty",
|
|
53
|
+
"Instagram accepted the upload but returned no attachment id.")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
message.update!(attachment_upload_id: attachment_id, media_kind: type)
|
|
57
|
+
SendMessageJob.perform_later(message.id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def fail_message(message, reason, error)
|
|
61
|
+
message.update!(status: "failed", failure_reason: reason, error_message: error)
|
|
62
|
+
message.broadcast_refresh
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -42,6 +42,12 @@ module InstagramConnect
|
|
|
42
42
|
sent_at || created_at
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# The attachment half of this send is done. Mirrors delivered_parts_count
|
|
46
|
+
# for the text half, so a resumed job never re-sends the photo.
|
|
47
|
+
def attachment_delivered?
|
|
48
|
+
attachment_delivered_at.present?
|
|
49
|
+
end
|
|
50
|
+
|
|
45
51
|
def deleted?
|
|
46
52
|
deleted_at.present?
|
|
47
53
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddAttachmentDeliveredAtToMessages < ActiveRecord::Migration[8.0]
|
|
2
|
+
# The resume cursor for the attachment half of a send, mirroring
|
|
3
|
+
# delivered_parts_count for the text half: a worker killed between the
|
|
4
|
+
# attachment and the text must not send the photo twice.
|
|
5
|
+
def change
|
|
6
|
+
add_column :instagram_connect_messages, :attachment_delivered_at, :datetime,
|
|
7
|
+
if_not_exists: true
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -15,6 +15,17 @@ module InstagramConnect
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def call
|
|
18
|
+
# An echo of a message this system already sent is a CONFIRMATION,
|
|
19
|
+
# not a new message: the send stored the mid first, so create! hits
|
|
20
|
+
# the unique index on [account_id, ig_message_id] and the event is
|
|
21
|
+
# banked failed — then every replay fails again, forever. Enrich the
|
|
22
|
+
# row Meta is echoing rather than duplicating it, and do not
|
|
23
|
+
# re-register it on the conversation (it was registered when sent).
|
|
24
|
+
if (existing = existing_message)
|
|
25
|
+
enrich(existing)
|
|
26
|
+
return existing
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
message = InstagramConnect::Message.create!(
|
|
19
30
|
conversation: conversation,
|
|
20
31
|
direction: echo? ? "outbound" : "inbound",
|
|
@@ -49,6 +60,28 @@ module InstagramConnect
|
|
|
49
60
|
|
|
50
61
|
private
|
|
51
62
|
|
|
63
|
+
def existing_message
|
|
64
|
+
return if mid.blank?
|
|
65
|
+
|
|
66
|
+
InstagramConnect::Message.find_by(account_id: conversation.account_id,
|
|
67
|
+
ig_message_id: mid)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Fill only what the original write could not know — Meta's clock, the
|
|
71
|
+
# media it processed, the story it was a reply to. Direction, status,
|
|
72
|
+
# source and body stay as first written: an echo must not rewrite a
|
|
73
|
+
# CMS-sent message into one that looks sent from the phone.
|
|
74
|
+
def enrich(message)
|
|
75
|
+
message.sent_at ||= envelope.occurred_at
|
|
76
|
+
message.media_status ||= media_status
|
|
77
|
+
message.media_kind ||= attachments.first&.dig("type")
|
|
78
|
+
message.story_id ||= story&.dig("id")
|
|
79
|
+
message.story_url ||= story&.dig("url")
|
|
80
|
+
message.save! if message.changed?
|
|
81
|
+
store_attachments(message) if message.attachments.none?
|
|
82
|
+
message
|
|
83
|
+
end
|
|
84
|
+
|
|
52
85
|
def payload
|
|
53
86
|
event["message"] || {}
|
|
54
87
|
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.
|
|
4
|
+
version: 0.3.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kshitiz Sinha
|
|
@@ -104,6 +104,7 @@ files:
|
|
|
104
104
|
- app/jobs/instagram_connect/sync_conversations_job.rb
|
|
105
105
|
- app/jobs/instagram_connect/sync_media_job.rb
|
|
106
106
|
- app/jobs/instagram_connect/sync_stories_job.rb
|
|
107
|
+
- app/jobs/instagram_connect/upload_attachment_job.rb
|
|
107
108
|
- app/models/instagram_connect/account.rb
|
|
108
109
|
- app/models/instagram_connect/api_budget.rb
|
|
109
110
|
- app/models/instagram_connect/application_record.rb
|
|
@@ -148,6 +149,7 @@ files:
|
|
|
148
149
|
- db/migrate/20260725205417_add_conversation_profile_fields.rb
|
|
149
150
|
- db/migrate/20260727074500_add_removed_flag_to_instagram_connect_media.rb
|
|
150
151
|
- db/migrate/20260727110000_add_children_to_instagram_connect_media.rb
|
|
152
|
+
- db/migrate/20260728020000_add_attachment_delivered_at_to_messages.rb
|
|
151
153
|
- docs/CONFIGURATION.md
|
|
152
154
|
- docs/app_review_guide.md
|
|
153
155
|
- docs/auth_paths.md
|