work
This commit is contained in:
parent
6bfb634a0c
commit
0d6b424e99
Binary file not shown.
|
After Width: | Height: | Size: 34 MiB |
Binary file not shown.
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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'
|
||||||
|
]
|
||||||
|
|
@ -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')
|
||||||
|
|
@ -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}")
|
||||||
|
|
@ -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']
|
||||||
|
|
@ -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)}")
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
|
@ -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 ""
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue