forked from MxOS/bottija-bot
update: recordatorio para reuniones grupo de telegram
This commit is contained in:
parent
523c0b9af5
commit
2af5c522f9
18 changed files with 1437 additions and 0 deletions
|
|
@ -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
132
agenda/README.md
Normal 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
|
||||
~~~
|
||||
BIN
agenda/__pycache__/base_de_datos.cpython-313.pyc
Normal file
BIN
agenda/__pycache__/base_de_datos.cpython-313.pyc
Normal file
Binary file not shown.
136
agenda/base_de_datos.py
Normal file
136
agenda/base_de_datos.py
Normal 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
320
agenda/crud_agenda.py
Normal 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()
|
||||
15
agenda/podman/base/Containerfile
Normal file
15
agenda/podman/base/Containerfile
Normal 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
|
||||
136
agenda/podman/base/base_de_datos.py
Normal file
136
agenda/podman/base/base_de_datos.py
Normal 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()
|
||||
2
agenda/podman/base/requirements.txt
Normal file
2
agenda/podman/base/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
python-telegram-bot
|
||||
pytz
|
||||
14
agenda/podman/build.sh
Normal file
14
agenda/podman/build.sh
Normal 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
18
agenda/podman/compose.yml
Normal 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:
|
||||
7
agenda/podman/crud/Containerfile
Normal file
7
agenda/podman/crud/Containerfile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
FROM localhost/agendadb:latest
|
||||
|
||||
WORKDIR /app
|
||||
COPY crud_agenda.py .
|
||||
|
||||
|
||||
ENTRYPOINT ["python","crud_agenda.py"]
|
||||
320
agenda/podman/crud/crud_agenda.py
Normal file
320
agenda/podman/crud/crud_agenda.py
Normal 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()
|
||||
7
agenda/podman/recordatorio/Containerfile
Normal file
7
agenda/podman/recordatorio/Containerfile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
FROM localhost/agendadb:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY recordatorio.py .
|
||||
|
||||
ENTRYPOINT ["python","recordatorio.py"]
|
||||
162
agenda/podman/recordatorio/recordatorio.py
Normal file
162
agenda/podman/recordatorio/recordatorio.py
Normal 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
3
agenda/podman/run.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
podman-compose up -d
|
||||
162
agenda/recordatorio.py
Normal file
162
agenda/recordatorio.py
Normal 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
2
agenda/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
python-telegram-bot
|
||||
pytz
|
||||
BIN
agenda/reunionesData/agenda_bot.db
Normal file
BIN
agenda/reunionesData/agenda_bot.db
Normal file
Binary file not shown.
Loading…
Reference in a new issue