@codybrom/denim 1.2.0 → 1.3.0
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/examples/edge-function.ts +91 -111
- package/mod.ts +168 -49
- package/package.json +1 -1
- package/readme.md +4 -5
package/deno.json
CHANGED
|
@@ -5,18 +5,11 @@ import {
|
|
|
5
5
|
createThreadsContainer,
|
|
6
6
|
publishThreadsContainer,
|
|
7
7
|
createCarouselItem,
|
|
8
|
-
checkHealth,
|
|
9
8
|
getPublishingLimit,
|
|
10
|
-
} from "jsr:@codybrom/denim
|
|
9
|
+
} from "jsr:@codybrom/denim@1.3.0";
|
|
11
10
|
|
|
12
11
|
async function postToThreads(request: ThreadsPostRequest): Promise<string> {
|
|
13
12
|
try {
|
|
14
|
-
// Check API health before posting
|
|
15
|
-
const healthStatus = await checkHealth();
|
|
16
|
-
if (healthStatus.status !== "ok") {
|
|
17
|
-
throw new Error(`API is not healthy. Status: ${healthStatus.status}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
13
|
// Check rate limit
|
|
21
14
|
const rateLimit = await getPublishingLimit(
|
|
22
15
|
request.userId,
|
|
@@ -49,118 +42,105 @@ async function postToThreads(request: ThreadsPostRequest): Promise<string> {
|
|
|
49
42
|
|
|
50
43
|
Deno.serve(async (req: Request) => {
|
|
51
44
|
const url = new URL(req.url);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
45
|
+
const paths = url.pathname.split("/").filter((segment) => segment !== "");
|
|
46
|
+
|
|
47
|
+
switch (req.method) {
|
|
48
|
+
case "GET": {
|
|
49
|
+
switch (paths[1]) {
|
|
50
|
+
case "rate-limit": {
|
|
51
|
+
const userId = url.searchParams.get("userId");
|
|
52
|
+
const accessToken = url.searchParams.get("accessToken");
|
|
53
|
+
if (!userId || !accessToken) {
|
|
54
|
+
return new Response(
|
|
55
|
+
JSON.stringify({ error: "Missing userId or accessToken" }),
|
|
56
|
+
{
|
|
57
|
+
status: 400,
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const rateLimit = await getPublishingLimit(userId, accessToken);
|
|
64
|
+
return new Response(JSON.stringify(rateLimit), {
|
|
65
|
+
status: 200,
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
70
|
+
status: 500,
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
67
74
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Rate limit check endpoint
|
|
73
|
-
if (req.method === "GET" && url.pathname === "/rate-limit") {
|
|
74
|
-
const userId = url.searchParams.get("userId");
|
|
75
|
-
const accessToken = url.searchParams.get("accessToken");
|
|
76
|
-
|
|
77
|
-
if (!userId || !accessToken) {
|
|
78
|
-
return new Response(
|
|
79
|
-
JSON.stringify({ error: "Missing userId or accessToken" }),
|
|
80
|
-
{
|
|
81
|
-
status: 400,
|
|
82
|
-
headers: { "Content-Type": "application/json" },
|
|
75
|
+
default: {
|
|
76
|
+
return new Response("Not Found", { status: 404 });
|
|
83
77
|
}
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const rateLimit = await getPublishingLimit(userId, accessToken);
|
|
89
|
-
return new Response(JSON.stringify(rateLimit), {
|
|
90
|
-
status: 200,
|
|
91
|
-
headers: { "Content-Type": "application/json" },
|
|
92
|
-
});
|
|
93
|
-
} catch (error) {
|
|
94
|
-
return new Response(JSON.stringify({ error: error.message }), {
|
|
95
|
-
status: 500,
|
|
96
|
-
headers: { "Content-Type": "application/json" },
|
|
97
|
-
});
|
|
78
|
+
}
|
|
98
79
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
80
|
+
case "POST": {
|
|
81
|
+
if (paths[1] === "post") {
|
|
82
|
+
try {
|
|
83
|
+
const body = await req.json();
|
|
84
|
+
if (!body.userId || !body.accessToken || !body.mediaType) {
|
|
85
|
+
return new Response(
|
|
86
|
+
JSON.stringify({
|
|
87
|
+
success: false,
|
|
88
|
+
error: "Missing required fields",
|
|
89
|
+
}),
|
|
90
|
+
{
|
|
91
|
+
status: 400,
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
}
|
|
94
|
+
);
|
|
112
95
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
96
|
+
const postRequest: ThreadsPostRequest = {
|
|
97
|
+
userId: body.userId,
|
|
98
|
+
accessToken: body.accessToken,
|
|
99
|
+
mediaType: body.mediaType,
|
|
100
|
+
text: body.text,
|
|
101
|
+
imageUrl: body.imageUrl,
|
|
102
|
+
videoUrl: body.videoUrl,
|
|
103
|
+
altText: body.altText,
|
|
104
|
+
linkAttachment: body.linkAttachment,
|
|
105
|
+
allowlistedCountryCodes: body.allowlistedCountryCodes,
|
|
106
|
+
replyControl: body.replyControl,
|
|
107
|
+
children: body.children,
|
|
108
|
+
};
|
|
109
|
+
if (postRequest.mediaType === "CAROUSEL" && body.carouselItems) {
|
|
110
|
+
postRequest.children = [];
|
|
111
|
+
for (const item of body.carouselItems) {
|
|
112
|
+
const itemId = await createCarouselItem({
|
|
113
|
+
userId: postRequest.userId,
|
|
114
|
+
accessToken: postRequest.accessToken,
|
|
115
|
+
mediaType: item.mediaType,
|
|
116
|
+
imageUrl: item.imageUrl,
|
|
117
|
+
videoUrl: item.videoUrl,
|
|
118
|
+
altText: item.altText,
|
|
119
|
+
});
|
|
120
|
+
postRequest.children.push(itemId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const publishedId = await postToThreads(postRequest);
|
|
124
|
+
return new Response(JSON.stringify({ success: true, publishedId }), {
|
|
125
|
+
status: 200,
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
140
127
|
});
|
|
141
|
-
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error("Error processing request:", error);
|
|
130
|
+
return new Response(
|
|
131
|
+
JSON.stringify({ success: false, error: error.message }),
|
|
132
|
+
{
|
|
133
|
+
status: 500,
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
}
|
|
136
|
+
);
|
|
142
137
|
}
|
|
143
138
|
}
|
|
144
|
-
|
|
145
|
-
const publishedId = await postToThreads(postRequest);
|
|
146
|
-
|
|
147
|
-
return new Response(JSON.stringify({ success: true, publishedId }), {
|
|
148
|
-
status: 200,
|
|
149
|
-
headers: { "Content-Type": "application/json" },
|
|
150
|
-
});
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error("Error processing request:", error);
|
|
153
|
-
return new Response(
|
|
154
|
-
JSON.stringify({ success: false, error: error.message }),
|
|
155
|
-
{
|
|
156
|
-
status: 500,
|
|
157
|
-
headers: { "Content-Type": "application/json" },
|
|
158
|
-
}
|
|
159
|
-
);
|
|
139
|
+
return new Response("Not Found", { status: 404 });
|
|
160
140
|
}
|
|
141
|
+
default:
|
|
142
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
161
143
|
}
|
|
162
|
-
|
|
163
|
-
return new Response("Not Found", { status: 404 });
|
|
164
144
|
});
|
|
165
145
|
|
|
166
146
|
/*
|
package/mod.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface ThreadsPostRequest {
|
|
|
28
28
|
altText?: string;
|
|
29
29
|
/** The URL to be attached as a link to the post (optional, for text posts only) */
|
|
30
30
|
linkAttachment?: string;
|
|
31
|
-
/** List of country codes where the post should be visible (optional) */
|
|
31
|
+
/** List of country codes where the post should be visible (optional - requires special API access) */
|
|
32
32
|
allowlistedCountryCodes?: string[];
|
|
33
33
|
/** Controls who can reply to the post (optional) */
|
|
34
34
|
replyControl?: "everyone" | "accounts_you_follow" | "mentioned_only";
|
|
@@ -48,11 +48,10 @@ export interface ThreadsPostRequest {
|
|
|
48
48
|
* const request: ThreadsPostRequest = {
|
|
49
49
|
* userId: "123456",
|
|
50
50
|
* accessToken: "your_access_token",
|
|
51
|
-
* mediaType: "
|
|
52
|
-
* text: "Check out this
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* replyControl: "everyone"
|
|
51
|
+
* mediaType: "VIDEO",
|
|
52
|
+
* text: "Check out this video!",
|
|
53
|
+
* videoUrl: "https://example.com/video.mp4",
|
|
54
|
+
* altText: "A cool video"
|
|
56
55
|
* };
|
|
57
56
|
* const containerId = await createThreadsContainer(request);
|
|
58
57
|
* ```
|
|
@@ -60,7 +59,93 @@ export interface ThreadsPostRequest {
|
|
|
60
59
|
export async function createThreadsContainer(
|
|
61
60
|
request: ThreadsPostRequest
|
|
62
61
|
): Promise<string> {
|
|
63
|
-
//
|
|
62
|
+
// Input validation
|
|
63
|
+
validateRequest(request);
|
|
64
|
+
|
|
65
|
+
const url = `${THREADS_API_BASE_URL}/${request.userId}/threads`;
|
|
66
|
+
const body = new URLSearchParams({
|
|
67
|
+
access_token: request.accessToken,
|
|
68
|
+
media_type: request.mediaType,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Add common optional parameters
|
|
72
|
+
if (request.text) body.append("text", request.text);
|
|
73
|
+
if (request.altText) body.append("alt_text", request.altText);
|
|
74
|
+
if (request.replyControl) body.append("reply_control", request.replyControl);
|
|
75
|
+
if (request.allowlistedCountryCodes) {
|
|
76
|
+
body.append(
|
|
77
|
+
"allowlisted_country_codes",
|
|
78
|
+
request.allowlistedCountryCodes.join(",")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
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) {
|
|
88
|
+
body.append("image_url", request.imageUrl);
|
|
89
|
+
} else if (request.mediaType === "TEXT" && request.linkAttachment) {
|
|
90
|
+
body.append("link_attachment", request.linkAttachment);
|
|
91
|
+
} else if (request.mediaType === "CAROUSEL" && request.children) {
|
|
92
|
+
body.append("children", request.children.join(","));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`Sending request to: ${url}`);
|
|
96
|
+
console.log(`Request body: ${body.toString()}`);
|
|
97
|
+
|
|
98
|
+
const response = await fetch(url, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: body,
|
|
101
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const responseText = await response.text();
|
|
105
|
+
console.log(`Response status: ${response.status} ${response.statusText}`);
|
|
106
|
+
console.log(`Response body: ${responseText}`);
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Failed to create Threads container: ${response.statusText}. Details: ${responseText}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const data = JSON.parse(responseText);
|
|
116
|
+
return data.id;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(`Failed to parse response JSON: ${error}`);
|
|
119
|
+
throw new Error(`Invalid response from Threads API: ${responseText}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validates the ThreadsPostRequest object to ensure correct usage of media-specific properties.
|
|
125
|
+
*
|
|
126
|
+
* @param request - The ThreadsPostRequest object to validate
|
|
127
|
+
* @throws Will throw an error if the request contains invalid combinations of media type and properties
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const request: ThreadsPostRequest = {
|
|
132
|
+
* userId: "123456",
|
|
133
|
+
* accessToken: "your_access_token",
|
|
134
|
+
* mediaType: "IMAGE",
|
|
135
|
+
* imageUrl: "https://example.com/image.jpg"
|
|
136
|
+
* };
|
|
137
|
+
* validateRequest(request); // This will not throw an error
|
|
138
|
+
*
|
|
139
|
+
* const invalidRequest: ThreadsPostRequest = {
|
|
140
|
+
* userId: "123456",
|
|
141
|
+
* accessToken: "your_access_token",
|
|
142
|
+
* mediaType: "TEXT",
|
|
143
|
+
* imageUrl: "https://example.com/image.jpg"
|
|
144
|
+
* };
|
|
145
|
+
* validateRequest(invalidRequest); // This will throw an error
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
function validateRequest(request: ThreadsPostRequest): void {
|
|
64
149
|
if (request.mediaType !== "IMAGE" && request.imageUrl) {
|
|
65
150
|
throw new Error("imageUrl can only be used with IMAGE media type");
|
|
66
151
|
}
|
|
@@ -79,30 +164,26 @@ export async function createThreadsContainer(
|
|
|
79
164
|
) {
|
|
80
165
|
throw new Error("CAROUSEL media type requires at least 2 children");
|
|
81
166
|
}
|
|
167
|
+
}
|
|
82
168
|
|
|
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> {
|
|
83
178
|
const url = `${THREADS_API_BASE_URL}/${request.userId}/threads`;
|
|
84
179
|
const body = new URLSearchParams({
|
|
85
180
|
access_token: request.accessToken,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
request.imageUrl && { image_url: request.imageUrl }),
|
|
90
|
-
...(request.mediaType === "VIDEO" &&
|
|
91
|
-
request.videoUrl && { video_url: request.videoUrl }),
|
|
181
|
+
is_carousel_item: "true",
|
|
182
|
+
media_type: "VIDEO",
|
|
183
|
+
video_url: request.videoUrl!,
|
|
92
184
|
...(request.altText && { alt_text: request.altText }),
|
|
93
|
-
...(request.mediaType === "TEXT" &&
|
|
94
|
-
request.linkAttachment && { link_attachment: request.linkAttachment }),
|
|
95
|
-
...(request.allowlistedCountryCodes && {
|
|
96
|
-
allowlisted_country_codes: request.allowlistedCountryCodes.join(","),
|
|
97
|
-
}),
|
|
98
|
-
...(request.replyControl && { reply_control: request.replyControl }),
|
|
99
|
-
...(request.mediaType === "CAROUSEL" &&
|
|
100
|
-
request.children && { children: request.children.join(",") }),
|
|
101
185
|
});
|
|
102
186
|
|
|
103
|
-
console.log(`Sending request to: ${url}`);
|
|
104
|
-
console.log(`Request body: ${body.toString()}`);
|
|
105
|
-
|
|
106
187
|
const response = await fetch(url, {
|
|
107
188
|
method: "POST",
|
|
108
189
|
body: body,
|
|
@@ -112,12 +193,10 @@ export async function createThreadsContainer(
|
|
|
112
193
|
});
|
|
113
194
|
|
|
114
195
|
const responseText = await response.text();
|
|
115
|
-
console.log(`Response status: ${response.status} ${response.statusText}`);
|
|
116
|
-
console.log(`Response body: ${responseText}`);
|
|
117
196
|
|
|
118
197
|
if (!response.ok) {
|
|
119
198
|
throw new Error(
|
|
120
|
-
`Failed to create
|
|
199
|
+
`Failed to create video item container: ${response.statusText}. Details: ${responseText}`
|
|
121
200
|
);
|
|
122
201
|
}
|
|
123
202
|
|
|
@@ -156,12 +235,7 @@ export async function createThreadsContainer(
|
|
|
156
235
|
* imageUrl: "https://example.com/image.jpg",
|
|
157
236
|
* altText: "A beautiful landscape"
|
|
158
237
|
* };
|
|
159
|
-
*
|
|
160
|
-
* const itemId = await createCarouselItem(itemRequest);
|
|
161
|
-
* console.log(`Carousel item created with ID: ${itemId}`);
|
|
162
|
-
* } catch (error) {
|
|
163
|
-
* console.error("Failed to create carousel item:", error);
|
|
164
|
-
* }
|
|
238
|
+
* const itemId = await createCarouselItem(itemRequest);
|
|
165
239
|
* ```
|
|
166
240
|
*/
|
|
167
241
|
export async function createCarouselItem(
|
|
@@ -216,14 +290,36 @@ export async function createCarouselItem(
|
|
|
216
290
|
}
|
|
217
291
|
}
|
|
218
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Checks the status of a Threads container.
|
|
295
|
+
* @param containerId - The ID of the container to check
|
|
296
|
+
* @param accessToken - The access token for authentication
|
|
297
|
+
* @returns A Promise that resolves to the container status
|
|
298
|
+
* @throws Will throw an error if the API request fails
|
|
299
|
+
*/
|
|
300
|
+
async function checkContainerStatus(
|
|
301
|
+
containerId: string,
|
|
302
|
+
accessToken: string
|
|
303
|
+
): Promise<string> {
|
|
304
|
+
const url = `${THREADS_API_BASE_URL}/${containerId}?fields=status,error_message&access_token=${accessToken}`;
|
|
305
|
+
|
|
306
|
+
const response = await fetch(url);
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new Error(`Failed to check container status: ${response.statusText}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const data = await response.json();
|
|
312
|
+
return data.status;
|
|
313
|
+
}
|
|
314
|
+
|
|
219
315
|
/**
|
|
220
316
|
* Publishes a Threads media container.
|
|
221
317
|
*
|
|
222
318
|
* @param userId - The user ID of the Threads account
|
|
223
319
|
* @param accessToken - The access token for authentication
|
|
224
320
|
* @param containerId - The ID of the container to publish
|
|
225
|
-
* @returns A Promise that resolves to the published
|
|
226
|
-
* @throws Will throw an error if the API request fails
|
|
321
|
+
* @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
|
|
227
323
|
*
|
|
228
324
|
* @example
|
|
229
325
|
* ```typescript
|
|
@@ -235,35 +331,52 @@ export async function publishThreadsContainer(
|
|
|
235
331
|
accessToken: string,
|
|
236
332
|
containerId: string
|
|
237
333
|
): Promise<string> {
|
|
238
|
-
const
|
|
239
|
-
const
|
|
334
|
+
const publishUrl = `${THREADS_API_BASE_URL}/${userId}/threads_publish`;
|
|
335
|
+
const publishBody = new URLSearchParams({
|
|
240
336
|
access_token: accessToken,
|
|
241
337
|
creation_id: containerId,
|
|
242
338
|
});
|
|
243
339
|
|
|
244
|
-
const
|
|
340
|
+
const publishResponse = await fetch(publishUrl, {
|
|
245
341
|
method: "POST",
|
|
246
|
-
body:
|
|
342
|
+
body: publishBody,
|
|
247
343
|
headers: {
|
|
248
344
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
249
345
|
},
|
|
250
346
|
});
|
|
251
347
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!response.ok) {
|
|
348
|
+
if (!publishResponse.ok) {
|
|
255
349
|
throw new Error(
|
|
256
|
-
`Failed to publish Threads container: ${
|
|
350
|
+
`Failed to publish Threads container: ${publishResponse.statusText}`
|
|
257
351
|
);
|
|
258
352
|
}
|
|
259
353
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
}
|
|
368
|
+
|
|
369
|
+
if (status === "ERROR") {
|
|
370
|
+
throw new Error(`Failed to publish container. Error: ${status}`);
|
|
266
371
|
}
|
|
372
|
+
|
|
373
|
+
if (status !== "PUBLISHED" && status !== "FINISHED") {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Container not published after ${maxAttempts} attempts. Current status: ${status}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return containerId; // Return the container ID as the published ID
|
|
267
380
|
}
|
|
268
381
|
|
|
269
382
|
/**
|
|
@@ -339,6 +452,12 @@ export function serveRequests() {
|
|
|
339
452
|
* @param userId - The user ID of the Threads account
|
|
340
453
|
* @param accessToken - The access token for authentication
|
|
341
454
|
* @returns A Promise that resolves to the rate limit usage information
|
|
455
|
+
* @throws Will throw an error if the API request fails
|
|
456
|
+
* @example
|
|
457
|
+
* ```typescript
|
|
458
|
+
* const rateLimit = await getPublishingLimit("123456", "your_access_token");
|
|
459
|
+
* console.log(`Current usage: ${rateLimit.quota_usage}`);
|
|
460
|
+
* ```
|
|
342
461
|
*/
|
|
343
462
|
export async function getPublishingLimit(
|
|
344
463
|
userId: string,
|
package/package.json
CHANGED
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.
|
|
35
|
+
import { ThreadsPostRequest, createThreadsContainer, publishThreadsContainer } from 'jsr:@codybrom/denim@^1.3.0';
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
### Basic Usage
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
import { createThreadsContainer, publishThreadsContainer, ThreadsPostRequest } from "jsr:@codybrom/denim@^1.
|
|
41
|
+
import { createThreadsContainer, publishThreadsContainer, ThreadsPostRequest } from "jsr:@codybrom/denim@^1.3.0";
|
|
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.
|
|
64
|
+
import { getPublishingLimit } from "jsr:@codybrom/denim@^1.3.0";
|
|
65
65
|
|
|
66
66
|
const userId = "YOUR_USER_ID";
|
|
67
67
|
const accessToken = "YOUR_ACCESS_TOKEN";
|
|
@@ -126,7 +126,7 @@ const videoRequest: ThreadsPostRequest = {
|
|
|
126
126
|
};
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
#### Video Post with Alt Text, Reply Control and Geo-gating
|
|
129
|
+
#### Video Post with Alt Text, Reply Control and Geo-gating* (requires special account permission)
|
|
130
130
|
|
|
131
131
|
```typescript
|
|
132
132
|
const videoRequest: ThreadsPostRequest = {
|
|
@@ -171,7 +171,6 @@ const carouselRequest: ThreadsPostRequest = {
|
|
|
171
171
|
text: "Check out this carousel post!",
|
|
172
172
|
children: [item1Id, item2Id],
|
|
173
173
|
replyControl: "everyone",
|
|
174
|
-
allowlistedCountryCodes: ["US", "CA", "MX"],
|
|
175
174
|
};
|
|
176
175
|
|
|
177
176
|
const containerId = await createThreadsContainer(carouselRequest);
|