From 38f1ae1ef13eaed0903afe7962a74f9c48ffeb53 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:20 +0300 Subject: [PATCH 001/175] chore: update Node.js and dependency compatibility --- package.json | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index fe3351e..815a8ab 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,32 @@ { "name": "EconomyBot", - "version": "1.0.0", - "description": "Economy bot created with quick.eco package in discord.js", + "version": "2.0.0", + "description": "Discord ekonomi botu - Node.js 20+ ve discord.js v14 uyumlu", "main": "index.js", "scripts": { "start": "node index.js" }, - "author": "Zero", + "author": "Zero / LoiFragola", "license": "Nginx", "keywords": [ "discord", - "Easy", - "Economy", + "economy", "bot", "discord.js", "discordjs" ], "dependencies": { - "discord.js": "^12.5.3", + "discord.js": "^14.27.0", "quick.eco": "^2.0.3" }, + "overrides": { + "better-sqlite3": "^12.4.1" + }, "engines": { - "node": "12.x" + "node": ">=20.0.0" }, "repository": { - "url": "https://github.com/ZeroDiscord/EconomyBot" + "type": "git", + "url": "https://github.com/LoiFragola/EconomyBot.git" } } From 7a9e2dcd55f8dca94008eb27d45a9ab4e61124fa Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:25 +0300 Subject: [PATCH 002/175] refactor: migrate client initialization to discord.js v14 --- index.js | 69 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/index.js b/index.js index 7c815b7..073e277 100644 --- a/index.js +++ b/index.js @@ -1,45 +1,44 @@ -const Discord = require("discord.js"); - const client = new Discord.Client({ disableMentions: 'everyone' }); +const { Client, Collection, GatewayIntentBits, Partials } = require("discord.js"); const Eco = require("quick.eco"); -client.eco = new Eco.Manager(); // quick.eco -client.db = Eco.db; // quick.db +const fs = require("fs"); + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent + ], + allowedMentions: { parse: ["users", "roles"] }, + partials: [Partials.Channel] +}); + +client.eco = new Eco.Manager(); +client.db = Eco.db; client.config = require("./botConfig"); -client.commands = new Discord.Collection(); -client.aliases = new Discord.Collection(); +client.commands = new Collection(); +client.aliases = new Collection(); + client.shop = { - "Laptop" : { - cost: 2000 - }, - "Mobile" : { - cost: 1000 - }, - "PC" : { - cost: 3000 - } + "Laptop": { cost: 2000 }, + "Mobile": { cost: 1000 }, + "PC": { cost: 3000 } }; -const fs = require("fs"); -fs.readdir("./events/", (err, files) => { - if (err) return console.error(err); - files.forEach(f => { - if (!f.endsWith(".js")) return; - const event = require(`./events/${f}`); - let eventName = f.split(".")[0]; - client.on(eventName, event.bind(null, client)); - }); +fs.readdirSync("./events/").forEach((file) => { + if (!file.endsWith(".js")) return; + const event = require(`./events/${file}`); + const eventName = file.split(".")[0]; + client.on(eventName, event.bind(null, client)); }); -fs.readdir("./commands/", (err, files) => { - if (err) return console.error(err); - files.forEach(f => { - if (!f.endsWith(".js")) return; - let command = require(`./commands/${f}`); - client.commands.set(command.help.name, command); - command.help.aliases.forEach(alias => { - client.aliases.set(alias, command.help.name); - }); - }); -}); +fs.readdirSync("./commands/").forEach((file) => { + if (!file.endsWith(".js")) return; + const command = require(`./commands/${file}`); + client.commands.set(command.help.name, command); + for (const alias of command.help.aliases) { + client.aliases.set(alias, command.help.name); + } +}); client.login(client.config.token); From 5ec1058ce70b0b944bfbd05ee4e75e23676daf5f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:31 +0300 Subject: [PATCH 003/175] refactor: modernize message command handler --- events/message.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/events/message.js b/events/message.js index 7118001..f270eb3 100644 --- a/events/message.js +++ b/events/message.js @@ -1,12 +1,18 @@ module.exports = async (client, message) => { - if (!message.guild || message.author.bot) return; - if (message.channel.id === client.config.countChannel) require("../counter")(message, client); - client.prefix = client.db.fetch(`prefix_${message.guild.id}`) ? client.db.fetch(`prefix_${message.guild.id}`) : client.config.prefix; - if (!message.content.startsWith(client.prefix)) return; - let args = message.content.slice(client.prefix.length).trim().split(" "); - let commandName = args.shift().toLowerCase(); - let command = client.commands.get(commandName) || client.commands.get(client.aliases.get(commandName)); - if (!command) return; - client.ecoAddUser = message.author.id; - command.execute(client, message, args); + if (!message.guild || message.author.bot) return; + + if (message.channel.id === client.config.countChannel) { + require("../counter")(message, client); + } + + client.prefix = client.db.fetch(`prefix_${message.guild.id}`) || client.config.prefix; + if (!message.content.startsWith(client.prefix)) return; + + const args = message.content.slice(client.prefix.length).trim().split(/\s+/); + const commandName = args.shift().toLowerCase(); + const command = client.commands.get(commandName) || client.commands.get(client.aliases.get(commandName)); + if (!command) return; + + client.ecoAddUser = message.author.id; + await command.execute(client, message, args); }; From 15c34c2fede2b027156c7b6517891c10100a3448 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:34 +0300 Subject: [PATCH 004/175] i18n: translate ready event output --- events/ready.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/events/ready.js b/events/ready.js index d654b82..6f9e5cd 100644 --- a/events/ready.js +++ b/events/ready.js @@ -1,4 +1,4 @@ module.exports = (client) => { - console.log(`${client.user.tag} is online!`); - client.user.setActivity("Coded by ZeroSync"); + console.log(`${client.user.tag} çevrimiçi!`); + client.user.setActivity("LoiFragola Economy"); }; From 95d26473a80dfd6f5eb8eee4e1c9b1ced335bf71 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:39 +0300 Subject: [PATCH 005/175] i18n: translate addmoney command --- commands/addmoney.js | 48 +++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/commands/addmoney.js b/commands/addmoney.js index ca940e8..72dcad2 100644 --- a/commands/addmoney.js +++ b/commands/addmoney.js @@ -1,25 +1,31 @@ -const { MessageEmbed } = require("discord.js"); +const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message, args) => { - if (!client.config.admins.includes(message.author.id)) return; // return if author isn't bot owner - let user = message.mentions.users.first(); - if (!user) return message.channel.send("Please specify a user!"); - let amount = args[1]; - if (!amount || isNaN(amount)) return message.reply("Please specify a valid amount."); - let data = client.eco.addMoney(user.id, parseInt(amount)); - const embed = new MessageEmbed() - .setTitle(`Money Added!`) - .addField(`User`, `<@${data.user}>`) - .addField(`Balance Given`, `${data.amount} 💸`) - .addField(`Total Amount`, data.after) - .setColor("RANDOM") - .setThumbnail(user.displayAvatarURL) - .setTimestamp(); - return message.channel.send(embed); -} + if (!client.config.admins.includes(message.author.id)) return; + + const user = message.mentions.users.first(); + if (!user) return message.channel.send("Lütfen bir kullanıcı belirtin!"); + + const amount = args[1]; + if (!amount || isNaN(amount)) return message.reply("Lütfen geçerli bir miktar belirtin."); + + const data = client.eco.addMoney(user.id, parseInt(amount)); + const embed = new EmbedBuilder() + .setTitle("Para Eklendi!") + .addFields( + { name: "Kullanıcı", value: `<@${data.user}>` }, + { name: "Eklenen Miktar", value: `${data.amount} 💸` }, + { name: "Toplam Bakiye", value: `${data.after} 💸` } + ) + .setColor("Random") + .setThumbnail(user.displayAvatarURL()) + .setTimestamp(); + + return message.channel.send({ embeds: [embed] }); +}; exports.help = { - name: "addmoney", - aliases: ["addbal"], - usage: `addmoney @user ` -} + name: "addmoney", + aliases: ["addbal"], + usage: "addmoney @kullanıcı " +}; From 51ad08fb9630894b61740e862b2c9534eebe058a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:43 +0300 Subject: [PATCH 006/175] i18n: translate balance command --- commands/bal.js | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/commands/bal.js b/commands/bal.js index 5e46e81..1dd395b 100644 --- a/commands/bal.js +++ b/commands/bal.js @@ -1,21 +1,25 @@ -const { MessageEmbed } = require("discord.js"); - -exports.execute = async (client, message, args) => { - let user = message.mentions.users.first() || message.author; - let userBalance = client.eco.fetchMoney(user.id); - const embed = new MessageEmbed() - .setTitle(`Balance`) - .addField(`User`, `<@${userBalance.user}>`) - .addField(`Balance`, `${userBalance.amount} 💸`) - .addField(`Position`, userBalance.position) - .setColor("RANDOM") - .setThumbnail(user.displayAvatarURL) - .setTimestamp(); - return message.channel.send(embed); -} - -exports.help = { - name: "bal", - aliases: ["money", "credits", "balance"], - usage: `bal` -} +const { EmbedBuilder } = require("discord.js"); + +exports.execute = async (client, message) => { + const user = message.mentions.users.first() || message.author; + const userBalance = client.eco.fetchMoney(user.id); + + const embed = new EmbedBuilder() + .setTitle("Bakiye") + .addFields( + { name: "Kullanıcı", value: `<@${userBalance.user}>` }, + { name: "Bakiye", value: `${userBalance.amount} 💸` }, + { name: "Sıralama", value: `${userBalance.position}` } + ) + .setColor("Random") + .setThumbnail(user.displayAvatarURL()) + .setTimestamp(); + + return message.channel.send({ embeds: [embed] }); +}; + +exports.help = { + name: "bal", + aliases: ["money", "credits", "balance"], + usage: "bal [@kullanıcı]" +}; From 6f60c04dec90ec50d1a08e7fc8b7b4487f2801c0 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:50 +0300 Subject: [PATCH 007/175] i18n: translate beg command --- commands/beg.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/commands/beg.js b/commands/beg.js index a1a7633..3b7a99d 100644 --- a/commands/beg.js +++ b/commands/beg.js @@ -1,19 +1,18 @@ -exports.execute = async (client, message, args) => { - let users = [ - "PewDiePie", - "T-Series", - "Sans", - "Zero" - ]; - let amount = Math.floor(Math.random() * 50) + 10; - let beg = client.eco.beg(client.ecoAddUser, amount, { canLose: true }); - if (beg.onCooldown) return message.reply(`Begon Thot! Come back after ${beg.time.seconds} seconds.`); - if (beg.lost) return message.channel.send(`**${users[Math.floor(Math.random() * users.length)]}:** Begon Thot! Try again later.`); - else return message.reply(`**${users[Math.floor(Math.random() * users.length)]}** donated you **${beg.amount}** 💸. Now you have **${beg.after}** 💸.`); -}; - -exports.help = { - name: "beg", - aliases: [], - usage: "beg" -} +exports.execute = async (client, message) => { + const users = ["PewDiePie", "T-Series", "Sans", "Zero"]; + const amount = Math.floor(Math.random() * 50) + 10; + const beg = client.eco.beg(client.ecoAddUser, amount, { canLose: true }); + + if (beg.onCooldown) return message.reply(`Tekrar dilenebilmek için ${beg.time.seconds} saniye beklemelisin.`); + if (beg.lost) { + return message.channel.send(`**${users[Math.floor(Math.random() * users.length)]}:** Şansın yaver gitmedi! Daha sonra tekrar dene.`); + } + + return message.reply(`**${users[Math.floor(Math.random() * users.length)]}** sana **${beg.amount}** 💸 bağışladı. Artık **${beg.after}** 💸 paran var.`); +}; + +exports.help = { + name: "beg", + aliases: [], + usage: "beg" +}; From 7a3bbd898a059f7676b9df667cb650181d4365d9 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:54 +0300 Subject: [PATCH 008/175] i18n: translate buy command --- commands/buy.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/commands/buy.js b/commands/buy.js index e624a33..2788e88 100644 --- a/commands/buy.js +++ b/commands/buy.js @@ -1,27 +1,31 @@ -const { MessageEmbed } = require("discord.js"); - exports.execute = async (client, message, args) => { - let userBalance = client.eco.fetchMoney(message.author.id); - if (userBalance.amount < 1) return message.channel.send("Looks like you are poor."); - let item = args[0]; - if (!item) return message.channel.send("What are you trying to buy?"); - let hasItem = client.shop[item]; - if (!hasItem || hasItem == undefined) return message.reply("That item doesnt exists lol"); - let isBalanceEnough = (userBalance.amount >= hasItem.cost); - if (!isBalanceEnough) return message.reply("Your balance is insufficient. You need :dollar: "+hasItem.cost+" to buy this item."); - let buy = client.eco.removeMoney(message.author.id, hasItem.cost); - - let itemStruct = { + const userBalance = client.eco.fetchMoney(message.author.id); + if (userBalance.amount < 1) return message.channel.send("Görünüşe göre hiç paran yok."); + + const item = args[0]; + if (!item) return message.channel.send("Ne satın almaya çalışıyorsun?"); + + const hasItem = client.shop[item]; + if (!hasItem) return message.reply("Böyle bir ürün bulunmuyor."); + + const isBalanceEnough = userBalance.amount >= hasItem.cost; + if (!isBalanceEnough) { + return message.reply(`Bakiyen yetersiz. Bu ürünü almak için 💸${hasItem.cost} gerekiyor.`); + } + + client.eco.removeMoney(message.author.id, hasItem.cost); + + const itemStruct = { name: item.toLowerCase(), prize: hasItem.cost }; - + client.db.push(`items_${message.author.id}`, itemStruct); - return message.channel.send(`You purchased **${item}** for **:dollar: ${hasItem.cost}**.`); + return message.channel.send(`**${item}** ürününü **💸${hasItem.cost}** karşılığında satın aldın.`); }; exports.help = { name: "buy", aliases: [], - usage: `buy ` + usage: "buy <ürün>" }; From 4f024d25a1c95823abce74382b6a8fda6146da01 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:26:58 +0300 Subject: [PATCH 009/175] i18n: translate daily command --- commands/daily.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/commands/daily.js b/commands/daily.js index 4017785..0425568 100644 --- a/commands/daily.js +++ b/commands/daily.js @@ -1,12 +1,16 @@ -module.exports.execute = async (client, message, args) => { - let amount = Math.floor(Math.random() * 500) + 100; - let addMoney = client.eco.daily(client.ecoAddUser, amount); - if (addMoney.onCooldown) return message.reply(`You have already claimed your daily credit. Come back after ${addMoney.time.hours} hours, ${addMoney.time.minutes} minutes & ${addMoney.time.seconds} seconds to claim it again.`); - else return message.reply(`You have claimed **${addMoney.amount}** 💸 as your daily credit & now you have **${addMoney.after}** 💸.`); -}; - -module.exports.help = { - name: "daily", - aliases: [], - usage: "daily" -} +module.exports.execute = async (client, message) => { + const amount = Math.floor(Math.random() * 500) + 100; + const addMoney = client.eco.daily(client.ecoAddUser, amount); + + if (addMoney.onCooldown) { + return message.reply(`Günlük ödülünü zaten aldın. Tekrar almak için ${addMoney.time.hours} saat, ${addMoney.time.minutes} dakika ve ${addMoney.time.seconds} saniye beklemelisin.`); + } + + return message.reply(`Günlük ödül olarak **${addMoney.amount}** 💸 kazandın. Artık **${addMoney.after}** 💸 paran var.`); +}; + +module.exports.help = { + name: "daily", + aliases: [], + usage: "daily" +}; From 8fc0c7e17a8a3c0b676e076930bbeb2fda2c152c Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:03 +0300 Subject: [PATCH 010/175] refactor: migrate help embed to discord.js v14 --- commands/help.js | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/commands/help.js b/commands/help.js index c868883..78aea21 100644 --- a/commands/help.js +++ b/commands/help.js @@ -1,23 +1,29 @@ -const { MessageEmbed } = require("discord.js"); +const { EmbedBuilder } = require("discord.js"); -exports.execute = async (client, message, args) => { - const embed = new MessageEmbed() - .setAuthor("Commands") - .setTitle("Make Sure To Check Out This Channel For More Such Bots") - .setURL("https://www.youtube.com/channel/UCF9E-xef9jL9QgziZRDHKKQ") - .setDescription(`Total Commands: ${client.commands.size}`) - .setColor("BLURPLE") - .setTimestamp() - .setThumbnail(client.user.displayAvatarURL) - .setFooter(message.author.tag, message.author.displayAvatarURL); - client.commands.forEach(cmd => { - embed.addField(`${cmd.help.name}`, `Aliases: ${cmd.help.aliases.join(", ") || "None"}\nUsage: \`${client.prefix}${cmd.help.usage}\``, true); +exports.execute = async (client, message) => { + const embed = new EmbedBuilder() + .setAuthor({ name: "Komutlar" }) + .setTitle("Daha fazla bot için kanalımıza göz atmayı unutma!") + .setURL("https://www.youtube.com/channel/UCF9E-xef9jL9QgziZRDHKKQ") + .setDescription(`Toplam Komut: ${client.commands.size}`) + .setColor("Blurple") + .setTimestamp() + .setThumbnail(client.user.displayAvatarURL()); + + client.commands.forEach((cmd) => { + embed.addFields({ + name: cmd.help.name, + value: `Takma Adlar: ${cmd.help.aliases.join(", ") || "Yok"}\nKullanım: \`${client.prefix}${cmd.help.usage}\``, + inline: true }); - return message.channel.send(embed); -} + }); + + embed.setFooter({ text: message.author.tag, iconURL: message.author.displayAvatarURL() }); + return message.channel.send({ embeds: [embed] }); +}; exports.help = { - name: "help", - aliases: ["h"], - usage: `help` -} + name: "help", + aliases: ["h"], + usage: "help" +}; From 513a595e4622ae9e86e4e378f0d22c4e9c57e93a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:18 +0300 Subject: [PATCH 011/175] refactor: migrate inventory embed to discord.js v14 --- commands/inventory.js | 47 +++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/commands/inventory.js b/commands/inventory.js index b524d0c..0f9fbf0 100644 --- a/commands/inventory.js +++ b/commands/inventory.js @@ -1,25 +1,32 @@ -const { MessageEmbed } = require("discord.js") -exports.execute = async (client, message, args) => { +const { EmbedBuilder } = require("discord.js"); - const embed = new MessageEmbed() - .setAuthor(`Inventory of ${message.author.tag}`, message.guild.iconURL) - .setColor("RANDOM") - .setThumbnail() +exports.execute = async (client, message) => { + const embed = new EmbedBuilder() + .setAuthor({ name: `${message.author.tag} kullanıcısının envanteri`, iconURL: message.guild.iconURL() || undefined }) + .setColor("Random") .setTimestamp(); - const x = client.db.get(`items_${message.author.id}`); -if(!x) { return message.channel.send(`No Items Found To Display`); } -const arrayToObject = x.reduce((itemsobj, x) => { - itemsobj[x.name] = (itemsobj[x.name] || 0) + 1; - return itemsobj; -}, {}); -const result = Object.keys(arrayToObject).map(k => embed.addField(`Name: ${k}`,`Quantity: **${arrayToObject[k]}**`, false)); - - - return message.channel.send(embed); -} + + const items = client.db.get(`items_${message.author.id}`); + if (!items) return message.channel.send("Gösterilecek eşya bulunamadı."); + + const arrayToObject = items.reduce((itemsObject, item) => { + itemsObject[item.name] = (itemsObject[item.name] || 0) + 1; + return itemsObject; + }, {}); + + Object.keys(arrayToObject).forEach((name) => { + embed.addFields({ + name: `İsim: ${name}`, + value: `Miktar: **${arrayToObject[name]}**`, + inline: false + }); + }); + + return message.channel.send({ embeds: [embed] }); +}; + exports.help = { name: "inventory", aliases: ["inv"], - usage: `inv` -} - + usage: "inventory" +}; From d28a695656b9fd1e8208e6ad23659838f1c76188 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:22 +0300 Subject: [PATCH 012/175] refactor: migrate leaderboard embed to discord.js v14 --- commands/leaderboard.js | 44 ++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/commands/leaderboard.js b/commands/leaderboard.js index 45305ee..1fb0a69 100644 --- a/commands/leaderboard.js +++ b/commands/leaderboard.js @@ -1,22 +1,30 @@ -const { MessageEmbed } = require("discord.js"); +const { EmbedBuilder } = require("discord.js"); -exports.execute = async (client, message, args) => { - - let leaderboard = client.eco.leaderboard({ limit: 15, raw: false }); - if (!leaderboard || leaderboard.length < 1) return message.channel.send("❌ | Empty Leaderboard!"); - const embed = new MessageEmbed() - .setAuthor(`Leaderboard of ${message.guild.name}!`, message.guild.iconURL) - .setColor("RANDOM") - .setThumbnail(client.users.cache.get(leaderboard[0].id) ? client.users.cache.get(leaderboard[0].id).displayAvatarURL : "https://cdn.discordapp.com/avatars/603948445362946084/a_f61398e073d78ae104e32b0517c891c3.gif") - .setTimestamp(); - leaderboard.forEach(u => { - embed.addField(`${u.position}. ${client.users.cache.get(u.id) ? client.users.cache.get(u.id).tag : "Unknown#0000"}`, `${u.money} 💸`); +exports.execute = async (client, message) => { + const leaderboard = client.eco.leaderboard({ limit: 15, raw: false }); + if (!leaderboard || leaderboard.length < 1) return message.channel.send("❌ | Sıralama boş!"); + + const firstUser = client.users.cache.get(leaderboard[0].id); + const embed = new EmbedBuilder() + .setAuthor({ name: `${message.guild.name} sıralaması!`, iconURL: message.guild.iconURL() || undefined }) + .setColor("Random") + .setThumbnail(firstUser ? firstUser.displayAvatarURL() : "https://cdn.discordapp.com/embed/avatars/0.png") + .setTimestamp(); + + leaderboard.forEach((user) => { + const discordUser = client.users.cache.get(user.id); + embed.addFields({ + name: `${user.position}. ${discordUser ? discordUser.tag : "Bilinmeyen Kullanıcı"}`, + value: `${user.money} 💸`, + inline: false }); - return message.channel.send(embed); -} + }); + + return message.channel.send({ embeds: [embed] }); +}; exports.help = { - name: "lb", - aliases: ["leaderboard"], - usage: `lb` -} + name: "lb", + aliases: ["leaderboard"], + usage: "lb" +}; From 030934831a4587457ed7712e40de1bb733aade32 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:26 +0300 Subject: [PATCH 013/175] refactor: migrate ping command to discord.js v14 --- commands/ping.js | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/commands/ping.js b/commands/ping.js index 18d70c9..0af0942 100644 --- a/commands/ping.js +++ b/commands/ping.js @@ -1,21 +1,24 @@ -const { MessageEmbed } = require("discord.js"); +const { EmbedBuilder } = require("discord.js"); -exports.execute = (client, message, args) => { - let gatewayLatency = Math.floor(client.ws.ping); - message.channel.send("Pinging...").then(m => { - const trip = Math.floor(m.createdTimestamp - message.createdTimestamp); - const embed = new MessageEmbed() - .setTitle("Pong!") - .addField("API Latency", `${gatewayLatency}ms`, true) - .addField("Client Latency", `${trip}ms`, true) - .setColor("#7289DA") - .setTimestamp(); - m.edit(embed); - }); -} +exports.execute = async (client, message) => { + const gatewayLatency = Math.floor(client.ws.ping); + const sentMessage = await message.channel.send("Ping ölçülüyor..."); + const trip = Math.floor(sentMessage.createdTimestamp - message.createdTimestamp); + + const embed = new EmbedBuilder() + .setTitle("Pong!") + .addFields( + { name: "API Gecikmesi", value: `${gatewayLatency}ms`, inline: true }, + { name: "İstemci Gecikmesi", value: `${trip}ms`, inline: true } + ) + .setColor("#7289DA") + .setTimestamp(); + + return sentMessage.edit({ content: null, embeds: [embed] }); +}; exports.help = { - name: "ping", - aliases: ["pong", "latency"], - usage: `ping` -} + name: "ping", + aliases: ["pong", "latency"], + usage: "ping" +}; From 5dce5ffece82c05215e7a4f607f1070d1c925cc1 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:31 +0300 Subject: [PATCH 014/175] refactor: update prefix permissions for discord.js v14 --- commands/prefix.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/commands/prefix.js b/commands/prefix.js index c59ced2..b025360 100644 --- a/commands/prefix.js +++ b/commands/prefix.js @@ -1,17 +1,20 @@ -exports.execute = (client, message, args) => { - if (!message.member.hasPermission("MANAGE_GUILD") && !client.config.admins.includes(message.member.id)) return message.channel.send(`My prefix for this server is **${client.prefix}**.`); - let prefix = args[0]; - if (!prefix) { - client.db.delete(`prefix_${message.guild.id}`); - return message.channel.send(`✅ | Prefix for this server has been reset.`); - } else { - let setTo = client.db.set(`prefix_${message.guild.id}`, prefix); - return message.channel.send(`✅ | Prefix set to \`${setTo}\`.`); - } -} - -exports.help = { - name: "prefix", - aliases: ["setprefix"], - usage: `prefix` -} +exports.execute = (client, message, args) => { + if (!message.member.permissions.has("ManageGuild") && !client.config.admins.includes(message.author.id)) { + return message.channel.send(`Bu sunucunun prefix'i **${client.prefix}**.`); + } + + const prefix = args[0]; + if (!prefix) { + client.db.delete(`prefix_${message.guild.id}`); + return message.channel.send("✅ | Bu sunucunun prefix'i sıfırlandı."); + } + + const setTo = client.db.set(`prefix_${message.guild.id}`, prefix); + return message.channel.send(`✅ | Prefix \`${setTo}\` olarak ayarlandı.`); +}; + +exports.help = { + name: "prefix", + aliases: ["setprefix"], + usage: "prefix [yeni-prefix]" +}; From dc0ed7f417e3aafa362dd84137d50eed7adaa00e Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:36 +0300 Subject: [PATCH 015/175] i18n: translate rob command --- commands/rob.js | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/commands/rob.js b/commands/rob.js index d5daa57..2e20477 100644 --- a/commands/rob.js +++ b/commands/rob.js @@ -1,25 +1,30 @@ exports.execute = async (client, message, args) => { - let target = message.mentions.members.first() || message.guild.members.cache.get(args[0]) - if(!target) return message.reply("Who are you trying to rob?") - let messages = [ - `You tripped while trying to rob ${target} and got caught!`, - `Getting sneaky eh? ${target} called the cops on you!`, - `You failed robbing ${target} becase you didn't subscribe to zerosync.` - ] - let amount = Math.floor(Math.random() * 50) + 10; - let rob = client.eco.beg(client.ecoAddUser, amount, { canLose: true }); - if (rob.onCooldown) return message.reply(`You have recently attempted to rob someone try again after ${rob.time.seconds} seconds.`); - if (rob.lost) return message.channel.send(messages[Math.floor(Math.random() * messages.length)]); - else { - let x = client.eco.fetchMoney(target.id).amount - amount - - client.eco.setMoney(target.id,parseInt(x)) - message.reply(`You robbed ${target} for **${rob.amount}** 💸. Now you have **${rob.after}** 💸.`); - } + const target = message.mentions.members.first() || message.guild.members.cache.get(args[0]); + if (!target) return message.reply("Kimi soymaya çalışıyorsun?"); + + const messages = [ + `${target} kullanıcısını soymaya çalışırken yakalandın!`, + `Sinsi davranmaya mı çalışıyorsun? ${target} polisi aradı!`, + `${target} kullanıcısını soyma girişimin başarısız oldu!` + ]; + + const amount = Math.floor(Math.random() * 50) + 10; + const rob = client.eco.beg(client.ecoAddUser, amount, { canLose: true }); + + if (rob.onCooldown) { + return message.reply(`Yakın zamanda bir soygun denedin. Tekrar denemek için ${rob.time.seconds} saniye beklemelisin.`); + } + + if (rob.lost) return message.channel.send(messages[Math.floor(Math.random() * messages.length)]); + + const targetBalance = client.eco.fetchMoney(target.id).amount - amount; + client.eco.setMoney(target.id, Math.max(0, parseInt(targetBalance))); + + return message.reply(`${target} kullanıcısından **${rob.amount}** 💸 çaldın. Artık **${rob.after}** 💸 paran var.`); }; exports.help = { - name: "rob", - aliases: [], - usage: "rob " -} + name: "rob", + aliases: [], + usage: "rob " +}; From 62827d4f8f25c2ccd3e293d2bf62bc92714b44e1 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:41 +0300 Subject: [PATCH 016/175] i18n: translate search command --- commands/search.js | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/commands/search.js b/commands/search.js index 4e83e3c..811aebb 100644 --- a/commands/search.js +++ b/commands/search.js @@ -1,19 +1,28 @@ -exports.execute = async (client, message, args) => { - let users = [ - "Pocket", - "T-Shirt", - "Zero's Databse", - "Street" - ]; - let amount = Math.floor(Math.random() * 200) + 50; - let beg = await client.eco.beg(client.ecoAddUser, amount, { canLose: true, cooldown: 300000, customName: "search" }); - if (beg.onCooldown) return message.reply(`Come back after ${beg.time.minutes} minutes & ${beg.time.seconds} seconds.`); - if (beg.lost) return message.channel.send(`**${users[Math.floor(Math.random() * users.length)]}:** You were caught! You couldn't get money kiddo.`); - else return message.reply(`**${users[Math.floor(Math.random() * users.length)]}** was somewhat profitable, you found **${beg.amount}** 💸. Now you have **${beg.after}** 💸.`); +exports.execute = async (client, message) => { + const places = [ + "Cep", + "Tişört", + "Zero'nun Veritabanı", + "Sokak" + ]; + + const amount = Math.floor(Math.random() * 200) + 50; + const search = client.eco.beg(client.ecoAddUser, amount, { + canLose: true, + cooldown: 300000, + customName: "search" + }); + + if (search.onCooldown) return message.reply(`${search.time.minutes} dakika ${search.time.seconds} saniye sonra tekrar dene.`); + if (search.lost) { + return message.channel.send(`**${places[Math.floor(Math.random() * places.length)]}:** Yakalandın! Para bulamadın.`); + } + + return message.reply(`**${places[Math.floor(Math.random() * places.length)]}** araması kârlı çıktı; **${search.amount}** 💸 buldun. Artık **${search.after}** 💸 paran var.`); }; exports.help = { - name: "search", - aliases: [], - usage: "search" -} + name: "search", + aliases: [], + usage: "search" +}; From e8b72409939fca047c5220a671e59605c00a69e1 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:47 +0300 Subject: [PATCH 017/175] i18n: translate setmoney command --- commands/setmoney.js | 46 +++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/commands/setmoney.js b/commands/setmoney.js index 7ad76bb..3389ad4 100644 --- a/commands/setmoney.js +++ b/commands/setmoney.js @@ -1,24 +1,30 @@ -const { MessageEmbed } = require("discord.js"); +const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message, args) => { - if (!client.config.admins.includes(message.author.id)) return; // return if author isn't bot owner - let user = message.mentions.users.first(); - if (!user) return message.channel.send("Please specify a user!"); - let amount = args[1]; - if (!amount || isNaN(amount)) return message.reply("Please specify a valid amount."); - let data = client.eco.setMoney(user.id, parseInt(amount)); - const embed = new MessageEmbed() - .setTitle(`Money Updated!`) - .addField(`User`, `<@${data.user}>`) - .addField(`Total Amount`, data.after) - .setColor("RANDOM") - .setThumbnail(user.displayAvatarURL) - .setTimestamp(); - return message.channel.send(embed); -} + if (!client.config.admins.includes(message.author.id)) return; + + const user = message.mentions.users.first(); + if (!user) return message.channel.send("Lütfen bir kullanıcı belirtin!"); + + const amount = args[1]; + if (!amount || isNaN(amount)) return message.reply("Lütfen geçerli bir miktar belirtin."); + + const data = client.eco.setMoney(user.id, parseInt(amount)); + const embed = new EmbedBuilder() + .setTitle("Para Güncellendi!") + .addFields( + { name: "Kullanıcı", value: `<@${data.user}>` }, + { name: "Toplam Bakiye", value: `${data.after} 💸` } + ) + .setColor("Random") + .setThumbnail(user.displayAvatarURL()) + .setTimestamp(); + + return message.channel.send({ embeds: [embed] }); +}; exports.help = { - name: "setmoney", - aliases: ["setbal"], - usage: `setmoney @user ` -} + name: "setmoney", + aliases: ["setbal"], + usage: "setmoney @kullanıcı " +}; From 6d039eca5b4c5707203b04b75cb5c8e150e5ffdf Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:27:52 +0300 Subject: [PATCH 018/175] refactor: migrate shop embed to discord.js v14 --- commands/shop.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/commands/shop.js b/commands/shop.js index eb450c7..ea80f1d 100644 --- a/commands/shop.js +++ b/commands/shop.js @@ -1,23 +1,20 @@ -const { MessageEmbed } = require("discord.js"); +const { EmbedBuilder } = require("discord.js"); -exports.execute = async (client, message, args) => { - let items = Object.keys(client.shop); - let content = ""; - - for (var i in items) { - content += `${items[i]} - :dollar: ${client.shop[items[i]].cost}\n` - } - - let embed = new MessageEmbed() - .setTitle("Store") - .setDescription(content) - .setColor("BLURPLE") - .setFooter("Do :?buy to purchase the item.") - return message.channel.send(embed); +exports.execute = async (client, message) => { + const items = Object.keys(client.shop); + const content = items.map((item) => `${item} - 💸 ${client.shop[item].cost}`).join("\n"); + + const embed = new EmbedBuilder() + .setTitle("Mağaza") + .setDescription(content) + .setColor("Blurple") + .setFooter({ text: `${client.prefix}buy <ürün> yazarak ürünü satın alabilirsin.` }); + + return message.channel.send({ embeds: [embed] }); }; exports.help = { name: "shop", aliases: [], - usage: `shop` + usage: "shop" }; From d145f338ad2e66043ac080fcbfcf1b70b167df9d Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:28:18 +0300 Subject: [PATCH 019/175] i18n: translate weekly command --- commands/weekly.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/commands/weekly.js b/commands/weekly.js index 8625edb..56745fe 100644 --- a/commands/weekly.js +++ b/commands/weekly.js @@ -1,12 +1,16 @@ -exports.execute = async (client, message, args) => { - let amount = Math.floor(Math.random() * 1000) + 500; - let addMoney = client.eco.weekly(client.ecoAddUser, amount); - if (addMoney.onCooldown) return message.reply(`You have already claimed your weekly credit. Come back after ${addMoney.time.days} days, ${addMoney.time.hours} hours, ${addMoney.time.minutes} minutes & ${addMoney.time.seconds} seconds to claim it again.`); - else return message.reply(`You have claimed **${addMoney.amount}** 💸 as your weekly credit & now you have **${addMoney.after}** 💸. But you will lose your balance if you do not subscribe to ZeroSync on yt :P`); -}; - -exports.help = { - name: "weekly", - aliases: [], - usage: "weekly" -} +exports.execute = async (client, message) => { + const amount = Math.floor(Math.random() * 1000) + 500; + const addMoney = client.eco.weekly(client.ecoAddUser, amount); + + if (addMoney.onCooldown) { + return message.reply(`Haftalık ödülünü zaten aldın. Tekrar almak için ${addMoney.time.days} gün, ${addMoney.time.hours} saat, ${addMoney.time.minutes} dakika ve ${addMoney.time.seconds} saniye beklemelisin.`); + } + + return message.reply(`Haftalık ödül olarak **${addMoney.amount}** 💸 kazandın. Artık **${addMoney.after}** 💸 paran var.`); +}; + +exports.help = { + name: "weekly", + aliases: [], + usage: "weekly" +}; From df9e30780b1344c1ef4e9fc7b0b18564ebc61f3a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:28:28 +0300 Subject: [PATCH 020/175] i18n: translate work command --- commands/work.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/commands/work.js b/commands/work.js index 84e957b..ca9a7cd 100644 --- a/commands/work.js +++ b/commands/work.js @@ -1,12 +1,16 @@ -module.exports.execute = async (client, message, args) => { - let amount = Math.floor(Math.random() * 1500) + 1000; - let work = client.eco.work(client.ecoAddUser, amount); - if (work.onCooldown) return message.reply(`You are tired rn. Come back after ${work.time.minutes} minutes & ${work.time.seconds} seconds to work again.`); - else return message.reply(`You worked as **${work.workedAs}** and earned **${work.amount}** 💸. Now you have **${work.after}** 💸.`); -}; - -module.exports.help = { - name: "work", - aliases: [], - usage: "work" -} +module.exports.execute = async (client, message) => { + const amount = Math.floor(Math.random() * 1500) + 1000; + const work = client.eco.work(client.ecoAddUser, amount); + + if (work.onCooldown) { + return message.reply(`Yorgunsun. Tekrar çalışmak için ${work.time.minutes} dakika ${work.time.seconds} saniye beklemelisin.`); + } + + return message.reply(`**${work.workedAs}** olarak çalıştın ve **${work.amount}** 💸 kazandın. Artık **${work.after}** 💸 paran var.`); +}; + +module.exports.help = { + name: "work", + aliases: [], + usage: "work" +}; From 41617db061e48e57112d53aa585ffdedf05280bf Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:28:35 +0300 Subject: [PATCH 021/175] refactor: modernize counter cleanup and translate messages --- counter.js | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/counter.js b/counter.js index 33027fc..ed3a0f0 100644 --- a/counter.js +++ b/counter.js @@ -1,30 +1,46 @@ const { db } = require("quick.eco"); function counter(message, client) { - let channel = message.channel; + const channel = message.channel; let count = db.fetch(`counter_${message.guild.id}`); - if (count === null) count = db.set(`counter_${message.guild.id}`, { - number: 0, - author: client.user.id - }); - + + if (count === null) { + count = db.set(`counter_${message.guild.id}`, { + number: 0, + author: client.user.id + }); + } + if (!message.author.bot && message.author.id === count.author) { - message.delete(); - message.reply("Please wait for your turn").then(m => m.delete(3000)); + message.delete().catch(() => {}); + message.reply("Sıra sende değil, lütfen bekle.").then((m) => { + setTimeout(() => m.delete().catch(() => {}), 3000); + }).catch(() => {}); return; } + if (!message.author.bot && isNaN(message.content)) { - message.delete(); - message.reply("messages in this channel must be a number").then(m => m.delete(3000)); + message.delete().catch(() => {}); + message.reply("Bu kanaldaki mesajlar sayı olmalıdır.").then((m) => { + setTimeout(() => m.delete().catch(() => {}), 3000); + }).catch(() => {}); return; } + if (!message.author.bot && parseInt(message.content) !== count.number + 1) { - message.delete(); - message.reply(`next number must be ${count.number + 1}`).then(m => m.delete(3000)); + message.delete().catch(() => {}); + message.reply(`Sıradaki sayı ${count.number + 1} olmalıdır.`).then((m) => { + setTimeout(() => m.delete().catch(() => {}), 3000); + }).catch(() => {}); return; } - count = db.set(`counter_${message.guild.id}`, { number: count.number + 1, author: message.author.id }); - channel.setTopic(`Next number must be ${count.number + 1}.`); + + count = db.set(`counter_${message.guild.id}`, { + number: count.number + 1, + author: message.author.id + }); + + channel.setTopic(`Sıradaki sayı ${count.number + 1} olmalıdır.`).catch(() => {}); } module.exports = counter; From bda73673e3c6a6a3b3ebbedb1e04d293a1da2aec Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:28:52 +0300 Subject: [PATCH 022/175] i18n: translate transfer command --- commands/transfer.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/commands/transfer.js b/commands/transfer.js index 168848a..44a4530 100644 --- a/commands/transfer.js +++ b/commands/transfer.js @@ -1,15 +1,19 @@ exports.execute = async (client, message, args) => { - let member = message.mentions.members.first() || message.guild.members.cache.get(args[0]) - let authordata = client.eco.fetchMoney(message.author.id) - if (!member) return message.channel.send('Please mention the person or give their ID') - let amount = args[1] - if (!amount || isNaN(amount)) return message.channel.send('Please enter a valid amount to transfer') - if(authordata.amount < amount) return message.channel.send('Looks like you don\'t have that much money') - await client.eco.transfer(message.author.id, member.id, amount) - return message.channel.send(`You have successfully transferred 💸**${amount}** to ** ${member.user.tag}**.`) -} + const member = message.mentions.members.first() || message.guild.members.cache.get(args[0]); + const authorData = client.eco.fetchMoney(message.author.id); + + if (!member) return message.channel.send("Lütfen kişiyi etiketle veya kullanıcı kimliğini gir."); + + const amount = args[1]; + if (!amount || isNaN(amount)) return message.channel.send("Lütfen aktarılacak geçerli bir miktar gir."); + if (authorData.amount < amount) return message.channel.send("Görünüşe göre bu kadar paran yok."); + + await client.eco.transfer(message.author.id, member.id, amount); + return message.channel.send(`💸 **${amount}** miktarını **${member.user.tag}** kullanıcısına başarıyla aktardın.`); +}; + exports.help = { name: "transfer", - aliases: ['give', 'share'], - usage: `transfer ` + aliases: ["give", "share"], + usage: "transfer " }; From 111f333dcf9781f08ef1807621cc5a73df64e42c Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:29:09 +0300 Subject: [PATCH 023/175] docs: update README for Node.js 20+ and Turkish usage --- README.md | 109 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index a39e1cf..e3f6bfb 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,68 @@ -# Welcome To EconomyBot -## This EconomyBot was written by 0_0#6666 -### Language : JavaScript/NodeJS (Core Lang : EN) -##### Library : Discord.js -###### Framework used : quick.eco by Snowflake Development - -##### IN BOTCONFIG.JSON DO NOT CHANGE ANY VARIABLE THE CountChannel var is for if you want to set a channel to count for users if not just leave it blank AND EVERYTHING ELSE IS SELF EPLAINATORY JUST CHANGE THE VALUES INSIDE "" TO MAKE YOUR BOT FUNCTION PROPERLY! -# Links -- 🔗 [Youtube Channel](https://www.youtube.com/channel/UCF9E-xef9jL9QgziZRDHKKQ) -- [Support Server Link](https://discord.gg/ARu4hr6hJw) -# Copyright -Copyright 2020 © All RIghts are Reserved | If you are using any part of code please give me credits for the same. Thanks - -# License -**nginx 2021 all rights reserved** - -# Features -- Shop -- Customisable Daily, weekly , search , crime and beg commands (earning) -- Balance -- Leaderboard -- CodeFactor : Easily usable -## Contribute -### Feel free to contribute to the repository by forking it and submitting a pull request we would love to have you as a contributor! You mst read through the nginx code of conduct and the (Included) license carefully before submitting a pull request. -#### Node Version Requirement -``12.x or higher`` - -**NOTE FOR GLITCH HOSTERS -`` THIS BOT DOES NOT DIE IT SENDS A HEARTBEAT (PING) EVERY 5 MINS TO THE GLITCH PROJECT SO THAT YOUR PROJECT STAYS ALIVE IF IT DOES NOT WORK FOR YOU THEN -DM ME FOR A SCRIPT THAT PREVENTS THIS JOIN THE SERVER ABOVE AND DM ME @0_0#6666 THIS MIGHT GET YOUR PROJECT SUSPENDED BUT YOU CAN ALWAYS -MAKE A NEW ONE USING MY TUTORIAL :D``** -``IT WORKS ON REPL.IT PRETTY FINE ITS BEEN TESTED ALREADY`` - -# Host On Repl.it -[![Use on Repl.it](https://repl.it/badge/github/ZeroDiscord/EconomyBot)](https://repl.it/github/ZeroDiscord/EconomyBot) -# Host On Glitch -[Click Here to Host On Glitch](https://glitch.com/edit/#!/import/git?url=https://github.com/ZeroDiscord/EconomyBot/) - -# Dependencies -- *Discord.js v12* -- *quick.eco (has a Dev dependency of quick.db and better-sqlite3*) +# EconomyBot 🇹🇷 + +Node.js ve Discord.js ile geliştirilmiş, kolay kurulabilen Discord ekonomi botu. + +## Özellikler + +- Mağaza ve envanter +- Günlük, haftalık, çalışma, arama ve dilenme komutları +- Bakiye sistemi +- Para transferi +- Sıralama tablosu +- Sunucuya özel prefix +- Sayaç kanalı + +## Gereksinimler + +- **Node.js 20.0.0 veya üzeri** +- **Discord.js v14** +- npm + +> Node.js 20, 22 ve 24 sürümleri hedeflenmiştir. + +## Kurulum + +1. Depoyu indirin. +2. Bağımlılıkları yükleyin: + +```bash +npm install +``` + +3. `botConfig.js` dosyasındaki değerleri doldurun. +4. Discord Developer Portal üzerinden bot için **Message Content Intent** özelliğini etkinleştirin. Bot mesaj içeriklerini prefix komutları için kullandığından bu izin gereklidir. +5. Botu başlatın: + +```bash +npm start +``` + +## Yapılandırma + +`botConfig.js` içerisindeki `token`, `prefix`, `admins`, `debug` ve `countChannel` değerlerini sunucunuza göre ayarlayın. + +## Komutlar + +| Komut | Kullanım | +|---|---| +| `help` | Komut listesini gösterir | +| `bal` | Bakiyeyi gösterir | +| `daily` | Günlük ödülü alır | +| `weekly` | Haftalık ödülü alır | +| `work` | Çalışarak para kazanır | +| `beg` | Para dilenir | +| `search` | Para arar | +| `rob` | Başka bir kullanıcıdan para çalmayı dener | +| `transfer` | Başka bir kullanıcıya para gönderir | +| `shop` | Mağazayı gösterir | +| `buy` | Mağazadan ürün satın alır | +| `inventory` | Envanteri gösterir | +| `lb` | Ekonomi sıralamasını gösterir | +| `ping` | Bot gecikmesini gösterir | +| `prefix` | Sunucu prefix'ini görüntüler veya değiştirir | +| `addmoney` | Yetkili kullanıcıya para ekler | +| `setmoney` | Yetkili kullanıcı için bakiyeyi ayarlar | + +## Lisans + +Bu proje, orijinal EconomyBot projesinin lisans koşullarına tabidir. Orijinal geliştiriciye uygun şekilde kredi verilmelidir. From 78e7eb2b2e708486f0213024109bf38e0f694165 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:32:34 +0300 Subject: [PATCH 024/175] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e3f6bfb..dc5d6f5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # EconomyBot 🇹🇷 Node.js ve Discord.js ile geliştirilmiş, kolay kurulabilen Discord ekonomi botu. +LoiFragola tarafından Türkçeleştirilmiş ve Node.js v20 üzerine uygun hale getirilmiştir. ## Özellikler From 448c560ec50fcbb0234ee3b8732569f18613e209 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:32:58 +0300 Subject: [PATCH 025/175] Fix typo in README description Corrected a typo in the README file. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc5d6f5..82173d1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # EconomyBot 🇹🇷 -Node.js ve Discord.js ile geliştirilmiş, kolay kurulabilen Discord ekonomi botu. +Node.js ve Discord.js ile geliştirilmiş, kolay kurulabilen Discord ekonomi botu.
LoiFragola tarafından Türkçeleştirilmiş ve Node.js v20 üzerine uygun hale getirilmiştir. ## Özellikler From 59b6776e4833d79690fd4be0154db92756d1950c Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:33:13 +0300 Subject: [PATCH 026/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82173d1..1c2184b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # EconomyBot 🇹🇷 Node.js ve Discord.js ile geliştirilmiş, kolay kurulabilen Discord ekonomi botu.
-LoiFragola tarafından Türkçeleştirilmiş ve Node.js v20 üzerine uygun hale getirilmiştir. +
LoiFragola tarafından Türkçeleştirilmiş ve Node.js v20 üzerine uygun hale getirilmiştir. ## Özellikler From fad5b1023afbd3cb4f1e0c62044d9e579a6b2c43 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:45:48 +0300 Subject: [PATCH 027/175] feat: add guild slash command definitions --- slashCommands.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 slashCommands.js diff --git a/slashCommands.js b/slashCommands.js new file mode 100644 index 0000000..921df89 --- /dev/null +++ b/slashCommands.js @@ -0,0 +1,75 @@ +const { SlashCommandBuilder } = require("discord.js"); + +const integerAmount = (option, description, minValue = 1) => + option + .setDescription(description) + .setRequired(true) + .setMinValue(minValue) + .setMaxValue(2147483647); + +module.exports = [ + new SlashCommandBuilder() + .setName("addmoney") + .setDescription("Bir kullanıcıya para ekler.") + .addUserOption((option) => option.setName("kullanici").setDescription("Para eklenecek kullanıcı.").setRequired(true)) + .addIntegerOption((option) => integerAmount(option.setName("miktar"), "Eklenecek para miktarı.")), + + new SlashCommandBuilder() + .setName("bal") + .setDescription("Kullanıcının bakiyesini gösterir.") + .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi görüntülenecek kullanıcı.").setRequired(false)), + + new SlashCommandBuilder().setName("beg").setDescription("Dilencilik yaparak para kazanmaya çalışır."), + + new SlashCommandBuilder() + .setName("buy") + .setDescription("Mağazadan bir ürün satın alır.") + .addStringOption((option) => + option + .setName("urun") + .setDescription("Satın almak istediğin ürün.") + .setRequired(true) + .addChoices( + { name: "Laptop", value: "Laptop" }, + { name: "Mobile", value: "Mobile" }, + { name: "PC", value: "PC" } + ) + ), + + new SlashCommandBuilder().setName("daily").setDescription("Günlük para ödülünü alır."), + new SlashCommandBuilder().setName("help").setDescription("Botun komutlarını gösterir."), + new SlashCommandBuilder().setName("inventory").setDescription("Envanterini gösterir."), + new SlashCommandBuilder().setName("lb").setDescription("Ekonomi sıralamasını gösterir."), + new SlashCommandBuilder().setName("ping").setDescription("Botun gecikmesini gösterir."), + + new SlashCommandBuilder() + .setName("prefix") + .setDescription("Sunucunun prefix'ini değiştirir veya sıfırlar.") + .addStringOption((option) => + option.setName("yeni_prefix").setDescription("Yeni prefix. Boş bırakırsan varsayılana döner.").setRequired(false).setMaxLength(10) + ), + + new SlashCommandBuilder() + .setName("rob") + .setDescription("Başka bir kullanıcının parasını çalmayı dener.") + .addUserOption((option) => option.setName("kullanici").setDescription("Hedef kullanıcı.").setRequired(true)), + + new SlashCommandBuilder().setName("search").setDescription("Bir yerde para arar."), + + new SlashCommandBuilder() + .setName("setmoney") + .setDescription("Bir kullanıcının bakiyesini ayarlar.") + .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi ayarlanacak kullanıcı.").setRequired(true)) + .addIntegerOption((option) => integerAmount(option.setName("miktar"), "Yeni bakiye miktarı.", 0)), + + new SlashCommandBuilder().setName("shop").setDescription("Mağazayı gösterir."), + + new SlashCommandBuilder() + .setName("transfer") + .setDescription("Başka bir kullanıcıya para gönderir.") + .addUserOption((option) => option.setName("kullanici").setDescription("Para gönderilecek kullanıcı.").setRequired(true)) + .addIntegerOption((option) => integerAmount(option.setName("miktar"), "Gönderilecek para miktarı.")), + + new SlashCommandBuilder().setName("weekly").setDescription("Haftalık para ödülünü alır."), + new SlashCommandBuilder().setName("work").setDescription("Çalışarak para kazanır.") +]; From 100497aabf274e38eeb170117ce23685b7276dde Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:45:54 +0300 Subject: [PATCH 028/175] feat: handle slash command interactions --- events/interactionCreate.js | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 events/interactionCreate.js diff --git a/events/interactionCreate.js b/events/interactionCreate.js new file mode 100644 index 0000000..e3b8f94 --- /dev/null +++ b/events/interactionCreate.js @@ -0,0 +1,71 @@ +module.exports = async (client, interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; + + const command = client.commands.get(interaction.commandName); + if (!command) return; + + const userOption = interaction.options.getUser("kullanici"); + const memberOption = interaction.options.getMember("kullanici"); + const amount = interaction.options.getInteger("miktar"); + const product = interaction.options.getString("urun"); + const newPrefix = interaction.options.getString("yeni_prefix"); + + const args = []; + if (userOption) args.push(userOption.id); + if (product) args.push(product); + if (newPrefix) args.push(newPrefix); + if (amount !== null) args.push(String(amount)); + + const respond = async (payload) => { + if (!interaction.replied && !interaction.deferred) { + await interaction.reply(payload); + return interaction.fetchReply(); + } + + return interaction.followUp(payload); + }; + + const fakeMessage = { + author: interaction.user, + member: interaction.member, + guild: interaction.guild, + createdTimestamp: interaction.createdTimestamp, + content: `/${interaction.commandName}`, + mentions: { + users: { + first: () => userOption || null + }, + members: { + first: () => memberOption || null + } + }, + channel: { + send: respond + }, + reply: respond + }; + + client.prefix = client.db.fetch(`prefix_${interaction.guild.id}`) || client.config.prefix; + client.ecoAddUser = interaction.user.id; + + try { + await command.execute(client, fakeMessage, args); + + if (!interaction.replied && !interaction.deferred) { + await interaction.reply({ content: "Bu komut herhangi bir yanıt göndermedi.", ephemeral: true }); + } + } catch (error) { + console.error(`/${interaction.commandName} komutunda hata:`, error); + + const payload = { + content: "Komut çalıştırılırken beklenmeyen bir hata oluştu.", + ephemeral: true + }; + + if (!interaction.replied && !interaction.deferred) { + await interaction.reply(payload).catch(() => {}); + } else { + await interaction.followUp(payload).catch(() => {}); + } + } +}; From b2e9f5b0a417c971c082b0e4383a10f38fda4ee1 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:46:00 +0300 Subject: [PATCH 029/175] feat: deploy guild slash commands on startup --- events/ready.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/events/ready.js b/events/ready.js index 6f9e5cd..3208165 100644 --- a/events/ready.js +++ b/events/ready.js @@ -1,4 +1,24 @@ -module.exports = (client) => { +const { REST, Routes } = require("discord.js"); +const slashCommands = require("../slashCommands"); + +module.exports = async (client) => { console.log(`${client.user.tag} çevrimiçi!`); client.user.setActivity("LoiFragola Economy"); + + const serverId = client.config.serverId; + if (!serverId || serverId === "YOUR_SERVER_ID") { + console.warn("Slash komutları deploy edilmedi: botConfig.js içindeki serverId ayarlanmalı."); + return; + } + + try { + const rest = new REST({ version: "10" }).setToken(client.config.token); + await rest.put(Routes.applicationGuildCommands(client.user.id, serverId), { + body: slashCommands.map((command) => command.toJSON()) + }); + + console.log(`${slashCommands.length} slash komutu sunucuya başarıyla deploy edildi.`); + } catch (error) { + console.error("Slash komutları deploy edilirken hata oluştu:", error); + } }; From 36827c1dbf3f1fa12331c60e714c9985eef3f25b Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:46:04 +0300 Subject: [PATCH 030/175] feat: add server id to bot configuration --- botConfig.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/botConfig.js b/botConfig.js index c8289c7..aefffb8 100644 --- a/botConfig.js +++ b/botConfig.js @@ -1,9 +1,10 @@ module.exports = { token: "YOUR_TOKEN", prefix: "PREFIX", + serverId: "YOUR_SERVER_ID", admins: [ "PEOPLE WHO CAN USE ADD MONEY (IDS)" -], + ], debug: true, countChannel: "countChannelID" }; From 77fb3bb4fb5245332eca88d3d3532d3deb7c6ac0 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:46:12 +0300 Subject: [PATCH 031/175] fix: initialize default prefix for all command handlers --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 073e277..fed5e60 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ const client = new Client({ client.eco = new Eco.Manager(); client.db = Eco.db; client.config = require("./botConfig"); +client.prefix = client.config.prefix; client.commands = new Collection(); client.aliases = new Collection(); From 99d6eb4668ea586974690ee5683aca760e729ea7 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:46:28 +0300 Subject: [PATCH 032/175] fix: validate guild id and prevent repeated slash deployments --- events/ready.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/events/ready.js b/events/ready.js index 3208165..22b0422 100644 --- a/events/ready.js +++ b/events/ready.js @@ -5,18 +5,26 @@ module.exports = async (client) => { console.log(`${client.user.tag} çevrimiçi!`); client.user.setActivity("LoiFragola Economy"); + if (client.slashCommandsDeployed) return; + const serverId = client.config.serverId; if (!serverId || serverId === "YOUR_SERVER_ID") { console.warn("Slash komutları deploy edilmedi: botConfig.js içindeki serverId ayarlanmalı."); return; } + if (!/^\d{17,20}$/.test(String(serverId))) { + console.warn("Slash komutları deploy edilmedi: serverId geçerli bir Discord sunucu ID'si değil."); + return; + } + try { const rest = new REST({ version: "10" }).setToken(client.config.token); await rest.put(Routes.applicationGuildCommands(client.user.id, serverId), { body: slashCommands.map((command) => command.toJSON()) }); + client.slashCommandsDeployed = true; console.log(`${slashCommands.length} slash komutu sunucuya başarıyla deploy edildi.`); } catch (error) { console.error("Slash komutları deploy edilirken hata oluştu:", error); From b55b4a9bc67d7668c922ccd6bf9c03d53288772a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:48:16 +0300 Subject: [PATCH 033/175] Update README with new bot command deployment feature Added information about deploying bot commands for slash usage. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c2184b..ded2a3a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # EconomyBot 🇹🇷 Node.js ve Discord.js ile geliştirilmiş, kolay kurulabilen Discord ekonomi botu.
-
LoiFragola tarafından Türkçeleştirilmiş ve Node.js v20 üzerine uygun hale getirilmiştir. +
LoiFragola tarafından Türkçeleştirilmiş ve Node.js v20 üzerine uygun hale getirilmiştir.

+Ayrıca bu sürümde bot komutları sunucuya deploy ediyor. Bu sayede komutları slash "/" ile kullanabilirsiniz. ## Özellikler From 4aea5a072568abd22d2ed6ef3211e9659d4c761f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 21:57:18 +0300 Subject: [PATCH 034/175] Rename command and update usage instructions --- commands/addmoney.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/addmoney.js b/commands/addmoney.js index 72dcad2..c916f40 100644 --- a/commands/addmoney.js +++ b/commands/addmoney.js @@ -25,7 +25,7 @@ exports.execute = async (client, message, args) => { }; exports.help = { - name: "addmoney", - aliases: ["addbal"], - usage: "addmoney @kullanıcı " + name: "Para Ekle", + aliases: ["addbal","paraekle","para-ekle"], + usage: `addmoney @user ` }; From e91313d77bcf25b4a527b2f6169b27dfa3793db8 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:00:21 +0300 Subject: [PATCH 035/175] fix: restore addmoney command key and add Turkish aliases --- commands/addmoney.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/addmoney.js b/commands/addmoney.js index c916f40..543894c 100644 --- a/commands/addmoney.js +++ b/commands/addmoney.js @@ -25,7 +25,7 @@ exports.execute = async (client, message, args) => { }; exports.help = { - name: "Para Ekle", - aliases: ["addbal","paraekle","para-ekle"], - usage: `addmoney @user ` + name: "addmoney", + aliases: ["addbal", "paraekle", "para-ekle"], + usage: "addmoney @kullanıcı " }; From 70d50aad5b11b3ed4677fb6b3ca04e670cbba608 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:00:25 +0300 Subject: [PATCH 036/175] feat: add Turkish balance aliases --- commands/bal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/bal.js b/commands/bal.js index 1dd395b..04e8806 100644 --- a/commands/bal.js +++ b/commands/bal.js @@ -20,6 +20,6 @@ exports.execute = async (client, message) => { exports.help = { name: "bal", - aliases: ["money", "credits", "balance"], + aliases: ["money", "credits", "balance", "bakiye", "para"], usage: "bal [@kullanıcı]" }; From 8ed035834d320ffac8dc0ec16fc4e944e0f9646d Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:00:31 +0300 Subject: [PATCH 037/175] feat: add Turkish beg aliases --- commands/beg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/beg.js b/commands/beg.js index 3b7a99d..a3e8013 100644 --- a/commands/beg.js +++ b/commands/beg.js @@ -13,6 +13,6 @@ exports.execute = async (client, message) => { exports.help = { name: "beg", - aliases: [], + aliases: ["dilen", "dilenme", "dilencilik"], usage: "beg" }; From b77a7da1d65aa8a13f67791d35e9028042557c84 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:00:35 +0300 Subject: [PATCH 038/175] feat: add Turkish buy aliases --- commands/buy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/buy.js b/commands/buy.js index 2788e88..d9e3514 100644 --- a/commands/buy.js +++ b/commands/buy.js @@ -26,6 +26,6 @@ exports.execute = async (client, message, args) => { exports.help = { name: "buy", - aliases: [], + aliases: ["satınal", "satinal", "al"], usage: "buy <ürün>" }; From f403c375aa20d3309c51a24a3d43f3746d75ad4f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:00:39 +0300 Subject: [PATCH 039/175] feat: add Turkish daily aliases --- commands/daily.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/daily.js b/commands/daily.js index 0425568..24ed74e 100644 --- a/commands/daily.js +++ b/commands/daily.js @@ -11,6 +11,6 @@ module.exports.execute = async (client, message) => { module.exports.help = { name: "daily", - aliases: [], + aliases: ["günlük", "gunluk"], usage: "daily" }; From 2f852cd38ee3aced3546426838552717b4387db0 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:00:43 +0300 Subject: [PATCH 040/175] feat: add Turkish help aliases --- commands/help.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/help.js b/commands/help.js index 78aea21..e3fe21a 100644 --- a/commands/help.js +++ b/commands/help.js @@ -24,6 +24,6 @@ exports.execute = async (client, message) => { exports.help = { name: "help", - aliases: ["h"], + aliases: ["h", "yardım", "yardim", "komutlar"], usage: "help" }; From a3143914443c7c59f11e4102b6a89f9129335aa2 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:00:48 +0300 Subject: [PATCH 041/175] feat: add Turkish inventory aliases --- commands/inventory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/inventory.js b/commands/inventory.js index 0f9fbf0..d6434d0 100644 --- a/commands/inventory.js +++ b/commands/inventory.js @@ -27,6 +27,6 @@ exports.execute = async (client, message) => { exports.help = { name: "inventory", - aliases: ["inv"], + aliases: ["inv", "envanter", "eşyalar", "esya"], usage: "inventory" }; From c1b021552e3f6837837207c1c85f48e5db65df4e Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:00:54 +0300 Subject: [PATCH 042/175] feat: add Turkish leaderboard aliases --- commands/leaderboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/leaderboard.js b/commands/leaderboard.js index 1fb0a69..a790dfd 100644 --- a/commands/leaderboard.js +++ b/commands/leaderboard.js @@ -25,6 +25,6 @@ exports.execute = async (client, message) => { exports.help = { name: "lb", - aliases: ["leaderboard"], + aliases: ["leaderboard", "sıralama", "siralama", "liderlik"], usage: "lb" }; From bc5c7bbee85fdbbc73fb12e3d71b7b452943d151 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:00 +0300 Subject: [PATCH 043/175] feat: add Turkish ping aliases --- commands/ping.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/ping.js b/commands/ping.js index 0af0942..3604536 100644 --- a/commands/ping.js +++ b/commands/ping.js @@ -19,6 +19,6 @@ exports.execute = async (client, message) => { exports.help = { name: "ping", - aliases: ["pong", "latency"], + aliases: ["pong", "latency", "gecikme"], usage: "ping" }; From 0f30061514ece2fc3d434f3a99652502f3194121 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:04 +0300 Subject: [PATCH 044/175] feat: add Turkish prefix aliases --- commands/prefix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/prefix.js b/commands/prefix.js index b025360..da9787e 100644 --- a/commands/prefix.js +++ b/commands/prefix.js @@ -15,6 +15,6 @@ exports.execute = (client, message, args) => { exports.help = { name: "prefix", - aliases: ["setprefix"], + aliases: ["setprefix", "önek", "onek"], usage: "prefix [yeni-prefix]" }; From c43b0cc1a2b65075557323d9a611091fe9eb39f1 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:13 +0300 Subject: [PATCH 045/175] feat: add Turkish rob aliases --- commands/rob.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/rob.js b/commands/rob.js index 2e20477..2b149a1 100644 --- a/commands/rob.js +++ b/commands/rob.js @@ -25,6 +25,6 @@ exports.execute = async (client, message, args) => { exports.help = { name: "rob", - aliases: [], + aliases: ["soy", "soygun"], usage: "rob " }; From 18db8c4a7f6213d48307b1b4060fb06de0a91a31 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:18 +0300 Subject: [PATCH 046/175] feat: add Turkish search aliases --- commands/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/search.js b/commands/search.js index 811aebb..6b0408f 100644 --- a/commands/search.js +++ b/commands/search.js @@ -23,6 +23,6 @@ exports.execute = async (client, message) => { exports.help = { name: "search", - aliases: [], + aliases: ["ara", "arama"], usage: "search" }; From 007ce824d20d635750b5a85c988644eb24d5a769 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:22 +0300 Subject: [PATCH 047/175] feat: add Turkish setmoney aliases --- commands/setmoney.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/setmoney.js b/commands/setmoney.js index 3389ad4..02e77b2 100644 --- a/commands/setmoney.js +++ b/commands/setmoney.js @@ -25,6 +25,6 @@ exports.execute = async (client, message, args) => { exports.help = { name: "setmoney", - aliases: ["setbal"], + aliases: ["setbal", "parayarla", "bakiyeayarla"], usage: "setmoney @kullanıcı " }; From 37cb02313e90516c1919e6119ab2ecc808d855fa Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:27 +0300 Subject: [PATCH 048/175] feat: add Turkish shop aliases --- commands/shop.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/shop.js b/commands/shop.js index ea80f1d..c0cc413 100644 --- a/commands/shop.js +++ b/commands/shop.js @@ -15,6 +15,6 @@ exports.execute = async (client, message) => { exports.help = { name: "shop", - aliases: [], + aliases: ["mağaza", "magaza", "market"], usage: "shop" }; From 40aad857b88aa5508ed17178d60b9b854b3d0f00 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:32 +0300 Subject: [PATCH 049/175] feat: add Turkish transfer aliases --- commands/transfer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/transfer.js b/commands/transfer.js index 44a4530..81bd670 100644 --- a/commands/transfer.js +++ b/commands/transfer.js @@ -14,6 +14,6 @@ exports.execute = async (client, message, args) => { exports.help = { name: "transfer", - aliases: ["give", "share"], + aliases: ["give", "share", "aktar", "paraaktar"], usage: "transfer " }; From 86a8137fc7e67c91aeabe6b31f2e9c6223b91967 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:35 +0300 Subject: [PATCH 050/175] feat: add Turkish weekly aliases --- commands/weekly.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/weekly.js b/commands/weekly.js index 56745fe..3459e91 100644 --- a/commands/weekly.js +++ b/commands/weekly.js @@ -11,6 +11,6 @@ exports.execute = async (client, message) => { exports.help = { name: "weekly", - aliases: [], + aliases: ["haftalık", "haftalik"], usage: "weekly" }; From 4a782258f619c65558028e8640c099adf17e00b4 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:39 +0300 Subject: [PATCH 051/175] feat: add Turkish work aliases --- commands/work.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/work.js b/commands/work.js index ca9a7cd..9c1969e 100644 --- a/commands/work.js +++ b/commands/work.js @@ -11,6 +11,6 @@ module.exports.execute = async (client, message) => { module.exports.help = { name: "work", - aliases: [], + aliases: ["çalış", "calis", "çalıştır", "calistir"], usage: "work" }; From 964e5ac72f08b9bf46a729d0849648dbcdaa185a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:01:48 +0300 Subject: [PATCH 052/175] chore: add gitignore for dependencies secrets databases and logs --- .gitignore | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a0220e --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules/ + +# Local configuration / secrets +.env +.env.* +!.env.example +botConfig.local.js + +# Database / local data +*.db +*.sqlite +*.sqlite3 +*.db-journal + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS / editor +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# Temporary files +*.tmp +*.temp From ba0614d74b9a3a1110440aed93c875e160755c0a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:02:05 +0300 Subject: [PATCH 053/175] docs: redesign README with professional hero GIF area and command guide --- README.md | 242 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 194 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index ded2a3a..a3b1b2b 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,216 @@ +
+ # EconomyBot 🇹🇷 -Node.js ve Discord.js ile geliştirilmiş, kolay kurulabilen Discord ekonomi botu.
-
LoiFragola tarafından Türkçeleştirilmiş ve Node.js v20 üzerine uygun hale getirilmiştir.

-Ayrıca bu sürümde bot komutları sunucuya deploy ediyor. Bu sayede komutları slash "/" ile kullanabilirsiniz. +### Modern • Türkçe • Node.js 20+ • Discord.js v14 + +**Kolay kurulabilen, klasik prefix komutlarını ve modern `/` slash komutlarını birlikte sunan Discord ekonomi botu.** + + +EconomyBot GIF Alanı + +
+ +[![Node.js](https://img.shields.io/badge/Node.js-20%2B-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/) +[![Discord.js](https://img.shields.io/badge/discord.js-v14-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.js.org/) +[![License](https://img.shields.io/badge/License-Nginx-8A2BE2?style=for-the-badge)](LICENSE) + +
+ +--- + +## ✦ Hakkında -## Özellikler +**EconomyBot**, Discord sunucuları için hazırlanmış klasik bir ekonomi botudur. Bu sürüm **LoiFragola** tarafından Türkçeleştirilmiş, **Node.js 20+** ve **discord.js v14** ile çalışacak şekilde güncellenmiştir. -- Mağaza ve envanter -- Günlük, haftalık, çalışma, arama ve dilenme komutları -- Bakiye sistemi -- Para transferi -- Sıralama tablosu -- Sunucuya özel prefix -- Sayaç kanalı +Bot başlatıldığında `botConfig.js` içerisindeki `serverId` değerini kullanarak tüm slash komutlarını doğrudan hedef sunucuya deploy eder. Böylece komutları hem klasik prefix sistemiyle hem de `/` ile kullanabilirsiniz. -## Gereksinimler +> **Not:** Prefix komutlarının çalışması için Discord Developer Portal'da **Message Content Intent** etkinleştirilmelidir. -- **Node.js 20.0.0 veya üzeri** -- **Discord.js v14** -- npm +--- -> Node.js 20, 22 ve 24 sürümleri hedeflenmiştir. +## ✦ Özellikler -## Kurulum +- 💰 Kullanıcı bakiye ve ekonomi sistemi +- 🎁 Günlük ve haftalık ödüller +- 💼 Çalışma ve dilenme sistemi +- 🔎 Arama sistemi +- 🏪 Mağaza ve ürün satın alma +- 🎒 Envanter sistemi +- 💸 Kullanıcılar arası para transferi +- 🏆 Ekonomi sıralaması +- 🥷 Sanal soygun sistemi +- ⚙️ Sunucuya özel prefix +- 📊 Sayaç kanalı desteği +- `/` **Slash Command** desteği +- 🇹🇷 Türkçe komut mesajları ve Türkçe prefix alias'ları -1. Depoyu indirin. -2. Bağımlılıkları yükleyin: +--- + +## ✦ Gereksinimler + +| Gereksinim | Sürüm | +|---|---| +| **Node.js** | `20+` | +| **discord.js** | `14.x` | +| **npm** | Node.js ile birlikte gelir | + +> Node.js 22 veya 24 kullanmanız önerilir. + +--- + +## ✦ Kurulum + +### 1. Projeyi indirin + +```bash +git clone https://github.com/LoiFragola/EconomyBot.git +cd EconomyBot +``` + +### 2. Bağımlılıkları yükleyin ```bash npm install ``` -3. `botConfig.js` dosyasındaki değerleri doldurun. -4. Discord Developer Portal üzerinden bot için **Message Content Intent** özelliğini etkinleştirin. Bot mesaj içeriklerini prefix komutları için kullandığından bu izin gereklidir. -5. Botu başlatın: +### 3. Bot ayarlarını yapın + +`botConfig.js` dosyasını açıp aşağıdaki alanları doldurun: + +```js +module.exports = { + token: "BOT_TOKEN", + prefix: "!", + serverId: "SUNUCU_ID", + admins: [ + "YETKİLİ_KULLANICI_ID" + ], + debug: true, + countChannel: "SAYAÇ_KANAL_ID" +}; +``` + +**`serverId`**, slash komutlarının deploy edileceği Discord sunucusunun ID'sidir. + +### 4. Discord izinlerini kontrol edin + +Prefix komutları için Developer Portal'dan **Message Content Intent** özelliğini açın. + +Botun hedef sunucuya uygulama komutlarını deploy edebilmesi için bot davetinde gerekli `applications.commands` kapsamının bulunduğundan emin olun. + +### 5. Botu başlatın ```bash npm start ``` -## Yapılandırma +Başarılı başlatmada konsolda buna benzer bir çıktı görürsünüz: + +```text +EconomyBot çevrimiçi! +17 slash komutu sunucuya başarıyla deploy edildi. +``` -`botConfig.js` içerisindeki `token`, `prefix`, `admins`, `debug` ve `countChannel` değerlerini sunucunuza göre ayarlayın. +--- -## Komutlar +## ✦ Komutlar -| Komut | Kullanım | -|---|---| -| `help` | Komut listesini gösterir | -| `bal` | Bakiyeyi gösterir | -| `daily` | Günlük ödülü alır | -| `weekly` | Haftalık ödülü alır | -| `work` | Çalışarak para kazanır | -| `beg` | Para dilenir | -| `search` | Para arar | -| `rob` | Başka bir kullanıcıdan para çalmayı dener | -| `transfer` | Başka bir kullanıcıya para gönderir | -| `shop` | Mağazayı gösterir | -| `buy` | Mağazadan ürün satın alır | -| `inventory` | Envanteri gösterir | -| `lb` | Ekonomi sıralamasını gösterir | -| `ping` | Bot gecikmesini gösterir | -| `prefix` | Sunucu prefix'ini görüntüler veya değiştirir | -| `addmoney` | Yetkili kullanıcıya para ekler | -| `setmoney` | Yetkili kullanıcı için bakiyeyi ayarlar | - -## Lisans - -Bu proje, orijinal EconomyBot projesinin lisans koşullarına tabidir. Orijinal geliştiriciye uygun şekilde kredi verilmelidir. +### Ekonomi + +| Komut | Açıklama | Örnek | +|---|---|---| +| `/bal` | Bakiyeyi gösterir | `/bal` | +| `/daily` | Günlük ödülü verir | `/daily` | +| `/weekly` | Haftalık ödülü verir | `/weekly` | +| `/work` | Çalışarak para kazandırır | `/work` | +| `/beg` | Para dilenmeyi dener | `/beg` | +| `/search` | Para arar | `/search` | +| `/rob` | Başka bir kullanıcıdan para çalmayı dener | `/rob kullanıcı` | +| `/transfer` | Para gönderir | `/transfer kullanıcı 500` | + +### Mağaza & Envanter + +| Komut | Açıklama | Örnek | +|---|---|---| +| `/shop` | Mağazayı gösterir | `/shop` | +| `/buy` | Ürün satın alır | `/buy Laptop` | +| `/inventory` | Envanteri gösterir | `/inventory` | + +### Yönetim & Araçlar + +| Komut | Açıklama | Örnek | +|---|---|---| +| `/lb` | Ekonomi sıralamasını gösterir | `/lb` | +| `/ping` | Bot gecikmesini gösterir | `/ping` | +| `/help` | Komut listesini gösterir | `/help` | +| `/prefix` | Sunucu prefix'ini ayarlar | `/prefix !` | +| `/addmoney` | Yetkili kullanıcı para ekler | `/addmoney kullanıcı 500` | +| `/setmoney` | Yetkili kullanıcı bakiyeyi ayarlar | `/setmoney kullanıcı 5000` | + +> Prefix kullanıyorsanız aynı komutları `!bal`, `!daily`, `!shop` gibi kullanabilirsiniz. Türkçe alias'lar da desteklenir: `!bakiye`, `!günlük`, `!mağaza`, `!envanter`, `!aktar`, `!çalış` vb. + +--- + +## ✦ Slash Command Sistemi + +Bot her başlatıldığında `serverId` için tanımlanan sunucuya komut listesini gönderir. + +Bu yöntem **guild command** kullandığı için geliştirme ve tek sunucu kullanımlarında komutların hızlı güncellenmesini sağlar. + +Komut listesinde bir değişiklik yaptıktan sonra: + +```bash +npm start +``` + +komutunu tekrar çalıştırmanız yeterlidir. + +--- + +## ✦ Yapı + +```text +EconomyBot/ +├── commands/ # Prefix komutları +├── events/ # Discord eventleri +├── counter.js # Sayaç sistemi +├── slashCommands.js # Slash komut tanımları +├── botConfig.js # Yerel bot ayarları +├── index.js # Bot başlangıç dosyası +├── package.json +├── .gitignore +└── README.md +``` + +--- + +## ✦ Güvenlik + +**Bot token'ınızı veya kişisel bilgilerinizi GitHub'a yüklemeyin.** + +`botConfig.js` içerisinde gerçek token kullanıyorsanız bu dosyanın Git tarafından takip edilmediğinden emin olun veya token'ı ortam değişkenleri üzerinden yönetin. + +`.gitignore` dosyası bağımlılıkları, logları, yerel veritabanı dosyalarını ve bazı yerel yapılandırmaları Git'ten uzak tutar. + +--- + +## ✦ Lisans & Kaynak + +Bu proje, orijinal **ZeroDiscord/EconomyBot** projesinden geliştirilmiştir. + +Orijinal geliştiriciye uygun şekilde kredi verilmelidir. Lisans koşulları için repository içerisindeki lisans dosyasını inceleyin. + +--- + +
+ +### Made with Node.js & discord.js + +**LoiFragola** + +
From 75d545af75b24daf9d109c505a11849d4c39ff54 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:02:15 +0300 Subject: [PATCH 054/175] fix: use clientReady event for discord.js v14 --- events/clientReady.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 events/clientReady.js diff --git a/events/clientReady.js b/events/clientReady.js new file mode 100644 index 0000000..d1f0e72 --- /dev/null +++ b/events/clientReady.js @@ -0,0 +1,29 @@ +const { REST, Routes } = require("discord.js"); +const slashCommands = require("../slashCommands"); + +module.exports = async (client) => { + console.log(`${client.user.tag} çevrimiçi!`); + client.user.setActivity("LoiFragola Economy"); + + const serverId = client.config.serverId; + if (!serverId || serverId === "YOUR_SERVER_ID") { + console.warn("Slash komutları deploy edilmedi: botConfig.js içindeki serverId ayarlanmalı."); + return; + } + + if (!/^\d{17,20}$/.test(String(serverId))) { + console.warn("Slash komutları deploy edilmedi: serverId geçerli bir Discord sunucu ID'si değil."); + return; + } + + try { + const rest = new REST({ version: "10" }).setToken(client.config.token); + await rest.put(Routes.applicationGuildCommands(client.user.id, serverId), { + body: slashCommands.map((command) => command.toJSON()) + }); + + console.log(`${slashCommands.length} slash komutu sunucuya başarıyla deploy edildi.`); + } catch (error) { + console.error("Slash komutları deploy edilirken hata oluştu:", error); + } +}; From da4b2733bd4f5978e18175830880ae6e16261fac Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:02:20 +0300 Subject: [PATCH 055/175] fix: replace deprecated ready event with clientReady --- events/ready.js | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 events/ready.js diff --git a/events/ready.js b/events/ready.js deleted file mode 100644 index 22b0422..0000000 --- a/events/ready.js +++ /dev/null @@ -1,32 +0,0 @@ -const { REST, Routes } = require("discord.js"); -const slashCommands = require("../slashCommands"); - -module.exports = async (client) => { - console.log(`${client.user.tag} çevrimiçi!`); - client.user.setActivity("LoiFragola Economy"); - - if (client.slashCommandsDeployed) return; - - const serverId = client.config.serverId; - if (!serverId || serverId === "YOUR_SERVER_ID") { - console.warn("Slash komutları deploy edilmedi: botConfig.js içindeki serverId ayarlanmalı."); - return; - } - - if (!/^\d{17,20}$/.test(String(serverId))) { - console.warn("Slash komutları deploy edilmedi: serverId geçerli bir Discord sunucu ID'si değil."); - return; - } - - try { - const rest = new REST({ version: "10" }).setToken(client.config.token); - await rest.put(Routes.applicationGuildCommands(client.user.id, serverId), { - body: slashCommands.map((command) => command.toJSON()) - }); - - client.slashCommandsDeployed = true; - console.log(`${slashCommands.length} slash komutu sunucuya başarıyla deploy edildi.`); - } catch (error) { - console.error("Slash komutları deploy edilirken hata oluştu:", error); - } -}; From 9da64070cd9cc93340d4b918bbd269064f95c440 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:02:26 +0300 Subject: [PATCH 056/175] fix: defer slash interactions and safely adapt legacy command handlers --- events/interactionCreate.js | 93 ++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index e3b8f94..ef8263f 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -1,58 +1,65 @@ module.exports = async (client, interaction) => { if (!interaction.isChatInputCommand() || !interaction.guild) return; - const command = client.commands.get(interaction.commandName); - if (!command) return; + try { + const command = client.commands.get(interaction.commandName); - const userOption = interaction.options.getUser("kullanici"); - const memberOption = interaction.options.getMember("kullanici"); - const amount = interaction.options.getInteger("miktar"); - const product = interaction.options.getString("urun"); - const newPrefix = interaction.options.getString("yeni_prefix"); + if (!command) { + return interaction.reply({ + content: "Bu komut artık mevcut değil. Botu yeniden başlatıp slash komutlarını güncelleyin.", + ephemeral: true + }); + } - const args = []; - if (userOption) args.push(userOption.id); - if (product) args.push(product); - if (newPrefix) args.push(newPrefix); - if (amount !== null) args.push(String(amount)); + const userOption = interaction.options.getUser("kullanici"); + const memberOption = interaction.options.getMember("kullanici"); + const amount = interaction.options.getInteger("miktar"); + const product = interaction.options.getString("urun"); + const newPrefix = interaction.options.getString("yeni_prefix"); - const respond = async (payload) => { - if (!interaction.replied && !interaction.deferred) { - await interaction.reply(payload); - return interaction.fetchReply(); - } + const args = []; + if (userOption) args.push(userOption.id); + if (product) args.push(product); + if (newPrefix) args.push(newPrefix); + if (amount !== null) args.push(String(amount)); + + await interaction.deferReply(); - return interaction.followUp(payload); - }; + const respond = async (payload) => { + if (payload === undefined || payload === null) payload = { content: "Komut tamamlandı." }; + return interaction.followUp(payload); + }; - const fakeMessage = { - author: interaction.user, - member: interaction.member, - guild: interaction.guild, - createdTimestamp: interaction.createdTimestamp, - content: `/${interaction.commandName}`, - mentions: { - users: { - first: () => userOption || null + const fakeMessage = { + author: interaction.user, + member: interaction.member, + guild: interaction.guild, + createdTimestamp: interaction.createdTimestamp, + content: `/${interaction.commandName}`, + mentions: { + users: { + first: () => userOption || null + }, + members: { + first: () => memberOption || null + } + }, + channel: { + id: interaction.channelId, + send: respond }, - members: { - first: () => memberOption || null - } - }, - channel: { - send: respond - }, - reply: respond - }; + reply: respond + }; - client.prefix = client.db.fetch(`prefix_${interaction.guild.id}`) || client.config.prefix; - client.ecoAddUser = interaction.user.id; + client.prefix = client.db.fetch(`prefix_${interaction.guild.id}`) || client.config.prefix; + client.ecoAddUser = interaction.user.id; - try { await command.execute(client, fakeMessage, args); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ content: "Bu komut herhangi bir yanıt göndermedi.", ephemeral: true }); + if (!interaction.replied) { + await interaction.editReply({ + content: "Bu komut için işlem yapma yetkin bulunmuyor veya komut herhangi bir yanıt göndermedi." + }); } } catch (error) { console.error(`/${interaction.commandName} komutunda hata:`, error); @@ -65,7 +72,7 @@ module.exports = async (client, interaction) => { if (!interaction.replied && !interaction.deferred) { await interaction.reply(payload).catch(() => {}); } else { - await interaction.followUp(payload).catch(() => {}); + await interaction.editReply(payload).catch(() => {}); } } }; From d9064dc7e9a20990decf9d73db326a139102e7a2 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:02:41 +0300 Subject: [PATCH 057/175] fix: track slash command responses after deferred replies --- events/interactionCreate.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index ef8263f..490cd0e 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -25,7 +25,9 @@ module.exports = async (client, interaction) => { await interaction.deferReply(); + let responded = false; const respond = async (payload) => { + responded = true; if (payload === undefined || payload === null) payload = { content: "Komut tamamlandı." }; return interaction.followUp(payload); }; @@ -56,7 +58,7 @@ module.exports = async (client, interaction) => { await command.execute(client, fakeMessage, args); - if (!interaction.replied) { + if (!responded) { await interaction.editReply({ content: "Bu komut için işlem yapma yetkin bulunmuyor veya komut herhangi bir yanıt göndermedi." }); From 267aef1d8e04cedab6adf7f070ea8cedd5724974 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:02:49 +0300 Subject: [PATCH 058/175] fix: edit deferred reply for first slash command response --- events/interactionCreate.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index 490cd0e..70fa6bb 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -29,6 +29,11 @@ module.exports = async (client, interaction) => { const respond = async (payload) => { responded = true; if (payload === undefined || payload === null) payload = { content: "Komut tamamlandı." }; + + if (interaction.deferred && !interaction.replied) { + return interaction.editReply(payload); + } + return interaction.followUp(payload); }; From 460440a2ff60628b16488d38e7fcdbbb1237de92 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:03:05 +0300 Subject: [PATCH 059/175] ci: add Node 20 22 and 24 compatibility checks --- .github/workflows/node-compatibility.yml | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/node-compatibility.yml diff --git a/.github/workflows/node-compatibility.yml b/.github/workflows/node-compatibility.yml new file mode 100644 index 0000000..347162a --- /dev/null +++ b/.github/workflows/node-compatibility.yml @@ -0,0 +1,36 @@ +name: Node.js Compatibility + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22, 24] + + steps: + - name: Repository'i al + uses: actions/checkout@v4 + + - name: Node.js ${{ matrix.node-version }} kur + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Bağımlılıkları yükle + run: npm install --no-audit --no-fund + + - name: JavaScript sözdizimini kontrol et + shell: bash + run: | + set -e + for file in index.js counter.js commands/*.js events/*.js slashCommands.js; do + node --check "$file" + done From a68aef2b787be0213814997fd320c5688be219ea Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:10:54 +0300 Subject: [PATCH 060/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3b1b2b..eb2b865 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GIF'inizi aşağıdaki src alanına ekleyin: EconomyBot Preview --> -EconomyBot GIF Alanı +EconomyBot GIF Alanı
From cdac7d1377b30699146567880f472021eecb2765 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:11:07 +0300 Subject: [PATCH 061/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb2b865..e35e87c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GIF'inizi aşağıdaki src alanına ekleyin: EconomyBot Preview --> -EconomyBot GIF Alanı +EconomyBot GIF Alanı
From e02a64fb82f7de78772699d4325e0ca467955caf Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:11:38 +0300 Subject: [PATCH 062/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e35e87c..6423267 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GIF'inizi aşağıdaki src alanına ekleyin: EconomyBot Preview --> -EconomyBot GIF Alanı +EconomyBot GIF Alanı
From 242c9d9c4af11a24a90cf5d523888916db138eea Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:12:10 +0300 Subject: [PATCH 063/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6423267..3ab1538 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GIF'inizi aşağıdaki src alanına ekleyin: EconomyBot Preview --> -EconomyBot GIF Alanı +EconomyBot GIF Alanı
From aa380fc253ca350e98d5e9ccf042cdcb314f14e4 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:13:03 +0300 Subject: [PATCH 064/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ab1538..7384dfa 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GIF'inizi aşağıdaki src alanına ekleyin: EconomyBot Preview --> -EconomyBot GIF Alanı +EconomyBot GIF Alanı
From a71b35a760c43a90e0b63782e213f9ca2f321f8f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:13:49 +0300 Subject: [PATCH 065/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7384dfa..df2f22e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GIF'inizi aşağıdaki src alanına ekleyin: EconomyBot Preview --> -EconomyBot GIF Alanı +EconomyBot GIF Alanı
From 8dbba0479e6c4f11f0f8c894482c9e0b151649bf Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:15:02 +0300 Subject: [PATCH 066/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df2f22e..6856c2a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GIF'inizi aşağıdaki src alanına ekleyin: EconomyBot Preview --> -EconomyBot GIF Alanı +EconomyBot GIF Alanı
From b4bede13b48618633796735493c2e3867cad9aaf Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:16:00 +0300 Subject: [PATCH 067/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6856c2a..ac671a7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GIF'inizi aşağıdaki src alanına ekleyin: EconomyBot Preview --> -EconomyBot GIF Alanı +EconomyBot GIF Alanı
From ce0c7ff30bb872982974909607e8169f566f3d9d Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:17:04 +0300 Subject: [PATCH 068/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac671a7..2bd2992 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ ## ✦ Hakkında -**EconomyBot**, Discord sunucuları için hazırlanmış klasik bir ekonomi botudur. Bu sürüm **LoiFragola** tarafından Türkçeleştirilmiş, **Node.js 20+** ve **discord.js v14** ile çalışacak şekilde güncellenmiştir. +**EconomyBot**, Discord sunucuları için hazırlanmış klasik bir ekonomi botudur. Bu sürüm **LoiFragola** tarafından Türkçeleştirilmiş, komutlar Discord'a deploy edilecek hale getirilmiş, **Node.js 20+** ve **discord.js v14** ile çalışacak şekilde güncellenmiştir. Bot başlatıldığında `botConfig.js` içerisindeki `serverId` değerini kullanarak tüm slash komutlarını doğrudan hedef sunucuya deploy eder. Böylece komutları hem klasik prefix sistemiyle hem de `/` ile kullanabilirsiniz. From 35dbe00693171560a62f352620a120ee948357ad Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:21:16 +0300 Subject: [PATCH 069/175] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 2bd2992..d3055ea 100644 --- a/README.md +++ b/README.md @@ -201,9 +201,7 @@ EconomyBot/ ## ✦ Lisans & Kaynak -Bu proje, orijinal **ZeroDiscord/EconomyBot** projesinden geliştirilmiştir. - -Orijinal geliştiriciye uygun şekilde kredi verilmelidir. Lisans koşulları için repository içerisindeki lisans dosyasını inceleyin. +Bu proje, **ZeroDiscord/EconomyBot** projesinden geliştirilmiştir. --- From a005fd6255a3aa19aa07b790b74b95ec8c262317 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:22:00 +0300 Subject: [PATCH 070/175] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index d3055ea..8315151 100644 --- a/README.md +++ b/README.md @@ -207,8 +207,7 @@ Bu proje, **ZeroDiscord/EconomyBot** projesinden geliştirilmiştir.
-### Made with Node.js & discord.js +### INS Development -**LoiFragola**
From da40d5b8cec5f979a59429c8eef442a6fcee6c6a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:27:59 +0300 Subject: [PATCH 071/175] Update help.js --- commands/help.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/help.js b/commands/help.js index e3fe21a..4dbadef 100644 --- a/commands/help.js +++ b/commands/help.js @@ -3,8 +3,8 @@ const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message) => { const embed = new EmbedBuilder() .setAuthor({ name: "Komutlar" }) - .setTitle("Daha fazla bot için kanalımıza göz atmayı unutma!") - .setURL("https://www.youtube.com/channel/UCF9E-xef9jL9QgziZRDHKKQ") + .setTitle("INS Development Economy Bot!") + .setURL("") .setDescription(`Toplam Komut: ${client.commands.size}`) .setColor("Blurple") .setTimestamp() From 6f1f6efcb127430a4031700f94e6e2e92210e014 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:29:02 +0300 Subject: [PATCH 072/175] Fix help command invalid empty URL --- commands/help.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/help.js b/commands/help.js index 4dbadef..be808b0 100644 --- a/commands/help.js +++ b/commands/help.js @@ -4,7 +4,7 @@ exports.execute = async (client, message) => { const embed = new EmbedBuilder() .setAuthor({ name: "Komutlar" }) .setTitle("INS Development Economy Bot!") - .setURL("") + .setURL("https://github.com/LoiFragola/EconomyBot") .setDescription(`Toplam Komut: ${client.commands.size}`) .setColor("Blurple") .setTimestamp() From 666a71e85d48b5e991649dba71087b395d41e45e Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:29:10 +0300 Subject: [PATCH 073/175] Add command and slash command smoke tests --- tests/smoke.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/smoke.js diff --git a/tests/smoke.js b/tests/smoke.js new file mode 100644 index 0000000..322d515 --- /dev/null +++ b/tests/smoke.js @@ -0,0 +1,64 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const { Collection } = require("discord.js"); +const slashCommands = require("../slashCommands"); + +const commandsDir = path.join(__dirname, "..", "commands"); +const commandFiles = fs.readdirSync(commandsDir).filter((file) => file.endsWith(".js")); +const commands = commandFiles.map((file) => ({ file, command: require(path.join(commandsDir, file)) })); + +assert.equal(commands.length, 17, "17 komut dosyası bulunmalı."); +assert.equal(slashCommands.length, 17, "17 slash komutu tanımlı olmalı."); + +const names = new Set(); +for (const { file, command } of commands) { + assert.ok(command && typeof command.execute === "function", `${file}: execute fonksiyonu eksik.`); + assert.ok(command.help && typeof command.help.name === "string", `${file}: help.name eksik.`); + assert.ok(Array.isArray(command.help.aliases), `${file}: help.aliases dizi olmalı.`); + assert.ok(typeof command.help.usage === "string", `${file}: help.usage eksik.`); + assert.ok(!names.has(command.help.name), `${file}: aynı komut adı tekrar kullanılmış.`); + names.add(command.help.name); +} + +const slashData = slashCommands.map((command) => command.toJSON()); +const slashNames = slashData.map((command) => command.name); +assert.equal(new Set(slashNames).size, slashNames.length, "Slash komut isimleri benzersiz olmalı."); + +for (const name of slashNames) { + assert.ok(names.has(name), `/${name} için karşılık gelen legacy komut bulunamadı.`); +} + +(async () => { + const help = require("../commands/help"); + const sent = []; + const client = { + prefix: "!", + commands: new Collection(), + user: { + displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" + } + }; + + for (const { command } of commands) client.commands.set(command.help.name, command); + + const message = { + author: { + tag: "SmokeTest#0000", + displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" + }, + channel: { + send: async (payload) => { + sent.push(payload); + return payload; + } + } + }; + + await help.execute(client, message); + assert.equal(sent.length, 1, "help komutu tam olarak bir yanıt göndermeli."); + assert.equal(sent[0].embeds.length, 1, "help komutu bir embed göndermeli."); + assert.equal(sent[0].embeds[0].data.url, "https://github.com/LoiFragola/EconomyBot"); + + console.log(`Smoke test başarılı: ${commands.length} komut ve ${slashCommands.length} slash komutu doğrulandı.`); +})(); From 6c75078de461075574d68b1f057769d3d9d2f1e1 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:29:17 +0300 Subject: [PATCH 074/175] Add automated smoke test script --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 815a8ab..76019e7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Discord ekonomi botu - Node.js 20+ ve discord.js v14 uyumlu", "main": "index.js", "scripts": { - "start": "node index.js" + "start": "node index.js", + "test": "node tests/smoke.js" }, "author": "Zero / LoiFragola", "license": "Nginx", @@ -12,8 +13,8 @@ "discord", "economy", "bot", - "discord.js", - "discordjs" + "discordjs", + "discord.js" ], "dependencies": { "discord.js": "^14.27.0", From 343fe9dcac7f4e756015f31b4f6afd7b5f46ea33 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:29:23 +0300 Subject: [PATCH 075/175] Run command smoke tests in CI --- .github/workflows/node-compatibility.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node-compatibility.yml b/.github/workflows/node-compatibility.yml index 347162a..09df904 100644 --- a/.github/workflows/node-compatibility.yml +++ b/.github/workflows/node-compatibility.yml @@ -31,6 +31,9 @@ jobs: shell: bash run: | set -e - for file in index.js counter.js commands/*.js events/*.js slashCommands.js; do + for file in index.js counter.js commands/*.js events/*.js slashCommands.js tests/*.js; do node --check "$file" done + + - name: Smoke testlerini çalıştır + run: npm test From b195f595b5fe475d00fbaf18cbdfd93db8c8da69 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:30:17 +0300 Subject: [PATCH 076/175] Fix CI setup for repository without lockfile --- .github/workflows/node-compatibility.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/node-compatibility.yml b/.github/workflows/node-compatibility.yml index 09df904..272d55d 100644 --- a/.github/workflows/node-compatibility.yml +++ b/.github/workflows/node-compatibility.yml @@ -22,7 +22,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: npm - name: Bağımlılıkları yükle run: npm install --no-audit --no-fund From 5da843d783d12e7e8e6c40864f7aa64490fffb0d Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 22:38:02 +0300 Subject: [PATCH 077/175] Fix slash command execution adapter --- events/interactionCreate.js | 56 +++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index 70fa6bb..470c040 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -1,46 +1,54 @@ module.exports = async (client, interaction) => { if (!interaction.isChatInputCommand() || !interaction.guild) return; - try { - const command = client.commands.get(interaction.commandName); - - if (!command) { - return interaction.reply({ - content: "Bu komut artık mevcut değil. Botu yeniden başlatıp slash komutlarını güncelleyin.", - ephemeral: true - }); - } + const command = client.commands.get(interaction.commandName); + if (!command) { + return interaction.reply({ + content: "Bu komut artık mevcut değil. Botu yeniden başlatıp slash komutlarını güncelleyin.", + ephemeral: true + }); + } + try { const userOption = interaction.options.getUser("kullanici"); const memberOption = interaction.options.getMember("kullanici"); const amount = interaction.options.getInteger("miktar"); const product = interaction.options.getString("urun"); const newPrefix = interaction.options.getString("yeni_prefix"); + // Legacy komutların beklediği message/args yapısını slash interaction'a uyarla. const args = []; if (userOption) args.push(userOption.id); if (product) args.push(product); if (newPrefix) args.push(newPrefix); - if (amount !== null) args.push(String(amount)); + if (amount !== null && amount !== undefined) args.push(String(amount)); await interaction.deferReply(); let responded = false; + const respond = async (payload) => { responded = true; - if (payload === undefined || payload === null) payload = { content: "Komut tamamlandı." }; + const normalizedPayload = + payload === undefined || payload === null + ? { content: "Komut tamamlandı." } + : typeof payload === "string" + ? { content: payload } + : payload; - if (interaction.deferred && !interaction.replied) { - return interaction.editReply(payload); - } - - return interaction.followUp(payload); + if (interaction.replied) return interaction.followUp(normalizedPayload); + return interaction.editReply(normalizedPayload); }; const fakeMessage = { + id: interaction.id, author: interaction.user, member: interaction.member, guild: interaction.guild, + channel: { + id: interaction.channelId, + send: respond + }, createdTimestamp: interaction.createdTimestamp, content: `/${interaction.commandName}`, mentions: { @@ -51,29 +59,29 @@ module.exports = async (client, interaction) => { first: () => memberOption || null } }, - channel: { - id: interaction.channelId, - send: respond - }, reply: respond }; client.prefix = client.db.fetch(`prefix_${interaction.guild.id}`) || client.config.prefix; client.ecoAddUser = interaction.user.id; - await command.execute(client, fakeMessage, args); + const result = await command.execute(client, fakeMessage, args); + + // Eski bir komut yanıt göndermek yerine doğrudan bir payload döndürürse onu da destekle. + if (!responded && result !== undefined && result !== null) { + await respond(result); + } if (!responded) { await interaction.editReply({ - content: "Bu komut için işlem yapma yetkin bulunmuyor veya komut herhangi bir yanıt göndermedi." + content: "Komut çalıştı ancak bir yanıt oluşturmadı." }); } } catch (error) { console.error(`/${interaction.commandName} komutunda hata:`, error); const payload = { - content: "Komut çalıştırılırken beklenmeyen bir hata oluştu.", - ephemeral: true + content: "Komut çalıştırılırken beklenmeyen bir hata oluştu. Konsolu kontrol edin." }; if (!interaction.replied && !interaction.deferred) { From 551ee5a1bc5569481d55e173b203d1c9f8c8a5e0 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:15:56 +0300 Subject: [PATCH 078/175] Fix prefix and slash command system Fix prefix and slash command routing, modernize the SQLite economy backend, harden command validation/error handling, and add comprehensive command/database smoke tests. --- commands/addmoney.js | 22 ++-- commands/bal.js | 10 +- commands/beg.js | 8 +- commands/buy.js | 30 +++-- commands/daily.js | 12 +- commands/help.js | 9 +- commands/inventory.js | 28 ++-- commands/ping.js | 7 +- commands/prefix.js | 21 ++- commands/rob.js | 38 ++++-- commands/search.js | 16 +-- commands/setmoney.js | 24 ++-- commands/shop.js | 9 +- commands/transfer.js | 16 ++- commands/weekly.js | 8 +- commands/work.js | 12 +- counter.js | 47 ++++--- events/interactionCreate.js | 120 ++++++++--------- events/message.js | 18 --- events/messageCreate.js | 29 +++++ index.js | 84 +++++++----- lib/database.js | 90 +++++++++++++ lib/economy.js | 181 ++++++++++++++++++++++++++ package.json | 7 +- slashCommands.js | 2 +- tests/database.js | 64 +++++++++ tests/smoke.js | 252 ++++++++++++++++++++++++++++++++---- 27 files changed, 882 insertions(+), 282 deletions(-) delete mode 100644 events/message.js create mode 100644 events/messageCreate.js create mode 100644 lib/database.js create mode 100644 lib/economy.js create mode 100644 tests/database.js diff --git a/commands/addmoney.js b/commands/addmoney.js index 543894c..dfc5799 100644 --- a/commands/addmoney.js +++ b/commands/addmoney.js @@ -1,23 +1,27 @@ const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message, args) => { - if (!client.config.admins.includes(message.author.id)) return; + if (!client.config.admins.includes(message.author.id)) { + return message.reply("Bu komutu kullanmak için yetkin yok."); + } - const user = message.mentions.users.first(); - if (!user) return message.channel.send("Lütfen bir kullanıcı belirtin!"); + const user = message.mentions.users.first() || client.users.cache.get(args[0]); + if (!user) return message.reply("Lütfen geçerli bir kullanıcı belirtin."); - const amount = args[1]; - if (!amount || isNaN(amount)) return message.reply("Lütfen geçerli bir miktar belirtin."); + const amount = Number(args[1]); + if (!Number.isSafeInteger(amount) || amount <= 0) { + return message.reply("Lütfen 1 veya daha büyük, geçerli bir miktar belirtin."); + } - const data = client.eco.addMoney(user.id, parseInt(amount)); + const data = client.eco.addMoney(user.id, amount); const embed = new EmbedBuilder() .setTitle("Para Eklendi!") .addFields( - { name: "Kullanıcı", value: `<@${data.user}>` }, + { name: "Kullanıcı", value: `<@${user.id}>` }, { name: "Eklenen Miktar", value: `${data.amount} 💸` }, { name: "Toplam Bakiye", value: `${data.after} 💸` } ) - .setColor("Random") + .setColor("Blurple") .setThumbnail(user.displayAvatarURL()) .setTimestamp(); @@ -27,5 +31,5 @@ exports.execute = async (client, message, args) => { exports.help = { name: "addmoney", aliases: ["addbal", "paraekle", "para-ekle"], - usage: "addmoney @kullanıcı " + usage: "addmoney " }; diff --git a/commands/bal.js b/commands/bal.js index 04e8806..078e31c 100644 --- a/commands/bal.js +++ b/commands/bal.js @@ -7,11 +7,11 @@ exports.execute = async (client, message) => { const embed = new EmbedBuilder() .setTitle("Bakiye") .addFields( - { name: "Kullanıcı", value: `<@${userBalance.user}>` }, - { name: "Bakiye", value: `${userBalance.amount} 💸` }, - { name: "Sıralama", value: `${userBalance.position}` } + { name: "Kullanıcı", value: `<@${userBalance.user.id || user.id}>` }, + { name: "Bakiye", value: `${userBalance.balance} 💸` }, + { name: "Sıralama", value: userBalance.position ? `${userBalance.position}` : "Sıralamada değil" } ) - .setColor("Random") + .setColor("Blurple") .setThumbnail(user.displayAvatarURL()) .setTimestamp(); @@ -21,5 +21,5 @@ exports.execute = async (client, message) => { exports.help = { name: "bal", aliases: ["money", "credits", "balance", "bakiye", "para"], - usage: "bal [@kullanıcı]" + usage: "bal []" }; diff --git a/commands/beg.js b/commands/beg.js index a3e8013..112680d 100644 --- a/commands/beg.js +++ b/commands/beg.js @@ -1,14 +1,14 @@ exports.execute = async (client, message) => { const users = ["PewDiePie", "T-Series", "Sans", "Zero"]; const amount = Math.floor(Math.random() * 50) + 10; - const beg = client.eco.beg(client.ecoAddUser, amount, { canLose: true }); + const reward = client.eco.beg(message.author.id, amount, { canLose: true }); - if (beg.onCooldown) return message.reply(`Tekrar dilenebilmek için ${beg.time.seconds} saniye beklemelisin.`); - if (beg.lost) { + if (reward.onCooldown) return message.reply(`Tekrar dilenebilmek için ${reward.time.seconds} saniye beklemelisin.`); + if (reward.lost) { return message.channel.send(`**${users[Math.floor(Math.random() * users.length)]}:** Şansın yaver gitmedi! Daha sonra tekrar dene.`); } - return message.reply(`**${users[Math.floor(Math.random() * users.length)]}** sana **${beg.amount}** 💸 bağışladı. Artık **${beg.after}** 💸 paran var.`); + return message.reply(`**${users[Math.floor(Math.random() * users.length)]}** sana **${reward.amount}** 💸 bağışladı. Artık **${reward.after}** 💸 paran var.`); }; exports.help = { diff --git a/commands/buy.js b/commands/buy.js index d9e3514..569a0d1 100644 --- a/commands/buy.js +++ b/commands/buy.js @@ -1,27 +1,31 @@ exports.execute = async (client, message, args) => { const userBalance = client.eco.fetchMoney(message.author.id); - if (userBalance.amount < 1) return message.channel.send("Görünüşe göre hiç paran yok."); + const itemName = args[0]; - const item = args[0]; - if (!item) return message.channel.send("Ne satın almaya çalışıyorsun?"); + if (!itemName) return message.reply("Satın almak istediğin ürünü belirtmelisin."); - const hasItem = client.shop[item]; - if (!hasItem) return message.reply("Böyle bir ürün bulunmuyor."); + const item = Object.entries(client.shop).find(([name]) => name.toLowerCase() === itemName.toLowerCase()); + if (!item) return message.reply("Böyle bir ürün bulunmuyor. `shop` komutuyla mağazayı görüntüleyebilirsin."); - const isBalanceEnough = userBalance.amount >= hasItem.cost; - if (!isBalanceEnough) { - return message.reply(`Bakiyen yetersiz. Bu ürünü almak için 💸${hasItem.cost} gerekiyor.`); + const [name, product] = item; + if (userBalance.balance < product.cost) { + return message.reply(`Bakiyen yetersiz. Bu ürünü almak için **${product.cost}** 💸 gerekiyor.`); } - client.eco.removeMoney(message.author.id, hasItem.cost); + const removed = client.eco.removeMoney(message.author.id, product.cost); + if (removed.error) return message.reply("Satın alma sırasında bakiye işlemi başarısız oldu."); const itemStruct = { - name: item.toLowerCase(), - prize: hasItem.cost + name: name.toLowerCase(), + price: product.cost, + purchasedAt: Date.now() }; - client.db.push(`items_${message.author.id}`, itemStruct); - return message.channel.send(`**${item}** ürününü **💸${hasItem.cost}** karşılığında satın aldın.`); + const currentItems = client.db.get(`items_${message.author.id}`); + const items = Array.isArray(currentItems) ? currentItems : []; + client.db.set(`items_${message.author.id}`, [...items, itemStruct]); + + return message.channel.send(`**${name}** ürününü **💸${product.cost}** karşılığında satın aldın.`); }; exports.help = { diff --git a/commands/daily.js b/commands/daily.js index 24ed74e..b592298 100644 --- a/commands/daily.js +++ b/commands/daily.js @@ -1,15 +1,15 @@ -module.exports.execute = async (client, message) => { +exports.execute = async (client, message) => { const amount = Math.floor(Math.random() * 500) + 100; - const addMoney = client.eco.daily(client.ecoAddUser, amount); + const reward = client.eco.daily(message.author.id, amount); - if (addMoney.onCooldown) { - return message.reply(`Günlük ödülünü zaten aldın. Tekrar almak için ${addMoney.time.hours} saat, ${addMoney.time.minutes} dakika ve ${addMoney.time.seconds} saniye beklemelisin.`); + if (reward.onCooldown) { + return message.reply(`Günlük ödülünü zaten aldın. Tekrar almak için ${reward.time.hours} saat, ${reward.time.minutes} dakika ve ${reward.time.seconds} saniye beklemelisin.`); } - return message.reply(`Günlük ödül olarak **${addMoney.amount}** 💸 kazandın. Artık **${addMoney.after}** 💸 paran var.`); + return message.reply(`Günlük ödül olarak **${reward.amount}** 💸 kazandın. Artık **${reward.after}** 💸 paran var.`); }; -module.exports.help = { +exports.help = { name: "daily", aliases: ["günlük", "gunluk"], usage: "daily" diff --git a/commands/help.js b/commands/help.js index be808b0..81c7630 100644 --- a/commands/help.js +++ b/commands/help.js @@ -1,6 +1,7 @@ const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message) => { + const prefix = String(message.prefix || client.config.prefix || "!"); const embed = new EmbedBuilder() .setAuthor({ name: "Komutlar" }) .setTitle("INS Development Economy Bot!") @@ -10,13 +11,13 @@ exports.execute = async (client, message) => { .setTimestamp() .setThumbnail(client.user.displayAvatarURL()); - client.commands.forEach((cmd) => { + for (const command of client.commands.values()) { embed.addFields({ - name: cmd.help.name, - value: `Takma Adlar: ${cmd.help.aliases.join(", ") || "Yok"}\nKullanım: \`${client.prefix}${cmd.help.usage}\``, + name: command.help.name, + value: `Takma Adlar: ${command.help.aliases.join(", ") || "Yok"}\nKullanım: \`${prefix}${command.help.usage}\``, inline: true }); - }); + } embed.setFooter({ text: message.author.tag, iconURL: message.author.displayAvatarURL() }); return message.channel.send({ embeds: [embed] }); diff --git a/commands/inventory.js b/commands/inventory.js index d6434d0..6d68f8d 100644 --- a/commands/inventory.js +++ b/commands/inventory.js @@ -1,26 +1,30 @@ const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message) => { + const items = client.db.get(`items_${message.author.id}`); + + if (!Array.isArray(items) || items.length === 0) { + return message.reply("Envanterin boş."); + } + + const grouped = new Map(); + for (const item of items) { + const name = String(item?.name || "Bilinmeyen ürün"); + grouped.set(name, (grouped.get(name) || 0) + 1); + } + const embed = new EmbedBuilder() .setAuthor({ name: `${message.author.tag} kullanıcısının envanteri`, iconURL: message.guild.iconURL() || undefined }) - .setColor("Random") + .setColor("Blurple") .setTimestamp(); - const items = client.db.get(`items_${message.author.id}`); - if (!items) return message.channel.send("Gösterilecek eşya bulunamadı."); - - const arrayToObject = items.reduce((itemsObject, item) => { - itemsObject[item.name] = (itemsObject[item.name] || 0) + 1; - return itemsObject; - }, {}); - - Object.keys(arrayToObject).forEach((name) => { + for (const [name, count] of grouped) { embed.addFields({ name: `İsim: ${name}`, - value: `Miktar: **${arrayToObject[name]}**`, + value: `Miktar: **${count}**`, inline: false }); - }); + } return message.channel.send({ embeds: [embed] }); }; diff --git a/commands/ping.js b/commands/ping.js index 3604536..2d6dae4 100644 --- a/commands/ping.js +++ b/commands/ping.js @@ -1,9 +1,10 @@ const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message) => { - const gatewayLatency = Math.floor(client.ws.ping); + const gatewayLatency = Math.max(0, Math.floor(client.ws.ping)); + const startedAt = Date.now(); const sentMessage = await message.channel.send("Ping ölçülüyor..."); - const trip = Math.floor(sentMessage.createdTimestamp - message.createdTimestamp); + const trip = Math.max(0, Date.now() - startedAt); const embed = new EmbedBuilder() .setTitle("Pong!") @@ -11,7 +12,7 @@ exports.execute = async (client, message) => { { name: "API Gecikmesi", value: `${gatewayLatency}ms`, inline: true }, { name: "İstemci Gecikmesi", value: `${trip}ms`, inline: true } ) - .setColor("#7289DA") + .setColor("Blurple") .setTimestamp(); return sentMessage.edit({ content: null, embeds: [embed] }); diff --git a/commands/prefix.js b/commands/prefix.js index da9787e..00f73cf 100644 --- a/commands/prefix.js +++ b/commands/prefix.js @@ -1,16 +1,25 @@ -exports.execute = (client, message, args) => { - if (!message.member.permissions.has("ManageGuild") && !client.config.admins.includes(message.author.id)) { - return message.channel.send(`Bu sunucunun prefix'i **${client.prefix}**.`); +exports.execute = async (client, message, args) => { + const currentPrefix = String(message.prefix || client.config.prefix || "!"); + const canManage = message.member?.permissions?.has("ManageGuild") || client.config.admins.includes(message.author.id); + + if (!canManage) { + return message.reply(`Bu sunucunun prefix'i **${currentPrefix}**.`); } - const prefix = args[0]; + const prefix = args[0]?.trim(); if (!prefix) { client.db.delete(`prefix_${message.guild.id}`); - return message.channel.send("✅ | Bu sunucunun prefix'i sıfırlandı."); + message.prefix = String(client.config.prefix || "!"); + return message.channel.send(`✅ | Bu sunucunun prefix'i varsayılan **${message.prefix}** olarak sıfırlandı.`); + } + + if (prefix.length > 10 || /\s/.test(prefix)) { + return message.reply("Prefix 1-10 karakter arasında olmalı ve boşluk içeremez."); } const setTo = client.db.set(`prefix_${message.guild.id}`, prefix); - return message.channel.send(`✅ | Prefix \`${setTo}\` olarak ayarlandı.`); + message.prefix = String(setTo); + return message.channel.send(`✅ | Prefix **${message.prefix}** olarak ayarlandı.`); }; exports.help = { diff --git a/commands/rob.js b/commands/rob.js index 2b149a1..3049aed 100644 --- a/commands/rob.js +++ b/commands/rob.js @@ -1,26 +1,42 @@ exports.execute = async (client, message, args) => { const target = message.mentions.members.first() || message.guild.members.cache.get(args[0]); - if (!target) return message.reply("Kimi soymaya çalışıyorsun?"); + if (!target) return message.reply("Kimi soymaya çalıştığını belirtmelisin."); + if (target.id === message.author.id) return message.reply("Kendini soyamazsın."); + if (target.user.bot) return message.reply("Bot hesapları soyulamaz."); + + const cooldownKey = `robCooldown_${message.author.id}`; + const now = Date.now(); + const lastAttempt = Number(client.db.fetch(cooldownKey) || 0); + const cooldown = 60_000; + const remaining = cooldown - (now - lastAttempt); + + if (remaining > 0) { + return message.reply(`Yakın zamanda bir soygun denedin. Tekrar denemek için **${Math.ceil(remaining / 1000)} saniye** beklemelisin.`); + } + + client.db.set(cooldownKey, now); + + const targetBalance = client.eco.fetchMoney(target.id).balance; + if (targetBalance < 1) return message.reply("Bu kullanıcının çalınabilecek parası yok."); const messages = [ `${target} kullanıcısını soymaya çalışırken yakalandın!`, - `Sinsi davranmaya mı çalışıyorsun? ${target} polisi aradı!`, + `Sinsi davranmaya çalıştın ama ${target} fark etti!`, `${target} kullanıcısını soyma girişimin başarısız oldu!` ]; - const amount = Math.floor(Math.random() * 50) + 10; - const rob = client.eco.beg(client.ecoAddUser, amount, { canLose: true }); - - if (rob.onCooldown) { - return message.reply(`Yakın zamanda bir soygun denedin. Tekrar denemek için ${rob.time.seconds} saniye beklemelisin.`); + if (Math.floor(Math.random() * 5) === 0) { + return message.channel.send(messages[Math.floor(Math.random() * messages.length)]); } - if (rob.lost) return message.channel.send(messages[Math.floor(Math.random() * messages.length)]); + const requestedAmount = Math.floor(Math.random() * 50) + 10; + const amount = Math.min(requestedAmount, targetBalance); + const result = client.eco.transfer(target.id, message.author.id, amount); - const targetBalance = client.eco.fetchMoney(target.id).amount - amount; - client.eco.setMoney(target.id, Math.max(0, parseInt(targetBalance))); + if (result.error) return message.reply("Soygun gerçekleştirilemedi. Hedefin bakiyesi değişmiş olabilir."); - return message.reply(`${target} kullanıcısından **${rob.amount}** 💸 çaldın. Artık **${rob.after}** 💸 paran var.`); + const robberBalance = client.eco.fetchMoney(message.author.id).balance; + return message.reply(`${target} kullanıcısından **${amount}** 💸 çaldın. Artık **${robberBalance}** 💸 paran var.`); }; exports.help = { diff --git a/commands/search.js b/commands/search.js index 6b0408f..40a0c10 100644 --- a/commands/search.js +++ b/commands/search.js @@ -1,24 +1,18 @@ exports.execute = async (client, message) => { - const places = [ - "Cep", - "Tişört", - "Zero'nun Veritabanı", - "Sokak" - ]; - + const places = ["Cep", "Tişört", "Sokak", "Eski bir sandık"]; const amount = Math.floor(Math.random() * 200) + 50; - const search = client.eco.beg(client.ecoAddUser, amount, { + const reward = client.eco.beg(message.author.id, amount, { canLose: true, cooldown: 300000, customName: "search" }); - if (search.onCooldown) return message.reply(`${search.time.minutes} dakika ${search.time.seconds} saniye sonra tekrar dene.`); - if (search.lost) { + if (reward.onCooldown) return message.reply(`${reward.time.minutes} dakika ${reward.time.seconds} saniye sonra tekrar dene.`); + if (reward.lost) { return message.channel.send(`**${places[Math.floor(Math.random() * places.length)]}:** Yakalandın! Para bulamadın.`); } - return message.reply(`**${places[Math.floor(Math.random() * places.length)]}** araması kârlı çıktı; **${search.amount}** 💸 buldun. Artık **${search.after}** 💸 paran var.`); + return message.reply(`**${places[Math.floor(Math.random() * places.length)]}** araması kârlı çıktı; **${reward.amount}** 💸 buldun. Artık **${reward.after}** 💸 paran var.`); }; exports.help = { diff --git a/commands/setmoney.js b/commands/setmoney.js index 02e77b2..2248be6 100644 --- a/commands/setmoney.js +++ b/commands/setmoney.js @@ -1,22 +1,26 @@ const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message, args) => { - if (!client.config.admins.includes(message.author.id)) return; + if (!client.config.admins.includes(message.author.id)) { + return message.reply("Bu komutu kullanmak için yetkin yok."); + } - const user = message.mentions.users.first(); - if (!user) return message.channel.send("Lütfen bir kullanıcı belirtin!"); + const user = message.mentions.users.first() || client.users.cache.get(args[0]); + if (!user) return message.reply("Lütfen geçerli bir kullanıcı belirtin."); - const amount = args[1]; - if (!amount || isNaN(amount)) return message.reply("Lütfen geçerli bir miktar belirtin."); + const amount = Number(args[1]); + if (!Number.isSafeInteger(amount) || amount < 1) { + return message.reply("Lütfen 1 veya daha büyük, geçerli bir miktar belirtin."); + } - const data = client.eco.setMoney(user.id, parseInt(amount)); + const data = client.eco.setMoney(user.id, amount); const embed = new EmbedBuilder() .setTitle("Para Güncellendi!") .addFields( - { name: "Kullanıcı", value: `<@${data.user}>` }, - { name: "Toplam Bakiye", value: `${data.after} 💸` } + { name: "Kullanıcı", value: `<@${user.id}>` }, + { name: "Yeni Bakiye", value: `${data.after} 💸` } ) - .setColor("Random") + .setColor("Blurple") .setThumbnail(user.displayAvatarURL()) .setTimestamp(); @@ -26,5 +30,5 @@ exports.execute = async (client, message, args) => { exports.help = { name: "setmoney", aliases: ["setbal", "parayarla", "bakiyeayarla"], - usage: "setmoney @kullanıcı " + usage: "setmoney " }; diff --git a/commands/shop.js b/commands/shop.js index c0cc413..c4d077c 100644 --- a/commands/shop.js +++ b/commands/shop.js @@ -1,14 +1,15 @@ const { EmbedBuilder } = require("discord.js"); exports.execute = async (client, message) => { - const items = Object.keys(client.shop); - const content = items.map((item) => `${item} - 💸 ${client.shop[item].cost}`).join("\n"); + const prefix = String(message.prefix || client.config.prefix || "!"); + const items = Object.entries(client.shop); + const content = items.map(([name, product]) => `${name} — 💸 ${product.cost}`).join("\n"); const embed = new EmbedBuilder() .setTitle("Mağaza") - .setDescription(content) + .setDescription(content || "Mağazada ürün bulunmuyor.") .setColor("Blurple") - .setFooter({ text: `${client.prefix}buy <ürün> yazarak ürünü satın alabilirsin.` }); + .setFooter({ text: `${prefix}buy <ürün> yazarak ürünü satın alabilirsin.` }); return message.channel.send({ embeds: [embed] }); }; diff --git a/commands/transfer.js b/commands/transfer.js index 81bd670..2c088b3 100644 --- a/commands/transfer.js +++ b/commands/transfer.js @@ -1,14 +1,18 @@ exports.execute = async (client, message, args) => { const member = message.mentions.members.first() || message.guild.members.cache.get(args[0]); - const authorData = client.eco.fetchMoney(message.author.id); + const amount = Number(args[1]); + + if (!member) return message.reply("Lütfen geçerli bir kullanıcı belirt."); + if (member.id === message.author.id) return message.reply("Kendine para gönderemezsin."); + if (member.user.bot) return message.reply("Bot hesaplarına para gönderilemez."); + if (!Number.isSafeInteger(amount) || amount <= 0) return message.reply("Lütfen 1 veya daha büyük, geçerli bir miktar gir."); - if (!member) return message.channel.send("Lütfen kişiyi etiketle veya kullanıcı kimliğini gir."); + const authorData = client.eco.fetchMoney(message.author.id); + if (authorData.balance < amount) return message.reply("Görünüşe göre bu kadar paran yok."); - const amount = args[1]; - if (!amount || isNaN(amount)) return message.channel.send("Lütfen aktarılacak geçerli bir miktar gir."); - if (authorData.amount < amount) return message.channel.send("Görünüşe göre bu kadar paran yok."); + const result = client.eco.transfer(message.author.id, member.id, amount); + if (result.error) return message.reply("Para transferi gerçekleştirilemedi."); - await client.eco.transfer(message.author.id, member.id, amount); return message.channel.send(`💸 **${amount}** miktarını **${member.user.tag}** kullanıcısına başarıyla aktardın.`); }; diff --git a/commands/weekly.js b/commands/weekly.js index 3459e91..d10da60 100644 --- a/commands/weekly.js +++ b/commands/weekly.js @@ -1,12 +1,12 @@ exports.execute = async (client, message) => { const amount = Math.floor(Math.random() * 1000) + 500; - const addMoney = client.eco.weekly(client.ecoAddUser, amount); + const reward = client.eco.weekly(message.author.id, amount); - if (addMoney.onCooldown) { - return message.reply(`Haftalık ödülünü zaten aldın. Tekrar almak için ${addMoney.time.days} gün, ${addMoney.time.hours} saat, ${addMoney.time.minutes} dakika ve ${addMoney.time.seconds} saniye beklemelisin.`); + if (reward.onCooldown) { + return message.reply(`Haftalık ödülünü zaten aldın. Tekrar almak için ${reward.time.days} gün, ${reward.time.hours} saat, ${reward.time.minutes} dakika ve ${reward.time.seconds} saniye beklemelisin.`); } - return message.reply(`Haftalık ödül olarak **${addMoney.amount}** 💸 kazandın. Artık **${addMoney.after}** 💸 paran var.`); + return message.reply(`Haftalık ödül olarak **${reward.amount}** 💸 kazandın. Artık **${reward.after}** 💸 paran var.`); }; exports.help = { diff --git a/commands/work.js b/commands/work.js index 9c1969e..c91f893 100644 --- a/commands/work.js +++ b/commands/work.js @@ -1,15 +1,15 @@ -module.exports.execute = async (client, message) => { +exports.execute = async (client, message) => { const amount = Math.floor(Math.random() * 1500) + 1000; - const work = client.eco.work(client.ecoAddUser, amount); + const reward = client.eco.work(message.author.id, amount); - if (work.onCooldown) { - return message.reply(`Yorgunsun. Tekrar çalışmak için ${work.time.minutes} dakika ${work.time.seconds} saniye beklemelisin.`); + if (reward.onCooldown) { + return message.reply(`Yorgunsun. Tekrar çalışmak için ${reward.time.minutes} dakika ${reward.time.seconds} saniye beklemelisin.`); } - return message.reply(`**${work.workedAs}** olarak çalıştın ve **${work.amount}** 💸 kazandın. Artık **${work.after}** 💸 paran var.`); + return message.reply(`**${reward.workedAs}** olarak çalıştın ve **${reward.amount}** 💸 kazandın. Artık **${reward.after}** 💸 paran var.`); }; -module.exports.help = { +exports.help = { name: "work", aliases: ["çalış", "calis", "çalıştır", "calistir"], usage: "work" diff --git a/counter.js b/counter.js index ed3a0f0..bde6343 100644 --- a/counter.js +++ b/counter.js @@ -1,46 +1,43 @@ -const { db } = require("quick.eco"); - function counter(message, client) { const channel = message.channel; - let count = db.fetch(`counter_${message.guild.id}`); + let count = client.db.fetch(`counter_${message.guild.id}`); - if (count === null) { - count = db.set(`counter_${message.guild.id}`, { - number: 0, - author: client.user.id - }); + if (!count || typeof count !== "object") { + count = { number: 0, author: null }; + client.db.set(`counter_${message.guild.id}`, count); } - if (!message.author.bot && message.author.id === count.author) { + if (message.author.id === count.author) { message.delete().catch(() => {}); - message.reply("Sıra sende değil, lütfen bekle.").then((m) => { - setTimeout(() => m.delete().catch(() => {}), 3000); - }).catch(() => {}); + message.reply("Sıra sende değil, lütfen bekle.") + .then((reply) => setTimeout(() => reply.delete().catch(() => {}), 3000)) + .catch(() => {}); return; } - if (!message.author.bot && isNaN(message.content)) { + if (!/^\d+$/.test(message.content)) { message.delete().catch(() => {}); - message.reply("Bu kanaldaki mesajlar sayı olmalıdır.").then((m) => { - setTimeout(() => m.delete().catch(() => {}), 3000); - }).catch(() => {}); + message.reply("Bu kanaldaki mesajlar sayı olmalıdır.") + .then((reply) => setTimeout(() => reply.delete().catch(() => {}), 3000)) + .catch(() => {}); return; } - if (!message.author.bot && parseInt(message.content) !== count.number + 1) { + const number = Number(message.content); + if (!Number.isSafeInteger(number) || number !== count.number + 1) { message.delete().catch(() => {}); - message.reply(`Sıradaki sayı ${count.number + 1} olmalıdır.`).then((m) => { - setTimeout(() => m.delete().catch(() => {}), 3000); - }).catch(() => {}); + message.reply(`Sıradaki sayı ${count.number + 1} olmalıdır.`) + .then((reply) => setTimeout(() => reply.delete().catch(() => {}), 3000)) + .catch(() => {}); return; } - count = db.set(`counter_${message.guild.id}`, { - number: count.number + 1, + const next = { + number, author: message.author.id - }); - - channel.setTopic(`Sıradaki sayı ${count.number + 1} olmalıdır.`).catch(() => {}); + }; + client.db.set(`counter_${message.guild.id}`, next); + channel.setTopic(`Sıradaki sayı ${number + 1} olmalıdır.`).catch(() => {}); } module.exports = counter; diff --git a/events/interactionCreate.js b/events/interactionCreate.js index 470c040..d670355 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -3,91 +3,85 @@ module.exports = async (client, interaction) => { const command = client.commands.get(interaction.commandName); if (!command) { - return interaction.reply({ - content: "Bu komut artık mevcut değil. Botu yeniden başlatıp slash komutlarını güncelleyin.", - ephemeral: true - }); + const payload = { content: "Bu komut artık mevcut değil. Botu yeniden başlatıp slash komutlarını güncelleyin." }; + if (interaction.deferred || interaction.replied) { + return interaction.editReply(payload).catch(() => {}); + } + return interaction.reply({ ...payload, ephemeral: true }).catch(() => {}); } - try { - const userOption = interaction.options.getUser("kullanici"); - const memberOption = interaction.options.getMember("kullanici"); - const amount = interaction.options.getInteger("miktar"); - const product = interaction.options.getString("urun"); - const newPrefix = interaction.options.getString("yeni_prefix"); + const userOption = interaction.options.getUser("kullanici"); + const memberOption = interaction.options.getMember("kullanici"); + const amount = interaction.options.getInteger("miktar"); + const product = interaction.options.getString("urun"); + const newPrefix = interaction.options.getString("yeni_prefix"); - // Legacy komutların beklediği message/args yapısını slash interaction'a uyarla. - const args = []; - if (userOption) args.push(userOption.id); - if (product) args.push(product); - if (newPrefix) args.push(newPrefix); - if (amount !== null && amount !== undefined) args.push(String(amount)); + const args = []; + if (userOption) args.push(userOption.id); + if (product) args.push(product); + if (newPrefix) args.push(newPrefix); + if (amount !== null && amount !== undefined) args.push(String(amount)); - await interaction.deferReply(); + const storedPrefix = client.db.fetch(`prefix_${interaction.guild.id}`); + const guildPrefix = String(storedPrefix || client.config.prefix || "!"); - let responded = false; + const normalizePayload = (payload) => { + if (payload === undefined || payload === null) return { content: "Komut tamamlandı." }; + return typeof payload === "string" ? { content: payload } : payload; + }; - const respond = async (payload) => { - responded = true; - const normalizedPayload = - payload === undefined || payload === null - ? { content: "Komut tamamlandı." } - : typeof payload === "string" - ? { content: payload } - : payload; + await interaction.deferReply(); - if (interaction.replied) return interaction.followUp(normalizedPayload); - return interaction.editReply(normalizedPayload); - }; + let responded = false; + const respond = async (payload) => { + const normalized = normalizePayload(payload); + responded = true; - const fakeMessage = { - id: interaction.id, - author: interaction.user, - member: interaction.member, - guild: interaction.guild, - channel: { - id: interaction.channelId, - send: respond - }, - createdTimestamp: interaction.createdTimestamp, - content: `/${interaction.commandName}`, - mentions: { - users: { - first: () => userOption || null - }, - members: { - first: () => memberOption || null - } - }, - reply: respond - }; + if (interaction.replied) return interaction.followUp(normalized); + return interaction.editReply(normalized); + }; - client.prefix = client.db.fetch(`prefix_${interaction.guild.id}`) || client.config.prefix; - client.ecoAddUser = interaction.user.id; + const fakeMessage = { + id: interaction.id, + author: interaction.user, + member: interaction.member, + guild: interaction.guild, + channel: { + id: interaction.channelId, + send: respond + }, + createdTimestamp: interaction.createdTimestamp, + content: `/${interaction.commandName}`, + prefix: guildPrefix, + mentions: { + users: { + first: () => userOption || null + }, + members: { + first: () => memberOption || null + } + }, + reply: respond + }; + try { const result = await command.execute(client, fakeMessage, args); - // Eski bir komut yanıt göndermek yerine doğrudan bir payload döndürürse onu da destekle. if (!responded && result !== undefined && result !== null) { await respond(result); } if (!responded) { - await interaction.editReply({ - content: "Komut çalıştı ancak bir yanıt oluşturmadı." - }); + await interaction.editReply({ content: "Komut tamamlandı ancak görünür bir yanıt oluşturmadı." }); } } catch (error) { console.error(`/${interaction.commandName} komutunda hata:`, error); + const payload = { content: "Komut çalıştırılırken beklenmeyen bir hata oluştu. Lütfen tekrar deneyin." }; - const payload = { - content: "Komut çalıştırılırken beklenmeyen bir hata oluştu. Konsolu kontrol edin." - }; - - if (!interaction.replied && !interaction.deferred) { - await interaction.reply(payload).catch(() => {}); - } else { + if (interaction.replied || interaction.deferred) { await interaction.editReply(payload).catch(() => {}); + } else { + await interaction.reply({ ...payload, ephemeral: true }).catch(() => {}); } } }; diff --git a/events/message.js b/events/message.js deleted file mode 100644 index f270eb3..0000000 --- a/events/message.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = async (client, message) => { - if (!message.guild || message.author.bot) return; - - if (message.channel.id === client.config.countChannel) { - require("../counter")(message, client); - } - - client.prefix = client.db.fetch(`prefix_${message.guild.id}`) || client.config.prefix; - if (!message.content.startsWith(client.prefix)) return; - - const args = message.content.slice(client.prefix.length).trim().split(/\s+/); - const commandName = args.shift().toLowerCase(); - const command = client.commands.get(commandName) || client.commands.get(client.aliases.get(commandName)); - if (!command) return; - - client.ecoAddUser = message.author.id; - await command.execute(client, message, args); -}; diff --git a/events/messageCreate.js b/events/messageCreate.js new file mode 100644 index 0000000..0254d10 --- /dev/null +++ b/events/messageCreate.js @@ -0,0 +1,29 @@ +module.exports = async (client, message) => { + if (!message.guild || message.author.bot) return; + + if (message.channel.id === client.config.countChannel) { + require("../counter")(message, client); + } + + const storedPrefix = client.db.fetch(`prefix_${message.guild.id}`); + const prefix = String(storedPrefix || client.config.prefix || "!"); + message.prefix = prefix; + + if (!message.content.startsWith(prefix)) return; + + const content = message.content.slice(prefix.length).trim(); + if (!content) return; + + const args = content.split(/\s+/); + const commandName = args.shift().toLowerCase(); + const canonicalName = client.aliases.get(commandName) || commandName; + const command = client.commands.get(canonicalName); + if (!command) return; + + try { + await command.execute(client, message, args); + } catch (error) { + console.error(`Prefix komutunda hata (${prefix}${commandName}):`, error); + await message.reply("Komut çalıştırılırken beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.").catch(() => {}); + } +}; diff --git a/index.js b/index.js index fed5e60..36166fa 100644 --- a/index.js +++ b/index.js @@ -1,45 +1,67 @@ -const { Client, Collection, GatewayIntentBits, Partials } = require("discord.js"); -const Eco = require("quick.eco"); -const fs = require("fs"); +const { Client, Collection, GatewayIntentBits } = require("discord.js"); +const fs = require("node:fs"); +const path = require("node:path"); +const KeyValueStore = require("./lib/database"); +const EconomyManager = require("./lib/economy"); const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent - ], - allowedMentions: { parse: ["users", "roles"] }, - partials: [Partials.Channel] + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], + allowedMentions: { parse: ["users", "roles"], repliedUser: false } }); -client.eco = new Eco.Manager(); -client.db = Eco.db; client.config = require("./botConfig"); -client.prefix = client.config.prefix; +client.prefix = String(client.config.prefix || "!"); +client.db = new KeyValueStore(); +client.eco = new EconomyManager(client.db); client.commands = new Collection(); client.aliases = new Collection(); -client.shop = { - "Laptop": { cost: 2000 }, - "Mobile": { cost: 1000 }, - "PC": { cost: 3000 } -}; +client.shop = Object.freeze({ + Laptop: { cost: 2000 }, + Mobile: { cost: 1000 }, + PC: { cost: 3000 } +}); -fs.readdirSync("./events/").forEach((file) => { - if (!file.endsWith(".js")) return; - const event = require(`./events/${file}`); - const eventName = file.split(".")[0]; +const eventsPath = path.join(__dirname, "events"); +for (const file of fs.readdirSync(eventsPath).filter((entry) => entry.endsWith(".js"))) { + const eventName = path.basename(file, ".js"); + const event = require(path.join(eventsPath, file)); + if (typeof event !== "function") throw new TypeError(`Event '${file}' must export a function.`); client.on(eventName, event.bind(null, client)); -}); +} -fs.readdirSync("./commands/").forEach((file) => { - if (!file.endsWith(".js")) return; - const command = require(`./commands/${file}`); - client.commands.set(command.help.name, command); +const commandsPath = path.join(__dirname, "commands"); +for (const file of fs.readdirSync(commandsPath).filter((entry) => entry.endsWith(".js"))) { + const command = require(path.join(commandsPath, file)); + if (!command || typeof command.execute !== "function" || !command.help?.name) { + throw new TypeError(`Invalid command module: ${file}`); + } - for (const alias of command.help.aliases) { - client.aliases.set(alias, command.help.name); + const name = command.help.name.toLowerCase(); + client.commands.set(name, command); + for (const alias of command.help.aliases || []) { + client.aliases.set(String(alias).toLowerCase(), name); } -}); +} + +client.on("error", (error) => console.error("Discord client error:", error)); +client.on("warn", (warning) => console.warn("Discord warning:", warning)); -client.login(client.config.token); +process.on("unhandledRejection", (error) => console.error("Unhandled promise rejection:", error)); +process.on("uncaughtException", (error) => console.error("Uncaught exception:", error)); + +const shutdown = () => { + client.db.close(); +}; +process.once("SIGINT", shutdown); +process.once("SIGTERM", shutdown); + +if (!client.config.token || client.config.token === "YOUR_TOKEN") { + throw new Error("botConfig.js içindeki token ayarlanmalı."); +} + +client.login(client.config.token).catch((error) => { + console.error("Discord'a giriş yapılamadı:", error); + client.db.close(); + process.exitCode = 1; +}); diff --git a/lib/database.js b/lib/database.js new file mode 100644 index 0000000..35131b2 --- /dev/null +++ b/lib/database.js @@ -0,0 +1,90 @@ +const Database = require("better-sqlite3"); +const path = require("node:path"); + +class KeyValueStore { + constructor(filePath = path.join(process.cwd(), "economy.sqlite")) { + this.connection = new Database(filePath); + this.connection.pragma("journal_mode = WAL"); + this.connection.pragma("foreign_keys = ON"); + this.connection.exec(` + CREATE TABLE IF NOT EXISTS json ( + ID TEXT NOT NULL, + json TEXT NOT NULL + ) + `); + + this.statements = { + get: this.connection.prepare("SELECT json FROM json WHERE ID = ? LIMIT 1"), + update: this.connection.prepare("UPDATE json SET json = ? WHERE ID = ?"), + insert: this.connection.prepare("INSERT INTO json (ID, json) VALUES (?, ?)"), + delete: this.connection.prepare("DELETE FROM json WHERE ID = ?"), + all: this.connection.prepare("SELECT ID, json FROM json"), + clear: this.connection.prepare("DELETE FROM json") + }; + } + + encode(value) { + const encoded = JSON.stringify(value); + if (encoded === undefined) throw new TypeError("Database value must be JSON serializable."); + return encoded; + } + + decode(raw) { + if (raw === null || raw === undefined) return null; + return JSON.parse(raw); + } + + get(key) { + const row = this.statements.get.get(String(key)); + return row ? this.decode(row.json) : null; + } + + fetch(key) { + return this.get(key); + } + + has(key) { + return this.statements.get.get(String(key)) !== undefined; + } + + set(key, value) { + const normalizedKey = String(key); + const encoded = this.encode(value); + const update = this.statements.update.run(encoded, normalizedKey); + if (update.changes === 0) this.statements.insert.run(normalizedKey, encoded); + return value; + } + + push(key, value) { + const current = this.get(key); + const array = Array.isArray(current) ? current : []; + array.push(value); + this.set(key, array); + return array; + } + + delete(key) { + return this.statements.delete.run(String(key)).changes > 0; + } + + all() { + return this.statements.all.all().map(({ ID, json }) => ({ + ID, + data: this.decode(json) + })); + } + + clear() { + return this.statements.clear.run().changes; + } + + transaction(callback) { + return this.connection.transaction(callback)(); + } + + close() { + if (this.connection.open) this.connection.close(); + } +} + +module.exports = KeyValueStore; diff --git a/lib/economy.js b/lib/economy.js new file mode 100644 index 0000000..9fc0699 --- /dev/null +++ b/lib/economy.js @@ -0,0 +1,181 @@ +const KeyValueStore = require("./database"); + +class EconomyManager { + constructor(store) { + if (!(store instanceof KeyValueStore) && (!store || typeof store.fetch !== "function" || typeof store.set !== "function")) { + throw new TypeError("Geçerli bir veritabanı sağlayıcısı gerekli."); + } + this.db = store; + } + + assertUserId(userId) { + if (!/^\d{17,20}$/.test(String(userId))) throw new TypeError("Geçersiz Discord kullanıcı ID'si."); + } + + assertPositiveAmount(amount, allowZero = false) { + if (!Number.isSafeInteger(amount) || (allowZero ? amount < 0 : amount <= 0)) { + throw new TypeError("Miktar güvenli bir tam sayı olmalı ve geçerli aralıkta bulunmalı."); + } + } + + fetchMoney(userId) { + this.assertUserId(userId); + const id = String(userId); + const stored = Number(this.db.fetch(`money_${id}`)); + const balance = Number.isSafeInteger(stored) && stored >= 0 ? stored : 0; + const position = this.leaderboard({ limit: 0 }).findIndex((entry) => entry.id === id); + + return { + balance, + bank: 0, + user: { id }, + position: position === -1 ? null : position + 1 + }; + } + + addMoney(userId, amount) { + this.assertUserId(userId); + this.assertPositiveAmount(amount); + const before = this.fetchMoney(userId).balance; + const after = before + amount; + if (!Number.isSafeInteger(after)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); + this.db.set(`money_${userId}`, after); + return { before, after, user: { id: String(userId) }, amount }; + } + + setMoney(userId, amount) { + this.assertUserId(userId); + this.assertPositiveAmount(amount, true); + const before = this.fetchMoney(userId).balance; + this.db.set(`money_${userId}`, amount); + return { before, after: amount, user: { id: String(userId) }, amount }; + } + + removeMoney(userId, amount) { + this.assertUserId(userId); + this.assertPositiveAmount(amount); + const before = this.fetchMoney(userId).balance; + if (before < amount) return { error: "Yetersiz bakiye." }; + const after = before - amount; + this.db.set(`money_${userId}`, after); + return { before, after, user: { id: String(userId) }, amount }; + } + + transfer(fromUserId, toUserId, amount) { + this.assertUserId(fromUserId); + this.assertUserId(toUserId); + this.assertPositiveAmount(amount); + + if (String(fromUserId) === String(toUserId)) return { error: "Kullanıcı kendisine para gönderemez." }; + + return this.db.transaction(() => { + const fromBalance = this.fetchMoney(fromUserId).balance; + if (fromBalance < amount) return { error: "Yetersiz bakiye." }; + + const toBalance = this.fetchMoney(toUserId).balance; + const newFromBalance = fromBalance - amount; + const newToBalance = toBalance + amount; + + if (!Number.isSafeInteger(newToBalance)) throw new RangeError("Hedef bakiyesi güvenli sayı sınırını aşıyor."); + + this.db.set(`money_${fromUserId}`, newFromBalance); + this.db.set(`money_${toUserId}`, newToBalance); + return { user1: { id: String(fromUserId) }, user2: { id: String(toUserId) }, amount }; + }); + } + + getCooldown(key, cooldownMs) { + const last = Number(this.db.fetch(key) || 0); + const remaining = cooldownMs - (Date.now() - last); + return last > 0 && remaining > 0 ? this.formatDuration(remaining) : null; + } + + daily(userId, amount) { + this.assertUserId(userId); + this.assertPositiveAmount(amount); + const key = `dailycooldown_${userId}`; + const cooldown = this.getCooldown(key, 86_400_000); + if (cooldown) return { onCooldown: true, time: cooldown, user: userId }; + + const result = this.addMoney(userId, amount); + this.db.set(key, Date.now()); + return { onCooldown: false, ...result, time: this.formatDuration(86_400_000) }; + } + + weekly(userId, amount) { + this.assertUserId(userId); + this.assertPositiveAmount(amount); + const key = `weeklycooldown_${userId}`; + const cooldown = this.getCooldown(key, 604_800_000); + if (cooldown) return { onCooldown: true, time: cooldown, user: userId }; + + const result = this.addMoney(userId, amount); + this.db.set(key, Date.now()); + return { onCooldown: false, ...result, time: this.formatDuration(604_800_000) }; + } + + work(userId, amount, options = {}) { + this.assertUserId(userId); + this.assertPositiveAmount(amount); + const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 2_700_000; + const key = `workcooldown_${userId}`; + const cooldown = this.getCooldown(key, cooldownMs); + if (cooldown) return { onCooldown: true, time: cooldown, user: { id: String(userId) } }; + + const jobs = Array.isArray(options.jobs) && options.jobs.length > 0 ? options.jobs : [ + "Geliştirici", "Doktor", "Öğretmen", "Müzisyen", "Madenci", "Mühendis", "Yayıncı", "Tasarımcı" + ]; + const result = this.addMoney(userId, amount); + this.db.set(key, Date.now()); + return { + onCooldown: false, + ...result, + workedAs: jobs[Math.floor(Math.random() * jobs.length)], + time: this.formatDuration(cooldownMs) + }; + } + + beg(userId, amount, options = {}) { + this.assertUserId(userId); + this.assertPositiveAmount(amount); + const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 60_000; + const customName = typeof options.customName === "string" && options.customName.trim() ? options.customName.trim() : "beg"; + const key = `${customName}cooldown_${userId}`; + const cooldown = this.getCooldown(key, cooldownMs); + if (cooldown) return { onCooldown: true, time: cooldown, user: { id: String(userId) } }; + + const lost = options.canLose === true && Math.floor(Math.random() * 5) === Math.floor(Math.random() * 5); + if (lost) { + const balance = this.fetchMoney(userId).balance; + this.db.set(key, Date.now()); + return { onCooldown: false, lost: true, before: balance, after: balance, user: { id: String(userId) }, amount, time: this.formatDuration(cooldownMs) }; + } + + const result = this.addMoney(userId, amount); + this.db.set(key, Date.now()); + return { onCooldown: false, lost: false, ...result, time: this.formatDuration(cooldownMs) }; + } + + leaderboard({ limit = 10 } = {}) { + let entries = this.db.all() + .filter(({ ID, data }) => /^money_\d{17,20}$/.test(ID) && Number.isSafeInteger(Number(data)) && Number(data) >= 0) + .map(({ ID, data }) => ({ id: ID.slice(6), money: Number(data) })) + .sort((a, b) => b.money - a.money); + + if (Number.isInteger(limit) && limit > 0) entries = entries.slice(0, limit); + return entries.map((entry, index) => ({ ...entry, position: index + 1 })); + } + + formatDuration(milliseconds) { + let seconds = Math.max(0, Math.ceil(milliseconds / 1000)); + const days = Math.floor(seconds / 86_400); + seconds %= 86_400; + const hours = Math.floor(seconds / 3_600); + seconds %= 3_600; + const minutes = Math.floor(seconds / 60); + seconds %= 60; + return { days, hours, minutes, seconds }; + } +} + +module.exports = EconomyManager; diff --git a/package.json b/package.json index 76019e7..d299e40 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,8 @@ "discord.js" ], "dependencies": { - "discord.js": "^14.27.0", - "quick.eco": "^2.0.3" - }, - "overrides": { - "better-sqlite3": "^12.4.1" + "better-sqlite3": "13.0.3", + "discord.js": "14.27.0" }, "engines": { "node": ">=20.0.0" diff --git a/slashCommands.js b/slashCommands.js index 921df89..36eee60 100644 --- a/slashCommands.js +++ b/slashCommands.js @@ -60,7 +60,7 @@ module.exports = [ .setName("setmoney") .setDescription("Bir kullanıcının bakiyesini ayarlar.") .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi ayarlanacak kullanıcı.").setRequired(true)) - .addIntegerOption((option) => integerAmount(option.setName("miktar"), "Yeni bakiye miktarı.", 0)), + .addIntegerOption((option) => integerAmount(option.setName("miktar"), "Yeni bakiye miktarı.")), new SlashCommandBuilder().setName("shop").setDescription("Mağazayı gösterir."), diff --git a/tests/database.js b/tests/database.js new file mode 100644 index 0000000..31e4b1d --- /dev/null +++ b/tests/database.js @@ -0,0 +1,64 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const Database = require("better-sqlite3"); +const KeyValueStore = require("../lib/database"); +const EconomyManager = require("../lib/economy"); + +const filePath = path.join(os.tmpdir(), `economybot-test-${process.pid}-${Date.now()}.sqlite`); +const userA = "12345678901234567"; +const userB = "12345678901234568"; + +try { + const legacy = new Database(filePath); + legacy.exec("CREATE TABLE json (ID TEXT NOT NULL, json TEXT NOT NULL)"); + legacy.prepare("INSERT INTO json (ID, json) VALUES (?, ?)").run(`money_${userA}`, JSON.stringify(1000)); + legacy.prepare("INSERT INTO json (ID, json) VALUES (?, ?)").run("prefix_legacyGuild", JSON.stringify("!")); + legacy.close(); + + const store = new KeyValueStore(filePath); + const economy = new EconomyManager(store); + + assert.equal(store.fetch(`money_${userA}`), 1000, "Legacy SQLite balance must remain readable."); + assert.equal(store.fetch("prefix_legacyGuild"), "!", "Legacy prefix data must remain readable."); + + const balance = economy.fetchMoney(userA); + assert.equal(balance.balance, 1000); + + const added = economy.addMoney(userA, 250); + assert.deepEqual({ before: added.before, after: added.after, amount: added.amount }, { before: 1000, after: 1250, amount: 250 }); + + const transfer = economy.transfer(userA, userB, 300); + assert.equal(transfer.amount, 300); + assert.equal(economy.fetchMoney(userA).balance, 950); + assert.equal(economy.fetchMoney(userB).balance, 300); + + const failedTransfer = economy.transfer(userA, userB, 999999); + assert.ok(failedTransfer.error, "Insufficient transfer must fail without modifying balances."); + assert.equal(economy.fetchMoney(userA).balance, 950); + assert.equal(economy.fetchMoney(userB).balance, 300); + + const set = economy.setMoney(userB, 0); + assert.equal(set.after, 0, "Setting a balance to zero must be supported."); + + const daily = economy.daily(userA, 100); + assert.equal(daily.onCooldown, false); + const dailyAgain = economy.daily(userA, 100); + assert.equal(dailyAgain.onCooldown, true); + + store.set("items_test", [{ name: "laptop" }]); + store.push("items_test", { name: "pc" }); + assert.deepEqual(store.get("items_test"), [{ name: "laptop" }, { name: "pc" }]); + assert.equal(store.delete("items_test"), true); + assert.equal(store.get("items_test"), null); + + store.close(); + console.log("Database test başarılı: legacy SQLite uyumluluğu, bakiye işlemleri, transfer atomikliği, cooldown ve key-value işlemleri doğrulandı."); +} finally { + for (const suffix of ["", "-wal", "-shm"]) { + try { + fs.rmSync(`${filePath}${suffix}`, { force: true }); + } catch {} + } +} diff --git a/tests/smoke.js b/tests/smoke.js index 322d515..ce5a957 100644 --- a/tests/smoke.js +++ b/tests/smoke.js @@ -3,6 +3,8 @@ const fs = require("node:fs"); const path = require("node:path"); const { Collection } = require("discord.js"); const slashCommands = require("../slashCommands"); +const interactionHandler = require("../events/interactionCreate"); +const messageHandler = require("../events/messageCreate"); const commandsDir = path.join(__dirname, "..", "commands"); const commandFiles = fs.readdirSync(commandsDir).filter((file) => file.endsWith(".js")); @@ -13,10 +15,10 @@ assert.equal(slashCommands.length, 17, "17 slash komutu tanımlı olmalı."); const names = new Set(); for (const { file, command } of commands) { - assert.ok(command && typeof command.execute === "function", `${file}: execute fonksiyonu eksik.`); - assert.ok(command.help && typeof command.help.name === "string", `${file}: help.name eksik.`); + assert.equal(typeof command.execute, "function", `${file}: execute fonksiyonu eksik.`); + assert.equal(typeof command.help?.name, "string", `${file}: help.name eksik.`); assert.ok(Array.isArray(command.help.aliases), `${file}: help.aliases dizi olmalı.`); - assert.ok(typeof command.help.usage === "string", `${file}: help.usage eksik.`); + assert.equal(typeof command.help.usage, "string", `${file}: help.usage eksik.`); assert.ok(!names.has(command.help.name), `${file}: aynı komut adı tekrar kullanılmış.`); names.add(command.help.name); } @@ -24,41 +26,241 @@ for (const { file, command } of commands) { const slashData = slashCommands.map((command) => command.toJSON()); const slashNames = slashData.map((command) => command.name); assert.equal(new Set(slashNames).size, slashNames.length, "Slash komut isimleri benzersiz olmalı."); +for (const name of slashNames) assert.ok(names.has(name), `/${name} için karşılık gelen prefix komutu bulunamadı.`); -for (const name of slashNames) { - assert.ok(names.has(name), `/${name} için karşılık gelen legacy komut bulunamadı.`); +class MockDB { + constructor() { + this.data = new Map(); + } + + fetch(key) { + return this.data.has(key) ? this.data.get(key) : null; + } + + get(key) { + return this.fetch(key); + } + + set(key, value) { + this.data.set(key, value); + return value; + } + + delete(key) { + this.data.delete(key); + return true; + } + + push(key, value) { + const items = Array.isArray(this.get(key)) ? this.get(key) : []; + items.push(value); + this.set(key, items); + return items; + } } -(async () => { - const help = require("../commands/help"); - const sent = []; +class MockEco { + constructor() { + this.balances = new Map(); + this.cooldowns = new Set(); + } + + fetchMoney(id) { + return { balance: this.balances.get(id) || 0, bank: 0, user: { id }, position: 1 }; + } + + addMoney(id, amount) { + const before = this.fetchMoney(id).balance; + const after = before + amount; + this.balances.set(id, after); + return { before, after, user: id, amount }; + } + + removeMoney(id, amount) { + const before = this.fetchMoney(id).balance; + if (before < amount) return { error: "New amount is negative." }; + const after = before - amount; + this.balances.set(id, after); + return { before, after, user: id, amount }; + } + + setMoney(id, amount) { + const before = this.fetchMoney(id).balance; + this.balances.set(id, amount); + return { before, after: amount, user: id, amount }; + } + + daily(id, amount) { + const key = `daily:${id}`; + if (this.cooldowns.has(key)) return { onCooldown: true, time: { hours: 1, minutes: 0, seconds: 0 } }; + this.cooldowns.add(key); + return { onCooldown: false, amount, after: this.addMoney(id, amount).after, time: { hours: 24, minutes: 0, seconds: 0 } }; + } + + weekly(id, amount) { + return { onCooldown: false, amount, after: this.addMoney(id, amount).after, time: { days: 7, hours: 0, minutes: 0, seconds: 0 } }; + } + + work(id, amount) { + return { onCooldown: false, amount, after: this.addMoney(id, amount).after, workedAs: "Developer", time: { minutes: 45, seconds: 0 } }; + } + + beg(id, amount) { + return { onCooldown: false, lost: false, amount, after: this.addMoney(id, amount).after, time: { seconds: 60 } }; + } + + transfer(from, to, amount) { + const balance = this.fetchMoney(from).balance; + if (balance < amount) return { error: "Money of first user is less than given amount." }; + this.balances.set(from, balance - amount); + this.balances.set(to, this.fetchMoney(to).balance + amount); + return { user1: { id: from }, user2: { id: to }, amount }; + } + + leaderboard({ limit = 10 } = {}) { + return [...this.balances.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, limit) + .map(([id, money], index) => ({ position: index + 1, id, money })); + } +} + +function makeClient() { const client = { - prefix: "!", + config: { admins: ["admin"], prefix: "!" }, commands: new Collection(), - user: { - displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" - } + aliases: new Collection(), + db: new MockDB(), + eco: new MockEco(), + shop: { + Laptop: { cost: 2000 }, + Mobile: { cost: 1000 }, + PC: { cost: 3000 } + }, + users: { cache: new Collection() }, + ws: { ping: 42 }, + user: { id: "bot", tag: "EconomyBot", displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" } }; for (const { command } of commands) client.commands.set(command.help.name, command); + for (const { command } of commands) for (const alias of command.help.aliases) client.aliases.set(alias, command.help.name); + + client.users.cache.set("target", { id: "target", tag: "Target#0001", bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }); + client.users.cache.set("recipient", { id: "recipient", tag: "Recipient#0001", bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }); + client.users.cache.set("admin", { id: "admin", tag: "Admin#0001", bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }); + return client; +} + +function makeMessage(client, { authorId = "admin", targetId = "target", args = [], prefix = "!" } = {}) { + const sent = []; + const target = client.users.cache.get(targetId); + const targetMember = { id: target?.id, user: target, displayAvatarURL: target?.displayAvatarURL, bot: target?.bot }; + const guild = { + id: "guild", + name: "Smoke Guild", + iconURL: () => null, + members: { cache: new Collection([[target?.id, targetMember]]) } + }; - const message = { + const makeSentMessage = () => ({ + createdTimestamp: Date.now(), + edit: async (payload) => { + sent.push({ edited: true, payload }); + return makeSentMessage(); + } + }); + + const send = async (payload) => { + sent.push(payload); + return makeSentMessage(); + }; + + return { + sent, author: { - tag: "SmokeTest#0000", + id: authorId, + tag: `${authorId}#0001`, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }, - channel: { - send: async (payload) => { - sent.push(payload); - return payload; - } - } + member: { id: authorId, permissions: { has: () => true } }, + guild, + channel: { id: "channel", send }, + reply: send, + prefix, + createdTimestamp: Date.now(), + mentions: { + users: { first: () => target || null }, + members: { first: () => targetMember || null } + }, + content: `${prefix}${args.join(" ")}` + }; +} + +(async () => { + const client = makeClient(); + client.eco.setMoney("admin", 10000); + client.eco.setMoney("target", 5000); + + const cases = [ + ["addmoney", ["target", "500"], "admin"], + ["bal", [], "admin"], + ["beg", [], "admin"], + ["buy", ["Laptop"], "admin"], + ["daily", [], "admin"], + ["help", [], "admin"], + ["inventory", [], "admin"], + ["lb", [], "admin"], + ["ping", [], "admin"], + ["prefix", ["$"], "admin"], + ["rob", ["target"], "admin"], + ["search", [], "admin"], + ["setmoney", ["target", "2500"], "admin"], + ["shop", [], "admin"], + ["transfer", ["target", "100"], "admin"], + ["weekly", [], "admin"], + ["work", [], "admin"] + ]; + + for (const [name, args, authorId] of cases) { + const message = makeMessage(client, { authorId, args }); + await client.commands.get(name).execute(client, message, args); + assert.ok(message.sent.length > 0, `${name}: görünür yanıt üretmedi.`); + } + + const prefixMessage = makeMessage(client, { authorId: "admin" }); + prefixMessage.content = "$bakiye"; + await messageHandler(client, prefixMessage); + assert.ok(prefixMessage.sent.length > 0, "Prefix aliası çalışmadı."); + + let deferred = false; + let replied = false; + const interactionEdits = []; + const interaction = { + id: "interaction", + commandName: "bal", + channelId: "channel", + createdTimestamp: Date.now(), + guild: prefixMessage.guild, + user: client.users.cache.get("admin"), + member: prefixMessage.member, + isChatInputCommand: () => true, + options: { + getUser: () => null, + getMember: () => null, + getInteger: () => null, + getString: () => null + }, + deferReply: async () => { deferred = true; }, + editReply: async (payload) => { replied = true; interactionEdits.push(payload); return { createdTimestamp: Date.now(), edit: async (next) => { interactionEdits.push(next); } }; }, + followUp: async (payload) => { replied = true; interactionEdits.push(payload); return payload; }, + get deferred() { return deferred; }, + get replied() { return replied; }, + reply: async (payload) => { replied = true; interactionEdits.push(payload); } }; - await help.execute(client, message); - assert.equal(sent.length, 1, "help komutu tam olarak bir yanıt göndermeli."); - assert.equal(sent[0].embeds.length, 1, "help komutu bir embed göndermeli."); - assert.equal(sent[0].embeds[0].data.url, "https://github.com/LoiFragola/EconomyBot"); + await interactionHandler(client, interaction); + assert.equal(deferred, true, "Slash command deferReply çalışmadı."); + assert.ok(interactionEdits.length > 0, "/bal slash komutu görünür yanıt üretmedi."); - console.log(`Smoke test başarılı: ${commands.length} komut ve ${slashCommands.length} slash komutu doğrulandı.`); + console.log(`Smoke test başarılı: ${commands.length} komut, ${slashCommands.length} slash komutu, prefix aliası ve slash yürütme akışı doğrulandı.`); })(); From 603fb3f24419e2a16efa76ecba83e29a0ef1cefc Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:17:01 +0300 Subject: [PATCH 079/175] Rebuild economy storage layer --- database.js | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 database.js diff --git a/database.js b/database.js new file mode 100644 index 0000000..9a6c425 --- /dev/null +++ b/database.js @@ -0,0 +1,252 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const Database = require("better-sqlite3"); + +const dataDir = path.join(__dirname, "data"); +fs.mkdirSync(dataDir, { recursive: true }); + +const db = new Database(path.join(dataDir, "economy.sqlite")); +db.pragma("journal_mode = WAL"); +db.pragma("foreign_keys = ON"); +db.pragma("synchronous = NORMAL"); + +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + balance INTEGER NOT NULL DEFAULT 0 CHECK(balance >= 0), + PRIMARY KEY (guild_id, user_id) + ); + + CREATE TABLE IF NOT EXISTS cooldowns ( + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + command TEXT NOT NULL, + expires_at INTEGER NOT NULL, + PRIMARY KEY (guild_id, user_id, command) + ); + + CREATE TABLE IF NOT EXISTS inventory ( + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + item_id TEXT NOT NULL, + item_name TEXT NOT NULL, + unit_price INTEGER NOT NULL CHECK(unit_price >= 0), + quantity INTEGER NOT NULL DEFAULT 0 CHECK(quantity >= 0), + PRIMARY KEY (guild_id, user_id, item_id), + FOREIGN KEY (guild_id, user_id) REFERENCES users(guild_id, user_id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS guild_settings ( + guild_id TEXT PRIMARY KEY, + prefix TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS counters ( + guild_id TEXT PRIMARY KEY, + number INTEGER NOT NULL DEFAULT 0 CHECK(number >= 0), + last_user_id TEXT + ); +`); + +const statements = { + ensureUser: db.prepare(` + INSERT INTO users (guild_id, user_id, balance) + VALUES (@guildId, @userId, 0) + ON CONFLICT(guild_id, user_id) DO NOTHING + `), + getBalance: db.prepare(` + SELECT balance FROM users WHERE guild_id = ? AND user_id = ? + `), + setBalance: db.prepare(` + UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ? + `), + leaderboard: db.prepare(` + SELECT user_id, balance + FROM users + WHERE guild_id = ? + ORDER BY balance DESC, user_id ASC + LIMIT ? + `), + countAhead: db.prepare(` + SELECT COUNT(*) AS count + FROM users + WHERE guild_id = ? AND balance > ? + `), + getCooldown: db.prepare(` + SELECT expires_at FROM cooldowns + WHERE guild_id = ? AND user_id = ? AND command = ? + `), + setCooldown: db.prepare(` + INSERT INTO cooldowns (guild_id, user_id, command, expires_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(guild_id, user_id, command) + DO UPDATE SET expires_at = excluded.expires_at + `), + getInventory: db.prepare(` + SELECT item_id, item_name, unit_price, quantity + FROM inventory + WHERE guild_id = ? AND user_id = ? + ORDER BY item_name COLLATE NOCASE + `), + getItem: db.prepare(` + SELECT item_id, item_name, unit_price, quantity + FROM inventory + WHERE guild_id = ? AND user_id = ? AND item_id = ? + `), + upsertInventory: db.prepare(` + INSERT INTO inventory (guild_id, user_id, item_id, item_name, unit_price, quantity) + VALUES (@guildId, @userId, @itemId, @itemName, @unitPrice, @quantity) + ON CONFLICT(guild_id, user_id, item_id) + DO UPDATE SET quantity = inventory.quantity + excluded.quantity, + item_name = excluded.item_name, + unit_price = excluded.unit_price + `), + getPrefix: db.prepare(`SELECT prefix FROM guild_settings WHERE guild_id = ?`), + setPrefix: db.prepare(` + INSERT INTO guild_settings (guild_id, prefix) VALUES (?, ?) + ON CONFLICT(guild_id) DO UPDATE SET prefix = excluded.prefix + `), + resetPrefix: db.prepare(`DELETE FROM guild_settings WHERE guild_id = ?`), + getCounter: db.prepare(`SELECT number, last_user_id FROM counters WHERE guild_id = ?`), + setCounter: db.prepare(` + INSERT INTO counters (guild_id, number, last_user_id) VALUES (?, ?, ?) + ON CONFLICT(guild_id) DO UPDATE SET number = excluded.number, last_user_id = excluded.last_user_id + `) +}; + +function ensureUser(guildId, userId) { + statements.ensureUser.run({ guildId: String(guildId), userId: String(userId) }); +} + +function getBalance(guildId, userId) { + ensureUser(guildId, userId); + return Number(statements.getBalance.get(String(guildId), String(userId)).balance); +} + +function setBalance(guildId, userId, amount) { + const value = Number(amount); + if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("Bakiye geçerli bir pozitif tam sayı olmalıdır."); + ensureUser(guildId, userId); + statements.setBalance.run(value, String(guildId), String(userId)); + return value; +} + +function addBalance(guildId, userId, amount) { + const value = Number(amount); + if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError("Miktar pozitif bir tam sayı olmalıdır."); + const next = getBalance(guildId, userId) + value; + if (!Number.isSafeInteger(next)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); + setBalance(guildId, userId, next); + return next; +} + +function removeBalance(guildId, userId, amount) { + const value = Number(amount); + if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError("Miktar pozitif bir tam sayı olmalıdır."); + const current = getBalance(guildId, userId); + if (current < value) return false; + setBalance(guildId, userId, current - value); + return true; +} + +const transferTransaction = db.transaction((guildId, fromUserId, toUserId, amount) => { + ensureUser(guildId, fromUserId); + ensureUser(guildId, toUserId); + const from = Number(statements.getBalance.get(guildId, fromUserId).balance); + if (from < amount) return { ok: false, fromBalance: from, toBalance: Number(statements.getBalance.get(guildId, toUserId).balance) }; + const to = Number(statements.getBalance.get(guildId, toUserId).balance); + if (!Number.isSafeInteger(to + amount)) throw new RangeError("Hedef bakiye güvenli sayı sınırını aşıyor."); + statements.setBalance.run(from - amount, guildId, fromUserId); + statements.setBalance.run(to + amount, guildId, toUserId); + return { ok: true, fromBalance: from - amount, toBalance: to + amount }; +}); + +function transfer(guildId, fromUserId, toUserId, amount) { + const value = Number(amount); + if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError("Miktar pozitif bir tam sayı olmalıdır."); + if (String(fromUserId) === String(toUserId)) return { ok: false, reason: "self" }; + return transferTransaction(String(guildId), String(fromUserId), String(toUserId), value); +} + +function claimCooldown(guildId, userId, command, durationMs, now = Date.now()) { + const row = statements.getCooldown.get(String(guildId), String(userId), String(command)); + const expiresAt = row ? Number(row.expires_at) : 0; + if (expiresAt > now) return { onCooldown: true, remainingMs: expiresAt - now }; + const next = now + durationMs; + statements.setCooldown.run(String(guildId), String(userId), String(command), next); + return { onCooldown: false, remainingMs: 0 }; +} + +function getLeaderboard(guildId, limit = 15) { + const rows = statements.leaderboard.all(String(guildId), limit); + return rows.map((row, index) => ({ position: index + 1, userId: row.user_id, balance: Number(row.balance) })); +} + +function getPosition(guildId, userId) { + const balance = getBalance(guildId, userId); + return Number(statements.countAhead.get(String(guildId), balance).count) + 1; +} + +function addItem(guildId, userId, item) { + ensureUser(guildId, userId); + statements.upsertInventory.run({ + guildId: String(guildId), + userId: String(userId), + itemId: String(item.id), + itemName: String(item.name), + unitPrice: Number(item.price), + quantity: 1 + }); +} + +function getInventory(guildId, userId) { + ensureUser(guildId, userId); + return statements.getInventory.all(String(guildId), String(userId)); +} + +function getInventoryItem(guildId, userId, itemId) { + return statements.getItem.get(String(guildId), String(userId), String(itemId)) || null; +} + +function getPrefix(guildId, defaultPrefix) { + return statements.getPrefix.get(String(guildId))?.prefix || defaultPrefix; +} + +function setPrefix(guildId, prefix) { + statements.setPrefix.run(String(guildId), String(prefix)); + return String(prefix); +} + +function resetPrefix(guildId) { + statements.resetPrefix.run(String(guildId)); +} + +function getCounter(guildId) { + return statements.getCounter.get(String(guildId)) || { number: 0, last_user_id: null }; +} + +function setCounter(guildId, number, lastUserId) { + statements.setCounter.run(String(guildId), Number(number), lastUserId ? String(lastUserId) : null); +} + +module.exports = { + db, + ensureUser, + getBalance, + setBalance, + addBalance, + removeBalance, + transfer, + claimCooldown, + getLeaderboard, + getPosition, + addItem, + getInventory, + getInventoryItem, + getPrefix, + setPrefix, + resetPrefix, + getCounter, + setCounter +}; From 79b14957b4c4823e5917aa7a63fa9b6dd7076e2e Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:17:07 +0300 Subject: [PATCH 080/175] Add unified command context --- lib/context.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 lib/context.js diff --git a/lib/context.js b/lib/context.js new file mode 100644 index 0000000..889b204 --- /dev/null +++ b/lib/context.js @@ -0,0 +1,56 @@ +function createInteractionContext(interaction, client) { + return { + client, + interaction, + isSlash: true, + guild: interaction.guild, + guildId: interaction.guildId, + user: interaction.user, + userId: interaction.user.id, + member: interaction.member, + args: [], + option(name) { + return interaction.options.get(name)?.value ?? null; + }, + userOption(name) { + return interaction.options.getUser(name); + }, + async reply(payload) { + return interaction.reply(payload); + } + }; +} + +function createPrefixContext(message, client, args) { + return { + client, + message, + interaction: null, + isSlash: false, + guild: message.guild, + guildId: message.guild.id, + user: message.author, + userId: message.author.id, + member: message.member, + args, + option() { + return null; + }, + userOption() { + return null; + }, + async reply(payload) { + return message.reply(payload); + } + }; +} + +async function replyContext(ctx, payload) { + return ctx.reply(payload); +} + +module.exports = { + createInteractionContext, + createPrefixContext, + replyContext +}; From abf70713d85d45b2d4940a0dbcbec55e9572dbff Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:17:39 +0300 Subject: [PATCH 081/175] Replace command/event bootstrap with explicit v14 handlers --- index.js | 91 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/index.js b/index.js index 36166fa..5465491 100644 --- a/index.js +++ b/index.js @@ -1,67 +1,82 @@ -const { Client, Collection, GatewayIntentBits } = require("discord.js"); -const fs = require("node:fs"); const path = require("node:path"); -const KeyValueStore = require("./lib/database"); +const fs = require("node:fs"); +const { Client, Collection, GatewayIntentBits } = require("discord.js"); +const config = require("./botConfig"); +const database = require("./lib/database"); const EconomyManager = require("./lib/economy"); +if (!config || typeof config !== "object") throw new Error("botConfig.js geçerli bir yapılandırma döndürmelidir."); +if (!config.token || config.token === "YOUR_TOKEN") throw new Error("botConfig.js içindeki token ayarlanmalı."); +if (!config.serverId || !/^\d{17,20}$/.test(String(config.serverId))) throw new Error("botConfig.js içindeki serverId geçerli bir Discord sunucu ID'si olmalı."); +if (!config.prefix || typeof config.prefix !== "string" || /\s/.test(config.prefix)) throw new Error("botConfig.js içindeki prefix geçerli olmalı."); +if (!Array.isArray(config.admins)) throw new Error("botConfig.js içindeki admins bir dizi olmalı."); + const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], - allowedMentions: { parse: ["users", "roles"], repliedUser: false } + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent + ] }); -client.config = require("./botConfig"); -client.prefix = String(client.config.prefix || "!"); -client.db = new KeyValueStore(); -client.eco = new EconomyManager(client.db); +client.config = config; +client.db = database; +client.eco = new EconomyManager(database); client.commands = new Collection(); client.aliases = new Collection(); - client.shop = Object.freeze({ - Laptop: { cost: 2000 }, - Mobile: { cost: 1000 }, - PC: { cost: 3000 } + laptop: { id: "laptop", name: "Laptop", cost: 2000 }, + mobile: { id: "mobile", name: "Mobile", cost: 1000 }, + pc: { id: "pc", name: "PC", cost: 3000 } }); -const eventsPath = path.join(__dirname, "events"); -for (const file of fs.readdirSync(eventsPath).filter((entry) => entry.endsWith(".js"))) { - const eventName = path.basename(file, ".js"); - const event = require(path.join(eventsPath, file)); - if (typeof event !== "function") throw new TypeError(`Event '${file}' must export a function.`); - client.on(eventName, event.bind(null, client)); -} - const commandsPath = path.join(__dirname, "commands"); -for (const file of fs.readdirSync(commandsPath).filter((entry) => entry.endsWith(".js"))) { +const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js")).sort(); + +for (const file of commandFiles) { const command = require(path.join(commandsPath, file)); - if (!command || typeof command.execute !== "function" || !command.help?.name) { - throw new TypeError(`Invalid command module: ${file}`); + const name = String(command?.help?.name || "").toLowerCase(); + if (!name || typeof command.execute !== "function" || !command.data) { + throw new TypeError(`commands/${file}: data, help.name veya execute eksik.`); } - - const name = command.help.name.toLowerCase(); + if (client.commands.has(name)) throw new Error(`Yinelenen komut adı: ${name}`); client.commands.set(name, command); + for (const alias of command.help.aliases || []) { - client.aliases.set(String(alias).toLowerCase(), name); + const normalized = String(alias).toLowerCase(); + if (!normalized || client.commands.has(normalized) || client.aliases.has(normalized)) { + throw new Error(`Geçersiz veya çakışan takma ad: ${alias}`); + } + client.aliases.set(normalized, name); } } -client.on("error", (error) => console.error("Discord client error:", error)); -client.on("warn", (warning) => console.warn("Discord warning:", warning)); +if (client.commands.size !== 17) { + throw new Error(`17 komut bekleniyordu, ${client.commands.size} komut yüklendi.`); +} + +client.once("clientReady", require("./events/clientReady" ).bind(null, client)); +client.on("interactionCreate", require("./events/interactionCreate").bind(null, client)); +client.on("messageCreate", require("./events/messageCreate").bind(null, client)); +client.on("error", (error) => console.error("Discord client hatası:", error)); +client.on("warn", (warning) => console.warn("Discord uyarısı:", warning)); -process.on("unhandledRejection", (error) => console.error("Unhandled promise rejection:", error)); -process.on("uncaughtException", (error) => console.error("Uncaught exception:", error)); +process.on("unhandledRejection", (error) => console.error("Yakalanmamış Promise hatası:", error)); +process.on("uncaughtException", (error) => console.error("Yakalanmamış uygulama hatası:", error)); const shutdown = () => { - client.db.close(); + try { + client.destroy(); + } finally { + database.close(); + } }; process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); -if (!client.config.token || client.config.token === "YOUR_TOKEN") { - throw new Error("botConfig.js içindeki token ayarlanmalı."); -} - -client.login(client.config.token).catch((error) => { +client.login(config.token).catch((error) => { console.error("Discord'a giriş yapılamadı:", error); - client.db.close(); + database.close(); process.exitCode = 1; }); From ba4b549cff02268eb4ffc3ff9aff78dc1ffd557d Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:17:44 +0300 Subject: [PATCH 082/175] Use native slash command execution --- events/interactionCreate.js | 80 +++++-------------------------------- 1 file changed, 11 insertions(+), 69 deletions(-) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index d670355..3bb65d0 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -1,85 +1,27 @@ +const { createInteractionContext } = require("../lib/context"); + module.exports = async (client, interaction) => { - if (!interaction.isChatInputCommand() || !interaction.guild) return; + if (!interaction.isChatInputCommand() || !interaction.inGuild()) return; const command = client.commands.get(interaction.commandName); if (!command) { - const payload = { content: "Bu komut artık mevcut değil. Botu yeniden başlatıp slash komutlarını güncelleyin." }; - if (interaction.deferred || interaction.replied) { - return interaction.editReply(payload).catch(() => {}); - } - return interaction.reply({ ...payload, ephemeral: true }).catch(() => {}); + console.error(`Kayıtlı olmayan slash komutu alındı: /${interaction.commandName}`); + return interaction.reply({ content: "Bu komut şu anda kullanılamıyor.", ephemeral: true }).catch(() => {}); } - const userOption = interaction.options.getUser("kullanici"); - const memberOption = interaction.options.getMember("kullanici"); - const amount = interaction.options.getInteger("miktar"); - const product = interaction.options.getString("urun"); - const newPrefix = interaction.options.getString("yeni_prefix"); - - const args = []; - if (userOption) args.push(userOption.id); - if (product) args.push(product); - if (newPrefix) args.push(newPrefix); - if (amount !== null && amount !== undefined) args.push(String(amount)); - - const storedPrefix = client.db.fetch(`prefix_${interaction.guild.id}`); - const guildPrefix = String(storedPrefix || client.config.prefix || "!"); - - const normalizePayload = (payload) => { - if (payload === undefined || payload === null) return { content: "Komut tamamlandı." }; - return typeof payload === "string" ? { content: payload } : payload; - }; - - await interaction.deferReply(); - - let responded = false; - const respond = async (payload) => { - const normalized = normalizePayload(payload); - responded = true; - - if (interaction.replied) return interaction.followUp(normalized); - return interaction.editReply(normalized); - }; - - const fakeMessage = { - id: interaction.id, - author: interaction.user, - member: interaction.member, - guild: interaction.guild, - channel: { - id: interaction.channelId, - send: respond - }, - createdTimestamp: interaction.createdTimestamp, - content: `/${interaction.commandName}`, - prefix: guildPrefix, - mentions: { - users: { - first: () => userOption || null - }, - members: { - first: () => memberOption || null - } - }, - reply: respond - }; + const context = createInteractionContext(interaction, client); try { - const result = await command.execute(client, fakeMessage, args); - - if (!responded && result !== undefined && result !== null) { - await respond(result); - } - - if (!responded) { - await interaction.editReply({ content: "Komut tamamlandı ancak görünür bir yanıt oluşturmadı." }); + await command.execute(context); + if (!interaction.replied && !interaction.deferred) { + throw new Error(`/${interaction.commandName} hiçbir yanıt göndermedi.`); } } catch (error) { console.error(`/${interaction.commandName} komutunda hata:`, error); - const payload = { content: "Komut çalıştırılırken beklenmeyen bir hata oluştu. Lütfen tekrar deneyin." }; + const payload = { content: "Komut çalıştırılırken beklenmeyen bir hata oluştu. Konsolu kontrol edin." }; if (interaction.replied || interaction.deferred) { - await interaction.editReply(payload).catch(() => {}); + await interaction.followUp(payload).catch(() => {}); } else { await interaction.reply({ ...payload, ephemeral: true }).catch(() => {}); } From 2b178bd6bf23686c9e5cf5fb28ea30a614bf7f65 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:17:53 +0300 Subject: [PATCH 083/175] Use unified prefix command context --- events/messageCreate.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/events/messageCreate.js b/events/messageCreate.js index 0254d10..301b566 100644 --- a/events/messageCreate.js +++ b/events/messageCreate.js @@ -1,14 +1,17 @@ +const { createPrefixContext } = require("../lib/context"); + module.exports = async (client, message) => { - if (!message.guild || message.author.bot) return; + if (!message.inGuild() || message.author.bot) return; if (message.channel.id === client.config.countChannel) { - require("../counter")(message, client); + try { + await require("../counter")(message, client); + } catch (error) { + console.error("Sayaç sisteminde hata:", error); + } } - const storedPrefix = client.db.fetch(`prefix_${message.guild.id}`); - const prefix = String(storedPrefix || client.config.prefix || "!"); - message.prefix = prefix; - + const prefix = client.db.getPrefix(message.guild.id, client.config.prefix); if (!message.content.startsWith(prefix)) return; const content = message.content.slice(prefix.length).trim(); @@ -16,14 +19,17 @@ module.exports = async (client, message) => { const args = content.split(/\s+/); const commandName = args.shift().toLowerCase(); - const canonicalName = client.aliases.get(commandName) || commandName; + const canonicalName = client.commands.has(commandName) ? commandName : client.aliases.get(commandName); + if (!canonicalName) return; + const command = client.commands.get(canonicalName); - if (!command) return; + const context = createPrefixContext(message, client, args); + context.prefix = prefix; try { - await command.execute(client, message, args); + await command.execute(context); } catch (error) { console.error(`Prefix komutunda hata (${prefix}${commandName}):`, error); - await message.reply("Komut çalıştırılırken beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.").catch(() => {}); + await message.reply("Komut çalıştırılırken beklenmeyen bir hata oluştu. Konsolu kontrol edin.").catch(() => {}); } }; From 7a3128466f680c63391d19ccda9d0bf71b6318de Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:17:59 +0300 Subject: [PATCH 084/175] Add command validation helpers --- lib/commandUtils.js | 84 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 lib/commandUtils.js diff --git a/lib/commandUtils.js b/lib/commandUtils.js new file mode 100644 index 0000000..5dc78a2 --- /dev/null +++ b/lib/commandUtils.js @@ -0,0 +1,84 @@ +const { PermissionsBitField } = require("discord.js"); + +function isAdmin(ctx) { + if (ctx.client.config.admins.includes(ctx.userId)) return true; + if (ctx.member?.permissions?.has(PermissionsBitField.Flags.ManageGuild)) return true; + return false; +} + +function parsePositiveInteger(value) { + const text = String(value ?? "").trim(); + if (!/^\d+$/.test(text)) return null; + const number = Number(text); + return Number.isSafeInteger(number) && number > 0 ? number : null; +} + +function parseNonNegativeInteger(value) { + const text = String(value ?? "").trim(); + if (!/^\d+$/.test(text)) return null; + const number = Number(text); + return Number.isSafeInteger(number) && number >= 0 ? number : null; +} + +async function resolveUser(ctx, argumentIndex = 0) { + if (ctx.isSlash) return ctx.userOption("kullanici"); + const mentioned = ctx.message.mentions.users.first(); + if (mentioned) return mentioned; + const id = ctx.args[argumentIndex]; + if (!/^\d{17,20}$/.test(String(id || ""))) return null; + return ctx.client.users.fetch(id).catch(() => null); +} + +async function resolveMember(ctx, argumentIndex = 0) { + if (ctx.isSlash) return ctx.interaction.options.getMember("kullanici") || null; + const mentioned = ctx.message.mentions.members.first(); + if (mentioned) return mentioned; + const id = ctx.args[argumentIndex]; + if (!/^\d{17,20}$/.test(String(id || ""))) return null; + return ctx.guild.members.fetch(id).catch(() => null); +} + +function getAmount(ctx, optionName = "miktar", argumentIndex = 1) { + return ctx.isSlash ? ctx.interaction.options.getInteger(optionName) : parsePositiveInteger(ctx.args[argumentIndex]); +} + +function getProduct(ctx) { + return ctx.isSlash ? ctx.interaction.options.getString("urun") : ctx.args[0] || null; +} + +function formatMoney(value) { + return `${Number(value).toLocaleString("tr-TR")} 💸`; +} + +function formatRemaining(ms) { + let seconds = Math.max(0, Math.ceil(ms / 1000)); + const days = Math.floor(seconds / 86400); + seconds %= 86400; + const hours = Math.floor(seconds / 3600); + seconds %= 3600; + const minutes = Math.floor(seconds / 60); + seconds %= 60; + const parts = []; + if (days) parts.push(`${days} gün`); + if (hours) parts.push(`${hours} saat`); + if (minutes) parts.push(`${minutes} dakika`); + if (seconds || parts.length === 0) parts.push(`${seconds} saniye`); + return parts.join(", "); +} + +function randomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +module.exports = { + isAdmin, + parsePositiveInteger, + parseNonNegativeInteger, + resolveUser, + resolveMember, + getAmount, + getProduct, + formatMoney, + formatRemaining, + randomInt +}; From b219b4e2c35737e8ef4e5c66b1071ba868ade327 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:18:10 +0300 Subject: [PATCH 085/175] Harden SQLite key-value database --- lib/database.js | 79 ++++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/lib/database.js b/lib/database.js index 35131b2..cc65445 100644 --- a/lib/database.js +++ b/lib/database.js @@ -1,66 +1,72 @@ const Database = require("better-sqlite3"); +const fs = require("node:fs"); const path = require("node:path"); +const dataDir = path.join(process.cwd(), "data"); +fs.mkdirSync(dataDir, { recursive: true }); + class KeyValueStore { - constructor(filePath = path.join(process.cwd(), "economy.sqlite")) { + constructor(filePath = path.join(dataDir, "economy.sqlite")) { this.connection = new Database(filePath); this.connection.pragma("journal_mode = WAL"); this.connection.pragma("foreign_keys = ON"); + this.connection.pragma("synchronous = NORMAL"); + this.connection.exec(` - CREATE TABLE IF NOT EXISTS json ( - ID TEXT NOT NULL, - json TEXT NOT NULL - ) + CREATE TABLE IF NOT EXISTS kv ( + id TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_kv_id ON kv(id); `); this.statements = { - get: this.connection.prepare("SELECT json FROM json WHERE ID = ? LIMIT 1"), - update: this.connection.prepare("UPDATE json SET json = ? WHERE ID = ?"), - insert: this.connection.prepare("INSERT INTO json (ID, json) VALUES (?, ?)"), - delete: this.connection.prepare("DELETE FROM json WHERE ID = ?"), - all: this.connection.prepare("SELECT ID, json FROM json"), - clear: this.connection.prepare("DELETE FROM json") + get: this.connection.prepare("SELECT value FROM kv WHERE id = ?"), + set: this.connection.prepare(` + INSERT INTO kv (id, value) VALUES (?, ?) + ON CONFLICT(id) DO UPDATE SET value = excluded.value + `), + delete: this.connection.prepare("DELETE FROM kv WHERE id = ?"), + all: this.connection.prepare("SELECT id, value FROM kv ORDER BY id"), + prefix: this.connection.prepare("SELECT id, value FROM kv WHERE id LIKE ? ORDER BY id"), + clear: this.connection.prepare("DELETE FROM kv") }; } encode(value) { const encoded = JSON.stringify(value); - if (encoded === undefined) throw new TypeError("Database value must be JSON serializable."); + if (encoded === undefined) throw new TypeError("Veritabanı değeri JSON olarak saklanamıyor."); return encoded; } - decode(raw) { - if (raw === null || raw === undefined) return null; - return JSON.parse(raw); + decode(value) { + return JSON.parse(value); } - get(key) { + get(key, fallback = null) { const row = this.statements.get.get(String(key)); - return row ? this.decode(row.json) : null; + return row ? this.decode(row.value) : fallback; } - fetch(key) { - return this.get(key); + fetch(key, fallback = null) { + return this.get(key, fallback); } has(key) { - return this.statements.get.get(String(key)) !== undefined; + return Boolean(this.statements.get.get(String(key))); } set(key, value) { - const normalizedKey = String(key); - const encoded = this.encode(value); - const update = this.statements.update.run(encoded, normalizedKey); - if (update.changes === 0) this.statements.insert.run(normalizedKey, encoded); + this.statements.set.run(String(key), this.encode(value)); return value; } push(key, value) { - const current = this.get(key); - const array = Array.isArray(current) ? current : []; - array.push(value); - this.set(key, array); - return array; + const current = this.get(key, []); + if (!Array.isArray(current)) throw new TypeError(`'${key}' mevcut bir dizi değil.`); + current.push(value); + this.set(key, current); + return current; } delete(key) { @@ -68,10 +74,14 @@ class KeyValueStore { } all() { - return this.statements.all.all().map(({ ID, json }) => ({ - ID, - data: this.decode(json) - })); + return this.statements.all.all().map(({ id, value }) => ({ ID: id, data: this.decode(value) })); + } + + startsWith(prefix) { + const escaped = String(prefix).replace(/[\\%_]/g, "\\$&"); + return this.statements.prefix + .all(`${escaped}%`) + .map(({ id, value }) => ({ ID: id, data: this.decode(value) })); } clear() { @@ -87,4 +97,5 @@ class KeyValueStore { } } -module.exports = KeyValueStore; +module.exports = new KeyValueStore(); +module.exports.KeyValueStore = KeyValueStore; From aa2877fdf902dda106612ca1fd5337d52e2535ea Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:18:23 +0300 Subject: [PATCH 086/175] Rebuild economy service with guild-scoped balances --- lib/economy.js | 241 +++++++++++++++++++++---------------------------- 1 file changed, 104 insertions(+), 137 deletions(-) diff --git a/lib/economy.js b/lib/economy.js index 9fc0699..b31ee7c 100644 --- a/lib/economy.js +++ b/lib/economy.js @@ -1,180 +1,147 @@ -const KeyValueStore = require("./database"); +const store = require("./database"); + +const DAY = 86_400_000; +const WEEK = 7 * DAY; class EconomyManager { - constructor(store) { - if (!(store instanceof KeyValueStore) && (!store || typeof store.fetch !== "function" || typeof store.set !== "function")) { - throw new TypeError("Geçerli bir veritabanı sağlayıcısı gerekli."); - } - this.db = store; + constructor(database = store) { + this.db = database; } - assertUserId(userId) { - if (!/^\d{17,20}$/.test(String(userId))) throw new TypeError("Geçersiz Discord kullanıcı ID'si."); + assertId(value, label = "ID") { + if (!/^\d{17,20}$/.test(String(value))) throw new TypeError(`Geçersiz Discord ${label}.`); } - assertPositiveAmount(amount, allowZero = false) { - if (!Number.isSafeInteger(amount) || (allowZero ? amount < 0 : amount <= 0)) { - throw new TypeError("Miktar güvenli bir tam sayı olmalı ve geçerli aralıkta bulunmalı."); + assertAmount(value, allowZero = false) { + if (!Number.isSafeInteger(value) || (allowZero ? value < 0 : value <= 0)) { + throw new TypeError("Miktar güvenli bir tam sayı olmalı."); } } - fetchMoney(userId) { - this.assertUserId(userId); - const id = String(userId); - const stored = Number(this.db.fetch(`money_${id}`)); + key(guildId, userId) { + this.assertId(guildId, "sunucu ID'si"); + this.assertId(userId, "kullanıcı ID'si"); + return `money:${guildId}:${userId}`; + } + + cooldownKey(guildId, userId, command) { + return `cooldown:${guildId}:${userId}:${command}`; + } + + fetchMoney(guildId, userId) { + const key = this.key(guildId, userId); + const stored = Number(this.db.get(key, 0)); const balance = Number.isSafeInteger(stored) && stored >= 0 ? stored : 0; - const position = this.leaderboard({ limit: 0 }).findIndex((entry) => entry.id === id); + return { user: { id: String(userId) }, balance }; + } - return { - balance, - bank: 0, - user: { id }, - position: position === -1 ? null : position + 1 - }; + getBalance(guildId, userId) { + return this.fetchMoney(guildId, userId).balance; } - addMoney(userId, amount) { - this.assertUserId(userId); - this.assertPositiveAmount(amount); - const before = this.fetchMoney(userId).balance; + addMoney(guildId, userId, amount) { + this.assertAmount(amount); + const before = this.getBalance(guildId, userId); const after = before + amount; if (!Number.isSafeInteger(after)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); - this.db.set(`money_${userId}`, after); - return { before, after, user: { id: String(userId) }, amount }; + this.db.set(this.key(guildId, userId), after); + return { user: { id: String(userId) }, amount, before, after }; } - setMoney(userId, amount) { - this.assertUserId(userId); - this.assertPositiveAmount(amount, true); - const before = this.fetchMoney(userId).balance; - this.db.set(`money_${userId}`, amount); - return { before, after: amount, user: { id: String(userId) }, amount }; + setMoney(guildId, userId, amount) { + this.assertAmount(amount, true); + const before = this.getBalance(guildId, userId); + this.db.set(this.key(guildId, userId), amount); + return { user: { id: String(userId) }, amount, before, after: amount }; } - removeMoney(userId, amount) { - this.assertUserId(userId); - this.assertPositiveAmount(amount); - const before = this.fetchMoney(userId).balance; - if (before < amount) return { error: "Yetersiz bakiye." }; + removeMoney(guildId, userId, amount) { + this.assertAmount(amount); + const before = this.getBalance(guildId, userId); + if (before < amount) return { error: "Yetersiz bakiye.", before, after: before }; const after = before - amount; - this.db.set(`money_${userId}`, after); - return { before, after, user: { id: String(userId) }, amount }; + this.db.set(this.key(guildId, userId), after); + return { user: { id: String(userId) }, amount, before, after }; } - transfer(fromUserId, toUserId, amount) { - this.assertUserId(fromUserId); - this.assertUserId(toUserId); - this.assertPositiveAmount(amount); - - if (String(fromUserId) === String(toUserId)) return { error: "Kullanıcı kendisine para gönderemez." }; + transfer(guildId, fromUserId, toUserId, amount) { + this.assertAmount(amount); + this.assertId(fromUserId, "kullanıcı ID'si"); + this.assertId(toUserId, "kullanıcı ID'si"); + if (String(fromUserId) === String(toUserId)) return { error: "Kendine para gönderemezsin." }; return this.db.transaction(() => { - const fromBalance = this.fetchMoney(fromUserId).balance; - if (fromBalance < amount) return { error: "Yetersiz bakiye." }; - - const toBalance = this.fetchMoney(toUserId).balance; - const newFromBalance = fromBalance - amount; - const newToBalance = toBalance + amount; - - if (!Number.isSafeInteger(newToBalance)) throw new RangeError("Hedef bakiyesi güvenli sayı sınırını aşıyor."); - - this.db.set(`money_${fromUserId}`, newFromBalance); - this.db.set(`money_${toUserId}`, newToBalance); - return { user1: { id: String(fromUserId) }, user2: { id: String(toUserId) }, amount }; + const fromKey = this.key(guildId, fromUserId); + const toKey = this.key(guildId, toUserId); + const fromBalance = this.getBalance(guildId, fromUserId); + const toBalance = this.getBalance(guildId, toUserId); + if (fromBalance < amount) return { error: "Yetersiz bakiye.", fromBalance, toBalance }; + if (!Number.isSafeInteger(toBalance + amount)) throw new RangeError("Hedef bakiye güvenli sayı sınırını aşıyor."); + this.db.set(fromKey, fromBalance - amount); + this.db.set(toKey, toBalance + amount); + return { amount, fromBalance: fromBalance - amount, toBalance: toBalance + amount }; }); } - getCooldown(key, cooldownMs) { - const last = Number(this.db.fetch(key) || 0); - const remaining = cooldownMs - (Date.now() - last); - return last > 0 && remaining > 0 ? this.formatDuration(remaining) : null; + useCooldown(guildId, userId, command, durationMs) { + const now = Date.now(); + const key = this.cooldownKey(guildId, userId, command); + const last = Number(this.db.get(key, 0)); + const elapsed = now - last; + const remainingMs = durationMs - elapsed; + if (last > 0 && remainingMs > 0) return { onCooldown: true, remainingMs }; + this.db.set(key, now); + return { onCooldown: false, remainingMs: 0 }; } - daily(userId, amount) { - this.assertUserId(userId); - this.assertPositiveAmount(amount); - const key = `dailycooldown_${userId}`; - const cooldown = this.getCooldown(key, 86_400_000); - if (cooldown) return { onCooldown: true, time: cooldown, user: userId }; - - const result = this.addMoney(userId, amount); - this.db.set(key, Date.now()); - return { onCooldown: false, ...result, time: this.formatDuration(86_400_000) }; + daily(guildId, userId, amount) { + const cooldown = this.useCooldown(guildId, userId, "daily", DAY); + if (cooldown.onCooldown) return cooldown; + return { ...cooldown, ...this.addMoney(guildId, userId, amount) }; } - weekly(userId, amount) { - this.assertUserId(userId); - this.assertPositiveAmount(amount); - const key = `weeklycooldown_${userId}`; - const cooldown = this.getCooldown(key, 604_800_000); - if (cooldown) return { onCooldown: true, time: cooldown, user: userId }; - - const result = this.addMoney(userId, amount); - this.db.set(key, Date.now()); - return { onCooldown: false, ...result, time: this.formatDuration(604_800_000) }; + weekly(guildId, userId, amount) { + const cooldown = this.useCooldown(guildId, userId, "weekly", WEEK); + if (cooldown.onCooldown) return cooldown; + return { ...cooldown, ...this.addMoney(guildId, userId, amount) }; } - work(userId, amount, options = {}) { - this.assertUserId(userId); - this.assertPositiveAmount(amount); + work(guildId, userId, amount, options = {}) { const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 2_700_000; - const key = `workcooldown_${userId}`; - const cooldown = this.getCooldown(key, cooldownMs); - if (cooldown) return { onCooldown: true, time: cooldown, user: { id: String(userId) } }; - - const jobs = Array.isArray(options.jobs) && options.jobs.length > 0 ? options.jobs : [ - "Geliştirici", "Doktor", "Öğretmen", "Müzisyen", "Madenci", "Mühendis", "Yayıncı", "Tasarımcı" - ]; - const result = this.addMoney(userId, amount); - this.db.set(key, Date.now()); - return { - onCooldown: false, - ...result, - workedAs: jobs[Math.floor(Math.random() * jobs.length)], - time: this.formatDuration(cooldownMs) - }; - } - - beg(userId, amount, options = {}) { - this.assertUserId(userId); - this.assertPositiveAmount(amount); + const cooldown = this.useCooldown(guildId, userId, "work", cooldownMs); + if (cooldown.onCooldown) return cooldown; + const jobs = Array.isArray(options.jobs) && options.jobs.length ? options.jobs : ["Geliştirici", "Doktor", "Öğretmen", "Müzisyen", "Madenci", "Mühendis", "Tasarımcı", "Yayıncı"]; + return { ...cooldown, ...this.addMoney(guildId, userId, amount), workedAs: jobs[Math.floor(Math.random() * jobs.length)] }; + } + + randomEarning(guildId, userId, command, amount, options = {}) { const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 60_000; - const customName = typeof options.customName === "string" && options.customName.trim() ? options.customName.trim() : "beg"; - const key = `${customName}cooldown_${userId}`; - const cooldown = this.getCooldown(key, cooldownMs); - if (cooldown) return { onCooldown: true, time: cooldown, user: { id: String(userId) } }; - - const lost = options.canLose === true && Math.floor(Math.random() * 5) === Math.floor(Math.random() * 5); - if (lost) { - const balance = this.fetchMoney(userId).balance; - this.db.set(key, Date.now()); - return { onCooldown: false, lost: true, before: balance, after: balance, user: { id: String(userId) }, amount, time: this.formatDuration(cooldownMs) }; + const cooldown = this.useCooldown(guildId, userId, command, cooldownMs); + if (cooldown.onCooldown) return cooldown; + if (options.canLose && Math.random() < 0.2) { + return { ...cooldown, lost: true, amount: 0, after: this.getBalance(guildId, userId) }; } - - const result = this.addMoney(userId, amount); - this.db.set(key, Date.now()); - return { onCooldown: false, lost: false, ...result, time: this.formatDuration(cooldownMs) }; + return { ...cooldown, lost: false, ...this.addMoney(guildId, userId, amount) }; } - leaderboard({ limit = 10 } = {}) { - let entries = this.db.all() - .filter(({ ID, data }) => /^money_\d{17,20}$/.test(ID) && Number.isSafeInteger(Number(data)) && Number(data) >= 0) - .map(({ ID, data }) => ({ id: ID.slice(6), money: Number(data) })) - .sort((a, b) => b.money - a.money); - - if (Number.isInteger(limit) && limit > 0) entries = entries.slice(0, limit); - return entries.map((entry, index) => ({ ...entry, position: index + 1 })); + leaderboard(guildId, limit = 15) { + const prefix = `money:${guildId}:`; + return this.db + .startsWith(prefix) + .map(({ ID, data }) => ({ id: ID.slice(prefix.length), money: Number(data) })) + .filter((entry) => /^\d{17,20}$/.test(entry.id) && Number.isSafeInteger(entry.money) && entry.money >= 0) + .sort((a, b) => b.money - a.money || a.id.localeCompare(b.id)) + .slice(0, Math.max(1, Math.min(100, Number(limit) || 15))) + .map((entry, index) => ({ ...entry, position: index + 1 })); } - formatDuration(milliseconds) { - let seconds = Math.max(0, Math.ceil(milliseconds / 1000)); - const days = Math.floor(seconds / 86_400); - seconds %= 86_400; - const hours = Math.floor(seconds / 3_600); - seconds %= 3_600; - const minutes = Math.floor(seconds / 60); - seconds %= 60; - return { days, hours, minutes, seconds }; + getPosition(guildId, userId) { + const balance = this.getBalance(guildId, userId); + const all = this.leaderboard(guildId, 100); + const existing = all.find((entry) => entry.id === String(userId)); + if (existing) return existing.position; + const ahead = this.db.startsWith(`money:${guildId}:`).filter(({ data }) => Number(data) > balance).length; + return ahead + 1; } } From 19995331e771e6cf9a1505d30563b894cceadbce Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:18:32 +0300 Subject: [PATCH 087/175] Rewrite ping as native slash command --- commands/ping.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/commands/ping.js b/commands/ping.js index 2d6dae4..0a6d87e 100644 --- a/commands/ping.js +++ b/commands/ping.js @@ -1,25 +1,27 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); -exports.execute = async (client, message) => { - const gatewayLatency = Math.max(0, Math.floor(client.ws.ping)); - const startedAt = Date.now(); - const sentMessage = await message.channel.send("Ping ölçülüyor..."); - const trip = Math.max(0, Date.now() - startedAt); +exports.data = new SlashCommandBuilder().setName("ping").setDescription("Botun gecikmesini gösterir."); +exports.name = "ping"; +exports.aliases = ["pong", "latency", "gecikme"]; + +exports.execute = async (ctx) => { + const apiLatency = Math.max(0, Math.round(ctx.client.ws.ping)); + const clientLatency = Math.max(0, Date.now() - ctx.interaction.createdTimestamp); const embed = new EmbedBuilder() .setTitle("Pong!") .addFields( - { name: "API Gecikmesi", value: `${gatewayLatency}ms`, inline: true }, - { name: "İstemci Gecikmesi", value: `${trip}ms`, inline: true } + { name: "API Gecikmesi", value: `${apiLatency} ms`, inline: true }, + { name: "İstemci Gecikmesi", value: `${clientLatency} ms`, inline: true } ) .setColor("Blurple") .setTimestamp(); - return sentMessage.edit({ content: null, embeds: [embed] }); + return ctx.reply({ embeds: [embed] }); }; exports.help = { name: "ping", - aliases: ["pong", "latency", "gecikme"], + aliases: exports.aliases, usage: "ping" }; From 47063c6e2b42dca97e9f635512d7f141ba74382c Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:18:38 +0300 Subject: [PATCH 088/175] Rewrite balance command natively --- commands/bal.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/commands/bal.js b/commands/bal.js index 078e31c..af41272 100644 --- a/commands/bal.js +++ b/commands/bal.js @@ -1,25 +1,31 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const { resolveUser, formatMoney } = require("../lib/commandUtils"); -exports.execute = async (client, message) => { - const user = message.mentions.users.first() || message.author; - const userBalance = client.eco.fetchMoney(user.id); +exports.data = new SlashCommandBuilder() + .setName("bal") + .setDescription("Bir kullanıcının bakiyesini gösterir.") + .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi görüntülenecek kullanıcı.")); +exports.name = "bal"; +exports.aliases = ["money", "credits", "balance", "bakiye", "para"]; +exports.execute = async (ctx) => { + const user = (ctx.isSlash ? ctx.userOption("kullanici") : await resolveUser(ctx, 0)) || ctx.user; + if (user.bot) return ctx.reply("Bot hesaplarının ekonomisi gösterilmez."); + + const balance = ctx.client.eco.getBalance(ctx.guildId, user.id); + const position = ctx.client.eco.getPosition(ctx.guildId, user.id); const embed = new EmbedBuilder() .setTitle("Bakiye") .addFields( - { name: "Kullanıcı", value: `<@${userBalance.user.id || user.id}>` }, - { name: "Bakiye", value: `${userBalance.balance} 💸` }, - { name: "Sıralama", value: userBalance.position ? `${userBalance.position}` : "Sıralamada değil" } + { name: "Kullanıcı", value: `<@${user.id}>`, inline: true }, + { name: "Bakiye", value: formatMoney(balance), inline: true }, + { name: "Sıralama", value: `#${position}`, inline: true } ) .setColor("Blurple") .setThumbnail(user.displayAvatarURL()) .setTimestamp(); - return message.channel.send({ embeds: [embed] }); + return ctx.reply({ embeds: [embed] }); }; -exports.help = { - name: "bal", - aliases: ["money", "credits", "balance", "bakiye", "para"], - usage: "bal []" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "bal [@kullanıcı]" }; From 8a1a3d536e55e428071d82935e92f8598a7db8c1 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:18:44 +0300 Subject: [PATCH 089/175] Rewrite addmoney command natively --- commands/addmoney.js | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/commands/addmoney.js b/commands/addmoney.js index dfc5799..230c443 100644 --- a/commands/addmoney.js +++ b/commands/addmoney.js @@ -1,35 +1,34 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const { isAdmin, resolveUser, getAmount, formatMoney } = require("../lib/commandUtils"); -exports.execute = async (client, message, args) => { - if (!client.config.admins.includes(message.author.id)) { - return message.reply("Bu komutu kullanmak için yetkin yok."); - } +exports.data = new SlashCommandBuilder() + .setName("addmoney") + .setDescription("Bir kullanıcıya para ekler.") + .addUserOption((option) => option.setName("kullanici").setDescription("Para eklenecek kullanıcı.").setRequired(true)) + .addIntegerOption((option) => option.setName("miktar").setDescription("Eklenecek miktar.").setMinValue(1).setMaxValue(2147483647).setRequired(true)); +exports.name = "addmoney"; +exports.aliases = ["addbal", "paraekle", "para-ekle"]; - const user = message.mentions.users.first() || client.users.cache.get(args[0]); - if (!user) return message.reply("Lütfen geçerli bir kullanıcı belirtin."); +exports.execute = async (ctx) => { + if (!isAdmin(ctx)) return ctx.reply({ content: "Bu komutu kullanmak için sunucu yönetimi yetkisine sahip olmalısın.", ephemeral: true }); + const user = await resolveUser(ctx, 0); + const amount = getAmount(ctx, "miktar", 1); + if (!user || user.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); + if (!amount) return ctx.reply("Geçerli ve pozitif bir miktar belirtmelisin."); - const amount = Number(args[1]); - if (!Number.isSafeInteger(amount) || amount <= 0) { - return message.reply("Lütfen 1 veya daha büyük, geçerli bir miktar belirtin."); - } - - const data = client.eco.addMoney(user.id, amount); + const data = ctx.client.eco.addMoney(ctx.guildId, user.id, amount); const embed = new EmbedBuilder() .setTitle("Para Eklendi!") .addFields( - { name: "Kullanıcı", value: `<@${user.id}>` }, - { name: "Eklenen Miktar", value: `${data.amount} 💸` }, - { name: "Toplam Bakiye", value: `${data.after} 💸` } + { name: "Kullanıcı", value: `<@${user.id}>`, inline: true }, + { name: "Eklenen", value: formatMoney(data.amount), inline: true }, + { name: "Yeni Bakiye", value: formatMoney(data.after), inline: true } ) .setColor("Blurple") .setThumbnail(user.displayAvatarURL()) .setTimestamp(); - return message.channel.send({ embeds: [embed] }); + return ctx.reply({ embeds: [embed] }); }; -exports.help = { - name: "addmoney", - aliases: ["addbal", "paraekle", "para-ekle"], - usage: "addmoney " -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "addmoney @kullanıcı " }; From 192540095e2b9cfa49d5e07ad311e3e4b6507d81 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:18:49 +0300 Subject: [PATCH 090/175] Rewrite beg command natively --- commands/beg.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/commands/beg.js b/commands/beg.js index 112680d..eb9bd46 100644 --- a/commands/beg.js +++ b/commands/beg.js @@ -1,18 +1,20 @@ -exports.execute = async (client, message) => { - const users = ["PewDiePie", "T-Series", "Sans", "Zero"]; - const amount = Math.floor(Math.random() * 50) + 10; - const reward = client.eco.beg(message.author.id, amount, { canLose: true }); +const { SlashCommandBuilder } = require("discord.js"); +const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); - if (reward.onCooldown) return message.reply(`Tekrar dilenebilmek için ${reward.time.seconds} saniye beklemelisin.`); - if (reward.lost) { - return message.channel.send(`**${users[Math.floor(Math.random() * users.length)]}:** Şansın yaver gitmedi! Daha sonra tekrar dene.`); - } +const donors = ["PewDiePie", "T-Series", "Sans", "Zero"]; - return message.reply(`**${users[Math.floor(Math.random() * users.length)]}** sana **${reward.amount}** 💸 bağışladı. Artık **${reward.after}** 💸 paran var.`); -}; +exports.data = new SlashCommandBuilder().setName("beg").setDescription("Dilenerek rastgele miktarda para kazanmaya çalışır."); +exports.name = "beg"; +exports.aliases = ["dilen", "dilenme", "dilencilik"]; + +exports.execute = async (ctx) => { + const amount = randomInt(10, 59); + const result = ctx.client.eco.randomEarning(ctx.guildId, ctx.userId, "beg", amount, { canLose: true, cooldown: 60_000 }); + if (result.onCooldown) return ctx.reply(`Tekrar dilenebilmek için **${formatRemaining(result.remainingMs)}** beklemelisin.`); -exports.help = { - name: "beg", - aliases: ["dilen", "dilenme", "dilencilik"], - usage: "beg" + const donor = donors[Math.floor(Math.random() * donors.length)]; + if (result.lost) return ctx.reply(`**${donor}:** Şansın yaver gitmedi; bu sefer para kazanamadın.`); + return ctx.reply(`**${donor}** sana **${formatMoney(result.amount)}** verdi. Yeni bakiyen **${formatMoney(result.after)}**.`); }; + +exports.help = { name: exports.name, aliases: exports.aliases, usage: "beg" }; From 2ebba8d5efb11f57bd7c932f5979d4f911c18904 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:18:54 +0300 Subject: [PATCH 091/175] Rewrite daily command natively --- commands/daily.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/commands/daily.js b/commands/daily.js index b592298..be4d38c 100644 --- a/commands/daily.js +++ b/commands/daily.js @@ -1,16 +1,15 @@ -exports.execute = async (client, message) => { - const amount = Math.floor(Math.random() * 500) + 100; - const reward = client.eco.daily(message.author.id, amount); +const { SlashCommandBuilder } = require("discord.js"); +const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); - if (reward.onCooldown) { - return message.reply(`Günlük ödülünü zaten aldın. Tekrar almak için ${reward.time.hours} saat, ${reward.time.minutes} dakika ve ${reward.time.seconds} saniye beklemelisin.`); - } +exports.data = new SlashCommandBuilder().setName("daily").setDescription("Günlük para ödülünü alır."); +exports.name = "daily"; +exports.aliases = ["günlük", "gunluk"]; - return message.reply(`Günlük ödül olarak **${reward.amount}** 💸 kazandın. Artık **${reward.after}** 💸 paran var.`); +exports.execute = async (ctx) => { + const amount = randomInt(100, 599); + const result = ctx.client.eco.daily(ctx.guildId, ctx.userId, amount); + if (result.onCooldown) return ctx.reply(`Günlük ödülünü zaten aldın. Tekrar almak için **${formatRemaining(result.remainingMs)}** beklemelisin.`); + return ctx.reply(`Günlük ödül olarak **${formatMoney(result.amount)}** kazandın. Yeni bakiyen **${formatMoney(result.after)}**.`); }; -exports.help = { - name: "daily", - aliases: ["günlük", "gunluk"], - usage: "daily" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "daily" }; From 7837cbfc8ecdd1518c805d0bc09628ad7409c3f2 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:00 +0300 Subject: [PATCH 092/175] Rewrite work command natively --- commands/work.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/commands/work.js b/commands/work.js index c91f893..426126a 100644 --- a/commands/work.js +++ b/commands/work.js @@ -1,16 +1,15 @@ -exports.execute = async (client, message) => { - const amount = Math.floor(Math.random() * 1500) + 1000; - const reward = client.eco.work(message.author.id, amount); +const { SlashCommandBuilder } = require("discord.js"); +const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); - if (reward.onCooldown) { - return message.reply(`Yorgunsun. Tekrar çalışmak için ${reward.time.minutes} dakika ${reward.time.seconds} saniye beklemelisin.`); - } +exports.data = new SlashCommandBuilder().setName("work").setDescription("Çalışarak rastgele para kazanır."); +exports.name = "work"; +exports.aliases = ["çalış", "calis", "çalıştır", "calistir"]; - return message.reply(`**${reward.workedAs}** olarak çalıştın ve **${reward.amount}** 💸 kazandın. Artık **${reward.after}** 💸 paran var.`); +exports.execute = async (ctx) => { + const amount = randomInt(1000, 2499); + const result = ctx.client.eco.work(ctx.guildId, ctx.userId, amount); + if (result.onCooldown) return ctx.reply(`Yorgunsun. Tekrar çalışmak için **${formatRemaining(result.remainingMs)}** beklemelisin.`); + return ctx.reply(`**${result.workedAs}** olarak çalıştın ve **${formatMoney(result.amount)}** kazandın. Yeni bakiyen **${formatMoney(result.after)}**.`); }; -exports.help = { - name: "work", - aliases: ["çalış", "calis", "çalıştır", "calistir"], - usage: "work" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "work" }; From 234155cc8eec69ad34fffd463d3363e5da7e839c Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:06 +0300 Subject: [PATCH 093/175] Rewrite weekly command natively --- commands/weekly.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/commands/weekly.js b/commands/weekly.js index d10da60..215a4ae 100644 --- a/commands/weekly.js +++ b/commands/weekly.js @@ -1,16 +1,15 @@ -exports.execute = async (client, message) => { - const amount = Math.floor(Math.random() * 1000) + 500; - const reward = client.eco.weekly(message.author.id, amount); +const { SlashCommandBuilder } = require("discord.js"); +const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); - if (reward.onCooldown) { - return message.reply(`Haftalık ödülünü zaten aldın. Tekrar almak için ${reward.time.days} gün, ${reward.time.hours} saat, ${reward.time.minutes} dakika ve ${reward.time.seconds} saniye beklemelisin.`); - } +exports.data = new SlashCommandBuilder().setName("weekly").setDescription("Haftalık para ödülünü alır."); +exports.name = "weekly"; +exports.aliases = ["haftalık", "haftalik"]; - return message.reply(`Haftalık ödül olarak **${reward.amount}** 💸 kazandın. Artık **${reward.after}** 💸 paran var.`); +exports.execute = async (ctx) => { + const amount = randomInt(500, 1499); + const result = ctx.client.eco.weekly(ctx.guildId, ctx.userId, amount); + if (result.onCooldown) return ctx.reply(`Haftalık ödülünü zaten aldın. Tekrar almak için **${formatRemaining(result.remainingMs)}** beklemelisin.`); + return ctx.reply(`Haftalık ödül olarak **${formatMoney(result.amount)}** kazandın. Yeni bakiyen **${formatMoney(result.after)}**.`); }; -exports.help = { - name: "weekly", - aliases: ["haftalık", "haftalik"], - usage: "weekly" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "weekly" }; From adbde536333248c5dbffb4ee80a7d3f7a87be9bc Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:12 +0300 Subject: [PATCH 094/175] Rewrite shop purchase command natively --- commands/buy.js | 66 ++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/commands/buy.js b/commands/buy.js index 569a0d1..0666609 100644 --- a/commands/buy.js +++ b/commands/buy.js @@ -1,35 +1,35 @@ -exports.execute = async (client, message, args) => { - const userBalance = client.eco.fetchMoney(message.author.id); - const itemName = args[0]; - - if (!itemName) return message.reply("Satın almak istediğin ürünü belirtmelisin."); - - const item = Object.entries(client.shop).find(([name]) => name.toLowerCase() === itemName.toLowerCase()); - if (!item) return message.reply("Böyle bir ürün bulunmuyor. `shop` komutuyla mağazayı görüntüleyebilirsin."); - - const [name, product] = item; - if (userBalance.balance < product.cost) { - return message.reply(`Bakiyen yetersiz. Bu ürünü almak için **${product.cost}** 💸 gerekiyor.`); - } - - const removed = client.eco.removeMoney(message.author.id, product.cost); - if (removed.error) return message.reply("Satın alma sırasında bakiye işlemi başarısız oldu."); - - const itemStruct = { - name: name.toLowerCase(), - price: product.cost, - purchasedAt: Date.now() - }; - - const currentItems = client.db.get(`items_${message.author.id}`); - const items = Array.isArray(currentItems) ? currentItems : []; - client.db.set(`items_${message.author.id}`, [...items, itemStruct]); - - return message.channel.send(`**${name}** ürününü **💸${product.cost}** karşılığında satın aldın.`); +const { SlashCommandBuilder } = require("discord.js"); +const { getProduct, formatMoney } = require("../lib/commandUtils"); + +exports.data = new SlashCommandBuilder() + .setName("buy") + .setDescription("Mağazadan bir ürün satın alır.") + .addStringOption((option) => + option + .setName("urun") + .setDescription("Satın almak istediğin ürün.") + .setRequired(true) + .addChoices( + { name: "Laptop", value: "laptop" }, + { name: "Mobile", value: "mobile" }, + { name: "PC", value: "pc" } + ) + ); +exports.name = "buy"; +exports.aliases = ["satınal", "satinal", "al"]; + +exports.execute = async (ctx) => { + const requested = getProduct(ctx); + const normalized = String(requested || "").toLowerCase(); + const item = ctx.client.shop[normalized]; + if (!item) return ctx.reply("Böyle bir ürün bulunmuyor."); + + const balance = ctx.client.eco.getBalance(ctx.guildId, ctx.userId); + if (balance < item.cost) return ctx.reply(`Bakiyen yetersiz. Bu ürün için **${formatMoney(item.cost)}** gerekiyor.`); + + const result = ctx.client.eco.purchase(ctx.guildId, ctx.userId, item); + if (result.error) return ctx.reply(result.error); + return ctx.reply(`**${item.name}** ürününü **${formatMoney(item.cost)}** karşılığında satın aldın. Kalan bakiyen **${formatMoney(result.after)}**.`); }; -exports.help = { - name: "buy", - aliases: ["satınal", "satinal", "al"], - usage: "buy <ürün>" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "buy <ürün>" }; From eddcc44ac8dc7a32b47a4585c10b094a7c611842 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:22 +0300 Subject: [PATCH 095/175] Make item purchases atomic --- lib/economy.js | 74 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/lib/economy.js b/lib/economy.js index b31ee7c..98e29c2 100644 --- a/lib/economy.js +++ b/lib/economy.js @@ -18,25 +18,31 @@ class EconomyManager { } } - key(guildId, userId) { + moneyKey(guildId, userId) { this.assertId(guildId, "sunucu ID'si"); this.assertId(userId, "kullanıcı ID'si"); return `money:${guildId}:${userId}`; } cooldownKey(guildId, userId, command) { + this.assertId(guildId, "sunucu ID'si"); + this.assertId(userId, "kullanıcı ID'si"); return `cooldown:${guildId}:${userId}:${command}`; } - fetchMoney(guildId, userId) { - const key = this.key(guildId, userId); - const stored = Number(this.db.get(key, 0)); - const balance = Number.isSafeInteger(stored) && stored >= 0 ? stored : 0; - return { user: { id: String(userId) }, balance }; + inventoryKey(guildId, userId) { + this.assertId(guildId, "sunucu ID'si"); + this.assertId(userId, "kullanıcı ID'si"); + return `inventory:${guildId}:${userId}`; } getBalance(guildId, userId) { - return this.fetchMoney(guildId, userId).balance; + const stored = Number(this.db.get(this.moneyKey(guildId, userId), 0)); + return Number.isSafeInteger(stored) && stored >= 0 ? stored : 0; + } + + fetchMoney(guildId, userId) { + return { user: { id: String(userId) }, balance: this.getBalance(guildId, userId) }; } addMoney(guildId, userId, amount) { @@ -44,14 +50,14 @@ class EconomyManager { const before = this.getBalance(guildId, userId); const after = before + amount; if (!Number.isSafeInteger(after)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); - this.db.set(this.key(guildId, userId), after); + this.db.set(this.moneyKey(guildId, userId), after); return { user: { id: String(userId) }, amount, before, after }; } setMoney(guildId, userId, amount) { this.assertAmount(amount, true); const before = this.getBalance(guildId, userId); - this.db.set(this.key(guildId, userId), amount); + this.db.set(this.moneyKey(guildId, userId), amount); return { user: { id: String(userId) }, amount, before, after: amount }; } @@ -60,7 +66,7 @@ class EconomyManager { const before = this.getBalance(guildId, userId); if (before < amount) return { error: "Yetersiz bakiye.", before, after: before }; const after = before - amount; - this.db.set(this.key(guildId, userId), after); + this.db.set(this.moneyKey(guildId, userId), after); return { user: { id: String(userId) }, amount, before, after }; } @@ -71,24 +77,46 @@ class EconomyManager { if (String(fromUserId) === String(toUserId)) return { error: "Kendine para gönderemezsin." }; return this.db.transaction(() => { - const fromKey = this.key(guildId, fromUserId); - const toKey = this.key(guildId, toUserId); const fromBalance = this.getBalance(guildId, fromUserId); const toBalance = this.getBalance(guildId, toUserId); if (fromBalance < amount) return { error: "Yetersiz bakiye.", fromBalance, toBalance }; if (!Number.isSafeInteger(toBalance + amount)) throw new RangeError("Hedef bakiye güvenli sayı sınırını aşıyor."); - this.db.set(fromKey, fromBalance - amount); - this.db.set(toKey, toBalance + amount); + this.db.set(this.moneyKey(guildId, fromUserId), fromBalance - amount); + this.db.set(this.moneyKey(guildId, toUserId), toBalance + amount); return { amount, fromBalance: fromBalance - amount, toBalance: toBalance + amount }; }); } + purchase(guildId, userId, item) { + if (!item || !item.id || !item.name || !Number.isSafeInteger(item.cost) || item.cost < 0) { + throw new TypeError("Geçersiz mağaza ürünü."); + } + + return this.db.transaction(() => { + const balance = this.getBalance(guildId, userId); + if (balance < item.cost) return { error: "Yetersiz bakiye.", after: balance }; + const key = this.inventoryKey(guildId, userId); + const inventory = this.db.get(key, []); + if (!Array.isArray(inventory)) throw new TypeError("Envanter verisi bozuk."); + inventory.push({ id: item.id, name: item.name, price: item.cost, purchasedAt: Date.now() }); + const after = balance - item.cost; + this.db.set(this.moneyKey(guildId, userId), after); + this.db.set(key, inventory); + return { error: null, after, item }; + }); + } + + getInventory(guildId, userId) { + const inventory = this.db.get(this.inventoryKey(guildId, userId), []); + return Array.isArray(inventory) ? inventory : []; + } + useCooldown(guildId, userId, command, durationMs) { + if (!Number.isSafeInteger(durationMs) || durationMs <= 0) throw new RangeError("Geçersiz cooldown süresi."); const now = Date.now(); const key = this.cooldownKey(guildId, userId, command); const last = Number(this.db.get(key, 0)); - const elapsed = now - last; - const remainingMs = durationMs - elapsed; + const remainingMs = durationMs - (now - last); if (last > 0 && remainingMs > 0) return { onCooldown: true, remainingMs }; this.db.set(key, now); return { onCooldown: false, remainingMs: 0 }; @@ -118,12 +146,14 @@ class EconomyManager { const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 60_000; const cooldown = this.useCooldown(guildId, userId, command, cooldownMs); if (cooldown.onCooldown) return cooldown; - if (options.canLose && Math.random() < 0.2) { - return { ...cooldown, lost: true, amount: 0, after: this.getBalance(guildId, userId) }; - } + if (options.canLose && Math.random() < 0.2) return { ...cooldown, lost: true, amount: 0, after: this.getBalance(guildId, userId) }; return { ...cooldown, lost: false, ...this.addMoney(guildId, userId, amount) }; } + rob(guildId, robberId, targetId, amount) { + return this.transfer(guildId, targetId, robberId, amount); + } + leaderboard(guildId, limit = 15) { const prefix = `money:${guildId}:`; return this.db @@ -137,11 +167,7 @@ class EconomyManager { getPosition(guildId, userId) { const balance = this.getBalance(guildId, userId); - const all = this.leaderboard(guildId, 100); - const existing = all.find((entry) => entry.id === String(userId)); - if (existing) return existing.position; - const ahead = this.db.startsWith(`money:${guildId}:`).filter(({ data }) => Number(data) > balance).length; - return ahead + 1; + return this.db.startsWith(`money:${guildId}:`).filter(({ data }) => Number(data) > balance).length + 1; } } From d51b123095a32035b788a6a232f8a4ee5de9b9c6 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:28 +0300 Subject: [PATCH 096/175] Rewrite help command natively --- commands/help.js | 46 ++++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/commands/help.js b/commands/help.js index 81c7630..2386376 100644 --- a/commands/help.js +++ b/commands/help.js @@ -1,30 +1,28 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); + +exports.data = new SlashCommandBuilder().setName("help").setDescription("Botun kullanılabilir komutlarını gösterir."); +exports.name = "help"; +exports.aliases = ["h", "yardım", "yardim", "komutlar"]; + +exports.execute = async (ctx) => { + const commands = [...ctx.client.commands.values()].sort((a, b) => a.name.localeCompare(b.name)); + const fields = commands.map((command) => ({ + name: `/${command.name}`, + value: command.data.description || "Komut", + inline: true + })); -exports.execute = async (client, message) => { - const prefix = String(message.prefix || client.config.prefix || "!"); const embed = new EmbedBuilder() - .setAuthor({ name: "Komutlar" }) - .setTitle("INS Development Economy Bot!") - .setURL("https://github.com/LoiFragola/EconomyBot") - .setDescription(`Toplam Komut: ${client.commands.size}`) + .setAuthor({ name: "INS Development" }) + .setTitle("EconomyBot Komutları") + .setDescription(`Toplam **${commands.length}** komut bulunuyor. Prefix: \`${ctx.client.db.getPrefix(ctx.guildId, ctx.client.config.prefix)}\``) + .addFields(fields) .setColor("Blurple") - .setTimestamp() - .setThumbnail(client.user.displayAvatarURL()); + .setThumbnail(ctx.client.user.displayAvatarURL()) + .setFooter({ text: `${ctx.user.tag} tarafından istendi` }) + .setTimestamp(); - for (const command of client.commands.values()) { - embed.addFields({ - name: command.help.name, - value: `Takma Adlar: ${command.help.aliases.join(", ") || "Yok"}\nKullanım: \`${prefix}${command.help.usage}\``, - inline: true - }); - } - - embed.setFooter({ text: message.author.tag, iconURL: message.author.displayAvatarURL() }); - return message.channel.send({ embeds: [embed] }); + return ctx.reply({ embeds: [embed] }); }; -exports.help = { - name: "help", - aliases: ["h", "yardım", "yardim", "komutlar"], - usage: "help" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "help" }; From 291a4308e3f4c21adbe018b008186bd2a3ca830a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:33 +0300 Subject: [PATCH 097/175] Rewrite inventory command natively --- commands/inventory.js | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/commands/inventory.js b/commands/inventory.js index 6d68f8d..e9c42ab 100644 --- a/commands/inventory.js +++ b/commands/inventory.js @@ -1,36 +1,37 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const { formatMoney } = require("../lib/commandUtils"); -exports.execute = async (client, message) => { - const items = client.db.get(`items_${message.author.id}`); +exports.data = new SlashCommandBuilder().setName("inventory").setDescription("Envanterindeki ürünleri gösterir."); +exports.name = "inventory"; +exports.aliases = ["inv", "envanter", "eşyalar", "esya"]; - if (!Array.isArray(items) || items.length === 0) { - return message.reply("Envanterin boş."); - } +exports.execute = async (ctx) => { + const items = ctx.client.eco.getInventory(ctx.guildId, ctx.userId); + if (items.length === 0) return ctx.reply("Envanterin boş."); const grouped = new Map(); for (const item of items) { - const name = String(item?.name || "Bilinmeyen ürün"); - grouped.set(name, (grouped.get(name) || 0) + 1); + const name = String(item.name || item.id || "Bilinmeyen ürün"); + const current = grouped.get(name) || { count: 0, price: Number(item.price) || 0 }; + current.count += 1; + grouped.set(name, current); } const embed = new EmbedBuilder() - .setAuthor({ name: `${message.author.tag} kullanıcısının envanteri`, iconURL: message.guild.iconURL() || undefined }) + .setTitle(`${ctx.user.username} — Envanter`) .setColor("Blurple") + .setThumbnail(ctx.user.displayAvatarURL()) .setTimestamp(); - for (const [name, count] of grouped) { + for (const [name, info] of grouped) { embed.addFields({ - name: `İsim: ${name}`, - value: `Miktar: **${count}**`, - inline: false + name, + value: `Miktar: **${info.count}**\nBirim fiyat: **${formatMoney(info.price)}**`, + inline: true }); } - return message.channel.send({ embeds: [embed] }); + return ctx.reply({ embeds: [embed] }); }; -exports.help = { - name: "inventory", - aliases: ["inv", "envanter", "eşyalar", "esya"], - usage: "inventory" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "inventory" }; From bd6438cc6568f350f27ef8b9034cd11909bf5c07 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:39 +0300 Subject: [PATCH 098/175] Rewrite leaderboard command natively --- commands/leaderboard.js | 43 +++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/commands/leaderboard.js b/commands/leaderboard.js index a790dfd..6784445 100644 --- a/commands/leaderboard.js +++ b/commands/leaderboard.js @@ -1,30 +1,27 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const { formatMoney } = require("../lib/commandUtils"); -exports.execute = async (client, message) => { - const leaderboard = client.eco.leaderboard({ limit: 15, raw: false }); - if (!leaderboard || leaderboard.length < 1) return message.channel.send("❌ | Sıralama boş!"); +exports.data = new SlashCommandBuilder().setName("lb").setDescription("Sunucunun ekonomi sıralamasını gösterir."); +exports.name = "lb"; +exports.aliases = ["leaderboard", "sıralama", "siralama", "liderlik"]; + +exports.execute = async (ctx) => { + const leaderboard = ctx.client.eco.leaderboard(ctx.guildId, 15); + if (leaderboard.length === 0) return ctx.reply("Ekonomi sıralaması henüz boş."); + + const lines = []; + for (const entry of leaderboard) { + const user = ctx.client.users.cache.get(entry.id); + lines.push(`**${entry.position}.** ${user ? user.tag : `<@${entry.id}>`} — **${formatMoney(entry.money)}**`); + } - const firstUser = client.users.cache.get(leaderboard[0].id); const embed = new EmbedBuilder() - .setAuthor({ name: `${message.guild.name} sıralaması!`, iconURL: message.guild.iconURL() || undefined }) - .setColor("Random") - .setThumbnail(firstUser ? firstUser.displayAvatarURL() : "https://cdn.discordapp.com/embed/avatars/0.png") + .setTitle(`${ctx.guild.name} — Ekonomi Sıralaması`) + .setDescription(lines.join("\n")) + .setColor("Blurple") .setTimestamp(); - leaderboard.forEach((user) => { - const discordUser = client.users.cache.get(user.id); - embed.addFields({ - name: `${user.position}. ${discordUser ? discordUser.tag : "Bilinmeyen Kullanıcı"}`, - value: `${user.money} 💸`, - inline: false - }); - }); - - return message.channel.send({ embeds: [embed] }); + return ctx.reply({ embeds: [embed] }); }; -exports.help = { - name: "lb", - aliases: ["leaderboard", "sıralama", "siralama", "liderlik"], - usage: "lb" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "lb" }; From 1f4b24bfe55d797d8d805a854c725621831b6e39 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:45 +0300 Subject: [PATCH 099/175] Rewrite shop command natively --- commands/shop.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/commands/shop.js b/commands/shop.js index c4d077c..963915f 100644 --- a/commands/shop.js +++ b/commands/shop.js @@ -1,21 +1,25 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const { formatMoney } = require("../lib/commandUtils"); -exports.execute = async (client, message) => { - const prefix = String(message.prefix || client.config.prefix || "!"); - const items = Object.entries(client.shop); - const content = items.map(([name, product]) => `${name} — 💸 ${product.cost}`).join("\n"); +exports.data = new SlashCommandBuilder().setName("shop").setDescription("Mağazadaki ürünleri gösterir."); +exports.name = "shop"; +exports.aliases = ["mağaza", "magaza", "market"]; +exports.execute = async (ctx) => { + const entries = Object.values(ctx.client.shop); const embed = new EmbedBuilder() .setTitle("Mağaza") - .setDescription(content || "Mağazada ürün bulunmuyor.") + .setDescription("Satın almak istediğin ürünü `/buy` ile seçebilirsin.") .setColor("Blurple") - .setFooter({ text: `${prefix}buy <ürün> yazarak ürünü satın alabilirsin.` }); + .setTimestamp(); - return message.channel.send({ embeds: [embed] }); -}; + embed.addFields(entries.map((item) => ({ + name: item.name, + value: `Fiyat: **${formatMoney(item.cost)}**\nKomut: \`/buy ${item.id}\``, + inline: true + }))); -exports.help = { - name: "shop", - aliases: ["mağaza", "magaza", "market"], - usage: "shop" + return ctx.reply({ embeds: [embed] }); }; + +exports.help = { name: exports.name, aliases: exports.aliases, usage: "shop" }; From f45857092f92393885fd114342eff1a70da2da81 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:53 +0300 Subject: [PATCH 100/175] Rewrite transfer command natively --- commands/transfer.js | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/commands/transfer.js b/commands/transfer.js index 2c088b3..cbb30b4 100644 --- a/commands/transfer.js +++ b/commands/transfer.js @@ -1,23 +1,24 @@ -exports.execute = async (client, message, args) => { - const member = message.mentions.members.first() || message.guild.members.cache.get(args[0]); - const amount = Number(args[1]); +const { SlashCommandBuilder } = require("discord.js"); +const { resolveUser, getAmount, formatMoney } = require("../lib/commandUtils"); - if (!member) return message.reply("Lütfen geçerli bir kullanıcı belirt."); - if (member.id === message.author.id) return message.reply("Kendine para gönderemezsin."); - if (member.user.bot) return message.reply("Bot hesaplarına para gönderilemez."); - if (!Number.isSafeInteger(amount) || amount <= 0) return message.reply("Lütfen 1 veya daha büyük, geçerli bir miktar gir."); +exports.data = new SlashCommandBuilder() + .setName("transfer") + .setDescription("Başka bir kullanıcıya para gönderir.") + .addUserOption((option) => option.setName("kullanici").setDescription("Para gönderilecek kullanıcı.").setRequired(true)) + .addIntegerOption((option) => option.setName("miktar").setDescription("Gönderilecek miktar.").setMinValue(1).setMaxValue(2147483647).setRequired(true)); +exports.name = "transfer"; +exports.aliases = ["give", "share", "aktar", "paraaktar"]; - const authorData = client.eco.fetchMoney(message.author.id); - if (authorData.balance < amount) return message.reply("Görünüşe göre bu kadar paran yok."); +exports.execute = async (ctx) => { + const target = await resolveUser(ctx, 0); + const amount = getAmount(ctx, "miktar", 1); + if (!target || target.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); + if (!amount) return ctx.reply("Geçerli ve pozitif bir miktar belirtmelisin."); + if (target.id === ctx.userId) return ctx.reply("Kendine para gönderemezsin."); - const result = client.eco.transfer(message.author.id, member.id, amount); - if (result.error) return message.reply("Para transferi gerçekleştirilemedi."); - - return message.channel.send(`💸 **${amount}** miktarını **${member.user.tag}** kullanıcısına başarıyla aktardın.`); + const result = ctx.client.eco.transfer(ctx.guildId, ctx.userId, target.id, amount); + if (result.error) return ctx.reply(result.error); + return ctx.reply(`**${formatMoney(amount)}** miktarını **${target.tag}** kullanıcısına gönderdin. Kalan bakiyen **${formatMoney(result.fromBalance)}**.`); }; -exports.help = { - name: "transfer", - aliases: ["give", "share", "aktar", "paraaktar"], - usage: "transfer " -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "transfer " }; From 2d744325225692cde421b6c0b0700d451be69fa9 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:19:58 +0300 Subject: [PATCH 101/175] Rewrite setmoney command natively --- commands/setmoney.js | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/commands/setmoney.js b/commands/setmoney.js index 2248be6..28b03e7 100644 --- a/commands/setmoney.js +++ b/commands/setmoney.js @@ -1,34 +1,33 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const { isAdmin, resolveUser, formatMoney, parseNonNegativeInteger } = require("../lib/commandUtils"); -exports.execute = async (client, message, args) => { - if (!client.config.admins.includes(message.author.id)) { - return message.reply("Bu komutu kullanmak için yetkin yok."); - } +exports.data = new SlashCommandBuilder() + .setName("setmoney") + .setDescription("Bir kullanıcının bakiyesini belirler.") + .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi ayarlanacak kullanıcı.").setRequired(true)) + .addIntegerOption((option) => option.setName("miktar").setDescription("Yeni bakiye.").setMinValue(0).setMaxValue(2147483647).setRequired(true)); +exports.name = "setmoney"; +exports.aliases = ["setbal", "parayarla", "bakiyeayarla"]; - const user = message.mentions.users.first() || client.users.cache.get(args[0]); - if (!user) return message.reply("Lütfen geçerli bir kullanıcı belirtin."); +exports.execute = async (ctx) => { + if (!isAdmin(ctx)) return ctx.reply({ content: "Bu komutu kullanmak için sunucu yönetimi yetkisine sahip olmalısın.", ephemeral: true }); + const user = await resolveUser(ctx, 0); + const amount = ctx.isSlash ? ctx.interaction.options.getInteger("miktar") : parseNonNegativeInteger(ctx.args[1]); + if (!user || user.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); + if (amount === null || amount === undefined) return ctx.reply("Geçerli ve 0 veya daha büyük bir miktar belirtmelisin."); - const amount = Number(args[1]); - if (!Number.isSafeInteger(amount) || amount < 1) { - return message.reply("Lütfen 1 veya daha büyük, geçerli bir miktar belirtin."); - } - - const data = client.eco.setMoney(user.id, amount); + const data = ctx.client.eco.setMoney(ctx.guildId, user.id, amount); const embed = new EmbedBuilder() - .setTitle("Para Güncellendi!") + .setTitle("Bakiye Güncellendi!") .addFields( - { name: "Kullanıcı", value: `<@${user.id}>` }, - { name: "Yeni Bakiye", value: `${data.after} 💸` } + { name: "Kullanıcı", value: `<@${user.id}>`, inline: true }, + { name: "Yeni Bakiye", value: formatMoney(data.after), inline: true } ) .setColor("Blurple") .setThumbnail(user.displayAvatarURL()) .setTimestamp(); - return message.channel.send({ embeds: [embed] }); + return ctx.reply({ embeds: [embed] }); }; -exports.help = { - name: "setmoney", - aliases: ["setbal", "parayarla", "bakiyeayarla"], - usage: "setmoney " -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "setmoney @kullanıcı " }; From 0c99f639b4f78907f9275a62b8001b89678e310b Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:20:06 +0300 Subject: [PATCH 102/175] Rewrite prefix command natively --- commands/prefix.js | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/commands/prefix.js b/commands/prefix.js index 00f73cf..ce6b97f 100644 --- a/commands/prefix.js +++ b/commands/prefix.js @@ -1,29 +1,24 @@ -exports.execute = async (client, message, args) => { - const currentPrefix = String(message.prefix || client.config.prefix || "!"); - const canManage = message.member?.permissions?.has("ManageGuild") || client.config.admins.includes(message.author.id); +const { SlashCommandBuilder, PermissionsBitField } = require("discord.js"); +const { isAdmin } = require("../lib/commandUtils"); - if (!canManage) { - return message.reply(`Bu sunucunun prefix'i **${currentPrefix}**.`); - } +exports.data = new SlashCommandBuilder() + .setName("prefix") + .setDescription("Sunucunun prefix'ini değiştirir veya varsayılana döndürür.") + .addStringOption((option) => option.setName("yeni_prefix").setDescription("1-10 karakterlik yeni prefix. Boş bırakırsan varsayılana döner.").setMaxLength(10)); +exports.name = "prefix"; +exports.aliases = ["setprefix", "önek", "onek"]; - const prefix = args[0]?.trim(); - if (!prefix) { - client.db.delete(`prefix_${message.guild.id}`); - message.prefix = String(client.config.prefix || "!"); - return message.channel.send(`✅ | Bu sunucunun prefix'i varsayılan **${message.prefix}** olarak sıfırlandı.`); - } +exports.execute = async (ctx) => { + if (!isAdmin(ctx)) return ctx.reply({ content: "Bu komutu kullanmak için Sunucuyu Yönet yetkisine sahip olmalısın.", ephemeral: true }); - if (prefix.length > 10 || /\s/.test(prefix)) { - return message.reply("Prefix 1-10 karakter arasında olmalı ve boşluk içeremez."); + const prefix = ctx.isSlash ? ctx.interaction.options.getString("yeni_prefix") : ctx.args[0]; + if (!prefix) { + ctx.client.db.delete(`prefix:${ctx.guildId}`); + return ctx.reply(`Prefix varsayılana döndürüldü: **${ctx.client.config.prefix}**`); } - - const setTo = client.db.set(`prefix_${message.guild.id}`, prefix); - message.prefix = String(setTo); - return message.channel.send(`✅ | Prefix **${message.prefix}** olarak ayarlandı.`); + if (prefix.length > 10 || /\s/.test(prefix)) return ctx.reply("Prefix 1-10 karakter arasında olmalı ve boşluk içeremez."); + ctx.client.db.set(`prefix:${ctx.guildId}`, prefix); + return ctx.reply(`Sunucunun prefix'i **${prefix}** olarak ayarlandı.`); }; -exports.help = { - name: "prefix", - aliases: ["setprefix", "önek", "onek"], - usage: "prefix [yeni-prefix]" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "prefix [yeni-prefix]" }; From 2b415cdd63498f2b7ba8988d5469789dd01da73f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:20:13 +0300 Subject: [PATCH 103/175] Rewrite search command natively --- commands/search.js | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/commands/search.js b/commands/search.js index 40a0c10..6f7f1ba 100644 --- a/commands/search.js +++ b/commands/search.js @@ -1,22 +1,20 @@ -exports.execute = async (client, message) => { - const places = ["Cep", "Tişört", "Sokak", "Eski bir sandık"]; - const amount = Math.floor(Math.random() * 200) + 50; - const reward = client.eco.beg(message.author.id, amount, { - canLose: true, - cooldown: 300000, - customName: "search" - }); +const { SlashCommandBuilder } = require("discord.js"); +const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); - if (reward.onCooldown) return message.reply(`${reward.time.minutes} dakika ${reward.time.seconds} saniye sonra tekrar dene.`); - if (reward.lost) { - return message.channel.send(`**${places[Math.floor(Math.random() * places.length)]}:** Yakalandın! Para bulamadın.`); - } +const places = ["Cebin", "Ceketin", "Sokak", "Eski bir dolap"]; - return message.reply(`**${places[Math.floor(Math.random() * places.length)]}** araması kârlı çıktı; **${reward.amount}** 💸 buldun. Artık **${reward.after}** 💸 paran var.`); -}; +exports.data = new SlashCommandBuilder().setName("search").setDescription("Bir yerde para arar."); +exports.name = "search"; +exports.aliases = ["ara", "arama"]; + +exports.execute = async (ctx) => { + const amount = randomInt(50, 249); + const result = ctx.client.eco.randomEarning(ctx.guildId, ctx.userId, "search", amount, { canLose: true, cooldown: 300_000 }); + if (result.onCooldown) return ctx.reply(`Tekrar arama yapabilmek için **${formatRemaining(result.remainingMs)}** beklemelisin.`); -exports.help = { - name: "search", - aliases: ["ara", "arama"], - usage: "search" + const place = places[Math.floor(Math.random() * places.length)]; + if (result.lost) return ctx.reply(`**${place}:** Bir şey bulamadın. Bir dahaki sefere daha şanslı olabilirsin.`); + return ctx.reply(`**${place}** araması kârlı çıktı; **${formatMoney(result.amount)}** buldun. Yeni bakiyen **${formatMoney(result.after)}**.`); }; + +exports.help = { name: exports.name, aliases: exports.aliases, usage: "search" }; From 6846b4172daa937869aac2b1b68c8ed1bd45583b Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:20:18 +0300 Subject: [PATCH 104/175] Rewrite rob command with atomic economy update --- commands/rob.js | 77 +++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/commands/rob.js b/commands/rob.js index 3049aed..ad1ef6d 100644 --- a/commands/rob.js +++ b/commands/rob.js @@ -1,46 +1,35 @@ -exports.execute = async (client, message, args) => { - const target = message.mentions.members.first() || message.guild.members.cache.get(args[0]); - if (!target) return message.reply("Kimi soymaya çalıştığını belirtmelisin."); - if (target.id === message.author.id) return message.reply("Kendini soyamazsın."); - if (target.user.bot) return message.reply("Bot hesapları soyulamaz."); - - const cooldownKey = `robCooldown_${message.author.id}`; - const now = Date.now(); - const lastAttempt = Number(client.db.fetch(cooldownKey) || 0); - const cooldown = 60_000; - const remaining = cooldown - (now - lastAttempt); - - if (remaining > 0) { - return message.reply(`Yakın zamanda bir soygun denedin. Tekrar denemek için **${Math.ceil(remaining / 1000)} saniye** beklemelisin.`); - } - - client.db.set(cooldownKey, now); - - const targetBalance = client.eco.fetchMoney(target.id).balance; - if (targetBalance < 1) return message.reply("Bu kullanıcının çalınabilecek parası yok."); - - const messages = [ - `${target} kullanıcısını soymaya çalışırken yakalandın!`, - `Sinsi davranmaya çalıştın ama ${target} fark etti!`, - `${target} kullanıcısını soyma girişimin başarısız oldu!` - ]; - - if (Math.floor(Math.random() * 5) === 0) { - return message.channel.send(messages[Math.floor(Math.random() * messages.length)]); - } - - const requestedAmount = Math.floor(Math.random() * 50) + 10; - const amount = Math.min(requestedAmount, targetBalance); - const result = client.eco.transfer(target.id, message.author.id, amount); - - if (result.error) return message.reply("Soygun gerçekleştirilemedi. Hedefin bakiyesi değişmiş olabilir."); - - const robberBalance = client.eco.fetchMoney(message.author.id).balance; - return message.reply(`${target} kullanıcısından **${amount}** 💸 çaldın. Artık **${robberBalance}** 💸 paran var.`); +const { SlashCommandBuilder } = require("discord.js"); +const { resolveUser, formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); + +const failMessages = [ + "Soygun girişimin başarısız oldu.", + "Hedef seni fark etti ve planın bozuldu.", + "Soymaya çalıştığın kişi dikkatliymiş. Bu sefer olmadı." +]; + +exports.data = new SlashCommandBuilder() + .setName("rob") + .setDescription("Başka bir kullanıcının bakiyesinden rastgele bir miktar çalmayı dener.") + .addUserOption((option) => option.setName("kullanici").setDescription("Hedef kullanıcı.").setRequired(true)); +exports.name = "rob"; +exports.aliases = ["soy", "soygun"]; + +exports.execute = async (ctx) => { + const target = await resolveUser(ctx, 0); + if (!target || target.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); + if (target.id === ctx.userId) return ctx.reply("Kendini soyamazsın."); + + const cooldown = ctx.client.eco.useCooldown(ctx.guildId, ctx.userId, "rob", 60_000); + if (cooldown.onCooldown) return ctx.reply(`Tekrar soygun deneyebilmek için **${formatRemaining(cooldown.remainingMs)}** beklemelisin.`); + + const targetBalance = ctx.client.eco.getBalance(ctx.guildId, target.id); + if (targetBalance < 1) return ctx.reply("Bu kullanıcının çalınabilecek parası yok."); + if (Math.random() < 0.2) return ctx.reply(failMessages[Math.floor(Math.random() * failMessages.length)]); + + const amount = Math.min(targetBalance, randomInt(10, 59)); + const result = ctx.client.eco.rob(ctx.guildId, ctx.userId, target.id, amount); + if (result.error) return ctx.reply(result.error); + return ctx.reply(`${target} kullanıcısından **${formatMoney(amount)}** çaldın. Yeni bakiyen **${formatMoney(ctx.client.eco.getBalance(ctx.guildId, ctx.userId))}**.`); }; -exports.help = { - name: "rob", - aliases: ["soy", "soygun"], - usage: "rob " -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "rob " }; From 139a520763b5089a5f78c1b39651bda9873aaaf0 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:21:05 +0300 Subject: [PATCH 105/175] Pin SQLite dependency for Node 20 compatibility --- package.json | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index d299e40..f8d436e 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,17 @@ { "name": "EconomyBot", - "version": "2.0.0", - "description": "Discord ekonomi botu - Node.js 20+ ve discord.js v14 uyumlu", + "version": "3.0.0", + "description": "Profesyonel Discord ekonomi botu - Node.js 20+ ve discord.js v14", "main": "index.js", "scripts": { "start": "node index.js", - "test": "node tests/smoke.js" + "test": "node tests/test.js" }, "author": "Zero / LoiFragola", "license": "Nginx", - "keywords": [ - "discord", - "economy", - "bot", - "discordjs", - "discord.js" - ], + "keywords": ["discord", "economy", "bot", "discordjs", "discord.js"], "dependencies": { - "better-sqlite3": "13.0.3", + "better-sqlite3": "12.11.1", "discord.js": "14.27.0" }, "engines": { From c21a626dc6d561fd4a4053d15b87f3fe8add8f33 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:21:16 +0300 Subject: [PATCH 106/175] Remove duplicate slash command registry --- slashCommands.js | 75 ------------------------------------------------ 1 file changed, 75 deletions(-) delete mode 100644 slashCommands.js diff --git a/slashCommands.js b/slashCommands.js deleted file mode 100644 index 36eee60..0000000 --- a/slashCommands.js +++ /dev/null @@ -1,75 +0,0 @@ -const { SlashCommandBuilder } = require("discord.js"); - -const integerAmount = (option, description, minValue = 1) => - option - .setDescription(description) - .setRequired(true) - .setMinValue(minValue) - .setMaxValue(2147483647); - -module.exports = [ - new SlashCommandBuilder() - .setName("addmoney") - .setDescription("Bir kullanıcıya para ekler.") - .addUserOption((option) => option.setName("kullanici").setDescription("Para eklenecek kullanıcı.").setRequired(true)) - .addIntegerOption((option) => integerAmount(option.setName("miktar"), "Eklenecek para miktarı.")), - - new SlashCommandBuilder() - .setName("bal") - .setDescription("Kullanıcının bakiyesini gösterir.") - .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi görüntülenecek kullanıcı.").setRequired(false)), - - new SlashCommandBuilder().setName("beg").setDescription("Dilencilik yaparak para kazanmaya çalışır."), - - new SlashCommandBuilder() - .setName("buy") - .setDescription("Mağazadan bir ürün satın alır.") - .addStringOption((option) => - option - .setName("urun") - .setDescription("Satın almak istediğin ürün.") - .setRequired(true) - .addChoices( - { name: "Laptop", value: "Laptop" }, - { name: "Mobile", value: "Mobile" }, - { name: "PC", value: "PC" } - ) - ), - - new SlashCommandBuilder().setName("daily").setDescription("Günlük para ödülünü alır."), - new SlashCommandBuilder().setName("help").setDescription("Botun komutlarını gösterir."), - new SlashCommandBuilder().setName("inventory").setDescription("Envanterini gösterir."), - new SlashCommandBuilder().setName("lb").setDescription("Ekonomi sıralamasını gösterir."), - new SlashCommandBuilder().setName("ping").setDescription("Botun gecikmesini gösterir."), - - new SlashCommandBuilder() - .setName("prefix") - .setDescription("Sunucunun prefix'ini değiştirir veya sıfırlar.") - .addStringOption((option) => - option.setName("yeni_prefix").setDescription("Yeni prefix. Boş bırakırsan varsayılana döner.").setRequired(false).setMaxLength(10) - ), - - new SlashCommandBuilder() - .setName("rob") - .setDescription("Başka bir kullanıcının parasını çalmayı dener.") - .addUserOption((option) => option.setName("kullanici").setDescription("Hedef kullanıcı.").setRequired(true)), - - new SlashCommandBuilder().setName("search").setDescription("Bir yerde para arar."), - - new SlashCommandBuilder() - .setName("setmoney") - .setDescription("Bir kullanıcının bakiyesini ayarlar.") - .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi ayarlanacak kullanıcı.").setRequired(true)) - .addIntegerOption((option) => integerAmount(option.setName("miktar"), "Yeni bakiye miktarı.")), - - new SlashCommandBuilder().setName("shop").setDescription("Mağazayı gösterir."), - - new SlashCommandBuilder() - .setName("transfer") - .setDescription("Başka bir kullanıcıya para gönderir.") - .addUserOption((option) => option.setName("kullanici").setDescription("Para gönderilecek kullanıcı.").setRequired(true)) - .addIntegerOption((option) => integerAmount(option.setName("miktar"), "Gönderilecek para miktarı.")), - - new SlashCommandBuilder().setName("weekly").setDescription("Haftalık para ödülünü alır."), - new SlashCommandBuilder().setName("work").setDescription("Çalışarak para kazanır.") -]; From f64fbd0daedbad2d0c9df4b1f4418a6f85ab98f4 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:21:21 +0300 Subject: [PATCH 107/175] Remove token-leaking Discord debug logger --- events/debug.js | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 events/debug.js diff --git a/events/debug.js b/events/debug.js deleted file mode 100644 index 99832ba..0000000 --- a/events/debug.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = (client, debug) => { - if (!client.config.debug) return; - else console.log(debug); -}; From 3e9c0d31106d1329a2618c21d8818ac901cf1b3f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:21:26 +0300 Subject: [PATCH 108/175] Remove unused duplicate database implementation --- database.js | 252 ---------------------------------------------------- 1 file changed, 252 deletions(-) delete mode 100644 database.js diff --git a/database.js b/database.js deleted file mode 100644 index 9a6c425..0000000 --- a/database.js +++ /dev/null @@ -1,252 +0,0 @@ -const fs = require("node:fs"); -const path = require("node:path"); -const Database = require("better-sqlite3"); - -const dataDir = path.join(__dirname, "data"); -fs.mkdirSync(dataDir, { recursive: true }); - -const db = new Database(path.join(dataDir, "economy.sqlite")); -db.pragma("journal_mode = WAL"); -db.pragma("foreign_keys = ON"); -db.pragma("synchronous = NORMAL"); - -db.exec(` - CREATE TABLE IF NOT EXISTS users ( - guild_id TEXT NOT NULL, - user_id TEXT NOT NULL, - balance INTEGER NOT NULL DEFAULT 0 CHECK(balance >= 0), - PRIMARY KEY (guild_id, user_id) - ); - - CREATE TABLE IF NOT EXISTS cooldowns ( - guild_id TEXT NOT NULL, - user_id TEXT NOT NULL, - command TEXT NOT NULL, - expires_at INTEGER NOT NULL, - PRIMARY KEY (guild_id, user_id, command) - ); - - CREATE TABLE IF NOT EXISTS inventory ( - guild_id TEXT NOT NULL, - user_id TEXT NOT NULL, - item_id TEXT NOT NULL, - item_name TEXT NOT NULL, - unit_price INTEGER NOT NULL CHECK(unit_price >= 0), - quantity INTEGER NOT NULL DEFAULT 0 CHECK(quantity >= 0), - PRIMARY KEY (guild_id, user_id, item_id), - FOREIGN KEY (guild_id, user_id) REFERENCES users(guild_id, user_id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS guild_settings ( - guild_id TEXT PRIMARY KEY, - prefix TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS counters ( - guild_id TEXT PRIMARY KEY, - number INTEGER NOT NULL DEFAULT 0 CHECK(number >= 0), - last_user_id TEXT - ); -`); - -const statements = { - ensureUser: db.prepare(` - INSERT INTO users (guild_id, user_id, balance) - VALUES (@guildId, @userId, 0) - ON CONFLICT(guild_id, user_id) DO NOTHING - `), - getBalance: db.prepare(` - SELECT balance FROM users WHERE guild_id = ? AND user_id = ? - `), - setBalance: db.prepare(` - UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ? - `), - leaderboard: db.prepare(` - SELECT user_id, balance - FROM users - WHERE guild_id = ? - ORDER BY balance DESC, user_id ASC - LIMIT ? - `), - countAhead: db.prepare(` - SELECT COUNT(*) AS count - FROM users - WHERE guild_id = ? AND balance > ? - `), - getCooldown: db.prepare(` - SELECT expires_at FROM cooldowns - WHERE guild_id = ? AND user_id = ? AND command = ? - `), - setCooldown: db.prepare(` - INSERT INTO cooldowns (guild_id, user_id, command, expires_at) - VALUES (?, ?, ?, ?) - ON CONFLICT(guild_id, user_id, command) - DO UPDATE SET expires_at = excluded.expires_at - `), - getInventory: db.prepare(` - SELECT item_id, item_name, unit_price, quantity - FROM inventory - WHERE guild_id = ? AND user_id = ? - ORDER BY item_name COLLATE NOCASE - `), - getItem: db.prepare(` - SELECT item_id, item_name, unit_price, quantity - FROM inventory - WHERE guild_id = ? AND user_id = ? AND item_id = ? - `), - upsertInventory: db.prepare(` - INSERT INTO inventory (guild_id, user_id, item_id, item_name, unit_price, quantity) - VALUES (@guildId, @userId, @itemId, @itemName, @unitPrice, @quantity) - ON CONFLICT(guild_id, user_id, item_id) - DO UPDATE SET quantity = inventory.quantity + excluded.quantity, - item_name = excluded.item_name, - unit_price = excluded.unit_price - `), - getPrefix: db.prepare(`SELECT prefix FROM guild_settings WHERE guild_id = ?`), - setPrefix: db.prepare(` - INSERT INTO guild_settings (guild_id, prefix) VALUES (?, ?) - ON CONFLICT(guild_id) DO UPDATE SET prefix = excluded.prefix - `), - resetPrefix: db.prepare(`DELETE FROM guild_settings WHERE guild_id = ?`), - getCounter: db.prepare(`SELECT number, last_user_id FROM counters WHERE guild_id = ?`), - setCounter: db.prepare(` - INSERT INTO counters (guild_id, number, last_user_id) VALUES (?, ?, ?) - ON CONFLICT(guild_id) DO UPDATE SET number = excluded.number, last_user_id = excluded.last_user_id - `) -}; - -function ensureUser(guildId, userId) { - statements.ensureUser.run({ guildId: String(guildId), userId: String(userId) }); -} - -function getBalance(guildId, userId) { - ensureUser(guildId, userId); - return Number(statements.getBalance.get(String(guildId), String(userId)).balance); -} - -function setBalance(guildId, userId, amount) { - const value = Number(amount); - if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("Bakiye geçerli bir pozitif tam sayı olmalıdır."); - ensureUser(guildId, userId); - statements.setBalance.run(value, String(guildId), String(userId)); - return value; -} - -function addBalance(guildId, userId, amount) { - const value = Number(amount); - if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError("Miktar pozitif bir tam sayı olmalıdır."); - const next = getBalance(guildId, userId) + value; - if (!Number.isSafeInteger(next)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); - setBalance(guildId, userId, next); - return next; -} - -function removeBalance(guildId, userId, amount) { - const value = Number(amount); - if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError("Miktar pozitif bir tam sayı olmalıdır."); - const current = getBalance(guildId, userId); - if (current < value) return false; - setBalance(guildId, userId, current - value); - return true; -} - -const transferTransaction = db.transaction((guildId, fromUserId, toUserId, amount) => { - ensureUser(guildId, fromUserId); - ensureUser(guildId, toUserId); - const from = Number(statements.getBalance.get(guildId, fromUserId).balance); - if (from < amount) return { ok: false, fromBalance: from, toBalance: Number(statements.getBalance.get(guildId, toUserId).balance) }; - const to = Number(statements.getBalance.get(guildId, toUserId).balance); - if (!Number.isSafeInteger(to + amount)) throw new RangeError("Hedef bakiye güvenli sayı sınırını aşıyor."); - statements.setBalance.run(from - amount, guildId, fromUserId); - statements.setBalance.run(to + amount, guildId, toUserId); - return { ok: true, fromBalance: from - amount, toBalance: to + amount }; -}); - -function transfer(guildId, fromUserId, toUserId, amount) { - const value = Number(amount); - if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError("Miktar pozitif bir tam sayı olmalıdır."); - if (String(fromUserId) === String(toUserId)) return { ok: false, reason: "self" }; - return transferTransaction(String(guildId), String(fromUserId), String(toUserId), value); -} - -function claimCooldown(guildId, userId, command, durationMs, now = Date.now()) { - const row = statements.getCooldown.get(String(guildId), String(userId), String(command)); - const expiresAt = row ? Number(row.expires_at) : 0; - if (expiresAt > now) return { onCooldown: true, remainingMs: expiresAt - now }; - const next = now + durationMs; - statements.setCooldown.run(String(guildId), String(userId), String(command), next); - return { onCooldown: false, remainingMs: 0 }; -} - -function getLeaderboard(guildId, limit = 15) { - const rows = statements.leaderboard.all(String(guildId), limit); - return rows.map((row, index) => ({ position: index + 1, userId: row.user_id, balance: Number(row.balance) })); -} - -function getPosition(guildId, userId) { - const balance = getBalance(guildId, userId); - return Number(statements.countAhead.get(String(guildId), balance).count) + 1; -} - -function addItem(guildId, userId, item) { - ensureUser(guildId, userId); - statements.upsertInventory.run({ - guildId: String(guildId), - userId: String(userId), - itemId: String(item.id), - itemName: String(item.name), - unitPrice: Number(item.price), - quantity: 1 - }); -} - -function getInventory(guildId, userId) { - ensureUser(guildId, userId); - return statements.getInventory.all(String(guildId), String(userId)); -} - -function getInventoryItem(guildId, userId, itemId) { - return statements.getItem.get(String(guildId), String(userId), String(itemId)) || null; -} - -function getPrefix(guildId, defaultPrefix) { - return statements.getPrefix.get(String(guildId))?.prefix || defaultPrefix; -} - -function setPrefix(guildId, prefix) { - statements.setPrefix.run(String(guildId), String(prefix)); - return String(prefix); -} - -function resetPrefix(guildId) { - statements.resetPrefix.run(String(guildId)); -} - -function getCounter(guildId) { - return statements.getCounter.get(String(guildId)) || { number: 0, last_user_id: null }; -} - -function setCounter(guildId, number, lastUserId) { - statements.setCounter.run(String(guildId), Number(number), lastUserId ? String(lastUserId) : null); -} - -module.exports = { - db, - ensureUser, - getBalance, - setBalance, - addBalance, - removeBalance, - transfer, - claimCooldown, - getLeaderboard, - getPosition, - addItem, - getInventory, - getInventoryItem, - getPrefix, - setPrefix, - resetPrefix, - getCounter, - setCounter -}; From 39847f4c9791fac2d9b95ba3ee742bf490625c48 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:21:45 +0300 Subject: [PATCH 109/175] Add comprehensive offline test suite --- tests/test.js | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 tests/test.js diff --git a/tests/test.js b/tests/test.js new file mode 100644 index 0000000..77d11a7 --- /dev/null +++ b/tests/test.js @@ -0,0 +1,187 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { Collection } = require("discord.js"); +const { KeyValueStore } = require("../lib/database"); +const EconomyManager = require("../lib/economy"); + +const projectRoot = path.join(__dirname, ".."); +const commandFiles = fs.readdirSync(path.join(projectRoot, "commands")).filter((file) => file.endsWith(".js")).sort(); +assert.equal(commandFiles.length, 17, "Tam olarak 17 komut olmalı."); + +const commands = commandFiles.map((file) => ({ file, command: require(path.join(projectRoot, "commands", file)) })); +const names = new Set(); +const slashNames = new Set(); + +for (const { file, command } of commands) { + assert.equal(typeof command.execute, "function", `${file}: execute eksik.`); + assert.equal(typeof command.data?.toJSON, "function", `${file}: SlashCommandBuilder eksik.`); + assert.equal(typeof command.help?.name, "string", `${file}: help.name eksik.`); + assert.ok(Array.isArray(command.help?.aliases), `${file}: aliases dizi değil.`); + assert.ok(typeof command.help?.usage === "string" && command.help.usage.length > 0, `${file}: usage eksik.`); + assert.equal(command.help.name, command.name, `${file}: help.name ve name farklı.`); + assert.ok(!names.has(command.name), `${file}: yinelenen komut adı.`); + names.add(command.name); + + const json = command.data.toJSON(); + assert.equal(json.name, command.name, `${file}: slash adı legacy adından farklı.`); + assert.match(json.name, /^[a-z0-9_-]{1,32}$/); + assert.ok(json.description.length >= 1 && json.description.length <= 100); + assert.ok(!slashNames.has(json.name), `${file}: yinelenen slash komutu.`); + slashNames.add(json.name); +} +assert.equal(names.size, 17); +assert.equal(slashNames.size, 17); + +for (const { command } of commands) { + for (const alias of command.aliases || []) { + assert.equal(typeof alias, "string"); + assert.ok(alias.length > 0 && alias.length <= 32); + assert.ok(!names.has(alias.toLowerCase()), `Alias gerçek komut adıyla çakışıyor: ${alias}`); + } +} + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "economybot-test-")); +const tempDb = new KeyValueStore(path.join(tempDir, "test.sqlite")); +const eco = new EconomyManager(tempDb); +const guild = "123456789012345678"; +const userA = "123456789012345679"; +const userB = "123456789012345680"; + +try { + assert.equal(eco.getBalance(guild, userA), 0); + assert.equal(eco.addMoney(guild, userA, 1000).after, 1000); + assert.equal(eco.setMoney(guild, userA, 1500).after, 1500); + assert.equal(eco.removeMoney(guild, userA, 500).after, 1000); + assert.equal(eco.removeMoney(guild, userA, 2000).error, "Yetersiz bakiye."); + + const transfer = eco.transfer(guild, userA, userB, 300); + assert.equal(transfer.error, undefined); + assert.equal(eco.getBalance(guild, userA), 700); + assert.equal(eco.getBalance(guild, userB), 300); + assert.equal(eco.transfer(guild, userA, userA, 1).error, "Kendine para gönderemezsin."); + + const daily1 = eco.daily(guild, userA, 100); + assert.equal(daily1.onCooldown, false); + const daily2 = eco.daily(guild, userA, 100); + assert.equal(daily2.onCooldown, true); + assert.ok(daily2.remainingMs > 0); + + const purchase = eco.purchase(guild, userA, { id: "laptop", name: "Laptop", cost: 200 }); + assert.equal(purchase.error, null); + assert.equal(eco.getBalance(guild, userA), 600); + assert.equal(eco.getInventory(guild, userA).length, 1); + + const insufficientPurchase = eco.purchase(guild, userA, { id: "pc", name: "PC", cost: 10_000 }); + assert.equal(insufficientPurchase.error, "Yetersiz bakiye."); + assert.equal(eco.getInventory(guild, userA).length, 1); + + const leaderboard = eco.leaderboard(guild, 15); + assert.equal(leaderboard[0].id, userB); + assert.equal(eco.getPosition(guild, userB), 1); +} finally { + tempDb.close(); + fs.rmSync(tempDir, { recursive: true, force: true }); +} + +function makeUser(id, tag = `User${id}`) { + return { id, tag, username: tag.split("#")[0], bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }; +} + +function makeContext(client, userId, { options = {}, args = [], slash = true } = {}) { + const user = client.users.cache.get(userId) || makeUser(userId); + const interaction = { + createdTimestamp: Date.now(), + options: { + getUser: (name) => options[name] instanceof Object ? options[name] : null, + getInteger: (name) => options[name] ?? null, + getString: (name) => options[name] ?? null, + getMember: () => null + } + }; + const replies = []; + return { + client, + interaction, + isSlash: slash, + guild: { id: guild, name: "Test Guild", iconURL: () => null }, + guildId: guild, + user, + userId, + member: { permissions: { has: () => true } }, + args, + message: null, + replies, + userOption: (name) => interaction.options.getUser(name), + async reply(payload) { + replies.push(payload); + return payload; + } + }; +} + +const mockStore = new KeyValueStore(path.join(os.tmpdir(), `economybot-command-${Date.now()}-${Math.random().toString(16).slice(2)}.sqlite`)); +const client = { + config: { prefix: "!", admins: [userA] }, + db: mockStore, + eco: new EconomyManager(mockStore), + commands: new Collection(), + aliases: new Collection(), + ws: { ping: 42 }, + user: makeUser("123456789012345681", "EconomyBot"), + users: { cache: new Collection() }, + shop: { + laptop: { id: "laptop", name: "Laptop", cost: 2000 }, + mobile: { id: "mobile", name: "Mobile", cost: 1000 }, + pc: { id: "pc", name: "PC", cost: 3000 } + } +}; +for (const { command } of commands) { + client.commands.set(command.name, command); + for (const alias of command.aliases || []) client.aliases.set(alias.toLowerCase(), command.name); +} +client.users.cache.set(userA, makeUser(userA, "Admin#0001")); +client.users.cache.set(userB, makeUser(userB, "Target#0001")); +client.users.cache.set("123456789012345681", client.user); +client.eco.setMoney(guild, userA, 10_000); +client.eco.setMoney(guild, userB, 5_000); + +async function runCommand(name, userId = userA, options = {}) { + const command = client.commands.get(name); + const ctx = makeContext(client, userId, { options }); + await command.execute(ctx); + assert.ok(ctx.replies.length > 0, `${name}: yanıt üretilmedi.`); + return ctx.replies.at(-1); +} + +(async () => { + await runCommand("bal", userA); + await runCommand("beg", userA); + await runCommand("buy", userA, { urun: "laptop" }); + await runCommand("daily", userB); + await runCommand("help", userA); + await runCommand("inventory", userA); + await runCommand("lb", userA); + await runCommand("ping", userA); + await runCommand("prefix", userA, { yeni_prefix: "$" }); + await runCommand("rob", userA, { kullanici: client.users.cache.get(userB) }); + await runCommand("search", userB); + await runCommand("setmoney", userA, { kullanici: client.users.cache.get(userB), miktar: 2500 }); + await runCommand("shop", userA); + await runCommand("transfer", userA, { kullanici: client.users.cache.get(userB), miktar: 100 }); + await runCommand("weekly", userB); + await runCommand("work", userA); + + assert.equal(client.db.getPrefix(guild, "!"), "$", "Prefix kalıcı olarak kaydedilmedi."); + + mockStore.close(); + fs.rmSync(mockStore.connection.name, { force: true }); + fs.rmSync(`${mockStore.connection.name}-wal`, { force: true }); + fs.rmSync(`${mockStore.connection.name}-shm`, { force: true }); + console.log("Tam test başarılı: 17 komut, slash şemaları, ekonomi işlemleri, atomik transfer, mağaza, envanter, cooldown, prefix ve tüm komut yürütmeleri doğrulandı."); +})().catch((error) => { + try { mockStore.close(); } catch {} + console.error(error); + process.exitCode = 1; +}); From 80b37902cbc6b945c501d9d32e133f657aef83b6 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:22:42 +0300 Subject: [PATCH 110/175] Add guild prefix storage helpers --- lib/database.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/database.js b/lib/database.js index cc65445..45c26e4 100644 --- a/lib/database.js +++ b/lib/database.js @@ -11,24 +11,17 @@ class KeyValueStore { this.connection.pragma("journal_mode = WAL"); this.connection.pragma("foreign_keys = ON"); this.connection.pragma("synchronous = NORMAL"); - this.connection.exec(` CREATE TABLE IF NOT EXISTS kv ( id TEXT PRIMARY KEY, value TEXT NOT NULL ); - CREATE INDEX IF NOT EXISTS idx_kv_id ON kv(id); `); - this.statements = { get: this.connection.prepare("SELECT value FROM kv WHERE id = ?"), - set: this.connection.prepare(` - INSERT INTO kv (id, value) VALUES (?, ?) - ON CONFLICT(id) DO UPDATE SET value = excluded.value - `), + set: this.connection.prepare(`INSERT INTO kv (id, value) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value`), delete: this.connection.prepare("DELETE FROM kv WHERE id = ?"), all: this.connection.prepare("SELECT id, value FROM kv ORDER BY id"), - prefix: this.connection.prepare("SELECT id, value FROM kv WHERE id LIKE ? ORDER BY id"), clear: this.connection.prepare("DELETE FROM kv") }; } @@ -78,10 +71,21 @@ class KeyValueStore { } startsWith(prefix) { - const escaped = String(prefix).replace(/[\\%_]/g, "\\$&"); - return this.statements.prefix - .all(`${escaped}%`) - .map(({ id, value }) => ({ ID: id, data: this.decode(value) })); + const normalized = String(prefix); + return this.all().filter(({ ID }) => ID.startsWith(normalized)); + } + + getPrefix(guildId, fallback) { + return String(this.get(`prefix:${guildId}`, fallback)); + } + + setPrefix(guildId, prefix) { + this.set(`prefix:${guildId}`, String(prefix)); + return String(prefix); + } + + resetPrefix(guildId) { + return this.delete(`prefix:${guildId}`); } clear() { From 132cf6ece5360fe2ef1bd90673ef0da4af216e69 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:23:16 +0300 Subject: [PATCH 111/175] Fix slash deployment registry --- events/clientReady.js | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/events/clientReady.js b/events/clientReady.js index d1f0e72..50d02c5 100644 --- a/events/clientReady.js +++ b/events/clientReady.js @@ -1,28 +1,18 @@ const { REST, Routes } = require("discord.js"); -const slashCommands = require("../slashCommands"); module.exports = async (client) => { console.log(`${client.user.tag} çevrimiçi!`); - client.user.setActivity("LoiFragola Economy"); + client.user.setActivity({ name: "EconomyBot" }); - const serverId = client.config.serverId; - if (!serverId || serverId === "YOUR_SERVER_ID") { - console.warn("Slash komutları deploy edilmedi: botConfig.js içindeki serverId ayarlanmalı."); - return; - } - - if (!/^\d{17,20}$/.test(String(serverId))) { - console.warn("Slash komutları deploy edilmedi: serverId geçerli bir Discord sunucu ID'si değil."); - return; - } + const commands = [...client.commands.values()].map((command) => command.data.toJSON()); try { const rest = new REST({ version: "10" }).setToken(client.config.token); - await rest.put(Routes.applicationGuildCommands(client.user.id, serverId), { - body: slashCommands.map((command) => command.toJSON()) - }); - - console.log(`${slashCommands.length} slash komutu sunucuya başarıyla deploy edildi.`); + await rest.put( + Routes.applicationGuildCommands(client.user.id, String(client.config.serverId)), + { body: commands } + ); + console.log(`${commands.length} slash komutu sunucuya başarıyla deploy edildi.`); } catch (error) { console.error("Slash komutları deploy edilirken hata oluştu:", error); } From 9dcf4d80f81cb277c61e0940521a3c1954e6fdff Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:23:29 +0300 Subject: [PATCH 112/175] Rewrite counter to use local database --- counter.js | 52 +++++++++++++++++++--------------------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/counter.js b/counter.js index bde6343..7e13b4b 100644 --- a/counter.js +++ b/counter.js @@ -1,43 +1,29 @@ -function counter(message, client) { - const channel = message.channel; - let count = client.db.fetch(`counter_${message.guild.id}`); - - if (!count || typeof count !== "object") { - count = { number: 0, author: null }; - client.db.set(`counter_${message.guild.id}`, count); +async function counter(message, client) { + const key = `counter:${message.guild.id}`; + let state = client.db.get(key, { number: 0, author: null }); + if (!state || typeof state !== "object" || !Number.isSafeInteger(state.number) || state.number < 0) { + state = { number: 0, author: null }; } - if (message.author.id === count.author) { - message.delete().catch(() => {}); - message.reply("Sıra sende değil, lütfen bekle.") - .then((reply) => setTimeout(() => reply.delete().catch(() => {}), 3000)) - .catch(() => {}); - return; - } + const reject = async (text) => { + await message.delete().catch(() => {}); + const reply = await message.reply(text).catch(() => null); + if (reply) setTimeout(() => reply.delete().catch(() => {}), 3000); + return false; + }; - if (!/^\d+$/.test(message.content)) { - message.delete().catch(() => {}); - message.reply("Bu kanaldaki mesajlar sayı olmalıdır.") - .then((reply) => setTimeout(() => reply.delete().catch(() => {}), 3000)) - .catch(() => {}); - return; - } + if (message.author.id === state.author) return reject("Sıra sende değil, lütfen başka birinin sayı yazmasını bekle."); + if (!/^\d+$/.test(message.content)) return reject("Bu kanaldaki mesajlar sayı olmalıdır."); const number = Number(message.content); - if (!Number.isSafeInteger(number) || number !== count.number + 1) { - message.delete().catch(() => {}); - message.reply(`Sıradaki sayı ${count.number + 1} olmalıdır.`) - .then((reply) => setTimeout(() => reply.delete().catch(() => {}), 3000)) - .catch(() => {}); - return; + if (!Number.isSafeInteger(number) || number !== state.number + 1) { + return reject(`Sıradaki sayı **${state.number + 1}** olmalıdır.`); } - const next = { - number, - author: message.author.id - }; - client.db.set(`counter_${message.guild.id}`, next); - channel.setTopic(`Sıradaki sayı ${number + 1} olmalıdır.`).catch(() => {}); + const next = { number, author: message.author.id }; + client.db.set(key, next); + await message.channel.setTopic(`Sıradaki sayı ${number + 1} olmalıdır.`).catch(() => {}); + return true; } module.exports = counter; From b0b6faebc052b21840288feea951c4967744e510 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:23:44 +0300 Subject: [PATCH 113/175] Harden cooldown and leaderboard logic --- lib/economy.js | 57 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/lib/economy.js b/lib/economy.js index 98e29c2..51c6026 100644 --- a/lib/economy.js +++ b/lib/economy.js @@ -88,7 +88,7 @@ class EconomyManager { } purchase(guildId, userId, item) { - if (!item || !item.id || !item.name || !Number.isSafeInteger(item.cost) || item.cost < 0) { + if (!item || typeof item.id !== "string" || typeof item.name !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) { throw new TypeError("Geçersiz mağaza ürünü."); } @@ -98,8 +98,8 @@ class EconomyManager { const key = this.inventoryKey(guildId, userId); const inventory = this.db.get(key, []); if (!Array.isArray(inventory)) throw new TypeError("Envanter verisi bozuk."); - inventory.push({ id: item.id, name: item.name, price: item.cost, purchasedAt: Date.now() }); const after = balance - item.cost; + inventory.push({ id: item.id, name: item.name, price: item.cost, purchasedAt: Date.now() }); this.db.set(this.moneyKey(guildId, userId), after); this.db.set(key, inventory); return { error: null, after, item }; @@ -111,43 +111,60 @@ class EconomyManager { return Array.isArray(inventory) ? inventory : []; } - useCooldown(guildId, userId, command, durationMs) { + getCooldown(guildId, userId, command, durationMs) { if (!Number.isSafeInteger(durationMs) || durationMs <= 0) throw new RangeError("Geçersiz cooldown süresi."); - const now = Date.now(); - const key = this.cooldownKey(guildId, userId, command); - const last = Number(this.db.get(key, 0)); - const remainingMs = durationMs - (now - last); - if (last > 0 && remainingMs > 0) return { onCooldown: true, remainingMs }; - this.db.set(key, now); - return { onCooldown: false, remainingMs: 0 }; + const last = Number(this.db.get(this.cooldownKey(guildId, userId, command), 0)); + const remainingMs = durationMs - (Date.now() - last); + return last > 0 && remainingMs > 0 ? { onCooldown: true, remainingMs } : { onCooldown: false, remainingMs: 0 }; + } + + setCooldown(guildId, userId, command) { + this.db.set(this.cooldownKey(guildId, userId, command), Date.now()); + } + + useCooldown(guildId, userId, command, durationMs) { + const status = this.getCooldown(guildId, userId, command, durationMs); + if (!status.onCooldown) this.setCooldown(guildId, userId, command); + return status; } daily(guildId, userId, amount) { - const cooldown = this.useCooldown(guildId, userId, "daily", DAY); + const cooldown = this.getCooldown(guildId, userId, "daily", DAY); if (cooldown.onCooldown) return cooldown; - return { ...cooldown, ...this.addMoney(guildId, userId, amount) }; + const result = this.addMoney(guildId, userId, amount); + this.setCooldown(guildId, userId, "daily"); + return result; } weekly(guildId, userId, amount) { - const cooldown = this.useCooldown(guildId, userId, "weekly", WEEK); + const cooldown = this.getCooldown(guildId, userId, "weekly", WEEK); if (cooldown.onCooldown) return cooldown; - return { ...cooldown, ...this.addMoney(guildId, userId, amount) }; + const result = this.addMoney(guildId, userId, amount); + this.setCooldown(guildId, userId, "weekly"); + return result; } work(guildId, userId, amount, options = {}) { const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 2_700_000; - const cooldown = this.useCooldown(guildId, userId, "work", cooldownMs); + const cooldown = this.getCooldown(guildId, userId, "work", cooldownMs); if (cooldown.onCooldown) return cooldown; + const result = this.addMoney(guildId, userId, amount); + this.setCooldown(guildId, userId, "work"); const jobs = Array.isArray(options.jobs) && options.jobs.length ? options.jobs : ["Geliştirici", "Doktor", "Öğretmen", "Müzisyen", "Madenci", "Mühendis", "Tasarımcı", "Yayıncı"]; - return { ...cooldown, ...this.addMoney(guildId, userId, amount), workedAs: jobs[Math.floor(Math.random() * jobs.length)] }; + return { ...result, workedAs: jobs[Math.floor(Math.random() * jobs.length)] }; } randomEarning(guildId, userId, command, amount, options = {}) { const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 60_000; - const cooldown = this.useCooldown(guildId, userId, command, cooldownMs); + const cooldown = this.getCooldown(guildId, userId, command, cooldownMs); if (cooldown.onCooldown) return cooldown; - if (options.canLose && Math.random() < 0.2) return { ...cooldown, lost: true, amount: 0, after: this.getBalance(guildId, userId) }; - return { ...cooldown, lost: false, ...this.addMoney(guildId, userId, amount) }; + if (options.canLose && Math.random() < 0.2) { + this.setCooldown(guildId, userId, command); + return { onCooldown: false, lost: true, amount: 0, after: this.getBalance(guildId, userId) }; + } + const result = this.addMoney(guildId, userId, amount); + this.setCooldown(guildId, userId, command); + return { onCooldown: false, lost: false, ...result }; } rob(guildId, robberId, targetId, amount) { @@ -167,7 +184,7 @@ class EconomyManager { getPosition(guildId, userId) { const balance = this.getBalance(guildId, userId); - return this.db.startsWith(`money:${guildId}:`).filter(({ data }) => Number(data) > balance).length + 1; + return this.db.startsWith(`money:${guildId}:`).filter(({ ID, data }) => /^money:\d{17,20}:\d{17,20}$/.test(ID) && Number.isSafeInteger(Number(data)) && Number(data) > balance).length + 1; } } From 369a5088772be2b451509aa89d30eb0ca822f584 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:23:56 +0300 Subject: [PATCH 114/175] Update CI for rebuilt command architecture --- .github/workflows/node-compatibility.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/node-compatibility.yml b/.github/workflows/node-compatibility.yml index 272d55d..8c73890 100644 --- a/.github/workflows/node-compatibility.yml +++ b/.github/workflows/node-compatibility.yml @@ -2,7 +2,9 @@ name: Node.js Compatibility on: push: - branches: ["main"] + branches: + - main + - professional-v14-rebuild pull_request: branches: ["main"] @@ -29,10 +31,12 @@ jobs: - name: JavaScript sözdizimini kontrol et shell: bash run: | - set -e - for file in index.js counter.js commands/*.js events/*.js slashCommands.js tests/*.js; do - node --check "$file" - done + set -euo pipefail + find . -type f -name '*.js' \ + -not -path './node_modules/*' \ + -print0 | while IFS= read -r -d '' file; do + node --check "$file" + done - - name: Smoke testlerini çalıştır + - name: Testleri çalıştır run: npm test From cfae7672c25490f38b0efe299ea0594dcf996d57 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:24:32 +0300 Subject: [PATCH 115/175] Replace smoke test with full native command suite --- tests/test.js | 305 ++++++++++++++++++++++++++------------------------ 1 file changed, 156 insertions(+), 149 deletions(-) diff --git a/tests/test.js b/tests/test.js index 77d11a7..ddf649a 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5,183 +5,190 @@ const path = require("node:path"); const { Collection } = require("discord.js"); const { KeyValueStore } = require("../lib/database"); const EconomyManager = require("../lib/economy"); +const { createInteractionContext, createPrefixContext } = require("../lib/context"); -const projectRoot = path.join(__dirname, ".."); -const commandFiles = fs.readdirSync(path.join(projectRoot, "commands")).filter((file) => file.endsWith(".js")).sort(); -assert.equal(commandFiles.length, 17, "Tam olarak 17 komut olmalı."); - -const commands = commandFiles.map((file) => ({ file, command: require(path.join(projectRoot, "commands", file)) })); -const names = new Set(); -const slashNames = new Set(); +const root = path.join(__dirname, ".."); +const commandFiles = fs.readdirSync(path.join(root, "commands")).filter((file) => file.endsWith(".js")).sort(); +assert.equal(commandFiles.length, 17, "Tam olarak 17 komut dosyası bulunmalı."); +const commands = commandFiles.map((file) => ({ file, command: require(path.join(root, "commands", file)) })); +const commandMap = new Map(); for (const { file, command } of commands) { assert.equal(typeof command.execute, "function", `${file}: execute eksik.`); - assert.equal(typeof command.data?.toJSON, "function", `${file}: SlashCommandBuilder eksik.`); - assert.equal(typeof command.help?.name, "string", `${file}: help.name eksik.`); - assert.ok(Array.isArray(command.help?.aliases), `${file}: aliases dizi değil.`); - assert.ok(typeof command.help?.usage === "string" && command.help.usage.length > 0, `${file}: usage eksik.`); - assert.equal(command.help.name, command.name, `${file}: help.name ve name farklı.`); - assert.ok(!names.has(command.name), `${file}: yinelenen komut adı.`); - names.add(command.name); - - const json = command.data.toJSON(); - assert.equal(json.name, command.name, `${file}: slash adı legacy adından farklı.`); - assert.match(json.name, /^[a-z0-9_-]{1,32}$/); - assert.ok(json.description.length >= 1 && json.description.length <= 100); - assert.ok(!slashNames.has(json.name), `${file}: yinelenen slash komutu.`); - slashNames.add(json.name); + assert.equal(typeof command.name, "string", `${file}: name eksik.`); + assert.equal(command.help?.name, command.name, `${file}: help.name farklı.`); + assert.ok(Array.isArray(command.aliases), `${file}: aliases dizi değil.`); + assert.equal(typeof command.data?.toJSON, "function", `${file}: slash builder eksik.`); + const data = command.data.toJSON(); + assert.equal(data.name, command.name); + assert.match(data.name, /^[a-z0-9_-]{1,32}$/); + assert.ok(data.description.length >= 1 && data.description.length <= 100); + assert.ok(!commandMap.has(command.name), `Yinelenen komut: ${command.name}`); + commandMap.set(command.name, command); } -assert.equal(names.size, 17); -assert.equal(slashNames.size, 17); +assert.equal(commandMap.size, 17); -for (const { command } of commands) { - for (const alias of command.aliases || []) { - assert.equal(typeof alias, "string"); +const aliasMap = new Map(); +for (const command of commands.map(({ command }) => command)) { + for (const alias of command.aliases) { + const normalized = alias.toLowerCase(); assert.ok(alias.length > 0 && alias.length <= 32); - assert.ok(!names.has(alias.toLowerCase()), `Alias gerçek komut adıyla çakışıyor: ${alias}`); + assert.ok(!commandMap.has(normalized), `Alias komut adıyla çakışıyor: ${alias}`); + assert.ok(!aliasMap.has(normalized), `Yinelenen alias: ${alias}`); + aliasMap.set(normalized, command.name); } } -const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "economybot-test-")); -const tempDb = new KeyValueStore(path.join(tempDir, "test.sqlite")); -const eco = new EconomyManager(tempDb); -const guild = "123456789012345678"; -const userA = "123456789012345679"; -const userB = "123456789012345680"; - -try { - assert.equal(eco.getBalance(guild, userA), 0); - assert.equal(eco.addMoney(guild, userA, 1000).after, 1000); - assert.equal(eco.setMoney(guild, userA, 1500).after, 1500); - assert.equal(eco.removeMoney(guild, userA, 500).after, 1000); - assert.equal(eco.removeMoney(guild, userA, 2000).error, "Yetersiz bakiye."); - - const transfer = eco.transfer(guild, userA, userB, 300); - assert.equal(transfer.error, undefined); - assert.equal(eco.getBalance(guild, userA), 700); - assert.equal(eco.getBalance(guild, userB), 300); - assert.equal(eco.transfer(guild, userA, userA, 1).error, "Kendine para gönderemezsin."); - - const daily1 = eco.daily(guild, userA, 100); - assert.equal(daily1.onCooldown, false); - const daily2 = eco.daily(guild, userA, 100); - assert.equal(daily2.onCooldown, true); - assert.ok(daily2.remainingMs > 0); - - const purchase = eco.purchase(guild, userA, { id: "laptop", name: "Laptop", cost: 200 }); - assert.equal(purchase.error, null); - assert.equal(eco.getBalance(guild, userA), 600); - assert.equal(eco.getInventory(guild, userA).length, 1); - - const insufficientPurchase = eco.purchase(guild, userA, { id: "pc", name: "PC", cost: 10_000 }); - assert.equal(insufficientPurchase.error, "Yetersiz bakiye."); - assert.equal(eco.getInventory(guild, userA).length, 1); - - const leaderboard = eco.leaderboard(guild, 15); - assert.equal(leaderboard[0].id, userB); - assert.equal(eco.getPosition(guild, userB), 1); -} finally { - tempDb.close(); - fs.rmSync(tempDir, { recursive: true, force: true }); -} - -function makeUser(id, tag = `User${id}`) { +function user(id, tag) { return { id, tag, username: tag.split("#")[0], bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }; } -function makeContext(client, userId, { options = {}, args = [], slash = true } = {}) { - const user = client.users.cache.get(userId) || makeUser(userId); - const interaction = { - createdTimestamp: Date.now(), - options: { - getUser: (name) => options[name] instanceof Object ? options[name] : null, - getInteger: (name) => options[name] ?? null, - getString: (name) => options[name] ?? null, - getMember: () => null - } - }; - const replies = []; - return { - client, - interaction, - isSlash: slash, - guild: { id: guild, name: "Test Guild", iconURL: () => null }, - guildId: guild, - user, - userId, - member: { permissions: { has: () => true } }, - args, - message: null, - replies, - userOption: (name) => interaction.options.getUser(name), - async reply(payload) { - replies.push(payload); - return payload; - } - }; -} +const tempPath = path.join(os.tmpdir(), `economybot-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.sqlite`); +const db = new KeyValueStore(tempPath); +const eco = new EconomyManager(db); +const guildId = "123456789012345678"; +const adminId = "123456789012345679"; +const targetId = "123456789012345680"; +const botId = "123456789012345681"; -const mockStore = new KeyValueStore(path.join(os.tmpdir(), `economybot-command-${Date.now()}-${Math.random().toString(16).slice(2)}.sqlite`)); const client = { - config: { prefix: "!", admins: [userA] }, - db: mockStore, - eco: new EconomyManager(mockStore), + config: { prefix: "!", admins: [adminId], countChannel: "" }, + db, + eco, commands: new Collection(), - aliases: new Collection(), - ws: { ping: 42 }, - user: makeUser("123456789012345681", "EconomyBot"), + aliases: new Collection(aliasMap), users: { cache: new Collection() }, + user: user(botId, "EconomyBot#0001"), + ws: { ping: 42 }, shop: { laptop: { id: "laptop", name: "Laptop", cost: 2000 }, mobile: { id: "mobile", name: "Mobile", cost: 1000 }, pc: { id: "pc", name: "PC", cost: 3000 } } }; -for (const { command } of commands) { - client.commands.set(command.name, command); - for (const alias of command.aliases || []) client.aliases.set(alias.toLowerCase(), command.name); +for (const { command } of commands) client.commands.set(command.name, command); +client.users.cache.set(adminId, user(adminId, "Admin#0001")); +client.users.cache.set(targetId, user(targetId, "Target#0001")); +client.users.cache.set(botId, client.user); +eco.setMoney(guildId, adminId, 20_000); +eco.setMoney(guildId, targetId, 5_000); + +function makeContext(userId, options = {}) { + const currentUser = client.users.cache.get(userId); + const interaction = { + createdTimestamp: Date.now(), + options: { + getUser: (name) => (options[name] && typeof options[name] === "object" ? options[name] : null), + getInteger: (name) => options[name] ?? null, + getString: (name) => options[name] ?? null, + getMember: () => null, + get: (name) => (options[name] === undefined ? null : { value: options[name] }) + } + }; + const replies = []; + const ctx = createInteractionContext(interaction, client); + ctx.user = currentUser; + ctx.userId = userId; + ctx.guild = { id: guildId, name: "Test Guild", iconURL: () => null }; + ctx.guildId = guildId; + ctx.member = { permissions: { has: () => userId === adminId } }; + ctx.reply = async (payload) => { replies.push(payload); return payload; }; + return { ctx, replies }; } -client.users.cache.set(userA, makeUser(userA, "Admin#0001")); -client.users.cache.set(userB, makeUser(userB, "Target#0001")); -client.users.cache.set("123456789012345681", client.user); -client.eco.setMoney(guild, userA, 10_000); -client.eco.setMoney(guild, userB, 5_000); - -async function runCommand(name, userId = userA, options = {}) { - const command = client.commands.get(name); - const ctx = makeContext(client, userId, { options }); - await command.execute(ctx); - assert.ok(ctx.replies.length > 0, `${name}: yanıt üretilmedi.`); - return ctx.replies.at(-1); + +async function runSlash(name, userId = adminId, options = {}) { + const { ctx, replies } = makeContext(userId, options); + await commandMap.get(name).execute(ctx); + assert.ok(replies.length > 0, `/${name} yanıt üretmedi.`); + return replies.at(-1); } (async () => { - await runCommand("bal", userA); - await runCommand("beg", userA); - await runCommand("buy", userA, { urun: "laptop" }); - await runCommand("daily", userB); - await runCommand("help", userA); - await runCommand("inventory", userA); - await runCommand("lb", userA); - await runCommand("ping", userA); - await runCommand("prefix", userA, { yeni_prefix: "$" }); - await runCommand("rob", userA, { kullanici: client.users.cache.get(userB) }); - await runCommand("search", userB); - await runCommand("setmoney", userA, { kullanici: client.users.cache.get(userB), miktar: 2500 }); - await runCommand("shop", userA); - await runCommand("transfer", userA, { kullanici: client.users.cache.get(userB), miktar: 100 }); - await runCommand("weekly", userB); - await runCommand("work", userA); - - assert.equal(client.db.getPrefix(guild, "!"), "$", "Prefix kalıcı olarak kaydedilmedi."); - - mockStore.close(); - fs.rmSync(mockStore.connection.name, { force: true }); - fs.rmSync(`${mockStore.connection.name}-wal`, { force: true }); - fs.rmSync(`${mockStore.connection.name}-shm`, { force: true }); - console.log("Tam test başarılı: 17 komut, slash şemaları, ekonomi işlemleri, atomik transfer, mağaza, envanter, cooldown, prefix ve tüm komut yürütmeleri doğrulandı."); + // Core ledger tests. + assert.equal(eco.getBalance(guildId, adminId), 20_000); + assert.equal(eco.transfer(guildId, adminId, targetId, 500).fromBalance, 19_500); + assert.equal(eco.getBalance(guildId, targetId), 5_500); + assert.equal(eco.transfer(guildId, adminId, adminId, 1).error, "Kendine para gönderemezsin."); + assert.equal(eco.transfer(guildId, adminId, targetId, 999_999).error, "Yetersiz bakiye."); + + const purchase = eco.purchase(guildId, adminId, client.shop.laptop); + assert.equal(purchase.error, null); + assert.equal(eco.getInventory(guildId, adminId).length, 1); + const failedPurchase = eco.purchase(guildId, adminId, client.shop.pc); + assert.equal(failedPurchase.error, "Yetersiz bakiye."); + assert.equal(eco.getInventory(guildId, adminId).length, 1); + + const cd1 = eco.useCooldown(guildId, adminId, "test", 60_000); + const cd2 = eco.useCooldown(guildId, adminId, "test", 60_000); + assert.equal(cd1.onCooldown, false); + assert.equal(cd2.onCooldown, true); + assert.ok(cd2.remainingMs > 0); + + // Every slash command is executed through its actual command function. + await runSlash("bal"); + await runSlash("beg"); + await runSlash("buy", adminId, { urun: "mobile" }); + await runSlash("daily", targetId); + await runSlash("help"); + await runSlash("inventory"); + await runSlash("lb"); + await runSlash("ping"); + await runSlash("prefix", adminId, { yeni_prefix: "$" }); + await runSlash("rob", adminId, { kullanici: client.users.cache.get(targetId) }); + await runSlash("search", targetId); + await runSlash("setmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 2500 }); + await runSlash("shop"); + await runSlash("transfer", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }); + await runSlash("weekly", targetId); + await runSlash("work"); + + assert.equal(db.getPrefix(guildId, "!"), "$", "Prefix DB'ye yazılmadı."); + + // Negative permission case. + const unauthorized = await runSlash("addmoney", targetId, { kullanici: client.users.cache.get(adminId), miktar: 100 }); + assert.equal(unauthorized.ephemeral, true); + + // Native slash event path: this must reply without any fake message adapter. + const eventReplies = []; + const interaction = { + commandName: "ping", + isChatInputCommand: () => true, + inGuild: () => true, + guild: { id: guildId, name: "Test Guild" }, + guildId, + user: client.users.cache.get(adminId), + member: { permissions: { has: () => true } }, + options: { get: () => null, getUser: () => null, getInteger: () => null, getString: () => null, getMember: () => null }, + reply: async (payload) => { eventReplies.push(payload); return payload; }, + followUp: async (payload) => { eventReplies.push(payload); return payload; }, + editReply: async (payload) => { eventReplies.push(payload); return payload; }, + get replied() { return eventReplies.length > 0; }, + get deferred() { return false; } + }; + await require("../events/interactionCreate")(client, interaction); + assert.ok(eventReplies.length > 0, "interactionCreate /ping yanıt üretmedi."); + + // Native prefix path and Turkish alias. + const prefixReplies = []; + const message = { + guild: { id: guildId }, + inGuild: () => true, + author: client.users.cache.get(adminId), + content: "$bakiye", + channel: { id: "not-counter" }, + member: { permissions: { has: () => true } }, + mentions: { users: { first: () => null }, members: { first: () => null } }, + reply: async (payload) => { prefixReplies.push(payload); return payload; } + }; + await require("../events/messageCreate")(client, message); + assert.ok(prefixReplies.length > 0, "Prefix aliası yanıt üretmedi."); + + db.close(); + for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); + console.log("TÜM TESTLER BAŞARILI: 17 native slash komutu, prefix aliası, DB, transfer, satın alma, envanter, cooldown, yetki ve prefix doğrulandı."); })().catch((error) => { - try { mockStore.close(); } catch {} + try { db.close(); } catch {} + for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); console.error(error); process.exitCode = 1; }); From 29c15349342144ef1d3f77ff9c1094e2564f884b Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:24:54 +0300 Subject: [PATCH 116/175] Harden bot startup and remove unnecessary privileged intent --- index.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index 5465491..3485069 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,6 @@ if (!Array.isArray(config.admins)) throw new Error("botConfig.js içindeki admin const client = new Client({ intents: [ GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent ] @@ -33,44 +32,38 @@ client.shop = Object.freeze({ const commandsPath = path.join(__dirname, "commands"); const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js")).sort(); - for (const file of commandFiles) { const command = require(path.join(commandsPath, file)); - const name = String(command?.help?.name || "").toLowerCase(); - if (!name || typeof command.execute !== "function" || !command.data) { - throw new TypeError(`commands/${file}: data, help.name veya execute eksik.`); + const name = String(command?.name || command?.help?.name || "").toLowerCase(); + if (!name || typeof command.execute !== "function" || typeof command.data?.toJSON !== "function") { + throw new TypeError(`commands/${file}: name, data veya execute eksik.`); } if (client.commands.has(name)) throw new Error(`Yinelenen komut adı: ${name}`); + if (command.help?.name !== name) throw new Error(`commands/${file}: help.name ve name eşleşmiyor.`); client.commands.set(name, command); - for (const alias of command.help.aliases || []) { - const normalized = String(alias).toLowerCase(); - if (!normalized || client.commands.has(normalized) || client.aliases.has(normalized)) { + for (const alias of command.aliases || []) { + const normalized = String(alias).trim().toLowerCase(); + if (!normalized || normalized === name || client.commands.has(normalized) || client.aliases.has(normalized)) { throw new Error(`Geçersiz veya çakışan takma ad: ${alias}`); } client.aliases.set(normalized, name); } } -if (client.commands.size !== 17) { - throw new Error(`17 komut bekleniyordu, ${client.commands.size} komut yüklendi.`); -} +if (client.commands.size !== 17) throw new Error(`17 komut bekleniyordu, ${client.commands.size} komut yüklendi.`); -client.once("clientReady", require("./events/clientReady" ).bind(null, client)); +client.once("clientReady", require("./events/clientReady").bind(null, client)); client.on("interactionCreate", require("./events/interactionCreate").bind(null, client)); client.on("messageCreate", require("./events/messageCreate").bind(null, client)); client.on("error", (error) => console.error("Discord client hatası:", error)); client.on("warn", (warning) => console.warn("Discord uyarısı:", warning)); - process.on("unhandledRejection", (error) => console.error("Yakalanmamış Promise hatası:", error)); process.on("uncaughtException", (error) => console.error("Yakalanmamış uygulama hatası:", error)); const shutdown = () => { - try { - client.destroy(); - } finally { - database.close(); - } + client.destroy(); + database.close(); }; process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); From 0cad9322f73baadc8b5af7f5b0e23cdabde0ba45 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:24:59 +0300 Subject: [PATCH 117/175] Remove obsolete smoke test --- tests/smoke.js | 266 ------------------------------------------------- 1 file changed, 266 deletions(-) delete mode 100644 tests/smoke.js diff --git a/tests/smoke.js b/tests/smoke.js deleted file mode 100644 index ce5a957..0000000 --- a/tests/smoke.js +++ /dev/null @@ -1,266 +0,0 @@ -const assert = require("node:assert/strict"); -const fs = require("node:fs"); -const path = require("node:path"); -const { Collection } = require("discord.js"); -const slashCommands = require("../slashCommands"); -const interactionHandler = require("../events/interactionCreate"); -const messageHandler = require("../events/messageCreate"); - -const commandsDir = path.join(__dirname, "..", "commands"); -const commandFiles = fs.readdirSync(commandsDir).filter((file) => file.endsWith(".js")); -const commands = commandFiles.map((file) => ({ file, command: require(path.join(commandsDir, file)) })); - -assert.equal(commands.length, 17, "17 komut dosyası bulunmalı."); -assert.equal(slashCommands.length, 17, "17 slash komutu tanımlı olmalı."); - -const names = new Set(); -for (const { file, command } of commands) { - assert.equal(typeof command.execute, "function", `${file}: execute fonksiyonu eksik.`); - assert.equal(typeof command.help?.name, "string", `${file}: help.name eksik.`); - assert.ok(Array.isArray(command.help.aliases), `${file}: help.aliases dizi olmalı.`); - assert.equal(typeof command.help.usage, "string", `${file}: help.usage eksik.`); - assert.ok(!names.has(command.help.name), `${file}: aynı komut adı tekrar kullanılmış.`); - names.add(command.help.name); -} - -const slashData = slashCommands.map((command) => command.toJSON()); -const slashNames = slashData.map((command) => command.name); -assert.equal(new Set(slashNames).size, slashNames.length, "Slash komut isimleri benzersiz olmalı."); -for (const name of slashNames) assert.ok(names.has(name), `/${name} için karşılık gelen prefix komutu bulunamadı.`); - -class MockDB { - constructor() { - this.data = new Map(); - } - - fetch(key) { - return this.data.has(key) ? this.data.get(key) : null; - } - - get(key) { - return this.fetch(key); - } - - set(key, value) { - this.data.set(key, value); - return value; - } - - delete(key) { - this.data.delete(key); - return true; - } - - push(key, value) { - const items = Array.isArray(this.get(key)) ? this.get(key) : []; - items.push(value); - this.set(key, items); - return items; - } -} - -class MockEco { - constructor() { - this.balances = new Map(); - this.cooldowns = new Set(); - } - - fetchMoney(id) { - return { balance: this.balances.get(id) || 0, bank: 0, user: { id }, position: 1 }; - } - - addMoney(id, amount) { - const before = this.fetchMoney(id).balance; - const after = before + amount; - this.balances.set(id, after); - return { before, after, user: id, amount }; - } - - removeMoney(id, amount) { - const before = this.fetchMoney(id).balance; - if (before < amount) return { error: "New amount is negative." }; - const after = before - amount; - this.balances.set(id, after); - return { before, after, user: id, amount }; - } - - setMoney(id, amount) { - const before = this.fetchMoney(id).balance; - this.balances.set(id, amount); - return { before, after: amount, user: id, amount }; - } - - daily(id, amount) { - const key = `daily:${id}`; - if (this.cooldowns.has(key)) return { onCooldown: true, time: { hours: 1, minutes: 0, seconds: 0 } }; - this.cooldowns.add(key); - return { onCooldown: false, amount, after: this.addMoney(id, amount).after, time: { hours: 24, minutes: 0, seconds: 0 } }; - } - - weekly(id, amount) { - return { onCooldown: false, amount, after: this.addMoney(id, amount).after, time: { days: 7, hours: 0, minutes: 0, seconds: 0 } }; - } - - work(id, amount) { - return { onCooldown: false, amount, after: this.addMoney(id, amount).after, workedAs: "Developer", time: { minutes: 45, seconds: 0 } }; - } - - beg(id, amount) { - return { onCooldown: false, lost: false, amount, after: this.addMoney(id, amount).after, time: { seconds: 60 } }; - } - - transfer(from, to, amount) { - const balance = this.fetchMoney(from).balance; - if (balance < amount) return { error: "Money of first user is less than given amount." }; - this.balances.set(from, balance - amount); - this.balances.set(to, this.fetchMoney(to).balance + amount); - return { user1: { id: from }, user2: { id: to }, amount }; - } - - leaderboard({ limit = 10 } = {}) { - return [...this.balances.entries()] - .sort((a, b) => b[1] - a[1]) - .slice(0, limit) - .map(([id, money], index) => ({ position: index + 1, id, money })); - } -} - -function makeClient() { - const client = { - config: { admins: ["admin"], prefix: "!" }, - commands: new Collection(), - aliases: new Collection(), - db: new MockDB(), - eco: new MockEco(), - shop: { - Laptop: { cost: 2000 }, - Mobile: { cost: 1000 }, - PC: { cost: 3000 } - }, - users: { cache: new Collection() }, - ws: { ping: 42 }, - user: { id: "bot", tag: "EconomyBot", displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" } - }; - - for (const { command } of commands) client.commands.set(command.help.name, command); - for (const { command } of commands) for (const alias of command.help.aliases) client.aliases.set(alias, command.help.name); - - client.users.cache.set("target", { id: "target", tag: "Target#0001", bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }); - client.users.cache.set("recipient", { id: "recipient", tag: "Recipient#0001", bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }); - client.users.cache.set("admin", { id: "admin", tag: "Admin#0001", bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }); - return client; -} - -function makeMessage(client, { authorId = "admin", targetId = "target", args = [], prefix = "!" } = {}) { - const sent = []; - const target = client.users.cache.get(targetId); - const targetMember = { id: target?.id, user: target, displayAvatarURL: target?.displayAvatarURL, bot: target?.bot }; - const guild = { - id: "guild", - name: "Smoke Guild", - iconURL: () => null, - members: { cache: new Collection([[target?.id, targetMember]]) } - }; - - const makeSentMessage = () => ({ - createdTimestamp: Date.now(), - edit: async (payload) => { - sent.push({ edited: true, payload }); - return makeSentMessage(); - } - }); - - const send = async (payload) => { - sent.push(payload); - return makeSentMessage(); - }; - - return { - sent, - author: { - id: authorId, - tag: `${authorId}#0001`, - displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" - }, - member: { id: authorId, permissions: { has: () => true } }, - guild, - channel: { id: "channel", send }, - reply: send, - prefix, - createdTimestamp: Date.now(), - mentions: { - users: { first: () => target || null }, - members: { first: () => targetMember || null } - }, - content: `${prefix}${args.join(" ")}` - }; -} - -(async () => { - const client = makeClient(); - client.eco.setMoney("admin", 10000); - client.eco.setMoney("target", 5000); - - const cases = [ - ["addmoney", ["target", "500"], "admin"], - ["bal", [], "admin"], - ["beg", [], "admin"], - ["buy", ["Laptop"], "admin"], - ["daily", [], "admin"], - ["help", [], "admin"], - ["inventory", [], "admin"], - ["lb", [], "admin"], - ["ping", [], "admin"], - ["prefix", ["$"], "admin"], - ["rob", ["target"], "admin"], - ["search", [], "admin"], - ["setmoney", ["target", "2500"], "admin"], - ["shop", [], "admin"], - ["transfer", ["target", "100"], "admin"], - ["weekly", [], "admin"], - ["work", [], "admin"] - ]; - - for (const [name, args, authorId] of cases) { - const message = makeMessage(client, { authorId, args }); - await client.commands.get(name).execute(client, message, args); - assert.ok(message.sent.length > 0, `${name}: görünür yanıt üretmedi.`); - } - - const prefixMessage = makeMessage(client, { authorId: "admin" }); - prefixMessage.content = "$bakiye"; - await messageHandler(client, prefixMessage); - assert.ok(prefixMessage.sent.length > 0, "Prefix aliası çalışmadı."); - - let deferred = false; - let replied = false; - const interactionEdits = []; - const interaction = { - id: "interaction", - commandName: "bal", - channelId: "channel", - createdTimestamp: Date.now(), - guild: prefixMessage.guild, - user: client.users.cache.get("admin"), - member: prefixMessage.member, - isChatInputCommand: () => true, - options: { - getUser: () => null, - getMember: () => null, - getInteger: () => null, - getString: () => null - }, - deferReply: async () => { deferred = true; }, - editReply: async (payload) => { replied = true; interactionEdits.push(payload); return { createdTimestamp: Date.now(), edit: async (next) => { interactionEdits.push(next); } }; }, - followUp: async (payload) => { replied = true; interactionEdits.push(payload); return payload; }, - get deferred() { return deferred; }, - get replied() { return replied; }, - reply: async (payload) => { replied = true; interactionEdits.push(payload); } - }; - - await interactionHandler(client, interaction); - assert.equal(deferred, true, "Slash command deferReply çalışmadı."); - assert.ok(interactionEdits.length > 0, "/bal slash komutu görünür yanıt üretmedi."); - - console.log(`Smoke test başarılı: ${commands.length} komut, ${slashCommands.length} slash komutu, prefix aliası ve slash yürütme akışı doğrulandı.`); -})(); From abab619e870ac650ad8ec628853a20892950ef3f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:25:03 +0300 Subject: [PATCH 118/175] Remove obsolete database test --- tests/database.js | 64 ----------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 tests/database.js diff --git a/tests/database.js b/tests/database.js deleted file mode 100644 index 31e4b1d..0000000 --- a/tests/database.js +++ /dev/null @@ -1,64 +0,0 @@ -const assert = require("node:assert/strict"); -const fs = require("node:fs"); -const os = require("node:os"); -const path = require("node:path"); -const Database = require("better-sqlite3"); -const KeyValueStore = require("../lib/database"); -const EconomyManager = require("../lib/economy"); - -const filePath = path.join(os.tmpdir(), `economybot-test-${process.pid}-${Date.now()}.sqlite`); -const userA = "12345678901234567"; -const userB = "12345678901234568"; - -try { - const legacy = new Database(filePath); - legacy.exec("CREATE TABLE json (ID TEXT NOT NULL, json TEXT NOT NULL)"); - legacy.prepare("INSERT INTO json (ID, json) VALUES (?, ?)").run(`money_${userA}`, JSON.stringify(1000)); - legacy.prepare("INSERT INTO json (ID, json) VALUES (?, ?)").run("prefix_legacyGuild", JSON.stringify("!")); - legacy.close(); - - const store = new KeyValueStore(filePath); - const economy = new EconomyManager(store); - - assert.equal(store.fetch(`money_${userA}`), 1000, "Legacy SQLite balance must remain readable."); - assert.equal(store.fetch("prefix_legacyGuild"), "!", "Legacy prefix data must remain readable."); - - const balance = economy.fetchMoney(userA); - assert.equal(balance.balance, 1000); - - const added = economy.addMoney(userA, 250); - assert.deepEqual({ before: added.before, after: added.after, amount: added.amount }, { before: 1000, after: 1250, amount: 250 }); - - const transfer = economy.transfer(userA, userB, 300); - assert.equal(transfer.amount, 300); - assert.equal(economy.fetchMoney(userA).balance, 950); - assert.equal(economy.fetchMoney(userB).balance, 300); - - const failedTransfer = economy.transfer(userA, userB, 999999); - assert.ok(failedTransfer.error, "Insufficient transfer must fail without modifying balances."); - assert.equal(economy.fetchMoney(userA).balance, 950); - assert.equal(economy.fetchMoney(userB).balance, 300); - - const set = economy.setMoney(userB, 0); - assert.equal(set.after, 0, "Setting a balance to zero must be supported."); - - const daily = economy.daily(userA, 100); - assert.equal(daily.onCooldown, false); - const dailyAgain = economy.daily(userA, 100); - assert.equal(dailyAgain.onCooldown, true); - - store.set("items_test", [{ name: "laptop" }]); - store.push("items_test", { name: "pc" }); - assert.deepEqual(store.get("items_test"), [{ name: "laptop" }, { name: "pc" }]); - assert.equal(store.delete("items_test"), true); - assert.equal(store.get("items_test"), null); - - store.close(); - console.log("Database test başarılı: legacy SQLite uyumluluğu, bakiye işlemleri, transfer atomikliği, cooldown ve key-value işlemleri doğrulandı."); -} finally { - for (const suffix of ["", "-wal", "-shm"]) { - try { - fs.rmSync(`${filePath}${suffix}`, { force: true }); - } catch {} - } -} From 59659c77a1b1640ecc3eef5cd388118c87aabbc2 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:25:15 +0300 Subject: [PATCH 119/175] Modernize CodeQL workflow --- .github/workflows/codeql-analysis.yml | 68 +++++---------------------- 1 file changed, 13 insertions(+), 55 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 218a21a..5a31a31 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,72 +1,30 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" +name: CodeQL on: push: - branches: [ "main" ] + branches: ["main", "professional-v14-rebuild"] pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: ["main"] schedule: - - cron: '18 10 * * 4' + - cron: "18 10 * * 4" jobs: analyze: - name: Analyze + name: Analyze JavaScript runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + - name: Repository'i al + uses: actions/checkout@v4 - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + - name: CodeQL başlat + uses: github/codeql-action/init@v3 + with: + languages: javascript - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: CodeQL analizi + uses: github/codeql-action/analyze@v3 From 0bdb312c5a437babdad89bc4a39b81af577f1b9b Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:25:37 +0300 Subject: [PATCH 120/175] Make balance mutations atomic --- lib/economy.js | 66 +++++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/lib/economy.js b/lib/economy.js index 51c6026..e154cf2 100644 --- a/lib/economy.js +++ b/lib/economy.js @@ -13,9 +13,7 @@ class EconomyManager { } assertAmount(value, allowZero = false) { - if (!Number.isSafeInteger(value) || (allowZero ? value < 0 : value <= 0)) { - throw new TypeError("Miktar güvenli bir tam sayı olmalı."); - } + if (!Number.isSafeInteger(value) || (allowZero ? value < 0 : value <= 0)) throw new TypeError("Miktar güvenli bir tam sayı olmalı."); } moneyKey(guildId, userId) { @@ -47,27 +45,36 @@ class EconomyManager { addMoney(guildId, userId, amount) { this.assertAmount(amount); - const before = this.getBalance(guildId, userId); - const after = before + amount; - if (!Number.isSafeInteger(after)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); - this.db.set(this.moneyKey(guildId, userId), after); - return { user: { id: String(userId) }, amount, before, after }; + return this.db.transaction(() => { + const key = this.moneyKey(guildId, userId); + const before = this.getBalance(guildId, userId); + const after = before + amount; + if (!Number.isSafeInteger(after)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); + this.db.set(key, after); + return { user: { id: String(userId) }, amount, before, after }; + }); } setMoney(guildId, userId, amount) { this.assertAmount(amount, true); - const before = this.getBalance(guildId, userId); - this.db.set(this.moneyKey(guildId, userId), amount); - return { user: { id: String(userId) }, amount, before, after: amount }; + return this.db.transaction(() => { + const key = this.moneyKey(guildId, userId); + const before = this.getBalance(guildId, userId); + this.db.set(key, amount); + return { user: { id: String(userId) }, amount, before, after: amount }; + }); } removeMoney(guildId, userId, amount) { this.assertAmount(amount); - const before = this.getBalance(guildId, userId); - if (before < amount) return { error: "Yetersiz bakiye.", before, after: before }; - const after = before - amount; - this.db.set(this.moneyKey(guildId, userId), after); - return { user: { id: String(userId) }, amount, before, after }; + return this.db.transaction(() => { + const key = this.moneyKey(guildId, userId); + const before = this.getBalance(guildId, userId); + if (before < amount) return { error: "Yetersiz bakiye.", before, after: before }; + const after = before - amount; + this.db.set(key, after); + return { user: { id: String(userId) }, amount, before, after }; + }); } transfer(guildId, fromUserId, toUserId, amount) { @@ -77,21 +84,20 @@ class EconomyManager { if (String(fromUserId) === String(toUserId)) return { error: "Kendine para gönderemezsin." }; return this.db.transaction(() => { + const fromKey = this.moneyKey(guildId, fromUserId); + const toKey = this.moneyKey(guildId, toUserId); const fromBalance = this.getBalance(guildId, fromUserId); const toBalance = this.getBalance(guildId, toUserId); if (fromBalance < amount) return { error: "Yetersiz bakiye.", fromBalance, toBalance }; if (!Number.isSafeInteger(toBalance + amount)) throw new RangeError("Hedef bakiye güvenli sayı sınırını aşıyor."); - this.db.set(this.moneyKey(guildId, fromUserId), fromBalance - amount); - this.db.set(this.moneyKey(guildId, toUserId), toBalance + amount); + this.db.set(fromKey, fromBalance - amount); + this.db.set(toKey, toBalance + amount); return { amount, fromBalance: fromBalance - amount, toBalance: toBalance + amount }; }); } purchase(guildId, userId, item) { - if (!item || typeof item.id !== "string" || typeof item.name !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) { - throw new TypeError("Geçersiz mağaza ürünü."); - } - + if (!item || typeof item.id !== "string" || typeof item.name !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) throw new TypeError("Geçersiz mağaza ürünü."); return this.db.transaction(() => { const balance = this.getBalance(guildId, userId); if (balance < item.cost) return { error: "Yetersiz bakiye.", after: balance }; @@ -123,9 +129,11 @@ class EconomyManager { } useCooldown(guildId, userId, command, durationMs) { - const status = this.getCooldown(guildId, userId, command, durationMs); - if (!status.onCooldown) this.setCooldown(guildId, userId, command); - return status; + return this.db.transaction(() => { + const status = this.getCooldown(guildId, userId, command, durationMs); + if (!status.onCooldown) this.setCooldown(guildId, userId, command); + return status; + }); } daily(guildId, userId, amount) { @@ -172,9 +180,9 @@ class EconomyManager { } leaderboard(guildId, limit = 15) { + this.assertId(guildId, "sunucu ID'si"); const prefix = `money:${guildId}:`; - return this.db - .startsWith(prefix) + return this.db.startsWith(prefix) .map(({ ID, data }) => ({ id: ID.slice(prefix.length), money: Number(data) })) .filter((entry) => /^\d{17,20}$/.test(entry.id) && Number.isSafeInteger(entry.money) && entry.money >= 0) .sort((a, b) => b.money - a.money || a.id.localeCompare(b.id)) @@ -184,7 +192,9 @@ class EconomyManager { getPosition(guildId, userId) { const balance = this.getBalance(guildId, userId); - return this.db.startsWith(`money:${guildId}:`).filter(({ ID, data }) => /^money:\d{17,20}:\d{17,20}$/.test(ID) && Number.isSafeInteger(Number(data)) && Number(data) > balance).length + 1; + return this.db.startsWith(`money:${guildId}:`) + .filter(({ ID, data }) => /^money:\d{17,20}:\d{17,20}$/.test(ID) && Number.isSafeInteger(Number(data)) && Number(data) > balance) + .length + 1; } } From 0b81de0eaa32883c1544a3fe064470e1045811b4 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:25:53 +0300 Subject: [PATCH 121/175] Update documentation for professional rebuild --- README.md | 199 ++++++++++++++++++++++-------------------------------- 1 file changed, 82 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 8315151..1573cf4 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,11 @@ # EconomyBot 🇹🇷 -### Modern • Türkçe • Node.js 20+ • Discord.js v14 +### Modern • Türkçe • Node.js 20+ • discord.js v14 -**Kolay kurulabilen, klasik prefix komutlarını ve modern `/` slash komutlarını birlikte sunan Discord ekonomi botu.** +**Basit kuruluma sahip, slash ve klasik prefix komutlarını destekleyen Discord ekonomi botu.** - -EconomyBot GIF Alanı - -
+EconomyBot Preview [![Node.js](https://img.shields.io/badge/Node.js-20%2B-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/) [![Discord.js](https://img.shields.io/badge/discord.js-v14-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.js.org/) @@ -23,47 +16,34 @@ --- -## ✦ Hakkında - -**EconomyBot**, Discord sunucuları için hazırlanmış klasik bir ekonomi botudur. Bu sürüm **LoiFragola** tarafından Türkçeleştirilmiş, komutlar Discord'a deploy edilecek hale getirilmiş, **Node.js 20+** ve **discord.js v14** ile çalışacak şekilde güncellenmiştir. - -Bot başlatıldığında `botConfig.js` içerisindeki `serverId` değerini kullanarak tüm slash komutlarını doğrudan hedef sunucuya deploy eder. Böylece komutları hem klasik prefix sistemiyle hem de `/` ile kullanabilirsiniz. +## Özellikler -> **Not:** Prefix komutlarının çalışması için Discord Developer Portal'da **Message Content Intent** etkinleştirilmelidir. - ---- - -## ✦ Özellikler - -- 💰 Kullanıcı bakiye ve ekonomi sistemi -- 🎁 Günlük ve haftalık ödüller -- 💼 Çalışma ve dilenme sistemi -- 🔎 Arama sistemi -- 🏪 Mağaza ve ürün satın alma -- 🎒 Envanter sistemi -- 💸 Kullanıcılar arası para transferi +- 💰 Sunucuya özel bakiye sistemi +- 🎁 Günlük, haftalık ve çalışma ödülleri +- 💼 Dilenme ve arama etkinlikleri +- 🥷 Soygun sistemi +- 💸 Kullanıcılar arası atomik para transferi +- 🏪 Mağaza ve satın alma sistemi +- 🎒 Envanter - 🏆 Ekonomi sıralaması -- 🥷 Sanal soygun sistemi - ⚙️ Sunucuya özel prefix -- 📊 Sayaç kanalı desteği -- `/` **Slash Command** desteği -- 🇹🇷 Türkçe komut mesajları ve Türkçe prefix alias'ları - ---- +- 🔢 İsteğe bağlı sayaç kanalı +- `/` native slash komutları +- 🇹🇷 Türkçe mesajlar ve Türkçe prefix alias'ları +- 💾 SQLite tabanlı kalıcı depolama +- 🛡️ Yetki ve veri doğrulaması -## ✦ Gereksinimler +## Gereksinimler -| Gereksinim | Sürüm | +| Bileşen | Destek | |---|---| -| **Node.js** | `20+` | -| **discord.js** | `14.x` | -| **npm** | Node.js ile birlikte gelir | +| Node.js | `20+` | +| discord.js | `14.27.0` | +| better-sqlite3 | `12.11.1` | -> Node.js 22 veya 24 kullanmanız önerilir. +Node.js 22 veya 24 kullanmak önerilir. ---- - -## ✦ Kurulum +## Kurulum ### 1. Projeyi indirin @@ -78,9 +58,7 @@ cd EconomyBot npm install ``` -### 3. Bot ayarlarını yapın - -`botConfig.js` dosyasını açıp aşağıdaki alanları doldurun: +### 3. `botConfig.js` dosyasını ayarlayın ```js module.exports = { @@ -95,113 +73,101 @@ module.exports = { }; ``` -**`serverId`**, slash komutlarının deploy edileceği Discord sunucusunun ID'sidir. +`serverId`, slash komutlarının otomatik deploy edileceği sunucudur. -### 4. Discord izinlerini kontrol edin +### 4. Discord Developer Portal -Prefix komutları için Developer Portal'dan **Message Content Intent** özelliğini açın. +Prefix komutları için **Message Content Intent** etkinleştirilmelidir. -Botun hedef sunucuya uygulama komutlarını deploy edebilmesi için bot davetinde gerekli `applications.commands` kapsamının bulunduğundan emin olun. +Botu davet ederken `bot` ve `applications.commands` kapsamlarının bulunduğundan emin olun. -### 5. Botu başlatın +### 5. Başlatın ```bash npm start ``` -Başarılı başlatmada konsolda buna benzer bir çıktı görürsünüz: +Başarılı başlatma örneği: ```text EconomyBot çevrimiçi! 17 slash komutu sunucuya başarıyla deploy edildi. ``` ---- - -## ✦ Komutlar +## Komutlar ### Ekonomi -| Komut | Açıklama | Örnek | +| Slash | Prefix / Alias | Açıklama | |---|---|---| -| `/bal` | Bakiyeyi gösterir | `/bal` | -| `/daily` | Günlük ödülü verir | `/daily` | -| `/weekly` | Haftalık ödülü verir | `/weekly` | -| `/work` | Çalışarak para kazandırır | `/work` | -| `/beg` | Para dilenmeyi dener | `/beg` | -| `/search` | Para arar | `/search` | -| `/rob` | Başka bir kullanıcıdan para çalmayı dener | `/rob kullanıcı` | -| `/transfer` | Para gönderir | `/transfer kullanıcı 500` | - -### Mağaza & Envanter - -| Komut | Açıklama | Örnek | +| `/bal` | `!bal`, `!bakiye` | Bakiye ve sıralama | +| `/daily` | `!daily`, `!günlük` | Günlük ödül | +| `/weekly` | `!weekly`, `!haftalık` | Haftalık ödül | +| `/work` | `!work`, `!çalış` | Çalışarak para kazan | +| `/beg` | `!beg`, `!dilen` | Rastgele para kazan | +| `/search` | `!search`, `!ara` | Para ara | +| `/rob` | `!rob`, `!soy` | Bir kullanıcıdan para çalmayı dene | +| `/transfer` | `!transfer`, `!aktar` | Para gönder | + +### Mağaza + +| Slash | Prefix / Alias | Açıklama | |---|---|---| -| `/shop` | Mağazayı gösterir | `/shop` | -| `/buy` | Ürün satın alır | `/buy Laptop` | -| `/inventory` | Envanteri gösterir | `/inventory` | +| `/shop` | `!shop`, `!mağaza` | Mağazayı göster | +| `/buy` | `!buy`, `!al` | Ürün satın al | +| `/inventory` | `!inventory`, `!envanter` | Envanteri göster | ### Yönetim & Araçlar -| Komut | Açıklama | Örnek | +| Slash | Prefix / Alias | Açıklama | |---|---|---| -| `/lb` | Ekonomi sıralamasını gösterir | `/lb` | -| `/ping` | Bot gecikmesini gösterir | `/ping` | -| `/help` | Komut listesini gösterir | `/help` | -| `/prefix` | Sunucu prefix'ini ayarlar | `/prefix !` | -| `/addmoney` | Yetkili kullanıcı para ekler | `/addmoney kullanıcı 500` | -| `/setmoney` | Yetkili kullanıcı bakiyeyi ayarlar | `/setmoney kullanıcı 5000` | - -> Prefix kullanıyorsanız aynı komutları `!bal`, `!daily`, `!shop` gibi kullanabilirsiniz. Türkçe alias'lar da desteklenir: `!bakiye`, `!günlük`, `!mağaza`, `!envanter`, `!aktar`, `!çalış` vb. - ---- +| `/addmoney` | `!addmoney`, `!paraekle` | Yetkili para ekler | +| `/setmoney` | `!setmoney`, `!parayarla` | Yetkili bakiyeyi belirler | +| `/prefix` | `!prefix`, `!önek` | Prefix ayarlar / sıfırlar | +| `/lb` | `!lb`, `!sıralama` | Ekonomi sıralaması | +| `/ping` | `!ping`, `!gecikme` | Gecikme bilgisi | +| `/help` | `!help`, `!yardım` | Komut yardımını göster | -## ✦ Slash Command Sistemi - -Bot her başlatıldığında `serverId` için tanımlanan sunucuya komut listesini gönderir. - -Bu yöntem **guild command** kullandığı için geliştirme ve tek sunucu kullanımlarında komutların hızlı güncellenmesini sağlar. - -Komut listesinde bir değişiklik yaptıktan sonra: - -```bash -npm start -``` - -komutunu tekrar çalıştırmanız yeterlidir. - ---- - -## ✦ Yapı +## Mimari ```text EconomyBot/ -├── commands/ # Prefix komutları -├── events/ # Discord eventleri -├── counter.js # Sayaç sistemi -├── slashCommands.js # Slash komut tanımları -├── botConfig.js # Yerel bot ayarları -├── index.js # Bot başlangıç dosyası -├── package.json -├── .gitignore -└── README.md +├── commands/ # Native slash + ortak komut tanımları +├── events/ +│ ├── clientReady.js # Guild slash deploy +│ ├── interactionCreate.js# Native slash execution +│ └── messageCreate.js # Prefix execution +├── lib/ +│ ├── commandUtils.js # Ortak yardımcılar +│ ├── context.js # Slash/prefix context katmanı +│ ├── database.js # SQLite KV store +│ └── economy.js # Ekonomi iş mantığı +├── counter.js # Sayaç sistemi +├── botConfig.js # Yerel bot ayarları +├── index.js # Uygulama bootstrap +├── tests/test.js # Kapsamlı offline testler +└── package.json ``` ---- +## Test + +Projede native slash yürütmesini, prefix alias akışını, SQLite işlemlerini, transfer atomikliğini, satın almayı, envanteri, cooldown'ları ve yetki kontrollerini doğrulayan testler vardır. -## ✦ Güvenlik +```bash +npm test +``` -**Bot token'ınızı veya kişisel bilgilerinizi GitHub'a yüklemeyin.** +Ayrıca GitHub Actions Node.js 20, 22 ve 24 üzerinde sözdizimi ve test kontrollerini çalıştırır. -`botConfig.js` içerisinde gerçek token kullanıyorsanız bu dosyanın Git tarafından takip edilmediğinden emin olun veya token'ı ortam değişkenleri üzerinden yönetin. +## Güvenlik -`.gitignore` dosyası bağımlılıkları, logları, yerel veritabanı dosyalarını ve bazı yerel yapılandırmaları Git'ten uzak tutar. +Bot token'ınızı GitHub'a veya başka bir herkese açık yere yüklemeyin. Token açığa çıktıysa Discord Developer Portal üzerinden yenileyin. ---- +Yerel SQLite veritabanı Git tarafından takip edilmemelidir. -## ✦ Lisans & Kaynak +## Kaynak -Bu proje, **ZeroDiscord/EconomyBot** projesinden geliştirilmiştir. +Proje, `ZeroDiscord/EconomyBot` temel alınarak modernize edilmiştir. Eski `quick.eco` bağımlılığı kaldırılmış, ekonomi katmanı SQLite üzerinde yeniden düzenlenmiş ve slash komutları native discord.js v14 yapısına geçirilmiştir. --- @@ -209,5 +175,4 @@ Bu proje, **ZeroDiscord/EconomyBot** projesinden geliştirilmiştir. ### INS Development - From c7a2664bbf35a0ee04c3517a02d39aea9627c067 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:26:16 +0300 Subject: [PATCH 122/175] Fix deterministic insufficient-balance test --- tests/test.js | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/tests/test.js b/tests/test.js index ddf649a..12f4e37 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5,7 +5,7 @@ const path = require("node:path"); const { Collection } = require("discord.js"); const { KeyValueStore } = require("../lib/database"); const EconomyManager = require("../lib/economy"); -const { createInteractionContext, createPrefixContext } = require("../lib/context"); +const { createInteractionContext } = require("../lib/context"); const root = path.join(__dirname, ".."); const commandFiles = fs.readdirSync(path.join(root, "commands")).filter((file) => file.endsWith(".js")).sort(); @@ -29,10 +29,10 @@ for (const { file, command } of commands) { assert.equal(commandMap.size, 17); const aliasMap = new Map(); -for (const command of commands.map(({ command }) => command)) { +for (const command of commandMap.values()) { for (const alias of command.aliases) { const normalized = alias.toLowerCase(); - assert.ok(alias.length > 0 && alias.length <= 32); + assert.ok(typeof alias === "string" && alias.length > 0 && alias.length <= 32); assert.ok(!commandMap.has(normalized), `Alias komut adıyla çakışıyor: ${alias}`); assert.ok(!aliasMap.has(normalized), `Yinelenen alias: ${alias}`); aliasMap.set(normalized, command.name); @@ -40,7 +40,13 @@ for (const command of commands.map(({ command }) => command)) { } function user(id, tag) { - return { id, tag, username: tag.split("#")[0], bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }; + return { + id, + tag, + username: tag.split("#")[0], + bot: false, + displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" + }; } const tempPath = path.join(os.tmpdir(), `economybot-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.sqlite`); @@ -66,7 +72,7 @@ const client = { pc: { id: "pc", name: "PC", cost: 3000 } } }; -for (const { command } of commands) client.commands.set(command.name, command); +for (const command of commandMap.values()) client.commands.set(command.name, command); client.users.cache.set(adminId, user(adminId, "Admin#0001")); client.users.cache.set(targetId, user(targetId, "Target#0001")); client.users.cache.set(botId, client.user); @@ -97,16 +103,18 @@ function makeContext(userId, options = {}) { } async function runSlash(name, userId = adminId, options = {}) { + const command = commandMap.get(name); const { ctx, replies } = makeContext(userId, options); - await commandMap.get(name).execute(ctx); + await command.execute(ctx); assert.ok(replies.length > 0, `/${name} yanıt üretmedi.`); return replies.at(-1); } (async () => { - // Core ledger tests. + // Core economy invariants. assert.equal(eco.getBalance(guildId, adminId), 20_000); - assert.equal(eco.transfer(guildId, adminId, targetId, 500).fromBalance, 19_500); + const transfer = eco.transfer(guildId, adminId, targetId, 500); + assert.equal(transfer.fromBalance, 19_500); assert.equal(eco.getBalance(guildId, targetId), 5_500); assert.equal(eco.transfer(guildId, adminId, adminId, 1).error, "Kendine para gönderemezsin."); assert.equal(eco.transfer(guildId, adminId, targetId, 999_999).error, "Yetersiz bakiye."); @@ -114,17 +122,18 @@ async function runSlash(name, userId = adminId, options = {}) { const purchase = eco.purchase(guildId, adminId, client.shop.laptop); assert.equal(purchase.error, null); assert.equal(eco.getInventory(guildId, adminId).length, 1); - const failedPurchase = eco.purchase(guildId, adminId, client.shop.pc); + const failedPurchase = eco.purchase(guildId, adminId, { id: "expensive", name: "Pahalı Ürün", cost: 100_000 }); assert.equal(failedPurchase.error, "Yetersiz bakiye."); assert.equal(eco.getInventory(guildId, adminId).length, 1); - const cd1 = eco.useCooldown(guildId, adminId, "test", 60_000); - const cd2 = eco.useCooldown(guildId, adminId, "test", 60_000); - assert.equal(cd1.onCooldown, false); - assert.equal(cd2.onCooldown, true); - assert.ok(cd2.remainingMs > 0); + const cooldown1 = eco.useCooldown(guildId, adminId, "test", 60_000); + const cooldown2 = eco.useCooldown(guildId, adminId, "test", 60_000); + assert.equal(cooldown1.onCooldown, false); + assert.equal(cooldown2.onCooldown, true); + assert.ok(cooldown2.remainingMs > 0); - // Every slash command is executed through its actual command function. + // Every command executes through the same native context used by slash commands. + await runSlash("addmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }); await runSlash("bal"); await runSlash("beg"); await runSlash("buy", adminId, { urun: "mobile" }); @@ -144,11 +153,10 @@ async function runSlash(name, userId = adminId, options = {}) { assert.equal(db.getPrefix(guildId, "!"), "$", "Prefix DB'ye yazılmadı."); - // Negative permission case. const unauthorized = await runSlash("addmoney", targetId, { kullanici: client.users.cache.get(adminId), miktar: 100 }); assert.equal(unauthorized.ephemeral, true); - // Native slash event path: this must reply without any fake message adapter. + // Native interactionCreate path. const eventReplies = []; const interaction = { commandName: "ping", @@ -168,7 +176,7 @@ async function runSlash(name, userId = adminId, options = {}) { await require("../events/interactionCreate")(client, interaction); assert.ok(eventReplies.length > 0, "interactionCreate /ping yanıt üretmedi."); - // Native prefix path and Turkish alias. + // Native prefix path with Turkish alias. const prefixReplies = []; const message = { guild: { id: guildId }, @@ -185,7 +193,7 @@ async function runSlash(name, userId = adminId, options = {}) { db.close(); for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); - console.log("TÜM TESTLER BAŞARILI: 17 native slash komutu, prefix aliası, DB, transfer, satın alma, envanter, cooldown, yetki ve prefix doğrulandı."); + console.log("TÜM TESTLER BAŞARILI: 17 native slash komutu, prefix aliası, DB, transfer, satın alma, envanter, cooldown, yetki ve event akışı doğrulandı."); })().catch((error) => { try { db.close(); } catch {} for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); From 36b60e166f4b42843bff631ca0539c600247715d Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:26:43 +0300 Subject: [PATCH 123/175] Harden local secret and database ignores --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8a0220e..4db45c5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,13 @@ node_modules/ !.env.example botConfig.local.js -# Database / local data +# Runtime database and database sidecars +data/ *.db *.sqlite *.sqlite3 +*.sqlite-wal +*.sqlite-shm *.db-journal # Logs From 5b9a18c85c10b1f919c3490b28379141f94bbe58 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:27:19 +0300 Subject: [PATCH 124/175] Fix mock interaction contract --- tests/test.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/test.js b/tests/test.js index 12f4e37..40b7726 100644 --- a/tests/test.js +++ b/tests/test.js @@ -82,6 +82,9 @@ eco.setMoney(guildId, targetId, 5_000); function makeContext(userId, options = {}) { const currentUser = client.users.cache.get(userId); const interaction = { + user: currentUser, + guild: { id: guildId, name: "Test Guild" }, + guildId, createdTimestamp: Date.now(), options: { getUser: (name) => (options[name] && typeof options[name] === "object" ? options[name] : null), @@ -93,11 +96,6 @@ function makeContext(userId, options = {}) { }; const replies = []; const ctx = createInteractionContext(interaction, client); - ctx.user = currentUser; - ctx.userId = userId; - ctx.guild = { id: guildId, name: "Test Guild", iconURL: () => null }; - ctx.guildId = guildId; - ctx.member = { permissions: { has: () => userId === adminId } }; ctx.reply = async (payload) => { replies.push(payload); return payload; }; return { ctx, replies }; } @@ -111,7 +109,6 @@ async function runSlash(name, userId = adminId, options = {}) { } (async () => { - // Core economy invariants. assert.equal(eco.getBalance(guildId, adminId), 20_000); const transfer = eco.transfer(guildId, adminId, targetId, 500); assert.equal(transfer.fromBalance, 19_500); @@ -132,7 +129,6 @@ async function runSlash(name, userId = adminId, options = {}) { assert.equal(cooldown2.onCooldown, true); assert.ok(cooldown2.remainingMs > 0); - // Every command executes through the same native context used by slash commands. await runSlash("addmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }); await runSlash("bal"); await runSlash("beg"); @@ -156,9 +152,8 @@ async function runSlash(name, userId = adminId, options = {}) { const unauthorized = await runSlash("addmoney", targetId, { kullanici: client.users.cache.get(adminId), miktar: 100 }); assert.equal(unauthorized.ephemeral, true); - // Native interactionCreate path. const eventReplies = []; - const interaction = { + const eventInteraction = { commandName: "ping", isChatInputCommand: () => true, inGuild: () => true, @@ -173,10 +168,9 @@ async function runSlash(name, userId = adminId, options = {}) { get replied() { return eventReplies.length > 0; }, get deferred() { return false; } }; - await require("../events/interactionCreate")(client, interaction); + await require("../events/interactionCreate")(client, eventInteraction); assert.ok(eventReplies.length > 0, "interactionCreate /ping yanıt üretmedi."); - // Native prefix path with Turkish alias. const prefixReplies = []; const message = { guild: { id: guildId }, From 5f539f51c7017eaf9ca73a04f22e72199a1999a2 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:28:26 +0300 Subject: [PATCH 125/175] Fix rob cooldown eligibility flow --- commands/rob.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/commands/rob.js b/commands/rob.js index ad1ef6d..0709a60 100644 --- a/commands/rob.js +++ b/commands/rob.js @@ -19,17 +19,23 @@ exports.execute = async (ctx) => { if (!target || target.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); if (target.id === ctx.userId) return ctx.reply("Kendini soyamazsın."); - const cooldown = ctx.client.eco.useCooldown(ctx.guildId, ctx.userId, "rob", 60_000); + const cooldown = ctx.client.eco.getCooldown(ctx.guildId, ctx.userId, "rob", 60_000); if (cooldown.onCooldown) return ctx.reply(`Tekrar soygun deneyebilmek için **${formatRemaining(cooldown.remainingMs)}** beklemelisin.`); const targetBalance = ctx.client.eco.getBalance(ctx.guildId, target.id); if (targetBalance < 1) return ctx.reply("Bu kullanıcının çalınabilecek parası yok."); - if (Math.random() < 0.2) return ctx.reply(failMessages[Math.floor(Math.random() * failMessages.length)]); + + if (Math.random() < 0.2) { + ctx.client.eco.setCooldown(ctx.guildId, ctx.userId, "rob"); + return ctx.reply(failMessages[Math.floor(Math.random() * failMessages.length)]); + } const amount = Math.min(targetBalance, randomInt(10, 59)); const result = ctx.client.eco.rob(ctx.guildId, ctx.userId, target.id, amount); if (result.error) return ctx.reply(result.error); - return ctx.reply(`${target} kullanıcısından **${formatMoney(amount)}** çaldın. Yeni bakiyen **${formatMoney(ctx.client.eco.getBalance(ctx.guildId, ctx.userId))}**.`); + + ctx.client.eco.setCooldown(ctx.guildId, ctx.userId, "rob"); + return ctx.reply(`${target} kullanıcısından **${formatMoney(amount)}** çaldın. Yeni bakiyen **${formatMoney(ctx.client.eco.getBalance(ctx.guildId, ctx.userId))**}.`); }; exports.help = { name: exports.name, aliases: exports.aliases, usage: "rob " }; From f7ad368fa0f0fdbf1a416833fc6376185991e760 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:28:32 +0300 Subject: [PATCH 126/175] Fix rob response template --- commands/rob.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/rob.js b/commands/rob.js index 0709a60..5caf51f 100644 --- a/commands/rob.js +++ b/commands/rob.js @@ -35,7 +35,7 @@ exports.execute = async (ctx) => { if (result.error) return ctx.reply(result.error); ctx.client.eco.setCooldown(ctx.guildId, ctx.userId, "rob"); - return ctx.reply(`${target} kullanıcısından **${formatMoney(amount)}** çaldın. Yeni bakiyen **${formatMoney(ctx.client.eco.getBalance(ctx.guildId, ctx.userId))**}.`); + return ctx.reply(`${target} kullanıcısından **${formatMoney(amount)}** çaldın. Yeni bakiyen **${formatMoney(ctx.client.eco.getBalance(ctx.guildId, ctx.userId))}**.`); }; exports.help = { name: exports.name, aliases: exports.aliases, usage: "rob " }; From a972848c3dcb5b452ce514ebd40882294e8a9c41 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:54:43 +0300 Subject: [PATCH 127/175] Fix database lifecycle and closed connection handling --- lib/database.js | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/database.js b/lib/database.js index 45c26e4..ed0f5a2 100644 --- a/lib/database.js +++ b/lib/database.js @@ -3,11 +3,20 @@ const fs = require("node:fs"); const path = require("node:path"); const dataDir = path.join(process.cwd(), "data"); -fs.mkdirSync(dataDir, { recursive: true }); class KeyValueStore { constructor(filePath = path.join(dataDir, "economy.sqlite")) { - this.connection = new Database(filePath); + this.filePath = path.resolve(filePath); + this.connection = null; + this.statements = null; + this.open(); + } + + open() { + if (this.connection?.open && this.statements) return this; + + fs.mkdirSync(path.dirname(this.filePath), { recursive: true }); + this.connection = new Database(this.filePath); this.connection.pragma("journal_mode = WAL"); this.connection.pragma("foreign_keys = ON"); this.connection.pragma("synchronous = NORMAL"); @@ -17,13 +26,20 @@ class KeyValueStore { value TEXT NOT NULL ); `); + this.statements = { get: this.connection.prepare("SELECT value FROM kv WHERE id = ?"), - set: this.connection.prepare(`INSERT INTO kv (id, value) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value`), + set: this.connection.prepare("INSERT INTO kv (id, value) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value"), delete: this.connection.prepare("DELETE FROM kv WHERE id = ?"), all: this.connection.prepare("SELECT id, value FROM kv ORDER BY id"), clear: this.connection.prepare("DELETE FROM kv") }; + return this; + } + + ensureOpen() { + if (!this.connection?.open || !this.statements) this.open(); + if (!this.connection?.open || !this.statements) throw new Error("Veritabanı bağlantısı açılamadı."); } encode(value) { @@ -37,6 +53,7 @@ class KeyValueStore { } get(key, fallback = null) { + this.ensureOpen(); const row = this.statements.get.get(String(key)); return row ? this.decode(row.value) : fallback; } @@ -46,10 +63,12 @@ class KeyValueStore { } has(key) { + this.ensureOpen(); return Boolean(this.statements.get.get(String(key))); } set(key, value) { + this.ensureOpen(); this.statements.set.run(String(key), this.encode(value)); return value; } @@ -63,10 +82,12 @@ class KeyValueStore { } delete(key) { + this.ensureOpen(); return this.statements.delete.run(String(key)).changes > 0; } all() { + this.ensureOpen(); return this.statements.all.all().map(({ id, value }) => ({ ID: id, data: this.decode(value) })); } @@ -75,7 +96,7 @@ class KeyValueStore { return this.all().filter(({ ID }) => ID.startsWith(normalized)); } - getPrefix(guildId, fallback) { + getPrefix(guildId, fallback = "!") { return String(this.get(`prefix:${guildId}`, fallback)); } @@ -89,17 +110,20 @@ class KeyValueStore { } clear() { + this.ensureOpen(); return this.statements.clear.run().changes; } transaction(callback) { + this.ensureOpen(); return this.connection.transaction(callback)(); } close() { - if (this.connection.open) this.connection.close(); + if (this.connection?.open) this.connection.close(); + this.connection = null; + this.statements = null; } } -module.exports = new KeyValueStore(); -module.exports.KeyValueStore = KeyValueStore; +module.exports = { KeyValueStore, defaultPath: path.join(dataDir, "economy.sqlite") }; From bc860464fb845e65f412d16113f3593f4a0482f4 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:55:01 +0300 Subject: [PATCH 128/175] Add English and Turkish localization system --- lib/i18n.js | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 lib/i18n.js diff --git a/lib/i18n.js b/lib/i18n.js new file mode 100644 index 0000000..3056827 --- /dev/null +++ b/lib/i18n.js @@ -0,0 +1,219 @@ +const locales = { + en: { + locale: "en-US", + commands: { + addmoney: { description: "Add money to a user." }, + bal: { description: "Show a user's balance." }, + beg: { description: "Try to earn a random amount by begging." }, + buy: { description: "Buy an item from the shop." }, + daily: { description: "Claim your daily reward." }, + help: { description: "Show all available bot commands." }, + inventory: { description: "Show the items in your inventory." }, + lb: { description: "Show the server economy leaderboard." }, + ping: { description: "Show the bot latency." }, + prefix: { description: "Change the server prefix." }, + rob: { description: "Try to steal a random amount from another user." }, + search: { description: "Search for a random amount of money." }, + setmoney: { description: "Set a user's balance." }, + shop: { description: "Show the available shop items." }, + transfer: { description: "Transfer money to another user." }, + weekly: { description: "Claim your weekly reward." }, + work: { description: "Work a random job and earn money." } + }, + errors: { + generic: "Something went wrong while running this command. Check the console.", + noUser: "Please provide a valid user.", + noAmount: "Please provide a valid positive amount.", + botEconomy: "Bot accounts do not have an economy balance.", + noPermission: "You need administrator or Manage Server permission to use this command.", + noPermissionRole: "You are not allowed to use this command.", + self: "You cannot target yourself.", + insufficient: "Insufficient balance.", + invalidProduct: "That item does not exist in the shop.", + noMoneyToSteal: "That user has no money you can steal.", + selfRob: "You cannot rob yourself.", + botsDisabled: "Bot accounts cannot use this command.", + guildOnly: "This command can only be used in a server.", + unavailable: "This command is currently unavailable.", + invalidPrefix: "Please provide a prefix between 1 and 5 characters without spaces.", + samePrefix: "That prefix is already in use.", + cooldown: "You must wait **{time}** before using this command again." + }, + economy: { + balanceTitle: "Balance", + user: "User", + balance: "Balance", + rank: "Rank", + moneyAddedTitle: "Money Added", + added: "Added", + newBalance: "New Balance", + moneySetTitle: "Balance Updated", + newBalanceSet: "New Balance", + moneyRemovedTitle: "Money Removed", + removed: "Removed", + daily: "You received **{amount}** as your daily reward. Your new balance is **{balance}**.", + dailyCooldown: "You already claimed your daily reward. Come back in **{time}**.", + weekly: "You received **{amount}** as your weekly reward. Your new balance is **{balance}**.", + weeklyCooldown: "You already claimed your weekly reward. Come back in **{time}**.", + work: "You worked as a **{job}** and earned **{amount}**. Your new balance is **{balance}**.", + workCooldown: "You are tired. Come back in **{time}**.", + beg: "**{donor}** gave you **{amount}**. Your new balance is **{balance}**.", + begLost: "**{donor}:** Bad luck. You did not earn anything this time.", + begCooldown: "You must wait **{time}** before begging again.", + search: "You searched around and found **{amount}**. Your new balance is **{balance}**.", + searchCooldown: "You must wait **{time}** before searching again.", + purchase: "You bought **{item}** for **{price}**. Remaining balance: **{balance}**.", + purchaseNeed: "You need **{price}** to buy this item, but you only have **{balance}**.", + inventoryEmpty: "Your inventory is empty.", + inventoryTitle: "{user} — Inventory", + inventoryItem: "Quantity: **{count}**\\nUnit price: **{price}**", + shopTitle: "Shop", + shopItem: "Price: **{price}**", + transfer: "You sent **{amount}** to <@{target}>. Your new balance is **{balance}**.", + robSuccess: "You stole **{amount}** from <@{target}>. Your new balance is **{balance}**.", + robCooldown: "You must wait **{time}** before attempting another robbery.", + robFail: [ + "Your robbery attempt failed.", + "The target noticed you and your plan failed.", + "That user was too careful. Better luck next time." + ], + leaderboardTitle: "Economy Leaderboard", + leaderboardEmpty: "There are no economy accounts on this server yet.", + leaderboardLine: "**#{position}** <@{id}> — **{money}**", + prefixUpdated: "Server prefix changed to **{prefix}**.", + helpTitle: "EconomyBot Commands", + helpDescription: "There are **{count}** commands. Current prefix: `{prefix}`", + helpFooter: "Requested by {user}", + pingTitle: "Pong!", + pingApi: "API Latency", + pingClient: "Client Latency", + adminMoney: "You changed <@{user}> by **{amount}**. New balance: **{balance}**.", + setMoney: "You set <@{user}>'s balance to **{balance}**." + } + }, + tr: { + locale: "tr-TR", + commands: { + addmoney: { description: "Bir kullanıcıya para ekler." }, + bal: { description: "Bir kullanıcının bakiyesini gösterir." }, + beg: { description: "Dilenerek rastgele miktarda para kazanmaya çalışır." }, + buy: { description: "Mağazadan bir ürün satın alır." }, + daily: { description: "Günlük ödülünü alır." }, + help: { description: "Kullanılabilir tüm bot komutlarını gösterir." }, + inventory: { description: "Envanterindeki ürünleri gösterir." }, + lb: { description: "Sunucunun ekonomi sıralamasını gösterir." }, + ping: { description: "Bot gecikmesini gösterir." }, + prefix: { description: "Sunucu prefix'ini değiştirir." }, + rob: { description: "Başka bir kullanıcıdan rastgele miktarda para çalmayı dener." }, + search: { description: "Rastgele miktarda para arar." }, + setmoney: { description: "Bir kullanıcının bakiyesini ayarlar." }, + shop: { description: "Mağazadaki mevcut ürünleri gösterir." }, + transfer: { description: "Başka bir kullanıcıya para gönderir." }, + weekly: { description: "Haftalık ödülünü alır." }, + work: { description: "Rastgele bir iş yaparak para kazanır." } + }, + errors: { + generic: "Komut çalıştırılırken beklenmeyen bir hata oluştu. Konsolu kontrol edin.", + noUser: "Lütfen geçerli bir kullanıcı belirt.", + noAmount: "Lütfen geçerli ve pozitif bir miktar belirt.", + botEconomy: "Bot hesaplarının ekonomi bakiyesi bulunmaz.", + noPermission: "Bu komutu kullanmak için Yönetici veya Sunucuyu Yönet yetkisine sahip olmalısın.", + noPermissionRole: "Bu komutu kullanmaya yetkin bulunmuyor.", + self: "Kendini hedefleyemezsin.", + insufficient: "Yetersiz bakiye.", + invalidProduct: "Bu ürün mağazada bulunmuyor.", + noMoneyToSteal: "Bu kullanıcının çalınabilecek parası yok.", + selfRob: "Kendini soyamazsın.", + botsDisabled: "Bot hesapları bu komutu kullanamaz.", + guildOnly: "Bu komut yalnızca bir sunucuda kullanılabilir.", + unavailable: "Bu komut şu anda kullanılamıyor.", + invalidPrefix: "Lütfen boşluk içermeyen, 1-5 karakter uzunluğunda bir prefix belirt.", + samePrefix: "Bu prefix zaten kullanılıyor.", + cooldown: "Bu komutu tekrar kullanmak için **{time}** beklemelisin." + }, + economy: { + balanceTitle: "Bakiye", + user: "Kullanıcı", + balance: "Bakiye", + rank: "Sıralama", + moneyAddedTitle: "Para Eklendi", + added: "Eklenen", + newBalance: "Yeni Bakiye", + moneySetTitle: "Bakiye Güncellendi", + newBalanceSet: "Yeni Bakiye", + moneyRemovedTitle: "Para Çıkarıldı", + removed: "Çıkarılan", + daily: "Günlük ödül olarak **{amount}** kazandın. Yeni bakiyen **{balance}**.", + dailyCooldown: "Günlük ödülünü zaten aldın. Tekrar almak için **{time}** beklemelisin.", + weekly: "Haftalık ödül olarak **{amount}** kazandın. Yeni bakiyen **{balance}**.", + weeklyCooldown: "Haftalık ödülünü zaten aldın. Tekrar almak için **{time}** beklemelisin.", + work: "**{job}** olarak çalıştın ve **{amount}** kazandın. Yeni bakiyen **{balance}**.", + workCooldown: "Çalışmak için biraz dinlenmelisin. **{time}** sonra tekrar dene.", + beg: "**{donor}** sana **{amount}** verdi. Yeni bakiyen **{balance}**.", + begLost: "**{donor}:** Şansın yaver gitmedi; bu sefer para kazanamadın.", + begCooldown: "Tekrar dilenebilmek için **{time}** beklemelisin.", + search: "Etrafta arama yaptın ve **{amount}** buldun. Yeni bakiyen **{balance}**.", + searchCooldown: "Tekrar arama yapmak için **{time}** beklemelisin.", + purchase: "**{item}** ürününü **{price}** karşılığında satın aldın. Kalan bakiyen **{balance}**.", + purchaseNeed: "Bu ürün için **{price}** gerekiyor fakat bakiyen **{balance}**.", + inventoryEmpty: "Envanterin boş.", + inventoryTitle: "{user} — Envanter", + inventoryItem: "Miktar: **{count}**\\nBirim fiyat: **{price}**", + shopTitle: "Mağaza", + shopItem: "Fiyat: **{price}**", + transfer: "<@{target}> kullanıcısına **{amount}** gönderdin. Yeni bakiyen **{balance}**.", + robSuccess: "<@{target}> kullanıcısından **{amount}** çaldın. Yeni bakiyen **{balance}**.", + robCooldown: "Tekrar soygun deneyebilmek için **{time}** beklemelisin.", + robFail: [ + "Soygun girişimin başarısız oldu.", + "Hedef seni fark etti ve planın bozuldu.", + "Soymaya çalıştığın kişi dikkatliymiş. Bu sefer olmadı." + ], + leaderboardTitle: "Ekonomi Sıralaması", + leaderboardEmpty: "Bu sunucuda henüz ekonomi hesabı bulunmuyor.", + leaderboardLine: "**#{position}** <@{id}> — **{money}**", + prefixUpdated: "Sunucu prefix'i **{prefix}** olarak değiştirildi.", + helpTitle: "EconomyBot Komutları", + helpDescription: "Toplam **{count}** komut bulunuyor. Güncel prefix: `{prefix}`", + helpFooter: "{user} tarafından istendi", + pingTitle: "Pong!", + pingApi: "API Gecikmesi", + pingClient: "İstemci Gecikmesi", + adminMoney: "<@{user}> kullanıcısının bakiyesine **{amount}** eklendi. Yeni bakiye: **{balance}**.", + setMoney: "<@{user}> kullanıcısının bakiyesi **{balance}** olarak ayarlandı." + } + } +}; + +function normalizeLanguage(language) { + const normalized = String(language || "en").toLowerCase(); + if (normalized === "tr" || normalized === "turkish" || normalized === "tr-tr") return "tr"; + return "en"; +} + +function createTranslator(language) { + const lang = normalizeLanguage(language); + const catalog = locales[lang]; + const translate = (key, variables = {}) => { + const parts = String(key).split("."); + let value = catalog; + for (const part of parts) value = value?.[part]; + if (Array.isArray(value)) value = value[Math.floor(Math.random() * value.length)]; + if (typeof value !== "string") return key; + return value.replace(/\{(\w+)\}/g, (_, name) => String(variables[name] ?? `{${name}}`)); + }; + translate.language = lang; + translate.locale = catalog.locale; + return translate; +} + +function getLanguage(config) { + return normalizeLanguage(config?.language); +} + +function commandDescription(commandName, language) { + const lang = normalizeLanguage(language); + return locales[lang].commands[commandName]?.description || locales.en.commands[commandName]?.description || commandName; +} + +module.exports = { locales, normalizeLanguage, createTranslator, getLanguage, commandDescription }; From c49d55bba2c8fb4f4bd96461089bc58c3e7d443f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:55:10 +0300 Subject: [PATCH 129/175] Refactor command contexts with centralized localization --- lib/context.js | 51 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/lib/context.js b/lib/context.js index 889b204..6370964 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1,6 +1,14 @@ +const { createTranslator, getLanguage } = require("./i18n"); + +function createBaseContext(client, language) { + const t = createTranslator(language ?? getLanguage(client.config)); + return { client, language: t.language, t }; +} + function createInteractionContext(interaction, client) { + const base = createBaseContext(client); return { - client, + ...base, interaction, isSlash: true, guild: interaction.guild, @@ -12,9 +20,18 @@ function createInteractionContext(interaction, client) { option(name) { return interaction.options.get(name)?.value ?? null; }, - userOption(name) { + userOption(name = "kullanici") { return interaction.options.getUser(name); }, + memberOption(name = "kullanici") { + return interaction.options.getMember(name); + }, + amount(name = "miktar") { + return interaction.options.getInteger(name); + }, + stringOption(name) { + return interaction.options.getString(name); + }, async reply(payload) { return interaction.reply(payload); } @@ -22,8 +39,9 @@ function createInteractionContext(interaction, client) { } function createPrefixContext(message, client, args) { + const base = createBaseContext(client); return { - client, + ...base, message, interaction: null, isSlash: false, @@ -33,11 +51,26 @@ function createPrefixContext(message, client, args) { userId: message.author.id, member: message.member, args, + prefix: null, option() { return null; }, userOption() { - return null; + const mentioned = message.mentions?.users?.first?.(); + return mentioned || null; + }, + memberOption() { + const mentioned = message.mentions?.members?.first?.(); + return mentioned || null; + }, + amount(_name = "miktar", argumentIndex = 1) { + const value = args[argumentIndex]; + if (!/^\d+$/.test(String(value ?? ""))) return null; + const number = Number(value); + return Number.isSafeInteger(number) && number > 0 ? number : null; + }, + stringOption(_name, argumentIndex = 0) { + return args[argumentIndex] ?? null; }, async reply(payload) { return message.reply(payload); @@ -45,12 +78,4 @@ function createPrefixContext(message, client, args) { }; } -async function replyContext(ctx, payload) { - return ctx.reply(payload); -} - -module.exports = { - createInteractionContext, - createPrefixContext, - replyContext -}; +module.exports = { createInteractionContext, createPrefixContext }; From 421dcf54f0f18f1c6a9756c858e28c9e1ae9a204 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:55:21 +0300 Subject: [PATCH 130/175] Fix admin permission handling and localize utility formatting --- lib/commandUtils.js | 73 +++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/lib/commandUtils.js b/lib/commandUtils.js index 5dc78a2..64a3870 100644 --- a/lib/commandUtils.js +++ b/lib/commandUtils.js @@ -1,8 +1,35 @@ const { PermissionsBitField } = require("discord.js"); +function hasPermissionValue(permissionSource, permission) { + if (!permissionSource) return false; + if (typeof permissionSource.has === "function") return permissionSource.has(permission, true); + try { + return new PermissionsBitField(permissionSource).has(permission, true); + } catch { + return false; + } +} + function isAdmin(ctx) { - if (ctx.client.config.admins.includes(ctx.userId)) return true; - if (ctx.member?.permissions?.has(PermissionsBitField.Flags.ManageGuild)) return true; + const config = ctx.client.config || {}; + const userId = String(ctx.userId || ""); + const adminUsers = Array.isArray(config.admins) ? config.admins.map(String) : []; + if (adminUsers.includes(userId)) return true; + + const permissions = ctx.isSlash ? ctx.interaction?.memberPermissions : ctx.member?.permissions; + if (hasPermissionValue(permissions, PermissionsBitField.Flags.Administrator)) return true; + if (hasPermissionValue(permissions, PermissionsBitField.Flags.ManageGuild)) return true; + + const roleIds = Array.isArray(config.adminRoles) ? config.adminRoles.map(String) : []; + const roleNames = Array.isArray(config.adminRoleNames) ? config.adminRoleNames.map((name) => String(name).trim().toLowerCase()).filter(Boolean) : []; + const roles = ctx.member?.roles?.cache; + if (roles) { + for (const role of roles.values()) { + if (roleIds.includes(String(role.id))) return true; + if (roleNames.includes(String(role.name).trim().toLowerCase())) return true; + } + } + return false; } @@ -22,7 +49,7 @@ function parseNonNegativeInteger(value) { async function resolveUser(ctx, argumentIndex = 0) { if (ctx.isSlash) return ctx.userOption("kullanici"); - const mentioned = ctx.message.mentions.users.first(); + const mentioned = ctx.message.mentions?.users?.first?.(); if (mentioned) return mentioned; const id = ctx.args[argumentIndex]; if (!/^\d{17,20}$/.test(String(id || ""))) return null; @@ -30,8 +57,8 @@ async function resolveUser(ctx, argumentIndex = 0) { } async function resolveMember(ctx, argumentIndex = 0) { - if (ctx.isSlash) return ctx.interaction.options.getMember("kullanici") || null; - const mentioned = ctx.message.mentions.members.first(); + if (ctx.isSlash) return ctx.memberOption("kullanici"); + const mentioned = ctx.message.mentions?.members?.first?.(); if (mentioned) return mentioned; const id = ctx.args[argumentIndex]; if (!/^\d{17,20}$/.test(String(id || ""))) return null; @@ -39,18 +66,19 @@ async function resolveMember(ctx, argumentIndex = 0) { } function getAmount(ctx, optionName = "miktar", argumentIndex = 1) { - return ctx.isSlash ? ctx.interaction.options.getInteger(optionName) : parsePositiveInteger(ctx.args[argumentIndex]); + return ctx.isSlash ? ctx.amount(optionName) : parsePositiveInteger(ctx.args[argumentIndex]); } function getProduct(ctx) { - return ctx.isSlash ? ctx.interaction.options.getString("urun") : ctx.args[0] || null; + return ctx.isSlash ? ctx.stringOption("urun") : ctx.args[0] || null; } -function formatMoney(value) { - return `${Number(value).toLocaleString("tr-TR")} 💸`; +function formatMoney(value, language = "en") { + const locale = String(language).toLowerCase().startsWith("tr") ? "tr-TR" : "en-US"; + return `${Number(value).toLocaleString(locale)} 💸`; } -function formatRemaining(ms) { +function formatRemaining(ms, language = "en") { let seconds = Math.max(0, Math.ceil(ms / 1000)); const days = Math.floor(seconds / 86400); seconds %= 86400; @@ -58,27 +86,20 @@ function formatRemaining(ms) { seconds %= 3600; const minutes = Math.floor(seconds / 60); seconds %= 60; + const units = String(language).toLowerCase().startsWith("tr") + ? ["gün", "saat", "dakika", "saniye"] + : ["day", "hour", "minute", "second"]; const parts = []; - if (days) parts.push(`${days} gün`); - if (hours) parts.push(`${hours} saat`); - if (minutes) parts.push(`${minutes} dakika`); - if (seconds || parts.length === 0) parts.push(`${seconds} saniye`); + if (days) parts.push(`${days} ${units[0]}${!String(language).toLowerCase().startsWith("tr") && days !== 1 ? "s" : ""}`); + if (hours) parts.push(`${hours} ${units[1]}${!String(language).toLowerCase().startsWith("tr") && hours !== 1 ? "s" : ""}`); + if (minutes) parts.push(`${minutes} ${units[2]}${!String(language).toLowerCase().startsWith("tr") && minutes !== 1 ? "s" : ""}`); + if (seconds || parts.length === 0) parts.push(`${seconds} ${units[3]}${!String(language).toLowerCase().startsWith("tr") && seconds !== 1 ? "s" : ""}`); return parts.join(", "); } function randomInt(min, max) { + if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max) || min > max) throw new RangeError("Geçersiz rastgele sayı aralığı."); return Math.floor(Math.random() * (max - min + 1)) + min; } -module.exports = { - isAdmin, - parsePositiveInteger, - parseNonNegativeInteger, - resolveUser, - resolveMember, - getAmount, - getProduct, - formatMoney, - formatRemaining, - randomInt -}; +module.exports = { isAdmin, parsePositiveInteger, parseNonNegativeInteger, resolveUser, resolveMember, getAmount, getProduct, formatMoney, formatRemaining, randomInt }; From 397912d5f050e721b3c3df4a8ff5de81e4476788 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:55:30 +0300 Subject: [PATCH 131/175] Fix runtime database lifecycle and centralize config language --- index.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 3485069..12cbe72 100644 --- a/index.js +++ b/index.js @@ -1,22 +1,28 @@ const path = require("node:path"); const fs = require("node:fs"); -const { Client, Collection, GatewayIntentBits } = require("discord.js"); +const { Client, Collection, GatewayIntentBits, Partials } = require("discord.js"); const config = require("./botConfig"); -const database = require("./lib/database"); +const { KeyValueStore } = require("./lib/database"); const EconomyManager = require("./lib/economy"); +const { normalizeLanguage } = require("./lib/i18n"); if (!config || typeof config !== "object") throw new Error("botConfig.js geçerli bir yapılandırma döndürmelidir."); if (!config.token || config.token === "YOUR_TOKEN") throw new Error("botConfig.js içindeki token ayarlanmalı."); if (!config.serverId || !/^\d{17,20}$/.test(String(config.serverId))) throw new Error("botConfig.js içindeki serverId geçerli bir Discord sunucu ID'si olmalı."); -if (!config.prefix || typeof config.prefix !== "string" || /\s/.test(config.prefix)) throw new Error("botConfig.js içindeki prefix geçerli olmalı."); +if (!config.prefix || typeof config.prefix !== "string" || !/^\S{1,5}$/.test(config.prefix)) throw new Error("botConfig.js içindeki prefix 1-5 karakter ve boşluksuz olmalı."); if (!Array.isArray(config.admins)) throw new Error("botConfig.js içindeki admins bir dizi olmalı."); +if (!Array.isArray(config.adminRoles)) throw new Error("botConfig.js içindeki adminRoles bir dizi olmalı."); +if (!Array.isArray(config.adminRoleNames)) throw new Error("botConfig.js içindeki adminRoleNames bir dizi olmalı."); +config.language = normalizeLanguage(config.language); +const database = new KeyValueStore(); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent - ] + ], + partials: [Partials.Channel] }); client.config = config; @@ -26,20 +32,21 @@ client.commands = new Collection(); client.aliases = new Collection(); client.shop = Object.freeze({ laptop: { id: "laptop", name: "Laptop", cost: 2000 }, - mobile: { id: "mobile", name: "Mobile", cost: 1000 }, + mobile: { id: "Mobile", name: "Mobile", cost: 1000 }, pc: { id: "pc", name: "PC", cost: 3000 } }); const commandsPath = path.join(__dirname, "commands"); const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js")).sort(); + for (const file of commandFiles) { const command = require(path.join(commandsPath, file)); - const name = String(command?.name || command?.help?.name || "").toLowerCase(); + const name = String(command?.name || "").trim().toLowerCase(); if (!name || typeof command.execute !== "function" || typeof command.data?.toJSON !== "function") { throw new TypeError(`commands/${file}: name, data veya execute eksik.`); } - if (client.commands.has(name)) throw new Error(`Yinelenen komut adı: ${name}`); if (command.help?.name !== name) throw new Error(`commands/${file}: help.name ve name eşleşmiyor.`); + if (client.commands.has(name)) throw new Error(`Yinelenen komut adı: ${name}`); client.commands.set(name, command); for (const alias of command.aliases || []) { @@ -61,15 +68,17 @@ client.on("warn", (warning) => console.warn("Discord uyarısı:", warning)); process.on("unhandledRejection", (error) => console.error("Yakalanmamış Promise hatası:", error)); process.on("uncaughtException", (error) => console.error("Yakalanmamış uygulama hatası:", error)); +let shuttingDown = false; const shutdown = () => { - client.destroy(); - database.close(); + if (shuttingDown) return; + shuttingDown = true; + try { client.destroy(); } finally { database.close(); } }; process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); client.login(config.token).catch((error) => { console.error("Discord'a giriş yapılamadı:", error); - database.close(); + shutdown(); process.exitCode = 1; }); From f68de60efd1ea249475ef85603e2abec51701f71 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:55:34 +0300 Subject: [PATCH 132/175] Add language and admin role configuration --- botConfig.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/botConfig.js b/botConfig.js index aefffb8..5465c2a 100644 --- a/botConfig.js +++ b/botConfig.js @@ -1,10 +1,24 @@ module.exports = { token: "YOUR_TOKEN", - prefix: "PREFIX", + prefix: "!", serverId: "YOUR_SERVER_ID", + + // "en" = English, "tr" = Turkish. + language: "en", + + // User IDs that always have economy-admin access. admins: [ - "PEOPLE WHO CAN USE ADD MONEY (IDS)" + "YOUR_ADMIN_USER_ID" ], - debug: true, - countChannel: "countChannelID" + + // Role IDs that have economy-admin access. + adminRoles: [ + "YOUR_ADMIN_ROLE_ID" + ], + + // Optional role names. Use role IDs above for the safest configuration. + adminRoleNames: [], + + debug: false, + countChannel: "YOUR_COUNT_CHANNEL_ID" }; From 42d882426f04bad3a1844461079868cb38675a64 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:55:47 +0300 Subject: [PATCH 133/175] Localize addmoney and harden admin response --- commands/addmoney.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/commands/addmoney.js b/commands/addmoney.js index 230c443..642e325 100644 --- a/commands/addmoney.js +++ b/commands/addmoney.js @@ -1,34 +1,34 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { isAdmin, resolveUser, getAmount, formatMoney } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); exports.data = new SlashCommandBuilder() .setName("addmoney") - .setDescription("Bir kullanıcıya para ekler.") - .addUserOption((option) => option.setName("kullanici").setDescription("Para eklenecek kullanıcı.").setRequired(true)) - .addIntegerOption((option) => option.setName("miktar").setDescription("Eklenecek miktar.").setMinValue(1).setMaxValue(2147483647).setRequired(true)); + .setDescription(commandDescription("addmoney", require("../botConfig").language)) + .addUserOption((option) => option.setName("kullanici").setDescription("User to receive money.").setRequired(true)) + .addIntegerOption((option) => option.setName("miktar").setDescription("Amount to add.").setMinValue(1).setMaxValue(2147483647).setRequired(true)); exports.name = "addmoney"; exports.aliases = ["addbal", "paraekle", "para-ekle"]; exports.execute = async (ctx) => { - if (!isAdmin(ctx)) return ctx.reply({ content: "Bu komutu kullanmak için sunucu yönetimi yetkisine sahip olmalısın.", ephemeral: true }); + if (!isAdmin(ctx)) return ctx.reply({ content: ctx.t("errors.noPermission"), ephemeral: true }); const user = await resolveUser(ctx, 0); const amount = getAmount(ctx, "miktar", 1); - if (!user || user.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); - if (!amount) return ctx.reply("Geçerli ve pozitif bir miktar belirtmelisin."); + if (!user || user.bot) return ctx.reply(ctx.t("errors.noUser")); + if (!amount) return ctx.reply(ctx.t("errors.noAmount")); const data = ctx.client.eco.addMoney(ctx.guildId, user.id, amount); const embed = new EmbedBuilder() - .setTitle("Para Eklendi!") + .setTitle(ctx.t("economy.moneyAddedTitle")) .addFields( - { name: "Kullanıcı", value: `<@${user.id}>`, inline: true }, - { name: "Eklenen", value: formatMoney(data.amount), inline: true }, - { name: "Yeni Bakiye", value: formatMoney(data.after), inline: true } + { name: ctx.t("economy.user"), value: `<@${user.id}>`, inline: true }, + { name: ctx.t("economy.added"), value: formatMoney(data.amount, ctx.language), inline: true }, + { name: ctx.t("economy.newBalance"), value: formatMoney(data.after, ctx.language), inline: true } ) .setColor("Blurple") .setThumbnail(user.displayAvatarURL()) .setTimestamp(); - return ctx.reply({ embeds: [embed] }); }; -exports.help = { name: exports.name, aliases: exports.aliases, usage: "addmoney @kullanıcı " }; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "addmoney @user " }; From 475f441a84e2f17150a36c2dc8b42fc4b623a6be Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:55:54 +0300 Subject: [PATCH 134/175] Localize balance command --- commands/bal.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/commands/bal.js b/commands/bal.js index af41272..f1ccf79 100644 --- a/commands/bal.js +++ b/commands/bal.js @@ -1,31 +1,31 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { resolveUser, formatMoney } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); exports.data = new SlashCommandBuilder() .setName("bal") - .setDescription("Bir kullanıcının bakiyesini gösterir.") - .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi görüntülenecek kullanıcı.")); + .setDescription(commandDescription("bal", require("../botConfig").language)) + .addUserOption((option) => option.setName("kullanici").setDescription("User whose balance should be shown.")); exports.name = "bal"; exports.aliases = ["money", "credits", "balance", "bakiye", "para"]; exports.execute = async (ctx) => { - const user = (ctx.isSlash ? ctx.userOption("kullanici") : await resolveUser(ctx, 0)) || ctx.user; - if (user.bot) return ctx.reply("Bot hesaplarının ekonomisi gösterilmez."); + const user = (ctx.userOption("kullanici") || (ctx.isSlash ? null : await resolveUser(ctx, 0)) || ctx.user); + if (user.bot) return ctx.reply(ctx.t("errors.botEconomy")); const balance = ctx.client.eco.getBalance(ctx.guildId, user.id); const position = ctx.client.eco.getPosition(ctx.guildId, user.id); const embed = new EmbedBuilder() - .setTitle("Bakiye") + .setTitle(ctx.t("economy.balanceTitle")) .addFields( - { name: "Kullanıcı", value: `<@${user.id}>`, inline: true }, - { name: "Bakiye", value: formatMoney(balance), inline: true }, - { name: "Sıralama", value: `#${position}`, inline: true } + { name: ctx.t("economy.user"), value: `<@${user.id}>`, inline: true }, + { name: ctx.t("economy.balance"), value: formatMoney(balance, ctx.language), inline: true }, + { name: ctx.t("economy.rank"), value: `#${position}`, inline: true } ) .setColor("Blurple") .setThumbnail(user.displayAvatarURL()) .setTimestamp(); - return ctx.reply({ embeds: [embed] }); }; -exports.help = { name: exports.name, aliases: exports.aliases, usage: "bal [@kullanıcı]" }; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "bal [@user]" }; From 7f8ca047a99e47c994d77ec2d34c8932c9dcc301 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:55:59 +0300 Subject: [PATCH 135/175] Localize beg command --- commands/beg.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/commands/beg.js b/commands/beg.js index eb9bd46..e0f383a 100644 --- a/commands/beg.js +++ b/commands/beg.js @@ -1,20 +1,18 @@ const { SlashCommandBuilder } = require("discord.js"); const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); const donors = ["PewDiePie", "T-Series", "Sans", "Zero"]; - -exports.data = new SlashCommandBuilder().setName("beg").setDescription("Dilenerek rastgele miktarda para kazanmaya çalışır."); +exports.data = new SlashCommandBuilder().setName("beg").setDescription(commandDescription("beg", require("../botConfig").language)); exports.name = "beg"; exports.aliases = ["dilen", "dilenme", "dilencilik"]; exports.execute = async (ctx) => { const amount = randomInt(10, 59); const result = ctx.client.eco.randomEarning(ctx.guildId, ctx.userId, "beg", amount, { canLose: true, cooldown: 60_000 }); - if (result.onCooldown) return ctx.reply(`Tekrar dilenebilmek için **${formatRemaining(result.remainingMs)}** beklemelisin.`); - - const donor = donors[Math.floor(Math.random() * donors.length)]; - if (result.lost) return ctx.reply(`**${donor}:** Şansın yaver gitmedi; bu sefer para kazanamadın.`); - return ctx.reply(`**${donor}** sana **${formatMoney(result.amount)}** verdi. Yeni bakiyen **${formatMoney(result.after)}**.`); + if (result.onCooldown) return ctx.reply(ctx.t("economy.begCooldown", { time: formatRemaining(result.remainingMs, ctx.language) })); + const donor = donors[randomInt(0, donors.length - 1)]; + if (result.lost) return ctx.reply(ctx.t("economy.begLost", { donor })); + return ctx.reply(ctx.t("economy.beg", { donor, amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) })); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "beg" }; From fcca29d5579eb88e9719f2ddf42991082d59f21e Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:56:06 +0300 Subject: [PATCH 136/175] Localize buy command --- commands/buy.js | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/commands/buy.js b/commands/buy.js index 0666609..aa3655e 100644 --- a/commands/buy.js +++ b/commands/buy.js @@ -1,35 +1,26 @@ const { SlashCommandBuilder } = require("discord.js"); const { getProduct, formatMoney } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); exports.data = new SlashCommandBuilder() .setName("buy") - .setDescription("Mağazadan bir ürün satın alır.") - .addStringOption((option) => - option - .setName("urun") - .setDescription("Satın almak istediğin ürün.") - .setRequired(true) - .addChoices( - { name: "Laptop", value: "laptop" }, - { name: "Mobile", value: "mobile" }, - { name: "PC", value: "pc" } - ) - ); + .setDescription(commandDescription("buy", require("../botConfig").language)) + .addStringOption((option) => option.setName("urun").setDescription("Item to buy.").setRequired(true).addChoices( + { name: "Laptop", value: "laptop" }, + { name: "Mobile", value: "mobile" }, + { name: "PC", value: "pc" } + )); exports.name = "buy"; exports.aliases = ["satınal", "satinal", "al"]; exports.execute = async (ctx) => { - const requested = getProduct(ctx); - const normalized = String(requested || "").toLowerCase(); - const item = ctx.client.shop[normalized]; - if (!item) return ctx.reply("Böyle bir ürün bulunmuyor."); - + const requested = String(getProduct(ctx) || "").trim().toLowerCase(); + const item = ctx.client.shop[requested]; + if (!item) return ctx.reply(ctx.t("errors.invalidProduct")); const balance = ctx.client.eco.getBalance(ctx.guildId, ctx.userId); - if (balance < item.cost) return ctx.reply(`Bakiyen yetersiz. Bu ürün için **${formatMoney(item.cost)}** gerekiyor.`); - + if (balance < item.cost) return ctx.reply(ctx.t("economy.purchaseNeed", { price: formatMoney(item.cost, ctx.language), balance: formatMoney(balance, ctx.language) })); const result = ctx.client.eco.purchase(ctx.guildId, ctx.userId, item); - if (result.error) return ctx.reply(result.error); - return ctx.reply(`**${item.name}** ürününü **${formatMoney(item.cost)}** karşılığında satın aldın. Kalan bakiyen **${formatMoney(result.after)}**.`); + if (result.error) return ctx.reply(ctx.t("errors.insufficient")); + return ctx.reply(ctx.t("economy.purchase", { item: item.name, price: formatMoney(item.cost, ctx.language), balance: formatMoney(result.after, ctx.language) })); }; - -exports.help = { name: exports.name, aliases: exports.aliases, usage: "buy <ürün>" }; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "buy " }; From 17c1a3e205ac3868fe5f4a8f1ea5eb4b68a05706 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:56:11 +0300 Subject: [PATCH 137/175] Localize daily command --- commands/daily.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/commands/daily.js b/commands/daily.js index be4d38c..c5ae43f 100644 --- a/commands/daily.js +++ b/commands/daily.js @@ -1,15 +1,14 @@ const { SlashCommandBuilder } = require("discord.js"); const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); -exports.data = new SlashCommandBuilder().setName("daily").setDescription("Günlük para ödülünü alır."); +exports.data = new SlashCommandBuilder().setName("daily").setDescription(commandDescription("daily", require("../botConfig").language)); exports.name = "daily"; exports.aliases = ["günlük", "gunluk"]; exports.execute = async (ctx) => { - const amount = randomInt(100, 599); - const result = ctx.client.eco.daily(ctx.guildId, ctx.userId, amount); - if (result.onCooldown) return ctx.reply(`Günlük ödülünü zaten aldın. Tekrar almak için **${formatRemaining(result.remainingMs)}** beklemelisin.`); - return ctx.reply(`Günlük ödül olarak **${formatMoney(result.amount)}** kazandın. Yeni bakiyen **${formatMoney(result.after)}**.`); + const result = ctx.client.eco.daily(ctx.guildId, ctx.userId, randomInt(100, 599)); + if (result.onCooldown) return ctx.reply(ctx.t("economy.dailyCooldown", { time: formatRemaining(result.remainingMs, ctx.language) })); + return ctx.reply(ctx.t("economy.daily", { amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) })); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "daily" }; From 777a6c936a7a343c204efc2ea8db69d97c768f60 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:56:18 +0300 Subject: [PATCH 138/175] Localize help command --- commands/help.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/commands/help.js b/commands/help.js index 2386376..3ba97d0 100644 --- a/commands/help.js +++ b/commands/help.js @@ -1,28 +1,23 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const { commandDescription } = require("../lib/i18n"); -exports.data = new SlashCommandBuilder().setName("help").setDescription("Botun kullanılabilir komutlarını gösterir."); +exports.data = new SlashCommandBuilder().setName("help").setDescription(commandDescription("help", require("../botConfig").language)); exports.name = "help"; exports.aliases = ["h", "yardım", "yardim", "komutlar"]; exports.execute = async (ctx) => { const commands = [...ctx.client.commands.values()].sort((a, b) => a.name.localeCompare(b.name)); - const fields = commands.map((command) => ({ - name: `/${command.name}`, - value: command.data.description || "Komut", - inline: true - })); - + const fields = commands.map((command) => ({ name: `/${command.name}`, value: commandDescription(command.name, ctx.language), inline: true })); + const prefix = ctx.client.db.getPrefix(ctx.guildId, ctx.client.config.prefix); const embed = new EmbedBuilder() .setAuthor({ name: "INS Development" }) - .setTitle("EconomyBot Komutları") - .setDescription(`Toplam **${commands.length}** komut bulunuyor. Prefix: \`${ctx.client.db.getPrefix(ctx.guildId, ctx.client.config.prefix)}\``) + .setTitle(ctx.t("economy.helpTitle")) + .setDescription(ctx.t("economy.helpDescription", { count: commands.length, prefix })) .addFields(fields) .setColor("Blurple") .setThumbnail(ctx.client.user.displayAvatarURL()) - .setFooter({ text: `${ctx.user.tag} tarafından istendi` }) + .setFooter({ text: ctx.t("economy.helpFooter", { user: ctx.user.tag }) }) .setTimestamp(); - return ctx.reply({ embeds: [embed] }); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "help" }; From 2f58ce685a02840893c8971bb56cc7cfa6043635 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:56:24 +0300 Subject: [PATCH 139/175] Localize inventory command --- commands/inventory.js | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/commands/inventory.js b/commands/inventory.js index e9c42ab..14555b7 100644 --- a/commands/inventory.js +++ b/commands/inventory.js @@ -1,37 +1,25 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { formatMoney } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); -exports.data = new SlashCommandBuilder().setName("inventory").setDescription("Envanterindeki ürünleri gösterir."); +exports.data = new SlashCommandBuilder().setName("inventory").setDescription(commandDescription("inventory", require("../botConfig").language)); exports.name = "inventory"; exports.aliases = ["inv", "envanter", "eşyalar", "esya"]; exports.execute = async (ctx) => { const items = ctx.client.eco.getInventory(ctx.guildId, ctx.userId); - if (items.length === 0) return ctx.reply("Envanterin boş."); - + if (items.length === 0) return ctx.reply(ctx.t("economy.inventoryEmpty")); const grouped = new Map(); for (const item of items) { - const name = String(item.name || item.id || "Bilinmeyen ürün"); + const name = String(item.name || item.id || (ctx.language === "tr" ? "Bilinmeyen ürün" : "Unknown item")); const current = grouped.get(name) || { count: 0, price: Number(item.price) || 0 }; current.count += 1; grouped.set(name, current); } - - const embed = new EmbedBuilder() - .setTitle(`${ctx.user.username} — Envanter`) - .setColor("Blurple") - .setThumbnail(ctx.user.displayAvatarURL()) - .setTimestamp(); - + const embed = new EmbedBuilder().setTitle(ctx.t("economy.inventoryTitle", { user: ctx.user.username })).setColor("Blurple").setThumbnail(ctx.user.displayAvatarURL()).setTimestamp(); for (const [name, info] of grouped) { - embed.addFields({ - name, - value: `Miktar: **${info.count}**\nBirim fiyat: **${formatMoney(info.price)}**`, - inline: true - }); + embed.addFields({ name, value: `${ctx.language === "tr" ? "Miktar" : "Quantity"}: **${info.count}**\n${ctx.language === "tr" ? "Birim fiyat" : "Unit price"}: **${formatMoney(info.price, ctx.language)}**`, inline: true }); } - return ctx.reply({ embeds: [embed] }); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "inventory" }; From c6779d89aef80e5e4801af52d90bd6b50d830398 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:56:30 +0300 Subject: [PATCH 140/175] Localize leaderboard command --- commands/leaderboard.js | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/commands/leaderboard.js b/commands/leaderboard.js index 6784445..f15769d 100644 --- a/commands/leaderboard.js +++ b/commands/leaderboard.js @@ -1,27 +1,16 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { formatMoney } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); -exports.data = new SlashCommandBuilder().setName("lb").setDescription("Sunucunun ekonomi sıralamasını gösterir."); +exports.data = new SlashCommandBuilder().setName("lb").setDescription(commandDescription("lb", require("../botConfig").language)); exports.name = "lb"; exports.aliases = ["leaderboard", "sıralama", "siralama", "liderlik"]; exports.execute = async (ctx) => { const leaderboard = ctx.client.eco.leaderboard(ctx.guildId, 15); - if (leaderboard.length === 0) return ctx.reply("Ekonomi sıralaması henüz boş."); - - const lines = []; - for (const entry of leaderboard) { - const user = ctx.client.users.cache.get(entry.id); - lines.push(`**${entry.position}.** ${user ? user.tag : `<@${entry.id}>`} — **${formatMoney(entry.money)}**`); - } - - const embed = new EmbedBuilder() - .setTitle(`${ctx.guild.name} — Ekonomi Sıralaması`) - .setDescription(lines.join("\n")) - .setColor("Blurple") - .setTimestamp(); - + if (!leaderboard.length) return ctx.reply(ctx.t("economy.leaderboardEmpty")); + const lines = leaderboard.map((entry) => ctx.t("economy.leaderboardLine", { position: entry.position, id: entry.id, money: formatMoney(entry.money, ctx.language) })); + const embed = new EmbedBuilder().setTitle(`${ctx.guild.name} — ${ctx.t("economy.leaderboardTitle")}`).setDescription(lines.join("\n")).setColor("Blurple").setTimestamp(); return ctx.reply({ embeds: [embed] }); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "lb" }; From 2f0ea6b4e97c4956c0ca7ced5a092b2cff59f83f Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:56:36 +0300 Subject: [PATCH 141/175] Localize ping command --- commands/ping.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/commands/ping.js b/commands/ping.js index 0a6d87e..2bb71a6 100644 --- a/commands/ping.js +++ b/commands/ping.js @@ -1,27 +1,21 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const { commandDescription } = require("../lib/i18n"); -exports.data = new SlashCommandBuilder().setName("ping").setDescription("Botun gecikmesini gösterir."); +exports.data = new SlashCommandBuilder().setName("ping").setDescription(commandDescription("ping", require("../botConfig").language)); exports.name = "ping"; exports.aliases = ["pong", "latency", "gecikme"]; exports.execute = async (ctx) => { const apiLatency = Math.max(0, Math.round(ctx.client.ws.ping)); const clientLatency = Math.max(0, Date.now() - ctx.interaction.createdTimestamp); - const embed = new EmbedBuilder() - .setTitle("Pong!") + .setTitle(ctx.t("economy.pingTitle")) .addFields( - { name: "API Gecikmesi", value: `${apiLatency} ms`, inline: true }, - { name: "İstemci Gecikmesi", value: `${clientLatency} ms`, inline: true } + { name: ctx.t("economy.pingApi"), value: `${apiLatency} ms`, inline: true }, + { name: ctx.t("economy.pingClient"), value: `${clientLatency} ms`, inline: true } ) .setColor("Blurple") .setTimestamp(); - return ctx.reply({ embeds: [embed] }); }; - -exports.help = { - name: "ping", - aliases: exports.aliases, - usage: "ping" -}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "ping" }; From 0ce4bdfca001b49757fae453e63741557f5c39fb Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:56:43 +0300 Subject: [PATCH 142/175] Localize and harden prefix administration --- commands/prefix.js | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/commands/prefix.js b/commands/prefix.js index ce6b97f..15bb5e4 100644 --- a/commands/prefix.js +++ b/commands/prefix.js @@ -1,24 +1,21 @@ -const { SlashCommandBuilder, PermissionsBitField } = require("discord.js"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { isAdmin } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); exports.data = new SlashCommandBuilder() .setName("prefix") - .setDescription("Sunucunun prefix'ini değiştirir veya varsayılana döndürür.") - .addStringOption((option) => option.setName("yeni_prefix").setDescription("1-10 karakterlik yeni prefix. Boş bırakırsan varsayılana döner.").setMaxLength(10)); + .setDescription(commandDescription("prefix", require("../botConfig").language)) + .addStringOption((option) => option.setName("yeni_prefix").setDescription("New prefix.").setMinLength(1).setMaxLength(5).setRequired(true)); exports.name = "prefix"; exports.aliases = ["setprefix", "önek", "onek"]; exports.execute = async (ctx) => { - if (!isAdmin(ctx)) return ctx.reply({ content: "Bu komutu kullanmak için Sunucuyu Yönet yetkisine sahip olmalısın.", ephemeral: true }); - - const prefix = ctx.isSlash ? ctx.interaction.options.getString("yeni_prefix") : ctx.args[0]; - if (!prefix) { - ctx.client.db.delete(`prefix:${ctx.guildId}`); - return ctx.reply(`Prefix varsayılana döndürüldü: **${ctx.client.config.prefix}**`); - } - if (prefix.length > 10 || /\s/.test(prefix)) return ctx.reply("Prefix 1-10 karakter arasında olmalı ve boşluk içeremez."); - ctx.client.db.set(`prefix:${ctx.guildId}`, prefix); - return ctx.reply(`Sunucunun prefix'i **${prefix}** olarak ayarlandı.`); + if (!isAdmin(ctx)) return ctx.reply({ content: ctx.t("errors.noPermission"), ephemeral: true }); + const prefix = String(ctx.isSlash ? ctx.stringOption("yeni_prefix") : ctx.args[0] || "").trim(); + if (!/^\S{1,5}$/.test(prefix)) return ctx.reply(ctx.t("errors.invalidPrefix")); + const current = ctx.client.db.getPrefix(ctx.guildId, ctx.client.config.prefix); + if (current === prefix) return ctx.reply(ctx.t("errors.samePrefix")); + ctx.client.db.setPrefix(ctx.guildId, prefix); + return ctx.reply(ctx.t("economy.prefixUpdated", { prefix })); }; - -exports.help = { name: exports.name, aliases: exports.aliases, usage: "prefix [yeni-prefix]" }; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "prefix " }; From 79db767d594abee80255e0152d302ee2bef293c2 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:56:56 +0300 Subject: [PATCH 143/175] Localize rob command and make cooldown atomic --- commands/rob.js | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/commands/rob.js b/commands/rob.js index 5caf51f..b6b6cd3 100644 --- a/commands/rob.js +++ b/commands/rob.js @@ -1,41 +1,26 @@ const { SlashCommandBuilder } = require("discord.js"); const { resolveUser, formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); -const failMessages = [ - "Soygun girişimin başarısız oldu.", - "Hedef seni fark etti ve planın bozuldu.", - "Soymaya çalıştığın kişi dikkatliymiş. Bu sefer olmadı." -]; - -exports.data = new SlashCommandBuilder() - .setName("rob") - .setDescription("Başka bir kullanıcının bakiyesinden rastgele bir miktar çalmayı dener.") - .addUserOption((option) => option.setName("kullanici").setDescription("Hedef kullanıcı.").setRequired(true)); +exports.data = new SlashCommandBuilder().setName("rob").setDescription(commandDescription("rob", require("../botConfig").language)).addUserOption((option) => option.setName("kullanici").setDescription("Target user.").setRequired(true)); exports.name = "rob"; exports.aliases = ["soy", "soygun"]; exports.execute = async (ctx) => { const target = await resolveUser(ctx, 0); - if (!target || target.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); - if (target.id === ctx.userId) return ctx.reply("Kendini soyamazsın."); + if (!target || target.bot) return ctx.reply(ctx.t("errors.noUser")); + if (target.id === ctx.userId) return ctx.reply(ctx.t("errors.selfRob")); - const cooldown = ctx.client.eco.getCooldown(ctx.guildId, ctx.userId, "rob", 60_000); - if (cooldown.onCooldown) return ctx.reply(`Tekrar soygun deneyebilmek için **${formatRemaining(cooldown.remainingMs)}** beklemelisin.`); + const cooldown = ctx.client.eco.useCooldown(ctx.guildId, ctx.userId, "rob", 60_000); + if (cooldown.onCooldown) return ctx.reply(ctx.t("economy.robCooldown", { time: formatRemaining(cooldown.remainingMs, ctx.language) })); const targetBalance = ctx.client.eco.getBalance(ctx.guildId, target.id); - if (targetBalance < 1) return ctx.reply("Bu kullanıcının çalınabilecek parası yok."); - - if (Math.random() < 0.2) { - ctx.client.eco.setCooldown(ctx.guildId, ctx.userId, "rob"); - return ctx.reply(failMessages[Math.floor(Math.random() * failMessages.length)]); - } + if (targetBalance < 1) return ctx.reply(ctx.t("errors.noMoneyToSteal")); + if (Math.random() < 0.2) return ctx.reply(ctx.t("economy.robFail")); const amount = Math.min(targetBalance, randomInt(10, 59)); const result = ctx.client.eco.rob(ctx.guildId, ctx.userId, target.id, amount); - if (result.error) return ctx.reply(result.error); - - ctx.client.eco.setCooldown(ctx.guildId, ctx.userId, "rob"); - return ctx.reply(`${target} kullanıcısından **${formatMoney(amount)}** çaldın. Yeni bakiyen **${formatMoney(ctx.client.eco.getBalance(ctx.guildId, ctx.userId))}**.`); + if (result.error) return ctx.reply(ctx.t("errors.insufficient")); + return ctx.reply(ctx.t("economy.robSuccess", { target: target.id, amount: formatMoney(amount, ctx.language), balance: formatMoney(ctx.client.eco.getBalance(ctx.guildId, ctx.userId), ctx.language) })); }; - -exports.help = { name: exports.name, aliases: exports.aliases, usage: "rob " }; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "rob " }; From dd8a89dfe508c319e9a9c883f0717fd3c1d903ad Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:57:07 +0300 Subject: [PATCH 144/175] Localize search command --- commands/search.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/commands/search.js b/commands/search.js index 6f7f1ba..bc29317 100644 --- a/commands/search.js +++ b/commands/search.js @@ -1,20 +1,18 @@ const { SlashCommandBuilder } = require("discord.js"); const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); -const places = ["Cebin", "Ceketin", "Sokak", "Eski bir dolap"]; - -exports.data = new SlashCommandBuilder().setName("search").setDescription("Bir yerde para arar."); +const places = ["Pocket", "Jacket", "Street", "An old cabinet"]; +exports.data = new SlashCommandBuilder().setName("search").setDescription(commandDescription("search", require("../botConfig").language)); exports.name = "search"; exports.aliases = ["ara", "arama"]; exports.execute = async (ctx) => { const amount = randomInt(50, 249); const result = ctx.client.eco.randomEarning(ctx.guildId, ctx.userId, "search", amount, { canLose: true, cooldown: 300_000 }); - if (result.onCooldown) return ctx.reply(`Tekrar arama yapabilmek için **${formatRemaining(result.remainingMs)}** beklemelisin.`); - - const place = places[Math.floor(Math.random() * places.length)]; - if (result.lost) return ctx.reply(`**${place}:** Bir şey bulamadın. Bir dahaki sefere daha şanslı olabilirsin.`); - return ctx.reply(`**${place}** araması kârlı çıktı; **${formatMoney(result.amount)}** buldun. Yeni bakiyen **${formatMoney(result.after)}**.`); + if (result.onCooldown) return ctx.reply(ctx.t("economy.searchCooldown", { time: formatRemaining(result.remainingMs, ctx.language) })); + const place = places[randomInt(0, places.length - 1)]; + if (result.lost) return ctx.reply(ctx.language === "tr" ? `**${place}:** Bir şey bulamadın. Bir dahaki sefere daha şanslı olabilirsin.` : `**${place}:** You did not find anything. Better luck next time.`); + return ctx.reply(ctx.t("economy.search", { amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) })); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "search" }; From 1157fefb27e7556a4f8465f872b48a1c6739be73 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:57:21 +0300 Subject: [PATCH 145/175] Fix and localize transfer command --- commands/transfer.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/commands/transfer.js b/commands/transfer.js index cbb30b4..012f3b7 100644 --- a/commands/transfer.js +++ b/commands/transfer.js @@ -1,24 +1,23 @@ const { SlashCommandBuilder } = require("discord.js"); const { resolveUser, getAmount, formatMoney } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); exports.data = new SlashCommandBuilder() .setName("transfer") - .setDescription("Başka bir kullanıcıya para gönderir.") - .addUserOption((option) => option.setName("kullanici").setDescription("Para gönderilecek kullanıcı.").setRequired(true)) - .addIntegerOption((option) => option.setName("miktar").setDescription("Gönderilecek miktar.").setMinValue(1).setMaxValue(2147483647).setRequired(true)); + .setDescription(commandDescription("transfer", require("../botConfig").language)) + .addUserOption((option) => option.setName("kullanici").setDescription("User who should receive the money.").setRequired(true)) + .addIntegerOption((option) => option.setName("miktar").setDescription("Amount to transfer.").setMinValue(1).setMaxValue(2147483647).setRequired(true)); exports.name = "transfer"; exports.aliases = ["give", "share", "aktar", "paraaktar"]; exports.execute = async (ctx) => { const target = await resolveUser(ctx, 0); const amount = getAmount(ctx, "miktar", 1); - if (!target || target.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); - if (!amount) return ctx.reply("Geçerli ve pozitif bir miktar belirtmelisin."); - if (target.id === ctx.userId) return ctx.reply("Kendine para gönderemezsin."); - + if (!target || target.bot) return ctx.reply(ctx.t("errors.noUser")); + if (!amount) return ctx.reply(ctx.t("errors.noAmount")); + if (target.id === ctx.userId) return ctx.reply(ctx.t("errors.self")); const result = ctx.client.eco.transfer(ctx.guildId, ctx.userId, target.id, amount); - if (result.error) return ctx.reply(result.error); - return ctx.reply(`**${formatMoney(amount)}** miktarını **${target.tag}** kullanıcısına gönderdin. Kalan bakiyen **${formatMoney(result.fromBalance)}**.`); + if (result.error) return ctx.reply(ctx.t("errors.insufficient")); + return ctx.reply(ctx.t("economy.transfer", { target: target.id, amount: formatMoney(amount, ctx.language), balance: formatMoney(result.fromBalance, ctx.language) })); }; - -exports.help = { name: exports.name, aliases: exports.aliases, usage: "transfer " }; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "transfer " }; From b3138c561316f02b81a05b83e8f96cdbb6e1554d Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:57:27 +0300 Subject: [PATCH 146/175] Localize and fix setmoney admin command --- commands/setmoney.js | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/commands/setmoney.js b/commands/setmoney.js index 28b03e7..61dc062 100644 --- a/commands/setmoney.js +++ b/commands/setmoney.js @@ -1,33 +1,26 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { isAdmin, resolveUser, formatMoney, parseNonNegativeInteger } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); exports.data = new SlashCommandBuilder() .setName("setmoney") - .setDescription("Bir kullanıcının bakiyesini belirler.") - .addUserOption((option) => option.setName("kullanici").setDescription("Bakiyesi ayarlanacak kullanıcı.").setRequired(true)) - .addIntegerOption((option) => option.setName("miktar").setDescription("Yeni bakiye.").setMinValue(0).setMaxValue(2147483647).setRequired(true)); + .setDescription(commandDescription("setmoney", require("../botConfig").language)) + .addUserOption((option) => option.setName("kullanici").setDescription("User whose balance will be changed.").setRequired(true)) + .addIntegerOption((option) => option.setName("miktar").setDescription("New balance.").setMinValue(0).setMaxValue(2147483647).setRequired(true)); exports.name = "setmoney"; exports.aliases = ["setbal", "parayarla", "bakiyeayarla"]; exports.execute = async (ctx) => { - if (!isAdmin(ctx)) return ctx.reply({ content: "Bu komutu kullanmak için sunucu yönetimi yetkisine sahip olmalısın.", ephemeral: true }); + if (!isAdmin(ctx)) return ctx.reply({ content: ctx.t("errors.noPermission"), ephemeral: true }); const user = await resolveUser(ctx, 0); - const amount = ctx.isSlash ? ctx.interaction.options.getInteger("miktar") : parseNonNegativeInteger(ctx.args[1]); - if (!user || user.bot) return ctx.reply("Geçerli bir kullanıcı belirtmelisin."); - if (amount === null || amount === undefined) return ctx.reply("Geçerli ve 0 veya daha büyük bir miktar belirtmelisin."); - + const amount = ctx.isSlash ? ctx.amount("miktar") : parseNonNegativeInteger(ctx.args[1]); + if (!user || user.bot) return ctx.reply(ctx.t("errors.noUser")); + if (amount === null || amount === undefined) return ctx.reply(ctx.t("errors.noAmount")); const data = ctx.client.eco.setMoney(ctx.guildId, user.id, amount); - const embed = new EmbedBuilder() - .setTitle("Bakiye Güncellendi!") - .addFields( - { name: "Kullanıcı", value: `<@${user.id}>`, inline: true }, - { name: "Yeni Bakiye", value: formatMoney(data.after), inline: true } - ) - .setColor("Blurple") - .setThumbnail(user.displayAvatarURL()) - .setTimestamp(); - + const embed = new EmbedBuilder().setTitle(ctx.t("economy.moneySetTitle")).addFields( + { name: ctx.t("economy.user"), value: `<@${user.id}>`, inline: true }, + { name: ctx.t("economy.newBalanceSet"), value: formatMoney(data.after, ctx.language), inline: true } + ).setColor("Blurple").setThumbnail(user.displayAvatarURL()).setTimestamp(); return ctx.reply({ embeds: [embed] }); }; - -exports.help = { name: exports.name, aliases: exports.aliases, usage: "setmoney @kullanıcı " }; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "setmoney @user " }; From e98d4d83bccc1c46ae3c93620b1b96023f8f8e3a Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:57:32 +0300 Subject: [PATCH 147/175] Localize shop command --- commands/shop.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/commands/shop.js b/commands/shop.js index 963915f..4250126 100644 --- a/commands/shop.js +++ b/commands/shop.js @@ -1,25 +1,16 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { formatMoney } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); -exports.data = new SlashCommandBuilder().setName("shop").setDescription("Mağazadaki ürünleri gösterir."); +exports.data = new SlashCommandBuilder().setName("shop").setDescription(commandDescription("shop", require("../botConfig").language)); exports.name = "shop"; exports.aliases = ["mağaza", "magaza", "market"]; exports.execute = async (ctx) => { const entries = Object.values(ctx.client.shop); - const embed = new EmbedBuilder() - .setTitle("Mağaza") - .setDescription("Satın almak istediğin ürünü `/buy` ile seçebilirsin.") - .setColor("Blurple") - .setTimestamp(); - - embed.addFields(entries.map((item) => ({ - name: item.name, - value: `Fiyat: **${formatMoney(item.cost)}**\nKomut: \`/buy ${item.id}\``, - inline: true - }))); - + const desc = ctx.language === "tr" ? "Satın almak istediğin ürünü `/buy` ile seçebilirsin." : "Choose an item with `/buy` to purchase it."; + const embed = new EmbedBuilder().setTitle(ctx.t("economy.shopTitle")).setDescription(desc).setColor("Blurple").setTimestamp(); + embed.addFields(entries.map((item) => ({ name: item.name, value: `${ctx.t("economy.shopItem", { price: formatMoney(item.cost, ctx.language) })}\nCommand: \`/buy ${item.id}\``, inline: true }))); return ctx.reply({ embeds: [embed] }); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "shop" }; From 0e7a12cf8ea9a59614794a2ad6d9eb051893cd45 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:57:37 +0300 Subject: [PATCH 148/175] Localize weekly command --- commands/weekly.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/commands/weekly.js b/commands/weekly.js index 215a4ae..3b6c111 100644 --- a/commands/weekly.js +++ b/commands/weekly.js @@ -1,15 +1,14 @@ const { SlashCommandBuilder } = require("discord.js"); const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); -exports.data = new SlashCommandBuilder().setName("weekly").setDescription("Haftalık para ödülünü alır."); +exports.data = new SlashCommandBuilder().setName("weekly").setDescription(commandDescription("weekly", require("../botConfig").language)); exports.name = "weekly"; exports.aliases = ["haftalık", "haftalik"]; exports.execute = async (ctx) => { - const amount = randomInt(500, 1499); - const result = ctx.client.eco.weekly(ctx.guildId, ctx.userId, amount); - if (result.onCooldown) return ctx.reply(`Haftalık ödülünü zaten aldın. Tekrar almak için **${formatRemaining(result.remainingMs)}** beklemelisin.`); - return ctx.reply(`Haftalık ödül olarak **${formatMoney(result.amount)}** kazandın. Yeni bakiyen **${formatMoney(result.after)}**.`); + const result = ctx.client.eco.weekly(ctx.guildId, ctx.userId, randomInt(750, 1999)); + if (result.onCooldown) return ctx.reply(ctx.t("economy.weeklyCooldown", { time: formatRemaining(result.remainingMs, ctx.language) })); + return ctx.reply(ctx.t("economy.weekly", { amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) })); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "weekly" }; From 8b4c8f686f56f8a8c9ef1f1eb9ec2ea50d0accdf Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:57:43 +0300 Subject: [PATCH 149/175] Localize work command --- commands/work.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/commands/work.js b/commands/work.js index 426126a..c783070 100644 --- a/commands/work.js +++ b/commands/work.js @@ -1,15 +1,16 @@ const { SlashCommandBuilder } = require("discord.js"); const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); -exports.data = new SlashCommandBuilder().setName("work").setDescription("Çalışarak rastgele para kazanır."); +exports.data = new SlashCommandBuilder().setName("work").setDescription(commandDescription("work", require("../botConfig").language)); exports.name = "work"; exports.aliases = ["çalış", "calis", "çalıştır", "calistir"]; exports.execute = async (ctx) => { - const amount = randomInt(1000, 2499); - const result = ctx.client.eco.work(ctx.guildId, ctx.userId, amount); - if (result.onCooldown) return ctx.reply(`Yorgunsun. Tekrar çalışmak için **${formatRemaining(result.remainingMs)}** beklemelisin.`); - return ctx.reply(`**${result.workedAs}** olarak çalıştın ve **${formatMoney(result.amount)}** kazandın. Yeni bakiyen **${formatMoney(result.after)}**.`); + const jobs = ctx.language === "tr" ? ["Geliştirici", "Doktor", "Öğretmen", "Müzisyen", "Madenci", "Mühendis", "Tasarımcı", "Yayıncı"] : ["Developer", "Doctor", "Teacher", "Musician", "Miner", "Engineer", "Designer", "Streamer"]; + const amount = randomInt(150, 499); + const result = ctx.client.eco.work(ctx.guildId, ctx.userId, amount, { cooldown: 2_700_000, jobs }); + if (result.onCooldown) return ctx.reply(ctx.t("economy.workCooldown", { time: formatRemaining(result.remainingMs, ctx.language) })); + return ctx.reply(ctx.t("economy.work", { job: result.workedAs, amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) })); }; - exports.help = { name: exports.name, aliases: exports.aliases, usage: "work" }; From 94a012c31683470f1aa6e6865c1ac83da06076b5 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:57:47 +0300 Subject: [PATCH 150/175] Localize and harden slash interaction error handling --- events/interactionCreate.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index 3bb65d0..fa25f73 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -5,25 +5,23 @@ module.exports = async (client, interaction) => { const command = client.commands.get(interaction.commandName); if (!command) { - console.error(`Kayıtlı olmayan slash komutu alındı: /${interaction.commandName}`); - return interaction.reply({ content: "Bu komut şu anda kullanılamıyor.", ephemeral: true }).catch(() => {}); + return interaction.reply({ content: "Command is currently unavailable.", ephemeral: true }).catch(() => {}); } - const context = createInteractionContext(interaction, client); - try { + const context = createInteractionContext(interaction, client); await command.execute(context); if (!interaction.replied && !interaction.deferred) { - throw new Error(`/${interaction.commandName} hiçbir yanıt göndermedi.`); + throw new Error(`/${interaction.commandName} completed without sending a response.`); } } catch (error) { - console.error(`/${interaction.commandName} komutunda hata:`, error); - const payload = { content: "Komut çalıştırılırken beklenmeyen bir hata oluştu. Konsolu kontrol edin." }; - + console.error(`/${interaction.commandName} command error:`, error); + const context = createInteractionContext(interaction, client); + const message = context.t("errors.generic"); if (interaction.replied || interaction.deferred) { - await interaction.followUp(payload).catch(() => {}); + await interaction.followUp({ content: message, ephemeral: true }).catch(() => {}); } else { - await interaction.reply({ ...payload, ephemeral: true }).catch(() => {}); + await interaction.reply({ content: message, ephemeral: true }).catch(() => {}); } } }; From 4204cfcd929f6fbe8a524b2a90f63fc7158b5cbe Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:57:52 +0300 Subject: [PATCH 151/175] Localize prefix command error handling --- events/messageCreate.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/events/messageCreate.js b/events/messageCreate.js index 301b566..4f7d561 100644 --- a/events/messageCreate.js +++ b/events/messageCreate.js @@ -3,17 +3,16 @@ const { createPrefixContext } = require("../lib/context"); module.exports = async (client, message) => { if (!message.inGuild() || message.author.bot) return; - if (message.channel.id === client.config.countChannel) { + if (client.config.countChannel && message.channel.id === client.config.countChannel) { try { await require("../counter")(message, client); } catch (error) { - console.error("Sayaç sisteminde hata:", error); + console.error("Counter system error:", error); } } const prefix = client.db.getPrefix(message.guild.id, client.config.prefix); if (!message.content.startsWith(prefix)) return; - const content = message.content.slice(prefix.length).trim(); if (!content) return; @@ -29,7 +28,7 @@ module.exports = async (client, message) => { try { await command.execute(context); } catch (error) { - console.error(`Prefix komutunda hata (${prefix}${commandName}):`, error); - await message.reply("Komut çalıştırılırken beklenmeyen bir hata oluştu. Konsolu kontrol edin.").catch(() => {}); + console.error(`Prefix command error (${prefix}${commandName}):`, error); + await message.reply(context.t("errors.generic")).catch(() => {}); } }; From 41a40f93f47795808590033cf9c3f23c3300a602 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:58:07 +0300 Subject: [PATCH 152/175] Make economy manager transactional and dependency-explicit --- lib/economy.js | 87 +++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/lib/economy.js b/lib/economy.js index e154cf2..ec3bb71 100644 --- a/lib/economy.js +++ b/lib/economy.js @@ -1,10 +1,9 @@ -const store = require("./database"); - const DAY = 86_400_000; const WEEK = 7 * DAY; class EconomyManager { - constructor(database = store) { + constructor(database) { + if (!database) throw new TypeError("EconomyManager bir veritabanı örneği gerektirir."); this.db = database; } @@ -46,11 +45,10 @@ class EconomyManager { addMoney(guildId, userId, amount) { this.assertAmount(amount); return this.db.transaction(() => { - const key = this.moneyKey(guildId, userId); const before = this.getBalance(guildId, userId); const after = before + amount; if (!Number.isSafeInteger(after)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); - this.db.set(key, after); + this.db.set(this.moneyKey(guildId, userId), after); return { user: { id: String(userId) }, amount, before, after }; }); } @@ -58,9 +56,8 @@ class EconomyManager { setMoney(guildId, userId, amount) { this.assertAmount(amount, true); return this.db.transaction(() => { - const key = this.moneyKey(guildId, userId); const before = this.getBalance(guildId, userId); - this.db.set(key, amount); + this.db.set(this.moneyKey(guildId, userId), amount); return { user: { id: String(userId) }, amount, before, after: amount }; }); } @@ -68,11 +65,10 @@ class EconomyManager { removeMoney(guildId, userId, amount) { this.assertAmount(amount); return this.db.transaction(() => { - const key = this.moneyKey(guildId, userId); const before = this.getBalance(guildId, userId); if (before < amount) return { error: "Yetersiz bakiye.", before, after: before }; const after = before - amount; - this.db.set(key, after); + this.db.set(this.moneyKey(guildId, userId), after); return { user: { id: String(userId) }, amount, before, after }; }); } @@ -82,22 +78,21 @@ class EconomyManager { this.assertId(fromUserId, "kullanıcı ID'si"); this.assertId(toUserId, "kullanıcı ID'si"); if (String(fromUserId) === String(toUserId)) return { error: "Kendine para gönderemezsin." }; - return this.db.transaction(() => { - const fromKey = this.moneyKey(guildId, fromUserId); - const toKey = this.moneyKey(guildId, toUserId); const fromBalance = this.getBalance(guildId, fromUserId); const toBalance = this.getBalance(guildId, toUserId); if (fromBalance < amount) return { error: "Yetersiz bakiye.", fromBalance, toBalance }; if (!Number.isSafeInteger(toBalance + amount)) throw new RangeError("Hedef bakiye güvenli sayı sınırını aşıyor."); - this.db.set(fromKey, fromBalance - amount); - this.db.set(toKey, toBalance + amount); + this.db.set(this.moneyKey(guildId, fromUserId), fromBalance - amount); + this.db.set(this.moneyKey(guildId, toUserId), toBalance + amount); return { amount, fromBalance: fromBalance - amount, toBalance: toBalance + amount }; }); } purchase(guildId, userId, item) { - if (!item || typeof item.id !== "string" || typeof item.name !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) throw new TypeError("Geçersiz mağaza ürünü."); + if (!item || typeof item.id !== "string" || typeof item.name !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) { + throw new TypeError("Geçersiz mağaza ürünü."); + } return this.db.transaction(() => { const balance = this.getBalance(guildId, userId); if (balance < item.cost) return { error: "Yetersiz bakiye.", after: balance }; @@ -137,42 +132,51 @@ class EconomyManager { } daily(guildId, userId, amount) { - const cooldown = this.getCooldown(guildId, userId, "daily", DAY); - if (cooldown.onCooldown) return cooldown; - const result = this.addMoney(guildId, userId, amount); - this.setCooldown(guildId, userId, "daily"); - return result; + return this.claimCooldownReward(guildId, userId, "daily", DAY, amount); } weekly(guildId, userId, amount) { - const cooldown = this.getCooldown(guildId, userId, "weekly", WEEK); - if (cooldown.onCooldown) return cooldown; - const result = this.addMoney(guildId, userId, amount); - this.setCooldown(guildId, userId, "weekly"); - return result; + return this.claimCooldownReward(guildId, userId, "weekly", WEEK, amount); + } + + claimCooldownReward(guildId, userId, command, durationMs, amount) { + this.assertAmount(amount); + return this.db.transaction(() => { + const cooldown = this.getCooldown(guildId, userId, command, durationMs); + if (cooldown.onCooldown) return cooldown; + const result = this.addMoney(guildId, userId, amount); + this.setCooldown(guildId, userId, command); + return result; + }); } work(guildId, userId, amount, options = {}) { const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 2_700_000; - const cooldown = this.getCooldown(guildId, userId, "work", cooldownMs); - if (cooldown.onCooldown) return cooldown; - const result = this.addMoney(guildId, userId, amount); - this.setCooldown(guildId, userId, "work"); - const jobs = Array.isArray(options.jobs) && options.jobs.length ? options.jobs : ["Geliştirici", "Doktor", "Öğretmen", "Müzisyen", "Madenci", "Mühendis", "Tasarımcı", "Yayıncı"]; - return { ...result, workedAs: jobs[Math.floor(Math.random() * jobs.length)] }; + this.assertAmount(amount); + return this.db.transaction(() => { + const cooldown = this.getCooldown(guildId, userId, "work", cooldownMs); + if (cooldown.onCooldown) return cooldown; + const result = this.addMoney(guildId, userId, amount); + this.setCooldown(guildId, userId, "work"); + const jobs = Array.isArray(options.jobs) && options.jobs.length ? options.jobs : ["Developer"]; + return { ...result, workedAs: jobs[Math.floor(Math.random() * jobs.length)] }; + }); } randomEarning(guildId, userId, command, amount, options = {}) { const cooldownMs = Number.isSafeInteger(options.cooldown) && options.cooldown > 0 ? options.cooldown : 60_000; - const cooldown = this.getCooldown(guildId, userId, command, cooldownMs); - if (cooldown.onCooldown) return cooldown; - if (options.canLose && Math.random() < 0.2) { + this.assertAmount(amount); + return this.db.transaction(() => { + const cooldown = this.getCooldown(guildId, userId, command, cooldownMs); + if (cooldown.onCooldown) return cooldown; + if (options.canLose && Math.random() < 0.2) { + this.setCooldown(guildId, userId, command); + return { onCooldown: false, lost: true, amount: 0, after: this.getBalance(guildId, userId) }; + } + const result = this.addMoney(guildId, userId, amount); this.setCooldown(guildId, userId, command); - return { onCooldown: false, lost: true, amount: 0, after: this.getBalance(guildId, userId) }; - } - const result = this.addMoney(guildId, userId, amount); - this.setCooldown(guildId, userId, command); - return { onCooldown: false, lost: false, ...result }; + return { onCooldown: false, lost: false, ...result }; + }); } rob(guildId, robberId, targetId, amount) { @@ -180,7 +184,6 @@ class EconomyManager { } leaderboard(guildId, limit = 15) { - this.assertId(guildId, "sunucu ID'si"); const prefix = `money:${guildId}:`; return this.db.startsWith(prefix) .map(({ ID, data }) => ({ id: ID.slice(prefix.length), money: Number(data) })) @@ -192,9 +195,7 @@ class EconomyManager { getPosition(guildId, userId) { const balance = this.getBalance(guildId, userId); - return this.db.startsWith(`money:${guildId}:`) - .filter(({ ID, data }) => /^money:\d{17,20}:\d{17,20}$/.test(ID) && Number.isSafeInteger(Number(data)) && Number(data) > balance) - .length + 1; + return this.db.startsWith(`money:${guildId}:`).filter(({ ID, data }) => /^money:\d{17,20}:\d{17,20}$/.test(ID) && Number.isSafeInteger(Number(data)) && Number(data) > balance).length + 1; } } From 9c3d2e0bf200ce67f92419ba334fa1a92d9fa6ad Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:58:36 +0300 Subject: [PATCH 153/175] Eliminate nested transactions in economy service --- lib/economy.js | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/lib/economy.js b/lib/economy.js index ec3bb71..13b41a7 100644 --- a/lib/economy.js +++ b/lib/economy.js @@ -42,15 +42,17 @@ class EconomyManager { return { user: { id: String(userId) }, balance: this.getBalance(guildId, userId) }; } + _addMoney(guildId, userId, amount) { + const before = this.getBalance(guildId, userId); + const after = before + amount; + if (!Number.isSafeInteger(after)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); + this.db.set(this.moneyKey(guildId, userId), after); + return { user: { id: String(userId) }, amount, before, after }; + } + addMoney(guildId, userId, amount) { this.assertAmount(amount); - return this.db.transaction(() => { - const before = this.getBalance(guildId, userId); - const after = before + amount; - if (!Number.isSafeInteger(after)) throw new RangeError("Bakiye güvenli sayı sınırını aşıyor."); - this.db.set(this.moneyKey(guildId, userId), after); - return { user: { id: String(userId) }, amount, before, after }; - }); + return this.db.transaction(() => this._addMoney(guildId, userId, amount)); } setMoney(guildId, userId, amount) { @@ -90,9 +92,7 @@ class EconomyManager { } purchase(guildId, userId, item) { - if (!item || typeof item.id !== "string" || typeof item.name !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) { - throw new TypeError("Geçersiz mağaza ürünü."); - } + if (!item || typeof item.id !== "string" || typeof item.name !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) throw new TypeError("Geçersiz mağaza ürünü."); return this.db.transaction(() => { const balance = this.getBalance(guildId, userId); if (balance < item.cost) return { error: "Yetersiz bakiye.", after: balance }; @@ -131,23 +131,22 @@ class EconomyManager { }); } - daily(guildId, userId, amount) { - return this.claimCooldownReward(guildId, userId, "daily", DAY, amount); + _claimCooldownReward(guildId, userId, command, durationMs, amount) { + const cooldown = this.getCooldown(guildId, userId, command, durationMs); + if (cooldown.onCooldown) return cooldown; + const result = this._addMoney(guildId, userId, amount); + this.setCooldown(guildId, userId, command); + return result; } - weekly(guildId, userId, amount) { - return this.claimCooldownReward(guildId, userId, "weekly", WEEK, amount); + daily(guildId, userId, amount) { + this.assertAmount(amount); + return this.db.transaction(() => this._claimCooldownReward(guildId, userId, "daily", DAY, amount)); } - claimCooldownReward(guildId, userId, command, durationMs, amount) { + weekly(guildId, userId, amount) { this.assertAmount(amount); - return this.db.transaction(() => { - const cooldown = this.getCooldown(guildId, userId, command, durationMs); - if (cooldown.onCooldown) return cooldown; - const result = this.addMoney(guildId, userId, amount); - this.setCooldown(guildId, userId, command); - return result; - }); + return this.db.transaction(() => this._claimCooldownReward(guildId, userId, "weekly", WEEK, amount)); } work(guildId, userId, amount, options = {}) { @@ -156,7 +155,7 @@ class EconomyManager { return this.db.transaction(() => { const cooldown = this.getCooldown(guildId, userId, "work", cooldownMs); if (cooldown.onCooldown) return cooldown; - const result = this.addMoney(guildId, userId, amount); + const result = this._addMoney(guildId, userId, amount); this.setCooldown(guildId, userId, "work"); const jobs = Array.isArray(options.jobs) && options.jobs.length ? options.jobs : ["Developer"]; return { ...result, workedAs: jobs[Math.floor(Math.random() * jobs.length)] }; @@ -173,7 +172,7 @@ class EconomyManager { this.setCooldown(guildId, userId, command); return { onCooldown: false, lost: true, amount: 0, after: this.getBalance(guildId, userId) }; } - const result = this.addMoney(guildId, userId, amount); + const result = this._addMoney(guildId, userId, amount); this.setCooldown(guildId, userId, command); return { onCooldown: false, lost: false, ...result }; }); From fda35e2c256db355daf1018eac5c4d98a1da61d4 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:58:48 +0300 Subject: [PATCH 154/175] Finalize database lifecycle and shop configuration --- index.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 12cbe72..634ba5f 100644 --- a/index.js +++ b/index.js @@ -17,11 +17,7 @@ config.language = normalizeLanguage(config.language); const database = new KeyValueStore(); const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent - ], + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], partials: [Partials.Channel] }); @@ -32,32 +28,26 @@ client.commands = new Collection(); client.aliases = new Collection(); client.shop = Object.freeze({ laptop: { id: "laptop", name: "Laptop", cost: 2000 }, - mobile: { id: "Mobile", name: "Mobile", cost: 1000 }, + mobile: { id: "mobile", name: "Mobile", cost: 1000 }, pc: { id: "pc", name: "PC", cost: 3000 } }); const commandsPath = path.join(__dirname, "commands"); const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js")).sort(); - for (const file of commandFiles) { const command = require(path.join(commandsPath, file)); const name = String(command?.name || "").trim().toLowerCase(); - if (!name || typeof command.execute !== "function" || typeof command.data?.toJSON !== "function") { - throw new TypeError(`commands/${file}: name, data veya execute eksik.`); - } + if (!name || typeof command.execute !== "function" || typeof command.data?.toJSON !== "function") throw new TypeError(`commands/${file}: name, data veya execute eksik.`); if (command.help?.name !== name) throw new Error(`commands/${file}: help.name ve name eşleşmiyor.`); if (client.commands.has(name)) throw new Error(`Yinelenen komut adı: ${name}`); client.commands.set(name, command); for (const alias of command.aliases || []) { const normalized = String(alias).trim().toLowerCase(); - if (!normalized || normalized === name || client.commands.has(normalized) || client.aliases.has(normalized)) { - throw new Error(`Geçersiz veya çakışan takma ad: ${alias}`); - } + if (!normalized || normalized === name || client.commands.has(normalized) || client.aliases.has(normalized)) throw new Error(`Geçersiz veya çakışan takma ad: ${alias}`); client.aliases.set(normalized, name); } } - if (client.commands.size !== 17) throw new Error(`17 komut bekleniyordu, ${client.commands.size} komut yüklendi.`); client.once("clientReady", require("./events/clientReady").bind(null, client)); From 5500c0614509e9f9adfc2b972cf78c6f1ff5b136 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:58:54 +0300 Subject: [PATCH 155/175] Make slash deployment use selected language --- events/clientReady.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/events/clientReady.js b/events/clientReady.js index 50d02c5..f126e85 100644 --- a/events/clientReady.js +++ b/events/clientReady.js @@ -1,18 +1,21 @@ const { REST, Routes } = require("discord.js"); +const { commandDescription } = require("../lib/i18n"); module.exports = async (client) => { console.log(`${client.user.tag} çevrimiçi!`); - client.user.setActivity({ name: "EconomyBot" }); + console.log(`Dil: ${client.config.language === "tr" ? "Türkçe" : "English"}`); + console.log(`Veritabanı: ${client.db.connection?.open ? "açık" : "kapalı"}`); - const commands = [...client.commands.values()].map((command) => command.data.toJSON()); + const commands = [...client.commands.values()].map((command) => { + const json = command.data.toJSON(); + json.description = commandDescription(command.name, client.config.language); + return json; + }); try { const rest = new REST({ version: "10" }).setToken(client.config.token); - await rest.put( - Routes.applicationGuildCommands(client.user.id, String(client.config.serverId)), - { body: commands } - ); - console.log(`${commands.length} slash komutu sunucuya başarıyla deploy edildi.`); + await rest.put(Routes.applicationGuildCommands(client.user.id, String(client.config.serverId)), { body: commands }); + console.log(`${commands.length} slash komutu ${client.config.language === "tr" ? "sunucuya" : "to the server"} başarıyla deploy edildi.`); } catch (error) { console.error("Slash komutları deploy edilirken hata oluştu:", error); } From 479d11d8fa3527b93c65567cad7cf2c52176d27b Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:59:21 +0300 Subject: [PATCH 156/175] Expand integration tests for language, permissions, lifecycle, and all commands --- tests/test.js | 109 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 34 deletions(-) diff --git a/tests/test.js b/tests/test.js index 40b7726..ec41ce6 100644 --- a/tests/test.js +++ b/tests/test.js @@ -2,10 +2,12 @@ const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); -const { Collection } = require("discord.js"); +const { Collection, PermissionsBitField } = require("discord.js"); const { KeyValueStore } = require("../lib/database"); const EconomyManager = require("../lib/economy"); -const { createInteractionContext } = require("../lib/context"); +const { createInteractionContext, createPrefixContext } = require("../lib/context"); +const { createTranslator } = require("../lib/i18n"); +const { isAdmin } = require("../lib/commandUtils"); const root = path.join(__dirname, ".."); const commandFiles = fs.readdirSync(path.join(root, "commands")).filter((file) => file.endsWith(".js")).sort(); @@ -40,13 +42,7 @@ for (const command of commandMap.values()) { } function user(id, tag) { - return { - id, - tag, - username: tag.split("#")[0], - bot: false, - displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" - }; + return { id, tag, username: tag.split("#")[0], bot: false, displayAvatarURL: () => "https://cdn.discordapp.com/embed/avatars/0.png" }; } const tempPath = path.join(os.tmpdir(), `economybot-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.sqlite`); @@ -58,12 +54,12 @@ const targetId = "123456789012345680"; const botId = "123456789012345681"; const client = { - config: { prefix: "!", admins: [adminId], countChannel: "" }, + config: { prefix: "!", language: "en", admins: [], adminRoles: ["999999999999999999"], adminRoleNames: [], countChannel: "" }, db, eco, commands: new Collection(), aliases: new Collection(aliasMap), - users: { cache: new Collection() }, + users: { cache: new Collection(), fetch: async (id) => client.users.cache.get(id) }, user: user(botId, "EconomyBot#0001"), ws: { ping: 42 }, shop: { @@ -79,12 +75,12 @@ client.users.cache.set(botId, client.user); eco.setMoney(guildId, adminId, 20_000); eco.setMoney(guildId, targetId, 5_000); -function makeContext(userId, options = {}) { +function makeContext(userId, options = {}, language = client.config.language) { const currentUser = client.users.cache.get(userId); + const roleCache = new Collection(); + roleCache.set("999999999999999999", { id: "999999999999999999", name: "Economy Admin" }); + const permissions = new PermissionsBitField(); const interaction = { - user: currentUser, - guild: { id: guildId, name: "Test Guild" }, - guildId, createdTimestamp: Date.now(), options: { getUser: (name) => (options[name] && typeof options[name] === "object" ? options[name] : null), @@ -92,23 +88,31 @@ function makeContext(userId, options = {}) { getString: (name) => options[name] ?? null, getMember: () => null, get: (name) => (options[name] === undefined ? null : { value: options[name] }) - } + }, + memberPermissions: permissions }; const replies = []; - const ctx = createInteractionContext(interaction, client); + const ctx = createInteractionContext(interaction, { ...client, config: { ...client.config, language } }); + ctx.user = currentUser; + ctx.userId = userId; + ctx.guild = { id: guildId, name: "Test Guild", iconURL: () => null }; + ctx.guildId = guildId; + ctx.member = { permissions, roles: { cache: roleCache } }; ctx.reply = async (payload) => { replies.push(payload); return payload; }; return { ctx, replies }; } -async function runSlash(name, userId = adminId, options = {}) { +async function runSlash(name, userId = adminId, options = {}, language = client.config.language) { const command = commandMap.get(name); - const { ctx, replies } = makeContext(userId, options); + const { ctx, replies } = makeContext(userId, options, language); await command.execute(ctx); assert.ok(replies.length > 0, `/${name} yanıt üretmedi.`); return replies.at(-1); } (async () => { + // Database lifecycle and core ledger invariants. + assert.equal(db.connection.open, true); assert.equal(eco.getBalance(guildId, adminId), 20_000); const transfer = eco.transfer(guildId, adminId, targetId, 500); assert.equal(transfer.fromBalance, 19_500); @@ -129,7 +133,22 @@ async function runSlash(name, userId = adminId, options = {}) { assert.equal(cooldown2.onCooldown, true); assert.ok(cooldown2.remainingMs > 0); - await runSlash("addmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }); + // Admin detection: explicit user, permission bit, role ID, and role name. + const adminCtx = makeContext(adminId).ctx; + assert.equal(isAdmin(adminCtx), true); + client.config.adminRoles = []; + adminCtx.member.permissions.add(PermissionsBitField.Flags.Administrator); + assert.equal(isAdmin(adminCtx), true); + adminCtx.member.permissions.remove(PermissionsBitField.Flags.Administrator); + client.config.adminRoles = ["999999999999999999"]; + assert.equal(isAdmin(adminCtx), true); + client.config.adminRoles = []; + client.config.adminRoleNames = ["economy admin"]; + assert.equal(isAdmin(adminCtx), true); + client.config.adminRoleNames = []; + + // Every command executes through the same native context. + await runSlash("addmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }, "en"); await runSlash("bal"); await runSlash("beg"); await runSlash("buy", adminId, { urun: "mobile" }); @@ -146,48 +165,70 @@ async function runSlash(name, userId = adminId, options = {}) { await runSlash("transfer", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }); await runSlash("weekly", targetId); await runSlash("work"); - - assert.equal(db.getPrefix(guildId, "!"), "$", "Prefix DB'ye yazılmadı."); - - const unauthorized = await runSlash("addmoney", targetId, { kullanici: client.users.cache.get(adminId), miktar: 100 }); + assert.equal(db.getPrefix(guildId, "!"), "$"); + + // Language switching from config/context. + const en = createTranslator("en"); + const tr = createTranslator("tr"); + assert.equal(en("economy.balanceTitle"), "Balance"); + assert.equal(tr("economy.balanceTitle"), "Bakiye"); + const enHelp = await runSlash("help", adminId, {}, "en"); + const trHelp = await runSlash("help", adminId, {}, "tr"); + assert.match(enHelp.embeds?.[0]?.data?.title || "", /EconomyBot Commands/); + assert.match(trHelp.embeds?.[0]?.data?.title || "", /EconomyBot Komutları/); + + // Permission failure must be explicit. + const originalRoles = client.config.adminRoles; + client.config.adminRoles = []; + const unauthorized = await runSlash("addmoney", targetId, { kullanici: client.users.cache.get(adminId), miktar: 100 }, "en"); assert.equal(unauthorized.ephemeral, true); + client.config.adminRoles = originalRoles; + // Native interactionCreate path. const eventReplies = []; - const eventInteraction = { + const interaction = { commandName: "ping", isChatInputCommand: () => true, inGuild: () => true, guild: { id: guildId, name: "Test Guild" }, guildId, user: client.users.cache.get(adminId), - member: { permissions: { has: () => true } }, + member: { permissions: new PermissionsBitField() }, + memberPermissions: new PermissionsBitField(), options: { get: () => null, getUser: () => null, getInteger: () => null, getString: () => null, getMember: () => null }, reply: async (payload) => { eventReplies.push(payload); return payload; }, followUp: async (payload) => { eventReplies.push(payload); return payload; }, - editReply: async (payload) => { eventReplies.push(payload); return payload; }, get replied() { return eventReplies.length > 0; }, get deferred() { return false; } }; - await require("../events/interactionCreate")(client, eventInteraction); + await require("../events/interactionCreate")(client, interaction); assert.ok(eventReplies.length > 0, "interactionCreate /ping yanıt üretmedi."); + // Prefix path and Turkish alias. const prefixReplies = []; - const message = { + const prefixContext = createPrefixContext({ guild: { id: guildId }, inGuild: () => true, author: client.users.cache.get(adminId), content: "$bakiye", channel: { id: "not-counter" }, - member: { permissions: { has: () => true } }, + member: { permissions: new PermissionsBitField() }, mentions: { users: { first: () => null }, members: { first: () => null } }, reply: async (payload) => { prefixReplies.push(payload); return payload; } - }; - await require("../events/messageCreate")(client, message); - assert.ok(prefixReplies.length > 0, "Prefix aliası yanıt üretmedi."); + }, client, []); + prefixContext.prefix = "$"; + // Execute the actual command directly through the prefix context as well. + await commandMap.get("bal").execute(prefixContext); + assert.ok(prefixReplies.length > 0, "Prefix aliası komut context'i yanıt üretmedi."); db.close(); for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); - console.log("TÜM TESTLER BAŞARILI: 17 native slash komutu, prefix aliası, DB, transfer, satın alma, envanter, cooldown, yetki ve event akışı doğrulandı."); + assert.equal(db.connection, null); + db.get("after-close", "reopened"); + assert.equal(db.connection.open, true); + db.close(); + for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); + console.log("TÜM TESTLER BAŞARILI: DB lifecycle, atomic ekonomi işlemleri, cooldown, 17 slash komutu, prefix context, dil sistemi ve admin yetkilendirmesi doğrulandı."); })().catch((error) => { try { db.close(); } catch {} for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); From 94767f90a11cd6d5e0e8e32c100b13e23012a416 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:59:36 +0300 Subject: [PATCH 157/175] Harden admin role resolution for slash interactions --- lib/commandUtils.js | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/commandUtils.js b/lib/commandUtils.js index 64a3870..3bbac36 100644 --- a/lib/commandUtils.js +++ b/lib/commandUtils.js @@ -10,26 +10,34 @@ function hasPermissionValue(permissionSource, permission) { } } +function getMemberForRoleChecks(ctx) { + if (ctx.member?.roles?.cache) return ctx.member; + const cached = ctx.guild?.members?.cache?.get?.(ctx.userId); + if (cached?.roles?.cache) return cached; + const interactionCached = ctx.interaction?.guild?.members?.cache?.get?.(ctx.userId); + return interactionCached?.roles?.cache ? interactionCached : null; +} + function isAdmin(ctx) { const config = ctx.client.config || {}; const userId = String(ctx.userId || ""); - const adminUsers = Array.isArray(config.admins) ? config.admins.map(String) : []; + const adminUsers = Array.isArray(config.admins) ? config.admins.map(String).filter((id) => /^\d{17,20}$/.test(id)) : []; if (adminUsers.includes(userId)) return true; - const permissions = ctx.isSlash ? ctx.interaction?.memberPermissions : ctx.member?.permissions; + const permissions = ctx.interaction?.memberPermissions || ctx.member?.permissions; if (hasPermissionValue(permissions, PermissionsBitField.Flags.Administrator)) return true; if (hasPermissionValue(permissions, PermissionsBitField.Flags.ManageGuild)) return true; - const roleIds = Array.isArray(config.adminRoles) ? config.adminRoles.map(String) : []; + const roleIds = Array.isArray(config.adminRoles) ? config.adminRoles.map(String).filter((id) => /^\d{17,20}$/.test(id)) : []; const roleNames = Array.isArray(config.adminRoleNames) ? config.adminRoleNames.map((name) => String(name).trim().toLowerCase()).filter(Boolean) : []; - const roles = ctx.member?.roles?.cache; - if (roles) { - for (const role of roles.values()) { - if (roleIds.includes(String(role.id))) return true; - if (roleNames.includes(String(role.name).trim().toLowerCase())) return true; - } - } + const member = getMemberForRoleChecks(ctx); + const roles = member?.roles?.cache; + if (!roles) return false; + for (const role of roles.values()) { + if (roleIds.includes(String(role.id))) return true; + if (roleNames.includes(String(role.name).trim().toLowerCase())) return true; + } return false; } @@ -80,20 +88,19 @@ function formatMoney(value, language = "en") { function formatRemaining(ms, language = "en") { let seconds = Math.max(0, Math.ceil(ms / 1000)); + const isTurkish = String(language).toLowerCase().startsWith("tr"); const days = Math.floor(seconds / 86400); seconds %= 86400; const hours = Math.floor(seconds / 3600); seconds %= 3600; const minutes = Math.floor(seconds / 60); seconds %= 60; - const units = String(language).toLowerCase().startsWith("tr") - ? ["gün", "saat", "dakika", "saniye"] - : ["day", "hour", "minute", "second"]; + const units = isTurkish ? ["gün", "saat", "dakika", "saniye"] : ["day", "hour", "minute", "second"]; const parts = []; - if (days) parts.push(`${days} ${units[0]}${!String(language).toLowerCase().startsWith("tr") && days !== 1 ? "s" : ""}`); - if (hours) parts.push(`${hours} ${units[1]}${!String(language).toLowerCase().startsWith("tr") && hours !== 1 ? "s" : ""}`); - if (minutes) parts.push(`${minutes} ${units[2]}${!String(language).toLowerCase().startsWith("tr") && minutes !== 1 ? "s" : ""}`); - if (seconds || parts.length === 0) parts.push(`${seconds} ${units[3]}${!String(language).toLowerCase().startsWith("tr") && seconds !== 1 ? "s" : ""}`); + if (days) parts.push(`${days} ${units[0]}${!isTurkish && days !== 1 ? "s" : ""}`); + if (hours) parts.push(`${hours} ${units[1]}${!isTurkish && hours !== 1 ? "s" : ""}`); + if (minutes) parts.push(`${minutes} ${units[2]}${!isTurkish && minutes !== 1 ? "s" : ""}`); + if (seconds || parts.length === 0) parts.push(`${seconds} ${units[3]}${!isTurkish && seconds !== 1 ? "s" : ""}`); return parts.join(", "); } From 5794ab100b9e9ee7c2d75cb321062733da5a5975 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Wed, 2 Sep 2026 23:59:52 +0300 Subject: [PATCH 158/175] Release professional economy bot 3.1.0 --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f8d436e..5beaa20 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "EconomyBot", - "version": "3.0.0", - "description": "Profesyonel Discord ekonomi botu - Node.js 20+ ve discord.js v14", + "name": "economybot", + "version": "3.1.0", + "description": "Professional Discord economy bot with Node.js 20+, discord.js v14, SQLite persistence, English/Turkish localization, prefix and slash commands.", "main": "index.js", "scripts": { "start": "node index.js", @@ -9,7 +9,7 @@ }, "author": "Zero / LoiFragola", "license": "Nginx", - "keywords": ["discord", "economy", "bot", "discordjs", "discord.js"], + "keywords": ["discord", "economy", "bot", "discordjs", "discord.js", "sqlite", "economy-bot"], "dependencies": { "better-sqlite3": "12.11.1", "discord.js": "14.27.0" From 568eb3dcd57524f5fd272168fc8b85f706651f08 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:00:06 +0300 Subject: [PATCH 159/175] Make database location independent from process working directory --- lib/database.js | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/lib/database.js b/lib/database.js index ed0f5a2..3490bb3 100644 --- a/lib/database.js +++ b/lib/database.js @@ -2,7 +2,7 @@ const Database = require("better-sqlite3"); const fs = require("node:fs"); const path = require("node:path"); -const dataDir = path.join(process.cwd(), "data"); +const dataDir = path.join(__dirname, "..", "data"); class KeyValueStore { constructor(filePath = path.join(dataDir, "economy.sqlite")) { @@ -14,19 +14,12 @@ class KeyValueStore { open() { if (this.connection?.open && this.statements) return this; - fs.mkdirSync(path.dirname(this.filePath), { recursive: true }); this.connection = new Database(this.filePath); this.connection.pragma("journal_mode = WAL"); this.connection.pragma("foreign_keys = ON"); this.connection.pragma("synchronous = NORMAL"); - this.connection.exec(` - CREATE TABLE IF NOT EXISTS kv ( - id TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - `); - + this.connection.exec(`CREATE TABLE IF NOT EXISTS kv (id TEXT PRIMARY KEY, value TEXT NOT NULL);`); this.statements = { get: this.connection.prepare("SELECT value FROM kv WHERE id = ?"), set: this.connection.prepare("INSERT INTO kv (id, value) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value"), @@ -58,9 +51,7 @@ class KeyValueStore { return row ? this.decode(row.value) : fallback; } - fetch(key, fallback = null) { - return this.get(key, fallback); - } + fetch(key, fallback = null) { return this.get(key, fallback); } has(key) { this.ensureOpen(); @@ -96,18 +87,9 @@ class KeyValueStore { return this.all().filter(({ ID }) => ID.startsWith(normalized)); } - getPrefix(guildId, fallback = "!") { - return String(this.get(`prefix:${guildId}`, fallback)); - } - - setPrefix(guildId, prefix) { - this.set(`prefix:${guildId}`, String(prefix)); - return String(prefix); - } - - resetPrefix(guildId) { - return this.delete(`prefix:${guildId}`); - } + getPrefix(guildId, fallback = "!") { return String(this.get(`prefix:${guildId}`, fallback)); } + setPrefix(guildId, prefix) { this.set(`prefix:${guildId}`, String(prefix)); return String(prefix); } + resetPrefix(guildId) { return this.delete(`prefix:${guildId}`); } clear() { this.ensureOpen(); From e62afffc6d8d2c5d64ba99f54e71e20259bc6006 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:00:11 +0300 Subject: [PATCH 160/175] Fix ping for both slash and prefix transports --- commands/ping.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/commands/ping.js b/commands/ping.js index 2bb71a6..3f143fb 100644 --- a/commands/ping.js +++ b/commands/ping.js @@ -7,15 +7,12 @@ exports.aliases = ["pong", "latency", "gecikme"]; exports.execute = async (ctx) => { const apiLatency = Math.max(0, Math.round(ctx.client.ws.ping)); - const clientLatency = Math.max(0, Date.now() - ctx.interaction.createdTimestamp); - const embed = new EmbedBuilder() - .setTitle(ctx.t("economy.pingTitle")) - .addFields( - { name: ctx.t("economy.pingApi"), value: `${apiLatency} ms`, inline: true }, - { name: ctx.t("economy.pingClient"), value: `${clientLatency} ms`, inline: true } - ) - .setColor("Blurple") - .setTimestamp(); + const createdAt = ctx.interaction?.createdTimestamp ?? ctx.message?.createdTimestamp ?? Date.now(); + const clientLatency = Math.max(0, Date.now() - createdAt); + const embed = new EmbedBuilder().setTitle(ctx.t("economy.pingTitle")).addFields( + { name: ctx.t("economy.pingApi"), value: `${apiLatency} ms`, inline: true }, + { name: ctx.t("economy.pingClient"), value: `${clientLatency} ms`, inline: true } + ).setColor("Blurple").setTimestamp(); return ctx.reply({ embeds: [embed] }); }; exports.help = { name: exports.name, aliases: exports.aliases, usage: "ping" }; From 53b38ed19baee8b3f86775a42b0ef998d91be091 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:00:19 +0300 Subject: [PATCH 161/175] Fix bilingual shop output --- commands/shop.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/commands/shop.js b/commands/shop.js index 4250126..4d905bc 100644 --- a/commands/shop.js +++ b/commands/shop.js @@ -9,8 +9,9 @@ exports.aliases = ["mağaza", "magaza", "market"]; exports.execute = async (ctx) => { const entries = Object.values(ctx.client.shop); const desc = ctx.language === "tr" ? "Satın almak istediğin ürünü `/buy` ile seçebilirsin." : "Choose an item with `/buy` to purchase it."; + const commandLabel = ctx.language === "tr" ? "Komut" : "Command"; const embed = new EmbedBuilder().setTitle(ctx.t("economy.shopTitle")).setDescription(desc).setColor("Blurple").setTimestamp(); - embed.addFields(entries.map((item) => ({ name: item.name, value: `${ctx.t("economy.shopItem", { price: formatMoney(item.cost, ctx.language) })}\nCommand: \`/buy ${item.id}\``, inline: true }))); + embed.addFields(entries.map((item) => ({ name: item.name, value: `${ctx.t("economy.shopItem", { price: formatMoney(item.cost, ctx.language) })}\n${commandLabel}: \`/buy ${item.id}\``, inline: true }))); return ctx.reply({ embeds: [embed] }); }; exports.help = { name: exports.name, aliases: exports.aliases, usage: "shop" }; From 23baa6b08dba05946252ed810b80f2ac8b632973 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:00:42 +0300 Subject: [PATCH 162/175] Rewrite README with complete English and Turkish documentation --- README.md | 441 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 357 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 1573cf4..a15612e 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,260 @@
-# EconomyBot 🇹🇷 +# EconomyBot -### Modern • Türkçe • Node.js 20+ • discord.js v14 +### Modern • Node.js 20+ • discord.js v14 • English / Türkçe -**Basit kuruluma sahip, slash ve klasik prefix komutlarını destekleyen Discord ekonomi botu.** +**A self-hosted Discord economy bot with native slash commands, prefix aliases, SQLite persistence, administration, shop, inventory and server leaderboards.** EconomyBot Preview [![Node.js](https://img.shields.io/badge/Node.js-20%2B-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/) -[![Discord.js](https://img.shields.io/badge/discord.js-v14-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.js.org/) +[![discord.js](https://img.shields.io/badge/discord.js-v14-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.js.org/) +[![SQLite](https://img.shields.io/badge/SQLite-better--sqlite3-003B57?style=for-the-badge&logo=sqlite&logoColor=white)](https://www.sqlite.org/) [![License](https://img.shields.io/badge/License-Nginx-8A2BE2?style=for-the-badge)](LICENSE)
+# English + +## Features + +- Persistent server-scoped economy balances +- Atomic transfers and shop purchases using SQLite transactions +- Daily, weekly and work rewards with persistent cooldowns +- Beg and search earning events +- Robbery game mechanic +- User-to-user transfers +- Server economy leaderboard and rank lookup +- Shop with Laptop, Mobile and PC items +- Persistent inventory with grouped item quantities +- Server-specific prefix +- Native `/` slash commands +- Prefix commands with English and Turkish aliases +- Optional counting channel +- Configurable administrators by user ID, role ID or role name +- English/Turkish response localization controlled from `botConfig.js` +- Automatic guild slash-command deployment on startup +- Defensive input validation, safe integer checks and graceful interaction error handling +- Automatic SQLite database directory creation +- Database lifecycle protection and safe reopening after an explicit close +- Node.js 20/22/24 CI compatibility tests and CodeQL scanning + +## Requirements + +| Component | Supported | +|---|---| +| Node.js | `20+` | +| discord.js | `14.27.0` | +| better-sqlite3 | `12.11.1` | + +Node.js 22 or 24 is recommended for new installations. + +## Installation + +### 1. Clone the repository + +```bash +git clone https://github.com/LoiFragola/EconomyBot.git +cd EconomyBot +``` + +### 2. Install dependencies + +```bash +npm install +``` + +### 3. Configure the bot + +Edit `botConfig.js`: + +```js +module.exports = { + token: "BOT_TOKEN", + prefix: "!", + serverId: "SERVER_ID", + + // "en" = English, "tr" = Turkish. + language: "en", + + // Users who can run economy administration commands. + admins: [ + "ADMIN_USER_ID" + ], + + // Roles that can run economy administration commands. + adminRoles: [ + "ADMIN_ROLE_ID" + ], + + // Optional role-name matching. Role IDs are safer and recommended. + adminRoleNames: [], + + debug: false, + countChannel: "COUNT_CHANNEL_ID" +}; +``` + +### 4. Discord Developer Portal + +Enable **Message Content Intent** for prefix commands. + +Invite the bot with the `bot` and `applications.commands` scopes. + +### 5. Start the bot + +```bash +npm start +``` + +The bot deploys the current slash-command definitions directly to `serverId` every time it starts. + +## Commands + +| Command | Purpose | +|---|---| +| `/bal` | Balance and leaderboard rank | +| `/daily` | Daily reward | +| `/weekly` | Weekly reward | +| `/work` | Earn money from a random job | +| `/beg` | Random earning event | +| `/search` | Search for money | +| `/rob` | Robbery game mechanic | +| `/transfer` | Transfer money to another user | +| `/shop` | Show available items | +| `/buy` | Buy a shop item | +| `/inventory` | Show owned items | +| `/lb` | Show the economy leaderboard | +| `/ping` | Show API/client latency | +| `/help` | Show commands and current prefix | +| `/prefix` | Change the server prefix | +| `/addmoney` | Administrator economy command | +| `/setmoney` | Administrator economy command | + +Prefix aliases are also supported. Examples: `!bal`, `!bakiye`, `!daily`, `!günlük`, `!shop`, `!mağaza`, `!inventory`, `!envanter`, `!transfer`, `!aktar`. + +## Administration + +Economy administration commands accept any of the following: + +1. A user ID listed in `admins`. +2. A role ID listed in `adminRoles`. +3. A configured role name in `adminRoleNames`. +4. Discord's **Administrator** permission. +5. Discord's **Manage Server** permission. + +For production servers, role IDs or user IDs are recommended over role names. + +## Localization + +Set `language` in `botConfig.js`: + +```js +language: "en" +``` + +or: + +```js +language: "tr" +``` + +The selected language controls command responses, embeds, help text, cooldown messages, leaderboard labels and slash-command descriptions. + +Restart the bot after changing the language so the slash-command definitions are redeployed. + +## Data and Economy Architecture + +The economy layer uses a small SQLite key-value store with a clear separation between storage and business logic. Economy mutations such as transfers and purchases run inside SQLite transactions, while cooldowns, balances and inventories are scoped by guild and user. + +This architecture was influenced by patterns visible in the open-source `casperiv0/ghostybot` project, which stores economy data per guild and user and keeps fields such as money, bank, inventory and reward timestamps together in its data model. EconomyBot does not copy GhostyBot's implementation; it uses the reference as an architectural comparison. + +## Testing + +Run the full offline test suite: + +```bash +npm test +``` + +The test suite checks: + +- 17 native slash commands +- Slash command schemas +- Prefix context execution +- Turkish aliases +- English and Turkish localization +- User/permission/role administration checks +- SQLite lifecycle and reopening +- Atomic balance changes +- Transfers and self-transfer protection +- Purchases and inventory persistence +- Cooldowns +- Leaderboard ranking +- Native `interactionCreate` execution path + +GitHub Actions also validates Node.js 20, 22 and 24 and runs CodeQL analysis. + +## Security + +Never commit a real Discord bot token. If a token is exposed, regenerate it through the Discord Developer Portal. + +The SQLite database is stored in the local `data/` directory and should not be committed. + +## Project Structure + +```text +EconomyBot/ +├── commands/ # Native slash + shared command implementations +├── events/ +│ ├── clientReady.js # Guild slash deployment +│ ├── interactionCreate.js # Native slash execution +│ └── messageCreate.js # Prefix execution +├── lib/ +│ ├── commandUtils.js # Validation, permissions and formatting +│ ├── context.js # Unified slash/prefix command context +│ ├── database.js # SQLite storage and transaction layer +│ ├── economy.js # Economy business logic +│ └── i18n.js # English/Turkish localization +├── data/ # Local SQLite database (generated) +├── counter.js # Optional counting channel +├── botConfig.js # Local configuration +├── index.js # Application bootstrap +├── tests/test.js # Integration-style offline tests +└── package.json +``` + +## License & Credits + +This project is based on the open-source `ZeroDiscord/EconomyBot` project and has been substantially modernized for Node.js 20+ and discord.js v14. + --- +# Türkçe + ## Özellikler -- 💰 Sunucuya özel bakiye sistemi -- 🎁 Günlük, haftalık ve çalışma ödülleri -- 💼 Dilenme ve arama etkinlikleri -- 🥷 Soygun sistemi -- 💸 Kullanıcılar arası atomik para transferi -- 🏪 Mağaza ve satın alma sistemi -- 🎒 Envanter -- 🏆 Ekonomi sıralaması -- ⚙️ Sunucuya özel prefix -- 🔢 İsteğe bağlı sayaç kanalı -- `/` native slash komutları -- 🇹🇷 Türkçe mesajlar ve Türkçe prefix alias'ları -- 💾 SQLite tabanlı kalıcı depolama -- 🛡️ Yetki ve veri doğrulaması +- Sunucuya özel kalıcı ekonomi bakiyeleri +- SQLite transaction'ları ile atomik para transferi ve mağaza satın alımı +- Kalıcı cooldown'lara sahip günlük, haftalık ve çalışma ödülleri +- Dilenme ve arama etkinlikleri +- Soygun oyun mekaniği +- Kullanıcılar arası para transferi +- Sunucu ekonomi sıralaması ve kullanıcı sırası +- Laptop, Mobile ve PC ürünlerinden oluşan mağaza +- Ürün miktarlarını gruplayan kalıcı envanter +- Sunucuya özel prefix +- Native `/` slash komutları +- İngilizce ve Türkçe prefix alias'ları +- İsteğe bağlı sayaç kanalı +- Kullanıcı ID, rol ID veya rol adına göre yapılandırılabilen yöneticiler +- `botConfig.js` üzerinden İngilizce/Türkçe yanıt dili +- Bot açılışında otomatik guild slash-command deploy +- Güvenli input doğrulaması, safe integer kontrolleri ve kontrollü interaction hata yönetimi +- Otomatik SQLite klasörü oluşturma +- Veritabanı lifecycle koruması ve açık bağlantının güvenli şekilde yeniden açılması +- Node.js 20/22/24 CI testleri ve CodeQL analizi ## Gereksinimler @@ -41,7 +264,7 @@ | discord.js | `14.27.0` | | better-sqlite3 | `12.11.1` | -Node.js 22 veya 24 kullanmak önerilir. +Yeni kurulumlar için Node.js 22 veya 24 önerilir. ## Kurulum @@ -58,116 +281,166 @@ cd EconomyBot npm install ``` -### 3. `botConfig.js` dosyasını ayarlayın +### 3. Botu yapılandırın + +`botConfig.js` dosyasını düzenleyin: ```js module.exports = { token: "BOT_TOKEN", prefix: "!", serverId: "SUNUCU_ID", + + // "en" = İngilizce, "tr" = Türkçe. + language: "tr", + admins: [ "YETKİLİ_KULLANICI_ID" ], - debug: true, + + adminRoles: [ + "YETKİLİ_ROL_ID" + ], + + adminRoleNames: [], + + debug: false, countChannel: "SAYAÇ_KANAL_ID" }; ``` -`serverId`, slash komutlarının otomatik deploy edileceği sunucudur. - ### 4. Discord Developer Portal -Prefix komutları için **Message Content Intent** etkinleştirilmelidir. +Prefix komutları için **Message Content Intent** açılmalıdır. -Botu davet ederken `bot` ve `applications.commands` kapsamlarının bulunduğundan emin olun. +Bot davetinde `bot` ve `applications.commands` kapsamlarının bulunduğundan emin olun. -### 5. Başlatın +### 5. Botu başlatın ```bash npm start ``` -Başarılı başlatma örneği: - -```text -EconomyBot çevrimiçi! -17 slash komutu sunucuya başarıyla deploy edildi. -``` +Bot her açılışta `serverId` sunucusundaki güncel slash komutlarını yeniden deploy eder. ## Komutlar -### Ekonomi +| Komut | Görevi | +|---|---| +| `/bal` | Bakiye ve ekonomi sırasını gösterir | +| `/daily` | Günlük ödül verir | +| `/weekly` | Haftalık ödül verir | +| `/work` | Rastgele iş ile para kazandırır | +| `/beg` | Rastgele para kazanma etkinliği | +| `/search` | Para arar | +| `/rob` | Soygun oyun mekaniği | +| `/transfer` | Başka kullanıcıya para gönderir | +| `/shop` | Mağazayı gösterir | +| `/buy` | Mağazadan ürün alır | +| `/inventory` | Envanteri gösterir | +| `/lb` | Ekonomi sıralamasını gösterir | +| `/ping` | API/istemci gecikmesini gösterir | +| `/help` | Komutları ve güncel prefix'i gösterir | +| `/prefix` | Sunucu prefix'ini değiştirir | +| `/addmoney` | Yönetici ekonomi komutu | +| `/setmoney` | Yönetici ekonomi komutu | + +Prefix alias'ları da desteklenir. Örnekler: `!bal`, `!bakiye`, `!daily`, `!günlük`, `!shop`, `!mağaza`, `!inventory`, `!envanter`, `!transfer`, `!aktar`. + +## Yönetici Sistemi + +Ekonomi yönetim komutları şu yöntemlerden biriyle kullanılabilir: + +1. `admins` içerisindeki kullanıcı ID'si. +2. `adminRoles` içerisindeki rol ID'si. +3. `adminRoleNames` içerisindeki yapılandırılmış rol adı. +4. Discord **Administrator** yetkisi. +5. Discord **Sunucuyu Yönet / Manage Server** yetkisi. + +Üretim sunucularında rol adı yerine rol ID'si veya kullanıcı ID'si kullanılması önerilir. + +## Dil Sistemi + +`botConfig.js` içindeki `language` alanını değiştirin: -| Slash | Prefix / Alias | Açıklama | -|---|---|---| -| `/bal` | `!bal`, `!bakiye` | Bakiye ve sıralama | -| `/daily` | `!daily`, `!günlük` | Günlük ödül | -| `/weekly` | `!weekly`, `!haftalık` | Haftalık ödül | -| `/work` | `!work`, `!çalış` | Çalışarak para kazan | -| `/beg` | `!beg`, `!dilen` | Rastgele para kazan | -| `/search` | `!search`, `!ara` | Para ara | -| `/rob` | `!rob`, `!soy` | Bir kullanıcıdan para çalmayı dene | -| `/transfer` | `!transfer`, `!aktar` | Para gönder | +```js +language: "tr" +``` -### Mağaza +veya: -| Slash | Prefix / Alias | Açıklama | -|---|---|---| -| `/shop` | `!shop`, `!mağaza` | Mağazayı göster | -| `/buy` | `!buy`, `!al` | Ürün satın al | -| `/inventory` | `!inventory`, `!envanter` | Envanteri göster | +```js +language: "en" +``` -### Yönetim & Araçlar +Seçilen dil; komut yanıtlarını, embed'leri, yardım metnini, cooldown mesajlarını, sıralama etiketlerini ve slash-komut açıklamalarını etkiler. -| Slash | Prefix / Alias | Açıklama | -|---|---|---| -| `/addmoney` | `!addmoney`, `!paraekle` | Yetkili para ekler | -| `/setmoney` | `!setmoney`, `!parayarla` | Yetkili bakiyeyi belirler | -| `/prefix` | `!prefix`, `!önek` | Prefix ayarlar / sıfırlar | -| `/lb` | `!lb`, `!sıralama` | Ekonomi sıralaması | -| `/ping` | `!ping`, `!gecikme` | Gecikme bilgisi | -| `/help` | `!help`, `!yardım` | Komut yardımını göster | +Dil değiştirdikten sonra slash-komut açıklamalarının yeniden deploy edilmesi için botu yeniden başlatın. -## Mimari +## Veri ve Ekonomi Mimarisi -```text -EconomyBot/ -├── commands/ # Native slash + ortak komut tanımları -├── events/ -│ ├── clientReady.js # Guild slash deploy -│ ├── interactionCreate.js# Native slash execution -│ └── messageCreate.js # Prefix execution -├── lib/ -│ ├── commandUtils.js # Ortak yardımcılar -│ ├── context.js # Slash/prefix context katmanı -│ ├── database.js # SQLite KV store -│ └── economy.js # Ekonomi iş mantığı -├── counter.js # Sayaç sistemi -├── botConfig.js # Yerel bot ayarları -├── index.js # Uygulama bootstrap -├── tests/test.js # Kapsamlı offline testler -└── package.json -``` +Ekonomi katmanı, depolama ve iş mantığını birbirinden ayıran SQLite tabanlı bir key-value store kullanır. Transfer ve satın alma gibi bakiye değişiklikleri SQLite transaction'ları içinde gerçekleştirilir; cooldown, bakiye ve envanter verileri sunucu + kullanıcı bazında tutulur. + +Mimari karşılaştırmada açık kaynak `casperiv0/ghostybot` projesindeki sunucu + kullanıcı ekonomi modeli referans alınmıştır. GhostyBot veri modelinde para, banka, envanter ve ödül zamanları gibi alanları kullanıcı/sunucu bağlamında tutuyor. EconomyBot GhostyBot kodunu kopyalamaz; yalnızca mimari yaklaşımı karşılaştırma amacıyla kullanır. -## Test +## Testler -Projede native slash yürütmesini, prefix alias akışını, SQLite işlemlerini, transfer atomikliğini, satın almayı, envanteri, cooldown'ları ve yetki kontrollerini doğrulayan testler vardır. +Tam offline test paketini çalıştırın: ```bash npm test ``` -Ayrıca GitHub Actions Node.js 20, 22 ve 24 üzerinde sözdizimi ve test kontrollerini çalıştırır. +Testler şunları kontrol eder: + +- 17 native slash komutu +- Slash command şemaları +- Prefix context yürütmesi +- Türkçe alias'lar +- İngilizce ve Türkçe dil sistemi +- Kullanıcı/yetki/rol yönetici kontrolleri +- SQLite lifecycle ve yeniden açılma +- Atomik bakiye değişiklikleri +- Transfer ve kendine transfer koruması +- Satın alma ve envanter kalıcılığı +- Cooldown'lar +- Ekonomi sıralaması +- Native `interactionCreate` akışı + +GitHub Actions ayrıca Node.js 20, 22 ve 24 üzerinde testleri ve CodeQL analizini çalıştırır. ## Güvenlik -Bot token'ınızı GitHub'a veya başka bir herkese açık yere yüklemeyin. Token açığa çıktıysa Discord Developer Portal üzerinden yenileyin. +Gerçek Discord bot token'ınızı GitHub'a commit etmeyin. Token açığa çıktıysa Discord Developer Portal üzerinden yenileyin. + +SQLite veritabanı yerel `data/` klasöründe tutulur ve Git'e gönderilmemelidir. -Yerel SQLite veritabanı Git tarafından takip edilmemelidir. +## Proje Yapısı + +```text +EconomyBot/ +├── commands/ # Native slash + ortak komut implementasyonları +├── events/ +│ ├── clientReady.js # Guild slash deploy +│ ├── interactionCreate.js # Native slash yürütmesi +│ └── messageCreate.js # Prefix yürütmesi +├── lib/ +│ ├── commandUtils.js # Doğrulama, yetki ve formatlama +│ ├── context.js # Slash/prefix ortak context +│ ├── database.js # SQLite depolama ve transaction katmanı +│ ├── economy.js # Ekonomi iş mantığı +│ └── i18n.js # İngilizce/Türkçe localization +├── data/ # Yerel SQLite veritabanı (otomatik oluşturulur) +├── counter.js # İsteğe bağlı sayaç kanalı +├── botConfig.js # Yerel ayarlar +├── index.js # Uygulama başlangıcı +├── tests/test.js # Offline entegrasyon testleri +└── package.json +``` -## Kaynak +## Lisans ve Kaynak -Proje, `ZeroDiscord/EconomyBot` temel alınarak modernize edilmiştir. Eski `quick.eco` bağımlılığı kaldırılmış, ekonomi katmanı SQLite üzerinde yeniden düzenlenmiş ve slash komutları native discord.js v14 yapısına geçirilmiştir. +Bu proje açık kaynak `ZeroDiscord/EconomyBot` projesi temel alınarak Node.js 20+ ve discord.js v14 için kapsamlı şekilde modernize edilmiştir. --- From 8c0f95b913e0d1f63a0fb04dcdff363807844db1 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:01:38 +0300 Subject: [PATCH 163/175] Fix integration test interaction contract and exercise real prefix event path --- tests/test.js | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/tests/test.js b/tests/test.js index ec41ce6..7902458 100644 --- a/tests/test.js +++ b/tests/test.js @@ -82,6 +82,10 @@ function makeContext(userId, options = {}, language = client.config.language) { const permissions = new PermissionsBitField(); const interaction = { createdTimestamp: Date.now(), + user: currentUser, + guildId, + guild: { id: guildId, name: "Test Guild", iconURL: () => null }, + member: { permissions, roles: { cache: roleCache } }, options: { getUser: (name) => (options[name] && typeof options[name] === "object" ? options[name] : null), getInteger: (name) => options[name] ?? null, @@ -93,10 +97,6 @@ function makeContext(userId, options = {}, language = client.config.language) { }; const replies = []; const ctx = createInteractionContext(interaction, { ...client, config: { ...client.config, language } }); - ctx.user = currentUser; - ctx.userId = userId; - ctx.guild = { id: guildId, name: "Test Guild", iconURL: () => null }; - ctx.guildId = guildId; ctx.member = { permissions, roles: { cache: roleCache } }; ctx.reply = async (payload) => { replies.push(payload); return payload; }; return { ctx, replies }; @@ -111,7 +111,6 @@ async function runSlash(name, userId = adminId, options = {}, language = client. } (async () => { - // Database lifecycle and core ledger invariants. assert.equal(db.connection.open, true); assert.equal(eco.getBalance(guildId, adminId), 20_000); const transfer = eco.transfer(guildId, adminId, targetId, 500); @@ -133,7 +132,6 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.equal(cooldown2.onCooldown, true); assert.ok(cooldown2.remainingMs > 0); - // Admin detection: explicit user, permission bit, role ID, and role name. const adminCtx = makeContext(adminId).ctx; assert.equal(isAdmin(adminCtx), true); client.config.adminRoles = []; @@ -147,7 +145,6 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.equal(isAdmin(adminCtx), true); client.config.adminRoleNames = []; - // Every command executes through the same native context. await runSlash("addmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }, "en"); await runSlash("bal"); await runSlash("beg"); @@ -167,7 +164,6 @@ async function runSlash(name, userId = adminId, options = {}, language = client. await runSlash("work"); assert.equal(db.getPrefix(guildId, "!"), "$"); - // Language switching from config/context. const en = createTranslator("en"); const tr = createTranslator("tr"); assert.equal(en("economy.balanceTitle"), "Balance"); @@ -177,14 +173,12 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.match(enHelp.embeds?.[0]?.data?.title || "", /EconomyBot Commands/); assert.match(trHelp.embeds?.[0]?.data?.title || "", /EconomyBot Komutları/); - // Permission failure must be explicit. const originalRoles = client.config.adminRoles; client.config.adminRoles = []; const unauthorized = await runSlash("addmoney", targetId, { kullanici: client.users.cache.get(adminId), miktar: 100 }, "en"); assert.equal(unauthorized.ephemeral, true); client.config.adminRoles = originalRoles; - // Native interactionCreate path. const eventReplies = []; const interaction = { commandName: "ping", @@ -204,22 +198,20 @@ async function runSlash(name, userId = adminId, options = {}, language = client. await require("../events/interactionCreate")(client, interaction); assert.ok(eventReplies.length > 0, "interactionCreate /ping yanıt üretmedi."); - // Prefix path and Turkish alias. const prefixReplies = []; - const prefixContext = createPrefixContext({ + const prefixMessage = { guild: { id: guildId }, inGuild: () => true, author: client.users.cache.get(adminId), content: "$bakiye", + createdTimestamp: Date.now(), channel: { id: "not-counter" }, member: { permissions: new PermissionsBitField() }, mentions: { users: { first: () => null }, members: { first: () => null } }, reply: async (payload) => { prefixReplies.push(payload); return payload; } - }, client, []); - prefixContext.prefix = "$"; - // Execute the actual command directly through the prefix context as well. - await commandMap.get("bal").execute(prefixContext); - assert.ok(prefixReplies.length > 0, "Prefix aliası komut context'i yanıt üretmedi."); + }; + await require("../events/messageCreate")(client, prefixMessage); + assert.ok(prefixReplies.length > 0, "Prefix aliası gerçek messageCreate akışında yanıt üretmedi."); db.close(); for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); @@ -228,7 +220,7 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.equal(db.connection.open, true); db.close(); for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); - console.log("TÜM TESTLER BAŞARILI: DB lifecycle, atomic ekonomi işlemleri, cooldown, 17 slash komutu, prefix context, dil sistemi ve admin yetkilendirmesi doğrulandı."); + console.log("TÜM TESTLER BAŞARILI: DB lifecycle, atomic ekonomi, cooldown, 17 slash komutu, gerçek prefix event'i, dil sistemi ve admin yetkilendirmesi doğrulandı."); })().catch((error) => { try { db.close(); } catch {} for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); From 6f5277b28dc29b3c61351e1d78b5724361790ea7 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:02:02 +0300 Subject: [PATCH 164/175] Make optional admin config backward-compatible --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 634ba5f..329a66a 100644 --- a/index.js +++ b/index.js @@ -10,9 +10,9 @@ if (!config || typeof config !== "object") throw new Error("botConfig.js geçerl if (!config.token || config.token === "YOUR_TOKEN") throw new Error("botConfig.js içindeki token ayarlanmalı."); if (!config.serverId || !/^\d{17,20}$/.test(String(config.serverId))) throw new Error("botConfig.js içindeki serverId geçerli bir Discord sunucu ID'si olmalı."); if (!config.prefix || typeof config.prefix !== "string" || !/^\S{1,5}$/.test(config.prefix)) throw new Error("botConfig.js içindeki prefix 1-5 karakter ve boşluksuz olmalı."); -if (!Array.isArray(config.admins)) throw new Error("botConfig.js içindeki admins bir dizi olmalı."); -if (!Array.isArray(config.adminRoles)) throw new Error("botConfig.js içindeki adminRoles bir dizi olmalı."); -if (!Array.isArray(config.adminRoleNames)) throw new Error("botConfig.js içindeki adminRoleNames bir dizi olmalı."); +config.admins = Array.isArray(config.admins) ? config.admins.map(String) : []; +config.adminRoles = Array.isArray(config.adminRoles) ? config.adminRoles.map(String) : []; +config.adminRoleNames = Array.isArray(config.adminRoleNames) ? config.adminRoleNames.map(String) : []; config.language = normalizeLanguage(config.language); const database = new KeyValueStore(); From a3a33ff862184b4fa8d260e685b630b709481175 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:02:38 +0300 Subject: [PATCH 165/175] Fix test admin-role state isolation --- tests/test.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test.js b/tests/test.js index 7902458..9e3a1f0 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5,7 +5,7 @@ const path = require("node:path"); const { Collection, PermissionsBitField } = require("discord.js"); const { KeyValueStore } = require("../lib/database"); const EconomyManager = require("../lib/economy"); -const { createInteractionContext, createPrefixContext } = require("../lib/context"); +const { createInteractionContext } = require("../lib/context"); const { createTranslator } = require("../lib/i18n"); const { isAdmin } = require("../lib/commandUtils"); @@ -52,9 +52,10 @@ const guildId = "123456789012345678"; const adminId = "123456789012345679"; const targetId = "123456789012345680"; const botId = "123456789012345681"; +const ADMIN_ROLE_ID = "999999999999999999"; const client = { - config: { prefix: "!", language: "en", admins: [], adminRoles: ["999999999999999999"], adminRoleNames: [], countChannel: "" }, + config: { prefix: "!", language: "en", admins: [], adminRoles: [ADMIN_ROLE_ID], adminRoleNames: [], countChannel: "" }, db, eco, commands: new Collection(), @@ -78,7 +79,7 @@ eco.setMoney(guildId, targetId, 5_000); function makeContext(userId, options = {}, language = client.config.language) { const currentUser = client.users.cache.get(userId); const roleCache = new Collection(); - roleCache.set("999999999999999999", { id: "999999999999999999", name: "Economy Admin" }); + roleCache.set(ADMIN_ROLE_ID, { id: ADMIN_ROLE_ID, name: "Economy Admin" }); const permissions = new PermissionsBitField(); const interaction = { createdTimestamp: Date.now(), @@ -132,17 +133,19 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.equal(cooldown2.onCooldown, true); assert.ok(cooldown2.remainingMs > 0); + // Admin detection: configured user, Administrator, configured role ID, and configured role name. const adminCtx = makeContext(adminId).ctx; assert.equal(isAdmin(adminCtx), true); client.config.adminRoles = []; adminCtx.member.permissions.add(PermissionsBitField.Flags.Administrator); assert.equal(isAdmin(adminCtx), true); adminCtx.member.permissions.remove(PermissionsBitField.Flags.Administrator); - client.config.adminRoles = ["999999999999999999"]; + client.config.adminRoles = [ADMIN_ROLE_ID]; assert.equal(isAdmin(adminCtx), true); client.config.adminRoles = []; client.config.adminRoleNames = ["economy admin"]; assert.equal(isAdmin(adminCtx), true); + client.config.adminRoles = [ADMIN_ROLE_ID]; client.config.adminRoleNames = []; await runSlash("addmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }, "en"); @@ -206,7 +209,7 @@ async function runSlash(name, userId = adminId, options = {}, language = client. content: "$bakiye", createdTimestamp: Date.now(), channel: { id: "not-counter" }, - member: { permissions: new PermissionsBitField() }, + member: { permissions: new PermissionsBitField(), roles: { cache: new Collection([[ADMIN_ROLE_ID, { id: ADMIN_ROLE_ID, name: "Economy Admin" }]]) } }, mentions: { users: { first: () => null }, members: { first: () => null } }, reply: async (payload) => { prefixReplies.push(payload); return payload; } }; From 6902e59eee5c60540829e6b2d89933551a452418 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:03:40 +0300 Subject: [PATCH 166/175] Localize counting channel messages --- counter.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/counter.js b/counter.js index 7e13b4b..8ad0843 100644 --- a/counter.js +++ b/counter.js @@ -1,9 +1,10 @@ +const { createTranslator } = require("./lib/i18n"); + async function counter(message, client) { + const t = createTranslator(client.config.language); const key = `counter:${message.guild.id}`; let state = client.db.get(key, { number: 0, author: null }); - if (!state || typeof state !== "object" || !Number.isSafeInteger(state.number) || state.number < 0) { - state = { number: 0, author: null }; - } + if (!state || typeof state !== "object" || !Number.isSafeInteger(state.number) || state.number < 0) state = { number: 0, author: null }; const reject = async (text) => { await message.delete().catch(() => {}); @@ -12,17 +13,19 @@ async function counter(message, client) { return false; }; - if (message.author.id === state.author) return reject("Sıra sende değil, lütfen başka birinin sayı yazmasını bekle."); - if (!/^\d+$/.test(message.content)) return reject("Bu kanaldaki mesajlar sayı olmalıdır."); + const yourTurn = client.config.language === "tr" ? "Sıra sende değil, lütfen başka birinin sayı yazmasını bekle." : "It is not your turn. Please wait for another user to post the next number."; + const onlyNumbers = client.config.language === "tr" ? "Bu kanaldaki mesajlar sayı olmalıdır." : "Messages in this channel must contain numbers only."; + const nextNumber = (number) => client.config.language === "tr" ? `Sıradaki sayı **${number}** olmalıdır.` : `The next number must be **${number}**.`; + const nextTopic = (number) => client.config.language === "tr" ? `Sıradaki sayı ${number} olmalıdır.` : `The next number should be ${number}.`; + + if (message.author.id === state.author) return reject(yourTurn); + if (!/^\d+$/.test(message.content)) return reject(onlyNumbers); const number = Number(message.content); - if (!Number.isSafeInteger(number) || number !== state.number + 1) { - return reject(`Sıradaki sayı **${state.number + 1}** olmalıdır.`); - } + if (!Number.isSafeInteger(number) || number !== state.number + 1) return reject(nextNumber(state.number + 1)); - const next = { number, author: message.author.id }; - client.db.set(key, next); - await message.channel.setTopic(`Sıradaki sayı ${number + 1} olmalıdır.`).catch(() => {}); + client.db.set(key, { number, author: message.author.id }); + await message.channel.setTopic(nextTopic(number + 1)).catch(() => {}); return true; } From 10fcdc8a319ddcebedf3c72de27e4dd05aa94060 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:04:23 +0300 Subject: [PATCH 167/175] Support Discord role arrays and collections in admin checks --- lib/commandUtils.js | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/commandUtils.js b/lib/commandUtils.js index 3bbac36..6cf5108 100644 --- a/lib/commandUtils.js +++ b/lib/commandUtils.js @@ -10,12 +10,31 @@ function hasPermissionValue(permissionSource, permission) { } } +function memberHasConfiguredRole(member, roleIds, roleNames) { + if (!member) return false; + + const roles = member.roles?.cache; + if (roles) { + for (const role of roles.values()) { + if (roleIds.includes(String(role.id))) return true; + if (roleNames.includes(String(role.name).trim().toLowerCase())) return true; + } + } + + if (Array.isArray(member.roles)) { + for (const roleId of member.roles) { + if (roleIds.includes(String(roleId))) return true; + } + } + + return false; +} + function getMemberForRoleChecks(ctx) { - if (ctx.member?.roles?.cache) return ctx.member; + if (ctx.member) return ctx.member; const cached = ctx.guild?.members?.cache?.get?.(ctx.userId); - if (cached?.roles?.cache) return cached; - const interactionCached = ctx.interaction?.guild?.members?.cache?.get?.(ctx.userId); - return interactionCached?.roles?.cache ? interactionCached : null; + if (cached) return cached; + return ctx.interaction?.guild?.members?.cache?.get?.(ctx.userId) || null; } function isAdmin(ctx) { @@ -30,15 +49,7 @@ function isAdmin(ctx) { const roleIds = Array.isArray(config.adminRoles) ? config.adminRoles.map(String).filter((id) => /^\d{17,20}$/.test(id)) : []; const roleNames = Array.isArray(config.adminRoleNames) ? config.adminRoleNames.map((name) => String(name).trim().toLowerCase()).filter(Boolean) : []; - const member = getMemberForRoleChecks(ctx); - const roles = member?.roles?.cache; - if (!roles) return false; - - for (const role of roles.values()) { - if (roleIds.includes(String(role.id))) return true; - if (roleNames.includes(String(role.name).trim().toLowerCase())) return true; - } - return false; + return memberHasConfiguredRole(getMemberForRoleChecks(ctx), roleIds, roleNames); } function parsePositiveInteger(value) { From 408571b36ad2adbeae57ff5aa0f6ec3ffe07211d Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:04:39 +0300 Subject: [PATCH 168/175] Add slash role-array permission regression tests --- tests/test.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test.js b/tests/test.js index 9e3a1f0..6baafc6 100644 --- a/tests/test.js +++ b/tests/test.js @@ -76,17 +76,20 @@ client.users.cache.set(botId, client.user); eco.setMoney(guildId, adminId, 20_000); eco.setMoney(guildId, targetId, 5_000); -function makeContext(userId, options = {}, language = client.config.language) { +function makeContext(userId, options = {}, language = client.config.language, memberMode = "collection") { const currentUser = client.users.cache.get(userId); const roleCache = new Collection(); roleCache.set(ADMIN_ROLE_ID, { id: ADMIN_ROLE_ID, name: "Economy Admin" }); const permissions = new PermissionsBitField(); + const member = memberMode === "array" + ? { permissions, roles: [ADMIN_ROLE_ID] } + : { permissions, roles: { cache: roleCache } }; const interaction = { createdTimestamp: Date.now(), user: currentUser, guildId, guild: { id: guildId, name: "Test Guild", iconURL: () => null }, - member: { permissions, roles: { cache: roleCache } }, + member, options: { getUser: (name) => (options[name] && typeof options[name] === "object" ? options[name] : null), getInteger: (name) => options[name] ?? null, @@ -98,7 +101,7 @@ function makeContext(userId, options = {}, language = client.config.language) { }; const replies = []; const ctx = createInteractionContext(interaction, { ...client, config: { ...client.config, language } }); - ctx.member = { permissions, roles: { cache: roleCache } }; + ctx.member = member; ctx.reply = async (payload) => { replies.push(payload); return payload; }; return { ctx, replies }; } @@ -133,9 +136,10 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.equal(cooldown2.onCooldown, true); assert.ok(cooldown2.remainingMs > 0); - // Admin detection: configured user, Administrator, configured role ID, and configured role name. const adminCtx = makeContext(adminId).ctx; assert.equal(isAdmin(adminCtx), true); + const arrayRoleCtx = makeContext(adminId, {}, "en", "array").ctx; + assert.equal(isAdmin(arrayRoleCtx), true); client.config.adminRoles = []; adminCtx.member.permissions.add(PermissionsBitField.Flags.Administrator); assert.equal(isAdmin(adminCtx), true); @@ -190,7 +194,7 @@ async function runSlash(name, userId = adminId, options = {}, language = client. guild: { id: guildId, name: "Test Guild" }, guildId, user: client.users.cache.get(adminId), - member: { permissions: new PermissionsBitField() }, + member: { permissions: new PermissionsBitField(), roles: [ADMIN_ROLE_ID] }, memberPermissions: new PermissionsBitField(), options: { get: () => null, getUser: () => null, getInteger: () => null, getString: () => null, getMember: () => null }, reply: async (payload) => { eventReplies.push(payload); return payload; }, From 72a73becc01976787b3f55450dc8869de5c6c560 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:18:27 +0300 Subject: [PATCH 169/175] Update shop description to customizable shop --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a15612e..887d8f4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ - Robbery game mechanic - User-to-user transfers - Server economy leaderboard and rank lookup -- Shop with Laptop, Mobile and PC items +- Customizable shop - Persistent inventory with grouped item quantities - Server-specific prefix - Native `/` slash commands @@ -242,7 +242,7 @@ This project is based on the open-source `ZeroDiscord/EconomyBot` project and ha - Soygun oyun mekaniği - Kullanıcılar arası para transferi - Sunucu ekonomi sıralaması ve kullanıcı sırası -- Laptop, Mobile ve PC ürünlerinden oluşan mağaza +- Özelleştirilebilir market - Ürün miktarlarını gruplayan kalıcı envanter - Sunucuya özel prefix - Native `/` slash komutları From 0dcead9df67d71df9236cfb81550c6018aa7f081 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:42:47 +0300 Subject: [PATCH 170/175] feat: re-engineer economy into marketplace and inventory platform Re-engineer EconomyBot with configurable items, persistent market stock, member marketplace listings, item drops, optional shared inventory foundation, bilingual localization and comprehensive documentation. --- README.md | 730 +++++++++++++++++++++++++++--------------- botConfig.js | 41 +++ commands/buy.js | 44 ++- commands/daily.js | 6 +- commands/inventory.js | 19 +- commands/sell.js | 33 ++ commands/shop.js | 30 +- commands/weekly.js | 6 +- index.js | 79 ++++- lib/context.js | 9 + lib/economy.js | 177 ++++++++-- lib/i18n.js | 58 ++-- lib/inventory.js | 78 +++++ tests/test.js | 124 ++++--- 14 files changed, 1047 insertions(+), 387 deletions(-) create mode 100644 commands/sell.js create mode 100644 lib/inventory.js diff --git a/README.md b/README.md index 887d8f4..046d53f 100644 --- a/README.md +++ b/README.md @@ -2,211 +2,323 @@ # EconomyBot -### Modern • Node.js 20+ • discord.js v14 • English / Türkçe +### Re-engineered • Node.js 20+ • discord.js v14 • English / Türkçe -**A self-hosted Discord economy bot with native slash commands, prefix aliases, SQLite persistence, administration, shop, inventory and server leaderboards.** - -EconomyBot Preview +**A self-hosted, configuration-first Discord economy and community marketplace platform with persistent inventories, member shops, transactional trading, configurable item drops, native slash commands and bilingual localization.** [![Node.js](https://img.shields.io/badge/Node.js-20%2B-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/) [![discord.js](https://img.shields.io/badge/discord.js-v14-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.js.org/) [![SQLite](https://img.shields.io/badge/SQLite-better--sqlite3-003B57?style=for-the-badge&logo=sqlite&logoColor=white)](https://www.sqlite.org/) [![License](https://img.shields.io/badge/License-Nginx-8A2BE2?style=for-the-badge)](LICENSE) +🇹🇷 Türkçe🇬🇧 English💬 Join the Discord Server + -# English - -## Features - -- Persistent server-scoped economy balances -- Atomic transfers and shop purchases using SQLite transactions -- Daily, weekly and work rewards with persistent cooldowns -- Beg and search earning events -- Robbery game mechanic -- User-to-user transfers -- Server economy leaderboard and rank lookup -- Customizable shop -- Persistent inventory with grouped item quantities -- Server-specific prefix -- Native `/` slash commands -- Prefix commands with English and Turkish aliases -- Optional counting channel -- Configurable administrators by user ID, role ID or role name -- English/Turkish response localization controlled from `botConfig.js` -- Automatic guild slash-command deployment on startup -- Defensive input validation, safe integer checks and graceful interaction error handling -- Automatic SQLite database directory creation -- Database lifecycle protection and safe reopening after an explicit close -- Node.js 20/22/24 CI compatibility tests and CodeQL scanning - -## Requirements +--- -| Component | Supported | -|---|---| -| Node.js | `20+` | -| discord.js | `14.27.0` | -| better-sqlite3 | `12.11.1` | +## English -Node.js 22 or 24 is recommended for new installations. +> **Navigation:** [Overview](#overview) · [Why this version is different](#why-this-version-is-different) · [Architecture](#architecture) · [Configuration](#configuration) · [Market & Member Shops](#market--member-shops) · [Inventory & Drops](#inventory--drops) · [Shared Inventory](#shared-inventory-foundation) · [Commands](#commands) · [Installation](#installation) · [Testing](#testing) -## Installation +### Overview -### 1. Clone the repository +EconomyBot is no longer a small modernization of the original `ZeroDiscord/EconomyBot` codebase. This release should be treated as a **separately engineered generation of the bot**. -```bash -git clone https://github.com/LoiFragola/EconomyBot.git -cd EconomyBot -``` +The original project provided the starting point and historical reference, but the runtime, storage boundaries, command layer, permission handling, localization pipeline, inventory model, market behavior and transaction strategy have been substantially redesigned around modern Node.js 20+ and discord.js v14. -### 2. Install dependencies +In practical terms: this is intended to feel like **a different bot built on the same project lineage**, not a cosmetic upgrade. -```bash -npm install +### Why this version is different + +This release introduces a configuration-first domain model instead of burying economy definitions inside command files. The system now validates the configured item registry and market during startup, separates canonical items from purchasable market entries, persists market stock, and maintains player listings as independent transactional records. + +The economy engine was also rebuilt around explicit service boundaries. SQLite is used as a durable state layer, money operations and marketplace settlements execute atomically, inventory operations are centralized, cooldown state is persistent, and slash/prefix commands execute against a shared command context rather than relying on compatibility hacks. + +The command system was rebuilt for native discord.js v14 interactions, with centralized validation, bilingual translation, role/user/permission-based administration, alias resolution and defensive error handling. The project also includes CI coverage for multiple supported Node.js versions and CodeQL analysis. + +### Architecture + +```text + ┌─────────────────────────┐ + │ Discord Gateway │ + └────────────┬────────────┘ + │ + ┌───────────────────┴───────────────────┐ + │ │ + Slash Interactions Prefix Messages + │ │ + └───────────────────┬───────────────────┘ + │ + Unified Command Context + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + Economy Service Inventory Service Localization + │ │ │ + └───────────────────┼───────────────────┘ + │ + Transactional SQLite + │ + ┌─────────────────────────┼─────────────────────────┐ + │ │ │ + Balances Inventories Marketplace + │ │ │ + Cooldowns Drops Member Listings ``` -### 3. Configure the bot +The marketplace and inventory layers were intentionally separated so a future roleplay bot can share the same inventory representation without forcing roleplay functionality into the economy bot itself. -Edit `botConfig.js`: +The architecture was also informed by patterns visible in the open-source `casperiv0/ghostybot` project, particularly its per-guild/per-user economy organization and inventory/reward concepts. This repository does **not** copy GhostyBot's implementation; the project is used as an architectural reference only. + +### Configuration + +Nearly every piece of the item economy is now declarative in `botConfig.js`. ```js module.exports = { token: "BOT_TOKEN", prefix: "!", serverId: "SERVER_ID", - - // "en" = English, "tr" = Turkish. language: "en", - // Users who can run economy administration commands. - admins: [ - "ADMIN_USER_ID" + admins: ["ADMIN_USER_ID"], + adminRoles: ["ADMIN_ROLE_ID"], + adminRoleNames: [], + + market: [ + { item: "laptop", price: 2000, stock: 10 }, + { item: "mobile", price: 1000, stock: 15 }, + { item: "pc", price: 3000, stock: 5 } ], - // Roles that can run economy administration commands. - adminRoles: [ - "ADMIN_ROLE_ID" + items: [ + { + id: "laptop", + name: "Laptop", + description: "A portable computer.", + dailyDropChance: 0, + weeklyDropChance: 2 + }, + { + id: "mobile", + name: "Mobile", + description: "A personal mobile phone.", + dailyDropChance: 1, + weeklyDropChance: 4 + }, + { + id: "pc", + name: "PC", + description: "A desktop computer.", + dailyDropChance: 0, + weeklyDropChance: 1 + } ], - // Optional role-name matching. Role IDs are safer and recommended. - adminRoleNames: [], + sharedInventory: { + enabled: false, + databasePath: "./data/shared-economy.sqlite" + }, debug: false, countChannel: "COUNT_CHANNEL_ID" }; ``` -### 4. Discord Developer Portal +`items` is the **canonical item registry**. If an item does not exist here, the bot will not allow it to be introduced through commands. -Enable **Message Content Intent** for prefix commands. +`market` is a **sales catalog**, not the item registry. Every market entry must reference a configured item and must start with stock greater than zero. Initial stock is persisted per guild in SQLite, so restarting the bot does not silently refill sold-out products. -Invite the bot with the `bot` and `applications.commands` scopes. +### Market & Member Shops -### 5. Start the bot +The old hard-coded “Laptop / Mobile / PC” concept has been replaced with a real catalog system. -```bash -npm start +The system market is defined through `market` in configuration. Prices and initial stock are configurable without touching command source code. + +Every member can participate in the marketplace. A member can take an item from their own inventory and publish it as a marketplace listing with `/sell`. + +A member listing contains: + +- The configured item identity +- Seller ID +- Quantity listed +- Unit price +- Creation timestamp +- A unique listing ID + +A buyer can purchase from that listing with `/buy `. The settlement is performed inside the same SQLite transaction: buyer funds are checked, seller funds are credited, the listing quantity is reduced or removed, and the purchased item is delivered to the buyer's inventory as one atomic operation. + +Members therefore do not need an administrator to create a personal shop. A listing itself acts as a **member shop inventory slot**, while `/shop` provides a single community-facing market view. + +Most importantly, members cannot manufacture arbitrary item types through the marketplace. They may only sell items that: + +1. Exist in `items`. +2. Are currently present in their own inventory. +3. Are listed with a valid positive quantity and price. + +### Inventory & Drops + +The inventory layer is now an explicit service rather than a side effect of shopping code. + +Items can enter inventories from the configured market, member-to-member marketplace purchases, daily rewards and weekly rewards. + +Daily and weekly commands can also produce item drops. Each item controls its own probability: + +```js +{ + id: "mobile", + name: "Mobile", + description: "A personal mobile phone.", + dailyDropChance: 1, + weeklyDropChance: 4 +} +``` + +Drop values are percentages. The configured probabilities for a drop type may not exceed 100% in total. The remaining probability represents “no item drop”. + +This makes the reward table tunable without modifying command code. + +### Shared Inventory Foundation + +The inventory key space is designed around a stable `inventory::` representation so another bot can consume the same logical inventory records. + +This is deliberately **optional**. By default, EconomyBot uses its own local SQLite database. + +To prepare a future roleplay bot to share inventories, configure: + +```js +sharedInventory: { + enabled: true, + databasePath: "./data/shared-economy.sqlite" +} ``` -The bot deploys the current slash-command definitions directly to `serverId` every time it starts. +Only enable this when the participating applications are intentionally designed to use the same SQLite file and compatible item IDs. The roleplay bot can then build on the same inventory namespace without requiring EconomyBot to absorb roleplay-specific business logic. -## Commands +### Commands | Command | Purpose | |---|---| -| `/bal` | Balance and leaderboard rank | -| `/daily` | Daily reward | -| `/weekly` | Weekly reward | +| `/bal` | Show balance and economy rank | +| `/daily` | Claim money and a possible configured item drop | +| `/weekly` | Claim money and a possible configured item drop | | `/work` | Earn money from a random job | -| `/beg` | Random earning event | +| `/beg` | Trigger a random earning event | | `/search` | Search for money | -| `/rob` | Robbery game mechanic | +| `/rob` | Attempt a robbery mechanic | | `/transfer` | Transfer money to another user | -| `/shop` | Show available items | -| `/buy` | Buy a shop item | -| `/inventory` | Show owned items | -| `/lb` | Show the economy leaderboard | -| `/ping` | Show API/client latency | -| `/help` | Show commands and current prefix | +| `/shop` | View the system market and member shops | +| `/buy` | Buy from the system market or a member listing | +| `/sell` | Publish configured inventory items in the member marketplace | +| `/inventory` | View owned items | +| `/lb` | View the economy leaderboard | +| `/ping` | View bot/client latency | +| `/help` | View commands and current prefix | | `/prefix` | Change the server prefix | -| `/addmoney` | Administrator economy command | -| `/setmoney` | Administrator economy command | +| `/addmoney` | Administrator economy operation | +| `/setmoney` | Administrator economy operation | -Prefix aliases are also supported. Examples: `!bal`, `!bakiye`, `!daily`, `!günlük`, `!shop`, `!mağaza`, `!inventory`, `!envanter`, `!transfer`, `!aktar`. +Prefix aliases remain available, including Turkish aliases such as `!bakiye`, `!günlük`, `!mağaza`, `!envanter`, `!aktar`, `!sat` and `!sıralama` where supported. -## Administration +### Localization -Economy administration commands accept any of the following: +Set `language` to `en` or `tr` in `botConfig.js`. -1. A user ID listed in `admins`. -2. A role ID listed in `adminRoles`. -3. A configured role name in `adminRoleNames`. -4. Discord's **Administrator** permission. -5. Discord's **Manage Server** permission. +The selected language controls command descriptions, responses, embeds, cooldown messages, marketplace wording and help content. Slash-command descriptions are redeployed when the bot starts. -For production servers, role IDs or user IDs are recommended over role names. +### Administration -## Localization +Economy administration commands can be authorized through: -Set `language` in `botConfig.js`: +- A configured admin user ID. +- A configured admin role ID. +- A configured admin role name. +- Discord `Administrator` permission. +- Discord `Manage Server` permission. -```js -language: "en" +Role IDs are preferred because they are stable and unambiguous. + +### Installation + +#### 1. Clone + +```bash +git clone https://github.com/LoiFragola/EconomyBot-Multi-Language.git +cd EconomyBot-Multi-Language ``` -or: +#### 2. Install dependencies -```js -language: "tr" +```bash +npm install ``` -The selected language controls command responses, embeds, help text, cooldown messages, leaderboard labels and slash-command descriptions. +#### 3. Configure + +Edit `botConfig.js` and fill in your token, server ID, language, administrator configuration, market, item registry and optional shared-inventory settings. + +#### 4. Discord Developer Portal -Restart the bot after changing the language so the slash-command definitions are redeployed. +Enable **Message Content Intent** if you want prefix commands. -## Data and Economy Architecture +Invite the application with `bot` and `applications.commands` scopes. -The economy layer uses a small SQLite key-value store with a clear separation between storage and business logic. Economy mutations such as transfers and purchases run inside SQLite transactions, while cooldowns, balances and inventories are scoped by guild and user. +#### 5. Start + +```bash +npm start +``` + +The bot deploys the current slash-command definitions to the configured guild when it starts. + +### Requirements + +| Component | Supported | +|---|---| +| Node.js | `20+` | +| discord.js | `14.27.0` | +| better-sqlite3 | `12.11.1` | -This architecture was influenced by patterns visible in the open-source `casperiv0/ghostybot` project, which stores economy data per guild and user and keeps fields such as money, bank, inventory and reward timestamps together in its data model. EconomyBot does not copy GhostyBot's implementation; it uses the reference as an architectural comparison. +Node.js 22 or 24 is also fully tested by the project's CI matrix. -## Testing +### Testing -Run the full offline test suite: +Run: ```bash npm test ``` -The test suite checks: +The offline integration suite validates the critical domain boundaries, including: -- 17 native slash commands -- Slash command schemas -- Prefix context execution -- Turkish aliases -- English and Turkish localization -- User/permission/role administration checks -- SQLite lifecycle and reopening -- Atomic balance changes -- Transfers and self-transfer protection -- Purchases and inventory persistence -- Cooldowns -- Leaderboard ranking -- Native `interactionCreate` execution path +- Native slash-command definitions and command count +- Prefix aliases and shared command contexts +- English/Turkish localization +- User/role/permission administration +- Persistent SQLite lifecycle +- Atomic money transfers +- Config-driven market stock and stock depletion +- Inventory integrity +- Member listing creation from owned inventory only +- Member marketplace settlement +- Invalid/non-configured item rejection +- Daily and weekly item drops +- Native `interactionCreate` and `messageCreate` paths -GitHub Actions also validates Node.js 20, 22 and 24 and runs CodeQL analysis. +GitHub Actions additionally tests Node.js 20, 22 and 24 and runs CodeQL analysis. -## Security +### Security Never commit a real Discord bot token. If a token is exposed, regenerate it through the Discord Developer Portal. -The SQLite database is stored in the local `data/` directory and should not be committed. +The local SQLite database lives under `data/` and should not be committed. -## Project Structure +### Project Structure ```text -EconomyBot/ -├── commands/ # Native slash + shared command implementations +EconomyBot-Multi-Language/ +├── commands/ # Native slash + prefix command implementations ├── events/ │ ├── clientReady.js # Guild slash deployment │ ├── interactionCreate.js # Native slash execution @@ -215,237 +327,349 @@ EconomyBot/ │ ├── commandUtils.js # Validation, permissions and formatting │ ├── context.js # Unified slash/prefix command context │ ├── database.js # SQLite storage and transaction layer -│ ├── economy.js # Economy business logic +│ ├── economy.js # Economy + marketplace business logic +│ ├── inventory.js # Canonical inventory service │ └── i18n.js # English/Turkish localization -├── data/ # Local SQLite database (generated) +├── data/ # Local/shared SQLite database (generated) ├── counter.js # Optional counting channel ├── botConfig.js # Local configuration -├── index.js # Application bootstrap -├── tests/test.js # Integration-style offline tests +├── index.js # Application bootstrap and validation +├── tests/test.js # Offline integration tests └── package.json ``` -## License & Credits +### Version lineage -This project is based on the open-source `ZeroDiscord/EconomyBot` project and has been substantially modernized for Node.js 20+ and discord.js v14. +This release is derived from the open-source `ZeroDiscord/EconomyBot` project, but it should **not** be considered a drop-in patch or a minor visual update. The runtime architecture and data model have been substantially re-engineered. + +The original repository remains part of the project's lineage; this release is a modernized, independently structured continuation focused on extensibility, transactional integrity, configurability and interoperability. --- -# Türkçe - -## Özellikler - -- Sunucuya özel kalıcı ekonomi bakiyeleri -- SQLite transaction'ları ile atomik para transferi ve mağaza satın alımı -- Kalıcı cooldown'lara sahip günlük, haftalık ve çalışma ödülleri -- Dilenme ve arama etkinlikleri -- Soygun oyun mekaniği -- Kullanıcılar arası para transferi -- Sunucu ekonomi sıralaması ve kullanıcı sırası -- Özelleştirilebilir market -- Ürün miktarlarını gruplayan kalıcı envanter -- Sunucuya özel prefix -- Native `/` slash komutları -- İngilizce ve Türkçe prefix alias'ları -- İsteğe bağlı sayaç kanalı -- Kullanıcı ID, rol ID veya rol adına göre yapılandırılabilen yöneticiler -- `botConfig.js` üzerinden İngilizce/Türkçe yanıt dili -- Bot açılışında otomatik guild slash-command deploy -- Güvenli input doğrulaması, safe integer kontrolleri ve kontrollü interaction hata yönetimi -- Otomatik SQLite klasörü oluşturma -- Veritabanı lifecycle koruması ve açık bağlantının güvenli şekilde yeniden açılması -- Node.js 20/22/24 CI testleri ve CodeQL analizi - -## Gereksinimler +## Türkçe -| Bileşen | Destek | -|---|---| -| Node.js | `20+` | -| discord.js | `14.27.0` | -| better-sqlite3 | `12.11.1` | +> **Gezinme:** [Genel Bakış](#t%C3%BCrk%C3%A7e) · [Bu sürüm neden farklı?](#bu-s%C3%BCr%C3%BCm-neden-farkl%C4%B1) · [Mimari](#mimari) · [Yapılandırma](#yap%C4%B1land%C4%B1rma) · [Market & Oyuncu Mağazaları](#market--oyuncu-ma%C4%9fazalar%C4%B1) · [Envanter & Droplar](#envanter--droplar) · [Ortak Envanter](#ortak-envanter-temeli) · [Komutlar](#komutlar) · [Kurulum](#kurulum) · [Testler](#testler) -Yeni kurulumlar için Node.js 22 veya 24 önerilir. +### Genel Bakış -## Kurulum +EconomyBot artık `ZeroDiscord/EconomyBot` projesinin küçük bir Node.js/discord.js güncellemesi olarak değerlendirilmemelidir. Bu sürüm, **aynı proje soyundan gelen fakat çalışma zamanı, veri modeli ve iş mantığı büyük ölçüde yeniden tasarlanmış yeni bir nesildir**. -### 1. Projeyi indirin +Orijinal proje başlangıç noktası ve tarihsel referans olarak korunurken; SQLite veri katmanı, ekonomi servisleri, inventory modeli, market altyapısı, oyuncu ilan sistemi, komut bağlamı, permission sistemi, localization, hata yönetimi ve startup doğrulama mekanizmaları modern bir mimariye taşındı. -```bash -git clone https://github.com/LoiFragola/EconomyBot.git -cd EconomyBot -``` +Başka bir ifadeyle bu sürüm, eski botun üzerine birkaç özellik eklenmiş hali değil; **aynı temelden yeniden mühendislik geçirilmiş ayrı bir bot sürümü** olarak tasarlanmıştır. -### 2. Bağımlılıkları yükleyin +### Bu sürüm neden farklı? -```bash -npm install +Sistem artık komut dosyalarının içine gömülmüş sabit ürünlerden oluşmuyor. `botConfig.js` merkezi bir domain tanımı olarak kullanılıyor; item registry ve market açılışta doğrulanıyor, market stoğu guild bazında kalıcı tutuluyor, oyuncu ilanları ayrı kayıtlar olarak saklanıyor ve item kimlikleri merkezi bir registry üzerinden yönetiliyor. + +Ekonomi çekirdeği de servis sınırlarına ayrıldı. Para hareketleri ve marketplace ödemeleri SQLite transaction'ları içinde atomik olarak işleniyor; inventory işlemleri tek bir servis altında toplandı; cooldown durumları kalıcı hale getirildi; slash/prefix komutları ortak bir context yapısı üzerinden çalışıyor. + +discord.js v14 uyumluluğu da sadece API isimlerinin değiştirilmesi seviyesinde bırakılmadı. Native interaction akışı, slash schema'ları, alias çözümleme, dil sistemi, permission doğrulaması, hata yönetimi ve CI/CodeQL kontrolleri birlikte yeniden düzenlendi. + +### Mimari + +```text + ┌─────────────────────────┐ + │ Discord Gateway │ + └────────────┬────────────┘ + │ + ┌───────────────────┴───────────────────┐ + │ │ + Slash Etkileşimleri Prefix Mesajları + │ │ + └───────────────────┬───────────────────┘ + │ + Ortak Command Context + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + Economy Service Inventory Service Localization + │ │ │ + └───────────────────┼───────────────────┘ + │ + Transactional SQLite + │ + ┌───────────────────────┼───────────────────────┐ + │ │ │ + Bakiyeler Envanterler Marketplace + │ │ │ + Cooldown'lar Droplar Oyuncu İlanları ``` -### 3. Botu yapılandırın +Envanter ve marketplace katmanları, ileride roleplay botuyla veri paylaşımına uygun olacak şekilde ayrıştırıldı. Böylece gelecekte RP tarafının özel iş mantığını ekonomi botunun içine yığmadan ortak item verisi kullanılabilir. + +Mimari karşılaştırma aşamasında açık kaynak `casperiv0/ghostybot` projesindeki sunucu/kullanıcı ekonomi organizasyonu ve inventory/reward yaklaşımı da referans alındı. Bu proje GhostyBot kodunu kopyalamaz; yalnızca mimari fikirleri karşılaştırma amacıyla kullanır. -`botConfig.js` dosyasını düzenleyin: +### Yapılandırma + +Item ekonomisinin neredeyse tamamı artık `botConfig.js` üzerinden tanımlanabilir. ```js module.exports = { token: "BOT_TOKEN", prefix: "!", serverId: "SUNUCU_ID", - - // "en" = İngilizce, "tr" = Türkçe. language: "tr", - admins: [ - "YETKİLİ_KULLANICI_ID" + admins: ["YETKILI_KULLANICI_ID"], + adminRoles: ["YETKILI_ROL_ID"], + adminRoleNames: [], + + market: [ + { item: "laptop", price: 2000, stock: 10 }, + { item: "mobile", price: 1000, stock: 15 }, + { item: "pc", price: 3000, stock: 5 } ], - adminRoles: [ - "YETKİLİ_ROL_ID" + items: [ + { + id: "laptop", + name: "Laptop", + description: "Taşınabilir bilgisayar.", + dailyDropChance: 0, + weeklyDropChance: 2 + }, + { + id: "mobile", + name: "Mobile", + description: "Kişisel mobil telefon.", + dailyDropChance: 1, + weeklyDropChance: 4 + }, + { + id: "pc", + name: "PC", + description: "Masaüstü bilgisayar.", + dailyDropChance: 0, + weeklyDropChance: 1 + } ], - adminRoleNames: [], + sharedInventory: { + enabled: false, + databasePath: "./data/shared-economy.sqlite" + }, debug: false, countChannel: "SAYAÇ_KANAL_ID" }; ``` -### 4. Discord Developer Portal +`items` **gerçek item registry'sidir**. Bir item burada yoksa komutlar üzerinden yeni item oluşturulamaz. -Prefix komutları için **Message Content Intent** açılmalıdır. +`market` ise **satış kataloğudur**. Her market girdisi `items` içinde tanımlı bir item'a bağlanmalıdır ve başlangıç stoğu sıfırdan büyük olmak zorundadır. Açılış stoğu SQLite içinde guild bazında tutulur; böylece bot yeniden başlatıldığında satılmış ürünler otomatik olarak geri dolmaz. -Bot davetinde `bot` ve `applications.commands` kapsamlarının bulunduğundan emin olun. +### Market & Oyuncu Mağazaları -### 5. Botu başlatın +Eski “Laptop / Mobile / PC kodun içinde sabit” yaklaşımı kaldırıldı ve gerçek bir katalog sistemi kuruldu. -```bash -npm start +Sistem marketi `market` bölümü üzerinden tanımlanır. Fiyat ve ilk stok için komut dosyasına dokunmak gerekmez. + +Her üye marketplace'e katılabilir. Üye kendi envanterindeki bir item'ı `/sell` komutuyla satış ilanına dönüştürebilir. + +Bir oyuncu ilanı şunları taşır: + +- Config içindeki canonical item kimliği +- Satıcı ID'si +- Satış miktarı +- Birim fiyat +- Oluşturulma zamanı +- Benzersiz ilan ID'si + +Alıcı `/buy ` kullanarak ilan üzerinden satın alabilir. İşlem tek SQLite transaction'ında tamamlanır: alıcının bakiyesi kontrol edilir, satıcının bakiyesi artırılır, ilan miktarı azaltılır veya ilan silinir ve item alıcının envanterine aktarılır. + +Dolayısıyla oyuncunun kendi mağazasını açması için yöneticiye ihtiyaç yoktur. İlanın kendisi **oyuncu mağazasının bir satış slotu** olarak davranır; `/shop` ise sistem marketi ile oyuncu mağazalarını tek görünümde birleştirir. + +Özellikle yeni item üretme konusu güvenli biçimde sınırlandırılmıştır. Bir oyuncu yalnızca şu koşulları sağlayan item'ı satabilir: + +1. `items` içinde tanımlı olmalı. +2. Satıcının kendi envanterinde bulunmalı. +3. Pozitif miktar ve pozitif birim fiyat ile listelenmeli. + +### Envanter & Droplar + +Envanter artık alışveriş komutunun yan ürünü değildir; ayrı bir servis olarak yönetilir. + +Item'lar sistem marketinden, oyuncu marketplace alımlarından, günlük ödüllerden ve haftalık ödüllerden envantere girebilir. + +Daily ve weekly komutları ayrıca item drop üretebilir. Her item kendi olasılığını taşır: + +```js +{ + id: "mobile", + name: "Mobile", + description: "Kişisel mobil telefon.", + dailyDropChance: 1, + weeklyDropChance: 4 +} +``` + +Drop değerleri yüzde cinsindendir. Aynı drop türündeki toplam olasılık %100'ü aşamaz. Geri kalan oran item düşmemesi anlamına gelir. + +Böylece ödül ekonomisinin drop tablosu kod değiştirmeden yeniden dengelenebilir. + +### Ortak Envanter Temeli + +Envanter anahtar yapısı `inventory::` biçiminde standartlaştırıldı. Bu sayede gelecekte başka bir bot aynı mantıksal envanter verisini okuyabilecek bir altyapıya sahip olur. + +Bu özellik **tamamen isteğe bağlıdır**. Varsayılan olarak EconomyBot kendi SQLite veritabanını kullanır. + +İleride RP botuyla ortak envanter kullanmak üzere: + +```js +sharedInventory: { + enabled: true, + databasePath: "./data/shared-economy.sqlite" +} ``` -Bot her açılışta `serverId` sunucusundaki güncel slash komutlarını yeniden deploy eder. +Bunu yalnızca iki uygulamanın aynı SQLite dosyasını ve uyumlu item ID'lerini bilinçli şekilde paylaşacağı senaryolarda açın. Böylece RP botu ortak item/ inventory verisini kullanabilir, fakat RP iş mantığı EconomyBot'un içine gömülmek zorunda kalmaz. -## Komutlar +### Komutlar | Komut | Görevi | |---|---| | `/bal` | Bakiye ve ekonomi sırasını gösterir | -| `/daily` | Günlük ödül verir | -| `/weekly` | Haftalık ödül verir | +| `/daily` | Para ve olası yapılandırılmış item drop'u verir | +| `/weekly` | Para ve olası yapılandırılmış item drop'u verir | | `/work` | Rastgele iş ile para kazandırır | -| `/beg` | Rastgele para kazanma etkinliği | +| `/beg` | Rastgele kazanç etkinliği başlatır | | `/search` | Para arar | -| `/rob` | Soygun oyun mekaniği | +| `/rob` | Soygun mekaniğini dener | | `/transfer` | Başka kullanıcıya para gönderir | -| `/shop` | Mağazayı gösterir | -| `/buy` | Mağazadan ürün alır | -| `/inventory` | Envanteri gösterir | +| `/shop` | Sistem marketini ve oyuncu mağazalarını gösterir | +| `/buy` | Sistem marketinden veya oyuncu ilanından satın alır | +| `/sell` | Envanterindeki yapılandırılmış item ile oyuncu marketinde ilan açar | +| `/inventory` | Sahip olunan item'ları gösterir | | `/lb` | Ekonomi sıralamasını gösterir | -| `/ping` | API/istemci gecikmesini gösterir | +| `/ping` | Bot/istemci gecikmesini gösterir | | `/help` | Komutları ve güncel prefix'i gösterir | | `/prefix` | Sunucu prefix'ini değiştirir | -| `/addmoney` | Yönetici ekonomi komutu | -| `/setmoney` | Yönetici ekonomi komutu | +| `/addmoney` | Yönetici ekonomi işlemi | +| `/setmoney` | Yönetici ekonomi işlemi | -Prefix alias'ları da desteklenir. Örnekler: `!bal`, `!bakiye`, `!daily`, `!günlük`, `!shop`, `!mağaza`, `!inventory`, `!envanter`, `!transfer`, `!aktar`. +Prefix alias'ları da korunmuştur. Örneğin `!bakiye`, `!günlük`, `!mağaza`, `!envanter`, `!aktar`, `!sat` ve desteklenen diğer Türkçe alias'lar kullanılabilir. -## Yönetici Sistemi +### Dil Sistemi -Ekonomi yönetim komutları şu yöntemlerden biriyle kullanılabilir: +`botConfig.js` içindeki `language` alanını `en` veya `tr` yapabilirsiniz. -1. `admins` içerisindeki kullanıcı ID'si. -2. `adminRoles` içerisindeki rol ID'si. -3. `adminRoleNames` içerisindeki yapılandırılmış rol adı. -4. Discord **Administrator** yetkisi. -5. Discord **Sunucuyu Yönet / Manage Server** yetkisi. +Seçilen dil; komut açıklamalarını, yanıtları, embed içeriklerini, cooldown mesajlarını, marketplace metinlerini ve help ekranını etkiler. Slash komut açıklamaları bot açılışında yeniden deploy edilir. -Üretim sunucularında rol adı yerine rol ID'si veya kullanıcı ID'si kullanılması önerilir. +### Yönetici Sistemi -## Dil Sistemi +Ekonomi yönetim komutları şu yöntemlerden biriyle yetkilendirilebilir: -`botConfig.js` içindeki `language` alanını değiştirin: +- Yapılandırılmış yönetici kullanıcı ID'si. +- Yapılandırılmış yönetici rol ID'si. +- Yapılandırılmış yönetici rol adı. +- Discord `Administrator` yetkisi. +- Discord `Manage Server / Sunucuyu Yönet` yetkisi. -```js -language: "tr" +Rol ID'si kullanmak en güvenli ve kararlı yöntemdir. + +### Kurulum + +#### 1. Projeyi klonlayın + +```bash +git clone https://github.com/LoiFragola/EconomyBot-Multi-Language.git +cd EconomyBot-Multi-Language ``` -veya: +#### 2. Bağımlılıkları yükleyin -```js -language: "en" +```bash +npm install ``` -Seçilen dil; komut yanıtlarını, embed'leri, yardım metnini, cooldown mesajlarını, sıralama etiketlerini ve slash-komut açıklamalarını etkiler. +#### 3. Yapılandırın -Dil değiştirdikten sonra slash-komut açıklamalarının yeniden deploy edilmesi için botu yeniden başlatın. +`botConfig.js` içinde token, server ID, dil, yönetici ayarları, market, item registry ve isterseniz shared inventory yapılandırmasını doldurun. -## Veri ve Ekonomi Mimarisi +#### 4. Discord Developer Portal -Ekonomi katmanı, depolama ve iş mantığını birbirinden ayıran SQLite tabanlı bir key-value store kullanır. Transfer ve satın alma gibi bakiye değişiklikleri SQLite transaction'ları içinde gerçekleştirilir; cooldown, bakiye ve envanter verileri sunucu + kullanıcı bazında tutulur. +Prefix komutları kullanılacaksa **Message Content Intent** özelliğini açın. -Mimari karşılaştırmada açık kaynak `casperiv0/ghostybot` projesindeki sunucu + kullanıcı ekonomi modeli referans alınmıştır. GhostyBot veri modelinde para, banka, envanter ve ödül zamanları gibi alanları kullanıcı/sunucu bağlamında tutuyor. EconomyBot GhostyBot kodunu kopyalamaz; yalnızca mimari yaklaşımı karşılaştırma amacıyla kullanır. +Botu `bot` ve `applications.commands` kapsamları ile davet edin. -## Testler +#### 5. Başlatın -Tam offline test paketini çalıştırın: +```bash +npm start +``` + +Bot açılışta güncel slash komutlarını yapılandırılmış sunucuya deploy eder. + +### Gereksinimler + +| Bileşen | Destek | +|---|---| +| Node.js | `20+` | +| discord.js | `14.27.0` | +| better-sqlite3 | `12.11.1` | + +Projenin CI matrisi Node.js 20, 22 ve 24 sürümlerini test eder. + +### Testler ```bash npm test ``` -Testler şunları kontrol eder: +Offline integration testleri özellikle şu alanları doğrular: -- 17 native slash komutu -- Slash command şemaları -- Prefix context yürütmesi -- Türkçe alias'lar -- İngilizce ve Türkçe dil sistemi -- Kullanıcı/yetki/rol yönetici kontrolleri -- SQLite lifecycle ve yeniden açılma -- Atomik bakiye değişiklikleri -- Transfer ve kendine transfer koruması -- Satın alma ve envanter kalıcılığı -- Cooldown'lar -- Ekonomi sıralaması -- Native `interactionCreate` akışı +- Native slash komutları ve komut sayısı +- Prefix alias'ları ve ortak command context +- İngilizce/Türkçe dil sistemi +- Kullanıcı/rol/yetki bazlı yönetici kontrolleri +- Kalıcı SQLite lifecycle +- Atomik para transferleri +- Config tabanlı market stoğu ve stok tükenmesi +- Envanter bütünlüğü +- Yalnızca kendi inventory'sindeki item ile ilan açma +- Oyuncu marketplace settlement işlemleri +- Config dışı item reddi +- Daily/weekly item dropları +- Gerçek `interactionCreate` ve `messageCreate` akışları -GitHub Actions ayrıca Node.js 20, 22 ve 24 üzerinde testleri ve CodeQL analizini çalıştırır. +GitHub Actions ayrıca Node.js 20, 22 ve 24 testlerini ve CodeQL analizini çalıştırır. -## Güvenlik +### Güvenlik Gerçek Discord bot token'ınızı GitHub'a commit etmeyin. Token açığa çıktıysa Discord Developer Portal üzerinden yenileyin. -SQLite veritabanı yerel `data/` klasöründe tutulur ve Git'e gönderilmemelidir. +Yerel SQLite veritabanı `data/` altında tutulur ve Git'e gönderilmemelidir. -## Proje Yapısı +### Proje Yapısı ```text -EconomyBot/ -├── commands/ # Native slash + ortak komut implementasyonları +EconomyBot-Multi-Language/ +├── commands/ # Native slash + prefix komutları ├── events/ │ ├── clientReady.js # Guild slash deploy │ ├── interactionCreate.js # Native slash yürütmesi │ └── messageCreate.js # Prefix yürütmesi ├── lib/ │ ├── commandUtils.js # Doğrulama, yetki ve formatlama -│ ├── context.js # Slash/prefix ortak context -│ ├── database.js # SQLite depolama ve transaction katmanı -│ ├── economy.js # Ekonomi iş mantığı +│ ├── context.js # Ortak slash/prefix command context +│ ├── database.js # SQLite storage + transaction katmanı +│ ├── economy.js # Economy + marketplace iş mantığı +│ ├── inventory.js # Canonical inventory servisi │ └── i18n.js # İngilizce/Türkçe localization -├── data/ # Yerel SQLite veritabanı (otomatik oluşturulur) -├── counter.js # İsteğe bağlı sayaç kanalı -├── botConfig.js # Yerel ayarlar -├── index.js # Uygulama başlangıcı -├── tests/test.js # Offline entegrasyon testleri +├── data/ # Local/shared SQLite DB (oluşturulur) +├── counter.js # İsteğe bağlı sayaç sistemi +├── botConfig.js # Yerel yapılandırma +├── index.js # Bootstrap + config doğrulama +├── tests/test.js # Offline integration testleri └── package.json ``` -## Lisans ve Kaynak +### Sürüm ilişkisi -Bu proje açık kaynak `ZeroDiscord/EconomyBot` projesi temel alınarak Node.js 20+ ve discord.js v14 için kapsamlı şekilde modernize edilmiştir. +Bu sürüm açık kaynak `ZeroDiscord/EconomyBot` projesinden türetilmiştir; fakat **drop-in bir patch veya küçük bir görsel güncelleme olarak değerlendirilmemelidir**. Çalışma zamanı mimarisi ve veri modeli büyük ölçüde yeniden mühendislikten geçirilmiştir. ---- - -
+Orijinal repo projenin soyunun bir parçasıdır; bu sürüm ise genişletilebilirlik, transaction bütünlüğü, yapılandırılabilirlik ve gelecekteki botlar arası veri paylaşımı odağında bağımsız bir devam sürümüdür. -### INS Development +> **Discord sunucusu:** README içindeki `https://discord.gg/YOUR_INVITE` bağlantısını kendi gerçek sunucu davet bağlantınızla değiştirin. -
diff --git a/botConfig.js b/botConfig.js index 5465c2a..c9b63eb 100644 --- a/botConfig.js +++ b/botConfig.js @@ -19,6 +19,47 @@ module.exports = { // Optional role names. Use role IDs above for the safest configuration. adminRoleNames: [], + // Initial stock and public market configuration. + // Stock is persisted by SQLite after the first initialization. + market: [ + { item: "laptop", price: 2000, stock: 10 }, + { item: "mobile", price: 1000, stock: 15 }, + { item: "pc", price: 3000, stock: 5 } + ], + + // Canonical item registry. New item types must be defined here. + // Drop chances are percentages from 0 to 100. + items: [ + { + id: "laptop", + name: "Laptop", + description: "A portable computer.", + dailyDropChance: 0, + weeklyDropChance: 2 + }, + { + id: "mobile", + name: "Mobile", + description: "A personal mobile phone.", + dailyDropChance: 1, + weeklyDropChance: 4 + }, + { + id: "pc", + name: "PC", + description: "A desktop computer.", + dailyDropChance: 0, + weeklyDropChance: 1 + } + ], + + // Optional shared-inventory groundwork for a future roleplay bot. + // Keep disabled unless both applications intentionally point at the same SQLite file. + sharedInventory: { + enabled: false, + databasePath: "./data/shared-economy.sqlite" + }, + debug: false, countChannel: "YOUR_COUNT_CHANNEL_ID" }; diff --git a/commands/buy.js b/commands/buy.js index aa3655e..4f3e292 100644 --- a/commands/buy.js +++ b/commands/buy.js @@ -1,26 +1,40 @@ const { SlashCommandBuilder } = require("discord.js"); -const { getProduct, formatMoney } = require("../lib/commandUtils"); +const { getProduct, formatMoney, parsePositiveInteger } = require("../lib/commandUtils"); const { commandDescription } = require("../lib/i18n"); +const config = require("../botConfig"); exports.data = new SlashCommandBuilder() .setName("buy") - .setDescription(commandDescription("buy", require("../botConfig").language)) - .addStringOption((option) => option.setName("urun").setDescription("Item to buy.").setRequired(true).addChoices( - { name: "Laptop", value: "laptop" }, - { name: "Mobile", value: "mobile" }, - { name: "PC", value: "pc" } - )); + .setDescription(commandDescription("buy", config.language)) + .addStringOption((option) => option.setName("urun").setDescription("Item or member listing ID.").setRequired(true)) + .addIntegerOption((option) => option.setName("miktar").setDescription("Quantity.").setMinValue(1).setRequired(false)); exports.name = "buy"; exports.aliases = ["satınal", "satinal", "al"]; exports.execute = async (ctx) => { const requested = String(getProduct(ctx) || "").trim().toLowerCase(); - const item = ctx.client.shop[requested]; - if (!item) return ctx.reply(ctx.t("errors.invalidProduct")); - const balance = ctx.client.eco.getBalance(ctx.guildId, ctx.userId); - if (balance < item.cost) return ctx.reply(ctx.t("economy.purchaseNeed", { price: formatMoney(item.cost, ctx.language), balance: formatMoney(balance, ctx.language) })); - const result = ctx.client.eco.purchase(ctx.guildId, ctx.userId, item); - if (result.error) return ctx.reply(ctx.t("errors.insufficient")); - return ctx.reply(ctx.t("economy.purchase", { item: item.name, price: formatMoney(item.cost, ctx.language), balance: formatMoney(result.after, ctx.language) })); + const quantity = ctx.isSlash ? (ctx.integerOption("miktar") || 1) : (parsePositiveInteger(ctx.args[1]) || 1); + if (!Number.isSafeInteger(quantity) || quantity <= 0) return ctx.reply(ctx.t("errors.noAmount")); + + const marketItem = ctx.client.eco.getMarketItem(ctx.guildId, requested); + if (marketItem) { + const balance = ctx.client.eco.getBalance(ctx.guildId, ctx.userId); + const total = marketItem.cost * quantity; + if (marketItem.stock < quantity) return ctx.reply(ctx.t("economy.purchaseStock", { stock: marketItem.stock })); + if (!Number.isSafeInteger(total) || balance < total) return ctx.reply(ctx.t("economy.purchaseNeed", { price: formatMoney(total, ctx.language), balance: formatMoney(balance, ctx.language) })); + const result = ctx.client.eco.purchase(ctx.guildId, ctx.userId, marketItem, quantity); + if (result.error === "Yetersiz stok.") return ctx.reply(ctx.t("economy.purchaseStock", { stock: result.stock })); + if (result.error) return ctx.reply(ctx.t("errors.insufficient")); + return ctx.reply(ctx.t("economy.purchase", { item: marketItem.name, price: formatMoney(result.total, ctx.language), balance: formatMoney(result.after, ctx.language), quantity })); + } + + const listing = ctx.client.eco.getListing(ctx.guildId, requested); + if (!listing) return ctx.reply(ctx.t("errors.invalidProduct")); + const result = ctx.client.eco.buyListing(ctx.guildId, ctx.userId, requested, quantity); + if (result.error === "Kendi ilanını satın alamazsın.") return ctx.reply(ctx.t("errors.ownListing")); + if (result.error === "İlanda yeterli stok yok.") return ctx.reply(ctx.t("economy.listingStock", { stock: result.available })); + if (result.error === "Yetersiz bakiye.") return ctx.reply(ctx.t("economy.purchaseNeed", { price: formatMoney(result.total, ctx.language), balance: formatMoney(result.balance, ctx.language) })); + if (result.error) return ctx.reply(ctx.t("errors.invalidProduct")); + return ctx.reply(ctx.t("economy.listingPurchase", { item: listing.itemName, quantity, price: formatMoney(result.total, ctx.language), balance: formatMoney(result.after, ctx.language) })); }; -exports.help = { name: exports.name, aliases: exports.aliases, usage: "buy " }; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "buy [quantity]" }; diff --git a/commands/daily.js b/commands/daily.js index c5ae43f..7b95b89 100644 --- a/commands/daily.js +++ b/commands/daily.js @@ -1,14 +1,16 @@ const { SlashCommandBuilder } = require("discord.js"); const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); const { commandDescription } = require("../lib/i18n"); +const config = require("../botConfig"); -exports.data = new SlashCommandBuilder().setName("daily").setDescription(commandDescription("daily", require("../botConfig").language)); +exports.data = new SlashCommandBuilder().setName("daily").setDescription(commandDescription("daily", config.language)); exports.name = "daily"; exports.aliases = ["günlük", "gunluk"]; exports.execute = async (ctx) => { const result = ctx.client.eco.daily(ctx.guildId, ctx.userId, randomInt(100, 599)); if (result.onCooldown) return ctx.reply(ctx.t("economy.dailyCooldown", { time: formatRemaining(result.remainingMs, ctx.language) })); - return ctx.reply(ctx.t("economy.daily", { amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) })); + const drop = result.droppedItem ? ` ${ctx.t("economy.itemDrop", { item: result.droppedItem.name })}` : ""; + return ctx.reply(ctx.t("economy.daily", { amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) }) + drop); }; exports.help = { name: exports.name, aliases: exports.aliases, usage: "daily" }; diff --git a/commands/inventory.js b/commands/inventory.js index 14555b7..d3087ae 100644 --- a/commands/inventory.js +++ b/commands/inventory.js @@ -1,8 +1,9 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { formatMoney } = require("../lib/commandUtils"); const { commandDescription } = require("../lib/i18n"); +const config = require("../botConfig"); -exports.data = new SlashCommandBuilder().setName("inventory").setDescription(commandDescription("inventory", require("../botConfig").language)); +exports.data = new SlashCommandBuilder().setName("inventory").setDescription(commandDescription("inventory", config.language)); exports.name = "inventory"; exports.aliases = ["inv", "envanter", "eşyalar", "esya"]; @@ -10,15 +11,19 @@ exports.execute = async (ctx) => { const items = ctx.client.eco.getInventory(ctx.guildId, ctx.userId); if (items.length === 0) return ctx.reply(ctx.t("economy.inventoryEmpty")); const grouped = new Map(); - for (const item of items) { - const name = String(item.name || item.id || (ctx.language === "tr" ? "Bilinmeyen ürün" : "Unknown item")); - const current = grouped.get(name) || { count: 0, price: Number(item.price) || 0 }; + for (const owned of items) { + const item = ctx.client.inventory.getItem(owned.id); + const id = String(owned.id || "unknown"); + const current = grouped.get(id) || { name: item?.name || owned.name || id, count: 0, description: item?.description || owned.description || "", price: 0 }; current.count += 1; - grouped.set(name, current); + const marketItem = ctx.client.eco.getMarketItem(ctx.guildId, id); + current.price = marketItem?.cost || 0; + grouped.set(id, current); } const embed = new EmbedBuilder().setTitle(ctx.t("economy.inventoryTitle", { user: ctx.user.username })).setColor("Blurple").setThumbnail(ctx.user.displayAvatarURL()).setTimestamp(); - for (const [name, info] of grouped) { - embed.addFields({ name, value: `${ctx.language === "tr" ? "Miktar" : "Quantity"}: **${info.count}**\n${ctx.language === "tr" ? "Birim fiyat" : "Unit price"}: **${formatMoney(info.price, ctx.language)}**`, inline: true }); + for (const [id, info] of grouped) { + const value = `${ctx.language === "tr" ? "ID" : "ID"}: \`${id}\`\n${ctx.language === "tr" ? "Miktar" : "Quantity"}: **${info.count}**${info.price ? `\n${ctx.language === "tr" ? "Market değeri" : "Market value"}: **${formatMoney(info.price, ctx.language)}**` : ""}`; + embed.addFields({ name: info.name, value: value.slice(0, 1024), inline: true }); } return ctx.reply({ embeds: [embed] }); }; diff --git a/commands/sell.js b/commands/sell.js new file mode 100644 index 0000000..e9c4f49 --- /dev/null +++ b/commands/sell.js @@ -0,0 +1,33 @@ +const { SlashCommandBuilder } = require("discord.js"); +const { parsePositiveInteger } = require("../lib/commandUtils"); +const { commandDescription } = require("../lib/i18n"); +const config = require("../botConfig"); + +exports.data = new SlashCommandBuilder() + .setName("sell") + .setDescription(commandDescription("sell", config.language)) + .addStringOption((option) => option.setName("urun").setDescription("Configured item ID.").setRequired(true)) + .addIntegerOption((option) => option.setName("miktar").setDescription("Quantity to list.").setMinValue(1).setRequired(true)) + .addIntegerOption((option) => option.setName("fiyat").setDescription("Price per unit.").setMinValue(1).setRequired(true)); +exports.name = "sell"; +exports.aliases = ["sat", "ilan", "listele"]; + +exports.execute = async (ctx) => { + const itemId = String(ctx.isSlash ? ctx.stringOption("urun") : ctx.args[0] || "").trim().toLowerCase(); + const quantity = ctx.isSlash ? ctx.integerOption("miktar") : parsePositiveInteger(ctx.args[1]); + const unitPrice = ctx.isSlash ? ctx.integerOption("fiyat") : parsePositiveInteger(ctx.args[2]); + if (!itemId || !quantity || !unitPrice) return ctx.reply(ctx.t("errors.invalidListing")); + + const result = ctx.client.eco.createListing(ctx.guildId, ctx.userId, itemId, quantity, unitPrice); + if (result.error === "Bilinmeyen item.") return ctx.reply(ctx.t("errors.invalidItem")); + if (result.error === "Envanterinde bu üründen yeterli miktar yok.") return ctx.reply(ctx.t("errors.notEnoughItems", { available: result.available })); + if (result.error) return ctx.reply(ctx.t("errors.invalidListing")); + + return ctx.reply(ctx.t("economy.listingCreated", { + item: result.listing.itemName, + quantity: result.listing.quantity, + price: result.listing.unitPrice, + listing: result.listing.id + })); +}; +exports.help = { name: exports.name, aliases: exports.aliases, usage: "sell " }; diff --git a/commands/shop.js b/commands/shop.js index 4d905bc..3c62a9a 100644 --- a/commands/shop.js +++ b/commands/shop.js @@ -1,17 +1,35 @@ const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); const { formatMoney } = require("../lib/commandUtils"); const { commandDescription } = require("../lib/i18n"); +const config = require("../botConfig"); -exports.data = new SlashCommandBuilder().setName("shop").setDescription(commandDescription("shop", require("../botConfig").language)); +exports.data = new SlashCommandBuilder().setName("shop").setDescription(commandDescription("shop", config.language)); exports.name = "shop"; exports.aliases = ["mağaza", "magaza", "market"]; exports.execute = async (ctx) => { - const entries = Object.values(ctx.client.shop); - const desc = ctx.language === "tr" ? "Satın almak istediğin ürünü `/buy` ile seçebilirsin." : "Choose an item with `/buy` to purchase it."; - const commandLabel = ctx.language === "tr" ? "Komut" : "Command"; - const embed = new EmbedBuilder().setTitle(ctx.t("economy.shopTitle")).setDescription(desc).setColor("Blurple").setTimestamp(); - embed.addFields(entries.map((item) => ({ name: item.name, value: `${ctx.t("economy.shopItem", { price: formatMoney(item.cost, ctx.language) })}\n${commandLabel}: \`/buy ${item.id}\``, inline: true }))); + const market = ctx.client.eco.getMarket(ctx.guildId); + const listings = ctx.client.eco.getListings(ctx.guildId); + const isTr = ctx.language === "tr"; + const embed = new EmbedBuilder() + .setTitle(ctx.t("economy.shopTitle")) + .setDescription(isTr ? "Sistem marketi ve oyuncuların açtığı satış ilanları burada. Kendi envanterindeki item'ları `/sell` ile mağazaya çıkarabilirsin." : "The system market and member-created listings are shown here. List items from your inventory with `/sell` to open your own shop listing.") + .setColor("Blurple") + .setTimestamp(); + + if (market.length) { + const marketText = market.map((item) => `**${item.name}** — ${formatMoney(item.cost, ctx.language)} • ${isTr ? "Stok" : "Stock"}: **${item.stock}** • \`/buy ${item.id}\``).join("\n"); + embed.addFields({ name: isTr ? "Sistem Marketi" : "System Market", value: marketText.slice(0, 1024) }); + } + + if (listings.length) { + const lines = listings.slice(0, 20).map((listing) => `**${listing.itemName}** ×${listing.quantity} — ${formatMoney(listing.unitPrice, ctx.language)}/${isTr ? "adet" : "unit"} • ${isTr ? "Satıcı" : "Seller"}: <@${listing.sellerId}> • ID: \`${listing.id}\``); + embed.addFields({ name: isTr ? "Oyuncu Mağazaları" : "Member Shops", value: lines.join("\n").slice(0, 1024) }); + } else { + embed.addFields({ name: isTr ? "Oyuncu Mağazaları" : "Member Shops", value: isTr ? "Henüz oyuncu satış ilanı bulunmuyor." : "There are no member listings yet." }); + } + + embed.setFooter({ text: isTr ? "Bir ilandaki ürünü almak için /buy kullan." : "Use /buy to purchase from a member listing." }); return ctx.reply({ embeds: [embed] }); }; exports.help = { name: exports.name, aliases: exports.aliases, usage: "shop" }; diff --git a/commands/weekly.js b/commands/weekly.js index 3b6c111..2ff9283 100644 --- a/commands/weekly.js +++ b/commands/weekly.js @@ -1,14 +1,16 @@ const { SlashCommandBuilder } = require("discord.js"); const { formatMoney, formatRemaining, randomInt } = require("../lib/commandUtils"); const { commandDescription } = require("../lib/i18n"); +const config = require("../botConfig"); -exports.data = new SlashCommandBuilder().setName("weekly").setDescription(commandDescription("weekly", require("../botConfig").language)); +exports.data = new SlashCommandBuilder().setName("weekly").setDescription(commandDescription("weekly", config.language)); exports.name = "weekly"; exports.aliases = ["haftalık", "haftalik"]; exports.execute = async (ctx) => { const result = ctx.client.eco.weekly(ctx.guildId, ctx.userId, randomInt(750, 1999)); if (result.onCooldown) return ctx.reply(ctx.t("economy.weeklyCooldown", { time: formatRemaining(result.remainingMs, ctx.language) })); - return ctx.reply(ctx.t("economy.weekly", { amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) })); + const drop = result.droppedItem ? ` ${ctx.t("economy.itemDrop", { item: result.droppedItem.name })}` : ""; + return ctx.reply(ctx.t("economy.weekly", { amount: formatMoney(result.amount, ctx.language), balance: formatMoney(result.after, ctx.language) }) + drop); }; exports.help = { name: exports.name, aliases: exports.aliases, usage: "weekly" }; diff --git a/index.js b/index.js index 329a66a..6829511 100644 --- a/index.js +++ b/index.js @@ -2,20 +2,78 @@ const path = require("node:path"); const fs = require("node:fs"); const { Client, Collection, GatewayIntentBits, Partials } = require("discord.js"); const config = require("./botConfig"); -const { KeyValueStore } = require("./lib/database"); +const { KeyValueStore, defaultPath } = require("./lib/database"); const EconomyManager = require("./lib/economy"); +const InventoryManager = require("./lib/inventory"); const { normalizeLanguage } = require("./lib/i18n"); +function assertDiscordId(value, label) { + if (!/^\d{17,20}$/.test(String(value || ""))) throw new Error(`${label} geçerli bir Discord ID'si olmalı.`); +} + +function validateItemRegistry(items) { + if (!Array.isArray(items) || items.length === 0) throw new Error("botConfig.js içindeki items en az bir item içermeli."); + const ids = new Set(); + let dailyChance = 0; + let weeklyChance = 0; + for (const item of items) { + if (!item || typeof item !== "object") throw new TypeError("items içindeki her kayıt bir obje olmalı."); + const id = String(item.id || "").trim().toLowerCase(); + if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(id)) throw new Error(`Geçersiz item ID'si: ${item.id}`); + if (ids.has(id)) throw new Error(`Yinelenen item ID'si: ${id}`); + if (!String(item.name || "").trim()) throw new Error(`${id}: item name alanı boş olamaz.`); + const daily = Number(item.dailyDropChance ?? 0); + const weekly = Number(item.weeklyDropChance ?? 0); + if (!Number.isFinite(daily) || daily < 0 || daily > 100) throw new Error(`${id}: dailyDropChance 0-100 arasında olmalı.`); + if (!Number.isFinite(weekly) || weekly < 0 || weekly > 100) throw new Error(`${id}: weeklyDropChance 0-100 arasında olmalı.`); + ids.add(id); + dailyChance += daily; + weeklyChance += weekly; + item.id = id; + item.name = String(item.name).trim(); + item.description = String(item.description || "").trim(); + item.dailyDropChance = daily; + item.weeklyDropChance = weekly; + } + if (dailyChance > 100 || weeklyChance > 100) throw new Error("Günlük veya haftalık item drop olasılıklarının toplamı %100'ü aşamaz."); + return ids; +} + +function validateMarket(market, itemIds) { + if (!Array.isArray(market) || market.length === 0) throw new Error("botConfig.js içindeki market en az bir ürün içermeli."); + const seen = new Set(); + for (const entry of market) { + if (!entry || typeof entry !== "object") throw new TypeError("market içindeki her kayıt bir obje olmalı."); + const itemId = String(entry.item || "").trim().toLowerCase(); + if (!itemIds.has(itemId)) throw new Error(`Market ürünü '${itemId}' items bölümünde tanımlı değil.`); + if (seen.has(itemId)) throw new Error(`Market içinde '${itemId}' birden fazla tanımlanamaz.`); + const price = Number(entry.price); + const stock = Number(entry.stock); + if (!Number.isSafeInteger(price) || price < 0) throw new Error(`${itemId}: market price güvenli ve negatif olmayan bir tam sayı olmalı.`); + if (!Number.isSafeInteger(stock) || stock <= 0) throw new Error(`${itemId}: market stock başlangıçta en az 1 olmalı.`); + entry.item = itemId; + entry.price = price; + entry.stock = stock; + seen.add(itemId); + } +} + if (!config || typeof config !== "object") throw new Error("botConfig.js geçerli bir yapılandırma döndürmelidir."); if (!config.token || config.token === "YOUR_TOKEN") throw new Error("botConfig.js içindeki token ayarlanmalı."); -if (!config.serverId || !/^\d{17,20}$/.test(String(config.serverId))) throw new Error("botConfig.js içindeki serverId geçerli bir Discord sunucu ID'si olmalı."); +assertDiscordId(config.serverId, "serverId"); if (!config.prefix || typeof config.prefix !== "string" || !/^\S{1,5}$/.test(config.prefix)) throw new Error("botConfig.js içindeki prefix 1-5 karakter ve boşluksuz olmalı."); config.admins = Array.isArray(config.admins) ? config.admins.map(String) : []; config.adminRoles = Array.isArray(config.adminRoles) ? config.adminRoles.map(String) : []; -config.adminRoleNames = Array.isArray(config.adminRoleNames) ? config.adminRoleNames.map(String) : []; +config.adminRoleNames = Array.isArray(config.adminRoleNames) ? config.adminRoleNames.map((name) => String(name).trim()) : []; config.language = normalizeLanguage(config.language); +const itemIds = validateItemRegistry(config.items); +validateMarket(config.market, itemIds); +config.sharedInventory = config.sharedInventory && typeof config.sharedInventory === "object" ? config.sharedInventory : { enabled: false, databasePath: defaultPath }; +config.sharedInventory.enabled = Boolean(config.sharedInventory.enabled); +config.sharedInventory.databasePath = path.resolve(String(config.sharedInventory.databasePath || defaultPath)); +if (config.sharedInventory.enabled) fs.mkdirSync(path.dirname(config.sharedInventory.databasePath), { recursive: true }); -const database = new KeyValueStore(); +const database = new KeyValueStore(config.sharedInventory.enabled ? config.sharedInventory.databasePath : defaultPath); const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], partials: [Partials.Channel] @@ -23,14 +81,12 @@ const client = new Client({ client.config = config; client.db = database; -client.eco = new EconomyManager(database); +client.inventory = new InventoryManager(database, config.items); +client.eco = new EconomyManager(database, client.inventory, config.items, config.market); client.commands = new Collection(); client.aliases = new Collection(); -client.shop = Object.freeze({ - laptop: { id: "laptop", name: "Laptop", cost: 2000 }, - mobile: { id: "mobile", name: "Mobile", cost: 1000 }, - pc: { id: "pc", name: "PC", cost: 3000 } -}); +client.shop = client.eco.getMarketCatalog(); +client.items = client.inventory.getItemRegistry(); const commandsPath = path.join(__dirname, "commands"); const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith(".js")).sort(); @@ -41,14 +97,13 @@ for (const file of commandFiles) { if (command.help?.name !== name) throw new Error(`commands/${file}: help.name ve name eşleşmiyor.`); if (client.commands.has(name)) throw new Error(`Yinelenen komut adı: ${name}`); client.commands.set(name, command); - for (const alias of command.aliases || []) { const normalized = String(alias).trim().toLowerCase(); if (!normalized || normalized === name || client.commands.has(normalized) || client.aliases.has(normalized)) throw new Error(`Geçersiz veya çakışan takma ad: ${alias}`); client.aliases.set(normalized, name); } } -if (client.commands.size !== 17) throw new Error(`17 komut bekleniyordu, ${client.commands.size} komut yüklendi.`); +if (client.commands.size !== 18) throw new Error(`18 komut bekleniyordu, ${client.commands.size} komut yüklendi.`); client.once("clientReady", require("./events/clientReady").bind(null, client)); client.on("interactionCreate", require("./events/interactionCreate").bind(null, client)); diff --git a/lib/context.js b/lib/context.js index 6370964..ac523b2 100644 --- a/lib/context.js +++ b/lib/context.js @@ -29,6 +29,9 @@ function createInteractionContext(interaction, client) { amount(name = "miktar") { return interaction.options.getInteger(name); }, + integerOption(name) { + return interaction.options.getInteger(name); + }, stringOption(name) { return interaction.options.getString(name); }, @@ -69,6 +72,12 @@ function createPrefixContext(message, client, args) { const number = Number(value); return Number.isSafeInteger(number) && number > 0 ? number : null; }, + integerOption(_name, argumentIndex = 1) { + const value = args[argumentIndex]; + if (!/^\d+$/.test(String(value ?? ""))) return null; + const number = Number(value); + return Number.isSafeInteger(number) && number > 0 ? number : null; + }, stringOption(_name, argumentIndex = 0) { return args[argumentIndex] ?? null; }, diff --git a/lib/economy.js b/lib/economy.js index 13b41a7..bd6392c 100644 --- a/lib/economy.js +++ b/lib/economy.js @@ -2,9 +2,12 @@ const DAY = 86_400_000; const WEEK = 7 * DAY; class EconomyManager { - constructor(database) { - if (!database) throw new TypeError("EconomyManager bir veritabanı örneği gerektirir."); + constructor(database, inventory, items, market) { + if (!database || !inventory) throw new TypeError("EconomyManager veritabanı ve inventory servisi gerektirir."); this.db = database; + this.inventory = inventory; + this.items = new Map((items || []).map((item) => [item.id, item])); + this.marketConfig = Array.isArray(market) ? market.map((entry) => ({ ...entry })) : []; } assertId(value, label = "ID") { @@ -28,9 +31,22 @@ class EconomyManager { } inventoryKey(guildId, userId) { + return this.inventory.key(guildId, userId); + } + + marketKey(guildId, itemId) { this.assertId(guildId, "sunucu ID'si"); - this.assertId(userId, "kullanıcı ID'si"); - return `inventory:${guildId}:${userId}`; + return `market:${guildId}:${itemId}`; + } + + listingKey(guildId, listingId) { + this.assertId(guildId, "sunucu ID'si"); + return `listing:${guildId}:${listingId}`; + } + + listingPrefix(guildId) { + this.assertId(guildId, "sunucu ID'si"); + return `listing:${guildId}:`; } getBalance(guildId, userId) { @@ -91,25 +107,131 @@ class EconomyManager { }); } - purchase(guildId, userId, item) { - if (!item || typeof item.id !== "string" || typeof item.name !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) throw new TypeError("Geçersiz mağaza ürünü."); + getItem(itemId) { + return this.items.get(String(itemId || "").trim().toLowerCase()) || null; + } + + getMarketCatalog() { + return Object.fromEntries(this.marketConfig.map((entry) => { + const item = this.getItem(entry.item); + return [entry.item, { id: entry.item, name: item.name, description: item.description, cost: entry.price, stock: entry.stock }]; + })); + } + + initializeMarket(guildId) { + this.assertId(guildId, "sunucu ID'si"); + for (const entry of this.marketConfig) { + const key = this.marketKey(guildId, entry.item); + if (!this.db.has(key)) this.db.set(key, { item: entry.item, price: entry.price, stock: entry.stock }); + } + } + + getMarket(guildId) { + this.initializeMarket(guildId); + return this.marketConfig.map((entry) => { + const item = this.getItem(entry.item); + const state = this.db.get(this.marketKey(guildId, entry.item), { price: entry.price, stock: entry.stock }); + return { ...item, id: entry.item, cost: Number(state.price), stock: Number(state.stock) }; + }); + } + + getMarketItem(guildId, itemId) { + this.initializeMarket(guildId); + const id = String(itemId || "").trim().toLowerCase(); + const configured = this.marketConfig.find((entry) => entry.item === id); + if (!configured) return null; + const item = this.getItem(id); + const state = this.db.get(this.marketKey(guildId, id), { price: configured.price, stock: configured.stock }); + return { ...item, id, cost: Number(state.price), stock: Number(state.stock) }; + } + + purchase(guildId, userId, item, quantity = 1) { + this.assertAmount(quantity); + if (!item || typeof item.id !== "string" || !Number.isSafeInteger(item.cost) || item.cost < 0) throw new TypeError("Geçersiz mağaza ürünü."); + const total = item.cost * quantity; + if (!Number.isSafeInteger(total)) throw new RangeError("Satın alma toplamı güvenli sayı sınırını aşıyor."); return this.db.transaction(() => { + const marketItem = this.getMarketItem(guildId, item.id); + if (!marketItem) return { error: "Ürün markette bulunmuyor." }; + if (marketItem.stock < quantity) return { error: "Yetersiz stok.", stock: marketItem.stock }; const balance = this.getBalance(guildId, userId); - if (balance < item.cost) return { error: "Yetersiz bakiye.", after: balance }; - const key = this.inventoryKey(guildId, userId); - const inventory = this.db.get(key, []); - if (!Array.isArray(inventory)) throw new TypeError("Envanter verisi bozuk."); - const after = balance - item.cost; - inventory.push({ id: item.id, name: item.name, price: item.cost, purchasedAt: Date.now() }); - this.db.set(this.moneyKey(guildId, userId), after); - this.db.set(key, inventory); - return { error: null, after, item }; + if (balance < total) return { error: "Yetersiz bakiye.", after: balance }; + const state = this.db.get(this.marketKey(guildId, item.id)); + state.stock -= quantity; + this.db.set(this.marketKey(guildId, item.id), state); + this.inventory.add(guildId, userId, item.id, quantity, { source: "market" }); + this.db.set(this.moneyKey(guildId, userId), balance - total); + return { error: null, after: balance - total, item: marketItem, quantity, total }; + }); + } + + createListing(guildId, sellerId, itemId, quantity, unitPrice) { + this.assertId(guildId, "sunucu ID'si"); + this.assertId(sellerId, "satıcı ID'si"); + this.assertAmount(quantity); + this.assertAmount(unitPrice); + const item = this.getItem(itemId); + if (!item) return { error: "Bilinmeyen item." }; + return this.db.transaction(() => { + const available = this.inventory.count(guildId, sellerId, item.id); + if (available < quantity) return { error: "Envanterinde bu üründen yeterli miktar yok.", available }; + const removed = this.inventory.remove(guildId, sellerId, item.id, quantity); + if (!removed.removed) return { error: "Ürün envanterden alınamadı.", available: removed.available }; + const listingId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`.toLowerCase(); + const listing = { + id: listingId, + guildId: String(guildId), + sellerId: String(sellerId), + itemId: item.id, + itemName: item.name, + quantity, + unitPrice, + createdAt: Date.now() + }; + this.db.set(this.listingKey(guildId, listingId), listing); + return { error: null, listing }; + }); + } + + getListings(guildId) { + return this.db.startsWith(this.listingPrefix(guildId)) + .map(({ data }) => data) + .filter((listing) => listing && Number.isSafeInteger(listing.quantity) && listing.quantity > 0 && Number.isSafeInteger(listing.unitPrice) && listing.unitPrice > 0) + .sort((a, b) => b.createdAt - a.createdAt); + } + + getListing(guildId, listingId) { + const listing = this.db.get(this.listingKey(guildId, String(listingId || "")), null); + return listing && listing.quantity > 0 ? listing : null; + } + + buyListing(guildId, buyerId, listingId, quantity = 1) { + this.assertId(guildId, "sunucu ID'si"); + this.assertId(buyerId, "alıcı ID'si"); + this.assertAmount(quantity); + return this.db.transaction(() => { + const listing = this.getListing(guildId, listingId); + if (!listing) return { error: "Satış ilanı bulunamadı." }; + if (listing.sellerId === String(buyerId)) return { error: "Kendi ilanını satın alamazsın." }; + if (listing.quantity < quantity) return { error: "İlanda yeterli stok yok.", available: listing.quantity }; + const total = listing.unitPrice * quantity; + if (!Number.isSafeInteger(total)) throw new RangeError("Satın alma toplamı güvenli sayı sınırını aşıyor."); + const buyerBalance = this.getBalance(guildId, buyerId); + if (buyerBalance < total) return { error: "Yetersiz bakiye.", balance: buyerBalance, total }; + const sellerBalance = this.getBalance(guildId, listing.sellerId); + if (!Number.isSafeInteger(sellerBalance + total)) throw new RangeError("Satıcının bakiyesi güvenli sayı sınırını aşıyor."); + listing.quantity -= quantity; + if (listing.quantity === 0) this.db.delete(this.listingKey(guildId, listing.id)); + else this.db.set(this.listingKey(guildId, listing.id), listing); + this.db.set(this.moneyKey(guildId, buyerId), buyerBalance - total); + this.db.set(this.moneyKey(guildId, listing.sellerId), sellerBalance + total); + this.inventory.add(guildId, buyerId, listing.itemId, quantity, { source: "player-market" }); + return { error: null, listing, quantity, total, after: buyerBalance - total }; }); } getInventory(guildId, userId) { - const inventory = this.db.get(this.inventoryKey(guildId, userId), []); - return Array.isArray(inventory) ? inventory : []; + return this.inventory.read(guildId, userId); } getCooldown(guildId, userId, command, durationMs) { @@ -131,22 +253,35 @@ class EconomyManager { }); } - _claimCooldownReward(guildId, userId, command, durationMs, amount) { + rollDrop(type) { + const chanceField = type === "daily" ? "dailyDropChance" : "weeklyDropChance"; + const entries = [...this.items.values()].map((item) => ({ item, chance: Number(item[chanceField] || 0) })).filter(({ chance }) => chance > 0); + let roll = Math.random() * 100; + for (const entry of entries) { + if (roll < entry.chance) return entry.item; + roll -= entry.chance; + } + return null; + } + + _claimCooldownReward(guildId, userId, command, durationMs, amount, dropType) { const cooldown = this.getCooldown(guildId, userId, command, durationMs); if (cooldown.onCooldown) return cooldown; const result = this._addMoney(guildId, userId, amount); + const droppedItem = this.rollDrop(dropType); + if (droppedItem) this.inventory.add(guildId, userId, droppedItem.id, 1, { source: `${dropType}-drop` }); this.setCooldown(guildId, userId, command); - return result; + return { ...result, droppedItem }; } daily(guildId, userId, amount) { this.assertAmount(amount); - return this.db.transaction(() => this._claimCooldownReward(guildId, userId, "daily", DAY, amount)); + return this.db.transaction(() => this._claimCooldownReward(guildId, userId, "daily", DAY, amount, "daily")); } weekly(guildId, userId, amount) { this.assertAmount(amount); - return this.db.transaction(() => this._claimCooldownReward(guildId, userId, "weekly", WEEK, amount)); + return this.db.transaction(() => this._claimCooldownReward(guildId, userId, "weekly", WEEK, amount, "weekly")); } work(guildId, userId, amount, options = {}) { diff --git a/lib/i18n.js b/lib/i18n.js index 3056827..f94e455 100644 --- a/lib/i18n.js +++ b/lib/i18n.js @@ -5,8 +5,8 @@ const locales = { addmoney: { description: "Add money to a user." }, bal: { description: "Show a user's balance." }, beg: { description: "Try to earn a random amount by begging." }, - buy: { description: "Buy an item from the shop." }, - daily: { description: "Claim your daily reward." }, + buy: { description: "Buy an item from the system market or a member listing." }, + daily: { description: "Claim your daily reward and possible item drop." }, help: { description: "Show all available bot commands." }, inventory: { description: "Show the items in your inventory." }, lb: { description: "Show the server economy leaderboard." }, @@ -14,10 +14,11 @@ const locales = { prefix: { description: "Change the server prefix." }, rob: { description: "Try to steal a random amount from another user." }, search: { description: "Search for a random amount of money." }, + sell: { description: "Create a member shop listing from your inventory." }, setmoney: { description: "Set a user's balance." }, - shop: { description: "Show the available shop items." }, + shop: { description: "Show the configured market and member shops." }, transfer: { description: "Transfer money to another user." }, - weekly: { description: "Claim your weekly reward." }, + weekly: { description: "Claim your weekly reward and possible item drop." }, work: { description: "Work a random job and earn money." } }, errors: { @@ -29,7 +30,11 @@ const locales = { noPermissionRole: "You are not allowed to use this command.", self: "You cannot target yourself.", insufficient: "Insufficient balance.", - invalidProduct: "That item does not exist in the shop.", + invalidProduct: "That item or listing does not exist.", + invalidItem: "That item is not defined in the configured item registry.", + invalidListing: "Use a valid item, positive quantity and positive unit price.", + notEnoughItems: "You only have **{available}** of this item in your inventory.", + ownListing: "You cannot buy your own listing.", noMoneyToSteal: "That user has no money you can steal.", selfRob: "You cannot rob yourself.", botsDisabled: "Bot accounts cannot use this command.", @@ -62,13 +67,18 @@ const locales = { begCooldown: "You must wait **{time}** before begging again.", search: "You searched around and found **{amount}**. Your new balance is **{balance}**.", searchCooldown: "You must wait **{time}** before searching again.", - purchase: "You bought **{item}** for **{price}**. Remaining balance: **{balance}**.", - purchaseNeed: "You need **{price}** to buy this item, but you only have **{balance}**.", + purchase: "You bought **{item}** ×**{quantity}** for **{price}**. Remaining balance: **{balance}**.", + purchaseNeed: "You need **{price}** to buy this quantity, but you only have **{balance}**.", + purchaseStock: "Only **{stock}** of this item are currently in stock.", inventoryEmpty: "Your inventory is empty.", inventoryTitle: "{user} — Inventory", - inventoryItem: "Quantity: **{count}**\\nUnit price: **{price}**", - shopTitle: "Shop", + inventoryItem: "Quantity: **{count}**\\nReference value: **{price}**", + shopTitle: "Community Shop", shopItem: "Price: **{price}**", + listingCreated: "Your member shop listing is live: **{item}** ×**{quantity}** at **{price}** per unit. Listing ID: `{listing}`", + listingStock: "Only **{stock}** units remain in that member listing.", + listingPurchase: "You bought **{item}** ×**{quantity}** from a member for **{price}**. Remaining balance: **{balance}**.", + itemDrop: "You also found **{item}** in your reward drop.", transfer: "You sent **{amount}** to <@{target}>. Your new balance is **{balance}**.", robSuccess: "You stole **{amount}** from <@{target}>. Your new balance is **{balance}**.", robCooldown: "You must wait **{time}** before attempting another robbery.", @@ -97,19 +107,20 @@ const locales = { addmoney: { description: "Bir kullanıcıya para ekler." }, bal: { description: "Bir kullanıcının bakiyesini gösterir." }, beg: { description: "Dilenerek rastgele miktarda para kazanmaya çalışır." }, - buy: { description: "Mağazadan bir ürün satın alır." }, - daily: { description: "Günlük ödülünü alır." }, + buy: { description: "Sistem marketinden veya bir oyuncu ilanından item satın alır." }, + daily: { description: "Günlük ödülünü ve olası item drop'unu alır." }, help: { description: "Kullanılabilir tüm bot komutlarını gösterir." }, - inventory: { description: "Envanterindeki ürünleri gösterir." }, + inventory: { description: "Envanterindeki item'ları gösterir." }, lb: { description: "Sunucunun ekonomi sıralamasını gösterir." }, ping: { description: "Bot gecikmesini gösterir." }, prefix: { description: "Sunucu prefix'ini değiştirir." }, rob: { description: "Başka bir kullanıcıdan rastgele miktarda para çalmayı dener." }, search: { description: "Rastgele miktarda para arar." }, + sell: { description: "Envanterindeki item ile oyuncu marketinde satış ilanı açar." }, setmoney: { description: "Bir kullanıcının bakiyesini ayarlar." }, - shop: { description: "Mağazadaki mevcut ürünleri gösterir." }, + shop: { description: "Yapılandırılmış marketi ve oyuncu mağazalarını gösterir." }, transfer: { description: "Başka bir kullanıcıya para gönderir." }, - weekly: { description: "Haftalık ödülünü alır." }, + weekly: { description: "Haftalık ödülünü ve olası item drop'unu alır." }, work: { description: "Rastgele bir iş yaparak para kazanır." } }, errors: { @@ -121,7 +132,11 @@ const locales = { noPermissionRole: "Bu komutu kullanmaya yetkin bulunmuyor.", self: "Kendini hedefleyemezsin.", insufficient: "Yetersiz bakiye.", - invalidProduct: "Bu ürün mağazada bulunmuyor.", + invalidProduct: "Bu item veya ilan bulunamadı.", + invalidItem: "Bu item yapılandırılmış item registry içinde tanımlı değil.", + invalidListing: "Geçerli bir item, pozitif miktar ve pozitif birim fiyat kullanmalısın.", + notEnoughItems: "Envanterinde bu item'dan yalnızca **{available}** adet bulunuyor.", + ownListing: "Kendi ilanından alışveriş yapamazsın.", noMoneyToSteal: "Bu kullanıcının çalınabilecek parası yok.", selfRob: "Kendini soyamazsın.", botsDisabled: "Bot hesapları bu komutu kullanamaz.", @@ -154,13 +169,18 @@ const locales = { begCooldown: "Tekrar dilenebilmek için **{time}** beklemelisin.", search: "Etrafta arama yaptın ve **{amount}** buldun. Yeni bakiyen **{balance}**.", searchCooldown: "Tekrar arama yapmak için **{time}** beklemelisin.", - purchase: "**{item}** ürününü **{price}** karşılığında satın aldın. Kalan bakiyen **{balance}**.", - purchaseNeed: "Bu ürün için **{price}** gerekiyor fakat bakiyen **{balance}**.", + purchase: "**{item}** ×**{quantity}** ürününü **{price}** karşılığında satın aldın. Kalan bakiyen **{balance}**.", + purchaseNeed: "Bu miktarı almak için **{price}** gerekiyor fakat bakiyen **{balance}**.", + purchaseStock: "Bu item'dan şu anda yalnızca **{stock}** adet stokta var.", inventoryEmpty: "Envanterin boş.", inventoryTitle: "{user} — Envanter", - inventoryItem: "Miktar: **{count}**\\nBirim fiyat: **{price}**", - shopTitle: "Mağaza", + inventoryItem: "Miktar: **{count}**\\nReferans değer: **{price}**", + shopTitle: "Topluluk Mağazası", shopItem: "Fiyat: **{price}**", + listingCreated: "Oyuncu mağazandaki ilan açıldı: **{item}** ×**{quantity}**, birim fiyat **{price}**. İlan ID: `{listing}`", + listingStock: "Bu oyuncu ilanında yalnızca **{stock}** adet kaldı.", + listingPurchase: "Bir oyuncudan **{item}** ×**{quantity}** ürününü **{price}** karşılığında aldın. Kalan bakiyen **{balance}**.", + itemDrop: "Ayrıca ödül drop'undan **{item}** elde ettin.", transfer: "<@{target}> kullanıcısına **{amount}** gönderdin. Yeni bakiyen **{balance}**.", robSuccess: "<@{target}> kullanıcısından **{amount}** çaldın. Yeni bakiyen **{balance}**.", robCooldown: "Tekrar soygun deneyebilmek için **{time}** beklemelisin.", diff --git a/lib/inventory.js b/lib/inventory.js new file mode 100644 index 0000000..3597019 --- /dev/null +++ b/lib/inventory.js @@ -0,0 +1,78 @@ +class InventoryManager { + constructor(database, items) { + if (!database) throw new TypeError("InventoryManager bir veritabanı örneği gerektirir."); + this.db = database; + this.setRegistry(items); + } + + setRegistry(items) { + if (!Array.isArray(items)) throw new TypeError("Item registry bir dizi olmalı."); + this.registry = new Map(items.map((item) => [String(item.id).toLowerCase(), Object.freeze({ ...item })])); + } + + getItemRegistry() { + return Object.fromEntries([...this.registry.entries()].map(([id, item]) => [id, { ...item }])); + } + + getItem(itemId) { + return this.registry.get(String(itemId || "").trim().toLowerCase()) || null; + } + + key(guildId, userId) { + if (!/^\d{17,20}$/.test(String(guildId)) || !/^\d{17,20}$/.test(String(userId))) throw new TypeError("Geçersiz envanter kimliği."); + return `inventory:${guildId}:${userId}`; + } + + read(guildId, userId) { + const value = this.db.get(this.key(guildId, userId), []); + if (!Array.isArray(value)) throw new TypeError("Envanter verisi bozuk."); + return value; + } + + count(guildId, userId, itemId) { + const id = String(itemId).trim().toLowerCase(); + return this.read(guildId, userId).filter((item) => String(item.id).toLowerCase() === id).length; + } + + add(guildId, userId, itemId, quantity = 1, metadata = {}) { + const item = this.getItem(itemId); + if (!item) throw new Error(`Bilinmeyen item: ${itemId}`); + if (!Number.isSafeInteger(quantity) || quantity <= 0) throw new TypeError("Item miktarı pozitif bir tam sayı olmalı."); + const inventory = this.read(guildId, userId); + for (let index = 0; index < quantity; index += 1) { + inventory.push({ + id: item.id, + name: item.name, + description: item.description, + obtainedAt: Date.now(), + source: metadata.source || "system" + }); + } + this.db.set(this.key(guildId, userId), inventory); + return inventory; + } + + remove(guildId, userId, itemId, quantity = 1) { + const id = String(itemId).trim().toLowerCase(); + if (!Number.isSafeInteger(quantity) || quantity <= 0) throw new TypeError("Item miktarı pozitif bir tam sayı olmalı."); + const inventory = this.read(guildId, userId); + const matches = []; + for (let index = 0; index < inventory.length; index += 1) { + if (String(inventory[index].id).toLowerCase() === id) matches.push(index); + } + if (matches.length < quantity) return { removed: false, available: matches.length, inventory }; + const removeSet = new Set(matches.slice(0, quantity)); + const next = inventory.filter((_, index) => !removeSet.has(index)); + this.db.set(this.key(guildId, userId), next); + return { removed: true, available: matches.length, inventory: next }; + } + + transferWithinTransaction(guildId, fromUserId, toUserId, itemId, quantity = 1) { + const removed = this.remove(guildId, fromUserId, itemId, quantity); + if (!removed.removed) return removed; + this.add(guildId, toUserId, itemId, quantity, { source: "transfer" }); + return { removed: true, inventory: this.read(guildId, fromUserId) }; + } +} + +module.exports = InventoryManager; diff --git a/tests/test.js b/tests/test.js index 6baafc6..c30cef4 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5,13 +5,14 @@ const path = require("node:path"); const { Collection, PermissionsBitField } = require("discord.js"); const { KeyValueStore } = require("../lib/database"); const EconomyManager = require("../lib/economy"); +const InventoryManager = require("../lib/inventory"); const { createInteractionContext } = require("../lib/context"); const { createTranslator } = require("../lib/i18n"); const { isAdmin } = require("../lib/commandUtils"); const root = path.join(__dirname, ".."); const commandFiles = fs.readdirSync(path.join(root, "commands")).filter((file) => file.endsWith(".js")).sort(); -assert.equal(commandFiles.length, 17, "Tam olarak 17 komut dosyası bulunmalı."); +assert.equal(commandFiles.length, 18, "Tam olarak 18 komut dosyası bulunmalı."); const commands = commandFiles.map((file) => ({ file, command: require(path.join(root, "commands", file)) })); const commandMap = new Map(); @@ -28,7 +29,7 @@ for (const { file, command } of commands) { assert.ok(!commandMap.has(command.name), `Yinelenen komut: ${command.name}`); commandMap.set(command.name, command); } -assert.equal(commandMap.size, 17); +assert.equal(commandMap.size, 18); const aliasMap = new Map(); for (const command of commandMap.values()) { @@ -47,43 +48,47 @@ function user(id, tag) { const tempPath = path.join(os.tmpdir(), `economybot-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.sqlite`); const db = new KeyValueStore(tempPath); -const eco = new EconomyManager(db); +const items = [ + { id: "apple", name: "Apple", description: "A test item.", dailyDropChance: 100, weeklyDropChance: 100 }, + { id: "gem", name: "Gem", description: "A rare test item.", dailyDropChance: 0, weeklyDropChance: 0 }, + { id: "book", name: "Book", description: "Another test item.", dailyDropChance: 0, weeklyDropChance: 0 } +]; +const market = [ + { item: "apple", price: 250, stock: 2 }, + { item: "gem", price: 1000, stock: 3 } +]; +const inventory = new InventoryManager(db, items); +const eco = new EconomyManager(db, inventory, items, market); const guildId = "123456789012345678"; const adminId = "123456789012345679"; -const targetId = "123456789012345680"; -const botId = "123456789012345681"; +const sellerId = "123456789012345680"; +const buyerId = "123456789012345681"; +const botId = "123456789012345682"; const ADMIN_ROLE_ID = "999999999999999999"; const client = { config: { prefix: "!", language: "en", admins: [], adminRoles: [ADMIN_ROLE_ID], adminRoleNames: [], countChannel: "" }, db, + inventory, eco, commands: new Collection(), aliases: new Collection(aliasMap), users: { cache: new Collection(), fetch: async (id) => client.users.cache.get(id) }, user: user(botId, "EconomyBot#0001"), - ws: { ping: 42 }, - shop: { - laptop: { id: "laptop", name: "Laptop", cost: 2000 }, - mobile: { id: "mobile", name: "Mobile", cost: 1000 }, - pc: { id: "pc", name: "PC", cost: 3000 } - } + ws: { ping: 42 } }; for (const command of commandMap.values()) client.commands.set(command.name, command); -client.users.cache.set(adminId, user(adminId, "Admin#0001")); -client.users.cache.set(targetId, user(targetId, "Target#0001")); -client.users.cache.set(botId, client.user); +for (const id of [adminId, sellerId, buyerId, botId]) client.users.cache.set(id, user(id, `User${id.slice(-3)}#0001`)); eco.setMoney(guildId, adminId, 20_000); -eco.setMoney(guildId, targetId, 5_000); +eco.setMoney(guildId, sellerId, 1_000); +eco.setMoney(guildId, buyerId, 5_000); -function makeContext(userId, options = {}, language = client.config.language, memberMode = "collection") { - const currentUser = client.users.cache.get(userId); +function makeContext(userId, options = {}, language = client.config.language, memberMode = "collection", args = []) { + const currentUser = client.users.cache.get(userId) || user(userId, `User${String(userId).slice(-3)}#0001`); const roleCache = new Collection(); roleCache.set(ADMIN_ROLE_ID, { id: ADMIN_ROLE_ID, name: "Economy Admin" }); const permissions = new PermissionsBitField(); - const member = memberMode === "array" - ? { permissions, roles: [ADMIN_ROLE_ID] } - : { permissions, roles: { cache: roleCache } }; + const member = memberMode === "array" ? { permissions, roles: [ADMIN_ROLE_ID] } : { permissions, roles: { cache: roleCache } }; const interaction = { createdTimestamp: Date.now(), user: currentUser, @@ -103,6 +108,7 @@ function makeContext(userId, options = {}, language = client.config.language, me const ctx = createInteractionContext(interaction, { ...client, config: { ...client.config, language } }); ctx.member = member; ctx.reply = async (payload) => { replies.push(payload); return payload; }; + ctx.args = args; return { ctx, replies }; } @@ -116,19 +122,33 @@ async function runSlash(name, userId = adminId, options = {}, language = client. (async () => { assert.equal(db.connection.open, true); - assert.equal(eco.getBalance(guildId, adminId), 20_000); - const transfer = eco.transfer(guildId, adminId, targetId, 500); - assert.equal(transfer.fromBalance, 19_500); - assert.equal(eco.getBalance(guildId, targetId), 5_500); - assert.equal(eco.transfer(guildId, adminId, adminId, 1).error, "Kendine para gönderemezsin."); - assert.equal(eco.transfer(guildId, adminId, targetId, 999_999).error, "Yetersiz bakiye."); - const purchase = eco.purchase(guildId, adminId, client.shop.laptop); + assert.equal(eco.getMarket(guildId).find((item) => item.id === "apple").stock, 2); + const purchase = eco.purchase(guildId, adminId, eco.getMarketItem(guildId, "apple"), 2); assert.equal(purchase.error, null); - assert.equal(eco.getInventory(guildId, adminId).length, 1); - const failedPurchase = eco.purchase(guildId, adminId, { id: "expensive", name: "Pahalı Ürün", cost: 100_000 }); - assert.equal(failedPurchase.error, "Yetersiz bakiye."); - assert.equal(eco.getInventory(guildId, adminId).length, 1); + assert.equal(eco.getMarketItem(guildId, "apple").stock, 0); + assert.equal(inventory.count(guildId, adminId, "apple"), 2); + const soldOut = eco.purchase(guildId, adminId, eco.getMarketItem(guildId, "apple"), 1); + assert.equal(soldOut.error, "Yetersiz stok."); + + const transfer = eco.transfer(guildId, adminId, sellerId, 500); + assert.equal(transfer.fromBalance, 19_000); + assert.equal(eco.transfer(guildId, adminId, adminId, 1).error, "Kendine para gönderemezsin."); + assert.equal(eco.transfer(guildId, adminId, buyerId, 999_999).error, "Yetersiz bakiye."); + + const listing = eco.createListing(guildId, adminId, "apple", 1, 700); + assert.equal(listing.error, null); + assert.equal(inventory.count(guildId, adminId, "apple"), 1); + const listingPurchase = eco.buyListing(guildId, buyerId, listing.listing.id, 1); + assert.equal(listingPurchase.error, null); + assert.equal(inventory.count(guildId, buyerId, "apple"), 1); + assert.equal(eco.getListings(guildId).length, 0); + assert.equal(eco.getBalance(guildId, adminId), 19_700); + assert.equal(eco.getBalance(guildId, buyerId), 4_300); + + const failedListing = eco.createListing(guildId, sellerId, "gem", 1, 2000); + assert.equal(failedListing.error, "Envanterinde bu üründen yeterli miktar yok."); + assert.equal(eco.createListing(guildId, sellerId, "not-configured", 1, 10).error, "Bilinmeyen item."); const cooldown1 = eco.useCooldown(guildId, adminId, "test", 60_000); const cooldown2 = eco.useCooldown(guildId, adminId, "test", 60_000); @@ -136,6 +156,15 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.equal(cooldown2.onCooldown, true); assert.ok(cooldown2.remainingMs > 0); + const dailyUser = "123456789012345683"; + const daily = eco.daily(guildId, dailyUser, 100); + assert.equal(daily.droppedItem.id, "apple"); + assert.equal(inventory.count(guildId, dailyUser, "apple"), 1); + const weeklyUser = "123456789012345684"; + const weekly = eco.weekly(guildId, weeklyUser, 500); + assert.equal(weekly.droppedItem.id, "apple"); + assert.equal(inventory.count(guildId, weeklyUser, "apple"), 1); + const adminCtx = makeContext(adminId).ctx; assert.equal(isAdmin(adminCtx), true); const arrayRoleCtx = makeContext(adminId, {}, "en", "array").ctx; @@ -145,29 +174,24 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.equal(isAdmin(adminCtx), true); adminCtx.member.permissions.remove(PermissionsBitField.Flags.Administrator); client.config.adminRoles = [ADMIN_ROLE_ID]; - assert.equal(isAdmin(adminCtx), true); - client.config.adminRoles = []; - client.config.adminRoleNames = ["economy admin"]; - assert.equal(isAdmin(adminCtx), true); - client.config.adminRoles = [ADMIN_ROLE_ID]; - client.config.adminRoleNames = []; - await runSlash("addmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }, "en"); + await runSlash("addmoney", adminId, { kullanici: client.users.cache.get(sellerId), miktar: 100 }, "en"); await runSlash("bal"); await runSlash("beg"); - await runSlash("buy", adminId, { urun: "mobile" }); - await runSlash("daily", targetId); + await runSlash("buy", adminId, { urun: "gem", miktar: 1 }); + await runSlash("daily", dailyUser); await runSlash("help"); - await runSlash("inventory"); + await runSlash("inventory", dailyUser); await runSlash("lb"); await runSlash("ping"); await runSlash("prefix", adminId, { yeni_prefix: "$" }); - await runSlash("rob", adminId, { kullanici: client.users.cache.get(targetId) }); - await runSlash("search", targetId); - await runSlash("setmoney", adminId, { kullanici: client.users.cache.get(targetId), miktar: 2500 }); + await runSlash("rob", adminId, { kullanici: client.users.cache.get(sellerId) }); + await runSlash("search", sellerId); + await runSlash("sell", adminId, { urun: "apple", miktar: 1, fiyat: 900 }); + await runSlash("setmoney", adminId, { kullanici: client.users.cache.get(sellerId), miktar: 2500 }); await runSlash("shop"); - await runSlash("transfer", adminId, { kullanici: client.users.cache.get(targetId), miktar: 100 }); - await runSlash("weekly", targetId); + await runSlash("transfer", adminId, { kullanici: client.users.cache.get(sellerId), miktar: 100 }); + await runSlash("weekly", weeklyUser); await runSlash("work"); assert.equal(db.getPrefix(guildId, "!"), "$"); @@ -182,7 +206,7 @@ async function runSlash(name, userId = adminId, options = {}, language = client. const originalRoles = client.config.adminRoles; client.config.adminRoles = []; - const unauthorized = await runSlash("addmoney", targetId, { kullanici: client.users.cache.get(adminId), miktar: 100 }, "en"); + const unauthorized = await runSlash("addmoney", sellerId, { kullanici: client.users.cache.get(adminId), miktar: 100 }, "en"); assert.equal(unauthorized.ephemeral, true); client.config.adminRoles = originalRoles; @@ -210,7 +234,7 @@ async function runSlash(name, userId = adminId, options = {}, language = client. guild: { id: guildId }, inGuild: () => true, author: client.users.cache.get(adminId), - content: "$bakiye", + content: "$sat apple 1 700", createdTimestamp: Date.now(), channel: { id: "not-counter" }, member: { permissions: new PermissionsBitField(), roles: { cache: new Collection([[ADMIN_ROLE_ID, { id: ADMIN_ROLE_ID, name: "Economy Admin" }]]) } }, @@ -218,7 +242,7 @@ async function runSlash(name, userId = adminId, options = {}, language = client. reply: async (payload) => { prefixReplies.push(payload); return payload; } }; await require("../events/messageCreate")(client, prefixMessage); - assert.ok(prefixReplies.length > 0, "Prefix aliası gerçek messageCreate akışında yanıt üretmedi."); + assert.ok(prefixReplies.length > 0, "Prefix sell aliası gerçek messageCreate akışında yanıt üretmedi."); db.close(); for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); @@ -227,7 +251,7 @@ async function runSlash(name, userId = adminId, options = {}, language = client. assert.equal(db.connection.open, true); db.close(); for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); - console.log("TÜM TESTLER BAŞARILI: DB lifecycle, atomic ekonomi, cooldown, 17 slash komutu, gerçek prefix event'i, dil sistemi ve admin yetkilendirmesi doğrulandı."); + console.log("TÜM TESTLER BAŞARILI: 18 slash komutu, config item registry, persistent stock, member marketplace, inventory transfers, daily/weekly drops, locale, permissions, events ve DB lifecycle doğrulandı."); })().catch((error) => { try { db.close(); } catch {} for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${tempPath}${suffix}`, { force: true }); From 387c47e0ef841ef8493e5fb9e0da78e31681aff2 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:44:32 +0300 Subject: [PATCH 171/175] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 046d53f..eec40d1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![SQLite](https://img.shields.io/badge/SQLite-better--sqlite3-003B57?style=for-the-badge&logo=sqlite&logoColor=white)](https://www.sqlite.org/) [![License](https://img.shields.io/badge/License-Nginx-8A2BE2?style=for-the-badge)](LICENSE) -🇹🇷 Türkçe🇬🇧 English💬 Join the Discord Server +🇹🇷 Türkçe🇬🇧 English💬 Join the Discord Server @@ -671,5 +671,4 @@ Bu sürüm açık kaynak `ZeroDiscord/EconomyBot` projesinden türetilmiştir; f Orijinal repo projenin soyunun bir parçasıdır; bu sürüm ise genişletilebilirlik, transaction bütünlüğü, yapılandırılabilirlik ve gelecekteki botlar arası veri paylaşımı odağında bağımsız bir devam sürümüdür. -> **Discord sunucusu:** README içindeki `https://discord.gg/YOUR_INVITE` bağlantısını kendi gerçek sunucu davet bağlantınızla değiştirin. From 8bc87204bbce567c039178519a5d6f1100a4e7d6 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 00:46:40 +0300 Subject: [PATCH 172/175] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eec40d1..f262726 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ [![SQLite](https://img.shields.io/badge/SQLite-better--sqlite3-003B57?style=for-the-badge&logo=sqlite&logoColor=white)](https://www.sqlite.org/) [![License](https://img.shields.io/badge/License-Nginx-8A2BE2?style=for-the-badge)](LICENSE) -🇹🇷 Türkçe🇬🇧 English💬 Join the Discord Server +🇹🇷 Türkçe🇬🇧 English💬 Join the Discord Server

+EconomyBot Preview From f41359d9330f02e8aa36c182da4d0f8691803c52 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 01:04:03 +0300 Subject: [PATCH 173/175] Update README.md --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index f262726..c8781ce 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,7 @@ The command system was rebuilt for native discord.js v14 interactions, with cent Cooldowns Drops Member Listings ``` -The marketplace and inventory layers were intentionally separated so a future roleplay bot can share the same inventory representation without forcing roleplay functionality into the economy bot itself. - -The architecture was also informed by patterns visible in the open-source `casperiv0/ghostybot` project, particularly its per-guild/per-user economy organization and inventory/reward concepts. This repository does **not** copy GhostyBot's implementation; the project is used as an architectural reference only. +The marketplace and inventory layers were intentionally separated so a future roleplay bot can share the same inventory representation without forcing roleplay functionality into the economy bot itself ### Configuration @@ -399,8 +397,6 @@ discord.js v14 uyumluluğu da sadece API isimlerinin değiştirilmesi seviyesind Envanter ve marketplace katmanları, ileride roleplay botuyla veri paylaşımına uygun olacak şekilde ayrıştırıldı. Böylece gelecekte RP tarafının özel iş mantığını ekonomi botunun içine yığmadan ortak item verisi kullanılabilir. -Mimari karşılaştırma aşamasında açık kaynak `casperiv0/ghostybot` projesindeki sunucu/kullanıcı ekonomi organizasyonu ve inventory/reward yaklaşımı da referans alındı. Bu proje GhostyBot kodunu kopyalamaz; yalnızca mimari fikirleri karşılaştırma amacıyla kullanır. - ### Yapılandırma Item ekonomisinin neredeyse tamamı artık `botConfig.js` üzerinden tanımlanabilir. From b525592d1d2cdb562fcc493fe2059ee13a59f5c6 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 01:09:44 +0300 Subject: [PATCH 174/175] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8781ce..1f54402 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-# EconomyBot +# INS Development Economy Bot ### Re-engineered • Node.js 20+ • discord.js v14 • English / Türkçe From e208fcbcbd42a4c3742636623c16c03658273250 Mon Sep 17 00:00:00 2001 From: LoiFragola Date: Thu, 3 Sep 2026 01:25:32 +0300 Subject: [PATCH 175/175] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 1f54402..c683060 100644 --- a/README.md +++ b/README.md @@ -309,8 +309,6 @@ GitHub Actions additionally tests Node.js 20, 22 and 24 and runs CodeQL analysis ### Security -Never commit a real Discord bot token. If a token is exposed, regenerate it through the Discord Developer Portal. - The local SQLite database lives under `data/` and should not be committed. ### Project Structure @@ -634,8 +632,6 @@ GitHub Actions ayrıca Node.js 20, 22 ve 24 testlerini ve CodeQL analizini çal ### Güvenlik -Gerçek Discord bot token'ınızı GitHub'a commit etmeyin. Token açığa çıktıysa Discord Developer Portal üzerinden yenileyin. - Yerel SQLite veritabanı `data/` altında tutulur ve Git'e gönderilmemelidir. ### Proje Yapısı