diff --git a/src/util/BitField.ts b/src/util/BitField.ts
new file mode 100644
index 00000000..01349a0b
--- /dev/null
+++ b/src/util/BitField.ts
@@ -0,0 +1,146 @@
+"use strict";
+
+// https://github.com/discordjs/discord.js/blob/master/src/util/BitField.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+
+export type BitFieldResolvable = number | BitField | string | BitFieldResolvable[];
+
+/**
+ * Data structure that makes it easy to interact with a bitfield.
+ */
+export class BitField {
+ public bitfield: number;
+
+ /**
+ * Numeric bitfield flags.
+ * <info>Defined in extension classes</info>
+ */
+ public static FLAGS: Record<string, number>;
+ /**
+ */
+ constructor(bits: BitFieldResolvable = 0) {
+ /**
+ * Bitfield of the packed bits
+ * @type {number}
+ */
+ this.bitfield = BitField.resolve(bits);
+ }
+
+ /**
+ * Checks whether the bitfield has a bit, or any of multiple bits.
+ */
+ any(bit: BitFieldResolvable): boolean {
+ return (this.bitfield & BitField.resolve(bit)) !== 0;
+ }
+
+ /**
+ * Checks if this bitfield equals another
+ */
+ equals(bit: BitFieldResolvable): boolean {
+ return this.bitfield === BitField.resolve(bit);
+ }
+
+ /**
+ * Checks whether the bitfield has a bit, or multiple bits.
+ */
+ has(bit: BitFieldResolvable): boolean {
+ if (Array.isArray(bit)) return bit.every((p) => this.has(p));
+ bit = BitField.resolve(bit);
+ return (this.bitfield & bit) === bit;
+ }
+
+ /**
+ * Gets all given bits that are missing from the bitfield.
+ */
+ missing(bits: BitFieldResolvable) {
+ if (!Array.isArray(bits)) bits = new BitField(bits).toArray();
+ return bits.filter((p) => !this.has(p));
+ }
+
+ /**
+ * Freezes these bits, making them immutable.
+ */
+ freeze(): Readonly<BitField> {
+ return Object.freeze(this);
+ }
+
+ /**
+ * Adds bits to these ones.
+ * @param {...BitFieldResolvable} [bits] Bits to add
+ * @returns {BitField} These bits or new BitField if the instance is frozen.
+ */
+ add(...bits: BitFieldResolvable[]): BitField {
+ let total = 0;
+ for (const bit of bits) {
+ total |= BitField.resolve(bit);
+ }
+ if (Object.isFrozen(this)) return new BitField(this.bitfield | total);
+ this.bitfield |= total;
+ return this;
+ }
+
+ /**
+ * Removes bits from these.
+ * @param {...BitFieldResolvable} [bits] Bits to remove
+ */
+ remove(...bits: BitFieldResolvable[]) {
+ let total = 0;
+ for (const bit of bits) {
+ total |= BitField.resolve(bit);
+ }
+ if (Object.isFrozen(this)) return new BitField(this.bitfield & ~total);
+ this.bitfield &= ~total;
+ return this;
+ }
+
+ /**
+ * Gets an object mapping field names to a {@link boolean} indicating whether the
+ * bit is available.
+ * @param {...*} hasParams Additional parameters for the has method, if any
+ */
+ serialize() {
+ const serialized: Record<string, boolean> = {};
+ for (const [flag, bit] of Object.entries(BitField.FLAGS)) serialized[flag] = this.has(bit);
+ return serialized;
+ }
+
+ /**
+ * Gets an {@link Array} of bitfield names based on the bits available.
+ */
+ toArray(): string[] {
+ return Object.keys(BitField.FLAGS).filter((bit) => this.has(bit));
+ }
+
+ toJSON() {
+ return this.bitfield;
+ }
+
+ valueOf() {
+ return this.bitfield;
+ }
+
+ *[Symbol.iterator]() {
+ yield* this.toArray();
+ }
+
+ /**
+ * Data that can be resolved to give a bitfield. This can be:
+ * * A bit number (this can be a number literal or a value taken from {@link BitField.FLAGS})
+ * * An instance of BitField
+ * * An Array of BitFieldResolvable
+ * @typedef {number|BitField|BitFieldResolvable[]} BitFieldResolvable
+ */
+
+ /**
+ * Resolves bitfields to their numeric form.
+ * @param {BitFieldResolvable} [bit=0] - bit(s) to resolve
+ * @returns {number}
+ */
+ static resolve(bit: BitFieldResolvable = 0): number {
+ if (typeof bit === "number" && bit >= 0) return bit;
+ if (bit instanceof BitField) return bit.bitfield;
+ if (Array.isArray(bit)) return bit.map((p) => this.resolve(p)).reduce((prev, p) => prev | p, 0);
+ if (typeof bit === "string" && typeof this.FLAGS[bit] !== "undefined") return this.FLAGS[bit];
+ throw new RangeError("BITFIELD_INVALID: " + bit);
+ }
+}
diff --git a/src/util/Config.ts b/src/util/Config.ts
new file mode 100644
index 00000000..c948d0eb
--- /dev/null
+++ b/src/util/Config.ts
@@ -0,0 +1,25 @@
+import "missing-native-js-functions";
+import db from "./Database";
+import { DefaultOptions } from "./Constants";
+import { ProviderCache } from "lambert-db";
+var Config: ProviderCache;
+
+async function init() {
+ Config = db.data.config.cache();
+ await Config.init();
+ await Config.set(DefaultOptions.merge(Config.cache));
+}
+
+function get() {
+ return <DefaultOptions>Config.get();
+}
+
+function set(val: any) {
+ return Config.set(val);
+}
+
+export default {
+ init,
+ get: get,
+ set: set,
+};
diff --git a/src/util/Constants.ts b/src/util/Constants.ts
new file mode 100644
index 00000000..ee2684b8
--- /dev/null
+++ b/src/util/Constants.ts
@@ -0,0 +1,679 @@
+import crypto from "crypto";
+import { VerifyOptions } from "jsonwebtoken";
+
+export interface DefaultOptions {
+ user: {
+ maxGuilds: number;
+ maxUsername: number;
+ maxFriends: number;
+ };
+ guild: {
+ maxRoles: number;
+ maxMembers: number;
+ maxChannels: number;
+ maxChannelsInCategory: number;
+ hideOfflineMember: number;
+ };
+ message: {
+ characters: number;
+ ttsCharacters: number;
+ maxReactions: number;
+ maxAttachmentSize: number;
+ };
+ channel: {
+ maxPins: number;
+ maxTopic: number;
+ };
+ server: {
+ jwtSecret: string;
+ ipRateLimit: {
+ enabled: boolean;
+ count: number;
+ timespan: number;
+ };
+ forwadedFor: false | string;
+ };
+}
+
+export const DefaultOptions: DefaultOptions = {
+ user: {
+ maxGuilds: 100,
+ maxUsername: 32,
+ maxFriends: 1000,
+ },
+ guild: {
+ maxRoles: 250,
+ maxMembers: 250000,
+ maxChannels: 500,
+ maxChannelsInCategory: 50,
+ hideOfflineMember: 1000,
+ },
+ message: {
+ characters: 2000,
+ ttsCharacters: 200,
+ maxReactions: 20,
+ maxAttachmentSize: 8388608,
+ },
+ channel: {
+ maxPins: 50,
+ maxTopic: 1024,
+ },
+ server: {
+ jwtSecret: crypto.randomBytes(256).toString("base64"),
+ ipRateLimit: {
+ enabled: true,
+ count: 1000,
+ timespan: 1000 * 60 * 10,
+ },
+ forwadedFor: false,
+ // forwadedFor: "X-Forwarded-For" // nginx/reverse proxy
+ // forwadedFor: "CF-Connecting-IP" // cloudflare:
+ },
+};
+
+export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
+
+export const WSCodes = {
+ 1000: "WS_CLOSE_REQUESTED",
+ 4004: "TOKEN_INVALID",
+ 4010: "SHARDING_INVALID",
+ 4011: "SHARDING_REQUIRED",
+ 4013: "INVALID_INTENTS",
+ 4014: "DISALLOWED_INTENTS",
+};
+
+const AllowedImageFormats = ["webp", "png", "jpg", "jpeg", "gif"];
+
+const AllowedImageSizes = Array.from({ length: 9 }, (e, i) => 2 ** (i + 4));
+
+function makeImageUrl(root: string, { format = "webp", size = 512 } = {}) {
+ if (format && !AllowedImageFormats.includes(format)) throw new Error("IMAGE_FORMAT: " + format);
+ if (size && !AllowedImageSizes.includes(size)) throw new RangeError("IMAGE_SIZE: " + size);
+ return `${root}.${format}${size ? `?size=${size}` : ""}`;
+}
+/**
+ * Options for Image URLs.
+ * @typedef {Object} ImageURLOptions
+ * @property {string} [format] One of `webp`, `png`, `jpg`, `jpeg`, `gif`. If no format is provided,
+ * defaults to `webp`.
+ * @property {boolean} [dynamic] If true, the format will dynamically change to `gif` for
+ * animated avatars; the default is false.
+ * @property {number} [size] One of `16`, `32`, `64`, `128`, `256`, `512`, `1024`, `2048`, `4096`
+ */
+
+export const Endpoints = {
+ CDN(root: string) {
+ return {
+ Emoji: (emojiID: string, format = "png") => `${root}/emojis/${emojiID}.${format}`,
+ Asset: (name: string) => `${root}/assets/${name}`,
+ DefaultAvatar: (discriminator: string) => `${root}/embed/avatars/${discriminator}.png`,
+ Avatar: (userID: string, hash: string, format = "webp", size: number, dynamic = false) => {
+ if (dynamic) format = hash.startsWith("a_") ? "gif" : format;
+ return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size });
+ },
+ Banner: (guildID: string, hash: string, format = "webp", size: number) =>
+ makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }),
+ Icon: (guildID: string, hash: string, format = "webp", size: number, dynamic = false) => {
+ if (dynamic) format = hash.startsWith("a_") ? "gif" : format;
+ return makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size });
+ },
+ AppIcon: (
+ clientID: string,
+ hash: string,
+ { format = "webp", size }: { format?: string; size?: number } = {}
+ ) => makeImageUrl(`${root}/app-icons/${clientID}/${hash}`, { size, format }),
+ AppAsset: (
+ clientID: string,
+ hash: string,
+ { format = "webp", size }: { format?: string; size?: number } = {}
+ ) => makeImageUrl(`${root}/app-assets/${clientID}/${hash}`, { size, format }),
+ GDMIcon: (channelID: string, hash: string, format = "webp", size: number) =>
+ makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }),
+ Splash: (guildID: string, hash: string, format = "webp", size: number) =>
+ makeImageUrl(`${root}/splashes/${guildID}/${hash}`, { size, format }),
+ DiscoverySplash: (guildID: string, hash: string, format = "webp", size: number) =>
+ makeImageUrl(`${root}/discovery-splashes/${guildID}/${hash}`, { size, format }),
+ TeamIcon: (
+ teamID: string,
+ hash: string,
+ { format = "webp", size }: { format?: string; size?: number } = {}
+ ) => makeImageUrl(`${root}/team-icons/${teamID}/${hash}`, { size, format }),
+ };
+ },
+ invite: (root: string, code: string) => `${root}/${code}`,
+ botGateway: "/gateway/bot",
+};
+
+/**
+ * The current status of the client. Here are the available statuses:
+ * * READY: 0
+ * * CONNECTING: 1
+ * * RECONNECTING: 2
+ * * IDLE: 3
+ * * NEARLY: 4
+ * * DISCONNECTED: 5
+ * * WAITING_FOR_GUILDS: 6
+ * * IDENTIFYING: 7
+ * * RESUMING: 8
+ * @typedef {number} Status
+ */
+export const Status = {
+ READY: 0,
+ CONNECTING: 1,
+ RECONNECTING: 2,
+ IDLE: 3,
+ NEARLY: 4,
+ DISCONNECTED: 5,
+ WAITING_FOR_GUILDS: 6,
+ IDENTIFYING: 7,
+ RESUMING: 8,
+};
+
+/**
+ * The current status of a voice connection. Here are the available statuses:
+ * * CONNECTED: 0
+ * * CONNECTING: 1
+ * * AUTHENTICATING: 2
+ * * RECONNECTING: 3
+ * * DISCONNECTED: 4
+ * @typedef {number} VoiceStatus
+ */
+export const VoiceStatus = {
+ CONNECTED: 0,
+ CONNECTING: 1,
+ AUTHENTICATING: 2,
+ RECONNECTING: 3,
+ DISCONNECTED: 4,
+};
+
+export const OPCodes = {
+ DISPATCH: 0,
+ HEARTBEAT: 1,
+ IDENTIFY: 2,
+ STATUS_UPDATE: 3,
+ VOICE_STATE_UPDATE: 4,
+ VOICE_GUILD_PING: 5,
+ RESUME: 6,
+ RECONNECT: 7,
+ REQUEST_GUILD_MEMBERS: 8,
+ INVALID_SESSION: 9,
+ HELLO: 10,
+ HEARTBEAT_ACK: 11,
+};
+
+export const VoiceOPCodes = {
+ IDENTIFY: 0,
+ SELECT_PROTOCOL: 1,
+ READY: 2,
+ HEARTBEAT: 3,
+ SESSION_DESCRIPTION: 4,
+ SPEAKING: 5,
+ HELLO: 8,
+ CLIENT_CONNECT: 12,
+ CLIENT_DISCONNECT: 13,
+};
+
+export const Events = {
+ RATE_LIMIT: "rateLimit",
+ CLIENT_READY: "ready",
+ GUILD_CREATE: "guildCreate",
+ GUILD_DELETE: "guildDelete",
+ GUILD_UPDATE: "guildUpdate",
+ GUILD_UNAVAILABLE: "guildUnavailable",
+ GUILD_AVAILABLE: "guildAvailable",
+ GUILD_MEMBER_ADD: "guildMemberAdd",
+ GUILD_MEMBER_REMOVE: "guildMemberRemove",
+ GUILD_MEMBER_UPDATE: "guildMemberUpdate",
+ GUILD_MEMBER_AVAILABLE: "guildMemberAvailable",
+ GUILD_MEMBER_SPEAKING: "guildMemberSpeaking",
+ GUILD_MEMBERS_CHUNK: "guildMembersChunk",
+ GUILD_INTEGRATIONS_UPDATE: "guildIntegrationsUpdate",
+ GUILD_ROLE_CREATE: "roleCreate",
+ GUILD_ROLE_DELETE: "roleDelete",
+ INVITE_CREATE: "inviteCreate",
+ INVITE_DELETE: "inviteDelete",
+ GUILD_ROLE_UPDATE: "roleUpdate",
+ GUILD_EMOJI_CREATE: "emojiCreate",
+ GUILD_EMOJI_DELETE: "emojiDelete",
+ GUILD_EMOJI_UPDATE: "emojiUpdate",
+ GUILD_BAN_ADD: "guildBanAdd",
+ GUILD_BAN_REMOVE: "guildBanRemove",
+ CHANNEL_CREATE: "channelCreate",
+ CHANNEL_DELETE: "channelDelete",
+ CHANNEL_UPDATE: "channelUpdate",
+ CHANNEL_PINS_UPDATE: "channelPinsUpdate",
+ MESSAGE_CREATE: "message",
+ MESSAGE_DELETE: "messageDelete",
+ MESSAGE_UPDATE: "messageUpdate",
+ MESSAGE_BULK_DELETE: "messageDeleteBulk",
+ MESSAGE_REACTION_ADD: "messageReactionAdd",
+ MESSAGE_REACTION_REMOVE: "messageReactionRemove",
+ MESSAGE_REACTION_REMOVE_ALL: "messageReactionRemoveAll",
+ MESSAGE_REACTION_REMOVE_EMOJI: "messageReactionRemoveEmoji",
+ USER_UPDATE: "userUpdate",
+ PRESENCE_UPDATE: "presenceUpdate",
+ VOICE_SERVER_UPDATE: "voiceServerUpdate",
+ VOICE_STATE_UPDATE: "voiceStateUpdate",
+ VOICE_BROADCAST_SUBSCRIBE: "subscribe",
+ VOICE_BROADCAST_UNSUBSCRIBE: "unsubscribe",
+ TYPING_START: "typingStart",
+ TYPING_STOP: "typingStop",
+ WEBHOOKS_UPDATE: "webhookUpdate",
+ ERROR: "error",
+ WARN: "warn",
+ DEBUG: "debug",
+ SHARD_DISCONNECT: "shardDisconnect",
+ SHARD_ERROR: "shardError",
+ SHARD_RECONNECTING: "shardReconnecting",
+ SHARD_READY: "shardReady",
+ SHARD_RESUME: "shardResume",
+ INVALIDATED: "invalidated",
+ RAW: "raw",
+};
+
+export const ShardEvents = {
+ CLOSE: "close",
+ DESTROYED: "destroyed",
+ INVALID_SESSION: "invalidSession",
+ READY: "ready",
+ RESUMED: "resumed",
+ ALL_READY: "allReady",
+};
+
+/**
+ * The type of Structure allowed to be a partial:
+ * * USER
+ * * CHANNEL (only affects DMChannels)
+ * * GUILD_MEMBER
+ * * MESSAGE
+ * * REACTION
+ * <warn>Partials require you to put checks in place when handling data, read the Partials topic listed in the
+ * sidebar for more information.</warn>
+ * @typedef {string} PartialType
+ */
+export const PartialTypes = keyMirror(["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE", "REACTION"]);
+
+/**
+ * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events:
+ * * READY
+ * * RESUMED
+ * * GUILD_CREATE
+ * * GUILD_DELETE
+ * * GUILD_UPDATE
+ * * INVITE_CREATE
+ * * INVITE_DELETE
+ * * GUILD_MEMBER_ADD
+ * * GUILD_MEMBER_REMOVE
+ * * GUILD_MEMBER_UPDATE
+ * * GUILD_MEMBERS_CHUNK
+ * * GUILD_INTEGRATIONS_UPDATE
+ * * GUILD_ROLE_CREATE
+ * * GUILD_ROLE_DELETE
+ * * GUILD_ROLE_UPDATE
+ * * GUILD_BAN_ADD
+ * * GUILD_BAN_REMOVE
+ * * GUILD_EMOJIS_UPDATE
+ * * CHANNEL_CREATE
+ * * CHANNEL_DELETE
+ * * CHANNEL_UPDATE
+ * * CHANNEL_PINS_UPDATE
+ * * MESSAGE_CREATE
+ * * MESSAGE_DELETE
+ * * MESSAGE_UPDATE
+ * * MESSAGE_DELETE_BULK
+ * * MESSAGE_REACTION_ADD
+ * * MESSAGE_REACTION_REMOVE
+ * * MESSAGE_REACTION_REMOVE_ALL
+ * * MESSAGE_REACTION_REMOVE_EMOJI
+ * * USER_UPDATE
+ * * PRESENCE_UPDATE
+ * * TYPING_START
+ * * VOICE_STATE_UPDATE
+ * * VOICE_SERVER_UPDATE
+ * * WEBHOOKS_UPDATE
+ * @typedef {string} WSEventType
+ */
+export const WSEvents = keyMirror([
+ "READY",
+ "RESUMED",
+ "GUILD_CREATE",
+ "GUILD_DELETE",
+ "GUILD_UPDATE",
+ "INVITE_CREATE",
+ "INVITE_DELETE",
+ "GUILD_MEMBER_ADD",
+ "GUILD_MEMBER_REMOVE",
+ "GUILD_MEMBER_UPDATE",
+ "GUILD_MEMBERS_CHUNK",
+ "GUILD_INTEGRATIONS_UPDATE",
+ "GUILD_ROLE_CREATE",
+ "GUILD_ROLE_DELETE",
+ "GUILD_ROLE_UPDATE",
+ "GUILD_BAN_ADD",
+ "GUILD_BAN_REMOVE",
+ "GUILD_EMOJIS_UPDATE",
+ "CHANNEL_CREATE",
+ "CHANNEL_DELETE",
+ "CHANNEL_UPDATE",
+ "CHANNEL_PINS_UPDATE",
+ "MESSAGE_CREATE",
+ "MESSAGE_DELETE",
+ "MESSAGE_UPDATE",
+ "MESSAGE_DELETE_BULK",
+ "MESSAGE_REACTION_ADD",
+ "MESSAGE_REACTION_REMOVE",
+ "MESSAGE_REACTION_REMOVE_ALL",
+ "MESSAGE_REACTION_REMOVE_EMOJI",
+ "USER_UPDATE",
+ "PRESENCE_UPDATE",
+ "TYPING_START",
+ "VOICE_STATE_UPDATE",
+ "VOICE_SERVER_UPDATE",
+ "WEBHOOKS_UPDATE",
+]);
+
+/**
+ * The type of a message, e.g. `DEFAULT`. Here are the available types:
+ * * DEFAULT
+ * * RECIPIENT_ADD
+ * * RECIPIENT_REMOVE
+ * * CALL
+ * * CHANNEL_NAME_CHANGE
+ * * CHANNEL_ICON_CHANGE
+ * * PINS_ADD
+ * * GUILD_MEMBER_JOIN
+ * * USER_PREMIUM_GUILD_SUBSCRIPTION
+ * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1
+ * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2
+ * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3
+ * * CHANNEL_FOLLOW_ADD
+ * * GUILD_DISCOVERY_DISQUALIFIED
+ * * GUILD_DISCOVERY_REQUALIFIED
+ * * REPLY
+ * @typedef {string} MessageType
+ */
+export const MessageTypes = [
+ "DEFAULT",
+ "RECIPIENT_ADD",
+ "RECIPIENT_REMOVE",
+ "CALL",
+ "CHANNEL_NAME_CHANGE",
+ "CHANNEL_ICON_CHANGE",
+ "PINS_ADD",
+ "GUILD_MEMBER_JOIN",
+ "USER_PREMIUM_GUILD_SUBSCRIPTION",
+ "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1",
+ "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2",
+ "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3",
+ "CHANNEL_FOLLOW_ADD",
+ null,
+ "GUILD_DISCOVERY_DISQUALIFIED",
+ "GUILD_DISCOVERY_REQUALIFIED",
+ null,
+ null,
+ null,
+ "REPLY",
+];
+
+/**
+ * The types of messages that are `System`. The available types are `MessageTypes` excluding:
+ * * DEFAULT
+ * * REPLY
+ * @typedef {string} SystemMessageType
+ */
+export const SystemMessageTypes = MessageTypes.filter(
+ (type: string | null) => type && type !== "DEFAULT" && type !== "REPLY"
+);
+
+/**
+ * <info>Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users</info>
+ * The type of an activity of a users presence, e.g. `PLAYING`. Here are the available types:
+ * * PLAYING
+ * * STREAMING
+ * * LISTENING
+ * * WATCHING
+ * * CUSTOM_STATUS
+ * * COMPETING
+ * @typedef {string} ActivityType
+ */
+export const ActivityTypes = ["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM_STATUS", "COMPETING"];
+
+export const ChannelTypes = {
+ TEXT: 0,
+ DM: 1,
+ VOICE: 2,
+ GROUP: 3,
+ CATEGORY: 4,
+ NEWS: 5,
+ STORE: 6,
+};
+
+export const ClientApplicationAssetTypes = {
+ SMALL: 1,
+ BIG: 2,
+};
+
+export const Colors = {
+ DEFAULT: 0x000000,
+ WHITE: 0xffffff,
+ AQUA: 0x1abc9c,
+ GREEN: 0x2ecc71,
+ BLUE: 0x3498db,
+ YELLOW: 0xffff00,
+ PURPLE: 0x9b59b6,
+ LUMINOUS_VIVID_PINK: 0xe91e63,
+ GOLD: 0xf1c40f,
+ ORANGE: 0xe67e22,
+ RED: 0xe74c3c,
+ GREY: 0x95a5a6,
+ NAVY: 0x34495e,
+ DARK_AQUA: 0x11806a,
+ DARK_GREEN: 0x1f8b4c,
+ DARK_BLUE: 0x206694,
+ DARK_PURPLE: 0x71368a,
+ DARK_VIVID_PINK: 0xad1457,
+ DARK_GOLD: 0xc27c0e,
+ DARK_ORANGE: 0xa84300,
+ DARK_RED: 0x992d22,
+ DARK_GREY: 0x979c9f,
+ DARKER_GREY: 0x7f8c8d,
+ LIGHT_GREY: 0xbcc0c0,
+ DARK_NAVY: 0x2c3e50,
+ BLURPLE: 0x7289da,
+ GREYPLE: 0x99aab5,
+ DARK_BUT_NOT_BLACK: 0x2c2f33,
+ NOT_QUITE_BLACK: 0x23272a,
+};
+
+/**
+ * The value set for the explicit content filter levels for a guild:
+ * * DISABLED
+ * * MEMBERS_WITHOUT_ROLES
+ * * ALL_MEMBERS
+ * @typedef {string} ExplicitContentFilterLevel
+ */
+export const ExplicitContentFilterLevels = ["DISABLED", "MEMBERS_WITHOUT_ROLES", "ALL_MEMBERS"];
+
+/**
+ * The value set for the verification levels for a guild:
+ * * NONE
+ * * LOW
+ * * MEDIUM
+ * * HIGH
+ * * VERY_HIGH
+ * @typedef {string} VerificationLevel
+ */
+export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"];
+
+/**
+ * An error encountered while performing an API request. Here are the potential errors:
+ * * UNKNOWN_ACCOUNT
+ * * UNKNOWN_APPLICATION
+ * * UNKNOWN_CHANNEL
+ * * UNKNOWN_GUILD
+ * * UNKNOWN_INTEGRATION
+ * * UNKNOWN_INVITE
+ * * UNKNOWN_MEMBER
+ * * UNKNOWN_MESSAGE
+ * * UNKNOWN_OVERWRITE
+ * * UNKNOWN_PROVIDER
+ * * UNKNOWN_ROLE
+ * * UNKNOWN_TOKEN
+ * * UNKNOWN_USER
+ * * UNKNOWN_EMOJI
+ * * UNKNOWN_WEBHOOK
+ * * UNKNOWN_BAN
+ * * UNKNOWN_GUILD_TEMPLATE
+ * * BOT_PROHIBITED_ENDPOINT
+ * * BOT_ONLY_ENDPOINT
+ * * CHANNEL_HIT_WRITE_RATELIMIT
+ * * MAXIMUM_GUILDS
+ * * MAXIMUM_FRIENDS
+ * * MAXIMUM_PINS
+ * * MAXIMUM_ROLES
+ * * MAXIMUM_WEBHOOKS
+ * * MAXIMUM_REACTIONS
+ * * MAXIMUM_CHANNELS
+ * * MAXIMUM_ATTACHMENTS
+ * * MAXIMUM_INVITES
+ * * GUILD_ALREADY_HAS_TEMPLATE
+ * * UNAUTHORIZED
+ * * ACCOUNT_VERIFICATION_REQUIRED
+ * * REQUEST_ENTITY_TOO_LARGE
+ * * FEATURE_TEMPORARILY_DISABLED
+ * * USER_BANNED
+ * * ALREADY_CROSSPOSTED
+ * * MISSING_ACCESS
+ * * INVALID_ACCOUNT_TYPE
+ * * CANNOT_EXECUTE_ON_DM
+ * * EMBED_DISABLED
+ * * CANNOT_EDIT_MESSAGE_BY_OTHER
+ * * CANNOT_SEND_EMPTY_MESSAGE
+ * * CANNOT_MESSAGE_USER
+ * * CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL
+ * * CHANNEL_VERIFICATION_LEVEL_TOO_HIGH
+ * * OAUTH2_APPLICATION_BOT_ABSENT
+ * * MAXIMUM_OAUTH2_APPLICATIONS
+ * * INVALID_OAUTH_STATE
+ * * MISSING_PERMISSIONS
+ * * INVALID_AUTHENTICATION_TOKEN
+ * * NOTE_TOO_LONG
+ * * INVALID_BULK_DELETE_QUANTITY
+ * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL
+ * * INVALID_OR_TAKEN_INVITE_CODE
+ * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE
+ * * INVALID_OAUTH_TOKEN
+ * * BULK_DELETE_MESSAGE_TOO_OLD
+ * * INVALID_FORM_BODY
+ * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT
+ * * INVALID_API_VERSION
+ * * CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL
+ * * REACTION_BLOCKED
+ * * RESOURCE_OVERLOADED
+ * @typedef {string} APIError
+ */
+export const APIErrors = {
+ UNKNOWN_ACCOUNT: 10001,
+ UNKNOWN_APPLICATION: 10002,
+ UNKNOWN_CHANNEL: 10003,
+ UNKNOWN_GUILD: 10004,
+ UNKNOWN_INTEGRATION: 10005,
+ UNKNOWN_INVITE: 10006,
+ UNKNOWN_MEMBER: 10007,
+ UNKNOWN_MESSAGE: 10008,
+ UNKNOWN_OVERWRITE: 10009,
+ UNKNOWN_PROVIDER: 10010,
+ UNKNOWN_ROLE: 10011,
+ UNKNOWN_TOKEN: 10012,
+ UNKNOWN_USER: 10013,
+ UNKNOWN_EMOJI: 10014,
+ UNKNOWN_WEBHOOK: 10015,
+ UNKNOWN_BAN: 10026,
+ UNKNOWN_GUILD_TEMPLATE: 10057,
+ BOT_PROHIBITED_ENDPOINT: 20001,
+ BOT_ONLY_ENDPOINT: 20002,
+ CHANNEL_HIT_WRITE_RATELIMIT: 20028,
+ MAXIMUM_GUILDS: 30001,
+ MAXIMUM_FRIENDS: 30002,
+ MAXIMUM_PINS: 30003,
+ MAXIMUM_ROLES: 30005,
+ MAXIMUM_WEBHOOKS: 30007,
+ MAXIMUM_REACTIONS: 30010,
+ MAXIMUM_CHANNELS: 30013,
+ MAXIMUM_ATTACHMENTS: 30015,
+ MAXIMUM_INVITES: 30016,
+ GUILD_ALREADY_HAS_TEMPLATE: 30031,
+ UNAUTHORIZED: 40001,
+ ACCOUNT_VERIFICATION_REQUIRED: 40002,
+ REQUEST_ENTITY_TOO_LARGE: 40005,
+ FEATURE_TEMPORARILY_DISABLED: 40006,
+ USER_BANNED: 40007,
+ ALREADY_CROSSPOSTED: 40033,
+ MISSING_ACCESS: 50001,
+ INVALID_ACCOUNT_TYPE: 50002,
+ CANNOT_EXECUTE_ON_DM: 50003,
+ EMBED_DISABLED: 50004,
+ CANNOT_EDIT_MESSAGE_BY_OTHER: 50005,
+ CANNOT_SEND_EMPTY_MESSAGE: 50006,
+ CANNOT_MESSAGE_USER: 50007,
+ CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008,
+ CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009,
+ OAUTH2_APPLICATION_BOT_ABSENT: 50010,
+ MAXIMUM_OAUTH2_APPLICATIONS: 50011,
+ INVALID_OAUTH_STATE: 50012,
+ MISSING_PERMISSIONS: 50013,
+ INVALID_AUTHENTICATION_TOKEN: 50014,
+ NOTE_TOO_LONG: 50015,
+ INVALID_BULK_DELETE_QUANTITY: 50016,
+ CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019,
+ INVALID_OR_TAKEN_INVITE_CODE: 50020,
+ CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021,
+ INVALID_OAUTH_TOKEN: 50025,
+ BULK_DELETE_MESSAGE_TOO_OLD: 50034,
+ INVALID_FORM_BODY: 50035,
+ INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036,
+ INVALID_API_VERSION: 50041,
+ CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074,
+ REACTION_BLOCKED: 90001,
+ RESOURCE_OVERLOADED: 130000,
+};
+
+/**
+ * The value set for a guild's default message notifications, e.g. `ALL`. Here are the available types:
+ * * ALL
+ * * MENTIONS
+ * @typedef {string} DefaultMessageNotifications
+ */
+export const DefaultMessageNotifications = ["ALL", "MENTIONS"];
+
+/**
+ * The value set for a team members's membership state:
+ * * INVITED
+ * * ACCEPTED
+ * @typedef {string} MembershipStates
+ */
+export const MembershipStates = [
+ // They start at 1
+ null,
+ "INVITED",
+ "ACCEPTED",
+];
+
+/**
+ * The value set for a webhook's type:
+ * * Incoming
+ * * Channel Follower
+ * @typedef {string} WebhookTypes
+ */
+export const WebhookTypes = [
+ // They start at 1
+ null,
+ "Incoming",
+ "Channel Follower",
+];
+
+function keyMirror(arr: string[]) {
+ let tmp = Object.create(null);
+ for (const value of arr) tmp[value] = value;
+ return tmp;
+}
diff --git a/src/util/MessageFlags.ts b/src/util/MessageFlags.ts
new file mode 100644
index 00000000..381b460e
--- /dev/null
+++ b/src/util/MessageFlags.ts
@@ -0,0 +1,14 @@
+// https://github.com/discordjs/discord.js/blob/master/src/util/MessageFlags.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+
+import { BitField } from "./BitField";
+
+export class MessageFlags extends BitField {
+ static FLAGS = {
+ CROSSPOSTED: 1 << 0,
+ IS_CROSSPOST: 1 << 1,
+ SUPPRESS_EMBEDS: 1 << 2,
+ SOURCE_MESSAGE_DELETED: 1 << 3,
+ URGENT: 1 << 4,
+ };
+}
diff --git a/src/util/Permissions.ts b/src/util/Permissions.ts
new file mode 100644
index 00000000..ff4e0f4e
--- /dev/null
+++ b/src/util/Permissions.ts
@@ -0,0 +1,56 @@
+// https://github.com/discordjs/discord.js/blob/master/src/util/Permissions.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+
+import { BitField } from "./BitField";
+
+export type PermissionResolvable = string | number | Permissions | PermissionResolvable[];
+
+export class Permissions extends BitField {
+ static FLAGS = {
+ CREATE_INSTANT_INVITE: 1 << 0,
+ KICK_MEMBERS: 1 << 1,
+ BAN_MEMBERS: 1 << 2,
+ ADMINISTRATOR: 1 << 3,
+ MANAGE_CHANNELS: 1 << 4,
+ MANAGE_GUILD: 1 << 5,
+ ADD_REACTIONS: 1 << 6,
+ VIEW_AUDIT_LOG: 1 << 7,
+ PRIORITY_SPEAKER: 1 << 8,
+ STREAM: 1 << 9,
+ VIEW_CHANNEL: 1 << 10,
+ SEND_MESSAGES: 1 << 11,
+ SEND_TTS_MESSAGES: 1 << 12,
+ MANAGE_MESSAGES: 1 << 13,
+ EMBED_LINKS: 1 << 14,
+ ATTACH_FILES: 1 << 15,
+ READ_MESSAGE_HISTORY: 1 << 16,
+ MENTION_EVERYONE: 1 << 17,
+ USE_EXTERNAL_EMOJIS: 1 << 18,
+ VIEW_GUILD_INSIGHTS: 1 << 19,
+ CONNECT: 1 << 20,
+ SPEAK: 1 << 21,
+ MUTE_MEMBERS: 1 << 22,
+ DEAFEN_MEMBERS: 1 << 23,
+ MOVE_MEMBERS: 1 << 24,
+ USE_VAD: 1 << 25,
+ CHANGE_NICKNAME: 1 << 26,
+ MANAGE_NICKNAMES: 1 << 27,
+ MANAGE_ROLES: 1 << 28,
+ MANAGE_WEBHOOKS: 1 << 29,
+ MANAGE_EMOJIS: 1 << 30,
+ };
+
+ any(permission: PermissionResolvable, checkAdmin = true) {
+ return (checkAdmin && super.has(Permissions.FLAGS.ADMINISTRATOR)) || super.any(permission);
+ }
+
+ /**
+ * Checks whether the bitfield has a permission, or multiple permissions.
+ * @param {PermissionResolvable} permission Permission(s) to check for
+ * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override
+ * @returns {boolean}
+ */
+ has(permission: PermissionResolvable, checkAdmin = true) {
+ return (checkAdmin && super.has(Permissions.FLAGS.ADMINISTRATOR)) || super.has(permission);
+ }
+}
diff --git a/src/util/Snowflake.ts b/src/util/Snowflake.ts
new file mode 100644
index 00000000..da6d7b19
--- /dev/null
+++ b/src/util/Snowflake.ts
@@ -0,0 +1,150 @@
+// @ts-nocheck
+
+// https://github.com/discordjs/discord.js/blob/master/src/util/Snowflake.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+"use strict";
+
+// Discord epoch (2015-01-01T00:00:00.000Z)
+
+/**
+ * A container for useful snowflake-related methods.
+ */
+export class Snowflake {
+ static EPOCH = 1420070400000;
+ static INCREMENT = 0;
+ static processId = 0;
+ static workerId = 0;
+
+ constructor() {
+ throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
+ }
+
+ /**
+ * A Twitter snowflake, except the epoch is 2015-01-01T00:00:00.000Z
+ * ```
+ * If we have a snowflake '266241948824764416' we can represent it as binary:
+ *
+ * 64 22 17 12 0
+ * 000000111011000111100001101001000101000000 00001 00000 000000000000
+ * number of ms since Discord epoch worker pid increment
+ * ```
+ * @typedef {string} Snowflake
+ */
+
+ /**
+ * Transforms a snowflake from a decimal string to a bit string.
+ * @param {Snowflake} num Snowflake to be transformed
+ * @returns {string}
+ * @private
+ */
+ static idToBinary(num) {
+ let bin = "";
+ let high = parseInt(num.slice(0, -10)) || 0;
+ let low = parseInt(num.slice(-10));
+ while (low > 0 || high > 0) {
+ bin = String(low & 1) + bin;
+ low = Math.floor(low / 2);
+ if (high > 0) {
+ low += 5000000000 * (high % 2);
+ high = Math.floor(high / 2);
+ }
+ }
+ return bin;
+ }
+
+ /**
+ * Transforms a snowflake from a bit string to a decimal string.
+ * @param {string} num Bit string to be transformed
+ * @returns {Snowflake}
+ * @private
+ */
+ static binaryToID(num) {
+ let dec = "";
+
+ while (num.length > 50) {
+ const high = parseInt(num.slice(0, -32), 2);
+ const low = parseInt((high % 10).toString(2) + num.slice(-32), 2);
+
+ dec = (low % 10).toString() + dec;
+ num =
+ Math.floor(high / 10).toString(2) +
+ Math.floor(low / 10)
+ .toString(2)
+ .padStart(32, "0");
+ }
+
+ num = parseInt(num, 2);
+ while (num > 0) {
+ dec = (num % 10).toString() + dec;
+ num = Math.floor(num / 10);
+ }
+
+ return dec;
+ }
+
+ /**
+ * Generates a Discord snowflake.
+ * <info>This hardcodes the worker ID as 1 and the process ID as 0.</info>
+ * @param {number|Date} [timestamp=Date.now()] Timestamp or date of the snowflake to generate
+ * @returns {Snowflake} The generated snowflake
+ */
+ static generate(timestamp = Date.now()) {
+ if (timestamp instanceof Date) timestamp = timestamp.getTime();
+ if (typeof timestamp !== "number" || isNaN(timestamp)) {
+ throw new TypeError(
+ `"timestamp" argument must be a number (received ${isNaN(timestamp) ? "NaN" : typeof timestamp})`
+ );
+ }
+ if (Snowflake.INCREMENT >= 4095) Snowflake.INCREMENT = 0;
+ let workerBin = Snowflake.workerId.toString(2).padStart(5, "0");
+ let processBin = Snowflake.processId.toString(2).padStart(5, "0");
+
+ const BINARY = `${(timestamp - EPOCH)
+ .toString(2)
+ .padStart(42, "0")}${workerBin}${processBin}${(Snowflake.INCREMENT++).toString(2).padStart(12, "0")}`;
+ return Snowflake.binaryToID(BINARY);
+ }
+
+ /**
+ * A deconstructed snowflake.
+ * @typedef {Object} DeconstructedSnowflake
+ * @property {number} timestamp Timestamp the snowflake was created
+ * @property {Date} date Date the snowflake was created
+ * @property {number} workerID Worker ID in the snowflake
+ * @property {number} processID Process ID in the snowflake
+ * @property {number} increment Increment in the snowflake
+ * @property {string} binary Binary representation of the snowflake
+ */
+
+ /**
+ * Deconstructs a Discord snowflake.
+ * @param {Snowflake} snowflake Snowflake to deconstruct
+ * @returns {DeconstructedSnowflake} Deconstructed snowflake
+ */
+ static deconstruct(snowflake) {
+ const BINARY = Snowflake.idToBinary(snowflake).toString(2).padStart(64, "0");
+ const res = {
+ timestamp: parseInt(BINARY.substring(0, 42), 2) + EPOCH,
+ workerID: parseInt(BINARY.substring(42, 47), 2),
+ processID: parseInt(BINARY.substring(47, 52), 2),
+ increment: parseInt(BINARY.substring(52, 64), 2),
+ binary: BINARY,
+ };
+ Object.defineProperty(res, "date", {
+ get: function get() {
+ return new Date(this.timestamp);
+ },
+ enumerable: true,
+ });
+ return res;
+ }
+
+ /**
+ * Discord's epoch value (2015-01-01T00:00:00.000Z).
+ * @type {number}
+ * @readonly
+ */
+ static get EPOCH() {
+ return EPOCH;
+ }
+}
diff --git a/src/util/UserFlags.ts b/src/util/UserFlags.ts
new file mode 100644
index 00000000..44486cb0
--- /dev/null
+++ b/src/util/UserFlags.ts
@@ -0,0 +1,22 @@
+// https://github.com/discordjs/discord.js/blob/master/src/util/UserFlags.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+
+import { BitField } from "./BitField";
+
+export class UserFlags extends BitField {
+ static FLAGS = {
+ DISCORD_EMPLOYEE: 1 << 0,
+ PARTNERED_SERVER_OWNER: 1 << 1,
+ HYPESQUAD_EVENTS: 1 << 2,
+ BUGHUNTER_LEVEL_1: 1 << 3,
+ HOUSE_BRAVERY: 1 << 6,
+ HOUSE_BRILLIANCE: 1 << 7,
+ HOUSE_BALANCE: 1 << 8,
+ EARLY_SUPPORTER: 1 << 9,
+ TEAM_USER: 1 << 10,
+ SYSTEM: 1 << 12,
+ BUGHUNTER_LEVEL_2: 1 << 14,
+ VERIFIED_BOT: 1 << 16,
+ EARLY_VERIFIED_BOT_DEVELOPER: 1 << 17,
+ };
+}
diff --git a/src/util/instanceOf.ts b/src/util/instanceOf.ts
new file mode 100644
index 00000000..d83dc39c
--- /dev/null
+++ b/src/util/instanceOf.ts
@@ -0,0 +1,121 @@
+// different version of lambert-server instanceOf with discord error format
+
+import { NextFunction, Request, Response } from "express";
+import { Tuple } from "lambert-server";
+
+const OPTIONAL_PREFIX = "$";
+
+export function check(schema: any) {
+ return (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const result = instanceOf(schema, req.body, { path: "body" });
+ if (result === true) return next();
+ throw result;
+ } catch (error) {
+ return res.status(400).json({ code: 50035, message: "Invalid Form Body", success: false, errors: error });
+ }
+ };
+}
+
+class FieldError extends Error {
+ constructor(public code: string, public message: string) {
+ super(message);
+ }
+}
+
+export function instanceOf(
+ type: any,
+ value: any,
+ { path = "", optional = false, errors = {} }: { path?: string; optional?: boolean; errors?: any } = {}
+): Boolean {
+ try {
+ if (!type) return true; // no type was specified
+
+ if (value == null) {
+ if (optional) return true;
+ throw new FieldError("BASE_TYPE_REQUIRED", `This field is required`);
+ }
+
+ switch (type) {
+ case String:
+ if (typeof value === "string") return true;
+ throw new FieldError("BASE_TYPE_STRING", `This field must be a string`);
+ case Number:
+ value = Number(value);
+ if (typeof value === "number" && !isNaN(value)) return true;
+ throw new FieldError("BASE_TYPE_NUMBER", `This field must be a number`);
+ case BigInt:
+ try {
+ value = BigInt(value);
+ if (typeof value === "bigint") return true;
+ } catch (error) {}
+ throw new FieldError("BASE_TYPE_BIGINT", `This field must be a bigint`);
+ case Boolean:
+ if (value == "true") value = true;
+ if (value == "false") value = false;
+ if (typeof value === "boolean") return true;
+ throw new FieldError("BASE_TYPE_BOOLEAN", `This field must be a boolean`);
+ }
+
+ if (typeof type === "object") {
+ if (type?.constructor?.name != "Object") {
+ if (type instanceof Tuple) {
+ if ((<Tuple>type).types.some((x) => instanceOf(x, value, { path, optional, errors }))) return true;
+ throw new FieldError("BASE_TYPE_CHOICES", `This field must be one of (${type.types})`);
+ }
+ if (value instanceof type) return true;
+ throw new FieldError("BASE_TYPE_CLASS", `This field must be an instance of ${type}`);
+ }
+ if (typeof value !== "object") throw new FieldError("BASE_TYPE_OBJECT", `This field must be a object`);
+
+ if (Array.isArray(type)) {
+ if (!Array.isArray(value)) throw new FieldError("BASE_TYPE_ARRAY", `This field must be an array`);
+ if (!type.length) return true; // type array didn't specify any type
+
+ return (
+ value.every((val, i) => {
+ errors[i] = {};
+ return (
+ instanceOf(type[0], val, { path: `${path}[${i}]`, optional, errors: errors[i] }) === true
+ );
+ }) || errors
+ );
+ }
+
+ const diff = Object.keys(value).missing(
+ Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x))
+ );
+
+ if (diff.length) throw new FieldError("UNKOWN_FIELD", `Unkown key ${diff}`);
+
+ return (
+ Object.keys(type).every((key) => {
+ let newKey = key;
+ const OPTIONAL = key.startsWith(OPTIONAL_PREFIX);
+ if (OPTIONAL) newKey = newKey.slice(OPTIONAL_PREFIX.length);
+ errors[key] = {};
+
+ return (
+ instanceOf(type[key], value[newKey], {
+ path: `${path}.${newKey}`,
+ optional: OPTIONAL,
+ errors: errors[key],
+ }) === true
+ );
+ }) || errors
+ );
+ } else if (typeof type === "number" || typeof type === "string" || typeof type === "boolean") {
+ if (value === type) return true;
+ throw new FieldError("BASE_TYPE_CONSTANT", `This field must be ${value}`);
+ } else if (typeof type === "bigint") {
+ if (BigInt(value) === type) return true;
+ throw new FieldError("BASE_TYPE_CONSTANT", `This field must be ${value}`);
+ }
+
+ return type == value;
+ } catch (error) {
+ let e = error as FieldError;
+ errors._errors = [{ message: e.message, code: e.code }];
+ return errors;
+ }
+}
|