1358 lines
60 KiB
Python
1358 lines
60 KiB
Python
import os
|
||
import logging
|
||
import time
|
||
import errno
|
||
from dotenv import load_dotenv
|
||
import telebot
|
||
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()
|
||
|
||
# Настройка логирования
|
||
logging.basicConfig(
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||
level=logging.INFO
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Получаем данные из переменных окружения
|
||
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')
|
||
MAX_CAPTION_LENGTH = int(os.getenv('MAX_CAPTION_LENGTH', 1000))
|
||
MAX_TEXT_LENGTH = int(os.getenv('MAX_TEXT_LENGTH', 4000))
|
||
|
||
# Устанавливаем единое ограничение длины текста
|
||
UNIFIED_MAX_LENGTH = 800
|
||
|
||
# Извлекаем путь из 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'
|
||
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)
|
||
|
||
# Создаем экземпляр бота
|
||
bot = telebot.TeleBot(RESPONDER_BOT_TOKEN)
|
||
|
||
def get_user_name(user):
|
||
"""Форматирует имя пользователя для записи в БД"""
|
||
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"""
|
||
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):
|
||
"""Проверяет, подписан ли пользователь уже на это событие"""
|
||
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)"""
|
||
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"""
|
||
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):
|
||
"""Возвращает список событий, на которые подписан пользователь, с информацией о статусе"""
|
||
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"""
|
||
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):
|
||
"""Сохраняет отметку пользователя в базе данных"""
|
||
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)"""
|
||
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):
|
||
"""Удаляет отметку пользователя из базы данных"""
|
||
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)"""
|
||
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):
|
||
"""Форматирует ссылку на событие в канале или на сам канал"""
|
||
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():
|
||
"""Создает клавиатуру для справки"""
|
||
keyboard = InlineKeyboardMarkup()
|
||
keyboard.row(
|
||
InlineKeyboardButton("📜 Подписки", callback_data="cmd_list"),
|
||
InlineKeyboardButton("📢 Канал", url=format_channel_link())
|
||
)
|
||
return keyboard
|
||
|
||
def create_main_keyboard():
|
||
"""Создает основную клавиатуру для /start без параметров"""
|
||
keyboard = InlineKeyboardMarkup()
|
||
keyboard.row(
|
||
InlineKeyboardButton("❓ Справка", callback_data="cmd_help"),
|
||
InlineKeyboardButton("📢 Канал", url=format_channel_link())
|
||
)
|
||
return keyboard
|
||
|
||
def create_manage_keyboard(post_id, is_subscribed):
|
||
"""Создает клавиатуру для управления подпиской на посты"""
|
||
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):
|
||
"""Создает клавиатуру для управления подпиской на события"""
|
||
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"""
|
||
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"""
|
||
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"""
|
||
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"""
|
||
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"""
|
||
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):
|
||
"""Отправляет сообщение с изображением или текстом"""
|
||
try:
|
||
# Дополнительная обрезка текста
|
||
if len(text) > UNIFIED_MAX_LENGTH:
|
||
text = text[:UNIFIED_MAX_LENGTH - 3] + "..."
|
||
|
||
if image_url:
|
||
# Загружаем изображение
|
||
image = download_image(image_url)
|
||
if image:
|
||
# Отправляем фото с подписью
|
||
bot.send_photo(
|
||
chat_id,
|
||
photo=image,
|
||
caption=text,
|
||
reply_markup=reply_markup,
|
||
parse_mode='HTML'
|
||
)
|
||
return True
|
||
else:
|
||
logger.warning(f"Не удалось загрузить изображение: {image_url}")
|
||
|
||
# Если изображение отсутствует или не загружено, отправляем текст
|
||
bot.send_message(
|
||
chat_id,
|
||
text,
|
||
reply_markup=reply_markup,
|
||
parse_mode='HTML'
|
||
)
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка отправки сообщения: {e}")
|
||
return False
|
||
|
||
def send_direct_message(user_id, message_text):
|
||
"""Отправляет сообщение пользователю по ID с обработкой ошибок"""
|
||
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
|
||
|
||
# Отправляем сообщение
|
||
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}")
|
||
return True, "Сообщение отправлено успешно"
|
||
|
||
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):
|
||
"""Отправляет сообщение для управления подпиской на посты"""
|
||
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):
|
||
"""Отправляет сообщение для управления подпиской на события"""
|
||
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 с параметром"""
|
||
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 = (
|
||
"🌟 <b>Привет!</b> 🌟\n\n"
|
||
"Я - информационный бот Зиланткона. "
|
||
"Здесь можно подписаться на интересные события будущего Зиланта - и я напомню,"
|
||
"когда их можно будет включить в свой План Захвата Конвента.\n\n"
|
||
)
|
||
|
||
bot.send_message(
|
||
message.chat.id,
|
||
welcome_text,
|
||
reply_markup=create_main_keyboard(),
|
||
parse_mode='HTML'
|
||
)
|
||
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 - переход в основной канал"""
|
||
try:
|
||
channel_link = format_channel_link()
|
||
bot.send_message(
|
||
message.chat.id,
|
||
"📢 <b>Основной канал Зиланткона</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"""
|
||
try:
|
||
help_text = (
|
||
"ℹ️ <b>Подсказка информ-бота Зиланткона</b>\n\n"
|
||
"Я запоминаю, кто чем интересовался, и напоминаю, "
|
||
"когда это становится можно включить в План Захвата.\n\n"
|
||
"<b>Доступные команды:</b>\n"
|
||
"• /start - начать работу с ботом (вы уже здесь!)\n"
|
||
"• /help - показать эту справку\n"
|
||
"• /home - перейти в основной канал Зиланткона\n"
|
||
"• /list - показать текущие подписки\n\n"
|
||
"Используйте кнопки ниже для быстрого доступа:"
|
||
)
|
||
|
||
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"""
|
||
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)
|
||
|
||
# Формируем ответ
|
||
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"
|
||
|
||
# Получаем связанные мероприятия (только опубликованные)
|
||
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"
|
||
|
||
# Добавляем подписки на события
|
||
if 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):
|
||
"""Обработчик нажатий на кнопки"""
|
||
try:
|
||
# Обработка командных кнопок
|
||
if call.data.startswith("cmd_"):
|
||
command = call.data[4:]
|
||
if command == "help":
|
||
help_text = (
|
||
"ℹ️ <b>Подсказка информ-бота Зиланткона</b>\n\n"
|
||
"Я запоминаю, кто чем интересовался, и напоминаю, "
|
||
"когда это становится можно включить в План Захвата.\n\n"
|
||
"<b>Доступные команды:</b>\n"
|
||
"• /start - начать работу с ботом (вы уже здесь!)\n"
|
||
"• /help - показать эту справку\n"
|
||
"• /home - перейти в основной канал Зиланткона\n"
|
||
"• /list - показать текущие подписки\n\n"
|
||
"Используйте кнопки ниже для быстрого доступа:"
|
||
)
|
||
|
||
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)
|
||
|
||
# Формируем ответ
|
||
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"
|
||
|
||
# Получаем связанные мероприятия (только опубликованные)
|
||
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"
|
||
|
||
# Добавляем подписки на события
|
||
if 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 = (
|
||
"🌟 <b>Привет!</b> 🌟\n\n"
|
||
"Я - информационный бот Зиланткона. "
|
||
"Здесь можно подписаться на интересные события будущего Зиланта - и я напомню,"
|
||
"когда их можно будет включить в свой План Захвата Конвента.\n\n"
|
||
)
|
||
|
||
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():
|
||
"""Настройка вебхука"""
|
||
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():
|
||
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 для отправки сообщений через бота"""
|
||
# Проверяем секретный токен для защиты 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__':
|
||
# Логируем запуск бота
|
||
log_event("system", "system", message="Бот запущен")
|
||
|
||
# Настраиваем вебхук
|
||
setup_webhook()
|
||
|
||
# Логируем запуск Flask
|
||
log_event("system", "system", message=f"Flask приложение запущено на порту {WEBHOOK_PORT}, путь: {WEBHOOK_PATH}")
|
||
|
||
# Запускаем Flask приложение
|
||
app.run(host='0.0.0.0', port=WEBHOOK_PORT)
|