@opencoredev/social-sdk 0.3.0 → 0.4.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/dist/cli-request.d.ts +15 -0
- package/dist/cli-request.js +193 -0
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +19 -21
- package/dist/cloud/common.d.ts +7 -6
- package/dist/cloud/common.js +35 -54
- package/dist/cloud/lifecycle.js +31 -35
- package/dist/cloud/media.d.ts +2 -2
- package/dist/cloud/media.js +13 -3
- package/dist/cloud/outcomes.d.ts +4 -3
- package/dist/cloud/outcomes.js +8 -15
- package/dist/cloud/post-for-me.js +41 -49
- package/dist/cloud/zernio.js +58 -98
- package/dist/core/client.js +79 -99
- package/dist/core/fields.d.ts +14 -0
- package/dist/core/fields.js +14 -0
- package/dist/core/idempotency.d.ts +7 -2
- package/dist/core/idempotency.js +37 -20
- package/dist/core/pagination.js +8 -7
- package/dist/core/types.d.ts +3 -2
- package/dist/platforms/bluesky.d.ts +65 -1
- package/dist/platforms/bluesky.js +675 -276
- package/dist/platforms/instagram.d.ts +2 -0
- package/dist/platforms/instagram.js +130 -105
- package/dist/platforms/linkedin.d.ts +58 -1
- package/dist/platforms/linkedin.js +877 -107
- package/dist/platforms/threads.d.ts +13 -1
- package/dist/platforms/threads.js +204 -302
- package/dist/platforms/tiktok.d.ts +4 -0
- package/dist/platforms/tiktok.js +140 -124
- package/dist/platforms/webhook-adapter.d.ts +9 -0
- package/dist/platforms/webhook-adapter.js +24 -0
- package/dist/platforms/x-engagement.js +7 -12
- package/dist/platforms/x-stream.d.ts +83 -0
- package/dist/platforms/x-stream.js +350 -0
- package/dist/platforms/x.d.ts +72 -0
- package/dist/platforms/x.js +328 -119
- package/dist/platforms/youtube-upload.d.ts +1 -1
- package/dist/platforms/youtube-upload.js +6 -2
- package/dist/platforms/youtube.d.ts +28 -4
- package/dist/platforms/youtube.js +291 -133
- package/dist/server/bluesky-oauth.d.ts +177 -0
- package/dist/server/bluesky-oauth.js +1229 -0
- package/dist/server/connections.d.ts +14 -0
- package/dist/server/connections.js +10 -2
- package/dist/server/egress.d.ts +14 -0
- package/dist/server/egress.js +115 -0
- package/dist/server/oauth-internal.d.ts +6 -0
- package/dist/server/oauth-internal.js +66 -0
- package/dist/server/oauth.d.ts +1 -1
- package/dist/server/oauth.js +46 -99
- package/dist/server/webhooks.d.ts +136 -3
- package/dist/server/webhooks.js +639 -25
- package/dist/testing/index.js +14 -28
- package/dist/transport/http.d.ts +1 -1
- package/dist/transport/http.js +0 -1
- package/dist/transport/json.d.ts +7 -0
- package/dist/transport/json.js +32 -4
- package/dist/transport/upload.d.ts +1 -1
- package/dist/transport/upload.js +46 -38
- package/dist/transport/validation.d.ts +16 -5
- package/dist/transport/validation.js +29 -7
- package/package.json +2 -2
|
@@ -9,6 +9,8 @@ export interface InstagramOptions {
|
|
|
9
9
|
readonly fetch?: typeof globalThis.fetch;
|
|
10
10
|
readonly clock?: () => Date;
|
|
11
11
|
readonly workflowStore?: InstagramWorkflowStore;
|
|
12
|
+
/** App secret that Meta uses to sign webhook deliveries (`X-Hub-Signature-256`). */
|
|
13
|
+
readonly webhookSecret?: string;
|
|
12
14
|
}
|
|
13
15
|
export interface InstagramNative {
|
|
14
16
|
readonly moderateComment: (input: {
|
|
@@ -1,11 +1,13 @@
|
|
|
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. */
|
|
2
1
|
import { defineAdapter } from "../core/adapter.js";
|
|
3
2
|
import { SocialError } from "../core/errors.js";
|
|
4
3
|
import { profileRef } from "../core/types.js";
|
|
5
|
-
import { managedHttp, publicFields } from "../cloud/common.js";
|
|
4
|
+
import { managedHttp, optionsObject, publicFields } from "../cloud/common.js";
|
|
6
5
|
import { HttpError } from "../transport/http.js";
|
|
7
|
-
import {
|
|
6
|
+
import { definedFields } from "../core/fields.js";
|
|
7
|
+
import { array, isBoolean, isJsonObject, isString, object, optionalNumber, optionalString, string, } from "../transport/validation.js";
|
|
8
8
|
import { httpsUrl } from "../transport/upload.js";
|
|
9
|
+
import { verifyMetaWebhook } from "../server/webhooks.js";
|
|
10
|
+
import { directWebhooks, webhookCapability } from "./webhook-adapter.js";
|
|
9
11
|
class MemoryInstagramWorkflowStore {
|
|
10
12
|
workflows = new Map();
|
|
11
13
|
claims = new Set();
|
|
@@ -43,12 +45,10 @@ export function instagram(options) {
|
|
|
43
45
|
: `https://graph.instagram.com/${apiVersion}`;
|
|
44
46
|
const request = managedHttp(origin, {
|
|
45
47
|
apiKey: options.auth.accessToken,
|
|
46
|
-
|
|
47
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
48
|
+
...definedFields({ fetch: options.fetch }),
|
|
48
49
|
});
|
|
49
50
|
const now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
50
51
|
const workflows = options.workflowStore ?? new MemoryInstagramWorkflowStore();
|
|
51
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated here.
|
|
52
52
|
const validatedObject = (value, operation) => {
|
|
53
53
|
try {
|
|
54
54
|
return object(value);
|
|
@@ -73,6 +73,47 @@ export function instagram(options) {
|
|
|
73
73
|
retryDisposition: { kind: "never" },
|
|
74
74
|
});
|
|
75
75
|
};
|
|
76
|
+
// Meta's Instagram Login mentions guide lists only GET /{ig-user-id}/tags and
|
|
77
|
+
// POST /{ig-user-id}/mentions. The mentioned_media and mentioned_comment field
|
|
78
|
+
// expansions are documented for Facebook Login only. Sources, accessed 2026-09-24
|
|
79
|
+
// against Graph API v25.0:
|
|
80
|
+
// https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-instagram-login/mentions
|
|
81
|
+
// https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-facebook-login/mentions
|
|
82
|
+
// https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/tags
|
|
83
|
+
// https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/mentioned_media
|
|
84
|
+
const requireFacebookLoginForMentionLookup = (field) => {
|
|
85
|
+
if (flavor !== "facebook-login")
|
|
86
|
+
throw new SocialError({
|
|
87
|
+
code: "unsupported_capability",
|
|
88
|
+
operation: "instagram.mentions.read",
|
|
89
|
+
message: `Instagram ${field} lookups require a Facebook Login Graph API access token. Instagram Login supports only the /{ig-user-id}/tags mentions edge.`,
|
|
90
|
+
retryDisposition: { kind: "never" },
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
// Meta documents `DELETE /{ig-media-id}` for Instagram API with Facebook Login only.
|
|
94
|
+
// Source: https://developers.facebook.com/docs/instagram-platform/reference/instagram-media
|
|
95
|
+
// (accessed 2026-09-24, Graph API v25.0).
|
|
96
|
+
const deleteMedia = async (mediaId, context, operation) => {
|
|
97
|
+
if (flavor !== "facebook-login")
|
|
98
|
+
throw new SocialError({
|
|
99
|
+
code: "unsupported_capability",
|
|
100
|
+
operation,
|
|
101
|
+
message: `${operation} is not supported with Instagram Login. Meta supports media deletion only through Instagram API with Facebook Login.`,
|
|
102
|
+
retryDisposition: { kind: "never" },
|
|
103
|
+
});
|
|
104
|
+
const result = await request(`/${encodeURIComponent(mediaId)}`, context, undefined, {}, "DELETE");
|
|
105
|
+
// SAFETY: request validates the Graph response as a JSON field before returning it.
|
|
106
|
+
const confirmed = isJsonObject(result)
|
|
107
|
+
? result["success"]
|
|
108
|
+
: undefined;
|
|
109
|
+
if (confirmed !== true)
|
|
110
|
+
throw new SocialError({
|
|
111
|
+
code: "ambiguous_outcome",
|
|
112
|
+
operation,
|
|
113
|
+
message: "Instagram did not confirm the media deletion. Reconcile before retrying.",
|
|
114
|
+
retryDisposition: { kind: "reconcile-first" },
|
|
115
|
+
});
|
|
116
|
+
};
|
|
76
117
|
const pageLimit = (limit, max = 50) => {
|
|
77
118
|
const value = limit ?? 25;
|
|
78
119
|
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
@@ -88,12 +129,10 @@ export function instagram(options) {
|
|
|
88
129
|
const paging = result["paging"] === undefined ? {} : object(result["paging"]);
|
|
89
130
|
const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
|
|
90
131
|
// Graph API omits `paging.next` on the last page even when `cursors.after` is present.
|
|
91
|
-
const nextCursor =
|
|
92
|
-
typeof cursors["after"] === "string" &&
|
|
93
|
-
cursors["after"].length > 0
|
|
132
|
+
const nextCursor = isString(paging["next"]) && isString(cursors["after"]) && cursors["after"].length > 0
|
|
94
133
|
? cursors["after"]
|
|
95
134
|
: undefined;
|
|
96
|
-
return { items, ...(
|
|
135
|
+
return { items, ...definedFields({ nextCursor }) };
|
|
97
136
|
};
|
|
98
137
|
const authorize = (ref, context) => {
|
|
99
138
|
if (ref.backend !== context.backendInstance ||
|
|
@@ -117,14 +156,13 @@ export function instagram(options) {
|
|
|
117
156
|
operation: "posts.list",
|
|
118
157
|
message: "Instagram feed limit must be an integer from 1 through 100.",
|
|
119
158
|
});
|
|
120
|
-
// oxlint-disable-next-line anti-slop/no-known-value-widening -- validated boundary or fixture contract.
|
|
121
159
|
const query = {
|
|
122
160
|
fields: "id,caption,media_type,media_product_type,permalink,timestamp,username",
|
|
161
|
+
...definedFields({
|
|
162
|
+
after: input.cursor,
|
|
163
|
+
limit: input.limit === undefined ? undefined : String(input.limit),
|
|
164
|
+
}),
|
|
123
165
|
};
|
|
124
|
-
if (input.cursor !== undefined)
|
|
125
|
-
query["after"] = input.cursor;
|
|
126
|
-
if (input.limit !== undefined)
|
|
127
|
-
query["limit"] = String(input.limit);
|
|
128
166
|
const result = object(await request(`/${encodeURIComponent(selected.accountId)}/media`, context, undefined, query));
|
|
129
167
|
const items = array(result["data"]).map((entry) => publicFields(entry, [
|
|
130
168
|
"id",
|
|
@@ -137,15 +175,11 @@ export function instagram(options) {
|
|
|
137
175
|
]));
|
|
138
176
|
const paging = result["paging"] === undefined ? {} : object(result["paging"]);
|
|
139
177
|
const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
|
|
140
|
-
|
|
141
|
-
const
|
|
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;
|
|
178
|
+
const hasNext = isString(paging["next"]) && paging["next"].length > 0;
|
|
179
|
+
const nextCursor = hasNext ? optionalString(cursors["after"]) : undefined;
|
|
145
180
|
return {
|
|
146
181
|
items,
|
|
147
|
-
|
|
148
|
-
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
182
|
+
...definedFields({ nextCursor }),
|
|
149
183
|
};
|
|
150
184
|
};
|
|
151
185
|
const getAccountMetrics = async (selected, context) => {
|
|
@@ -241,7 +275,6 @@ export function instagram(options) {
|
|
|
241
275
|
await workflows.update(workflowId, { stage: "unknown" });
|
|
242
276
|
let result;
|
|
243
277
|
try {
|
|
244
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
245
278
|
result = object(await request(`/${encodeURIComponent(account.accountId)}/media_publish`, context, {
|
|
246
279
|
creation_id: containerId,
|
|
247
280
|
}));
|
|
@@ -360,13 +393,12 @@ export function instagram(options) {
|
|
|
360
393
|
};
|
|
361
394
|
const listMentions = async ({ account, cursor, limit, context, }) => {
|
|
362
395
|
authorize(account, context);
|
|
363
|
-
|
|
396
|
+
// Both login flavors expose GET /{ig-user-id}/tags; only the host and scopes differ.
|
|
364
397
|
const query = {
|
|
365
398
|
fields: "id,caption,media_type,media_product_type,permalink,timestamp,username",
|
|
366
399
|
limit: String(pageLimit(limit)),
|
|
400
|
+
...definedFields({ after: cursor }),
|
|
367
401
|
};
|
|
368
|
-
if (cursor !== undefined)
|
|
369
|
-
query["after"] = cursor;
|
|
370
402
|
const result = validatedObject(await request(`/${encodeURIComponent(account.accountId)}/tags`, context, undefined, query), "instagram.mentions.read");
|
|
371
403
|
return page(result, [
|
|
372
404
|
"id",
|
|
@@ -407,6 +439,18 @@ export function instagram(options) {
|
|
|
407
439
|
: ["instagram_business_basic", "instagram_business_content_publish"],
|
|
408
440
|
notes: "Professional accounts, public HTTPS media, explicit native continuation for processing containers. Carousels must have matching aspect ratios to avoid upstream cropping.",
|
|
409
441
|
},
|
|
442
|
+
{
|
|
443
|
+
platform: "instagram",
|
|
444
|
+
operation: "posts.schedule",
|
|
445
|
+
availability: "unsupported-by-platform",
|
|
446
|
+
notes: "The content publishing API has no publish-time parameter; media_publish publishes immediately and unpublished containers expire after 24 hours.",
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
platform: "instagram",
|
|
450
|
+
operation: "posts.update",
|
|
451
|
+
availability: "unsupported-by-platform",
|
|
452
|
+
notes: "Instagram Graph API does not provide an edit endpoint for published media.",
|
|
453
|
+
},
|
|
410
454
|
...[
|
|
411
455
|
"accounts.read",
|
|
412
456
|
"posts.list",
|
|
@@ -463,21 +507,24 @@ export function instagram(options) {
|
|
|
463
507
|
{ platform: "instagram", operation: "stories.publish", availability: "available" },
|
|
464
508
|
{
|
|
465
509
|
platform: "instagram",
|
|
466
|
-
operation: "
|
|
467
|
-
availability:
|
|
468
|
-
|
|
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",
|
|
510
|
+
operation: "profile.update",
|
|
511
|
+
availability: "unsupported-by-platform",
|
|
512
|
+
notes: "The IG User node supports reads only. Updating it is not supported.",
|
|
480
513
|
},
|
|
514
|
+
...["posts.delete", "posts.removeFromPlatform"].map((operation) => flavor === "facebook-login"
|
|
515
|
+
? {
|
|
516
|
+
platform: "instagram",
|
|
517
|
+
operation,
|
|
518
|
+
availability: "available",
|
|
519
|
+
requiredScopes: ["instagram_basic", "instagram_manage_contents"],
|
|
520
|
+
notes: "Deletes non-ad posts, Stories, Reels, and whole carousel albums through DELETE /{ig-media-id}. Individual carousel children cannot be deleted.",
|
|
521
|
+
}
|
|
522
|
+
: {
|
|
523
|
+
platform: "instagram",
|
|
524
|
+
operation,
|
|
525
|
+
availability: "unsupported-by-platform",
|
|
526
|
+
notes: "Meta documents media deletion for Instagram API with Facebook Login only. See https://developers.facebook.com/docs/instagram-platform/reference/instagram-media",
|
|
527
|
+
}),
|
|
481
528
|
...(flavor === "facebook-login"
|
|
482
529
|
? [
|
|
483
530
|
{
|
|
@@ -494,27 +541,17 @@ export function instagram(options) {
|
|
|
494
541
|
operation: "publishing.limit.read",
|
|
495
542
|
availability: "available",
|
|
496
543
|
},
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
]),
|
|
544
|
+
{
|
|
545
|
+
platform: "instagram",
|
|
546
|
+
operation: "mentions.read",
|
|
547
|
+
availability: "available",
|
|
548
|
+
requiredScopes: flavor === "facebook-login"
|
|
549
|
+
? ["instagram_basic", "instagram_manage_comments", "pages_read_engagement"]
|
|
550
|
+
: ["instagram_business_basic", "instagram_business_manage_comments"],
|
|
551
|
+
notes: flavor === "facebook-login"
|
|
552
|
+
? "Reads the paginated /{ig-user-id}/tags edge. Native mentionedMedia and mentionedComment look up single @mentions. Story mentions and private media are not returned."
|
|
553
|
+
: "Reads the paginated /{ig-user-id}/tags edge on graph.instagram.com. mentionedMedia and mentionedComment require Facebook Login. Story mentions and private media are not returned.",
|
|
554
|
+
},
|
|
518
555
|
{
|
|
519
556
|
platform: "instagram",
|
|
520
557
|
operation: "product.tagging",
|
|
@@ -525,8 +562,16 @@ export function instagram(options) {
|
|
|
525
562
|
operation: "messages.read",
|
|
526
563
|
availability: "approval-dependent",
|
|
527
564
|
},
|
|
565
|
+
webhookCapability("instagram", "Verifies Meta X-Hub-Signature-256 with the app secret and decodes object=instagram deliveries. Answer the GET handshake with answerMetaWebhookChallenge."),
|
|
566
|
+
{
|
|
567
|
+
platform: "instagram",
|
|
568
|
+
operation: "notifications.read",
|
|
569
|
+
availability: "unsupported-by-platform",
|
|
570
|
+
notes: "The Instagram API has no notifications edge. Use mentions.read, tags, comments, or webhooks instead.",
|
|
571
|
+
},
|
|
528
572
|
],
|
|
529
573
|
},
|
|
574
|
+
webhooks: directWebhooks("instagram", (input) => verifyMetaWebhook({ ...input, secret: options.webhookSecret ?? "" }), now),
|
|
530
575
|
accounts: {
|
|
531
576
|
async list(_input, context) {
|
|
532
577
|
return { items: [await readAccount(context)] };
|
|
@@ -558,6 +603,8 @@ export function instagram(options) {
|
|
|
558
603
|
catch {
|
|
559
604
|
fail("instagram.url", "Use public HTTPS media without local hosts or credentials.");
|
|
560
605
|
}
|
|
606
|
+
if (item.kind === "document")
|
|
607
|
+
fail("instagram.document", "Instagram publishing accepts image and video media only.");
|
|
561
608
|
if (item.kind === "image" && item.mimeType !== "image/jpeg")
|
|
562
609
|
fail("instagram.jpeg", "Instagram image publishing requires JPEG media.");
|
|
563
610
|
if (item.kind === "video" &&
|
|
@@ -596,29 +643,21 @@ export function instagram(options) {
|
|
|
596
643
|
operation: "posts.publish",
|
|
597
644
|
message: "Public HTTPS media required.",
|
|
598
645
|
});
|
|
599
|
-
const config =
|
|
646
|
+
const config = optionsObject(target);
|
|
647
|
+
const shareToFeed = config["shareToFeed"];
|
|
600
648
|
await workflows.update(workflow.id, { stage: "unknown" });
|
|
601
649
|
const created = object(await request(`/${encodeURIComponent(target.account.accountId)}/media`, context, {
|
|
602
650
|
...(item.kind === "image"
|
|
603
651
|
? {
|
|
604
652
|
image_url: item.source.url,
|
|
605
|
-
|
|
606
|
-
...(item.altText === undefined ? {} : { alt_text: item.altText }),
|
|
653
|
+
...definedFields({ alt_text: item.altText }),
|
|
607
654
|
}
|
|
608
655
|
: {
|
|
609
656
|
video_url: item.source.url,
|
|
610
657
|
media_type: media.length > 1 ? "VIDEO" : "REELS",
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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.
|
|
619
|
-
...(typeof config["shareToFeed"] === "boolean"
|
|
620
|
-
? { share_to_feed: config["shareToFeed"] }
|
|
621
|
-
: {}),
|
|
658
|
+
...definedFields({
|
|
659
|
+
share_to_feed: isBoolean(shareToFeed) ? shareToFeed : undefined,
|
|
660
|
+
}),
|
|
622
661
|
}),
|
|
623
662
|
...(media.length > 1
|
|
624
663
|
? { is_carousel_item: true }
|
|
@@ -739,8 +778,7 @@ export function instagram(options) {
|
|
|
739
778
|
},
|
|
740
779
|
async removeFromPlatform(ref, context) {
|
|
741
780
|
authorize(ref, context);
|
|
742
|
-
|
|
743
|
-
await request(`/${encodeURIComponent(ref.postId)}`, context, undefined, {}, "DELETE");
|
|
781
|
+
await deleteMedia(ref.postId, context, "instagram.posts.removeFromPlatform");
|
|
744
782
|
},
|
|
745
783
|
},
|
|
746
784
|
comments: {
|
|
@@ -749,9 +787,8 @@ export function instagram(options) {
|
|
|
749
787
|
const query = {
|
|
750
788
|
fields: "id,text,timestamp,username",
|
|
751
789
|
limit: String(pageLimit(input.limit)),
|
|
790
|
+
...definedFields({ after: input.cursor }),
|
|
752
791
|
};
|
|
753
|
-
if (input.cursor !== undefined)
|
|
754
|
-
query["after"] = input.cursor;
|
|
755
792
|
const result = validatedObject(await request(`/${encodeURIComponent(ref.postId)}/comments`, context, undefined, query), "instagram.comments.read");
|
|
756
793
|
return page(result, ["id", "text", "timestamp", "username"]);
|
|
757
794
|
},
|
|
@@ -855,12 +892,12 @@ export function instagram(options) {
|
|
|
855
892
|
accountId: account.accountId,
|
|
856
893
|
profileId: id,
|
|
857
894
|
}),
|
|
858
|
-
...(
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
:
|
|
863
|
-
|
|
895
|
+
...definedFields({
|
|
896
|
+
displayName: optionalString(profile["name"]),
|
|
897
|
+
handle: optionalString(profile["username"]),
|
|
898
|
+
avatarUrl: optionalString(profile["profile_picture_url"]),
|
|
899
|
+
bio: optionalString(profile["biography"]),
|
|
900
|
+
}),
|
|
864
901
|
native: profile,
|
|
865
902
|
};
|
|
866
903
|
},
|
|
@@ -883,37 +920,32 @@ export function instagram(options) {
|
|
|
883
920
|
const query = {
|
|
884
921
|
fields: "id,text,timestamp,username",
|
|
885
922
|
limit: String(pageLimit(limit)),
|
|
923
|
+
...definedFields({ after: cursor }),
|
|
886
924
|
};
|
|
887
|
-
if (cursor !== undefined)
|
|
888
|
-
query["after"] = cursor;
|
|
889
925
|
return page(validatedObject(await request(`/${encodeURIComponent(commentId)}/replies`, context, undefined, query), "instagram.comments.replies.read"), ["id", "text", "timestamp", "username"]);
|
|
890
926
|
},
|
|
891
927
|
listMentions,
|
|
892
928
|
async mentionedMedia({ account, mediaId, context }) {
|
|
893
929
|
authorize(account, context);
|
|
894
|
-
|
|
895
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider object boundary.
|
|
930
|
+
requireFacebookLoginForMentionLookup("mentioned_media");
|
|
896
931
|
return validatedObject(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
897
932
|
fields: `mentioned_media.media_id(${encodeURIComponent(mediaId)}){id,caption,media_type,media_url,timestamp,username,comments_count,like_count}`,
|
|
898
933
|
}), "instagram.mentions.read");
|
|
899
934
|
},
|
|
900
935
|
async mentionedComment({ account, commentId, context }) {
|
|
901
936
|
authorize(account, context);
|
|
902
|
-
|
|
903
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated provider object boundary.
|
|
937
|
+
requireFacebookLoginForMentionLookup("mentioned_comment");
|
|
904
938
|
return validatedObject(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
905
939
|
fields: `mentioned_comment.comment_id(${encodeURIComponent(commentId)}){id,text,timestamp,like_count,media}`,
|
|
906
940
|
}), "instagram.mentions.read");
|
|
907
941
|
},
|
|
908
942
|
async listTaggedMedia({ account, cursor, limit, context }) {
|
|
909
943
|
authorize(account, context);
|
|
910
|
-
requireFacebookLogin("instagram.mentions.read");
|
|
911
944
|
const query = {
|
|
912
945
|
fields: "id,caption,media_type,permalink,timestamp,username",
|
|
913
946
|
limit: String(pageLimit(limit)),
|
|
947
|
+
...definedFields({ after: cursor }),
|
|
914
948
|
};
|
|
915
|
-
if (cursor !== undefined)
|
|
916
|
-
query["after"] = cursor;
|
|
917
949
|
return page(validatedObject(await request(`/${encodeURIComponent(account.accountId)}/tags`, context, undefined, query), "instagram.mentions.read"), ["id", "caption", "media_type", "permalink", "timestamp", "username"]);
|
|
918
950
|
},
|
|
919
951
|
async hashtagMedia({ account, hashtagId, kind, cursor, limit, context }) {
|
|
@@ -922,9 +954,8 @@ export function instagram(options) {
|
|
|
922
954
|
const query = {
|
|
923
955
|
fields: "id,caption,media_type,permalink,timestamp,username",
|
|
924
956
|
limit: String(pageLimit(limit, 50)),
|
|
957
|
+
...definedFields({ after: cursor }),
|
|
925
958
|
};
|
|
926
|
-
if (cursor !== undefined)
|
|
927
|
-
query["after"] = cursor;
|
|
928
959
|
return page(validatedObject(await request(`/${encodeURIComponent(hashtagId)}/${kind}_media`, context, undefined, query), "instagram.hashtags.search"), ["id", "caption", "media_type", "permalink", "timestamp", "username"]);
|
|
929
960
|
},
|
|
930
961
|
async businessDiscovery({ account, username, fields, context }) {
|
|
@@ -938,7 +969,6 @@ export function instagram(options) {
|
|
|
938
969
|
});
|
|
939
970
|
const selectedFields = fields ??
|
|
940
971
|
`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
972
|
return object(await request(`/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
943
973
|
fields: selectedFields,
|
|
944
974
|
}));
|
|
@@ -973,8 +1003,7 @@ export function instagram(options) {
|
|
|
973
1003
|
const created = object(await request(`/${encodeURIComponent(account.accountId)}/media`, context, {
|
|
974
1004
|
media_type: "REELS",
|
|
975
1005
|
video_url: videoUrl,
|
|
976
|
-
|
|
977
|
-
...(caption ? { caption } : {}),
|
|
1006
|
+
...definedFields({ caption: caption === "" ? undefined : caption }),
|
|
978
1007
|
}));
|
|
979
1008
|
return publishContainer(account, string(created["id"]), context);
|
|
980
1009
|
},
|
|
@@ -988,8 +1017,7 @@ export function instagram(options) {
|
|
|
988
1017
|
},
|
|
989
1018
|
async deletePost({ account, postId, context }) {
|
|
990
1019
|
authorize(account, context);
|
|
991
|
-
|
|
992
|
-
await request(`/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
|
|
1020
|
+
await deleteMedia(postId, context, "instagram.posts.delete");
|
|
993
1021
|
},
|
|
994
1022
|
async hashtagSearch({ account, hashtag, context }) {
|
|
995
1023
|
authorize(account, context);
|
|
@@ -1014,15 +1042,12 @@ export function instagram(options) {
|
|
|
1014
1042
|
},
|
|
1015
1043
|
async publishingLimit({ account, context }) {
|
|
1016
1044
|
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.
|
|
1019
1045
|
return object(await request(`/${encodeURIComponent(account.accountId)}/content_publishing_limit`, context, undefined, { fields: "config,quota_usage" }));
|
|
1020
1046
|
},
|
|
1021
1047
|
async mentions({ account, cursor, limit, context }) {
|
|
1022
1048
|
return listMentions({
|
|
1023
1049
|
account,
|
|
1024
|
-
...(
|
|
1025
|
-
...(limit === undefined ? {} : { limit }),
|
|
1050
|
+
...definedFields({ cursor, limit }),
|
|
1026
1051
|
context,
|
|
1027
1052
|
});
|
|
1028
1053
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AdapterOperationContext, ConnectedAccountRef, JsonObject, MediaRef } from "../core/types.js";
|
|
1
|
+
import type { AdapterOperationContext, ConnectedAccountRef, JsonObject, MediaRef, Page } from "../core/types.js";
|
|
2
2
|
export interface LinkedInOptions {
|
|
3
3
|
readonly auth: {
|
|
4
4
|
readonly accessToken: string;
|
|
@@ -8,6 +8,8 @@ export interface LinkedInOptions {
|
|
|
8
8
|
readonly apiVersion: string;
|
|
9
9
|
readonly fetch?: typeof globalThis.fetch;
|
|
10
10
|
readonly clock?: () => Date;
|
|
11
|
+
/** App client secret that LinkedIn uses to sign webhook deliveries (`X-LI-Signature`). */
|
|
12
|
+
readonly webhookSecret?: string;
|
|
11
13
|
}
|
|
12
14
|
export interface LinkedInTimeInterval {
|
|
13
15
|
readonly granularity: "DAY" | "WEEK" | "MONTH";
|
|
@@ -44,8 +46,45 @@ export interface LinkedInShareStatistics {
|
|
|
44
46
|
readonly interval?: LinkedInTimeInterval | undefined;
|
|
45
47
|
readonly metrics: Readonly<Record<string, number>>;
|
|
46
48
|
}
|
|
49
|
+
/** One approved organization role returned by LinkedIn's `organizationAcls` roleAssignee finder. */
|
|
50
|
+
export interface LinkedInOrganizationRole {
|
|
51
|
+
readonly organization: `urn:li:organization:${string}`;
|
|
52
|
+
readonly role: "ADMINISTRATOR";
|
|
53
|
+
readonly state: "APPROVED";
|
|
54
|
+
}
|
|
47
55
|
export interface LinkedInNative {
|
|
56
|
+
/**
|
|
57
|
+
* Lists organizations the authenticated member administers (approved `ADMINISTRATOR` roles).
|
|
58
|
+
* Requires `rw_organization_admin` or `r_organization_admin`. Use the result to configure
|
|
59
|
+
* another adapter instance; this instance still acts only as its configured author.
|
|
60
|
+
*/
|
|
61
|
+
readonly listAdministeredOrganizations: (input: {
|
|
62
|
+
readonly account: ConnectedAccountRef;
|
|
63
|
+
readonly cursor?: string;
|
|
64
|
+
readonly limit?: number;
|
|
65
|
+
readonly context: AdapterOperationContext;
|
|
66
|
+
}) => Promise<Page<LinkedInOrganizationRole>>;
|
|
48
67
|
readonly imageStatus: (ref: MediaRef, context: AdapterOperationContext) => Promise<JsonObject>;
|
|
68
|
+
/**
|
|
69
|
+
* Reads one uploaded video's owner and processing status with a single GET. Call it again later
|
|
70
|
+
* while the status is PROCESSING or WAITING_UPLOAD, or use `waitForVideo`. Other methods never
|
|
71
|
+
* poll on their own.
|
|
72
|
+
*/
|
|
73
|
+
readonly videoStatus: (ref: MediaRef, context: AdapterOperationContext) => Promise<JsonObject>;
|
|
74
|
+
/**
|
|
75
|
+
* Opt-in bounded wait for one uploaded video. Reads the status like `videoStatus`, and while it is
|
|
76
|
+
* PROCESSING or WAITING_UPLOAD waits `intervalMs` and reads again, up to `maxChecks` reads. It
|
|
77
|
+
* stops early when the next wait would exceed the context's elapsed budget, and throws
|
|
78
|
+
* `cancelled` when the context signal aborts. Returns the last status it read, which may still be
|
|
79
|
+
* PROCESSING; it never throws for slow processing and never creates a post.
|
|
80
|
+
*/
|
|
81
|
+
readonly waitForVideo: (ref: MediaRef, context: AdapterOperationContext, options?: LinkedInVideoWaitOptions) => Promise<JsonObject>;
|
|
82
|
+
/** Reads `id`, `owner` and `status` (`WAITING_UPLOAD`, `PROCESSING`, `AVAILABLE`, `PROCESSING_FAILED`). */
|
|
83
|
+
readonly documentStatus: (ref: MediaRef, context: AdapterOperationContext) => Promise<JsonObject>;
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated Upload video bytes with `social.media.upload` and check processing with
|
|
86
|
+
* `videoStatus`. This method always throws `unsupported_capability`.
|
|
87
|
+
*/
|
|
49
88
|
readonly registerVideo: (input: {
|
|
50
89
|
readonly account: ConnectedAccountRef;
|
|
51
90
|
readonly byteSize: number;
|
|
@@ -80,6 +119,18 @@ export interface LinkedInNative {
|
|
|
80
119
|
readonly postId: string;
|
|
81
120
|
readonly context: AdapterOperationContext;
|
|
82
121
|
}) => Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Deletes a comment with the Comments API. `postId` is the share or ugcPost URN, and
|
|
124
|
+
* `commentId` is the complete `commentUrn` from comment reads. Organization authors are sent
|
|
125
|
+
* as the `actor`. LinkedIn does not document which comments an actor may delete, so expect
|
|
126
|
+
* only the configured author's own comments to succeed.
|
|
127
|
+
*/
|
|
128
|
+
readonly deleteComment: (input: {
|
|
129
|
+
readonly account: ConnectedAccountRef;
|
|
130
|
+
readonly postId: string;
|
|
131
|
+
readonly commentId: string;
|
|
132
|
+
readonly context: AdapterOperationContext;
|
|
133
|
+
}) => Promise<void>;
|
|
83
134
|
readonly organizationAnalytics: (input: {
|
|
84
135
|
readonly account: ConnectedAccountRef;
|
|
85
136
|
readonly query?: JsonObject;
|
|
@@ -105,4 +156,10 @@ export interface LinkedInNative {
|
|
|
105
156
|
readonly context: AdapterOperationContext;
|
|
106
157
|
}) => Promise<number | undefined>;
|
|
107
158
|
}
|
|
159
|
+
export interface LinkedInVideoWaitOptions {
|
|
160
|
+
/** Delay between status reads. Integer from 1,000 to 60,000 ms; defaults to 5,000. */
|
|
161
|
+
readonly intervalMs?: number;
|
|
162
|
+
/** Maximum status reads, including the first. Integer from 1 to 60; defaults to 12. */
|
|
163
|
+
readonly maxChecks?: number;
|
|
164
|
+
}
|
|
108
165
|
export declare function linkedin(options: LinkedInOptions): import("../core/adapter.js").SocialAdapter<LinkedInNative>;
|