173 lines
9.8 KiB
Python
173 lines
9.8 KiB
Python
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() |