@codybrom/denim 1.3.4 → 1.3.5
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/deno.json +1 -1
- package/deno.lock +40 -1
- package/examples/edge-function.ts +56 -20
- package/mock_threads_api.ts +174 -0
- package/mod.ts +310 -143
- package/mod_test.ts +354 -471
- package/package.json +1 -1
- package/types.ts +235 -0
package/mod.ts
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
// mod.ts
|
|
2
|
+
import type {
|
|
3
|
+
ThreadsPostRequest,
|
|
4
|
+
PublishingLimit,
|
|
5
|
+
ThreadsPost,
|
|
6
|
+
ThreadsListResponse,
|
|
7
|
+
MockThreadsAPI,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
export type {
|
|
10
|
+
ThreadsPostRequest,
|
|
11
|
+
PublishingLimit,
|
|
12
|
+
ThreadsPost,
|
|
13
|
+
ThreadsListResponse,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves the mock API instance if available.
|
|
18
|
+
*
|
|
19
|
+
* @returns The mock API instance or null if not available
|
|
20
|
+
*/
|
|
21
|
+
function getAPI(): MockThreadsAPI | null {
|
|
22
|
+
return (globalThis as { threadsAPI?: MockThreadsAPI }).threadsAPI || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
1
25
|
/**
|
|
2
26
|
* @module
|
|
3
27
|
*
|
|
@@ -8,34 +32,6 @@
|
|
|
8
32
|
/** The base URL for the Threads API */
|
|
9
33
|
export const THREADS_API_BASE_URL = "https://graph.threads.net/v1.0";
|
|
10
34
|
|
|
11
|
-
/**
|
|
12
|
-
* Represents a request to post content on Threads.
|
|
13
|
-
*/
|
|
14
|
-
export interface ThreadsPostRequest {
|
|
15
|
-
/** The user ID of the Threads account */
|
|
16
|
-
userId: string;
|
|
17
|
-
/** The access token for authentication */
|
|
18
|
-
accessToken: string;
|
|
19
|
-
/** The type of media being posted */
|
|
20
|
-
mediaType: "TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL";
|
|
21
|
-
/** The text content of the post (optional) */
|
|
22
|
-
text?: string;
|
|
23
|
-
/** The URL of the image to be posted (optional, for IMAGE type) */
|
|
24
|
-
imageUrl?: string;
|
|
25
|
-
/** The URL of the video to be posted (optional, for VIDEO type) */
|
|
26
|
-
videoUrl?: string;
|
|
27
|
-
/** The accessibility text for the image or video (optional) */
|
|
28
|
-
altText?: string;
|
|
29
|
-
/** The URL to be attached as a link to the post (optional, for text posts only) */
|
|
30
|
-
linkAttachment?: string;
|
|
31
|
-
/** List of country codes where the post should be visible (optional - requires special API access) */
|
|
32
|
-
allowlistedCountryCodes?: string[];
|
|
33
|
-
/** Controls who can reply to the post (optional) */
|
|
34
|
-
replyControl?: "everyone" | "accounts_you_follow" | "mentioned_only";
|
|
35
|
-
/** Array of carousel item IDs (required for CAROUSEL type, not applicable for other types) */
|
|
36
|
-
children?: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
35
|
/**
|
|
40
36
|
* Creates a Threads media container.
|
|
41
37
|
*
|
|
@@ -58,65 +54,82 @@ export interface ThreadsPostRequest {
|
|
|
58
54
|
*/
|
|
59
55
|
export async function createThreadsContainer(
|
|
60
56
|
request: ThreadsPostRequest
|
|
61
|
-
): Promise<string> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const body = new URLSearchParams({
|
|
67
|
-
access_token: request.accessToken,
|
|
68
|
-
media_type: request.mediaType,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Add common optional parameters
|
|
72
|
-
if (request.text) body.append("text", request.text);
|
|
73
|
-
if (request.altText) body.append("alt_text", request.altText);
|
|
74
|
-
if (request.replyControl) body.append("reply_control", request.replyControl);
|
|
75
|
-
if (request.allowlistedCountryCodes) {
|
|
76
|
-
body.append(
|
|
77
|
-
"allowlisted_country_codes",
|
|
78
|
-
request.allowlistedCountryCodes.join(",")
|
|
79
|
-
);
|
|
57
|
+
): Promise<string | { id: string; permalink: string }> {
|
|
58
|
+
const api = getAPI();
|
|
59
|
+
if (api) {
|
|
60
|
+
// Use mock API
|
|
61
|
+
return api.createThreadsContainer(request);
|
|
80
62
|
}
|
|
63
|
+
try {
|
|
64
|
+
// Input validation
|
|
65
|
+
validateRequest(request);
|
|
66
|
+
|
|
67
|
+
const url = `${THREADS_API_BASE_URL}/${request.userId}/threads`;
|
|
68
|
+
const body = new URLSearchParams({
|
|
69
|
+
access_token: request.accessToken,
|
|
70
|
+
media_type: request.mediaType,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Add common optional parameters
|
|
74
|
+
if (request.text) body.append("text", request.text);
|
|
75
|
+
if (request.altText) body.append("alt_text", request.altText);
|
|
76
|
+
if (request.replyControl)
|
|
77
|
+
body.append("reply_control", request.replyControl);
|
|
78
|
+
if (request.allowlistedCountryCodes) {
|
|
79
|
+
body.append(
|
|
80
|
+
"allowlisted_country_codes",
|
|
81
|
+
request.allowlistedCountryCodes.join(",")
|
|
82
|
+
);
|
|
83
|
+
}
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
// Handle media type specific parameters
|
|
86
|
+
if (request.mediaType === "VIDEO" && request.videoUrl) {
|
|
87
|
+
const videoItemId = await createVideoItemContainer(request);
|
|
88
|
+
body.set("media_type", "CAROUSEL");
|
|
89
|
+
body.append("children", videoItemId);
|
|
90
|
+
} else if (request.mediaType === "IMAGE" && request.imageUrl) {
|
|
91
|
+
body.append("image_url", request.imageUrl);
|
|
92
|
+
} else if (request.mediaType === "TEXT" && request.linkAttachment) {
|
|
93
|
+
body.append("link_attachment", request.linkAttachment);
|
|
94
|
+
} else if (request.mediaType === "CAROUSEL" && request.children) {
|
|
95
|
+
body.append("children", request.children.join(","));
|
|
96
|
+
}
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
console.log(`Sending request to: ${url}`);
|
|
99
|
+
console.log(`Request body: ${body.toString()}`);
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const response = await fetch(url, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: body,
|
|
104
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
105
|
+
});
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
console.log(`Response status: ${response.status} ${response.statusText}`);
|
|
106
|
-
console.log(`Response body: ${responseText}`);
|
|
107
|
+
const responseText = await response.text();
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
|
|
109
|
+
console.log(`Response status: ${response.status} ${response.statusText}`);
|
|
110
|
+
console.log(`Response body: ${responseText}`);
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(`Internal Server Error. Details: ${responseText}`);
|
|
114
|
+
}
|
|
113
115
|
|
|
114
|
-
try {
|
|
115
116
|
const data = JSON.parse(responseText);
|
|
116
|
-
|
|
117
|
+
|
|
118
|
+
// If getPermalink is true, fetch the permalink
|
|
119
|
+
if (request.getPermalink) {
|
|
120
|
+
const threadData = await getSingleThread(data.id, request.accessToken);
|
|
121
|
+
return {
|
|
122
|
+
id: data.id,
|
|
123
|
+
permalink: threadData.permalink || "",
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
return data.id;
|
|
127
|
+
}
|
|
117
128
|
} catch (error) {
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
// Access error message safely
|
|
130
|
+
const errorMessage =
|
|
131
|
+
error instanceof Error ? error.message : "Unknown Error";
|
|
132
|
+
throw new Error(`Failed to create Threads container: ${errorMessage}`);
|
|
120
133
|
}
|
|
121
134
|
}
|
|
122
135
|
|
|
@@ -168,6 +181,7 @@ function validateRequest(request: ThreadsPostRequest): void {
|
|
|
168
181
|
|
|
169
182
|
/**
|
|
170
183
|
* Creates a video item container for Threads.
|
|
184
|
+
*
|
|
171
185
|
* @param request - The ThreadsPostRequest object containing video post details
|
|
172
186
|
* @returns A Promise that resolves to the video item container ID
|
|
173
187
|
* @throws Will throw an error if the API request fails
|
|
@@ -242,7 +256,12 @@ export async function createCarouselItem(
|
|
|
242
256
|
request: Omit<ThreadsPostRequest, "mediaType"> & {
|
|
243
257
|
mediaType: "IMAGE" | "VIDEO";
|
|
244
258
|
}
|
|
245
|
-
): Promise<string> {
|
|
259
|
+
): Promise<string | { id: string }> {
|
|
260
|
+
const api = getAPI();
|
|
261
|
+
if (api) {
|
|
262
|
+
// Use mock API
|
|
263
|
+
return api.createCarouselItem(request);
|
|
264
|
+
}
|
|
246
265
|
if (request.mediaType !== "IMAGE" && request.mediaType !== "VIDEO") {
|
|
247
266
|
throw new Error("Carousel items must be either IMAGE or VIDEO type");
|
|
248
267
|
}
|
|
@@ -292,6 +311,7 @@ export async function createCarouselItem(
|
|
|
292
311
|
|
|
293
312
|
/**
|
|
294
313
|
* Checks the status of a Threads container.
|
|
314
|
+
*
|
|
295
315
|
* @param containerId - The ID of the container to check
|
|
296
316
|
* @param accessToken - The access token for authentication
|
|
297
317
|
* @returns A Promise that resolves to the container status
|
|
@@ -329,56 +349,91 @@ async function checkContainerStatus(
|
|
|
329
349
|
export async function publishThreadsContainer(
|
|
330
350
|
userId: string,
|
|
331
351
|
accessToken: string,
|
|
332
|
-
containerId: string
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
headers: {
|
|
344
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
if (!publishResponse.ok) {
|
|
349
|
-
throw new Error(
|
|
350
|
-
`Failed to publish Threads container: ${publishResponse.statusText}`
|
|
352
|
+
containerId: string,
|
|
353
|
+
getPermalink: boolean = false
|
|
354
|
+
): Promise<string | { id: string; permalink: string }> {
|
|
355
|
+
const api = getAPI();
|
|
356
|
+
if (api) {
|
|
357
|
+
// Use mock API
|
|
358
|
+
return api.publishThreadsContainer(
|
|
359
|
+
userId,
|
|
360
|
+
accessToken,
|
|
361
|
+
containerId,
|
|
362
|
+
getPermalink
|
|
351
363
|
);
|
|
352
364
|
}
|
|
365
|
+
try {
|
|
366
|
+
const publishUrl = `${THREADS_API_BASE_URL}/${userId}/threads_publish`;
|
|
367
|
+
const publishBody = new URLSearchParams({
|
|
368
|
+
access_token: accessToken,
|
|
369
|
+
creation_id: containerId,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const publishResponse = await fetch(publishUrl, {
|
|
373
|
+
method: "POST",
|
|
374
|
+
body: publishBody,
|
|
375
|
+
headers: {
|
|
376
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (!publishResponse.ok) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Failed to publish Threads container: ${publishResponse.statusText}`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
353
385
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
386
|
+
const publishData = await publishResponse.json();
|
|
387
|
+
|
|
388
|
+
if (getPermalink) {
|
|
389
|
+
const mediaId = publishData.id;
|
|
390
|
+
const permalinkUrl = `${THREADS_API_BASE_URL}/${mediaId}?fields=permalink&access_token=${accessToken}`;
|
|
391
|
+
const permalinkResponse = await fetch(permalinkUrl);
|
|
392
|
+
|
|
393
|
+
if (permalinkResponse.ok) {
|
|
394
|
+
const permalinkData = await permalinkResponse.json();
|
|
395
|
+
return {
|
|
396
|
+
id: mediaId,
|
|
397
|
+
permalink: permalinkData.permalink,
|
|
398
|
+
};
|
|
399
|
+
} else {
|
|
400
|
+
throw new Error("Failed to fetch permalink");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
358
403
|
|
|
359
|
-
|
|
360
|
-
status
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
404
|
+
// Check container status
|
|
405
|
+
let status = await checkContainerStatus(containerId, accessToken);
|
|
406
|
+
let attempts = 0;
|
|
407
|
+
const maxAttempts = 5;
|
|
408
|
+
|
|
409
|
+
while (
|
|
410
|
+
status !== "PUBLISHED" &&
|
|
411
|
+
status !== "FINISHED" &&
|
|
412
|
+
attempts < maxAttempts
|
|
413
|
+
) {
|
|
414
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second
|
|
415
|
+
status = await checkContainerStatus(containerId, accessToken);
|
|
416
|
+
attempts++;
|
|
417
|
+
}
|
|
368
418
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
419
|
+
if (status === "ERROR") {
|
|
420
|
+
throw new Error(`Failed to publish container. Error: ${status}`);
|
|
421
|
+
}
|
|
372
422
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
423
|
+
if (status !== "PUBLISHED" && status !== "FINISHED") {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`Container not published after ${maxAttempts} attempts. Current status: ${status}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
378
428
|
|
|
379
|
-
|
|
429
|
+
return publishData.id;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (error instanceof Error) {
|
|
432
|
+
throw new Error(`Failed to publish Threads container: ${error.message}`);
|
|
433
|
+
}
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
380
436
|
}
|
|
381
|
-
|
|
382
437
|
/**
|
|
383
438
|
* Serves HTTP requests to create and publish Threads posts.
|
|
384
439
|
*
|
|
@@ -386,6 +441,8 @@ export async function publishThreadsContainer(
|
|
|
386
441
|
* containing ThreadsPostRequest data. It creates a container and
|
|
387
442
|
* immediately publishes it.
|
|
388
443
|
*
|
|
444
|
+
* @throws Will throw an error if the request is invalid or if there's an error during processing
|
|
445
|
+
*
|
|
389
446
|
* @example
|
|
390
447
|
* ```typescript
|
|
391
448
|
* // Start the server
|
|
@@ -393,6 +450,8 @@ export async function publishThreadsContainer(
|
|
|
393
450
|
* ```
|
|
394
451
|
*/
|
|
395
452
|
export function serveRequests() {
|
|
453
|
+
const api = getAPI();
|
|
454
|
+
|
|
396
455
|
Deno.serve(async (req) => {
|
|
397
456
|
if (req.method !== "POST") {
|
|
398
457
|
return new Response("Method Not Allowed", { status: 405 });
|
|
@@ -416,17 +475,47 @@ export function serveRequests() {
|
|
|
416
475
|
);
|
|
417
476
|
}
|
|
418
477
|
|
|
419
|
-
|
|
420
|
-
|
|
478
|
+
let containerResult;
|
|
479
|
+
let publishResult;
|
|
480
|
+
|
|
481
|
+
if (api) {
|
|
482
|
+
// Use mock API
|
|
483
|
+
containerResult = await api.createThreadsContainer(requestData);
|
|
484
|
+
publishResult = await api.publishThreadsContainer(
|
|
485
|
+
requestData.userId,
|
|
486
|
+
requestData.accessToken,
|
|
487
|
+
typeof containerResult === "string"
|
|
488
|
+
? containerResult
|
|
489
|
+
: containerResult.id,
|
|
490
|
+
requestData.getPermalink
|
|
491
|
+
);
|
|
492
|
+
} else {
|
|
493
|
+
// Use real API calls
|
|
494
|
+
containerResult = await createThreadsContainer(requestData);
|
|
495
|
+
if (typeof containerResult === "string") {
|
|
496
|
+
publishResult = await publishThreadsContainer(
|
|
497
|
+
requestData.userId,
|
|
498
|
+
requestData.accessToken,
|
|
499
|
+
containerResult,
|
|
500
|
+
requestData.getPermalink
|
|
501
|
+
);
|
|
502
|
+
} else {
|
|
503
|
+
publishResult = containerResult;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
421
506
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
507
|
+
let responseData;
|
|
508
|
+
if (typeof publishResult === "string") {
|
|
509
|
+
responseData = { success: true, publishedId: publishResult };
|
|
510
|
+
} else {
|
|
511
|
+
responseData = {
|
|
512
|
+
success: true,
|
|
513
|
+
publishedId: publishResult.id,
|
|
514
|
+
permalink: publishResult.permalink,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
428
517
|
|
|
429
|
-
return new Response(JSON.stringify(
|
|
518
|
+
return new Response(JSON.stringify(responseData), {
|
|
430
519
|
headers: { "Content-Type": "application/json" },
|
|
431
520
|
});
|
|
432
521
|
} catch (error) {
|
|
@@ -462,13 +551,12 @@ export function serveRequests() {
|
|
|
462
551
|
export async function getPublishingLimit(
|
|
463
552
|
userId: string,
|
|
464
553
|
accessToken: string
|
|
465
|
-
): Promise<{
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
}> {
|
|
554
|
+
): Promise<PublishingLimit> {
|
|
555
|
+
const api = getAPI();
|
|
556
|
+
if (api) {
|
|
557
|
+
// Use mock API
|
|
558
|
+
return api.getPublishingLimit(userId, accessToken);
|
|
559
|
+
}
|
|
472
560
|
const url = `${THREADS_API_BASE_URL}/${userId}/threads_publishing_limit`;
|
|
473
561
|
const params = new URLSearchParams({
|
|
474
562
|
access_token: accessToken,
|
|
@@ -476,15 +564,94 @@ export async function getPublishingLimit(
|
|
|
476
564
|
});
|
|
477
565
|
|
|
478
566
|
const response = await fetch(`${url}?${params}`);
|
|
567
|
+
if (!response.ok) {
|
|
568
|
+
throw new Error(`Failed to get publishing limit: ${response.statusText}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
479
571
|
const data = await response.json();
|
|
572
|
+
return data.data[0];
|
|
573
|
+
}
|
|
480
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Retrieves a list of all threads created by a user.
|
|
577
|
+
*
|
|
578
|
+
* @param userId - The user ID of the Threads account
|
|
579
|
+
* @param accessToken - The access token for authentication
|
|
580
|
+
* @param options - Optional parameters for the request
|
|
581
|
+
* @param options.since - Start date for fetching threads (ISO 8601 format)
|
|
582
|
+
* @param options.until - End date for fetching threads (ISO 8601 format)
|
|
583
|
+
* @param options.limit - Maximum number of threads to return
|
|
584
|
+
* @param options.after - Cursor for pagination (next page)
|
|
585
|
+
* @param options.before - Cursor for pagination (previous page)
|
|
586
|
+
* @returns A Promise that resolves to the ThreadsListResponse
|
|
587
|
+
* @throws Will throw an error if the API request fails
|
|
588
|
+
*/
|
|
589
|
+
export async function getThreadsList(
|
|
590
|
+
userId: string,
|
|
591
|
+
accessToken: string,
|
|
592
|
+
options?: {
|
|
593
|
+
since?: string;
|
|
594
|
+
until?: string;
|
|
595
|
+
limit?: number;
|
|
596
|
+
after?: string;
|
|
597
|
+
before?: string;
|
|
598
|
+
}
|
|
599
|
+
): Promise<ThreadsListResponse> {
|
|
600
|
+
const api = getAPI();
|
|
601
|
+
if (api) {
|
|
602
|
+
// Use mock API
|
|
603
|
+
return api.getThreadsList(userId, accessToken, options);
|
|
604
|
+
}
|
|
605
|
+
const fields =
|
|
606
|
+
"id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post";
|
|
607
|
+
const url = new URL(`${THREADS_API_BASE_URL}/${userId}/threads`);
|
|
608
|
+
url.searchParams.append("fields", fields);
|
|
609
|
+
url.searchParams.append("access_token", accessToken);
|
|
610
|
+
|
|
611
|
+
if (options) {
|
|
612
|
+
if (options.since) url.searchParams.append("since", options.since);
|
|
613
|
+
if (options.until) url.searchParams.append("until", options.until);
|
|
614
|
+
if (options.limit)
|
|
615
|
+
url.searchParams.append("limit", options.limit.toString());
|
|
616
|
+
if (options.after) url.searchParams.append("after", options.after);
|
|
617
|
+
if (options.before) url.searchParams.append("before", options.before);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const response = await fetch(url.toString());
|
|
481
621
|
if (!response.ok) {
|
|
482
|
-
throw new Error(
|
|
483
|
-
`Failed to get publishing limit: ${
|
|
484
|
-
data.error?.message || response.statusText
|
|
485
|
-
}`
|
|
486
|
-
);
|
|
622
|
+
throw new Error(`Failed to retrieve threads list: ${response.statusText}`);
|
|
487
623
|
}
|
|
488
624
|
|
|
489
|
-
return
|
|
625
|
+
return await response.json();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Retrieves a single Threads media object.
|
|
630
|
+
*
|
|
631
|
+
* @param mediaId - The ID of the Threads media object
|
|
632
|
+
* @param accessToken - The access token for authentication
|
|
633
|
+
* @returns A Promise that resolves to the ThreadsPost object
|
|
634
|
+
* @throws Will throw an error if the API request fails
|
|
635
|
+
*/
|
|
636
|
+
export async function getSingleThread(
|
|
637
|
+
mediaId: string,
|
|
638
|
+
accessToken: string
|
|
639
|
+
): Promise<ThreadsPost> {
|
|
640
|
+
const api = getAPI();
|
|
641
|
+
if (api) {
|
|
642
|
+
// Use mock API
|
|
643
|
+
return api.getSingleThread(mediaId, accessToken);
|
|
644
|
+
}
|
|
645
|
+
const fields =
|
|
646
|
+
"id,media_product_type,media_type,media_url,permalink,owner,username,text,timestamp,shortcode,thumbnail_url,children,is_quote_post";
|
|
647
|
+
const url = new URL(`${THREADS_API_BASE_URL}/${mediaId}`);
|
|
648
|
+
url.searchParams.append("fields", fields);
|
|
649
|
+
url.searchParams.append("access_token", accessToken);
|
|
650
|
+
|
|
651
|
+
const response = await fetch(url.toString());
|
|
652
|
+
if (!response.ok) {
|
|
653
|
+
throw new Error(`Failed to retrieve thread: ${response.statusText}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return await response.json();
|
|
490
657
|
}
|