import discord import json import re import os from urllib.parse import urlparse, parse_qs, urlencode, urlunparse from discord import app_commands from dotenv import load_dotenv load_dotenv() def load_json(filename): try: with open(filename, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return {} def save_json(data, filename): with open(filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=4) rules = load_json("url_rules.json") server_settings = load_json("server_settings.json") intents = discord.Intents.default() intents.message_content = True client = discord.Client(intents=intents) tree = app_commands.CommandTree(client) def clean_url(url): parsed_url = urlparse(url) providers = [p for p in rules["providers"].items() if p[0] != "globalRules"] + [ ("globalRules", rules["providers"].get("globalRules", {})) ] for _, data in providers: if re.match(data.get("urlPattern", ""), url): query_params = parse_qs(parsed_url.query, keep_blank_values=True) filtered_params = { k: v for k, v in query_params.items() if not any(re.fullmatch(rule, k) for rule in data.get("rules", [])) } cleaned_query = urlencode(filtered_params, doseq=True) return ( urlunparse(( parsed_url.scheme, parsed_url.netloc, parsed_url.path, parsed_url.params, cleaned_query, parsed_url.fragment, )) if cleaned_query != parsed_url.query else None ) return None @client.event async def on_ready(): await tree.sync() @client.event async def on_message(message): if message.author == client.user or server_settings.get( str(message.guild.id), {} ).get("disabled", False): return for word in message.content.split(): if word.startswith(("http://", "https://")) and (cleaned := clean_url(word)): await message.channel.send(f"Cleaned link: <{cleaned}>") break @tree.command( name="toggle", description="Toggle automatic link cleaning for this server (requires Manage Messages permission).", ) async def toggle(interaction: discord.Interaction): if not interaction.user.guild_permissions.manage_messages: await interaction.response.send_message( "You need 'Manage Messages' permission to use this command.", ephemeral=True ) return guild_id = str(interaction.guild.id) server_settings[guild_id] = { "disabled": not server_settings.get(guild_id, {}).get("disabled", False) } save_json(server_settings, "server_settings.json") await interaction.response.send_message( f"Link cleaning {'disabled' if server_settings[guild_id]['disabled'] else 'enabled'}." ) @tree.command( name="bomb", description="Cleans the first link found in the last 20 messages." ) async def bomb(interaction: discord.Interaction): async for msg in interaction.channel.history(limit=20): for word in msg.content.split(): if word.startswith(("http://", "https://")) and ( cleaned := clean_url(word) ): await interaction.response.send_message(f"Cleaned link: <{cleaned}>") return await interaction.response.send_message("No links found.") @tree.command( name="about", description="Explains the purpose of the bot and its privacy features.", ) async def about(interaction: discord.Interaction): await interaction.response.send_message( "This bot removes tracking parameters from links to improve privacy. Use `/toggle` to enable/disable auto-cleaning, `/bomb` to clean a recent link manually, and `/about` for info." ) TOKEN = os.getenv("DISCORD_BOT_TOKEN") client.run(TOKEN)