@codybrom/denim 1.0.2
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 +19 -0
- package/LICENSE +21 -0
- package/deno.json +9 -0
- package/deno.lock +26 -0
- package/examples/edge-function.ts +159 -0
- package/mod.ts +204 -0
- package/mod_test.ts +120 -0
- package/package.json +23 -0
- package/readme.md +162 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Publish package
|
|
19
|
+
run: npx jsr publish
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Cody Bromley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/deno.json
ADDED
package/deno.lock
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "3",
|
|
3
|
+
"packages": {
|
|
4
|
+
"specifiers": {
|
|
5
|
+
"jsr:@std/assert@1": "jsr:@std/assert@1.0.1",
|
|
6
|
+
"jsr:@std/internal@^1.0.1": "jsr:@std/internal@1.0.1"
|
|
7
|
+
},
|
|
8
|
+
"jsr": {
|
|
9
|
+
"@std/assert@1.0.1": {
|
|
10
|
+
"integrity": "13590ef8e5854f59e4ad252fd987e83239a1bf1f16cb9c69c1d123ebb807a75b",
|
|
11
|
+
"dependencies": [
|
|
12
|
+
"jsr:@std/internal@^1.0.1"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"@std/internal@1.0.1": {
|
|
16
|
+
"integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"remote": {
|
|
21
|
+
"https://deno.land/std@0.153.0/fmt/colors.ts": "ff7dc9c9f33a72bd48bc24b21bbc1b4545d8494a431f17894dbc5fe92a938fc4",
|
|
22
|
+
"https://deno.land/std@0.153.0/testing/_diff.ts": "141f978a283defc367eeee3ff7b58aa8763cf7c8e0c585132eae614468e9d7b8",
|
|
23
|
+
"https://deno.land/std@0.153.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832",
|
|
24
|
+
"https://deno.land/std@0.153.0/testing/asserts.ts": "d6595cfc330b4233546a047a0d7d57940771aa9d97a172ceb91e84ae6200b3af"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// examples/edge-function.ts
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ThreadsPostRequest,
|
|
5
|
+
createThreadsContainer,
|
|
6
|
+
publishThreadsContainer,
|
|
7
|
+
} from "jsr:@codybrom/denim@^1.0.2";
|
|
8
|
+
|
|
9
|
+
async function postToThreads(request: ThreadsPostRequest): Promise<string> {
|
|
10
|
+
try {
|
|
11
|
+
if (request.mediaType === "VIDEO" && request.videoUrl) {
|
|
12
|
+
delete request.imageUrl;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const containerId = await createThreadsContainer(request);
|
|
16
|
+
console.log(`Container created with ID: ${containerId}`);
|
|
17
|
+
|
|
18
|
+
const publishedId = await publishThreadsContainer(
|
|
19
|
+
request.userId,
|
|
20
|
+
request.accessToken,
|
|
21
|
+
containerId
|
|
22
|
+
);
|
|
23
|
+
console.log(`Post published with ID: ${publishedId}`);
|
|
24
|
+
|
|
25
|
+
return publishedId;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error("Error posting to Threads:", error);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Deno.serve(async (req: Request) => {
|
|
33
|
+
// Health check endpoint
|
|
34
|
+
if (req.method === "GET" && new URL(req.url).pathname === "/health") {
|
|
35
|
+
return new Response(JSON.stringify({ status: "ok" }), {
|
|
36
|
+
status: 200,
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (req.method !== "POST") {
|
|
42
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Log incoming request (without sensitive data)
|
|
46
|
+
console.log(`Received ${req.method} request to ${new URL(req.url).pathname}`);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
let body;
|
|
50
|
+
try {
|
|
51
|
+
body = await req.json();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return new Response(
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
success: false,
|
|
56
|
+
error: "Invalid JSON in request body",
|
|
57
|
+
details: error.message,
|
|
58
|
+
}),
|
|
59
|
+
{
|
|
60
|
+
status: 400,
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!body.userId || !body.accessToken || !body.mediaType) {
|
|
67
|
+
return new Response(
|
|
68
|
+
JSON.stringify({ success: false, error: "Missing required fields" }),
|
|
69
|
+
{
|
|
70
|
+
status: 400,
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const postRequest: ThreadsPostRequest = {
|
|
77
|
+
userId: body.userId,
|
|
78
|
+
accessToken: body.accessToken,
|
|
79
|
+
mediaType: body.mediaType,
|
|
80
|
+
text: body.text,
|
|
81
|
+
imageUrl: body.imageUrl,
|
|
82
|
+
videoUrl: body.videoUrl,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const publishedId = await postToThreads(postRequest);
|
|
86
|
+
|
|
87
|
+
return new Response(JSON.stringify({ success: true, publishedId }), {
|
|
88
|
+
status: 200,
|
|
89
|
+
headers: { "Content-Type": "application/json" },
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("Error processing request:", error);
|
|
93
|
+
return new Response(
|
|
94
|
+
JSON.stringify({ success: false, error: error.message }),
|
|
95
|
+
{
|
|
96
|
+
status: 500,
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
To use this example:
|
|
105
|
+
|
|
106
|
+
1. Deploy this file to your serverless platform that supports Deno.
|
|
107
|
+
|
|
108
|
+
2. Send POST requests to <YOUR_FUNCTION_URI> with JSON body containing:
|
|
109
|
+
- YOUR_AUTH_KEY: Your custom authorization key (if used, otherwise remove the header)
|
|
110
|
+
- userId: Your Threads user ID
|
|
111
|
+
- accessToken: Your Threads API access token
|
|
112
|
+
- mediaType: "TEXT", "IMAGE", or "VIDEO"
|
|
113
|
+
- text: The text content of your post
|
|
114
|
+
- imageUrl: URL of the image (for IMAGE posts)
|
|
115
|
+
- videoUrl: URL of the video (for VIDEO posts)
|
|
116
|
+
|
|
117
|
+
Example curl commands:
|
|
118
|
+
|
|
119
|
+
# Post a text-only Thread
|
|
120
|
+
curl -X POST <YOUR_FUNCTION_URI> \
|
|
121
|
+
-H "Content-Type: application/json" \
|
|
122
|
+
-H "Authorization: Bearer YOUR_AUTH_KEY" \
|
|
123
|
+
-d '{
|
|
124
|
+
"userId": "YOUR_USER_ID",
|
|
125
|
+
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
126
|
+
"mediaType": "TEXT",
|
|
127
|
+
"text": "Hello from Denim!"
|
|
128
|
+
}'
|
|
129
|
+
|
|
130
|
+
# Post an image Thread
|
|
131
|
+
curl -X POST <YOUR_FUNCTION_URI> \
|
|
132
|
+
-H "Content-Type: application/json" \
|
|
133
|
+
-H "Authorization: Bearer YOUR_AUTH_KEY" \
|
|
134
|
+
-d '{
|
|
135
|
+
"userId": "YOUR_USER_ID",
|
|
136
|
+
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
137
|
+
"mediaType": "IMAGE",
|
|
138
|
+
"text": "Check out this image I posted with Denim!",
|
|
139
|
+
"imageUrl": "https://example.com/image.jpg"
|
|
140
|
+
}'
|
|
141
|
+
|
|
142
|
+
# Post a video Thread
|
|
143
|
+
curl -X POST <YOUR_FUNCTION_URI> \
|
|
144
|
+
-H "Content-Type: application/json" \
|
|
145
|
+
-H "Authorization: Bearer YOUR_AUTH_KEY" \
|
|
146
|
+
-d '{
|
|
147
|
+
"userId": "YOUR_USER_ID",
|
|
148
|
+
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
149
|
+
"mediaType": "VIDEO",
|
|
150
|
+
"text": "Watch this video I posted with Denim!",
|
|
151
|
+
"videoUrl": "https://example.com/video.mp4"
|
|
152
|
+
}'
|
|
153
|
+
|
|
154
|
+
Note: If both videoUrl and imageUrl are provided in a request with mediaType "VIDEO",
|
|
155
|
+
the imageUrl will be ignored, and only the video will be posted.
|
|
156
|
+
|
|
157
|
+
Security Note: Ensure that your function is deployed with appropriate access controls
|
|
158
|
+
and authentication mechanisms to protect sensitive data like access tokens.
|
|
159
|
+
*/
|
package/mod.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to interact with the Threads API,
|
|
5
|
+
* allowing users to create and publish posts on Threads.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The base URL for the Threads API */
|
|
9
|
+
export const THREADS_API_BASE_URL = "https://graph.threads.net/v1.0";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Represents a request to post content on Threads.
|
|
13
|
+
*/
|
|
14
|
+
export interface ThreadsPostRequest {
|
|
15
|
+
/** The user ID of the Threads account */
|
|
16
|
+
userId: string;
|
|
17
|
+
/** The access token for authentication */
|
|
18
|
+
accessToken: string;
|
|
19
|
+
/** The type of media being posted */
|
|
20
|
+
mediaType: "TEXT" | "IMAGE" | "VIDEO";
|
|
21
|
+
/** The text content of the post (optional) */
|
|
22
|
+
text?: string;
|
|
23
|
+
/** The URL of the image to be posted (optional) */
|
|
24
|
+
imageUrl?: string;
|
|
25
|
+
/** The URL of the video to be posted (optional) */
|
|
26
|
+
videoUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a Threads media container.
|
|
31
|
+
*
|
|
32
|
+
* @param request - The ThreadsPostRequest object containing post details
|
|
33
|
+
* @returns A Promise that resolves to the container ID
|
|
34
|
+
* @throws Will throw an error if the API request fails
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const request: ThreadsPostRequest = {
|
|
39
|
+
* userId: "123456",
|
|
40
|
+
* accessToken: "your_access_token",
|
|
41
|
+
* mediaType: "TEXT",
|
|
42
|
+
* text: "Hello, Threads!"
|
|
43
|
+
* };
|
|
44
|
+
* const containerId = await createThreadsContainer(request);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export async function createThreadsContainer(
|
|
48
|
+
request: ThreadsPostRequest
|
|
49
|
+
): Promise<string> {
|
|
50
|
+
const url = `${THREADS_API_BASE_URL}/${request.userId}/threads`;
|
|
51
|
+
const body = new URLSearchParams({
|
|
52
|
+
access_token: request.accessToken,
|
|
53
|
+
media_type: request.mediaType,
|
|
54
|
+
...(request.text && { text: request.text }),
|
|
55
|
+
...(request.imageUrl && { image_url: request.imageUrl }),
|
|
56
|
+
...(request.videoUrl && { video_url: request.videoUrl }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
console.log(`Sending request to: ${url}`);
|
|
60
|
+
console.log(`Request body: ${body.toString()}`);
|
|
61
|
+
|
|
62
|
+
const response = await fetch(url, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
body: body,
|
|
65
|
+
headers: {
|
|
66
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const responseText = await response.text();
|
|
71
|
+
console.log(`Response status: ${response.status} ${response.statusText}`);
|
|
72
|
+
console.log(`Response body: ${responseText}`);
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Failed to create Threads container: ${response.statusText}. Details: ${responseText}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(responseText);
|
|
82
|
+
return data.id;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`Failed to parse response JSON: ${error}`);
|
|
85
|
+
throw new Error(`Invalid response from Threads API: ${responseText}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Publishes a Threads media container.
|
|
91
|
+
*
|
|
92
|
+
* @param userId - The user ID of the Threads account
|
|
93
|
+
* @param accessToken - The access token for authentication
|
|
94
|
+
* @param containerId - The ID of the container to publish
|
|
95
|
+
* @returns A Promise that resolves to the published post ID
|
|
96
|
+
* @throws Will throw an error if the API request fails
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const publishedId = await publishThreadsContainer("123456", "your_access_token", "container_id");
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export async function publishThreadsContainer(
|
|
104
|
+
userId: string,
|
|
105
|
+
accessToken: string,
|
|
106
|
+
containerId: string
|
|
107
|
+
): Promise<string> {
|
|
108
|
+
const url = `${THREADS_API_BASE_URL}/${userId}/threads_publish`;
|
|
109
|
+
const body = new URLSearchParams({
|
|
110
|
+
access_token: accessToken,
|
|
111
|
+
creation_id: containerId,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const response = await fetch(url, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: body,
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const responseText = await response.text();
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Failed to publish Threads container: ${response.statusText}. Details: ${responseText}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const data = JSON.parse(responseText);
|
|
132
|
+
return data.id;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`Failed to parse publish response JSON: ${error}`);
|
|
135
|
+
throw new Error(`Invalid response from Threads API: ${responseText}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Serves HTTP requests to create and publish Threads posts.
|
|
141
|
+
*
|
|
142
|
+
* This function sets up a server that listens for POST requests
|
|
143
|
+
* containing ThreadsPostRequest data. It creates a container and
|
|
144
|
+
* immediately publishes it.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* // Start the server
|
|
149
|
+
* serveRequests();
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function serveRequests() {
|
|
153
|
+
Deno.serve(async (req) => {
|
|
154
|
+
if (req.method !== "POST") {
|
|
155
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const requestData: ThreadsPostRequest = await req.json();
|
|
160
|
+
console.log(`Received request data: ${JSON.stringify(requestData)}`);
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
!requestData.userId ||
|
|
164
|
+
!requestData.accessToken ||
|
|
165
|
+
!requestData.mediaType
|
|
166
|
+
) {
|
|
167
|
+
return new Response(
|
|
168
|
+
JSON.stringify({ success: false, error: "Missing required fields" }),
|
|
169
|
+
{
|
|
170
|
+
status: 400,
|
|
171
|
+
headers: { "Content-Type": "application/json" },
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Create the Threads container
|
|
177
|
+
const containerId = await createThreadsContainer(requestData);
|
|
178
|
+
|
|
179
|
+
// Immediately attempt to publish the Threads container
|
|
180
|
+
const publishedId = await publishThreadsContainer(
|
|
181
|
+
requestData.userId,
|
|
182
|
+
requestData.accessToken,
|
|
183
|
+
containerId
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return new Response(JSON.stringify({ success: true, publishedId }), {
|
|
187
|
+
headers: { "Content-Type": "application/json" },
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error("Error posting to Threads:", error);
|
|
191
|
+
return new Response(
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
success: false,
|
|
194
|
+
error: error.message,
|
|
195
|
+
stack: error.stack,
|
|
196
|
+
}),
|
|
197
|
+
{
|
|
198
|
+
status: 500,
|
|
199
|
+
headers: { "Content-Type": "application/json" },
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
package/mod_test.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// mod_test.ts
|
|
2
|
+
import {
|
|
3
|
+
assertEquals,
|
|
4
|
+
assertRejects,
|
|
5
|
+
} from "https://deno.land/std@0.153.0/testing/asserts.ts";
|
|
6
|
+
import {
|
|
7
|
+
createThreadsContainer,
|
|
8
|
+
publishThreadsContainer,
|
|
9
|
+
type ThreadsPostRequest,
|
|
10
|
+
} from "./mod.ts";
|
|
11
|
+
|
|
12
|
+
// Mock fetch response
|
|
13
|
+
globalThis.fetch = (
|
|
14
|
+
input: string | URL | Request,
|
|
15
|
+
_init?: RequestInit
|
|
16
|
+
): Promise<Response> => {
|
|
17
|
+
const url =
|
|
18
|
+
typeof input === "string"
|
|
19
|
+
? input
|
|
20
|
+
: input instanceof URL
|
|
21
|
+
? input.toString()
|
|
22
|
+
: input.url;
|
|
23
|
+
|
|
24
|
+
if (url.includes("threads_publish")) {
|
|
25
|
+
return Promise.resolve({
|
|
26
|
+
ok: true,
|
|
27
|
+
status: 200,
|
|
28
|
+
statusText: "OK",
|
|
29
|
+
text: () => Promise.resolve(JSON.stringify({ id: "published123" })),
|
|
30
|
+
} as Response);
|
|
31
|
+
} else if (url.includes("threads")) {
|
|
32
|
+
return Promise.resolve({
|
|
33
|
+
ok: true,
|
|
34
|
+
status: 200,
|
|
35
|
+
statusText: "OK",
|
|
36
|
+
text: () => Promise.resolve(JSON.stringify({ id: "container123" })),
|
|
37
|
+
} as Response);
|
|
38
|
+
}
|
|
39
|
+
return Promise.resolve({
|
|
40
|
+
ok: false,
|
|
41
|
+
status: 500,
|
|
42
|
+
statusText: "Internal Server Error",
|
|
43
|
+
text: () => Promise.resolve("Error"),
|
|
44
|
+
} as Response);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
Deno.test("createThreadsContainer should return container ID", async () => {
|
|
48
|
+
const requestData: ThreadsPostRequest = {
|
|
49
|
+
userId: "12345",
|
|
50
|
+
accessToken: "token",
|
|
51
|
+
mediaType: "TEXT",
|
|
52
|
+
text: "Hello, Threads!",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const containerId = await createThreadsContainer(requestData);
|
|
56
|
+
assertEquals(containerId, "container123");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
Deno.test("publishThreadsContainer should return published ID", async () => {
|
|
60
|
+
const userId = "12345";
|
|
61
|
+
const accessToken = "token";
|
|
62
|
+
const containerId = "container123";
|
|
63
|
+
|
|
64
|
+
const publishedId = await publishThreadsContainer(
|
|
65
|
+
userId,
|
|
66
|
+
accessToken,
|
|
67
|
+
containerId
|
|
68
|
+
);
|
|
69
|
+
assertEquals(publishedId, "published123");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
Deno.test("createThreadsContainer should throw error on failure", async () => {
|
|
73
|
+
const requestData: ThreadsPostRequest = {
|
|
74
|
+
userId: "12345",
|
|
75
|
+
accessToken: "token",
|
|
76
|
+
mediaType: "TEXT",
|
|
77
|
+
text: "Hello, Threads!",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
globalThis.fetch = (): Promise<Response> =>
|
|
81
|
+
Promise.resolve({
|
|
82
|
+
ok: false,
|
|
83
|
+
status: 500,
|
|
84
|
+
statusText: "Internal Server Error",
|
|
85
|
+
text: () => Promise.resolve("Error"),
|
|
86
|
+
} as Response);
|
|
87
|
+
|
|
88
|
+
await assertRejects(
|
|
89
|
+
async () => {
|
|
90
|
+
await createThreadsContainer(requestData);
|
|
91
|
+
},
|
|
92
|
+
Error,
|
|
93
|
+
"Failed to create Threads container"
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
Deno.test("publishThreadsContainer should throw error on failure", async () => {
|
|
98
|
+
const userId = "12345";
|
|
99
|
+
const accessToken = "token";
|
|
100
|
+
const containerId = "container123";
|
|
101
|
+
|
|
102
|
+
globalThis.fetch = (
|
|
103
|
+
_input: string | URL | Request,
|
|
104
|
+
_init?: RequestInit
|
|
105
|
+
): Promise<Response> =>
|
|
106
|
+
Promise.resolve({
|
|
107
|
+
ok: false,
|
|
108
|
+
status: 500,
|
|
109
|
+
statusText: "Internal Server Error",
|
|
110
|
+
text: () => Promise.resolve("Error"),
|
|
111
|
+
} as Response);
|
|
112
|
+
|
|
113
|
+
await assertRejects(
|
|
114
|
+
async () => {
|
|
115
|
+
await publishThreadsContainer(userId, accessToken, containerId);
|
|
116
|
+
},
|
|
117
|
+
Error,
|
|
118
|
+
"Failed to publish Threads container"
|
|
119
|
+
);
|
|
120
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codybrom/denim",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Typescript/Deno module to simplify posting to Threads with text, images, or videos",
|
|
5
|
+
"main": "mod.ts",
|
|
6
|
+
"directories": {
|
|
7
|
+
"example": "examples"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/codybrom/denim.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"threads",
|
|
15
|
+
"threads-api"
|
|
16
|
+
],
|
|
17
|
+
"author": "Cody Bromley",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/codybrom/denim/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/codybrom/denim#readme"
|
|
23
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Denim
|
|
2
|
+
|
|
3
|
+
[](https://jsr.io/@codybrom/denim) [](https://jsr.io/@codybrom/denim)
|
|
4
|
+
|
|
5
|
+
**Denim** is a Deno module that provides a simple interface for posting single Threads posts using text, images, or videos.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Create and publish posts on Threads
|
|
10
|
+
- Support for text-only, image, and video posts
|
|
11
|
+
- Easy-to-use API
|
|
12
|
+
- Deployable as an edge function
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Using with Deno
|
|
17
|
+
|
|
18
|
+
To add Denim to your Deno project, you can use the following command:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
deno add @codybrom/denim
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This will add the latest version of Denim to your project's dependencies.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
To import straight from JSR,
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { ThreadsPostRequest, createThreadsContainer, publishThreadsContainer } from 'jsr:@codybrom/denim@^1.0.2';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Basic Usage
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { createThreadsContainer, publishThreadsContainer, ThreadsPostRequest } from "jsr:@codybrom/denim@^1.0.2";
|
|
39
|
+
|
|
40
|
+
const request: ThreadsPostRequest = {
|
|
41
|
+
userId: "YOUR_USER_ID",
|
|
42
|
+
accessToken: "YOUR_ACCESS_TOKEN",
|
|
43
|
+
mediaType: "TEXT",
|
|
44
|
+
text: "Hello, Threads!",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Create a container
|
|
48
|
+
const containerId = await createThreadsContainer(request);
|
|
49
|
+
|
|
50
|
+
// Publish the container
|
|
51
|
+
const publishedId = await publishThreadsContainer(request.userId, request.accessToken, containerId);
|
|
52
|
+
|
|
53
|
+
console.log(`Post published with ID: ${publishedId}`);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Posting Different Media Types
|
|
57
|
+
|
|
58
|
+
#### Text-only Post
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const textRequest: ThreadsPostRequest = {
|
|
62
|
+
userId: "YOUR_USER_ID",
|
|
63
|
+
accessToken: "YOUR_ACCESS_TOKEN",
|
|
64
|
+
mediaType: "TEXT",
|
|
65
|
+
text: "This is a text-only post on Threads!",
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Image Post
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const imageRequest: ThreadsPostRequest = {
|
|
73
|
+
userId: "YOUR_USER_ID",
|
|
74
|
+
accessToken: "YOUR_ACCESS_TOKEN",
|
|
75
|
+
mediaType: "IMAGE",
|
|
76
|
+
text: "Check out this image!",
|
|
77
|
+
imageUrl: "https://example.com/image.jpg",
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Video Post
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const videoRequest: ThreadsPostRequest = {
|
|
85
|
+
userId: "YOUR_USER_ID",
|
|
86
|
+
accessToken: "YOUR_ACCESS_TOKEN",
|
|
87
|
+
mediaType: "VIDEO",
|
|
88
|
+
text: "Watch this video!",
|
|
89
|
+
videoUrl: "https://example.com/video.mp4",
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Deploying as an Edge Function
|
|
94
|
+
|
|
95
|
+
Denim can be easily deployed as an edge function. An example implementation is provided in `examples/edge-function.ts`.
|
|
96
|
+
|
|
97
|
+
To deploy:
|
|
98
|
+
|
|
99
|
+
1. Copy the `examples/edge-function.ts` file to your project.
|
|
100
|
+
2. Deploy this file to your serverless platform that supports Deno.
|
|
101
|
+
3. Send POST requests to your function's URI with the appropriate JSON body.
|
|
102
|
+
|
|
103
|
+
### Example cURL Commands
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Post a text-only Thread
|
|
107
|
+
curl -X POST <YOUR_FUNCTION_URI> \
|
|
108
|
+
-H "Content-Type: application/json" \
|
|
109
|
+
-H "Authorization: Bearer YOUR_AUTH_KEY" \
|
|
110
|
+
-d '{
|
|
111
|
+
"userId": "YOUR_USER_ID",
|
|
112
|
+
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
113
|
+
"mediaType": "TEXT",
|
|
114
|
+
"text": "Hello from Denim!"
|
|
115
|
+
}'
|
|
116
|
+
|
|
117
|
+
# Post an image Thread
|
|
118
|
+
curl -X POST <YOUR_FUNCTION_URI> \
|
|
119
|
+
-H "Content-Type: application/json" \
|
|
120
|
+
-H "Authorization: Bearer YOUR_AUTH_KEY" \
|
|
121
|
+
-d '{
|
|
122
|
+
"userId": "YOUR_USER_ID",
|
|
123
|
+
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
124
|
+
"mediaType": "IMAGE",
|
|
125
|
+
"text": "Check out this image I posted with Denim!",
|
|
126
|
+
"imageUrl": "https://example.com/image.jpg"
|
|
127
|
+
}'
|
|
128
|
+
|
|
129
|
+
# Post a video Thread
|
|
130
|
+
curl -X POST <YOUR_FUNCTION_URI> \
|
|
131
|
+
-H "Content-Type: application/json" \
|
|
132
|
+
-H "Authorization: Bearer YOUR_AUTH_KEY" \
|
|
133
|
+
-d '{
|
|
134
|
+
"userId": "YOUR_USER_ID",
|
|
135
|
+
"accessToken": "YOUR_ACCESS_TOKEN",
|
|
136
|
+
"mediaType": "VIDEO",
|
|
137
|
+
"text": "Watch this video I posted with Denim!",
|
|
138
|
+
"videoUrl": "https://example.com/video.mp4"
|
|
139
|
+
}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Note: Replace with your actual authorization headers if your edge function requires them (or remove them).
|
|
143
|
+
|
|
144
|
+
## Security Note
|
|
145
|
+
|
|
146
|
+
Ensure that your function is deployed with appropriate access controls and authentication mechanisms to protect sensitive data like access tokens.
|
|
147
|
+
|
|
148
|
+
## Testing
|
|
149
|
+
|
|
150
|
+
To run the tests:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
deno test mod_test.ts
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Contributing
|
|
157
|
+
|
|
158
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
[MIT License](LICENSE)
|