@codybrom/denim 1.3.0 → 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 (4) hide show
  1. package/deno.json +1 -1
  2. package/mod.ts +68 -80
  3. package/package.json +2 -2
  4. package/readme.md +4 -4
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codybrom/denim",
3
- "version": "1.3.0",
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
 
@@ -80,12 +80,10 @@ export async function createThreadsContainer(
80
80
  }
81
81
 
82
82
  // Handle media type specific parameters
83
- if (request.mediaType === "VIDEO") {
84
- const videoItemId = await createVideoItemContainer(request);
85
- body.set("media_type", "CAROUSEL");
86
- body.append("children", videoItemId);
87
- } else if (request.mediaType === "IMAGE" && request.imageUrl) {
83
+ if (request.mediaType === "IMAGE" && request.imageUrl) {
88
84
  body.append("image_url", request.imageUrl);
85
+ } else if (request.mediaType === "VIDEO" && request.videoUrl) {
86
+ body.append("video_url", request.videoUrl);
89
87
  } else if (request.mediaType === "TEXT" && request.linkAttachment) {
90
88
  body.append("link_attachment", request.linkAttachment);
91
89
  } else if (request.mediaType === "CAROUSEL" && request.children) {
@@ -113,7 +111,7 @@ export async function createThreadsContainer(
113
111
 
114
112
  try {
115
113
  const data = JSON.parse(responseText);
116
- return data.id;
114
+ return { containerId: data.id, mediaType: request.mediaType };
117
115
  } catch (error) {
118
116
  console.error(`Failed to parse response JSON: ${error}`);
119
117
  throw new Error(`Invalid response from Threads API: ${responseText}`);
@@ -166,49 +164,6 @@ function validateRequest(request: ThreadsPostRequest): void {
166
164
  }
167
165
  }
168
166
 
169
- /**
170
- * Creates a video item container for Threads.
171
- * @param request - The ThreadsPostRequest object containing video post details
172
- * @returns A Promise that resolves to the video item container ID
173
- * @throws Will throw an error if the API request fails
174
- */
175
- async function createVideoItemContainer(
176
- request: ThreadsPostRequest
177
- ): Promise<string> {
178
- const url = `${THREADS_API_BASE_URL}/${request.userId}/threads`;
179
- const body = new URLSearchParams({
180
- access_token: request.accessToken,
181
- is_carousel_item: "true",
182
- media_type: "VIDEO",
183
- video_url: request.videoUrl!,
184
- ...(request.altText && { alt_text: request.altText }),
185
- });
186
-
187
- const response = await fetch(url, {
188
- method: "POST",
189
- body: body,
190
- headers: {
191
- "Content-Type": "application/x-www-form-urlencoded",
192
- },
193
- });
194
-
195
- const responseText = await response.text();
196
-
197
- if (!response.ok) {
198
- throw new Error(
199
- `Failed to create video item container: ${response.statusText}. Details: ${responseText}`
200
- );
201
- }
202
-
203
- try {
204
- const data = JSON.parse(responseText);
205
- return data.id;
206
- } catch (error) {
207
- console.error(`Failed to parse response JSON: ${error}`);
208
- throw new Error(`Invalid response from Threads API: ${responseText}`);
209
- }
210
- }
211
-
212
167
  /**
213
168
  * Creates a carousel item for a Threads carousel post.
214
169
  *
@@ -318,19 +273,44 @@ async function checkContainerStatus(
318
273
  * @param userId - The user ID of the Threads account
319
274
  * @param accessToken - The access token for authentication
320
275
  * @param containerId - The ID of the container to publish
276
+ * @param mediaType - The type of media being published
321
277
  * @returns A Promise that resolves to the published container ID
322
- * @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)
323
279
  *
324
280
  * @example
325
281
  * ```typescript
326
- * 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);
327
284
  * ```
328
285
  */
329
286
  export async function publishThreadsContainer(
330
287
  userId: string,
331
288
  accessToken: string,
332
- containerId: string
289
+ containerId: string,
290
+ mediaType: string
333
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
+
334
314
  const publishUrl = `${THREADS_API_BASE_URL}/${userId}/threads_publish`;
335
315
  const publishBody = new URLSearchParams({
336
316
  access_token: accessToken,
@@ -345,35 +325,40 @@ export async function publishThreadsContainer(
345
325
  },
346
326
  });
347
327
 
328
+ const responseText = await publishResponse.text();
329
+
348
330
  if (!publishResponse.ok) {
331
+ console.error(`Publish response body: ${responseText}`);
349
332
  throw new Error(
350
- `Failed to publish Threads container: ${publishResponse.statusText}`
333
+ `Failed to publish Threads container: ${publishResponse.statusText}. Details: ${responseText}`
351
334
  );
352
335
  }
353
336
 
354
- // Check container status
355
- let status = await checkContainerStatus(containerId, accessToken);
356
- let attempts = 0;
357
- const maxAttempts = 5;
358
-
359
- while (
360
- status !== "PUBLISHED" &&
361
- status !== "FINISHED" &&
362
- attempts < maxAttempts
363
- ) {
364
- await new Promise((resolve) => setTimeout(resolve, 60000)); // Wait for 1 minute
365
- status = await checkContainerStatus(containerId, accessToken);
366
- attempts++;
367
- }
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
+ }
368
352
 
369
- if (status === "ERROR") {
370
- throw new Error(`Failed to publish container. Error: ${status}`);
371
- }
353
+ if (status === "ERROR") {
354
+ throw new Error(`Failed to publish container. Error: ${status}`);
355
+ }
372
356
 
373
- if (status !== "PUBLISHED" && status !== "FINISHED") {
374
- throw new Error(
375
- `Container not published after ${maxAttempts} attempts. Current status: ${status}`
376
- );
357
+ if (status !== "PUBLISHED" && status !== "FINISHED") {
358
+ throw new Error(
359
+ `Container not published after ${maxAttempts} attempts. Current status: ${status}`
360
+ );
361
+ }
377
362
  }
378
363
 
379
364
  return containerId; // Return the container ID as the published ID
@@ -417,13 +402,16 @@ export function serveRequests() {
417
402
  }
418
403
 
419
404
  // Create the Threads container
420
- const containerId = await createThreadsContainer(requestData);
405
+ const { containerId, mediaType } = await createThreadsContainer(
406
+ requestData
407
+ );
421
408
 
422
- // Immediately attempt to publish the Threads container
409
+ // Attempt to publish the Threads container
423
410
  const publishedId = await publishThreadsContainer(
424
411
  requestData.userId,
425
412
  requestData.accessToken,
426
- containerId
413
+ containerId,
414
+ mediaType
427
415
  );
428
416
 
429
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.0",
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"
package/readme.md CHANGED
@@ -32,13 +32,13 @@ This will add the latest version of Denim to your project's dependencies.
32
32
  To import straight from JSR:
33
33
 
34
34
  ```typescript
35
- import { ThreadsPostRequest, createThreadsContainer, publishThreadsContainer } from 'jsr:@codybrom/denim@^1.3.0';
35
+ import { ThreadsPostRequest, createThreadsContainer, publishThreadsContainer } from 'jsr:@codybrom/denim';
36
36
  ```
37
37
 
38
38
  ### Basic Usage
39
39
 
40
40
  ```typescript
41
- import { createThreadsContainer, publishThreadsContainer, ThreadsPostRequest } from "jsr:@codybrom/denim@^1.3.0";
41
+ import { createThreadsContainer, publishThreadsContainer, ThreadsPostRequest } from "jsr:@codybrom/denim";
42
42
 
43
43
  const request: ThreadsPostRequest = {
44
44
  userId: "YOUR_USER_ID",
@@ -61,7 +61,7 @@ console.log(`Post published with ID: ${publishedId}`);
61
61
  #### Retrieving Publishing Rate Limit
62
62
 
63
63
  ```typescript
64
- import { getPublishingLimit } from "jsr:@codybrom/denim@^1.3.0";
64
+ import { getPublishingLimit } from "jsr:@codybrom/denim";
65
65
 
66
66
  const userId = "YOUR_USER_ID";
67
67
  const accessToken = "YOUR_ACCESS_TOKEN";
@@ -144,7 +144,7 @@ const videoRequest: ThreadsPostRequest = {
144
144
  #### Carousel Post
145
145
 
146
146
  ```typescript
147
- import { createCarouselItem, createThreadsContainer, publishThreadsContainer, ThreadsPostRequest } from "jsr:@codybrom/denim@^1.0.4";
147
+ import { createCarouselItem, createThreadsContainer, publishThreadsContainer, ThreadsPostRequest } from "jsr:@codybrom/denim";
148
148
 
149
149
  // First, create carousel items
150
150
  const item1Id = await createCarouselItem({