108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
export type DiscordGame = 't5' | 't6';
|
|
|
|
export interface DiscordSettings {
|
|
enabled: boolean;
|
|
guildId: string;
|
|
channels: Record<DiscordGame, string>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>[];
|
|
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<DiscordGame, DiscordPublicServer[]>;
|
|
}
|