@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.
@@ -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
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@codybrom/denim",
3
+ "version": "1.0.2",
4
+ "description": "A Deno function that posts to Threads with text, image, or video.",
5
+ "entry": "./mod.ts",
6
+ "exports": {
7
+ ".": "./mod.ts"
8
+ }
9
+ }
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
+ [![JSR](https://jsr.io/badges/@codybrom/denim)](https://jsr.io/@codybrom/denim) [![JSR Score](https://jsr.io/badges/@codybrom/denim/score)](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)