update: recordatorio para reuniones grupo de telegram

This commit is contained in:
David Isaías Rosales Casillas 2025-06-23 10:49:19 -06:00
commit 2af5c522f9
18 changed files with 1437 additions and 0 deletions

View file

@ -6,3 +6,4 @@ Este repositorio contiene desarrollos y mejoras para Botija, el bot de Telegram
Index:
- [ToDo](todo)
- [Agenda](agenda)

132
agenda/README.md Normal file
View file

@ -0,0 +1,132 @@
# ToDo Bot
Gestiona las reuniones programadas relacionadas con temas de MxOS: permite crearlas, editarlas y eliminarlas. Además, notifica en el canal general las próximas reuniones y envía recordatorios automáticos a dicho canal con antelación de 60, 30, 15, 5 ,1 y 2 minutos antes del inicio de cada reunión.
Funciones:
|Acción |Comando |
|--- | --- |
|Agenda reunión |/agenda crear [título de la reunión] [fecha en dd-mm-aaaa] [HH:MM CST] |
|Mostrar reunión vigente |/agenda ls |
|Modificar reunión por titulo |/agenda modificar [id_agenda] -t [título de la reunión] |
|Modificar reunión por fecha |/agenda modificar [id_agenda] -f [fecha en dd-mm-aaaa] |
|Modificar reunión por hora |/agenda modificar [id_agenda] -h [HH:MM] |
|Eliminar reunión |/agenda [id agenda] |
## USO
### Python
Declarar variable de entorno para TOKEN de telegram
~~~bash
export TELEGRAM_BOT_TOKEN=<TOKEN>
export REUNION_URL=<URL general de reuniones>
~~~
Instalar las dependencias
~~~bash
pip install -r requirements.txt
~~~
Ejecutar:
1. Sistema CRUD
~~~bash
python crud_agenda.py
~~~
El bot estará en modo escucha hasta recibir un comando
~~~bash
[anduril@elendil todo]$ python crud_agenda.py
2025-06-01 11:49:07,297 - telegram.ext.Application - INFO - Application started
~~~
2. Sistema para checar cuando inicia la reunión
~~~bash
python recordatorio.py
~~~
El bot estará en modo escucha hasta recibir un comando
~~~bash
[anduril@elendil todo]$ python recordatorio.py
2025-06-01 11:49:07,297 - telegram.ext.Application - INFO - Application started
~~~
Al iniciar la aplicación, se creará una base de datos en sqlite llamada `agenda_bot.db` en el directorio `reunionesData`, la cual llevará el registro de todas las tareas a guardar.
Para detener el bot desde terminal presionar CTRL+C
>Nota: se recomienda ejecutar el script en una sesion de tmux o nohub para ejecucion del script en segundo plano
### Podman
Ejecutar los siguientes pasos con un usuario que no sea root
1. Instalar podman-compose
~~~bash
pip install podman-compose
~~~
2. Clonar el repositorio
3. Inicializar las varibles de entorno
~~~bash
export TELEGRAM_BOT_TOKEN=<TOKEN>
export REUNION_URL=<URL general de reuniones>
~~~
4. Ingresar al repositorio y construir la imagen de contenedor ejecutando el script `build.sh`
~~~bash
$ sh build.sh
~~~
5. Ejecutar el script `run.sh` para iniciar los contenedores
~~~bash
sh run.sh
~~~
>: nota: los archivos del volumen de podman pueden ser exportados:
>: `podman volume export <nombre_del_volumen> --output <ruta_del_archivo_tar>`
6. Verificar que esten corriendo los contenedores con `podman ps`:
~~~bash
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09f0b8a3d87c localhost/crud-agenda:latest 2 hours ago Up 2 hours 8080/tcp podman_crud_1
00a306d8f9a1 localhost/recordatorio:latest 2 hours ago Up 2 hours 8080/tcp podman_recordatorio_1
~~~
>Nota: La imagen de python ubi RHEL usa el workdir en /opt/app-root/src/
- Verifiar si esta recibiendo llamadas
~~~bash
podman logs <id_del_contenedor>
~~~
Ejemplo:
~~~bash
[devops@zoro todo]$ podman logs `c9d054b52482`
2025-06-16 19:48:59,784 - __main__ - INFO - Iniciando bot de notificaciones de eventos
2025-06-16 19:48:59,815 - __main__ - INFO - Minutos restantes: 2 para prueba con podman compose
~~~

Binary file not shown.

136
agenda/base_de_datos.py Normal file
View file

@ -0,0 +1,136 @@
#!/usr/bin/env python
"""
Este programa es software libre: usted puede redistribuirlo y/o modificarlo
bajo los términos de la Licencia Pública General de GNU publicada por la
Free Software Foundation, GPLv3.
Programa para hacer un todo list en topics de telegram, usando el framework de python-telegram-bot
Uso:
- Agenda reunión /agenda crear [Título de la reunión] [Fecha en dd-mm-aaaa] [HH:MM CST]
- Mostrar reunión vigente /agenda ls
- Modificar reunión por titulo /agenda modificar [id_agenda] -t [Título de la reunión]
- Modificar reunión por fecha /agenda modificar [id_agenda] -f [Fecha en dd-mm-aaaa]
- Modificar reunión por hora /agenda modificar [id_agenda] -h [HH:MM CST]
- Eliminar reunión /agenda [id agenda]
Precionar Ctrl-C para enviar señal de interrupcion del proceso en la linea de comandos
"""
import sqlite3
from typing import List, Tuple
class reunionesBaseDeDatos:
def __init__(self, nombre_db='reunionesData/agenda_bot.db') -> None:
self.conn =sqlite3.connect(nombre_db, check_same_thread=False)
self._crear_tablas()
def cerrar(self) -> None:
self.conn.close()
def _crear_tablas(self) -> None:
with self.conn:
#se uso varchar porque telegram en topico GENERAL tienen valor NONE
self.conn.execute('''
CREATE TABLE IF NOT EXISTS topics (
id varchar(100) PRIMARY KEY,
chat_id INTEGER not null
)
''')
self.conn.execute('''
CREATE TABLE IF NOT EXISTS reuniones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo varchar(255) not null,
fecha varchar(15) not null,
hora varchar(10) not null,
zona_horaria varchar(5) not null,
estatus BOOLEAN DEFAULT 1,
creacion DATETIME DEFAULT CURRENT_TIMESTAMP,
topic_id varchar(100) not null,
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE
)
''')
def guardar_reunion(self, topic_id: str, chat_id: str, titulo: str, fecha: str, hora: str, zona_horaria: str) -> None:
with self.conn:
#insertar tipic id si no existe
cursor = self.conn.execute('INSERT OR IGNORE INTO topics (id, chat_id) VALUES (?, ?)', (topic_id,chat_id,))
#insertar mensaje
cursor = self.conn. execute(
'INSERT INTO reuniones (topic_id, titulo, fecha, hora, zona_horaria ) VALUES (?, ?, ?, ?, ?)',
(topic_id, titulo, fecha, hora, zona_horaria))
return cursor.lastrowid
def mostrar_reuniones(self) -> List[Tuple[int, str]]:
with self.conn:
cursor = self.conn.execute(
'select id,titulo,fecha,hora,zona_horaria FROM reuniones'
)
return cursor.fetchall()
def traer_eventos(self, fecha: str) -> List[Tuple[int, str]]:
with self.conn:
cursor = self.conn.execute(
'select id,titulo,fecha,hora,zona_horaria from reuniones where fecha = ?',
(fecha,)
)
return cursor.fetchall()
def eliminar_evento(self, tarea_id: int) -> bool:
with self.conn:
cursor = self.conn.execute(
'DELETE from reuniones WHERE id = ?',
(tarea_id,)
)
return cursor.rowcount > 0
def modificar_titulo(self, reunion_id: int, nuevo_titulo: str) -> bool:
with self.conn:
cursor = self.conn.execute(
'UPDATE reuniones SET titulo = ? WHERE id = ?',
(nuevo_titulo, reunion_id)
)
return cursor.rowcount > 0
def modificar_fecha(self, reunion_id: int, nueva_fecha: str) -> bool:
with self.conn:
cursor = self.conn.execute(
'UPDATE reuniones SET fecha = ? WHERE id = ?',
(nueva_fecha, reunion_id)
)
return cursor.rowcount > 0
def modificar_hora(self, reunion_id: int, nueva_hora: str) -> bool:
with self.conn:
cursor = self.conn.execute(
'UPDATE reuniones SET hora = ? WHERE id = ?',
(nueva_hora, reunion_id)
)
return cursor.rowcount > 0
def obtener_chat_id(self) -> list:
with self.conn:
cursor = self.conn.execute(
'SELECT chat_id FROM topics LIMIT 1'
)
return cursor.fetchone()

320
agenda/crud_agenda.py Normal file
View file

@ -0,0 +1,320 @@
#!/usr/bin/env python
# pylint: disable=unused-argument
"""
Este programa es software libre: usted puede redistribuirlo y/o modificarlo
bajo los términos de la Licencia Pública General de GNU publicada por la
Free Software Foundation, GPLv3.
Programa para guardar reuniones de MxOS y avisar en el grupo de telegram cada 60, 30, 15 min hasta que inicie la sesion, usando el framework de python-telegram-bot
Uso:
- Agenda reunión /agenda crear [Título de la reunión] [Fecha en dd-mm-aaaa] [HH:MM CST]
- Mostrar reunión vigente /agenda ls
- Modificar reunión por titulo /agenda modificar [id_agenda] -t [Título de la reunión]
- Modificar reunión por fecha /agenda modificar [id_agenda] -f [Fecha en dd-mm-aaaa]
- Modificar reunión por hora /agenda modificar [id_agenda] -h [HH:MM CST]
- Eliminar reunión /agenda [id agenda]
Precionar Ctrl-C para enviar señal de interrupcion del proceso en la linea de comandos
"""
import logging
import os, re, time
from datetime import datetime
from threading import Thread
from base_de_datos import reunionesBaseDeDatos
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
ConversationHandler,
MessageHandler,
Updater,
)
# Habilitar logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
# set higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("httpx").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
# crear directorio para base de datos
if not os.path.exists('reunionesData'):
print('Creando directorio para base de datos en ruta actual! \n')
os.mkdir('reunionesData')
#inicializar base de datos
db = reunionesBaseDeDatos()
# funcion para comando /agenda
async def agenda(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Iniciar conversacion y preguntar al usuario por el evento a crear."""
if not context.args:
await ayuda(update,context)
return
subcomando = context.args[0]
argumentos = context.args[1:]
if subcomando == "crear":
#Guardar evento
await crear(update,context, argumentos)
elif subcomando == "ls":
await ls(update,context)
elif subcomando == "rm":
if re.search("[0-9]+", str(argumentos[0])):
await rm(update,context, int(argumentos[0]))
else:
await update.message.reply_text("/todo rm [Número de tarea a eliminar] \n")
elif subcomando == "modificar":
await modificar(update,context,argumentos)
elif subcomando == "ayuda":
await ayuda(update,context)
else:
await ayuda(update,context)
async def ayuda(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Opciones: \n" +
"/agenda crear [título de la reunión] [fecha en dd-mm-aaa] [HH:MM] CST \n" +
"/agenda modificar [id_eveto] -t [Nuevo titulo] \t Modifica evento por título\n" +
"/agenda modificar [id_eveto] -h [HH:MM] \t Modifica evento por hora \n" +
"/agenda modificar [id_eveto] -f [dd-mm-aaa] \t Modifica evento por fecha \n" +
"/agenda ls \t\t lista eventos\n" +
"/agenda rm [Número de evento a eliminar] \t\tElimina evento\n")
return ConversationHandler.END
def formato_fecha(fecha: str) -> bool:
if re.search("^(0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-(\\d{4})$", fecha):
return True
else:
return False
def verificar_fecha(fecha: str) -> bool:
if datetime.strptime(fecha, "%d-%m-%Y").date() >= datetime.now().date():
return True
else:
return False
def formato_hora(hora: str) -> bool:
if re.search("^([01][0-9]|2[0-3]):[0-5][0-9]$", hora):
return True
else:
return False
def formato_zona(zona_horaria: str) -> bool:
if re.search("UTC$|utc$|CST$|cst$", zona_horaria):
return True
else:
return False
async def crear(update: Update, context: ContextTypes.DEFAULT_TYPE, argumentos) -> int:
# Quien envia la solicitud
user = update.message.from_user
#Traer el topic id
topic_id = update.message.message_thread_id
# traer el chat_id
chat_id = update.message.chat.id
#enviar mensaje si no se pasa nada al comando
if len(argumentos) <= 0:
await update.message.reply_text("/agenda crear [título de la reunión] [fecha en dd-mm-aaa] [HH:MM] CST \n")
return
# Parsear los datos de entrada
zona_horaria = argumentos.pop()
hora = argumentos.pop()
fecha = argumentos.pop()
titulo = " ".join(argumentos)
logger.info("%s ingreso: %s", user.first_name, titulo)
logger.info("Topic id es:" + str(topic_id))
#Verificar si el formato de la fecha es valida
if formato_fecha(fecha):
if verificar_fecha(fecha): #verificar si la fecha no esta en el pasado
print("Día del evento: "+ fecha)
else:
await update.message.reply_text("Fecha no valida, el evento está en el pasado. \n\n")
logger.info(f"El usuario {user.first_name} introdujo {fecha} hoy es {datetime.now().date()}")
print("Fecha no valida, el evento está en el pasado.")
return
else:
await update.message.reply_text("Fecha no válida, introduce dd-mm-aaaa \n\n")
return
#Verifiar si hora es valida
if formato_hora(str(hora)):
print("Hora a guardar: "+ hora)
else:
await update.message.reply_text("Formato de hora no válido, introduce HH:MM \n\n")
return
#Verificar si es zona horaria valida
if formato_zona(zona_horaria):
print("Zona horaria: "+ zona_horaria)
else:
await update.message.reply_text("Zona horaria no válida, introduce utc|UTC|cst|CST \n\n")
return
if str(topic_id) == "None":
topic_id = "General"
#Guardar evento
db.guardar_reunion(topic_id, chat_id, titulo, fecha, hora, zona_horaria)
await update.message.reply_text("¡Listo!, la reunion fue guardada\n\n")
return ConversationHandler.END
async def ls(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Listar las reuniones guardadas"""
#esto trae el usuario
user = update.message.from_user
#Traer todas las reuniones
topic_messages = db.mostrar_reuniones()
#Juntar las columnas reunion fecha hora y zona horaria
reuniones = []
for fila in topic_messages:
reuniones.append(("Reunion "+str(fila[1]) +" el día "+str(fila[2]) +" a las "+ str(fila[3]+" "+str(fila[4]))))
if len(reuniones) > 0:
mensaje_final = ''
for mensaje in range(0, len(reuniones)):
mensaje_final += "\n"+str(mensaje)+".- "+reuniones[mensaje]
logger.info(user.first_name + " consulto reuniones, las cuales son:")
logger.info(mensaje_final + "\n")
await update.message.reply_text("Reuniones a recordar:" + mensaje_final)
else:
await update.message.reply_text("Todavia no hay reuniones, ¿quieres agendar una? \n\n /agenda crear [título de la reunión] [fecha en dd-mm-aaa] CST")
return ConversationHandler.END
async def rm(update: Update, context: ContextTypes.DEFAULT_TYPE, numero_de_mensaje) -> int:
#Elimina evento agendado derivado del response de /agenda ls
#esto trae el usuario
user = update.message.from_user
#obteniendo mensajes
topic_messages = db.mostrar_reuniones()
if (int(numero_de_mensaje) >= 0 and int(numero_de_mensaje) < len(topic_messages)):
logger.info(topic_messages[int(numero_de_mensaje)])
mensaje_a_borrar = topic_messages[int(numero_de_mensaje)]
id_mensaje_a_borrar = mensaje_a_borrar[0]
logger.info(id_mensaje_a_borrar)
if db.eliminar_evento(id_mensaje_a_borrar):
logger.info(user.first_name + " eliminió reunion")
await update.message.reply_text("Recordatorio eliminado :) \n")
elif len(topic_messages) == 0:
await update.message.reply_text("Todavia no hay eventos, ¿quieres agendar uno? \n\n /agenda crear [título de la reunión] [fecha en dd-mm-aaa] [HH:MM] CST")
else:
await update.message.reply_text("Introduce un ID valido :) \n")
return ConversationHandler.END
async def modificar(update: Update, context: ContextTypes.DEFAULT_TYPE, argumentos) -> int:
"""Modificar reunion
Los argumentos que se traen son:
por título: [id, fecha]
por fecha: [id, dd-mm-aaaa]
por hora: [id, HH:MM]
"""
#id de mensaje
id_mensaje = argumentos.pop(0)
#argumento del comando
comando = argumentos.pop(0)
#obteniendo mensaje a modificar
accion = " ".join(argumentos)
# esto trae el user
user = update.message.from_user
# esto trae el topic id
topic_id = update.message.message_thread_id
#Traer todas las reuniones
topic_messages = db.mostrar_reuniones()
if re.search("[0-9]+", str(id_mensaje)):
if (int(id_mensaje) >= 0 and int(id_mensaje) < len(topic_messages)):
tupla_mensajes = topic_messages[int(id_mensaje)]
if comando == "-t":
logger.info(f"usuario {user.first_name} modificó título: '{tupla_mensajes[1]}' a {accion}")
db.modificar_titulo(tupla_mensajes[0],accion)
await update.message.reply_text("¡Título actualizado! \n\n")
elif comando == "-h":
if formato_hora(accion):
logger.info(f"usuario {user.first_name} modificó hora: '{tupla_mensajes[3]}' a {accion}")
db.modificar_hora(tupla_mensajes[0],accion)
await update.message.reply_text("¡Hora actualizada! \n\n")
else:
await update.message.reply_text("Formato de hora no válido, introduce HH:MM \n\n")
elif comando == "-f":
if formato_fecha(accion) or verificar_fecha(accion):
logger.info(f"usuario {user.first_name} modificó fecha: '{tupla_mensajes[2]}' a {accion}")
db.modificar_fecha(tupla_mensajes[0],accion)
await update.message.reply_text("¡Fecha actualizada! \n\n")
else:
await update.message.reply_text("Fecha no válida, introduce dd-mm-aaaa \n\n")
else:
await update.message.reply_text("Modificar reunión por titulo \n /agenda modificar [id_agenda] -t [Título de la reunión] \n" +
"Modificar reunión por fecha \n /agenda modificar [id_agenda] -f [Fecha en dd-mm-aaaa] \n" +
"Modificar reunión por hora \n /agenda modificar [id_agenda] -h [HH:MM CST] \n" )
else:
await update.message.reply_text("Introduce un ID valido")
else:
await update.message.reply_text("Modificar reunión por titulo \n /agenda modificar [id_agenda] -t [Título de la reunión] \n" +
"Modificar reunión por fecha \n /agenda modificar [id_agenda] -f [Fecha en dd-mm-aaaa] \n" +
"Modificar reunión por hora \n /agenda modificar [id_agenda] -h [HH:MM CST] \n" )
return ConversationHandler.END
def main() -> None:
try:
# Crear aplicacion y pasar variable de entorno TELEGRAM_BOT_TOKEN
application = Application.builder().token(os.environ.get("TELEGRAM_BOT_TOKEN")).build()
# Ejecutando la funcion principal
application.add_handler(CommandHandler("agenda", agenda))
# Ejecuta el bot hasta que se precione Ctrl-C
application.run_polling(allowed_updates=Update.ALL_TYPES)
finally:
db.cerrar()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,15 @@
FROM python:3.12-slim
# Instalar tzdata para configurar zona horaria
RUN apt-get update && apt-get install -y tzdata sqlite3 && rm -rf /var/lib/apt/lists/*
ENV TZ=America/Mexico_City
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN mkdir -p /app/reunionesData
WORKDIR /app
COPY base_de_datos.py .
COPY requirements.txt .
RUN pip install -r requirements.txt

View file

@ -0,0 +1,136 @@
#!/usr/bin/env python
"""
Este programa es software libre: usted puede redistribuirlo y/o modificarlo
bajo los términos de la Licencia Pública General de GNU publicada por la
Free Software Foundation, GPLv3.
Programa para hacer un todo list en topics de telegram, usando el framework de python-telegram-bot
Uso:
- Agenda reunión /agenda crear [Título de la reunión] [Fecha en dd-mm-aaaa] [HH:MM CST]
- Mostrar reunión vigente /agenda ls
- Modificar reunión por titulo /agenda modificar [id_agenda] -t [Título de la reunión]
- Modificar reunión por fecha /agenda modificar [id_agenda] -f [Fecha en dd-mm-aaaa]
- Modificar reunión por hora /agenda modificar [id_agenda] -h [HH:MM CST]
- Eliminar reunión /agenda [id agenda]
Precionar Ctrl-C para enviar señal de interrupcion del proceso en la linea de comandos
"""
import sqlite3
from typing import List, Tuple
class reunionesBaseDeDatos:
def __init__(self, nombre_db='reunionesData/agenda_bot.db') -> None:
self.conn =sqlite3.connect(nombre_db, check_same_thread=False)
self._crear_tablas()
def cerrar(self) -> None:
self.conn.close()
def _crear_tablas(self) -> None:
with self.conn:
#se uso varchar porque telegram en topico GENERAL tienen valor NONE
self.conn.execute('''
CREATE TABLE IF NOT EXISTS topics (
id varchar(100) PRIMARY KEY,
chat_id INTEGER not null
)
''')
self.conn.execute('''
CREATE TABLE IF NOT EXISTS reuniones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo varchar(255) not null,
fecha varchar(15) not null,
hora varchar(10) not null,
zona_horaria varchar(5) not null,
estatus BOOLEAN DEFAULT 1,
creacion DATETIME DEFAULT CURRENT_TIMESTAMP,
topic_id varchar(100) not null,
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE
)
''')
def guardar_reunion(self, topic_id: str, chat_id: str, titulo: str, fecha: str, hora: str, zona_horaria: str) -> None:
with self.conn:
#insertar tipic id si no existe
cursor = self.conn.execute('INSERT OR IGNORE INTO topics (id, chat_id) VALUES (?, ?)', (topic_id,chat_id,))
#insertar mensaje
cursor = self.conn. execute(
'INSERT INTO reuniones (topic_id, titulo, fecha, hora, zona_horaria ) VALUES (?, ?, ?, ?, ?)',
(topic_id, titulo, fecha, hora, zona_horaria))
return cursor.lastrowid
def mostrar_reuniones(self) -> List[Tuple[int, str]]:
with self.conn:
cursor = self.conn.execute(
'select id,titulo,fecha,hora,zona_horaria FROM reuniones'
)
return cursor.fetchall()
def traer_eventos(self, fecha: str) -> List[Tuple[int, str]]:
with self.conn:
cursor = self.conn.execute(
'select id,titulo,fecha,hora,zona_horaria from reuniones where fecha = ?',
(fecha,)
)
return cursor.fetchall()
def eliminar_evento(self, tarea_id: int) -> bool:
with self.conn:
cursor = self.conn.execute(
'DELETE from reuniones WHERE id = ?',
(tarea_id,)
)
return cursor.rowcount > 0
def modificar_titulo(self, reunion_id: int, nuevo_titulo: str) -> bool:
with self.conn:
cursor = self.conn.execute(
'UPDATE reuniones SET titulo = ? WHERE id = ?',
(nuevo_titulo, reunion_id)
)
return cursor.rowcount > 0
def modificar_fecha(self, reunion_id: int, nueva_fecha: str) -> bool:
with self.conn:
cursor = self.conn.execute(
'UPDATE reuniones SET fecha = ? WHERE id = ?',
(nueva_fecha, reunion_id)
)
return cursor.rowcount > 0
def modificar_hora(self, reunion_id: int, nueva_hora: str) -> bool:
with self.conn:
cursor = self.conn.execute(
'UPDATE reuniones SET hora = ? WHERE id = ?',
(nueva_hora, reunion_id)
)
return cursor.rowcount > 0
def obtener_chat_id(self) -> list:
with self.conn:
cursor = self.conn.execute(
'SELECT chat_id FROM topics LIMIT 1'
)
return cursor.fetchone()

View file

@ -0,0 +1,2 @@
python-telegram-bot
pytz

14
agenda/podman/build.sh Normal file
View file

@ -0,0 +1,14 @@
#!/bin/bash
echo "Construyendo imagen de base de datos"
podman build -t agendadb:latest base/
echo "Construyendo imagen CRUD agenda"
podman build -t crud-agenda:latest crud/
echo "Construyendo imagen recordatorio"
podman build -t recordatorio:latest recordatorio/

18
agenda/podman/compose.yml Normal file
View file

@ -0,0 +1,18 @@
version: "1.0"
services:
crud:
image: localhost/crud-agenda:latest
environment:
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
volumes:
- agenda_data:/app/reunionesData
recordatorio:
image: localhost/recordatorio:latest
environment:
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
REUNION_URL: ${REUNION_URL}
volumes:
- agenda_data:/app/reunionesData
volumes:
agenda_data:

View file

@ -0,0 +1,7 @@
FROM localhost/agendadb:latest
WORKDIR /app
COPY crud_agenda.py .
ENTRYPOINT ["python","crud_agenda.py"]

View file

@ -0,0 +1,320 @@
#!/usr/bin/env python
# pylint: disable=unused-argument
"""
Este programa es software libre: usted puede redistribuirlo y/o modificarlo
bajo los términos de la Licencia Pública General de GNU publicada por la
Free Software Foundation, GPLv3.
Programa para guardar reuniones de MxOS y avisar en el grupo de telegram cada 60, 30, 15 min hasta que inicie la sesion, usando el framework de python-telegram-bot
Uso:
- Agenda reunión /agenda crear [Título de la reunión] [Fecha en dd-mm-aaaa] [HH:MM CST]
- Mostrar reunión vigente /agenda ls
- Modificar reunión por titulo /agenda modificar [id_agenda] -t [Título de la reunión]
- Modificar reunión por fecha /agenda modificar [id_agenda] -f [Fecha en dd-mm-aaaa]
- Modificar reunión por hora /agenda modificar [id_agenda] -h [HH:MM CST]
- Eliminar reunión /agenda [id agenda]
Precionar Ctrl-C para enviar señal de interrupcion del proceso en la linea de comandos
"""
import logging
import os, re, time
from datetime import datetime
from threading import Thread
from base_de_datos import reunionesBaseDeDatos
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
ConversationHandler,
MessageHandler,
Updater,
)
# Habilitar logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
# set higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("httpx").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
# crear directorio para base de datos
if not os.path.exists('reunionesData'):
print('Creando directorio para base de datos en ruta actual! \n')
os.mkdir('reunionesData')
#inicializar base de datos
db = reunionesBaseDeDatos()
# funcion para comando /agenda
async def agenda(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Iniciar conversacion y preguntar al usuario por el evento a crear."""
if not context.args:
await ayuda(update,context)
return
subcomando = context.args[0]
argumentos = context.args[1:]
if subcomando == "crear":
#Guardar evento
await crear(update,context, argumentos)
elif subcomando == "ls":
await ls(update,context)
elif subcomando == "rm":
if re.search("[0-9]+", str(argumentos[0])):
await rm(update,context, int(argumentos[0]))
else:
await update.message.reply_text("/todo rm [Número de tarea a eliminar] \n")
elif subcomando == "modificar":
await modificar(update,context,argumentos)
elif subcomando == "ayuda":
await ayuda(update,context)
else:
await ayuda(update,context)
async def ayuda(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Opciones: \n" +
"/agenda crear [título de la reunión] [fecha en dd-mm-aaa] [HH:MM] CST \n" +
"/agenda modificar [id_eveto] -t [Nuevo titulo] \t Modifica evento por título\n" +
"/agenda modificar [id_eveto] -h [HH:MM] \t Modifica evento por hora \n" +
"/agenda modificar [id_eveto] -f [dd-mm-aaa] \t Modifica evento por fecha \n" +
"/agenda ls \t\t lista eventos\n" +
"/agenda rm [Número de evento a eliminar] \t\tElimina evento\n")
return ConversationHandler.END
def formato_fecha(fecha: str) -> bool:
if re.search("^(0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-(\\d{4})$", fecha):
return True
else:
return False
def verificar_fecha(fecha: str) -> bool:
if datetime.strptime(fecha, "%d-%m-%Y").date() >= datetime.now().date():
return True
else:
return False
def formato_hora(hora: str) -> bool:
if re.search("^([01][0-9]|2[0-3]):[0-5][0-9]$", hora):
return True
else:
return False
def formato_zona(zona_horaria: str) -> bool:
if re.search("UTC$|utc$|CST$|cst$", zona_horaria):
return True
else:
return False
async def crear(update: Update, context: ContextTypes.DEFAULT_TYPE, argumentos) -> int:
# Quien envia la solicitud
user = update.message.from_user
#Traer el topic id
topic_id = update.message.message_thread_id
# traer el chat_id
chat_id = update.message.chat.id
#enviar mensaje si no se pasa nada al comando
if len(argumentos) <= 0:
await update.message.reply_text("/agenda crear [título de la reunión] [fecha en dd-mm-aaa] [HH:MM] CST \n")
return
# Parsear los datos de entrada
zona_horaria = argumentos.pop()
hora = argumentos.pop()
fecha = argumentos.pop()
titulo = " ".join(argumentos)
logger.info("%s ingreso: %s", user.first_name, titulo)
logger.info("Topic id es:" + str(topic_id))
#Verificar si el formato de la fecha es valida
if formato_fecha(fecha):
if verificar_fecha(fecha): #verificar si la fecha no esta en el pasado
print("Día del evento: "+ fecha)
else:
await update.message.reply_text("Fecha no valida, el evento está en el pasado. \n\n")
logger.info(f"El usuario {user.first_name} introdujo {fecha} hoy es {datetime.now().date()}")
print("Fecha no valida, el evento está en el pasado.")
return
else:
await update.message.reply_text("Fecha no válida, introduce dd-mm-aaaa \n\n")
return
#Verifiar si hora es valida
if formato_hora(str(hora)):
print("Hora a guardar: "+ hora)
else:
await update.message.reply_text("Formato de hora no válido, introduce HH:MM \n\n")
return
#Verificar si es zona horaria valida
if formato_zona(zona_horaria):
print("Zona horaria: "+ zona_horaria)
else:
await update.message.reply_text("Zona horaria no válida, introduce utc|UTC|cst|CST \n\n")
return
if str(topic_id) == "None":
topic_id = "General"
#Guardar evento
db.guardar_reunion(topic_id, chat_id, titulo, fecha, hora, zona_horaria)
await update.message.reply_text("¡Listo!, la reunion fue guardada\n\n")
return ConversationHandler.END
async def ls(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Listar las reuniones guardadas"""
#esto trae el usuario
user = update.message.from_user
#Traer todas las reuniones
topic_messages = db.mostrar_reuniones()
#Juntar las columnas reunion fecha hora y zona horaria
reuniones = []
for fila in topic_messages:
reuniones.append(("Reunion "+str(fila[1]) +" el día "+str(fila[2]) +" a las "+ str(fila[3]+" "+str(fila[4]))))
if len(reuniones) > 0:
mensaje_final = ''
for mensaje in range(0, len(reuniones)):
mensaje_final += "\n"+str(mensaje)+".- "+reuniones[mensaje]
logger.info(user.first_name + " consulto reuniones, las cuales son:")
logger.info(mensaje_final + "\n")
await update.message.reply_text("Reuniones a recordar:" + mensaje_final)
else:
await update.message.reply_text("Todavia no hay reuniones, ¿quieres agendar una? \n\n /agenda crear [título de la reunión] [fecha en dd-mm-aaa] CST")
return ConversationHandler.END
async def rm(update: Update, context: ContextTypes.DEFAULT_TYPE, numero_de_mensaje) -> int:
#Elimina evento agendado derivado del response de /agenda ls
#esto trae el usuario
user = update.message.from_user
#obteniendo mensajes
topic_messages = db.mostrar_reuniones()
if (int(numero_de_mensaje) >= 0 and int(numero_de_mensaje) < len(topic_messages)):
logger.info(topic_messages[int(numero_de_mensaje)])
mensaje_a_borrar = topic_messages[int(numero_de_mensaje)]
id_mensaje_a_borrar = mensaje_a_borrar[0]
logger.info(id_mensaje_a_borrar)
if db.eliminar_evento(id_mensaje_a_borrar):
logger.info(user.first_name + " eliminió reunion")
await update.message.reply_text("Recordatorio eliminado :) \n")
elif len(topic_messages) == 0:
await update.message.reply_text("Todavia no hay eventos, ¿quieres agendar uno? \n\n /agenda crear [título de la reunión] [fecha en dd-mm-aaa] [HH:MM] CST")
else:
await update.message.reply_text("Introduce un ID valido :) \n")
return ConversationHandler.END
async def modificar(update: Update, context: ContextTypes.DEFAULT_TYPE, argumentos) -> int:
"""Modificar reunion
Los argumentos que se traen son:
por título: [id, fecha]
por fecha: [id, dd-mm-aaaa]
por hora: [id, HH:MM]
"""
#id de mensaje
id_mensaje = argumentos.pop(0)
#argumento del comando
comando = argumentos.pop(0)
#obteniendo mensaje a modificar
accion = " ".join(argumentos)
# esto trae el user
user = update.message.from_user
# esto trae el topic id
topic_id = update.message.message_thread_id
#Traer todas las reuniones
topic_messages = db.mostrar_reuniones()
if re.search("[0-9]+", str(id_mensaje)):
if (int(id_mensaje) >= 0 and int(id_mensaje) < len(topic_messages)):
tupla_mensajes = topic_messages[int(id_mensaje)]
if comando == "-t":
logger.info(f"usuario {user.first_name} modificó título: '{tupla_mensajes[1]}' a {accion}")
db.modificar_titulo(tupla_mensajes[0],accion)
await update.message.reply_text("¡Título actualizado! \n\n")
elif comando == "-h":
if formato_hora(accion):
logger.info(f"usuario {user.first_name} modificó hora: '{tupla_mensajes[3]}' a {accion}")
db.modificar_hora(tupla_mensajes[0],accion)
await update.message.reply_text("¡Hora actualizada! \n\n")
else:
await update.message.reply_text("Formato de hora no válido, introduce HH:MM \n\n")
elif comando == "-f":
if formato_fecha(accion) or verificar_fecha(accion):
logger.info(f"usuario {user.first_name} modificó fecha: '{tupla_mensajes[2]}' a {accion}")
db.modificar_fecha(tupla_mensajes[0],accion)
await update.message.reply_text("¡Fecha actualizada! \n\n")
else:
await update.message.reply_text("Fecha no válida, introduce dd-mm-aaaa \n\n")
else:
await update.message.reply_text("Modificar reunión por titulo \n /agenda modificar [id_agenda] -t [Título de la reunión] \n" +
"Modificar reunión por fecha \n /agenda modificar [id_agenda] -f [Fecha en dd-mm-aaaa] \n" +
"Modificar reunión por hora \n /agenda modificar [id_agenda] -h [HH:MM CST] \n" )
else:
await update.message.reply_text("Introduce un ID valido")
else:
await update.message.reply_text("Modificar reunión por titulo \n /agenda modificar [id_agenda] -t [Título de la reunión] \n" +
"Modificar reunión por fecha \n /agenda modificar [id_agenda] -f [Fecha en dd-mm-aaaa] \n" +
"Modificar reunión por hora \n /agenda modificar [id_agenda] -h [HH:MM CST] \n" )
return ConversationHandler.END
def main() -> None:
try:
# Crear aplicacion y pasar variable de entorno TELEGRAM_BOT_TOKEN
application = Application.builder().token(os.environ.get("TELEGRAM_BOT_TOKEN")).build()
# Ejecutando la funcion principal
application.add_handler(CommandHandler("agenda", agenda))
# Ejecuta el bot hasta que se precione Ctrl-C
application.run_polling(allowed_updates=Update.ALL_TYPES)
finally:
db.cerrar()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,7 @@
FROM localhost/agendadb:latest
WORKDIR /app
COPY recordatorio.py .
ENTRYPOINT ["python","recordatorio.py"]

View file

@ -0,0 +1,162 @@
#!/usr/bin/env python
# pylint: disable=unused-argument
"""
Este programa es software libre: usted puede redistribuirlo y/o modificarlo
bajo los términos de la Licencia Pública General de GNU publicada por la
Free Software Foundation, GPLv3.
Programa para guardar reuniones de MxOS y avisar en el grupo de telegram cada 60, 30, 15 min hasta que inicie la sesion, usando el framework de python-telegram-bot
"""
from telegram import Bot
from telegram.error import TelegramError
from datetime import datetime
from base_de_datos import reunionesBaseDeDatos
import logging
import os, time
import asyncio
import pytz
from typing import Dict, Set, Tuple
# Configuración básica de logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
# Configuración
TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
URL = os.environ.get("REUNION_URL")
ZONAS_HORARIAS = {
"CST": "America/Mexico_City",
"EST": "America/Cancun",
"MST": "America/Hermosillo",
"PT": "America/Tijuana"
}
class EventNotifier:
def __init__(self):
self.db = reunionesBaseDeDatos()
self.bot = Bot(token=TOKEN)
# Diccionario para registrar qué notificaciones ya se enviaron
self.notificaciones_enviadas: Dict[Tuple[str, str], Set[int]] = {}
# Intervalos de notificación en minutos (ordenados de mayor a menor)
self.INTERVALOS_NOTIFICACION = [60, 30, 10, 5, 2, 0]
def obtener_hora_actual(self, zona: str) -> str:
"""Obtiene la hora actual en la zona horaria especificada."""
tz = pytz.timezone(ZONAS_HORARIAS.get(zona, "America/Mexico_City"))
return datetime.now(tz).strftime("%H:%M")
def horas_a_minutos(self, hora: str) -> int:
"""Convierte una hora en formato HH:MM a minutos totales."""
try:
horas, minutos = map(int, hora.split(':'))
return horas * 60 + minutos
except (ValueError, AttributeError):
return 0
def minutos_restantes(self, hora_evento: str, hora_actual: str) -> int:
"""Calcula los minutos que faltan para el evento."""
return self.horas_a_minutos(hora_evento) - self.horas_a_minutos(hora_actual)
async def enviar_notificacion(self, evento: Tuple, minutos_restantes: int) -> bool:
#Envía la notificación al chat de Telegram
id, nombre_evento, _, hora_evento, zona = evento
evento_key = (id, nombre_evento, hora_evento, zona)
# Verificar si ya se envió esta notificación
if evento_key not in self.notificaciones_enviadas:
self.notificaciones_enviadas[evento_key] = set()
if minutos_restantes in self.notificaciones_enviadas[evento_key]:
return False
try:
logger.info(f"Minutos restantes: {minutos_restantes} para {nombre_evento}")
if minutos_restantes == 0:
mensaje = f"⚠️ ¡¡CAELE a la reunión!!: {nombre_evento} \n\n ESTÁ POR INICIAR estamos en: {URL}"
await self.bot.send_message(chat_id=CHAT_ID, text=mensaje)
logger.info(f"Notificación enviada: {mensaje}")
logger.info(f"Eliminando reunion: {nombre_evento} \n")
self.db.eliminar_evento(id)
elif minutos_restantes == 60 or minutos_restantes == 30 or minutos_restantes == 15 or minutos_restantes == 10 or minutos_restantes == 5 or minutos_restantes == 2:
mensaje = f"🔔 Recordatorio: El evento {nombre_evento} inicia en {minutos_restantes} minutos"
await self.bot.send_message(chat_id=CHAT_ID, text=mensaje)
logger.info(f"Notificación enviada: {mensaje}\n")
elif minutos_restantes > 0 and minutos_restantes < 2:
mensaje = f"🔔 Recordatorio: El evento {nombre_evento} inicia en {minutos_restantes} minuto"
await self.bot.send_message(chat_id=CHAT_ID, text=mensaje)
logger.info(f"Notificación enviada: {mensaje}\n")
self.notificaciones_enviadas[evento_key].add(minutos_restantes)
return True
except TelegramError as e:
logger.error(f"Error al enviar mensaje: {e}")
return False
def necesita_notificacion(self, minutos_restantes: int) -> bool:
# Determina si se debe enviar notificación para los minutos restantes dados.
return any(
abs(minutos_restantes - intervalo) <= 1 # Margen de 1 minuto
for intervalo in self.INTERVALOS_NOTIFICACION
)
async def procesar_evento(self, evento: Tuple):
#Procesa un único evento y envía notificaciones según sea necesario
id, nombre, fecha, hora_evento, zona = evento
hora_actual = self.obtener_hora_actual(zona)
minutos = self.minutos_restantes(hora_evento, hora_actual)
if self.necesita_notificacion(minutos):
await self.enviar_notificacion(evento, minutos)
elif minutos < 0:
# Limpiar eventos pasados del registro
evento_key = (nombre, hora_evento, zona)
if evento_key in self.notificaciones_enviadas:
del self.notificaciones_enviadas[evento_key]
async def procesar_eventos(self):
#Procesa todos los eventos del día actual
fecha_actual = datetime.now().strftime("%d-%m-%Y")
eventos_hoy = self.db.traer_eventos(fecha_actual)
if not eventos_hoy:
logger.debug("No hay eventos programados para hoy")
return
chat_ID = self.db.obtener_chat_id()
global CHAT_ID
CHAT_ID = str(chat_ID[0])
# Procesar cada evento concurrentemente
await asyncio.gather(*[self.procesar_evento(evento) for evento in eventos_hoy])
async def run(self):
"""Ejecuta el bot en un bucle infinito."""
logger.info("Iniciando bot de notificaciones de eventos")
try:
while True:
await self.procesar_eventos()
except asyncio.CancelledError:
logger.info("Bot detenido por señal de interrupción")
except Exception as e:
logger.error(f"Error inesperado: {e}")
finally:
logger.info("Bot detenido")
if __name__ == '__main__':
notifier = EventNotifier()
try:
asyncio.run(notifier.run())
except KeyboardInterrupt:
notifier.db.cerrar()
logger.info("Bot detenido por el usuario")

3
agenda/podman/run.sh Normal file
View file

@ -0,0 +1,3 @@
#!/bin/bash
podman-compose up -d

162
agenda/recordatorio.py Normal file
View file

@ -0,0 +1,162 @@
#!/usr/bin/env python
# pylint: disable=unused-argument
"""
Este programa es software libre: usted puede redistribuirlo y/o modificarlo
bajo los términos de la Licencia Pública General de GNU publicada por la
Free Software Foundation, GPLv3.
Programa para guardar reuniones de MxOS y avisar en el grupo de telegram cada 60, 30, 15 min hasta que inicie la sesion, usando el framework de python-telegram-bot
"""
from telegram import Bot
from telegram.error import TelegramError
from datetime import datetime
from base_de_datos import reunionesBaseDeDatos
import logging
import os, time
import asyncio
import pytz
from typing import Dict, Set, Tuple
# Configuración básica de logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
# Configuración
TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
URL = os.environ.get("REUNION_URL")
ZONAS_HORARIAS = {
"CST": "America/Mexico_City",
"EST": "America/Cancun",
"MST": "America/Hermosillo",
"PT": "America/Tijuana"
}
class EventNotifier:
def __init__(self):
self.db = reunionesBaseDeDatos()
self.bot = Bot(token=TOKEN)
# Diccionario para registrar qué notificaciones ya se enviaron
self.notificaciones_enviadas: Dict[Tuple[str, str], Set[int]] = {}
# Intervalos de notificación en minutos (ordenados de mayor a menor)
self.INTERVALOS_NOTIFICACION = [60, 30, 10, 5, 2, 0]
def obtener_hora_actual(self, zona: str) -> str:
"""Obtiene la hora actual en la zona horaria especificada."""
tz = pytz.timezone(ZONAS_HORARIAS.get(zona, "America/Mexico_City"))
return datetime.now(tz).strftime("%H:%M")
def horas_a_minutos(self, hora: str) -> int:
"""Convierte una hora en formato HH:MM a minutos totales."""
try:
horas, minutos = map(int, hora.split(':'))
return horas * 60 + minutos
except (ValueError, AttributeError):
return 0
def minutos_restantes(self, hora_evento: str, hora_actual: str) -> int:
"""Calcula los minutos que faltan para el evento."""
return self.horas_a_minutos(hora_evento) - self.horas_a_minutos(hora_actual)
async def enviar_notificacion(self, evento: Tuple, minutos_restantes: int) -> bool:
#Envía la notificación al chat de Telegram
id, nombre_evento, _, hora_evento, zona = evento
evento_key = (id, nombre_evento, hora_evento, zona)
# Verificar si ya se envió esta notificación
if evento_key not in self.notificaciones_enviadas:
self.notificaciones_enviadas[evento_key] = set()
if minutos_restantes in self.notificaciones_enviadas[evento_key]:
return False
try:
logger.info(f"Minutos restantes: {minutos_restantes} para {nombre_evento}")
if minutos_restantes == 0:
mensaje = f"⚠️ ¡¡CAELE a la reunión!!: {nombre_evento} \n\n ESTÁ POR INICIAR estamos en: {URL}"
await self.bot.send_message(chat_id=CHAT_ID, text=mensaje)
logger.info(f"Notificación enviada: {mensaje}")
logger.info(f"Eliminando reunion: {nombre_evento} \n")
self.db.eliminar_evento(id)
elif minutos_restantes == 60 or minutos_restantes == 30 or minutos_restantes == 15 or minutos_restantes == 10 or minutos_restantes == 5 or minutos_restantes == 2:
mensaje = f"🔔 Recordatorio: El evento {nombre_evento} inicia en {minutos_restantes} minutos"
await self.bot.send_message(chat_id=CHAT_ID, text=mensaje)
logger.info(f"Notificación enviada: {mensaje}\n")
elif minutos_restantes > 0 and minutos_restantes < 2:
mensaje = f"🔔 Recordatorio: El evento {nombre_evento} inicia en {minutos_restantes} minuto"
await self.bot.send_message(chat_id=CHAT_ID, text=mensaje)
logger.info(f"Notificación enviada: {mensaje}\n")
self.notificaciones_enviadas[evento_key].add(minutos_restantes)
return True
except TelegramError as e:
logger.error(f"Error al enviar mensaje: {e}")
return False
def necesita_notificacion(self, minutos_restantes: int) -> bool:
# Determina si se debe enviar notificación para los minutos restantes dados.
return any(
abs(minutos_restantes - intervalo) <= 1 # Margen de 1 minuto
for intervalo in self.INTERVALOS_NOTIFICACION
)
async def procesar_evento(self, evento: Tuple):
#Procesa un único evento y envía notificaciones según sea necesario
id, nombre, fecha, hora_evento, zona = evento
hora_actual = self.obtener_hora_actual(zona)
minutos = self.minutos_restantes(hora_evento, hora_actual)
if self.necesita_notificacion(minutos):
await self.enviar_notificacion(evento, minutos)
elif minutos < 0:
# Limpiar eventos pasados del registro
evento_key = (nombre, hora_evento, zona)
if evento_key in self.notificaciones_enviadas:
del self.notificaciones_enviadas[evento_key]
async def procesar_eventos(self):
#Procesa todos los eventos del día actual
fecha_actual = datetime.now().strftime("%d-%m-%Y")
eventos_hoy = self.db.traer_eventos(fecha_actual)
if not eventos_hoy:
logger.debug("No hay eventos programados para hoy")
return
chat_ID = self.db.obtener_chat_id()
global CHAT_ID
CHAT_ID = str(chat_ID[0])
# Procesar cada evento concurrentemente
await asyncio.gather(*[self.procesar_evento(evento) for evento in eventos_hoy])
async def run(self):
"""Ejecuta el bot en un bucle infinito."""
logger.info("Iniciando bot de notificaciones de eventos")
try:
while True:
await self.procesar_eventos()
except asyncio.CancelledError:
logger.info("Bot detenido por señal de interrupción")
except Exception as e:
logger.error(f"Error inesperado: {e}")
finally:
logger.info("Bot detenido")
if __name__ == '__main__':
notifier = EventNotifier()
try:
asyncio.run(notifier.run())
except KeyboardInterrupt:
notifier.db.cerrar()
logger.info("Bot detenido por el usuario")

2
agenda/requirements.txt Normal file
View file

@ -0,0 +1,2 @@
python-telegram-bot
pytz

Binary file not shown.