Skip to content

Commit

Permalink
(0.11.0) A few changes
Browse files Browse the repository at this point in the history
- Make ID -> Id more consistent across the project
- Rename method files to not include s at the end
- Add BotMethods.getApplicationInfo
- bump deps
- Add a tokenless file for getting an oauth2 token. More to come soon
- HTTP delete is not allowed to have a body
- remove explicit dep on undici
- min version of node is v16.15.0 for fetch
  • Loading branch information
PapiOphidian committed Feb 10, 2025
1 parent a2ed29a commit 3bdbce9
Show file tree
Hide file tree
Showing 16 changed files with 180 additions and 133 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Part of the WeatherStack

SnowTransfer is a small library specially made to **only** cover the REST/HTTP area of the discord api.
It makes no assumptions about the rest of your stack, therefore you can use it anywhere as long as you use node 14 or higher.
It makes no assumptions about the rest of your stack, therefore you can use it anywhere as long as you use node 16.15.0 or higher.

### Some of the things that make SnowTransfer awesome:
- No requirement for other components
Expand All @@ -27,7 +27,7 @@ I've written a general whitepaper on the idea of microservice bots, which you ca
You can find the docs at [https://daswolke.github.io/SnowTransfer/](https://daswolke.github.io/SnowTransfer/)

### Installation:
To install SnowTransfer, make sure that you have node 14 or higher and npm installed on your computer.
To install SnowTransfer, make sure that you have node 16.15.0 or higher and npm installed on your computer.

Then run the following command in a terminal `npm install snowtransfer`

Expand Down
25 changes: 12 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "snowtransfer",
"version": "0.10.9",
"version": "0.11.0",
"description": "Minimalistic Rest client for the Discord Api",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"node": ">=14.18.0"
"node": ">=16.15.0"
},
"keywords": [
"discord",
Expand All @@ -18,7 +18,7 @@
"microservice bot"
],
"scripts": {
"build:src": "tsup src/index.ts --clean --dts --sourcemap --format cjs --target node14 --minify-whitespace --minify-syntax --treeshake && node ./sourceMapPostProcess.js",
"build:src": "tsup src/index.ts --clean --dts --sourcemap --format cjs --target node16 --minify-whitespace --minify-syntax --treeshake && node ./sourceMapPostProcess.js",
"build:docs": "typedoc --name SnowTransfer --excludeExternals --sort static-first --sort alphabetical"
},
"repository": {
Expand All @@ -29,18 +29,17 @@
"author": "wolke <[email protected]>",
"license": "MIT",
"dependencies": {
"discord-api-types": "^0.37.114",
"undici": "^7.2.0"
"discord-api-types": "^0.37.119"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@typescript-eslint/eslint-plugin": "^8.18.2",
"@typescript-eslint/parser": "^8.18.2",
"eslint": "^9.17.0",
"tsup": "^8.3.5",
"typedoc": "^0.27.6",
"typedoc-plugin-mdn-links": "^4.0.6",
"typescript": "^5.7.2"
"@types/node": "^22.13.1",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
"eslint": "^9.20.0",
"tsup": "^8.3.6",
"typedoc": "^0.27.7",
"typedoc-plugin-mdn-links": "^4.0.12",
"typescript": "^5.7.3"
},
"files": [
"dist",
Expand Down
1 change: 0 additions & 1 deletion src/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Blob, File } from "buffer";
import { Readable } from "stream";
import { ReadableStream } from "stream/web";
import { FormData, Response } from "undici";

const mentionRegex = /@([^<>@ ]*)/gsmu;
const isValidUserMentionRegex = /^[&!]?\d+$/;
Expand Down
167 changes: 84 additions & 83 deletions src/Endpoints.ts

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions src/RequestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import path = require("path");
import { EventEmitter } from "events";
import crypto = require("crypto");

import { fetch, FormData, Headers, Response } from "undici"; // Not using global.fetch yet until the Node ecosystem matures

import Endpoints = require("./Endpoints");
const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), { encoding: "utf8" })); // otherwise, the json was included in the build
import Constants = require("./Constants");
Expand All @@ -28,7 +26,7 @@ const webhooksRegex = /^\/webhooks\/(\d+)\/[A-Za-z0-9-_]{64,}/;
const isMessageEndpointRegex = /\/messages\/:id$/;
const isGuildChannelsRegex = /\/guilds\/\d+\/channels$/;

const disallowedBodyMethods = new Set(["head", "get"]);
const disallowedBodyMethods = new Set(["head", "get", "delete"]);

/**
* @since 0.3.0
Expand Down Expand Up @@ -113,7 +111,7 @@ export class Ratelimiter<B extends typeof GlobalBucket = typeof GlobalBucket> {
public routify(url: string, method: string): string {
let route = url.replace(routeRegex, function (match, p: string) {
return p === "channels" || p === "guilds" || p === "webhooks" ? match : `/${p}/:id`;
}).replace(reactionsRegex, "/reactions/:id").replace(reactionsUserRegex, "/reactions/:id/:userID").replace(webhooksRegex, "/webhooks/$1/:token");
}).replace(reactionsRegex, "/reactions/:id").replace(reactionsUserRegex, "/reactions/:id/:userId").replace(webhooksRegex, "/webhooks/$1/:token");

if (method.toUpperCase() === "DELETE" && isMessageEndpointRegex.test(route)) route = method + route;
else if (method.toUpperCase() === "GET" && isGuildChannelsRegex.test(route)) route = "/guilds/:id/channels";
Expand Down Expand Up @@ -348,13 +346,15 @@ export class RequestHandler extends EventEmitter {
* @param data data to send, if any
* @returns Result of the request
*/
public request<T extends "json" | "multipart">(endpoint: string, params: Record<string, any> = {}, method: HTTPMethod, dataType: T = "json" as T, data?: T extends "json" ? any : FormData, extraHeaders?: Record<string, string>, retries = this.options.retryLimit): Promise<any> {
public request(endpoint: string, params: Record<string, any> | undefined, method: HTTPMethod, dataType: "json", data?: any, extraHeaders?: Record<string, string>, retries?: number): Promise<any>
public request(endpoint: string, params: Record<string, any> | undefined, method: HTTPMethod, dataType: "multipart", data?: FormData, extraHeaders?: Record<string, string>, retries?: number): Promise<any>
public request(endpoint: string, params: Record<string, any> = {}, method: HTTPMethod, dataType: "json" | "multipart", data?: any, extraHeaders?: Record<string, string>, retries = this.options.retryLimit): Promise<any> {
// const stack = new Error().stack as string;
return new Promise(async (res, rej) => {
const fn = async (bkt?: GlobalBucket | undefined) => {
const reqID = crypto.randomBytes(20).toString("hex");
const reqId = crypto.randomBytes(20).toString("hex");
try {
this.emit("request", reqID, { endpoint, method, dataType, data: data ?? {} });
this.emit("request", reqId, { endpoint, method, dataType, data: data ?? {} });

const before = Date.now();

Expand All @@ -376,7 +376,7 @@ export class RequestHandler extends EventEmitter {
if (bkt) this._applyRatelimitHeaders(bkt, request.headers);

if (request.status && !Constants.OK_STATUS_CODES.has(request.status) && request.status !== 429) {
if (this.options.retryFailed && !Constants.DO_NOT_RETRY_STATUS_CODES.has(request.status) && retries !== 0) return this.request(endpoint, params, method, dataType, data, extraHeaders, retries--).then(res).catch(rej);
if (this.options.retryFailed && !Constants.DO_NOT_RETRY_STATUS_CODES.has(request.status) && retries !== 0) return this.request(endpoint, params, method, dataType as any, data, extraHeaders, retries--).then(res).catch(rej);
throw new DiscordAPIError(
endpoint,
{ message: await request.text() },
Expand All @@ -403,7 +403,7 @@ export class RequestHandler extends EventEmitter {
throw new DiscordAPIError(endpoint, { message: "You're being ratelimited", code: 429 }, method, request.status);
}

this.emit("done", reqID, request);
this.emit("done", reqId, request);

if (request.body) {
let b: any;
Expand All @@ -416,7 +416,7 @@ export class RequestHandler extends EventEmitter {
} else return res(undefined);
} catch (error) {
// if (error && error.stack) error.stack = stack;
this.emit("requestError", reqID, error);
this.emit("requestError", reqId, error);
return rej(error as Error);
}
};
Expand Down
14 changes: 7 additions & 7 deletions src/SnowTransfer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Ratelimiter, RequestHandler } from "./RequestHandler";
import AuditLogMethods = require("./methods/AuditLog");
import AutoModerationMethods = require("./methods/AutoModeration");
import BotMethods = require("./methods/Bots");
import ChannelMethods = require("./methods/Channels");
import BotMethods = require("./methods/Bot");
import ChannelMethods = require("./methods/Channel");
import GuildAssetsMethods = require("./methods/GuildAssets");
import GuildMethods = require("./methods/Guilds");
import GuildMethods = require("./methods/Guild");
import GuildScheduledEventMethods = require("./methods/GuildScheduledEvent");
import GuildTemplateMethods = require("./methods/GuildTemplate");
import InteractionMethods = require("./methods/Interactions");
import InviteMethods = require("./methods/Invites");
import InteractionMethods = require("./methods/Interaction");
import InviteMethods = require("./methods/Invite");
import StageInstanceMethods = require("./methods/StageInstance");
import UserMethods = require("./methods/Users");
import UserMethods = require("./methods/User");
import VoiceMethods = require("./methods/Voice");
import WebhookMethods = require("./methods/Webhooks");
import WebhookMethods = require("./methods/Webhook");
import Endpoints = require("./Endpoints");
import Constants = require("./Constants");

Expand Down
18 changes: 11 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import AuditLogMethods2 = require("./methods/AuditLog");
import AutoModerationMethods2 = require("./methods/AutoModeration");
import BotMethods2 = require("./methods/Bots");
import ChannelMethods2 = require("./methods/Channels");
import BotMethods2 = require("./methods/Bot");
import ChannelMethods2 = require("./methods/Channel");
import GuildAssetsMethods2 = require("./methods/GuildAssets");
import GuildMethods2 = require("./methods/Guilds");
import GuildMethods2 = require("./methods/Guild");
import GuildScheduledEventMethods2 = require("./methods/GuildScheduledEvent");
import GuildTemplateMethods2 = require("./methods/GuildTemplate");
import InteractionMethods2 = require("./methods/Interactions");
import InviteMethods2 = require("./methods/Invites");
import InteractionMethods2 = require("./methods/Interaction");
import InviteMethods2 = require("./methods/Invite");
import StageInstanceMethods2 = require("./methods/StageInstance");
import UserMethods2 = require("./methods/Users");
import UserMethods2 = require("./methods/User");
import VoiceMethods2 = require("./methods/Voice");
import WebhookMethods2 = require("./methods/Webhooks");
import WebhookMethods2 = require("./methods/Webhook");

import tokenless2 = require("./tokenless");

import Constants2 = require("./Constants");
import Endpoints2 = require("./Endpoints");
Expand Down Expand Up @@ -43,6 +45,8 @@ export {
VoiceMethods2 as VoiceMethods,
WebhookMethods2 as WebhookMethods,

tokenless2 as tokenless,

Constants2 as Constants,
Endpoints2 as Endpoints,
SnowTransfer2 as SnowTransfer,
Expand Down
16 changes: 15 additions & 1 deletion src/methods/Bots.ts → src/methods/Bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { RequestHandler as RH } from "../RequestHandler";

import type {
RESTGetAPIGatewayBotResult,
RESTGetAPIGatewayResult
RESTGetAPIGatewayResult,
APIApplication
} from "discord-api-types/v10";

/**
Expand Down Expand Up @@ -49,6 +50,19 @@ class BotMethods {
public getGatewayBot(): Promise<RESTGetAPIGatewayBotResult> {
return this.requestHandler.request(Endpoints.GATEWAY_BOT, {}, "get", "json");
}

/**
* Get the Application Object for the CurrentUser
* @since 0.11.0
* @returns An [Application object](https://discord.com/developers/docs/resources/application#application-object-application-structure)
*
* @example
* const client = new SnowTransfer("TOKEN")
* const result = await client.bot.getApplicationInfo()
*/
public getApplicationInfo(): Promise<APIApplication> {
return this.requestHandler.request(Endpoints.OAUTH2_APPLICATION("@me"), {}, "get", "json")
}
}

export = BotMethods;
2 changes: 0 additions & 2 deletions src/methods/Channels.ts → src/methods/Channel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { fetch } from "undici";

import type { RequestHandler as RH, RESTPostAPIAttachmentsRefreshURLsResult } from "../RequestHandler";

import Endpoints = require("../Endpoints");
Expand Down
File renamed without changes.
2 changes: 0 additions & 2 deletions src/methods/GuildAssets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { FormData } from "undici";

import Constants = require("../Constants");
import Endpoints = require("../Endpoints");

Expand Down
2 changes: 1 addition & 1 deletion src/methods/Interactions.ts → src/methods/Interaction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Endpoints = require("../Endpoints");
import Constants = require("../Constants");

import type WHM = require("./Webhooks");
import type WHM = require("./Webhook");
import type { RequestHandler as RH } from "../RequestHandler";

import type {
Expand Down
File renamed without changes.
File renamed without changes.
8 changes: 4 additions & 4 deletions src/methods/Webhooks.ts → src/methods/Webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,12 @@ class WebhookMethods {
* const message = await client.webhook.editWebhookMessage("webhook Id", "webhook token", "message Id", { content: "New content" })
*/
public async editWebhookMessage(webhookId: string, token: string, messageId: string, data: RESTPatchAPIWebhookWithTokenMessageJSONBody & { thread_id?: string; files?: Array<{ name: string; file: Buffer | Readable | ReadableStream; }> }): Promise<RESTPatchAPIWebhookWithTokenMessageResult> {
let threadID: string | undefined = undefined;
if (data.thread_id) threadID = data.thread_id;
let threadId: string | undefined = undefined;
if (data.thread_id) threadId = data.thread_id;
delete data.thread_id;

if (data.files) return this.requestHandler.request(Endpoints.WEBHOOK_TOKEN_MESSAGE(webhookId, token, messageId), { thread_id: threadID }, "patch", "multipart", await Constants.standardMultipartHandler(data as Parameters<typeof Constants["standardMultipartHandler"]>["0"]));
else return this.requestHandler.request(Endpoints.WEBHOOK_TOKEN_MESSAGE(webhookId, token, messageId), { thread_id: threadID }, "patch", "json", data);
if (data.files) return this.requestHandler.request(Endpoints.WEBHOOK_TOKEN_MESSAGE(webhookId, token, messageId), { thread_id: threadId }, "patch", "multipart", await Constants.standardMultipartHandler(data as Parameters<typeof Constants["standardMultipartHandler"]>["0"]));
else return this.requestHandler.request(Endpoints.WEBHOOK_TOKEN_MESSAGE(webhookId, token, messageId), { thread_id: threadId }, "patch", "json", data);
}

/**
Expand Down
34 changes: 34 additions & 0 deletions src/tokenless.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { RESTPostOAuth2AccessTokenResult } from "discord-api-types/v10";

import Endpoints = require("./Endpoints");

/**
* Get an oauth token after being authorized
* @since 0.11.0
* @param clientId The ID of your application. For older bots, this is different from your bot's user ID
* @param redirectURI The URI Discord will redirect the user to after they authorize
* @param clientSecret The secret of your client you can obtain from the Application page
* @param code The code returned from Discord from the oauth authorize flow
* @returns The authorization
*
* @example
* const { tokenless } = require("snowtransfer")
* const result = await tokenless.getOauth2Token(id, redirectURI, secret, code)
*/
async function getOauth2Token(clientId: string, redirectURI: string, clientSecret: string, code: string): Promise<RESTPostOAuth2AccessTokenResult> {
const response = await fetch(`${Endpoints.BASE_HOST}${Endpoints.OAUTH2_TOKEN}`, {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectURI
})
});
return response.json() as Promise<RESTPostOAuth2AccessTokenResult>;
}

export = {
getOauth2Token
}

0 comments on commit 3bdbce9

Please sign in to comment.