from sqlalchemy import Column, Integer, String, Sequence, create_engine from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy import Column, Integer, Text, ForeignKey, DateTime, BigInteger from sqlalchemy.sql import func from sqlalchemy import text from app.core.config import config from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from datetime import datetime, timezone from sqlalchemy.orm import relationship SQLALCHEMY_DATABASE_URL = config.DATABASE_URL if SQLALCHEMY_DATABASE_URL.startswith("sqlite://"): SQLALCHEMY_DATABASE_URL = SQLALCHEMY_DATABASE_URL.replace("sqlite://", "sqlite+aiosqlite://") elif SQLALCHEMY_DATABASE_URL.startswith("postgresql://"): SQLALCHEMY_DATABASE_URL = SQLALCHEMY_DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://") connect_args = {"check_same_thread": False} if "sqlite" in SQLALCHEMY_DATABASE_URL else {} engine = create_async_engine(SQLALCHEMY_DATABASE_URL, connect_args=connect_args) from sqlalchemy import event @event.listens_for(engine.sync_engine, "connect") def register_sqlite_functions(dbapi_connection, connection_record): real_conn = dbapi_connection if hasattr(real_conn, "_conn"): real_conn = real_conn._conn if hasattr(real_conn, "create_function"): try: real_conn.create_function("LOWER", 1, lambda s: s.lower() if s is not None else None) except Exception: pass AsyncSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession, expire_on_commit=False) Base = declarative_base() class User(Base): __tablename__ = "users" id = Column(Integer, Sequence('user_id_seq', start=100), primary_key=True, index=True) first_name = Column(String(50), nullable=False, server_default="User") last_name = Column(String(50), nullable=True) username = Column(String, unique=True, index=True) about = Column(String, nullable=True) phone = Column(String(20), unique=True, nullable=True) email = Column(String(255), unique=True, nullable=True) totp_secret = Column(String(32), nullable=True) # Temporary secret until verified totp_temp_secret = Column(String(32), nullable=True) hashed_password = Column(String) public_key = Column(String, nullable=True) encrypted_private_key = Column(String, nullable=True) fcm_token = Column(String, nullable=True) avatar_file_id = Column(String, nullable=True) # Privacy settings show_email = Column(Integer, nullable=False, server_default="1") # 1 = true, 0 = false show_phone = Column(Integer, nullable=False, server_default="1") show_avatar = Column(Integer, nullable=False, server_default="1") show_about = Column(Integer, nullable=False, server_default="1") show_username = Column(Integer, nullable=False, server_default="1") show_last_online = Column(Integer, nullable=False, server_default="1") last_online = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) is_blocked = Column(Integer, default=0, server_default="0") sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan") class Message(Base): __tablename__ = "messages" id = Column(Integer, primary_key=True, index=True) sender_id = Column(Integer, ForeignKey("users.id")) receiver_id = Column(Integer, ForeignKey("users.id")) content = Column(Text) timestamp = Column(DateTime(timezone=True), server_default=func.now()) delivered_at = Column(DateTime(timezone=True), nullable=True) read_at = Column(DateTime(timezone=True), nullable=True) reply_to_id = Column(Integer, ForeignKey("messages.id"), nullable=True) reply_to_text = Column(Text, nullable=True) edited_at = Column(DateTime(timezone=True), nullable=True) message_type = Column(String, nullable=False, server_default="text") file_id = Column(String, nullable=True) encrypted_key = Column(String, nullable=True) class MediaItem(Base): __tablename__ = "media_items" id = Column(Integer, primary_key=True, index=True) # Уникальный внутренний UUID файла (используется в эндпоинтах API) file_id = Column(String(32), unique=True, nullable=False, index=True) # ID владельца из таблицы пользователей owner_id = Column(Integer, ForeignKey( "users.id"), nullable=True, index=True) # Оригинальное имя файла (например, "photo.jpg") original_filename = Column(String(255), nullable=True) # MIME-тип файла (например, "image/jpeg") content_type = Column(String(100), nullable=True) # ID файла внутри Google Drive (заменяет старый storage_filename) storage_file_id = Column(String(255), nullable=False) # Размер файла в байтах (BigInteger обязателен для поддержки квот > 2 ГБ) size_bytes = Column(BigInteger, nullable=False) # Таймстампы created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) class Session(Base): __tablename__ = "sessions" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) # Сюда пишем строку JWT-токена, которую использует клиент session_token = Column(String, unique=False, index=True, nullable=True) # Метаданные устройства device_name = Column(String, default="Неизвестное устройство") ip_address = Column(String, default="0.0.0.0") app_name = Column(String, nullable=True) app_version = Column(String, nullable=True) platform = Column(String, nullable=True) created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) last_active = Column(DateTime, default=lambda: datetime.now(timezone.utc)) fcm_token = Column(String, nullable=True) # FCM-токен для пуш-уведомлений # Обратная связь с пользователем user = relationship("User", back_populates="sessions") async def init_models(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # Миграции колонок для SQLite if "sqlite" in SQLALCHEMY_DATABASE_URL: async with engine.connect() as conn: # messages cols_res = await conn.execute(text("PRAGMA table_info(messages)")) existing = {row[1] for row in cols_res.fetchall()} if "delivered_at" not in existing: await conn.execute(text("ALTER TABLE messages ADD COLUMN delivered_at DATETIME")) if "read_at" not in existing: await conn.execute(text("ALTER TABLE messages ADD COLUMN read_at DATETIME")) if "reply_to_id" not in existing: await conn.execute(text("ALTER TABLE messages ADD COLUMN reply_to_id INTEGER REFERENCES messages(id)")) if "reply_to_text" not in existing: await conn.execute(text("ALTER TABLE messages ADD COLUMN reply_to_text TEXT")) if "edited_at" not in existing: await conn.execute(text("ALTER TABLE messages ADD COLUMN edited_at DATETIME")) if "message_type" not in existing: await conn.execute(text("ALTER TABLE messages ADD COLUMN message_type VARCHAR(32) DEFAULT 'text' NOT NULL")) if "file_id" not in existing: await conn.execute(text("ALTER TABLE messages ADD COLUMN file_id VARCHAR(255)")) if "encrypted_key" not in existing: await conn.execute(text("ALTER TABLE messages ADD COLUMN encrypted_key VARCHAR(1024)")) # users cols_u_res = await conn.execute(text("PRAGMA table_info(users)")) existing_u = {row[1] for row in cols_u_res.fetchall()} if "about" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN about TEXT")) if "phone" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN phone VARCHAR(20)")) if "email" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN email VARCHAR(255)")) if "show_email" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN show_email INTEGER DEFAULT 1")) if "show_phone" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN show_phone INTEGER DEFAULT 1")) if "show_avatar" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN show_avatar INTEGER DEFAULT 1")) if "show_about" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN show_about INTEGER DEFAULT 1")) if "show_username" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN show_username INTEGER DEFAULT 1")) if "show_last_online" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN show_last_online INTEGER DEFAULT 1")) if "last_online" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN last_online DATETIME")) await conn.execute(text("UPDATE users SET last_online = datetime('now')")) if "avatar_file_id" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN avatar_file_id VARCHAR(255)")) if "totp_temp_secret" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN totp_temp_secret VARCHAR(32)")) if "is_blocked" not in existing_u: await conn.execute(text("ALTER TABLE users ADD COLUMN is_blocked INTEGER DEFAULT 0")) await conn.commit()