Initial clean history
CI/CD / Test (push) Failing after 4m32s
CI/CD / Deploy (push) Has been skipped

This commit is contained in:
User
2026-05-11 16:46:25 +03:00
commit bf89e671d8
33 changed files with 1333 additions and 0 deletions
View File
+169
View File
@@ -0,0 +1,169 @@
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
import re
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 _is_direct_message(self, user_id: int, peer_id: int) -> bool:
return user_id == peer_id
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)
elif state == "START" or state is None:
# Начальное состояние — кнопка "Зарегистрироваться"
keyboard.add_button(phrases.BUTTON_REGISTER, color=VkKeyboardColor.POSITIVE, payload='register_btn')
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
if not self._is_direct_message(user_id, peer_id):
logger.debug(f"Ignoring chat message from {user_id} in peer {peer_id}")
continue
# Очистка текста от VK-ссылок вида [club123456|текст] и [user123|текст]
text = re.sub(r'\[club\d+\|([^]]*)\]', r'\1', text)
text = re.sub(r'\[user\d+\|([^]]*)\]', r'\1', text)
text = re.sub(r'\[id\d+\|([^]]*)\]', r'\1', 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)
# Обработка нажатия callback-кнопки "Зарегистрироваться"
elif event.type == VkBotEventType.MESSAGE_NEW_CALLBACK and event.message:
user_id = event.message.from_id
peer_id = event.message.peer_id
callback_text = event.message.get_text() if event.message.get_text() else ""
if not self._is_direct_message(user_id, peer_id):
logger.debug(f"Ignoring chat callback from {user_id} in peer {peer_id}")
continue
logger.debug(f"Callback from {user_id} in peer {peer_id}: {callback_text}")
if not self.middlewares.process(user_id, callback_text):
continue
# Проверяем, что нажата кнопка "Зарегистрироваться"
if event.message.get_payload() and 'register_btn' in str(event.message.get_payload()):
# Начинаем диалог заново
response = self.fsm._reset_dialog(user_id)
# Если сессии нет, значит диалог ещё не начат
if user_id not in self.fsm.sessions:
response = self.fsm._start_dialog(user_id)
state = self.fsm.sessions.get(user_id, {}).get("state", "START")
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)