@codybrom/denim 1.2.0 → 1.3.1
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 +149 -64
- package/package.json +1 -1
- package/readme.md +5 -6
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,55 +59,58 @@ export interface ThreadsPostRequest {
|
|
|
60
59
|
export async function createThreadsContainer(
|
|
61
60
|
request: ThreadsPostRequest
|
|
62
61
|
): Promise<string> {
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
throw new Error("imageUrl can only be used with IMAGE media type");
|
|
66
|
-
}
|
|
67
|
-
if (request.mediaType !== "VIDEO" && request.videoUrl) {
|
|
68
|
-
throw new Error("videoUrl can only be used with VIDEO media type");
|
|
69
|
-
}
|
|
70
|
-
if (request.mediaType !== "TEXT" && request.linkAttachment) {
|
|
71
|
-
throw new Error("linkAttachment can only be used with TEXT media type");
|
|
72
|
-
}
|
|
73
|
-
if (request.mediaType !== "CAROUSEL" && request.children) {
|
|
74
|
-
throw new Error("children can only be used with CAROUSEL media type");
|
|
75
|
-
}
|
|
76
|
-
if (
|
|
77
|
-
request.mediaType === "CAROUSEL" &&
|
|
78
|
-
(!request.children || request.children.length < 2)
|
|
79
|
-
) {
|
|
80
|
-
throw new Error("CAROUSEL media type requires at least 2 children");
|
|
81
|
-
}
|
|
62
|
+
// Input validation
|
|
63
|
+
validateRequest(request);
|
|
82
64
|
|
|
83
65
|
const url = `${THREADS_API_BASE_URL}/${request.userId}/threads`;
|
|
84
66
|
const body = new URLSearchParams({
|
|
85
67
|
access_token: request.accessToken,
|
|
86
68
|
media_type: request.mediaType,
|
|
87
|
-
...(request.text && { text: request.text }),
|
|
88
|
-
...(request.mediaType === "IMAGE" &&
|
|
89
|
-
request.imageUrl && { image_url: request.imageUrl }),
|
|
90
|
-
...(request.mediaType === "VIDEO" &&
|
|
91
|
-
request.videoUrl && { video_url: request.videoUrl }),
|
|
92
|
-
...(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
69
|
});
|
|
102
70
|
|
|
71
|
+
// Add common optional parameters
|
|
72
|
+
if (request.text) body.append("text", request.text);
|
|
73
|
+
if (request.replyControl) body.append("reply_control", request.replyControl);
|
|
74
|
+
if (request.allowlistedCountryCodes) {
|
|
75
|
+
body.append(
|
|
76
|
+
"allowlisted_country_codes",
|
|
77
|
+
request.allowlistedCountryCodes.join(",")
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 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;
|
|
105
|
+
}
|
|
106
|
+
|
|
103
107
|
console.log(`Sending request to: ${url}`);
|
|
104
108
|
console.log(`Request body: ${body.toString()}`);
|
|
105
109
|
|
|
106
110
|
const response = await fetch(url, {
|
|
107
111
|
method: "POST",
|
|
108
112
|
body: body,
|
|
109
|
-
headers: {
|
|
110
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
111
|
-
},
|
|
113
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
const responseText = await response.text();
|
|
@@ -130,6 +132,49 @@ export async function createThreadsContainer(
|
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Validates the ThreadsPostRequest object to ensure correct usage of media-specific properties.
|
|
137
|
+
*
|
|
138
|
+
* @param request - The ThreadsPostRequest object to validate
|
|
139
|
+
* @throws Will throw an error if the request contains invalid combinations of media type and properties
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const request: ThreadsPostRequest = {
|
|
144
|
+
* userId: "123456",
|
|
145
|
+
* accessToken: "your_access_token",
|
|
146
|
+
* mediaType: "IMAGE",
|
|
147
|
+
* imageUrl: "https://example.com/image.jpg"
|
|
148
|
+
* };
|
|
149
|
+
* validateRequest(request); // This will not throw an error
|
|
150
|
+
*
|
|
151
|
+
* const invalidRequest: ThreadsPostRequest = {
|
|
152
|
+
* userId: "123456",
|
|
153
|
+
* accessToken: "your_access_token",
|
|
154
|
+
* mediaType: "TEXT",
|
|
155
|
+
* imageUrl: "https://example.com/image.jpg"
|
|
156
|
+
* };
|
|
157
|
+
* validateRequest(invalidRequest); // This will throw an error
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
function validateRequest(request: ThreadsPostRequest): void {
|
|
161
|
+
if (request.mediaType !== "IMAGE" && request.imageUrl) {
|
|
162
|
+
throw new Error("imageUrl can only be used with IMAGE media type");
|
|
163
|
+
}
|
|
164
|
+
if (request.mediaType !== "TEXT" && request.linkAttachment) {
|
|
165
|
+
throw new Error("linkAttachment can only be used with TEXT media type");
|
|
166
|
+
}
|
|
167
|
+
if (request.mediaType !== "CAROUSEL" && request.children) {
|
|
168
|
+
throw new Error("children can only be used with CAROUSEL media type");
|
|
169
|
+
}
|
|
170
|
+
if (
|
|
171
|
+
request.mediaType === "CAROUSEL" &&
|
|
172
|
+
(!request.children || request.children.length < 2)
|
|
173
|
+
) {
|
|
174
|
+
throw new Error("CAROUSEL media type requires at least 2 children");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
133
178
|
/**
|
|
134
179
|
* Creates a carousel item for a Threads carousel post.
|
|
135
180
|
*
|
|
@@ -156,12 +201,7 @@ export async function createThreadsContainer(
|
|
|
156
201
|
* imageUrl: "https://example.com/image.jpg",
|
|
157
202
|
* altText: "A beautiful landscape"
|
|
158
203
|
* };
|
|
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
|
-
* }
|
|
204
|
+
* const itemId = await createCarouselItem(itemRequest);
|
|
165
205
|
* ```
|
|
166
206
|
*/
|
|
167
207
|
export async function createCarouselItem(
|
|
@@ -216,14 +256,36 @@ export async function createCarouselItem(
|
|
|
216
256
|
}
|
|
217
257
|
}
|
|
218
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Checks the status of a Threads container.
|
|
261
|
+
* @param containerId - The ID of the container to check
|
|
262
|
+
* @param accessToken - The access token for authentication
|
|
263
|
+
* @returns A Promise that resolves to the container status
|
|
264
|
+
* @throws Will throw an error if the API request fails
|
|
265
|
+
*/
|
|
266
|
+
async function checkContainerStatus(
|
|
267
|
+
containerId: string,
|
|
268
|
+
accessToken: string
|
|
269
|
+
): Promise<string> {
|
|
270
|
+
const url = `${THREADS_API_BASE_URL}/${containerId}?fields=status,error_message&access_token=${accessToken}`;
|
|
271
|
+
|
|
272
|
+
const response = await fetch(url);
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
throw new Error(`Failed to check container status: ${response.statusText}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const data = await response.json();
|
|
278
|
+
return data.status;
|
|
279
|
+
}
|
|
280
|
+
|
|
219
281
|
/**
|
|
220
282
|
* Publishes a Threads media container.
|
|
221
283
|
*
|
|
222
284
|
* @param userId - The user ID of the Threads account
|
|
223
285
|
* @param accessToken - The access token for authentication
|
|
224
286
|
* @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
|
|
287
|
+
* @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
|
|
227
289
|
*
|
|
228
290
|
* @example
|
|
229
291
|
* ```typescript
|
|
@@ -235,35 +297,52 @@ export async function publishThreadsContainer(
|
|
|
235
297
|
accessToken: string,
|
|
236
298
|
containerId: string
|
|
237
299
|
): Promise<string> {
|
|
238
|
-
const
|
|
239
|
-
const
|
|
300
|
+
const publishUrl = `${THREADS_API_BASE_URL}/${userId}/threads_publish`;
|
|
301
|
+
const publishBody = new URLSearchParams({
|
|
240
302
|
access_token: accessToken,
|
|
241
303
|
creation_id: containerId,
|
|
242
304
|
});
|
|
243
305
|
|
|
244
|
-
const
|
|
306
|
+
const publishResponse = await fetch(publishUrl, {
|
|
245
307
|
method: "POST",
|
|
246
|
-
body:
|
|
308
|
+
body: publishBody,
|
|
247
309
|
headers: {
|
|
248
310
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
249
311
|
},
|
|
250
312
|
});
|
|
251
313
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!response.ok) {
|
|
314
|
+
if (!publishResponse.ok) {
|
|
255
315
|
throw new Error(
|
|
256
|
-
`Failed to publish Threads container: ${
|
|
316
|
+
`Failed to publish Threads container: ${publishResponse.statusText}`
|
|
257
317
|
);
|
|
258
318
|
}
|
|
259
319
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
}
|
|
334
|
+
|
|
335
|
+
if (status === "ERROR") {
|
|
336
|
+
throw new Error(`Failed to publish container. Error: ${status}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (status !== "PUBLISHED" && status !== "FINISHED") {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Container not published after ${maxAttempts} attempts. Current status: ${status}`
|
|
342
|
+
);
|
|
266
343
|
}
|
|
344
|
+
|
|
345
|
+
return containerId; // Return the container ID as the published ID
|
|
267
346
|
}
|
|
268
347
|
|
|
269
348
|
/**
|
|
@@ -339,6 +418,12 @@ export function serveRequests() {
|
|
|
339
418
|
* @param userId - The user ID of the Threads account
|
|
340
419
|
* @param accessToken - The access token for authentication
|
|
341
420
|
* @returns A Promise that resolves to the rate limit usage information
|
|
421
|
+
* @throws Will throw an error if the API request fails
|
|
422
|
+
* @example
|
|
423
|
+
* ```typescript
|
|
424
|
+
* const rateLimit = await getPublishingLimit("123456", "your_access_token");
|
|
425
|
+
* console.log(`Current usage: ${rateLimit.quota_usage}`);
|
|
426
|
+
* ```
|
|
342
427
|
*/
|
|
343
428
|
export async function getPublishingLimit(
|
|
344
429
|
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
|
|
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";
|
|
@@ -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 = {
|
|
@@ -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({
|
|
@@ -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);
|