@opencoredev/social-sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +223 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud/common.d.ts +26 -0
- package/dist/cloud/common.d.ts.map +1 -0
- package/dist/cloud/common.js +307 -0
- package/dist/cloud/common.js.map +1 -0
- package/dist/cloud/lifecycle.d.ts +11 -0
- package/dist/cloud/lifecycle.d.ts.map +1 -0
- package/dist/cloud/lifecycle.js +108 -0
- package/dist/cloud/lifecycle.js.map +1 -0
- package/dist/cloud/media.d.ts +24 -0
- package/dist/cloud/media.d.ts.map +1 -0
- package/dist/cloud/media.js +97 -0
- package/dist/cloud/media.js.map +1 -0
- package/dist/cloud/outcomes.d.ts +10 -0
- package/dist/cloud/outcomes.d.ts.map +1 -0
- package/dist/cloud/outcomes.js +188 -0
- package/dist/cloud/outcomes.js.map +1 -0
- package/dist/cloud/post-for-me.d.ts +70 -0
- package/dist/cloud/post-for-me.d.ts.map +1 -0
- package/dist/cloud/post-for-me.js +326 -0
- package/dist/cloud/post-for-me.js.map +1 -0
- package/dist/cloud/zernio.d.ts +112 -0
- package/dist/cloud/zernio.d.ts.map +1 -0
- package/dist/cloud/zernio.js +542 -0
- package/dist/cloud/zernio.js.map +1 -0
- package/dist/core/adapter.d.ts +118 -0
- package/dist/core/adapter.d.ts.map +1 -0
- package/dist/core/adapter.js +4 -0
- package/dist/core/adapter.js.map +1 -0
- package/dist/core/client.d.ts +108 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +893 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/concurrency.d.ts +10 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +78 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/errors.d.ts +45 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +44 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/idempotency.d.ts +34 -0
- package/dist/core/idempotency.d.ts.map +1 -0
- package/dist/core/idempotency.js +59 -0
- package/dist/core/idempotency.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/pagination.d.ts +12 -0
- package/dist/core/pagination.d.ts.map +1 -0
- package/dist/core/pagination.js +81 -0
- package/dist/core/pagination.js.map +1 -0
- package/dist/core/types.d.ts +329 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +7 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/platforms/bluesky.d.ts +132 -0
- package/dist/platforms/bluesky.d.ts.map +1 -0
- package/dist/platforms/bluesky.js +1049 -0
- package/dist/platforms/bluesky.js.map +1 -0
- package/dist/platforms/instagram.d.ts +63 -0
- package/dist/platforms/instagram.d.ts.map +1 -0
- package/dist/platforms/instagram.js +676 -0
- package/dist/platforms/instagram.js.map +1 -0
- package/dist/platforms/linkedin.d.ts +54 -0
- package/dist/platforms/linkedin.d.ts.map +1 -0
- package/dist/platforms/linkedin.js +580 -0
- package/dist/platforms/linkedin.js.map +1 -0
- package/dist/platforms/threads.d.ts +102 -0
- package/dist/platforms/threads.d.ts.map +1 -0
- package/dist/platforms/threads.js +571 -0
- package/dist/platforms/threads.js.map +1 -0
- package/dist/platforms/tiktok.d.ts +32 -0
- package/dist/platforms/tiktok.d.ts.map +1 -0
- package/dist/platforms/tiktok.js +511 -0
- package/dist/platforms/tiktok.js.map +1 -0
- package/dist/platforms/x-engagement.d.ts +17 -0
- package/dist/platforms/x-engagement.d.ts.map +1 -0
- package/dist/platforms/x-engagement.js +47 -0
- package/dist/platforms/x-engagement.js.map +1 -0
- package/dist/platforms/x.d.ts +82 -0
- package/dist/platforms/x.d.ts.map +1 -0
- package/dist/platforms/x.js +511 -0
- package/dist/platforms/x.js.map +1 -0
- package/dist/platforms/youtube-upload.d.ts +33 -0
- package/dist/platforms/youtube-upload.d.ts.map +1 -0
- package/dist/platforms/youtube-upload.js +281 -0
- package/dist/platforms/youtube-upload.js.map +1 -0
- package/dist/platforms/youtube.d.ts +55 -0
- package/dist/platforms/youtube.d.ts.map +1 -0
- package/dist/platforms/youtube.js +561 -0
- package/dist/platforms/youtube.js.map +1 -0
- package/dist/server/connections.d.ts +124 -0
- package/dist/server/connections.d.ts.map +1 -0
- package/dist/server/connections.js +334 -0
- package/dist/server/connections.js.map +1 -0
- package/dist/server/credentials.d.ts +52 -0
- package/dist/server/credentials.d.ts.map +1 -0
- package/dist/server/credentials.js +115 -0
- package/dist/server/credentials.js.map +1 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/oauth.d.ts +48 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +483 -0
- package/dist/server/oauth.js.map +1 -0
- package/dist/server/webhooks.d.ts +58 -0
- package/dist/server/webhooks.d.ts.map +1 -0
- package/dist/server/webhooks.js +198 -0
- package/dist/server/webhooks.js.map +1 -0
- package/dist/testing/index.d.ts +47 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +413 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport/binary.d.ts +3 -0
- package/dist/transport/binary.d.ts.map +1 -0
- package/dist/transport/binary.js +30 -0
- package/dist/transport/binary.js.map +1 -0
- package/dist/transport/budget.d.ts +4 -0
- package/dist/transport/budget.d.ts.map +1 -0
- package/dist/transport/budget.js +27 -0
- package/dist/transport/budget.js.map +1 -0
- package/dist/transport/http.d.ts +44 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +230 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/json.d.ts +3 -0
- package/dist/transport/json.d.ts.map +1 -0
- package/dist/transport/json.js +11 -0
- package/dist/transport/json.js.map +1 -0
- package/dist/transport/upload.d.ts +25 -0
- package/dist/transport/upload.d.ts.map +1 -0
- package/dist/transport/upload.js +133 -0
- package/dist/transport/upload.js.map +1 -0
- package/dist/transport/validation.d.ts +6 -0
- package/dist/transport/validation.d.ts.map +1 -0
- package/dist/transport/validation.js +24 -0
- package/dist/transport/validation.js.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { defineAdapter } from "../core/adapter.js";
|
|
2
|
+
import { SocialError } from "../core/errors.js";
|
|
3
|
+
import { managedHttp, publicFields } from "../cloud/common.js";
|
|
4
|
+
import { array, object, optionalNumber, optionalString, string } from "../transport/validation.js";
|
|
5
|
+
import { httpsUrl } from "../transport/upload.js";
|
|
6
|
+
class MemoryInstagramWorkflowStore {
|
|
7
|
+
workflows = new Map();
|
|
8
|
+
claims = new Set();
|
|
9
|
+
async create(input) {
|
|
10
|
+
const workflow = { ...input, id: `igwf_${globalThis.crypto.randomUUID()}` };
|
|
11
|
+
this.workflows.set(workflow.id, workflow);
|
|
12
|
+
return workflow;
|
|
13
|
+
}
|
|
14
|
+
async get(id) {
|
|
15
|
+
return this.workflows.get(id);
|
|
16
|
+
}
|
|
17
|
+
async update(id, update) {
|
|
18
|
+
const current = this.workflows.get(id);
|
|
19
|
+
if (!current)
|
|
20
|
+
throw new Error("Instagram workflow not found");
|
|
21
|
+
const next = { ...current, ...update, id };
|
|
22
|
+
this.workflows.set(id, next);
|
|
23
|
+
return next;
|
|
24
|
+
}
|
|
25
|
+
async claim(id) {
|
|
26
|
+
if (!this.workflows.has(id) || this.claims.has(id))
|
|
27
|
+
return false;
|
|
28
|
+
this.claims.add(id);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
async release(id) {
|
|
32
|
+
this.claims.delete(id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function instagram(options) {
|
|
36
|
+
const apiVersion = "v25.0";
|
|
37
|
+
const request = managedHttp(`https://graph.instagram.com/${apiVersion}`, {
|
|
38
|
+
apiKey: options.auth.accessToken,
|
|
39
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
40
|
+
});
|
|
41
|
+
const now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
42
|
+
const workflows = options.workflowStore ?? new MemoryInstagramWorkflowStore();
|
|
43
|
+
const authorize = (ref, context) => {
|
|
44
|
+
if (ref.backend !== context.backendInstance ||
|
|
45
|
+
ref.platform !== "instagram" ||
|
|
46
|
+
ref.accountId !== options.auth.accountId)
|
|
47
|
+
throw new SocialError({
|
|
48
|
+
code: "unauthorized",
|
|
49
|
+
operation: "instagram",
|
|
50
|
+
message: "Account reference does not match this Instagram Login authorization.",
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const status = async (id, context) => object(await request(`/${encodeURIComponent(id)}`, context, undefined, {
|
|
54
|
+
fields: "id,status_code,status",
|
|
55
|
+
}));
|
|
56
|
+
const listPosts = async (selected, input, context) => {
|
|
57
|
+
authorize(selected, context);
|
|
58
|
+
if (input.limit !== undefined &&
|
|
59
|
+
(!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100))
|
|
60
|
+
throw new SocialError({
|
|
61
|
+
code: "invalid_input",
|
|
62
|
+
operation: "posts.list",
|
|
63
|
+
message: "Instagram feed limit must be an integer from 1 through 100.",
|
|
64
|
+
});
|
|
65
|
+
const query = {
|
|
66
|
+
fields: "id,caption,media_type,media_product_type,permalink,timestamp,username",
|
|
67
|
+
};
|
|
68
|
+
if (input.cursor !== undefined)
|
|
69
|
+
query["after"] = input.cursor;
|
|
70
|
+
if (input.limit !== undefined)
|
|
71
|
+
query["limit"] = String(input.limit);
|
|
72
|
+
const result = object(await request(`/${encodeURIComponent(selected.accountId)}/media`, context, undefined, query));
|
|
73
|
+
const items = array(result["data"]).map((entry) => publicFields(entry, [
|
|
74
|
+
"id",
|
|
75
|
+
"caption",
|
|
76
|
+
"media_type",
|
|
77
|
+
"media_product_type",
|
|
78
|
+
"permalink",
|
|
79
|
+
"timestamp",
|
|
80
|
+
"username",
|
|
81
|
+
]));
|
|
82
|
+
const paging = result["paging"] === undefined ? {} : object(result["paging"]);
|
|
83
|
+
const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
|
|
84
|
+
const hasNext = typeof paging["next"] === "string" && paging["next"].length > 0;
|
|
85
|
+
const nextCursor = hasNext && typeof cursors["after"] === "string" ? cursors["after"] : undefined;
|
|
86
|
+
return {
|
|
87
|
+
items,
|
|
88
|
+
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const getAccountMetrics = async (selected, context) => {
|
|
92
|
+
authorize(selected, context);
|
|
93
|
+
const result = object(await request("/me", context, undefined, {
|
|
94
|
+
fields: "user_id,followers_count,media_count",
|
|
95
|
+
}));
|
|
96
|
+
const id = optionalString(result["user_id"]) ?? optionalString(result["id"]);
|
|
97
|
+
if (id !== selected.accountId)
|
|
98
|
+
throw new SocialError({
|
|
99
|
+
code: "unauthorized",
|
|
100
|
+
operation: "analytics.read",
|
|
101
|
+
message: "Instagram returned metrics for a different account.",
|
|
102
|
+
});
|
|
103
|
+
return ["followers_count", "media_count"].flatMap((name) => {
|
|
104
|
+
const value = optionalNumber(result[name]);
|
|
105
|
+
return value === undefined
|
|
106
|
+
? []
|
|
107
|
+
: [
|
|
108
|
+
{
|
|
109
|
+
name,
|
|
110
|
+
value,
|
|
111
|
+
unit: "count",
|
|
112
|
+
period: "lifetime",
|
|
113
|
+
fetchedAt: now(),
|
|
114
|
+
freshness: "unknown",
|
|
115
|
+
source: "Instagram Login Graph v25.0",
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
const publishContainer = async (account, containerId, context, workflowId) => {
|
|
121
|
+
authorize(account, context);
|
|
122
|
+
if (workflowId) {
|
|
123
|
+
const workflow = await workflows.get(workflowId);
|
|
124
|
+
if (workflow?.nativeId)
|
|
125
|
+
return {
|
|
126
|
+
...processing(account, workflowId, "PUBLISHED"),
|
|
127
|
+
state: "published",
|
|
128
|
+
post: {
|
|
129
|
+
kind: "platform-post",
|
|
130
|
+
version: 1,
|
|
131
|
+
backend: account.backend,
|
|
132
|
+
platform: "instagram",
|
|
133
|
+
accountId: account.accountId,
|
|
134
|
+
postId: workflow.nativeId,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const state = await status(containerId, context);
|
|
139
|
+
const code = string(state["status_code"]);
|
|
140
|
+
const base = {
|
|
141
|
+
account,
|
|
142
|
+
targetIndex: 0,
|
|
143
|
+
observedAt: now(),
|
|
144
|
+
backendState: code,
|
|
145
|
+
delivery: {
|
|
146
|
+
kind: "delivery",
|
|
147
|
+
version: 1,
|
|
148
|
+
backend: account.backend,
|
|
149
|
+
platform: "instagram",
|
|
150
|
+
accountId: account.accountId,
|
|
151
|
+
deliveryId: containerId,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
if (code === "IN_PROGRESS")
|
|
155
|
+
return { ...base, state: "processing" };
|
|
156
|
+
if (code === "ERROR" || code === "EXPIRED")
|
|
157
|
+
return {
|
|
158
|
+
...base,
|
|
159
|
+
state: "failed",
|
|
160
|
+
code: "media_error",
|
|
161
|
+
message: "Instagram container failed or expired before publication.",
|
|
162
|
+
retryDisposition: { kind: "never" },
|
|
163
|
+
};
|
|
164
|
+
if (code === "PUBLISHED")
|
|
165
|
+
return {
|
|
166
|
+
...base,
|
|
167
|
+
state: "unknown",
|
|
168
|
+
reason: "unmapped-state",
|
|
169
|
+
diagnostic: "Instagram reports a published container but the native media ID was not persisted by this workflow.",
|
|
170
|
+
};
|
|
171
|
+
if (code !== "FINISHED")
|
|
172
|
+
return {
|
|
173
|
+
...base,
|
|
174
|
+
state: "unknown",
|
|
175
|
+
reason: "unmapped-state",
|
|
176
|
+
diagnostic: "Container is already published or has an unmapped state. No repeated publish was dispatched.",
|
|
177
|
+
};
|
|
178
|
+
if (workflowId)
|
|
179
|
+
await workflows.update(workflowId, { stage: "unknown" });
|
|
180
|
+
let result;
|
|
181
|
+
try {
|
|
182
|
+
result = object(await request(`/${encodeURIComponent(account.accountId)}/media_publish`, context, {
|
|
183
|
+
creation_id: containerId,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (workflowId)
|
|
188
|
+
await workflows.update(workflowId, { stage: "unknown" });
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
const nativeId = string(result["id"]);
|
|
192
|
+
if (workflowId)
|
|
193
|
+
await workflows.update(workflowId, { nativeId, stage: "published", parentId: containerId });
|
|
194
|
+
return {
|
|
195
|
+
...base,
|
|
196
|
+
state: "published",
|
|
197
|
+
backendState: "PUBLISHED",
|
|
198
|
+
post: {
|
|
199
|
+
kind: "platform-post",
|
|
200
|
+
version: 1,
|
|
201
|
+
backend: account.backend,
|
|
202
|
+
platform: "instagram",
|
|
203
|
+
accountId: account.accountId,
|
|
204
|
+
postId: nativeId,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
const processing = (account, workflowId, backendState) => ({
|
|
209
|
+
state: "processing",
|
|
210
|
+
targetIndex: 0,
|
|
211
|
+
account,
|
|
212
|
+
observedAt: now(),
|
|
213
|
+
backendState,
|
|
214
|
+
delivery: {
|
|
215
|
+
kind: "delivery",
|
|
216
|
+
version: 1,
|
|
217
|
+
backend: account.backend,
|
|
218
|
+
platform: "instagram",
|
|
219
|
+
accountId: account.accountId,
|
|
220
|
+
deliveryId: workflowId,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const continueWorkflow = async (workflow, account, context) => {
|
|
224
|
+
if (workflow.nativeId)
|
|
225
|
+
return {
|
|
226
|
+
...processing(account, workflow.id, "PUBLISHED"),
|
|
227
|
+
state: "published",
|
|
228
|
+
post: {
|
|
229
|
+
kind: "platform-post",
|
|
230
|
+
version: 1,
|
|
231
|
+
backend: account.backend,
|
|
232
|
+
platform: "instagram",
|
|
233
|
+
accountId: account.accountId,
|
|
234
|
+
postId: workflow.nativeId,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
if (workflow.stage === "unknown")
|
|
238
|
+
return {
|
|
239
|
+
...processing(account, workflow.id, "AMBIGUOUS"),
|
|
240
|
+
state: "unknown",
|
|
241
|
+
reason: "ambiguous-submission",
|
|
242
|
+
diagnostic: "Instagram publish acceptance is unknown; no replay was attempted.",
|
|
243
|
+
};
|
|
244
|
+
if (workflow.parentId)
|
|
245
|
+
return publishContainer(account, workflow.parentId, context, workflow.id);
|
|
246
|
+
for (const child of workflow.childIds) {
|
|
247
|
+
const childStatus = await status(child, context);
|
|
248
|
+
const code = string(childStatus["status_code"]);
|
|
249
|
+
if (code !== "FINISHED")
|
|
250
|
+
return processing(account, workflow.id, code);
|
|
251
|
+
}
|
|
252
|
+
await workflows.update(workflow.id, { stage: "unknown" });
|
|
253
|
+
const parent = object(await request(`/${encodeURIComponent(account.accountId)}/media`, context, {
|
|
254
|
+
media_type: "CAROUSEL",
|
|
255
|
+
children: workflow.childIds.join(","),
|
|
256
|
+
caption: workflow.caption,
|
|
257
|
+
}));
|
|
258
|
+
const parentId = string(parent["id"]);
|
|
259
|
+
await workflows.update(workflow.id, { parentId, stage: "parent" });
|
|
260
|
+
return publishContainer(account, parentId, context, workflow.id);
|
|
261
|
+
};
|
|
262
|
+
const resumeWorkflow = async (workflow, account, context) => {
|
|
263
|
+
if (!(await workflows.claim(workflow.id)))
|
|
264
|
+
return processing(account, workflow.id, "CLAIMED");
|
|
265
|
+
try {
|
|
266
|
+
const current = (await workflows.get(workflow.id)) ?? workflow;
|
|
267
|
+
return await continueWorkflow(current, account, context);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
await workflows.release?.(workflow.id);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const readAccount = async (context) => {
|
|
274
|
+
const response = object(await request("/me", context, undefined, { fields: "user_id,username,account_type" }));
|
|
275
|
+
const id = optionalString(response["user_id"]) ?? optionalString(response["id"]);
|
|
276
|
+
if (id !== options.auth.accountId)
|
|
277
|
+
throw new SocialError({
|
|
278
|
+
code: "unauthorized",
|
|
279
|
+
operation: "accounts.read",
|
|
280
|
+
message: "Instagram returned an account different from the configured professional account.",
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
ref: {
|
|
284
|
+
kind: "connected-account",
|
|
285
|
+
version: 1,
|
|
286
|
+
backend: context.backendInstance,
|
|
287
|
+
platform: "instagram",
|
|
288
|
+
accountId: id,
|
|
289
|
+
},
|
|
290
|
+
displayName: string(response["username"]),
|
|
291
|
+
status: "connected",
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
return defineAdapter({
|
|
295
|
+
id: "instagram",
|
|
296
|
+
capabilities: {
|
|
297
|
+
schemaVersion: 1,
|
|
298
|
+
backend: "instagram",
|
|
299
|
+
apiRevision: "Instagram Login Graph v25.0",
|
|
300
|
+
runtime: ["node22", "node24", "bun"],
|
|
301
|
+
capabilities: [
|
|
302
|
+
{
|
|
303
|
+
platform: "instagram",
|
|
304
|
+
operation: "posts.publish",
|
|
305
|
+
availability: "available",
|
|
306
|
+
formats: ["image", "video", "carousel"],
|
|
307
|
+
requiredScopes: ["instagram_business_basic", "instagram_business_content_publish"],
|
|
308
|
+
notes: "Professional accounts, public HTTPS media, explicit native continuation for processing containers. Carousels must have matching aspect ratios to avoid upstream cropping.",
|
|
309
|
+
},
|
|
310
|
+
...[
|
|
311
|
+
"accounts.read",
|
|
312
|
+
"posts.list",
|
|
313
|
+
"posts.read",
|
|
314
|
+
"posts.status",
|
|
315
|
+
"comments.read",
|
|
316
|
+
"comments.write",
|
|
317
|
+
"analytics.read",
|
|
318
|
+
].map((operation) => ({
|
|
319
|
+
operation,
|
|
320
|
+
platform: "instagram",
|
|
321
|
+
availability: "available",
|
|
322
|
+
})),
|
|
323
|
+
{
|
|
324
|
+
platform: "instagram",
|
|
325
|
+
operation: "reels.publish",
|
|
326
|
+
availability: "available",
|
|
327
|
+
formats: ["video"],
|
|
328
|
+
},
|
|
329
|
+
{ platform: "instagram", operation: "stories.publish", availability: "available" },
|
|
330
|
+
{ platform: "instagram", operation: "posts.delete", availability: "available" },
|
|
331
|
+
{ platform: "instagram", operation: "hashtags.search", availability: "available" },
|
|
332
|
+
{
|
|
333
|
+
platform: "instagram",
|
|
334
|
+
operation: "publishing.limit.read",
|
|
335
|
+
availability: "available",
|
|
336
|
+
},
|
|
337
|
+
{ platform: "instagram", operation: "mentions.read", availability: "available" },
|
|
338
|
+
{
|
|
339
|
+
platform: "instagram",
|
|
340
|
+
operation: "product.tagging",
|
|
341
|
+
availability: "approval-dependent",
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
platform: "instagram",
|
|
345
|
+
operation: "messages.read",
|
|
346
|
+
availability: "approval-dependent",
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
accounts: {
|
|
351
|
+
async list(_input, context) {
|
|
352
|
+
return { items: [await readAccount(context)] };
|
|
353
|
+
},
|
|
354
|
+
async get(ref, context) {
|
|
355
|
+
authorize(ref, context);
|
|
356
|
+
return readAccount(context);
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
posts: {
|
|
360
|
+
list: listPosts,
|
|
361
|
+
prepareTarget(target) {
|
|
362
|
+
const issues = [];
|
|
363
|
+
const fail = (code, message) => issues.push({ code, message, severity: "error", targetIndex: target.targetIndex });
|
|
364
|
+
if (target.account.platform !== "instagram" ||
|
|
365
|
+
target.account.accountId !== options.auth.accountId)
|
|
366
|
+
fail("instagram.account", "Select the configured professional Instagram account.");
|
|
367
|
+
const media = target.content.media ?? [];
|
|
368
|
+
if (media.length < 1 || media.length > 10)
|
|
369
|
+
fail("instagram.media", "Instagram requires 1 to 10 image/video attachments.");
|
|
370
|
+
const firstRatio = media[0]?.width && media[0]?.height ? media[0].width / media[0].height : undefined;
|
|
371
|
+
for (const item of media) {
|
|
372
|
+
if (item.source.kind !== "https-url")
|
|
373
|
+
fail("instagram.source", "Instagram Login publishing requires public HTTPS media URLs.");
|
|
374
|
+
else
|
|
375
|
+
try {
|
|
376
|
+
httpsUrl(item.source.url);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
fail("instagram.url", "Use public HTTPS media without local hosts or credentials.");
|
|
380
|
+
}
|
|
381
|
+
if (item.kind === "image" && item.mimeType !== "image/jpeg")
|
|
382
|
+
fail("instagram.jpeg", "Instagram image publishing requires JPEG media.");
|
|
383
|
+
if (item.kind === "video" &&
|
|
384
|
+
item.mimeType !== "video/mp4" &&
|
|
385
|
+
item.mimeType !== "video/quicktime")
|
|
386
|
+
fail("instagram.video", "Provide MP4 or MOV video with a supported codec.");
|
|
387
|
+
if (media.length > 1 &&
|
|
388
|
+
(!item.width ||
|
|
389
|
+
!item.height ||
|
|
390
|
+
firstRatio === undefined ||
|
|
391
|
+
Math.abs(item.width / item.height - firstRatio) > 0.001))
|
|
392
|
+
fail("instagram.carousel_crop", "Provide matching known aspect ratios for every carousel item; the SDK will not silently accept cropping.");
|
|
393
|
+
}
|
|
394
|
+
if ((target.content.text?.length ?? 0) > 2200)
|
|
395
|
+
fail("instagram.caption", "Caption exceeds 2,200 characters.");
|
|
396
|
+
if (target.schedule || target.replyTo || target.content.link)
|
|
397
|
+
fail("instagram.operation", "Use an explicit job runner, comment reply, or caption link instead of unsupported structured options.");
|
|
398
|
+
return issues;
|
|
399
|
+
},
|
|
400
|
+
async publishTarget(target, context) {
|
|
401
|
+
authorize(target.account, context);
|
|
402
|
+
const media = target.content.media ?? [];
|
|
403
|
+
const children = [];
|
|
404
|
+
const workflow = await workflows.create({
|
|
405
|
+
backend: target.account.backend,
|
|
406
|
+
accountId: target.account.accountId,
|
|
407
|
+
childIds: [],
|
|
408
|
+
caption: target.content.text ?? "",
|
|
409
|
+
stage: "children",
|
|
410
|
+
});
|
|
411
|
+
try {
|
|
412
|
+
for (const item of media) {
|
|
413
|
+
if (item.source.kind !== "https-url")
|
|
414
|
+
throw new SocialError({
|
|
415
|
+
code: "invalid_input",
|
|
416
|
+
operation: "posts.publish",
|
|
417
|
+
message: "Public HTTPS media required.",
|
|
418
|
+
});
|
|
419
|
+
const config = target.options === undefined ? {} : object(target.options);
|
|
420
|
+
await workflows.update(workflow.id, { stage: "unknown" });
|
|
421
|
+
const created = object(await request(`/${encodeURIComponent(target.account.accountId)}/media`, context, {
|
|
422
|
+
...(item.kind === "image"
|
|
423
|
+
? {
|
|
424
|
+
image_url: item.source.url,
|
|
425
|
+
...(item.altText === undefined ? {} : { alt_text: item.altText }),
|
|
426
|
+
}
|
|
427
|
+
: {
|
|
428
|
+
video_url: item.source.url,
|
|
429
|
+
media_type: media.length > 1 ? "VIDEO" : "REELS",
|
|
430
|
+
...(typeof config["shareToFeed"] === "boolean"
|
|
431
|
+
? { share_to_feed: config["shareToFeed"] }
|
|
432
|
+
: {}),
|
|
433
|
+
}),
|
|
434
|
+
...(media.length > 1
|
|
435
|
+
? { is_carousel_item: true }
|
|
436
|
+
: { caption: target.content.text ?? "" }),
|
|
437
|
+
}));
|
|
438
|
+
children.push(string(created["id"]));
|
|
439
|
+
await workflows.update(workflow.id, { childIds: [...children], stage: "children" });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
if (error instanceof SocialError)
|
|
444
|
+
throw new SocialError({
|
|
445
|
+
code: "ambiguous_outcome",
|
|
446
|
+
operation: "instagram.posts.publish",
|
|
447
|
+
message: error.message,
|
|
448
|
+
details: {
|
|
449
|
+
createdContainerIds: children,
|
|
450
|
+
workflowId: workflow.id,
|
|
451
|
+
},
|
|
452
|
+
retryDisposition: { kind: "reconcile-first" },
|
|
453
|
+
cause: error,
|
|
454
|
+
});
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
let containerId = children[0];
|
|
458
|
+
if (!containerId)
|
|
459
|
+
throw new Error("Missing Instagram container");
|
|
460
|
+
if (children.length > 1) {
|
|
461
|
+
const result = await resumeWorkflow(workflow, target.account, context);
|
|
462
|
+
return { ...result, targetIndex: target.targetIndex };
|
|
463
|
+
}
|
|
464
|
+
await workflows.update(workflow.id, { parentId: containerId, stage: "parent" });
|
|
465
|
+
const result = await publishContainer(target.account, containerId, context, workflow.id);
|
|
466
|
+
return { ...result, targetIndex: target.targetIndex };
|
|
467
|
+
},
|
|
468
|
+
async get(ref, context) {
|
|
469
|
+
authorize(ref, context);
|
|
470
|
+
return publicFields(await request(`/${encodeURIComponent(ref.postId)}`, context, undefined, {
|
|
471
|
+
fields: "id,caption,media_type,media_product_type,permalink,timestamp,username",
|
|
472
|
+
}), [
|
|
473
|
+
"id",
|
|
474
|
+
"caption",
|
|
475
|
+
"media_type",
|
|
476
|
+
"media_product_type",
|
|
477
|
+
"permalink",
|
|
478
|
+
"timestamp",
|
|
479
|
+
"username",
|
|
480
|
+
]);
|
|
481
|
+
},
|
|
482
|
+
async getDelivery(ref, context) {
|
|
483
|
+
authorize(ref, context);
|
|
484
|
+
const workflow = await workflows.get(ref.deliveryId);
|
|
485
|
+
if (workflow) {
|
|
486
|
+
if (workflow.backend !== ref.backend || workflow.accountId !== ref.accountId)
|
|
487
|
+
throw new SocialError({
|
|
488
|
+
code: "unauthorized",
|
|
489
|
+
operation: "instagram.posts.status",
|
|
490
|
+
message: "Instagram workflow handle is not authorized for this account.",
|
|
491
|
+
});
|
|
492
|
+
const account = {
|
|
493
|
+
kind: "connected-account",
|
|
494
|
+
version: 1,
|
|
495
|
+
backend: ref.backend,
|
|
496
|
+
platform: "instagram",
|
|
497
|
+
accountId: ref.accountId,
|
|
498
|
+
};
|
|
499
|
+
if (workflow.nativeId)
|
|
500
|
+
return {
|
|
501
|
+
...processing(account, workflow.id, "PUBLISHED"),
|
|
502
|
+
state: "published",
|
|
503
|
+
post: {
|
|
504
|
+
kind: "platform-post",
|
|
505
|
+
version: 1,
|
|
506
|
+
backend: ref.backend,
|
|
507
|
+
platform: "instagram",
|
|
508
|
+
accountId: ref.accountId,
|
|
509
|
+
postId: workflow.nativeId,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
if (workflow.stage === "unknown")
|
|
513
|
+
return {
|
|
514
|
+
...processing(account, workflow.id, "AMBIGUOUS"),
|
|
515
|
+
state: "unknown",
|
|
516
|
+
reason: "ambiguous-submission",
|
|
517
|
+
diagnostic: "Explicit resumePublication is required.",
|
|
518
|
+
};
|
|
519
|
+
return processing(account, workflow.id, workflow.stage.toUpperCase());
|
|
520
|
+
}
|
|
521
|
+
const result = await status(ref.deliveryId, context);
|
|
522
|
+
const code = string(result["status_code"]);
|
|
523
|
+
const base = {
|
|
524
|
+
account: {
|
|
525
|
+
kind: "connected-account",
|
|
526
|
+
version: 1,
|
|
527
|
+
backend: ref.backend,
|
|
528
|
+
platform: "instagram",
|
|
529
|
+
accountId: ref.accountId,
|
|
530
|
+
},
|
|
531
|
+
targetIndex: 0,
|
|
532
|
+
observedAt: now(),
|
|
533
|
+
backendState: code,
|
|
534
|
+
delivery: { kind: "delivery", version: 1, ...ref },
|
|
535
|
+
};
|
|
536
|
+
if (code === "IN_PROGRESS" || code === "FINISHED")
|
|
537
|
+
return { ...base, state: "processing" };
|
|
538
|
+
if (code === "ERROR" || code === "EXPIRED")
|
|
539
|
+
return {
|
|
540
|
+
...base,
|
|
541
|
+
state: "failed",
|
|
542
|
+
code: "media_error",
|
|
543
|
+
message: "Instagram container failed or expired.",
|
|
544
|
+
retryDisposition: { kind: "never" },
|
|
545
|
+
};
|
|
546
|
+
return {
|
|
547
|
+
...base,
|
|
548
|
+
state: "unknown",
|
|
549
|
+
reason: "unmapped-state",
|
|
550
|
+
diagnostic: "Container status alone does not identify a published native post. Reconcile stored publish results.",
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
comments: {
|
|
555
|
+
async list(ref, input, context) {
|
|
556
|
+
if (input.cursor !== undefined)
|
|
557
|
+
throw new SocialError({
|
|
558
|
+
code: "unsupported_capability",
|
|
559
|
+
operation: "comments.read",
|
|
560
|
+
message: "This adapter does not expose upstream comment pagination.",
|
|
561
|
+
});
|
|
562
|
+
authorize(ref, context);
|
|
563
|
+
const result = object(await request(`/${encodeURIComponent(ref.postId)}/comments`, context, undefined, {
|
|
564
|
+
fields: "id,text,timestamp,username",
|
|
565
|
+
limit: "25",
|
|
566
|
+
}));
|
|
567
|
+
return {
|
|
568
|
+
items: array(result["data"]).map((entry) => publicFields(entry, ["id", "text", "timestamp", "username"])),
|
|
569
|
+
};
|
|
570
|
+
},
|
|
571
|
+
async reply(ref, content, context) {
|
|
572
|
+
authorize(ref, context);
|
|
573
|
+
const result = object(await request(`/${encodeURIComponent(ref.commentId)}/replies`, context, {
|
|
574
|
+
message: content.text,
|
|
575
|
+
}));
|
|
576
|
+
return { ...ref, commentId: string(result["id"]) };
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
analytics: {
|
|
580
|
+
getAccountMetrics,
|
|
581
|
+
async getPostMetrics(ref, context) {
|
|
582
|
+
authorize(ref, context);
|
|
583
|
+
const result = object(await request(`/${encodeURIComponent(ref.postId)}/insights`, context, undefined, {
|
|
584
|
+
metric: "likes,comments,saved,shares,reach",
|
|
585
|
+
}));
|
|
586
|
+
return array(result["data"]).flatMap((entry) => {
|
|
587
|
+
const row = object(entry);
|
|
588
|
+
const value = array(row["values"])[0];
|
|
589
|
+
if (!value)
|
|
590
|
+
return [];
|
|
591
|
+
const count = optionalNumber(object(value)["value"]);
|
|
592
|
+
return count === undefined
|
|
593
|
+
? []
|
|
594
|
+
: [
|
|
595
|
+
{
|
|
596
|
+
name: string(row["name"]),
|
|
597
|
+
value: count,
|
|
598
|
+
unit: "count",
|
|
599
|
+
period: "lifetime",
|
|
600
|
+
fetchedAt: now(),
|
|
601
|
+
freshness: "unknown",
|
|
602
|
+
source: `instagram:${apiVersion}`,
|
|
603
|
+
},
|
|
604
|
+
];
|
|
605
|
+
});
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
native: {
|
|
609
|
+
publishContainer,
|
|
610
|
+
async publishCarousel(account, children, caption, context) {
|
|
611
|
+
authorize(account, context);
|
|
612
|
+
const workflow = await workflows.create({
|
|
613
|
+
backend: account.backend,
|
|
614
|
+
accountId: account.accountId,
|
|
615
|
+
childIds: [...children],
|
|
616
|
+
caption,
|
|
617
|
+
stage: "children",
|
|
618
|
+
});
|
|
619
|
+
return resumeWorkflow(workflow, account, context);
|
|
620
|
+
},
|
|
621
|
+
async resumePublication(account, workflowId, context) {
|
|
622
|
+
authorize(account, context);
|
|
623
|
+
const workflow = await workflows.get(workflowId);
|
|
624
|
+
if (!workflow ||
|
|
625
|
+
workflow.backend !== context.backendInstance ||
|
|
626
|
+
workflow.accountId !== account.accountId)
|
|
627
|
+
throw new SocialError({
|
|
628
|
+
code: "unauthorized",
|
|
629
|
+
operation: "instagram.posts.resume",
|
|
630
|
+
message: "Instagram workflow handle is not authorized for this account.",
|
|
631
|
+
});
|
|
632
|
+
return resumeWorkflow(workflow, account, context);
|
|
633
|
+
},
|
|
634
|
+
async publishReel({ account, videoUrl, caption, context }) {
|
|
635
|
+
authorize(account, context);
|
|
636
|
+
const created = object(await request(`/${encodeURIComponent(account.accountId)}/media`, context, {
|
|
637
|
+
media_type: "REELS",
|
|
638
|
+
video_url: videoUrl,
|
|
639
|
+
...(caption ? { caption } : {}),
|
|
640
|
+
}));
|
|
641
|
+
return publishContainer(account, string(created["id"]), context);
|
|
642
|
+
},
|
|
643
|
+
async publishStory({ account, mediaUrl, context }) {
|
|
644
|
+
authorize(account, context);
|
|
645
|
+
const created = object(await request(`/${encodeURIComponent(account.accountId)}/media`, context, {
|
|
646
|
+
media_type: "STORIES",
|
|
647
|
+
image_url: mediaUrl,
|
|
648
|
+
}));
|
|
649
|
+
return publishContainer(account, string(created["id"]), context);
|
|
650
|
+
},
|
|
651
|
+
async deletePost({ account, postId, context }) {
|
|
652
|
+
authorize(account, context);
|
|
653
|
+
await request(`/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
|
|
654
|
+
},
|
|
655
|
+
async hashtagSearch({ account, hashtag, context }) {
|
|
656
|
+
authorize(account, context);
|
|
657
|
+
const tag = object(await request("/ig_hashtag_search", context, undefined, {
|
|
658
|
+
user_id: account.accountId,
|
|
659
|
+
q: hashtag.replace(/^#/, ""),
|
|
660
|
+
}));
|
|
661
|
+
return tag;
|
|
662
|
+
},
|
|
663
|
+
async publishingLimit({ account, context }) {
|
|
664
|
+
authorize(account, context);
|
|
665
|
+
return object(await request(`/${encodeURIComponent(account.accountId)}/content_publishing_limit`, context, undefined, { fields: "config,quota_usage" }));
|
|
666
|
+
},
|
|
667
|
+
async mentions({ account, context }) {
|
|
668
|
+
authorize(account, context);
|
|
669
|
+
return object(await request(`/${encodeURIComponent(account.accountId)}/tags`, context, undefined, {
|
|
670
|
+
fields: "id,caption,media_type,timestamp",
|
|
671
|
+
}));
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=instagram.js.map
|