diff --git a/.eslintignore b/.eslintignore index af6ab76..b053eec 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,7 @@ scripts/* *.config.js .DS_Store node_modules +**/bun.lockb coverage .next build @@ -17,4 +18,5 @@ build !jest.config.js !plopfile.js !react-shim.js -!tsup.config.ts \ No newline at end of file +!tsup.config.ts +**/favicon.ico \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 3387a46..adc5294 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -43,7 +43,11 @@ "prettier/prettier": [ "warn", { - "tabWidth": 4 + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "es5" } ], "no-unused-vars": "off", @@ -99,26 +103,14 @@ }, { "blankLine": "always", - "prev": [ - "const", - "let", - "var" - ], + "prev": ["const", "let", "var"], "next": "*" }, { "blankLine": "any", - "prev": [ - "const", - "let", - "var" - ], - "next": [ - "const", - "let", - "var" - ] + "prev": ["const", "let", "var"], + "next": ["const", "let", "var"] } ] } -} \ No newline at end of file +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..90755ac --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/api/package.json b/api/package.json index 4ac8df5..fb64225 100644 --- a/api/package.json +++ b/api/package.json @@ -1,21 +1,21 @@ { - "name": "@chatr/api", - "type": "module", - "version": "0.1.0", - "scripts": { - "dev": "bun with-env bun --watch src/index.ts --dev", - "with-env": "dotenv -e ../.env --" - }, - "dependencies": { - "cors": "^2.8.5", - "cron": "^3.1.7", - "express": "^4.19.2", - "mysql2": "^3.10.3" - }, - "devDependencies": { - "@types/bun": "latest", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "dotenv-cli": "^7.4.2" - } + "name": "@chatr/api", + "type": "module", + "version": "0.1.0", + "scripts": { + "dev": "bun with-env bun --watch src/index.ts --dev", + "with-env": "dotenv -e ../.env --" + }, + "dependencies": { + "cors": "^2.8.5", + "cron": "^3.1.7", + "express": "^4.19.2", + "mysql2": "^3.10.3" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "dotenv-cli": "^7.4.2" + } } diff --git a/api/src/db/index.ts b/api/src/db/index.ts index 114fed6..59bf52a 100644 --- a/api/src/db/index.ts +++ b/api/src/db/index.ts @@ -2,15 +2,15 @@ import mysql from "mysql2"; // Create a MySQL connection pool export const pool = mysql.createPool({ - host: process.env.MYSQL_ADDRESS as string, - port: parseInt(process.env.MYSQL_PORT as string), - user: process.env.MYSQL_USER as string, - password: process.env.MYSQL_PASSWORD as string, - database: process.env.MYSQL_DATABASE as string, + host: process.env.MYSQL_ADDRESS as string, + port: parseInt(process.env.MYSQL_PORT as string), + user: process.env.MYSQL_USER as string, + password: process.env.MYSQL_PASSWORD as string, + database: process.env.MYSQL_DATABASE as string, }); -export * from './init'; -export * from './queries/guilds'; -export * from './queries/users'; -export * from './queries/updates'; -export * from './queries/tracking'; \ No newline at end of file +export * from "./init"; +export * from "./queries/guilds"; +export * from "./queries/users"; +export * from "./queries/updates"; +export * from "./queries/tracking"; diff --git a/api/src/db/init.ts b/api/src/db/init.ts index af162b3..dfaf7db 100644 --- a/api/src/db/init.ts +++ b/api/src/db/init.ts @@ -1,7 +1,7 @@ import { pool } from "."; export async function initTables() { - const createGuildsTable = ` + const createGuildsTable = ` CREATE TABLE IF NOT EXISTS guilds ( id VARCHAR(255) NOT NULL PRIMARY KEY, name VARCHAR(255), @@ -13,7 +13,7 @@ export async function initTables() { is_in_guild BOOLEAN DEFAULT TRUE ) `; - const createUsersTable = ` + const createUsersTable = ` CREATE TABLE IF NOT EXISTS users ( id VARCHAR(255) NOT NULL, guild_id VARCHAR(255) NOT NULL, @@ -28,7 +28,7 @@ export async function initTables() { PRIMARY KEY (id, guild_id) ) `; - const createRolesTable = ` + const createRolesTable = ` CREATE TABLE IF NOT EXISTS roles ( id VARCHAR(255) NOT NULL PRIMARY KEY, guild_id VARCHAR(255) NOT NULL, @@ -36,7 +36,7 @@ export async function initTables() { level INT NOT NULL ) `; - const createTrackingTable = ` + const createTrackingTable = ` CREATE TABLE IF NOT EXISTS tracking ( time TIMESTAMP, user_id VARCHAR(255) NOT NULL, @@ -45,35 +45,35 @@ export async function initTables() { ) `; - pool.query(createGuildsTable, (err) => { - if (err) { - console.error("Error creating guilds table:", err); - } else { - console.log("Guilds table created"); - } - }); + pool.query(createGuildsTable, (err) => { + if (err) { + console.error("Error creating guilds table:", err); + } else { + console.log("Guilds table created"); + } + }); - pool.query(createUsersTable, (err) => { - if (err) { - console.error("Error creating users table:", err); - } else { - console.log("Users table created"); - } - }); + pool.query(createUsersTable, (err) => { + if (err) { + console.error("Error creating users table:", err); + } else { + console.log("Users table created"); + } + }); - pool.query(createRolesTable, (err) => { - if (err) { - console.error("Error creating roles table:", err); - } else { - console.log("Roles table created"); - } - }); + pool.query(createRolesTable, (err) => { + if (err) { + console.error("Error creating roles table:", err); + } else { + console.log("Roles table created"); + } + }); - pool.query(createTrackingTable, (err) => { - if (err) { - console.error("Error creating tracking table:", err); - } else { - console.log("Tracking table created"); - } - }); + pool.query(createTrackingTable, (err) => { + if (err) { + console.error("Error creating tracking table:", err); + } else { + console.log("Tracking table created"); + } + }); } diff --git a/api/src/db/queries/guilds.ts b/api/src/db/queries/guilds.ts index 2b631d1..f9a1663 100644 --- a/api/src/db/queries/guilds.ts +++ b/api/src/db/queries/guilds.ts @@ -1,33 +1,41 @@ import type { QueryError } from "mysql2"; + import { pool } from ".."; export interface Guild { - id: string; - name: string; - icon: string; - members: number; - cooldown: number; - updates_enabled: 0 | 1; - updates_channel_id: string | null; + id: string; + name: string; + icon: string; + members: number; + cooldown: number; + updates_enabled: 0 | 1; + updates_channel_id: string | null; } - -export async function getGuild(guildId: string): Promise<[QueryError, null] | [null, Guild | null]> { - return new Promise((resolve, reject) => { - pool.query("SELECT * FROM guilds WHERE id = ? AND is_in_guild = ?", [guildId, true], (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, (results as Guild[])[0]]); - } - }); - }); +export async function getGuild( + guildId: string +): Promise<[QueryError, null] | [null, Guild | null]> { + return new Promise((resolve, reject) => { + pool.query( + "SELECT * FROM guilds WHERE id = ? AND is_in_guild = ?", + [guildId, true], + (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, (results as Guild[])[0]]); + } + } + ); + }); } -export async function updateGuild(guild: Omit): Promise<[QueryError | null, null] | [null, Guild[]]> { - return new Promise((resolve, reject) => { - pool.query( - ` +export async function updateGuild( + guild: Omit +): Promise<[QueryError | null, null] | [null, Guild[]]> { + return new Promise((resolve, reject) => { + pool.query( + ` INSERT INTO guilds (id, name, icon, members, is_in_guild) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE @@ -36,89 +44,103 @@ export async function updateGuild(guild: Omit { - if (err) { - reject([err, null]); - } else { - resolve([null, results as Guild[]]); - } - }, - ); - }); + [guild.id, guild.name, guild.icon, guild.members, true], + (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, results as Guild[]]); + } + } + ); + }); } -export async function removeGuild(guildId: string): Promise<[QueryError, null] | [null, true]> { - return new Promise((resolve, reject) => { - pool.query("UPDATE guilds SET is_in_guild = ? WHERE id = ?", [false, guildId], (err) => { - if (err) { - reject([err, null]); - } else { - resolve([null, true]); - } - }); - }); +export async function removeGuild( + guildId: string +): Promise<[QueryError, null] | [null, true]> { + return new Promise((resolve, reject) => { + pool.query( + "UPDATE guilds SET is_in_guild = ? WHERE id = ?", + [false, guildId], + (err) => { + if (err) { + reject([err, null]); + } else { + resolve([null, true]); + } + } + ); + }); } -export async function setCooldown(guildId: string, cooldown: number): Promise<[QueryError, null] | [null, Guild]> { - return new Promise((resolve, reject) => { - pool.query("UPDATE guilds SET cooldown = ? WHERE id = ?", [cooldown, guildId], (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, (results as Guild[])[0]]); - } - }); - }) +export async function setCooldown( + guildId: string, + cooldown: number +): Promise<[QueryError, null] | [null, Guild]> { + return new Promise((resolve, reject) => { + pool.query( + "UPDATE guilds SET cooldown = ? WHERE id = ?", + [cooldown, guildId], + (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, (results as Guild[])[0]]); + } + } + ); + }); } interface BotInfo { - total_guilds: number; - total_members: number; - user_count?: number; + total_guilds: number; + total_members: number; + user_count?: number; } -export async function getBotInfo(): Promise<[QueryError | null, BotInfo | null]> { - return new Promise((resolve, reject) => { - pool.query("SELECT COUNT(*) AS total_guilds, SUM(members) AS total_members FROM guilds", (err, results) => { - if (err) { - reject([err, null]); - } else { - const botInfo: BotInfo = { - total_guilds: (results as BotInfo[])[0].total_guilds, - total_members: (results as BotInfo[])[0].total_members ?? 0, - }; - getUsersCount() - .then(([userCountError, userCount]) => { - if (userCountError) { - reject([userCountError, null]); - } else { - botInfo.user_count = userCount; - resolve([null, botInfo]); - } - }) - .catch((error) => { - reject([error, null]); - }); - } - }); - }); +export async function getBotInfo(): Promise< + [QueryError | null, BotInfo | null] +> { + return new Promise((resolve, reject) => { + pool.query( + "SELECT COUNT(*) AS total_guilds, SUM(members) AS total_members FROM guilds", + (err, results) => { + if (err) { + reject([err, null]); + } else { + const botInfo: BotInfo = { + total_guilds: (results as BotInfo[])[0].total_guilds, + total_members: + (results as BotInfo[])[0].total_members ?? 0, + }; + + getUsersCount() + .then(([userCountError, userCount]) => { + if (userCountError) { + reject([userCountError, null]); + } else { + botInfo.user_count = userCount; + resolve([null, botInfo]); + } + }) + .catch((error) => { + reject([error, null]); + }); + } + } + ); + }); } export async function getUsersCount(): Promise<[QueryError | null, number]> { - return new Promise((resolve, reject) => { - pool.query("SELECT COUNT(*) AS count FROM users", (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, (results as { count: number }[])[0].count]); - } - }); - }); + return new Promise((resolve, reject) => { + pool.query("SELECT COUNT(*) AS count FROM users", (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, (results as { count: number }[])[0].count]); + } + }); + }); } diff --git a/api/src/db/queries/tracking.ts b/api/src/db/queries/tracking.ts index 24a60da..2130126 100644 --- a/api/src/db/queries/tracking.ts +++ b/api/src/db/queries/tracking.ts @@ -1,100 +1,144 @@ // TODO: Move this file to a utils folder or something - this is NOT queries +import type { QueryError } from "mysql2"; + import { CronJob } from "cron"; + import { pool } from ".."; -import type { QueryError } from "mysql2"; let usersToUpdate: Record = {}; -let timestamp: string - -export async function addUserToTrackingData(userId: string, guildId: string) : Promise { - console.log("Adding user to tracking data:", userId, guildId); - if (!usersToUpdate[guildId]) { - usersToUpdate[guildId] = []; - } - if (!usersToUpdate[guildId].includes(userId)) { - usersToUpdate[guildId].push(userId); - return; - } - return; +let timestamp: string; + +export async function addUserToTrackingData( + userId: string, + guildId: string +): Promise { + console.log("Adding user to tracking data:", userId, guildId); + if (!usersToUpdate[guildId]) { + usersToUpdate[guildId] = []; + } + if (!usersToUpdate[guildId].includes(userId)) { + usersToUpdate[guildId].push(userId); + + return; + } + + return; } async function doTrackingJob() { - timestamp = new Date().toISOString().slice(0, 19).replace('T', ' '); - const usersToUpdateTemp = { ...usersToUpdate }; - usersToUpdate = {}; - console.log("Updating users:", usersToUpdateTemp); - if (!Object.keys(usersToUpdateTemp).length) { - console.log("No users to update!"); - return; - } - const guildIds = Object.keys(usersToUpdateTemp); - for (const guildId of guildIds) { - const userIds = usersToUpdateTemp[guildId]; - const userIdsString = userIds.join(","); - const [err, results] = await getUsersXp(userIdsString, guildId); - if (err) { - console.error("Error getting users:", err); - return; - } - console.log("Results:", results); - for (const result of results) { - const { id, guild_id, xp } = result; - await insertUserDataToTracking(id, guild_id, xp); - } - } + timestamp = new Date().toISOString().slice(0, 19).replace("T", " "); + const usersToUpdateTemp = { ...usersToUpdate }; + + usersToUpdate = {}; + console.log("Updating users:", usersToUpdateTemp); + if (!Object.keys(usersToUpdateTemp).length) { + console.log("No users to update!"); + + return; + } + const guildIds = Object.keys(usersToUpdateTemp); + + for (const guildId of guildIds) { + const userIds = usersToUpdateTemp[guildId]; + const userIdsString = userIds.join(","); + const [err, results] = await getUsersXp(userIdsString, guildId); + + if (err) { + console.error("Error getting users:", err); + + return; + } + console.log("Results:", results); + for (const result of results) { + const { id, guild_id, xp } = result; + + await insertUserDataToTracking(id, guild_id, xp); + } + } } const trackingJob = new CronJob("*/5 * * * * *", doTrackingJob); + trackingJob.start(); -export async function getUsersTrackingData(userId: string, guildId: string): Promise<[QueryError | null, any]> { - return new Promise((resolve, reject) => { - pool.query("SELECT * FROM tracking WHERE user_id = ? AND guild_id = ?", [userId, guildId], (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, results]); - } - }); - }); +export async function getUsersTrackingData( + userId: string, + guildId: string +): Promise<[QueryError | null, any]> { + return new Promise((resolve, reject) => { + pool.query( + "SELECT * FROM tracking WHERE user_id = ? AND guild_id = ?", + [userId, guildId], + (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, results]); + } + } + ); + }); } -export async function getGuildTrackingData(guildId: string, override: number | null): Promise<[QueryError | null, null] | [null, any]> { - const topNumber: number = override || 10; - - return new Promise((resolve, reject) => { - pool.query("SELECT * FROM tracking WHERE guild_id = ? ORDER BY xp DESC, time ASC LIMIT ?", [guildId, topNumber], (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, results]); - } - }); - }); +export async function getGuildTrackingData( + guildId: string, + override: number | null +): Promise<[QueryError | null, null] | [null, any]> { + const topNumber: number = override || 10; + + return new Promise((resolve, reject) => { + pool.query( + "SELECT * FROM tracking WHERE guild_id = ? ORDER BY xp DESC, time ASC LIMIT ?", + [guildId, topNumber], + (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, results]); + } + } + ); + }); } -async function getUsersXp(userString: string, guildId: string): Promise<[QueryError | null, any]> { - return new Promise((resolve, reject) => { - pool.query("SELECT * FROM users WHERE id IN (?) AND guild_id = ?", [userString, guildId], (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, results]); - } - }); - }); +async function getUsersXp( + userString: string, + guildId: string +): Promise<[QueryError | null, any]> { + return new Promise((resolve, reject) => { + pool.query( + "SELECT * FROM users WHERE id IN (?) AND guild_id = ?", + [userString, guildId], + (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, results]); + } + } + ); + }); } -async function insertUserDataToTracking(userId: string, guildId: string, xp: number): Promise<[QueryError | null, null]> { - const time = timestamp; - return new Promise((resolve, reject) => { - pool.query("INSERT INTO tracking (user_id, guild_id, xp, time) VALUES (?, ?, ?, ?)", [userId, guildId, xp, time], (err) => { - if (err) { - reject([err, null]); - } else { - resolve([null, null]); - } - }); - }); -} \ No newline at end of file +async function insertUserDataToTracking( + userId: string, + guildId: string, + xp: number +): Promise<[QueryError | null, null]> { + const time = timestamp; + + return new Promise((resolve, reject) => { + pool.query( + "INSERT INTO tracking (user_id, guild_id, xp, time) VALUES (?, ?, ?, ?)", + [userId, guildId, xp, time], + (err) => { + if (err) { + reject([err, null]); + } else { + resolve([null, null]); + } + } + ); + }); +} diff --git a/api/src/db/queries/updates.ts b/api/src/db/queries/updates.ts index 8c5c296..26a2d7c 100644 --- a/api/src/db/queries/updates.ts +++ b/api/src/db/queries/updates.ts @@ -1,87 +1,91 @@ import type { QueryError } from "mysql2"; + import { pool } from ".."; export interface Updates { - guild_id: string; - channel_id: string; - enabled: boolean; + guild_id: string; + channel_id: string; + enabled: boolean; } -export async function enableUpdates(guildId: string): Promise<[QueryError | null, boolean]> { - return new Promise((resolve, reject) => { - pool.query( - ` +export async function enableUpdates( + guildId: string +): Promise<[QueryError | null, boolean]> { + return new Promise((resolve, reject) => { + pool.query( + ` UPDATE guilds SET updates_enabled = TRUE WHERE id = ? `, - [ - guildId, - ], - (err) => { - if (err) { - reject([err, false]); - } else { - resolve([null, true]); - } - }, - ); - }); + [guildId], + (err) => { + if (err) { + reject([err, false]); + } else { + resolve([null, true]); + } + } + ); + }); } -export async function disableUpdates(guildId: string): Promise<[QueryError | null, boolean]> { - return new Promise((resolve, reject) => { - pool.query( - ` +export async function disableUpdates( + guildId: string +): Promise<[QueryError | null, boolean]> { + return new Promise((resolve, reject) => { + pool.query( + ` UPDATE guilds SET updates_enabled = FALSE WHERE id = ? `, - [ - guildId, - ], - (err) => { - if (err) { - reject([err, false]); - } else { - resolve([null, true]); - } - }, - ); - }); + [guildId], + (err) => { + if (err) { + reject([err, false]); + } else { + resolve([null, true]); + } + } + ); + }); } -export async function setUpdatesChannel(guildId: string, channelId: string | null): Promise<[QueryError | null, boolean]> { - console.log("Setting updates channel", guildId, channelId); - return new Promise((resolve, reject) => { - pool.query( - ` +export async function setUpdatesChannel( + guildId: string, + channelId: string | null +): Promise<[QueryError | null, boolean]> { + console.log("Setting updates channel", guildId, channelId); + + return new Promise((resolve, reject) => { + pool.query( + ` UPDATE guilds SET updates_channel_id = ? WHERE id = ? `, - [ - channelId, - guildId, - ], - (err) => { - if (err) { - reject([err, false]); - } else { - resolve([null, true]); - } - }, - ); - }); + [channelId, guildId], + (err) => { + if (err) { + reject([err, false]); + } else { + resolve([null, true]); + } + } + ); + }); } -export async function getAllServersWithUpdatesEnabled(): Promise<[QueryError | null, Updates[]]> { - return new Promise((resolve, reject) => { - pool.query( - ` +export async function getAllServersWithUpdatesEnabled(): Promise< + [QueryError | null, Updates[]] +> { + return new Promise((resolve, reject) => { + pool.query( + ` SELECT id, updates_channel_id, updates_enabled FROM guilds WHERE updates_enabled = TRUE `, - (err, results) => { - if (err) { - reject([err, []]); - } else { - resolve([null, results as Updates[]]); - } - }, - ); - }); -} \ No newline at end of file + (err, results) => { + if (err) { + reject([err, []]); + } else { + resolve([null, results as Updates[]]); + } + } + ); + }); +} diff --git a/api/src/db/queries/users.ts b/api/src/db/queries/users.ts index c521456..fb2893a 100644 --- a/api/src/db/queries/users.ts +++ b/api/src/db/queries/users.ts @@ -1,91 +1,142 @@ import type { QueryError } from "mysql2"; + import { pool } from ".."; export interface User { - id: string; - guild_id: string; - name: string; - nickname: string; - pfp: string; - xp: number; - level: number; - xp_needed_next_level: number; - progress_next_level: number; + id: string; + guild_id: string; + name: string; + nickname: string; + pfp: string; + xp: number; + level: number; + xp_needed_next_level: number; + progress_next_level: number; } -export async function getUsers(guildId: string): Promise<[QueryError, null] | [null, User[]]> { - return new Promise((resolve, reject) => { - pool.query("SELECT * FROM users WHERE guild_id = ? AND user_is_in_guild = ? ORDER BY xp DESC", [guildId, true], (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, (results as User[])]); - } - }); - }); +export async function getUsers( + guildId: string +): Promise<[QueryError, null] | [null, User[]]> { + return new Promise((resolve, reject) => { + pool.query( + "SELECT * FROM users WHERE guild_id = ? AND user_is_in_guild = ? ORDER BY xp DESC", + [guildId, true], + (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, results as User[]]); + } + } + ); + }); } -export async function getUser(userId: string, guildId: string): Promise<[QueryError, null] | [null, User | null]> { - return new Promise((resolve, reject) => { - pool.query("SELECT * FROM users WHERE id = ? AND guild_id = ?", [userId, guildId], (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, (results as User[])[0]]); - } - }); - }); +export async function getUser( + userId: string, + guildId: string +): Promise<[QueryError, null] | [null, User | null]> { + return new Promise((resolve, reject) => { + pool.query( + "SELECT * FROM users WHERE id = ? AND guild_id = ?", + [userId, guildId], + (err, results) => { + if (err) { + reject([err, null]); + } else { + resolve([null, (results as User[])[0]]); + } + } + ); + }); } -export async function removeUser(userId: string, guildId: string): Promise<[QueryError | null, boolean]> { - return new Promise((resolve, reject) => { - pool.query("UPDATE users SET user_is_in_guild = ? WHERE id = ? AND guild_id = ?", [false, userId, guildId], (err) => { - if (err) { - reject([err, false]); - } else { - resolve([null, true]); - } - }); - }); - +export async function removeUser( + userId: string, + guildId: string +): Promise<[QueryError | null, boolean]> { + return new Promise((resolve, reject) => { + pool.query( + "UPDATE users SET user_is_in_guild = ? WHERE id = ? AND guild_id = ?", + [false, userId, guildId], + (err) => { + if (err) { + reject([err, false]); + } else { + resolve([null, true]); + } + } + ); + }); } -export async function setXP(guildId: string, userId: string, xp: number): Promise<[QueryError | null, boolean]> { - const newLevel = Math.floor(Math.sqrt(xp / 100)); - const nextLevel = newLevel + 1; - const nextLevelXp = Math.pow(nextLevel, 2) * 100; - const xpNeededForNextLevel = nextLevelXp - xp; - const currentLevelXp = Math.pow(newLevel, 2) * 100; - const progressToNextLevel = - ((xp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; +export async function setXP( + guildId: string, + userId: string, + xp: number +): Promise<[QueryError | null, boolean]> { + const newLevel = Math.floor(Math.sqrt(xp / 100)); + const nextLevel = newLevel + 1; + const nextLevelXp = Math.pow(nextLevel, 2) * 100; + const xpNeededForNextLevel = nextLevelXp - xp; + const currentLevelXp = Math.pow(newLevel, 2) * 100; + const progressToNextLevel = + ((xp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; - return new Promise((resolve, reject) => { - pool.query("UPDATE users SET xp = ?, level = ?, xp_needed_next_level = ?, progress_next_level = ?, user_is_in_guild = ? WHERE id = ? AND guild_id = ?", [xp, newLevel, xpNeededForNextLevel.toFixed(2), progressToNextLevel.toFixed(2), true, userId, guildId], (err) => { - if (err) { - reject([err, false]); - } else { - resolve([null, true]); - } - }); - }); + return new Promise((resolve, reject) => { + pool.query( + "UPDATE users SET xp = ?, level = ?, xp_needed_next_level = ?, progress_next_level = ?, user_is_in_guild = ? WHERE id = ? AND guild_id = ?", + [ + xp, + newLevel, + xpNeededForNextLevel.toFixed(2), + progressToNextLevel.toFixed(2), + true, + userId, + guildId, + ], + (err) => { + if (err) { + reject([err, false]); + } else { + resolve([null, true]); + } + } + ); + }); } -export async function setLevel(guildId: string, userId: string, level: number): Promise<[QueryError | null, boolean]> { - const newXp = Math.pow(level, 2) * 100; - const nextLevel = level + 1; - const nextLevelXp = Math.pow(nextLevel, 2) * 100; - const xpNeededForNextLevel = nextLevelXp - newXp; - const currentLevelXp = Math.pow(level, 2) * 100; - const progressToNextLevel = - ((newXp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; +export async function setLevel( + guildId: string, + userId: string, + level: number +): Promise<[QueryError | null, boolean]> { + const newXp = Math.pow(level, 2) * 100; + const nextLevel = level + 1; + const nextLevelXp = Math.pow(nextLevel, 2) * 100; + const xpNeededForNextLevel = nextLevelXp - newXp; + const currentLevelXp = Math.pow(level, 2) * 100; + const progressToNextLevel = + ((newXp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; - return new Promise((resolve, reject) => { - pool.query("UPDATE users SET xp = ?, level = ?, xp_needed_next_level = ?, progress_next_level = ? WHERE id = ? AND guild_id = ?", [newXp, level, xpNeededForNextLevel.toFixed(2), progressToNextLevel.toFixed(2), userId, guildId], (err) => { - if (err) { - reject([err, false]); - } else { - resolve([null, true]); - } - }); - }); + return new Promise((resolve, reject) => { + pool.query( + "UPDATE users SET xp = ?, level = ?, xp_needed_next_level = ?, progress_next_level = ? WHERE id = ? AND guild_id = ?", + [ + newXp, + level, + xpNeededForNextLevel.toFixed(2), + progressToNextLevel.toFixed(2), + userId, + guildId, + ], + (err) => { + if (err) { + reject([err, false]); + } else { + resolve([null, true]); + } + } + ); + }); } diff --git a/api/src/index.ts b/api/src/index.ts index 5a798ee..c579d15 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,6 +1,31 @@ -import express, { type NextFunction, type Request, type Response } from "express"; +import express, { + type NextFunction, + type Request, + type Response, +} from "express"; import cors from "cors"; -import { getBotInfo, getGuild, getUser, getUsers, initTables, pool, updateGuild, enableUpdates, disableUpdates, setCooldown, setUpdatesChannel, setXP, setLevel, removeGuild, removeUser, getAllServersWithUpdatesEnabled, addUserToTrackingData, getGuildTrackingData, getUsersTrackingData } from "./db"; + +import { + getBotInfo, + getGuild, + getUser, + getUsers, + initTables, + pool, + updateGuild, + enableUpdates, + disableUpdates, + setCooldown, + setUpdatesChannel, + setXP, + setLevel, + removeGuild, + removeUser, + getAllServersWithUpdatesEnabled, + addUserToTrackingData, + getGuildTrackingData, + getUsersTrackingData, +} from "./db"; const app = express(); const PORT = 18103; @@ -15,62 +40,64 @@ await initTables(); console.log("Tables initialized"); function authMiddleware(req: Request, res: Response, next: NextFunction) { - if (!req.headers.authorization || req.headers.authorization !== process.env.AUTH) { - return res - .status(403) - .json({ message: "Access denied" }); - } - next(); + if ( + !req.headers.authorization || + req.headers.authorization !== process.env.AUTH + ) { + return res.status(403).json({ message: "Access denied" }); + } + next(); } app.post("/post/:guild", authMiddleware, async (req, res) => { - const { guild } = req.params; - const { name, icon, members } = req.body; - - const [err, results] = await updateGuild({ - id: guild, - name, - icon, - members, - }); - - if (err) { - res.status(500).json({ message: "Internal server error" }); - } else { - res.status(200).json(results); - } + const { guild } = req.params; + const { name, icon, members } = req.body; + + const [err, results] = await updateGuild({ + id: guild, + name, + icon, + members, + }); + + if (err) { + res.status(500).json({ message: "Internal server error" }); + } else { + res.status(200).json(results); + } }); -app.post('/post/:guild/remove', authMiddleware, async (req, res) => { - const { guild } = req.params; - const [err, results] = await removeGuild(guild); +app.post("/post/:guild/remove", authMiddleware, async (req, res) => { + const { guild } = req.params; + const [err, results] = await removeGuild(guild); - if (err) { - res.status(500).json({ message: "Internal server error" }); - } else { - res.status(200).json(results); - } -}) + if (err) { + res.status(500).json({ message: "Internal server error" }); + } else { + res.status(200).json(results); + } +}); -app.post('/post/:guild/:user/remove', authMiddleware, async (req, res) => { - const { guild, user } = req.params; - const [err, results] = await removeUser(user, guild); +app.post("/post/:guild/:user/remove", authMiddleware, async (req, res) => { + const { guild, user } = req.params; + const [err, results] = await removeUser(user, guild); - if (err) { - res.status(500).json({ message: "Internal server error" }); - } else { - res.status(200).json(results); - } -}) + if (err) { + res.status(500).json({ message: "Internal server error" }); + } else { + res.status(200).json(results); + } +}); app.post("/post/:guild/:user", authMiddleware, async (req, res) => { - const { guild, user } = req.params; - const { name, pfp, xp, nickname } = req.body; - console.log(req.body); - const xpValue = parseInt(xp); + const { guild, user } = req.params; + const { name, pfp, xp, nickname } = req.body; - if (xpValue == 0) { - const updateQuery = ` + console.log(req.body); + const xpValue = parseInt(xp); + + if (xpValue == 0) { + const updateQuery = ` INSERT INTO users (id, guild_id, pfp, name, nickname) VALUES (?, ?, ?, ?, ?) @@ -80,53 +107,42 @@ app.post("/post/:guild/:user", authMiddleware, async (req, res) => { nickname = VALUES(nickname) `; - pool.query( - updateQuery, - [ - user, - guild, - pfp, - name, - nickname, - ], - (err) => { - if (err) { - console.error("Error updating XP:", err); - return res - .status(500) - .json({ success: false, message: "Internal server error" }); - } else { - res - .status(200) - .json({ - success: true - }); - } - }, - ); - } - - const [err, result] = await getUser(user, guild); - - if (err) { - console.error("Error fetching XP:", err); - return res.status(500).json({ message: "Internal server error" }); - } - - - const currentXp = result?.xp ?? 0; - const currentLevelSaved = result?.level ?? 0; - const newXp = currentXp + xpValue; - - const currentLevel = Math.floor(Math.sqrt(newXp / 100)); - const nextLevel = currentLevel + 1; - const nextLevelXp = Math.pow(nextLevel, 2) * 100; - const xpNeededForNextLevel = nextLevelXp - newXp; - const currentLevelXp = Math.pow(currentLevel, 2) * 100; - const progressToNextLevel = - ((newXp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; - - const updateQuery = ` + pool.query(updateQuery, [user, guild, pfp, name, nickname], (err) => { + if (err) { + console.error("Error updating XP:", err); + + return res + .status(500) + .json({ success: false, message: "Internal server error" }); + } else { + res.status(200).json({ + success: true, + }); + } + }); + } + + const [err, result] = await getUser(user, guild); + + if (err) { + console.error("Error fetching XP:", err); + + return res.status(500).json({ message: "Internal server error" }); + } + + const currentXp = result?.xp ?? 0; + const currentLevelSaved = result?.level ?? 0; + const newXp = currentXp + xpValue; + + const currentLevel = Math.floor(Math.sqrt(newXp / 100)); + const nextLevel = currentLevel + 1; + const nextLevelXp = Math.pow(nextLevel, 2) * 100; + const xpNeededForNextLevel = nextLevelXp - newXp; + const currentLevelXp = Math.pow(currentLevel, 2) * 100; + const progressToNextLevel = + ((newXp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; + + const updateQuery = ` INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -140,632 +156,822 @@ app.post("/post/:guild/:user", authMiddleware, async (req, res) => { progress_next_level = VALUES(progress_next_level) `; - pool.query( - updateQuery, - [ - user, - guild, - newXp, - pfp, - name, - nickname, - currentLevel, - xpNeededForNextLevel, - progressToNextLevel.toFixed(2), - ], - (err) => { - if (err) { - console.error("Error updating XP:", err); - return res - .status(500) - .json({ success: false, message: "Internal server error" }); - } else { - res - .status(200) - .json({ - success: true, - sendUpdateEvent: currentLevelSaved !== currentLevel, - level: currentLevel, - }); - } - }, - ); + pool.query( + updateQuery, + [ + user, + guild, + newXp, + pfp, + name, + nickname, + currentLevel, + xpNeededForNextLevel, + progressToNextLevel.toFixed(2), + ], + (err) => { + if (err) { + console.error("Error updating XP:", err); + + return res + .status(500) + .json({ success: false, message: "Internal server error" }); + } else { + res.status(200).json({ + success: true, + sendUpdateEvent: currentLevelSaved !== currentLevel, + level: currentLevel, + }); + } + } + ); }); -app.get('/get/botinfo', async (_req, res) => { - const [err, data] = await getBotInfo(); - if (err) { - console.error("Error fetching bot info:", err); - return res.status(500).json({ message: "Internal server error" }); - } - return res.status(200).json(data); +app.get("/get/botinfo", async (_req, res) => { + const [err, data] = await getBotInfo(); + + if (err) { + console.error("Error fetching bot info:", err); + + return res.status(500).json({ message: "Internal server error" }); + } + + return res.status(200).json(data); }); -app.get('/get/dbusage', (_req, res) => { - pool.query(`SELECT table_schema AS "name", SUM(data_length + index_length) / 1024 / 1024 AS "size" FROM information_schema.TABLES GROUP BY table_schema;`, (err, results) => { - if (err) { - console.error("Error fetching database size:", err); - return res.status(500).json({ message: "Internal server error" }); - } else { - const discordXpBot = results.find((result) => result.name === process.env.MYSQL_DATABASE); - if (discordXpBot) { - return res.status(200).json({ sizeInMB: parseFloat(discordXpBot.size) }); - } else { - return res.status(404).json({ message: "Database not found" }); - } - } - }) +app.get("/get/dbusage", (_req, res) => { + pool.query( + `SELECT table_schema AS "name", SUM(data_length + index_length) / 1024 / 1024 AS "size" FROM information_schema.TABLES GROUP BY table_schema;`, + (err, results) => { + if (err) { + console.error("Error fetching database size:", err); + + return res + .status(500) + .json({ message: "Internal server error" }); + } else { + const discordXpBot = results.find( + (result) => result.name === process.env.MYSQL_DATABASE + ); + + if (discordXpBot) { + return res + .status(200) + .json({ sizeInMB: parseFloat(discordXpBot.size) }); + } else { + return res + .status(404) + .json({ message: "Database not found" }); + } + } + } + ); }); -app.get('/get/tracking/:guild', async (req, res) => { - const { guild } = req.params; +app.get("/get/tracking/:guild", async (req, res) => { + const { guild } = req.params; + + const [err, data] = await getGuildTrackingData(guild, null); - const [err, data] = await getGuildTrackingData(guild, null); + if (err) { + console.error("Error fetching tracking data:", err); - if (err) { - console.error("Error fetching tracking data:", err); - return res.status(500).json({ message: "Internal server error" }); - } + return res.status(500).json({ message: "Internal server error" }); + } - return res.status(200).json(data); + return res.status(200).json(data); }); -app.get('/get/tracking/:guild/:user', async (req, res) => { - const { guild, user } = req.params; +app.get("/get/tracking/:guild/:user", async (req, res) => { + const { guild, user } = req.params; - const [err, data] = await getUsersTrackingData(user, guild); + const [err, data] = await getUsersTrackingData(user, guild); - if (err) { - console.error("Error fetching tracking data:", err); - return res.status(500).json({ message: "Internal server error" }); - } + if (err) { + console.error("Error fetching tracking data:", err); - return res.status(200).json(data); + return res.status(500).json({ message: "Internal server error" }); + } + + return res.status(200).json(data); }); app.get("/get/:guild/:user", async (req, res) => { - const { guild, user } = req.params; - - const [err, result] = await getUser(user, guild); - - if (err) { - console.error("Error fetching user:", err); - res.status(500).json({ message: "Internal server error" }); - } else if (result) { - res.status(200).json(result); - } else { - res.status(404).json({ message: "User not found" }); - } + const { guild, user } = req.params; + + const [err, result] = await getUser(user, guild); + + if (err) { + console.error("Error fetching user:", err); + res.status(500).json({ message: "Internal server error" }); + } else if (result) { + res.status(200).json(result); + } else { + res.status(404).json({ message: "User not found" }); + } }); app.get("/get/:guild", async (req, res) => { - const { guild } = req.params; - - const [guildErr, guildData] = await getGuild(guild); - const [usersErr, usersData] = await getUsers(guild); - - if (guildErr) { - console.error("Error fetching guild:", guildErr); - res.status(500).json({ message: "Internal server error" }); - } else if (usersErr) { - console.error("Error fetching users:", usersErr); - res.status(500).json({ message: "Internal server error" }); - } else if (!guildData) { - res.status(404).json({ message: "Guild not found" }); - } else { - const totalXp = usersData.reduce((sum, user) => sum + user.xp, 0); - res.status(200).json({ - guild: guildData, - leaderboard: usersData, - totalXp: totalXp, - }); - } + const { guild } = req.params; + + const [guildErr, guildData] = await getGuild(guild); + const [usersErr, usersData] = await getUsers(guild); + + if (guildErr) { + console.error("Error fetching guild:", guildErr); + res.status(500).json({ message: "Internal server error" }); + } else if (usersErr) { + console.error("Error fetching users:", usersErr); + res.status(500).json({ message: "Internal server error" }); + } else if (!guildData) { + res.status(404).json({ message: "Guild not found" }); + } else { + const totalXp = usersData.reduce((sum, user) => sum + user.xp, 0); + + res.status(200).json({ + guild: guildData, + leaderboard: usersData, + totalXp: totalXp, + }); + } }); app.post("/admin/:action/:guild/:target", authMiddleware, async (req, res) => { - const { guild, action, target } = req.params; - const { extraData } = req.body; - - switch (action) { - case "include": - // TODO: implement this - // target: channel id - // run function to include target to guild - break; - case "exclude": - // TODO: implement this - // target: channel id - // run function to exclude target from guild - break; - case "updates": - if (target !== "enable" && target !== "disable" && target !== "set" && target !== "get") { - return res.status(400).json({ message: "Illegal request" }); - } - - switch (target) { - case "enable": - try { - const [err, success] = await enableUpdates(guild); - if (err) { - return res.status(500).json({ message: "Internal server error", err }); - } else { - return res.status(200).json(success); - } - } catch (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - case "disable": - try { - const [err, success] = await disableUpdates(guild); - if (err) { - return res.status(500).json({ message: "Internal server error", err }); - } else { - return res.status(200).json(success); - } - } catch (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - case 'set': - if (!extraData || typeof extraData.channelId === "undefined") { - return res.status(400).json({ message: "Illegal request" }); - } - - try { - const [err, success] = await setUpdatesChannel(guild, extraData.channelId); - if (err) { - return res.status(500).json({ message: 'Internal server error', err }); - } else { - return res.status(200).json(success); - } - } catch (err) { - return res.status(500).json({ message: 'Internal server error', err }); - } - default: - if (guild == "all") { - try { - const [err, data] = await getAllServersWithUpdatesEnabled(); - if (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - return res.status(200).json(data); - } catch (error) { - return res.status(500).json({ message: "Internal server error" }); - } - } - try { - const [err, data] = await getGuild(guild); - if (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - return res.status(200).json({ - enabled: ((data?.updates_enabled ?? 1) === 1), - channel: data?.updates_channel_id ?? null, - }); - } catch (error) { - return res.status(500).json({ message: "Internal server error" }); - } - } - case "roles": - if (target !== "add" && target !== "remove" && target !== "get") { - return res.status(400).json({ message: "Illegal request" }); - } - - if ((target === "add" || target === "remove") && !extraData) { - return res.status(400).json({ message: "Illegal request" }); - } - - switch (target) { - case "get": - try { - const data = await adminRolesGet(guild); - return res.status(200).json(data); - } catch (error) { - return res.status(500).json({ message: "Internal server error" }); - } - case "remove": - try { - const data = await adminRolesRemove(guild, extraData.role); - return res.status(200).json(data); - } catch (error) { - return res.status(500).json({ message: "Internal server error" }); - } - case "add": - try { - const data = await adminRolesAdd( - guild, - extraData.role, - extraData.level, - ); - return res.status(200).json(data); - } catch (error) { - return res.status(500).json({ message: "Internal server error" }); - } - default: - return res.status(500).json({ message: "Internal server error" }); - } - case "cooldown": - if (target !== "set" && target !== "get") { - return res.status(400).json({ message: "Illegal request" }); - } - - if (target === "set" && !extraData) { - return res.status(400).json({ message: "Illegal request" }); - } - - switch (target) { - case "get": - try { - const [err, data] = await getGuild(guild); - if (err) { - return res.status(500).json({ message: "Internal server error" }); - } - return res.status(200).json({ cooldown: data?.cooldown ?? 30_000 }); - } catch (error) { - return res.status(500).json({ message: "Internal server error" }); - } - case "set": - try { - const data = await setCooldown(guild, extraData.cooldown); - return res.status(200).json(data); - } catch (error) { - return res.status(500).json({ message: "Internal server error" }); - } - default: - return res.status(500).json({ message: "Internal server error" }); - } - case "set": { - if (target !== "xp" && target !== "level") { - return res.status(400).json({ message: "Illegal request" }); - } - - if (!extraData || !extraData.user || !extraData.value) { - return res.status(400).json({ message: "Illegal request" }); - } - - switch (target) { - case "xp": - try { - const [err, success] = await setXP(guild, extraData.user, extraData.value); - if (err) { - return res.status(500).json({ message: "Internal server error", err }); - } else { - return res.status(200).json(success); - } - } catch (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - case "level": - try { - const [err, success] = await setLevel(guild, extraData.user, extraData.value); - if (err) { - return res.status(500).json({ message: "Internal server error", err }); - } else { - return res.status(200).json(success); - } - } catch (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - default: - return res.status(500).json({ message: "Internal server error" }); - } - } - case "sync": { - if (target !== "polaris" && target !== "mee6" && target !== "lurkr") { - return res.status(400).json({ message: "Illegal request" }); - } - - switch (target) { - case "polaris": { - try { - const [err, success] = await syncFromPolaris(guild); - if (err) { - if (err instanceof Error && err.message === "Server not found in Polaris") { - return res.status(404).json({ message: "Server not found in Polaris" }); - } - return res.status(500).json({ message: "Internal server error", err }); - } else { - return res.status(200).json(success); - } - } catch (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - } - case "mee6": { - try { - const [err, success] = await syncFromMee6(guild); - if (err) { - if (err instanceof Error && err.message === "Server not found in MEE6") { - return res.status(404).json({ message: "Server not found in MEE6" }); - } - return res.status(500).json({ message: "Internal server error", err }); - } else { - return res.status(200).json(success); - } - } catch (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - } - case "lurkr": { - try { - const [err, success] = await syncFromLurkr(guild); - if (err) { - if (err instanceof Error && err.message === "Server not found in Lurkr") { - return res.status(404).json({ message: "Server not found in Lurkr" }); - } - return res.status(500).json({ message: "Internal server error", err }); - } else { - return res.status(200).json(success); - } - } catch (err) { - return res.status(500).json({ message: "Internal server error", err }); - } - } - default: - return res.status(500).json({ message: "Internal server error" }); - } - } - case "tracking": { - await addUserToTrackingData(target, guild); - break; - } - default: - return res.status(400).json({ message: "Illegal request" }); - } + const { guild, action, target } = req.params; + const { extraData } = req.body; + + switch (action) { + case "include": + // TODO: implement this + // target: channel id + // run function to include target to guild + break; + case "exclude": + // TODO: implement this + // target: channel id + // run function to exclude target from guild + break; + case "updates": + if ( + target !== "enable" && + target !== "disable" && + target !== "set" && + target !== "get" + ) { + return res.status(400).json({ message: "Illegal request" }); + } + + switch (target) { + case "enable": + try { + const [err, success] = await enableUpdates(guild); + + if (err) { + return res.status(500).json({ + message: "Internal server error", + err, + }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res + .status(500) + .json({ message: "Internal server error", err }); + } + case "disable": + try { + const [err, success] = await disableUpdates(guild); + + if (err) { + return res.status(500).json({ + message: "Internal server error", + err, + }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res + .status(500) + .json({ message: "Internal server error", err }); + } + case "set": + if ( + !extraData || + typeof extraData.channelId === "undefined" + ) { + return res + .status(400) + .json({ message: "Illegal request" }); + } + + try { + const [err, success] = await setUpdatesChannel( + guild, + extraData.channelId + ); + + if (err) { + return res.status(500).json({ + message: "Internal server error", + err, + }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res + .status(500) + .json({ message: "Internal server error", err }); + } + default: + if (guild == "all") { + try { + const [err, data] = + await getAllServersWithUpdatesEnabled(); + + if (err) { + return res.status(500).json({ + message: "Internal server error", + err, + }); + } + + return res.status(200).json(data); + } catch (error) { + return res + .status(500) + .json({ message: "Internal server error" }); + } + } + try { + const [err, data] = await getGuild(guild); + + if (err) { + return res.status(500).json({ + message: "Internal server error", + err, + }); + } + + return res.status(200).json({ + enabled: (data?.updates_enabled ?? 1) === 1, + channel: data?.updates_channel_id ?? null, + }); + } catch (error) { + return res + .status(500) + .json({ message: "Internal server error" }); + } + } + case "roles": + if (target !== "add" && target !== "remove" && target !== "get") { + return res.status(400).json({ message: "Illegal request" }); + } + + if ((target === "add" || target === "remove") && !extraData) { + return res.status(400).json({ message: "Illegal request" }); + } + + switch (target) { + case "get": + try { + const data = await adminRolesGet(guild); + + return res.status(200).json(data); + } catch (error) { + return res + .status(500) + .json({ message: "Internal server error" }); + } + case "remove": + try { + const data = await adminRolesRemove( + guild, + extraData.role + ); + + return res.status(200).json(data); + } catch (error) { + return res + .status(500) + .json({ message: "Internal server error" }); + } + case "add": + try { + const data = await adminRolesAdd( + guild, + extraData.role, + extraData.level + ); + + return res.status(200).json(data); + } catch (error) { + return res + .status(500) + .json({ message: "Internal server error" }); + } + default: + return res + .status(500) + .json({ message: "Internal server error" }); + } + case "cooldown": + if (target !== "set" && target !== "get") { + return res.status(400).json({ message: "Illegal request" }); + } + + if (target === "set" && !extraData) { + return res.status(400).json({ message: "Illegal request" }); + } + + switch (target) { + case "get": + try { + const [err, data] = await getGuild(guild); + + if (err) { + return res + .status(500) + .json({ message: "Internal server error" }); + } + + return res + .status(200) + .json({ cooldown: data?.cooldown ?? 30_000 }); + } catch (error) { + return res + .status(500) + .json({ message: "Internal server error" }); + } + case "set": + try { + const data = await setCooldown( + guild, + extraData.cooldown + ); + + return res.status(200).json(data); + } catch (error) { + return res + .status(500) + .json({ message: "Internal server error" }); + } + default: + return res + .status(500) + .json({ message: "Internal server error" }); + } + case "set": { + if (target !== "xp" && target !== "level") { + return res.status(400).json({ message: "Illegal request" }); + } + + if (!extraData || !extraData.user || !extraData.value) { + return res.status(400).json({ message: "Illegal request" }); + } + + switch (target) { + case "xp": + try { + const [err, success] = await setXP( + guild, + extraData.user, + extraData.value + ); + + if (err) { + return res.status(500).json({ + message: "Internal server error", + err, + }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res + .status(500) + .json({ message: "Internal server error", err }); + } + case "level": + try { + const [err, success] = await setLevel( + guild, + extraData.user, + extraData.value + ); + + if (err) { + return res.status(500).json({ + message: "Internal server error", + err, + }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res + .status(500) + .json({ message: "Internal server error", err }); + } + default: + return res + .status(500) + .json({ message: "Internal server error" }); + } + } + case "sync": { + if ( + target !== "polaris" && + target !== "mee6" && + target !== "lurkr" + ) { + return res.status(400).json({ message: "Illegal request" }); + } + + switch (target) { + case "polaris": { + try { + const [err, success] = await syncFromPolaris(guild); + + if (err) { + if ( + err instanceof Error && + err.message === "Server not found in Polaris" + ) { + return res.status(404).json({ + message: "Server not found in Polaris", + }); + } + + return res.status(500).json({ + message: "Internal server error", + err, + }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res + .status(500) + .json({ message: "Internal server error", err }); + } + } + case "mee6": { + try { + const [err, success] = await syncFromMee6(guild); + + if (err) { + if ( + err instanceof Error && + err.message === "Server not found in MEE6" + ) { + return res.status(404).json({ + message: "Server not found in MEE6", + }); + } + + return res.status(500).json({ + message: "Internal server error", + err, + }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res + .status(500) + .json({ message: "Internal server error", err }); + } + } + case "lurkr": { + try { + const [err, success] = await syncFromLurkr(guild); + + if (err) { + if ( + err instanceof Error && + err.message === "Server not found in Lurkr" + ) { + return res.status(404).json({ + message: "Server not found in Lurkr", + }); + } + + return res.status(500).json({ + message: "Internal server error", + err, + }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res + .status(500) + .json({ message: "Internal server error", err }); + } + } + default: + return res + .status(500) + .json({ message: "Internal server error" }); + } + } + case "tracking": { + await addUserToTrackingData(target, guild); + break; + } + default: + return res.status(400).json({ message: "Illegal request" }); + } }); -app.get("/invite", (_req, res) => res.status(308).redirect("https://discord.com/oauth2/authorize?client_id=1245807579624378601&permissions=1099780115520&integration_type=0&scope=bot+applications.commands")); +app.get("/invite", (_req, res) => + res + .status(308) + .redirect( + "https://discord.com/oauth2/authorize?client_id=1245807579624378601&permissions=1099780115520&integration_type=0&scope=bot+applications.commands" + ) +); -app.get('/support', (_req, res) => res.status(308).redirect('https://discord.gg/fpJVTkVngm')); +app.get("/support", (_req, res) => + res.status(308).redirect("https://discord.gg/fpJVTkVngm") +); app.use((_req, res) => { - res.status(404).send() + res.status(404).send(); }); app.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); + console.log(`Server running on http://localhost:${PORT}`); }); // TODO: actually implement this in a real way //#region Admin: Roles async function adminRolesGet(guild: string) { - const selectRolesQuery = `SELECT id, level FROM roles WHERE guild_id = ?`; - - return new Promise((resolve, reject) => { - pool.query(selectRolesQuery, [guild], (err, results) => { - if (err) { - console.error("Error fetching roles:", err); - reject(err); - } else { - resolve(results); - } - }); - }); + const selectRolesQuery = `SELECT id, level FROM roles WHERE guild_id = ?`; + + return new Promise((resolve, reject) => { + pool.query(selectRolesQuery, [guild], (err, results) => { + if (err) { + console.error("Error fetching roles:", err); + reject(err); + } else { + resolve(results); + } + }); + }); } async function adminRolesRemove(guild: string, role: string) { - const deleteRoleQuery = ` + const deleteRoleQuery = ` DELETE FROM roles WHERE id = ? AND guild_id = ? `; - return new Promise((resolve, reject) => { - pool.query(deleteRoleQuery, [role, guild], (err, results) => { - if (err) { - console.error("Error removing role:", err); - reject(err); - } else { - resolve(results); - } - }); - }); + return new Promise((resolve, reject) => { + pool.query(deleteRoleQuery, [role, guild], (err, results) => { + if (err) { + console.error("Error removing role:", err); + reject(err); + } else { + resolve(results); + } + }); + }); } async function adminRolesAdd(guild: string, role: string, level: number) { - const insertRoleQuery = ` + const insertRoleQuery = ` INSERT INTO roles (id, guild_id, level) VALUES (?, ?, ?) `; - return new Promise((resolve, reject) => { - pool.query(insertRoleQuery, [role, guild, level], (err, results) => { - if (err) { - console.error("Error adding role:", err); - reject(err); - } else { - resolve(results); - } - }); - }); + return new Promise((resolve, reject) => { + pool.query(insertRoleQuery, [role, guild, level], (err, results) => { + if (err) { + console.error("Error adding role:", err); + reject(err); + } else { + resolve(results); + } + }); + }); } //#endregion //#region Syncing async function syncFromPolaris(guild: string) { - const res = await fetch(`https://gdcolon.com/polaris/api/leaderboard/${guild}`); - const data = await res.json(); - if (data.apiError && data.code === "invalidServer") { - return [new Error("Server not found in Polaris"), false]; - } - const users = data.leaderboard; - for (let i = 1; i < data.pageInfo.pageCount; i++) { - const res = await fetch(`https://gdcolon.com/polaris/api/leaderboard/${guild}?page=${i + 1}`); - const data = await res.json(); - users.push(...data.leaderboard); - } - - if (users.length === 0) { - return [new Error("No users found"), false]; - } - - try { - for (const user of users) { - const xpValue = user.xp; - const level = Math.floor(Math.sqrt(xpValue / 100)); - const nextLevel = level + 1; - const nextLevelXp = Math.pow(nextLevel, 2) * 100; - const xpNeededForNextLevel = nextLevelXp - xpValue; - const currentLevelXp = Math.pow(level, 2) * 100; - const progressToNextLevel = - ((xpValue - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; - - await new Promise((resolve, reject) => { - pool.query( - `INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - user.id, - guild, - xpValue, - user.avatar, - user.username, - user.nickname ?? user.displayName, - level, - xpNeededForNextLevel, - progressToNextLevel.toFixed(2), - ], - (err) => { - if (err) { - console.error("Error syncing from Polaris:", err); - reject(err); - } else { - resolve(null); - } - }, - ); - }); - } - return [null, true] - } catch (err) { - return [err, false]; - } - + const res = await fetch( + `https://gdcolon.com/polaris/api/leaderboard/${guild}` + ); + const data = await res.json(); + + if (data.apiError && data.code === "invalidServer") { + return [new Error("Server not found in Polaris"), false]; + } + const users = data.leaderboard; + + for (let i = 1; i < data.pageInfo.pageCount; i++) { + const res = await fetch( + `https://gdcolon.com/polaris/api/leaderboard/${guild}?page=${i + 1}` + ); + const data = await res.json(); + + users.push(...data.leaderboard); + } + + if (users.length === 0) { + return [new Error("No users found"), false]; + } + + try { + for (const user of users) { + const xpValue = user.xp; + const level = Math.floor(Math.sqrt(xpValue / 100)); + const nextLevel = level + 1; + const nextLevelXp = Math.pow(nextLevel, 2) * 100; + const xpNeededForNextLevel = nextLevelXp - xpValue; + const currentLevelXp = Math.pow(level, 2) * 100; + const progressToNextLevel = + ((xpValue - currentLevelXp) / (nextLevelXp - currentLevelXp)) * + 100; + + await new Promise((resolve, reject) => { + pool.query( + `INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + user.id, + guild, + xpValue, + user.avatar, + user.username, + user.nickname ?? user.displayName, + level, + xpNeededForNextLevel, + progressToNextLevel.toFixed(2), + ], + (err) => { + if (err) { + console.error("Error syncing from Polaris:", err); + reject(err); + } else { + resolve(null); + } + } + ); + }); + } + + return [null, true]; + } catch (err) { + return [err, false]; + } } async function syncFromMee6(guild: string) { - const res = await fetch(`https://mee6.xyz/api/plugins/levels/leaderboard/${guild}?limit=1000&page=0`); - const data = await res.json(); - if (data.status_code === 404) { - return [new Error("Server not found in MEE6"), false]; - } - const users = data.players; - let pageNumber = 1; - // this is needed because MEE6 doesn't give us the total amount of pages - // eslint-disable-next-line no-constant-condition - while (true) { - const res = await fetch(`https://mee6.xyz/api/plugins/levels/leaderboard/${guild}?limit=1000&page=${pageNumber}`); - const data = await res.json(); - users.push(...data.players); - if (data.players.length < 1000) break; - pageNumber += 1; - } - - if (users.length === 0) { - return [new Error("No users found"), false]; - } - - try { - for (const user of users) { - const xpValue = user.xp; - const level = Math.floor(Math.sqrt(xpValue / 100)); - const nextLevel = level + 1; - const nextLevelXp = Math.pow(nextLevel, 2) * 100; - const xpNeededForNextLevel = nextLevelXp - xpValue; - const currentLevelXp = Math.pow(level, 2) * 100; - const progressToNextLevel = - ((xpValue - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; - - await new Promise((resolve, reject) => { - pool.query( - `INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - user.id, - guild, - xpValue, - `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp`, - user.username, - user.username, - level, - xpNeededForNextLevel, - progressToNextLevel.toFixed(2), - ], - (err) => { - if (err) { - console.error("Error syncing from MEE6:", err); - reject(err); - } else { - resolve(null); - } - }, - ); - }); - } - return [null, true] - } catch (err) { - return [err, false]; - } + const res = await fetch( + `https://mee6.xyz/api/plugins/levels/leaderboard/${guild}?limit=1000&page=0` + ); + const data = await res.json(); + + if (data.status_code === 404) { + return [new Error("Server not found in MEE6"), false]; + } + const users = data.players; + let pageNumber = 1; + + // this is needed because MEE6 doesn't give us the total amount of pages + // eslint-disable-next-line no-constant-condition + while (true) { + const res = await fetch( + `https://mee6.xyz/api/plugins/levels/leaderboard/${guild}?limit=1000&page=${pageNumber}` + ); + const data = await res.json(); + + users.push(...data.players); + if (data.players.length < 1000) break; + pageNumber += 1; + } + + if (users.length === 0) { + return [new Error("No users found"), false]; + } + + try { + for (const user of users) { + const xpValue = user.xp; + const level = Math.floor(Math.sqrt(xpValue / 100)); + const nextLevel = level + 1; + const nextLevelXp = Math.pow(nextLevel, 2) * 100; + const xpNeededForNextLevel = nextLevelXp - xpValue; + const currentLevelXp = Math.pow(level, 2) * 100; + const progressToNextLevel = + ((xpValue - currentLevelXp) / (nextLevelXp - currentLevelXp)) * + 100; + + await new Promise((resolve, reject) => { + pool.query( + `INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + user.id, + guild, + xpValue, + `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp`, + user.username, + user.username, + level, + xpNeededForNextLevel, + progressToNextLevel.toFixed(2), + ], + (err) => { + if (err) { + console.error("Error syncing from MEE6:", err); + reject(err); + } else { + resolve(null); + } + } + ); + }); + } + + return [null, true]; + } catch (err) { + return [err, false]; + } } async function syncFromLurkr(guild: string) { - const res = await fetch(`https://api.lurkr.gg/v2/levels/${guild}?page=1`); - const data = await res.json(); - if (data.message === "Guild no found") { - return [new Error("Server not found in Lurkr"), false]; - } - const users = data.levels; - - if (users.length === 0) { - return [new Error("No users found"), false]; - } - - let pageNumber = 2; - // this is needed because Lurkr doesn't give us the total amount of pages - // eslint-disable-next-line no-constant-condition - while (true) { - const res = await fetch(`https://api.lurkr.gg/v2/levels/${guild}?page=${pageNumber}`); - const data = await res.json(); - users.push(...data.levels); - if (data.levels.length < 100) break; - pageNumber += 1; - } - - try { - for (const user of users) { - const xpValue = user.xp; - const level = Math.floor(Math.sqrt(user.xp / 100)); - const nextLevel = level + 1; - const nextLevelXp = Math.pow(nextLevel, 2) * 100; - const xpNeededForNextLevel = nextLevelXp - user.xp; - const currentLevelXp = Math.pow(level, 2) * 100; - const progressToNextLevel = - ((user.xp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100; - - await new Promise((resolve, reject) => { - pool.query( - `INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - user.userId, - guild, - xpValue, - `https://cdn.discordapp.com/avatars/${user.userId}/${user.user.avatar}.webp`, - user.user.username, - user.user.username, - level, - xpNeededForNextLevel, - progressToNextLevel.toFixed(2), - ], - (err) => { - if (err) { - console.error("Error syncing from Lurkr:", err); - reject(err); - } else { - resolve(null); - } - }, - ); - }); - } - return [null, true] - } catch (err) { - return [err, false]; - } + const res = await fetch(`https://api.lurkr.gg/v2/levels/${guild}?page=1`); + const data = await res.json(); + + if (data.message === "Guild no found") { + return [new Error("Server not found in Lurkr"), false]; + } + const users = data.levels; + + if (users.length === 0) { + return [new Error("No users found"), false]; + } + + let pageNumber = 2; + + // this is needed because Lurkr doesn't give us the total amount of pages + // eslint-disable-next-line no-constant-condition + while (true) { + const res = await fetch( + `https://api.lurkr.gg/v2/levels/${guild}?page=${pageNumber}` + ); + const data = await res.json(); + + users.push(...data.levels); + if (data.levels.length < 100) break; + pageNumber += 1; + } + + try { + for (const user of users) { + const xpValue = user.xp; + const level = Math.floor(Math.sqrt(user.xp / 100)); + const nextLevel = level + 1; + const nextLevelXp = Math.pow(nextLevel, 2) * 100; + const xpNeededForNextLevel = nextLevelXp - user.xp; + const currentLevelXp = Math.pow(level, 2) * 100; + const progressToNextLevel = + ((user.xp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * + 100; + + await new Promise((resolve, reject) => { + pool.query( + `INSERT INTO users (id, guild_id, xp, pfp, name, nickname, level, xp_needed_next_level, progress_next_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + user.userId, + guild, + xpValue, + `https://cdn.discordapp.com/avatars/${user.userId}/${user.user.avatar}.webp`, + user.user.username, + user.user.username, + level, + xpNeededForNextLevel, + progressToNextLevel.toFixed(2), + ], + (err) => { + if (err) { + console.error("Error syncing from Lurkr:", err); + reject(err); + } else { + resolve(null); + } + } + ); + }); + } + + return [null, true]; + } catch (err) { + return [err, false]; + } } //#endregion diff --git a/bot/package.json b/bot/package.json index fc76c1f..aa888cf 100644 --- a/bot/package.json +++ b/bot/package.json @@ -1,21 +1,21 @@ { - "name": "@chatr/bot", - "type": "module", - "version": "0.1.0", - "scripts": { - "dev": "bun with-env bun --watch src/index.ts --dev", - "with-env": "dotenv -e ../.env --" - }, - "dependencies": { - "canvacord": "^6.0.2", - "colorthief": "^2.4.0", - "cron": "^3.1.7", - "discord.js": "^14.15.3" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5.0.0" - } + "name": "@chatr/bot", + "type": "module", + "version": "0.1.0", + "scripts": { + "dev": "bun with-env bun --watch src/index.ts --dev", + "with-env": "dotenv -e ../.env --" + }, + "dependencies": { + "canvacord": "^6.0.2", + "colorthief": "^2.4.0", + "cron": "^3.1.7", + "discord.js": "^14.15.3" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } } diff --git a/bot/src/commands.ts b/bot/src/commands.ts index 7112ddb..22f9ced 100644 --- a/bot/src/commands.ts +++ b/bot/src/commands.ts @@ -1,764 +1,1084 @@ -import client from '.'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type CommandInteraction, ChannelType, type APIApplicationCommandOption, GuildMember, AttachmentBuilder, ComponentType } from 'discord.js'; -import { heapStats } from 'bun:jsc'; -import { getGuildLeaderboard, makeGETRequest, getRoles, removeRole, addRole, enableUpdates, disableUpdates, getCooldown, setCooldown, getUpdatesChannel, setUpdatesChannel, setXP, setLevel, syncFromBot, getDBSize } from './utils/requestAPI'; -import convertToLevels from './utils/convertToLevels'; -import quickEmbed from './utils/quickEmbed'; -import { Font, RankCardBuilder } from 'canvacord'; -import leaderboardEmbed from './utils/leaderboardEmbed'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type CommandInteraction, + ChannelType, + type APIApplicationCommandOption, + GuildMember, + AttachmentBuilder, + ComponentType, +} from "discord.js"; +import { heapStats } from "bun:jsc"; +import { Font, RankCardBuilder } from "canvacord"; + +import { + getGuildLeaderboard, + makeGETRequest, + getRoles, + removeRole, + addRole, + enableUpdates, + disableUpdates, + getCooldown, + setCooldown, + getUpdatesChannel, + setUpdatesChannel, + setXP, + setLevel, + syncFromBot, + getDBSize, +} from "./utils/requestAPI"; +import convertToLevels from "./utils/convertToLevels"; +import quickEmbed from "./utils/quickEmbed"; +import leaderboardEmbed from "./utils/leaderboardEmbed"; + +import client from "."; Font.loadDefault(); interface Command { - data: { - options: APIApplicationCommandOption[]; - name: string; - description: string; - integration_types: number[]; - contexts: number[]; - }; - execute: (interaction: CommandInteraction) => Promise; + data: { + options: APIApplicationCommandOption[]; + name: string; + description: string; + integration_types: number[]; + contexts: number[]; + }; + execute: (interaction: CommandInteraction) => Promise; } const commands: Record = { - ping: { - data: { - options: [], - name: 'ping', - description: 'Check the ping of the bot!', - integration_types: [0, 1], - contexts: [0, 1, 2], - }, - execute: async (interaction) => { - await interaction - .reply({ - ephemeral: false, - content: `Ping: ${interaction.client.ws.ping}ms`, - }) - .catch(console.error); - }, - }, - help: { - data: { - options: [], - name: 'help', - description: 'Get help on what each command does!', - integration_types: [0, 1], - contexts: [0, 1, 2], - }, - execute: async (interaction) => { - await client.application?.commands?.fetch().catch(console.error); - const chat_commands = client.application?.commands.cache.map((a) => { - return `: ${a.description}`; - }); - await interaction - .reply({ - ephemeral: true, - content: `Commands:\n${chat_commands?.join('\n')}`, - }) - .catch(console.error); - }, - }, - sourcecode: { - data: { - options: [], - name: 'sourcecode', - description: "Get the link of the app's source code.", - integration_types: [0, 1], - contexts: [0, 1, 2], - }, - execute: async (interaction) => { - await interaction - .reply({ - ephemeral: true, - content: `[Github repository](https://github.com/GalvinPython/chatr)`, - }) - .catch(console.error); - }, - }, - uptime: { - data: { - options: [], - name: 'uptime', - description: 'Check the uptime of the bot!', - integration_types: [0, 1], - contexts: [0, 1, 2], - }, - execute: async (interaction) => { - await interaction - .reply({ - ephemeral: false, - content: `Uptime: ${(performance.now() / (86400 * 1000)).toFixed( - 2, - )} days`, - }) - .catch(console.error); - }, - }, - usage: { - data: { - options: [], - name: 'usage', - description: 'Check the heap size and disk usage of the bot!', - integration_types: [0, 1], - contexts: [0, 1, 2], - }, - execute: async (interaction) => { - const heap = heapStats(); - Bun.gc(false); - const dbSize = await getDBSize(); - await interaction - .reply({ - ephemeral: false, - content: [ - `Heap size: ${(heap.heapSize / 1024 / 1024).toFixed(2)} MB / ${( - heap.heapCapacity / - 1024 / - 1024 - ).toFixed(2)} MB (${(heap.extraMemorySize / 1024 / 1024).toFixed(2,)} MB) (${heap.objectCount.toLocaleString()} objects, ${heap.protectedObjectCount.toLocaleString()} protected-objects)`, - `Disk usage: ${dbSize.sizeInMB.toFixed(2)} MB`, - ] - .join('\n') - .slice(0, 2000), - }) - .catch(console.error); - }, - }, - xp: { - data: { - options: [{ - name: 'user', - description: 'The user you want to check the XP of.', - type: 6, - required: false, - }], - name: 'xp', - description: 'Get your XP and Points', - integration_types: [0], - contexts: [0, 2], - }, - execute: async (interaction) => { - await interaction.deferReply() - - const optionUser = interaction.options.get('user')?.value as string | null; - const member = (optionUser ? interaction.guild!.members.cache.get(optionUser) : interaction.member) as GuildMember; - await interaction.guild!.members.fetch({ user: member.id, force: true }) - const guild = interaction.guild?.id - const user = member.id; - const leaderboard = await getGuildLeaderboard(guild as string); - const xp = await makeGETRequest(guild as string, user) - - if (!xp || leaderboard.length === 0) { - await interaction.followUp({ - ephemeral: true, - content: "No XP data available." - }); - return; - } - - const rank = leaderboard.leaderboard.findIndex((entry: ({ id: string; })) => entry.id === user) + 1; - - const card = new RankCardBuilder() - .setDisplayName(member.displayName) - .setAvatar(member.displayAvatarURL({ forceStatic: true, size: 4096 })) // user avatar - .setCurrentXP(xp.xp) // current xp - .setRequiredXP(xp.xp + xp.xp_needed_next_level) // required xp - .setLevel(xp.level) // user level - .setRank(rank) // user rank - .setOverlay(member.user.banner ? 95 : 90) // overlay percentage. Overlay is a semi-transparent layer on top of the background - .setBackground(member.user.bannerURL({ forceStatic: true, size: 4096 }) ?? "#23272a") - - if (interaction.user.discriminator !== "0") { - card.setUsername("#" + member.user.discriminator) - } else { - card.setUsername("@" + member.user.username) - } - - const color = member.roles.highest.hexColor ?? "#ffffff" - - card.setStyles({ - progressbar: { - thumb: { - style: { - backgroundColor: color - } - } - }, - }) - - const image = await card.build({ - format: "png" - }); - const attachment = new AttachmentBuilder(image, { name: `${user}.png` }); - - const msg = await interaction.followUp({ - files: [attachment], - components: [ - new ActionRowBuilder().setComponents( - new ButtonBuilder() - .setCustomId("text-mode") - .setLabel("Use text mode") - .setStyle(ButtonStyle.Secondary) - ) - ], - fetchReply: true - }); - - const collector = msg.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 60 * 1000 - }); - - collector.on("collect", async (i) => { - if (i.user.id !== user) - return i.reply({ - content: "You're not the one who initialized this message! Try running /xp on your own.", - ephemeral: true - }); - - if (i.customId !== "text-mode") return; - - const progress = xp.progress_next_level; - const progressBar = createProgressBar(progress); - - await i.update({ - embeds: [ - quickEmbed( - { - color, - title: 'XP', - description: `<@${user}> you have ${xp.xp.toLocaleString()} XP! (Level ${convertToLevels(xp.xp)})`, - }, - interaction - ).addFields([ - { - name: 'Rank', - value: `#${rank.toLocaleString()}`, - }, - { - name: 'Progress To Next Level', - value: `${progressBar} ${progress}%`, - inline: true, - }, - { - name: 'XP Required', - value: `${xp.xp_needed_next_level.toLocaleString()} XP`, - inline: true, - }, - ]), - ], - files: [], - components: [] - }) - }) - - function createProgressBar(progress: number): string { - const filled = Math.floor(progress / 10); - const empty = 10 - filled; - return 'â–°'.repeat(filled) + 'â–±'.repeat(empty); - } - } - }, - top: { - data: { - options: [], - name: 'top', - description: 'Get the top users for the server', - integration_types: [0], - contexts: [0, 2], - }, - execute: async (interaction) => { - if (interaction?.guildId) { - const guild = interaction.guild?.id; - - try { - const [embed, row] = await leaderboardEmbed(guild as string, interaction); - await interaction.reply({ embeds: [embed], components: [row] }); - } catch (error) { - console.error('Error executing command:', error); - await interaction.reply('There was an error retrieving the leaderboard.'); - } - } else { - await interaction.reply('This command can only be used in a guild.'); - } - } - }, - cansee: { - data: { - options: [], - name: 'cansee', - description: 'Check what channels the bot can see', - integration_types: [0], - contexts: [0, 2], - }, - execute: async (interaction) => { - if (!interaction.memberPermissions?.has('ManageChannels')) { - const errorEmbed = quickEmbed({ - color: 'Red', - title: 'Error!', - description: 'Missing permissions: `Manage Channels`' - }, interaction); - await interaction.reply({ - ephemeral: true, - embeds: [errorEmbed] - }) - .catch(console.error); - return; - } - - const channels = await interaction.guild?.channels.fetch(); - const accessibleChannels = channels?.filter(channel => channel && channel.permissionsFor(interaction.client.user)?.has('ViewChannel') && channel.type !== ChannelType.GuildCategory); - - await interaction - .reply({ - ephemeral: true, - content: accessibleChannels?.map(channel => `<#${channel?.id}>`).join('\n') - }) - .catch(console.error); - }, - }, - roles: { - data: { - options: [ - { - name: 'action', - description: 'Select an action', - type: 3, - required: true, - choices: [ - { - name: 'Get', - value: 'get', - }, - { - name: 'Add', - value: 'add', - }, - { - name: 'Remove', - value: 'remove', - } - ] - }, - { - name: 'role', - description: 'Enter the role name. Required for add and remove actions.', - type: 8, - required: false, - }, - { - name: 'level', - description: 'Enter the level. Required for add action.', - type: 4, - required: false, - choices: [] - } - ], - name: 'roles', - description: 'Manage your roles for levels!', - integration_types: [0], - contexts: [0, 2], - }, - execute: async (interaction) => { - if (!interaction.memberPermissions?.has('ManageRoles')) { - const errorEmbed = quickEmbed({ - color: 'Red', - title: 'Error!', - description: 'Missing permissions: `Manage Roles`' - }, interaction); - await interaction.reply({ - ephemeral: true, - embeds: [errorEmbed] - }) - .catch(console.error); - return; - } - - const action = interaction.options.get('action')?.value; - const role = interaction.options.get('role')?.value; - const level = interaction.options.get('level')?.value; - let apiSuccess; - let roles; - switch (action) { - case 'get': - roles = await getRoles(interaction.guildId as string); - if (Object.keys(roles).length === 0) { - await interaction.reply({ ephemeral: true, content: 'No roles found! This was either an error from the API or you have none!' }); - return; - } - await interaction.reply({ ephemeral: true, content: `Roles:\n${roles.map((entry: { role_id: string; level: number }) => `<@&${entry.role_id}> - Level ${entry.level}`).join('\n')}` }); - return; - case 'add': - if (!role || !level) { - await interaction.reply({ ephemeral: true, content: 'ERROR: One of these two values were not specified! [role, level]' }); - return; - } - apiSuccess = await addRole(interaction.guildId as string, role as string, parseInt(level as string)); - if (apiSuccess) { - await interaction.reply({ ephemeral: true, content: `Successfully added <@&${role}> to level ${level}` }); - return; - } - await interaction.reply({ ephemeral: true, content: `ERROR: Couldn't add <@&${role}> to level ${level}` }); - return; - default: - if (!role) { - await interaction.reply({ ephemeral: true, content: 'ERROR: Role was not specified!' }); - } - apiSuccess = await removeRole(interaction.guildId as string, role as string); - if (apiSuccess) { - await interaction.reply({ ephemeral: true, content: `Successfully removed <@&${role}>` }); - return; - } - await interaction.reply({ ephemeral: true, content: `ERROR: Couldn't remove <@&${role}>` }); - return; - } - } - }, - updates: { - data: { - options: [{ - name: 'action', - description: 'Select an action', - type: 3, - required: true, - choices: [ - { - name: 'Check', - value: 'check', - }, - { - name: 'Enable', - value: 'enable', - }, - { - name: 'Disable', - value: 'disable', - }, - { - name: 'Set', - value: 'set', - }, - { - name: 'Reset to Default', - value: 'reset', - }, - ] - }, { - name: 'channel', - description: 'Enter the channel ID. Required for set action.', - type: 7, - required: false, - }], - name: 'updates', - description: 'Get the latest updates on the bot!', - integration_types: [0], - contexts: [0, 2], - }, - execute: async (interaction) => { - if (!interaction.memberPermissions?.has('ManageRoles')) { - const errorEmbed = quickEmbed({ - color: 'Red', - title: 'Error!', - description: 'Missing permissions: `Manage Roles`' - }, interaction); - await interaction.reply({ - ephemeral: true, - embeds: [errorEmbed] - }) - .catch(console.error); - return; - } - - const action = interaction.options.get('action')?.value; - const channelId = interaction.channelId; - let success - let data - - switch (action) { - case 'enable': - success = await enableUpdates(interaction.guildId as string); - if (!success) { - await interaction.reply({ ephemeral: true, content: 'Error enabling updates for this server' }).catch(console.error); - return; - } - await interaction.reply({ ephemeral: true, content: `Updates are now enabled for this server` }).catch(console.error); - return; - case 'disable': - success = await disableUpdates(interaction.guildId as string); - if (!success) { - await interaction.reply({ ephemeral: true, content: 'Error disabling updates for this server' }).catch(console.error); - return; - } - await interaction.reply({ ephemeral: true, content: 'Updates are now disabled for this server' }).catch(console.error); - return; - case 'set': - if (!channelId) { - await interaction.reply({ ephemeral: true, content: 'ERROR: Channel was not specified!' }); - return; - } - success = await setUpdatesChannel(interaction.guildId as string, channelId); - if (!success) { - await interaction.reply({ ephemeral: true, content: 'Error setting updates channel for this server' }).catch(console.error); - return; - } - await interaction.reply({ ephemeral: true, content: `Updates channel has been set to <#${channelId}>` }).catch(console.error); - return; - case 'reset': - success = await setUpdatesChannel(interaction.guildId as string, null); - if (!success) { - await interaction.reply({ ephemeral: true, content: 'Error resetting updates channel for this server' }).catch(console.error); - return; - } - await interaction.reply({ ephemeral: true, content: `Updates channel has been reset to default` }).catch(console.error); - return - default: - data = await getUpdatesChannel(interaction.guildId as string); - if (!data || Object.keys(data).length === 0) { - await interaction.reply({ ephemeral: true, content: 'No data found' }).catch(console.error); - return; - } - await interaction.reply({ - embeds: [ - quickEmbed({ - color: 'Blurple', - title: 'Updates', - description: 'Updates for this server', - }, interaction) - .addFields( - { - name: 'Enabled', - value: data.enabled ? 'Yes' : 'No', - inline: true, - }, - { - name: 'Channel', - value: data.channel ? `<#${data.channel}>` : 'N/A', - inline: true, - }, - ) - ], - ephemeral: true - }).catch(console.error); - return; - } - }, - }, - cooldown: { - data: { - options: [{ - name: 'action', - description: 'Select an action', - type: 3, - required: true, - choices: [ - { - name: 'Get', - value: 'get', - }, - { - name: 'Set', - value: 'set', - } - ] - }, { - name: 'cooldown', - description: 'Enter the cooldown in seconds. Required for set action.', - type: 4, - required: false, - choices: [] - }], - name: 'cooldown', - description: 'Manage the cooldown for XP!', - integration_types: [0], - contexts: [0, 2], - }, - execute: async (interaction) => { - if (!interaction.memberPermissions?.has('ManageChannels')) { - const errorEmbed = quickEmbed({ - color: 'Red', - title: 'Error!', - description: 'Missing permissions: `Manage Channels`' - }, interaction); - await interaction.reply({ - ephemeral: true, - embeds: [errorEmbed] - }) - .catch(console.error); - return; - } - - const action = interaction.options.get('action')?.value; - const cooldown = interaction.options.get('cooldown')?.value; - - let cooldownData; - let apiSuccess; - - switch (action) { - case 'get': - cooldownData = await getCooldown(interaction.guildId as string); - if (!cooldownData) { - await interaction.reply({ ephemeral: true, content: 'Error fetching cooldown data!' }); - return; - } - await interaction.reply({ ephemeral: true, content: `Cooldown: ${(cooldownData?.cooldown ?? 30_000) / 1000} seconds` }); - return; - case 'set': - if (!cooldown) { - await interaction.reply({ ephemeral: true, content: 'ERROR: Cooldown was not specified!' }); - return; - } - apiSuccess = await setCooldown(interaction.guildId as string, parseInt(cooldown as string) * 1000); - if (!apiSuccess) { - await interaction.reply({ ephemeral: true, content: 'Error setting cooldown!' }); - return; - } - await interaction.reply({ ephemeral: true, content: `Cooldown set to ${cooldown} seconds` }); - return; - } - } - }, - set: { - data: { - options: [{ - name: 'user', - description: 'The user you want to update the XP or level of.', - type: 6, - required: true, - }, { - name: 'type', - description: 'Select the data type to set', - type: 3, - required: true, - choices: [ - { - name: 'XP', - value: 'xp', - }, - { - name: 'Level', - value: 'level', - } - ] - }, { - name: 'value', - description: 'The new value to set', - type: 3, - required: true, - }], - name: 'set', - description: 'Set the XP or level of a user!', - integration_types: [0], - contexts: [0, 2], - }, - execute: async (interaction) => { - if (!interaction.memberPermissions?.has('ManageGuild')) { - const errorEmbed = quickEmbed({ - color: 'Red', - title: 'Error!', - description: 'Missing permissions: `Manage Server`' - }, interaction); - await interaction.reply({ - ephemeral: true, - embeds: [errorEmbed] - }) - .catch(console.error); - return; - } - - const user = interaction.options.get('user')?.value as string; - const type = interaction.options.get('type')?.value; - const value = interaction.options.get('value')?.value; - - let apiSuccess; - switch (type) { - case 'xp': - apiSuccess = await setXP(interaction.guildId as string, user, parseInt(value as string)); - if (!apiSuccess) { - await interaction.reply({ ephemeral: true, content: 'Error setting XP!' }); - return; - } - await interaction.reply({ ephemeral: true, content: `XP set to ${value} for <@${user}>` }); - return; - case 'level': - apiSuccess = await setLevel(interaction.guildId as string, user, parseInt(value as string)); - if (!apiSuccess) { - await interaction.reply({ ephemeral: true, content: 'Error setting level!' }); - return; - } - await interaction.reply({ ephemeral: true, content: `Level set to ${value} for <@${user}>` }); - return; - } - } - }, - sync: { - data: { - options: [{ - name: 'bot', - description: 'Select the bot to sync XP data from', - type: 3, - required: true, - choices: [ - { - name: 'Polaris', - value: 'polaris', - }, - { - name: 'MEE6', - value: 'mee6', - }, - { - name: 'Lurkr', - value: 'lurkr', - }, - ] - }], - name: 'sync', - description: 'Sync XP data from another bot!', - integration_types: [0], - contexts: [0, 2], - }, - execute: async (interaction) => { - if (!interaction.memberPermissions?.has('ManageGuild')) { - const errorEmbed = quickEmbed({ - color: 'Red', - title: 'Error!', - description: 'Missing permissions: `Manage Server`' - }, interaction); - await interaction.reply({ - ephemeral: true, - embeds: [errorEmbed] - }) - .catch(console.error); - return; - } - - const bot = interaction.options.get('bot')?.value; - const formattedBotNames = { - 'polaris': 'Polaris', - 'mee6': 'MEE6', - 'lurkr': 'Lurkr' - }; - - await interaction.reply({ ephemeral: true, content: `Syncing data from ${formattedBotNames[bot as keyof typeof formattedBotNames]}...` }); - const apiSuccess = await syncFromBot(interaction.guildId as string, bot as string); - if (!apiSuccess) { - await interaction.editReply({ content: `Error syncing data! This might mean that ${formattedBotNames[bot as keyof typeof formattedBotNames]} is not set up for this server, or the leaderboard for this server is not public.` }); - return; - } - await interaction.editReply({ content: 'Data synced!' }); - return; - } - } + ping: { + data: { + options: [], + name: "ping", + description: "Check the ping of the bot!", + integration_types: [0, 1], + contexts: [0, 1, 2], + }, + execute: async (interaction) => { + await interaction + .reply({ + ephemeral: false, + content: `Ping: ${interaction.client.ws.ping}ms`, + }) + .catch(console.error); + }, + }, + help: { + data: { + options: [], + name: "help", + description: "Get help on what each command does!", + integration_types: [0, 1], + contexts: [0, 1, 2], + }, + execute: async (interaction) => { + await client.application?.commands?.fetch().catch(console.error); + const chat_commands = client.application?.commands.cache.map( + (a) => { + return `: ${a.description}`; + } + ); + + await interaction + .reply({ + ephemeral: true, + content: `Commands:\n${chat_commands?.join("\n")}`, + }) + .catch(console.error); + }, + }, + sourcecode: { + data: { + options: [], + name: "sourcecode", + description: "Get the link of the app's source code.", + integration_types: [0, 1], + contexts: [0, 1, 2], + }, + execute: async (interaction) => { + await interaction + .reply({ + ephemeral: true, + content: `[Github repository](https://github.com/GalvinPython/chatr)`, + }) + .catch(console.error); + }, + }, + uptime: { + data: { + options: [], + name: "uptime", + description: "Check the uptime of the bot!", + integration_types: [0, 1], + contexts: [0, 1, 2], + }, + execute: async (interaction) => { + await interaction + .reply({ + ephemeral: false, + content: `Uptime: ${( + performance.now() / + (86400 * 1000) + ).toFixed(2)} days`, + }) + .catch(console.error); + }, + }, + usage: { + data: { + options: [], + name: "usage", + description: "Check the heap size and disk usage of the bot!", + integration_types: [0, 1], + contexts: [0, 1, 2], + }, + execute: async (interaction) => { + const heap = heapStats(); + + Bun.gc(false); + const dbSize = await getDBSize(); + + await interaction + .reply({ + ephemeral: false, + content: [ + `Heap size: ${(heap.heapSize / 1024 / 1024).toFixed(2)} MB / ${( + heap.heapCapacity / + 1024 / + 1024 + ).toFixed( + 2 + )} MB (${(heap.extraMemorySize / 1024 / 1024).toFixed(2)} MB) (${heap.objectCount.toLocaleString()} objects, ${heap.protectedObjectCount.toLocaleString()} protected-objects)`, + `Disk usage: ${dbSize.sizeInMB.toFixed(2)} MB`, + ] + .join("\n") + .slice(0, 2000), + }) + .catch(console.error); + }, + }, + xp: { + data: { + options: [ + { + name: "user", + description: "The user you want to check the XP of.", + type: 6, + required: false, + }, + ], + name: "xp", + description: "Get your XP and Points", + integration_types: [0], + contexts: [0, 2], + }, + execute: async (interaction) => { + await interaction.deferReply(); + + const optionUser = interaction.options.get("user")?.value as + | string + | null; + const member = ( + optionUser + ? interaction.guild!.members.cache.get(optionUser) + : interaction.member + ) as GuildMember; + + await interaction.guild!.members.fetch({ + user: member.id, + force: true, + }); + const guild = interaction.guild?.id; + const user = member.id; + const leaderboard = await getGuildLeaderboard(guild as string); + const xp = await makeGETRequest(guild as string, user); + + if (!xp || leaderboard.length === 0) { + await interaction.followUp({ + ephemeral: true, + content: "No XP data available.", + }); + + return; + } + + const rank = + leaderboard.leaderboard.findIndex( + (entry: { id: string }) => entry.id === user + ) + 1; + + const card = new RankCardBuilder() + .setDisplayName(member.displayName) + .setAvatar( + member.displayAvatarURL({ forceStatic: true, size: 4096 }) + ) // user avatar + .setCurrentXP(xp.xp) // current xp + .setRequiredXP(xp.xp + xp.xp_needed_next_level) // required xp + .setLevel(xp.level) // user level + .setRank(rank) // user rank + .setOverlay(member.user.banner ? 95 : 90) // overlay percentage. Overlay is a semi-transparent layer on top of the background + .setBackground( + member.user.bannerURL({ forceStatic: true, size: 4096 }) ?? + "#23272a" + ); + + if (interaction.user.discriminator !== "0") { + card.setUsername("#" + member.user.discriminator); + } else { + card.setUsername("@" + member.user.username); + } + + const color = member.roles.highest.hexColor ?? "#ffffff"; + + card.setStyles({ + progressbar: { + thumb: { + style: { + backgroundColor: color as any, + }, + }, + }, + }); + + const image = await card.build({ + format: "png", + }); + const attachment = new AttachmentBuilder(image, { + name: `${user}.png`, + }); + + const msg = await interaction.followUp({ + files: [attachment], + components: [ + new ActionRowBuilder().setComponents( + new ButtonBuilder() + .setCustomId("text-mode") + .setLabel("Use text mode") + .setStyle(ButtonStyle.Secondary) + ), + ], + fetchReply: true, + }); + + const collector = msg.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 60 * 1000, + }); + + collector.on("collect", async (i) => { + if (i.user.id !== user) + return i.reply({ + content: + "You're not the one who initialized this message! Try running /xp on your own.", + ephemeral: true, + }); + + if (i.customId !== "text-mode") return; + + const progress = xp.progress_next_level; + const progressBar = createProgressBar(progress); + + await i.update({ + embeds: [ + quickEmbed( + { + color, + title: "XP", + description: `<@${user}> you have ${xp.xp.toLocaleString()} XP! (Level ${convertToLevels(xp.xp)})`, + }, + interaction + ).addFields([ + { + name: "Rank", + value: `#${rank.toLocaleString()}`, + }, + { + name: "Progress To Next Level", + value: `${progressBar} ${progress}%`, + inline: true, + }, + { + name: "XP Required", + value: `${xp.xp_needed_next_level.toLocaleString()} XP`, + inline: true, + }, + ]), + ], + files: [], + components: [], + }); + }); + + function createProgressBar(progress: number): string { + const filled = Math.floor(progress / 10); + const empty = 10 - filled; + + return "â–°".repeat(filled) + "â–±".repeat(empty); + } + }, + }, + top: { + data: { + options: [], + name: "top", + description: "Get the top users for the server", + integration_types: [0], + contexts: [0, 2], + }, + execute: async (interaction) => { + if (interaction?.guildId) { + const guild = interaction.guild?.id; + + try { + const result = await leaderboardEmbed( + guild as string, + interaction + ); + + if (!result) return; + + await interaction.reply({ + embeds: [result.embed], + components: [result.row], + }); + } catch (error) { + console.error("Error executing command:", error); + await interaction.reply( + "There was an error retrieving the leaderboard." + ); + } + } else { + await interaction.reply( + "This command can only be used in a guild." + ); + } + }, + }, + cansee: { + data: { + options: [], + name: "cansee", + description: "Check what channels the bot can see", + integration_types: [0], + contexts: [0, 2], + }, + execute: async (interaction) => { + if (!interaction.memberPermissions?.has("ManageChannels")) { + const errorEmbed = quickEmbed( + { + color: "Red", + title: "Error!", + description: "Missing permissions: `Manage Channels`", + }, + interaction + ); + + await interaction + .reply({ + ephemeral: true, + embeds: [errorEmbed], + }) + .catch(console.error); + + return; + } + + const channels = await interaction.guild?.channels.fetch(); + const accessibleChannels = channels?.filter( + (channel) => + channel && + channel + .permissionsFor(interaction.client.user) + ?.has("ViewChannel") && + channel.type !== ChannelType.GuildCategory + ); + + await interaction + .reply({ + ephemeral: true, + content: accessibleChannels + ?.map((channel) => `<#${channel?.id}>`) + .join("\n"), + }) + .catch(console.error); + }, + }, + roles: { + data: { + options: [ + { + name: "action", + description: "Select an action", + type: 3, + required: true, + choices: [ + { + name: "Get", + value: "get", + }, + { + name: "Add", + value: "add", + }, + { + name: "Remove", + value: "remove", + }, + ], + }, + { + name: "role", + description: + "Enter the role name. Required for add and remove actions.", + type: 8, + required: false, + }, + { + name: "level", + description: "Enter the level. Required for add action.", + type: 4, + required: false, + choices: [], + }, + ], + name: "roles", + description: "Manage your roles for levels!", + integration_types: [0], + contexts: [0, 2], + }, + execute: async (interaction) => { + if (!interaction.memberPermissions?.has("ManageRoles")) { + const errorEmbed = quickEmbed( + { + color: "Red", + title: "Error!", + description: "Missing permissions: `Manage Roles`", + }, + interaction + ); + + await interaction + .reply({ + ephemeral: true, + embeds: [errorEmbed], + }) + .catch(console.error); + + return; + } + + const action = interaction.options.get("action")?.value; + const role = interaction.options.get("role")?.value; + const level = interaction.options.get("level")?.value; + let apiSuccess; + let roles; + + switch (action) { + case "get": + roles = await getRoles(interaction.guildId as string); + if (Object.keys(roles).length === 0) { + await interaction.reply({ + ephemeral: true, + content: + "No roles found! This was either an error from the API or you have none!", + }); + + return; + } + await interaction.reply({ + ephemeral: true, + content: `Roles:\n${roles.map((entry: { role_id: string; level: number }) => `<@&${entry.role_id}> - Level ${entry.level}`).join("\n")}`, + }); + + return; + case "add": + if (!role || !level) { + await interaction.reply({ + ephemeral: true, + content: + "ERROR: One of these two values were not specified! [role, level]", + }); + + return; + } + apiSuccess = await addRole( + interaction.guildId as string, + role as string, + parseInt(level as string) + ); + if (apiSuccess) { + await interaction.reply({ + ephemeral: true, + content: `Successfully added <@&${role}> to level ${level}`, + }); + + return; + } + await interaction.reply({ + ephemeral: true, + content: `ERROR: Couldn't add <@&${role}> to level ${level}`, + }); + + return; + default: + if (!role) { + await interaction.reply({ + ephemeral: true, + content: "ERROR: Role was not specified!", + }); + } + apiSuccess = await removeRole( + interaction.guildId as string, + role as string + ); + if (apiSuccess) { + await interaction.reply({ + ephemeral: true, + content: `Successfully removed <@&${role}>`, + }); + + return; + } + await interaction.reply({ + ephemeral: true, + content: `ERROR: Couldn't remove <@&${role}>`, + }); + + return; + } + }, + }, + updates: { + data: { + options: [ + { + name: "action", + description: "Select an action", + type: 3, + required: true, + choices: [ + { + name: "Check", + value: "check", + }, + { + name: "Enable", + value: "enable", + }, + { + name: "Disable", + value: "disable", + }, + { + name: "Set", + value: "set", + }, + { + name: "Reset to Default", + value: "reset", + }, + ], + }, + { + name: "channel", + description: + "Enter the channel ID. Required for set action.", + type: 7, + required: false, + }, + ], + name: "updates", + description: "Get the latest updates on the bot!", + integration_types: [0], + contexts: [0, 2], + }, + execute: async (interaction) => { + if (!interaction.memberPermissions?.has("ManageRoles")) { + const errorEmbed = quickEmbed( + { + color: "Red", + title: "Error!", + description: "Missing permissions: `Manage Roles`", + }, + interaction + ); + + await interaction + .reply({ + ephemeral: true, + embeds: [errorEmbed], + }) + .catch(console.error); + + return; + } + + const action = interaction.options.get("action")?.value; + const channelId = interaction.channelId; + let success; + let data; + + switch (action) { + case "enable": + success = await enableUpdates( + interaction.guildId as string + ); + if (!success) { + await interaction + .reply({ + ephemeral: true, + content: + "Error enabling updates for this server", + }) + .catch(console.error); + + return; + } + await interaction + .reply({ + ephemeral: true, + content: `Updates are now enabled for this server`, + }) + .catch(console.error); + + return; + case "disable": + success = await disableUpdates( + interaction.guildId as string + ); + if (!success) { + await interaction + .reply({ + ephemeral: true, + content: + "Error disabling updates for this server", + }) + .catch(console.error); + + return; + } + await interaction + .reply({ + ephemeral: true, + content: "Updates are now disabled for this server", + }) + .catch(console.error); + + return; + case "set": + if (!channelId) { + await interaction.reply({ + ephemeral: true, + content: "ERROR: Channel was not specified!", + }); + + return; + } + success = await setUpdatesChannel( + interaction.guildId as string, + channelId + ); + if (!success) { + await interaction + .reply({ + ephemeral: true, + content: + "Error setting updates channel for this server", + }) + .catch(console.error); + + return; + } + await interaction + .reply({ + ephemeral: true, + content: `Updates channel has been set to <#${channelId}>`, + }) + .catch(console.error); + + return; + case "reset": + success = await setUpdatesChannel( + interaction.guildId as string, + null + ); + if (!success) { + await interaction + .reply({ + ephemeral: true, + content: + "Error resetting updates channel for this server", + }) + .catch(console.error); + + return; + } + await interaction + .reply({ + ephemeral: true, + content: `Updates channel has been reset to default`, + }) + .catch(console.error); + + return; + default: + data = await getUpdatesChannel( + interaction.guildId as string + ); + if (!data || Object.keys(data).length === 0) { + await interaction + .reply({ + ephemeral: true, + content: "No data found", + }) + .catch(console.error); + + return; + } + await interaction + .reply({ + embeds: [ + quickEmbed( + { + color: "Blurple", + title: "Updates", + description: "Updates for this server", + }, + interaction + ).addFields( + { + name: "Enabled", + value: data.enabled ? "Yes" : "No", + inline: true, + }, + { + name: "Channel", + value: data.channel + ? `<#${data.channel}>` + : "N/A", + inline: true, + } + ), + ], + ephemeral: true, + }) + .catch(console.error); + + return; + } + }, + }, + cooldown: { + data: { + options: [ + { + name: "action", + description: "Select an action", + type: 3, + required: true, + choices: [ + { + name: "Get", + value: "get", + }, + { + name: "Set", + value: "set", + }, + ], + }, + { + name: "cooldown", + description: + "Enter the cooldown in seconds. Required for set action.", + type: 4, + required: false, + choices: [], + }, + ], + name: "cooldown", + description: "Manage the cooldown for XP!", + integration_types: [0], + contexts: [0, 2], + }, + execute: async (interaction) => { + if (!interaction.memberPermissions?.has("ManageChannels")) { + const errorEmbed = quickEmbed( + { + color: "Red", + title: "Error!", + description: "Missing permissions: `Manage Channels`", + }, + interaction + ); + + await interaction + .reply({ + ephemeral: true, + embeds: [errorEmbed], + }) + .catch(console.error); + + return; + } + + const action = interaction.options.get("action")?.value; + const cooldown = interaction.options.get("cooldown")?.value; + + let cooldownData; + let apiSuccess; + + switch (action) { + case "get": + cooldownData = await getCooldown( + interaction.guildId as string + ); + if (!cooldownData) { + await interaction.reply({ + ephemeral: true, + content: "Error fetching cooldown data!", + }); + + return; + } + await interaction.reply({ + ephemeral: true, + content: `Cooldown: ${(cooldownData?.cooldown ?? 30_000) / 1000} seconds`, + }); + + return; + case "set": + if (!cooldown) { + await interaction.reply({ + ephemeral: true, + content: "ERROR: Cooldown was not specified!", + }); + + return; + } + apiSuccess = await setCooldown( + interaction.guildId as string, + parseInt(cooldown as string) * 1000 + ); + if (!apiSuccess) { + await interaction.reply({ + ephemeral: true, + content: "Error setting cooldown!", + }); + + return; + } + await interaction.reply({ + ephemeral: true, + content: `Cooldown set to ${cooldown} seconds`, + }); + + return; + } + }, + }, + set: { + data: { + options: [ + { + name: "user", + description: + "The user you want to update the XP or level of.", + type: 6, + required: true, + }, + { + name: "type", + description: "Select the data type to set", + type: 3, + required: true, + choices: [ + { + name: "XP", + value: "xp", + }, + { + name: "Level", + value: "level", + }, + ], + }, + { + name: "value", + description: "The new value to set", + type: 3, + required: true, + }, + ], + name: "set", + description: "Set the XP or level of a user!", + integration_types: [0], + contexts: [0, 2], + }, + execute: async (interaction) => { + if (!interaction.memberPermissions?.has("ManageGuild")) { + const errorEmbed = quickEmbed( + { + color: "Red", + title: "Error!", + description: "Missing permissions: `Manage Server`", + }, + interaction + ); + + await interaction + .reply({ + ephemeral: true, + embeds: [errorEmbed], + }) + .catch(console.error); + + return; + } + + const user = interaction.options.get("user")?.value as string; + const type = interaction.options.get("type")?.value; + const value = interaction.options.get("value")?.value; + + let apiSuccess; + + switch (type) { + case "xp": + apiSuccess = await setXP( + interaction.guildId as string, + user, + parseInt(value as string) + ); + if (!apiSuccess) { + await interaction.reply({ + ephemeral: true, + content: "Error setting XP!", + }); + + return; + } + await interaction.reply({ + ephemeral: true, + content: `XP set to ${value} for <@${user}>`, + }); + + return; + case "level": + apiSuccess = await setLevel( + interaction.guildId as string, + user, + parseInt(value as string) + ); + if (!apiSuccess) { + await interaction.reply({ + ephemeral: true, + content: "Error setting level!", + }); + + return; + } + await interaction.reply({ + ephemeral: true, + content: `Level set to ${value} for <@${user}>`, + }); + + return; + } + }, + }, + sync: { + data: { + options: [ + { + name: "bot", + description: "Select the bot to sync XP data from", + type: 3, + required: true, + choices: [ + { + name: "Polaris", + value: "polaris", + }, + { + name: "MEE6", + value: "mee6", + }, + { + name: "Lurkr", + value: "lurkr", + }, + ], + }, + ], + name: "sync", + description: "Sync XP data from another bot!", + integration_types: [0], + contexts: [0, 2], + }, + execute: async (interaction) => { + if (!interaction.memberPermissions?.has("ManageGuild")) { + const errorEmbed = quickEmbed( + { + color: "Red", + title: "Error!", + description: "Missing permissions: `Manage Server`", + }, + interaction + ); + + await interaction + .reply({ + ephemeral: true, + embeds: [errorEmbed], + }) + .catch(console.error); + + return; + } + + const bot = interaction.options.get("bot")?.value; + const formattedBotNames = { + polaris: "Polaris", + mee6: "MEE6", + lurkr: "Lurkr", + }; + + await interaction.reply({ + ephemeral: true, + content: `Syncing data from ${formattedBotNames[bot as keyof typeof formattedBotNames]}...`, + }); + const apiSuccess = await syncFromBot( + interaction.guildId as string, + bot as string + ); + + if (!apiSuccess) { + await interaction.editReply({ + content: `Error syncing data! This might mean that ${formattedBotNames[bot as keyof typeof formattedBotNames]} is not set up for this server, or the leaderboard for this server is not public.`, + }); + + return; + } + await interaction.editReply({ content: "Data synced!" }); + + return; + }, + }, }; // Convert commands to a Map const commandsMap = new Map(); + for (const key in commands) { - if (Object.prototype.hasOwnProperty.call(commands, key)) { - const command = commands[key]; - console.log('loading ' + key); - commandsMap.set(key, command); - } + if (Object.prototype.hasOwnProperty.call(commands, key)) { + const command = commands[key]; + + console.log("loading " + key); + commandsMap.set(key, command); + } } export default commandsMap; diff --git a/bot/src/events/command.ts b/bot/src/events/command.ts index ca4db37..599c081 100644 --- a/bot/src/events/command.ts +++ b/bot/src/events/command.ts @@ -1,15 +1,17 @@ -import { Events } from 'discord.js'; -import client from '../index'; +import { Events } from "discord.js"; -import commands from '../commands'; +import client from "../index"; +import commands from "../commands"; client.on(Events.InteractionCreate, async (interaction) => { if (interaction.isChatInputCommand()) { const getCommand = commands.get(interaction.commandName); + if (!getCommand) return console.log( - `${interaction.user.displayName} tried to do /${interaction.commandName} (${interaction.commandId}) but it wasn't found.`, + `${interaction.user.displayName} tried to do /${interaction.commandName} (${interaction.commandId}) but it wasn't found.` ); + return getCommand.execute(interaction); } -}); \ No newline at end of file +}); diff --git a/bot/src/events/guildAdd.ts b/bot/src/events/guildAdd.ts index b37c0a7..5aa2395 100644 --- a/bot/src/events/guildAdd.ts +++ b/bot/src/events/guildAdd.ts @@ -1,12 +1,20 @@ import { Events } from "discord.js"; + import client from "../index"; import { updateGuildInfo } from "../utils/requestAPI"; client.on(Events.GuildCreate, async (guild) => { - try { - await updateGuildInfo(guild.id, guild.name, guild.iconURL() ?? 'https://cdn.discordapp.com/embed/avatars/0.png', guild.memberCount); - console.log(`Joined guild ${guild.name} with ${guild.memberCount} members`); - } catch (e) { - console.error(e); - } -}) \ No newline at end of file + try { + await updateGuildInfo( + guild.id, + guild.name, + guild.iconURL() ?? "https://cdn.discordapp.com/embed/avatars/0.png", + guild.memberCount + ); + console.log( + `Joined guild ${guild.name} with ${guild.memberCount} members` + ); + } catch (e) { + console.error(e); + } +}); diff --git a/bot/src/events/guildMemberUpdate.ts b/bot/src/events/guildMemberUpdate.ts index 4e63fbd..7a80ecd 100644 --- a/bot/src/events/guildMemberUpdate.ts +++ b/bot/src/events/guildMemberUpdate.ts @@ -1,14 +1,29 @@ import { Events } from "discord.js"; + import client from "../index"; import { makePOSTRequest } from "../utils/requestAPI"; client.on(Events.GuildMemberUpdate, async (_oldMember, newMember) => { - console.log(`Updating user ${newMember.user.username} for ${newMember.guild.name}`); - if (newMember.user.bot) return; - try { - await makePOSTRequest(newMember.guild.id, newMember.id, null, null, newMember.displayAvatarURL(), newMember.user.username, newMember.nickname ?? newMember.user.globalName ?? newMember.user.username); - console.log(`Updated user ${newMember.user.username} for ${newMember.guild.name}`); - } catch (e) { - console.error(e); - } -}) \ No newline at end of file + console.log( + `Updating user ${newMember.user.username} for ${newMember.guild.name}` + ); + if (newMember.user.bot) return; + try { + await makePOSTRequest( + newMember.guild.id, + newMember.id, + null, + null, + newMember.displayAvatarURL(), + newMember.user.username, + newMember.nickname ?? + newMember.user.globalName ?? + newMember.user.username + ); + console.log( + `Updated user ${newMember.user.username} for ${newMember.guild.name}` + ); + } catch (e) { + console.error(e); + } +}); diff --git a/bot/src/events/guildRemove.ts b/bot/src/events/guildRemove.ts index 7018b49..8f23271 100644 --- a/bot/src/events/guildRemove.ts +++ b/bot/src/events/guildRemove.ts @@ -1,12 +1,15 @@ import { Events } from "discord.js"; + import client from "../index"; import { removeGuild } from "../utils/requestAPI"; client.on(Events.GuildDelete, async (guild) => { - try { - await removeGuild(guild.id); - console.log(`Left guild ${guild.name} with ${guild.memberCount} members. The database has been locked`); - } catch (e) { - console.error(e); - } -}) \ No newline at end of file + try { + await removeGuild(guild.id); + console.log( + `Left guild ${guild.name} with ${guild.memberCount} members. The database has been locked` + ); + } catch (e) { + console.error(e); + } +}); diff --git a/bot/src/events/guildUpdate.ts b/bot/src/events/guildUpdate.ts index c2d4124..87483a8 100644 --- a/bot/src/events/guildUpdate.ts +++ b/bot/src/events/guildUpdate.ts @@ -1,12 +1,21 @@ import { Events } from "discord.js"; + import client from "../index"; import { updateGuildInfo } from "../utils/requestAPI"; client.on(Events.GuildUpdate, async (_oldGuild, newGuild) => { - try { - await updateGuildInfo(newGuild.id, newGuild.name, newGuild?.iconURL() ?? 'https://cdn.discordapp.com/embed/avatars/0.png', newGuild.memberCount); - console.log(`Updated guild ${newGuild.name} with ${newGuild.memberCount} members`); - } catch (e) { - console.error(e); - } -}) \ No newline at end of file + try { + await updateGuildInfo( + newGuild.id, + newGuild.name, + newGuild?.iconURL() ?? + "https://cdn.discordapp.com/embed/avatars/0.png", + newGuild.memberCount + ); + console.log( + `Updated guild ${newGuild.name} with ${newGuild.memberCount} members` + ); + } catch (e) { + console.error(e); + } +}); diff --git a/bot/src/events/memberRemove.ts b/bot/src/events/memberRemove.ts index e3d398d..1a8df20 100644 --- a/bot/src/events/memberRemove.ts +++ b/bot/src/events/memberRemove.ts @@ -1,16 +1,22 @@ import { Events } from "discord.js"; + import client from "../index"; import { removeUser } from "../utils/requestAPI"; client.on(Events.GuildMemberRemove, async (member) => { - try { - const success = await removeUser(member.id, member.guild.id); - if (success) { - console.log(`Removed user ${member.user.username} from the database`); - } else { - console.error(`Failed to remove user ${member.user.username} from the database`); - } - } catch (e) { - console.error(e); - } -}) \ No newline at end of file + try { + const success = await removeUser(member.id, member.guild.id); + + if (success) { + console.log( + `Removed user ${member.user.username} from the database` + ); + } else { + console.error( + `Failed to remove user ${member.user.username} from the database` + ); + } + } catch (e) { + console.error(e); + } +}); diff --git a/bot/src/events/messageCreate.ts b/bot/src/events/messageCreate.ts index ce37c7f..c79a991 100644 --- a/bot/src/events/messageCreate.ts +++ b/bot/src/events/messageCreate.ts @@ -1,29 +1,58 @@ -import { Message } from 'discord.js'; -import client from '../index'; -import { addUserToTrackingData, getCooldown, makePOSTRequest, updateGuildInfo } from '../utils/requestAPI'; +import { Message } from "discord.js"; + +import client from "../index"; +import { + addUserToTrackingData, + getCooldown, + makePOSTRequest, + updateGuildInfo, +} from "../utils/requestAPI"; const cooldowns = new Map(); // Run this event whenever a message has been sent -client.on('messageCreate', async (message: Message) => { - if (message.author.bot) return; +client.on("messageCreate", async (message: Message) => { + if (message.author.bot) return; + + const cooldownTime = + (await getCooldown(message.guildId as string))?.cooldown ?? 30_000; + + const cooldown = cooldowns.get(message.author.id); + + if (cooldown && Date.now() - cooldown < cooldownTime) return; - const cooldownTime = (await getCooldown(message.guildId as string))?.cooldown ?? 30_000; + const xpToGive: number = message.content.length; + const pfp: string = + message.member?.displayAvatarURL() ?? message.author.displayAvatarURL(); + const name: string = message.author.username; + const nickname: string = + message.member?.nickname ?? + message.author.globalName ?? + message.author.username; - const cooldown = cooldowns.get(message.author.id); - if (cooldown && Date.now() - cooldown < cooldownTime) return; + await makePOSTRequest( + message.guildId as string, + message.author.id, + message.channel.id, + xpToGive, + pfp, + name, + nickname + ); + cooldowns.set(message.author.id, Date.now()); - const xpToGive: number = message.content.length; - const pfp: string = message.member?.displayAvatarURL() ?? message.author.displayAvatarURL() - const name: string = message.author.username; - const nickname: string = message.member?.nickname ?? message.author.globalName ?? message.author.username; - await makePOSTRequest(message.guildId as string, message.author.id, message.channel.id, xpToGive, pfp, name, nickname); - cooldowns.set(message.author.id, Date.now()); + const guildName = message.guild?.name; + const guildIcon = + message.guild?.iconURL() ?? + "https://cdn.discordapp.com/embed/avatars/0.png"; + const guildMembers = message.guild?.memberCount; - const guildName = message.guild?.name; - const guildIcon = message.guild?.iconURL() ?? 'https://cdn.discordapp.com/embed/avatars/0.png'; - const guildMembers = message.guild?.memberCount; - await updateGuildInfo(message.guildId as string, guildName as string, guildIcon as string, guildMembers as number); + await updateGuildInfo( + message.guildId as string, + guildName as string, + guildIcon as string, + guildMembers as number + ); - await addUserToTrackingData(message.author.id, message.guildId as string); + await addUserToTrackingData(message.author.id, message.guildId as string); }); diff --git a/bot/src/events/ready.ts b/bot/src/events/ready.ts index 0230b06..b1d2ec8 100644 --- a/bot/src/events/ready.ts +++ b/bot/src/events/ready.ts @@ -1,30 +1,32 @@ -import { ActivityType, Events, PresenceUpdateStatus } from 'discord.js'; -import client from '../index'; -import cron from 'cron'; -import sendAutoUpdates from '../utils/sendAutoUpdates'; +import { ActivityType, Events, PresenceUpdateStatus } from "discord.js"; +import cron from "cron"; + +import client from "../index"; +import sendAutoUpdates from "../utils/sendAutoUpdates"; // update the bot's presence function updatePresence() { - if (!client?.user) return; - client.user.setPresence({ - activities: [ - { - name: `${client.guilds.cache.size} servers with ${client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0).toLocaleString('en-US')} members.`, - type: ActivityType.Watching, - }, - ], - status: PresenceUpdateStatus.Online, - }); + if (!client?.user) return; + client.user.setPresence({ + activities: [ + { + name: `${client.guilds.cache.size} servers with ${client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0).toLocaleString("en-US")} members.`, + type: ActivityType.Watching, + }, + ], + status: PresenceUpdateStatus.Online, + }); } // Log into the bot client.once(Events.ClientReady, async (bot) => { - console.log(`Ready! Logged in as ${bot.user?.tag}`); - updatePresence(); - // Create a cron job to update the server count in the status every minute - const job = new cron.CronJob('0 * * * *', sendAutoUpdates); - job.start(); + console.log(`Ready! Logged in as ${bot.user?.tag}`); + updatePresence(); + // Create a cron job to update the server count in the status every minute + const job = new cron.CronJob("0 * * * *", sendAutoUpdates); + + job.start(); }); // Update the server count in the status every minute -setInterval(updatePresence, 60000); \ No newline at end of file +setInterval(updatePresence, 60000); diff --git a/bot/src/index.ts b/bot/src/index.ts index e3314db..e3f7ec4 100644 --- a/bot/src/index.ts +++ b/bot/src/index.ts @@ -1,45 +1,48 @@ // Check if DISCORD_TOKEN has been provided as an environment variable, and is a valid regex pattern const discordToken: string | undefined = process.argv.includes("--dev") - ? process.env?.DISCORD_TOKEN_DEV - : process.env?.DISCORD_TOKEN; + ? process.env?.DISCORD_TOKEN_DEV + : process.env?.DISCORD_TOKEN; if (!discordToken || discordToken === "YOUR_TOKEN_HERE") - throw "You MUST provide a discord token in .env!"; + throw "You MUST provide a discord token in .env!"; // If it has, run the bot +import fs from "fs/promises"; +import path from "path"; + import { - Client, - GatewayIntentBits, - REST, - Routes, - type APIApplicationCommand, + Client, + GatewayIntentBits, + REST, + Routes, + type APIApplicationCommand, } from "discord.js"; + import commandsMap from "./commands"; -import fs from "fs/promises"; -import path from "path"; const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildMembers, - ], + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers, + ], }); // Update the commands console.log(`Refreshing ${commandsMap.size} commands`); const rest = new REST().setToken(discordToken); const getAppId: { id?: string | null } = (await rest.get( - Routes.currentApplication(), + Routes.currentApplication() )) || { id: null }; + if (!getAppId?.id) - throw "No application ID was able to be found with this token"; + throw "No application ID was able to be found with this token"; const data = (await rest.put(Routes.applicationCommands(getAppId.id), { - body: [...commandsMap.values()].map((a) => { - return a.data; - }), + body: [...commandsMap.values()].map((a) => { + return a.data; + }), })) as APIApplicationCommand[]; console.log(`Successfully reloaded ${data.length} application (/) commands.`); @@ -50,6 +53,7 @@ export default client; // Import events const getEvents = await fs.readdir(path.join(process.cwd(), "src/events")); + for await (const file of getEvents) { - await import("./events/" + file); + await import("./events/" + file); } diff --git a/bot/src/types.d.ts b/bot/src/types.d.ts index 172a3b6..e14305e 100644 --- a/bot/src/types.d.ts +++ b/bot/src/types.d.ts @@ -1,3 +1,3 @@ declare module "colorthief" { - function getColor(url: string): Promise<[number, number, number]>; + function getColor(url: string): Promise<[number, number, number]>; } diff --git a/bot/src/utils/convertToLevels.ts b/bot/src/utils/convertToLevels.ts index 544b50e..6eccb50 100644 --- a/bot/src/utils/convertToLevels.ts +++ b/bot/src/utils/convertToLevels.ts @@ -1,3 +1,3 @@ -export default function(points: number): number { +export default function (points: number): number { return Math.floor(Math.sqrt(points / 100)); -} \ No newline at end of file +} diff --git a/bot/src/utils/handleLevelChange.ts b/bot/src/utils/handleLevelChange.ts index 6033330..a373ffb 100644 --- a/bot/src/utils/handleLevelChange.ts +++ b/bot/src/utils/handleLevelChange.ts @@ -1,14 +1,25 @@ // import quickEmbed from "./quickEmbed"; import type { TextChannel } from "discord.js"; + import client from ".."; + import { getUpdatesChannel } from "./requestAPI"; -export default async function(guild: string, user: string, channelId: string, level: number) { - const hasUpdates = await getUpdatesChannel(guild); - if (!hasUpdates.enabled) return; +export default async function ( + guild: string, + user: string, + channelId: string, + level: number +) { + const hasUpdates = await getUpdatesChannel(guild); + + if (!hasUpdates.enabled) return; + + const channel = (await client.channels.fetch( + hasUpdates.channelId ?? channelId + )) as TextChannel; - const channel = await client.channels.fetch(hasUpdates.channelId ?? channelId) as TextChannel; - if (channel) { - channel.send(`<@${user}> has reached level ${level}!`); - } + if (channel) { + channel.send(`<@${user}> has reached level ${level}!`); + } } diff --git a/bot/src/utils/leaderboardEmbed.ts b/bot/src/utils/leaderboardEmbed.ts index 2267acd..5e1fca9 100644 --- a/bot/src/utils/leaderboardEmbed.ts +++ b/bot/src/utils/leaderboardEmbed.ts @@ -1,41 +1,48 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; + +import client from ".."; + import quickEmbed from "./quickEmbed"; import { getGuildLeaderboard } from "./requestAPI"; -import client from ".."; export default async function (guild: string, interaction?: any) { - const leaderboard = await getGuildLeaderboard(guild); - - if (leaderboard.length === 0) { - await interaction.reply('No leaderboard data available.'); - return; - } - - // Create a new embed using the custom embed function - const leaderboardEmbed = quickEmbed({ - color: 'Blurple', - title: `Leaderboard for ${interaction ? interaction.guild?.name : (await client.guilds.fetch(guild)).name}`, - description: 'Top 10 Users' - }, interaction); - - // Add a field for each user with a mention - leaderboard.leaderboard.slice(0, 10).forEach((entry: { id: string; xp: number; }, index: number) => { - leaderboardEmbed.addFields([ - { - name: `${index + 1}.`, - value: `<@${entry.id}>: ${entry.xp.toLocaleString("en-US")} XP`, - inline: false - } - ]); - }); - - const button = new ButtonBuilder() - .setLabel('Leaderboard') - .setURL(`https://chatr.fun/leaderboard/${guild}`) - .setStyle(ButtonStyle.Link); - - const row = new ActionRowBuilder() - .addComponents(button); - - return [leaderboardEmbed, row]; -} \ No newline at end of file + const leaderboard = await getGuildLeaderboard(guild); + + if (leaderboard.length === 0) { + await interaction.reply("No leaderboard data available."); + + return; + } + + // Create a new embed using the custom embed function + const leaderboardEmbed = quickEmbed( + { + color: "Blurple", + title: `Leaderboard for ${interaction ? interaction.guild?.name : (await client.guilds.fetch(guild)).name}`, + description: "Top 10 Users", + }, + interaction + ); + + // Add a field for each user with a mention + leaderboard.leaderboard + .slice(0, 10) + .forEach((entry: { id: string; xp: number }, index: number) => { + leaderboardEmbed.addFields([ + { + name: `${index + 1}.`, + value: `<@${entry.id}>: ${entry.xp.toLocaleString("en-US")} XP`, + inline: false, + }, + ]); + }); + + const button = new ButtonBuilder() + .setLabel("Leaderboard") + .setURL(`https://chatr.fun/leaderboard/${guild}`) + .setStyle(ButtonStyle.Link); + + const row = new ActionRowBuilder().addComponents(button); + + return { embed: leaderboardEmbed, row }; +} diff --git a/bot/src/utils/quickEmbed.ts b/bot/src/utils/quickEmbed.ts index a313a6b..570b844 100644 --- a/bot/src/utils/quickEmbed.ts +++ b/bot/src/utils/quickEmbed.ts @@ -1,23 +1,32 @@ -import { EmbedBuilder, type Client, type ColorResolvable, type CommandInteraction } from "discord.js"; +import { + EmbedBuilder, + type Client, + type ColorResolvable, + type CommandInteraction, +} from "discord.js"; export default function ( - { color, title, description }: { color: ColorResolvable; title: string; description: string }, - interaction?: CommandInteraction, - client?: Client + { + color, + title, + description, + }: { color: ColorResolvable; title: string; description: string }, + interaction?: CommandInteraction, + client?: Client ) { - return new EmbedBuilder() - .setColor(color) - .setTitle(title) - .setDescription(description) - .setTimestamp() - .setFooter({ - text: - interaction?.client.user.displayName ?? - client?.user?.displayName ?? - 'Chatr', - iconURL: - interaction?.client?.user?.avatarURL() ?? - client?.user?.avatarURL() ?? - 'https://cdn.discordapp.com/embed/avatars/0.png', - }); -} \ No newline at end of file + return new EmbedBuilder() + .setColor(color) + .setTitle(title) + .setDescription(description) + .setTimestamp() + .setFooter({ + text: + interaction?.client.user.displayName ?? + client?.user?.displayName ?? + "Chatr", + iconURL: + interaction?.client?.user?.avatarURL() ?? + client?.user?.avatarURL() ?? + "https://cdn.discordapp.com/embed/avatars/0.png", + }); +} diff --git a/bot/src/utils/requestAPI.ts b/bot/src/utils/requestAPI.ts index e016da3..9e3f1e2 100644 --- a/bot/src/utils/requestAPI.ts +++ b/bot/src/utils/requestAPI.ts @@ -1,174 +1,227 @@ import handleLevelChange from "./handleLevelChange"; -export async function makePOSTRequest(guild: string, user: string, channel: string | null, xp: number | null, pfp: string, name: string, nickname: string) { - xp = xp ?? 0 - await fetch(`http://localhost:18103/post/${guild}/${user}`, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - method: 'POST', - body: JSON.stringify({ xp, pfp, name, nickname }), - }).then(res => { - return res.json() - }).then(data => { - if (!channel) return - if (data.sendUpdateEvent) handleLevelChange(guild, user, channel, data.level) - }) +export async function makePOSTRequest( + guild: string, + user: string, + channel: string | null, + xp: number | null, + pfp: string, + name: string, + nickname: string +) { + xp = xp ?? 0; + await fetch(`http://localhost:18103/post/${guild}/${user}`, { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + method: "POST", + body: JSON.stringify({ xp, pfp, name, nickname }), + }) + .then((res) => { + return res.json(); + }) + .then((data) => { + if (!channel) return; + if (data.sendUpdateEvent) + handleLevelChange(guild, user, channel, data.level); + }); } export async function makeGETRequest(guild: string, user: string) { - const response = await fetch(`http://localhost:18103/get/${guild}/${user}`); + const response = await fetch(`http://localhost:18103/get/${guild}/${user}`); - if (!response.ok) { - console.error(`HTTP error! Status: ${response.status}`); - return null; - } + if (!response.ok) { + console.error(`HTTP error! Status: ${response.status}`); - const data = await response.json(); - return data; + return null; + } + + const data = await response.json(); + + return data; } export async function getGuildLeaderboard(guild: string) { - const response = await fetch(`http://localhost:18103/get/${guild}`) + const response = await fetch(`http://localhost:18103/get/${guild}`); + + if (!response.ok) { + console.error(`HTTP error! Status: ${response.status}`); - if (!response.ok) { - console.error(`HTTP error! Status: ${response.status}`); - return null; - } + return null; + } - const data = await response.json(); - return data; + const data = await response.json(); + + return data; } -export async function updateGuildInfo(guild: string, name: string, icon: string, members: number) { - await fetch(`http://localhost:18103/post/${guild}`, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - method: 'POST', - body: JSON.stringify({ name, icon, members }), - }).then(res => { - return res.json() - }).then(data => { - console.dir(data, { depth: null }) - }) +export async function updateGuildInfo( + guild: string, + name: string, + icon: string, + members: number +) { + await fetch(`http://localhost:18103/post/${guild}`, { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + method: "POST", + body: JSON.stringify({ name, icon, members }), + }) + .then((res) => { + return res.json(); + }) + .then((data) => { + console.dir(data, { depth: null }); + }); } export async function removeGuild(guild: string) { - await fetch(`http://localhost:18103/post/${guild}/remove`, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - method: 'POST', - }) + await fetch(`http://localhost:18103/post/${guild}/remove`, { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + method: "POST", + }); } export async function removeUser(guild: string, user: string) { - const response = await fetch(`http://localhost:18103/post/${guild}/${user}/remove`, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - method: 'POST', - }) - return response.status === 200; + const response = await fetch( + `http://localhost:18103/post/${guild}/${user}/remove`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + method: "POST", + } + ); + + return response.status === 200; } export async function setXP(guild: string, user: string, xp: number) { - const response = await fetch(`http://localhost:18103/admin/set/${guild}/xp`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({ extraData: { user, value: xp } }), - "method": "POST" - }); - return response.status === 200; + const response = await fetch( + `http://localhost:18103/admin/set/${guild}/xp`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({ extraData: { user, value: xp } }), + method: "POST", + } + ); + + return response.status === 200; } export async function setLevel(guild: string, user: string, level: number) { - const response = await fetch(`http://localhost:18103/admin/set/${guild}/level`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({ extraData: { user, value: level } }), - "method": "POST" - }); - return response.status === 200; + const response = await fetch( + `http://localhost:18103/admin/set/${guild}/level`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({ extraData: { user, value: level } }), + method: "POST", + } + ); + + return response.status === 200; } export async function getDBSize() { - const response = await fetch('http://localhost:18103/get/dbusage') - if (!response.ok) { - console.error(`HTTP error! Status: ${response.status}`); - return null; - } - return response.json(); + const response = await fetch("http://localhost:18103/get/dbusage"); + + if (!response.ok) { + console.error(`HTTP error! Status: ${response.status}`); + + return null; + } + + return response.json(); } export async function addUserToTrackingData(userId: string, guildId: string) { - await fetch(`http://localhost:18103/admin/tracking/${guildId}/${userId}`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "method": "POST" - }); + await fetch(`http://localhost:18103/admin/tracking/${guildId}/${userId}`, { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + method: "POST", + }); } //#region Roles export async function getRoles(guild: string) { - const response = await fetch(`http://localhost:18103/admin/roles/${guild}/get`, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - body: JSON.stringify({}), - referrerPolicy: 'strict-origin-when-cross-origin', - method: 'POST', - }); - - return response.status === 200 ? response.json() : {}; + const response = await fetch( + `http://localhost:18103/admin/roles/${guild}/get`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({}), + referrerPolicy: "strict-origin-when-cross-origin", + method: "POST", + } + ); + + return response.status === 200 ? response.json() : {}; } // export async function getRole(guild: string, role: string) { // TODO: Implement this? // } -export async function removeRole(guild: string, role: string): Promise { - const response = await fetch(`http://localhost:18103/admin/roles/${guild}/remove`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({ - extraData: { - role: role, - } - }), - "method": "POST" - }); - - return response.status === 200; +export async function removeRole( + guild: string, + role: string +): Promise { + const response = await fetch( + `http://localhost:18103/admin/roles/${guild}/remove`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({ + extraData: { + role: role, + }, + }), + method: "POST", + } + ); + + return response.status === 200; } -export async function addRole(guild: string, role: string, level: number): Promise { - const response = await fetch(`http://localhost:18103/admin/roles/${guild}/add`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({ - extraData: { - role: role, - level: level - } - }), - "method": "POST" - }); - - return response.status === 200; +export async function addRole( + guild: string, + role: string, + level: number +): Promise { + const response = await fetch( + `http://localhost:18103/admin/roles/${guild}/add`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({ + extraData: { + role: role, + level: level, + }, + }), + method: "POST", + } + ); + + return response.status === 200; } // export async function updateRole(guild: string, role: string, level: number) { // TODO: Implement this? @@ -177,99 +230,134 @@ export async function addRole(guild: string, role: string, level: number): Promi //#region Updates export async function getUpdatesChannel(guild: string) { - const response = await fetch(`http://localhost:18103/admin/updates/${guild}/get`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string - }, - "body": JSON.stringify({}), - "method": "POST" - }); - return response.status === 200 ? response.json() : {}; + const response = await fetch( + `http://localhost:18103/admin/updates/${guild}/get`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({}), + method: "POST", + } + ); + + return response.status === 200 ? response.json() : {}; } -export async function setUpdatesChannel(guild: string, channelId: string | null) { - const response = await fetch(`http://localhost:18103/admin/updates/${guild}/set`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({ extraData: { channelId } }), - "method": "POST" - }); - return response.status === 200; +export async function setUpdatesChannel( + guild: string, + channelId: string | null +) { + const response = await fetch( + `http://localhost:18103/admin/updates/${guild}/set`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({ extraData: { channelId } }), + method: "POST", + } + ); + + return response.status === 200; } export async function enableUpdates(guild: string) { - const response = await fetch(`http://localhost:18103/admin/updates/${guild}/enable`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({}), - "method": "POST" - }); - return response.status === 200; + const response = await fetch( + `http://localhost:18103/admin/updates/${guild}/enable`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({}), + method: "POST", + } + ); + + return response.status === 200; } export async function disableUpdates(guild: string) { - const response = await fetch(`http://localhost:18103/admin/updates/${guild}/disable`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({}), - "method": "POST" - }); - return response.status === 200; + const response = await fetch( + `http://localhost:18103/admin/updates/${guild}/disable`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({}), + method: "POST", + } + ); + + return response.status === 200; } export async function getAllGuildsWithUpdatesEnabled() { - const response = await fetch(`http://localhost:18103/admin/updates/all/get`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({}), - "method": "POST" - }); - return response.json(); + const response = await fetch( + `http://localhost:18103/admin/updates/all/get`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({}), + method: "POST", + } + ); + + return response.json(); } //#endregion //#region Cooldowns export async function getCooldown(guild: string) { - const response = await fetch(`http://localhost:18103/admin/cooldown/${guild}/get`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({}), - "method": "POST" - }); - return response.json(); + const response = await fetch( + `http://localhost:18103/admin/cooldown/${guild}/get`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({}), + method: "POST", + } + ); + + return response.json(); } export async function setCooldown(guild: string, cooldown: number) { - const response = await fetch(`http://localhost:18103/admin/cooldown/${guild}/set`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({ extraData: { cooldown } }), - "method": "POST" - }); - return response.status === 200; + const response = await fetch( + `http://localhost:18103/admin/cooldown/${guild}/set`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({ extraData: { cooldown } }), + method: "POST", + } + ); + + return response.status === 200; } //#endregion //#region Sync export async function syncFromBot(guild: string, bot: string) { - const response = await fetch(`http://localhost:18103/admin/sync/${guild}/${bot}`, { - "headers": { - 'Content-Type': 'application/json', - 'Authorization': process.env.AUTH as string, - }, - "body": JSON.stringify({}), - "method": "POST" - }); - return response.status === 200; + const response = await fetch( + `http://localhost:18103/admin/sync/${guild}/${bot}`, + { + headers: { + "Content-Type": "application/json", + Authorization: process.env.AUTH as string, + }, + body: JSON.stringify({}), + method: "POST", + } + ); + + return response.status === 200; } //#endregion diff --git a/bot/src/utils/sendAutoUpdates.ts b/bot/src/utils/sendAutoUpdates.ts index 414fde2..59261b1 100644 --- a/bot/src/utils/sendAutoUpdates.ts +++ b/bot/src/utils/sendAutoUpdates.ts @@ -1,19 +1,30 @@ import type { TextChannel } from "discord.js"; + import client from ".."; + import leaderboardEmbed from "./leaderboardEmbed"; import { getAllGuildsWithUpdatesEnabled } from "./requestAPI"; interface Guild { - id: string; - updates_channel_id: string; + id: string; + updates_channel_id: string; } export default async function () { - const allGuildsData = await getAllGuildsWithUpdatesEnabled() - - allGuildsData.forEach(async (guild: Guild) => { - const [embed, row] = await leaderboardEmbed(guild.id) - const channel = await client.channels.fetch(guild.updates_channel_id) as TextChannel; - await channel?.send({ embeds: [embed], components: [row] }); - }) -} \ No newline at end of file + const allGuildsData = await getAllGuildsWithUpdatesEnabled(); + + allGuildsData.forEach(async (guild: Guild) => { + const result = await leaderboardEmbed(guild.id); + + if (!result) return; + + const channel = (await client.channels.fetch( + guild.updates_channel_id + )) as TextChannel; + + await channel?.send({ + embeds: [result.embed], + components: [result.row], + }); + }); +} diff --git a/package.json b/package.json index cbe3a92..ba83206 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "dev": "turbo run dev", "lint": "eslint \"bot/**\" \"web/**\" \"api/**\" --ext .ts,.tsx -c .eslintrc.json", - "build": "cd web && bun run build" + "build": "cd web && bun run build" }, "devDependencies": { "@eslint/js": "^9.7.0", @@ -36,4 +36,4 @@ "peerDependencies": { "typescript": "^5.0.0" } -} +} \ No newline at end of file diff --git a/web/components/icons.tsx b/web/components/icons.tsx index 78bfa69..d96b745 100644 --- a/web/components/icons.tsx +++ b/web/components/icons.tsx @@ -1,192 +1,193 @@ import * as React from "react"; + import { IconSvgProps } from "@/types"; export const DiscordIcon: React.FC = ({ - size = 24, - width, - height, - ...props + size = 24, + width, + height, + ...props }) => { - return ( - - - - ); + return ( + + + + ); }; export const TwitterIcon: React.FC = ({ - size = 24, - width, - height, - ...props + size = 24, + width, + height, + ...props }) => { - return ( - - - - ); + return ( + + + + ); }; export const GithubIcon: React.FC = ({ - size = 24, - width, - height, - ...props + size = 24, + width, + height, + ...props }) => { - return ( - - - - ); + return ( + + + + ); }; export const MoonFilledIcon = ({ - size = 24, - width, - height, - ...props + size = 24, + width, + height, + ...props }: IconSvgProps) => ( - + ); export const SunFilledIcon = ({ - size = 24, - width, - height, - ...props + size = 24, + width, + height, + ...props }: IconSvgProps) => ( - + ); export const HeartFilledIcon = ({ - size = 24, - width, - height, - ...props + size = 24, + width, + height, + ...props }: IconSvgProps) => ( - + ); export const SearchIcon = (props: IconSvgProps) => ( - + ); export const NextUILogo: React.FC = (props) => { - const { width, height = 40 } = props; + const { width, height = 40 } = props; - return ( - - - - - - ); + return ( + + + + + + ); }; diff --git a/web/components/navbar.tsx b/web/components/navbar.tsx index 44e8efa..f9ee30b 100644 --- a/web/components/navbar.tsx +++ b/web/components/navbar.tsx @@ -1,67 +1,67 @@ import { - Link, - Navbar as NextUINavbar, - NavbarContent, - NavbarMenu, - NavbarMenuToggle, - NavbarBrand, - NavbarItem, - NavbarMenuItem, + Link, + Navbar as NextUINavbar, + NavbarContent, + NavbarMenu, + NavbarMenuToggle, + NavbarBrand, + NavbarItem, + NavbarMenuItem, } from "@nextui-org/react"; - import { link as linkStyles } from "@nextui-org/theme"; - -import { siteConfig } from "@/config/site"; import NextLink from "next/link"; import clsx from "clsx"; -import { - TwitterIcon, - GithubIcon, - DiscordIcon, -} from "@/components/icons"; +import { siteConfig } from "@/config/site"; +import { TwitterIcon, GithubIcon, DiscordIcon } from "@/components/icons"; export const Navbar = () => { - return ( - - - - -

Chatr

-
-
-
- {siteConfig.navItems.map((item) => ( - - - {item.label} - - - ))} -
-
+ return ( + + + + +

Chatr

+
+
+
+ {siteConfig.navItems.map((item) => ( + + + {item.label} + + + ))} +
+
- - - - - - - - - - - - - - {/* */} - - + + - - - - - - + + + + + + - -
- {siteConfig.navItems.map((item, index) => ( - - - {item.label} - - - ))} -
-
-
- ); + +
+ {siteConfig.navItems.map((item, index) => ( + + + {item.label} + + + ))} +
+
+
+ ); }; diff --git a/web/components/primitives.ts b/web/components/primitives.ts index fe8e997..8fe6f52 100644 --- a/web/components/primitives.ts +++ b/web/components/primitives.ts @@ -1,53 +1,53 @@ import { tv } from "tailwind-variants"; export const title = tv({ - base: "tracking-tight inline font-semibold", - variants: { - color: { - violet: "from-[#FF1CF7] to-[#b249f8]", - yellow: "from-[#FF705B] to-[#FFB457]", - blue: "from-[#5EA2EF] to-[#0072F5]", - cyan: "from-[#00b7fa] to-[#01cfea]", - green: "from-[#6FEE8D] to-[#17c964]", - pink: "from-[#FF72E1] to-[#F54C7A]", - foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", - }, - size: { - sm: "text-3xl lg:text-4xl", - md: "text-[2.3rem] lg:text-5xl leading-9", - lg: "text-4xl lg:text-6xl", - }, - fullWidth: { - true: "w-full block", - }, - }, - defaultVariants: { - size: "md", - }, - compoundVariants: [ - { - color: [ - "violet", - "yellow", - "blue", - "cyan", - "green", - "pink", - "foreground", - ], - class: "bg-clip-text text-transparent bg-gradient-to-b", - }, - ], + base: "tracking-tight inline font-semibold", + variants: { + color: { + violet: "from-[#FF1CF7] to-[#b249f8]", + yellow: "from-[#FF705B] to-[#FFB457]", + blue: "from-[#5EA2EF] to-[#0072F5]", + cyan: "from-[#00b7fa] to-[#01cfea]", + green: "from-[#6FEE8D] to-[#17c964]", + pink: "from-[#FF72E1] to-[#F54C7A]", + foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", + }, + size: { + sm: "text-3xl lg:text-4xl", + md: "text-[2.3rem] lg:text-5xl leading-9", + lg: "text-4xl lg:text-6xl", + }, + fullWidth: { + true: "w-full block", + }, + }, + defaultVariants: { + size: "md", + }, + compoundVariants: [ + { + color: [ + "violet", + "yellow", + "blue", + "cyan", + "green", + "pink", + "foreground", + ], + class: "bg-clip-text text-transparent bg-gradient-to-b", + }, + ], }); export const subtitle = tv({ - base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", - variants: { - fullWidth: { - true: "!w-full", - }, - }, - defaultVariants:{ - fullWidth: true, - }, + base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", + variants: { + fullWidth: { + true: "!w-full", + }, + }, + defaultVariants: { + fullWidth: true, + }, }); diff --git a/web/components/search.tsx b/web/components/search.tsx index 59e77b5..c718f80 100644 --- a/web/components/search.tsx +++ b/web/components/search.tsx @@ -1,39 +1,40 @@ -import { useState } from 'react'; -import { useRouter } from 'next/router'; +import { useState } from "react"; +import { useRouter } from "next/router"; + import { subtitle } from "@/components/primitives"; export const Search = () => { - const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(''); + const router = useRouter(); + const [searchQuery, setSearchQuery] = useState(""); - const handleSearch = () => { - if (searchQuery.trim() !== '') { - router.push(`/leaderboard/${searchQuery}`); - } - }; + const handleSearch = () => { + if (searchQuery.trim() !== "") { + router.push(`/leaderboard/${searchQuery}`); + } + }; - const handleInputChange = (event: React.ChangeEvent) => { - setSearchQuery(event.target.value); - }; + const handleInputChange = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + }; - return ( -
-
- + return ( +
+
+ - -
-
- ); -} \ No newline at end of file + +
+
+ ); +}; diff --git a/web/config/fonts.ts b/web/config/fonts.ts index b4411e2..120c402 100644 --- a/web/config/fonts.ts +++ b/web/config/fonts.ts @@ -1,11 +1,11 @@ -import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google" +import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google"; export const fontSans = FontSans({ - subsets: ["latin"], - variable: "--font-sans", -}) + subsets: ["latin"], + variable: "--font-sans", +}); export const fontMono = FontMono({ - subsets: ["latin"], - variable: "--font-mono", -}) + subsets: ["latin"], + variable: "--font-mono", +}); diff --git a/web/config/site.ts b/web/config/site.ts index c14b6e7..6abfa7f 100644 --- a/web/config/site.ts +++ b/web/config/site.ts @@ -1,29 +1,29 @@ export type SiteConfig = typeof siteConfig; export const siteConfig = { - name: "Chatr", - description: "Chatr is a next generation Discord XP bot.", - navItems: [ - { - label: "Home", - href: "/", - }, - { - label: "Dashboard", - href: "https://dashboard.chatr.fun", - }, - { - label: "Docs", - href: "https://docs.chatr.fun", - }, - // { - // label: "#", - // href: "#", - // }, - ], - links: { - github: "https://github.com/GalvinPython/chatr", - twitter: "https://twitter.com/reallygalvin", - discord: "https://discord.gg/fpJVTkVngm", - }, + name: "Chatr", + description: "Chatr is a next generation Discord XP bot.", + navItems: [ + { + label: "Home", + href: "/", + }, + { + label: "Dashboard", + href: "https://dashboard.chatr.fun", + }, + { + label: "Docs", + href: "https://docs.chatr.fun", + }, + // { + // label: "#", + // href: "#", + // }, + ], + links: { + github: "https://github.com/GalvinPython/chatr", + twitter: "https://twitter.com/reallygalvin", + discord: "https://discord.gg/fpJVTkVngm", + }, }; diff --git a/web/layouts/default.tsx b/web/layouts/default.tsx index b0c60c2..c795bc7 100644 --- a/web/layouts/default.tsx +++ b/web/layouts/default.tsx @@ -1,18 +1,17 @@ -import { Navbar } from "@/components/navbar"; import { Head } from "./head"; +import { Navbar } from "@/components/navbar"; + export default function DefaultLayout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - return ( -
- - -
- {children} -
-
- ); + return ( +
+ + +
{children}
+
+ ); } diff --git a/web/layouts/head.tsx b/web/layouts/head.tsx index 472be93..a021a2d 100644 --- a/web/layouts/head.tsx +++ b/web/layouts/head.tsx @@ -1,20 +1,21 @@ import React from "react"; import NextHead from "next/head"; + import { siteConfig } from "@/config/site"; export const Head = () => { - return ( - - {siteConfig.name} - - - - - - - ); + return ( + + {siteConfig.name} + + + + + + + ); }; diff --git a/web/next.config.mjs b/web/next.config.mjs index 5505f3b..3e18a9f 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -1,23 +1,23 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, - poweredByHeader: false, - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '**', - port: '', - pathname: '**', - }, - ], - }, - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, -} + reactStrictMode: true, + poweredByHeader: false, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "**", + port: "", + pathname: "**", + }, + ], + }, + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, + }, +}; -export default nextConfig +export default nextConfig; diff --git a/web/package.json b/web/package.json index 7df0808..7565764 100644 --- a/web/package.json +++ b/web/package.json @@ -1,33 +1,33 @@ { - "name": "@chatr/web", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --port 56413", - "build": "next build", - "start": "next start --port 56414", - "lint": "next lint" - }, - "dependencies": { - "@nextui-org/react": "^2.3.0", - "@types/node": "20.5.7", - "@types/react": "18.3.3", - "@types/react-dom": "18.3.0", - "autoprefixer": "10.4.19", - "clsx": "^2.0.0", - "framer-motion": "^11.1.1", - "highcharts": "^11.4.6", - "highcharts-react-official": "^3.2.1", - "intl-messageformat": "^10.5.0", - "next": "14.2.1", - "next-themes": "^0.3.0", - "postcss": "8.4.38", - "react": "18.3.1", - "react-dom": "18.3.1", - "react-odometer": "^0.0.1", - "react-odometerjs": "^3.1.3", - "tailwind-variants": "^0.2.1", - "tailwindcss": "3.4.3", - "typescript": "5.5.4" - } + "name": "@chatr/web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --port 56413", + "build": "next build", + "start": "next start --port 56414", + "lint": "next lint" + }, + "dependencies": { + "@nextui-org/react": "^2.3.0", + "@types/node": "20.5.7", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "autoprefixer": "10.4.19", + "clsx": "^2.0.0", + "framer-motion": "^11.1.1", + "highcharts": "^11.4.6", + "highcharts-react-official": "^3.2.1", + "intl-messageformat": "^10.5.0", + "next": "14.2.1", + "next-themes": "^0.3.0", + "postcss": "8.4.38", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-odometer": "^0.0.1", + "react-odometerjs": "^3.1.3", + "tailwind-variants": "^0.2.1", + "tailwindcss": "3.4.3", + "typescript": "5.5.4" + } } diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index edb01da..3c5a301 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -2,23 +2,24 @@ import type { AppProps } from "next/app"; import { NextUIProvider } from "@nextui-org/react"; import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { useRouter } from "next/router"; + import { fontSans, fontMono } from "@/config/fonts"; -import {useRouter} from 'next/router'; import "@/styles/globals.css"; export default function App({ Component, pageProps }: AppProps) { - const router = useRouter(); + const router = useRouter(); - return ( - - - - - - ); + return ( + + + + + + ); } export const fonts = { - sans: fontSans.style.fontFamily, - mono: fontMono.style.fontFamily, + sans: fontSans.style.fontFamily, + mono: fontMono.style.fontFamily, }; diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index da4618a..59e5eb5 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -1,13 +1,13 @@ -import { Html, Head, Main, NextScript } from 'next/document' +import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { - return ( - - - -
- - - - ) + return ( + + + +
+ + + + ); } diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 72d00c8..36508d2 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,118 +1,121 @@ import { Link } from "@nextui-org/link"; import { button as buttonStyles } from "@nextui-org/theme"; +import { Component } from "react"; + import { siteConfig } from "@/config/site"; import { title, subtitle } from "@/components/primitives"; import { GithubIcon } from "@/components/icons"; import DefaultLayout from "@/layouts/default"; import { Search } from "@/components/search"; -import { Component } from "react"; interface PageState { - success: boolean; - totalGuilds: number; - totalMembers: number; - trackedUsers: number; + success: boolean; + totalGuilds: number; + totalMembers: number; + trackedUsers: number; } interface PageProps { - success: boolean; - data: { - total_guilds: number; - total_members: number; - user_count: number; - }; + success: boolean; + data: { + total_guilds: number; + total_members: number; + user_count: number; + }; } class IndexPage extends Component { - - constructor(props: PageProps) { - super(props); - this.state = { - success: false, - totalGuilds: props.data.total_guilds, - totalMembers: props.data.total_members, - trackedUsers: props.data.user_count, - }; - } + constructor(props: PageProps) { + super(props); + this.state = { + success: false, + totalGuilds: props.data.total_guilds, + totalMembers: props.data.total_members, + trackedUsers: props.data.user_count, + }; + } - render() { - return ( - -
-
-

Chatr

-

.fun

-

- A next generation Discord XP bot. -

-

chatr.fun is not affiliated with Discord

-
+ render() { + return ( + +
+
+

Chatr

+

.fun

+

+ A next generation Discord XP bot. +

+

chatr.fun is not affiliated with Discord

+
-
- - - GitHub - -
+
+ + + GitHub + +
-
- -
+
+ +
-
-

Statistics

-

- Total Guilds: {this.state.totalGuilds} -

-

- Total Members: {this.state.totalMembers} -

-

- Tracked Users: {this.state.trackedUsers} -

-
-
-
- ); - } +
+

+ Statistics +

+

+ Total Guilds: {this.state.totalGuilds} +

+

+ Total Members: {this.state.totalMembers} +

+

+ Tracked Users: {this.state.trackedUsers} +

+
+
+
+ ); + } } export async function getServerSideProps() { - try { - const res = await fetch("http://localhost:18103/get/botinfo"); + try { + const res = await fetch("http://localhost:18103/get/botinfo"); + + if (res.ok) { + return { + props: { + success: true, + data: await res.json(), + }, + }; + } else { + return { + props: { + success: false, + data: null, + }, + }; + } + } catch (error) { + console.error(error); - if (res.ok) { - return { - props: { - success: true, - data: await res.json(), - }, - }; - } else { - return { - props: { - success: false, - data: null, - }, - }; - } - } catch (error) { - console.error(error); - return { - props: { - success: false, - data: null, - }, - }; - } + return { + props: { + success: false, + data: null, + }, + }; + } } -export default IndexPage; \ No newline at end of file +export default IndexPage; diff --git a/web/pages/leaderboard/[server].tsx b/web/pages/leaderboard/[server].tsx index 33906b7..3c53ccd 100644 --- a/web/pages/leaderboard/[server].tsx +++ b/web/pages/leaderboard/[server].tsx @@ -1,16 +1,17 @@ -import React, { Component } from 'react'; -import DefaultLayout from "@/layouts/default"; -import Highcharts from 'highcharts'; -import HighchartsReact from 'highcharts-react-official'; +import React, { Component } from "react"; +import Highcharts from "highcharts"; +import HighchartsReact from "highcharts-react-official"; import dynamic from "next/dynamic"; -import Image from 'next/image'; +import Image from "next/image"; import "odometer/themes/odometer-theme-default.css"; -import { ChartOptions, ChartPointsFormatted } from '@/types/chart'; -import { PropsGuilds } from '@/types/props'; -import { Leaderboard } from '@/types/leaderboard'; -import Link from 'next/link'; +import Link from "next/link"; + +import DefaultLayout from "@/layouts/default"; +import { Leaderboard } from "@/types/leaderboard"; +import { PropsGuilds } from "@/types/props"; +import { ChartOptions, ChartPointsFormatted } from "@/types/chart"; -const Odometer = dynamic(import('react-odometerjs'), { +const Odometer = dynamic(import("react-odometerjs"), { ssr: false, }); @@ -29,14 +30,16 @@ interface PageState { } class IndexPage extends Component { - - interval: Timer | null = null + interval: Timer | null = null; constructor(props: PropsGuilds) { super(props); this.state = { - urlToFetch: process.env.NODE_ENV === 'development' ? 'http://localhost:18103' : 'https://api.chatr.fun', + urlToFetch: + process.env.NODE_ENV === "development" + ? "http://localhost:18103" + : "https://api.chatr.fun", isLoading: true, discordGuildExists: props.discordGuildExists, discordGuildId: props.discordGuildId, @@ -48,64 +51,64 @@ class IndexPage extends Component { leaderboard: props.leaderboard, chartOptions: { chart: { - backgroundColor: 'transparent', + backgroundColor: "transparent", type: "line", - zoomType: 'x' + zoomType: "x", }, title: { text: "Total XP", style: { - color: 'gray', - font: "Roboto Medium" - } + color: "gray", + font: "Roboto Medium", + }, }, xAxis: { - type: 'datetime', + type: "datetime", tickPixelInterval: 150, labels: { style: { - color: 'gray', - font: "Roboto Medium" - } + color: "gray", + font: "Roboto Medium", + }, }, - visible: true + visible: true, }, yAxis: { gridLineColor: "gray", title: { - text: '' + text: "", }, labels: { style: { - color: 'gray', - font: "Roboto Medium" - } + color: "gray", + font: "Roboto Medium", + }, }, - visible: true + visible: true, }, plotOptions: { series: { threshold: null, fillOpacity: 0.25, animation: false, - lineWidth: 3 + lineWidth: 3, }, area: { - fillOpacity: 0.25 + fillOpacity: 0.25, }, }, credits: { enabled: true, text: "chatr.fun", - href: '#uwu' + href: "#uwu", }, time: { - useUTC: false + useUTC: false, }, tooltip: { shared: true, formatter(this: ChartPointsFormatted) { - if (!this.points || this.points.length === 0) return ''; + if (!this.points || this.points.length === 0) return ""; const point = this.points[0]; @@ -113,31 +116,43 @@ class IndexPage extends Component { const lastY = point.series.yData[index - 1]; const dif = point.y - lastY; - let r = Highcharts.dateFormat('%A %b %e, %H:%M:%S', new Date(point.x).getTime()) + + let r = + Highcharts.dateFormat( + "%A %b %e, %H:%M:%S", + new Date(point.x).getTime() + ) + '
\u25CF ' + - point.series.name + ': ' + Number(point.y).toLocaleString(); + point.series.name + + ": " + + Number(point.y).toLocaleString(); if (dif < 0) { - r += ' (' + - Number(dif).toLocaleString() + ')'; + r += + ' (' + + Number(dif).toLocaleString() + + ")"; } if (dif > 0) { - r += ' (+' + - Number(dif).toLocaleString() + ')'; + r += + ' (+' + + Number(dif).toLocaleString() + + ")"; } return r; - } + }, }, - series: [{ - name: 'Total XP', - data: [], - showInLegend: false, - marker: { enabled: false }, - color: '#FFF', - lineColor: '#4093f1', - lineWidth: 4 - }] + series: [ + { + name: "Total XP", + data: [], + showInLegend: false, + marker: { enabled: false }, + color: "#FFF", + lineColor: "#4093f1", + lineWidth: 4, + }, + ], }, }; } @@ -148,43 +163,49 @@ class IndexPage extends Component { return; } else { fetch(`${this.state.urlToFetch}/get/${this.state.discordGuildId}`) - .then(response => response.json()) - .then(data => { + .then((response) => response.json()) + .then((data) => { const points = data.totalXp; const leaderboard = data.leaderboard; // Update the chart data - this.setState(prevState => { + this.setState((prevState) => { const newDataPoint = [Date.now(), points]; - const updatedData = [...prevState.chartOptions.series[0].data, newDataPoint]; + const updatedData = [ + ...prevState.chartOptions.series[0].data, + newDataPoint, + ]; if (updatedData.length > 1800) { updatedData.shift(); } if (updatedData.length == 2) { - console.log(updatedData[1]) - if (updatedData[1][0] < (updatedData[0][0] + 1000)) { - updatedData.shift() + console.log(updatedData[1]); + if (updatedData[1][0] < updatedData[0][0] + 1000) { + updatedData.shift(); } } return { odometerPoints: points, - odometerMembersBeingTracked: data.leaderboard.length, + odometerMembersBeingTracked: + data.leaderboard.length, odometerMembers: data.guild.members, chartOptions: { ...prevState.chartOptions, - series: [{ - ...prevState.chartOptions.series[0], - data: updatedData as [number, number][], - }], + series: [ + { + ...prevState.chartOptions.series[0], + data: updatedData as [number, number][], + }, + ], }, leaderboard, // Update the leaderboard isLoading: false, // Reset isLoading flag }; }); }) - .catch(error => { + .catch((error) => { console.log(error); this.setState({ isLoading: false }); // Reset isLoading flag }); @@ -205,13 +226,21 @@ class IndexPage extends Component { } render() { - const { discordGuildExists, odometerPoints, odometerMembersBeingTracked, odometerMembers, chartOptions, leaderboard } = this.state; + const { + discordGuildExists, + odometerPoints, + odometerMembersBeingTracked, + odometerMembers, + chartOptions, + leaderboard, + } = this.state; if (!discordGuildExists) { // Redirect to 404 - if (typeof window != 'undefined') { - window.location.href = '/404'; + if (typeof window != "undefined") { + window.location.href = "/404"; } + return null; } @@ -221,14 +250,19 @@ class IndexPage extends Component {
User Avatar
-

{this.state.discordGuildName}

+

+ {this.state.discordGuildName} +

@@ -237,13 +271,21 @@ class IndexPage extends Component {
-

Realtime

+

+ Realtime +

-
Total XP
+
+ Total XP +
@@ -252,26 +294,41 @@ class IndexPage extends Component {
-
Members
+
+ Members +
- + +
+
+ Members Tracked
-
Members Tracked
- +
{/* Tracking */}
-

Tracking (Coming Soon)

+

+ Tracking (Coming Soon) +

@@ -280,7 +337,13 @@ class IndexPage extends Component {
-

Leaderboard

+

+ Leaderboard +

@@ -288,67 +351,106 @@ class IndexPage extends Component {
{leaderboard && leaderboard.length > 0 ? ( leaderboard.map((user, index) => { - const xpNeededNextLevel = user.xp_needed_next_level; - const totalXpForNextLevel = user.xp + xpNeededNextLevel; - const progressPercentage = user.progress_next_level; - - return ( - -
-
- {index + 1}. -
- {user.name} -
-

{user.nickname || user.name}

-
-
- - - - XP -
-
- - - - Level -
-
- - - - XP Needed -
-
-
-
- - {/* Progress Bar */} -
-
-
- {`${user.xp} / ${totalXpForNextLevel}`} - {`${progressPercentage}%`} -
-
- - ); + const xpNeededNextLevel = + user.xp_needed_next_level; + const totalXpForNextLevel = + user.xp + xpNeededNextLevel; + const progressPercentage = + user.progress_next_level; + + return ( + +
+
+ + {index + 1}. + +
+ {user.name} +
+

+ {user.nickname || user.name} +

+
+
+ + + + + XP + +
+
+ + + + + Level + +
+
+ + + + + XP Needed + +
+
+
+
+ + {/* Progress Bar */} +
+
+
+ {`${user.xp} / ${totalXpForNextLevel}`} + {`${progressPercentage}%`} +
+
+ + ); }) ) : ( -

No leaderboard data available.

+

+ No leaderboard data available. +

)}
- + ); } } -export async function getServerSideProps(context: { query: { server: string }; }) { +export async function getServerSideProps(context: { + query: { server: string }; +}) { const { server } = context.query; try { @@ -356,6 +458,7 @@ export async function getServerSideProps(context: { query: { server: string }; } if (response.ok) { const data = await response.json(); + return { props: { discordGuildExists: true, @@ -366,10 +469,11 @@ export async function getServerSideProps(context: { query: { server: string }; } odometerMembers: data.guild.members, odometerMembersBeingTracked: data.leaderboard.length, leaderboard: data.leaderboard, - } + }, }; } else { console.error("Error fetching profile:", response.statusText); + return { props: { discordGuildExists: false, @@ -380,11 +484,12 @@ export async function getServerSideProps(context: { query: { server: string }; } odometerMembers: null, odometerMembersBeingTracked: null, leaderboard: null, - } + }, }; } } catch (error) { console.error("Error fetching profile:", error); + return { props: { discordGuildExists: false, @@ -395,7 +500,7 @@ export async function getServerSideProps(context: { query: { server: string }; } odometerMembers: null, odometerMembersBeingTracked: null, leaderboard: null, - } + }, }; } } diff --git a/web/pages/leaderboard/[server]/[user].tsx b/web/pages/leaderboard/[server]/[user].tsx index 75f7dbd..f390b90 100644 --- a/web/pages/leaderboard/[server]/[user].tsx +++ b/web/pages/leaderboard/[server]/[user].tsx @@ -1,354 +1,429 @@ -import React, { Component } from 'react'; -import DefaultLayout from "@/layouts/default"; -import Highcharts from 'highcharts'; -import HighchartsReact from 'highcharts-react-official'; +import React, { Component } from "react"; +import Highcharts from "highcharts"; +import HighchartsReact from "highcharts-react-official"; import dynamic from "next/dynamic"; -import Image from 'next/image'; +import Image from "next/image"; + +import DefaultLayout from "@/layouts/default"; import "odometer/themes/odometer-theme-default.css"; -import { ChartOptions, ChartPointsFormatted } from '@/types/chart'; -import { PropsUsers } from '@/types/props'; +import { ChartOptions, ChartPointsFormatted } from "@/types/chart"; +import { PropsUsers } from "@/types/props"; -const Odometer = dynamic(import('react-odometerjs'), { - ssr: false, +const Odometer = dynamic(import("react-odometerjs"), { + ssr: false, }); interface PageState { - urlToFetch: string; - isLoading: boolean; - discordAccountExists: boolean; - discordUserId: string; - discordGuildId: string; - discordAvatarURL: string; - // discordBannerURL: string; (we do not have, but maybe in the future) - discordUsername: string; - discordDisplayName: string; - odometerPoints: number; - odometerLevel: number; - odometerPointsNeededToNextLevel: number; - odometerPointsNeededForNextLevel: number; - odometerProgressToNextLevelPercentage: number; - chartOptions: ChartOptions; + urlToFetch: string; + isLoading: boolean; + discordAccountExists: boolean; + discordUserId: string; + discordGuildId: string; + discordAvatarURL: string; + // discordBannerURL: string; (we do not have, but maybe in the future) + discordUsername: string; + discordDisplayName: string; + odometerPoints: number; + odometerLevel: number; + odometerPointsNeededToNextLevel: number; + odometerPointsNeededForNextLevel: number; + odometerProgressToNextLevelPercentage: number; + chartOptions: ChartOptions; } class IndexPage extends Component { + interval: Timer | null = null; + + constructor(props: PropsUsers) { + super(props); + + this.state = { + urlToFetch: + process.env.NODE_ENV === "development" + ? "http://localhost:18103" + : "https://api.chatr.fun", + isLoading: true, // Flag to indicate whether a request is in progress + discordAccountExists: props.discordAccountExists, + discordUserId: props.discordUserId, + discordGuildId: props.discordGuildId, + discordAvatarURL: props.discordAvatarURL, + discordUsername: props.discordUsername, + discordDisplayName: props.discordDisplayName, + odometerPoints: props.odometerPoints, + odometerLevel: props.odometerLevel, + odometerPointsNeededToNextLevel: + props.odometerPointsNeededToNextLevel, + odometerPointsNeededForNextLevel: + props.odometerPointsNeededForNextLevel, + odometerProgressToNextLevelPercentage: + props.odometerProgressToNextLevelPercentage, + chartOptions: { + chart: { + backgroundColor: "transparent", + type: "line", + zoomType: "x", + }, + title: { + text: "XP", + style: { + color: "gray", + font: "Roboto Medium", + }, + }, + xAxis: { + type: "datetime", + tickPixelInterval: 150, + labels: { + style: { + color: "gray", + font: "Roboto Medium", + }, + }, + visible: true, + }, + yAxis: { + gridLineColor: "gray", + title: { + text: "", + }, + labels: { + style: { + color: "gray", + font: "Roboto Medium", + }, + }, + visible: true, + }, + plotOptions: { + series: { + threshold: null, + fillOpacity: 0.25, + animation: false, + lineWidth: 3, + }, + area: { + fillOpacity: 0.25, + }, + }, + credits: { + enabled: true, + text: "chatr.fun", + href: "#uwu", + }, + time: { + useUTC: false, + }, + tooltip: { + shared: true, + formatter(this: ChartPointsFormatted) { + if (!this.points || this.points.length === 0) return ""; + + const point = this.points[0]; + + const index = point.series.xData.indexOf(point.x); + const lastY = point.series.yData[index - 1]; + const dif = point.y - lastY; + + let r = + Highcharts.dateFormat( + "%A %b %e, %H:%M:%S", + new Date(point.x).getTime() + ) + + '
\u25CF ' + + point.series.name + + ": " + + Number(point.y).toLocaleString(); + + if (dif < 0) { + r += + ' (' + + Number(dif).toLocaleString() + + ")"; + } + if (dif > 0) { + r += + ' (+' + + Number(dif).toLocaleString() + + ")"; + } + + return r; + }, + }, + series: [ + { + name: "Total XP", + data: [], + showInLegend: false, + marker: { enabled: false }, + color: "#FFF", + lineColor: "#4093f1", + lineWidth: 4, + }, + ], + }, + }; + } + + fetchData = () => { + console.log(this.state); + if (this.state.discordUserId == null) { + return; + } else { + fetch( + `${this.state.urlToFetch}/get/${this.state.discordGuildId}/${this.state.discordUserId}` + ) + .then((response) => response.json()) + .then((data) => { + const points = data.xp; + + // Update the chart data + this.setState((prevState) => { + const newDataPoint = [Date.now(), points]; + const updatedData = [ + ...prevState.chartOptions.series[0].data, + newDataPoint, + ]; + + if (updatedData.length > 1800) { + updatedData.shift(); + } + if (updatedData.length == 2) { + console.log(updatedData[1]); + if (updatedData[1][0] < updatedData[0][0] + 1000) { + updatedData.shift(); + } + } + + return { + odometerPoints: points, + odometerPointsNeededToNextLevel: + data.xp_needed_next_level, + odometerPointsNeededForNextLevel: + points + data.xp_needed_next_level, + odometerProgressToNextLevelPercentage: + data.progress_next_level, + odometerLevel: data.level, + chartOptions: { + ...prevState.chartOptions, + series: [ + { + ...prevState.chartOptions.series[0], + data: updatedData as [number, number][], + }, + ], + }, + isLoading: false, // Reset isLoading flag + }; + }); + }) + .catch((error) => { + console.log(error); + this.setState({ isLoading: false }); // Reset isLoading flag + }); + } + }; - interval: Timer | null = null - - constructor(props: PropsUsers) { - super(props); - - this.state = { - urlToFetch: process.env.NODE_ENV === 'development' ? 'http://localhost:18103' : 'https://api.chatr.fun', - isLoading: true, // Flag to indicate whether a request is in progress - discordAccountExists: props.discordAccountExists, - discordUserId: props.discordUserId, - discordGuildId: props.discordGuildId, - discordAvatarURL: props.discordAvatarURL, - discordUsername: props.discordUsername, - discordDisplayName: props.discordDisplayName, - odometerPoints: props.odometerPoints, - odometerLevel: props.odometerLevel, - odometerPointsNeededToNextLevel: props.odometerPointsNeededToNextLevel, - odometerPointsNeededForNextLevel: props.odometerPointsNeededForNextLevel, - odometerProgressToNextLevelPercentage: props.odometerProgressToNextLevelPercentage, - chartOptions: { - chart: { - backgroundColor: 'transparent', - type: "line", - zoomType: 'x' - }, - title: { - text: "XP", - style: { - color: 'gray', - font: "Roboto Medium" - } - }, - xAxis: { - type: 'datetime', - tickPixelInterval: 150, - labels: { - style: { - color: 'gray', - font: "Roboto Medium" - } - }, - visible: true - }, - yAxis: { - gridLineColor: "gray", - title: { - text: '' - }, - labels: { - style: { - color: 'gray', - font: "Roboto Medium" - } - }, - visible: true - }, - plotOptions: { - series: { - threshold: null, - fillOpacity: 0.25, - animation: false, - lineWidth: 3 - }, - area: { - fillOpacity: 0.25 - }, - }, - credits: { - enabled: true, - text: "chatr.fun", - href: '#uwu' - }, - time: { - useUTC: false - }, - tooltip: { - shared: true, - formatter(this: ChartPointsFormatted) { - if (!this.points || this.points.length === 0) return ''; - - const point = this.points[0]; - - const index = point.series.xData.indexOf(point.x); - const lastY = point.series.yData[index - 1]; - const dif = point.y - lastY; - - let r = Highcharts.dateFormat('%A %b %e, %H:%M:%S', new Date(point.x).getTime()) + - '
\u25CF ' + - point.series.name + ': ' + Number(point.y).toLocaleString(); - - if (dif < 0) { - r += ' (' + - Number(dif).toLocaleString() + ')'; - } - if (dif > 0) { - r += ' (+' + - Number(dif).toLocaleString() + ')'; - } - - return r; - } - }, - series: [{ - name: 'Total XP', - data: [], - showInLegend: false, - marker: { enabled: false }, - color: '#FFF', - lineColor: '#4093f1', - lineWidth: 4 - }] - }, - }; - } - - fetchData = () => { - console.log(this.state); - if (this.state.discordUserId == null) { - return; - } else { - fetch(`${this.state.urlToFetch}/get/${this.state.discordGuildId}/${this.state.discordUserId}`) - .then(response => response.json()) - .then(data => { - const points = data.xp; - - // Update the chart data - this.setState(prevState => { - const newDataPoint = [Date.now(), points]; - const updatedData = [...prevState.chartOptions.series[0].data, newDataPoint]; - - if (updatedData.length > 1800) { - updatedData.shift(); - } - if (updatedData.length == 2) { - console.log(updatedData[1]) - if (updatedData[1][0] < (updatedData[0][0] + 1000)) { - updatedData.shift() - } - } - - return { - odometerPoints: points, - odometerPointsNeededToNextLevel: data.xp_needed_next_level, - odometerPointsNeededForNextLevel: points + data.xp_needed_next_level, - odometerProgressToNextLevelPercentage: data.progress_next_level, - odometerLevel: data.level, - chartOptions: { - ...prevState.chartOptions, - series: [{ - ...prevState.chartOptions.series[0], - data: updatedData as [number, number][], - }], - }, - isLoading: false, // Reset isLoading flag - }; - }); - }) - .catch(error => { - console.log(error); - this.setState({ isLoading: false }); // Reset isLoading flag - }); - } - }; - - componentDidMount() { - this.fetchData(); // Fetch initial data when component mounts - - // Make the updating interval 5 seconds to prevent overloading the server and duplicate responses + componentDidMount() { + this.fetchData(); // Fetch initial data when component mounts + + // Make the updating interval 5 seconds to prevent overloading the server and duplicate responses this.interval = setInterval(this.fetchData, 5000); - } - - componentWillUnmount() { - if (this.interval) { - clearInterval(this.interval); // Clear interval when component unmounts - } - } - - - render() { - const { discordAccountExists, odometerPoints, odometerPointsNeededToNextLevel, odometerPointsNeededForNextLevel, odometerProgressToNextLevelPercentage, odometerLevel, chartOptions } = this.state; - - if (!discordAccountExists) { - // Redirect to 404 - if (typeof window != 'undefined') { - window.location.href = '/404'; - } - return null; - } - - return ( - -
-
- {/* +
+
+ {/* Banner */} -
- User Avatar -
-

{this.state.discordDisplayName}

-

{this.state.discordUsername}

-
-
-
- -
-
- -
-
XP
-
- -
-
-
- -
-
Points To Next Level
-
-
-
- -
-
Points For Next Level
-
-
-
- -
-
Progress To Next Level (%)
-
-
-
- -
-
Level
-
-
- -
- -
-
- - ); - } +
+ User Avatar +
+

+ {this.state.discordDisplayName} +

+

+ {this.state.discordUsername} +

+
+
+
+ +
+
+ +
+
XP
+
+ +
+
+
+ +
+
+ Points To Next Level +
+
+
+
+ +
+
+ Points For Next Level +
+
+
+
+ +
+
+ Progress To Next Level (%) +
+
+
+
+ +
+
+ Level +
+
+
+ +
+ +
+
+
+ ); + } } -export async function getServerSideProps(context: { query: { server: string; user: string; }; }) { - const { server, user } = context.query; - - try { - const response = await fetch(`http://localhost:18103/get/${server}/${user}`); - - if (response.ok) { - const data = await response.json(); - return { - props: { - discordAccountExists: true, - discordUserId: user, - discordGuildId: server, - discordAvatarURL: data.pfp, - discordUsername: data.name, - discordDisplayName: data.nickname, - odometerPoints: data.xp, - odometerLevel: data.level, - odometerPointsNeededToNextLevel: data.xp_needed_next_level, - odometerPointsNeededForNextLevel: Number(data.xp + data.xp_needed_next_level), - odometerProgressToNextLevelPercentage: data.progress_next_level, - } - }; - } else { - console.error("Error fetching profile:", response.statusText); - return { - props: { - discordAccountExists: false, - discordUserId: user, - discordGuildId: server, - discordAvatarURL: null, - discordUsername: null, - discordDisplayName: null, - odometerPoints: null, - odometerLevel: null, - odometerPointsNeededToNextLevel: null, - odometerPointsNeededForNextLevel: null, - odometerProgressToNextLevelPercentage: null, - } - }; - } - } catch (error) { - console.error("Error fetching profile:", error); - return { - props: { - discordAccountExists: false, - discordUserId: user, - discordGuildId: server, - discordAvatarURL: null, - discordUsername: null, - discordDisplayName: null, - odometerPoints: null, - odometerLevel: null, - odometerPointsNeededToNextLevel: null, - odometerPointsNeededForNextLevel: null, - odometerProgressToNextLevelPercentage: null, - } - }; - } +export async function getServerSideProps(context: { + query: { server: string; user: string }; +}) { + const { server, user } = context.query; + + try { + const response = await fetch( + `http://localhost:18103/get/${server}/${user}` + ); + + if (response.ok) { + const data = await response.json(); + + return { + props: { + discordAccountExists: true, + discordUserId: user, + discordGuildId: server, + discordAvatarURL: data.pfp, + discordUsername: data.name, + discordDisplayName: data.nickname, + odometerPoints: data.xp, + odometerLevel: data.level, + odometerPointsNeededToNextLevel: data.xp_needed_next_level, + odometerPointsNeededForNextLevel: Number( + data.xp + data.xp_needed_next_level + ), + odometerProgressToNextLevelPercentage: + data.progress_next_level, + }, + }; + } else { + console.error("Error fetching profile:", response.statusText); + + return { + props: { + discordAccountExists: false, + discordUserId: user, + discordGuildId: server, + discordAvatarURL: null, + discordUsername: null, + discordDisplayName: null, + odometerPoints: null, + odometerLevel: null, + odometerPointsNeededToNextLevel: null, + odometerPointsNeededForNextLevel: null, + odometerProgressToNextLevelPercentage: null, + }, + }; + } + } catch (error) { + console.error("Error fetching profile:", error); + + return { + props: { + discordAccountExists: false, + discordUserId: user, + discordGuildId: server, + discordAvatarURL: null, + discordUsername: null, + discordDisplayName: null, + odometerPoints: null, + odometerLevel: null, + odometerPointsNeededToNextLevel: null, + odometerPointsNeededForNextLevel: null, + odometerProgressToNextLevelPercentage: null, + }, + }; + } } -export default IndexPage; \ No newline at end of file +export default IndexPage; diff --git a/web/public/next.svg b/web/public/next.svg index 5174b28..894a78b 100644 --- a/web/public/next.svg +++ b/web/public/next.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + +; diff --git a/web/public/vercel.svg b/web/public/vercel.svg index d2f8422..e65b56d 100644 --- a/web/public/vercel.svg +++ b/web/public/vercel.svg @@ -1 +1,6 @@ - \ No newline at end of file + + +; diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts index ddce02a..66926a3 100644 --- a/web/tailwind.config.ts +++ b/web/tailwind.config.ts @@ -1,16 +1,17 @@ -import {nextui} from '@nextui-org/react'; -import type {Config} from 'tailwindcss'; +import type { Config } from "tailwindcss"; + +import { nextui } from "@nextui-org/react"; export default { - content: [ - './pages/**/*.{js,ts,jsx,tsx,mdx}', - './components/**/*.{js,ts,jsx,tsx,mdx}', - './app/**/*.{js,ts,jsx,tsx,mdx}', - '../node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' - ], - theme: { - extend: {}, - }, - darkMode: "class", - plugins: [nextui()], -} satisfies Config + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "../node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + darkMode: "class", + plugins: [nextui()], +} satisfies Config; diff --git a/web/tsconfig.json b/web/tsconfig.json index 8b8e581..335a4d4 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,23 +1,23 @@ { - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] } diff --git a/web/types/chart.d.ts b/web/types/chart.d.ts index 03fb366..c1da924 100644 --- a/web/types/chart.d.ts +++ b/web/types/chart.d.ts @@ -13,74 +13,74 @@ export interface ChartPointsFormatted { } export interface ChartOptions { - chart: { - backgroundColor: string; - type: string; - zoomType: string; - }; - title: { - text: string; - style: { - color: string; - font: string; - }; - }; - xAxis: { - type: string; - tickPixelInterval: number; - labels: { - style: { - color: string; - font: string; - }; - }; - visible: boolean; - }; - yAxis: { - gridLineColor: string; - title: { - text: string; - }; - labels: { - style: { - color: string; - font: string; - }; - }; - visible: boolean; - }; - plotOptions: { - series: { - threshold: null; - fillOpacity: number; - animation: boolean; - lineWidth: number; - }; - area: { - fillOpacity: number; - }; - }; - credits: { - enabled: boolean; - text: string; - href: string; - }; - time: { - useUTC: boolean; - }; - tooltip: { - shared: boolean; - formatter: (this: ChartPointsFormatted) => string; - }; - series: { - name: string; - data: [number, number][]; - showInLegend: boolean; - marker: { - enabled: boolean; - }; - color: string; - lineColor: string; - lineWidth: number; - }[]; -} \ No newline at end of file + chart: { + backgroundColor: string; + type: string; + zoomType: string; + }; + title: { + text: string; + style: { + color: string; + font: string; + }; + }; + xAxis: { + type: string; + tickPixelInterval: number; + labels: { + style: { + color: string; + font: string; + }; + }; + visible: boolean; + }; + yAxis: { + gridLineColor: string; + title: { + text: string; + }; + labels: { + style: { + color: string; + font: string; + }; + }; + visible: boolean; + }; + plotOptions: { + series: { + threshold: null; + fillOpacity: number; + animation: boolean; + lineWidth: number; + }; + area: { + fillOpacity: number; + }; + }; + credits: { + enabled: boolean; + text: string; + href: string; + }; + time: { + useUTC: boolean; + }; + tooltip: { + shared: boolean; + formatter: (this: ChartPointsFormatted) => string; + }; + series: { + name: string; + data: [number, number][]; + showInLegend: boolean; + marker: { + enabled: boolean; + }; + color: string; + lineColor: string; + lineWidth: number; + }[]; +} diff --git a/web/types/index.ts b/web/types/index.ts index 1a73f1b..f6db063 100644 --- a/web/types/index.ts +++ b/web/types/index.ts @@ -1,5 +1,5 @@ -import {SVGProps} from "react"; +import { SVGProps } from "react"; export type IconSvgProps = SVGProps & { - size?: number; + size?: number; }; diff --git a/web/types/leaderboard.d.ts b/web/types/leaderboard.d.ts index d749d17..ef2e761 100644 --- a/web/types/leaderboard.d.ts +++ b/web/types/leaderboard.d.ts @@ -1,11 +1,11 @@ export interface Leaderboard { - id: string; - guild_id: string; - name: string; - nickname: string; - pfp: string; - xp: number; - level: number; - xp_needed_next_level: number; - progress_next_level: string; -} \ No newline at end of file + id: string; + guild_id: string; + name: string; + nickname: string; + pfp: string; + xp: number; + level: number; + xp_needed_next_level: number; + progress_next_level: string; +} diff --git a/web/types/props.d.ts b/web/types/props.d.ts index 914bdd3..fdb1048 100644 --- a/web/types/props.d.ts +++ b/web/types/props.d.ts @@ -22,5 +22,5 @@ export interface PropsGuilds { odometerPoints: number; odometerMembers: number; odometerMembersBeingTracked: number; - leaderboard: Leaderboard[]; -} \ No newline at end of file + leaderboard: Leaderboard[]; +}