diff --git a/README.md b/README.md index 98604e8..785a0ee 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Bot for the [6b6t Discord Server](https://discord.6b6t.org) | Command | Description | Permissions | |---------|-------------|-------------| | `/ip` | Get the 6b6t server IP addresses | Everyone | -| `/playercount` | Check current players online | Everyone | +| `/playercount` | Check current players online and server uptime | Everyone | | `/version` | Get the current Minecraft version | Everyone | | `/shop` | Information about the 6b6t shop | Everyone | | `/boost` | Information about Discord boosting perks | Everyone | diff --git a/biome.json b/biome.json index fce2b64..0fb6323 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/src/commands/getuser.ts b/src/commands/getuser.ts index a170f91..eb4d440 100644 --- a/src/commands/getuser.ts +++ b/src/commands/getuser.ts @@ -33,7 +33,7 @@ const GetUserCommand: Command = { } await interaction.editReply( - `**${discordUser.tag}** is linked to **${info.name}**.\n` + + `Discord user **${discordUser.tag}** is linked to Minecraft user **${info.name}**.\n` + `Top Rank: **${info.topRank}**\n` + `First Join Year: **${info.firstJoinYear}**`, ); diff --git a/src/commands/playercount.ts b/src/commands/playercount.ts index 21c17e8..fb74f50 100644 --- a/src/commands/playercount.ts +++ b/src/commands/playercount.ts @@ -6,7 +6,7 @@ import { getServerData } from "../utils/helpers"; const PlayerCountCommand: Command = { data: new SlashCommandBuilder() .setName("playercount") - .setDescription("See 6b6t's current player count"), + .setDescription("See 6b6t's current player count and uptime"), async execute(interaction: CommandInteraction) { const data = await getServerData(config.statusHost); @@ -15,10 +15,41 @@ const PlayerCountCommand: Command = { content: "Failed to get server data", ephemeral: true, }); + return; + } + + let uptimeStr = ""; + if (data.uptime.serverStartUnix) { + const diff = Math.floor(Date.now() / 1000 - data.uptime.serverStartUnix); + const days = Math.floor(diff / 86400); + const hours = Math.floor((diff % 86400) / 3600); + const minutes = Math.floor((diff % 3600) / 60); + const seconds = diff % 60; + const parts = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0) parts.push(`${hours}h`); + if (minutes > 0) parts.push(`${minutes}m`); + if (seconds > 0) parts.push(`${seconds}s`); + uptimeStr = parts.join(" "); + } else if (data.uptime.currentUptimeHours) { + const totalSeconds = Math.floor(data.uptime.currentUptimeHours * 3600); + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + const parts = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0) parts.push(`${hours}h`); + if (minutes > 0) parts.push(`${minutes}m`); + if (seconds > 0) parts.push(`${seconds}s`); + uptimeStr = parts.join(" "); + } else { + await interaction.reply(`The server is currently down.`); + return; } await interaction.reply( - `There are currently ${data.players.now} players online on 6b6t.`, + `There are currently ${data.players.online} players online on 6b6t. The server has been up for ${uptimeStr}.`, ); }, }; diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index 3b436c7..23ffbab 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -3,6 +3,9 @@ import config from "./config/config"; import { CommandManager } from "./utils/commandManager"; import "dotenv/config"; +const token = process.env.DISCORD_TOKEN; +if (!token) throw new Error("Environment variable DISCORD_TOKEN is not set"); + const commandManager = new CommandManager(); await commandManager.loadCommands(); @@ -10,7 +13,7 @@ const commands = Array.from(commandManager.getCommands().values()).map( (command) => command.data.toJSON(), ); -const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN!); +const rest = new REST({ version: "10" }).setToken(token); (async () => { try { diff --git a/src/index.ts b/src/index.ts index d2e4153..1b079b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,13 +40,13 @@ const client = new Client({ const commandManager = new CommandManager(); async function initializeBot() { + const token = process.env.DISCORD_TOKEN; + if (!token) throw new Error("Environment variable DISCORD_TOKEN is not set"); try { console.log("Loading commands..."); await commandManager.loadCommands(); - const rest = new REST({ version: "10" }).setToken( - process.env.DISCORD_TOKEN!, - ); + const rest = new REST({ version: "10" }).setToken(token); const commands = commandManager.getCommandsJSON(); console.log( `Registering ${commands.length} commands to guild ${config.guildId}...`, @@ -57,7 +57,7 @@ async function initializeBot() { ); console.log("Initializing bot..."); - await client.login(process.env.DISCORD_TOKEN!); + await client.login(token); console.log( `Bot initialized and ready to serve in guild: ${config.guildId}`, diff --git a/src/types/command.ts b/src/types/command.ts index d01a996..1fceb4f 100644 --- a/src/types/command.ts +++ b/src/types/command.ts @@ -2,10 +2,16 @@ import type { ButtonInteraction, ChatInputCommandInteraction, ModalSubmitInteraction, + SlashCommandBuilder, + SlashCommandOptionsOnlyBuilder, + SlashCommandSubcommandsOnlyBuilder, } from "discord.js"; export interface Command { - data: any; + data: + | SlashCommandBuilder + | SlashCommandSubcommandsOnlyBuilder + | SlashCommandOptionsOnlyBuilder; cooldown?: number; admin?: boolean; execute: (interaction: ChatInputCommandInteraction) => Promise; diff --git a/src/types/index.ts b/src/types/index.ts index 06c10cd..866df5f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ import type { ChatInputCommandInteraction, + ClientEvents, SlashCommandBuilder, } from "discord.js"; @@ -8,7 +9,7 @@ export interface Command { execute: (interaction: ChatInputCommandInteraction) => Promise; } -export interface Event { - name: string; - execute: (...args: any[]) => void; +export interface Event { + name: K; + execute: (...args: ClientEvents[K]) => void; } diff --git a/src/utils/commandManager.ts b/src/utils/commandManager.ts index 170976b..cdf3c60 100644 --- a/src/utils/commandManager.ts +++ b/src/utils/commandManager.ts @@ -53,18 +53,18 @@ export class CommandManager { userId: string, cooldownSeconds: number, ): number { - if (!this.cooldowns.has(commandName)) { - this.cooldowns.set(commandName, new Collection()); + let timestamps = this.cooldowns.get(commandName); + + if (!timestamps) { + timestamps = new Collection(); + this.cooldowns.set(commandName, timestamps); } const now = Date.now(); - const timestamps = this.cooldowns.get(commandName)!; + const expirationTime = timestamps.get(userId); - if (timestamps.has(userId)) { - const expirationTime = timestamps.get(userId)!; - if (now < expirationTime) { - return Math.ceil((expirationTime - now) / 1000); - } + if (expirationTime && now < expirationTime) { + return Math.ceil((expirationTime - now) / 1000); } timestamps.set(userId, now + cooldownSeconds * 1000); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index df3fee4..1314af9 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -3,6 +3,8 @@ import type { RowDataPacket } from "mysql2"; import { getAllUuidDiscordMappings } from "./link/storage"; import { getStatsPool } from "./mysql-client"; +const SERVER_API = `https://www.6b6t.org/api`; + export type UserInfo = { topRank: string; firstJoinYear: number; @@ -151,23 +153,26 @@ export async function botHasRecentMessages( } export async function getServerData(host: string): Promise { - const mcUrl = `https://mcapi.us/server/status?ip=${host}&port=25565`; - const versionUrl = `https://www.6b6t.org/api/version`; + const mcUrl = `https://api.mcstatus.io/v2/status/java/${host}`; + const versionUrl = `${SERVER_API}/version`; + const uptimeUrl = `${SERVER_API}/uptime`; try { - const [mcRes, versionRes] = await Promise.all([ + const [mcRes, versionRes, uptimeRes] = await Promise.all([ fetch(mcUrl), fetch(versionUrl), + fetch(uptimeUrl), ]); - if (!mcRes.ok || !versionRes.ok) return null; + if (!mcRes.ok || !versionRes.ok || !uptimeRes.ok) return null; - const [mcData, versionData] = await Promise.all([ + const [mcData, versionData, uptimeData] = await Promise.all([ mcRes.json(), versionRes.json(), + uptimeRes.json(), ]); - return { ...mcData, ...versionData }; + return { ...mcData, ...versionData, uptime: uptimeData.statistics }; } catch (error) { console.error("Error fetching server data:", error); return null;