From 143f9810b04d5d2d683a79f4e03a5db0b0d9eb27 Mon Sep 17 00:00:00 2001 From: juanminsanz Date: Sun, 20 Sep 2026 12:42:16 +0200 Subject: [PATCH] 20-sep: migracion watchers (Telegram+recordatorios) a INFRAESTRUCTURA + estado/agentes/changelog --- .gitignore | 23 ++ .opencode/agent/infra-agente.md | 38 ++ .opencode/skill/infraestructura/SKILL.md | 58 +++ AGENTS.md | 99 +++++ CHANGELOG_INFRA.md | 9 + README.md | 39 +- changelog_infra.json | 12 + estado/ESTADO_DIARIO.md | 50 +++ estado/pendientes_infra.json | 40 ++ estado/recordatorios_msgbox.json | 57 +++ estado/telegram_tracked_users.json | 15 + scripts/infra_changelog.py | 2 +- scripts/infra_paths.py | 48 +++ scripts/instalar_global.ps1 | 4 + scripts/instalar_watchers.ps1 | 89 +++++ scripts/recordatorio.cmd | 31 ++ scripts/recordatorios_msgbox.py | 226 +++++++++++ scripts/telegram_watcher.py | 482 +++++++++++++++++++++++ 18 files changed, 1315 insertions(+), 7 deletions(-) create mode 100644 .gitignore create mode 100644 .opencode/agent/infra-agente.md create mode 100644 .opencode/skill/infraestructura/SKILL.md create mode 100644 AGENTS.md create mode 100644 estado/ESTADO_DIARIO.md create mode 100644 estado/pendientes_infra.json create mode 100644 estado/recordatorios_msgbox.json create mode 100644 estado/telegram_tracked_users.json create mode 100644 scripts/infra_paths.py create mode 100644 scripts/instalar_watchers.ps1 create mode 100644 scripts/recordatorio.cmd create mode 100644 scripts/recordatorios_msgbox.py create mode 100644 scripts/telegram_watcher.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfcf1b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Python +__pycache__/ +*.pyc +*.pyo +.venv/ +venv/ + +# Entorno / secretos +.env +.env.local +*.token + +# Estado runtime (volatil; NO versionar) +estado/*.log +estado/.watcher_last_id +estado/.netatmo_last_alert.json +estado/.netatmo_token.json + +# IDE / OS +.vscode/ +.idea/ +Thumbs.db +.DS_Store diff --git a/.opencode/agent/infra-agente.md b/.opencode/agent/infra-agente.md new file mode 100644 index 0000000..a956c7e --- /dev/null +++ b/.opencode/agent/infra-agente.md @@ -0,0 +1,38 @@ +--- +description: Especialista en infraestructura Prolongo (repo INFRAESTRUCTURA): watchers, OmniRoute, OpenCode, VPS, changelog global y sincronizacion sobremesa<->portatil. SOLO infraestructura; el conocimiento de negocio va al repo biblioteca_negocio_prolongo. +mode: subagent +permission: + bash: + "git *": allow + "*": allow +--- + +Eres el agente especialista en **infraestructura** del equipo Prolongo, trabajando +en el repo `INFRAESTRUCTURA`. + +**Skill obligatorio:** carga el skill `infraestructura` y sigue sus instrucciones. + +**Ambito (Regla 31):** +- SI es infraestructura → este repo: watchers, automatizaciones, plugins de OpenCode, + OmniRoute, configuracion de servicios/VPS, red, drivers, tareas programadas. +- NO es infraestructura → repo `biblioteca_negocio_prolongo`: conocimiento de negocio, + laboratorio, empresa, memoria, guardias. + +**Reglas inviolables:** +1. Todo cambio de infraestructura se anota en el changelog (`scripts/infra_changelog.py add`) + con `transferible: si|no`. +2. NUNCA guardar secretos/tokens en el repo (van a `.env` o variables de usuario). +3. Los watchers viven en `scripts/` y su estado en `estado/`; se instalan con + `scripts/instalar_watchers.ps1`. +4. Regla 5 (mensajes): NUNCA enviar mensajes por ningun medio sin permiso explicito + doble factor. +5. Regla 15 (Jira): solo consulta GET. + +**Tareas tipicas:** +- Migrar/crear/actualizar watchers y automatizaciones. +- Registrar cambios de infraestructura en el changelog y avisar de pendientes de replicar. +- Mantener `estado/ESTADO_DIARIO.md` y `estado/pendientes_infra.json`. +- Verificar/relanzar tareas programadas (OmniRoute Watchdog, Telegram Watcher, Recordatorios Msgbox). +- Desplegar/verificar servicios del VPS (`vps/`). + +**Sincronizacion:** Engram (proyecto `INFRAESTRUCTURA`), Gitea (`jjminguez/INFRAESTRUCTURA`), +Obsidian (`03-IT/`). El protocolo `re` lee el changelog (paso 4b). diff --git a/.opencode/skill/infraestructura/SKILL.md b/.opencode/skill/infraestructura/SKILL.md new file mode 100644 index 0000000..5fdac29 --- /dev/null +++ b/.opencode/skill/infraestructura/SKILL.md @@ -0,0 +1,58 @@ +--- +name: infraestructura +description: Trabajo en el repo INFRAESTRUCTURA (infraestructura compartida Prolongo): watchers, OmniRoute, OpenCode, VPS, changelog global sobremesa<->portatil, estado y agentes. Use when the user works on infrastructure, watchers, scheduled tasks, OmniRoute, OpenCode plugins, VPS services, or the global changelog. +--- + +# INFRAESTRUCTURA — Infraestructura compartida + +## Cuando usar este repo (Regla 31) +- **Va aquí**: watchers, automatizaciones, plugins de OpenCode, OmniRoute, config de + servicios/VPS, red, drivers, tareas programadas, changelog global. +- **Va a `biblioteca_negocio_prolongo`**: conocimiento de negocio/laboratorio, memoria, + guardias, pendientes de negocio. + +## Estructura +- `estado/` — ESTADO_DIARIO.md, pendientes_infra.json, log y datos de los watchers. +- `scripts/` — watchers + herramientas (infra_changelog, infra_paths, instaladores). +- `omniroute/`, `opencode/`, `vps/` — servicios y su config. +- `AGENTS.md` — reglas del repo (leer siempre). + +## Flujo habitual +1. **Antes de tocar algo**: `git pull` y leer `AGENTS.md` + `estado/ESTADO_DIARIO.md`. +2. **Hacer el cambio**. +3. **Registrarlo en el changelog**: + ```powershell + python scripts\infra_changelog.py add --titulo "..." --categoria --transferible si|no --detalle "..." + ``` +4. **Si es un watcher/servicio**: reinstalar/repuntar con `scripts\instalar_watchers.ps1` + o `scripts\instalar_global.ps1`. +5. **Actualizar** `estado/ESTADO_DIARIO.md` y, si procede, `estado/pendientes_infra.json`. +6. **Guardar**: Engram (`mem_save`, proyecto `INFRAESTRUCTURA`) + `git add/commit/push`. + +## Watchers +Viven en `scripts/`; estado en `estado/`. Resuelven rutas con `scripts/infra_paths.py` +(estado local; `pendientes.json` y módulos compartidos desde el repo hermano +`biblioteca_negocio_prolongo`, override con `BIBLIOTECA_REPO`). + +```powershell +# Instalar tareas programadas (AtLogOn + reinicio si falla) +powershell -ExecutionPolicy Bypass -File scripts\instalar_watchers.ps1 + +# Verificar procesos +Get-CimInstance Win32_Process -Filter "Name='python.exe'" | + Where-Object { $_.CommandLine -match 'telegram_watcher|recordatorios_msgbox' } +``` + +## Changelog global +```powershell +python scripts\infra_changelog.py pendientes # que falta replicar en ESTE equipo +python scripts\infra_changelog.py aplicado +python scripts\infra_changelog.py estado +``` +El `re` de biblioteca lee esto (paso 4b) y avisa de lo pendiente del otro equipo. + +## Reglas +- NUNCA secretos/tokens en el repo. +- Regla 5: no enviar mensajes sin permiso + doble factor. +- Regla 15: Jira solo GET. +- Regla 24: NET/VB/4GL solo lectura. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1c3e33c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,99 @@ +# AGENTS.md — Repo INFRAESTRUCTURA (infraestructura compartida Prolongo) + +> Repo hermano de `biblioteca_negocio_prolongo`. Aquí vive **todo lo que hace +> funcionar** los equipos (sobremesa, portátil, VPS): watchers, automatizaciones, +> plugins, configuración de servicios y el **changelog global**. +> +> **Regla 31**: si algo sirve para "hacer funcionar" o automatizar la +> infraestructura → este repo. Si es "conocer el negocio" o memoria → biblioteca. + +## Estructura + +| Ruta | Qué es | +|------|--------| +| `changelog_infra.json` | Fuente de verdad del changelog global (por equipo) | +| `CHANGELOG_INFRA.md` | Versión legible (generada; no editar a mano) | +| `AGENTS.md` | Este fichero: reglas del repo de infraestructura | +| `estado/` | Estado y memoria de infraestructura (ver abajo) | +| `estado/ESTADO_DIARIO.md` | Diario de cambios de infraestructura | +| `estado/pendientes_infra.json` | Pendientes propios de infraestructura | +| `estado/telegram_mensajes.log` | Log del watcher de Telegram (runtime, gitignored) | +| `estado/recordatorios_msgbox.json` | Recordatorios locales del message box | +| `estado/telegram_tracked_users.json` | Config del watcher (contactos vigilados, etc.) | +| `scripts/infra_changelog.py` | Añadir / consultar / marcar cambios del changelog | +| `scripts/infra_paths.py` | Resolución de rutas compartidas (INFRAESTRUCTURA ↔ biblioteca) | +| `scripts/instalar_global.ps1` | Instalador global del equipo (OmniRoute + OpenCode + watchers) | +| `scripts/instalar_watchers.ps1` | Registra las tareas programadas de los watchers | +| `scripts/telegram_watcher.py` | Watcher de Telegram (chats → `estado/telegram_mensajes.log`) | +| `scripts/recordatorios_msgbox.py` | Recordatorios con message box de Windows | +| `scripts/recordatorio.cmd` | Comando corto `recordatorio` (listar/done/add/test/sync) | +| `omniroute/` | Vigilante y autostart de OmniRoute | +| `opencode/` | Plugin global de OpenCode | +| `vps/` | Configuración de servicios del VPS (Gitea, etc.) | + +## Reglas del repo + +1. **Solo se versiona infraestructura.** Nada de conocimiento de negocio. +2. Todo cambio de infraestructura se **anota en el changelog** (abajo) con + `transferible: si|no` para que el otro equipo sepa si debe replicarlo. +3. **Nunca** guardar secretos/tokens en el repo (van a `.env` o variables de + usuario; `.gitignore` ya cubre lo típico). +4. Los watchers se ejecutan como **tareas programadas** al iniciar sesión; si se + caen, relanzarlos a mano y, si hace falta, reinstalar con + `scripts/instalar_watchers.ps1`. +5. Este repo está registrado en **Engram** (proyecto `INFRAESTRUCTURA`), + **Gitea** (`jjminguez/INFRAESTRUCTURA`) y **Obsidian** (`03-IT/`). + +## Watchers + +Los watchers viven en `scripts/` y su estado en `estado/`: + +```powershell +# Instalar/reinstalar tareas programadas (AtLogOn + reinicio si falla) +powershell -ExecutionPolicy Bypass -File scripts\instalar_watchers.ps1 + +# Verificar que corren +Get-CimInstance Win32_Process -Filter "Name='python.exe'" | + Where-Object { $_.CommandLine -match 'telegram_watcher|recordatorios_msgbox' } +``` + +`telegram_watcher.py` y `recordatorios_msgbox.py` resuelven las rutas con +`scripts/infra_paths.py`: +- Estado → `INFRAESTRUCTURA/estado/` +- Datos de negocio (`pendientes.json`) y módulos aún compartidos + (`idd_channel`, `netatmo_tool`, `alarma_twilio`) → repo + `biblioteca_negocio_prolongo` (hermano; override con `BIBLIOTECA_REPO`). + +## Changelog global (sobremesa ↔ portátil ↔ VPS) + +```powershell +# Añadir un cambio (detecta el equipo automáticamente) +python scripts\infra_changelog.py add --titulo "..." --categoria watchers --transferible si --detalle "..." + +# Ver qué falta replicar en ESTE equipo +python scripts\infra_changelog.py pendientes + +# Marcar como aplicado aquí +python scripts\infra_changelog.py aplicado + +# Resumen +python scripts\infra_changelog.py estado +``` + +El protocolo `re` de cada equipo lee este fichero (paso 4b) y avisa de lo pendiente. + +## Instalación en un equipo nuevo (p. ej. el portátil) + +```powershell +git clone https://git.v-encore-lab.com/jjminguez/INFRAESTRUCTURA.git +cd INFRAESTRUCTURA +powershell -ExecutionPolicy Bypass -File scripts\instalar_global.ps1 +``` + +Prerrequisitos: Python en el PATH; Node.js + `npm i -g omniroute` para OmniRoute. +Copiar además `~/.omniroute` (credenciales), `~/.config/opencode/opencode.jsonc` y +`~/.telegram-agent` (sesión de Telegram) desde el otro equipo. + +> Al arrancar el portátil por primera vez, el `re` avisará de los cambios +> transferibles pendientes (incluida esta migración de watchers) y se aplican con +> `instalar_global.ps1`. diff --git a/CHANGELOG_INFRA.md b/CHANGELOG_INFRA.md index d2ab85b..c3c005c 100644 --- a/CHANGELOG_INFRA.md +++ b/CHANGELOG_INFRA.md @@ -8,6 +8,7 @@ - **2026-09-20-omniroute-arranque-fiable-tarea-programada-vigilante** [omniroute] OmniRoute arranque fiable (tarea programada + vigilante) (aplicado en: sobremesa) - **2026-09-20-omniroute-505-modelos-expuestos-en-opencode** [omniroute] OmniRoute: 505 modelos expuestos en OpenCode (aplicado en: sobremesa) - **2026-09-20-plugin-global-opencode-relanza-omniroute** [opencode] Plugin global OpenCode relanza OmniRoute (aplicado en: sobremesa) +- **2026-09-20-migracion-de-watchers-telegram-recordatorios-a-infraestructu** [watchers] Migracion de watchers (Telegram + recordatorios) a INFRAESTRUCTURA (aplicado en: sobremesa) ## Historial @@ -50,3 +51,11 @@ - **Transferible**: no - **Aplicado en**: VPS - **Detalle**: VPS: /opt/gitea-watchdog/gitea_watchdog.sh + timer systemd cada 2min (recrea gitea si falla 2 veces). Verificado parando gitea. Monitor Uptime-Kuma 'Gitea - Repositorios' corregido (URL tenia punto final) con alerta email SMTP. Ficheros en INFRAESTRUCTURA/vps/gitea/. + +### 2026-09-20 · Migracion de watchers (Telegram + recordatorios) a INFRAESTRUCTURA +- **id**: `2026-09-20-migracion-de-watchers-telegram-recordatorios-a-infraestructu` +- **Equipo**: sobremesa +- **Categoria**: watchers +- **Transferible**: si +- **Aplicado en**: sobremesa +- **Detalle**: telegram_watcher.py, recordatorios_msgbox.py y recordatorio.cmd movidos de biblioteca_negocio_prolongo/scripts a INFRAESTRUCTURA/scripts. Rutas resueltas con scripts/infra_paths.py: estado en INFRAESTRUCTURA/estado; pendientes.json y modulos compartidos (idd_channel, netatmo_tool, alarma_twilio) desde el repo hermano (override BIBLIOTECA_REPO). Tareas programadas nuevas (Telegram Watcher, Recordatorios Msgbox) via scripts/instalar_watchers.ps1 (integrado en instalar_global.ps1). En el portatil: git clone INFRAESTRUCTURA, ejecutar scripts/instalar_global.ps1, copiar ~/.telegram-agent y ~/.omniroute, y eliminar el autostart .lnk antiguo si existe. Estado migrado a INFRAESTRUCTURA/estado/. diff --git a/README.md b/README.md index 4eb2dd5..a85bfa8 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,31 @@ # INFRAESTRUCTURA Repo de infraestructura compartida entre equipos (sobremesa, portátil, VPS). +Todo lo que **hace funcionar** los equipos vive aquí (Regla 31); el conocimiento de +negocio vive en `biblioteca_negocio_prolongo`. ## Contenido | Ruta | Qué es | |------|--------| -| `changelog_infra.json` | Fuente de verdad del **changelog global de infraestructura** (por equipo) | +| `AGENTS.md` | Reglas del repo de infraestructura (leer siempre) | +| `changelog_infra.json` | Fuente de verdad del **changelog global** (por equipo) | | `CHANGELOG_INFRA.md` | Versión legible (generada; no editar a mano) | -| `scripts/infra_changelog.py` | Añadir / consultar / marcar cambios | +| `estado/` | Estado y memoria de infraestructura (diario, pendientes, log watcher) | +| `scripts/infra_changelog.py` | Añadir / consultar / marcar cambios del changelog | +| `scripts/infra_paths.py` | Resolución de rutas compartidas (INFRAESTRUCTURA ↔ biblioteca) | +| `scripts/telegram_watcher.py` | Watcher de Telegram → `estado/telegram_mensajes.log` | +| `scripts/recordatorios_msgbox.py` | Recordatorios con message box de Windows | +| `scripts/recordatorio.cmd` | Comando corto `recordatorio` | +| `scripts/instalar_watchers.ps1` | Registra las tareas programadas de los watchers | +| `scripts/instalar_global.ps1` | Instalador global del equipo (OmniRoute + OpenCode + watchers) | | `omniroute/omniroute_watchdog.ps1` | Vigilante de OmniRoute (arranque + autocuración) | | `omniroute/install_omniroute_autostart.ps1` | Registra la tarea programada `OmniRoute Watchdog` | | `opencode/omniroute-bootstrap.ts` | Plugin GLOBAL de OpenCode: relanza OmniRoute al arrancar OpenCode | -| `scripts/instalar_global.ps1` | Instala el plugin global y la tarea de arranque | +| `vps/` | Configuración de servicios del VPS (Gitea, etc.) | +| `.opencode/` | Agente `infra-agente` y skill `infraestructura` | -## Changelog global (sobremesa ↔ portátil) +## Changelog global (sobremesa ↔ portátil ↔ VPS) Registra cada cambio de infraestructura y si es transferible al otro equipo: @@ -32,7 +43,16 @@ python scripts/infra_changelog.py aplicado python scripts/infra_changelog.py estado ``` -El protocolo `re` de cada equipo lee este fichero y avisa de lo pendiente. +El protocolo `re` de cada equipo lee este fichero (paso 4b) y avisa de lo pendiente. + +## Watchers + +Viven en `scripts/` y su estado en `estado/`. Se instalan como tareas programadas +(AtLogOn + reinicio si falla): + +```powershell +powershell -ExecutionPolicy Bypass -File scripts/instalar_watchers.ps1 +``` ## Instalación en un equipo nuevo (p. ej. el portátil) @@ -43,4 +63,11 @@ powershell -ExecutionPolicy Bypass -File scripts/instalar_global.ps1 ``` Prerrequisitos de OmniRoute: Node.js + `npm i -g omniroute`, y copiar `~/.omniroute` -(credenciales) y `~/.config/opencode/opencode.jsonc`. +(credenciales) y `~/.config/opencode/opencode.jsonc`. Para el watcher de Telegram, +copiar también `~/.telegram-agent` (sesión). + +## Integración + +- **Engram**: proyecto `INFRAESTRUCTURA` (memoria del asistente). +- **Gitea**: `jjminguez/INFRAESTRUCTURA` (`git.v-encore-lab.com`). +- **Obsidian**: documentación en `03-IT/` del vault `biblioteca_prolongo`. diff --git a/changelog_infra.json b/changelog_infra.json index 8fdb6f3..8ab1ee5 100644 --- a/changelog_infra.json +++ b/changelog_infra.json @@ -60,6 +60,18 @@ "VPS" ], "detalle": "VPS: /opt/gitea-watchdog/gitea_watchdog.sh + timer systemd cada 2min (recrea gitea si falla 2 veces). Verificado parando gitea. Monitor Uptime-Kuma 'Gitea - Repositorios' corregido (URL tenia punto final) con alerta email SMTP. Ficheros en INFRAESTRUCTURA/vps/gitea/." + }, + { + "id": "2026-09-20-migracion-de-watchers-telegram-recordatorios-a-infraestructu", + "fecha": "2026-09-20", + "equipo": "sobremesa", + "categoria": "watchers", + "titulo": "Migracion de watchers (Telegram + recordatorios) a INFRAESTRUCTURA", + "transferible": true, + "aplicado_en": [ + "sobremesa" + ], + "detalle": "telegram_watcher.py, recordatorios_msgbox.py y recordatorio.cmd movidos de biblioteca_negocio_prolongo/scripts a INFRAESTRUCTURA/scripts. Rutas resueltas con scripts/infra_paths.py: estado en INFRAESTRUCTURA/estado; pendientes.json y modulos compartidos (idd_channel, netatmo_tool, alarma_twilio) desde el repo hermano (override BIBLIOTECA_REPO). Tareas programadas nuevas (Telegram Watcher, Recordatorios Msgbox) via scripts/instalar_watchers.ps1 (integrado en instalar_global.ps1). En el portatil: git clone INFRAESTRUCTURA, ejecutar scripts/instalar_global.ps1, copiar ~/.telegram-agent y ~/.omniroute, y eliminar el autostart .lnk antiguo si existe. Estado migrado a INFRAESTRUCTURA/estado/." } ] } diff --git a/estado/ESTADO_DIARIO.md b/estado/ESTADO_DIARIO.md new file mode 100644 index 0000000..440a216 --- /dev/null +++ b/estado/ESTADO_DIARIO.md @@ -0,0 +1,50 @@ +# ESTADO DIARIO — INFRAESTRUCTURA + +> Diario de cambios de infraestructura (sobremesa / portátil / VPS). +> La entrada más reciente va ARRIBA. Para el diario de negocio, ver +> `biblioteca_negocio_prolongo/ESTADO_DIARIO.md`. + +--- + +## 20 septiembre 2026 (domingo) — Migración de watchers a INFRAESTRUCTURA + +### Tareas realizadas +- **Watchers migrados** de `biblioteca_negocio_prolongo/scripts/` a `INFRAESTRUCTURA/scripts/`: + - `telegram_watcher.py` (chats → `estado/telegram_mensajes.log`) + - `recordatorios_msgbox.py` (message box; datos en `estado/recordatorios_msgbox.json`) + - `recordatorio.cmd` (comando corto `recordatorio`) +- **Resolución de rutas** con `scripts/infra_paths.py`: estado en `INFRAESTRUCTURA/estado/`, + datos de negocio (`pendientes.json`) y módulos aún compartidos (`idd_channel`, + `netatmo_tool`, `alarma_twilio`) desde el repo hermano `biblioteca_negocio_prolongo` + (override con `BIBLIOTECA_REPO`). +- **Tareas programadas** nuevas vía `scripts/instalar_watchers.ps1`: `Telegram Watcher` + y `Recordatorios Msgbox` (AtLogOn + reinicio si falla). Sustituyen al autostart por + `.lnk` (que no existía en el sobremesa). +- **Estado migrado** a `INFRAESTRUCTURA/estado/`: log, `.watcher_last_id`, + `telegram_tracked_users.json`, `.netatmo_last_alert.json`, `recordatorios_msgbox.json`. +- **Referencias actualizadas** en `biblioteca_negocio_prolongo`: AGENTS.md (Regla 6), + skills `telegram-procesamiento` y `protocolo-re`, agente `telegram-agente.md`, + `INSTALACION_OPENCODE.md`, `scripts/telegram_relogin.py`. +- **Estructura nueva** en este repo: `AGENTS.md`, `estado/`, `.opencode/agent/`, + `.opencode/skill/`, `.gitignore`. +- **Registrado** en Engram (proyecto `INFRAESTRUCTURA`) y en Obsidian (`03-IT/`). + +### Archivos modificados +- `INFRAESTRUCTURA/scripts/{telegram_watcher.py,recordatorios_msgbox.py,recordatorio.cmd,infra_paths.py,instalar_watchers.ps1,instalar_global.ps1}` +- `INFRAESTRUCTURA/estado/*`, `INFRAESTRUCTURA/AGENTS.md`, `INFRAESTRUCTURA/.gitignore`, `README.md` +- `biblioteca_negocio_prolongo/{AGENTS.md,INSTALACION_OPENCODE.md,.opencode/skill/*,.opencode/agent/telegram-agente.md,scripts/telegram_relogin.py}` +- Eliminados de biblioteca: `scripts/telegram_watcher.py`, `scripts/recordatorios_msgbox.py`, `scripts/recordatorio.cmd` + +### Descubrimientos +- `wmic` no existe en Windows 11 nuevo → usar CIM (PowerShell). +- Los watchers dependen de módulos que siguen en biblioteca porque los **bots del VPS** + los importan desde allí (`idd_channel`, `netatmo_tool`, `alarma_twilio`). Pendiente de + centralizar en fase 2 (requiere redeploy del VPS). +- El autostart por `.lnk` (`recordatorio_msgbox.lnk`) no existía en el sobremesa; se + sustituye por tareas programadas, más robustas. + +### Pendiente para el PORTÁTIL +- Al arrancar: `git clone` de `INFRAESTRUCTURA`, ejecutar + `scripts\instalar_global.ps1` y marcar en el changelog + `aplicado 2026-09-20-migracion-watchers-a-infraestructura`. +- Copiar `~/.telegram-agent` (sesión) y `~/.omniroute` (credenciales) desde el sobremesa. diff --git a/estado/pendientes_infra.json b/estado/pendientes_infra.json new file mode 100644 index 0000000..011a7b1 --- /dev/null +++ b/estado/pendientes_infra.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "proyecto": "INFRAESTRUCTURA", + "actualizado": "2026-09-20", + "total_pending": 4, + "pending": [ + { + "id": 1, + "task": "Fase 2: centralizar en INFRAESTRUCTURA los modulos aun compartidos (idd_channel, netatmo_tool, alarma_twilio) y redeployar los bots del VPS", + "categoria": "scripts", + "responsable": "yo", + "transferible": true, + "notas": "Ahora se resuelven desde biblioteca_negocio_prolongo via infra_paths.bootstrap_biblioteca_paths(). Requiere actualizar los bots del VPS (/opt) antes de moverlos." + }, + { + "id": 2, + "task": "Migrar por lotes el resto de scripts de infraestructura de biblioteca_negocio_prolongo/scripts a INFRAESTRUCTURA (Regla 31: cuando se toquen)", + "categoria": "scripts", + "responsable": "yo", + "transferible": true, + "notas": "Candidatos: aviso_sonoro.py, jira_*_watcher.py, sync_*, telegram_* (no VPS), watchdog_obsidian_sync.ps1, install_service.ps1." + }, + { + "id": 3, + "task": "Portatil: clonar INFRAESTRUCTURA, ejecutar instalar_global.ps1 y marcar aplicado 2026-09-20-migracion-watchers-a-infraestructura", + "categoria": "otros", + "responsable": "yo", + "transferible": false, + "notas": "Al arrancar el portatil el re lo avisara (paso 4b). Copiar ~/.telegram-agent y ~/.omniroute." + }, + { + "id": 4, + "task": "Eliminar el autostart antiguo (recordatorio_msgbox.lnk) si existiera, tras instalar las tareas programadas", + "categoria": "otros", + "responsable": "yo", + "transferible": true, + "notas": "En el sobremesa no existia; en el portatil hay que comprobar la carpeta Startup." + } + ] +} diff --git a/estado/recordatorios_msgbox.json b/estado/recordatorios_msgbox.json new file mode 100644 index 0000000..e53b900 --- /dev/null +++ b/estado/recordatorios_msgbox.json @@ -0,0 +1,57 @@ +{ + "recordatorios": [ + { + "id": 1, + "texto": "Enviar a Romero: recepcion jamones faccsa + controles temperatura Dompal", + "fecha": "diario", + "hora": "12:00", + "done": true + }, + { + "id": 2, + "texto": "Acabar programa de mantenimiento de incentivos por empleado", + "fecha": "diario", + "hora": "13:00", + "done": true + }, + { + "id": 3, + "texto": "Hablar con Fernandez para que implante la rama codex_im26_113_conciliacion_banking_play en NET (pendiente #108)", + "fecha": "2026-09-01", + "hora": "10:00", + "done": true, + "pendiente_id": 108 + }, + { + "id": 4, + "texto": "Identificar quien es Joaquin (ext 1120) — Oscar pasara el listin; reintentar llamada (pendiente #109)", + "fecha": "2026-09-01", + "hora": "10:30", + "done": true, + "pendiente_id": 109 + }, + { + "id": 5, + "texto": "[pendiente #110] IIT-130 Retirar tablas obsoletas transformaciones Compras (3-1-G y 3-1-H, tablas cp_def_transf_cab/lin y cp_transf_cab/lin) — Oscar lo asigna a Juanjo para revisar y reasignar a quien lo lleve a cabo. Analisis en Google Docs (link en correo). Impacto en repos NET, 4GL y VB", + "fecha": "2026-09-01", + "hora": "12:00", + "done": true, + "pendiente_id": 110 + }, + { + "id": 6, + "texto": "[pendiente #111] Cerrar publicacion Calidad ISO: CQ017 (v33) y CQ097 (v17) UPDATE usu en cal_paginas_ope codopepag=4 + copiar PDFs correctos a C:\\inetpub\\wwwroot\\Calidad\\pdf + borrar .tmp de \\\\pc041\\paginas$ + plantear a Sistemas desajuste carpetas (dir_web_vigor)", + "fecha": "2026-09-01", + "hora": "09:00", + "done": true, + "pendiente_id": 111 + }, + { + "id": 7, + "texto": "DIA 7: TERMINAR IIT-130 (desarrollo) + ARREGLAR DISTRIBUCION para poder distribuirlo. PREGUNTAR A ROMERO por G-2-1 (Entrada Palets a Camara, pry_VE_Entrada_Congelar) y G-2-2 (Repetir Etiqueta Palet, pry_ve_reetiquetadopalets). Arreglar aplicativos: Z-2f. Revisar pendientes: api+web IIS (esperando ACamara), repaso bbdd, documento DeCA, Bankinplay caso Carlos Hebilla, habilitar JIT Visual.", + "fecha": "2026-09-07", + "hora": "08:00", + "done": true + } + ] +} \ No newline at end of file diff --git a/estado/telegram_tracked_users.json b/estado/telegram_tracked_users.json new file mode 100644 index 0000000..c0ef0b1 --- /dev/null +++ b/estado/telegram_tracked_users.json @@ -0,0 +1,15 @@ +{ + "contactos_vigilados": [ + "Romero", + "Fernandez", + "Dani", + "Oscar", + "Maribel" + ], + "avisar_canal_idd": true, + "silenciado": false, + "silenciado_hasta": 0, + "avisar_alexa": false, + "avisar_sonido_pc": false, + "cooldown_segundos": 60 +} \ No newline at end of file diff --git a/scripts/infra_changelog.py b/scripts/infra_changelog.py index 864f863..6a4778b 100644 --- a/scripts/infra_changelog.py +++ b/scripts/infra_changelog.py @@ -31,7 +31,7 @@ REPO = Path(__file__).resolve().parent.parent JSON_FILE = REPO / "changelog_infra.json" MD_FILE = REPO / "CHANGELOG_INFRA.md" -CATEGORIAS = ["omniroute", "opencode", "red", "servicios", "drivers", "memoria", "otros"] +CATEGORIAS = ["omniroute", "opencode", "watchers", "scripts", "red", "servicios", "drivers", "memoria", "otros"] # --- Deteccion de equipo --------------------------------------------------- diff --git a/scripts/infra_paths.py b/scripts/infra_paths.py new file mode 100644 index 0000000..7e84517 --- /dev/null +++ b/scripts/infra_paths.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""infra_paths.py — Resolucion de rutas compartidas del repo INFRAESTRUCTURA. + +Los scripts de infraestructura viven en `INFRAESTRUCTURA/scripts/` y necesitan: + - Estado runtime propio del repo -> `INFRAESTRUCTURA/estado/` + - Datos de negocio y modulos aun compartidos del repo + `biblioteca_negocio_prolongo` -> pendientes.json, idd_channel, netatmo_tool, alarma_twilio + +La ubicacion del repo de negocio se resuelve asi (por orden): + 1. Variable de entorno `BIBLIOTECA_REPO` + 2. Repo hermano de INFRAESTRUCTURA: `../biblioteca_negocio_prolongo` + +En el portatil la estructura de carpetas es la misma (`Documents/GitHub/...`), por lo +que el hermano funciona sin configurar nada; `BIBLIOTECA_REPO` permite sobreescribirlo. +""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +INFRA_ROOT = Path(__file__).resolve().parent.parent +SCRIPTS_DIR = INFRA_ROOT / "scripts" +ESTADO_DIR = INFRA_ROOT / "estado" + + +def biblioteca_repo() -> Path: + """Devuelve la ruta del repo de negocio (biblioteca_negocio_prolongo).""" + env = os.environ.get("BIBLIOTECA_REPO") + if env: + return Path(env).expanduser().resolve() + return (INFRA_ROOT.parent / "biblioteca_negocio_prolongo").resolve() + + +def bootstrap_biblioteca_paths() -> Path: + """Anade al sys.path los modulos que aun viven en el repo de negocio. + + Transitorio: `idd_channel`, `netatmo_tool` y `alarma_twilio` siguen en + biblioteca_negocio_prolongo porque los bots del VPS los importan desde alli. + Cuando se centralicen en INFRAESTRUCTURA, esta funcion quedara vacia. + """ + repo = biblioteca_repo() + for candidato in (repo, repo / "scripts"): + sp = str(candidato) + if candidato.exists() and sp not in sys.path: + sys.path.insert(0, sp) + return repo diff --git a/scripts/instalar_global.ps1 b/scripts/instalar_global.ps1 index d02843f..c5289b7 100644 --- a/scripts/instalar_global.ps1 +++ b/scripts/instalar_global.ps1 @@ -82,6 +82,10 @@ if (-not $NoStart) { Write-Host ' - Vigilante arrancado.' -ForegroundColor Yellow } +# 4) Watchers de infraestructura (Telegram + recordatorios) ----------------- +Write-Host '' +& (Join-Path $PSScriptRoot 'instalar_watchers.ps1') -NoStart:$NoStart + Write-Host '' Write-Host 'Listo. Comprobar:' -ForegroundColor Cyan Write-Host " Get-ScheduledTask -TaskName '$TaskName' | Get-ScheduledTaskInfo" diff --git a/scripts/instalar_watchers.ps1 b/scripts/instalar_watchers.ps1 new file mode 100644 index 0000000..d60aa3e --- /dev/null +++ b/scripts/instalar_watchers.ps1 @@ -0,0 +1,89 @@ +<# +.SYNOPSIS + Registra en el equipo actual las tareas programadas de los watchers de + infraestructura (Telegram + recordatorios con message box). + +.DESCRIPTION + Los watchers viven ahora en `INFRAESTRUCTURA/scripts/` (centralizados; antes + en biblioteca_negocio_prolongo/scripts/). Este instalador crea/actualiza dos + tareas programadas que arrancan al iniciar sesion y se reinician si fallan: + + 1. `Telegram Watcher` -> scripts\telegram_watcher.py + 2. `Recordatorios Msgbox` -> scripts\recordatorios_msgbox.py + + No requiere administrador. Idempotente: re-ejecutar tras un git pull. + +.NOTES + Repo: INFRAESTRUCTURA. Los datos de estado quedan en INFRAESTRUCTURA\estado\. +#> +[CmdletBinding()] +param( + [switch]$NoStart +) + +$ErrorActionPreference = 'Stop' + +$RepoRoot = Split-Path -Parent $PSScriptRoot +$PwshExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' +$UserId = "$env:USERDOMAIN\$env:USERNAME" + +# Resolver el interprete de Python +$Python = (Get-Command python -ErrorAction SilentlyContinue).Source +if (-not $Python) { throw 'No se encuentra python en el PATH.' } + +$Watchers = @( + @{ Task = 'Telegram Watcher'; Script = Join-Path $RepoRoot 'scripts\telegram_watcher.py' }, + @{ Task = 'Recordatorios Msgbox'; Script = Join-Path $RepoRoot 'scripts\recordatorios_msgbox.py' } +) + +Write-Host '== Instalando tareas de watchers (INFRAESTRUCTURA) ==' -ForegroundColor Cyan +Write-Host " - Python: $Python" +Write-Host " - Repo : $RepoRoot" + +foreach ($w in $Watchers) { + if (-not (Test-Path $w.Script)) { throw "No se encuentra el script: $($w.Script)" } + + if (Get-ScheduledTask -TaskName $w.Task -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $w.Task -Confirm:$false + } + + $Action = New-ScheduledTaskAction ` + -Execute $PwshExe ` + -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -Command `"& '$Python' '$($w.Script)'`"" ` + -WorkingDirectory $RepoRoot + + $Trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME + + $Settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -RestartCount 999 ` + -RestartInterval (New-TimeSpan -Minutes 1) ` + -MultipleInstances IgnoreNew + $Settings.ExecutionTimeLimit = 'PT0S' + $Settings.Hidden = $true + + $Principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Limited + + Register-ScheduledTask ` + -TaskName $w.Task ` + -Action $Action ` + -Trigger $Trigger ` + -Settings $Settings ` + -Principal $Principal ` + -Description "Watcher de infraestructura: $($w.Script)" ` + -Force | Out-Null + + Write-Host " - Tarea '$($w.Task)' registrada -> $($w.Script)" -ForegroundColor Green + + if (-not $NoStart) { + Start-ScheduledTask -TaskName $w.Task + Write-Host " arrancada." -ForegroundColor Yellow + } +} + +Write-Host '' +Write-Host 'Listo. Comprobar:' -ForegroundColor Cyan +Write-Host " Get-ScheduledTask -TaskName 'Telegram Watcher','Recordatorios Msgbox'" +Write-Host " Get-Content `"$RepoRoot\estado\telegram_mensajes.log`" -Tail 5" diff --git a/scripts/recordatorio.cmd b/scripts/recordatorio.cmd new file mode 100644 index 0000000..d39c916 --- /dev/null +++ b/scripts/recordatorio.cmd @@ -0,0 +1,31 @@ +@echo off +rem recordatorio.cmd — Comando corto para gestionar recordatorios con message box. +rem Uso: +rem recordatorio listar todos los recordatorios +rem recordatorio done marcar como terminado (ya no sale) +rem recordatorio add "texto" --hora 14:00 [--fecha diario|YYYY-MM-DD] +rem recordatorio test mostrar el cuadro ahora mismo +rem recordatorio sync crear recordatorios desde pendientes.json (tareas con hora) +set PY=python +set SCR=C:\Users\juanm\Documents\GitHub\INFRAESTRUCTURA\scripts\recordatorios_msgbox.py +if "%~1"=="" goto listar +if /i "%~1"=="done" ( + %PY% "%SCR%" --done %2 + goto fin +) +if /i "%~1"=="add" ( + %PY% "%SCR%" --add %2 --hora %3 --fecha %4 + goto fin +) +if /i "%~1"=="test" ( + %PY% "%SCR%" --test %2 + goto fin +) +if /i "%~1"=="sync" ( + %PY% "%SCR%" --sync + goto fin +) +if /i "%~1"=="listar" goto listar +:listar +%PY% "%SCR%" --listar +:fin diff --git a/scripts/recordatorios_msgbox.py b/scripts/recordatorios_msgbox.py new file mode 100644 index 0000000..704e470 --- /dev/null +++ b/scripts/recordatorios_msgbox.py @@ -0,0 +1,226 @@ +"""recordatorios_msgbox.py — Recordatorios con cuadros de dialogo (MessageBox) en Windows. + +Muestra un message box cuando la hora de un recordatorio llega (o ya ha pasado y no esta hecho). +- Si el usuario pulsa SI -> se marca como hecho (done=True) y no vuelve a salir. +- Si pulsa NO -> se pospone 5 minutos (snooze). +- Si se ejecuta con --done se marca como terminado (lo usa el asistente cuando el usuario dice "terminada"). +- Si se ejecuta con --listar muestra el estado de todos los recordatorios. + +Fichero de datos: recordatorios_msgbox.json (formato: id, texto, fecha [diario|YYYY-MM-DD], hora HH:MM, done). + +Uso: + python scripts/recordatorios_msgbox.py # bucle de monitorizacion (correr en background) + python scripts/recordatorios_msgbox.py --listar # listar estado + python scripts/recordatorios_msgbox.py --done # marcar como terminado + python scripts/recordatorios_msgbox.py --add "texto" --hora HH:MM [--fecha diario|YYYY-MM-DD] + python scripts/recordatorios_msgbox.py --test # mostrar el cuadro sin esperar a la hora + python scripts/recordatorios_msgbox.py --sync # crear recordatorios desde pendientes.json (tareas con campo hora) +""" +import argparse +import ctypes +import json +import sys +import time +from datetime import datetime +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import infra_paths # noqa: E402 (resolucion de rutas del repo INFRAESTRUCTURA) + +DATA = infra_paths.ESTADO_DIR / 'recordatorios_msgbox.json' +PENDIENTES = infra_paths.biblioteca_repo() / 'pendientes.json' + +# Constantes de MessageBoxW de Win32 +MB_OK = 0x0 +MB_YESNO = 0x4 +MB_ICONINFORMATION = 0x40 +MB_ICONQUESTION = 0x20 +MB_SYSTEMMODAL = 0x1000 +IDYES = 6 +IDNO = 7 + +SLEEP_SEG = 15 # cadencia de comprobacion en segundos +SNOOZE_MIN = 5 # minutos a posponer si pulsa NO + + +def cargar(): + try: + return json.loads(DATA.read_text(encoding='utf-8-sig')) + except FileNotFoundError: + return {'recordatorios': []} + + +def guardar(data): + DATA.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding='utf-8') + + +def fecha_hoy(): + return datetime.now().strftime('%Y-%m-%d') + + +def hora_ahora(): + return datetime.now().strftime('%H:%M') + + +def msgbox(texto, titulo, yesno=False): + """Muestra un cuadro de dialogo nativo de Windows. Devuelve IDYES/IDNO/1.""" + flags = MB_SYSTEMMODAL | MB_ICONINFORMATION + if yesno: + flags |= MB_YESNO + else: + flags |= MB_OK + return ctypes.windll.user32.MessageBoxW(None, texto, titulo, flags) + + +def es_hora(r): + """Comprueba si toca mostrar el recordatorio.""" + if r.get('done'): + return False + if r.get('fecha', 'diario') == 'diario': + return True + return r.get('fecha') <= fecha_hoy() + + +def run(): + data = cargar() + ultima_vez = {} # id -> 'HH:MM' ya mostrado hoy (o fecha mostrada) + print(f'[recordatorios_msgbox] Monitorizando {len(data["recordatorios"])} recordatorios. Ctrl+C para salir.') + while True: + data = cargar() + ahora = fecha_hoy() + ' ' + hora_ahora() + for r in data['recordatorios']: + if not es_hora(r): + continue + # evita repetir en la misma hora ya mostrada para ese id + clave = f"{r['id']}-{fecha_hoy()}-{r.get('hora')}" + if ultima_vez.get(clave): + continue + if hora_ahora() >= r.get('hora', ''): + ultima_vez[clave] = True + texto = r.get('texto', '') + res = msgbox(texto, f'RECORDATORIO {r.get("hora")}', yesno=True) + if res == IDYES: + r['done'] = True + guardar(data) + print(f'[{ahora}] #{r["id"]} marcado como HECHO: {texto[:60]}') + else: + # snooze: fuerza a no repetir en los proximos SNOOZE_MIN minutos + ultima_vez[clave] = True + print(f'[{ahora}] #{r["id"]} pospuesto (NO).') + time.sleep(SLEEP_SEG) + + +def listar(): + data = cargar() + if not data['recordatorios']: + print('No hay recordatorios.') + return + print(f'{"ID":>3} {"HORA":>5} {"FECHA":<12} {"ESTADO":<10} TAREA') + for r in sorted(data['recordatorios'], key=lambda x: (x.get('hora', ''), x.get('id', 0))): + estado = 'HECHO' if r.get('done') else 'PENDIENTE' + print(f'{r.get("id", 0):>3} {r.get("hora", ""):>5} {str(r.get("fecha", "")):<12} {estado:<10} {r.get("texto", "")[:70]}') + + +def marcar_done(i): + data = cargar() + for r in data['recordatorios']: + if r.get('id') == i: + r['done'] = True + guardar(data) + print(f'#{i} marcado como terminado. Ya no saldra mas.') + return + print(f'No existe el recordatorio #{i}') + + +def add(texto, hora, fecha): + data = cargar() + ids = [r.get('id', 0) for r in data['recordatorios']] + nid = max(ids) + 1 if ids else 1 + data['recordatorios'].append({'id': nid, 'texto': texto, 'fecha': fecha, 'hora': hora, 'done': False}) + guardar(data) + print(f'Recordatorio #{nid} anadido: {hora} [{fecha}] {texto}') + + +def test(i): + data = cargar() + r = next((x for x in data['recordatorios'] if x.get('id') == i), None) + if not r: + print(f'No existe el recordatorio #{i}') + return + res = msgbox(r.get('texto', ''), f'TEST RECORDATORIO {r.get("hora")}', yesno=True) + if res == IDYES: + r['done'] = True + guardar(data) + print(f'#{i} marcado como HECHO (test).') + + +def sync(): + """Crea recordatorios desde pendientes.json: tareas activas con campo 'hora' (HH:MM). + La fecha sale del campo 'date' del pendiente o 'diario' si es de hoy.""" + try: + data = cargar() + pends = json.loads(PENDIENTES.read_text(encoding='utf-8-sig')) + except FileNotFoundError: + print(f'[sync] No existe {PENDIENTES.name}. Nada que sincronizar.') + return + hoy = fecha_hoy() + creados = 0 + for p in pends.get('pending', []): + if p.get('done'): + continue + hora = p.get('hora') + if not hora: + continue + fecha_p = p.get('date', hoy) + texto = p.get('task', '') + # evita duplicados: mismo pendiente_id, o mismo texto y misma hora + pid = p.get('id') + if any(r.get('pendiente_id') == pid for r in data['recordatorios']): + continue + if any(r.get('texto') == texto and r.get('hora') == hora for r in data['recordatorios']): + continue + ids = [r.get('id', 0) for r in data['recordatorios']] + nid = max(ids) + 1 if ids else 1 + data['recordatorios'].append({ + 'id': nid, 'texto': f'[pendiente #{p.get("id")}] {texto}', + 'fecha': fecha_p if fecha_p else 'diario', 'hora': hora, 'done': False, + 'pendiente_id': p.get('id') + }) + creados += 1 + print(f'[sync] #{nid} creado desde pendiente #{p.get("id")}: {hora} {texto[:60]}') + if creados: + guardar(data) + print(f'[sync] Sincronizacion completa. {creados} recordatorio(s) nuevo(s).') + + +def main(): + ap = argparse.ArgumentParser(description='Recordatorios con message box') + ap.add_argument('--listar', action='store_true', help='listar estado de los recordatorios') + ap.add_argument('--done', type=int, metavar='ID', help='marcar recordatorio como terminado') + ap.add_argument('--add', metavar='TEXTO', help='anadir recordatorio (usa --hora y --fecha)') + ap.add_argument('--hora', default='12:00', help='hora HH:MM (por defecto 12:00)') + ap.add_argument('--fecha', default='diario', help='fecha YYYY-MM-DD o "diario" (por defecto diario)') + ap.add_argument('--test', type=int, metavar='ID', help='mostrar el cuadro ahora (sin esperar hora)') + ap.add_argument('--sync', action='store_true', help='crear recordatorios desde pendientes.json con hora') + args = ap.parse_args() + + if args.sync: + sync() + elif args.listar: + listar() + elif args.done is not None: + marcar_done(args.done) + elif args.add: + add(args.add, args.hora, args.fecha) + elif args.test is not None: + test(args.test) + else: + run() + + +if __name__ == '__main__': + try: + sys.stdout.reconfigure(encoding='utf-8') + except Exception: + pass + main() diff --git a/scripts/telegram_watcher.py b/scripts/telegram_watcher.py new file mode 100644 index 0000000..27841d3 --- /dev/null +++ b/scripts/telegram_watcher.py @@ -0,0 +1,482 @@ +""" +telegram_watcher.py — Muestra mensajes entrantes de Telegram en consola y gestiona canal IDD. +Avisos exclusivos al canal IDD: +- Aviso de conexión de Telegram al canal IDD +- Alertas de lectura (doble check) de contactos vigilados al canal IDD +- Solo contestar comandos dirigidos al canal IDD +- Soporte para silenciar/activar avisos (/silenciar, /activar) +""" +import asyncio, sys, os, json, time, re +from datetime import datetime, timezone +from pathlib import Path + +C = { + 'red': '\033[91m', 'green': '\033[92m', 'yellow': '\033[93m', + 'blue': '\033[94m', 'magenta': '\033[95m', 'cyan': '\033[96m', + 'white': '\033[97m', 'bold': '\033[1m', 'reset': '\033[0m', + 'bg_red': '\033[41m', +} + +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPT_DIR)) +import infra_paths # noqa: E402 (resolucion de rutas del repo INFRAESTRUCTURA) + +ESTADO_DIR = infra_paths.ESTADO_DIR +BIBLIOTECA_REPO = infra_paths.bootstrap_biblioteca_paths() +LOG_FILE = ESTADO_DIR / 'telegram_mensajes.log' +LAST_ID_FILE = ESTADO_DIR / '.watcher_last_id' +TRACKED_USERS_FILE = ESTADO_DIR / 'telegram_tracked_users.json' + +ALERT_KEYWORDS = ['guardia', 'crítico', 'urgente', 'caído', 'error', 'sin asignar', + 'RELEVANTE', 'incidencia', 'alarma', 'paro', 'parada'] + +ALARMA_COMMANDS = ['/alarma', '!alarma', '.alarma'] +ALARMA_TWILIO_DIR = BIBLIOTECA_REPO / 'alarma_twilio' + +SILENCIAR_COMMANDS = ['/silenciar', '!silenciar', '/mute', '!mute', '/pausar', '!pausar'] +ACTIVAR_COMMANDS = ['/activar', '!activar', '/unmute', '!unmute', '/reanudar', '!reanudar'] + +NETATMO_COMMANDS = [ + '/domotica', '!domotica', '/menu', '!menu', + '/netatmo', '!netatmo', '/ambiente', '!ambiente', '/clima', '!clima', '/sensores', '!sensores', + '/temperatura', '!temperatura', '/temp', '!temp', + '/humedad', '!humedad', '/hum', '!hum', + '/co2', '!co2', + '/ruido', '!ruido', + '/anunciar', '!anunciar', + '/alertas', '!alertas' +] + +IDD_CHAT_ID = os.environ.get('TELEGRAM_CHAT_ID_2', '-1003967773087') + +def load_tracked_config(): + defaults = { + "contactos_vigilados": ["Romero", "Fernandez", "Dani", "Oscar", "Maribel"], + "avisar_canal_idd": True, + "silenciado": False, + "silenciado_hasta": 0, + "avisar_alexa": False, + "avisar_sonido_pc": False, + "cooldown_segundos": 60 + } + if TRACKED_USERS_FILE.exists(): + try: + with open(TRACKED_USERS_FILE, "r", encoding="utf-8") as f: + c = json.load(f) + defaults.update(c) + except Exception: + pass + return defaults + +def save_tracked_config(c): + try: + with open(TRACKED_USERS_FILE, "w", encoding="utf-8") as f: + json.dump(c, f, indent=2) + except Exception as e: + print(f"Error guardando config tracked: {e}", file=sys.stderr) + +def is_silenciado(): + c = load_tracked_config() + if c.get("silenciado", False): + hasta = c.get("silenciado_hasta", 0) + if hasta == 0: + return True + if time.time() < hasta: + return True + else: + c["silenciado"] = False + c["silenciado_hasta"] = 0 + save_tracked_config(c) + return False + return False + +def play_beep(): + try: + import winsound + winsound.Beep(1000, 200) + except: + pass + +def send_to_idd(text, parse_mode="Markdown"): + """Envia mensaje estrictamente al canal IDD.""" + sys.path.insert(0, str(SCRIPT_DIR)) + try: + import idd_channel + return idd_channel.send_message(text) + except Exception as e: + print(f"Error enviando a IDD: {e}", file=sys.stderr) + return None + +def log_message(chat_name, sender_name, text, is_alert): + try: + entry = json.dumps({ + 'ts': datetime.now().isoformat(), + 'chat': chat_name, + 'sender': sender_name, + 'text': text[:500] if text else '', + 'alert': is_alert, + }, ensure_ascii=False) + with open(LOG_FILE, 'a', encoding='utf-8') as f: + f.write(entry + '\n') + except Exception as e: + print(f'{C["red"]}Error log: {e}{C["reset"]}') + +async def handle_silenciar_command(text): + c = load_tracked_config() + parts = text.strip().split() + segundos = 0 + tiempo_str = "de forma indefinida (hasta /activar)" + + if len(parts) > 1: + param = parts[1].lower() + m_h = re.match(r'^(\d+)\s*h$', param) + m_m = re.match(r'^(\d+)\s*m$', param) + if m_h: + horas = int(m_h.group(1)) + segundos = horas * 3600 + tiempo_str = f"por {horas} hora(s)" + elif m_m: + minutos = int(m_m.group(1)) + segundos = minutos * 60 + tiempo_str = f"por {minutos} minuto(s)" + + c["silenciado"] = True + c["silenciado_hasta"] = int(time.time() + segundos) if segundos > 0 else 0 + save_tracked_config(c) + + send_to_idd(f"🔕 *Avisos Silenciados*\nLos avisos de lectura y alertas en IDD han sido silenciados {tiempo_str}.\nUsa `/activar` para reanudar.") + +async def handle_activar_command(text): + c = load_tracked_config() + c["silenciado"] = False + c["silenciado_hasta"] = 0 + save_tracked_config(c) + send_to_idd("🔔 *Avisos Reactivados*\nLas notificaciones de lectura y alertas ambientales vuelven a estar activas en este canal.") + +async def handle_alarma_command(text, chat_name, sender_name, client, dialog): + parts = text.strip().split(maxsplit=2) + if len(parts) < 2: + send_to_idd("ℹ️ Usos:\n!alarma +34657866417\n!alarma +34657866417 Mensaje personalizado") + return + + numero = parts[1] + mensaje = parts[2] if len(parts) >= 3 else None + + env_path = ALARMA_TWILIO_DIR / '.env' + if env_path.exists(): + from dotenv import load_dotenv + load_dotenv(env_path) + + sys.path.insert(0, str(ALARMA_TWILIO_DIR)) + from config import validate_config + from twilio_service import TwilioAlarmaService + + errores = validate_config() + if errores: + send_to_idd("❌ Twilio no configurado:\n" + "\n".join(errores)) + return + + msg = f"📞 Llamando a {numero}..." + if mensaje: + msg += f"\n📝 Mensaje: {mensaje[:80]}" + send_to_idd(msg) + + def ejecutar(): + svc = TwilioAlarmaService() + sid = svc.lanzar_llamada(numero, mensaje) + estado, contestado = svc.esperar_resultado(sid) + return estado, contestado + + loop = asyncio.get_event_loop() + estado, contestado = await loop.run_in_executor(None, ejecutar) + + emoji_map = { + "completed": "✅", "busy": "📞", "no-answer": "📵", + "failed": "❌", "canceled": "🚫", "timeout": "⏰", + } + emoji = emoji_map.get(estado, "❓") + desc = f"{emoji} {numero}: {estado}" + if contestado: + desc += f" ({contestado})" + if contestado == "human": + desc += " 🎯 Humano detectado" + elif contestado and "machine" in contestado: + desc += " 🤖 Contestador automático" + + send_to_idd(desc) + +async def handle_netatmo_command(text, chat_name, sender_name, client, dialog): + cmd = text.strip().lower().split()[0] + sys.path.insert(0, str(SCRIPT_DIR)) + import netatmo_tool + + def consultar(): + try: + token = netatmo_tool.get_access_token() + data = netatmo_tool.get_homecoach_data(token) + sensores = netatmo_tool.extraer_sensores(data) + cfg = netatmo_tool.load_config() + return sensores, cfg, None + except Exception as e: + return None, None, str(e) + + loop = asyncio.get_event_loop() + sensores, cfg, error = await loop.run_in_executor(None, consultar) + + if error: + send_to_idd(f"⚠️ Error consultando Netatmo: {error}") + return + + if cmd in ('/temperatura', '!temperatura', '/temp', '!temp'): + msg = netatmo_tool.generar_mensaje_telegram("temp", sensores, cfg) + elif cmd in ('/humedad', '!humedad', '/hum', '!hum'): + msg = netatmo_tool.generar_mensaje_telegram("hum", sensores, cfg) + elif cmd in ('/co2', '!co2'): + msg = netatmo_tool.generar_mensaje_telegram("co2", sensores, cfg) + elif cmd in ('/ruido', '!ruido'): + msg = netatmo_tool.generar_mensaje_telegram("ruido", sensores, cfg) + elif cmd in ('/anunciar', '!anunciar'): + netatmo_tool.cmd_anunciar(sensores) + msg = "📢 Anuncio enviado a Alexa:\nTemperatura y calidad ambiental leídas por altavoz Echo." + elif cmd in ('/alertas', '!alertas'): + alertas_voz, alertas_tg = netatmo_tool.evaluar_alertas(sensores, cfg) + if alertas_tg: + msg = "🚨 PARÁMETROS FUERA DE RANGO:\n" + "\n".join(f"• {a}" for a in alertas_tg) + else: + msg = "✅ Todos los parámetros están dentro del rango seguro configurado." + else: + msg = netatmo_tool.generar_mensaje_telegram("menu", sensores, cfg) + + netatmo_tool.notificar_telegram(msg) + +async def main(): + try: + from telethon import TelegramClient, events + except ImportError: + print(f'{C["red"]}pip install telethon{C["reset"]}') + sys.exit(1) + + api_id = int(os.environ.get('TELEGRAM_API_ID', '0')) + api_hash = os.environ.get('TELEGRAM_API_HASH', '') + if not api_id or not api_hash: + import subprocess + try: + r1 = subprocess.run(['powershell','-Command','[Environment]::GetEnvironmentVariable(\"TELEGRAM_API_ID\",\"User\")'], capture_output=True, text=True) + r2 = subprocess.run(['powershell','-Command','[Environment]::GetEnvironmentVariable(\"TELEGRAM_API_HASH\",\"User\")'], capture_output=True, text=True) + api_id = int(r1.stdout.strip()) if r1.stdout.strip() else 0 + api_hash = r2.stdout.strip() + except: + pass + + if not api_id or not api_hash: + print(f'{C["red"]}Falta TELEGRAM_API_ID / TELEGRAM_API_HASH{C["reset"]}') + sys.exit(1) + + session_file = str(Path.home() / '.telegram-agent' / 'sessions' / 'watcher') + client = TelegramClient(session_file, api_id, api_hash) + await client.start(phone='+34657866417') + + me = await client.get_me() + print(f'{C["green"]}Conectado: {me.first_name} (@{me.username or "sin"}){C["reset"]}') + print(f'{C["cyan"]}Canal de avisos exclusivo: IDD ({IDD_CHAT_ID}){C["reset"]}') + print(f'{C["cyan"]}Log: {LOG_FILE}{C["reset"]}') + print(f'{"─" * 60}\n') + + # Enviar aviso de conexión al canal IDD + send_to_idd(f"🟢 *Telegram Conectado* ({me.first_name})\nMonitorizando lecturas de contactos y canal IDD.") + + # Recuperar ultimos IDs procesados por chat + last_ids = {} + if LAST_ID_FILE.exists(): + try: + last_ids = json.loads(LAST_ID_FILE.read_text()) + except: + pass + + # Registro de eventos de confirmacion de lectura (Doble Check) -> SOLO IDD + last_read_triggers = {} + @client.on(events.MessageRead(outbox=True)) + async def on_message_read(event): + try: + if is_silenciado(): + return + + c = load_tracked_config() + vigilados = c.get("contactos_vigilados", ["Romero", "Fernandez", "Dani", "Oscar", "Maribel"]) + + chat = await event.get_chat() + chat_title = getattr(chat, "title", "") or getattr(chat, "first_name", "") or str(event.chat_id) + username = getattr(chat, "username", "") or "" + + coincide = False + nombre_vig = chat_title + for v in vigilados: + v_clean = v.lower().strip() + if v_clean and (v_clean in chat_title.lower() or (username and v_clean in username.lower()) or v_clean in str(event.chat_id)): + coincide = True + nombre_vig = v + break + + if not coincide: + return + + now_t = time.time() + if (now_t - last_read_triggers.get(event.chat_id, 0)) < c.get("cooldown_segundos", 60): + return + last_read_triggers[event.chat_id] = now_t + + hora_now = datetime.now().strftime("%H:%M:%S") + print(f"\n{C['bg_red']}{C['white']} 👀 LEÍDO {C['reset']} {C['bold']}{C['green']}[{nombre_vig}]{C['reset']} {hora_now} ha leído tu mensaje en '{chat_title}'") + + entry = json.dumps({ + "ts": datetime.now().isoformat(), + "tipo": "LECTURA", + "contacto": nombre_vig, + "chat": chat_title + }, ensure_ascii=False) + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(entry + "\n") + + # Avisar exclusivamente al canal IDD + msg_idd = ( + f"👀 *LECTURA CONFIRMADA*\n" + f"━━━━━━━━━━━━━━━━━━━━━━\n" + f"• *Contacto:* `{nombre_vig}`\n" + f"• *Chat:* {chat_title}\n" + f"• *Hora:* {hora_now}" + ) + send_to_idd(msg_idd) + + except Exception as e: + pass + + print(f'{C["white"]}Escuchando... (Ctrl+C para salir){C["reset"]}\n') + + last_netatmo_check = 0 + + while True: + try: + # Comprobacion periodica de alertas Netatmo (cada 5 minutos = 300s) -> SOLO IDD + now = time.time() + if now - last_netatmo_check > 300: + last_netatmo_check = now + try: + if not is_silenciado(): + sys.path.insert(0, str(SCRIPT_DIR)) + import netatmo_tool + def check_alertas(): + tok = netatmo_tool.get_access_token() + dt = netatmo_tool.get_homecoach_data(tok) + sens = netatmo_tool.extraer_sensores(dt) + c = netatmo_tool.load_config() + return netatmo_tool.evaluar_alertas(sens, c), sens, c + + loop = asyncio.get_event_loop() + (voz, tg), sens, cfg = await loop.run_in_executor(None, check_alertas) + if voz: + disparar = False + cooldown_sec = cfg.get("cooldown_minutos", 30) * 60 + msg_voz = ". ".join(voz) + last_alert_file = ESTADO_DIR / ".netatmo_last_alert.json" + if last_alert_file.exists(): + try: + with open(last_alert_file, "r", encoding="utf-8") as f: + last_d = json.load(f) + if last_d.get("msg") != msg_voz or (now - last_d.get("timestamp", 0)) >= cooldown_sec: + disparar = True + except Exception: + disparar = True + else: + disparar = True + + if disparar: + # Notificar al canal IDD + alerta_html = ( + f"🚨 ALERTA DOMÓTICA — {sens[0].get('nombre', 'Netatmo')}\n" + f"━━━━━━━━━━━━━━━━━━━━━━\n" + + "\n".join(f"• {a}" for a in tg) + "\n" + f"━━━━━━━━━━━━━━━━━━━━━━\n" + f"Detectado a las {datetime.now().strftime('%H:%M:%S')}" + ) + netatmo_tool.notificar_telegram(alerta_html) + with open(last_alert_file, "w", encoding="utf-8") as f: + json.dump({"timestamp": now, "msg": msg_voz, "fecha": datetime.now().isoformat()}, f, indent=2) + except Exception as e: + pass + + dialogs = await client.get_dialogs(limit=30) + + for dialog in dialogs: + try: + dialog_key = str(dialog.id) + last_id = last_ids.get(dialog_key, 0) + messages = await client.get_messages(dialog, limit=5) + new_max = last_id + + for msg in reversed(messages): + if msg.id <= last_id or not msg.message: + continue + new_max = max(new_max, msg.id) + + chat_name = dialog.name or str(dialog.id) + sender = await msg.get_sender() + sender_name = getattr(sender, 'first_name', '') or getattr(sender, 'title', '') or str(msg.sender_id) if sender else '??' + text = msg.message + is_alert = any(kw in (text or '').lower() for kw in ALERT_KEYWORDS) + + alert_prefix = f'{C["bg_red"]}{C["white"]} ALERTA {C["reset"]} ' if is_alert else '' + chat_color = C['cyan'] if 'jira' in chat_name.lower() or 'informe' in chat_name.lower() else C['magenta'] if 'alerta' in chat_name.lower() else C['white'] + + ts = msg.date.strftime('%H:%M:%S') if msg.date else '--:--:--' + print(f'\n{alert_prefix}{C["bold"]}{chat_color}[{chat_name}]{C["reset"]} {ts} {C["yellow"]}{sender_name}{C["reset"]}') + for line in text.split('\n')[:15]: + print(f' {chat_color}│{C["reset"]} {line}') + if len(text.split('\n')) > 15: + print(f' {chat_color}│{C["reset"]} ...') + + log_message(chat_name, sender_name, text, is_alert) + if is_alert: + play_beep() + + # Solo procesar comandos si vienen del canal IDD o Saved Messages + is_idd_channel = (str(dialog.id) == str(IDD_CHAT_ID) or chat_name == "IDD" or "idd" in chat_name.lower()) + if text and is_idd_channel: + clean_text = text.strip() + if any(clean_text.startswith(cmd) for cmd in SILENCIAR_COMMANDS): + await handle_silenciar_command(clean_text) + elif any(clean_text.startswith(cmd) for cmd in ACTIVAR_COMMANDS): + await handle_activar_command(clean_text) + elif any(clean_text.startswith(cmd) for cmd in ALARMA_COMMANDS): + await handle_alarma_command(text, chat_name, sender_name, client, dialog) + elif any(clean_text.startswith(cmd) for cmd in NETATMO_COMMANDS): + await handle_netatmo_command(text, chat_name, sender_name, client, dialog) + + if new_max > last_id: + last_ids[dialog_key] = new_max + + except Exception as e: + err = str(e) + if 'FLOOD_WAIT' in err: + wait = int(''.join(c for c in err if c.isdigit()) or '5') + print(f'{C["yellow"]}Rate limit {wait}s...{C["reset"]}') + await asyncio.sleep(wait) + elif 'AUTH_KEY' not in err and 'database' not in err.lower(): + pass + + LAST_ID_FILE.write_text(json.dumps(last_ids)) + + except Exception as e: + err = str(e) + if 'FLOOD' in err: + print(f'{C["yellow"]}Flood, esperando 10s...{C["reset"]}') + else: + print(f'{C["red"]}Error: {err[:100]}{C["reset"]}') + + await asyncio.sleep(5) + + +if __name__ == '__main__': + try: + asyncio.run(main()) + except KeyboardInterrupt: + print(f'\n{C["yellow"]}Cerrado.{C["reset"]}')