- Новая модель P2PMessage для хранения личных сообщений - Миграция 008_add_p2p_messages.py - Сервис P2PMessageService для работы с P2P сообщениями - Команда /chat с меню чата - Выбор пользователя из списка - Отправка текста, фото, видео, документов - История последних диалогов - Счетчик непрочитанных сообщений - FSM состояния для управления диалогами
244 lines
12 KiB
Python
244 lines
12 KiB
Python
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, JSON, UniqueConstraint, BigInteger
|
||
from sqlalchemy.orm import relationship
|
||
from datetime import datetime, timezone
|
||
from .database import Base
|
||
import secrets
|
||
|
||
|
||
class User(Base):
|
||
"""Модель пользователя с регистрацией"""
|
||
__tablename__ = "users"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
telegram_id = Column(BigInteger, unique=True, nullable=False, index=True)
|
||
username = Column(String(255))
|
||
first_name = Column(String(255))
|
||
last_name = Column(String(255))
|
||
phone = Column(String(20), nullable=True) # Телефон для верификации
|
||
club_card_number = Column(String(50), unique=True, nullable=True, index=True) # Номер клубной карты
|
||
is_registered = Column(Boolean, default=False) # Прошел ли полную регистрацию
|
||
is_admin = Column(Boolean, default=False)
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
|
||
# Секретный код для верификации выигрыша (генерируется при регистрации)
|
||
verification_code = Column(String(10), unique=True, nullable=True)
|
||
|
||
# Связи
|
||
accounts = relationship("Account", back_populates="owner", cascade="all, delete-orphan")
|
||
participations = relationship("Participation", back_populates="user")
|
||
winners = relationship("Winner", back_populates="user")
|
||
|
||
def __repr__(self):
|
||
return f"<User(telegram_id={self.telegram_id}, club_card={self.club_card_number})>"
|
||
|
||
def generate_verification_code(self):
|
||
"""Генерирует уникальный код верификации"""
|
||
self.verification_code = secrets.token_hex(4).upper() # 8-символьный код
|
||
|
||
|
||
class Account(Base):
|
||
"""Модель счета клиента (может быть несколько у одного пользователя)"""
|
||
__tablename__ = "accounts"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
account_number = Column(String(20), unique=True, nullable=False, index=True) # XX-XX-XX-XX-XX-XX-XX
|
||
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
is_active = Column(Boolean, default=True) # Активен ли счет
|
||
|
||
# Связи
|
||
owner = relationship("User", back_populates="accounts")
|
||
participations = relationship("Participation", back_populates="account")
|
||
|
||
def __repr__(self):
|
||
return f"<Account(number={self.account_number}, owner_id={self.owner_id})>"
|
||
|
||
|
||
class WinnerVerification(Base):
|
||
"""Модель верификации победителя"""
|
||
__tablename__ = "winner_verifications"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
winner_id = Column(Integer, ForeignKey("winners.id"), nullable=False, unique=True)
|
||
verification_token = Column(String(32), unique=True, nullable=False) # Токен для подтверждения
|
||
is_verified = Column(Boolean, default=False)
|
||
verified_at = Column(DateTime(timezone=True), nullable=True)
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
expires_at = Column(DateTime(timezone=True), nullable=False) # Срок действия токена
|
||
|
||
# Связи
|
||
winner = relationship("Winner", back_populates="verification")
|
||
|
||
def __repr__(self):
|
||
return f"<WinnerVerification(winner_id={self.winner_id}, verified={self.is_verified})>"
|
||
|
||
@staticmethod
|
||
def generate_token():
|
||
"""Генерирует уникальный токен верификации"""
|
||
return secrets.token_urlsafe(24)
|
||
|
||
|
||
class Lottery(Base):
|
||
"""Модель розыгрыша"""
|
||
__tablename__ = "lotteries"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
title = Column(String(500), nullable=False)
|
||
description = Column(Text)
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
start_date = Column(DateTime(timezone=True))
|
||
end_date = Column(DateTime(timezone=True))
|
||
is_active = Column(Boolean, default=True)
|
||
is_completed = Column(Boolean, default=False)
|
||
prizes = Column(JSON) # Список призов в формате JSON
|
||
creator_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
|
||
# Настройки для ручного управления победителями
|
||
manual_winners = Column(JSON, default=lambda: {}) # {место: telegram_id}
|
||
draw_results = Column(JSON) # Результаты розыгрыша
|
||
|
||
# Тип отображения победителей: "username", "chat_id", "account_number"
|
||
winner_display_type = Column(String(20), default="username")
|
||
|
||
# Связи
|
||
creator = relationship("User")
|
||
participations = relationship("Participation", back_populates="lottery")
|
||
|
||
def __repr__(self):
|
||
return f"<Lottery(id={self.id}, title={self.title})>"
|
||
|
||
|
||
class Participation(Base):
|
||
"""Модель участия в розыгрыше"""
|
||
__tablename__ = "participations"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
|
||
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True) # Привязка к счету
|
||
account_number = Column(String(20), nullable=True, index=True) # Дублируем для быстрого доступа
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
|
||
# Связи
|
||
user = relationship("User", back_populates="participations")
|
||
lottery = relationship("Lottery", back_populates="participations")
|
||
account = relationship("Account", back_populates="participations")
|
||
|
||
def __repr__(self):
|
||
if self.account_number:
|
||
return f"<Participation(account={self.account_number}, lottery_id={self.lottery_id})>"
|
||
return f"<Participation(user_id={self.user_id}, lottery_id={self.lottery_id})>"
|
||
|
||
|
||
class Winner(Base):
|
||
"""Модель победителя розыгрыша"""
|
||
__tablename__ = "winners"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||
account_number = Column(String(20), nullable=True, index=True)
|
||
place = Column(Integer, nullable=False)
|
||
prize = Column(String(500))
|
||
is_manual = Column(Boolean, default=False)
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
|
||
# Статус подтверждения выигрыша
|
||
is_notified = Column(Boolean, default=False) # Отправлено ли уведомление
|
||
is_claimed = Column(Boolean, default=False) # Подтвердил ли победитель
|
||
claimed_at = Column(DateTime(timezone=True), nullable=True) # Время подтверждения
|
||
|
||
# Связи
|
||
user = relationship("User", back_populates="winners")
|
||
lottery = relationship("Lottery")
|
||
verification = relationship("WinnerVerification", back_populates="winner", uselist=False)
|
||
|
||
def __repr__(self):
|
||
if self.account_number:
|
||
return f"<Winner(lottery_id={self.lottery_id}, account={self.account_number}, place={self.place})>"
|
||
return f"<Winner(lottery_id={self.lottery_id}, user_id={self.user_id}, place={self.place})>"
|
||
|
||
|
||
class ChatSettings(Base):
|
||
"""Настройки системы чата"""
|
||
__tablename__ = "chat_settings"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
mode = Column(String(20), nullable=False, default='broadcast') # broadcast или forward
|
||
forward_chat_id = Column(String(50), nullable=True) # ID группы/канала для пересылки
|
||
global_ban = Column(Boolean, default=False) # Глобальный бан чата
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||
|
||
def __repr__(self):
|
||
return f"<ChatSettings(mode={self.mode}, global_ban={self.global_ban})>"
|
||
|
||
|
||
class BannedUser(Base):
|
||
"""Забаненные пользователи (не могут отправлять сообщения)"""
|
||
__tablename__ = "banned_users"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
telegram_id = Column(BigInteger, nullable=False, index=True)
|
||
banned_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
reason = Column(Text, nullable=True)
|
||
banned_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
is_active = Column(Boolean, default=True, index=True) # Активен ли бан
|
||
|
||
# Связи
|
||
user = relationship("User", foreign_keys=[user_id])
|
||
admin = relationship("User", foreign_keys=[banned_by])
|
||
|
||
def __repr__(self):
|
||
return f"<BannedUser(telegram_id={self.telegram_id}, is_active={self.is_active})>"
|
||
|
||
|
||
class ChatMessage(Base):
|
||
"""История сообщений чата (для модерации)"""
|
||
__tablename__ = "chat_messages"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||
telegram_message_id = Column(Integer, nullable=False)
|
||
message_type = Column(String(20), nullable=False) # text, photo, video, document, animation, sticker, voice, etc.
|
||
text = Column(Text, nullable=True) # Текст сообщения
|
||
file_id = Column(String(255), nullable=True) # ID файла в Telegram
|
||
forwarded_message_ids = Column(JSON, nullable=True) # Список telegram_message_id пересланных сообщений {"user_telegram_id": message_id}
|
||
is_deleted = Column(Boolean, default=False, index=True)
|
||
deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True)
|
||
|
||
# Связи
|
||
sender = relationship("User", foreign_keys=[user_id])
|
||
moderator = relationship("User", foreign_keys=[deleted_by])
|
||
|
||
def __repr__(self):
|
||
return f"<ChatMessage(id={self.id}, user_id={self.user_id}, type={self.message_type})>"
|
||
|
||
|
||
class P2PMessage(Base):
|
||
"""P2P сообщения между пользователями"""
|
||
__tablename__ = "p2p_messages"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||
recipient_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||
message_type = Column(String(20), nullable=False) # text, photo, video, etc.
|
||
text = Column(Text, nullable=True)
|
||
file_id = Column(String(255), nullable=True)
|
||
sender_message_id = Column(Integer, nullable=False) # ID сообщения у отправителя
|
||
recipient_message_id = Column(Integer, nullable=True) # ID сообщения у получателя
|
||
is_read = Column(Boolean, default=False, index=True)
|
||
read_at = Column(DateTime(timezone=True), nullable=True)
|
||
reply_to_id = Column(Integer, ForeignKey("p2p_messages.id"), nullable=True) # Ответ на сообщение
|
||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True)
|
||
|
||
# Связи
|
||
sender = relationship("User", foreign_keys=[sender_id], backref="sent_p2p_messages")
|
||
recipient = relationship("User", foreign_keys=[recipient_id], backref="received_p2p_messages")
|
||
reply_to = relationship("P2PMessage", remote_side=[id], backref="replies")
|
||
|
||
def __repr__(self):
|
||
return f"<P2PMessage(id={self.id}, from={self.sender_id}, to={self.recipient_id})>" |