pnp-scheduler/source/Discord/Commands/Groups.ts

282 lines
No EOL
9.1 KiB
TypeScript

import {
SlashCommandBuilder,
ChatInputCommandInteraction,
MessageFlags,
InteractionReplyOptions,
GuildMember,
EmbedBuilder,
AutocompleteInteraction,
roleMention, time, userMention, GuildMemberRoleManager
} from "discord.js";
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
import {GroupModel} from "../../Models/GroupModel";
import {GroupRepository} from "../../Repositories/GroupRepository";
import {Container} from "../../Container/Container";
import {GroupSelection} from "../CommandPartials/GroupSelection";
import {UserError} from "../UserError";
import {ArrayUtils} from "../../Utilities/ArrayUtils";
import {GroupConfigurationRenderer} from "../../Groups/GroupConfigurationRenderer";
import {GroupConfigurationHandler} from "../../Groups/GroupConfigurationHandler";
import {GroupConfigurationTransformers} from "../../Groups/GroupConfigurationTransformers";
import {GroupConfigurationRepository} from "../../Repositories/GroupConfigurationRepository";
import {PlaydateRepository} from "../../Repositories/PlaydateRepository";
import {Nullable} from "../../types/Nullable";
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('groups')
.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)
)
);
}
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
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;
default:
throw new Error("Unsupported command");
}
return Promise.resolve();
}
private create(interaction: ChatInputCommandInteraction): void {
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 (!(<Nullable<GuildMemberRoleManager>>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 validName = this.validateGroupName(name);
// @ts-expect-error Is correct, since the valid name can return either true or a string and the error should only be thrown if it's a string.
if (name !== true) {
throw new UserError(`Your group name contains one or more invalid character sequences: ${validName}`)
}
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>(GroupRepository.name).create(group);
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral })
}
private validateGroupName(name: string): true|string{
const lowercaseName = name.toLowerCase();
for (const invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) {
if (!lowercaseName.includes(invalidcharactersequence)) {
continue
}
return invalidcharactersequence
}
return true;
}
private list(interaction: ChatInputCommandInteraction) {
const repo = Container.get<GroupRepository>(GroupRepository.name);
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
const embed = new EmbedBuilder()
.setTitle("Your groups on this server:")
.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>(GroupRepository.name);
if (group.leader.memberid != interaction.member?.user.id) {
throw new UserError("Can't remove group. You are not the leader.");
}
repo.deleteGroup(group);
const embed = new EmbedBuilder()
.setTitle("Group deleted.")
.setDescription(
`:x: Deleted \`${group.name}\`. ${ArrayUtils.chooseRandom(GroupCommand.GOODBYE_MESSAGES)}`
)
await interaction.reply({
embeds: [
embed
],
flags: MessageFlags.Ephemeral,
})
}
async handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
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 configurationRenderer = new GroupConfigurationRenderer(
new GroupConfigurationHandler(
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
group
),
new GroupConfigurationTransformers(),
)
await configurationRenderer.setup(interaction);
}
private async transferLeadership(interaction: ChatInputCommandInteraction) {
const group = GroupSelection.getGroup(interaction);
const repo = Container.get<GroupRepository>(GroupRepository.name);
if (group.leader.memberid != interaction.member?.user.id) {
throw new UserError(
"Can't transfer leadership. You are not the leader."
);
}
const newLeader = <GuildMember>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);
const embed = new EmbedBuilder()
.setTitle("Leadership transferred")
.setDescription(
`Leadership was successfully transferred to ${userMention(newLeader.user.id)}`
)
await interaction.reply({
embeds: [
embed
],
flags: MessageFlags.Ephemeral,
})
}
}