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
+
+
+
-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.
-
-
- 
- 
-
+
+
+
-
+
+
+
+
-## 🖥️ 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
+[](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;