deploy #2
@@ -0,0 +1,16 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Warning" ADD COLUMN "warningNumber" INTEGER;
|
||||||
|
|
||||||
|
-- Backfill per-guild sequential numbers (oldest first)
|
||||||
|
WITH numbered AS (
|
||||||
|
SELECT id, ROW_NUMBER() OVER (PARTITION BY "guildId" ORDER BY "createdAt" ASC, id ASC) AS rn
|
||||||
|
FROM "Warning"
|
||||||
|
)
|
||||||
|
UPDATE "Warning" AS w
|
||||||
|
SET "warningNumber" = numbered.rn
|
||||||
|
FROM numbered
|
||||||
|
WHERE w.id = numbered.id;
|
||||||
|
|
||||||
|
ALTER TABLE "Warning" ALTER COLUMN "warningNumber" SET NOT NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "Warning_guildId_warningNumber_key" ON "Warning"("guildId", "warningNumber");
|
||||||
@@ -130,6 +130,7 @@ model Case {
|
|||||||
|
|
||||||
model Warning {
|
model Warning {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
warningNumber Int
|
||||||
guildId String
|
guildId String
|
||||||
userId String
|
userId String
|
||||||
moderatorId String
|
moderatorId String
|
||||||
@@ -140,6 +141,7 @@ model Warning {
|
|||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
case Case? @relation(fields: [caseId], references: [id], onDelete: SetNull)
|
case Case? @relation(fields: [caseId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@unique([guildId, warningNumber])
|
||||||
@@index([guildId, userId])
|
@@index([guildId, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ChannelType, PermissionFlagsBits, type Guild, type GuildMember, type Message, type TextChannel } from 'discord.js';
|
import { ChannelType, PermissionFlagsBits, type Guild, type GuildMember, type Message, type TextChannel } from 'discord.js';
|
||||||
import type { BotContext } from '../../types.js';
|
import type { BotContext } from '../../types.js';
|
||||||
import { createCase } from '../moderation/service.js';
|
import { createCase, createWarning } from '../moderation/service.js';
|
||||||
|
import { getGuildLocale } from '../../i18n.js';
|
||||||
import {
|
import {
|
||||||
getAutoModConfig,
|
getAutoModConfig,
|
||||||
getEnabledAutoModRules,
|
getEnabledAutoModRules,
|
||||||
@@ -41,15 +42,8 @@ async function applyAutoModAction(
|
|||||||
const baseReason = `AutoMod (${ruleType}): ${reason}`;
|
const baseReason = `AutoMod (${ruleType}): ${reason}`;
|
||||||
|
|
||||||
if (action === 'WARN') {
|
if (action === 'WARN') {
|
||||||
await context.prisma.warning.create({
|
const locale = await getGuildLocale(context.prisma, guild.id);
|
||||||
data: {
|
const modCase = await createCase(context, {
|
||||||
guildId: guild.id,
|
|
||||||
userId: message.author.id,
|
|
||||||
moderatorId,
|
|
||||||
reason: baseReason
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await createCase(context, {
|
|
||||||
guildId: guild.id,
|
guildId: guild.id,
|
||||||
targetUserId: message.author.id,
|
targetUserId: message.author.id,
|
||||||
moderatorId,
|
moderatorId,
|
||||||
@@ -59,6 +53,16 @@ async function applyAutoModAction(
|
|||||||
source: 'automod',
|
source: 'automod',
|
||||||
metadata: { ruleType, automod: true }
|
metadata: { ruleType, automod: true }
|
||||||
});
|
});
|
||||||
|
await createWarning(context, {
|
||||||
|
guildId: guild.id,
|
||||||
|
userId: message.author.id,
|
||||||
|
moderatorId,
|
||||||
|
reason: baseReason,
|
||||||
|
caseId: modCase.id,
|
||||||
|
guildName: guild.name,
|
||||||
|
locale,
|
||||||
|
notifyUser: message.author
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,9 +96,12 @@ export const warnCommandData = applyCommandDescription(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addSubcommand((s) =>
|
.addSubcommand((s) =>
|
||||||
applyCommandDescription(s.setName('remove'), 'moderation.warn.remove.description').addStringOption(
|
applyCommandDescription(s.setName('remove'), 'moderation.warn.remove.description').addIntegerOption(
|
||||||
(o) =>
|
(o) =>
|
||||||
applyOptionDescription(o.setName('warning_id').setRequired(true), 'moderation.warn.options.warning_id')
|
applyOptionDescription(
|
||||||
|
o.setName('id').setRequired(true).setMinValue(1),
|
||||||
|
'moderation.warn.options.warning_id'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addSubcommand((s) =>
|
.addSubcommand((s) =>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { t, tf } from '@nexumi/shared';
|
|||||||
import type { SlashCommand } from '../../types.js';
|
import type { SlashCommand } from '../../types.js';
|
||||||
import { requirePermission } from '../../permissions.js';
|
import { requirePermission } from '../../permissions.js';
|
||||||
import { assertModerableMember } from '../../hierarchy.js';
|
import { assertModerableMember } from '../../hierarchy.js';
|
||||||
import { applyWarnEscalation, createCase } from './service.js';
|
import { applyWarnEscalation, createCase, createWarning } from './service.js';
|
||||||
import { parseDuration, parseTimeoutDuration, tryParseDuration } from './duration.js';
|
import { parseDuration, parseTimeoutDuration, tryParseDuration } from './duration.js';
|
||||||
import { getGuildLocale } from '../../i18n.js';
|
import { getGuildLocale } from '../../i18n.js';
|
||||||
import { promptDestructiveConfirmation } from './confirmations.js';
|
import { promptDestructiveConfirmation } from './confirmations.js';
|
||||||
@@ -307,19 +307,15 @@ const warnCommand: SlashCommand = {
|
|||||||
action: 'WARN_ADD',
|
action: 'WARN_ADD',
|
||||||
reason
|
reason
|
||||||
});
|
});
|
||||||
await context.prisma.user.upsert({
|
const { warning } = await createWarning(context, {
|
||||||
where: { id: user.id },
|
|
||||||
update: {},
|
|
||||||
create: { id: user.id }
|
|
||||||
});
|
|
||||||
const warning = await context.prisma.warning.create({
|
|
||||||
data: {
|
|
||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
reason,
|
reason,
|
||||||
caseId: modCase.id
|
caseId: modCase.id,
|
||||||
}
|
guildName: interaction.guild!.name,
|
||||||
|
locale,
|
||||||
|
notifyUser: user
|
||||||
});
|
});
|
||||||
const escalationResult = await applyWarnEscalation(
|
const escalationResult = await applyWarnEscalation(
|
||||||
interaction,
|
interaction,
|
||||||
@@ -330,8 +326,8 @@ const warnCommand: SlashCommand = {
|
|||||||
);
|
);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: escalationResult
|
content: escalationResult
|
||||||
? `${tf(locale, 'moderation.warning.created', { warningId: warning.id })}\n${escalationResult}`
|
? `${tf(locale, 'moderation.warning.created', { warningNumber: warning.warningNumber })}\n${escalationResult}`
|
||||||
: tf(locale, 'moderation.warning.created', { warningId: warning.id }),
|
: tf(locale, 'moderation.warning.created', { warningNumber: warning.warningNumber }),
|
||||||
ephemeral: true
|
ephemeral: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -349,8 +345,8 @@ const warnCommand: SlashCommand = {
|
|||||||
? t(locale, 'moderation.warning.none')
|
? t(locale, 'moderation.warning.none')
|
||||||
: warnings
|
: warnings
|
||||||
.map(
|
.map(
|
||||||
(w: { id: string; reason: string | null }) =>
|
(w: { warningNumber: number; reason: string | null }) =>
|
||||||
`- ${w.id}: ${w.reason ?? t(locale, 'moderation.warning.defaultReason')}`
|
`- #${w.warningNumber}: ${w.reason ?? t(locale, 'moderation.warning.defaultReason')}`
|
||||||
)
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
await interaction.reply({ content, ephemeral: true });
|
await interaction.reply({ content, ephemeral: true });
|
||||||
@@ -358,9 +354,9 @@ const warnCommand: SlashCommand = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'remove') {
|
if (sub === 'remove') {
|
||||||
const warningId = interaction.options.getString('warning_id', true);
|
const warningNumber = interaction.options.getInteger('id', true);
|
||||||
const warning = await context.prisma.warning.findFirst({
|
const warning = await context.prisma.warning.findFirst({
|
||||||
where: { id: warningId, guildId: interaction.guildId! }
|
where: { warningNumber, guildId: interaction.guildId! }
|
||||||
});
|
});
|
||||||
if (!warning) {
|
if (!warning) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
@@ -376,7 +372,7 @@ const warnCommand: SlashCommand = {
|
|||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
...auditFrom(interaction),
|
...auditFrom(interaction),
|
||||||
action: 'WARN_REMOVE',
|
action: 'WARN_REMOVE',
|
||||||
reason: `Warning ${warningId} removed`
|
reason: `Warning #${warningNumber} removed`
|
||||||
});
|
});
|
||||||
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
await replySuccessCase(interaction, locale, modCase.caseNumber);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { PermissionFlagsBits, type ChatInputCommandInteraction } from 'discord.js';
|
import {
|
||||||
import type { Prisma } from '@prisma/client';
|
PermissionFlagsBits,
|
||||||
import { buildCaseMetadata, type CaseAuditSource, t, type Locale } from '@nexumi/shared';
|
type ChatInputCommandInteraction,
|
||||||
|
type User
|
||||||
|
} from 'discord.js';
|
||||||
|
import type { Prisma, Warning } from '@prisma/client';
|
||||||
|
import { buildCaseMetadata, type CaseAuditSource, t, tf, type Locale } from '@nexumi/shared';
|
||||||
|
import { logger } from '../../logger.js';
|
||||||
import { safeRemoveBullJob } from '../../lib/safe-remove-job.js';
|
import { safeRemoveBullJob } from '../../lib/safe-remove-job.js';
|
||||||
import { moderationQueue } from '../../queues.js';
|
import { moderationQueue } from '../../queues.js';
|
||||||
import type { BotContext } from '../../types.js';
|
import type { BotContext } from '../../types.js';
|
||||||
@@ -17,6 +22,121 @@ type CreateCaseInput = {
|
|||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CreateWarningInput = {
|
||||||
|
guildId: string;
|
||||||
|
userId: string;
|
||||||
|
moderatorId: string;
|
||||||
|
reason?: string;
|
||||||
|
caseId?: string;
|
||||||
|
guildName: string;
|
||||||
|
locale: Locale;
|
||||||
|
/** Discord user to DM; when omitted the user is fetched by id. */
|
||||||
|
notifyUser?: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreatedWarning = {
|
||||||
|
warning: Warning;
|
||||||
|
warningCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function notifyWarnedUser(params: {
|
||||||
|
user: User;
|
||||||
|
guildName: string;
|
||||||
|
reason: string;
|
||||||
|
warningCount: number;
|
||||||
|
warningNumber: number;
|
||||||
|
locale: Locale;
|
||||||
|
}): Promise<void> {
|
||||||
|
try {
|
||||||
|
await params.user.send(
|
||||||
|
tf(params.locale, 'moderation.warning.dm', {
|
||||||
|
guild: params.guildName,
|
||||||
|
reason: params.reason,
|
||||||
|
count: params.warningCount,
|
||||||
|
warningNumber: params.warningNumber
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
{ error, userId: params.user.id, warningNumber: params.warningNumber },
|
||||||
|
'Failed to DM warned user'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createWarning(
|
||||||
|
context: BotContext,
|
||||||
|
input: CreateWarningInput
|
||||||
|
): Promise<CreatedWarning> {
|
||||||
|
await context.prisma.user.upsert({
|
||||||
|
where: { id: input.userId },
|
||||||
|
update: {},
|
||||||
|
create: { id: input.userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
let warning: Warning | null = null;
|
||||||
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
|
try {
|
||||||
|
warning = await context.prisma.$transaction(
|
||||||
|
async (tx) => {
|
||||||
|
const last = await tx.warning.findFirst({
|
||||||
|
where: { guildId: input.guildId },
|
||||||
|
orderBy: { warningNumber: 'desc' },
|
||||||
|
select: { warningNumber: true }
|
||||||
|
});
|
||||||
|
return tx.warning.create({
|
||||||
|
data: {
|
||||||
|
guildId: input.guildId,
|
||||||
|
warningNumber: (last?.warningNumber ?? 0) + 1,
|
||||||
|
userId: input.userId,
|
||||||
|
moderatorId: input.moderatorId,
|
||||||
|
reason: input.reason,
|
||||||
|
caseId: input.caseId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ isolationLevel: 'Serializable' }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
const code =
|
||||||
|
typeof error === 'object' && error && 'code' in error
|
||||||
|
? String((error as { code: unknown }).code)
|
||||||
|
: '';
|
||||||
|
if (code !== 'P2002' && attempt === 4) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (attempt === 4) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!warning) {
|
||||||
|
throw new Error('Failed to create warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
const warningCount = await context.prisma.warning.count({
|
||||||
|
where: { guildId: input.guildId, userId: input.userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
const reason = input.reason ?? t(input.locale, 'moderation.warning.defaultReason');
|
||||||
|
const notifyUser =
|
||||||
|
input.notifyUser ?? (await context.client.users.fetch(input.userId).catch(() => null));
|
||||||
|
if (notifyUser) {
|
||||||
|
await notifyWarnedUser({
|
||||||
|
user: notifyUser,
|
||||||
|
guildName: input.guildName,
|
||||||
|
reason,
|
||||||
|
warningCount,
|
||||||
|
warningNumber: warning.warningNumber,
|
||||||
|
locale: input.locale
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { warning, warningCount };
|
||||||
|
}
|
||||||
|
|
||||||
export async function createCase(context: BotContext, input: CreateCaseInput) {
|
export async function createCase(context: BotContext, input: CreateCaseInput) {
|
||||||
await context.prisma.guild.upsert({
|
await context.prisma.guild.upsert({
|
||||||
where: { id: input.guildId },
|
where: { id: input.guildId },
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export function WarningsTable({ guildId, initialWarnings }: WarningsTableProps)
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border text-left text-xs uppercase text-muted-foreground">
|
<tr className="border-b border-border text-left text-xs uppercase text-muted-foreground">
|
||||||
|
<th className="py-2 pr-4">#</th>
|
||||||
<th className="py-2 pr-4">{t('modulePages.moderation.target')}</th>
|
<th className="py-2 pr-4">{t('modulePages.moderation.target')}</th>
|
||||||
<th className="py-2 pr-4">{t('modulePages.moderation.moderator')}</th>
|
<th className="py-2 pr-4">{t('modulePages.moderation.moderator')}</th>
|
||||||
<th className="py-2 pr-4">{t('modulePages.moderation.reason')}</th>
|
<th className="py-2 pr-4">{t('modulePages.moderation.reason')}</th>
|
||||||
@@ -115,6 +116,7 @@ export function WarningsTable({ guildId, initialWarnings }: WarningsTableProps)
|
|||||||
<tbody className="divide-y divide-border">
|
<tbody className="divide-y divide-border">
|
||||||
{warnings.map((entry) => (
|
{warnings.map((entry) => (
|
||||||
<tr key={entry.id}>
|
<tr key={entry.id}>
|
||||||
|
<td className="py-2 pr-4 font-mono text-xs">#{entry.warningNumber}</td>
|
||||||
<td className="py-2 pr-4 font-mono text-xs">{entry.userId}</td>
|
<td className="py-2 pr-4 font-mono text-xs">{entry.userId}</td>
|
||||||
<td className="py-2 pr-4 font-mono text-xs">{entry.moderatorId}</td>
|
<td className="py-2 pr-4 font-mono text-xs">{entry.moderatorId}</td>
|
||||||
<td className="max-w-64 truncate py-2 pr-4">
|
<td className="max-w-64 truncate py-2 pr-4">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export async function listWarnings(guildId: string, query: WarningListQuery): Pr
|
|||||||
|
|
||||||
return warnings.map((warning) => ({
|
return warnings.map((warning) => ({
|
||||||
id: warning.id,
|
id: warning.id,
|
||||||
|
warningNumber: warning.warningNumber,
|
||||||
userId: warning.userId,
|
userId: warning.userId,
|
||||||
moderatorId: warning.moderatorId,
|
moderatorId: warning.moderatorId,
|
||||||
reason: warning.reason,
|
reason: warning.reason,
|
||||||
|
|||||||
@@ -366,7 +366,7 @@
|
|||||||
"deleteCase": "Fall löschen",
|
"deleteCase": "Fall löschen",
|
||||||
"confirmDeleteCase": "Diesen Fall wirklich löschen?",
|
"confirmDeleteCase": "Diesen Fall wirklich löschen?",
|
||||||
"warningsTitle": "Verwarnungen",
|
"warningsTitle": "Verwarnungen",
|
||||||
"warningsDescription": "Letzte Verwarnungen. Entfernen löscht den Warn-Eintrag.",
|
"warningsDescription": "Letzte Verwarnungen mit kurzer Nummer (#). Entfernen löscht den Warn-Eintrag.",
|
||||||
"warningsEmpty": "Keine Verwarnungen gefunden.",
|
"warningsEmpty": "Keine Verwarnungen gefunden.",
|
||||||
"warningsSearchUser": "User-ID filtern",
|
"warningsSearchUser": "User-ID filtern",
|
||||||
"removeWarning": "Verwarnung entfernen",
|
"removeWarning": "Verwarnung entfernen",
|
||||||
|
|||||||
@@ -366,7 +366,7 @@
|
|||||||
"deleteCase": "Delete case",
|
"deleteCase": "Delete case",
|
||||||
"confirmDeleteCase": "Really delete this case?",
|
"confirmDeleteCase": "Really delete this case?",
|
||||||
"warningsTitle": "Warnings",
|
"warningsTitle": "Warnings",
|
||||||
"warningsDescription": "Recent warnings. Remove deletes the warning entry.",
|
"warningsDescription": "Recent warnings with short numbers (#). Remove deletes the warning entry.",
|
||||||
"warningsEmpty": "No warnings found.",
|
"warningsEmpty": "No warnings found.",
|
||||||
"warningsSearchUser": "Filter by user ID",
|
"warningsSearchUser": "Filter by user ID",
|
||||||
"removeWarning": "Remove warning",
|
"removeWarning": "Remove warning",
|
||||||
|
|||||||
@@ -617,3 +617,20 @@
|
|||||||
- [ ] Alten Cron-Eintrag (jobId `repeat:…`) im Dashboard löschen → kein API-Error -8
|
- [ ] Alten Cron-Eintrag (jobId `repeat:…`) im Dashboard löschen → kein API-Error -8
|
||||||
- [ ] Dark Mode: Cron-Feld und datetime-local lesbar/bedienbar
|
- [ ] Dark Mode: Cron-Feld und datetime-local lesbar/bedienbar
|
||||||
|
|
||||||
|
## Post-Phase – Warn-DM + kurze Warnungsnummern (Status: implementiert)
|
||||||
|
|
||||||
|
### Abgeschlossen (Code)
|
||||||
|
|
||||||
|
- Verwarnte Nutzer erhalten eine DM mit Servername, Grund, aktueller Warnungsanzahl und Nr.
|
||||||
|
- Pro Guild sequenzielle kurze `warningNumber` (wie Case-Nummern); `/warn list` und Dashboard zeigen `#N`
|
||||||
|
- `/warn remove` akzeptiert Integer-Option `id` statt langer CUID
|
||||||
|
- Gemeinsame `createWarning`-Hilfe (Slash + AutoMod), Migration `20260725180000_warning_number`
|
||||||
|
|
||||||
|
### Manuell testen
|
||||||
|
|
||||||
|
- [ ] `/warn add` → Moderator sieht `#N`; Ziel-User erhält DM (Server, Grund, Count)
|
||||||
|
- [ ] User mit geschlossenen DMs → Warn trotzdem gespeichert, Bot-Log warnt
|
||||||
|
- [ ] `/warn list` zeigt `#1`, `#2`, …; `/warn remove id:1` entfernt korrekt
|
||||||
|
- [ ] AutoMod-WARN → ebenfalls DM + Case verknüpft
|
||||||
|
- [ ] Dashboard: Warn-Tabelle zeigt `#`-Spalte
|
||||||
|
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ export const commandLocales = {
|
|||||||
en: 'Optional escalation reason'
|
en: 'Optional escalation reason'
|
||||||
},
|
},
|
||||||
'moderation.warn.options.warning_id': {
|
'moderation.warn.options.warning_id': {
|
||||||
de: 'Verwarnungs-ID',
|
de: 'Kurze Verwarnungsnummer (z. B. aus /warn list)',
|
||||||
en: 'Warning ID'
|
en: 'Short warning number (e.g. from /warn list)'
|
||||||
},
|
},
|
||||||
'moderation.choice.timeout': {
|
'moderation.choice.timeout': {
|
||||||
de: 'Timeout',
|
de: 'Timeout',
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export type CaseListQuery = z.infer<typeof CaseListQuerySchema>;
|
|||||||
|
|
||||||
export const WarningDashboardSchema = z.object({
|
export const WarningDashboardSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
warningNumber: z.number().int().positive(),
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
moderatorId: z.string(),
|
moderatorId: z.string(),
|
||||||
reason: z.string().nullable(),
|
reason: z.string().nullable(),
|
||||||
|
|||||||
@@ -133,10 +133,12 @@ const de: Dictionary = {
|
|||||||
'Der Bot kann dieses Mitglied wegen der Rollenhierarchie oder fehlender Rechte nicht moderieren.',
|
'Der Bot kann dieses Mitglied wegen der Rollenhierarchie oder fehlender Rechte nicht moderieren.',
|
||||||
'moderation.hierarchy.notMember': 'Dieses Mitglied ist nicht auf dem Server.',
|
'moderation.hierarchy.notMember': 'Dieses Mitglied ist nicht auf dem Server.',
|
||||||
'moderation.reason.none': 'Kein Grund angegeben',
|
'moderation.reason.none': 'Kein Grund angegeben',
|
||||||
'moderation.warning.created': 'Verwarnung erstellt: {warningId}',
|
'moderation.warning.created': 'Verwarnung #{warningNumber} erstellt.',
|
||||||
'moderation.warning.none': 'Keine Verwarnungen vorhanden.',
|
'moderation.warning.none': 'Keine Verwarnungen vorhanden.',
|
||||||
'moderation.warning.defaultReason': 'Kein Grund',
|
'moderation.warning.defaultReason': 'Kein Grund',
|
||||||
'moderation.warning.notFound': 'Verwarnung nicht gefunden.',
|
'moderation.warning.notFound': 'Verwarnung nicht gefunden.',
|
||||||
|
'moderation.warning.dm':
|
||||||
|
'Du hast eine Verwarnung auf **{guild}** erhalten.\nGrund: {reason}\nVerwarnungen: {count} (Nr. #{warningNumber})',
|
||||||
'moderation.purge.done': '{deletedCount} Nachrichten gelöscht (Fall #{caseNumber}).',
|
'moderation.purge.done': '{deletedCount} Nachrichten gelöscht (Fall #{caseNumber}).',
|
||||||
'moderation.note.none': 'Keine Notizen vorhanden.',
|
'moderation.note.none': 'Keine Notizen vorhanden.',
|
||||||
'moderation.case.notFound': 'Fall nicht gefunden.',
|
'moderation.case.notFound': 'Fall nicht gefunden.',
|
||||||
@@ -750,10 +752,12 @@ const en: Dictionary = {
|
|||||||
'The bot cannot moderate this member due to role hierarchy or missing permissions.',
|
'The bot cannot moderate this member due to role hierarchy or missing permissions.',
|
||||||
'moderation.hierarchy.notMember': 'That member is not on this server.',
|
'moderation.hierarchy.notMember': 'That member is not on this server.',
|
||||||
'moderation.reason.none': 'No reason provided',
|
'moderation.reason.none': 'No reason provided',
|
||||||
'moderation.warning.created': 'Warning created: {warningId}',
|
'moderation.warning.created': 'Warning #{warningNumber} created.',
|
||||||
'moderation.warning.none': 'No warnings.',
|
'moderation.warning.none': 'No warnings.',
|
||||||
'moderation.warning.defaultReason': 'No reason',
|
'moderation.warning.defaultReason': 'No reason',
|
||||||
'moderation.warning.notFound': 'Warning not found.',
|
'moderation.warning.notFound': 'Warning not found.',
|
||||||
|
'moderation.warning.dm':
|
||||||
|
'You received a warning on **{guild}**.\nReason: {reason}\nWarnings: {count} (No. #{warningNumber})',
|
||||||
'moderation.purge.done': 'Deleted {deletedCount} messages (case #{caseNumber}).',
|
'moderation.purge.done': 'Deleted {deletedCount} messages (case #{caseNumber}).',
|
||||||
'moderation.note.none': 'No notes.',
|
'moderation.note.none': 'No notes.',
|
||||||
'moderation.case.notFound': 'Case not found.',
|
'moderation.case.notFound': 'Case not found.',
|
||||||
|
|||||||
Reference in New Issue
Block a user