import { AutocompleteInteraction, ChatInputCommandInteraction, GuildMember, GuildMemberRoleManager, hyperlink, inlineCode, InteractionReplyOptions, MessageFlags, PermissionFlagsBits, roleMention, SlashCommandBuilder, Snowflake, time, userMention } from "discord.js"; import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command"; import {GroupModel} from "../../Database/Models/GroupModel"; import {GroupRepository} from "../../Database/Repositories/GroupRepository"; import {Container} from "../../Container/Container"; import {GroupSelection} from "../CommandPartials/GroupSelection"; import {UserError} from "../UserError"; import {ArrayUtils} from "../../Utilities/ArrayUtils"; import {GroupConfigurationRepository} from "../../Database/Repositories/GroupConfigurationRepository"; import {PlaydateRepository} from "../../Database/Repositories/PlaydateRepository"; import {Nullable} from "../../types/Nullable"; import {MenuRenderer} from "../../Menu/MenuRenderer"; import {MenuItemType} from "../../Menu/MenuRenderer.types"; import {MenuTraversal} from "../../Menu/MenuTraversal"; import {ConfigurationHandler} from "../../Configuration/ConfigurationHandler"; import {GroupConfigurationProvider} from "../../Configuration/Groups/GroupConfigurationProvider"; import {MenuHandler} from "../../Configuration/MenuHandler"; import {ServerConfigurationProvider} from "../../Configuration/Server/ServerConfigurationProvider"; import {ServerConfigurationRepository} from "../../Database/Repositories/ServerConfigurationRepository"; import {PermissionError} from "../PermissionError"; import {EmbedLibrary, EmbedType} from "../EmbedLibrary"; import {EventHandler} from "../../Events/EventHandler"; import {ElementChangedEvent} from "../../Events/EventClasses/ElementChangedEvent"; import Groups from "../../Database/tables/Groups"; import {TimezoneHandler, TimezoneSaveTarget} from "../../Configuration/TimezoneHandler"; export class GroupCommand implements Command, ChatInteractionCommand, AutocompleteCommand { private static GOODBYE_MESSAGES: string[] = [ 'Sad to see you go.', 'May your next adventure be fruitful.', 'I hope, I served you well.', 'I wish you and your group good luck on your next adventures.', ] private static INVALID_CHARACTER_SEQUENCES: string[] = [ "http://", "https://" ] definition(): SlashCommandBuilder { // @ts-expect-error Slash command expects more than needed. return new SlashCommandBuilder() .setName('group') .setDescription(`Manages groups`) .addSubcommand(create => create.setName("create") .setDescription("Creates a new group, with executing user being the leader") .addStringOption((option) => option.setName("name") .setDescription("Defines the name for the group.") .setRequired(true) .setMaxLength(64) ) .addRoleOption((builder) => builder.setName("role") .setDescription("Defines the role, where all the members are located in.") .setRequired(true) ) ) .addSubcommand(listCommand => listCommand .setName("list") .setDescription("Displays the groups you are apart of.") ) .addSubcommand(command => command .setName('config') .setDescription("Starts the config manager for the group.") .addIntegerOption(GroupSelection.createOptionSetup()) ) .addSubcommand(command => command .setName("remove") .setDescription("Deletes a group you are the leader for.") .addIntegerOption(GroupSelection.createOptionSetup()) ) .addSubcommand(command => command .setName("transfer") .setDescription("Transfers leadership of a group to a different person") .addIntegerOption(GroupSelection.createOptionSetup) .addUserOption(option => option .setName("target") .setDescription("The member, that is the new leader") .setRequired(true) ) ) .addSubcommand(command => command .setName("timezone") .setDescription("Sets the timezone for the group, if a value is provided. If not, the current timezone is displayed.") .addIntegerOption(GroupSelection.createOptionSetup()) .addStringOption(option => option .setName('timezone') .setDescription("The timezone the group should use.") .setRequired(false) ) ); } async execute(interaction: ChatInputCommandInteraction): Promise { switch (interaction.options.getSubcommand()) { case "create": this.create(interaction); break; case "list": this.list(interaction); break; case "remove": await this.remove(interaction); break; case "config": await this.runConfigurator(interaction); break; case "transfer": await this.transferLeadership(interaction); break; case "timezone": await this.handleTimezone(interaction); break; default: throw new Error("Unsupported command"); } return Promise.resolve(); } private create(interaction: ChatInputCommandInteraction): void { if (!this.allowedCreate(interaction)) { throw new PermissionError("You don't have the permissions for it!") } const name = interaction.options.getString("name") ?? ''; const role = interaction.options.getRole("role", true); if (role.id === interaction.guildId) { throw new UserError("Creating a group for everyone is not permitted!"); } if (!(>interaction.member?.roles)?.cache.has(role?.id)) { throw new UserError( "You are not part of the role, you try to create a group for.", "Add yourself to the group or ask your admin to do so." ); } const invalidName = this.validateGroupName(name); if (invalidName) { throw new UserError(`Your group name contains one or more invalid character sequences: ${invalidName}`) } const group: GroupModel = { id: -1, name: name, leader: { server: interaction.guildId ?? '', memberid: interaction.member?.user.id ?? '' }, role: { server: interaction.guildId ?? '', roleid: role?.id ?? '' } } Container.get(GroupRepository.name).create(group); interaction.reply({ embeds: [ EmbedLibrary.base( 'Created group', `:white_check_mark: Created group \`${name}\``, EmbedType.Success ) ], flags: MessageFlags.Ephemeral }) } private allowedCreate(interaction: ChatInputCommandInteraction): boolean { if ((interaction.member)?.permissions.has(PermissionFlagsBits.Administrator)) { return true; } const config = new ConfigurationHandler( new ServerConfigurationProvider( Container.get(ServerConfigurationRepository.name), interaction.guildId ) ); const configValue = config.getConfigurationByPath("permissions.groupCreation.allowEveryone").value; return configValue === true; } private validateGroupName(name: string): string | null { const lowercaseName = name.toLowerCase(); for (const invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) { if (!lowercaseName.includes(invalidcharactersequence)) { continue } return invalidcharactersequence } return null; } private list(interaction: ChatInputCommandInteraction) { const repo = Container.get(GroupRepository.name); const groups = repo.findGroupsByMember(interaction.member); const playdateRepo = Container.get(PlaydateRepository.name); const embed = EmbedLibrary.base("Your groups on this server:", '', EmbedType.Info) .setFields( groups.map(group => { const nextPlaydate = playdateRepo.getNextPlaydateForGroup(group); const values = [ `Role: ${roleMention(group.role.roleid)}`, `Leader/GM: ${userMention(group.leader.memberid)}` ]; if (nextPlaydate) { values.push( `Next Playdate: ${time(nextPlaydate.from_time, "F")}` ) } return { name: group.name, value: values.join("\n") } }) ) const reply: InteractionReplyOptions = { embeds: [ embed ], allowedMentions: {roles: []}, flags: MessageFlags.Ephemeral } interaction.reply(reply); } private async remove(interaction: ChatInputCommandInteraction) { const group = GroupSelection.getGroup(interaction); const repo = Container.get(GroupRepository.name); if (group.leader.memberid != interaction.member?.user.id) { throw new PermissionError("You are not the leader."); } repo.deleteGroup(group); await interaction.reply({ embeds: [ EmbedLibrary.base( "Group deleted", `:x: Deleted \`${group.name}\`. ${ArrayUtils.chooseRandom(GroupCommand.GOODBYE_MESSAGES)}`, EmbedType.Success ) ], flags: MessageFlags.Ephemeral, }) } async handleAutocomplete(interaction: AutocompleteInteraction): Promise { const option = interaction.options.getFocused(true); if (option.name == "group") { await GroupSelection.handleAutocomplete(interaction, true); return; } } private async runConfigurator(interaction: ChatInputCommandInteraction) { const group = GroupSelection.getGroup(interaction); const menuHandler = new MenuHandler( new ConfigurationHandler( new GroupConfigurationProvider( Container.get(GroupConfigurationRepository.name), group ) ) ) const menu = new MenuRenderer( new MenuTraversal( menuHandler.fillMenuItems( [ { traversalKey: "channels", label: "Channels", description: "Provides settings to define in what channels the bot sends messages, when not directly interacting with it.", type: MenuItemType.Collection, children: [ { traversalKey: "notifications", label: "Notifications", description: "Sets the channel, where the group gets notified, when things are happening, such as a new playdate is created.", }, { traversalKey: "playdateReminders", label: 'Playdate Reminders', description: "Sets the channel, where the group gets reminded of upcoming playdates.", } ] }, { traversalKey: "permissions", label: "Permissions", description: "Allows customization, how the members are allowed to interact with the data stored in the group.", type: MenuItemType.Collection, children: [ { traversalKey: "allowMemberManagingPlaydates", label: "Manage Playdates", description: "Defines if the members are allowed to manage playdates like adding or deleting them.", } ] }, { traversalKey: "calendar", label: "Calendar", description: "Provides settings for the metadata contained in the playdate exports.", type: MenuItemType.Collection, children: [ { traversalKey: "title", label: "Title", description: "Defines how the calendar entry should be called.", }, { traversalKey: "description", label: "Description", description: "Sets the description for the calendar entry.", }, { traversalKey: "location", label: "Location", description: "Sets the location where the calendar should point to." } ] }, ] ), 'Group Configuration', "This UI allows you to change settings for your group." ), null,null, group.name ) menu.display(interaction); } private async transferLeadership(interaction: ChatInputCommandInteraction) { const group = GroupSelection.getGroup(interaction); const repo = Container.get(GroupRepository.name); if (group.leader.memberid != interaction.member?.user.id) { throw new PermissionError( "You are not the leader." ); } const newLeader = interaction.options.getMember("target"); if (!newLeader.roles.cache.has(group.role.roleid)) { throw new UserError( "Can't transfer leadership: The target member is not part of your group.", "Add the user to the role this group is part in or ask your server admin to do it." ); } group.leader.memberid = newLeader.id repo.update(group); Container.get(EventHandler.name) .dispatch(new ElementChangedEvent( Groups.name, { id: group.id, leader: { memberid: newLeader.id } } )) await interaction.reply({ embeds: [ EmbedLibrary.base( 'Leadership transferred', `Leadership was successfully transferred to ${userMention(newLeader.user.id)}`, EmbedType.Success ) ], flags: MessageFlags.Ephemeral, }) } private async handleTimezone(interaction: ChatInputCommandInteraction) { const group = GroupSelection.getGroup(interaction); const enteredTimezone = interaction.options.getString('timezone'); const timezoneHandler = new TimezoneHandler( interaction.guildId ?? '', group ); if (!enteredTimezone) { await interaction.reply( { embeds: [ EmbedLibrary.withGroup( group, "Timezone", `The group currently uses the timezone ${inlineCode(timezoneHandler.getCurrentTimezone())}.`, EmbedType.Info ) ] } ) return; } if (!TimezoneHandler.ALL_TIMEZONES.includes(enteredTimezone)) { throw new UserError( `Invalid timezone provided: ${enteredTimezone}`, `Try using timezones found in this list using the 'TZ Identifier'. ${hyperlink("List", 'https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List')}` ) } timezoneHandler.save(enteredTimezone, TimezoneSaveTarget.Group); await interaction.reply( { embeds: [ EmbedLibrary.withGroup( group, "Timezone changed", `The group now uses the timezone ${inlineCode(enteredTimezone)}.`, EmbedType.Info ) ] } ) } }