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 * as esbuild from "esbuild";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
const isDev = process.env.BUILD_TARGET !== 'DOCKER';
|
||||||
|
|
||||||
const context = await esbuild.context({
|
const context = await esbuild.context({
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
path.join('source', 'main.ts'),
|
path.join('source', 'main.ts'),
|
||||||
|
|
@ -10,7 +12,7 @@ const context = await esbuild.context({
|
||||||
outdir: './dist/',
|
outdir: './dist/',
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
target: 'node10.4',
|
target: 'node10.4',
|
||||||
sourcemap: 'linked',
|
sourcemap: isDev ? 'external' : null,
|
||||||
loader: {
|
loader: {
|
||||||
'.node': 'copy',
|
'.node': 'copy',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
"@types/deepmerge": "^2.1.0",
|
"@types/deepmerge": "^2.1.0",
|
||||||
|
"@types/lodash": "^4.17.18",
|
||||||
"@types/log4js": "^0.0.33",
|
"@types/log4js": "^0.0.33",
|
||||||
"@types/node": "^22.13.9",
|
"@types/node": "^22.13.9",
|
||||||
"better-sqlite3": "^11.8.1",
|
"better-sqlite3": "^11.8.1",
|
||||||
|
|
@ -20,6 +21,7 @@
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
"node-cron": "^4.0.7",
|
"node-cron": "^4.0.7",
|
||||||
"node-ical": "^0.20.1",
|
"node-ical": "^0.20.1",
|
||||||
|
|
@ -1636,6 +1638,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/log4js": {
|
||||||
"version": "0.0.33",
|
"version": "0.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/log4js/-/log4js-0.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/log4js/-/log4js-0.0.33.tgz",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
"@types/deepmerge": "^2.1.0",
|
"@types/deepmerge": "^2.1.0",
|
||||||
|
"@types/lodash": "^4.17.18",
|
||||||
"@types/log4js": "^0.0.33",
|
"@types/log4js": "^0.0.33",
|
||||||
"@types/node": "^22.13.9",
|
"@types/node": "^22.13.9",
|
||||||
"better-sqlite3": "^11.8.1",
|
"better-sqlite3": "^11.8.1",
|
||||||
|
|
@ -26,6 +27,7 @@
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
"node-cron": "^4.0.7",
|
"node-cron": "^4.0.7",
|
||||||
"node-ical": "^0.20.1",
|
"node-ical": "^0.20.1",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,17 @@ import deepmerge from "deepmerge";
|
||||||
import {isPlainObject} from "is-plain-object";
|
import {isPlainObject} from "is-plain-object";
|
||||||
import {Nullable} from "../types/Nullable";
|
import {Nullable} from "../types/Nullable";
|
||||||
import {ConfigurationTransformer, TransformerResults} from "./ConfigurationTransformer";
|
import {ConfigurationTransformer, TransformerResults} from "./ConfigurationTransformer";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
export enum PathConfigurationFrom {
|
||||||
|
Database,
|
||||||
|
Default
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathConfiguration = {
|
||||||
|
value: TransformerResults,
|
||||||
|
from: PathConfigurationFrom
|
||||||
|
}
|
||||||
|
|
||||||
export class ConfigurationHandler<
|
export class ConfigurationHandler<
|
||||||
TProviderModel extends ConfigurationModel = ConfigurationModel,
|
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);
|
const configuration = this.provider.get(path);
|
||||||
if (!configuration) {
|
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> {
|
private getCompleteDatabaseConfig(): Partial<TRuntimeConfiguration> {
|
||||||
const values = this.provider.getAll();
|
const values = this.provider.getAll();
|
||||||
const configuration: Partial<TRuntimeConfiguration> = {};
|
const configuration: Partial<TRuntimeConfiguration> = {};
|
||||||
|
|
||||||
values.forEach((configValue) => {
|
values.forEach((configValue) => {
|
||||||
const value = this.transformer.getValue(configValue);
|
const value = this.transformer.getValue(configValue);
|
||||||
setPath(configuration, configValue.key, value);
|
_.set(configuration, configValue.key, value);
|
||||||
})
|
})
|
||||||
|
|
||||||
return configuration;
|
return configuration;
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,6 @@ export type CalendarRuntimeGroupConfiguration = {
|
||||||
location: null | string
|
location: null | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupConfigurationResult =
|
|
||||||
ChannelId | Intl.Locale | boolean | string | null
|
|
||||||
|
|
||||||
export class GroupConfigurationProvider implements ConfigurationProvider<
|
export class GroupConfigurationProvider implements ConfigurationProvider<
|
||||||
GroupConfigurationModel,
|
GroupConfigurationModel,
|
||||||
RuntimeGroupConfiguration
|
RuntimeGroupConfiguration
|
||||||
|
|
@ -66,9 +63,13 @@ export class GroupConfigurationProvider implements ConfigurationProvider<
|
||||||
if (value.id) {
|
if (value.id) {
|
||||||
// @ts-expect-error id is set, due to the check on line above
|
// @ts-expect-error id is set, due to the check on line above
|
||||||
this.repository.update(value);
|
this.repository.update(value);
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.repository.create(value);
|
this.repository.create({
|
||||||
|
...value,
|
||||||
|
group: this.group
|
||||||
|
});
|
||||||
}
|
}
|
||||||
getTransformer(): ConfigurationTransformer {
|
getTransformer(): ConfigurationTransformer {
|
||||||
return new ConfigurationTransformer(
|
return new ConfigurationTransformer(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import {ConfigurationHandler} from "./ConfigurationHandler";
|
import {ConfigurationHandler, PathConfigurationFrom} from "./ConfigurationHandler";
|
||||||
import {
|
import {
|
||||||
AnyMenuItem,
|
AnyMenuItem,
|
||||||
CollectionMenuItem,
|
CollectionMenuItem,
|
||||||
FieldMenuItem, FieldMenuItemContext, FieldMenuItemSaveValue,
|
FieldMenuItem,
|
||||||
|
FieldMenuItemContext,
|
||||||
|
FieldMenuItemSaveValue,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuItemType,
|
MenuItemType,
|
||||||
PromptMenuItem
|
PromptMenuItem
|
||||||
|
|
@ -12,11 +14,11 @@ import {
|
||||||
channelMention,
|
channelMention,
|
||||||
ChannelSelectMenuBuilder,
|
ChannelSelectMenuBuilder,
|
||||||
ChannelType,
|
ChannelType,
|
||||||
inlineCode,
|
|
||||||
italic,
|
italic,
|
||||||
StringSelectMenuBuilder, TextInputBuilder, TextInputStyle
|
StringSelectMenuBuilder,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import {ChannelId} from "../types/DiscordTypes";
|
|
||||||
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
||||||
import {TransformerType} from "./ConfigurationTransformer";
|
import {TransformerType} from "./ConfigurationTransformer";
|
||||||
|
|
||||||
|
|
@ -101,14 +103,14 @@ export class MenuHandler {
|
||||||
|
|
||||||
private getChannelValue(context: FieldMenuItemContext): string {
|
private getChannelValue(context: FieldMenuItemContext): string {
|
||||||
const value = this.config.getConfigurationByPath(context.path.join('.'));
|
const value = this.config.getConfigurationByPath(context.path.join('.'));
|
||||||
if (value === undefined) {
|
|
||||||
return italic("None");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
const isDefault = value.from === PathConfigurationFrom.Default;
|
||||||
return inlineCode("None");
|
const display = !value ? "None" : channelMention(<string>value.value);
|
||||||
|
|
||||||
|
if (isDefault) {
|
||||||
|
return italic(`Default (${display})`)
|
||||||
}
|
}
|
||||||
return channelMention(<ChannelId>value);
|
return display;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getChannelMenuBuilder(): MessageActionRowComponentBuilder {
|
private getChannelMenuBuilder(): MessageActionRowComponentBuilder {
|
||||||
|
|
@ -119,11 +121,14 @@ export class MenuHandler {
|
||||||
|
|
||||||
private getPermissionBooleanValue(context: FieldMenuItemContext) {
|
private getPermissionBooleanValue(context: FieldMenuItemContext) {
|
||||||
const value = this.config.getConfigurationByPath(context.path.join('.'));
|
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() {
|
private getPermissionBooleanBuilder() {
|
||||||
|
|
@ -144,15 +149,8 @@ export class MenuHandler {
|
||||||
|
|
||||||
private getStringValue(context: FieldMenuItemContext): string {
|
private getStringValue(context: FieldMenuItemContext): string {
|
||||||
const value = this.config.getConfigurationByPath(context.path.join('.'));
|
const value = this.config.getConfigurationByPath(context.path.join('.'));
|
||||||
if (!value) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value !== 'string') {
|
return value.value === null ? "" : <string>value.value;
|
||||||
throw new TypeError(`Value of type ${typeof value} can't be used for a string value!`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStringBuilder(): TextInputBuilder {
|
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 {CommandDeployer} from "../Discord/CommandDeployer";
|
||||||
import {REST} from "discord.js";
|
import {REST} from "discord.js";
|
||||||
import {log} from "node:util";
|
import {log} from "node:util";
|
||||||
|
import {ServerConfigurationRepository} from "../Database/Repositories/ServerConfigurationRepository";
|
||||||
|
|
||||||
export enum ServiceHint {
|
export enum ServiceHint {
|
||||||
App,
|
App,
|
||||||
|
|
@ -58,6 +59,7 @@ export class Services {
|
||||||
container.set<GroupRepository>(new GroupRepository(db));
|
container.set<GroupRepository>(new GroupRepository(db));
|
||||||
container.set<PlaydateRepository>(new PlaydateRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
container.set<PlaydateRepository>(new PlaydateRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||||
container.set<GroupConfigurationRepository>(new GroupConfigurationRepository(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 {
|
private static setupLogger(hint: ServiceHint): Logger {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export class DatabaseUpdater {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!DBSQLColumns) {
|
if (!DBSQLColumns) {
|
||||||
Container.get<Logger>("logger").log("Request failed...");
|
Container.get<Logger>("logger").warn("Request for database columns failed!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ export class DatabaseUpdater {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (missingColumns.length < 1) {
|
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;
|
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 {DatabaseDefinition} from "./DatabaseDefinition";
|
||||||
import Playdate from "./tables/Playdate";
|
import Playdate from "./tables/Playdate";
|
||||||
import GroupConfiguration from "./tables/GroupConfiguration";
|
import GroupConfiguration from "./tables/GroupConfiguration";
|
||||||
|
import ServerConfiguration from "./tables/ServerConfiguration";
|
||||||
|
|
||||||
const definitions = new Set<DatabaseDefinition>([
|
const definitions = new Set<DatabaseDefinition>([
|
||||||
Groups,
|
Groups,
|
||||||
Playdate,
|
Playdate,
|
||||||
GroupConfiguration
|
GroupConfiguration,
|
||||||
|
ServerConfiguration
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default definitions;
|
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;
|
||||||
|
|
@ -39,6 +39,10 @@ export class GroupSelection {
|
||||||
throw new UserError("No group found");
|
throw new UserError("No group found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (group.role.server !== interaction.guildId) {
|
||||||
|
throw new Error("Invalid access to group detected...");
|
||||||
|
}
|
||||||
|
|
||||||
return <GroupModel>group;
|
return <GroupModel>group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,11 +4,13 @@ import {GroupCommand} from "./Groups";
|
||||||
import {PlaydatesCommand} from "./Playdates";
|
import {PlaydatesCommand} from "./Playdates";
|
||||||
import {RESTPostAPIChatInputApplicationCommandsJSONBody} from "discord.js";
|
import {RESTPostAPIChatInputApplicationCommandsJSONBody} from "discord.js";
|
||||||
import {Nullable} from "../../types/Nullable";
|
import {Nullable} from "../../types/Nullable";
|
||||||
|
import {ServerCommand} from "./Server";
|
||||||
|
|
||||||
const commands: Set<Command> = new Set<Command>([
|
const commands: Set<Command> = new Set<Command>([
|
||||||
new HelloWorldCommand(),
|
new HelloWorldCommand(),
|
||||||
new GroupCommand(),
|
new GroupCommand(),
|
||||||
new PlaydatesCommand()
|
new PlaydatesCommand(),
|
||||||
|
new ServerCommand()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default class Commands {
|
export default class Commands {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import {
|
||||||
GuildMember,
|
GuildMember,
|
||||||
GuildMemberRoleManager,
|
GuildMemberRoleManager,
|
||||||
InteractionReplyOptions,
|
InteractionReplyOptions,
|
||||||
MessageFlags,
|
MessageFlags, PermissionFlagsBits,
|
||||||
roleMention,
|
roleMention,
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder, Snowflake,
|
||||||
time,
|
time,
|
||||||
userMention
|
userMention
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
|
|
@ -28,6 +28,9 @@ import {MenuTraversal} from "../../Menu/MenuTraversal";
|
||||||
import {ConfigurationHandler} from "../../Configuration/ConfigurationHandler";
|
import {ConfigurationHandler} from "../../Configuration/ConfigurationHandler";
|
||||||
import {GroupConfigurationProvider} from "../../Configuration/Groups/GroupConfigurationProvider";
|
import {GroupConfigurationProvider} from "../../Configuration/Groups/GroupConfigurationProvider";
|
||||||
import {MenuHandler} from "../../Configuration/MenuHandler";
|
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 {
|
export class GroupCommand implements Command, ChatInteractionCommand, AutocompleteCommand {
|
||||||
private static GOODBYE_MESSAGES: string[] = [
|
private static GOODBYE_MESSAGES: string[] = [
|
||||||
|
|
@ -114,6 +117,10 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
||||||
}
|
}
|
||||||
|
|
||||||
private create(interaction: ChatInputCommandInteraction): void {
|
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 name = interaction.options.getString("name") ?? '';
|
||||||
const role = interaction.options.getRole("role", true);
|
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})
|
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 {
|
private validateGroupName(name: string): string | null {
|
||||||
const lowercaseName = name.toLowerCase();
|
const lowercaseName = name.toLowerCase();
|
||||||
for (const invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) {
|
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);
|
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||||
if (group.leader.memberid != interaction.member?.user.id) {
|
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);
|
repo.deleteGroup(group);
|
||||||
|
|
@ -320,8 +343,8 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
||||||
|
|
||||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||||
if (group.leader.memberid != interaction.member?.user.id) {
|
if (group.leader.memberid != interaction.member?.user.id) {
|
||||||
throw new UserError(
|
throw new PermissionError(
|
||||||
"Can't transfer leadership. You are not the leader."
|
"You are not the leader."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {GroupConfigurationRepository} from "../../Database/Repositories/GroupCon
|
||||||
import {GroupRepository} from "../../Database/Repositories/GroupRepository";
|
import {GroupRepository} from "../../Database/Repositories/GroupRepository";
|
||||||
import {GroupConfigurationProvider} from "../../Configuration/Groups/GroupConfigurationProvider";
|
import {GroupConfigurationProvider} from "../../Configuration/Groups/GroupConfigurationProvider";
|
||||||
import { ConfigurationHandler } from "../../Configuration/ConfigurationHandler";
|
import { ConfigurationHandler } from "../../Configuration/ConfigurationHandler";
|
||||||
|
import {PermissionError} from "../PermissionError";
|
||||||
|
|
||||||
export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand {
|
export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand {
|
||||||
definition(): SlashCommandBuilder {
|
definition(): SlashCommandBuilder {
|
||||||
|
|
@ -216,7 +217,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
||||||
|
|
||||||
private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||||
if (!this.interactionIsAllowedToManage(<ChatInputCommandInteraction>interaction, group)) {
|
if (!this.interactionIsAllowedToManage(<ChatInputCommandInteraction>interaction, group)) {
|
||||||
throw new UserError(
|
throw new PermissionError(
|
||||||
"You are not allowed to delete playdates for this group.",
|
"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."
|
"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
|
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 {ModalInteractionEvent} from "../Events/EventClasses/ModalInteractionEvent";
|
||||||
import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent";
|
import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent";
|
||||||
import {log} from "node:util";
|
import {log} from "node:util";
|
||||||
|
import {PermissionError} from "./PermissionError";
|
||||||
|
|
||||||
enum InteractionRoutingType {
|
enum InteractionRoutingType {
|
||||||
Unrouted,
|
Unrouted,
|
||||||
|
|
@ -95,16 +96,12 @@ export class InteractionRouter {
|
||||||
|
|
||||||
let userMessage = ":x: There was an error while executing this command!";
|
let userMessage = ":x: There was an error while executing this command!";
|
||||||
let logErrorMessage = true;
|
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:
|
if ("getDiscordMessage" in e) {
|
||||||
${inlineCode(e.tryInstead)}`
|
userMessage = e.getDiscordMessage(e);
|
||||||
}
|
}
|
||||||
|
if ("shouldLog" in e) {
|
||||||
logErrorMessage = false;
|
logErrorMessage = e.shouldLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logErrorMessage) {
|
if (logErrorMessage) {
|
||||||
|
|
|
||||||
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 {
|
export class UserError extends Error {
|
||||||
|
|
||||||
|
shouldLog: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public readonly tryInstead: string | null = null
|
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 = groupConfig.getConfigurationByPath("channels.playdateReminders").value;
|
||||||
const targetChannel = config.channels?.playdateReminders;
|
|
||||||
|
|
||||||
if (!targetChannel) {
|
if (!targetChannel) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sendReminder(playdate, targetChannel);
|
return this.sendReminder(playdate, <ChannelId>targetChannel);
|
||||||
}, this)
|
}, this)
|
||||||
|
|
||||||
await Promise.all(promises);
|
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) {
|
if (!targetChannel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {MessageActionRowComponentBuilder} from "@discordjs/builders";
|
||||||
import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent";
|
import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent";
|
||||||
import {MenuTraversal} from "./MenuTraversal";
|
import {MenuTraversal} from "./MenuTraversal";
|
||||||
import {Prompt} from "./Modals/Prompt";
|
import {Prompt} from "./Modals/Prompt";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
export class MenuRenderer {
|
export class MenuRenderer {
|
||||||
private readonly menuId: string;
|
private readonly menuId: string;
|
||||||
|
|
@ -86,20 +87,14 @@ export class MenuRenderer {
|
||||||
|
|
||||||
private getComponentForMenuItem(menuItem: AnyMenuItem): ActionRowBuilder<MessageActionRowComponentBuilder>[] {
|
private getComponentForMenuItem(menuItem: AnyMenuItem): ActionRowBuilder<MessageActionRowComponentBuilder>[] {
|
||||||
if (menuItem.type === MenuItemType.Collection) {
|
if (menuItem.type === MenuItemType.Collection) {
|
||||||
const rowCount = Math.ceil(menuItem.children.length / MenuRenderer.MAX_BUTTON_PER_ROW);
|
const rows = _.chunk<AnyMenuItem>(menuItem.children, MenuRenderer.MAX_BUTTON_PER_ROW);
|
||||||
if (rowCount > MenuRenderer.MAX_USER_ROW_COUNT) {
|
if (rows.length > MenuRenderer.MAX_USER_ROW_COUNT) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`A collection can only have a max of ${MenuRenderer.MAX_USER_ROW_COUNT * MenuRenderer.MAX_BUTTON_PER_ROW} entries!`
|
`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())
|
return rows
|
||||||
.map((index) => {
|
|
||||||
const childStart = index * MenuRenderer.MAX_BUTTON_PER_ROW;
|
|
||||||
return menuItem.children.slice(childStart, MenuRenderer.MAX_BUTTON_PER_ROW);
|
|
||||||
})
|
|
||||||
|
|
||||||
return rows.reverse()
|
|
||||||
.map((items) => new ActionRowBuilder<ButtonBuilder>()
|
.map((items) => new ActionRowBuilder<ButtonBuilder>()
|
||||||
.setComponents(
|
.setComponents(
|
||||||
...items.map(item => new ButtonBuilder()
|
...items.map(item => new ButtonBuilder()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue