import { ActivityType, Status } from 'discord.js'; import { BOT_STATUS_REDIS_KEY, PRESENCE_REDIS_KEY, BotPresenceConfigSchema, BotStatusPayloadSchema, type BotPresenceConfig } from '@nexumi/shared'; import { redis } from './redis.js'; import { logger } from './logger.js'; import type { BotContext } from './types.js'; const BOT_STATUS_SHARDS_HASH = 'bot:status:shards'; const BOT_STATUS_TTL_SECONDS = 120; function shardStatusLabel(wsStatus: number): string { switch (wsStatus) { case Status.Ready: return 'ready'; case Status.Connecting: return 'connecting'; case Status.Reconnecting: return 'reconnecting'; case Status.Idle: return 'idle'; case Status.Nearly: return 'nearly'; case Status.Disconnected: return 'disconnected'; case Status.WaitingForGuilds: return 'waiting_for_guilds'; case Status.Identifying: return 'identifying'; case Status.Resuming: return 'resuming'; default: return 'unknown'; } } export async function publishBotStatus(context: BotContext): Promise { const shardId = context.client.shard?.ids[0] ?? 0; const shardEntry = { id: shardId, status: shardStatusLabel(context.client.ws.status), guildCount: context.client.guilds.cache.size, ping: context.client.ws.ping }; await redis.hset(BOT_STATUS_SHARDS_HASH, String(shardId), JSON.stringify(shardEntry)); await redis.expire(BOT_STATUS_SHARDS_HASH, BOT_STATUS_TTL_SECONDS); const all = await redis.hgetall(BOT_STATUS_SHARDS_HASH); const shards = Object.values(all) .map((raw) => { try { return JSON.parse(raw) as { id: number; status: string; guildCount: number; ping: number; }; } catch { return null; } }) .filter((entry): entry is NonNullable => entry !== null) .sort((a, b) => a.id - b.id); if (shards.length === 0) { return; } const payload = BotStatusPayloadSchema.parse({ shards, apiLatencyMs: Math.round(shards.reduce((sum, shard) => sum + shard.ping, 0) / shards.length), guildCount: shards.reduce((sum, shard) => sum + shard.guildCount, 0), uptimeSeconds: Math.floor(process.uptime()), updatedAt: new Date().toISOString() }); await redis.set(BOT_STATUS_REDIS_KEY, JSON.stringify(payload), 'EX', BOT_STATUS_TTL_SECONDS); } const ACTIVITY_TYPE_MAP: Record = { Playing: ActivityType.Playing, Listening: ActivityType.Listening, Watching: ActivityType.Watching, Competing: ActivityType.Competing }; export async function readPresenceConfig(context: BotContext): Promise { const cached = await redis.get(PRESENCE_REDIS_KEY); if (cached) { const parsed = BotPresenceConfigSchema.safeParse(JSON.parse(cached)); if (parsed.success) { return parsed.data; } } const row = await context.prisma.botPresenceConfig.upsert({ where: { id: 'singleton' }, create: { id: 'singleton' }, update: {} }); const rotating = Array.isArray(row.rotatingMessages) ? row.rotatingMessages.filter((item: unknown): item is string => typeof item === 'string') : []; const config = BotPresenceConfigSchema.parse({ status: row.status, activityType: row.activityType, activityText: row.activityText, rotatingMessages: rotating, maintenanceMode: row.maintenanceMode, maintenanceMessage: row.maintenanceMessage }); await redis.set(PRESENCE_REDIS_KEY, JSON.stringify(config)); return config; } export async function applyBotPresence(context: BotContext): Promise { const config = await readPresenceConfig(context); const messages = config.rotatingMessages.length > 0 ? config.rotatingMessages : [config.activityText]; const index = Math.floor(Date.now() / 60_000) % messages.length; const text = messages[index] ?? config.activityText; await context.client.user?.setPresence({ status: config.status, activities: [ { name: text, type: ACTIVITY_TYPE_MAP[config.activityType] } ] }); } export async function isMaintenanceMode(context: BotContext): Promise<{ enabled: boolean; message: string | null; }> { const config = await readPresenceConfig(context); return { enabled: config.maintenanceMode, message: config.maintenanceMessage }; } export async function runPresenceRefreshJob(context: BotContext): Promise { try { await applyBotPresence(context); } catch (error) { logger.warn({ error }, 'Failed to apply bot presence'); } try { await publishBotStatus(context); } catch (error) { logger.warn({ error }, 'Failed to publish bot status'); } }