disci
Version:
A HTTP Request handler for discord interactions
1,633 lines (1,611 loc) • 43.3 kB
JavaScript
// src/InteractionHandler.ts
import { EventEmitter } from "eventemitter3";
import {
InteractionResponseType as InteractionResponseType3,
InteractionType as InteractionType6
} from "discord-api-types/v10";
// src/utils/constants.ts
var DiscordEpoch = 14200704e5;
var defaultOptions = {
rest: {
token: typeof process !== "undefined" && process.env.TOKEN || ""
}
};
// src/utils/helpers.ts
var convertSnowflakeToTimeStamp = (id) => {
const milliseconds = BigInt(id) >> 22n;
return Number(milliseconds) + DiscordEpoch;
};
function tryAndValue(fn) {
try {
return fn();
} catch {
return null;
}
}
var serializeObject = (obj) => {
const newObj = {};
for (const [key, value] of Object.entries(obj)) {
if (key === null || key === void 0)
continue;
if (value === null || value === void 0)
continue;
Object.defineProperty(newObj, key, { value, enumerable: true });
}
return newObj;
};
function isBufferLike(value) {
return value instanceof ArrayBuffer || value instanceof Uint8Array || value instanceof Uint8ClampedArray;
}
// src/utils/Factories.ts
import {
InteractionType as InteractionType5,
ApplicationCommandType as ApplicationCommandType3,
ChannelType as ChannelType2
} from "discord-api-types/v10";
// src/structures/ApplicationCommand.ts
import {
ApplicationCommandType,
InteractionType as InteractionType2
} from "discord-api-types/v10";
// src/structures/Bitfield.ts
import {
ChannelFlags,
MessageFlags,
PermissionFlagsBits,
UserFlags
} from "discord-api-types/v10";
var BitField = class _BitField {
static Flags = {};
static None = 0n;
bitfield;
/**
* Create a new Bitfield Instance
* @param baseBits - Base bits to institate the class with
*/
constructor(baseBits = _BitField.None) {
this.bitfield = _BitField.resolve(baseBits);
}
/**
* Adds bits to the bitfield
* @param bits New bits to add
* @returns
*/
add(bits) {
let resolvedBits = 0n;
for (const bit of bits) {
resolvedBits |= _BitField.resolve(bit);
}
this.bitfield |= resolvedBits;
return this;
}
/**
* Removes bits from the bitfield
* @param bits bits to remove
* @returns
*/
remove(bits) {
let resolvedBits = 0n;
for (const bit of bits) {
resolvedBits |= _BitField.resolve(bit);
}
this.bitfield &= ~resolvedBits;
return this;
}
/**
* checks if all bits are present in the bitfield
* @param bits bits to check
* @returns
*/
has(bits) {
const resolvedBits = _BitField.resolve(bits);
return (this.bitfield & resolvedBits) === resolvedBits;
}
equals(bit) {
return !!(this.bitfield & _BitField.resolve(bit));
}
static resolve(bit) {
switch (typeof bit) {
case "bigint":
return bit;
case "number":
return BigInt(bit);
default:
if (Array.isArray(bit)) {
return bit.map((b) => _BitField.resolve(b)).reduce((prev, cur) => prev | cur, _BitField.None);
} else if (bit instanceof _BitField) {
return bit.bitfield;
} else
throw new TypeError(`Expected a bitfieldResolvable`);
}
}
};
var PermissionsBitField = class _PermissionsBitField extends BitField {
static Flags = PermissionFlagsBits;
*[Symbol.iterator]() {
yield* this.array;
}
get array() {
return Object.keys(PermissionFlagsBits).filter(
(flag) => this.has(
_PermissionsBitField.Flags[flag]
)
);
}
};
var MessageFlagsBitField = class extends BitField {
static Flags = MessageFlags;
};
var ChannelFlagsBitField = class extends BitField {
static Flags = ChannelFlags;
};
// src/structures/BaseInteraction.ts
import {
ApplicationCommandOptionType,
InteractionResponseType,
InteractionType,
MessageFlags as MessageFlags3
} from "discord-api-types/v10";
// src/structures/primitives/User.ts
import { Routes } from "discord-api-types/v10";
var PartialUser = class {
/**
* The handler than initiated this class
*/
handler;
/**
* The user's id
*/
id;
constructor(handler, data) {
Object.defineProperty(this, "handler", { value: handler });
this.id = data.id;
}
/**
* Fetch the user this partial belongs to
*/
async fetch() {
const user = await this.handler.api.get(Routes.user(this.id));
return new User(this.handler, user);
}
toString() {
return `<@${this.id}>`;
}
};
var User = class extends PartialUser {
/**
* Create a new user from discord data
* @param apiData - data from discord api
*/
constructor(handler, apiData) {
super(handler, { id: apiData.id });
this.apiData = apiData;
this.username = apiData.username;
}
/**
* The username of this user.Not unique
*/
username;
/**
* Tag of this user.
* @deprecated tag is no longer relevant
*/
get tag() {
return `${this.username}`;
}
};
// src/structures/primitives/Member.ts
var Member = class {
handler;
/**
* User instance belonging to this member
*/
user;
constructor(handler, apiMember) {
Reflect.defineProperty(this, "handler", { value: handler });
this.user = new User(handler, apiMember.user);
}
toString() {
return `<@${this.user.id}>`;
}
};
// src/structures/primitives/Webhook.ts
import {
Routes as Routes4
} from "discord-api-types/v10";
// src/structures/primitives/Message.ts
import {
MessageFlags as MessageFlags2,
Routes as Routes3
} from "discord-api-types/v10";
// src/structures/primitives/Channel.ts
import {
Routes as Routes2,
ChannelType,
ThreadAutoArchiveDuration
} from "discord-api-types/v10";
var GenericPartialChannel = class {
handler;
/**
* Id of this channel
*/
id;
constructor(handler, data) {
this.handler = handler;
this.id = data.id;
}
toString() {
return `<#${this.id}>`;
}
/**
* TimeStamp of when this channel was created
*/
get createdTimestamp() {
return convertSnowflakeToTimeStamp(this.id);
}
/**
* The time the channel was created
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* Fetch the channel represented by the partial
*/
async fetch() {
const apiChannel = await this.handler.api.get(
Routes2.channel(this.id)
);
return ChannelFactory.from(this.handler, apiChannel);
}
};
var BaseChannel = class extends GenericPartialChannel {
/**
* Type of this channel
*/
type;
/**
* Name of this channel
*/
name;
flags;
/**
*
* @param handler
* @param apiChannel
*/
constructor(handler, apiChannel) {
super(handler, apiChannel);
this.type = apiChannel.type;
this.name = apiChannel.name;
this.flags = new ChannelFlagsBitField(apiChannel.flags);
}
isGuildChannel() {
return "guildId" in this;
}
isTextBased() {
return [ChannelType.GuildText, ChannelType.DM].includes(this.type);
}
isThreadChannel() {
return [
ChannelType.PrivateThread,
ChannelType.PublicThread,
ChannelType.AnnouncementThread
].includes(this.type);
}
};
var BaseTextChannel = class extends BaseChannel {
constructor(handler, apiChannel) {
super(handler, apiChannel);
}
};
var GuildTextChannel = class extends BaseTextChannel {
type = ChannelType.GuildText;
guildId;
nsfw;
constructor(handler, apiChannel) {
super(handler, apiChannel);
this.guildId = apiChannel.guild_id;
this.nsfw = Boolean(apiChannel.nsfw);
}
};
var DMTextChannel = class extends BaseTextChannel {
type = ChannelType.DM;
constructor(handler, apiChannel) {
super(handler, apiChannel);
}
};
var ThreadChannel = class extends BaseChannel {
rawThread;
/**
* ID of the Parent channel of this thread
*/
parentId;
/**
* Parent channel of this thread
*/
parent;
/**
* ID of the thread creator
*/
ownerId;
/**
* The thread creator as a user
*/
owner;
/**
* Number of messages (not including the initial message or deleted messages) in a thread
*
*/
messageCount;
/**
* Total count of messages ever sent in this thread
*/
totalSentMessages;
/**
* Whether the thread is archived
*/
archived;
/**
* Duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080
*/
autoArchiveDuration;
/**
* Whether the thread is locked; when a thread is locked, only users with `MANAGE_THREADS` can unarchive it
*/
locked;
/**
* Whether non-moderators can add other non-moderators to the thread; only available on private threads
*/
invitable;
archivedTimeStamp;
constructor(handler, apiThread) {
super(handler, apiThread);
this.rawThread = apiThread;
this.parentId = apiThread.parent_id ?? null;
this.ownerId = apiThread.owner_id ?? null;
this.messageCount = apiThread.message_count ?? 0;
this.totalSentMessages = apiThread.total_message_sent ?? null;
this.archived = apiThread.thread_metadata?.archived ?? false;
this.autoArchiveDuration = apiThread.thread_metadata?.auto_archive_duration ?? ThreadAutoArchiveDuration.OneDay;
this.locked = apiThread.thread_metadata?.locked ?? false;
this.invitable = apiThread.thread_metadata?.invitable ?? false;
this.archivedTimeStamp = apiThread.thread_metadata?.archive_timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
if (this.parentId)
this.parent = new GenericPartialChannel(handler, { id: this.parentId });
if (this.ownerId)
this.owner = new PartialUser(handler, { id: this.ownerId });
}
get archivedAt() {
return new Date(this.archivedTimeStamp);
}
get createdAt() {
return new Date(this.rawThread.thread_metadata?.create_timestamp ?? 0);
}
};
// src/structures/primitives/Message.ts
var Message = class {
handler;
/**
* Id of this message
*/
id;
/**
* Embeds for this message
*/
embeds;
/**
* Content of this message
*/
content;
/**
* Timestamp of the message was sent at
*/
createdTimestamp;
/**
* TImestamp of when this message was last edited (if applicable)
*/
editedTimestamp;
/**
* The user who created this message (if created by a user)
*/
author;
/**
* Webhook that created this message (if created by webhook)
*/
webhook;
/**
* Channel this message was created in
*/
channelId;
channel;
/**
* Flags for this message
*/
flags;
constructor(handler, apiData) {
Object.defineProperty(this, "handler", { value: handler });
this.id = apiData.id;
this.embeds = apiData.embeds ?? [];
this.createdTimestamp = Date.parse(apiData.timestamp);
this.editedTimestamp = apiData.edited_timestamp ? Date.parse(apiData.edited_timestamp) : null;
this.channelId = apiData.channel_id;
this.channel = new GenericPartialChannel(this.handler, {
id: this.channelId
});
if ("flags" in apiData) {
this.flags = new MessageFlagsBitField(apiData.flags);
} else
this.flags = new MessageFlagsBitField();
if ("content" in apiData) {
this.content = apiData.content;
}
if (apiData.webhook_id) {
this.webhook = new WebhookPartial(handler, { id: apiData.webhook_id });
} else {
this.author = new User(this.handler, apiData.author);
}
}
/**
* The time the message was sent at
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* The time the message was last edited at (if applicable)
*/
get editedAt() {
return this.editedTimestamp ? new Date(this.editedTimestamp) : void 0;
}
/**
* Whether this message has a thread associated with it
*/
get hasThread() {
return this.flags.has(MessageFlags2.HasThread);
}
/**
* Delete this Message
*/
async delete() {
await this.handler.api.delete(
Routes3.channelMessage(this.channelId, this.id)
);
return this;
}
async addReaction(emoji) {
const e = typeof emoji === "string" ? emoji : `${emoji.name}:${emoji.id}`;
await this.handler.api.put(
Routes3.channelMessageOwnReaction(this.channelId, this.id, e)
);
}
/**
* Pins this message
*/
async pin() {
await this.handler.api.put(
Routes3.channelPin(this.channelId, this.id)
);
}
/**
* Unpin this message
*/
async unpin() {
await this.handler.api.delete(
Routes3.channelPin(this.channelId, this.id)
);
}
/**
* Creates a new thread from an existing message.
* @param threadOptions Options for this thread
* @returns
*/
async startThread(threadOptions) {
if (this.hasThread)
throw new Error(`This message already contains a thread`);
if (!this.channel)
throw new Error(`Channel for message could not be resolved`);
const data = await this.handler.api.post(
Routes3.threads(this.channel.id, this.id),
{
body: {
name: threadOptions.name,
auto_archive_duration: threadOptions.autoArchiveDuration,
rate_limit_per_user: threadOptions.rateLimitPerUser
}
}
);
return new ThreadChannel(this.handler, data);
}
/**
* Internal method to resolve data for message Create
* @private
*/
static resolveMessageParams(params) {
const msg = {};
const files = [];
if (params.content) {
if (typeof params.content !== "string")
throw new TypeError(`Expected a string for message content`);
msg.content = params.content;
}
if (params.embeds) {
if (!Array.isArray(params.embeds))
throw new TypeError(`Expected an array for embeds`);
msg.embeds = params.embeds;
}
if (params.components) {
if (!Array.isArray(params.components))
throw new TypeError(`Expected an array for Component Action rows`);
msg.components = params.components;
}
if (params.files) {
if (!Array.isArray(params.files))
throw new TypeError(`Expected an array for Files`);
files.push(...params.files);
}
if (params.flags) {
const bitfield = MessageFlagsBitField.resolve(params.flags);
msg.flags = bitfield.toString();
}
return {
body: msg,
files
};
}
};
// src/structures/primitives/Webhook.ts
var WebhookPartial = class {
/**
* The handler than initiated this class
*/
handler;
/**
* The id of the webhook
*/
id;
/**
* The secure token of the webhook
*/
token;
constructor(handler, data) {
this.handler = handler;
this.id = data.id;
this.token = data.token;
}
/**
* Fetch the webhook this id belongs to
*/
async fetch() {
const webhook = await this.handler.api.get(
// * takes token of undefined as optional parameter
Routes4.webhook(this.id, this.token)
);
return new Webhook(this.handler, webhook);
}
};
var Webhook = class extends WebhookPartial {
/**
*The type of the webhook
*
*See https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types
*/
type;
/**
* Owner of this webhook
*/
owner;
/**
* The application that created this werbhook
*/
applicationId;
constructor(handler, data) {
super(handler, { id: data.id, token: data.token });
this.type = data.type;
this.owner = data.user ? new User(this.handler, data.user) : null;
this.applicationId = data.application_id;
}
/**
* Gets a message that was sent by this webhook.
*/
async fetchMessage(messageId, { threadId } = {}) {
if (!this.token)
throw new TypeError(`This webhook does not contain a Token`);
const query = {};
if (threadId)
query.threadId = threadId;
const message = await this.handler.api.get(
Routes4.webhookMessage(this.id, this.token, messageId),
{
query
}
);
return new Message(this.handler, message);
}
/**
*
* @param messageId id of the message to edit
* @param newMessage new data to edit
*/
async editReply(messageId, newMessage) {
if (!this.token)
throw new Error(`This webhook does not contain a Token`);
const resolvedParams = Message.resolveMessageParams(newMessage);
const edited = await this.handler.api.patch(
Routes4.webhookMessage(this.id, this.token, messageId),
{
body: resolvedParams.body,
files: resolvedParams.files
}
);
return new Message(this.handler, edited);
}
/**
* Sends a message with this webhook.
*/
async send(messageData, { threadId } = {}) {
if (!this.token)
throw new Error(`This webhook does not contain a Token`);
const resolvedParams = Message.resolveMessageParams(messageData);
const createdMessage = await this.handler.api.post(
Routes4.webhookMessage(this.id, this.token),
{
body: resolvedParams.body,
files: resolvedParams.files,
query: {
wait: true,
thread_id: threadId
}
}
);
return new Message(this.handler, createdMessage);
}
};
// src/structures/primitives/Guild.ts
import {
CDNRoutes,
GuildFeature,
ImageFormat,
Routes as Routes5
} from "discord-api-types/v10";
var PartialGuild = class {
handler;
/**
* Id of this guild
*/
id;
constructor(handler, { id }) {
Object.defineProperty(this, "handler", { value: handler });
this.id = id;
}
/**
* Fetch the guild this partial belongs to
* @param opts.withCounts when true, will return approximate member and presence counts for the guild
*/
async fetch({ withCounts } = {}) {
const guild = await this.handler.api.get(Routes5.guild(this.id), {
query: withCounts ? {
with_counts: "true"
} : {}
});
return new Guild(this.handler, guild);
}
};
var Guild = class extends PartialGuild {
/**
* Owner of this Guild as a partial
*/
owner;
/**
* Name of this guild
*/
name;
/**
* Approximate Member count not always present (use Guild.fetch() with "withCounts" enabled)
*/
approximateMemberCount;
/**
* Approximate Presence count not always present (use Guild.fetch() with "withCounts" enabled)
*/
approximatePresenceCount;
/**
* The description for the guild
*/
description;
/**
* Enabled guild features (animated banner, news, auto moderation, etc).
* @link https://discord.com/developers/docs/resources/guild#guild-object-guild-features
*/
features;
/**
* Icon hash for this guild's Icon
* @link https://discord.com/developers/docs/reference#image-formatting
*/
iconHash;
constructor(handler, apiData) {
super(handler, { id: apiData.id });
this.owner = new PartialUser(handler, { id: apiData.owner_id });
this.name = apiData.name;
this.description = apiData.description;
this.features = apiData.features;
this.iconHash = apiData.icon;
if ("approximate_member_count" in apiData) {
this.approximateMemberCount = apiData.approximate_member_count;
}
if ("approximate_presence_count" in apiData) {
this.approximatePresenceCount = apiData.approximate_presence_count;
}
}
/**
* boolean to indicate if this guild is a verified guild or not
*/
get verified() {
return this.features.includes(GuildFeature.Verified);
}
/**
* boolean to indicate if this guild is a partnered guild or not
*/
get partnered() {
return this.features.includes(GuildFeature.Partnered);
}
/**
* TimeStamp of when this guild was created
*/
get createdTimestamp() {
return convertSnowflakeToTimeStamp(this.id);
}
/**
* The time this guild was created as a date
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* iconURL gets the current guild icon.
* @link https://discord.com/developers/docs/reference#image-formatting
*/
iconURL(opts = { size: 128 }) {
return this.iconHash && `${"https://cdn.discordapp.com" /* DiscordCdn */}/${CDNRoutes.guildIcon(
this.id,
this.iconHash,
opts.format ?? (this.iconHash.startsWith("a_") ? ImageFormat.GIF : ImageFormat.JPEG)
)}`;
}
};
// src/structures/BaseInteraction.ts
var BaseInteraction = class {
/**
* Create a new received interaction
* @param handler
* @param RawInteractionData
*/
constructor(handler, RawInteractionData) {
this.RawInteractionData = RawInteractionData;
Reflect.defineProperty(this, "handler", { value: handler });
Reflect.defineProperty(this, "rawData", { value: RawInteractionData });
this.id = RawInteractionData.id;
this.applicationId = RawInteractionData.application_id;
this.token = RawInteractionData.token;
this.type = RawInteractionData.type;
this.version = RawInteractionData.version;
this.guildLocale = RawInteractionData.guild_locale ?? null;
if (RawInteractionData.guild_id && RawInteractionData.member) {
this.guildId = RawInteractionData.guild_id;
this.guild = new PartialGuild(this.handler, { id: this.guildId });
this.member = new Member(this.handler, RawInteractionData.member);
Reflect.defineProperty(this, "user", {
get: () => {
return this.member?.user;
}
});
} else if (RawInteractionData.user) {
this.user = new User(this.handler, RawInteractionData.user);
}
if (RawInteractionData.channel_id) {
this.channelId = RawInteractionData.channel_id;
this.channel = new GenericPartialChannel(this.handler, {
id: this.channelId
});
}
const permissions = RawInteractionData.app_permissions;
if (permissions) {
this.appPermissions = new PermissionsBitField(BigInt(permissions));
}
this.responded = false;
}
/**
* ID of the interaction
*/
id;
/**
* ID of the application this interaction is for
*/
applicationId;
/**
* Token of this interaction
*/
token;
/**
* Type of this interaction
*/
type;
/**
* Id of the Guild that the interaction was sent from
*/
guildId;
/**
* Guild the interaction was sent from as a partial guild
*/
guild;
/**
* Id of the Channel that the interaction was sent from
*/
channelId;
/**
* Channel the interactions was send from
*/
channel;
/**
* Readonly Property, as per the Discord docs always 1
* https://discord.com/developers/docs/interactions/receiving-and-responding
*/
version;
appPermissions;
callback;
/**
* If this interaction has Already been responded to
*/
responded;
/**
* The user who invoked this interaction
*/
user;
/**
* Guild member who invoked this interaction (if any)
*/
member;
/**
* Handler that initiated this class
*/
handler;
/**
* The preferred locale from the guild this interaction was sent in
*/
guildLocale;
/**
* Internal function. define the function used to respond the interaction
* @param fn
* @private
* @ignore
*/
useCallback(fn) {
Reflect.defineProperty(this, "callback", {
value: fn,
enumerable: false
});
return this;
}
/**
* Timestamp of this interaction
*/
get createdTimestamp() {
return convertSnowflakeToTimeStamp(this.id);
}
/**
* Created time as a date
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* Indicates whether this interaction is a {@link ApplicationCommand}
*/
isCommand() {
return this.type === InteractionType.ApplicationCommand;
}
/**
* Indicates whether this interaction is a {@link AutoCompleteInteraction}
*/
isAutoComplete() {
return this.type === InteractionType.ApplicationCommandAutocomplete;
}
/**
* Indicates whether this interaction is a {@link ComponentInteraction}
*/
isComponent() {
return this.type === InteractionType.MessageComponent;
}
/**
* Indicates whether this interaction can be replied to (i.e {@link BaseReplyInteraction}).
*/
isRepliable() {
return !this.responded && this.type != InteractionType.ApplicationCommandAutocomplete;
}
/**
* Respond to this interaction, Raw method
* @returns
* @private
* @ignore
*/
_respond(response) {
if (this.responded)
throw new Error(`This interaction has already been responded to.`);
this.callback(response);
this.responded = true;
return this;
}
};
var BaseReplyInteraction = class extends BaseInteraction {
deferReply({ fetchReply = false, ephemeral = false } = {}) {
if (this.responded)
throw new Error(
`This Interaction already timed out or has been replied to`
);
this._respond({
type: InteractionResponseType.DeferredChannelMessageWithSource,
data: {
flags: ephemeral ? MessageFlags3.Ephemeral : void 0
}
});
if (fetchReply) {
return this.fetchReply();
}
return Promise.resolve(this);
}
/**
* Fetch the reply that was sent for this interaction
* @returns Message
*/
fetchReply() {
if (!this.responded)
throw new Error(`Please Respond to this interaction before fetching it.`);
return Webhook.prototype.fetchMessage.call(
{
id: this.applicationId,
token: this.token,
handler: this.handler
},
"@original"
);
}
reply(opts) {
if (this.responded)
throw new Error(
`This interaction either timed out or already been responded to`
);
const APIResponse = {
type: InteractionResponseType.ChannelMessageWithSource,
data: {}
};
if (opts) {
APIResponse.data = Message.resolveMessageParams(opts).body;
} else
throw new TypeError(`CreateMessage Options are required`);
this._respond(APIResponse);
if (opts.fetchReply === true)
return this.fetchReply();
return Promise.resolve(this);
}
/**
* Edit previously sent responses
*/
editReply(message) {
if (!this.responded)
throw new Error(`Interaction was not responded to`);
return Webhook.prototype.editReply.call(
{
id: this.applicationId,
token: this.token,
handler: this.handler
},
"@original",
message
);
}
};
var InteractionOptions = class {
_options;
subCommand;
group;
constructor(options) {
this._options = options ?? [];
if (options[0]?.type === ApplicationCommandOptionType.Subcommand) {
this.subCommand = options[0].name;
this._options = options[0].options ?? [];
}
if (options[0]?.type === ApplicationCommandOptionType.SubcommandGroup) {
this.group = options[0].name;
this._options = options[0].options ?? [];
}
}
getSubCommand(required = false) {
if (required && !this.subCommand)
throw new TypeError(`Subcommand Not found`);
return this.subCommand ?? null;
}
getSubCommandGroup(required = false) {
if (required && !this.group)
throw new TypeError(`Subcommand Not found`);
return this.group ?? null;
}
get(name, required = false) {
const opt = this._options.find(
(o) => o.name === name
);
if (required && !opt)
throw new TypeError(`Missing interaction option: ${name}`);
return opt ?? null;
}
_getType(name, expectedTypes, properties, required) {
const option = this.get(name, required);
if (!option)
return null;
if (!expectedTypes.includes(option.type))
throw new TypeError(
`Expected Type of option to be ${expectedTypes.join(" ")} Received ${option.type}`
);
if (required && properties.every(
(prop) => option[prop] == null || typeof option[prop] == "undefined"
))
throw new TypeError(`Expected Value to be available`);
return option;
}
getString(name, required = false) {
return this._getType(
name,
[ApplicationCommandOptionType.String],
["value"],
required
)?.value ?? null;
}
getBoolean(name, required = false) {
return this._getType(
name,
[ApplicationCommandOptionType.Boolean],
["value"],
required
)?.value ?? null;
}
getNumber(name, required = false) {
return this._getType(
name,
[ApplicationCommandOptionType.Number],
["value"],
required
)?.value ?? null;
}
getInteger(name, required = false) {
return this._getType(
name,
[ApplicationCommandOptionType.Integer],
["value"],
required
)?.value ?? null;
}
getUserId(name, required = false) {
return this._getType(
name,
[ApplicationCommandOptionType.User],
["value"],
required
)?.value ?? null;
}
getChannelId(name, required = false) {
return this._getType(
name,
[ApplicationCommandOptionType.Channel],
["value"],
required
)?.value ?? null;
}
getRolelId(name, required = false) {
return this._getType(
name,
[ApplicationCommandOptionType.Role],
["value"],
required
)?.value ?? null;
}
getMentionable(name, required = false) {
return this._getType(
name,
[ApplicationCommandOptionType.Mentionable],
["value"],
required
)?.value ?? null;
}
getFocused(full) {
const focusedOption = this._options.find(
(option) => option.focused
);
if (!focusedOption)
throw new Error(`No Focused option found`);
return full ? focusedOption : focusedOption.value;
}
};
// src/structures/ApplicationCommand.ts
var ApplicationCommand = class extends BaseReplyInteraction {
type = InteractionType2.ApplicationCommand;
/**
* Type of this command
* https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types
*/
commandType;
/**
* Name of this command
*/
commandName;
/**
* Id Of **The Application command** (not interaction)
*/
commandId;
/**
* Resolved Data of this interaction
*/
resolved;
constructor(handler, rawData) {
super(handler, rawData);
const data = rawData.data;
this.commandType = data.type;
this.commandName = data.name;
this.commandId = data.id;
this.resolved = {
users: /* @__PURE__ */ new Map(),
members: /* @__PURE__ */ new Map(),
roles: /* @__PURE__ */ new Map(),
messages: /* @__PURE__ */ new Map()
};
}
/**
* If this is a Message Context menu
*/
isMessageMenu() {
return this.commandType === ApplicationCommandType.Message;
}
/**
* If this is a User Context menu
*/
isUserMenu() {
return this.commandType === ApplicationCommandType.User;
}
/**
* If this is a Slash Command
*/
isSlashCommand() {
return this.commandType === ApplicationCommandType.ChatInput;
}
/**
* Alias to isSlashCommand
*/
isChatInputInteraction() {
return this.isSlashCommand();
}
};
var ChatInputInteraction = class extends ApplicationCommand {
options;
commandType = ApplicationCommandType.ChatInput;
constructor(handler, rawData) {
super(handler, rawData);
this.options = new InteractionOptions(rawData.data.options ?? []);
}
};
var MessageCommandInteraction = class extends ApplicationCommand {
commandType = ApplicationCommandType.Message;
constructor(handler, rawData) {
super(handler, rawData);
}
};
var UserCommandInteraction = class extends ApplicationCommand {
commandType = ApplicationCommandType.User;
/**
* Id of the Target user
*/
targetId;
/**
* Target User
*/
targetUser;
constructor(handler, rawData) {
super(handler, rawData);
const data = rawData.data;
this.targetId = data.target_id;
if (data.resolved.users) {
for (const [id, user] of Object.entries(data.resolved.users)) {
this.resolved.users.set(id, new User(this.handler, user));
}
}
if (data.resolved.members) {
for (const [id, member] of Object.entries(data.resolved.members)) {
this.resolved.members.set(id, member);
}
}
this.targetUser = this.resolved.users.get(this.targetId);
}
};
// src/structures/AutoCompleteInteraction.ts
import {
InteractionResponseType as InteractionResponseType2,
InteractionType as InteractionType3
} from "discord-api-types/v10";
var AutoCompleteInteraction = class extends BaseInteraction {
type = InteractionType3.ApplicationCommandAutocomplete;
/**
* Id of the command this autocomplete was sent for
*/
commandId;
/**
* Name of the command this autocomplete was sent for
*/
commandName;
/**
* Type of the command this autocomplete was sent for
*/
commandType;
/**
* Guild id of the command this autocomplete was sent for
*/
commandGuildId;
options;
constructor(handler, rawData) {
super(handler, rawData);
const data = rawData.data;
this.commandId = data.id;
this.commandName = data.name;
this.commandType = data.type;
this.commandGuildId = data.guild_id;
this.options = new InteractionOptions(data.options ?? []);
}
/**
* Send autocomplete results
*
* @example
* ```ts
* interaction.respond([
* "regular Choice",
* { name: 'choice', value: 'choice value' }
* ])
* ```
*
* @example
* ```ts
* // for no choices screen
* interaction.respond([])
* ```
*/
respond(choices) {
if (!Array.isArray(choices))
throw new TypeError(`Expected autocomplete choices to be a array`);
const _choices = this.getChoices(choices);
this._respond({
type: InteractionResponseType2.ApplicationCommandAutocompleteResult,
data: {
choices: _choices
}
});
return this;
}
getChoices(choices) {
return choices.map(
(choice) => typeof choice !== "string" ? choice : { name: choice, value: choice }
);
}
};
// src/structures/ComponentInteraction.ts
import {
InteractionType as InteractionType4
} from "discord-api-types/v10";
var ComponentInteraction = class extends BaseReplyInteraction {
type = InteractionType4.MessageComponent;
customId;
constructor(handler, apiData) {
super(handler, apiData);
const data = apiData.data;
this.customId = data.custom_id;
}
};
// src/utils/Factories.ts
var InteractionFactory = class {
static from(handler, apiInteraction) {
switch (apiInteraction.type) {
case InteractionType5.ApplicationCommand:
return ApplicationCommandFactory.from(handler, apiInteraction);
case InteractionType5.ApplicationCommandAutocomplete:
return new AutoCompleteInteraction(handler, apiInteraction);
case InteractionType5.MessageComponent:
return ComponentInteractionFactory.from(handler, apiInteraction);
default:
return null;
}
}
};
var ComponentInteractionFactory = class {
static from(handler, apiComponent) {
return new ComponentInteraction(handler, apiComponent);
}
};
var ChannelFactory = class {
static from(handler, apiChannel) {
switch (apiChannel.type) {
case ChannelType2.GuildText:
return new GuildTextChannel(handler, apiChannel);
case ChannelType2.DM:
return new DMTextChannel(handler, apiChannel);
case ChannelType2.PrivateThread:
case ChannelType2.AnnouncementThread:
case ChannelType2.PublicThread:
return new ThreadChannel(handler, apiChannel);
default:
return null;
}
}
};
var ApplicationCommandFactory = class {
static from(handler, apiAppCommand) {
switch (apiAppCommand.data.type) {
case ApplicationCommandType3.ChatInput:
return new ChatInputInteraction(
handler,
apiAppCommand
);
case ApplicationCommandType3.Message:
return new MessageCommandInteraction(
handler,
apiAppCommand
);
case ApplicationCommandType3.User:
return new UserCommandInteraction(
handler,
apiAppCommand
);
default:
return null;
}
}
};
// src/utils/REST.ts
var UserAgent = `DiscordBot (https://github.com/typicalninja/disci, 0.0.1)`.trim();
var Rest = class {
authPrefix;
authToken;
rootUrl;
/**
* for support of serverless and other platforms
*/
constructor(_opts) {
this.authPrefix = _opts.authPrefix || "Bot";
this.authToken = _opts.token || null;
this.rootUrl = _opts.rootUrl ? _opts.rootUrl.endsWith("/") ? _opts.rootUrl.slice(0, _opts.rootUrl.length - 1) : _opts.rootUrl : "https://discord.com/api" /* DiscordApi */;
}
/**
* Set the current active token, request may fail if this is not set
* @param token
* @returns
*/
setToken(token) {
if (typeof token !== "string" || token === "")
throw new TypeError(
`Token must be a valid string, received ${typeof token}`
);
this.authToken = token;
return this;
}
async makeRequest(method, path, opts) {
const request = this.getRequest(path, method, opts);
const req = await fetch(this.getUrl(path, opts?.query), request.init);
if (!req.ok) {
const errors = await tryAndValue(() => req.json());
throw new Error(
`Request to [${method}:${path}] returned ${req.status} [${req.statusText}]`,
{
cause: errors
}
);
}
const contentType = req.headers.get("content-type");
if (contentType && contentType.includes("application/json"))
return await req.json();
else
return await req.arrayBuffer();
}
get(path, opts) {
return this.makeRequest("GET", path, opts);
}
post(path, opts) {
return this.makeRequest("POST", path, opts);
}
put(path, opts) {
return this.makeRequest("PUT", path, opts);
}
patch(path, opts) {
return this.makeRequest("PATCH", path, opts);
}
delete(path, opts) {
return this.makeRequest("DELETE", path, opts);
}
getUrl(path, queryParams) {
let url;
if (path.startsWith("/")) {
url = `${this.rootUrl}${path}`;
} else if (path.startsWith("https://") || path.startsWith("http://")) {
url = path;
} else {
url = `${this.rootUrl}/${path}`;
}
if (queryParams) {
url = `${url}?${new URLSearchParams(
queryParams
).toString()}`;
}
return url;
}
get authHeader() {
return `${this.authPrefix} ${this.authToken}`;
}
getRequest(path, method, options) {
const baseHeaders = {
"User-Agent": UserAgent
};
if (options?.auth !== false) {
if (!this.authToken)
throw new Error(`Auth token was expected for request but was not set`);
baseHeaders.Authorization = this.authHeader;
}
if (options?.reason && options.reason.length) {
baseHeaders["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
}
if (options?.headers) {
Object.assign(baseHeaders, options.headers);
}
let fBody;
if (options?.files?.length) {
const formData = new FormData();
for (const [index, file] of options.files.entries()) {
const fileKey = file.key ?? `files[${index}]`;
if (isBufferLike(file.data)) {
const contentType = file.contentType;
if (!contentType)
throw new Error(
`Expected content type for file (${fileKey}) to be a string`
);
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 (options.body) {
options.body = serializeObject(options.body);
if (options.appendBodyToForm) {
for (const [key, value] of Object.entries(
options.body
)) {
formData.append(key, value);
}
} else
formData.append("payload_json", JSON.stringify(options.body));
}
fBody = formData;
} else if (options?.body) {
fBody = JSON.stringify(serializeObject(options.body));
baseHeaders["Content-Type"] = "application/json";
}
const request = {
method: method.toUpperCase(),
headers: baseHeaders,
body: fBody
};
return {
init: request,
url: this.getUrl(path, options?.query)
};
}
};
// src/InteractionHandler.ts
var InteractionHandler = class extends EventEmitter {
options;
/**
* Handler Rest Manager
*/
api;
constructor(options = {}) {
super();
this.options = Object.assign(defaultOptions, options);
this.api = new Rest(this.options.rest);
}
/**
* Process a request and return a response according to the request.
* This does not verify the validity of the request
*
* @param body body of the received request
* @param signal Abort controller signal allow you to control when the handler ends (timeouts etc)
* @returns A json object containing data to be responded with
*
*
* @example
*
* ```ts
* // get the request here
*
* // verify it here
* if(!(await isVerified(request))) return new Response("Invalid Headers, Unauthorized", { status: 401 })
*
* const timeOutAbort = new AbortController();
* const timeout = setTimeout(() => {
* timeOutAbort.abort("Time out");
* }, 3000);
*
* try {
* const handled = await processRequest(body, timeOutAbort.signal)
* // if it resolved that means handler successfully resolved
* // remember to remove the timeout
* clearTimeout(timeout)
* // it safe to return the response as a json response
* return new Response(handled, { status: 200 })
* }
* catch {
* return new Response("Server Error", { status: 500 })
* }
* ```
*/
processRequest(body, signal) {
return new Promise((resolve, reject) => {
const rawInteraction = tryAndValue(
() => typeof body === "string" ? JSON.parse(body) : body
);
if (!rawInteraction)
return reject(
new TypeError(
`Failed to parse received interaction to a valid interaction`
)
);
const interaction = InteractionFactory.from(this, rawInteraction);
if (interaction) {
interaction.useCallback((response) => resolve(response));
if (signal) {
signal.addEventListener("abort", () => {
interaction.useCallback(() => {
throw new Error(`Interaction timed out (via abort)`);
});
reject(signal.reason);
});
}
return this.emit("interactionCreate", interaction);
} else if (rawInteraction.type === InteractionType6.Ping) {
return resolve({
type: InteractionResponseType3.Pong
});
} else {
reject(
new TypeError(
`Unsupported Interaction of type ${rawInteraction.type} received`
)
);
}
});
}
};
export {
ApplicationCommand,
AutoCompleteInteraction,
BaseInteraction,
BaseReplyInteraction,
ChatInputInteraction,
ComponentInteraction,
InteractionHandler,
InteractionOptions,
MessageCommandInteraction,
PartialGuild,
PartialUser,
Rest,
UserCommandInteraction,
Webhook,
WebhookPartial
};
//! impl: discord.js (https://github.com/discordjs/discord.js/blob/main/packages/rest/src/lib/utils/utils.ts#L140)
//! impl: from d.js