Files
GoogleSheetAnBot/bot.py
T
2026-09-29 13:10:37 +00:00

1298 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import logging
import re
import time
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
import gspread
from google.auth.transport.requests import AuthorizedSession
from google.oauth2.service_account import Credentials
from aiogram import Bot, Dispatcher, F, types
from aiogram.filters import Command
from gspread.utils import rowcol_to_a1
# ==============================================================================
# 1. ЛОГИРОВАНИЕ
# ==============================================================================
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
)
# ==============================================================================
# 2. НАСТРОЙКИ
# ==============================================================================
BOT_TOKEN = "8962252858:AAEkZSgyWKaRHB_wlISvhcRVsbV91F32oi0"
SPREADSHEET_ID = "1JU8RuJNGgDetNpSaAoUhbUwAiuPDNOSdFG2SfaWjmTI"
BOT_MENTION = "@googleanvrn_bot"
BASE_DIR = Path(__file__).resolve().parent
CREDENTIALS_FILE = BASE_DIR / "credentials.json"
SCOPES = [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive.metadata.readonly",
]
CACHE_TTL_SECONDS = 30
# ==============================================================================
# 3. ГРУППЫ И АЛИАСЫ
# ==============================================================================
# Каноническое название -> варианты, которыми его могут написать.
#
# Составные aliases вида "движение надежда" намеренно не используются:
# они могут поглотить ДВЕ отдельные группы как одну.
GROUP_ALIASES = {
"Космос": [
"космос", "космоса", "космосу", "космосом", "космосе",
"группа космос", "группы космос",
"группа космоса", "группы космоса",
],
"Юго-запад": [
"юз",
"юго запад", "юго-запад",
"юго запада", "юго-запада",
"юго западу", "юго-западу",
"юго западом", "юго-западом",
"юго западе", "юго-западе",
"группа юз", "группы юз",
"группа юго запад", "группы юго запад",
"группа юго-запад", "группы юго-запад",
"группа юго запада", "группы юго запада",
"группа юго-запада", "группы юго-запада",
],
"Семья": [
"семья", "семьи", "семье", "семью", "семьей", "семьёй",
"группа семья", "группы семья",
"группа семьи", "группы семьи",
"группа семье", "группы семье",
"группа семью", "группы семью",
],
"Старый Арбат": [
"арбат", "арбата", "арбатy", "арбатом", "арбате",
"старый арбат", "старого арбата",
"старому арбату", "старым арбатом", "старом арбате",
"группа арбат", "группы арбат",
"группа арбата", "группы арбата",
"группа старый арбат", "группы старый арбат",
"группа старого арбата", "группы старого арбата",
],
"Надежда": [
"надежда", "надежды", "надежде", "надежду", "надеждой",
"группа надежда", "группы надежда",
"группа надежды", "группы надежды",
"группа надежде", "группы надежде",
"группа надежду", "группы надежду",
],
"Сказка": [
"сказка", "сказки", "сказке", "сказку", "сказкой",
"группа сказка", "группы сказка",
"группа сказки", "группы сказки",
"группа сказке", "группы сказке",
"группа сказку", "группы сказку",
],
"Московский проспект": [
"мп",
"московский проспект", "московского проспекта",
"московскому проспекту", "московским проспектом",
"московском проспекте",
"группа мп", "группы мп",
"группа московский проспект", "группы московский проспект",
"группа московского проспекта", "группы московского проспекта",
],
"ЛитКом": [
"литком", "литкома", "литкому", "литкомом", "литкоме",
"группа литком", "группы литком",
"группа литкома", "группы литкома",
],
"ПСО": [
"псо",
"группа псо", "группы псо",
],
"БУ": [
"бу",
"группа бу", "группы бу",
],
"ПМ": [
"пм",
"группа пм", "группы пм",
],
"РС": [
"рс",
"группа рс", "группы рс",
],
"Движение": [
"движение", "движения", "движению", "движением", "движении",
"нововоронеж", "нововоронежа", "нововоронежу",
"нововоронежем", "нововоронеже",
"группа движение", "группы движение",
"группа движения", "группы движения",
"группа нововоронеж", "группы нововоронеж",
"группа нововоронежа", "группы нововоронежа",
],
"Только сегодня Калач": [
"калач", "калача", "калачу", "калачом", "калаче",
"только сегодня",
"только сегодня калач", "только сегодня калача",
"только сегодня калачу", "только сегодня калачом",
"группа калач", "группы калач",
"группа калача", "группы калача",
"группа только сегодня", "группы только сегодня",
"группа только сегодня калач", "группы только сегодня калач",
],
"Виктория Лиски": [
"лиски", "лисок", "лискам", "лисками", "лисках",
"виктория", "виктории", "викторию", "викторией",
"виктория лиски", "виктории лиски", "викторию лиски",
"группа виктория", "группы виктория",
"группа виктории", "группы виктории",
"группа викторию", "группы викторию",
"группа лиски", "группы лиски",
"группа виктория лиски", "группы виктория лиски",
],
"Щепка Курск": [
"щепка", "щепки", "щепке", "щепку", "щепкой",
"щепка курск", "щепки курск",
"щепка курска", "щепки курска",
"группа щепка", "группы щепка",
"группа щепки", "группы щепки",
"группа щепка курск", "группы щепка курск",
],
"Кузня Курск": [
"кузня", "кузни", "кузне", "кузню", "кузней", "кузнёй",
"кузня курск", "кузни курск",
"кузня курска", "кузни курска",
"группа кузня", "группы кузня",
"группа кузни", "группы кузни",
"группа кузня курск", "группы кузня курск",
],
"Высота Белгород": [
"высота", "высоты", "высоте", "высоту", "высотой",
"высота белгород", "высоты белгород",
"высота белгорода", "высоты белгорода",
"группа высота", "группы высота",
"группа высоты", "группы высоты",
"группа высота белгород", "группы высота белгород",
],
"Наркоз Липецк": [
"наркоз", "наркоза", "наркозу", "наркозом", "наркозе",
"липецк", "липецка", "липецку", "липецком", "липецке",
"наркоз липецк", "наркоза липецк", "наркоз липецка",
"группа наркоз", "группы наркоз",
"группа наркоза", "группы наркоза",
"группа липецк", "группы липецк",
"группа наркоз липецк", "группы наркоз липецк",
],
"Волна Липецк": [
"волна", "волны", "волне", "волну", "волной",
"волна липецк", "волны липецк",
"волна липецка", "волны липецка",
"группа волна", "группы волна",
"группа волны", "группы волны",
"группа волна липецк", "группы волна липецк",
],
"Утренняя звезда Тамбов": [
"тамбов", "тамбова", "тамбову", "тамбовом", "тамбове",
"утренняя звезда", "утренней звезды",
"утренней звезде", "утреннюю звезду",
"утренней звездой",
"утренняя звезда тамбов",
"утренней звезды тамбов",
"группа тамбов", "группы тамбов",
"группа утренняя звезда", "группы утренняя звезда",
"группа утренней звезды", "группы утренней звезды",
],
"Детектор Старый Оскол": [
"детектор", "детектора", "детектору", "детектором", "детекторе",
"старый оскол", "старого оскола", "старому осколу",
"старым осколом", "старом осколе",
"детектор старый оскол", "детектора старый оскол",
"группа детектор", "группы детектор",
"группа детектора", "группы детектора",
"группа старый оскол", "группы старый оскол",
],
"Среда Белгород": [
"среда", "среды", "среде", "среду", "средой",
"среда белгород", "среды белгород",
"среда белгорода", "среды белгорода",
"группа среда", "группы среда",
"группа среды", "группы среды",
"группа среда белгород", "группы среда белгород",
],
"Девочки": [
"девочки",
"девочек",
"девочкам",
"девочками",
"девочках",
"группа девочки",
"группы девочки",
"группа девочек",
"группы девочек",
"группа девочкам",
"группы девочкам",
"группа девочками",
"группы девочками",
"группа девочках",
"группы девочках",
],
}
# ==============================================================================
# 4. НОРМАЛИЗАЦИЯ И АЛИАСЫ
# ==============================================================================
def norm(text: str) -> str:
text = str(text or "").lower().replace("ё", "е")
text = re.sub(r"[^а-яa-z0-9@_-]+", " ", text)
return re.sub(r"\s+", " ", text).strip()
ALIAS_TO_CANONICAL = {}
for canonical, aliases in GROUP_ALIASES.items():
ALIAS_TO_CANONICAL[norm(canonical)] = canonical
for alias in aliases:
ALIAS_TO_CANONICAL[norm(alias)] = canonical
ALIASES = sorted(
(
(alias, canonical)
for canonical, aliases in GROUP_ALIASES.items()
for alias in aliases
),
key=lambda item: len(norm(item[0])),
reverse=True,
)
def canonicalize_flag_name(value: str) -> str | None:
"""Возвращает canonical-группу для значения из row 5 или None."""
value_norm = norm(value)
if not value_norm:
return None
return ALIAS_TO_CANONICAL.get(value_norm)
def _alias_pattern(alias: str) -> re.Pattern:
"""Компилирует безопасный поиск alias по границам слов."""
return re.compile(
rf"(?<![а-яa-z0-9_-])"
rf"{re.escape(norm(alias))}"
rf"(?![а-яa-z0-9_-])"
)
COMPILED_ALIASES = [
(alias, canonical, _alias_pattern(alias))
for alias, canonical in ALIASES
]
def find_groups(text: str) -> list[str]:
"""
Находит все группы в порядке их фактического появления в сообщении.
Пересекающиеся aliases схлопываются в одно совпадение,
при этом предпочтение получает более длинный alias.
Отдельные повторные упоминания одной и той же группы НЕ удаляются:
это позволяет корректно обнаруживать неоднозначные сообщения
вида "Космос ... Космос ... ЮЗ".
"""
text_norm = norm(text)
if not text_norm:
return []
matches = []
for alias, canonical, pattern in COMPILED_ALIASES:
for match in pattern.finditer(text_norm):
matches.append(
{
"start": match.start(),
"end": match.end(),
"length": match.end() - match.start(),
"canonical": canonical,
}
)
matches.sort(
key=lambda item: (-item["length"], item["start"])
)
selected = []
for candidate in matches:
if any(
candidate["start"] < existing["end"]
and candidate["end"] > existing["start"]
for existing in selected
):
continue
selected.append(candidate)
selected.sort(
key=lambda item: item["start"]
)
return [
item["canonical"]
for item in selected
]
def parse_flag_event(text: str) -> dict | None:
"""Разбирает сообщение в событие изменения флага."""
s = norm(text)
if "флаг" not in s:
return None
groups = find_groups(s)
if not groups:
return None
# Возврат собственного флага.
# Достаточно указать одну группу:
# "Космос вернул свой флаг".
if re.search(r"\bсвой\s+флаг(?:а|и)?\b", s):
return {
"kind": "own_return",
"owner": groups[0],
}
# Обычное событие: ровно две разные группы.
if len(groups) != 2:
return None
winner, owner = groups
if winner == owner:
return None
return {
"kind": "capture",
"winner": winner,
"owner": owner,
}
# ==============================================================================
# 5. GOOGLE SHEETS HELPERS
# ==============================================================================
def get_col_index(headers: list, target_name: str) -> int | None:
"""Ищет колонку по нормализованному canonical-названию группы."""
target = norm(target_name)
for index, header in enumerate(headers, start=1):
if norm(header) == target:
return index
return None
def split_flags(value: str) -> list[str]:
"""Разделяет содержимое row 5 по запятым, ; и переносам строк."""
return [
item.strip()
for item in re.split(
r"[,;\n]+",
str(value or ""),
)
if item.strip()
]
def _pad_rows(
data: list[list],
width: int,
) -> list[list]:
"""Дополняет считанные строки до одинаковой ширины."""
rows = [
list(row)
for row in data[:5]
]
while len(rows) < 5:
rows.append([])
for row in rows:
if len(row) < width:
row.extend(
[""] * (width - len(row))
)
return rows
def apply_event(
sheet_obj,
event: dict,
) -> None:
"""
Применяет событие к таблице.
Инвариант:
один собственный флаг владельца не может одновременно находиться
более чем в одной ячейке row 5.
Перед установкой новой позиции флаг удаляется из ВСЕХ колонок row 5.
Все реальные изменения отправляются одним batch_update().
"""
data = sheet_obj.get("1:5")
if not data or not data[0]:
raise ValueError(
"Не удалось прочитать заголовки таблицы"
)
headers = data[0]
width = len(headers)
rows = _pad_rows(
data,
width,
)
row_4 = rows[3]
row_5 = rows[4]
if event["kind"] == "own_return":
owner = event["owner"]
owner_col = get_col_index(
headers,
owner,
)
if not owner_col:
raise ValueError(
f"Не найдена колонка для '{owner}' в таблице"
)
# Убираем owner из всех row 5,
# включая старые aliases/варианты написания.
for col_index, value in enumerate(row_5):
flags = split_flags(value)
filtered = [
flag
for flag in flags
if canonicalize_flag_name(flag) != owner
]
new_value = ", ".join(filtered)
if new_value != str(value or ""):
row_5[col_index] = new_value
row_4[owner_col - 1] = "на месте"
elif event["kind"] == "capture":
winner = event["winner"]
owner = event["owner"]
winner_col = get_col_index(
headers,
winner,
)
owner_col = get_col_index(
headers,
owner,
)
if not winner_col or not owner_col:
missing = []
if not winner_col:
missing.append(winner)
if not owner_col:
missing.append(owner)
raise ValueError(
f"Не найдены колонки: {', '.join(missing)}"
)
# Сначала удаляем owner из ВСЕХ старых позиций.
for col_index, value in enumerate(row_5):
flags = split_flags(value)
filtered = [
flag
for flag in flags
if canonicalize_flag_name(flag) != owner
]
new_value = ", ".join(filtered)
if new_value != str(value or ""):
row_5[col_index] = new_value
# Затем помещаем owner в колонку winner.
winner_index = winner_col - 1
winner_flags = split_flags(
row_5[winner_index]
)
existing_canonical = {
canonicalize_flag_name(flag) or norm(flag)
for flag in winner_flags
}
if owner not in existing_canonical:
winner_flags.append(owner)
row_5[winner_index] = ", ".join(
winner_flags
)
# Статус собственного флага owner.
row_4[owner_col - 1] = f"у {winner}"
else:
raise ValueError(
f"Неизвестный тип события: {event['kind']}"
)
original_rows = _pad_rows(
data,
width,
)
original_row_4 = original_rows[3]
original_row_5 = original_rows[4]
updates = []
# Изменения строки 4.
for index in range(width):
old = str(
original_row_4[index] or ""
)
new = str(
row_4[index] or ""
)
if old != new:
updates.append(
{
"range": rowcol_to_a1(
4,
index + 1,
),
"values": [[new]],
}
)
# Изменения строки 5.
for index in range(width):
old = str(
original_row_5[index] or ""
)
new = str(
row_5[index] or ""
)
if old != new:
updates.append(
{
"range": rowcol_to_a1(
5,
index + 1,
),
"values": [[new]],
}
)
if updates:
sheet_obj.batch_update(
updates
)
# ==============================================================================
# 6. КЭШ СВОДКИ
# ==============================================================================
_summary_cache = {
"data": None,
"timestamp": 0.0,
"modified_time": None,
}
# Последовательная обработка операций записи.
# Это защищает от одновременного чтения/изменения/записи
# одной и той же версии таблицы.
sheet_update_lock = asyncio.Lock()
def invalidate_summary_cache() -> None:
_summary_cache["data"] = None
_summary_cache["timestamp"] = 0.0
_summary_cache["modified_time"] = None
def get_sheet_modified_time(
creds,
) -> str:
"""
Возвращает дату и время последнего изменения файла Google Sheets
в часовом поясе Africa/Cairo.
"""
try:
session = AuthorizedSession(
creds
)
response = session.get(
f"https://www.googleapis.com/drive/v3/files/{SPREADSHEET_ID}",
params={
"fields": "modifiedTime",
},
timeout=20,
)
if response.status_code != 200:
logging.warning(
"Не удалось получить modifiedTime таблицы: "
"HTTP %s: %s",
response.status_code,
response.text,
)
return "не удалось определить"
modified_time = response.json().get(
"modifiedTime"
)
if not modified_time:
return "не удалось определить"
dt = datetime.fromisoformat(
modified_time.replace(
"Z",
"+00:00",
)
)
dt = dt.astimezone(
ZoneInfo("Africa/Cairo")
)
return dt.strftime(
"%d.%m.%Y %H:%M:%S"
)
except Exception as exc:
logging.warning(
"Ошибка получения времени изменения таблицы: %s",
exc,
)
return "не удалось определить"
def build_summary_from_data(
data: list[list],
) -> str:
if not data or len(data) < 5:
return "📊 Таблица пуста или не заполнена."
headers = data[0]
row_4 = (
data[3]
if len(data) > 3
else []
)
row_5 = (
data[4]
if len(data) > 4
else []
)
known_groups = {
norm(item)
for item in GROUP_ALIASES
}
own_lines = []
captured_lines = []
# Колонка 0 = служебная "Группы и комитеты".
for col in range(
1,
len(headers),
):
group = str(
headers[col] or ""
).strip()
if not group:
continue
if norm(group) not in known_groups:
continue
own_status = (
str(
row_4[col] or ""
).strip()
if col < len(row_4)
else ""
)
captured = (
str(
row_5[col] or ""
).strip()
if col < len(row_5)
else ""
)
if not own_status:
own_text = (
"статус своего флага не указан"
)
elif norm(own_status) == "на месте":
own_text = (
"свой флаг на месте"
)
else:
own_text = (
f"свой флаг {own_status}"
)
own_lines.append(
f"🚩 *{group}*: {own_text}"
)
for flag in split_flags(
captured
):
captured_lines.append(
f"🏴 {group} ← {flag}"
)
sections = []
# Первый блок: где сейчас находится свой флаг.
if own_lines:
sections.append(
"🚩 СОСТОЯНИЕ СОБСТВЕННЫХ ФЛАГОВ\n"
+ "\n".join(own_lines)
)
# Второй блок: кто какие чужие флаги захватил.
if captured_lines:
sections.append(
"\n🏴 ЗАХВАЧЕННЫЕ ФЛАГИ\n"
+ "\n".join(captured_lines)
)
return (
"\n".join(sections)
if sections
else "📊 Таблица пуста или не заполнена."
)
def generate_summary(
sheet_obj,
) -> str:
data = sheet_obj.get(
"1:5"
)
return build_summary_from_data(
data
)
def get_cached_summary(
sheet_obj,
creds,
) -> tuple[str, str]:
now = time.time()
if (
_summary_cache["data"] is not None
and now - _summary_cache["timestamp"]
< CACHE_TTL_SECONDS
):
return (
_summary_cache["data"],
_summary_cache["modified_time"],
)
summary = generate_summary(
sheet_obj
)
modified_time = get_sheet_modified_time(
creds
)
_summary_cache["data"] = summary
_summary_cache["timestamp"] = now
_summary_cache["modified_time"] = modified_time
return (
summary,
modified_time,
)
# ==============================================================================
# 7. TELEGRAM HELPERS / FILTERS
# ==============================================================================
def get_message_text(
message: types.Message,
) -> str:
return (
message.text
or message.caption
or ""
)
def has_bot_mention(
text: str,
) -> bool:
return norm(BOT_MENTION) in norm(text)
def is_summary_text(
text: str,
) -> bool:
"""
Распознаёт только команду сводки.
Поддерживаются:
- флаги
- /флаги
- @googleanvrn_bot флаги
Команда summary намеренно НЕ поддерживается.
"""
clean = norm(text)
if not clean:
return False
mention = norm(
BOT_MENTION
)
if mention:
clean = clean.replace(
mention,
" ",
).strip()
clean = clean.lstrip(
"/"
).strip()
return clean == "флаги"
def is_summary_filter(
message: types.Message,
) -> bool:
return is_summary_text(
get_message_text(message)
)
def is_flag_event_filter(
message: types.Message,
) -> bool:
text = get_message_text(
message
)
if not text:
return False
if is_summary_text(text):
return False
return (
has_bot_mention(text)
and "флаг" in norm(text)
)
# ==============================================================================
# 8. ПОМОЩЬ
# ==============================================================================
HELP_TEXT = (
"🤖 Бот учета флагов\n\n"
"Если захватили флаг:\n"
f"{BOT_MENTION} Космос забрал флаг у ЮЗ\n\n"
"Если вернули свой флаг:\n"
f"{BOT_MENTION} Космос вернул свой флаг\n\n"
"Чтобы посмотреть статистику:\n"
"флаги\n\n"
"Как пользоваться ботом:\n"
"/help\n"
"/помощь"
)
# ==============================================================================
# 9. GOOGLE + TELEGRAM
# ==============================================================================
def connect_sheet():
if not CREDENTIALS_FILE.exists():
raise FileNotFoundError(
"Файл credentials.json не найден: "
f"{CREDENTIALS_FILE}"
)
if not BOT_TOKEN.strip():
raise ValueError(
"BOT_TOKEN не заполнен"
)
if not SPREADSHEET_ID.strip():
raise ValueError(
"SPREADSHEET_ID не заполнен"
)
creds = Credentials.from_service_account_file(
str(CREDENTIALS_FILE),
scopes=SCOPES,
)
gc = gspread.authorize(
creds
)
worksheet = gc.open_by_key(
SPREADSHEET_ID
).sheet1
logging.info(
"✅ Google Таблица успешно подключена"
)
return (
creds,
gc,
worksheet,
)
async def cmd_start(
message: types.Message,
):
await message.answer(
HELP_TEXT
)
async def cmd_help(
message: types.Message,
):
await message.answer(
HELP_TEXT
)
async def cmd_summary(
message: types.Message,
):
await message.answer(
"⏳ Собираю данные из таблицы..."
)
try:
summary, modified_time = (
await asyncio.to_thread(
get_cached_summary,
sheet,
creds,
)
)
response = (
f"🕒 *Последнее обновление таблицы:* "
f"{modified_time}\n\n"
f"{summary}"
)
await message.answer(
response,
parse_mode="Markdown",
)
except gspread.exceptions.APIError as exc:
if "429" in str(exc):
logging.warning(
"⚠️ Превышен лимит запросов Google Sheets (429)"
)
await message.answer(
"⚠️ Google временно ограничил чтение таблицы. "
"Повторите запрос через минуту."
)
else:
logging.error(
"Ошибка Google API: %s",
exc,
exc_info=True,
)
await message.answer(
"❌ Ошибка при обращении к Google Таблице."
)
except Exception as exc:
logging.error(
"Ошибка генерации сводки: %s",
exc,
exc_info=True,
)
await message.answer(
"❌ Не удалось прочитать таблицу."
)
async def handle_flag_event(
message: types.Message,
):
text = get_message_text(
message
)
event = parse_flag_event(
text
)
if not event:
await message.reply(
"⚠️ Не удалось однозначно распознать событие.\n\n"
"Примеры:\n"
f"{BOT_MENTION} Космос забрал флаг у ЮЗ\n"
f"{BOT_MENTION} Космос вернул свой флаг"
)
return
try:
# Все изменения таблицы выполняются последовательно.
async with sheet_update_lock:
await asyncio.to_thread(
apply_event,
sheet,
event,
)
invalidate_summary_cache()
if event["kind"] == "own_return":
response = (
f"✅ Зафиксировано: *{event['owner']}* "
"вернул(а) свой флаг. "
"Теперь он на месте."
)
else:
response = (
f"✅ Зафиксировано: *{event['winner']}* "
f"забрал(а) флаг у *{event['owner']}*."
)
await message.reply(
response,
parse_mode="Markdown",
)
except ValueError as exc:
logging.warning(
"Ошибка валидации: %s",
exc,
)
await message.reply(
f"❌ {exc}"
)
except gspread.exceptions.APIError as exc:
if "429" in str(exc):
logging.warning(
"⚠️ Превышен лимит запросов Google Sheets (429)"
)
await message.reply(
"⚠️ Google временно ограничил запись в таблицу. "
"Повторите через минуту."
)
else:
logging.error(
"Ошибка Google API: %s",
exc,
exc_info=True,
)
await message.reply(
"❌ Ошибка записи в Google Таблицу."
)
except Exception as exc:
logging.error(
"Критическая ошибка записи: %s",
exc,
exc_info=True,
)
await message.reply(
"❌ Произошла непредвиденная ошибка при записи."
)
# ==============================================================================
# 10. ЗАПУСК
# ==============================================================================
async def main():
global bot, dp, gc, sheet, creds
creds, gc, sheet = connect_sheet()
bot = Bot(
token=BOT_TOKEN
)
dp = Dispatcher()
# /start
dp.message.register(
cmd_start,
Command("start"),
)
# /help
dp.message.register(
cmd_help,
Command("help"),
)
# /помощь
dp.message.register(
cmd_help,
Command("помощь"),
)
# /флаги
dp.message.register(
cmd_summary,
Command("флаги"),
)
# флаги
# @googleanvrn_bot флаги
dp.message.register(
cmd_summary,
is_summary_filter,
)
# События захвата/возврата флагов.
dp.message.register(
handle_flag_event,
F.text | F.caption,
is_flag_event_filter,
)
logging.info(
"🚀 Бот запущен"
)
await dp.start_polling(
bot
)
bot = None
gc = None
sheet = None
creds = None
dp = Dispatcher()
# ==============================================================================
# 11. ENTRY POINT
# ==============================================================================
if __name__ == "__main__":
try:
asyncio.run(
main()
)
except KeyboardInterrupt:
logging.info(
"🛑 Бот остановлен пользователем"
)