/ src / modules / commands / addActivityRole.ts
addActivityRole.ts
  1  import { __h_dc } from './../messages';
  2  import {
  3    Role,
  4    CommandInteraction,
  5    PermissionsBitField,
  6    StringSelectMenuInteraction,
  7    InteractionType,
  8    EmbedBuilder,
  9    ActionRowBuilder,
 10    StringSelectMenuBuilder,
 11    APIRole,
 12    SlashCommandBuilder,
 13    ComponentType,
 14    Colors,
 15    ChannelType
 16  } from 'discord.js';
 17  
 18  import { Command } from '../commandHandler';
 19  import config from '../config';
 20  import { Locale, log, __ } from '../messages';
 21  import { prepare, getLang } from '../db';
 22  
 23  export default {
 24    data: new SlashCommandBuilder()
 25      .setName('addactivityrole')
 26      .setDescription('Adds an activity role to your guild.')
 27      .setDescriptionLocalizations(__h_dc('Adds an activity role to your guild.'))
 28      .setDefaultMemberPermissions(PermissionsBitField.Flags.ManageRoles)
 29      .setDMPermission(false)
 30      .addStringOption(option =>
 31        option
 32          .setName('activity')
 33          .setDescription('the name of the discord activity')
 34          .setDescriptionLocalizations(__h_dc('the name of the discord activity'))
 35          .setRequired(true)
 36      )
 37      .addRoleOption(option =>
 38        option
 39          .setName('role')
 40          .setDescription(
 41            'If not provided, the bot will look for roles with the same name or create a new one'
 42          )
 43          .setDescriptionLocalizations(
 44            __h_dc(
 45              'If not provided, the bot will look for roles with the same name or create a new one'
 46            )
 47          )
 48          .setRequired(false)
 49      )
 50      .addBooleanOption(option =>
 51        option
 52          .setName('exact_activity_name')
 53          .setDescription(
 54            "If false, the activity name 'Chrome' would also trigger for 'Google Chrome'"
 55          )
 56          .setDescriptionLocalizations(
 57            __h_dc("If false, the activity name 'Chrome' would also trigger for 'Google Chrome'")
 58          )
 59          .setRequired(false)
 60      )
 61      .addBooleanOption(option =>
 62        option
 63          .setName('permanent')
 64          .setDescription('the role will not be removed again if set to true')
 65          .setDescriptionLocalizations(
 66            __h_dc('the role will not be removed again if set to true')
 67          )
 68          .setRequired(false)
 69      ),
 70    execute: async interaction => {
 71      const locale = getLang(interaction);
 72      if (!interaction.channel) return;
 73      if (interaction.channel.type !== ChannelType.GuildText) {
 74        await interaction.reply(__({ phrase: 'This command can only be used in text channels.', locale }));
 75        return;
 76      }
 77  
 78      const activityName = interaction.options.get('activity', true)?.value as string;
 79      if (activityName.length > 1024) {
 80        await interaction.reply({
 81          content: __({
 82            phrase: 'The activity name is too long! Maximum is 1024 characters.',
 83            locale
 84          }),
 85          ephemeral: true
 86        });
 87        return;
 88      }
 89  
 90      const exactActivityName = (interaction.options.get('exact_activity_name', false)?.value as boolean | undefined) ?? false;
 91      const permanent = (interaction.options.get('permanent', false)?.value as boolean | undefined) ?? false;
 92  
 93      let role = interaction.options.get('role', false)?.role;
 94      if (!role) {
 95        // role not provided
 96        const possibleRoles = interaction.guild?.roles.cache.filter(role => {
 97          return role.name.toLowerCase().includes(activityName.toLowerCase());
 98        });
 99        if (!possibleRoles || possibleRoles.size === 0) {
100          // create role
101          role = await createRole(interaction, activityName);
102          process(interaction, role, activityName, exactActivityName, permanent, locale);
103        } else if (possibleRoles.size === 1) {
104          // use role
105          role = possibleRoles.first()!;
106          process(interaction, role, activityName, exactActivityName, permanent, locale);
107        } else {
108          // select role
109          const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
110            new StringSelectMenuBuilder()
111              .setCustomId('addactivityrole:roleSelector')
112              .setPlaceholder(__({ phrase: "Please select a role for '%s'", locale }, activityName))
113              .addOptions([
114                ...possibleRoles.map(role => {
115                  return {
116                    label: role.name,
117                    description: role.id,
118                    value: role.id
119                  };
120                }),
121                {
122                  label: __({ phrase: 'Create %s', locale }, activityName),
123                  description: __(
124                    { phrase: "Create a new role with the name '%s'", locale },
125                    activityName
126                  ),
127                  value: 'create'
128                }
129              ])
130          );
131          interaction.channel
132            .createMessageComponentCollector({
133              componentType: ComponentType.StringSelect,
134              filter: componentInteraction =>
135                componentInteraction.customId === 'addactivityrole:roleSelector',
136              time: 60000,
137              max: 1
138            })
139            .once('collect', async selectMenuInteraction => {
140              if (selectMenuInteraction.user.id !== interaction.user.id) return;
141              if (!selectMenuInteraction.isStringSelectMenu()) return;
142              if (selectMenuInteraction.customId !== 'addactivityrole:roleSelector') return;
143              if (selectMenuInteraction.values[0] === 'create') {
144                role = await createRole(interaction, activityName);
145              } else {
146                role =
147                  selectMenuInteraction.guild!.roles.cache.get(selectMenuInteraction.values[0]) ||
148                  null;
149              }
150              if (role) {
151                process(selectMenuInteraction, role, activityName, exactActivityName, permanent, locale);
152              }
153            });
154          interaction.reply({
155            components: [row],
156            ephemeral: true
157          });
158        }
159      } else {
160        process(interaction, role, activityName, exactActivityName, permanent, locale);
161      }
162    }
163  } as Command;
164  
165  async function createRole(interaction: CommandInteraction, activityName: string) {
166    return await interaction.guild!.roles.create({
167      name: activityName,
168      color: config.COLOR,
169      mentionable: true
170    });
171  }
172  
173  function reply(
174    interaction: CommandInteraction | StringSelectMenuInteraction,
175    content?: string,
176    embeds?: EmbedBuilder[]
177  ) {
178    if (interaction.type === InteractionType.ApplicationCommand) {
179      interaction.reply({ content, embeds, ephemeral: true });
180    } else if (interaction.isStringSelectMenu()) {
181      interaction.update({ content, embeds, components: [] });
182    }
183  }
184  
185  function process(
186    interaction: CommandInteraction | StringSelectMenuInteraction,
187    role: Role | APIRole,
188    activityName: string,
189    exactActivityName: boolean,
190    permanent: boolean,
191    locale: Locale
192  ) {
193    if (!role) reply(interaction, __({ phrase: ':x: That role does not exist! :x:', locale }));
194    if (role.name === '@everyone') {
195      reply(interaction, __({ phrase: "You can't use \\@everyone as an activity role.", locale }));
196      return;
197    }
198    if (
199      !interaction.guild?.members.me?.roles.highest?.position ||
200      !interaction.guild.members.me.roles.highest.id
201    )
202      return;
203    if (
204      role.position &&
205      interaction.guild.members.me?.roles.highest?.position &&
206      role.position >= interaction.guild.members.me.roles.highest.position
207    ) {
208      reply(interaction, undefined, [
209        new EmbedBuilder()
210          .setColor(Colors.Red)
211          .setDescription(
212            __({
213              phrase:
214                'To assign roles, my highest role needs to be higher than the role I am assigning.\nMove any of my roles higher than the role I should manage.',
215              locale
216            })
217          )
218          .addFields(
219            {
220              name: __({ phrase: 'My highest role:', locale }),
221              value:
222                `<@&${interaction.guild.members.me.roles.highest.id}> ` +
223                __(
224                  { phrase: '(position #%s)', locale },
225                  interaction.guild.members.me.roles.highest.position.toString()
226                )
227            },
228            {
229              name: __({ phrase: 'the activity role:', locale }),
230              value:
231                `<@&${role.id}> ` + __({ phrase: '(position #%s)', locale }, role.position.toString())
232            }
233          )
234      ]);
235      return;
236    }
237    if (
238      prepare('SELECT * FROM activityRoles WHERE guildID = ? AND roleID = ? AND activityName = ?')
239        .get(interaction.guild!.id, role.id, activityName)
240    ) {
241      reply(
242        interaction,
243        __({ phrase: ':x: That activity role already exists in this guild! :x:', locale })
244      );
245      return;
246    } else {
247      prepare('INSERT INTO activityRoles VALUES (?, ?, ?, ?, ?)').run(
248        interaction.guild!.id,
249        activityName,
250        role.id,
251        Number(exactActivityName),
252        Number(!permanent)
253      );
254      log.info(
255        `New activity role added: in guild ${interaction.guild.name} (${interaction.guild.id}) role: ${role.name} (${role.id}) activityName: ${activityName}, exactActivityName: ${exactActivityName}, permanent: ${permanent}`
256      );
257      reply(interaction, undefined, [
258        new EmbedBuilder()
259          .setColor(config.COLOR)
260          .setTitle(__({ phrase: 'Success!', locale }))
261          .addFields(
262            { name: __({ phrase: 'Activity', locale }), value: activityName },
263            { name: __({ phrase: 'Role', locale }), value: `<@&${role.id}>` },
264            {
265              name: __({ phrase: 'Exact Activity Name', locale }),
266              value: exactActivityName ? __('Yes') : __('No')
267            },
268            {
269              name: __({ phrase: 'Permanent', locale }),
270              value: permanent ? __({ phrase: 'Yes', locale }) : __({ phrase: 'No', locale })
271            }
272          )
273      ]);
274    }
275  }