@opencoredev/social-sdk 0.2.1 → 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 +87 -0
- package/dist/platforms/x.js +648 -120
- 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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
/* oxlint-disable anti-slop/no-chained-type-assertions, anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof, anti-slop/require-readable-spacing, anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract. */
|
|
2
1
|
import { connectedAccountRef, defineAdapter, platformPostRef, profileRef, } from "../core/index.js";
|
|
3
2
|
import { SocialError } from "../core/errors.js";
|
|
4
|
-
import { managedHttp, publicFields } from "../cloud/common.js";
|
|
5
|
-
import {
|
|
3
|
+
import { managedHttp, optionsObject, publicFields } from "../cloud/common.js";
|
|
4
|
+
import { definedFields } from "../core/fields.js";
|
|
5
|
+
import { object, array, isJsonArray, isJsonObject, isString, optionalArray, optionalNumber, optionalObject, optionalString, } from "../transport/validation.js";
|
|
6
6
|
import { httpsUrl } from "../transport/upload.js";
|
|
7
|
+
import { verifyMetaWebhook } from "../server/webhooks.js";
|
|
8
|
+
import { directWebhooks, webhookCapability } from "./webhook-adapter.js";
|
|
7
9
|
export class MemoryThreadsWorkflowStore {
|
|
8
10
|
rows = new Map();
|
|
9
11
|
claims = new Set();
|
|
@@ -47,14 +49,12 @@ export function threads(options) {
|
|
|
47
49
|
const backend = options.backend ?? "default", version = options.graphVersion ?? "v1.0", auth = options.auth, store = options.workflowStore ?? new MemoryThreadsWorkflowStore(), now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
48
50
|
const http = managedHttp(`https://graph.threads.net/${version}`, {
|
|
49
51
|
apiKey: auth.accessToken,
|
|
50
|
-
|
|
51
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
52
|
+
...definedFields({ fetch: options.fetch }),
|
|
52
53
|
});
|
|
53
54
|
async function request(path, init, operation, context) {
|
|
54
55
|
const [raw = "", qs] = path.split("?", 2);
|
|
55
56
|
const query = Object.fromEntries(new URLSearchParams(qs ?? ""));
|
|
56
57
|
try {
|
|
57
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
58
58
|
return object(await http(`/${raw.replace(/^\//, "")}`, context, init.method === "POST" ? {} : undefined, query, init.method === "POST" || init.method === "DELETE" || init.method === "PUT"
|
|
59
59
|
? init.method
|
|
60
60
|
: "GET"));
|
|
@@ -71,37 +71,24 @@ export function threads(options) {
|
|
|
71
71
|
}
|
|
72
72
|
const account = accountRef(backend, auth.userId);
|
|
73
73
|
function pageFrom(result) {
|
|
74
|
-
const items = array(result["data"]).map((entry) =>
|
|
75
|
-
// SAFETY: object() validates each provider data entry as a JSON object.
|
|
76
|
-
return object(entry);
|
|
77
|
-
});
|
|
74
|
+
const items = array(result["data"]).map((entry) => object(entry));
|
|
78
75
|
const paging = result["paging"] === undefined ? {} : object(result["paging"]);
|
|
79
76
|
const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
|
|
80
77
|
// Graph omits paging.next on the last page even when cursors.after is present.
|
|
81
|
-
const nextCursor =
|
|
78
|
+
const nextCursor = isString(paging["next"]) && paging["next"].length > 0
|
|
82
79
|
? optionalString(cursors["after"])
|
|
83
80
|
: undefined;
|
|
84
|
-
return { items, ...(
|
|
81
|
+
return { items, ...definedFields({ nextCursor }) };
|
|
85
82
|
}
|
|
86
83
|
async function readAccount(context) {
|
|
87
84
|
const v = await request("/me?fields=id,username,threads_profile_picture_url,threads_biography", { method: "GET" }, "threads.accounts.read", context);
|
|
88
85
|
const id = v["id"], username = v["username"];
|
|
89
|
-
|
|
90
|
-
if (typeof id !== "string" || id !== auth.userId)
|
|
86
|
+
if (!isString(id) || id !== auth.userId)
|
|
91
87
|
fail("threads.accounts.read", "Threads returned an unauthorized account.", "unauthorized");
|
|
92
88
|
return {
|
|
93
89
|
ref: account,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
97
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
98
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
99
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
100
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
101
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
102
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
103
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
104
|
-
...(typeof username === "string" ? { handle: username } : {}),
|
|
90
|
+
displayName: optionalString(username) ?? auth.handle ?? auth.userId,
|
|
91
|
+
...definedFields({ handle: optionalString(username) }),
|
|
105
92
|
status: "connected",
|
|
106
93
|
};
|
|
107
94
|
}
|
|
@@ -121,15 +108,11 @@ export function threads(options) {
|
|
|
121
108
|
const items = array(result["data"]).map((entry) => publicFields(entry, ["id", "text", "username", "media_type", "permalink", "timestamp"]));
|
|
122
109
|
const paging = result["paging"] === undefined ? {} : object(result["paging"]);
|
|
123
110
|
const cursors = paging["cursors"] === undefined ? {} : object(paging["cursors"]);
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const nextCursor =
|
|
127
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
128
|
-
hasNext && typeof cursors["after"] === "string" ? cursors["after"] : undefined;
|
|
111
|
+
const hasNext = isString(paging["next"]) && paging["next"].length > 0;
|
|
112
|
+
const nextCursor = hasNext ? optionalString(cursors["after"]) : undefined;
|
|
129
113
|
return {
|
|
130
114
|
items,
|
|
131
|
-
|
|
132
|
-
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
115
|
+
...definedFields({ nextCursor }),
|
|
133
116
|
};
|
|
134
117
|
}
|
|
135
118
|
async function getAccountMetrics(selected, context) {
|
|
@@ -147,8 +130,7 @@ export function threads(options) {
|
|
|
147
130
|
const allowed = new Set([...metricNames, "link_total_values"]);
|
|
148
131
|
return array(result["data"]).flatMap((entry) => {
|
|
149
132
|
const row = object(entry);
|
|
150
|
-
|
|
151
|
-
const name = typeof row["name"] === "string" ? row["name"] : undefined;
|
|
133
|
+
const name = optionalString(row["name"]);
|
|
152
134
|
if (name === undefined || !allowed.has(name))
|
|
153
135
|
return [];
|
|
154
136
|
const total = row["total_value"] === undefined ? undefined : object(row["total_value"]);
|
|
@@ -179,8 +161,7 @@ export function threads(options) {
|
|
|
179
161
|
value,
|
|
180
162
|
unit: "count",
|
|
181
163
|
period,
|
|
182
|
-
|
|
183
|
-
...(totalValue === undefined && latestEnd !== undefined ? { measuredAt: latestEnd } : {}),
|
|
164
|
+
...definedFields({ measuredAt: totalValue === undefined ? latestEnd : undefined }),
|
|
184
165
|
fetchedAt: now(),
|
|
185
166
|
freshness: "unknown",
|
|
186
167
|
source: `threads:${version}`,
|
|
@@ -189,180 +170,123 @@ export function threads(options) {
|
|
|
189
170
|
});
|
|
190
171
|
}
|
|
191
172
|
async function status(id, c) {
|
|
192
|
-
return request(`${encodeURIComponent(id)}?fields=id,status,error_message`,
|
|
193
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
194
|
-
{ method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.container.status", c);
|
|
173
|
+
return request(`${encodeURIComponent(id)}?fields=id,status,error_message`, { method: "GET", ...definedFields({ signal: c.signal }) }, "threads.container.status", c);
|
|
195
174
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
199
|
-
// oxlint-disable-next-line anti-slop/no-chained-type-assertions -- provider payload is validated at this adapter boundary.
|
|
200
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
|
|
201
|
-
// oxlint-disable-next-line anti-slop/no-chained-type-assertions -- validated external boundary or fixture contract.
|
|
202
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
|
|
203
|
-
// oxlint-disable-next-line anti-slop/no-chained-type-assertions -- validated external boundary or fixture contract.
|
|
204
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
|
|
205
|
-
({
|
|
206
|
-
state,
|
|
207
|
-
targetIndex: 0,
|
|
208
|
-
account: a,
|
|
209
|
-
observedAt: now(),
|
|
210
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
211
|
-
...(backendState ? { backendState } : {}),
|
|
212
|
-
delivery: {
|
|
175
|
+
function outcomeFields(a, id, backendState) {
|
|
176
|
+
const delivery = {
|
|
213
177
|
kind: "delivery",
|
|
214
178
|
version: 1,
|
|
215
179
|
backend: a.backend,
|
|
216
180
|
platform: "threads",
|
|
217
181
|
accountId: a.accountId,
|
|
218
182
|
deliveryId: id,
|
|
219
|
-
}
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
targetIndex: 0,
|
|
186
|
+
account: a,
|
|
187
|
+
observedAt: now(),
|
|
188
|
+
...definedFields({ backendState: backendState === "" ? undefined : backendState }),
|
|
189
|
+
delivery,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const processing = (a, id, backendState) => ({
|
|
193
|
+
state: "processing",
|
|
194
|
+
...outcomeFields(a, id, backendState),
|
|
195
|
+
});
|
|
196
|
+
const published = (a, id, postId) => ({
|
|
197
|
+
state: "published",
|
|
198
|
+
...outcomeFields(a, id, "PUBLISHED"),
|
|
199
|
+
post: platformPostRef({ backend, platform: "threads", accountId: a.accountId, postId }),
|
|
220
200
|
});
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
201
|
+
const ambiguous = (a, id, backendState, diagnostic) => ({
|
|
202
|
+
state: "unknown",
|
|
203
|
+
...outcomeFields(a, id, backendState),
|
|
204
|
+
reason: "ambiguous-submission",
|
|
205
|
+
...definedFields({ diagnostic }),
|
|
206
|
+
});
|
|
207
|
+
const terminalFailure = (a, id, state) => ({
|
|
208
|
+
state: "failed",
|
|
209
|
+
...outcomeFields(a, id, state),
|
|
225
210
|
code: "media_error",
|
|
226
211
|
message: "Threads container failed or expired before publication.",
|
|
227
212
|
retryDisposition: { kind: "never" },
|
|
228
213
|
});
|
|
214
|
+
function replyControlParam(value) {
|
|
215
|
+
if (value === "accountsYouFollow")
|
|
216
|
+
return "accounts_you_follow";
|
|
217
|
+
if (value === "mentionedOnly")
|
|
218
|
+
return "mentioned_only";
|
|
219
|
+
return "everyone";
|
|
220
|
+
}
|
|
221
|
+
function parentMediaType(childCount, singleKind) {
|
|
222
|
+
if (childCount)
|
|
223
|
+
return "CAROUSEL";
|
|
224
|
+
if (singleKind === "video")
|
|
225
|
+
return "VIDEO";
|
|
226
|
+
if (singleKind === "image")
|
|
227
|
+
return "IMAGE";
|
|
228
|
+
return "TEXT";
|
|
229
|
+
}
|
|
229
230
|
async function resume(w, a, c) {
|
|
230
231
|
if (w.nativeId)
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
...base(a, w.id, "published", "PUBLISHED"),
|
|
234
|
-
post: platformPostRef({
|
|
235
|
-
backend,
|
|
236
|
-
platform: "threads",
|
|
237
|
-
accountId: a.accountId,
|
|
238
|
-
postId: w.nativeId,
|
|
239
|
-
}),
|
|
240
|
-
};
|
|
232
|
+
return published(a, w.id, w.nativeId);
|
|
241
233
|
if (w.stage === "unknown")
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
...base(a, w.id, "unknown", w.backendState),
|
|
245
|
-
reason: "ambiguous-submission",
|
|
246
|
-
diagnostic: "Threads publish acceptance is unknown; no replay was attempted.",
|
|
247
|
-
};
|
|
234
|
+
return ambiguous(a, w.id, w.backendState, "Threads publish acceptance is unknown; no replay was attempted.");
|
|
248
235
|
let cur = w;
|
|
249
|
-
const items =
|
|
236
|
+
const items = optionalArray(cur.options["_mediaItems"]) ?? [];
|
|
250
237
|
while (cur.childIds.length < items.length) {
|
|
251
238
|
const item = object(items[cur.childIds.length]);
|
|
239
|
+
const url = String(item["url"]);
|
|
240
|
+
const isVideo = item["kind"] === "video";
|
|
252
241
|
const p = new URLSearchParams({
|
|
253
|
-
media_type:
|
|
242
|
+
media_type: isVideo ? "VIDEO" : "IMAGE",
|
|
254
243
|
is_carousel_item: "true",
|
|
255
|
-
...(
|
|
256
|
-
?
|
|
257
|
-
:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
261
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
262
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
263
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
264
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
265
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
266
|
-
...(typeof item["altText"] === "string" ? { alt_text: item["altText"] } : {}),
|
|
244
|
+
...definedFields({
|
|
245
|
+
video_url: isVideo ? url : undefined,
|
|
246
|
+
image_url: isVideo ? undefined : url,
|
|
247
|
+
alt_text: optionalString(item["altText"]),
|
|
248
|
+
}),
|
|
267
249
|
});
|
|
268
250
|
await store.update(cur.id, { stage: "unknown", backendState: "CREATING_CHILD" });
|
|
269
|
-
const child = await request(`${a.accountId}/threads?${p}`,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
273
|
-
if (typeof child["id"] !== "string")
|
|
251
|
+
const child = await request(`${a.accountId}/threads?${p}`, { method: "POST", ...definedFields({ signal: c.signal }) }, "threads.container.child", c);
|
|
252
|
+
const childId = optionalString(child["id"]);
|
|
253
|
+
if (childId === undefined)
|
|
274
254
|
fail("threads.container.child", "Threads did not return a child container ID.");
|
|
275
255
|
cur = await store.update(cur.id, {
|
|
276
|
-
childIds: [...cur.childIds,
|
|
256
|
+
childIds: [...cur.childIds, childId],
|
|
277
257
|
stage: "children",
|
|
278
258
|
});
|
|
279
259
|
}
|
|
280
260
|
for (const id of cur.childIds) {
|
|
281
261
|
const s = await status(id, c);
|
|
282
|
-
|
|
283
|
-
const st = typeof s["status"] === "string" ? s["status"] : "";
|
|
262
|
+
const st = optionalString(s["status"]) ?? "";
|
|
284
263
|
if (st === "ERROR" || st === "EXPIRED")
|
|
285
264
|
return terminalFailure(a, w.id, st);
|
|
286
265
|
if (st !== "FINISHED" && st !== "PUBLISHED")
|
|
287
|
-
return
|
|
266
|
+
return processing(a, w.id, st || "PROCESSING");
|
|
288
267
|
}
|
|
289
268
|
if (!cur.parentId) {
|
|
290
269
|
const singleKind = cur.options["_mediaKind"];
|
|
291
|
-
const singleUrl = cur.options["_mediaUrl"];
|
|
270
|
+
const singleUrl = optionalString(cur.options["_mediaUrl"]);
|
|
271
|
+
const replyControl = optionalString(cur.options["replyControl"]);
|
|
292
272
|
const p = new URLSearchParams({
|
|
293
|
-
media_type: cur.childIds.length
|
|
294
|
-
|
|
295
|
-
:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
304
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
305
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
306
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
307
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
308
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
309
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
310
|
-
...(typeof singleUrl === "string"
|
|
311
|
-
? singleKind === "video"
|
|
312
|
-
? { video_url: singleUrl }
|
|
313
|
-
: { image_url: singleUrl }
|
|
314
|
-
: {}),
|
|
315
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
316
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
317
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
318
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
319
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
320
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
321
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
322
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
323
|
-
...(typeof cur.options["_altText"] === "string"
|
|
324
|
-
? { alt_text: cur.options["_altText"] }
|
|
325
|
-
: {}),
|
|
326
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
327
|
-
...(cur.caption ? { text: cur.caption } : {}),
|
|
328
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
329
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
330
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
331
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
332
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
333
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
334
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
335
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
336
|
-
...(typeof cur.options["replyControl"] === "string"
|
|
337
|
-
? {
|
|
338
|
-
reply_control: cur.options["replyControl"] === "accountsYouFollow"
|
|
339
|
-
? "accounts_you_follow"
|
|
340
|
-
: cur.options["replyControl"] === "mentionedOnly"
|
|
341
|
-
? "mentioned_only"
|
|
342
|
-
: "everyone",
|
|
343
|
-
}
|
|
344
|
-
: {}),
|
|
345
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
346
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
347
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
348
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
349
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
350
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
351
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
352
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
353
|
-
...(typeof cur.options["_replyToId"] === "string"
|
|
354
|
-
? { reply_to_id: cur.options["_replyToId"] }
|
|
355
|
-
: {}),
|
|
273
|
+
media_type: parentMediaType(cur.childIds.length, singleKind),
|
|
274
|
+
...definedFields({
|
|
275
|
+
children: cur.childIds.length ? cur.childIds.join(",") : undefined,
|
|
276
|
+
video_url: singleKind === "video" ? singleUrl : undefined,
|
|
277
|
+
image_url: singleKind === "video" ? undefined : singleUrl,
|
|
278
|
+
alt_text: optionalString(cur.options["_altText"]),
|
|
279
|
+
text: cur.caption === "" ? undefined : cur.caption,
|
|
280
|
+
reply_control: replyControl === undefined ? undefined : replyControlParam(replyControl),
|
|
281
|
+
reply_to_id: optionalString(cur.options["_replyToId"]),
|
|
282
|
+
}),
|
|
356
283
|
});
|
|
357
284
|
// Persist ambiguity before dispatch so a process crash cannot recreate a
|
|
358
285
|
// possibly accepted container under the same continuation handle.
|
|
359
286
|
await store.update(cur.id, { stage: "unknown", backendState: "CREATING_PARENT" });
|
|
360
|
-
const parent = await request(`${a.accountId}/threads?${p}`,
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const parentId = parent["id"];
|
|
364
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
365
|
-
if (typeof parentId !== "string")
|
|
287
|
+
const parent = await request(`${a.accountId}/threads?${p}`, { method: "POST", ...definedFields({ signal: c.signal }) }, "threads.container.create", c);
|
|
288
|
+
const parentId = optionalString(parent["id"]);
|
|
289
|
+
if (parentId === undefined)
|
|
366
290
|
fail("threads.container.create", "Threads did not return a container ID.");
|
|
367
291
|
cur = await store.update(cur.id, { parentId, stage: "parent" });
|
|
368
292
|
}
|
|
@@ -370,57 +294,30 @@ export function threads(options) {
|
|
|
370
294
|
if (!parentId)
|
|
371
295
|
fail("threads.container.create", "Missing parent container ID.");
|
|
372
296
|
const ps = await status(parentId, c);
|
|
373
|
-
|
|
374
|
-
const pst = typeof ps["status"] === "string" ? ps["status"] : "";
|
|
297
|
+
const pst = optionalString(ps["status"]) ?? "";
|
|
375
298
|
if (pst === "ERROR" || pst === "EXPIRED")
|
|
376
299
|
return terminalFailure(a, cur.id, pst);
|
|
377
300
|
if (pst === "PUBLISHED") {
|
|
378
301
|
await store.update(cur.id, { stage: "unknown", backendState: "PUBLISHED_WITHOUT_NATIVE_ID" });
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
...base(a, cur.id, "unknown", pst),
|
|
382
|
-
reason: "ambiguous-submission",
|
|
383
|
-
};
|
|
302
|
+
return ambiguous(a, cur.id, pst);
|
|
384
303
|
}
|
|
385
304
|
if (pst !== "FINISHED")
|
|
386
|
-
return
|
|
305
|
+
return processing(a, cur.id, pst || "PROCESSING");
|
|
387
306
|
await store.update(cur.id, { stage: "unknown", backendState: "PUBLISHING" });
|
|
388
307
|
try {
|
|
389
|
-
const pub = await request(`${a.accountId}/threads_publish?creation_id=${encodeURIComponent(parentId)}`,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const nativeId = pub["id"];
|
|
393
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
394
|
-
if (typeof nativeId !== "string") {
|
|
308
|
+
const pub = await request(`${a.accountId}/threads_publish?creation_id=${encodeURIComponent(parentId)}`, { method: "POST", ...definedFields({ signal: c.signal }) }, "threads.publish", c);
|
|
309
|
+
const nativeId = optionalString(pub["id"]);
|
|
310
|
+
if (nativeId === undefined) {
|
|
395
311
|
await store.update(cur.id, { stage: "unknown", backendState: "MISSING_ID" });
|
|
396
|
-
|
|
397
|
-
return {
|
|
398
|
-
...base(a, cur.id, "unknown", "MISSING_ID"),
|
|
399
|
-
reason: "ambiguous-submission",
|
|
400
|
-
diagnostic: "Threads accepted publish without a native post ID.",
|
|
401
|
-
};
|
|
312
|
+
return ambiguous(a, cur.id, "MISSING_ID", "Threads accepted publish without a native post ID.");
|
|
402
313
|
}
|
|
403
314
|
await store.update(cur.id, { nativeId, stage: "published", backendState: "PUBLISHED" });
|
|
404
|
-
|
|
405
|
-
return {
|
|
406
|
-
...base(a, cur.id, "published", "PUBLISHED"),
|
|
407
|
-
post: platformPostRef({
|
|
408
|
-
backend,
|
|
409
|
-
platform: "threads",
|
|
410
|
-
accountId: a.accountId,
|
|
411
|
-
postId: nativeId,
|
|
412
|
-
}),
|
|
413
|
-
};
|
|
315
|
+
return published(a, cur.id, nativeId);
|
|
414
316
|
}
|
|
415
317
|
catch (e) {
|
|
416
318
|
await store.update(cur.id, { stage: "unknown", backendState: "AMBIGUOUS" });
|
|
417
319
|
if (e instanceof SocialError)
|
|
418
|
-
|
|
419
|
-
return {
|
|
420
|
-
...base(a, cur.id, "unknown", "AMBIGUOUS"),
|
|
421
|
-
reason: "ambiguous-submission",
|
|
422
|
-
diagnostic: e.message,
|
|
423
|
-
};
|
|
320
|
+
return ambiguous(a, cur.id, "AMBIGUOUS", e.message);
|
|
424
321
|
throw e;
|
|
425
322
|
}
|
|
426
323
|
}
|
|
@@ -431,6 +328,7 @@ export function threads(options) {
|
|
|
431
328
|
runtime: ["node>=22.12", "bun"],
|
|
432
329
|
capabilities: [
|
|
433
330
|
{ operation: "accounts.read", platform: "threads", availability: "available" },
|
|
331
|
+
webhookCapability("threads", "Verifies Meta X-Hub-Signature-256 with the app secret and decodes replies, mentions, publish, and delete deliveries. Answer the GET handshake with answerMetaWebhookChallenge."),
|
|
434
332
|
{
|
|
435
333
|
operation: "posts.publish",
|
|
436
334
|
platform: "threads",
|
|
@@ -438,6 +336,12 @@ export function threads(options) {
|
|
|
438
336
|
formats: ["text", "image", "video", "carousel"],
|
|
439
337
|
requiredScopes: ["threads_basic", "threads_content_publish"],
|
|
440
338
|
},
|
|
339
|
+
{
|
|
340
|
+
operation: "posts.schedule",
|
|
341
|
+
platform: "threads",
|
|
342
|
+
availability: "unsupported-by-platform",
|
|
343
|
+
notes: "Threads publishing endpoints have no publish-time parameter; threads_publish publishes immediately.",
|
|
344
|
+
},
|
|
441
345
|
{ operation: "posts.read", platform: "threads", availability: "available" },
|
|
442
346
|
{ operation: "posts.list", platform: "threads", availability: "available" },
|
|
443
347
|
{ operation: "posts.status", platform: "threads", availability: "available" },
|
|
@@ -472,6 +376,13 @@ export function threads(options) {
|
|
|
472
376
|
requiredScopes: ["threads_manage_replies"],
|
|
473
377
|
notes: "Hiding replies and pending-reply moderation require Threads reply-management permissions.",
|
|
474
378
|
},
|
|
379
|
+
{
|
|
380
|
+
operation: "comments.delete",
|
|
381
|
+
platform: "threads",
|
|
382
|
+
availability: "available",
|
|
383
|
+
requiredScopes: ["threads_basic", "threads_delete"],
|
|
384
|
+
notes: "Deletes only replies published by the authenticated user. Threads allows 100 deletions per account per day. Use hideReply for other users' replies.",
|
|
385
|
+
},
|
|
475
386
|
{
|
|
476
387
|
operation: "posts.quote",
|
|
477
388
|
platform: "threads",
|
|
@@ -496,7 +407,20 @@ export function threads(options) {
|
|
|
496
407
|
availability: "available",
|
|
497
408
|
requiredScopes: ["threads_basic", "threads_delete"],
|
|
498
409
|
},
|
|
410
|
+
// Source, accessed 2026-09-24: https://developers.facebook.com/docs/threads/posts
|
|
411
|
+
{
|
|
412
|
+
operation: "posts.update",
|
|
413
|
+
platform: "threads",
|
|
414
|
+
availability: "unsupported-by-platform",
|
|
415
|
+
notes: "The Threads API has no endpoint to edit a published post; text and attachments are set when the container is created.",
|
|
416
|
+
},
|
|
499
417
|
{ operation: "profile.read", platform: "threads", availability: "available" },
|
|
418
|
+
{
|
|
419
|
+
operation: "profile.update",
|
|
420
|
+
platform: "threads",
|
|
421
|
+
availability: "unsupported-by-platform",
|
|
422
|
+
notes: "The Threads API exposes profile reads only. It has no profile write endpoint.",
|
|
423
|
+
},
|
|
500
424
|
{
|
|
501
425
|
operation: "search.keyword",
|
|
502
426
|
platform: "threads",
|
|
@@ -510,6 +434,24 @@ export function threads(options) {
|
|
|
510
434
|
requiredScopes: ["threads_basic"],
|
|
511
435
|
notes: "The profile endpoint reads the authorized app-scoped user; profile lookup requires threads_profile_discovery.",
|
|
512
436
|
},
|
|
437
|
+
{
|
|
438
|
+
// https://developers.facebook.com/docs/threads/threads-profiles and
|
|
439
|
+
// https://developers.facebook.com/docs/threads/reference/user (accessed 2026-09-24):
|
|
440
|
+
// profile_lookup requires an exact username match; there is no profile search endpoint.
|
|
441
|
+
operation: "profiles.search",
|
|
442
|
+
platform: "threads",
|
|
443
|
+
availability: "unsupported-by-platform",
|
|
444
|
+
notes: "Threads has no profile search endpoint. Profile discovery only looks up an exact username, which profiles.read covers.",
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
// https://developers.facebook.com/docs/threads/reference/user and
|
|
448
|
+
// https://developers.facebook.com/docs/threads/insights (accessed 2026-09-24): the user
|
|
449
|
+
// node has no followers or following edge; follower data is aggregate counts only.
|
|
450
|
+
operation: "graph.read",
|
|
451
|
+
platform: "threads",
|
|
452
|
+
availability: "unsupported-by-platform",
|
|
453
|
+
notes: "Threads does not expose follower or following lists. The followers_count metric is available through analytics.account.read.",
|
|
454
|
+
},
|
|
513
455
|
{
|
|
514
456
|
operation: "search.posts",
|
|
515
457
|
platform: "threads",
|
|
@@ -529,12 +471,16 @@ export function threads(options) {
|
|
|
529
471
|
availability: "unsupported-by-platform",
|
|
530
472
|
notes: "Threads does not expose messaging APIs to standard apps.",
|
|
531
473
|
},
|
|
474
|
+
{
|
|
475
|
+
operation: "notifications.read",
|
|
476
|
+
platform: "threads",
|
|
477
|
+
availability: "unsupported-by-platform",
|
|
478
|
+
notes: "The Threads API has no notifications endpoint. Use mentions.read, reply reads, or webhooks instead.",
|
|
479
|
+
},
|
|
532
480
|
],
|
|
533
481
|
};
|
|
534
482
|
const native = {
|
|
535
|
-
getPost: (id, c) => request(`${encodeURIComponent(id)}?fields=id,text,username,media_type,permalink`,
|
|
536
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
537
|
-
{ method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.posts.get", c),
|
|
483
|
+
getPost: (id, c) => request(`${encodeURIComponent(id)}?fields=id,text,username,media_type,permalink`, { method: "GET", ...definedFields({ signal: c.signal }) }, "threads.posts.get", c),
|
|
538
484
|
getContainer: (id, c) => status(id, c),
|
|
539
485
|
resumePublication: async (a, id, c) => {
|
|
540
486
|
authorize(a, "threads.posts.resume");
|
|
@@ -542,7 +488,7 @@ export function threads(options) {
|
|
|
542
488
|
if (!w || w.backend !== backend || w.accountId !== a.accountId)
|
|
543
489
|
fail("threads.posts.resume", "Workflow handle is not authorized for this account.", "unauthorized");
|
|
544
490
|
if (!(await store.claim(id)))
|
|
545
|
-
return
|
|
491
|
+
return processing(a, id, "CLAIMED");
|
|
546
492
|
try {
|
|
547
493
|
return await resume((await store.get(id)) ?? w, a, c);
|
|
548
494
|
}
|
|
@@ -607,6 +553,16 @@ export function threads(options) {
|
|
|
607
553
|
authorize(account, "threads.comments.moderate");
|
|
608
554
|
return request(`${encodeURIComponent(replyId)}/manage_reply?hide=${String(hide)}`, { method: "POST" }, "threads.comments.moderate", context);
|
|
609
555
|
},
|
|
556
|
+
async deleteComment({ account, commentId, context }) {
|
|
557
|
+
// Sources (accessed 2026-09-24): https://developers.facebook.com/docs/threads/posts/delete-posts
|
|
558
|
+
// and https://developers.facebook.com/docs/threads/retrieve-and-manage-replies/create-replies
|
|
559
|
+
authorize(account, "threads.comments.delete");
|
|
560
|
+
if (!/^\d+$/.test(commentId))
|
|
561
|
+
fail("threads.comments.delete", "Provide the numeric Threads reply media ID.", "invalid_input");
|
|
562
|
+
const result = await request(encodeURIComponent(commentId), { method: "DELETE" }, "threads.comments.delete", context);
|
|
563
|
+
if (result["success"] !== true)
|
|
564
|
+
fail("threads.comments.delete", "Threads did not confirm the reply deletion.");
|
|
565
|
+
},
|
|
610
566
|
async listConversation({ account, mediaId, cursor, context }) {
|
|
611
567
|
authorize(account, "threads.comments.conversation");
|
|
612
568
|
return request(`${encodeURIComponent(mediaId)}/conversation?fields=id,text,username,permalink,timestamp,is_reply,hide_status${cursor ? `&after=${encodeURIComponent(cursor)}` : ""}`, { method: "GET" }, "threads.comments.conversation", context);
|
|
@@ -631,6 +587,7 @@ export function threads(options) {
|
|
|
631
587
|
id: backend,
|
|
632
588
|
capabilities,
|
|
633
589
|
native,
|
|
590
|
+
webhooks: directWebhooks("threads", (input) => verifyMetaWebhook({ ...input, secret: options.webhookSecret ?? "" }), now),
|
|
634
591
|
accounts: {
|
|
635
592
|
list: async (_input, context) => ({ items: [await readAccount(context)] }),
|
|
636
593
|
get: async (ref, context) => {
|
|
@@ -643,7 +600,7 @@ export function threads(options) {
|
|
|
643
600
|
authorize(account, "profiles.read");
|
|
644
601
|
let result;
|
|
645
602
|
if (input.handle !== undefined) {
|
|
646
|
-
result = await request(`profile_lookup?username=${encodeURIComponent(input.handle)}&fields=
|
|
603
|
+
result = await request(`profile_lookup?username=${encodeURIComponent(input.handle)}&fields=username,name,profile_picture_url,biography,is_verified`, { method: "GET" }, "profiles.read", context);
|
|
647
604
|
}
|
|
648
605
|
else {
|
|
649
606
|
const profileId = input.profileId ?? account.accountId;
|
|
@@ -651,7 +608,7 @@ export function threads(options) {
|
|
|
651
608
|
fail("profiles.read", "Threads profile reads are limited to the authorized app-scoped user.", "unauthorized");
|
|
652
609
|
result = await request(`${encodeURIComponent(profileId)}?fields=id,username,name,threads_profile_picture_url,threads_biography,is_verified`, { method: "GET" }, "profiles.read", context);
|
|
653
610
|
}
|
|
654
|
-
const returnedProfileId = optionalString(result["id"]);
|
|
611
|
+
const returnedProfileId = input.handle === undefined ? optionalString(result["id"]) : undefined;
|
|
655
612
|
const profileId = returnedProfileId ??
|
|
656
613
|
input.profileId ??
|
|
657
614
|
(input.handle ? `lookup:${input.handle}` : undefined);
|
|
@@ -669,10 +626,7 @@ export function threads(options) {
|
|
|
669
626
|
accountId: account.accountId,
|
|
670
627
|
profileId,
|
|
671
628
|
}),
|
|
672
|
-
...(displayName
|
|
673
|
-
...(handle === undefined ? {} : { handle }),
|
|
674
|
-
...(avatarUrl === undefined ? {} : { avatarUrl }),
|
|
675
|
-
...(bio === undefined ? {} : { bio }),
|
|
629
|
+
...definedFields({ displayName, handle, avatarUrl, bio }),
|
|
676
630
|
native: returnedProfileId === undefined ? { ...result, _profileIdUnavailable: true } : result,
|
|
677
631
|
};
|
|
678
632
|
},
|
|
@@ -734,11 +688,13 @@ export function threads(options) {
|
|
|
734
688
|
add("media.format", "Threads images must be JPEG or PNG.");
|
|
735
689
|
if (item.kind === "video" && item.mimeType && item.mimeType !== "video/mp4")
|
|
736
690
|
add("media.format", "Threads videos must be MP4.");
|
|
691
|
+
if (item.kind === "document")
|
|
692
|
+
add("media.format", "Threads publishing accepts image and video media only.");
|
|
737
693
|
if (item.altText && item.altText.length > 1000)
|
|
738
694
|
add("media.alt_text", "Alt text is limited to 1,000 characters.");
|
|
739
695
|
}
|
|
740
696
|
if (target.options !== undefined) {
|
|
741
|
-
const o =
|
|
697
|
+
const o = optionsObject(target);
|
|
742
698
|
if (Object.keys(o).some((key) => key !== "replyControl"))
|
|
743
699
|
add("options.unmapped", "A supplied option has no Threads mapping.");
|
|
744
700
|
const replyControl = o["replyControl"];
|
|
@@ -756,32 +712,23 @@ export function threads(options) {
|
|
|
756
712
|
authorize(target.account, "threads.posts.publish");
|
|
757
713
|
const media = target.content.media ?? [];
|
|
758
714
|
const item = media.length === 1 ? media[0] : undefined;
|
|
759
|
-
|
|
715
|
+
const mediaItems = media.length > 1
|
|
716
|
+
? media.map((entry) => ({
|
|
717
|
+
kind: entry.kind,
|
|
718
|
+
url: entry.source.kind === "https-url" ? entry.source.url : "",
|
|
719
|
+
...definedFields({ altText: entry.altText === "" ? undefined : entry.altText }),
|
|
720
|
+
}))
|
|
721
|
+
: undefined;
|
|
722
|
+
const singleSource = item?.source.kind === "https-url" ? item.source : undefined;
|
|
760
723
|
const opts = {
|
|
761
|
-
|
|
762
|
-
...
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
?
|
|
768
|
-
|
|
769
|
-
kind: entry.kind,
|
|
770
|
-
url: entry.source.kind === "https-url" ? entry.source.url : "",
|
|
771
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
772
|
-
...(entry.altText ? { altText: entry.altText } : {}),
|
|
773
|
-
})),
|
|
774
|
-
}
|
|
775
|
-
: {}),
|
|
776
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
777
|
-
...(item?.source.kind === "https-url"
|
|
778
|
-
? {
|
|
779
|
-
_mediaKind: item.kind,
|
|
780
|
-
_mediaUrl: item.source.url,
|
|
781
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
782
|
-
...(item.altText ? { _altText: item.altText } : {}),
|
|
783
|
-
}
|
|
784
|
-
: {}),
|
|
724
|
+
...optionsObject(target),
|
|
725
|
+
...definedFields({
|
|
726
|
+
_replyToId: target.replyTo?.postId,
|
|
727
|
+
_mediaItems: mediaItems,
|
|
728
|
+
_mediaKind: singleSource === undefined ? undefined : item?.kind,
|
|
729
|
+
_mediaUrl: singleSource?.url,
|
|
730
|
+
_altText: singleSource === undefined || item?.altText === "" ? undefined : item?.altText,
|
|
731
|
+
}),
|
|
785
732
|
};
|
|
786
733
|
const w = await store.create({
|
|
787
734
|
backend,
|
|
@@ -791,9 +738,8 @@ export function threads(options) {
|
|
|
791
738
|
options: opts,
|
|
792
739
|
stage: "children",
|
|
793
740
|
});
|
|
794
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
795
741
|
if (!(await store.claim(w.id)))
|
|
796
|
-
return
|
|
742
|
+
return processing(target.account, w.id, "CLAIMED");
|
|
797
743
|
try {
|
|
798
744
|
try {
|
|
799
745
|
const outcome = await resume((await store.get(w.id)) ?? w, target.account, context);
|
|
@@ -802,10 +748,8 @@ export function threads(options) {
|
|
|
802
748
|
catch (error) {
|
|
803
749
|
if (error instanceof SocialError && error.code === "ambiguous_outcome")
|
|
804
750
|
return {
|
|
805
|
-
...
|
|
751
|
+
...ambiguous(target.account, w.id, "AMBIGUOUS", error.message),
|
|
806
752
|
targetIndex: target.targetIndex,
|
|
807
|
-
reason: "ambiguous-submission",
|
|
808
|
-
diagnostic: error.message,
|
|
809
753
|
};
|
|
810
754
|
throw error;
|
|
811
755
|
}
|
|
@@ -831,28 +775,15 @@ export function threads(options) {
|
|
|
831
775
|
const w = await store.get(ref.deliveryId);
|
|
832
776
|
if (!w || w.backend !== backend || w.accountId !== ref.accountId)
|
|
833
777
|
fail("threads.posts.status", "Workflow handle is not authorized for this account.", "unauthorized");
|
|
778
|
+
const owner = accountRef(backend, ref.accountId);
|
|
834
779
|
if (w.nativeId)
|
|
835
|
-
|
|
836
|
-
return {
|
|
837
|
-
...base(accountRef(backend, ref.accountId), w.id, "published", "PUBLISHED"),
|
|
838
|
-
post: platformPostRef({
|
|
839
|
-
backend,
|
|
840
|
-
platform: "threads",
|
|
841
|
-
accountId: ref.accountId,
|
|
842
|
-
postId: w.nativeId,
|
|
843
|
-
}),
|
|
844
|
-
};
|
|
780
|
+
return published(owner, w.id, w.nativeId);
|
|
845
781
|
if (w.stage === "unknown")
|
|
846
|
-
|
|
847
|
-
return {
|
|
848
|
-
...base(accountRef(backend, ref.accountId), w.id, "unknown", w.backendState),
|
|
849
|
-
reason: "ambiguous-submission",
|
|
850
|
-
diagnostic: "Explicit resumePublication is required.",
|
|
851
|
-
};
|
|
782
|
+
return ambiguous(owner, w.id, w.backendState, "Explicit resumePublication is required.");
|
|
852
783
|
if (!(await store.claim(w.id)))
|
|
853
|
-
return
|
|
784
|
+
return processing(owner, w.id, "CLAIMED");
|
|
854
785
|
try {
|
|
855
|
-
return await resume((await store.get(w.id)) ?? w,
|
|
786
|
+
return await resume((await store.get(w.id)) ?? w, owner, c);
|
|
856
787
|
}
|
|
857
788
|
finally {
|
|
858
789
|
await store.release?.(w.id);
|
|
@@ -863,35 +794,11 @@ export function threads(options) {
|
|
|
863
794
|
async list(post, input, context) {
|
|
864
795
|
authorize(post, "threads.comments.list");
|
|
865
796
|
const result = await request(`${encodeURIComponent(post.postId)}/replies?fields=id,text,username,timestamp${input.cursor ? `&after=${encodeURIComponent(input.cursor)}` : ""}`, { method: "GET" }, "threads.comments.list", context);
|
|
866
|
-
|
|
867
|
-
const
|
|
797
|
+
const rows = (optionalArray(result["data"]) ?? []).filter(isJsonObject);
|
|
798
|
+
const cursors = optionalObject(optionalObject(result["paging"])?.["cursors"]);
|
|
868
799
|
return {
|
|
869
800
|
items: rows,
|
|
870
|
-
|
|
871
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
872
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
873
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
874
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
875
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
876
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
877
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
878
|
-
...(typeof result["paging"] === "object" &&
|
|
879
|
-
result["paging"] &&
|
|
880
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
881
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
882
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
|
|
883
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
|
|
884
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
885
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
|
|
886
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
887
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
|
|
888
|
-
typeof result["paging"]["cursors"] === "object" &&
|
|
889
|
-
result["paging"]["cursors"] !== null &&
|
|
890
|
-
typeof result["paging"]["cursors"]["after"] === "string"
|
|
891
|
-
? {
|
|
892
|
-
nextCursor: String(result["paging"]["cursors"]["after"]),
|
|
893
|
-
}
|
|
894
|
-
: {}),
|
|
801
|
+
...definedFields({ nextCursor: optionalString(cursors?.["after"]) }),
|
|
895
802
|
};
|
|
896
803
|
},
|
|
897
804
|
async reply(comment, content, context) {
|
|
@@ -910,21 +817,16 @@ export function threads(options) {
|
|
|
910
817
|
getAccountMetrics,
|
|
911
818
|
async getPostMetrics(post, c) {
|
|
912
819
|
authorize(post, "threads.analytics");
|
|
913
|
-
const r = await request(`${encodeURIComponent(post.postId)}/insights?metric=views,likes,replies,reposts,quotes,shares,link_total_values`,
|
|
914
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
915
|
-
{ method: "GET", ...(c.signal ? { signal: c.signal } : {}) }, "threads.analytics", c);
|
|
820
|
+
const r = await request(`${encodeURIComponent(post.postId)}/insights?metric=views,likes,replies,reposts,quotes,shares,link_total_values`, { method: "GET", ...definedFields({ signal: c.signal }) }, "threads.analytics", c);
|
|
916
821
|
return array(r["data"]).flatMap((x) => {
|
|
917
822
|
const row = object(x);
|
|
918
|
-
const first =
|
|
919
|
-
const value =
|
|
920
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
921
|
-
first && typeof first === "object"
|
|
823
|
+
const first = optionalArray(row["values"])?.[0];
|
|
824
|
+
const value = isJsonObject(first) || isJsonArray(first)
|
|
922
825
|
? optionalNumber(object(first)["value"])
|
|
923
826
|
: optionalNumber(row["value"]);
|
|
924
|
-
const rawName = row["name"];
|
|
827
|
+
const rawName = optionalString(row["name"]);
|
|
925
828
|
const name = rawName === "link_total_values" ? "clicks" : rawName;
|
|
926
|
-
|
|
927
|
-
return value === undefined || typeof name !== "string"
|
|
829
|
+
return value === undefined || name === undefined
|
|
928
830
|
? []
|
|
929
831
|
: [
|
|
930
832
|
{
|