Class: InstagramConnect::Client
- Inherits:
-
Object
- Object
- InstagramConnect::Client
- Defined in:
- lib/instagram_connect/client.rb
Overview
Thin wrapper over the Meta Graph API, bound to one account's access token. Every call returns a Result — API-level failures never raise, callers branch on success?. The Graph host + version come from the configured auth strategy.
Constant Summary collapse
- TIMEOUT =
30
Instance Method Summary collapse
-
#account_profile(ig_user_id: @ig_user_id, fields: %w[username name profile_picture_url followers_count media_count])) ⇒ Object
The connected account's own profile.
-
#collect(path, query = {}, limit_pages: 25) ⇒ Object
Every record across every page, for callers that just want the list.
- #container_status(container_id:) ⇒ Object
- #conversation_messages(conversation_id:, limit: 20) ⇒ Object
-
#create_media_container(ig_user_id: @ig_user_id, **params) ⇒ Object
--- Publishing ------------------------------------------------------.
- #delete_comment(comment_id:) ⇒ Object
- #delete_messenger_profile(fields:) ⇒ Object
-
#each_page(path, query = {}, limit_pages: 25) ⇒ Object
Walks a Graph collection to the end, following Meta's own cursors.
- #fetch_media_binary(url:) ⇒ Object
-
#granted_asset_ids ⇒ Object
The asset ids this token actually carries, from Meta's own record of the consent.
- #hide_comment(comment_id:, hidden: true) ⇒ Object
-
#initialize(access_token:, config: InstagramConnect.configuration, ig_user_id: nil) ⇒ Client
constructor
A new instance of Client.
- #list_comments(media_id:, limit: 50) ⇒ Object
-
#list_conversations(ig_user_id: @ig_user_id, limit: 50) ⇒ Object
Threads that already exist on the account.
-
#list_media(ig_user_id: @ig_user_id, limit: 25) ⇒ Object
--- Reads -----------------------------------------------------------.
-
#list_pages ⇒ Object
FB-Login only: the Pages this user administers, with their linked IG business account — used to resolve the account identity after OAuth.
- #media_insights(media_id:, metrics: %w[reach likes comments])) ⇒ Object
- #messenger_profile(fields:) ⇒ Object
-
#page(page_id) ⇒ Object
One Page by id.
-
#private_reply(comment_id:, text:) ⇒ Object
One-time private reply to a comment (comment -> DM), valid 7 days.
- #profile(igsid:, fields: %w[name username profile_pic])) ⇒ Object
- #publish_media(creation_id:, ig_user_id: @ig_user_id) ⇒ Object
- #publishing_limit(ig_user_id: @ig_user_id) ⇒ Object
-
#reply_comment(comment_id:, text:) ⇒ Object
--- Comments --------------------------------------------------------.
- #send_attachment(recipient_id:, attachment_id:, tag: nil) ⇒ Object
- #send_media(recipient_id:, url:, type: "image", tag: nil) ⇒ Object
- #send_quick_replies(recipient_id:, text:, replies:, tag: nil) ⇒ Object
- #send_reaction(recipient_id:, message_id:, reaction: "love") ⇒ Object
-
#send_sender_action(recipient_id:, action:) ⇒ Object
Typing indicators and read receipts.
-
#send_text(recipient_id:, text:, tag: nil) ⇒ Object
--- Direct messages -------------------------------------------------.
-
#set_messenger_profile(**fields) ⇒ Object
Ice breakers and the persistent menu both live on the messenger profile.
-
#upload_attachment(file:, type: "image") ⇒ Object
Uploads bytes to Meta and returns a reusable attachment id.
Constructor Details
#initialize(access_token:, config: InstagramConnect.configuration, ig_user_id: nil) ⇒ Client
Returns a new instance of Client.
10 11 12 13 14 15 |
# File 'lib/instagram_connect/client.rb', line 10 def initialize(access_token:, config: InstagramConnect.configuration, ig_user_id: nil) @access_token = access_token @config = config @ig_user_id = ig_user_id @strategy = Auth.for(config) end |
Instance Method Details
#account_profile(ig_user_id: @ig_user_id, fields: %w[username name profile_picture_url followers_count media_count])) ⇒ Object
The connected account's own profile. A different call from #profile, which looks up the person on the other end of a thread.
145 146 147 148 |
# File 'lib/instagram_connect/client.rb', line 145 def account_profile(ig_user_id: @ig_user_id, fields: %w[username name profile_picture_url followers_count media_count]) get("/#{require_ig_user_id(ig_user_id)}", { fields: Array(fields).join(",") }) end |
#collect(path, query = {}, limit_pages: 25) ⇒ Object
Every record across every page, for callers that just want the list.
218 219 220 221 222 223 224 |
# File 'lib/instagram_connect/client.rb', line 218 def collect(path, query = {}, limit_pages: 25) records = [] result = each_page(path, query, limit_pages: limit_pages) { |page| records.concat(page.records) } return result if result.failure? Result.ok(data: { "data" => records }) end |
#container_status(container_id:) ⇒ Object
124 125 126 |
# File 'lib/instagram_connect/client.rb', line 124 def container_status(container_id:) get("/#{container_id}", { fields: "status_code,status" }) end |
#conversation_messages(conversation_id:, limit: 20) ⇒ Object
169 170 171 172 |
# File 'lib/instagram_connect/client.rb', line 169 def (conversation_id:, limit: 20) get("/#{conversation_id}", { fields: "messages.limit(#{limit}){id,created_time,from,to,message}" }) end |
#create_media_container(ig_user_id: @ig_user_id, **params) ⇒ Object
--- Publishing ------------------------------------------------------
116 117 118 |
# File 'lib/instagram_connect/client.rb', line 116 def create_media_container(ig_user_id: @ig_user_id, **params) post("/#{require_ig_user_id(ig_user_id)}/media", params) end |
#delete_comment(comment_id:) ⇒ Object
106 107 108 |
# File 'lib/instagram_connect/client.rb', line 106 def delete_comment(comment_id:) delete("/#{comment_id}") end |
#delete_messenger_profile(fields:) ⇒ Object
84 85 86 87 88 89 |
# File 'lib/instagram_connect/client.rb', line 84 def delete_messenger_profile(fields:) parse(HTTParty.delete(url("/me/messenger_profile"), headers: bearer.merge("Content-Type" => "application/json"), body: { fields: Array(fields), platform: "instagram" }.to_json, timeout: TIMEOUT)) end |
#each_page(path, query = {}, limit_pages: 25) ⇒ Object
Walks a Graph collection to the end, following Meta's own cursors.
Offsets are not safe here: a collection can shift between calls, so an
offset silently skips or repeats rows. limit_pages exists because these
collections can be unbounded and every page spends rate budget.
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/instagram_connect/client.rb', line 199 def each_page(path, query = {}, limit_pages: 25) cursor = nil pages = 0 while pages < limit_pages page_query = cursor ? query.merge(after: cursor) : query result = get(path, page_query) return result unless result.success? yield result pages += 1 cursor = result.next_cursor break if cursor.nil? end Result.ok(data: { "pages" => pages }) end |
#fetch_media_binary(url:) ⇒ Object
226 227 228 229 230 231 232 233 |
# File 'lib/instagram_connect/client.rb', line 226 def fetch_media_binary(url:) response = HTTParty.get(url, headers: bearer, timeout: TIMEOUT, follow_redirects: true) unless response.success? return Result.error("media fetch failed: HTTP #{response.code}", error_code: response.code) end body = response.body.to_s Result.ok(data: { body: body, mime: response.headers["content-type"], size: body.bytesize }) end |
#granted_asset_ids ⇒ Object
The asset ids this token actually carries, from Meta's own record of the consent. Needs no permission beyond the app's own credentials.
184 185 186 187 188 189 190 191 192 |
# File 'lib/instagram_connect/client.rb', line 184 def granted_asset_ids result = get("/debug_token", { input_token: access_token, access_token: "#{config.app_id}|#{config.app_secret}" }) return [] unless result.success? Array(result.data.dig("data", "granular_scopes")) .flat_map { |scope| Array(scope["target_ids"]) } .uniq end |
#hide_comment(comment_id:, hidden: true) ⇒ Object
102 103 104 |
# File 'lib/instagram_connect/client.rb', line 102 def hide_comment(comment_id:, hidden: true) post("/#{comment_id}", { hide: hidden }) end |
#list_comments(media_id:, limit: 50) ⇒ Object
110 111 112 |
# File 'lib/instagram_connect/client.rb', line 110 def list_comments(media_id:, limit: 50) get("/#{media_id}/comments", { fields: "id,text,username,timestamp,parent_id", limit: limit }) end |
#list_conversations(ig_user_id: @ig_user_id, limit: 50) ⇒ Object
Threads that already exist on the account. Webhooks only ever tell us about NEW activity, so without this an inbox starts empty and stays empty until somebody happens to message in. Meta returns the 20 most recent messages per thread and no more — that is the whole history available to any app.
164 165 166 167 |
# File 'lib/instagram_connect/client.rb', line 164 def list_conversations(ig_user_id: @ig_user_id, limit: 50) collect("/#{require_ig_user_id(ig_user_id)}/conversations", { platform: "instagram", fields: "id,updated_time", limit: limit }) end |
#list_media(ig_user_id: @ig_user_id, limit: 25) ⇒ Object
--- Reads -----------------------------------------------------------
134 135 136 137 |
# File 'lib/instagram_connect/client.rb', line 134 def list_media(ig_user_id: @ig_user_id, limit: 25) get("/#{require_ig_user_id(ig_user_id)}/media", { fields: "id,caption,media_type,media_url,permalink,timestamp", limit: limit }) end |
#list_pages ⇒ Object
FB-Login only: the Pages this user administers, with their linked IG business account — used to resolve the account identity after OAuth.
156 157 158 |
# File 'lib/instagram_connect/client.rb', line 156 def list_pages get("/me/accounts", { fields: "id,name,access_token,instagram_business_account" }) end |
#media_insights(media_id:, metrics: %w[reach likes comments])) ⇒ Object
139 140 141 |
# File 'lib/instagram_connect/client.rb', line 139 def media_insights(media_id:, metrics: %w[reach likes comments]) get("/#{media_id}/insights", { metric: Array(metrics).join(",") }) end |
#messenger_profile(fields:) ⇒ Object
80 81 82 |
# File 'lib/instagram_connect/client.rb', line 80 def messenger_profile(fields:) get("/me/messenger_profile", { fields: Array(fields).join(","), platform: "instagram" }) end |
#page(page_id) ⇒ Object
One Page by id. /me/accounts answers for the Pages a token may enumerate; this answers for a Page it may read. Those are not the same set — a Page granted through the Login-for-Business asset picker is readable here while absent from the listing.
178 179 180 |
# File 'lib/instagram_connect/client.rb', line 178 def page(page_id) get("/#{page_id}", { fields: "id,name,access_token,instagram_business_account" }) end |
#private_reply(comment_id:, text:) ⇒ Object
One-time private reply to a comment (comment -> DM), valid 7 days.
92 93 94 |
# File 'lib/instagram_connect/client.rb', line 92 def private_reply(comment_id:, text:) post("/me/messages", { recipient: { comment_id: comment_id }, message: { text: text } }) end |
#profile(igsid:, fields: %w[name username profile_pic])) ⇒ Object
150 151 152 |
# File 'lib/instagram_connect/client.rb', line 150 def profile(igsid:, fields: %w[name username profile_pic]) get("/#{igsid}", { fields: Array(fields).join(",") }) end |
#publish_media(creation_id:, ig_user_id: @ig_user_id) ⇒ Object
120 121 122 |
# File 'lib/instagram_connect/client.rb', line 120 def publish_media(creation_id:, ig_user_id: @ig_user_id) post("/#{require_ig_user_id(ig_user_id)}/media_publish", { creation_id: creation_id }) end |
#publishing_limit(ig_user_id: @ig_user_id) ⇒ Object
128 129 130 |
# File 'lib/instagram_connect/client.rb', line 128 def publishing_limit(ig_user_id: @ig_user_id) get("/#{require_ig_user_id(ig_user_id)}/content_publishing_limit", { fields: "quota_usage,config" }) end |
#reply_comment(comment_id:, text:) ⇒ Object
--- Comments --------------------------------------------------------
98 99 100 |
# File 'lib/instagram_connect/client.rb', line 98 def reply_comment(comment_id:, text:) post("/#{comment_id}/replies", { message: text }) end |
#send_attachment(recipient_id:, attachment_id:, tag: nil) ⇒ Object
51 52 53 54 |
# File 'lib/instagram_connect/client.rb', line 51 def (recipient_id:, attachment_id:, tag: nil) (recipient: { id: recipient_id }, message: { attachment: { payload: { attachment_id: } } }, tag: tag) end |
#send_media(recipient_id:, url:, type: "image", tag: nil) ⇒ Object
23 24 25 26 |
# File 'lib/instagram_connect/client.rb', line 23 def send_media(recipient_id:, url:, type: "image", tag: nil) = { type: type, payload: { url: url } } (recipient: { id: recipient_id }, message: { attachment: }, tag: tag) end |
#send_quick_replies(recipient_id:, text:, replies:, tag: nil) ⇒ Object
43 44 45 46 47 48 49 |
# File 'lib/instagram_connect/client.rb', line 43 def send_quick_replies(recipient_id:, text:, replies:, tag: nil) quick_replies = Array(replies).map do |reply| { content_type: "text", title: reply[:title].to_s[0, 20], payload: reply[:payload].to_s } end (recipient: { id: recipient_id }, message: { text: text, quick_replies: quick_replies }, tag: tag) end |
#send_reaction(recipient_id:, message_id:, reaction: "love") ⇒ Object
28 29 30 31 32 33 34 |
# File 'lib/instagram_connect/client.rb', line 28 def send_reaction(recipient_id:, message_id:, reaction: "love") post("/me/messages", { recipient: { id: recipient_id }, sender_action: "react", payload: { message_id: , reaction: reaction } }) end |
#send_sender_action(recipient_id:, action:) ⇒ Object
Typing indicators and read receipts. Meta is explicit that a sender action request must carry ONLY the recipient and the action — bundling it with a message silently drops one of the two.
39 40 41 |
# File 'lib/instagram_connect/client.rb', line 39 def send_sender_action(recipient_id:, action:) post("/me/messages", { recipient: { id: recipient_id }, sender_action: action }) end |
#send_text(recipient_id:, text:, tag: nil) ⇒ Object
--- Direct messages -------------------------------------------------
19 20 21 |
# File 'lib/instagram_connect/client.rb', line 19 def send_text(recipient_id:, text:, tag: nil) (recipient: { id: recipient_id }, message: { text: text }, tag: tag) end |
#set_messenger_profile(**fields) ⇒ Object
Ice breakers and the persistent menu both live on the messenger profile.
76 77 78 |
# File 'lib/instagram_connect/client.rb', line 76 def set_messenger_profile(**fields) post("/me/messenger_profile", fields.merge(platform: "instagram")) end |
#upload_attachment(file:, type: "image") ⇒ Object
Uploads bytes to Meta and returns a reusable attachment id.
Preferred over handing Meta a signed URL to the host's storage: a signed
URL is a bearer capability for a customer's file, sitting on the public
internet for as long as its TTL, fetched from an address we do not
control. This moves the bytes over an authenticated POST instead, and the
returned id can be reused rather than re-uploading the same file.
file must be a File or Tempfile — HTTParty builds the multipart body
from objects that expose a path, which is also what ActiveStorage's
blob.open yields.
66 67 68 69 70 71 72 73 |
# File 'lib/instagram_connect/client.rb', line 66 def (file:, type: "image") body = { platform: "instagram", message: { attachment: { type: type, payload: { is_reusable: true } } }.to_json, filedata: file } post_multipart("/me/message_attachments", body) end |