205 lines
9.2 KiB
Python
205 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
||
|
||
import re
|
||
import time
|
||
import logging
|
||
import asyncio
|
||
|
||
from config import DISCORD_CONFIG
|
||
|
||
# Discord импорты - только если Discord включен
|
||
if DISCORD_CONFIG['enabled']:
|
||
try:
|
||
import discord
|
||
except ImportError:
|
||
logging.error("Discord.py не установлен, но Discord включен в конфигурации")
|
||
DISCORD_CONFIG['enabled'] = False
|
||
|
||
|
||
class DiscordClient:
|
||
def __init__(self, content_processor):
|
||
self.logger = logging.getLogger(__name__)
|
||
self.content_processor = content_processor
|
||
self.config = DISCORD_CONFIG
|
||
|
||
if not self.config['enabled']:
|
||
self.logger.info("Discord клиент отключен в конфигурации")
|
||
|
||
async def get_messages(self, client, channel_id):
|
||
"""Получение сообщений из Discord канала"""
|
||
if not self.is_enabled():
|
||
return [], []
|
||
|
||
self.logger.debug("Получаем сообщения из Discord канала")
|
||
channel = client.get_channel(channel_id)
|
||
if not channel:
|
||
self.logger.error(f"ID канала Discord {channel_id} не существует")
|
||
return [], []
|
||
|
||
messages = []
|
||
titles = []
|
||
|
||
async for message in channel.history(limit=100):
|
||
self.logger.debug(f"Сообщение Discord: {message.content}")
|
||
messages.append(message.content)
|
||
|
||
# Извлекаем заголовок из сообщения
|
||
title = self._extract_title_from_message(message.content)
|
||
if title:
|
||
titles.append(title)
|
||
|
||
self.logger.debug(f"Найдено {len(messages)} сообщений и {len(titles)} заголовков в Discord")
|
||
return messages, titles
|
||
|
||
def _extract_title_from_message(self, message_text):
|
||
"""Извлечение заголовка из текста сообщения"""
|
||
if not message_text:
|
||
return None
|
||
|
||
# Ищем заголовки в формате "### Заголовок\t"
|
||
pattern = re.compile(r'^### (.*?)\t', re.MULTILINE)
|
||
match = pattern.search(message_text)
|
||
if match:
|
||
return match.group(1).strip()
|
||
|
||
# Ищем заголовки в формате "------ ### Заголовок\t"
|
||
pattern2 = re.compile(r'--+\n### (.*?)\t', re.MULTILINE)
|
||
match2 = pattern2.search(message_text)
|
||
if match2:
|
||
return match2.group(1).strip()
|
||
|
||
# Если не найдено, ищем в первой строке
|
||
lines = message_text.strip().split('\n')
|
||
if lines:
|
||
first_line = lines[0].strip()
|
||
# Убираем префиксы типа "###", "**", и табы
|
||
first_line = re.sub(r'^[#*\s\t-]+', '', first_line)
|
||
first_line = re.sub(r'\t.*$', '', first_line)
|
||
return first_line.strip() if first_line else None
|
||
|
||
return None
|
||
|
||
async def send_message(self, channel, content):
|
||
"""Отправка сообщения в Discord канал с разбивкой длинных сообщений"""
|
||
if not self.is_enabled():
|
||
return
|
||
|
||
try:
|
||
# Discord лимит: 2000 символов
|
||
max_length = 2000
|
||
|
||
if len(content) <= max_length:
|
||
# Короткое сообщение - отправляем как есть
|
||
await channel.send(content)
|
||
else:
|
||
# Длинное сообщение - разбиваем умно по строкам
|
||
self.logger.warning(f"Сообщение слишком длинное ({len(content)} символов), разбиваем на части")
|
||
|
||
parts = []
|
||
current_part = ""
|
||
|
||
for line in content.split('\n'):
|
||
# Если добавление этой строки превысит лимит
|
||
if len(current_part + line + '\n') > max_length:
|
||
if current_part:
|
||
parts.append(current_part.rstrip())
|
||
current_part = ""
|
||
current_part += line + '\n'
|
||
|
||
# Добавляем последнюю часть
|
||
if current_part:
|
||
parts.append(current_part.rstrip())
|
||
|
||
self.logger.info(f"Сообщение разбито на {len(parts)} частей для Discord")
|
||
|
||
# Отправляем каждую часть
|
||
for i, part in enumerate(parts, 1):
|
||
if len(parts) > 1:
|
||
part_content = f"[Часть {i}/{len(parts)}]\n\n{part}"
|
||
else:
|
||
part_content = part
|
||
await channel.send(part_content)
|
||
|
||
# Небольшая задержка между частями
|
||
if i < len(parts):
|
||
await asyncio.sleep(1)
|
||
|
||
self.logger.info("Сообщение успешно отправлено в Discord")
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка отправки сообщения в Discord: {e}")
|
||
|
||
async def check_and_publish_news(self, news_list):
|
||
"""Проверка и публикация новостей в Discord"""
|
||
if not self.is_enabled():
|
||
self.logger.info("Discord отключен, пропускаем публикацию")
|
||
return
|
||
|
||
self.logger.info("Начинаем проверку новостей для Discord")
|
||
|
||
intents = discord.Intents.default()
|
||
intents.messages = True
|
||
client = discord.Client(intents=intents)
|
||
|
||
@client.event
|
||
async def on_ready():
|
||
self.logger.debug(f"Успешный логин в Discord: {client.user}")
|
||
channel_id = self.config['channel_id']
|
||
|
||
# Получаем существующие сообщения и заголовки
|
||
discord_messages, discord_titles = await self.get_messages(client, channel_id)
|
||
|
||
if not news_list:
|
||
self.logger.warning("Список новостей пуст")
|
||
await client.close()
|
||
return
|
||
|
||
# Фильтруем новости для публикации
|
||
list_for_public = []
|
||
for topic_id, topic_title in news_list:
|
||
# Проверяем по заголовкам и по полному тексту сообщений
|
||
title_exists = any(topic_title == title for title in discord_titles)
|
||
text_contains = any(topic_title in (msg or '') for msg in discord_messages)
|
||
|
||
if not title_exists and not text_contains:
|
||
list_for_public.append((topic_id, topic_title))
|
||
else:
|
||
self.logger.debug(f"Новость '{topic_title}' уже есть в Discord (title_exists={title_exists}, text_contains={text_contains})")
|
||
|
||
if not list_for_public:
|
||
self.logger.warning("Новостей для публикации в Discord нет")
|
||
await client.close()
|
||
return
|
||
|
||
self.logger.info(f"Новости для публикации в Discord: {list_for_public}")
|
||
|
||
channel = client.get_channel(channel_id)
|
||
if not channel:
|
||
self.logger.error(f"ID канала Discord {channel_id} не существует")
|
||
await client.close()
|
||
return
|
||
|
||
# Публикуем новости в обратном порядке, чтобы новые оказались сверху в ленте
|
||
for topic_id, topic_title in reversed(list_for_public):
|
||
from site_api import SiteAPI
|
||
site_api = SiteAPI()
|
||
text_data = site_api.get_news_content(topic_id, self.content_processor)
|
||
|
||
if text_data:
|
||
content = f"----------------------------------------------------------\n### {topic_title}\t\n" + text_data + "\n@here"
|
||
await self.send_message(channel, content)
|
||
time.sleep(1.0)
|
||
else:
|
||
self.logger.warning(f"Не удалось получить содержимое новости {topic_id}")
|
||
|
||
await client.close()
|
||
|
||
try:
|
||
await client.start(self.config['token'])
|
||
except Exception as e:
|
||
self.logger.error(f"Ошибка запуска Discord клиента: {e}")
|
||
|
||
def is_enabled(self):
|
||
"""Проверка, включен ли Discord клиент"""
|
||
return (self.config['enabled'] and
|
||
self.config['token'] and
|
||
self.config['channel_id']) |