From d29a1ec2b7b9e2b72fc8805d3980b9e699808669 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Tue, 6 Jan 2026 20:51:39 +0100 Subject: [PATCH] chore: update terminal adding nicer graphics --- src/modules/terminal/terminal.service.ts | 263 ++++++++++++++++------- 1 file changed, 184 insertions(+), 79 deletions(-) diff --git a/src/modules/terminal/terminal.service.ts b/src/modules/terminal/terminal.service.ts index 0e69692..e496a03 100644 --- a/src/modules/terminal/terminal.service.ts +++ b/src/modules/terminal/terminal.service.ts @@ -1,13 +1,30 @@ -import { TextChannel, Message, ContainerBuilder, TextDisplayBuilder, SectionBuilder, MessageFlags } from "discord.js"; +import { + TextChannel, + ContainerBuilder, + TextDisplayBuilder, + SectionBuilder, + SeparatorBuilder, + ThumbnailBuilder, + MessageFlags, + SeparatorSpacingSize +} 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 { config, saveConfig } from "@/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 + // Limit to one terminal for now if (config.terminal) { try { const oldChannel = await AuroraClient.channels.fetch(config.terminal.channelId) as TextChannel; @@ -16,11 +33,11 @@ export const terminalService = { if (oldMsg) await oldMsg.delete(); } } catch (e) { - // ignore if old message doesn't exist + // Ignore if old message doesn't exist } } - const msg = await channel.send({ content: "๐Ÿ”„ Initializing Aurora Observatory..." }); + const msg = await channel.send({ content: "๐Ÿ”„ Initializing Aurora Station..." }); config.terminal = { channelId: channel.id, @@ -48,8 +65,6 @@ export const terminalService = { 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, @@ -64,24 +79,34 @@ export const terminalService = { }, buildMessage: async () => { - // 1. Data Fetching + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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); + // 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); - const formatUser = (u: typeof users.$inferSelect, i: number) => { - const star = i === 0 ? "๐ŸŒŸ" : i === 1 ? "โญ" : "โœจ"; - return `${star} <@${u.id}>`; - }; + // Guild member count (if available) + const guild = AuroraClient.guilds.cache.first(); + const memberCount = guild?.memberCount ?? totalUsers; - 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...*"; + // 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); - // 3. Lootdrops Data + // Lootdrops const activeDrops = await DrizzleClient.query.lootdrops.findMany({ where: (lootdrops, { isNull }) => isNull(lootdrops.claimedBy), limit: 1, @@ -94,78 +119,158 @@ export const terminalService = { orderBy: desc(lootdrops.createdAt) }); - // 4. System Stats - const memoryUsage = process.memoryUsage(); - const uptime = process.uptime(); - const uptimeHours = Math.floor(uptime / 3600); - const uptimeMinutes = Math.floor((uptime % 3600) / 60); - const ramUsed = Math.round(memoryUsage.heapUsed / 1024 / 1024); - - // --- CONTAINER 1: Header --- - const headerContainer = new ContainerBuilder() - .addTextDisplayComponents( - new TextDisplayBuilder().setContent("# ๐ŸŒŒ AURORA OBSERVATORY"), - new TextDisplayBuilder().setContent(`*Current Moon Phase: Waxing Crescent ๐ŸŒ’ โ€ข System Online for ${uptimeHours}h ${uptimeMinutes}m*`) - ); - - // --- 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\`\n> **Memory**: \`${ramUsed}MB\`${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 --- + // Recent transactions const recentTx = await DrizzleClient.query.transactions.findMany({ - limit: 5, + limit: 3, 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 = "๐ŸŽ"; - if (tx.type.includes("SHOP")) icon = "๐Ÿ›’"; - if (tx.type.includes("DAILY")) icon = "โ˜€๏ธ"; - const user = allUsers.find(u => u.id === tx.userId); + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // HELPER FORMATTERS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - // Clean up description - const channelId = tx.description?.split(" ").pop() || ""; - let text = tx.description || "Unknown interaction"; - if (channelId.match(/^\d+$/)) { - text = text.replace(channelId, "").trim(); - } + const getMedal = (i: number) => i === 0 ? "๐Ÿฅ‡" : i === 1 ? "๐Ÿฅˆ" : "๐Ÿฅ‰"; - return ` ${icon} **${user ? user.username : 'Unknown'}**: ${text}`; - }); + 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 echoesContainer = new ContainerBuilder() + 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("## ๐Ÿ“ก COSMIC ECHOES"), - new TextDisplayBuilder().setContent(activityLines.join("\n") || "Silence...") + 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(" โ€ข "); - return [headerContainer, logContainer, leaderContainer, echoesContainer]; + const headerContainer = new ContainerBuilder() + .setAccentColor(COLORS.HEADER) + .addSectionComponents(headerSection) + .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent(statsText), + 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 => 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 => { + const time = Math.floor(tx.createdAt!.getTime() / 1000); + const icon = getActivityIcon(tx.type); + const user = allUsers.find(u => 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]; } };