@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.
Files changed (46) hide show
  1. package/.github/workflows/publish.yml +17 -7
  2. package/.vscode/settings.json +34 -9
  3. package/CHANGELOG.md +128 -0
  4. package/deno.json +22 -8
  5. package/deno.lock +17 -59
  6. package/examples/edge-function.ts +171 -177
  7. package/mod.ts +138 -635
  8. package/mod_test.ts +1287 -431
  9. package/package.json +22 -22
  10. package/readme.md +155 -191
  11. package/src/api/createCarouselItem.ts +86 -0
  12. package/src/api/createThreadsContainer.ts +122 -0
  13. package/src/api/debugToken.ts +35 -0
  14. package/src/api/deleteThread.ts +36 -0
  15. package/src/api/exchangeCodeForToken.ts +50 -0
  16. package/src/api/exchangeToken.ts +36 -0
  17. package/src/api/getAppAccessToken.ts +35 -0
  18. package/src/api/getConversation.ts +51 -0
  19. package/src/api/getGhostPosts.ts +50 -0
  20. package/src/api/getLocation.ts +38 -0
  21. package/src/api/getMediaInsights.ts +39 -0
  22. package/src/api/getMentions.ts +57 -0
  23. package/src/api/getOEmbed.ts +41 -0
  24. package/src/api/getProfile.ts +46 -0
  25. package/src/api/getProfilePosts.ts +53 -0
  26. package/src/api/getPublishingLimit.ts +59 -0
  27. package/src/api/getReplies.ts +51 -0
  28. package/src/api/getSingleThread.ts +37 -0
  29. package/src/api/getThreadsList.ts +49 -0
  30. package/src/api/getUserInsights.ts +54 -0
  31. package/src/api/getUserReplies.ts +54 -0
  32. package/src/api/lookupProfile.ts +53 -0
  33. package/src/api/manageReply.ts +41 -0
  34. package/src/api/publishThreadsContainer.ts +107 -0
  35. package/src/api/refreshToken.ts +33 -0
  36. package/src/api/repost.ts +38 -0
  37. package/src/api/searchKeyword.ts +86 -0
  38. package/src/api/searchLocations.ts +46 -0
  39. package/src/constants.ts +80 -0
  40. package/src/types.ts +925 -0
  41. package/src/utils/checkContainerStatus.ts +39 -0
  42. package/src/utils/getAPI.ts +13 -0
  43. package/src/utils/mock_threads_api.ts +582 -0
  44. package/src/utils/validateRequest.ts +166 -0
  45. package/mock_threads_api.ts +0 -174
  46. package/types.ts +0 -235
@@ -1,196 +1,190 @@
1
1
  // examples/edge-function.ts
2
2
 
3
3
  import {
4
- type ThreadsPostRequest,
5
- createThreadsContainer,
6
- publishThreadsContainer,
7
- createCarouselItem,
8
- getPublishingLimit,
9
- } from "jsr:@codybrom/denim@1.3.5";
4
+ createCarouselItem,
5
+ createThreadsContainer,
6
+ getPublishingLimit,
7
+ publishThreadsContainer,
8
+ type ThreadsPostRequest,
9
+ } from "@codybrom/denim";
10
10
 
11
11
  async function postToThreads(
12
- request: ThreadsPostRequest
12
+ request: ThreadsPostRequest,
13
13
  ): Promise<{ id: string; permalink: string }> {
14
- try {
15
- // Check rate limit
16
- const rateLimit = await getPublishingLimit(
17
- request.userId,
18
- request.accessToken
19
- );
20
- if (rateLimit.quota_usage >= rateLimit.config.quota_total) {
21
- throw new Error("Rate limit exceeded. Please try again later.");
22
- }
23
-
24
- if (request.mediaType === "VIDEO" && request.videoUrl) {
25
- delete request.imageUrl;
26
- }
27
-
28
- const containerResult = await createThreadsContainer(request);
29
- console.log(
30
- `Container created with ID: ${
31
- typeof containerResult === "string"
32
- ? containerResult
33
- : containerResult.id
34
- }`
35
- );
36
-
37
- const publishedResult = await publishThreadsContainer(
38
- request.userId,
39
- request.accessToken,
40
- typeof containerResult === "string"
41
- ? containerResult
42
- : containerResult.id,
43
- true // Get permalink
44
- );
45
-
46
- console.log(
47
- `Post published with ID: ${
48
- typeof publishedResult === "string"
49
- ? publishedResult
50
- : publishedResult.id
51
- }`
52
- );
53
-
54
- if (typeof publishedResult === "string") {
55
- return { id: publishedResult, permalink: "" };
56
- }
57
-
58
- return {
59
- id: publishedResult.id,
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
- const url = new URL(req.url);
70
- const paths = url.pathname.split("/").filter((segment) => segment !== "");
71
-
72
- switch (req.method) {
73
- case "GET": {
74
- switch (paths[1]) {
75
- case "rate-limit": {
76
- const userId = url.searchParams.get("userId");
77
- const accessToken = url.searchParams.get("accessToken");
78
- if (!userId || !accessToken) {
79
- return new Response(
80
- JSON.stringify({ error: "Missing userId or accessToken" }),
81
- {
82
- status: 400,
83
- headers: { "Content-Type": "application/json" },
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
- });
98
- }
99
- }
100
- default: {
101
- return new Response("Not Found", { status: 404 });
102
- }
103
- }
104
- }
105
- case "POST": {
106
- if (paths[1] === "post") {
107
- try {
108
- const body = await req.json();
109
- if (!body.userId || !body.accessToken || !body.mediaType) {
110
- return new Response(
111
- JSON.stringify({
112
- success: false,
113
- error: "Missing required fields",
114
- }),
115
- {
116
- status: 400,
117
- headers: { "Content-Type": "application/json" },
118
- }
119
- );
120
- }
121
- const postRequest: ThreadsPostRequest = {
122
- userId: body.userId,
123
- accessToken: body.accessToken,
124
- mediaType: body.mediaType,
125
- text: body.text,
126
- imageUrl: body.imageUrl,
127
- videoUrl: body.videoUrl,
128
- altText: body.altText,
129
- linkAttachment: body.linkAttachment,
130
- replyControl: body.replyControl,
131
- children: body.children,
132
- };
133
- if (postRequest.mediaType === "CAROUSEL" && body.carouselItems) {
134
- postRequest.children = [];
135
- for (const item of body.carouselItems) {
136
- const itemId = await createCarouselItem({
137
- userId: postRequest.userId,
138
- accessToken: postRequest.accessToken,
139
- mediaType: item.mediaType,
140
- imageUrl: item.imageUrl,
141
- videoUrl: item.videoUrl,
142
- altText: item.altText,
143
- });
144
- postRequest.children.push(
145
- typeof itemId === "string" ? itemId : itemId.id
146
- );
147
- }
148
- }
149
- const publishedResult = await postToThreads(postRequest);
150
- return new Response(
151
- JSON.stringify({
152
- success: true,
153
- id: publishedResult.id,
154
- permalink: publishedResult.permalink,
155
- }),
156
- {
157
- status: 200,
158
- headers: { "Content-Type": "application/json" },
159
- }
160
- );
161
- } catch (error) {
162
- console.error("Error processing request:", error);
163
- return new Response(
164
- JSON.stringify({ success: false, error: error.message }),
165
- {
166
- status: 500,
167
- headers: { "Content-Type": "application/json" },
168
- }
169
- );
170
- }
171
- }
172
- return new Response("Not Found", { status: 404 });
173
- }
174
- default:
175
- return new Response("Method Not Allowed", { status: 405 });
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