Initial commit

This commit is contained in:
2026-04-30 18:38:38 +03:00
commit 8d71819caf
20 changed files with 2134 additions and 0 deletions

260
services/instructions.py Normal file
View File

@@ -0,0 +1,260 @@
from database.db import get_connection
from database.repository import (
get_terminal_steps,
set_instruction_progress,
get_instruction_progress,
clear_instruction_progress,
find_instructions,
)
from keyboards.factory import pause_keyboard, back_to_main, build_keyboard
from services.log_export import log_event
PAUSE_PROMPT = "Помогла ли эта часть?"
PAUSE_YES = {"да, помогло", "да помогло", "да"}
PAUSE_NO = {"нет, дальше", "нет дальше", "нет"}
# ─────────────────────────────────────────────
# PINPAD — простой ответ
# ─────────────────────────────────────────────
async def send_pinpad_error(message, code: str, keyboard=None):
"""
Отправляет простой ответ для ошибок пинпада:
причина + что делать
"""
with get_connection() as conn:
cur = conn.cursor()
cur.execute(
"SELECT reason, action FROM pinpad_errors WHERE code = ?",
(code,)
)
row = cur.fetchone()
if not row:
return False # сигнал, что ошибки нет
reason, action = row
log_event(message.from_id, "pinpad_error", str(code))
action_text = (action or "").strip()
if action_text.lower().startswith("terminal:"):
query = action_text.split(":", 1)[1].strip()
if query:
found = find_instructions(query.lower())
if len(found) == 1:
instruction_id, _ = found[0]
await send_terminal_instruction(
message,
instruction_id,
keyboard=keyboard or back_to_main(),
)
return True
if len(found) > 1:
buttons = [{"title": title} for _, title in found]
kb = build_keyboard(buttons, back=True)
await message.answer(
"🔎 Найдено несколько вариантов. Выберите нужный:",
keyboard=kb,
)
return True
await message.answer(
f"❗ Ошибка пинпада {code}\n\n"
f"📌 Причина:\n{reason}\n\n"
f"Не удалось найти инструкцию по ключу: {query or 'не задан'}",
keyboard=keyboard or back_to_main(),
)
return True
await message.answer(
f"❗ Ошибка пинпада {code}\n\n"
f"📌 Причина:\n{reason}\n\n"
f"✅ Что делать:\n{action}",
keyboard=keyboard
)
return True
# ─────────────────────────────────────────────
# TERMINAL — подробная инструкция
# ─────────────────────────────────────────────
async def _send_text(target, text: str, keyboard=None):
if hasattr(target, "answer"):
await target.answer(text, keyboard=keyboard)
else:
await target.send_message(text, keyboard=keyboard)
async def _send_attachment(target, attachment: str):
if hasattr(target, "answer"):
await target.answer(attachment=attachment)
else:
await target.send_message(attachment=attachment)
def _parse_goto(value: str):
parts = (value or "").strip().split(":", 1)
if not parts[0].isdigit():
return None
instr_id = int(parts[0])
step_index = 0
if len(parts) > 1:
if not parts[1].isdigit():
return None
step_num = int(parts[1])
if step_num > 0:
step_index = step_num - 1
return instr_id, step_index
async def _send_terminal_from_index(target, instruction_id: int, start_index: int, keyboard=None, max_hops: int = 5):
"""
Отправляет пошаговую инструкцию терминала:
несколько сообщений подряд (text / image)
"""
steps = get_terminal_steps(instruction_id)
if not steps:
await _send_text(target, "❌ Для этой инструкции нет шагов.", keyboard=keyboard)
return
for idx, (step_type, content) in enumerate(steps[start_index:], start=start_index):
if step_type == "text":
await _send_text(target, content, keyboard=keyboard)
elif step_type == "image":
await _send_attachment(target, content)
elif step_type == "pause":
await _send_text(target, PAUSE_PROMPT, keyboard=pause_keyboard())
pause_at_end = (idx == len(steps) - 1)
set_instruction_progress(
target.from_id if hasattr(target, "from_id") else target.user_id,
instruction_id,
idx + 1,
pause_at_end
)
return
elif step_type == "goto":
if max_hops <= 0:
await _send_text(target, "❌ Превышено число переходов между инструкциями.", keyboard=keyboard)
return
parsed = _parse_goto(content)
if not parsed:
await _send_text(target, f"❌ Некорректный goto: {content}", keyboard=keyboard)
return
next_id, next_index = parsed
user_id = target.from_id if hasattr(target, "from_id") else target.user_id
log_event(user_id, "terminal_instruction", str(next_id))
clear_instruction_progress(target.from_id if hasattr(target, "from_id") else target.user_id)
await _send_terminal_from_index(
target,
next_id,
next_index,
keyboard=keyboard,
max_hops=max_hops - 1,
)
return
clear_instruction_progress(target.from_id if hasattr(target, "from_id") else target.user_id)
async def send_terminal_instruction(message, instruction_id: int, keyboard=None):
log_event(message.from_id, "terminal_instruction", str(instruction_id))
await _send_terminal_from_index(message, instruction_id, 0, keyboard)
# ─────────────────────────────────────────────
# УНИВЕРСАЛЬНЫЙ ХЕЛПЕР (по желанию)
# ─────────────────────────────────────────────
async def send_instruction(message, instruction_id: int, keyboard=None):
"""
Алиас, чтобы использовать одно имя в коде
"""
await send_terminal_instruction(message, instruction_id, keyboard)
async def handle_pause_response(message) -> bool:
if not message.text:
return False
progress = get_instruction_progress(message.from_id)
if not progress:
return False
text = message.text.strip().lower()
instruction_id, next_step, pause_at_end = progress
if text in PAUSE_YES:
clear_instruction_progress(message.from_id)
await message.answer("Отлично, остановил инструкцию.", keyboard=back_to_main())
return True
if text in PAUSE_NO:
if pause_at_end:
clear_instruction_progress(message.from_id)
await message.answer(
"Похоже, инструкция не помогла. Заполните, пожалуйста, форму:\n"
"https://example.com/google-form",
keyboard=back_to_main()
)
return True
steps = get_terminal_steps(instruction_id)
if next_step >= len(steps):
clear_instruction_progress(message.from_id)
await message.answer(
"Похоже, инструкция не помогла. Заполните, пожалуйста, форму:\n"
"https://example.com/google-form",
keyboard=back_to_main()
)
return True
await _send_terminal_from_index(message, instruction_id, next_step, keyboard=back_to_main())
return True
return False
async def handle_pause_event(event) -> bool:
payload = event.get_payload_json() if hasattr(event, "get_payload_json") else None
if not payload or payload.get("pause") not in ("yes", "no"):
return False
progress = get_instruction_progress(event.user_id)
if not progress:
return False
instruction_id, next_step, pause_at_end = progress
if payload["pause"] == "yes":
clear_instruction_progress(event.user_id)
await event.send_message("Отлично, всё работает.", keyboard=back_to_main())
return True
if pause_at_end:
clear_instruction_progress(event.user_id)
await event.send_message(
"Похоже, инструкция не помогла. Заполните, пожалуйста, форму:\n"
"https://example.com/google-form",
keyboard=back_to_main()
)
return True
steps = get_terminal_steps(instruction_id)
if next_step >= len(steps):
clear_instruction_progress(event.user_id)
await event.send_message(
"Похоже, инструкция не помогла. Заполните, пожалуйста, форму:\n"
"https://example.com/google-form",
keyboard=back_to_main()
)
return True
await _send_terminal_from_index(
event,
instruction_id,
next_step,
keyboard=back_to_main()
)
return True