@codybrom/denim 1.3.4 → 1.3.6
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/deno.lock +40 -1
- package/examples/edge-function.ts +56 -20
- package/mock_threads_api.ts +174 -0
- package/mod.ts +293 -141
- package/mod_test.ts +405 -471
- package/package.json +1 -1
- package/types.ts +235 -0
package/deno.json
CHANGED
package/deno.lock
CHANGED
|
@@ -17,10 +17,49 @@
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
+
"redirects": {
|
|
21
|
+
"https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.224.0/testing/asserts.ts"
|
|
22
|
+
},
|
|
20
23
|
"remote": {
|
|
21
24
|
"https://deno.land/std@0.153.0/fmt/colors.ts": "ff7dc9c9f33a72bd48bc24b21bbc1b4545d8494a431f17894dbc5fe92a938fc4",
|
|
22
25
|
"https://deno.land/std@0.153.0/testing/_diff.ts": "141f978a283defc367eeee3ff7b58aa8763cf7c8e0c585132eae614468e9d7b8",
|
|
23
26
|
"https://deno.land/std@0.153.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832",
|
|
24
|
-
"https://deno.land/std@0.153.0/testing/
|
|
27
|
+
"https://deno.land/std@0.153.0/testing/_test_suite.ts": "2d07073d5460a4e3ec50c55ae822cd9bd136926d7363091379947fef9c73c3e4",
|
|
28
|
+
"https://deno.land/std@0.153.0/testing/asserts.ts": "d6595cfc330b4233546a047a0d7d57940771aa9d97a172ceb91e84ae6200b3af",
|
|
29
|
+
"https://deno.land/std@0.153.0/testing/bdd.ts": "35060cefd9cc21b414f4d89453b3551a3d52ec50aeff25db432503c5485b2f72",
|
|
30
|
+
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
|
31
|
+
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
|
32
|
+
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
|
33
|
+
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
|
34
|
+
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
|
35
|
+
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
|
36
|
+
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
|
37
|
+
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
|
38
|
+
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
|
39
|
+
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
|
40
|
+
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
|
41
|
+
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
|
42
|
+
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
|
43
|
+
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
|
44
|
+
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
|
45
|
+
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
|
46
|
+
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
|
47
|
+
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
|
48
|
+
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
|
49
|
+
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
|
50
|
+
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
|
51
|
+
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
|
52
|
+
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
|
53
|
+
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
|
54
|
+
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
|
55
|
+
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
|
56
|
+
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
|
57
|
+
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
|
58
|
+
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
|
59
|
+
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
|
60
|
+
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
|
61
|
+
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
|
62
|
+
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
|
|
63
|
+
"https://deno.land/std@0.224.0/testing/asserts.ts": "d0cdbabadc49cc4247a50732ee0df1403fdcd0f95360294ad448ae8c240f3f5c"
|
|
25
64
|
}
|
|
26
65
|
}
|
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
publishThreadsContainer,
|
|
7
7
|
createCarouselItem,
|
|
8
8
|
getPublishingLimit,
|
|
9
|
-
} from "jsr:@codybrom/denim@1.3.
|
|
9
|
+
} from "jsr:@codybrom/denim@1.3.5";
|
|
10
10
|
|
|
11
|
-
async function postToThreads(
|
|
11
|
+
async function postToThreads(
|
|
12
|
+
request: ThreadsPostRequest
|
|
13
|
+
): Promise<{ id: string; permalink: string }> {
|
|
12
14
|
try {
|
|
13
15
|
// Check rate limit
|
|
14
16
|
const rateLimit = await getPublishingLimit(
|
|
@@ -23,17 +25,40 @@ async function postToThreads(request: ThreadsPostRequest): Promise<string> {
|
|
|
23
25
|
delete request.imageUrl;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
console.log(
|
|
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
|
+
);
|
|
28
36
|
|
|
29
|
-
const
|
|
37
|
+
const publishedResult = await publishThreadsContainer(
|
|
30
38
|
request.userId,
|
|
31
39
|
request.accessToken,
|
|
32
|
-
|
|
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
|
+
}`
|
|
33
52
|
);
|
|
34
|
-
console.log(`Post published with ID: ${publishedId}`);
|
|
35
53
|
|
|
36
|
-
|
|
54
|
+
if (typeof publishedResult === "string") {
|
|
55
|
+
return { id: publishedResult, permalink: "" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
id: publishedResult.id,
|
|
60
|
+
permalink: publishedResult.permalink,
|
|
61
|
+
};
|
|
37
62
|
} catch (error) {
|
|
38
63
|
console.error("Error posting to Threads:", error);
|
|
39
64
|
throw error;
|
|
@@ -102,7 +127,6 @@ Deno.serve(async (req: Request) => {
|
|
|
102
127
|
videoUrl: body.videoUrl,
|
|
103
128
|
altText: body.altText,
|
|
104
129
|
linkAttachment: body.linkAttachment,
|
|
105
|
-
allowlistedCountryCodes: body.allowlistedCountryCodes,
|
|
106
130
|
replyControl: body.replyControl,
|
|
107
131
|
children: body.children,
|
|
108
132
|
};
|
|
@@ -117,14 +141,23 @@ Deno.serve(async (req: Request) => {
|
|
|
117
141
|
videoUrl: item.videoUrl,
|
|
118
142
|
altText: item.altText,
|
|
119
143
|
});
|
|
120
|
-
postRequest.children.push(
|
|
144
|
+
postRequest.children.push(
|
|
145
|
+
typeof itemId === "string" ? itemId : itemId.id
|
|
146
|
+
);
|
|
121
147
|
}
|
|
122
148
|
}
|
|
123
|
-
const
|
|
124
|
-
return new Response(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
);
|
|
128
161
|
} catch (error) {
|
|
129
162
|
console.error("Error processing request:", error);
|
|
130
163
|
return new Response(
|
|
@@ -169,10 +202,11 @@ Deno.serve(async (req: Request) => {
|
|
|
169
202
|
"userId": "YOUR_USER_ID",
|
|
170
203
|
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
171
204
|
"mediaType": "TEXT",
|
|
172
|
-
"text": "Hello from Denim!"
|
|
205
|
+
"text": "Hello from Denim!",
|
|
206
|
+
"linkAttachment": "https://example.com"
|
|
173
207
|
}'
|
|
174
208
|
|
|
175
|
-
# Post an image Thread
|
|
209
|
+
# Post an image Thread with alt text
|
|
176
210
|
curl -X POST <YOUR_FUNCTION_URI>/post \
|
|
177
211
|
-H "Content-Type: application/json" \
|
|
178
212
|
-d '{
|
|
@@ -180,10 +214,11 @@ Deno.serve(async (req: Request) => {
|
|
|
180
214
|
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
181
215
|
"mediaType": "IMAGE",
|
|
182
216
|
"text": "Check out this image I posted with Denim!",
|
|
183
|
-
"imageUrl": "https://example.com/image.jpg"
|
|
217
|
+
"imageUrl": "https://example.com/image.jpg",
|
|
218
|
+
"altText": "A beautiful sunset over the ocean"
|
|
184
219
|
}'
|
|
185
220
|
|
|
186
|
-
# Post a video Thread
|
|
221
|
+
# Post a video Thread with reply control
|
|
187
222
|
curl -X POST <YOUR_FUNCTION_URI>/post \
|
|
188
223
|
-H "Content-Type: application/json" \
|
|
189
224
|
-d '{
|
|
@@ -191,7 +226,8 @@ Deno.serve(async (req: Request) => {
|
|
|
191
226
|
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
192
227
|
"mediaType": "VIDEO",
|
|
193
228
|
"text": "Watch this video I posted with Denim!",
|
|
194
|
-
"videoUrl": "https://example.com/video.mp4"
|
|
229
|
+
"videoUrl": "https://example.com/video.mp4",
|
|
230
|
+
"replyControl": "mentioned_only"
|
|
195
231
|
}'
|
|
196
232
|
|
|
197
233
|
# Post a carousel Thread
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// mock_threads_api.ts
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ThreadsContainer,
|
|
5
|
+
ThreadsPost,
|
|
6
|
+
ThreadsProfile,
|
|
7
|
+
PublishingLimit,
|
|
8
|
+
ThreadsPostRequest,
|
|
9
|
+
ThreadsListResponse,
|
|
10
|
+
} from "./types.ts";
|
|
11
|
+
|
|
12
|
+
export class MockThreadsAPI implements MockThreadsAPI {
|
|
13
|
+
private containers: Map<string, ThreadsContainer> = new Map();
|
|
14
|
+
private posts: Map<string, ThreadsPost> = new Map();
|
|
15
|
+
private users: Map<string, ThreadsProfile> = new Map();
|
|
16
|
+
private publishingLimits: Map<string, PublishingLimit> = new Map();
|
|
17
|
+
private errorMode = false;
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
// Initialize with some sample data
|
|
21
|
+
this.users.set("12345", {
|
|
22
|
+
id: "12345",
|
|
23
|
+
username: "testuser",
|
|
24
|
+
name: "Test User",
|
|
25
|
+
threadsProfilePictureUrl: "https://example.com/profile.jpg",
|
|
26
|
+
threadsBiography: "This is a test user",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this.publishingLimits.set("12345", {
|
|
30
|
+
quota_usage: 10,
|
|
31
|
+
config: {
|
|
32
|
+
quota_total: 250,
|
|
33
|
+
quota_duration: 86400,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setErrorMode(mode: boolean) {
|
|
39
|
+
this.errorMode = mode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
createThreadsContainer(
|
|
43
|
+
request: ThreadsPostRequest
|
|
44
|
+
): Promise<string | { id: string; permalink: string }> {
|
|
45
|
+
if (this.errorMode) {
|
|
46
|
+
return Promise.reject(new Error("Failed to create Threads container"));
|
|
47
|
+
}
|
|
48
|
+
const containerId = `container_${Math.random().toString(36).substring(7)}`;
|
|
49
|
+
const permalink = `https://www.threads.net/@${request.userId}/post/${containerId}`;
|
|
50
|
+
const container: ThreadsContainer = {
|
|
51
|
+
id: containerId,
|
|
52
|
+
permalink,
|
|
53
|
+
status: "FINISHED",
|
|
54
|
+
};
|
|
55
|
+
this.containers.set(containerId, container);
|
|
56
|
+
|
|
57
|
+
// Create a post immediately when creating a container
|
|
58
|
+
const postId = `post_${Math.random().toString(36).substring(7)}`;
|
|
59
|
+
const post: ThreadsPost = {
|
|
60
|
+
id: postId,
|
|
61
|
+
media_product_type: "THREADS",
|
|
62
|
+
media_type: request.mediaType,
|
|
63
|
+
permalink,
|
|
64
|
+
owner: { id: request.userId },
|
|
65
|
+
username: "testuser",
|
|
66
|
+
text: request.text || "",
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
shortcode: postId,
|
|
69
|
+
is_quote_post: false,
|
|
70
|
+
hasReplies: false,
|
|
71
|
+
isReply: false,
|
|
72
|
+
isReplyOwnedByMe: false,
|
|
73
|
+
};
|
|
74
|
+
this.posts.set(postId, post);
|
|
75
|
+
|
|
76
|
+
// Always return an object with both id and permalink
|
|
77
|
+
return Promise.resolve({ id: containerId, permalink });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
publishThreadsContainer(
|
|
81
|
+
_userId: string,
|
|
82
|
+
_accessToken: string,
|
|
83
|
+
containerId: string,
|
|
84
|
+
getPermalink: boolean = false
|
|
85
|
+
): Promise<string | { id: string; permalink: string }> {
|
|
86
|
+
if (this.errorMode) {
|
|
87
|
+
return Promise.reject(new Error("Failed to publish Threads container"));
|
|
88
|
+
}
|
|
89
|
+
const container = this.containers.get(containerId);
|
|
90
|
+
if (!container) {
|
|
91
|
+
return Promise.reject(new Error("Container not found"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Find the post associated with this container
|
|
95
|
+
const existingPost = Array.from(this.posts.values()).find(
|
|
96
|
+
(post) => post.permalink === container.permalink
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!existingPost) {
|
|
100
|
+
return Promise.reject(
|
|
101
|
+
new Error("Post not found for the given container")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Promise.resolve(
|
|
106
|
+
getPermalink
|
|
107
|
+
? { id: existingPost.id, permalink: existingPost.permalink || "" }
|
|
108
|
+
: existingPost.id
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
createCarouselItem(
|
|
113
|
+
request: Omit<ThreadsPostRequest, "mediaType"> & {
|
|
114
|
+
mediaType: "IMAGE" | "VIDEO";
|
|
115
|
+
}
|
|
116
|
+
): Promise<string | { id: string }> {
|
|
117
|
+
const itemId = `item_${Math.random().toString(36).substring(7)}`;
|
|
118
|
+
const container: ThreadsContainer = {
|
|
119
|
+
id: itemId,
|
|
120
|
+
permalink: `https://www.threads.net/@${request.userId}/post/${itemId}`,
|
|
121
|
+
status: "FINISHED",
|
|
122
|
+
};
|
|
123
|
+
this.containers.set(itemId, container);
|
|
124
|
+
return Promise.resolve({ id: itemId });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getPublishingLimit(
|
|
128
|
+
userId: string,
|
|
129
|
+
_accessToken: string
|
|
130
|
+
): Promise<PublishingLimit> {
|
|
131
|
+
if (this.errorMode) {
|
|
132
|
+
return Promise.reject(new Error("Failed to get publishing limit"));
|
|
133
|
+
}
|
|
134
|
+
const limit = this.publishingLimits.get(userId);
|
|
135
|
+
if (!limit) {
|
|
136
|
+
return Promise.reject(new Error("Publishing limit not found"));
|
|
137
|
+
}
|
|
138
|
+
return Promise.resolve(limit);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getThreadsList(
|
|
142
|
+
userId: string,
|
|
143
|
+
_accessToken: string,
|
|
144
|
+
options?: {
|
|
145
|
+
since?: string;
|
|
146
|
+
until?: string;
|
|
147
|
+
limit?: number;
|
|
148
|
+
after?: string;
|
|
149
|
+
before?: string;
|
|
150
|
+
}
|
|
151
|
+
): Promise<ThreadsListResponse> {
|
|
152
|
+
const threads = Array.from(this.posts.values())
|
|
153
|
+
.filter((post) => post.owner.id === userId)
|
|
154
|
+
.slice(0, options?.limit || 25);
|
|
155
|
+
|
|
156
|
+
return Promise.resolve({
|
|
157
|
+
data: threads as ThreadsPost[],
|
|
158
|
+
paging: {
|
|
159
|
+
cursors: {
|
|
160
|
+
before: "BEFORE_CURSOR",
|
|
161
|
+
after: "AFTER_CURSOR",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getSingleThread(mediaId: string, _accessToken: string): Promise<ThreadsPost> {
|
|
168
|
+
const post = this.posts.get(mediaId);
|
|
169
|
+
if (!post) {
|
|
170
|
+
return Promise.reject(new Error("Thread not found"));
|
|
171
|
+
}
|
|
172
|
+
return Promise.resolve(post as ThreadsPost);
|
|
173
|
+
}
|
|
174
|
+
}
|