all repos — kitten @ 90ca75c6c44a211aee767566849ada894cd415fb

pkgs/rest/src/client.ts (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
import type { ApiResponse, DiscordError, DiscordRestClient } from "./types";

export interface ClientOptions {
	token: string;
	baseUrl?: string;
}

export function RESTClient(options: ClientOptions): DiscordRestClient {
	const baseUrl = options.baseUrl || "https://discord.com/api/v10";

	const executeRequest = async (
		segments: string[],
		method: string,
		body?: unknown,
		query?: Record<string, unknown>,
	): Promise<ApiResponse<unknown>> => {
		let url = `${baseUrl}/${segments.join("/")}`;

		if (query) {
			const searchParams = new URLSearchParams();
			for (const [key, val] of Object.entries(query)) {
				if (val !== undefined && val !== null) {
					searchParams.append(key, String(val));
				}
			}
			const queryString = searchParams.toString();
			if (queryString) url += `?${queryString}`;
		}

		try {
			const res = await fetch(url, {
				method,
				headers: {
					Authorization: `Bot ${options.token}`,
					"Content-Type": "application/json",
				},
				body: body ? JSON.stringify(body) : undefined,
			});

			const isOk = res.ok;
			const status = res.status;

			let data: unknown = null;

			if (status !== 204) data = (await res.json().catch(() => null)) as unknown;
			if (isOk) return { ok: true, data, status };

			const errorData: DiscordError =
				data && typeof data === "object" && "message" in data
					? (data as DiscordError)
					: { message: "Unknown API Error" };

			return { ok: false, data: errorData, status };
		} catch (error) {
			const message = error instanceof Error ? error.message : "Network Error";
			return {
				ok: false,
				data: { message },
				status: 0,
			};
		}
	};

	const createProxy = (pathSegments: string[] = []): unknown => {
		const dummyTarget = () => {};

		const handler: ProxyHandler<typeof dummyTarget> = {
			apply(_target, _thisArg, argList) {
				const nextSegments = [...pathSegments, ...argList.map(String)];
				return createProxy(nextSegments);
			},

			get(_target, prop) {
				const propStr = String(prop);

				if (prop === "then") {
					const promise = executeRequest(pathSegments, "GET");
					return promise.then.bind(promise);
				}

				const lowercaseProp = propStr.toLowerCase();
				const methodMap: Record<string, string> = {
					create: "POST",
					post: "POST",
					delete: "DELETE",
					update: "PATCH",
					patch: "PATCH",
					put: "PUT",
					get: "GET",
				};

				if (lowercaseProp in methodMap) {
					const httpMethod = methodMap[lowercaseProp];
					if (!httpMethod) throw new Error(`unsupported HTTP method: ${lowercaseProp}`);

					return async (
						bodyOrOptions?: unknown,
						methodOptions?: { query?: Record<string, unknown> },
					) => {
						let body: unknown;
						let query: Record<string, unknown> | undefined;

						if (httpMethod === "GET" || httpMethod === "DELETE") {
							query = (bodyOrOptions as { query?: Record<string, unknown> } | undefined)?.query;
						} else {
							body = bodyOrOptions;
							query = methodOptions?.query;
						}

						return executeRequest(pathSegments, httpMethod, body, query);
					};
				}

				const nextSegment = prop === "me" ? "@me" : propStr;
				return createProxy([...pathSegments, nextSegment]);
			},
		};

		return new Proxy(dummyTarget, handler);
	};

	return createProxy([]) as unknown as DiscordRestClient;
}