@codybrom/denim 1.3.6 → 2.0.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/.github/workflows/publish.yml +17 -7
- package/.vscode/settings.json +34 -9
- package/CHANGELOG.md +128 -0
- package/deno.json +22 -8
- package/deno.lock +17 -59
- package/examples/edge-function.ts +171 -177
- package/mod.ts +138 -635
- package/mod_test.ts +1287 -431
- package/package.json +22 -22
- package/readme.md +155 -191
- package/src/api/createCarouselItem.ts +86 -0
- package/src/api/createThreadsContainer.ts +122 -0
- package/src/api/debugToken.ts +35 -0
- package/src/api/deleteThread.ts +36 -0
- package/src/api/exchangeCodeForToken.ts +50 -0
- package/src/api/exchangeToken.ts +36 -0
- package/src/api/getAppAccessToken.ts +35 -0
- package/src/api/getConversation.ts +51 -0
- package/src/api/getGhostPosts.ts +50 -0
- package/src/api/getLocation.ts +38 -0
- package/src/api/getMediaInsights.ts +39 -0
- package/src/api/getMentions.ts +57 -0
- package/src/api/getOEmbed.ts +41 -0
- package/src/api/getProfile.ts +46 -0
- package/src/api/getProfilePosts.ts +53 -0
- package/src/api/getPublishingLimit.ts +59 -0
- package/src/api/getReplies.ts +51 -0
- package/src/api/getSingleThread.ts +37 -0
- package/src/api/getThreadsList.ts +49 -0
- package/src/api/getUserInsights.ts +54 -0
- package/src/api/getUserReplies.ts +54 -0
- package/src/api/lookupProfile.ts +53 -0
- package/src/api/manageReply.ts +41 -0
- package/src/api/publishThreadsContainer.ts +107 -0
- package/src/api/refreshToken.ts +33 -0
- package/src/api/repost.ts +38 -0
- package/src/api/searchKeyword.ts +86 -0
- package/src/api/searchLocations.ts +46 -0
- package/src/constants.ts +80 -0
- package/src/types.ts +925 -0
- package/src/utils/checkContainerStatus.ts +39 -0
- package/src/utils/getAPI.ts +13 -0
- package/src/utils/mock_threads_api.ts +582 -0
- package/src/utils/validateRequest.ts +166 -0
- package/mock_threads_api.ts +0 -174
- package/types.ts +0 -235
|
@@ -1,196 +1,190 @@
|
|
|
1
1
|
// examples/edge-function.ts
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "
|
|
4
|
+
createCarouselItem,
|
|
5
|
+
createThreadsContainer,
|
|
6
|
+
getPublishingLimit,
|
|
7
|
+
publishThreadsContainer,
|
|
8
|
+
type ThreadsPostRequest,
|
|
9
|
+
} from "@codybrom/denim";
|
|
10
10
|
|
|
11
11
|
async function postToThreads(
|
|
12
|
-
|
|
12
|
+
request: ThreadsPostRequest,
|
|
13
13
|
): Promise<{ id: string; permalink: string }> {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
permalink: publishedResult.permalink,
|
|
61
|
-
};
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error("Error posting to Threads:", error);
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
14
|
+
try {
|
|
15
|
+
// Check rate limit
|
|
16
|
+
const rateLimit = await getPublishingLimit(
|
|
17
|
+
request.userId,
|
|
18
|
+
request.accessToken,
|
|
19
|
+
);
|
|
20
|
+
if (
|
|
21
|
+
(rateLimit.quota_usage ?? 0) >= (rateLimit.config?.quota_total ?? 250)
|
|
22
|
+
) {
|
|
23
|
+
throw new Error("Rate limit exceeded. Please try again later.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (request.mediaType === "VIDEO" && request.videoUrl) {
|
|
27
|
+
delete request.imageUrl;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const containerId = await createThreadsContainer(request);
|
|
31
|
+
console.log(`Container created with ID: ${containerId}`);
|
|
32
|
+
|
|
33
|
+
const publishedResult = await publishThreadsContainer(
|
|
34
|
+
request.userId,
|
|
35
|
+
request.accessToken,
|
|
36
|
+
containerId,
|
|
37
|
+
true, // Get permalink
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
console.log(
|
|
41
|
+
`Post published with ID: ${
|
|
42
|
+
typeof publishedResult === "string"
|
|
43
|
+
? publishedResult
|
|
44
|
+
: publishedResult.id
|
|
45
|
+
}`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (typeof publishedResult === "string") {
|
|
49
|
+
return { id: publishedResult, permalink: "" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: publishedResult.id,
|
|
54
|
+
permalink: publishedResult.permalink,
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("Error posting to Threads:", error);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
Deno.serve(async (req: Request) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
63
|
+
const url = new URL(req.url);
|
|
64
|
+
const paths = url.pathname.split("/").filter((segment) => segment !== "");
|
|
65
|
+
|
|
66
|
+
switch (req.method) {
|
|
67
|
+
case "GET": {
|
|
68
|
+
switch (paths[1]) {
|
|
69
|
+
case "rate-limit": {
|
|
70
|
+
const userId = url.searchParams.get("userId");
|
|
71
|
+
const accessToken = url.searchParams.get("accessToken");
|
|
72
|
+
if (!userId || !accessToken) {
|
|
73
|
+
return new Response(
|
|
74
|
+
JSON.stringify({ error: "Missing userId or accessToken" }),
|
|
75
|
+
{
|
|
76
|
+
status: 400,
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const rateLimit = await getPublishingLimit(userId, accessToken);
|
|
83
|
+
return new Response(JSON.stringify(rateLimit), {
|
|
84
|
+
status: 200,
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
});
|
|
87
|
+
} catch (error: unknown) {
|
|
88
|
+
const message = error instanceof Error
|
|
89
|
+
? error.message
|
|
90
|
+
: String(error);
|
|
91
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
92
|
+
status: 500,
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
default: {
|
|
98
|
+
return new Response("Not Found", { status: 404 });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
case "POST": {
|
|
103
|
+
if (paths[1] === "post") {
|
|
104
|
+
try {
|
|
105
|
+
const body = await req.json();
|
|
106
|
+
if (!body.userId || !body.accessToken || !body.mediaType) {
|
|
107
|
+
return new Response(
|
|
108
|
+
JSON.stringify({
|
|
109
|
+
success: false,
|
|
110
|
+
error: "Missing required fields",
|
|
111
|
+
}),
|
|
112
|
+
{
|
|
113
|
+
status: 400,
|
|
114
|
+
headers: { "Content-Type": "application/json" },
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const postRequest: ThreadsPostRequest = {
|
|
119
|
+
userId: body.userId,
|
|
120
|
+
accessToken: body.accessToken,
|
|
121
|
+
mediaType: body.mediaType,
|
|
122
|
+
text: body.text,
|
|
123
|
+
imageUrl: body.imageUrl,
|
|
124
|
+
videoUrl: body.videoUrl,
|
|
125
|
+
altText: body.altText,
|
|
126
|
+
linkAttachment: body.linkAttachment,
|
|
127
|
+
replyControl: body.replyControl,
|
|
128
|
+
children: body.children,
|
|
129
|
+
};
|
|
130
|
+
if (postRequest.mediaType === "CAROUSEL" && body.carouselItems) {
|
|
131
|
+
postRequest.children = [];
|
|
132
|
+
for (const item of body.carouselItems) {
|
|
133
|
+
const itemId = await createCarouselItem({
|
|
134
|
+
userId: postRequest.userId,
|
|
135
|
+
accessToken: postRequest.accessToken,
|
|
136
|
+
mediaType: item.mediaType,
|
|
137
|
+
imageUrl: item.imageUrl,
|
|
138
|
+
videoUrl: item.videoUrl,
|
|
139
|
+
altText: item.altText,
|
|
140
|
+
});
|
|
141
|
+
postRequest.children.push(itemId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const publishedResult = await postToThreads(postRequest);
|
|
145
|
+
return new Response(
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
success: true,
|
|
148
|
+
id: publishedResult.id,
|
|
149
|
+
permalink: publishedResult.permalink,
|
|
150
|
+
}),
|
|
151
|
+
{
|
|
152
|
+
status: 200,
|
|
153
|
+
headers: { "Content-Type": "application/json" },
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
} catch (error: unknown) {
|
|
157
|
+
console.error("Error processing request:", error);
|
|
158
|
+
const message = error instanceof Error
|
|
159
|
+
? error.message
|
|
160
|
+
: String(error);
|
|
161
|
+
return new Response(
|
|
162
|
+
JSON.stringify({ success: false, error: message }),
|
|
163
|
+
{
|
|
164
|
+
status: 500,
|
|
165
|
+
headers: { "Content-Type": "application/json" },
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return new Response("Not Found", { status: 404 });
|
|
171
|
+
}
|
|
172
|
+
default:
|
|
173
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
174
|
+
}
|
|
177
175
|
});
|
|
178
176
|
|
|
179
177
|
/*
|
|
180
178
|
To use this example:
|
|
181
|
-
|
|
179
|
+
|
|
182
180
|
1. Deploy this file to your serverless platform that supports Deno.
|
|
183
|
-
|
|
181
|
+
|
|
184
182
|
2. Send requests to <YOUR_FUNCTION_URI> with the following endpoints:
|
|
185
|
-
|
|
186
|
-
GET /health - Check the API health status
|
|
183
|
+
|
|
187
184
|
GET /rate-limit?userId=YOUR_USER_ID&accessToken=YOUR_ACCESS_TOKEN - Check rate limit
|
|
188
185
|
POST /post - Create and publish a post (see below for details)
|
|
189
186
|
|
|
190
187
|
Example curl commands:
|
|
191
|
-
|
|
192
|
-
# Check API health
|
|
193
|
-
curl -X GET <YOUR_FUNCTION_URI>/health
|
|
194
188
|
|
|
195
189
|
# Check rate limit
|
|
196
190
|
curl -X GET "<YOUR_FUNCTION_URI>/rate-limit?userId=YOUR_USER_ID&accessToken=YOUR_ACCESS_TOKEN"
|
|
@@ -217,7 +211,7 @@ Deno.serve(async (req: Request) => {
|
|
|
217
211
|
"imageUrl": "https://example.com/image.jpg",
|
|
218
212
|
"altText": "A beautiful sunset over the ocean"
|
|
219
213
|
}'
|
|
220
|
-
|
|
214
|
+
|
|
221
215
|
# Post a video Thread with reply control
|
|
222
216
|
curl -X POST <YOUR_FUNCTION_URI>/post \
|
|
223
217
|
-H "Content-Type: application/json" \
|
|
@@ -229,7 +223,7 @@ Deno.serve(async (req: Request) => {
|
|
|
229
223
|
"videoUrl": "https://example.com/video.mp4",
|
|
230
224
|
"replyControl": "mentioned_only"
|
|
231
225
|
}'
|
|
232
|
-
|
|
226
|
+
|
|
233
227
|
# Post a carousel Thread
|
|
234
228
|
curl -X POST <YOUR_FUNCTION_URI>/post \
|
|
235
229
|
-H "Content-Type: application/json" \
|
|
@@ -251,7 +245,7 @@ Deno.serve(async (req: Request) => {
|
|
|
251
245
|
}
|
|
252
246
|
]
|
|
253
247
|
}'
|
|
254
|
-
|
|
248
|
+
|
|
255
249
|
Note: If both videoUrl and imageUrl are provided in a request with mediaType "VIDEO",
|
|
256
250
|
the imageUrl will be ignored, and only the video will be posted.
|
|
257
251
|
|