@shivaedev/discord 0.1.0 → 0.2.0
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/CHANGELOG.md +10 -0
- package/README.md +58 -1
- package/dist/DiscordConfig.d.ts +7 -0
- package/dist/DiscordConfig.d.ts.map +1 -0
- package/dist/DiscordConfig.js +5 -0
- package/dist/DiscordConfig.js.map +1 -0
- package/dist/DiscordGateway.d.ts +9 -0
- package/dist/DiscordGateway.d.ts.map +1 -0
- package/dist/DiscordGateway.js +13 -0
- package/dist/DiscordGateway.js.map +1 -0
- package/dist/DiscordRest.d.ts +8 -0
- package/dist/DiscordRest.d.ts.map +1 -0
- package/dist/DiscordRest.js +12 -0
- package/dist/DiscordRest.js.map +1 -0
- package/dist/DiscordToken.d.ts +6 -0
- package/dist/DiscordToken.d.ts.map +1 -0
- package/dist/DiscordToken.js +4 -0
- package/dist/DiscordToken.js.map +1 -0
- package/dist/GatewayTransport.d.ts +9 -0
- package/dist/GatewayTransport.d.ts.map +1 -0
- package/dist/GatewayTransport.js +4 -0
- package/dist/GatewayTransport.js.map +1 -0
- package/dist/adapters/restStatus.d.ts +3 -0
- package/dist/adapters/restStatus.d.ts.map +1 -0
- package/dist/adapters/restStatus.js +8 -0
- package/dist/adapters/restStatus.js.map +1 -0
- package/dist/adapters/routeKey.d.ts +5 -0
- package/dist/adapters/routeKey.d.ts.map +1 -0
- package/dist/adapters/routeKey.js +16 -0
- package/dist/adapters/routeKey.js.map +1 -0
- package/dist/adapters/webSocketLayer.d.ts +4 -0
- package/dist/adapters/webSocketLayer.d.ts.map +1 -0
- package/dist/adapters/webSocketLayer.js +7 -0
- package/dist/adapters/webSocketLayer.js.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/gatewayConnection.d.ts +6 -0
- package/dist/gatewayConnection.d.ts.map +1 -0
- package/dist/gatewayConnection.js +85 -0
- package/dist/gatewayConnection.js.map +1 -0
- package/dist/gatewayProtocol.d.ts +41 -0
- package/dist/gatewayProtocol.d.ts.map +1 -0
- package/dist/gatewayProtocol.js +21 -0
- package/dist/gatewayProtocol.js.map +1 -0
- package/dist/rateLimits.d.ts +8 -0
- package/dist/rateLimits.d.ts.map +1 -0
- package/dist/rateLimits.js +36 -0
- package/dist/rateLimits.js.map +1 -0
- package/dist/request.d.ts +4 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +6 -0
- package/dist/request.js.map +1 -0
- package/dist/restClient.d.ts +11 -0
- package/dist/restClient.d.ts.map +1 -0
- package/dist/restClient.js +51 -0
- package/dist/restClient.js.map +1 -0
- package/dist/runGateway.d.ts +6 -0
- package/dist/runGateway.d.ts.map +1 -0
- package/dist/runGateway.js +29 -0
- package/dist/runGateway.js.map +1 -0
- package/package.json +28 -5
- package/src/DiscordConfig.ts +9 -0
- package/src/DiscordGateway.ts +13 -0
- package/src/DiscordRest.ts +12 -0
- package/src/DiscordToken.ts +3 -0
- package/src/GatewayTransport.ts +9 -0
- package/src/adapters/restStatus.ts +9 -0
- package/src/adapters/routeKey.ts +15 -0
- package/src/adapters/webSocketLayer.ts +7 -0
- package/src/errors.ts +13 -0
- package/src/gatewayConnection.ts +89 -0
- package/src/gatewayProtocol.ts +42 -0
- package/src/rateLimits.ts +39 -0
- package/src/request.ts +7 -0
- package/src/restClient.ts +64 -0
- package/src/runGateway.ts +32 -0
- package/dist/discord.d.ts +0 -2
- package/dist/discord.d.ts.map +0 -1
- package/dist/discord.js +0 -2
- package/dist/discord.js.map +0 -1
- package/src/discord.ts +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect, Schema, Semaphore } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/http";
|
|
3
|
+
import type { HttpMethod } from "effect/http/HttpMethod";
|
|
4
|
+
import { DiscordToken } from "#DiscordToken.ts";
|
|
5
|
+
import { DiscordRateLimited, DiscordTransportError } from "#errors.ts";
|
|
6
|
+
export declare const initializeRest: Effect.Effect<{
|
|
7
|
+
lock: Semaphore.Semaphore;
|
|
8
|
+
request: (method: HttpMethod, path: string, body?: unknown) => Effect.Effect<unknown, DiscordRateLimited | import("effect/http/HttpBody").HttpBodyError | import("effect/http/HttpClientError").HttpClientError | import("@shivaedev/http/errors.ts").InvalidBodyLimit | import("@shivaedev/http/errors.ts").RemoteGone | import("@shivaedev/http/errors.ts").RemoteHttpError | import("@shivaedev/http/errors.ts").RemoteNotFound | import("@shivaedev/http/errors.ts").RemoteRateLimited | import("@shivaedev/http/errors.ts").RemoteUnauthorized | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | Schema.SchemaError, never>;
|
|
9
|
+
}, never, DiscordToken | HttpClient.HttpClient>;
|
|
10
|
+
export declare function restRequest(state: Effect.Success<typeof initializeRest>): (method: HttpMethod, path: string, body?: unknown) => Effect.Effect<unknown, DiscordRateLimited | DiscordTransportError | import("effect/http/HttpBody").HttpBodyError | import("@shivaedev/http/errors.ts").InvalidBodyLimit | import("@shivaedev/http/errors.ts").RemoteGone | import("@shivaedev/http/errors.ts").RemoteHttpError | import("@shivaedev/http/errors.ts").RemoteNotFound | import("@shivaedev/http/errors.ts").RemoteRateLimited | import("@shivaedev/http/errors.ts").RemoteUnauthorized | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | Schema.SchemaError, never>;
|
|
11
|
+
//# sourceMappingURL=restClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restClient.d.ts","sourceRoot":"","sources":["../src/restClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAY,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAqB,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAMzD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAOvE,eAAO,MAAM,cAAc;;;+CAuCzB,CAAC;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,cAAc,CAAC,YACvD,UAAU,QAAQ,MAAM,SAAS,OAAO,khBAKxD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema, Semaphore } from "effect";
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
+
import { readBoundedJson } from "@shivaedev/http/body.ts";
|
|
4
|
+
import { checkStatus } from "@shivaedev/http/checkStatus.ts";
|
|
5
|
+
import { isEmpty, isRateLimited } from "#adapters/restStatus.ts";
|
|
6
|
+
import { routeKey } from "#adapters/routeKey.ts";
|
|
7
|
+
import { DiscordConfig } from "#DiscordConfig.ts";
|
|
8
|
+
import { DiscordToken } from "#DiscordToken.ts";
|
|
9
|
+
import { DiscordRateLimited, DiscordTransportError } from "#errors.ts";
|
|
10
|
+
import { rateLimits } from "#rateLimits.ts";
|
|
11
|
+
const Limited = Schema.Struct({ global: Schema.optional(Schema.Boolean), retryAfter: Schema.Number }).pipe(Schema.encodeKeys({ retryAfter: "retry_after" }));
|
|
12
|
+
export const initializeRest = Effect.gen(function* () {
|
|
13
|
+
const client = HttpClient.withScope(yield* HttpClient.HttpClient);
|
|
14
|
+
const token = yield* DiscordToken;
|
|
15
|
+
const config = yield* DiscordConfig;
|
|
16
|
+
const rates = rateLimits();
|
|
17
|
+
const lock = yield* Semaphore.make(1);
|
|
18
|
+
const request = Effect.fnUntraced(function* (method, path, body) {
|
|
19
|
+
const { major, route } = routeKey(method, path);
|
|
20
|
+
let input = HttpClientRequest.make(method)(`${config.apiUrl}${path}`).pipe(HttpClientRequest.setHeader("Authorization", `Bot ${Redacted.value(token)}`));
|
|
21
|
+
if (body !== undefined) {
|
|
22
|
+
input = yield* HttpClientRequest.bodyJson(input, body);
|
|
23
|
+
}
|
|
24
|
+
for (let attempt = 0;; attempt += 1) {
|
|
25
|
+
yield* rates.wait(route, major);
|
|
26
|
+
const answer = yield* Effect.scoped(Effect.gen(function* () {
|
|
27
|
+
const response = yield* client.execute(input);
|
|
28
|
+
yield* rates.observe(route, major, response.headers);
|
|
29
|
+
if (isRateLimited(response.status)) {
|
|
30
|
+
const limit = yield* readBoundedJson(response, 64 * 1024, Limited);
|
|
31
|
+
yield* rates.refuse(route, major, limit.retryAfter, limit.global === true);
|
|
32
|
+
if (attempt === 3) {
|
|
33
|
+
return yield* new DiscordRateLimited({ retryAfter: limit.retryAfter });
|
|
34
|
+
}
|
|
35
|
+
return { retry: true, value: undefined };
|
|
36
|
+
}
|
|
37
|
+
yield* checkStatus(response);
|
|
38
|
+
const value = isEmpty(response.status) ? undefined : yield* readBoundedJson(response, 1024 * 1024, Schema.Unknown);
|
|
39
|
+
return { retry: false, value };
|
|
40
|
+
}));
|
|
41
|
+
if (!answer.retry) {
|
|
42
|
+
return answer.value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return { lock, request };
|
|
47
|
+
});
|
|
48
|
+
export function restRequest(state) {
|
|
49
|
+
return (method, path, body) => state.request(method, path, body).pipe(state.lock.withPermit, Effect.catchTag("HttpClientError", () => Effect.fail(new DiscordTransportError({}))));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=restClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restClient.js","sourceRoot":"","sources":["../src/restClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { DiscordGatewayClosed } from "#errors.ts";
|
|
3
|
+
import { GatewayTransport } from "#GatewayTransport.ts";
|
|
4
|
+
import type { GatewayDispatch } from "#gatewayProtocol.ts";
|
|
5
|
+
export declare const runGateway: (dispatch: (event: GatewayDispatch) => Effect.Effect<void>) => Effect.Effect<never, DiscordGatewayClosed | import("#errors.ts").DiscordGatewayProtocolError, import("./DiscordToken.ts").DiscordToken | GatewayTransport>;
|
|
6
|
+
//# sourceMappingURL=runGateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runGateway.d.ts","sourceRoot":"","sources":["../src/runGateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,qBAAqB,CAAC;AAE3E,eAAO,MAAM,UAAU,qBAAkD,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,+JAuB9G,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Socket } from "effect/socket";
|
|
3
|
+
import { DiscordConfig } from "#DiscordConfig.ts";
|
|
4
|
+
import { DiscordGatewayClosed } from "#errors.ts";
|
|
5
|
+
import { GatewayTransport } from "#GatewayTransport.ts";
|
|
6
|
+
import { gatewayConnection } from "#gatewayConnection.ts";
|
|
7
|
+
export const runGateway = Effect.fnUntraced(function* (dispatch) {
|
|
8
|
+
const config = yield* DiscordConfig;
|
|
9
|
+
const transport = yield* GatewayTransport;
|
|
10
|
+
const session = { resumeUrl: undefined, sequence: null, sessionId: undefined };
|
|
11
|
+
while (true) {
|
|
12
|
+
const socket = yield* transport.open(`${session.resumeUrl ?? config.gatewayUrl}/?v=10&encoding=json`);
|
|
13
|
+
yield* gatewayConnection(session, dispatch)
|
|
14
|
+
.pipe(Effect.provideService(Socket.Socket, socket))
|
|
15
|
+
.pipe(Effect.catchTag("SocketError", (error) => {
|
|
16
|
+
const code = error.reason._tag === "SocketCloseError" ? error.reason.code : undefined;
|
|
17
|
+
if (code === 4004 || (code !== undefined && code >= 4010 && code <= 4014)) {
|
|
18
|
+
return Effect.fail(new DiscordGatewayClosed({ code }));
|
|
19
|
+
}
|
|
20
|
+
if (code === 4007 || code === 4009) {
|
|
21
|
+
session.sessionId = undefined;
|
|
22
|
+
session.resumeUrl = undefined;
|
|
23
|
+
session.sequence = null;
|
|
24
|
+
}
|
|
25
|
+
return Effect.sleep("1 second");
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=runGateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runGateway.js","sourceRoot":"","sources":["../src/runGateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG1D,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,QAAyD;IAC/G,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,aAAa,CAAC;IACpC,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,gBAAgB,CAAC;IAC1C,MAAM,OAAO,GAAmB,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;IAC/F,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC,UAAU,sBAAsB,CAAC,CAAC;QACtG,KAAK,CAAC,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC;aACzC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;aAClD,IAAI,CACJ,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE;YACxC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;YACtF,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;gBAC3E,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACxD,CAAC;YACD,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBACpC,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;gBAC9B,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;gBAC9B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;YACzB,CAAC;YACD,OAAO,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACjC,CAAC,CAAC,CACF,CAAC;IACJ,CAAC;AACF,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/discord",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Discord integrations for Effect applications",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/discord#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -14,9 +14,17 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"type": "module",
|
|
17
|
+
"imports": {
|
|
18
|
+
"#test/*": "./src/test-support/*",
|
|
19
|
+
"#*.ts": {
|
|
20
|
+
"@shivaedev/source": "./src/*.ts",
|
|
21
|
+
"types": "./dist/*.d.ts",
|
|
22
|
+
"default": "./dist/*.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
17
25
|
"exports": {
|
|
18
26
|
"./*.ts": {
|
|
19
|
-
"source": "./src/*.ts",
|
|
27
|
+
"@shivaedev/source": "./src/*.ts",
|
|
20
28
|
"types": "./dist/*.d.ts",
|
|
21
29
|
"default": "./dist/*.js"
|
|
22
30
|
},
|
|
@@ -25,13 +33,27 @@
|
|
|
25
33
|
"files": [
|
|
26
34
|
"dist",
|
|
27
35
|
"src",
|
|
36
|
+
"!src/**/*.test.ts",
|
|
37
|
+
"!src/**/*.spec.ts",
|
|
38
|
+
"!src/**/test-support",
|
|
28
39
|
"CHANGELOG.md",
|
|
29
40
|
"README.md"
|
|
30
41
|
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@shivaedev/http": "0.6.0",
|
|
44
|
+
"@shivaedev/service": "0.7.0"
|
|
45
|
+
},
|
|
31
46
|
"devDependencies": {
|
|
32
|
-
"@shivaedev/quality": "0.
|
|
47
|
+
"@shivaedev/quality": "0.60.0",
|
|
48
|
+
"@shivaedev/test-kits": "0.3.0",
|
|
49
|
+
"@shivaedev/test-story": "0.38.0",
|
|
33
50
|
"@types/node": "24.19.2",
|
|
34
|
-
"
|
|
51
|
+
"effect": "4.0.6",
|
|
52
|
+
"typescript": "7.0.2",
|
|
53
|
+
"vitest": "5.0.5"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"effect": "4.0.6"
|
|
35
57
|
},
|
|
36
58
|
"engines": {
|
|
37
59
|
"node": ">=24"
|
|
@@ -42,7 +64,8 @@
|
|
|
42
64
|
},
|
|
43
65
|
"scripts": {
|
|
44
66
|
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
|
|
45
|
-
"ready": "pnpm typecheck && pnpm build",
|
|
67
|
+
"ready": "pnpm typecheck && pnpm test && pnpm build",
|
|
68
|
+
"test": "vitest run",
|
|
46
69
|
"typecheck": "tsc --noEmit"
|
|
47
70
|
}
|
|
48
71
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
|
|
3
|
+
export const DiscordConfig = Context.Reference<{
|
|
4
|
+
readonly apiUrl: string;
|
|
5
|
+
readonly gatewayUrl: string;
|
|
6
|
+
readonly intents: number;
|
|
7
|
+
}>("@shivaedev/discord/DiscordConfig", {
|
|
8
|
+
defaultValue: () => ({ apiUrl: "https://discord.com/api/v10", gatewayUrl: "wss://gateway.discord.gg", intents: 0 }),
|
|
9
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
3
|
+
import { DiscordConfig } from "#DiscordConfig.ts";
|
|
4
|
+
import { DiscordToken } from "#DiscordToken.ts";
|
|
5
|
+
import { GatewayTransport } from "#GatewayTransport.ts";
|
|
6
|
+
import { runGateway } from "#runGateway.ts";
|
|
7
|
+
|
|
8
|
+
export const DiscordGateway = defineService({
|
|
9
|
+
id: "@shivaedev/discord/DiscordGateway",
|
|
10
|
+
initialize: Effect.void,
|
|
11
|
+
methods: () => ({ run: runGateway }),
|
|
12
|
+
requires: [DiscordConfig, DiscordToken, GatewayTransport],
|
|
13
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpClient } from "effect/http";
|
|
2
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
3
|
+
import { DiscordConfig } from "#DiscordConfig.ts";
|
|
4
|
+
import { DiscordToken } from "#DiscordToken.ts";
|
|
5
|
+
import { initializeRest, restRequest } from "#restClient.ts";
|
|
6
|
+
|
|
7
|
+
export const DiscordRest = defineService({
|
|
8
|
+
id: "@shivaedev/discord/DiscordRest",
|
|
9
|
+
initialize: initializeRest,
|
|
10
|
+
methods: (state) => ({ request: restRequest(state) }),
|
|
11
|
+
requires: [HttpClient.HttpClient, DiscordToken, DiscordConfig],
|
|
12
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context, type Effect } from "effect";
|
|
2
|
+
import type { Socket } from "effect/socket";
|
|
3
|
+
|
|
4
|
+
export class GatewayTransport extends Context.Service<
|
|
5
|
+
GatewayTransport,
|
|
6
|
+
{
|
|
7
|
+
readonly open: (url: string) => Effect.Effect<Socket.Socket>;
|
|
8
|
+
}
|
|
9
|
+
>()("@shivaedev/discord/GatewayTransport") {}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
2
|
+
|
|
3
|
+
export function isRateLimited(status: number) {
|
|
4
|
+
return status === HttpStatus.fromLiteral("TooManyRequests");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isEmpty(status: number) {
|
|
8
|
+
return status === HttpStatus.fromLiteral("NoContent");
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function routeKey(method: string, path: string) {
|
|
2
|
+
const parts = path.split("?")[0]?.split("/") ?? [];
|
|
3
|
+
let major = "";
|
|
4
|
+
if (parts[1] === "channels" || parts[1] === "guilds") {
|
|
5
|
+
major = parts[2] ?? "";
|
|
6
|
+
}
|
|
7
|
+
if (parts[1] === "webhooks") {
|
|
8
|
+
major = `${parts[2]}/${parts[3]}`;
|
|
9
|
+
}
|
|
10
|
+
const normalized = parts.map((part, index) => (/^\d+$/u.test(part) && index !== 2 ? ":id" : part));
|
|
11
|
+
if (parts[1] === "channels" || parts[1] === "guilds" || parts[1] === "webhooks") {
|
|
12
|
+
normalized[2] = ":major";
|
|
13
|
+
}
|
|
14
|
+
return { major, route: `${method} ${normalized.join("/")}` };
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { Socket } from "effect/socket";
|
|
3
|
+
import { GatewayTransport } from "#GatewayTransport.ts";
|
|
4
|
+
|
|
5
|
+
export const webSocketLayer = Layer.succeed(GatewayTransport)({
|
|
6
|
+
open: (url) => Socket.makeWebSocket(url).pipe(Effect.provide(Socket.layerWebSocketConstructorGlobal)),
|
|
7
|
+
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export class DiscordTransportError extends Schema.TaggedError<DiscordTransportError>()("DiscordTransportError", {}) {}
|
|
4
|
+
|
|
5
|
+
export class DiscordRateLimited extends Schema.TaggedError<DiscordRateLimited>()("DiscordRateLimited", {
|
|
6
|
+
retryAfter: Schema.Number,
|
|
7
|
+
}) {}
|
|
8
|
+
|
|
9
|
+
export class DiscordGatewayClosed extends Schema.TaggedError<DiscordGatewayClosed>()("DiscordGatewayClosed", {
|
|
10
|
+
code: Schema.Number,
|
|
11
|
+
}) {}
|
|
12
|
+
|
|
13
|
+
export class DiscordGatewayProtocolError extends Schema.TaggedError<DiscordGatewayProtocolError>()("DiscordGatewayProtocolError", {}) {}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Deferred, Effect, Redacted, Schema } from "effect";
|
|
2
|
+
import { Socket } from "effect/socket";
|
|
3
|
+
import { DiscordConfig } from "#DiscordConfig.ts";
|
|
4
|
+
import { DiscordToken } from "#DiscordToken.ts";
|
|
5
|
+
import { decodeData, decodeFrame, type GatewayDispatch, type GatewayFrame, type GatewaySession, Hello, Ready, reconnect } from "#gatewayProtocol.ts";
|
|
6
|
+
|
|
7
|
+
export function gatewayConnection(session: GatewaySession, dispatch: (event: GatewayDispatch) => Effect.Effect<void>) {
|
|
8
|
+
return Effect.gen(function* () {
|
|
9
|
+
const socket = yield* Socket.Socket;
|
|
10
|
+
const token = yield* DiscordToken;
|
|
11
|
+
const { intents } = yield* DiscordConfig;
|
|
12
|
+
const reader = yield* socket.reader;
|
|
13
|
+
const writer = yield* socket.writer;
|
|
14
|
+
const failed = yield* Deferred.make<never, Socket.SocketError>();
|
|
15
|
+
let acknowledged = true;
|
|
16
|
+
const send = Effect.fnUntraced(function* (op: number, d: unknown) {
|
|
17
|
+
const frame = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ d, op }).pipe(Effect.orDie);
|
|
18
|
+
yield* writer.write(frame);
|
|
19
|
+
});
|
|
20
|
+
const startHeartbeat = Effect.fnUntraced(function* (interval: number) {
|
|
21
|
+
const pulse = Effect.gen(function* () {
|
|
22
|
+
while (true) {
|
|
23
|
+
yield* Effect.sleep(interval);
|
|
24
|
+
if (!acknowledged) {
|
|
25
|
+
return yield* reconnect();
|
|
26
|
+
}
|
|
27
|
+
acknowledged = false;
|
|
28
|
+
yield* send(1, session.sequence);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
yield* pulse.pipe(
|
|
32
|
+
Effect.catch((error) => Deferred.fail(failed, error)),
|
|
33
|
+
Effect.forkScoped,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
const hello = Effect.fnUntraced(function* (data: unknown) {
|
|
37
|
+
const { heartbeatInterval } = yield* decodeData(Hello, data);
|
|
38
|
+
const payload =
|
|
39
|
+
session.sessionId === undefined
|
|
40
|
+
? { intents, properties: { browser: "platform", device: "platform", os: "linux" }, token: Redacted.value(token) }
|
|
41
|
+
: { seq: session.sequence, "session_id": session.sessionId, token: Redacted.value(token) };
|
|
42
|
+
yield* send(session.sessionId === undefined ? 2 : 6, payload);
|
|
43
|
+
yield* startHeartbeat(heartbeatInterval);
|
|
44
|
+
});
|
|
45
|
+
const deliver = Effect.fnUntraced(function* (frame: GatewayFrame) {
|
|
46
|
+
if (frame.t === "READY") {
|
|
47
|
+
const ready = yield* decodeData(Ready, frame.d);
|
|
48
|
+
session.sessionId = ready.sessionId;
|
|
49
|
+
session.resumeUrl = ready.resumeUrl;
|
|
50
|
+
}
|
|
51
|
+
if (frame.t !== undefined && frame.t !== null && frame.s !== undefined && frame.s !== null) {
|
|
52
|
+
yield* dispatch({ data: frame.d, sequence: frame.s, type: frame.t });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const handle = Effect.fnUntraced(function* (frame: GatewayFrame) {
|
|
56
|
+
if (frame.s !== undefined && frame.s !== null) {
|
|
57
|
+
session.sequence = frame.s;
|
|
58
|
+
}
|
|
59
|
+
switch (frame.op) {
|
|
60
|
+
case 10:
|
|
61
|
+
return yield* hello(frame.d);
|
|
62
|
+
case 11:
|
|
63
|
+
acknowledged = true;
|
|
64
|
+
return;
|
|
65
|
+
case 1:
|
|
66
|
+
return yield* send(1, session.sequence);
|
|
67
|
+
case 7:
|
|
68
|
+
return yield* reconnect();
|
|
69
|
+
case 9:
|
|
70
|
+
if (frame.d !== true) {
|
|
71
|
+
session.sessionId = undefined;
|
|
72
|
+
session.resumeUrl = undefined;
|
|
73
|
+
session.sequence = null;
|
|
74
|
+
}
|
|
75
|
+
return yield* reconnect();
|
|
76
|
+
case 0:
|
|
77
|
+
return yield* deliver(frame);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const consume = Effect.gen(function* () {
|
|
81
|
+
while (true) {
|
|
82
|
+
for (const raw of yield* reader.pull) {
|
|
83
|
+
yield* handle(yield* decodeFrame(raw));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return yield* Effect.raceFirst(consume, Deferred.await(failed));
|
|
88
|
+
}).pipe(Effect.scoped);
|
|
89
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Socket } from "effect/socket";
|
|
3
|
+
import { DiscordGatewayProtocolError } from "#errors.ts";
|
|
4
|
+
|
|
5
|
+
export const GatewayFrame = Schema.Struct({
|
|
6
|
+
d: Schema.Unknown,
|
|
7
|
+
op: Schema.Number,
|
|
8
|
+
s: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
9
|
+
t: Schema.optional(Schema.NullOr(Schema.String)),
|
|
10
|
+
});
|
|
11
|
+
export type GatewayFrame = typeof GatewayFrame.Type;
|
|
12
|
+
|
|
13
|
+
export interface GatewayDispatch {
|
|
14
|
+
readonly data: unknown;
|
|
15
|
+
readonly sequence: number;
|
|
16
|
+
readonly type: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GatewaySession {
|
|
20
|
+
resumeUrl: string | undefined;
|
|
21
|
+
sequence: number | null;
|
|
22
|
+
sessionId: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function decodeFrame(frame: string | Uint8Array) {
|
|
26
|
+
return Schema.decodeUnknownEffect(Schema.fromJsonString(GatewayFrame))(typeof frame === "string" ? frame : new TextDecoder().decode(frame)).pipe(
|
|
27
|
+
Effect.mapError(() => new DiscordGatewayProtocolError({})),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const Hello = Schema.Struct({ heartbeatInterval: Schema.Number }).pipe(Schema.encodeKeys({ heartbeatInterval: "heartbeat_interval" }));
|
|
32
|
+
export const Ready = Schema.Struct({ resumeUrl: Schema.String, sessionId: Schema.String }).pipe(
|
|
33
|
+
Schema.encodeKeys({ resumeUrl: "resume_gateway_url", sessionId: "session_id" }),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export function decodeData<TSchema extends Schema.ConstraintDecoder<unknown>>(schema: TSchema, data: unknown) {
|
|
37
|
+
return Schema.decodeUnknownEffect(schema)(data).pipe(Effect.mapError(() => new DiscordGatewayProtocolError({})));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function reconnect() {
|
|
41
|
+
return Effect.fail(new Socket.SocketError({ reason: new Socket.SocketCloseError({ code: 4000 }) }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
import type { Headers } from "effect/http";
|
|
3
|
+
|
|
4
|
+
export function rateLimits() {
|
|
5
|
+
const routes = new Map<string, string>();
|
|
6
|
+
const blocked = new Map<string, number>();
|
|
7
|
+
let globalReset = 0;
|
|
8
|
+
function key(route: string, major: string) {
|
|
9
|
+
return `${routes.get(route) ?? route}:${major}`;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
observe: (route: string, major: string, headers: Headers.Headers) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const bucket = headers["x-ratelimit-bucket"];
|
|
15
|
+
if (bucket !== undefined) {
|
|
16
|
+
routes.set(route, bucket);
|
|
17
|
+
}
|
|
18
|
+
if (headers["x-ratelimit-remaining"] === "0") {
|
|
19
|
+
blocked.set(key(route, major), (yield* Clock.currentTimeMillis) + Number(headers["x-ratelimit-reset-after"] ?? 0) * 1000);
|
|
20
|
+
}
|
|
21
|
+
}),
|
|
22
|
+
refuse: (route: string, major: string, seconds: number, global: boolean) =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const reset = (yield* Clock.currentTimeMillis) + seconds * 1000;
|
|
25
|
+
if (global) {
|
|
26
|
+
globalReset = Math.max(globalReset, reset);
|
|
27
|
+
} else {
|
|
28
|
+
blocked.set(key(route, major), reset);
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
wait: (route: string, major: string) =>
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const delay = Math.max(globalReset, blocked.get(key(route, major)) ?? 0) - (yield* Clock.currentTimeMillis);
|
|
34
|
+
if (delay > 0) {
|
|
35
|
+
yield* Effect.sleep(delay);
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/request.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { HttpMethod } from "effect/http/HttpMethod";
|
|
3
|
+
import { DiscordRest } from "#DiscordRest.ts";
|
|
4
|
+
|
|
5
|
+
export function request<TSchema extends Schema.ConstraintDecoder<unknown>>(method: HttpMethod, path: string, schema: TSchema, body?: unknown) {
|
|
6
|
+
return Effect.flatMap(DiscordRest, (rest) => rest.request(method, path, body)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(schema)));
|
|
7
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema, Semaphore } from "effect";
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
+
import type { HttpMethod } from "effect/http/HttpMethod";
|
|
4
|
+
import { readBoundedJson } from "@shivaedev/http/body.ts";
|
|
5
|
+
import { checkStatus } from "@shivaedev/http/checkStatus.ts";
|
|
6
|
+
import { isEmpty, isRateLimited } from "#adapters/restStatus.ts";
|
|
7
|
+
import { routeKey } from "#adapters/routeKey.ts";
|
|
8
|
+
import { DiscordConfig } from "#DiscordConfig.ts";
|
|
9
|
+
import { DiscordToken } from "#DiscordToken.ts";
|
|
10
|
+
import { DiscordRateLimited, DiscordTransportError } from "#errors.ts";
|
|
11
|
+
import { rateLimits } from "#rateLimits.ts";
|
|
12
|
+
|
|
13
|
+
const Limited = Schema.Struct({ global: Schema.optional(Schema.Boolean), retryAfter: Schema.Number }).pipe(
|
|
14
|
+
Schema.encodeKeys({ retryAfter: "retry_after" }),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const initializeRest = Effect.gen(function* () {
|
|
18
|
+
const client = HttpClient.withScope(yield* HttpClient.HttpClient);
|
|
19
|
+
const token = yield* DiscordToken;
|
|
20
|
+
const config = yield* DiscordConfig;
|
|
21
|
+
const rates = rateLimits();
|
|
22
|
+
const lock = yield* Semaphore.make(1);
|
|
23
|
+
const request = Effect.fnUntraced(function* (method: HttpMethod, path: string, body?: unknown) {
|
|
24
|
+
const { major, route } = routeKey(method, path);
|
|
25
|
+
let input = HttpClientRequest.make(method)(`${config.apiUrl}${path}`).pipe(
|
|
26
|
+
HttpClientRequest.setHeader("Authorization", `Bot ${Redacted.value(token)}`),
|
|
27
|
+
);
|
|
28
|
+
if (body !== undefined) {
|
|
29
|
+
input = yield* HttpClientRequest.bodyJson(input, body);
|
|
30
|
+
}
|
|
31
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
32
|
+
yield* rates.wait(route, major);
|
|
33
|
+
const answer = yield* Effect.scoped(
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const response = yield* client.execute(input);
|
|
36
|
+
yield* rates.observe(route, major, response.headers);
|
|
37
|
+
if (isRateLimited(response.status)) {
|
|
38
|
+
const limit = yield* readBoundedJson(response, 64 * 1024, Limited);
|
|
39
|
+
yield* rates.refuse(route, major, limit.retryAfter, limit.global === true);
|
|
40
|
+
if (attempt === 3) {
|
|
41
|
+
return yield* new DiscordRateLimited({ retryAfter: limit.retryAfter });
|
|
42
|
+
}
|
|
43
|
+
return { retry: true, value: undefined };
|
|
44
|
+
}
|
|
45
|
+
yield* checkStatus(response);
|
|
46
|
+
const value = isEmpty(response.status) ? undefined : yield* readBoundedJson(response, 1024 * 1024, Schema.Unknown);
|
|
47
|
+
return { retry: false, value };
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
if (!answer.retry) {
|
|
51
|
+
return answer.value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return { lock, request };
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export function restRequest(state: Effect.Success<typeof initializeRest>) {
|
|
59
|
+
return (method: HttpMethod, path: string, body?: unknown) =>
|
|
60
|
+
state.request(method, path, body).pipe(
|
|
61
|
+
state.lock.withPermit,
|
|
62
|
+
Effect.catchTag("HttpClientError", () => Effect.fail(new DiscordTransportError({}))),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Socket } from "effect/socket";
|
|
3
|
+
import { DiscordConfig } from "#DiscordConfig.ts";
|
|
4
|
+
import { DiscordGatewayClosed } from "#errors.ts";
|
|
5
|
+
import { GatewayTransport } from "#GatewayTransport.ts";
|
|
6
|
+
import { gatewayConnection } from "#gatewayConnection.ts";
|
|
7
|
+
import type { GatewayDispatch, GatewaySession } from "#gatewayProtocol.ts";
|
|
8
|
+
|
|
9
|
+
export const runGateway = Effect.fnUntraced(function* (dispatch: (event: GatewayDispatch) => Effect.Effect<void>) {
|
|
10
|
+
const config = yield* DiscordConfig;
|
|
11
|
+
const transport = yield* GatewayTransport;
|
|
12
|
+
const session: GatewaySession = { resumeUrl: undefined, sequence: null, sessionId: undefined };
|
|
13
|
+
while (true) {
|
|
14
|
+
const socket = yield* transport.open(`${session.resumeUrl ?? config.gatewayUrl}/?v=10&encoding=json`);
|
|
15
|
+
yield* gatewayConnection(session, dispatch)
|
|
16
|
+
.pipe(Effect.provideService(Socket.Socket, socket))
|
|
17
|
+
.pipe(
|
|
18
|
+
Effect.catchTag("SocketError", (error) => {
|
|
19
|
+
const code = error.reason._tag === "SocketCloseError" ? error.reason.code : undefined;
|
|
20
|
+
if (code === 4004 || (code !== undefined && code >= 4010 && code <= 4014)) {
|
|
21
|
+
return Effect.fail(new DiscordGatewayClosed({ code }));
|
|
22
|
+
}
|
|
23
|
+
if (code === 4007 || code === 4009) {
|
|
24
|
+
session.sessionId = undefined;
|
|
25
|
+
session.resumeUrl = undefined;
|
|
26
|
+
session.sequence = null;
|
|
27
|
+
}
|
|
28
|
+
return Effect.sleep("1 second");
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
});
|
package/dist/discord.d.ts
DELETED
package/dist/discord.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../src/discord.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
|
package/dist/discord.js
DELETED
package/dist/discord.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"discord.js","sourceRoot":"","sources":["../src/discord.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
|
package/src/discord.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|