diff --git a/dist/Command.js b/dist/Command.js index c3f7cf8..e51bffe 100644 --- a/dist/Command.js +++ b/dist/Command.js @@ -41,7 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { var cooldown_1 = __importDefault(require("./models/cooldown")); var Command = /** @class */ (function () { function Command(instance, client, names, callback, error, _a) { - var category = _a.category, minArgs = _a.minArgs, maxArgs = _a.maxArgs, syntaxError = _a.syntaxError, expectedArgs = _a.expectedArgs, description = _a.description, requiredPermissions = _a.requiredPermissions, permissions = _a.permissions, cooldown = _a.cooldown, globalCooldown = _a.globalCooldown, _b = _a.ownerOnly, ownerOnly = _b === void 0 ? false : _b, _c = _a.hidden, hidden = _c === void 0 ? false : _c, _d = _a.guildOnly, guildOnly = _d === void 0 ? false : _d, _e = _a.testOnly, testOnly = _e === void 0 ? false : _e, _f = _a.slash, slash = _f === void 0 ? false : _f; + var category = _a.category, minArgs = _a.minArgs, maxArgs = _a.maxArgs, syntaxError = _a.syntaxError, expectedArgs = _a.expectedArgs, description = _a.description, requiredPermissions = _a.requiredPermissions, permissions = _a.permissions, cooldown = _a.cooldown, globalCooldown = _a.globalCooldown, _b = _a.ownerOnly, ownerOnly = _b === void 0 ? false : _b, _c = _a.hidden, hidden = _c === void 0 ? false : _c, _d = _a.guildOnly, guildOnly = _d === void 0 ? false : _d, _e = _a.testOnly, testOnly = _e === void 0 ? false : _e, _f = _a.slash, slash = _f === void 0 ? false : _f, _g = _a.options, options = _g === void 0 ? [] : _g; this._names = []; this._category = ''; this._minArgs = 0; @@ -60,6 +60,7 @@ var Command = /** @class */ (function () { this._guildOnly = false; this._testOnly = false; this._slash = false; + this._options = []; this.instance = instance; this.client = client; this._names = typeof names === 'string' ? [names] : names; @@ -85,6 +86,7 @@ var Command = /** @class */ (function () { this._callback = callback; this._error = error; this._slash = slash; + this._options = Array.isArray(options) ? options : (options ? [options] : []); if (this.cooldown && this.globalCooldown) { throw new Error("Command \"" + names[0] + "\" has both a global and per-user cooldown. Commands can only have up to one of these properties."); } @@ -449,6 +451,13 @@ var Command = /** @class */ (function () { enumerable: false, configurable: true }); + Object.defineProperty(Command.prototype, "options", { + get: function () { + return this._options; + }, + enumerable: false, + configurable: true + }); return Command; }()); module.exports = Command; diff --git a/dist/CommandHandler.js b/dist/CommandHandler.js index e1827c9..551a445 100644 --- a/dist/CommandHandler.js +++ b/dist/CommandHandler.js @@ -338,7 +338,7 @@ var CommandHandler = /** @class */ (function () { } CommandHandler.prototype.registerCommand = function (instance, client, file, fileName) { return __awaiter(this, void 0, void 0, function () { - var configuration, _a, name, category, commands, aliases, init, callback, execute, run, error, description, requiredPermissions, permissions, testOnly, slash, expectedArgs, minArgs, callbackCounter, names, _i, _b, perm, missing, slashCommands, options, split, a, item, _c, _d, id, hasCallback, command, _e, names_1, name_2; + var configuration, _a, name, category, commands, aliases, init, callback, execute, run, error, description, requiredPermissions, permissions, testOnly, slash, options, expectedArgs, minArgs, callbackCounter, names, _i, _b, perm, missing, slashCommands_1, ApplicationOptions, split, a, item, option, _c, _d, id, hasCallback, command, _e, names_1, name_2; return __generator(this, function (_f) { switch (_f.label) { case 0: @@ -347,7 +347,7 @@ var CommandHandler = /** @class */ (function () { if (configuration.default && Object.keys(configuration).length === 1) { configuration = configuration.default; } - _a = configuration.name, name = _a === void 0 ? fileName : _a, category = configuration.category, commands = configuration.commands, aliases = configuration.aliases, init = configuration.init, callback = configuration.callback, execute = configuration.execute, run = configuration.run, error = configuration.error, description = configuration.description, requiredPermissions = configuration.requiredPermissions, permissions = configuration.permissions, testOnly = configuration.testOnly, slash = configuration.slash, expectedArgs = configuration.expectedArgs, minArgs = configuration.minArgs; + _a = configuration.name, name = _a === void 0 ? fileName : _a, category = configuration.category, commands = configuration.commands, aliases = configuration.aliases, init = configuration.init, callback = configuration.callback, execute = configuration.execute, run = configuration.run, error = configuration.error, description = configuration.description, requiredPermissions = configuration.requiredPermissions, permissions = configuration.permissions, testOnly = configuration.testOnly, slash = configuration.slash, options = configuration.options, expectedArgs = configuration.expectedArgs, minArgs = configuration.minArgs; callbackCounter = 0; if (callback) ++callbackCounter; @@ -402,20 +402,86 @@ var CommandHandler = /** @class */ (function () { if (minArgs !== undefined && !expectedArgs) { throw new Error("WOKCommands > Command \"" + names[0] + "\" has \"minArgs\" property defined without \"expectedArgs\" property as a slash command."); } - slashCommands = instance.slashCommands; - options = []; + if (options !== undefined && options.length === 0) { + console.warn("WOKCommands > Command \"" + names[0] + "\" has \"options\" property defined but nothing in it"); + } + slashCommands_1 = instance.slashCommands; + ApplicationOptions = []; + if (options) { + options.map(function (element, index) { + if (!element.type) { + element.type = slashCommands_1.getOptionFromName("STRING"); + } + else if (typeof element.type === "string") { + element.type = slashCommands_1.getOptionFromName(element.type); + } + if (element.choices && !(element.type === 3 || element.type === 4)) { + throw new Error("WOKCommands > Command \"" + names[0] + "\" > options " + index + " > choices is only avaiable for STRING and INTEGER"); + } + else if (element.choices) { + if (element.choices.length > 25) { + throw new Error("WOKCommands > Command \"" + names[0] + "\" > options " + index + " > choices is too long, Discord allows a maximum of 25 "); + } + element.choices = element.choices.map(function (element2, index2) { + var el; + if (typeof element2 === "object") { + el = element2; + if (!el.name) { + throw new Error("WOKCommands > Command \"" + names[0] + "\" > options " + index + " > choices " + index2 + " > name is required"); + } + if (!el.value) { + el.value = el.name; + } + if (el.name.length > 100) { + throw new Error("WOKCommands > Command \"" + names[0] + "\" > options " + index + " > choices " + index2 + " > the name is too long, only up to 100 Characters allowed"); + } + if (typeof (el.value) === "string" && el.value.length > 100) { + throw new Error("WOKCommands > Command \"" + names[0] + "\" > options " + index + " > choices " + index2 + " > the value in string format is too long, only up to 100 Characters allowed"); + } + } + else if (typeof element2 === "string") { + el = { name: element2, value: element2 }; + } + else if (typeof element2 === "number") { + var value = Math.round(element2); + if (value !== element2) { + console.warn("WOKCommands > Command \"" + names[0] + "\" > options " + index + " > choices " + index2 + " > the value is not na INTEGER so we round() it"); + } + el = { name: value.toString(), value: value }; + } + else { + throw new Error("WOKCommands > Command \"" + names[0] + "\" > options " + index + " > choices " + index2 + " > isn't in a suitable format, please provide a string or a correct object"); + } + return el; + }); + } + return element; // @ts-ignore + }); + } if (expectedArgs) { split = expectedArgs .substring(1, expectedArgs.length - 1) .split(/[>\]] [<\[]/); for (a = 0; a < split.length; ++a) { item = split[a]; - options.push({ - name: item.replace(/ /g, "-"), + option = { + name: item.replace(/ /g, "-").toLowerCase(), description: item, - type: 3, + // @ts-ignore + type: (options === null || options === void 0 ? void 0 : options[a]) ? options[a].type : slashCommands_1.getOptionFromName("STRING"), required: a < minArgs, - }); + }; + if ((options === null || options === void 0 ? void 0 : options[a]) && options[a].choices) { + option.choices = options[a].choices; + } + if ((options === null || options === void 0 ? void 0 : options[a]) && options[a].whole) { + slashCommands_1.setWhole(names[0], option.name); + } + if (!(option.name.match(/^[a-z0-9_-]{1,32}$/))) { + throw new Error("WOKCommands > Command \"" + names[0] + "\" has an unsuitable name for Slash Commands, it was already tried to replace \" \" with \"-\" and make everything lowercase!"); + } + // @ts-ignore + ApplicationOptions.push(option); } } if (!testOnly) return [3 /*break*/, 5]; @@ -424,7 +490,7 @@ var CommandHandler = /** @class */ (function () { case 1: if (!(_c < _d.length)) return [3 /*break*/, 4]; id = _d[_c]; - return [4 /*yield*/, slashCommands.create(names[0], description, options, id)]; + return [4 /*yield*/, slashCommands_1.editOrCreateCommand({ name: names[0], description: description, options: ApplicationOptions }, id)]; case 2: _f.sent(); _f.label = 3; @@ -432,7 +498,7 @@ var CommandHandler = /** @class */ (function () { _c++; return [3 /*break*/, 1]; case 4: return [3 /*break*/, 7]; - case 5: return [4 /*yield*/, slashCommands.create(names[0], description, options)]; + case 5: return [4 /*yield*/, slashCommands_1.editOrCreateCommand({ name: names[0], description: description, options: ApplicationOptions })]; case 6: _f.sent(); _f.label = 7; diff --git a/dist/SlashCommands.js b/dist/SlashCommands.js index a62ed1a..559bf49 100644 --- a/dist/SlashCommands.js +++ b/dist/SlashCommands.js @@ -51,26 +51,37 @@ var SlashCommands = /** @class */ (function () { function SlashCommands(instance, listen) { var _this = this; if (listen === void 0) { listen = true; } + this._whole = {}; this._instance = instance; this._client = instance.client; if (listen) { // @ts-ignore this._client.ws.on("INTERACTION_CREATE", function (interaction) { return __awaiter(_this, void 0, void 0, function () { - var member, data, guild_id, channel_id, name, options, command, guild, args, channel; + var member, data, guild_id, channel_id, type, user, Appdata, name, options, resolved, guild, args, channel; return __generator(this, function (_a) { - member = interaction.member, data = interaction.data, guild_id = interaction.guild_id, channel_id = interaction.channel_id; - name = data.name, options = data.options; - command = name.toLowerCase(); - guild = this._client.guilds.cache.get(guild_id); - args = this.getArrayFromOptions(guild, options); - channel = guild === null || guild === void 0 ? void 0 : guild.channels.cache.get(channel_id); - this.invokeCommand(interaction, command, args, member, guild, channel); - return [2 /*return*/]; + switch (_a.label) { + case 0: + member = interaction.member, data = interaction.data, guild_id = interaction.guild_id, channel_id = interaction.channel_id, type = interaction.type, user = interaction.user; + if (!(type === 1)) return [3 /*break*/, 2]; + return [4 /*yield*/, this.createInteractionResponse(interaction, 1)]; + case 1: + _a.sent(); + return [2 /*return*/]; + case 2: + Appdata = data; + name = Appdata.name, options = Appdata.options, resolved = Appdata.resolved; + guild = guild_id ? this._client.guilds.cache.get(guild_id) : undefined; + args = this.getArrayFromOptions(guild, name, options, resolved); + channel = channel_id ? guild === null || guild === void 0 ? void 0 : guild.channels.cache.get(channel_id) : undefined; + interaction.channel_type = user ? "DM" : "GUILD"; + this.invokeCommand(interaction, name, args, member, guild, channel, Appdata); + return [2 /*return*/]; + } }); }); }); } } - SlashCommands.prototype.get = function (guildId) { + SlashCommands.prototype.getCommands = function (guildId) { return __awaiter(this, void 0, void 0, function () { var app; return __generator(this, function (_a) { @@ -86,8 +97,7 @@ var SlashCommands = /** @class */ (function () { }); }); }; - SlashCommands.prototype.create = function (name, description, options, guildId) { - if (options === void 0) { options = []; } + SlashCommands.prototype.createCommand = function (data, guildId) { return __awaiter(this, void 0, void 0, function () { var app; return __generator(this, function (_a) { @@ -98,18 +108,14 @@ var SlashCommands = /** @class */ (function () { app.guilds(guildId); } return [4 /*yield*/, app.commands.post({ - data: { - name: name, - description: description, - options: options, - }, + data: data })]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }; - SlashCommands.prototype.delete = function (commandId, guildId) { + SlashCommands.prototype.deleteCommand = function (commandId, guildId) { return __awaiter(this, void 0, void 0, function () { var app; return __generator(this, function (_a) { @@ -125,17 +131,158 @@ var SlashCommands = /** @class */ (function () { }); }); }; + SlashCommands.prototype.getCommand = function (commandId, guildId) { + return __awaiter(this, void 0, void 0, function () { + var app; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + app = this._client.api.applications(this._client.user.id); + if (guildId) { + app.guilds(guildId); + } + return [4 /*yield*/, app.commands(commandId).get()]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.editCommand = function (commandId, data, guildId) { + return __awaiter(this, void 0, void 0, function () { + var app; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + app = this._client.api.applications(this._client.user.id); + if (guildId) { + app.guilds(guildId); + } + return [4 /*yield*/, app.commands(commandId).patch({ data: data })]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.editOrCreateCommand = function (data, guildId) { + return __awaiter(this, void 0, void 0, function () { + var AllCommands, isAlreadyThere; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.getCommands(guildId)]; + case 1: + AllCommands = _a.sent(); + isAlreadyThere = AllCommands.filter(function (command) { return data.name == command.name; }); + if (!(isAlreadyThere && isAlreadyThere.length > 0)) return [3 /*break*/, 3]; + return [4 /*yield*/, this.editCommand(isAlreadyThere[0].id, data, guildId)]; + case 2: return [2 /*return*/, _a.sent()]; + case 3: return [4 /*yield*/, this.createCommand(data, guildId)]; + case 4: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.isTheSame = function (data, data2) { + var _a; + var o = data.options; + var o2 = data2.options; + var options = (o === o2 || (o && o2 && o.length == o2.length && JSON.stringify(o) === JSON.stringify(o))); + return (_a = (data.name === data2.name && data.description === data2.description && options)) !== null && _a !== void 0 ? _a : false; + }; + //TODO if needed: Bulk Overwrite Global/Guild Application Commands: PUT/applications/{application.id}/commands + SlashCommands.prototype.getCommandsPermissions = function (guildId) { + return __awaiter(this, void 0, void 0, function () { + var app; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + app = this._client.api.applications(this._client.user.id).guilds(guildId); + return [4 /*yield*/, app.commands.permissions.get()]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.getCommandPermissions = function (commandId, guildId) { + return __awaiter(this, void 0, void 0, function () { + var app; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + app = this._client.api.applications(this._client.user.id).guilds(guildId); + return [4 /*yield*/, app.commands(commandId).permissions.get()]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.editCommandPermissions = function (commandId, data, guildId) { + return __awaiter(this, void 0, void 0, function () { + var app; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + app = this._client.api.applications(this._client.user.id); + if (guildId) { + app.guilds(guildId); + } + return [4 /*yield*/, app.commands(commandId).put({ data: data })]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + //TODO if necessary Batch Edit Application Command Permissions: PUT/applications/{application.id}/guilds/{guild.id}/commands/permissions // Checks if string is a user id, if true, returns a Guild Member object SlashCommands.prototype.getMemberIfExists = function (value, guild) { if (value && typeof value === "string" && - value.startsWith("<@!") && + (value.startsWith("<@!") || value.startsWith("<@")) && value.endsWith(">")) { - value = value.substring(3, value.length - 1); + value = value.substring((value.substring(2, 3) == "!" ? 3 : 2), value.length - 1); value = guild === null || guild === void 0 ? void 0 : guild.members.cache.get(value); } return value; }; + SlashCommands.prototype.setWhole = function (CommandName, ArgumentName) { + var _a; + if (!((_a = this._whole) === null || _a === void 0 ? void 0 : _a[CommandName])) { + this._whole[CommandName] = []; + } + this._whole[CommandName].push(ArgumentName); + }; + SlashCommands.prototype.isWhole = function (CommandName, ArgumentName) { + var _a; + if (((_a = this._whole) === null || _a === void 0 ? void 0 : _a[CommandName])) { + var isThere = this._whole[CommandName].find(function (element) { return element == ArgumentName; }); + return !!isThere; + } + return false; + }; + SlashCommands.prototype.detectType = function (value, resolved) { + var _a, _b, _c; + if (!value) { + return undefined; + } + else if ((_a = resolved === null || resolved === void 0 ? void 0 : resolved.users) === null || _a === void 0 ? void 0 : _a[value]) { + return "users"; + } + else if ((_b = resolved === null || resolved === void 0 ? void 0 : resolved.channels) === null || _b === void 0 ? void 0 : _b[value]) { + return "channels"; + } + else if ((_c = resolved === null || resolved === void 0 ? void 0 : resolved.roles) === null || _c === void 0 ? void 0 : _c[value]) { + return "roles"; + } + return undefined; + }; + SlashCommands.prototype.isMemberString = function (value) { + if (value && + typeof value === "string" && + (value.startsWith("<@!") || value.startsWith("<@")) && + value.endsWith(">")) { + return true; + } + return false; + }; SlashCommands.prototype.getObjectFromOptions = function (guild, options) { var args = {}; if (!options) { @@ -147,15 +294,85 @@ var SlashCommands = /** @class */ (function () { } return args; }; - SlashCommands.prototype.getArrayFromOptions = function (guild, options) { + SlashCommands.prototype.getArrayFromOptions = function (guild, CommandName, options, resolved) { + var _this = this; var args = []; if (!options) { return args; } - for (var _i = 0, options_2 = options; _i < options_2.length; _i++) { - var value = options_2[_i].value; - args.push(this.getMemberIfExists(value, guild)); - } + options.forEach(function (option, index) { + var _a, _b, _c, _d; + var name = option.name, type = option.type, value = option.value; + var isWhole = _this.isWhole(CommandName, name); + var result; + switch (type) { + case 1: + //TODO just give it up + result = ""; + break; + case 2: + //TODO just give it up + result = ""; + break; + case 3: + if (_this.isMemberString(value !== null && value !== void 0 ? value : "")) { + console.warn("WOKCommands > Use the types option to get some better user experience with avaible dropdown of the users etc, using string for users is deprecated"); + } + result = value; + break; + case 4: + result = value; + break; + case 5: + result = value; + break; + case 6: + if (!isWhole && value && ((_a = resolved === null || resolved === void 0 ? void 0 : resolved.users) === null || _a === void 0 ? void 0 : _a[value])) { + result = resolved.users[value]; + } + else if (guild) { + var user = guild.members.cache.get(value); + result = user !== null && user !== void 0 ? user : value; + } + break; + case 7: + if (!isWhole && value && ((_b = resolved === null || resolved === void 0 ? void 0 : resolved.channels) === null || _b === void 0 ? void 0 : _b[value])) { + result = resolved.channels[value]; + } + else if (guild) { + var channel = guild.channels.cache.get(value); + result = channel !== null && channel !== void 0 ? channel : value; + } + break; + case 8: + if (!isWhole && value && ((_c = resolved === null || resolved === void 0 ? void 0 : resolved.roles) === null || _c === void 0 ? void 0 : _c[value])) { + result = resolved.roles[value]; + } + else if (guild) { + var role = guild.roles.cache.get(value); + result = role !== null && role !== void 0 ? role : value; + } + break; + case 9: + var type_1 = _this.detectType(value, resolved); + // @ts-ignore + if (value && type_1 && ((_d = resolved === null || resolved === void 0 ? void 0 : resolved[type_1]) === null || _d === void 0 ? void 0 : _d[value])) { + // @ts-ignore + result = resolved[type_1][value]; + } + else if (guild) { + // @ts-ignore + var mentionable = guild[type_1].cache.get(value); + result = mentionable !== null && mentionable !== void 0 ? mentionable : value; + } + break; + default: + throw new Error("WOKCommands > FATAL ERROR, this SHOULDN'T HAPPEN EVER AT ALL, RUN FOREST RUN!!!"); + } + if (result) { + args.push(result); + } + }); return args; }; SlashCommands.prototype.createAPIMessage = function (interaction, content) { @@ -175,58 +392,315 @@ var SlashCommands = /** @class */ (function () { }); }); }; - SlashCommands.prototype.invokeCommand = function (interaction, commandName, options, member, guild, channel) { + SlashCommands.prototype.getInteractionResponseByToken = function (application_id, token) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.getInteractionResponse({ token: token, application_id: application_id })]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.deleteInteractionResponseByToken = function (application_id, token) { return __awaiter(this, void 0, void 0, function () { - var command, result, data, embed; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.deleteInteractionResponse({ token: token, application_id: application_id })]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.createInteractionResponse = function (interaction, type, data, ephemeral) { + return __awaiter(this, void 0, void 0, function () { + var Send; return __generator(this, function (_a) { switch (_a.label) { + case 0: + Send = { type: type }; + if (data && ephemeral) { + data.flags = 64; + } + Send.data = data; + return [4 /*yield*/, this._client.api + // @ts-ignore + .interactions(interaction.id, interaction.token) + .callback.post({ data: Send })]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.getInteractionResponse = function (interaction) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages["@original"].get()]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.editInteractionResponse = function (interaction, data) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages["@original"].patch({ data: data })]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + //ATTENTION, if the message is ephemeral you can't delete it, only the user who got the message can see and delete it!! + SlashCommands.prototype.deleteInteractionResponse = function (interaction) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages["@original"].delete()]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.createFollowupMessage = function (interaction, data, ephemeral) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (data && ephemeral) { + data.flags = 64; + } + return [4 /*yield*/, this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .post({ data: data })]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.editFollowupMessage = function (interaction, data, message) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages(message.id).patch({ data: data })]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + //ATTENTION, if the message is ephemeral you can't delete it, only the user who got the message can see and delete it!! + SlashCommands.prototype.deleteFollowupMessage = function (interaction, message) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages(message.id).delete()]; + case 1: + // @ts-ignore + return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + SlashCommands.prototype.invokeCommand = function (interaction, commandName, options, //parsed args + member, guild, channel, rawArgs) { + return __awaiter(this, void 0, void 0, function () { + var command, result, patch, embed, _a; + var _this = this; + return __generator(this, function (_b) { + switch (_b.label) { case 0: command = this._instance.commandHandler.getCommand(commandName); if (!command || !command.callback) { return [2 /*return*/, false]; } + interaction.status = {}; + interaction.delete = function () { return __awaiter(_this, void 0, void 0, function () { + var respond; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.deleteInteractionResponse(interaction)]; + case 1: + respond = _a.sent(); + interaction.status.deletet = true; + return [2 /*return*/, respond]; + } + }); + }); }; + interaction.loading = function () { return __awaiter(_this, void 0, void 0, function () { + var respond, respondMessage; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.createInteractionResponse(interaction, 5)]; + case 1: + respond = _a.sent(); + interaction.status.loaded = true; + return [4 /*yield*/, this.getInteractionResponse(interaction)]; + case 2: + respondMessage = _a.sent(); + return [2 /*return*/, respondMessage]; + } + }); + }); }; + interaction.reply = function (data) { return __awaiter(_this, void 0, void 0, function () { + var DataToSend, respond, DataToSend, respond, respondMessage; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!interaction.status.loaded) return [3 /*break*/, 2]; + DataToSend = void 0; + //TODO enable support for also passing an embed as data + if (typeof data === "string") { + DataToSend = { content: data }; + } + else { + DataToSend = data; + } + return [4 /*yield*/, this.editInteractionResponse(interaction, DataToSend)]; + case 1: + respond = _a.sent(); + interaction.status.send = true; + return [2 /*return*/, respond]; + case 2: + if (!!interaction.status.send) return [3 /*break*/, 5]; + DataToSend = void 0; + //TODO enable support for also passing an embed as data + if (typeof data === "string") { + DataToSend = { content: data }; + } + else { + DataToSend = data; + } + return [4 /*yield*/, this.createInteractionResponse(interaction, 4, DataToSend)]; + case 3: + respond = _a.sent(); + interaction.status.send = true; + return [4 /*yield*/, this.getInteractionResponse(interaction)]; + case 4: + respondMessage = _a.sent(); + return [2 /*return*/, respondMessage]; + case 5: + console.error("WOKCommands > Interaction \"" + interaction.id + "\" loaded and send the message already"); + return [2 /*return*/, Promise.reject("WOKCommands > Interaction \"" + interaction.id + "\" loaded and send the message already")]; + } + }); + }); }; + interaction.edit = function (data) { return __awaiter(_this, void 0, void 0, function () { + var DataToSend, respond; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + //TODO enable support for also passing an embed as data + if (typeof data === "string") { + DataToSend = { content: data }; + } + else { + DataToSend = data; + } + return [4 /*yield*/, this.editInteractionResponse(interaction, DataToSend)]; + case 1: + respond = _a.sent(); + interaction.status.send = true; + return [2 /*return*/, respond]; + } + }); + }); }; + interaction.followUpMessages = { create: this.createFollowupMessage, delete: this.deleteFollowupMessage, edit: this.editFollowupMessage }; return [4 /*yield*/, command.callback({ member: member, guild: guild, channel: channel, args: options, - // @ts-ignore - text: options.join ? options.join(" ") : "", + slash: true, + rawArgs: rawArgs, client: this._client, instance: this._instance, interaction: interaction, })]; case 1: - result = _a.sent(); - if (!result) { - console.error("WOKCommands > Command \"" + commandName + "\" did not return any content from it's callback function. This is required as it is a slash command."); + result = _b.sent(); + if (interaction.status.send) { + return [2 /*return*/, true]; + } + if (interaction.status.loaded) { + console.error("WOKCommands > Command \"" + commandName + "\" used loading, but not send, thats a mi of old and new methods, switch fully to the new ones to fix this"); + return [2 /*return*/, false]; + } + if (!result && !interaction.status.send) { + /* console.error( + `WOKCommands > Command "${commandName}" didn't send anything, and didn't return a value as fallback action` + ); */ return [2 /*return*/, false]; } - data = { - content: result, - }; + if (interaction.status.deletet && result) { + console.error("WOKCommands > Command \"" + commandName + "\" the interaction response was already deletet"); + return [2 /*return*/, false]; + } + if (result) { + console.warn("WOKCommands > Command \"" + commandName + "\" returned something from the callback, this is deprecated and will be removed later on"); + } + patch = {}; if (!(typeof result === "object")) return [3 /*break*/, 3]; embed = new discord_js_1.MessageEmbed(result); + // @ts-ignore + _a = patch; return [4 /*yield*/, this.createAPIMessage(interaction, embed)]; case 2: - data = _a.sent(); - _a.label = 3; - case 3: // @ts-ignore - this._client.api - // @ts-ignore - .interactions(interaction.id, interaction.token) - .callback.post({ - data: { - type: 4, - data: data, - }, - }); + _a.embeds = [(_b.sent())]; + return [3 /*break*/, 4]; + case 3: + patch.content = result; + _b.label = 4; + case 4: + this.createInteractionResponse(interaction, 4, patch); return [2 /*return*/, true]; } }); }); }; + SlashCommands.prototype.getOptionFromName = function (name) { + var _values = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + var response = 3; + var _names = ["SUB_COMMAND", "SUB_COMMAND_GROUP", "STRING", "INTEGER", "BOOLEAN", "USER", "CHANNEL", "ROLE", "MENTIONABLE"]; + _names.forEach(function (_name, i) { if (_name == name.toUpperCase()) { + response = _values[i]; + } }); + // @ts-ignore + return response; + }; return SlashCommands; }()); module.exports = SlashCommands; diff --git a/dist/commands/slash.js b/dist/commands/slash.js index dec62b5..7b83007 100644 --- a/dist/commands/slash.js +++ b/dist/commands/slash.js @@ -51,7 +51,7 @@ module.exports = { channel = options.channel, instance = options.instance, args = options.args; guild = channel.guild; slashCommands = instance.slashCommands; - return [4 /*yield*/, slashCommands.get()]; + return [4 /*yield*/, slashCommands.getCommands()]; case 1: global = _a.sent(); if (args.length && args[0] === "delete") { @@ -61,7 +61,7 @@ module.exports = { return [2 /*return*/]; } useGuild = global.filter(function (cmd) { return cmd.id === targetCommand_1; }).length === 0; - slashCommands.delete(targetCommand_1, useGuild ? guild.id : undefined); + slashCommands.deleteCommand(targetCommand_1, useGuild ? guild.id : undefined); if (useGuild) { channel.send("Slash command with the ID \"" + targetCommand_1 + "\" has been deleted from guild \"" + guild.id + "\""); } @@ -74,7 +74,7 @@ module.exports = { .addField("How to delete a slash command:", "_" + instance.getPrefix(guild) + "slash delete ") .addField("List of global slash commands:", global.length ? global.map(function (cmd) { return cmd.name + ": " + cmd.id; }) : "None"); if (!guild) return [3 /*break*/, 3]; - return [4 /*yield*/, slashCommands.get(guild.id)]; + return [4 /*yield*/, slashCommands.getCommands(guild.id)]; case 2: guildOnly = _a.sent(); embed.addField("List of slash commands for \"" + guild.name + "\" only", guildOnly.length diff --git a/dist/types/Interaction.js b/dist/types/Interaction.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/dist/types/Interaction.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/Command.ts b/src/Command.ts index e37944f..db63850 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -4,7 +4,8 @@ import WOKCommands from '.' import permissions from './permissions' import ICommand from './interfaces/ICommand' import cooldownSchema from './models/cooldown' - +import {InternalSlashCommandOptions +} from "./types/Interaction"; class Command { private instance: WOKCommands private client: Client @@ -32,7 +33,7 @@ class Command { private _guildOnly = false private _testOnly = false private _slash: boolean | string = false - + private _options:InternalSlashCommandOptions[] = [] constructor( instance: WOKCommands, client: Client, @@ -55,6 +56,7 @@ class Command { guildOnly = false, testOnly = false, slash = false, + options = [] }: ICommand ) { this.instance = instance @@ -84,6 +86,7 @@ class Command { this._callback = callback this._error = error this._slash = slash + this._options = Array.isArray(options) ? options : (options ? [options] : []) if (this.cooldown && this.globalCooldown) { throw new Error( @@ -470,6 +473,9 @@ class Command { public get slash(): boolean | string { return this._slash } + public get options(): InternalSlashCommandOptions[] { + return this._options + } } export = Command diff --git a/src/CommandHandler.ts b/src/CommandHandler.ts index 017331f..c950409 100644 --- a/src/CommandHandler.ts +++ b/src/CommandHandler.ts @@ -12,7 +12,13 @@ import cooldown from "./models/cooldown"; import { permissionList } from "./permissions"; import CommandErrors from "./enums/CommandErrors"; import Events from "./enums/Events"; - +import { + ApplicationCommandOption, + InternalSlashCommandOptions, + ApplicationCommandOptionChoice +} from "./types/Interaction"; +import SlashCommands from "./SlashCommands"; +import { SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION } from "constants"; class CommandHandler { private _commands: Map = new Map(); @@ -373,6 +379,7 @@ class CommandHandler { permissions, testOnly, slash, + options, expectedArgs, minArgs, } = configuration; @@ -462,9 +469,75 @@ class CommandHandler { `WOKCommands > Command "${names[0]}" has "minArgs" property defined without "expectedArgs" property as a slash command.` ); } - + if (options !== undefined && options.length===0) { + console.warn( + `WOKCommands > Command "${names[0]}" has "options" property defined but nothing in it` + ); + } const slashCommands = instance.slashCommands; - const options: object[] = []; + const ApplicationOptions: ApplicationCommandOption[] = []; + if(options){ + options.map((element:InternalSlashCommandOptions,index:number) => { + if(!element.type){ + element.type=slashCommands.getOptionFromName("STRING"); + }else if(typeof element.type ==="string"){ + element.type=slashCommands.getOptionFromName(element.type); + } + if(element.choices && !( element.type === 3 || element.type === 4 )){ + throw new Error( + `WOKCommands > Command "${names[0]}" > options ${index} > choices is only avaiable for STRING and INTEGER` + ); + }else if(element.choices){ + if(element.choices.length>25){ + throw new Error( + `WOKCommands > Command "${names[0]}" > options ${index} > choices is too long, Discord allows a maximum of 25 ` + ); + } + element.choices=element.choices.map((element2:any,index2:number)=>{ + let el:ApplicationCommandOptionChoice; + if(typeof element2 === "object"){ + el=element2; + if(!el.name){ + throw new Error( + `WOKCommands > Command "${names[0]}" > options ${index} > choices ${index2} > name is required` + ); + } + if(!el.value){ + el.value=el.name; + } + if(el.name.length>100){ + throw new Error( + `WOKCommands > Command "${names[0]}" > options ${index} > choices ${index2} > the name is too long, only up to 100 Characters allowed` + ); + } + if(typeof(el.value) === "string" && el.value.length>100){ + throw new Error( + `WOKCommands > Command "${names[0]}" > options ${index} > choices ${index2} > the value in string format is too long, only up to 100 Characters allowed` + ); + } + + }else if(typeof element2 === "string"){ + el={name:element2,value:element2}; + }else if(typeof element2 === "number"){ + let value=Math.round(element2) + if(value!==element2){ + console.warn( + `WOKCommands > Command "${names[0]}" > options ${index} > choices ${index2} > the value is not na INTEGER so we round() it` + ); + } + el={name:value.toString(),value}; + }else{ + throw new Error( + `WOKCommands > Command "${names[0]}" > options ${index} > choices ${index2} > isn't in a suitable format, please provide a string or a correct object` + ); + } + return el; + }) + + } + return element // @ts-ignore + }); + } if (expectedArgs) { const split = expectedArgs @@ -473,22 +546,35 @@ class CommandHandler { for (let a = 0; a < split.length; ++a) { const item = split[a]; - - options.push({ - name: item.replace(/ /g, "-"), + const option:ApplicationCommandOption = { + name: item.replace(/ /g, "-").toLowerCase(), description: item, - type: 3, + // @ts-ignore + type: options?.[a] ? options[a].type : slashCommands.getOptionFromName("STRING"), required: a < minArgs, - }); + } + if(options?.[a]&&options[a].choices){ + option.choices=options[a].choices; + } + if(options?.[a]&&options[a].whole){ + slashCommands.setWhole(names[0],option.name) + } + if(!(option.name.match(/^[a-z0-9_-]{1,32}$/))){ + throw new Error( + `WOKCommands > Command "${names[0]}" has an unsuitable name for Slash Commands, it was already tried to replace " " with "-" and make everything lowercase!` + ); + } + // @ts-ignore + ApplicationOptions.push(option); } } if (testOnly) { for (const id of instance.testServers) { - await slashCommands.create(names[0], description, options, id); + await slashCommands.editOrCreateCommand({name:names[0], description, options:ApplicationOptions}, id); } } else { - await slashCommands.create(names[0], description, options); + await slashCommands.editOrCreateCommand({name:names[0], description, options:ApplicationOptions}); } } @@ -571,6 +657,7 @@ class CommandHandler { public getICommand(name: string): ICommand | undefined { return this.commands.find((command) => command.names.includes(name)); + } public async fetchDisabledCommands() { diff --git a/src/SlashCommands.ts b/src/SlashCommands.ts index 47eb5d2..a462e4b 100644 --- a/src/SlashCommands.ts +++ b/src/SlashCommands.ts @@ -1,39 +1,60 @@ import { APIMessage, - APIMessageContentResolvable, + Message, Channel, Client, Guild, GuildMember, MessageEmbed, + Snowflake } from "discord.js"; import WOKCommands from "."; -import ISlashCommand from "./interfaces/ISlashCommand"; - +import {Interaction, + InteractionApplicationCommandCallbackData, + ApplicationCommandInteractionData, + ApplicationCommandInteractionDataOption, + InteractionResponse,InteractionCallbackType, + EditWebhookMessage, + ExecuteWebhook, + ApplicationCommand, + ApplicationCommandOptionType_value, + ApplicationCommandSend, + GuildApplicationCommandPermissions, + ApplicationCommandPermissions, + ApplicationCommandInteractionDataResolved, + WholeStorage +} from "./types/Interaction"; class SlashCommands { private _client: Client; private _instance: WOKCommands; - + private _whole:WholeStorage= {}; constructor(instance: WOKCommands, listen = true) { this._instance = instance; this._client = instance.client; if (listen) { // @ts-ignore - this._client.ws.on("INTERACTION_CREATE", async (interaction) => { - const { member, data, guild_id, channel_id } = interaction; - const { name, options } = data; - - const command = name.toLowerCase(); - const guild = this._client.guilds.cache.get(guild_id); - const args = this.getArrayFromOptions(guild, options); - const channel = guild?.channels.cache.get(channel_id); - this.invokeCommand(interaction, command, args, member, guild, channel); + this._client.ws.on("INTERACTION_CREATE", async (interaction:Interaction) => { + const { member, data, guild_id, channel_id,type,user } = interaction; + //type === 1 is the ping request from discord, I don't know if discord ever makes one, but if so, we respond automatically + if(type===1){ + await this.createInteractionResponse(interaction,1) + return; + } + // if type !== 1 data is always present!! + const Appdata:ApplicationCommandInteractionData=data!!; + const { name, options ,resolved} = Appdata; + //console.log(Appdata) + const guild = guild_id?this._client.guilds.cache.get(guild_id):undefined; + const args = this.getArrayFromOptions(guild,name,options,resolved); + const channel = channel_id?guild?.channels.cache.get(channel_id):undefined; + interaction.channel_type=user?"DM":"GUILD"; + this.invokeCommand(interaction, name, args, member, guild, channel,Appdata); }); } } - public async get(guildId?: string): Promise { + public async getCommands(guildId?: string): Promise { // @ts-ignore const app = this._client.api.applications(this._client.user.id); if (guildId) { @@ -43,12 +64,10 @@ class SlashCommands { return await app.commands.get(); } - public async create( - name: string, - description: string, - options: Object[] = [], + public async createCommand( + data:ApplicationCommandSend, guildId?: string - ): Promise { + ): Promise { // @ts-ignore const app = this._client.api.applications(this._client.user.id); if (guildId) { @@ -56,15 +75,11 @@ class SlashCommands { } return await app.commands.post({ - data: { - name, - description, - options, - }, + data }); } - public async delete(commandId: string, guildId?: string): Promise { + public async deleteCommand(commandId: string, guildId?: string): Promise { // @ts-ignore const app = this._client.api.applications(this._client.user.id); if (guildId) { @@ -74,58 +89,226 @@ class SlashCommands { return await app.commands(commandId).delete(); } + public async getCommand(commandId: string,guildId?: string): Promise { + // @ts-ignore + const app = this._client.api.applications(this._client.user.id); + if (guildId) { + app.guilds(guildId); + } + return await app.commands(commandId).get(); + } + + public async editCommand(commandId: string,data:ApplicationCommandSend,guildId?: string): Promise { + // @ts-ignore + const app = this._client.api.applications(this._client.user.id); + if (guildId) { + app.guilds(guildId); + } + return await app.commands(commandId).patch({data}); + } + public async editOrCreateCommand(data:ApplicationCommandSend,guildId?: string): Promise { + const AllCommands = await this.getCommands(guildId) + const isAlreadyThere = AllCommands.filter((command) => data.name==command.name); + /* if(this.isTheSame(isAlreadyThere[0],data)){ + return Promise.reject("Exactly same exists already"); + } */ + if(isAlreadyThere&&isAlreadyThere.length>0){ + return await this.editCommand(isAlreadyThere[0].id,data,guildId) + }else{ + return await this.createCommand(data,guildId) + } + } + + private isTheSame(data:ApplicationCommand,data2:ApplicationCommandSend): boolean { + let o=data.options; + let o2=data2.options; + let options= (o===o2 || (o&&o2 && o.length==o2.length && JSON.stringify(o)===JSON.stringify(o))) + return (data.name===data2.name && data.description===data2.description && options)??false; + } + //TODO if needed: Bulk Overwrite Global/Guild Application Commands: PUT/applications/{application.id}/commands + + public async getCommandsPermissions(guildId: string): Promise { + // @ts-ignore + const app = this._client.api.applications(this._client.user.id).guilds(guildId); + return await app.commands.permissions.get(); + } + + public async getCommandPermissions(commandId: string,guildId: string): Promise { + // @ts-ignore + const app = this._client.api.applications(this._client.user.id).guilds(guildId); + return await app.commands(commandId).permissions.get(); + } + + public async editCommandPermissions(commandId: string,data:[ApplicationCommandPermissions],guildId: string): Promise { + // @ts-ignore + const app = this._client.api.applications(this._client.user.id); + if (guildId) { + app.guilds(guildId); + } + return await app.commands(commandId).put({data}); + } + + //TODO if necessary Batch Edit Application Command Permissions: PUT/applications/{application.id}/guilds/{guild.id}/commands/permissions + // Checks if string is a user id, if true, returns a Guild Member object private getMemberIfExists(value: string, guild: any) { if ( value && typeof value === "string" && - value.startsWith("<@!") && + (value.startsWith("<@!") || value.startsWith("<@")) && value.endsWith(">") ) { - value = value.substring(3, value.length - 1); + value = value.substring((value.substring(2,3)=="!"?3:2), value.length - 1); value = guild?.members.cache.get(value); } - return value; } + public setWhole(CommandName:string,ArgumentName:string){ + if(!(this._whole?.[CommandName])){ + this._whole[CommandName]=[] + } + this._whole[CommandName].push(ArgumentName) + } + + private isWhole(CommandName:string,ArgumentName:string):boolean{ + if((this._whole?.[CommandName])){ + let isThere=this._whole[CommandName].find((element:string)=>{return element==ArgumentName}) + return !!isThere + } + return false; + } + + private detectType(value: string|undefined, resolved: ApplicationCommandInteractionDataResolved | undefined):string|undefined { + if(!value){ + return undefined; + }else if(resolved?.users?.[value]){ + return "users" + }else if(resolved?.channels?.[value]){ + return "channels" + }else if(resolved?.roles?.[value]){ + return "roles" + } + return undefined; + } + + private isMemberString(value: string):boolean{ + if ( + value && + typeof value === "string" && + (value.startsWith("<@!") || value.startsWith("<@")) && + value.endsWith(">") + ) { + return true; + } + return false; + } + public getObjectFromOptions( guild: { members: { cache: any } }, - options?: { name: string; value: string }[] + options?: ApplicationCommandInteractionDataOption[] ): Object { const args: { [key: string]: any } = {}; if (!options) { return args; } - for (const { name, value } of options) { - args[name] = this.getMemberIfExists(value, guild); + args[name] = this.getMemberIfExists(value!!, guild); } return args; } public getArrayFromOptions( - guild: { members: { cache: any } } | undefined, - options?: { name: string; value: string }[] - ): string[] { - const args: string[] = []; + guild: { members: { cache: any } ,channels: { cache: any },roles: { cache: any }} | undefined, + CommandName:string, + options?: ApplicationCommandInteractionDataOption[], + resolved?:ApplicationCommandInteractionDataResolved, + ): any[] { + const args: any[] = []; if (!options) { return args; } - - for (const { value } of options) { - args.push(this.getMemberIfExists(value, guild)); + options.forEach((option:ApplicationCommandInteractionDataOption,index:number)=>{ + const {name,type,value} = option; + const isWhole:boolean = this.isWhole(CommandName,name) + let result; + switch(type) { + case 1: + //TODO just give it up + result="" + break; + case 2: + //TODO just give it up + result="" + break; + case 3: + if(this.isMemberString(value??"")){ + console.warn( + `WOKCommands > Use the types option to get some better user experience with avaible dropdown of the users etc, using string for users is deprecated` + ); + } + result=value; + break; + case 4: + result=value + break; + case 5: + result=value; + break; + case 6: + if(!isWhole&&value&&resolved?.users?.[value]){ + result=resolved.users[value]; + }else if(guild){ + let user=guild.members.cache.get(value); + result=user??value; + } + break; + case 7: + if(!isWhole&&value&&resolved?.channels?.[value]){ + result=resolved.channels[value]; + }else if(guild){ + let channel=guild.channels.cache.get(value); + result=channel??value; + } + break; + case 8: + if(!isWhole&&value&&resolved?.roles?.[value]){ + result=resolved.roles[value]; + }else if(guild){ + let role=guild.roles.cache.get(value); + result=role??value; + } + break; + case 9: + let type=this.detectType(value,resolved) + // @ts-ignore + if(value&&type&&resolved?.[type]?.[value]){ + // @ts-ignore + result=resolved[type][value]; + }else if(guild){ + // @ts-ignore + let mentionable=guild[type].cache.get(value); + result=mentionable??value; + } + break; + default: + throw new Error( + `WOKCommands > FATAL ERROR, this SHOULDN'T HAPPEN EVER AT ALL, RUN FOREST RUN!!!` + ); + } + if(result){ + args.push(result); } - + }); return args; } public async createAPIMessage( - interaction: APIMessageContentResolvable, + interaction: Interaction, content: any - ) { + ):Promise { const { data, files } = await APIMessage.create( // @ts-ignore this._client.channels.resolve(interaction.channel_id), @@ -137,62 +320,212 @@ class SlashCommands { return { ...data, files }; } + public async getInteractionResponseByToken(application_id: Snowflake,token:string): Promise { + // @ts-ignore + return await this.getInteractionResponse({token,application_id}) + } + public async deleteInteractionResponseByToken(application_id: Snowflake,token:string): Promise { + // @ts-ignore + return await this.deleteInteractionResponse({token,application_id}) + } + + private async createInteractionResponse(interaction: Interaction, type: InteractionCallbackType,data?: InteractionApplicationCommandCallbackData,ephemeral?:boolean): Promise { + let Send:InteractionResponse={type} + if(data&&ephemeral){ + data.flags=64; + } + Send.data=data; + // @ts-ignore + return await this._client.api + // @ts-ignore + .interactions(interaction.id, interaction.token) + .callback.post({data:Send}); + } + private async getInteractionResponse(interaction: Interaction): Promise { + // @ts-ignore + return await this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages["@original"].get(); + } + private async editInteractionResponse(interaction: Interaction, data: EditWebhookMessage): Promise { + // @ts-ignore + return await this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages["@original"].patch({data}); + } + //ATTENTION, if the message is ephemeral you can't delete it, only the user who got the message can see and delete it!! + private async deleteInteractionResponse(interaction: Interaction): Promise { + // @ts-ignore + return await this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages["@original"].delete(); + } + private async createFollowupMessage(interaction: Interaction, data:ExecuteWebhook,ephemeral?:boolean): Promise { + if(data&&ephemeral){ + data.flags=64; + } + // @ts-ignore + return await this._client.api + // @ts-ignore + .webhooks(interaction.application_id,interaction.token) + .post({data}); + } + private async editFollowupMessage(interaction: Interaction, data: EditWebhookMessage, message:Message): Promise { + // @ts-ignore + return await this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages(message.id).patch({data}); + } + //ATTENTION, if the message is ephemeral you can't delete it, only the user who got the message can see and delete it!! + private async deleteFollowupMessage(interaction: Interaction, message:Message): Promise { + // @ts-ignore + return await this._client.api + // @ts-ignore + .webhooks(interaction.application_id, interaction.token) + .messages(message.id).delete(); + } + public async invokeCommand( - interaction: APIMessageContentResolvable, + interaction: Interaction, commandName: string, - options: object, - member: GuildMember, + options: object, //parsed args + member: GuildMember | undefined, guild: Guild | undefined, - channel: Channel | undefined + channel: Channel | undefined, + rawArgs: ApplicationCommandInteractionData ): Promise { const command = this._instance.commandHandler.getCommand(commandName); if (!command || !command.callback) { return false; } + interaction.status={}; + interaction.delete = async ():Promise=>{ + let respond = await this.deleteInteractionResponse(interaction) + interaction.status.deletet=true; + return respond; + } + interaction.loading = async ():Promise=>{ + let respond = await this.createInteractionResponse(interaction,5) + interaction.status.loaded=true; + let respondMessage = await this.getInteractionResponse(interaction) + return respondMessage; + } + interaction.reply=async(data:InteractionApplicationCommandCallbackData | string):Promise=>{ + if(interaction.status.loaded){ + let DataToSend:EditWebhookMessage ; + //TODO enable support for also passing an embed as data + if (typeof data === "string") { + DataToSend={content:data} + }else{ + DataToSend=data + } + let respond = await this.editInteractionResponse(interaction,DataToSend) + interaction.status.send=true; + return respond; + }else if(!interaction.status.send){ + let DataToSend:InteractionApplicationCommandCallbackData ; + //TODO enable support for also passing an embed as data + if (typeof data === "string") { + DataToSend={content:data} + }else{ + DataToSend=data + } + let respond = await this.createInteractionResponse(interaction,4,DataToSend) + interaction.status.send=true; + let respondMessage = await this.getInteractionResponse(interaction) + return respondMessage; + }else{ + console.error( + `WOKCommands > Interaction "${interaction.id}" loaded and send the message already` + ); + return Promise.reject(`WOKCommands > Interaction "${interaction.id}" loaded and send the message already`); + } + } + interaction.edit=async(data:InteractionApplicationCommandCallbackData | string):Promise=>{ + let DataToSend:EditWebhookMessage ; + //TODO enable support for also passing an embed as data + if (typeof data === "string") { + DataToSend={content:data} + }else{ + DataToSend=data + } + let respond = await this.editInteractionResponse(interaction,DataToSend) + interaction.status.send=true; + return respond; + } + + interaction.followUpMessages={create:this.createFollowupMessage,delete:this.deleteFollowupMessage,edit:this.editFollowupMessage}; let result = await command.callback({ member, guild, channel, args: options, - // @ts-ignore - text: options.join ? options.join(" ") : "", + slash:true, + rawArgs, client: this._client, instance: this._instance, interaction, }); - if (!result) { + if(interaction.status.send){ + return true; + } + if(interaction.status.loaded){ + console.error( + `WOKCommands > Command "${commandName}" used loading, but not send, thats a mi of old and new methods, switch fully to the new ones to fix this` + ); + return false; + } + + if (!result&&!interaction.status.send) { + /* console.error( + `WOKCommands > Command "${commandName}" didn't send anything, and didn't return a value as fallback action` + ); */ + return false; + } + + if(interaction.status.deletet&&result){ console.error( - `WOKCommands > Command "${commandName}" did not return any content from it's callback function. This is required as it is a slash command.` + `WOKCommands > Command "${commandName}" the interaction response was already deletet` ); return false; } - let data: any = { - content: result, - }; + if (result) { + console.warn( + `WOKCommands > Command "${commandName}" returned something from the callback, this is deprecated and will be removed later on` + ); + } + let patch: InteractionApplicationCommandCallbackData = {} // Handle embeds if (typeof result === "object") { const embed = new MessageEmbed(result); - data = await this.createAPIMessage(interaction, embed); + // @ts-ignore + patch.embeds = [(await this.createAPIMessage(interaction, embed))]; + }else{ + patch.content= result; } + this.createInteractionResponse(interaction,4,patch) + return true; + } + public getOptionFromName(name:string):ApplicationCommandOptionType_value{ + const _values = [1,2,3,4,5,6,7,8,9]; + let response=3; + const _names = ["SUB_COMMAND","SUB_COMMAND_GROUP", "STRING","INTEGER","BOOLEAN","USER","CHANNEL","ROLE" ,"MENTIONABLE"] + _names.forEach((_name,i)=>{if(_name==name.toUpperCase()){response=_values[i];}}) // @ts-ignore - this._client.api - // @ts-ignore - .interactions(interaction.id, interaction.token) - .callback.post({ - data: { - type: 4, - data, - }, - }); - - return true; + return response; } } + + export = SlashCommands; diff --git a/src/commands/slash.ts b/src/commands/slash.ts index 3c47216..81d305b 100644 --- a/src/commands/slash.ts +++ b/src/commands/slash.ts @@ -14,7 +14,7 @@ export = { const { guild } = channel; const { slashCommands } = instance; - const global = await slashCommands.get(); + const global = await slashCommands.getCommands(); if (args.length && args[0] === "delete") { const targetCommand = args[1]; @@ -26,7 +26,7 @@ export = { const useGuild = global.filter((cmd) => cmd.id === targetCommand).length === 0; - slashCommands.delete(targetCommand, useGuild ? guild.id : undefined); + slashCommands.deleteCommand(targetCommand, useGuild ? guild.id : undefined); if (useGuild) { channel.send( @@ -51,7 +51,7 @@ export = { ); if (guild) { - const guildOnly = await slashCommands.get(guild.id); + const guildOnly = await slashCommands.getCommands(guild.id); embed.addField( `List of slash commands for "${guild.name}" only`, diff --git a/src/interfaces/ICommand.ts b/src/interfaces/ICommand.ts index 13013db..1b5f2ae 100644 --- a/src/interfaces/ICommand.ts +++ b/src/interfaces/ICommand.ts @@ -1,6 +1,9 @@ -import { Client, Message, PermissionString, TextChannel } from 'discord.js' +import { Client, Message, PermissionString, TextChannel,GuildMember,Guild } from 'discord.js' import WOKCommands from '..' - +import {InternalSlashCommandOptions, + ApplicationCommandInteractionData, + Interaction + } from "../types/Interaction"; export default interface ICommand { names: string[] | string category: string @@ -14,12 +17,18 @@ export default interface ICommand { permissions?: PermissionString[] callback?: { message: Message - channel: TextChannel + channel?: TextChannel args: string[] - text: string + text?: string client: Client - prefix: string + prefix?: string instance: WOKCommands + rawArgs?:ApplicationCommandInteractionData + slash?:boolean + interaction?: Interaction + member?: GuildMember + guild?: Guild + } cooldown?: string globalCooldown?: string @@ -28,4 +37,5 @@ export default interface ICommand { guildOnly?: boolean testOnly?: boolean slash?: boolean | 'both' + options?:InternalSlashCommandOptions[] } diff --git a/src/interfaces/ISlashCommand.ts b/src/interfaces/ISlashCommand.ts deleted file mode 100644 index 9c2ebff..0000000 --- a/src/interfaces/ISlashCommand.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default interface ISlashCommand { - id: string; - application_id: string; - name: string; - description: string; - version: string; - default_permission: boolean; -} diff --git a/src/types/Interaction.ts b/src/types/Interaction.ts new file mode 100644 index 0000000..f6de5f3 --- /dev/null +++ b/src/types/Interaction.ts @@ -0,0 +1,185 @@ +import { Snowflake,User,MessageMentionOptions,MessageEmbed ,GuildMember,Role,Channel,MessageAttachment,FileOptions,Message} from "discord.js"; +//Discord.js doesn't support these types, so they had to be added, note not all types are fully tested +export type ApplicationCommandInteractionData = { + id:Snowflake; + name:string; + resolved?:ApplicationCommandInteractionDataResolved; + options?:ApplicationCommandInteractionDataOption[]; + +} + +export type ApplicationCommandInteractionDataResolved = { + users?:{[ID:string]:User}; + members?:{[ID:string]:GuildMember}; //Partial Member objects are missing user, deaf and mute fields + roles?:{[ID:string]:Role}; + channels?:{[ID:string]:Channel}; // Partial Channel objects only have id, name, type and permissions fields +} + + + +export type ApplicationCommandOptionType_value = 1 | 2 | 3 | 4 | 5 |6 | 7 | 8 | 9; +export type ApplicationCommandOptionType_name = ("SUB_COMMAND" | + "SUB_COMMAND_GROUP" | + "STRING" | + "INTEGER" | + "BOOLEAN" | + "USER" | + "CHANNEL" | + "ROLE" | + "MENTIONABLE"); ///ATTENTION FOR DOCUMENTATION, SUB_COMMAND AND SUB_COMMAND_GROUP ARE COMPLEX AND NOT MUCH USED; IF YOU USE IT YOU LIKELY GET SOME DISCORD API ERRORS, THUS AVOID IT IF NOT ABSOLUTELY NECESSARY + +export type ApplicationCommandOptionType = { + name: ApplicationCommandOptionType_name, + value:ApplicationCommandOptionType_value +} + +export type ApplicationCommandInteractionDataOption = { +name:string; +type:ApplicationCommandOptionType_value; +value?:string; +options?:ApplicationCommandInteractionDataOption[]; //its a recursive loop, that results in a mess, but its only set with SUB_COMMAND /-_GROUP +} + +export type InteractionType = 1 | 2; //1 = Ping, 2 = ApplicationCommand + + +export type ChannelType = "DM" | "GUILD"; + +export type Interaction = { + id: Snowflake; + application_id: Snowflake; + type: InteractionType; + data?: ApplicationCommandInteractionData; + guild_id?:Snowflake; + channel_id?:Snowflake; + member?:GuildMember; + user?:User; + token:string; + version:number; + + //custom properties + channel_type:ChannelType; + status:{ + loaded?:boolean; + send?:boolean; + deletet?:boolean; + }; + reply?(data:InteractionApplicationCommandCallbackData| string): Promise; + loading?(): Promise; + edit?(data:InteractionApplicationCommandCallbackData| string): Promise; + delete?():Promise; + followUpMessages?:{ + create(interaction: Interaction, data:ExecuteWebhook,ephemeral?:boolean):Promise; + delete(interaction: Interaction, message:Message):Promise; + edit(interaction: Interaction, data: EditWebhookMessage, message:Message):Promise; + } +} + +export type InteractionApplicationCommandCallbackData = { + tts?:boolean; + content?:string; + embeds?:MessageEmbed[]; //max 10 !!! + allowed_mentions?:MessageMentionOptions; + flags?:number; + + +} + +export type InteractionCallbackType = 1 | 4 | 5; // 1 Pong, 4 = ChannelMessageWithSource, 5 = DeferredChannelMessageWithSource; +export type InteractionResponse = { + type:InteractionCallbackType; + data?:InteractionApplicationCommandCallbackData; +} + + +export type EditWebhookMessage = { + content?:string; + embeds?:MessageEmbed[]; //max 10 !!! + file?:(MessageAttachment | FileOptions | string); + payload_json?:string; + allowed_mentions?:MessageMentionOptions; + attachments?:MessageAttachment[]; +} + + +export type ExecuteWebhook = { + content?:string; + username?:string; + avatar_url?:string; + tts?:boolean; + embeds?:MessageEmbed[]; //max 10 !!! + file?:(MessageAttachment | FileOptions | string); + payload_json?:string; + allowed_mentions?:MessageMentionOptions; + flags?:number; +} + + +export type ApplicationCommand = { + id: Snowflake, + application_id: Snowflake, + name:string, //1-32 lowercase character name matching ^[\w-]{1,32}$ + description: string, + options?:ApplicationCommandOption[], //max Length = 25 + default_permission?: boolean +} + +export type ApplicationCommandSend = { + name:string, + description: string, + options?:ApplicationCommandOption[], //max Length = 25 + default_permission?: boolean +} + + + +export type ApplicationCommandOption = { + type:ApplicationCommandOptionType_value; + name:string, + description:string, + required?:boolean, + choices?:ApplicationCommandOptionChoice[], // max 25 + options?:ApplicationCommandOption[] +} + +export type ApplicationCommandOptionChoice = { + name:string, //max length = 100 + value:string | number // if string 100 = max Length +} + +export type GuildApplicationCommandPermissions = { + id:Snowflake, + application_id:Snowflake, + guild_id:Snowflake, + permissions:ApplicationCommandPermissions[] +} + +export type ApplicationCommandPermissions = { + id:Snowflake, + type:ApplicationCommandPermissionType, + permission:boolean //hasPermission (true has it, or false does not have it) +} + +export type ApplicationCommandPermissionType = { + name:"ROLE" | "USER", + value:1 |2 +} + +export type MessageInteraction = { + id:Snowflake, + type:InteractionType, + name:string, + user:GuildMember + +} + + +export type InternalSlashCommandOptions = { + type?:ApplicationCommandOptionType_value|ApplicationCommandOptionType_name|string, //lowercase is also parsed, if its invalid its going to be a string + choices?:ApplicationCommandOptionChoice[], + whole?:boolean +} + +export type WholeStorage = { + [CommandName:string]:string[]; +}