@jackwener/opencli 1.6.0 → 1.6.2
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.
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -45
- package/README.zh-CN.md +32 -34
- package/autoresearch/browse-tasks.json +18 -20
- package/autoresearch/commands/debug.ts +163 -0
- package/autoresearch/commands/fix.ts +145 -0
- package/autoresearch/commands/plan.ts +88 -0
- package/autoresearch/commands/run.ts +138 -0
- package/autoresearch/config.ts +82 -0
- package/autoresearch/engine.ts +359 -0
- package/autoresearch/eval-all.ts +127 -0
- package/autoresearch/eval-browse.ts +1 -1
- package/autoresearch/eval-publish.ts +238 -0
- package/autoresearch/eval-save.ts +249 -0
- package/autoresearch/eval-skill.ts +14 -8
- package/autoresearch/eval-v2ex.ts +220 -0
- package/autoresearch/eval-zhihu.ts +230 -0
- package/autoresearch/logger.ts +69 -0
- package/autoresearch/presets/combined-reliability.ts +27 -0
- package/autoresearch/presets/index.ts +23 -0
- package/autoresearch/presets/operate-reliability.ts +24 -0
- package/autoresearch/presets/save-reliability.ts +26 -0
- package/autoresearch/presets/skill-quality.ts +20 -0
- package/autoresearch/presets/v2ex-reliability.ts +24 -0
- package/autoresearch/presets/zhihu-reliability.ts +25 -0
- package/autoresearch/publish-tasks.json +345 -0
- package/autoresearch/run-save.sh +11 -0
- package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
- package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
- package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
- package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
- package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
- package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
- package/autoresearch/save-tasks.json +281 -0
- package/autoresearch/v2ex-tasks.json +899 -0
- package/autoresearch/zhihu-tasks.json +848 -0
- package/bun.lock +615 -0
- package/dist/browser/base-page.d.ts +4 -2
- package/dist/browser/base-page.js +37 -4
- package/dist/browser/bridge.js +10 -8
- package/dist/browser/cdp.js +2 -6
- package/dist/browser/daemon-client.d.ts +11 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.d.ts +4 -2
- package/dist/browser/dom-helpers.js +42 -31
- package/dist/browser/dom-snapshot.js +23 -1
- package/dist/browser/page.d.ts +7 -2
- package/dist/browser/page.js +112 -30
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +1 -0
- package/dist/cli-manifest.json +1133 -182
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +48 -7
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +88 -0
- package/dist/clis/1688/item.d.ts +70 -0
- package/dist/clis/1688/item.js +187 -0
- package/dist/clis/1688/item.test.d.ts +1 -0
- package/dist/clis/1688/item.test.js +67 -0
- package/dist/clis/1688/search.d.ts +56 -0
- package/dist/clis/1688/search.js +309 -0
- package/dist/clis/1688/search.test.d.ts +1 -0
- package/dist/clis/1688/search.test.js +75 -0
- package/dist/clis/1688/shared.d.ts +112 -0
- package/dist/clis/1688/shared.js +514 -0
- package/dist/clis/1688/shared.test.d.ts +1 -0
- package/dist/clis/1688/shared.test.js +57 -0
- package/dist/clis/1688/store.d.ts +45 -0
- package/dist/clis/1688/store.js +226 -0
- package/dist/clis/1688/store.test.d.ts +1 -0
- package/dist/clis/1688/store.test.js +62 -0
- package/dist/clis/amazon/bestsellers.d.ts +0 -20
- package/dist/clis/amazon/bestsellers.js +6 -129
- package/dist/clis/amazon/bestsellers.test.js +12 -3
- package/dist/clis/amazon/movers-shakers.d.ts +1 -0
- package/dist/clis/amazon/movers-shakers.js +7 -0
- package/dist/clis/amazon/new-releases.d.ts +1 -0
- package/dist/clis/amazon/new-releases.js +7 -0
- package/dist/clis/amazon/rankings.d.ts +59 -0
- package/dist/clis/amazon/rankings.js +226 -0
- package/dist/clis/amazon/rankings.test.d.ts +1 -0
- package/dist/clis/amazon/rankings.test.js +41 -0
- package/dist/clis/amazon/shared.d.ts +11 -0
- package/dist/clis/amazon/shared.js +121 -11
- package/dist/clis/amazon/shared.test.js +11 -0
- package/dist/clis/bilibili/comments.js +2 -2
- package/dist/clis/bilibili/comments.test.js +3 -2
- package/dist/clis/bilibili/download.js +2 -1
- package/dist/clis/bilibili/subtitle.js +4 -3
- package/dist/clis/bilibili/subtitle.test.js +2 -1
- package/dist/clis/bilibili/utils.d.ts +5 -0
- package/dist/clis/bilibili/utils.js +30 -0
- package/dist/clis/bilibili/utils.test.d.ts +1 -0
- package/dist/clis/bilibili/utils.test.js +17 -0
- package/dist/clis/douban/marks.js +1 -1
- package/dist/clis/douban/subject.yaml +50 -19
- package/dist/clis/doubao/utils.js +32 -12
- package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
- package/dist/clis/douyin/_shared/transcode.test.js +0 -2
- package/dist/clis/douyin/draft.test.js +0 -2
- package/dist/clis/facebook/search.test.js +0 -2
- package/dist/clis/gemini/ask.js +9 -3
- package/dist/clis/gemini/ask.test.d.ts +1 -0
- package/dist/clis/gemini/ask.test.js +100 -0
- package/dist/clis/gemini/reply-state.test.d.ts +1 -0
- package/dist/clis/gemini/reply-state.test.js +641 -0
- package/dist/clis/gemini/utils.d.ts +44 -1
- package/dist/clis/gemini/utils.js +528 -61
- package/dist/clis/gemini/utils.test.js +149 -2
- package/dist/clis/hupu/detail.d.ts +1 -0
- package/dist/clis/hupu/detail.js +72 -0
- package/dist/clis/hupu/hot.yaml +43 -0
- package/dist/clis/hupu/like.d.ts +1 -0
- package/dist/clis/hupu/like.js +75 -0
- package/dist/clis/hupu/reply.d.ts +1 -0
- package/dist/clis/hupu/reply.js +71 -0
- package/dist/clis/hupu/search.d.ts +1 -0
- package/dist/clis/hupu/search.js +59 -0
- package/dist/clis/hupu/unlike.d.ts +1 -0
- package/dist/clis/hupu/unlike.js +75 -0
- package/dist/clis/hupu/utils.d.ts +20 -0
- package/dist/clis/hupu/utils.js +319 -0
- package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
- package/dist/clis/instagram/_shared/private-publish.js +1030 -0
- package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
- package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
- package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
- package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
- package/dist/clis/instagram/_shared/runtime-info.js +81 -0
- package/dist/clis/instagram/note.d.ts +1 -0
- package/dist/clis/instagram/note.js +222 -0
- package/dist/clis/instagram/note.test.d.ts +1 -0
- package/dist/clis/instagram/note.test.js +81 -0
- package/dist/clis/instagram/post.d.ts +4 -0
- package/dist/clis/instagram/post.js +1496 -0
- package/dist/clis/instagram/post.test.d.ts +1 -0
- package/dist/clis/instagram/post.test.js +1647 -0
- package/dist/clis/instagram/reel.d.ts +1 -0
- package/dist/clis/instagram/reel.js +826 -0
- package/dist/clis/instagram/reel.test.d.ts +1 -0
- package/dist/clis/instagram/reel.test.js +167 -0
- package/dist/clis/instagram/story.d.ts +1 -0
- package/dist/clis/instagram/story.js +115 -0
- package/dist/clis/instagram/story.test.d.ts +1 -0
- package/dist/clis/instagram/story.test.js +167 -0
- package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
- package/dist/clis/sinafinance/stock-rank.js +65 -0
- package/dist/clis/substack/utils.test.js +0 -2
- package/dist/clis/twitter/post.js +72 -45
- package/dist/clis/twitter/post.test.d.ts +1 -0
- package/dist/clis/twitter/post.test.js +116 -0
- package/dist/clis/twitter/reply.d.ts +12 -0
- package/dist/clis/twitter/reply.js +257 -35
- package/dist/clis/twitter/reply.test.d.ts +1 -0
- package/dist/clis/twitter/reply.test.js +151 -0
- package/dist/clis/twitter/search.js +67 -5
- package/dist/clis/twitter/search.test.js +83 -5
- package/dist/clis/xianyu/chat.d.ts +7 -0
- package/dist/clis/xianyu/chat.js +146 -0
- package/dist/clis/xianyu/chat.test.d.ts +1 -0
- package/dist/clis/xianyu/chat.test.js +15 -0
- package/dist/clis/xianyu/item.d.ts +7 -0
- package/dist/clis/xianyu/item.js +152 -0
- package/dist/clis/xianyu/item.test.d.ts +1 -0
- package/dist/clis/xianyu/item.test.js +56 -0
- package/dist/clis/xianyu/search.d.ts +10 -0
- package/dist/clis/xianyu/search.js +134 -0
- package/dist/clis/xianyu/search.test.d.ts +1 -0
- package/dist/clis/xianyu/search.test.js +17 -0
- package/dist/clis/xianyu/utils.d.ts +1 -0
- package/dist/clis/xianyu/utils.js +8 -0
- package/dist/clis/xiaoe/catalog.yaml +129 -0
- package/dist/clis/xiaoe/content.yaml +43 -0
- package/dist/clis/xiaoe/courses.yaml +73 -0
- package/dist/clis/xiaoe/detail.yaml +39 -0
- package/dist/clis/xiaoe/play-url.yaml +124 -0
- package/dist/clis/xiaohongshu/comments.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
- package/dist/clis/xiaohongshu/download.test.js +0 -2
- package/dist/clis/xiaohongshu/note.test.js +0 -2
- package/dist/clis/xiaohongshu/publish.test.js +0 -2
- package/dist/clis/xiaohongshu/search.js +29 -20
- package/dist/clis/xiaohongshu/search.test.js +56 -48
- package/dist/clis/yuanbao/ask.d.ts +21 -0
- package/dist/clis/yuanbao/ask.js +427 -0
- package/dist/clis/yuanbao/ask.test.d.ts +1 -0
- package/dist/clis/yuanbao/ask.test.js +124 -0
- package/dist/clis/yuanbao/new.d.ts +1 -0
- package/dist/clis/yuanbao/new.js +70 -0
- package/dist/clis/yuanbao/new.test.d.ts +1 -0
- package/dist/clis/yuanbao/new.test.js +30 -0
- package/dist/clis/yuanbao/shared.d.ts +13 -0
- package/dist/clis/yuanbao/shared.js +49 -0
- package/dist/clis/zhihu/question.js +30 -19
- package/dist/clis/zhihu/question.test.js +34 -16
- package/dist/commanderAdapter.js +8 -4
- package/dist/commanderAdapter.test.js +42 -0
- package/dist/completion.js +3 -1
- package/dist/completion.test.d.ts +1 -0
- package/dist/completion.test.js +23 -0
- package/dist/doctor.js +1 -1
- package/dist/electron-apps.d.ts +2 -0
- package/dist/electron-apps.js +7 -1
- package/dist/errors.js +1 -1
- package/dist/execution.js +25 -35
- package/dist/explore.js +1 -1
- package/dist/launcher.d.ts +4 -0
- package/dist/launcher.js +64 -8
- package/dist/launcher.test.js +88 -7
- package/dist/output.d.ts +2 -0
- package/dist/output.js +10 -1
- package/dist/output.test.d.ts +0 -3
- package/dist/output.test.js +59 -92
- package/dist/pipeline/executor.test.js +0 -2
- package/dist/pipeline/steps/download.test.js +0 -2
- package/dist/registry.d.ts +2 -0
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +1 -0
- package/dist/types.d.ts +9 -2
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/1688.md +52 -0
- package/docs/adapters/browser/36kr.md +2 -1
- package/docs/adapters/browser/doubao.md +5 -1
- package/docs/adapters/browser/hupu.md +53 -0
- package/docs/adapters/browser/sinafinance.md +32 -2
- package/docs/adapters/browser/weibo.md +6 -1
- package/docs/adapters/browser/wikipedia.md +2 -0
- package/docs/adapters/browser/xianyu.md +42 -0
- package/docs/adapters/browser/xiaoe.md +44 -0
- package/docs/adapters/browser/yuanbao.md +64 -0
- package/docs/adapters/index.md +14 -5
- package/docs/comparison.md +1 -1
- package/docs/developer/ai-workflow.md +2 -2
- package/docs/developer/contributing.md +1 -1
- package/docs/developer/testing.md +2 -0
- package/docs/guide/plugins.md +1 -0
- package/docs/guide/troubleshooting.md +11 -0
- package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
- package/docs/zh/guide/plugins.md +1 -0
- package/extension/dist/background.js +1127 -0
- package/extension/src/background.test.ts +39 -0
- package/extension/src/background.ts +223 -34
- package/extension/src/cdp.ts +194 -4
- package/extension/src/protocol.ts +22 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +1 -1
- package/skills/opencli-explorer/SKILL.md +1 -1
- package/skills/opencli-oneshot/SKILL.md +2 -2
- package/skills/opencli-operate/SKILL.md +120 -27
- package/skills/opencli-usage/SKILL.md +31 -20
- package/skills/opencli-usage/browser.md +114 -16
- package/skills/opencli-usage/public-api.md +32 -3
- package/skills/smart-search/SKILL.md +156 -0
- package/skills/smart-search/references/sources-ai.md +74 -0
- package/skills/smart-search/references/sources-info.md +43 -0
- package/skills/smart-search/references/sources-media.md +50 -0
- package/skills/smart-search/references/sources-other.md +42 -0
- package/skills/smart-search/references/sources-shopping.md +31 -0
- package/skills/smart-search/references/sources-social.md +51 -0
- package/skills/smart-search/references/sources-tech.md +42 -0
- package/skills/smart-search/references/sources-travel.md +20 -0
- package/src/browser/base-page.ts +41 -6
- package/src/browser/bridge.ts +11 -8
- package/src/browser/cdp.ts +1 -8
- package/src/browser/daemon-client.ts +11 -1
- package/src/browser/dom-helpers.ts +43 -31
- package/src/browser/dom-snapshot.ts +23 -1
- package/src/browser/page.ts +115 -31
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.ts +2 -0
- package/src/cli.test.ts +133 -0
- package/src/cli.ts +73 -11
- package/src/clis/1688/item.test.ts +69 -0
- package/src/clis/1688/item.ts +282 -0
- package/src/clis/1688/search.test.ts +81 -0
- package/src/clis/1688/search.ts +402 -0
- package/src/clis/1688/shared.test.ts +75 -0
- package/src/clis/1688/shared.ts +623 -0
- package/src/clis/1688/store.test.ts +69 -0
- package/src/clis/1688/store.ts +300 -0
- package/src/clis/amazon/bestsellers.test.ts +12 -3
- package/src/clis/amazon/bestsellers.ts +6 -178
- package/src/clis/amazon/movers-shakers.ts +8 -0
- package/src/clis/amazon/new-releases.ts +8 -0
- package/src/clis/amazon/rankings.test.ts +47 -0
- package/src/clis/amazon/rankings.ts +312 -0
- package/src/clis/amazon/shared.test.ts +16 -0
- package/src/clis/amazon/shared.ts +134 -12
- package/src/clis/bilibili/comments.test.ts +4 -3
- package/src/clis/bilibili/comments.ts +2 -2
- package/src/clis/bilibili/download.ts +2 -1
- package/src/clis/bilibili/subtitle.test.ts +2 -1
- package/src/clis/bilibili/subtitle.ts +4 -3
- package/src/clis/bilibili/utils.test.ts +21 -0
- package/src/clis/bilibili/utils.ts +27 -0
- package/src/clis/douban/marks.ts +1 -1
- package/src/clis/douban/subject.yaml +50 -19
- package/src/clis/doubao/utils.ts +32 -12
- package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
- package/src/clis/douyin/_shared/transcode.test.ts +0 -2
- package/src/clis/douyin/draft.test.ts +0 -2
- package/src/clis/facebook/search.test.ts +0 -2
- package/src/clis/gemini/ask.test.ts +116 -0
- package/src/clis/gemini/ask.ts +10 -3
- package/src/clis/gemini/reply-state.test.ts +708 -0
- package/src/clis/gemini/utils.test.ts +184 -2
- package/src/clis/gemini/utils.ts +588 -60
- package/src/clis/hupu/detail.ts +126 -0
- package/src/clis/hupu/hot.yaml +43 -0
- package/src/clis/hupu/like.ts +76 -0
- package/src/clis/hupu/reply.ts +76 -0
- package/src/clis/hupu/search.ts +95 -0
- package/src/clis/hupu/unlike.ts +76 -0
- package/src/clis/hupu/utils.ts +381 -0
- package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
- package/src/clis/instagram/_shared/private-publish.ts +1303 -0
- package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
- package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
- package/src/clis/instagram/_shared/runtime-info.ts +91 -0
- package/src/clis/instagram/note.test.ts +96 -0
- package/src/clis/instagram/note.ts +254 -0
- package/src/clis/instagram/post.test.ts +1716 -0
- package/src/clis/instagram/post.ts +1620 -0
- package/src/clis/instagram/reel.test.ts +191 -0
- package/src/clis/instagram/reel.ts +886 -0
- package/src/clis/instagram/story.test.ts +191 -0
- package/src/clis/instagram/story.ts +151 -0
- package/src/clis/sinafinance/stock-rank.ts +68 -0
- package/src/clis/substack/utils.test.ts +0 -2
- package/src/clis/twitter/post.test.ts +157 -0
- package/src/clis/twitter/post.ts +82 -48
- package/src/clis/twitter/reply.test.ts +177 -0
- package/src/clis/twitter/reply.ts +285 -39
- package/src/clis/twitter/search.test.ts +88 -5
- package/src/clis/twitter/search.ts +68 -5
- package/src/clis/xianyu/chat.test.ts +20 -0
- package/src/clis/xianyu/chat.ts +175 -0
- package/src/clis/xianyu/item.test.ts +67 -0
- package/src/clis/xianyu/item.ts +172 -0
- package/src/clis/xianyu/search.test.ts +22 -0
- package/src/clis/xianyu/search.ts +151 -0
- package/src/clis/xianyu/utils.ts +9 -0
- package/src/clis/xiaoe/catalog.yaml +129 -0
- package/src/clis/xiaoe/content.yaml +43 -0
- package/src/clis/xiaoe/courses.yaml +73 -0
- package/src/clis/xiaoe/detail.yaml +39 -0
- package/src/clis/xiaoe/play-url.yaml +124 -0
- package/src/clis/xiaohongshu/comments.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
- package/src/clis/xiaohongshu/download.test.ts +0 -2
- package/src/clis/xiaohongshu/note.test.ts +0 -2
- package/src/clis/xiaohongshu/publish.test.ts +0 -2
- package/src/clis/xiaohongshu/search.test.ts +59 -48
- package/src/clis/xiaohongshu/search.ts +31 -21
- package/src/clis/yuanbao/ask.test.ts +156 -0
- package/src/clis/yuanbao/ask.ts +522 -0
- package/src/clis/yuanbao/new.test.ts +36 -0
- package/src/clis/yuanbao/new.ts +81 -0
- package/src/clis/yuanbao/shared.ts +57 -0
- package/src/clis/zhihu/question.test.ts +42 -17
- package/src/clis/zhihu/question.ts +31 -26
- package/src/commanderAdapter.test.ts +51 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/completion.test.ts +30 -0
- package/src/completion.ts +3 -1
- package/src/doctor.ts +1 -1
- package/src/electron-apps.ts +9 -1
- package/src/errors.ts +1 -1
- package/src/execution.ts +26 -30
- package/src/explore.ts +1 -1
- package/src/launcher.test.ts +121 -7
- package/src/launcher.ts +87 -9
- package/src/output.test.ts +50 -90
- package/src/output.ts +10 -1
- package/src/pipeline/executor.test.ts +0 -2
- package/src/pipeline/steps/download.test.ts +0 -2
- package/src/registry.ts +2 -0
- package/src/serialization.ts +2 -0
- package/src/types.ts +9 -2
- package/tests/e2e/browser-auth.test.ts +9 -0
- package/CLI-EXPLORER.md +0 -724
- package/CLI-ONESHOT.md +0 -216
- package/SKILL.md +0 -59
|
@@ -0,0 +1,1303 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
import { CommandExecutionError } from '../../../errors.js';
|
|
8
|
+
import type { BrowserCookie, IPage } from '../../../types.js';
|
|
9
|
+
import type { InstagramProtocolCaptureEntry } from './protocol-capture.js';
|
|
10
|
+
import { instagramPrivateApiFetch } from './protocol-capture.js';
|
|
11
|
+
import {
|
|
12
|
+
buildReadInstagramRuntimeInfoJs,
|
|
13
|
+
extractInstagramRuntimeInfo,
|
|
14
|
+
type InstagramRuntimeInfo,
|
|
15
|
+
} from './runtime-info.js';
|
|
16
|
+
export {
|
|
17
|
+
buildReadInstagramRuntimeInfoJs,
|
|
18
|
+
extractInstagramRuntimeInfo,
|
|
19
|
+
type InstagramRuntimeInfo,
|
|
20
|
+
resolveInstagramRuntimeInfo,
|
|
21
|
+
} from './runtime-info.js';
|
|
22
|
+
|
|
23
|
+
export interface InstagramPrivateApiContext {
|
|
24
|
+
asbdId: string;
|
|
25
|
+
csrfToken: string;
|
|
26
|
+
igAppId: string;
|
|
27
|
+
igWwwClaim: string;
|
|
28
|
+
instagramAjax: string;
|
|
29
|
+
webSessionId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface InstagramImageAsset {
|
|
33
|
+
filePath: string;
|
|
34
|
+
fileName: string;
|
|
35
|
+
mimeType: string;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
byteLength: number;
|
|
39
|
+
bytes: Buffer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PreparedInstagramImageAsset extends InstagramImageAsset {
|
|
43
|
+
cleanupPath?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type InstagramMediaKind = 'image' | 'video';
|
|
47
|
+
|
|
48
|
+
export interface InstagramMediaItem {
|
|
49
|
+
type: InstagramMediaKind;
|
|
50
|
+
filePath: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface InstagramVideoAsset {
|
|
54
|
+
filePath: string;
|
|
55
|
+
fileName: string;
|
|
56
|
+
mimeType: string;
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
durationMs: number;
|
|
60
|
+
byteLength: number;
|
|
61
|
+
bytes: Buffer;
|
|
62
|
+
coverImage: PreparedInstagramImageAsset;
|
|
63
|
+
cleanupPaths?: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type PreparedInstagramMediaAsset =
|
|
67
|
+
| { type: 'image'; asset: PreparedInstagramImageAsset }
|
|
68
|
+
| { type: 'video'; asset: InstagramVideoAsset };
|
|
69
|
+
|
|
70
|
+
type StoryPayloadInput = {
|
|
71
|
+
uploadId: string;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
now?: () => number;
|
|
75
|
+
jazoest: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type StoryVideoPayloadInput = StoryPayloadInput & {
|
|
79
|
+
durationMs: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type PrivateApiFetchInit = {
|
|
83
|
+
method?: string;
|
|
84
|
+
headers?: Record<string, string>;
|
|
85
|
+
body?: unknown;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type PrivateApiFetchLike = (url: string | URL, init?: PrivateApiFetchInit) => Promise<Response>;
|
|
89
|
+
|
|
90
|
+
const INSTAGRAM_MIN_FEED_ASPECT_RATIO = 4 / 5;
|
|
91
|
+
const INSTAGRAM_MAX_FEED_ASPECT_RATIO = 1.91;
|
|
92
|
+
const INSTAGRAM_MIN_STORY_ASPECT_RATIO = 9 / 16;
|
|
93
|
+
const INSTAGRAM_MAX_STORY_ASPECT_RATIO = 3 / 4;
|
|
94
|
+
const INSTAGRAM_PRIVATE_PAD_COLOR = 'FFFFFF';
|
|
95
|
+
const INSTAGRAM_HOME_URL = 'https://www.instagram.com/';
|
|
96
|
+
const INSTAGRAM_PRIVATE_CAPTURE_PATTERN = '/api/v1/|/graphql/';
|
|
97
|
+
const INSTAGRAM_PRIVATE_CONFIG_RETRY_BUDGET = 2;
|
|
98
|
+
const INSTAGRAM_PRIVATE_UPLOAD_RETRY_BUDGET = 2;
|
|
99
|
+
const INSTAGRAM_PRIVATE_SIDECAR_TRANSCODE_ATTEMPTS = 20;
|
|
100
|
+
const INSTAGRAM_PRIVATE_SIDECAR_TRANSCODE_WAIT_MS = 2000;
|
|
101
|
+
const INSTAGRAM_MAX_STORY_VIDEO_DURATION_MS = 15_000;
|
|
102
|
+
const INSTAGRAM_STORY_SIG_KEY = '19ce5f445dbfd9d29c59dc2a78c616a7fc090a8e018b9267bc4240a30244c53b';
|
|
103
|
+
const INSTAGRAM_STORY_SIG_KEY_VERSION = '4';
|
|
104
|
+
const INSTAGRAM_STORY_DEVICE = {
|
|
105
|
+
manufacturer: 'samsung',
|
|
106
|
+
model: 'SM-G930F',
|
|
107
|
+
android_version: 24,
|
|
108
|
+
android_release: '7.0',
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
111
|
+
export function derivePrivateApiContextFromCapture(
|
|
112
|
+
entries: InstagramProtocolCaptureEntry[],
|
|
113
|
+
): InstagramPrivateApiContext | null {
|
|
114
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
115
|
+
const headers = entries[index]?.requestHeaders ?? {};
|
|
116
|
+
const context = {
|
|
117
|
+
asbdId: String(headers['X-ASBD-ID'] || ''),
|
|
118
|
+
csrfToken: String(headers['X-CSRFToken'] || ''),
|
|
119
|
+
igAppId: String(headers['X-IG-App-ID'] || ''),
|
|
120
|
+
igWwwClaim: String(headers['X-IG-WWW-Claim'] || ''),
|
|
121
|
+
instagramAjax: String(headers['X-Instagram-AJAX'] || ''),
|
|
122
|
+
webSessionId: String(headers['X-Web-Session-ID'] || ''),
|
|
123
|
+
};
|
|
124
|
+
if (
|
|
125
|
+
context.asbdId
|
|
126
|
+
&& context.csrfToken
|
|
127
|
+
&& context.igAppId
|
|
128
|
+
&& context.igWwwClaim
|
|
129
|
+
&& context.instagramAjax
|
|
130
|
+
&& context.webSessionId
|
|
131
|
+
) {
|
|
132
|
+
return context;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function derivePartialPrivateApiContextFromCapture(
|
|
139
|
+
entries: InstagramProtocolCaptureEntry[],
|
|
140
|
+
): Partial<InstagramPrivateApiContext> {
|
|
141
|
+
const context: Partial<InstagramPrivateApiContext> = {};
|
|
142
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
143
|
+
const headers = entries[index]?.requestHeaders ?? {};
|
|
144
|
+
if (!context.asbdId && headers['X-ASBD-ID']) context.asbdId = String(headers['X-ASBD-ID']);
|
|
145
|
+
if (!context.csrfToken && headers['X-CSRFToken']) context.csrfToken = String(headers['X-CSRFToken']);
|
|
146
|
+
if (!context.igAppId && headers['X-IG-App-ID']) context.igAppId = String(headers['X-IG-App-ID']);
|
|
147
|
+
if (!context.igWwwClaim && headers['X-IG-WWW-Claim']) context.igWwwClaim = String(headers['X-IG-WWW-Claim']);
|
|
148
|
+
if (!context.instagramAjax && headers['X-Instagram-AJAX']) context.instagramAjax = String(headers['X-Instagram-AJAX']);
|
|
149
|
+
if (!context.webSessionId && headers['X-Web-Session-ID']) context.webSessionId = String(headers['X-Web-Session-ID']);
|
|
150
|
+
}
|
|
151
|
+
return context;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function deriveInstagramJazoest(value: string): string {
|
|
155
|
+
if (!value) return '';
|
|
156
|
+
const sum = Array.from(value).reduce((total, char) => total + char.charCodeAt(0), 0);
|
|
157
|
+
return `2${sum}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sleep(ms: number): Promise<void> {
|
|
161
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isTransientPrivateFetchError(error: unknown): boolean {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
return /fetch failed|network|socket hang up|econnreset|etimedout/i.test(message);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getCookieValue(cookies: BrowserCookie[], name: string): string {
|
|
170
|
+
return cookies.find((cookie) => cookie.name === name)?.value || '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function resolveInstagramPrivatePublishConfig(page: IPage): Promise<{
|
|
174
|
+
apiContext: InstagramPrivateApiContext;
|
|
175
|
+
jazoest: string;
|
|
176
|
+
}> {
|
|
177
|
+
let lastError: unknown;
|
|
178
|
+
for (let attempt = 0; attempt < INSTAGRAM_PRIVATE_CONFIG_RETRY_BUDGET; attempt += 1) {
|
|
179
|
+
try {
|
|
180
|
+
if (typeof page.startNetworkCapture === 'function') {
|
|
181
|
+
await page.startNetworkCapture(INSTAGRAM_PRIVATE_CAPTURE_PATTERN);
|
|
182
|
+
}
|
|
183
|
+
await page.goto(`${INSTAGRAM_HOME_URL}?__opencli_private_probe=${Date.now()}`);
|
|
184
|
+
await page.wait({ time: 2 });
|
|
185
|
+
|
|
186
|
+
const [cookies, runtime, entries] = await Promise.all([
|
|
187
|
+
page.getCookies({ domain: 'instagram.com' }),
|
|
188
|
+
page.evaluate(buildReadInstagramRuntimeInfoJs()) as Promise<InstagramRuntimeInfo>,
|
|
189
|
+
typeof page.readNetworkCapture === 'function'
|
|
190
|
+
? page.readNetworkCapture() as Promise<unknown[]>
|
|
191
|
+
: Promise.resolve([]),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const captureEntries = (Array.isArray(entries) ? entries : []) as InstagramProtocolCaptureEntry[];
|
|
195
|
+
const capturedContext = derivePrivateApiContextFromCapture(captureEntries)
|
|
196
|
+
?? derivePartialPrivateApiContextFromCapture(captureEntries);
|
|
197
|
+
|
|
198
|
+
const csrfToken = runtime?.csrfToken || getCookieValue(cookies, 'csrftoken') || capturedContext.csrfToken || '';
|
|
199
|
+
const igAppId = runtime?.appId || capturedContext.igAppId || '';
|
|
200
|
+
const instagramAjax = runtime?.instagramAjax || capturedContext.instagramAjax || '';
|
|
201
|
+
if (!csrfToken) {
|
|
202
|
+
throw new CommandExecutionError('Instagram private route could not derive CSRF token from browser session');
|
|
203
|
+
}
|
|
204
|
+
if (!igAppId) {
|
|
205
|
+
throw new CommandExecutionError('Instagram private route could not derive X-IG-App-ID from instagram runtime');
|
|
206
|
+
}
|
|
207
|
+
if (!instagramAjax) {
|
|
208
|
+
throw new CommandExecutionError('Instagram private route could not derive X-Instagram-AJAX from instagram runtime');
|
|
209
|
+
}
|
|
210
|
+
const asbdId = capturedContext.asbdId || '';
|
|
211
|
+
const igWwwClaim = capturedContext.igWwwClaim || '';
|
|
212
|
+
const webSessionId = capturedContext.webSessionId || '';
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
apiContext: {
|
|
216
|
+
asbdId,
|
|
217
|
+
csrfToken,
|
|
218
|
+
igAppId,
|
|
219
|
+
igWwwClaim,
|
|
220
|
+
instagramAjax,
|
|
221
|
+
webSessionId,
|
|
222
|
+
},
|
|
223
|
+
jazoest: deriveInstagramJazoest(csrfToken),
|
|
224
|
+
};
|
|
225
|
+
} catch (error) {
|
|
226
|
+
lastError = error;
|
|
227
|
+
if (!isTransientPrivateFetchError(error) || attempt >= INSTAGRAM_PRIVATE_CONFIG_RETRY_BUDGET - 1) {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function buildConfigureBody(input: {
|
|
236
|
+
uploadId: string;
|
|
237
|
+
caption: string;
|
|
238
|
+
jazoest: string;
|
|
239
|
+
}): string {
|
|
240
|
+
const body = new URLSearchParams();
|
|
241
|
+
body.set('archive_only', 'false');
|
|
242
|
+
body.set('caption', input.caption);
|
|
243
|
+
body.set('clips_share_preview_to_feed', '1');
|
|
244
|
+
body.set('disable_comments', '0');
|
|
245
|
+
body.set('disable_oa_reuse', 'false');
|
|
246
|
+
body.set('igtv_share_preview_to_feed', '1');
|
|
247
|
+
body.set('is_meta_only_post', '0');
|
|
248
|
+
body.set('is_unified_video', '1');
|
|
249
|
+
body.set('like_and_view_counts_disabled', '0');
|
|
250
|
+
body.set('media_share_flow', 'creation_flow');
|
|
251
|
+
body.set('share_to_facebook', '');
|
|
252
|
+
body.set('share_to_fb_destination_type', 'USER');
|
|
253
|
+
body.set('source_type', 'library');
|
|
254
|
+
body.set('upload_id', input.uploadId);
|
|
255
|
+
body.set('video_subtitles_enabled', '0');
|
|
256
|
+
body.set('jazoest', input.jazoest);
|
|
257
|
+
return body.toString();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function buildConfigureSidecarPayload(input: {
|
|
261
|
+
uploadIds: string[];
|
|
262
|
+
caption: string;
|
|
263
|
+
clientSidecarId: string;
|
|
264
|
+
jazoest: string;
|
|
265
|
+
}): Record<string, unknown> {
|
|
266
|
+
return {
|
|
267
|
+
archive_only: false,
|
|
268
|
+
caption: input.caption,
|
|
269
|
+
children_metadata: input.uploadIds.map((uploadId) => ({ upload_id: uploadId })),
|
|
270
|
+
client_sidecar_id: input.clientSidecarId,
|
|
271
|
+
disable_comments: '0',
|
|
272
|
+
is_meta_only_post: false,
|
|
273
|
+
is_open_to_public_submission: false,
|
|
274
|
+
like_and_view_counts_disabled: 0,
|
|
275
|
+
media_share_flow: 'creation_flow',
|
|
276
|
+
share_to_facebook: '',
|
|
277
|
+
share_to_fb_destination_type: 'USER',
|
|
278
|
+
source_type: 'library',
|
|
279
|
+
jazoest: input.jazoest,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function inferMimeType(filePath: string): string {
|
|
284
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
285
|
+
switch (ext) {
|
|
286
|
+
case '.png':
|
|
287
|
+
return 'image/png';
|
|
288
|
+
case '.webp':
|
|
289
|
+
return 'image/webp';
|
|
290
|
+
case '.jpg':
|
|
291
|
+
case '.jpeg':
|
|
292
|
+
default:
|
|
293
|
+
return 'image/jpeg';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function readPngDimensions(bytes: Buffer): { width: number; height: number } | null {
|
|
298
|
+
if (bytes.length < 24) return null;
|
|
299
|
+
if (bytes.subarray(0, 8).toString('hex').toUpperCase() !== '89504E470D0A1A0A') return null;
|
|
300
|
+
if (bytes.subarray(12, 16).toString('ascii') !== 'IHDR') return null;
|
|
301
|
+
return {
|
|
302
|
+
width: bytes.readUInt32BE(16),
|
|
303
|
+
height: bytes.readUInt32BE(20),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function readJpegDimensions(bytes: Buffer): { width: number; height: number } | null {
|
|
308
|
+
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return null;
|
|
309
|
+
let offset = 2;
|
|
310
|
+
while (offset + 9 < bytes.length) {
|
|
311
|
+
if (bytes[offset] !== 0xff) {
|
|
312
|
+
offset += 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const marker = bytes[offset + 1];
|
|
316
|
+
offset += 2;
|
|
317
|
+
if (marker === 0xd8 || marker === 0xd9) continue;
|
|
318
|
+
if (offset + 2 > bytes.length) break;
|
|
319
|
+
const segmentLength = bytes.readUInt16BE(offset);
|
|
320
|
+
if (segmentLength < 2 || offset + segmentLength > bytes.length) break;
|
|
321
|
+
const isStartOfFrame = marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
|
|
322
|
+
if (isStartOfFrame && segmentLength >= 7) {
|
|
323
|
+
return {
|
|
324
|
+
height: bytes.readUInt16BE(offset + 3),
|
|
325
|
+
width: bytes.readUInt16BE(offset + 5),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
offset += segmentLength;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function readWebpDimensions(bytes: Buffer): { width: number; height: number } | null {
|
|
334
|
+
if (bytes.length < 30) return null;
|
|
335
|
+
if (bytes.subarray(0, 4).toString('ascii') !== 'RIFF' || bytes.subarray(8, 12).toString('ascii') !== 'WEBP') {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const chunkType = bytes.subarray(12, 16).toString('ascii');
|
|
340
|
+
if (chunkType === 'VP8X' && bytes.length >= 30) {
|
|
341
|
+
return {
|
|
342
|
+
width: 1 + bytes.readUIntLE(24, 3),
|
|
343
|
+
height: 1 + bytes.readUIntLE(27, 3),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (chunkType === 'VP8 ' && bytes.length >= 30) {
|
|
348
|
+
return {
|
|
349
|
+
width: bytes.readUInt16LE(26) & 0x3fff,
|
|
350
|
+
height: bytes.readUInt16LE(28) & 0x3fff,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (chunkType === 'VP8L' && bytes.length >= 25) {
|
|
355
|
+
const bits = bytes.readUInt32LE(21);
|
|
356
|
+
return {
|
|
357
|
+
width: (bits & 0x3fff) + 1,
|
|
358
|
+
height: ((bits >> 14) & 0x3fff) + 1,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readImageDimensions(filePath: string, bytes: Buffer): { width: number; height: number } {
|
|
366
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
367
|
+
const dimensions = ext === '.png'
|
|
368
|
+
? readPngDimensions(bytes)
|
|
369
|
+
: ext === '.webp'
|
|
370
|
+
? readWebpDimensions(bytes)
|
|
371
|
+
: readJpegDimensions(bytes);
|
|
372
|
+
if (!dimensions) {
|
|
373
|
+
throw new CommandExecutionError(`Failed to read image dimensions for ${filePath}`);
|
|
374
|
+
}
|
|
375
|
+
return dimensions;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function readImageAsset(filePath: string): InstagramImageAsset {
|
|
379
|
+
const bytes = fs.readFileSync(filePath);
|
|
380
|
+
const { width, height } = readImageDimensions(filePath, bytes);
|
|
381
|
+
return {
|
|
382
|
+
filePath,
|
|
383
|
+
fileName: path.basename(filePath),
|
|
384
|
+
mimeType: inferMimeType(filePath),
|
|
385
|
+
width,
|
|
386
|
+
height,
|
|
387
|
+
byteLength: bytes.length,
|
|
388
|
+
bytes,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function isInstagramFeedAspectRatioAllowed(width: number, height: number): boolean {
|
|
393
|
+
const ratio = width / Math.max(height, 1);
|
|
394
|
+
return ratio >= INSTAGRAM_MIN_FEED_ASPECT_RATIO - 0.001
|
|
395
|
+
&& ratio <= INSTAGRAM_MAX_FEED_ASPECT_RATIO + 0.001;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function getInstagramFeedNormalizedDimensions(
|
|
399
|
+
width: number,
|
|
400
|
+
height: number,
|
|
401
|
+
): { width: number; height: number } | null {
|
|
402
|
+
const ratio = width / Math.max(height, 1);
|
|
403
|
+
if (ratio < INSTAGRAM_MIN_FEED_ASPECT_RATIO) {
|
|
404
|
+
return {
|
|
405
|
+
width: Math.ceil(height * INSTAGRAM_MIN_FEED_ASPECT_RATIO),
|
|
406
|
+
height,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (ratio > INSTAGRAM_MAX_FEED_ASPECT_RATIO) {
|
|
410
|
+
return {
|
|
411
|
+
width,
|
|
412
|
+
height: Math.ceil(width / INSTAGRAM_MAX_FEED_ASPECT_RATIO),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function isInstagramStoryAspectRatioAllowed(width: number, height: number): boolean {
|
|
419
|
+
const ratio = width / Math.max(height, 1);
|
|
420
|
+
return ratio >= INSTAGRAM_MIN_STORY_ASPECT_RATIO - 0.001
|
|
421
|
+
&& ratio <= INSTAGRAM_MAX_STORY_ASPECT_RATIO + 0.001;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function getInstagramStoryNormalizedDimensions(
|
|
425
|
+
width: number,
|
|
426
|
+
height: number,
|
|
427
|
+
): { width: number; height: number } | null {
|
|
428
|
+
const ratio = width / Math.max(height, 1);
|
|
429
|
+
if (ratio < INSTAGRAM_MIN_STORY_ASPECT_RATIO) {
|
|
430
|
+
return {
|
|
431
|
+
width: Math.ceil(height * INSTAGRAM_MIN_STORY_ASPECT_RATIO),
|
|
432
|
+
height,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (ratio > INSTAGRAM_MAX_STORY_ASPECT_RATIO) {
|
|
436
|
+
return {
|
|
437
|
+
width,
|
|
438
|
+
height: Math.ceil(width / INSTAGRAM_MAX_STORY_ASPECT_RATIO),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function buildPrivateNormalizedImagePath(filePath: string): string {
|
|
445
|
+
const parsed = path.parse(filePath);
|
|
446
|
+
return path.join(
|
|
447
|
+
os.tmpdir(),
|
|
448
|
+
`opencli-instagram-private-${parsed.name}-${crypto.randomUUID()}${parsed.ext || '.png'}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function prepareImageAssetForPrivateUpload(filePath: string): PreparedInstagramImageAsset {
|
|
453
|
+
const asset = readImageAsset(filePath);
|
|
454
|
+
const normalizedDimensions = getInstagramFeedNormalizedDimensions(asset.width, asset.height);
|
|
455
|
+
if (!normalizedDimensions) {
|
|
456
|
+
return asset;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (process.platform !== 'darwin') {
|
|
460
|
+
throw new CommandExecutionError(
|
|
461
|
+
`Instagram private publish does not support auto-normalizing ${asset.fileName} on ${process.platform}`,
|
|
462
|
+
`Use images within ${INSTAGRAM_MIN_FEED_ASPECT_RATIO.toFixed(2)}-${INSTAGRAM_MAX_FEED_ASPECT_RATIO.toFixed(2)} aspect ratio, or use the UI route`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const outputPath = buildPrivateNormalizedImagePath(filePath);
|
|
467
|
+
const result = spawnSync('sips', [
|
|
468
|
+
'--padToHeightWidth',
|
|
469
|
+
String(normalizedDimensions.height),
|
|
470
|
+
String(normalizedDimensions.width),
|
|
471
|
+
'--padColor',
|
|
472
|
+
INSTAGRAM_PRIVATE_PAD_COLOR,
|
|
473
|
+
filePath,
|
|
474
|
+
'--out',
|
|
475
|
+
outputPath,
|
|
476
|
+
], {
|
|
477
|
+
encoding: 'utf8',
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (result.error || result.status !== 0 || !fs.existsSync(outputPath)) {
|
|
481
|
+
const detail = [result.error?.message, result.stderr, result.stdout]
|
|
482
|
+
.map((value) => String(value || '').trim())
|
|
483
|
+
.filter(Boolean)
|
|
484
|
+
.join(' ');
|
|
485
|
+
throw new CommandExecutionError(
|
|
486
|
+
`Instagram private publish failed to normalize ${asset.fileName}`,
|
|
487
|
+
detail || 'sips padToHeightWidth failed',
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
...readImageAsset(outputPath),
|
|
493
|
+
cleanupPath: outputPath,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function prepareImageAssetForPrivateStoryUpload(filePath: string): PreparedInstagramImageAsset {
|
|
498
|
+
const asset = readImageAsset(filePath);
|
|
499
|
+
const normalizedDimensions = getInstagramStoryNormalizedDimensions(asset.width, asset.height);
|
|
500
|
+
if (!normalizedDimensions) {
|
|
501
|
+
return asset;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (process.platform !== 'darwin') {
|
|
505
|
+
throw new CommandExecutionError(
|
|
506
|
+
`Instagram private story publish does not support auto-normalizing ${asset.fileName} on ${process.platform}`,
|
|
507
|
+
`Use images within ${INSTAGRAM_MIN_STORY_ASPECT_RATIO.toFixed(2)}-${INSTAGRAM_MAX_STORY_ASPECT_RATIO.toFixed(2)} aspect ratio, or use the UI route`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const outputPath = buildPrivateNormalizedImagePath(filePath);
|
|
512
|
+
const result = spawnSync('sips', [
|
|
513
|
+
'--padToHeightWidth',
|
|
514
|
+
String(normalizedDimensions.height),
|
|
515
|
+
String(normalizedDimensions.width),
|
|
516
|
+
'--padColor',
|
|
517
|
+
INSTAGRAM_PRIVATE_PAD_COLOR,
|
|
518
|
+
filePath,
|
|
519
|
+
'--out',
|
|
520
|
+
outputPath,
|
|
521
|
+
], {
|
|
522
|
+
encoding: 'utf8',
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
if (result.error || result.status !== 0 || !fs.existsSync(outputPath)) {
|
|
526
|
+
const detail = [result.error?.message, result.stderr, result.stdout]
|
|
527
|
+
.map((value) => String(value || '').trim())
|
|
528
|
+
.filter(Boolean)
|
|
529
|
+
.join(' ');
|
|
530
|
+
throw new CommandExecutionError(
|
|
531
|
+
`Instagram private story publish failed to normalize ${asset.fileName}`,
|
|
532
|
+
detail || 'sips padToHeightWidth failed',
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
...readImageAsset(outputPath),
|
|
538
|
+
cleanupPath: outputPath,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function runSwiftJsonScript<T>(script: string, args: string[], stage: string): T {
|
|
543
|
+
const scriptPath = path.join(os.tmpdir(), `opencli-instagram-${crypto.randomUUID()}.swift`);
|
|
544
|
+
fs.writeFileSync(scriptPath, script);
|
|
545
|
+
try {
|
|
546
|
+
const result = spawnSync('swift', [scriptPath, ...args], {
|
|
547
|
+
encoding: 'utf8',
|
|
548
|
+
});
|
|
549
|
+
if (result.error || result.status !== 0) {
|
|
550
|
+
const detail = [result.error?.message, result.stderr, result.stdout]
|
|
551
|
+
.map((value) => String(value || '').trim())
|
|
552
|
+
.filter(Boolean)
|
|
553
|
+
.join(' ');
|
|
554
|
+
throw new CommandExecutionError(
|
|
555
|
+
`Instagram private publish failed to ${stage}`,
|
|
556
|
+
detail || 'swift helper failed',
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return JSON.parse(String(result.stdout || '{}')) as T;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (error instanceof CommandExecutionError) throw error;
|
|
562
|
+
throw new CommandExecutionError(
|
|
563
|
+
`Instagram private publish failed to ${stage}`,
|
|
564
|
+
error instanceof Error ? error.message : String(error),
|
|
565
|
+
);
|
|
566
|
+
} finally {
|
|
567
|
+
fs.rmSync(scriptPath, { force: true });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function readVideoMetadata(filePath: string): { width: number; height: number; durationMs: number } {
|
|
572
|
+
if (process.platform !== 'darwin') {
|
|
573
|
+
throw new CommandExecutionError(
|
|
574
|
+
`Instagram private mixed-media publish does not support reading video metadata on ${process.platform}`,
|
|
575
|
+
'Use macOS for private mixed-media publishing, or rely on the UI fallback',
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const metadata = runSwiftJsonScript<{ width?: number; height?: number; durationMs?: number }>(`
|
|
580
|
+
import AVFoundation
|
|
581
|
+
import Foundation
|
|
582
|
+
|
|
583
|
+
let path = CommandLine.arguments[1]
|
|
584
|
+
let url = URL(fileURLWithPath: path)
|
|
585
|
+
let asset = AVURLAsset(url: url)
|
|
586
|
+
guard let track = asset.tracks(withMediaType: .video).first else {
|
|
587
|
+
fputs("{\\"error\\":\\"missing-video-track\\"}", stderr)
|
|
588
|
+
exit(1)
|
|
589
|
+
}
|
|
590
|
+
let transformed = track.naturalSize.applying(track.preferredTransform)
|
|
591
|
+
let width = Int(abs(transformed.width.rounded()))
|
|
592
|
+
let height = Int(abs(transformed.height.rounded()))
|
|
593
|
+
let durationMs = Int((CMTimeGetSeconds(asset.duration) * 1000.0).rounded())
|
|
594
|
+
let payload: [String: Int] = [
|
|
595
|
+
"width": width,
|
|
596
|
+
"height": height,
|
|
597
|
+
"durationMs": durationMs,
|
|
598
|
+
]
|
|
599
|
+
let data = try JSONSerialization.data(withJSONObject: payload, options: [])
|
|
600
|
+
FileHandle.standardOutput.write(data)
|
|
601
|
+
`, [filePath], 'read video metadata');
|
|
602
|
+
|
|
603
|
+
if (!metadata.width || !metadata.height || !metadata.durationMs) {
|
|
604
|
+
throw new CommandExecutionError(`Instagram private publish failed to read video metadata for ${filePath}`);
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
width: metadata.width,
|
|
608
|
+
height: metadata.height,
|
|
609
|
+
durationMs: metadata.durationMs,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function buildPrivateVideoCoverPath(filePath: string): string {
|
|
614
|
+
const parsed = path.parse(filePath);
|
|
615
|
+
return path.join(
|
|
616
|
+
os.tmpdir(),
|
|
617
|
+
`opencli-instagram-private-video-cover-${parsed.name}-${crypto.randomUUID()}.jpg`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function buildPrivateStoryVideoPath(filePath: string): string {
|
|
622
|
+
const parsed = path.parse(filePath);
|
|
623
|
+
return path.join(
|
|
624
|
+
os.tmpdir(),
|
|
625
|
+
`opencli-instagram-story-video-${parsed.name}-${crypto.randomUUID()}${parsed.ext || '.mp4'}`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function generateVideoCoverImage(filePath: string): PreparedInstagramImageAsset {
|
|
630
|
+
if (process.platform !== 'darwin') {
|
|
631
|
+
throw new CommandExecutionError(
|
|
632
|
+
`Instagram private mixed-media publish does not support generating video covers on ${process.platform}`,
|
|
633
|
+
'Use macOS for private mixed-media publishing, or rely on the UI fallback',
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const outputPath = buildPrivateVideoCoverPath(filePath);
|
|
638
|
+
runSwiftJsonScript<{ ok?: boolean }>(`
|
|
639
|
+
import AVFoundation
|
|
640
|
+
import AppKit
|
|
641
|
+
import Foundation
|
|
642
|
+
|
|
643
|
+
let inputPath = CommandLine.arguments[1]
|
|
644
|
+
let outputPath = CommandLine.arguments[2]
|
|
645
|
+
let asset = AVURLAsset(url: URL(fileURLWithPath: inputPath))
|
|
646
|
+
let generator = AVAssetImageGenerator(asset: asset)
|
|
647
|
+
generator.appliesPreferredTrackTransform = true
|
|
648
|
+
let image = try generator.copyCGImage(at: CMTime(seconds: 0, preferredTimescale: 600), actualTime: nil)
|
|
649
|
+
let rep = NSBitmapImageRep(cgImage: image)
|
|
650
|
+
guard let data = rep.representation(using: .jpeg, properties: [.compressionFactor: 0.9]) else {
|
|
651
|
+
fputs("{\\"error\\":\\"jpeg-encode-failed\\"}", stderr)
|
|
652
|
+
exit(1)
|
|
653
|
+
}
|
|
654
|
+
try data.write(to: URL(fileURLWithPath: outputPath))
|
|
655
|
+
let payload = ["ok": true]
|
|
656
|
+
let json = try JSONSerialization.data(withJSONObject: payload, options: [])
|
|
657
|
+
FileHandle.standardOutput.write(json)
|
|
658
|
+
`, [filePath, outputPath], 'generate video cover');
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
...readImageAsset(outputPath),
|
|
662
|
+
cleanupPath: outputPath,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function readVideoAsset(filePath: string): InstagramVideoAsset {
|
|
667
|
+
const bytes = fs.readFileSync(filePath);
|
|
668
|
+
const metadata = readVideoMetadata(filePath);
|
|
669
|
+
const coverImage = generateVideoCoverImage(filePath);
|
|
670
|
+
return {
|
|
671
|
+
filePath,
|
|
672
|
+
fileName: path.basename(filePath),
|
|
673
|
+
mimeType: 'video/mp4',
|
|
674
|
+
width: metadata.width,
|
|
675
|
+
height: metadata.height,
|
|
676
|
+
durationMs: metadata.durationMs,
|
|
677
|
+
byteLength: bytes.length,
|
|
678
|
+
bytes,
|
|
679
|
+
coverImage,
|
|
680
|
+
cleanupPaths: coverImage.cleanupPath ? [coverImage.cleanupPath] : [],
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function trimVideoForInstagramStory(filePath: string, maxDurationMs: number): string {
|
|
685
|
+
if (process.platform !== 'darwin') {
|
|
686
|
+
throw new CommandExecutionError(
|
|
687
|
+
`Instagram private story publish does not support trimming long videos on ${process.platform}`,
|
|
688
|
+
'Use macOS for private story video publishing, or trim the video to 15 seconds first',
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const outputPath = buildPrivateStoryVideoPath(filePath);
|
|
693
|
+
runSwiftJsonScript<{ ok?: boolean }>(`
|
|
694
|
+
import AVFoundation
|
|
695
|
+
import Foundation
|
|
696
|
+
|
|
697
|
+
let inputPath = CommandLine.arguments[1]
|
|
698
|
+
let outputPath = CommandLine.arguments[2]
|
|
699
|
+
let durationMs = Int(CommandLine.arguments[3]) ?? 15000
|
|
700
|
+
let asset = AVURLAsset(url: URL(fileURLWithPath: inputPath))
|
|
701
|
+
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
|
702
|
+
fputs("{\\"error\\":\\"missing-export-session\\"}", stderr)
|
|
703
|
+
exit(1)
|
|
704
|
+
}
|
|
705
|
+
exportSession.outputURL = URL(fileURLWithPath: outputPath)
|
|
706
|
+
exportSession.outputFileType = .mp4
|
|
707
|
+
exportSession.shouldOptimizeForNetworkUse = true
|
|
708
|
+
exportSession.timeRange = CMTimeRange(
|
|
709
|
+
start: .zero,
|
|
710
|
+
duration: CMTime(seconds: Double(durationMs) / 1000.0, preferredTimescale: 600)
|
|
711
|
+
)
|
|
712
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
713
|
+
exportSession.exportAsynchronously {
|
|
714
|
+
semaphore.signal()
|
|
715
|
+
}
|
|
716
|
+
semaphore.wait()
|
|
717
|
+
if exportSession.status != .completed {
|
|
718
|
+
let message = exportSession.error?.localizedDescription ?? "export-failed"
|
|
719
|
+
fputs(message, stderr)
|
|
720
|
+
exit(1)
|
|
721
|
+
}
|
|
722
|
+
let payload = ["ok": true]
|
|
723
|
+
let json = try JSONSerialization.data(withJSONObject: payload, options: [])
|
|
724
|
+
FileHandle.standardOutput.write(json)
|
|
725
|
+
`, [filePath, outputPath, String(maxDurationMs)], 'trim story video');
|
|
726
|
+
|
|
727
|
+
return outputPath;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function prepareVideoAssetForPrivateStoryUpload(filePath: string): InstagramVideoAsset {
|
|
731
|
+
const asset = readVideoAsset(filePath);
|
|
732
|
+
if (asset.durationMs <= INSTAGRAM_MAX_STORY_VIDEO_DURATION_MS) {
|
|
733
|
+
return asset;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const trimmedPath = trimVideoForInstagramStory(filePath, INSTAGRAM_MAX_STORY_VIDEO_DURATION_MS);
|
|
737
|
+
const trimmedAsset = readVideoAsset(trimmedPath);
|
|
738
|
+
return {
|
|
739
|
+
...trimmedAsset,
|
|
740
|
+
cleanupPaths: [
|
|
741
|
+
...(trimmedAsset.cleanupPaths || []),
|
|
742
|
+
trimmedPath,
|
|
743
|
+
],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function toUnixSeconds(now: () => number): number {
|
|
748
|
+
const value = now();
|
|
749
|
+
return value > 10_000_000_000 ? Math.floor(value / 1000) : Math.floor(value);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export function buildConfigureToStoryPhotoPayload(input: StoryPayloadInput): Record<string, unknown> {
|
|
753
|
+
const now = input.now ?? (() => Date.now());
|
|
754
|
+
const timestamp = toUnixSeconds(now);
|
|
755
|
+
return {
|
|
756
|
+
source_type: '4',
|
|
757
|
+
upload_id: input.uploadId,
|
|
758
|
+
story_media_creation_date: String(timestamp - 17),
|
|
759
|
+
client_shared_at: String(timestamp - 5),
|
|
760
|
+
client_timestamp: String(timestamp),
|
|
761
|
+
configure_mode: 1,
|
|
762
|
+
edits: {
|
|
763
|
+
crop_original_size: [input.width, input.height],
|
|
764
|
+
crop_center: [0, 0],
|
|
765
|
+
crop_zoom: 1.3333334,
|
|
766
|
+
},
|
|
767
|
+
extra: {
|
|
768
|
+
source_width: input.width,
|
|
769
|
+
source_height: input.height,
|
|
770
|
+
},
|
|
771
|
+
jazoest: input.jazoest,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function buildConfigureToStoryVideoPayload(input: StoryVideoPayloadInput): Record<string, unknown> {
|
|
776
|
+
const now = input.now ?? (() => Date.now());
|
|
777
|
+
const timestamp = toUnixSeconds(now);
|
|
778
|
+
const durationSeconds = Number((input.durationMs / 1000).toFixed(3));
|
|
779
|
+
return {
|
|
780
|
+
source_type: '4',
|
|
781
|
+
upload_id: input.uploadId,
|
|
782
|
+
story_media_creation_date: String(timestamp - 17),
|
|
783
|
+
client_shared_at: String(timestamp - 5),
|
|
784
|
+
client_timestamp: String(timestamp),
|
|
785
|
+
configure_mode: 1,
|
|
786
|
+
poster_frame_index: 0,
|
|
787
|
+
length: durationSeconds,
|
|
788
|
+
audio_muted: false,
|
|
789
|
+
filter_type: '0',
|
|
790
|
+
video_result: 'deprecated',
|
|
791
|
+
extra: {
|
|
792
|
+
source_width: input.width,
|
|
793
|
+
source_height: input.height,
|
|
794
|
+
},
|
|
795
|
+
jazoest: input.jazoest,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function buildFormEncodedBodyFromPayload(payload: Record<string, unknown>): string {
|
|
800
|
+
const body = new URLSearchParams();
|
|
801
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
802
|
+
if (value === undefined || value === null) continue;
|
|
803
|
+
if (typeof value === 'object') {
|
|
804
|
+
body.set(key, JSON.stringify(value));
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
body.set(key, String(value));
|
|
808
|
+
}
|
|
809
|
+
return body.toString();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function buildSignedBody(payload: Record<string, unknown>): string {
|
|
813
|
+
const jsonPayload = JSON.stringify(payload);
|
|
814
|
+
const signature = crypto
|
|
815
|
+
.createHmac('sha256', INSTAGRAM_STORY_SIG_KEY)
|
|
816
|
+
.update(jsonPayload)
|
|
817
|
+
.digest('hex');
|
|
818
|
+
const body = new URLSearchParams();
|
|
819
|
+
body.set('ig_sig_key_version', INSTAGRAM_STORY_SIG_KEY_VERSION);
|
|
820
|
+
body.set('signed_body', `${signature}.${jsonPayload}`);
|
|
821
|
+
return body.toString();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function buildPrivateApiHeaders(context: InstagramPrivateApiContext): Record<string, string> {
|
|
825
|
+
return Object.fromEntries(Object.entries({
|
|
826
|
+
'X-ASBD-ID': context.asbdId,
|
|
827
|
+
'X-CSRFToken': context.csrfToken,
|
|
828
|
+
'X-IG-App-ID': context.igAppId,
|
|
829
|
+
'X-IG-WWW-Claim': context.igWwwClaim,
|
|
830
|
+
'X-Instagram-AJAX': context.instagramAjax,
|
|
831
|
+
'X-Web-Session-ID': context.webSessionId,
|
|
832
|
+
}).filter(([, value]) => !!value));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function buildRuploadHeaders(
|
|
836
|
+
asset: InstagramImageAsset,
|
|
837
|
+
uploadId: string,
|
|
838
|
+
context: InstagramPrivateApiContext,
|
|
839
|
+
): Record<string, string> {
|
|
840
|
+
return {
|
|
841
|
+
...buildPrivateApiHeaders(context),
|
|
842
|
+
'Accept': '*/*',
|
|
843
|
+
'Content-Type': asset.mimeType,
|
|
844
|
+
'Offset': '0',
|
|
845
|
+
'X-Entity-Length': String(asset.byteLength),
|
|
846
|
+
'X-Entity-Name': `fb_uploader_${uploadId}`,
|
|
847
|
+
'X-Entity-Type': asset.mimeType,
|
|
848
|
+
'X-Instagram-Rupload-Params': JSON.stringify({
|
|
849
|
+
media_type: 1,
|
|
850
|
+
upload_id: uploadId,
|
|
851
|
+
upload_media_height: asset.height,
|
|
852
|
+
upload_media_width: asset.width,
|
|
853
|
+
}),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function buildVideoEditParams(asset: InstagramVideoAsset): Record<string, number | boolean> {
|
|
858
|
+
const cropSize = Math.min(asset.width, asset.height);
|
|
859
|
+
const trimEndSeconds = Number((asset.durationMs / 1000).toFixed(3));
|
|
860
|
+
return {
|
|
861
|
+
crop_height: cropSize,
|
|
862
|
+
crop_width: cropSize,
|
|
863
|
+
crop_x1: Math.max(0, Math.floor((asset.width - cropSize) / 2)),
|
|
864
|
+
crop_y1: Math.max(0, Math.floor((asset.height - cropSize) / 2)),
|
|
865
|
+
mute: false,
|
|
866
|
+
trim_end: trimEndSeconds,
|
|
867
|
+
trim_start: 0,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function buildVideoRuploadHeaders(
|
|
872
|
+
asset: InstagramVideoAsset,
|
|
873
|
+
uploadId: string,
|
|
874
|
+
context: InstagramPrivateApiContext,
|
|
875
|
+
): Record<string, string> {
|
|
876
|
+
return {
|
|
877
|
+
...buildPrivateApiHeaders(context),
|
|
878
|
+
'Accept': '*/*',
|
|
879
|
+
'Offset': '0',
|
|
880
|
+
'X-Entity-Length': String(asset.byteLength),
|
|
881
|
+
'X-Entity-Name': `fb_uploader_${uploadId}`,
|
|
882
|
+
'X-Instagram-Rupload-Params': JSON.stringify({
|
|
883
|
+
'client-passthrough': '1',
|
|
884
|
+
'is_unified_video': '0',
|
|
885
|
+
'is_sidecar': '1',
|
|
886
|
+
'media_type': 2,
|
|
887
|
+
'for_album': false,
|
|
888
|
+
'video_format': '',
|
|
889
|
+
'upload_id': uploadId,
|
|
890
|
+
'upload_media_duration_ms': asset.durationMs,
|
|
891
|
+
'upload_media_height': asset.height,
|
|
892
|
+
'upload_media_width': asset.width,
|
|
893
|
+
'video_transform': null,
|
|
894
|
+
'video_edit_params': buildVideoEditParams(asset),
|
|
895
|
+
}),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function buildStoryVideoRuploadHeaders(
|
|
900
|
+
asset: InstagramVideoAsset,
|
|
901
|
+
uploadId: string,
|
|
902
|
+
context: InstagramPrivateApiContext,
|
|
903
|
+
): Record<string, string> {
|
|
904
|
+
return {
|
|
905
|
+
...buildPrivateApiHeaders(context),
|
|
906
|
+
'Accept': '*/*',
|
|
907
|
+
'Offset': '0',
|
|
908
|
+
'X-Entity-Length': String(asset.byteLength),
|
|
909
|
+
'X-Entity-Name': `fb_uploader_${uploadId}`,
|
|
910
|
+
'X-Instagram-Rupload-Params': JSON.stringify({
|
|
911
|
+
'client-passthrough': '1',
|
|
912
|
+
'media_type': 2,
|
|
913
|
+
'upload_id': uploadId,
|
|
914
|
+
'upload_media_duration_ms': asset.durationMs,
|
|
915
|
+
'upload_media_height': asset.height,
|
|
916
|
+
'upload_media_width': asset.width,
|
|
917
|
+
'video_transform': null,
|
|
918
|
+
'video_edit_params': buildVideoEditParams(asset),
|
|
919
|
+
}),
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function buildVideoCoverRuploadHeaders(
|
|
924
|
+
asset: InstagramVideoAsset,
|
|
925
|
+
uploadId: string,
|
|
926
|
+
context: InstagramPrivateApiContext,
|
|
927
|
+
): Record<string, string> {
|
|
928
|
+
return {
|
|
929
|
+
...buildPrivateApiHeaders(context),
|
|
930
|
+
'Accept': '*/*',
|
|
931
|
+
'Content-Type': asset.coverImage.mimeType,
|
|
932
|
+
'Offset': '0',
|
|
933
|
+
'X-Entity-Length': String(asset.coverImage.byteLength),
|
|
934
|
+
'X-Entity-Name': `fb_uploader_${uploadId}`,
|
|
935
|
+
'X-Entity-Type': asset.coverImage.mimeType,
|
|
936
|
+
'X-Instagram-Rupload-Params': JSON.stringify({
|
|
937
|
+
media_type: 2,
|
|
938
|
+
upload_id: uploadId,
|
|
939
|
+
upload_media_height: asset.height,
|
|
940
|
+
upload_media_width: asset.width,
|
|
941
|
+
}),
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function parseJsonResponse(response: Response, stage: string): Promise<any> {
|
|
946
|
+
const text = await response.text();
|
|
947
|
+
let data: any;
|
|
948
|
+
try {
|
|
949
|
+
data = text ? JSON.parse(text) : {};
|
|
950
|
+
} catch {
|
|
951
|
+
throw new CommandExecutionError(`Instagram private publish ${stage} returned invalid JSON`);
|
|
952
|
+
}
|
|
953
|
+
if (!response.ok) {
|
|
954
|
+
const detail = text ? ` ${text.slice(0, 500)}` : '';
|
|
955
|
+
throw new CommandExecutionError(`Instagram private publish ${stage} failed: ${response.status}${detail}`);
|
|
956
|
+
}
|
|
957
|
+
return data;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async function fetchPrivateUploadWithRetry(
|
|
961
|
+
fetcher: PrivateApiFetchLike,
|
|
962
|
+
url: string,
|
|
963
|
+
init: PrivateApiFetchInit,
|
|
964
|
+
): Promise<Response> {
|
|
965
|
+
let lastError: unknown;
|
|
966
|
+
for (let attempt = 0; attempt < INSTAGRAM_PRIVATE_UPLOAD_RETRY_BUDGET; attempt += 1) {
|
|
967
|
+
try {
|
|
968
|
+
return await fetcher(url, init);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
lastError = error;
|
|
971
|
+
if (!isTransientPrivateFetchError(error) || attempt >= INSTAGRAM_PRIVATE_UPLOAD_RETRY_BUDGET - 1) {
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function prepareInstagramMediaAsset(item: InstagramMediaItem): Promise<PreparedInstagramMediaAsset> {
|
|
980
|
+
if (item.type === 'video') {
|
|
981
|
+
return {
|
|
982
|
+
type: 'video',
|
|
983
|
+
asset: readVideoAsset(item.filePath),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
type: 'image',
|
|
988
|
+
asset: prepareImageAssetForPrivateUpload(item.filePath),
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function cleanupPreparedMediaAssets(assets: PreparedInstagramMediaAsset[]): void {
|
|
993
|
+
for (const prepared of assets) {
|
|
994
|
+
if (prepared.type === 'image') {
|
|
995
|
+
if (prepared.asset.cleanupPath) {
|
|
996
|
+
fs.rmSync(prepared.asset.cleanupPath, { force: true });
|
|
997
|
+
}
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
for (const cleanupPath of prepared.asset.cleanupPaths || []) {
|
|
1001
|
+
fs.rmSync(cleanupPath, { force: true });
|
|
1002
|
+
}
|
|
1003
|
+
if (prepared.asset.coverImage.cleanupPath) {
|
|
1004
|
+
fs.rmSync(prepared.asset.coverImage.cleanupPath, { force: true });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function uploadPreparedMediaAsset(
|
|
1010
|
+
fetcher: PrivateApiFetchLike,
|
|
1011
|
+
prepared: PreparedInstagramMediaAsset,
|
|
1012
|
+
uploadId: string,
|
|
1013
|
+
context: InstagramPrivateApiContext,
|
|
1014
|
+
mode: 'feed' | 'story' = 'feed',
|
|
1015
|
+
): Promise<void> {
|
|
1016
|
+
if (prepared.type === 'image') {
|
|
1017
|
+
const response = await fetchPrivateUploadWithRetry(fetcher, `https://i.instagram.com/rupload_igphoto/fb_uploader_${uploadId}`, {
|
|
1018
|
+
method: 'POST',
|
|
1019
|
+
headers: buildRuploadHeaders(prepared.asset, uploadId, context),
|
|
1020
|
+
body: prepared.asset.bytes,
|
|
1021
|
+
});
|
|
1022
|
+
const json = await parseJsonResponse(response, 'upload');
|
|
1023
|
+
if (String(json?.status || '') !== 'ok') {
|
|
1024
|
+
throw new CommandExecutionError(`Instagram private publish upload failed for ${prepared.asset.fileName}`);
|
|
1025
|
+
}
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const videoResponse = await fetchPrivateUploadWithRetry(fetcher, `https://i.instagram.com/rupload_igvideo/fb_uploader_${uploadId}`, {
|
|
1030
|
+
method: 'POST',
|
|
1031
|
+
headers: mode === 'story'
|
|
1032
|
+
? buildStoryVideoRuploadHeaders(prepared.asset, uploadId, context)
|
|
1033
|
+
: buildVideoRuploadHeaders(prepared.asset, uploadId, context),
|
|
1034
|
+
body: prepared.asset.bytes,
|
|
1035
|
+
});
|
|
1036
|
+
const videoJson = await parseJsonResponse(videoResponse, 'video upload');
|
|
1037
|
+
if (String(videoJson?.status || '') !== 'ok') {
|
|
1038
|
+
throw new CommandExecutionError(`Instagram private publish video upload failed for ${prepared.asset.fileName}`);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const coverResponse = await fetchPrivateUploadWithRetry(fetcher, `https://i.instagram.com/rupload_igphoto/fb_uploader_${uploadId}`, {
|
|
1042
|
+
method: 'POST',
|
|
1043
|
+
headers: buildVideoCoverRuploadHeaders(prepared.asset, uploadId, context),
|
|
1044
|
+
body: prepared.asset.coverImage.bytes,
|
|
1045
|
+
});
|
|
1046
|
+
const coverJson = await parseJsonResponse(coverResponse, 'video cover upload');
|
|
1047
|
+
if (String(coverJson?.status || '') !== 'ok') {
|
|
1048
|
+
throw new CommandExecutionError(`Instagram private publish video cover upload failed for ${prepared.asset.fileName}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function publishSidecarWithRetry(input: {
|
|
1053
|
+
fetcher: PrivateApiFetchLike;
|
|
1054
|
+
payload: Record<string, unknown>;
|
|
1055
|
+
apiContext: InstagramPrivateApiContext;
|
|
1056
|
+
waitMs?: (ms: number) => Promise<void>;
|
|
1057
|
+
}): Promise<{ code?: string }> {
|
|
1058
|
+
const waitMs = input.waitMs ?? sleep;
|
|
1059
|
+
const requestInit: PrivateApiFetchInit = {
|
|
1060
|
+
method: 'POST',
|
|
1061
|
+
headers: {
|
|
1062
|
+
...buildPrivateApiHeaders(input.apiContext),
|
|
1063
|
+
'Content-Type': 'application/json',
|
|
1064
|
+
},
|
|
1065
|
+
body: JSON.stringify(input.payload),
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
for (let attempt = 0; attempt < INSTAGRAM_PRIVATE_SIDECAR_TRANSCODE_ATTEMPTS; attempt += 1) {
|
|
1069
|
+
const response = await input.fetcher('https://www.instagram.com/api/v1/media/configure_sidecar/', requestInit);
|
|
1070
|
+
const text = await response.text();
|
|
1071
|
+
let json: any = {};
|
|
1072
|
+
try {
|
|
1073
|
+
json = text ? JSON.parse(text) : {};
|
|
1074
|
+
} catch {
|
|
1075
|
+
throw new CommandExecutionError('Instagram private publish configure_sidecar returned invalid JSON');
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (!response.ok) {
|
|
1079
|
+
const detail = text ? ` ${text.slice(0, 500)}` : '';
|
|
1080
|
+
throw new CommandExecutionError(`Instagram private publish configure_sidecar failed: ${response.status}${detail}`);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const message = String(json?.message || '');
|
|
1084
|
+
if (
|
|
1085
|
+
response.status === 202
|
|
1086
|
+
|| /transcode not finished yet/i.test(message)
|
|
1087
|
+
) {
|
|
1088
|
+
if (attempt >= INSTAGRAM_PRIVATE_SIDECAR_TRANSCODE_ATTEMPTS - 1) {
|
|
1089
|
+
throw new CommandExecutionError(
|
|
1090
|
+
'Instagram private publish configure_sidecar timed out waiting for video transcode',
|
|
1091
|
+
text.slice(0, 500),
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
await waitMs(INSTAGRAM_PRIVATE_SIDECAR_TRANSCODE_WAIT_MS);
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (String(json?.status || '').toLowerCase() === 'fail') {
|
|
1099
|
+
throw new CommandExecutionError(
|
|
1100
|
+
'Instagram private publish configure_sidecar failed',
|
|
1101
|
+
message || text.slice(0, 500),
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return { code: json?.media?.code };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
throw new CommandExecutionError('Instagram private publish configure_sidecar failed');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export async function publishMediaViaPrivateApi(input: {
|
|
1112
|
+
page: unknown;
|
|
1113
|
+
mediaItems: InstagramMediaItem[];
|
|
1114
|
+
caption: string;
|
|
1115
|
+
apiContext: InstagramPrivateApiContext;
|
|
1116
|
+
jazoest: string;
|
|
1117
|
+
now?: () => number;
|
|
1118
|
+
fetcher?: PrivateApiFetchLike;
|
|
1119
|
+
prepareMediaAsset?: (item: InstagramMediaItem) => PreparedInstagramMediaAsset | Promise<PreparedInstagramMediaAsset>;
|
|
1120
|
+
waitMs?: (ms: number) => Promise<void>;
|
|
1121
|
+
}): Promise<{ code?: string; uploadIds: string[] }> {
|
|
1122
|
+
const now = input.now ?? (() => Date.now());
|
|
1123
|
+
const clientSidecarId = String(now());
|
|
1124
|
+
const uploadIds = input.mediaItems.length > 1
|
|
1125
|
+
? input.mediaItems.map((_, index) => String(now() + index + 1))
|
|
1126
|
+
: [String(now())];
|
|
1127
|
+
const fetcher: PrivateApiFetchLike = input.fetcher ?? ((url, init) => instagramPrivateApiFetch(input.page as any, url, init as any));
|
|
1128
|
+
const prepareMediaAsset = input.prepareMediaAsset ?? prepareInstagramMediaAsset;
|
|
1129
|
+
const assets = await Promise.all(input.mediaItems.map((item) => prepareMediaAsset(item)));
|
|
1130
|
+
|
|
1131
|
+
try {
|
|
1132
|
+
for (let index = 0; index < assets.length; index += 1) {
|
|
1133
|
+
const asset = assets[index]!;
|
|
1134
|
+
const uploadId = uploadIds[index]!;
|
|
1135
|
+
await uploadPreparedMediaAsset(fetcher, asset, uploadId, input.apiContext);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (uploadIds.length === 1) {
|
|
1139
|
+
if (assets[0]?.type !== 'image') {
|
|
1140
|
+
throw new CommandExecutionError('Instagram private publish only supports single-video uploads through instagram reel');
|
|
1141
|
+
}
|
|
1142
|
+
const response = await fetcher('https://www.instagram.com/api/v1/media/configure/', {
|
|
1143
|
+
method: 'POST',
|
|
1144
|
+
headers: {
|
|
1145
|
+
...buildPrivateApiHeaders(input.apiContext),
|
|
1146
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1147
|
+
},
|
|
1148
|
+
body: buildConfigureBody({
|
|
1149
|
+
uploadId: uploadIds[0]!,
|
|
1150
|
+
caption: input.caption,
|
|
1151
|
+
jazoest: input.jazoest,
|
|
1152
|
+
}),
|
|
1153
|
+
});
|
|
1154
|
+
const json = await parseJsonResponse(response, 'configure');
|
|
1155
|
+
return { code: json?.media?.code, uploadIds };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const result = await publishSidecarWithRetry({
|
|
1159
|
+
fetcher,
|
|
1160
|
+
payload: buildConfigureSidecarPayload({
|
|
1161
|
+
uploadIds,
|
|
1162
|
+
caption: input.caption,
|
|
1163
|
+
clientSidecarId,
|
|
1164
|
+
jazoest: input.jazoest,
|
|
1165
|
+
}),
|
|
1166
|
+
apiContext: input.apiContext,
|
|
1167
|
+
waitMs: input.waitMs,
|
|
1168
|
+
});
|
|
1169
|
+
return { code: result.code, uploadIds };
|
|
1170
|
+
} finally {
|
|
1171
|
+
cleanupPreparedMediaAssets(assets);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export async function publishImagesViaPrivateApi(input: {
|
|
1176
|
+
page: unknown;
|
|
1177
|
+
imagePaths: string[];
|
|
1178
|
+
caption: string;
|
|
1179
|
+
apiContext: InstagramPrivateApiContext;
|
|
1180
|
+
jazoest: string;
|
|
1181
|
+
now?: () => number;
|
|
1182
|
+
fetcher?: PrivateApiFetchLike;
|
|
1183
|
+
prepareAsset?: (filePath: string) => PreparedInstagramImageAsset | Promise<PreparedInstagramImageAsset>;
|
|
1184
|
+
waitMs?: (ms: number) => Promise<void>;
|
|
1185
|
+
}): Promise<{ code?: string; uploadIds: string[] }> {
|
|
1186
|
+
return publishMediaViaPrivateApi({
|
|
1187
|
+
page: input.page,
|
|
1188
|
+
mediaItems: input.imagePaths.map((filePath) => ({ type: 'image' as const, filePath })),
|
|
1189
|
+
caption: input.caption,
|
|
1190
|
+
apiContext: input.apiContext,
|
|
1191
|
+
jazoest: input.jazoest,
|
|
1192
|
+
now: input.now,
|
|
1193
|
+
fetcher: input.fetcher,
|
|
1194
|
+
waitMs: input.waitMs,
|
|
1195
|
+
prepareMediaAsset: input.prepareAsset
|
|
1196
|
+
? async (item) => ({
|
|
1197
|
+
type: 'image' as const,
|
|
1198
|
+
asset: await input.prepareAsset!(item.filePath),
|
|
1199
|
+
})
|
|
1200
|
+
: undefined,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
export async function publishStoryViaPrivateApi(input: {
|
|
1205
|
+
page: unknown;
|
|
1206
|
+
mediaItem: InstagramMediaItem;
|
|
1207
|
+
content: string;
|
|
1208
|
+
apiContext: InstagramPrivateApiContext;
|
|
1209
|
+
jazoest: string;
|
|
1210
|
+
currentUserId?: string;
|
|
1211
|
+
now?: () => number;
|
|
1212
|
+
fetcher?: PrivateApiFetchLike;
|
|
1213
|
+
prepareMediaAsset?: (item: InstagramMediaItem) => PreparedInstagramMediaAsset | Promise<PreparedInstagramMediaAsset>;
|
|
1214
|
+
}): Promise<{ mediaPk?: string; uploadId: string }> {
|
|
1215
|
+
const now = input.now ?? (() => Date.now());
|
|
1216
|
+
const uploadId = String(now());
|
|
1217
|
+
const fetcher: PrivateApiFetchLike = input.fetcher ?? ((url, init) => instagramPrivateApiFetch(input.page as any, url, init as any));
|
|
1218
|
+
const prepareMediaAsset = input.prepareMediaAsset ?? (async (item: InstagramMediaItem) => item.type === 'video'
|
|
1219
|
+
? { type: 'video' as const, asset: prepareVideoAssetForPrivateStoryUpload(item.filePath) }
|
|
1220
|
+
: { type: 'image' as const, asset: prepareImageAssetForPrivateStoryUpload(item.filePath) });
|
|
1221
|
+
const prepared = await prepareMediaAsset(input.mediaItem);
|
|
1222
|
+
const currentUserId = input.currentUserId
|
|
1223
|
+
|| ('getCookies' in (input.page as any)
|
|
1224
|
+
? String((await ((input.page as IPage).getCookies?.({ domain: 'instagram.com' }) ?? Promise.resolve([] as BrowserCookie[])))
|
|
1225
|
+
.find((cookie) => cookie.name === 'ds_user_id')?.value || '')
|
|
1226
|
+
: '');
|
|
1227
|
+
if (!currentUserId) {
|
|
1228
|
+
throw new CommandExecutionError('Instagram story publish could not derive current user id from browser session');
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const signedPayloadBase = {
|
|
1232
|
+
_csrftoken: input.apiContext.csrfToken,
|
|
1233
|
+
_uid: currentUserId,
|
|
1234
|
+
_uuid: crypto.randomUUID(),
|
|
1235
|
+
device: INSTAGRAM_STORY_DEVICE,
|
|
1236
|
+
};
|
|
1237
|
+
const buildSignedStoryPhotoBody = (width: number, height: number) => buildSignedBody({
|
|
1238
|
+
...buildConfigureToStoryPhotoPayload({
|
|
1239
|
+
uploadId,
|
|
1240
|
+
width,
|
|
1241
|
+
height,
|
|
1242
|
+
now,
|
|
1243
|
+
jazoest: input.jazoest,
|
|
1244
|
+
}),
|
|
1245
|
+
...signedPayloadBase,
|
|
1246
|
+
});
|
|
1247
|
+
const buildSignedStoryVideoBody = (width: number, height: number, durationMs: number) => buildSignedBody({
|
|
1248
|
+
...buildConfigureToStoryVideoPayload({
|
|
1249
|
+
uploadId,
|
|
1250
|
+
width,
|
|
1251
|
+
height,
|
|
1252
|
+
durationMs,
|
|
1253
|
+
now,
|
|
1254
|
+
jazoest: input.jazoest,
|
|
1255
|
+
}),
|
|
1256
|
+
...signedPayloadBase,
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
try {
|
|
1260
|
+
await uploadPreparedMediaAsset(fetcher, prepared, uploadId, input.apiContext, 'story');
|
|
1261
|
+
|
|
1262
|
+
if (prepared.type === 'image') {
|
|
1263
|
+
const response = await fetcher('https://i.instagram.com/api/v1/media/configure_to_story/', {
|
|
1264
|
+
method: 'POST',
|
|
1265
|
+
headers: {
|
|
1266
|
+
...buildPrivateApiHeaders(input.apiContext),
|
|
1267
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1268
|
+
},
|
|
1269
|
+
body: buildSignedStoryPhotoBody(prepared.asset.width, prepared.asset.height),
|
|
1270
|
+
});
|
|
1271
|
+
const json = await parseJsonResponse(response, 'configure_to_story');
|
|
1272
|
+
return {
|
|
1273
|
+
mediaPk: String(json?.media?.pk || json?.media?.id || '').split('_')[0] || undefined,
|
|
1274
|
+
uploadId,
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
await parseJsonResponse(await fetcher('https://i.instagram.com/api/v1/media/configure_to_story/', {
|
|
1279
|
+
method: 'POST',
|
|
1280
|
+
headers: {
|
|
1281
|
+
...buildPrivateApiHeaders(input.apiContext),
|
|
1282
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1283
|
+
},
|
|
1284
|
+
body: buildSignedStoryPhotoBody(prepared.asset.width, prepared.asset.height),
|
|
1285
|
+
}), 'configure_to_story cover');
|
|
1286
|
+
|
|
1287
|
+
const response = await fetcher('https://i.instagram.com/api/v1/media/configure_to_story/?video=1', {
|
|
1288
|
+
method: 'POST',
|
|
1289
|
+
headers: {
|
|
1290
|
+
...buildPrivateApiHeaders(input.apiContext),
|
|
1291
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1292
|
+
},
|
|
1293
|
+
body: buildSignedStoryVideoBody(prepared.asset.width, prepared.asset.height, prepared.asset.durationMs),
|
|
1294
|
+
});
|
|
1295
|
+
const json = await parseJsonResponse(response, 'configure_to_story');
|
|
1296
|
+
return {
|
|
1297
|
+
mediaPk: String(json?.media?.pk || json?.media?.id || '').split('_')[0] || undefined,
|
|
1298
|
+
uploadId,
|
|
1299
|
+
};
|
|
1300
|
+
} finally {
|
|
1301
|
+
cleanupPreparedMediaAssets([prepared]);
|
|
1302
|
+
}
|
|
1303
|
+
}
|