This commit is contained in:
Ivan Vetrov 2026-04-15 14:00:23 +03:00
parent 6bfb634a0c
commit 0d6b424e99
13 changed files with 2095 additions and 0 deletions

BIN
bot-cat.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 MiB

BIN
bot-cat.mp4 Normal file

Binary file not shown.

13
config/.env Normal file
View File

@ -0,0 +1,13 @@
# Файл .env для конфигурации
# Скопируй этот файл в .env.local и заполни своими значениями
# Telegram Bot Token (получи у @BotFather)
TELEGRAM_BOT_TOKEN=8046228351:AAFhlgWjNo4vn9L_kp4OP5YzZlNa6Vp_lJE
# Опциональный ID администратора для логирования
ADMIN_ID=1821732339
# Дополнительные настройки
DEBUG=False
DEBUG=False
LOG_LEVEL=INFO

12
config/.env.example Normal file
View File

@ -0,0 +1,12 @@
# Файл .env для конфигурации
# Скопируй этот файл в .env.local и заполни своими значениями
# Telegram Bot Token (получи у @BotFather)
TELEGRAM_BOT_TOKEN=7123456789:ABCDEFGHIJKLMNOP1234567890ABCDEFGH
# Опциональный ID администратора для логирования
ADMIN_ID=123456789
# Дополнительные настройки
DEBUG=False
LOG_LEVEL=INFO

19
config/__init__.py Normal file
View File

@ -0,0 +1,19 @@
"""
Конфигурация бота Fly-Fly
"""
from .config import (
TELEGRAM_BOT_TOKEN,
POINTS_SCHOOL_COMPLETE,
POINTS_REFERRAL_BONUS,
POINTS_BUTTERFLY_ACTIVATION,
DATABASE_FILE
)
__all__ = [
'TELEGRAM_BOT_TOKEN',
'POINTS_SCHOOL_COMPLETE',
'POINTS_REFERRAL_BONUS',
'POINTS_BUTTERFLY_ACTIVATION',
'DATABASE_FILE'
]

78
config/config.py Normal file
View File

@ -0,0 +1,78 @@
# Конфигурация бота Fly-Fly
import os
from dotenv import load_dotenv
# Загружаем .env из текущей папки (корень проекта)
# Ищем сначала .env в корневой папке проекта, затем в папке config
base_dir = os.path.dirname(os.path.dirname(__file__))
env_file = os.path.join(base_dir, '.env')
if not os.path.exists(env_file):
env_file = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(env_file)
# Telegram Bot Token
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
# Опциональный прокси для Telegram (например: socks5h://user:pass@host:port)
TELEGRAM_PROXY_URL = os.getenv('TELEGRAM_PROXY_URL')
# Таймауты запросов к Telegram API (секунды)
TELEGRAM_CONNECT_TIMEOUT = int(os.getenv('TELEGRAM_CONNECT_TIMEOUT', '30'))
TELEGRAM_READ_TIMEOUT = int(os.getenv('TELEGRAM_READ_TIMEOUT', '120'))
# Пути к файлам данных
DATABASE_FILE = 'data/users.json'
STATS_FILE = 'data/stats.json'
# Константы системы
POINTS_SCHOOL_COMPLETE = 10
POINTS_REFERRAL_BONUS = 5
POINTS_BUTTERFLY_ACTIVATION = 30
# Максимум приглашений в месяц для бонуса активации
MAX_ACTIVATIONS_PER_MONTH = 1
# Таймауты (в секундах)
SCHOOL_QUESTION_TIMEOUT = 120
# ID администратора для логирования (установлен явно)
ADMIN_ID = 7665285886
# ID группы для проверки подписки (ссылка: https://t.me/+2RjGyDueqqAyM2Ey)
# Чтобы получить ID, отправьте сообщение в @userinfobot или @RawDataBot
REQUIRED_GROUP_CHAT_ID = os.getenv('REQUIRED_GROUP_CHAT_ID', '-1002395700013')
REQUIRED_GROUP_LINK = "https://t.me/+2RjGyDueqqAyM2Ey"
# Сообщения
MESSAGES = {
'welcome_owner': (
"🦋 **Привет, ловец бабочек!**\n\n"
"Добро пожаловать в мир **Fly-Fly**! 🌟\n\n"
"Ты стал владельцем Бабочкария и готов начать приключение? "
"Давай сначала пройдём школу бабочковода и узнаем, как ухаживать за этими чудесными созданиями!"
),
'welcome_referral': (
"🦋 **Тебя пригласили в стаю!**\n\n"
"Ура! Твой друг пригласил тебя присоединиться к его команде в **Fly-Fly**! 🌟\n\n"
"Сначала пройдём школу бабочковода, а потом ты влишься в стаю!"
),
'school_start': (
"📚 **Школа бабочковода**\n\n"
"Здесь ты узнаешь всё о бабочках и станешь настоящим ловцом! "
"Всего 5 увлекательных вопросов. Готов? 🦋"
),
'school_complete': (
"🎉 **Отлично! Ты окончил школу!**\n\n"
"Ты получил **+{points} очков**! 📈\n\n"
"Теперь ты готов ловить бабочек и создавать свою стаю!"
),
'referral_bonus': (
"✨ **Новый друг присоединился!**\n\n"
"{friend_name} вступил в твою стаю! "
"Ты получил **+{points} очков**! 🎊"
),
}
# ID или username требуемой группы (например: -1001234567890 или @my_group)
# Если не указан — проверка членства группы отключена
REQUIRED_GROUP_CHAT_ID = os.getenv('REQUIRED_GROUP_CHAT_ID')

23
main.py Normal file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""
Главный скрипт запуска бота Fly-Fly
Запуск: python main.py
"""
import sys
import os
# Добавляем текущую папку в path
sys.path.insert(0, os.path.dirname(__file__))
from src.bot import bot
if __name__ == "__main__":
print("🦋 Бот Fly-Fly запущен!")
print("🤖 @fly_fly_team_bot")
print("Ожидаю сообщений...\n")
try:
bot.infinity_polling()
except Exception as e:
print(f"❌ Ошибка: {e}")

11
src/__init__.py Normal file
View File

@ -0,0 +1,11 @@
"""
Telegram Bot Fly-Fly - пакет исходного кода
"""
__version__ = "1.0.0"
__author__ = "Fly-Fly Team"
from .bot import bot
from .database import Database
__all__ = ['bot', 'Database']

166
src/admin_tools.py Normal file
View File

@ -0,0 +1,166 @@
# Дополнительные команды администратора и отладки
import json
from datetime import datetime
from .database import Database
from config import TELEGRAM_BOT_TOKEN
class AdminTools:
"""Инструменты для администратора"""
def __init__(self, admin_id: int):
self.admin_id = admin_id
self.db = Database()
def get_stats(self) -> str:
"""Получить общую статистику бота"""
data = self.db.load_data()
total_users = len(data)
school_completed = sum(1 for u in data.values() if u.get('school_completed', False))
butterfly_owners = sum(1 for u in data.values() if u.get('is_butterfly_owner', False))
total_points = sum(u.get('total_points', 0) for u in data.values())
stats = (
f"📊 **Статистика бота**\n\n"
f"👥 Всего пользователей: {total_users}\n"
f"📚 Прошли школу: {school_completed}\n"
f"🦋 Владельцев Бабочкария: {butterfly_owners}\n"
f"Всего очков в системе: {total_points}\n"
)
return stats
def reset_user_data(self, user_id: int) -> bool:
"""Сбросить данные пользователя (для тестирования)"""
try:
data = self.db.load_data()
user_id_str = str(user_id)
if user_id_str in data:
del data[user_id_str]
self.db.save_data(data)
return True
except:
pass
return False
def backup_database(self, filename: str = None) -> bool:
"""Создать резервную копию базы данных"""
try:
import shutil
from datetime import datetime
if filename is None:
filename = f"backup_users_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
backup_path = f"backups/{filename}"
import os
os.makedirs("backups", exist_ok=True)
shutil.copy(self.db.users_file, backup_path)
return True
except:
return False
def export_leaderboard(self) -> str:
"""Экспортировать лидерборд"""
ranking = self.db.get_users_ranking()
teams_ranking = self.db.get_teams_ranking()
text = "🏆 **ЛИДЕРБОРД**\n\n"
text += "👥 **Топ пользователей:**\n"
for idx, user in enumerate(ranking, 1):
medal = "🥇" if idx == 1 else "🥈" if idx == 2 else "🥉" if idx == 3 else f"{idx}️⃣"
text += f"{medal} {user['first_name']}{user['total_points']}\n"
text += "\n🏆 **Топ стай:**\n"
for idx, team in enumerate(teams_ranking, 1):
medal = "🥇" if idx == 1 else "🥈" if idx == 2 else "🥉" if idx == 3 else f"{idx}️⃣"
text += f"{medal} {team['team_name']}{team['total_points']}\n"
return text
class DebugTools:
"""Инструменты отладки"""
def __init__(self):
self.db = Database()
def validate_database(self) -> list:
"""Проверить целостность базы данных"""
errors = []
data = self.db.load_data()
for user_id_str, user in data.items():
# Проверяем обязательные поля
if 'telegram_id' not in user:
errors.append(f"User {user_id_str}: missing telegram_id")
if 'first_name' not in user:
errors.append(f"User {user_id_str}: missing first_name")
if 'total_points' not in user:
errors.append(f"User {user_id_str}: missing total_points")
# Проверяем типы данных
if not isinstance(user.get('total_points', 0), int):
errors.append(f"User {user_id_str}: total_points is not int")
if not isinstance(user.get('school_completed', False), bool):
errors.append(f"User {user_id_str}: school_completed is not bool")
return errors
def print_user_data(self, user_id: int):
"""Распечатать данные пользователя для отладки"""
user = self.db.get_user(user_id)
if user:
print(json.dumps(user, indent=2, ensure_ascii=False))
else:
print(f"User {user_id} not found")
def clean_up_invalid_data(self):
"""Очистить некорректные данные"""
data = self.db.load_data()
for user_id_str, user in data.items():
# Исправляем типы данных
if 'total_points' in user and not isinstance(user['total_points'], int):
try:
user['total_points'] = int(user['total_points'])
except:
user['total_points'] = 0
if 'school_completed' in user and not isinstance(user['school_completed'], bool):
user['school_completed'] = bool(user['school_completed'])
self.db.save_data(data)
# Пример использования (для тестирования)
if __name__ == "__main__":
# Инструменты отладки
debug = DebugTools()
print("🔍 Проверка целостности базы данных...")
errors = debug.validate_database()
if errors:
print(f"❌ Найдено {len(errors)} ошибок:")
for error in errors:
print(f" - {error}")
else:
print("✅ База данных в порядке!")
# Получить общую статистику
print("\n📊 Статистика бота:")
db = Database()
data = db.load_data()
print(f"Всего пользователей: {len(data)}")

1142
src/bot.py Normal file

File diff suppressed because it is too large Load Diff

311
src/database.py Normal file
View File

@ -0,0 +1,311 @@
# Модель данных и работа с JSON
import json
import os
from datetime import datetime
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict, field
import hashlib
@dataclass
class User:
"""Модель пользователя"""
telegram_id: int
first_name: str
username: Optional[str] = None
is_butterfly_owner: bool = False # Владелец Бабочкария
school_completed: bool = False
total_points: int = 0
team_id: Optional[int] = None # ID капитана стаи
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
last_activation_date: Optional[str] = None # Дата последней активации Бабочкария
invited_friends: List[int] = field(default_factory=list) # IDs приглашённых друзей
referred_by: Optional[int] = None # ID того, кто пригласил
profile_updated: bool = False
@dataclass
class Team:
"""Модель стаи бабочек"""
team_id: int # ID капитана
team_name: str
members: List[int] = field(default_factory=list) # IDs участников
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
total_points: int = 0 # Сумма очков всех участников
def get_points(self, users_data: Dict) -> int:
"""Вычислить сумму очков стаи"""
total = 0
for member_id in self.members:
if member_id in users_data:
total += users_data[member_id].get('total_points', 0)
return total
@dataclass
class SchoolQuestion:
"""Модель вопроса школы"""
question_number: int
title: str
learning_text: str
question_text: str
options: List[str]
correct_answer: int # Индекс правильного ответа
explanation: str
class Database:
"""Класс для работы с базой данных"""
def __init__(self, users_file: str = 'data/users.json'):
self.users_file = users_file
self.ensure_database_exists()
def ensure_database_exists(self):
"""Создать директорию и файлы, если их нет"""
os.makedirs('data', exist_ok=True)
if not os.path.exists(self.users_file):
self.save_data({})
def load_data(self) -> Dict:
"""Загрузить все данные пользователей"""
try:
with open(self.users_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Ошибка при загрузке данных: {e}")
return {}
def save_data(self, data: Dict):
"""Сохранить данные пользователей"""
try:
with open(self.users_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"Ошибка при сохранении данных: {e}")
def get_user(self, telegram_id: int) -> Optional[Dict]:
"""Получить пользователя по ID"""
data = self.load_data()
user_id_str = str(telegram_id)
return data.get(user_id_str)
def user_exists(self, telegram_id: int) -> bool:
"""Проверить, существует ли пользователь"""
return self.get_user(telegram_id) is not None
def create_user(self, telegram_id: int, first_name: str,
username: Optional[str] = None,
is_butterfly_owner: bool = False,
referred_by: Optional[int] = None) -> Dict:
"""Создать нового пользователя"""
data = self.load_data()
user_id_str = str(telegram_id)
if user_id_str in data:
return data[user_id_str]
new_user = {
'telegram_id': telegram_id,
'first_name': first_name,
'username': username,
'is_butterfly_owner': is_butterfly_owner,
'school_completed': False,
'total_points': 0,
'team_id': None,
'team_name': None,
'created_at': datetime.now().isoformat(),
'last_activation_date': None,
'invited_friends': [],
'referred_by': referred_by,
'profile_updated': False
}
data[user_id_str] = new_user
self.save_data(data)
return new_user
def update_user(self, telegram_id: int, **kwargs):
"""Обновить данные пользователя"""
data = self.load_data()
user_id_str = str(telegram_id)
if user_id_str in data:
data[user_id_str].update(kwargs)
self.save_data(data)
return data[user_id_str]
return None
def add_points(self, telegram_id: int, points: int):
"""Добавить очки пользователю"""
user = self.get_user(telegram_id)
if user:
current_points = user.get('total_points', 0)
self.update_user(telegram_id, total_points=current_points + points)
def get_team_members(self, team_id: int) -> List[Dict]:
"""Получить всех членов стаи"""
data = self.load_data()
members = []
for user_id_str, user in data.items():
if user.get('team_id') == team_id:
members.append(user)
return members
def get_teams_ranking(self) -> List[Dict]:
"""Получить рейтинг стай (топ-10)"""
data = self.load_data()
teams = {}
# Собрать стаи
for user_id_str, user in data.items():
if user.get('is_butterfly_owner'):
team_id = int(user_id_str)
members = self.get_team_members(team_id)
team_points = sum(m.get('total_points', 0) for m in members)
team_name = f"Стая {user.get('first_name', 'Аноним')}"
teams[team_id] = {
'team_id': team_id,
'team_name': team_name,
'captain_name': user.get('first_name', 'Аноним'),
'members_count': len(members),
'total_points': team_points
}
# Сортировать по очкам
sorted_teams = sorted(teams.values(), key=lambda x: x['total_points'], reverse=True)
return sorted_teams[:10]
def get_users_ranking(self) -> List[Dict]:
"""Получить рейтинг пользователей (топ-10)"""
data = self.load_data()
users = []
for user_id_str, user in data.items():
users.append({
'telegram_id': int(user_id_str),
'first_name': user.get('first_name', 'Аноним'),
'total_points': user.get('total_points', 0),
'team_id': user.get('team_id')
})
# Сортировать по очкам
sorted_users = sorted(users, key=lambda x: x['total_points'], reverse=True)
return sorted_users[:10]
def get_user_ranking_position(self, telegram_id: int) -> int:
"""Получить позицию пользователя в общем рейтинге"""
ranking = self.get_users_ranking()
for idx, user in enumerate(ranking, 1):
if user['telegram_id'] == telegram_id:
return idx
# Если не в топ-10, считаем полный рейтинг
data = self.load_data()
all_users = []
for user_id_str, user in data.items():
all_users.append({
'telegram_id': int(user_id_str),
'total_points': user.get('total_points', 0)
})
all_users = sorted(all_users, key=lambda x: x['total_points'], reverse=True)
for idx, user in enumerate(all_users, 1):
if user['telegram_id'] == telegram_id:
return idx
return len(all_users) + 1
def get_team_ranking_position(self, team_id: int) -> int:
"""Получить позицию стаи в рейтинге"""
ranking = self.get_teams_ranking()
for idx, team in enumerate(ranking, 1):
if team['team_id'] == team_id:
return idx
return None
def create_team(self, captain_id: int, team_name: str) -> Dict:
"""Создать новую стаю"""
user = self.get_user(captain_id)
if not user:
return None
# Обновляем пользователя - он становится капитаном
self.update_user(captain_id, team_id=captain_id, team_name=team_name)
return {
'team_id': captain_id,
'team_name': team_name,
'members': [captain_id],
'created_at': datetime.now().isoformat()
}
def get_team(self, team_id: int) -> Optional[Dict]:
"""Получить информацию о стае"""
captain = self.get_user(team_id)
if not captain:
return None
# Капитан должен иметь team_id == team_id (быть капитаном)
if captain.get('team_id') != team_id:
return None
members = self.get_team_members(team_id)
total_points = sum(m.get('total_points', 0) for m in members)
return {
'team_id': team_id,
'team_name': captain.get('team_name', f"Стая {captain.get('first_name', 'Аноним')}"),
'captain_name': captain.get('first_name', 'Аноним'),
'members': [m['telegram_id'] for m in members],
'members_count': len(members),
'total_points': total_points,
'created_at': captain.get('created_at')
}
def add_member_to_team(self, team_id: int, user_id: int) -> bool:
"""Добавить пользователя в стаю"""
user = self.get_user(user_id)
if not user:
return False
# Обновляем team_id пользователя
self.update_user(user_id, team_id=team_id)
return True
def remove_member_from_team(self, team_id: int, user_id: int) -> bool:
"""Удалить пользователя из стаи"""
user = self.get_user(user_id)
if not user or user.get('team_id') != team_id:
return False
# Обновляем team_id пользователя на None и очищаем team_name
self.update_user(user_id, team_id=None, team_name=None)
return True
def update_team_name(self, team_id: int, new_name: str) -> bool:
"""Изменить название стаи"""
captain = self.get_user(team_id)
if not captain or captain.get('team_id') != team_id:
return False
self.update_user(team_id, team_name=new_name)
return True
def disband_team(self, team_id: int) -> bool:
"""Разойти стаю - удалить всех участников из команды"""
members = self.get_team_members(team_id)
for member in members:
self.update_user(member['telegram_id'], team_id=None, team_name=None)
# Капитан тоже выходит
self.update_user(team_id, team_id=None, team_name=None)
return True
def mark_school_completed(self, telegram_id: int) -> bool:
"""Отметить школу как пройденную"""
self.update_user(telegram_id, school_completed=True)
return True

117
src/school.py Normal file
View File

@ -0,0 +1,117 @@
# Содержание школы бабочковода
from dataclasses import dataclass
from typing import List
@dataclass
class Question:
"""Класс для вопроса школы"""
number: int
title: str
learning_text: str
question_text: str
options: List[str]
correct_answer: int # 0-3
explanation: str
# Школа бабочковода - 5 вопросов
SCHOOL_CURRICULUM = [
Question(
number=1,
title="Кто такие бабочки?",
learning_text=(
"🦋 **Интересный факт:**\n\n"
"Бабочки — это насекомые с четырьмя крыльями, покрытыми чешуйками. "
"Их крылья создают прекрасные узоры, которые помогают им общаться с другими бабочками. "
"Есть более 400 000 видов бабочек! Красиво, правда? ✨"
),
question_text="Сколько крыльев у бабочки?",
options=["2 крыла", "4 крыла", "6 крыльев", "8 крыльев"],
correct_answer=1,
explanation="Правильно! У бабочек 4 крыла, расположенные попарно спереди и сзади. Они используют их, чтобы летать в поисках цветов и друзей."
),
Question(
number=2,
title="Что едят бабочки?",
learning_text=(
"🌸 **Вкусная жизнь:**\n\n"
"Бабочки — большие любительницы сладкого! Они пьют нектар из цветов, "
"используя свой длинный хоботок (как соломинка). "
"Некоторые бабочки также едят спелые фрукты и грязь для получения минералов."
),
question_text="Как бабочка пьет нектар из цветка?",
options=["Грызёт цветок", "Использует хоботок как трубочку", "Проглатывает весь цветок", "Не пьет вообще"],
correct_answer=1,
explanation="Умница! Бабочки имеют длинный хоботок (спираль), который они разворачивают и вставляют в цветок, как соломинку, чтобы пить нектар."
),
Question(
number=3,
title="Жизненный цикл бабочки",
learning_text=(
"🔄 **Волшебное превращение:**\n\n"
"Бабочка проходит четыре стадии жизни:\n"
"1⃣ Яйцо (самое маленькое)\n"
"2⃣ Гусеница (очень голодная и прожорливая!)\n"
"3⃣ Куколка (спит внутри кокона)\n"
"4⃣ Бабочка (летает и радуется жизни)\n\n"
"Всё это называется метаморфозом — чудесным преобразованием!"
),
question_text="Сколько стадий в жизни бабочки?",
options=["1 стадия", "2 стадии", "3 стадии", "4 стадии"],
correct_answer=3,
explanation="Отлично! Бабочка проходит 4 стадии: яйцо → гусеница → куколка → бабочка. Это невероятное путешествие длится несколько недель!"
),
Question(
number=4,
title="Где живут бабочки?",
learning_text=(
"🌍 **По всему миру:**\n\n"
"Бабочки живут почти везде на Земле! Они встречаются в лесах, полях, горах, "
"садах, парках и даже в городах. Одни бабочки любят тепло, другие — прохладу. "
"Больше всего бабочек в тропических лесах, где очень много разных цветов!"
),
question_text="Где ты можешь встретить бабочку?",
options=["Только в лесу", "Только в пустыне", "Только в горах", "В садах, лесах, полях и парках"],
correct_answer=3,
explanation="Верно! Бабочки встречаются почти везде, особенно там, где растут цветы. Прогулка по парку летом — отличный способ увидеть их!"
),
Question(
number=5,
title="Как стать ловцом бабочек?",
learning_text=(
"🎯 **Искусство наблюдения:**\n\n"
"Настоящий ловец бабочек — это не тот, кто их ловит! "
"Это человек, который понимает, уважает и защищает бабочек. "
"Ловец наблюдает, учится, делится знаниями и помогает сохранить эти чудесные создания. "
"Присоединись к нашему сообществу Fly-Fly и стань часть большой стаи ловцов!"
),
question_text="Что нужно ловцу бабочек в первую очередь?",
options=["Сачок и банка", "Терпение и знания", "Огромная коллекция", "Быстрые ноги"],
correct_answer=1,
explanation="Совершенно верно! Настоящий ловец полон терпения и знаний. Он наблюдает, учится, и со временем становится экспертом в мире бабочек!"
),
]
def get_question(number: int) -> Question:
"""Получить вопрос по номеру (1-5)"""
if 1 <= number <= 5:
return SCHOOL_CURRICULUM[number - 1]
return None
def validate_answer(question_number: int, answer_index: int) -> bool:
"""Проверить, правильный ли ответ"""
question = get_question(question_number)
if question:
return answer_index == question.correct_answer
return False
def get_answer_explanation(question_number: int) -> str:
"""Получить объяснение после ответа"""
question = get_question(question_number)
if question:
return question.explanation
return ""

203
src/utils.py Normal file
View File

@ -0,0 +1,203 @@
# Утилиты и вспомогательные функции
import json
from datetime import datetime, timedelta
from typing import Optional, Dict
from .database import Database
class PointsValidator:
"""Валидатор для защиты от повторных начислений"""
VALIDATION_FILE = 'data/points_log.json'
@staticmethod
def _load_log():
"""Загрузить лог начисленных очков"""
try:
with open(PointsValidator.VALIDATION_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {}
@staticmethod
def _save_log(log: Dict):
"""Сохранить лог начисленных очков"""
try:
with open(PointsValidator.VALIDATION_FILE, 'w', encoding='utf-8') as f:
json.dump(log, f, ensure_ascii=False, indent=2)
except:
pass
@staticmethod
def can_complete_school(user_id: int) -> bool:
"""Проверить, может ли пользователь пройти школу"""
log = PointsValidator._load_log()
user_id_str = str(user_id)
if user_id_str not in log:
return True
user_log = log[user_id_str]
if 'school_completed' in user_log:
return False
return True
@staticmethod
def mark_school_completed(user_id: int):
"""Отметить школу как пройденную"""
log = PointsValidator._load_log()
user_id_str = str(user_id)
if user_id_str not in log:
log[user_id_str] = {}
log[user_id_str]['school_completed'] = datetime.now().isoformat()
PointsValidator._save_log(log)
@staticmethod
def can_activate_butterfly_farm(user_id: int) -> bool:
"""Проверить, может ли пользователь активировать Бабочкарий"""
log = PointsValidator._load_log()
user_id_str = str(user_id)
if user_id_str not in log:
return True
user_log = log[user_id_str]
activations = user_log.get('butterfly_activations', [])
# Проверяем, была ли активация в этом месяце
today = datetime.now()
current_month = today.strftime('%Y-%m')
for activation_date_str in activations:
try:
activation_date = datetime.fromisoformat(activation_date_str)
if activation_date.strftime('%Y-%m') == current_month:
return False
except:
pass
return True
@staticmethod
def mark_butterfly_activation(user_id: int):
"""Отметить активацию Бабочкария"""
log = PointsValidator._load_log()
user_id_str = str(user_id)
if user_id_str not in log:
log[user_id_str] = {}
if 'butterfly_activations' not in log[user_id_str]:
log[user_id_str]['butterfly_activations'] = []
log[user_id_str]['butterfly_activations'].append(datetime.now().isoformat())
PointsValidator._save_log(log)
class TeamManager:
"""Менеджер для работы со стаями"""
TEAMS_FILE = 'data/teams.json'
@staticmethod
def _load_teams():
"""Загрузить данные о стаях"""
try:
with open(TeamManager.TEAMS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {}
@staticmethod
def _save_teams(teams: Dict):
"""Сохранить данные о стаях"""
try:
with open(TeamManager.TEAMS_FILE, 'w', encoding='utf-8') as f:
json.dump(teams, f, ensure_ascii=False, indent=2)
except:
pass
@staticmethod
def get_team_name(team_id: int) -> Optional[str]:
"""Получить название стаи"""
teams = TeamManager._load_teams()
team_id_str = str(team_id)
if team_id_str in teams:
return teams[team_id_str].get('team_name')
return None
@staticmethod
def update_team_name(team_id: int, new_name: str):
"""Обновить название стаи"""
teams = TeamManager._load_teams()
team_id_str = str(team_id)
if team_id_str in teams:
teams[team_id_str]['team_name'] = new_name
TeamManager._save_teams(teams)
@staticmethod
def add_member(team_id: int, member_id: int):
"""Добавить участника в стаю"""
teams = TeamManager._load_teams()
team_id_str = str(team_id)
if team_id_str not in teams:
teams[team_id_str] = {
'team_name': f'Стая {team_id}',
'captain_id': team_id,
'members': [],
'created_at': datetime.now().isoformat()
}
if member_id not in teams[team_id_str]['members']:
teams[team_id_str]['members'].append(member_id)
TeamManager._save_teams(teams)
@staticmethod
def remove_member(team_id: int, member_id: int):
"""Удалить участника из стаи"""
teams = TeamManager._load_teams()
team_id_str = str(team_id)
if team_id_str in teams:
if member_id in teams[team_id_str]['members']:
teams[team_id_str]['members'].remove(member_id)
TeamManager._save_teams(teams)
class MessageFormatter:
"""Форматтер для сообщений"""
@staticmethod
def format_points_message(points: int, reason: str) -> str:
"""Форматировать сообщение о начислении очков"""
emoji = "" if points > 0 else ""
sign = "+" if points > 0 else ""
return f"{emoji} {sign}{points} очков за {reason}"
@staticmethod
def format_ranking_row(position: int, name: str, points: int) -> str:
"""Форматировать строку рейтинга"""
medals = {1: "🥇", 2: "🥈", 3: "🥉"}
medal = medals.get(position, f"{position}️⃣")
return f"{medal} {name}{points}"
# Функции для работы с HTML/Markdown
def escape_markdown(text: str) -> str:
"""Экранировать специальные символы Markdown"""
special_chars = ['*', '_', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
for char in special_chars:
text = text.replace(char, f'\\{char}')
return text