import { TextChannel, ContainerBuilder, TextDisplayBuilder, SectionBuilder, SeparatorBuilder, ThumbnailBuilder, MessageFlags, SeparatorSpacingSize } from "discord.js"; import { AuroraClient } from "@/lib/BotClient"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import { users, transactions, lootdrops, inventory } from "@db/schema"; import { desc, sql } from "drizzle-orm"; import { config, saveConfig } from "@shared/lib/config"; // Color palette for containers (hex as decimal) const COLORS = { HEADER: 0x9B59B6, // Purple - mystical LEADERS: 0xF1C40F, // Gold - achievement ACTIVITY: 0x3498DB, // Blue - activity ALERT: 0xE74C3C // Red - active events }; export const terminalService = { init: async (channel: TextChannel) => { // Limit to one terminal for now if (config.terminal) { try { const oldChannel = await AuroraClient.channels.fetch(config.terminal.channelId) as TextChannel; if (oldChannel) { const oldMsg = await oldChannel.messages.fetch(config.terminal.messageId); if (oldMsg) await oldMsg.delete(); } } catch (e) { // Ignore if old message doesn't exist } } const msg = await channel.send({ content: "๐Ÿ”„ Initializing Aurora Station..." }); config.terminal = { channelId: channel.id, messageId: msg.id }; saveConfig(config); await terminalService.update(); }, update: async () => { if (!config.terminal) return; try { const channel = await AuroraClient.channels.fetch(config.terminal.channelId).catch(() => null) as TextChannel; if (!channel) { console.warn("Terminal channel not found"); return; } const message = await channel.messages.fetch(config.terminal.messageId).catch(() => null); if (!message) { console.warn("Terminal message not found"); return; } const containers = await terminalService.buildMessage(); 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 () => { // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // DATA FETCHING // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const allUsers = await DrizzleClient.select().from(users); const totalUsers = allUsers.length; const totalWealth = allUsers.reduce((acc: bigint, u: any) => acc + (u.balance || 0n), 0n); // System stats const uptime = process.uptime(); const uptimeHours = Math.floor(uptime / 3600); const uptimeMinutes = Math.floor((uptime % 3600) / 60); const ping = AuroraClient.ws.ping; const now = Math.floor(Date.now() / 1000); // Guild member count (if available) const guild = AuroraClient.guilds.cache.first(); const memberCount = guild?.memberCount ?? totalUsers; // Additional metrics const avgLevel = totalUsers > 0 ? Math.round(allUsers.reduce((acc: number, u: any) => acc + (u.level || 1), 0) / totalUsers) : 1; const topStreak = allUsers.reduce((max: number, u: any) => Math.max(max, u.dailyStreak || 0), 0); // Items in circulation const itemsResult = await DrizzleClient .select({ total: sql`COALESCE(SUM(${inventory.quantity}), 0)` }) .from(inventory); const totalItems = Number(itemsResult[0]?.total || 0); // Last command timestamp const lastCmd = AuroraClient.lastCommandTimestamp ? `` : "*Never*"; // Leaderboards 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); // Lootdrops const activeDrops = await DrizzleClient.query.lootdrops.findMany({ where: (lootdrops: any, { isNull }: any) => isNull(lootdrops.claimedBy), limit: 1, orderBy: desc(lootdrops.createdAt) }); const recentDrops = await DrizzleClient.query.lootdrops.findMany({ where: (lootdrops: any, { isNotNull }: any) => isNotNull(lootdrops.claimedBy), limit: 1, orderBy: desc(lootdrops.createdAt) }); // Recent transactions const recentTx = await DrizzleClient.query.transactions.findMany({ limit: 3, orderBy: [desc(transactions.createdAt)] }); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // HELPER FORMATTERS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const getMedal = (i: number) => i === 0 ? "๐Ÿฅ‡" : i === 1 ? "๐Ÿฅˆ" : "๐Ÿฅ‰"; const formatLeaderEntry = (u: typeof users.$inferSelect, i: number, type: 'level' | 'wealth') => { const medal = getMedal(i); const value = type === 'level' ? `Lvl ${u.level ?? 1}` : `${Number(u.balance ?? 0).toLocaleString()} AU`; return `${medal} **${u.username}** โ€” ${value}`; }; const getActivityIcon = (type: string) => { if (type.includes("LOOT")) return "๐ŸŒ "; if (type.includes("GIFT")) return "๐ŸŽ"; if (type.includes("SHOP")) return "๐Ÿ›’"; if (type.includes("DAILY")) return "โ˜€๏ธ"; if (type.includes("QUEST")) return "๐Ÿ“œ"; return "๐Ÿ’ซ"; }; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // CONTAINER 1: HEADER - Station Overview // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const botAvatar = AuroraClient.user?.displayAvatarURL({ size: 64 }) ?? ""; const headerSection = new SectionBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent("# ๐Ÿ”ฎ AURORA STATION"), new TextDisplayBuilder().setContent("-# Real-time server observatory") ) .setThumbnailAccessory( new ThumbnailBuilder().setURL(botAvatar) ); const statsText = [ `๐Ÿ“ก **Uptime** ${uptimeHours}h ${uptimeMinutes}m`, `๐Ÿ“ **Ping** ${ping}ms`, `๐Ÿ‘ฅ **Students** ${totalUsers}`, `๐Ÿช™ **Economy** ${totalWealth.toLocaleString()} AU` ].join(" โ€ข "); const secondaryStats = [ `๐Ÿ“ฆ **Items** ${totalItems.toLocaleString()}`, `๐Ÿ“ˆ **Avg Lvl** ${avgLevel}`, `๐Ÿ”ฅ **Top Streak** ${topStreak}d`, `โšก **Last Cmd** ${lastCmd}` ].join(" โ€ข "); const headerContainer = new ContainerBuilder() .setAccentColor(COLORS.HEADER) .addSectionComponents(headerSection) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)) .addTextDisplayComponents( new TextDisplayBuilder().setContent(statsText), new TextDisplayBuilder().setContent(secondaryStats), new TextDisplayBuilder().setContent(`-# Updated `) ); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // CONTAINER 2: LEADERBOARDS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const levelLeaderText = topLevels.length > 0 ? topLevels.map((u, i) => formatLeaderEntry(u, i, 'level')).join("\n") : "*No data yet*"; const wealthLeaderText = topWealth.length > 0 ? topWealth.map((u, i) => formatLeaderEntry(u, i, 'wealth')).join("\n") : "*No data yet*"; const leadersContainer = new ContainerBuilder() .setAccentColor(COLORS.LEADERS) .addTextDisplayComponents( new TextDisplayBuilder().setContent("## โœจ CONSTELLATION LEADERS") ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)) .addTextDisplayComponents( new TextDisplayBuilder().setContent(`**Brightest Stars**\n${levelLeaderText}`), new TextDisplayBuilder().setContent(`**Gilded Nebulas**\n${wealthLeaderText}`) ); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // CONTAINER 3: LIVE ACTIVITY // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // Determine if there's an active lootdrop const hasActiveDrop = activeDrops.length > 0 && activeDrops[0]; const activityColor = hasActiveDrop ? COLORS.ALERT : COLORS.ACTIVITY; const activityContainer = new ContainerBuilder() .setAccentColor(activityColor) .addTextDisplayComponents( new TextDisplayBuilder().setContent("## ๐ŸŒ  LIVE ACTIVITY") ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)); // Active lootdrop or recent event if (hasActiveDrop) { const drop = activeDrops[0]!; const expiresTimestamp = Math.floor(drop.expiresAt!.getTime() / 1000); activityContainer.addTextDisplayComponents( new TextDisplayBuilder().setContent( `๐Ÿšจ **SHOOTING STAR ACTIVE**\n` + `> **Reward:** \`${drop.rewardAmount} ${drop.currency}\`\n` + `> **Location:** <#${drop.channelId}>\n` + `> **Expires:** ` ) ); } else if (recentDrops.length > 0 && recentDrops[0]) { const drop = recentDrops[0]; const claimer = allUsers.find((u: any) => u.id === drop.claimedBy); const claimedTimestamp = drop.createdAt ? Math.floor(drop.createdAt.getTime() / 1000) : now; activityContainer.addTextDisplayComponents( new TextDisplayBuilder().setContent( `โœ… **Last Star Claimed**\n` + `> **${claimer?.username ?? 'Unknown'}** collected \`${drop.rewardAmount} ${drop.currency}\` ` ) ); } else { activityContainer.addTextDisplayComponents( new TextDisplayBuilder().setContent(`-# The sky is quiet... waiting for the next star.`) ); } // Recent transactions if (recentTx.length > 0) { activityContainer.addSeparatorComponents( new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small) ); const txLines = recentTx.map((tx: any) => { const time = Math.floor(tx.createdAt!.getTime() / 1000); const icon = getActivityIcon(tx.type); const user = allUsers.find((u: any) => u.id === tx.userId); // Clean description (remove trailing channel IDs) let desc = tx.description || "Unknown"; desc = desc.replace(/\s*\d{17,19}\s*$/, "").trim(); return `${icon} **${user?.username ?? 'Unknown'}**: ${desc} ยท `; }); activityContainer.addTextDisplayComponents( new TextDisplayBuilder().setContent("**Recent Echoes**"), new TextDisplayBuilder().setContent(txLines.join("\n")) ); } return [headerContainer, leadersContainer, activityContainer]; } };