Update configuration and README for improved setup and security; disable default API and emoji sync, and enhance Discord permission checks in API routes.
This commit is contained in:
@@ -26,24 +26,61 @@ server = "https://discord.gg/codexdev"
|
||||
serverLink = "https://discord.gg/codexdev"
|
||||
ch = "https://discord.com/channels/699587669059174461/1271825678710476911"
|
||||
|
||||
CMD_WEBHOOK_URL = os.getenv("CMD_WEBHOOK_URL")
|
||||
# ── Security / feature toggles ────────────────────────────────────────────────
|
||||
|
||||
def _env_bool(key: str, default: bool = False) -> bool:
|
||||
raw = os.getenv(key, str(default)).strip().lower()
|
||||
return raw in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
JISHAKU_ENABLED = _env_bool("JISHAKU_ENABLED", False)
|
||||
API_ENABLED = _env_bool("API_ENABLED", False)
|
||||
TUNNEL_ENABLED = _env_bool("TUNNEL_ENABLED", False)
|
||||
TUNNEL_ALLOW_BINARY_DOWNLOAD = _env_bool("TUNNEL_ALLOW_BINARY_DOWNLOAD", False)
|
||||
|
||||
API_HOST = os.getenv("API_HOST", "127.0.0.1").strip() or "127.0.0.1"
|
||||
API_PORT = int(os.getenv("API_PORT", "8000"))
|
||||
|
||||
# ── Webhooks (only active when explicitly configured) ─────────────────────────
|
||||
|
||||
def _valid_webhook_url(url: str | None) -> str | None:
|
||||
if not url:
|
||||
return None
|
||||
url = url.strip()
|
||||
if not url or url == "https://discord.com/api/webhooks/":
|
||||
return None
|
||||
if "discord.com/api/webhooks/" not in url and "discordapp.com/api/webhooks/" not in url:
|
||||
return None
|
||||
return url
|
||||
|
||||
CMD_WEBHOOK_URL = _valid_webhook_url(os.getenv("CMD_WEBHOOK_URL"))
|
||||
|
||||
# ── Optional Discord log / stats channels (unset = disabled) ──────────────────
|
||||
|
||||
def _parse_optional_id(env_key: str) -> int | None:
|
||||
raw = os.getenv(env_key, "").strip()
|
||||
if not raw or not raw.isdigit():
|
||||
return None
|
||||
return int(raw)
|
||||
|
||||
LOG_CHANNEL_ID = _parse_optional_id("LOG_CHANNEL_ID")
|
||||
SERVER_COUNT_CHANNEL_ID = _parse_optional_id("SERVER_COUNT_CHANNEL_ID")
|
||||
USER_COUNT_CHANNEL_ID = _parse_optional_id("USER_COUNT_CHANNEL_ID")
|
||||
|
||||
# ── Owner / Staff IDs ─────────────────────────────────────────────────────────
|
||||
# Edit OWNER_IDS in .env — comma-separated, no spaces needed.
|
||||
# Example: OWNER_IDS = 870179991462236170,767979794411028491,1432771000629596225
|
||||
# REQUIRED: set OWNER_IDS in .env — no hardcoded fallback IDs.
|
||||
|
||||
def _parse_ids(env_key: str, defaults: list[int]) -> list[int]:
|
||||
def _parse_ids(env_key: str) -> list[int]:
|
||||
raw = os.getenv(env_key, "").strip()
|
||||
if not raw:
|
||||
return defaults
|
||||
ids = [int(p.strip()) for p in raw.split(",") if p.strip().isdigit()]
|
||||
return ids or defaults
|
||||
return []
|
||||
return [int(p.strip()) for p in raw.split(",") if p.strip().isdigit()]
|
||||
|
||||
OWNER_IDS: list[int] = _parse_ids("OWNER_IDS", [870179991462236170])
|
||||
OWNER_IDS: list[int] = _parse_ids("OWNER_IDS")
|
||||
OWNER_IDS_STR: list[str] = [str(i) for i in OWNER_IDS]
|
||||
|
||||
# Aliases kept for backwards compatibility with files that import these names
|
||||
BOT_OWNER_IDS = OWNER_IDS
|
||||
BOT_OWNER_IDS_STR = OWNER_IDS_STR
|
||||
STAFF_IDS = OWNER_IDS
|
||||
STAFF_IDS_STR = OWNER_IDS_STR
|
||||
STAFF_IDS_STR = OWNER_IDS_STR
|
||||
|
||||
@@ -13,14 +13,15 @@
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import discord
|
||||
from utils.config import BotName
|
||||
try:
|
||||
from discord.ext import menus
|
||||
from discord.ext import commands
|
||||
except ModuleNotFoundError:
|
||||
os.system("pip install git+https://github.com/Rapptz/discord-ext-menus")
|
||||
except ModuleNotFoundError as exc:
|
||||
raise ImportError(
|
||||
"discord-ext-menus is required. Install it with: pip install git+https://github.com/Rapptz/discord-ext-menus"
|
||||
) from exc
|
||||
|
||||
from .paginator import Paginator as EmbedPaginator
|
||||
from discord.ext.commands import Context, Paginator as CmdPaginator
|
||||
|
||||
66
bot/utils/safe_parse.py
Normal file
66
bot/utils/safe_parse.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ Safe parsing helpers — no eval() on untrusted data ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import operator
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_role_id_list(raw: str | None) -> list[int]:
|
||||
"""Parse a stored role-ID list (JSON or legacy Python repr)."""
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
parsed = ast.literal_eval(raw)
|
||||
except (ValueError, SyntaxError):
|
||||
return []
|
||||
if not isinstance(parsed, list):
|
||||
return []
|
||||
return [int(x) for x in parsed if str(x).isdigit()]
|
||||
|
||||
|
||||
def dump_role_id_list(ids: list[int]) -> str:
|
||||
return json.dumps(ids)
|
||||
|
||||
|
||||
_MATH_ALLOWED = re.compile(r"^[\d+\-*/().\s]+$")
|
||||
|
||||
|
||||
def _eval_math_node(node: ast.AST) -> float:
|
||||
if isinstance(node, ast.Expression):
|
||||
return _eval_math_node(node.body)
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
||||
return float(node.value)
|
||||
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
|
||||
value = _eval_math_node(node.operand)
|
||||
return value if isinstance(node.op, ast.UAdd) else -value
|
||||
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)):
|
||||
left = _eval_math_node(node.left)
|
||||
right = _eval_math_node(node.right)
|
||||
ops = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
}
|
||||
if isinstance(node.op, ast.Div) and right == 0:
|
||||
raise ZeroDivisionError("division by zero")
|
||||
return ops[type(node.op)](left, right)
|
||||
raise ValueError("unsupported expression")
|
||||
|
||||
|
||||
def safe_math_eval(expression: str) -> str:
|
||||
"""Evaluate basic arithmetic safely (digits, +, -, *, /, parentheses)."""
|
||||
expr = expression.strip()
|
||||
if not expr or not _MATH_ALLOWED.match(expr):
|
||||
raise ValueError("invalid characters in expression")
|
||||
tree = ast.parse(expr, mode="eval")
|
||||
return str(_eval_math_node(tree))
|
||||
@@ -50,10 +50,10 @@ import threading
|
||||
import subprocess
|
||||
|
||||
# ── env vars ──────────────────────────────────────────────────────────────────
|
||||
TUNNEL_ENABLED = os.getenv("TUNNEL_ENABLED", "true").strip().lower() == "true"
|
||||
from utils.config import TUNNEL_ENABLED, TUNNEL_ALLOW_BINARY_DOWNLOAD, API_PORT
|
||||
|
||||
CF_TUNNEL_TOKEN = os.getenv("CF_TUNNEL_TOKEN", "").strip() # token from Cloudflare dashboard
|
||||
CF_TUNNEL_URL = os.getenv("CF_TUNNEL_URL", "").strip() # e.g. https://api.yourdomain.com
|
||||
API_PORT = int(os.getenv("API_PORT", "8000"))
|
||||
|
||||
# ── colours ───────────────────────────────────────────────────────────────────
|
||||
_CYAN = "\033[36m"
|
||||
@@ -64,30 +64,15 @@ _RESET = "\033[0m"
|
||||
|
||||
|
||||
def _ensure_pycloudflared() -> bool:
|
||||
"""Install pycloudflared via pip if it's not available. Returns True on success."""
|
||||
"""Return True if pycloudflared is installed (no automatic pip install)."""
|
||||
try:
|
||||
import pycloudflared # noqa: F401
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
print(f"{_YELLOW}◈ Tunnel: pycloudflared not found — installing via pip…{_RESET}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[__import__("sys").executable, "-m", "pip", "install", "pycloudflared"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=120,
|
||||
text=True,
|
||||
print(
|
||||
f"{_RED}◈ Tunnel: pycloudflared is not installed.\n"
|
||||
f" Install manually: pip install pycloudflared{_RESET}"
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f"{_GREEN}◈ Tunnel: pycloudflared installed successfully.{_RESET}")
|
||||
return True
|
||||
else:
|
||||
print(f"{_RED}◈ Tunnel: pip install failed:\n{result.stdout}{_RESET}")
|
||||
return False
|
||||
except Exception as exc:
|
||||
print(f"{_RED}◈ Tunnel: could not install pycloudflared — {exc}{_RESET}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -157,8 +142,9 @@ def _get_binary() -> str | None:
|
||||
import shutil
|
||||
import stat
|
||||
|
||||
# ── Step 0: make sure pycloudflared is installed ──────────────────────────
|
||||
_ensure_pycloudflared()
|
||||
# ── Step 0: check pycloudflared is installed ─────────────────────────────
|
||||
if not _ensure_pycloudflared():
|
||||
return None
|
||||
|
||||
candidates: list[str] = []
|
||||
|
||||
@@ -222,7 +208,15 @@ def _get_binary() -> str | None:
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
continue
|
||||
|
||||
# ── Step 4: direct GitHub download as last resort ─────────────────────────
|
||||
# ── Step 4: direct GitHub download (opt-in only) ─────────────────────────
|
||||
if not TUNNEL_ALLOW_BINARY_DOWNLOAD:
|
||||
print(
|
||||
f"{_YELLOW}◈ Tunnel: no working binary found.\n"
|
||||
f" Install pycloudflared manually, or set TUNNEL_ALLOW_BINARY_DOWNLOAD=true "
|
||||
f"to allow downloading cloudflared from GitHub.{_RESET}"
|
||||
)
|
||||
return None
|
||||
|
||||
print(f"{_YELLOW}◈ Tunnel: no working binary found — attempting direct download…{_RESET}")
|
||||
direct = _download_cloudflared_direct()
|
||||
if direct and os.path.isfile(direct):
|
||||
@@ -288,7 +282,7 @@ def _run_tunnel(binary: str, token: str, port: int, public_url: str) -> None:
|
||||
announced = True
|
||||
if public_url:
|
||||
print(f"{_GREEN}◈ Tunnel: API is live at {public_url}{_RESET}")
|
||||
print(f"{_CYAN} ↳ NEXT_PUBLIC_API_URL = {public_url}/api/v1{_RESET}")
|
||||
print(f"{_CYAN} ↳ DASHBOARD_API_URL = {public_url}/api/v1{_RESET}")
|
||||
else:
|
||||
print(f"{_GREEN}◈ Tunnel: connected — check CF_TUNNEL_URL in .env for your public URL{_RESET}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user