#!/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'])