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 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

- +
- - - - - +
diff --git a/bot.py b/bot.py index 83036cb..89e4d2d 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 @@ -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) @@ -98,7 +100,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]}") 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.") 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)) 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): 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: 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)) diff --git a/cogs/update.py b/cogs/update.py index 8e59bd7..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 = "/home/server/wdiscordbotserver/" - repo_url = "https://gitlab.com/pancakes1234/wdiscordbotserver.git" - restart_script = "/home/server/wdiscordbotserver/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)) diff --git a/cogs/userinfo.py b/cogs/userinfo.py new file mode 100644 index 0000000..08de432 --- /dev/null +++ b/cogs/userinfo.py @@ -0,0 +1,303 @@ +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)") + 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 + # Status + if isinstance(member, discord.Member): + status = str(member.status).capitalize() + else: + status = "Unknown" + # 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") 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): + # 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: + 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" + + # 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 == self.developer_id: + 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 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) + 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) + # 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 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) + + # 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 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) + 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) + +async def setup(bot: commands.Bot): + await bot.add_cog(UserInfoCog(bot) \ No newline at end of file diff --git a/cogs/world_time.py b/cogs/world_time.py new file mode 100644 index 0000000..22d48aa --- /dev/null +++ b/cogs/world_time.py @@ -0,0 +1,45 @@ +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="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: + 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 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 = "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)) diff --git a/keys.env b/keys.env new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/keys.env @@ -0,0 +1 @@ +placeholder \ No newline at end of file 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 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 diff --git a/user_data.json b/user_data.json new file mode 100644 index 0000000..e69de29