summaryrefslogtreecommitdiff
path: root/node_modules/@discordjs/rest/dist/web.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@discordjs/rest/dist/web.js')
-rw-r--r--node_modules/@discordjs/rest/dist/web.js1355
1 files changed, 1355 insertions, 0 deletions
diff --git a/node_modules/@discordjs/rest/dist/web.js b/node_modules/@discordjs/rest/dist/web.js
new file mode 100644
index 0000000..191975b
--- /dev/null
+++ b/node_modules/@discordjs/rest/dist/web.js
@@ -0,0 +1,1355 @@
+"use strict";
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
+var __export = (target, all) => {
+ for (var name in all)
+ __defProp(target, name, { get: all[name], enumerable: true });
+};
+var __copyProps = (to, from, except, desc) => {
+ if (from && typeof from === "object" || typeof from === "function") {
+ for (let key of __getOwnPropNames(from))
+ if (!__hasOwnProp.call(to, key) && key !== except)
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+ }
+ return to;
+};
+var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
+
+// src/web.ts
+var web_exports = {};
+__export(web_exports, {
+ ALLOWED_EXTENSIONS: () => ALLOWED_EXTENSIONS,
+ ALLOWED_SIZES: () => ALLOWED_SIZES,
+ ALLOWED_STICKER_EXTENSIONS: () => ALLOWED_STICKER_EXTENSIONS,
+ BurstHandlerMajorIdKey: () => BurstHandlerMajorIdKey,
+ CDN: () => CDN,
+ DefaultRestOptions: () => DefaultRestOptions,
+ DefaultUserAgent: () => DefaultUserAgent,
+ DefaultUserAgentAppendix: () => DefaultUserAgentAppendix,
+ DiscordAPIError: () => DiscordAPIError,
+ HTTPError: () => HTTPError,
+ OverwrittenMimeTypes: () => OverwrittenMimeTypes,
+ REST: () => REST,
+ RESTEvents: () => RESTEvents,
+ RateLimitError: () => RateLimitError,
+ RequestMethod: () => RequestMethod,
+ calculateUserDefaultAvatarIndex: () => calculateUserDefaultAvatarIndex,
+ makeURLSearchParams: () => makeURLSearchParams,
+ parseResponse: () => parseResponse,
+ version: () => version
+});
+module.exports = __toCommonJS(web_exports);
+
+// src/environment.ts
+var defaultStrategy;
+function setDefaultStrategy(newStrategy) {
+ defaultStrategy = newStrategy;
+}
+__name(setDefaultStrategy, "setDefaultStrategy");
+function getDefaultStrategy() {
+ return defaultStrategy;
+}
+__name(getDefaultStrategy, "getDefaultStrategy");
+
+// src/lib/utils/constants.ts
+var import_util = require("@discordjs/util");
+var import_v10 = require("discord-api-types/v10");
+var DefaultUserAgent = `DiscordBot (https://discord.js.org, 2.0.1)`;
+var DefaultUserAgentAppendix = (0, import_util.getUserAgentAppendix)();
+var DefaultRestOptions = {
+ agent: null,
+ api: "https://discord.com/api",
+ authPrefix: "Bot",
+ cdn: "https://cdn.discordapp.com",
+ headers: {},
+ invalidRequestWarningInterval: 0,
+ globalRequestsPerSecond: 50,
+ offset: 50,
+ rejectOnRateLimit: null,
+ retries: 3,
+ timeout: 15e3,
+ userAgentAppendix: DefaultUserAgentAppendix,
+ version: import_v10.APIVersion,
+ hashSweepInterval: 144e5,
+ // 4 Hours
+ hashLifetime: 864e5,
+ // 24 Hours
+ handlerSweepInterval: 36e5,
+ // 1 Hour
+ async makeRequest(...args) {
+ return getDefaultStrategy()(...args);
+ }
+};
+var RESTEvents = /* @__PURE__ */ ((RESTEvents2) => {
+ RESTEvents2["Debug"] = "restDebug";
+ RESTEvents2["HandlerSweep"] = "handlerSweep";
+ RESTEvents2["HashSweep"] = "hashSweep";
+ RESTEvents2["InvalidRequestWarning"] = "invalidRequestWarning";
+ RESTEvents2["RateLimited"] = "rateLimited";
+ RESTEvents2["Response"] = "response";
+ return RESTEvents2;
+})(RESTEvents || {});
+var ALLOWED_EXTENSIONS = ["webp", "png", "jpg", "jpeg", "gif"];
+var ALLOWED_STICKER_EXTENSIONS = ["png", "json", "gif"];
+var ALLOWED_SIZES = [16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
+var OverwrittenMimeTypes = {
+ // https://github.com/discordjs/discord.js/issues/8557
+ "image/apng": "image/png"
+};
+var BurstHandlerMajorIdKey = "burst";
+
+// src/lib/CDN.ts
+var CDN = class {
+ constructor(base = DefaultRestOptions.cdn) {
+ this.base = base;
+ }
+ static {
+ __name(this, "CDN");
+ }
+ /**
+ * Generates an app asset URL for a client's asset.
+ *
+ * @param clientId - The client id that has the asset
+ * @param assetHash - The hash provided by Discord for this asset
+ * @param options - Optional options for the asset
+ */
+ appAsset(clientId, assetHash, options) {
+ return this.makeURL(`/app-assets/${clientId}/${assetHash}`, options);
+ }
+ /**
+ * Generates an app icon URL for a client's icon.
+ *
+ * @param clientId - The client id that has the icon
+ * @param iconHash - The hash provided by Discord for this icon
+ * @param options - Optional options for the icon
+ */
+ appIcon(clientId, iconHash, options) {
+ return this.makeURL(`/app-icons/${clientId}/${iconHash}`, options);
+ }
+ /**
+ * Generates an avatar URL, e.g. for a user or a webhook.
+ *
+ * @param id - The id that has the icon
+ * @param avatarHash - The hash provided by Discord for this avatar
+ * @param options - Optional options for the avatar
+ */
+ avatar(id, avatarHash, options) {
+ return this.dynamicMakeURL(`/avatars/${id}/${avatarHash}`, avatarHash, options);
+ }
+ /**
+ * Generates a user avatar decoration URL.
+ *
+ * @param userId - The id of the user
+ * @param userAvatarDecoration - The hash provided by Discord for this avatar decoration
+ * @param options - Optional options for the avatar decoration
+ */
+ avatarDecoration(userId, userAvatarDecoration, options) {
+ return this.makeURL(`/avatar-decorations/${userId}/${userAvatarDecoration}`, options);
+ }
+ /**
+ * Generates a banner URL, e.g. for a user or a guild.
+ *
+ * @param id - The id that has the banner splash
+ * @param bannerHash - The hash provided by Discord for this banner
+ * @param options - Optional options for the banner
+ */
+ banner(id, bannerHash, options) {
+ return this.dynamicMakeURL(`/banners/${id}/${bannerHash}`, bannerHash, options);
+ }
+ /**
+ * Generates an icon URL for a channel, e.g. a group DM.
+ *
+ * @param channelId - The channel id that has the icon
+ * @param iconHash - The hash provided by Discord for this channel
+ * @param options - Optional options for the icon
+ */
+ channelIcon(channelId, iconHash, options) {
+ return this.makeURL(`/channel-icons/${channelId}/${iconHash}`, options);
+ }
+ /**
+ * Generates a default avatar URL
+ *
+ * @param index - The default avatar index
+ * @remarks
+ * To calculate the index for a user do `(userId >> 22) % 6`,
+ * or `discriminator % 5` if they're using the legacy username system.
+ */
+ defaultAvatar(index) {
+ return this.makeURL(`/embed/avatars/${index}`, { extension: "png" });
+ }
+ /**
+ * Generates a discovery splash URL for a guild's discovery splash.
+ *
+ * @param guildId - The guild id that has the discovery splash
+ * @param splashHash - The hash provided by Discord for this splash
+ * @param options - Optional options for the splash
+ */
+ discoverySplash(guildId, splashHash, options) {
+ return this.makeURL(`/discovery-splashes/${guildId}/${splashHash}`, options);
+ }
+ /**
+ * Generates an emoji's URL for an emoji.
+ *
+ * @param emojiId - The emoji id
+ * @param extension - The extension of the emoji
+ */
+ emoji(emojiId, extension) {
+ return this.makeURL(`/emojis/${emojiId}`, { extension });
+ }
+ /**
+ * Generates a guild member avatar URL.
+ *
+ * @param guildId - The id of the guild
+ * @param userId - The id of the user
+ * @param avatarHash - The hash provided by Discord for this avatar
+ * @param options - Optional options for the avatar
+ */
+ guildMemberAvatar(guildId, userId, avatarHash, options) {
+ return this.dynamicMakeURL(`/guilds/${guildId}/users/${userId}/avatars/${avatarHash}`, avatarHash, options);
+ }
+ /**
+ * Generates a guild member banner URL.
+ *
+ * @param guildId - The id of the guild
+ * @param userId - The id of the user
+ * @param bannerHash - The hash provided by Discord for this banner
+ * @param options - Optional options for the banner
+ */
+ guildMemberBanner(guildId, userId, bannerHash, options) {
+ return this.dynamicMakeURL(`/guilds/${guildId}/users/${userId}/banner`, bannerHash, options);
+ }
+ /**
+ * Generates an icon URL, e.g. for a guild.
+ *
+ * @param id - The id that has the icon splash
+ * @param iconHash - The hash provided by Discord for this icon
+ * @param options - Optional options for the icon
+ */
+ icon(id, iconHash, options) {
+ return this.dynamicMakeURL(`/icons/${id}/${iconHash}`, iconHash, options);
+ }
+ /**
+ * Generates a URL for the icon of a role
+ *
+ * @param roleId - The id of the role that has the icon
+ * @param roleIconHash - The hash provided by Discord for this role icon
+ * @param options - Optional options for the role icon
+ */
+ roleIcon(roleId, roleIconHash, options) {
+ return this.makeURL(`/role-icons/${roleId}/${roleIconHash}`, options);
+ }
+ /**
+ * Generates a guild invite splash URL for a guild's invite splash.
+ *
+ * @param guildId - The guild id that has the invite splash
+ * @param splashHash - The hash provided by Discord for this splash
+ * @param options - Optional options for the splash
+ */
+ splash(guildId, splashHash, options) {
+ return this.makeURL(`/splashes/${guildId}/${splashHash}`, options);
+ }
+ /**
+ * Generates a sticker URL.
+ *
+ * @param stickerId - The sticker id
+ * @param extension - The extension of the sticker
+ * @privateRemarks
+ * Stickers cannot have a `.webp` extension, so we default to a `.png`
+ */
+ sticker(stickerId, extension = "png") {
+ return this.makeURL(`/stickers/${stickerId}`, { allowedExtensions: ALLOWED_STICKER_EXTENSIONS, extension });
+ }
+ /**
+ * Generates a sticker pack banner URL.
+ *
+ * @param bannerId - The banner id
+ * @param options - Optional options for the banner
+ */
+ stickerPackBanner(bannerId, options) {
+ return this.makeURL(`/app-assets/710982414301790216/store/${bannerId}`, options);
+ }
+ /**
+ * Generates a team icon URL for a team's icon.
+ *
+ * @param teamId - The team id that has the icon
+ * @param iconHash - The hash provided by Discord for this icon
+ * @param options - Optional options for the icon
+ */
+ teamIcon(teamId, iconHash, options) {
+ return this.makeURL(`/team-icons/${teamId}/${iconHash}`, options);
+ }
+ /**
+ * Generates a cover image for a guild scheduled event.
+ *
+ * @param scheduledEventId - The scheduled event id
+ * @param coverHash - The hash provided by discord for this cover image
+ * @param options - Optional options for the cover image
+ */
+ guildScheduledEventCover(scheduledEventId, coverHash, options) {
+ return this.makeURL(`/guild-events/${scheduledEventId}/${coverHash}`, options);
+ }
+ /**
+ * Constructs the URL for the resource, checking whether or not `hash` starts with `a_` if `dynamic` is set to `true`.
+ *
+ * @param route - The base cdn route
+ * @param hash - The hash provided by Discord for this icon
+ * @param options - Optional options for the link
+ */
+ dynamicMakeURL(route, hash, { forceStatic = false, ...options } = {}) {
+ return this.makeURL(route, !forceStatic && hash.startsWith("a_") ? { ...options, extension: "gif" } : options);
+ }
+ /**
+ * Constructs the URL for the resource
+ *
+ * @param route - The base cdn route
+ * @param options - The extension/size options for the link
+ */
+ makeURL(route, { allowedExtensions = ALLOWED_EXTENSIONS, extension = "webp", size } = {}) {
+ extension = String(extension).toLowerCase();
+ if (!allowedExtensions.includes(extension)) {
+ throw new RangeError(`Invalid extension provided: ${extension}
+Must be one of: ${allowedExtensions.join(", ")}`);
+ }
+ if (size && !ALLOWED_SIZES.includes(size)) {
+ throw new RangeError(`Invalid size provided: ${size}
+Must be one of: ${ALLOWED_SIZES.join(", ")}`);
+ }
+ const url = new URL(`${this.base}${route}.${extension}`);
+ if (size) {
+ url.searchParams.set("size", String(size));
+ }
+ return url.toString();
+ }
+};
+
+// src/lib/errors/DiscordAPIError.ts
+function isErrorGroupWrapper(error) {
+ return Reflect.has(error, "_errors");
+}
+__name(isErrorGroupWrapper, "isErrorGroupWrapper");
+function isErrorResponse(error) {
+ return typeof Reflect.get(error, "message") === "string";
+}
+__name(isErrorResponse, "isErrorResponse");
+var DiscordAPIError = class _DiscordAPIError extends Error {
+ /**
+ * @param rawError - The error reported by Discord
+ * @param code - The error code reported by Discord
+ * @param status - The status code of the response
+ * @param method - The method of the request that erred
+ * @param url - The url of the request that erred
+ * @param bodyData - The unparsed data for the request that errored
+ */
+ constructor(rawError, code, status, method, url, bodyData) {
+ super(_DiscordAPIError.getMessage(rawError));
+ this.rawError = rawError;
+ this.code = code;
+ this.status = status;
+ this.method = method;
+ this.url = url;
+ this.requestBody = { files: bodyData.files, json: bodyData.body };
+ }
+ static {
+ __name(this, "DiscordAPIError");
+ }
+ requestBody;
+ /**
+ * The name of the error
+ */
+ get name() {
+ return `${_DiscordAPIError.name}[${this.code}]`;
+ }
+ static getMessage(error) {
+ let flattened = "";
+ if ("code" in error) {
+ if (error.errors) {
+ flattened = [...this.flattenDiscordError(error.errors)].join("\n");
+ }
+ return error.message && flattened ? `${error.message}
+${flattened}` : error.message || flattened || "Unknown Error";
+ }
+ return error.error_description ?? "No Description";
+ }
+ static *flattenDiscordError(obj, key = "") {
+ if (isErrorResponse(obj)) {
+ return yield `${key.length ? `${key}[${obj.code}]` : `${obj.code}`}: ${obj.message}`.trim();
+ }
+ for (const [otherKey, val] of Object.entries(obj)) {
+ const nextKey = otherKey.startsWith("_") ? key : key ? Number.isNaN(Number(otherKey)) ? `${key}.${otherKey}` : `${key}[${otherKey}]` : otherKey;
+ if (typeof val === "string") {
+ yield val;
+ } else if (isErrorGroupWrapper(val)) {
+ for (const error of val._errors) {
+ yield* this.flattenDiscordError(error, nextKey);
+ }
+ } else {
+ yield* this.flattenDiscordError(val, nextKey);
+ }
+ }
+ }
+};
+
+// src/lib/errors/HTTPError.ts
+var HTTPError = class _HTTPError extends Error {
+ /**
+ * @param status - The status code of the response
+ * @param statusText - The status text of the response
+ * @param method - The method of the request that erred
+ * @param url - The url of the request that erred
+ * @param bodyData - The unparsed data for the request that errored
+ */
+ constructor(status, statusText, method, url, bodyData) {
+ super(statusText);
+ this.status = status;
+ this.method = method;
+ this.url = url;
+ this.requestBody = { files: bodyData.files, json: bodyData.body };
+ }
+ static {
+ __name(this, "HTTPError");
+ }
+ requestBody;
+ name = _HTTPError.name;
+};
+
+// src/lib/errors/RateLimitError.ts
+var RateLimitError = class _RateLimitError extends Error {
+ static {
+ __name(this, "RateLimitError");
+ }
+ timeToReset;
+ limit;
+ method;
+ hash;
+ url;
+ route;
+ majorParameter;
+ global;
+ constructor({ timeToReset, limit, method, hash, url, route, majorParameter, global }) {
+ super();
+ this.timeToReset = timeToReset;
+ this.limit = limit;
+ this.method = method;
+ this.hash = hash;
+ this.url = url;
+ this.route = route;
+ this.majorParameter = majorParameter;
+ this.global = global;
+ }
+ /**
+ * The name of the error
+ */
+ get name() {
+ return `${_RateLimitError.name}[${this.route}]`;
+ }
+};
+
+// src/lib/REST.ts
+var import_collection = require("@discordjs/collection");
+var import_snowflake = require("@sapphire/snowflake");
+var import_async_event_emitter = require("@vladfrangu/async_event_emitter");
+var import_magic_bytes = require("magic-bytes.js");
+
+// src/lib/utils/types.ts
+var RequestMethod = /* @__PURE__ */ ((RequestMethod2) => {
+ RequestMethod2["Delete"] = "DELETE";
+ RequestMethod2["Get"] = "GET";
+ RequestMethod2["Patch"] = "PATCH";
+ RequestMethod2["Post"] = "POST";
+ RequestMethod2["Put"] = "PUT";
+ return RequestMethod2;
+})(RequestMethod || {});
+
+// src/lib/utils/utils.ts
+function serializeSearchParam(value) {
+ switch (typeof value) {
+ case "string":
+ return value;
+ case "number":
+ case "bigint":
+ case "boolean":
+ return value.toString();
+ case "object":
+ if (value === null)
+ return null;
+ if (value instanceof Date) {
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
+ }
+ if (typeof value.toString === "function" && value.toString !== Object.prototype.toString)
+ return value.toString();
+ return null;
+ default:
+ return null;
+ }
+}
+__name(serializeSearchParam, "serializeSearchParam");
+function makeURLSearchParams(options) {
+ const params = new URLSearchParams();
+ if (!options)
+ return params;
+ for (const [key, value] of Object.entries(options)) {
+ const serialized = serializeSearchParam(value);
+ if (serialized !== null)
+ params.append(key, serialized);
+ }
+ return params;
+}
+__name(makeURLSearchParams, "makeURLSearchParams");
+async function parseResponse(res) {
+ if (res.headers.get("Content-Type")?.startsWith("application/json")) {
+ return res.json();
+ }
+ return res.arrayBuffer();
+}
+__name(parseResponse, "parseResponse");
+function hasSublimit(bucketRoute, body, method) {
+ if (bucketRoute === "/channels/:id") {
+ if (typeof body !== "object" || body === null)
+ return false;
+ if (method !== "PATCH" /* Patch */)
+ return false;
+ const castedBody = body;
+ return ["name", "topic"].some((key) => Reflect.has(castedBody, key));
+ }
+ return true;
+}
+__name(hasSublimit, "hasSublimit");
+function shouldRetry(error) {
+ if (error.name === "AbortError")
+ return true;
+ return "code" in error && error.code === "ECONNRESET" || error.message.includes("ECONNRESET");
+}
+__name(shouldRetry, "shouldRetry");
+async function onRateLimit(manager, rateLimitData) {
+ const { options } = manager;
+ if (!options.rejectOnRateLimit)
+ return;
+ const shouldThrow = typeof options.rejectOnRateLimit === "function" ? await options.rejectOnRateLimit(rateLimitData) : options.rejectOnRateLimit.some((route) => rateLimitData.route.startsWith(route.toLowerCase()));
+ if (shouldThrow) {
+ throw new RateLimitError(rateLimitData);
+ }
+}
+__name(onRateLimit, "onRateLimit");
+function calculateUserDefaultAvatarIndex(userId) {
+ return Number(BigInt(userId) >> 22n) % 6;
+}
+__name(calculateUserDefaultAvatarIndex, "calculateUserDefaultAvatarIndex");
+async function sleep(ms) {
+ return new Promise((resolve) => {
+ setTimeout(() => resolve(), ms);
+ });
+}
+__name(sleep, "sleep");
+function isBufferLike(value) {
+ return value instanceof ArrayBuffer || value instanceof Uint8Array || value instanceof Uint8ClampedArray;
+}
+__name(isBufferLike, "isBufferLike");
+
+// src/lib/handlers/Shared.ts
+var invalidCount = 0;
+var invalidCountResetTime = null;
+function incrementInvalidCount(manager) {
+ if (!invalidCountResetTime || invalidCountResetTime < Date.now()) {
+ invalidCountResetTime = Date.now() + 1e3 * 60 * 10;
+ invalidCount = 0;
+ }
+ invalidCount++;
+ const emitInvalid = manager.options.invalidRequestWarningInterval > 0 && invalidCount % manager.options.invalidRequestWarningInterval === 0;
+ if (emitInvalid) {
+ manager.emit("invalidRequestWarning" /* InvalidRequestWarning */, {
+ count: invalidCount,
+ remainingTime: invalidCountResetTime - Date.now()
+ });
+ }
+}
+__name(incrementInvalidCount, "incrementInvalidCount");
+async function makeNetworkRequest(manager, routeId, url, options, requestData, retries) {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), manager.options.timeout);
+ if (requestData.signal) {
+ if (requestData.signal.aborted)
+ controller.abort();
+ else
+ requestData.signal.addEventListener("abort", () => controller.abort());
+ }
+ let res;
+ try {
+ res = await manager.options.makeRequest(url, { ...options, signal: controller.signal });
+ } catch (error) {
+ if (!(error instanceof Error))
+ throw error;
+ if (shouldRetry(error) && retries !== manager.options.retries) {
+ return null;
+ }
+ throw error;
+ } finally {
+ clearTimeout(timeout);
+ }
+ if (manager.listenerCount("response" /* Response */)) {
+ manager.emit(
+ "response" /* Response */,
+ {
+ method: options.method ?? "get",
+ path: routeId.original,
+ route: routeId.bucketRoute,
+ options,
+ data: requestData,
+ retries
+ },
+ res instanceof Response ? res.clone() : { ...res }
+ );
+ }
+ return res;
+}
+__name(makeNetworkRequest, "makeNetworkRequest");
+async function handleErrors(manager, res, method, url, requestData, retries) {
+ const status = res.status;
+ if (status >= 500 && status < 600) {
+ if (retries !== manager.options.retries) {
+ return null;
+ }
+ throw new HTTPError(status, res.statusText, method, url, requestData);
+ } else {
+ if (status >= 400 && status < 500) {
+ if (status === 401 && requestData.auth) {
+ manager.setToken(null);
+ }
+ const data = await parseResponse(res);
+ throw new DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
+ }
+ return res;
+ }
+}
+__name(handleErrors, "handleErrors");
+
+// src/lib/handlers/BurstHandler.ts
+var BurstHandler = class {
+ /**
+ * @param manager - The request manager
+ * @param hash - The hash that this RequestHandler handles
+ * @param majorParameter - The major parameter for this handler
+ */
+ constructor(manager, hash, majorParameter) {
+ this.manager = manager;
+ this.hash = hash;
+ this.majorParameter = majorParameter;
+ this.id = `${hash}:${majorParameter}`;
+ }
+ static {
+ __name(this, "BurstHandler");
+ }
+ /**
+ * {@inheritdoc IHandler.id}
+ */
+ id;
+ /**
+ * {@inheritDoc IHandler.inactive}
+ */
+ inactive = false;
+ /**
+ * Emits a debug message
+ *
+ * @param message - The message to debug
+ */
+ debug(message) {
+ this.manager.emit("restDebug" /* Debug */, `[REST ${this.id}] ${message}`);
+ }
+ /**
+ * {@inheritDoc IHandler.queueRequest}
+ */
+ async queueRequest(routeId, url, options, requestData) {
+ return this.runRequest(routeId, url, options, requestData);
+ }
+ /**
+ * The method that actually makes the request to the API, and updates info about the bucket accordingly
+ *
+ * @param routeId - The generalized API route with literal ids for major parameters
+ * @param url - The fully resolved URL to make the request to
+ * @param options - The fetch options needed to make the request
+ * @param requestData - Extra data from the user's request needed for errors and additional processing
+ * @param retries - The number of retries this request has already attempted (recursion)
+ */
+ async runRequest(routeId, url, options, requestData, retries = 0) {
+ const method = options.method ?? "get";
+ const res = await makeNetworkRequest(this.manager, routeId, url, options, requestData, retries);
+ if (res === null) {
+ return this.runRequest(routeId, url, options, requestData, ++retries);
+ }
+ const status = res.status;
+ let retryAfter = 0;
+ const retry = res.headers.get("Retry-After");
+ if (retry)
+ retryAfter = Number(retry) * 1e3 + this.manager.options.offset;
+ if (status === 401 || status === 403 || status === 429) {
+ incrementInvalidCount(this.manager);
+ }
+ if (status >= 200 && status < 300) {
+ return res;
+ } else if (status === 429) {
+ const isGlobal = res.headers.has("X-RateLimit-Global");
+ await onRateLimit(this.manager, {
+ timeToReset: retryAfter,
+ limit: Number.POSITIVE_INFINITY,
+ method,
+ hash: this.hash,
+ url,
+ route: routeId.bucketRoute,
+ majorParameter: this.majorParameter,
+ global: isGlobal
+ });
+ this.debug(
+ [
+ "Encountered unexpected 429 rate limit",
+ ` Global : ${isGlobal}`,
+ ` Method : ${method}`,
+ ` URL : ${url}`,
+ ` Bucket : ${routeId.bucketRoute}`,
+ ` Major parameter: ${routeId.majorParameter}`,
+ ` Hash : ${this.hash}`,
+ ` Limit : ${Number.POSITIVE_INFINITY}`,
+ ` Retry After : ${retryAfter}ms`,
+ ` Sublimit : None`
+ ].join("\n")
+ );
+ await sleep(retryAfter);
+ return this.runRequest(routeId, url, options, requestData, retries);
+ } else {
+ const handled = await handleErrors(this.manager, res, method, url, requestData, retries);
+ if (handled === null) {
+ return this.runRequest(routeId, url, options, requestData, ++retries);
+ }
+ return handled;
+ }
+ }
+};
+
+// src/lib/handlers/SequentialHandler.ts
+var import_async_queue = require("@sapphire/async-queue");
+var SequentialHandler = class {
+ /**
+ * @param manager - The request manager
+ * @param hash - The hash that this RequestHandler handles
+ * @param majorParameter - The major parameter for this handler
+ */
+ constructor(manager, hash, majorParameter) {
+ this.manager = manager;
+ this.hash = hash;
+ this.majorParameter = majorParameter;
+ this.id = `${hash}:${majorParameter}`;
+ }
+ static {
+ __name(this, "SequentialHandler");
+ }
+ /**
+ * {@inheritDoc IHandler.id}
+ */
+ id;
+ /**
+ * The time this rate limit bucket will reset
+ */
+ reset = -1;
+ /**
+ * The remaining requests that can be made before we are rate limited
+ */
+ remaining = 1;
+ /**
+ * The total number of requests that can be made before we are rate limited
+ */
+ limit = Number.POSITIVE_INFINITY;
+ /**
+ * The interface used to sequence async requests sequentially
+ */
+ #asyncQueue = new import_async_queue.AsyncQueue();
+ /**
+ * The interface used to sequence sublimited async requests sequentially
+ */
+ #sublimitedQueue = null;
+ /**
+ * A promise wrapper for when the sublimited queue is finished being processed or null when not being processed
+ */
+ #sublimitPromise = null;
+ /**
+ * Whether the sublimit queue needs to be shifted in the finally block
+ */
+ #shiftSublimit = false;
+ /**
+ * {@inheritDoc IHandler.inactive}
+ */
+ get inactive() {
+ return this.#asyncQueue.remaining === 0 && (this.#sublimitedQueue === null || this.#sublimitedQueue.remaining === 0) && !this.limited;
+ }
+ /**
+ * If the rate limit bucket is currently limited by the global limit
+ */
+ get globalLimited() {
+ return this.manager.globalRemaining <= 0 && Date.now() < this.manager.globalReset;
+ }
+ /**
+ * If the rate limit bucket is currently limited by its limit
+ */
+ get localLimited() {
+ return this.remaining <= 0 && Date.now() < this.reset;
+ }
+ /**
+ * If the rate limit bucket is currently limited
+ */
+ get limited() {
+ return this.globalLimited || this.localLimited;
+ }
+ /**
+ * The time until queued requests can continue
+ */
+ get timeToReset() {
+ return this.reset + this.manager.options.offset - Date.now();
+ }
+ /**
+ * Emits a debug message
+ *
+ * @param message - The message to debug
+ */
+ debug(message) {
+ this.manager.emit("restDebug" /* Debug */, `[REST ${this.id}] ${message}`);
+ }
+ /**
+ * Delay all requests for the specified amount of time, handling global rate limits
+ *
+ * @param time - The amount of time to delay all requests for
+ */
+ async globalDelayFor(time) {
+ await sleep(time);
+ this.manager.globalDelay = null;
+ }
+ /**
+ * {@inheritDoc IHandler.queueRequest}
+ */
+ async queueRequest(routeId, url, options, requestData) {
+ let queue = this.#asyncQueue;
+ let queueType = 0 /* Standard */;
+ if (this.#sublimitedQueue && hasSublimit(routeId.bucketRoute, requestData.body, options.method)) {
+ queue = this.#sublimitedQueue;
+ queueType = 1 /* Sublimit */;
+ }
+ await queue.wait({ signal: requestData.signal });
+ if (queueType === 0 /* Standard */) {
+ if (this.#sublimitedQueue && hasSublimit(routeId.bucketRoute, requestData.body, options.method)) {
+ queue = this.#sublimitedQueue;
+ const wait = queue.wait();
+ this.#asyncQueue.shift();
+ await wait;
+ } else if (this.#sublimitPromise) {
+ await this.#sublimitPromise.promise;
+ }
+ }
+ try {
+ return await this.runRequest(routeId, url, options, requestData);
+ } finally {
+ queue.shift();
+ if (this.#shiftSublimit) {
+ this.#shiftSublimit = false;
+ this.#sublimitedQueue?.shift();
+ }
+ if (this.#sublimitedQueue?.remaining === 0) {
+ this.#sublimitPromise?.resolve();
+ this.#sublimitedQueue = null;
+ }
+ }
+ }
+ /**
+ * The method that actually makes the request to the api, and updates info about the bucket accordingly
+ *
+ * @param routeId - The generalized api route with literal ids for major parameters
+ * @param url - The fully resolved url to make the request to
+ * @param options - The fetch options needed to make the request
+ * @param requestData - Extra data from the user's request needed for errors and additional processing
+ * @param retries - The number of retries this request has already attempted (recursion)
+ */
+ async runRequest(routeId, url, options, requestData, retries = 0) {
+ while (this.limited) {
+ const isGlobal = this.globalLimited;
+ let limit2;
+ let timeout;
+ let delay;
+ if (isGlobal) {
+ limit2 = this.manager.options.globalRequestsPerSecond;
+ timeout = this.manager.globalReset + this.manager.options.offset - Date.now();
+ if (!this.manager.globalDelay) {
+ this.manager.globalDelay = this.globalDelayFor(timeout);
+ }
+ delay = this.manager.globalDelay;
+ } else {
+ limit2 = this.limit;
+ timeout = this.timeToReset;
+ delay = sleep(timeout);
+ }
+ const rateLimitData = {
+ timeToReset: timeout,
+ limit: limit2,
+ method: options.method ?? "get",
+ hash: this.hash,
+ url,
+ route: routeId.bucketRoute,
+ majorParameter: this.majorParameter,
+ global: isGlobal
+ };
+ this.manager.emit("rateLimited" /* RateLimited */, rateLimitData);
+ await onRateLimit(this.manager, rateLimitData);
+ if (isGlobal) {
+ this.debug(`Global rate limit hit, blocking all requests for ${timeout}ms`);
+ } else {
+ this.debug(`Waiting ${timeout}ms for rate limit to pass`);
+ }
+ await delay;
+ }
+ if (!this.manager.globalReset || this.manager.globalReset < Date.now()) {
+ this.manager.globalReset = Date.now() + 1e3;
+ this.manager.globalRemaining = this.manager.options.globalRequestsPerSecond;
+ }
+ this.manager.globalRemaining--;
+ const method = options.method ?? "get";
+ const res = await makeNetworkRequest(this.manager, routeId, url, options, requestData, retries);
+ if (res === null) {
+ return this.runRequest(routeId, url, options, requestData, ++retries);
+ }
+ const status = res.status;
+ let retryAfter = 0;
+ const limit = res.headers.get("X-RateLimit-Limit");
+ const remaining = res.headers.get("X-RateLimit-Remaining");
+ const reset = res.headers.get("X-RateLimit-Reset-After");
+ const hash = res.headers.get("X-RateLimit-Bucket");
+ const retry = res.headers.get("Retry-After");
+ this.limit = limit ? Number(limit) : Number.POSITIVE_INFINITY;
+ this.remaining = remaining ? Number(remaining) : 1;
+ this.reset = reset ? Number(reset) * 1e3 + Date.now() + this.manager.options.offset : Date.now();
+ if (retry)
+ retryAfter = Number(retry) * 1e3 + this.manager.options.offset;
+ if (hash && hash !== this.hash) {
+ this.debug(["Received bucket hash update", ` Old Hash : ${this.hash}`, ` New Hash : ${hash}`].join("\n"));
+ this.manager.hashes.set(`${method}:${routeId.bucketRoute}`, { value: hash, lastAccess: Date.now() });
+ } else if (hash) {
+ const hashData = this.manager.hashes.get(`${method}:${routeId.bucketRoute}`);
+ if (hashData) {
+ hashData.lastAccess = Date.now();
+ }
+ }
+ let sublimitTimeout = null;
+ if (retryAfter > 0) {
+ if (res.headers.has("X-RateLimit-Global")) {
+ this.manager.globalRemaining = 0;
+ this.manager.globalReset = Date.now() + retryAfter;
+ } else if (!this.localLimited) {
+ sublimitTimeout = retryAfter;
+ }
+ }
+ if (status === 401 || status === 403 || status === 429) {
+ incrementInvalidCount(this.manager);
+ }
+ if (res.ok) {
+ return res;
+ } else if (status === 429) {
+ const isGlobal = this.globalLimited;
+ let limit2;
+ let timeout;
+ if (isGlobal) {
+ limit2 = this.manager.options.globalRequestsPerSecond;
+ timeout = this.manager.globalReset + this.manager.options.offset - Date.now();
+ } else {
+ limit2 = this.limit;
+ timeout = this.timeToReset;
+ }
+ await onRateLimit(this.manager, {
+ timeToReset: timeout,
+ limit: limit2,
+ method,
+ hash: this.hash,
+ url,
+ route: routeId.bucketRoute,
+ majorParameter: this.majorParameter,
+ global: isGlobal
+ });
+ this.debug(
+ [
+ "Encountered unexpected 429 rate limit",
+ ` Global : ${isGlobal.toString()}`,
+ ` Method : ${method}`,
+ ` URL : ${url}`,
+ ` Bucket : ${routeId.bucketRoute}`,
+ ` Major parameter: ${routeId.majorParameter}`,
+ ` Hash : ${this.hash}`,
+ ` Limit : ${limit2}`,
+ ` Retry After : ${retryAfter}ms`,
+ ` Sublimit : ${sublimitTimeout ? `${sublimitTimeout}ms` : "None"}`
+ ].join("\n")
+ );
+ if (sublimitTimeout) {
+ const firstSublimit = !this.#sublimitedQueue;
+ if (firstSublimit) {
+ this.#sublimitedQueue = new import_async_queue.AsyncQueue();
+ void this.#sublimitedQueue.wait();
+ this.#asyncQueue.shift();
+ }
+ this.#sublimitPromise?.resolve();
+ this.#sublimitPromise = null;
+ await sleep(sublimitTimeout);
+ let resolve;
+ const promise = new Promise((res2) => resolve = res2);
+ this.#sublimitPromise = { promise, resolve };
+ if (firstSublimit) {
+ await this.#asyncQueue.wait();
+ this.#shiftSublimit = true;
+ }
+ }
+ return this.runRequest(routeId, url, options, requestData, retries);
+ } else {
+ const handled = await handleErrors(this.manager, res, method, url, requestData, retries);
+ if (handled === null) {
+ return this.runRequest(routeId, url, options, requestData, ++retries);
+ }
+ return handled;
+ }
+ }
+};
+
+// src/lib/REST.ts
+var REST = class _REST extends import_async_event_emitter.AsyncEventEmitter {
+ static {
+ __name(this, "REST");
+ }
+ /**
+ * The {@link https://undici.nodejs.org/#/docs/api/Agent | Agent} for all requests
+ * performed by this manager.
+ */
+ agent = null;
+ cdn;
+ /**
+ * The number of requests remaining in the global bucket
+ */
+ globalRemaining;
+ /**
+ * The promise used to wait out the global rate limit
+ */
+ globalDelay = null;
+ /**
+ * The timestamp at which the global bucket resets
+ */
+ globalReset = -1;
+ /**
+ * API bucket hashes that are cached from provided routes
+ */
+ hashes = new import_collection.Collection();
+ /**
+ * Request handlers created from the bucket hash and the major parameters
+ */
+ handlers = new import_collection.Collection();
+ #token = null;
+ hashTimer;
+ handlerTimer;
+ options;
+ constructor(options = {}) {
+ super();
+ this.cdn = new CDN(options.cdn ?? DefaultRestOptions.cdn);
+ this.options = { ...DefaultRestOptions, ...options };
+ this.options.offset = Math.max(0, this.options.offset);
+ this.globalRemaining = Math.max(1, this.options.globalRequestsPerSecond);
+ this.agent = options.agent ?? null;
+ this.setupSweepers();
+ }
+ setupSweepers() {
+ const validateMaxInterval = /* @__PURE__ */ __name((interval) => {
+ if (interval > 144e5) {
+ throw new Error("Cannot set an interval greater than 4 hours");
+ }
+ }, "validateMaxInterval");
+ if (this.options.hashSweepInterval !== 0 && this.options.hashSweepInterval !== Number.POSITIVE_INFINITY) {
+ validateMaxInterval(this.options.hashSweepInterval);
+ this.hashTimer = setInterval(() => {
+ const sweptHashes = new import_collection.Collection();
+ const currentDate = Date.now();
+ this.hashes.sweep((val, key) => {
+ if (val.lastAccess === -1)
+ return false;
+ const shouldSweep = Math.floor(currentDate - val.lastAccess) > this.options.hashLifetime;
+ if (shouldSweep) {
+ sweptHashes.set(key, val);
+ this.emit("restDebug" /* Debug */, `Hash ${val.value} for ${key} swept due to lifetime being exceeded`);
+ }
+ return shouldSweep;
+ });
+ this.emit("hashSweep" /* HashSweep */, sweptHashes);
+ }, this.options.hashSweepInterval);
+ this.hashTimer.unref?.();
+ }
+ if (this.options.handlerSweepInterval !== 0 && this.options.handlerSweepInterval !== Number.POSITIVE_INFINITY) {
+ validateMaxInterval(this.options.handlerSweepInterval);
+ this.handlerTimer = setInterval(() => {
+ const sweptHandlers = new import_collection.Collection();
+ this.handlers.sweep((val, key) => {
+ const { inactive } = val;
+ if (inactive) {
+ sweptHandlers.set(key, val);
+ this.emit("restDebug" /* Debug */, `Handler ${val.id} for ${key} swept due to being inactive`);
+ }
+ return inactive;
+ });
+ this.emit("handlerSweep" /* HandlerSweep */, sweptHandlers);
+ }, this.options.handlerSweepInterval);
+ this.handlerTimer.unref?.();
+ }
+ }
+ /**
+ * Runs a get request from the api
+ *
+ * @param fullRoute - The full route to query
+ * @param options - Optional request options
+ */
+ async get(fullRoute, options = {}) {
+ return this.request({ ...options, fullRoute, method: "GET" /* Get */ });
+ }
+ /**
+ * Runs a delete request from the api
+ *
+ * @param fullRoute - The full route to query
+ * @param options - Optional request options
+ */
+ async delete(fullRoute, options = {}) {
+ return this.request({ ...options, fullRoute, method: "DELETE" /* Delete */ });
+ }
+ /**
+ * Runs a post request from the api
+ *
+ * @param fullRoute - The full route to query
+ * @param options - Optional request options
+ */
+ async post(fullRoute, options = {}) {
+ return this.request({ ...options, fullRoute, method: "POST" /* Post */ });
+ }
+ /**
+ * Runs a put request from the api
+ *
+ * @param fullRoute - The full route to query
+ * @param options - Optional request options
+ */
+ async put(fullRoute, options = {}) {
+ return this.request({ ...options, fullRoute, method: "PUT" /* Put */ });
+ }
+ /**
+ * Runs a patch request from the api
+ *
+ * @param fullRoute - The full route to query
+ * @param options - Optional request options
+ */
+ async patch(fullRoute, options = {}) {
+ return this.request({ ...options, fullRoute, method: "PATCH" /* Patch */ });
+ }
+ /**
+ * Runs a request from the api
+ *
+ * @param options - Request options
+ */
+ async request(options) {
+ const response = await this.queueRequest(options);
+ return parseResponse(response);
+ }
+ /**
+ * Sets the default agent to use for requests performed by this manager
+ *
+ * @param agent - The agent to use
+ */
+ setAgent(agent) {
+ this.agent = agent;
+ return this;
+ }
+ /**
+ * Sets the authorization token that should be used for requests
+ *
+ * @param token - The authorization token to use
+ */
+ setToken(token) {
+ this.#token = token;
+ return this;
+ }
+ /**
+ * Queues a request to be sent
+ *
+ * @param request - All the information needed to make a request
+ * @returns The response from the api request
+ */
+ async queueRequest(request) {
+ const routeId = _REST.generateRouteData(request.fullRoute, request.method);
+ const hash = this.hashes.get(`${request.method}:${routeId.bucketRoute}`) ?? {
+ value: `Global(${request.method}:${routeId.bucketRoute})`,
+ lastAccess: -1
+ };
+ const handler = this.handlers.get(`${hash.value}:${routeId.majorParameter}`) ?? this.createHandler(hash.value, routeId.majorParameter);
+ const { url, fetchOptions } = await this.resolveRequest(request);
+ return handler.queueRequest(routeId, url, fetchOptions, {
+ body: request.body,
+ files: request.files,
+ auth: request.auth !== false,
+ signal: request.signal
+ });
+ }
+ /**
+ * Creates a new rate limit handler from a hash, based on the hash and the major parameter
+ *
+ * @param hash - The hash for the route
+ * @param majorParameter - The major parameter for this handler
+ * @internal
+ */
+ createHandler(hash, majorParameter) {
+ const queue = majorParameter === BurstHandlerMajorIdKey ? new BurstHandler(this, hash, majorParameter) : new SequentialHandler(this, hash, majorParameter);
+ this.handlers.set(queue.id, queue);
+ return queue;
+ }
+ /**
+ * Formats the request data to a usable format for fetch
+ *
+ * @param request - The request data
+ */
+ async resolveRequest(request) {
+ const { options } = this;
+ let query = "";
+ if (request.query) {
+ const resolvedQuery = request.query.toString();
+ if (resolvedQuery !== "") {
+ query = `?${resolvedQuery}`;
+ }
+ }
+ const headers = {
+ ...this.options.headers,
+ "User-Agent": `${DefaultUserAgent} ${options.userAgentAppendix}`.trim()
+ };
+ if (request.auth !== false) {
+ if (!this.#token) {
+ throw new Error("Expected token to be set for this request, but none was present");
+ }
+ headers.Authorization = `${request.authPrefix ?? this.options.authPrefix} ${this.#token}`;
+ }
+ if (request.reason?.length) {
+ headers["X-Audit-Log-Reason"] = encodeURIComponent(request.reason);
+ }
+ const url = `${options.api}${request.versioned === false ? "" : `/v${options.version}`}${request.fullRoute}${query}`;
+ let finalBody;
+ let additionalHeaders = {};
+ if (request.files?.length) {
+ const formData = new FormData();
+ for (const [index, file] of request.files.entries()) {
+ const fileKey = file.key ?? `files[${index}]`;
+ if (isBufferLike(file.data)) {
+ let contentType = file.contentType;
+ if (!contentType) {
+ const [parsedType] = (0, import_magic_bytes.filetypeinfo)(file.data);
+ if (parsedType) {
+ contentType = OverwrittenMimeTypes[parsedType.mime] ?? parsedType.mime ?? "application/octet-stream";
+ }
+ }
+ formData.append(fileKey, new Blob([file.data], { type: contentType }), file.name);
+ } else {
+ formData.append(fileKey, new Blob([`${file.data}`], { type: file.contentType }), file.name);
+ }
+ }
+ if (request.body != null) {
+ if (request.appendToFormData) {
+ for (const [key, value] of Object.entries(request.body)) {
+ formData.append(key, value);
+ }
+ } else {
+ formData.append("payload_json", JSON.stringify(request.body));
+ }
+ }
+ finalBody = formData;
+ } else if (request.body != null) {
+ if (request.passThroughBody) {
+ finalBody = request.body;
+ } else {
+ finalBody = JSON.stringify(request.body);
+ additionalHeaders = { "Content-Type": "application/json" };
+ }
+ }
+ const method = request.method.toUpperCase();
+ const fetchOptions = {
+ // Set body to null on get / head requests. This does not follow fetch spec (likely because it causes subtle bugs) but is aligned with what request was doing
+ body: ["GET", "HEAD"].includes(method) ? null : finalBody,
+ headers: { ...request.headers, ...additionalHeaders, ...headers },
+ method,
+ // Prioritize setting an agent per request, use the agent for this instance otherwise.
+ dispatcher: request.dispatcher ?? this.agent ?? void 0
+ };
+ return { url, fetchOptions };
+ }
+ /**
+ * Stops the hash sweeping interval
+ */
+ clearHashSweeper() {
+ clearInterval(this.hashTimer);
+ }
+ /**
+ * Stops the request handler sweeping interval
+ */
+ clearHandlerSweeper() {
+ clearInterval(this.handlerTimer);
+ }
+ /**
+ * Generates route data for an endpoint:method
+ *
+ * @param endpoint - The raw endpoint to generalize
+ * @param method - The HTTP method this endpoint is called without
+ * @internal
+ */
+ static generateRouteData(endpoint, method) {
+ if (endpoint.startsWith("/interactions/") && endpoint.endsWith("/callback")) {
+ return {
+ majorParameter: BurstHandlerMajorIdKey,
+ bucketRoute: "/interactions/:id/:token/callback",
+ original: endpoint
+ };
+ }
+ const majorIdMatch = /^\/(?:channels|guilds|webhooks)\/(\d{17,19})/.exec(endpoint);
+ const majorId = majorIdMatch?.[1] ?? "global";
+ const baseRoute = endpoint.replaceAll(/\d{17,19}/g, ":id").replace(/\/reactions\/(.*)/, "/reactions/:reaction");
+ let exceptions = "";
+ if (method === "DELETE" /* Delete */ && baseRoute === "/channels/:id/messages/:id") {
+ const id = /\d{17,19}$/.exec(endpoint)[0];
+ const timestamp = import_snowflake.DiscordSnowflake.timestampFrom(id);
+ if (Date.now() - timestamp > 1e3 * 60 * 60 * 24 * 14) {
+ exceptions += "/Delete Old Message";
+ }
+ }
+ return {
+ majorParameter: majorId,
+ bucketRoute: baseRoute + exceptions,
+ original: endpoint
+ };
+ }
+};
+
+// src/shared.ts
+var version = "2.0.1";
+
+// src/web.ts
+setDefaultStrategy(fetch);
+// Annotate the CommonJS export names for ESM import in node:
+0 && (module.exports = {
+ ALLOWED_EXTENSIONS,
+ ALLOWED_SIZES,
+ ALLOWED_STICKER_EXTENSIONS,
+ BurstHandlerMajorIdKey,
+ CDN,
+ DefaultRestOptions,
+ DefaultUserAgent,
+ DefaultUserAgentAppendix,
+ DiscordAPIError,
+ HTTPError,
+ OverwrittenMimeTypes,
+ REST,
+ RESTEvents,
+ RateLimitError,
+ RequestMethod,
+ calculateUserDefaultAvatarIndex,
+ makeURLSearchParams,
+ parseResponse,
+ version
+});
+//# sourceMappingURL=web.js.map \ No newline at end of file