@codybrom/denim 1.3.1 → 1.3.2
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 +77 -55
- 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: string; 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: data.id, 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
|
}
|
|
@@ -284,19 +273,44 @@ async function checkContainerStatus(
|
|
|
284
273
|
* @param userId - The user ID of the Threads account
|
|
285
274
|
* @param accessToken - The access token for authentication
|
|
286
275
|
* @param containerId - The ID of the container to publish
|
|
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", containerId, mediaType);
|
|
293
284
|
* ```
|
|
294
285
|
*/
|
|
295
286
|
export async function publishThreadsContainer(
|
|
296
287
|
userId: string,
|
|
297
288
|
accessToken: string,
|
|
298
|
-
containerId: string
|
|
289
|
+
containerId: string,
|
|
290
|
+
mediaType: string
|
|
299
291
|
): Promise<string> {
|
|
292
|
+
if (mediaType === "VIDEO") {
|
|
293
|
+
// Check container status before publishing for videos
|
|
294
|
+
let status = await checkContainerStatus(containerId, accessToken);
|
|
295
|
+
let attempts = 0;
|
|
296
|
+
const maxAttempts = 10; // Increased from 5 to 10 for videos
|
|
297
|
+
|
|
298
|
+
while (status !== "FINISHED" && attempts < maxAttempts) {
|
|
299
|
+
await new Promise((resolve) => setTimeout(resolve, 30000)); // Wait for 30 seconds
|
|
300
|
+
status = await checkContainerStatus(containerId, accessToken);
|
|
301
|
+
attempts++;
|
|
302
|
+
console.log(
|
|
303
|
+
`Video container status check attempt ${attempts}: ${status}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (status !== "FINISHED") {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Video container not ready after ${maxAttempts} attempts. Current status: ${status}`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
300
314
|
const publishUrl = `${THREADS_API_BASE_URL}/${userId}/threads_publish`;
|
|
301
315
|
const publishBody = new URLSearchParams({
|
|
302
316
|
access_token: accessToken,
|
|
@@ -311,35 +325,40 @@ export async function publishThreadsContainer(
|
|
|
311
325
|
},
|
|
312
326
|
});
|
|
313
327
|
|
|
328
|
+
const responseText = await publishResponse.text();
|
|
329
|
+
|
|
314
330
|
if (!publishResponse.ok) {
|
|
331
|
+
console.error(`Publish response body: ${responseText}`);
|
|
315
332
|
throw new Error(
|
|
316
|
-
`Failed to publish Threads container: ${publishResponse.statusText}`
|
|
333
|
+
`Failed to publish Threads container: ${publishResponse.statusText}. Details: ${responseText}`
|
|
317
334
|
);
|
|
318
335
|
}
|
|
319
336
|
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
337
|
+
// For non-video posts, we still need to check if the post was actually published
|
|
338
|
+
if (mediaType !== "VIDEO") {
|
|
339
|
+
let status = await checkContainerStatus(containerId, accessToken);
|
|
340
|
+
let attempts = 0;
|
|
341
|
+
const maxAttempts = 5;
|
|
342
|
+
|
|
343
|
+
while (
|
|
344
|
+
status !== "PUBLISHED" &&
|
|
345
|
+
status !== "FINISHED" &&
|
|
346
|
+
attempts < maxAttempts
|
|
347
|
+
) {
|
|
348
|
+
await new Promise((resolve) => setTimeout(resolve, 60000)); // Wait for 1 minute
|
|
349
|
+
status = await checkContainerStatus(containerId, accessToken);
|
|
350
|
+
attempts++;
|
|
351
|
+
}
|
|
334
352
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
353
|
+
if (status === "ERROR") {
|
|
354
|
+
throw new Error(`Failed to publish container. Error: ${status}`);
|
|
355
|
+
}
|
|
338
356
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
357
|
+
if (status !== "PUBLISHED" && status !== "FINISHED") {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Container not published after ${maxAttempts} attempts. Current status: ${status}`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
343
362
|
}
|
|
344
363
|
|
|
345
364
|
return containerId; // Return the container ID as the published ID
|
|
@@ -383,13 +402,16 @@ export function serveRequests() {
|
|
|
383
402
|
}
|
|
384
403
|
|
|
385
404
|
// Create the Threads container
|
|
386
|
-
const containerId = await createThreadsContainer(
|
|
405
|
+
const { containerId, mediaType } = await createThreadsContainer(
|
|
406
|
+
requestData
|
|
407
|
+
);
|
|
387
408
|
|
|
388
|
-
//
|
|
409
|
+
// Attempt to publish the Threads container
|
|
389
410
|
const publishedId = await publishThreadsContainer(
|
|
390
411
|
requestData.userId,
|
|
391
412
|
requestData.accessToken,
|
|
392
|
-
containerId
|
|
413
|
+
containerId,
|
|
414
|
+
mediaType
|
|
393
415
|
);
|
|
394
416
|
|
|
395
417
|
return new Response(JSON.stringify({ success: true, publishedId }), {
|
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.2",
|
|
4
|
+
"description": "Typescript/Deno module to simplify posting to Threads",
|
|
5
5
|
"main": "mod.ts",
|
|
6
6
|
"directories": {
|
|
7
7
|
"example": "examples"
|