instagram_connect 0.3.8 → 0.3.9
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 +22 -0
- data/app/jobs/instagram_connect/sync_media_job.rb +3 -2
- data/app/jobs/instagram_connect/sync_stories_job.rb +60 -0
- data/app/models/instagram_connect/media_item.rb +11 -0
- data/db/migrate/20260727110000_add_children_to_instagram_connect_media.rb +9 -0
- data/lib/instagram_connect/client.rb +20 -3
- 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: 3a9c934d89a0e97c513a99c5352da9227b5133f1011557deb0ea87f4d8a6ce9d
|
|
4
|
+
data.tar.gz: a7b0af0a9b85bbeaf6c3015f45f3a467c9f7a23b5f73356ba34819389a887190
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35cca9a90ea1822ff47b9b6fbb09addb00c60a0484519842fdd54285b51cb0643f41f64105bd6f9a96e157637dbf6daa3f5e9b5c2ea4dabb07a6436eb9201dcd
|
|
7
|
+
data.tar.gz: bdb981d70acbe14a489d95b7ea8ea5c5229aa2462f2998aca1ad8328146e25dec95101c3583da7a77901da5c16429d385b1860366454b5b1c921d6396e1b5ad2
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,28 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.9] - 2026-07-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Stories sync.** `SyncStoriesJob` walks the `/stories` edge and mirrors live stories as
|
|
14
|
+
`media_product_type: STORY` rows. Meta lists only the stories inside their 24 hours and sends no
|
|
15
|
+
expiry webhook, so the job upserts what it sees and marks nothing — expired stories keep their
|
|
16
|
+
rows forever, same never-delete rule as posts.
|
|
17
|
+
- **Carousel slides.** `MEDIA_FIELDS` now requests `children{id,media_type,media_url,thumbnail_url}`
|
|
18
|
+
and the new `children` json column stores them, so hosts can render every frame of a carousel,
|
|
19
|
+
not just the cover. `media_product_type` is requested too (FEED vs REELS vs STORY).
|
|
20
|
+
- **`MediaItem#live_story?`** — a story lives 24 hours from posting; unknown posted_at counts as expired.
|
|
21
|
+
- **`MediaItem.posts` scope** — everything that belongs on a feed grid. NULL-safe: rows synced
|
|
22
|
+
before `media_product_type` was requested still count as posts.
|
|
23
|
+
- `Client#list_stories`, and the media field lists now live on `Client`
|
|
24
|
+
(`Client::MEDIA_FIELDS` / `Client::STORY_FIELDS`).
|
|
25
|
+
|
|
26
|
+
### Migration
|
|
27
|
+
|
|
28
|
+
- `add_column :instagram_connect_media, :children, :json` (`if_not_exists`).
|
|
29
|
+
|
|
30
|
+
|
|
9
31
|
## [0.3.8] - 2026-07-27
|
|
10
32
|
|
|
11
33
|
### Added
|
|
@@ -20,8 +20,7 @@ module InstagramConnect
|
|
|
20
20
|
# like_count and comments_count come straight off the media node — no
|
|
21
21
|
# insights permission needed for the account's own posts. thumbnail_url is
|
|
22
22
|
# the poster frame for videos, whose media_url is a playable mp4.
|
|
23
|
-
MEDIA_FIELDS =
|
|
24
|
-
"timestamp,like_count,comments_count".freeze
|
|
23
|
+
MEDIA_FIELDS = Client::MEDIA_FIELDS
|
|
25
24
|
|
|
26
25
|
def self.enqueue_throttled(account)
|
|
27
26
|
Rails.cache.fetch("instagram_connect:sync_media:#{account.id}", expires_in: THROTTLE) do
|
|
@@ -69,6 +68,8 @@ module InstagramConnect
|
|
|
69
68
|
thumbnail_url: item["thumbnail_url"],
|
|
70
69
|
like_count: item["like_count"],
|
|
71
70
|
comments_count: item["comments_count"],
|
|
71
|
+
media_product_type: item["media_product_type"],
|
|
72
|
+
children: item.dig("children", "data"),
|
|
72
73
|
posted_at: parse_time(item["timestamp"]),
|
|
73
74
|
synced_at: Time.current
|
|
74
75
|
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module InstagramConnect
|
|
2
|
+
# Mirrors the account's stories. Meta's /stories edge returns only the
|
|
3
|
+
# stories still inside their 24 hours, and sends no webhook when one
|
|
4
|
+
# expires — so this walk upserts what it sees and marks nothing: a story
|
|
5
|
+
# that stops appearing has simply expired, which the row's posted_at
|
|
6
|
+
# already tells the host. Expired stories keep their rows forever, same
|
|
7
|
+
# never-delete rule as posts; their CDN URLs die with the story.
|
|
8
|
+
#
|
|
9
|
+
# Safe to re-run: MediaItem.record upserts on (account, ig_media_id).
|
|
10
|
+
class SyncStoriesJob < ApplicationJob
|
|
11
|
+
queue_as :default
|
|
12
|
+
|
|
13
|
+
# Hosts enqueue this from screens; the cache key stops a busy screen
|
|
14
|
+
# enqueueing it once per page view.
|
|
15
|
+
THROTTLE = 10.minutes
|
|
16
|
+
|
|
17
|
+
def self.enqueue_throttled(account)
|
|
18
|
+
Rails.cache.fetch("instagram_connect:sync_stories:#{account.id}", expires_in: THROTTLE) do
|
|
19
|
+
perform_later(account.id)
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def perform(account_id)
|
|
25
|
+
account = Account.find_by(id: account_id)
|
|
26
|
+
return if account.nil? || !account.active?
|
|
27
|
+
|
|
28
|
+
result = account.client.list_stories
|
|
29
|
+
unless result.success?
|
|
30
|
+
return logger.error("[instagram_connect] stories sync failed for " \
|
|
31
|
+
"account=#{account.id}: #{result.error_message}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Array(result.data["data"]).each { |item| upsert(account, item) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def upsert(account, item)
|
|
40
|
+
MediaItem.record(
|
|
41
|
+
account: account,
|
|
42
|
+
ig_media_id: item["id"],
|
|
43
|
+
media_type: item["media_type"],
|
|
44
|
+
media_product_type: item["media_product_type"] || "STORY",
|
|
45
|
+
caption: item["caption"],
|
|
46
|
+
permalink: item["permalink"],
|
|
47
|
+
media_url: item["media_url"],
|
|
48
|
+
thumbnail_url: item["thumbnail_url"],
|
|
49
|
+
posted_at: parse_time(item["timestamp"]),
|
|
50
|
+
synced_at: Time.current
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_time(value)
|
|
55
|
+
Time.zone.parse(value.to_s)
|
|
56
|
+
rescue ArgumentError, TypeError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -17,6 +17,10 @@ module InstagramConnect
|
|
|
17
17
|
validates :ig_media_id, presence: true, uniqueness: { scope: :account_id }
|
|
18
18
|
|
|
19
19
|
scope :stories, -> { where(media_product_type: "STORY") }
|
|
20
|
+
# Everything that belongs on a feed grid. NULL-safe on purpose: rows
|
|
21
|
+
# synced before media_product_type was requested have nil there, and a
|
|
22
|
+
# plain where.not would silently drop them.
|
|
23
|
+
scope :posts, -> { where("media_product_type IS NULL OR media_product_type <> 'STORY'") }
|
|
20
24
|
scope :recent, -> { order(Arel.sql("posted_at IS NULL, posted_at DESC")) }
|
|
21
25
|
|
|
22
26
|
# Upserts by Meta's id. Media arrives from several directions — a webhook
|
|
@@ -40,5 +44,12 @@ module InstagramConnect
|
|
|
40
44
|
def story?
|
|
41
45
|
media_product_type == "STORY"
|
|
42
46
|
end
|
|
47
|
+
|
|
48
|
+
# A story lives for 24 hours from posting; after that Instagram drops it
|
|
49
|
+
# and its CDN URLs die. Unknown posted_at counts as expired — better a
|
|
50
|
+
# grayed card than a player pointed at a dead URL.
|
|
51
|
+
def live_story?
|
|
52
|
+
story? && posted_at.present? && posted_at > 24.hours.ago
|
|
53
|
+
end
|
|
43
54
|
end
|
|
44
55
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddChildrenToInstagramConnectMedia < ActiveRecord::Migration[8.0]
|
|
2
|
+
# The slides of a carousel post, as Meta returns them: an array of
|
|
3
|
+
# {id, media_type, media_url, thumbnail_url} hashes. json, not jsonb,
|
|
4
|
+
# because the gem runs on sqlite too. if_not_exists so a host that ran a
|
|
5
|
+
# copy of this migration before adopting the gem version is unaffected.
|
|
6
|
+
def change
|
|
7
|
+
add_column :instagram_connect_media, :children, :json, if_not_exists: true
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -131,11 +131,28 @@ module InstagramConnect
|
|
|
131
131
|
|
|
132
132
|
# --- Reads -----------------------------------------------------------
|
|
133
133
|
|
|
134
|
+
# Everything the mirror stores about a post: engagement counts come straight
|
|
135
|
+
# off the media node (no insights permission needed for the account's own
|
|
136
|
+
# posts), thumbnail_url is the video poster frame, media_product_type tells
|
|
137
|
+
# FEED from REELS, and children are a carousel's slides.
|
|
138
|
+
MEDIA_FIELDS = "id,caption,media_type,media_url,thumbnail_url,permalink," \
|
|
139
|
+
"timestamp,like_count,comments_count,media_product_type," \
|
|
140
|
+
"children{id,media_type,media_url,thumbnail_url}".freeze
|
|
141
|
+
|
|
142
|
+
# A story is a media node too, but the engagement fields don't exist on it.
|
|
143
|
+
STORY_FIELDS = "id,caption,media_type,media_url,thumbnail_url,permalink," \
|
|
144
|
+
"timestamp,media_product_type".freeze
|
|
145
|
+
|
|
134
146
|
def list_media(ig_user_id: @ig_user_id, limit: 25)
|
|
135
147
|
get("/#{require_ig_user_id(ig_user_id)}/media",
|
|
136
|
-
{ fields:
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
{ fields: MEDIA_FIELDS, limit: limit })
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The account's LIVE stories — Meta returns only the ones still inside
|
|
152
|
+
# their 24 hours. Expired stories simply stop appearing here.
|
|
153
|
+
def list_stories(ig_user_id: @ig_user_id, limit: 25)
|
|
154
|
+
get("/#{require_ig_user_id(ig_user_id)}/stories",
|
|
155
|
+
{ fields: STORY_FIELDS, limit: limit })
|
|
139
156
|
end
|
|
140
157
|
|
|
141
158
|
def media_insights(media_id:, metrics: %w[reach likes comments])
|
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.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kshitiz Sinha
|
|
@@ -102,6 +102,7 @@ files:
|
|
|
102
102
|
- app/jobs/instagram_connect/send_message_job.rb
|
|
103
103
|
- app/jobs/instagram_connect/sync_conversations_job.rb
|
|
104
104
|
- app/jobs/instagram_connect/sync_media_job.rb
|
|
105
|
+
- app/jobs/instagram_connect/sync_stories_job.rb
|
|
105
106
|
- app/models/instagram_connect/account.rb
|
|
106
107
|
- app/models/instagram_connect/api_budget.rb
|
|
107
108
|
- app/models/instagram_connect/application_record.rb
|
|
@@ -145,6 +146,7 @@ files:
|
|
|
145
146
|
- db/migrate/20260725204815_create_instagram_connect_api_budgets.rb
|
|
146
147
|
- db/migrate/20260725205417_add_conversation_profile_fields.rb
|
|
147
148
|
- db/migrate/20260727074500_add_removed_flag_to_instagram_connect_media.rb
|
|
149
|
+
- db/migrate/20260727110000_add_children_to_instagram_connect_media.rb
|
|
148
150
|
- docs/CONFIGURATION.md
|
|
149
151
|
- docs/app_review_guide.md
|
|
150
152
|
- docs/auth_paths.md
|