@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.
Files changed (3) hide show
  1. package/deno.json +1 -1
  2. package/mod.ts +92 -61
  3. package/package.json +2 -2
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codybrom/denim",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "A Deno function for posting to Threads.",
5
5
  "entry": "./mod.ts",
6
6
  "exports": {
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
- switch (request.mediaType) {
83
- case "VIDEO":
84
- if (!request.videoUrl)
85
- throw new Error("videoUrl is required for VIDEO media type");
86
- body.append("video_url", request.videoUrl);
87
- if (request.altText) body.append("alt_text", request.altText);
88
- break;
89
- case "IMAGE":
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 or if publishing times out
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 publishedId = await publishThreadsContainer("123456", "your_access_token", "container_id");
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: string
299
- ): Promise<string> {
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
- // Check container status
321
- let status = await checkContainerStatus(containerId, accessToken);
322
- let attempts = 0;
323
- const maxAttempts = 5;
324
-
325
- while (
326
- status !== "PUBLISHED" &&
327
- status !== "FINISHED" &&
328
- attempts < maxAttempts
329
- ) {
330
- await new Promise((resolve) => setTimeout(resolve, 60000)); // Wait for 1 minute
331
- status = await checkContainerStatus(containerId, accessToken);
332
- attempts++;
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
- if (status === "ERROR") {
336
- throw new Error(`Failed to publish container. Error: ${status}`);
337
- }
359
+ if (status === "ERROR") {
360
+ throw new Error(`Failed to publish container. Error: ${status}`);
361
+ }
338
362
 
339
- if (status !== "PUBLISHED" && status !== "FINISHED") {
340
- throw new Error(
341
- `Container not published after ${maxAttempts} attempts. Current status: ${status}`
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(requestData);
411
+ const { containerId, mediaType } = await createThreadsContainer(
412
+ requestData
413
+ );
387
414
 
388
- // Immediately attempt to publish the Threads container
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(JSON.stringify({ success: true, publishedId }), {
396
- headers: { "Content-Type": "application/json" },
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.1",
4
- "description": "Typescript/Deno module to simplify posting to Threads with text, images, or videos",
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"