Release 0.9.0

This commit is contained in:
2025-11-21 07:26:02 +01:00
committed by ecv
commit 472f0812e7
240 changed files with 20033 additions and 0 deletions

View File

@@ -0,0 +1,673 @@
# chrani-bot-tng Deployment-Anleitung
Umfassende Anleitung zur Installation, Konfiguration und zum Betrieb von chrani-bot-tng mit modernem Python, gunicorn und nginx.
## Inhaltsverzeichnis
1. [Systemanforderungen](#systemanforderungen)
2. [Schnellstart - Lokales Testen](#schnellstart---lokales-testen)
3. [Produktions-Deployment mit gunicorn](#produktions-deployment-mit-gunicorn)
4. [Deployment mit nginx (Reverse Proxy)](#deployment-mit-nginx-reverse-proxy)
5. [Systemd-Service einrichten](#systemd-service-einrichten)
6. [Fehlerbehebung](#fehlerbehebung)
7. [Wartung und Updates](#wartung-und-updates)
---
## Systemanforderungen
### Unterstützte Systeme
- **Linux**: Ubuntu 20.04+, Debian 10+, CentOS 8+, Fedora, Arch Linux
- **Python**: 3.8 oder höher (empfohlen: Python 3.10+)
- **Weitere Software**:
- Git
- pip (Python Package Manager)
- virtualenv oder venv
- Optional: nginx (für Reverse Proxy)
- Optional: systemd (für Service-Management)
### Mindestanforderungen Hardware
- **CPU**: 1 Core
- **RAM**: 512 MB (1 GB empfohlen)
- **Festplatte**: 500 MB freier Speicher
---
## Schnellstart - Lokales Testen
Diese Anleitung zeigt Ihnen, wie Sie chrani-bot-tng lokal auf Ihrem Computer zum Testen ausführen können.
### Schritt 1: Repository klonen
```bash
cd ~
git clone https://github.com/your-username/chrani-bot-tng.git
cd chrani-bot-tng
```
### Schritt 2: Python Virtual Environment erstellen
Ein Virtual Environment isoliert die Python-Abhängigkeiten des Projekts von Ihrem System.
```bash
# Virtual Environment erstellen
python3 -m venv venv
# Virtual Environment aktivieren
source venv/bin/activate
# Ihr Prompt sollte jetzt mit (venv) beginnen
```
### Schritt 3: Abhängigkeiten installieren
```bash
# pip aktualisieren
pip install --upgrade pip
# Projektabhängigkeiten installieren
pip install -r requirements.txt
```
### Schritt 4: Konfiguration anpassen
Die Bot-Konfiguration befindet sich in JSON-Dateien im `bot/options/` Verzeichnis.
```bash
# Verzeichnis prüfen
ls -la bot/options/
# Beispiel: Webserver-Konfiguration bearbeiten
nano bot/options/module_webserver.json
```
**Wichtige Einstellungen** in `module_webserver.json`:
```json
{
"host": "0.0.0.0",
"port": 5000,
"Flask_secret_key": "ÄNDERN-SIE-DIES-IN-PRODUKTION"
}
```
**Telnet-Konfiguration** (für Verbindung zum 7 Days to Die Server):
```bash
nano bot/options/module_telnet.json
```
Passen Sie die Telnet-Verbindungsdetails an Ihren Gameserver an.
### Schritt 5: Bot starten (Entwicklungsmodus)
Es gibt zwei Möglichkeiten, den Bot zu starten:
#### Methode A: Standalone-Modus (Flask Development Server)
```bash
# Mit dem originalen Entry-Point
python3 app.py
```
Der Bot sollte starten und ausgeben:
```
modules started: ['module_dom', 'module_storage', 'module_telnet', ...]
```
Der Webserver läuft auf: `http://localhost:5000`
#### Methode B: Mit gunicorn (empfohlen für Tests)
```bash
# Mit gunicorn starten (wie in Produktion)
gunicorn -c gunicorn.conf.py wsgi:application
```
Vorteile:
- Näher an der Produktionsumgebung
- Bessere Performance
- WebSocket-Support optimiert
### Schritt 6: Im Browser testen
Öffnen Sie Ihren Browser und navigieren Sie zu:
```
http://localhost:5000
```
Sie sollten die LCARS-Style Weboberfläche sehen. Klicken Sie auf "use your steam-account to log in" um sich zu authentifizieren.
### Schritt 7: Bot beenden
```bash
# STRG+C drücken um den Bot zu stoppen
# Virtual Environment deaktivieren
deactivate
```
---
## Produktions-Deployment mit gunicorn
Für den Produktionsbetrieb ist gunicorn deutlich besser geeignet als der Flask Development Server.
### Warum gunicorn?
- **Performance**: Optimiert für hohe Last und viele gleichzeitige Verbindungen
- **Stabilität**: Automatisches Neustart bei Fehlern
- **WebSocket-Support**: Mit gevent-websocket-Worker für Socket.IO optimiert
- **Production-Ready**: Bewährt in vielen großen Projekten
### Schritt 1: Konfiguration überprüfen
Die Datei `gunicorn.conf.py` enthält die gunicorn-Konfiguration:
```bash
nano gunicorn.conf.py
```
**Wichtige Einstellungen**:
```python
# Server Socket - wo gunicorn lauscht
bind = "0.0.0.0:5000" # Alle Interfaces, Port 5000
# Worker - WICHTIG: Für WebSocket nur 1 Worker!
workers = 1
worker_class = "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" # für WebSocket-Upgrades
worker_connections = 1000
# Timeouts
timeout = 120
keepalive = 5
```
Hinweis: In `wsgi.py` exportiert `wsgi:application` die Flask-App (`webserver_module.app`), WebSockets laufen über den gevent-websocket-Worker.
### Schritt 2: Bot mit gunicorn starten
```bash
# Virtual Environment aktivieren
cd ~/chrani-bot-tng
source venv/bin/activate
# Mit gunicorn starten
gunicorn -c gunicorn.conf.py wsgi:application
```
Sie sollten folgende Ausgabe sehen:
```
============================================================
chrani-bot-tng is starting...
============================================================
Worker spawned (pid: 12345)
Worker initialized (pid: 12345)
webserver: Running under WSGI server mode
modules started: [...]
============================================================
chrani-bot-tng is ready to accept connections
Listening on: 0.0.0.0:5000
============================================================
```
### Schritt 3: Verbindung testen
```bash
# In einem neuen Terminal:
curl http://localhost:5000
# Oder im Browser:
# http://your-server-ip:5000
```
### Schritt 4: Bot im Hintergrund laufen lassen
```bash
# Mit nohup (einfache Methode)
nohup gunicorn -c gunicorn.conf.py wsgi:application > gunicorn.log 2>&1 &
# Prozess-ID wird angezeigt
echo $! > gunicorn.pid
# Logs anschauen
tail -f gunicorn.log
# Bot stoppen
kill $(cat gunicorn.pid)
```
**Besser**: Verwenden Sie systemd (siehe Abschnitt unten)
---
## Deployment mit nginx (Reverse Proxy)
nginx als Reverse Proxy bietet zusätzliche Vorteile:
- **SSL/TLS-Terminierung** (HTTPS-Verschlüsselung)
- **Load Balancing** (bei mehreren Instanzen)
- **Static File Serving** (schnelleres Ausliefern von CSS/JS)
- **DDoS-Schutz** und Rate Limiting
- **Besseres Logging**
### Schritt 1: nginx installieren
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install nginx
# CentOS/RHEL
sudo yum install nginx
# Fedora
sudo dnf install nginx
```
### Schritt 2: nginx-Konfiguration erstellen
```bash
# Beispielkonfiguration kopieren
sudo cp nginx.conf.example /etc/nginx/sites-available/chrani-bot-tng
# Konfiguration bearbeiten
sudo nano /etc/nginx/sites-available/chrani-bot-tng
```
**Wichtige Anpassungen**:
1. **Server-Name ändern**:
```nginx
server_name your-domain.com www.your-domain.com;
```
2. **SSL-Zertifikate** (für HTTPS):
```nginx
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
```
3. **Upstream-Server prüfen**:
```nginx
upstream chrani_bot_app {
server 127.0.0.1:5000 fail_timeout=0;
}
```
### Schritt 3: Konfiguration aktivieren
```bash
# Symlink erstellen
sudo ln -s /etc/nginx/sites-available/chrani-bot-tng /etc/nginx/sites-enabled/
# Standard-Site deaktivieren (optional)
sudo rm /etc/nginx/sites-enabled/default
# Konfiguration testen
sudo nginx -t
# Bei erfolgreicher Prüfung:
sudo systemctl reload nginx
```
### Schritt 4: Firewall konfigurieren
```bash
# ufw (Ubuntu/Debian)
sudo ufw allow 'Nginx Full'
sudo ufw enable
# firewalld (CentOS/RHEL/Fedora)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
```
### Schritt 5: SSL-Zertifikat mit Let's Encrypt (optional)
```bash
# Certbot installieren
sudo apt install certbot python3-certbot-nginx # Ubuntu/Debian
# sudo yum install certbot python3-certbot-nginx # CentOS
# Zertifikat erstellen (interaktiv)
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
# Automatische Erneuerung testen
sudo certbot renew --dry-run
```
### Schritt 6: Zugriff testen
```bash
# HTTP (sollte zu HTTPS umleiten)
curl -I http://your-domain.com
# HTTPS
curl -I https://your-domain.com
```
Im Browser: `https://your-domain.com`
---
## Systemd-Service einrichten
Mit systemd läuft chrani-bot-tng als System-Service, startet automatisch beim Booten und wird bei Fehlern neu gestartet.
### Schritt 1: Service-Datei anpassen
```bash
# Service-Datei bearbeiten
nano chrani-bot-tng.service
```
**Wichtige Anpassungen**:
```ini
# Benutzer und Gruppe ändern
User=your-username
Group=your-username
# Pfade anpassen
WorkingDirectory=/home/your-username/chrani-bot-tng
Environment="PATH=/home/your-username/chrani-bot-tng/venv/bin"
ExecStart=/home/your-username/chrani-bot-tng/venv/bin/gunicorn -c gunicorn.conf.py wsgi:application
```
### Schritt 2: Service installieren
```bash
# Service-Datei nach systemd kopieren
sudo cp chrani-bot-tng.service /etc/systemd/system/
# systemd neu laden
sudo systemctl daemon-reload
# Service aktivieren (Autostart beim Booten)
sudo systemctl enable chrani-bot-tng
# Service starten
sudo systemctl start chrani-bot-tng
```
### Schritt 3: Service-Status prüfen
```bash
# Status anzeigen
sudo systemctl status chrani-bot-tng
# Logs anzeigen
sudo journalctl -u chrani-bot-tng -f
# Logs der letzten Stunde
sudo journalctl -u chrani-bot-tng --since "1 hour ago"
```
### Schritt 4: Service-Befehle
```bash
# Service stoppen
sudo systemctl stop chrani-bot-tng
# Service neu starten
sudo systemctl restart chrani-bot-tng
# Service neu laden (bei Konfigurationsänderungen)
sudo systemctl reload chrani-bot-tng
# Autostart deaktivieren
sudo systemctl disable chrani-bot-tng
```
---
## Fehlerbehebung
### Problem: "Module not found" Fehler
**Symptom**: ImportError beim Starten
**Lösung**:
```bash
# Virtual Environment aktiviert?
source venv/bin/activate
# Dependencies neu installieren
pip install -r requirements.txt
# Python-Version prüfen (mindestens 3.8)
python3 --version
```
### Problem: Port bereits in Benutzung
**Symptom**: "Address already in use"
**Lösung**:
```bash
# Prozess auf Port 5000 finden
sudo lsof -i :5000
# Oder mit netstat
sudo netstat -tulpn | grep :5000
# Prozess beenden
sudo kill -9 <PID>
# Oder anderen Port in gunicorn.conf.py verwenden
```
### Problem: WebSocket-Verbindung schlägt fehl
**Symptom**: "WebSocket connection failed" im Browser
**Lösung**:
1. Prüfen Sie, dass nur 1 Worker in `gunicorn.conf.py` konfiguriert ist:
```python
workers = 1
worker_class = "gevent"
```
2. nginx-Konfiguration prüfen (WebSocket-Headers):
```nginx
location /socket.io/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
### Problem: 502 Bad Gateway (nginx)
**Symptom**: nginx zeigt "502 Bad Gateway"
**Lösung**:
```bash
# Ist gunicorn gestartet?
sudo systemctl status chrani-bot-tng
# Logs prüfen
sudo journalctl -u chrani-bot-tng -n 50
# nginx-Fehlerlog prüfen
sudo tail -f /var/log/nginx/chrani-bot-tng-error.log
# Upstream in nginx.conf prüfen
# Stimmt IP und Port mit gunicorn überein?
```
### Problem: Hohe CPU-Last
**Symptom**: Server reagiert langsam, CPU bei 100%
**Lösung**:
```bash
# Prozesse prüfen
top -u your-username
# gunicorn-Worker-Anzahl anpassen (aber max. 1 für WebSocket!)
# Timeout erhöhen in gunicorn.conf.py
timeout = 300
# Logs auf Endlosschleifen prüfen
sudo journalctl -u chrani-bot-tng -f
```
### Problem: Telnet-Verbindung zum Gameserver schlägt fehl
**Symptom**: Bot kann sich nicht mit dem 7 Days to Die Server verbinden
**Lösung**:
1. Telnet-Einstellungen prüfen:
```bash
nano bot/options/module_telnet.json
```
2. Verbindung manuell testen:
```bash
telnet <gameserver-ip> <telnet-port>
# Passwort eingeben
```
3. Firewall-Regeln prüfen
4. Gameserver-Konfiguration prüfen (telnet aktiviert?)
---
## Wartung und Updates
### Code-Updates einspielen
```bash
# Zum Projektverzeichnis
cd ~/chrani-bot-tng
# Änderungen abrufen
git fetch origin
# Aktuellen Branch prüfen
git branch
# Updates herunterladen und anwenden
git pull origin main # oder Ihr Branch-Name
# Dependencies aktualisieren
source venv/bin/activate
pip install -r requirements.txt --upgrade
# Service neu starten
sudo systemctl restart chrani-bot-tng
```
### Backup erstellen
```bash
# Gesamtes Projektverzeichnis sichern
tar -czf chrani-bot-tng-backup-$(date +%Y%m%d).tar.gz ~/chrani-bot-tng
# Nur Konfiguration sichern
tar -czf chrani-bot-config-backup-$(date +%Y%m%d).tar.gz ~/chrani-bot-tng/bot/options
# Backup an sicheren Ort verschieben
mv chrani-bot-*.tar.gz /path/to/backup/location/
```
### Logs rotieren
nginx rotiert Logs automatisch. Für gunicorn/systemd:
```bash
# Log-Größe prüfen
sudo journalctl --disk-usage
# Alte Logs löschen (älter als 7 Tage)
sudo journalctl --vacuum-time=7d
# Oder nach Größe (maximal 500 MB behalten)
sudo journalctl --vacuum-size=500M
```
### Performance-Monitoring
```bash
# Systemressourcen überwachen
htop
# Nur chrani-bot-tng Prozesse
htop -u your-username
# Netzwerk-Verbindungen prüfen
sudo netstat -tulpn | grep gunicorn
# Echtzeit-Logs
sudo journalctl -u chrani-bot-tng -f
```
---
## Zusammenfassung der Befehle
### Lokaler Test (Schnellstart)
```bash
# Setup
git clone <repository-url>
cd chrani-bot-tng
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Starten
gunicorn -c gunicorn.conf.py wsgi:application
# Stoppen: STRG+C
```
### Produktions-Deployment
```bash
# Einmalige Einrichtung
sudo cp chrani-bot-tng.service /etc/systemd/system/
sudo cp nginx.conf.example /etc/nginx/sites-available/chrani-bot-tng
sudo ln -s /etc/nginx/sites-available/chrani-bot-tng /etc/nginx/sites-enabled/
sudo systemctl daemon-reload
sudo systemctl enable chrani-bot-tng
sudo systemctl enable nginx
# Starten
sudo systemctl start chrani-bot-tng
sudo systemctl start nginx
# Status
sudo systemctl status chrani-bot-tng
sudo journalctl -u chrani-bot-tng -f
```
---
## Weitere Ressourcen
- **Gunicorn Dokumentation**: https://docs.gunicorn.org/
- **nginx Dokumentation**: https://nginx.org/en/docs/
- **Flask-SocketIO Dokumentation**: https://flask-socketio.readthedocs.io/
- **systemd Dokumentation**: https://www.freedesktop.org/software/systemd/man/
---
## Support
Bei Problemen:
1. Prüfen Sie die Logs: `sudo journalctl -u chrani-bot-tng -n 100`
2. Suchen Sie in den GitHub Issues
3. Erstellen Sie ein neues Issue mit:
- Fehlermeldung
- Logs
- Systeminfo (OS, Python-Version, etc.)
---
**Viel Erfolg mit chrani-bot-tng!** 🚀

View File

@@ -0,0 +1,106 @@
# Logging System Analysis & Plan
## System Architecture Overview
```
Browser (JavaScript)
↕ Socket.io
Python Backend (Flask + Socket.io)
↕ Telnet
7D2D Game Server
```
## Current Logging State
### Python Backend
- **Format:** Inconsistent mix of:
- `print(f"[PREFIX] message")`
- `print("{}: message".format(module))`
- Plain `print("message")`
- **No:**
- Timestamps
- Log levels (ERROR, WARN, INFO, DEBUG)
- User context
- Correlation IDs
- Structured data
### JavaScript Frontend
- **Issues:**
- Ding/Dong logs every 10 seconds (spam)
- Many debug messages that don't help troubleshooting
- No structured logging
- Success messages mixed with errors
## Event Flow Analysis
### 1. User Action Flow
```
User clicks checkbox (Browser)
→ Widget event sent via Socket.io
→ Python: webserver receives event
→ Python: Action handler (e.g., dom_management/select)
→ Python: DOM upsert
→ Python: Callback triggers
→ Python: Widget handler updates
→ Socket.io sends update back
→ Browser: DOM updated
```
**Critical Logging Points:**
- [ ] Action received (user, action, data)
- [ ] Action validation failed
- [ ] DOM operation (path, method, user)
- [ ] Callback trigger (which handler, why)
- [ ] Socket send (to whom, what type)
### 2. Tile Request Flow
```
Browser requests tile (HTTP GET)
→ Python: webserver /map_tiles route
→ Python: Auth check
→ Python: Proxy to game server
→ 7D2D: Returns tile OR error
→ Python: Forward response OR error
→ Browser: Displays tile OR 404
```
**Critical Logging Points:**
- [ ] Tile request (only on ERROR)
- [ ] Auth failure
- [ ] Game server error (status code, url)
- [ ] Network timeout
### 3. Telnet Command Flow
```
Python: Action needs game data
→ Python: Telnet send command
→ 7D2D: Processes command
→ 7D2D: Returns response
→ Python: Parse response
→ Python: Update DOM
→ Python: Trigger callbacks
```
## Log Format
### Python Backend Format
```
[LEVEL] [TIMESTAMP] event_name | context_key=value context_key=value
```
**Example:**
```
[ERROR] [2025-01-19 12:34:56.123] tile_fetch_failed | user=steamid123 z=4 x=-2 y=1 status=404 url=http://...
[WARN ] [2025-01-19 12:34:57.456] auth_missing_sid | user=steamid456 action=tile_request
[INFO ] [2025-01-19 12:00:00.000] module_loaded | module=webserver version=1.0
```
### JavaScript Frontend Format
```
[PREFIX] event_name | context
```
**Example:**
```
[SOCKET ERROR] event_processing_failed | data_type=widget_content error=Cannot read property 'id' of undefined
[MAP ERROR ] shape_creation_failed | location_id=map_owner_loc1 shape=circle error=Invalid radius
```

View File

@@ -0,0 +1,211 @@
### Add a menu button and view to a module widget (HowTo)
This guide explains how to add a new button to a widget's pullout menu and display a corresponding screen ("view"). It follows the menu pattern used in the `locations` module and the example implemented in the `telnet` module.
The pattern consists of four parts:
- A view registry in the widget Python file that defines available views and their labels.
- A shared Jinja2 macro `construct_view_menu` to render the menu.
- A small widget template `control_view_menu.html` that invokes the macro for the module.
- A server action `toggle_<module>_widget_view` that switches the current view.
Below, `<module>` stands for your module name (e.g., `telnet`, `locations`), and `<view_id>` is the identifier of the view you are adding (e.g., `test`).
---
#### 1) Ensure the view menu macro is available for your module
Your module's Jinja2 macros file (e.g., `bot/modules/<module>/templates/jinja2_macros.html`) should contain the macro `construct_view_menu`. If your module doesn't have it yet, mirror the implementation from:
- `bot/modules/locations/templates/jinja2_macros.html`
The macro expects these parameters:
- `views`: dict of view entries (see step 2)
- `current_view`: name of the active view
- `module_name`: the module identifier (e.g., `telnet`)
- `steamid`: the current user's steamid
- `default_view`: the view to return to from an active state (usually `frontend`)
The macro renders toggle links that emit the standard socket event:
```
['widget_event', [module_name, ['toggle_' ~ module_name ~ '_widget_view', {'steamid': steamid, 'action': action}]]]
```
---
#### 2) Define or extend the VIEW_REGISTRY in the widget Python file
In your widget file (e.g., `bot/modules/<module>/widgets/<widget_name>.py`) define a `VIEW_REGISTRY` mapping of view IDs to config objects. Each entry can have:
- `label_active`: label when this view is currently active (typically `back`)
- `label_inactive`: label when the view is not active (e.g., `options`, `test`)
- `action`: the action string to be sent by the menu (e.g., `show_options`, `show_test`)
- `include_in_menu`: whether to show it in the menu (set `False` for base `frontend`)
Example (excerpt with a new `test` view):
```python
VIEW_REGISTRY = {
'frontend': {
'label_active': 'back',
'label_inactive': 'main',
'action': 'show_frontend',
'include_in_menu': False
},
'options': {
'label_active': 'back',
'label_inactive': 'options',
'action': 'show_options',
'include_in_menu': True
},
'test': {
'label_active': 'back',
'label_inactive': 'test',
'action': 'show_test',
'include_in_menu': True
}
}
```
Update `select_view` to route to your new view:
```python
def select_view(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
current_view = module.get_current_view(dispatchers_steamid)
if current_view == 'options':
options_view(module, dispatchers_steamid=dispatchers_steamid)
elif current_view == 'test':
test_view(module, dispatchers_steamid=dispatchers_steamid)
else:
frontend_view(module, dispatchers_steamid=dispatchers_steamid)
```
---
#### 3) Create the control_view_menu.html template for the widget
Create a template next to your widget's templates folder that renders the menu by importing the macro and passing the registry:
`bot/modules/<module>/templates/<widget_name>/control_view_menu.html`
```jinja2
{%- from 'jinja2_macros.html' import construct_view_menu with context -%}
<div id="<widget_dom_id>_options_toggle" class="pull_out right">
{{ construct_view_menu(
views=views,
current_view=current_view,
module_name='<module>',
steamid=steamid,
default_view='frontend'
)}}
</div>
```
Replace `<widget_dom_id>` with the widget DOM id (e.g., `telnet_table_widget_options_toggle`), and `<module>` with the module name literal (e.g., `telnet`).
---
#### 4) Render the menu in each view and add your new view function
In every view function (`frontend_view`, `options_view`, and your new `test_view`) render the menu and include it into the page as `options_toggle`:
```python
template_view_menu = module.templates.get_template('<widget_name>/control_view_menu.html')
current_view = module.get_current_view(dispatchers_steamid)
options_toggle = module.template_render_hook(
module,
template=template_view_menu,
views=VIEW_REGISTRY,
current_view=current_view,
steamid=dispatchers_steamid
)
data_to_emit = module.template_render_hook(
module,
template=template_for_this_view,
options_toggle=options_toggle,
# ... other template params ...
)
```
Create the new view template (e.g., `view_test.html`) that uses the aside area to render the menu:
```jinja2
<header>
<div>
<span>Some Title</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table>
<thead>
<tr>
<th>Test View</th>
</tr>
</thead>
<tbody>
<tr><td><span>Test</span></td></tr>
</tbody>
</table>
</main>
```
---
#### 5) Wire the server action to toggle views
Each module has an action named `toggle_<module>_widget_view` that updates the current view for a dispatcher. Extend it to support your new action string (`show_<view_id>`):
`bot/modules/<module>/actions/toggle_<module>_widget_view.py`
```python
def main_function(module, event_data, dispatchers_steamid):
action = event_data[1].get('action', None)
event_data[1]['action_identifier'] = action_name
if action == 'show_options':
current_view = 'options'
elif action == 'show_frontend':
current_view = 'frontend'
elif action == 'show_test':
current_view = 'test'
else:
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
module.set_current_view(dispatchers_steamid, {
'current_view': current_view
})
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
```
Make sure the view ID you set here matches the view IDs used in `VIEW_REGISTRY` and `select_view`.
---
#### 6) Reference implementation to compare
Use these files as working examples:
- Locations menu macro: `bot/modules/locations/templates/jinja2_macros.html`
- Locations widget menu template: `bot/modules/locations/templates/manage_locations_widget/control_view_menu.html`
- Telnet macros with menu: `bot/modules/telnet/templates/jinja2_macros.html`
- Telnet menu template: `bot/modules/telnet/templates/telnet_log_widget/control_view_menu.html`
- Telnet widget with added view: `bot/modules/telnet/widgets/telnet_log_widget.py`
- Telnet action wiring: `bot/modules/telnet/actions/toggle_telnet_widget_view.py`
- Telnet new view template: `bot/modules/telnet/templates/telnet_log_widget/view_test.html`
---
#### 7) Quick checklist
- [ ] `VIEW_REGISTRY` contains your `<view_id>` with a correct `action` (`show_<view_id>`) and `include_in_menu = True`.
- [ ] `select_view` routes to `<view_id>` by calling `<view_id>_view`.
- [ ] The new `<view_id>_view` renders `options_toggle` using `control_view_menu.html`.
- [ ] The new view has a template and includes `{{ options_toggle }}` in the aside.
- [ ] The module's `toggle_<module>_widget_view` action handles `show_<view_id>` and sets `current_view` accordingly.
- [ ] Menu renders without errors and buttons switch between views (`back` returns to `frontend`).
---
#### Notes
- Keep naming consistent: action strings are `show_<view_id>`, event name is `toggle_<module>_widget_view`.
- The base view `frontend` is usually not included in the menu (`include_in_menu: False`), but is the `default_view` used for the "back" behavior.
- Follow the module's existing code style and avoid unrelated refactors when introducing new views/buttons.

View File

@@ -0,0 +1,74 @@
# chrani-bot-tng Environment Configuration
# Copy this file to .env and adjust the values for your setup
# ============================================================================
# Application Settings
# ============================================================================
# Set to 'true' when running under gunicorn or other WSGI servers
RUNNING_UNDER_WSGI=true
# Flask secret key - CHANGE THIS to a random string in production!
FLASK_SECRET_KEY=change-this-to-a-random-secret-key
# ============================================================================
# Server Configuration
# ============================================================================
# Host to bind to (use 0.0.0.0 for all interfaces, 127.0.0.1 for localhost only)
HOST=0.0.0.0
# Port to run on
PORT=5000
# ============================================================================
# 7 Days to Die Server Settings
# ============================================================================
# Telnet connection settings for the game server
TELNET_HOST=localhost
TELNET_PORT=8081
TELNET_PASSWORD=your-telnet-password
# ============================================================================
# Logging
# ============================================================================
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_LEVEL=INFO
# ============================================================================
# Development Settings
# ============================================================================
# Enable Flask debug mode (DO NOT use in production!)
FLASK_DEBUG=false
# Enable SocketIO debug mode
SOCKETIO_DEBUG=false
# Enable engineio logger
ENGINEIO_LOGGER=false
# ============================================================================
# Production Settings
# ============================================================================
# Number of gunicorn workers (for WebSocket use 1 worker with gevent)
GUNICORN_WORKERS=1
# Gunicorn worker class (use 'gevent' for WebSocket support)
GUNICORN_WORKER_CLASS=gevent
# Maximum number of concurrent connections per worker
GUNICORN_WORKER_CONNECTIONS=1000
# Request timeout in seconds
GUNICORN_TIMEOUT=120
# ============================================================================
# Database/Storage (if applicable)
# ============================================================================
# Add any database connection strings or storage paths here
# DATA_DIR=/var/lib/chrani-bot-tng

View File

@@ -0,0 +1,51 @@
[Unit]
Description=chrani-bot-tng - 7 Days to Die Server Management Bot
After=network.target
[Service]
Type=notify
# The specific user that our service will run as
# CHANGE THIS to your actual username
User=your-username
Group=your-username
# Set working directory
WorkingDirectory=/path/to/chrani-bot-tng
# Environment variables
Environment="PATH=/path/to/chrani-bot-tng/venv/bin"
Environment="RUNNING_UNDER_WSGI=true"
# If you're using a .env file for configuration
# EnvironmentFile=/path/to/chrani-bot-tng/.env
# The command to start the service
ExecStart=/path/to/chrani-bot-tng/venv/bin/gunicorn -c gunicorn.conf.py wsgi:application
# Restart policy
Restart=always
RestartSec=10
# Performance and security settings
RuntimeDirectory=chrani-bot-tng
RuntimeDirectoryMode=755
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=chrani-bot-tng
# Security hardening (optional but recommended)
NoNewPrivileges=true
PrivateTmp=true
# Resource limits (adjust as needed)
LimitNOFILE=65536
LimitNPROC=512
# Timeout for start/stop
TimeoutStartSec=300
TimeoutStopSec=300
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
"""
Gunicorn Configuration File for chrani-bot-tng
This configuration is optimized for running the bot with Flask-SocketIO and gevent.
"""
import multiprocessing
import os
# Server Socket
bind = "0.0.0.0:5000"
backlog = 2048
# Worker Processes
# For WebSocket support with gevent, use only 1 worker
# Multiple workers don't work well with socket.io state
workers = 1
worker_class = "gevent"
worker_connections = 1000
max_requests = 0 # Disable automatic worker restart
max_requests_jitter = 0
timeout = 120
keepalive = 5
# Logging
accesslog = "-" # Log to stdout
errorlog = "-" # Log to stderr
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
# Process Naming
proc_name = "chrani-bot-tng"
# Server Mechanics
daemon = False
pidfile = None
umask = 0
user = None
group = None
tmp_upload_dir = None
# SSL (uncomment and configure if using HTTPS directly with gunicorn)
# keyfile = "/path/to/keyfile"
# certfile = "/path/to/certfile"
# ca_certs = "/path/to/ca_certs"
# cert_reqs = 0
# ssl_version = 2
# ciphers = None
# Server Hooks
def on_starting(server):
"""
Called just before the master process is initialized.
"""
print("=" * 60)
print("chrani-bot-tng is starting...")
print("=" * 60)
def on_reload(server):
"""
Called to recycle workers during a reload via SIGHUP.
"""
print("Reloading workers...")
def when_ready(server):
"""
Called just after the server is started.
"""
print("=" * 60)
print("chrani-bot-tng is ready to accept connections")
print(f"Listening on: {bind}")
print("=" * 60)
def pre_fork(server, worker):
"""
Called just before a worker is forked.
"""
pass
def post_fork(server, worker):
"""
Called just after a worker has been forked.
"""
print(f"Worker spawned (pid: {worker.pid})")
def post_worker_init(worker):
"""
Called just after a worker has initialized the application.
"""
print(f"Worker initialized (pid: {worker.pid})")
def worker_int(worker):
"""
Called just after a worker received the SIGINT or SIGQUIT signal.
"""
print(f"Worker received INT or QUIT signal (pid: {worker.pid})")
def worker_abort(worker):
"""
Called when a worker received the SIGABRT signal.
"""
print(f"Worker received SIGABRT signal (pid: {worker.pid})")
def pre_exec(server):
"""
Called just before a new master process is forked.
"""
print("Forking new master process...")
def pre_request(worker, req):
"""
Called just before a worker processes the request.
"""
worker.log.debug(f"{req.method} {req.path}")
def post_request(worker, req, environ, resp):
"""
Called after a worker processes the request.
"""
pass
def child_exit(server, worker):
"""
Called just after a worker has been exited.
"""
print(f"Worker exited (pid: {worker.pid})")
def worker_exit(server, worker):
"""
Called just after a worker has been exited.
"""
print(f"Worker process exiting (pid: {worker.pid})")
def nworkers_changed(server, new_value, old_value):
"""
Called just after num_workers has been changed.
"""
print(f"Number of workers changed from {old_value} to {new_value}")
def on_exit(server):
"""
Called just before exiting gunicorn.
"""
print("=" * 60)
print("chrani-bot-tng is shutting down...")
print("=" * 60)

View File

@@ -0,0 +1,156 @@
# nginx configuration for chrani-bot-tng
#
# INSTALLATION:
# 1. Copy this file to /etc/nginx/sites-available/chrani-bot-tng
# 2. Update the server_name to match your domain
# 3. Update the paths to SSL certificates if using HTTPS
# 4. Create a symlink: sudo ln -s /etc/nginx/sites-available/chrani-bot-tng /etc/nginx/sites-enabled/
# 5. Test config: sudo nginx -t
# 6. Reload nginx: sudo systemctl reload nginx
# Upstream configuration for gunicorn
upstream chrani_bot_app {
# Use Unix socket for better performance (recommended)
# Make sure this matches the bind setting in gunicorn.conf.py
server 127.0.0.1:5000 fail_timeout=0;
# Alternative: Unix socket (requires changing gunicorn bind setting)
# server unix:/var/run/chrani-bot-tng/gunicorn.sock fail_timeout=0;
}
# HTTP Server (redirects to HTTPS)
server {
listen 80;
listen [::]:80;
server_name your-domain.com www.your-domain.com;
# Redirect all HTTP traffic to HTTPS
return 301 https://$server_name$request_uri;
}
# HTTPS Server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-domain.com www.your-domain.com;
# SSL Configuration
# Update these paths with your actual certificate paths
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL Security Settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Logging
access_log /var/log/nginx/chrani-bot-tng-access.log;
error_log /var/log/nginx/chrani-bot-tng-error.log;
# Max upload size (adjust as needed)
client_max_body_size 4M;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Main location block - proxy to gunicorn
location / {
proxy_pass http://chrani_bot_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Disable buffering for better real-time response
proxy_buffering off;
proxy_redirect off;
}
# WebSocket support for Socket.IO
location /socket.io/ {
proxy_pass http://chrani_bot_app/socket.io/;
proxy_http_version 1.1;
# WebSocket specific headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Standard proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts for WebSocket
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
# Disable buffering for WebSocket
proxy_buffering off;
}
# Static files (if you have any)
location /static/ {
alias /path/to/chrani-bot-tng/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Favicon
location = /favicon.ico {
access_log off;
log_not_found off;
}
# Robots.txt
location = /robots.txt {
access_log off;
log_not_found off;
}
}
# Alternative: HTTP-only configuration (for local/dev use)
# Uncomment this and comment out the HTTPS server above if not using SSL
#
# server {
# listen 80;
# listen [::]:80;
# server_name your-domain.com www.your-domain.com;
#
# access_log /var/log/nginx/chrani-bot-tng-access.log;
# error_log /var/log/nginx/chrani-bot-tng-error.log;
#
# client_max_body_size 4M;
#
# location / {
# proxy_pass http://chrani_bot_app;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_buffering off;
# proxy_redirect off;
# }
#
# location /socket.io/ {
# proxy_pass http://chrani_bot_app/socket.io/;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_buffering off;
# }
# }

View File

@@ -0,0 +1,345 @@
#!/usr/bin/env python3
"""
Callback Dict Usage Analyzer
Scans the codebase for all callback_dict usage:
- dom.data.upsert() calls
- dom.data.append() calls
- dom.data.remove_key_by_path() calls
- widget_meta["handlers"] registrations
Generates a complete inventory for Phase 0 of the refactoring plan.
"""
import os
import re
from pathlib import Path
from typing import List, Dict, Tuple
class CallbackAnalyzer:
def __init__(self, root_dir: str):
self.root_dir = Path(root_dir)
self.results = {
"upsert_calls": [],
"append_calls": [],
"remove_calls": [],
"handler_registrations": []
}
def analyze(self):
"""Run all analysis passes."""
print("🔍 Analyzing callback_dict usage...")
print(f"📁 Root directory: {self.root_dir}\n")
# Find all Python files in bot/modules
modules_dir = self.root_dir / "bot" / "modules"
if not modules_dir.exists():
print(f"❌ Error: {modules_dir} does not exist!")
return
python_files = list(modules_dir.rglob("*.py"))
print(f"📄 Found {len(python_files)} Python files\n")
# Analyze each file
for py_file in python_files:
self._analyze_file(py_file)
# Print results
self._print_results()
# Generate markdown report
self._generate_report()
def _analyze_file(self, file_path: Path):
"""Analyze a single Python file."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
relative_path = file_path.relative_to(self.root_dir)
# Find upsert calls
self._find_upsert_calls(relative_path, content, lines)
# Find append calls
self._find_append_calls(relative_path, content, lines)
# Find remove calls
self._find_remove_calls(relative_path, content, lines)
# Find handler registrations
self._find_handler_registrations(relative_path, content, lines)
except Exception as e:
print(f"⚠️ Error analyzing {file_path}: {e}")
def _find_upsert_calls(self, file_path: Path, content: str, lines: List[str]):
"""Find all dom.data.upsert() calls."""
# Pattern: module.dom.data.upsert( or self.dom.data.upsert(
pattern = r'(module|self)\.dom\.data\.upsert\('
for match in re.finditer(pattern, content):
line_num = content[:match.start()].count('\n') + 1
# Try to extract callback levels
min_level, max_level = self._extract_callback_levels(lines, line_num)
# Try to determine data depth
depth = self._estimate_data_depth(lines, line_num)
self.results["upsert_calls"].append({
"file": str(file_path),
"line": line_num,
"min_callback_level": min_level,
"max_callback_level": max_level,
"estimated_depth": depth
})
def _find_append_calls(self, file_path: Path, content: str, lines: List[str]):
"""Find all dom.data.append() calls."""
pattern = r'(module|self)\.dom\.data\.append\('
for match in re.finditer(pattern, content):
line_num = content[:match.start()].count('\n') + 1
self.results["append_calls"].append({
"file": str(file_path),
"line": line_num
})
def _find_remove_calls(self, file_path: Path, content: str, lines: List[str]):
"""Find all dom.data.remove_key_by_path() calls."""
pattern = r'(module|self)\.dom\.data\.remove_key_by_path\('
for match in re.finditer(pattern, content):
line_num = content[:match.start()].count('\n') + 1
self.results["remove_calls"].append({
"file": str(file_path),
"line": line_num
})
def _find_handler_registrations(self, file_path: Path, content: str, lines: List[str]):
"""Find all widget_meta handler registrations."""
# Look for widget_meta = { ... "handlers": { ... } ... }
if 'widget_meta' not in content:
return
if '"handlers"' not in content and "'handlers'" not in content:
return
# Find line with handlers dict
in_handlers = False
handlers_start = None
for i, line in enumerate(lines):
if '"handlers"' in line or "'handlers'" in line:
in_handlers = True
handlers_start = i + 1
continue
if in_handlers:
# Look for path patterns
# Pattern: "path/pattern": handler_function,
path_match = re.search(r'["\']([^"\']+)["\']:\s*(\w+)', line)
if path_match:
path_pattern = path_match.group(1)
handler_name = path_match.group(2)
# Calculate depth
depth = path_pattern.count('/')
self.results["handler_registrations"].append({
"file": str(file_path),
"line": i + 1,
"path_pattern": path_pattern,
"handler_function": handler_name,
"depth": depth
})
# Check if we've left the handlers dict
if '}' in line and ',' not in line:
in_handlers = False
def _extract_callback_levels(self, lines: List[str], start_line: int) -> Tuple[str, str]:
"""Try to extract min_callback_level and max_callback_level from upsert call."""
min_level = "None"
max_level = "None"
# Look at next ~10 lines for callback level params
for i in range(start_line - 1, min(start_line + 10, len(lines))):
line = lines[i]
if 'min_callback_level' in line:
match = re.search(r'min_callback_level\s*=\s*(\d+|None)', line)
if match:
min_level = match.group(1)
if 'max_callback_level' in line:
match = re.search(r'max_callback_level\s*=\s*(\d+|None)', line)
if match:
max_level = match.group(1)
# Stop at closing paren
if ')' in line:
break
return min_level, max_level
def _estimate_data_depth(self, lines: List[str], start_line: int) -> int:
"""Estimate the depth of data being upserted by counting dict nesting."""
depth = 0
brace_count = 0
# Look backwards for opening brace
for i in range(start_line - 1, max(0, start_line - 30), -1):
line = lines[i]
if 'upsert({' in line or 'upsert( {' in line:
# Count colons/keys in following lines until we reach reasonable depth
for j in range(i, min(i + 50, len(lines))):
check_line = lines[j]
# Count dictionary depth by tracking braces
brace_count += check_line.count('{')
brace_count -= check_line.count('}')
if brace_count < 0:
break
# Rough estimate: each key-value pair increases depth
if '":' in check_line or "\':" in check_line:
depth += 1
break
# Depth is usually around 4-6 for locations/players
return min(depth, 10) # Cap at reasonable number
def _print_results(self):
"""Print analysis results to console."""
print("\n" + "=" * 70)
print("📊 ANALYSIS RESULTS")
print("=" * 70 + "\n")
print(f"🔹 Upsert Calls: {len(self.results['upsert_calls'])}")
print(f"🔹 Append Calls: {len(self.results['append_calls'])}")
print(f"🔹 Remove Calls: {len(self.results['remove_calls'])}")
print(f"🔹 Handler Registrations: {len(self.results['handler_registrations'])}")
print("\n" + "-" * 70)
print("UPSERT CALLS BY CALLBACK LEVELS:")
print("-" * 70)
# Group by callback levels
level_groups = {}
for call in self.results["upsert_calls"]:
key = f"min={call['min_callback_level']}, max={call['max_callback_level']}"
if key not in level_groups:
level_groups[key] = []
level_groups[key].append(call)
for levels, calls in sorted(level_groups.items()):
print(f"\n{levels}: {len(calls)} calls")
for call in calls:
print(f" 📄 {call['file']}:{call['line']}")
print("\n" + "-" * 70)
print("HANDLER REGISTRATIONS BY DEPTH:")
print("-" * 70)
# Group by depth
depth_groups = {}
for handler in self.results["handler_registrations"]:
depth = handler['depth']
if depth not in depth_groups:
depth_groups[depth] = []
depth_groups[depth].append(handler)
for depth in sorted(depth_groups.keys()):
handlers = depth_groups[depth]
print(f"\nDepth {depth}: {len(handlers)} handlers")
for h in handlers:
print(f" 📄 {h['file']}:{h['line']}")
print(f" Pattern: {h['path_pattern']}")
print(f" Handler: {h['handler_function']}")
def _generate_report(self):
"""Generate markdown report."""
report_path = self.root_dir / "CALLBACK_DICT_INVENTORY.md"
with open(report_path, 'w', encoding='utf-8') as f:
f.write("# Callback Dict Usage Inventory\n\n")
f.write("Generated by analyze_callback_usage.py\n\n")
f.write("## Summary\n\n")
f.write(f"- **Upsert Calls:** {len(self.results['upsert_calls'])}\n")
f.write(f"- **Append Calls:** {len(self.results['append_calls'])}\n")
f.write(f"- **Remove Calls:** {len(self.results['remove_calls'])}\n")
f.write(f"- **Handler Registrations:** {len(self.results['handler_registrations'])}\n\n")
f.write("---\n\n")
f.write("## Upsert Calls\n\n")
f.write("| File | Line | Min Level | Max Level | Est. Depth |\n")
f.write("|------|------|-----------|-----------|------------|\n")
for call in self.results["upsert_calls"]:
f.write(f"| {call['file']} | {call['line']} | "
f"{call['min_callback_level']} | {call['max_callback_level']} | "
f"{call['estimated_depth']} |\n")
f.write("\n---\n\n")
f.write("## Append Calls\n\n")
f.write("| File | Line |\n")
f.write("|------|------|\n")
for call in self.results["append_calls"]:
f.write(f"| {call['file']} | {call['line']} |\n")
f.write("\n---\n\n")
f.write("## Remove Calls\n\n")
f.write("| File | Line |\n")
f.write("|------|------|\n")
for call in self.results["remove_calls"]:
f.write(f"| {call['file']} | {call['line']} |\n")
f.write("\n---\n\n")
f.write("## Handler Registrations\n\n")
f.write("| File | Line | Depth | Path Pattern | Handler Function |\n")
f.write("|------|------|-------|--------------|------------------|\n")
for handler in self.results["handler_registrations"]:
f.write(f"| {handler['file']} | {handler['line']} | "
f"{handler['depth']} | `{handler['path_pattern']}` | "
f"`{handler['handler_function']}` |\n")
f.write("\n---\n\n")
f.write("## Next Steps\n\n")
f.write("1. Review each upsert call - does it send complete or partial data?\n")
f.write("2. Review each handler - does it expect complete or partial data?\n")
f.write("3. Identify mismatches that will benefit from enrichment\n")
f.write("4. Plan which files need updating in Phase 2 and Phase 3\n")
print(f"\n✅ Report generated: {report_path}")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
root = sys.argv[1]
else:
# Assume we're in the scripts directory
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
analyzer = CallbackAnalyzer(root)
analyzer.analyze()
print("\n✨ Analysis complete!")
print("📋 Review CALLBACK_DICT_INVENTORY.md for detailed results")
print("📖 See CALLBACK_DICT_REFACTOR_PLAN.md for next steps\n")

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
Debug script to see raw telnet responses from 7D2D server.
This helps update the regex patterns in the action files.
"""
import telnetlib
import json
import time
import re
# Load config
with open('bot/options/module_telnet.json', 'r') as f:
config = json.load(f)
HOST = config['host']
PORT = config['port']
PASSWORD = config['password']
print(f"Connecting to {HOST}:{PORT}...")
# Connect
tn = telnetlib.Telnet(HOST, PORT, timeout=5)
# Wait for password prompt
response = tn.read_until(b"Please enter password:", timeout=3)
print("Got password prompt")
# Send password
tn.write(PASSWORD.encode('ascii') + b"\r\n")
# Wait for welcome message
time.sleep(1)
welcome = tn.read_very_eager().decode('utf-8')
print("Connected!\n")
# Commands to test
commands = [
'admin list',
'lp',
'gettime',
'getgamepref',
'getgamestat',
'listents'
]
for cmd in commands:
print(f"\n{'='*80}")
print(f"COMMAND: {cmd}")
print('='*80)
# Send command
tn.write(cmd.encode('ascii') + b"\r\n")
# Wait a bit for response
time.sleep(2)
# Read response
response = tn.read_very_eager().decode('utf-8')
print("RAW RESPONSE:")
print(repr(response)) # Show with escape characters
print("\nFORMATTED:")
print(response)
print()
tn.write(b"exit\r\n")
tn.close()
print("\n" + "="*80)
print("Done! Use these responses to update the regex patterns.")
print("="*80)

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
Comprehensive telnet command tester for 7D2D server
Tests all bot commands and validates regex patterns
"""
import telnetlib
import json
import time
import re
from pathlib import Path
# Load config
config_path = Path(__file__).parent / 'bot' / 'options' / 'module_telnet.json'
if config_path.exists():
with open(config_path, 'r') as f:
config = json.load(f)
else:
# Manual config if file doesn't exist
config = {
'host': input('Server IP: '),
'port': int(input('Server Port: ')),
'password': input('Telnet Password: ')
}
print(f"\n{'='*80}")
print(f"Connecting to {config['host']}:{config['port']}")
print(f"{'='*80}\n")
# Connect
tn = telnetlib.Telnet(config['host'], config['port'], timeout=10)
# Wait for password prompt
time.sleep(0.5)
output = tn.read_very_eager().decode('ascii', errors='ignore')
print('[CONNECTION] Established')
# Send password
tn.write((config['password'] + '\n').encode('ascii'))
time.sleep(0.5)
output = tn.read_very_eager().decode('ascii', errors='ignore')
print('[AUTH] Authenticated\n')
# Test commands with their expected regex patterns
commands = [
{
'name': 'admin list',
'command': 'admin list',
'regex': r"Executing\scommand\s\'admin list\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_adminlist>(?:Defined User Permissions\:.*?(?=Defined Group Permissions|$)))",
'wait': 1,
'description': 'Get admin list'
},
{
'name': 'lp (getplayers)',
'command': 'lp',
'regex': r"Executing\scommand\s\'lp\'\sby\sTelnet\sfrom\s"
r"(?P<called_by>.*?)\r?\n"
r"(?P<raw_playerdata>[\s\S]*?)"
r"Total\sof\s(?P<player_count>\d{1,2})\sin\sthe\sgame",
'wait': 1,
'description': 'Get player list'
},
{
'name': 'gettime',
'command': 'gettime',
'regex': r"Day\s(?P<day>\d{1,5}),\s(?P<hour>\d{1,2}):(?P<minute>\d{1,2})",
'wait': 1,
'description': 'Get game time'
},
{
'name': 'listents (getentities)',
'command': 'listents',
'regex': r"Executing\scommand\s\'listents\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_entity_data>[\s\S]*?)"
r"Total\sof\s(?P<entity_count>\d{1,3})\sin\sthe\sgame",
'wait': 1,
'description': 'Get entity list'
},
{
'name': 'getgamepref',
'command': 'getgamepref',
'regex': r"Executing\scommand\s\'getgamepref\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_gameprefs>(?:GamePref\..*?\r?\n)+)",
'wait': 2,
'description': 'Get game preferences'
},
{
'name': 'getgamestat',
'command': 'getgamestat',
'regex': r"Executing\scommand\s\'getgamestat\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_gamestats>(?:GameStat\..*?\r?\n)+)",
'wait': 2,
'description': 'Get game statistics'
},
{
'name': 'version',
'command': 'version',
'regex': None, # Just check raw output
'wait': 1,
'description': 'Get server version'
},
{
'name': 'help',
'command': 'help',
'regex': None, # Just check raw output
'wait': 2,
'description': 'Get available commands'
}
]
results = {
'passed': [],
'failed': [],
'no_regex': []
}
for test in commands:
print(f"\n{'='*80}")
print(f"TEST: {test['name']}")
print(f"Description: {test['description']}")
print(f"{'='*80}")
# Send command
print(f"\n>>> Sending: {test['command']}")
tn.write((test['command'] + '\n').encode('ascii'))
# Wait for response
time.sleep(test['wait'])
output = tn.read_very_eager().decode('ascii', errors='ignore')
# Show raw output
print(f"\n--- RAW OUTPUT (repr) ---")
print(repr(output))
print(f"\n--- RAW OUTPUT (formatted) ---")
print(output)
# Test regex if provided
if test['regex']:
print(f"\n--- REGEX TEST ---")
print(f"Pattern: {test['regex'][:100]}...")
matches = list(re.finditer(test['regex'], output, re.MULTILINE | re.DOTALL))
if matches:
print(f"✓ REGEX MATCHED! ({len(matches)} match(es))")
for i, match in enumerate(matches, 1):
print(f"\nMatch {i}:")
for group_name, group_value in match.groupdict().items():
value_preview = repr(group_value)[:100]
print(f" {group_name}: {value_preview}")
results['passed'].append(test['name'])
else:
print(f"✗ REGEX FAILED - NO MATCH!")
results['failed'].append(test['name'])
else:
print(f"\n--- NO REGEX TEST (raw output only) ---")
results['no_regex'].append(test['name'])
print(f"\n{'='*80}\n")
time.sleep(0.5) # Small delay between commands
# Close connection
tn.close()
# Summary
print(f"\n\n{'='*80}")
print("SUMMARY")
print(f"{'='*80}")
print(f"✓ Passed: {len(results['passed'])} - {', '.join(results['passed']) if results['passed'] else 'none'}")
print(f"✗ Failed: {len(results['failed'])} - {', '.join(results['failed']) if results['failed'] else 'none'}")
print(f"- No test: {len(results['no_regex'])} - {', '.join(results['no_regex']) if results['no_regex'] else 'none'}")
print(f"{'='*80}\n")
if results['failed']:
print("⚠️ SOME TESTS FAILED - Regex patterns need to be fixed!")
exit(1)
else:
print("✓ All regex tests passed!")
exit(0)