├── .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 |
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