diff --git a/.gitignore b/.gitignore index 372138e..46c9d51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,4 @@ -# Data files data.json timeouts.json - -# Environment files .env - -# Luvit files -deps/ -lit-* - -# Logs *.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 67dbde4..a7df482 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,5 +7,6 @@ RUN pip install --no-cache-dir -r requirements.txt COPY bot.py . COPY cogs/ ./cogs/ +COPY config.toml . CMD ["python", "bot.py"] \ No newline at end of file diff --git a/README.md b/README.md index cf5060b..1324688 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,29 @@
- Booly - discord.py python Made by Chersbobers :3 -
# 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. - Fork freely just credit booly # Installation - there is 2 methods of install: 1. prehosted 100 servers limit servers can be slow and updates are tested there. 2. self host it ## 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. ## 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) ### 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) - Also a discord bot with Presence Intent, Server Members Intent and Message Content Intent Envs: @@ -79,6 +70,12 @@ For the invite link it just needs bot and applications.commands - `/youtubestatus` - Check YouTube notification status - `/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 - `/ping` - Check bot latency - `/serverinfo` - Get information about the server diff --git a/bot.py b/bot.py index 17a6cc8..8d8d111 100644 --- a/bot.py +++ b/bot.py @@ -3,21 +3,25 @@ from discord.ext import commands import os import json import logging +import tomllib from aiohttp import web import asyncio logging.basicConfig(level=logging.INFO) logger = logging.getLogger('bot') -CONFIG = { - 'xp_min': 15, - 'xp_max': 25, - 'xp_cooldown': 60, - 'xp_per_level': 100, - 'level_up_multiplier': 10, - 'data_file': 'data.json', - 'video_check_interval': 300 -} +def load_config(): + base_path = os.path.dirname(__file__) + config_path = os.path.join(base_path, "config.toml") + try: + print(f"Loading config from: {config_path}") + with open(config_path, "rb") as f: + return tomllib.load(f) + except FileNotFoundError: + logger.error("config.toml missing.") + exit(1) + +CONFIG = load_config() class SimpleDB: def __init__(self, filename): @@ -41,13 +45,8 @@ class SimpleDB: 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 + 'coins': 0, 'bank': 0, 'level': 1, 'xp': 0, + 'last_message': 0, 'last_daily': 0, 'last_work': 0 } return self.data['users'][key] @@ -55,15 +54,6 @@ class SimpleDB: 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 class MyBot(commands.Bot): def __init__(self): @@ -72,85 +62,57 @@ class MyBot(commands.Bot): intents.members = True super().__init__(command_prefix='/', intents=intents) - self.db = SimpleDB(CONFIG['data_file']) + self.db = SimpleDB(CONFIG['bot']['data_file']) self.config = CONFIG async def setup_hook(self): - await self.load_extension('cogs.leveling') - await self.load_extension('cogs.system') - await self.load_extension('cogs.economy') - await self.load_extension('cogs.fun') + for extension in self.config['bot']['enabled_cogs']: + try: + await self.load_extension(extension) + logger.info(f'Loaded: {extension}') + except Exception as e: + logger.error(f'Error {extension}: {e}') await self.tree.sync() - logger.info('All cogs loaded and commands synced!') bot = MyBot() async def health_check(request): 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(): app = web.Application() app.router.add_get('/', health_check) app.router.add_get('/health', health_check) - + app.router.add_get('/{code}', redirect_handler) runner = web.AppRunner(app) await runner.setup() - port = int(os.getenv('PORT', 8080)) site = web.TCPSite(runner, '0.0.0.0', port) await site.start() - logger.info(f'Health check server running on port {port}') @bot.event async def on_ready(): asyncio.create_task(start_web_server()) - 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="/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) + await bot.change_presence(activity=discord.Game(name="Commands")) if __name__ == '__main__': token = os.getenv('DISCORD_TOKEN') if not token: - logger.error('DISCORD_TOKEN not set!') + logger.error('DISCORD_TOKEN not set') exit(1) - - logger.info("everythings working") - logger.info('hello from chersbobers and booly co :3') bot.run(token) \ No newline at end of file diff --git a/cogs/utility.py b/cogs/utility.py new file mode 100644 index 0000000..0bc134d --- /dev/null +++ b/cogs/utility.py @@ -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)) \ No newline at end of file diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..442c07f --- /dev/null +++ b/config.toml @@ -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 \ No newline at end of file diff --git a/render.yaml b/render.yaml index 39aaae1..5887299 100644 --- a/render.yaml +++ b/render.yaml @@ -1,5 +1,5 @@ services: - - type: worker + - type: web name: tooly-bot runtime: docker plan: free