adb-plugin-levels 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitkeep ADDED
File without changes
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Levels & XP System
2
+
3
+ A plugin for AdvancedDiscordBot that adds experience points, levels, and role rewards based on user activity.
4
+
5
+ ## Features
6
+
7
+ - Track XP per user per guild
8
+ - Award XP on message activity (with configurable rate limiting)
9
+ - Level-up notifications
10
+ - Role rewards for reaching specific levels
11
+ - Leaderboard commands
12
+ - Admin configuration
13
+
14
+ ## Commands
15
+
16
+ - `/level` - Check your or another user's level and XP
17
+ - `/leaderboard` - View the server XP ranking (top 10)
18
+ - `/level-config` - Admin: Configure XP rates, cooldowns, and level-up channel
19
+ - `/level-roles` - Admin: Manage role rewards for specific levels
20
+
21
+ ## Installation
22
+
23
+ 1. Copy this folder to your bot's `plugins/` directory
24
+ 2. Restart the bot (required for slash command registration)
25
+ 3. Use `/level-config` to set up the plugin
26
+
27
+ ## Configuration
28
+
29
+ - `xpPerMessage`: Amount of XP awarded per message (default: 5)
30
+ - `xpCooldown`: Seconds between XP awards for the same user (default: 60)
31
+ - `xpPerMinuteLimit`: Maximum XP a user can earn per minute (default: 100)
32
+ - `levelUpChannelId`: Channel ID for level-up announcements (optional)
33
+
34
+ ## How It Works
35
+
36
+ XP is awarded based on message activity with anti-spam protection. Levels are calculated using the formula: `level = floor(sqrt(xp / 100))`.
37
+
38
+ When a user levels up:
39
+ - Their level is updated in the database
40
+ - A level-up event is emitted for other plugins to listen to
41
+ - A message is sent in the configured level-up channel (if set)
42
+ - Any role rewards for the new level are automatically assigned
43
+
44
+ ## Database Models
45
+
46
+ The plugin creates three namespaced collections:
47
+ - `plugin_adb-plugin-levels_level` - Stores user XP and levels
48
+ - `plugin_adb-plugin-levels_levelconfig` - Stores guild configuration
49
+ - `plugin_adb-plugin-levels_leaderole` - Stores level-to-role mappings
50
+
51
+ ## Dependencies
52
+
53
+ - discord.js ^14.14.1
54
+ - mongoose ^8.0.3
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,48 @@
1
+ const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
2
+
3
+ const data = new SlashCommandBuilder()
4
+ .setName('leaderboard')
5
+ .setDescription('View the server XP leaderboard');
6
+
7
+ async function execute(interaction) {
8
+ try {
9
+ const guildId = interaction.guild.id;
10
+ const Level = require('../../models/Level');
11
+
12
+ const leaderboard = await Level.find({ guildId })
13
+ .sort({ xp: -1 })
14
+ .limit(10)
15
+ .populate('userId', 'username discriminator avatar');
16
+
17
+ if (leaderboard.length === 0) {
18
+ await interaction.reply({
19
+ content: 'No one has earned XP yet!',
20
+ ephemeral: true
21
+ });
22
+ return;
23
+ }
24
+
25
+ const embed = new EmbedBuilder()
26
+ .setTitle(`${interaction.guild.name} Leaderboard`)
27
+ .setColor(0xffd700);
28
+
29
+ const description = leaderboard.map((entry, index) => {
30
+ const medal = ['🥇', '🥈', '🥉'][index] || `${index + 1}.`;
31
+ const xp = (entry.xp || 0).toLocaleString();
32
+ const level = Math.floor(Math.sqrt((entry.xp || 0) / 100));
33
+ const username = interaction.guild.members.cache.get(entry.userId)?.user.username || 'Unknown User';
34
+ return `${medal} **${username}** - Level ${level} • ${xp} XP`;
35
+ }).join('\n');
36
+
37
+ embed.setDescription(description);
38
+ await interaction.reply({ embeds: [embed] });
39
+ } catch (error) {
40
+ console.error(error);
41
+ await interaction.reply({
42
+ content: 'There was an error while fetching the leaderboard!',
43
+ ephemeral: true
44
+ });
45
+ }
46
+ }
47
+
48
+ module.exports = { data, execute };
@@ -0,0 +1,80 @@
1
+ const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
2
+
3
+ const data = new SlashCommandBuilder()
4
+ .setName('level-config')
5
+ .setDescription('Configure the levels/XP system')
6
+ .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
7
+ .addIntegerOption(option =>
8
+ option.setName('xp-per-message')
9
+ .setDescription('XP awarded per message (1-50)')
10
+ .setMinValue(1)
11
+ .setMaxValue(50))
12
+ .addIntegerOption(option =>
13
+ option.setName('xp-cooldown')
14
+ .setDescription('Seconds between XP awards (10-300)')
15
+ .setMinValue(10)
16
+ .setMaxValue(300))
17
+ .addIntegerOption(option =>
18
+ option.setName('xp-per-minute-limit')
19
+ .setDescription('Max XP per minute (10-1000)')
20
+ .setMinValue(10)
21
+ .setMaxValue(1000))
22
+ .addChannelOption(option =>
23
+ option.setName('level-up-channel')
24
+ .setDescription('Channel for level-up announcements')
25
+ .addChannelTypes(0)); // GUILD_TEXT
26
+
27
+ async function execute(interaction) {
28
+ try {
29
+ const guildId = interaction.guild.id;
30
+ const updates = {};
31
+
32
+ if (interaction.options.getInteger('xp-per-message') !== null) {
33
+ updates.xpPerMessage = interaction.options.getInteger('xp-per-message');
34
+ }
35
+ if (interaction.options.getInteger('xp-cooldown') !== null) {
36
+ updates.xpCooldown = interaction.options.getInteger('xp-cooldown');
37
+ }
38
+ if (interaction.options.getInteger('xp-per-minute-limit') !== null) {
39
+ updates.xpPerMinuteLimit = interaction.options.getInteger('xp-per-minute-limit');
40
+ }
41
+ if (interaction.options.getChannel('level-up-channel') !== null) {
42
+ updates.levelUpChannelId = interaction.options.getChannel('level-up-channel').id;
43
+ }
44
+
45
+ // Update guild config
46
+ const config = await ctx.db.getServerConfig(guildId) || {};
47
+ if (!config.xpPerModule) config.xpPerModule = {};
48
+ config.xpPerModule.levels = { ...config.xpPerModule.levels || {}, ...updates };
49
+ await ctx.db.updateServerConfig(guildId, config);
50
+
51
+ const embed = new EmbedBuilder()
52
+ .setTitle('Levels Configuration Updated')
53
+ .setColor(0x00ff00)
54
+ .setDescription('The levels/XP system settings have been updated.');
55
+
56
+ if (updates.xpPerMessage !== undefined) {
57
+ embed.addFields({ name: 'XP per Message', value: `${updates.xpPerMessage}` });
58
+ }
59
+ if (updates.xpCooldown !== undefined) {
60
+ addFields({ name: 'XP Cooldown', value: `${updates.xpCooldown}s` });
61
+ }
62
+ if (updates.xpPerMinuteLimit !== undefined) {
63
+ embed.addFields({ name: 'XP per Minute Limit', value: `${updates.xpPerMinuteLimit}` });
64
+ }
65
+ if (updates.levelUpChannelId !== undefined) {
66
+ const channel = interaction.guild.channels.cache.get(updates.levelUpChannelId);
67
+ embed.addFields({ name: 'Level-Up Channel', value: `${channel}` });
68
+ }
69
+
70
+ await interaction.reply({ embeds: [embed] });
71
+ } catch (error) {
72
+ console.error(error);
73
+ await interaction.reply({
74
+ content: 'There was an error while updating the configuration!',
75
+ ephemeral: true
76
+ });
77
+ }
78
+ }
79
+
80
+ module.exports = { data, execute };
@@ -0,0 +1,98 @@
1
+ const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
2
+
3
+ const data = new SlashCommandBuilder()
4
+ .setName('level-roles')
5
+ .setDescription('Manage role rewards for levels')
6
+ .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
7
+ .addSubcommand(subcommand =>
8
+ subcommand
9
+ .setName('add')
10
+ .setDescription('Add a role reward for a level')
11
+ .addIntegerOption(option =>
12
+ option.setName('level')
13
+ .setDescription('The level required')
14
+ .setMinValue(1)
15
+ .setRequired(true))
16
+ .addRoleOption(option =>
17
+ option.setName('role')
18
+ .setDescription('The role to reward')
19
+ .setRequired(true)))
20
+ .addSubcommand(subcommand =>
21
+ subcommand
22
+ .setName('remove')
23
+ .setDescription('Remove a role reward for a level')
24
+ .addIntegerOption(option =>
25
+ option.setName('level')
26
+ .setDescription('The level to remove')
27
+ .setMinValue(1)
28
+ .setRequired(true)))
29
+ .addSubcommand(subcommand =>
30
+ subcommand
31
+ .setName('list')
32
+ .setDescription('List all role rewards for this server'));
33
+
34
+ async function execute(interaction) {
35
+ try {
36
+ const guildId = interaction.guild.id;
37
+ const LevelRole = require('../../models/LevelRole');
38
+
39
+ const subcommand = interaction.options.getSubcommand();
40
+
41
+ if (subcommand === 'add') {
42
+ const level = interaction.options.getInteger('level');
43
+ const role = interaction.options.getRole('role');
44
+
45
+ await LevelRole.findOneAndUpdate(
46
+ { guildId, level },
47
+ { $set: { roleId: role.id } },
48
+ { upsert: true, new: true }
49
+ );
50
+
51
+ await interaction.reply({
52
+ content: `Successfully set ${role} as reward for reaching level ${level}!`,
53
+ ephemeral: true
54
+ });
55
+ }
56
+ else if (subcommand === 'remove') {
57
+ const level = interaction.options.getInteger('level');
58
+
59
+ await LevelRole.deleteOne({ guildId, level });
60
+
61
+ await interaction.reply({
62
+ content: `Successfully removed role reward for level ${level}!`,
63
+ ephemeral: true
64
+ });
65
+ }
66
+ else if (subcommand === 'list') {
67
+ const roles = await LevelRole.find({ guildId }).sort({ level: 1 });
68
+
69
+ if (roles.length === 0) {
70
+ await interaction.reply({
71
+ content: 'No role rewards configured for this server.',
72
+ ephemeral: true
73
+ });
74
+ return;
75
+ }
76
+
77
+ const embed = new EmbedBuilder()
78
+ .setTitle('Level Role Rewards')
79
+ .setColor(0x9b59b6);
80
+
81
+ const description = roles.map(r => {
82
+ const role = interaction.guild.roles.cache.get(r.roleId);
83
+ return `**Level ${r.level}** → ${role ? role : '@deleted-role'}`;
84
+ }).join('\n');
85
+
86
+ embed.setDescription(description);
87
+ await interaction.reply({ embeds: [embed] });
88
+ }
89
+ } catch (error) {
90
+ console.error(error);
91
+ await interaction.reply({
92
+ content: 'There was an error while managing level roles!',
93
+ ephemeral: true
94
+ });
95
+ }
96
+ }
97
+
98
+ module.exports = { data, execute };
@@ -0,0 +1,57 @@
1
+ const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
2
+
3
+ const data = new SlashCommandBuilder()
4
+ .setName('level')
5
+ .setDescription('Check your or another user\'s level and XP')
6
+ .addUserOption(option =>
7
+ option.setName('user')
8
+ .setDescription('The user to check')
9
+ .setRequired(false));
10
+
11
+ async function execute(interaction) {
12
+ try {
13
+ const targetUser = interaction.options.getUser('user') || interaction.user;
14
+ const guildId = interaction.guild.id;
15
+ const userId = targetUser.id;
16
+
17
+ const Level = require('../../models/Level');
18
+ const levelData = await Level.findOne({ guildId, userId });
19
+
20
+ if (!levelData) {
21
+ await interaction.reply({
22
+ content: `${targetUser} has not earned any XP yet.`,
23
+ ephemeral: true
24
+ });
25
+ return;
26
+ }
27
+
28
+ // Simple level formula: level = floor(sqrt(xp / 100))
29
+ const xp = levelData.xp || 0;
30
+ const level = Math.floor(Math.sqrt(xp / 100));
31
+ const xpForCurrentLevel = level * level * 100;
32
+ const xpForNextLevel = (level + 1) * (level + 1) * 100;
33
+ const xpNeeded = xpForNextLevel - xp;
34
+ const progressPercent = ((xp - xpForCurrentLevel) / (xpForNextLevel - xpForCurrentLevel)) * 100;
35
+
36
+ const embed = new EmbedBuilder()
37
+ .setTitle(`${targetUser.username}'s Level`)
38
+ .setThumbnail(targetUser.displayAvatarURL())
39
+ .addFields(
40
+ { name: 'Level', value: `${level}`, inline: true },
41
+ { name: 'XP', value: `${xp.toLocaleString()} / ${xpForNextLevel.toLocaleString()}`, inline: true },
42
+ { name: 'Progress', value: `${progressPercent.toFixed(1)}%`, inline: true }
43
+ )
44
+ .setColor(0x0099ff)
45
+ .setTimestamp();
46
+
47
+ await interaction.reply({ embeds: [embed] });
48
+ } catch (error) {
49
+ console.error(error);
50
+ await interaction.reply({
51
+ content: 'There was an error while fetching the level!',
52
+ ephemeral: true
53
+ });
54
+ }
55
+ }
56
+
57
+ module.exports = { data, execute };
package/index.js ADDED
@@ -0,0 +1,106 @@
1
+ const { SlashCommandBuilder } = require('discord.js');
2
+
3
+ async function load(ctx) {
4
+ ctx.logger.info('Levels plugin loading...');
5
+
6
+ // Register commands
7
+ const levelCmd = require('./commands/level');
8
+ const leaderboardCmd = require('./commands/leaderboard');
9
+ const levelConfigCmd = require('./commands/level-config');
10
+ const levelRolesCmd = require('./commands/level-roles');
11
+
12
+ ctx.registerCommand(levelCmd);
13
+ ctx.registerCommand(leaderboardCmd);
14
+ ctx.registerCommand(levelConfigCmd);
15
+ ctx.registerCommand(levelRolesCmd);
16
+
17
+ // Define models
18
+ const LevelSchema = require('./models/Level');
19
+ const LevelConfigSchema = require('./models/LevelConfig');
20
+ const LevelRoleSchema = require('./models/LevelRole');
21
+
22
+ const LevelModel = ctx.defineModel('Level', LevelSchema);
23
+ const LevelConfigModel = ctx.defineModel('LevelConfig', LevelConfigSchema);
24
+ const LevelRoleModel = ctx.defineModel('LevelRole', LevelRoleSchema);
25
+
26
+ // Listen for message events to award XP
27
+ ctx.client.on('messageCreate', async (message) => {
28
+ if (message.author.bot || !message.guild) return;
29
+
30
+ try {
31
+ // Get guild config
32
+ const config = await ctx.db.getServerConfig(message.guild.id) || {};
33
+ const xpPerMessage = config.xpPerModule?.levels?.xpPerMessage ?? 5;
34
+ const xpCooldown = config.xpPerModule?.levels?.xpCooldown ?? 60;
35
+ const xpPerMinuteLimit = config.xpPerModule?.levels?.xpPerMinuteLimit ?? 100;
36
+
37
+ // Simple rate limiting (in production use Redis or similar)
38
+ const now = Date.now();
39
+ const key = `xp_${message.guild.id}_${message.author.id}`;
40
+
41
+ // For simplicity, we'll just award XP with basic cooldown per user
42
+ // A real implementation would use a proper rate limiter
43
+ const lastXp = await ctx.db.get(`xp:${key}`) || 0;
44
+ if (now - lastXp < xpCooldown * 1000) return;
45
+
46
+ // Award XP
47
+ const levelData = await LevelModel.findOneAndUpdate(
48
+ { guildId: message.guild.id, userId: message.author.id },
49
+ { $inc: { xp: xpPerMessage }, $set: { lastMessageAt: new Date() } },
50
+ { upsert: true, new: true }
51
+ );
52
+
53
+ // Check for level up
54
+ const oldLevel = Math.floor(Math.sqrt(levelData.xp / 100)); // Simple level formula
55
+ const newLevel = Math.floor(Math.sqrt((levelData.xp + xpPerMessage) / 100));
56
+
57
+ if (newLevel > oldLevel) {
58
+ // Level up!
59
+ await LevelModel.updateOne(
60
+ { guildId: message.guild.id, userId: message.author.id },
61
+ { $set: { level: newLevel } }
62
+ );
63
+
64
+ // Emit level up event for other plugins
65
+ ctx.hooks.emitHook('onLevelUp', {
66
+ user: message.author,
67
+ newLevel: newLevel,
68
+ guild: message.guild
69
+ });
70
+
71
+ // Send level up message if channel configured
72
+ const levelUpChannelId = config.xpPerModule?.levels?.levelUpChannelId;
73
+ if (levelUpChannelId) {
74
+ const channel = message.guild.channels.cache.get(levelUpChannelId);
75
+ if (channel) {
76
+ channel.send(`${message.author} has reached level ${newLevel}! :tada:`);
77
+ }
78
+ }
79
+
80
+ // Check for role rewards
81
+ const roleRewards = await LevelRoleModel.find({ guildId: message.guild.id, level: newLevel });
82
+ for (const reward of roleRewards) {
83
+ const role = message.guild.roles.cache.get(reward.roleId);
84
+ if (role && !message.member.roles.cache.has(role.id)) {
85
+ await message.member.roles.add(role);
86
+ try {
87
+ await message.author.send(`You earned the ${role.name} role for reaching level ${newLevel}!`);
88
+ } catch (e) {
89
+ // DMs closed
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ // Update XP timestamp
96
+ await ctx.db.set(`xp:${key}`, now);
97
+
98
+ } catch (error) {
99
+ ctx.logger.error('Error in levels plugin:', error);
100
+ }
101
+ });
102
+
103
+ ctx.logger.info('Levels plugin loaded!');
104
+ }
105
+
106
+ module.exports = { load };
@@ -0,0 +1,17 @@
1
+ const { Schema, model } = require('mongoose');
2
+
3
+ const LevelSchema = new Schema({
4
+ guildId: { type: String, required: true, index: true },
5
+ userId: { type: String, required: true, index: true },
6
+ xp: { type: Number, default: 0 },
7
+ level: { type: Number, default: 0 },
8
+ lastMessageAt: { type: Date, default: Date.now }
9
+ }, {
10
+ collection: 'plugin_adb-plugin-levels_level',
11
+ timestamps: true
12
+ });
13
+
14
+ // Compound index for efficient queries
15
+ LevelSchema.index({ guildId: 1, userId: 1 }, { unique: true });
16
+
17
+ module.exports = LevelSchema;
@@ -0,0 +1,14 @@
1
+ const { Schema, model } = require('mongoose');
2
+
3
+ const LevelConfigSchema = new Schema({
4
+ guildId: { type: String, required: true, unique: true },
5
+ xpPerMessage: { type: Number, default: 5 },
6
+ xpCooldown: { type: Number, default: 60 },
7
+ xpPerMinuteLimit: { type: Number, default: 100 },
8
+ levelUpChannelId: { type: String, default: null }
9
+ }, {
10
+ collection: 'plugin_adb-plugin-levels_levelconfig',
11
+ timestamps: true
12
+ });
13
+
14
+ module.exports = LevelConfigSchema;
@@ -0,0 +1,15 @@
1
+ const { Schema, model } = require('mongoose');
2
+
3
+ const LevelRoleSchema = new Schema({
4
+ guildId: { type: String, required: true, index: true },
5
+ level: { type: Number, required: true, min: 1 },
6
+ roleId: { type: String, required: true }
7
+ }, {
8
+ collection: 'plugin_adb-plugin-levels_leaderole',
9
+ timestamps: true
10
+ });
11
+
12
+ // Compound index for efficient queries
13
+ LevelRoleSchema.index({ guildId: 1, level: 1 }, { unique: true });
14
+
15
+ module.exports = LevelRoleSchema;
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "adb-plugin-levels",
3
+ "version": "1.0.0",
4
+ "description": "Levels & XP System plugin for AdvancedDiscordBot",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "discord.js": "^14.14.1",
8
+ "mongoose": "^8.0.3"
9
+ }
10
+ }
package/plugin.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "adb-plugin-levels",
3
+ "displayName": "Levels & XP System",
4
+ "version": "1.0.0",
5
+ "description": "Track user activity with XP, levels, and role rewards",
6
+ "author": "dead",
7
+ "main": "index.js",
8
+ "requiresRestart": false,
9
+ "permissions": [
10
+ "db.read",
11
+ "db.write",
12
+ "commands.register",
13
+ "events.register"
14
+ ],
15
+ "configSchema": {
16
+ "type": "object",
17
+ "properties": {
18
+ "xpPerMessage": {
19
+ "type": "number",
20
+ "default": 5,
21
+ "minimum": 1,
22
+ "maximum": 50
23
+ },
24
+ "xpCooldown": {
25
+ "type": "number",
26
+ "default": 60,
27
+ "minimum": 10,
28
+ "maximum": 300
29
+ },
30
+ "xpPerMinuteLimit": {
31
+ "type": "number",
32
+ "default": 100,
33
+ "minimum": 10,
34
+ "maximum": 1000
35
+ },
36
+ "levelUpChannelId": {
37
+ "type": "string",
38
+ "default": null
39
+ }
40
+ }
41
+ }
42
+ }