data-custom-id
Version:
Hold data in Discord's Interaction Custom IDs.
290 lines (289 loc) • 11.9 kB
JavaScript
"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;