import { ButtonInteraction, Client, CommandInteraction, GuildMember, Interaction, Message, MessageActionRow, MessageButton, MessageSelectMenu, MessageSelectOptionData, PartialDMChannel, SelectMenuInteraction, TextBasedChannels } from "discord.js"; import { TextChannel } from "discord.js"; import * as Opt from "../types/Opt"; import refreshCommands from "./slash"; import { InteractionRoles } from "./data"; type ReplyableInteraction = ButtonInteraction | CommandInteraction | SelectMenuInteraction; const isReplyableInteraction = (res: any): boolean => res instanceof ButtonInteraction || res instanceof CommandInteraction || res instanceof SelectMenuInteraction; const sendMessage = (res: ReplyableInteraction | TextBasedChannels, msg: any, ephemeral?: boolean) => { let arg: any = {}; if (typeof msg === 'object') arg = msg; else arg.content = msg; if (isReplyableInteraction(res) && ephemeral) arg.ephemeral = true; if (isReplyableInteraction(res)) (res as ReplyableInteraction).reply(arg); else (res as TextBasedChannels).send(arg); } //initialize discord const Discord = require('discord.js'), client: Client = new Discord.Client({ intents: [ 'GUILD_MEMBERS' ] }); /** * send message containing menu * @param res text channel to send message or interaction to reply to * @param menuOpts menu data object */ function sendMenu(res: ReplyableInteraction|TextBasedChannels, menuOpts: Opt.Menu, ephemeral?: boolean) { let row = new MessageActionRow() let select = new MessageSelectMenu() .setCustomId(menuOpts.id) .setPlaceholder(menuOpts.palceholder) .setMaxValues(menuOpts.max === 'all' ? menuOpts.options.length : menuOpts.max) .setMinValues(0); let options: MessageSelectOptionData[] = []; for (let i = 0; i < menuOpts.options.length; i++) { let opts: Opt.MenuOptions = menuOpts.options[i]; let option: MessageSelectOptionData = { label: opts.label, value: opts.value }; if (opts.emoji && opts.emoji.length) option.emoji = opts.emoji; if (opts.description && opts.description.length) option.description = opts.description; if (opts.default) option.default = true; options.push(option); } select.addOptions(options); row.addComponents(select) let arg = { content: menuOpts.message, components: [row] }; sendMessage(res, arg, ephemeral); } /** * send message containing row * @param c text channel to send message * @param rowOpts row data object */ function sendRow(res: ReplyableInteraction|TextBasedChannels, rowOpts: Opt.Row, ephemeral?: boolean) { let row = new MessageActionRow(); let options: MessageButton[] = []; for (let i = 0; i < rowOpts.buttons.length; i++) { let opts: Opt.RowButton = rowOpts.buttons[i]; let option = new MessageButton() .setStyle(opts.style) .setLabel(opts.label) .setCustomId(opts.id); options.push(option); } row.addComponents(options); let arg = { content: rowOpts.message, components: [row] }; sendMessage(res, arg, ephemeral); } //on interaction event client.on('interactionCreate', (interaction: Interaction) => { if (interaction.inGuild()) { if (interaction.isSelectMenu()) handleSelect(interaction as SelectMenuInteraction); else if (interaction.isButton()) handleButton(interaction as ButtonInteraction); else if (interaction.isCommand()) handleCommand(interaction as CommandInteraction); } }); async function handleButton(interaction: ButtonInteraction) { //start typing await interaction.deferReply({ ephemeral: true }); //get custom id let customId: string; if (interaction.component instanceof MessageButton) customId = interaction.component.customId; else if ('custom_id' in interaction.component) customId = interaction.component.custom_id; //list of button rows from data let rows = InteractionRoles.filter(opt => opt.type === 'row'); //loop through each the buttons rows for (let i = 0; i < rows.length; i++) { let row = rows[i] as Opt.Row; //filter to just buttons with matching id let matchingButtons = row.buttons.filter(opt => opt.id === customId); if (matchingButtons.length) { //if there's no role id, then nothing can be done if (matchingButtons[0].roleid && interaction.member instanceof GuildMember) { //since it is a GuildMember, let's force typescript to rememeber it let member = interaction.member as GuildMember; //get corresponding role let role = await interaction.guild.roles.fetch(matchingButtons[0].roleid); //if clicker (member) does have the role if (member.roles.cache.some(r => r.id === role.id)) { //remove role await member.roles.remove(role.id); interaction.editReply(`Removed \`${role.name}\``); } else { //add role await member.roles.add(role.id); interaction.editReply(`Added \`${role.name}\``); } break; } } } }; //on menu click async function handleSelect(interaction: SelectMenuInteraction) { //start typing await interaction.deferReply({ ephemeral: true }); //get custom id let customId: string; if (interaction.component instanceof MessageSelectMenu) customId = interaction.component.customId; else if ('custom_id' in interaction.component) customId = interaction.component.custom_id; //list of menus from data with matching id let menus = InteractionRoles.filter(opt => opt.type === 'menu' && opt.id === customId); if (menus.length) { //use first menu with matching id let m = menus[0] as Opt.Menu, vals = interaction.values || []; //list of changes let changed: { add: string[], rem: string[] } = { add: [], rem: [] }; //loop through all the options in the menu for (let i = 0; i < m.options.length; i++) { let opt: Opt.MenuOptions = m.options[i], {roleid} = opt; //if there's no role id, then nothing can be done if (roleid && interaction.member instanceof GuildMember) { //since it is a GuildMember, let's force typescript to rememeber it let member = interaction.member as GuildMember; //get corresponding role let role = await interaction.guild.roles.fetch(roleid); //if enabled if (vals && vals.includes(opt.value)) { //if member doesn't have the role if (!member.roles.cache.some(r => r.id === role.id)) { //give role await member.roles.add(role); changed.add.push(role.name); } //else if member does have the role } else if (member.roles.cache.some(r => r.id === role.id)) { //remove role await member.roles.remove(role); changed.rem.push(role.name); } } } //convert changes to a message let message = []; if (changed.add.length) message.push(`Added \`${changed.add.join(', ')}\``); if (changed.rem.length) message.push(`Removed \`${changed.rem.join(', ')}\``); interaction.editReply(message.length ? message.join('\n') : 'No Changes'); } else interaction.editReply('Error: Unknown menu'); }; //on slash command async function handleCommand(interaction: CommandInteraction) { let category: number; switch (interaction.commandName) { case 'roles': category = interaction.options.getInteger('category', true); sendRoles(interaction, InteractionRoles[category].id, true); break; case 'rolesall': if (interaction.user.id === '167336999844315137') { let givenChannel = interaction.options.getChannel('channel'), channel: TextChannel = await client.channels.fetch(interaction.channelId) as TextChannel; if (givenChannel && givenChannel.type === 'GUILD_TEXT') channel = givenChannel as TextChannel; if (channel) { category = interaction.options.getInteger('category'); sendRoles(channel, category ? InteractionRoles[category].id : undefined); interaction.reply({ content: `sending message in <#${channel.id}>`, ephemeral: true }) } else interaction.reply({ content: 'error finding channel to send message in', ephemeral: true }) } else interaction.reply({ content: 'missing permissions', ephemeral: true }) break; case 'updateroles': //TODO reload data.ts //TODO reload slash.ts interaction.reply({ content: 'command incomplete', ephemeral: true }) break; } } async function sendRoles(res: ReplyableInteraction|TextBasedChannels, id?: string, ephemeral?: boolean) { if (id) { let rowarr = InteractionRoles.filter(r => r.id === id); if (rowarr.length) { let row = rowarr[0]; if (row.type === 'menu') sendMenu(res, row as Opt.Menu, ephemeral); else if (row.type === 'row') sendRow(res, row as Opt.Row, ephemeral); } else sendMessage(res, `invalid id ${id}`, ephemeral); } else if (isReplyableInteraction(res)) sendMessage(res, `missing id`, ephemeral); else { for (let i = 0; i < InteractionRoles.length; i++) { switch (InteractionRoles[i].type) { case 'menu': sendMenu(res, InteractionRoles[i] as Opt.Menu, ephemeral); break; case 'row': sendRow(res, InteractionRoles[i] as Opt.Row, ephemeral); break; } } } } //register slash commands client.on('ready', () => { console.log('Ready') setInterval(() => refreshCommands(client.user.id), 1000*60*60*24); refreshCommands(client.user.id) }); //login with token client.login(require('fs').readFileSync('../token').toString());