From 410473ecf86b36473c57c6f079994ae6ca9cbf68 Mon Sep 17 00:00:00 2001 From: ION606 Date: Sat, 8 Apr 2023 16:33:58 -0400 Subject: [PATCH] added message select menus --- structures/client/client.js | 7 ++ structures/guilds/Channel.js | 12 +-- structures/interactions/Button.js | 32 ++++++ structures/interactions/ButtonStyles.js | 7 ++ structures/interactions/StringSelectMenu.js | 112 ++++++++++++++++++++ structures/messages/MessageActionRow.js | 20 ++++ structures/messages/message.js | 78 +++++++++----- tests/Buttontests.js | 32 ++++++ tests/test.js | 4 + 9 files changed, 274 insertions(+), 30 deletions(-) create mode 100644 structures/interactions/ButtonStyles.js create mode 100644 structures/interactions/StringSelectMenu.js create mode 100644 structures/messages/MessageActionRow.js create mode 100644 tests/Buttontests.js diff --git a/structures/client/client.js b/structures/client/client.js index f17ee80..989cc7f 100644 --- a/structures/client/client.js +++ b/structures/client/client.js @@ -150,6 +150,13 @@ export class Client extends EventEmitter { baseURL: "https://discord.com/api/", headers: { Authorization: token } }); + + this.axiosCustom.interceptors.response.use((response) => { + return response; + }, function (err) { + // console.log(err); + throw `REQUEST FAILED WITH STATUS CODE ${err.response.status} AND REASON "${err.response.data.message}"`; + }); return new Promise((resolve, reject) => { this.ws = new WebSocket("wss://gateway.discord.gg/?v=10&encoding=json"); diff --git a/structures/guilds/Channel.js b/structures/guilds/Channel.js index f4fecff..859a2b8 100644 --- a/structures/guilds/Channel.js +++ b/structures/guilds/Channel.js @@ -65,7 +65,7 @@ export class Channel extends DataManager { */ async send(inp) { return new Promise(async (resolve) => { - const toSend = (typeof inp == 'string') ? inp : inp.content; + const content = (typeof inp == 'string') ? inp : inp.content; var embds = undefined; if (inp.embeds) { @@ -75,11 +75,11 @@ export class Channel extends DataManager { } } - const response = await this.client.axiosCustom.post(`/channels/${this.id}/messages`, { - content: toSend, - message_reference: inp.message_reference || undefined, - embeds: embds - }); + const toSend = (typeof inp == 'string') ? { content: inp } : structuredClone(inp); + toSend["embeds"] = embds; + toSend["message_reference"] = inp.message_reference || undefined; + + const response = await this.client.axiosCustom.post(`/channels/${this.id}/messages`, toSend); resolve(new message(response.data, this.client)); }); diff --git a/structures/interactions/Button.js b/structures/interactions/Button.js index 1df8a22..8f7ada5 100644 --- a/structures/interactions/Button.js +++ b/structures/interactions/Button.js @@ -1,4 +1,36 @@ +// https://github.com/discordjs/discord-api-types/blob/main/rest/v10/index.ts +// https://discord.com/developers/docs/interactions/message-components +import {MessageButtonStyles} from './ButtonStyles.js'; export class Button { + /** @type {MessageButtonStyles} */ + style; + + /** @type {String} */ + label; + + /** @type {{name: String, id: String, animated: Boolean}} */ + emoji; + + /** @type {String} */ + custom_id; + + /** @type {String} */ + url; + + /** @type {String} */ + label; + + /** @type {Boolean} */ + disabled; + + toObj() { + var obj = {type: 2}; + for (const i in this) { + obj[i] = this[i]; + } + return obj; + } + constructor() {} } \ No newline at end of file diff --git a/structures/interactions/ButtonStyles.js b/structures/interactions/ButtonStyles.js new file mode 100644 index 0000000..626d356 --- /dev/null +++ b/structures/interactions/ButtonStyles.js @@ -0,0 +1,7 @@ +export const MessageButtonStyles = Object.freeze({ + PRIMARY: 1, + SECONDARY: 2, + SUCCESS: 3, + DANGER: 4, + LINK: 5 +}); \ No newline at end of file diff --git a/structures/interactions/StringSelectMenu.js b/structures/interactions/StringSelectMenu.js new file mode 100644 index 0000000..d4f88f4 --- /dev/null +++ b/structures/interactions/StringSelectMenu.js @@ -0,0 +1,112 @@ +import {messageChannelTypes} from '../messages/messageChannelTypes.js'; + +export class StringMenuComponent { + /** @type {String} */ + label; + + /** @type {String} */ + value; + + /** @type {String} */ + description; + + /** @type {{id: String, name: String, animated: Boolean}} */ + emoji; + + /** @type {Boolean} */ + default; + + toJSON() { + const obj = {}; + for (const k in this) { + obj[k] = this[k]; + } + return obj; + } +} + +export const SelectMenuTypes = Object.freeze({ + TEXT: 3, + USER: 5, + ROLE: 6, + MENTIONABLE: 7, + CHANNELS: 8 +}); + + +class BaseSelectMenu { + #type; + + /** @type {String} */ + custom_id; + + /** @type {Boolean} */ + disabled; + + /** @type {Number} */ + min_values; + + /** @type {Number} */ + max_values; + + /** @type {String} */ + placeholder; + + toObj() { + if (!this.custom_id) throw "PLEASE ENTER A CUSTOM ID!"; + var obj = { type: this.#type }; + for (const k in this) { + obj[k] = this[k]; + } + + return obj; + } + + constructor(type) { this.#type = type; this.min_values = 1; this.max_values = 1; } +} + +export class StringSelectMenu extends BaseSelectMenu { + /** @type {StringMenuComponent[]} */ + options; + + toObj() { + const obj = super.toObj(); + obj["options"] = []; + for (const k of this.options) { + obj["options"].push(k.toJSON()); + } + return obj; + } + + constructor() { super(SelectMenuTypes.TEXT); this.options = []; } +} + +export class ChannelSelectMenu extends BaseSelectMenu { + /** @type { messageChannelTypes[] } */ + options; + + toObj() { + const obj = super.toObj(); + obj["channel_types"] = []; + for (const k of this.options) { + obj["channel_types"].push(k); + } + } + + constructor() { super(SelectMenuTypes.CHANNELS); this.options = []; } +} + + +export class userSelectMenu extends BaseSelectMenu { + constructor() { super(SelectMenuTypes.USER); } +} + + +export class RoleSelectMenu extends BaseSelectMenu { + constructor() { super(SelectMenuTypes.ROLE); } +} + + +export class MentionableSelectMenu extends BaseSelectMenu { + constructor() { super(SelectMenuTypes.MENTIONABLE); } +} \ No newline at end of file diff --git a/structures/messages/MessageActionRow.js b/structures/messages/MessageActionRow.js new file mode 100644 index 0000000..1e61017 --- /dev/null +++ b/structures/messages/MessageActionRow.js @@ -0,0 +1,20 @@ + +export class MessageActionRow { + /** @type {Object[]} */ + components; + + addComponent(comp) { + if (this.components.length > 5) throw "MAXIMUM SIZE REACHED!"; + this.components.push(comp); + } + + toObj() { + const o = {type: 1, components: []}; + for (const k of this.components) { + o.components.push(k.toObj()); + } + return o; + } + + constructor() { this.components = []; } +} \ No newline at end of file diff --git a/structures/messages/message.js b/structures/messages/message.js index 0bccbc2..1648dff 100644 --- a/structures/messages/message.js +++ b/structures/messages/message.js @@ -3,6 +3,7 @@ import axios from 'axios'; import { Channel } from '../guilds/Channel.js'; import Guild from '../guilds/Guild.js'; import { DataManager } from '../DataManager.js'; +import { MessageActionRow } from './MessageActionRow.js'; export class message extends DataManager { @@ -57,24 +58,46 @@ export class message extends DataManager { /** @type {Channel} */ channel; + /** @type {MessageActionRow[]} */ + components; /** - * @param {String} content + * @param {MessageActionRow} ar + */ + addComponents(ar) { + if (this.components.length > 5) throw "MAXIMUM SIZE REACHED (5)"; + this.components.push(ar); + } + + + /** + * @param {String | message} content * @returns {Promise} */ reply(inp) { return new Promise(async (resolve, reject) => { - const refObj = { - message_id: this.id, - channel_id: this.channel.id, - guild_id: this.guild_id, - fail_if_not_exists: false //when sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true + try { + const refObj = { + message_id: this.id, + channel_id: this.channel.id, + guild_id: this.guild_id, + fail_if_not_exists: false //when sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true + } + + const toSend = (typeof inp == 'string') ? {content: inp} : structuredClone(inp); + toSend['message_reference'] = refObj; + + if (inp.components) { + toSend["components"] = []; + for (const k of inp.components) { + toSend["components"].push(k.toObj()); + } + } + + resolve(await this.channel.send(toSend)); + } catch(err) { + throw err; } - - const toSend = (typeof inp == 'string') ? {content: inp} : inp; - toSend['message_reference'] = refObj; - - resolve(await this.channel.send(toSend)); }); } @@ -99,6 +122,13 @@ export class message extends DataManager { async edit(inp) { return new Promise(async (resolve, reject) => { const newMsg = (typeof inp == "string") ? {content: inp} : inp; + + if (this.components) { + newMsg.components = []; + for (const k of this.components) { + newMsg.components.push(k.toObj()); + } + } this.client.axiosCustom.patch(`/channels/${this.channel.id}/messages/${this.id}`, newMsg).then((response) => { resolve(response.data); @@ -117,23 +147,23 @@ export class message extends DataManager { this.guild = guild; - for (const k in this) { - if (msgRaw[k] != undefined) { - if (k == 'type') { - this.type = (msgRaw['guild_id']) ? msgRaw[k] : 1; + for (const k in msgRaw) { + if (k == 'type') { + this.type = (msgRaw['guild_id']) ? msgRaw[k] : 1; + } + else if (k == "author") { + this[k] = new author(msgRaw[k]); + } + else { + if (k == 'channel_id') { + this.channel = new Channel({id: msgRaw[k]}, this.guild, client); } - else if (k == "author") { - this[k] = new author(msgRaw[k]); - } - else { - if (k == 'channel_id') { - this.channel = new Channel({id: msgRaw[k]}, this.guild, client); - } - this[k] = msgRaw[k]; - } + this[k] = msgRaw[k]; } } + + if (this.components == undefined) this.components = []; } } diff --git a/tests/Buttontests.js b/tests/Buttontests.js new file mode 100644 index 0000000..e32dc94 --- /dev/null +++ b/tests/Buttontests.js @@ -0,0 +1,32 @@ +import { Button } from "../structures/interactions/Button.js"; +import { Channel } from "../structures/guilds/Channel.js"; +import { message } from "../structures/messages/message.js"; +import { MessageButtonStyles } from "../structures/interactions/ButtonStyles.js"; +import { MessageActionRow } from "../structures/messages/MessageActionRow.js"; +import { ChannelSelectMenu, StringMenuComponent, StringSelectMenu } from "../structures/interactions/StringSelectMenu.js"; + +/** + * @param {message} mog + */ +export async function buttonTests(mog) { + var m = new message(); + // const comp = new Button(); + // comp.style = MessageButtonStyles.SUCCESS; + // comp.label = "HELLO WORLD"; + // comp.custom_id = "temptemp"; + + const c = new StringSelectMenu(); + const comp2 = new StringMenuComponent(); + comp2.value = 'llllll'; + comp2.label = 'llllll'; + c.options.push(comp2); + c.custom_id = "temp"; + + const row = new MessageActionRow(); + // row.addComponent(comp); + row.addComponent(c); + m.addComponents(row); + m.content = "OOGA BOOGA"; + + mog.reply(m); +} \ No newline at end of file diff --git a/tests/test.js b/tests/test.js index d57bcc8..e7dad91 100644 --- a/tests/test.js +++ b/tests/test.js @@ -1,5 +1,6 @@ import { Client, gateWayIntents, message, Interaction } from '../structures/types.js'; import config from '../config.json' assert { type: 'json' }; +import { buttonTests } from './Buttontests.js'; const { bottoken } = config; var c = new Client({ @@ -23,6 +24,9 @@ c.login(bottoken); c.on('messageRecieved', /**@param {message} message*/ async (message) => { + if (message.content == 'buttontest') { + return buttonTests(message); + } (await import('./messageTests.js')).default(message); });