diff --git a/.flake8 b/.flake8 index a0135fb..639ca0c 100644 --- a/.flake8 +++ b/.flake8 @@ -94,6 +94,8 @@ ignore = E712 ; Line too long (133 > 120 characters) E501 + ;the function is too complex + C901 per-file-ignores = ; all tests diff --git a/bot/database/task_dao.py b/bot/database/task_dao.py index eda55ad..1a9fea2 100644 --- a/bot/database/task_dao.py +++ b/bot/database/task_dao.py @@ -73,3 +73,70 @@ def missed_user_tasks(self, user: User): ) .all() ) + + def decided_users(self, task_name: str): + completed_tasks = ( + self.session.query(Task) + .filter(Task.name == task_name, Task.completed == True) + .all() + ) + user_ids = {task.assigned_user_id for task in completed_tasks} + + return len(user_ids) + + def all_users(self, task_name: str): + tasks = ( + self.session.query(Task) + .filter( + Task.name == task_name, + ) + .all() + ) + user_ids = {task.assigned_user_id for task in tasks} + + return len(user_ids) + + def index_of_time(self, task_name: str, assigned_user_id: int): + task = ( + self.session.query(Task) + .filter( + Task.name == task_name, + Task.assigned_user_id == assigned_user_id, + Task.completed == True, + ) + .first() + ) + + if task is None or task.deadline is None: + return 0 # Задача не найдена или у задачи нет дедлайна + + time_taken = datetime.now() - task.deadline + time_until_deadline = ( + task.deadline - datetime.now() + ) # отрицательное, если просрочено + + # Абсолютные значения времени в секундах + time_taken_sec = abs(time_taken.total_seconds()) + time_until_deadline_sec = abs(time_until_deadline.total_seconds()) + + if time_taken_sec == 0: + return None # избегаем деления на ноль + + ratio = round(time_until_deadline_sec / time_taken_sec, 2) + + return ratio + + def score_for_tasks(self, task_name: str, user_id: int): + from settings import Config + + S_min = Config.s_min + N = self.decided_users(task_name) + N_total = self.all_users(task_name) + time_index = self.index_of_time(task_name, user_id) + + if time_index is None: + return S_min # если задача не выполнена или нет дедлайна + + R_time = min(time_index, 0.35) + score = max(S_min, 500 * (1 - N / N_total) * (1 + R_time)) + return score diff --git a/bot/database/user_dao.py b/bot/database/user_dao.py index 85991fe..ef791f6 100644 --- a/bot/database/user_dao.py +++ b/bot/database/user_dao.py @@ -32,9 +32,7 @@ def create_user( def get_all_students(self) -> List[User]: """Получить всех студентов (исключая учителей по tg_id).""" return ( - self.session.query(User) - .filter(User.tg_id.notin_(config.teacher_ids)) - .all() + self.session.query(User).filter(User.tg_id.notin_(config.teacher_ids)).all() ) def get_all_active_students(self) -> List[User]: @@ -47,20 +45,12 @@ def get_all_active_students(self) -> List[User]: def get_user_id_by_username(self, username: str) -> Optional[int]: """Вернуть внутренний ID пользователя по username или None.""" - user = ( - self.session.query(User) - .filter(User.username == username) - .first() - ) + user = self.session.query(User).filter(User.username == username).first() return user.id if user else None def get_user_by_tg_id(self, tg_id: int) -> Optional[User]: """Получить пользователя по его Telegram ID.""" - return ( - self.session.query(User) - .filter(User.tg_id == tg_id) - .first() - ) + return self.session.query(User).filter(User.tg_id == tg_id).first() def get_all_students_with_tasks(self) -> List[User]: """Получить всех студентов вместе с их невыполненными заданиями.""" @@ -85,18 +75,11 @@ def heal(self, user: User) -> None: def get_teachers(self) -> List[User]: """Получить всех учителей (старшекурсников) по tg_id.""" teachers = ( - self.session.query(User) - .filter(User.tg_id.in_(config.teacher_ids)) - .all() + self.session.query(User).filter(User.tg_id.in_(config.teacher_ids)).all() ) logger.info(f"Получены учителя: {teachers}") return teachers def leaderboard(self, limit: int = 20) -> List[User]: """Извлечь топ-`limit` пользователей, сортируя по убыванию points.""" - return ( - self.session.query(User) - .order_by(User.points.desc()) - .limit(limit) - .all() - ) + return self.session.query(User).order_by(User.points.desc()).limit(limit).all() diff --git a/bot/handlers/leaderboard.py b/bot/handlers/leaderboard.py index 5dc3a12..f2d945b 100644 --- a/bot/handlers/leaderboard.py +++ b/bot/handlers/leaderboard.py @@ -32,13 +32,13 @@ def format_user_status(user: User, top_rating: list[User]) -> list[str]: messages.append(f"🎉 Вы на {rank}-м месте в топ-20!") else: if user.points == 0: - messages.append("😔 У вас пока нет баллов. Решайте задачи, чтобы попасть в топ!") + messages.append( + "😔 У вас пока нет баллов. Решайте задачи, чтобы попасть в топ!" + ) else: last_top_score = top_rating[-1].points if top_rating else 0 needed = last_top_score - user.points + 1 - messages.append( - f"👉 Чтобы войти в топ-20, нужно ещё {needed} балл(ов)." - ) + messages.append(f"👉 Чтобы войти в топ-20, нужно ещё {needed} балл(ов).") return messages diff --git a/bot/main.py b/bot/main.py index 440f4be..ac19f3f 100644 --- a/bot/main.py +++ b/bot/main.py @@ -91,5 +91,6 @@ async def main(): await dp.start_polling(bot) + if __name__ == "__main__": asyncio.run(main()) diff --git a/bot/settings.py b/bot/settings.py index 54a358a..27adc1e 100644 --- a/bot/settings.py +++ b/bot/settings.py @@ -8,7 +8,7 @@ class Config(BaseSettings): BOT_TOKEN: str = ( - "8163900085:AAFw6f80JCzYlc77bvxeP9hhbMaEeveE8Is" # your tg bot token from botfather + "7664854738:AAGMUTXm2uT7eUR4O8tl7lLs145f7Fv5KoM" # your tg bot token from botfather ) DATABASE_URL: str = "postgresql://ctf:ctf@localhost:5432/ctf" ADMIN_NICKNAMES: str = "tgadminnick1,tgadminnick2" @@ -19,6 +19,8 @@ class Config(BaseSettings): ENV: str = "dev" minimum_xp_count_to_heal: int = 10 _teacher_ids: list[int] = "393200400,704339275" + + s_min: int = 50 HEAL_LIMIT: int = 3 @property diff --git a/bot/tasks.py b/bot/tasks.py index 580d0d6..ab7d9d7 100644 --- a/bot/tasks.py +++ b/bot/tasks.py @@ -5,6 +5,7 @@ from utils.notifications import Notifications from database.db import get_db from database.user_dao import UserDAO +from database.task_dao import TaskDao from utils.root_me import get_solved_tasks_of_student @@ -15,40 +16,73 @@ async def sync_education_tasks(bot: Bot): while True: with get_db() as session: - # Fetch all users with their tasks - dao = UserDAO(session) - users = dao.get_all_students_with_tasks() - for user in users: - if user.root_me_nickname: - # try: - solved_tasks = await get_solved_tasks_of_student( - user.root_me_nickname - ) - for task in user.tasks: - task.completed = task.name in solved_tasks - if not task.completed and task.is_expired and not task.violation_recorded: - user.lives -= 1 - user.violations += 1 - task.violation_recorded = True # Отмечаем, что нарушение обработано - teacher_message = ( - f"Задача {task.name} истека у студента {user}." - ) - logger.info(teacher_message) - notify = Notifications(bot) - await notify.say_about_deadline_fail(teacher_message) - student_message = f"Ты потерял 1 HP за задачу {task.name}. 😢 Пожалуйста, старайся выполнять задания вовремя, чтобы избежать потерь.\ - Если у тебя есть вопросы или трудности, не стесняйся обращаться за помощью в общий чат." - logger.info(student_message) - await notify._say_student(user, student_message) - - session.commit() - logger.info(f"Synced tasks for user: {user.username}") - # except Exception as e: - # logger.error(f"Error syncing tasks for {user.username}: {e.}") - await asyncio.sleep(60) + await process_all_users_tasks(bot, session) await asyncio.sleep(0.1) +async def process_all_users_tasks(bot: Bot, session): + dao = UserDAO(session) + users = dao.get_all_students_with_tasks() + for user in users: + if user.root_me_nickname: + await process_user_tasks(bot, session, user) + logger.info(f"Synced tasks for user: {user.username}") + await asyncio.sleep(60) + + +async def process_user_tasks(bot: Bot, session, user): + task_dao = TaskDao(session) + notify = Notifications(bot) + + try: + solved_tasks = await get_solved_tasks_of_student(user.root_me_nickname) + for task in user.tasks: + await process_single_task(task_dao, notify, user, task, solved_tasks) + session.commit() + except Exception as e: + logger.error(f"Error syncing tasks for {user.username}: {e}") + + +async def process_single_task(task_dao, notify, user, task, solved_tasks): + task.completed = task.name in solved_tasks + + if task.completed: + await handle_completed_task(task_dao, notify, user, task) + elif not task.completed and task.is_expired and not task.violation_recorded: + await handle_expired_task(notify, user, task) + + +async def handle_completed_task(task_dao, notify, user, task): + score = task_dao.score_for_tasks(task.name, user.id) + user.points += score + + student_message = f"Молодец, ты решил задачу {task.name} и получил {score} очков" + admin_log = f"{user.username} - {user.full_name} решил задачу {task.name} и получил {score} очков" + + logger.info(admin_log) + await notify._say_teachers(admin_log) + await notify._say_student(user, student_message) + + +async def handle_expired_task(notify, user, task): + user.lives -= 1 + user.violations += 1 + task.violation_recorded = True + + teacher_message = f"Задача {task.name} истека у студента {user}." + logger.info(teacher_message) + + await notify.say_about_deadline_fail(teacher_message) + + student_message = ( + f"Ты потерял 1 HP за задачу {task.name}. 😢 Пожалуйста, старайся выполнять задания вовремя, " + "чтобы избежать потерь. Если у тебя есть вопросы или трудности, " + "не стесняйся обращаться за помощью в общий чат." + ) + logger.info(student_message) + await notify._say_student(user, student_message) + + async def restore_student_lives(): """Восстановление жизней всех активных студентов до 3-х.""" try: diff --git a/bot/utils/notifications.py b/bot/utils/notifications.py index 151bc18..a01080c 100644 --- a/bot/utils/notifications.py +++ b/bot/utils/notifications.py @@ -55,7 +55,7 @@ async def _say_teachers(self, message: str): async def _say_student(self, student: User, message: str): """Написать студенту о чем-то.""" try: - await self.bot.send_message(chat_id=student.tg_id, text=message) + await self.bot.send_message(text=message, chat_id=student.tg_id) logger.info(f"Sent {message} to {student.full_name} - @{student.username}") except TelegramBadRequest: logger.warning(