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

398 lines
No EOL
13 KiB
TypeScript

import {
AutocompleteInteraction,
ChatInputCommandInteraction,
EmbedBuilder,
GuildMember,
GuildMemberRoleManager,
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 GroupConfiguration from "../../Database/tables/GroupConfiguration";
import Groups from "../../Database/tables/Groups";
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)
)
);
}
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 {
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 (!(<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 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>(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 ((<GuildMember>interaction.member)?.permissions.has(PermissionFlagsBits.Administrator)) {
return true;
}
const config = new ConfigurationHandler(
new ServerConfigurationProvider(
Container.get<ServerConfigurationRepository>(ServerConfigurationRepository.name),
<Snowflake>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>(GroupRepository.name);
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
const playdateRepo = Container.get<PlaydateRepository>(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>(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<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 menuHandler = new MenuHandler(
new ConfigurationHandler(
new GroupConfigurationProvider(
Container.get<GroupConfigurationRepository>(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>(GroupRepository.name);
if (group.leader.memberid != interaction.member?.user.id) {
throw new PermissionError(
"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);
Container.get<EventHandler>(EventHandler.name)
.dispatch(new ElementChangedEvent<GroupModel>(
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,
})
}
}