@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.
- package/deno.json +1 -1
- package/mod.ts +68 -80
- package/package.json +2 -2
- package/readme.md +4 -4
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
|
|
|
@@ -80,12 +80,10 @@ export async function createThreadsContainer(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// Handle media type specific parameters
|
|
83
|
-
if (request.mediaType === "
|
|
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
|
|
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
|
|
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
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
353
|
+
if (status === "ERROR") {
|
|
354
|
+
throw new Error(`Failed to publish container. Error: ${status}`);
|
|
355
|
+
}
|
|
372
356
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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(
|
|
405
|
+
const { containerId, mediaType } = await createThreadsContainer(
|
|
406
|
+
requestData
|
|
407
|
+
);
|
|
421
408
|
|
|
422
|
-
//
|
|
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.
|
|
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"
|
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
|
|
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
|
|
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
|
|
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
|
|
147
|
+
import { createCarouselItem, createThreadsContainer, publishThreadsContainer, ThreadsPostRequest } from "jsr:@codybrom/denim";
|
|
148
148
|
|
|
149
149
|
// First, create carousel items
|
|
150
150
|
const item1Id = await createCarouselItem({
|