New stable update from 1.0.0 to 1.1.0

New stable update. from 1.0.0 to 1.1.0
This commit is contained in:
chersbobers 2026-02-01 21:40:05 +13:00 committed by GitHub
commit b75a2d765c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 213 additions and 96 deletions

9
.gitignore vendored
View file

@ -1,13 +1,4 @@
# Data files
data.json data.json
timeouts.json timeouts.json
# Environment files
.env .env
# Luvit files
deps/
lit-*
# Logs
*.log *.log

View file

@ -7,5 +7,6 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py . COPY bot.py .
COPY cogs/ ./cogs/ COPY cogs/ ./cogs/
COPY config.toml .
CMD ["python", "bot.py"] CMD ["python", "bot.py"]

View file

@ -1,38 +1,29 @@
<div align="center"> <div align="center">
<img src="https://files.catbox.moe/zuj0ob.png" alt="Booly" width="100%"> <img src="https://files.catbox.moe/zuj0ob.png" alt="Booly" width="100%">
<img src="https://img.shields.io/badge/discord.py-2.0+-5865F2?style=flat&logo=discord&logoColor=white" alt="discord.py"> <img src="https://img.shields.io/badge/discord.py-2.0+-5865F2?style=flat&logo=discord&logoColor=white" alt="discord.py">
<img src="https://img.shields.io/badge/Python-3.8+-3776AB?style=flat&logo=python&logoColor=white" alt="python"> <img src="https://img.shields.io/badge/Python-3.8+-3776AB?style=flat&logo=python&logoColor=white" alt="python">
<img src="https://img.shields.io/badge/made%20by-chersbobers%20%3A3-FF69B4?style=flat&labelColor=555555" alt="Made by Chersbobers :3"> <img src="https://img.shields.io/badge/made%20by-chersbobers%20%3A3-FF69B4?style=flat&labelColor=555555" alt="Made by Chersbobers :3">
</div> </div>
# Overview # Overview
Booly bot (or better tooly) is a fork of my original discord bot tooly bot this is a updated fork that is easier to develop and use. Booly bot (or better tooly) is a fork of my original discord bot tooly bot this is a updated fork that is easier to develop and use.
Fork freely just credit booly Fork freely just credit booly
# Installation # Installation
there is 2 methods of install: there is 2 methods of install:
1. prehosted 100 servers limit servers can be slow and updates are tested there. 1. prehosted 100 servers limit servers can be slow and updates are tested there.
2. self host it 2. self host it
## Prehosted ## Prehosted
[__Click me for invite__](__https://discord.com/oauth2/authorize?client_id=1466398693383995558&permissions=8&integration_type=0&scope=bot+applications.commands__)
[Click me for invite](https://discord.com/oauth2/authorize?client_id=1466398693383995558&permissions=8&integration_type=0&scope=bot+applications.commands)
notes: again servers will most likely be slow and only 100 servers at a time if it reaches over a 100 servers I might host another one links will be updated. notes: again servers will most likely be slow and only 100 servers at a time if it reaches over a 100 servers I might host another one links will be updated.
## Self hosting ## Self hosting
I recommended using the main stable repo (https://github.com/chersbobers/booly) for yours but the nightly branch is usable too (https://github.com/chersbobers/booly/tree/nightly) I recommended using the main stable repo (https://github.com/chersbobers/booly) for yours but the nightly branch is usable too (https://github.com/chersbobers/booly/tree/nightly)
### what you need ### what you need
A server I use render because im broke with uptimerobot (note: Oregen servers are ip banned for me they might not be for you) A server I use render because im broke with uptimerobot (note: Oregen servers are ip banned for me they might not be for you)
Also a discord bot with Presence Intent, Server Members Intent and Message Content Intent Also a discord bot with Presence Intent, Server Members Intent and Message Content Intent
Envs: Envs:
@ -79,6 +70,12 @@ For the invite link it just needs bot and applications.commands
- `/youtubestatus` - Check YouTube notification status - `/youtubestatus` - Check YouTube notification status
- `/testlastvideo` - Test the last video notification - `/testlastvideo` - Test the last video notification
## Utility
- `/shorten` - Shorten a long URL with optional custom code
- `/expand` - Get the original URL from a short code
- `/listshort` - List all shortened URLs in the server
- `/deleteshort` - Delete a shortened URL by code
## Info Commands ## Info Commands
- `/ping` - Check bot latency - `/ping` - Check bot latency
- `/serverinfo` - Get information about the server - `/serverinfo` - Get information about the server

114
bot.py
View file

@ -3,21 +3,25 @@ from discord.ext import commands
import os import os
import json import json
import logging import logging
import tomllib
from aiohttp import web from aiohttp import web
import asyncio import asyncio
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('bot') logger = logging.getLogger('bot')
CONFIG = { def load_config():
'xp_min': 15, base_path = os.path.dirname(__file__)
'xp_max': 25, config_path = os.path.join(base_path, "config.toml")
'xp_cooldown': 60, try:
'xp_per_level': 100, print(f"Loading config from: {config_path}")
'level_up_multiplier': 10, with open(config_path, "rb") as f:
'data_file': 'data.json', return tomllib.load(f)
'video_check_interval': 300 except FileNotFoundError:
} logger.error("config.toml missing.")
exit(1)
CONFIG = load_config()
class SimpleDB: class SimpleDB:
def __init__(self, filename): def __init__(self, filename):
@ -41,13 +45,8 @@ class SimpleDB:
key = f"{guild_id}_{user_id}" key = f"{guild_id}_{user_id}"
if key not in self.data['users']: if key not in self.data['users']:
self.data['users'][key] = { self.data['users'][key] = {
'coins': 0, 'coins': 0, 'bank': 0, 'level': 1, 'xp': 0,
'bank': 0, 'last_message': 0, 'last_daily': 0, 'last_work': 0
'level': 1,
'xp': 0,
'last_message': 0,
'last_daily': 0,
'last_work': 0
} }
return self.data['users'][key] return self.data['users'][key]
@ -55,15 +54,6 @@ class SimpleDB:
key = f"{guild_id}_{user_id}" key = f"{guild_id}_{user_id}"
self.data['users'][key] = data self.data['users'][key] = data
self.save() 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
class MyBot(commands.Bot): class MyBot(commands.Bot):
def __init__(self): def __init__(self):
@ -72,85 +62,57 @@ class MyBot(commands.Bot):
intents.members = True intents.members = True
super().__init__(command_prefix='/', intents=intents) super().__init__(command_prefix='/', intents=intents)
self.db = SimpleDB(CONFIG['data_file']) self.db = SimpleDB(CONFIG['bot']['data_file'])
self.config = CONFIG self.config = CONFIG
async def setup_hook(self): async def setup_hook(self):
await self.load_extension('cogs.leveling') for extension in self.config['bot']['enabled_cogs']:
await self.load_extension('cogs.system') try:
await self.load_extension('cogs.economy') await self.load_extension(extension)
await self.load_extension('cogs.fun') logger.info(f'Loaded: {extension}')
except Exception as e:
logger.error(f'Error {extension}: {e}')
await self.tree.sync() await self.tree.sync()
logger.info('All cogs loaded and commands synced!')
bot = MyBot() bot = MyBot()
async def health_check(request): async def health_check(request):
return web.Response(text="Bot is running!") return web.Response(text="Bot is running!")
async def redirect_handler(request):
code = request.match_info.get('code', '')
if os.path.exists(CONFIG['bot']['data_file']):
try:
with open(CONFIG['bot']['data_file'], 'r') as f:
data = json.load(f)
for guild_id, guild_data in data.get('guilds', {}).items():
if 'urls' in guild_data and code in guild_data['urls']:
return web.Response(status=301, headers={'Location': guild_data['urls'][code]})
except Exception as e:
logger.error(f'Error: {e}')
return web.Response(text='Not Found', status=404)
async def start_web_server(): async def start_web_server():
app = web.Application() app = web.Application()
app.router.add_get('/', health_check) app.router.add_get('/', health_check)
app.router.add_get('/health', health_check) app.router.add_get('/health', health_check)
app.router.add_get('/{code}', redirect_handler)
runner = web.AppRunner(app) runner = web.AppRunner(app)
await runner.setup() await runner.setup()
port = int(os.getenv('PORT', 8080)) port = int(os.getenv('PORT', 8080))
site = web.TCPSite(runner, '0.0.0.0', port) site = web.TCPSite(runner, '0.0.0.0', port)
await site.start() await site.start()
logger.info(f'Health check server running on port {port}')
@bot.event @bot.event
async def on_ready(): async def on_ready():
asyncio.create_task(start_web_server()) asyncio.create_task(start_web_server())
logger.info(f'Logged in as {bot.user}') 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="Commands"))
await bot.change_presence(activity=discord.Game(name="/help"))
logger.info('All systems operational!')
@bot.tree.command(name='help', description='Show all available commands')
async def help_command(interaction: discord.Interaction):
embed = discord.Embed(
title='Bot Commands',
description='Here are all the slash commands you can use!',
color=0x5865F2
)
embed.add_field(
name='Leveling & Economy',
value='`/rank` - View your rank\n`/leaderboard` - Top 10 users\n`/balance` - Check balance\n`/daily` - Daily reward\n`/work` - Work for coins',
inline=False
)
embed.add_field(
name='Fun',
value='`/8ball` - Magic 8ball\n`/roll` - Roll dice\n`/flip` - Flip coin\n`/cat` - Random cat\n`/dog` - Random dog',
inline=False
)
embed.add_field(
name='System & Moderation',
value='`/kick` - Kick member\n`/ban` - Ban member\n`/unban` - Unban user\n`/timeout` - Timeout member\n`/warn` - Warn member\n`/warnings` - View warnings\n`/clearwarnings` - Clear warnings\n`/purge` - Delete messages\n`/lock` - Lock channel\n`/unlock` - Unlock channel',
inline=False
)
embed.add_field(
name='Reaction Roles & YouTube',
value='`/reactionrole` - Create reaction role\n`/removereactionrole` - Remove reaction role\n`/listreactionroles` - List reaction roles\n`/createreactionpanel` - Create panel\n`/setupyoutube` - Setup YouTube\n`/toggleyoutube` - Toggle YouTube\n`/youtubestatus` - YouTube status\n`/testlastvideo` - Test video',
inline=False
)
embed.add_field(
name='Info',
value='`/ping` - Bot latency\n`/serverinfo` - Server info\n`/userinfo` - User info',
inline=False
)
await interaction.response.send_message(embed=embed)
if __name__ == '__main__': if __name__ == '__main__':
token = os.getenv('DISCORD_TOKEN') token = os.getenv('DISCORD_TOKEN')
if not token: if not token:
logger.error('DISCORD_TOKEN not set!') logger.error('DISCORD_TOKEN not set')
exit(1) exit(1)
logger.info("everythings working")
logger.info('hello from chersbobers and booly co :3')
bot.run(token) bot.run(token)

143
cogs/utility.py Normal file
View file

@ -0,0 +1,143 @@
import discord
from discord.ext import commands
from discord import app_commands
import re
import random
import string
class Utility(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.domain = "u.chers.moe"
def is_valid_url(self, url):
url_pattern = re.compile(
r'^https?://'
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
r'localhost|'
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
r'(?::\d+)?'
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
return url_pattern.match(url) is not None
def generate_short_code(self, length=6):
chars = string.ascii_letters + string.digits
return ''.join(random.choice(chars) for _ in range(length))
def get_guild_data(self, guild_id):
if 'guilds' not in self.bot.db.data:
self.bot.db.data['guilds'] = {}
if str(guild_id) not in self.bot.db.data['guilds']:
self.bot.db.data['guilds'][str(guild_id)] = {'urls': {}}
if 'urls' not in self.bot.db.data['guilds'][str(guild_id)]:
self.bot.db.data['guilds'][str(guild_id)]['urls'] = {}
return self.bot.db.data['guilds'][str(guild_id)]
@app_commands.command(name='shorten', description='Shorten a long URL')
@app_commands.describe(
url='The URL you want to shorten',
code='Custom short code (optional)'
)
async def shorten_url(self, interaction: discord.Interaction, url: str, code: str = None):
if not self.is_valid_url(url):
await interaction.response.send_message(
'Please provide a valid URL',
ephemeral=True
)
return
guild_data = self.get_guild_data(interaction.guild_id)
for existing_code, stored_url in guild_data['urls'].items():
if stored_url == url:
shortened = f"https://{self.domain}/{existing_code}"
embed = discord.Embed(title='URL Already Shortened', color=0x5865F2)
embed.add_field(name='Original', value=url[:100] + ('...' if len(url) > 100 else ''), inline=False)
embed.add_field(name='Shortened', value=shortened, inline=False)
await interaction.response.send_message(embed=embed)
return
if code:
if code in guild_data['urls']:
await interaction.response.send_message(
'This short code is already taken',
ephemeral=True
)
return
else:
code = self.generate_short_code()
while code in guild_data['urls']:
code = self.generate_short_code()
guild_data['urls'][code] = url
self.bot.db.save()
shortened = f"https://{self.domain}/{code}"
embed = discord.Embed(title='URL Shortened', color=0x5865F2)
embed.add_field(name='Original', value=url[:100] + ('...' if len(url) > 100 else ''), inline=False)
embed.add_field(name='Shortened', value=shortened, inline=False)
embed.add_field(name='Code', value=code, inline=False)
await interaction.response.send_message(embed=embed)
@app_commands.command(name='expand', description='Get the original URL from a short code')
@app_commands.describe(code='The short code to expand')
async def expand_url(self, interaction: discord.Interaction, code: str):
guild_data = self.get_guild_data(interaction.guild_id)
if code in guild_data['urls']:
original_url = guild_data['urls'][code]
embed = discord.Embed(title='URL Expanded', color=0x5865F2)
embed.add_field(name='Code', value=code, inline=False)
embed.add_field(name='Original URL', value=original_url, inline=False)
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message('Short code not found', ephemeral=True)
@app_commands.command(name='listshort', description='List all shortened URLs')
async def list_short(self, interaction: discord.Interaction):
guild_data = self.get_guild_data(interaction.guild_id)
if not guild_data['urls']:
await interaction.response.send_message('No shortened URLs found', ephemeral=True)
return
embed = discord.Embed(title='Shortened URLs', color=0x5865F2)
count = 0
for code, url in guild_data['urls'].items():
if count >= 25:
break
shortened = f"https://{self.domain}/{code}"
embed.add_field(
name=code,
value=f"{url[:50]}{'...' if len(url) > 50 else ''}\n{shortened}",
inline=False
)
count += 1
if len(guild_data['urls']) > 25:
embed.set_footer(text=f'Showing 25 of {len(guild_data["urls"])} URLs')
await interaction.response.send_message(embed=embed)
@app_commands.command(name='deleteshort', description='Delete a shortened URL')
@app_commands.describe(code='The short code to delete')
async def delete_short(self, interaction: discord.Interaction, code: str):
guild_data = self.get_guild_data(interaction.guild_id)
if code in guild_data['urls']:
url = guild_data['urls'][code]
del guild_data['urls'][code]
self.bot.db.save()
embed = discord.Embed(title='URL Deleted', color=0x5865F2)
embed.add_field(name='Code', value=code, inline=False)
embed.add_field(name='Original URL', value=url, inline=False)
await interaction.response.send_message(embed=embed)
else:
await interaction.response.send_message('Short code not found', ephemeral=True)
async def setup(bot):
await bot.add_cog(Utility(bot))

23
config.toml Normal file
View file

@ -0,0 +1,23 @@
# this is the deafult config file
[bot]
data_file = "data.json"
# this allows you to pick your cogs if installing multiple
enabled_cogs = [
"cogs.leveling",
"cogs.system",
"cogs.economy",
"cogs.fun",
"cogs.utility"
]
# this might not even need a comment but xp
[xp]
min = 15
max = 25
cooldown = 60
per_level = 100
level_up_multiplier = 10
# port and how many times it checks for videos
[web]
video_check_interval = 300
port = 3000

View file

@ -1,5 +1,5 @@
services: services:
- type: worker - type: web
name: tooly-bot name: tooly-bot
runtime: docker runtime: docker
plan: free plan: free