@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.
Files changed (3) hide show
  1. package/deno.json +1 -1
  2. package/mod.ts +77 -55
  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.2",
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: 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
- 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: 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 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", 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
- // 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
- }
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
- if (status === "ERROR") {
336
- throw new Error(`Failed to publish container. Error: ${status}`);
337
- }
353
+ if (status === "ERROR") {
354
+ throw new Error(`Failed to publish container. Error: ${status}`);
355
+ }
338
356
 
339
- if (status !== "PUBLISHED" && status !== "FINISHED") {
340
- throw new Error(
341
- `Container not published after ${maxAttempts} attempts. Current status: ${status}`
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(requestData);
405
+ const { containerId, mediaType } = await createThreadsContainer(
406
+ requestData
407
+ );
387
408
 
388
- // Immediately attempt to publish the Threads container
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.1",
4
- "description": "Typescript/Deno module to simplify posting to Threads with text, images, or videos",
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"