From 29aa61345cce711d2c823d36c5a5139e7385ca3b Mon Sep 17 00:00:00 2001 From: Maximus7474 Date: Wed, 1 Jul 2026 19:32:01 +0200 Subject: [PATCH 1/6] feat(core): discord punishment channel logging --- core/modules/ConfigStore/schema/discordBot.ts | 12 ++- core/modules/ConfigStore/schema/oldConfig.ts | 3 +- core/modules/DiscordBot/index.ts | 76 +++++++++++++++++-- core/routes/history/actions.ts | 36 +++++++++ core/routes/player/actions.ts | 64 ++++++++++++++++ core/routes/settings/saveConfigs.ts | 5 +- 6 files changed, 184 insertions(+), 12 deletions(-) diff --git a/core/modules/ConfigStore/schema/discordBot.ts b/core/modules/ConfigStore/schema/discordBot.ts index 144f8ff53..dd8c9bb96 100644 --- a/core/modules/ConfigStore/schema/discordBot.ts +++ b/core/modules/ConfigStore/schema/discordBot.ts @@ -25,7 +25,14 @@ const guild = typeNullableConfig({ fixer: SYM_FIXER_DEFAULT, }); -const warningsChannel = typeNullableConfig({ +const punishmentsChannel = typeNullableConfig({ + name: 'Restarts Channel ID', + default: null, + validator: discordSnowflakeSchema.nullable(), + fixer: SYM_FIXER_DEFAULT, +}); + +const restartsChannel = typeNullableConfig({ name: 'Warnings Channel ID', default: null, validator: discordSnowflakeSchema.nullable(), @@ -63,7 +70,8 @@ export default { enabled, token, guild, - warningsChannel, + punishmentsChannel, + restartsChannel, embedJson, embedConfigJson, } as const; diff --git a/core/modules/ConfigStore/schema/oldConfig.ts b/core/modules/ConfigStore/schema/oldConfig.ts index c9e70c026..133e436e3 100644 --- a/core/modules/ConfigStore/schema/oldConfig.ts +++ b/core/modules/ConfigStore/schema/oldConfig.ts @@ -34,7 +34,8 @@ const restructureOldConfig = (old: any) => { enabled: old?.discordBot?.enabled, token: old?.discordBot?.token, guild: old?.discordBot?.guild, - warningsChannel: old?.discordBot?.announceChannel, //NOTE:renamed + punishmentsChannel: old?.discordBot?.punishmentsChannel, + restartsChannel: old?.discordBot?.announceChannel, //NOTE:renamed embedJson: old?.discordBot?.embedJson, embedConfigJson: old?.discordBot?.embedConfigJson, }, diff --git a/core/modules/DiscordBot/index.ts b/core/modules/DiscordBot/index.ts index 7645dc6b4..3fb3ada03 100644 --- a/core/modules/DiscordBot/index.ts +++ b/core/modules/DiscordBot/index.ts @@ -7,6 +7,7 @@ import consoleFactory from '@lib/console'; import { embedColors } from './discordHelpers'; import { DiscordBotStatus } from '@shared/enums'; import { UpdateConfigKeySet } from '@modules/ConfigStore/utils'; +import { AuthedAdmin } from '@modules/WebServer/authLogic'; const console = consoleFactory(modulename); @@ -20,10 +21,16 @@ type AnnouncementType = { description: string | MessageTranslationType; type: keyof typeof embedColors; } +type PunishmentType = { + admin: AuthedAdmin; + title?: string | MessageTranslationType; + description: string | MessageTranslationType; + type: keyof typeof embedColors; +} type SpawnConfig = Pick< TxConfigs['discordBot'], - 'enabled' | 'token' | 'guild' | 'warningsChannel' + 'enabled' | 'token' | 'guild' | 'punishmentsChannel' | 'restartsChannel' >; @@ -59,6 +66,7 @@ export default class DiscordBot { guild: Discord.Guild | undefined; guildName: string | undefined; announceChannel: Discord.TextBasedChannel | undefined; + punishmentsChannel: Discord.TextBasedChannel | undefined; #lastDisallowedIntentsError: number = 0; //ms #lastGuildMembersCacheRefresh: number = 0; //ms #lastStatus = DiscordBotStatus.Disabled; @@ -161,7 +169,7 @@ export default class DiscordBot { async sendAnnouncement(content: AnnouncementType) { if (!txConfig.discordBot.enabled) return; if ( - !txConfig.discordBot.warningsChannel + !txConfig.discordBot.punishmentsChannel || !this.#client?.isReady() || !this.announceChannel ) { @@ -191,6 +199,47 @@ export default class DiscordBot { } + /** + * Send an announcement to the configured channel + */ + async sendPunishment(content: PunishmentType) { + if (!txConfig.discordBot.enabled) return; + if ( + !txConfig.discordBot.punishmentsChannel + || !this.#client?.isReady() + || !this.punishmentsChannel + ) { + console.verbose.warn('not ready yet to send announcement'); + return false; + } + + try { + let title; + if (content.title) { + title = (typeof content.title === 'string') + ? content.title + : txCore.translator.t(content.title.key, content.title.data); + } + let description; + if (content.description) { + description = (typeof content.description === 'string') + ? content.description + : txCore.translator.t(content.description.key, content.description.data); + } + + const embed = new EmbedBuilder({ title, description }) + .setColor(embedColors[content.type]) + .setAuthor({ + name: content.admin.name, + iconURL: content.admin.profilePicture, + }); + await this.punishmentsChannel.send({ embeds: [embed] }); + } catch (error) { + console.error(`Error sending Discord punishment: ${(error as Error).message}`); + } + } + + /** * Update persistent status and activity */ @@ -239,7 +288,8 @@ export default class DiscordBot { enabled: txConfig.discordBot.enabled, token: txConfig.discordBot.token, guild: txConfig.discordBot.guild, - warningsChannel: txConfig.discordBot.warningsChannel, + punishmentsChannel: txConfig.discordBot.punishmentsChannel, + restartsChannel: txConfig.discordBot.restartsChannel, } if (!botCfg.enabled) return; @@ -354,13 +404,25 @@ export default class DiscordBot { } } + //Fetching warnings channel + if (botCfg.punishmentsChannel) { + const fetchedChannel = this.#client.channels.cache.find((x) => x.id === botCfg.punishmentsChannel); + if (!fetchedChannel) { + return sendError(`Channel ${botCfg.punishmentsChannel} not found.`); + } else if (fetchedChannel.type !== ChannelType.GuildText && fetchedChannel.type !== ChannelType.GuildAnnouncement) { + return sendError(`Channel ${botCfg.punishmentsChannel} - ${(fetchedChannel as any)?.name} is not a text or announcement channel.`); + } else { + this.punishmentsChannel = fetchedChannel; + } + } + //Fetching announcements channel - if (botCfg.warningsChannel) { - const fetchedChannel = this.#client.channels.cache.find((x) => x.id === botCfg.warningsChannel); + if (botCfg.restartsChannel) { + const fetchedChannel = this.#client.channels.cache.find((x) => x.id === botCfg.restartsChannel); if (!fetchedChannel) { - return sendError(`Channel ${botCfg.warningsChannel} not found.`); + return sendError(`Channel ${botCfg.restartsChannel} not found.`); } else if (fetchedChannel.type !== ChannelType.GuildText && fetchedChannel.type !== ChannelType.GuildAnnouncement) { - return sendError(`Channel ${botCfg.warningsChannel} - ${(fetchedChannel as any)?.name} is not a text or announcement channel.`); + return sendError(`Channel ${botCfg.restartsChannel} - ${(fetchedChannel as any)?.name} is not a text or announcement channel.`); } else { this.announceChannel = fetchedChannel; } diff --git a/core/routes/history/actions.ts b/core/routes/history/actions.ts index 8756bd392..1ee53f745 100644 --- a/core/routes/history/actions.ts +++ b/core/routes/history/actions.ts @@ -99,6 +99,22 @@ async function handleBandIds(ctx: AuthedCtx): Promise { expiration, false ); + + txCore.discordBot.sendPunishment({ + admin: ctx.admin, + type: 'danger', + title: { + key: 'ban_messages.embed.title', + }, + description: { + key: 'ban_messages.embed.idban_description', + data: { + expiration: expiration ? `` : 'X', + reason, + identifiers: identifiers.join('\n'), + } + } + }); } catch (error) { return { error: `Failed to ban identifiers: ${(error as Error).message}` }; } @@ -162,6 +178,26 @@ async function handleRevokeAction(ctx: AuthedCtx): Promise { try { action = txCore.database.actions.revoke(actionId, ctx.admin.name, perms) as DatabaseActionType; ctx.admin.logAction(`Revoked ${action.type} id ${actionId} from ${action.playerName ?? 'identifiers'}`); + + txCore.discordBot.sendPunishment({ + admin: ctx.admin, + type: 'info', + title: { + key: 'revocation_messages.embed.title', + data: { + action: action.type, + }, + }, + description: { + key: 'revocation_messages.embed.description', + data: { + actionId, + action: action.type, + target: action.playerName ?? 'identifiers', + identifiers: action.ids.join('\n'), + }, + }, + }); } catch (error) { return { error: `Failed to revoke action: ${(error as Error).message}` }; } diff --git a/core/routes/player/actions.ts b/core/routes/player/actions.ts index e75166088..586682ee0 100644 --- a/core/routes/player/actions.ts +++ b/core/routes/player/actions.ts @@ -117,6 +117,22 @@ async function handleWarning(ctx: AuthedCtx, player: PlayerClass): Promise` : 'X', + identifiers: player.allIdentifiers.join('\n'), + } + } + }); } catch (error) { return { error: `Failed to ban player: ${(error as Error).message}` }; } @@ -359,6 +392,21 @@ async function handleDirectMessage(ctx: AuthedCtx, player: PlayerClass): Promise try { ctx.admin.logAction(`DM to "${player.displayName}": ${message}`); + + txCore.discordBot.sendPunishment({ + admin: ctx.admin, + type: 'danger', + title: { + key: 'dm_messages.embed.title', + }, + description: { + key: 'dm_messages.embed.description', + data: { + player: player.displayName, + message, + } + } + }); // Dispatch `txAdmin:events:playerDirectMessage` txCore.fxRunner.sendEvent('playerDirectMessage', { @@ -406,6 +454,22 @@ async function handleKick(ctx: AuthedCtx, player: PlayerClass): Promise { [schemas.enabled, inputConfig.discordBot.enabled], [schemas.token, inputConfig.discordBot.token], [schemas.guild, inputConfig.discordBot.guild], - [schemas.warningsChannel, inputConfig.discordBot.warningsChannel], + [schemas.restartsChannel, inputConfig.discordBot.restartsChannel], + [schemas.punishmentsChannel, inputConfig.discordBot.punishmentsChannel], ]); if (validationError) { return sendTypedResp({ @@ -360,7 +361,7 @@ const handleDiscordCard: CardHandler = async (inputConfig, sendTypedResp) => { //They have been validated, so this is fine token: inputConfig.discordBot.token as any, guild: inputConfig.discordBot.guild as any, - warningsChannel: inputConfig.discordBot.warningsChannel as any, + punishmentsChannel: inputConfig.discordBot.punishmentsChannel as any, }); } catch (error) { const errorCode = (error as any).code; From 8bd8903d881d6562ecec0a0071e18ae70b962ed1 Mon Sep 17 00:00:00 2001 From: Maximus7474 Date: Wed, 1 Jul 2026 19:32:23 +0200 Subject: [PATCH 2/6] chore(panel/settings): added punishment channel field --- panel/src/pages/Settings/tabCards/discord.tsx | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/panel/src/pages/Settings/tabCards/discord.tsx b/panel/src/pages/Settings/tabCards/discord.tsx index 8091e6026..e736e91b9 100644 --- a/panel/src/pages/Settings/tabCards/discord.tsx +++ b/panel/src/pages/Settings/tabCards/discord.tsx @@ -26,7 +26,8 @@ export const pageConfigs = { botEnabled: getPageConfig('discordBot', 'enabled'), botToken: getPageConfig('discordBot', 'token'), discordGuild: getPageConfig('discordBot', 'guild'), - warningsChannel: getPageConfig('discordBot', 'warningsChannel'), + restartsChannel: getPageConfig('discordBot', 'restartsChannel'), + punishmentsChannel: getPageConfig('discordBot', 'punishmentsChannel'), embedJson: getPageConfig('discordBot', 'embedJson'), embedConfigJson: getPageConfig('discordBot', 'embedConfigJson'), } as const; @@ -49,7 +50,8 @@ export default function ConfigCardDiscord({ cardCtx, pageCtx }: SettingsCardProp //Refs for configs that don't use state const botTokenRef = useRef(null); const discordGuildRef = useRef(null); - const warningsChannelRef = useRef(null); + const punishmentsChannelRef = useRef(null); + const restartsChannelRef = useRef(null); //Marshalling Utils const emptyToNull = (str?: string) => { @@ -63,7 +65,8 @@ export default function ConfigCardDiscord({ cardCtx, pageCtx }: SettingsCardProp const overwrites = { botToken: emptyToNull(botTokenRef.current?.value), discordGuild: emptyToNull(discordGuildRef.current?.value), - warningsChannel: emptyToNull(warningsChannelRef.current?.value), + punishmentsChannel: emptyToNull(punishmentsChannelRef.current?.value), + restartsChannel: emptyToNull(restartsChannelRef.current?.value), }; const res = getConfigDiff(cfg, states, overwrites, false); @@ -144,11 +147,11 @@ export default function ConfigCardDiscord({ cardCtx, pageCtx }: SettingsCardProp To get the Server ID, go to Discord's settings and enable developer mode, then right-click on the guild icon select "Copy ID". - + enable developer mode, then right-click on the channel name and select "Copy ID". + + + + The ID of the channel to send Punishments (eg warns, kicks, bans).
+ You can leave it blank to disable this feature.
+ To get the channel ID, go to Discord's settings and enable developer mode, then right-click on the channel name and select "Copy ID". +
+
{/*