@codybrom/denim 1.3.1 → 1.3.3
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/mod.ts +92 -61
- package/package.json +2 -2
package/deno.json
CHANGED
package/mod.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface ThreadsPostRequest {
|
|
|
40
40
|
* Creates a Threads media container.
|
|
41
41
|
*
|
|
42
42
|
* @param request - The ThreadsPostRequest object containing post details
|
|
43
|
-
* @returns A Promise that resolves to the container ID
|
|
43
|
+
* @returns A Promise that resolves to an object containing the container ID and media type
|
|
44
44
|
* @throws Will throw an error if the API request fails
|
|
45
45
|
*
|
|
46
46
|
* @example
|
|
@@ -53,12 +53,12 @@ export interface ThreadsPostRequest {
|
|
|
53
53
|
* videoUrl: "https://example.com/video.mp4",
|
|
54
54
|
* altText: "A cool video"
|
|
55
55
|
* };
|
|
56
|
-
* const containerId = await createThreadsContainer(request);
|
|
56
|
+
* const { containerId, mediaType } = await createThreadsContainer(request);
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
59
|
export async function createThreadsContainer(
|
|
60
60
|
request: ThreadsPostRequest
|
|
61
|
-
): Promise<string> {
|
|
61
|
+
): Promise<{ containerId: number; mediaType: string }> {
|
|
62
62
|
// Input validation
|
|
63
63
|
validateRequest(request);
|
|
64
64
|
|
|
@@ -70,6 +70,7 @@ export async function createThreadsContainer(
|
|
|
70
70
|
|
|
71
71
|
// Add common optional parameters
|
|
72
72
|
if (request.text) body.append("text", request.text);
|
|
73
|
+
if (request.altText) body.append("alt_text", request.altText);
|
|
73
74
|
if (request.replyControl) body.append("reply_control", request.replyControl);
|
|
74
75
|
if (request.allowlistedCountryCodes) {
|
|
75
76
|
body.append(
|
|
@@ -79,29 +80,14 @@ export async function createThreadsContainer(
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
// Handle media type specific parameters
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (!request.imageUrl)
|
|
91
|
-
throw new Error("imageUrl is required for IMAGE media type");
|
|
92
|
-
body.append("image_url", request.imageUrl);
|
|
93
|
-
if (request.altText) body.append("alt_text", request.altText);
|
|
94
|
-
break;
|
|
95
|
-
case "TEXT":
|
|
96
|
-
if (request.linkAttachment)
|
|
97
|
-
body.append("link_attachment", request.linkAttachment);
|
|
98
|
-
break;
|
|
99
|
-
case "CAROUSEL":
|
|
100
|
-
if (!request.children || request.children.length < 2) {
|
|
101
|
-
throw new Error("CAROUSEL media type requires at least 2 children");
|
|
102
|
-
}
|
|
103
|
-
body.append("children", request.children.join(","));
|
|
104
|
-
break;
|
|
83
|
+
if (request.mediaType === "IMAGE" && request.imageUrl) {
|
|
84
|
+
body.append("image_url", request.imageUrl);
|
|
85
|
+
} else if (request.mediaType === "VIDEO" && request.videoUrl) {
|
|
86
|
+
body.append("video_url", request.videoUrl);
|
|
87
|
+
} else if (request.mediaType === "TEXT" && request.linkAttachment) {
|
|
88
|
+
body.append("link_attachment", request.linkAttachment);
|
|
89
|
+
} else if (request.mediaType === "CAROUSEL" && request.children) {
|
|
90
|
+
body.append("children", request.children.join(","));
|
|
105
91
|
}
|
|
106
92
|
|
|
107
93
|
console.log(`Sending request to: ${url}`);
|
|
@@ -125,7 +111,7 @@ export async function createThreadsContainer(
|
|
|
125
111
|
|
|
126
112
|
try {
|
|
127
113
|
const data = JSON.parse(responseText);
|
|
128
|
-
return data.id;
|
|
114
|
+
return { containerId: parseInt(data.id, 10), mediaType: request.mediaType };
|
|
129
115
|
} catch (error) {
|
|
130
116
|
console.error(`Failed to parse response JSON: ${error}`);
|
|
131
117
|
throw new Error(`Invalid response from Threads API: ${responseText}`);
|
|
@@ -161,6 +147,9 @@ function validateRequest(request: ThreadsPostRequest): void {
|
|
|
161
147
|
if (request.mediaType !== "IMAGE" && request.imageUrl) {
|
|
162
148
|
throw new Error("imageUrl can only be used with IMAGE media type");
|
|
163
149
|
}
|
|
150
|
+
if (request.mediaType !== "VIDEO" && request.videoUrl) {
|
|
151
|
+
throw new Error("videoUrl can only be used with VIDEO media type");
|
|
152
|
+
}
|
|
164
153
|
if (request.mediaType !== "TEXT" && request.linkAttachment) {
|
|
165
154
|
throw new Error("linkAttachment can only be used with TEXT media type");
|
|
166
155
|
}
|
|
@@ -283,24 +272,52 @@ async function checkContainerStatus(
|
|
|
283
272
|
*
|
|
284
273
|
* @param userId - The user ID of the Threads account
|
|
285
274
|
* @param accessToken - The access token for authentication
|
|
286
|
-
* @param containerId - The ID of the container to publish
|
|
275
|
+
* @param containerId - The ID of the container to publish (as a number)
|
|
276
|
+
* @param mediaType - The type of media being published
|
|
287
277
|
* @returns A Promise that resolves to the published container ID
|
|
288
|
-
* @throws Will throw an error if the API request fails
|
|
278
|
+
* @throws Will throw an error if the API request fails, if publishing times out, or if the container is not ready (for videos)
|
|
289
279
|
*
|
|
290
280
|
* @example
|
|
291
281
|
* ```typescript
|
|
292
|
-
* const
|
|
282
|
+
* const { containerId, mediaType } = await createThreadsContainer(request);
|
|
283
|
+
* const publishedId = await publishThreadsContainer("123456", "your_access_token", parseInt(containerId, 10), mediaType);
|
|
293
284
|
* ```
|
|
294
285
|
*/
|
|
295
286
|
export async function publishThreadsContainer(
|
|
296
287
|
userId: string,
|
|
297
288
|
accessToken: string,
|
|
298
|
-
containerId:
|
|
299
|
-
|
|
289
|
+
containerId: number,
|
|
290
|
+
mediaType: string
|
|
291
|
+
): Promise<number> {
|
|
292
|
+
if (mediaType === "VIDEO") {
|
|
293
|
+
// Check container status before publishing for videos
|
|
294
|
+
let status = await checkContainerStatus(
|
|
295
|
+
containerId.toString(),
|
|
296
|
+
accessToken
|
|
297
|
+
);
|
|
298
|
+
let attempts = 0;
|
|
299
|
+
const maxAttempts = 10; // Increased from 5 to 10 for videos
|
|
300
|
+
|
|
301
|
+
while (status !== "FINISHED" && attempts < maxAttempts) {
|
|
302
|
+
await new Promise((resolve) => setTimeout(resolve, 30000)); // Wait for 30 seconds
|
|
303
|
+
status = await checkContainerStatus(containerId.toString(), accessToken);
|
|
304
|
+
attempts++;
|
|
305
|
+
console.log(
|
|
306
|
+
`Video container status check attempt ${attempts}: ${status}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (status !== "FINISHED") {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Video container not ready after ${maxAttempts} attempts. Current status: ${status}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
300
317
|
const publishUrl = `${THREADS_API_BASE_URL}/${userId}/threads_publish`;
|
|
301
318
|
const publishBody = new URLSearchParams({
|
|
302
319
|
access_token: accessToken,
|
|
303
|
-
creation_id: containerId,
|
|
320
|
+
creation_id: containerId.toString(), // Convert to string for URLSearchParams, but it will be sent as a number
|
|
304
321
|
});
|
|
305
322
|
|
|
306
323
|
const publishResponse = await fetch(publishUrl, {
|
|
@@ -311,35 +328,43 @@ export async function publishThreadsContainer(
|
|
|
311
328
|
},
|
|
312
329
|
});
|
|
313
330
|
|
|
331
|
+
const responseText = await publishResponse.text();
|
|
332
|
+
|
|
314
333
|
if (!publishResponse.ok) {
|
|
334
|
+
console.error(`Publish response body: ${responseText}`);
|
|
315
335
|
throw new Error(
|
|
316
|
-
`Failed to publish Threads container: ${publishResponse.statusText}`
|
|
336
|
+
`Failed to publish Threads container: ${publishResponse.statusText}. Details: ${responseText}`
|
|
317
337
|
);
|
|
318
338
|
}
|
|
319
339
|
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
340
|
+
// For non-video posts, we still need to check if the post was actually published
|
|
341
|
+
if (mediaType !== "VIDEO") {
|
|
342
|
+
let status = await checkContainerStatus(
|
|
343
|
+
containerId.toString(),
|
|
344
|
+
accessToken
|
|
345
|
+
);
|
|
346
|
+
let attempts = 0;
|
|
347
|
+
const maxAttempts = 5;
|
|
348
|
+
|
|
349
|
+
while (
|
|
350
|
+
status !== "PUBLISHED" &&
|
|
351
|
+
status !== "FINISHED" &&
|
|
352
|
+
attempts < maxAttempts
|
|
353
|
+
) {
|
|
354
|
+
await new Promise((resolve) => setTimeout(resolve, 60000)); // Wait for 1 minute
|
|
355
|
+
status = await checkContainerStatus(containerId.toString(), accessToken);
|
|
356
|
+
attempts++;
|
|
357
|
+
}
|
|
334
358
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
359
|
+
if (status === "ERROR") {
|
|
360
|
+
throw new Error(`Failed to publish container. Error: ${status}`);
|
|
361
|
+
}
|
|
338
362
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
363
|
+
if (status !== "PUBLISHED" && status !== "FINISHED") {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Container not published after ${maxAttempts} attempts. Current status: ${status}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
343
368
|
}
|
|
344
369
|
|
|
345
370
|
return containerId; // Return the container ID as the published ID
|
|
@@ -383,18 +408,24 @@ export function serveRequests() {
|
|
|
383
408
|
}
|
|
384
409
|
|
|
385
410
|
// Create the Threads container
|
|
386
|
-
const containerId = await createThreadsContainer(
|
|
411
|
+
const { containerId, mediaType } = await createThreadsContainer(
|
|
412
|
+
requestData
|
|
413
|
+
);
|
|
387
414
|
|
|
388
|
-
//
|
|
415
|
+
// Attempt to publish the Threads container
|
|
389
416
|
const publishedId = await publishThreadsContainer(
|
|
390
417
|
requestData.userId,
|
|
391
418
|
requestData.accessToken,
|
|
392
|
-
containerId
|
|
419
|
+
containerId,
|
|
420
|
+
mediaType
|
|
393
421
|
);
|
|
394
422
|
|
|
395
|
-
return new Response(
|
|
396
|
-
|
|
397
|
-
|
|
423
|
+
return new Response(
|
|
424
|
+
JSON.stringify({ success: true, publishedId: publishedId.toString() }),
|
|
425
|
+
{
|
|
426
|
+
headers: { "Content-Type": "application/json" },
|
|
427
|
+
}
|
|
428
|
+
);
|
|
398
429
|
} catch (error) {
|
|
399
430
|
console.error("Error posting to Threads:", error);
|
|
400
431
|
return new Response(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codybrom/denim",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "Typescript/Deno module to simplify posting to Threads
|
|
3
|
+
"version": "1.3.3",
|
|
4
|
+
"description": "Typescript/Deno module to simplify posting to Threads",
|
|
5
5
|
"main": "mod.ts",
|
|
6
6
|
"directories": {
|
|
7
7
|
"example": "examples"
|