Merge branch 'main' into 'main'

feat: Enhance user info display with additional fields for accent color,...

See merge request pancakes1234/wdiscordbotserver!3
This commit is contained in:
ザカリアス・ウィリアム・ポージー 2025-06-05 02:44:04 +09:00
commit c9a56a9bfe
15 changed files with 898 additions and 527 deletions

View File

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

View File

@ -1,21 +1,50 @@
<?php
// Improved Discord Bot Admin API
// Error reporting (disable on production)
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Configure secure session parameters:
session_set_cookie_params([
'httponly' => 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();
?>
<!DOCTYPE html>
<html>
@ -64,8 +93,9 @@ if (!isset($_SESSION['logged_in'])) {
<body>
<div class="login-container">
<h2>Login</h2>
<?php if(isset($error)) echo "<p class='error'>{$error}</p>"; ?>
<form method="POST">
<?php if(isset($error)) { echo "<p class='error'>" . htmlspecialchars($error) . "</p>"; } ?>
<form method="POST" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($loginToken); ?>">
<label>Username</label>
<input type="text" name="username" required>
<label>Password</label>
@ -76,65 +106,76 @@ if (!isset($_SESSION['logged_in'])) {
</body>
</html>
<?php
exit; // Do not execute the rest until logged in.
exit;
}
// === End Authentication ===
// --- Action Handlers ---
$output = "";
// Determine which action to do.
$action = "";
if (isset($_GET['action'])) {
$action = $_GET['action'];
} elseif (isset($_POST['action'])) {
$action = $_POST['action'];
function handleVersionAction() {
$botDir = '/home/server/wdiscordbotserver';
if (is_dir($botDir)) {
$cmd = 'cd ' . escapeshellarg($botDir) . ' && git rev-parse HEAD 2>&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) {
<head>
<title>Discord Bot Admin API</title>
<style>
/* General styling */
body {
font-family: Arial, sans-serif;
background: #e9e9e9;
@ -160,7 +200,7 @@ switch ($action) {
header {
margin-bottom: 20px;
}
header button {
header button, header form button {
padding: 10px 20px;
margin-right: 10px;
border: none;
@ -169,7 +209,7 @@ switch ($action) {
cursor: pointer;
border-radius: 3px;
}
header button:hover {
header button:hover, header form button:hover {
background: #0056b3;
}
.output {
@ -211,7 +251,7 @@ switch ($action) {
}
</style>
<script>
// For Version and Update actions we can use fetch to load the result via AJAX.
// For Version and Update actions we can use AJAX.
function doAction(action) {
if (action === "data" || action === "terminal") {
window.location.href = "?action=" + action;
@ -220,116 +260,114 @@ switch ($action) {
.then(response => response.text())
.then(data => {
document.getElementById("output").innerText = data;
})
.catch(err => {
document.getElementById("output").innerText = "Error: " + err;
});
}
}
// SSH Connect functionality using the "ssh://" protocol.
function doSSH() {
var sshUser = "<?php echo getenv('user'); ?>"; // SSH username from the env.
var sshHost = window.location.hostname; // use current hostname.
var sshUser = "<?php echo htmlspecialchars(getenv('user')); ?>";
var sshHost = window.location.hostname;
window.location.href = "ssh://" + sshUser + "@" + sshHost;
}
function showTab(tab) {
document.getElementById('wetty').style.display = (tab === 'wetty') ? 'block' : 'none';
document.getElementById('xterm').style.display = (tab === 'xterm') ? 'block' : 'none';
}
window.addEventListener('load', function() {
if(document.getElementById('xterm-container')) {
const terminal = new Terminal();
terminal.open(document.getElementById('xterm-container'));
const socket = new WebSocket('ws://' + window.location.hostname + ':3001');
socket.onopen = function() {
terminal.write("Connected to shell\r\n");
};
terminal.onData(function(data) {
socket.send(data);
});
socket.onmessage = function(event) {
terminal.write(event.data);
};
socket.onerror = function() {
terminal.write("\r\nError connecting to shell.\r\n");
};
socket.onclose = function() {
terminal.write("\r\nConnection closed.\r\n");
};
}
});
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
</head>
<body>
<div class="container">
<header>
<h1>Discord Bot Admin API</h1>
<button onclick="doAction('version')">Version</button>
<button onclick="doAction('update')">Update</button>
<!-- Update action now uses a form (POST with CSRF token) to improve safety -->
<form style="display:inline;" method="POST" action="?action=update" onsubmit="return confirm('Are you sure you want to update the bot?');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(getCsrfToken()); ?>">
<button type="submit">Update</button>
</form>
<button onclick="window.location.href='?action=data'">Data</button>
<button onclick="doSSH()">SSH Connect</button>
<button onclick="window.location.href='?action=terminal'">Terminal</button>
</header>
<!-- Output area for AJAX-returned actions -->
<!-- AJAX Output Area -->
<div id="output" class="output"><?php echo htmlspecialchars($output); ?></div>
<?php
// === Data Section ===
if ($action === "data") {
if (!isset($_GET['file'])) {
$baseDir = '/home/server';
echo "<h2>Files in {$baseDir}</h2>";
echo "<ul class='file-list'>";
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($baseDir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $fileInfo) {
$filePath = $fileInfo->getPathname();
echo "<li><a href='?action=data&file=" . urlencode($filePath) . "'>" . htmlspecialchars($filePath) . "</a></li>";
<?php if ($action === "data"):
$baseDir = realpath('/home/server');
if (!isset($_GET['file'])): ?>
<h2>Files in <?php echo htmlspecialchars($baseDir); ?></h2>
<ul class="file-list">
<?php
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($baseDir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $fileInfo) {
$filePath = $fileInfo->getPathname();
echo "<li><a href='?action=data&file=" . urlencode($filePath) . "'>" . htmlspecialchars($filePath) . "</a></li>";
}
} catch (Exception $e) {
echo "<li>Error reading files: " . htmlspecialchars($e->getMessage()) . "</li>";
}
echo "</ul>";
} else {
?>
</ul>
<?php else:
$file = $_GET['file'];
$baseDir = realpath('/home/server');
$realFile = realpath($file);
if ($realFile === false || strpos($realFile, $baseDir) !== 0) {
echo "<p>Invalid file.</p>";
} else {
echo "<h2>Editing: " . htmlspecialchars($realFile) . "</h2>";
echo "<form method='POST' action='?action=data&file=" . urlencode($realFile) . "'>";
echo "<textarea name='content'>" . htmlspecialchars(file_get_contents($realFile)) . "</textarea><br>";
echo "<input type='hidden' name='action' value='data'>";
echo "<input type='submit' value='Save' style='padding:10px 20px; margin-top:10px;'>";
echo "</form>";
}
}
}
// === 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): ?>
<p>Invalid file.</p>
<?php else: ?>
<h2>Editing: <?php echo htmlspecialchars($realFile); ?></h2>
<form method="POST" action="?action=data&file=<?php echo urlencode($realFile); ?>">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(getCsrfToken()); ?>">
<textarea name="content"><?php echo htmlspecialchars(file_get_contents($realFile)); ?></textarea><br>
<input type="hidden" name="action" value="data">
<input type="submit" value="Save" style="padding:10px 20px; margin-top:10px;">
</form>
<?php endif;
endif;
elseif ($action === "terminal"): ?>
<div id="terminal-tabs" style="margin-bottom:10px;">
<button onclick="showTab('wetty')">Wetty Terminal</button>
<button onclick="showTab('xterm')">Xterm Terminal</button>
</div>
<div id="wetty" class="terminal-tab" style="display:block;">
<h2>Wetty Terminal</h2>
<iframe src="http://<?php echo $_SERVER['HTTP_HOST']; ?>:3000" width="100%" height="500px" frameborder="0"></iframe>
<iframe src="http://<?php echo htmlspecialchars($_SERVER['HTTP_HOST']); ?>:3000" width="100%" height="500px" frameborder="0"></iframe>
</div>
<div id="xterm" class="terminal-tab" style="display:none;">
<h2>Xterm Terminal</h2>
<div id="xterm-container" style="width: 100%; height: 500px;"></div>
</div>
<!-- Load xterm.js resources -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
<script>
function showTab(tab) {
document.getElementById('wetty').style.display = (tab === 'wetty') ? 'block' : 'none';
document.getElementById('xterm').style.display = (tab === 'xterm') ? 'block' : 'none';
}
// Initialize xterm.js for the Xterm Terminal.
// NOTE: You MUST run a WebSocket Node.js server (for example, using node-pty and ws)
// listening on port 3001 to support this connection.
window.addEventListener('load', function() {
// Initialize the terminal only once.
const terminal = new Terminal();
terminal.open(document.getElementById('xterm-container'));
const socket = new WebSocket('ws://<?php echo $_SERVER['HTTP_HOST']; ?>:3001');
socket.onopen = function() {
terminal.write("Connected to shell\r\n");
};
terminal.onData(function(data) {
socket.send(data);
});
socket.onmessage = function(event) {
terminal.write(event.data);
};
socket.onerror = function() {
terminal.write("\r\nError connecting to shell.\r\n");
};
socket.onclose = function() {
terminal.write("\r\nConnection closed.\r\n");
};
});
</script>
<?php
endif;
?>
<?php endif; ?>
</div>
</body>
</html>

6
bot.py
View File

@ -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]}")

View File

@ -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.")

View File

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

View File

@ -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):

View File

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

View File

@ -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 servers 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))

View File

@ -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...")
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}")
try:
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])
except Exception as 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))

303
cogs/userinfo.py Normal file
View File

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

45
cogs/world_time.py Normal file
View File

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

1
keys.env Normal file
View File

@ -0,0 +1 @@
placeholder

1
pip.txt Normal file
View File

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

181
setup.sh Normal file
View File

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

0
user_data.json Normal file
View File