Moltes famílies tenen familiars que viuen sols — avis, persones amb mobilitat reduïda, o qualsevol persona vulnerable — i la preocupació diària de "com estarà?" és constant. En aquest article veurem com construir un bot de Telegram que s'encarrega d'aquesta vigilància de forma automàtica, discreta i configurable per a múltiples persones alhora.

Què fa exactament el bot?

El sistema envia tres missatges al dia a la persona monitoritzada (a les 08:00, 15:00 i 21:00), analitza les respostes automàticament i escala al contacte d'emergència si detecta alguna anomalia. Tot amb un sol bot de Telegram i quatre fitxers Python.

Les situacions que gestiona automàticament:

  • No resposta en 60 minuts: envia un recordatori amable.
  • No resposta en 5 minuts més: avisa el contacte d'emergència.
  • Menció de caiguda o dolor: avís immediat al contacte, sense esperar cap timeout.
  • Tristesa acumulada o canvi de patró: avís al contacte amb el motiu detectat.

Requisits previs

Necessites Python 3.10 o superior i un compte de Telegram. Comprova que tens Python instal·lat:

python3 --version

Instal·la les dues dependències necessàries:

pip install python-telegram-bot apscheduler

Pas 1 — Crear el bot a Telegram

Obre Telegram i busca @BotFather. Escriu /newbot, dona-li un nom (ex: "Monitor Familiar") i un nom d'usuari acabat en _bot. BotFather et donarà un token — guarda'l, el necessitaràs al pas següent.

Pas 2 — Obtenir els chat_id de cada persona

Cada persona (i cada contacte d'emergència) ha d'enviar un missatge al bot. Després, visita aquesta URL al navegador substituint TOKEN pel teu:

https://api.telegram.org/botTOKEN/getUpdates

Al JSON que apareix, busca el camp "chat" → "id". Apunta aquell número per a cada persona i cada contacte d'emergència.

Pas 3 — Crear els fitxers del projecte

Crea una carpeta monitor-bot/ i afegeix-hi quatre fitxers. L'estructura queda així:

monitor-bot/
├── config.json      ← token, persones, horaris
├── bot.py           ← bot principal (arrencar aquest)
├── logger.py        ← guarda converses i anomalies
└── analyzer.py      ← detecta anomalies en el text

Pas 4 — Configurar config.json

Edita config.json amb el teu token i les dades de cada persona. Pots afegir tants usuaris com vulguis al array "usuaris":

{
  "telegram_bot_token": "EL_TEU_TOKEN",
  "horaris": {
    "mati":   "08:00",
    "migdia": "15:00",
    "nit":    "21:00"
  },
  "timeouts": {
    "primer_avis_minuts": 60,
    "segon_avis_minuts":  5
  },
  "llindar_anomalia_dies": 3,
  "usuaris": [
    {
      "id":  "usuari_1",
      "nom": "Maria",
      "chat_id": "111111111",
      "contacte_emergencia": {
        "nom":     "Joan",
        "chat_id": "222222222"
      }
    }
  ]
}

Pas 5 — Els tres mòduls Python

El fitxer logger.py gestiona el log. Crea una carpeta per usuari dins de log/ amb un JSON de converses i un CSV d'anomalies:

import json, csv, os
from datetime import datetime, timedelta

def _path_json(uid): return os.path.join("log", uid, "conversations.json")
def _path_csv(uid):  return os.path.join("log", uid, "anomalies.csv")
def _path_global():  return os.path.join("log", "anomalies_global.csv")

def init_logs(uid):
    directori = os.path.join("log", uid)
    os.makedirs(directori, exist_ok=True)
    if not os.path.exists(_path_json(uid)):
        with open(_path_json(uid), "w", encoding="utf-8") as f:
            json.dump([], f)
    if not os.path.exists(_path_csv(uid)):
        with open(_path_csv(uid), "w", newline="", encoding="utf-8") as f:
            csv.writer(f).writerow(["timestamp","persona","tipus_anomalia",
                                    "missatge_original","resposta","escalat","contacte_avisat"])
    if not os.path.exists(_path_global()):
        os.makedirs("log", exist_ok=True)
        with open(_path_global(), "w", newline="", encoding="utf-8") as f:
            csv.writer(f).writerow(["timestamp","usuari_id","persona","tipus_anomalia",
                                    "missatge_original","resposta","escalat","contacte_avisat"])

def guardar_conversa(uid, nom, hora, missatge, resposta, temps_min, anomalies, escalat):
    with open(_path_json(uid), "r", encoding="utf-8") as f:
        data = json.load(f)
    data.append({"timestamp": datetime.now().isoformat(), "usuari_id": uid,
                  "persona": nom, "hora_programada": hora, "missatge_enviat": missatge,
                  "resposta": resposta, "temps_resposta_min": temps_min,
                  "anomalies_detectades": anomalies, "escalat": escalat})
    with open(_path_json(uid), "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

def guardar_anomalia(uid, nom, tipus, missatge, resposta, escalat, contacte_avisat):
    ts = datetime.now().strftime("%Y-%m-%d %H:%M")
    with open(_path_csv(uid), "a", newline="", encoding="utf-8") as f:
        csv.writer(f).writerow([ts, nom, tipus, missatge, resposta or "", escalat, contacte_avisat])
    with open(_path_global(), "a", newline="", encoding="utf-8") as f:
        csv.writer(f).writerow([ts, uid, nom, tipus, missatge, resposta or "", escalat, contacte_avisat])

def llegir_historial_recent(uid, dies=7):
    path = _path_json(uid)
    if not os.path.exists(path): return []
    with open(path, "r", encoding="utf-8") as f:
        data = json.load(f)
    llindar = datetime.now() - timedelta(days=dies)
    return [e for e in data if datetime.fromisoformat(e["timestamp"]) > llindar]

El fitxer analyzer.py analitza el text de cada resposta buscant paraules clau i detecta patrons anòmals comparant l'historial recent:

from logger import llegir_historial_recent

KEYWORDS = {
    "caiguda": [
        "he caigut", "m'he caigut", "caiguda", "no puc aixecar",
        "estic a terra", "no em puc moure",
    ],
    "dolor_malaltia": [
        "em fa mal", "no em trobo bé", "estic malalt", "estic malalta",
        "febre", "em trobo fatal", "mal de cap", "no puc respirar",
    ],
    "tristesa": [
        "estic trist", "estic trista", "no tinc ganes",
        "estic cansat de tot", "no vull res", "em sento sola",
        "tot va malament", "estic deprimit",
    ],
}

POSITIVES = ["bé", "molt bé", "perfecte", "genial", "fenomenal",
             "content", "contenta", "alegre", "feliç", "descansat", "ok"]

def detectar_anomalies_text(text):
    if not text: return []
    tl = text.lower().strip()
    anomalies = [t for t, kws in KEYWORDS.items() if any(k in tl for k in kws)]
    if len(tl.split()) <= 3 and not any(p in tl for p in POSITIVES):
        anomalies.append("resposta_curta")
    return anomalies

def detectar_anomalia_patro(uid):
    hist = llegir_historial_recent(uid, dies=7)
    if len(hist) < 3: return {"alertar": False, "motiu": ""}
    recent = hist[-9:]
    curtes = sum(1 for e in recent if "resposta_curta" in e.get("anomalies_detectades", []))
    if curtes >= 6:
        return {"alertar": True, "motiu": f"Respostes curtes: {curtes} dels últims {len(recent)} missatges."}
    dies_tristes = set(e["timestamp"][:10] for e in hist if "tristesa" in e.get("anomalies_detectades", []))
    if len(dies_tristes) >= 3:
        return {"alertar": True, "motiu": f"Tristesa {len(dies_tristes)} dies seguits."}
    return {"alertar": False, "motiu": ""}

def anomalia_requereix_escalat_immediat(anomalies):
    return "caiguda" in anomalies or "dolor_malaltia" in anomalies

I finalment bot.py, el fitxer principal. Gestiona el scheduler, els timeouts asíncrons per a cada usuari i tota la lògica d'escalat:

import asyncio, json, random, logging
from datetime import datetime
from telegram import Bot
from telegram.ext import Application, MessageHandler, filters, ContextTypes
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from logger   import init_logs, guardar_conversa, guardar_anomalia
from analyzer import detectar_anomalies_text, detectar_anomalia_patro, anomalia_requereix_escalat_immediat

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.FileHandler("log/bot.log", encoding="utf-8"), logging.StreamHandler()])
log = logging.getLogger(__name__)

with open("config.json", encoding="utf-8") as f: CONFIG = json.load(f)
TOKEN    = CONFIG["telegram_bot_token"]
USUARIS  = CONFIG["usuaris"]
HORARIS  = CONFIG["horaris"]
TIMEOUT_1 = CONFIG["timeouts"]["primer_avis_minuts"] * 60
TIMEOUT_2 = CONFIG["timeouts"]["segon_avis_minuts"]  * 60
CHAT_ID_A_USUARI = {u["chat_id"]: u for u in USUARIS}

def missatges_hora(nom):
    return {
        HORARIS["mati"]:   [f"Bon dia, {nom}! 🌅 Com has dormit avui?",
                            f"Bon dia, {nom}! ☀️ Que tens de fer avui?"],
        HORARIS["migdia"]: [f"Hola, {nom}! 🍽️ Ja has dinat?",
                            f"Hola, {nom}! 😄 Que has dinat? Estava bo?"],
        HORARIS["nit"]:    [f"Bona nit, {nom}! 🌙 Ja has sopat?",
                            f"Hola, {nom}! 🌆 Com ha anat el dia?"],
    }

def missatges_seguiment(nom):
    return [f"Estàs bé, {nom}? 🙏 Necessites quelcom?",
            f"Hola {nom}! Volia saber si estàs bé. 💙"]

estats = {u["id"]: {"esperant_resposta": False, "hora_enviat": None,
                    "missatge_enviat": None, "hora_programada": None,
                    "task_timeout": None} for u in USUARIS}
bot = Bot(token=TOKEN)

async def enviar_missatge(text, chat_id):
    try:
        await bot.send_message(chat_id=chat_id, text=text)
    except Exception as e: log.error(f"Error: {e}")

async def avisar_contacte(usuari, motiu):
    nom = usuari["nom"]; c = usuari["contacte_emergencia"]
    text = (f"⚠️ AVÍS — {nom} no respon o necessita ajuda.\n"
            f"Motiu: {motiu}\n"
            f"Hora: {datetime.now().strftime('%H:%M del %d/%m/%Y')}\n"
            f"Sisplau comprova com està {nom}.")
    await enviar_missatge(text, c["chat_id"])

async def gestionar_timeout(usuari):
    uid = usuari["id"]; nom = usuari["nom"]; estat = estats[uid]
    await asyncio.sleep(TIMEOUT_1)
    if not estat["esperant_resposta"]: return
    await enviar_missatge(random.choice(missatges_seguiment(nom)), usuari["chat_id"])
    guardar_anomalia(uid, nom, "no_resposta_1h", estat["missatge_enviat"], None, False, False)
    await asyncio.sleep(TIMEOUT_2)
    if not estat["esperant_resposta"]: return
    await avisar_contacte(usuari, f"{nom} no respon des de les {estat['hora_programada']}.")
    guardar_conversa(uid, nom, estat["hora_programada"], estat["missatge_enviat"],
                     None, None, ["no_resposta_total"], True)
    guardar_anomalia(uid, nom, "no_resposta_total", estat["missatge_enviat"], None, True, True)
    estat["esperant_resposta"] = False

async def fer_checkin_usuari(usuari, hora):
    uid = usuari["id"]; estat = estats[uid]
    missatge = random.choice(missatges_hora(usuari["nom"])[hora])
    await enviar_missatge(missatge, usuari["chat_id"])
    estat.update({"esperant_resposta": True, "hora_enviat": datetime.now(),
                  "missatge_enviat": missatge, "hora_programada": hora})
    if estat["task_timeout"] and not estat["task_timeout"].done():
        estat["task_timeout"].cancel()
    estat["task_timeout"] = asyncio.create_task(gestionar_timeout(usuari))

async def fer_checkin_tots(hora):
    await asyncio.gather(*[fer_checkin_usuari(u, hora) for u in USUARIS])

async def gestionar_resposta(update, context: ContextTypes.DEFAULT_TYPE):
    chat_id = str(update.message.chat_id)
    usuari  = CHAT_ID_A_USUARI.get(chat_id)
    if not usuari: return
    uid = usuari["id"]; estat = estats[uid]
    text = update.message.text
    temps_min = int((datetime.now() - estat["hora_enviat"]).total_seconds() / 60) if estat["hora_enviat"] else None
    estat["esperant_resposta"] = False
    anomalies = detectar_anomalies_text(text)
    patro = detectar_anomalia_patro(uid)
    if patro["alertar"]: anomalies.append("canvi_patro")
    guardar_conversa(uid, usuari["nom"], estat.get("hora_programada","?"),
                     estat.get("missatge_enviat",""), text, temps_min, anomalies, bool(anomalies))
    if anomalia_requereix_escalat_immediat(anomalies):
        for t in ["caiguda", "dolor_malaltia"]:
            if t in anomalies:
                await avisar_contacte(usuari, f"{usuari['nom']} ha mencionat '{t}': \"{text}\"")
                guardar_anomalia(uid, usuari["nom"], t, estat.get("missatge_enviat",""), text, True, True)
        return
    if patro["alertar"]:
        await avisar_contacte(usuari, patro["motiu"])
        guardar_anomalia(uid, usuari["nom"], "canvi_patro", estat.get("missatge_enviat",""), text, True, True)
    for t in anomalies:
        if t not in ["caiguda","dolor_malaltia","canvi_patro"]:
            guardar_anomalia(uid, usuari["nom"], t, estat.get("missatge_enviat",""), text, False, False)

async def main():
    for u in USUARIS: init_logs(u["id"])
    app = Application.builder().token(TOKEN).build()
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, gestionar_resposta))
    scheduler = AsyncIOScheduler()
    for clau in ["mati", "migdia", "nit"]:
        h, m = [int(x) for x in HORARIS[clau].split(":")]
        scheduler.add_job(fer_checkin_tots, "cron", hour=h, minute=m, args=[HORARIS[clau]])
    scheduler.start()
    log.info(f"✅ Bot actiu! {len(USUARIS)} usuaris.")
    await app.run_polling()

if __name__ == "__main__": asyncio.run(main())

Pas 6 — Arrencar el bot

Des de la carpeta monitor-bot/, executa:

python3 bot.py

Hauries de veure al terminal:

🤖 Bot iniciant amb 1 usuaris configurats:
   → Maria (contacte: Joan)
✅ Bot actiu! Check-ins programats: 08:00 / 15:00 / 21:00

Mantenir el bot actiu 24/7

Per a ús real, configura un servei systemd perquè el bot s'arrenqui automàticament i es reiniciï si cau. Crea el fitxer /etc/systemd/system/monitor-bot.service:

[Unit]
Description=OpenClaw Monitor Bot
After=network.target

[Service]
WorkingDirectory=/ruta/completa/monitor-bot
ExecStart=/usr/bin/python3 bot.py
Restart=always
User=el_teu_usuari

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable monitor-bot
sudo systemctl start monitor-bot

Afegir més persones

El bot suporta N persones alhora. Per afegir-ne una de nova, simplement afegeix un bloc nou al array "usuaris" de config.json i reinicia el bot. Cada persona té el seu propi estat independent, la seva pròpia carpeta de log i el seu propi contacte d'emergència.

Reflexió final

El sistema és intencionadament discret: la persona rep missatges amables tres vegades al dia i respon amb una frase curta. Si tot va bé, no passa res més. Però si hi ha un problema, el bot actua sol. Per a famílies amb parents que viuen sols, pot ser una eina de tranquil·litat real sense necessitat de trucar cada dia.