UNPKG

data-custom-id

Version:

Hold data in Discord's Interaction Custom IDs.

290 lines (289 loc) 11.9 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; exports.__esModule = true; exports.DataCustomIdLengthError = exports.defaultEncodeOptions = void 0; var qs_1 = require("qs"); exports.defaultEncodeOptions = { skipFalsyValues: true, convertTrueToOne: false }; /** * DataCustomIdLengthError is thrown by `DataCustomId.toString()` when the * length of the resulting string is greater than Discord's maximum length of 100 characters. */ var DataCustomIdLengthError = /** @class */ (function (_super) { __extends(DataCustomIdLengthError, _super); function DataCustomIdLengthError(message) { var _this = _super.call(this, message) || this; _this.name = "DataCustomIdLengthError"; return _this; } return DataCustomIdLengthError; }(Error)); exports.DataCustomIdLengthError = DataCustomIdLengthError; /** * DataCustomId lets you store data inside Discord's Custom ID system. * This is useful for storing state within multi-interaction flows. * * Data is stored using a system similar to URL query strings appended to * the provided custom ID. * * It's important to note that you **can not** guarantee data integrity. * Custom IDs are sent by the client, and can be modified by users. * **DO NOT INCLUDE SENSITIVE DATA IN CUSTOM IDS!**. * * Once instances are created, the raw Custom ID (`customId.rawId`) is immutable. * If you want to keep the current state and change the raw ID, you can * create a new instance with the new raw ID, then call `newCustomId.copyFieldsFrom(oldCustomId)`. * * @author iamtheyammer */ var DataCustomId = /** @class */ (function () { /** * Creates a new DataCustomId instance with a given Custom ID which * may or may not contain data. * * Fields should be strings, optionally separated by `/` characters. * This will allow you to use `customId.pathParts`. * * If a custom ID is provided, it will be parsed, * allowing you to use methods like `getFields` and `addFields`. * * Custom IDs with**out** data must not have `?` or `&` characters. * Those characters are used to store data. * * Discord limits Custom IDs to 100 characters, but this limit is not enforced * until you call `toString()`. * * @param id {string} The raw custom ID with or without fields appended. */ function DataCustomId(id) { if (id === void 0) { id = ""; } this.fields = {}; if (id.includes("?")) { this.rawId = id.slice(0, id.indexOf("?")); this.fields = (0, qs_1.parse)(id.slice(id.indexOf("?") + 1), { // because we encode with commas to save space comma: true }); } else { this.rawId = id; } this.pathParts = this.rawId.split("/"); return this; } /** * Adds a field to the custom ID. * * Keep names and values short to avoid hitting Discord's limit, which is not enforced * until you call `toString()`. * * @param key The key (name) of the field. * @param value Its value: a string, array of strings, or boolean. */ DataCustomId.prototype.addField = function (key, value) { // @ts-ignore - qs will handle serialization of "incompatible" types this.fields[key] = value; return this; }; /** * Add multiple fields to the custom ID. * Overwrites existing fields with the same name. * @param fields Fields to add to the custom ID. * @returns The current instance for chaining. */ DataCustomId.prototype.addFields = function (fields) { this.fields = __assign(__assign({}, this.fields), fields); return this; }; /** * Removes a field from the custom ID. * @param key The key (name) of the field to remove. * @returns The current instance for chaining. */ DataCustomId.prototype.removeField = function (key) { delete this.fields[key]; return this; }; /** * Copies all fields from another custom ID to this custom ID. * Useful for continuing state from one custom ID to another. * * Overwrites existing fields with the same name. * @param other DataCustomId instance to copy fields from. * @returns The current instance for chaining. */ DataCustomId.prototype.copyFieldsFrom = function (other) { this.fields = __assign(__assign({}, this.fields), other.fields); return this; }; /** * Returns all the Custom ID's fields. */ DataCustomId.prototype.getFields = function () { return this.fields; }; /** * Returns the value of a field, coalesced to a string. * * @param key The key (name) of the field. * @returns A string with the value, or `""` if the field does not exist. */ DataCustomId.prototype.getStringField = function (key) { return typeof this.fields[key] === "string" ? this.fields[key] : ""; }; /** * Returns the value of a field, coalesced to a string array. * @param key The key (name) of the field. * @returns An array of strings, or an empty array if the field does not exist. */ DataCustomId.prototype.getStringArrayField = function (key) { if (typeof this.fields[key] === "undefined") { return []; } return Array.isArray(this.fields[key]) ? this.fields[key] : [this.fields[key]]; }; /** * Returns the value of a field, coalesced to a number or float. * * Floats can only have base 10. * * @param key The key (name) of the field. * @param float Whether the number should be parsed with `parseFloat()`. Doesn't support bases. Default false. * @param base The base to parse the number in. Numbers only (no floats). Default 10. * @returns A number or `NaN` if the field doesn't exist or isn't a number. */ DataCustomId.prototype.getNumericField = function (key, float, base) { if (float === void 0) { float = false; } if (base === void 0) { base = 10; } return typeof this.fields[key] === "string" ? (float ? parseFloat : parseInt)(this.fields[key], base) // @ts-ignore - possible to have a number if someone added it and queried it without serialization : typeof this.fields[key] === "number" ? this.fields[key] : NaN; }; /** * Returns the value of a field, coalesced to a number array. * If the value contains non-numbers, they will be in the return value as NaN. * Remember to use isNaN() to check for NaN values, not `value === NaN` (that _does not_ work!). * * @param key The key (name) of the field. * @param float Whether the numbers should be parsed with `parseFloat()`. Doesn't support bases. Default false. * @param base The base to parse the number in. Numbers only (no floats). Default 10. * @returns A number array, or an empty array if the field does not exist. */ DataCustomId.prototype.getNumericArrayField = function (key, float, base) { if (float === void 0) { float = false; } if (base === void 0) { base = 10; } if (typeof this.fields[key] === "undefined") { return []; } return this.getStringArrayField(key).map(function (x) { return parseInt(x, 10); }); }; /** * Returns the value of a field, coalesced to a boolean. * If the value of the field is not `true` or `1`, it will be `false`. * * @param key The key (name) of the field. * @returns A boolean with the value, or `false` if the field does not exist. */ DataCustomId.prototype.getBooleanField = function (key) { return (this.fields[key] === "true" || this.fields[key] === "1" || this.fields[key] === true || // @ts-ignore - it's possible for someone to add this field pre-encoding. this.fields[key] === 1); }; /** * Compresses fields based on the passed-in compression options. * @param fields The fields to compress. * @param options Compression options. * @returns A new object with compressed fields. * @private */ DataCustomId.compressFields = function (fields, options) { // if every compression option is disabled, we can just return the fields if (Object.values(options).every(function (v) { return v === false; })) { return fields; } var compressedFields = {}; for (var key in fields) { var value = fields[key]; if (options.skipFalsyValues) { if (!value) { continue; } } if (options.convertTrueToOne) { if (value === true || value === "true") { compressedFields[key] = "1"; continue; } } compressedFields[key] = value; } return compressedFields; }; /** * Returns the raw ID string with all fields encoded. * * A Custom ID `/ban` with fields `{ "reason": "spam"}` should return `/ban?reason=spam`. * `true` and falsy values may or may not be encoded depending on selected options. * * @throws {DataCustomIdLengthError} if the serialized string is over Discord's 100-character limit. * @returns The Custom ID value with all fields encoded. Use this as the value for the Custom ID field in a Discord API request. */ DataCustomId.prototype.toString = function (compressionOptions) { if (compressionOptions === void 0) { compressionOptions = exports.defaultEncodeOptions; } var compressedFields = DataCustomId.compressFields(this.fields, compressionOptions); var encodedFields = (0, qs_1.stringify)(compressedFields, { // add ? addQueryPrefix: true, // rather than ?key[]=value&key[]=value2 arrayFormat: "comma", // save space skipNulls: true, // not necessary to URL encode encode: false }); var finalId = encodedFields.length > 1 ? "".concat(this.rawId).concat(encodedFields) : this.rawId; if (finalId.length > 100) { throw new DataCustomIdLengthError("DataCustomId is over Discord's limit of 100 characters: ".concat(finalId)); } return finalId; }; return DataCustomId; }()); exports["default"] = DataCustomId; module.exports = DataCustomId; module.exports.DataCustomIdLengthError = DataCustomIdLengthError; module.exports.defaultEncodeOptions = exports.defaultEncodeOptions;