From bc339a2671463a63f2a9a306c2dec001989d7798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 04:22:36 +0900 Subject: [PATCH 01/28] Upload New File --- keys.env | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 keys.env diff --git a/keys.env b/keys.env new file mode 100644 index 0000000..43dd235 --- /dev/null +++ b/keys.env @@ -0,0 +1,7 @@ +SLIPSTREAM_OPENROUTER_KEY=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 +DISCORD_TOKEN="MTM2MTgwNTE3MjkxNzE0MTgzNQ.G1II_V.OhJx67VHlKHBj4gCWTTbQoI45T0rfL2gtk15vs" +AI1_API_KEY="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" +SERVICE_KEY="2359" +GENIUS_API_KEY = "spwrTZ3d4KGmczcxSVlgkH7q5b4CE0cDJ2EudX57dHJ74d5ce60IKCxJNQ8o4nJBcIelHd-nne5_6dv2Z-R9oA" +AI_API_KEY = "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" +SERP_API_KEY, = "134ecd1798cd4853cf70a4a4a055f9bcd3bda45e9731b66fe6a16748c3462e57" \ No newline at end of file From f329503cbc2c4630dc1a47fb4aa8b12ff759ce44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 04:24:02 +0900 Subject: [PATCH 02/28] Edit bot.py --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 83036cb..944ef09 100644 --- a/bot.py +++ b/bot.py @@ -32,7 +32,7 @@ sys.stderr = DualStream(sys.stderr, log_file) print("Logging started.") # Load environment variables -load_dotenv("/home/server/keys.env") +load_dotenv("keys.env") discord_token = os.getenv("DISCORD_TOKEN") # Ensure token is set From 0b6ce45d4f0b2441aabfeadffce73db6000c147f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 04:39:20 +0900 Subject: [PATCH 03/28] Add new file --- pip.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 pip.txt diff --git a/pip.txt b/pip.txt new file mode 100644 index 0000000..5de44a4 --- /dev/null +++ b/pip.txt @@ -0,0 +1 @@ +pip install aiohappyeyeballs==2.6.1 aiohttp==3.11.16 aiosignal==1.3.2 annotated-types==0.6.0 anyio==4.9.0 archspec==0.2.3 async-timeout==5.0.1 attrs==25.3.0 beautifulsoup4==4.13.4 blis==1.3.0 boltons==24.1.0 Brotli==1.1.0 catalogue==2.0.10 certifi==2025.1.31 cffi==1.17.1 charset-normalizer==3.3.2 ChatterBot==1.2.6 chatterbot-corpus==1.2.2 click==8.1.8 cloudpathlib==0.21.0 confection==0.1.5 cryptography==43.0.3 cymem==2.0.11 decorator==5.2.1 discord.py==2.5.2 distro==1.9.0 docx2pdf==0.1.8 et_xmlfile==2.0.0 fastapi==0.115.12 filelock==3.18.0 frozendict==2.4.2 frozenlist==1.5.0 fsspec==2025.3.2 GPUtil==1.4.0 greenlet==3.2.1 h11==0.14.0 hf-xet==1.1.1 httpcore==1.0.8 httpx==0.28.1 huggingface-hub==0.31.1 idna==3.7 imageio==2.37.0 imageio-ffmpeg==0.6.0 inflate64==1.0.1 inquirerpy==0.3.4 Jinja2==3.1.6 jiter==0.9.0 jsonpatch==1.33 jsonpointer==2.1 langcodes==3.5.0 language_data==1.3.0 lxml==5.4.0 lyricsgenius==3.6.2 marisa-trie==1.2.1 markdown-it-py==2.2.0 MarkupSafe==3.0.2 mathparse==0.1.5 mdurl==0.1.0 moviepy==2.1.2 mpmath==1.3.0 multidict==6.4.3 multivolumefile==0.2.3 murmurhash==1.0.12 networkx==3.4.2 numpy==2.2.5 nvidia-cublas-cu12==12.6.4.1 nvidia-cuda-cupti-cu12==12.6.80 nvidia-cuda-nvrtc-cu12==12.6.77 nvidia-cuda-runtime-cu12==12.6.77 nvidia-cudnn-cu12==9.5.1.17 nvidia-cufft-cu12==11.3.0.4 nvidia-cufile-cu12==1.11.1.6 nvidia-curand-cu12==10.3.7.77 nvidia-cusolver-cu12==11.7.1.2 nvidia-cusparse-cu12==12.5.4.2 nvidia-cusparselt-cu12==0.6.3 nvidia-nccl-cu12==2.26.2 nvidia-nvjitlink-cu12==12.6.85 nvidia-nvtx-cu12==12.6.77 openai==0.28.0 opencv-python==4.11.0.86 openpyxl==3.1.5 openrouter==1.0 packaging==24.2 pfzy==0.3.4 pillow==10.4.0 platformdirs==3.10.0 pluggy==1.5.0 preshed==3.0.9 proglog==0.1.12 prompt_toolkit==3.0.51 propcache==0.3.1 psutil==7.0.0 py7zr==0.22.0 pybcj==1.0.6 pycosat==0.6.6 pycparser==2.21 pycryptodomex==3.23.0 pydantic==2.10.3 pydantic_core==2.27.1 pydub==0.25.1 Pygments==2.15.1 PyNaCl==1.5.0 PyPDF2==3.0.1 pyppmd==1.1.1 PySocks==1.7.1 python-dateutil==2.9.0.post0 python-docx==1.1.2 python-dotenv==1.1.0 python-pptx==1.0.2 PyYAML==6.0.2 pyzstd==0.17.0 regex==2024.11.6 requests==2.32.3 rich==13.9.4 ruamel.yaml==0.18.6 ruamel.yaml.clib==0.2.8 rule34==1.8.1 safetensors==0.5.3 setuptools==75.8.0 shellingham==1.5.4 six==1.17.0 smart-open==7.1.0 sniffio==1.3.1 soupsieve==2.7 spacy==3.8.5 spacy-legacy==3.0.12 spacy-loggers==1.0.5 SQLAlchemy==2.0.40 srsly==2.5.1 starlette==0.46.2 sympy==1.14.0 texttable==1.7.0 thinc==8.3.6 tokenizers==0.21.1 torch==2.7.0 tqdm==4.67.1 transformers==4.51.3 triton==3.3.0 truststore==0.10.0 typer==0.15.2 typing_extensions==4.13.2 urllib3==2.3.0 uvicorn==0.34.2 wasabi==1.1.3 wcwidth==0.2.13 weasel==0.4.1 wheel==0.45.1 whois==1.20240129.2 wrapt==1.17.2 XlsxWriter==3.2.3 yarl==1.19.0 youtube-dl==2021.12.17 zstandard==0.23.0 \ No newline at end of file From 113e9969ddae8d693c11f355895c8947be843ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 04:50:56 +0900 Subject: [PATCH 04/28] Edit bot.py --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 944ef09..4db977b 100644 --- a/bot.py +++ b/bot.py @@ -98,7 +98,7 @@ def catch_exceptions(func): # Load cog files dynamically async def load_cogs(): - for filename in os.listdir("/home/server/wdiscordbotserver/cogs/"): + for filename in os.listdir("cogs"): if filename.endswith(".py"): try: await bot.load_extension(f"cogs.{filename[:-3]}") From dbab715239ccbb6b51451258fac5f6b18fca74c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 04:51:48 +0900 Subject: [PATCH 05/28] Delete serverconfig.py --- cogs/serverconfig.py | 161 ------------------------------------------- 1 file changed, 161 deletions(-) delete mode 100644 cogs/serverconfig.py diff --git a/cogs/serverconfig.py b/cogs/serverconfig.py deleted file mode 100644 index 25545c4..0000000 --- a/cogs/serverconfig.py +++ /dev/null @@ -1,161 +0,0 @@ -import discord -from discord.ext import commands -from discord import app_commands -import os -import json - -# Path to the JSON config file -CONFIG_FILE = "/home/server/serverconfig.json" - -def load_config() -> dict: - """Load the server configuration from file. - If the file does not exist or is invalid, create a new empty configuration.""" - if not os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE, "w") as f: - json.dump({}, f) - return {} - try: - with open(CONFIG_FILE, "r") as f: - return json.load(f) - except json.JSONDecodeError: - return {} - -def save_config(data: dict) -> None: - """Save the configuration JSON to file.""" - with open(CONFIG_FILE, "w") as f: - json.dump(data, f, indent=4) - -async def global_disabled_check(interaction: discord.Interaction) -> bool: - """ - Global check for all app (slash) commands. - If the command (except for serverconfig itself) is marked as disabled in this server’s config, - send an ephemeral message and prevent execution. - """ - # If interaction comes from a DM, allow it. - if interaction.guild is None: - return True - - # Always allow the serverconfig command so admins can change settings. - if interaction.command and interaction.command.name == "serverconfig": - return True - - config = load_config() - guild_id = str(interaction.guild.id) - disabled_commands = config.get(guild_id, []) - - if interaction.command and interaction.command.name in disabled_commands: - if not interaction.response.is_done(): - await interaction.response.send_message( - "This command has been disabled by server admins.", ephemeral=True - ) - # Raising a CheckFailure prevents the command from running. - raise app_commands.CheckFailure("Command disabled.") - return True - -class ServerConfigCog(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @app_commands.command( - name="serverconfig", - description="Enable or disable a command in this server." - ) - @app_commands.describe( - command="The name of the command to configure", - enabled="Type 'yes' to enable or 'no' to disable." - ) - async def serverconfig( - self, - interaction: discord.Interaction, - command: str, - enabled: str - ): - # Check if the user has admin permissions. - if not interaction.user.guild_permissions.administrator: - await interaction.response.send_message( - "You do not have permission to use this command.", - ephemeral=True - ) - return - - # Normalize the enabled flag. - enabled_flag = enabled.lower() - if enabled_flag not in ["yes", "no"]: - await interaction.response.send_message( - "Invalid 'enabled' option. Please use 'yes' or 'no'.", - ephemeral=True - ) - return - - # Verify that the provided command exists. - found = False - # Check the classic text commands. - for cmd in self.bot.commands: - if cmd.name == command: - found = True - break - # Also check application (slash) commands from the tree. - if not found: - for cmd in self.bot.tree.get_commands(): - if cmd.name == command: - found = True - break - if not found: - await interaction.response.send_message( - f"The command '{command}' was not found.", - ephemeral=True - ) - return - - # Load the configuration. - config = load_config() - guild_id = str(interaction.guild.id) - if guild_id not in config: - config[guild_id] = [] - - if enabled_flag == "no": - # Add the command to the disabled list if not already present. - if command not in config[guild_id]: - config[guild_id].append(command) - save_config(config) - await interaction.response.send_message( - f"Command '{command}' has been **disabled** in this server.", - ephemeral=True - ) - else: # enabled_flag == "yes" - # Remove the command from the disabled list if present. - if command in config[guild_id]: - config[guild_id].remove(command) - save_config(config) - await interaction.response.send_message( - f"Command '{command}' has been **enabled** in this server.", - ephemeral=True - ) - - @serverconfig.autocomplete("command") - async def command_autocomplete( - self, interaction: discord.Interaction, current: str - ) -> list[app_commands.Choice[str]]: - """ - Autocomplete for the 'command' parameter. - It searches both classic and slash commands for matches. - """ - choices = set() - # Get names of text commands. - for cmd in self.bot.commands: - choices.add(cmd.name) - # Get names of app commands. - for cmd in self.bot.tree.get_commands(): - choices.add(cmd.name) - # Filter and send at most 25 matching choices. - filtered = [ - app_commands.Choice(name=cmd, value=cmd) - for cmd in choices - if current.lower() in cmd.lower() - ] - return filtered[:25] - -async def setup(bot: commands.Bot): - # Register the global check – it will run for every application (slash) command. - bot.tree.interaction_check = global_disabled_check - await bot.add_cog(ServerConfigCog(bot)) From d5fbbf5074e2599239ceb0593318671476fc5fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 04:52:19 +0900 Subject: [PATCH 06/28] Edit randomgpu.py --- cogs/randomgpu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/randomgpu.py b/cogs/randomgpu.py index bbc0d88..1d63923 100644 --- a/cogs/randomgpu.py +++ b/cogs/randomgpu.py @@ -7,7 +7,7 @@ import random class GPU(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gpu_file = "/home/server/wdiscordbotserver/data/allgpus.json" + self.gpu_file = "allgpus.json" self.gpus = self.load_gpus() def load_gpus(self): From 1c4f0d3aac68cae3f7b0d185ba5b0d58bafee2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 04:53:07 +0900 Subject: [PATCH 07/28] Edit randomphone.py --- cogs/randomphone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/randomphone.py b/cogs/randomphone.py index dad6f1c..54cd1d7 100644 --- a/cogs/randomphone.py +++ b/cogs/randomphone.py @@ -12,7 +12,7 @@ class RandomPhoneCog(commands.Cog): def load_devices(self): try: - with open("/home/server/wdiscordbotserver/data/devices.json", "r") as f: + with open("devices.json", "r") as f: data = json.load(f) return data.get("RECORDS", []) except Exception as e: From 08247d89e4afb7d788f7803bd3136b242a581fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 05:40:11 +0900 Subject: [PATCH 08/28] Add new file --- setup.sh | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 setup.sh diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..bfa6029 --- /dev/null +++ b/setup.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# This script updates the system, installs required system packages, +# clones the Discord bot repository, sets up a Python virtual environment, +# installs Python dependencies, and finally runs the bot. + +# Exit immediately if a command exits with a non-zero status. +set -e + +echo "Updating package lists and installing system dependencies..." +sudo apt update +sudo apt install python3.12-venv -y +sudo apt-get install python3-pip -y + +echo "Cloning the Discord bot repository..." +git clone https://gitlab.com/pancakes1234/wdiscordbot-internal-server-aws.git + +echo "Setting up the Python virtual environment..." +python3 -m venv venv +# Activate the virtual environment so that subsequent pip installs are local +source venv/bin/activate + +echo "Installing Python dependencies..." +pip install \ + aiohappyeyeballs==2.6.1 \ + aiohttp==3.11.16 \ + aiosignal==1.3.2 \ + annotated-types==0.6.0 \ + anyio==4.9.0 \ + archspec==0.2.3 \ + async-timeout==5.0.1 \ + attrs==25.3.0 \ + beautifulsoup4==4.13.4 \ + blis==1.3.0 \ + boltons==24.1.0 \ + Brotli==1.1.0 \ + catalogue==2.0.10 \ + certifi==2025.1.31 \ + cffi==1.17.1 \ + charset-normalizer==3.3.2 \ + ChatterBot==1.2.6 \ + chatterbot-corpus==1.2.2 \ + click==8.1.8 \ + cloudpathlib==0.21.0 \ + confection==0.1.5 \ + cryptography==43.0.3 \ + cymem==2.0.11 \ + decorator==5.2.1 \ + discord.py==2.5.2 \ + distro==1.9.0 \ + docx2pdf==0.1.8 \ + et_xmlfile==2.0.0 \ + fastapi==0.115.12 \ + filelock==3.18.0 \ + frozendict==2.4.2 \ + frozenlist==1.5.0 \ + fsspec==2025.3.2 \ + GPUtil==1.4.0 \ + greenlet==3.2.1 \ + h11==0.14.0 \ + hf-xet==1.1.1 \ + httpcore==1.0.8 \ + httpx==0.28.1 \ + huggingface-hub==0.31.1 \ + idna==3.7 \ + imageio==2.37.0 \ + imageio-ffmpeg==0.6.0 \ + inflate64==1.0.1 \ + inquirerpy==0.3.4 \ + Jinja2==3.1.6 \ + jiter==0.9.0 \ + jsonpatch==1.33 \ + jsonpointer==2.1 \ + langcodes==3.5.0 \ + language_data==1.3.0 \ + lxml==5.4.0 \ + lyricsgenius==3.6.2 \ + marisa-trie==1.2.1 \ + markdown-it-py==2.2.0 \ + MarkupSafe==3.0.2 \ + mathparse==0.1.5 \ + mdurl==0.1.0 \ + moviepy==2.1.2 \ + mpmath==1.3.0 \ + multidict==6.4.3 \ + multivolumefile==0.2.3 \ + murmurhash==1.0.12 \ + networkx==3.4.2 \ + numpy==2.2.5 \ + nvidia-cublas-cu12==12.6.4.1 \ + nvidia-cuda-cupti-cu12==12.6.80 \ + nvidia-cuda-nvrtc-cu12==12.6.77 \ + nvidia-cuda-runtime-cu12==12.6.77 \ + nvidia-cudnn-cu12==9.5.1.17 \ + nvidia-cufft-cu12==11.3.0.4 \ + nvidia-cufile-cu12==1.11.1.6 \ + nvidia-curand-cu12==10.3.7.77 \ + nvidia-cusolver-cu12==11.7.1.2 \ + nvidia-cusparse-cu12==12.5.4.2 \ + nvidia-cusparselt-cu12==0.6.3 \ + nvidia-nccl-cu12==2.26.2 \ + nvidia-nvjitlink-cu12==12.6.85 \ + nvidia-nvtx-cu12==12.6.77 \ + openai==0.28.0 \ + opencv-python==4.11.0.86 \ + openpyxl==3.1.5 \ + openrouter==1.0 \ + packaging==24.2 \ + pfzy==0.3.4 \ + pillow==10.4.0 \ + platformdirs==3.10.0 \ + pluggy==1.5.0 \ + preshed==3.0.9 \ + proglog==0.1.12 \ + prompt_toolkit==3.0.51 \ + propcache==0.3.1 \ + psutil==7.0.0 \ + py7zr==0.22.0 \ + pybcj==1.0.6 \ + pycosat==0.6.6 \ + pycparser==2.21 \ + pycryptodomex==3.23.0 \ + pydantic==2.10.3 \ + pydantic_core==2.27.1 \ + pydub==0.25.1 \ + Pygments==2.15.1 \ + PyNaCl==1.5.0 \ + PyPDF2==3.0.1 \ + pyppmd==1.1.1 \ + PySocks==1.7.1 \ + python-dateutil==2.9.0.post0 \ + python-docx==1.1.2 \ + python-dotenv==1.1.0 \ + python-pptx==1.0.2 \ + PyYAML==6.0.2 \ + pyzstd==0.17.0 \ + regex==2024.11.6 \ + requests==2.32.3 \ + rich==13.9.4 \ + ruamel.yaml==0.18.6 \ + ruamel.yaml.clib==0.2.8 \ + rule34==1.8.1 \ + safetensors==0.5.3 \ + setuptools==75.8.0 \ + shellingham==1.5.4 \ + six==1.17.0 \ + smart-open==7.1.0 \ + sniffio==1.3.1 \ + soupsieve==2.7 \ + spacy==3.8.5 \ + spacy-legacy==3.0.12 \ + spacy-loggers==1.0.5 \ + SQLAlchemy==2.0.40 \ + srsly==2.5.1 \ + starlette==0.46.2 \ + sympy==1.14.0 \ + texttable==1.7.0 \ + thinc==8.3.6 \ + tokenizers==0.21.1 \ + torch==2.7.0 \ + tqdm==4.67.1 \ + transformers==4.51.3 \ + triton==3.3.0 \ + truststore==0.10.0 \ + typer==0.15.2 \ + typing_extensions==4.13.2 \ + urllib3==2.3.0 \ + uvicorn==0.34.2 \ + wasabi==1.1.3 \ + wcwidth==0.2.13 \ + weasel==0.4.1 \ + wheel==0.45.1 \ + whois==1.20240129.2 \ + wrapt==1.17.2 \ + XlsxWriter==3.2.3 \ + yarl==1.19.0 \ + youtube-dl==2021.12.17 \ + zstandard==0.23.0 + +echo "Changing directory to the cloned repository and starting the bot..." +cd wdiscordbot-internal-server-aws +python3 bot.py \ No newline at end of file From 7733363b70e50cfcd3831007dc823cbda615e830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 07:42:29 +0900 Subject: [PATCH 09/28] Edit update.py --- cogs/update.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/update.py b/cogs/update.py index 8e59bd7..aaff472 100644 --- a/cogs/update.py +++ b/cogs/update.py @@ -16,9 +16,9 @@ class GitUpdateCog(commands.Cog): await interaction.response.send_message("You do not have permission to run this command.", ephemeral=True) return await interaction.response.send_message("Initiating update. The bot will restart shortly...") - target_dir = "/home/server/wdiscordbotserver/" - repo_url = "https://gitlab.com/pancakes1234/wdiscordbotserver.git" - restart_script = "/home/server/wdiscordbotserver/bot.py" + target_dir = "./wdiscordbot-internal-server-aws" + repo_url = "https://gitlab.com/pancakes1234/wdiscordbot-internal-server-aws.git" + restart_script = "./wdiscordbot-internal-server-aws/bot.py" try: if os.path.exists(target_dir): From bd3410929b1edb2efe28447face0ed3cfdcfd59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 22 May 2025 07:44:01 +0900 Subject: [PATCH 10/28] Edit update.py --- cogs/update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/update.py b/cogs/update.py index aaff472..fc7b198 100644 --- a/cogs/update.py +++ b/cogs/update.py @@ -16,9 +16,9 @@ class GitUpdateCog(commands.Cog): await interaction.response.send_message("You do not have permission to run this command.", ephemeral=True) return await interaction.response.send_message("Initiating update. The bot will restart shortly...") - target_dir = "./wdiscordbot-internal-server-aws" + target_dir = "~/wdiscordbot-internal-server-aws" repo_url = "https://gitlab.com/pancakes1234/wdiscordbot-internal-server-aws.git" - restart_script = "./wdiscordbot-internal-server-aws/bot.py" + restart_script = "~/wdiscordbot-internal-server-aws/bot.py" try: if os.path.exists(target_dir): From 110a45d39a3e7d109b68ade0f210596dd06e3516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Sat, 24 May 2025 20:29:28 +0900 Subject: [PATCH 11/28] Edit update.py --- cogs/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/update.py b/cogs/update.py index fc7b198..ed67f57 100644 --- a/cogs/update.py +++ b/cogs/update.py @@ -18,7 +18,7 @@ class GitUpdateCog(commands.Cog): await interaction.response.send_message("Initiating update. The bot will restart shortly...") target_dir = "~/wdiscordbot-internal-server-aws" repo_url = "https://gitlab.com/pancakes1234/wdiscordbot-internal-server-aws.git" - restart_script = "~/wdiscordbot-internal-server-aws/bot.py" + restart_script = "home/ubuntu/wdiscordbot-internal-server-aws/bot.py" try: if os.path.exists(target_dir): From fa873a559d0815cb98629b982437d57b4b99fc7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Tue, 27 May 2025 03:22:59 +0900 Subject: [PATCH 12/28] Edit ai.py --- cogs/ai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/ai.py b/cogs/ai.py index 0df7b2d..d8324d8 100644 --- a/cogs/ai.py +++ b/cogs/ai.py @@ -43,7 +43,7 @@ class ImprovedAICog(commands.Cog): # Configuration self.default_config = { - "model": "meta-llama/llama-4-maverick:free", + "model": "google/gemini-2.5-flash-preview-05-20", "temperature": 0.75, "max_tokens": 1500, "top_p": 0.9, From 8e8541bab91f3bf940d23cc055baa784384fe45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Tue, 27 May 2025 20:15:57 +0900 Subject: [PATCH 13/28] Update file update.py --- cogs/update.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/cogs/update.py b/cogs/update.py index ed67f57..900c9e8 100644 --- a/cogs/update.py +++ b/cogs/update.py @@ -10,36 +10,50 @@ class GitUpdateCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - @app_commands.command(name="update", description="Updates the bot code from GitLab and restarts the bot. (Admin Only)") + @app_commands.command( + name="update", + description="Updates the bot code from GitLab and restarts the bot. (Admin Only)" + ) async def update(self, interaction: discord.Interaction): + # Check for administrator permission if not interaction.user.guild_permissions.administrator: await interaction.response.send_message("You do not have permission to run this command.", ephemeral=True) return - await interaction.response.send_message("Initiating update. The bot will restart shortly...") - target_dir = "~/wdiscordbot-internal-server-aws" - repo_url = "https://gitlab.com/pancakes1234/wdiscordbot-internal-server-aws.git" - restart_script = "home/ubuntu/wdiscordbot-internal-server-aws/bot.py" + # Respond with an initial message + await interaction.response.send_message("Initiating update. The bot will restart shortly...", ephemeral=True) + + # Define absolute paths and repository URL + target_dir = "/home/ubuntu/wdiscordbot-internal-server-aws" + repo_url = "https://gitlab.com/pancakes1234/wdiscordbot-internal-server-aws.git" + restart_script = "/home/ubuntu/wdiscordbot-internal-server-aws/bot.py" + try: if os.path.exists(target_dir): shutil.rmtree(target_dir) - await interaction.edit_original_response(content=f"Removed directory: {target_dir}") + await interaction.followup.send(f"Removed directory: {target_dir}", ephemeral=True) else: - await interaction.edit_original_response(content=f"Directory {target_dir} does not exist; proceeding with clone...") + await interaction.followup.send(f"Directory {target_dir} does not exist; proceeding with clone...", ephemeral=True) + + # Clone the repository subprocess.run(["git", "clone", repo_url, target_dir], check=True) - await interaction.edit_original_response(content="Repository cloned successfully.") + await interaction.followup.send("Repository cloned successfully.", ephemeral=True) except Exception as e: error_msg = f"Update failed: {e}" print(error_msg) - await interaction.edit_original_response(content=error_msg) + await interaction.followup.send(error_msg, ephemeral=True) return + try: - await interaction.edit_original_response(content="Bot has updated to the latest commit and is restarting...") + await interaction.followup.send("Bot has updated to the latest commit and is restarting...", ephemeral=True) + # Optionally change working directory if your bot expects it: + os.chdir(target_dir) + # Restart the bot by replacing the current process with the new version os.execv(sys.executable, [sys.executable, restart_script]) - # If os.execv returns, it means it failed except Exception as e: - await interaction.edit_original_response(content=f"Failed to restart bot: {e}") - + error_msg = f"Failed to restart bot: {e}" + print(error_msg) + await interaction.followup.send(error_msg, ephemeral=True) async def setup(bot: commands.Bot): await bot.add_cog(GitUpdateCog(bot)) From 78ff3f3c5822310967f56d4eb528cf732aec34c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 29 May 2025 00:12:23 +0900 Subject: [PATCH 14/28] Edit randomphone.py From 3924c934943811d3668a64b0f914b94da52cc489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 29 May 2025 00:34:32 +0900 Subject: [PATCH 15/28] Edit index.php --- API/index.php | 320 ++++++++++++++++++++++++++++---------------------- 1 file changed, 179 insertions(+), 141 deletions(-) diff --git a/API/index.php b/API/index.php index 706d4ce..e45c86f 100644 --- a/API/index.php +++ b/API/index.php @@ -1,21 +1,50 @@ true, + 'secure' => true, // Ensure HTTPS is used in production + 'samesite' => 'Strict' +]); session_start(); -// === Login & Authentication === -// Only proceed if the current session is authenticated. -// If not, show a login form. -if (!isset($_SESSION['logged_in'])) { - if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['password'])) { - $user_env = getenv('user'); // environment variable "user" - $pass_env = getenv('pass'); // environment variable "pass" - if ($_POST['username'] === $user_env && $_POST['password'] === $pass_env) { - $_SESSION['logged_in'] = true; - header("Location: index.php"); - exit; +// --- CSRF Utility Functions --- +function getCsrfToken() { + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + return $_SESSION['csrf_token']; +} + +function validateCsrfToken($token) { + return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); +} + +// --- Login and Authentication --- +if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) { + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['password'], $_POST['csrf_token'])) { + if (!validateCsrfToken($_POST['csrf_token'])) { + $error = "Invalid CSRF token."; } else { - $error = "Invalid credentials."; + $envUser = getenv('user'); + $envPass = getenv('pass'); + // Using hash_equals for timing attack prevention + if (hash_equals($_POST['username'], $envUser) && hash_equals($_POST['password'], $envPass)) { + $_SESSION['logged_in'] = true; + session_regenerate_id(true); + header("Location: " . $_SERVER['PHP_SELF']); + exit; + } else { + $error = "Invalid credentials."; + } } } + $loginToken = getCsrfToken(); ?> @@ -64,8 +93,9 @@ if (!isset($_SESSION['logged_in'])) {

Login

- {$error}

"; ?> -
+ " . htmlspecialchars($error) . "

"; } ?> + + @@ -76,65 +106,76 @@ if (!isset($_SESSION['logged_in'])) { &1'; + return trim(shell_exec($cmd)); + } + return "Directory not found."; } -$output = ""; // To hold any output from actions +function handleUpdateAction() { + // Only allow updates using a POST request with a valid CSRF token. + if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['csrf_token']) || !validateCsrfToken($_POST['csrf_token'])) { + return "Unauthorized update request."; + } + $botDir = '/home/server/wdiscordbotserver'; + $result = ""; + if (is_dir($botDir)) { + $rmCmd = 'rm -rf ' . escapeshellarg($botDir) . ' 2>&1'; + $result .= shell_exec($rmCmd); + } + $cloneCmd = 'git clone https://gitlab.com/pancakes1234/wdiscordbotserver.git ' . escapeshellarg($botDir) . ' 2>&1'; + $result .= shell_exec($cloneCmd); + return $result; +} -// Handle the different actions. -switch ($action) { - case "version": - // Gets the current commit from /home/server/wdiscordbotserver. - $botDir = '/home/server/wdiscordbotserver'; - if (is_dir($botDir)) { - $cmd = 'cd ' . escapeshellarg($botDir) . ' && git rev-parse HEAD 2>&1'; - $output = shell_exec($cmd); +function handleDataAction() { + $baseDir = realpath('/home/server'); + $file = $_GET['file'] ?? null; + $response = ""; + if ($file) { + $realFile = realpath($file); + if ($realFile === false || strpos($realFile, $baseDir) !== 0) { + $response = "Invalid file."; } else { - $output = "Directory not found."; - } - break; - - case "update": - // Removes the folder and clones the repository anew. - $botDir = '/home/server/wdiscordbotserver'; - if (is_dir($botDir)) { - $rmCmd = 'rm -rf ' . escapeshellarg($botDir) . ' 2>&1'; - $output .= shell_exec($rmCmd); - } - $cloneCmd = 'git clone https://gitlab.com/pancakes1234/wdiscordbotserver.git ' . escapeshellarg($botDir) . ' 2>&1'; - $output .= shell_exec($cloneCmd); - break; - - case "data": - // If editing a file, process its content. - if (isset($_GET['file'])) { - $file = $_GET['file']; - $baseDir = realpath('/home/server'); - $realFile = realpath($file); - if ($realFile === false || strpos($realFile, $baseDir) !== 0) { - $output = "Invalid file."; - } else { - if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['content'])) { - file_put_contents($realFile, $_POST['content']); - $output = "File updated successfully."; + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['content'], $_POST['csrf_token'])) { + if (!validateCsrfToken($_POST['csrf_token'])) { + $response = "Invalid CSRF token."; + } else { + if (file_put_contents($realFile, $_POST['content']) !== false) { + $response = "File updated successfully."; + } else { + $response = "Failed to update file."; + } } } } - break; + } + return $response; +} - // Other actions (such as terminal) will be handled in the UI below. +// --- Process Request Actions --- +$action = $_GET['action'] ?? ($_POST['action'] ?? ""); +switch ($action) { + case "version": + $output = handleVersionAction(); + break; + case "update": + $output = handleUpdateAction(); + break; + case "data": + $output = handleDataAction(); + break; + // Additional action cases (e.g., "terminal") can be handled below. default: - // No action or unrecognized action. break; } ?> @@ -143,7 +184,6 @@ switch ($action) { Discord Bot Admin API + +

Discord Bot Admin API

- + + + + +
- +
- Files in {$baseDir}"; - echo "
    "; - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($baseDir, RecursiveDirectoryIterator::SKIP_DOTS) - ); - foreach ($iterator as $fileInfo) { - $filePath = $fileInfo->getPathname(); - echo "
  • " . htmlspecialchars($filePath) . "
  • "; + +

    Files in

    +
      + getPathname(); + echo "
    • " . htmlspecialchars($filePath) . "
    • "; + } + } catch (Exception $e) { + echo "
    • Error reading files: " . htmlspecialchars($e->getMessage()) . "
    • "; } - echo "
    "; - } else { + ?> +
+ Invalid file.

"; - } else { - echo "

Editing: " . htmlspecialchars($realFile) . "

"; - echo "
"; - echo "
"; - echo ""; - echo ""; - echo "
"; - } - } - } - // === Terminal Section === - elseif ($action === "terminal") : - // This section provides two terminal options: - // 1. Wetty Terminal via an iframe (assumes Wetty is running on port 3000) - // 2. A direct integration of xterm.js (which connects via WebSocket to a Node.js pty server on port 3001) - ?> + if ($realFile === false || strpos($realFile, $baseDir) !== 0): ?> +

Invalid file.

+ +

Editing:

+
+ +
+ + +
+

Wetty Terminal

- +
- - - - - +
From d802cb44ca8573953a03512a7832f178fcc95152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Sat, 31 May 2025 01:56:25 +0900 Subject: [PATCH 16/28] Edit getajob.py --- cogs/getajob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/getajob.py b/cogs/getajob.py index e6a89a9..49f0d38 100644 --- a/cogs/getajob.py +++ b/cogs/getajob.py @@ -59,7 +59,7 @@ class CareerLinks(commands.Cog): @app_commands.command(name="getajob", description="Get a fucking job.") async def careers(self, interaction: discord.Interaction): name, url = random.choice(CAREER_LINKS) - await interaction.response.send_message(f"Get a job. \n**{name}**: {url}") + await interaction.response.send_message(f"Get a fucking job. \n**{name}**: {url}") async def setup(bot): await bot.add_cog(CareerLinks(bot)) From 5ad75e645efa6f01290a0e2e74a6a0ae87edffc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Sat, 31 May 2025 02:10:44 +0900 Subject: [PATCH 17/28] Edit .gitlab-ci.yml --- .gitlab-ci.yml | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 327433e..42f2423 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,10 +1,19 @@ -# The Docker image that will be used to build your app -image: node:lts -create-pages: - pages: - # The folder that contains the files to be exposed at the Page URL - publish: website - rules: - # This ensures that only pushes to the default branch will trigger - # a pages deploy - - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH +# You can override the included template(s) by including variable overrides +# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings +# Secret Detection customization: https://docs.gitlab.com/user/application_security/secret_detection/pipeline/configure +# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings +# Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings +# Note that environment variables can be set in several places +# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence +stages: +- test +- secret-detection +sast: + stage: test +include: +- template: Security/SAST.gitlab-ci.yml +- template: Security/Secret-Detection.gitlab-ci.yml +variables: + SECRET_DETECTION_ENABLED: 'true' +secret_detection: + stage: secret-detection From 74505979c1ca2da3f2eacb44623cdc6db0cb90df Mon Sep 17 00:00:00 2001 From: pancakes-proxy Date: Mon, 2 Jun 2025 04:24:20 +0900 Subject: [PATCH 18/28] fix: Update paths for guild configuration and OpenRouter model data to use relative paths --- cogs/aimod.py | 332 ++++++++++++++++++++------------------------------ 1 file changed, 135 insertions(+), 197 deletions(-) diff --git a/cogs/aimod.py b/cogs/aimod.py index 99d31fe..a26036a 100644 --- a/cogs/aimod.py +++ b/cogs/aimod.py @@ -26,7 +26,7 @@ OPENROUTER_MODEL = "google/gemini-2.5-flash-preview-05-20" # Make sure this mode MOD_LOG_API_SECRET_ENV_VAR = "MOD_LOG_API_SECRET" # --- Per-Guild Discord Configuration --- -GUILD_CONFIG_DIR = "/home/ubuntu/wdiscordbot-json-data" # Using the existing directory for all json data +GUILD_CONFIG_DIR = os.path.join(os.getcwd(), "wdiscordbot-json-data") # Using relative path from current working directory GUILD_CONFIG_PATH = os.path.join(GUILD_CONFIG_DIR, "guild_config.json") USER_INFRACTIONS_PATH = os.path.join(GUILD_CONFIG_DIR, "user_infractions.json") @@ -207,7 +207,7 @@ class ModerationCog(commands.Cog): def _load_openrouter_models(self): """Loads OpenRouter model data from the JSON file.""" - models_json_path = "/home/ubuntu/wdiscordbot-internal-server-aws/data/openrouter_models.json" # Relative to bot's root + models_json_path = os.path.join(os.getcwd(), "data", "openrouter_models.json") # Relative to bot's root try: if os.path.exists(models_json_path): with open(models_json_path, "r", encoding="utf-8") as f: @@ -611,200 +611,7 @@ class ModerationCog(commands.Cog): # self.bot.tree.add_command(self.modsetmodel) # self.bot.tree.add_command(self.modgetmodel) - async def query_openrouter(self, message: discord.Message, message_content: str, user_history: str, image_data_list=None): - """ - Sends the message content, user history, and additional context to the OpenRouter API for analysis. - Optionally includes image data for visual content moderation. - Args: - message: The original discord.Message object. - message_content: The text content of the message. - user_history: A string summarizing the user's past infractions. - image_data_list: Optional list of tuples (mime_type, image_bytes, attachment_type, filename) for image moderation. - - Returns: - A dictionary containing the AI's decision, or None if an error occurs. - Expected format: - { - "reasoning": str, - "violation": bool, - "rule_violated": str ("None", "1", "5A", etc.), - "action": str ("IGNORE", "WARN", "DELETE", "BAN", "NOTIFY_MODS") - } - """ - print(f"query_openrouter called. API key available: {self.openrouter_api_key is not None}") - # Check if the API key was successfully fetched - if not self.openrouter_api_key: - print("Error: OpenRouter API Key is not available. Cannot query API.") - return None - - # Construct the prompt for the AI model - system_prompt_text = f"""You are an AI moderation assistant for a Discord server. -Your primary function is to analyze message content and attached media based STRICTLY on the server rules provided below, using all available context. - -Server Rules: ---- -{SERVER_RULES} ---- - -Context Provided: -You will receive the following information to aid your analysis: -- User's Server Role: (e.g., "Server Owner", "Admin", "Moderator", "Member"). -- Channel Category: The name of the category the channel belongs to. -- Channel Age-Restricted/NSFW (Discord Setting): Boolean (true/false). -- Replied-to Message: If the current message is a reply, the content of the original message will be provided. This is crucial for understanding direct interactions. -- Recent Channel History: The last few messages in the channel to understand the flow of conversation. -- Attached Media: If the message contains image, GIF, or video attachments, they will be provided as image_url objects in the content array. For GIFs and videos, only the first frame is extracted. - -Instructions: -1. Review the "Message Content" and any attached media against EACH rule, considering ALL provided context (User Role, Channel Info, Replied-to Message, Recent Channel History). - - The "Channel Age-Restricted/NSFW (Discord Setting)" is the definitive indicator for NSFW content by Discord. - - The "Channel Category" provides general context. - - **"Replied-to Message" and "Recent Channel History" are vital for understanding banter, jokes, and ongoing discussions. A statement that seems offensive in isolation might be acceptable within the flow of conversation or as a direct reply.** - - If images, GIFs, or videos are attached, analyze ALL of them for rule violations. For GIFs and videos, only the first frame is provided. - - Pay special attention to images that may contain NSFW content, pornography, gore, or other prohibited visual content. - - If multiple attachments are present, a violation in ANY of them should be flagged. -2. Determine if ANY rule is violated. When evaluating, consider the server's culture where **extremely edgy, dark, and sexual humor, including potentially offensive jokes (e.g., rape jokes, saying you want to be raped), are common and generally permissible IF THEY ARE CLEARLY JOKES, part of an established banter, or a direct non-malicious reply, and not targeted harassment or explicit rule violations.** -* **NSFW Content:** -The only rule regarding NSFW content is that **real-life pornography is strictly prohibited**. -Full-on pornographic images are permitted in designated NSFW channels. -Stickers and emojis are NOT considered "full-on pornographic images" and are allowed in any channel. - - For general disrespectful behavior, harassment, or bullying (Rule 2 & 3): Only flag a violation if the intent appears **genuinely malicious, targeted, or serious, even after considering conversational history and replies.** Lighthearted insults or "wild" statements within an ongoing banter are generally permissible. - - For **explicit slurs or severe discriminatory language** (Rule 3): These are violations **regardless of joking intent if they are used in a targeted or hateful manner**. Context from replies and history is still important to assess targeting. - - CRITICAL: You should NOT consider the word "retard" or "retarded" as a slur in this server, as it is commonly used in a non-offensive context. -After considering the above, pay EXTREME attention to rules 5 (Pedophilia) and 5A (IRL Porn) – these are always severe. Rule 4 (AI Porn) is also critical. Prioritize these severe violations. -3. Respond ONLY with a single JSON object containing the following keys: - - "reasoning": string (A concise explanation for your decision, referencing the specific rule and content). - - "violation": boolean (true if any rule is violated, false otherwise) - - "rule_violated": string (The number of the rule violated, e.g., "1", "5A", "None". If multiple rules are violated, state the MOST SEVERE one, prioritizing 5A > 5 > 4 > 3 > 2 > 1). - - "action": string (Suggest ONE action from: "IGNORE", "WARN", "DELETE", "TIMEOUT_SHORT", "TIMEOUT_MEDIUM", "TIMEOUT_LONG", "KICK", "BAN", "NOTIFY_MODS", "SUICIDAL". - Consider the user's infraction history. If the user has prior infractions for similar or escalating behavior, suggest a more severe action than if it were a first-time offense for a minor rule. - Progressive Discipline Guide (unless overridden by severity): - - First minor offense: "WARN" (and "DELETE" if content is removable like Rule 1/4). - - Second minor offense / First moderate offense: "TIMEOUT_SHORT" (e.g., 10 minutes). - - Repeated moderate offenses: "TIMEOUT_MEDIUM" (e.g., 1 hour). - - Multiple/severe offenses: "TIMEOUT_LONG" (e.g., 1 day), "KICK", or "BAN". - Spamming: - - If a user continuously sends very long messages that are off-topic, repetitive, or appear to be meaningless spam (e.g., character floods, nonsensical text), suggest "TIMEOUT_MEDIUM" or "TIMEOUT_LONG" depending on severity and history, even if the content itself doesn't violate other specific rules. This is to maintain chat readability. - Rule Severity Guidelines (use your judgment): - - Consider the severity of each rule violation on its own merits. - - Consider the user's history of past infractions when determining appropriate action. - - Consider the context of the message and channel when evaluating violations. - - You have full discretion to determine the most appropriate action for any violation. - Suicidal Content: - If the message content expresses **clear, direct, and serious suicidal ideation, intent, planning, or recent attempts** (e.g., 'I am going to end my life and have a plan', 'I survived my attempt last night', 'I wish I hadn't woken up after trying'), ALWAYS use "SUICIDAL" as the action, and set "violation" to true, with "rule_violated" as "Suicidal Content". - For casual, edgy, hyperbolic, or ambiguous statements like 'imma kms', 'just kill me now', 'I want to die (lol)', or phrases that are clearly part of edgy humor/banter rather than a genuine cry for help, you should lean towards "IGNORE" or "NOTIFY_MODS" if there's slight ambiguity but no clear serious intent. **Do NOT flag 'imma kms' as "SUICIDAL" unless there is very strong supporting context indicating genuine, immediate, and serious intent.** - If unsure but suspicious, or if the situation is complex: "NOTIFY_MODS". - Default action for minor first-time rule violations should be "WARN" or "DELETE" (if applicable). - Do not suggest "KICK" or "BAN" lightly; reserve for severe or repeated major offenses. - Timeout durations: TIMEOUT_SHORT (approx 10 mins), TIMEOUT_MEDIUM (approx 1 hour), TIMEOUT_LONG (approx 1 day to 1 week). - The system will handle the exact timeout duration; you just suggest the category.) - -Example Response (Violation): -{{ - "reasoning": "The message content clearly depicts IRL non-consensual sexual content involving minors, violating rule 5A.", - "violation": true, - "rule_violated": "5A", - "action": "BAN" -}} - -Example Response (No Violation): -{{ - "reasoning": "The message is a respectful discussion and contains no prohibited content.", - "violation": false, - "rule_violated": "None", - "action": "IGNORE" -}} - -Example Response (Suicidal Content): -{{ - "reasoning": "The user's message 'I want to end my life' indicates clear suicidal intent.", - "violation": true, - "rule_violated": "Suicidal Content", - "action": "SUICIDAL" -}} -""" - - system_prompt_text = f"""You are an AI moderation assistant for a Discord server. -Your primary function is to analyze message content and attached media based STRICTLY on the server rules provided below, using all available context. - -Server Rules: ---- -{SERVER_RULES} ---- - -Context Provided: -You will receive the following information to aid your analysis: -- User's Server Role: (e.g., "Server Owner", "Admin", "Moderator", "Member"). -- Channel Category: The name of the category the channel belongs to. -- Channel Age-Restricted/NSFW (Discord Setting): Boolean (true/false). -- Replied-to Message: If the current message is a reply, the content of the original message will be provided. This is crucial for understanding direct interactions. -- Recent Channel History: The last few messages in the channel to understand the flow of conversation. - -Instructions: -1. Review the "Message Content" against EACH rule, considering ALL provided context (User Role, Channel Info, Replied-to Message, Recent Channel History). - - The "Channel Age-Restricted/NSFW (Discord Setting)" is the definitive indicator for NSFW content by Discord. - - The "Channel Category" provides general context. - - **"Replied-to Message" and "Recent Channel History" are vital for understanding banter, jokes, and ongoing discussions. A statement that seems offensive in isolation might be acceptable within the flow of conversation or as a direct reply.** -2. Determine if ANY rule is violated. When evaluating, consider the server's culture where **extremely edgy, dark, and sexual humor, including potentially offensive jokes (e.g., rape jokes, saying you want to be raped), are common and generally permissible IF THEY ARE CLEARLY JOKES, part of an established banter, or a direct non-malicious reply, and not targeted harassment or explicit rule violations.** - - For Rule 1 (NSFW content): - The only rules regarding NSFW content is that **real-life pornography is strictly prohibited**, and Full-on pornographic images are only permitted in designated NSFW channels. - Stickers and emojis are NOT considered "full-on pornographic images" and are allowed in any channel. - - For general disrespectful behavior, harassment, or bullying (Rule 2 & 3): Only flag a violation if the intent appears **genuinely malicious, targeted, or serious, even after considering conversational history and replies.** Lighthearted insults or "wild" statements within an ongoing banter are generally permissible. - - For **explicit slurs or severe discriminatory language** (Rule 3): These are violations **regardless of joking intent if they are used in a targeted or hateful manner**. Context from replies and history is still important to assess targeting. - - CRITICAL: You should NOT consider the word "retard" or "retarded" as a slur in this server, as it is commonly used in a non-offensive context. -After considering the above, pay EXTREME attention to rules 5 (Pedophilia) and 5A (IRL Porn) – these are always severe. Rule 4 (AI Porn) is also critical. Prioritize these severe violations. -3. Respond ONLY with a single JSON object containing the following keys: - - "reasoning": string (A concise explanation for your decision, referencing the specific rule and content). - - "violation": boolean (true if any rule is violated, false otherwise) - - "rule_violated": string (The number of the rule violated, e.g., "1", "5A", "None". If multiple rules are violated, state the MOST SEVERE one, prioritizing 5A > 5 > 4 > 3 > 2 > 1). - - "action": string (Suggest ONE action from: "IGNORE", "WARN", "DELETE", "TIMEOUT_SHORT", "TIMEOUT_MEDIUM", "TIMEOUT_LONG", "KICK", "BAN", "NOTIFY_MODS", "SUICIDAL". - Consider the user's infraction history. If the user has prior infractions for similar or escalating behavior, suggest a more severe action than if it were a first-time offense for a minor rule. - Progressive Discipline Guide (unless overridden by severity): - - First minor offense: "WARN" (and "DELETE" if content is removable like Rule 1/4). - - Second minor offense / First moderate offense: "TIMEOUT_SHORT" (e.g., 10 minutes). - - Repeated moderate offenses: "TIMEOUT_MEDIUM" (e.g., 1 hour). - - Multiple/severe offenses: "TIMEOUT_LONG" (e.g., 1 day), "KICK", or "BAN". - Spamming: - - If a user continuously sends very long messages that are off-topic, repetitive, or appear to be meaningless spam (e.g., character floods, nonsensical text), suggest "TIMEOUT_MEDIUM" or "TIMEOUT_LONG" depending on severity and history, even if the content itself doesn't violate other specific rules. This is to maintain chat readability. - Rule Severity Guidelines (use your judgment): - - Consider the severity of each rule violation on its own merits. - - Consider the user's history of past infractions when determining appropriate action. - - Consider the context of the message and channel when evaluating violations. - - You have full discretion to determine the most appropriate action for any violation. - Suicidal Content: - If the message content expresses **clear, direct, and serious suicidal ideation, intent, planning, or recent attempts** (e.g., 'I am going to end my life and have a plan', 'I survived my attempt last night', 'I wish I hadn't woken up after trying'), ALWAYS use "SUICIDAL" as the action, and set "violation" to true, with "rule_violated" as "Suicidal Content". - For casual, edgy, hyperbolic, or ambiguous statements like 'imma kms', 'just kill me now', 'I want to die (lol)', or phrases that are clearly part of edgy humor/banter rather than a genuine cry for help, you should lean towards "IGNORE" or "NOTIFY_MODS" if there's slight ambiguity but no clear serious intent. **Do NOT flag 'imma kms' as "SUICIDAL" unless there is very strong supporting context indicating genuine, immediate, and serious intent.** - If unsure but suspicious, or if the situation is complex: "NOTIFY_MODS". - Default action for minor first-time rule violations should be "WARN" or "DELETE" (if applicable). - Do not suggest "KICK" or "BAN" lightly; reserve for severe or repeated major offenses. - Timeout durations: TIMEOUT_SHORT (approx 10 mins), TIMEOUT_MEDIUM (approx 1 hour), TIMEOUT_LONG (approx 1 day to 1 week). - The system will handle the exact timeout duration; you just suggest the category.) - -Example Response (Violation): -{{ - "reasoning": "The message content clearly depicts IRL non-consensual sexual content involving minors, violating rule 5A.", - "violation": true, - "rule_violated": "5A", - "action": "BAN" -}} - -Example Response (No Violation): -{{ - "reasoning": "The message is a respectful discussion and contains no prohibited content.", - "violation": false, - "rule_violated": "None", - "action": "IGNORE" -}} - -Example Response (Suicidal Content): -{{ - "reasoning": "The user's message 'I want to end my life' indicates clear suicidal intent.", - "violation": true, - "rule_violated": "Suicidal Content", - "action": "SUICIDAL" -}} -""" async def query_openrouter(self, message: discord.Message, message_content: str, user_history: str, image_data_list=None): """ @@ -920,13 +727,144 @@ Example Response (Suicidal Content): }} """ + # Get the model from guild config, fall back to global default + guild_id = message.guild.id + model_used = get_guild_config(guild_id, "AI_MODEL", OPENROUTER_MODEL) + + # Gather context information + user_role = "Member" # Default + if message.author.guild_permissions.administrator: + user_role = "Admin" + elif message.author.guild_permissions.manage_messages: + user_role = "Moderator" + elif message.guild.owner_id == message.author.id: + user_role = "Server Owner" + + # Get channel category + channel_category = message.channel.category.name if message.channel.category else "No Category" + + # Check if channel is NSFW + is_nsfw_channel = getattr(message.channel, 'nsfw', False) + + # Get replied-to message content if this is a reply + replied_to_content = "" + if message.reference and message.reference.message_id: + try: + replied_message = await message.channel.fetch_message(message.reference.message_id) + replied_to_content = f"Replied-to Message: {replied_message.author.display_name}: {replied_message.content[:200]}" + except: + replied_to_content = "Replied-to Message: [Could not fetch]" + + # Get recent channel history (last 3 messages before this one) + recent_history = [] + try: + async for hist_message in message.channel.history(limit=4, before=message): + if not hist_message.author.bot: + recent_history.append(f"{hist_message.author.display_name}: {hist_message.content[:100]}") + except: + recent_history = ["[Could not fetch recent history]"] + + recent_history_text = "\n".join(recent_history[:3]) if recent_history else "No recent history available." + + # Construct the user prompt with context + user_prompt = f""" +**Context Information:** +- User's Server Role: {user_role} +- Channel Category: {channel_category} +- Channel Age-Restricted/NSFW (Discord Setting): {is_nsfw_channel} +- {replied_to_content} +- Recent Channel History: +{recent_history_text} + +**User's Infraction History:** +{user_history} + +**Message Content:** +{message_content if message_content else "[No text content]"} +""" + + # Prepare the messages array for the API + messages = [ + {"role": "system", "content": system_prompt_text}, + {"role": "user", "content": [{"type": "text", "text": user_prompt}]} + ] + + # Add images to the user message if present + if image_data_list: + for mime_type, image_bytes, attachment_type, filename in image_data_list: + # Convert image bytes to base64 + image_base64 = base64.b64encode(image_bytes).decode('utf-8') + image_url = f"data:{mime_type};base64,{image_base64}" + + messages[1]["content"].append({ + "type": "image_url", + "image_url": {"url": image_url} + }) + print(f"Added {attachment_type} attachment to AI analysis: {filename}") + + # Prepare the API request + headers = { + "Authorization": f"Bearer {self.openrouter_api_key}", + "Content-Type": "application/json" + } + + payload = { + "model": model_used, + "messages": messages, + "max_tokens": 500, + "temperature": 0.1 + } + + try: + async with self.session.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=30) as response: + if response.status == 200: + response_data = await response.json() + ai_response_text = response_data.get("choices", [{}])[0].get("message", {}).get("content", "") + + if not ai_response_text: + print("Error: Empty response from OpenRouter API.") + return None + + # Parse the JSON response from the AI + try: + # Clean the response text (remove markdown code blocks if present) + clean_response = ai_response_text.strip() + if clean_response.startswith("```json"): + clean_response = clean_response[7:] + if clean_response.endswith("```"): + clean_response = clean_response[:-3] + clean_response = clean_response.strip() + + ai_decision = json.loads(clean_response) + + # Validate the response structure + required_keys = ["reasoning", "violation", "rule_violated", "action"] + if not all(key in ai_decision for key in required_keys): + print(f"Error: AI response missing required keys. Got: {ai_decision}") + return None + + print(f"AI Decision: {ai_decision}") + return ai_decision + + except json.JSONDecodeError as e: + print(f"Error parsing AI response as JSON: {e}") + print(f"Raw AI response: {ai_response_text}") + return None + + else: + error_text = await response.text() + print(f"OpenRouter API error {response.status}: {error_text}") + return None + + except Exception as e: + print(f"Exception during OpenRouter API call: {e}") + return None + async def handle_violation(self, message: discord.Message, ai_decision: dict, notify_mods_message: str = None): """ Takes action based on the AI's violation decision. Also transmits action info via HTTP POST with API key header. """ - import datetime - import aiohttp rule_violated = ai_decision.get("rule_violated", "Unknown") reasoning = ai_decision.get("reasoning", "No reasoning provided.") From f11e51955545fe6514c26bc9ec7e2afac8af9325 Mon Sep 17 00:00:00 2001 From: pancakes-proxy Date: Tue, 3 Jun 2025 03:52:24 +0900 Subject: [PATCH 19/28] feat: Add UserInfoCog to display detailed user information in Discord --- cogs/userinfo.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 cogs/userinfo.py diff --git a/cogs/userinfo.py b/cogs/userinfo.py new file mode 100644 index 0000000..276e158 --- /dev/null +++ b/cogs/userinfo.py @@ -0,0 +1,86 @@ +import discord +from discord.ext import commands +from discord import app_commands +from typing import Optional + +class UserInfoCog(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(name="aboutuser", description="Display info about a user or yourself.") + @app_commands.describe(user="The user to get info about (optional)") + async def aboutuser(self, interaction: discord.Interaction, user: Optional[discord.Member] = None): + member = user or interaction.user + # Fetch up-to-date member info + if interaction.guild: + member = interaction.guild.get_member(member.id) or member + # Fetch user object for banner/profile + user_obj = member._user if hasattr(member, '_user') else member + # Banner fetching (API call) + banner_url = None + try: + user_obj = await self.bot.fetch_user(member.id) + if user_obj.banner: + banner_url = user_obj.banner.url + except Exception: + pass + # Devices + if isinstance(member, discord.Member): + status = str(member.status).title() + devices = [] + if hasattr(member, "devices"): + for dev in getattr(member, "devices", []): + devices.append(str(dev).title()) + else: + # Fallback for older discord.py + if member.desktop_status != discord.Status.offline: + devices.append("Desktop") + if member.mobile_status != discord.Status.offline: + devices.append("Mobile") + if member.web_status != discord.Status.offline: + devices.append("Web") + device_str = ", ".join(devices) if devices else "Unknown" + else: + status = "Unknown" + device_str = "Unknown" + # Activities + activities = [] + if hasattr(member, "activities"): + for activity in member.activities: + if isinstance(activity, discord.Game): + activities.append(f"Playing {activity.name}") + elif isinstance(activity, discord.Spotify): + activities.append(f"Listening to {activity.title} by {', '.join(activity.artists)}") + elif isinstance(activity, discord.Activity): + activities.append(f"{activity.type.name.title()}: {activity.name}") + activity_str = ", ".join(activities) if activities else "None" + # Roles + if isinstance(member, discord.Member) and interaction.guild: + roles = [role.mention for role in member.roles if role != interaction.guild.default_role] + roles_str = ", ".join(roles) if roles else "None" + else: + roles_str = "None" + # Embed + embed = discord.Embed( + title=f"User Info: {member.display_name}", + color=member.color if hasattr(member, 'color') else discord.Color.blurple(), + description=f"Profile of {member.mention}" + ) + if banner_url: + embed.set_image(url=banner_url) + embed.set_thumbnail(url=member.display_avatar.url) + embed.add_field(name="Nickname", value=member.nick or "None", inline=True) + embed.add_field(name="Username", value=f"{member.name}#{member.discriminator}", inline=True) + embed.add_field(name="User ID", value=member.id, inline=True) + embed.add_field(name="Status", value=status, inline=True) + embed.add_field(name="Device", value=device_str, inline=True) + embed.add_field(name="Activity", value=activity_str, inline=True) + embed.add_field(name="Roles", value=roles_str, inline=False) + embed.add_field(name="Account Created", value=member.created_at.strftime('%Y-%m-%d %H:%M:%S'), inline=True) + if hasattr(member, 'joined_at') and member.joined_at: + embed.add_field(name="Joined Server", value=member.joined_at.strftime('%Y-%m-%d %H:%M:%S'), inline=True) + embed.set_footer(text=f"Requested by {interaction.user.display_name}", icon_url=interaction.user.display_avatar.url) + await interaction.response.send_message(embed=embed) + +async def setup(bot: commands.Bot): + await bot.add_cog(UserInfoCog(bot)) From c1e23c9eebd0113b735be730f5d3fbc7243c26cf Mon Sep 17 00:00:00 2001 From: pancakes-proxy Date: Tue, 3 Jun 2025 03:59:28 +0900 Subject: [PATCH 20/28] fix: Improve user status and device detection in UserInfoCog --- cogs/userinfo.py | 53 +++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/cogs/userinfo.py b/cogs/userinfo.py index 276e158..7d626fa 100644 --- a/cogs/userinfo.py +++ b/cogs/userinfo.py @@ -24,35 +24,45 @@ class UserInfoCog(commands.Cog): banner_url = user_obj.banner.url except Exception: pass - # Devices + # Status if isinstance(member, discord.Member): - status = str(member.status).title() - devices = [] - if hasattr(member, "devices"): - for dev in getattr(member, "devices", []): - devices.append(str(dev).title()) - else: - # Fallback for older discord.py - if member.desktop_status != discord.Status.offline: - devices.append("Desktop") - if member.mobile_status != discord.Status.offline: - devices.append("Mobile") - if member.web_status != discord.Status.offline: - devices.append("Web") - device_str = ", ".join(devices) if devices else "Unknown" + status = str(member.status).capitalize() else: status = "Unknown" - device_str = "Unknown" - # Activities + # Devices (accurate for discord.py 2.3+) + device_map = { + "desktop": "Desktop", + "mobile": "Mobile", + "web": "Web" + } + devices = set() + if hasattr(member, "devices") and member.devices: + for dev in member.devices: + devices.add(device_map.get(str(dev), str(dev).capitalize())) + else: + # Fallback for older discord.py + if hasattr(member, "desktop_status") and member.desktop_status != discord.Status.offline: + devices.add("Desktop") + if hasattr(member, "mobile_status") and member.mobile_status != discord.Status.offline: + devices.add("Mobile") + if hasattr(member, "web_status") and member.web_status != discord.Status.offline: + devices.add("Web") + device_str = ", ".join(devices) if devices else "Offline/Unknown" + # Activities (show all, including custom, game, music, etc.) activities = [] - if hasattr(member, "activities"): + if hasattr(member, "activities") and member.activities: for activity in member.activities: if isinstance(activity, discord.Game): activities.append(f"Playing {activity.name}") elif isinstance(activity, discord.Spotify): activities.append(f"Listening to {activity.title} by {', '.join(activity.artists)}") + elif isinstance(activity, discord.CustomActivity): + if activity.name: + activities.append(f"Custom Status: {activity.name}") elif isinstance(activity, discord.Activity): - activities.append(f"{activity.type.name.title()}: {activity.name}") + # General fallback for other activity types + act_type = getattr(activity.type, 'name', str(activity.type)).title() + activities.append(f"{act_type}: {activity.name}") activity_str = ", ".join(activities) if activities else "None" # Roles if isinstance(member, discord.Member) and interaction.guild: @@ -76,9 +86,10 @@ class UserInfoCog(commands.Cog): embed.add_field(name="Device", value=device_str, inline=True) embed.add_field(name="Activity", value=activity_str, inline=True) embed.add_field(name="Roles", value=roles_str, inline=False) - embed.add_field(name="Account Created", value=member.created_at.strftime('%Y-%m-%d %H:%M:%S'), inline=True) + # Account created + embed.add_field(name="Account Created", value=member.created_at.strftime('%Y-%m-%d %H:%M:%S UTC'), inline=True) if hasattr(member, 'joined_at') and member.joined_at: - embed.add_field(name="Joined Server", value=member.joined_at.strftime('%Y-%m-%d %H:%M:%S'), inline=True) + embed.add_field(name="Joined Server", value=member.joined_at.strftime('%Y-%m-%d %H:%M:%S UTC'), inline=True) embed.set_footer(text=f"Requested by {interaction.user.display_name}", icon_url=interaction.user.display_avatar.url) await interaction.response.send_message(embed=embed) From 6ab23ccdb8914a30d1d4a1c89faae8e098c90f93 Mon Sep 17 00:00:00 2001 From: pancakes-proxy Date: Tue, 3 Jun 2025 04:02:17 +0900 Subject: [PATCH 21/28] feat: Enable presence and member intents for enhanced user status and device detection --- bot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot.py b/bot.py index 4db977b..89e4d2d 100644 --- a/bot.py +++ b/bot.py @@ -42,6 +42,8 @@ if not discord_token: # Configure bot with intents intents = discord.Intents.default() intents.message_content = True +intents.presences = True # Enable presence intent for status/device/activity detection +intents.members = True # Enable member intent for full member info # Technically no reason to have a prefix set because the bot only uses slash commands. bot = commands.Bot(command_prefix="/", intents=intents) From a2c086998aede17a1244f74d4a53e0de6824045c Mon Sep 17 00:00:00 2001 From: pancakes-proxy Date: Tue, 3 Jun 2025 04:13:09 +0900 Subject: [PATCH 22/28] feat: Add user badges display in UserInfoCog for enhanced user profile information --- cogs/userinfo.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/cogs/userinfo.py b/cogs/userinfo.py index 7d626fa..2c0f1d7 100644 --- a/cogs/userinfo.py +++ b/cogs/userinfo.py @@ -70,12 +70,42 @@ class UserInfoCog(commands.Cog): roles_str = ", ".join(roles) if roles else "None" else: roles_str = "None" + + # Badges + badge_map = { + "staff": "Discord Staff πŸ›‘οΈ", + "partner": "Partner ⭐", + "hypesquad": "HypeSquad Event πŸ†", + "bug_hunter": "Bug Hunter πŸ›", + "hypesquad_bravery": "Bravery 🦁", + "hypesquad_brilliance": "Brilliance 🧠", + "hypesquad_balance": "Balance βš–οΈ", + "early_supporter": "Early Supporter πŸ•°οΈ", + "team_user": "Team User πŸ‘₯", + "system": "System πŸ€–", + "bug_hunter_level_2": "Bug Hunter Level 2 🐞", + "verified_bot": "Verified Bot πŸ€–", + "verified_developer": "Early Verified Bot Dev πŸ› οΈ", + "discord_certified_moderator": "Certified Mod πŸ›‘οΈ", + "active_developer": "Active Developer πŸ§‘β€πŸ’»" + } + user_flags = getattr(user_obj, 'public_flags', None) + badges = [] + if user_flags: + for flag in badge_map: + if getattr(user_flags, flag, False): + badges.append(badge_map[flag]) + badge_str = ", ".join(badges) if badges else "" + if member.id == 1141746562922459136: + badge_str = (badge_str + ", " if badge_str else "") + "Bot Developer πŸ› οΈ" # Embed embed = discord.Embed( title=f"User Info: {member.display_name}", color=member.color if hasattr(member, 'color') else discord.Color.blurple(), description=f"Profile of {member.mention}" ) + if badge_str: + embed.add_field(name="Badge", value=badge_str, inline=False) if banner_url: embed.set_image(url=banner_url) embed.set_thumbnail(url=member.display_avatar.url) From 2570f6c1241e7044ffdf5d8837c9531d02b4e0a2 Mon Sep 17 00:00:00 2001 From: pancakes-proxy Date: Mon, 2 Jun 2025 13:13:58 -0400 Subject: [PATCH 23/28] feat: Enhance user info display with additional fields for accent color, avatar decoration, Nitro status, pronouns, locale, server boosting status, and mutual servers --- cogs/userinfo.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/cogs/userinfo.py b/cogs/userinfo.py index 2c0f1d7..cdd7758 100644 --- a/cogs/userinfo.py +++ b/cogs/userinfo.py @@ -106,6 +106,7 @@ class UserInfoCog(commands.Cog): ) if badge_str: embed.add_field(name="Badge", value=badge_str, inline=False) + # If banner_url exists, set as embed image at the top if banner_url: embed.set_image(url=banner_url) embed.set_thumbnail(url=member.display_avatar.url) @@ -120,6 +121,35 @@ class UserInfoCog(commands.Cog): embed.add_field(name="Account Created", value=member.created_at.strftime('%Y-%m-%d %H:%M:%S UTC'), inline=True) if hasattr(member, 'joined_at') and member.joined_at: embed.add_field(name="Joined Server", value=member.joined_at.strftime('%Y-%m-%d %H:%M:%S UTC'), inline=True) + # --- Additional User Info (TOS-compliant) --- + # Accent color + accent_color = getattr(user_obj, 'accent_color', None) + if accent_color: + embed.add_field(name="Accent Color", value=str(accent_color), inline=True) + # Avatar Decoration (if available) + avatar_decoration = getattr(user_obj, 'avatar_decoration', None) + if avatar_decoration: + embed.add_field(name="Avatar Decoration", value=str(avatar_decoration), inline=True) + # Nitro status (public flag) + if user_flags and getattr(user_flags, 'premium', False): + embed.add_field(name="Nitro", value="Yes", inline=True) + # Pronouns (if available) + pronouns = getattr(user_obj, 'pronouns', None) + if pronouns: + embed.add_field(name="Pronouns", value=pronouns, inline=True) + # Locale/language (if available) + locale = getattr(user_obj, 'locale', None) + if locale: + embed.add_field(name="Locale", value=locale, inline=True) + # Server boosting status + if isinstance(member, discord.Member) and getattr(member, 'premium_since', None): + embed.add_field(name="Server Booster", value=f"Since {member.premium_since.strftime('%Y-%m-%d %H:%M:%S UTC')}", inline=True) + # Mutual servers (if bot shares more than one) + if hasattr(self.bot, 'guilds'): + mutual_guilds = [g.name for g in self.bot.guilds if g.get_member(member.id)] + if len(mutual_guilds) > 1: + embed.add_field(name="Mutual Servers", value=", ".join(mutual_guilds), inline=False) + # End of additional info embed.set_footer(text=f"Requested by {interaction.user.display_name}", icon_url=interaction.user.display_avatar.url) await interaction.response.send_message(embed=embed) From 9fbe200c73c27f4febcc27433eacbc96c6747099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Tue, 3 Jun 2025 21:47:17 +0900 Subject: [PATCH 24/28] added a manual data set command because of fuckass discord API --- cogs/userinfo.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 3 deletions(-) diff --git a/cogs/userinfo.py b/cogs/userinfo.py index cdd7758..08de432 100644 --- a/cogs/userinfo.py +++ b/cogs/userinfo.py @@ -2,10 +2,142 @@ import discord from discord.ext import commands from discord import app_commands from typing import Optional +import json +import os class UserInfoCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot + self.user_data_file = "user_data.json" + self.user_data = self._load_user_data() + self.developer_id = 1141746562922459136 # The developer's ID + + def _load_user_data(self): + """Load user data from JSON file""" + if os.path.exists(self.user_data_file): + try: + with open(self.user_data_file, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Error loading user data: {e}") + return {} + return {} + + def _save_user_data(self): + """Save user data to JSON file""" + try: + with open(self.user_data_file, 'w') as f: + json.dump(self.user_data, f, indent=4) + except Exception as e: + print(f"Error saving user data: {e}") + + @app_commands.command(name="setuser", description="Set custom data for a user") + @app_commands.describe( + user="The user to set data for", + nickname="Custom nickname", + pronouns="User's pronouns", + bio="Short biography", + birthday="User's birthday (YYYY-MM-DD)", + location="User's location", + custom_field="Name of custom field", + custom_value="Value for custom field" + ) + async def setuser( + self, + interaction: discord.Interaction, + user: discord.Member, + nickname: Optional[str] = None, + pronouns: Optional[str] = None, + bio: Optional[str] = None, + birthday: Optional[str] = None, + location: Optional[str] = None, + custom_field: Optional[str] = None, + custom_value: Optional[str] = None + ): + """Set custom data for a user - only works for the developer""" + # Check if the command user is the developer + if interaction.user.id != self.developer_id: + await interaction.response.send_message("This command is only available to the bot developer.", ephemeral=True) + return + + # Initialize user data if not exists + user_id = str(user.id) + if user_id not in self.user_data: + self.user_data[user_id] = {} + + # Update fields if provided + fields_updated = [] + + if nickname is not None: + self.user_data[user_id]["nickname"] = nickname + fields_updated.append("Nickname") + + if pronouns is not None: + self.user_data[user_id]["pronouns"] = pronouns + fields_updated.append("Pronouns") + + if bio is not None: + self.user_data[user_id]["bio"] = bio + fields_updated.append("Bio") + + if birthday is not None: + self.user_data[user_id]["birthday"] = birthday + fields_updated.append("Birthday") + + if location is not None: + self.user_data[user_id]["location"] = location + fields_updated.append("Location") + + if custom_field is not None and custom_value is not None: + if "custom_fields" not in self.user_data[user_id]: + self.user_data[user_id]["custom_fields"] = {} + self.user_data[user_id]["custom_fields"][custom_field] = custom_value + fields_updated.append(f"Custom field '{custom_field}'") + + # Save data + self._save_user_data() + + # Create response embed + embed = discord.Embed( + title=f"User Data Updated", + description=f"Updated data for {user.mention}", + color=discord.Color.green() + ) + + if fields_updated: + embed.add_field(name="Fields Updated", value=", ".join(fields_updated), inline=False) + + # Show current values + embed.add_field(name="Current Values", value="User's current data:", inline=False) + for field, value in self.user_data[user_id].items(): + if field != "custom_fields": + embed.add_field(name=field.capitalize(), value=value, inline=True) + + # Show custom fields if any + if "custom_fields" in self.user_data[user_id] and self.user_data[user_id]["custom_fields"]: + custom_fields_text = "\n".join([f"**{k}**: {v}" for k, v in self.user_data[user_id]["custom_fields"].items()]) + embed.add_field(name="Custom Fields", value=custom_fields_text, inline=False) + else: + embed.description = "No fields were updated." + + await interaction.response.send_message(embed=embed, ephemeral=True) + + @app_commands.command(name="clearuserdata", description="Clear custom data for a user") + @app_commands.describe(user="The user to clear data for") + async def clearuserdata(self, interaction: discord.Interaction, user: discord.Member): + """Clear all custom data for a user - only works for the developer""" + # Check if the command user is the developer + if interaction.user.id != self.developer_id: + await interaction.response.send_message("This command is only available to the bot developer.", ephemeral=True) + return + + user_id = str(user.id) + if user_id in self.user_data: + del self.user_data[user_id] + self._save_user_data() + await interaction.response.send_message(f"All custom data for {user.mention} has been cleared.", ephemeral=True) + else: + await interaction.response.send_message(f"No custom data found for {user.mention}.", ephemeral=True) @app_commands.command(name="aboutuser", description="Display info about a user or yourself.") @app_commands.describe(user="The user to get info about (optional)") @@ -96,7 +228,7 @@ class UserInfoCog(commands.Cog): if getattr(user_flags, flag, False): badges.append(badge_map[flag]) badge_str = ", ".join(badges) if badges else "" - if member.id == 1141746562922459136: + if member.id == self.developer_id: badge_str = (badge_str + ", " if badge_str else "") + "Bot Developer πŸ› οΈ" # Embed embed = discord.Embed( @@ -133,9 +265,23 @@ class UserInfoCog(commands.Cog): # Nitro status (public flag) if user_flags and getattr(user_flags, 'premium', False): embed.add_field(name="Nitro", value="Yes", inline=True) + + # Add custom user data if available + user_id = str(member.id) + if user_id in self.user_data: + # Add custom data fields + for field, value in self.user_data[user_id].items(): + if field != "custom_fields": + embed.add_field(name=field.capitalize(), value=value, inline=True) + + # Add custom fields if any + if "custom_fields" in self.user_data[user_id] and self.user_data[user_id]["custom_fields"]: + custom_fields_text = "\n".join([f"**{k}**: {v}" for k, v in self.user_data[user_id]["custom_fields"].items()]) + embed.add_field(name="Custom Fields", value=custom_fields_text, inline=False) + # Pronouns (if available) pronouns = getattr(user_obj, 'pronouns', None) - if pronouns: + if pronouns and user_id not in self.user_data: # Only show Discord's pronouns if we don't have custom ones embed.add_field(name="Pronouns", value=pronouns, inline=True) # Locale/language (if available) locale = getattr(user_obj, 'locale', None) @@ -154,4 +300,4 @@ class UserInfoCog(commands.Cog): await interaction.response.send_message(embed=embed) async def setup(bot: commands.Bot): - await bot.add_cog(UserInfoCog(bot)) + await bot.add_cog(UserInfoCog(bot) \ No newline at end of file From dae237a4e95ce311172dc896c452ea5754e62c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Tue, 3 Jun 2025 21:47:47 +0900 Subject: [PATCH 25/28] Add new file --- user_data.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 user_data.json diff --git a/user_data.json b/user_data.json new file mode 100644 index 0000000..e69de29 From 6384a67f20fedcc9d3e091175fa61d04e71f4487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 5 Jun 2025 02:38:49 +0900 Subject: [PATCH 26/28] Add new file --- cogs/world_time.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 cogs/world_time.py diff --git a/cogs/world_time.py b/cogs/world_time.py new file mode 100644 index 0000000..f9c3d65 --- /dev/null +++ b/cogs/world_time.py @@ -0,0 +1,46 @@ +import discord +from discord import app_commands +from discord.ext import commands +import pytz +import random +import datetime + +class WorldTime(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(name="worldtime", description="Display time in different timezones") + @app_commands.describe(timezone="The timezone to display (optional)") + async def worldtime(self, interaction: discord.Interaction, timezone: str = None): + now = datetime.datetime.now(datetime.timezone.utc) + + if timezone: + # User specified a timezone + try: + tz = pytz.timezone(timezone) + local_time = now.astimezone(tz) + await interaction.response.send_message(f"**Time in {timezone}**: {local_time.strftime('%Y-%m-%d %H:%M:%S')}") + except pytz.exceptions.UnknownTimeZoneError: + # Handle invalid timezone + await interaction.response.send_message(f"Unknown timezone: `{timezone}`\nTry using a timezone from the IANA Time Zone Database (e.g., 'America/New_York', 'Europe/London')", ephemeral=True) + else: + # No timezone specified, show 5 random timezones + all_timezones = list(pytz.all_timezones) + random_timezones = random.sample(all_timezones, 5) + + embed = discord.Embed(title="World Time", color=discord.Color.blue()) + embed.description = "Here are 5 random timezones:" + + for tz_name in random_timezones: + tz = pytz.timezone(tz_name) + local_time = now.astimezone(tz) + embed.add_field( + name=tz_name, + value=local_time.strftime('%Y-%m-%d %H:%M:%S'), + inline=False + ) + + await interaction.response.send_message(embed=embed) + +async def setup(bot): + await bot.add_cog(WorldTime(bot)) From c889e9be0fea74137c68980c2ac3e6519ae5eda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 5 Jun 2025 02:41:42 +0900 Subject: [PATCH 27/28] Edit world_time.py --- cogs/world_time.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cogs/world_time.py b/cogs/world_time.py index f9c3d65..22d48aa 100644 --- a/cogs/world_time.py +++ b/cogs/world_time.py @@ -9,13 +9,12 @@ class WorldTime(commands.Cog): def __init__(self, bot): self.bot = bot - @app_commands.command(name="worldtime", description="Display time in different timezones") + @app_commands.command(name="world_time", description="Display timezones") @app_commands.describe(timezone="The timezone to display (optional)") async def worldtime(self, interaction: discord.Interaction, timezone: str = None): now = datetime.datetime.now(datetime.timezone.utc) if timezone: - # User specified a timezone try: tz = pytz.timezone(timezone) local_time = now.astimezone(tz) @@ -24,12 +23,12 @@ class WorldTime(commands.Cog): # Handle invalid timezone await interaction.response.send_message(f"Unknown timezone: `{timezone}`\nTry using a timezone from the IANA Time Zone Database (e.g., 'America/New_York', 'Europe/London')", ephemeral=True) else: - # No timezone specified, show 5 random timezones + # no selected zone so displays fuckass zones :3 all_timezones = list(pytz.all_timezones) random_timezones = random.sample(all_timezones, 5) embed = discord.Embed(title="World Time", color=discord.Color.blue()) - embed.description = "Here are 5 random timezones:" + embed.description = "Timezones" for tz_name in random_timezones: tz = pytz.timezone(tz_name) From b14984d5b5fe0c803b1f305a1ee15678162478dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B6=E3=82=AB=E3=83=AA=E3=82=A2=E3=82=B9=E3=83=BB?= =?UTF-8?q?=E3=82=A6=E3=82=A3=E3=83=AA=E3=82=A2=E3=83=A0=E3=83=BB=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=BC?= <26689019-pancakes1234@users.noreply.gitlab.com> Date: Thu, 5 Jun 2025 02:43:08 +0900 Subject: [PATCH 28/28] Edit keys.env --- keys.env | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/keys.env b/keys.env index 43dd235..b3a4252 100644 --- a/keys.env +++ b/keys.env @@ -1,7 +1 @@ -SLIPSTREAM_OPENROUTER_KEY=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 -DISCORD_TOKEN="MTM2MTgwNTE3MjkxNzE0MTgzNQ.G1II_V.OhJx67VHlKHBj4gCWTTbQoI45T0rfL2gtk15vs" -AI1_API_KEY="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" -SERVICE_KEY="2359" -GENIUS_API_KEY = "spwrTZ3d4KGmczcxSVlgkH7q5b4CE0cDJ2EudX57dHJ74d5ce60IKCxJNQ8o4nJBcIelHd-nne5_6dv2Z-R9oA" -AI_API_KEY = "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" -SERP_API_KEY, = "134ecd1798cd4853cf70a4a4a055f9bcd3bda45e9731b66fe6a16748c3462e57" \ No newline at end of file +placeholder \ No newline at end of file