feat(permissions): Adds server permissions
This commit is contained in:
parent
d46bbd84c5
commit
cf9c88a2d6
24 changed files with 415 additions and 69 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import * as esbuild from "esbuild";
|
||||
import path from "path";
|
||||
|
||||
const isDev = process.env.BUILD_TARGET !== 'DOCKER';
|
||||
|
||||
const context = await esbuild.context({
|
||||
entryPoints: [
|
||||
path.join('source', 'main.ts'),
|
||||
|
|
@ -10,7 +12,7 @@ const context = await esbuild.context({
|
|||
outdir: './dist/',
|
||||
platform: 'node',
|
||||
target: 'node10.4',
|
||||
sourcemap: 'linked',
|
||||
sourcemap: isDev ? 'external' : null,
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
}
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/deepmerge": "^2.1.0",
|
||||
"@types/lodash": "^4.17.18",
|
||||
"@types/log4js": "^0.0.33",
|
||||
"@types/node": "^22.13.9",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
"esbuild": "^0.25.0",
|
||||
"ics": "^3.8.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"log4js": "^6.9.1",
|
||||
"node-cron": "^4.0.7",
|
||||
"node-ical": "^0.20.1",
|
||||
|
|
@ -1636,6 +1638,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.18.tgz",
|
||||
"integrity": "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/log4js": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/log4js/-/log4js-0.0.33.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"dependencies": {
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/deepmerge": "^2.1.0",
|
||||
"@types/lodash": "^4.17.18",
|
||||
"@types/log4js": "^0.0.33",
|
||||
"@types/node": "^22.13.9",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
"esbuild": "^0.25.0",
|
||||
"ics": "^3.8.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"log4js": "^6.9.1",
|
||||
"node-cron": "^4.0.7",
|
||||
"node-ical": "^0.20.1",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,17 @@ import deepmerge from "deepmerge";
|
|||
import {isPlainObject} from "is-plain-object";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {ConfigurationTransformer, TransformerResults} from "./ConfigurationTransformer";
|
||||
import _ from "lodash";
|
||||
|
||||
export enum PathConfigurationFrom {
|
||||
Database,
|
||||
Default
|
||||
}
|
||||
|
||||
export type PathConfiguration = {
|
||||
value: TransformerResults,
|
||||
from: PathConfigurationFrom
|
||||
}
|
||||
|
||||
export class ConfigurationHandler<
|
||||
TProviderModel extends ConfigurationModel = ConfigurationModel,
|
||||
|
|
@ -48,22 +59,27 @@ export class ConfigurationHandler<
|
|||
)
|
||||
}
|
||||
|
||||
public getConfigurationByPath(path: string): Nullable<TransformerResults> {
|
||||
public getConfigurationByPath(path: string): PathConfiguration {
|
||||
const configuration = this.provider.get(path);
|
||||
if (!configuration) {
|
||||
return;
|
||||
return {
|
||||
value: _.get(this.provider.defaults, path, null),
|
||||
from: PathConfigurationFrom.Default
|
||||
};
|
||||
}
|
||||
|
||||
return this.transformer.getValue(configuration);
|
||||
return {
|
||||
value: this.transformer.getValue(configuration),
|
||||
from: PathConfigurationFrom.Database
|
||||
};
|
||||
}
|
||||
|
||||
private getCompleteDatabaseConfig(): Partial<TRuntimeConfiguration> {
|
||||
const values = this.provider.getAll();
|
||||
const configuration: Partial<TRuntimeConfiguration> = {};
|
||||
|
||||
values.forEach((configValue) => {
|
||||
const value = this.transformer.getValue(configValue);
|
||||
setPath(configuration, configValue.key, value);
|
||||
_.set(configuration, configValue.key, value);
|
||||
})
|
||||
|
||||
return configuration;
|
||||
|
|
|
|||
|
|
@ -29,9 +29,6 @@ export type CalendarRuntimeGroupConfiguration = {
|
|||
location: null | string
|
||||
}
|
||||
|
||||
export type GroupConfigurationResult =
|
||||
ChannelId | Intl.Locale | boolean | string | null
|
||||
|
||||
export class GroupConfigurationProvider implements ConfigurationProvider<
|
||||
GroupConfigurationModel,
|
||||
RuntimeGroupConfiguration
|
||||
|
|
@ -66,9 +63,13 @@ export class GroupConfigurationProvider implements ConfigurationProvider<
|
|||
if (value.id) {
|
||||
// @ts-expect-error id is set, due to the check on line above
|
||||
this.repository.update(value);
|
||||
return
|
||||
}
|
||||
|
||||
this.repository.create(value);
|
||||
this.repository.create({
|
||||
...value,
|
||||
group: this.group
|
||||
});
|
||||
}
|
||||
getTransformer(): ConfigurationTransformer {
|
||||
return new ConfigurationTransformer(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import {ConfigurationHandler} from "./ConfigurationHandler";
|
||||
import {ConfigurationHandler, PathConfigurationFrom} from "./ConfigurationHandler";
|
||||
import {
|
||||
AnyMenuItem,
|
||||
CollectionMenuItem,
|
||||
FieldMenuItem, FieldMenuItemContext, FieldMenuItemSaveValue,
|
||||
FieldMenuItem,
|
||||
FieldMenuItemContext,
|
||||
FieldMenuItemSaveValue,
|
||||
MenuItem,
|
||||
MenuItemType,
|
||||
PromptMenuItem
|
||||
|
|
@ -12,11 +14,11 @@ import {
|
|||
channelMention,
|
||||
ChannelSelectMenuBuilder,
|
||||
ChannelType,
|
||||
inlineCode,
|
||||
italic,
|
||||
StringSelectMenuBuilder, TextInputBuilder, TextInputStyle
|
||||
StringSelectMenuBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle
|
||||
} from "discord.js";
|
||||
import {ChannelId} from "../types/DiscordTypes";
|
||||
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
||||
import {TransformerType} from "./ConfigurationTransformer";
|
||||
|
||||
|
|
@ -101,14 +103,14 @@ export class MenuHandler {
|
|||
|
||||
private getChannelValue(context: FieldMenuItemContext): string {
|
||||
const value = this.config.getConfigurationByPath(context.path.join('.'));
|
||||
if (value === undefined) {
|
||||
return italic("None");
|
||||
|
||||
const isDefault = value.from === PathConfigurationFrom.Default;
|
||||
const display = !value ? "None" : channelMention(<string>value.value);
|
||||
|
||||
if (isDefault) {
|
||||
return italic(`Default (${display})`)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return inlineCode("None");
|
||||
}
|
||||
return channelMention(<ChannelId>value);
|
||||
return display;
|
||||
}
|
||||
|
||||
private getChannelMenuBuilder(): MessageActionRowComponentBuilder {
|
||||
|
|
@ -119,11 +121,14 @@ export class MenuHandler {
|
|||
|
||||
private getPermissionBooleanValue(context: FieldMenuItemContext) {
|
||||
const value = this.config.getConfigurationByPath(context.path.join('.'));
|
||||
if (value === undefined) {
|
||||
return italic("None");
|
||||
}
|
||||
|
||||
return value ? 'Allowed' : "Disallowed";
|
||||
const isDefault = value.from === PathConfigurationFrom.Default;
|
||||
const display = value.value === null ? "None" : value.value == true ? "Allowed" : "Disallowed";
|
||||
|
||||
if (isDefault) {
|
||||
return italic(`Default (${display})`)
|
||||
}
|
||||
return display;
|
||||
}
|
||||
|
||||
private getPermissionBooleanBuilder() {
|
||||
|
|
@ -144,15 +149,8 @@ export class MenuHandler {
|
|||
|
||||
private getStringValue(context: FieldMenuItemContext): string {
|
||||
const value = this.config.getConfigurationByPath(context.path.join('.'));
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError(`Value of type ${typeof value} can't be used for a string value!`)
|
||||
}
|
||||
|
||||
return value;
|
||||
return value.value === null ? "" : <string>value.value;
|
||||
}
|
||||
|
||||
private getStringBuilder(): TextInputBuilder {
|
||||
|
|
|
|||
70
source/Configuration/Server/ServerConfigurationProvider.ts
Normal file
70
source/Configuration/Server/ServerConfigurationProvider.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import {ConfigurationProvider} from "../ConfigurationProvider";
|
||||
import {ServerConfigurationModel} from "../../Database/Models/ServerConfigurationModel";
|
||||
import {ServerConfigurationRepository} from "../../Database/Repositories/ServerConfigurationRepository";
|
||||
import {Snowflake} from "discord.js";
|
||||
import { ConfigurationModel } from "../../Database/Models/ConfigurationModel";
|
||||
import { Model } from "../../Database/Models/Model";
|
||||
import { Nullable } from "../../types/Nullable";
|
||||
import {ConfigurationTransformer, TransformerType} from "../ConfigurationTransformer";
|
||||
|
||||
export type RuntimeServerConfiguration = {
|
||||
permissions: PermissionRuntimeServerConfiguration
|
||||
}
|
||||
|
||||
export type PermissionRuntimeServerConfiguration = {
|
||||
groupCreation: GroupCreatePermissionRuntimeServerConfiguration
|
||||
}
|
||||
|
||||
export type GroupCreatePermissionRuntimeServerConfiguration = {
|
||||
allowEveryone: boolean
|
||||
}
|
||||
|
||||
export class ServerConfigurationProvider implements ConfigurationProvider<
|
||||
ServerConfigurationModel,
|
||||
RuntimeServerConfiguration
|
||||
> {
|
||||
constructor(
|
||||
private readonly repository: ServerConfigurationRepository,
|
||||
private readonly serverid: Snowflake
|
||||
) {
|
||||
}
|
||||
|
||||
get defaults(): RuntimeServerConfiguration {
|
||||
return {
|
||||
permissions: {
|
||||
groupCreation: {
|
||||
allowEveryone: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get(path: string): Nullable<ServerConfigurationModel> {
|
||||
return this.repository.findConfigurationByPath(this.serverid, path);
|
||||
}
|
||||
getAll(): ServerConfigurationModel[] {
|
||||
return this.repository.findServerConfigurations(this.serverid);
|
||||
}
|
||||
save(value: Omit<ConfigurationModel, "id"> & Partial<Model>): void {
|
||||
if (value.id) {
|
||||
// @ts-expect-error id is set, due to the check on line above
|
||||
this.repository.update(value);
|
||||
return
|
||||
}
|
||||
|
||||
this.repository.create({
|
||||
...value,
|
||||
serverid: this.serverid
|
||||
});
|
||||
}
|
||||
getTransformer(): ConfigurationTransformer {
|
||||
return new ConfigurationTransformer(
|
||||
[
|
||||
{
|
||||
path: ['permissions', 'groupCreation', 'allowEveryone'],
|
||||
type: TransformerType.PermissionBoolean
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import Commands from "../Discord/Commands/Commands";
|
|||
import {CommandDeployer} from "../Discord/CommandDeployer";
|
||||
import {REST} from "discord.js";
|
||||
import {log} from "node:util";
|
||||
import {ServerConfigurationRepository} from "../Database/Repositories/ServerConfigurationRepository";
|
||||
|
||||
export enum ServiceHint {
|
||||
App,
|
||||
|
|
@ -58,6 +59,7 @@ export class Services {
|
|||
container.set<GroupRepository>(new GroupRepository(db));
|
||||
container.set<PlaydateRepository>(new PlaydateRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||
container.set<GroupConfigurationRepository>(new GroupConfigurationRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||
container.set<ServerConfigurationRepository>(new ServerConfigurationRepository(db));
|
||||
}
|
||||
|
||||
private static setupLogger(hint: ServiceHint): Logger {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class DatabaseUpdater {
|
|||
);
|
||||
|
||||
if (!DBSQLColumns) {
|
||||
Container.get<Logger>("logger").log("Request failed...");
|
||||
Container.get<Logger>("logger").warn("Request for database columns failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ export class DatabaseUpdater {
|
|||
)
|
||||
|
||||
if (missingColumns.length < 1) {
|
||||
Container.get<Logger>("logger").log(`No new columns found for ${definition.name}`)
|
||||
Container.get<Logger>("logger").debug(`No new columns found for ${definition.name}`)
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
6
source/Database/Models/ServerConfigurationModel.ts
Normal file
6
source/Database/Models/ServerConfigurationModel.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import {ConfigurationModel} from "./ConfigurationModel";
|
||||
import {Snowflake} from "discord.js";
|
||||
|
||||
export type ServerConfigurationModel = ConfigurationModel & {
|
||||
serverid: Snowflake
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import {Repository} from "./Repository";
|
||||
import {Nullable} from "../../types/Nullable";
|
||||
import {DatabaseConnection} from "../DatabaseConnection";
|
||||
import {ServerConfigurationModel} from "../Models/ServerConfigurationModel";
|
||||
import ServerConfiguration, {DBServerConfiguration} from "../tables/ServerConfiguration";
|
||||
import {Snowflake} from "discord.js";
|
||||
|
||||
export class ServerConfigurationRepository extends Repository<ServerConfigurationModel, DBServerConfiguration> {
|
||||
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
) {
|
||||
super(
|
||||
database,
|
||||
ServerConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
public findServerConfigurations(server: Snowflake): ServerConfigurationModel[] {
|
||||
return this.database.fetchAll<number, DBServerConfiguration>(
|
||||
`SELECT * FROM serverConfiguration WHERE serverid = ?`,
|
||||
server
|
||||
).map((config) => {
|
||||
return this.convertToModelType(config);
|
||||
})
|
||||
}
|
||||
|
||||
public findConfigurationByPath(server: Snowflake, path: string): Nullable<ServerConfigurationModel> {
|
||||
const result = this.database.fetch<number, DBServerConfiguration>(
|
||||
`SELECT * FROM serverConfiguration WHERE serverid = ? AND key = ?`,
|
||||
server,
|
||||
path
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.convertToModelType(result);
|
||||
}
|
||||
|
||||
|
||||
protected convertToModelType(intermediateModel: DBServerConfiguration | undefined): ServerConfigurationModel {
|
||||
if (!intermediateModel) {
|
||||
throw new Error("No intermediate model provided");
|
||||
}
|
||||
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
serverid: intermediateModel.serverid,
|
||||
key: intermediateModel.key,
|
||||
value: intermediateModel.value,
|
||||
}
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<ServerConfigurationModel>): object {
|
||||
return {
|
||||
serverid: instance.serverid ?? undefined,
|
||||
key: instance.key ?? undefined,
|
||||
value: instance.value ?? undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,13 @@ import Groups from "./tables/Groups";
|
|||
import {DatabaseDefinition} from "./DatabaseDefinition";
|
||||
import Playdate from "./tables/Playdate";
|
||||
import GroupConfiguration from "./tables/GroupConfiguration";
|
||||
import ServerConfiguration from "./tables/ServerConfiguration";
|
||||
|
||||
const definitions = new Set<DatabaseDefinition>([
|
||||
Groups,
|
||||
Playdate,
|
||||
GroupConfiguration
|
||||
GroupConfiguration,
|
||||
ServerConfiguration
|
||||
]);
|
||||
|
||||
export default definitions;
|
||||
37
source/Database/tables/ServerConfiguration.ts
Normal file
37
source/Database/tables/ServerConfiguration.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import {DatabaseDefinition} from "../DatabaseDefinition";
|
||||
|
||||
export type DBServerConfiguration = {
|
||||
id: number;
|
||||
serverid: string;
|
||||
key: string,
|
||||
value: string
|
||||
}
|
||||
|
||||
const dbDefinition: DatabaseDefinition = {
|
||||
name: "serverConfiguration",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "INTEGER",
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: "serverid",
|
||||
type: "VARCHAR",
|
||||
size: 32
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
type: "VARCHAR",
|
||||
size: 32
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "VARCHAR",
|
||||
size: 2 ^ 11
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default dbDefinition;
|
||||
|
|
@ -38,6 +38,10 @@ export class GroupSelection {
|
|||
if (!group) {
|
||||
throw new UserError("No group found");
|
||||
}
|
||||
|
||||
if (group.role.server !== interaction.guildId) {
|
||||
throw new Error("Invalid access to group detected...");
|
||||
}
|
||||
|
||||
return <GroupModel>group;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import {GroupCommand} from "./Groups";
|
|||
import {PlaydatesCommand} from "./Playdates";
|
||||
import {RESTPostAPIChatInputApplicationCommandsJSONBody} from "discord.js";
|
||||
import {Nullable} from "../../types/Nullable";
|
||||
import {ServerCommand} from "./Server";
|
||||
|
||||
const commands: Set<Command> = new Set<Command>([
|
||||
new HelloWorldCommand(),
|
||||
new GroupCommand(),
|
||||
new PlaydatesCommand()
|
||||
new PlaydatesCommand(),
|
||||
new ServerCommand()
|
||||
]);
|
||||
|
||||
export default class Commands {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import {
|
|||
GuildMember,
|
||||
GuildMemberRoleManager,
|
||||
InteractionReplyOptions,
|
||||
MessageFlags,
|
||||
MessageFlags, PermissionFlagsBits,
|
||||
roleMention,
|
||||
SlashCommandBuilder,
|
||||
SlashCommandBuilder, Snowflake,
|
||||
time,
|
||||
userMention
|
||||
} from "discord.js";
|
||||
|
|
@ -28,6 +28,9 @@ 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";
|
||||
|
||||
export class GroupCommand implements Command, ChatInteractionCommand, AutocompleteCommand {
|
||||
private static GOODBYE_MESSAGES: string[] = [
|
||||
|
|
@ -114,6 +117,10 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
|
|
@ -151,6 +158,22 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, 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) {
|
||||
|
|
@ -209,7 +232,7 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
|
||||
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.");
|
||||
throw new PermissionError("You are not the leader.");
|
||||
}
|
||||
|
||||
repo.deleteGroup(group);
|
||||
|
|
@ -320,8 +343,8 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
|
||||
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."
|
||||
throw new PermissionError(
|
||||
"You are not the leader."
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {GroupConfigurationRepository} from "../../Database/Repositories/GroupCon
|
|||
import {GroupRepository} from "../../Database/Repositories/GroupRepository";
|
||||
import {GroupConfigurationProvider} from "../../Configuration/Groups/GroupConfigurationProvider";
|
||||
import { ConfigurationHandler } from "../../Configuration/ConfigurationHandler";
|
||||
import {PermissionError} from "../PermissionError";
|
||||
|
||||
export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand {
|
||||
definition(): SlashCommandBuilder {
|
||||
|
|
@ -216,7 +217,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
|
||||
private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
if (!this.interactionIsAllowedToManage(<ChatInputCommandInteraction>interaction, group)) {
|
||||
throw new UserError(
|
||||
throw new PermissionError(
|
||||
"You are not allowed to delete playdates for this group.",
|
||||
"Ask your Game Master to delete the playdate or ask him to allow everyone to do so."
|
||||
)
|
||||
|
|
@ -401,6 +402,6 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
group
|
||||
)
|
||||
);
|
||||
return config.getConfigurationByPath("permissions.allowMemberManagingPlaydates") === true;
|
||||
return config.getConfigurationByPath("permissions.allowMemberManagingPlaydates").value === true;
|
||||
}
|
||||
}
|
||||
79
source/Discord/Commands/Server.ts
Normal file
79
source/Discord/Commands/Server.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import {CacheType, ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder, Snowflake} from "discord.js";
|
||||
import {ChatInteractionCommand, Command} from "./Command";
|
||||
import {GroupSelection} from "../CommandPartials/GroupSelection";
|
||||
import {MenuHandler} from "../../Configuration/MenuHandler";
|
||||
import {ConfigurationHandler} from "../../Configuration/ConfigurationHandler";
|
||||
import {GroupConfigurationProvider} from "../../Configuration/Groups/GroupConfigurationProvider";
|
||||
import {Container} from "../../Container/Container";
|
||||
import {GroupConfigurationRepository} from "../../Database/Repositories/GroupConfigurationRepository";
|
||||
import {MenuRenderer} from "../../Menu/MenuRenderer";
|
||||
import {MenuTraversal} from "../../Menu/MenuTraversal";
|
||||
import {MenuItemType} from "../../Menu/MenuRenderer.types";
|
||||
import {ServerConfigurationProvider} from "../../Configuration/Server/ServerConfigurationProvider";
|
||||
import {ServerConfigurationRepository} from "../../Database/Repositories/ServerConfigurationRepository";
|
||||
|
||||
export class ServerCommand implements Command, ChatInteractionCommand {
|
||||
definition(): SlashCommandBuilder {
|
||||
return new SlashCommandBuilder()
|
||||
.setName("server")
|
||||
.setDescription("Allows server administrators to adjust things about the PnP Scheduler bot.")
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.addSubcommand(command => command
|
||||
.setName("config")
|
||||
.setDescription("Starts the configurator for the server settings")
|
||||
)
|
||||
}
|
||||
|
||||
async execute(interaction: ChatInputCommandInteraction<CacheType>): Promise<void> {
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case "config":
|
||||
await this.startConfiguration(interaction);
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private async startConfiguration(interaction: ChatInputCommandInteraction) {
|
||||
const menuHandler = new MenuHandler(
|
||||
new ConfigurationHandler(
|
||||
new ServerConfigurationProvider(
|
||||
Container.get<ServerConfigurationRepository>(ServerConfigurationRepository.name),
|
||||
<Snowflake>interaction.guildId
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const menu = new MenuRenderer(
|
||||
new MenuTraversal(
|
||||
menuHandler.fillMenuItems(
|
||||
[
|
||||
{
|
||||
traversalKey: "permissions",
|
||||
label: "Permissions",
|
||||
description: "Allows customization, how the server members are allowed to interact with the PnP Scheduler.",
|
||||
type: MenuItemType.Collection,
|
||||
children: [
|
||||
{
|
||||
traversalKey: "groupCreation",
|
||||
label: "Group Creation",
|
||||
description: "Sets the permissions, who is allowed to create groups.",
|
||||
type: MenuItemType.Collection,
|
||||
children: [
|
||||
{
|
||||
traversalKey: "allowEveryone",
|
||||
label: "Group Creation",
|
||||
description: "Defines if all members are allowed to create groups.",
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
),
|
||||
'Server Configuration',
|
||||
"This UI allows you to change settings for your server."
|
||||
)
|
||||
)
|
||||
|
||||
menu.display(interaction);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import {EventHandler} from "../Events/EventHandler";
|
|||
import {ModalInteractionEvent} from "../Events/EventClasses/ModalInteractionEvent";
|
||||
import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent";
|
||||
import {log} from "node:util";
|
||||
import {PermissionError} from "./PermissionError";
|
||||
|
||||
enum InteractionRoutingType {
|
||||
Unrouted,
|
||||
|
|
@ -95,17 +96,13 @@ export class InteractionRouter {
|
|||
|
||||
let userMessage = ":x: There was an error while executing this command!";
|
||||
let logErrorMessage = true;
|
||||
if (e.constructor.name === UserError.name) {
|
||||
userMessage = `:x: \`${e.message}\` - Please validate your request!`
|
||||
if (e.tryInstead) {
|
||||
userMessage += `
|
||||
|
||||
You can try the following:
|
||||
${inlineCode(e.tryInstead)}`
|
||||
}
|
||||
|
||||
logErrorMessage = false;
|
||||
|
||||
if ("getDiscordMessage" in e) {
|
||||
userMessage = e.getDiscordMessage(e);
|
||||
}
|
||||
if ("shouldLog" in e) {
|
||||
logErrorMessage = e.shouldLog;
|
||||
}
|
||||
|
||||
if (logErrorMessage) {
|
||||
this.logger.error(e)
|
||||
|
|
|
|||
24
source/Discord/PermissionError.ts
Normal file
24
source/Discord/PermissionError.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import {inlineCode} from "discord.js";
|
||||
|
||||
export class PermissionError extends Error {
|
||||
shouldLog: boolean = false;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly tryInstead: string | null = null
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public getDiscordMessage(e: PermissionError): string {
|
||||
let userMessage = `:x: You can not perform this action! ${inlineCode(e.message)}`
|
||||
if (e.tryInstead) {
|
||||
userMessage += `
|
||||
|
||||
You can try the following:
|
||||
${inlineCode(e.tryInstead)}`
|
||||
}
|
||||
|
||||
return userMessage;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
import {inlineCode} from "discord.js";
|
||||
|
||||
export class UserError extends Error {
|
||||
|
||||
shouldLog: boolean = false;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly tryInstead: string | null = null
|
||||
|
|
@ -7,4 +12,15 @@ export class UserError extends Error {
|
|||
|
||||
}
|
||||
|
||||
public getDiscordMessage(e: UserError): string {
|
||||
let userMessage = `:x: \`${e.message}\` - Please validate your request!`
|
||||
if (e.tryInstead) {
|
||||
userMessage += `
|
||||
|
||||
You can try the following:
|
||||
${inlineCode(e.tryInstead)}`
|
||||
}
|
||||
|
||||
return userMessage;
|
||||
}
|
||||
}
|
||||
|
|
@ -73,14 +73,12 @@ export class ReminderEvent implements TimedEvent {
|
|||
)
|
||||
);
|
||||
|
||||
const config = groupConfig.getCompleteConfiguration();
|
||||
const targetChannel = config.channels?.playdateReminders;
|
||||
|
||||
const targetChannel = groupConfig.getConfigurationByPath("channels.playdateReminders").value;
|
||||
if (!targetChannel) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.sendReminder(playdate, targetChannel);
|
||||
return this.sendReminder(playdate, <ChannelId>targetChannel);
|
||||
}, this)
|
||||
|
||||
await Promise.all(promises);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export async function sendCreatedNotificationEventHandler(event: ElementCreatedE
|
|||
)
|
||||
);
|
||||
|
||||
const targetChannel = groupConfig.getConfigurationByPath('channels.newPlaydates');
|
||||
const targetChannel = groupConfig.getConfigurationByPath('channels.newPlaydates').value;
|
||||
if (!targetChannel) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
|||
import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent";
|
||||
import {MenuTraversal} from "./MenuTraversal";
|
||||
import {Prompt} from "./Modals/Prompt";
|
||||
import _ from "lodash";
|
||||
|
||||
export class MenuRenderer {
|
||||
private readonly menuId: string;
|
||||
|
|
@ -86,20 +87,14 @@ export class MenuRenderer {
|
|||
|
||||
private getComponentForMenuItem(menuItem: AnyMenuItem): ActionRowBuilder<MessageActionRowComponentBuilder>[] {
|
||||
if (menuItem.type === MenuItemType.Collection) {
|
||||
const rowCount = Math.ceil(menuItem.children.length / MenuRenderer.MAX_BUTTON_PER_ROW);
|
||||
if (rowCount > MenuRenderer.MAX_USER_ROW_COUNT) {
|
||||
const rows = _.chunk<AnyMenuItem>(menuItem.children, MenuRenderer.MAX_BUTTON_PER_ROW);
|
||||
if (rows.length > MenuRenderer.MAX_USER_ROW_COUNT) {
|
||||
throw new TypeError(
|
||||
`A collection can only have a max of ${MenuRenderer.MAX_USER_ROW_COUNT * MenuRenderer.MAX_BUTTON_PER_ROW} entries!`
|
||||
);
|
||||
}
|
||||
|
||||
const rows = Array.from(Array(rowCount).keys())
|
||||
.map((index) => {
|
||||
const childStart = index * MenuRenderer.MAX_BUTTON_PER_ROW;
|
||||
return menuItem.children.slice(childStart, MenuRenderer.MAX_BUTTON_PER_ROW);
|
||||
})
|
||||
|
||||
return rows.reverse()
|
||||
return rows
|
||||
.map((items) => new ActionRowBuilder<ButtonBuilder>()
|
||||
.setComponents(
|
||||
...items.map(item => new ButtonBuilder()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue