465 lines
33 KiB
Python
465 lines
33 KiB
Python
import asyncio
|
||
import logging
|
||
import time
|
||
import gspread
|
||
from google.oauth2.service_account import Credentials
|
||
from aiogram import Bot, Dispatcher, types
|
||
from aiogram.filters import Command
|
||
|
||
# ==============================================================================
|
||
# 1. НАСТРОЙКА ЛОГИРОВАНИЯ (Обязательно в самом верху файла!)
|
||
# ==============================================================================
|
||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||
|
||
# ==============================================================================
|
||
# --- НАСТРОЙКИ ---
|
||
# ВСТАВЬТЕ СЮДА НОВЫЙ ТОКЕН ПОСЛЕ /revoke В BOTFATHER!
|
||
BOT_TOKEN = "8962252858:AAEkZSgyWKaRHB_wlISvhcRVsbV91F32oi0"
|
||
# Вставьте ID вашей Google Таблицы (длинная строка из адресной строки между /d/ и /edit)
|
||
SPREADSHEET_ID = "1JU8RuJNGgDetNpSaAoUhbUwAiuPDNOSdFG2SfaWjmTI"
|
||
|
||
# ВАЖНО: Все ключи должны быть в нижнем регистре для корректного поиска!
|
||
GROUP_ALIASES = {
|
||
# Космос
|
||
"космос": "Космос", "космоса": "Космос", "космосу": "Космос", "космосом": "Космос", "космосе": "Космос",
|
||
"группа космос": "Космос", "группы космос": "Космос", "группа космоса": "Космос", "группы космоса": "Космос",
|
||
|
||
# Юго-запад
|
||
"юз": "Юго-запад",
|
||
"юго запад": "Юго-запад", "юго-запад": "Юго-запад",
|
||
"юго запада": "Юго-запад", "юго-запада": "Юго-запад",
|
||
"юго западу": "Юго-запад", "юго-западу": "Юго-запад",
|
||
"юго западом": "Юго-запад", "юго-западом": "Юго-запад",
|
||
"юго западе": "Юго-запад", "юго-западе": "Юго-запад",
|
||
"группа юз": "Юго-запад", "группы юз": "Юго-запад",
|
||
"группа юго запад": "Юго-запад", "группы юго запад": "Юго-запад",
|
||
"группа юго-запад": "Юго-запад", "группы юго-запад": "Юго-запад",
|
||
"группа юго запада": "Юго-запад", "группы юго запада": "Юго-запад",
|
||
"группа юго-запада": "Юго-запад", "группы юго-запада": "Юго-запад",
|
||
|
||
# Семья
|
||
"семья": "Семья", "семьи": "Семья", "семье": "Семья", "семью": "Семья", "семьей": "Семья", "семьёй": "Семья",
|
||
"группа семья": "Семья", "группы семья": "Семья", "группа семьи": "Семья", "группы семьи": "Семья",
|
||
"группа семье": "Семья", "группы семье": "Семья", "группа семью": "Семья", "группы семью": "Семья",
|
||
|
||
# Старый Арбат
|
||
"арбат": "Старый Арбат", "арбата": "Старый Арбат", "арбату": "Старый Арбат", "арбатом": "Старый Арбат", "арбате": "Старый Арбат",
|
||
"старый арбат": "Старый Арбат", "старого арбата": "Старый Арбат", "старому арбату": "Старый Арбат",
|
||
"старым арбатом": "Старый Арбат", "старом арбате": "Старый Арбат",
|
||
"группа арбат": "Старый Арбат", "группы арбат": "Старый Арбат", "группа арбата": "Старый Арбат", "группы арбата": "Старый Арбат",
|
||
"группа старый арбат": "Старый Арбат", "группы старый арбат": "Старый Арбат",
|
||
"группа старого арбата": "Старый Арбат", "группы старого арбата": "Старый Арбат",
|
||
|
||
# Надежда
|
||
"надежда": "Надежда", "надежды": "Надежда", "надежде": "Надежда", "надежду": "Надежда", "надеждой": "Надежда",
|
||
"движение надежда": "Надежда", "движение надежды": "Надежда", "движения надежда": "Надежда", "движения надежды": "Надежда",
|
||
"группа надежда": "Надежда", "группы надежда": "Надежда", "группа надежды": "Надежда", "группы надежды": "Надежда",
|
||
"группа надежде": "Надежда", "группы надежде": "Надежда", "группа надежду": "Надежда", "группы надежду": "Надежда",
|
||
|
||
# Сказка
|
||
"сказка": "Сказка", "сказки": "Сказка", "сказке": "Сказка", "сказку": "Сказка", "сказкой": "Сказка",
|
||
"группа сказка": "Сказка", "группы сказка": "Сказка", "группа сказки": "Сказка", "группы сказки": "Сказка",
|
||
"группа сказке": "Сказка", "группы сказке": "Сказка", "группа сказку": "Сказка", "группы сказку": "Сказка",
|
||
|
||
# Московский проспект
|
||
"мп": "Московский проспект",
|
||
"московский проспект": "Московский проспект", "московского проспекта": "Московский проспект",
|
||
"московскому проспекту": "Московский проспект", "московским проспектом": "Московский проспект",
|
||
"московском проспекте": "Московский проспект",
|
||
"группа мп": "Московский проспект", "группы мп": "Московский проспект",
|
||
"группа московский проспект": "Московский проспект", "группы московский проспект": "Московский проспект",
|
||
"группа московского проспекта": "Московский проспект", "группы московского проспекта": "Московский проспект",
|
||
|
||
# ЛитКом
|
||
"литком": "ЛитКом", "литкома": "ЛитКом", "литкому": "ЛитКом", "литкомом": "ЛитКом", "литкоме": "ЛитКом",
|
||
"группа литком": "ЛитКом", "группы литком": "ЛитКом", "группа литкома": "ЛитКом", "группы литкома": "ЛитКом",
|
||
|
||
# ПСО
|
||
"псо": "ПСО",
|
||
"группа псо": "ПСО", "группы псо": "ПСО",
|
||
|
||
# БУ
|
||
"бу": "БУ",
|
||
"группа бу": "БУ", "группы бу": "БУ",
|
||
|
||
# ПМ
|
||
"пм": "ПМ",
|
||
"группа пм": "ПМ", "группы пм": "ПМ",
|
||
|
||
# РС
|
||
"рс": "РС",
|
||
"группа рс": "РС", "группы рс": "РС",
|
||
|
||
# Движение (Нововоронеж)
|
||
"движение": "Движение", "движения": "Движение", "движению": "Движение", "движением": "Движение", "движении": "Движение",
|
||
"нововоронеж": "Движение", "нововоронежа": "Движение", "нововоронежу": "Движение", "нововоронежем": "Движение", "нововоронеже": "Движение",
|
||
"движение нововоронеж": "Движение", "движение нововоронежа": "Движение",
|
||
"движения нововоронеж": "Движение", "движения нововоронежа": "Движение",
|
||
"группа движение": "Движение", "группы движение": "Движение",
|
||
"группа движения": "Движение", "группы движения": "Движение",
|
||
"группа нововоронеж": "Движение", "группы нововоронеж": "Движение",
|
||
"группа нововоронежа": "Движение", "группы нововоронежа": "Движение",
|
||
|
||
# Только сегодня Калач
|
||
"калач": "Только сегодня Калач", "калача": "Только сегодня Калач", "калачу": "Только сегодня Калач",
|
||
"калачом": "Только сегодня Калач", "калаче": "Только сегодня Калач",
|
||
"только сегодня": "Только сегодня Калач",
|
||
"только сегодня калач": "Только сегодня Калач", "только сегодня калача": "Только сегодня Калач",
|
||
"только сегодня калачу": "Только сегодня Калач", "только сегодня калачом": "Только сегодня Калач",
|
||
"группа калач": "Только сегодня Калач", "группы калач": "Только сегодня Калач",
|
||
"группа калача": "Только сегодня Калач", "группы калача": "Только сегодня Калач",
|
||
"группа только сегодня": "Только сегодня Калач", "группы только сегодня": "Только сегодня Калач",
|
||
"группа только сегодня калач": "Только сегодня Калач", "группы только сегодня калач": "Только сегодня Калач",
|
||
|
||
# Виктория Лиски
|
||
"лиски": "Виктория Лиски", "лисок": "Виктория Лиски", "лискам": "Виктория Лиски", "лисками": "Виктория Лиски", "лисках": "Виктория Лиски",
|
||
"виктория": "Виктория Лиски", "виктории": "Виктория Лиски",
|
||
"викторию": "Виктория Лиски", "викторией": "Виктория Лиски",
|
||
"виктория лиски": "Виктория Лиски", "виктории лиски": "Виктория Лиски", "викторию лиски": "Виктория Лиски",
|
||
"группа виктория": "Виктория Лиски", "группы виктория": "Виктория Лиски",
|
||
"группа виктории": "Виктория Лиски", "группы виктории": "Виктория Лиски",
|
||
"группа викторию": "Виктория Лиски", "группы викторию": "Виктория Лиски",
|
||
"группа лиски": "Виктория Лиски", "группы лиски": "Виктория Лиски",
|
||
"группа виктория лиски": "Виктория Лиски", "группы виктория лиски": "Виктория Лиски",
|
||
|
||
# Щепка Курск
|
||
"щепка": "Щепка Курск", "щепки": "Щепка Курск", "щепке": "Щепка Курск", "щепку": "Щепка Курск", "щепкой": "Щепка Курск",
|
||
"щепка курск": "Щепка Курск", "щепки курск": "Щепка Курск", "щепка курска": "Щепка Курск", "щепки курска": "Щепка Курск",
|
||
"группа щепка": "Щепка Курск", "группы щепка": "Щепка Курск", "группа щепки": "Щепка Курск", "группы щепки": "Щепка Курск",
|
||
"группа щепка курск": "Щепка Курск", "группы щепка курск": "Щепка Курск",
|
||
|
||
# Кузня Курск
|
||
"кузня": "Кузня Курск", "кузни": "Кузня Курск", "кузне": "Кузня Курск", "кузню": "Кузня Курск", "кузней": "Кузня Курск", "кузнёй": "Кузня Курск",
|
||
"кузня курск": "Кузня Курск", "кузни курск": "Кузня Курск", "кузня курска": "Кузня Курск", "кузни курска": "Кузня Курск",
|
||
"группа кузня": "Кузня Курск", "группы кузня": "Кузня Курск", "группа кузни": "Кузня Курск", "группы кузни": "Кузня Курск",
|
||
"группа кузня курск": "Кузня Курск", "группы кузня курск": "Кузня Курск",
|
||
|
||
# Высота Белгород
|
||
"высота": "Высота Белгород", "высоты": "Высота Белгород", "высоте": "Высота Белгород", "высоту": "Высота Белгород", "высотой": "Высота Белгород",
|
||
"высота белгород": "Высота Белгород", "высоты белгород": "Высота Белгород",
|
||
"высота белгорода": "Высота Белгород", "высоты белгорода": "Высота Белгород",
|
||
"группа высота": "Высота Белгород", "группы высота": "Высота Белгород",
|
||
"группа высоты": "Высота Белгород", "группы высоты": "Высота Белгород",
|
||
"группа высота белгород": "Высота Белгород", "группы высота белгород": "Высота Белгород",
|
||
|
||
# Наркоз Липецк
|
||
"наркоз": "Наркоз Липецк", "наркоза": "Наркоз Липецк", "наркозу": "Наркоз Липецк", "наркозом": "Наркоз Липецк", "наркозе": "Наркоз Липецк",
|
||
"липецк": "Наркоз Липецк", "липецка": "Наркоз Липецк", "липецку": "Наркоз Липецк", "липецком": "Наркоз Липецк", "липецке": "Наркоз Липецк",
|
||
"наркоз липецк": "Наркоз Липецк", "наркоза липецк": "Наркоз Липецк", "наркоз липецка": "Наркоз Липецк",
|
||
"группа наркоз": "Наркоз Липецк", "группы наркоз": "Наркоз Липецк",
|
||
"группа наркоза": "Наркоз Липецк", "группы наркоза": "Наркоз Липецк",
|
||
"группа липецк": "Наркоз Липецк", "группы липецк": "Наркоз Липецк",
|
||
"группа наркоз липецк": "Наркоз Липецк", "группы наркоз липецк": "Наркоз Липецк",
|
||
|
||
# Волна Липецк
|
||
"волна": "Волна Липецк", "волны": "Волна Липецк", "волне": "Волна Липецк", "волну": "Волна Липецк", "волной": "Волна Липецк",
|
||
"волна липецк": "Волна Липецк", "волны липецк": "Волна Липецк", "волна липецка": "Волна Липецк", "волны липецка": "Волна Липецк",
|
||
"группа волна": "Волна Липецк", "группы волна": "Волна Липецк",
|
||
"группа волны": "Волна Липецк", "группы волны": "Волна Липецк",
|
||
"группа волна липецк": "Волна Липецк", "группы волна липецк": "Волна Липецк",
|
||
|
||
# Утренняя звезда Тамбов
|
||
"тамбов": "Утренняя звезда Тамбов", "тамбова": "Утренняя звезда Тамбов", "тамбову": "Утренняя звезда Тамбов",
|
||
"тамбовом": "Утренняя звезда Тамбов", "тамбове": "Утренняя звезда Тамбов",
|
||
"утренняя звезда": "Утренняя звезда Тамбов", "утренней звезды": "Утренняя звезда Тамбов",
|
||
"утренней звезде": "Утренняя звезда Тамбов", "утреннюю звезду": "Утренняя звезда Тамбов",
|
||
"утренней звездой": "Утренняя звезда Тамбов",
|
||
"утренняя звезда тамбов": "Утренняя звезда Тамбов", "утренней звезды тамбов": "Утренняя звезда Тамбов",
|
||
"группа тамбов": "Утренняя звезда Тамбов", "группы тамбов": "Утренняя звезда Тамбов",
|
||
"группа утренняя звезда": "Утренняя звезда Тамбов", "группы утренняя звезда": "Утренняя звезда Тамбов",
|
||
"группа утренней звезды": "Утренняя звезда Тамбов", "группы утренней звезды": "Утренняя звезда Тамбов",
|
||
|
||
# Детектор Старый Оскол
|
||
"детектор": "Детектор Старый Оскол", "детектора": "Детектор Старый Оскол", "детектору": "Детектор Старый Оскол",
|
||
"детектором": "Детектор Старый Оскол", "детекторе": "Детектор Старый Оскол",
|
||
"старый оскол": "Детектор Старый Оскол", "старого оскола": "Детектор Старый Оскол",
|
||
"старому осколу": "Детектор Старый Оскол", "старым осколом": "Детектор Старый Оскол",
|
||
"старом осколе": "Детектор Старый Оскол",
|
||
"детектор старый оскол": "Детектор Старый Оскол", "детектора старый оскол": "Детектор Старый Оскол",
|
||
"группа детектор": "Детектор Старый Оскол", "группы детектор": "Детектор Старый Оскол",
|
||
"группа детектора": "Детектор Старый Оскол", "группы детектора": "Детектор Старый Оскол",
|
||
"группа старый оскол": "Детектор Старый Оскол", "группы старый оскол": "Детектор Старый Оскол",
|
||
|
||
# Среда Белгород
|
||
"среда": "Среда Белгород", "среды": "Среда Белгород", "среде": "Среда Белгород", "среду": "Среда Белгород", "средой": "Среда Белгород",
|
||
"среда белгород": "Среда Белгород", "среды белгород": "Среда Белгород",
|
||
"среда белгорода": "Среда Белгород", "среды белгорода": "Среда Белгород",
|
||
"группа среда": "Среда Белгород", "группы среда": "Среда Белгород",
|
||
"группа среды": "Среда Белгород", "группы среды": "Среда Белгород",
|
||
"группа среда белгород": "Среда Белгород", "группы среда белгород": "Среда Белгород",
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 3. ПОДКЛЮЧЕНИЕ К GOOGLE ТАБЛИЦЕ
|
||
# ==============================================================================
|
||
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
|
||
try:
|
||
creds = Credentials.from_service_account_file('credentials.json', scopes=SCOPES)
|
||
gc = gspread.authorize(creds)
|
||
sheet = gc.open_by_key(SPREADSHEET_ID).sheet1
|
||
logging.info("✅ Google Таблица успешно подключена!")
|
||
except FileNotFoundError:
|
||
logging.error("❌ Файл 'credentials.json' не найден. Положите его в папку с ботом.")
|
||
exit(1)
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка подключения к Google Таблице: {e}")
|
||
exit(1)
|
||
|
||
# ==============================================================================
|
||
# 4. НАСТРОЙКА БОТА
|
||
# ==============================================================================
|
||
bot = Bot(token=BOT_TOKEN)
|
||
dp = Dispatcher()
|
||
|
||
# ==============================================================================
|
||
# 5. ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ (Синхронные, будут вызываться через asyncio.to_thread)
|
||
# ==============================================================================
|
||
|
||
def get_col_index(sheet_obj, target_name):
|
||
"""Находит номер колонки по названию группы (игнорирует переносы строк и регистр)"""
|
||
headers = sheet_obj.row_values(1)
|
||
target_clean = target_name.lower().replace('\n', ' ').replace(' ', ' ').strip()
|
||
for i, h in enumerate(headers):
|
||
h_clean = h.lower().replace('\n', ' ').replace(' ', ' ').strip()
|
||
if target_clean == h_clean:
|
||
return i + 1 # gspread использует индексацию с 1
|
||
return None
|
||
|
||
def get_message_text(message: types.Message) -> str:
|
||
"""Безопасно извлекает текст: либо из сообщения, либо из подписи (caption) к фото/медиа."""
|
||
return message.text or message.caption or ""
|
||
|
||
def parse_flag_event(text: str):
|
||
"""Анализирует текст. Первая найденная группа = забирает, вторая = отдает."""
|
||
text_lower = text.lower()
|
||
if 'флаг' not in text_lower:
|
||
return None, None, False
|
||
|
||
is_own = 'свой' in text_lower
|
||
found_groups = []
|
||
# Сортируем по длине ключа, чтобы "юго-запад" матчился раньше, чем "юз"
|
||
sorted_aliases = sorted(GROUP_ALIASES.keys(), key=len, reverse=True)
|
||
|
||
for alias in sorted_aliases:
|
||
if alias in text_lower:
|
||
canonical_name = GROUP_ALIASES[alias]
|
||
if canonical_name not in found_groups:
|
||
found_groups.append(canonical_name)
|
||
|
||
if len(found_groups) >= 2:
|
||
return found_groups[0], found_groups[1], is_own
|
||
|
||
return None, None, False
|
||
|
||
def update_horizontal_sheet(sheet_obj, winner: str, loser: str, is_own: bool):
|
||
"""Обновляет данные, минимизируя количество запросов к API (Batch Read)"""
|
||
col_winner = get_col_index(sheet_obj, winner)
|
||
col_loser = get_col_index(sheet_obj, loser)
|
||
|
||
if not col_winner or not col_loser:
|
||
raise ValueError(f"Не найдены колонки для '{winner}' или '{loser}' в таблице")
|
||
|
||
# Читаем 4-ю и 5-ю строки ОДНИМ запросом для получения актуальных данных
|
||
data = sheet_obj.get('4:5')
|
||
row_4 = data[0] if len(data) > 0 else []
|
||
row_5 = data[1] if len(data) > 1 else []
|
||
|
||
idx_winner = col_winner - 1
|
||
idx_loser = col_loser - 1
|
||
|
||
if is_own:
|
||
sheet_obj.update_cell(4, col_winner, "на месте")
|
||
cell_val = row_5[idx_loser] if idx_loser < len(row_5) else ""
|
||
captured_list = [x.strip() for x in str(cell_val).replace('\n', ',').split(',') if x.strip()]
|
||
if winner in captured_list:
|
||
captured_list.remove(winner)
|
||
sheet_obj.update_cell(5, col_loser, ", ".join(captured_list))
|
||
else:
|
||
sheet_obj.update_cell(4, col_loser, f"у {winner}")
|
||
cell_val = row_5[idx_winner] if idx_winner < len(row_5) else ""
|
||
captured_list = [x.strip() for x in str(cell_val).replace('\n', ',').split(',') if x.strip()]
|
||
if loser not in captured_list:
|
||
captured_list.append(loser)
|
||
sheet_obj.update_cell(5, col_winner, ", ".join(captured_list))
|
||
|
||
# Сбрасываем кэш сводки после любого изменения, чтобы следующий запрос показал свежие данные
|
||
global _summary_cache
|
||
_summary_cache["timestamp"] = 0
|
||
|
||
|
||
# ==============================================================================
|
||
# 6. КЭШИРОВАНИЕ И ГЕНЕРАЦИЯ СВОДКИ (Оптимизация против ошибки 429)
|
||
# ==============================================================================
|
||
_summary_cache = {"data": None, "timestamp": 0}
|
||
CACHE_TTL_SECONDS = 30 # Храним кэш 30 секунд для защиты от спама командой /флаги
|
||
|
||
def generate_summary(sheet_obj) -> str:
|
||
"""Формирует текстовую сводку, делая ВСЕГО ОДИН запрос к API Google"""
|
||
try:
|
||
# Читаем строки с 1 по 5 включительно ЗА ОДИН запрос!
|
||
data = sheet_obj.get('1:5')
|
||
except Exception as e:
|
||
raise e
|
||
|
||
if not data or len(data) < 5:
|
||
return "📊 Таблица пуста или не заполнена."
|
||
|
||
headers = data[0] # 1-я строка (заголовки)
|
||
row_4 = data[3] # 4-я строка (статус своего флага)
|
||
row_5 = data[4] # 5-я строка (захваченные флаги)
|
||
|
||
summary_lines = []
|
||
|
||
# Начинаем с индекса 1 (вторая колонка), так как 0-я это "Группы и комитеты"
|
||
for col in range(1, len(headers)):
|
||
group = headers[col]
|
||
if not group or not str(group).strip():
|
||
continue
|
||
|
||
own_flag_status = row_4[col] if col < len(row_4) else ""
|
||
captured = row_5[col] if col < len(row_5) else ""
|
||
|
||
flags_text = []
|
||
|
||
if own_flag_status:
|
||
clean_status = str(own_flag_status).strip()
|
||
if clean_status.lower() == "на месте":
|
||
flags_text.append("свой флаг на месте")
|
||
else:
|
||
flags_text.append(f"свой флаг {clean_status}")
|
||
else:
|
||
flags_text.append("статус своего флага не указан")
|
||
|
||
if captured:
|
||
captured_list = [x.strip() for x in str(captured).replace('\n', ',').split(',') if x.strip()]
|
||
for cap in captured_list:
|
||
flags_text.append(f"захвачен флаг: {cap}")
|
||
|
||
summary_lines.append(f"🚩 **{group}**: {', '.join(flags_text)}")
|
||
|
||
return "\n".join(summary_lines) if summary_lines else "📊 Таблица пуста или не заполнена."
|
||
|
||
def get_cached_summary(sheet_obj) -> str:
|
||
"""Возвращает кэшированную сводку или генерирует новую, если кэш устарел"""
|
||
current_time = time.time()
|
||
if _summary_cache["data"] and (current_time - _summary_cache["timestamp"]) < CACHE_TTL_SECONDS:
|
||
return _summary_cache["data"]
|
||
|
||
new_summary = generate_summary(sheet_obj)
|
||
_summary_cache["data"] = new_summary
|
||
_summary_cache["timestamp"] = current_time
|
||
return new_summary
|
||
|
||
|
||
# ==============================================================================
|
||
# 7. ФИЛЬТРЫ СООБЩЕНИЙ
|
||
# ==============================================================================
|
||
|
||
def is_summary_filter(message: types.Message) -> bool:
|
||
"""Проверяет, является ли сообщение запросом сводки"""
|
||
text = get_message_text(message).lower().strip()
|
||
if not text:
|
||
return False
|
||
|
||
clean_text = text.replace('@googleanvrn_bot', '').strip()
|
||
clean_text = clean_text.lstrip('/').strip()
|
||
|
||
return clean_text in ['флаги', 'summary']
|
||
|
||
def is_flag_event_filter(message: types.Message) -> bool:
|
||
"""Проверяет, является ли сообщение событием захвата флага"""
|
||
text = get_message_text(message)
|
||
if not text:
|
||
return False
|
||
|
||
text_lower = text.lower().strip()
|
||
|
||
# ВАЖНО: Сначала исключаем запросы сводки, чтобы слово "флаги" не триггерило этот фильтр
|
||
if is_summary_filter(message):
|
||
return False
|
||
|
||
return bool('@googleanvrn_bot' in text_lower and 'флаг' in text_lower)
|
||
|
||
|
||
# ==============================================================================
|
||
# 8. ОБРАБОТЧИКИ СООБЩЕНИЙ (Асинхронные)
|
||
# ==============================================================================
|
||
|
||
@dp.message(Command("start"))
|
||
async def cmd_start(message: types.Message):
|
||
await message.answer(
|
||
"🤖 Бот для учета флагов запущен!\n\n"
|
||
"Вы можете отправить обычное сообщение или фото с подписью:\n"
|
||
"@googleAnVrn_bot Космос забрал свой флаг у ЮЗ\n"
|
||
"@googleAnVrn_bot Литком забрали флаг у РС\n\n"
|
||
"Для получения сводки используйте: /флаги или @googleAnVrn_bot флаги"
|
||
)
|
||
|
||
@dp.message(is_summary_filter)
|
||
async def cmd_summary(message: types.Message):
|
||
await message.answer("⏳ Собираю данные из таблицы...")
|
||
try:
|
||
# Используем кэшированную версию функции в отдельном потоке
|
||
summary = await asyncio.to_thread(get_cached_summary, sheet)
|
||
await message.answer(summary, parse_mode="Markdown")
|
||
|
||
except gspread.exceptions.APIError as api_err:
|
||
# Специальная обработка ошибки 429 (Quota Exceeded)
|
||
if "429" in str(api_err):
|
||
logging.warning("⚠️ Превышен лимит запросов к Google Sheets (429).")
|
||
await message.answer(
|
||
"⚠️ Слишком много запросов к таблице прямо сейчас. "
|
||
"Google временно ограничил чтение. Пожалуйста, подождите 1 минуту и попробуйте снова."
|
||
)
|
||
else:
|
||
logging.error(f"Ошибка Google API: {api_err}", exc_info=True)
|
||
await message.answer("❌ Произошла ошибка при обращении к Google Таблице.")
|
||
|
||
except Exception as e:
|
||
logging.error(f"Критическая ошибка генерации сводки: {e}", exc_info=True)
|
||
await message.answer("❌ Не удалось прочитать таблицу.")
|
||
|
||
|
||
@dp.message(is_flag_event_filter)
|
||
async def handle_flag_event(message: types.Message):
|
||
text_to_parse = get_message_text(message)
|
||
winner, loser, is_own = parse_flag_event(text_to_parse)
|
||
|
||
if not winner or not loser:
|
||
await message.reply(
|
||
"⚠️ Не удалось распознать две группы в сообщении или подписи к фото. "
|
||
"Используйте понятные названия (например: Космос, ЮЗ, ЛитКом, РС, Надежда, Сказка, Семья, МП, Щепка, Кузня, Высота, Только сегодня, Виктория)."
|
||
)
|
||
return
|
||
|
||
try:
|
||
# Оборачиваем синхронную работу с gspread в asyncio.to_thread
|
||
await asyncio.to_thread(update_horizontal_sheet, sheet, winner, loser, is_own)
|
||
|
||
if is_own:
|
||
text_response = f"✅ Зафиксировано: **{winner}** вернул(а) свой флаг (теперь он на месте)."
|
||
else:
|
||
text_response = f"✅ Зафиксировано: **{winner}** забрал(а) флаг у **{loser}**."
|
||
|
||
await message.reply(text_response, parse_mode="Markdown")
|
||
|
||
except ValueError as ve:
|
||
logging.warning(f"Ошибка валидации данных: {ve}")
|
||
await message.reply(f"❌ Ошибка: {ve}. Проверьте заголовки в таблице.")
|
||
except Exception as e:
|
||
logging.error(f"Критическая ошибка записи в таблицу: {e}", exc_info=True)
|
||
await message.reply("❌ Произошла непредвиденная ошибка при записи в таблицу.")
|
||
|
||
|
||
# ==============================================================================
|
||
# 9. ЗАПУСК
|
||
# ==============================================================================
|
||
async def main():
|
||
logging.info("🚀 Бот запущен и начинает слушать сообщения...")
|
||
await dp.start_polling(bot)
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
asyncio.run(main())
|
||
except KeyboardInterrupt:
|
||
logging.info("🛑 Бот остановлен пользователем.") |