modified: .gitignore
modified: Dockerfile modified: README.md modified: app.py
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -129,3 +129,6 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# Secrets and local cookies
|
||||||
|
cookies.txt
|
||||||
@@ -22,8 +22,8 @@ ENV DB_PORT=$DB_PORT
|
|||||||
ENV DB_NAME=$DB_NAME
|
ENV DB_NAME=$DB_NAME
|
||||||
ENV DB_USER=$DB_USER
|
ENV DB_USER=$DB_USER
|
||||||
ENV DB_PASSWORD=$DB_PASSWORD
|
ENV DB_PASSWORD=$DB_PASSWORD
|
||||||
ENV YTDL_COOKIES_B64=$YTDL_COOKIES_B64
|
|
||||||
ENV YTDL_COOKIES_FILE=$YTDL_COOKIES_FILE
|
ENV YTDL_COOKIES_FILE=$YTDL_COOKIES_FILE
|
||||||
|
ENV YTDL_COOKIES_B64=$YTDL_COOKIES_B64
|
||||||
ENV YTDL_COOKIES_FROM_BROWSER=$YTDL_COOKIES_FROM_BROWSER
|
ENV YTDL_COOKIES_FROM_BROWSER=$YTDL_COOKIES_FROM_BROWSER
|
||||||
ENV YTDL_UA=$YTDL_UA
|
ENV YTDL_UA=$YTDL_UA
|
||||||
ENV YTDL_YT_CLIENT=$YTDL_YT_CLIENT
|
ENV YTDL_YT_CLIENT=$YTDL_YT_CLIENT
|
||||||
|
|||||||
63
README.md
63
README.md
@@ -4,23 +4,18 @@ Ein Discord Bot für Hearts of Iron IV, der über Coolify deployed werden kann.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ELO-System (Standard/Competitive), Spiele-Lifecycle (/hoi4create, /hoi4setup, /hoi4end)
|
- **Basic Commands**: Ping, Info, Server-Informationen
|
||||||
- Stats, History und Leaderboard
|
- **HOI4 Theme**: Speziell für Hearts of Iron IV Communities
|
||||||
- Emoji-/Länder-Tag-Unterstützung (tags.txt, emotes.markdown)
|
- **Docker Support**: Bereit für Deployment über Coolify
|
||||||
- Automatische Rollenvergabe nach ELO (pro Kategorie)
|
- **Error Handling**: Robuste Fehlerbehandlung
|
||||||
- YouTube Musikplayer (yt-dlp + ffmpeg) mit Queue und Lautstärke
|
- **Logging**: Ausführliche Logs für Debugging
|
||||||
- Docker/Coolify ready
|
|
||||||
|
|
||||||
## Verfügbare Befehle (Auszug)
|
## Verfügbare Befehle
|
||||||
|
|
||||||
- `/hoi4create <type> <name>` – Neues Spiel anlegen (type: standard/competitive)
|
- `!ping` - Zeigt die Bot-Latenz an
|
||||||
- `/hoi4setup <game> <user> <team> <t-level> [country]` – Spieler hinzufügen (T1/2/3; country optional als HOI4-Tag)
|
- `!info` - Zeigt Bot-Informationen an
|
||||||
- `/hoi4end <game> <winner_team|draw>` – Spiel beenden, ELO berechnen, Rollen syncen
|
- `!help_hoi4` - Zeigt alle verfügbaren Befehle an
|
||||||
- `/hoi4stats [user]` – ELO, Rank, Win/Draw/Loss
|
- `!server_info` - Zeigt Informationen über den aktuellen Server
|
||||||
- `/hoi4history [limit] [player] [name] [type]` – Abgeschlossene Spiele filtern
|
|
||||||
- `/hoi4leaderboard [type] [limit]` – Top ELOs
|
|
||||||
- Musik: `/join`, `/leave`, `/play <url|search>`, `/skip`, `/stop`, `/pause`, `/resume`, `/queue`, `/np`, `/volume <0-200>`
|
|
||||||
- Diagnose: `/webtest [query]` – Testet Web-Konnektivität und yt-dlp-Extraction
|
|
||||||
|
|
||||||
## Setup für Coolify
|
## Setup für Coolify
|
||||||
|
|
||||||
@@ -43,13 +38,8 @@ Der Bot benötigt folgende Permissions:
|
|||||||
### 3. Coolify Deployment
|
### 3. Coolify Deployment
|
||||||
|
|
||||||
1. **Repository**: Verbinde dein Git Repository mit Coolify
|
1. **Repository**: Verbinde dein Git Repository mit Coolify
|
||||||
2. **Umgebungsvariablen**: Setze folgende Variablen in Coolify:
|
2. **Umgebungsvariablen**: Setze folgende Variable in Coolify:
|
||||||
- `DISCORD_TOKEN` = Dein Bot Token
|
- `DISCORD_TOKEN` = Dein Bot Token
|
||||||
- Entweder `DATABASE_URL` (mysql://user:pass@host:port/db) ODER Einzelwerte `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
|
|
||||||
- Für YouTube (optional, empfohlen bei „Sign in to confirm you’re not a bot“):
|
|
||||||
- `YTDL_COOKIES_B64` = Base64-kodierte cookies.txt (Netscape-Format), ODER
|
|
||||||
- `YTDL_COOKIES_FILE` = Pfad zu cookies.txt im Container (z. B. /app/cookies.txt)
|
|
||||||
- Optional: `YTDL_UA` (User-Agent), `YTDL_YT_CLIENT` (z. B. android/web)
|
|
||||||
|
|
||||||
3. **Build Settings**:
|
3. **Build Settings**:
|
||||||
- Coolify wird automatisch das Dockerfile verwenden
|
- Coolify wird automatisch das Dockerfile verwenden
|
||||||
@@ -86,37 +76,6 @@ Der Bot benötigt folgende Permissions:
|
|||||||
python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### YouTube-Cookies bereitstellen (empfohlen)
|
|
||||||
|
|
||||||
YouTube kann Server/Container ohne Cookies blockieren („Sign in to confirm you’re not a bot“). Übergib Cookies im Netscape-Format:
|
|
||||||
|
|
||||||
Variante A – Base64 via Env (einfach in Coolify)
|
|
||||||
|
|
||||||
1. Exportiere cookies.txt aus deinem Browser (Erweiterung „Get cookies.txt“ oder yt-dlp Export aus lokaler Browser-Session).
|
|
||||||
2. Base64-kodiere die Datei (Windows PowerShell):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
[Convert]::ToBase64String([IO.File]::ReadAllBytes("cookies.txt")) | Set-Clipboard
|
|
||||||
```
|
|
||||||
|
|
||||||
3. In Coolify `YTDL_COOKIES_B64` setzen (Inhalt aus der Zwischenablage einfügen) und neu deployen.
|
|
||||||
|
|
||||||
Variante B – Datei mounten
|
|
||||||
|
|
||||||
- cookies.txt in den Container mounten (z. B. `/app/cookies.txt`) und `YTDL_COOKIES_FILE=/app/cookies.txt` setzen.
|
|
||||||
|
|
||||||
Optional: `YTDL_UA` setzen (aktueller Browser UA), `YTDL_YT_CLIENT` (z. B. `android`).
|
|
||||||
|
|
||||||
### Web testen (/webtest)
|
|
||||||
|
|
||||||
Mit `/webtest` prüft der Bot:
|
|
||||||
|
|
||||||
- HTTP-Reachability (Google 204, YouTube, Discord API)
|
|
||||||
- yt-dlp-Extraction mit deiner Query oder einem Default-Suchbegriff
|
|
||||||
- ob Cookies konfiguriert sind (Datei/B64/Browser)
|
|
||||||
|
|
||||||
So kannst du schnell sehen, ob der Container Internetzugriff hat und ob yt-dlp wegen Cookies scheitert.
|
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
### Lokal mit Docker testen
|
### Lokal mit Docker testen
|
||||||
|
|||||||
224
app.py
224
app.py
@@ -11,8 +11,6 @@ from typing import Optional, List, Dict
|
|||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import functools
|
import functools
|
||||||
import traceback
|
import traceback
|
||||||
import aiohttp
|
|
||||||
import time
|
|
||||||
import base64
|
import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -374,40 +372,33 @@ except Exception as e:
|
|||||||
print(f"⚠️ Cookie configuration error: {e}")
|
print(f"⚠️ Cookie configuration error: {e}")
|
||||||
|
|
||||||
YTDL_OPTS = {
|
YTDL_OPTS = {
|
||||||
'format': 'bestaudio[ext=m4a]/bestaudio/best',
|
'format': 'bestaudio/best',
|
||||||
'noplaylist': True,
|
'noplaylist': True,
|
||||||
'quiet': True,
|
'quiet': True,
|
||||||
'default_search': 'ytsearch',
|
'default_search': 'ytsearch',
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_ytdl_opts(client_hint: Optional[str] = None, use_cookies: bool = True) -> Dict:
|
def get_ytdl_opts() -> Dict:
|
||||||
"""Build yt-dlp options dynamically, injecting cookies and headers if configured.
|
"""Build yt-dlp options dynamically, injecting cookies and headers if configured."""
|
||||||
client_hint: preferred YouTube client (e.g., 'android', 'web', 'ios', 'tv').
|
|
||||||
"""
|
|
||||||
opts = dict(YTDL_OPTS)
|
opts = dict(YTDL_OPTS)
|
||||||
# UA and extractor tweaks
|
# UA and extractor tweaks
|
||||||
ua = os.getenv('YTDL_UA') or (
|
ua = os.getenv('YTDL_UA') or (
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
||||||
'(KHTML, like Gecko) Chrome/118.0 Safari/537.36'
|
'(KHTML, like Gecko) Chrome/118.0 Safari/537.36'
|
||||||
)
|
)
|
||||||
yt_client_env = os.getenv('YTDL_YT_CLIENT', 'android')
|
yt_client = os.getenv('YTDL_YT_CLIENT', 'android')
|
||||||
yt_client = client_hint or yt_client_env
|
|
||||||
opts['http_headers'] = {'User-Agent': ua}
|
opts['http_headers'] = {'User-Agent': ua}
|
||||||
# Extractor args can help avoid some player config checks
|
# Extractor args can help avoid some player config checks
|
||||||
opts.setdefault('extractor_args', {})
|
opts.setdefault('extractor_args', {})
|
||||||
opts['extractor_args'].setdefault('youtube', {})
|
opts['extractor_args'].setdefault('youtube', {})
|
||||||
# player_client hint
|
# player_client hint (e.g., android)
|
||||||
opts['extractor_args']['youtube']['player_client'] = [yt_client]
|
opts['extractor_args']['youtube']['player_client'] = [yt_client]
|
||||||
# Use cookies if available
|
# Use cookies if available
|
||||||
if use_cookies:
|
if YTDL_COOKIEFILE:
|
||||||
if YTDL_COOKIEFILE:
|
opts['cookiefile'] = YTDL_COOKIEFILE
|
||||||
opts['cookiefile'] = YTDL_COOKIEFILE
|
elif YTDL_COOKIESFROMBROWSER:
|
||||||
elif YTDL_COOKIESFROMBROWSER:
|
opts['cookiesfrombrowser'] = YTDL_COOKIESFROMBROWSER
|
||||||
opts['cookiesfrombrowser'] = YTDL_COOKIESFROMBROWSER
|
|
||||||
# Optional force IPv4 (set YTDL_FORCE_IPV4=1)
|
|
||||||
if os.getenv('YTDL_FORCE_IPV4'):
|
|
||||||
opts['force_ipv4'] = True
|
|
||||||
return opts
|
return opts
|
||||||
|
|
||||||
FFMPEG_BEFORE = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5"
|
FFMPEG_BEFORE = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5"
|
||||||
@@ -453,81 +444,24 @@ async def _ensure_connected(ctx: commands.Context) -> Optional[discord.VoiceClie
|
|||||||
async def _ytdlp_extract(loop: asyncio.AbstractEventLoop, query: str) -> Optional[Dict]:
|
async def _ytdlp_extract(loop: asyncio.AbstractEventLoop, query: str) -> Optional[Dict]:
|
||||||
if not ytdlp:
|
if not ytdlp:
|
||||||
return None
|
return None
|
||||||
|
def _extract():
|
||||||
# Try multiple YouTube client profiles to bypass some restrictions
|
with ytdlp.YoutubeDL(get_ytdl_opts()) as ytdl:
|
||||||
env_client = os.getenv('YTDL_YT_CLIENT', 'android')
|
return ytdl.extract_info(query, download=False)
|
||||||
cookies_present = bool(YTDL_COOKIEFILE or YTDL_COOKIESFROMBROWSER)
|
|
||||||
|
|
||||||
# Helper to try a sequence of clients with/without cookies
|
|
||||||
async def try_clients(clients: List[str], use_cookies: bool) -> Optional[Dict]:
|
|
||||||
nonlocal query
|
|
||||||
last_err: Optional[Exception] = None
|
|
||||||
for client in clients:
|
|
||||||
def _extract_with_client():
|
|
||||||
with ytdlp.YoutubeDL(get_ytdl_opts(client, use_cookies=use_cookies)) as ytdl:
|
|
||||||
return ytdl.extract_info(query, download=False)
|
|
||||||
try:
|
|
||||||
info = await loop.run_in_executor(executor, _extract_with_client)
|
|
||||||
if info is None:
|
|
||||||
continue
|
|
||||||
if 'entries' in info:
|
|
||||||
info = info['entries'][0]
|
|
||||||
return info
|
|
||||||
except Exception as e:
|
|
||||||
last_err = e
|
|
||||||
msg = str(e)
|
|
||||||
print(f"⚠️ yt-dlp attempt failed (cookies={'on' if use_cookies else 'off'}) client='{client}': {msg[:200]}")
|
|
||||||
retriable_markers = (
|
|
||||||
'Sign in to confirm', 'Use --cookies', 'pass cookies', 'HTTP Error 403',
|
|
||||||
'Signature extraction failed', 'Requested format is not available',
|
|
||||||
'Only images are available', 'missing a url', 'SABR streaming'
|
|
||||||
)
|
|
||||||
if any(m in msg for m in retriable_markers):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
if last_err:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Build ordered lists
|
|
||||||
# When cookies are present, android/ios are skipped by yt-dlp; so try web/tv first with cookies.
|
|
||||||
# If that fails (e.g., SABR), try without cookies to enable android extraction.
|
|
||||||
cookie_clients: List[str] = []
|
|
||||||
for c in [env_client, 'web', 'tv']:
|
|
||||||
if c in ('web', 'tv') and c not in cookie_clients:
|
|
||||||
cookie_clients.append(c)
|
|
||||||
nocookie_clients: List[str] = []
|
|
||||||
for c in [env_client, 'android', 'ios', 'web', 'tv']:
|
|
||||||
if c not in nocookie_clients:
|
|
||||||
nocookie_clients.append(c)
|
|
||||||
|
|
||||||
last_error: Optional[Exception] = None
|
|
||||||
# Pass 1: with cookies (if provided)
|
|
||||||
if cookies_present:
|
|
||||||
info = await try_clients(cookie_clients, use_cookies=True)
|
|
||||||
if info:
|
|
||||||
return info
|
|
||||||
# Pass 2: without cookies to allow android/ios
|
|
||||||
info = await try_clients(nocookie_clients, use_cookies=False)
|
|
||||||
if info:
|
|
||||||
return info
|
|
||||||
else:
|
|
||||||
# No cookies: try standard nocookie list
|
|
||||||
info = await try_clients(nocookie_clients, use_cookies=False)
|
|
||||||
if info:
|
|
||||||
return info
|
|
||||||
|
|
||||||
# All attempts failed
|
|
||||||
# At this point, all attempts failed; print last traceback if any
|
|
||||||
try:
|
try:
|
||||||
if cookies_present:
|
info = await loop.run_in_executor(executor, _extract)
|
||||||
print("❌ yt-dlp: All attempts failed with and without cookies. Consider trying another client or refreshing cookies.")
|
if info is None:
|
||||||
else:
|
return None
|
||||||
print("❌ yt-dlp: All attempts failed without cookies. Consider providing cookies or changing YTDL_YT_CLIENT.")
|
if 'entries' in info:
|
||||||
except Exception:
|
info = info['entries'][0]
|
||||||
pass
|
return info
|
||||||
return None
|
except Exception as e:
|
||||||
|
# Make YouTube cookie issues clearer in logs
|
||||||
|
msg = str(e)
|
||||||
|
if 'Sign in to confirm you’re not a bot' in msg or 'Use --cookies' in msg or 'pass cookies' in msg:
|
||||||
|
print("❌ yt-dlp error: YouTube requires cookies to proceed.")
|
||||||
|
print(" Provide YTDL_COOKIES_FILE or YTDL_COOKIES_B64 (Netscape cookies.txt).")
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
async def _create_audio_source(loop: asyncio.AbstractEventLoop, search: str, volume: float):
|
async def _create_audio_source(loop: asyncio.AbstractEventLoop, search: str, volume: float):
|
||||||
# Accept either URL or search text; prepend ytsearch1: if not a URL
|
# Accept either URL or search text; prepend ytsearch1: if not a URL
|
||||||
@@ -622,11 +556,8 @@ async def play(ctx: commands.Context, *, query: str):
|
|||||||
await ctx.reply("🔎 Searching…")
|
await ctx.reply("🔎 Searching…")
|
||||||
item = await _create_audio_source(loop, query, state['volume'])
|
item = await _create_audio_source(loop, query, state['volume'])
|
||||||
if not item:
|
if not item:
|
||||||
# Give a hint if cookies or client change likely required
|
# Give a hint if cookies likely required
|
||||||
hint = (
|
hint = "If this is YouTube, the server IP may be challenged. Provide YTDL_COOKIES_FILE or YTDL_COOKIES_B64."
|
||||||
"If this is YouTube, try YTDL_YT_CLIENT=android (or web) and/or provide cookies via "
|
|
||||||
"YTDL_COOKIES_FILE or YTDL_COOKIES_B64."
|
|
||||||
)
|
|
||||||
await ctx.reply(f"❌ Couldn't get audio from that query.\n{hint}")
|
await ctx.reply(f"❌ Couldn't get audio from that query.\n{hint}")
|
||||||
return
|
return
|
||||||
state['queue'].append(item)
|
state['queue'].append(item)
|
||||||
@@ -713,83 +644,6 @@ async def volume(ctx: commands.Context, percent: int):
|
|||||||
now['source'].volume = vol
|
now['source'].volume = vol
|
||||||
await ctx.reply(f"🔊 Volume set to {percent}%.")
|
await ctx.reply(f"🔊 Volume set to {percent}%.")
|
||||||
|
|
||||||
# ------------- Connectivity / Web Test -------------
|
|
||||||
|
|
||||||
@bot.hybrid_command(name='webtest', description='Test outbound web access and YouTube extraction')
|
|
||||||
async def webtest(ctx: commands.Context, *, query: Optional[str] = None):
|
|
||||||
"""Check basic web reachability and try a lightweight yt-dlp extraction.
|
|
||||||
- Tests HTTP GET to a few endpoints
|
|
||||||
- Tries yt-dlp search or URL extraction (if yt-dlp is available)
|
|
||||||
"""
|
|
||||||
urls = [
|
|
||||||
("Google 204", "https://www.google.com/generate_204"),
|
|
||||||
("YouTube", "https://www.youtube.com"),
|
|
||||||
("Discord API", "https://discord.com/api/v10/gateway"),
|
|
||||||
]
|
|
||||||
results = []
|
|
||||||
timeout = aiohttp.ClientTimeout(total=8)
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
||||||
for name, url in urls:
|
|
||||||
t0 = time.monotonic()
|
|
||||||
status = None
|
|
||||||
err = None
|
|
||||||
try:
|
|
||||||
async with session.get(url, allow_redirects=True) as resp:
|
|
||||||
status = resp.status
|
|
||||||
except Exception as e:
|
|
||||||
err = str(e)
|
|
||||||
dt = (time.monotonic() - t0) * 1000
|
|
||||||
results.append((name, url, status, err, dt))
|
|
||||||
except Exception as e:
|
|
||||||
results.append(("session", "<session>", None, f"Session error: {e}", 0.0))
|
|
||||||
|
|
||||||
# Try yt-dlp extraction if available
|
|
||||||
ytdlp_ok = False
|
|
||||||
ytdlp_msg = ""
|
|
||||||
chosen = query or "ytsearch1:never gonna give you up"
|
|
||||||
try:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
info = await _ytdlp_extract(loop, chosen)
|
|
||||||
if info:
|
|
||||||
ytdlp_ok = True
|
|
||||||
ytdlp_msg = f"Success: {info.get('title', 'unknown title')}"
|
|
||||||
else:
|
|
||||||
ytdlp_msg = "No info returned (possibly cookies required or blocked)."
|
|
||||||
except Exception as e:
|
|
||||||
ytdlp_msg = f"Error: {e}"
|
|
||||||
|
|
||||||
# Cookies configured?
|
|
||||||
cookies_file = os.getenv('YTDL_COOKIES_FILE')
|
|
||||||
cookies_b64 = bool(os.getenv('YTDL_COOKIES_B64'))
|
|
||||||
from_browser = os.getenv('YTDL_COOKIES_FROM_BROWSER')
|
|
||||||
|
|
||||||
desc_lines = []
|
|
||||||
for name, url, status, err, dt in results:
|
|
||||||
if status is not None:
|
|
||||||
desc_lines.append(f"• {name}: {status} ({dt:.0f} ms)")
|
|
||||||
else:
|
|
||||||
desc_lines.append(f"• {name}: ❌ {err or 'unknown error'}")
|
|
||||||
|
|
||||||
desc = "\n".join(desc_lines)
|
|
||||||
embed = discord.Embed(title="🌐 Web Test", description=desc, color=discord.Color.teal())
|
|
||||||
embed.add_field(name="yt-dlp", value=("✅ " + ytdlp_msg) if ytdlp_ok else ("❌ " + ytdlp_msg), inline=False)
|
|
||||||
|
|
||||||
cookie_status = []
|
|
||||||
if cookies_file and os.path.exists(cookies_file):
|
|
||||||
cookie_status.append(f"file: {cookies_file}")
|
|
||||||
elif cookies_file:
|
|
||||||
cookie_status.append(f"file missing: {cookies_file}")
|
|
||||||
if cookies_b64:
|
|
||||||
cookie_status.append("b64: provided")
|
|
||||||
if from_browser:
|
|
||||||
cookie_status.append(f"from-browser: {from_browser}")
|
|
||||||
if not cookie_status:
|
|
||||||
cookie_status.append("none")
|
|
||||||
embed.add_field(name="yt-dlp cookies", value=", ".join(cookie_status), inline=False)
|
|
||||||
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
|
|
||||||
def _flag_from_iso2(code: str) -> Optional[str]:
|
def _flag_from_iso2(code: str) -> Optional[str]:
|
||||||
"""Return unicode flag from 2-letter ISO code (e.g., 'DE' -> 🇩🇪)."""
|
"""Return unicode flag from 2-letter ISO code (e.g., 'DE' -> 🇩🇪)."""
|
||||||
if not code or len(code) != 2:
|
if not code or len(code) != 2:
|
||||||
@@ -1855,23 +1709,11 @@ async def on_command_error(ctx, error):
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
# Send detailed error to user if owner; be safe for slash interactions
|
# Send detailed error to user if owner
|
||||||
try:
|
if ctx.author.id == OWNER_ID:
|
||||||
content_owner = f"❌ **Error Details (Owner only):**\n```python\n{type(error).__name__}: {str(error)[:1800]}\n```"
|
await ctx.send(f"❌ **Error Details (Owner only):**\n```python\n{type(error).__name__}: {str(error)[:1800]}\n```")
|
||||||
content_user = "❌ An unknown error occurred!"
|
else:
|
||||||
if hasattr(ctx, 'interaction') and ctx.interaction is not None:
|
await ctx.send("❌ An unknown error occurred!")
|
||||||
if not ctx.interaction.response.is_done():
|
|
||||||
await ctx.interaction.response.send_message(content_owner if ctx.author.id == OWNER_ID else content_user, ephemeral=ctx.author.id != OWNER_ID)
|
|
||||||
else:
|
|
||||||
await ctx.interaction.followup.send(content_owner if ctx.author.id == OWNER_ID else content_user, ephemeral=ctx.author.id != OWNER_ID)
|
|
||||||
else:
|
|
||||||
await ctx.send(content_owner if ctx.author.id == OWNER_ID else content_user)
|
|
||||||
except Exception as send_err:
|
|
||||||
# Final fallback: try reply, then ignore
|
|
||||||
try:
|
|
||||||
await ctx.reply("❌ Error occurred, and sending details failed.")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
"""Main function to start the bot"""
|
"""Main function to start the bot"""
|
||||||
|
|||||||
Reference in New Issue
Block a user