[object Object]

← back to Norma Platform

IG: #fabricfriday reshare tool (hashtag-search picks + sassy credit caption, draft/gated --go); crediting blocked on oEmbed perm/login

5dec32de77bb700dadefc6e76766de4df6114ed4 · 2026-08-14 12:14:19 -0700 · Steve

Files touched

Diff

commit 5dec32de77bb700dadefc6e76766de4df6114ed4
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 14 12:14:19 2026 -0700

    IG: #fabricfriday reshare tool (hashtag-search picks + sassy credit caption, draft/gated --go); crediting blocked on oEmbed perm/login
---
 agents/instagram-agent/fabric-friday-reshare.js | 94 +++++++++++++++++++++++++
 1 file changed, 94 insertions(+)

diff --git a/agents/instagram-agent/fabric-friday-reshare.js b/agents/instagram-agent/fabric-friday-reshare.js
new file mode 100644
index 0000000..913c6c8
--- /dev/null
+++ b/agents/instagram-agent/fabric-friday-reshare.js
@@ -0,0 +1,94 @@
+#!/usr/bin/env node
+/**
+ * fabric-friday-reshare.js — find REAL accounts posting to #fabricfriday / #fabricfridays
+ * and reshare the good ones to @fabric_fridays with a sassy, complimentary credit caption.
+ *
+ *   node fabric-friday-reshare.js            # DRAFT: show picks + sassy captions, post nothing
+ *   node fabric-friday-reshare.js --go       # LIVE: reshare the picks to @fabric_fridays
+ *   node fabric-friday-reshare.js --n 3      # cap number of reshares (default 3)
+ *
+ * How it works: IG Graph hashtag search -> recent_media (image/carousel only, has media_url),
+ * skip our own posts + obvious non-vendors, resolve the poster's @handle via oEmbed so we can
+ * CREDIT them (reposting without credit reads as theft), then compose a playful "regram" caption.
+ * Reposting = re-upload their media_url as our own post + credit + tasteful sass — never mean.
+ */
+const accounts = require('./accounts');
+const fs = require('fs');
+const path = require('path');
+
+const args = process.argv.slice(2);
+const GO = args.includes('--go');
+const N = parseInt((() => { const i = args.indexOf('--n'); return i >= 0 ? args[i + 1] : '3'; })(), 10);
+
+const acct = accounts.resolve('fabric_fridays');
+const { ig_user_id: ID, access_token: T, graph_host: H, graph_version: V } = acct;
+const HASHTAG_IDS = { fabricfriday: '17841548773109912', fabricfridays: '17843754310062659' };
+
+// Tasteful, complimentary sass — hypes the maker, never mocks. Varied so reshares aren't identical.
+const SASS = [
+  (u) => `Okay ${u}, this is doing WAY too much — in the exact right way. 🧵 That's how you Fabric Friday. 👏`,
+  (u) => `We saw ${u} pull up to Fabric Friday with THIS and had to share. The audacity of good taste. ✨`,
+  (u) => `${u} said "let me just casually drop a masterpiece on Fabric Friday" — and we're not okay. 🫠 Gorgeous.`,
+  (u) => `Fabric Friday flex of the week goes to ${u}. Show-off. (Please keep showing off.) 💅`,
+  (u) => `${u} understood the assignment and then some. This is the Fabric Friday content we signed up for. 🔥`,
+];
+
+async function g(url) { const r = await fetch(url); return r.json(); }
+
+async function authorHandle(permalink) {
+  // oEmbed returns author_name = the @handle; hashtag media hides username, so this is how we credit.
+  try {
+    const j = await g(`${H}/${V}/instagram_oembed?url=${encodeURIComponent(permalink)}&fields=author_name&access_token=${encodeURIComponent(T)}`);
+    if (j.author_name) return j.author_name.startsWith('@') ? j.author_name : `@${j.author_name}`;
+  } catch { /* fall through */ }
+  return null;
+}
+
+async function publish(imageUrl, caption) {
+  const create = await (await fetch(`${H}/${V}/${ID}/media`, { method: 'POST', body: new URLSearchParams({ image_url: imageUrl, caption, access_token: T }) })).json();
+  if (!create.id) throw new Error('container: ' + JSON.stringify(create).slice(0, 160));
+  for (let i = 0; i < 15; i++) { const s = await g(`${H}/${V}/${create.id}?fields=status_code&access_token=${encodeURIComponent(T)}`); if (s.status_code === 'FINISHED') break; if (s.status_code === 'ERROR') throw new Error('container ERROR'); await new Promise((r) => setTimeout(r, 2000)); }
+  const pub = await (await fetch(`${H}/${V}/${ID}/media_publish`, { method: 'POST', body: new URLSearchParams({ creation_id: create.id, access_token: T }) })).json();
+  if (!pub.id) throw new Error('publish: ' + JSON.stringify(pub).slice(0, 160));
+  return pub.id;
+}
+
+(async () => {
+  // Gather candidates from both hashtags
+  const seen = new Set(); const cands = [];
+  for (const [tag, hid] of Object.entries(HASHTAG_IDS)) {
+    const j = await g(`${H}/${V}/${hid}/recent_media?user_id=${ID}&fields=id,caption,media_type,media_url,permalink,like_count,comments_count&limit=20&access_token=${encodeURIComponent(T)}`);
+    for (const m of (j.data || [])) {
+      if (seen.has(m.permalink)) continue; seen.add(m.permalink);
+      // reshareable = single image OR carousel with a fetchable cover image
+      if (!m.media_url || !/^https?:/.test(m.media_url)) continue;
+      if (!['IMAGE', 'CAROUSEL_ALBUM'].includes(m.media_type)) continue;
+      const cap = (m.caption || '').toLowerCase();
+      // skip obvious non-vendor / spam / giveaway noise
+      if (/follow to win|giveaway|link in bio to win|dm to buy|onlyfans|crypto/i.test(cap)) continue;
+      cands.push({ ...m, tag, score: (m.like_count || 0) + 3 * (m.comments_count || 0) });
+    }
+  }
+  cands.sort((a, b) => b.score - a.score);
+  const picks = cands.slice(0, N);
+
+  console.log(`${GO ? 'LIVE RESHARE' : 'DRAFT'} — ${picks.length} pick(s) from #fabricfriday(s), ranked by engagement:\n`);
+  const results = [];
+  for (let i = 0; i < picks.length; i++) {
+    const p = picks[i];
+    const handle = (await authorHandle(p.permalink)) || 'this maker';
+    const cred = handle === 'this maker' ? '(tap through to the original)' : `📸 ${handle}`;
+    const sass = SASS[i % SASS.length](handle);
+    const caption = `${sass}\n\n${cred} · via #${p.tag}\n#fabricfriday #fabric #textiles #interiordesign #designtrade #designerwallcoverings`;
+    console.log(`── pick ${i + 1}  ❤${p.like_count || '?'} 💬${p.comments_count || '?'}  ${p.permalink}`);
+    console.log(`   source cap: "${(p.caption || '').replace(/\n/g, ' ').slice(0, 80)}"`);
+    console.log(`   OUR caption:\n     ${caption.replace(/\n/g, '\n     ')}\n`);
+    if (GO) {
+      try { const id = await publish(p.media_url, caption); console.log(`   ✓ reshared → media_id ${id}\n`); results.push({ ...p, our_media_id: id, caption }); }
+      catch (e) { console.log(`   ✗ ${e.message}\n`); }
+      if (i < picks.length - 1) await new Promise((r) => setTimeout(r, 5000));
+    }
+  }
+  if (GO && results.length) fs.appendFileSync(path.join(__dirname, 'data', 'fabric-friday-reshares.jsonl'), results.map((r) => JSON.stringify({ ts: new Date().toISOString(), ...r })).join('\n') + '\n');
+  console.log(GO ? `Reshared ${results.length}.` : 'DRAFT only — review the sass, then run with --go to reshare.');
+})().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });

← abf763c IG cadence: 3 posts/day per account, spaced ~3.3h during bus  ·  back to Norma Platform  ·  IG fabric-friday reshare: match #fabricfriday posts against 9cc01ed →