Bot de Telegram para repartir las tareas de la casa entre dos o más personas. Cada mañana asigna las tareas del día al miembro con menos puntos acumulados en el mes, y al completarlas se suman puntos para llevar un balance mensual.
El proyecto sigue una arquitectura en capas con dependencia unidireccional:
apps/bots/telegram/ ← entrypoint del bot (Starlette + python-telegram-bot)
│
modules/tasks/ ← lógica de dominio (servicios, repositorio, tipos)
│
core/ ← infraestructura compartida (DB, config, identidad, utils)
Reglas de dependencia:
core/no puede importar demodules/niapps/.modules/puede importar solo decore/.apps/puede importar de ambos.
| Archivo | Propósito |
|---|---|
config.py |
Carga variables de entorno desde .env usando python-dotenv |
utils/date.py |
Utilidades de fecha: get_today(), format_date(), to_db_date(), next_due_date(), month_key(), arrays DAYS y MONTHS |
utils/string.py |
Utilidades de texto: normalize_string(), html_escape() |
db.py |
Conexión SQLite con row_factory = sqlite3.Row y PRAGMA foreign_keys = ON |
schema.sql |
Esquema de la base de datos (users, tasks, assignments) |
identity.py |
Consulta de usuarios desde la DB |
seed.py |
Carga datos iniciales desde archivos YAML en seed/ |
| Archivo | Propósito |
|---|---|
types.py |
Dataclasses: Task, Assignment, TaskOperationResult, AssignmentCompletionResult and enums: TaskOperationStatus, AssignmentCompletionStatus |
repository.py |
Consultas SQL (tasks, assignments, puntos por usuario) |
service.py |
Lógica de negocio: asignar tareas, marcar como hechas, balance mensual |
| Archivo | Propósito |
|---|---|
main.py |
Punto de entrada: servidor Starlette + Uvicorn, rutas webhook |
app.py |
Construcción de la aplicación python-telegram-bot con handlers |
jobs.py |
Envío de asignaciones diarias a cada usuario |
messages_es.py |
Mensajes de texto en español (i18n listo para agregar otros idiomas) |
trigger_daily.py |
Script CLI para ejecutar asignaciones diarias sin servidor web |
handlers/commands.py |
Comandos /start, /help, /tasks, /add_task, /list_tasks, /edit_task, /delete_task, /assignments, /balance |
handlers/messages.py |
Mensajes de texto y botones inline |
- Python 3.12 o superior
- Un bot de Telegram (crear con @BotFather)
- (Opcional) Docker
- Clona el repositorio y crea un entorno virtual:
git clone <repo>
cd home-os
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate- Instala el proyecto en modo editable con dependencias de desarrollo:
pip install -e ".[dev]"- Copia el archivo de ejemplo y completa las variables:
cp .env.example .envEdita .env con los siguientes valores:
| Variable | Descripción |
|---|---|
TELEGRAM_BOT_TOKEN |
Token del bot (de @BotFather) |
SEBASTIAN_TELEGRAM_CHAT_ID |
Chat ID del primer integrante |
ANTONIA_TELEGRAM_CHAT_ID |
Chat ID del segundo integrante |
WEBHOOK_URL |
URL pública del bot (para webhook) |
WEBHOOK_SECRET |
Token aleatorio para validar requests |
Para obtener tu
chat_id, envía un mensaje a @userinfobot en Telegram.
| Variable | Defecto | Descripción |
|---|---|---|
HOME_OS_DB_PATH |
./homeos.db |
Ruta al archivo SQLite |
TZ |
America/Santiago |
Zona horaria (usada por core.utils.get_today()) |
PORT |
8080 |
Puerto del servidor webhook |
python -m apps.bots.telegram.mainSi WEBHOOK_URL y WEBHOOK_SECRET están configurados, levanta un servidor Starlette + Uvicorn en http://0.0.0.0:8080 en modo webhook.
Si no, arranca en modo polling — el bot escucha actualizaciones de Telegram sin necesidad de un servidor accesible públicamente.
Para probar o ejecutar la rutina de asignación diaria localmente sin levantar el bot ni configurar un cron:
python -m apps.bots.telegram.trigger_dailyEsto inicializa la base de datos, ejecuta el mismo algoritmo de asignación que usa el cron en producción y envía los mensajes por Telegram a cada integrante.
python -c "import core, modules.tasks, apps.bots.telegram; print('imports OK')"ruff check .No hay typechecker configurado. Ruff usa line-length = 100.
Al arrancar, el bot inicializa la DB y carga datos desde seed/:
Define los integrantes de la casa. El telegram_chat_id soporta expansión de variables de entorno con sintaxis $VAR_NAME:
users:
- id: sebastian
name: Sebastian
telegram_chat_id: $SEBASTIAN_TELEGRAM_CHAT_ID
- id: antonia
name: Antonia
telegram_chat_id: $ANTONIA_TELEGRAM_CHAT_IDDefine las tareas del hogar. Cada tarea puede ser:
- Recurrente (con
frequency_days): se asigna automáticamente cada N días. - Ocasional (sin
frequency_days): los integrantes la marcan como hecha cuando quieran.
Para tareas recurrentes, el next_due_date se puede definir de dos formas:
start_offset_days: se calcula comohoy + offset.next_due_date: fecha exacta en formatoYYYY-MM-DD(tiene prioridad sobrestart_offset_days).
tasks:
- name: Lavar la loza
points: 3
frequency_days: 2
next_due_date: "2026-07-13"
- name: Regar las plantas
points: 2
frequency_days: 7
start_offset_days: 1
- name: Sacar el reciclaje
points: 2Las tareas se cargan una sola vez al crear la DB. Si la tabla tasks ya tiene datos, el seed se salta.
Define asignaciones históricas para poblar la DB (útil para migrar datos o reiniciar). Usa task_name y user_name para referenciar por nombre (se resuelven a IDs automáticamente).
assignments:
- task_name: Lavar la loza
user_name: Sebastián
assigned_at: "2026-07-06"
status: completed
completed_at: "2026-07-06"
points_awarded: 6
- task_name: Regar las plantas
user_name: Antonia
assigned_at: "2026-07-06"
status: completed
completed_at: "2026-07-06"
points_awarded: 2
- task_name: Lavar la loza
user_name: Sebastián
assigned_at: "2026-07-08"
status: failedLas asignaciones se cargan una sola vez. Si la tabla assignments ya tiene datos, el seed se salta.
| Comando / Acción | Descripción |
|---|---|---|
| /start | Mensaje de bienvenida con instrucciones |
| /help | Muestra la ayuda (alias de /start) |
| /tasks | Explicación de los comandos CRUD de tareas |
| /add_task <name> <points> [freq] | Crea una tarea nueva |
| /list_tasks | Lista todas las tareas con formato tabla |
| /edit_task <name> <field> <value> | Edita nombre, puntos o frecuencia de una tarea |
| /delete_task <name> | Elimina una tarea (solo si no tiene asignaciones pendientes) |
| /assignments | Muestra las tareas pendientes de hoy con botones |
| /balance | Muestra los puntos acumulados este mes |
| Escribir nombre de tarea | Marca una tarea como completada (coincidencia exacta, case-insensitive) |
| Botón inline | Marca la tarea como completada desde el mensaje de la mañana |
- Se marcan como
failedlas tareas pendientes de días anteriores. - Se buscan tareas recurrentes cuya
next_due_datesea hoy o anterior, ordenadas por puntos de mayor a menor. - Se asignan al integrante con menos puntos acumulados en el mes actual. En caso de empate, se elige al azar.
- Cada integrante tiene un tope diario de puntos igual a
1.5 × la tarea con más puntos del día. Al alcanzarlo, no recibe más tareas ese día. Las tareas que nadie puede tomar se saltan y quedan pendientes para el próximo ciclo. - Se envía un mensaje a cada integrante con sus tareas y botones para marcar como hecha.
- Al marcar como hecha, se recalcula
next_due_date = today + frequency_days.
cp .env.example .env
# completa TELEGRAM_BOT_TOKEN y los chat IDs
docker compose up --buildLa base de datos persiste en ./data gracias al volumen definido en docker-compose.yml.
La interfaz entre la lógica de dominio (modules/tasks/service.py) y el bot. No se cambia sin conversarlo.
def create_task(name: str, points: int, frequency_days: int | None = None) -> TaskOperationResult
def update_active_task(task_id: int, **kwargs: str | int | None) -> TaskOperationResult
def soft_delete_active_task(task_id: int) -> TaskOperationResult
def get_daily_assignments(day: date) -> list[Assignment] # Si no hay asignaciones para el día, las genera automáticamente
def get_pending_assignments(day: date) -> list[Assignment]
def fail_stale_pending_assignments(day: date) -> int
def mark_assignment_done(text: str, user_id: str, day: date) -> AssignmentCompletionResult
def get_month_balance(month: str) -> dict[str, int]El bot corre en modo webhook sobre Starlette + Uvicorn con dos rutas:
POST /telegram— recibe updates de Telegram (validado con headerX-Telegram-Bot-Api-Secret-Token).GET|POST /trigger-daily/<WEBHOOK_SECRET>— dispara la asignación diaria. Lo llama un cron externo.
La máquina de Fly.io se apaga automáticamente sin tráfico (auto_stop_machines = 'stop', min_machines_running = 0) y arranca con cada request entrante. Así solo se paga el cómputo cuando el bot está activo (~USD 0.15/mes por el volumen).
fly launch --no-deploy
fly volumes create homeos_data --region gru --size 1
fly secrets set \
TELEGRAM_BOT_TOKEN=<token> \
WEBHOOK_SECRET=$(openssl rand -hex 32) \
WEBHOOK_URL=https://<tu-app>.fly.dev \
ANTONIA_TELEGRAM_CHAT_ID=<chat_id> \
SEBASTIAN_TELEGRAM_CHAT_ID=<chat_id>
fly deployConfigurar un cron externo (ej. cron-job.org) que haga una request a las 07:00 hora de Chile:
GET https://<tu-app>.fly.dev/trigger-daily/<WEBHOOK_SECRET>
La zona horaria de Chile es America/Santiago. En horario de verano (septiembre-marzo) son UTC-3; en invierno UTC-4. cron-job.org permite fijar la zona horaria directamente.
SQLite, creada automáticamente al arrancar. Tablas:
- users —
id,name,telegram_chat_id - tasks —
id,name,points,frequency_days,next_due_date,deleted_at(soft delete) - assignments —
id,task_id,user_id,assigned_at,completed_at,status(pending|completed|failed),points_awarded
Índices únicos:
idx_tasks_unique_active_name— un nombre activo por tarea (WHERE deleted_at IS NULL)idx_assignment_one_pending_per_task— una asignación pendiente por tareaidx_assignment_one_completed_per_day— una asignación completada por tarea por día
El archivo .db no se versiona (en .gitignore). Se regenera solo con datos de seed si no existe.
La DB de producción vive en un volumen de Fly.io (/app/data/homeos.db). Para descargar una copia local:
.\scripts\private\backup_db.ps1Esto:
- Inicia la máquina si está detenida
- Descarga la DB a
data/homeos_<timestamp>.dbydata/homeos.db - Verifica la integridad de la copia
- Detiene la máquina para ahorrar costos
Para especificar otro directorio de salida:
.\scripts\private\backup_db.ps1 -OutputDir "backups"# Verificar estado
fly status
# Si la máquina está detenida, iniciarla
fly machine start <machine_id>
# Descargar la DB
fly ssh sftp get /app/data/homeos.db data/homeos.db
# Detener la máquina después del backup
fly machine stop <machine_id>Sin descargar la DB, ejecuta inspect_db.py directamente en el servidor de Fly.io:
python scripts/private/inspect_prod_db.pyEsto:
- Inicia la máquina si está detenida
- Sube el script de inspección a
/tmp/inspect.pyen el servidor - Lo ejecuta contra la DB de producción
- Muestra tablas, conteo de filas y las primeras 25 filas de cada tabla
python scripts/private/inspect_db.py data/homeos.dbMuestra tablas, conteo de filas y las primeras 25 filas de cada tabla.
- La DB en
data/está en.gitignorey no se versiona. - Ambos scripts (
backup_db.ps1einspect_prod_db.py) auto-inician la máquina si está detenida y la detienen después de operar. - Los scripts están en
scripts/private/(también en.gitignore).
- No hay scheduler en proceso. Las asignaciones diarias las dispara un cron externo, o localmente con
python -m apps.bots.telegram.trigger_daily. assignmentscon statuspendingde días anteriores se marcan comofailedal ejecutar la rutina diaria.- Las tareas se asignan al usuario con menor puntaje acumulado en el mes, no aleatoriamente ni por turnos fijos.