import { TextChannel, Message, ContainerBuilder, TextDisplayBuilder, SectionBuilder, MessageFlags } from "discord.js"; import { AuroraClient } from "@/lib/BotClient"; import { DrizzleClient } from "@/lib/DrizzleClient"; import { users, transactions, lootdrops } from "@/db/schema"; import { desc } from "drizzle-orm"; import { readFileSync, writeFileSync, existsSync } from "fs"; import { join } from "path"; // Simple persistence for the terminal message ID const TERMINAL_DATA_PATH = join(process.cwd(), 'terminal-data.json'); interface TerminalData { channelId: string; messageId: string; } export const terminalService = { data: null as TerminalData | null, loadData: () => { if (existsSync(TERMINAL_DATA_PATH)) { try { terminalService.data = JSON.parse(readFileSync(TERMINAL_DATA_PATH, 'utf-8')); } catch (e) { console.error("Failed to load terminal data", e); } } }, saveData: (data: TerminalData) => { terminalService.data = data; writeFileSync(TERMINAL_DATA_PATH, JSON.stringify(data, null, 2)); }, init: async (channel: TextChannel) => { // limit to one terminal for now if (terminalService.data) { try { const oldChannel = await AuroraClient.channels.fetch(terminalService.data.channelId) as TextChannel; if (oldChannel) { const oldMsg = await oldChannel.messages.fetch(terminalService.data.messageId); if (oldMsg) await oldMsg.delete(); } } catch (e) { // ignore if old message doesn't exist } } const msg = await channel.send({ content: "🔄 Initializing Aurora Observatory..." }); terminalService.saveData({ channelId: channel.id, messageId: msg.id }); await terminalService.update(); }, update: async () => { if (!terminalService.data) { terminalService.loadData(); } if (!terminalService.data) return; try { const channel = await AuroraClient.channels.fetch(terminalService.data.channelId) as TextChannel; if (!channel) return; const message = await channel.messages.fetch(terminalService.data.messageId); if (!message) return; const containers = await terminalService.buildMessage(); // Components V2 requires the IsComponentsV2 flag and no content/embeds // Disable allowedMentions to prevent pings from the dashboard await message.edit({ content: null, embeds: null as any, components: containers as any, flags: MessageFlags.IsComponentsV2, allowedMentions: { parse: [] } }); } catch (error) { console.error("Failed to update terminal:", error); } }, buildMessage: async () => { // 1. Data Fetching const allUsers = await DrizzleClient.select().from(users); const totalUsers = allUsers.length; const totalWealth = allUsers.reduce((acc, u) => acc + (u.balance || 0n), 0n); // 2. Leaderboards Calculation const topLevels = [...allUsers].sort((a, b) => (b.level || 0) - (a.level || 0)).slice(0, 3); const topWealth = [...allUsers].sort((a, b) => Number(b.balance || 0n) - Number(a.balance || 0n)).slice(0, 3); const formatUser = (u: typeof users.$inferSelect, i: number) => { const star = i === 0 ? "🌟" : i === 1 ? "⭐" : "✨"; return `${star} <@${u.id}>`; }; const levelText = topLevels.map((u, i) => `> ${formatUser(u, i)} • Lvl ${u.level}`).join("\n") || "> *The sky is empty...*"; const wealthText = topWealth.map((u, i) => `> ${formatUser(u, i)} • ${u.balance} AU`).join("\n") || "> *The sky is empty...*"; // 3. Lootdrops Data const activeDrops = await DrizzleClient.query.lootdrops.findMany({ where: (lootdrops, { isNull }) => isNull(lootdrops.claimedBy), limit: 1, orderBy: desc(lootdrops.createdAt) }); const recentDrops = await DrizzleClient.query.lootdrops.findMany({ where: (lootdrops, { isNotNull }) => isNotNull(lootdrops.claimedBy), limit: 1, orderBy: desc(lootdrops.createdAt) }); // --- CONTAINER 1: Header --- const headerContainer = new ContainerBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent("# 🌌 AURORA OBSERVATORY"), new TextDisplayBuilder().setContent("*Current Moon Phase: Waxing Crescent 🌒*") ); // --- CONTAINER 2: Observation Log --- let phenomenaContent = ""; if (activeDrops.length > 0 && activeDrops[0]) { const drop = activeDrops[0]; phenomenaContent = `\n**SHOOTING STAR DETECTED**\nRadiance: \`${drop.rewardAmount} ${drop.currency}\`\nCoordinates: <#${drop.channelId}>\nImpact: `; } else if (recentDrops.length > 0 && recentDrops[0]) { const drop = recentDrops[0]; const claimer = allUsers.find(u => u.id === drop.claimedBy); phenomenaContent = `\n**RECENT EVENT**\nStar yielded \`${drop.rewardAmount} ${drop.currency}\` to ${claimer ? `<@${claimer.id}>` : '**Unknown**'}`; } const logContainer = new ContainerBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent("## 🔭 OBSERVATION LOG"), new TextDisplayBuilder().setContent(`> **Stargazers**: \`${totalUsers}\`\n> **Astral Wealth**: \`${totalWealth.toLocaleString()} AU\`${phenomenaContent}`) ); // --- CONTAINER 3: Leaders --- const leaderContainer = new ContainerBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent("## ✨ CONSTELLATION LEADERS"), new TextDisplayBuilder().setContent(`**Brightest Stars**\n${levelText}`), new TextDisplayBuilder().setContent(`**Gilded Nebulas**\n${wealthText}`) ); // --- CONTAINER 4: Echoes --- const recentTx = await DrizzleClient.query.transactions.findMany({ limit: 5, orderBy: [desc(transactions.createdAt)] }); const activityLines = recentTx.map(tx => { const time = Math.floor(tx.createdAt!.getTime() / 1000); let icon = "💫"; if (tx.type.includes("LOOT")) icon = "🌠"; if (tx.type.includes("GIFT")) icon = "🌕"; const user = allUsers.find(u => u.id === tx.userId); // the description might contain a channel id all the way at the end const channelId = tx.description?.split(" ").pop() || ""; const text = tx.description?.replace(channelId, "<#" + channelId + ">") || ""; return ` ${icon} ${user ? `<@${user.id}>` : '**Unknown**'}: ${text}`; }); const echoesContainer = new ContainerBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent("## 📡 COSMIC ECHOES"), new TextDisplayBuilder().setContent(activityLines.join("\n") || "Silence...") ); return [headerContainer, logContainer, leaderContainer, echoesContainer]; } };