@opencoredev/social-sdk 0.1.1 → 0.2.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/README.md +10 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud/common.d.ts.map +1 -1
- package/dist/cloud/common.js +48 -20
- package/dist/cloud/common.js.map +1 -1
- package/dist/cloud/lifecycle.d.ts.map +1 -1
- package/dist/cloud/lifecycle.js +5 -0
- package/dist/cloud/lifecycle.js.map +1 -1
- package/dist/cloud/media.d.ts.map +1 -1
- package/dist/cloud/media.js +12 -8
- package/dist/cloud/media.js.map +1 -1
- package/dist/cloud/outcomes.d.ts.map +1 -1
- package/dist/cloud/outcomes.js +17 -9
- package/dist/cloud/outcomes.js.map +1 -1
- package/dist/cloud/post-for-me.d.ts.map +1 -1
- package/dist/cloud/post-for-me.js +99 -28
- package/dist/cloud/post-for-me.js.map +1 -1
- package/dist/cloud/zernio.d.ts.map +1 -1
- package/dist/cloud/zernio.js +100 -9
- package/dist/cloud/zernio.js.map +1 -1
- package/dist/core/adapter.d.ts +42 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/client.d.ts +35 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +503 -110
- package/dist/core/client.js.map +1 -1
- package/dist/core/concurrency.d.ts.map +1 -1
- package/dist/core/concurrency.js +5 -3
- package/dist/core/concurrency.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +14 -7
- package/dist/core/errors.js.map +1 -1
- package/dist/core/idempotency.d.ts.map +1 -1
- package/dist/core/idempotency.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +1 -0
- package/dist/core/pagination.js.map +1 -1
- package/dist/core/types.d.ts +70 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -1
- package/dist/platforms/bluesky.d.ts +131 -1
- package/dist/platforms/bluesky.d.ts.map +1 -1
- package/dist/platforms/bluesky.js +772 -44
- package/dist/platforms/bluesky.js.map +1 -1
- package/dist/platforms/instagram.d.ts +70 -3
- package/dist/platforms/instagram.d.ts.map +1 -1
- package/dist/platforms/instagram.js +403 -47
- package/dist/platforms/instagram.js.map +1 -1
- package/dist/platforms/linkedin.d.ts +55 -0
- package/dist/platforms/linkedin.d.ts.map +1 -1
- package/dist/platforms/linkedin.js +350 -39
- package/dist/platforms/linkedin.js.map +1 -1
- package/dist/platforms/threads.d.ts +37 -4
- package/dist/platforms/threads.d.ts.map +1 -1
- package/dist/platforms/threads.js +419 -44
- package/dist/platforms/threads.js.map +1 -1
- package/dist/platforms/tiktok.d.ts.map +1 -1
- package/dist/platforms/tiktok.js +109 -26
- package/dist/platforms/tiktok.js.map +1 -1
- package/dist/platforms/x-engagement.d.ts.map +1 -1
- package/dist/platforms/x-engagement.js +4 -0
- package/dist/platforms/x-engagement.js.map +1 -1
- package/dist/platforms/x.d.ts +227 -9
- package/dist/platforms/x.d.ts.map +1 -1
- package/dist/platforms/x.js +826 -49
- package/dist/platforms/x.js.map +1 -1
- package/dist/platforms/youtube-upload.d.ts.map +1 -1
- package/dist/platforms/youtube-upload.js +25 -9
- package/dist/platforms/youtube-upload.js.map +1 -1
- package/dist/platforms/youtube.d.ts +56 -2
- package/dist/platforms/youtube.d.ts.map +1 -1
- package/dist/platforms/youtube.js +571 -31
- package/dist/platforms/youtube.js.map +1 -1
- package/dist/server/connections.d.ts.map +1 -1
- package/dist/server/connections.js +4 -2
- package/dist/server/connections.js.map +1 -1
- package/dist/server/credentials.d.ts.map +1 -1
- package/dist/server/credentials.js +10 -16
- package/dist/server/credentials.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth.d.ts +5 -0
- package/dist/server/oauth.d.ts.map +1 -1
- package/dist/server/oauth.js +118 -47
- package/dist/server/oauth.js.map +1 -1
- package/dist/server/webhooks.d.ts.map +1 -1
- package/dist/server/webhooks.js +11 -4
- package/dist/server/webhooks.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +153 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/transport/binary.d.ts.map +1 -1
- package/dist/transport/binary.js.map +1 -1
- package/dist/transport/budget.d.ts.map +1 -1
- package/dist/transport/budget.js.map +1 -1
- package/dist/transport/http.d.ts +2 -1
- package/dist/transport/http.d.ts.map +1 -1
- package/dist/transport/http.js +42 -12
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/json.d.ts +7 -1
- package/dist/transport/json.d.ts.map +1 -1
- package/dist/transport/json.js +7 -1
- package/dist/transport/json.js.map +1 -1
- package/dist/transport/upload.d.ts +1 -0
- package/dist/transport/upload.d.ts.map +1 -1
- package/dist/transport/upload.js +38 -17
- package/dist/transport/upload.js.map +1 -1
- package/dist/transport/validation.d.ts.map +1 -1
- package/dist/transport/validation.js +1 -0
- package/dist/transport/validation.js.map +1 -1
- package/package.json +2 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
/* oxlint-disable anti-slop/no-conditional-empty-object-spread, anti-slop/no-known-value-widening, anti-slop/no-runtime-typeof, anti-slop/no-unsafe-dictionary-type, anti-slop/require-readable-spacing -- provider query and response boundaries are validated locally. */
|
|
1
2
|
import { defineAdapter } from "../core/adapter.js";
|
|
2
3
|
import { SocialError } from "../core/errors.js";
|
|
4
|
+
import { profileRef } from "../core/types.js";
|
|
3
5
|
import { managedHttp, publicFields } from "../cloud/common.js";
|
|
6
|
+
import { HttpError } from "../transport/http.js";
|
|
4
7
|
import { array, object, optionalNumber, optionalString, string } from "../transport/validation.js";
|
|
5
8
|
import { httpsUrl } from "../transport/upload.js";
|
|
6
9
|
class MemoryInstagramWorkflowStore {
|
|
@@ -34,12 +37,64 @@ class MemoryInstagramWorkflowStore {
|
|
|
34
37
|
}
|
|
35
38
|
export function instagram(options) {
|
|
36
39
|
const apiVersion = "v25.0";
|
|
37
|
-
const
|
|
40
|
+
const flavor = options.auth.flavor ?? "instagram-login";
|
|
41
|
+
const origin = flavor === "facebook-login"
|
|
42
|
+
? `https://graph.facebook.com/${apiVersion}`
|
|
43
|
+
: `https://graph.instagram.com/${apiVersion}`;
|
|
44
|
+
const request = managedHttp(origin, {
|
|
38
45
|
apiKey: options.auth.accessToken,
|
|
46
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
39
47
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
40
48
|
});
|
|
41
49
|
const now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
42
50
|
const workflows = options.workflowStore ?? new MemoryInstagramWorkflowStore();
|
|
51
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated here.
|
|
52
|
+
const validatedObject = (value, operation) => {
|
|
53
|
+
try {
|
|
54
|
+
return object(value);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof HttpError && error.kind === "invalid-response")
|
|
58
|
+
throw new SocialError({
|
|
59
|
+
code: "upstream_failure",
|
|
60
|
+
operation,
|
|
61
|
+
message: error.message,
|
|
62
|
+
cause: error,
|
|
63
|
+
});
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const requireFacebookLogin = (operation) => {
|
|
68
|
+
if (flavor !== "facebook-login")
|
|
69
|
+
throw new SocialError({
|
|
70
|
+
code: "unsupported_capability",
|
|
71
|
+
operation,
|
|
72
|
+
message: `${operation} requires a Facebook Login Graph API access token for an Instagram professional account.`,
|
|
73
|
+
retryDisposition: { kind: "never" },
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const pageLimit = (limit, max = 50) => {
|
|
77
|
+
const value = limit ?? 25;
|
|
78
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
79
|
+
throw new SocialError({
|
|
80
|
+
code: "invalid_input",
|
|
81
|
+
operation: "instagram.pagination",
|
|
82
|
+
message: `Instagram page limit must be an integer from 1 through ${max}.`,
|
|
83
|
+
});
|
|
84
|
+
return value;
|
|
85
|
+
};
|
|
86
|
+
const page = (result, fields) => {
|
|
87
|
+
const items = array(result["data"]).map((entry) => publicFields(entry, fields));
|
|
88
|
+
const paging = result["paging"] === undefined ? {} : object(result["paging"]);
|
|
89
|
+
const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
|
|
90
|
+
// Graph API omits `paging.next` on the last page even when `cursors.after` is present.
|
|
91
|
+
const nextCursor = typeof paging["next"] === "string" &&
|
|
92
|
+
typeof cursors["after"] === "string" &&
|
|
93
|
+
cursors["after"].length > 0
|
|
94
|
+
? cursors["after"]
|
|
95
|
+
: undefined;
|
|
96
|
+
return { items, ...(nextCursor === undefined ? {} : { nextCursor }) };
|
|
97
|
+
};
|
|
43
98
|
const authorize = (ref, context) => {
|
|
44
99
|
if (ref.backend !== context.backendInstance ||
|
|
45
100
|
ref.platform !== "instagram" ||
|
|
@@ -47,7 +102,7 @@ export function instagram(options) {
|
|
|
47
102
|
throw new SocialError({
|
|
48
103
|
code: "unauthorized",
|
|
49
104
|
operation: "instagram",
|
|
50
|
-
message: "Account reference does not match this Instagram
|
|
105
|
+
message: "Account reference does not match this Instagram authorization.",
|
|
51
106
|
});
|
|
52
107
|
};
|
|
53
108
|
const status = async (id, context) => object(await request(`/${encodeURIComponent(id)}`, context, undefined, {
|
|
@@ -62,6 +117,7 @@ export function instagram(options) {
|
|
|
62
117
|
operation: "posts.list",
|
|
63
118
|
message: "Instagram feed limit must be an integer from 1 through 100.",
|
|
64
119
|
});
|
|
120
|
+
// oxlint-disable-next-line anti-slop/no-known-value-widening -- validated boundary or fixture contract.
|
|
65
121
|
const query = {
|
|
66
122
|
fields: "id,caption,media_type,media_product_type,permalink,timestamp,username",
|
|
67
123
|
};
|
|
@@ -81,17 +137,23 @@ export function instagram(options) {
|
|
|
81
137
|
]));
|
|
82
138
|
const paging = result["paging"] === undefined ? {} : object(result["paging"]);
|
|
83
139
|
const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
|
|
140
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
84
141
|
const hasNext = typeof paging["next"] === "string" && paging["next"].length > 0;
|
|
85
|
-
const nextCursor =
|
|
142
|
+
const nextCursor =
|
|
143
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
144
|
+
hasNext && typeof cursors["after"] === "string" ? cursors["after"] : undefined;
|
|
86
145
|
return {
|
|
87
146
|
items,
|
|
147
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
88
148
|
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
89
149
|
};
|
|
90
150
|
};
|
|
91
151
|
const getAccountMetrics = async (selected, context) => {
|
|
92
152
|
authorize(selected, context);
|
|
93
|
-
const result = object(await request("/me", context, undefined, {
|
|
94
|
-
fields: "
|
|
153
|
+
const result = object(await request(flavor === "facebook-login" ? `/${encodeURIComponent(selected.accountId)}` : "/me", context, undefined, {
|
|
154
|
+
fields: flavor === "facebook-login"
|
|
155
|
+
? "id,followers_count,media_count"
|
|
156
|
+
: "user_id,followers_count,media_count",
|
|
95
157
|
}));
|
|
96
158
|
const id = optionalString(result["user_id"]) ?? optionalString(result["id"]);
|
|
97
159
|
if (id !== selected.accountId)
|
|
@@ -112,7 +174,7 @@ export function instagram(options) {
|
|
|
112
174
|
period: "lifetime",
|
|
113
175
|
fetchedAt: now(),
|
|
114
176
|
freshness: "unknown",
|
|
115
|
-
source: "Instagram Login Graph v25.0
|
|
177
|
+
source: `${flavor === "facebook-login" ? "Facebook Login" : "Instagram Login"} Graph v25.0`,
|
|
116
178
|
},
|
|
117
179
|
];
|
|
118
180
|
});
|
|
@@ -148,7 +210,7 @@ export function instagram(options) {
|
|
|
148
210
|
backend: account.backend,
|
|
149
211
|
platform: "instagram",
|
|
150
212
|
accountId: account.accountId,
|
|
151
|
-
deliveryId: containerId,
|
|
213
|
+
deliveryId: workflowId ?? containerId,
|
|
152
214
|
},
|
|
153
215
|
};
|
|
154
216
|
if (code === "IN_PROGRESS")
|
|
@@ -179,6 +241,7 @@ export function instagram(options) {
|
|
|
179
241
|
await workflows.update(workflowId, { stage: "unknown" });
|
|
180
242
|
let result;
|
|
181
243
|
try {
|
|
244
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
182
245
|
result = object(await request(`/${encodeURIComponent(account.accountId)}/media_publish`, context, {
|
|
183
246
|
creation_id: containerId,
|
|
184
247
|
}));
|
|
@@ -271,7 +334,11 @@ export function instagram(options) {
|
|
|
271
334
|
}
|
|
272
335
|
};
|
|
273
336
|
const readAccount = async (context) => {
|
|
274
|
-
const response = object(await request("/me", context, undefined, {
|
|
337
|
+
const response = object(await request(flavor === "facebook-login" ? `/${encodeURIComponent(options.auth.accountId)}` : "/me", context, undefined, {
|
|
338
|
+
fields: flavor === "facebook-login"
|
|
339
|
+
? "id,username,account_type"
|
|
340
|
+
: "user_id,username,account_type",
|
|
341
|
+
}));
|
|
275
342
|
const id = optionalString(response["user_id"]) ?? optionalString(response["id"]);
|
|
276
343
|
if (id !== options.auth.accountId)
|
|
277
344
|
throw new SocialError({
|
|
@@ -291,12 +358,43 @@ export function instagram(options) {
|
|
|
291
358
|
status: "connected",
|
|
292
359
|
};
|
|
293
360
|
};
|
|
361
|
+
const listMentions = async ({ account, cursor, limit, context, }) => {
|
|
362
|
+
authorize(account, context);
|
|
363
|
+
requireFacebookLogin("instagram.mentions.read");
|
|
364
|
+
const query = {
|
|
365
|
+
fields: "id,caption,media_type,media_product_type,permalink,timestamp,username",
|
|
366
|
+
limit: String(pageLimit(limit)),
|
|
367
|
+
};
|
|
368
|
+
if (cursor !== undefined)
|
|
369
|
+
query["after"] = cursor;
|
|
370
|
+
const result = validatedObject(await request(`/${encodeURIComponent(account.accountId)}/tags`, context, undefined, query), "instagram.mentions.read");
|
|
371
|
+
return page(result, [
|
|
372
|
+
"id",
|
|
373
|
+
"caption",
|
|
374
|
+
"media_type",
|
|
375
|
+
"media_product_type",
|
|
376
|
+
"permalink",
|
|
377
|
+
"timestamp",
|
|
378
|
+
"username",
|
|
379
|
+
]);
|
|
380
|
+
};
|
|
381
|
+
const profileFields = [
|
|
382
|
+
"id",
|
|
383
|
+
"username",
|
|
384
|
+
"name",
|
|
385
|
+
"biography",
|
|
386
|
+
"profile_picture_url",
|
|
387
|
+
"followers_count",
|
|
388
|
+
"follows_count",
|
|
389
|
+
"media_count",
|
|
390
|
+
"website",
|
|
391
|
+
];
|
|
294
392
|
return defineAdapter({
|
|
295
393
|
id: "instagram",
|
|
296
394
|
capabilities: {
|
|
297
395
|
schemaVersion: 1,
|
|
298
396
|
backend: "instagram",
|
|
299
|
-
apiRevision: "Instagram Login Graph v25.0
|
|
397
|
+
apiRevision: `${flavor === "facebook-login" ? "Facebook Login" : "Instagram Login"} Graph v25.0`,
|
|
300
398
|
runtime: ["node22", "node24", "bun"],
|
|
301
399
|
capabilities: [
|
|
302
400
|
{
|
|
@@ -304,7 +402,9 @@ export function instagram(options) {
|
|
|
304
402
|
operation: "posts.publish",
|
|
305
403
|
availability: "available",
|
|
306
404
|
formats: ["image", "video", "carousel"],
|
|
307
|
-
requiredScopes:
|
|
405
|
+
requiredScopes: flavor === "facebook-login"
|
|
406
|
+
? ["instagram_basic", "instagram_content_publish", "pages_read_engagement"]
|
|
407
|
+
: ["instagram_business_basic", "instagram_business_content_publish"],
|
|
308
408
|
notes: "Professional accounts, public HTTPS media, explicit native continuation for processing containers. Carousels must have matching aspect ratios to avoid upstream cropping.",
|
|
309
409
|
},
|
|
310
410
|
...[
|
|
@@ -315,11 +415,45 @@ export function instagram(options) {
|
|
|
315
415
|
"comments.read",
|
|
316
416
|
"comments.write",
|
|
317
417
|
"analytics.read",
|
|
418
|
+
"analytics.account.read",
|
|
318
419
|
].map((operation) => ({
|
|
319
420
|
operation,
|
|
320
421
|
platform: "instagram",
|
|
321
422
|
availability: "available",
|
|
322
423
|
})),
|
|
424
|
+
{
|
|
425
|
+
platform: "instagram",
|
|
426
|
+
operation: "comments.moderate",
|
|
427
|
+
availability: "available",
|
|
428
|
+
requiredScopes: flavor === "facebook-login"
|
|
429
|
+
? ["instagram_manage_comments", "pages_read_engagement"]
|
|
430
|
+
: ["instagram_business_manage_comments"],
|
|
431
|
+
notes: "Hide or unhide a comment, or enable or disable comments on media. Instagram Login and Facebook Login are supported where the account and app permissions qualify.",
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
platform: "instagram",
|
|
435
|
+
operation: "comments.delete",
|
|
436
|
+
availability: "available",
|
|
437
|
+
requiredScopes: flavor === "facebook-login"
|
|
438
|
+
? ["instagram_manage_comments", "pages_read_engagement"]
|
|
439
|
+
: ["instagram_business_manage_comments"],
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
platform: "instagram",
|
|
443
|
+
operation: "comments.replies.read",
|
|
444
|
+
availability: "available",
|
|
445
|
+
requiredScopes: flavor === "facebook-login"
|
|
446
|
+
? ["instagram_manage_comments", "pages_read_engagement"]
|
|
447
|
+
: ["instagram_business_manage_comments"],
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
platform: "instagram",
|
|
451
|
+
operation: "profiles.read",
|
|
452
|
+
availability: "available",
|
|
453
|
+
requiredScopes: flavor === "facebook-login"
|
|
454
|
+
? ["instagram_basic", "instagram_manage_insights", "pages_read_engagement"]
|
|
455
|
+
: ["instagram_business_basic", "instagram_business_manage_insights"],
|
|
456
|
+
},
|
|
323
457
|
{
|
|
324
458
|
platform: "instagram",
|
|
325
459
|
operation: "reels.publish",
|
|
@@ -327,14 +461,60 @@ export function instagram(options) {
|
|
|
327
461
|
formats: ["video"],
|
|
328
462
|
},
|
|
329
463
|
{ platform: "instagram", operation: "stories.publish", availability: "available" },
|
|
330
|
-
{
|
|
331
|
-
|
|
464
|
+
{
|
|
465
|
+
platform: "instagram",
|
|
466
|
+
operation: "posts.delete",
|
|
467
|
+
availability: flavor === "facebook-login"
|
|
468
|
+
? "available"
|
|
469
|
+
: "not-implemented-by-adapter",
|
|
470
|
+
...(flavor === "facebook-login"
|
|
471
|
+
? { requiredScopes: ["instagram_basic", "pages_read_engagement"] }
|
|
472
|
+
: {}),
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
platform: "instagram",
|
|
476
|
+
operation: "posts.removeFromPlatform",
|
|
477
|
+
availability: flavor === "facebook-login"
|
|
478
|
+
? "available"
|
|
479
|
+
: "not-implemented-by-adapter",
|
|
480
|
+
},
|
|
481
|
+
...(flavor === "facebook-login"
|
|
482
|
+
? [
|
|
483
|
+
{
|
|
484
|
+
platform: "instagram",
|
|
485
|
+
operation: "hashtags.search",
|
|
486
|
+
availability: "available",
|
|
487
|
+
requiredScopes: ["instagram_basic"],
|
|
488
|
+
notes: "Requires Facebook Login, Instagram Public Content Access feature approval, and a qualifying Instagram professional account.",
|
|
489
|
+
},
|
|
490
|
+
]
|
|
491
|
+
: []),
|
|
332
492
|
{
|
|
333
493
|
platform: "instagram",
|
|
334
494
|
operation: "publishing.limit.read",
|
|
335
495
|
availability: "available",
|
|
336
496
|
},
|
|
337
|
-
|
|
497
|
+
...(flavor === "facebook-login"
|
|
498
|
+
? [
|
|
499
|
+
{
|
|
500
|
+
platform: "instagram",
|
|
501
|
+
operation: "mentions.read",
|
|
502
|
+
availability: "available",
|
|
503
|
+
requiredScopes: [
|
|
504
|
+
"instagram_basic",
|
|
505
|
+
"instagram_manage_comments",
|
|
506
|
+
"pages_read_engagement",
|
|
507
|
+
],
|
|
508
|
+
notes: "Reads the paginated /{ig-user-id}/tags edge.",
|
|
509
|
+
},
|
|
510
|
+
]
|
|
511
|
+
: [
|
|
512
|
+
{
|
|
513
|
+
platform: "instagram",
|
|
514
|
+
operation: "mentions.read",
|
|
515
|
+
availability: "not-implemented-by-adapter",
|
|
516
|
+
},
|
|
517
|
+
]),
|
|
338
518
|
{
|
|
339
519
|
platform: "instagram",
|
|
340
520
|
operation: "product.tagging",
|
|
@@ -422,11 +602,20 @@ export function instagram(options) {
|
|
|
422
602
|
...(item.kind === "image"
|
|
423
603
|
? {
|
|
424
604
|
image_url: item.source.url,
|
|
605
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
425
606
|
...(item.altText === undefined ? {} : { alt_text: item.altText }),
|
|
426
607
|
}
|
|
427
608
|
: {
|
|
428
609
|
video_url: item.source.url,
|
|
429
610
|
media_type: media.length > 1 ? "VIDEO" : "REELS",
|
|
611
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
612
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
613
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
614
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
615
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
616
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
617
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
618
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
430
619
|
...(typeof config["shareToFeed"] === "boolean"
|
|
431
620
|
? { share_to_feed: config["shareToFeed"] }
|
|
432
621
|
: {}),
|
|
@@ -509,14 +698,7 @@ export function instagram(options) {
|
|
|
509
698
|
postId: workflow.nativeId,
|
|
510
699
|
},
|
|
511
700
|
};
|
|
512
|
-
|
|
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());
|
|
701
|
+
return resumeWorkflow(workflow, account, context);
|
|
520
702
|
}
|
|
521
703
|
const result = await status(ref.deliveryId, context);
|
|
522
704
|
const code = string(result["status_code"]);
|
|
@@ -533,7 +715,12 @@ export function instagram(options) {
|
|
|
533
715
|
backendState: code,
|
|
534
716
|
delivery: { kind: "delivery", version: 1, ...ref },
|
|
535
717
|
};
|
|
536
|
-
if (code === "
|
|
718
|
+
if (code === "FINISHED") {
|
|
719
|
+
const workflow = await workflows.get(ref.deliveryId);
|
|
720
|
+
if (workflow)
|
|
721
|
+
return resumeWorkflow(workflow, base.account, context);
|
|
722
|
+
}
|
|
723
|
+
if (code === "IN_PROGRESS")
|
|
537
724
|
return { ...base, state: "processing" };
|
|
538
725
|
if (code === "ERROR" || code === "EXPIRED")
|
|
539
726
|
return {
|
|
@@ -550,23 +737,23 @@ export function instagram(options) {
|
|
|
550
737
|
diagnostic: "Container status alone does not identify a published native post. Reconcile stored publish results.",
|
|
551
738
|
};
|
|
552
739
|
},
|
|
740
|
+
async removeFromPlatform(ref, context) {
|
|
741
|
+
authorize(ref, context);
|
|
742
|
+
requireFacebookLogin("instagram.posts.removeFromPlatform");
|
|
743
|
+
await request(`/${encodeURIComponent(ref.postId)}`, context, undefined, {}, "DELETE");
|
|
744
|
+
},
|
|
553
745
|
},
|
|
554
746
|
comments: {
|
|
555
747
|
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
748
|
authorize(ref, context);
|
|
563
|
-
const
|
|
749
|
+
const query = {
|
|
564
750
|
fields: "id,text,timestamp,username",
|
|
565
|
-
limit:
|
|
566
|
-
}));
|
|
567
|
-
return {
|
|
568
|
-
items: array(result["data"]).map((entry) => publicFields(entry, ["id", "text", "timestamp", "username"])),
|
|
751
|
+
limit: String(pageLimit(input.limit)),
|
|
569
752
|
};
|
|
753
|
+
if (input.cursor !== undefined)
|
|
754
|
+
query["after"] = input.cursor;
|
|
755
|
+
const result = validatedObject(await request(`/${encodeURIComponent(ref.postId)}/comments`, context, undefined, query), "instagram.comments.read");
|
|
756
|
+
return page(result, ["id", "text", "timestamp", "username"]);
|
|
570
757
|
},
|
|
571
758
|
async reply(ref, content, context) {
|
|
572
759
|
authorize(ref, context);
|
|
@@ -580,9 +767,14 @@ export function instagram(options) {
|
|
|
580
767
|
getAccountMetrics,
|
|
581
768
|
async getPostMetrics(ref, context) {
|
|
582
769
|
authorize(ref, context);
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
}));
|
|
770
|
+
const media = validatedObject(await request(`/${encodeURIComponent(ref.postId)}`, context, undefined, {
|
|
771
|
+
fields: "media_product_type",
|
|
772
|
+
}), "instagram.analytics.read");
|
|
773
|
+
const result = validatedObject(await request(`/${encodeURIComponent(ref.postId)}/insights`, context, undefined, {
|
|
774
|
+
metric: media["media_product_type"] === "STORY"
|
|
775
|
+
? "shares,reach"
|
|
776
|
+
: "likes,comments,saved,shares,reach",
|
|
777
|
+
}), "instagram.analytics.read");
|
|
586
778
|
return array(result["data"]).flatMap((entry) => {
|
|
587
779
|
const row = object(entry);
|
|
588
780
|
const value = array(row["values"])[0];
|
|
@@ -605,7 +797,152 @@ export function instagram(options) {
|
|
|
605
797
|
});
|
|
606
798
|
},
|
|
607
799
|
},
|
|
800
|
+
graph: {
|
|
801
|
+
async getProfile(account, input, context) {
|
|
802
|
+
authorize(account, context);
|
|
803
|
+
let profile;
|
|
804
|
+
if (input.profileId === undefined && input.handle === undefined) {
|
|
805
|
+
const own = validatedObject(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
806
|
+
fields: flavor === "facebook-login"
|
|
807
|
+
? "id,username,name,biography,profile_picture_url,followers_count,follows_count,media_count,website"
|
|
808
|
+
: "user_id,id,username,name,biography,profile_picture_url,followers_count,follows_count,media_count,website",
|
|
809
|
+
}), "profiles.read");
|
|
810
|
+
profile = publicFields(own, profileFields);
|
|
811
|
+
const ownId = optionalString(own["user_id"]);
|
|
812
|
+
if (ownId !== undefined)
|
|
813
|
+
profile = { ...profile, id: ownId };
|
|
814
|
+
}
|
|
815
|
+
else if (input.profileId !== undefined) {
|
|
816
|
+
profile = publicFields(await request(`/${encodeURIComponent(input.profileId)}`, context, undefined, {
|
|
817
|
+
fields: "id,username,name,biography,profile_picture_url,followers_count,follows_count,media_count,website",
|
|
818
|
+
}), profileFields);
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
requireFacebookLogin("instagram.profiles.read");
|
|
822
|
+
const username = input.handle;
|
|
823
|
+
if (username === undefined)
|
|
824
|
+
throw new SocialError({
|
|
825
|
+
code: "invalid_input",
|
|
826
|
+
operation: "profiles.read",
|
|
827
|
+
message: "An Instagram profile ID or handle is required.",
|
|
828
|
+
});
|
|
829
|
+
if (!/^[A-Za-z0-9._]{1,30}$/.test(username))
|
|
830
|
+
throw new SocialError({
|
|
831
|
+
code: "invalid_input",
|
|
832
|
+
operation: "profiles.read",
|
|
833
|
+
message: "Instagram profile handles must contain only letters, numbers, periods, or underscores.",
|
|
834
|
+
});
|
|
835
|
+
const response = object(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
836
|
+
fields: `business_discovery.username(${username}){id,username,name,biography,profile_picture_url,followers_count,follows_count,media_count,website}`,
|
|
837
|
+
}));
|
|
838
|
+
profile = publicFields(response["business_discovery"], [
|
|
839
|
+
"id",
|
|
840
|
+
"username",
|
|
841
|
+
"name",
|
|
842
|
+
"biography",
|
|
843
|
+
"profile_picture_url",
|
|
844
|
+
"followers_count",
|
|
845
|
+
"follows_count",
|
|
846
|
+
"media_count",
|
|
847
|
+
"website",
|
|
848
|
+
]);
|
|
849
|
+
}
|
|
850
|
+
const id = string(profile["id"]);
|
|
851
|
+
return {
|
|
852
|
+
ref: profileRef({
|
|
853
|
+
backend: account.backend,
|
|
854
|
+
platform: "instagram",
|
|
855
|
+
accountId: account.accountId,
|
|
856
|
+
profileId: id,
|
|
857
|
+
}),
|
|
858
|
+
...(typeof profile["name"] === "string" ? { displayName: profile["name"] } : {}),
|
|
859
|
+
...(typeof profile["username"] === "string" ? { handle: profile["username"] } : {}),
|
|
860
|
+
...(typeof profile["profile_picture_url"] === "string"
|
|
861
|
+
? { avatarUrl: profile["profile_picture_url"] }
|
|
862
|
+
: {}),
|
|
863
|
+
...(typeof profile["biography"] === "string" ? { bio: profile["biography"] } : {}),
|
|
864
|
+
native: profile,
|
|
865
|
+
};
|
|
866
|
+
},
|
|
867
|
+
},
|
|
608
868
|
native: {
|
|
869
|
+
async moderateComment({ account, commentId, hidden, context }) {
|
|
870
|
+
authorize(account, context);
|
|
871
|
+
await request(`/${encodeURIComponent(commentId)}`, context, undefined, { hide: String(hidden) }, "POST");
|
|
872
|
+
},
|
|
873
|
+
async deleteComment({ account, commentId, context }) {
|
|
874
|
+
authorize(account, context);
|
|
875
|
+
await request(`/${encodeURIComponent(commentId)}`, context, undefined, {}, "DELETE");
|
|
876
|
+
},
|
|
877
|
+
async setCommentsEnabled({ account, mediaId, enabled, context }) {
|
|
878
|
+
authorize(account, context);
|
|
879
|
+
await request(`/${encodeURIComponent(mediaId)}`, context, undefined, { comment_enabled: String(enabled) }, "POST");
|
|
880
|
+
},
|
|
881
|
+
async listCommentReplies({ account, commentId, cursor, limit, context }) {
|
|
882
|
+
authorize(account, context);
|
|
883
|
+
const query = {
|
|
884
|
+
fields: "id,text,timestamp,username",
|
|
885
|
+
limit: String(pageLimit(limit)),
|
|
886
|
+
};
|
|
887
|
+
if (cursor !== undefined)
|
|
888
|
+
query["after"] = cursor;
|
|
889
|
+
return page(validatedObject(await request(`/${encodeURIComponent(commentId)}/replies`, context, undefined, query), "instagram.comments.replies.read"), ["id", "text", "timestamp", "username"]);
|
|
890
|
+
},
|
|
891
|
+
listMentions,
|
|
892
|
+
async mentionedMedia({ account, mediaId, context }) {
|
|
893
|
+
authorize(account, context);
|
|
894
|
+
requireFacebookLogin("instagram.mentions.read");
|
|
895
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider object boundary.
|
|
896
|
+
return validatedObject(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
897
|
+
fields: `mentioned_media.media_id(${encodeURIComponent(mediaId)}){id,caption,media_type,media_url,timestamp,username,comments_count,like_count}`,
|
|
898
|
+
}), "instagram.mentions.read");
|
|
899
|
+
},
|
|
900
|
+
async mentionedComment({ account, commentId, context }) {
|
|
901
|
+
authorize(account, context);
|
|
902
|
+
requireFacebookLogin("instagram.mentions.read");
|
|
903
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider object boundary.
|
|
904
|
+
return validatedObject(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
905
|
+
fields: `mentioned_comment.comment_id(${encodeURIComponent(commentId)}){id,text,timestamp,like_count,media}`,
|
|
906
|
+
}), "instagram.mentions.read");
|
|
907
|
+
},
|
|
908
|
+
async listTaggedMedia({ account, cursor, limit, context }) {
|
|
909
|
+
authorize(account, context);
|
|
910
|
+
requireFacebookLogin("instagram.mentions.read");
|
|
911
|
+
const query = {
|
|
912
|
+
fields: "id,caption,media_type,permalink,timestamp,username",
|
|
913
|
+
limit: String(pageLimit(limit)),
|
|
914
|
+
};
|
|
915
|
+
if (cursor !== undefined)
|
|
916
|
+
query["after"] = cursor;
|
|
917
|
+
return page(validatedObject(await request(`/${encodeURIComponent(account.accountId)}/tags`, context, undefined, query), "instagram.mentions.read"), ["id", "caption", "media_type", "permalink", "timestamp", "username"]);
|
|
918
|
+
},
|
|
919
|
+
async hashtagMedia({ account, hashtagId, kind, cursor, limit, context }) {
|
|
920
|
+
authorize(account, context);
|
|
921
|
+
requireFacebookLogin("instagram.hashtags.search");
|
|
922
|
+
const query = {
|
|
923
|
+
fields: "id,caption,media_type,permalink,timestamp,username",
|
|
924
|
+
limit: String(pageLimit(limit, 50)),
|
|
925
|
+
};
|
|
926
|
+
if (cursor !== undefined)
|
|
927
|
+
query["after"] = cursor;
|
|
928
|
+
return page(validatedObject(await request(`/${encodeURIComponent(hashtagId)}/${kind}_media`, context, undefined, query), "instagram.hashtags.search"), ["id", "caption", "media_type", "permalink", "timestamp", "username"]);
|
|
929
|
+
},
|
|
930
|
+
async businessDiscovery({ account, username, fields, context }) {
|
|
931
|
+
authorize(account, context);
|
|
932
|
+
requireFacebookLogin("instagram.profiles.businessDiscovery");
|
|
933
|
+
if (!/^[A-Za-z0-9._]{1,30}$/.test(username))
|
|
934
|
+
throw new SocialError({
|
|
935
|
+
code: "invalid_input",
|
|
936
|
+
operation: "instagram.profiles.businessDiscovery",
|
|
937
|
+
message: "Business discovery usernames must contain only letters, numbers, periods, or underscores.",
|
|
938
|
+
});
|
|
939
|
+
const selectedFields = fields ??
|
|
940
|
+
`business_discovery.username(${username}){id,username,name,biography,followers_count,media_count,profile_picture_url,media.limit(25){id,caption,media_type,permalink,timestamp}}`;
|
|
941
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider object boundary.
|
|
942
|
+
return object(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
943
|
+
fields: selectedFields,
|
|
944
|
+
}));
|
|
945
|
+
},
|
|
609
946
|
publishContainer,
|
|
610
947
|
async publishCarousel(account, children, caption, context) {
|
|
611
948
|
authorize(account, context);
|
|
@@ -636,39 +973,58 @@ export function instagram(options) {
|
|
|
636
973
|
const created = object(await request(`/${encodeURIComponent(account.accountId)}/media`, context, {
|
|
637
974
|
media_type: "REELS",
|
|
638
975
|
video_url: videoUrl,
|
|
976
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
639
977
|
...(caption ? { caption } : {}),
|
|
640
978
|
}));
|
|
641
979
|
return publishContainer(account, string(created["id"]), context);
|
|
642
980
|
},
|
|
643
|
-
async publishStory({ account, mediaUrl, context }) {
|
|
981
|
+
async publishStory({ account, mediaUrl, videoUrl, context }) {
|
|
644
982
|
authorize(account, context);
|
|
645
983
|
const created = object(await request(`/${encodeURIComponent(account.accountId)}/media`, context, {
|
|
646
984
|
media_type: "STORIES",
|
|
647
|
-
image_url: mediaUrl,
|
|
985
|
+
...(videoUrl === undefined ? { image_url: mediaUrl } : { video_url: videoUrl }),
|
|
648
986
|
}));
|
|
649
987
|
return publishContainer(account, string(created["id"]), context);
|
|
650
988
|
},
|
|
651
989
|
async deletePost({ account, postId, context }) {
|
|
652
990
|
authorize(account, context);
|
|
991
|
+
requireFacebookLogin("instagram.posts.delete");
|
|
653
992
|
await request(`/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
|
|
654
993
|
},
|
|
655
994
|
async hashtagSearch({ account, hashtag, context }) {
|
|
656
995
|
authorize(account, context);
|
|
657
|
-
|
|
996
|
+
requireFacebookLogin("instagram.hashtags.search");
|
|
997
|
+
const normalized = hashtag.replace(/^#/, "").trim();
|
|
998
|
+
if (!normalized)
|
|
999
|
+
throw new SocialError({
|
|
1000
|
+
code: "invalid_input",
|
|
1001
|
+
operation: "instagram.hashtags.search",
|
|
1002
|
+
message: "Instagram hashtag search requires a non-empty hashtag name.",
|
|
1003
|
+
});
|
|
1004
|
+
const result = validatedObject(await request("/ig_hashtag_search", context, undefined, {
|
|
658
1005
|
user_id: account.accountId,
|
|
659
|
-
q:
|
|
660
|
-
}));
|
|
661
|
-
return
|
|
1006
|
+
q: normalized,
|
|
1007
|
+
}), "instagram.hashtags.search");
|
|
1008
|
+
return {
|
|
1009
|
+
data: array(result["data"]).map((entry) => {
|
|
1010
|
+
const row = object(entry);
|
|
1011
|
+
return { id: string(row["id"]) };
|
|
1012
|
+
}),
|
|
1013
|
+
};
|
|
662
1014
|
},
|
|
663
1015
|
async publishingLimit({ account, context }) {
|
|
664
1016
|
authorize(account, context);
|
|
1017
|
+
// SAFETY: object() establishes an object response; this native method intentionally
|
|
1018
|
+
// preserves Meta's provider-specific config object after validating its outer shape.
|
|
665
1019
|
return object(await request(`/${encodeURIComponent(account.accountId)}/content_publishing_limit`, context, undefined, { fields: "config,quota_usage" }));
|
|
666
1020
|
},
|
|
667
|
-
async mentions({ account, context }) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1021
|
+
async mentions({ account, cursor, limit, context }) {
|
|
1022
|
+
return listMentions({
|
|
1023
|
+
account,
|
|
1024
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
1025
|
+
...(limit === undefined ? {} : { limit }),
|
|
1026
|
+
context,
|
|
1027
|
+
});
|
|
672
1028
|
},
|
|
673
1029
|
},
|
|
674
1030
|
});
|