instagram_connect 0.3.6 → 0.3.7

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: a8f007049f1317825ec16b8574067644b890352e47f04f987b779a9b893584a5
4
- data.tar.gz: 714e3b145ab61b7e9e469890efe362630ddd269afae1f156050b6171ce748524
3
+ metadata.gz: 9cb1aea4b378e02bab73277ace0304ecdd751c54e535bc6840670870b844fddd
4
+ data.tar.gz: acc6b2589e468d17333a6920c1c5e4c609d8a636629e4ee62e35d8bb5e8cc6fa
5
5
  SHA512:
6
- metadata.gz: 24d080252c2155439ee8515953a2037c071001933d0f1ff6083c66a40b7f5b7504b642df340521196f4e9c40cbb18ebaed0a89a3215abb87ed87afe06cc5ad3f
7
- data.tar.gz: fc1e3e4ee479b7c6813e4e5a6bf3e567ae9b49360c8f57e5896d59e7f1528061945d7d7e5a3112420323b12d5270e272d902eb85fa234c30e9ecc1523a590e77
6
+ metadata.gz: d61e2c92a923fdc3b4fe5b47ecdc9709044635fc658fcc2251928dc0bbf44d3b4234916d4416142a89241caaf667ea2f7635569d0cbd6dfe98cfd478fbd42f42
7
+ data.tar.gz: e81459599d577b340e459571f643dace5a94a3a353921147877c89ac6e99a7647a8808da6ad5f5a00b85c943b4bf175b6903431a18efec073834c0aa6944f92f
data/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.7] - 2026-07-27
10
+
11
+ ### Added
12
+
13
+ - **`SyncMediaJob` — the gem now owns the post mirror.** Walks the account's entire media listing
14
+ (single page by default, full history with cursors via `full: true`), upserting caption,
15
+ permalink, type and timestamps into `MediaItem`. A host built this app-side first; per the
16
+ boundary rule — the gem owns the Graph protocol and every Instagram row — it moves here, and the
17
+ host's copy should be deleted on upgrade.
18
+ - **`removed_from_instagram_at` on media, with `MediaItem#removed?`.** Instagram sends no webhook
19
+ for a deleted post; absence from the account's own **completed** listing is the only signal. A
20
+ vanished post is stamped, never destroyed, so hosts can render it grayed and locked. A failed or
21
+ partial walk marks nothing. The migration uses `if_not_exists` because one host added the column
22
+ app-side before the gem owned it.
23
+ - `SyncMediaJob.enqueue_throttled(account)` for screen-triggered refreshes (10-minute throttle).
24
+
25
+
9
26
  ## [0.3.6] - 2026-07-26
10
27
 
11
28
  ### Fixed
@@ -0,0 +1,89 @@
1
+ module InstagramConnect
2
+ # Mirrors the account's posts — every post, from any app, not just ones
3
+ # published through this gem.
4
+ #
5
+ # The comments webhook names only a media id, so without this a host shows
6
+ # conversations grouped under bare 17-digit numbers. And Instagram sends no
7
+ # webhook when a post is deleted, so absence from the account's own COMPLETED
8
+ # listing is the only deletion signal there is: a vanished post is stamped
9
+ # removed_from_instagram_at, never destroyed. A partial or failed walk marks
10
+ # nothing — absence from an incomplete listing proves nothing.
11
+ #
12
+ # Safe to re-run: MediaItem.record upserts on (account, ig_media_id).
13
+ class SyncMediaJob < ApplicationJob
14
+ queue_as :default
15
+
16
+ # Hosts enqueue this from screens; the cache key stops a busy inbox
17
+ # enqueueing it once per page view.
18
+ THROTTLE = 10.minutes
19
+
20
+ MEDIA_FIELDS = "id,caption,media_type,media_url,permalink,timestamp".freeze
21
+
22
+ def self.enqueue_throttled(account)
23
+ Rails.cache.fetch("instagram_connect:sync_media:#{account.id}", expires_in: THROTTLE) do
24
+ perform_later(account.id)
25
+ true
26
+ end
27
+ end
28
+
29
+ # full: walks the entire history with cursors and, having seen it all,
30
+ # marks the known posts the listing no longer contains.
31
+ def perform(account_id, full: false)
32
+ account = Account.find_by(id: account_id)
33
+ return if account.nil? || !account.active?
34
+
35
+ started_at = Time.current
36
+ result = fetch(account, full: full)
37
+ unless result.success?
38
+ # A sync that fails must say so — the silent version of this class of
39
+ # job has already cost a production afternoon.
40
+ return logger.error("[instagram_connect] media sync failed for " \
41
+ "account=#{account.id}: #{result.error_message}")
42
+ end
43
+
44
+ Array(result.data["data"]).each { |item| upsert(account, item) }
45
+ mark_removed(account, started_at) if full
46
+ end
47
+
48
+ private
49
+
50
+ def fetch(account, full:)
51
+ return account.client.list_media unless full
52
+
53
+ account.client.collect("/#{account.ig_user_id}/media",
54
+ { fields: MEDIA_FIELDS, limit: 25 })
55
+ end
56
+
57
+ def upsert(account, item)
58
+ media = MediaItem.record(
59
+ account: account,
60
+ ig_media_id: item["id"],
61
+ media_type: item["media_type"],
62
+ caption: item["caption"],
63
+ permalink: item["permalink"],
64
+ media_url: item["media_url"],
65
+ posted_at: parse_time(item["timestamp"]),
66
+ synced_at: Time.current
67
+ )
68
+ # Explicit, because record compacts nils out of its attributes: a post
69
+ # that reappears in the listing un-grays.
70
+ media.update!(removed_from_instagram_at: nil) if media.removed_from_instagram_at
71
+ end
72
+
73
+ def mark_removed(account, started_at)
74
+ marked = MediaItem.where(account_id: account.id, removed_from_instagram_at: nil)
75
+ .where(synced_at: ...started_at)
76
+ .update_all(removed_from_instagram_at: Time.current)
77
+ return unless marked.positive?
78
+
79
+ logger.info("[instagram_connect] #{marked} post(s) newly absent from " \
80
+ "Instagram for account=#{account.id}")
81
+ end
82
+
83
+ def parse_time(value)
84
+ Time.zone.parse(value.to_s)
85
+ rescue ArgumentError, TypeError
86
+ nil
87
+ end
88
+ end
89
+ end
@@ -22,6 +22,12 @@ module InstagramConnect
22
22
  # Upserts by Meta's id. Media arrives from several directions — a webhook
23
23
  # about a comment, a story expiring, a sync — and whichever gets there first
24
24
  # should create the row without the others having to care.
25
+ # Stamped by SyncMediaJob when a completed listing no longer contains this
26
+ # post. Deleted on Instagram never means deleted here.
27
+ def removed?
28
+ removed_from_instagram_at.present?
29
+ end
30
+
25
31
  def self.record(account:, ig_media_id:, **attributes)
26
32
  media = find_or_initialize_by(account_id: account.id, ig_media_id: ig_media_id.to_s)
27
33
  media.assign_attributes(attributes.compact)
@@ -0,0 +1,13 @@
1
+ class AddRemovedFlagToInstagramConnectMedia < ActiveRecord::Migration[7.1]
2
+ # Instagram sends no webhook when a post is deleted; absence from the
3
+ # account's own completed listing is the only signal there is. A vanished
4
+ # post is stamped, never destroyed — hosts render it as a locked, grayed,
5
+ # still-viewable row.
6
+ #
7
+ # if_not_exists because one host added this column app-side before the gem
8
+ # owned it; re-running here must be a no-op there.
9
+ def change
10
+ add_column :instagram_connect_media, :removed_from_instagram_at, :datetime,
11
+ if_not_exists: true
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module InstagramConnect
2
- VERSION = "0.3.6"
2
+ VERSION = "0.3.7"
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.6
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kshitiz Sinha
@@ -101,6 +101,7 @@ files:
101
101
  - app/jobs/instagram_connect/replay_events_job.rb
102
102
  - app/jobs/instagram_connect/send_message_job.rb
103
103
  - app/jobs/instagram_connect/sync_conversations_job.rb
104
+ - app/jobs/instagram_connect/sync_media_job.rb
104
105
  - app/models/instagram_connect/account.rb
105
106
  - app/models/instagram_connect/api_budget.rb
106
107
  - app/models/instagram_connect/application_record.rb
@@ -143,6 +144,7 @@ files:
143
144
  - db/migrate/20260725204022_add_per_account_token_fields.rb
144
145
  - db/migrate/20260725204815_create_instagram_connect_api_budgets.rb
145
146
  - db/migrate/20260725205417_add_conversation_profile_fields.rb
147
+ - db/migrate/20260727074500_add_removed_flag_to_instagram_connect_media.rb
146
148
  - docs/CONFIGURATION.md
147
149
  - docs/app_review_guide.md
148
150
  - docs/auth_paths.md