Added level system and move commands to categories

This commit is contained in:
Ryahn 2025-05-14 12:36:13 -05:00
parent 23460dfdbf
commit 70f8c23bbf
32 changed files with 825 additions and 60 deletions

37
.dockerignore Normal file
View File

@ -0,0 +1,37 @@
# Dependencies
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
# Environment variables
.env
.env.local
.env.*.local
# Version control
.git
.gitignore
# IDE and editor files
.idea
.vscode
*.swp
*.swo
# OS generated files
.DS_Store
Thumbs.db
# Build output
dist
build
coverage
# Docker files
Dockerfile
docker-compose.yml
.dockerignore
# Development config
botconfig.dev.json

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/.babelrc
/yarn.lock
/botconfig.json
/botconfig.dev.json

27
Dockerfile Normal file
View File

@ -0,0 +1,27 @@
# Use Debian Bookworm as the base image
FROM debian:bookworm-slim
# Install Node.js and npm
RUN apt-get update && apt-get install -y \
curl \
gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
fonts-liberation fonts-dejavu fontconfig \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Create app directory
WORKDIR /usr/src/app
# Copy package.json and package-lock.json (if available)
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy application code
COPY . .
# Start the application
CMD ["npm", "start"]

BIN
assets/level_clean.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

BIN
assets/progress_fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

View File

@ -0,0 +1,92 @@
const Embed = require("../../functions/embed")
const { updateUserXP, getUserXP } = require("../../models/user");
const { xpSystem } = require("../../functions/level/xpSystem");
module.exports = {
config: {
name: "adjustxp",
cooldown: 5000,
available: true,
usage: true,
permissions: [],
aliases: [],
roles: ['staff'],
dm: false,
},
run: async (client, message, args, db) => {
if (!args[0]) return message.reply("You must mention a user to adjust their XP", false);
if (message.mentionIds && message.mentionIds.length > 0) {
let targetUser;
try {
const server = await client.servers.fetch(message.server.id);
const member = await server.fetchMember(message.mentionIds[0]);
if (!member || !member.id || !member.id.user) {
throw new Error("Invalid member structure");
}
// Fetch the user using the user ID from the member object
targetUser = await client.users.fetch(member.id.user);
if (!targetUser || !targetUser.username) {
throw new Error("Could not fetch user information");
}
} catch (fetchError) {
console.error("Fetch error details:", fetchError.message);
return message.reply({
embeds: [new Embed()
.setDescription("Could not find the mentioned user.")
.setColor(`#FF0000`)]
}, false);
}
if (!args[1]) return message.reply("You must specify an amount to adjust the XP by. `!adjust_xp @USER +/-<AMOUNT>`", false);
// Validate the amount is a valid number
const amountStr = args[1].replace("+", "").replace("-", "");
const amount = parseInt(amountStr);
if (isNaN(amount)) {
return message.reply({
embeds: [new Embed()
.setDescription("Invalid amount specified. Please provide a valid number.")
.setColor(`#FF0000`)]
}, false);
}
try {
const userDoc = await getUserXP(targetUser.id);
const currentXP = userDoc.xp || 0;
const newXP = args[1].includes("+") ? currentXP + amount : currentXP - amount;
// Ensure the new XP value is not negative
if (newXP < 0) {
return message.reply({
embeds: [new Embed()
.setDescription("Cannot set XP below 0.")
.setColor(`#FF0000`)]
}, false);
}
await updateUserXP(targetUser, newXP);
const adjusted_amount = args[1].includes("+") ? `+${amount}` : `-${amount}`;
await xpSystem(client, message);
return message.reply({
embeds: [new Embed()
.setDescription(`XP adjusted for ${targetUser.username} by ${adjusted_amount}`)
.setColor(`#00FF00`)]
}, false);
} catch (error) {
console.error("Error updating XP:", error);
return message.reply({
embeds: [new Embed()
.setDescription("An error occurred while updating the XP.")
.setColor(`#FF0000`)]
}, false);
}
} else {
return message.reply("You must mention a user to adjust their XP", false);
}
},
};

View File

@ -1,4 +1,4 @@
const Embed = require("../functions/embed")
const Embed = require("../../functions/embed")
module.exports = {
config: {

View File

@ -1,10 +1,25 @@
const axios = require(`axios`);
const path = require('path');
const botConfig = require(path.join(__dirname, '../botconfig.json'));
const { generateUniqueId } = require(path.join(__dirname, '../functions/randomStr'));
const botConfig = require(path.join(__dirname, '../../botconfig.json'));
const { generateUniqueId } = require(path.join(__dirname, '../../functions/randomStr'));
const https = require('https');
const Invites = require(path.join(__dirname, '../models/registerInvite'));
const Embed = require(path.join(__dirname, '../functions/embed'));
const Invites = require(path.join(__dirname, '../../models/registerInvite'));
const Embed = require(path.join(__dirname, '../../functions/embed'));
const DELAY = '5m';
// Helper function to parse time string to milliseconds
const parseTimeToMs = (timeStr) => {
const unit = timeStr.slice(-1).toLowerCase();
const value = parseInt(timeStr.slice(0, -1));
switch(unit) {
case 's': return value * 1000; // seconds
case 'm': return value * 60 * 1000; // minutes
case 'h': return value * 60 * 60 * 1000; // hours
case 'd': return value * 24 * 60 * 60 * 1000; // days
default: return 300000; // default 5 minutes if invalid format
}
};
// Create axios instance outside of module.exports
const makeRequest = axios.create({
@ -17,6 +32,16 @@ const makeRequest = axios.create({
})
});
// Helper function to delete messages after delay
const deleteAfterDelay = async (message, delay = parseTimeToMs(DELAY)) => {
try {
await new Promise(resolve => setTimeout(resolve, delay));
await message.delete().catch(() => {});
} catch (error) {
console.error('Error deleting message:', error);
}
};
module.exports = {
config: {
name: `register`,
@ -29,24 +54,41 @@ module.exports = {
dm: true
},
run: async (client, message, args, db) => {
console.log('[Register Command] Command started with args:', {
args: args,
channelType: message.channel.type,
author: message.author.username,
guild: message.server?.name || 'DM'
});
try {
// If not in DM, check for permissions and roles
if (message.channel.type !== 'DirectMessage') {
console.log('[Register Command] Not in DM, checking permissions');
// Get allowed role IDs from config
const allowedRoleIds = module.exports.config.roles
.map(roleName => botConfig.roles[0][roleName])
.filter(Boolean);
console.log('[Register Command] Allowed role IDs:', allowedRoleIds);
console.log('[Register Command] User roles:', message.member.roles);
console.log('[Register Command] Bot config roles:', botConfig.roles[0]);
const hasRequiredRole = message.member.roles.some(roleId => allowedRoleIds.includes(roleId));
console.log('[Register Command] Has required role:', hasRequiredRole);
console.log('[Register Command] Is owner:', client.config.owners.includes(message.authorId));
if (!hasRequiredRole && !client.config.owners.includes(message.authorId)) {
return message.reply({
console.log('[Register Command] User lacks required permissions');
const reply = await message.reply({
embeds: [
new Embed()
.setColor("#FF0000")
.setDescription(`You don't have the required roles to use this command.`)
]
});
// Delete both messages after 5 minutes
deleteAfterDelay(message);
deleteAfterDelay(reply);
return;
}
}
@ -80,41 +122,46 @@ module.exports = {
await registerInvite.save();
return message.reply({
const reply = await message.reply({
content: `Run this command in a DM to the bot and use this invite code: \`${inviteCode}\`\n
**Usage:** \`!register ${inviteCode} <username> <email> <f95zone profile url>\`\n
**Note: This invite code will expire in 10 minutes.**`
});
return;
}
const [invite, username, email, profileUrl] = args;
if (!invite || !username || !email || !profileUrl) {
return message.reply({
const reply = await message.reply({
content: `[REG3] Please provide a invite code, username, email, and f95zone profile url. \n\n**Usage:** \`${client.prefix}register <invite> <username> <email> <f95zone profile url>\``
});
return;
}
// DM flow - Use client.database instead of direct model
const registerInvite = await Invites.findOne({ invite: invite });
if (!registerInvite) {
return message.reply({
const reply = await message.reply({
content: `[REG1] You don't have an invite code. Please run the command outside of DMs.`
});
return;
}
if (registerInvite.createdAt + 10 * 60 * 1000 < Date.now()) {
await registerInvite.deleteOne({ invite: invite });
return message.reply({
const reply = await message.reply({
content: `[REG2] Your invite code has expired. Please run the command outside of DMs.`
});
return;
}
if (invite !== registerInvite.invite) {
return message.reply({
const reply = await message.reply({
content: `[REG4] Invalid invite code. Please run the command outside of DMs.`
});
return;
}
let f95zoneId = profileUrl.split('/')[4];
@ -134,40 +181,49 @@ module.exports = {
if (data.status === 'success') {
await registerInvite.deleteOne({ invite: invite });
return message.reply({
content: `[REG5] You have been registered. Please check your email for verification.`
const reply = await message.reply({
content: `[REG5] You have been registered. Please check your email for verification.\n
**If you do not receive email in 5 minutes. Please check your spam or trash for email.**`
});
return;
} else if (data.status === 'invalid_email') {
return message.reply({
const reply = await message.reply({
content: `[REG5.1] An error occurred during registration. ${data.msg}`
});
return;
} else if (data.status === 'username_exists') {
return message.reply({
const reply = await message.reply({
content: `[REG5.2] An error occurred during registration. ${data.msg}`
});
return;
} else if (data.status === 'invalid_f95_id') {
return message.reply({
const reply = await message.reply({
content: `[REG5.3] An error occurred during registration. ${data.msg}`
});
return;
} else if (data.status === 'email_exists') {
return message.reply({
const reply = await message.reply({
content: `[REG5.4] An error occurred during registration. ${data.msg}`
});
return;
} else if (data.status === 'error') {
return message.reply({
const reply = await message.reply({
content: `[REG5.5] An error occurred during registration. ${data.msg}`
});
return;
}
} catch (apiError) {
return message.reply({
const reply = await message.reply({
content: `[REG6] An error occurred during registration. Please try again later.\n\`${apiError}\``
});
return;
}
} catch (error) {
console.error('Registration Error:', error);
return message.reply({
const reply = await message.reply({
content: `[REG7] An unexpected error occurred. Please try again later.\n\`${error}\``
});
return;
}
},
};

View File

@ -1,4 +1,4 @@
const Reload = require("../functions/reload")
const Reload = require("../../functions/reload")
module.exports = {
config: {
name: "reload",

View File

@ -1,6 +1,6 @@
const fs = require("fs");
const path = require("path");
const Embed = require("../functions/embed")
const Embed = require("../../functions/embed")
const Uploader = require("revolt-uploader");
module.exports = {

View File

@ -1,12 +1,12 @@
const Embed = require("../functions/embed")
const Embed = require("../../functions/embed")
const fetch = require('node-fetch-commonjs')
const config = require('../config')
const config = require('../../config')
module.exports = {
config: {
name: "gif",
usage: true,
cooldown: 5000,
cooldown: 10000,
available: true,
permissions: [],
roles: [],

View File

@ -1,4 +1,4 @@
const Embed = require("../functions/embed");
const Embed = require("../../functions/embed");
const axios = require('axios');
module.exports = {

View File

@ -1,6 +1,6 @@
const Embed = require("../functions/embed")
const CommandsDB = require('../models/commands');
const { random } = require('../functions/randomStr');
const Embed = require("../../functions/embed")
const CommandsDB = require('../../models/commands');
const { random } = require('../../functions/randomStr');
const moment = require('moment');
module.exports = {

View File

@ -1,8 +1,8 @@
const Embed = require(`../functions/embed`)
const Polls = require(`../functions/poll`)
const dhms = require(`../functions/dhms`);
const PollDB = require("../models/polls");
const SavedPolls = require(`../models/savedPolls`)
const Embed = require(`../../functions/embed`)
const Polls = require(`../../functions/poll`)
const dhms = require(`../../functions/dhms`);
const PollDB = require("../../models/polls");
const SavedPolls = require(`../../models/savedPolls`)
module.exports = {
config: {

View File

@ -1,8 +1,8 @@
const Embed = require(`../functions/embed`)
const Polls = require(`../functions/poll`)
const dhms = require(`../functions/dhms`);
const PollDB = require("../models/rule7");
const SavedPolls = require(`../models/savedPolls`)
const Embed = require(`../../functions/embed`)
const Polls = require(`../../functions/poll`)
const dhms = require(`../../functions/dhms`);
const PollDB = require("../../models/rule7");
const SavedPolls = require(`../../models/savedPolls`)
module.exports = {
config: {

View File

@ -1,4 +1,4 @@
const Embed = require("../functions/embed")
const Embed = require("../../functions/embed")
module.exports = {
config: {

View File

@ -1,4 +1,4 @@
const Embed = require("../functions/embed")
const Embed = require("../../functions/embed")
module.exports = {
config: {

View File

@ -1,7 +1,7 @@
const Embed = require("../functions/embed");
const Giveaway = require("../models/giveaways");
const SavedPolls = require("../models/savedPolls");
const { dependencies } = require("../package.json");
const Embed = require("../../functions/embed");
const Giveaway = require("../../models/giveaways");
const SavedPolls = require("../../models/savedPolls");
const { dependencies } = require("../../package.json");
module.exports = {
config: {
name: "info",

46
commands/utility/level.js Normal file
View File

@ -0,0 +1,46 @@
const { getUserXP } = require("../../models/user");
const generateLevelCard = require("../../functions/level/generate_level_card");
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
module.exports = {
config: {
name: "level",
usage: true,
cooldown: 5000,
available: true,
permissions: [],
roles: [],
dm: false,
aliases: ['lvl']
},
run: async (client, message, args, db) => {
try {
const user = await getUserXP(message.author.id);
const levelCardBuffer = await generateLevelCard(user);
// Create a temporary file path
const tempFilePath = path.join(os.tmpdir(), `level_card_${message.author.id}.png`);
// Save the buffer to the temporary file
await fs.writeFile(tempFilePath, levelCardBuffer);
// Upload the file
const attachment = await client.Uploader.uploadFile(tempFilePath, "level_card.png");
// Send the message with the attachment
await message.channel.sendMessage({
attachments: [attachment]
});
// Clean up the temporary file
await fs.unlink(tempFilePath).catch(console.error);
} catch (error) {
console.error('Error in level command:', error);
await message.channel.sendMessage({
content: "❌ An error occurred while generating your level card. Please try again later."
});
}
},
};

42
docker-compose.yml Normal file
View File

@ -0,0 +1,42 @@
version: '3.8'
services:
app:
build: .
container_name: zonies-bot
restart: unless-stopped
environment:
- MONGODB_URI=mongodb://mongodb:27017/zonies
volumes:
- ./assets:/usr/src/app/assets
- ./botconfig.json:/usr/src/app/botconfig.json
depends_on:
- mongodb
networks:
- app-network
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '1'
memory: 2G
mongodb:
image: mongo:6.0
container_name: zonies-mongodb
restart: unless-stopped
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mongodb_data:

View File

@ -5,6 +5,7 @@ const EditCollector = require(path.join(__dirname, "../functions/messageEdit"));
const CommandDB = require(path.join(__dirname, "../models/commands"));
const { isJson } = require(path.join(__dirname, "../functions/randomStr"));
const botConfig = require(path.join(__dirname, "../botconfig.json"));
const {xpSystem} = require(path.join(__dirname, "../functions/level/xpSystem"));
module.exports = async (client, message) => {
// Early return checks
@ -13,12 +14,18 @@ module.exports = async (client, message) => {
const isDM = message.channel.type === "DirectMessage";
// Get guild settings (if not DM)
const db = isDM ? { prefix: client.botConfig.prefix, language: 'en' } : await client.database.getGuild(message.server.id, true);
const db = isDM ? { prefix: client.config.prefix, language: 'en' } : await client.database.getGuild(message.server.id, true);
if (!isDM) {
await xpSystem(client, message);
}
// Check if message starts with prefix
if (!message.content.startsWith(db.prefix)) {
// Handle bot mention
if (message.content && (new RegExp(`^(<@!?${client.user.id}>)`)).test(message.content)) {
console.log('[MessageCreate] Bot was mentioned');
const mention = new Embed()
.setColor("#A52F05")
.setTitle(client.user.username)
@ -41,6 +48,7 @@ module.exports = async (client, message) => {
// Handle custom commands
let check = await CommandDB.findOne({ name: cmd }).select("name").lean();
if (check) {
console.log('[MessageCreate] Custom command found:', cmd);
CommandDB.findOne({ name: cmd }).then((data) => {
if (isJson(data.content)) {
let items = JSON.parse(data.content);
@ -53,6 +61,7 @@ module.exports = async (client, message) => {
// Command handling
let commandfile = client.commands.get(cmd) || client.commands.get(client.aliases.get(cmd));
if (commandfile) {
// DM Check - if command doesn't allow DMs and we're in a DM, return
if (isDM && !commandfile.config.dm) {

View File

@ -0,0 +1,88 @@
const { createCanvas, loadImage } = require('canvas');
const path = require('path');
/**
* Calculates the XP required for the next level
* @param {number} currentLevel - Current user level
* @returns {number} XP required for next level
*/
function calculateNextLevelXP(currentLevel) {
// Using the inverse of the level calculation formula from xpSystem.js
// level = 0.47 * sqrt(xp)
// Therefore, xp = (level/0.47)^2
return Math.ceil(Math.pow((currentLevel + 1) / 0.47, 2));
}
/**
* Generates a level card image for a user
* @param {Object} user - User object containing id, username, xp, and level
* @returns {Promise<Buffer>} - Returns a buffer containing the generated image
*/
async function generateLevelCard(user) {
// Create canvas with dimensions matching the background image
const canvas = createCanvas(1516, 662);
const ctx = canvas.getContext('2d');
try {
// Load background image
const background = await loadImage(path.join(__dirname, '../../assets/level_clean.png'));
ctx.drawImage(background, 0, 0, canvas.width, canvas.height);
// Load progress bar fill image
const progressFill = await loadImage(path.join(__dirname, '../../assets/progress_fill.png'));
// Calculate XP progress using the same formula as xpSystem.js
const currentLevelXP = Math.floor(Math.pow(user.level / 0.47, 2));
const nextLevelXP = Math.floor(calculateNextLevelXP(user.level));
const xpInLevel = user.xp - currentLevelXP;
const xpForLevel = nextLevelXP - currentLevelXP;
// Clamp progress between 0 and 1
const progress = Math.max(0, Math.min(1, xpInLevel / xpForLevel));
// Clamp displayed XP to not exceed xpForLevel
const displayXP = Math.min(Math.max(0, xpInLevel), xpForLevel);
// Draw username
ctx.font = 'bold 60px Arial';
ctx.fillStyle = '#FFFFFF';
ctx.textAlign = 'left';
ctx.fillText(user.username, 420, 250);
// Draw level
ctx.font = 'bold 48px Arial';
ctx.fillStyle = '#FFD700'; // Gold color for level
ctx.fillText(`Level ${user.level}`, 420, 330);
// Progress bar dimensions and position
const progressBarX = 472;
const progressBarY = 388;
const progressBarWidth = 800;
const progressBarHeight = 60;
// Draw progress bar fill
ctx.drawImage(
progressFill,
progressBarX,
progressBarY,
progressBarWidth * progress,
progressBarHeight
);
// Draw XP progress text inside the bar, right-aligned
ctx.font = 'bold 36px Arial';
ctx.fillStyle = '#FFFFFF';
ctx.textAlign = 'center';
ctx.fillText(
`${displayXP}/${xpForLevel} XP`,
progressBarX + progressBarWidth / 2,
progressBarY + progressBarHeight / 2 + 12 // adjust as needed
);
// Convert canvas to buffer
return canvas.toBuffer('image/png');
} catch (error) {
console.error('Error generating level card:', error);
throw error;
}
}
module.exports = generateLevelCard;

View File

@ -0,0 +1,84 @@
const Embed = require("../embed");
const { User, getUserXP, checkUser, updateUserMessageCount, updateUserXPAndLevel } = require("../../models/user");
const { XPSetting, getXPSettings } = require("../../models/xp_setting");
const logger = require("../logger");
const self = module.exports = {
xpSystem: async (client, message) => {
try {
// Check if message is from a server
if (!message.server) {
return; // Skip XP for DMs
}
await checkUser(message.author);
const user = await getUserXP(message.author.id);
const settings = await getXPSettings(message.server.id);
// If no settings exist for this server, create default settings
if (!settings) {
const defaultSettings = {
serverId: message.server.id,
messages_per_xp: 3,
min_xp_per_gain: 2,
max_xp_per_gain: 12,
weekend_multiplier: 2,
weekend_days: "sat,sun",
double_xp_enabled: false,
level_up_enabled: true,
level_up_channel: '01HF7B18Z864E10XSF22F9RFZQ'
};
await XPSetting.create(defaultSettings);
return; // Skip this message, will work from next message
}
user.message_count++;
if (user.message_count >= settings.messages_per_xp) {
user.message_count = 0;
let xpGain = Math.floor(Math.random() * (settings.max_xp_per_gain - settings.min_xp_per_gain + 1)) + settings.min_xp_per_gain;
if (settings.double_xp_enabled || self.isWeekend(settings.weekend_days)) {
xpGain *= settings.weekend_multiplier;
}
user.xp += xpGain;
const newLevel = self.calculateLevel(user.xp);
if (newLevel > user.level) {
user.level = newLevel;
if (settings.level_up_enabled) {
const channel = client.channels.get(settings.level_up_channel);
if (channel) {
await self.sendLevelUpMessage(channel, message.author, newLevel);
}
}
}
await updateUserXPAndLevel(message.author.id, user.xp, user.level, user.message_count);
logger.info('XP SYSTEM', `${message.author.username} gained ${xpGain} XP and is now level ${user.level}`);
} else {
await updateUserMessageCount(message.author.id, user.message_count);
}
} catch (error) {
logger.error('XP SYSTEM', error);
}
},
isWeekend: (weekendDays) => {
const today = new Date().toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase();
return weekendDays.split(',').includes(today);
},
calculateLevel: (xp) => Math.floor(0.47 * Math.sqrt(xp)),
sendLevelUpMessage: async (channel, user, newLevel) => {
const embed = {
title: 'Level Up!',
description: `Congratulations ${user.username}! You've reached level ${newLevel}!`,
colour: '#00FF00'
};
await channel.sendMessage({ embeds: [embed] });
}
};

View File

@ -1,7 +1,30 @@
const { readdirSync } = require("fs")
const { readdirSync, statSync } = require("fs")
const { join } = require("path")
const color = require("../functions/colorCodes")
/**
* Recursively gets all command files from a directory
* @param {string} dir - Directory to scan
* @returns {string[]} Array of command file paths
*/
function getCommandFiles(dir) {
const files = []
const items = readdirSync(dir)
for (const item of items) {
const path = join(dir, item)
const stat = statSync(path)
if (stat.isDirectory()) {
files.push(...getCommandFiles(path))
} else if (item.endsWith('.js')) {
files.push(path)
}
}
return files
}
/**
* Loads and registers all command files from the commands directory
* @param {Object} client - The Discord client instance
@ -10,14 +33,14 @@ const color = require("../functions/colorCodes")
module.exports = async (client) => {
try {
const commandsPath = join(__dirname, "..", "commands")
const commandFiles = readdirSync(commandsPath).filter(file => file.endsWith(".js"))
const commandFiles = getCommandFiles(commandsPath)
let loadedCommands = 0
let failedCommands = 0
for (const file of commandFiles) {
try {
const command = require(join(commandsPath, file))
const command = require(file)
// Validate command structure
if (!command.config?.name) {
@ -26,6 +49,8 @@ module.exports = async (client) => {
continue
}
console.log(color("%", `%b[Command_Handler]%7 :: Loading %e${command.config.name}%7 command`))
// Register command and aliases
client.commands.set(command.config.name, command)
if (command.config.aliases?.length) {

View File

@ -1,12 +1,27 @@
const { Client } = require("revolt.js");
const { Collection } = require('@discordjs/collection');
const { token, mongoDB, api } = require("./botconfig.json");
const logger = require('./functions/logger');
const checkPolls = require('./functions/checkPolls');
const color = require("./functions/colorCodes");
const Uploader = require("revolt-uploader");
const TranslationHandler = require('./handlers/translation');
const DatabaseHandler = require('./handlers/database');
// let config;
// if (process.env.NODE_ENV === "development") {
// config = require("./botconfig.dev.json");
// } else {
// config = require("./botconfig.json");
// }
const config = require("./botconfig.json");
// Validate required configuration
if (!config || !config.token || !config.mongoDB || !config.api) {
throw new Error('Missing required configuration. Please check your botconfig.json file.');
}
const { token, mongoDB, api } = config;
console.log(mongoDB)
class Bot {
constructor(config) {

BIN
level_card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

View File

@ -2,7 +2,7 @@ const { Schema, model } = require("mongoose");
const guilds = new Schema({
id: { type: String },
prefix: { type: String, default: "f!" },
prefix: { type: String, default: "!" },
language: { type: String, default: "en_EN" },
joined: { type: String, default: Date.now() / 1000 | 0 },
dm: { type: Boolean, default: true },

118
models/user.js Normal file
View File

@ -0,0 +1,118 @@
const { Schema, model } = require("mongoose");
const user = new Schema({
id: { type: String },
username: { type: String },
xp: { type: Number, default: 0 },
level: { type: Number, default: 1 },
message_count: { type: Number, default: 0 }
});
async function checkUser(user) {
const userId = user.id;
const username = user.username;
// Find user by id
const existingUser = await User.findOne({ id: userId });
// If user doesn't exist, create new user
if (!existingUser) {
await User.create({
id: userId,
username: username,
xp: 0,
level: 1,
message_count: 0
});
}
}
async function getUserXP(userId) {
let userDoc = await User.findOne({ id: userId });
// If user doesn't exist, create new user
if (!userDoc) {
userDoc = await User.create({
id: userId,
xp: 0,
level: 1,
message_count: 0
});
}
return userDoc;
}
async function updateUserXP(user, xp) {
const userId = user.id;
const userDoc = await User.findOne({ id: userId });
userDoc.xp = xp;
await userDoc.save();
}
async function updateUserLevel(user, level) {
const userId = user.id;
const userDoc = await User.findOne({ id: userId });
userDoc.level = level;
await userDoc.save();
}
async function updateUserXPAndLevel(userId, xp, level, message_count) {
const userDoc = await User.findOne({ id: userId });
if (!userDoc) {
return await User.create({
id: userId,
xp: xp,
level: level,
message_count: message_count
});
}
userDoc.xp = xp;
userDoc.level = level;
userDoc.message_count = message_count;
await userDoc.save();
}
async function updateUserMessageCount(userId, message_count) {
const userDoc = await User.findOne({ id: userId });
if (!userDoc) {
return await User.create({
id: userId,
xp: 0,
level: 1,
message_count: message_count
});
}
userDoc.message_count = message_count;
await userDoc.save();
}
/**
* Retrieves the top users sorted by XP
* @param {number} limit - Maximum number of users to return (default: 10)
* @returns {Promise<Array>} Array of user objects containing id, username, xp, and level
*/
async function getLeaderboard(limit = 10) {
const users = await User.find()
.sort({ xp: -1 })
.limit(limit)
.select('id username xp level');
return users.map(user => ({
user_id: user.id,
username: user.username,
xp: user.xp,
level: user.level
}));
}
const User = model("user", user);
module.exports = {
checkUser,
getUserXP,
updateUserXP,
updateUserLevel,
updateUserXPAndLevel,
getLeaderboard,
updateUserMessageCount,
User
};

30
models/xp_setting.js Normal file
View File

@ -0,0 +1,30 @@
const { Schema, model } = require("mongoose");
const xpSetting = new Schema({
messages_per_xp: { type: Number },
min_xp_per_gain: { type: Number },
max_xp_per_gain: { type: Number },
weekend_multiplier: { type: Number },
weekend_days: { type: String },
double_xp_enabled: { type: Boolean },
serverId: { type: String },
level_up_channel: { type: String },
level_up_enabled: { type: Boolean }
});
async function getXPSettings(serverId) {
const settings = await XPSetting.findOne({ serverId: serverId });
return settings;
}
async function updateXPSettings(serverId, settings) {
await XPSetting.updateOne({ serverId: serverId }, { $set: settings });
}
const XPSetting = model("xpSetting", xpSetting);
module.exports = {
getXPSettings,
updateXPSettings,
XPSetting
};

93
package-lock.json generated
View File

@ -21,6 +21,9 @@
"revolt.js": "npm:revolt.js-update@^7.0.0-beta.9",
"screen": "^0.2.10",
"wumpfetch": "^0.3.1"
},
"devDependencies": {
"cross-env": "^7.0.3"
}
},
"node_modules/@discordjs/collection": {
@ -385,6 +388,40 @@
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
@ -674,6 +711,13 @@
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
@ -1171,6 +1215,16 @@
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
@ -1359,6 +1413,29 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shiki": {
"version": "0.14.4",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.4.tgz",
@ -1655,6 +1732,22 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",

View File

@ -1,7 +1,8 @@
{
"main": "index.js",
"scripts": {
"start": "node index.js"
"start": "node index.js",
"dev": "cross-env NODE_ENV=development node index.js"
},
"license": "MIT",
"dependencies": {
@ -26,5 +27,8 @@
"Discord": "ainzooalgown",
"Revolt": "Ryahn#1337",
"GitHub": "https://github.com/Ryahn/zonies"
},
"devDependencies": {
"cross-env": "^7.0.3"
}
}

10
test.js
View File

@ -15,13 +15,11 @@
// })
(async () => {
const axios = require("axios");
const query = 'yeet'
const url = ('https://api.urbandictionary.com/v0/define?term=' + query)
const response = await axios.get(url);
const data = response.data;
const def = data.list[0];
const xp = Math.floor(0.47 * Math.sqrt(12345632346));
const level = Math.floor(xp / 100);
console.log(xp);
console.log(level);
})();