""" Thread Series Models - Multi-post thread content scheduling. """ from datetime import datetime from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Boolean from sqlalchemy.orm import relationship import enum from app.core.database import Base class ThreadSeriesStatus(enum.Enum): """Status options for thread series.""" DRAFT = "draft" SCHEDULED = "scheduled" PUBLISHING = "publishing" COMPLETED = "completed" PAUSED = "paused" CANCELLED = "cancelled" class ThreadSeries(Base): """ A series of related posts published as a thread. E.g., Educational threads, story threads, tip series. """ __tablename__ = "thread_series" id = Column(Integer, primary_key=True, index=True) # Series info name = Column(String(255), nullable=False) description = Column(Text, nullable=True) topic = Column(String(100), nullable=True) # Platform (threads work best on X and Threads) platform = Column(String(50), nullable=False, index=True) # Schedule configuration schedule_type = Column(String(20), default="sequential") # sequential: posts one after another # timed: posts at specific intervals interval_minutes = Column(Integer, default=5) # Time between posts start_time = Column(DateTime, nullable=True) # When to start publishing # Thread structure total_posts = Column(Integer, default=0) posts_published = Column(Integer, default=0) # Status status = Column(String(20), default="draft", index=True) # First post in chain (for reply chain) first_platform_post_id = Column(String(100), nullable=True) # AI generation settings ai_generated = Column(Boolean, default=False) generation_prompt = Column(Text, nullable=True) # Metadata hashtags = Column(JSON, nullable=True) # Common hashtags for the series created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) completed_at = Column(DateTime, nullable=True) # Relationships posts = relationship("ThreadPost", back_populates="series", order_by="ThreadPost.sequence_number") def __repr__(self): return f"" def to_dict(self, include_posts: bool = True): """Convert to dictionary.""" result = { "id": self.id, "name": self.name, "description": self.description, "topic": self.topic, "platform": self.platform, "schedule_type": self.schedule_type, "interval_minutes": self.interval_minutes, "start_time": self.start_time.isoformat() if self.start_time else None, "total_posts": self.total_posts, "posts_published": self.posts_published, "status": self.status, "ai_generated": self.ai_generated, "hashtags": self.hashtags, "created_at": self.created_at.isoformat() if self.created_at else None, "completed_at": self.completed_at.isoformat() if self.completed_at else None } if include_posts and self.posts: result["posts"] = [p.to_dict() for p in self.posts] return result class ThreadPost(Base): """ Individual post within a thread series. """ __tablename__ = "thread_posts" id = Column(Integer, primary_key=True, index=True) # Parent series series_id = Column(Integer, ForeignKey("thread_series.id", ondelete="CASCADE"), nullable=False) # Position in thread sequence_number = Column(Integer, nullable=False) # 1, 2, 3, ... # Content content = Column(Text, nullable=False) image_url = Column(String(500), nullable=True) # Associated post (once created) post_id = Column(Integer, ForeignKey("posts.id"), nullable=True) # Platform post ID (for reply chain) platform_post_id = Column(String(100), nullable=True) reply_to_platform_id = Column(String(100), nullable=True) # ID of post to reply to # Schedule scheduled_at = Column(DateTime, nullable=True) # Status status = Column(String(20), default="pending") # pending, scheduled, published, failed error_message = Column(Text, nullable=True) published_at = Column(DateTime, nullable=True) # Timestamps created_at = Column(DateTime, default=datetime.utcnow) # Relationships series = relationship("ThreadSeries", back_populates="posts") post = relationship("Post", backref="thread_post") def __repr__(self): return f"" def to_dict(self): """Convert to dictionary.""" return { "id": self.id, "series_id": self.series_id, "sequence_number": self.sequence_number, "content": self.content[:100] + "..." if len(self.content) > 100 else self.content, "full_content": self.content, "image_url": self.image_url, "post_id": self.post_id, "platform_post_id": self.platform_post_id, "scheduled_at": self.scheduled_at.isoformat() if self.scheduled_at else None, "status": self.status, "error_message": self.error_message, "published_at": self.published_at.isoformat() if self.published_at else None }