forked from syntaxbullet/AuroraBot-discord
chore: update terminal adding nicer graphics
This commit is contained in:
@@ -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: <t:${Math.floor(drop.expiresAt!.getTime() / 1000)}:R>`;
|
||||
} 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 `<t:${time}:R> ${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 <t:${now}:R>`)
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 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:** <t:${expiresTimestamp}:R>`
|
||||
)
|
||||
);
|
||||
} 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}\` <t:${claimedTimestamp}:R>`
|
||||
)
|
||||
);
|
||||
} 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} · <t:${time}:R>`;
|
||||
});
|
||||
|
||||
activityContainer.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent("**Recent Echoes**"),
|
||||
new TextDisplayBuilder().setContent(txLines.join("\n"))
|
||||
);
|
||||
}
|
||||
|
||||
return [headerContainer, leadersContainer, activityContainer];
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user