export type DiscordGame = 't5' | 't6'; export interface DiscordSettings { enabled: boolean; guildId: string; channels: Record; excludedServers: string[]; showPlayers: boolean; showAddresses: boolean; leaderboardEnabled: boolean; } export const defaultDiscordSettings: DiscordSettings = { enabled: false, guildId: '', channels: { t5: '', t6: '' }, excludedServers: [], showPlayers: false, showAddresses: false, leaderboardEnabled: false }; export const discordSnowflake = (value: unknown): value is string => typeof value === 'string' && /^[1-9][0-9]{16,19}$/.test(value); export function validateDiscordSettings(value: unknown): DiscordSettings { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Provide Discord settings.'); const input = value as Record; for (const key of ['enabled', 'showPlayers', 'showAddresses', 'leaderboardEnabled']) { if (typeof input[key] !== 'boolean') throw new Error(`Invalid ${key} setting.`); } if (typeof input.guildId !== 'string' || (input.guildId && !discordSnowflake(input.guildId))) throw new Error('Provide a valid Discord server ID.'); if (!input.channels || typeof input.channels !== 'object' || Array.isArray(input.channels)) throw new Error('Provide Discord channels for each game.'); const channels = input.channels as Record; for (const game of ['t5', 't6'] as const) { if ( typeof channels[game] !== 'string' || (channels[game] && !discordSnowflake(channels[game])) ) throw new Error('Channel IDs must be valid Discord IDs or empty.'); } if ( !Array.isArray(input.excludedServers) || input.excludedServers.length > 100 || input.excludedServers.some( (name) => typeof name !== 'string' || !/^[A-Za-z0-9_.-]{1,128}$/.test(name) ) ) throw new Error('Choose up to 100 servers to exclude from Discord.'); if (input.enabled && (!input.guildId || (!channels.t5 && !channels.t6))) throw new Error('Choose a Discord server and at least one board channel before enabling.'); return { enabled: input.enabled as boolean, guildId: input.guildId, channels: { t5: channels.t5 as string, t6: channels.t6 as string }, excludedServers: [...new Set(input.excludedServers as string[])].sort(), showPlayers: input.showPlayers as boolean, showAddresses: input.showAddresses as boolean, leaderboardEnabled: input.leaderboardEnabled as boolean }; } export interface DiscordPublicServer { name: string; game: DiscordGame; label: string; map: string; mapKey: string; state: 'empty' | 'in-progress' | 'full' | 'starting' | 'offline' | 'stale' | 'unknown'; players: number | null; capacity: number | null; round: number | null; observedAt: string | null; playerNames: string[]; connection: string | null; } export interface DiscordMessage { content?: string; flags?: number; components?: Record[]; allowed_mentions: { parse: string[] }; } export interface DiscordDeliveryStatus { lastAttemptAt: string | null; lastSuccessAt: string | null; lastError: string | null; boards: { game: DiscordGame; channelId: string; messageCount: number }[]; activeSubscriptions: number; pendingNotifications: number; } export interface DiscordAdminState { settings: DiscordSettings; credentials: { applicationId: string; publicKeyConfigured: boolean; botTokenConfigured: boolean; }; status: DiscordDeliveryStatus; servers: { name: string; game: DiscordGame; label: string; map: string }[]; preview: Record; }