Initial commit: VK Sales Bot project structure
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
VK_GROUP_ID=233127658
|
||||||
|
VK_TOKEN="vk1.a.LAMaFdJdNPYBM2OyaMnw71WS4I4ITwTirs7dxRa3zcrVCzWIEp_wOYe75zZp_ZDpXARQFV4ZXlNpqLtCWUDpAsXGKDfhYjuT9G1vjw0blI3zXTh5k6S-P0Txy5a_JUbZiAfSVIZEYCb6hHs_mv71te9TpHaw-knSwbmxLYRklAIwX5hBlRe-vWM8cBZljmdNx7vh0Poe0-W6hnoEEDh6uQ"
|
||||||
|
ADMIN_IDS=25076348
|
||||||
|
DATA_DIR=data
|
||||||
|
LEADS_FILE=data/leads.xlsx
|
||||||
|
BACKUP_DIR=data/backups
|
||||||
|
LOG_LEVEL=DEBUG
|
||||||
|
TIMEZONE=Europe/Moscow
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
VK_GROUP_ID=233127658
|
||||||
|
VK_TOKEN=vk1.a.XTkUvOpbeC9ReN0WxLLynwe19Xdfr5kt4_lWcFpJINpS4O-MsQMeSxc9WWj7IqBTDDOLcpdgjGS4MdBsorXIEosmvV-iYwbExBqZaRTdtl7DcrRsYg0uW1gavDc_SBItLjoCnje7WlO5vz8i5pxXgkSuiki9vMbfYZdjYZYF8q5z7sResjJ-ZdBKS73WMCmrgQx0I22rGY0rRR7HKCeC_g
|
||||||
|
ADMIN_IDS=25076348
|
||||||
|
DATA_DIR=data
|
||||||
|
LEADS_FILE=data/leads.xlsx
|
||||||
|
BACKUP_DIR=data/backups
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
TIMEZONE=Europe/Moscow
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://app.kilo.ai/config.json"
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"python-envs.defaultEnvManager": "ms-python.python:system"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
CMD ["python", "main.py"]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,84 @@
|
|||||||
|
# Все тексты бота вынесены сюда для быстрой правки
|
||||||
|
ASK_PARENT_CONSENT = (
|
||||||
|
"👪 Укажите, пожалуйста, контактные данные родителя или опекуна. "
|
||||||
|
"Вы можете предоставить их сейчас или пропустить этот шаг. "
|
||||||
|
"Напишите 'пропустить', чтобы пропустить, или укажите ФИО родителя (два слова на кириллице):"
|
||||||
|
)
|
||||||
|
ASK_PARENT_FIO = "Укажите ФИО родителя (Фамилия Имя):"
|
||||||
|
ASK_PARENT_PHONE = "Укажите телефон родителя в формате +7 (XXX) XXX-XX-XX:"
|
||||||
|
PARENT_FIO_INVALID = "❌ ФИО родителя должно состоять из двух слов (Фамилия Имя) кириллицей."
|
||||||
|
PARENT_PHONE_INVALID = "❌ Неверный формат телефона родителя. Пример: +7 (912) 345-67-89"
|
||||||
|
PARENT_DATA_SKIPPED = "✅ Данные родителя пропущены. Продолжаем оформление."
|
||||||
|
PARENT_DATA_SAVED = "✅ Данные родителя сохранены."
|
||||||
|
|
||||||
|
GREETING = (
|
||||||
|
"Здравствуйте! 👋 Меня зовут Алексей, я специалист отдела продаж.\n\n"
|
||||||
|
"Подскажите, как к вам обращаться? (Фамилия, Имя, Отчество)"
|
||||||
|
)
|
||||||
|
|
||||||
|
ASK_FI = "Пожалуйста, укажите вашу фамилию и имя (два слова на кириллице):"
|
||||||
|
FIO_INVALID = "❌ ФИО должно состоять из двух слов (Фамилия Имя) и содержать только буквы кириллицы. Попробуйте ещё раз:"
|
||||||
|
|
||||||
|
ASK_PHONE = "Отлично! Теперь укажите ваш номер телефона в формате +7 (XXX) XXX-XX-XX:"
|
||||||
|
PHONE_INVALID = "❌ Неверный формат. Нужно +7 и 10 цифр после кода страны. Пример: +7 (912) 345-67-89"
|
||||||
|
|
||||||
|
ASK_TIME = "Укажите удобное время по МСК для звонка (например: 'завтра после 18', 'в среду утром', 'сегодня в 15:00'):"
|
||||||
|
TIME_INVALID = "❌ Не понял время. Попробуйте сказать иначе: 'завтра в 10 утра', 'сегодня после обеда'."
|
||||||
|
|
||||||
|
CONSENT_TEXT = (
|
||||||
|
"🔐 Перед началом работы, пожалуйста, ознакомьтесь с условиями обработки персональных данных.\n\n"
|
||||||
|
"Мы собираем ваши ФИО, номер телефона и предпочтительное время звонка для связи с вами "
|
||||||
|
"по вопросам, связанным с услугами нашей компании. Ваши данные будут использоваться только "
|
||||||
|
"сотрудниками нашей компании и не будут переданы третьим лицам.\n\n"
|
||||||
|
"Дайте согласие на обработку персональных данных:"
|
||||||
|
)
|
||||||
|
ASK_CONSENT = "Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):"
|
||||||
|
CONSENT_YES = "✅ Спасибо за согласие! Продолжим оформление заявки."
|
||||||
|
CONSENT_NO = "❌ К сожалению, без согласия на обработку данных мы не можем продолжить. Спасибо за обращение! Хорошего дня."
|
||||||
|
|
||||||
|
BUTTON_CONSENT_YES = "✅ Да, согласен"
|
||||||
|
BUTTON_CONSENT_NO = "❌ Нет, не согласен"
|
||||||
|
|
||||||
|
CONSENT_YES_PHRASES = ["да", "yes", "согласен", "даю согласие", "хорошо", "ok"]
|
||||||
|
CONSENT_NO_PHRASES = [
|
||||||
|
"нет", "no", "не согласен", "отказываюсь", "не даю согласие",
|
||||||
|
"нет, не согласен", "не хочу давать согласие", "отказ"
|
||||||
|
]
|
||||||
|
|
||||||
|
CONFIRM_MESSAGE = (
|
||||||
|
"Проверьте данные:\n"
|
||||||
|
"ФИ: {fio}\n"
|
||||||
|
"Телефон: {phone}\n"
|
||||||
|
"Время звонка по МСК: {time}\n"
|
||||||
|
"{parent_info}"
|
||||||
|
"Всё верно? (Да / Нет)"
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIRM_YES = "Спасибо! Ваши данные сохранены. Менеджер свяжется с вами в указанное время. Хорошего дня! 🌟"
|
||||||
|
CONFIRM_NO = "Давайте начнём заново. " + GREETING
|
||||||
|
|
||||||
|
OBJECTION_NOT_NOW = (
|
||||||
|
"Понимаю, возможно сейчас неудобно. Я оставлю заявку в статусе 'отложено'. "
|
||||||
|
"Напишите 'начать заново', когда будете готовы продолжить."
|
||||||
|
)
|
||||||
|
OBJECTION_THINK = (
|
||||||
|
"Хорошо, подумайте. Я сохраню текущие данные. Для продолжения напишите 'продолжить'."
|
||||||
|
)
|
||||||
|
OBJECTION_NO_PHONE = (
|
||||||
|
"К сожалению, без номера телефона мы не сможем с вами связаться. "
|
||||||
|
"Если передумаете – напишите 'начать заново'."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кнопки
|
||||||
|
BUTTON_YES = "✅ Да"
|
||||||
|
BUTTON_NO = "❌ Нет"
|
||||||
|
BUTTON_RESTART = "🔄 Начать заново"
|
||||||
|
BUTTON_CONTINUE = "➡️ Продолжить"
|
||||||
|
|
||||||
|
# Команды
|
||||||
|
CMD_START = "/start"
|
||||||
|
CMD_STATUS = "/status"
|
||||||
|
CMD_EXPORT = "/export"
|
||||||
|
CMD_BACKUP = "/backup"
|
||||||
|
CMD_STATS = "/stats"
|
||||||
|
CMD_RELOAD = "/reload"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
VK_GROUP_ID = int(os.getenv("VK_GROUP_ID"))
|
||||||
|
VK_TOKEN = os.getenv("VK_TOKEN")
|
||||||
|
ADMIN_IDS = [int(x.strip()) for x in os.getenv("ADMIN_IDS", "").split(",") if x.strip()]
|
||||||
|
|
||||||
|
DATA_DIR = Path(os.getenv("DATA_DIR", BASE_DIR / "data"))
|
||||||
|
LEADS_FILE = Path(os.getenv("LEADS_FILE", DATA_DIR / "leads.xlsx"))
|
||||||
|
BACKUP_DIR = Path(os.getenv("BACKUP_DIR", DATA_DIR / "backups"))
|
||||||
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
TIMEZONE = os.getenv("TIMEZONE", "Europe/Moscow")
|
||||||
|
|
||||||
|
# Создаём директории
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
BACKUP_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Настройки валидации
|
||||||
|
PHONE_MASK = "+7 (___) ___-__-__"
|
||||||
|
PHONE_LENGTH = 11 # цифр после +7
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
|||||||
|
class ValidationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ExportError(Exception):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from pathlib import Path
|
||||||
|
from filelock import FileLock
|
||||||
|
from datetime import datetime
|
||||||
|
from loguru import logger
|
||||||
|
from core.models import LeadData
|
||||||
|
from config import settings
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
class ExcelExporter:
|
||||||
|
def __init__(self, file_path: Path):
|
||||||
|
self.file_path = file_path
|
||||||
|
self.lock_path = file_path.with_suffix('.lock')
|
||||||
|
self._init_file()
|
||||||
|
|
||||||
|
def _init_file(self):
|
||||||
|
"""Создаёт файл с заголовками, если не существует"""
|
||||||
|
if not self.file_path.exists():
|
||||||
|
df = pd.DataFrame(columns=[
|
||||||
|
"timestamp", "user_id", "full_name", "phone",
|
||||||
|
"preferred_time", "status", "consent_given",
|
||||||
|
"parent_full_name", "parent_phone"
|
||||||
|
])
|
||||||
|
df.to_excel(self.file_path, index=False, engine='openpyxl')
|
||||||
|
|
||||||
|
def save_lead(self, lead: LeadData):
|
||||||
|
"""Атомарная дозапись с блокировкой"""
|
||||||
|
lock = FileLock(self.lock_path)
|
||||||
|
with lock:
|
||||||
|
try:
|
||||||
|
# Читаем существующий DataFrame
|
||||||
|
df = pd.read_excel(self.file_path, engine='openpyxl')
|
||||||
|
# Добавляем новую строку
|
||||||
|
new_row = pd.DataFrame([{
|
||||||
|
"timestamp": lead.timestamp,
|
||||||
|
"user_id": lead.user_id,
|
||||||
|
"full_name": lead.full_name,
|
||||||
|
"phone": lead.phone,
|
||||||
|
"preferred_time": lead.preferred_time,
|
||||||
|
"status": lead.status,
|
||||||
|
"consent_given": lead.consent_given,
|
||||||
|
"parent_full_name": lead.parent_full_name,
|
||||||
|
"parent_phone": lead.parent_phone
|
||||||
|
}])
|
||||||
|
df = pd.concat([df, new_row], ignore_index=True)
|
||||||
|
# Записываем обратно
|
||||||
|
df.to_excel(self.file_path, index=False, engine='openpyxl')
|
||||||
|
logger.info(f"Lead saved for user {lead.user_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save lead: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def backup(self):
|
||||||
|
"""Создаёт копию файла с меткой времени"""
|
||||||
|
if self.file_path.exists():
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
backup_path = settings.BACKUP_DIR / f"leads_backup_{timestamp}.xlsx"
|
||||||
|
shutil.copy2(self.file_path, backup_path)
|
||||||
|
logger.info(f"Backup created: {backup_path}")
|
||||||
|
|
||||||
|
def get_stats(self) -> dict:
|
||||||
|
"""Возвращает статистику по лидам"""
|
||||||
|
if not self.file_path.exists():
|
||||||
|
return {"total": 0, "today": 0, "statuses": {}}
|
||||||
|
df = pd.read_excel(self.file_path, engine='openpyxl')
|
||||||
|
total = len(df)
|
||||||
|
today = len(df[pd.to_datetime(df['timestamp']).dt.date == datetime.now().date()])
|
||||||
|
statuses = df['status'].value_counts().to_dict()
|
||||||
|
return {"total": total, "today": today, "statuses": statuses}
|
||||||
|
|
||||||
|
def export(self, target_path: Path = None):
|
||||||
|
"""Принудительный экспорт (копия)"""
|
||||||
|
if target_path is None:
|
||||||
|
target_path = settings.DATA_DIR / f"export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||||
|
shutil.copy2(self.file_path, target_path)
|
||||||
|
return target_path
|
||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
from enum import StrEnum, auto
|
||||||
|
from typing import Dict, Optional
|
||||||
|
from loguru import logger
|
||||||
|
from core.models import LeadData
|
||||||
|
from core.validator import validate_fio, validate_phone, parse_time
|
||||||
|
from core.exporter import ExcelExporter
|
||||||
|
from config import phrases
|
||||||
|
|
||||||
|
class DialogState(StrEnum):
|
||||||
|
START = auto()
|
||||||
|
ASK_CONSENT = auto()
|
||||||
|
COLLECT_FI = auto() # ФИО пользователя
|
||||||
|
ASK_PARENT_DATA = auto() # Спрашиваем, хочет ли пользователь ввести данные родителей
|
||||||
|
COLLECT_PARENT_FIO = auto() # Ввод ФИО родителя
|
||||||
|
COLLECT_PARENT_PHONE = auto() # Ввод телефона родителя
|
||||||
|
COLLECT_PHONE = auto()
|
||||||
|
COLLECT_TIME = auto()
|
||||||
|
CONFIRM = auto()
|
||||||
|
FINISHED = auto()
|
||||||
|
OBJECTION_HANDLED = auto()
|
||||||
|
|
||||||
|
class DialogManager:
|
||||||
|
def __init__(self, exporter: ExcelExporter):
|
||||||
|
self.exporter = exporter
|
||||||
|
self.sessions: Dict[int, dict] = {}
|
||||||
|
|
||||||
|
# def _handle_age(self, user_id: int, text: str) -> str:
|
||||||
|
# result = validate_age(text)
|
||||||
|
# if not result.is_valid:
|
||||||
|
# return result.error_message + "\n" + phrases.ASK_AGE
|
||||||
|
# session = self.sessions[user_id]
|
||||||
|
# session["data"].age = result.value
|
||||||
|
# logger.info(f"User {user_id} age = {result.value}")
|
||||||
|
# if result.value < 18:
|
||||||
|
# session["state"] = DialogState.ASK_PARENT_DATA
|
||||||
|
# logger.info(f"State changed to ASK_PARENT_DATA for user {user_id}")
|
||||||
|
# return phrases.ASK_PARENT_CONSENT
|
||||||
|
# else:
|
||||||
|
# session["state"] = DialogState.COLLECT_PHONE
|
||||||
|
# logger.info(f"Age >=18, state changed to COLLECT_PHONE")
|
||||||
|
# return phrases.ASK_PHONE
|
||||||
|
|
||||||
|
def _handle_parent_consent(self, user_id: int, text: str) -> str:
|
||||||
|
"""Обрабатывает ответ на вопрос про данные родителей.
|
||||||
|
Если пользователь вводит 'пропустить' и т.п. — пропускает.
|
||||||
|
Иначе считает, что это и есть ФИО родителя, и переходит к телефону.
|
||||||
|
"""
|
||||||
|
session = self.sessions[user_id]
|
||||||
|
text_lower = text.lower().strip()
|
||||||
|
if text_lower in ("пропустить", "нет", "не надо", "skip", "не хочу"):
|
||||||
|
session["state"] = DialogState.COLLECT_PHONE
|
||||||
|
logger.info(f"Parent data skipped for user {user_id}")
|
||||||
|
return phrases.PARENT_DATA_SKIPPED + "\n" + phrases.ASK_PHONE
|
||||||
|
else:
|
||||||
|
# Считаем, что пользователь сразу ввёл ФИО родителя
|
||||||
|
result = validate_fio(text)
|
||||||
|
if not result.is_valid:
|
||||||
|
return phrases.PARENT_FIO_INVALID + "\n" + phrases.ASK_PARENT_CONSENT
|
||||||
|
session["data"].parent_full_name = result.value
|
||||||
|
session["state"] = DialogState.COLLECT_PARENT_PHONE
|
||||||
|
logger.info(f"Parent FIO saved for user {user_id}, state changed to COLLECT_PARENT_PHONE")
|
||||||
|
return phrases.ASK_PARENT_PHONE
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_parent_fio(self, user_id: int, text: str) -> str:
|
||||||
|
result = validate_fio(text) # используем ту же валидацию, что и для ФИО пользователя
|
||||||
|
if not result.is_valid:
|
||||||
|
# Подменяем сообщение об ошибке для родителя
|
||||||
|
return phrases.PARENT_FIO_INVALID + "\n" + phrases.ASK_PARENT_FIO
|
||||||
|
session = self.sessions[user_id]
|
||||||
|
session["data"].parent_full_name = result.value
|
||||||
|
session["state"] = DialogState.COLLECT_PARENT_PHONE
|
||||||
|
return phrases.ASK_PARENT_PHONE
|
||||||
|
|
||||||
|
def _handle_parent_phone(self, user_id: int, text: str) -> str:
|
||||||
|
result = validate_phone(text) # используем существующий валидатор телефона
|
||||||
|
if not result.is_valid:
|
||||||
|
return phrases.PARENT_PHONE_INVALID + "\n" + phrases.ASK_PARENT_PHONE
|
||||||
|
session = self.sessions[user_id]
|
||||||
|
session["data"].parent_phone = result.value
|
||||||
|
session["state"] = DialogState.COLLECT_PHONE
|
||||||
|
return phrases.PARENT_DATA_SAVED + "\n" + phrases.ASK_PHONE
|
||||||
|
|
||||||
|
def handle_message(self, user_id: int, text: str) -> str:
|
||||||
|
"""Обрабатывает сообщение и возвращает ответ бота"""
|
||||||
|
logger.info(f"=== HANDLE: user {user_id}, text='{text}', current state={self.sessions.get(user_id, {}).get('state')} ===")
|
||||||
|
# Команда /start или "начать заново"
|
||||||
|
if text.lower() in (phrases.CMD_START, phrases.BUTTON_RESTART.lower(), "начать заново"):
|
||||||
|
return self._reset_dialog(user_id)
|
||||||
|
|
||||||
|
session = self.sessions.get(user_id)
|
||||||
|
if not session:
|
||||||
|
return self._start_dialog(user_id)
|
||||||
|
|
||||||
|
state = session["state"]
|
||||||
|
lead = session["data"]
|
||||||
|
|
||||||
|
# Обработка возражений на любом этапе
|
||||||
|
if self._is_objection(text):
|
||||||
|
return self._handle_objection(user_id, text)
|
||||||
|
|
||||||
|
# Продолжение после возражения "подумаю"
|
||||||
|
if state == DialogState.OBJECTION_HANDLED and text.lower() == phrases.BUTTON_CONTINUE.lower():
|
||||||
|
# Возвращаемся к предыдущему состоянию
|
||||||
|
prev_state = session.get("prev_state", DialogState.COLLECT_FI)
|
||||||
|
session["state"] = prev_state
|
||||||
|
# Не рекурсивный вызов, а продолжение основного потока
|
||||||
|
state = session["state"]
|
||||||
|
lead = session["data"]
|
||||||
|
|
||||||
|
# Обычные состояния
|
||||||
|
if state == DialogState.ASK_CONSENT:
|
||||||
|
return self._handle_consent(user_id, text)
|
||||||
|
if state == DialogState.COLLECT_FI:
|
||||||
|
return self._handle_fio(user_id, text)
|
||||||
|
elif state == DialogState.ASK_PARENT_DATA: # Спрашиваем про данные родителей
|
||||||
|
return self._handle_parent_consent(user_id, text)
|
||||||
|
elif state == DialogState.COLLECT_PARENT_FIO:
|
||||||
|
return self._handle_parent_fio(user_id, text)
|
||||||
|
elif state == DialogState.COLLECT_PARENT_PHONE:
|
||||||
|
return self._handle_parent_phone(user_id, text)
|
||||||
|
elif state == DialogState.COLLECT_PHONE:
|
||||||
|
return self._handle_phone(user_id, text)
|
||||||
|
elif state == DialogState.COLLECT_TIME:
|
||||||
|
return self._handle_time(user_id, text)
|
||||||
|
elif state == DialogState.CONFIRM:
|
||||||
|
return self._handle_confirm(user_id, text)
|
||||||
|
|
||||||
|
def _start_dialog(self, user_id: int) -> str:
|
||||||
|
self.sessions[user_id] = {
|
||||||
|
"state": DialogState.ASK_CONSENT, # ← было COLLECT_FI
|
||||||
|
"data": LeadData(user_id=user_id)
|
||||||
|
}
|
||||||
|
return phrases.ASK_CONSENT
|
||||||
|
|
||||||
|
def _reset_dialog(self, user_id: int) -> str:
|
||||||
|
if user_id in self.sessions:
|
||||||
|
del self.sessions[user_id]
|
||||||
|
return self._start_dialog(user_id)
|
||||||
|
|
||||||
|
def _handle_fio(self, user_id: int, text: str) -> str:
|
||||||
|
result = validate_fio(text)
|
||||||
|
if not result.is_valid:
|
||||||
|
return result.error_message + "\n" + phrases.ASK_FI
|
||||||
|
session = self.sessions[user_id]
|
||||||
|
session["data"].full_name = result.value
|
||||||
|
session["state"] = DialogState.ASK_PARENT_DATA
|
||||||
|
return phrases.ASK_PARENT_CONSENT
|
||||||
|
|
||||||
|
def _handle_phone(self, user_id: int, text: str) -> str:
|
||||||
|
result = validate_phone(text)
|
||||||
|
if not result.is_valid:
|
||||||
|
return result.error_message + "\n" + phrases.ASK_PHONE
|
||||||
|
session = self.sessions[user_id]
|
||||||
|
session["data"].phone = result.value
|
||||||
|
session["state"] = DialogState.COLLECT_TIME
|
||||||
|
return phrases.ASK_TIME
|
||||||
|
|
||||||
|
def _handle_time(self, user_id: int, text: str) -> str:
|
||||||
|
result = parse_time(text)
|
||||||
|
if not result.is_valid:
|
||||||
|
return result.error_message + "\n" + phrases.ASK_TIME
|
||||||
|
session = self.sessions[user_id]
|
||||||
|
session["data"].preferred_time = result.value
|
||||||
|
parent_info = ""
|
||||||
|
if session["data"].parent_full_name and session["data"].parent_phone:
|
||||||
|
parent_info = f"Родитель: {session['data'].parent_full_name}, тел: {session['data'].parent_phone}\n"
|
||||||
|
elif session["data"].parent_full_name:
|
||||||
|
parent_info = f"Родитель: {session['data'].parent_full_name} (телефон не указан)\n"
|
||||||
|
elif session["data"].parent_phone:
|
||||||
|
parent_info = f"Родитель: телефон {session['data'].parent_phone} (ФИО не указано)\n"
|
||||||
|
session["state"] = DialogState.CONFIRM
|
||||||
|
return phrases.CONFIRM_MESSAGE.format(
|
||||||
|
fio=session["data"].full_name,
|
||||||
|
phone=session["data"].phone,
|
||||||
|
time=session["data"].preferred_time,
|
||||||
|
parent_info=parent_info
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_confirm(self, user_id: int, text: str) -> str:
|
||||||
|
if text.lower() in ("да", phrases.BUTTON_YES.lower()):
|
||||||
|
lead = self.sessions[user_id]["data"]
|
||||||
|
lead.status = "completed"
|
||||||
|
self.exporter.save_lead(lead)
|
||||||
|
self.sessions[user_id]["state"] = DialogState.FINISHED
|
||||||
|
return phrases.CONFIRM_YES
|
||||||
|
elif text.lower() in ("нет", phrases.BUTTON_NO.lower()):
|
||||||
|
return self._reset_dialog(user_id)
|
||||||
|
else:
|
||||||
|
return "Пожалуйста, ответьте 'Да' или 'Нет'."
|
||||||
|
|
||||||
|
def _is_objection(self, text: str) -> bool:
|
||||||
|
obj_phrases = ["не сейчас", "подумаю", "не хочу оставлять телефон", "не хочу говорить номер"]
|
||||||
|
return any(phrase in text.lower() for phrase in obj_phrases)
|
||||||
|
|
||||||
|
def _handle_objection(self, user_id: int, text: str) -> str:
|
||||||
|
session = self.sessions.get(user_id)
|
||||||
|
if not session:
|
||||||
|
return self._start_dialog(user_id)
|
||||||
|
|
||||||
|
lead = session["data"]
|
||||||
|
if "не сейчас" in text.lower():
|
||||||
|
lead.status = "postponed"
|
||||||
|
self.exporter.save_lead(lead)
|
||||||
|
self.sessions[user_id]["state"] = DialogState.FINISHED
|
||||||
|
return phrases.OBJECTION_NOT_NOW
|
||||||
|
elif "подумаю" in text.lower():
|
||||||
|
# Сохраняем текущее состояние, переходим в OBJECTION_HANDLED
|
||||||
|
session["prev_state"] = session["state"]
|
||||||
|
session["state"] = DialogState.OBJECTION_HANDLED
|
||||||
|
return phrases.OBJECTION_THINK
|
||||||
|
elif any(phrase in text.lower() for phrase in ["не хочу оставлять телефон", "не хочу говорить номер"]):
|
||||||
|
lead.status = "rejected"
|
||||||
|
self.exporter.save_lead(lead)
|
||||||
|
self.sessions[user_id]["state"] = DialogState.FINISHED
|
||||||
|
return phrases.OBJECTION_NO_PHONE
|
||||||
|
return self.handle_message(user_id, text) # fallback
|
||||||
|
|
||||||
|
def _handle_consent(self, user_id: int, text: str) -> str:
|
||||||
|
text_lower = text.lower().strip()
|
||||||
|
session = self.sessions.get(user_id)
|
||||||
|
if not session:
|
||||||
|
return self._start_dialog(user_id)
|
||||||
|
|
||||||
|
if any(phrase in text_lower for phrase in phrases.CONSENT_YES_PHRASES):
|
||||||
|
session["data"].consent_given = True
|
||||||
|
session["state"] = DialogState.COLLECT_FI # ← было COLLECT_FIO
|
||||||
|
return phrases.CONSENT_YES + "\n" + phrases.ASK_FI # ← было ASK_FIO
|
||||||
|
elif any(phrase in text_lower for phrase in phrases.CONSENT_NO_PHRASES):
|
||||||
|
del self.sessions[user_id]
|
||||||
|
return phrases.CONSENT_NO
|
||||||
|
else:
|
||||||
|
return "Пожалуйста, ответьте 'Да' (согласен) или 'Нет' (не согласен)."
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class LeadData(BaseModel):
|
||||||
|
user_id: int
|
||||||
|
full_name: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
preferred_time: Optional[str] = None
|
||||||
|
timestamp: datetime = Field(default_factory=datetime.now)
|
||||||
|
status: str = "new"
|
||||||
|
consent_given: bool = False
|
||||||
|
parent_full_name: Optional[str] = None
|
||||||
|
parent_phone: Optional[str] = None
|
||||||
|
|
||||||
|
class ValidationResult(BaseModel):
|
||||||
|
is_valid: bool
|
||||||
|
value: Optional[str] = None
|
||||||
|
error_message: Optional[str] = None
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from loguru import logger
|
||||||
|
import re
|
||||||
|
import dateparser
|
||||||
|
from datetime import datetime
|
||||||
|
from core.models import ValidationResult
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def validate_fio(text: str) -> ValidationResult:
|
||||||
|
"""Проверка ФИ: 2 слова, кириллица, возможен дефис"""
|
||||||
|
text = text.strip()
|
||||||
|
words = text.split()
|
||||||
|
if len(words) != 2:
|
||||||
|
return ValidationResult(is_valid=False, error_message="Должно быть 2 слова")
|
||||||
|
pattern = r'^[А-ЯЁа-яё\-]+$'
|
||||||
|
for word in words:
|
||||||
|
if not re.match(pattern, word):
|
||||||
|
return ValidationResult(is_valid=False, error_message="Только буквы кириллицы и дефис")
|
||||||
|
return ValidationResult(is_valid=True, value=text)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_phone(phone: str) -> ValidationResult:
|
||||||
|
"""Принимает +7 (XXX) XXX-XX-XX или просто 10 цифр после 7/8"""
|
||||||
|
cleaned = re.sub(r'\D', '', phone)
|
||||||
|
if len(cleaned) == 11 and cleaned.startswith('7'):
|
||||||
|
cleaned = '8' + cleaned[1:]
|
||||||
|
if len(cleaned) == 10:
|
||||||
|
cleaned = '8' + cleaned
|
||||||
|
if len(cleaned) == 11 and cleaned.startswith('8'):
|
||||||
|
formatted = f"+7 ({cleaned[1:4]}) {cleaned[4:7]}-{cleaned[7:9]}-{cleaned[9:]}"
|
||||||
|
return ValidationResult(is_valid=True, value=formatted)
|
||||||
|
return ValidationResult(is_valid=False, error_message="Некорректный номер")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time(text: str) -> ValidationResult:
|
||||||
|
"""Парсит естественно-языковое время, возвращает строку для отображения"""
|
||||||
|
text_lower = text.lower().strip()
|
||||||
|
if any(phrase in text_lower for phrase in ["любое время", "не важно", "в любое", "когда удобно"]):
|
||||||
|
return ValidationResult(is_valid=True, value="Любое удобное время")
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = dateparser.parse(
|
||||||
|
text,
|
||||||
|
settings={'TIMEZONE': settings.TIMEZONE, 'RETURN_AS_TIMEZONE_AWARE': False}
|
||||||
|
)
|
||||||
|
if parsed and parsed > datetime.now():
|
||||||
|
formatted = parsed.strftime("%d.%m.%Y в %H:%M")
|
||||||
|
return ValidationResult(is_valid=True, value=formatted)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to parse time '{text}': {e}")
|
||||||
|
|
||||||
|
return ValidationResult(is_valid=False, error_message="Не удалось распознать время. Укажите, например: 'завтра после 18', 'в среду утром' или 'любое время'.")
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
$body = @{
|
||||||
|
name = 'bot_vk_ikp'
|
||||||
|
description = 'VK Sales Bot'
|
||||||
|
private = $false
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)
|
||||||
|
|
||||||
|
$headers = @{
|
||||||
|
'Authorization' = 'Bearer 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'
|
||||||
|
'Accept' = 'application/vnd.github.v3+json'
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Invoke-RestMethod -Method POST -Uri 'https://api.github.com/user/repos' -Headers $headers -Body $bytes -ContentType 'application/json; charset=utf-8'
|
||||||
|
Write-Host "Repository created: $($result.html_url)"
|
||||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
|||||||
|
# Документация разработчика
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
Проект построен на **конечном автомате (FSM)**. Диалог управляется классом `DialogManager`.
|
||||||
|
Состояния: `COLLECT_FI` → `COLLECT_PHONE` → `COLLECT_TIME` → `CONFIRM` → `FINISHED`.
|
||||||
|
Поддерживаются возражения и перезапуск.
|
||||||
|
|
||||||
|
**Компоненты:**
|
||||||
|
- `core/fsm.py` – логика диалога.
|
||||||
|
- `core/validator.py` – валидация ФИО, телефона, парсинг времени.
|
||||||
|
- `core/exporter.py` – работа с Excel (атомарная запись, бэкапы).
|
||||||
|
- `services/vk_bot.py` – интеграция с VK API, обработка команд.
|
||||||
|
- `utils/middleware.py` – мидлвары (логирование, аналитика).
|
||||||
|
|
||||||
|
## Добавление нового поля в анкету
|
||||||
|
1. Добавить текст в `config/phrases.py`.
|
||||||
|
2. Расширить модель `LeadData` в `core/models.py`.
|
||||||
|
3. Добавить состояние в `DialogState` и методы в `DialogManager`.
|
||||||
|
4. Добавить валидатор в `core/validator.py`.
|
||||||
|
5. Обновить `ExcelExporter._init_file` и метод сохранения.
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
Установите тестовые зависимости: `pip install pytest pytest-asyncio responses`.
|
||||||
|
Запуск: `pytest tests/`.
|
||||||
|
Пример теста для валидатора:
|
||||||
|
```python
|
||||||
|
def test_validate_fio():
|
||||||
|
from core.validator import validate_fio
|
||||||
|
assert validate_fio("Иванов Иван Иванович").is_valid
|
||||||
|
assert not validate_fio("John Doe").is_valid
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Руководство оператора "Сотрудник первой линии"
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
1. Убедитесь, что установлен Python 3.11+ и все зависимости (`pip install -r requirements.txt`).
|
||||||
|
2. Скопируйте `.env.example` в `.env` и укажите токен группы VK.
|
||||||
|
3. Запустите бота: `python main.py`
|
||||||
|
4. Проверьте статус: напишите боту `/status` (только для администраторов, указанных в ADMIN_IDS).
|
||||||
|
|
||||||
|
## Управление
|
||||||
|
- `/status` – проверить, работает ли бот.
|
||||||
|
- `/export` – создать копию файла лидов с меткой времени.
|
||||||
|
- `/backup` – вручную создать резервную копию.
|
||||||
|
- `/stats` – показать статистику (всего лидов, за сегодня, по статусам).
|
||||||
|
- `/reload` – перезагрузить текстовые фразы без перезапуска.
|
||||||
|
|
||||||
|
## Просмотр данных
|
||||||
|
Файл с лидами находится в `data/leads.xlsx`. Резервные копии – в `data/backups/`.
|
||||||
|
|
||||||
|
## Решение проблем
|
||||||
|
- Бот не отвечает: проверьте токен и права доступа группы (нужны `messages` и `groups`).
|
||||||
|
- Ошибка записи в файл: убедитесь, что папка `data` существует и доступна для записи.
|
||||||
|
- Бот не видит сообщения: группа должна быть публичной или добавьте бота в беседу.
|
||||||
+590
@@ -0,0 +1,590 @@
|
|||||||
|
2026-05-04T13:22:21.443454+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T13:22:21.685842+0300 | INFO | Bot started
|
||||||
|
2026-05-04T13:22:21.686877+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T13:23:07.722613+0300 | DEBUG | Message from 25076348 in peer 2000000002: [club233127658|@ikpro] 🔄 Начать заново
|
||||||
|
2026-05-04T13:23:07.724180+0300 | INFO | Processed message from 25076348: [club233127658|@ikpro] 🔄 Начать заново
|
||||||
|
2026-05-04T13:23:07.724696+0300 | INFO | === HANDLE: user 25076348, text='[club233127658|@ikpro] 🔄 Начать заново', current state=None ===
|
||||||
|
2026-05-04T13:23:07.727877+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-04T13:23:11.904304+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-04T13:23:11.904304+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-04T13:23:11.904840+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-04T13:23:11.904840+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-04T13:23:34.586781+0300 | DEBUG | Message from 25076348 in peer 2000000002: ффф ыавпва
|
||||||
|
2026-05-04T13:23:34.586781+0300 | INFO | Processed message from 25076348: ффф ыавпва
|
||||||
|
2026-05-04T13:23:34.587312+0300 | INFO | === HANDLE: user 25076348, text='ффф ыавпва', current state=collect_fi ===
|
||||||
|
2026-05-04T13:23:34.588357+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T13:23:37.967348+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T13:23:37.967876+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T13:23:37.967876+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T13:23:37.967876+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T13:23:37.968411+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T13:23:37.968411+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T13:23:37.969451+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T13:36:16.863923+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T13:36:17.084594+0300 | INFO | Bot started
|
||||||
|
2026-05-04T13:36:17.085630+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T13:37:06.672828+0300 | DEBUG | Message from 25076348 in peer 2000000002: Привет
|
||||||
|
2026-05-04T13:37:06.673343+0300 | INFO | Processed message from 25076348: Привет
|
||||||
|
2026-05-04T13:37:06.673343+0300 | INFO | === HANDLE: user 25076348, text='Привет', current state=None ===
|
||||||
|
2026-05-04T13:37:06.673883+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-04T13:37:09.159946+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-04T13:37:09.159946+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-04T13:37:09.160473+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-04T13:37:09.160473+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-04T13:37:15.495991+0300 | DEBUG | Message from 25076348 in peer 2000000002: ааа ррр
|
||||||
|
2026-05-04T13:37:15.496513+0300 | INFO | Processed message from 25076348: ааа ррр
|
||||||
|
2026-05-04T13:37:15.497027+0300 | INFO | === HANDLE: user 25076348, text='ааа ррр', current state=collect_fi ===
|
||||||
|
2026-05-04T13:37:15.497027+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T13:37:18.753724+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T13:37:18.753724+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T13:37:18.754244+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T13:37:18.754244+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T13:37:18.754244+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T13:37:18.754244+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T13:37:18.754756+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T13:37:22.075536+0300 | DEBUG | Message from 25076348 in peer 2000000002: 23
|
||||||
|
2026-05-04T13:37:22.076047+0300 | INFO | Processed message from 25076348: 23
|
||||||
|
2026-05-04T13:37:22.076047+0300 | INFO | === HANDLE: user 25076348, text='23', current state=collect_age ===
|
||||||
|
2026-05-04T13:37:22.076047+0300 | DEBUG | validate_age input: ''23''
|
||||||
|
2026-05-04T13:37:22.076566+0300 | DEBUG | cleaned: '23'
|
||||||
|
2026-05-04T13:37:22.076566+0300 | DEBUG | age valid: 23
|
||||||
|
2026-05-04T13:37:22.076566+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:09:25.567877+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T14:09:25.744659+0300 | INFO | Bot started
|
||||||
|
2026-05-04T14:09:25.745178+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T14:09:34.624213+0300 | DEBUG | Message from 25076348 in peer 2000000002: Привет
|
||||||
|
2026-05-04T14:09:34.624213+0300 | INFO | Processed message from 25076348: Привет
|
||||||
|
2026-05-04T14:09:34.624213+0300 | INFO | === HANDLE: user 25076348, text='Привет', current state=None ===
|
||||||
|
2026-05-04T14:09:34.624740+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-04T14:09:36.950764+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-04T14:09:36.950764+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-04T14:09:36.951300+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-04T14:09:36.951300+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-04T14:09:45.685252+0300 | DEBUG | Message from 25076348 in peer 2000000002: вав авп
|
||||||
|
2026-05-04T14:09:45.685252+0300 | INFO | Processed message from 25076348: вав авп
|
||||||
|
2026-05-04T14:09:45.685252+0300 | INFO | === HANDLE: user 25076348, text='вав авп', current state=collect_fi ===
|
||||||
|
2026-05-04T14:09:45.685773+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:09:49.196572+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T14:09:49.197077+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T14:09:49.197077+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T14:09:49.197077+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T14:09:49.197077+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T14:09:49.197077+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T14:09:49.197077+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:09:53.320692+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12лет
|
||||||
|
2026-05-04T14:09:53.320692+0300 | INFO | Processed message from 25076348: 12лет
|
||||||
|
2026-05-04T14:09:53.320692+0300 | INFO | === HANDLE: user 25076348, text='12лет', current state=collect_age ===
|
||||||
|
2026-05-04T14:09:53.321204+0300 | DEBUG | validate_age input: ''12лет''
|
||||||
|
2026-05-04T14:09:53.321204+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T14:09:53.321204+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T14:09:53.321724+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:10:19.043522+0300 | DEBUG | Message from 25076348 in peer 2000000002: 13
|
||||||
|
2026-05-04T14:10:19.043522+0300 | INFO | Processed message from 25076348: 13
|
||||||
|
2026-05-04T14:10:19.043522+0300 | INFO | === HANDLE: user 25076348, text='13', current state=collect_age ===
|
||||||
|
2026-05-04T14:10:19.044052+0300 | DEBUG | validate_age input: ''13''
|
||||||
|
2026-05-04T14:10:19.044052+0300 | DEBUG | cleaned: '13'
|
||||||
|
2026-05-04T14:10:19.044052+0300 | DEBUG | age valid: 13
|
||||||
|
2026-05-04T14:10:19.044052+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:10:22.029805+0300 | DEBUG | Message from 25076348 in peer 2000000002: 18
|
||||||
|
2026-05-04T14:10:22.029805+0300 | INFO | Processed message from 25076348: 18
|
||||||
|
2026-05-04T14:10:22.029805+0300 | INFO | === HANDLE: user 25076348, text='18', current state=collect_age ===
|
||||||
|
2026-05-04T14:10:22.030324+0300 | DEBUG | validate_age input: ''18''
|
||||||
|
2026-05-04T14:10:22.030324+0300 | DEBUG | cleaned: '18'
|
||||||
|
2026-05-04T14:10:22.030324+0300 | DEBUG | age valid: 18
|
||||||
|
2026-05-04T14:10:22.030324+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:52:44.897174+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T14:52:45.151496+0300 | INFO | Bot started
|
||||||
|
2026-05-04T14:52:45.152530+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T14:53:41.300176+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет
|
||||||
|
2026-05-04T14:53:41.302249+0300 | INFO | Processed message from 25076348: привет
|
||||||
|
2026-05-04T14:53:41.302249+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None ===
|
||||||
|
2026-05-04T14:53:41.306431+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-04T14:53:43.736066+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-04T14:53:43.736066+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-04T14:53:43.736066+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-04T14:53:43.736606+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-04T14:53:49.142783+0300 | DEBUG | Message from 25076348 in peer 2000000002: ппп ррр
|
||||||
|
2026-05-04T14:53:49.142783+0300 | INFO | Processed message from 25076348: ппп ррр
|
||||||
|
2026-05-04T14:53:49.143321+0300 | INFO | === HANDLE: user 25076348, text='ппп ррр', current state=collect_fi ===
|
||||||
|
2026-05-04T14:53:49.152638+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:53:51.561650+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T14:53:51.561650+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T14:53:51.562169+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T14:53:51.562169+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T14:53:51.562169+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T14:53:51.562710+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T14:53:51.563228+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:53:57.436097+0300 | DEBUG | Message from 25076348 in peer 2000000002: 13
|
||||||
|
2026-05-04T14:53:57.436097+0300 | INFO | Processed message from 25076348: 13
|
||||||
|
2026-05-04T14:53:57.436624+0300 | INFO | === HANDLE: user 25076348, text='13', current state=collect_age ===
|
||||||
|
2026-05-04T14:53:57.436624+0300 | DEBUG | validate_age input: ''13''
|
||||||
|
2026-05-04T14:53:57.436624+0300 | DEBUG | cleaned: '13'
|
||||||
|
2026-05-04T14:53:57.436624+0300 | DEBUG | age valid: 13
|
||||||
|
2026-05-04T14:53:57.437143+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T14:54:17.932582+0300 | ERROR | Fatal error: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
|
||||||
|
Traceback (most recent call last):
|
||||||
|
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
|
||||||
|
response = self._make_request(
|
||||||
|
│ └ <function HTTPConnectionPool._make_request at 0x000001F29A685300>
|
||||||
|
└ <urllib3.connectionpool.HTTPSConnectionPool object at 0x000001F29A814050>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 534, in _make_request
|
||||||
|
response = conn.getresponse()
|
||||||
|
│ └ <function HTTPConnection.getresponse at 0x000001F29A6551C0>
|
||||||
|
└ <HTTPSConnection(host='lp.vk.com', port=443) at 0x1f29a814d10>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connection.py", line 571, in getresponse
|
||||||
|
httplib_response = super().getresponse()
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 1374, in getresponse
|
||||||
|
response.begin()
|
||||||
|
│ └ <function HTTPResponse.begin at 0x000001F29A55B4C0>
|
||||||
|
└ <http.client.HTTPResponse object at 0x000001F29A4FF4F0>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 318, in begin
|
||||||
|
version, status, reason = self._read_status()
|
||||||
|
│ └ <function HTTPResponse._read_status at 0x000001F29A55B420>
|
||||||
|
└ <http.client.HTTPResponse object at 0x000001F29A4FF4F0>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 287, in _read_status
|
||||||
|
raise RemoteDisconnected("Remote end closed connection without"
|
||||||
|
└ <class 'http.client.RemoteDisconnected'>
|
||||||
|
|
||||||
|
http.client.RemoteDisconnected: Remote end closed connection without response
|
||||||
|
|
||||||
|
|
||||||
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\adapters.py", line 645, in send
|
||||||
|
resp = conn.urlopen(
|
||||||
|
│ └ <function HTTPConnectionPool.urlopen at 0x000001F29A6854E0>
|
||||||
|
└ <urllib3.connectionpool.HTTPSConnectionPool object at 0x000001F29A814050>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 841, in urlopen
|
||||||
|
retries = retries.increment(
|
||||||
|
│ └ <function Retry.increment at 0x000001F29A46DEE0>
|
||||||
|
└ Retry(total=0, connect=None, read=False, redirect=None, status=None)
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\util\retry.py", line 490, in increment
|
||||||
|
raise reraise(type(error), error, _stacktrace)
|
||||||
|
│ │ │ └ <traceback object at 0x000001F298640380>
|
||||||
|
│ │ └ ProtocolError('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
|
||||||
|
│ └ ProtocolError('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
|
||||||
|
└ <function reraise at 0x000001F29A46C5E0>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\util\util.py", line 38, in reraise
|
||||||
|
raise value.with_traceback(tb)
|
||||||
|
│ └ None
|
||||||
|
└ None
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
|
||||||
|
response = self._make_request(
|
||||||
|
│ └ <function HTTPConnectionPool._make_request at 0x000001F29A685300>
|
||||||
|
└ <urllib3.connectionpool.HTTPSConnectionPool object at 0x000001F29A814050>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 534, in _make_request
|
||||||
|
response = conn.getresponse()
|
||||||
|
│ └ <function HTTPConnection.getresponse at 0x000001F29A6551C0>
|
||||||
|
└ <HTTPSConnection(host='lp.vk.com', port=443) at 0x1f29a814d10>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connection.py", line 571, in getresponse
|
||||||
|
httplib_response = super().getresponse()
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 1374, in getresponse
|
||||||
|
response.begin()
|
||||||
|
│ └ <function HTTPResponse.begin at 0x000001F29A55B4C0>
|
||||||
|
└ <http.client.HTTPResponse object at 0x000001F29A4FF4F0>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 318, in begin
|
||||||
|
version, status, reason = self._read_status()
|
||||||
|
│ └ <function HTTPResponse._read_status at 0x000001F29A55B420>
|
||||||
|
└ <http.client.HTTPResponse object at 0x000001F29A4FF4F0>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 287, in _read_status
|
||||||
|
raise RemoteDisconnected("Remote end closed connection without"
|
||||||
|
└ <class 'http.client.RemoteDisconnected'>
|
||||||
|
|
||||||
|
urllib3.exceptions.ProtocolError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
|
||||||
|
|
||||||
|
|
||||||
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
|
||||||
|
File "m:\bot_vk_ikp\vk-sales-bot\main.py", line 28, in <module>
|
||||||
|
main()
|
||||||
|
└ <function main at 0x000001F2FEC99EE0>
|
||||||
|
|
||||||
|
> File "m:\bot_vk_ikp\vk-sales-bot\main.py", line 20, in main
|
||||||
|
bot.run()
|
||||||
|
│ └ <function VKBot.run at 0x000001F29A7D77E0>
|
||||||
|
└ <services.vk_bot.VKBot object at 0x000001F29A6BA5D0>
|
||||||
|
|
||||||
|
File "m:\bot_vk_ikp\vk-sales-bot\services\vk_bot.py", line 90, in run
|
||||||
|
for event in self.longpoll.listen():
|
||||||
|
│ │ │ └ <function VkBotLongPoll.listen at 0x000001F29A7D4180>
|
||||||
|
│ │ └ <vk_api.bot_longpoll.VkBotLongPoll object at 0x000001F29A7AF160>
|
||||||
|
│ └ <services.vk_bot.VKBot object at 0x000001F29A6BA5D0>
|
||||||
|
└ <<class 'vk_api.bot_longpoll.VkBotMessageEvent'>({'group_id': 233127658, 'type': 'message_new', 'event_id': 'a8c630d5264c1fc3...
|
||||||
|
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\vk_api\bot_longpoll.py", line 286, in listen
|
||||||
|
for event in self.check():
|
||||||
|
│ │ └ <function VkBotLongPoll.check at 0x000001F29A7D40E0>
|
||||||
|
│ └ <vk_api.bot_longpoll.VkBotLongPoll object at 0x000001F29A7AF160>
|
||||||
|
└ <<class 'vk_api.bot_longpoll.VkBotMessageEvent'>({'group_id': 233127658, 'type': 'message_new', 'event_id': 'a8c630d5264c1fc3...
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\vk_api\bot_longpoll.py", line 255, in check
|
||||||
|
response = self.session.get(
|
||||||
|
│ └ <member 'session' of 'VkBotLongPoll' objects>
|
||||||
|
└ <vk_api.bot_longpoll.VkBotLongPoll object at 0x000001F29A7AF160>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\sessions.py", line 605, in get
|
||||||
|
return self.request("GET", url, **kwargs)
|
||||||
|
│ │ │ └ {'params': {'act': 'a_check', 'key': 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJxdWV1ZV9pZCI6IjIzMzEyNzY1OCIsInVudGlsIjoxNzc3OD...
|
||||||
|
│ │ └ 'https://lp.vk.com/whp/233127658'
|
||||||
|
│ └ <function Session.request at 0x000001F29A7BF4C0>
|
||||||
|
└ <requests.sessions.Session object at 0x000001F29A7E9E90>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\sessions.py", line 592, in request
|
||||||
|
resp = self.send(prep, **send_kwargs)
|
||||||
|
│ │ │ └ {'timeout': 35, 'allow_redirects': True, 'proxies': OrderedDict(), 'stream': False, 'verify': True, 'cert': None}
|
||||||
|
│ │ └ <PreparedRequest [GET]>
|
||||||
|
│ └ <function Session.send at 0x000001F29A7BF9C0>
|
||||||
|
└ <requests.sessions.Session object at 0x000001F29A7E9E90>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\sessions.py", line 706, in send
|
||||||
|
r = adapter.send(request, **kwargs)
|
||||||
|
│ │ │ └ {'timeout': 35, 'proxies': OrderedDict(), 'stream': False, 'verify': True, 'cert': None}
|
||||||
|
│ │ └ <PreparedRequest [GET]>
|
||||||
|
│ └ <function HTTPAdapter.send at 0x000001F29A7BED40>
|
||||||
|
└ <requests.adapters.HTTPAdapter object at 0x000001F29A7EA390>
|
||||||
|
File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\adapters.py", line 660, in send
|
||||||
|
raise ConnectionError(err, request=request)
|
||||||
|
│ └ <PreparedRequest [GET]>
|
||||||
|
└ <class 'requests.exceptions.ConnectionError'>
|
||||||
|
|
||||||
|
requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
|
||||||
|
2026-05-04T15:32:36.840163+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T15:32:37.085881+0300 | INFO | Bot started
|
||||||
|
2026-05-04T15:32:37.086882+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T15:32:43.889888+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет
|
||||||
|
2026-05-04T15:32:43.891134+0300 | INFO | Processed message from 25076348: привет
|
||||||
|
2026-05-04T15:32:43.891134+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None ===
|
||||||
|
2026-05-04T15:32:43.894134+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-04T15:32:47.508547+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-04T15:32:47.508547+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-04T15:32:47.508547+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-04T15:32:47.508547+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-04T15:32:55.451299+0300 | DEBUG | Message from 25076348 in peer 2000000002: ааа ппп
|
||||||
|
2026-05-04T15:32:55.451299+0300 | INFO | Processed message from 25076348: ааа ппп
|
||||||
|
2026-05-04T15:32:55.451299+0300 | INFO | === HANDLE: user 25076348, text='ааа ппп', current state=collect_fi ===
|
||||||
|
2026-05-04T15:32:55.453302+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T15:32:59.095014+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T15:32:59.095014+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T15:32:59.095014+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T15:32:59.095014+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T15:32:59.096013+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T15:32:59.096013+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T15:32:59.098012+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T15:33:02.913004+0300 | DEBUG | Message from 25076348 in peer 2000000002: 23
|
||||||
|
2026-05-04T15:33:02.914007+0300 | INFO | Processed message from 25076348: 23
|
||||||
|
2026-05-04T15:33:02.914007+0300 | INFO | === HANDLE: user 25076348, text='23', current state=collect_age ===
|
||||||
|
2026-05-04T15:33:02.914007+0300 | DEBUG | validate_age input: ''23''
|
||||||
|
2026-05-04T15:33:02.914007+0300 | DEBUG | cleaned: '23'
|
||||||
|
2026-05-04T15:33:02.914007+0300 | DEBUG | age valid: 23
|
||||||
|
2026-05-04T15:33:02.915005+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:09:52.845335+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T18:09:53.046373+0300 | INFO | Bot started
|
||||||
|
2026-05-04T18:09:53.047373+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T18:10:10.241270+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет
|
||||||
|
2026-05-04T18:10:10.242269+0300 | INFO | Processed message from 25076348: привет
|
||||||
|
2026-05-04T18:10:10.242269+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None ===
|
||||||
|
2026-05-04T18:10:10.245270+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-04T18:10:13.272789+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-04T18:10:13.273787+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-04T18:10:13.273787+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-04T18:10:13.273787+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-04T18:10:18.694847+0300 | DEBUG | Message from 25076348 in peer 2000000002: аа рп
|
||||||
|
2026-05-04T18:10:18.694847+0300 | INFO | Processed message from 25076348: аа рп
|
||||||
|
2026-05-04T18:10:18.694847+0300 | INFO | === HANDLE: user 25076348, text='аа рп', current state=collect_fi ===
|
||||||
|
2026-05-04T18:10:18.696350+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:10:21.671256+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T18:10:21.671256+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T18:10:21.672255+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T18:10:21.672255+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T18:10:21.672255+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T18:10:21.672255+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T18:10:21.672255+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:12:55.999161+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T18:12:56.620865+0300 | INFO | Bot started
|
||||||
|
2026-05-04T18:12:56.620865+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T18:13:16.401304+0300 | DEBUG | Message from 25076348 in peer 2000000002: 5
|
||||||
|
2026-05-04T18:13:16.402310+0300 | INFO | Processed message from 25076348: 5
|
||||||
|
2026-05-04T18:13:16.402310+0300 | INFO | === HANDLE: user 25076348, text='5', current state=None ===
|
||||||
|
2026-05-04T18:13:16.402310+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-04T18:13:19.498303+0300 | DEBUG | Message from 25076348 in peer 2000000002: всыв
|
||||||
|
2026-05-04T18:13:19.498303+0300 | INFO | Processed message from 25076348: всыв
|
||||||
|
2026-05-04T18:13:19.498303+0300 | INFO | === HANDLE: user 25076348, text='всыв', current state=ask_consent ===
|
||||||
|
2026-05-04T18:13:19.498303+0300 | INFO | Response to user 25076348: Пожалуйста, ответьте 'Да' (согласен) или 'Нет' (не согласен).
|
||||||
|
2026-05-04T18:13:22.869492+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-04T18:13:22.869492+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-04T18:13:22.870492+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-04T18:13:22.870492+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-04T18:13:30.113454+0300 | DEBUG | Message from 25076348 in peer 2000000002: пав пав
|
||||||
|
2026-05-04T18:13:30.113454+0300 | INFO | Processed message from 25076348: пав пав
|
||||||
|
2026-05-04T18:13:30.113454+0300 | INFO | === HANDLE: user 25076348, text='пав пав', current state=collect_fi ===
|
||||||
|
2026-05-04T18:13:30.114454+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:13:32.914351+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T18:13:32.915351+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T18:13:32.915351+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T18:13:32.915351+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T18:13:32.915351+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T18:13:32.915351+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T18:13:32.915351+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:13:36.111171+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T18:13:36.111171+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T18:13:36.111171+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T18:13:36.112171+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T18:13:36.112171+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T18:13:36.112171+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T18:13:36.112171+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:13:43.488167+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12лет
|
||||||
|
2026-05-04T18:13:43.488167+0300 | INFO | Processed message from 25076348: 12лет
|
||||||
|
2026-05-04T18:13:43.488167+0300 | INFO | === HANDLE: user 25076348, text='12лет', current state=collect_age ===
|
||||||
|
2026-05-04T18:13:43.489166+0300 | DEBUG | validate_age input: ''12лет''
|
||||||
|
2026-05-04T18:13:43.489166+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T18:13:43.489166+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T18:13:43.489166+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:13:48.007000+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 лет
|
||||||
|
2026-05-04T18:13:48.007000+0300 | INFO | Processed message from 25076348: 12 лет
|
||||||
|
2026-05-04T18:13:48.007000+0300 | INFO | === HANDLE: user 25076348, text='12 лет', current state=collect_age ===
|
||||||
|
2026-05-04T18:13:48.008153+0300 | DEBUG | validate_age input: ''12 лет''
|
||||||
|
2026-05-04T18:13:48.008153+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T18:13:48.008153+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T18:13:48.008153+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:13:51.898194+0300 | DEBUG | Message from 25076348 in peer 2000000002: 1
|
||||||
|
2026-05-04T18:13:51.898194+0300 | INFO | Processed message from 25076348: 1
|
||||||
|
2026-05-04T18:13:51.899194+0300 | INFO | === HANDLE: user 25076348, text='1', current state=collect_age ===
|
||||||
|
2026-05-04T18:13:51.899194+0300 | DEBUG | validate_age input: ''1''
|
||||||
|
2026-05-04T18:13:51.899194+0300 | DEBUG | cleaned: '1'
|
||||||
|
2026-05-04T18:13:51.899194+0300 | DEBUG | age valid: 1
|
||||||
|
2026-05-04T18:13:51.899194+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:13:55.621492+0300 | DEBUG | Message from 25076348 in peer 2000000002: 120
|
||||||
|
2026-05-04T18:13:55.622492+0300 | INFO | Processed message from 25076348: 120
|
||||||
|
2026-05-04T18:13:55.622492+0300 | INFO | === HANDLE: user 25076348, text='120', current state=collect_age ===
|
||||||
|
2026-05-04T18:13:55.622492+0300 | DEBUG | validate_age input: ''120''
|
||||||
|
2026-05-04T18:13:55.622492+0300 | DEBUG | cleaned: '120'
|
||||||
|
2026-05-04T18:13:55.622492+0300 | DEBUG | age valid: 120
|
||||||
|
2026-05-04T18:13:55.622492+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:14:00.545873+0300 | DEBUG | Message from 25076348 in peer 2000000002: 158
|
||||||
|
2026-05-04T18:14:00.545873+0300 | INFO | Processed message from 25076348: 158
|
||||||
|
2026-05-04T18:14:00.545873+0300 | INFO | === HANDLE: user 25076348, text='158', current state=collect_age ===
|
||||||
|
2026-05-04T18:14:00.545873+0300 | DEBUG | validate_age input: ''158''
|
||||||
|
2026-05-04T18:14:00.545873+0300 | DEBUG | cleaned: '158'
|
||||||
|
2026-05-04T18:14:00.546870+0300 | INFO | Response to user 25076348: Возраст должен быть от 1 до 120 лет.
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T18:14:29.195477+0300 | DEBUG | Message from 25076348 in peer 2000000002: 11
|
||||||
|
2026-05-04T18:14:29.195477+0300 | INFO | Processed message from 25076348: 11
|
||||||
|
2026-05-04T18:14:29.196478+0300 | INFO | === HANDLE: user 25076348, text='11', current state=collect_age ===
|
||||||
|
2026-05-04T18:14:29.196478+0300 | DEBUG | validate_age input: ''11''
|
||||||
|
2026-05-04T18:14:29.196478+0300 | DEBUG | cleaned: '11'
|
||||||
|
2026-05-04T18:14:29.196478+0300 | DEBUG | age valid: 11
|
||||||
|
2026-05-04T18:14:29.196478+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T21:42:30.942354+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T21:42:31.199461+0300 | INFO | Bot started
|
||||||
|
2026-05-04T21:42:31.200460+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T21:43:03.630075+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-04T21:43:04.103937+0300 | INFO | Bot started
|
||||||
|
2026-05-04T21:43:04.104937+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-04T21:43:32.010181+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет
|
||||||
|
2026-05-04T21:43:32.012181+0300 | INFO | Processed message from 25076348: привет
|
||||||
|
2026-05-04T21:43:32.012181+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None ===
|
||||||
|
2026-05-04T21:43:32.015181+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-04T21:43:34.178176+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-04T21:43:34.178176+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-04T21:43:34.178176+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-04T21:43:34.178176+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-04T21:43:40.704112+0300 | DEBUG | Message from 25076348 in peer 2000000002: ааа ппп
|
||||||
|
2026-05-04T21:43:40.704112+0300 | INFO | Processed message from 25076348: ааа ппп
|
||||||
|
2026-05-04T21:43:40.704112+0300 | INFO | === HANDLE: user 25076348, text='ааа ппп', current state=collect_fi ===
|
||||||
|
2026-05-04T21:43:40.706111+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T21:43:43.405452+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T21:43:43.405452+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T21:43:43.406451+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T21:43:43.406451+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T21:43:43.406451+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T21:43:43.406451+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T21:43:43.407452+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T21:43:53.242075+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-04T21:43:53.243076+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-04T21:43:53.243076+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-04T21:43:53.243076+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-04T21:43:53.243076+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T21:43:53.243076+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T21:43:53.243076+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-04T21:44:00.659922+0300 | DEBUG | Message from 25076348 in peer 2000000002: "12"
|
||||||
|
2026-05-04T21:44:00.659922+0300 | INFO | Processed message from 25076348: "12"
|
||||||
|
2026-05-04T21:44:00.659922+0300 | INFO | === HANDLE: user 25076348, text='"12"', current state=collect_age ===
|
||||||
|
2026-05-04T21:44:00.659922+0300 | DEBUG | validate_age input: ''"12"''
|
||||||
|
2026-05-04T21:44:00.659922+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-04T21:44:00.660921+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-04T21:44:00.660921+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-05T07:00:09.347195+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-05T07:00:09.569224+0300 | INFO | Bot started
|
||||||
|
2026-05-05T07:00:09.570223+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-05T07:00:20.263695+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-05T07:00:20.447752+0300 | INFO | Bot started
|
||||||
|
2026-05-05T07:00:20.447752+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-05T07:00:51.798725+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет
|
||||||
|
2026-05-05T07:00:51.798725+0300 | INFO | Processed message from 25076348: привет
|
||||||
|
2026-05-05T07:00:51.798725+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None ===
|
||||||
|
2026-05-05T07:00:51.799725+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-05T07:00:55.300865+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-05T07:00:55.300865+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-05T07:00:55.301864+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-05T07:00:55.301864+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-05T07:01:00.589119+0300 | DEBUG | Message from 25076348 in peer 2000000002: ааа ппп
|
||||||
|
2026-05-05T07:01:00.589119+0300 | INFO | Processed message from 25076348: ааа ппп
|
||||||
|
2026-05-05T07:01:00.590119+0300 | INFO | === HANDLE: user 25076348, text='ааа ппп', current state=collect_fi ===
|
||||||
|
2026-05-05T07:01:00.590119+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-05T07:01:03.015859+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-05T07:01:03.015859+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-05T07:01:03.015859+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-05T07:01:03.016362+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-05T07:01:03.016362+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-05T07:01:03.016362+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-05T07:01:03.016362+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-05T07:01:04.461068+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12
|
||||||
|
2026-05-05T07:01:04.461068+0300 | INFO | Processed message from 25076348: 12
|
||||||
|
2026-05-05T07:01:04.461068+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age ===
|
||||||
|
2026-05-05T07:01:04.461068+0300 | DEBUG | validate_age input: ''12''
|
||||||
|
2026-05-05T07:01:04.462069+0300 | DEBUG | cleaned: '12'
|
||||||
|
2026-05-05T07:01:04.462069+0300 | DEBUG | age valid: 12
|
||||||
|
2026-05-05T07:01:04.462069+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-05T07:01:06.186293+0300 | DEBUG | Message from 25076348 in peer 2000000002: 31
|
||||||
|
2026-05-05T07:01:06.186798+0300 | INFO | Processed message from 25076348: 31
|
||||||
|
2026-05-05T07:01:06.186798+0300 | INFO | === HANDLE: user 25076348, text='31', current state=collect_age ===
|
||||||
|
2026-05-05T07:01:06.186798+0300 | DEBUG | validate_age input: ''31''
|
||||||
|
2026-05-05T07:01:06.186798+0300 | DEBUG | cleaned: '31'
|
||||||
|
2026-05-05T07:01:06.186798+0300 | DEBUG | age valid: 31
|
||||||
|
2026-05-05T07:01:06.187799+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-05T07:01:08.344400+0300 | DEBUG | Message from 25076348 in peer 2000000002: 41
|
||||||
|
2026-05-05T07:01:08.344400+0300 | INFO | Processed message from 25076348: 41
|
||||||
|
2026-05-05T07:01:08.344400+0300 | INFO | === HANDLE: user 25076348, text='41', current state=collect_age ===
|
||||||
|
2026-05-05T07:01:08.344400+0300 | DEBUG | validate_age input: ''41''
|
||||||
|
2026-05-05T07:01:08.344400+0300 | DEBUG | cleaned: '41'
|
||||||
|
2026-05-05T07:01:08.344400+0300 | DEBUG | age valid: 41
|
||||||
|
2026-05-05T07:01:08.345902+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст).
|
||||||
|
Укажите ваш возраст (целое число лет):
|
||||||
|
2026-05-05T08:33:06.946148+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-05T08:33:07.210217+0300 | INFO | Bot started
|
||||||
|
2026-05-05T08:33:07.211222+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-05T08:33:29.318049+0300 | DEBUG | Message from 25076348 in peer 2000000002: авсыв
|
||||||
|
2026-05-05T08:33:29.319049+0300 | INFO | Processed message from 25076348: авсыв
|
||||||
|
2026-05-05T08:33:29.319049+0300 | INFO | === HANDLE: user 25076348, text='авсыв', current state=None ===
|
||||||
|
2026-05-05T08:33:29.321050+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-05T08:33:34.372307+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-05T08:33:34.372307+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-05T08:33:34.372307+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-05T08:33:34.372307+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-05T08:33:39.200486+0300 | DEBUG | Message from 25076348 in peer 2000000002: аваыв павпва
|
||||||
|
2026-05-05T08:33:39.200486+0300 | INFO | Processed message from 25076348: аваыв павпва
|
||||||
|
2026-05-05T08:33:39.200486+0300 | INFO | === HANDLE: user 25076348, text='аваыв павпва', current state=collect_fi ===
|
||||||
|
2026-05-05T08:33:39.201486+0300 | INFO | Response to user 25076348: 👪 Укажите, пожалуйста, контактные данные родителя или опекуна. Вы можете предоставить их сейчас или
|
||||||
|
2026-05-05T08:34:08.120612+0300 | DEBUG | Message from 25076348 in peer 2000000002: пропустить
|
||||||
|
2026-05-05T08:34:08.121613+0300 | INFO | Processed message from 25076348: пропустить
|
||||||
|
2026-05-05T08:34:08.121613+0300 | INFO | === HANDLE: user 25076348, text='пропустить', current state=ask_parent_data ===
|
||||||
|
2026-05-05T08:34:08.121613+0300 | INFO | Parent data skipped for user 25076348
|
||||||
|
2026-05-05T08:34:08.121613+0300 | INFO | Response to user 25076348: ✅ Данные родителя пропущены. Продолжаем оформление.
|
||||||
|
Отлично! Теперь укажите ваш номер телефона в фор
|
||||||
|
2026-05-05T08:34:34.619625+0300 | DEBUG | Message from 25076348 in peer 2000000002: +7(900)000-00-00
|
||||||
|
2026-05-05T08:34:34.619625+0300 | INFO | Processed message from 25076348: +7(900)000-00-00
|
||||||
|
2026-05-05T08:34:34.619625+0300 | INFO | === HANDLE: user 25076348, text='+7(900)000-00-00', current state=collect_phone ===
|
||||||
|
2026-05-05T08:34:34.619625+0300 | INFO | Response to user 25076348: Укажите удобное время по МСК для звонка (например: 'завтра после 18', 'в среду утром', 'сегодня в 15
|
||||||
|
2026-05-05T08:34:52.867495+0300 | DEBUG | Message from 25076348 in peer 2000000002: сегодня в 15:00
|
||||||
|
2026-05-05T08:34:52.867495+0300 | INFO | Processed message from 25076348: сегодня в 15:00
|
||||||
|
2026-05-05T08:34:52.867495+0300 | INFO | === HANDLE: user 25076348, text='сегодня в 15:00', current state=collect_time ===
|
||||||
|
2026-05-05T08:34:54.349050+0300 | INFO | Response to user 25076348: Проверьте данные:
|
||||||
|
ФИ: аваыв павпва
|
||||||
|
Телефон: +7 (900) 000-00-00
|
||||||
|
Время звонка по МСК: 05.05.2026 в 15:
|
||||||
|
2026-05-05T08:34:58.958675+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-05T08:34:58.958675+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-05T08:34:58.958675+0300 | INFO | === HANDLE: user 25076348, text='да', current state=collect_time ===
|
||||||
|
2026-05-05T08:34:59.533476+0300 | INFO | Response to user 25076348: Не удалось распознать время. Укажите, например: 'завтра после 18', 'в среду утром' или 'любое время'
|
||||||
|
2026-05-05T08:35:39.028252+0300 | DEBUG | Message from 25076348 in peer 2000000002: сегодня в 15:00
|
||||||
|
2026-05-05T08:35:39.028252+0300 | INFO | Processed message from 25076348: сегодня в 15:00
|
||||||
|
2026-05-05T08:35:39.028252+0300 | INFO | === HANDLE: user 25076348, text='сегодня в 15:00', current state=collect_time ===
|
||||||
|
2026-05-05T08:35:39.030760+0300 | INFO | Response to user 25076348: Проверьте данные:
|
||||||
|
ФИ: аваыв павпва
|
||||||
|
Телефон: +7 (900) 000-00-00
|
||||||
|
Время звонка по МСК: 05.05.2026 в 15:
|
||||||
|
2026-05-05T08:35:46.969686+0300 | DEBUG | Message from 25076348 in peer 2000000002: Да
|
||||||
|
2026-05-05T08:35:46.969686+0300 | INFO | Processed message from 25076348: Да
|
||||||
|
2026-05-05T08:35:46.970686+0300 | INFO | === HANDLE: user 25076348, text='Да', current state=collect_time ===
|
||||||
|
2026-05-05T08:35:46.974190+0300 | INFO | Response to user 25076348: Не удалось распознать время. Укажите, например: 'завтра после 18', 'в среду утром' или 'любое время'
|
||||||
|
2026-05-05T09:07:22.565746+0300 | INFO | Initializing bot...
|
||||||
|
2026-05-05T09:07:22.797990+0300 | INFO | Bot started
|
||||||
|
2026-05-05T09:07:22.798991+0300 | INFO | Backup scheduler started
|
||||||
|
2026-05-05T09:07:25.017845+0300 | DEBUG | Message from 25076348 in peer 2000000002: авп
|
||||||
|
2026-05-05T09:07:25.017845+0300 | INFO | Processed message from 25076348: авп
|
||||||
|
2026-05-05T09:07:25.018845+0300 | INFO | === HANDLE: user 25076348, text='авп', current state=None ===
|
||||||
|
2026-05-05T09:07:25.018845+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет):
|
||||||
|
2026-05-05T09:07:29.353564+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-05T09:07:29.354564+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-05T09:07:29.354564+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent ===
|
||||||
|
2026-05-05T09:07:29.354564+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки.
|
||||||
|
Пожалуйста, укажите вашу фамилию и имя (два слов
|
||||||
|
2026-05-05T09:07:36.689785+0300 | DEBUG | Message from 25076348 in peer 2000000002: ваыва авп
|
||||||
|
2026-05-05T09:07:36.689785+0300 | INFO | Processed message from 25076348: ваыва авп
|
||||||
|
2026-05-05T09:07:36.689785+0300 | INFO | === HANDLE: user 25076348, text='ваыва авп', current state=collect_fi ===
|
||||||
|
2026-05-05T09:07:36.689785+0300 | INFO | Response to user 25076348: 👪 Укажите, пожалуйста, контактные данные родителя или опекуна. Вы можете предоставить их сейчас или
|
||||||
|
2026-05-05T09:07:47.065944+0300 | DEBUG | Message from 25076348 in peer 2000000002: пропустить
|
||||||
|
2026-05-05T09:07:47.065944+0300 | INFO | Processed message from 25076348: пропустить
|
||||||
|
2026-05-05T09:07:47.065944+0300 | INFO | === HANDLE: user 25076348, text='пропустить', current state=ask_parent_data ===
|
||||||
|
2026-05-05T09:07:47.066947+0300 | INFO | Parent data skipped for user 25076348
|
||||||
|
2026-05-05T09:07:47.066947+0300 | INFO | Response to user 25076348: ✅ Данные родителя пропущены. Продолжаем оформление.
|
||||||
|
Отлично! Теперь укажите ваш номер телефона в фор
|
||||||
|
2026-05-05T09:08:13.305154+0300 | DEBUG | Message from 25076348 in peer 2000000002: +7(999)000-00-00
|
||||||
|
2026-05-05T09:08:13.306155+0300 | INFO | Processed message from 25076348: +7(999)000-00-00
|
||||||
|
2026-05-05T09:08:13.306155+0300 | INFO | === HANDLE: user 25076348, text='+7(999)000-00-00', current state=collect_phone ===
|
||||||
|
2026-05-05T09:08:13.306155+0300 | INFO | Response to user 25076348: Укажите удобное время по МСК для звонка (например: 'завтра после 18', 'в среду утром', 'сегодня в 15
|
||||||
|
2026-05-05T09:08:26.129071+0300 | DEBUG | Message from 25076348 in peer 2000000002: сегодня в 15:00
|
||||||
|
2026-05-05T09:08:26.130574+0300 | INFO | Processed message from 25076348: сегодня в 15:00
|
||||||
|
2026-05-05T09:08:26.130574+0300 | INFO | === HANDLE: user 25076348, text='сегодня в 15:00', current state=collect_time ===
|
||||||
|
2026-05-05T09:08:27.594462+0300 | INFO | Response to user 25076348: Проверьте данные:
|
||||||
|
ФИ: ваыва авп
|
||||||
|
Телефон: +7 (999) 000-00-00
|
||||||
|
Время звонка по МСК: 05.05.2026 в 15:00
|
||||||
|
|
||||||
|
2026-05-05T09:08:31.676242+0300 | DEBUG | Message from 25076348 in peer 2000000002: [club233127658|@ikpro] ✅ Да
|
||||||
|
2026-05-05T09:08:31.677239+0300 | INFO | Processed message from 25076348: [club233127658|@ikpro] ✅ Да
|
||||||
|
2026-05-05T09:08:31.677239+0300 | INFO | === HANDLE: user 25076348, text='[club233127658|@ikpro] ✅ Да', current state=confirm ===
|
||||||
|
2026-05-05T09:08:31.677239+0300 | INFO | Response to user 25076348: Пожалуйста, ответьте 'Да' или 'Нет'.
|
||||||
|
2026-05-05T09:08:38.235869+0300 | DEBUG | Message from 25076348 in peer 2000000002: да
|
||||||
|
2026-05-05T09:08:38.235869+0300 | INFO | Processed message from 25076348: да
|
||||||
|
2026-05-05T09:08:38.236871+0300 | INFO | === HANDLE: user 25076348, text='да', current state=confirm ===
|
||||||
|
2026-05-05T09:08:38.653742+0300 | INFO | Lead saved for user 25076348
|
||||||
|
2026-05-05T09:08:38.653742+0300 | INFO | Response to user 25076348: Спасибо! Ваши данные сохранены. Менеджер свяжется с вами в указанное время. Хорошего дня! 🌟
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from utils.logger import logger
|
||||||
|
from core.exporter import ExcelExporter
|
||||||
|
from core.fsm import DialogManager
|
||||||
|
from services.vk_bot import VKBot
|
||||||
|
from config import settings
|
||||||
|
from utils.middleware import logging_middleware
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Точка входа в приложение"""
|
||||||
|
logger.info("Initializing bot...")
|
||||||
|
exporter = ExcelExporter(settings.LEADS_FILE)
|
||||||
|
fsm = DialogManager(exporter)
|
||||||
|
bot = VKBot(fsm, exporter)
|
||||||
|
|
||||||
|
# Добавляем мидлвары (можно расширить)
|
||||||
|
bot.middlewares.add(logging_middleware)
|
||||||
|
|
||||||
|
try:
|
||||||
|
bot.run()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Bot stopped")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Fatal error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
vk_api==11.9.9
|
||||||
|
pandas==2.2.0
|
||||||
|
openpyxl==3.1.2
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
loguru==0.7.2
|
||||||
|
pydantic==2.5.3
|
||||||
|
dateparser==1.2.0
|
||||||
|
filelock==3.13.1
|
||||||
|
schedule==1.2.0
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,127 @@
|
|||||||
|
import vk_api
|
||||||
|
from vk_api.bot_longpoll import VkBotLongPoll, VkBotEventType
|
||||||
|
from vk_api.keyboard import VkKeyboard, VkKeyboardColor
|
||||||
|
from loguru import logger
|
||||||
|
from config import settings, phrases
|
||||||
|
from core.fsm import DialogManager
|
||||||
|
from core.exporter import ExcelExporter
|
||||||
|
from utils.backup import schedule_daily_backup
|
||||||
|
from utils.middleware import MiddlewareChain
|
||||||
|
from vk_api.exceptions import ApiError
|
||||||
|
import requests.exceptions
|
||||||
|
|
||||||
|
class VKBot:
|
||||||
|
def __init__(self, fsm: DialogManager, exporter: ExcelExporter):
|
||||||
|
self.fsm = fsm
|
||||||
|
self.exporter = exporter
|
||||||
|
self.vk_session = vk_api.VkApi(token=settings.VK_TOKEN)
|
||||||
|
self.longpoll = VkBotLongPoll(self.vk_session, settings.VK_GROUP_ID)
|
||||||
|
self.vk = self.vk_session.get_api()
|
||||||
|
self.middlewares = MiddlewareChain()
|
||||||
|
|
||||||
|
def _send_message(self, peer_id: int, text: str, keyboard=None):
|
||||||
|
"""Отправка сообщения с возможной клавиатурой"""
|
||||||
|
try:
|
||||||
|
self.vk.messages.send(
|
||||||
|
peer_id=peer_id,
|
||||||
|
message=text,
|
||||||
|
random_id=0,
|
||||||
|
keyboard=keyboard.get_keyboard() if keyboard else None
|
||||||
|
)
|
||||||
|
|
||||||
|
except ApiError as e:
|
||||||
|
if e.code == 901:
|
||||||
|
logger.warning(f"Can't send to peer {peer_id}: {e}")
|
||||||
|
# Здесь можно отправить уведомление администратору или сохранить ID
|
||||||
|
else:
|
||||||
|
logger.error(f"VK API error {e.code} for peer {peer_id}: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to send message to {peer_id}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _get_keyboard_for_state(self, state):
|
||||||
|
"""Генерирует клавиатуру в зависимости от состояния"""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
if state == "ASK_CONSENT": # <-- новый блок
|
||||||
|
keyboard.add_button(phrases.BUTTON_CONSENT_YES, color=VkKeyboardColor.POSITIVE)
|
||||||
|
keyboard.add_button(phrases.BUTTON_CONSENT_NO, color=VkKeyboardColor.NEGATIVE)
|
||||||
|
elif state in ("CONFIRM", "confirm"):
|
||||||
|
keyboard.add_button(phrases.BUTTON_YES, color=VkKeyboardColor.POSITIVE)
|
||||||
|
keyboard.add_button(phrases.BUTTON_NO, color=VkKeyboardColor.NEGATIVE)
|
||||||
|
else:
|
||||||
|
keyboard.add_button(phrases.BUTTON_RESTART, color=VkKeyboardColor.SECONDARY)
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
def _handle_command(self, user_id: int, text: str) -> bool:
|
||||||
|
"""Обработка команд администрирования"""
|
||||||
|
if user_id not in settings.ADMIN_IDS:
|
||||||
|
return False
|
||||||
|
if text == phrases.CMD_STATUS:
|
||||||
|
self._send_message(user_id, "✅ Бот работает и принимает сообщения.")
|
||||||
|
return True
|
||||||
|
elif text == phrases.CMD_EXPORT:
|
||||||
|
path = self.exporter.export()
|
||||||
|
self._send_message(user_id, f"Экспорт создан: {path}")
|
||||||
|
return True
|
||||||
|
elif text == phrases.CMD_BACKUP:
|
||||||
|
self.exporter.backup()
|
||||||
|
self._send_message(user_id, "Резервная копия создана.")
|
||||||
|
return True
|
||||||
|
elif text == phrases.CMD_STATS:
|
||||||
|
stats = self.exporter.get_stats()
|
||||||
|
msg = (f"📊 Статистика:\nВсего лидов: {stats['total']}\n"
|
||||||
|
f"За сегодня: {stats['today']}\nСтатусы: {stats['statuses']}")
|
||||||
|
self._send_message(user_id, msg)
|
||||||
|
return True
|
||||||
|
elif text == phrases.CMD_RELOAD:
|
||||||
|
import importlib
|
||||||
|
import config.phrases
|
||||||
|
importlib.reload(config.phrases)
|
||||||
|
# Обновляем ссылку на phrases в текущем модуле
|
||||||
|
globals()['phrases'] = config.phrases
|
||||||
|
self._send_message(user_id, "Конфигурация перезагружена.")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
logger.info("Bot started")
|
||||||
|
schedule_daily_backup(self.exporter)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
for event in self.longpoll.listen():
|
||||||
|
try:
|
||||||
|
if event.type == VkBotEventType.MESSAGE_NEW and event.message:
|
||||||
|
user_id = event.message.from_id
|
||||||
|
peer_id = event.message.peer_id
|
||||||
|
text = event.message.text
|
||||||
|
logger.debug(f"Message from {user_id} in peer {peer_id}: {text}")
|
||||||
|
|
||||||
|
if not self.middlewares.process(user_id, text):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._handle_command(user_id, text):
|
||||||
|
continue
|
||||||
|
|
||||||
|
response = self.fsm.handle_message(user_id, text)
|
||||||
|
logger.info(f"Response to user {user_id}: {response[:100]}")
|
||||||
|
state = self.fsm.sessions.get(user_id, {}).get("state", "")
|
||||||
|
keyboard = self._get_keyboard_for_state(state)
|
||||||
|
self._send_message(peer_id, response, keyboard)
|
||||||
|
|
||||||
|
except ApiError as e:
|
||||||
|
logger.error(f"VK API error while processing event: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing event: {e}", exc_info=True)
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError as e:
|
||||||
|
logger.warning(f"LongPoll connection lost: {e}. Reconnecting in 5 seconds...")
|
||||||
|
import time
|
||||||
|
time.sleep(5)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fatal error in longpoll loop: {e}", exc_info=True)
|
||||||
|
logger.warning("Reconnecting in 10 seconds...")
|
||||||
|
import time
|
||||||
|
time.sleep(10)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""
|
||||||
|
Заглушка для проверки токена VK API.
|
||||||
|
|
||||||
|
Перед использованием:
|
||||||
|
1. Скопируйте .env.example в .env
|
||||||
|
2. Заполните VK_TOKEN и VK_GROUP_ID в .env
|
||||||
|
3. Запустите: python start.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from vk_api import VkApi
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
token = os.getenv("VK_TOKEN")
|
||||||
|
if not token:
|
||||||
|
print("❌ Ошибка: VK_TOKEN не найден в .env файле!")
|
||||||
|
print("Скопируйте .env.example в .env и заполните данные.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
vk_session = VkApi(token=token)
|
||||||
|
try:
|
||||||
|
perms = vk_session.get_api().account.getAppPermissions()
|
||||||
|
print(f"✅ Токен активен. Права: {perms}")
|
||||||
|
print("4096 = messages, 8192 = groups, 12288 = messages + groups")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка проверки токена: {e}")
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import pytest
|
||||||
|
from core.validator import validate_fio, validate_phone, parse_time
|
||||||
|
|
||||||
|
def test_fio_valid():
|
||||||
|
res = validate_fio("Иванов Иван Иванович")
|
||||||
|
assert res.is_valid
|
||||||
|
assert res.value == "Иванов Иван Иванович"
|
||||||
|
|
||||||
|
def test_fio_invalid():
|
||||||
|
res = validate_fio("Иванов Иван")
|
||||||
|
assert not res.is_valid
|
||||||
|
|
||||||
|
def test_phone_valid():
|
||||||
|
res = validate_phone("+7 (912) 345-67-89")
|
||||||
|
assert res.is_valid
|
||||||
|
assert "912" in res.value
|
||||||
|
|
||||||
|
def test_phone_clean():
|
||||||
|
res = validate_phone("89123456789")
|
||||||
|
assert res.is_valid
|
||||||
|
|
||||||
|
def test_parse_time():
|
||||||
|
res = parse_time("завтра в 15:00")
|
||||||
|
assert res.is_valid
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,29 @@
|
|||||||
|
import schedule
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_daily_backup(exporter):
|
||||||
|
"""Запускает ежесуточное резервное копирование в отдельном потоке"""
|
||||||
|
|
||||||
|
def job():
|
||||||
|
try:
|
||||||
|
exporter.backup()
|
||||||
|
logger.info("Daily backup executed successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Backup failed: {e}")
|
||||||
|
|
||||||
|
schedule.every().day.at("03:00").do(job)
|
||||||
|
|
||||||
|
def run_scheduler():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
schedule.run_pending()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Scheduler error: {e}")
|
||||||
|
time.sleep(30) # Уменьшил интервал с 60 до 30 секунд для более точного срабатывания
|
||||||
|
|
||||||
|
thread = threading.Thread(target=run_scheduler, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
logger.info("Backup scheduler started")
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from loguru import logger
|
||||||
|
import sys
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
logger.remove()
|
||||||
|
logger.add(
|
||||||
|
sys.stdout,
|
||||||
|
format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan> - {message}",
|
||||||
|
level=settings.LOG_LEVEL
|
||||||
|
)
|
||||||
|
logger.add(
|
||||||
|
"logs/bot.log",
|
||||||
|
rotation="1 day",
|
||||||
|
retention="30 days",
|
||||||
|
format="{time} | {level} | {message}",
|
||||||
|
level="DEBUG"
|
||||||
|
)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from loguru import logger
|
||||||
|
from typing import Dict, List
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class MiddlewareChain:
|
||||||
|
"""Простая цепочка мидлваров (логирование, антиспам, аналитика)"""
|
||||||
|
def __init__(self):
|
||||||
|
self.middlewares: List[callable] = []
|
||||||
|
|
||||||
|
def add(self, middleware: callable):
|
||||||
|
self.middlewares.append(middleware)
|
||||||
|
|
||||||
|
def process(self, user_id: int, text: str) -> bool:
|
||||||
|
for mw in self.middlewares:
|
||||||
|
if not mw(user_id, text):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Антиспам-фильтр ====================
|
||||||
|
|
||||||
|
class SpamFilter:
|
||||||
|
"""Простой антиспам-фильтр: блокирует пользователя, если он отправляет
|
||||||
|
более `max_messages` сообщений за `window_seconds` секунд."""
|
||||||
|
|
||||||
|
def __init__(self, max_messages: int = 5, window_seconds: int = 60):
|
||||||
|
self.max_messages = max_messages
|
||||||
|
self.window_seconds = window_seconds
|
||||||
|
# user_id -> [timestamp1, timestamp2, ...]
|
||||||
|
self._messages: Dict[int, List[float]] = {}
|
||||||
|
|
||||||
|
def is_spam(self, user_id: int) -> bool:
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
window_start = now - self.window_seconds
|
||||||
|
|
||||||
|
# Инициализируем список, если нет
|
||||||
|
if user_id not in self._messages:
|
||||||
|
self._messages[user_id] = []
|
||||||
|
|
||||||
|
# Удаляем старые записи за пределами окна
|
||||||
|
self._messages[user_id] = [
|
||||||
|
ts for ts in self._messages[user_id] if ts > window_start
|
||||||
|
]
|
||||||
|
|
||||||
|
# Проверяем, не превышен ли лимит
|
||||||
|
if len(self._messages[user_id]) >= self.max_messages:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Записываем текущее сообщение
|
||||||
|
self._messages[user_id].append(now)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reset(self, user_id: int):
|
||||||
|
"""Сброс счётчика для пользователя (например, при /start)"""
|
||||||
|
self._messages.pop(user_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Логирование ====================
|
||||||
|
|
||||||
|
def logging_middleware(user_id: int, text: str) -> bool:
|
||||||
|
logger.info(f"Processed message from {user_id}: {text}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Пример: мидлвар для аналитики ====================
|
||||||
|
|
||||||
|
def analytics_middleware(user_id: int, text: str) -> bool:
|
||||||
|
"""Заглушка для аналитики — можно расширить позже."""
|
||||||
|
logger.debug(f"Analytics: user {user_id} sent '{text}'")
|
||||||
|
return True
|
||||||
Reference in New Issue
Block a user