Skip to content

Commit 39127eb

Browse files
initialize
0 parents  commit 39127eb

File tree

8 files changed

+454
-0
lines changed

8 files changed

+454
-0
lines changed

.github/workflows/publish.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Publish NPM package
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v2
13+
with:
14+
fetch-depth: 0
15+
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v2
18+
with:
19+
node-version: "20"
20+
registry-url: "https://registry.npmjs.org"
21+
22+
- name: Install dependencies
23+
run: npm install
24+
25+
# - name: Test
26+
# run: npm test
27+
28+
- name: Check the version
29+
id: check
30+
run: |
31+
CURRENT_VERSION=$(jq -r .version package.json)
32+
echo "Current version: $CURRENT_VERSION"
33+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
34+
echo "Latest tag: $LATEST_TAG"
35+
36+
LATEST_VERSION=${LATEST_TAG#v}
37+
38+
if [ "$LATEST_VERSION" = "$CURRENT_VERSION" ];
39+
then
40+
echo "Version is equal"
41+
echo "version_equal=true" >> $GITHUB_OUTPUT
42+
echo "new_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
43+
else
44+
echo "Version is not equal"
45+
echo "version_equal=false" >> $GITHUB_OUTPUT
46+
fi
47+
48+
- name: Build
49+
run: npm run build
50+
if: steps.check.outputs.version_equal == 'true'
51+
52+
- name: Publish
53+
if: steps.check.outputs.version_equal == 'true'
54+
run: npm publish --access public --no-git-checks
55+
env:
56+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
57+
58+
# - name: Git Info Update
59+
# if: steps.check.outputs.version_equal == 'true'
60+
# run: |
61+
# git config --local user.email "github-actions[bot]@users.noreply.github.com"
62+
# git config --local user.name "github-actions[bot]"
63+
- name: Create GitHub release
64+
if: steps.check.outputs.version_equal == 'true'
65+
run: |
66+
gh release create "v${{ steps.check.outputs.new_version }}" --title "v${{ steps.check.outputs.new_version }}"
67+
env:
68+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package-lock.json
2+
node_modules
3+
dist
4+
tsconfig.tsbuildinfo

.npmignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.github
2+
node_modules
3+
src
4+
tsconfig.json
5+
tsconfig.tsbuildinfo

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 discord.https
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.

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Cloudflare Adapter
2+
3+
[![npm version](https://img.shields.io/npm/v/@discord.https/cloudflare-adapter.svg)](https://www.npmjs.com/package/@discord.https/cloudflare-adapter)
4+
[![License](https://img.shields.io/npm/l/@discord.https/cloudflare-adapter.svg)](LICENSE)
5+
[![Downloads](https://img.shields.io/npm/dm/@discord.https/cloudflare-adapter.svg)](https://www.npmjs.com/package/@discord.https/cloudflare-adapter)
6+
7+
**@discord.https/cloudflare-adapter** is an adapter for integrating [**discord.https**](https://www.npmjs.com/package/discord.https) with [Cloudflare Workers](https://workers.cloudflare.com/).
8+
9+
## Installation
10+
11+
```bash
12+
npm install @discord.https/cloudflare-adapter discord.https
13+
```
14+
15+
## Usage
16+
17+
```typescript
18+
import Client from "discord.https";
19+
import CloudflareAdapter from "@discord.https/cloudflare-adapter";
20+
21+
import UtilityRoute from "./command/utility/index.js";
22+
import HelloRoute from "./command/fun/hello.js";
23+
24+
const adapter = new CloudflareAdapter();
25+
26+
export default {
27+
// Cloudflare Workers entry point
28+
async fetch(request, env, ctx) {
29+
const client = new Client({
30+
token: env.DISCORD_BOT_TOKEN,
31+
publicKey: env.DISCORD_PUBLIC_KEY,
32+
httpAdapter: adapter,
33+
debug: true,
34+
});
35+
36+
// Register your routes.
37+
client.register(UtilityRoute, HelloRoute);
38+
39+
// Handle Discord interactions on the "/interactions" endpoint
40+
return await client.listen("interactions", request);
41+
},
42+
};
43+
```

package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@discord.https/cloudflare-adapter",
3+
"version": "1.0.0",
4+
"description": "An adapter for integrating discord.https with Cloudflare workers.",
5+
"main": "./dist/index.js",
6+
"type": "module",
7+
"scripts": {
8+
"build": "tsc --build"
9+
},
10+
"types": "./dist/index.d.ts",
11+
"files": [
12+
"dist",
13+
"README.md"
14+
],
15+
"exports": {
16+
".": {
17+
"import": "./dist/index.js",
18+
"types": "./dist/index.d.ts"
19+
}
20+
},
21+
"repository": {
22+
"type": "git",
23+
"url": "https://github.com/discord-http/cloudflare-adapter.git"
24+
},
25+
"author": "discord.https",
26+
"license": "MIT",
27+
"keywords": [
28+
"discord-cloudflare",
29+
"discord",
30+
"discord.js",
31+
"discord-http-interactions",
32+
"discord-interactions",
33+
"http-interactions",
34+
"discord.https",
35+
"discord-api",
36+
"interaction-router"
37+
]
38+
}

src/index.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
export interface HttpAdapter {
2+
listen(
3+
endpoint: string,
4+
handler: (req: any, res: any) => Promise<any>,
5+
...args: any[]
6+
): Promise<any> | any;
7+
8+
getRequestBody(req: any): Promise<Uint8Array>;
9+
}
10+
11+
export interface HttpAdapterRequest {
12+
method: string;
13+
url: string;
14+
headers: Record<string, string | string[]>;
15+
}
16+
17+
export interface HttpAdapterSererResponse {
18+
headersSent: boolean;
19+
writeHead(status: number, headers?: Record<string, string>): void;
20+
end(chunk?: string): void;
21+
}
22+
23+
class WorkerServerResponse implements HttpAdapterSererResponse {
24+
private statusCode = 200;
25+
private headers: Record<string, string> = {};
26+
private chunks: Uint8Array<ArrayBuffer>[] = [];
27+
28+
public headersSent: boolean = false;
29+
private resolved = false;
30+
private resolveResponsePromise?: () => void;
31+
32+
writeHead(status: number, headers?: Record<string, string>) {
33+
if (this.headersSent) {
34+
throw new Error("Cannot modify headers after they have been sent.");
35+
}
36+
this.statusCode = status;
37+
if (headers) Object.assign(this.headers, headers);
38+
}
39+
40+
end(chunk?: string) {
41+
if (this.headersSent) {
42+
throw new Error("Cannot send body after headers have been sent.");
43+
}
44+
if (chunk) this.chunks.push(new TextEncoder().encode(chunk));
45+
this.headersSent = true;
46+
if (!this.resolved) {
47+
this.resolved = true;
48+
49+
if (this.resolveResponsePromise) {
50+
this.resolveResponsePromise(); // promise revoled
51+
}
52+
}
53+
}
54+
55+
toResponse(): Response {
56+
const body = this.chunks.length ? new Blob(this.chunks) : null;
57+
return new Response(body, {
58+
status: this.statusCode,
59+
headers: this.headers,
60+
});
61+
}
62+
async waitForResponse(): Promise<void> {
63+
return new Promise((resolve) => {
64+
if (this.resolved) {
65+
resolve();
66+
} else {
67+
this.resolveResponsePromise = resolve; // Resolve when end() is called
68+
}
69+
});
70+
}
71+
}
72+
73+
class WorkerIncomingMessage implements HttpAdapterRequest {
74+
headers: Record<string, string | string[]> = {};
75+
url: string;
76+
method: string;
77+
constructor(private request: Request) {
78+
request.headers.get("test");
79+
request.headers.forEach((value, key) => {
80+
this.headers[key] = value;
81+
});
82+
/*
83+
* "https://domain.name/endpoint/endpoint1?query=124".split('/') => ["https", "", "endpoint", "endpoint2?query=123"]
84+
* .slice(3) => ["endpoint", "endpoint2"]
85+
* join('/') => endpoint/endpoint2?query=123
86+
* "/" + endpoint/endpoint2?query=123 => /endpoint/endpoint2?query=123
87+
*/
88+
this.url = "/" + request.url.split("/").slice(3).join("/");
89+
this.method = request.method;
90+
this.request = request;
91+
}
92+
arrayBuffer(): Promise<ArrayBuffer> {
93+
return this.request.arrayBuffer();
94+
}
95+
}
96+
97+
/**
98+
* Adapter for using discord.https with Cloudflare Workers.
99+
*
100+
* This class implements the HttpAdapter interface and provides
101+
* methods to handle incoming requests and send responses in a
102+
* Cloudflare Worker environment.
103+
*
104+
*
105+
*
106+
* @example
107+
* const adapter = new CloudflareAdapter();
108+
*
109+
* export default {
110+
* // Cloudflare Workers entry point
111+
* async fetch(request, env, ctx) {
112+
* const client = new Client({
113+
* token: env.DISCORD_BOT_TOKEN,
114+
* publicKey: env.DISCORD_PUBLIC_KEY,
115+
* httpAdapter: adapter,
116+
* debug: true,
117+
* });
118+
*
119+
* // Register your routes
120+
* client.register(UtilityRoute, HelloRoute);
121+
*
122+
* // Handle Discord interactions on the "/interactions" endpoint
123+
* return await client.listen("interactions", request);
124+
* }
125+
* }
126+
*/
127+
128+
class CloudflareAdapter implements HttpAdapter {
129+
/**
130+
*
131+
* @internal
132+
*
133+
*/
134+
async listen(
135+
endpoint: string,
136+
handler: (req: any, res: any) => Promise<void>,
137+
request: Request
138+
) {
139+
const req = new WorkerIncomingMessage(request);
140+
const res = new WorkerServerResponse();
141+
142+
// It took almost an entire hour to debug this...
143+
// Weirdly, although res.waitForResponse() is blocking execution and waiting for the handler, which was working outside the javascript event loop,
144+
// Cloudflare just cancels the request for some reason??
145+
// Now, after adding await, it works?
146+
// Trust me, waitForResponse is blocking and will never allow execution to pass unless the .end() method from WorkerServerResponse is invoked.
147+
// Whatever, adding await to handler won't hurt...
148+
149+
await handler(req, res);
150+
await res.waitForResponse();
151+
return res.toResponse();
152+
}
153+
154+
/**
155+
*
156+
* Reads the request body as a Uint8Array.
157+
* @internal
158+
*
159+
*/
160+
async getRequestBody(req: WorkerIncomingMessage): Promise<Uint8Array> {
161+
return req.arrayBuffer().then((buffer) => new Uint8Array(buffer));
162+
}
163+
}
164+
export default CloudflareAdapter;

0 commit comments

Comments
 (0)