Implementación inicial del sistema de automatización de redes sociales

- Estructura completa del proyecto con FastAPI
- Modelos de base de datos (productos, servicios, posts, calendario, interacciones)
- Publishers para X, Threads, Instagram, Facebook
- Generador de contenido con DeepSeek API
- Worker de Celery con tareas programadas
- Dashboard básico con templates HTML
- Docker Compose para despliegue
- Documentación completa

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 01:11:44 +00:00
commit 049d2133f9
53 changed files with 5876 additions and 0 deletions

24
app/models/__init__.py Normal file
View File

@@ -0,0 +1,24 @@
"""
Modelos de base de datos SQLAlchemy.
"""
from app.core.database import Base
from app.models.product import Product
from app.models.service import Service
from app.models.tip_template import TipTemplate
from app.models.post import Post
from app.models.content_calendar import ContentCalendar
from app.models.image_template import ImageTemplate
from app.models.interaction import Interaction
__all__ = [
"Base",
"Product",
"Service",
"TipTemplate",
"Post",
"ContentCalendar",
"ImageTemplate",
"Interaction"
]

View File

@@ -0,0 +1,66 @@
"""
Modelo de ContentCalendar - Calendario de publicación.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Time, Boolean, DateTime
from sqlalchemy.dialects.postgresql import ARRAY
from app.core.database import Base
class ContentCalendar(Base):
"""Modelo para el calendario de contenido."""
__tablename__ = "content_calendar"
id = Column(Integer, primary_key=True, index=True)
# Programación
day_of_week = Column(Integer, nullable=False) # 0=Lunes, 6=Domingo
time = Column(Time, nullable=False) # Hora de publicación
# Tipo de contenido
content_type = Column(String(50), nullable=False)
# Tipos: tip_tech, dato_curioso, producto, servicio, etc.
# Plataformas
platforms = Column(ARRAY(String), nullable=False)
# Ejemplo: ["x", "threads", "instagram"]
# Configuración
is_active = Column(Boolean, default=True)
requires_approval = Column(Boolean, default=False)
# Restricciones opcionales
category_filter = Column(String(100), nullable=True) # Solo tips de esta categoría
priority = Column(Integer, default=0) # Mayor = más prioritario
# Descripción
description = Column(String(255), nullable=True)
# Ejemplo: "Tip tech diario matutino"
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<ContentCalendar {self.day_of_week} {self.time} - {self.content_type}>"
def to_dict(self):
"""Convertir a diccionario."""
days = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"]
return {
"id": self.id,
"day_of_week": self.day_of_week,
"day_name": days[self.day_of_week] if 0 <= self.day_of_week <= 6 else "Desconocido",
"time": self.time.strftime("%H:%M") if self.time else None,
"content_type": self.content_type,
"platforms": self.platforms,
"is_active": self.is_active,
"requires_approval": self.requires_approval,
"category_filter": self.category_filter,
"priority": self.priority,
"description": self.description,
"created_at": self.created_at.isoformat() if self.created_at else None
}

View File

@@ -0,0 +1,75 @@
"""
Modelo de ImageTemplate - Plantillas para generar imágenes.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.dialects.postgresql import ARRAY
from app.core.database import Base
class ImageTemplate(Base):
"""Modelo para plantillas de imágenes."""
__tablename__ = "image_templates"
id = Column(Integer, primary_key=True, index=True)
# Información básica
name = Column(String(100), nullable=False, index=True)
description = Column(String(255), nullable=True)
# Categoría
category = Column(String(50), nullable=False)
# Categorías: tip, producto, servicio, promocion, etc.
# Archivo de plantilla
template_file = Column(String(255), nullable=False) # Ruta al archivo HTML/template
# Variables que acepta la plantilla
variables = Column(ARRAY(String), nullable=False)
# Ejemplo: ["titulo", "contenido", "hashtags", "logo"]
# Configuración de diseño
design_config = Column(JSON, nullable=True)
# Ejemplo: {
# "width": 1080,
# "height": 1080,
# "background_color": "#1a1a2e",
# "accent_color": "#d4a574",
# "font_family": "Inter"
# }
# Tamaños de salida
output_sizes = Column(JSON, nullable=True)
# Ejemplo: {
# "instagram": {"width": 1080, "height": 1080},
# "x": {"width": 1200, "height": 675},
# "facebook": {"width": 1200, "height": 630}
# }
# Estado
is_active = Column(Boolean, default=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<ImageTemplate {self.name}>"
def to_dict(self):
"""Convertir a diccionario."""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"category": self.category,
"template_file": self.template_file,
"variables": self.variables,
"design_config": self.design_config,
"output_sizes": self.output_sizes,
"is_active": self.is_active,
"created_at": self.created_at.isoformat() if self.created_at else None
}

87
app/models/interaction.py Normal file
View File

@@ -0,0 +1,87 @@
"""
Modelo de Interaction - Interacciones en redes sociales.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey
from app.core.database import Base
class Interaction(Base):
"""Modelo para interacciones (comentarios, DMs, menciones)."""
__tablename__ = "interactions"
id = Column(Integer, primary_key=True, index=True)
# Plataforma
platform = Column(String(50), nullable=False, index=True)
# Valores: x, threads, instagram, facebook
# Tipo de interacción
interaction_type = Column(String(50), nullable=False, index=True)
# Tipos: comment, reply, dm, mention, like, repost, quote
# Post relacionado (si aplica)
post_id = Column(Integer, ForeignKey("posts.id"), nullable=True)
# ID externo en la plataforma
external_id = Column(String(100), nullable=False, unique=True)
external_post_id = Column(String(100), nullable=True) # ID del post en la plataforma
# Autor de la interacción
author_username = Column(String(100), nullable=False)
author_name = Column(String(255), nullable=True)
author_profile_url = Column(String(500), nullable=True)
author_avatar_url = Column(String(500), nullable=True)
# Contenido
content = Column(Text, nullable=True)
# Respuesta
responded = Column(Boolean, default=False, index=True)
response_content = Column(Text, nullable=True)
responded_at = Column(DateTime, nullable=True)
response_external_id = Column(String(100), nullable=True)
# Clasificación
is_lead = Column(Boolean, default=False) # Potencial cliente
sentiment = Column(String(50), nullable=True) # positive, negative, neutral
priority = Column(Integer, default=0) # 0=normal, 1=importante, 2=urgente
# Estado
is_read = Column(Boolean, default=False)
is_archived = Column(Boolean, default=False)
# Timestamps
interaction_at = Column(DateTime, nullable=False) # Cuándo ocurrió en la plataforma
created_at = Column(DateTime, default=datetime.utcnow) # Cuándo se guardó aquí
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<Interaction {self.platform} - {self.interaction_type} - {self.author_username}>"
def to_dict(self):
"""Convertir a diccionario."""
return {
"id": self.id,
"platform": self.platform,
"interaction_type": self.interaction_type,
"post_id": self.post_id,
"external_id": self.external_id,
"author_username": self.author_username,
"author_name": self.author_name,
"author_profile_url": self.author_profile_url,
"author_avatar_url": self.author_avatar_url,
"content": self.content,
"responded": self.responded,
"response_content": self.response_content,
"responded_at": self.responded_at.isoformat() if self.responded_at else None,
"is_lead": self.is_lead,
"sentiment": self.sentiment,
"priority": self.priority,
"is_read": self.is_read,
"interaction_at": self.interaction_at.isoformat() if self.interaction_at else None,
"created_at": self.created_at.isoformat() if self.created_at else None
}

134
app/models/post.py Normal file
View File

@@ -0,0 +1,134 @@
"""
Modelo de Post - Posts generados y programados.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, JSON, Enum
from sqlalchemy.dialects.postgresql import ARRAY
import enum
from app.core.database import Base
class PostStatus(enum.Enum):
"""Estados posibles de un post."""
DRAFT = "draft"
PENDING_APPROVAL = "pending_approval"
APPROVED = "approved"
SCHEDULED = "scheduled"
PUBLISHING = "publishing"
PUBLISHED = "published"
FAILED = "failed"
CANCELLED = "cancelled"
class ContentType(enum.Enum):
"""Tipos de contenido."""
TIP_TECH = "tip_tech"
DATO_CURIOSO = "dato_curioso"
FRASE_MOTIVACIONAL = "frase_motivacional"
EFEMERIDE = "efemeride"
PRODUCTO = "producto"
SERVICIO = "servicio"
HILO_EDUCATIVO = "hilo_educativo"
CASO_EXITO = "caso_exito"
PROMOCION = "promocion"
ANUNCIO = "anuncio"
MANUAL = "manual"
class Post(Base):
"""Modelo para posts de redes sociales."""
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
# Contenido
content = Column(Text, nullable=False)
content_type = Column(String(50), nullable=False, index=True)
# Contenido adaptado por plataforma (opcional)
content_x = Column(Text, nullable=True) # Versión para X (280 chars)
content_threads = Column(Text, nullable=True)
content_instagram = Column(Text, nullable=True)
content_facebook = Column(Text, nullable=True)
# Plataformas destino
platforms = Column(ARRAY(String), nullable=False)
# Ejemplo: ["x", "threads", "instagram", "facebook"]
# Estado y programación
status = Column(String(50), default="draft", index=True)
scheduled_at = Column(DateTime, nullable=True, index=True)
published_at = Column(DateTime, nullable=True)
# Imagen
image_url = Column(String(500), nullable=True)
image_template_id = Column(Integer, ForeignKey("image_templates.id"), nullable=True)
# IDs de publicación en cada plataforma
platform_post_ids = Column(JSON, nullable=True)
# Ejemplo: {"x": "123456", "instagram": "789012", ...}
# Errores de publicación
error_message = Column(Text, nullable=True)
retry_count = Column(Integer, default=0)
# Aprobación
approval_required = Column(Boolean, default=False)
approved_by = Column(String(100), nullable=True)
approved_at = Column(DateTime, nullable=True)
# Relaciones con contenido fuente
product_id = Column(Integer, ForeignKey("products.id"), nullable=True)
service_id = Column(Integer, ForeignKey("services.id"), nullable=True)
tip_template_id = Column(Integer, ForeignKey("tip_templates.id"), nullable=True)
# Metadatos
hashtags = Column(ARRAY(String), nullable=True)
mentions = Column(ARRAY(String), nullable=True)
# Métricas (actualizadas después de publicar)
metrics = Column(JSON, nullable=True)
# Ejemplo: {"likes": 10, "retweets": 5, "comments": 3}
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<Post {self.id} - {self.status}>"
def to_dict(self):
"""Convertir a diccionario."""
return {
"id": self.id,
"content": self.content,
"content_type": self.content_type,
"content_x": self.content_x,
"content_threads": self.content_threads,
"content_instagram": self.content_instagram,
"content_facebook": self.content_facebook,
"platforms": self.platforms,
"status": self.status,
"scheduled_at": self.scheduled_at.isoformat() if self.scheduled_at else None,
"published_at": self.published_at.isoformat() if self.published_at else None,
"image_url": self.image_url,
"platform_post_ids": self.platform_post_ids,
"error_message": self.error_message,
"approval_required": self.approval_required,
"hashtags": self.hashtags,
"metrics": self.metrics,
"created_at": self.created_at.isoformat() if self.created_at else None
}
def get_content_for_platform(self, platform: str) -> str:
"""Obtener contenido adaptado para una plataforma específica."""
platform_content = {
"x": self.content_x,
"threads": self.content_threads,
"instagram": self.content_instagram,
"facebook": self.content_facebook
}
return platform_content.get(platform) or self.content

80
app/models/product.py Normal file
View File

@@ -0,0 +1,80 @@
"""
Modelo de Producto - Catálogo de equipos de cómputo e impresoras 3D.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, Float, Boolean, DateTime, JSON
from sqlalchemy.dialects.postgresql import ARRAY
from app.core.database import Base
class Product(Base):
"""Modelo para productos del catálogo."""
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
# Información básica
name = Column(String(255), nullable=False, index=True)
description = Column(Text, nullable=True)
short_description = Column(String(500), nullable=True) # Para posts
# Categorización
category = Column(String(100), nullable=False, index=True) # laptop, desktop, impresora_3d, etc.
subcategory = Column(String(100), nullable=True)
brand = Column(String(100), nullable=True)
model = Column(String(100), nullable=True)
# Precio y stock
price = Column(Float, nullable=False)
price_usd = Column(Float, nullable=True) # Precio en dólares (opcional)
stock = Column(Integer, default=0)
is_available = Column(Boolean, default=True)
# Especificaciones técnicas (JSON flexible)
specs = Column(JSON, nullable=True)
# Ejemplo: {"cpu": "Intel i7", "ram": "16GB", "storage": "512GB SSD"}
# Imágenes
images = Column(ARRAY(String), nullable=True) # URLs de imágenes
main_image = Column(String(500), nullable=True)
# SEO y marketing
tags = Column(ARRAY(String), nullable=True) # Tags para búsqueda
highlights = Column(ARRAY(String), nullable=True) # Puntos destacados
# Control de publicación
is_featured = Column(Boolean, default=False) # Producto destacado
last_posted_at = Column(DateTime, nullable=True) # Última vez que se publicó
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<Product {self.name}>"
def to_dict(self):
"""Convertir a diccionario."""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"short_description": self.short_description,
"category": self.category,
"subcategory": self.subcategory,
"brand": self.brand,
"model": self.model,
"price": self.price,
"stock": self.stock,
"is_available": self.is_available,
"specs": self.specs,
"images": self.images,
"main_image": self.main_image,
"tags": self.tags,
"highlights": self.highlights,
"is_featured": self.is_featured,
"created_at": self.created_at.isoformat() if self.created_at else None
}

90
app/models/service.py Normal file
View File

@@ -0,0 +1,90 @@
"""
Modelo de Servicio - Servicios de consultoría de Consultoría AS.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.dialects.postgresql import ARRAY
from app.core.database import Base
class Service(Base):
"""Modelo para servicios de consultoría."""
__tablename__ = "services"
id = Column(Integer, primary_key=True, index=True)
# Información básica
name = Column(String(255), nullable=False, index=True)
description = Column(Text, nullable=False)
short_description = Column(String(500), nullable=True) # Para posts
# Categorización
category = Column(String(100), nullable=False, index=True)
# Categorías: ia, automatizacion, desarrollo, iot, voip, crm, etc.
# Sectores objetivo
target_sectors = Column(ARRAY(String), nullable=True)
# Ejemplo: ["hoteles", "construccion", "logistica", "retail"]
# Beneficios y características
benefits = Column(ARRAY(String), nullable=True)
# Ejemplo: ["Ahorro de tiempo", "Reducción de errores", "24/7"]
features = Column(ARRAY(String), nullable=True)
# Características técnicas del servicio
# Casos de éxito
case_studies = Column(JSON, nullable=True)
# Ejemplo: [{"client": "Hotel X", "result": "50% menos tiempo en reservas"}]
# Imágenes e íconos
icon = Column(String(100), nullable=True) # Nombre del ícono
images = Column(ARRAY(String), nullable=True)
main_image = Column(String(500), nullable=True)
# Precios (opcional, puede ser "cotizar")
price_range = Column(String(100), nullable=True) # "Desde $5,000 MXN"
has_free_demo = Column(Boolean, default=False)
# SEO y marketing
tags = Column(ARRAY(String), nullable=True)
call_to_action = Column(String(255), nullable=True) # "Agenda una demo"
# Control de publicación
is_active = Column(Boolean, default=True)
is_featured = Column(Boolean, default=False)
last_posted_at = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<Service {self.name}>"
def to_dict(self):
"""Convertir a diccionario."""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"short_description": self.short_description,
"category": self.category,
"target_sectors": self.target_sectors,
"benefits": self.benefits,
"features": self.features,
"case_studies": self.case_studies,
"icon": self.icon,
"images": self.images,
"main_image": self.main_image,
"price_range": self.price_range,
"has_free_demo": self.has_free_demo,
"tags": self.tags,
"call_to_action": self.call_to_action,
"is_active": self.is_active,
"is_featured": self.is_featured,
"created_at": self.created_at.isoformat() if self.created_at else None
}

View File

@@ -0,0 +1,77 @@
"""
Modelo de TipTemplate - Banco de tips para generar contenido automático.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime
from sqlalchemy.dialects.postgresql import ARRAY
from app.core.database import Base
class TipTemplate(Base):
"""Modelo para templates de tips tech."""
__tablename__ = "tip_templates"
id = Column(Integer, primary_key=True, index=True)
# Categoría del tip
category = Column(String(100), nullable=False, index=True)
# Categorías: hardware, software, seguridad, productividad, ia, redes, etc.
subcategory = Column(String(100), nullable=True)
# Contenido del tip
title = Column(String(255), nullable=False) # Título corto
template = Column(Text, nullable=False) # Template con variables
# Variables que se pueden reemplazar
# Ejemplo: ["sistema_operativo", "herramienta", "beneficio"]
variables = Column(ARRAY(String), nullable=True)
# Variaciones del tip (diferentes formas de decirlo)
variations = Column(ARRAY(String), nullable=True)
# Metadatos
difficulty = Column(String(50), nullable=True) # basico, intermedio, avanzado
target_audience = Column(String(100), nullable=True) # empresas, desarrolladores, general
# Control de uso
used_count = Column(Integer, default=0)
last_used = Column(DateTime, nullable=True)
# Plataformas recomendadas
recommended_platforms = Column(ARRAY(String), nullable=True)
# Ejemplo: ["x", "threads", "instagram"]
# Estado
is_active = Column(Boolean, default=True)
is_evergreen = Column(Boolean, default=True) # ¿Siempre relevante?
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<TipTemplate {self.title}>"
def to_dict(self):
"""Convertir a diccionario."""
return {
"id": self.id,
"category": self.category,
"subcategory": self.subcategory,
"title": self.title,
"template": self.template,
"variables": self.variables,
"variations": self.variations,
"difficulty": self.difficulty,
"target_audience": self.target_audience,
"used_count": self.used_count,
"last_used": self.last_used.isoformat() if self.last_used else None,
"recommended_platforms": self.recommended_platforms,
"is_active": self.is_active,
"is_evergreen": self.is_evergreen,
"created_at": self.created_at.isoformat() if self.created_at else None
}