180 lines
7.4 KiB
TypeScript
180 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 { 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: <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\`${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 `<t:${time}:F> ${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];
|
|
}
|
|
};
|