""" Модели базы данных """ from datetime import datetime from sqlalchemy import ( Column, Integer, BigInteger, String, Boolean, DateTime, Text, Float, ForeignKey, Index ) from sqlalchemy.orm import relationship, declarative_base Base = declarative_base() def get_level_by_rating(rating: int) -> str: """Определить уровень по рейтингу""" if rating >= 1000: return 'Лидер' elif rating >= 500: return 'Активист' elif rating >= 100: return 'Сосед' else: return 'Новичок' class User(Base): """Пользователи (жильцы)""" __tablename__ = 'users' user_id = Column(BigInteger, primary_key=True) username = Column(String(255), nullable=True) first_name = Column(String(255), nullable=True) last_name = Column(String(255), nullable=True) apartment = Column(String(10), nullable=True) # Номер квартиры phone = Column(String(20), nullable=True) # Телефон жильца verified = Column(Boolean, default=False) # Прошёл верификацию verification_date = Column(DateTime, nullable=True) join_date = Column(DateTime, default=datetime.utcnow) message_count = Column(Integer, default=0) is_banned = Column(Boolean, default=False) is_admin = Column(Boolean, default=False) last_seen = Column(DateTime, nullable=True) # Рейтинг активности rating = Column(Integer, default=0) # Баллы активности level = Column(String(50), default='Новичок') # Новичок, Сосед, Активист, Лидер # Для детекта шпионов spy_score = Column(Integer, default=0) # 0-100 spy_flags = Column(Text, nullable=True) # JSON список флагов # Для анти-спама spam_warning_count = Column(Integer, default=0) # Количество предупреждений за спам # Для анти-спам лимитов is_muted = Column(Boolean, default=False) # Замучен ли mute_until = Column(DateTime, nullable=True) # До когда мут rate_limit_warnings = Column(Integer, default=0) # Предупреждения за лимиты # Для детекта мультиаккаунтов multicount_score = Column(Integer, default=0) # Баллы подозрительности suspected_device_id = Column(String(100), nullable=True) # ID устройства (если есть) suspected_alt_accounts = Column(Text, nullable=True) # JSON список подозрительных аккаунтов # Для рейтинга токсичности toxicity_score = Column(Integer, default=0) # Средний балл токсичности toxic_messages_count = Column(Integer, default=0) # Количество токсичных сообщений complaints_count = Column(Integer, default=0) # Количество жалоб # Связи messages = relationship('Message', back_populates='user', lazy='dynamic') __table_args__ = ( Index('ix_users_apartment', 'apartment'), Index('ix_users_verified', 'verified'), Index('ix_users_spy_score', 'spy_score'), Index('ix_users_rating', 'rating'), ) def __repr__(self): return f"" @property def full_name(self): parts = [self.first_name, self.last_name] return ' '.join(filter(None, parts)) or self.username or f'user{self.user_id}' def update_level(self): """Обновить уровень на основе рейтинга""" self.level = get_level_by_rating(self.rating) class Message(Base): """Сообщения в чате""" __tablename__ = 'messages' id = Column(Integer, primary_key=True, autoincrement=True) message_id = Column(BigInteger) user_id = Column(BigInteger, ForeignKey('users.user_id')) chat_id = Column(BigInteger) text = Column(Text, nullable=True) timestamp = Column(DateTime, default=datetime.utcnow) reply_to_message_id = Column(BigInteger, nullable=True) forward_from = Column(String(255), nullable=True) topic = Column(String(50), nullable=True) # Тема сообщения (лифты, парковка, уборка...) # Связи user = relationship('User', back_populates='messages') __table_args__ = ( Index('ix_messages_chat_time', 'chat_id', 'timestamp'), Index('ix_messages_user_time', 'user_id', 'timestamp'), Index('ix_messages_topic', 'topic'), ) def __repr__(self): return f"" class Service(Base): """Контакты служб и мастеров""" __tablename__ = 'services' service_id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(255), nullable=False) category = Column(String(100), nullable=False) # emergency, plumber, electrician, etc. phone = Column(String(20), nullable=False) description = Column(Text, nullable=True) image_path = Column(String(500), nullable=True) # Путь к картинке rating = Column(Float, default=0.0) reviews_count = Column(Integer, default=0) verified_by_admin = Column(Boolean, default=False) created_at = Column(DateTime, default=datetime.utcnow) __table_args__ = ( Index('ix_services_category', 'category'), ) def __repr__(self): return f"" class VerificationRequest(Base): """Заявки на верификацию""" __tablename__ = 'verification_requests' id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) apartment = Column(String(10), nullable=False) photo_file_id = Column(String(255), nullable=True) # Фото квитанции comment = Column(Text, nullable=True) status = Column(String(50), default='pending') # pending, approved, rejected created_at = Column(DateTime, default=datetime.utcnow) reviewed_at = Column(DateTime, nullable=True) reviewed_by = Column(BigInteger, nullable=True) # Admin ID reject_reason = Column(Text, nullable=True) __table_args__ = ( Index('ix_verification_status', 'status'), ) def __repr__(self): return f"" class SpyLog(Base): """Логи детекта шпионов""" __tablename__ = 'spy_logs' id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) spy_score = Column(Integer, nullable=False) flags = Column(Text, nullable=False) # JSON список detected_at = Column(DateTime, default=datetime.utcnow) action_taken = Column(String(50), nullable=True) # alerted, warned, banned __table_args__ = ( Index('ix_spy_logs_user_time', 'user_id', 'detected_at'), ) def __repr__(self): return f"" class Announcement(Base): """Объявления для жильцов""" __tablename__ = 'announcements' id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(255), nullable=False) text = Column(Text, nullable=False) category = Column(String(50), default='general') # general, emergency, utility created_by = Column(BigInteger, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) is_pinned = Column(Boolean, default=False) is_active = Column(Boolean, default=True) __table_args__ = ( Index('ix_announcements_category', 'category'), Index('ix_announcements_active', 'is_active'), ) def __repr__(self): return f"" class Poll(Base): """Опросы/голосования жильцов""" __tablename__ = 'polls' poll_id = Column(Integer, primary_key=True, autoincrement=True) message_id = Column(BigInteger, nullable=False) # ID сообщения с опросом в чате chat_id = Column(BigInteger, nullable=False) # ID чата question = Column(Text, nullable=False) # Вопрос опроса options = Column(Text, nullable=False) # JSON: ["Вариант 1", "Вариант 2", ...] votes = Column(Text, nullable=True) # JSON: {"0": [user_id1, user_id2], "1": [user_id3]} created_by = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) created_at = Column(DateTime, default=datetime.utcnow) is_active = Column(Boolean, default=True) # Активен ли опрос closed_at = Column(DateTime, nullable=True) # Дата закрытия # Связи creator = relationship('User', backref='polls') __table_args__ = ( Index('ix_polls_chat_active', 'chat_id', 'is_active'), Index('ix_polls_created_at', 'created_at'), ) def __repr__(self): return f"" @property def total_votes(self) -> int: """Общее количество голосов""" import json if not self.votes: return 0 votes_data = json.loads(self.votes) return sum(len(v) for v in votes_data.values()) def get_results(self) -> list: """Получить результаты в формате [(вариант, голоса, процент), ...]""" import json if not self.votes: options_list = json.loads(self.options) return [(opt, 0, 0.0) for opt in options_list] options_list = json.loads(self.options) votes_data = json.loads(self.votes) total = self.total_votes results = [] for i, option in enumerate(options_list): vote_count = len(votes_data.get(str(i), [])) percent = (vote_count / total * 100) if total > 0 else 0.0 results.append((option, vote_count, round(percent, 1))) return results class SpamWarning(Base): """Предупреждения за спам/запрещённые ссылки""" __tablename__ = 'spam_warnings' warning_id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) message_text = Column(Text, nullable=True) # Текст сообщения reason = Column(Text, nullable=True) # Причина предупреждения created_at = Column(DateTime, default=datetime.utcnow) is_active = Column(Boolean, default=True) # Активно ли предупреждение # Связи user = relationship('User', backref='spam_warnings') __table_args__ = ( Index('ix_spam_warnings_user', 'user_id'), Index('ix_spam_warnings_active', 'is_active'), ) def __repr__(self): return f"" class MessageRate(Base): """Счётчик сообщений для rate limiting""" __tablename__ = 'message_rates' id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) timestamp = Column(DateTime, default=datetime.utcnow) # Связи user = relationship('User', backref='message_rates') __table_args__ = ( Index('ix_message_rates_user', 'user_id'), Index('ix_message_rates_time', 'timestamp'), ) def __repr__(self): return f"" class Schedule(Base): """Расписание отключений (вода, свет, отопление)""" __tablename__ = 'schedules' schedule_id = Column(Integer, primary_key=True, autoincrement=True) schedule_type = Column(String(50), nullable=False) # water, electricity, heating, etc. title = Column(String(255), nullable=False) description = Column(Text, nullable=True) start_time = Column(DateTime, nullable=False) end_time = Column(DateTime, nullable=False) created_by = Column(BigInteger, ForeignKey('users.user_id'), nullable=True) created_at = Column(DateTime, default=datetime.utcnow) reminder_sent = Column(Boolean, default=False) # Отправлено ли напоминание is_active = Column(Boolean, default=True) # Связи creator = relationship('User', backref='schedules') __table_args__ = ( Index('ix_schedules_type', 'schedule_type'), Index('ix_schedules_time', 'start_time', 'end_time'), Index('ix_schedules_active', 'is_active'), ) def __repr__(self): return f"" @property def duration_hours(self) -> float: """Длительность отключения в часах""" if self.start_time and self.end_time: delta = self.end_time - self.start_time return round(delta.total_seconds() / 3600, 1) return 0 def get_type_emoji(self) -> str: """Получить emoji типа отключения""" emojis = { 'water': '💧', 'electricity': '⚡', 'heating': '🔥', 'gas': '🔶', 'elevator': '🛗', 'general': '📋' } return emojis.get(self.schedule_type, '📋') class Ad(Base): """Объявления жильцов (куплю, продам, отдам, найду)""" __tablename__ = 'ads' ad_id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) category = Column(String(50), nullable=False) # sell, buy, give, lost, found, service title = Column(String(255), nullable=False) description = Column(Text, nullable=True) price = Column(Integer, nullable=True) # Цена в рублях phone = Column(String(50), nullable=True) # Контактный телефон photo_file_id = Column(String(255), nullable=True) # ID фото в Telegram message_id = Column(BigInteger, nullable=True) # ID сообщения в чате created_at = Column(DateTime, default=datetime.utcnow) expires_at = Column(DateTime, nullable=True) # Дата истечения (через 7 дней) is_active = Column(Boolean, default=True) is_moderated = Column(Boolean, default=False) # Проверено админом # Связи user = relationship('User', backref='ads') __table_args__ = ( Index('ix_ads_category', 'category'), Index('ix_ads_active', 'is_active'), Index('ix_ads_created_at', 'created_at'), ) def __repr__(self): return f"" @property def days_left(self) -> int: """Осталось дней до истечения""" if self.expires_at: delta = self.expires_at - datetime.utcnow() return max(0, delta.days) return 7 def get_category_emoji(self) -> str: """Получить emoji категории""" emojis = { 'sell': '💰', 'buy': '🛒', 'give': '🎁', 'lost': '😿', 'found': '😺', 'service': '🔧' } return emojis.get(self.category, '📋') def get_category_name(self) -> str: """Получить название категории""" names = { 'sell': 'Продам', 'buy': 'Куплю', 'give': 'Отдам', 'lost': 'Потеряно', 'found': 'Нашлось', 'service': 'Услуга' } return names.get(self.category, 'Общее') class PaymentReminder(Base): """Напоминания о платежах ЖКХ""" __tablename__ = 'payment_reminders' reminder_id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(255), nullable=False) # Название платежа amount = Column(Integer, nullable=True) # Сумма в рублях due_date = Column(DateTime, nullable=False) # Дата оплаты (число месяца) description = Column(Text, nullable=True) # Описание payment_link = Column(String(500), nullable=True) # Ссылка на оплату is_recurring = Column(Boolean, default=True) # Ежемесячный платёж created_by = Column(BigInteger, ForeignKey('users.user_id'), nullable=True) created_at = Column(DateTime, default=datetime.utcnow) is_active = Column(Boolean, default=True) last_reminder_sent = Column(DateTime, nullable=True) # Последнее напоминание # Связи creator = relationship('User', backref='payment_reminders') __table_args__ = ( Index('ix_payment_reminders_due_date', 'due_date'), Index('ix_payment_reminders_active', 'is_active'), ) def __repr__(self): return f"" def get_days_until_due(self, current_day: int = None) -> int: """Сколько дней до срока оплаты""" from datetime import datetime if current_day is None: current_day = datetime.utcnow().day due_day = self.due_date.day if hasattr(self.due_date, 'day') else self.due_date if due_day >= current_day: return due_day - current_day else: return (30 - current_day) + due_day class ProfileHistory(Base): """История изменений профиля пользователя""" __tablename__ = 'profile_history' id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) field_name = Column(String(50), nullable=False) # username, first_name, last_name, apartment old_value = Column(Text, nullable=True) # Старое значение new_value = Column(Text, nullable=True) # Новое значение changed_at = Column(DateTime, default=datetime.utcnow) # Связи user = relationship('User', backref='profile_history') __table_args__ = ( Index('ix_profile_history_user', 'user_id'), Index('ix_profile_history_field', 'field_name'), Index('ix_profile_history_time', 'changed_at'), ) def __repr__(self): return f"" class Achievement(Base): """Достижения пользователей""" __tablename__ = 'achievements' id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) achievement_type = Column(String(50), nullable=False) # Тип достижения earned_at = Column(DateTime, default=datetime.utcnow) # Когда получено achievement_data = Column(Text, nullable=True) # JSON с доп. данными # Связи user = relationship('User', backref='achievements_list') __table_args__ = ( Index('ix_achievements_user', 'user_id'), Index('ix_achievements_type', 'achievement_type'), ) def __repr__(self): return f"" class Thank(Base): """Благодарности пользователей""" __tablename__ = 'thanks' id = Column(Integer, primary_key=True, autoincrement=True) from_user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) # Кто поблагодарил to_user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) # Кого поблагодарили message = Column(Text, nullable=True) # Текст благодарности created_at = Column(DateTime, default=datetime.utcnow) # Связи from_user = relationship('User', foreign_keys=[from_user_id], backref='thanks_given') to_user = relationship('User', foreign_keys=[to_user_id], backref='thanks_received') __table_args__ = ( Index('ix_thanks_from', 'from_user_id'), Index('ix_thanks_to', 'to_user_id'), Index('ix_thanks_created', 'created_at'), ) def __repr__(self): return f"" class Event(Base): """События дома (собрания, субботники, праздники)""" __tablename__ = 'events' id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(255), nullable=False) # Название события description = Column(Text, nullable=True) # Описание event_type = Column(String(50), default='general') # Тип: meeting, subbotnik, holiday event_date = Column(DateTime, nullable=False) # Дата и время события location = Column(String(255), nullable=True) # Место проведения created_by = Column(BigInteger, ForeignKey('users.user_id'), nullable=True) created_at = Column(DateTime, default=datetime.utcnow) is_active = Column(Boolean, default=True) reminder_sent = Column(Boolean, default=False) # Напоминание отправлено # Связи creator = relationship('User', backref='created_events') __table_args__ = ( Index('ix_events_date', 'event_date'), Index('ix_events_type', 'event_type'), Index('ix_events_active', 'is_active'), ) def __repr__(self): return f"" class EventParticipant(Base): """Участники событий""" __tablename__ = 'event_participants' id = Column(Integer, primary_key=True, autoincrement=True) event_id = Column(Integer, ForeignKey('events.id'), nullable=False) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) joined_at = Column(DateTime, default=datetime.utcnow) # Связи event = relationship('Event', backref='participants_list') user = relationship('User', backref='events_participating') __table_args__ = ( Index('ix_event_participants_event', 'event_id'), Index('ix_event_participants_user', 'user_id'), Index('ix_event_participants_unique', 'event_id', 'user_id', unique=True), ) def __repr__(self): return f"" class InitiativeGroup(Base): """Участники инициативной группы""" __tablename__ = 'initiative_group' id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False, unique=True) role = Column(String(50), default='member') # admin, member, coordinator joined_at = Column(DateTime, default=datetime.utcnow) is_active = Column(Boolean, default=True) notes = Column(Text, nullable=True) # Заметки о роли # Связи user = relationship('User', backref='initiative_membership') __table_args__ = ( Index('ix_initiative_group_user', 'user_id'), Index('ix_initiative_group_active', 'is_active'), ) def __repr__(self): return f"" class ScheduledPost(Base): """Запланированные посты для отложенленной публикации""" __tablename__ = 'scheduled_posts' id = Column(Integer, primary_key=True, autoincrement=True) text = Column(Text, nullable=False) # Текст поста photo_file_id = Column(String(255), nullable=True) # ID фото в Telegram document_file_id = Column(String(255), nullable=True) # ID документа (PDF, DOCX...) document_name = Column(String(255), nullable=True) # Имя файла документа topic_id = Column(BigInteger, nullable=True) # ID темы форума (message_thread_id) topic_name = Column(String(50), nullable=True) # Название темы ('general', 'memes'...) recipients = Column(String(50), default='chat_only') # Получатели: 'chat_only', 'all_verified', 'all_and_chat' scheduled_time = Column(DateTime, nullable=False) # Когда отправить status = Column(String(20), default='pending') # 'pending', 'sent', 'failed', 'cancelled' created_by = Column(BigInteger, nullable=True) # ID админа создавшего пост created_at = Column(DateTime, default=datetime.utcnow) sent_at = Column(DateTime, nullable=True) # Когда фактически отправлено error_message = Column(Text, nullable=True) # Текст ошибки если failed message_id = Column(BigInteger, nullable=True) # ID отправленного сообщения в Telegram __table_args__ = ( Index('ix_scheduled_posts_time', 'scheduled_time'), Index('ix_scheduled_posts_status', 'status'), Index('ix_scheduled_posts_topic', 'topic_name'), ) def __repr__(self): return f"" @property def is_due(self) -> bool: """Пора ли отправлять пост""" from datetime import datetime return datetime.utcnow() >= self.scheduled_time and self.status == 'pending' def get_topic_emoji(self) -> str: """Получить emoji темы""" emojis = { 'general': '💬', 'ads': '📢', 'memes': '😂', 'meetings': '🏛️', 'kgm': '🗑️', 'lostfound': '🔍', 'auto': '🚗', 'phones': '📞', 'links': '🔗', 'uo': '📝', } return emojis.get(self.topic_name, '📝') def get_status_emoji(self) -> str: """Получить emoji статуса""" emojis = { 'pending': '⏳', 'sent': '✅', 'failed': '❌', 'cancelled': '🚫', } return emojis.get(self.status, '❓') class Broadcast(Base): """Умные рассылки с отслеживанием прочтения""" __tablename__ = 'broadcasts' id = Column(Integer, primary_key=True, autoincrement=True) message_id = Column(BigInteger, nullable=True) # ID сообщения в Telegram (в чате) chat_id = Column(BigInteger, nullable=True) # ID чата text = Column(Text, nullable=False) # Текст рассылки photo_file_id = Column(String(255), nullable=True) # ID фото sent_at = Column(DateTime, nullable=True) # Когда отправлено sent_by = Column(BigInteger, nullable=True) # ID админа total_sent = Column(Integer, default=0) # Сколько получателей has_read_button = Column(Boolean, default=True) # Показывать кнопку "Прочитал" is_reminder_sent = Column(Boolean, default=False) # Было ли напоминание непрочитавшим created_at = Column(DateTime, default=datetime.utcnow) broadcast_type = Column(String(50), default='manual') # 'manual', 'digest', 'emergency' __table_args__ = ( Index('ix_broadcasts_sent_at', 'sent_at'), Index('ix_broadcasts_type', 'broadcast_type'), ) def __repr__(self): return f"" @property def read_count(self) -> int: """Сколько прочитали (запрос к БД)""" # Это свойство вычисляется через relationship return 0 @property def unread_count(self) -> int: """Сколько НЕ прочитали""" return self.total_sent - self.read_count class BroadcastRead(Base): """Кто прочитал рассылку""" __tablename__ = 'broadcast_reads' id = Column(Integer, primary_key=True, autoincrement=True) broadcast_id = Column(BigInteger, ForeignKey('broadcasts.id'), nullable=False) user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) read_at = Column(DateTime, default=datetime.utcnow) # Связи broadcast = relationship('Broadcast', backref='reads') user = relationship('User', backref='broadcast_reads') __table_args__ = ( Index('ix_broadcast_read_broadcast', 'broadcast_id'), Index('ix_broadcast_read_user', 'user_id'), Index('ix_broadcast_read_unique', 'broadcast_id', 'user_id', unique=True), ) def __repr__(self): return f"" class Digest(Base): """Еженедельные дайджесты""" __tablename__ = 'digests' id = Column(Integer, primary_key=True, autoincrement=True) period_start = Column(DateTime, nullable=False) # Начало периода period_end = Column(DateTime, nullable=False) # Конец периода week_number = Column(Integer, nullable=False) # Номер недели year = Column(Integer, nullable=False) # Год content = Column(Text, nullable=True) # JSON с данными дайджеста digest_text = Column(Text, nullable=True) # Сформированный текст status = Column(String(50), default='draft') # 'draft', 'pending_approval', 'approved', 'rejected', 'sent' message_id = Column(BigInteger, nullable=True) # ID сообщения в Telegram created_at = Column(DateTime, default=datetime.utcnow) approved_at = Column(DateTime, nullable=True) approved_by = Column(BigInteger, nullable=True) # Admin ID sent_at = Column(DateTime, nullable=True) rejected_by = Column(BigInteger, nullable=True) rejection_reason = Column(Text, nullable=True) # Причина отклонения __table_args__ = ( Index('ix_digests_week', 'year', 'week_number'), Index('ix_digests_status', 'status'), Index('ix_digests_period', 'period_start', 'period_end'), ) def __repr__(self): return f"" @property def status_emoji(self) -> str: emojis = { 'draft': '📝', 'pending_approval': '⏳', 'approved': '✅', 'rejected': '❌', 'sent': '📨', } return emojis.get(self.status, '❓') @property def period_label(self) -> str: """Красивая метка периода""" return f"{self.period_start.strftime('%d.%m')} - {self.period_end.strftime('%d.%m.%Y')}" class EmailAudit(Base): """Аудит переписки с органами""" __tablename__ = 'email_audit' id = Column(Integer, primary_key=True, autoincrement=True) domain = Column(String(255), unique=True, nullable=False) description = Column(String(255), nullable=True) inbound_count = Column(Integer, default=0) outbound_count = Column(Integer, default=0) last_email_date = Column(DateTime, nullable=True) updated_at = Column(DateTime, default=datetime.utcnow) def __repr__(self): return f"" class EmailExportLog(Base): """Логи работы Email Auditor""" __tablename__ = 'email_export_logs' id = Column(Integer, primary_key=True, autoincrement=True) start_time = Column(DateTime, default=datetime.utcnow) end_time = Column(DateTime, nullable=True) status = Column(String(50)) # 'success', 'error', 'running' new_emails_count = Column(Integer, default=0) report_path = Column(String(255), nullable=True) class Artifact(Base): """Медиа-артефакты для повторного использования в рассылках""" __tablename__ = 'artifacts' id = Column(Integer, primary_key=True, autoincrement=True) filename = Column(String(255), nullable=False) file_type = Column(String(50), nullable=False) # 'photo', 'document' file_id = Column(String(255), nullable=False, unique=True) created_at = Column(DateTime, default=datetime.utcnow) def __repr__(self): return f""