diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index d2789b3..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "env": { - "commonjs": true, - "es2021": true, - "node": true - }, - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": "latest" - }, - "rules": { - "indent": [ - "error", - "tab" - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ] - } -} diff --git a/.gitignore b/.gitignore index 55f867b..c9f06bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ -package-lock.json +# IDE/Editor files +.idea + +# Dependencies node_modules -.idea \ No newline at end of file + +# Lock files +pnpm-lock.yaml +pnpm-workspace.yaml \ No newline at end of file diff --git a/.npmignore b/.npmignore index 05607a1..8bdcfb2 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,20 @@ +# Configurations +eslint.config.cjs +.eslintrc.json +tsconfig.json +pnpm-lock.yaml +pnpm-workspace.yaml + +# Version control / CI +.github .gitignore + +# IDE / editor settings .vscode -Examples -.tgz \ No newline at end of file +.idea + +# Dependencies +node_modules + +# Documentation +Examples \ No newline at end of file diff --git a/README.md b/README.md index 60ec6af..8aa6ccc 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ -

- -

Simply-XP

+

+ Simply-XP logo +

-

A Simple, Easy and Beginner friendly XP System.
Supports Discord.JS

Developed by Rahuletto#0243 & Maintained by Abadima#6356

+A simple, beginner-friendly XP system that brings levels, ranks, and role rewards to Discord.js. -

- -
-
- + npm version + npm downloads + CodeFactor grade

-
+

+ Docs + Discord support +

-## 🖥️ How to install ? +## 🖥️ How to install? ``` npm install simply-xp @@ -26,32 +27,24 @@ npm install simply-xp yarn add simply-xp ``` -
- -# 🎉 Recent Updates 🎉 +## 🎉 Recent Updates - Fixed Various Bugs - Updated `Chart.js` - D.JS v14 Support +- Patched v1.3.5 Bugs +# ⭐ Features -# But Why ? - -- Easiest XP System in Discord.js -- Fastest Support ever -- Rank Card Maker +- Easy XP System +- Automatic Role Assignment +- Beautiful Rank Cards +- Optimized for Performance +- Lightweight & Flexible - Beginner Friendly -- Easier than other XP Systems -- Inbuilt Auto Roles system -- Flexible and simple at the same time -
+## **Need Help?** Join the [Discord Server](https://discord.gg/3JzDV9T5Fn) -## **Need Help ? Join the [Discord Server](https://discord.gg/3JzDV9T5Fn)** +## 👥 Contact & Support -
- -

👥 Contact us | Support

-

- -

\ No newline at end of file +[![Discord invite](https://invidget.switchblade.xyz/3JzDV9T5Fn)](https://discord.gg/3JzDV9T5Fn) diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 0000000..3745651 --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,28 @@ +module.exports = [ + { + files: ["src/**/*.js", "src/*.js", "*.js"], + languageOptions: { + globals: { + require: "readonly", + module: "readonly", + __dirname: "readonly", + __filename: "readonly", + process: "readonly", + console: "readonly" + }, + parserOptions: { + ecmaVersion: "latest", + sourceType: "script" + } + }, + rules: { + indent: ["error", "tab"], + "linebreak-style": ["error", "unix"], + quotes: ["error", "double"], + semi: ["error", "always"] + } + }, + { + ignores: ["node_modules/**"] + } +]; \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 0c0db0e..753bbf4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,107 +1,108 @@ -import { Message, Client } from 'discord.js' +import { Message } from "discord.js"; -type HexColorString = `#${string}` | string +type HexColorString = `#${string}` | string; export type connectOptions = { - notify?: boolean -} + auto_purge?: boolean; + notify?: boolean; +}; export declare function connect( db: string, options?: connectOptions -): Promise +): Promise; export declare function addLevel( message: Message, userID: string, guildID: string, level: number -): Promise +): Promise; export declare function addXP( message: Message, userID: string, guildID: string, xp: number -): Promise +): Promise; export type chartsOptions = { - position?: string - background?: HexColorString - type?: 'bar' | 'line' | 'radar' | 'doughnut' | 'polarArea' -} + position?: string; + background?: HexColorString; + type?: "bar" | "line" | "radar" | "doughnut" | "polarArea"; +}; export declare function charts( message: Message, options?: chartsOptions -): Promise +): Promise; -export declare function create(userID: string, guildID: string): Promise +export declare function create(userID: string, guildID: string): Promise; -export declare function fetch(userID: string, guildID: string): Promise +export declare function fetch(userID: string, guildID: string): Promise; export declare function leaderboard( userID: string, guildID: string, limit?: number -): Promise +): Promise; export declare function lvlRole( message: Message, userID: string, guildID: string -): Promise +): Promise; export type rankOptions = { - slash?: boolean - background?: string - color?: HexColorString -} + slash?: boolean; + background?: string; + color?: HexColorString; +}; export declare function rank( message: Message, userID: string, guildID: string, options?: rankOptions -): Promise +): Promise; -export declare function reset(userID: string, guildID: string): Promise +export declare function reset(userID: string, guildID: string): Promise; export type lvladdOptions = { - level: string - role: string -} + level: string; + role: string; +}; export type lvlremoveOptions = { - level: string -} + level: string; +}; export declare function setLevel( message: Message, userID: string, guildID: string, level: number -): Promise +): Promise; export declare function setXP( userID: string, guildID: string, xp: number -): Promise +): Promise; /** For levelUp event */ export type Data = { - xp: string - level: number - userID: string - guildID: string -} + xp: string; + level: number; + userID: string; + guildID: string; +}; export type Role = { - lvl: string - role: string -} + lvl: string; + role: string; +}; -declare module 'discord.js' { +declare module "discord.js" { export interface ClientEvents { - levelUp: [Message, Data, Role] + levelUp: [Message, Data, Role]; } } diff --git a/package.json b/package.json index 44adc60..fc62403 100644 --- a/package.json +++ b/package.json @@ -1,53 +1,80 @@ { "name": "simply-xp", - "version": "1.3.5-beta-7", + "version": "1.3.8-fix.0", "description": "A Simple, Easy and Beginner Friendly XP System", "main": "simplyxp.js", "typings": "index.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "beta": "eslint . --fix && npm update && npm publish --tag beta" + "beta": "eslint . --fix && pnpm update && npm publish --tag beta", + "legacy": "eslint . --fix && pnpm update && npm publish --tag legacy" }, - "author": "Rahuletto", + "author": { + "name": "Abadima", + "url": "https://abadima.dev" + }, + "maintainers": [ + { + "name": "Abadima", + "url": "https://abadima.dev" + } + ], + "contributors": [ + { + "name": "Rahuletto", + "role": "Original Author", + "url": "https://marban.is-a.dev/" + } + ], + "license": "Apache-2.0", "keywords": [ "xp", - "simply", - "easy", - "xp-system", - "system", - "mongoose", + "experience", + "level", + "leveling", + "leveling system", "discord", - "discord xp", - "discord-xp", - "simply-djs", + "discord bot", "discord.js", - "simplydjs", - "weky", - "nuggies", - "experience", - "level role", - "amari", - "package", + "discord-xp", "leaderboard", - "mee6", + "rank", + "rank card", + "leaderboard card", + "gamification", + "xp system", + "simply", + "simplydjs", "charts", - "ChartJS", - "level up" + "@napi-rs/canvas", + "chart.js", + "mongoose", + "mongodb", + "sqlite", + "nodejs", + "bot", + "mee6", + "amari", + "weky" ], - "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/Rahuletto/simply-xp" + "url": "git+https://github.com/Abadima/simply-xp.git" + }, + "homepage": "https://simplyxp.js.org", + "bugs": { + "url": "https://github.com/Abadima/simply-xp/issues" }, + "readme": "README.md", "dependencies": { - "@napi-rs/canvas": "^0.1.41", + "@napi-rs/canvas": "0.1.88", "chart.js": "^3.9.1", - "mongoose": "^6.11.2" + "mongoose": "^7.8.8" }, "peerDependencies": { "discord.js": ">=13.12.0" }, "devDependencies": { - "eslint": "^8.43.0" + "eslint": "^9.39.2" } -} +} \ No newline at end of file diff --git a/simplyxp.js b/simplyxp.js index fccbbac..7f48dc5 100644 --- a/simplyxp.js +++ b/simplyxp.js @@ -1,31 +1,33 @@ try { - require('discord.js'); + require("discord.js"); } catch (e) { - console.warn('[XP] Discord.js is recommended for this package.'); + console.warn("[XP] Discord.js is recommended for this package."); } -module.exports.roleSetup = require('./src/roleSetup'); +module.exports.options = { auto_purge: false }; -module.exports.addLevel = require('./src/addLevel'); +module.exports.roleSetup = require("./src/roleSetup"); -module.exports.addXP = require('./src/addXP'); +module.exports.addLevel = require("./src/addLevel"); -module.exports.charts = require('./src/charts'); +module.exports.addXP = require("./src/addXP"); -module.exports.connect = require('./src/connect'); +module.exports.charts = require("./src/charts"); -module.exports.create = require('./src/create'); +module.exports.connect = require("./src/connect"); -module.exports.fetch = require('./src/fetch'); +module.exports.create = require("./src/create"); -module.exports.leaderboard = require('./src/leaderboard'); +module.exports.fetch = require("./src/fetch"); -module.exports.lvlRole = require('./src/lvlRole'); +module.exports.leaderboard = require("./src/leaderboard"); -module.exports.rank = require('./src/rank'); +module.exports.lvlRole = require("./src/lvlRole"); -module.exports.setLevel = require('./src/setLevel'); +module.exports.rank = require("./src/rank"); -module.exports.setXP = require('./src/setXP'); +module.exports.setLevel = require("./src/setLevel"); -module.exports.reset = require('./src/reset'); +module.exports.setXP = require("./src/setXP"); + +module.exports.reset = require("./src/reset"); \ No newline at end of file diff --git a/src/Fonts/Baloo-Regular.ttf b/src/Fonts/Baloo-Regular.ttf deleted file mode 100644 index a3ab1c5..0000000 Binary files a/src/Fonts/Baloo-Regular.ttf and /dev/null differ diff --git a/src/Fonts/Baloo2-Regular.woff2 b/src/Fonts/Baloo2-Regular.woff2 new file mode 100644 index 0000000..7d602f2 Binary files /dev/null and b/src/Fonts/Baloo2-Regular.woff2 differ diff --git a/src/addLevel.js b/src/addLevel.js index bf49121..411e778 100644 --- a/src/addLevel.js +++ b/src/addLevel.js @@ -1,5 +1,6 @@ -const levels = require('../src/models/level.js'); -let {roleSetup} = require('../simplyxp'); +const levels = require("../src/models/level.js"); +const notifyLevelUp = require("./utils/levelUpNotifier"); +const buildLevelPayload = require("./utils/levelPayload"); /** * @param {Discord.Message} message @@ -8,15 +9,13 @@ let {roleSetup} = require('../simplyxp'); * @param {number} level */ async function addLevel(message, userID, guildID, level) { - if (!userID) throw new Error('[XP] User ID was not provided.'); + if (!userID) throw new Error("[XP] User ID was not provided."); - if (!guildID) throw new Error('[XP] Guild ID was not provided.'); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); - if (!level) throw new Error('[XP] Level amount is not provided.'); + if (level == null || Number.isNaN(Number(level))) throw new Error("[XP] Invalid level amount."); - let {client} = message; - - const user = await levels.findOne({user: userID, guild: guildID}); + const user = await levels.findOne({ user: userID, guild: guildID }); if (!user) { const newUser = new levels({ @@ -26,7 +25,7 @@ async function addLevel(message, userID, guildID, level) { level: 0 }); - await newUser.save().catch(() => console.log('[XP] Failed to save new user to database')); + await newUser.save().catch(() => console.log("[XP] Failed to save new user to database")); let xp = (level * 10) ** 2; @@ -44,17 +43,10 @@ async function addLevel(message, userID, guildID, level) { console.log(`[XP] Failed to add Level | User: ${userID} | Err: ${e}`) ); - if (level1 !== level) { - let data = { - xp: user.xp, - level: user.level, - userID, - guildID - }; - - let role = await roleSetup.find(client, guildID, level); + if (level1 !== user.level) { + const data = buildLevelPayload(user, userID, guildID); - client.emit('levelUp', message, data, role); + await notifyLevelUp(message, data, user.level); } return { diff --git a/src/addXP.js b/src/addXP.js index 34128e0..7cd9f83 100644 --- a/src/addXP.js +++ b/src/addXP.js @@ -1,5 +1,5 @@ -const levels = require('../src/models/level.js'); -let {roleSetup} = require('../simplyxp'); +const levels = require("../src/models/level.js"); +let { roleSetup } = require("../simplyxp"); /** * @param {Discord.Message} message @@ -9,45 +9,45 @@ let {roleSetup} = require('../simplyxp'); */ async function addXP(message, userID, guildID, xp) { - if (!userID) throw new Error('[XP] User ID was not provided.'); + if (!userID) throw new Error("[XP] User ID was not provided."); - if (!guildID) throw new Error('[XP] Guild ID was not provided.'); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); - if (!xp) throw new Error('[XP] XP amount is not provided.'); + if (xp == null || Number.isNaN(Number(xp))) throw new Error("[XP] Invalid XP amount."); - let {client} = message; + let { client } = message; let min; let max; if (xp.min) { if (!xp.max) throw new Error( - '[XP] XP min amount is provided but max amount is not provided.' + "[XP] XP min amount is provided but max amount is not provided." ); min = Number(xp.min); - if (Number(xp.min).toString() === 'NaN') - throw new Error('[XP] XP amount (min) is not a number.'); + if (Number(xp.min).toString() === "NaN") + throw new Error("[XP] XP amount (min) is not a number."); } if (xp.max) { if (!xp.min) throw new Error( - '[XP] XP max amount is provided but min amount is not provided.' + "[XP] XP max amount is provided but min amount is not provided." ); max = Number(xp.max); - if (Number(xp.max).toString() === 'NaN') - throw new Error('[XP] XP amount (max) is not a number.'); + if (Number(xp.max).toString() === "NaN") + throw new Error("[XP] XP amount (max) is not a number."); } if (xp.min && xp.max) { xp = Math.floor(Math.random() * (max - min) + min); } - const user = await levels.findOne({user: userID, guild: guildID}); + const user = await levels.findOne({ user: userID, guild: guildID }); let lvl = Math.floor(0.1 * Math.sqrt(xp)); @@ -60,7 +60,7 @@ async function addXP(message, userID, guildID, xp) { level: lvl }); - await newUser.save().catch(() => console.log('[XP] Failed to save new user to database')); + await newUser.save().catch(() => console.log("[XP] Failed to save new user to database")); return { level: 0, @@ -92,7 +92,7 @@ async function addXP(message, userID, guildID, xp) { let role = await roleSetup.find(client, guildID, level); - client.emit('levelUp', message, data, role); + client.emit("levelUp", message, data, role); } return { diff --git a/src/charts.js b/src/charts.js index bca84dc..b2a0565 100644 --- a/src/charts.js +++ b/src/charts.js @@ -1,19 +1,14 @@ -let leaderboard = require('./leaderboard'); +let leaderboard = require("./leaderboard"); /** * @param {Discord.Message} message - * @param {import('../index').chartsOptions} options + * @param {import("../index").chartsOptions} options */ async function charts(message, options) { - try { - require('canvas'); - } catch { - throw '[XP] This requires canvas to be installed. \n"npm install canvas"'; - } - const ChartJS = require('chart.js'); - const Canvas = require('canvas'); - let {client} = message; + const ChartJS = require("chart.js"); + const Canvas = require("@napi-rs/canvas"); + let { client } = message; let data = []; let pos = options?.position || 5; @@ -30,30 +25,30 @@ async function charts(message, options) { }); new ChartJS(ctx, { - type: options.type || 'bar', + type: options.type || "bar", data: { labels: uzern, datasets: [ { - label: 'Leaderboards', + label: "Leaderboards", data: data, backgroundColor: [ - 'rgba(255, 99, 132, 0.5)', - 'rgba(255, 159, 64, 0.5)', - 'rgba(255, 205, 86, 0.5)', - 'rgba(75, 192, 192, 0.5)', - 'rgba(54, 162, 235, 0.5)', - 'rgba(153, 102, 255, 0.5)', - 'rgb(201, 203, 207, 0.5)' + "rgba(255, 99, 132, 0.5)", + "rgba(255, 159, 64, 0.5)", + "rgba(255, 205, 86, 0.5)", + "rgba(75, 192, 192, 0.5)", + "rgba(54, 162, 235, 0.5)", + "rgba(153, 102, 255, 0.5)", + "rgb(201, 203, 207, 0.5)" ], borderColor: [ - 'rgb(255, 99, 132)', - 'rgb(255, 159, 64)', - 'rgb(255, 205, 86)', - 'rgb(75, 192, 192)', - 'rgb(54, 162, 235)', - 'rgb(153, 102, 255)', - 'rgb(201, 203, 207)' + "rgb(255, 99, 132)", + "rgb(255, 159, 64)", + "rgb(255, 205, 86)", + "rgb(75, 192, 192)", + "rgb(54, 162, 235)", + "rgb(153, 102, 255)", + "rgb(201, 203, 207)" ], borderWidth: 2 } @@ -64,18 +59,18 @@ async function charts(message, options) { plugins: { title: { display: true, - text: 'XP Datasheet' + text: "XP Datasheet" } } }, plugins: [ { - id: 'simply-xp', + id: "simply-xp", beforeDraw: (chart) => { - const ctx = chart.canvas.getContext('2d'); + const ctx = chart.canvas.getContext("2d"); ctx.save(); - ctx.globalCompositeOperation = 'destination-over'; - ctx.fillStyle = options.background || '#2F3136'; + ctx.globalCompositeOperation = "destination-over"; + ctx.fillStyle = options.background || "#2F3136"; ctx.fillRect(0, 0, chart.width, chart.height); ctx.restore(); } @@ -84,8 +79,8 @@ async function charts(message, options) { }).update(); return { - attachment: ctx.toBuffer('image/png'), - name: 'chart.png' + attachment: ctx.toBuffer("image/png"), + name: "chart.png" }; } diff --git a/src/connect.js b/src/connect.js index b4b0a7c..e20aba5 100644 --- a/src/connect.js +++ b/src/connect.js @@ -1,22 +1,26 @@ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); +const simplyxp = require("../simplyxp"); /** * @param {string} db - * @param {import('../index').connectOptions} options + * @param {import("../index").connectOptions} options */ -async function connect(db, options = []) { - if (!db) throw new Error('[XP] Database URL was not provided'); - mongoose.set('strictQuery', true); - +async function connect(db, options = {}) { + if (!db) throw new Error("[XP] Database URL was not provided"); + mongoose.set("strictQuery", true); + mongoose.connect(db, { useNewUrlParser: true, useUnifiedTopology: true }); + // Propagate auto_purge option to module options + if (options.auto_purge !== undefined) { + simplyxp.options.auto_purge = options.auto_purge; + } - if (options.notify === false) return; - else return console.log('{ XP } Database Connected'); + if (options.notify) return console.log("{ XP } Database Connected"); } module.exports = connect; \ No newline at end of file diff --git a/src/create.js b/src/create.js index c46e875..9889cba 100644 --- a/src/create.js +++ b/src/create.js @@ -1,4 +1,4 @@ -const levels = require('../src/models/level.js'); +const levels = require("../src/models/level.js"); /** * @param {string} userID @@ -6,9 +6,9 @@ const levels = require('../src/models/level.js'); */ async function create(userID, guildID) { - if (!userID) throw new Error('[XP] User ID was not provided.'); + if (!userID) throw new Error("[XP] User ID was not provided."); - if (!guildID) throw new Error('[XP] User ID was not provided.'); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); let uzer = await levels.findOne({ user: userID, guild: guildID }); @@ -20,7 +20,7 @@ async function create(userID, guildID) { }); await newuser .save() - .catch(() => console.log('[XP] Failed to save new use to database')); + .catch(() => console.log("[XP] Failed to save new use to database")); return true; } diff --git a/src/fetch.js b/src/fetch.js index ec062c9..c6c4d33 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -1,4 +1,6 @@ -const levels = require('../src/models/level.js'); +const levels = require("../src/models/level.js"); +const getUserPosition = require("./utils/getUserPosition"); +const shortener = require("./utils/shortener"); /** * @param {string} userID @@ -6,9 +8,9 @@ const levels = require('../src/models/level.js'); */ async function fetch(userID, guildID) { - if (!userID) throw new Error('[XP] User ID was not provided.'); + if (!userID) throw new Error("[XP] User ID was not provided."); - if (!guildID) throw new Error('[XP] Guild ID was not provided.'); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); let user = await levels.findOne({ user: userID, @@ -25,38 +27,12 @@ async function fetch(userID, guildID) { await user.save(); } - const leaderboard = await levels - .find({ - guild: guildID - }) - .sort([['xp', 'descending']]) - .exec(); - - if (user === null) - return { - level: 0, - xp: 0, - reqxp: 100, - rank: leaderboard.findIndex((i) => i.user === userID) + 1, - shortxp: 0, - shortreq: 100 - }; - - user.position = leaderboard.findIndex((i) => i.user === userID) + 1; + user.position = (await getUserPosition(userID, guildID)) || 1; let targetxp = user.level + 1; let target = targetxp * targetxp * 100; - function shortener(count) { - const COUNT_ABBRS = ['', 'k', 'M', 'T']; - - const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); - let result = parseFloat((count / Math.pow(1000, i)).toFixed(2)); - result += `${COUNT_ABBRS[i]}`; - return result; - } - let shortXP = shortener(user.xp); let shortReqXP = shortener(target); diff --git a/src/leaderboard.js b/src/leaderboard.js index 99b32db..ee1a2cf 100644 --- a/src/leaderboard.js +++ b/src/leaderboard.js @@ -1,4 +1,6 @@ -const levels = require('../src/models/level.js'); +const levels = require("../src/models/level.js"); +const shortener = require("./utils/shortener"); +const { options } = require("../simplyxp"); /** * @param {Discord.Client} client @@ -7,46 +9,87 @@ const levels = require('../src/models/level.js'); */ async function leaderboard(client, guildID, limit) { - if (!guildID) throw new Error('[XP] Guild ID was not provided.'); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); let g = client.guilds.cache.get(guildID); - if (!g) throw new Error('[XP] Guild was not found.'); + if (!g) throw new Error("[XP] Guild was not found."); - let leaderboard = await levels.find({guild: guildID}).sort([['xp', 'descending']]); + const leaderboard = await levels + .find({ guild: guildID }) + .sort([["xp", "descending"]]); - let led = []; + const led = []; + let subtractPos = 0; + let entryIndex = 0; + const shouldPurge = Boolean(options?.auto_purge); + const limitNumber = limit ? Number(limit) : null; - function shortener(count) { - const COUNT_ABBRS = ['', 'k', 'M', 'T']; + // Fetch all members concurrently to avoid sequential awaits in the loop + const entriesWithMembers = await Promise.all( + leaderboard.map(async (rawEntry) => { + if (!rawEntry || typeof rawEntry !== "object") { + return { rawEntry: null, member: null }; + } - const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); - let result = parseFloat((count / Math.pow(1000, i)).toFixed(2)); - result += `${COUNT_ABBRS[i]}`; - return result; - } + const normalizedEntry = + typeof rawEntry.toObject === "function" ? rawEntry.toObject() : rawEntry; + + const { user: userID } = normalizedEntry; + let member = null; + if (userID) { + member = await g.members.fetch(userID).catch(() => null); + } + + return { rawEntry: normalizedEntry, member }; + }) + ); + + // Collect entries to purge for batch deletion + const entriesToPurge = []; + + for (const entry of entriesWithMembers) { + entryIndex += 1; + + const { rawEntry, member } = entry; + if (!rawEntry) { + continue; + } - const led2 = leaderboard.map(async (key) => { - const user = await g.members.fetch(key.user).catch(() => null); - if (!user) return levels.deleteOne({user: key.user, guild: guildID}); - if (key.xp === 0) return; - let pos = leaderboard.indexOf(key) + 1; + const { guild: entryGuildID, user: userID, xp, level } = rawEntry; + if (!member && shouldPurge) { + entriesToPurge.push({ user: userID, guild: entryGuildID }); + } + if (xp === 0 || !member) { + subtractPos += 1; + continue; + } - if (limit) { - if (pos > Number(limit)) return; + const pos = entryIndex - subtractPos; + if (limitNumber && pos > limitNumber) { + if (!shouldPurge) break; + continue; } led.push({ - guildID: key.guild, - userID: key.user, - xp: key.xp, - shortxp: shortener(key.xp), - level: key.level, + guildID: entryGuildID, + userID, + xp, + shortxp: shortener(xp), + level, position: pos, - username: user.user.username, - tag: user.user.tag + username: member.user.username, + tag: member.user.tag }); - }); - return Promise.all(led2).then(() => led); + } + + // Batch delete purged entries + if (entriesToPurge.length > 0) { + await levels.deleteMany({ + $or: entriesToPurge + }); + } + + return led; } module.exports = leaderboard; \ No newline at end of file diff --git a/src/lvlRole.js b/src/lvlRole.js index 17e9207..a3a5eeb 100644 --- a/src/lvlRole.js +++ b/src/lvlRole.js @@ -1,5 +1,5 @@ -const levels = require('../src/models/level.js'); -const lrole = require('../src/models/lvlrole.js'); +const levels = require("../src/models/level.js"); +const lrole = require("../src/models/lvlrole.js"); /** * @param {Discord.Message} message @@ -26,7 +26,7 @@ async function lvlRole(message, userID, guildID) { await newuser .save() - .catch(() => console.log('[XP] Failed to save new user to database')); + .catch(() => console.log("[XP] Failed to save new user to database")); } e.forEach((ee) => { @@ -41,7 +41,7 @@ async function lvlRole(message, userID, guildID) { else { u.roles.add(real).catch(() => { message.channel.send( - '[XP] ERROR: Role is higher than me. `MISSING_PERMISSIONS`' + "[XP] ERROR: Role is higher than me. `MISSING_PERMISSIONS`" ); }); } diff --git a/src/models/level.js b/src/models/level.js index b17cb15..d4e6253 100644 --- a/src/models/level.js +++ b/src/models/level.js @@ -1,4 +1,4 @@ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); const Levelz = new mongoose.Schema({ user: { type: String, unique: true }, @@ -7,4 +7,6 @@ const Levelz = new mongoose.Schema({ level: { type: Number, default: 0 } }); -module.exports = mongoose.model('Simply-XP', Levelz); +Levelz.index({ guild: 1, xp: -1 }); + +module.exports = mongoose.model("Simply-XP", Levelz); \ No newline at end of file diff --git a/src/models/lvlrole.js b/src/models/lvlrole.js index 64d5466..75fba2e 100644 --- a/src/models/lvlrole.js +++ b/src/models/lvlrole.js @@ -1,8 +1,8 @@ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); const rol = new mongoose.Schema({ gid: { type: String }, lvlrole: { type: Array } }); -module.exports = mongoose.model('Simply-XP-LevelRole', rol); +module.exports = mongoose.model("Simply-XP-LevelRole", rol); \ No newline at end of file diff --git a/src/rank.js b/src/rank.js index dfaf04f..ef738cb 100644 --- a/src/rank.js +++ b/src/rank.js @@ -1,32 +1,27 @@ -const levels = require('../src/models/level.js'); -const {join} = require('path'); +const levels = require("../src/models/level.js"); +const { join } = require("path"); +const getUserPosition = require("./utils/getUserPosition"); +const shortener = require("./utils/shortener"); /** * @param {Discord.Message} message * @param {string} userID * @param {string} guildID - * @param {import('../index').rankOptions} options + * @param {import("../index").rankOptions} options */ async function rank(message, userID, guildID, options = []) { - if (!userID) throw new Error('[XP] User ID was not provided.'); + if (!userID) throw new Error("[XP] User ID was not provided."); - if (!guildID) throw new Error('[XP] Guild ID was not provided.'); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); const user = await levels.findOne({ user: userID, guild: guildID }); - if (!user) throw new Error('[XP] NO_DATA | User has no XP data.'); + if (!user) throw new Error("[XP] NO_DATA | User has no XP data."); - const leaderboard = await levels - .find({ - guild: guildID - }) - .sort([['xp', 'descending']]) - .exec(); - - user.position = leaderboard.findIndex((i) => i.user === userID) + 1; + user.position = (await getUserPosition(userID, guildID)) || 1; let targetxp = user.level + 1; @@ -46,52 +41,73 @@ async function rank(message, userID, guildID, options = []) { async function rankCard(message, options = []) { try { - const Canvas = require('@napi-rs/canvas'); + const Canvas = require("@napi-rs/canvas"); Canvas.GlobalFonts.registerFromPath( - join(__dirname, 'Fonts', 'Baloo-Regular.ttf'), - 'Sans Serif' + join(__dirname, "Fonts", "Baloo2-Regular.woff2"), + "Sans Serif" ); const member = options.member; + if (!member) { + throw new Error("[XP] Member not found or not cached."); + } + const canvas = Canvas.createCanvas(1080, 400), - ctx = canvas.getContext('2d'); + ctx = canvas.getContext("2d"); const name = member.tag; - const noSymbols = (string) => string.replace(/[\u007f-\uffff]/g, ''); + const noSymbols = (string) => string.replace(/[\u007f-\uffff]/g, ""); - let fsiz = '45px'; + let fsiz = "45px"; if (message.guild.name.length >= 23) { - fsiz = '38px'; + fsiz = "38px"; } if (message.guild.name.length >= 40) { - fsiz = '28px'; + fsiz = "28px"; } if (message.guild.name.length >= 63) { - fsiz = '22px'; + fsiz = "22px"; } - let BackgroundRadius = '20', + let BackgroundRadius = "20", BackGroundImg = - options.background || - 'https://i.ibb.co/QQvMqf7/gradient.jpg', - AttachmentName = 'rank.png', - AttachmentDesc = 'Rank Card', + options.background || + "https://i.ibb.co/QQvMqf7/gradient.jpg", + AttachmentName = "rank.png", + AttachmentDesc = "Rank Card", Username = noSymbols(name), - AvatarRoundRadius = '50', - DrawLayerColor = '#000000', + AvatarRoundRadius = "50", + DrawLayerColor = "#000000", DrawLayerOpacity = 0.4, - BoxColor = options.color || '#096DD1', - LevelBarFill = options.lvlbar || '#ffffff', - LevelBarBackground = options.lvlbarBg || '#ffffff', + BoxColor = options.color || "#096DD1", + LevelBarFill = options.lvlbar || "#ffffff", + LevelBarBackground = options.lvlbarBg || "#ffffff", Rank = options.rank, - TextEXP = shortener(options.currentXP) + ' XP', + TextEXP = shortener(options.currentXP) + " XP", LvlText = `Level ${shortener(options.level)}`, - BarRadius = '20', - TextXpNeded = '{current}/{needed}', + BarRadius = "20", + TextXpNeded = "{current}/{needed}", CurrentXP = options.currentXP, NeededXP = options.neededXP; + const drawStatBadge = (x, y, text, textY) => { + ctx.save(); + RoundedBox(ctx, x, y, 180, 50, 20); + ctx.strokeStyle = "#BFC85A22"; + ctx.stroke(); + ctx.clip(); + ctx.fillStyle = BoxColor; + ctx.globalAlpha = 1; + ctx.fillRect(x, y, 180, 50); + ctx.globalAlpha = 1; + ctx.fillStyle = "#ffffff"; + ctx.font = "32px \"Sans Serif\""; + ctx.textAlign = "center"; + ctx.fillText(text, x + 90, textY); + ctx.restore(); + }; + ctx.beginPath(); ctx.moveTo(Number(BackgroundRadius), 0); ctx.lineTo(1080 - Number(BackgroundRadius), 0); @@ -109,12 +125,18 @@ async function rank(message, userID, guildID, options = []) { ctx.lineTo(0, Number(BackgroundRadius)); ctx.quadraticCurveTo(0, 0, Number(BackgroundRadius), 0); ctx.closePath(); + ctx.save(); ctx.clip(); - ctx.fillStyle = '#000000'; + ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, 1080, 400); let background = await Canvas.loadImage(BackGroundImg); ctx.globalAlpha = 0.7; - ctx.drawImage(background, 0, 0, 1080, 400); + const canvasWidth = canvas.width; + const canvasHeight = canvas.height; + const scale = canvasWidth / background.width; + const scaledHeight = background.height * scale; + const yOffset = (canvasHeight - scaledHeight) / 2; + ctx.drawImage(background, 0, yOffset, canvasWidth, scaledHeight); ctx.restore(); ctx.fillStyle = DrawLayerColor; @@ -132,66 +154,39 @@ async function rank(message, userID, guildID, options = []) { ctx.drawImage(avatar, 70, 30, 180, 180); ctx.restore(); - ctx.save(); - RoundedBox(ctx, 70, 240 + 50 + 30, 180, 50, 20); - ctx.strokeStyle = '#BFC85A22'; - ctx.stroke(); - ctx.clip(); - ctx.fillStyle = BoxColor; - ctx.globalAlpha = 1; - ctx.fillRect(70, 320, 180, 50); - ctx.globalAlpha = 1; - ctx.fillStyle = '#ffffff'; - ctx.font = '32px "Sans Serif"'; - ctx.textAlign = 'center'; - ctx.fillText(TextEXP, 160, 358); - ctx.restore(); - - ctx.save(); - RoundedBox(ctx, 70, 240, 180, 50, 20); - ctx.strokeStyle = '#BFC85A22'; - ctx.stroke(); - ctx.clip(); - ctx.fillStyle = BoxColor; - ctx.globalAlpha = 1; - ctx.fillRect(70, 240, 180, 50, 50); - ctx.globalAlpha = 1; - ctx.fillStyle = '#ffffff'; - ctx.font = '32px "Sans Serif"'; - ctx.textAlign = 'center'; - ctx.fillText(LvlText, 70 + 180 / 2, 278); - ctx.restore(); + drawStatBadge(70, 320, TextEXP, 358); + drawStatBadge(70, 240, LvlText, 278); ctx.save(); - ctx.textAlign = 'left'; - ctx.fillStyle = '#ffffff'; - ctx.shadowColor = '#000000'; + ctx.textAlign = "left"; + ctx.fillStyle = "#ffffff"; + ctx.shadowColor = "#000000"; ctx.shadowBlur = 15; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1; - ctx.font = '39px "Sans Serif"'; + ctx.font = "39px \"Sans Serif\""; ctx.fillText(Username, 390, 80); ctx.restore(); ctx.save(); - ctx.textAlign = 'right'; - ctx.fillStyle = '#ffffff'; - ctx.shadowColor = '#000000'; + ctx.textAlign = "right"; + ctx.fillStyle = "#ffffff"; + ctx.shadowColor = "#000000"; ctx.shadowBlur = 15; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1; - ctx.font = '55px "Sans Serif"'; - ctx.fillText('#' + Rank, canvas.width - 55, 80); + ctx.font = "55px \"Sans Serif\""; + ctx.fillText("#" + Rank, canvas.width - 55, 80); ctx.restore(); ctx.save(); RoundedBox(ctx, 390, 305, 660, 70, Number(20)); - ctx.strokeStyle = '#BFC85A22'; + ctx.strokeStyle = "#BFC85A22"; ctx.stroke(); ctx.clip(); - ctx.fillStyle = '#ffffff'; + ctx.fillStyle = "#ffffff"; ctx.font = `${fsiz} "Sans Serif"`; - ctx.textAlign = 'center'; + ctx.textAlign = "center"; ctx.fillText(message.guild.name, 720, 355); ctx.globalAlpha = 0.2; ctx.fillRect(390, 305, 660, 70); @@ -199,7 +194,7 @@ async function rank(message, userID, guildID, options = []) { ctx.save(); RoundedBox(ctx, 390, 145, 660, 50, Number(BarRadius)); - ctx.strokeStyle = '#BFC85A22'; + ctx.strokeStyle = "#BFC85A22"; ctx.stroke(); ctx.clip(); ctx.fillStyle = LevelBarBackground; @@ -212,7 +207,7 @@ async function rank(message, userID, guildID, options = []) { ctx.save(); RoundedBox(ctx, 390, 145, progress, 50, Number(BarRadius)); - ctx.strokeStyle = '#BFC85A22'; + ctx.strokeStyle = "#BFC85A22"; ctx.stroke(); ctx.clip(); ctx.fillStyle = LevelBarFill; @@ -221,25 +216,25 @@ async function rank(message, userID, guildID, options = []) { ctx.restore(); ctx.save(); - ctx.textAlign = 'left'; - ctx.fillStyle = '#ffffff'; + ctx.textAlign = "left"; + ctx.fillStyle = "#ffffff"; ctx.globalAlpha = 0.8; - ctx.font = '30px "Sans Serif"'; - ctx.fillText('Next Level: ' + shortener(NeededXP) + ' XP', 390, 230); + ctx.font = "30px \"Sans Serif\""; + ctx.fillText("Next Level: " + shortener(NeededXP) + " XP", 390, 230); ctx.restore(); const latestXP = Number(CurrentXP) - Number(NeededXP); const textXPEdited = TextXpNeded.replace(/{needed}/g, shortener(NeededXP).toString()) .replace(/{current}/g, shortener(CurrentXP).toString()) .replace(/{latest}/g, latestXP.toString()); - ctx.textAlign = 'center'; - ctx.fillStyle = '#474747'; + ctx.textAlign = "center"; + ctx.fillStyle = "#474747"; ctx.globalAlpha = 1; - ctx.font = '30px "Sans Serif"'; + ctx.font = "30px \"Sans Serif\""; ctx.fillText(textXPEdited, 730, 180); return { - attachment: canvas.toBuffer('image/webp'), + attachment: canvas.toBuffer("image/webp"), description: AttachmentDesc, name: AttachmentName }; @@ -268,28 +263,5 @@ function RoundedBox(ctx, x, y, width, height, radius) { ctx.closePath(); } -function shortener(count) { - const COUNT_ABBRS = [ - '', - 'k', - 'M', - 'B', - 'T', - 'Q', - 'Q+', - 'S', - 'S+', - 'O', - 'N', - 'D', - 'U' - ]; - - const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); - let result = parseFloat((count / Math.pow(1000, i)).toFixed(2)); - result += `${COUNT_ABBRS[i]}`; - return result; -} - module.exports = rank; diff --git a/src/reset.js b/src/reset.js index ecffc2a..ad8e435 100644 --- a/src/reset.js +++ b/src/reset.js @@ -1,4 +1,4 @@ -const levels = require('../src/models/level.js'); +const levels = require("../src/models/level.js"); /** * @param {string} userID @@ -6,9 +6,9 @@ const levels = require('../src/models/level.js'); */ async function reset(userID, guildID) { - if (!userID) throw new Error('[XP] User ID was not provided.'); + if (!userID) throw new Error("[XP] User ID was not provided."); - if (!guildID) throw new Error('[XP] User ID was not provided.'); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); await levels .findOneAndUpdate({ user: userID, guild: guildID }, { xp: 0, level: 0 }) diff --git a/src/roleSetup.js b/src/roleSetup.js index ff20bfe..375d6b3 100644 --- a/src/roleSetup.js +++ b/src/roleSetup.js @@ -1,11 +1,11 @@ -const lrole = require('../src/models/lvlrole.js'); +const lrole = require("../src/models/lvlrole.js"); class roleSetup { /** - * @param {Discord.Client} client - * @param {string} guildID - * @param {import('../index').lvladdOptions} options - */ + * @param {Discord.Client} client + * @param {string} guildID + * @param {import("../index").lvladdOptions} options + */ static async add(client, guildID, options = []) { let rol = await lrole.findOne({ @@ -21,7 +21,7 @@ class roleSetup { let roll = g.roles.cache.find((r) => r.id === options.role); if (roll) { - if (rol) throw new Error('Level Already Exist. Use delete'); + if (rol) throw new Error("Level Already Exist. Use delete"); else if (!rol) { let newrol = await lrole.findOne({ gid: guildID @@ -48,17 +48,17 @@ class roleSetup { } } else { throw new Error( - 'Role ID is invalid. | ' + - `Guild ID: ${guildID} | Role ID: ${options.role}` + "Role ID is invalid. | " + + `Guild ID: ${guildID} | Role ID: ${options.role}` ); } } /** - * @param {Discord.Client} client - * @param {string} guildID - * @param {import('../index').lvlremoveOptions} options - */ + * @param {Discord.Client} client + * @param {string} guildID + * @param {import("../index").lvlremoveOptions} options + */ static async remove(client, guildID, options = []) { let rol = await lrole.find({ @@ -66,7 +66,7 @@ class roleSetup { }); if (!rol || rol.length === 0) - throw new Error('Level role with this level does not exist'); + throw new Error("Level role with this level does not exist"); rol = rol[0].lvlrole.find((item) => item.lvl === options.level) || undefined; if (rol) { @@ -80,13 +80,13 @@ class roleSetup { ); return true; - } else throw new Error('Level role with this level does not exist'); + } else throw new Error("Level role with this level does not exist"); } /** - * @param {Discord.Client} client - * @param {string} guildID - */ + * @param {Discord.Client} client + * @param {string} guildID + */ static async fetch(client, guildID) { let rol = await lrole.find({ @@ -99,10 +99,10 @@ class roleSetup { } /** - * @param {Discord.Client} client - * @param {string} guildID - * @param {string} level - */ + * @param {Discord.Client} client + * @param {string} guildID + * @param {string} level + */ static async find(client, guildID, level) { let rol = await lrole.find({ diff --git a/src/setLevel.js b/src/setLevel.js index 32c8fb3..64b9969 100644 --- a/src/setLevel.js +++ b/src/setLevel.js @@ -1,64 +1,36 @@ -const levels = require('../src/models/level.js'); -let { roleSetup } = require('../simplyxp'); +const levels = require("../src/models/level.js"); +const notifyLevelUp = require("./utils/levelUpNotifier"); +const buildLevelPayload = require("./utils/levelPayload"); /** * @param {Discord.Message} message * @param {string} userID * @param {string} guildID - * @param {string} level + * @param {string|number} level */ async function setLevel(message, userID, guildID, level) { - if (!userID) throw new Error('[XP] User ID was not provided.'); - - if (!guildID) throw new Error('[XP] Guild ID was not provided.'); - - if (!level) throw new Error('[XP] Level amount is not provided.'); - - let { client } = message; - - const user = await levels.findOne({ user: userID, guild: guildID }); - - if (!user) { - const newUser = new levels({ - user: userID, - guild: guildID, - xp: 0, - level: 0 - }); - - await newUser - .save() - .catch(() => console.log('[XP] Failed to save new user to database')); - - let xp = (level * 10) ** 2; - - return { - level: level, - exp: xp - }; - } - let level1 = user.level; - - user.xp = (level * 10) ** 2; - user.level = Math.floor(0.1 * Math.sqrt(user.xp)); - - await user - .save() - .catch((e) => - console.log(`[XP] Failed to set Level | User: ${userID} | Err: ${e}`) - ); - - if (level1 !== level) { - let data = { - xp: user.xp, - level: user.level, - userID, - guildID - }; - - let role = await roleSetup.find(client, guildID, level); - - client.emit('levelUp', message, data, role); + if (!userID) throw new Error("[XP] User ID was not provided."); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); + if (level == null || isNaN(Number(level))) throw new Error("[XP] Invalid level amount."); + + // Coerce level to number to ensure proper comparisons + const newLevel = Number(level); + + // Get the previous user document to capture the old level + const previousUser = await levels.findOne({ user: userID, guild: guildID }); + const previousLevel = previousUser ? previousUser.level : 0; + + const user = await levels.findOneAndUpdate( + { user: userID, guild: guildID }, + { xp: (newLevel * 10) ** 2, level: Math.floor(0.1 * Math.sqrt((newLevel * 10) ** 2)) }, + { upsert: true, new: true } + ); + + // Compare previous level with new level to determine if levelUp should be emitted + if (previousLevel !== user.level) { + const data = buildLevelPayload(user, userID, guildID); + + await notifyLevelUp(message, data, user.level); } return { @@ -67,4 +39,4 @@ async function setLevel(message, userID, guildID, level) { }; } -module.exports = setLevel; +module.exports = setLevel; \ No newline at end of file diff --git a/src/setXP.js b/src/setXP.js index 3389456..8caf055 100644 --- a/src/setXP.js +++ b/src/setXP.js @@ -1,51 +1,19 @@ -const levels = require('../src/models/level.js'); - -/** - * @param {string} userID - * @param {string} guildID - * @param {string} xp - */ +const levels = require("../src/models/level.js"); async function setXP(userID, guildID, xp) { - if (!userID) throw new Error('[XP] User ID was not provided.'); - - if (!guildID) throw new Error('[XP] Guild ID was not provided.'); - - if (!xp) throw new Error('[XP] XP amount is not provided.'); - - if (Number(xp).toString() === 'NaN') - throw new Error('[XP] XP amount is not a number.'); - - const user = await levels.findOne({ user: userID, guild: guildID }); - - let lvl = Math.floor(0.1 * Math.sqrt(xp)); - - if (!user) { - const newUser = new levels({ - user: userID, - guild: guildID, - xp: xp, - level: lvl - }); - - await newUser - .save() - .catch(() => console.log('[XP] Failed to save new use to database')); + if (!userID) throw new Error("[XP] User ID was not provided."); + if (!guildID) throw new Error("[XP] Guild ID was not provided."); + if (xp == null || isNaN(Number(xp))) throw new Error("[XP] Invalid XP amount."); - return { - xp: 0 - }; - } - user.xp = xp; - user.level = Math.floor(0.1 * Math.sqrt(user.xp)); + const lvl = Math.floor(0.1 * Math.sqrt(xp)); - await user - .save() - .catch((e) => - console.log(`[XP] Failed to set XP | User: ${userID} | Err: ${e}`) - ); + await levels.findOneAndUpdate( + { user: userID, guild: guildID }, + { xp: xp, level: lvl }, + { upsert: true } + ).catch((e) => console.log(`[XP] Failed to set XP | User: ${userID} | Err: ${e}`)); - return { xp }; + return { xp, level: lvl }; } -module.exports = setXP; +module.exports = setXP; \ No newline at end of file diff --git a/src/utils/getUserPosition.js b/src/utils/getUserPosition.js new file mode 100644 index 0000000..81128e4 --- /dev/null +++ b/src/utils/getUserPosition.js @@ -0,0 +1,28 @@ +const levels = require("../models/level.js"); + +/** + * Walks the sorted leaderboard for a guild to discover a user's global position. + * @param {string} userID + * @param {string} guildID + * @returns {Promise} + */ +async function getUserPosition(userID, guildID) { + const cursor = levels.find({ guild: guildID }).sort({ xp: -1 }).cursor(); + let position = 0; + + try { + for await (const entry of cursor) { + position += 1; + if (entry.user === userID) { + return position; + } + } + return null; + } finally { + if (typeof cursor.close === "function") { + await cursor.close(); + } + } +} + +module.exports = getUserPosition; \ No newline at end of file diff --git a/src/utils/levelPayload.js b/src/utils/levelPayload.js new file mode 100644 index 0000000..eca7f50 --- /dev/null +++ b/src/utils/levelPayload.js @@ -0,0 +1,17 @@ +/** + * Build the shared payload used when notifying about a level change. + * @param {Object} user + * @param {string} userID + * @param {string} guildID + * @returns {{xp:number,level:number,userID:string,guildID:string}} + */ +function buildLevelPayload(user, userID, guildID) { + return { + xp: user.xp, + level: user.level, + userID, + guildID + }; +} + +module.exports = buildLevelPayload; diff --git a/src/utils/levelUpNotifier.js b/src/utils/levelUpNotifier.js new file mode 100644 index 0000000..d0b7579 --- /dev/null +++ b/src/utils/levelUpNotifier.js @@ -0,0 +1,15 @@ +const { roleSetup } = require("../../simplyxp"); + +/** + * Emits the shared level-up event using the configured role setup. + * @param {Discord.Message} message + * @param {Object} data + * @param {number|string} roleLevel + * @returns {Promise} + */ +async function notifyLevelUp(message, data, roleLevel) { + const role = await roleSetup.find(message.client, data.guildID, roleLevel); + message.client.emit("levelUp", message, data, role); +} + +module.exports = notifyLevelUp; diff --git a/src/utils/shortener.js b/src/utils/shortener.js new file mode 100644 index 0000000..5c7d70a --- /dev/null +++ b/src/utils/shortener.js @@ -0,0 +1,18 @@ +/** + * Format a large XP count using common abbreviations. + * @param {number} count + * @returns {string} + */ +function shortener(count) { + const numericCount = Number(count) || 0; + const isNegative = numericCount < 0; + const absCount = Math.abs(numericCount); + const suffixes = ["", "k", "M", "B", "T", "Q", "Q+", "S", "S+", "O", "N", "D", "U"]; + const index = absCount === 0 ? 0 : Math.floor(Math.log10(absCount) / 3); + const safeIndex = Math.min(Math.max(index, 0), suffixes.length - 1); + const scaled = absCount / Math.pow(1000, safeIndex); + const formatted = scaled.toFixed(2) + suffixes[safeIndex]; + return isNegative ? "-" + formatted : formatted; +} + +module.exports = shortener;