/
github_better
/
GenAI
Обзор
Документация
Войти
/
github_better
/
GenAI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Bot.py
1 680 строк
75 KB
github_better
upload files
10 ноя 2025, 20:04
10 ноя 2025, 20:04
a9e213d
Код
Авторство
О чём код?
import os import uuid import asyncio import logging from functools import partial import secrets import json import re from aiogram import Bot, types from aiogram.dispatcher import Dispatcher from aiogram.utils import executor from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from dotenv import load_dotenv import Lib.AI_request as AI_request import Lib.Memory as Memory import Lib.Text_formatting as Text_formatting import Lib.Translate_text as Translate_text import Lib.Prompt_editor as Prompt_editor import Database.Database as Database import Lib.Inventory as Inventory import Lib.Kboards as Kboards import Lib.Stats as Stats import Lib.Game_time as Game_time from Lib.Translate_text import translate_text from Lib.Debugger import debug_print # Размер памяти, должно делиться на 2 SHORT_SIZE = 4 #4 MIDDLE_SIZE = 6 #6 LONG_SIZE = 8 #8 # Не обязана делиться на 2 INFINITY_SIZE = 4 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) load_dotenv("Secret.env") TOKEN = os.getenv("BOT_TOKEN") BOT_TAG = "@bla_bla_bla1_bot" bot = Bot(token=TOKEN, parse_mode="HTML") storage = MemoryStorage() dp = Dispatcher(bot, storage=storage) sessions = {} async def to_thread(func, *args, **kwargs): debug_print(logger) """ Выполняет блокирующую функцию в пуле потоков, чтобы не блокировать event loop. :param func: функция для выполнения :param args: позиционные аргументы функции :param kwargs: именованные аргументы функции :return: результат выполнения функции """ return await asyncio.to_thread(partial(func, *args, **kwargs)) async def send_to_ai(*args, **kwargs): debug_print(logger) """ Прокси для вызова AI_request.send_to_AI в потоке. :param args: позиционные аргументы для AI_request.send_to_AI :param kwargs: именованные аргументы для AI_request.send_to_AI :return: результат от AI """ return await to_thread(AI_request.send_to_AI, *args, **kwargs) async def continue_ai(*args, **kwargs): debug_print(logger) """ Прокси для вызова AI_request.continue_answer в потоке. Используется когда нужно получить продолжение ответа от AI. :param args: позиционные аргументы для AI_request.continue_answer :param kwargs: именованные аргументы для AI_request.continue_answer :return: продолжение ответа AI """ return await to_thread(AI_request.continue_answer, *args, **kwargs) class UserSession: """ Хранилище состояния пользователя в оперативной памяти процесса. Содержит данные сессии, историю диалога, объекты памяти и игровые параметры. При перезапуске процесса все данные теряются. :param chat_id: идентификатор чата пользователя """ def __init__(self, chat_id): self.chat_id = chat_id self.username = None self.user_id = None self.session_uuid = None self.lang = 'en' self.history_id = None self.system_prompt = '' self.original_dialog = [] self.current_location = '' self.previous_location = '' self.long_m = Memory.Long_memory(LONG_SIZE) self.middle_m = Memory.Middle_memory(MIDDLE_SIZE, self.long_m, self.lang) self.short_m = Memory.Short_memory(SHORT_SIZE, self.middle_m) self.infinity_m = None self.in_dialog = False self.last_inline_message_id = None self.dialog_shown = False self.current_time = "" self.current_stats = "" self.current_inventory = "" self.current_weight = 0 self.current_slots = 0 def init_infinity(self, history_id): debug_print(logger) """ Инициализация бесконечной памяти после создания или загрузки history_id. :param history_id: идентификатор истории :return: ничего не возвращает """ self.infinity_m = Memory.Infinity_memory(INFINITY_SIZE, history_id) async def safe_delete_message(chat_id, message_id): debug_print(logger) """ Удаляет сообщение по идентификатору, игнорируя ошибки. :param chat_id: идентификатор чата :param message_id: идентификатор сообщения :return: ничего не возвращает """ try: await bot.delete_message(chat_id, message_id) except Exception: debug_print(logger, f"Couldn't delete message {message_id} in chat {chat_id}") async def send_inline(chat_id, text, reply_markup): debug_print(logger) """ Отправляет inline-сообщение и сохраняет его ID в сессии для последующего удаления. :param chat_id: идентификатор чата :param text: текст сообщения :param reply_markup: разметка inline-клавиатуры :return: отправленное сообщение """ msg = await bot.send_message(chat_id, text, reply_markup=reply_markup) sess = sessions.get(chat_id) if sess: sess.last_inline_message_id = msg.message_id return msg async def ai_call_with_proc(sess: UserSession, chat_id: int, call_func, *args, **kwargs): debug_print(logger) """ Универсальная обертка для вызовов AI с отображением индикатора "Processing...". Отправляет индикатор, выполняет функцию в потоке, затем удаляет индикатор. :param sess: объект сессии пользователя :param chat_id: идентификатор чата :param call_func: функция для вызова :param args: позиционные аргументы функции :param kwargs: именованные аргументы функции :return: результат выполнения call_func """ proc_msg = await bot.send_message(chat_id, translate_text("Processing...", "en", sess.lang)) try: if args and isinstance(args[0], str): pass result = await to_thread(call_func, *args, **kwargs) return result finally: await safe_delete_message(chat_id, proc_msg.message_id) # --- Хендлеры бота --- @dp.callback_query_handler(lambda c: c.data == 'load_history') async def cb_load_history(cq: types.CallbackQuery): debug_print(logger) """ Обработчик загрузки списка сохраненных историй пользователя. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) if not sess: await bot.answer_callback_query(cq.id, "Session not found enter: /start") return try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) histories_for_user = await to_thread(Database.find, "histories", "session_id", sess.session_uuid) if not histories_for_user: await bot.send_message(chat_id, translate_text("You have no saved histories yet.", "en", sess.lang)) await send_inline(chat_id, translate_text("Main menu", "en", sess.lang), Kboards.main_menu_kb(sess.lang)) return # --- формируем клавиатуру со списком историй --- kb = InlineKeyboardMarkup(row_width=1) for h in histories_for_user: title = h.get("history_name") or f"history_{h.get('history_id')}" kb.add(InlineKeyboardButton(title, callback_data=f"loadhist:{h.get('history_id')}")) kb.add(InlineKeyboardButton(translate_text("Back", "en", sess.lang), callback_data="back_to_main")) await send_inline(chat_id, translate_text("Choose history to load:", "en", sess.lang), kb) @dp.callback_query_handler(lambda c: c.data and c.data.startswith('loadhist:')) async def cb_loadhist(cq: types.CallbackQuery): debug_print(logger) """ Обработчик загрузки выбранной истории из базы данных. Восстанавливает все параметры: память, инвентарь, статы, локацию, время. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) hist_id = cq.data.split(':', 1)[1] recs = await to_thread(Database.find, "histories", "history_id", hist_id) history_record = recs[0] if recs else None if not history_record: await bot.send_message(chat_id, translate_text("History not found", "en", sess.lang)) return sess.history_id = history_record["history_id"] sess.system_prompt = history_record.get("system_prompt", "") # --- загружаем историю сообщений --- memories = await to_thread(Database.take_list, "messages", {"history_id": sess.history_id}, "memory") parsed = [] if memories: last_snapshot = memories[-1] try: parsed = json.loads(last_snapshot) except Exception as e: debug_print(logger, f"Error parsing messages memory JSON: {e}") parsed = [] # --- распределяем сообщения по уровням памяти --- total = len(parsed) s = min(SHORT_SIZE, total) rem = total - s m = min(MIDDLE_SIZE, rem) l = min(LONG_SIZE, max(0, rem - m)) # --- безопасно восстановим короткую память (она сама при необходимости протолкнёт лишние пары в middle) --- try: sess.short_m.restore_messages(parsed[0:s]) except Exception as e: debug_print(logger, f"[LOAD_HISTORY] short_m.restore_messages failed: {e}") # fallback — прямое присвоение только в крайнем случае sess.short_m.messages = parsed[0:s].copy() # --- безопасно восстановим среднюю память (она сама при необходимости протолкнёт лишние пары в long) --- try: sess.middle_m.restore_messages(parsed[s:s + m]) except Exception as e: debug_print(logger, f"[LOAD_HISTORY] middle_m.restore_messages failed: {e}") sess.middle_m.messages = parsed[s:s + m].copy() # --- безопасно восстановим долгую память (она сама усекает лишнее по лимиту) --- try: sess.long_m.restore_messages(parsed[s + m:s + m + l]) except Exception as e: debug_print(logger, f"[LOAD_HISTORY] long_m.restore_messages failed: {e}") sess.long_m.messages = parsed[s + m:s + m + l].copy() # --- инициализация infinity memory --- sess.init_infinity(sess.history_id) original_facts_str = history_record.get("original_facts", "[]") try: original_facts_list = json.loads(original_facts_str) if isinstance(original_facts_str, str) else original_facts_str if not isinstance(original_facts_list, list): original_facts_list = [] except Exception: original_facts_list = [] sess.infinity_m.fact_storage = original_facts_list # --- ЗАГРУЗКА ИНВЕНТАРЯ из БД --- inv_raw = history_record.get("current_inventory", "[]") or "[]" try: sess.current_inventory = json.loads(inv_raw) if isinstance(inv_raw, str) else inv_raw if not isinstance(sess.current_inventory, list): sess.current_inventory = [] except Exception as e: debug_print(logger, f"Ошибка при разборе current_inventory: {e}") sess.current_inventory = [] sess.current_weight, sess.current_slots = Inventory.recalc_inventory_stats(sess.current_inventory) # --- ход, время, статы, локация --- try: stored_turn = history_record.get("current_turn") sess.infinity_m.turn_counter = int(stored_turn) if stored_turn is not None else 0 except Exception: sess.infinity_m.turn_counter = 0 sess.current_time = history_record.get("current_time_text", "") sess.current_stats = history_record.get("current_stats", "") debug_print(logger, f"current stats:{sess.current_stats}") # --- оригинальный диалог --- orig_json = history_record.get("original_dialog") if orig_json: try: full = json.loads(orig_json) N = 50 sess.original_dialog = full[-N:] if len(full) > N else full except Exception: sess.original_dialog = await to_thread(Database.take_last_n_original_dialog, sess.history_id, 50) else: sess.original_dialog = await to_thread(Database.take_last_n_original_dialog, sess.history_id, 50) # --- текущая локация --- short_and_middle_history = sess.short_m.get_memory() + sess.middle_m.get_memory() current_location = Text_formatting.parce_location_data(sess.original_dialog) current_location = await to_thread(AI_request.short_current_location, current_location, sess.lang, short_and_middle_history) sess.current_location = current_location sess.previous_location = current_location debug_print(logger, f"current location:{sess.current_location}") # --- сохраняем локацию в БД --- await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_location", sess.current_location) # --- визуальная часть --- await bot.send_message(chat_id, translate_text("Loaded history. Entering dialogue mode.", "en", sess.lang)) try: chunks = Text_formatting.format_dialog_preview(sess.original_dialog, cut_len=400) for chunk in chunks: await bot.send_message(chat_id, chunk, parse_mode="HTML") except Exception: await bot.send_message(chat_id, "--- dialog preview ---") sess.in_dialog = True sess.dialog_shown = True await bot.send_message( chat_id, translate_text("You are now in dialog mode. Use the keyboard to send messages.", "en", sess.lang), reply_markup=Kboards.exit_reply_kb(sess.lang) ) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) @dp.callback_query_handler(lambda c: c.data == 'make_new_history') async def cb_make_new_history(cq: types.CallbackQuery): debug_print(logger) """ Обработчик начала создания новой истории - запрашивает у пользователя имя истории. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) if not sess: await bot.answer_callback_query(cq.id, "Session not found enter: /start") return try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) sess.temp_step = None await bot.send_message(chat_id, translate_text("Enter name of your new adventure", "en", sess.lang)) @dp.message_handler(lambda m: m.chat.id in sessions and sessions[m.chat.id].in_dialog is False and sessions[m.chat.id].username is not None) async def create_history_steps(message: types.Message): debug_print(logger) """ Многошаговый обработчик создания новой истории. Последовательно собирает: имя -> мир -> персонаж -> уровень цензуры -> стартовая локация -> время -> стартовое событие. После сбора всех данных создает историю, генерирует первый ответ AI и переключает в режим диалога. :param message: объект входящего сообщения :return: ничего не возвращает """ chat_id = message.chat.id sess = sessions[chat_id] text = message.text.strip() if not hasattr(sess, 'temp_step'): return temp = getattr(sess, 'temp_step', None) # --- Шаг 1: Имя истории --- if temp is None: sess.temp_name_of_story = text sess.temp_step = 'world' mes = translate_text("Describe the world's setting.", "en", sess.lang) + "\n" + \ translate_text("For example: Medieval age or the distant future or steampunk or " "the zombie apocalypse or whatever you want", "en", sess.lang) await message.reply(mes) return # --- Шаг 2: Мир --- if temp == 'world': sess.temp_world = text sess.temp_step = 'character' mes = translate_text("Describe your character.", "en", sess.lang) + "\n" + \ translate_text("For example: Bill the mighty knight or Isabella queen of aragon or whoever you want", "en", sess.lang) await message.reply(mes) return # --- Шаг 3: Персонаж --- if temp == 'character': sess.temp_character_params = text sess.temp_step = 'cens' kb = InlineKeyboardMarkup(row_width=3) kb.add(InlineKeyboardButton("C0", callback_data="cens_C0"), InlineKeyboardButton("C1", callback_data="cens_C1"), InlineKeyboardButton("C2", callback_data="cens_C2")) kb.add(InlineKeyboardButton("C3", callback_data="cens_C3"), InlineKeyboardButton("C4", callback_data="cens_C4")) await message.reply( "<C0> " + translate_text("no descriptions of intimacy or violence (even hints).", "en", sess.lang) + "\n" + "<C1> ;" + translate_text("violence abstract, no blood or pain; intimacy forbidden.", "en", sess.lang) + "\n" + "<C2> " + translate_text("intimacy and violence allowed in general terms, no details.", "en", sess.lang) + "\n" + "<C3> " + translate_text("intimacy in detail, violence abstract, no cruelty.", "en", sess.lang) + "\n" + "<C4> " + translate_text("Uncensored", "en", sess.lang) ) await message.reply(translate_text("Enter censured level (C0 C1 C2 C3 C4)", "en", sess.lang), reply_markup=kb) return # --- Шаг 4: Стартовая локация --- if temp == 'start_loc': sess.temp_starting_location = text sess.temp_step = 'current_time' mes = translate_text( "Enter the time when your adventure will start in the format: ", "en", sess.lang) + \ "\nYYYY:MM:DD:HH:MM (2025:09:23:14:30)\n"\ + translate_text("or enter 1 and the time will be determined automatically.", "en", sess.lang) await message.reply(mes) return # --- Шаг 5: Время --- if temp == 'current_time': if text.strip() == "1": # автоматическая генерация времени sess.current_time = AI_request.default_start_time( sess.temp_world, sess.temp_character_params, sess.temp_starting_location, getattr(sess, "temp_launch_event", "") ) sess.temp_step = 'launch_event' mes = translate_text("Describe the first event in your adventure.", "en", sess.lang) + "\n" + \ translate_text("For example: Bill gets out of bed or Isabella orders an attack on the neighboring kingdom " "or My martian colony is rebelling against the rule of Terra", "en", sess.lang) await message.reply(mes) else: # валидация введенного времени text = text.strip() parts = text.split(":") if len(parts) != 5: await message.reply( translate_text( "Invalid time format. Please use YEAR:MONTH:DAY:HOUR:MINUTE (e.g. 2025:09:23:14:30).", "en", sess.lang ) ) sess.temp_step = 'current_time' return try: year = int(parts[0]) # может быть отрицательным month = int(parts[1]) day = int(parts[2]) hour = int(parts[3]) minute = int(parts[4]) # Простая проверка диапазонов if not (1 <= month <= 12 and 1 <= day <= 31 and 0 <= hour <= 23 and 0 <= minute <= 59): raise ValueError("Out of range") sess.current_time = text sess.temp_step = 'launch_event' await message.reply(translate_text("Enter your launch event:", "en", sess.lang)) except ValueError: sess.temp_step = 'current_time' await message.reply( translate_text( "Invalid time format. Please use YEAR:MONTH:DAY:HOUR:MINUTE (e.g. 2025:09:23:14:30). " "Negative years are allowed for BC.", "en", sess.lang ) ) return # --- Шаг 6: Стартовое событие и создание истории --- if temp == 'launch_event': sess.temp_launch_event = text world = sess.temp_world character_params = sess.temp_character_params cens_tag = sess.temp_cens_tag name_of_story = sess.temp_name_of_story starting_location = sess.temp_starting_location launch_event = sess.temp_launch_event # --- Сокращаем длинные тексты --- if len(world) > 700: world = await to_thread(Memory.abstractive_summary, world, Memory.percent_summary(len(world)), sess.lang) if len(character_params) > 700: character_params = await to_thread(Memory.abstractive_summary, character_params, Memory.percent_summary(len(character_params)), sess.lang) # --- Формируем промпт --- parameters = await to_thread(Prompt_editor.get_parameters, world, character_params, cens_tag, f"{sess.lang}") system_prompt = await to_thread(Prompt_editor.edit_story_prompt, parameters, "Text/DNDEN1.txt") sess.system_prompt = system_prompt # --- Первый запрос к AI --- first_ask = f"Begin the narrative from the character's starting location and Launch event: \n" \ f"Starting location: {starting_location}\n" \ f"Launch event: {launch_event}\n" \ f"Enter your answer on the language: {sess.lang}" first_answer = await ai_call_with_proc(sess, chat_id, AI_request.send_to_AI, first_ask, system_prompt) continue_flag = Text_formatting.find_end_tag(first_answer, "ANSWER") # --- Если ответ неполный, запрашиваем продолжение --- if continue_flag == False: await message.reply( Translate_text.translate_text("For some reason, your question seems very complicated...", "en", sess.lang)) ask_clean = Text_formatting.destroy_text_between_tag(first_ask, "SPECIAL") first_answer = await ai_call_with_proc(sess, chat_id, AI_request.continue_answer, ask_clean, first_answer, sess.short_m.get_memory() + sess.middle_m.get_memory(), system_prompt) if first_answer == -1: first_answer = await ai_call_with_proc(sess, chat_id, AI_request.send_to_AI, first_ask, system_prompt) # Очищаем память для новой истории sess.short_m.delete_all_memory() sess.middle_m.delete_all_memory() sess.long_m.delete_all_memory() # --- Добавляем в память --- clean_first_answer = Text_formatting.kill_all_tags(first_answer) sess.original_dialog = [{'role': 'user', 'content': ""}, {'role': 'assistant', 'content': clean_first_answer}] sess.short_m.add_memory(translate_text("first message", "en", sess.lang), first_answer) first_answer_clean = Text_formatting.kill_all_tags(first_answer) first_answer_short = first_answer_clean #sess.infinity_m.take_fact(first_ask, first_answer, sess.lang, sess.system_prompt) TODO # --- Создаем сессию, если нужно --- if not sess.session_uuid: sess.session_uuid = str(uuid.uuid4()) await to_thread(Database.make_new_user, { "user_id": sess.user_id or str(uuid.uuid4()), "mail": None, "age": None, "session_id": sess.session_uuid, "username": sess.username, "language": sess.lang }) # --- Создаем запись истории в БД --- history_id = await to_thread(Database.add_history, sess.user_id, sess.session_uuid, name_of_story, system_prompt, starting_location, launch_event) sess.history_id = history_id sess.init_infinity(history_id) await to_thread( Database.give_value, "histories", {"history_id": sess.history_id}, "current_time_text", sess.current_time ) #sess.infinity_m.turn_counter = 0 #sess.infinity_m.fact_storage = [] # --- Сохраняем диалог --- await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "original_dialog", json.dumps(sess.original_dialog, ensure_ascii=False)) sess.current_location = starting_location await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_location", sess.current_location) debug_print(logger, f"curr_loc: {sess.current_location}") # --- Генерируем начальные статы --- sess.current_stats = str( AI_request.default_start_stats(world, character_params, starting_location, launch_event)) await to_thread( Database.give_value, "histories", {"history_id": sess.history_id}, "current_stats", sess.current_stats ) # --- Генерируем стартовый инвентарь --- sess.current_inventory = AI_request.start_inventory(world, sess.current_stats, starting_location, launch_event, sess.current_time, sess.temp_character_params, sess.lang) pair = Text_formatting.parse_weight_and_slots_new_objs(str(sess.current_inventory)) sess.current_weight = pair[0] sess.current_slots = pair[1] # --- Сохраняем инвентарь в БД --- try: await to_thread( Database.give_value, "histories", {"history_id": sess.history_id}, "current_inventory", json.dumps(sess.current_inventory, ensure_ascii=False) ) await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_weight", float(sess.current_weight)) await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_slots", int(sess.current_slots)) except Exception as e: debug_print(logger, f"Error{e}") # --- Сохраняем снимок памяти --- snapshot = sess.short_m.get_memory() + sess.middle_m.get_memory() + sess.long_m.get_memory() await to_thread(Database.give_list, "messages", {"history_id": sess.history_id}, "memory", [json.dumps(snapshot, ensure_ascii=False)]) # --- Переключаемся в режим диалога --- sess.in_dialog = True sess.dialog_shown = True await message.reply(first_answer_short) await message.reply(translate_text("Entering dialog mode. Write your character's actions in the chat.", "en", sess.lang), reply_markup=Kboards.exit_reply_kb(sess.lang)) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) # --- Удаляем временные атрибуты --- for attr in ['temp_step', 'temp_name_of_story', 'temp_world', 'temp_character_params', 'temp_cens_tag', 'temp_starting_location', 'temp_launch_event']: if hasattr(sess, attr): delattr(sess, attr) return return @dp.message_handler(lambda m: m.chat.id in sessions and sessions[m.chat.id].in_dialog) async def dialog_message_handler(message: types.Message): debug_print(logger) """ Основной обработчик сообщений в режиме диалога. Обрабатывает команды просмотра времени, статов, инвентаря, выброса предметов и выхода. Отправляет сообщения пользователя в AI, обрабатывает ответы, обновляет игровое состояние. Выполняет броски d20 для проверки успешности действий персонажа. Обновляет инвентарь, статы, время, локацию и сохраняет все изменения в БД. :param message: объект входящего сообщения :return: ничего не возвращает """ chat_id = message.chat.id sess = sessions[chat_id] # --- Показываем превью диалога при первом сообщении --- if sess.in_dialog and not getattr(sess, 'dialog_shown', False): try: chunks = Text_formatting.format_dialog_preview(sess.original_dialog, cut_len=400) for chunk in chunks: await bot.send_message(chat_id, chunk, parse_mode="HTML") except Exception: pass sess.dialog_shown = True text = message.text # --- Обработка команды просмотра времени --- if Text_formatting.find_tag(text, "TIME"): pretty = Game_time.pretty_time_format(sess.current_time, sess.lang) await message.reply(pretty) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) return # --- Обработка команды просмотра статов --- if Text_formatting.find_tag(text, "STATS"): pretty = Stats.pretty_stats(sess.current_stats, sess.lang) await message.reply(pretty) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) return # --- Обработка команды просмотра инвентаря --- if Text_formatting.find_tag(text, "INV"): pretty = Inventory.pretty_inv(sess.current_inventory, sess) await message.reply(pretty) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) return # --- Обработка команды выброса предмета --- if Text_formatting.find_tag(text, "DROP"): pretty = Inventory.pretty_inv(sess.current_inventory, sess) await message.reply( pretty + "\n\n" + translate_text("Write the item number to drop or 0 to cancel", "en", sess.lang)) sess.drop_mode = True return # --- Если юзер в режиме дропа --- if getattr(sess, "drop_mode", False): sess.drop_mode = False try: choice = int(text.strip()) except ValueError: await message.reply(translate_text("Invalid input", "en", sess.lang)) return if choice == 0: await message.reply(translate_text("Canceled.", "en", sess.lang)) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) return dropped = await Inventory.drop_from_inventory(sess, choice) if dropped: dropped_obj = f"{dropped.get('name')} x{dropped.get('count', 1)}" await message.reply( translate_text("You dropped: ", "en", sess.lang) + dropped_obj) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) # Формируем нейтральную запись для истории info = Inventory.add_information_about_user_drop_inv_in_hist(dropped_obj, sess.current_location) # Добавляем в short memory sess.short_m.add_memory( ask=f"'Drop item manually': {dropped_obj}", answer=info["content"] ) # Добавим в original_dialog sess.original_dialog.append({"role": "user", "content": f"<DO>Dropped {dropped_obj}</DO>"}) sess.original_dialog.append(info) else: await message.reply(translate_text("Invalid choice.", "en", sess.lang)) return # --- Брать предмет вручуню прописать его --- if Text_formatting.find_tag(text, "TAKEITEM"): mess = translate_text("Enter the items you want to add.", "en", sess.lang) + "\n" + \ translate_text("Input format: name, quantity, weight of one item, slots", "en", sess.lang) + "\n" + \ translate_text("Example: Long sword 1 5 1; Gold coins 5 0.1 0", "en", sess.lang) await message.reply(mess) sess.takeitem = True return if getattr(sess, "takeitem", False): sess.takeitem = False try: choice = text.strip() except ValueError: await message.reply(translate_text("Invalid input", "en", sess.lang)) return new_objects = Inventory.parse_user_items(choice) debug_print(logger, f"{new_objects}") for item in new_objects: sess.current_inventory = Inventory.stack_or_add(sess.current_inventory, item) # пересчёт характеристик sess.current_weight, sess.current_slots = Inventory.recalc_inventory_stats(sess.current_inventory) # --- сохраняем обновлённый инвентарь в БД --- try: Database.give_value( "histories", {"history_id": sess.history_id}, "current_inventory", json.dumps(sess.current_inventory, ensure_ascii=False) ) await message.reply(translate_text("Items added successfully.", "en", sess.lang)) except Exception as e: debug_print(logger, "error in save") await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) return # --- Поменять статы вручную --- if Text_formatting.find_tag(text, "CHANGESTATS"): pretty = Stats.pretty_stats_with_numbers(sess.current_stats, sess.lang) await message.reply(pretty) await message.reply(translate_text("Select the parameter number that you want to change or 0 to cancel", "en", sess.lang)) sess.change_stats_mode = True return # --- Изменение стат персонажа вручную --- if getattr(sess, "change_stats_mode", False): sess.change_stats_mode = False try: choice = int(text.strip()) except ValueError: await message.reply(translate_text("Invalid input", "en", sess.lang)) return if choice == 0: await message.reply(translate_text("Canceled.", "en", sess.lang)) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) return sess.stat_change_number = choice pretty = Stats.pretty_stats_with_numbers(sess.current_stats, sess.lang) name = Stats.get_name_of_stats_from_number(pretty, choice) if name == "-1": await message.reply(translate_text("Invalid input", "en", sess.lang)) return # Ждём ввод нового значения await message.reply(translate_text("Enter new value of: ", "en", sess.lang) + " " + str(name)) sess.change_stats_mode3 = True return # --- Второй шаг: получение нового значения --- if getattr(sess, "change_stats_mode3", False): sess.change_stats_mode3 = False try: choice = int(text.strip()) except ValueError: await message.reply(translate_text("Invalid input", "en", sess.lang)) return Stats.change_stats_from_number(sess, sess.stat_change_number, choice) try: from Database.Database import give_value Database.give_value( "histories", {"history_id": sess.history_id}, "current_stats", sess.current_stats ) except Exception as e: debug_print(logger, f"Ошибка при сохранении в базу: {e}") await message.reply(translate_text("The attribute has been changed", "en", sess.lang)) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) return # --- Если ввел перезапуск (/start) --- if text.strip() == "/start": await message.reply( translate_text("Exiting dialog mode...", "en", sess.lang), reply_markup=types.ReplyKeyboardRemove() ) # Выход из режима диалога sess.in_dialog = False sess.dialog_shown = False # Сброс временных атрибутов создания истории for attr in ["temp_step", "temp_name_of_story", "temp_world", "temp_character_params", "temp_cens_tag", "temp_starting_location", "temp_launch_event", "take_item", "drop_mode", "change_stats_mode"]: if hasattr(sess, attr): delattr(sess, attr) # Показать главное меню await send_inline( chat_id, translate_text("Main menu", "en", sess.lang), Kboards.main_menu_kb(sess.lang) ) return # --- Обработка команды выхода --- if text.strip().lower() == translate_text("Exit", "en", sess.lang).lower(): # сохраняем все данные перед выходом fact_for_database = json.dumps(sess.infinity_m.get_all_facts_as_list(), ensure_ascii=False) await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "original_facts", fact_for_database) await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "system_prompt", sess.system_prompt) try: await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_turn", int(sess.infinity_m.turn_counter)) except Exception as e: debug_print(logger, "error") sess.in_dialog = False sess.dialog_shown = False await message.reply(translate_text("Goodbye", "en", sess.lang), reply_markup=Kboards.hide_reply_kb()) if sess.last_inline_message_id: try: await safe_delete_message(chat_id, sess.last_inline_message_id) except Exception: pass await send_inline(chat_id, translate_text("Main menu", "en", sess.lang), Kboards.main_menu_kb(sess.lang)) return # --- Формируем запрос к AI --- if sess.current_location: ask = text + f"<SPECIAL>Current location: {sess.current_location}, " \ f"BUT DON'T FORGET THE LOCATION DESCRIPTION ONLY WHEN CHANGING THE SCENE." else: ask = text + f"<SPECIAL>Current location: {sess.previous_location}, " \ f"BUT DON'T FORGET THE LOCATION DESCRIPTION ONLY WHEN CHANGING THE SCENE." if sess.current_time: ask += ( "Consider the current time as contextual background, without dwelling on it. " f"Time format: YEAR:MONTH:DAY:HOUR:MINUTE.\nCurrent time: {sess.current_time}.") # --- Собираем историю для контекста --- history = [AI_request.resumator(sess.short_m.get_memory(), sess.middle_m.get_memory(), sess.long_m.get_memory(), sess.lang)] debug_print(logger, f"resum: {history}") if sess.current_inventory: inv_text = json.dumps(sess.current_inventory, ensure_ascii=False) history.append({"role": "system", "content": f"<INVENTORY>{Text_formatting.sanitize_inventory_for_history(inv_text)}</INVENTORY>"}) # --- Проверяем, нужен ли бросок кубика --- if Text_formatting.find_tag(ask, "STORY"): # действия без бросков hard_action = 0 ask += "</SPECIAL>" result = -1 outcome = "NONE" else: # --- Получаем строку сложности и бонусов --- comp_str = AI_request.complexity_of_action(sess.current_stats, history, ask) # --- Парсим три значения --- m = re.search(r"\b(\d{1,2})\s+([012])\s+([+\-]?\d+)\b", str(comp_str)) if not m: base_dc, roll_marker, stat_bonus = 10, 0, 0 # fallback если ответ не в нужном формате else: base_dc = int(m.group(1)) # сложность (DC) roll_marker = int(m.group(2)) # 0 / 1 / 2 stat_bonus = int(m.group(3)) # бонус # --- Обработка простых случаев --- if base_dc <= 5: hard_action = 0 ask += "</SPECIAL>" result = -1 outcome = "NONE" await message.reply(Translate_text.translate_text("No checks are required", "en", sess.lang)) else: # --- Делаем броски --- roll1 = secrets.randbelow(20) + 1 roll2 = secrets.randbelow(20) + 1 if roll_marker in (1, 2) else None # выбираем бросок с учётом преимущества/помехи if roll_marker == 1: # преимущество chosen = max(roll1, roll2) text_ = Translate_text.translate_text("with advantage", "en", "ru") roll_type_text = f"Roll {text_}: max({roll1}, {roll2})" elif roll_marker == 2: # помеха chosen = min(roll1, roll2) text_ = Translate_text.translate_text("with disadvantage", "en", "ru") roll_type_text = f"Roll {text_}: min({roll1}, {roll2})" else: # обычный бросок chosen = roll1 roll_type_text = f"Roll: {roll1}" # --- Применяем бонус и определяем исход --- total = chosen + stat_bonus if total >= base_dc: if chosen == 20: outcome = "critical success" elif chosen == 1: outcome = "critical failure" else: outcome = "success" else: if chosen == 1: outcome = "critical failure" else: outcome = "failure" # --- Красивый вывод пользователю --- res_ = Translate_text.translate_text("Result", "en", "ru") dif_ = Translate_text.translate_text("Complexity", "en", "ru") if outcome == "failure" or outcome == "critical failure": msg_text = ( f"🎲 {roll_type_text} → {chosen} {stat_bonus:+d} = {total}\n" f"🧩 {dif_}: {base_dc}\n" f"❌ {res_}: {outcome.capitalize()}" ) else: msg_text = ( f"🎲 {roll_type_text} → {chosen} {stat_bonus:+d} = {total}\n" f"🧩 {dif_}: {base_dc}\n" f"✅ {res_}: {outcome.capitalize()}" ) await message.reply(msg_text) # --- Добавляем в запрос для модели --- ask += ( f"D20 Roll: {chosen}; Complexity: {base_dc}; Result: {outcome}. " f"Apply this strictly when describing the outcome of an action." "</SPECIAL>" ) debug_print(logger, f"short:\n {sess.short_m.get_memory()}\n middle:\n {sess.middle_m.get_memory()}\n long:\n {sess.long_m.get_memory()}\n") # --- Сохраняем факты в БД --- fact_for_database = json.dumps(sess.infinity_m.get_all_facts_as_list(), ensure_ascii=False) await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "original_facts", fact_for_database) # --- Получаем факты из бесконечной памяти --- current_inf_mem = sess.infinity_m.get_facts_for_prompt(INFINITY_SIZE + 1) final_prompt = sess.system_prompt + "\n[FACTS]:\n" + str(current_inf_mem) debug_print(logger, f"current inf mem: {sess.infinity_m.fact_storage}") # --- Отправляем запрос к AI --- answer = await ai_call_with_proc(sess, chat_id, AI_request.send_to_AI, ask, final_prompt, history) # --- Проверяем, завершен ли ответ --- continue_flag = Text_formatting.find_end_tag(answer, "ANSWER") if continue_flag == False: await message.reply( Translate_text.translate_text("For some reason, your question seems very complicated...", "en", sess.lang)) ask_clean = Text_formatting.destroy_text_between_tag(ask, "SPECIAL") answer = await ai_call_with_proc(sess, chat_id, AI_request.continue_answer, ask_clean, answer, sess.short_m.get_memory() + sess.middle_m.get_memory(), sess.system_prompt) if answer == -1: answer = await ai_call_with_proc(sess, chat_id, AI_request.send_to_AI, ask, sess.system_prompt, history) # --- Проверяем флаг смерти --- dead_flag = False if Text_formatting.find_tag(answer, "RESTART"): dead_flag = True # --- Очищаем теги из запроса --- ask_no_tags = Text_formatting.destroy_text_between_tag(ask, "SPECIAL") ask_no_tags = str(ask_no_tags).replace(BOT_TAG, "") ask_no_tags = str(ask_no_tags).replace(BOT_TAG + " ", "") ask_no_tags = str(ask_no_tags).replace(" " + BOT_TAG + " ", "") clear_ans_for_mem = Text_formatting.kill_all_tags(answer) # --- Добавляем в память --- sess.short_m.add_memory(ask_no_tags, clear_ans_for_mem) # --- Добавляем в original_dialog --- ask_text = {'role': 'user', 'content': ask_no_tags} answer_text = {'role': 'assistant', 'content': answer} sess.original_dialog.append(ask_text) sess.original_dialog.append(answer_text) await to_thread(Database.append_to_original_dialog, sess.history_id, [ask_text, answer_text]) # --- Сохраняем снимок памяти --- try: snapshot = sess.short_m.get_memory() + sess.middle_m.get_memory() + sess.long_m.get_memory() await to_thread(Database.give_list, "messages", {"history_id": sess.history_id}, "memory", [json.dumps(snapshot, ensure_ascii=False)]) except Exception as e: debug_print(logger, "snapshot error") answer_clean = Text_formatting.kill_all_tags(answer) answer_short = answer_clean await message.reply(answer_short) await Kboards.send_all_toolbars(sess.chat_id, sess.lang, bot, sessions) # --- Обновляем время --- time_for_action = AI_request.time_for_action(ask_no_tags, history) sess.current_time = Game_time.sum_time(sess.current_time, time_for_action) await to_thread( Database.give_value, "histories", {"history_id": sess.history_id}, "current_time_text", sess.current_time) debug_print(logger, f"Time for action {time_for_action}") # --- Получаем новые предметы --- new_objects = AI_request.take_inventory(ask, answer_short, str(sess.current_inventory), sess.lang) debug_print(logger, f"New obj INVENTORY: {new_objects}") pair_max = Text_formatting.parse_max_weight_and_slots(sess.current_stats.strip("`json\n ")) max_weight = pair_max[0] max_slots = pair_max[1] pair_new = Text_formatting.parse_weight_and_slots_new_objs(str(new_objects)) add_weight = pair_new[0] add_slots = pair_new[1] debug_print(logger, f"Add weight and slots: {add_weight, add_slots}") # --- Добавляем предметы в инвентарь --- if sess.current_weight + add_weight > max_weight or sess.current_slots + add_slots > max_slots: await Inventory.try_add_to_inventory(sess, new_objects, message) else: # корректно вызвать функцию добавления и сохранить результат sess.current_inventory = Text_formatting.add_new_obj_to_inv(sess.current_inventory, str(new_objects)) sess.current_weight += add_weight sess.current_slots += add_slots try: parsed_obj = json.loads(str(new_objects).replace("'", '"')) except Exception: parsed_obj = new_objects # fallback: если это не JSON, оставляем как есть if parsed_obj and parsed_obj != [] and str(parsed_obj).strip() not in ("[]", "", "None"): text_tr = Translate_text.translate_text("Added to inventory", "en", sess.lang) # нормализуем список objs = parsed_obj if isinstance(parsed_obj, list) else [parsed_obj] # удаляем id и формируем красивый текст pretty_items = [] for o in objs: if isinstance(o, dict): o = {k: v for k, v in o.items() if k != "id"} # убираем id name = o.get("name", "???") count = o.get("count", 1) desc = o.get("desc", "") pretty_items.append(f"<b>{name}</b> x{count}" + (f" — {desc}" if desc else "")) else: pretty_items.append(str(o)) items_str = "\n".join(pretty_items) await message.reply(f"👜 {text_tr}:\n{items_str}") # --- стекуем предметы --- new_list = Inventory.normalize_new_objects(new_objects) for obj in new_list: sess.current_inventory = Inventory.stack_or_add(sess.current_inventory, obj) sess.current_weight, sess.current_slots = Inventory.recalc_inventory_stats(sess.current_inventory) # --- обновляем факт в бесконечной памяти --- if sess.infinity_m: sess.infinity_m.update_inventory_fact(sess) sess.infinity_m.count_hit(ask_no_tags, answer) sess.infinity_m.take_fact(ask_no_tags, answer_clean, sess.lang, sess.system_prompt) try: sess.infinity_m.cleanup_expired_facts( last_ask=ask, last_answer=answer, full_history=history, prompt=sess.system_prompt ) except Exception as e: debug_print(logger, f"cleanup_expired_facts failed: {e}") # --- Обновляем локацию --- if time_for_action > 7 * 24 * 60: current_location = "" else: current_location = Text_formatting.parce_location_data(sess.original_dialog) if current_location == "" or current_location == sess.previous_location: current_location = sess.previous_location else: short_and_middle_history = sess.short_m.get_memory() + sess.middle_m.get_memory() current_location = await to_thread(AI_request.short_current_location, current_location, sess.lang, short_and_middle_history) sess.previous_location = current_location await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_location", current_location) sess.current_location = current_location debug_print(logger, f"curr_loc: {sess.current_location}") # --- Обновляем статы --- old_stats = sess.current_stats sess.current_stats = str(AI_request.change_stats(sess.current_stats, history, answer_clean)) stat_changes = Stats.change_stats(old_stats, sess.current_stats, sess.lang) if stat_changes != "-1": await message.reply(stat_changes) # сохраняем в БД await to_thread( Database.give_value, "histories", {"history_id": sess.history_id}, "current_stats", sess.current_stats ) # --- Обрабатываем выброс предметов --- drop_obj = AI_request.drop_some_objects(str(sess.current_inventory), ask_no_tags, answer_clean) try: parsed_obj = json.loads(str(drop_obj).replace("'", '"')) except Exception: parsed_obj = drop_obj # fallback: если это не JSON, оставляем как есть if parsed_obj and parsed_obj != [] and str(parsed_obj).strip() not in ("[]", "", "None"): drop_text = Translate_text.translate_text("Discarded items", "en", sess.lang) # --- нормализуем список --- objs = parsed_obj if isinstance(parsed_obj, list) else [parsed_obj] # --- удаляем id и формируем красивый текст --- pretty_items = [] for o in objs: if isinstance(o, dict): o = {k: v for k, v in o.items() if k != "id"} # убираем id name = o.get("name", "???") count = o.get("count", 1) desc = o.get("desc", "") pretty_items.append(f"<b>{name}</b> x{count}" + (f" — {desc}" if desc else "")) else: pretty_items.append(str(o)) items_str = "\n".join(pretty_items) await message.reply(f"{drop_text}:\n{items_str}") # --- Удаляем выброшенные предметы --- sess.current_inventory = Inventory.remove_items_by_name(sess.current_inventory, drop_obj) sess.current_weight, sess.current_slots = Inventory.recalc_inventory_stats(sess.current_inventory) # --- сохраняем текущее состояние инвентаря --- await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_inventory", json.dumps(sess.current_inventory, ensure_ascii=False) ) await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_weight", sess.current_weight ) await to_thread(Database.give_value, "histories", {"history_id": sess.history_id}, "current_slots", sess.current_slots ) # --- Проверка смерти персонажа --- if dead_flag or Stats.check_hp(sess.current_stats): await message.reply(translate_text("You dead", "en", sess.lang)) sess.in_dialog = False sess.dialog_shown = False await message.reply(translate_text("Goodbye", "en", sess.lang), reply_markup = Kboards.main_menu_kb(sess.lang)) return @dp.message_handler(commands=['start']) async def cmd_start(message: types.Message): debug_print(logger, "start lol") """ Обработчик команды /start - создает новую сессию и предлагает выбрать язык. :param message: объект входящего сообщения :return: ничего не возвращает """ chat_id = message.chat.id sessions[chat_id] = UserSession(chat_id) await bot.send_message(chat_id, "Welcome Choose language", reply_markup=Kboards.language_kb()) @dp.callback_query_handler(lambda c: c.data and c.data.startswith('cens_')) async def cb_cens(cq: types.CallbackQuery): debug_print(logger) """ Обработчик выбора уровня цензуры при создании истории. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) if not sess: return sess.temp_cens_tag = '<' + cq.data.split('_', 1)[1] + '>' sess.temp_step = 'start_loc' mes = translate_text("Enter your start location:", "en", sess.lang) + "\n" +\ translate_text("For example: Bill's Castle or a colony on Mars or Zombie Shelter in downtown New York", "en", sess.lang) await bot.send_message(chat_id, mes) @dp.callback_query_handler(lambda c: c.data and c.data.startswith('lang_')) async def callback_lang(cq: types.CallbackQuery): debug_print(logger) """ Обработчик выбора языка интерфейса. Создает пользователя в БД, если его нет, или загружает существующего. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.setdefault(chat_id, UserSession(chat_id)) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass lang = str(cq.data).replace("lang_", "") sess.lang = lang # Сохраним старые сообщения, чтобы не потерять их try: old_mid_msgs = sess.middle_m.get_memory() except Exception: old_mid_msgs = [] # Пересоздаём среднюю память с новым языком sess.middle_m = Memory.Middle_memory(MIDDLE_SIZE, sess.long_m, lang) # Если были старые сообщения — восстановим их (метод restore_messages у вас есть) try: if old_mid_msgs: sess.middle_m.restore_messages(old_mid_msgs) except Exception: # если что-то пошло не так — просто продолжим, но не роняем программу pass # Обновим указатель в короткой памяти, чтобы short_m отправляла overflow в новую middle_m sess.short_m.middle_memory_pointer = sess.middle_m await bot.answer_callback_query(cq.id) # Проверяем в БД по user_telegram_id user_id_from_db = await to_thread(Database.take_value, "mainDB", {"user_telegram_id": chat_id}, "user_id") session_uuid = await to_thread(Database.take_value, "mainDB", {"user_telegram_id": chat_id}, "session_id") if not user_id_from_db: # --- создаем нового пользователя --- new_user_id = str(uuid.uuid4()) new_session_id = str(uuid.uuid4()) await to_thread(Database.make_new_user, { "user_id": new_user_id, "user_telegram_id": chat_id, "mail": None, "age": None, "session_id": new_session_id, "username": cq.from_user.username or f"user_{chat_id}", "language": sess.lang }) sess.user_id = new_user_id sess.session_uuid = new_session_id sess.username = cq.from_user.username or f"user_{chat_id}" else: # --- загружаем существующего пользователя --- sess.user_id = user_id_from_db sess.username = await to_thread(Database.take_value, "mainDB", {"user_id": sess.user_id}, "username") if not session_uuid: new_session_id = str(uuid.uuid4()) await to_thread(Database.give_value, "mainDB", {"user_id": sess.user_id}, "session_id", new_session_id) sess.session_uuid = new_session_id else: sess.session_uuid = session_uuid if sess.last_inline_message_id: try: await safe_delete_message(chat_id, sess.last_inline_message_id) except Exception: pass await send_inline(chat_id, translate_text("Main menu", "en", sess.lang), Kboards.main_menu_kb(sess.lang)) @dp.callback_query_handler(lambda c: c.data == 'back_to_main') async def back_to_main_cb(cq: types.CallbackQuery): debug_print(logger) """ Обработчик возврата в главное меню. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) if not sess: await bot.answer_callback_query(cq.id, "Session not found enter: /start") return try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) await send_inline(chat_id, translate_text("Main menu", "en", sess.lang), Kboards.main_menu_kb(sess.lang)) @dp.callback_query_handler(lambda c: c.data == 'profile') async def cb_profile(cq: types.CallbackQuery): debug_print(logger) """ Обработчик открытия меню профиля пользователя. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) if not sess: await bot.answer_callback_query(cq.id, "Session not found enter: /start") return try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) await send_inline(chat_id, translate_text("Profile menu", "en", sess.lang), Kboards.account_kb(sess.lang)) @dp.callback_query_handler(lambda c: c.data == 'delete_history') async def cb_delete_history(cq: types.CallbackQuery): debug_print(logger) """ Обработчик отображения списка историй для удаления. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) histories_for_user = await to_thread(Database.find, "histories", "session_id", sess.session_uuid) if not histories_for_user: await bot.send_message(chat_id, translate_text("You have no saved histories yet.", "en", sess.lang)) await send_inline(chat_id, translate_text("Profile menu", "en", sess.lang), Kboards.account_kb(sess.lang)) return # --- формируем список историй для удаления --- kb = InlineKeyboardMarkup(row_width=1) for h in histories_for_user: kb.add(InlineKeyboardButton(h.get('history_name'), callback_data=f'delhist:{h.get("history_id")}')) kb.add(InlineKeyboardButton(translate_text("Back", "en", sess.lang), callback_data='back_to_main')) await send_inline(chat_id, translate_text("Choose history to delete:", "en", sess.lang), kb) @dp.callback_query_handler(lambda c: c.data and c.data.startswith('delhist:')) async def cb_delhist(cq: types.CallbackQuery): debug_print(logger) """ Обработчик удаления выбранной истории после проверки принадлежности сессии. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) hist_id = cq.data.split(':', 1)[1] recs = await to_thread(Database.find, "histories", "history_id", hist_id) if not recs: await bot.send_message(chat_id, translate_text("History not found", "en", sess.lang)) return rec = recs[0] # --- Проверка, принадлежит ли история текущей сессии --- if str(rec.get('session_id')) != str(sess.session_uuid): await bot.send_message(chat_id, translate_text("History not found for your session.", "en", sess.lang)) return await to_thread(Database.delete_history, hist_id) await bot.send_message(chat_id, translate_text("Deleted history", "en", sess.lang) + f": {rec.get('history_name')}") await send_inline(chat_id, translate_text("Main menu", "en", sess.lang), Kboards.main_menu_kb(sess.lang)) @dp.callback_query_handler(lambda c: c.data == 'change_language') async def cb_change_language(cq: types.CallbackQuery): debug_print(logger) """ Обработчик открытия меню смены языка интерфейса. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) await send_inline(chat_id, "Choose language", Kboards.language_kb()) @dp.callback_query_handler(lambda c: c.data == 'delete_acc') async def cb_delete_acc(cq: types.CallbackQuery): debug_print(logger) """ Обработчик запроса на удаление аккаунта - запрашивает подтверждение. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) if not sess: await bot.send_message(chat_id, "Session not found. Use /start") return kb = InlineKeyboardMarkup(row_width=2) kb.add( InlineKeyboardButton(translate_text("Confirm delete", "en", sess.lang), callback_data="confirm_delete_acc"), InlineKeyboardButton(translate_text("Back", "en", sess.lang), callback_data="back_to_main") ) await bot.send_message(chat_id, translate_text("Are you sure you want to delete your account?", "en", sess.lang), reply_markup=kb) @dp.callback_query_handler(lambda c: c.data == 'confirm_delete_acc') async def confirm_delete_acc_cb(cq: types.CallbackQuery): debug_print(logger) """ Обработчик подтверждения удаления аккаунта - удаляет все данные пользователя из БД. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) if not sess or not sess.user_id: lang = sess.lang if sess else 'en' await bot.send_message(chat_id, translate_text("No account to delete.", "en", lang)) return try: await to_thread(Database.delete_user_data, sess.user_id) except Exception as e: debug_print(logger, "error") await bot.send_message(chat_id, translate_text("Failed to delete account. Try again later.", "en", sess.lang)) return try: del sessions[chat_id] except Exception: pass await bot.send_message(chat_id, translate_text("Account deleted.", "en", sess.lang)) await send_inline(chat_id, translate_text("Main menu", "en", sess.lang), Kboards.main_menu_kb(sess.lang)) @dp.message_handler() async def default_handler(message: types.Message): debug_print(logger) """ Обработчик по умолчанию для всех непойманных сообщений. :param message: объект входящего сообщения :return: ничего не возвращает """ chat_id = message.chat.id sess = sessions.get(chat_id) if not sess: await message.reply("Please /start first.") return await message.reply(translate_text("Use the main menu", "en", sess.lang), reply_markup = Kboards.main_menu_kb(sess.lang)) @dp.callback_query_handler(lambda c: c.data == 'game_guide') async def cb_important(cq: types.CallbackQuery): debug_print(logger) """ Обработчик открытия меню с игровым гайдом. :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) if not sess: await bot.answer_callback_query(cq.id, "Session not found enter: /start") return try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) kb = Kboards.information_kb(sess.lang) await send_inline(chat_id, translate_text("Game guide", "en", sess.lang), kb) @dp.callback_query_handler(lambda c: c.data in ('future_developments', 'feedback')) async def cb_imp_placeholder(cq: types.CallbackQuery): debug_print(logger) """ Обработчик заглушек для пунктов меню информации (в разработке). :param cq: объект callback-запроса :return: ничего не возвращает """ chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) await bot.send_message(chat_id, Translate_text.translate_text("In progress", "en", sess.lang)) kb = Kboards.information_kb(sess.lang) await send_inline(chat_id, translate_text("Game guide", "en", sess.lang), kb) @dp.callback_query_handler(lambda c: c.data in 'how_to_play') async def cb_imp_placeholder(cq: types.CallbackQuery): debug_print(logger) chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) mes = Text_formatting.take_game_guide(sess.lang) await bot.send_message(chat_id, mes, parse_mode=None) kb = Kboards.information_kb(sess.lang) await send_inline(chat_id, translate_text("Game guide", "en", sess.lang), kb) @dp.callback_query_handler(lambda c: c.data in 'usage_policy') async def cb_imp_placeholder(cq: types.CallbackQuery): debug_print(logger) chat_id = cq.message.chat.id sess = sessions.get(chat_id) try: await safe_delete_message(chat_id, cq.message.message_id) except Exception: pass await bot.answer_callback_query(cq.id) mes = Text_formatting.take_polit() await bot.send_message(chat_id, mes, parse_mode=None) kb = Kboards.information_kb(sess.lang) await send_inline(chat_id, translate_text("Game guide", "en", sess.lang), kb) if __name__ == '__main__': """ Точка входа в приложение - инициализирует БД и запускает бота в режиме polling. """ Database.init() logger.info("Bot started") executor.start_polling(dp, skip_updates=True)