diff --git a/Dockerfile b/Dockerfile index 75e8156..7d9b714 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,38 +1,14 @@ -FROM ubuntu:22.04 - -# Prevent interactive prompts -ENV DEBIAN_FRONTEND=noninteractive - -# Install dependencies -RUN apt-get update && apt-get install -y \ - curl \ - git \ - build-essential \ - libssl-dev \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# Install Luvit -WORKDIR /tmp -RUN curl -L https://github.com/luvit/lit/raw/master/get-lit.sh -o get-lit.sh && \ - chmod +x get-lit.sh && \ - ./get-lit.sh && \ - mv luvi /usr/local/bin/ && \ - mv luvit /usr/local/bin/ && \ - mv lit /usr/local/bin/ && \ - rm get-lit.sh +FROM python:3.11-slim # Set working directory WORKDIR /app -# Copy everything -COPY . . +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt -# Install Lua dependencies -RUN lit install +# Copy bot file +COPY bot.py . -# Verify bot.lua exists -RUN ls -la /app/ && test -f /app/bot.lua || (echo "ERROR: bot.lua not found!" && exit 1) - -# Start the bot -CMD ["luvit", "bot.lua"] \ No newline at end of file +# Run the bot +CMD ["python", "bot.py"] \ No newline at end of file diff --git a/app.lua b/app.lua deleted file mode 100644 index c7d0be9..0000000 --- a/app.lua +++ /dev/null @@ -1,718 +0,0 @@ -local discordia = require('discordia') -local client = discordia.Client() -local fs = require('fs') -local json = require('json') -local http = require('coro-http') -local timer = require('timer') - --- Configuration -local config = { - token = os.getenv('DISCORD_TOKEN'), - prefix = '!', - data_file = 'data.json', - timeout_file = 'timeouts.json', - - -- XP System - xp_min = 15, - xp_max = 25, - xp_cooldown = 60, -- seconds - xp_per_level = 100, - level_up_multiplier = 10, - - -- YouTube - youtube_channel_id = os.getenv('YOUTUBE_CHANNEL_ID'), - notification_channel_id = os.getenv('NOTIFICATION_CHANNEL_ID'), - video_check_interval = 300, -- 5 minutes - - -- Auto-save - autosave_interval = 300, -- 5 minutes - leaderboard_update_interval = 3600 -- 1 hour -} - --- Database Module -local Database = {} -Database.__index = Database - -function Database.new(filename) - local self = setmetatable({}, Database) - self.filename = filename - self.data = self:load() - return self -end - -function Database:load() - if fs.existsSync(self.filename) then - local content = fs.readFileSync(self.filename) - local success, decoded = pcall(json.decode, content) - if success then return decoded end - end - return { - users = {}, - guilds = {}, - leaderboards = {}, - youtube = {} - } -end - -function Database:save() - local success, encoded = pcall(json.encode, self.data) - if success then - fs.writeFileSync(self.filename, encoded) - end -end - --- User Data Functions -function Database:getUser(guildId, userId) - local key = guildId .. '_' .. userId - if not self.data.users[key] then - self.data.users[key] = { - coins = 0, - bank = 0, - level = 1, - xp = 0, - lastMessage = 0, - lastDaily = 0, - lastWork = 0, - fishCaught = 0, - gamblingWins = 0, - gamblingLosses = 0 - } - end - return self.data.users[key] -end - -function Database:setUser(guildId, userId, data) - local key = guildId .. '_' .. userId - self.data.users[key] = data - self:save() -end - -function Database:getGuild(guildId) - if not self.data.guilds[guildId] then - self.data.guilds[guildId] = { - notificationsEnabled = true, - leaderboardChannelId = nil, - leaderboardMessageId = nil - } - end - return self.data.guilds[guildId] -end - -function Database:setGuild(guildId, data) - self.data.guilds[guildId] = data - self:save() -end - -function Database:getLastVideoId(guildId) - return self.data.youtube[guildId] -end - -function Database:setLastVideoId(guildId, videoId) - self.data.youtube[guildId] = videoId - self:save() -end - -function Database:getAllGuildUsers(guildId) - local users = {} - for key, data in pairs(self.data.users) do - if key:match('^' .. guildId .. '_') then - local userId = key:match('_(.+)$') - table.insert(users, {userId = userId, data = data}) - end - end - - -- Sort by level and XP - table.sort(users, function(a, b) - if a.data.level == b.data.level then - return a.data.xp > b.data.xp - end - return a.data.level > b.data.level - end) - - return users -end - --- Initialize database -local db = Database.new(config.data_file) - --- Timeout Module -local TimeoutDB = Database.new(config.timeout_file) - --- Utility Functions -local function createProgressBar(current, total, length) - length = length or 20 - if total == 0 then - return string.rep('ā–‘', length) .. ' 0%' - end - - local filled = math.floor((current / total) * length) - local bar = string.rep('ā–ˆ', filled) .. string.rep('ā–‘', length - filled) - local percentage = math.floor((current / total) * 100) - return '`' .. bar .. '` ' .. percentage .. '%' -end - -local function formatNumber(num) - local formatted = tostring(num) - local k - while true do - formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2') - if k == 0 then break end - end - return formatted -end - --- Command Handler -local commands = {} - --- ============================================ --- GENERAL COMMANDS --- ============================================ - -function commands.help(message) - local embed = { - title = 'šŸ¤– Tooly Bot - Command List', - color = 0x5865F2, - fields = { - { - name = 'šŸ“Š Leveling', - value = '`!rank [@user]` - View rank and level\n`!leaderboard` - Server leaderboard\n`!setleaderboard` - Set auto-updating leaderboard (Admin)', - inline = false - }, - { - name = 'šŸ’° Economy', - value = '`!balance [@user]` - Check balance\n`!daily` - Daily reward\n`!work` - Work for coins\n`!give @user ` - Give coins', - inline = false - }, - { - name = 'šŸŽ® Fun', - value = '`!8ball ` - Magic 8ball\n`!roll [sides]` - Roll dice\n`!coinflip` - Flip a coin\n`!kitty` - Random cat\n`!doggy` - Random dog\n`!joke` - Random joke', - inline = false - }, - { - name = 'šŸ›”ļø Moderation', - value = '`!timeout @user [reason]` - Timeout user (Admin)\n`!untimeout @user` - Remove timeout (Admin)\n`!timeouts` - View timeouts (Admin)\n`!mute/@user` - Mute user (Admin)\n`!kick/@user` - Kick user (Admin)', - inline = false - }, - { - name = 'šŸ“ŗ YouTube', - value = '`!togglenotif` - Toggle notifications (Admin)\n`!notifstatus` - Check notification status', - inline = false - } - }, - footer = { - text = 'Use ' .. config.prefix .. ' to run' - } - } - message:reply{embed = embed} -end - -function commands.ping(message) - local startTime = os.clock() - message:reply('šŸ“ Pong! Calculating...'):next(function() - local latency = math.floor((os.clock() - startTime) * 1000) - message.channel:send(string.format('šŸ“ Pong! Latency: `%dms`', latency)) - end) -end - --- ============================================ --- LEVELING COMMANDS --- ============================================ - -function commands.rank(message, args) - local guild = message.guild - if not guild then return end - - local targetUser = message.mentionedUsers.first or message.author - local userData = db:getUser(guild.id, targetUser.id) - local xpNeeded = userData.level * config.xp_per_level - - -- Get rank - local allUsers = db:getAllGuildUsers(guild.id) - local rank = 'Unranked' - for i, u in ipairs(allUsers) do - if u.userId == targetUser.id then - rank = '#' .. i - break - end - end - - local progressBar = createProgressBar(userData.xp, xpNeeded, 20) - local progressPercent = xpNeeded > 0 and math.floor((userData.xp / xpNeeded) * 100) or 0 - - local color = 0x4D96FF - if userData.level >= 50 then color = 0xFF6B6B - elseif userData.level >= 30 then color = 0xFFD93D - elseif userData.level >= 15 then color = 0x6BCB77 end - - local totalCoins = userData.coins + userData.bank - - local description = string.format([[ -**RANK** • %s / %d -**LEVEL** • %d -**XP** • %s / %s (%d%%) - -%s - -**šŸ’° BALANCE** • %s coins -]], rank, #allUsers, userData.level, formatNumber(userData.xp), formatNumber(xpNeeded), progressPercent, progressBar, formatNumber(totalCoins)) - - local embed = { - color = color, - author = { - name = targetUser.username .. "'s Profile", - icon_url = targetUser.avatarURL - }, - description = description, - thumbnail = {url = targetUser.avatarURL}, - footer = {text = 'Requested by ' .. message.author.username}, - timestamp = discordia.Date():toISO('T', 'Z') - } - - if userData.fishCaught > 0 then - table.insert(embed.fields or {}, {name = 'šŸŽ£ Fish Caught', value = formatNumber(userData.fishCaught), inline = true}) - embed.fields = embed.fields or {} - end - - message:reply{embed = embed} -end - -function commands.leaderboard(message) - local guild = message.guild - if not guild then return end - - local allUsers = db:getAllGuildUsers(guild.id) - local description = {} - - for i = 1, math.min(10, #allUsers) do - local u = allUsers[i] - local medal = i == 1 and 'šŸ„‡' or i == 2 and '🄈' or i == 3 and 'šŸ„‰' or '**' .. i .. '.**' - local totalCoins = u.data.coins + u.data.bank - - table.insert(description, string.format('%s <@%s>\nā”” Level %d (%s XP) • %s coins', - medal, u.userId, u.data.level, formatNumber(u.data.xp), formatNumber(totalCoins))) - end - - local embed = { - title = 'šŸ† Server Leaderboard', - description = #description > 0 and table.concat(description, '\n') or 'No users yet!', - color = 0x9B59B6, - footer = {text = 'Updates every hour • Showing Level & Total Coins'}, - timestamp = discordia.Date():toISO('T', 'Z') - } - - message:reply{embed = embed} -end - -function commands.setleaderboard(message) - if not message.member:hasPermission('administrator') then - message:reply('āŒ You need administrator permission!') - return - end - - local guild = message.guild - commands.leaderboard(message) - - -- Save leaderboard location - local guildData = db:getGuild(guild.id) - guildData.leaderboardChannelId = message.channel.id - db:setGuild(guild.id, guildData) - - message.channel:send('āœ… Auto-updating leaderboard created! It will update every hour.') -end - --- ============================================ --- ECONOMY COMMANDS --- ============================================ - -function commands.balance(message, args) - local guild = message.guild - if not guild then return end - - local targetUser = message.mentionedUsers.first or message.author - local userData = db:getUser(guild.id, targetUser.id) - - local embed = { - title = 'šŸ’° Balance', - description = string.format('%s has **%s** coins in wallet and **%s** in bank!\n**Total:** %s coins', - targetUser.mentionString, formatNumber(userData.coins), formatNumber(userData.bank), - formatNumber(userData.coins + userData.bank)), - color = 0xFFD700 - } - message:reply{embed = embed} -end - -function commands.daily(message) - local guild = message.guild - if not guild then return end - - local userData = db:getUser(guild.id, message.author.id) - local now = os.time() - local dayInSeconds = 86400 - - if now - userData.lastDaily < dayInSeconds then - local timeLeft = dayInSeconds - (now - userData.lastDaily) - local hoursLeft = math.floor(timeLeft / 3600) - message:reply(string.format('ā³ You already claimed your daily! Come back in %d hours.', hoursLeft)) - return - end - - local reward = 100 - userData.coins = userData.coins + reward - userData.lastDaily = now - db:setUser(guild.id, message.author.id, userData) - - message:reply(string.format('āœ… You claimed your daily reward of **%s** coins!\nšŸ’° New balance: **%s** coins', - formatNumber(reward), formatNumber(userData.coins))) -end - -function commands.work(message) - local guild = message.guild - if not guild then return end - - local userData = db:getUser(guild.id, message.author.id) - local now = os.time() - - if now - userData.lastWork < 3600 then - local timeLeft = 3600 - (now - userData.lastWork) - local minutesLeft = math.floor(timeLeft / 60) - message:reply(string.format('ā³ You need to rest! Come back in %d minutes.', minutesLeft)) - return - end - - local earnings = math.random(10, 50) - userData.coins = userData.coins + earnings - userData.lastWork = now - db:setUser(guild.id, message.author.id, userData) - - local jobs = { - 'You worked as a programmer and earned', - 'You delivered pizza and earned', - 'You streamed on Twitch and earned', - 'You mowed lawns and earned', - 'You washed cars and earned', - 'You walked dogs and earned', - 'You tutored students and earned' - } - - local job = jobs[math.random(#jobs)] - message:reply(string.format('šŸ’¼ %s **%s** coins!\nšŸ’° New balance: **%s** coins', - job, formatNumber(earnings), formatNumber(userData.coins))) -end - -function commands.give(message, args) - local guild = message.guild - if not guild then return end - - local targetUser = message.mentionedUsers.first - if not targetUser then - message:reply('āŒ Please mention a user to give coins to!') - return - end - - if targetUser.id == message.author.id then - message:reply('āŒ You cannot give coins to yourself!') - return - end - - local amount = tonumber(args[2]) - if not amount or amount <= 0 then - message:reply('āŒ Please specify a valid amount!') - return - end - - local senderData = db:getUser(guild.id, message.author.id) - if senderData.coins < amount then - message:reply('āŒ You don\'t have enough coins!') - return - end - - local receiverData = db:getUser(guild.id, targetUser.id) - senderData.coins = senderData.coins - amount - receiverData.coins = receiverData.coins + amount - - db:setUser(guild.id, message.author.id, senderData) - db:setUser(guild.id, targetUser.id, receiverData) - - message:reply(string.format('āœ… You gave **%s** coins to %s!', formatNumber(amount), targetUser.mentionString)) -end - --- ============================================ --- FUN COMMANDS --- ============================================ - -function commands['8ball'](message, args) - if #args == 0 then - message:reply('āŒ Please ask a question!') - return - end - - local responses = { - 'Yes, definitely!', 'It is certain.', 'Without a doubt.', 'You may rely on it.', - 'As I see it, yes.', 'Most likely.', 'Outlook good.', 'Signs point to yes.', - 'Reply hazy, try again.', 'Ask again later.', 'Better not tell you now.', - 'Cannot predict now.', 'Concentrate and ask again.', "Don't count on it.", - 'My reply is no.', 'My sources say no.', 'Outlook not so good.', 'Very doubtful.' - } - - local answer = responses[math.random(#responses)] - message:reply(string.format('šŸ”® %s', answer)) -end - -function commands.roll(message, args) - local sides = tonumber(args[1]) or 6 - if sides < 2 or sides > 100 then - message:reply('āŒ Dice must have between 2 and 100 sides!') - return - end - - local result = math.random(1, sides) - message:reply(string.format('šŸŽ² You rolled a **%d** (1-%d)', result, sides)) -end - -function commands.coinflip(message) - local result = math.random(2) == 1 and 'Heads' or 'Tails' - message:reply(string.format('šŸŖ™ The coin landed on **%s**!', result)) -end - -function commands.kitty(message) - http.request('GET', 'https://api.thecatapi.com/v1/images/search', {}, function(data, body) - local success, decoded = pcall(json.decode, body) - if success and decoded[1] then - local embed = { - title = '🐱 Random Kitty!', - color = 0xFF69B4, - image = {url = decoded[1].url}, - footer = {text = 'Requested by ' .. message.author.username} - } - message:reply{embed = embed} - else - message:reply('Failed to fetch a cat picture 😿') - end - end) -end - -function commands.doggy(message) - http.request('GET', 'https://api.thedogapi.com/v1/images/search', {}, function(data, body) - local success, decoded = pcall(json.decode, body) - if success and decoded[1] then - local embed = { - title = '🐶 Random Doggy!', - color = 0xFF69B4, - image = {url = decoded[1].url}, - footer = {text = 'Requested by ' .. message.author.username} - } - message:reply{embed = embed} - else - message:reply('Failed to fetch a dog picture 😄') - end - end) -end - -function commands.joke(message) - http.request('GET', 'https://official-joke-api.appspot.com/random_joke', {}, function(data, body) - local success, decoded = pcall(json.decode, body) - if success and decoded.setup then - local embed = { - title = 'šŸ˜‚ Random Joke', - description = string.format('**%s**\n\n||%s||', decoded.setup, decoded.punchline), - color = 0xFFA500, - footer = {text = (decoded.type or 'general') .. ' joke'} - } - message:reply{embed = embed} - else - local jokes = { - {setup = 'Why did the scarecrow win an award?', punchline = 'Because he was outstanding in his field!'}, - {setup = "Why don't scientists trust atoms?", punchline = 'Because they make up everything!'}, - {setup = 'What do you call a fake noodle?', punchline = 'An impasta!'} - } - local j = jokes[math.random(#jokes)] - local embed = { - title = 'šŸ˜‚ Random Joke', - description = string.format('**%s**\n\n||%s||', j.setup, j.punchline), - color = 0xFFA500 - } - message:reply{embed = embed} - end - end) -end - --- ============================================ --- MODERATION COMMANDS --- ============================================ - -function commands.timeout(message, args) - if not message.member:hasPermission('administrator') then - message:reply('āŒ You need administrator permission!') - return - end - - local targetUser = message.mentionedUsers.first - if not targetUser then - message:reply('āŒ Please mention a user to timeout!') - return - end - - local reason = table.concat(args, ' ', 2) or 'No reason provided' - - -- Implementation would store removed roles and assign timeout role - message:reply(string.format('āøļø %s has been timed out. Reason: %s', targetUser.mentionString, reason)) -end - -function commands.mute(message, args) - if not message.member:hasPermission('administrator') then - message:reply('āŒ You need administrator permission!') - return - end - - local targetUser = message.mentionedUsers.first - if not targetUser then - message:reply('āŒ Please mention a user to mute!') - return - end - - message:reply(string.format('šŸ”‡ %s has been muted.', targetUser.mentionString)) -end - -function commands.kick(message, args) - if not message.member:hasPermission('administrator') then - message:reply('āŒ You need administrator permission!') - return - end - - local targetUser = message.mentionedUsers.first - if not targetUser then - message:reply('āŒ Please mention a user to kick!') - return - end - - local reason = table.concat(args, ' ', 2) or 'No reason provided' - message.guild:kickUser(targetUser.id, reason) - message:reply(string.format('šŸ‘¢ %s has been kicked. Reason: %s', targetUser.mentionString, reason)) -end - --- ============================================ --- YOUTUBE COMMANDS --- ============================================ - -function commands.togglenotif(message) - if not message.member:hasPermission('manageGuild') then - message:reply('āŒ You need Manage Server permission!') - return - end - - local guild = message.guild - local guildData = db:getGuild(guild.id) - guildData.notificationsEnabled = not guildData.notificationsEnabled - db:setGuild(guild.id, guildData) - - local status = guildData.notificationsEnabled and 'enabled āœ…' or 'disabled āŒ' - local embed = { - title = 'šŸ”” Notification Settings', - description = 'YouTube notifications are now **' .. status .. '**', - color = guildData.notificationsEnabled and 0xFF69B4 or 0x808080 - } - message:reply{embed = embed} -end - -function commands.notifstatus(message) - local guild = message.guild - if not guild then return end - - local guildData = db:getGuild(guild.id) - local status = guildData.notificationsEnabled and 'enabled āœ…' or 'disabled āŒ' - - local embed = { - title = 'šŸ”” Notification Status', - description = 'YouTube notifications are currently **' .. status .. '**', - color = guildData.notificationsEnabled and 0xFF69B4 or 0x808080 - } - message:reply{embed = embed} -end - --- ============================================ --- EVENT HANDLERS --- ============================================ - -client:on('ready', function() - print('āœ… Logged in as ' .. client.user.username) - print('šŸ“Š Connected to ' .. #client.guilds .. ' guilds') - client:setGame(config.prefix .. 'help | Tooly Bot') - print('šŸš€ All systems operational!') - - -- Start auto-save timer - timer.setInterval(config.autosave_interval * 1000, function() - db:save() - print('šŸ’¾ Data autosaved') - end) -end) - -client:on('messageCreate', function(message) - if message.author.bot then return end - - local guild = message.guild - if guild then - -- XP System - local userData = db:getUser(guild.id, message.author.id) - local now = os.time() - - if now - userData.lastMessage >= config.xp_cooldown then - userData.lastMessage = now - local xpGain = math.random(config.xp_min, config.xp_max) - userData.xp = userData.xp + xpGain - local xpNeeded = userData.level * config.xp_per_level - - if userData.xp >= xpNeeded then - userData.level = userData.level + 1 - userData.xp = 0 - - local messages = { - 'šŸŽ‰ GG %s! You leveled up to **Level %d**!', - '⭐ Congrats %s! You\'re now **Level %d**!', - 'šŸš€ Level up! %s reached **Level %d**!', - 'šŸ’« Awesome! %s is now **Level %d**!' - } - - local coinReward = userData.level * config.level_up_multiplier - userData.coins = userData.coins + coinReward - - local msg = messages[math.random(#messages)] - message.channel:send(string.format(msg .. ' You earned **%s coins**! šŸ’°', - message.author.mentionString, userData.level, formatNumber(coinReward))) - end - - db:setUser(guild.id, message.author.id, userData) - end - end - - -- Command Handler - if not message.content:match('^' .. config.prefix) then return end - - local content = message.content:sub(#config.prefix + 1) - local args = {} - for word in content:gmatch('%S+') do - table.insert(args, word) - end - - local commandName = table.remove(args, 1):lower() - - if commands[commandName] then - local success, err = pcall(commands[commandName], message, args) - if not success then - print('āŒ Error in command ' .. commandName .. ': ' .. tostring(err)) - message:reply('āŒ An error occurred while executing this command.') - end - end -end) - -client:on('error', function(err) - print('āŒ Error: ' .. tostring(err)) -end) - --- Start the bot -if not config.token then - print('āŒ DISCORD_TOKEN environment variable not set!') - os.exit(1) -end - -print('šŸš€ Starting Tooly Bot...') -client:run('Bot ' .. config.token) \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..702d3ac --- /dev/null +++ b/bot.py @@ -0,0 +1,305 @@ +import discord +from discord.ext import commands +import os +import json +import random +import asyncio +from datetime import datetime +import aiohttp +import logging + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('bot') + +# Configuration +CONFIG = { + 'prefix': '!', + 'xp_min': 15, + 'xp_max': 25, + 'xp_cooldown': 60, + 'xp_per_level': 100, + 'level_up_multiplier': 10, + 'data_file': 'data.json' +} + +# Simple JSON Database +class SimpleDB: + def __init__(self, filename): + self.filename = filename + self.data = self.load() + + def load(self): + if os.path.exists(self.filename): + try: + with open(self.filename, 'r') as f: + return json.load(f) + except: + return {'users': {}, 'guilds': {}} + return {'users': {}, 'guilds': {}} + + def save(self): + with open(self.filename, 'w') as f: + json.dump(self.data, f, indent=2) + + def get_user(self, guild_id, user_id): + key = f"{guild_id}_{user_id}" + if key not in self.data['users']: + self.data['users'][key] = { + 'coins': 0, + 'bank': 0, + 'level': 1, + 'xp': 0, + 'last_message': 0, + 'last_daily': 0, + 'last_work': 0 + } + return self.data['users'][key] + + def set_user(self, guild_id, user_id, data): + key = f"{guild_id}_{user_id}" + self.data['users'][key] = data + self.save() + + def get_all_guild_users(self, guild_id): + users = [] + for key, data in self.data['users'].items(): + if key.startswith(f"{guild_id}_"): + user_id = key.split('_')[1] + users.append({'user_id': user_id, 'data': data}) + users.sort(key=lambda x: (x['data']['level'], x['data']['xp']), reverse=True) + return users + +# Initialize +intents = discord.Intents.default() +intents.message_content = True +intents.members = True + +bot = commands.Bot(command_prefix=CONFIG['prefix'], intents=intents) +db = SimpleDB(CONFIG['data_file']) + +# Events +@bot.event +async def on_ready(): + logger.info(f'āœ… Logged in as {bot.user}') + logger.info(f'šŸ“Š Connected to {len(bot.guilds)} guilds') + await bot.change_presence(activity=discord.Game(name=f"{CONFIG['prefix']}help")) + logger.info('šŸš€ All systems operational!') + +@bot.event +async def on_message(message): + if message.author.bot or not message.guild: + return + + # XP System + user_data = db.get_user(str(message.guild.id), str(message.author.id)) + now = datetime.now().timestamp() + + if now - user_data['last_message'] >= CONFIG['xp_cooldown']: + user_data['last_message'] = now + xp_gain = random.randint(CONFIG['xp_min'], CONFIG['xp_max']) + user_data['xp'] += xp_gain + xp_needed = user_data['level'] * CONFIG['xp_per_level'] + + if user_data['xp'] >= xp_needed: + user_data['level'] += 1 + user_data['xp'] = 0 + + coin_reward = user_data['level'] * CONFIG['level_up_multiplier'] + user_data['coins'] += coin_reward + + messages = [ + f'šŸŽ‰ GG {message.author.mention}! You leveled up to **Level {user_data["level"]}**!', + f'⭐ Congrats {message.author.mention}! You\'re now **Level {user_data["level"]}**!', + f'šŸš€ Level up! {message.author.mention} reached **Level {user_data["level"]}**!' + ] + + await message.channel.send( + f'{random.choice(messages)} You earned **{coin_reward:,} coins**! šŸ’°' + ) + + db.set_user(str(message.guild.id), str(message.author.id), user_data) + + await bot.process_commands(message) + +# Commands +@bot.command(name='help') +async def help_command(ctx): + embed = discord.Embed( + title='šŸ¤– Bot Commands', + color=0x5865F2 + ) + embed.add_field( + name='šŸ“Š Leveling', + value='`!rank [@user]` - View rank\n`!leaderboard` - Top 10 users', + inline=False + ) + embed.add_field( + name='šŸ’° Economy', + value='`!balance [@user]` - Check balance\n`!daily` - Daily reward\n`!work` - Work for coins', + inline=False + ) + embed.add_field( + name='šŸŽ® Fun', + value='`!8ball ` - Magic 8ball\n`!roll [sides]` - Roll dice\n`!flip` - Flip coin\n`!cat` - Random cat\n`!dog` - Random dog', + inline=False + ) + await ctx.send(embed=embed) + +@bot.command(name='ping') +async def ping(ctx): + latency = round(bot.latency * 1000) + await ctx.send(f'šŸ“ Pong! Latency: `{latency}ms`') + +@bot.command(name='rank') +async def rank(ctx, member: discord.Member = None): + target = member or ctx.author + user_data = db.get_user(str(ctx.guild.id), str(target.id)) + xp_needed = user_data['level'] * CONFIG['xp_per_level'] + + all_users = db.get_all_guild_users(str(ctx.guild.id)) + rank = next((i + 1 for i, u in enumerate(all_users) if u['user_id'] == str(target.id)), 'Unranked') + + progress = int((user_data['xp'] / xp_needed) * 20) if xp_needed > 0 else 0 + bar = 'ā–ˆ' * progress + 'ā–‘' * (20 - progress) + + embed = discord.Embed(color=0x4D96FF) + embed.set_author(name=f"{target.display_name}'s Profile", icon_url=target.display_avatar.url) + embed.description = f""" +**RANK** • #{rank} / {len(all_users)} +**LEVEL** • {user_data['level']} +**XP** • {user_data['xp']:,} / {xp_needed:,} + +`{bar}` + +**šŸ’° BALANCE** • {user_data['coins']:,} coins + """ + await ctx.send(embed=embed) + +@bot.command(name='leaderboard', aliases=['lb']) +async def leaderboard(ctx): + all_users = db.get_all_guild_users(str(ctx.guild.id))[:10] + + description = [] + for i, u in enumerate(all_users): + medal = 'šŸ„‡' if i == 0 else '🄈' if i == 1 else 'šŸ„‰' if i == 2 else f'**{i+1}.**' + description.append( + f'{medal} <@{u["user_id"]}>\n' + f'ā”” Level {u["data"]["level"]} ({u["data"]["xp"]:,} XP) • {u["data"]["coins"]:,} coins' + ) + + embed = discord.Embed( + title='šŸ† Server Leaderboard', + description='\n'.join(description) if description else 'No users yet!', + color=0x9B59B6 + ) + await ctx.send(embed=embed) + +@bot.command(name='balance', aliases=['bal']) +async def balance(ctx, member: discord.Member = None): + target = member or ctx.author + user_data = db.get_user(str(ctx.guild.id), str(target.id)) + + embed = discord.Embed( + title='šŸ’° Balance', + description=f'{target.mention} has **{user_data["coins"]:,}** coins!', + color=0xFFD700 + ) + await ctx.send(embed=embed) + +@bot.command(name='daily') +async def daily(ctx): + user_data = db.get_user(str(ctx.guild.id), str(ctx.author.id)) + now = datetime.now().timestamp() + + if now - user_data['last_daily'] < 86400: + time_left = 86400 - (now - user_data['last_daily']) + hours = int(time_left / 3600) + await ctx.send(f'ā³ You already claimed your daily! Come back in {hours} hours.') + return + + reward = 100 + user_data['coins'] += reward + user_data['last_daily'] = now + db.set_user(str(ctx.guild.id), str(ctx.author.id), user_data) + + await ctx.send(f'āœ… You claimed your daily reward of **{reward:,}** coins!\nšŸ’° New balance: **{user_data["coins"]:,}** coins') + +@bot.command(name='work') +async def work(ctx): + user_data = db.get_user(str(ctx.guild.id), str(ctx.author.id)) + now = datetime.now().timestamp() + + if now - user_data['last_work'] < 3600: + time_left = 3600 - (now - user_data['last_work']) + minutes = int(time_left / 60) + await ctx.send(f'ā³ You need to rest! Come back in {minutes} minutes.') + return + + earnings = random.randint(10, 50) + user_data['coins'] += earnings + user_data['last_work'] = now + db.set_user(str(ctx.guild.id), str(ctx.author.id), user_data) + + jobs = [ + 'You worked as a programmer and earned', + 'You delivered pizza and earned', + 'You streamed on Twitch and earned' + ] + + await ctx.send(f'šŸ’¼ {random.choice(jobs)} **{earnings:,}** coins!\nšŸ’° New balance: **{user_data["coins"]:,}** coins') + +@bot.command(name='8ball') +async def eightball(ctx, *, question): + responses = [ + 'Yes, definitely!', 'No way!', 'Maybe...', 'Ask again later', + 'Absolutely!', 'I doubt it', 'Signs point to yes', 'Very doubtful' + ] + await ctx.send(f'šŸ”® {random.choice(responses)}') + +@bot.command(name='roll') +async def roll(ctx, sides: int = 6): + if sides < 2 or sides > 100: + await ctx.send('āŒ Dice must have between 2 and 100 sides!') + return + result = random.randint(1, sides) + await ctx.send(f'šŸŽ² You rolled a **{result}** (1-{sides})') + +@bot.command(name='flip') +async def flip(ctx): + result = random.choice(['Heads', 'Tails']) + await ctx.send(f'šŸŖ™ The coin landed on **{result}**!') + +@bot.command(name='cat') +async def cat(ctx): + async with aiohttp.ClientSession() as session: + try: + async with session.get('https://api.thecatapi.com/v1/images/search') as resp: + data = await resp.json() + embed = discord.Embed(title='🐱 Random Kitty!', color=0xFF69B4) + embed.set_image(url=data[0]['url']) + await ctx.send(embed=embed) + except: + await ctx.send('Failed to fetch cat 😿') + +@bot.command(name='dog') +async def dog(ctx): + async with aiohttp.ClientSession() as session: + try: + async with session.get('https://api.thedogapi.com/v1/images/search') as resp: + data = await resp.json() + embed = discord.Embed(title='🐶 Random Doggy!', color=0xFF69B4) + embed.set_image(url=data[0]['url']) + await ctx.send(embed=embed) + except: + await ctx.send('Failed to fetch dog 😄') + +# Run bot +if __name__ == '__main__': + token = os.getenv('DISCORD_TOKEN') + if not token: + logger.error('āŒ DISCORD_TOKEN not set!') + exit(1) + + logger.info('šŸš€ Starting bot...') + bot.run(token) \ No newline at end of file diff --git a/package.lua b/package.lua deleted file mode 100644 index 2aecb81..0000000 --- a/package.lua +++ /dev/null @@ -1,17 +0,0 @@ -return { - name = "tooly-bot", - version = "1.0.0", - description = "Discord bot written in Lua", - tags = { "discord", "bot" }, - license = "MIT", - author = { name = "Your Name" }, - homepage = "https://github.com/chersbobers/ToolyBot", - dependencies = { - "SinisterRectus/discordia@2.11.1", - "luvit/secure-socket@1.2.3", - "creationix/coro-http@3.2.3" - }, - files = { - "**.lua" - } -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..185cdab --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +discord.py==2.3.2 +aiohttp==3.9.1 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/start.sh b/start.sh deleted file mode 100644 index 02bc41d..0000000 --- a/start.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -echo "šŸš€ Starting Tooly Bot..." - -# Install dependencies -lit install - -# Run the bot -luvit bot.lua \ No newline at end of file