diff --git a/news-bot.py b/news-bot.py
deleted file mode 100755
index 5e89a89..0000000
--- a/news-bot.py
+++ /dev/null
@@ -1,597 +0,0 @@
-#!/usr/bin/env python3
-
-import re
-import sys
-import time
-import schedule
-import asyncio
-import discord
-import logging
-import colorlog
-import requests
-import html2text
-import urllib.parse
-
-from telethon import events
-from bs4 import BeautifulSoup
-from telethon.sync import TelegramClient
-from telethon.errors import FloodWaitError
-
-import keys
-
-url_post = "https://linux-gaming.ru/posts.json"
-url_news = f"https://linux-gaming.ru/c/news/{keys.cat_num}.json"
-url_vk_post = "https://api.vk.com/method/wall.post"
-url_vk_get = "https://api.vk.com/method/wall.get"
-url_changelog = "https://gitlab.eterfund.ru/Castro-Fidel/PortWINE/raw/master/data_from_portwine/changelog_ru"
-
-heads_site = {
- "Content-Type": "application/json",
- "Api-Key": keys.api_key_site,
- "Api-Username": "linux-gaming"
-}
-
-params_get = {
- 'access_token': keys.user_token_vk,
- 'v': '5.199', # Версия API
- 'owner_id': str(keys.own_id),
- 'count': str(100),
- 'offset': str(0),
- 'filter': 'all'
-}
-
-logger = logging.getLogger()
-logger.setLevel(logging.DEBUG)
-
-handler = colorlog.StreamHandler()
-handler.setFormatter(colorlog.ColoredFormatter(
- '%(log_color)s%(asctime)s - %(levelname)s: %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S',
- log_colors={
- 'DEBUG': 'cyan',
- 'INFO': 'green',
- 'WARNING': 'yellow',
- 'ERROR': 'red',
- 'CRITICAL': 'red,bg_white',
- }
-))
-
-logger.addHandler(handler)
-
-
-async def job():
- matches_changelog, last_changelog, resp_changelog = resp_change()
- check_version(matches_changelog, last_changelog, resp_changelog)
- await check_discord_public()
- check_vk_posts()
- await check_tg_news()
-
-
-def make_soup(resp_changelog):
- logging.debug(f"Вызываем make_soup")
- return BeautifulSoup(resp_changelog.text, 'html.parser')
-
-
-def html_to_text(html_content):
- logging.debug(f"Вызываем html_to_text")
- logging.debug(f"HTML на входе {html_content}")
-
- h = html2text.HTML2Text()
- h.ignore_links = False # Сохранение ссылок
- h.ignore_images = True # Игнорирование изображений
- h.bypass_tables = True # Сохранение таблиц
- h.reference_links = True # Сохранение оригинальных ссылок
- markdown_text = h.handle(html_content)
-
- logging.debug(f"Текст до обработки регулярками {markdown_text}")
-
- # Удаление переносов строк из-за -
- markdown_text = re.sub(r'-\s*\n\s*', '-', markdown_text, flags=re.DOTALL)
- markdown_text = re.sub(r'-\s*\n*', '-', markdown_text, flags=re.DOTALL)
-
- # Убираем переносы строк внутри круглых скобок ()
- markdown_text = re.sub(r'\((.*?)\)', lambda x: '(' + x.group(1).replace('\n', ' ') + ')', markdown_text, flags=re.DOTALL)
-
- # Убираем переносы строк внутри квадратных скобок []
- markdown_text = re.sub(r'\[(.*?)\]', lambda x: '[' + x.group(1).replace('\n', ' ') + ']', markdown_text, flags=re.DOTALL)
-
- # Удаление строк, содержащих '* * *'
- markdown_text = re.sub(r'^.*\* \* \*.*$', '', markdown_text, flags=re.MULTILINE)
-
- # Преобразование всех ссылок с параметрами URL
- markdown_text = convert_links(markdown_text)
-
- # Работа с #
- patterns_to_remove = [
- r'###',
- r'##',
- r'#',
- r'\[scripts\]\(\/tag\/scripts\) version \d+ ',
- r'##\[scripts\]\(\) version \d+ ',
- r'\d{4}×\d{3} \d+ KB'
- ]
- for pattern in patterns_to_remove:
- markdown_text = re.sub(pattern, '', markdown_text)
-
- # Удаление избыточных пустых строк после удаления строк
- markdown_text = re.sub(r'\n\s*\n', '\n', markdown_text)
-
- # Замена текстов типа "image1280×474 99.7 KB", "807×454 64.1 KB" на "."
- markdown_text = re.sub(r'image\d+×\d+\s+\d+(\.\d+)?\s+KB', '.', markdown_text)
- markdown_text = re.sub(r'\d+×\d+\s+\d+(\.\d+)?\s+KB', '.', markdown_text)
-
- # Изменение ссылок без описания
- markdown_text = re.sub(r'\[\]\((https:\/\/[^\)]+)\)', r'[.](\1)', markdown_text)
- markdown_text = re.sub(r'\[\s]\((https:\/\/[^\)]+)\)', r'[.](\1)', markdown_text)
-
- # Удаление дублирующихся ссылок
- markdown_text = remove_duplicate_links(markdown_text)
-
- # # Добавление переноса после "История изменений:"
- # re.sub(r'^.*\* \* \*.*$', '', markdown_text)
- # markdown_text = re.sub(r'История изменений:', r'\n', markdown_text)
-
- # Удаление лишних отступов для строк, начинающихся с '*'
- markdown_text = re.sub(r' \*', r'*', markdown_text)
-
- # Перемещение ссылки на изображение в конец последней строки
- image_link = "[.](https://linux-gaming.ru/uploads/default/original/1X/5cfa59077a5275971401fab0114e56f3ffdd0ec4.png)"
- if image_link in markdown_text:
- # Удаляем строку с изображением из текста
- markdown_text = markdown_text.replace(image_link, '')
- markdown_text = markdown_text + image_link
-
- logging.debug(f"Текст после обработки {markdown_text}")
- return markdown_text
-
-
-def convert_links(text):
- logging.debug(f"Входим в convert_links")
- url_pattern = re.compile(r'https?://[^\s\)]+')
- url_pattern = url_pattern.sub(lambda match: decode_url_params(match.group(0)), text)
- logging.debug(f"Возврат url_pattern {url_pattern}")
- return url_pattern
-
-
-def decode_url_params(url):
- logging.debug(f"Входим в decode_url_params")
- parsed_url = urllib.parse.urlparse(url)
- query_params = urllib.parse.parse_qs(parsed_url.query)
- for key, values in query_params.items():
- if key.lower() == 'to' and values:
- return urllib.parse.unquote(values[0])
- logging.debug(f"Возврат url {url}")
- return url
-
-
-def remove_empty_lines(text_data):
- logging.debug(f"Входим в remove_empty_lines")
- lines = text_data.splitlines()
- non_empty_lines = [line for line in lines if line.strip()]
- non_empty_lines = '\n'.join(non_empty_lines)
- logging.debug(f"Возврат non_empty_lines {non_empty_lines}")
- return non_empty_lines
-
-
-def remove_markdown_links(markdown_text):
- logging.debug(f"Входим в remove_markdown_links")
- # Регулярное выражение для поиска Markdown-ссылок и замена их на только URL
- markdown_text = re.sub(r'\[.*?\]\((https?://.*?)\)', r'\1' or r'(https?://.*?)\)', markdown_text)
- logging.debug(f"Возврат markdown_text {markdown_text}")
- return markdown_text
-
-
-def remove_duplicate_links(text):
- logging.debug(f"Входим в remove_duplicate_links")
- seen_links = set()
-
- def replace_link(match):
- link = match.group(2)
- if link in seen_links:
- return ''
- seen_links.add(link)
- return match.group(0)
-
- # Регулярное выражение для поиска Markdown-ссылок
- link_pattern = re.compile(r'(\[.*?\]\((https:\/\/.*?)\))')
- text = re.sub(link_pattern, replace_link, text)
- logging.debug(f"Возвращаем text {text}")
- return text
-
-
-def extract_links(text):
- logging.debug(f"Входим в extract_links")
- # Регулярное выражение для поиска ссылок
- url_pattern = re.compile(r'https?://\S+')
- url_pattern = url_pattern.findall(text)
- logging.debug(f"Возвращаем url_pattern {url_pattern}")
- return url_pattern
-
-
-def script_content(script_ver, next_version, resp_changelog):
- logging.debug(f"Вход в script_content c версией стабильного скрипта {script_ver}")
- soup = make_soup(resp_changelog)
- page_text = str(soup)
- page_text = page_text.replace("Вы можете помочь развитию проекта: https://linux-gaming.ru/donate/", '')
-
- # Находим текст до определенного текста, тега или класса (например, до тега
)
- last_text = f"###Scripts version {next_version}### / stable"
- last_text = str(last_text)
- index_last_text = page_text.find(last_text)
-
- if index_last_text != -1:
- changelog_text_last = page_text[:index_last_text]
- prev_text = f"###Scripts version {script_ver}### / stable"
- index_script_ver = changelog_text_last.find(prev_text)
- changelog_text = changelog_text_last[index_script_ver:]
- changelog_text = re.sub(r'###Scripts version (\d+)### / Дата: (\d{2}\.\d{2}\.\d{4}) / Размер скачиваемого обновления: \d+ \S+', r'\1 - \2'":", changelog_text)
- changelog_text = re.sub(r'###Scripts version (\d+)### / stable / Дата: (\d{2}\.\d{2}\.\d{4}) / Размер скачиваемого обновления: \d+ \S+', r'\1 - \2'":", changelog_text)
- post_text = (f"-----------------------------\n") + changelog_text
- site_text = (f"[center][img]/uploads/default/original/1X/5cfa59077a5275971401fab0114e56f3ffdd0ec4.png[/img][/center]\n{post_text}")
-
- logging.debug(f"Сообщение на сайт {site_text}")
-
- post_data = {
- "title": f"Кумулятивное обновление скриптов {script_ver}",
- "raw": site_text,
- "category": keys.cat_num
- }
- logging.debug(f"Возвращаем post_text - {post_text}\n post_data - {post_data}")
- return post_text, post_data, post_text
-
-
-def news_content(post_id):
- logging.debug(f"Запрос содержимого поста новости с ID: {post_id}")
- response = response_get(f"https://linux-gaming.ru/t/{post_id}.json", heads_site)
- if response and response.status_code == 200:
- topic_data = response.json()
- posts = topic_data.get('post_stream', {}).get('posts', [])
- # Найти первый пост
- for post in posts:
- if post.get('post_number') == 1:
- html_content = post.get('cooked', 'Нет содержимого')
- text_data = html_to_text(html_content)
- return text_data
- logging.error(f"Первый пост не найден в теме с ID: {post_id}")
- return None
- else:
- logging.error(f"Не удалось получить содержимое поста с ID: {post_id}")
- return None
-
-
-def response_get(url, heads_site=None):
- try:
- if heads_site == params_get:
- return requests.get(url, params=params_get)
- elif heads_site == heads_site:
- return requests.get(url, headers=heads_site)
- except requests.RequestException as err:
- logging.error(f"Ошибка запроса {err}")
-
-
-def resp_change():
- resp_changelog = response_get(url_changelog, heads_site)
-
- if resp_changelog and resp_changelog.status_code == 200:
- matches_changelog = re.findall(r'###Scripts version (\d+)### / stable', resp_changelog.text)
- logging.debug(f"Найдены версии в истории изменений: {matches_changelog}")
- last_changelog = int(max(matches_changelog))
- logging.info(f"Последняя стабильная версия в истории изменений: {last_changelog}")
- return matches_changelog, last_changelog, resp_changelog
- else:
- logging.error(
- f'Ошибка при запросе changelog: {resp_changelog.status_code if resp_changelog else "No Response"}')
- return None, None
-
-
-def news():
- resp_topics = response_get(url_news, heads_site)
-
- if resp_topics.status_code == 200:
- data = resp_topics.json()
- topics = data['topic_list']['topics']
- list_titles_and_ids = [(topic['id'], str(topic['title'])) for topic in topics]
- filtered_list_titles_and_ids = [(id, title) for id, title in list_titles_and_ids if not title == ('Описание '
- 'категории '
- '«Новости»')]
- return filtered_list_titles_and_ids
- else:
- logging.error(f"Ошибка при запросе тем с сайта: {resp_topics.status_code if resp_topics else 'Нет доступа к сайту'}")
- return []
-
-
-def site_post(url, headers, json):
- while True:
- title = json.get('title')
- try:
- resp_post = requests.post(url=url, headers=headers, json=json)
- if resp_post.status_code == 200:
- logging.info("Новость опубликована на сайте!")
- return resp_post
- elif resp_post.status_code == 422:
- logging.warning(f'Новость "{title}" уже опубликована: {resp_post.status_code}')
- return resp_post
- else:
- logging.error(f'Ошибка при отправке новости "{title}" на сайт: {resp_post.status_code}')
- except requests.RequestException as error:
- logging.error(f'Ошибка при отправке новости "{title}" на сайт: {error}')
- time.sleep(900)
-
-
-def check_version(matches_changelog, last_changelog, resp_changelog):
- list_titles_and_ids = news()
- pattern = re.compile(r'Кумулятивное обновление скриптов (\d+)')
-
- def extract_number(title):
- match = pattern.search(title)
- if match:
- return int(match.group(1))
- return None
-
- def posting_news():
- list_new_ver = []
- for script_ver, next_version in zip(reversed(matches_changelog[:-1]), reversed(matches_changelog[1:])):
- logging.info(f"Найдена новая версия скрипта {script_ver}")
- changelog_text, post_data, params = script_content(script_ver, next_version, resp_changelog)
- if post_data:
- logging.debug(f"Публикуем {post_data}")
- site_post(url_post, heads_site, post_data)
-
- if not list_new_ver:
- logging.warning(f"Не найдена новая стабильная версия скрипта")
-
- numbers = [extract_number(title) for _, title in list_titles_and_ids if extract_number(title) is not None]
- if numbers:
- last_topics_script = max(numbers)
- logging.info(f"Последняя новость на сайте о версии: {last_topics_script}")
-
- if last_topics_script < last_changelog:
- posting_news()
-
- else:
- logging.warning("Нет новых версий скриптов PortProton")
- else:
- logging.warning("На сайте нет новостей о скриптах")
- posting_news()
-
-
-async def discord_post(post_text, client):
- channel = client.get_channel(keys.dicord_channel)
- await channel.send(f"{post_text}")
-
-
-async def get_discord_messages(client_discord, channel_id):
- channel = client_discord.get_channel(channel_id)
- if not channel:
- logging.error(f"ID канала Discord {channel_id} не существует")
- return []
-
- messages = []
- async for message in channel.history(limit=100):
- logging.debug(message)
- messages.append(message.content)
- pattern = re.compile(r'----------------------------------------------------------\n### (.*?)\t\n', re.DOTALL)
- for message in messages:
- matches = pattern.findall(message)
- if matches:
- messages.extend(matches)
- logging.debug(f"Найдены сообщения в дискорде: {messages}")
- return messages
-
-
-async def check_discord_public():
- intents = discord.Intents.default()
- intents.messages = True
- client_discord = discord.Client(intents=intents)
-
- @client_discord.event
- async def on_ready():
- logging.debug(f"Успешный логин в discord {client_discord.user}")
- channel_id = keys.dicord_channel
- discord_messages = await get_discord_messages(client_discord, channel_id)
-
- list_titles_and_ids = news()
- if list_titles_and_ids:
- list_for_public = []
-
- for topic_id, topic_title in list_titles_and_ids:
- if topic_title not in discord_messages and topic_id > keys.start_topic_id:
- list_for_public.append((topic_id, topic_title))
-
- if not list_for_public:
- logging.warning(f"Новостей для публикации в дискорд нет")
- await client_discord.close()
- else:
- logging.info(f"Новости для публикации в дискорд: {list_for_public}")
- channel = client_discord.get_channel(channel_id)
- if not channel:
- logging.error(f"ID канала Discord {channel_id} не существует")
- await client_discord.close()
- return
-
- for topic_id, topic_title in reversed(list_for_public):
- text_data = news_content(topic_id)
- if text_data:
- content = f"----------------------------------------------------------\n### {topic_title}\t\n" + text_data + "\n" + "@here"
- # Разбиваем содержимое на части по 4000 символов
- for i in range(0, len(content), 2000):
- await channel.send(content[i:i+2000])
- time.sleep(1.0)
- await client_discord.close()
-
- await client_discord.start(keys.discord_token)
-
-
-def vk_post(url, post_text, links=None):
- params_post = {
- 'access_token': keys.api_key_vk,
- 'v': '5.199', # Версия API VK
- 'owner_id': str(keys.own_id)
- # Дополнительные параметры можно добавить здесь
- }
- post_text = re.sub(r'\* ', '•', post_text)
- print(post_text)
- post_text = re.sub(r' •', '➜', post_text)
- post_text = re.sub(r' •', '➜', post_text)
- data = {
- 'message': f'{post_text}' # Перемещаем текст статьи в тело POST-запроса
- }
- if links:
- params_post['attachments'] = links
- try:
- # Отправляем POST-запрос к VK API
- resp_post = requests.post(url=url, params=params_post, data=data)
-
- if resp_post.status_code == 200:
- logging.info("Сообщение успешно опубликовано.")
- logging.info(resp_post.json()) # Выводим ответ сервера в формате JSON
- else:
- logging.error(f"Ошибка при публикации сообщения в ВК:, {resp_post.status_code} - {resp_post.reason}")
-
- return resp_post
- except requests.RequestException as err:
- logging.error(f"VK post failed: {err}")
- return None
-
-
-def get_vk_topics():
- wall_posts = []
- while True:
- wall_data = response_get(url_vk_get, params_get)
- wall_data_json = wall_data.json()
- if 'error' in wall_data_json:
- error_code = wall_data_json['error']['error_code']
- error_msg = wall_data_json['error']['error_msg']
- logging.error(f"Ошибка {error_code}: {error_msg}")
- sys.exit(f"Ошибка {error_code}: {error_msg}")
-
- items = wall_data_json.get('response', {}).get('items', [])
- if not items:
- logging.warning("Постов на стене нет")
- break
-
- wall_posts.extend((post['text'] for post in items if 'text' in post))
- if len(items) < 100:
- break
-
- params_get['offset'] = str(int(params_get['offset']) + 100)
-
- pattern = re.compile(r'----------------------------------------------------------\n### (.*?)\t\n', re.DOTALL)
- for message in wall_posts:
- matches = pattern.findall(message)
- if matches:
- wall_posts.extend(matches)
- logging.debug(f"Найдены посты в ВК: {wall_posts}")
-
- return wall_posts
-
-
-def check_vk_posts():
- vk_posts = get_vk_topics()
- if not vk_posts:
- logging.warning(f"Постов на стене нет {vk_posts}")
- else:
- list_titles_and_ids = news()
- list_titles_and_ids = [pair for pair in list_titles_and_ids if pair[0] >= keys.start_topic_id]
-
- if list_titles_and_ids:
- list_for_public = []
- # Отфильтровываем посты ниже определенного ID статьи
- for topic_id, topic_title in list_titles_and_ids:
- # Сравнение заголовков с текстами постов
- if not any(topic_title in vk_posts for vk_posts in vk_posts):
- list_for_public.append((topic_id, topic_title))
-
- if not list_for_public:
- logging.warning(f"Новостей для публикации в ВК нет")
- else:
- # Отфильтровываем посты ниже определенного ID статьи
- for topic_id, topic_title in reversed(list_for_public):
- logging.info(f"Новости для публикации в ВК: {list_for_public}")
- text_data = news_content(topic_id)
- if text_data:
- content = f"{topic_title}\t\n" + text_data + "\n"
- content = remove_markdown_links(content)
-
- content = re.sub(r'https://linux-gaming.ru/uploads/default/original/1X/5cfa59077a5275971401fab0114e56f3ffdd0ec4.png', '\n', content, flags=re.DOTALL)
- links = extract_links(content)
-
- if "Кумулятивное обновление скриптов" in topic_title:
- # Пример добавления изображения с постом
- vk_post(url_vk_post, content, "photo-99238527_457244491")
- else:
- if links:
- vk_post(url_vk_post, content, links)
- else:
- vk_post(url_vk_post, content)
- else:
- logging.warning(f"Не удалось получить текст новости {topic_id}")
- time.sleep(1.0)
- else:
- logging.warning(f"Новостей для публикации в ВК нет")
-
-
-def tg_post(post_text, client_tg):
- # Отправка сообщения
- client_tg.send_message(keys.channel_username_tg, post_text)
- # Завершение сеанса
- client_tg.disconnect()
-
-
-async def get_tg_messages(client_tg, channel_username_tg):
- messages = []
- async for message in client_tg.iter_messages(channel_username_tg, limit=100):
- if message.text: # Проверка на NoneType
- logging.debug(f"Найдены сообщения в Telegram канале {message.text}")
- messages.append(message.text)
- return messages
-
-
-async def check_tg_news():
- session_file = 'LG_news'
- client_tg = TelegramClient(session_file, keys.api_id_tg, keys.api_hash_tg)
-
- async with client_tg:
- await client_tg.start()
- tg_messages = await get_tg_messages(client_tg, keys.channel_username_tg)
- list_titles_and_ids = news()
- if list_titles_and_ids:
- list_for_public = []
-
- for topic_id, topic_title in list_titles_and_ids:
- if all(topic_title not in (msg or '') for msg in tg_messages) and topic_id > keys.start_topic_id:
- list_for_public.append((topic_id, topic_title))
-
- if not list_for_public:
- logging.warning(f"Новостей для публикации в Telegram нет")
- else:
- logging.info(f"Новости для публикации в Telegram: {list_for_public}")
- for topic_id, topic_title in reversed(list_for_public):
- text_data = news_content(topic_id)
- if text_data:
- content = f"### {topic_title}\t\n" + text_data + "\n"
- while True:
- try:
- await client_tg.send_message(keys.channel_username_tg, content)
- break
- except FloodWaitError as e:
- logging.warning(f"Flood wait error: нужно подождать {e.seconds} секунд.")
- await asyncio.sleep(e.seconds) # Ждем
- time.sleep(1.0) # указанное время перед повторной попыткой
-
-
-def run_job():
- asyncio.run(job())
-
-
-if __name__ == '__main__':
- # Выполняем задачу немедленно при старте
- run_job()
-
- # Планируем выполнение задачи каждые N минут
- schedule.every(30).minutes.do(check_vk_posts).do(run_job)
-
- logger.info("Запуск планировщика задач")
- while True:
- schedule.run_pending()
- time.sleep(5)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4331a18
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,23 @@
+# Основные зависимости для bot-news-linux-gaming
+# Обновлено: 2025-08-18
+
+# Telegram клиент
+Telethon>=1.40.0
+
+# HTTP запросы
+requests>=2.32.0
+
+# Планировщик задач
+schedule>=1.2.0
+
+# Логирование с цветами
+colorlog>=6.9.0
+
+# Конвертация HTML в текст
+html2text>=2025.4.0
+
+# Парсинг HTML
+beautifulsoup4>=4.13.0
+
+# Discord клиент (опционально)
+# discord.py>=2.3.0
\ No newline at end of file