python again
This commit is contained in:
parent
4bd5bb4484
commit
4a06ff27c0
6 changed files with 316 additions and 775 deletions
40
Dockerfile
40
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"]
|
||||
# Run the bot
|
||||
CMD ["python", "bot.py"]
|
||||
718
app.lua
718
app.lua
|
|
@ -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 <amount>` - Give coins',
|
||||
inline = false
|
||||
},
|
||||
{
|
||||
name = '🎮 Fun',
|
||||
value = '`!8ball <question>` - 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 .. '<command> 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)
|
||||
305
bot.py
Normal file
305
bot.py
Normal file
|
|
@ -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 <question>` - 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)
|
||||
17
package.lua
17
package.lua
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
discord.py==2.3.2
|
||||
aiohttp==3.9.1
|
||||
python-dotenv==1.0.0
|
||||
8
start.sh
8
start.sh
|
|
@ -1,8 +0,0 @@
|
|||
#!/bin/bash
|
||||
echo "🚀 Starting Tooly Bot..."
|
||||
|
||||
# Install dependencies
|
||||
lit install
|
||||
|
||||
# Run the bot
|
||||
luvit bot.lua
|
||||
Loading…
Reference in a new issue