diff --git a/src/structure/components/commands/utility/Selfrole.js b/src/structure/components/commands/utility/Selfrole.js new file mode 100644 index 0000000..4bfcf83 --- /dev/null +++ b/src/structure/components/commands/utility/Selfrole.js @@ -0,0 +1,87 @@ +const { Emojis } = require("../../../../constants"); +const { EmbedLimits } = require("../../../../constants/Constants"); +const { SlashCommand } = require("../../../interfaces"); + +class SelfroleCommand extends SlashCommand { + constructor(client) { + super(client, { + name: 'selfrole', + description: 'Manage your selfroles', + module: 'utility', + guildOnly: true, + options: [{ + name: ['add', 'remove'], + type: 'SUB_COMMAND', + options: [{ + name: 'roles', + type: 'ROLES', + description: 'Roles to give/take', + required: true + }] + }, { + name: ['list', 'clear'], + type: 'SUB_COMMAND' + }], + clientPermissions: ['MANAGE_ROLES'] + }); + } + + async execute(invoker, { roles }) { + + const { subcommand: { name: subcommand } } = invoker; + if (subcommand === 'list') return this._listRoles(invoker); + + const { guild, member } = invoker; + const { selfrole } = await guild.settings(); + if (!selfrole.roles.length) return { index: 'COMMAND_SELFROLE_NONE', emoji: 'failure' }; + + const memberRoles = member.roles.cache.map((r) => r.id); + const _roles = roles.value.filter((r) => selfrole.roles.includes(r.id)); + + if (subcommand === 'add') { + await member.roles.set([...new Set([...memberRoles, ..._roles.map((r) => r.id)])]); + return { index: 'COMMAND_SELFROLE_ADD_SUCCESS', emoji: 'success', params: { roles: _roles.map((r) => r.name).join('**, **') } }; + } else if (subcommand === 'remove') { + const ids = _roles.map((r) => r.id); + const set = memberRoles.filter((r) => !ids.includes(r)); + await member.roles.set([...new Set(set)]); + return { index: 'COMMAND_SELFROLE_REMOVE_SUCCESS', emoji: 'success', params: { roles: _roles.map((r) => r.name).join('**, **') } }; + } else if (subcommand === 'clear') { + const set = memberRoles.filter((r) => !selfrole.roles.includes(r)); + await member.roles.set([...new Set(set)]); + return { index: 'COMMAND_SELFROLE_CLEAR_SUCCESS', emoji: 'success', }; + } + + } + + async _listRoles(invoker) { + + const { guild, member } = invoker; + const { selfrole } = await guild.settings(); + const memberRoles = member.roles.cache; + if (!selfrole.roles.length) return { index: 'COMMAND_SELFROLE_NONE', emoji: 'failure' }; + + const fields = []; + const roles = selfrole.roles.map((r) => `${memberRoles.has(r) ? Emojis.success : Emojis.failure} <@&${r}>`); + let str = ''; + for (const role of roles) { + const tmp = `${str}\n${role}`; + if (tmp.length > EmbedLimits.fieldValue) { + fields.push({ name: '\u200b', value: str }); + str = role; + } else str = tmp; + } + fields.push({ name: '\u200b', value: str }); + + return { + embed: { + title: guild.format('COMMAND_SELFROLE_LIST'), + fields + } + }; + + } + +} + +module.exports = SelfroleCommand; \ No newline at end of file diff --git a/src/structure/components/observers/UtilityHook.js b/src/structure/components/observers/UtilityHook.js index d5f4e99..ce3e15f 100644 --- a/src/structure/components/observers/UtilityHook.js +++ b/src/structure/components/observers/UtilityHook.js @@ -21,6 +21,7 @@ class UtilityHook extends Observer { ['inviteCreate', this.inviteCreate.bind(this)], ['inviteDelete', this.inviteDelete.bind(this)], // ['messageReactionAdd', this.reactionAdd.bind(this)] + ['interactionCreate', this.selfrole.bind(this)] ]; } @@ -163,6 +164,7 @@ class UtilityHook extends Observer { } + // Code for enforcing single reaction per user for polls, disabled due to rate limit issues async reactionAdd(reaction, user) { if (reaction.partial) reaction = await reaction.fetch(); if (user.partial) user = await user.fetch(); @@ -187,6 +189,37 @@ class UtilityHook extends Observer { } } + async selfrole(interaction) { + if (!interaction.isSelectMenu() && !interaction.isButton()) return; + const { guild, message, customId, values, channel, member } = interaction; + if (!guild || !member || !customId.includes('selfrole')) return; + const { selfrole } = await guild.settings(); + if (!selfrole.message || selfrole.message !== message.id || + !selfrole.channel || selfrole.channel !== channel.id || + !selfrole.roles.length) return; + + const missing = guild.me.permissions.missing(['MANAGE_ROLES']); + if (missing.length) + return this.client.emit('utilityError', { guild, utility: 'selfrole', reason: 'UTILITY_SELFROLE_PERMS', params: { missing: missing.join(', ') } }); + + const memberRoles = member.roles.cache.map((r) => r.id); + if (interaction.isButton()) { + const assignThese = memberRoles.filter((r) => !selfrole.roles.includes(r)); + await member.roles.set(assignThese); + } else { + for (const value of values) { + if (!selfrole.roles.includes(value)) continue; + const index = memberRoles.indexOf(value); + if (index >= 0) memberRoles.splice(index, 1); + else memberRoles.push(value); + } + await member.roles.set(memberRoles); + } + + await interaction.update({ components: message.components }); + + } + } module.exports = UtilityHook; \ No newline at end of file diff --git a/src/structure/components/settings/utility/Selfrole.js b/src/structure/components/settings/utility/Selfrole.js new file mode 100644 index 0000000..1883db5 --- /dev/null +++ b/src/structure/components/settings/utility/Selfrole.js @@ -0,0 +1,114 @@ +const { Emojis } = require("../../../../constants"); +const { Util } = require("../../../../utilities"); +const { Setting } = require("../../../interfaces"); + +class SelfroleSetting extends Setting { + + constructor(client) { + super(client, { + name: 'selfrole', + description: 'Configure roles that users can assign to themselves', + module: 'utility', + display: 'Self Role', + default: { + roles: [], + message: null, + channel: null, + text: null + }, + commandOptions: [{ + name: 'roles', + description: 'Modify roles users can give themselves', + choices: [ + { name: 'add', value: 'add' }, + { name: 'remove', value: 'remove' }, + { name: 'set', value: 'set' }, + { name: 'reset', value: 'reset' }, + ], + }, { + name: 'channel', + description: 'Optional channel to output message for selecting roles', + type: 'TEXT_CHANNEL' + }, { + name: 'text', + description: 'Optional text to display above the select menu' + }] + }); + } + + async execute(invoker, { roles, channel, text }, setting) { + + if (!roles && !channel && !text) return { error: true, index: 'SETTING_MISSING_ARG' }; + const { guild } = invoker; + + if (roles) { + const response = await this._prompt(invoker, { + index: `SETTING_PROMPT_${roles.value.toUpperCase()}`, + params: { list: 'roles' } + }); + if (response.error) return response; + const params = Util.parseQuotes(response).map(([param]) => param); + const values = (await guild.resolveRoles(params)).filter((r) => !r.managed).map((r) => r.id); + this[roles.value](setting.roles, values); + if(setting.roles.length >= 25 && (setting.channel || channel)) await invoker.channel.send(guild.format('SETTING_SELFROLE_WARNING')); + } + + // old channel for deleting old message if one exists + const oldChannel = await guild.resolveChannel(setting.channel); + const newChannel = channel?.value || oldChannel; + if (channel) setting.channel = channel.value.id; // Set the new channel if one is given + if (text) setting.text = text.value; + + // If an old channel exists, and a message exists, and either all roles were removed or the channel changed, delete the old message + if (oldChannel && setting.message && (!setting.roles.length || newChannel !== oldChannel)) { + const message = await oldChannel.messages.fetch(setting.message).catch(() => null); + if (message) { + await message.delete(); + setting.message = null; + } + } + + if (setting.roles.length && setting.channel && setting.roles.length <= 25) { + const roles = await guild.resolveRoles(setting.roles); + const components = [{ + type: 'ACTION_ROW', + components: [{ + type: 'SELECT_MENU', + customId: 'selfrole-select', + maxValues: roles.length, + options: roles.map((r) => { + return { label: r.name, value: r.id }; + }) + }] + }, { + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + customId: 'selfrole-clear', + label: 'Clear', + style: 'PRIMARY', + emoji: Emojis.failure + }] + }]; + const payload = { + content: setting.text || guild.format('SETTING_SELFROLE_SELECT'), + components + }; + + const msg = await oldChannel.messages.fetch(setting.message).catch(() => null); + if (msg && newChannel === oldChannel) { + await msg.edit(payload); + } else { + const msg = await newChannel.send(payload); + setting.message = msg.id; + } + + } + + return { index: 'SETTING_SUCCESS_ALT' }; + + } + +} + +module.exports = SelfroleSetting; \ No newline at end of file