Chepuhagram/srv/app/db/models.py

173 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()