├── .gitignore ├── models ├── rewards.js └── levels.js ├── .github └── FUNDING.yml ├── LICENSE ├── package.json ├── .eslintrc.js ├── index.d.ts ├── test └── README.md ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | test.js 4 | -------------------------------------------------------------------------------- /models/rewards.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const RewardSchema = new mongoose.Schema({ 4 | guildID: { type: String }, 5 | rewards: { type: Array, default: [], required: true }, 6 | lastUpdated: { type: Date, default: new Date() }, 7 | }); 8 | 9 | module.exports = mongoose.model('Rewards', RewardSchema); 10 | -------------------------------------------------------------------------------- /models/levels.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const LevelSchema = new mongoose.Schema({ 4 | userID: { type: String }, 5 | guildID: { type: String }, 6 | xp: { type: Number, default: 0 }, 7 | level: { type: Number, default: 0 }, 8 | lastUpdated: { type: Date, default: new Date() }, 9 | }); 10 | 11 | module.exports = mongoose.model('Levels', LevelSchema); 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Nigbub 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://paypal.me/mraugu 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 MrAugu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-xp", 3 | "version": "1.1.18", 4 | "description": "A lightweight and easy to use economy framework for discord bots, uses MongoDB.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "mongoose": "^6.5.1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/MrAugu/discord-xp.git" 12 | }, 13 | "keywords": [ 14 | "discord", 15 | "economy", 16 | "discord.js", 17 | "bot", 18 | "bots", 19 | "leveling", 20 | "levels", 21 | "discord-levels", 22 | "discord-xp", 23 | "xp", 24 | "mongo", 25 | "mongoose", 26 | "mongodb", 27 | "discord-economy", 28 | "discord-eco", 29 | "framework" 30 | ], 31 | "author": "MrAugu", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/MrAugu/discord-xp/issues" 35 | }, 36 | "homepage": "https://github.com/MrAugu/discord-xp#readme", 37 | "devDependencies": { 38 | "eslint": "^8.21.0", 39 | "eslint-config-standard": "^17.0.0", 40 | "eslint-plugin-import": "^2.26.0", 41 | "eslint-plugin-n": "^15.2.4", 42 | "eslint-plugin-promise": "^6.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': 'eslint:recommended', 3 | 'env': { 4 | 'node': true, 5 | 'es6': true, 6 | }, 7 | 'parserOptions': { 8 | 'ecmaVersion': 2019, 9 | }, 10 | 'rules': { 11 | 'brace-style': ['error', 'stroustrup', { 'allowSingleLine': true }], 12 | 'comma-dangle': ['error', 'always-multiline'], 13 | 'comma-spacing': 'error', 14 | 'comma-style': 'error', 15 | 'curly': ['error', 'multi-line', 'consistent'], 16 | 'dot-location': ['error', 'property'], 17 | 'handle-callback-err': 'off', 18 | 'indent': ['error', 'tab'], 19 | 'max-nested-callbacks': ['error', { 'max': 4 }], 20 | 'max-statements-per-line': ['error', { 'max': 2 }], 21 | 'no-console': 'off', 22 | 'no-empty-function': 'error', 23 | 'no-floating-decimal': 'error', 24 | 'no-inline-comments': 'error', 25 | 'no-lonely-if': 'error', 26 | 'no-multi-spaces': 'error', 27 | 'no-multiple-empty-lines': ['error', { 'max': 2, 'maxEOF': 1, 'maxBOF': 0 }], 28 | 'no-shadow': ['error', { 'allow': ['err', 'resolve', 'reject'] }], 29 | 'no-trailing-spaces': ['error'], 30 | 'no-var': 'error', 31 | 'object-curly-spacing': ['error', 'always'], 32 | 'prefer-const': 'error', 33 | 'quotes': ['error', 'single'], 34 | 'semi': ['error', 'always'], 35 | 'space-before-blocks': 'error', 36 | 'space-before-function-paren': ['error', { 37 | 'anonymous': 'never', 38 | 'named': 'never', 39 | 'asyncArrow': 'always', 40 | }], 41 | 'space-in-parens': 'error', 42 | 'space-infix-ops': 'error', 43 | 'space-unary-ops': 'error', 44 | 'spaced-comment': 'error', 45 | 'yoda': 'error', 46 | }, 47 | }; 48 | 49 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for discord-xp v1.1.8 2 | // Project: https://github.com/MrAugu/discord-xp 3 | // Definitions by: Nico Finkernagel 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | import { Client } from "discord.js"; 7 | 8 | type User = { 9 | userID: string; 10 | guildID: string; 11 | xp: number; 12 | level: number; 13 | lastUpdated: Date; 14 | cleanXp: number; 15 | cleanNextLevelXp: number; 16 | }; 17 | 18 | type LeaderboardUser = { 19 | guildID: string; 20 | userID: string; 21 | xp: number; 22 | level: number; 23 | position: number; 24 | username: String | null; 25 | discriminator: String | null; 26 | }; 27 | 28 | declare module "discord-xp" { 29 | export default class DiscordXp { 30 | static async setURL(dbURL: string): Promise; 31 | static async createUser(userId: string, guildId: string): Promise; 32 | static async deleteUser(userId: string, guildId: string): Promise; 33 | static async deleteGuild(guildId: string): Promise; 34 | static async appendXp(userId: string, guildId: string, xp: number): Promise; 35 | static async appendLevel(userId: string, guildId: string, levels: number): Promise; 36 | static async setXp(userId: string, guildId: string, xp: number): Promise; 37 | static async setLevel(userId: string, guildId: string, level: number): Promise; 38 | static async fetch(userId: string, guildId: string, fetchPosition = false): Promise; 39 | static async subtractXp(userId: string, guildId: string, xp: number): Promise; 40 | static async subtractLevel(userId: string, guildId: string, level: number): Promise; 41 | static async fetchLeaderboard(guildId: String, limit: number): Promise; 42 | static async computeLeaderboard(client: Client, leaderboard: User[], fetchUsers = false): Promise; 43 | static xpFor(targetLevel: number): number; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Setting Up 3 | First things first, we include the module into the project. 4 | ```js 5 | const Levels = require("discord-xp"); 6 | ``` 7 | After that, you need to provide a valid mongo database url, and set it. You can do so by: 8 | ```js 9 | Levels.setURL("mongodb://..."); // You only need to do this ONCE per process. 10 | ``` 11 | 12 | *Examples assume that you have setted up the module as presented in 'Setting Up' section.* 13 | *Following examples assume that your `Discord.Client` is called `client`.* 14 | 15 | *Following examples assume that your `client.on("messageCreate", message` is called `message`.* 16 | 17 | *Following example contains isolated code which you need to integrate in your own command handler.* 18 | 19 | *Following example assumes that you are able to write asynchronous code (use `await`).* 20 | 21 | # Examples 22 | Examples: 23 | - [Allocating Random XP For Each Message Sent](https://github.com/MrAugu/discord-xp/blob/master/test/README.md#allocating-random-xp-for-each-message-sent) 24 | - [Rank Command](https://github.com/MrAugu/discord-xp/blob/master/test/README.md#rank-command) 25 | - [Leaderboard Command](https://github.com/MrAugu/discord-xp/blob/master/test/README.md#leaderboard-command) 26 | - [Position of a user in the leaderboard](https://github.com/MrAugu/discord-xp/blob/master/test/README.md#position-of-a-user-in-the-leaderboard) 27 | - [Canvacord Integration](https://github.com/MrAugu/discord-xp/blob/master/test/README.md#canvacord-integration) 28 | 29 | --- 30 | 31 | ## Allocating Random XP For Each Message Sent 32 | 33 | ```js 34 | client.on("messageCreate", async (message) => { 35 | if (!message.guild) return; 36 | if (message.author.bot) return; 37 | 38 | const randomAmountOfXp = Math.floor(Math.random() * 29) + 1; // Min 1, Max 30 39 | const hasLeveledUp = await Levels.appendXp(message.author.id, message.guild.id, randomAmountOfXp); 40 | if (hasLeveledUp) { 41 | const user = await Levels.fetch(message.author.id, message.guild.id); 42 | message.channel.send({ content: `${message.author}, congratulations! You have leveled up to **${user.level}**. :tada:` }); 43 | } 44 | }); 45 | ``` 46 | 47 | ## Rank Command 48 | 49 | ```js 50 | const target = message.mentions.users.first() || message.author; // Grab the target. 51 | 52 | const user = await Levels.fetch(target.id, message.guild.id); // Selects the target from the database. 53 | 54 | if (!user) return message.channel.send("Seems like this user has not earned any xp so far."); // If there isnt such user in the database, we send a message in general. 55 | 56 | message.channel.send(`> **${target.tag}** is currently level ${user.level}.`); // We show the level. 57 | ``` 58 | 59 | ## Leaderboard Command 60 | 61 | ```js 62 | const rawLeaderboard = await Levels.fetchLeaderboard(message.guild.id, 10); // We grab top 10 users with most xp in the current server. 63 | 64 | if (rawLeaderboard.length < 1) return reply("Nobody's in leaderboard yet."); 65 | 66 | const leaderboard = await Levels.computeLeaderboard(client, rawLeaderboard, true); // We process the leaderboard. 67 | 68 | const lb = leaderboard.map(e => `${e.position}. ${e.username}#${e.discriminator}\nLevel: ${e.level}\nXP: ${e.xp.toLocaleString()}`); // We map the outputs. 69 | 70 | message.channel.send(`**Leaderboard**:\n\n${lb.join("\n\n")}`); 71 | ``` 72 | 73 | ## Position of a user in the leaderboard 74 | ```js 75 | const target = message.mentions.users.first() || message.author; // Grab the target. 76 | 77 | const user = await Levels.fetch(target.id, message.guild.id, true); // Selects the target from the database. 78 | 79 | console.log(user.position); 80 | ``` 81 | 82 | ## Canvacord Integration 83 | 84 | Obviously you need the npm package `canvacord` for that. Install it with `npm install canvacord`. 85 | 86 | ```js 87 | const canvacord = require('canvacord'); 88 | 89 | const target = message.mentions.users.first() || message.author; // Grab the target. 90 | 91 | const user = await Levels.fetch(target.id, message.guild.id, true); // Selects the target from the database. 92 | 93 | const rank = new canvacord.Rank() // Build the Rank Card 94 | .setAvatar(target.displayAvatarURL({format: 'png', size: 512})) 95 | .setCurrentXP(user.xp) // Current User Xp 96 | .setRequiredXP(Levels.xpFor(user.level + 1)) // We calculate the required Xp for the next level 97 | .setRank(user.position) // Position of the user on the leaderboard 98 | .setLevel(user.level) // Current Level of the user 99 | .setProgressBar("#FFFFFF") 100 | .setUsername(target.username) 101 | .setDiscriminator(target.discriminator); 102 | 103 | rank.build() 104 | .then(data => { 105 | const attachment = new Discord.MessageAttachment(data, "RankCard.png"); 106 | message.channel.send(attachment); 107 | }); 108 | ``` 109 | While this previous example works **perfectly** fine a lot of people asked how they could only get the required xp needed for the next level and the actual xp progress in the current level. 110 | 111 | ```js 112 | 113 | .cleanXp // Gets the current xp in the current level 114 | .cleanNextLevelXp // Gets the actual xp needed to reach the next level 115 | 116 | ``` 117 | 118 | Resulting code: 119 | 120 | ```js 121 | const canvacord = require('canvacord'); 122 | 123 | const target = message.mentions.users.first() || message.author; // Grab the target. 124 | 125 | const user = await Levels.fetch(target.id, message.guild.id, true); // Selects the target from the database. 126 | 127 | const rank = new canvacord.Rank() // Build the Rank Card 128 | .setAvatar(target.displayAvatarURL({format: 'png', size: 512})) 129 | .setCurrentXP(user.cleanXp) // Current User Xp for the current level 130 | .setRequiredXP(user.cleanNextLevelXp) //The required Xp for the next level 131 | .setRank(user.position) // Position of the user on the leaderboard 132 | .setLevel(user.level) // Current Level of the user 133 | .setProgressBar("#FFFFFF") 134 | .setUsername(target.username) 135 | .setDiscriminator(target.discriminator); 136 | 137 | rank.build() 138 | .then(data => { 139 | const attachment = new Discord.MessageAttachment(data, "RankCard.png"); 140 | message.channel.send(attachment); 141 | }); 142 | 143 | ``` 144 | 145 | 146 | *It's time for you to get creative..* 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

Discord server

3 | 4 | # Discord-Xp 5 | - A lightweight and easy to use xp framework for discord bots, uses MongoDB. 6 | - If you need help feel free to join our discord server to talk and help you with your code. 7 | - If you encounter any of those fell free to open an issue in our github repository. 8 | 9 | # Download & Update 10 | You can download it from npm: 11 | ```cli 12 | npm i discord-xp 13 | ``` 14 | You can update to a newer version to receive updates using npm. 15 | ```cli 16 | npm update discord-xp 17 | ``` 18 | 19 | # Changelog 20 | - **25 August 2022** (v1.1.18): `WARNING: This version contains breaking changes in the way the package parses number input!` 21 | * The following methods now throw a TypeError if an invalid amount of xp was provided (xp is 0 or lower): `appendXp(), substractXp(), setXp()` 22 | 23 | - **07 August 2022** (v1.1.17): 24 | * Adding cleanDatabase() method. 25 | * Adding role rewards with the following methods: `createRoleReward(), deleteRoleReward(), fetchRoleReward()` 26 | 27 | - **27 May 2021** (v1.1.11): 28 | * Adding deleteGuild() method. 29 | 30 | - **3 April 2021** (v1.1.10): 31 | * Adding TS typings. 32 | 33 | - **25 February 2021** (v1.1.8): 34 | * Preventing further deprection warnings to be displayed, if you encounter any of these deprecation issues, update the module. 35 | 36 | - **22 November 2020** (v1.1.7): 37 | 38 | `WARNING: This semi-major version contains breaking changes in the way leaderboard computing function works.` 39 | * Added an optional `fetchPosition` argument to the `Levels.fetch` which will add the leaderboard rank as the `position` property. Caution: Will be slower on larger servers. 40 | * `Levels.computeLeaderboard` is now asynchronous and can take in a third parameter called `fetchUsers` which will fetch all users on the leaderboard. This parameter **does not** require additional Gateway Intents. Caution: Will be substantially slower if you do not have `Guild_Members` intent and catch some users beforehand. 41 | 42 | - **16 July 2020**: 43 | * Added `xpFor` method to calculate xp required for a specific level. 44 | ```js 45 | /* xpFor Example */ 46 | const Levels = require("discord-xp"); 47 | // Returns the xp required to reach level 30. 48 | var xpRequired = Levels.xpFor(30); 49 | 50 | console.log(xpRequired); // Output: 90000 51 | ``` 52 | 53 | # Setting Up 54 | First things first, we include the module into the project. 55 | ```js 56 | const Levels = require("discord-xp"); 57 | ``` 58 | After that, you need to provide a valid mongo database url, and set it. You can do so by: 59 | ```js 60 | Levels.setURL("mongodb://..."); // You only need to do this ONCE per process. 61 | ``` 62 | 63 | # Examples 64 | *Examples can be found in /test* 65 | 66 | # Methods 67 | 68 | **createRoleReward** 69 | 70 | Creates a role reward entry in database for the guild if it doesnt exist. 71 | ```js 72 | Levels.createRoleReward(, , ); 73 | ``` 74 | - Output: 75 | ``` 76 | Promise 77 | ``` 78 | **deleteRoleReward** 79 | 80 | Deletes a role reward entry in database for the guild. 81 | ```js 82 | Levels.deleteRoleReward(, ); 83 | ``` 84 | - Output: 85 | ``` 86 | Promise 87 | ``` 88 | **fetchRoleReward** 89 | 90 | Fetches a role reward entry in database for the guild. 91 | ```js 92 | Levels.fetchRoleReward(, ); 93 | ``` 94 | - Output: 95 | ``` 96 | Promise 97 | ``` 98 | **cleanDataBase** 99 | 100 | Cleans the database from unknown users for a guild. 101 | ```js 102 | Levels.CleanDatabase(, ); 103 | ``` 104 | - Output: 105 | ``` 106 | Promise 107 | ``` 108 | ---------------------------- 109 | **createUser** 110 | 111 | Creates an entry in database for that user if it doesnt exist. 112 | ```js 113 | Levels.createUser(, ); 114 | ``` 115 | - Output: 116 | ``` 117 | Promise 118 | ``` 119 | **deleteUser** 120 | 121 | If the entry exists, it deletes it from database. 122 | ```js 123 | Levels.deleteUser(, ); 124 | ``` 125 | - Output: 126 | ``` 127 | Promise 128 | ``` 129 | **deleteGuild** 130 | 131 | If the entry exists, it deletes it from database. 132 | ```js 133 | Levels.deleteGuild(); 134 | ``` 135 | - Output: 136 | ``` 137 | Promise 138 | ``` 139 | **appendXp** 140 | 141 | It adds a specified amount of xp to the current amount of xp for that user, in that guild. It re-calculates the level. It creates a new user with that amount of xp, if there is no entry for that user. 142 | ```js 143 | Levels.appendXp(, , ); 144 | ``` 145 | - Output: 146 | ``` 147 | Promise 148 | ``` 149 | **appendLevel** 150 | 151 | It adds a specified amount of levels to current amount, re-calculates and sets the xp reqired to reach the new amount of levels. 152 | ```js 153 | Levels.appendLevel(, , ); 154 | ``` 155 | - Output: 156 | ``` 157 | Promise 158 | ``` 159 | **setXp** 160 | 161 | It sets the xp to a specified amount and re-calculates the level. 162 | ```js 163 | Levels.setXp(, , ); 164 | ``` 165 | - Output: 166 | ``` 167 | Promise 168 | ``` 169 | **setLevel** 170 | 171 | Calculates the xp required to reach a specified level and updates it. 172 | ```js 173 | Levels.setLevel(, , ); 174 | ``` 175 | - Output: 176 | ``` 177 | Promise 178 | ``` 179 | **fetch** (**Updated recently!**) 180 | 181 | Retrives selected entry from the database, if it exists. 182 | ```js 183 | Levels.fetch(, , ); 184 | ``` 185 | - Output: 186 | ``` 187 | Promise 188 | ``` 189 | **subtractXp** 190 | 191 | It removes a specified amount of xp to the current amount of xp for that user, in that guild. It re-calculates the level. 192 | ```js 193 | Levels.subtractXp(, , ); 194 | ``` 195 | - Output: 196 | ``` 197 | Promise 198 | ``` 199 | **subtractLevel** 200 | 201 | It removes a specified amount of levels to current amount, re-calculates and sets the xp reqired to reach the new amount of levels. 202 | ```js 203 | Levels.subtractLevel(, , ); 204 | ``` 205 | - Output: 206 | ``` 207 | Promise 208 | ``` 209 | **fetchLeaderboard** 210 | 211 | It gets a specified amount of entries from the database, ordered from higgest to lowest within the specified limit of entries. 212 | ```js 213 | Levels.fetchLeaderboard(, ); 214 | ``` 215 | - Output: 216 | ``` 217 | Promise 218 | ``` 219 | **computeLeaderboard** (**Updated recently!**) 220 | 221 | It returns a new array of object that include level, xp, guild id, user id, leaderboard position, username and discriminator. 222 | ```js 223 | Levels.computeLeaderboard(, , ); 224 | ``` 225 | - Output: 226 | ``` 227 | Promise 228 | ``` 229 | **xpFor** 230 | 231 | It returns a number that indicates amount of xp required to reach a level based on the input. 232 | ```js 233 | Levels.xpFor(); 234 | ``` 235 | - Output: 236 | ``` 237 | Integer 238 | ``` 239 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const mongoose = require('mongoose'); 3 | const levels = require('./models/levels.js'); 4 | const rewards = require('./models/rewards.js'); 5 | let mongoUrl; 6 | 7 | // Checking if the person has NodeJS v16 or higher 8 | if(process.version.slice(1, 3) - 0 < 16) { 9 | throw new Error( 10 | `NodeJS Version 16 or newer is required, but you are using ${process.version}. See https://nodejs.org to update.`, 11 | ); 12 | } 13 | 14 | class DiscordXp { 15 | 16 | /** 17 | * @param {string} [dbUrl] - A valid mongo database URI. 18 | */ 19 | 20 | static async setURL(dbUrl) { 21 | if (!dbUrl) throw new TypeError('A database url was not provided.'); 22 | mongoUrl = dbUrl; 23 | return mongoose.connect(dbUrl); 24 | } 25 | 26 | /** 27 | * @param {string} [guildId] - Discord guild id. 28 | * @param {number} [level] - level. 29 | *@param {string} [roleId] - Level for which the reward will be created. 30 | */ 31 | 32 | static async createRoleReward(guildId, level, roleId) { 33 | if (!guildId) throw new TypeError('A guild id was not provided.'); 34 | if (!level) throw new TypeError('A level was not provided.'); 35 | if (!roleId) throw new TypeError('A role id was not provided.'); 36 | 37 | const guildEntry = await rewards.findOne({ guildID: guildId }); 38 | const isReward = await rewards.findOne({ 39 | guildID: guildId, 40 | rewards: { 41 | $elemMatch: { 42 | level: level, 43 | }, 44 | }, 45 | }); 46 | if(!guildEntry) { 47 | const newReward = new rewards({ 48 | guildID: guildId, 49 | rewards: { level: level, roleId: roleId }, 50 | }); 51 | 52 | await newReward.save().catch(e => console.log(`Failed to create role reward: ${e}`)); 53 | 54 | return newReward; 55 | } 56 | // guildEntry exists but level reward also 57 | if (guildEntry && isReward) return false; 58 | if(guildEntry && !isReward) { 59 | guildEntry.rewards.push({ level: level, roleId: roleId }); 60 | await guildEntry.save().catch(e => console.log(`Failed to create role reward: ${e}`)); 61 | return guildEntry; 62 | } 63 | } 64 | 65 | /** 66 | * @param {string} [guildId] - Discord guild id. 67 | * @param {number} [level] - Level for which the reward will be deleted. 68 | */ 69 | 70 | static async deleteRoleReward(guildId, level) { 71 | if (!guildId) throw new TypeError('A guild id was not provided.'); 72 | if (!level) throw new TypeError('A level was not provided.'); 73 | 74 | const isReward = await rewards.findOne({ 75 | guildID: guildId, 76 | rewards: { 77 | $elemMatch: { 78 | level: level, 79 | }, 80 | }, 81 | }); 82 | if (!isReward) return false; 83 | 84 | const filteredRewardEntries = isReward.rewards.filter(item => item.level !== level); 85 | 86 | isReward.rewards = filteredRewardEntries; 87 | isReward.lastUpdated = new Date(); 88 | 89 | await isReward.save().catch(e => console.log(`Failed to delete role reward: ${e}`)); 90 | 91 | return isReward; 92 | } 93 | 94 | /** 95 | * @param {string} [guildId] - Discord guild id. 96 | * @param {number} [level] - Level for which the reward will be fetched. 97 | */ 98 | 99 | static async fetchRoleReward(guildId, level) { 100 | if (!guildId) throw new TypeError('A guild id was not provided.'); 101 | if (!level) throw new TypeError('A level was not provided.'); 102 | 103 | const isReward = await rewards.findOne({ 104 | guildID: guildId, 105 | rewards: { 106 | $elemMatch: { 107 | level: level, 108 | }, 109 | }, 110 | }); 111 | if (!isReward) return false; 112 | 113 | const filteredRewardEntries = isReward.rewards.filter(item => item.level === level); 114 | 115 | return filteredRewardEntries[0]; 116 | } 117 | 118 | /** 119 | * @param {string} [client] - Your Discord.CLient. 120 | * @param {string} [guildId] - The guild which entries should be cleaned. 121 | */ 122 | 123 | static async cleanDatabase(client, guildId) { 124 | if (!client) throw new TypeError('A client was not provided.'); 125 | if (!guildId) throw new TypeError('A guild id was not provided.'); 126 | 127 | const users = await levels.find({ guildID: guildId }); 128 | 129 | // return users.slice(0, limit); 130 | 131 | const computedArray = []; 132 | 133 | 134 | for (const user of users) { 135 | try { 136 | const isUser = await client.users.fetch(user.userID); 137 | } 138 | catch (error) { 139 | computedArray.push(user.userID); 140 | return await levels.findOneAndDelete({ userID: user.userID, guildID: guildId }).catch(e => console.log(`Failed to delete user: ${e}`)); 141 | } 142 | } 143 | 144 | return computedArray; 145 | } 146 | 147 | /** 148 | * @param {string} [userId] - Discord user id. 149 | * @param {string} [guildId] - Discord guild id. 150 | */ 151 | 152 | static async createUser(userId, guildId) { 153 | if (!userId) throw new TypeError('An user id was not provided.'); 154 | if (!guildId) throw new TypeError('A guild id was not provided.'); 155 | 156 | const isUser = await levels.findOne({ userID: userId, guildID: guildId }); 157 | if (isUser) return false; 158 | 159 | const newUser = new levels({ 160 | userID: userId, 161 | guildID: guildId, 162 | }); 163 | 164 | await newUser.save().catch(e => console.log(`Failed to create user: ${e}`)); 165 | 166 | return newUser; 167 | } 168 | 169 | /** 170 | * @param {string} [userId] - Discord user id. 171 | * @param {string} [guildId] - Discord guild id. 172 | */ 173 | 174 | static async deleteUser(userId, guildId) { 175 | if (!userId) throw new TypeError('An user id was not provided.'); 176 | if (!guildId) throw new TypeError('A guild id was not provided.'); 177 | 178 | const user = await levels.findOne({ userID: userId, guildID: guildId }); 179 | if (!user) return false; 180 | 181 | await levels.findOneAndDelete({ userID: userId, guildID: guildId }).catch(e => console.log(`Failed to delete user: ${e}`)); 182 | 183 | return user; 184 | } 185 | 186 | /** 187 | * @param {string} [userId] - Discord user id. 188 | * @param {string} [guildId] - Discord guild id. 189 | * @param {number} [xp] - Amount of xp to append. 190 | */ 191 | 192 | static async appendXp(userId, guildId, xp) { 193 | if (!userId) throw new TypeError('An user id was not provided.'); 194 | if (!guildId) throw new TypeError('A guild id was not provided.'); 195 | if (xp <= 0 || !xp || isNaN(parseInt(xp))) throw new TypeError('An amount of xp was not provided/was invalid.'); 196 | 197 | 198 | const user = await levels.findOne({ userID: userId, guildID: guildId }); 199 | 200 | if (!user) { 201 | const newUser = new levels({ 202 | userID: userId, 203 | guildID: guildId, 204 | xp: xp, 205 | level: Math.floor(0.1 * Math.sqrt(xp)), 206 | }); 207 | 208 | await newUser.save().catch(e => console.log('Failed to save new user.')); 209 | 210 | return (Math.floor(0.1 * Math.sqrt(xp)) > 0); 211 | } 212 | 213 | user.xp += parseInt(xp, 10); 214 | user.level = Math.floor(0.1 * Math.sqrt(user.xp)); 215 | user.lastUpdated = new Date(); 216 | 217 | await user.save().catch(e => console.log(`Failed to append xp: ${e}`)); 218 | /* 219 | const isReward = await rewards.findOne({ guildID: guildId, rewards: level }); 220 | if(Math.floor(0.1 * Math.sqrt(user.xp -= xp)) < user.level && isReward) return isReward; 221 | */ 222 | return (Math.floor(0.1 * Math.sqrt(user.xp -= xp)) < user.level); 223 | } 224 | 225 | /** 226 | * @param {string} [userId] - Discord user id. 227 | * @param {string} [guildId] - Discord guild id. 228 | * @param {number} [levels] - Amount of levels to append. 229 | */ 230 | 231 | static async appendLevel(userId, guildId, levelss) { 232 | if (!userId) throw new TypeError('An user id was not provided.'); 233 | if (!guildId) throw new TypeError('A guild id was not provided.'); 234 | if (!levelss) throw new TypeError('An amount of levels was not provided.'); 235 | 236 | const user = await levels.findOne({ userID: userId, guildID: guildId }); 237 | if (!user) return false; 238 | 239 | user.level += parseInt(levelss, 10); 240 | user.xp = user.level * user.level * 100; 241 | user.lastUpdated = new Date(); 242 | 243 | user.save().catch(e => console.log(`Failed to append level: ${e}`)); 244 | 245 | return user; 246 | } 247 | 248 | /** 249 | * @param {string} [userId] - Discord user id. 250 | * @param {string} [guildId] - Discord guild id. 251 | * @param {number} [xp] - Amount of xp to set. 252 | */ 253 | 254 | static async setXp(userId, guildId, xp) { 255 | if (!userId) throw new TypeError('An user id was not provided.'); 256 | if (!guildId) throw new TypeError('A guild id was not provided.'); 257 | if (xp <= 0 || !xp || isNaN(parseInt(xp))) throw new TypeError('An amount of xp was not provided/was invalid.'); 258 | 259 | const user = await levels.findOne({ userID: userId, guildID: guildId }); 260 | if (!user) return false; 261 | 262 | user.xp = xp; 263 | user.level = Math.floor(0.1 * Math.sqrt(user.xp)); 264 | user.lastUpdated = new Date(); 265 | 266 | user.save().catch(e => console.log(`Failed to set xp: ${e}`)); 267 | 268 | return user; 269 | } 270 | 271 | /** 272 | * @param {string} [userId] - Discord user id. 273 | * @param {string} [guildId] - Discord guild id. 274 | * @param {number} [level] - A level to set. 275 | */ 276 | 277 | static async setLevel(userId, guildId, level) { 278 | if (!userId) throw new TypeError('An user id was not provided.'); 279 | if (!guildId) throw new TypeError('A guild id was not provided.'); 280 | if (!level) throw new TypeError('A level was not provided.'); 281 | 282 | const user = await levels.findOne({ userID: userId, guildID: guildId }); 283 | if (!user) return false; 284 | 285 | user.level = level; 286 | user.xp = level * level * 100; 287 | user.lastUpdated = new Date(); 288 | 289 | user.save().catch(e => console.log(`Failed to set level: ${e}`)); 290 | 291 | return user; 292 | } 293 | 294 | /** 295 | * @param {string} [userId] - Discord user id. 296 | * @param {string} [guildId] - Discord guild id. 297 | */ 298 | 299 | static async fetch(userId, guildId, fetchPosition = false) { 300 | if (!userId) throw new TypeError('An user id was not provided.'); 301 | if (!guildId) throw new TypeError('A guild id was not provided.'); 302 | 303 | const user = await levels.findOne({ 304 | userID: userId, 305 | guildID: guildId, 306 | }); 307 | if (!user) return false; 308 | 309 | if (fetchPosition === true) { 310 | const leaderboard = await levels.find({ 311 | guildID: guildId, 312 | }).sort([['xp', 'descending']]).exec(); 313 | 314 | user.position = leaderboard.findIndex(i => i.userID === userId) + 1; 315 | } 316 | 317 | 318 | /* To be used with canvacord or displaying xp in a pretier fashion, with each level the cleanXp stats from 0 and goes until cleanNextLevelXp when user levels up and gets back to 0 then the cleanNextLevelXp is re-calculated */ 319 | user.cleanXp = user.xp - this.xpFor(user.level); 320 | user.cleanNextLevelXp = this.xpFor(user.level + 1) - this.xpFor(user.level); 321 | 322 | return user; 323 | } 324 | 325 | /** 326 | * @param {string} [userId] - Discord user id. 327 | * @param {string} [guildId] - Discord guild id. 328 | * @param {number} [xp] - Amount of xp to subtract. 329 | */ 330 | 331 | static async subtractXp(userId, guildId, xp) { 332 | if (!userId) throw new TypeError('An user id was not provided.'); 333 | if (!guildId) throw new TypeError('A guild id was not provided.'); 334 | if (xp <= 0 || !xp || isNaN(parseInt(xp))) throw new TypeError('An amount of xp was not provided/was invalid.'); 335 | 336 | const user = await levels.findOne({ userID: userId, guildID: guildId }); 337 | if (!user) return false; 338 | 339 | user.xp -= xp; 340 | user.level = Math.floor(0.1 * Math.sqrt(user.xp)); 341 | user.lastUpdated = new Date(); 342 | 343 | user.save().catch(e => console.log(`Failed to subtract xp: ${e}`)); 344 | 345 | return user; 346 | } 347 | 348 | /** 349 | * @param {string} [userId] - Discord user id. 350 | * @param {string} [guildId] - Discord guild id. 351 | * @param {number} [levels] - Amount of levels to subtract. 352 | */ 353 | 354 | static async subtractLevel(userId, guildId, levelss) { 355 | if (!userId) throw new TypeError('An user id was not provided.'); 356 | if (!guildId) throw new TypeError('A guild id was not provided.'); 357 | if (!levelss) throw new TypeError('An amount of levels was not provided.'); 358 | 359 | const user = await levels.findOne({ userID: userId, guildID: guildId }); 360 | if (!user) return false; 361 | 362 | user.level -= levelss; 363 | user.xp = user.level * user.level * 100; 364 | user.lastUpdated = new Date(); 365 | 366 | user.save().catch(e => console.log(`Failed to subtract levels: ${e}`)); 367 | 368 | return user; 369 | } 370 | 371 | /** 372 | * @param {string} [guildId] - Discord guild id. 373 | * @param {number} [limit] - Amount of maximum enteries to return. 374 | */ 375 | 376 | 377 | static async fetchLeaderboard(guildId, limit) { 378 | if (!guildId) throw new TypeError('A guild id was not provided.'); 379 | if (!limit) throw new TypeError('A limit was not provided.'); 380 | 381 | const users = await levels.find({ guildID: guildId }).sort([['xp', 'descending']]).limit(limit).exec(); 382 | 383 | return users; 384 | } 385 | 386 | /** 387 | * @param {string} [client] - Your Discord.CLient. 388 | * @param {array} [leaderboard] - The output from 'fetchLeaderboard' function. 389 | */ 390 | 391 | static async computeLeaderboard(client, leaderboard, fetchUsers = false) { 392 | if (!client) throw new TypeError('A client was not provided.'); 393 | if (!leaderboard) throw new TypeError('A leaderboard id was not provided.'); 394 | 395 | if (leaderboard.length < 1) return []; 396 | 397 | const computedArray = []; 398 | 399 | if (fetchUsers) { 400 | for (const key of leaderboard) { 401 | const user = await client.users.fetch(key.userID) || { username: 'Unknown', discriminator: '0000' }; 402 | computedArray.push({ 403 | guildID: key.guildID, 404 | userID: key.userID, 405 | xp: key.xp, 406 | level: key.level, 407 | position: (leaderboard.findIndex(i => i.guildID === key.guildID && i.userID === key.userID) + 1), 408 | username: user.username, 409 | discriminator: user.discriminator, 410 | }); 411 | } 412 | } 413 | else { 414 | leaderboard.map(key => computedArray.push({ 415 | guildID: key.guildID, 416 | userID: key.userID, 417 | xp: key.xp, 418 | level: key.level, 419 | position: (leaderboard.findIndex(i => i.guildID === key.guildID && i.userID === key.userID) + 1), 420 | username: client.users.cache.get(key.userID) ? client.users.cache.get(key.userID).username : 'Unknown', 421 | discriminator: client.users.cache.get(key.userID) ? client.users.cache.get(key.userID).discriminator : '0000', 422 | })); 423 | } 424 | 425 | return computedArray; 426 | } 427 | 428 | /* 429 | * @param {number} [targetLevel] - Xp required to reach that level. 430 | */ 431 | static xpFor(targetLevel) { 432 | if (isNaN(targetLevel) || isNaN(parseInt(targetLevel, 10))) throw new TypeError('Target level should be a valid number.'); 433 | if (isNaN(targetLevel)) targetLevel = parseInt(targetLevel, 10); 434 | if (targetLevel < 0) throw new RangeError('Target level should be a positive number.'); 435 | return targetLevel * targetLevel * 100; 436 | } 437 | 438 | /** 439 | * @param {string} [guildId] - Discord guild id. 440 | */ 441 | 442 | static async deleteGuild(guildId) { 443 | if (!guildId) throw new TypeError('A guild id was not provided.'); 444 | 445 | const guild = await levels.findOne({ guildID: guildId }); 446 | if (!guild) return false; 447 | 448 | await levels.deleteMany({ guildID: guildId }).catch(e => console.log(`Failed to delete guild: ${e}`)); 449 | 450 | return guild; 451 | } 452 | } 453 | 454 | module.exports = DiscordXp; 455 | --------------------------------------------------------------------------------