diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..96cf3c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,13 @@ +blank_issues_enabled: true +contact_links: + - name: WGDashboard Community Support (Github) + url: https://github.com/orgs/WGDashboard/discussions + about: Please ask and answer questions here. + + - name: WGDashboard Community Support (Discord) + url: https://discord.gg/72TwzjeuWm + about: Discord Server about WGDashboard + + - name: WGDashboard Issue reporting (Bugs and Feature Requests (FR)) + url: https://github.com/WGDashboard/WGDashboard/issues + about: Please report bugs or feature requests here so they can be tracked diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c3aaf24 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/src" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/.github" + schedule: + interval: "weekly" + + - package-ecosystem: "docker" + directory: "/docker" + schedule: + interval: "weekly" + + - package-ecosystem: "docker-compose" + directory: "/docker" + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql-analyze.yaml b/.github/workflows/codeql-analyze.yaml new file mode 100644 index 0000000..f6b2267 --- /dev/null +++ b/.github/workflows/codeql-analyze.yaml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + workflow_dispatch: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '30 5 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.gitignore b/.gitignore index 2eea525..2216582 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.env \ No newline at end of file +.env +**venv** +**cache** +*.log +src/data \ No newline at end of file diff --git a/README.md b/README.md index 79a87f8..9032be2 100644 --- a/README.md +++ b/README.md @@ -1,484 +1,9 @@ -# 🤖 WGDashboard Telegram Bot +# 🤖 WGDashboard Erinys (Telegram Bot) -[**🇪🇸 Español**](#-español) | [**🇬🇧 English**](#-english) +[**🇬🇧 English**](#-english) | [**🇪🇸 Español**](./docs/README_ESPANOL.md) --- -## 🇪🇸 Español - -# 🤖 Bot de Telegram para WGDashboard - -Bot profesional de Telegram para administrar y consultar WireGuard mediante la API de WGDashboard. Permite a administradores gestionar peers, monitorear estado del servidor y ejecutar acciones administrativas directamente desde Telegram. - -## ✨ Características principales - -### Administradores -- 📡 **Gestión de Configuraciones**: Ver todas las configuraciones WireGuard -- 👥 **Gestión de Peers**: Crear, eliminar, ver detalles y monitorear peers -- 🔒 **Restricciones**: Restringir/permitir acceso de peers específicos -- 🧹 **Limpiar Tráfico**: Resetear contadores de datos de peers -- ⏰ **Schedule Jobs**: Crear trabajos programados (límites de datos y fechas de expiración) -- 🖥️ **Estado del Sistema**: Monitorear CPU, memoria, discos e interfaces -- 📊 **Estadísticas**: Ver información detallada del sistema -- 👷 **Supervisión de Operadores**: Ver actividad de operadores autorizados -- 🌐 **Multiidioma**: Soporte para español e inglés - -### Operadores -- ➕ **Crear Peers Temporales**: Generar peers con límites automáticos -- 📥 **Descargar Config**: Obtener archivo `.conf` para el nuevo peer -- 🌐 **Cambiar Idioma**: Seleccionar entre español e inglés -- ⏳ **Límites Automáticos**: 1 GB de datos y 24 horas de duración por peer - -## 📁 Estructura del proyecto - -``` -TGBot-saveOK/ -├── main.py # Punto de entrada del bot -├── config.py # Configuración y variables de entorno -├── handlers.py # Handlers de comandos y callbacks (4100+ líneas) -├── keyboards.py # Teclados inline de Telegram -├── i18n.py # Sistema de traducción multiidioma -├── operators.py # Control de operadores autorizados -├── utils.py # Funciones utilitarias -├── wg_api.py # Cliente de la API WGDashboard -├── setup_logging.py # Configuración de logs -├── .env # Variables de entorno (NO subir a git) -├── requirements.txt # Dependencias del proyecto -├── README.md # Este archivo -├── locales/ -│ ├── es.json # Traducción al español -│ └── en.json # Traducción al inglés -└── info/ - ├── resumen.md # Documentación técnica - ├── CAMBIOS_REALIZADOS.md # Historial de cambios - └── IMPLEMENTACION_*.md # Documentación de features -``` - -## 🛠️ Requisitos previos - -### Sistema operativo -- Linux (Ubuntu 20.04 LTS o superior recomendado) -- Python 3.10 o superior -- pip (Python Package Manager) - -### Dependencias externas -- **WireGuard**: Instalado y configurado en el servidor -- **WGDashboard**: Funcionando y accesible via API -- **Bot de Telegram**: Creado con [@BotFather](https://t.me/BotFather) - -## 📦 Instalación paso a paso - -### 1. Clonar o descargar el proyecto - -```bash -git clone https://github.com/tu-usuario/TGBot-saveOK.git -cd TGBot-saveOK -``` - -### 2. Crear entorno virtual (recomendado) - -```bash -python3 -m venv .venv -source .venv/bin/activate # En Windows: .venv\Scripts\activate -``` - -### 3. Instalar dependencias - -```bash -pip install -r requirements.txt -``` - -### 4. Configurar variables de entorno - -Crear archivo `.env` en la raíz del proyecto: - -```bash -# ===== TELEGRAM ===== -TELEGRAM_BOT_TOKEN=tu_token_aqui # Obtener de @BotFather en Telegram - -# ===== WGDASHBOARD API ===== -WG_API_BASE_URL=https://tu-dominio.com/api # URL base de la API -WG_API_KEY=tu_api_key_aqui # API Key de WGDashboard -WG_API_PREFIX=wg # Prefijo de la API (si aplica) -API_TIMEOUT=10 # Timeout en segundos - -# ===== LOGGING ===== -LOG_FILE=wg_bot.log # Archivo de logs -LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR - -# ===== ACCESO ===== -ALLOWED_USERS=123456789,987654321 # IDs de Telegram autorizados -ROLE_ADMIN=admin # Rol de administrador -ROLE_OPERATOR=operator # Rol de operador -ADMIN_IDS=123456789 # IDs que son administradores -OPERATOR_IDS=987654321,111222333 # IDs que son operadores - -# ===== LÍMITES DE OPERADOR ===== -OPERATOR_DATA_LIMIT_GB=1 # GB por peer creado -OPERATOR_TIME_LIMIT_HOURS=24 # Horas de duración del peer -``` - -### 5. Configurar usuarios autorizados - -Editar `config.py` y agregar los IDs de Telegram: - -```python -# IDs autorizados a usar el bot -ALLOWED_USERS = [ - 123456789, # Admin 1 - 987654321, # Admin 2 - 111222333, # Operador 1 -] - -# Configuración de roles -ADMIN_USERS = [123456789, 987654321] -OPERATOR_USERS = [111222333] - -# Límites para operadores -OPERATOR_DATA_LIMIT_GB = 1 # 1 GB por peer -OPERATOR_TIME_LIMIT_HOURS = 24 # 24 horas de duración -``` - -**Para obtener tu ID de Telegram:** -1. Abre [@userinfobot](https://t.me/userinfobot) en Telegram -2. El bot te mostrará tu ID - -## 🚀 Ejecutar el bot - -### Desarrollo local - -```bash -# Con el entorno virtual activado -python main.py -``` - -### Producción en VPS - -#### Opción 1: Usando tmux (simple) - -```bash -# Instalar tmux -sudo apt-get install tmux - -# Crear sesión -tmux new-session -d -s wgbot -c /ruta/al/bot python main.py - -# Ver sesión -tmux list-sessions - -# Conectar a sesión -tmux attach-session -t wgbot - -# Desconectar (Ctrl+b, luego d) -``` - -#### Opción 2: Usando systemd (recomendado) - -**Crear archivo de servicio:** - -```bash -sudo nano /etc/systemd/system/wgbot.service -``` - -**Contenido del archivo:** - -```ini -[Unit] -Description=WGDashboard Telegram Bot -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=wgbot -Group=wgbot -WorkingDirectory=/home/wgbot/TGBot-saveOK -Environment="PATH=/home/wgbot/TGBot-saveOK/.venv/bin" -ExecStart=/home/wgbot/TGBot-saveOK/.venv/bin/python main.py -Restart=always -RestartSec=10 -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target -``` - -**Habilitar e iniciar el servicio:** - -```bash -# Recargar systemd -sudo systemctl daemon-reload - -# Habilitar para que inicie automáticamente -sudo systemctl enable wgbot - -# Iniciar el bot -sudo systemctl start wgbot - -# Ver estado -sudo systemctl status wgbot - -# Ver logs en tiempo real -sudo journalctl -u wgbot -f - -# Detener -sudo systemctl stop wgbot - -# Reiniciar -sudo systemctl restart wgbot -``` - -#### Opción 3: Usando Docker (si está disponible) - -```dockerfile -FROM python:3.11-slim - -WORKDIR /app - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -CMD ["python", "main.py"] -``` - -```bash -# Construir imagen -docker build -t wgbot . - -# Ejecutar contenedor -docker run -d --name wgbot --env-file .env wgbot -``` - -## 📋 Comandos disponibles - -### Para Administradores - -| Comando | Descripción | -|---------|------------| -| `/start` | Inicia el bot y muestra menú principal | -| `/help` | Muestra ayuda y comandos disponibles | -| `/stats` | Muestra estadísticas del sistema | -| `/configs` | Lista todas las configuraciones WireGuard | - -**Funciones via menú:** -- 📡 Configuraciones - Gestionar todas las configs -- 👥 Peers - Ver, crear, eliminar, listar -- 🖥️ Estado del Sistema - CPU, RAM, discos, network -- ⚡ Protocolos - Ver protocolos habilitados -- 👷 Operadores - Ver actividad de operadores -- 🌐 Idioma - Cambiar entre español e inglés - -### Para Operadores - -| Comando | Descripción | -|---------|------------| -| `/start` | Muestra menú para crear peers | -| `/help` | Información sobre límites y funciones | - -**Funciones:** -- ➕ Crear Peer - Generar nuevo peer temporal -- 📥 Descargar - Obtener archivo de configuración -- 🌐 Idioma - Cambiar idioma - -## 🔐 Seguridad y control de acceso - -### Niveles de permisos - -**Administrador (acceso completo)** -- Todas las funciones del bot -- Gestión total de peers -- Acceso a estadísticas -- Supervisión de operadores - -**Operador (acceso limitado)** -- Solo crear peers temporales -- Descargar configuración de peers creados -- Sin acceso a gestión de peers existentes - -**No autorizado (sin acceso)** -- El bot rechaza cualquier comando -- Mensaje de error: "Acceso restringido" - -### Mejores prácticas de seguridad - -1. **API Key de WGDashboard** - - Usar en variables de entorno `.env` - - Nunca subir `.env` a git - - Agregar a `.gitignore` - -2. **Token del Bot de Telegram** - - Mantener en `.env` - - Cambiar si se expone - - Usar restricciones de webhook si es posible - -3. **Acceso al servidor** - - Firewall activo - - SSH con claves públicas - - Cambiar puerto SSH por defecto - - Monitorear logs regularmente - -4. **WGDashboard** - - Detrás de proxy reverso (nginx) - - HTTPS obligatorio - - Autenticación fuerte - - Backups regulares - -## 📊 Funcionalidades detalladas - -### Gestión de Peers - -#### Crear Peer -- Nombre único -- IP automática -- Claves WireGuard generadas -- Archivo `.conf` enviado directamente -- Límites automáticos aplicados - -#### Detalles de Peer -- Información de red (IP, endpoint, DNS) -- Estado de conexión (conectado/desconectado) -- Tráfico (enviado, recibido, acumulativo) -- Claves (pública, pre-shared) -- Trabajos programados -- Enlaces compartidos - -#### Restricciones -- Restringir peer (bloquea conexión) -- Permitir access (quita restricción) -- Visualización por página -- Filtros disponibles - -### Schedule Jobs - -Los trabajos programados permiten: -- **Límite de datos**: Restringir cuando alcanza X GB -- **Fecha de expiración**: Restringir automáticamente en fecha X -- Crear/eliminar jobs sobre la marcha -- Gestión completa de tareas - -### Estadísticas del Sistema - -Monitorea en tiempo real: -- 💻 Uso de CPU (%) -- 🧠 Uso de RAM (% y detalles) -- 💾 Uso de discos (principales) -- 📡 Interfaces de red (tráfico) -- 🔗 Interfaces WireGuard - -## 🌐 Multiidioma - -El bot soporta automáticamente: -- 🇪🇸 **Español** - Interfaz completa en español -- 🇬🇧 **Inglés** - Interfaz completa en inglés - -Los usuarios pueden cambiar el idioma desde el menú en cualquier momento. Las preferencias se guardan automáticamente. - -## 📝 Logs y debugging - -### Ver logs en tiempo real - -```bash -# Si usas systemd -sudo journalctl -u wgbot -f - -# Si ejecutas directamente -tail -f wg_bot.log -``` - -### Niveles de log - -``` -DEBUG - Información detallada para debugging -INFO - Eventos importantes -WARNING - Advertencias (problemas potenciales) -ERROR - Errores que afectan funcionamiento -``` - -### Configurar nivel de log - -Editar `.env`: -``` -LOG_LEVEL=DEBUG # Para más detalle en desarrollo -``` - -## 🆘 Troubleshooting - -### El bot no responde - -```bash -# Verificar que está corriendo -sudo systemctl status wgbot - -# Ver logs -sudo journalctl -u wgbot -n 50 - -# Reiniciar -sudo systemctl restart wgbot -``` - -### Error: "No se puede conectar a API" - -- Verificar `WG_API_BASE_URL` en `.env` -- Verificar `WG_API_KEY` es correcta -- Probar conectividad: `curl https://tu-url/api/handshake` -- Verificar firewall/proxy permite la conexión - -### Error: "Token de Telegram inválido" - -- Verificar `TELEGRAM_BOT_TOKEN` en `.env` -- Crear nuevo token con [@BotFather](https://t.me/BotFather) -- Verificar no hay espacios en blanco - -### El bot no ve mis cambios - -```bash -# Reiniciar para cargar cambios -sudo systemctl restart wgbot - -# O si usas tmux -tmux send-keys -t wgbot "C-c" -python main.py -``` - -## 📚 Documentación adicional - -- `info/resumen.md` - Análisis técnico de la API -- `info/CAMBIOS_REALIZADOS.md` - Historial de cambios -- `info/IMPLEMENTACION_*.md` - Detalles de features - -## 🤝 Contribuciones - -Las contribuciones son bienvenidas. Por favor: - -1. Fork del proyecto -2. Crear rama para tu feature (`git checkout -b feature/AmazingFeature`) -3. Commit cambios (`git commit -m 'Add AmazingFeature'`) -4. Push a rama (`git push origin feature/AmazingFeature`) -5. Abrir Pull Request - -## 📄 Licencia - -Este proyecto se distribuye bajo la licencia MIT. Puedes usarlo, modificarlo y redistribuirlo libremente. - -## 👨‍💻 Autor - -**Jorge Elián Martinez Perdomo** - -Bot profesional de Telegram para administración de WireGuard usando WGDashboard - -- GitHub: [@jemartinezp1994](https://github.com/jemartinezp1994) -- Telegram: [@jemp01K](https://t.me/jemp01K) -- Discord: ff_c3r0 - ---- - -## 🇬🇧 English - -# 🤖 Telegram Bot for WGDashboard - Professional Telegram bot to manage and monitor WireGuard through the WGDashboard API. Allows administrators to manage peers, monitor server status, and execute administrative actions directly from Telegram. ## ✨ Main Features @@ -528,7 +53,7 @@ TGBot-saveOK/ ## 🛠️ Prerequisites ### Operating System -- Linux (Ubuntu 20.04 LTS or higher recommended) +- Linux (Ubuntu 22.04 LTS or higher recommended) - Python 3.10 or higher - pip (Python Package Manager) @@ -542,8 +67,8 @@ TGBot-saveOK/ ### 1. Clone or download the project ```bash -git clone https://github.com/your-username/TGBot-saveOK.git -cd TGBot-saveOK +git clone https://github.com/WGDashboard/WGErinys +cd WGErinys ``` ### 2. Create virtual environment (recommended) @@ -625,25 +150,7 @@ python main.py ### Production on VPS -#### Option 1: Using tmux (simple) - -```bash -# Install tmux -sudo apt-get install tmux - -# Create session -tmux new-session -d -s wgbot -c /path/to/bot python main.py - -# View sessions -tmux list-sessions - -# Connect to session -tmux attach-session -t wgbot - -# Disconnect (Ctrl+b, then d) -``` - -#### Option 2: Using systemd (recommended) +#### Option 1: Using systemd (recommended) **Create service file:** @@ -700,27 +207,32 @@ sudo systemctl stop wgbot sudo systemctl restart wgbot ``` -#### Option 3: Using Docker (if available) +#### Option 2: Using Docker (if available) -```dockerfile -FROM python:3.11-slim +```bash +# Build image +docker build . -f docker/Dockerfile -t wgerinys:latest -WORKDIR /app +# Run container +docker run -d --name wgerinys --env-file .env wgerinys:latest +``` -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +#### Option 3: Using tmux (simple) -COPY . . +```bash +# Install tmux +sudo apt-get install tmux -CMD ["python", "main.py"] -``` +# Create session +tmux new-session -d -s wgbot -c /path/to/bot python main.py -```bash -# Build image -docker build -t wgbot . +# View sessions +tmux list-sessions -# Run container -docker run -d --name wgbot --env-file .env wgbot +# Connect to session +tmux attach-session -t wgbot + +# Disconnect (Ctrl+b, then d) ``` ## 📋 Available Commands diff --git a/docker/Dockerfile b/docker/Dockerfile index 1f6890f..6888202 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1 +1,14 @@ -FROM debian:stable-slim AS builder +# Use a small Debian base image for our image. +FROM python:3.13-slim-trixie + +RUN mkdir -p /opt/wgerinys/src + +COPY ./src/requirements.txt /opt/wgerinys/src/requirements.txt + +RUN pip3 install --upgrade pip && \ + pip3 install -r /opt/wgerinys/src/requirements.txt && \ + ls -la /opt/wgerinys/src/ + +COPY ./src /opt/wgerinys/src + +ENTRYPOINT ["python3", "/opt/wgerinys/src/main.py"] \ No newline at end of file diff --git a/docs/README_ESPANOL.md b/docs/README_ESPANOL.md new file mode 100644 index 0000000..aee2b28 --- /dev/null +++ b/docs/README_ESPANOL.md @@ -0,0 +1,455 @@ +# 🤖 WGDashboard Erinys (Telegram Bot) + +--- + +Bot profesional de Telegram para administrar y consultar WireGuard mediante la API de WGDashboard. Permite a administradores gestionar peers, monitorear estado del servidor y ejecutar acciones administrativas directamente desde Telegram. + +## ✨ Características principales + +### Administradores +- 📡 **Gestión de Configuraciones**: Ver todas las configuraciones WireGuard +- 👥 **Gestión de Peers**: Crear, eliminar, ver detalles y monitorear peers +- 🔒 **Restricciones**: Restringir/permitir acceso de peers específicos +- 🧹 **Limpiar Tráfico**: Resetear contadores de datos de peers +- ⏰ **Schedule Jobs**: Crear trabajos programados (límites de datos y fechas de expiración) +- 🖥️ **Estado del Sistema**: Monitorear CPU, memoria, discos e interfaces +- 📊 **Estadísticas**: Ver información detallada del sistema +- 👷 **Supervisión de Operadores**: Ver actividad de operadores autorizados +- 🌐 **Multiidioma**: Soporte para español e inglés + +### Operadores +- ➕ **Crear Peers Temporales**: Generar peers con límites automáticos +- 📥 **Descargar Config**: Obtener archivo `.conf` para el nuevo peer +- 🌐 **Cambiar Idioma**: Seleccionar entre español e inglés +- ⏳ **Límites Automáticos**: 1 GB de datos y 24 horas de duración por peer + +## 📁 Estructura del proyecto + +``` +TGBot-saveOK/ +├── main.py # Punto de entrada del bot +├── config.py # Configuración y variables de entorno +├── handlers.py # Handlers de comandos y callbacks (4100+ líneas) +├── keyboards.py # Teclados inline de Telegram +├── i18n.py # Sistema de traducción multiidioma +├── operators.py # Control de operadores autorizados +├── utils.py # Funciones utilitarias +├── wg_api.py # Cliente de la API WGDashboard +├── setup_logging.py # Configuración de logs +├── .env # Variables de entorno (NO subir a git) +├── requirements.txt # Dependencias del proyecto +├── README.md # Este archivo +├── locales/ +│ ├── es.json # Traducción al español +│ └── en.json # Traducción al inglés +└── info/ + ├── resumen.md # Documentación técnica + ├── CAMBIOS_REALIZADOS.md # Historial de cambios + └── IMPLEMENTACION_*.md # Documentación de features +``` + +## 🛠️ Requisitos previos + +### Sistema operativo +- Linux (Ubuntu 20.04 LTS o superior recomendado) +- Python 3.10 o superior +- pip (Python Package Manager) + +### Dependencias externas +- **WireGuard**: Instalado y configurado en el servidor +- **WGDashboard**: Funcionando y accesible via API +- **Bot de Telegram**: Creado con [@BotFather](https://t.me/BotFather) + +## 📦 Instalación paso a paso + +### 1. Clonar o descargar el proyecto + +```bash +git clone https://github.com/WGDashboard/WGErinys +cd WGErinys +``` + +### 2. Crear entorno virtual (recomendado) + +```bash +python3 -m venv .venv +source .venv/bin/activate # En Windows: .venv\Scripts\activate +``` + +### 3. Instalar dependencias + +```bash +pip install -r requirements.txt +``` + +### 4. Configurar variables de entorno + +Crear archivo `.env` en la raíz del proyecto: + +```bash +# ===== TELEGRAM ===== +TELEGRAM_BOT_TOKEN=tu_token_aqui # Obtener de @BotFather en Telegram + +# ===== WGDASHBOARD API ===== +WG_API_BASE_URL=https://tu-dominio.com/api # URL base de la API +WG_API_KEY=tu_api_key_aqui # API Key de WGDashboard +WG_API_PREFIX=wg # Prefijo de la API (si aplica) +API_TIMEOUT=10 # Timeout en segundos + +# ===== LOGGING ===== +LOG_FILE=wg_bot.log # Archivo de logs +LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR + +# ===== ACCESO ===== +ALLOWED_USERS=123456789,987654321 # IDs de Telegram autorizados +ROLE_ADMIN=admin # Rol de administrador +ROLE_OPERATOR=operator # Rol de operador +ADMIN_IDS=123456789 # IDs que son administradores +OPERATOR_IDS=987654321,111222333 # IDs que son operadores + +# ===== LÍMITES DE OPERADOR ===== +OPERATOR_DATA_LIMIT_GB=1 # GB por peer creado +OPERATOR_TIME_LIMIT_HOURS=24 # Horas de duración del peer +``` + +### 5. Configurar usuarios autorizados + +Editar `config.py` y agregar los IDs de Telegram: + +```python +# IDs autorizados a usar el bot +ALLOWED_USERS = [ + 123456789, # Admin 1 + 987654321, # Admin 2 + 111222333, # Operador 1 +] + +# Configuración de roles +ADMIN_USERS = [123456789, 987654321] +OPERATOR_USERS = [111222333] + +# Límites para operadores +OPERATOR_DATA_LIMIT_GB = 1 # 1 GB por peer +OPERATOR_TIME_LIMIT_HOURS = 24 # 24 horas de duración +``` + +**Para obtener tu ID de Telegram:** +1. Abre [@userinfobot](https://t.me/userinfobot) en Telegram +2. El bot te mostrará tu ID + +## 🚀 Ejecutar el bot + +### Desarrollo local + +```bash +# Con el entorno virtual activado +python main.py +``` + +### Producción en VPS + +#### Opción 1: Usando systemd (recomendado) + +**Crear archivo de servicio:** + +```bash +sudo nano /etc/systemd/system/wgbot.service +``` + +**Contenido del archivo:** + +```ini +[Unit] +Description=WGDashboard Telegram Bot +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=wgbot +Group=wgbot +WorkingDirectory=/home/wgbot/TGBot-saveOK +Environment="PATH=/home/wgbot/TGBot-saveOK/.venv/bin" +ExecStart=/home/wgbot/TGBot-saveOK/.venv/bin/python main.py +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +``` + +**Habilitar e iniciar el servicio:** + +```bash +# Recargar systemd +sudo systemctl daemon-reload + +# Habilitar para que inicie automáticamente +sudo systemctl enable wgbot + +# Iniciar el bot +sudo systemctl start wgbot + +# Ver estado +sudo systemctl status wgbot + +# Ver logs en tiempo real +sudo journalctl -u wgbot -f + +# Detener +sudo systemctl stop wgbot + +# Reiniciar +sudo systemctl restart wgbot +``` + +#### Opción 2: Usando Docker (si está disponible) + +```bash +# Construir imagen +docker build . -f docker/Dockerfile -t wgerinys:latest + +# Ejecutar contenedor +docker run -d --name wgerinys --env-file .env wgerinys:latest +``` + +#### Opción 3: Usando tmux (simple) + +```bash +# Instalar tmux +sudo apt-get install tmux + +# Crear sesión +tmux new-session -d -s wgbot -c /ruta/al/bot python main.py + +# Ver sesión +tmux list-sessions + +# Conectar a sesión +tmux attach-session -t wgbot + +# Desconectar (Ctrl+b, luego d) +``` + +## 📋 Comandos disponibles + +### Para Administradores + +| Comando | Descripción | +|---------|------------| +| `/start` | Inicia el bot y muestra menú principal | +| `/help` | Muestra ayuda y comandos disponibles | +| `/stats` | Muestra estadísticas del sistema | +| `/configs` | Lista todas las configuraciones WireGuard | + +**Funciones via menú:** +- 📡 Configuraciones - Gestionar todas las configs +- 👥 Peers - Ver, crear, eliminar, listar +- 🖥️ Estado del Sistema - CPU, RAM, discos, network +- ⚡ Protocolos - Ver protocolos habilitados +- 👷 Operadores - Ver actividad de operadores +- 🌐 Idioma - Cambiar entre español e inglés + +### Para Operadores + +| Comando | Descripción | +|---------|------------| +| `/start` | Muestra menú para crear peers | +| `/help` | Información sobre límites y funciones | + +**Funciones:** +- ➕ Crear Peer - Generar nuevo peer temporal +- 📥 Descargar - Obtener archivo de configuración +- 🌐 Idioma - Cambiar idioma + +## 🔐 Seguridad y control de acceso + +### Niveles de permisos + +**Administrador (acceso completo)** +- Todas las funciones del bot +- Gestión total de peers +- Acceso a estadísticas +- Supervisión de operadores + +**Operador (acceso limitado)** +- Solo crear peers temporales +- Descargar configuración de peers creados +- Sin acceso a gestión de peers existentes + +**No autorizado (sin acceso)** +- El bot rechaza cualquier comando +- Mensaje de error: "Acceso restringido" + +### Mejores prácticas de seguridad + +1. **API Key de WGDashboard** + - Usar en variables de entorno `.env` + - Nunca subir `.env` a git + - Agregar a `.gitignore` + +2. **Token del Bot de Telegram** + - Mantener en `.env` + - Cambiar si se expone + - Usar restricciones de webhook si es posible + +3. **Acceso al servidor** + - Firewall activo + - SSH con claves públicas + - Cambiar puerto SSH por defecto + - Monitorear logs regularmente + +4. **WGDashboard** + - Detrás de proxy reverso (nginx) + - HTTPS obligatorio + - Autenticación fuerte + - Backups regulares + +## 📊 Funcionalidades detalladas + +### Gestión de Peers + +#### Crear Peer +- Nombre único +- IP automática +- Claves WireGuard generadas +- Archivo `.conf` enviado directamente +- Límites automáticos aplicados + +#### Detalles de Peer +- Información de red (IP, endpoint, DNS) +- Estado de conexión (conectado/desconectado) +- Tráfico (enviado, recibido, acumulativo) +- Claves (pública, pre-shared) +- Trabajos programados +- Enlaces compartidos + +#### Restricciones +- Restringir peer (bloquea conexión) +- Permitir access (quita restricción) +- Visualización por página +- Filtros disponibles + +### Schedule Jobs + +Los trabajos programados permiten: +- **Límite de datos**: Restringir cuando alcanza X GB +- **Fecha de expiración**: Restringir automáticamente en fecha X +- Crear/eliminar jobs sobre la marcha +- Gestión completa de tareas + +### Estadísticas del Sistema + +Monitorea en tiempo real: +- 💻 Uso de CPU (%) +- 🧠 Uso de RAM (% y detalles) +- 💾 Uso de discos (principales) +- 📡 Interfaces de red (tráfico) +- 🔗 Interfaces WireGuard + +## 🌐 Multiidioma + +El bot soporta automáticamente: +- 🇪🇸 **Español** - Interfaz completa en español +- 🇬🇧 **Inglés** - Interfaz completa en inglés + +Los usuarios pueden cambiar el idioma desde el menú en cualquier momento. Las preferencias se guardan automáticamente. + +## 📝 Logs y debugging + +### Ver logs en tiempo real + +```bash +# Si usas systemd +sudo journalctl -u wgbot -f + +# Si ejecutas directamente +tail -f wg_bot.log +``` + +### Niveles de log + +``` +DEBUG - Información detallada para debugging +INFO - Eventos importantes +WARNING - Advertencias (problemas potenciales) +ERROR - Errores que afectan funcionamiento +``` + +### Configurar nivel de log + +Editar `.env`: +``` +LOG_LEVEL=DEBUG # Para más detalle en desarrollo +``` + +## 🆘 Troubleshooting + +### El bot no responde + +```bash +# Verificar que está corriendo +sudo systemctl status wgbot + +# Ver logs +sudo journalctl -u wgbot -n 50 + +# Reiniciar +sudo systemctl restart wgbot +``` + +### Error: "No se puede conectar a API" + +- Verificar `WG_API_BASE_URL` en `.env` +- Verificar `WG_API_KEY` es correcta +- Probar conectividad: `curl https://tu-url/api/handshake` +- Verificar firewall/proxy permite la conexión + +### Error: "Token de Telegram inválido" + +- Verificar `TELEGRAM_BOT_TOKEN` en `.env` +- Crear nuevo token con [@BotFather](https://t.me/BotFather) +- Verificar no hay espacios en blanco + +### El bot no ve mis cambios + +```bash +# Reiniciar para cargar cambios +sudo systemctl restart wgbot + +# O si usas tmux +tmux send-keys -t wgbot "C-c" +python main.py +``` + +## 📚 Documentación adicional + +- `info/resumen.md` - Análisis técnico de la API +- `info/CAMBIOS_REALIZADOS.md` - Historial de cambios +- `info/IMPLEMENTACION_*.md` - Detalles de features + +## 🤝 Contribuciones + +Las contribuciones son bienvenidas. Por favor: + +1. Fork del proyecto +2. Crear rama para tu feature (`git checkout -b feature/AmazingFeature`) +3. Commit cambios (`git commit -m 'Add AmazingFeature'`) +4. Push a rama (`git push origin feature/AmazingFeature`) +5. Abrir Pull Request + +## 📄 Licencia + +Este proyecto se distribuye bajo la licencia MIT. Puedes usarlo, modificarlo y redistribuirlo libremente. + +## 👨‍💻 Autor + +**Jorge Elián Martinez Perdomo** + +Bot profesional de Telegram para administración de WireGuard usando WGDashboard + +- GitHub: [@jemartinezp1994](https://github.com/jemartinezp1994) +- Telegram: [@jemp01K](https://t.me/jemp01K) +- Discord: ff_c3r0 diff --git a/src/main.py b/src/main.py index 9471a9e..faf8f47 100644 --- a/src/main.py +++ b/src/main.py @@ -5,7 +5,7 @@ import signal import sys import asyncio -from handlers import language_command + from telegram import Update from telegram.ext import ( ApplicationBuilder, @@ -17,13 +17,14 @@ ) # Importar configuraciones y módulos -import config -from setup_logging import logger -from handlers import ( - start_command, help_command, stats_command, configs_command, - callback_handler, text_message_handler +from modules import config + +from modules.setup_logging import logger +from modules.handlers import ( + callback_handler, configs_command, help_command, language_command, + start_command, stats_command, text_message_handler ) -from utils import is_allowed, t +from modules.utils import is_allowed, translate # ================= FUNCIONES DE UTILIDAD ================= # def validate_environment(): @@ -84,7 +85,7 @@ async def stats_command_admin(update: Update, context: ContextTypes.DEFAULT_TYPE user_id = update.effective_user.id if not is_admin(user_id): await update.message.reply_text( - t(user_id, "errors.access_denied"), + translate(user_id, "errors.access_denied"), parse_mode="Markdown" ) return @@ -94,7 +95,7 @@ async def configs_command_admin(update: Update, context: ContextTypes.DEFAULT_TY user_id = update.effective_user.id if not is_admin(user_id): await update.message.reply_text( - t(user_id, "errors.access_denied"), + translate(user_id, "errors.access_denied"), parse_mode="Markdown" ) return @@ -174,4 +175,4 @@ def main(): logger.info("👋 Bot detenido correctamente") if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/src/config.py b/src/modules/config.py similarity index 86% rename from src/config.py rename to src/modules/config.py index 3f7b1f7..34ac42d 100644 --- a/src/config.py +++ b/src/modules/config.py @@ -13,12 +13,12 @@ TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "") # ================= WGDASHBOARD API ================= # -WG_API_BASE_URL = os.getenv("WG_API_BASE_URL", "http://localhost:10086/api") -WG_API_KEY = os.getenv("WG_API_KEY", "") +WGD_API_BASE_URL = os.getenv("WGD_API_BASE_URL", "http://localhost:10086/api") +WGD_API_KEY = os.getenv("WGD_API_KEY", "") API_TIMEOUT = int(os.getenv("API_TIMEOUT", "10")) # Opcional: Prefijo para la URL del dashboard -WG_API_PREFIX = os.getenv("WG_API_PREFIX", "") +WGD_API_PREFIX = os.getenv("WGD_API_PREFIX", "") # ================= SEGURIDAD ================= # # Roles @@ -39,7 +39,7 @@ OPERATORS_DB = os.path.join(DATA_DIR, "operator_peers.json") # ================= LOGGING ================= # -LOG_FILE = os.getenv("LOG_FILE", "wg_bot.log") +LOG_FILE = os.getenv("LOG_FILE", "wgd_bot.log") LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") LOG_MAX_SIZE = 10 * 1024 * 1024 # 10MB LOG_BACKUP_COUNT = 5 @@ -61,11 +61,11 @@ def validate_config(): if not TELEGRAM_BOT_TOKEN: errors.append("TELEGRAM_BOT_TOKEN no configurado") - if not WG_API_KEY: - errors.append("WG_API_KEY no configurada") + if not WGD_API_KEY: + errors.append("WGD_API_KEY no configurada") - if not WG_API_BASE_URL: - errors.append("WG_API_BASE_URL no configurada") + if not WGD_API_BASE_URL: + errors.append("WGD_API_BASE_URL no configurada") if not ALLOWED_USERS: errors.append("ALLOWED_USERS está vacío") diff --git a/src/handlers.py b/src/modules/handlers.py similarity index 75% rename from src/handlers.py rename to src/modules/handlers.py index 76186b0..fab9b58 100644 --- a/src/handlers.py +++ b/src/modules/handlers.py @@ -28,27 +28,27 @@ except ImportError: HAS_CRYPTOGRAPHY = False -from config import ROLE_OPERATOR, ALLOWED_USERS, ROLE_ADMIN, OPERATOR_DATA_LIMIT_GB, OPERATOR_TIME_LIMIT_HOURS -from operators import operators_db -from utils import is_allowed, is_admin, is_operator, can_operator_create_peer, log_command_with_role +from .config import ROLE_OPERATOR, ALLOWED_USERS, ROLE_ADMIN, OPERATOR_DATA_LIMIT_GB, OPERATOR_TIME_LIMIT_HOURS +from .operators import operators_db +from .utils import is_allowed, is_admin, is_operator, can_operator_create_peer, log_command_with_role -from config import ALLOWED_USERS -from wg_api import api_client -from keyboards import ( +from .config import ALLOWED_USERS +from .wgd_api import api_client +from .keyboards import ( main_menu, config_menu, paginated_configs_menu, restrictions_menu, paginated_restricted_peers_menu, paginated_unrestricted_peers_menu, paginated_reset_traffic_menu, confirmation_menu, back_button, refresh_button, operator_main_menu, InlineKeyboardMarkup, decode_callback_data ) -from utils import ( +from .utils import ( is_allowed, get_user_name, format_peer_info, format_system_status, format_config_summary, send_large_message, log_command, log_callback, log_error, format_bytes_human, format_time_ago, log_callback_with_role, log_command_with_role, is_admin, is_operator, can_operator_create_peer, - t, get_user_language, set_user_language, escape_markdown + translate, get_user_language, set_user_language, escape_markdown ) logger = logging.getLogger(__name__) @@ -56,7 +56,7 @@ # ================= FUNCIONES AUXILIARES ================= # def format_peer_for_list(peer: Dict, user_id: int) -> str: """Formatea un peer para la lista básica""" - name = peer.get('name', t(user_id, "common.no_name")) + name = peer.get('name', translate(user_id, "common.no_name")) latest_handshake = peer.get('latest_handshake_seconds', 0) status = peer.get('status', 'stopped') @@ -64,8 +64,8 @@ def format_peer_for_list(peer: Dict, user_id: int) -> str: status_emoji = "✅" if status == 'running' and latest_handshake > 0 else "❌" last_seen = format_time_ago(latest_handshake, user_id) - allowed_ip = peer.get('allowed_ip', t(user_id, "common.na")) - return f"{status_emoji} **{name}** - IP: `{allowed_ip}` - {t(user_id, 'peers.connected').split(':')[0]}: {last_seen}" + allowed_ip = peer.get('allowed_ip', translate(user_id, "common.na")) + return f"{status_emoji} **{name}** - IP: `{allowed_ip}` - {translate(user_id, 'peers.connected').split(':')[0]}: {last_seen}" @@ -73,13 +73,13 @@ def format_schedule_job_for_list(job: Dict, user_id: int) -> str: """Formatea un schedule job para la lista""" action = job.get('Action', 'desconocido') field = job.get('Field', 'desconocido') - value = job.get('Value', t(user_id, "common.na")) + value = job.get('Value', translate(user_id, "common.na")) if field == "total_data": - field_text = t(user_id, "schedule.data_limit") + field_text = translate(user_id, "schedule.data_limit") value_display = f"{value} GB" elif field == "date": - field_text = t(user_id, "schedule.expiry_date") + field_text = translate(user_id, "schedule.expiry_date") value_display = value else: field_text = field @@ -89,16 +89,16 @@ def format_schedule_job_for_list(job: Dict, user_id: int) -> str: def format_peer_for_detail(peer: Dict, user_id: int) -> str: """Formatea un peer para vista detallada (con Markdown)""" - name = peer.get('name', t(user_id, "common.no_name")) + name = peer.get('name', translate(user_id, "common.no_name")) status = peer.get('status', 'stopped') latest_handshake = peer.get('latest_handshake_seconds', 0) - allowed_ip = peer.get('allowed_ip', t(user_id, "common.na")) - endpoint = peer.get('endpoint', t(user_id, "common.na")) + allowed_ip = peer.get('allowed_ip', translate(user_id, "common.na")) + endpoint = peer.get('endpoint', translate(user_id, "common.na")) transfer_rx = peer.get('transfer_rx', 0) transfer_tx = peer.get('transfer_tx', 0) status_emoji = "✅" if status == 'running' and latest_handshake > 0 else "❌" - status_text = t(user_id, "peers.status_connected") if status == 'running' and latest_handshake > 0 else t(user_id, "peers.status_disconnected") + status_text = translate(user_id, "peers.status_connected") if status == 'running' and latest_handshake > 0 else translate(user_id, "peers.status_disconnected") last_seen = format_time_ago(latest_handshake, user_id) @@ -118,10 +118,10 @@ def format_bytes(size): lines = [ f"{status_emoji} *{name}*", f"IP: `{allowed_ip}`", - f"{t(user_id, 'restrictions.status')}: {status_text}", - f"{t(user_id, 'peers.connected').split(':')[0]}: {last_seen}", - f"{t(user_id, 'traffic.received')}: {rx_fmt}", - f"{t(user_id, 'traffic.sent')}: {tx_fmt}", + f"{translate(user_id, 'restrictions.status')}: {status_text}", + f"{translate(user_id, 'peers.connected').split(':')[0]}: {last_seen}", + f"{translate(user_id, 'traffic.received')}: {rx_fmt}", + f"{translate(user_id, 'traffic.sent')}: {tx_fmt}", f"Endpoint: `{endpoint}`" ] @@ -130,17 +130,17 @@ def format_bytes(size): def format_peer_for_detail_plain(peer: Dict, user_id: int) -> str: """Formatea un peer para vista detallada (texto plano con TODOS los detalles)""" # Información básica - name = peer.get('name', t(user_id, "common.no_name")) + name = peer.get('name', translate(user_id, "common.no_name")) status = peer.get('status', 'stopped') latest_handshake = peer.get('latest_handshake_seconds', 0) - allowed_ip = peer.get('allowed_ip', t(user_id, "common.na")) + allowed_ip = peer.get('allowed_ip', translate(user_id, "common.na")) peer_id = peer.get('id', 'N/A') endpoint = peer.get('endpoint', 'N/A') remote_endpoint = peer.get('remote_endpoint', 'N/A') # Estado y tiempo status_emoji = "✅" if status == 'running' and latest_handshake > 0 else "❌" - status_text = t(user_id, "peers.status_connected") if status == 'running' and latest_handshake > 0 else t(user_id, "peers.status_disconnected") + status_text = translate(user_id, "peers.status_connected") if status == 'running' and latest_handshake > 0 else translate(user_id, "peers.status_disconnected") last_seen = format_time_ago(latest_handshake, user_id) # Datos de tráfico @@ -165,63 +165,63 @@ def format_peer_for_detail_plain(peer: Dict, user_id: int) -> str: # Construir el mensaje detallado usando traducciones pub_display = f"{peer_id[:20]}...{peer_id[-10:] if len(peer_id) > 30 else ''}" - preshared_status = t(user_id, "peers.details_preshared_yes") if preshared_key else t(user_id, "peers.details_preshared_no") + preshared_status = translate(user_id, "peers.details_preshared_yes") if preshared_key else translate(user_id, "peers.details_preshared_no") lines = [ - t(user_id, "peers.details_separator"), - t(user_id, "peers.details_name", emoji=status_emoji, name=name), - t(user_id, "peers.details_separator"), + translate(user_id, "peers.details_separator"), + translate(user_id, "peers.details_name", emoji=status_emoji, name=name), + translate(user_id, "peers.details_separator"), "", - t(user_id, "peers.details_network_header"), - t(user_id, "peers.details_assigned_ip", ip=allowed_ip), - t(user_id, "peers.details_local_endpoint", endpoint=endpoint), - t(user_id, "peers.details_remote_endpoint", remote=remote_endpoint), - t(user_id, "peers.details_endpoint_allowed_ip", value=endpoint_allowed_ip), - t(user_id, "peers.details_dns", dns=dns), + translate(user_id, "peers.details_network_header"), + translate(user_id, "peers.details_assigned_ip", ip=allowed_ip), + translate(user_id, "peers.details_local_endpoint", endpoint=endpoint), + translate(user_id, "peers.details_remote_endpoint", remote=remote_endpoint), + translate(user_id, "peers.details_endpoint_allowed_ip", value=endpoint_allowed_ip), + translate(user_id, "peers.details_dns", dns=dns), "", - t(user_id, "peers.details_security_header"), - t(user_id, "peers.details_public_key", public=pub_display), - t(user_id, "peers.details_preshared", status=preshared_status), + translate(user_id, "peers.details_security_header"), + translate(user_id, "peers.details_public_key", public=pub_display), + translate(user_id, "peers.details_preshared", status=preshared_status), "", - t(user_id, "peers.details_connection_header"), - t(user_id, "peers.details_status", status=status_text), - t(user_id, "peers.details_last_handshake", last_seen=last_seen), - t(user_id, "peers.details_keepalive", keepalive=keepalive), + translate(user_id, "peers.details_connection_header"), + translate(user_id, "peers.details_status", status=status_text), + translate(user_id, "peers.details_last_handshake", last_seen=last_seen), + translate(user_id, "peers.details_keepalive", keepalive=keepalive), "", - t(user_id, "peers.details_traffic_header"), - t(user_id, "peers.details_received_current", value=format_bytes_human(total_receive)), - t(user_id, "peers.details_sent_current", value=format_bytes_human(total_sent)), - t(user_id, "peers.details_total_current", value=format_bytes_human(total_receive + total_sent)), - t(user_id, "peers.details_received_cumu", value=format_bytes_human(cumu_receive)), - t(user_id, "peers.details_sent_cumu", value=format_bytes_human(cumu_sent)), - (t(user_id, "peers.details_data_limit", value=total_data_limit) if total_data_limit > 0 else t(user_id, "peers.details_data_limit_none")), + translate(user_id, "peers.details_traffic_header"), + translate(user_id, "peers.details_received_current", value=format_bytes_human(total_receive)), + translate(user_id, "peers.details_sent_current", value=format_bytes_human(total_sent)), + translate(user_id, "peers.details_total_current", value=format_bytes_human(total_receive + total_sent)), + translate(user_id, "peers.details_received_cumu", value=format_bytes_human(cumu_receive)), + translate(user_id, "peers.details_sent_cumu", value=format_bytes_human(cumu_sent)), + (translate(user_id, "peers.details_data_limit", value=total_data_limit) if total_data_limit > 0 else translate(user_id, "peers.details_data_limit_none")), "", - t(user_id, "peers.details_technical_header"), - t(user_id, "peers.details_mtu", mtu=mtu), + translate(user_id, "peers.details_technical_header"), + translate(user_id, "peers.details_mtu", mtu=mtu), "", ] # Agregar Jobs si existen (usar traducciones) if jobs: - lines.append(t(user_id, "peers.details_jobs_header")) + lines.append(translate(user_id, "peers.details_jobs_header")) for i, job in enumerate(jobs, 1): action = job.get('Action', 'Desconocida') field = job.get('Field', 'Desconocido') value = job.get('Value', 'N/A') - lines.append(t(user_id, "peers.details_job_line", index=i, action=action, field=field, value=value)) + lines.append(translate(user_id, "peers.details_job_line", index=i, action=action, field=field, value=value)) lines.append("") # Agregar ShareLinks si existen (usar traducciones) if share_links: - lines.append(t(user_id, "peers.details_share_links_header")) + lines.append(translate(user_id, "peers.details_share_links_header")) for i, link in enumerate(share_links, 1): share_id = link.get('ShareID', 'N/A') - exp_date = link.get('ExpireDate', t(user_id, "common.na")) + exp_date = link.get('ExpireDate', translate(user_id, "common.na")) short_id = f"{share_id[:15]}..." - lines.append(t(user_id, "peers.details_share_link_line", index=i, short=short_id, exp_date=exp_date)) + lines.append(translate(user_id, "peers.details_share_link_line", index=i, short=short_id, exp_date=exp_date)) lines.append("") - lines.append(t(user_id, "peers.details_separator")) + lines.append(translate(user_id, "peers.details_separator")) return "\n".join(lines) @@ -315,13 +315,13 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): welcome_text = "" if is_admin(user_id): - welcome_text = t(user_id, "welcome.admin_title", name=get_user_name(update)) + "\n\n" - welcome_text += t(user_id, "welcome.admin_message") + welcome_text = translate(user_id, "welcome.admin_title", name=get_user_name(update)) + "\n\n" + welcome_text += translate(user_id, "welcome.admin_message") keyboard = main_menu(is_admin(user_id), is_operator(user_id), user_id) elif is_operator(user_id): - welcome_text = t(user_id, "welcome.operator_title", name=get_user_name(update)) + "\n\n" - welcome_text += t(user_id, "welcome.operator_message") + welcome_text = translate(user_id, "welcome.operator_title", name=get_user_name(update)) + "\n\n" + welcome_text += translate(user_id, "welcome.operator_message") keyboard = operator_main_menu(user_id) await update.message.reply_text( @@ -339,11 +339,11 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): log_command_with_role(update, "help") if is_admin(user_id): - help_text = t(user_id, "help.admin") + help_text = translate(user_id, "help.admin") keyboard = main_menu(is_admin(user_id), is_operator(user_id), user_id) elif is_operator(user_id): - help_text = t(user_id, "help.operator") + help_text = translate(user_id, "help.operator") keyboard = operator_main_menu(user_id) await update.message.reply_text( @@ -363,24 +363,24 @@ async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE): keyboard = InlineKeyboardMarkup([ [ InlineKeyboardButton( - t(user_id, "language.spanish"), + translate(user_id, "language.spanish"), callback_data="set_language:es" ), InlineKeyboardButton( - t(user_id, "language.english"), + translate(user_id, "language.english"), callback_data="set_language:en" ) ], [ InlineKeyboardButton( - t(user_id, "menu.back"), + translate(user_id, "menu.back"), callback_data="main_menu" ) ] ]) await update.message.reply_text( - t(user_id, "language.select"), + translate(user_id, "language.select"), reply_markup=keyboard, parse_mode="Markdown" ) @@ -394,7 +394,7 @@ async def stats_command(update: Update, context: ContextTypes.DEFAULT_TYPE): log_command(update, "stats") await update.message.reply_text( - t(user_id, "system.getting_stats"), + translate(user_id, "system.getting_stats"), reply_markup=refresh_button("system_status", user_id) ) @@ -403,7 +403,7 @@ async def stats_command(update: Update, context: ContextTypes.DEFAULT_TYPE): if not result.get("status"): await update.message.reply_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=refresh_button("system_status", user_id) ) return @@ -426,7 +426,7 @@ async def configs_command(update: Update, context: ContextTypes.DEFAULT_TYPE): log_command(update, "configs") await update.message.reply_text( - t(user_id, "configurations.getting"), + translate(user_id, "configurations.getting"), reply_markup=refresh_button("configs", user_id) ) @@ -434,7 +434,7 @@ async def configs_command(update: Update, context: ContextTypes.DEFAULT_TYPE): if not result.get("status"): await update.message.reply_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=refresh_button("configs", user_id) ) return @@ -443,7 +443,7 @@ async def configs_command(update: Update, context: ContextTypes.DEFAULT_TYPE): if not configs: await update.message.reply_text( - t(user_id, "errors.no_configs"), + translate(user_id, "errors.no_configs"), reply_markup=refresh_button("configs", user_id) ) return @@ -454,10 +454,10 @@ async def configs_command(update: Update, context: ContextTypes.DEFAULT_TYPE): total_configs = len(configs) total_pages = (total_configs - 1) // 8 + 1 - message = t(user_id, "configurations.title") + "\n" - message += t(user_id, "configurations.page", current=1, total=total_pages) + "\n" - message += t(user_id, "configurations.total", count=total_configs) + "\n\n" - message += t(user_id, "configurations.select_config") + message = translate(user_id, "configurations.title") + "\n" + message += translate(user_id, "configurations.page", current=1, total=total_pages) + "\n" + message += translate(user_id, "configurations.total", count=total_configs) + "\n\n" + message += translate(user_id, "configurations.select_config") await update.message.reply_text( message, @@ -500,7 +500,7 @@ async def callback_handler(update: Update, context: CallbackContext): for admin_action in admin_only_actions: if callback_data.startswith(admin_action): await query.edit_message_text( - t(user_id, "errors.access_denied_operator"), + translate(user_id, "errors.access_denied_operator"), reply_markup=operator_main_menu(user_id), parse_mode="Markdown" ) @@ -554,7 +554,7 @@ async def callback_handler(update: Update, context: CallbackContext): lang = callback_data.split(":")[1] if set_user_language(user_id, lang): await query.edit_message_text( - t(user_id, "language.changed", language="Español" if lang == "es" else "English"), + translate(user_id, "language.changed", language="Español" if lang == "es" else "English"), reply_markup=main_menu(is_admin(user_id), is_operator(user_id), user_id), parse_mode="Markdown" ) @@ -831,8 +831,8 @@ async def handle_operator_main_menu(query): if not username.strip(): username = "Operador" - welcome_text = t(user_id, "welcome.operator_title", name=username) + "\n\n" - welcome_text += t(user_id, "welcome.operator_message") + welcome_text = translate(user_id, "welcome.operator_title", name=username) + "\n\n" + welcome_text += translate(user_id, "welcome.operator_message") await query.edit_message_text( welcome_text, @@ -845,8 +845,8 @@ async def handle_restrictions_menu(query, config_name: str): """Muestra el menú de restricciones""" user_id = query.from_user.id await query.edit_message_text( - t(user_id, "restrictions.title", config=config_name) + "\n\n" + - t(user_id, "restrictions.select_option"), + translate(user_id, "restrictions.title", config=config_name) + "\n\n" + + translate(user_id, "restrictions.select_option"), reply_markup=restrictions_menu(config_name, user_id), parse_mode="Markdown" ) @@ -854,13 +854,13 @@ async def handle_restrictions_menu(query, config_name: str): async def handle_restricted_peers_list(query, context: CallbackContext, config_name: str, page: int = 0): """Muestra la lista paginada de peers restringidos - VERSIÓN SIMPLIFICADA""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "restrictions.getting_restricted")) + await query.edit_message_text(translate(user_id, "restrictions.getting_restricted")) result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"restrictions:{config_name}", user_id) ) return @@ -869,7 +869,7 @@ async def handle_restricted_peers_list(query, context: CallbackContext, config_n if not restricted_peers: await query.edit_message_text( - t(user_id, "errors.no_restricted"), + translate(user_id, "errors.no_restricted"), reply_markup=back_button(f"restrictions:{config_name}", user_id), parse_mode="Markdown" ) @@ -886,10 +886,10 @@ async def handle_restricted_peers_list(query, context: CallbackContext, config_n keyboard = paginated_restricted_peers_menu(restricted_peers, config_name, page, user_id) - message = t(user_id, "restrictions.restricted_list", config=config_name) + "\n\n" + message = translate(user_id, "restrictions.restricted_list", config=config_name) + "\n\n" message += f"📊 Total: {total_peers}\n" - message += t(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n\n" - message += t(user_id, "restrictions.select_peer_unrestrict") + message += translate(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n\n" + message += translate(user_id, "restrictions.select_peer_unrestrict") await query.edit_message_text( message, @@ -900,13 +900,13 @@ async def handle_restricted_peers_list(query, context: CallbackContext, config_n async def handle_unrestricted_peers_list(query, context: CallbackContext, config_name: str, page: int = 0): """Muestra la lista paginada de peers NO restringidos - VERSIÓN SIMPLIFICADA""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "restrictions.getting_unrestricted")) + await query.edit_message_text(translate(user_id, "restrictions.getting_unrestricted")) result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"restrictions:{config_name}", user_id) ) return @@ -925,7 +925,7 @@ async def handle_unrestricted_peers_list(query, context: CallbackContext, config if not unrestricted_peers: await query.edit_message_text( - t(user_id, "errors.all_restricted"), + translate(user_id, "errors.all_restricted"), reply_markup=back_button(f"restrictions:{config_name}", user_id), parse_mode="Markdown" ) @@ -942,10 +942,10 @@ async def handle_unrestricted_peers_list(query, context: CallbackContext, config keyboard = paginated_unrestricted_peers_menu(unrestricted_peers, config_name, page, user_id) - message = t(user_id, "restrictions.available_list", config=config_name) + "\n\n" - message += t(user_id, "restrictions.available_peers", count=total_peers) + "\n" - message += t(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n\n" - message += t(user_id, "restrictions.select_peer_restrict") + message = translate(user_id, "restrictions.available_list", config=config_name) + "\n\n" + message += translate(user_id, "restrictions.available_peers", count=total_peers) + "\n" + message += translate(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n\n" + message += translate(user_id, "restrictions.select_peer_restrict") await query.edit_message_text( message, @@ -959,24 +959,24 @@ async def language_command_callback(query, context): keyboard = InlineKeyboardMarkup([ [ InlineKeyboardButton( - t(user_id, "language.spanish"), + translate(user_id, "language.spanish"), callback_data="set_language:es" ), InlineKeyboardButton( - t(user_id, "language.english"), + translate(user_id, "language.english"), callback_data="set_language:en" ) ], [ InlineKeyboardButton( - t(user_id, "menu.back"), + translate(user_id, "menu.back"), callback_data="main_menu" ) ] ]) await query.edit_message_text( - t(user_id, "language.select"), + translate(user_id, "language.select"), reply_markup=keyboard, parse_mode="Markdown" ) @@ -984,14 +984,14 @@ async def language_command_callback(query, context): async def handle_unrestrict_simple(query, context: CallbackContext, config_name: str, peer_index: int): """Quitar restricción de forma simplificada - VERSIÓN SEGURA CON MARKDOWN""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "restrictions.removing")) + await query.edit_message_text(translate(user_id, "restrictions.removing")) # Obtener peers del contexto restricted_peers = context.user_data.get(f'restricted_peers_{config_name}', []) if peer_index < 0 or peer_index >= len(restricted_peers): await query.edit_message_text( - t(user_id, "errors.invalid_index"), + translate(user_id, "errors.invalid_index"), reply_markup=back_button(f"restricted_peers:{config_name}:0", user_id) ) return @@ -1002,7 +1002,7 @@ async def handle_unrestrict_simple(query, context: CallbackContext, config_name: if not public_key: await query.edit_message_text( - t(user_id, "errors.no_public_key"), + translate(user_id, "errors.no_public_key"), reply_markup=back_button(f"restricted_peers:{config_name}:0", user_id) ) return @@ -1012,10 +1012,10 @@ async def handle_unrestrict_simple(query, context: CallbackContext, config_name: if result.get("status"): await query.edit_message_text( - t(user_id, "success.restriction_removed", peer_name=peer_name), + translate(user_id, "success.restriction_removed", peer_name=peer_name), reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton(t(user_id, "actions.update_list"), callback_data=f"restricted_peers:{config_name}:0")], - [InlineKeyboardButton(t(user_id, "menu.back"), callback_data=f"restrictions:{config_name}")] + [InlineKeyboardButton(translate(user_id, "actions.update_list"), callback_data=f"restricted_peers:{config_name}:0")], + [InlineKeyboardButton(translate(user_id, "menu.back"), callback_data=f"restrictions:{config_name}")] ]), parse_mode="Markdown" ) @@ -1030,14 +1030,14 @@ async def handle_unrestrict_simple(query, context: CallbackContext, config_name: async def handle_restrict_simple(query, context: CallbackContext, config_name: str, peer_index: int): """Restringir peer de forma simplificada - VERSIÓN SEGURA CON MARKDOWN""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "restrictions.restricting")) + await query.edit_message_text(translate(user_id, "restrictions.restricting")) # Obtener peers del contexto unrestricted_peers = context.user_data.get(f'unrestricted_peers_{config_name}', []) if peer_index < 0 or peer_index >= len(unrestricted_peers): await query.edit_message_text( - t(user_id, "errors.invalid_index"), + translate(user_id, "errors.invalid_index"), reply_markup=back_button(f"restrict_peer_menu:{config_name}:0", user_id) ) return @@ -1048,7 +1048,7 @@ async def handle_restrict_simple(query, context: CallbackContext, config_name: s if not public_key: await query.edit_message_text( - t(user_id, "errors.no_public_key"), + translate(user_id, "errors.no_public_key"), reply_markup=back_button(f"restrict_peer_menu:{config_name}:0", user_id) ) return @@ -1058,10 +1058,10 @@ async def handle_restrict_simple(query, context: CallbackContext, config_name: s if result.get("status"): await query.edit_message_text( - t(user_id, "success.peer_restricted", peer_name=peer_name), + translate(user_id, "success.peer_restricted", peer_name=peer_name), reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton(t(user_id, "actions.update_list"), callback_data=f"restrict_peer_menu:{config_name}:0")], - [InlineKeyboardButton(t(user_id, "menu.back"), callback_data=f"restrictions:{config_name}")] + [InlineKeyboardButton(translate(user_id, "actions.update_list"), callback_data=f"restrict_peer_menu:{config_name}:0")], + [InlineKeyboardButton(translate(user_id, "menu.back"), callback_data=f"restrictions:{config_name}")] ]), parse_mode="Markdown" ) @@ -1111,19 +1111,19 @@ async def handle_unrestrict_confirm(query, context: CallbackContext, config_name allowed_ip = peer_info.get('allowed_ip', 'N/A') - message = f"⚠️ *{t(user_id, 'restrictions.confirm_remove')}*\n\n" - message += f"{t(user_id, 'restrictions.confirm_remove_message')}\n\n" - message += f"*{t(user_id, 'schedule.peer_label')}* {peer_name}\n" - message += f"*{t(user_id, 'schedule.config_label')}* {config_name}\n" + message = f"⚠️ *{translate(user_id, 'restrictions.confirm_remove')}*\n\n" + message += f"{translate(user_id, 'restrictions.confirm_remove_message')}\n\n" + message += f"*{translate(user_id, 'schedule.peer_label')}* {peer_name}\n" + message += f"*{translate(user_id, 'schedule.config_label')}* {config_name}\n" message += f"*IP:* `{allowed_ip}`\n" - message += f"*{t(user_id, 'configurations.public_key_label')}* `{public_key[:30]}...`\n\n" - message += t(user_id, "restrictions.unrestrict_info") + message += f"*{translate(user_id, 'configurations.public_key_label')}* `{public_key[:30]}...`\n\n" + message += translate(user_id, "restrictions.unrestrict_info") # Codificar la clave pública completa from keyboards import safe_callback_data safe_public_key = safe_callback_data(public_key) - keyboard = confirmation_menu(config_name, safe_public_key, "unrestrict", t(user_id, "restrictions.remove_restriction_button", default="Quitar Restricción")) + keyboard = confirmation_menu(config_name, safe_public_key, "unrestrict", translate(user_id, "restrictions.remove_restriction_button", default="Quitar Restricción")) await query.edit_message_text( message, @@ -1141,22 +1141,22 @@ async def handle_unrestrict_confirm(query, context: CallbackContext, config_name async def handle_unrestrict_execute(query, config_name: str, public_key: str): """Ejecuta la acción de quitar restricción a un peer""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "restrictions.removing")) + await query.edit_message_text(translate(user_id, "restrictions.removing")) result = api_client.allow_access_peer(config_name, public_key) if result.get("status"): await query.edit_message_text( - f"{t(user_id, 'restrictions.unrestrict_success')}\n\n" - f"{t(user_id, 'restrictions.unrestrict_success_msg', config=config_name)}", + f"{translate(user_id, 'restrictions.unrestrict_success')}\n\n" + f"{translate(user_id, 'restrictions.unrestrict_success_msg', config=config_name)}", reply_markup=back_button(f"restricted_peers:{config_name}:0", user_id), parse_mode="Markdown" ) else: error_msg = result.get('message', 'Error desconocido') await query.edit_message_text( - f"{t(user_id, 'restrictions.unrestrict_error')}\n\n" - f"*{t(user_id, 'errors.error_label')}:* {error_msg}", + f"{translate(user_id, 'restrictions.unrestrict_error')}\n\n" + f"*{translate(user_id, 'errors.error_label')}:* {error_msg}", reply_markup=back_button(f"restricted_peers:{config_name}:0", user_id), parse_mode="Markdown" ) @@ -1166,13 +1166,13 @@ async def handle_unrestrict_execute(query, config_name: str, public_key: str): async def handle_reset_traffic_menu(query, context: CallbackContext, config_name: str, page: int = 0): """Muestra el menú para seleccionar peer para resetear tráfico""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "traffic.getting_peers", config=config_name)) + await query.edit_message_text(translate(user_id, "traffic.getting_peers", config=config_name)) result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -1181,7 +1181,7 @@ async def handle_reset_traffic_menu(query, context: CallbackContext, config_name if not peers: await query.edit_message_text( - t(user_id, "errors.no_peers", config=config_name), + translate(user_id, "errors.no_peers", config=config_name), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -1197,10 +1197,10 @@ async def handle_reset_traffic_menu(query, context: CallbackContext, config_name keyboard = paginated_reset_traffic_menu(peers, config_name, page, user_id) - message = t(user_id, "traffic.title", config=config_name) + "\n\n" - message += t(user_id, "traffic.total_peers", count=total_peers) + "\n" - message += t(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n\n" - message += t(user_id, "traffic.select_peer") + message = translate(user_id, "traffic.title", config=config_name) + "\n\n" + message += translate(user_id, "traffic.total_peers", count=total_peers) + "\n" + message += translate(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n\n" + message += translate(user_id, "traffic.select_peer") await query.edit_message_text( message, @@ -1215,7 +1215,7 @@ async def handle_reset_traffic_confirm(query, config_name: str, peer_index: int, result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"reset_traffic:{config_name}:{page}", user_id) ) return @@ -1224,7 +1224,7 @@ async def handle_reset_traffic_confirm(query, config_name: str, peer_index: int, if peer_index < 0 or peer_index >= len(peers): await query.edit_message_text( - t(user_id, "errors.invalid_index"), + translate(user_id, "errors.invalid_index"), reply_markup=back_button(f"reset_traffic:{config_name}:{page}", user_id) ) return @@ -1235,7 +1235,7 @@ async def handle_reset_traffic_confirm(query, config_name: str, peer_index: int, if not public_key: await query.edit_message_text( - t(user_id, "errors.no_public_key"), + translate(user_id, "errors.no_public_key"), reply_markup=back_button(f"reset_traffic:{config_name}:{page}", user_id) ) return @@ -1254,26 +1254,26 @@ async def handle_reset_traffic_confirm(query, config_name: str, peer_index: int, config_name_safe = escape_markdown(config_name) public_key_short_safe = public_key[:30] + "..." - message = f"⚠️ *{t(user_id, 'traffic.confirm')}*\n\n" - message += f"{t(user_id, 'traffic.confirm_message')}\n\n" - message += f"*{t(user_id, 'schedule.peer_label')}* {peer_name_safe}\n" - message += f"*{t(user_id, 'schedule.config_label')}* {config_name_safe}\n" - message += f"*{t(user_id, 'traffic.current_data')}*\n" - message += f" ⬇️ {t(user_id, 'traffic.received', value=format_bytes_human(total_receive))}\n" - message += f" ⬆️ {t(user_id, 'traffic.sent', value=format_bytes_human(total_sent))}\n" - message += f" 📊 {t(user_id, 'traffic.total', value=total_data)}\n\n" - message += f"*{t(user_id, 'configurations.public_key_label')}:* `{public_key_short_safe}`\n\n" - message += f"⚠️ *{t(user_id, 'traffic.irreversible')}*" + message = f"⚠️ *{translate(user_id, 'traffic.confirm')}*\n\n" + message += f"{translate(user_id, 'traffic.confirm_message')}\n\n" + message += f"*{translate(user_id, 'schedule.peer_label')}* {peer_name_safe}\n" + message += f"*{translate(user_id, 'schedule.config_label')}* {config_name_safe}\n" + message += f"*{translate(user_id, 'traffic.current_data')}*\n" + message += f" ⬇️ {translate(user_id, 'traffic.received', value=format_bytes_human(total_receive))}\n" + message += f" ⬆️ {translate(user_id, 'traffic.sent', value=format_bytes_human(total_sent))}\n" + message += f" 📊 {ranslatet(user_id, 'traffic.total', value=total_data)}\n\n" + message += f"*{translate(user_id, 'configurations.public_key_label')}:* `{public_key_short_safe}`\n\n" + message += f"⚠️ *{translate(user_id, 'traffic.irreversible')}*" # Crear teclado directamente sin usar confirmation_menu keyboard = InlineKeyboardMarkup([ [ InlineKeyboardButton( - t(user_id, "actions.yes") + ", " + t(user_id, "actions.clean_traffic"), + translate(user_id, "actions.yes") + ", " + translate(user_id, "actions.clean_traffic"), callback_data=f"reset_traffic_final:{config_name}:{peer_index}:{page}" ), InlineKeyboardButton( - t(user_id, "actions.cancel"), + translate(user_id, "actions.cancel"), callback_data=f"reset_traffic:{config_name}:{page}" ) ] @@ -1293,13 +1293,13 @@ async def handle_reset_traffic_final(query, context: CallbackContext, config_nam logger.info(f"Reset traffic - Config: {config_name}, Peer idx: {peer_idx}, Page: {page_num}") - await query.edit_message_text(t(user_id, "traffic.resetting")) + await query.edit_message_text(translate(user_id, "traffic.resetting")) # Obtener información del peer para obtener su clave pública result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"reset_traffic:{config_name}:{page_num}", user_id) ) return @@ -1308,7 +1308,7 @@ async def handle_reset_traffic_final(query, context: CallbackContext, config_nam if peer_idx < 0 or peer_idx >= len(peers): await query.edit_message_text( - t(user_id, "errors.invalid_index"), + translate(user_id, "errors.invalid_index"), reply_markup=back_button(f"reset_traffic:{config_name}:{page_num}", user_id) ) return @@ -1319,7 +1319,7 @@ async def handle_reset_traffic_final(query, context: CallbackContext, config_nam if not public_key: await query.edit_message_text( - t(user_id, "errors.no_public_key"), + translate(user_id, "errors.no_public_key"), reply_markup=back_button(f"reset_traffic:{config_name}:{page_num}", user_id) ) return @@ -1337,17 +1337,17 @@ async def handle_reset_traffic_final(query, context: CallbackContext, config_nam config_name_safe = escape_markdown(config_name) await query.edit_message_text( - t(user_id, "success.data_reset", peer_name=peer_name_safe, config_name=config_name_safe), + translate(user_id, "success.data_reset", peer_name=peer_name_safe, config_name=config_name_safe), reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton(t(user_id, "actions.update_list"), callback_data=f"reset_traffic:{config_name}:{page_num}")], - [InlineKeyboardButton(t(user_id, "actions.back_to_config"), callback_data=f"cfg:{config_name}")] + [InlineKeyboardButton(translate(user_id, "actions.update_list"), callback_data=f"reset_traffic:{config_name}:{page_num}")], + [InlineKeyboardButton(translate(user_id, "actions.back_to_config"), callback_data=f"cfg:{config_name}")] ]), parse_mode="Markdown" ) else: error_msg = escape_markdown(result.get('message', 'Error desconocido')) await query.edit_message_text( - f"❌ *{t(user_id, 'errors.api_error', message=error_msg)}*\n\n" + f"❌ *{translate(user_id, 'errors.api_error', message=error_msg)}*\n\n" f"*Endpoint usado:* /resetPeerData/{config_name}\n" f"*Public key:* `{public_key[:30]}...`", reply_markup=back_button(f"reset_traffic:{config_name}:{page_num}", user_id), @@ -1474,24 +1474,24 @@ async def handle_restrict_confirm(query, context: CallbackContext, config_name: allowed_ip = peer_info.get('allowed_ip', 'N/A') status = peer_info.get('status', 'stopped') - status_text = t(user_id, "status.connected") if status == 'running' else t(user_id, "status.disconnected") + status_text = translate(user_id, "status.connected") if status == 'running' else translate(user_id, "status.disconnected") # Construir mensaje de confirmación - message = f"{t(user_id, 'restrictions.confirm_title')}\n\n" - message += f"{t(user_id, 'restrictions.confirm_message')}\n\n" - message += f"*{t(user_id, 'schedule.peer_label')}* {peer_name}\n" - message += f"*{t(user_id, 'schedule.config_label')}* {config_name}\n" - message += f"*{t(user_id, 'restrictions.ip')}* `{allowed_ip}`\n" - message += f"*{t(user_id, 'restrictions.status')}* {status_text}\n" - message += f"*{t(user_id, 'restrictions.public_key')}* `{public_key[:30]}...`\n\n" - message += t(user_id, 'restrictions.warning') + message = f"{translate(user_id, 'restrictions.confirm_title')}\n\n" + message += f"{translate(user_id, 'restrictions.confirm_message')}\n\n" + message += f"*{translate(user_id, 'schedule.peer_label')}* {peer_name}\n" + message += f"*{translate(user_id, 'schedule.config_label')}* {config_name}\n" + message += f"*{translate(user_id, 'restrictions.ip')}* `{allowed_ip}`\n" + message += f"*{translate(user_id, 'restrictions.status')}* {status_text}\n" + message += f"*{translate(user_id, 'restrictions.public_key')}* `{public_key[:30]}...`\n\n" + message += translate(user_id, 'restrictions.warning') # Codificar la clave pública completa from keyboards import safe_callback_data safe_public_key = safe_callback_data(public_key) # Teclado de confirmación - keyboard = confirmation_menu(config_name, safe_public_key, "restrict", t(user_id, "actions.restrict"), user_id=user_id) + keyboard = confirmation_menu(config_name, safe_public_key, "restrict", translate(user_id, "actions.restrict"), user_id=user_id) await query.edit_message_text( message, @@ -1502,29 +1502,29 @@ async def handle_restrict_confirm(query, context: CallbackContext, config_name: except Exception as e: logger.error(f"Error en confirmación de restricción: {str(e)}") await query.edit_message_text( - f"❌ {t(user_id, 'errors.request_processing_error', message=str(e))}", + f"❌ {translate(user_id, 'errors.request_processing_error', message=str(e))}", reply_markup=back_button(f"restrict_peer_menu:{config_name}:0", user_id) ) async def handle_restrict_execute(query, config_name: str, public_key: str): """Ejecuta la acción de restringir un peer""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "restrictions.restricting")) + await query.edit_message_text(translate(user_id, "restrictions.restricting")) result = api_client.restrict_peer(config_name, public_key) if result.get("status"): await query.edit_message_text( - f"{t(user_id, 'restrictions.success')}\n\n" - f"{t(user_id, 'restrictions.success_msg', config=config_name)}", + f"{translate(user_id, 'restrictions.success')}\n\n" + f"{translate(user_id, 'restrictions.success_msg', config=config_name)}", reply_markup=back_button(f"restrict_peer_menu:{config_name}:0", user_id), parse_mode="Markdown" ) else: error_msg = result.get('message', 'Error desconocido') await query.edit_message_text( - f"{t(user_id, 'restrictions.error')}\n\n" - f"{t(user_id, 'restrictions.error_msg', message=error_msg)}", + f"{translate(user_id, 'restrictions.error')}\n\n" + f"{translate(user_id, 'restrictions.error_msg', message=error_msg)}", reply_markup=back_button(f"restrict_peer_menu:{config_name}:0", user_id), parse_mode="Markdown" ) @@ -1534,7 +1534,7 @@ async def handle_main_menu(query): """Muestra el menú principal para administradores""" user_id = query.from_user.id await query.edit_message_text( - t(user_id, "menu.main") + "\n" + t(user_id, "menu.select_option"), + translate(user_id, "menu.main") + "\n" + translate(user_id, "menu.select_option"), reply_markup=main_menu(is_admin(user_id), is_operator(user_id), user_id), parse_mode="Markdown" ) @@ -1542,31 +1542,31 @@ async def handle_main_menu(query): async def handle_handshake(query): """Verifica la conexión con la API""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "system.connecting")) + await query.edit_message_text(translate(user_id, "system.connecting")) result = api_client.handshake() if result.get("status"): await query.edit_message_text( - t(user_id, "success.connection_ok"), + translate(user_id, "success.connection_ok"), reply_markup=refresh_button("handshake", user_id) ) else: await query.edit_message_text( - t(user_id, "errors.connection_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.connection_error", message=result.get('message', 'Error desconocido')), reply_markup=refresh_button("handshake", user_id) ) async def handle_configs(query, page: int = 0): """Muestra la lista de configuraciones""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "configurations.getting")) + await query.edit_message_text(translate(user_id, "configurations.getting")) result = api_client.get_configurations() if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=refresh_button("configs", user_id) ) return @@ -1575,7 +1575,7 @@ async def handle_configs(query, page: int = 0): if not configs: await query.edit_message_text( - t(user_id, "errors.no_configs"), + translate(user_id, "errors.no_configs"), reply_markup=refresh_button("configs", user_id) ) return @@ -1586,10 +1586,10 @@ async def handle_configs(query, page: int = 0): total_configs = len(configs) total_pages = (total_configs - 1) // 8 + 1 - message = t(user_id, "configurations.title") + "\n" - message += t(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n" - message += t(user_id, "configurations.total", count=total_configs) + "\n\n" - message += t(user_id, "configurations.select_config") + message = translate(user_id, "configurations.title") + "\n" + message += translate(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n" + message += translate(user_id, "configurations.total", count=total_configs) + "\n\n" + message += translate(user_id, "configurations.select_config") await query.edit_message_text( message, @@ -1600,13 +1600,13 @@ async def handle_configs(query, page: int = 0): async def handle_configs_summary(query): """Muestra un resumen de todas las configuraciones""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "configurations.getting_summary")) + await query.edit_message_text(translate(user_id, "configurations.getting_summary")) result = api_client.get_configurations() if not result.get("status"): await query.edit_message_text( - f"❌ {t(user_id, 'errors.api_error', message=result.get('message', 'Error desconocido'))}", + f"❌ {translate(user_id, 'errors.api_error', message=result.get('message', 'Error desconocido'))}", reply_markup=refresh_button("configs_summary", user_id) ) return @@ -1624,7 +1624,7 @@ async def show_configurations(update: Update, context: ContextTypes.DEFAULT_TYPE """Función auxiliar para mostrar menú de configuraciones""" user_id = update.from_user.id if hasattr(update, 'message') else update.callback_query.from_user.id if hasattr(update, 'message'): - await update.message.reply_text(t(user_id, "configurations.getting")) + await update.message.reply_text(translate(user_id, "configurations.getting")) query = update is_message = True else: @@ -1634,7 +1634,7 @@ async def show_configurations(update: Update, context: ContextTypes.DEFAULT_TYPE result = api_client.get_configurations() if not result.get("status"): - error_msg = f"❌ {t(user_id, 'errors.api_error', message=result.get('message', 'Error desconocido'))}" + error_msg = f"❌ {translate(user_id, 'errors.api_error', message=result.get('message', 'Error desconocido'))}" if is_message: await update.message.reply_text(error_msg) else: @@ -1644,7 +1644,7 @@ async def show_configurations(update: Update, context: ContextTypes.DEFAULT_TYPE configs = result.get("data", []) if not configs: - no_configs_msg = t(user_id, "errors.no_configs_available") + no_configs_msg = translate(user_id, "errors.no_configs_available") if is_message: await update.message.reply_text(no_configs_msg) else: @@ -1656,10 +1656,10 @@ async def show_configurations(update: Update, context: ContextTypes.DEFAULT_TYPE total_configs = len(configs) total_pages = (total_configs - 1) // 8 + 1 - message = f"{t(user_id, 'configurations.available_configs')}\n" - message += f"{t(user_id, 'configurations.page', current=page + 1, total=total_pages)}\n" - message += f"{t(user_id, 'configurations.total_count', count=total_configs)}\n\n" - message += t(user_id, "configurations.select_config_options") + message = f"{translate(user_id, 'configurations.available_configs')}\n" + message += f"{translate(user_id, 'configurations.page', current=page + 1, total=total_pages)}\n" + message += f"{translate(user_id, 'configurations.total_count', count=total_configs)}\n\n" + message += translate(user_id, "configurations.select_config_options") if is_message: await update.message.reply_text(message, reply_markup=keyboard, parse_mode="Markdown") @@ -1669,13 +1669,13 @@ async def show_configurations(update: Update, context: ContextTypes.DEFAULT_TYPE async def handle_config_detail(query, config_name: str): """Muestra el menú de una configuración específica""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "configurations.getting_info", name=config_name)) + await query.edit_message_text(translate(user_id, "configurations.getting_info", name=config_name)) result = api_client.get_configuration_detail(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button("configs", user_id) ) return @@ -1695,18 +1695,18 @@ async def handle_config_detail(query, config_name: str): if peers_result.get("status"): restricted_count = peers_result.get("metadata", {}).get("restricted", 0) - message = t(user_id, "configurations.detail", name=config_name) + "\n\n" - message += t(user_id, "configurations.port", port=listen_port) + "\n" - message += f"🔑 {t(user_id, 'configurations.public_key_label')}: `{public_key[:30]}...`\n" - message += t(user_id, "configurations.peers", connected=connected_peers, total=total_peers) + "\n" - message += t(user_id, "configurations.restricted", count=restricted_count) + "\n\n" - message += t(user_id, "configurations.options") + "\n" - message += t(user_id, "configurations.full_details") + "\n" - message += t(user_id, "configurations.delete_peer") + "\n" - message += t(user_id, "configurations.add_peer") + "\n" - message += t(user_id, "configurations.schedule_jobs") + "\n" - message += t(user_id, "configurations.restrictions") + "\n" - message += t(user_id, "configurations.refresh") + message = translate(user_id, "configurations.detail", name=config_name) + "\n\n" + message += translate(user_id, "configurations.port", port=listen_port) + "\n" + message += f"🔑 {ranslatet(user_id, 'configurations.public_key_label')}: `{public_key[:30]}...`\n" + message += translate(user_id, "configurations.peers", connected=connected_peers, total=total_peers) + "\n" + message += translate(user_id, "configurations.restricted", count=restricted_count) + "\n\n" + message += translate(user_id, "configurations.options") + "\n" + message += translate(user_id, "configurations.full_details") + "\n" + message += translate(user_id, "configurations.delete_peer") + "\n" + message += translate(user_id, "configurations.add_peer") + "\n" + message += translate(user_id, "configurations.schedule_jobs") + "\n" + message += translate(user_id, "configurations.restrictions") + "\n" + message += translate(user_id, "configurations.refresh") await query.edit_message_text( message, @@ -1718,17 +1718,17 @@ async def handle_operator_download_template(query, context: CallbackContext, con """Descarga plantilla para operadores (sin claves privadas)""" try: # Obtener información del servidor por defecto - from config import WG_API_BASE_URL + from config import WGD_API_BASE_URL import urllib.parse - parsed_url = urllib.parse.urlparse(WG_API_BASE_URL) + parsed_url = urllib.parse.urlparse(WGD_API_BASE_URL) server_host = parsed_url.hostname # Obtener información de la configuración config_result = api_client.get_configuration_detail(config_name) if not config_result.get("status"): await query.edit_message_text( - t(user_id, "errors.config_info_error"), + translate(user_id, "errors.config_info_error"), reply_markup=operator_main_menu(user_id) ) return @@ -1744,10 +1744,10 @@ async def handle_operator_download_template(query, context: CallbackContext, con allowed_ip = peer_data.get('allowed_ip', '10.21.0.2/32') if peer_data else "10.21.0.x/32" # Usar endpoint personalizado si existe - from config import WG_API_BASE_URL + from config import WGD_API_BASE_URL import urllib.parse - parsed_url = urllib.parse.urlparse(WG_API_BASE_URL) + parsed_url = urllib.parse.urlparse(WGD_API_BASE_URL) server_host = parsed_url.hostname if endpoint: @@ -1757,9 +1757,9 @@ async def handle_operator_download_template(query, context: CallbackContext, con endpoint_port = listen_port # Construir plantilla internacionalizada - template = f"{t(user_id, 'operator.template_header', peer_name=peer_name)}" - template += f"\n[Interface]\nPrivateKey = {t(user_id, 'operator.template_private_key_placeholder')}\nAddress = {allowed_ip}\nDNS = {dns}\n\n[Peer]\nPublicKey = {server_public_key}\nAllowedIPs = 0.0.0.0/0\nEndpoint = {endpoint_host}:{endpoint_port}\nPersistentKeepalive = 21" - template += f"{t(user_id, 'operator.template_limits_notice')}" + template = f"{translate(user_id, 'operator.template_header', peer_name=peer_name)}" + template += f"\n[Interface]\nPrivateKey = {translate(user_id, 'operator.template_private_key_placeholder')}\nAddress = {allowed_ip}\nDNS = {dns}\n\n[Peer]\nPublicKey = {server_public_key}\nAllowedIPs = 0.0.0.0/0\nEndpoint = {endpoint_host}:{endpoint_port}\nPersistentKeepalive = 21" + template += f"{translate(user_id, 'operator.template_limits_notice')}" # Nombre del archivo filename = f"{peer_name}_TEMPLATE.conf" @@ -1772,17 +1772,17 @@ async def handle_operator_download_template(query, context: CallbackContext, con # Enviar archivo await query.message.reply_document( document=InputFile(file_like, filename=filename), - caption=t(user_id, "operator.template_caption", peer_name=peer_name) + caption=translate(user_id, "operator.template_caption", peer_name=peer_name) ) # Actualizar mensaje con instrucciones await query.edit_message_text( - t(user_id, "operator.template_generated_body", + translate(user_id, "operator.template_generated_body", peer_name=peer_name, allowed_ip=allowed_ip, dns=dns, endpoint_host=endpoint_host, endpoint_port=endpoint_port, server_public_key=server_public_key[:30]), reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton(t(user_id, "actions.back_to_menu"), callback_data="operator_main_menu")] + [InlineKeyboardButton(translate(user_id, "actions.back_to_menu"), callback_data="operator_main_menu")] ]), parse_mode="Markdown" ) @@ -1790,8 +1790,8 @@ async def handle_operator_download_template(query, context: CallbackContext, con except Exception as e: logger.error(f"Error al descargar plantilla para operador: {str(e)}") await query.edit_message_text( - f"❌ {t(user_id, 'operator.template_error')}\n\n" - f"*{t(user_id, 'errors.error_label')}* {str(e)}", + f"❌ {translate(user_id, 'operator.template_error')}\n\n" + f"*{translate(user_id, 'errors.error_label')}* {str(e)}", reply_markup=operator_main_menu(user_id), parse_mode="Markdown" ) @@ -1804,13 +1804,13 @@ async def handle_peers_detailed(query, config_name: str): async def handle_peers_detailed_paginated(query, config_name: str, page: int = 0): """Muestra información detallada de peers paginada - VERSIÓN SIN FORMATO""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "peers.preparing_details")) + await query.edit_message_text(translate(user_id, "peers.preparing_details")) result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - f"❌ {t(user_id, 'errors.api_error', message=result.get('message', 'Error desconocido'))}", + f"❌ {translate(user_id, 'errors.api_error', message=result.get('message', 'Error desconocido'))}", reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -1819,7 +1819,7 @@ async def handle_peers_detailed_paginated(query, config_name: str, page: int = 0 if not peers: await query.edit_message_text( - t(user_id, "peers.no_peers_in_config", config=config_name), + translate(user_id, "peers.no_peers_in_config", config=config_name), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -1839,9 +1839,9 @@ async def handle_peers_detailed_paginated(query, config_name: str, page: int = 0 page_peers = peers[start_idx:end_idx] # Construir mensaje para esta página - SIN FORMATO MARKDOWN - message = f"{t(user_id, 'peers.details_title', config=config_name)}\n\n" - message += f"{t(user_id, 'peers.page', current=page+1, total=total_pages)}\n" - message += f"{t(user_id, 'peers.showing', start=start_idx+1, end=min(end_idx, total_peers), total=total_peers)}\n\n" + message = f"{translate(user_id, 'peers.details_title', config=config_name)}\n\n" + message += f"{translate(user_id, 'peers.page', current=page+1, total=total_pages)}\n" + message += f"{translate(user_id, 'peers.showing', start=start_idx+1, end=min(end_idx, total_peers), total=total_peers)}\n\n" for i, peer in enumerate(page_peers, start_idx + 1): message += f"Peer #{i}\n" @@ -1855,12 +1855,12 @@ async def handle_peers_detailed_paginated(query, config_name: str, page: int = 0 nav_buttons = [] if page > 0: nav_buttons.append( - InlineKeyboardButton(t(user_id, "peers.nav_prev"), callback_data=f"peers_detailed_paginated:{config_name}:{page-1}") + InlineKeyboardButton(translate(user_id, "peers.nav_prev"), callback_data=f"peers_detailed_paginated:{config_name}:{page-1}") ) if page < total_pages - 1: nav_buttons.append( - InlineKeyboardButton(t(user_id, "peers.nav_next"), callback_data=f"peers_detailed_paginated:{config_name}:{page+1}") + InlineKeyboardButton(translate(user_id, "peers.nav_next"), callback_data=f"peers_detailed_paginated:{config_name}:{page+1}") ) if nav_buttons: @@ -1872,7 +1872,7 @@ async def handle_peers_detailed_paginated(query, config_name: str, page: int = 0 # ]) keyboard.append([ - InlineKeyboardButton(t(user_id, "peers.back_menu"), callback_data=f"cfg:{config_name}") + InlineKeyboardButton(translate(user_id, "peers.back_menu"), callback_data=f"cfg:{config_name}") ]) await query.edit_message_text( @@ -1884,13 +1884,13 @@ async def handle_peers_detailed_paginated(query, config_name: str, page: int = 0 async def handle_delete_peer_menu(query, config_name: str, page: int = 0): """Muestra el menú para seleccionar peer a eliminar""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "peers.getting")) + await query.edit_message_text(translate(user_id, "peers.getting")) result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -1899,7 +1899,7 @@ async def handle_delete_peer_menu(query, config_name: str, page: int = 0): if not peers: await query.edit_message_text( - t(user_id, "peers.no_peers", config=config_name), + translate(user_id, "peers.no_peers", config=config_name), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -1933,15 +1933,15 @@ async def handle_delete_peer_menu(query, config_name: str, page: int = 0): keyboard.append(nav_buttons) # Botón para cancelar - keyboard.append([InlineKeyboardButton(t(user_id, "actions.cancel"), callback_data=f"cfg:{config_name}")]) + keyboard.append([InlineKeyboardButton(translate(user_id, "actions.cancel"), callback_data=f"cfg:{config_name}")]) total_peers = len(peers) total_pages = (total_peers - 1) // 8 + 1 - message = t(user_id, "peers.delete_title", config=config_name) + "\n" - message += t(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n" - message += t(user_id, "configurations.total", count=total_peers) + "\n\n" - message += t(user_id, "peers.delete_select") + message = translate(user_id, "peers.delete_title", config=config_name) + "\n" + message += translate(user_id, "configurations.page", current=page + 1, total=total_pages) + "\n" + message += translate(user_id, "configurations.total", count=total_peers) + "\n\n" + message += translate(user_id, "peers.delete_select") await query.edit_message_text( message, @@ -1960,7 +1960,7 @@ async def handle_delete_peer_confirm(query, config_name: str, peer_index: str): result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -1969,7 +1969,7 @@ async def handle_delete_peer_confirm(query, config_name: str, peer_index: str): if idx < 0 or idx >= len(peers): await query.edit_message_text( - t(user_id, "errors.invalid_index"), + translate(user_id, "errors.invalid_index"), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -1984,29 +1984,29 @@ async def handle_delete_peer_confirm(query, config_name: str, peer_index: str): if not peer_key: await query.edit_message_text( - t(user_id, "errors.no_public_key"), + translate(user_id, "errors.no_public_key"), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return - message = t(user_id, "peers.confirm_delete") + "\n\n" - message += t(user_id, "peers.confirm_delete_message") + "\n\n" - message += t(user_id, "peers.config", config=config_name) + "\n" - message += f"*{t(user_id, 'schedule.peer_label')}* {peer_name}\n" - message += t(user_id, "peers.public_key", key=peer_key[:30]) + "\n\n" - message += t(user_id, "peers.irreversible") + message = translate(user_id, "peers.confirm_delete") + "\n\n" + message += translate(user_id, "peers.confirm_delete_message") + "\n\n" + message += translate(user_id, "peers.config", config=config_name) + "\n" + message += f"*{translate(user_id, 'schedule.peer_label')}* {peer_name}\n" + message += translate(user_id, "peers.public_key", key=peer_key[:30]) + "\n\n" + message += translate(user_id, "peers.irreversible") # CORREGIDO: Añadir el cuarto parámetro action_text await query.edit_message_text( message, - reply_markup=confirmation_menu(config_name, peer_index, "delete_peer", t(user_id, "actions.delete"), user_id=user_id), + reply_markup=confirmation_menu(config_name, peer_index, "delete_peer", translate(user_id, "actions.delete"), user_id=user_id), parse_mode="Markdown" ) except ValueError as e: user_id = query.from_user.id await query.edit_message_text( - t(user_id, "errors.invalid_index") + f"\n{str(e)}", + translate(user_id, "errors.invalid_index") + f"\n{str(e)}", reply_markup=back_button(f"cfg:{config_name}", user_id) ) @@ -2021,7 +2021,7 @@ async def handle_delete_peer_final(query, config_name: str, peer_index: str): result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -2030,7 +2030,7 @@ async def handle_delete_peer_final(query, config_name: str, peer_index: str): if idx < 0 or idx >= len(peers): await query.edit_message_text( - t(user_id, "errors.invalid_index"), + translate(user_id, "errors.invalid_index"), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return @@ -2040,30 +2040,30 @@ async def handle_delete_peer_final(query, config_name: str, peer_index: str): if not peer_key: await query.edit_message_text( - t(user_id, "errors.no_public_key"), + translate(user_id, "errors.no_public_key"), reply_markup=back_button(f"cfg:{config_name}", user_id) ) return - await query.edit_message_text(t(user_id, "peers.deleting")) + await query.edit_message_text(translate(user_id, "peers.deleting")) result = api_client.delete_peer(config_name, peer_key) if result.get("status"): await query.edit_message_text( - t(user_id, "success.peer_deleted", config_name=config_name), + translate(user_id, "success.peer_deleted", config_name=config_name), reply_markup=back_button(f"cfg:{config_name}", user_id) ) else: await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=back_button(f"cfg:{config_name}", user_id) ) except ValueError as e: user_id = query.from_user.id await query.edit_message_text( - t(user_id, "errors.invalid_index") + f"\n{str(e)}", + translate(user_id, "errors.invalid_index") + f"\n{str(e)}", reply_markup=back_button(f"cfg:{config_name}", user_id) ) @@ -2082,21 +2082,21 @@ async def handle_add_peer(query, context: CallbackContext, config_name: str): minutes = int((remaining.total_seconds() % 3600) // 60) if hours > 0: - time_msg = f"{hours} {t(user_id, 'time.hours')} {t(user_id, 'time.and')} {minutes} {t(user_id, 'time.minutes')}" + time_msg = f"{hours} {translate(user_id, 'time.hours')} {translate(user_id, 'time.and')} {minutes} {translate(user_id, 'time.minutes')}" else: - time_msg = f"{minutes} {t(user_id, 'time.minutes')}" + time_msg = f"{minutes} {translate(user_id, 'time.minutes')}" await query.edit_message_text( - f"⏰ {t(user_id, 'operator.limit_reached')}\n\n" + f"⏰ {translate(user_id, 'operator.limit_reached')}\n\n" f"{error_msg}\n\n" - f"⏳ {t(user_id, 'operator.time_remaining')}: *{time_msg}*\n\n" - f"{t(user_id, 'operator.create_peer_after_time')}", + f"⏳ {translate(user_id, 'operator.time_remaining')}: *{time_msg}*\n\n" + f"{translate(user_id, 'operator.create_peer_after_time')}", reply_markup=operator_main_menu(user_id), parse_mode="Markdown" ) else: await query.edit_message_text( - f"❌ {t(user_id, 'operator.cannot_create_more_peers')}\n\n" + f"❌ {translate(user_id, 'operator.cannot_create_more_peers')}\n\n" f"{error_msg}", reply_markup=operator_main_menu(user_id), parse_mode="Markdown" @@ -2127,24 +2127,24 @@ async def handle_add_peer(query, context: CallbackContext, config_name: str): address = config_data.get('Address', '10.21.0.0/24') listen_port = config_data.get('ListenPort', 'N/A') - add_peer_title = t(user_id, "peer_creation.add_peer", config=config_name) - current_config = t(user_id, "peer_creation.current_config") - network = t(user_id, "peer_creation.network", address=address) - port_label = t(user_id, "peer_creation.port_label", port=listen_port) + add_peer_title = translate(user_id, "peer_creation.add_peer", config=config_name) + current_config = translate(user_id, "peer_creation.current_config") + network = translate(user_id, "peer_creation.network", address=address) + port_label = translate(user_id, "peer_creation.port_label", port=listen_port) message = f"{add_peer_title}\n\n" message += f"{current_config}\n" message += f"{network}\n" message += f"{port_label}\n\n" else: - add_peer_title = t(user_id, "peer_creation.add_peer", config=config_name) + add_peer_title = translate(user_id, "peer_creation.add_peer", config=config_name) message = f"{add_peer_title}\n\n" - send_name = t(user_id, "peer_creation.send_name") - requirements = t(user_id, "peer_creation.requirements") - req_chars = t(user_id, "peer_creation.req_chars") - req_length = t(user_id, "peer_creation.req_length") - req_example = t(user_id, "peer_creation.req_example") - send_now = t(user_id, "peer_creation.send_now_or_cancel") + send_name = translate(user_id, "peer_creation.send_name") + requirements = translate(user_id, "peer_creation.requirements") + req_chars = translate(user_id, "peer_creation.req_chars") + req_length = translate(user_id, "peer_creation.req_length") + req_example = translate(user_id, "peer_creation.req_example") + send_now = translate(user_id, "peer_creation.send_now_or_cancel") message += f"{send_name}\n\n" message += f"{requirements}\n" message += f"{req_chars}\n" @@ -2153,9 +2153,9 @@ async def handle_add_peer(query, context: CallbackContext, config_name: str): message += send_now if is_operator(user_id): - keyboard = [[InlineKeyboardButton(t(user_id, "actions.cancel"), callback_data="operator_main_menu")]] + keyboard = [[InlineKeyboardButton(translate(user_id, "actions.cancel"), callback_data="operator_main_menu")]] else: - keyboard = [[InlineKeyboardButton(t(user_id, "actions.cancel"), callback_data=f"cfg:{config_name}")]] + keyboard = [[InlineKeyboardButton(translate(user_id, "actions.cancel"), callback_data=f"cfg:{config_name}")]] await query.edit_message_text( message, @@ -2167,7 +2167,7 @@ async def handle_download_peer_config(query, context: CallbackContext, peer_hash """Descarga la configuración de un peer usando hash""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "operator.downloading")) + await query.edit_message_text(translate(user_id, "operator.downloading")) try: # Obtener datos del peer desde el contexto @@ -2189,7 +2189,7 @@ async def handle_download_peer_config(query, context: CallbackContext, peer_hash if not peer_data: await query.edit_message_text( - t(user_id, "errors.no_peer_info"), + translate(user_id, "errors.no_peer_info"), reply_markup=operator_main_menu(user_id) if is_operator(user_id) else main_menu(is_admin(user_id), is_operator(user_id), user_id) ) return @@ -2208,17 +2208,17 @@ async def handle_download_peer_config(query, context: CallbackContext, peer_hash return # Obtener información del servidor (usar endpoint personalizado si existe) - from config import WG_API_BASE_URL + from config import WGD_API_BASE_URL import urllib.parse - parsed_url = urllib.parse.urlparse(WG_API_BASE_URL) + parsed_url = urllib.parse.urlparse(WGD_API_BASE_URL) server_host = parsed_url.hostname # Obtener información de la configuración config_result = api_client.get_configuration_detail(config_name) if not config_result.get("status"): await query.edit_message_text( - t(user_id, "errors.config_info_error"), + translate(user_id, "errors.config_info_error"), reply_markup=operator_main_menu(user_id) if is_operator(user_id) else back_button(f"cfg:{config_name}", user_id) ) return @@ -2229,7 +2229,7 @@ async def handle_download_peer_config(query, context: CallbackContext, peer_hash if not server_public_key: await query.edit_message_text( - t(user_id, "errors.server_key_error"), + translate(user_id, "errors.server_key_error"), reply_markup=operator_main_menu(user_id) if is_operator(user_id) else back_button(f"cfg:{config_name}", user_id) ) return @@ -2269,30 +2269,30 @@ async def handle_download_peer_config(query, context: CallbackContext, peer_hash # Primero, enviar el archivo await query.message.reply_document( document=InputFile(file_like, filename=filename), - caption=t(user_id, "operator.download_caption", peer_name=peer_name, config_name=config_name) + caption=translate(user_id, "operator.download_caption", peer_name=peer_name, config_name=config_name) ) # Luego actualizar el mensaje original if is_operator(user_id): # Para operadores: botón para volver al menú keyboard = [ - [InlineKeyboardButton(t(user_id, "actions.back_to_menu"), callback_data="operator_main_menu")], - [InlineKeyboardButton(t(user_id, "actions.download_again"), callback_data=f"download_config:{peer_hash}")] + [InlineKeyboardButton(translate(user_id, "actions.back_to_menu"), callback_data="operator_main_menu")], + [InlineKeyboardButton(translate(user_id, "actions.download_again"), callback_data=f"download_config:{peer_hash}")] ] await query.edit_message_text( - t(user_id, "operator.download_success", filename=filename, host=endpoint_host, port=endpoint_port, ip=allowed_ip), + translate(user_id, "operator.download_success", filename=filename, host=endpoint_host, port=endpoint_port, ip=allowed_ip), reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown" ) else: # Para admins: botones normales await query.edit_message_text( - t(user_id, "operator.download_success_admin", filename=filename, host=endpoint_host, port=endpoint_port, ip=allowed_ip), + translate(user_id, "operator.download_success_admin", filename=filename, host=endpoint_host, port=endpoint_port, ip=allowed_ip), reply_markup=InlineKeyboardMarkup([ [ - InlineKeyboardButton(t(user_id, "actions.back_to_config"), callback_data=f"cfg:{config_name}"), - InlineKeyboardButton(t(user_id, "actions.download_again"), callback_data=f"download_config:{peer_hash}") + InlineKeyboardButton(translate(user_id, "actions.back_to_config"), callback_data=f"cfg:{config_name}"), + InlineKeyboardButton(translate(user_id, "actions.download_again"), callback_data=f"download_config:{peer_hash}") ] ]), parse_mode="Markdown" @@ -2301,7 +2301,7 @@ async def handle_download_peer_config(query, context: CallbackContext, peer_hash except Exception as e: logger.error(f"Error al descargar configuración: {str(e)}") await query.edit_message_text( - t(user_id, "operator.download_error", message=str(e)), + translate(user_id, "operator.download_error", message=str(e)), reply_markup=operator_main_menu(user_id) if is_operator(user_id) else back_button("main_menu", user_id), parse_mode="Markdown" ) @@ -2309,12 +2309,12 @@ async def handle_download_peer_config(query, context: CallbackContext, peer_hash async def handle_schedule_jobs_menu(query, context: CallbackContext, config_name: str): """Muestra el menú inicial de Schedule Jobs con lista de peers""" user_id = query.from_user.id - getting_peers_text = t(user_id, "schedule.getting_peers", config=config_name) + getting_peers_text = translate(user_id, "schedule.getting_peers", config=config_name) await query.edit_message_text(getting_peers_text) result = api_client.get_peers(config_name) if not result.get("status"): - error_msg = t(user_id, "errors.api_error", message=result.get('message')) + error_msg = translate(user_id, "errors.api_error", message=result.get('message')) await query.edit_message_text( error_msg, reply_markup=back_button(f"cfg:{config_name}", user_id) @@ -2324,7 +2324,7 @@ async def handle_schedule_jobs_menu(query, context: CallbackContext, config_name peers = result.get("data", []) if not peers: - no_peers_text = t(user_id, "errors.no_peers", config=config_name) + no_peers_text = translate(user_id, "errors.no_peers", config=config_name) await query.edit_message_text( no_peers_text, reply_markup=back_button(f"cfg:{config_name}", user_id) @@ -2343,14 +2343,14 @@ async def handle_schedule_jobs_menu(query, context: CallbackContext, config_name ]) # Botones de navegación - back_text = t(user_id, "menu.back") + back_text = translate(user_id, "menu.back") keyboard.append([ InlineKeyboardButton(back_text, callback_data=f"cfg:{config_name}") ]) - title = t(user_id, "schedule.title", config=config_name) - total_peers = t(user_id, "schedule.total_peers", count=len(peers)) - select_peer = t(user_id, "schedule.select_peer") + title = translate(user_id, "schedule.title", config=config_name) + total_peers = translate(user_id, "schedule.total_peers", count=len(peers)) + select_peer = translate(user_id, "schedule.select_peer") message = f"{title}\n\n" message += f"{total_peers}\n\n" @@ -2365,12 +2365,12 @@ async def handle_schedule_jobs_menu(query, context: CallbackContext, config_name async def handle_schedule_job_peer_selected(query, context: CallbackContext, config_name: str, peer_index: int): """Muestra el menú de Schedule Jobs para un peer específico""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "schedule.getting_peer_info")) + await query.edit_message_text(translate(user_id, "schedule.getting_peer_info")) # Obtener información del peer usando el índice result = api_client.get_peers(config_name) if not result.get("status"): - error_msg = t(user_id, "errors.api_error", message=result.get('message')) + error_msg = translate(user_id, "errors.api_error", message=result.get('message')) await query.edit_message_text( error_msg, reply_markup=back_button(f"schedule_jobs_menu:{config_name}", user_id) @@ -2381,7 +2381,7 @@ async def handle_schedule_job_peer_selected(query, context: CallbackContext, con if peer_index < 0 or peer_index >= len(peers): await query.edit_message_text( - t(user_id, "errors.invalid_index"), + translate(user_id, "errors.invalid_index"), reply_markup=back_button(f"schedule_jobs_menu:{config_name}", user_id) ) return @@ -2401,31 +2401,31 @@ async def handle_schedule_job_peer_selected(query, context: CallbackContext, con peer_name_html = html_escape(peer_name) config_name_html = html_escape(config_name) - title_text = t(user_id, "schedule.for_peer", peer=peer_name_html) + title_text = translate(user_id, "schedule.for_peer", peer=peer_name_html) message = f"{html_escape(title_text)}\n\n" - message += f"{html_escape(t(user_id, 'schedule.config_label'))}: {config_name_html}\n" - message += f"{html_escape(t(user_id, 'schedule.peer_label'))}: {peer_name_html}\n\n" + message += f"{html_escape(translate(user_id, 'schedule.config_label'))}: {config_name_html}\n" + message += f"{html_escape(translate(user_id, 'schedule.peer_label'))}: {peer_name_html}\n\n" if jobs: - active_jobs_text = t(user_id, "schedule.active_jobs", count=len(jobs)) + active_jobs_text = translate(user_id, "schedule.active_jobs", count=len(jobs)) message += f"{html_escape(active_jobs_text)}\n\n" for i, job in enumerate(jobs, 1): job_text = format_schedule_job_for_list(job, user_id) message += f"{i}. {html_escape(job_text)}\n" message += "\n" else: - no_jobs_text = t(user_id, "schedule.no_jobs") + no_jobs_text = translate(user_id, "schedule.no_jobs") message += f"{html_escape(no_jobs_text)}\n" - add_new_text = t(user_id, "schedule.add_new") + add_new_text = translate(user_id, "schedule.add_new") message += f"{html_escape(add_new_text)}" # Crear teclado keyboard = [] # Botones para agregar jobs - data_limit_text = t(user_id, "schedule.data_limit_button") - expiry_date_text = t(user_id, "schedule.expiry_date_button") + data_limit_text = translate(user_id, "schedule.data_limit_button") + expiry_date_text = translate(user_id, "schedule.expiry_date_button") keyboard.append([ InlineKeyboardButton(data_limit_text, callback_data=f"add_schedule_job_data:{config_name}:{peer_index}"), InlineKeyboardButton(expiry_date_text, callback_data=f"add_schedule_job_date:{config_name}:{peer_index}") @@ -2433,15 +2433,15 @@ async def handle_schedule_job_peer_selected(query, context: CallbackContext, con # Si hay jobs, mostrar botones para eliminarlos if jobs: - delete_job_text = t(user_id, "schedule.delete_job") + delete_job_text = translate(user_id, "schedule.delete_job") keyboard.append([ InlineKeyboardButton(delete_job_text, callback_data=f"delete_schedule_job_all:{config_name}:{peer_index}") ]) # Botones de navegación keyboard.append([ - InlineKeyboardButton(t(user_id, "schedule.back_list"), callback_data=f"schedule_jobs_menu:{config_name}"), - InlineKeyboardButton(t(user_id, "schedule.update"), callback_data=f"schedule_job_peer:{config_name}:{peer_index}") + InlineKeyboardButton(translate(user_id, "schedule.back_list"), callback_data=f"schedule_jobs_menu:{config_name}"), + InlineKeyboardButton(translate(user_id, "schedule.update"), callback_data=f"schedule_job_peer:{config_name}:{peer_index}") ]) await query.edit_message_text( @@ -2461,7 +2461,7 @@ async def handle_add_schedule_job_data(query, context: CallbackContext, config_n result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - f"❌ {t(user_id, 'errors.get_peer_info_error', message=result.get('message'))}", + f"❌ {translate(user_id, 'errors.get_peer_info_error', message=result.get('message'))}", reply_markup=back_button(f"schedule_job_peer:{config_name}:{idx}", user_id) ) return @@ -2469,7 +2469,7 @@ async def handle_add_schedule_job_data(query, context: CallbackContext, config_n peers = result.get("data", []) if idx < 0 or idx >= len(peers): await query.edit_message_text( - f"❌ {t(user_id, 'errors.invalid_index')}", + f"❌ {translate(user_id, 'errors.invalid_index')}", reply_markup=back_button(f"schedule_job_peer:{config_name}:{idx}", user_id) ) return @@ -2492,16 +2492,16 @@ async def handle_add_schedule_job_data(query, context: CallbackContext, config_n # Usar HTML para evitar problemas de parsing peer_name_html = html_escape(peer_name) - message = f"⏰ {html_escape(t(user_id, 'schedule.add_data_title', peer=peer_name_html))}\n\n" - message += html_escape(t(user_id, "schedule.add_data_msg")) + "\n\n" - message += html_escape(t(user_id, "schedule.add_data_example")) + "\n\n" - message += html_escape(t(user_id, "schedule.add_data_auto")) + "\n\n" - message += html_escape(t(user_id, "schedule.send_cancel")) + message = f"⏰ {html_escape(translate(user_id, 'schedule.add_data_title', peer=peer_name_html))}\n\n" + message += html_escape(translate(user_id, "schedule.add_data_msg")) + "\n\n" + message += html_escape(translate(user_id, "schedule.add_data_example")) + "\n\n" + message += html_escape(translate(user_id, "schedule.add_data_auto")) + "\n\n" + message += html_escape(translate(user_id, "schedule.send_cancel")) # Guardar en el contexto que estamos esperando el valor context.user_data['waiting_for_schedule_job_value'] = True - keyboard = [[InlineKeyboardButton(t(user_id, "schedule.cancel_button"), callback_data=f"schedule_job_peer:{config_name}:{idx}")]] + keyboard = [[InlineKeyboardButton(translate(user_id, "schedule.cancel_button"), callback_data=f"schedule_job_peer:{config_name}:{idx}")]] try: await query.edit_message_text( @@ -2527,7 +2527,7 @@ async def handle_add_schedule_job_date(query, context: CallbackContext, config_n result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - f"❌ {t(user_id, 'errors.get_peer_info_error', message=result.get('message'))}", + f"❌ {translate(user_id, 'errors.get_peer_info_error', message=result.get('message'))}", reply_markup=back_button(f"schedule_job_peer:{config_name}:{idx}", user_id) ) return @@ -2535,7 +2535,7 @@ async def handle_add_schedule_job_date(query, context: CallbackContext, config_n peers = result.get("data", []) if idx < 0 or idx >= len(peers): await query.edit_message_text( - f"❌ {t(user_id, 'errors.invalid_index')}", + f"❌ {translate(user_id, 'errors.invalid_index')}", reply_markup=back_button(f"schedule_job_peer:{config_name}:{idx}", user_id) ) return @@ -2558,16 +2558,16 @@ async def handle_add_schedule_job_date(query, context: CallbackContext, config_n # Usar HTML para evitar problemas de parsing peer_name_html = html_escape(peer_name) - message = f"⏰ {html_escape(t(user_id, 'schedule.add_date_title', peer=peer_name_html))}\n\n" - message += html_escape(t(user_id, "schedule.add_date_msg")) + "\n\n" - message += html_escape(t(user_id, "schedule.add_date_format")) + "\n\n" - message += html_escape(t(user_id, "schedule.add_date_auto")) + "\n\n" - message += html_escape(t(user_id, "schedule.send_cancel")) + message = f"⏰ {html_escape(translate(user_id, 'schedule.add_date_title', peer=peer_name_html))}\n\n" + message += html_escape(translate(user_id, "schedule.add_date_msg")) + "\n\n" + message += html_escape(translate(user_id, "schedule.add_date_format")) + "\n\n" + message += html_escape(translate(user_id, "schedule.add_date_auto")) + "\n\n" + message += html_escape(translate(user_id, "schedule.send_cancel")) # Guardar en el contexto que estamos esperando el valor context.user_data['waiting_for_schedule_job_value'] = True - keyboard = [[InlineKeyboardButton(t(user_id, "schedule.cancel_button"), callback_data=f"schedule_job_peer:{config_name}:{idx}")]] + keyboard = [[InlineKeyboardButton(translate(user_id, "schedule.cancel_button"), callback_data=f"schedule_job_peer:{config_name}:{idx}")]] try: await query.edit_message_text( @@ -2585,13 +2585,13 @@ async def handle_add_schedule_job_date(query, context: CallbackContext, config_n async def handle_schedule_jobs_list(query, context: CallbackContext, config_name: str, peer_index: str, page: int = 0): """Muestra la lista paginada de Schedule Jobs""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "schedule.getting_peer_jobs")) + await query.edit_message_text(translate(user_id, "schedule.getting_peer_jobs")) # Obtener información del peer result = api_client.get_peers(config_name) if not result.get("status"): await query.edit_message_text( - f"❌ {t(user_id, 'errors.get_peer_info_error', message=result.get('message'))}", + f"❌ {translate(user_id, 'errors.get_peer_info_error', message=result.get('message'))}", reply_markup=back_button(f"schedule_jobs_menu:{config_name}", user_id) ) return @@ -2601,7 +2601,7 @@ async def handle_schedule_jobs_list(query, context: CallbackContext, config_name idx = int(peer_index) if idx < 0 or idx >= len(peers): await query.edit_message_text( - f"❌ {t(user_id, 'errors.invalid_index')}", + f"❌ {translate(user_id, 'errors.invalid_index')}", reply_markup=back_button(f"schedule_jobs_menu:{config_name}", user_id) ) return @@ -2620,12 +2620,12 @@ async def handle_schedule_jobs_list(query, context: CallbackContext, config_name job_text = format_schedule_job_for_list(job, user_id) # Información del job - keyboard.append([InlineKeyboardButton(f"{t(user_id, 'peers.peer_number', number=i+1)}. {job_text}", callback_data="noop")]) + keyboard.append([InlineKeyboardButton(f"{translate(user_id, 'peers.peer_number', number=i+1)}. {job_text}", callback_data="noop")]) # Botón para eliminar este job - usando el callback_data correcto keyboard.append([ InlineKeyboardButton( - t(user_id, "actions.delete_this_job"), + translate(user_id, "actions.delete_this_job"), callback_data=f"delete_schedule_job_confirm:{config_name}:{idx}:{i}" ) ]) @@ -2635,12 +2635,12 @@ async def handle_schedule_jobs_list(query, context: CallbackContext, config_name if page > 0: nav_buttons.append( - InlineKeyboardButton(t(user_id, "menu.previous"), callback_data=f"page_schedule_jobs:{config_name}:{idx}:{page-1}") + InlineKeyboardButton(translate(user_id, "menu.previous"), callback_data=f"page_schedule_jobs:{config_name}:{idx}:{page-1}") ) if end_idx < len(jobs): nav_buttons.append( - InlineKeyboardButton(t(user_id, "menu.next"), callback_data=f"page_schedule_jobs:{config_name}:{idx}:{page+1}") + InlineKeyboardButton(translate(user_id, "menu.next"), callback_data=f"page_schedule_jobs:{config_name}:{idx}:{page+1}") ) if nav_buttons: @@ -2648,11 +2648,11 @@ async def handle_schedule_jobs_list(query, context: CallbackContext, config_name # Botones de acción keyboard.append([ - InlineKeyboardButton(t(user_id, "schedule.add_new_button"), callback_data=f"add_schedule_job_data:{config_name}:{idx}"), - InlineKeyboardButton(t(user_id, "schedule.delete_one_button"), callback_data=f"delete_schedule_job_all:{config_name}:{idx}") + InlineKeyboardButton(translate(user_id, "schedule.add_new_button"), callback_data=f"add_schedule_job_data:{config_name}:{idx}"), + InlineKeyboardButton(translate(user_id, "schedule.delete_one_button"), callback_data=f"delete_schedule_job_all:{config_name}:{idx}") ]) keyboard.append([ - InlineKeyboardButton(t(user_id, "menu.back"), callback_data=f"schedule_job_peer:{config_name}:{idx}") + InlineKeyboardButton(translate(user_id, "menu.back"), callback_data=f"schedule_job_peer:{config_name}:{idx}") ]) total_jobs = len(jobs) @@ -2677,7 +2677,7 @@ async def handle_delete_schedule_job_confirm(query, context: CallbackContext, co idx = int(peer_index) # Obtener información actualizada del peer - await query.edit_message_text(t(user_id, "schedule.getting_jobs")) + await query.edit_message_text(translate(user_id, "schedule.getting_jobs")) result = api_client.get_peers(config_name) if not result.get("status"): @@ -2722,11 +2722,11 @@ async def handle_delete_schedule_job_confirm(query, context: CallbackContext, co ]) keyboard.append([ - InlineKeyboardButton(t(user_id, "schedule.back_list"), callback_data=f"schedule_job_peer:{config_name}:{idx}") + InlineKeyboardButton(translate(user_id, "schedule.back_list"), callback_data=f"schedule_job_peer:{config_name}:{idx}") ]) - message = t(user_id, "schedule.delete_confirm_title", peer=peer_name) + "\n\n" - message += t(user_id, "schedule.delete_select") + "\n\n" + message = translate(user_id, "schedule.delete_confirm_title", peer=peer_name) + "\n\n" + message += translate(user_id, "schedule.delete_select") + "\n\n" await query.edit_message_text( message, @@ -2760,25 +2760,25 @@ async def handle_delete_schedule_job_confirm(query, context: CallbackContext, co value = job.get('Value', 'N/A') if field == "total_data": - field_display = t(user_id, "schedule.data_limit_label", value=value) + field_display = translate(user_id, "schedule.data_limit_label", value=value) elif field == "date": - field_display = t(user_id, "schedule.expiry_date_label", value=value) + field_display = translate(user_id, "schedule.expiry_date_label", value=value) else: field_display = f"{field}: {value}" - message = f"{t(user_id, 'schedule.delete_confirm_single')}\n\n" - message += f"{t(user_id, 'schedule.delete_confirm_msg')}\n\n" - message += f"*{t(user_id, 'schedule.peer_label')}* {peer_name}\n" - message += f"*{t(user_id, 'schedule.config_label')}* {config_name}\n" - message += f"*{t(user_id, 'schedule.action_label')}* {action.upper()}\n" + message = f"{translate(user_id, 'schedule.delete_confirm_single')}\n\n" + message += f"{translate(user_id, 'schedule.delete_confirm_msg')}\n\n" + message += f"*{translate(user_id, 'schedule.peer_label')}* {peer_name}\n" + message += f"*{translate(user_id, 'schedule.config_label')}* {config_name}\n" + message += f"*{translate(user_id, 'schedule.action_label')}* {action.upper()}\n" message += f"*{field_display}*\n\n" - message += t(user_id, "schedule.irreversible") + message += translate(user_id, "schedule.irreversible") # CORREGIDO: Usar el formato simplificado en lugar de confirmation_menu keyboard = [ [ - InlineKeyboardButton(t(user_id, "schedule.yes_delete"), callback_data=f"delete_schedule_job_execute:{config_name}:{idx}:{job_idx}"), - InlineKeyboardButton(t(user_id, "schedule.no_cancel"), callback_data=f"schedule_job_peer:{config_name}:{idx}") + InlineKeyboardButton(translate(user_id, "schedule.yes_delete"), callback_data=f"delete_schedule_job_execute:{config_name}:{idx}:{job_idx}"), + InlineKeyboardButton(translate(user_id, "schedule.no_cancel"), callback_data=f"schedule_job_peer:{config_name}:{idx}") ] ] @@ -2803,7 +2803,7 @@ async def handle_delete_schedule_job_execute(query, context: CallbackContext, co idx = int(peer_index) job_idx = int(job_index) - await query.edit_message_text(t(user_id, "schedule.deleting")) + await query.edit_message_text(translate(user_id, "schedule.deleting")) # Obtener información actualizada del peer result = api_client.get_peers(config_name) @@ -2857,10 +2857,10 @@ async def handle_delete_schedule_job_execute(query, context: CallbackContext, co config_name_escaped = config_name await query.edit_message_text( - f"{t(user_id, 'schedule.delete_success_title')}\n\n" - f"*{t(user_id, 'schedule.peer_label')}* {peer_name_escaped}\n" - f"*{t(user_id, 'schedule.job_label')}* {job_info_escaped}\n\n" - f"{t(user_id, 'schedule.delete_permanent')}", + f"{translate(user_id, 'schedule.delete_success_title')}\n\n" + f"*{translate(user_id, 'schedule.peer_label')}* {peer_name_escaped}\n" + f"*{translate(user_id, 'schedule.job_label')}* {job_info_escaped}\n\n" + f"{translate(user_id, 'schedule.delete_permanent')}", reply_markup=back_button(f"schedule_job_peer:{config_name}:{idx}", user_id), parse_mode="Markdown" # Markdown format ) @@ -2870,20 +2870,20 @@ async def handle_delete_schedule_job_execute(query, context: CallbackContext, co # Ofrecer opciones alternativas keyboard = [ [ - InlineKeyboardButton(t(user_id, "actions.try_again"), callback_data=f"delete_schedule_job_execute:{config_name}:{idx}:{job_idx}"), - InlineKeyboardButton(t(user_id, "actions.view_list"), callback_data=f"schedule_job_peer:{config_name}:{idx}") + InlineKeyboardButton(translate(user_id, "actions.try_again"), callback_data=f"delete_schedule_job_execute:{config_name}:{idx}:{job_idx}"), + InlineKeyboardButton(translate(user_id, "actions.view_list"), callback_data=f"schedule_job_peer:{config_name}:{idx}") ], [ - InlineKeyboardButton(t(user_id, "menu.help"), callback_data="help"), - InlineKeyboardButton(t(user_id, "menu.main"), callback_data="main_menu") + InlineKeyboardButton(translate(user_id, "menu.help"), callback_data="help"), + InlineKeyboardButton(translate(user_id, "menu.main"), callback_data="main_menu") ] ] await query.edit_message_text( - f"❌ *{t(user_id, 'schedule.delete_error_title')}*\n\n" - f"*{t(user_id, 'schedule.job_label')}* {job_info_escaped}\n" + f"❌ *{translate(user_id, 'schedule.delete_error_title')}*\n\n" + f"*{translate(user_id, 'schedule.job_label')}* {job_info_escaped}\n" f"*Error:* {error_msg}\n\n" - f"{t(user_id, 'schedule.solutions')}", + f"{translate(user_id, 'schedule.solutions')}", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown" # Markdown format ) @@ -2892,7 +2892,7 @@ async def handle_delete_schedule_job_execute(query, context: CallbackContext, co logger.error(f"Error al eliminar schedule job: {str(e)}", exc_info=True) error_msg = str(e) await query.edit_message_text( - f"❌ *{t(user_id, 'schedule.delete_error_title')}*\n\n" + f"❌ *{translate(user_id, 'schedule.delete_error_title')}*\n\n" f"*Error:* {error_msg}\n\n", reply_markup=back_button(f"schedule_job_peer:{config_name}:{peer_index}", user_id), parse_mode="Markdown" # Markdown format @@ -2901,13 +2901,13 @@ async def handle_delete_schedule_job_execute(query, context: CallbackContext, co async def handle_system_status(query): """Muestra el estado del sistema""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "system.getting_stats")) + await query.edit_message_text(translate(user_id, "system.getting_stats")) result = api_client.get_system_status() if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=refresh_button("system_status", user_id) ) return @@ -2924,13 +2924,13 @@ async def handle_system_status(query): async def handle_protocols(query): """Muestra los protocolos habilitados""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "protocols.getting")) + await query.edit_message_text(translate(user_id, "protocols.getting")) result = api_client.get_protocols() if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')), reply_markup=refresh_button("protocols", user_id) ) return @@ -2939,21 +2939,21 @@ async def handle_protocols(query): if not protocols: await query.edit_message_text( - t(user_id, "errors.no_configs"), + translate(user_id, "errors.no_configs"), reply_markup=refresh_button("protocols", user_id) ) return - message = t(user_id, "protocols.title") + "\n\n" + message = translate(user_id, "protocols.title") + "\n\n" for protocol in protocols: if protocol in ["wg", "awg"]: - message += t(user_id, f"protocols.{protocol}") + "\n" + message += translate(user_id, f"protocols.{protocol}") + "\n" else: emoji = "✅" if protocol in ["wg", "awg"] else "⚙️" message += f"{emoji} {protocol.upper()}\n" - message += "\n" + t(user_id, "protocols.notes") + message += "\n" + translate(user_id, "protocols.notes") await query.edit_message_text( message, @@ -2964,14 +2964,14 @@ async def handle_protocols(query): async def handle_stats(query): """Muestra estadísticas específicas de WireGuard sin datos de transferencia""" user_id = query.from_user.id - await query.edit_message_text(t(user_id, "system.getting_stats")) + await query.edit_message_text(translate(user_id, "system.getting_stats")) # Obtener todas las configuraciones para calcular estadísticas configs_result = api_client.get_configurations() if not configs_result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=configs_result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=configs_result.get('message', 'Error desconocido')), reply_markup=refresh_button("stats", user_id) ) return @@ -2980,13 +2980,13 @@ async def handle_stats(query): if not configs: await query.edit_message_text( - t(user_id, "errors.no_configs"), + translate(user_id, "errors.no_configs"), reply_markup=refresh_button("stats", user_id) ) return lines = [] - lines.append(t(user_id, "system.status") + "\n") + lines.append(translate(user_id, "system.status") + "\n") total_peers = 0 total_connected = 0 @@ -3001,18 +3001,18 @@ async def handle_stats(query): total_connected += config_connected # Estadísticas generales - lines.append(t(user_id, "system.total_configs", count=len(configs))) - lines.append(t(user_id, "system.total_peers", count=total_peers)) - lines.append(t(user_id, "system.connected_peers", count=total_connected)) + lines.append(translate(user_id, "system.total_configs", count=len(configs))) + lines.append(translate(user_id, "system.total_peers", count=total_peers)) + lines.append(translate(user_id, "system.connected_peers", count=total_connected)) if total_peers > 0: connection_rate = (total_connected / total_peers) * 100 - lines.append(t(user_id, "system.connection_rate", percent=connection_rate) + "\n") + lines.append(translate(user_id, "system.connection_rate", percent=connection_rate) + "\n") else: lines.append("\n") # Configuraciones individuales (mostrar solo las principales) - lines.append(t(user_id, "system.active_configs")) + lines.append(translate(user_id, "system.active_configs")) # Ordenar configuraciones por número de peers conectados (más activas primero) sorted_configs = sorted(configs, key=lambda x: x.get('ConnectedPeers', 0), reverse=True) @@ -3024,15 +3024,15 @@ async def handle_stats(query): listen_port = config.get('ListenPort', 'N/A') status_emoji = "✅" if connected > 0 else "⚠️" - lines.append(f" {status_emoji} **{name}** ({t(user_id, 'system.port', port=listen_port)})") - lines.append(f" 👥 {t(user_id, 'system.connected', connected=connected, peers=peers)}") + lines.append(f" {status_emoji} **{name}** ({translate(user_id, 'system.port', port=listen_port)})") + lines.append(f" 👥 {ranslatet(user_id, 'system.connected', connected=connected, peers=peers)}") if len(configs) > 5: - lines.append(t(user_id, "system.and_more", count=len(configs) - 5)) + lines.append(translate(user_id, "system.and_more", count=len(configs) - 5)) # Agregar timestamp de la última actualización from datetime import datetime - lines.append(t(user_id, "system.last_update", date=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) + lines.append(translate(user_id, "system.last_update", date=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) formatted_text = "\n".join(lines) @@ -3047,7 +3047,7 @@ async def handle_help(query): user_id = query.from_user.id if is_operator(user_id): - help_text = t(user_id, "help.operator") + help_text = translate(user_id, "help.operator") await query.edit_message_text( help_text, @@ -3055,7 +3055,7 @@ async def handle_help(query): parse_mode="Markdown" ) else: - help_text = t(user_id, "help.admin") + help_text = translate(user_id, "help.admin") await query.edit_message_text( help_text, @@ -3069,13 +3069,13 @@ async def handle_operators_list(query, context: CallbackContext): if not is_admin(user_id): await query.edit_message_text( - t(user_id, "errors.access_denied"), + translate(user_id, "errors.access_denied"), reply_markup=main_menu(is_admin(user_id), is_operator(user_id), user_id), parse_mode="Markdown" ) return - await query.edit_message_text(t(user_id, "operators.getting_info")) + await query.edit_message_text(translate(user_id, "operators.getting_info")) # Obtener información de todos los operadores operators_info = [] @@ -3086,14 +3086,14 @@ async def handle_operators_list(query, context: CallbackContext): if not operator_users: await query.edit_message_text( - t(user_id, "errors.no_operators"), + translate(user_id, "errors.no_operators"), reply_markup=refresh_button("operators_list", user_id), parse_mode="Markdown" ) return # Construir mensaje con información detallada - message_lines = [t(user_id, "operators.title") + "\n"] + message_lines = [translate(user_id, "operators.title") + "\n"] total_peers_created = 0 @@ -3105,7 +3105,7 @@ async def handle_operators_list(query, context: CallbackContext): match = re.search(r'(?:Operator|Operador)\s+(\d+)', user_name, re.IGNORECASE) if match: op_num = match.group(1) - user_name = t(user_id, "operators.operator_name", number=op_num) + user_name = translate(user_id, "operators.operator_name", number=op_num) user_name = escape_markdown(user_name) @@ -3120,7 +3120,7 @@ async def handle_operators_list(query, context: CallbackContext): # Formatear información del operador message_lines.append(f"\n**{user_name}**") message_lines.append(f" 👤 ID: `{uid}`") - message_lines.append(f" 📊 {t(user_id, 'operators.peers_created')}: {num_peers}") + message_lines.append(f" 📊 {ranslatet(user_id, 'operators.peers_created')}: {num_peers}") # Información del último peer creado if user_peers: @@ -3132,21 +3132,21 @@ async def handle_operators_list(query, context: CallbackContext): last_created = datetime.fromisoformat(last_peer.get('created_at', '')) last_created_str = last_created.strftime("%d/%m/%Y %H:%M") except: - last_created_str = t(user_id, "errors.unknown_date") + last_created_str = translate(user_id, "errors.unknown_date") peer_name = escape_markdown(last_peer.get('peer_name', '')) config_name = escape_markdown(last_peer.get('config_name', '')) - message_lines.append(f" {t(user_id, 'operators.last_peer', name=peer_name, date=last_created_str)}") - message_lines.append(f" {t(user_id, 'operators.config', name=config_name)}") + message_lines.append(f" {translate(user_id, 'operators.last_peer', name=peer_name, date=last_created_str)}") + message_lines.append(f" {translate(user_id, 'operators.config', name=config_name)}") # Mostrar endpoint si está disponible endpoint = last_peer.get('endpoint') if endpoint: - message_lines.append(f" 🌐 {t(user_id, 'operators.endpoint_label')}: `{endpoint}`") + message_lines.append(f" 🌐 {ranslatet(user_id, 'operators.endpoint_label')}: `{endpoint}`") # Estado de creación if can_create: - message_lines.append(f" {t(user_id, 'operators.can_create_now')}") + message_lines.append(f" {translate(user_id, 'operators.can_create_now')}") else: if next_allowed: now = datetime.now() @@ -3159,27 +3159,27 @@ async def handle_operators_list(query, context: CallbackContext): else: time_msg = f"{minutes}m" - message_lines.append(f" {t(user_id, 'operators.can_create_in', time=time_msg)}") + message_lines.append(f" {translate(user_id, 'operators.can_create_in', time=time_msg)}") else: - message_lines.append(f" {t(user_id, 'operators.cannot_create')}") + message_lines.append(f" {translate(user_id, 'operators.cannot_create')}") # Resumen general - message_lines.append(f"\n{t(user_id, 'operators.summary')}") - message_lines.append(t(user_id, "operators.total_operators", count=len(operator_users))) - message_lines.append(t(user_id, "operators.total_peers", count=total_peers_created)) + message_lines.append(f"\n{translate(user_id, 'operators.summary')}") + message_lines.append(translate(user_id, "operators.total_operators", count=len(operator_users))) + message_lines.append(translate(user_id, "operators.total_peers", count=total_peers_created)) # Usamos datetime ya importado al principio del archivo - message_lines.append(t(user_id, "operators.last_update", date=datetime.now().strftime('%d/%m/%Y %H:%M:%S'))) + message_lines.append(translate(user_id, "operators.last_update", date=datetime.now().strftime('%d/%m/%Y %H:%M:%S'))) message = "\n".join(message_lines) # Crear teclado con acciones adicionales keyboard = [ [ - InlineKeyboardButton(t(user_id, "menu.refresh"), callback_data="operators_list"), - InlineKeyboardButton(t(user_id, "operators.detailed_button"), callback_data="operators_detailed") + InlineKeyboardButton(translate(user_id, "menu.refresh"), callback_data="operators_list"), + InlineKeyboardButton(translate(user_id, "operators.detailed_button"), callback_data="operators_detailed") ], [ - InlineKeyboardButton(t(user_id, "menu.main"), callback_data="main_menu") + InlineKeyboardButton(translate(user_id, "menu.main"), callback_data="main_menu") ] ] @@ -3209,7 +3209,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP del context.user_data[key] await update.message.reply_text( - t(user_id, "input.creation_cancel"), + translate(user_id, "input.creation_cancel"), reply_markup=operator_main_menu() if is_operator(user_id) else main_menu(is_admin(user_id), is_operator(user_id)) ) return @@ -3223,7 +3223,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP del context.user_data[key] await update.message.reply_text( - t(user_id, "input.schedule_cancel"), + translate(user_id, "input.schedule_cancel"), reply_markup=operator_main_menu() if is_operator(user_id) else main_menu(is_admin(user_id), is_operator(user_id)) ) return @@ -3235,12 +3235,12 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP if is_operator(user_id): await update.message.reply_text( - t(user_id, "input.operation_cancel_user"), + translate(user_id, "input.operation_cancel_user"), reply_markup=operator_main_menu() ) else: await update.message.reply_text( - t(user_id, "input.operation_cancel_admin"), + translate(user_id, "input.operation_cancel_admin"), reply_markup=main_menu(is_admin(user_id), is_operator(user_id)) ) return @@ -3269,7 +3269,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP else: # Si no está en un flujo permitido, ignorar el mensaje await update.message.reply_text( - t(user_id, "errors.operators_only_menu"), + translate(user_id, "errors.operators_only_menu"), reply_markup=operator_main_menu(), parse_mode=None ) @@ -3283,7 +3283,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP if not all([config_name, peer_index, job_type]): await update.message.reply_text( - t(user_id, "errors.schedule_config_error"), + translate(user_id, "errors.schedule_config_error"), parse_mode=None ) return @@ -3294,7 +3294,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP peer_data = context.user_data.get(f'schedule_peer_{config_name}_{peer_index}') if not peer_data: await update.message.reply_text( - t(user_id, "errors.peer_not_found"), + translate(user_id, "errors.peer_not_found"), parse_mode=None ) return @@ -3307,13 +3307,13 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP # Validar que sea un número if not value.isdigit(): await update.message.reply_text( - t(user_id, "schedule.invalid_number"), + translate(user_id, "schedule.invalid_number"), parse_mode=None ) return field = "total_data" - field_display = t(user_id, "schedule.data_limit") + field_display = translate(user_id, "schedule.data_limit") processed_value = value # El número tal cual value_display = f"{value} GB" @@ -3324,7 +3324,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP if not re.match(r'^\d{1,2}/\d{1,2}/\d{4}$', value): await update.message.reply_text( - t(user_id, "schedule.invalid_date_format"), + translate(user_id, "schedule.invalid_date_format"), parse_mode=None ) return @@ -3336,13 +3336,13 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP # Convertir a formato YYYY-MM-DD HH:MM:SS field = "date" - field_display = t(user_id, "schedule.expiry_date") + field_display = translate(user_id, "schedule.expiry_date") processed_value = f"{year:04d}-{month:02d}-{day:02d} 00:00:00" value_display = value except ValueError: await update.message.reply_text( - t(user_id, "schedule.invalid_date_value"), + translate(user_id, "schedule.invalid_date_value"), parse_mode=None ) return @@ -3355,7 +3355,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP } # Mensaje de progreso - status_msg = await update.message.reply_text(t(user_id, "input.creating_job")) + status_msg = await update.message.reply_text(translate(user_id, "input.creating_job")) # Enviar a la API result = api_client.create_schedule_job(config_name, public_key, job_data) @@ -3373,22 +3373,22 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP field_display_escaped = field_display value_display_escaped = value_display - message = t(user_id, "input.job_created_title") + "\n\n" - message += f"*{t(user_id, 'schedule.peer_label')}* {peer_name_escaped}\n" - message += f"*{t(user_id, 'schedule.config_label')}* {config_name_escaped}\n" - message += f"*{t(user_id, 'schedule.action_label')}* RESTRICT\n" - message += f"*{t(user_id, 'input.field')}* {field_display_escaped}\n" - message += f"*{t(user_id, 'input.value')}* {value_display_escaped}\n\n" - message += t(user_id, "input.job_created_msg") + message = translate(user_id, "input.job_created_title") + "\n\n" + message += f"*{translate(user_id, 'schedule.peer_label')}* {peer_name_escaped}\n" + message += f"*{translate(user_id, 'schedule.config_label')}* {config_name_escaped}\n" + message += f"*{translate(user_id, 'schedule.action_label')}* RESTRICT\n" + message += f"*{translate(user_id, 'input.field')}* {field_display_escaped}\n" + message += f"*{translate(user_id, 'input.value')}* {value_display_escaped}\n\n" + message += translate(user_id, "input.job_created_msg") keyboard = [ [ - InlineKeyboardButton(t(user_id, "input.back_schedule"), + InlineKeyboardButton(translate(user_id, "input.back_schedule"), callback_data=f"schedule_job_peer:{config_name}:{peer_index}"), - InlineKeyboardButton(t(user_id, "input.add_another"), + InlineKeyboardButton(translate(user_id, "input.add_another"), callback_data=f"schedule_job_peer:{config_name}:{peer_index}") ], - [InlineKeyboardButton(t(user_id, "menu.main"), callback_data="main_menu")] + [InlineKeyboardButton(translate(user_id, "menu.main"), callback_data="main_menu")] ] await status_msg.edit_text( @@ -3407,7 +3407,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP # Validar el nombre if not message_text or len(message_text) > 32: await update.message.reply_text( - t(user_id, "peer_creation.invalid_name_length"), + translate(user_id, "peer_creation.invalid_name_length"), parse_mode=None ) return @@ -3430,10 +3430,10 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP context.user_data['waiting_for_operator_peer_name'] = False await update.message.reply_text( - t(user_id, "peer_creation.name_accepted", name=peer_name) + "\n\n" + - t(user_id, "peer_creation.send_endpoint") + "\n\n" + - t(user_id, "peer_creation.endpoint_format") + "\n" + - t(user_id, "peer_creation.endpoint_examples") + "\n\n" + + translate(user_id, "peer_creation.name_accepted", name=peer_name) + "\n\n" + + translate(user_id, "peer_creation.send_endpoint") + "\n\n" + + translate(user_id, "peer_creation.endpoint_format") + "\n" + + translate(user_id, "peer_creation.endpoint_examples") + "\n\n" + "Envía el endpoint ahora o escribe */cancel* para cancelar.", parse_mode="Markdown" ) @@ -3447,7 +3447,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP import re if not re.match(r'^[a-zA-Z0-9\.\-]+:\d+$', endpoint): await update.message.reply_text( - t(user_id, "peer_creation.invalid_endpoint_format"), + translate(user_id, "peer_creation.invalid_endpoint_format"), parse_mode=None ) return @@ -3460,7 +3460,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP raise ValueError except: await update.message.reply_text( - t(user_id, "peer_creation.invalid_port"), + translate(user_id, "peer_creation.invalid_port"), parse_mode=None ) return @@ -3493,7 +3493,7 @@ async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYP # Validar el nombre if not message_text or len(message_text) > 32: await update.message.reply_text( - t(user_id, "peer_creation.invalid_name_length"), + translate(user_id, "peer_creation.invalid_name_length"), parse_mode=None ) return @@ -3551,27 +3551,27 @@ async def generate_peer_automatically(update: Update, context: ContextTypes.DEFA hours = int(remaining.total_seconds() // 3600) minutes = int((remaining.total_seconds() % 3600) // 60) - time_msg = t(user_id, "rate_limit.hours_minutes", hours=hours, minutes=minutes) if hours > 0 else t(user_id, "rate_limit.minutes", minutes=minutes) + time_msg = translate(user_id, "rate_limit.hours_minutes", hours=hours, minutes=minutes) if hours > 0 else translate(user_id, "rate_limit.minutes", minutes=minutes) await update.message.reply_text( - f"{t(user_id, 'rate_limit.title')}\n\n" - f"{t(user_id, error_key, time=time_msg)}\n\n" - f"{t(user_id, 'rate_limit.remaining', time=time_msg)}\n\n" - f"{t(user_id, 'rate_limit.can_create_later')}", + f"{translate(user_id, 'rate_limit.title')}\n\n" + f"{translate(user_id, error_key, time=time_msg)}\n\n" + f"{translate(user_id, 'rate_limit.remaining', time=time_msg)}\n\n" + f"{translate(user_id, 'rate_limit.can_create_later')}", reply_markup=operator_main_menu(user_id), parse_mode="Markdown" ) return else: await update.message.reply_text( - f"{t(user_id, 'rate_limit.no_more_peers')}\n\n" - f"{t(user_id, error_key or 'errors.access_denied')}", + f"{translate(user_id, 'rate_limit.no_more_peers')}\n\n" + f"{translate(user_id, error_key or 'errors.access_denied')}", reply_markup=operator_main_menu(user_id), parse_mode="Markdown" ) return - await update.message.reply_text(t(user_id, "peer_creation.generating", name=peer_name, config=config_name)) + await update.message.reply_text(translate(user_id, "peer_creation.generating", name=peer_name, config=config_name)) # 1. Generar claves WireGuard y pre-shared key private_key, public_key = generate_wireguard_keys() @@ -3601,7 +3601,7 @@ async def generate_peer_automatically(update: Update, context: ContextTypes.DEFA config_result = api_client.get_configuration_detail(config_name) if not config_result.get("status"): await update.message.reply_text( - t(user_id, "errors.api_error", message=config_result.get('message', 'Error desconocido')), + translate(user_id, "errors.api_error", message=config_result.get('message', 'Error desconocido')), parse_mode=None ) return @@ -3657,7 +3657,7 @@ async def generate_peer_automatically(update: Update, context: ContextTypes.DEFA } # 6. Enviar a la API - await update.message.reply_text(t(user_id, "peer_creation.sending_data")) + await update.message.reply_text(translate(user_id, "peer_creation.sending_data")) result = api_client.add_peer(config_name, peer_data) @@ -3697,26 +3697,26 @@ async def generate_peer_automatically(update: Update, context: ContextTypes.DEFA } # Enviar jobs a la API - await update.message.reply_text(t(user_id, "peer_creation.limits_configured")) + await update.message.reply_text(translate(user_id, "peer_creation.limits_configured")) result_gb = api_client.create_schedule_job(config_name, public_key, job_data_gb) result_date = api_client.create_schedule_job(config_name, public_key, job_data_date) jobs_status = "" if result_gb.get("status") and result_date.get("status"): - jobs_status = t(user_id, "peer_creation.creation_success_limit") + jobs_status = translate(user_id, "peer_creation.creation_success_limit") else: - jobs_status = t(user_id, "peer_creation.creation_warn_limit") + jobs_status = translate(user_id, "peer_creation.creation_warn_limit") if not result_gb.get("status"): - jobs_status += t(user_id, "peer_creation.limit_data_error", message=result_gb.get('message')) + jobs_status += translate(user_id, "peer_creation.limit_data_error", message=result_gb.get('message')) if not result_date.get("status"): - jobs_status += t(user_id, "peer_creation.limit_time_error", message=result_date.get('message')) + jobs_status += translate(user_id, "peer_creation.limit_time_error", message=result_date.get('message')) jobs_status += "\n" # ================= MENSAJE FINAL SEGÚN ROL ================= # if is_operator(user_id): # Para operadores: mostrar información específica con límites - message = t(user_id, "peer_creation.operator_success_msg", + message = translate(user_id, "peer_creation.operator_success_msg", name=peer_name, config=config_name, ip=allowed_ip, endpoint=endpoint, limits_status=jobs_status, public_key=public_key[:30], private_key=private_key[:30], @@ -3724,12 +3724,12 @@ async def generate_peer_automatically(update: Update, context: ContextTypes.DEFA # Para operadores, mostrar solo botón de descarga keyboard = [ - [InlineKeyboardButton(t(user_id, "actions.download"), callback_data=f"download_config:{peer_hash}")], - [InlineKeyboardButton(t(user_id, "menu.back"), callback_data="operator_main_menu")] + [InlineKeyboardButton(translate(user_id, "actions.download"), callback_data=f"download_config:{peer_hash}")], + [InlineKeyboardButton(translate(user_id, "menu.back"), callback_data="operator_main_menu")] ] else: # Admin - message = t(user_id, "peer_creation.admin_success_msg", + message = translate(user_id, "peer_creation.admin_success_msg", name=peer_name, config=config_name, ip=allowed_ip, endpoint=endpoint, public_key=public_key, private_key=private_key, preshared_key=preshared_key) @@ -3737,8 +3737,8 @@ async def generate_peer_automatically(update: Update, context: ContextTypes.DEFA # Para admins, mostrar botones normales keyboard = [ [ - InlineKeyboardButton(t(user_id, "actions.back_to_config"), callback_data=f"cfg:{config_name}"), - InlineKeyboardButton(t(user_id, "actions.download"), callback_data=f"download_config:{peer_hash}") + InlineKeyboardButton(translate(user_id, "actions.back_to_config"), callback_data=f"cfg:{config_name}"), + InlineKeyboardButton(translate(user_id, "actions.download"), callback_data=f"download_config:{peer_hash}") ] ] @@ -3753,10 +3753,10 @@ async def generate_peer_automatically(update: Update, context: ContextTypes.DEFA error_msg = result.get('message', 'Error desconocido') if is_operator(user_id): - keyboard = [[InlineKeyboardButton(t(user_id, "menu.back"), callback_data="operator_main_menu")]] - text = t(user_id, "peer_creation.creation_error_operator", message=error_msg) + keyboard = [[InlineKeyboardButton(translate(user_id, "menu.back"), callback_data="operator_main_menu")]] + text = translate(user_id, "peer_creation.creation_error_operator", message=error_msg) else: - keyboard = [[InlineKeyboardButton(t(user_id, "actions.back_to_config"), callback_data=f"cfg:{config_name}")]] + keyboard = [[InlineKeyboardButton(translate(user_id, "actions.back_to_config"), callback_data=f"cfg:{config_name}")]] text = f"❌ *Error al agregar peer*\n\n*Error:* {error_msg}\n\nIntenta nuevamente o contacta al administrador." reply_markup = InlineKeyboardMarkup(keyboard) @@ -3791,13 +3791,13 @@ async def handle_operator_create_peer(query, context: CallbackContext): hours = int(remaining.total_seconds() // 3600) minutes = int((remaining.total_seconds() % 3600) // 60) - time_msg = t(user_id, "rate_limit.hours_minutes", hours=hours, minutes=minutes) if hours > 0 else t(user_id, "rate_limit.minutes", minutes=minutes) + time_msg = translate(user_id, "rate_limit.hours_minutes", hours=hours, minutes=minutes) if hours > 0 else translate(user_id, "rate_limit.minutes", minutes=minutes) # Use t() for localized title and message - message_text = f"{t(user_id, 'rate_limit.title')}\n\n" \ - f"{t(user_id, error_msg, time=time_msg)}\n\n" \ - f"{t(user_id, 'rate_limit.remaining', time=time_msg)}\n\n" \ - f"{t(user_id, 'rate_limit.can_create_later')} ({timestamp})" + message_text = f"{translate(user_id, 'rate_limit.title')}\n\n" \ + f"{translate(user_id, error_msg, time=time_msg)}\n\n" \ + f"{translate(user_id, 'rate_limit.remaining', time=time_msg)}\n\n" \ + f"{translate(user_id, 'rate_limit.can_create_later')} ({timestamp})" await query.edit_message_text( message_text, @@ -3807,8 +3807,8 @@ async def handle_operator_create_peer(query, context: CallbackContext): return else: await query.edit_message_text( - f"{t(user_id, 'rate_limit.no_more_peers')} ({timestamp})\n\n" - f"{t(user_id, error_msg or 'errors.access_denied')}", + f"{translate(user_id, 'rate_limit.no_more_peers')} ({timestamp})\n\n" + f"{translate(user_id, error_msg or 'errors.access_denied')}", reply_markup=operator_main_menu(user_id), parse_mode="Markdown" ) @@ -3818,7 +3818,7 @@ async def handle_operator_create_peer(query, context: CallbackContext): result = api_client.get_configurations() if not result.get("status"): await query.edit_message_text( - t(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')) + f" ({timestamp})", + translate(user_id, "errors.api_error", message=result.get('message', 'Error desconocido')) + f" ({timestamp})", reply_markup=operator_main_menu(user_id) ) return @@ -3826,7 +3826,7 @@ async def handle_operator_create_peer(query, context: CallbackContext): configs = result.get("data", []) if not configs: await query.edit_message_text( - t(user_id, "errors.no_configs") + f" ({timestamp})", + translate(user_id, "errors.no_configs") + f" ({timestamp})", reply_markup=operator_main_menu(user_id) ) return @@ -3839,10 +3839,10 @@ async def handle_operator_create_peer(query, context: CallbackContext): context.user_data['config_name_for_operator_peer'] = config_name await query.edit_message_text( - t(user_id, "peer_creation.add_peer", config=config_name) + f" ({timestamp})\n\n" + - t(user_id, "peer_creation.send_name"), + translate(user_id, "peer_creation.add_peer", config=config_name) + f" ({timestamp})\n\n" + + translate(user_id, "peer_creation.send_name"), reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton(t(user_id, "actions.cancel"), callback_data="operator_main_menu")] + [InlineKeyboardButton(translate(user_id, "actions.cancel"), callback_data="operator_main_menu")] ]), parse_mode="Markdown" ) @@ -3853,13 +3853,13 @@ async def handle_operators_detailed(query, context: CallbackContext): if not is_admin(user_id): await query.edit_message_text( - t(user_id, "errors.access_denied"), + translate(user_id, "errors.access_denied"), reply_markup=main_menu(is_admin(user_id), is_operator(user_id), user_id), parse_mode="Markdown" ) return - await query.edit_message_text(t(user_id, "operators.getting_detailed")) + await query.edit_message_text(translate(user_id, "operators.getting_detailed")) # Obtener información de todos los operadores operator_users = {uid: info for uid, info in ALLOWED_USERS.items() @@ -3867,13 +3867,13 @@ async def handle_operators_detailed(query, context: CallbackContext): if not operator_users: await query.edit_message_text( - t(user_id, "errors.no_operators"), + translate(user_id, "errors.no_operators"), reply_markup=refresh_button("operators_list", user_id), parse_mode="Markdown" ) return - message_lines = [t(user_id, "operators.detailed") + "\n"] + message_lines = [translate(user_id, "operators.detailed") + "\n"] all_peers = [] @@ -3885,14 +3885,14 @@ async def handle_operators_detailed(query, context: CallbackContext): match = re.search(r'(?:Operator|Operador)\s+(\d+)', user_name, re.IGNORECASE) if match: op_num = match.group(1) - user_name = t(user_id, "operators.operator_name", number=op_num) + user_name = translate(user_id, "operators.operator_name", number=op_num) user_name = escape_markdown(user_name) user_peers = operators_db.get_user_peers(uid) message_lines.append(f"\n**{user_name}** (ID: `{uid}`) ") - message_lines.append(t(user_id, "operators.total_peers_count", count=len(user_peers))) + message_lines.append(translate(user_id, "operators.total_peers_count", count=len(user_peers))) if user_peers: # Ordenar por fecha descendente @@ -3910,23 +3910,23 @@ async def handle_operators_detailed(query, context: CallbackContext): dt = datetime.fromisoformat(created_at) created_str = dt.strftime("%d/%m/%Y %H:%M") except: - created_str = t(user_id, "errors.unknown_date") + created_str = translate(user_id, "errors.unknown_date") message_lines.append(f" {i}. **{peer_name}**") - message_lines.append(f" {t(user_id, 'operators.created_at', date=created_str)}") - message_lines.append(f" {t(user_id, 'operators.config', name=config_name)}") - message_lines.append(f" {t(user_id, 'operators.endpoint', endpoint=endpoint)}") - message_lines.append(f" {t(user_id, 'operators.key', key=public_key_short)}") + message_lines.append(f" {translate(user_id, 'operators.created_at', date=created_str)}") + message_lines.append(f" {translate(user_id, 'operators.config', name=config_name)}") + message_lines.append(f" {translate(user_id, 'operators.endpoint', endpoint=endpoint)}") + message_lines.append(f" {translate(user_id, 'operators.key', key=public_key_short)}") if len(user_peers) > 5: - message_lines.append(f" {t(user_id, 'operators.and_more', count=len(user_peers) - 5)}") + message_lines.append(f" {translate(user_id, 'operators.and_more', count=len(user_peers) - 5)}") all_peers.extend(user_peers) # Resumen - message_lines.append(f"\n{t(user_id, 'operators.summary')}") - message_lines.append(t(user_id, "operators.total_operators", count=len(operator_users))) - message_lines.append(t(user_id, "operators.total_peers", count=len(all_peers))) + message_lines.append(f"\n{translate(user_id, 'operators.summary')}") + message_lines.append(translate(user_id, "operators.total_operators", count=len(operator_users))) + message_lines.append(translate(user_id, "operators.total_peers", count=len(all_peers))) # Encontrar el peer más reciente if all_peers: @@ -3947,10 +3947,10 @@ async def handle_operators_detailed(query, context: CallbackContext): latest_date = datetime.fromisoformat(latest_peer.get('created_at', '')) latest_date_str = latest_date.strftime("%d/%m/%Y %H:%M") except: - latest_date_str = t(user_id, "errors.unknown_date") + latest_date_str = translate(user_id, "errors.unknown_date") latest_peer_name = escape_markdown(latest_peer_name) - message_lines.append(t(user_id, "operators.latest_created_by", peer=latest_peer_name, operator=latest_operator_id, date=latest_date_str)) + message_lines.append(translate(user_id, "operators.latest_created_by", peer=latest_peer_name, operator=latest_operator_id, date=latest_date_str)) message = "\n".join(message_lines) @@ -3973,20 +3973,20 @@ async def handle_operators_detailed(query, context: CallbackContext): # Agregar teclado al último mensaje keyboard = [ - [InlineKeyboardButton(t(user_id, "menu.back"), callback_data="operators_list")], - [InlineKeyboardButton(t(user_id, "menu.main"), callback_data="main_menu")] + [InlineKeyboardButton(translate(user_id, "menu.back"), callback_data="operators_list")], + [InlineKeyboardButton(translate(user_id, "menu.main"), callback_data="main_menu")] ] await query.message.reply_text( - t(user_id, "operators.end_report"), + translate(user_id, "operators.end_report"), reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown" ) else: # Enviar mensaje completo keyboard = [ - [InlineKeyboardButton(t(user_id, "menu.back"), callback_data="operators_list")], - [InlineKeyboardButton(t(user_id, "menu.main"), callback_data="main_menu")] + [InlineKeyboardButton(translate(user_id, "menu.back"), callback_data="operators_list")], + [InlineKeyboardButton(translate(user_id, "menu.main"), callback_data="main_menu")] ] await query.edit_message_text( diff --git a/src/i18n.py b/src/modules/i18n.py similarity index 99% rename from src/i18n.py rename to src/modules/i18n.py index 4d27827..9f09850 100644 --- a/src/i18n.py +++ b/src/modules/i18n.py @@ -5,7 +5,7 @@ import json import os from typing import Dict, Any, Optional -from config import DATA_DIR +from .config import DATA_DIR class I18n: """Gestor de traducciones multiidioma completo""" diff --git a/src/keyboards.py b/src/modules/keyboards.py similarity index 99% rename from src/keyboards.py rename to src/modules/keyboards.py index bb6de97..6887141 100644 --- a/src/keyboards.py +++ b/src/modules/keyboards.py @@ -6,8 +6,8 @@ from typing import List, Dict, Any, Optional from telegram import InlineKeyboardButton, InlineKeyboardMarkup -from config import ITEMS_PER_PAGE -from utils import t +from .config import ITEMS_PER_PAGE +from .utils import translate def safe_callback_data(text: str) -> str: """Codifica texto para hacerlo seguro para callback_data""" diff --git a/src/operators.py b/src/modules/operators.py similarity index 98% rename from src/operators.py rename to src/modules/operators.py index b89d44d..3ed4dae 100644 --- a/src/operators.py +++ b/src/modules/operators.py @@ -8,7 +8,7 @@ from typing import Dict, List, Any, Optional, Tuple import logging -from config import OPERATORS_DB, OPERATOR_LIMIT_HOURS, OPERATOR_DATA_LIMIT_GB, OPERATOR_TIME_LIMIT_HOURS +from .config import OPERATORS_DB, OPERATOR_LIMIT_HOURS, OPERATOR_DATA_LIMIT_GB, OPERATOR_TIME_LIMIT_HOURS logger = logging.getLogger(__name__) diff --git a/src/regenerate_translations.py b/src/modules/regenerate_translations.py similarity index 100% rename from src/regenerate_translations.py rename to src/modules/regenerate_translations.py diff --git a/src/setup_logging.py b/src/modules/setup_logging.py similarity index 96% rename from src/setup_logging.py rename to src/modules/setup_logging.py index 8f37d8b..dfff735 100644 --- a/src/setup_logging.py +++ b/src/modules/setup_logging.py @@ -5,7 +5,7 @@ import logging import logging.handlers import os -from config import LOG_FILE, LOG_LEVEL, LOG_MAX_SIZE, LOG_BACKUP_COUNT +from .config import LOG_FILE, LOG_LEVEL, LOG_MAX_SIZE, LOG_BACKUP_COUNT def setup_logging(): """Configura el sistema de logging""" diff --git a/src/update_translations.py b/src/modules/update_translations.py similarity index 100% rename from src/update_translations.py rename to src/modules/update_translations.py diff --git a/src/utils.py b/src/modules/utils.py similarity index 79% rename from src/utils.py rename to src/modules/utils.py index 8cd6525..8c50099 100644 --- a/src/utils.py +++ b/src/modules/utils.py @@ -12,8 +12,8 @@ # Eliminado import circular directo # from i18n import i18n -from config import ALLOWED_USERS, MAX_PEERS_DISPLAY, ROLE_ADMIN, ROLE_OPERATOR -from operators import operators_db +from .config import ALLOWED_USERS, MAX_PEERS_DISPLAY, ROLE_ADMIN, ROLE_OPERATOR +from .operators import operators_db logger = logging.getLogger(__name__) @@ -90,16 +90,16 @@ def format_size(bytes_size: int) -> str: def format_handshake_time(seconds: Optional[int], user_id: int) -> str: """Formatea el tiempo desde último handshake""" if not seconds: - return t(user_id, "time.never") + return translate(user_id, "time.never") if seconds < 60: - return t(user_id, "time.seconds", count=seconds) + return translate(user_id, "time.seconds", count=seconds) elif seconds < 3600: - return t(user_id, "time.minutes", count=seconds // 60) + return translate(user_id, "time.minutes", count=seconds // 60) elif seconds < 86400: - return t(user_id, "time.hours", count=seconds // 3600) + return translate(user_id, "time.hours", count=seconds // 3600) else: - return t(user_id, "time.days", count=seconds // 86400) + return translate(user_id, "time.days", count=seconds // 86400) def format_bytes_human(bytes_size: float) -> str: """Formatea bytes a formato humano legible (recibe MB)""" @@ -122,39 +122,39 @@ def format_bytes_human(bytes_size: float) -> str: def format_time_ago(seconds: int, user_id: int) -> str: """Formatea segundos a 'hace X tiempo'""" if seconds <= 0: - return t(user_id, "time.never") + return translate(user_id, "time.never") if seconds < 60: - return t(user_id, "time.ago_seconds", count=int(seconds)) + return translate(user_id, "time.ago_seconds", count=int(seconds)) elif seconds < 3600: minutes = seconds // 60 - return t(user_id, "time.ago_minutes", count=int(minutes)) + return translate(user_id, "time.ago_minutes", count=int(minutes)) elif seconds < 86400: hours = seconds // 3600 - return t(user_id, "time.ago_hours", count=int(hours)) + return translate(user_id, "time.ago_hours", count=int(hours)) else: days = seconds // 86400 - return t(user_id, "time.ago_days", count=int(days)) + return translate(user_id, "time.ago_days", count=int(days)) def format_time_remaining(seconds: int, user_id: int) -> str: """Formatea segundos restantes en formato legible""" if seconds <= 0: - return t(user_id, "time.now") + return translate(user_id, "time.now") hours = seconds // 3600 minutes = (seconds % 3600) // 60 if hours > 0: - return t(user_id, "time.hm_format", hours=hours, minutes=minutes) + return translate(user_id, "time.hm_format", hours=hours, minutes=minutes) else: - return t(user_id, "time.m_format", minutes=minutes) + return translate(user_id, "time.m_format", minutes=minutes) def format_peer_info(peer: Dict, user_id: int) -> str: """Formatea la información de un peer""" lines = [] # Información básica - name = peer.get('name', t(user_id, "common.no_name")) + name = peer.get('name', translate(user_id, "common.no_name")) public_key = peer.get('id', '') # En esta API, la clave pública está en 'id' lines.append(f"👤 **{name}**") @@ -163,22 +163,22 @@ def format_peer_info(peer: Dict, user_id: int) -> str: latest_handshake = peer.get('latest_handshake_seconds', 0) if status == 'running' and latest_handshake > 0: - lines.append(f" 🔗 {t(user_id, 'peers.connected').split(':')[0]}: {format_handshake_time(latest_handshake, user_id)}") - status_text = t(user_id, "peers.status_connected") + lines.append(f" 🔗 {ranslatet(user_id, 'peers.connected').split(':')[0]}: {format_handshake_time(latest_handshake, user_id)}") + status_text = translate(user_id, "peers.status_connected") else: - status_text = t(user_id, "peers.status_disconnected") - lines.append(f" 📊 {t(user_id, 'system.status')}: {status_text}") + status_text = translate(user_id, "peers.status_disconnected") + lines.append(f" 📊 {ranslatet(user_id, 'system.status')}: {status_text}") # Transferencia de datos (en MB) total_receive = peer.get('total_receive', 0) # MB total_sent = peer.get('total_sent', 0) # MB - lines.append(f" ⬇️ {t(user_id, 'traffic.received', value=format_bytes_human(total_receive))}") - lines.append(f" ⬆️ {t(user_id, 'traffic.sent', value=format_bytes_human(total_sent))}") + lines.append(f" ⬇️ {translate(user_id, 'traffic.received', value=format_bytes_human(total_receive))}") + lines.append(f" ⬆️ {translate(user_id, 'traffic.sent', value=format_bytes_human(total_sent))}") # IPs - allowed_ip = peer.get('allowed_ip', t(user_id, "common.na")) - endpoint = peer.get('endpoint', t(user_id, "common.na")) + allowed_ip = peer.get('allowed_ip', translate(user_id, "common.na")) + endpoint = peer.get('endpoint', translate(user_id, "common.na")) lines.append(f" 📍 IP: `{allowed_ip}`") lines.append(f" 🌐 Endpoint: `{endpoint}`") @@ -190,51 +190,51 @@ def format_peer_info(peer: Dict, user_id: int) -> str: def format_system_status(status_data: Dict, user_id: int) -> str: """Formatea el estado del sistema""" - lines = [t(user_id, "system.title") + "\n"] + lines = [translate(user_id, "system.title") + "\n"] # CPU cpu = status_data.get('CPU', {}) cpu_percent = float(cpu.get('cpu_percent', 0)) - lines.append(t(user_id, "system.cpu", percent=cpu_percent)) + lines.append(translate(user_id, "system.cpu", percent=cpu_percent)) # Memoria memory = status_data.get('Memory', {}).get('VirtualMemory', {}) mem_percent = float(memory.get('percent', 0)) mem_total = format_size(memory.get('total', 0)) mem_available = format_size(memory.get('available', 0)) - lines.append(t(user_id, "system.memory", percent=f"{float(mem_percent):.1f}")) - lines.append(t(user_id, "system.memory_details", total=mem_total, available=mem_available)) + lines.append(translate(user_id, "system.memory", percent=f"{float(mem_percent):.1f}")) + lines.append(translate(user_id, "system.memory_details", total=mem_total, available=mem_available)) # Swap swap = status_data.get('Memory', {}).get('SwapMemory', {}) if swap.get('total', 0) > 0: swap_percent = float(swap.get('percent', 0)) swap_total = format_size(swap.get('total', 0)) - lines.append(t(user_id, "system.swap", percent=swap_percent, total=swap_total)) + lines.append(translate(user_id, "system.swap", percent=swap_percent, total=swap_total)) # Discos (solo los principales) disks = status_data.get('Disks', []) if disks: - lines.append(t(user_id, "system.disks")) + lines.append(translate(user_id, "system.disks")) for disk in disks[:3]: # Mostrar solo 3 discos mount = disk.get('mountPoint', 'N/A') percent = disk.get('percent', 0) free = format_size(disk.get('free', 0)) - lines.append(t(user_id, "system.disk_details", mount=mount, percent=percent, free=free)) + lines.append(translate(user_id, "system.disk_details", mount=mount, percent=percent, free=free)) # Interfaces de red - MODIFICADO PARA MOSTRAR TODAS LAS INTERFACES interfaces = status_data.get('NetworkInterfaces', {}) if interfaces: - lines.append(t(user_id, "system.interfaces")) + lines.append(translate(user_id, "system.interfaces")) # Ordenar interfaces para mostrar primero las importantes interface_order = ['lo', 'ens3', 'eth0', 'eth1'] # Interfaces principales - wg_interfaces = [] + wgd_interfaces = [] other_interfaces = [] for iface_name, iface_data in interfaces.items(): if iface_name.startswith('wg'): - wg_interfaces.append((iface_name, iface_data)) + wgd_interfaces.append((iface_name, iface_data)) elif iface_name not in interface_order: other_interfaces.append((iface_name, iface_data)) @@ -244,35 +244,35 @@ def format_system_status(status_data: Dict, user_id: int) -> str: iface_data = interfaces[iface_name] sent = format_size(iface_data.get('bytes_sent', 0)) recv = format_size(iface_data.get('bytes_recv', 0)) - lines.append(t(user_id, "system.interface_details", name=iface_name, sent=sent, recv=recv)) + lines.append(translate(user_id, "system.interface_details", name=iface_name, sent=sent, recv=recv)) # Mostrar todas las interfaces WireGuard - if wg_interfaces: - lines.append(t(user_id, "system.wg_interfaces")) - for iface_name, iface_data in sorted(wg_interfaces): + if wgd_interfaces: + lines.append(translate(user_id, "system.wgd_interfaces")) + for iface_name, iface_data in sorted(wgd_interfaces): sent = format_size(iface_data.get('bytes_sent', 0)) recv = format_size(iface_data.get('bytes_recv', 0)) - lines.append(t(user_id, "system.interface_details", name=iface_name, sent=sent, recv=recv)) + lines.append(translate(user_id, "system.interface_details", name=iface_name, sent=sent, recv=recv)) # Mostrar otras interfaces (limitado a 5 para no hacer muy largo el mensaje) if other_interfaces: - lines.append(t(user_id, "system.other_interfaces")) + lines.append(translate(user_id, "system.other_interfaces")) for iface_name, iface_data in sorted(other_interfaces)[:5]: # Máximo 5 sent = format_size(iface_data.get('bytes_sent', 0)) recv = format_size(iface_data.get('bytes_recv', 0)) - lines.append(t(user_id, "system.interface_details", name=iface_name, sent=sent, recv=recv)) + lines.append(translate(user_id, "system.interface_details", name=iface_name, sent=sent, recv=recv)) if len(other_interfaces) > 5: - lines.append(t(user_id, "operators.and_more", count=len(other_interfaces) - 5)) + lines.append(translate(user_id, "operators.and_more", count=len(other_interfaces) - 5)) return "\n".join(lines) def format_config_summary(configs: List[Dict], user_id: int) -> str: """Formatea un resumen de todas las configuraciones""" if not configs: - return t(user_id, "summary.no_configs") + return translate(user_id, "summary.no_configs") - lines = [t(user_id, "summary.title") + "\n"] + lines = [translate(user_id, "summary.title") + "\n"] total_peers = 0 total_connected = 0 @@ -287,10 +287,10 @@ def format_config_summary(configs: List[Dict], user_id: int) -> str: total_connected += connected status_emoji = "✅" if connected > 0 else "⚠️" - lines.append(t(user_id, "summary.config_line", status=status_emoji, name=name, port=listen_port)) - lines.append(t(user_id, "summary.peers_line", connected=connected, total=peers)) + lines.append(translate(user_id, "summary.config_line", status=status_emoji, name=name, port=listen_port)) + lines.append(translate(user_id, "summary.peers_line", connected=connected, total=peers)) - lines.append(f"\n" + t(user_id, "summary.totals", connected=total_connected, total=total_peers)) + lines.append(f"\n" + translate(user_id, "summary.totals", connected=total_connected, total=total_peers)) return "\n".join(lines) @@ -426,7 +426,7 @@ def safe_message(text: str, parse_mode: str = "HTML", **kwargs) -> dict: return {"text": formatted_text, "parse_mode": parse_mode} -def t(user_id: int, key_path: str, **kwargs) -> str: +def translate(user_id: int, key_path: str, **kwargs) -> str: """Función helper para obtener traducciones""" # Importar aquí para evitar dependencias circulares from i18n import i18n diff --git a/src/wg_api.py b/src/modules/wgd_api.py similarity index 98% rename from src/wg_api.py rename to src/modules/wgd_api.py index 2a9efa9..acea0b5 100644 --- a/src/wg_api.py +++ b/src/modules/wgd_api.py @@ -12,7 +12,7 @@ import psutil import platform -from config import WG_API_BASE_URL, WG_API_KEY, API_TIMEOUT, WG_API_PREFIX +from .config import WGD_API_BASE_URL, WGD_API_KEY, API_TIMEOUT, WGD_API_PREFIX logger = logging.getLogger(__name__) @@ -24,12 +24,12 @@ class WGApiClient: """Cliente para interactuar con la API de WGDashboard""" def __init__(self): - self.base_url = WG_API_BASE_URL.rstrip('/') - if WG_API_PREFIX: - self.base_url = f"{self.base_url.rstrip('/')}/{WG_API_PREFIX.lstrip('/')}" + self.base_url = WGD_API_BASE_URL.rstrip('/') + if WGD_API_PREFIX: + self.base_url = f"{self.base_url.rstrip('/')}/{WGD_API_PREFIX.lstrip('/')}" self.headers = { - "wg-dashboard-apikey": WG_API_KEY, + "wg-dashboard-apikey": WGD_API_KEY, "Content-Type": "application/json", "User-Agent": "WGDashboard-Bot/1.0" } diff --git a/src/requirements.txt b/src/requirements.txt index a292fe4..6e53113 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,4 +1,5 @@ -python-telegram-bot>=20.0 -requests>=2.28.0 -psutil>=5.9.0 -cryptography>=41.0.0 +cryptography==46.0.4 +dotenv==0.9.9 +psutil==7.2.2 +python-telegram-bot==22.6 +requests==2.32.5 \ No newline at end of file