Files
discord-rpg-concept/src/modules/terminal/terminal.service.ts
2026-01-06 20:36:26 +01:00

172 lines
7.4 KiB
TypeScript

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 { config, saveConfig } from "@/lib/config";
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 Observatory..." });
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();
// 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)
});
// 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 ---
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 = "🎁";
if (tx.type.includes("SHOP")) icon = "🛒";
if (tx.type.includes("DAILY")) icon = "☀️";
const user = allUsers.find(u => u.id === tx.userId);
// Clean up description
const channelId = tx.description?.split(" ").pop() || "";
let text = tx.description || "Unknown interaction";
if (channelId.match(/^\d+$/)) {
text = text.replace(channelId, "").trim();
}
return `<t:${time}:R> ${icon} **${user ? user.username : 'Unknown'}**: ${text}`;
});
const echoesContainer = new ContainerBuilder()
.addTextDisplayComponents(
new TextDisplayBuilder().setContent("## 📡 COSMIC ECHOES"),
new TextDisplayBuilder().setContent(activityLines.join("\n") || "Silence...")
);
return [headerContainer, logContainer, leaderContainer, echoesContainer];
}
};