diff --git a/Mini_Docs/charts.md b/Mini_Docs/charts.md
index f071078..a0faa93 100644
--- a/Mini_Docs/charts.md
+++ b/Mini_Docs/charts.md
@@ -1,13 +1,14 @@
-## charts(guildId, options) ⇒ Promise.<void>
+## charts(guildId, options) ⇒ Promise.<{attachment: Buffer, description: string, name: string}>
Creates a chart
**Kind**: global function
+**Returns**: Promise.<{attachment: Buffer, description: string, name: string}>
- Chart attachment
**Throws**:
-- XpFatal
If invalid parameters are provided
+- XpFatal
If invalid parameters are provided, or if there are not enough users to create a chart
**Link**: `Documentation:` https://simplyxp.js.org/docs/charts
diff --git a/Mini_Docs/reset.md b/Mini_Docs/reset.md
index 37d9fc5..8a7cdca 100644
--- a/Mini_Docs/reset.md
+++ b/Mini_Docs/reset.md
@@ -1,6 +1,6 @@
-## reset(userId, guildId, username, erase) ⇒ Promise.<boolean>
+## reset(userId, guildId, erase, username) ⇒ Promise.<boolean>
Reset user levels to 0 in a guild
@@ -11,10 +11,10 @@ Reset user levels to 0 in a guild
**Link**: `Documentation:` https://simplyxp.js.org/docs/reset
-| Param | Type | Default | Description |
-|----------|----------------------|--------------------|--------------------------------|
-| userId | string
| | |
-| guildId | string
| | |
-| erase | boolean
| false
| Erase user entry from database |
-| username | string
| | |
+| Param | Type | Default | Description |
+|----------|----------------------|--------------------|-------------------------------------------|
+| userId | string
| | |
+| guildId | string
| | |
+| erase | boolean
| false
| Erase user entry from the database |
+| username | string
| | Username to use if auto_create is enabled |
diff --git a/Mini_Docs/types/ChartOptions.md b/Mini_Docs/types/ChartOptions.md
index 7fda322..5d14cff 100644
--- a/Mini_Docs/types/ChartOptions.md
+++ b/Mini_Docs/types/ChartOptions.md
@@ -2,16 +2,18 @@
### Properties
-- `backgroundColor` (HexColor, optional): The background color of the chart.
-- `limit` (number, optional): The limit of the chart.
-- `type` (`"bar" | "line" | "pie" | "doughnut" | "radar" | "polarArea"`, optional): The type of the chart.
+- font `(string)`: The font to be used in the chart.
+- limit `(2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10)`: The maximum number of users to be displayed in the chart.
+- theme `("blue" | "dark" | "discord" | "green" | "orange" | "red" | "space" | "yellow")`: The theme to be used in the chart.
+- type `("bar" | "doughnut" | "pie")`: The type of chart to be created.
### Example
```typescript
export interface ChartOptions {
- backgroundColor?: HexColor;
- limit?: number;
- type?: "bar" | "line" | "pie" | "doughnut" | "radar" | "polarArea";
+ font?: string;
+ limit?: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
+ theme?: "blue" | "dark" | "discord" | "green" | "orange" | "red" | "space" | "yellow";
+ type?: "bar" | "doughnut" | "pie";
}
```
\ No newline at end of file
diff --git a/Mini_Docs/types/leaderboardCardOptions.md b/Mini_Docs/types/leaderboardCardOptions.md
index d3a9c7f..6e66c37 100644
--- a/Mini_Docs/types/leaderboardCardOptions.md
+++ b/Mini_Docs/types/leaderboardCardOptions.md
@@ -2,7 +2,7 @@
### Description
-An interface representing the options for a rank card.
+An interface representing the options for a leaderboard card.
### Properties
diff --git a/Tests/charts.png b/Tests/charts.png
new file mode 100644
index 0000000..ed52f59
Binary files /dev/null and b/Tests/charts.png differ
diff --git a/Tests/leaderboard.png b/Tests/leaderboard.png
index e32c09e..caa65a0 100644
Binary files a/Tests/leaderboard.png and b/Tests/leaderboard.png differ
diff --git a/Tests/rankcard.png b/Tests/rankcard.png
index 57950d2..67c1f6e 100644
Binary files a/Tests/rankcard.png and b/Tests/rankcard.png differ
diff --git a/Tests/test.cjs b/Tests/test.cjs
index 7855d36..b4c00f1 100644
--- a/Tests/test.cjs
+++ b/Tests/test.cjs
@@ -7,46 +7,44 @@ async function test(dbType) {
switch (dbType) {
case "mongodb":
await xp.connect(MongoURL, {
+ auto_create: true,
type: "mongodb",
debug: true
});
break;
case "sqlite":
await xp.connect("Tests/test.db", {
+ auto_create: true,
type: "sqlite",
debug: true
});
break;
}
- await xp.create("1234567890", "0987654321", "Abadima");
+ /* await xp.create("1234567890", "0987654321", "Abadima")
- await xp.create("1234567891", "0987654321", "Elizabeth");
+ await xp.create("1234567891", "0987654321", "Elizabeth");
- await xp.create("1234567892", "0987654321", "Jena");
+ await xp.create("1234567892", "0987654321", "Jena").then(console.log)
- await xp.create("1234567893", "0987654321", "Rahul");
+ await xp.create("1234567893", "0987654321", "Rahul");
- await xp.create("1234567894", "0987654321", "Snowball");
+ await xp.create("1234567894", "0987654321", "Snowball");*/
- await xp.setLevel("1234567890", "0987654321", Infinity);
+ await xp.setLevel("1234567890", "0987654321", 25, "Abadima");
- await xp.setLevel("1234567891", "0987654321", 50);
+ await xp.setLevel("1234567893", "0987654321", 20, "Rahul");
- await xp.setLevel("1234567893", "0987654321", 420690000);
+ await xp.setLevel("1234567894", "0987654321", 15, "Jena");
- await xp.setLevel("1234567892", "0987654321", 0);
+ await xp.addLevel("1234567892", "0987654321", 10, "Ash");
- await xp.addXP("1234567892", "0987654321", 650);
-
- await xp.reset("1234567892", "0987654321").then(console.log);
-
- await xp.fetch("1234567892", "0987654321").then(console.log);
+ await xp.setLevel("1234567891", "0987654321", 5, "Elizabeth");
await xp.rankCard(
{id: "0987654321", name: "SimplyTests"},
{
- avatarURL: "https://cdn.discordapp.com/avatars/326815959358898189/02ed1eef72af8eca955f35e309f8f3aa.webp",
+ avatarURL: "https://cdn.discordapp.com/avatars/326815959358898189/67f99af24216f6d98d8d61a3b127d160.webp",
id: "1234567890", username: "Abadima"
},
{background: "https://img.freepik.com/free-vector/gradient-wavy-purple-background_23-2149117433.jpg"}
@@ -67,6 +65,23 @@ async function test(dbType) {
require("fs").writeFileSync("Tests/leaderboard.png", results.attachment);
});
});
+
+ await xp.charts("0987654321", {
+ theme: "space",
+ type: "doughnut"
+ }).then(results => {
+ require("fs").writeFileSync("Tests/charts.png", results.attachment);
+ });
+
+ await xp.reset("1234567890", "0987654321", true);
+
+ await xp.reset("1234567893", "0987654321", true);
+
+ await xp.reset("1234567894", "0987654321", true);
+
+ await xp.reset("1234567892", "0987654321", true);
+
+ await xp.reset("1234567891", "0987654321", true);
}
test("mongodb");
\ No newline at end of file
diff --git a/UPDATES@DEV.md b/UPDATES@DEV.md
index 87896b6..217e0f9 100644
--- a/UPDATES@DEV.md
+++ b/UPDATES@DEV.md
@@ -1,5 +1,23 @@
# VERSION 2@DEV CHANGELOGS
+## [DEV 4](https://github.com/Abadima/simply-xp/releases/tag/v2.0.0-dev.4)
+
+### Additions
+
+- `charts()` is now added, starting with `bar`, `doughnut` and `pie` types, and many themes to choose from.
+- `migrate.fromDB()` now supports migrating from SQLite to MongoDB.
+
+### Bug Fixes
+
+- Fix `db.createOne()` not returning created user (MongoDB), this should fix multiple functions not working properly.
+- Fix `db.addXP()` throwing unnecessary error when checking if new user is considered levelled up.
+- `rankCard()` and `leaderboardCard()` now throws better error when image(s) provided can't be loaded.
+
+### Improvements
+
+- Optimized font file, lowers package size (~57.31%)
+- Updated JSDocs to improve accuracy.
+
## [DEV 3 FIX 2](https://github.com/Abadima/simply-xp/releases/tag/v2.0.0-dev.3)
### Bug Fixes
@@ -55,7 +73,7 @@
### Changes
- Updated JSDocs, changed some types to interfaces.
-- Optimized font file, lowers package size.
+- Optimized font file, lowers package size (~32.12%)
## [DEV 1](https://github.com/Abadima/simply-xp/releases/tag/v2.0.0-dev.1)
diff --git a/package.json b/package.json
index 4633aad..5c14d14 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "simply-xp",
- "version": "2.0.0-dev.3-fix.2",
+ "version": "2.0.0-dev.4",
"description": "The easiest way to implement xp system",
"main": "lib/xp.js",
"scripts": {
@@ -40,18 +40,18 @@
"url": "git+https://github.com/Rahuletto/simply-xp.git"
},
"dependencies": {
- "@napi-rs/canvas": "^0.1.43"
+ "@napi-rs/canvas": "^0.1.44"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.4",
- "@types/node": "^20.5.0",
- "@typescript-eslint/eslint-plugin": "^6.3.0",
- "@typescript-eslint/parser": "^6.3.0",
- "better-sqlite3": "8.5.0",
- "discord.js": "^14.12.1",
+ "@types/node": "^20.5.4",
+ "@typescript-eslint/eslint-plugin": "^6.4.1",
+ "@typescript-eslint/parser": "^6.4.1",
+ "better-sqlite3": "8.5.1",
+ "discord.js": "^14.13.0",
"eslint": "^8.47.0",
"jsdoc-to-markdown": "^8.0.0",
- "mongodb": "^5.7.0",
+ "mongodb": "^5.8.1",
"typescript": "^5.1.6",
"uglify-js": "^3.17.4"
},
diff --git a/src/Fonts/Baloo-Regular.eot b/src/Fonts/Baloo-Regular.eot
deleted file mode 100644
index a19ebbc..0000000
Binary files a/src/Fonts/Baloo-Regular.eot and /dev/null differ
diff --git a/src/Fonts/BalooBhaijaan-Regular.otf b/src/Fonts/BalooBhaijaan-Regular.otf
new file mode 100644
index 0000000..a395563
Binary files /dev/null and b/src/Fonts/BalooBhaijaan-Regular.otf differ
diff --git a/src/add.ts b/src/add.ts
index da2bd04..43a2683 100644
--- a/src/add.ts
+++ b/src/add.ts
@@ -29,9 +29,7 @@ export async function addLevel(userId: string, guildId: string, level: number, u
if (!user) {
if (xp.auto_create && username) return await db.createOne({
collection: "simply-xps",
- data: {
- guild: guildId, user: userId, name: username, level, xp: convertFrom(level)
- }
+ data: {guild: guildId, user: userId, name: username, level, xp: convertFrom(level)}
}) as UserResult;
else throw new XpFatal({function: "addLevel()", message: "User does not exist"});
} else {
@@ -113,5 +111,5 @@ export async function addXP(userId: string, guildId: string,
}) as UserResult;
}
- return {...data, hasLevelledUp: data.level > user.level};
+ return {...data, hasLevelledUp: user ? data.level > user.level : true};
}
\ No newline at end of file
diff --git a/src/cards.ts b/src/cards.ts
index 3500752..04aef9d 100644
--- a/src/cards.ts
+++ b/src/cards.ts
@@ -78,7 +78,14 @@ export type rankLocales = {
* @returns {Promise<{attachment: Buffer, description: string, name: string}>}
* @throws {XpFatal} - If parameters are not provided correctly
*/
-export async function rankCard(guild: { id: string, name: string }, user: UserOptions, options: RankCardOptions = {}, locales: rankLocales = {}): Promise<{ attachment: Buffer; description: string; name: string; }> {
+export async function rankCard(guild: {
+ id: string,
+ name: string
+}, user: UserOptions, options: RankCardOptions = {}, locales: rankLocales = {}): Promise<{
+ attachment: Buffer;
+ description: string;
+ name: string;
+}> {
if (!guild) throw new XpFatal({function: "rankCard()", message: "No Guild Provided"});
if (!user) throw new XpFatal({function: "rankCard()", message: "No User Provided"});
@@ -91,7 +98,7 @@ export async function rankCard(guild: { id: string, name: string }, user: UserOp
if (!locales?.xp) locales.xp = "XP";
XpLog.debug("rankCard()", "LEGACY MODE ENABLED");
- XpLog.info("rankCard()", "Modern RankCard is not supported yet, coming soon!");
+ XpLog.info("rankCard()", "Modern RankCard is coming in dev.5");
if (!user?.avatarURL.endsWith(".png") && !user.avatarURL.endsWith(".jpg") && !user.avatarURL.endsWith(".webp")) {
throw new XpFatal({
@@ -99,26 +106,36 @@ export async function rankCard(guild: { id: string, name: string }, user: UserOp
});
}
- // check if user.id and user.username and user.displayAvatarURL() exists
+
if (!user || !user.id || !user.username) {
throw new XpFatal({
function: "rankCard()", message: "Invalid User Provided, user must contain id, username, and avatarURL."
});
}
- GlobalFonts.registerFromPath(options?.font || join(__dirname, "Fonts", "Baloo-Regular.eot"), "Sans Serif");
+ GlobalFonts.registerFromPath(options?.font || join(__dirname, "Fonts", "BalooBhaijaan-Regular.otf"), "Sans Serif");
+
+ if (!cachedRankImage) cachedRankImage = await loadImage(options?.background || "https://i.ibb.co/dck2Tnt/rank-card.webp").catch(() => {
+ throw new XpFatal({
+ function: "rankCard()", message: "Unable to load background image, is it valid?"
+ });
+ });
- if (!cachedRankImage) cachedRankImage = await loadImage(options?.background || "https://i.ibb.co/dck2Tnt/rank-card.webp");
+ const avatarURL = await loadImage(user.avatarURL).catch(() => {
+ throw new XpFatal({
+ function: "rankCard()", message: "Unable to load user's AvatarURL, is it reachable?"
+ });
+ });
let dbUser = await db.findOne({collection: "simply-xps", data: {guild: guild.id, user: user.id}}) as User;
if (!dbUser) {
- if (xp.auto_create) dbUser = await create(user.id, guild.id, user.username);
+ if (xp.auto_create) dbUser = await create(user.id, guild.id, user.username) as User;
else throw new XpFatal({function: "rankCard()", message: "User not found in database"});
}
const users = await db.find({collection: "simply-xps", data: {guild: guild.id}}) as User[];
- const position: number = users.sort((a, b) => b.xp - a.xp).findIndex((u) => u.user === user.id) + 1;
+ dbUser.position = users.sort((a, b) => b.xp - a.xp).findIndex((u) => u.user === user.id) + 1;
if (options?.legacy) {
const Username = user.username.replace(/[\u007f-\uffff]/g, ""),
@@ -162,7 +179,7 @@ export async function rankCard(guild: { id: string, name: string }, user: UserOp
context.lineWidth = 15;
context.stroke();
context.clip();
- context.drawImage(await loadImage(user.avatarURL), 70, 30, 180, 180);
+ context.drawImage(avatarURL, 70, 30, 180, 180);
context.restore();
context.save();
@@ -212,7 +229,7 @@ export async function rankCard(guild: { id: string, name: string }, user: UserOp
context.shadowOffsetX = 1;
context.shadowOffsetY = 1;
context.font = "55px \"Sans Serif\"";
- context.fillText("#" + position, canvas.width - 55, 80);
+ context.fillText("#" + dbUser.position, canvas.width - 55, 80);
context.restore();
context.save();
@@ -263,7 +280,7 @@ export async function rankCard(guild: { id: string, name: string }, user: UserOp
context.fillText(textXPEdited, 730, 180);
} else {
- // TODO: Add support for modern mode\
+ // TODO: Add support for modern mode
canvas = createCanvas(1080, 360);
}
@@ -286,13 +303,29 @@ export async function rankCard(guild: { id: string, name: string }, user: UserOp
* @returns {Promise<{attachment: Buffer, description: string, name: string}>}
* @throws {XpFatal} - If parameters are not provided correctly
*/
-export async function leaderboardCard(data: Array, options: LeaderboardOptions = {}, guildInfo?: { name: string, imageURL: string, memberCount: number }, locales: LeaderboardLocales = {}): Promise<{ attachment: Buffer; description: string; name: string; }> {
- if (!data || data.length < 1) throw new XpFatal({function: "leaderboardCard()", message: "There must be at least 1 user in the data array"});
+export async function leaderboardCard(data: Array, options: LeaderboardOptions = {}, guildInfo?: {
+ name: string,
+ imageURL: string,
+ memberCount: number
+}, locales: LeaderboardLocales = {}): Promise<{ attachment: Buffer; description: string; name: string; }> {
+ if (!data || data.length < 1) throw new XpFatal({
+ function: "leaderboardCard()",
+ message: "There must be at least 1 user in the data array"
+ });
+
+ if (!cachedLeaderboardArtwork && options?.artworkImage) cachedLeaderboardArtwork = await loadImage(options.artworkImage).catch(() => {
+ throw new XpFatal({
+ function: "leaderboardCard()", message: "Unable to load artwork image, is it valid?"
+ });
+ });
- if (!cachedLeaderboardArtwork && options?.artworkImage) cachedLeaderboardArtwork = await loadImage(options.artworkImage);
- if (!cachedLeaderboardImage && options?.backgroundImage) cachedLeaderboardImage = await loadImage(options.backgroundImage);
+ if (!cachedLeaderboardImage && options?.backgroundImage) cachedLeaderboardImage = await loadImage(options.backgroundImage).catch(() => {
+ throw new XpFatal({
+ function: "leaderboardCard()", message: "Unable to load background image, is it valid?"
+ });
+ });
- GlobalFonts.registerFromPath(options?.font || join(__dirname, "Fonts", "Baloo-Regular.eot"), "Sans Serif");
+ GlobalFonts.registerFromPath(options?.font || join(__dirname, "Fonts", "BalooBhaijaan-Regular.otf"), "Sans Serif");
if (!locales.level) locales.level = "LEVEL";
if (!locales.members) locales.members = "Members";
@@ -440,13 +473,20 @@ export async function leaderboardCard(data: Array, options: LeaderboardOpt
};
}
-function RoundedBox(ctx: {
+/**
+ * @constructor
+ * @private
+ */
+export function RoundedBox(ctx: {
beginPath: () => void;
moveTo: (arg0: number, arg1: number) => void;
lineTo: (arg0: number, arg1: number) => void;
quadraticCurveTo: (arg0: number, arg1: number, arg2: number, arg3: number) => void;
closePath: () => void;
-}, x: number, y: number, width: number, height: number, radius: number, roundCorners: { top?: boolean, bottom?: boolean } = {top: true, bottom: true}) {
+}, x: number, y: number, width: number, height: number, radius: number, roundCorners: {
+ top?: boolean,
+ bottom?: boolean
+} = {top: true, bottom: true}) {
ctx.beginPath();
ctx.moveTo(x + (roundCorners.top ? radius : 0), y);
ctx.lineTo(x + width - (roundCorners.top ? radius : 0), y);
diff --git a/src/charts.ts b/src/charts.ts
index 9d02570..4b1e938 100644
--- a/src/charts.ts
+++ b/src/charts.ts
@@ -1,32 +1,380 @@
+import {createCanvas, GlobalFonts} from "@napi-rs/canvas";
+import {join} from "path";
+import {leaderboard} from "./leaderboard";
+import {RoundedBox} from "./cards";
import {XpFatal} from "./functions/xplogs";
-// import {leaderboard} from "./leaderboard";
-
-type HexColor = `#${string}` | `0x${string}`;
+/**
+ * Chart options
+ * @property {string} font - Font of the chart
+ * @property {"blue" | "dark" | "discord" | "green" | "orange" | "red" | "space" | "yellow"} theme - Theme of the chart
+ * @property {number} limit - Limit of users to return (2-10)
+ */
export interface ChartOptions {
- backgroundColor?: HexColor;
- limit?: number;
- type?: "bar" | "line" | "pie" | "doughnut" | "radar" | "polarArea";
+ font?: string;
+ limit?: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
+ theme?: "blue" | "dark" | "discord" | "green" | "orange" | "red" | "space" | "yellow";
+ type?: "bar" | "doughnut" | "pie";
}
-// TODO: Add support for charts
/**
* Creates a chart
* @async
* @param {string} guildId
* @param {ChartOptions?} options
* @link `Documentation:` https://simplyxp.js.org/docs/charts
- * @returns {Promise}
- * @throws {XpFatal} If invalid parameters are provided
+ * @returns {Promise<{attachment: Buffer, description: string, name: string}>} Chart attachment
+ * @throws {XpFatal} If invalid parameters are provided, or if there are not enough users to create a chart
*/
-export async function charts(guildId: string, options: ChartOptions = {}): Promise {
+export async function charts(guildId: string, options: ChartOptions = {}): Promise<{
+ attachment: Buffer; description: string; name: string;
+}> {
if (!guildId) throw new XpFatal({function: "charts()", message: "No Guild ID Provided"});
if (!options) throw new XpFatal({function: "charts()", message: "No Options Provided"});
- if (options.limit && options.limit > 10) options.limit = 10;
+ if (!options.theme) options.theme = "blue";
if (!options.type) options.type = "bar";
+ let colors = {
+ background: "#FFFFFF",
+ barColor: "#FFFFFF",
+ pieColors: ["#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF"],
+ textColor: "#FFFFFF"
+ };
+
+ const users = await leaderboard(guildId, Math.min(Math.max(options?.limit || 10, 2), 10)).catch((XPError) => {
+ throw new XpFatal({function: "charts()", message: XPError.message});
+ });
+
+ if (users.length < 2) throw new XpFatal({function: "charts()", message: "Not enough users to create a chart"});
+ users.sort((a, b) => b.position - a.position);
+
+ GlobalFonts.registerFromPath(options?.font || join(__dirname, "Fonts", "BalooBhaijaan-Regular.otf"), "Sans Serif");
+
+ switch (options.theme) {
+ case "blue":
+ colors = {
+ background: "#1e1e3c",
+ barColor: "#747fff",
+ pieColors: ["#747fff", "#55b9f3", "#4dc7ec", "#3ad5e5", "#32e3dd", "#2cf2d4", "#26ffd2", "#30edb4", "#3cda96", "#48c878"],
+ textColor: "#FFFFFF"
+ };
+ break;
+
+ case "dark":
+ colors = {
+ background: "#1e1e1e",
+ barColor: "#747474",
+ pieColors: ["#747474", "#8f8f8f", "#a8a8a8", "#c1c1c1", "#dadada", "#f4f4f4", "#ffffff", "#ffffff", "#ffffff", "#ffffff"],
+ textColor: "#FFFFFF"
+ };
+ break;
+
+ case "discord":
+ colors = {
+ background: "#36393f",
+ barColor: "#5865F2",
+ pieColors: ["#5865F2", "#57F287", "#FEE75C", "#ED4245", "#F47FFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFFFF"],
+ textColor: "#FFFFFF"
+ };
+ break;
+
+ case "green":
+ colors = {
+ background: "#1e321e",
+ barColor: "#74ff7f",
+ pieColors: ["#74ff7f", "#55f3a0", "#4decb2", "#3dd5c3", "#32cdd5", "#2cc6e6", "#26bfee", "#30a8e6", "#3c91dd", "#487ad4"],
+ textColor: "#FFFFFF"
+ };
+ break;
+
+ case "orange":
+ colors = {
+ background: "#321e1e",
+ barColor: "#ff9f74",
+ pieColors: ["#ff9f74", "#f3b055", "#ecbe4d", "#d5c63d", "#cdd532", "#c6e62c", "#bfe626", "#a8e630", "#91dd3c", "#7ad448"],
+ textColor: "#FFFFFF"
+ };
+ break;
+
+ case "red":
+ colors = {
+ background: "#321e1e",
+ barColor: "#ff7474",
+ pieColors: ["#ff7474", "#f35555", "#ec4d4d", "#d53d3d", "#cd3232", "#c62c2c", "#bf2626", "#a83030", "#913c3c", "#7a4848"],
+ textColor: "#FFFFFF"
+ };
+ break;
+
+ case "space":
+ colors = {
+ background: "#001F3F",
+ barColor: "#192E5B",
+ pieColors: ["#192E5B", "#1F3F7F", "#264FA3", "#2C5FC7", "#337FEA", "#3D8FFF", "#4D9FFF", "#5DAFFF", "#6DBFFF", "#7DCFFF"],
+ textColor: "#FFFFFF"
+ };
+ break;
+
+ case "yellow":
+ colors = {
+ background: "#32321e",
+ barColor: "#ffff74",
+ pieColors: ["#ffff74", "#f3f355", "#ecf24d", "#d5eb3d", "#cde532", "#c6e02c", "#bfe626", "#a8df30", "#91d93c", "#7ad448"],
+ textColor: "#FFFFFF"
+ };
+ break;
+ }
+
+ const canvas = createCanvas(920, 600),
+ context = canvas.getContext("2d"),
+ maxLevel = Math.max(...users.map((user) => user.level));
+
+ RoundedBox(context, 0, 0, canvas.width, canvas.height, 25);
+ context.clip();
+
+ context.fillStyle = colors.background;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ if (options.theme === "space") {
+ // Clear the canvas
+ context.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Create a background gradient to represent the vastness of space
+ const spaceGradient = context.createRadialGradient(
+ canvas.width / 2, canvas.height / 2, 1,
+ canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height)
+ );
+ spaceGradient.addColorStop(0, "#000000"); // Dark black at the center
+ spaceGradient.addColorStop(1, "#001F3F"); // Dark blue at the outer edge
+ context.fillStyle = spaceGradient;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Add a realistic moon to the top left
+ const moonRadius = 100;
+ const moonGradient = context.createRadialGradient(
+ 150, 150, 10,
+ 150, 150, moonRadius
+ );
+ moonGradient.addColorStop(0, "#F2F2F2"); // Light gray color for the moon
+ moonGradient.addColorStop(0.8, "#D3D3D3"); // Slightly darker gray towards the edge
+ moonGradient.addColorStop(1, "#001F3F"); // Dark blue color for the shadow
+ context.fillStyle = moonGradient;
+
+ context.beginPath();
+ context.arc(150, 150, moonRadius, 0, 2 * Math.PI);
+ context.fill();
+
+ // Add distant planets with realistic colors
+ const planetColors = ["#6B6B6B", "#AA8F00", "#473E83", "#456579"];
+ for (let i = 0; i < planetColors.length; i++) {
+ const planetX = Math.random() * canvas.width;
+ const planetY = Math.random() * canvas.height;
+ const planetRadius = Math.random() * 50 + 30; // Varying sizes
+ context.beginPath();
+ context.arc(planetX, planetY, planetRadius, 0, 2 * Math.PI);
+ context.fillStyle = planetColors[i] || "#FFFFFF";
+ context.fill();
+ }
+
+ context.filter = "blur(5px)";
+ context.drawImage(canvas, 0, 0);
+ context.filter = "none";
+
+ // Add distant stars
+ for (let i = 0; i < 100; i++) {
+ const x = Math.random() * canvas.width;
+ const y = Math.random() * canvas.height;
+ const radius = Math.random() * 2; // Smaller stars for depth
+ context.beginPath();
+ context.arc(x, y, radius, 0, 2 * Math.PI);
+ context.fillStyle = "#FFFFFF";
+ context.fill();
+ }
+ }
+
+
+ switch (options.type) {
+ case "bar": {
+ const maxValueLabelWidth = context.measureText(maxLevel.toString()).width;
+
+ const chartAreaWidth = canvas.width - maxValueLabelWidth - 20 * 3 - 20 * 2;
+ const chartAreaHeight = canvas.height - 100 - 20 * 2;
+
+ const barWidth = chartAreaWidth / users.length - 20;
+
+ const chartStartX = 20 + maxValueLabelWidth + 20 * 2;
+ const chartStartY = canvas.height - 50 - 20;
+
+ await Promise.all(
+ users.map(async (user, index) => {
+ const barHeight = (user.level / maxLevel) * chartAreaHeight;
+
+ const barX = chartStartX + index * (barWidth + 20);
+ const barY = chartStartY - barHeight;
+
+ context.fillStyle = colors.barColor;
+ context.strokeStyle = colors.barColor;
+ context.lineWidth = 2;
+
+ RoundedBox(context, barX, barY, barWidth, barHeight, 10);
+
+ context.fill();
+ context.stroke();
+
+ const textX = barX + barWidth / 2; // Center x-coordinate for both username and level text
+
+
+ context.fillStyle = colors.textColor;
+ context.font = "22px Sans Serif";
+ const levelText = user.level.toString();
+ const levelTextWidth = context.measureText(levelText).width;
+ const levelTextY = barY - 10;
+
+ context.fillText(levelText, textX - levelTextWidth / 2, levelTextY);
+
+ const usernameText = user?.name || user.user;
+ let usernameTextWidth = context.measureText(usernameText).width;
+
+ context.font = `${Math.min(Math.floor(16 * (barWidth / usernameTextWidth)), 18)}px Sans Serif`;
+ usernameTextWidth = context.measureText(usernameText).width;
+
+ const usernameTextY = chartStartY + 30;
+
+
+ if (options.theme === "space") {
+ const textPadding = 5;
+ const textBackgroundWidth = usernameTextWidth + textPadding * 2;
+ const textBackgroundX = textX - textBackgroundWidth / 2;
+ const textBackgroundY = usernameTextY - 18; // Adjust the value as needed
+
+ context.fillStyle = "rgba(0, 0, 0, 0.5)"; // Translucent black background
+ context.fillRect(textBackgroundX, textBackgroundY, textBackgroundWidth, 22);
+ }
+
+ context.fillStyle = colors.textColor;
+ context.fillText(usernameText, textX - usernameTextWidth / 2, usernameTextY);
+ })
+ );
+
+ }
+ break;
+
+ case "doughnut": {
+ const chartAreaWidth = canvas.width - 20 * 2;
+ const chartAreaHeight = canvas.height - 20 * 2;
+ const totalLevelSum = users.reduce((sum, user) => sum + user.level, 0);
+
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+
+ const outerRadius = Math.min(chartAreaWidth, chartAreaHeight) / 3; // Adjust the divisor for a smaller outer radius
+ const innerRadius = outerRadius * 0.6; // Adjust the multiplier for the size of the hole
+
+ let startAngle = -Math.PI / 2;
+
+ await Promise.all(
+ users.map(async (user, index) => {
+ const slicePercentage = user.level / totalLevelSum;
+ const endAngle = startAngle + 2 * Math.PI * slicePercentage;
+ context.fillStyle = colors.pieColors[index % colors.pieColors.length] || "#FFFFFF";
+
+ context.beginPath();
+ context.moveTo(centerX + outerRadius * Math.cos(startAngle), centerY + outerRadius * Math.sin(startAngle));
+ context.arc(centerX, centerY, outerRadius, startAngle, endAngle);
+ context.lineTo(centerX + innerRadius * Math.cos(endAngle), centerY + innerRadius * Math.sin(endAngle));
+ context.arc(centerX, centerY, innerRadius, endAngle, startAngle, true);
+ context.closePath();
+ context.fill();
+
+ startAngle = endAngle;
+ })
+ );
+
+ // Render legend
+ const legendX = 20; // Legend position from left
+ const legendY = canvas.height - 20 - users.length * 20; // Legend position from bottom
+ const legendSpacing = 20; // Vertical spacing between legend items
+
+ context.fillStyle = "rgba(0,0,0,0.25)";
+ context.fillRect(legendX - 5, legendY - 5, 200, users.length * legendSpacing + 5);
+
+ context.font = "12px Sans Serif";
+ users.forEach((user, index) => {
+ const legendText = user?.name || user.user;
+ const legendColor = colors.pieColors[index % colors.pieColors.length];
+ const legendColorBoxX = legendX;
+ const legendItemY = legendY + index * legendSpacing;
+
+ // Place colored squares to the right and usernames to the left
+ context.fillStyle = legendColor || "#FFFFFF";
+ context.fillRect(legendColorBoxX, legendItemY, 15, 15);
+
+ context.fillStyle = colors.textColor;
+ context.fillText(legendText, legendColorBoxX + 20, legendItemY + 11.5);
+ });
+
+ }
+ break;
+
+ case "pie": {
+ const chartAreaWidth = canvas.width - 20 * 2;
+ const chartAreaHeight = canvas.height - 20 * 2;
+ const totalLevelSum = users.reduce((sum, user) => sum + user.level, 0);
+
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+
+ const radius = Math.min(chartAreaWidth, chartAreaHeight) / 3; // Adjust the divisor for a smaller radius
+
+ let startAngle = -Math.PI / 2;
+
+ await Promise.all(
+ users.map(async (user, index) => {
+ const slicePercentage = user.level / totalLevelSum;
+ const endAngle = startAngle + 2 * Math.PI * slicePercentage;
+ context.fillStyle = colors.pieColors[index % colors.pieColors.length] || "#FFFFFF";
+
+ context.beginPath();
+ context.moveTo(centerX, centerY);
+ context.arc(centerX, centerY, radius, startAngle, endAngle);
+ context.closePath();
+ context.fill();
+
+ startAngle = endAngle;
+ })
+ );
+
+ // Render legend
+ const legendX = 20; // Legend position from left
+ const legendY = canvas.height - 20 - users.length * 20; // Legend position from bottom
+ const legendSpacing = 20; // Vertical spacing between legend items
+
+ context.fillStyle = "rgba(0,0,0,0.25)";
+ context.fillRect(legendX - 5, legendY - 5, 200, users.length * legendSpacing + 5);
+
+ context.font = "12px Sans Serif";
+ users.forEach((user, index) => {
+ const legendText = user?.name || user.user;
+ const legendColor = colors.pieColors[index % colors.pieColors.length];
+ const legendColorBoxX = legendX;
+ const legendItemY = legendY + index * legendSpacing;
+
+ // Place colored squares to the right and usernames to the left
+ context.fillStyle = legendColor || "#FFFFFF";
+ context.fillRect(legendColorBoxX, legendItemY, 15, 15);
+
+ context.fillStyle = colors.textColor;
+ context.fillText(legendText, legendColorBoxX + 20, legendItemY + 11.5);
+ });
+
+ }
+ break;
- // const users = await leaderboard(guildId, options?.limit || 10);
+ default:
+ throw new XpFatal({function: "charts()", message: "Invalid chart type provided"});
+ }
- throw new XpFatal({function: "charts()", message: "[V2] Under Development | Should be here within 2-3 dev releases."});
+ return {
+ attachment: canvas.toBuffer("image/png"),
+ description: "Chart", name: "chart.png"
+ };
}
\ No newline at end of file
diff --git a/src/fetch.ts b/src/fetch.ts
index 70ebf80..cc5c02c 100644
--- a/src/fetch.ts
+++ b/src/fetch.ts
@@ -1,6 +1,7 @@
import {XpFatal} from "./functions/xplogs";
import {User} from "./leaderboard";
import {xp} from "../xp";
+import {UserResult} from "./functions/database";
/**
* Fetch user data
@@ -16,14 +17,17 @@ export async function fetch(userId: string, guildId: string, username: string):
if (!userId) throw new XpFatal({function: "create()", message: "User ID was not provided"});
if (!guildId) throw new XpFatal({function: "create()", message: "Guild ID was not provided"});
- const users: User[] = await (await import("./functions/database")).db.find({collection: "simply-xps", data: {guild: guildId}}) as User[];
- const user = users.find((u) => u.user === userId);
+ const users: User[] = await (await import("./functions/database")).db.find({
+ collection: "simply-xps", data: {guild: guildId}
+ }) as User[];
+
+ let user: User | UserResult | undefined = users.find((u) => u.user === userId);
if (!user) {
- if (xp.auto_create && username) return (await import("./create")).create(guildId, userId, username);
- throw new XpFatal({function: "fetch()", message: "User data not found"});
+ if (xp.auto_create && username) user = await (await import("./create")).create(guildId, userId, username);
+ else throw new XpFatal({function: "fetch()", message: "User data not found"});
}
const position = users.sort((a, b) => b.xp - a.xp).findIndex((u) => u.user === userId) + 1;
- return {name: user?.name, user: user.user, guild: user.guild, level: user.level, position: position, xp: user.xp};
+ return {name: user?.name, user: user.user, guild: user.guild, level: user.level, position, xp: user.xp};
}
\ No newline at end of file
diff --git a/src/functions/database.ts b/src/functions/database.ts
index bd23c86..43d031a 100644
--- a/src/functions/database.ts
+++ b/src/functions/database.ts
@@ -103,7 +103,8 @@ export class db {
switch (xp.dbType) {
case "mongodb":
- result = (xp.database as MongoClient).db().collection(query.collection).insertOne(query.data).catch(error => handleError(error, "createOne()")) as Document;
+ (xp.database as MongoClient).db().collection(query.collection).insertOne(query.data).catch(error => handleError(error, "createOne()"));
+ result = db.findOne(query);
break;
case "sqlite":
diff --git a/src/leaderboard.ts b/src/leaderboard.ts
index ce399ed..6ee17bf 100644
--- a/src/leaderboard.ts
+++ b/src/leaderboard.ts
@@ -14,7 +14,7 @@ export interface User {
guild: string;
user: string;
name?: string | null;
- position?: number;
+ position: number;
level: number;
xp: number;
}
diff --git a/src/migrate.ts b/src/migrate.ts
index 9cfe679..ce1269a 100644
--- a/src/migrate.ts
+++ b/src/migrate.ts
@@ -1,7 +1,7 @@
import {XpFatal, XpLog} from "./functions/xplogs";
-import {db} from "./functions/database";
+import {db, UserResult} from "./functions/database";
import {convertFrom, xp} from "../xp";
-import {MongoClient} from "mongodb";
+import {Document, MongoClient} from "mongodb";
import {Database} from "better-sqlite3";
import {checkPackageVersion} from "./connect";
@@ -60,45 +60,51 @@ export class migrate {
});
if (xp.dbType === dbType) return XpLog.info("migrate.fromDB()", "Same database received, that was unnecessary!");
+ let results: UserResult[];
switch (dbType) {
case "mongodb":
try {
- const goodVersion = await checkPackageVersion("mongodb");
- if (!goodVersion) return XpLog.err("migrate.fromDB()", "MongoDB V4 or higher is required");
+ if (!await checkPackageVersion("mongodb")) return XpLog.err("migrate.fromDB()", "MongoDB V4 or higher is required");
- (connection as MongoClient).db().collection("simply-xps").find().toArray().then(async (results) => {
- XpLog.debug("migrate.fromDB()", `FOUND ${results.length} DOCUMENTS`);
+ results = (connection as MongoClient).db().collection("simply-xps").find().toArray() as Document as UserResult[];
+ XpLog.debug("migrate.fromDB()", `FOUND ${results.length} DOCUMENTS`);
- for (const user of results) {
- if (!await db.findOne({
- collection: "simply-xps", data: {guild: user.guild, user: user.user}
- })) {
- await db.createOne({
- collection: "simply-xps",
- data: {guild: user.guild, user: user.user, xp: user.xp, level: user.level}
- });
- } else {
- await db.updateOne({
- collection: "simply-xps",
- data: {guild: user.guild, user: user.user}
- }, {
- collection: "simply-xps",
- data: {guild: user.guild, user: user.user, name: user.name, xp: user.xp, level: user.level}
- });
- }
- }
- return true;
- });
} catch (error) {
XpLog.err("migrate.fromDB()", error as string);
+ return false;
}
- return false;
+ break;
case "sqlite":
- // TODO: SQLite migration
- XpLog.warn("migrate.fromDB()", "SQLite migration is not yet supported, soon though!");
- return false;
+ try {
+ if (!await checkPackageVersion("sqlite")) return XpLog.err("migrate.fromDB()", "better-sqlite3 V7 or higher is required");
+
+ results = (connection as Database).prepare("SELECT * FROM `simply-xps`").all() as UserResult[];
+ XpLog.debug("migrate.fromDB()", `FOUND ${results.length} ROWS`);
+
+ } catch (error) {
+ XpLog.err("migrate.fromDB()", error as string);
+ return false;
+ }
+ break;
}
+
+ await Promise.all(results.map(async (user) => {
+ if (!await db.findOne({collection: "simply-xps", data: {guild: user.guild, user: user.user}})) {
+ return db.createOne({
+ collection: "simply-xps", data: {guild: user.guild, user: user.user, xp: user.xp, level: user.level}
+ });
+ } else {
+ return db.updateOne({
+ collection: "simply-xps", data: {guild: user.guild, user: user.user}
+ }, {
+ collection: "simply-xps",
+ data: {guild: user.guild, user: user.user, name: user.name, xp: user.xp, level: user.level}
+ });
+ }
+ }));
+
+ return true;
}
}
\ No newline at end of file
diff --git a/src/reset.ts b/src/reset.ts
index d0b3cec..90a8f1d 100644
--- a/src/reset.ts
+++ b/src/reset.ts
@@ -13,7 +13,7 @@ import {db} from "./functions/database";
* @returns {Promise}
* @throws {XpFatal} If an invalid type is provided or if the value is not provided.
*/
-export async function reset(userId: string, guildId: string, erase: boolean = false, username?: string ): Promise {
+export async function reset(userId: string, guildId: string, erase: boolean = false, username?: string): Promise {
if (!userId || !guildId) {
throw new XpFatal({function: "reset()", message: "Invalid parameters provided"});
}
diff --git a/tsconfig.json b/tsconfig.json
index 98cc788..e7601d4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -29,7 +29,7 @@
"lib"
],
"include": [
- "./src/fonts/Baloo-Regular.ttf",
+ "./src/fonts/BalooBhaijaan-Regular.otf",
"./src/**/*",
"xp.ts"
],
diff --git a/xp.ts b/xp.ts
index c06844a..4d75e3e 100644
--- a/xp.ts
+++ b/xp.ts
@@ -6,7 +6,7 @@ import {MongoClient} from "mongodb";
export interface XPClient {
dbType: "mongodb" | "sqlite";
- database: MongoClient | Database | undefined; // TODO: Remove undefined from database property
+ database: MongoClient | Database | undefined;
auto_create: boolean;
auto_purge: boolean;
notify: boolean;