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;