import { z } from 'zod'; import { SelfRoleBehaviorSchema, SelfRoleEntrySchema, SelfRoleModeSchema, SuggestionStatusSchema, TagResponseTypeSchema, TicketModeSchema } from './phase4.js'; import { SocialFeedTypeSchema } from './phase5.js'; /** * Discord snowflake ID: 17-20 digit numeric string. */ export const SnowflakeSchema = z.string().regex(/^\d{17,20}$/, 'Invalid Discord ID'); /** * Optional snowflake field for forms: accepts an empty string (treated as * "unset"/cleared) or a valid snowflake. Kept as a plain string (rather than * transformed to `undefined`) so that `JSON.stringify` does not silently * drop the field when a user clears it in the dashboard - the API layer * maps `''` to `null` when persisting. */ export const OptionalSnowflakeSchema = z.union([z.literal(''), SnowflakeSchema]); function nonEmptyPatch(schema: z.ZodObject) { return schema.partial().refine((value) => Object.keys(value).length > 0, { message: 'At least one field is required' }); } // --------------------------------------------------------------------------- // Moderation // --------------------------------------------------------------------------- export const EscalationActionSchema = z.enum(['TIMEOUT', 'KICK', 'BAN']); export type EscalationAction = z.infer; export const EscalationRuleDashboardSchema = z.object({ id: z.string().optional(), warnCount: z.number().int().positive(), action: EscalationActionSchema, durationMs: z.number().int().positive().nullable().optional(), reason: z.string().max(500).nullable().optional(), enabled: z.boolean().default(true) }); export type EscalationRuleDashboard = z.infer; export const ModerationDashboardSchema = z.object({ moderationEnabled: z.boolean(), escalations: z.array(EscalationRuleDashboardSchema) }); export type ModerationDashboard = z.infer; export const ModerationDashboardPatchSchema = z .object({ moderationEnabled: z.boolean().optional(), escalations: z.array(EscalationRuleDashboardSchema).optional() }) .refine((value) => Object.keys(value).length > 0, { message: 'At least one field is required' }); export type ModerationDashboardPatch = z.infer; export const CaseDashboardSchema = z.object({ id: z.string(), caseNumber: z.number().int(), targetUserId: z.string(), moderatorId: z.string(), action: z.string(), reason: z.string().nullable(), createdAt: z.string(), deletedAt: z.string().nullable() }); export type CaseDashboard = z.infer; export const CaseListQuerySchema = z.object({ search: z.string().trim().max(100).optional(), action: z.string().trim().max(50).optional(), limit: z.coerce.number().int().positive().max(100).default(25) }); export type CaseListQuery = z.infer; export const WarningDashboardSchema = z.object({ id: z.string(), userId: z.string(), moderatorId: z.string(), reason: z.string().nullable(), caseId: z.string().nullable(), createdAt: z.string() }); export type WarningDashboard = z.infer; export const WarningListQuerySchema = z.object({ userId: SnowflakeSchema.optional() }); export type WarningListQuery = z.infer; // --------------------------------------------------------------------------- // AutoMod // --------------------------------------------------------------------------- export const AntiRaidActionSchema = z.enum(['LOCKDOWN']); export const AutomodConfigDashboardSchema = z.object({ enabled: z.boolean(), antiRaidEnabled: z.boolean(), antiRaidJoinThreshold: z.number().int().positive(), antiRaidWindowSeconds: z.number().int().positive(), antiRaidAction: AntiRaidActionSchema, antiNukeEnabled: z.boolean(), antiNukeBanThreshold: z.number().int().positive(), antiNukeChannelThreshold: z.number().int().positive(), antiNukeWindowSeconds: z.number().int().positive(), lockdownActive: z.boolean() }); export type AutomodConfigDashboard = z.infer; export const AutomodConfigDashboardPatchSchema = nonEmptyPatch(AutomodConfigDashboardSchema); export type AutomodConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Logging // --------------------------------------------------------------------------- export const LogEventTypeDashboardSchema = z.enum([ 'MESSAGE_EDIT', 'MESSAGE_DELETE', 'MESSAGE_BULK_DELETE', 'MEMBER_JOIN', 'MEMBER_LEAVE', 'MEMBER_BAN', 'MEMBER_UNBAN', 'MEMBER_UPDATE', 'CHANNEL_CREATE', 'CHANNEL_DELETE', 'CHANNEL_UPDATE', 'VOICE_JOIN', 'VOICE_LEAVE', 'VOICE_MOVE', 'INVITE_CREATE', 'EMOJI_CREATE', 'EMOJI_UPDATE', 'EMOJI_DELETE', 'STICKER_CREATE', 'STICKER_UPDATE', 'STICKER_DELETE', 'THREAD_CREATE', 'THREAD_UPDATE', 'THREAD_DELETE', 'GUILD_UPDATE', 'MOD_ACTION' ]); export type LogEventTypeDashboard = z.infer; export const LogChannelMappingSchema = z.object({ eventType: LogEventTypeDashboardSchema, channelId: SnowflakeSchema }); export type LogChannelMapping = z.infer; export const LoggingConfigDashboardSchema = z.object({ enabled: z.boolean(), ignoreBots: z.boolean(), ignoredChannelIds: z.array(z.string()), ignoredRoleIds: z.array(z.string()), retentionDays: z.number().int().min(0).max(3650), logChannels: z.array(LogChannelMappingSchema) }); export type LoggingConfigDashboard = z.infer; export const LoggingConfigDashboardPatchSchema = z .object({ enabled: z.boolean().optional(), ignoreBots: z.boolean().optional(), ignoredChannelIds: z.array(z.string()).optional(), ignoredRoleIds: z.array(z.string()).optional(), retentionDays: z.number().int().min(0).max(3650).optional(), logChannels: z.array(LogChannelMappingSchema).optional() }) .refine((value) => Object.keys(value).length > 0, { message: 'At least one field is required' }); export type LoggingConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Welcome & Leave // --------------------------------------------------------------------------- export const WelcomeMessageTypeDashboardSchema = z.enum(['TEXT', 'EMBED', 'IMAGE']); export const WelcomeConfigDashboardSchema = z.object({ welcomeEnabled: z.boolean(), leaveEnabled: z.boolean(), welcomeChannelId: OptionalSnowflakeSchema, leaveChannelId: OptionalSnowflakeSchema, welcomeType: WelcomeMessageTypeDashboardSchema, welcomeContent: z.string().max(2000).nullable().optional(), welcomeEmbed: z.record(z.string(), z.unknown()).nullable().optional(), leaveContent: z.string().max(2000).nullable().optional(), leaveEmbed: z.record(z.string(), z.unknown()).nullable().optional(), welcomeDmEnabled: z.boolean(), welcomeDmContent: z.string().max(2000).nullable().optional(), userAutoroleIds: z.array(z.string()), botAutoroleIds: z.array(z.string()), imageTitle: z.string().max(200).nullable().optional(), imageSubtitle: z.string().max(200).nullable().optional() }); export type WelcomeConfigDashboard = z.infer; export const WelcomeConfigDashboardPatchSchema = nonEmptyPatch(WelcomeConfigDashboardSchema); export type WelcomeConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Verification // --------------------------------------------------------------------------- export const VerificationModeDashboardSchema = z.enum(['BUTTON', 'CAPTCHA']); export const VerificationFailActionDashboardSchema = z.enum(['KICK', 'BAN', 'NONE']); export const VerificationConfigDashboardSchema = z.object({ enabled: z.boolean(), channelId: OptionalSnowflakeSchema, verifiedRoleId: OptionalSnowflakeSchema, unverifiedRoleId: OptionalSnowflakeSchema, mode: VerificationModeDashboardSchema, minAccountAgeDays: z.number().int().nonnegative(), failAction: VerificationFailActionDashboardSchema }); export type VerificationConfigDashboard = z.infer; export const VerificationConfigDashboardPatchSchema = nonEmptyPatch(VerificationConfigDashboardSchema); export type VerificationConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Leveling // --------------------------------------------------------------------------- export const LevelUpModeDashboardSchema = z.enum(['CHANNEL', 'DM', 'OFF']); export const LevelingConfigDashboardSchema = z.object({ enabled: z.boolean(), textXpMin: z.number().int().positive(), textXpMax: z.number().int().positive(), textCooldownSeconds: z.number().int().positive(), voiceXpPerMinute: z.number().int().nonnegative(), noXpChannelIds: z.array(z.string()), noXpRoleIds: z.array(z.string()), roleMultipliers: z.record(z.string(), z.number().positive()), channelMultipliers: z.record(z.string(), z.number().positive()), levelUpMode: LevelUpModeDashboardSchema, levelUpChannelId: OptionalSnowflakeSchema, levelUpMessage: z.string().max(500).nullable().optional(), stackRewards: z.boolean(), cardAccentColor: z.string().regex(/^#[0-9a-fA-F]{6}$/), cardBackgroundColor: z.string().regex(/^#[0-9a-fA-F]{6}$/) }); export type LevelingConfigDashboard = z.infer; export const LevelingConfigDashboardPatchSchema = nonEmptyPatch(LevelingConfigDashboardSchema); export type LevelingConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Economy // --------------------------------------------------------------------------- export const EconomyConfigDashboardSchema = z.object({ enabled: z.boolean(), currencyName: z.string().min(1).max(32), currencySymbol: z.string().min(1).max(8), dailyAmount: z.number().int().nonnegative(), weeklyAmount: z.number().int().nonnegative(), workMin: z.number().int().nonnegative(), workMax: z.number().int().nonnegative(), workCooldownSeconds: z.number().int().positive(), startingBalance: z.number().int().nonnegative() }); export type EconomyConfigDashboard = z.infer; export const EconomyConfigDashboardPatchSchema = nonEmptyPatch(EconomyConfigDashboardSchema); export type EconomyConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Fun // --------------------------------------------------------------------------- export const FunConfigDashboardSchema = z.object({ enabled: z.boolean(), mediaEnabled: z.boolean() }); export type FunConfigDashboard = z.infer; export const FunConfigDashboardPatchSchema = nonEmptyPatch(FunConfigDashboardSchema); export type FunConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Tickets // --------------------------------------------------------------------------- export const TicketConfigDashboardSchema = z.object({ enabled: z.boolean(), mode: TicketModeSchema, categoryId: OptionalSnowflakeSchema, logChannelId: OptionalSnowflakeSchema, transcriptDm: z.boolean(), inactivityHours: z.number().int().positive(), ratingEnabled: z.boolean() }); export type TicketConfigDashboard = z.infer; export const TicketConfigDashboardPatchSchema = nonEmptyPatch(TicketConfigDashboardSchema); export type TicketConfigDashboardPatch = z.infer; export const TicketCategoryDashboardSchema = z.object({ id: z.string().optional(), name: z.string().min(1).max(100), description: z.string().max(500).nullable().optional(), emoji: z.string().max(32).nullable().optional(), supportRoleIds: z.array(z.string()) }); export type TicketCategoryDashboard = z.infer; export const TicketCategoryDashboardCreateSchema = TicketCategoryDashboardSchema.omit({ id: true }); export type TicketCategoryDashboardCreate = z.infer; export const TicketCategoryDashboardUpdateSchema = nonEmptyPatch(TicketCategoryDashboardCreateSchema); export type TicketCategoryDashboardUpdate = z.infer; // --------------------------------------------------------------------------- // Giveaways // --------------------------------------------------------------------------- export const GiveawayDashboardSchema = z.object({ id: z.string(), guildId: z.string(), channelId: z.string(), messageId: z.string().nullable(), prize: z.string(), winnerCount: z.number().int(), endsAt: z.string(), requiredRoleId: z.string().nullable(), requiredLevel: z.number().int().nullable(), requiredMemberDays: z.number().int().nullable(), paused: z.boolean(), endedAt: z.string().nullable(), winners: z.array(z.string()), entrants: z.array(z.string()), hostId: z.string(), createdAt: z.string() }); export type GiveawayDashboard = z.infer; export const GiveawayCreateDashboardSchema = z.object({ channelId: SnowflakeSchema, prize: z.string().min(1).max(200), winnerCount: z.number().int().positive().max(50), endsAt: z.string().refine((value) => !Number.isNaN(Date.parse(value)), 'Invalid date'), requiredRoleId: OptionalSnowflakeSchema.optional(), requiredLevel: z.number().int().nonnegative().nullable().optional(), requiredMemberDays: z.number().int().nonnegative().nullable().optional() }); export type GiveawayCreateDashboard = z.infer; export const GiveawayActionSchema = z.enum(['end', 'pause', 'unpause']); export type GiveawayAction = z.infer; // --------------------------------------------------------------------------- // Self Roles // --------------------------------------------------------------------------- export const SelfRolePanelDashboardSchema = z.object({ id: z.string().optional(), channelId: SnowflakeSchema, title: z.string().min(1).max(200), description: z.string().max(1000).nullable().optional(), mode: SelfRoleModeSchema, behavior: SelfRoleBehaviorSchema, roles: z.array(SelfRoleEntrySchema).min(1), expiresAt: z.string().nullable().optional() }); export type SelfRolePanelDashboard = z.infer; export const SelfRolePanelDashboardCreateSchema = SelfRolePanelDashboardSchema.omit({ id: true }); export type SelfRolePanelDashboardCreate = z.infer; export const SelfRolePanelDashboardUpdateSchema = nonEmptyPatch(SelfRolePanelDashboardCreateSchema); export type SelfRolePanelDashboardUpdate = z.infer; // --------------------------------------------------------------------------- // Tags // --------------------------------------------------------------------------- export const TagDashboardSchema = z.object({ id: z.string().optional(), name: z.string().min(1).max(50), content: z.string().max(2000).nullable().optional(), responseType: TagResponseTypeSchema, triggerWord: z.string().max(100).nullable().optional(), allowedRoleIds: z.array(z.string()), allowedChannelIds: z.array(z.string()) }); export type TagDashboard = z.infer; export const TagDashboardCreateSchema = TagDashboardSchema.omit({ id: true }); export type TagDashboardCreate = z.infer; export const TagDashboardUpdateSchema = nonEmptyPatch(TagDashboardCreateSchema); export type TagDashboardUpdate = z.infer; // --------------------------------------------------------------------------- // Starboard // --------------------------------------------------------------------------- export const StarboardConfigDashboardSchema = z.object({ enabled: z.boolean(), channelId: OptionalSnowflakeSchema, emoji: z.string().min(1).max(32), threshold: z.number().int().positive(), allowSelfStar: z.boolean(), ignoredChannelIds: z.array(z.string()) }); export type StarboardConfigDashboard = z.infer; export const StarboardConfigDashboardPatchSchema = nonEmptyPatch(StarboardConfigDashboardSchema); export type StarboardConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Suggestions // --------------------------------------------------------------------------- export const SuggestionConfigDashboardSchema = z.object({ enabled: z.boolean(), openChannelId: OptionalSnowflakeSchema, approvedChannelId: OptionalSnowflakeSchema, deniedChannelId: OptionalSnowflakeSchema, createThread: z.boolean() }); export type SuggestionConfigDashboard = z.infer; export const SuggestionConfigDashboardPatchSchema = nonEmptyPatch(SuggestionConfigDashboardSchema); export type SuggestionConfigDashboardPatch = z.infer; export const SuggestionDashboardSchema = z.object({ id: z.string(), authorId: z.string(), content: z.string(), status: SuggestionStatusSchema, upvotes: z.number().int(), downvotes: z.number().int(), staffReason: z.string().nullable(), createdAt: z.string() }); export type SuggestionDashboard = z.infer; export const SuggestionListQuerySchema = z.object({ status: SuggestionStatusSchema.optional(), limit: z.coerce.number().int().positive().max(100).default(25) }); export type SuggestionListQuery = z.infer; export const SuggestionActionSchema = z.object({ status: SuggestionStatusSchema, staffReason: z.string().max(500).nullable().optional() }); export type SuggestionAction = z.infer; // --------------------------------------------------------------------------- // Birthdays // --------------------------------------------------------------------------- export const BirthdayConfigDashboardSchema = z.object({ enabled: z.boolean(), channelId: OptionalSnowflakeSchema, roleId: OptionalSnowflakeSchema, message: z.string().max(500).nullable().optional(), timezone: z.string().min(1).max(64) }); export type BirthdayConfigDashboard = z.infer; export const BirthdayConfigDashboardPatchSchema = nonEmptyPatch(BirthdayConfigDashboardSchema); export type BirthdayConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Temp-Voice // --------------------------------------------------------------------------- export const TempVoiceConfigDashboardSchema = z.object({ enabled: z.boolean(), hubChannelId: OptionalSnowflakeSchema, categoryId: OptionalSnowflakeSchema, defaultName: z.string().min(1).max(100), defaultLimit: z.number().int().nonnegative().max(99) }); export type TempVoiceConfigDashboard = z.infer; export const TempVoiceConfigDashboardPatchSchema = nonEmptyPatch(TempVoiceConfigDashboardSchema); export type TempVoiceConfigDashboardPatch = z.infer; // --------------------------------------------------------------------------- // Server Stats // --------------------------------------------------------------------------- export const StatsConfigDashboardSchema = z.object({ enabled: z.boolean(), membersChannelId: OptionalSnowflakeSchema, onlineChannelId: OptionalSnowflakeSchema, boostsChannelId: OptionalSnowflakeSchema, membersTemplate: z.string().min(1).max(100), onlineTemplate: z.string().min(1).max(100), boostsTemplate: z.string().min(1).max(100) }); export type StatsConfigDashboard = z.infer; export const StatsConfigDashboardPatchSchema = nonEmptyPatch(StatsConfigDashboardSchema); export type StatsConfigDashboardPatch = z.infer; export const ActivityStatSummarySchema = z.object({ messageCount: z.number().int().nonnegative(), voiceMinutes: z.number().int().nonnegative(), activeUserCount: z.number().int().nonnegative() }); export type ActivityStatSummary = z.infer; // --------------------------------------------------------------------------- // Social Feeds // --------------------------------------------------------------------------- export const SocialFeedDashboardSchema = z.object({ id: z.string().optional(), type: SocialFeedTypeSchema, sourceId: z.string().min(1).max(200), channelId: SnowflakeSchema, rolePingId: OptionalSnowflakeSchema.optional(), template: z.string().max(500).nullable().optional(), enabled: z.boolean() }); export type SocialFeedDashboard = z.infer; export const SocialFeedDashboardCreateSchema = SocialFeedDashboardSchema.omit({ id: true }); export type SocialFeedDashboardCreate = z.infer; export const SocialFeedDashboardUpdateSchema = nonEmptyPatch(SocialFeedDashboardCreateSchema); export type SocialFeedDashboardUpdate = z.infer; // --------------------------------------------------------------------------- // Scheduler // --------------------------------------------------------------------------- export const ScheduledMessageDashboardSchema = z.object({ id: z.string(), channelId: z.string(), creatorId: z.string(), content: z.string().nullable(), rolePingId: z.string().nullable(), cron: z.string().nullable(), runAt: z.string().nullable(), enabled: z.boolean(), createdAt: z.string() }); export type ScheduledMessageDashboard = z.infer; export const ScheduledMessageDashboardCreateSchema = z .object({ channelId: SnowflakeSchema, content: z.string().max(2000).nullable().optional(), rolePingId: OptionalSnowflakeSchema.optional(), cron: z.string().min(1).max(100).nullable().optional(), runAt: z .string() .refine((value) => !Number.isNaN(Date.parse(value)), 'Invalid date') .nullable() .optional() }) .refine((value) => Boolean(value.cron?.trim()) || Boolean(value.runAt), { message: 'Either cron or runAt is required' }) .refine((value) => Boolean(value.content?.trim()), { message: 'Content is required' }); export type ScheduledMessageDashboardCreate = z.infer; // --------------------------------------------------------------------------- // Guild Backups // --------------------------------------------------------------------------- export const GuildBackupDashboardSchema = z.object({ id: z.string(), name: z.string(), createdById: z.string(), createdAt: z.string(), roleCount: z.number().int().nonnegative(), channelCount: z.number().int().nonnegative() }); export type GuildBackupDashboard = z.infer; export const GuildBackupCreateDashboardSchema = z.object({ name: z.string().min(1).max(100) }); export type GuildBackupCreateDashboard = z.infer; // --------------------------------------------------------------------------- // Command Overrides // --------------------------------------------------------------------------- export const CommandOverrideDashboardSchema = z.object({ commandName: z.string().min(1).max(100), enabled: z.boolean(), allowedRoleIds: z.array(SnowflakeSchema), deniedRoleIds: z.array(SnowflakeSchema), allowedChannelIds: z.array(SnowflakeSchema), deniedChannelIds: z.array(SnowflakeSchema), cooldownSeconds: z.number().int().nonnegative().nullable() }); export type CommandOverrideDashboard = z.infer; export const CommandOverrideDashboardUpsertSchema = z.object({ commandName: z.string().min(1).max(100), enabled: z.boolean().default(true), allowedRoleIds: z.array(SnowflakeSchema).default([]), deniedRoleIds: z.array(SnowflakeSchema).default([]), allowedChannelIds: z.array(SnowflakeSchema).default([]), deniedChannelIds: z.array(SnowflakeSchema).default([]), cooldownSeconds: z.number().int().nonnegative().nullable().optional() }); export type CommandOverrideDashboardUpsert = z.infer; export const CommandGlobalRulesSchema = z.object({ allowedChannelIds: z.array(SnowflakeSchema), deniedChannelIds: z.array(SnowflakeSchema) }); export type CommandGlobalRules = z.infer; export const CommandGlobalRulesPatchSchema = CommandGlobalRulesSchema.partial().refine( (value) => Object.keys(value).length > 0, { message: 'At least one field is required' } ); export type CommandGlobalRulesPatch = z.infer; export const DiscordChannelOptionSchema = z.object({ id: SnowflakeSchema, name: z.string(), type: z.number().int(), parentId: SnowflakeSchema.nullable().optional() }); export type DiscordChannelOption = z.infer; export const DiscordRoleOptionSchema = z.object({ id: SnowflakeSchema, name: z.string(), color: z.number().int(), position: z.number().int() }); export type DiscordRoleOption = z.infer; /** * Known slash command names, aggregated from every module's * `command-definitions.ts` (`SlashCommandBuilder#setName`). Used by the * dashboard's command-management page to list overridable commands without * requiring a live bot connection. */ export const KNOWN_COMMAND_NAMES = [ 'ban', 'unban', 'kick', 'timeout', 'untimeout', 'warn', 'purge', 'slowmode', 'lock', 'unlock', 'nick', 'case', 'modnote', 'automod', 'welcome', 'verify', 'rank', 'leaderboard', 'xp', 'balance', 'daily', 'weekly', 'work', 'pay', 'gamble', 'slots', 'blackjack', 'coinflip', 'shop', 'inventory', 'eco', 'userinfo', 'serverinfo', 'roleinfo', 'channelinfo', 'avatar', 'banner', 'poll', 'remindme', 'reminders', 'afk', 'emoji', 'sticker', 'timestamp', 'snipe', 'editsnipe', 'embed', '8ball', 'dice', 'flip', 'rps', 'choose', 'trivia', 'tictactoe', 'connect4', 'hangman', 'meme', 'cat', 'dog', 'giveaway', 'ticket', 'selfroles', 'tag', 'starboard', 'suggest', 'suggestion', 'birthday', 'voice', 'stats', 'invites', 'feeds', 'schedule', 'announce', 'backup', 'help', 'info', 'invite', 'support', 'privacy', 'gdpr' ] as const; export type KnownCommandName = (typeof KNOWN_COMMAND_NAMES)[number];