@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codybrom/denim",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "A Deno function for posting to Threads.",
5
5
  "entry": "./mod.ts",
6
6
  "exports": {
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/asserts.ts": "d6595cfc330b4233546a047a0d7d57940771aa9d97a172ceb91e84ae6200b3af"
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.0";
9
+ } from "jsr:@codybrom/denim@1.3.5";
10
10
 
11
- async function postToThreads(request: ThreadsPostRequest): Promise<string> {
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 containerId = await createThreadsContainer(request);
27
- console.log(`Container created with ID: ${containerId}`);
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 publishedId = await publishThreadsContainer(
37
+ const publishedResult = await publishThreadsContainer(
30
38
  request.userId,
31
39
  request.accessToken,
32
- containerId
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
- return publishedId;
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(itemId);
144
+ postRequest.children.push(
145
+ typeof itemId === "string" ? itemId : itemId.id
146
+ );
121
147
  }
122
148
  }
123
- const publishedId = await postToThreads(postRequest);
124
- return new Response(JSON.stringify({ success: true, publishedId }), {
125
- status: 200,
126
- headers: { "Content-Type": "application/json" },
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
+ }