- Problema que Resuelve
- Descripción General
- Arquitectura
- Diseño UI/UX
- Decisiones Técnicas
- Instalación y Configuración
- Comandos Disponibles
- Estimaciones y Complejidad
- Qué Haría Diferente
- Desarrollo y Contribución
En los últimos años se ha vuelto mas común el trabajo en casa y también siempre que seamos estudiantes tenemos a dedicar unas horas de enfoque en resolver informes y proyectos de grado, y esto ha llevado a pasar largas jornadas dentro de una pantalla, sería muy común sentirse solo pero poco a poco en plataformas de streaming como Twitch (siendo una de las mas famosas) suelen crearse comunidades pequeñas de Coworking y Study With Me, las cuales buscan atraer a personas y tener algo en común que hacer mientras cumplimos nuestras obligaciones, este bot permite que el streamer brinde a sus viwers una forma minimalista y cheveré de sentirse parte de estas sesiones ya que centraliza todas las tareas que tienen pendientes los viwers y que quieren completar.
Solución implementada:
Este bot permite a los usuarios del chat crear, visualizar y completar tareas personales sin salir de la plataforma de Twitch. Esto mejora el engagement de la comunidad y ayuda a los espectadores a mantener productividad mientras disfrutan del contenido del streamer.
Impacto medible:
- Reduce la fricción de gestionar tareas externas (como abrir otra aplicación)
- Fomenta la participación de la comunidad con una herramienta útil
- Proporciona al streamer un overlay visual en OBS para mostrar las tareas en pantalla
Bot modular para Twitch que permite gestionar tareas directamente desde el chat. Incluye integración con Spotify para mostrar la canción actual en transmisión, un timer Pomodoro para gestión de tiempo en sesiones de estudio, y un sistema de overlay para OBS.
Stack tecnológico:
- Backend: Node.js con TypeScript, Express, tmi.js (cliente de Twitch IRC)
- Frontend (Overlay): React 19 con Vite
- Almacenamiento: Supabase (Postgres con Realtime) + archivos JSON legacy para migraciones
- Testing: Jest para pruebas unitarias
- Calidad de código: Biome para linting y formateo
- Arquitectura: Monorepo con pnpm workspaces
Este proyecto utiliza una arquitectura monorepo basada en Scope Rule y Screaming Architecture, organizando el código en tres paquetes independientes pero relacionados:
bot-twitch/
├── packages/
│ ├── shared/ # Tipos compartidos (Single Source of Truth)
│ │ ├── src/
│ │ │ ├── task/ # Modelos de dominio de tareas
│ │ │ └── spotify/ # Modelos de dominio de Spotify
│ │ └── package.json
│ │
│ ├── bot-backend/ # Backend Node.js + Twitch Bot
│ │ ├── src/
│ │ │ ├── features/ # Features del bot
│ │ │ │ ├── task-management/
│ │ │ │ │ ├── task-management.service.ts
│ │ │ │ │ ├── commands/ # !task, !done, !cleardone, etc.
│ │ │ │ │ └── models.ts
│ │ │ │ ├── spotify-integration/
│ │ │ │ │ ├── spotify-integration.service.ts
│ │ │ │ │ ├── spotify.routes.ts
│ │ │ │ │ └── models.ts
│ │ │ │ └── pomodoro-timer/
│ │ │ │ ├── pomodoro-timer.service.ts
│ │ │ │ ├── pomodoro-config.service.ts
│ │ │ │ ├── pomodoro-stats.service.ts
│ │ │ │ └── pomodoro.routes.ts
│ │ │ ├── infrastructure/ # Config, logging, rate-limiting
│ │ │ │ ├── config/
│ │ │ │ ├── logging/
│ │ │ │ └── rate-limiting/
│ │ │ └── shared/ # Bot commands compartidos (!hello, !help)
│ │ ├── app.ts # Punto de entrada
│ │ └── package.json
│ │
│ └── obs-overlay/ # Frontend React para OBS
│ ├── src/
│ │ ├── features/
│ │ │ ├── stream-task-display/ # Lista de tareas en stream
│ │ │ │ ├── stream-task-display.tsx
│ │ │ │ └── components/
│ │ │ ├── now-playing-display/ # Widget de Spotify
│ │ │ │ ├── now-playing-display.tsx
│ │ │ │ └── components/
│ │ │ └── pomodoro-display/ # Timer Pomodoro
│ │ │ ├── pomodoro-display.tsx
│ │ │ ├── components/
│ │ │ └── hooks/
│ │ ├── shared/ # Estilos y componentes globales
│ │ └── App.tsx # Orquestador de features
│ └── package.json
│
├── pnpm-workspace.yaml
└── package.json
"El scope determina la estructura"
- Código usado por 2+ features → DEBE ir en
shared/oinfrastructure/ - Código usado por 1 feature → DEBE quedarse local en esa feature
- SIN EXCEPCIONES
Ejemplo:
Tasktype está enpackages/shared/porque lo usanbot-backendyobs-overlaytask-management.service.tsestá enfeatures/task-management/porque solo esa feature lo usa
La estructura grita la funcionalidad del negocio:
stream-task-display- Muestra tareas en el streamnow-playing-display- Muestra la canción actual de Spotifytask-management- Gestiona tareas de usuariospomodoro-timer- Sistema de gestión de tiempo con técnica Pomodoro
Los nombres de features describen qué hace la aplicación, no cómo lo implementa.
- Containers: Manejan lógica de negocio, estado y datos (e.g.,
stream-task-display.tsx) - Presentational: Componentes puros que reciben props (e.g.,
components/TaskItem.tsx) - El container principal DEBE tener el mismo nombre que la feature
Usuario en Twitch Chat
|
| (!task estudiar)
v
bot-backend/app.ts (Cliente IRC)
|
| Parsea comando
v
features/task-management/commands/task.command.ts
|
| Llama a servicio
v
features/task-management/task-management.service.ts
|
| CRUD en tasks.json
v
data/tasks.json
|
| WebSocket event
v
obs-overlay/features/stream-task-display/
|
| Renderiza
v
OBS Overlay en stream
| Patrón | Ubicación | Justificación |
|---|---|---|
| Command Pattern | bot-backend/features/*/commands/ |
Cada comando es un módulo independiente |
| Service Layer | bot-backend/features/*/services |
Abstrae lógica de negocio |
| Repository Pattern | task-management.service.ts |
Abstrae acceso a datos |
| Monorepo | packages/* |
Comparte tipos entre frontend y backend |
# Instalar dependencias de todo el monorepo
pnpm install
# Desarrollo completo (backend + overlay + bot)
pnpm run dev:full
# Solo backend
pnpm run dev
# Solo overlay
pnpm run dev:overlay
# Tests
pnpm test
# Build de todos los paquetes
pnpm run build
# Linting
pnpm run lintSe opto por un diseño minimalista que permita a los usuarios enfocarse en sesiones de estudio y de trabajo
Se puede ver el diseño implementado en figma a través del siguiente link:
Decisión: Migrar de JavaScript a TypeScript para el backend.
Razón: El proyecto inicialmente estaba en JavaScript, pero conforme crecía la base de código, los errores de tipo en runtime se volvieron más frecuentes. TypeScript proporciona:
- Detección temprana de errores en tiempo de desarrollo
- Mejor autocompletado y documentación en el IDE
- Refactorización más segura al modificar interfaces de servicios
Trade-off: Añade complejidad en la configuración inicial y requiere un paso de compilación, pero el beneficio en mantenibilidad supera el costo.
Decisión: Reorganizar el proyecto en un monorepo con tres paquetes: shared, bot-backend, y obs-overlay.
Razón:
- Single Source of Truth: Los tipos de
TaskySpotifyTrackestán enpackages/sharedy se comparten entre backend y frontend - Independencia: Cada paquete puede tener sus propias dependencias y configuración
- Escalabilidad: Facilita añadir nuevos paquetes (e.g., un CLI, una app móvil)
Implementación: Utilizamos pnpm workspaces para gestionar dependencias entre paquetes de forma eficiente.
Decisión: Aplicar estrictamente la Scope Rule para determinar dónde colocar cada archivo.
Razón:
- Previene duplicación: Si un tipo o función se usa en 2+ lugares, automáticamente se mueve a
shared - Facilita comprensión: Un desarrollador nuevo puede entender qué hace la app solo viendo los nombres de las features
- Escalabilidad: Cuando se añade una feature, es claro dónde va cada archivo
Ejemplo práctico:
CompactTaskListcomponent está enstream-task-display/components/porque solo esa feature lo usaglobal-styles.tsestá enobs-overlay/shared/porquestream-task-displayynow-playing-displaylo usan
Decisión: Usar un archivo JSON para persistir datos en lugar de una base de datos (MySQL, PostgreSQL, MongoDB).
Razón:
- Simplicidad: Para un MVP con decenas de usuarios concurrentes, un archivo JSON es suficiente
- Cero configuración: No requiere instalación de servidor de base de datos ni gestión de conexiones
- Portabilidad: El archivo
tasks.jsones autodocumentado y fácil de inspeccionar
Limitaciones conocidas:
- No es adecuado para alta concurrencia (múltiples escrituras simultáneas pueden causar corrupción)
- No tiene capacidades de consulta avanzadas
- Escala verticalmente (todo en memoria)
Decisión: Implementar un sistema de rate limiting propio en infrastructure/rate-limiting/rateLimiter.ts.
Razón:
- Control granular por usuario de Twitch (no por IP)
- Lógica específica: limitar comandos por usuario en ventanas de tiempo
- Aprendizaje: implementar el algoritmo de "token bucket" desde cero
Implementación: Cada usuario tiene un bucket de tokens que se regenera cada X segundos. Esto previene spam sin afectar usuarios legítimos.
Decisión: Añadir un servidor Express para manejar OAuth 2.0 con Spotify y exponer endpoints HTTP.
Razón:
- El protocolo OAuth requiere un callback HTTP (no se puede hacer solo con IRC de Twitch)
- Permite que el overlay de OBS consuma datos mediante HTTP/WebSockets
- Separa preocupaciones: el bot IRC maneja comandos, el servidor Express maneja APIs externas
Flujo implementado:
- Usuario autentifica con Spotify vía
/auth - Callback recibe el token y lo almacena
- Overlay consulta
/api/spotify/current-trackpara mostrar la canción actual
- Node.js >= 18.0.0
- pnpm instalado
- Cuenta de Twitch con token OAuth (Generar aquí)
- (Opcional) Cuenta de Spotify Developer para integración musical
# 1. Clonar el repositorio
git clone https://github.com/juancholopes/bot-twitch.git
cd bot-twitch
# 2. Instalar dependencias (todo el monorepo)
pnpm install
# 3. Configurar variables de entorno
cp .env.example .env
# Editar .env con tus credenciales
# Para Supabase puedes usar .env.example.supabase como guía
# También copiar .env en el paquete del backend
cp .env packages/bot-backend/.env
# 4. Compilar paquetes
pnpm run build
# 5. Ejecutar en desarrollo (backend + overlay)
pnpm run dev:fullCrear un archivo .env en la raíz del proyecto y en packages/bot-backend/:
# Configuración del servidor
PORT=3000
# Credenciales de Twitch
OAUTH_TOKEN=oauth:tu_token_aqui
TWITCH_USERNAME=nombre_del_bot
CHANEL_NAME=canal_donde_opera
# Supabase (backend)
SUPABASE_URL=https://tu-proyecto.supabase.co
SUPABASE_SERVICE_ROLE_KEY=tu_service_role_key
# Supabase (overlay)
VITE_SUPABASE_URL=https://tu-proyecto.supabase.co
VITE_SUPABASE_ANON_KEY=tu_anon_key
# Credenciales de Spotify (opcional)
CLIENT_ID=tu_client_id
CLIENT_SECRET=tu_client_secret
REFRESH_TOKEN=tu_refresh_tokenCómo obtener credenciales:
| Servicio | Guía |
|---|---|
| Twitch OAuth | Visitar https://twitchapps.com/tmi/ y seguir instrucciones |
| Spotify API | Crear app en Spotify Dashboard y configurar redirect URI |
# Verificar que el servidor inicia correctamente
pnpm run dev:full
# En otra terminal, verificar el endpoint de salud
curl http://localhost:3000/health
# Debe responder: {"status":"ok"}| Comando | Parámetros | Descripción | Ejemplo | Restricciones |
|---|---|---|---|---|
!task |
<lista_tareas> |
Agrega tareas personales separadas por coma | !task estudiar TS, hacer ejercicio |
Máximo 5 tareas por comando, máximo 5 tareas totales por usuario |
!mytasks / !list |
- | Muestra tus tareas pendientes y completadas | !mytasks |
Cooldown de 10 segundos |
!done |
<números> |
Marca tareas como completadas por su número | !done 1, 3 |
Solo tareas propias |
!cleardone |
- | Elimina todas tus tareas completadas | !cleardone |
Libera espacio para nuevas tareas |
!hello |
- | Saludo del bot | !hello |
- |
!help |
- | Muestra ayuda de comandos | !help |
- |
| Comando | Descripción | Ejemplo | Precaución |
|---|---|---|---|
!delete |
Elimina todas las tareas de todos los usuarios | !delete |
Acción irreversible, usar con cuidado |
LÍMITES POR USUARIO
- Máximo 5 tareas totales (pendientes + completadas)
- Máximo 5 tareas por comando !task
- Cooldown de 10 segundos entre comandos
FORMATO DE DATOS
- Las tareas se almacenan en MAYÚSCULAS internamente
- Se muestran en minúsculas al usuario para legibilidad
- Persistencia principal en Supabase (legacy JSON en ./data/tasks.json solo para backup/migración)
| Fase | Tiempo estimado | Detalles |
|---|---|---|
| Setup inicial + MVP (comandos básicos) | 8-10 horas | Configuración de Twitch IRC, comandos !task y !mytasks, persistencia JSON |
| Migración a TypeScript | 6-8 horas | Refactorización completa, configuración de tsconfig, tipado de interfaces |
| Integración de Spotify | 10-12 horas | Implementación OAuth 2.0, manejo de refresh tokens, endpoints HTTP |
| Overlay OBS con React | 12-15 horas | Setup de Vite, componentes React, WebSockets para actualización en tiempo real |
| Refactoring a Monorepo | 8-10 horas | Reorganización en packages, Scope Rule, Screaming Architecture |
| Testing y documentación | 5-6 horas | Pruebas unitarias con Jest, escritura de README |
| Total estimado | 49-61 horas | Proyecto desarrollado en ~3 semanas (part-time) |
| Funcionalidad | Complejidad | Tiempo para nueva feature similar | Desafíos técnicos |
|---|---|---|---|
| Añadir nuevo comando simple | Baja | 1-2 horas | Principalmente boilerplate, seguir patrón existente |
| Modificar lógica de persistencia | Media | 4-6 horas | Requiere cambios en service y manejo de migraciones de datos |
| Integrar nueva API externa | Alta | 8-12 horas | OAuth, manejo de errores de red, rate limits de la API externa |
| Añadir base de datos real (PostgreSQL) | Alta | 16-20 horas | Diseño de esquema, ORM (Prisma), migraciones, testing con DB de prueba |
RIESGOS IDENTIFICADOS
1. Rate limits de Spotify API
- Impacto: Overlay deja de actualizar
- Mitigación: Caché de 30 segundos, manejo de errores 429
2. Corrupción de tasks.json en escrituras concurrentes
- Impacto: Pérdida de datos de usuarios
- Mitigación: File locking (implementar en 3-4 horas)
3. Cambio en API de Twitch IRC
- Impacto: Bot deja de recibir mensajes
- Mitigación: Monitoreo de deprecation notices de tmi.js
Por qué:
El archivo JSON funciona para un MVP, pero en producción con 100+ usuarios concurrentes podría causar:
- Race conditions en escrituras simultáneas
- Lentitud al cargar todo el archivo en memoria
- Dificultad para hacer queries complejas (ej: "top 10 usuarios con más tareas completadas")
Solución:
Usaría PostgreSQL con Prisma ORM porque:
- Prisma genera tipos TypeScript automáticamente desde el esquema
- Transacciones ACID previenen corrupción de datos
- Facilita migraciones de esquema con
prisma migrate
Tiempo de implementación: 12-16 horas adicionales inicialmente, pero ahorra debugging futuro.
Problema actual:
El overlay hace polling cada 5 segundos para obtener actualizaciones de tareas. Esto causa:
- Latencia de hasta 5 segundos para ver cambios
- Desperdicio de recursos en requests HTTP innecesarios
Solución:
Implementar WebSockets bidireccionales con Socket.io:
// Cuando un usuario completa una tarea en el chat
taskService.completeTask(userId, taskId)
.then(() => {
io.emit('task:updated', { userId, taskId }); // Notifica al overlay en tiempo real
});Beneficio: Actualización instantánea en OBS sin polling.
Tiempo de implementación: 6-8 horas (Socket.io está parcialmente implementado pero no integrado).
Limitación actual:
Solo hay dos roles: streamer (puede !delete) y usuario normal. No hay moderadores ni VIPs con permisos especiales.
Mejora:
Implementar un sistema de roles basado en Twitch badges:
enum UserRole {
VIEWER = 0,
SUBSCRIBER = 1,
VIP = 2,
MODERATOR = 3,
BROADCASTER = 4
}
// Configuración por comando
const permissions = {
'!delete': UserRole.MODERATOR, // Mods también pueden limpiar tareas
'!task': UserRole.VIEWER
};Beneficio: Mayor flexibilidad para delegar administración del bot.
Tiempo de implementación: 3-4 horas.
Carencia actual:
Solo hay tests unitarios de servicios individuales. No se prueba el flujo completo de un comando (recibir mensaje → procesar → responder).
Solución:
Usar supertest para tests de integración de endpoints HTTP y mock de tmi.js para simular mensajes de chat:
describe('!task command integration', () => {
it('should add task and persist to database', async () => {
const mockMessage = createMockTwitchMessage('!task estudiar TS');
await handleCommand(mockMessage);
const tasks = await taskService.getTasks('testuser');
expect(tasks).toContainEqual({ text: 'ESTUDIAR TS', completed: false });
});
});Tiempo de implementación: 10-12 horas para suite completa.
Problema actual:
Para cambiar límites como "máximo 5 tareas por usuario", hay que modificar el código y recompilar.
Solución:
Archivo config.json con valores por defecto:
{
"tasks": {
"maxPerUser": 5,
"maxPerCommand": 5,
"cooldownSeconds": 10
},
"rateLimiting": {
"enabled": true,
"tokensPerMinute": 5
}
}Beneficio: El streamer puede ajustar configuración sin conocimientos técnicos.
Tiempo de implementación: 2-3 horas.
CLEAN CODE
- Funciones pequeñas (máximo 20 líneas)
- Nombres descriptivos (getUserTasks vs getData)
- Evitar comentarios innecesarios (el código se explica)
SOLID PRINCIPLES
- Single Responsibility: Cada servicio hace una cosa
- Open/Closed: Comandos extendibles sin modificar core
- Dependency Inversion: Services inyectados, no importados
ERROR HANDLING
- Try-catch en todas las operaciones I/O
- Errores específicos con mensajes claros
- Logging de errores con stack trace
- Graceful degradation (si Spotify falla, bot sigue activo)
Escenario: Quieres añadir un comando !stats que muestre estadísticas del usuario.
Tiempo estimado: 1.5 - 2 horas
# 1. Crear archivo del comando (10 min)
touch packages/bot-backend/src/features/task-management/commands/stats.command.ts
# 2. Implementar la lógica (45 min)
# Ver ejemplo en packages/bot-backend/src/features/task-management/commands/mytasks.command.ts
# 3. Exportar en index (5 min)
# Añadir en packages/bot-backend/src/features/task-management/commands/index.ts
# 4. Registrar en el bot (10 min)
# Añadir case en packages/bot-backend/app.ts
# 5. Tests unitarios (20 min)
# Crear packages/bot-backend/src/features/task-management/commands/__tests__/stats.command.test.ts
# 6. Documentación (10 min)
# Actualizar README.mdPlantilla de comando:
// packages/bot-backend/src/features/task-management/commands/stats.command.ts
import { Client } from 'tmi.js';
import { taskManagementService } from '../task-management.service';
import { logger } from '@infrastructure/logging';
export async function handleStatsCommand(
channel: string,
user: string,
client: Client
): Promise<void> {
try {
const stats = await taskManagementService.getUserStats(user);
client.say(
channel,
`@${user} - Tareas totales: ${stats.total}, Completadas: ${stats.completed}, Pendientes: ${stats.pending}`
);
logger.info(`Stats shown for ${user}`);
} catch (error) {
logger.error(`Error in stats command: ${error}`);
client.say(channel, `@${user}, hubo un error al obtener tus estadísticas.`);
}
}El proyecto usa un logger centralizado con niveles de severidad:
| Nivel | Uso | Ejemplo |
|---|---|---|
DEBUG |
Información de desarrollo | logger.debug('User data:', userData) |
INFO |
Operaciones normales | logger.info('Task added for user123') |
WARN |
Situaciones anómalas no críticas | logger.warn('Rate limit approaching for user') |
ERROR |
Errores capturados | logger.error('Failed to save tasks:', error) |
Configuración: En producción, solo se muestran niveles INFO y superiores (configurar LOG_LEVEL en .env).
| Método | Ruta | Descripción | Respuesta |
|---|---|---|---|
GET |
/health |
Health check | {"status":"ok"} |
GET |
/api/spotify/current-track |
Canción actual de Spotify | {"name":"Song","artist":"Artist","isPlaying":true} |
GET |
/api/tasks |
Todas las tareas | [{user, tasks, completed}] |
GET |
/api/pomodoro/state |
Estado actual del timer Pomodoro | {"phase":"work","remainingSeconds":4800,"isRunning":true} |
GET |
/api/pomodoro/config |
Configuración del timer | {"workDuration":80,"shortBreakDuration":10} |
GET |
/api/pomodoro/stats/today |
Estadísticas del día | {"sessionsCompleted":3,"totalWorkTime":240} |
Nota: Los endpoints /api/* están protegidos con CORS configurado para permitir solo el origen del overlay.
Aplicando Scope Rule:
- Crear el directorio de la feature:
mkdir -p packages/obs-overlay/src/features/viewer-count-display- Crear el container (mismo nombre que la feature):
// packages/obs-overlay/src/features/viewer-count-display/viewer-count-display.tsx
import React from 'react';
export function ViewerCountDisplay() {
// Lógica y estado aquí
return <div>Viewers: {count}</div>;
}- Componentes locales si solo esta feature los usa:
mkdir packages/obs-overlay/src/features/viewer-count-display/components- Si 2+ features usan un componente, moverlo a shared:
mv component packages/obs-overlay/src/shared/components/- tmi.js Documentation - Cliente IRC de Twitch
- Spotify Web API - Integración musical
- TypeScript Handbook - Guía de TypeScript
- React 19 Documentation - Framework del overlay
- pnpm Workspaces - Gestión del monorepo
Juan Carlos López
ISC