Env for default member permissions (#185)

*  Add env variable for default member permissions

* 🐛 Config loading in environment loader
This commit is contained in:
Manuel 2023-05-16 08:34:16 +02:00 committed by GitHub
parent 0de04ecb20
commit 182459f401
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 134 additions and 37 deletions

View File

@ -22,8 +22,8 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@discord-nestjs/common": "^5.2.4", "@discord-nestjs/common": "^5.2.5",
"@discord-nestjs/core": "^5.3.6", "@discord-nestjs/core": "^5.3.7",
"@discordjs/opus": "^0.9.0", "@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.16.0", "@discordjs/voice": "^0.16.0",
"@jellyfin/sdk": "^0.7.0", "@jellyfin/sdk": "^0.7.0",
@ -45,7 +45,8 @@
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"ws": "^8.13.0", "ws": "^8.13.0",
"zod": "^3.21.4" "zod": "^3.21.4",
"zod-validation-error": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^9.4.2", "@nestjs/cli": "^9.4.2",

View File

@ -1,12 +1,10 @@
import { DiscordModule } from '@discord-nestjs/core'; import { DiscordModule } from '@discord-nestjs/core';
import { Module } from '@nestjs/common'; import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import * as Joi from 'joi';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path'; import { join } from 'path';
import { DiscordConfigService } from './clients/discord/discord.config.service'; import { DiscordConfigService } from './clients/discord/discord.config.service';
@ -16,22 +14,23 @@ import { CommandModule } from './commands/command.module';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { PlaybackModule } from './playback/playback.module'; import { PlaybackModule } from './playback/playback.module';
import { UpdatesModule } from './updates/updates.module'; import { UpdatesModule } from './updates/updates.module';
import {
environmentVariablesSchema,
getEnvironmentVariables,
} from './utils/environment';
import { fromZodError } from 'zod-validation-error';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
validationSchema: Joi.object({ validate(config) {
DISCORD_CLIENT_TOKEN: Joi.string().required(), try {
JELLYFIN_SERVER_ADDRESS: Joi.string().uri().required(), const parsed = environmentVariablesSchema.parse(config);
JELLYFIN_AUTHENTICATION_USERNAME: Joi.string().required(), return parsed;
JELLYFIN_AUTHENTICATION_PASSWORD: Joi.string().required(), } catch (err) {
UPDATER_DISABLE_NOTIFICATIONS: Joi.boolean(), throw fromZodError(err);
LOG_LEVEL: Joi.string() }
.valid('error', 'warn', 'log', 'debug', 'verbose') },
.insensitive()
.default('log'),
PORT: Joi.number().min(1),
}),
}), }),
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client'), rootPath: join(__dirname, '..', 'client'),
@ -52,4 +51,21 @@ import { UpdatesModule } from './updates/updates.module';
controllers: [], controllers: [],
providers: [], providers: [],
}) })
export class AppModule {} export class AppModule implements OnModuleInit {
private readonly logger = new Logger(AppModule.name);
onModuleInit() {
const variables = getEnvironmentVariables();
if (!variables.ALLOW_EVERYONE_FOR_DEFAULT_PERMS) {
return;
}
this.logger.warn(
'WARNING: You are using a potentially dangerous configuration: Everyone on your server has access to your bot. Ensure, that your bot is properly secured. Disable this by setting the environment variable ALLOW_EVERYONE to false',
);
this.logger.warn(
'WARNING: You are using a feature, that will only work for new server invitations. The permissions on existing servers will not be changed',
);
}
}

View File

@ -114,7 +114,11 @@ export class DiscordVoiceService {
} }
playResource(resource: AudioResource<unknown>) { playResource(resource: AudioResource<unknown>) {
this.logger.debug(`Playing audio resource with volume ${resource.volume}`); this.logger.debug(
`Playing audio resource with volume ${
resource.volume?.volume ?? 'unknown'
}`,
);
this.createAndReturnOrGetAudioPlayer().play(resource); this.createAndReturnOrGetAudioPlayer().play(resource);
this.audioResource = resource; this.audioResource = resource;
} }

View File

@ -6,11 +6,13 @@ import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { defaultMemberPermissions } from 'src/utils/environment';
@Injectable() @Injectable()
@Command({ @Command({
name: 'disconnect', name: 'disconnect',
description: 'Join your current voice channel', description: 'Join your current voice channel',
defaultMemberPermissions: defaultMemberPermissions,
}) })
export class DisconnectCommand { export class DisconnectCommand {
constructor( constructor(

View File

@ -5,11 +5,13 @@ import { Injectable } from '@nestjs/common';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { defaultMemberPermissions } from 'src/utils/environment';
@Injectable() @Injectable()
@Command({ @Command({
name: 'help', name: 'help',
description: 'Get help if you&apos;re having problems with this bot', description: 'Get help if you&apos;re having problems with this bot',
defaultMemberPermissions: defaultMemberPermissions,
}) })
export class HelpCommand { export class HelpCommand {
constructor(private readonly discordMessageService: DiscordMessageService) {} constructor(private readonly discordMessageService: DiscordMessageService) {}

View File

@ -6,10 +6,12 @@ import { CommandInteraction } from 'discord.js';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { defaultMemberPermissions } from 'src/utils/environment';
@Command({ @Command({
name: 'next', name: 'next',
description: 'Go to the next track in the playlist', description: 'Go to the next track in the playlist',
defaultMemberPermissions: defaultMemberPermissions,
}) })
@Injectable() @Injectable()
export class SkipTrackCommand { export class SkipTrackCommand {

View File

@ -6,11 +6,13 @@ import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { defaultMemberPermissions } from 'src/utils/environment';
@Injectable() @Injectable()
@Command({ @Command({
name: 'pause', name: 'pause',
description: 'Pause or resume the playback of the current track', description: 'Pause or resume the playback of the current track',
defaultMemberPermissions: defaultMemberPermissions,
}) })
export class PausePlaybackCommand { export class PausePlaybackCommand {
constructor( constructor(

View File

@ -20,19 +20,21 @@ import {
InteractionReplyOptions, InteractionReplyOptions,
} from 'discord.js'; } from 'discord.js';
import { PlaybackService } from '../../playback/playback.service';
import { formatMillisecondsAsHumanReadable } from '../../utils/timeUtils';
import { DiscordMessageService } from '../../clients/discord/discord.message.service'; import { DiscordMessageService } from '../../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../../clients/discord/discord.voice.service';
import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service'; import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service';
import { SearchHint } from '../../models/search/SearchHint'; import { SearchHint } from '../../models/search/SearchHint';
import { PlaybackService } from '../../playback/playback.service';
import { formatMillisecondsAsHumanReadable } from '../../utils/timeUtils';
import { defaultMemberPermissions } from 'src/utils/environment';
import { PlayCommandParams, SearchType } from './play.params.ts'; import { PlayCommandParams, SearchType } from './play.params.ts';
@Injectable() @Injectable()
@Command({ @Command({
name: 'play', name: 'play',
description: 'Search for an item on your Jellyfin instance', description: 'Search for an item on your Jellyfin instance',
defaultMemberPermissions: defaultMemberPermissions,
}) })
export class PlayItemCommand { export class PlayItemCommand {
private readonly logger: Logger = new Logger(PlayItemCommand.name); private readonly logger: Logger = new Logger(PlayItemCommand.name);

View File

@ -26,20 +26,23 @@ import { DiscordMessageService } from '../../clients/discord/discord.message.ser
import { Track } from '../../models/shared/Track'; import { Track } from '../../models/shared/Track';
import { PlaybackService } from '../../playback/playback.service'; import { PlaybackService } from '../../playback/playback.service';
import { chunkArray } from '../../utils/arrayUtils'; import { chunkArray } from '../../utils/arrayUtils';
import { trimStringToFixedLength, zeroPad } from '../../utils/stringUtils/stringUtils'; import {
trimStringToFixedLength,
zeroPad,
} from '../../utils/stringUtils/stringUtils';
import { Interval } from '@nestjs/schedule'; import { Interval } from '@nestjs/schedule';
import { lightFormat } from 'date-fns'; import { lightFormat } from 'date-fns';
import { PlaylistInteractionCollector } from './playlist.interaction-collector'; import { PlaylistInteractionCollector } from './playlist.interaction-collector';
import { PlaylistCommandParams } from './playlist.params'; import { PlaylistCommandParams } from './playlist.params';
import { PlaylistTempCommandData } from './playlist.types'; import { PlaylistTempCommandData } from './playlist.types';
import { tr } from 'date-fns/locale'; import { defaultMemberPermissions } from 'src/utils/environment';
import { takeCoverage } from 'v8';
@Injectable() @Injectable()
@Command({ @Command({
name: 'playlist', name: 'playlist',
description: 'Print the current track information', description: 'Print the current track information',
defaultMemberPermissions: defaultMemberPermissions,
}) })
@UseInterceptors(CollectorInterceptor) @UseInterceptors(CollectorInterceptor)
@UseCollectors(PlaylistInteractionCollector) @UseCollectors(PlaylistInteractionCollector)

View File

@ -6,11 +6,13 @@ import { CommandInteraction } from 'discord.js';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { defaultMemberPermissions } from 'src/utils/environment';
@Injectable() @Injectable()
@Command({ @Command({
name: 'previous', name: 'previous',
description: 'Go to the previous track', description: 'Go to the previous track',
defaultMemberPermissions: defaultMemberPermissions,
}) })
export class PreviousTrackCommand { export class PreviousTrackCommand {
constructor( constructor(

View File

@ -12,10 +12,12 @@ import { JellyfinSearchService } from 'src/clients/jellyfin/jellyfin.search.serv
import { SearchHint } from 'src/models/search/SearchHint'; import { SearchHint } from 'src/models/search/SearchHint';
import { PlaybackService } from 'src/playback/playback.service'; import { PlaybackService } from 'src/playback/playback.service';
import { RandomCommandParams } from './random.params'; import { RandomCommandParams } from './random.params';
import { defaultMemberPermissions } from 'src/utils/environment';
@Command({ @Command({
name: 'random', name: 'random',
description: 'Enqueues a random selection of tracks to your playlist', description: 'Enqueues a random selection of tracks to your playlist',
defaultMemberPermissions: defaultMemberPermissions,
}) })
@Injectable() @Injectable()
export class EnqueueRandomItemsCommand { export class EnqueueRandomItemsCommand {

View File

@ -1,4 +1,9 @@
import { Command, Handler, IA, InjectDiscordClient } from '@discord-nestjs/core'; import {
Command,
Handler,
IA,
InjectDiscordClient,
} from '@discord-nestjs/core';
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
@ -8,13 +13,14 @@ import { Client, CommandInteraction, Status } from 'discord.js';
import { formatDuration, intervalToDuration } from 'date-fns'; import { formatDuration, intervalToDuration } from 'date-fns';
import { Constants } from '../utils/constants';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { JellyfinService } from '../clients/jellyfin/jellyfin.service'; import { JellyfinService } from '../clients/jellyfin/jellyfin.service';
import { Constants } from '../utils/constants';
@Command({ @Command({
name: 'status', name: 'status',
description: 'Display the current status for troubleshooting', description: 'Display the current status for troubleshooting',
defaultMemberPermissions: 'ViewChannel',
}) })
@Injectable() @Injectable()
export class StatusCommand { export class StatusCommand {

View File

@ -7,10 +7,12 @@ import { CommandInteraction } from 'discord.js';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { defaultMemberPermissions } from 'src/utils/environment';
@Command({ @Command({
name: 'stop', name: 'stop',
description: 'Stop playback entirely and clear the current playlist', description: 'Stop playback entirely and clear the current playlist',
defaultMemberPermissions: defaultMemberPermissions,
}) })
@Injectable() @Injectable()
export class StopPlaybackCommand { export class StopPlaybackCommand {

View File

@ -6,11 +6,13 @@ import { CommandInteraction, GuildMember } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { defaultMemberPermissions } from 'src/utils/environment';
@Injectable() @Injectable()
@Command({ @Command({
name: 'summon', name: 'summon',
description: 'Join your current voice channel', description: 'Join your current voice channel',
defaultMemberPermissions: defaultMemberPermissions,
}) })
export class SummonCommand { export class SummonCommand {
private readonly logger = new Logger(SummonCommand.name); private readonly logger = new Logger(SummonCommand.name);

View File

@ -10,11 +10,13 @@ import { DiscordVoiceService } from 'src/clients/discord/discord.voice.service';
import { PlaybackService } from 'src/playback/playback.service'; import { PlaybackService } from 'src/playback/playback.service';
import { sleep } from 'src/utils/timeUtils'; import { sleep } from 'src/utils/timeUtils';
import { VolumeCommandParams } from './volume.params'; import { VolumeCommandParams } from './volume.params';
import { defaultMemberPermissions } from 'src/utils/environment';
@Injectable() @Injectable()
@Command({ @Command({
name: 'volume', name: 'volume',
description: 'Change the volume', description: 'Change the volume',
defaultMemberPermissions: defaultMemberPermissions,
}) })
export class VolumeCommand { export class VolumeCommand {
private readonly logger = new Logger(VolumeCommand.name); private readonly logger = new Logger(VolumeCommand.name);

View File

@ -2,6 +2,7 @@ import { LogLevel } from '@nestjs/common/services';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { INestApplication } from '@nestjs/common';
function getLoggingLevels(): LogLevel[] { function getLoggingLevels(): LogLevel[] {
if (!process.env.LOG_LEVEL) { if (!process.env.LOG_LEVEL) {
@ -25,8 +26,10 @@ function getLoggingLevels(): LogLevel[] {
} }
} }
export let app: INestApplication;
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { app = await NestFactory.create(AppModule, {
logger: getLoggingLevels(), logger: getLoggingLevels(),
}); });
app.enableShutdownHooks(); app.enableShutdownHooks();

39
src/utils/environment.ts Normal file
View File

@ -0,0 +1,39 @@
import { PermissionResolvable } from 'discord.js';
import { z } from 'zod';
import { fromZodError } from 'zod-validation-error';
import * as env from 'dotenv';
env.config();
export const environmentVariablesSchema = z.object({
DISCORD_CLIENT_TOKEN: z.string(),
JELLYFIN_SERVER_ADDRESS: z.string().url(),
JELLYFIN_AUTHENTICATION_USERNAME: z.string(),
JELLYFIN_AUTHENTICATION_PASSWORD: z.string(),
UPDATER_DISABLE_NOTIFICATIONS: z
.enum(['true', 'false'])
.default('false')
.transform((value) => Boolean(value)),
LOG_LEVEL: z
.enum(['ERROR', 'WARN', 'LOG', 'DEBUG', 'VERBOSE'])
.default('LOG'),
PORT: z.preprocess(
(value) => (Number.isInteger(value) ? Number(value) : undefined),
z.number().positive().max(9999).default(3000),
),
ALLOW_EVERYONE_FOR_DEFAULT_PERMS: z
.enum(['true', 'false'])
.default('false')
.transform((value) => Boolean(value)),
});
export const getEnvironmentVariables = () => {
try {
return environmentVariablesSchema.strip().parse(process.env);
} catch (err) {
throw fromZodError(err);
}
};
export const defaultMemberPermissions: PermissionResolvable | undefined =
getEnvironmentVariables().ALLOW_EVERYONE_FOR_DEFAULT_PERMS ? 'ViewChannel' : undefined;

View File

@ -374,19 +374,19 @@
dependencies: dependencies:
"@jridgewell/trace-mapping" "0.3.9" "@jridgewell/trace-mapping" "0.3.9"
"@discord-nestjs/common@^5.2.4": "@discord-nestjs/common@^5.2.5":
version "5.2.4" version "5.2.5"
resolved "https://registry.yarnpkg.com/@discord-nestjs/common/-/common-5.2.4.tgz#294aa89f76651a69692acda81513ea99e7c59ebb" resolved "https://registry.yarnpkg.com/@discord-nestjs/common/-/common-5.2.5.tgz#29ffa962658176e9406ced6220cd165be3f1900c"
integrity sha512-/HbRSqat/3q15nE2OO+QvOSiPopQkHB31KnwEAhzJIUWgqq1l+UdRwqHFm+2pbbcRBQx/o/wpp2ljRgUB1eoGQ== integrity sha512-sVtXLb1wIiUXOaleLidDC5wfdQrP3DyCjIfQaYgzuoIJPKFAs3FQs0Q43DGXqWj6qeyTaZEr+JgwhYYO4Ju5Ww==
dependencies: dependencies:
"@nestjs/mapped-types" "1.2.2" "@nestjs/mapped-types" "1.2.2"
class-transformer "0.5.1" class-transformer "0.5.1"
class-validator "0.14.0" class-validator "0.14.0"
"@discord-nestjs/core@^5.3.6": "@discord-nestjs/core@^5.3.7":
version "5.3.6" version "5.3.7"
resolved "https://registry.yarnpkg.com/@discord-nestjs/core/-/core-5.3.6.tgz#ff7f7d456d05ada0cbe51b070b43551d5278c466" resolved "https://registry.yarnpkg.com/@discord-nestjs/core/-/core-5.3.7.tgz#17da51aadcdd240161b14b8d8b19c299970e8ac1"
integrity sha512-AHPkivFJoL+2G+rqZVzmNGL4X1TGGXvJDgNlULHRN/wn4GXVbHXdB3tLJY5PimgTFtj8jn+XFIbVLlLE/W9kLA== integrity sha512-wJMNeUAh0ZzN+Q0NAexPsd9oXSE/L/YrBd6/vWmd4t0NF0SgrwOqu5roh7IaB8Q040Ysh7wTqhFvEfHn8PMzhQ==
dependencies: dependencies:
class-transformer "0.5.1" class-transformer "0.5.1"
@ -5477,6 +5477,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod-validation-error@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-1.3.0.tgz#6a37e8b1896e45362a4e4cf9506eca203fad3c6e"
integrity sha512-4WoQnuWnj06kwKR4A+cykRxFmy+CTvwMQO5ogTXLiVx1AuvYYmMjixh7sbkSsQTr1Fvtss6d5kVz8PGeMPUQjQ==
zod@^3.21.4: zod@^3.21.4:
version "3.21.4" version "3.21.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"