from datetime import datetime from sqlalchemy import ( Column, Integer, BigInteger, String, DateTime, ForeignKey, Text, Boolean, UniqueConstraint, ) from sqlalchemy.orm import relationship from app.db.base import Base class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True) tg_id = Column(BigInteger, unique=True, nullable=False, index=True) name = Column(String(255)) created_at = Column(DateTime, default=datetime.utcnow, nullable=False) class Chat(Base): __tablename__ = "chats" id = Column(Integer, primary_key=True) chat_id = Column(BigInteger, unique=True, nullable=False, index=True) type = Column(String(32)) # group | supergroup | channel title = Column(String(255)) owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) can_post = Column(Boolean, default=False, nullable=False) created_at = Column(DateTime, default=datetime.utcnow, nullable=False) owner = relationship("User") class Draft(Base): __tablename__ = "drafts" id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), index=True) text = Column(Text, nullable=True) status = Column(String(16), default="editing", index=True) # editing|ready|sent|cancelled created_at = Column(DateTime, default=datetime.utcnow, nullable=False) updated_at = Column(DateTime, default=datetime.utcnow, nullable=False) user = relationship("User") media = relationship("DraftMedia", cascade="all, delete-orphan", back_populates="draft") class DraftMedia(Base): __tablename__ = "draft_media" id = Column(Integer, primary_key=True) draft_id = Column(Integer, ForeignKey("drafts.id"), index=True) kind = Column(String(16)) # photo | video | animation file_id = Column(String(255)) order = Column(Integer, default=0) draft = relationship("Draft", back_populates="media") class Delivery(Base): __tablename__ = "deliveries" id = Column(Integer, primary_key=True) draft_id = Column(Integer, ForeignKey("drafts.id"), index=True) chat_id = Column(BigInteger, index=True) status = Column(String(16), default="new", index=True) # new | sent | failed error = Column(Text, nullable=True) message_ids = Column(Text, nullable=True) # csv для альбомов/нескольких сообщений content_hash = Column(String(128), index=True, nullable=True) # анти-дубликаты created_at = Column(DateTime, default=datetime.utcnow, nullable=False) # --- учёт участников (для аналитики/модерации) --- class ChatMember(Base): __tablename__ = "chat_members" id = Column(Integer, primary_key=True) chat_id = Column(BigInteger, index=True, nullable=False) user_id = Column(Integer, ForeignKey("users.id"), index=True, nullable=False) tg_user_id = Column(BigInteger, index=True, nullable=False) username = Column(String(255)) first_name = Column(String(255)) last_name = Column(String(255)) status = Column(String(32), index=True) # member | administrator | creator | left | kicked ... is_admin = Column(Boolean, default=False, nullable=False) first_seen_at = Column(DateTime, nullable=False, default=datetime.utcnow) last_seen_at = Column(DateTime, nullable=False, default=datetime.utcnow) user = relationship("User") __table_args__ = (UniqueConstraint("chat_id", "tg_user_id", name="uq_chat_members_chat_user"),) # --- политики безопасности и словари --- class SecurityPolicy(Base): __tablename__ = "security_policies" id = Column(Integer, primary_key=True) owner_user_id = Column(Integer, ForeignKey("users.id"), index=True, nullable=True) # NULL = глобальная name = Column(String(100), nullable=False) # категории словарей (True = блокировать) block_adult = Column(Boolean, default=True, nullable=False) block_spam = Column(Boolean, default=True, nullable=False) block_scam = Column(Boolean, default=True, nullable=False) block_profanity = Column(Boolean, default=False, nullable=False) # лимиты и режимы cooldown_seconds = Column(Integer, default=30, nullable=False) # пауза между постами/чат duplicate_window_seconds = Column(Integer, default=120, nullable=False) # окно дублей (сек) max_links = Column(Integer, default=3, nullable=False) max_mentions = Column(Integer, default=5, nullable=False) use_whitelist = Column(Boolean, default=False, nullable=False) # наказания/эскалация (для входящих сообщений в группе) enforce_action_default = Column(String(16), default="delete", nullable=False) # delete|warn|timeout|ban|none timeout_minutes = Column(Integer, default=10, nullable=False) strikes_to_warn = Column(Integer, default=1, nullable=False) strikes_to_timeout = Column(Integer, default=2, nullable=False) strikes_to_ban = Column(Integer, default=3, nullable=False) user_msg_per_minute = Column(Integer, default=0, nullable=False) # 0 = выключено class ChatSecurity(Base): __tablename__ = "chat_security" id = Column(Integer, primary_key=True) chat_id = Column(BigInteger, index=True, nullable=False, unique=True) policy_id = Column(Integer, ForeignKey("security_policies.id"), index=True, nullable=False) enabled = Column(Boolean, default=False, nullable=False) # включена ли модерация для этого чата class SpamDictionary(Base): __tablename__ = "spam_dictionaries" id = Column(Integer, primary_key=True) owner_user_id = Column(Integer, ForeignKey("users.id"), index=True, nullable=True) # NULL = глобальная name = Column(String(120), nullable=False) category = Column(String(32), nullable=False) # adult | spam | scam | profanity | custom kind = Column(String(16), nullable=False) # plain | regex lang = Column(String(8), nullable=True) created_at = Column(DateTime, default=datetime.utcnow, nullable=False) class DictionaryEntry(Base): __tablename__ = "dictionary_entries" id = Column(Integer, primary_key=True) dictionary_id = Column(Integer, ForeignKey("spam_dictionaries.id"), index=True, nullable=False) pattern = Column(Text, nullable=False) # слово/фраза или регулярка is_regex = Column(Boolean, default=False, nullable=False) class PolicyDictionaryLink(Base): __tablename__ = "policy_dict_links" id = Column(Integer, primary_key=True) policy_id = Column(Integer, ForeignKey("security_policies.id"), index=True, nullable=False) dictionary_id = Column(Integer, ForeignKey("spam_dictionaries.id"), index=True, nullable=False) __table_args__ = (UniqueConstraint("policy_id", "dictionary_id", name="uq_policy_dict"),) class DomainRule(Base): __tablename__ = "domain_rules" id = Column(Integer, primary_key=True) policy_id = Column(Integer, ForeignKey("security_policies.id"), index=True, nullable=False) domain = Column(String(255), nullable=False) kind = Column(String(16), nullable=False) # whitelist | blacklist __table_args__ = (UniqueConstraint("policy_id", "domain", "kind", name="uq_domain_rule"),) # --- журнал модерации/событий --- class ModerationLog(Base): __tablename__ = "moderation_logs" id = Column(Integer, primary_key=True) chat_id = Column(BigInteger, index=True, nullable=False) tg_user_id = Column(BigInteger, index=True, nullable=False) message_id = Column(BigInteger, nullable=True) reason = Column(Text, nullable=False) # причины (через '; '), либо текст ошибки action = Column(String(16), nullable=False) # delete|warn|timeout|ban|error|none created_at = Column(DateTime, default=datetime.utcnow, nullable=False) class UserStrike(Base): __tablename__ = "user_strikes" id = Column(Integer, primary_key=True) chat_id = Column(BigInteger, index=True, nullable=False) tg_user_id = Column(BigInteger, index=True, nullable=False) strikes = Column(Integer, default=0, nullable=False) updated_at = Column(DateTime, default=datetime.utcnow, nullable=False) __table_args__ = (UniqueConstraint("chat_id", "tg_user_id", name="uq_strikes_chat_user"),) class MessageEvent(Base): __tablename__ = "message_events" id = Column(Integer, primary_key=True) chat_id = Column(BigInteger, index=True, nullable=False) tg_user_id = Column(BigInteger, index=True, nullable=False) message_id = Column(BigInteger, nullable=True) content_hash = Column(String(128), index=True, nullable=True) created_at = Column(DateTime, default=datetime.utcnow, nullable=False)