Files
Zilant2025/tg_mainbot.py
T
gitadmin 7c4efdbc8e
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline was successful
[ВОЛК] коррекция бота под повторные попытки
2026-01-11 19:05:01 +03:00

1536 lines
70 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 os
import logging
import time
import errno
from dotenv import load_dotenv
import telebot
from telebot import apihelper
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
import pymysql
from datetime import datetime, timezone
import sys
from urllib.parse import urlparse
import requests
from io import BytesIO
from flask import Flask, request
from formatter import get_post_text, get_event_text
# Создаем Flask app на верхнем уровне для экспорта
app = Flask(__name__)
# Загружаем переменные окружения из файла .env
load_dotenv()
# Получаем данные из переменных окружения
RESPONDER_BOT_TOKEN = os.getenv('RESPONDER_BOT_TOKEN')
CHANNEL_ID = os.getenv('CHANNEL_ID')
WEBHOOK_URL = os.getenv('WEBHOOK_URL')
WEBHOOK_PORT = int(os.getenv('WEBHOOK_PORT', '8443'))
WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET')
LOG_FILE = os.getenv('LOG_FILE', 'bot.log')
# Настройка логирования
logger = logging.getLogger('TG_bot')
logger.setLevel(logging.INFO)
# Отключаем propagate, чтобы сообщения не уходили в root logger
logger.propagate = False
# Очищаем существующие handlers, если они есть (на случай повторного импорта)
if logger.handlers:
logger.handlers.clear()
formatter = logging.Formatter('[%(asctime)s] [%(name)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
class RetryFileHandler(logging.FileHandler):
def emit(self, record):
for _ in range(5):
try:
super().emit(record)
return
except (IOError, PermissionError):
time.sleep(0.5)
print(f"Failed to write to log file after 5 attempts: {record.msg}")
file_handler = RetryFileHandler(LOG_FILE, encoding='utf-8')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Логируем успешную настройку логирования (это должно попасть и в консоль, и в файл)
logger.info(f"Логирование настроено. Файл лога: {LOG_FILE}")
logger.info(f"Количество обработчиков: {len(logger.handlers)}")
MAX_CAPTION_LENGTH = int(os.getenv('MAX_CAPTION_LENGTH', 1000))
MAX_TEXT_LENGTH = int(os.getenv('MAX_TEXT_LENGTH', 4000))
# Устанавливаем единое ограничение длины текста
UNIFIED_MAX_LENGTH = 800
# Режим работы бота (ZILANT или VOLK)
BOT_MODE = os.getenv('WORKMODE', 'ZILANT').upper()
if BOT_MODE not in ('ZILANT', 'VOLK'):
logger.warning(f"Неизвестный режим бота: {BOT_MODE}, используем ZILANT")
BOT_MODE = 'ZILANT'
logger.info(f"Режим работы бота: {BOT_MODE}")
def get_welcome_text():
"""Возвращает приветственный текст в зависимости от режима"""
logger.info(f"starting get_welcome_text")
if BOT_MODE == 'VOLK':
return (
"🌟 <b>Привет!</b> 🌟\n\n"
"Я - информационный бот ВОЛКа. "
"Здесь можно подписаться на интересные события будущего конвента - и потом напомнить самому себе, "
"что именно показалось интересным.\n\n"
)
else: # ZILANT
return (
"🌟 <b>Привет!</b> 🌟\n\n"
"Я - информационный бот Зиланткона. "
"Здесь можно подписаться на интересные события будущего Зиланта - и я напомню,"
"когда их можно будет включить в свой План Захвата Конвента.\n\n"
)
def get_help_text():
"""Возвращает текст справки в зависимости от режима"""
logger.info(f"starting get_help_text")
if BOT_MODE == 'VOLK':
return (
"️ <b>Подсказка информ-бота ВОЛКа</b>\n\n"
"Я запоминаю, кто чем интересовался, и позволяю "
"вспомнить, что именно вызывало интерес.\n\n"
"<b>Доступные команды:</b>\n"
"• /start - начать работу с ботом (вы уже здесь!)\n"
"• /help - показать эту справку\n"
"• /home - перейти в основной канал ВОЛКа\n"
"• /list - показать текущие подписки\n\n"
"Используйте кнопки ниже для быстрого доступа:"
)
else: # ZILANT
return (
"️ <b>Подсказка информ-бота Зиланткона</b>\n\n"
"Я запоминаю, кто чем интересовался, и напоминаю, "
"когда это становится можно включить в План Захвата.\n\n"
"<b>Доступные команды:</b>\n"
"• /start - начать работу с ботом (вы уже здесь!)\n"
"• /help - показать эту справку\n"
"• /home - перейти в основной канал Зиланткона\n"
"• /list - показать текущие подписки\n\n"
"Используйте кнопки ниже для быстрого доступа:"
)
# Извлекаем путь из WEBHOOK_URL
if WEBHOOK_URL:
try:
parsed_url = urlparse(WEBHOOK_URL)
WEBHOOK_PATH = parsed_url.path
if not WEBHOOK_PATH:
WEBHOOK_PATH = '/'
logger.info(f"Извлечен путь вебхука: {WEBHOOK_PATH}")
except Exception as e:
logger.error(f"Ошибка парсинга WEBHOOK_URL: {e}")
WEBHOOK_PATH = '/zil_bot'
logger.info(f"Форсирован путь вебхука: {WEBHOOK_PATH}")
else:
WEBHOOK_PATH = '/zil_bot'
logger.warning("WEBHOOK_URL не задан, используем путь по умолчанию")
# Данные для подключения к MariaDB
MDB_HOST = os.getenv('MDB_HOST')
MDB_USER = os.getenv('MDB_USER')
MDB_PW = os.getenv('MDB_PW')
MDBASE = os.getenv('MDBASE')
# Проверка обязательных переменных
required_vars = {
"RESPONDER_BOT_TOKEN": RESPONDER_BOT_TOKEN,
"CHANNEL_ID": CHANNEL_ID,
"WEBHOOK_URL": WEBHOOK_URL,
"MDB_HOST": MDB_HOST,
"MDB_USER": MDB_USER,
"MDB_PW": MDB_PW,
"MDBASE": MDBASE
}
for name, value in required_vars.items():
if not value:
logger.error(f"Требуется переменная окружения {name}")
exit(1)
# Настройка таймаутов для Telegram API
apihelper.READ_TIMEOUT = 120 # Таймаут на чтение ответа (увеличен для больших сообщений)
apihelper.CONNECT_TIMEOUT = 30 # Таймаут на подключение
apihelper.RETRY_ON_TIMEOUT = True # Автоматические повторные попытки при таймауте
# Создаем экземпляр бота
bot = telebot.TeleBot(RESPONDER_BOT_TOKEN)
def get_user_name(user):
"""Форматирует имя пользователя для записи в БД"""
# logger.info(f"starting get_user_name")
if user.username:
return f"@{user.username}"
else:
# Используем имя и фамилию, если username отсутствует
name_parts = []
if user.first_name:
name_parts.append(user.first_name)
if user.last_name:
name_parts.append(user.last_name)
return ' '.join(name_parts) if name_parts else "Неизвестный пользователь"
def format_user_info(user_id, user_name):
"""Форматирует информацию о пользователе для логов"""
return f"{user_name} ({user_id})"
def log_event(event_type, user_info, post_id=None, message=None):
"""Логирует событие с повторными попытками при блокировке файла"""
timestamp = datetime.now(timezone.utc).strftime('[%Y-%m-%d %H:%M:%S]')
log_line = f"{timestamp} [TG_bot] "
if event_type == "subscribe":
log_line += f"Пользователь {user_info} подписался на событие ID {post_id}"
elif event_type == "unsubscribe":
log_line += f"Пользователь {user_info} отписался от событие ID {post_id}"
elif event_type == "list_request":
log_line += f"Пользователь {user_info} запросил список подписок"
elif event_type == "error":
log_line += f"ОШИБКА: {message} (Пользователь {user_info})"
elif event_type == "security":
log_line += f"SECURITY: {message}"
elif event_type == "system":
log_line += f"SYSTEM: {message}"
elif event_type == "info":
log_line += f"ИНФО: {message} (Пользователь {user_info})"
else:
log_line += f"Неизвестное событие: {event_type} (Пользователь {user_info})"
# Параметры повторных попыток
intervals = [0.1, 0.2, 0.4, 0.8] # Интервалы между попытками
max_attempts = len(intervals) + 1 # +1 для первой попытки
attempt = 0
while attempt < max_attempts:
try:
with open(LOG_FILE, 'a', encoding='utf-8') as log_file:
log_file.write(log_line + '\n')
log_file.flush() # Обеспечить немедленную запись
return
except (IOError, OSError) as e:
# Проверяем, является ли ошибка блокировкой файла
if e.errno in (errno.EAGAIN, errno.EACCES, errno.EWOULDBLOCK, errno.EBUSY):
if attempt < max_attempts - 1:
time.sleep(intervals[attempt])
attempt += 1
continue
# Для других ошибок сразу прерываем цикл
break
except Exception as e:
# Обрабатываем все остальные исключения
break
# Все попытки провалились
error_msg = f"Не удалось записать в лог после {max_attempts} попыток. Ошибка: {e}"
sys.stderr.write(error_msg + '\n')
sys.stderr.write(f"Событие: {log_line}\n")
def create_db_connection():
"""Создает подключение к базе данных MariaDB через pymysql"""
logger.info(f"starting create_db_connection")
try:
conn = pymysql.connect(
host=MDB_HOST,
user=MDB_USER,
password=MDB_PW,
database=MDBASE,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor,
init_command="SET time_zone='+00:00'" # Устанавливаем часовой пояс UTC
)
return conn
except pymysql.Error as e:
error_msg = f"Ошибка подключения к базе данных: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return None
def is_user_subscribed(post_id, user_id):
"""Проверяет, подписан ли пользователь уже на это событие"""
logger.info(f"starting is_user_subscribed")
conn = create_db_connection()
if conn is None:
return False
try:
with conn.cursor() as cursor:
sql = """
SELECT COUNT(*) as count
FROM marks
WHERE tg_post_id = %s AND tg_user_id = %s
"""
cursor.execute(sql, (post_id, user_id))
result = cursor.fetchone()
return result['count'] > 0
except pymysql.Error as e:
error_msg = f"Ошибка при проверке подписки: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return False
finally:
conn.close()
def is_user_subscribed_evt(event_id, user_id):
"""Проверяет, подписан ли пользователь уже на это событие (таблица events)"""
logger.info(f"starting is_user_subscribed_evt")
conn = create_db_connection()
if conn is None:
return False
try:
with conn.cursor() as cursor:
sql = """
SELECT COUNT(*) as count
FROM marks_evt
WHERE tg_event_id = %s AND tg_user_id = %s
"""
cursor.execute(sql, (event_id, user_id))
result = cursor.fetchone()
return result['count'] > 0
except pymysql.Error as e:
error_msg = f"Ошибка при проверке подписки на событие: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return False
finally:
conn.close()
def get_user_subscriptions(user_id):
"""Возвращает список событий, на которые подписан пользователь, с shortname"""
logger.info(f"starting get_user_subscriptions")
conn = create_db_connection()
if conn is None:
return []
try:
with conn.cursor() as cursor:
sql = """
SELECT m.tg_post_id, COALESCE(p.shortname, '') as shortname
FROM marks m
LEFT JOIN posts p ON m.tg_post_id = p.tg_message_id
WHERE m.tg_user_id = %s
ORDER BY m.created_at DESC
"""
cursor.execute(sql, (user_id,))
result = cursor.fetchall()
return [{'post_id': row['tg_post_id'], 'shortname': row['shortname']} for row in result]
except pymysql.Error as e:
error_msg = f"Ошибка при получении подписок: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return []
finally:
conn.close()
def get_user_subscriptions_evt(user_id):
"""Возвращает список событий, на которые подписан пользователь, с информацией о статусе"""
logger.info(f"starting get_user_subscriptions_evt")
conn = create_db_connection()
if conn is None:
return []
try:
with conn.cursor() as cursor:
sql = """
SELECT m.tg_event_id, COALESCE(e.name, '') as name, e.is_posted_tg, e.is_visible
FROM marks_evt m
LEFT JOIN events e ON m.tg_event_id = e.tg_message_id
WHERE m.tg_user_id = %s
ORDER BY m.created_at DESC
"""
cursor.execute(sql, (user_id,))
result = cursor.fetchall()
return [{'event_id': row['tg_event_id'], 'name': row['name'], 'is_posted_tg': row['is_posted_tg'], 'is_visible': row['is_visible']} for row in result]
except pymysql.Error as e:
error_msg = f"Ошибка при получении подписок на события: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return []
finally:
conn.close()
def get_linked_events_for_post(post_id):
"""Возвращает список опубликованных мероприятий, связанных с анонсом через таблицу interlinks"""
logger.info(f"starting get_linked_events_for_post")
conn = create_db_connection()
if conn is None:
return []
try:
with conn.cursor() as cursor:
sql = """
SELECT e.name, e.tg_message_id
FROM interlinks i
JOIN events e ON i.event_id = e.id
JOIN posts p ON i.post_id = p.id
WHERE p.tg_message_id = %s AND e.name IS NOT NULL AND e.name != '' AND e.is_posted_tg = 1
ORDER BY e.name
"""
cursor.execute(sql, (post_id,))
result = cursor.fetchall()
return [{'name': row['name'], 'tg_message_id': row['tg_message_id']} for row in result]
except pymysql.Error as e:
error_msg = f"Ошибка при получении связанных мероприятий: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return []
finally:
conn.close()
def save_mark_to_db(post_id, user_id, user_name):
"""Сохраняет отметку пользователя в базе данных"""
logger.info(f"starting save_mark_to_db")
conn = create_db_connection()
if conn is None:
return False
try:
with conn.cursor() as cursor:
# Используем INSERT IGNORE для предотвращения дубликатов
sql = """
INSERT IGNORE INTO marks (tg_post_id, tg_user_id, tg_user_name, created_at)
VALUES (%s, %s, %s, UTC_TIMESTAMP())
"""
cursor.execute(sql, (post_id, user_id, user_name))
conn.commit()
if cursor.rowcount > 0:
logger.info(f"Добавлена запись: событие={post_id}, пользователь={user_id}, имя={user_name}")
user_info = format_user_info(user_id, user_name)
log_event("subscribe", user_info, post_id)
return True
else:
logger.info(f"Запись уже существует: событие={post_id}, пользователь={user_id}")
# Добавляем запись в лог о попытке повторной подписки
user_info = format_user_info(user_id, user_name)
log_event("info", user_info, post_id,
f"Попытка повторной подписки: событие={post_id}, пользователь={user_info}")
return False
except pymysql.Error as e:
error_msg = f"Ошибка при сохранении в БД: {e}"
logger.error(error_msg)
user_info = format_user_info(user_id, user_name)
log_event("error", user_info, post_id, error_msg)
return False
finally:
conn.close()
def save_mark_to_db_evt(event_id, user_id, user_name):
"""Сохраняет отметку пользователя в базе данных для событий (таблица marks_evt)"""
logger.info(f"starting save_mark_to_db_evt")
conn = create_db_connection()
if conn is None:
return False
try:
with conn.cursor() as cursor:
sql = """
INSERT IGNORE INTO marks_evt (tg_event_id, tg_user_id, tg_user_name, created_at)
VALUES (%s, %s, %s, UTC_TIMESTAMP())
"""
cursor.execute(sql, (event_id, user_id, user_name))
conn.commit()
if cursor.rowcount > 0:
logger.info(f"Добавлена запись: событие={event_id}, пользователь={user_id}, имя={user_name}")
user_info = format_user_info(user_id, user_name)
log_event("subscribe", user_info, event_id)
return True
else:
logger.info(f"Запись уже существует: событие={event_id}, пользователь={user_id}")
user_info = format_user_info(user_id, user_name)
log_event("info", user_info, event_id,
f"Попытка повторной подписки: событие={event_id}, пользователь={user_info}")
return False
except pymysql.Error as e:
error_msg = f"Ошибка при сохранении в БД (events): {e}"
logger.error(error_msg)
user_info = format_user_info(user_id, user_name)
log_event("error", user_info, event_id, error_msg)
return False
finally:
conn.close()
def remove_mark_from_db(post_id, user_id, user_name):
"""Удаляет отметку пользователя из базы данных"""
logger.info(f"starting remove_mark_from_db")
conn = create_db_connection()
if conn is None:
return False
try:
with conn.cursor() as cursor:
sql = """
DELETE FROM marks
WHERE tg_post_id = %s AND tg_user_id = %s
"""
cursor.execute(sql, (post_id, user_id))
conn.commit()
if cursor.rowcount > 0:
logger.info(f"Удалена запись: событие={post_id}, пользователь={user_id}")
user_info = format_user_info(user_id, user_name)
log_event("unsubscribe", user_info, post_id)
return True
else:
logger.info(f"Запись не найдена: событие={post_id}, пользователь={user_id}")
# Добавляем запись в лог о попытке отписки без подписки
user_info = format_user_info(user_id, user_name)
log_event("info", user_info, post_id,
f"Попытка отписки без подписки: событие={post_id}, пользователь={user_info}")
return False
except pymysql.Error as e:
error_msg = f"Ошибка при удалении из БД: {e}"
logger.error(error_msg)
user_info = format_user_info(user_id, user_name)
log_event("error", user_info, post_id, error_msg)
return False
finally:
conn.close()
def remove_mark_from_db_evt(event_id, user_id, user_name):
"""Удаляет отметку пользователя из базы данных для событий (таблица marks_evt)"""
logger.info(f"starting remove_mark_from_db_evt")
conn = create_db_connection()
if conn is None:
return False
try:
with conn.cursor() as cursor:
sql = """
DELETE FROM marks_evt
WHERE tg_event_id = %s AND tg_user_id = %s
"""
cursor.execute(sql, (event_id, user_id))
conn.commit()
if cursor.rowcount > 0:
logger.info(f"Удалена запись: событие={event_id}, пользователь={user_id}")
user_info = format_user_info(user_id, user_name)
log_event("unsubscribe", user_info, event_id)
return True
else:
logger.info(f"Запись не найдена: событие={event_id}, пользователь={user_id}")
user_info = format_user_info(user_id, user_name)
log_event("info", user_info, event_id,
f"Попытка отписки без подписки: событие={event_id}, пользователь={user_info}")
return False
except pymysql.Error as e:
error_msg = f"Ошибка при удалении из БД (events): {e}"
logger.error(error_msg)
user_info = format_user_info(user_id, user_name)
log_event("error", user_info, event_id, error_msg)
return False
finally:
conn.close()
def format_channel_link(post_id=None):
"""Форматирует ссылку на событие в канале или на сам канал"""
logger.info(f"starting format_channel_link")
if not CHANNEL_ID:
return ""
if post_id:
if CHANNEL_ID.startswith('@'):
return f"https://t.me/{CHANNEL_ID[1:]}/{post_id}"
elif CHANNEL_ID.startswith('-100'):
return f"https://t.me/c/{CHANNEL_ID[4:]}/{post_id}"
else:
return f"https://t.me/c/{CHANNEL_ID}/{post_id}"
else:
if CHANNEL_ID.startswith('@'):
return f"https://t.me/{CHANNEL_ID[1:]}"
elif CHANNEL_ID.startswith('-100'):
return f"https://t.me/c/{CHANNEL_ID[4:]}"
else:
return f"https://t.me/c/{CHANNEL_ID}"
def create_help_keyboard():
"""Создает клавиатуру для справки"""
logger.info(f"starting create_help_keyboard")
keyboard = InlineKeyboardMarkup()
keyboard.row(
InlineKeyboardButton("📜 Подписки", callback_data="cmd_list"),
InlineKeyboardButton("📢 Канал", url=format_channel_link())
)
return keyboard
def create_main_keyboard():
"""Создает основную клавиатуру для /start без параметров"""
logger.info(f"starting create_main_keyboard")
keyboard = InlineKeyboardMarkup()
keyboard.row(
InlineKeyboardButton("❓ Справка", callback_data="cmd_help"),
InlineKeyboardButton("📢 Канал", url=format_channel_link())
)
return keyboard
def create_manage_keyboard(post_id, is_subscribed):
"""Создает клавиатуру для управления подпиской на посты"""
logger.info(f"starting create_manage_keyboard")
keyboard = InlineKeyboardMarkup()
channel_link = format_channel_link(post_id)
if is_subscribed:
keyboard.row(
InlineKeyboardButton("❓ Справка", callback_data="cmd_help"),
InlineKeyboardButton("❌ Отписаться", callback_data=f"unsubscribe_{post_id}"),
InlineKeyboardButton("↪ Назад", url=channel_link)
)
else:
keyboard.row(
InlineKeyboardButton("❓ Справка", callback_data="cmd_help"),
InlineKeyboardButton("✔ Подписаться", callback_data=f"subscribe_{post_id}"),
InlineKeyboardButton("↪ Назад", url=channel_link)
)
return keyboard
def create_manage_keyboard_evt(event_id, is_subscribed):
"""Создает клавиатуру для управления подпиской на события"""
logger.info(f"starting create_manage_keyboard_evt")
keyboard = InlineKeyboardMarkup()
channel_link = format_channel_link(event_id)
if is_subscribed:
keyboard.row(
InlineKeyboardButton("❓ Справка", callback_data="cmd_help"),
InlineKeyboardButton("❌ Отписаться", callback_data=f"unsubscribe_evt_{event_id}"),
InlineKeyboardButton("↪ Назад", url=channel_link)
)
else:
keyboard.row(
InlineKeyboardButton("❓ Справка", callback_data="cmd_help"),
InlineKeyboardButton("✔ Подписаться", callback_data=f"subscribe_evt_{event_id}"),
InlineKeyboardButton("↪ Назад", url=channel_link)
)
return keyboard
def get_post_data(tg_message_id):
"""Получает данные о событии по его tg_message_id"""
logger.info(f"starting get_post_data")
conn = create_db_connection()
if conn is None:
return None
try:
with conn.cursor() as cursor:
sql = """
SELECT
vk_post_id,
COALESCE(text, '') AS text,
COALESCE(image_url, '') AS image_url,
COALESCE(vk_post_url, '') AS vk_post_url,
COALESCE(poll_question, '') AS poll_question,
COALESCE(poll_options, '[]') AS poll_options,
poll_multiple,
is_poll,
COALESCE(shortname, '') AS shortname
FROM posts
WHERE tg_message_id = %s
"""
cursor.execute(sql, (tg_message_id,))
return cursor.fetchone()
except pymysql.Error as e:
error_msg = f"Ошибка при получении данных о событии: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return None
finally:
conn.close()
def get_event_data(tg_message_id):
"""Получает данные о событии из таблица events по его tg_message_id"""
logger.info(f"starting get_event_data")
conn = create_db_connection()
if conn is None:
return None
try:
with conn.cursor() as cursor:
sql = """
SELECT
about_social_picture,
COALESCE(name, '') AS name,
COALESCE(about, '') AS about
FROM events
WHERE tg_message_id = %s
"""
cursor.execute(sql, (tg_message_id,))
return cursor.fetchone()
except pymysql.Error as e:
error_msg = f"Ошибка при получении данных о событии (events): {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return None
finally:
conn.close()
def format_post_message(tg_message_id):
"""Форматирует сообщение о событии с помощью модуля formatter"""
logger.info(f"starting format_post_message")
post_data = get_post_data(tg_message_id)
if not post_data:
error_msg = f"Событие с tg_message_id={tg_message_id} не найдено в базе."
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return {
'text': "❌ Событие не найдено в базе данных. Пожалуйста, сообщите администратору.",
'image_url': None
}
try:
# Подготавливаем данные для formatter
vk_post_id = post_data['vk_post_id'] or 0
text = post_data['text'] or ""
image_url = post_data['image_url'] or ""
vk_post_url = post_data['vk_post_url'] or ""
poll_question = post_data['poll_question'] or ""
poll_options = post_data['poll_options'] or "[]"
poll_multiple = bool(post_data['poll_multiple']) if post_data['poll_multiple'] is not None else False
post_tuple = (
vk_post_id,
text,
image_url,
vk_post_url,
poll_question,
poll_options,
poll_multiple,
False, # is_event
False # Добавленный элемент со значением False
)
# Форматируем событие с единым ограничением длины
formatted = get_post_text(
post_tuple,
max_caption_length=UNIFIED_MAX_LENGTH,
max_text_length=UNIFIED_MAX_LENGTH
)
# Используем caption для сообщений с изображениями, text_message для текстовых
if image_url:
base_text = formatted.get('caption') or formatted.get('base_text') or "❌ Не удалось подготовить текст о событии."
else:
base_text = formatted.get('text_message') or formatted.get('base_text') or "❌ Не удалось подготовить текст о событии."
# Дополнительная обрезка на случай, если форматтер не обрезал текст
if len(base_text) > UNIFIED_MAX_LENGTH:
base_text = base_text[:UNIFIED_MAX_LENGTH - 3] + "..."
return {
'text': base_text,
'image_url': image_url if image_url else None
}
except Exception as e:
error_msg = f"Ошибка при форматировании текста о событии: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return {
'text': "❌ Произошла ошибка при форматировании текста о событии. Пожалуйста, попробуйте позже.",
'image_url': None
}
def format_event_message(tg_message_id):
"""Форматирует сообщение о событии из таблицы events"""
logger.info(f"starting format_event_message")
event_data = get_event_data(tg_message_id)
if not event_data:
error_msg = f"Событие с tg_message_id={tg_message_id} не найдено в базе events."
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return {
'text': "❌ Событие не найдено в базе данных. Пожалуйста, сообщите администратору.",
'image_url': None
}
try:
name = event_data['name'] or ""
about = event_data['about'] or ""
image_url = event_data['about_social_picture'] or ""
# Форматируем текст события
formatted_about = get_event_text(about, UNIFIED_MAX_LENGTH)
# Формируем итоговый текст
if name and formatted_about:
text = f"<b>{name}</b>\n{formatted_about}"
elif name:
text = f"<b>{name}</b>"
elif formatted_about:
text = formatted_about
else:
text = "❌ Не удалось подготовить текст о событии."
# Дополнительная обрезка на случай, если текст слишком длинный
if len(text) > UNIFIED_MAX_LENGTH:
text = text[:UNIFIED_MAX_LENGTH - 3] + "..."
return {
'text': text,
'image_url': image_url if image_url else None
}
except Exception as e:
error_msg = f"Ошибка при форматировании текста о событии (events): {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return {
'text': "❌ Произошла ошибка при форматировании текста о событии. Пожалуйста, попробуйте позже.",
'image_url': None
}
def download_image(image_url):
"""Загружает изображение по URL"""
logger.info(f"starting download_image")
try:
response = requests.get(image_url, timeout=10)
response.raise_for_status()
return BytesIO(response.content)
except Exception as e:
logger.error(f"Ошибка загрузки изображения: {e}")
return None
def send_post_message(chat_id, text, image_url=None, reply_markup=None):
"""Отправляет сообщение с изображением или текстом с повторными попытками при таймауте"""
logger.info(f"starting send_post_message")
# Параметры для повторных попыток при таймауте
max_retries = 5
retry_interval = 2
# Дополнительная обрезка текста
if len(text) > UNIFIED_MAX_LENGTH:
text = text[:UNIFIED_MAX_LENGTH - 3] + "..."
if image_url:
# Загружаем изображение
image = download_image(image_url)
if image:
# Отправляем фото с подписью с повторными попытками
for attempt in range(1, max_retries + 1):
try:
bot.send_photo(
chat_id,
photo=image,
caption=text,
reply_markup=reply_markup,
parse_mode='HTML'
)
logger.info(f"Сообщение с изображением успешно отправлено (попытка {attempt})")
return True
except Exception as e:
error_str = str(e)
# Проверяем, является ли это таймаутом
if 'timeout' in error_str.lower() or 'timed out' in error_str.lower():
if attempt < max_retries:
logger.warning(f"Таймаут при отправке фото (попытка {attempt}/{max_retries}), повтор через {retry_interval} сек...")
time.sleep(retry_interval)
continue
else:
logger.error(f"Таймаут при отправке фото после {max_retries} попыток: {e}")
return False
else:
# Для других ошибок не повторяем
logger.error(f"Ошибка отправки фото: {e}")
return False
return False
else:
logger.warning(f"Не удалось загрузить изображение: {image_url}")
# Если изображение отсутствует или не загружено, отправляем текст с повторными попытками
for attempt in range(1, max_retries + 1):
try:
bot.send_message(
chat_id,
text,
reply_markup=reply_markup,
parse_mode='HTML'
)
logger.info(f"Текстовое сообщение успешно отправлено (попытка {attempt})")
return True
except Exception as e:
error_str = str(e)
# Проверяем, является ли это таймаутом
if 'timeout' in error_str.lower() or 'timed out' in error_str.lower():
if attempt < max_retries:
logger.warning(f"Таймаут при отправке сообщения (попытка {attempt}/{max_retries}), повтор через {retry_interval} сек...")
time.sleep(retry_interval)
continue
else:
logger.error(f"Таймаут при отправке сообщения после {max_retries} попыток: {e}")
return False
else:
# Для других ошибок не повторяем
logger.error(f"Ошибка отправки сообщения: {e}")
return False
return False
def send_direct_message(user_id, message_text):
"""Отправляет сообщение пользователю по ID с обработкой ошибок и повторными попытками при таймауте"""
logger.info(f"starting send_direct_message")
# Параметры для повторных попыток при таймауте
max_retries = 5
retry_interval = 2
try:
# Проверяем, что сообщение не пустое
if not message_text or not message_text.strip():
error_msg = "Пустое сообщение"
logger.warning(error_msg)
log_event("error", f"user_{user_id}", message=error_msg)
return False, error_msg
# Отправляем сообщение с повторными попытками
for attempt in range(1, max_retries + 1):
try:
bot.send_message(
user_id,
message_text,
parse_mode='HTML'
)
# Логируем успешную отправку
log_event("info", f"user_{user_id}", message=f"Сообщение отправлено: {message_text[:50]}...")
logger.info(f"Сообщение успешно отправлено пользователю {user_id} (попытка {attempt})")
return True, "Сообщение отправлено успешно"
except Exception as e:
error_str = str(e)
# Проверяем, является ли это таймаутом
if 'timeout' in error_str.lower() or 'timed out' in error_str.lower():
if attempt < max_retries:
logger.warning(f"Таймаут при отправке сообщения пользователю {user_id} (попытка {attempt}/{max_retries}), повтор через {retry_interval} сек...")
time.sleep(retry_interval)
continue
else:
error_msg = f"Таймаут при отправке сообщения после {max_retries} попыток: {e}"
logger.error(error_msg)
log_event("error", f"user_{user_id}", message=error_msg)
return False, f"Таймаут при отправке сообщения. Попробуйте позже."
else:
# Для других ошибок выходим из цикла и обрабатываем дальше
break
except telebot.apihelper.ApiTelegramException as e:
error_msg = f"Ошибка Telegram API: {e}"
logger.error(error_msg)
log_event("error", f"user_{user_id}", message=error_msg)
# Определяем тип ошибки для более информативного ответа
if e.error_code == 403:
return False, f"Пользователь {user_id} заблокировал бота или не начал с ним диалог. Попросите пользователя отправить команду /start боту."
elif e.error_code == 400:
return False, f"Неверный ID пользователя: {user_id}. Проверьте правильность ID."
elif e.error_code == 429:
return False, f"Превышен лимит отправки сообщений для пользователя {user_id}. Попробуйте позже."
elif e.error_code == 404:
return False, f"Пользователь {user_id} не найден. Проверьте правильность ID."
else:
return False, f"Ошибка Telegram API (код {e.error_code}): {e.description}"
except Exception as e:
error_msg = f"Неожиданная ошибка при отправке сообщения: {e}"
logger.error(error_msg)
log_event("error", f"user_{user_id}", message=error_msg)
return False, f"Неожиданная ошибка: {str(e)}"
def send_management_message(chat_id, post_id, user_id, user_name):
"""Отправляет сообщение для управления подпиской на посты"""
logger.info(f"starting send_management_message")
try:
# Проверяем статус подписки
is_subscribed = is_user_subscribed(post_id, user_id)
# Форматируем текст о событии
post_data = format_post_message(post_id)
# Создаем клавиатуру управления
keyboard = create_manage_keyboard(post_id, is_subscribed)
# Отправляем сообщение
send_post_message(
chat_id,
post_data['text'],
image_url=post_data['image_url'],
reply_markup=keyboard
)
except Exception as e:
error_msg = f"Ошибка отправки сообщения управления: {e}"
logger.error(error_msg)
user_info = format_user_info(user_id, user_name)
log_event("error", user_info, post_id, error_msg)
def send_management_message_evt(chat_id, event_id, user_id, user_name):
"""Отправляет сообщение для управления подпиской на события"""
logger.info(f"starting send_management_message_evt")
try:
# Проверяем статус подписки
is_subscribed = is_user_subscribed_evt(event_id, user_id)
# Форматируем текст о событии
event_data = format_event_message(event_id)
# Создаем клавиатуру управления
keyboard = create_manage_keyboard_evt(event_id, is_subscribed)
# Отправляем сообщение
send_post_message(
chat_id,
event_data['text'],
image_url=event_data['image_url'],
reply_markup=keyboard
)
except Exception as e:
error_msg = f"Ошибка отправки сообщения управления (events): {e}"
logger.error(error_msg)
user_info = format_user_info(user_id, user_name)
log_event("error", user_info, event_id, error_msg)
@bot.message_handler(commands=['start'])
def handle_start(message):
"""Обработчик команды /start с параметром"""
logger.info(f"starting handle_start")
try:
# Извлекаем аргументы из команды /start
args = message.text.split()
user = message.from_user
user_id = user.id
user_name = get_user_name(user)
if len(args) > 1:
# Обработка команды подписки на посты
if args[1].startswith('post_'):
post_id = args[1].split('_')[1]
# Отправляем сообщение управления подпиской
send_management_message(message.chat.id, post_id, user_id, user_name)
return
# Обработка команды подписки на события
elif args[1].startswith('event_'):
event_id = args[1].split('_')[1]
# Отправляем сообщение управления подпиской
send_management_message_evt(message.chat.id, event_id, user_id, user_name)
return
# Команда /start без параметров
welcome_text = get_welcome_text()
# Отправляем приветственное сообщение с повторными попытками при таймауте
max_retries = 5
retry_interval = 2
for attempt in range(1, max_retries + 1):
try:
bot.send_message(
message.chat.id,
welcome_text,
reply_markup=create_main_keyboard(),
parse_mode='HTML'
)
logger.info(f"Приветственное сообщение отправлено (попытка {attempt})")
break
except Exception as e:
error_str = str(e)
if 'timeout' in error_str.lower() or 'timed out' in error_str.lower():
if attempt < max_retries:
logger.warning(f"Таймаут при отправке приветственного сообщения (попытка {attempt}/{max_retries}), повтор через {retry_interval} сек...")
time.sleep(retry_interval)
continue
else:
logger.error(f"Таймаут при отправке приветственного сообщения после {max_retries} попыток: {e}")
raise
else:
raise
except Exception as e:
error_msg = f"Ошибка в обработке /start: {e}"
logger.error(error_msg)
user_info = format_user_info(message.from_user.id, get_user_name(message.from_user)) if message.from_user else "unknown"
log_event("error", user_info, message=error_msg)
@bot.message_handler(commands=['home'])
def handle_home(message):
"""Обработчик команды /home - переход в основной канал"""
logger.info(f"starting handle_home")
try:
channel_link = format_channel_link()
channel_name = "ВОЛКа" if BOT_MODE == 'VOLK' else "Зиланткона"
bot.send_message(
message.chat.id,
f"📢 <b>Основной канал {channel_name}</b>\n\n",
reply_markup=InlineKeyboardMarkup().row(
InlineKeyboardButton("📢 Перейти в канал", url=channel_link)
),
parse_mode='HTML'
)
except Exception as e:
error_msg = f"Ошибка в обработке /home: {e}"
logger.error(error_msg)
user_info = format_user_info(message.from_user.id, get_user_name(message.from_user)) if message.from_user else "unknown"
log_event("error", user_info, message=error_msg)
@bot.message_handler(commands=['help'])
def handle_help(message):
"""Обработчик команды /help"""
logger.info(f"starting handle_help")
try:
help_text = get_help_text()
bot.send_message(
message.chat.id,
help_text,
reply_markup=create_help_keyboard(),
parse_mode='HTML'
)
except Exception as e:
error_msg = f"Ошибка в обработке /help: {e}"
logger.error(error_msg)
user_info = format_user_info(message.from_user.id, get_user_name(message.from_user)) if message.from_user else "unknown"
log_event("error", user_info, message=error_msg)
@bot.message_handler(commands=['list'])
def handle_list(message):
"""Обработчик команды /list"""
logger.info(f"starting handle_list")
try:
user_id = message.from_user.id
user_name = get_user_name(message.from_user)
# Получаем подписки на посты и события
post_subscriptions = get_user_subscriptions(user_id)
event_subscriptions = get_user_subscriptions_evt(user_id) if BOT_MODE == 'ZILANT' else []
# Формируем ответ
response = "📋 <b>Ваши подписки</b>\n\n"
# Добавляем подписки на посты
if post_subscriptions:
response += "📝 <b>Анонсы:</b>\n"
for sub in post_subscriptions:
post_id = sub['post_id']
shortname = sub['shortname']
post_link = format_channel_link(post_id)
# Используем shortname если есть, иначе ID события
if shortname:
display_name = shortname
else:
# Получаем vk_post_id для отображения
post_data = get_post_data(post_id)
vk_post_id = post_data['vk_post_id'] if post_data else "N/A"
display_name = f"Анонс ВК {vk_post_id}"
# Названия постов выводим подчеркнутыми и жирным шрифтом
response += f"• <u><b><a href='{post_link}'>{display_name}</a></b></u>\n"
# Получаем связанные мероприятия (только опубликованные) - только для ZILANT
if BOT_MODE == 'ZILANT':
linked_events = get_linked_events_for_post(post_id)
if linked_events:
for event in linked_events:
event_link = format_channel_link(event['tg_message_id'])
response += f" └─ <a href='{event_link}'>{event['name']}</a>\n"
response += "\n"
# Добавляем подписки на события (только для ZILANT)
if BOT_MODE == 'ZILANT' and event_subscriptions:
response += "🎭 <b>Мероприятия:</b>\n"
for sub in event_subscriptions:
event_id = sub['event_id']
name = sub['name']
is_posted_tg = sub.get('is_posted_tg', False)
is_visible = sub.get('is_visible', False)
# Используем name если есть, иначе ID события
display_name = name if name else f"Мероприятие {event_id}"
# Проверяем статус события
if is_posted_tg and is_visible:
# Опубликованное и видимое событие - с ссылкой
event_link = format_channel_link(event_id)
response += f"• <a href='{event_link}'>{display_name}</a>\n"
else:
# Неопубликованное или невидимое событие - без ссылки и зачеркнутым
response += f"• <s>{display_name}</s>\n"
# Если нет подписок
if not post_subscriptions and not event_subscriptions:
response += "Вы пока не подписаны ни на одно событие.\n\n"
bot.send_message(
message.chat.id,
response,
reply_markup=create_main_keyboard(),
parse_mode='HTML',
disable_web_page_preview=True
)
# Логируем запрос списка подписок
user_info = format_user_info(user_id, user_name)
log_event("list_request", user_info)
except Exception as e:
error_msg = f"Ошибка в обработке /list: {e}"
logger.error(error_msg)
user_info = format_user_info(message.from_user.id, get_user_name(message.from_user)) if message.from_user else "unknown"
log_event("error", user_info, message=error_msg)
@bot.callback_query_handler(func=lambda call: True)
def handle_callback(call):
"""Обработчик нажатий на кнопки"""
logger.info(f"starting handle_callback")
try:
# Обработка командных кнопок
if call.data.startswith("cmd_"):
command = call.data[4:]
if command == "help":
help_text = get_help_text()
bot.send_message(
call.message.chat.id,
help_text,
reply_markup=create_help_keyboard(),
parse_mode='HTML'
)
elif command == "list":
user = call.from_user
user_id = user.id
user_name = get_user_name(user)
# Получаем подписки на посты и события
post_subscriptions = get_user_subscriptions(user_id)
event_subscriptions = get_user_subscriptions_evt(user_id) if BOT_MODE == 'ZILANT' else []
# Формируем ответ
response = "📋 <b>Ваши подписки</b>\n\n"
# Добавляем подписки на посты
if post_subscriptions:
response += "📝 <b>Анонсы:</b>\n"
for sub in post_subscriptions:
post_id = sub['post_id']
shortname = sub['shortname']
post_link = format_channel_link(post_id)
# Используем shortname если есть, иначе ID события
if shortname:
display_name = shortname
else:
# Получаем vk_post_id для отображения
post_data = get_post_data(post_id)
vk_post_id = post_data['vk_post_id'] if post_data else "N/A"
display_name = f"Анонс ВК {vk_post_id}"
# Названия постов выводим подчеркнутыми и жирным шрифтом
response += f"• <u><b><a href='{post_link}'>{display_name}</a></b></u>\n"
# Получаем связанные мероприятия (только опубликованные) - только для ZILANT
if BOT_MODE == 'ZILANT':
linked_events = get_linked_events_for_post(post_id)
if linked_events:
for event in linked_events:
event_link = format_channel_link(event['tg_message_id'])
response += f" └─ <a href='{event_link}'>{event['name']}</a>\n"
response += "\n"
# Добавляем подписки на события (только для ZILANT)
if BOT_MODE == 'ZILANT' and event_subscriptions:
response += "🎭 <b>Мероприятия:</b>\n"
for sub in event_subscriptions:
event_id = sub['event_id']
name = sub['name']
is_posted_tg = sub.get('is_posted_tg', False)
is_visible = sub.get('is_visible', False)
# Используем name если есть, иначе ID события
display_name = name if name else f"Мероприятие {event_id}"
# Проверяем статус события
if is_posted_tg and is_visible:
# Опубликованное и видимое событие - с ссылкой
event_link = format_channel_link(event_id)
response += f"• <a href='{event_link}'>{display_name}</a>\n"
else:
# Неопубликованное или невидимое событие - без ссылки и зачеркнутым
response += f"• <s>{display_name}</s>\n"
# Если нет подписок
if not post_subscriptions and not event_subscriptions:
response += "Вы пока не подписаны ни на одно событие.\n\n"
bot.send_message(
call.message.chat.id,
response,
reply_markup=create_main_keyboard(),
parse_mode='HTML',
disable_web_page_preview=True
)
# Логируем запрос списка подписок
user_info = format_user_info(user_id, user_name)
log_event("list_request", user_info)
elif command == "back":
# Возврат к главному меню
welcome_text = get_welcome_text()
bot.send_message(
call.message.chat.id,
welcome_text,
reply_markup=create_main_keyboard(),
parse_mode='HTML'
)
# Подтверждаем получение callback
bot.answer_callback_query(call.id)
return
# Обработка кнопок управления подпиской на посты
if (call.data.startswith("subscribe_") or call.data.startswith("unsubscribe_")) and not call.data.startswith(("subscribe_evt_", "unsubscribe_evt_")):
user = call.from_user
user_id = user.id
user_name = get_user_name(user)
# Разделяем данные callback
parts = call.data.split('_', 1)
action = parts[0]
post_id = parts[1]
# Выполняем действие
if action == "subscribe":
save_mark_to_db(post_id, user_id, user_name)
result_text = "✅ Вы успешно подписались!"
else:
remove_mark_from_db(post_id, user_id, user_name)
result_text = "✅ Вы успешно отписались!"
# Обновляем сообщение с новым статусом
try:
# Получаем текущие данные о событии
post_data = format_post_message(post_id)
text = post_data['text']
# Создаем новую клавиатуру
is_subscribed = action == "subscribe"
keyboard = create_manage_keyboard(post_id, is_subscribed)
# Для сообщений с изображением
if call.message.content_type == 'photo':
# Получаем file_id существующего изображения
file_id = call.message.photo[-1].file_id
# Редактируем подпись к изображению
bot.edit_message_caption(
chat_id=call.message.chat.id,
message_id=call.message.message_id,
caption=text,
reply_markup=keyboard,
parse_mode='HTML'
)
else:
# Редактируем текстовое сообщение
bot.edit_message_text(
chat_id=call.message.chat.id,
message_id=call.message.message_id,
text=text,
reply_markup=keyboard,
parse_mode='HTML'
)
# Отправляем отдельное сообщение о результате
bot.answer_callback_query(call.id, result_text)
except Exception as e:
logger.error(f"Ошибка обновления сообщения: {e}")
bot.answer_callback_query(call.id, f"{result_text} Но не удалось обновить сообщение.")
return
# Обработка кнопок управления подпиской на события
if call.data.startswith("subscribe_evt_") or call.data.startswith("unsubscribe_evt_"):
user = call.from_user
user_id = user.id
user_name = get_user_name(user)
parts = call.data.split('_', 2)
action = parts[0]
event_id = parts[2]
# Выполняем действие
if action == "subscribe":
save_mark_to_db_evt(event_id, user_id, user_name)
result_text = "✅ Вы успешно подписались!"
else:
remove_mark_from_db_evt(event_id, user_id, user_name)
result_text = "✅ Вы успешно отписались!"
# Обновляем сообщение с новым статусом
try:
# Получаем текущие данные о событии
event_data = format_event_message(event_id)
text = event_data['text']
# Создаем новую клавиатуру
is_subscribed = action == "subscribe"
keyboard = create_manage_keyboard_evt(event_id, is_subscribed)
# Для сообщений с изображением
if call.message.content_type == 'photo':
# Получаем file_id существующего изображения
file_id = call.message.photo[-1].file_id
# Редактируем подпись к изображению
bot.edit_message_caption(
chat_id=call.message.chat.id,
message_id=call.message.message_id,
caption=text,
reply_markup=keyboard,
parse_mode='HTML'
)
else:
# Редактируем текстовое сообщение
bot.edit_message_text(
chat_id=call.message.chat.id,
message_id=call.message.message_id,
text=text,
reply_markup=keyboard,
parse_mode='HTML'
)
# Отправляем отдельное сообщение о результате
bot.answer_callback_query(call.id, result_text)
except Exception as e:
logger.error(f"Ошибка обновления сообщения (events): {e}")
bot.answer_callback_query(call.id, f"{result_text} Но не удалось обновить сообщение.")
return
# Подтверждаем получение callback для любых других нажатий
bot.answer_callback_query(call.id)
except Exception as e:
error_msg = f"Ошибка обработки callback: {e}"
logger.error(error_msg)
user_info = format_user_info(call.from_user.id, get_user_name(call.from_user)) if call.from_user else "unknown"
log_event("error", user_info, message=error_msg)
bot.answer_callback_query(call.id, "Произошла ошибка. Пожалуйста, попробуйте позже.")
def setup_webhook():
"""Настройка вебхука"""
logger.info(f"starting setup_webhook")
try:
# Удаляем предыдущий вебхук
bot.remove_webhook()
# Устанавливаем новый вебхук
bot.set_webhook(
url=WEBHOOK_URL,
secret_token=WEBHOOK_SECRET,
max_connections=40
)
logger.info(f"Вебхук установлен: {WEBHOOK_URL}")
logger.info(f"Секретный токен: {'установлен' if WEBHOOK_SECRET else 'не установлен'}")
logger.info(f"Прослушивание порта: {WEBHOOK_PORT}")
logger.info(f"Путь вебхука: {WEBHOOK_PATH}")
# Логируем успешную настройку вебхука
log_event("system", "system", message=f"Вебхук установлен на {WEBHOOK_URL}")
except Exception as e:
error_msg = f"Ошибка настройка вебхука: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
exit(1)
# Используем путь из WEBHOOK_URL
@app.route(WEBHOOK_PATH, methods=['POST'])
def webhook():
logger.info(f"starting webhook")
if request.headers.get('X-Telegram-Bot-Api-Secret-Token') != WEBHOOK_SECRET:
logger.warning("Неверный секретный токен!")
log_event("security", "system", message="Попытка доступа с неверным секретным токеном")
return "Unauthorized", 401
json_data = request.get_json()
try:
update = telebot.types.Update.de_json(json_data)
bot.process_new_updates([update])
return "OK", 200
except Exception as e:
error_msg = f"Ошибка обработки вебхука: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return "Internal Server Error", 500
@app.route('/send_message', methods=['POST'])
@app.route(f'{WEBHOOK_PATH}/send_message', methods=['POST'])
def send_message_api():
"""API endpoint для отправки сообщений через бота"""
logger.info(f"starting send_message_api")
# Проверяем секретный токен для защиты API
api_token = request.headers.get('X-API-Secret-Token')
if api_token != WEBHOOK_SECRET:
logger.warning("Неверный API токен!")
log_event("security", "system", message="Попытка доступа к API с неверным токеном")
return {"success": False, "error": "Unauthorized", "error_code": "INVALID_TOKEN"}, 401
try:
data = request.get_json()
if not data:
return {"success": False, "error": "Отсутствуют данные", "error_code": "NO_DATA"}, 400
user_id = data.get('user_id')
message_text = data.get('message')
if not user_id:
return {"success": False, "error": "Не указан user_id", "error_code": "NO_USER_ID"}, 400
if not message_text:
return {"success": False, "error": "Не указан текст сообщения", "error_code": "NO_MESSAGE"}, 400
# Проверяем, что user_id является числом
try:
user_id = int(user_id)
except (ValueError, TypeError):
return {"success": False, "error": "user_id должен быть числом", "error_code": "INVALID_USER_ID"}, 400
# Отправляем сообщение
success, result_message = send_direct_message(user_id, message_text)
if success:
return {"success": True, "message": result_message, "user_id": user_id}, 200
else:
return {"success": False, "error": result_message, "user_id": user_id, "error_code": "TELEGRAM_ERROR"}, 400
except Exception as e:
error_msg = f"Ошибка в API отправки сообщений: {e}"
logger.error(error_msg)
log_event("error", "system", message=error_msg)
return {"success": False, "error": "Внутренняя ошибка сервера", "error_code": "INTERNAL_ERROR"}, 500
if __name__ == '__main__':
# Логируем запуск бота
logger.info(f"starting main")
log_event("system", "system", message="Бот запущен")
# Настраиваем вебхук
setup_webhook()
# Логируем запуск Flask
logger.info(f"starting Flask")
log_event("system", "system", message=f"Flask приложение запущено на порту {WEBHOOK_PORT}, путь: {WEBHOOK_PATH}")
# Запускаем Flask приложение
app.run(host='0.0.0.0', port=WEBHOOK_PORT)