372 lines
17 KiB
Python
372 lines
17 KiB
Python
import os
|
|
import logging
|
|
import asyncio
|
|
import pymysql
|
|
import time
|
|
import html
|
|
from datetime import datetime, timezone, timedelta
|
|
from telegram import Bot, InlineKeyboardMarkup, InlineKeyboardButton
|
|
from telegram.error import TelegramError, RetryAfter
|
|
from dotenv import load_dotenv
|
|
from formatter import get_event_text
|
|
|
|
# Загрузка переменных окружения
|
|
load_dotenv()
|
|
|
|
# Настройки из переменных окружения
|
|
BOT_TOKEN = os.getenv('POSTER_BOT_TOKEN')
|
|
RESPONDER_BOT_NAME = os.getenv('RESPONDER_BOT_NAME')
|
|
CHANNEL_ID = os.getenv('CHANNEL_ID')
|
|
MDB_HOST = os.getenv('MDB_HOST')
|
|
MDB_USER = os.getenv('MDB_USER')
|
|
MDB_PW = os.getenv('MDB_PW')
|
|
MDBASE = os.getenv('MDBASE')
|
|
DESC_PREFIX = os.getenv('DESC_PREFIX')
|
|
PZK_PREFIX = os.getenv('PZK_PREFIX')
|
|
LOG_FILE = os.getenv('LOG_FILE', 'evtg_publisher.log')
|
|
EVENT_POST_DELAY = int(os.getenv('EVENT_POST_DELAY', 0)) # Задержка в минутах
|
|
WORKMODE = os.getenv('WORKMODE', 'ZILANT') # Режим работы: ZILANT или VOLK
|
|
|
|
# Параметры длины сообщений
|
|
MAX_CAPTION_LENGTH = int(os.getenv('MAX_CAPTION_LENGTH', 1000))
|
|
MAX_TEXT_LENGTH = int(os.getenv('MAX_TEXT_LENGTH', 4000))
|
|
|
|
# Режим публикации без звука
|
|
PUBLISH_SILENTLY = os.getenv('PUBLISH_SILENTLY', 'false').lower() in ('true', '1', 'yes', 'on')
|
|
|
|
# Использование бота подписки
|
|
USE_SUBSCRIPTION_BOT = os.getenv('USE_SUBSCRIPTION_BOT', 'true').lower() in ('true', '1', 'yes', 'on')
|
|
|
|
# Настройка логирования
|
|
logger = logging.getLogger('TG_EV_post')
|
|
logger.setLevel(logging.INFO)
|
|
|
|
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)
|
|
|
|
def format_tags(tags_str):
|
|
"""Форматирование тегов с добавлением #"""
|
|
if not tags_str:
|
|
return ""
|
|
tags = tags_str.split()
|
|
formatted_tags = []
|
|
for tag in tags:
|
|
if len(tag) > 1 and not tag.startswith('#'):
|
|
formatted_tags.append(f"#{tag}")
|
|
else:
|
|
formatted_tags.append(tag)
|
|
return " ".join(formatted_tags)
|
|
|
|
def prepare_text(text):
|
|
"""Подготовка текстовых полей с экранированием HTML-сущностей"""
|
|
if not text:
|
|
return ""
|
|
|
|
# Экранируем специальные символы HTML
|
|
text = html.escape(str(text))
|
|
|
|
return text
|
|
|
|
async def tg_post_event(bot, event_data):
|
|
"""Публикация одного события"""
|
|
try:
|
|
# Форматирование текста
|
|
tags = format_tags(event_data['tags'])
|
|
unit_name = prepare_text(event_data['unit_name'] or "")
|
|
name = prepare_text(event_data['name'] or "")
|
|
|
|
# Получаем и подготавливаем описание события
|
|
about_raw = event_data['about']
|
|
|
|
# Формируем базовый текст (теги + название площадки + название события)
|
|
caption_parts = []
|
|
if tags:
|
|
caption_parts.append(tags)
|
|
if unit_name:
|
|
caption_parts.append(unit_name)
|
|
if name:
|
|
caption_parts.append(f"<b>{name}</b>")
|
|
base_text = "\n".join(caption_parts)
|
|
base_text_length = len(base_text)
|
|
|
|
# Предварительный расчет строки ссылок (с запасом 20 символов для message_id)
|
|
number = event_data['number']
|
|
placeholder_message_id = '0' * 20 # Заполнитель для message_id
|
|
|
|
# Формируем ссылки в зависимости от USE_SUBSCRIPTION_BOT
|
|
if USE_SUBSCRIPTION_BOT:
|
|
links_line_placeholder = f'🔎 <a href="{DESC_PREFIX}{number}/">Инфо</a> | 📌 <a href="{PZK_PREFIX}{number}/">Иду</a> | 🔜 <a href="https://t.me/{RESPONDER_BOT_NAME}?start=event_{placeholder_message_id}">Подписка</a>'
|
|
else:
|
|
links_line_placeholder = f'🔎 <a href="{DESC_PREFIX}{number}/">Инфо</a>'
|
|
links_line_length = len(links_line_placeholder)
|
|
|
|
# Определяем максимальную длину в зависимости от типа сообщения
|
|
image_url = event_data['about_social_picture']
|
|
has_image = image_url and image_url.strip()
|
|
max_length = MAX_CAPTION_LENGTH if has_image else MAX_TEXT_LENGTH
|
|
|
|
# Вычисляем доступную длину для текста события
|
|
available_length = max_length - base_text_length - links_line_length - 3 # -3 для символов переноса строки
|
|
if available_length < 0:
|
|
available_length = 0
|
|
|
|
# Получаем обработанный текст с учетом доступной длины
|
|
about = get_event_text(about_raw, available_length) if about_raw else ""
|
|
|
|
# Формируем первоначальный текст
|
|
initial_text = f"{base_text}\n\n{about}" if about else base_text
|
|
|
|
# Логируем текст для отладки
|
|
logger.info(f"Текст для публикации события номер {event_data['number']}: {initial_text}")
|
|
|
|
# Публикация первоначального сообщения без ссылок
|
|
if has_image:
|
|
try:
|
|
message = await bot.send_photo(
|
|
chat_id=CHANNEL_ID,
|
|
photo=image_url,
|
|
caption=initial_text,
|
|
parse_mode="HTML",
|
|
disable_notification=PUBLISH_SILENTLY
|
|
)
|
|
except TelegramError as e:
|
|
logger.warning(f"Не удалось отправить изображение для события номер {event_data['number']}: {e}. Отправляем текстовое сообщение.")
|
|
message = await bot.send_message(
|
|
chat_id=CHANNEL_ID,
|
|
text=initial_text,
|
|
parse_mode="HTML",
|
|
disable_notification=PUBLISH_SILENTLY,
|
|
disable_web_page_preview=True # Отключаем предпросмотр ссылок
|
|
)
|
|
else:
|
|
message = await bot.send_message(
|
|
chat_id=CHANNEL_ID,
|
|
text=initial_text,
|
|
parse_mode="HTML",
|
|
disable_notification=PUBLISH_SILENTLY,
|
|
disable_web_page_preview=True # Отключаем предпросмотр ссылок
|
|
)
|
|
|
|
# Получаем ID сообщения для использования в ссылке подписки
|
|
message_id = message.message_id
|
|
|
|
# Формируем финальный текст с ссылками
|
|
if USE_SUBSCRIPTION_BOT:
|
|
links_line = f'🔎 <a href="{DESC_PREFIX}{number}/">Инфо</a> | 📌 <a href="{PZK_PREFIX}{number}/">Иду</a> | 🔜 <a href="https://t.me/{RESPONDER_BOT_NAME}?start=event_{message_id}">Подписка</a>'
|
|
else:
|
|
links_line = f'🔎 <a href="{DESC_PREFIX}{number}/">Инфо</a>'
|
|
final_text = f"{initial_text}\n\n{links_line}"
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
# Редактируем сообщение, добавляя ссылки
|
|
if has_image:
|
|
try:
|
|
await bot.edit_message_caption(
|
|
chat_id=CHANNEL_ID,
|
|
message_id=message_id,
|
|
caption=final_text,
|
|
parse_mode="HTML"
|
|
)
|
|
except TelegramError as e:
|
|
logger.warning(f"Не удалось отредактировать подпись для события номер {event_data['number']}: {e}. Пробуем отредактировать текстовое сообщение.")
|
|
await bot.edit_message_text(
|
|
chat_id=CHANNEL_ID,
|
|
message_id=message_id,
|
|
text=final_text,
|
|
parse_mode="HTML",
|
|
disable_web_page_preview=True # Отключаем предпросмотр ссылок
|
|
)
|
|
else:
|
|
await bot.edit_message_text(
|
|
chat_id=CHANNEL_ID,
|
|
message_id=message_id,
|
|
text=final_text,
|
|
parse_mode="HTML",
|
|
disable_web_page_preview=True # Отключаем предпросмотр ссылок
|
|
)
|
|
|
|
return message_id # Возвращаем только message_id
|
|
|
|
except RetryAfter as e:
|
|
# Обработка ошибки FloodWait (429)
|
|
logger.error(f"Получена ошибка FloodWait (429) при публикации события номер {event_data['number']}: {e}")
|
|
logger.error(f"Необходимо подождать {e.retry_after} секунд перед следующей попыткой")
|
|
raise # Пробрасываем исключение выше для обработки на уровне цикла
|
|
except TelegramError as e:
|
|
logger.error(f"Ошибка Telegram при публикации события номер {event_data['number']}: {e}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Неожиданная ошибка при публикации события номер {event_data['number']}: {e}")
|
|
return None
|
|
|
|
async def tg_post_event_by_id(id_event):
|
|
"""Публикация события по ID"""
|
|
logger.info(f"Запуск публикации для события ID {id_event}")
|
|
conn = None
|
|
try:
|
|
conn = pymysql.connect(
|
|
host=MDB_HOST,
|
|
user=MDB_USER,
|
|
password=MDB_PW,
|
|
database=MDBASE,
|
|
charset='utf8mb4',
|
|
cursorclass=pymysql.cursors.DictCursor
|
|
)
|
|
|
|
# Проверка времени изменения только в режиме ZILANT
|
|
with conn.cursor() as cursor:
|
|
if WORKMODE == "ZILANT":
|
|
# Вычисляем время, до которого должно быть изменено событие для публикации
|
|
modified_before = datetime.now(timezone.utc) - timedelta(minutes=EVENT_POST_DELAY)
|
|
|
|
cursor.execute("""
|
|
SELECT id_event, number, tags, unit_name, name, about, about_social_picture, tg_message_id
|
|
FROM events
|
|
WHERE id_event = %s
|
|
AND marked_to_publication = True
|
|
AND is_posted_tg = False
|
|
AND modified <= %s
|
|
""", (id_event, modified_before))
|
|
event = cursor.fetchone()
|
|
|
|
if not event:
|
|
logger.info(f"Событие ID {id_event} не найдено, уже опубликовано или изменено менее чем {EVENT_POST_DELAY} минут назад")
|
|
return
|
|
else:
|
|
# В режиме VOLK проверка времени не выполняется
|
|
cursor.execute("""
|
|
SELECT id_event, number, tags, unit_name, name, about, about_social_picture, tg_message_id
|
|
FROM events
|
|
WHERE id_event = %s
|
|
AND marked_to_publication = True
|
|
AND is_posted_tg = False
|
|
""", (id_event,))
|
|
event = cursor.fetchone()
|
|
|
|
if not event:
|
|
logger.info(f"Событие ID {id_event} не найдено или уже опубликовано")
|
|
return
|
|
|
|
old_message_id = event.get('tg_message_id') # Сохраняем старый ID сообщения
|
|
bot = Bot(token=BOT_TOKEN)
|
|
tg_message_id = await tg_post_event(bot, event) # Получаем новый message_id
|
|
|
|
if tg_message_id:
|
|
# Используем UTC время для записи в базу данных
|
|
current_time_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
# Обновляем подписки если был предыдущий message_id
|
|
if old_message_id:
|
|
cursor.execute("""
|
|
UPDATE marks_evt
|
|
SET tg_event_id = %s
|
|
WHERE tg_event_id = %s
|
|
""", (tg_message_id, old_message_id))
|
|
logger.info(f"Обновлены подписки для события {id_event}: {cursor.rowcount} записей")
|
|
|
|
cursor.execute("""
|
|
UPDATE events
|
|
SET is_posted_tg = True,
|
|
tg_message_id = %s,
|
|
tg_posted_date = %s
|
|
WHERE id_event = %s
|
|
""", (tg_message_id, current_time_utc, id_event))
|
|
conn.commit()
|
|
logger.info(f"Событие номер {event['number']} успешно опубликовано")
|
|
else:
|
|
logger.error(f"Не удалось опубликовать событие ID {id_event}")
|
|
|
|
except RetryAfter as e:
|
|
raise e
|
|
except Exception as e:
|
|
logger.error(f"Ошибка при публикации: {e}")
|
|
finally:
|
|
if conn:
|
|
conn.close()
|
|
|
|
async def tg_post_all_events():
|
|
"""Публикация всех неопубликованных событий (максимум 10 за один вызов)"""
|
|
logger.info("Запуск скрипта публикации всех событий")
|
|
conn = None
|
|
try:
|
|
conn = pymysql.connect(
|
|
host=MDB_HOST,
|
|
user=MDB_USER,
|
|
password=MDB_PW,
|
|
database=MDBASE,
|
|
charset='utf8mb4',
|
|
cursorclass=pymysql.cursors.DictCursor
|
|
)
|
|
|
|
with conn.cursor() as cursor:
|
|
# Проверка времени изменения только в режиме ZILANT
|
|
if WORKMODE == "ZILANT":
|
|
# Вычисляем время, до которого должно быть изменено событие для публикации
|
|
modified_before = datetime.now(timezone.utc) - timedelta(minutes=EVENT_POST_DELAY)
|
|
|
|
cursor.execute("""
|
|
SELECT id_event
|
|
FROM events
|
|
WHERE marked_to_publication = True
|
|
AND is_posted_tg = False
|
|
AND modified <= %s
|
|
LIMIT 5
|
|
""", (modified_before,))
|
|
else:
|
|
# В режиме VOLK проверка времени не выполняется
|
|
cursor.execute("""
|
|
SELECT id_event
|
|
FROM events
|
|
WHERE marked_to_publication = True
|
|
AND is_posted_tg = False
|
|
LIMIT 5
|
|
""")
|
|
events = cursor.fetchall()
|
|
|
|
event_count = 0
|
|
for event in events:
|
|
try:
|
|
await tg_post_event_by_id(event['id_event'])
|
|
event_count += 1
|
|
|
|
# Задержка между публикациями разных событий
|
|
if event_count < len(events):
|
|
await asyncio.sleep(2)
|
|
|
|
except RetryAfter as e:
|
|
logger.error(f"Прерываем публикацию из-за ошибки FloodWait. Ожидание: {e.retry_after} секунд")
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"Ошибка при публикации события ID {event['id_event']}: {e}")
|
|
# Продолжаем публикацию следующих событий, несмотря на ошибку
|
|
|
|
logger.info(f"Опубликовано событий в этом запуске: {event_count}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Ошибка при публикации: {e}")
|
|
finally:
|
|
if conn:
|
|
conn.close()
|
|
logger.info("Завершение работы скрипта")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(tg_post_all_events())
|