Compare commits
5 Commits
de5a5e9248
...
master
Author | SHA1 | Date | |
---|---|---|---|
47e1adfed7 | |||
8a01f52c69 | |||
0990e5191a | |||
2de09231ea | |||
50d505f887 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,3 +6,6 @@ __pycache__
|
||||
keys.py
|
||||
keys_*.py
|
||||
!keys_example.py
|
||||
venv/
|
||||
.env
|
||||
*.pyc
|
Binary file not shown.
33
bot-news-linux-gaming.service
Normal file
33
bot-news-linux-gaming.service
Normal file
@@ -0,0 +1,33 @@
|
||||
[Unit]
|
||||
Description=Linux Gaming News Bot
|
||||
Documentation=https://github.com/xpamych/bot-news-linux-gaming
|
||||
After=network.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=xpamych
|
||||
Group=xpamych
|
||||
WorkingDirectory=/home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming
|
||||
ExecStart=/usr/bin/python3 /home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming/news-bot-modular.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Переменные окружения
|
||||
Environment=PYTHONPATH=/home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
# Ограничения безопасности
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming
|
||||
|
||||
# Логирование
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=news-bot
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@@ -131,10 +131,16 @@ class ContentProcessor:
|
||||
|
||||
def extract_links(self, text):
|
||||
self.logger.debug("Извлекаем ссылки из текста")
|
||||
url_pattern = re.compile(r'https?://\S+')
|
||||
# Улучшенное регулярное выражение, исключающее конечные знаки препинания
|
||||
url_pattern = re.compile(r'https?://[^\s\)\]\}\>,;]+')
|
||||
links = url_pattern.findall(text)
|
||||
self.logger.debug(f"Найденные ссылки: {links}")
|
||||
return links
|
||||
# Дополнительная очистка: убираем точки и запятые в конце
|
||||
cleaned_links = []
|
||||
for link in links:
|
||||
link = link.rstrip('.,!?')
|
||||
cleaned_links.append(link)
|
||||
self.logger.debug(f"Найденные ссылки: {cleaned_links}")
|
||||
return cleaned_links
|
||||
|
||||
def format_for_vk(self, content):
|
||||
"""Форматирование контента для VK"""
|
||||
|
@@ -80,14 +80,50 @@ class DiscordClient:
|
||||
return None
|
||||
|
||||
async def send_message(self, channel, content):
|
||||
"""Отправка сообщения в Discord канал"""
|
||||
"""Отправка сообщения в Discord канал с разбивкой длинных сообщений"""
|
||||
if not self.is_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
# Разбиваем содержимое на части по 2000 символов (лимит Discord)
|
||||
for i in range(0, len(content), 2000):
|
||||
await channel.send(content[i:i+2000])
|
||||
# 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}")
|
||||
@@ -142,7 +178,7 @@ class DiscordClient:
|
||||
await client.close()
|
||||
return
|
||||
|
||||
# Публикуем новости
|
||||
# Публикуем новости в обратном порядке, чтобы новые оказались сверху в ленте
|
||||
for topic_id, topic_title in reversed(list_for_public):
|
||||
from site_api import SiteAPI
|
||||
site_api = SiteAPI()
|
||||
|
127
install-service.sh
Executable file
127
install-service.sh
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Установочный скрипт для Linux Gaming News Bot
|
||||
# Создает systemd service и настраивает его для автозапуска
|
||||
|
||||
set -e # Выход при любой ошибке
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функции для цветного вывода
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Проверка запуска от root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_error "Скрипт должен быть запущен с правами root (sudo)"
|
||||
print_info "Используйте: sudo ./install-service.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Определение текущего пользователя (не root)
|
||||
REAL_USER=$(who am i | awk '{print $1}')
|
||||
if [ -z "$REAL_USER" ]; then
|
||||
REAL_USER=$(logname 2>/dev/null || echo $SUDO_USER)
|
||||
fi
|
||||
|
||||
if [ -z "$REAL_USER" ]; then
|
||||
print_error "Не удалось определить имя пользователя"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Установка сервиса для пользователя: $REAL_USER"
|
||||
|
||||
# Определение директории проекта
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$SCRIPT_DIR"
|
||||
|
||||
print_info "Директория проекта: $PROJECT_DIR"
|
||||
|
||||
# Проверка существования файлов
|
||||
if [ ! -f "$PROJECT_DIR/news-bot-modular.py" ]; then
|
||||
print_error "Не найден файл news-bot-modular.py в $PROJECT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PROJECT_DIR/bot-news-linux-gaming.service" ]; then
|
||||
print_error "Не найден файл bot-news-linux-gaming.service в $PROJECT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверка наличия keys.py
|
||||
if [ ! -f "$PROJECT_DIR/keys.py" ]; then
|
||||
print_warning "Не найден файл keys.py с настройками"
|
||||
print_info "Создайте keys.py на основе keys_example.py перед запуском сервиса:"
|
||||
print_info " cp keys_example.py keys.py"
|
||||
print_info " nano keys.py # заполните реальными ключами"
|
||||
fi
|
||||
|
||||
# Создание временного файла сервиса с правильными путями
|
||||
TEMP_SERVICE=$(mktemp)
|
||||
sed "s|/home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming|$PROJECT_DIR|g" "$PROJECT_DIR/bot-news-linux-gaming.service" > "$TEMP_SERVICE"
|
||||
sed -i "s|User=xpamych|User=$REAL_USER|g" "$TEMP_SERVICE"
|
||||
sed -i "s|Group=xpamych|Group=$REAL_USER|g" "$TEMP_SERVICE"
|
||||
|
||||
print_info "Копирование systemd unit файла..."
|
||||
cp "$TEMP_SERVICE" /etc/systemd/system/bot-news-linux-gaming.service
|
||||
rm "$TEMP_SERVICE"
|
||||
|
||||
print_info "Установка прав доступа..."
|
||||
chmod 644 /etc/systemd/system/bot-news-linux-gaming.service
|
||||
chown root:root /etc/systemd/system/bot-news-linux-gaming.service
|
||||
|
||||
print_info "Перезагрузка systemd..."
|
||||
systemctl daemon-reload
|
||||
|
||||
print_success "Сервис успешно установлен!"
|
||||
print_info ""
|
||||
print_info "Для управления сервисом используйте команды:"
|
||||
print_info " sudo systemctl enable bot-news-linux-gaming # Включить автозапуск"
|
||||
print_info " sudo systemctl start bot-news-linux-gaming # Запустить сервис"
|
||||
print_info " sudo systemctl status bot-news-linux-gaming # Посмотреть статус"
|
||||
print_info " sudo systemctl stop bot-news-linux-gaming # Остановить сервис"
|
||||
print_info " sudo systemctl disable bot-news-linux-gaming # Отключить автозапуск"
|
||||
print_info ""
|
||||
print_info "Логи сервиса:"
|
||||
print_info " sudo journalctl -u bot-news-linux-gaming -f # Следить за логами"
|
||||
print_info " sudo journalctl -u bot-news-linux-gaming -n 50 # Последние 50 строк"
|
||||
print_info ""
|
||||
|
||||
# Предложение сразу включить и запустить
|
||||
read -p "Включить автозапуск и запустить сервис сейчас? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
print_info "Включение автозапуска..."
|
||||
systemctl enable bot-news-linux-gaming
|
||||
|
||||
print_info "Запуск сервиса..."
|
||||
systemctl start bot-news-linux-gaming
|
||||
|
||||
sleep 2
|
||||
|
||||
print_info "Статус сервиса:"
|
||||
systemctl status bot-news-linux-gaming --no-pager
|
||||
|
||||
print_success "Сервис запущен и добавлен в автозагрузку!"
|
||||
else
|
||||
print_info "Сервис установлен, но не запущен. Запустите его командой:"
|
||||
print_info " sudo systemctl enable --now bot-news-linux-gaming"
|
||||
fi
|
597
news-bot.py
597
news-bot.py
@@ -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/", '')
|
||||
|
||||
# Находим текст до определенного текста, тега или класса (например, до тега <hr>)
|
||||
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)
|
23
requirements.txt
Normal file
23
requirements.txt
Normal file
@@ -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
|
15
run_bot.sh
Executable file
15
run_bot.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для запуска бота в виртуальном окружении
|
||||
# Использование: ./run_bot.sh
|
||||
|
||||
# Проверка наличия виртуального окружения
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Виртуальное окружение не найдено!"
|
||||
echo "Сначала запустите: bash setup_venv.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Запуск бота через виртуальное окружение
|
||||
echo "Запуск бота..."
|
||||
./venv/bin/python3 news-bot-modular.py
|
155
service-control.sh
Executable file
155
service-control.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Простой скрипт управления Linux Gaming News Bot
|
||||
|
||||
SERVICE_NAME="bot-news-linux-gaming"
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Функция показа статуса
|
||||
show_status() {
|
||||
print_info "Статус сервиса $SERVICE_NAME:"
|
||||
systemctl status $SERVICE_NAME --no-pager -l
|
||||
}
|
||||
|
||||
# Функция показа логов
|
||||
show_logs() {
|
||||
local lines=${1:-50}
|
||||
print_info "Последние $lines строк логов:"
|
||||
sudo journalctl -u $SERVICE_NAME -n $lines --no-pager
|
||||
}
|
||||
|
||||
# Функция следения за логами
|
||||
follow_logs() {
|
||||
print_info "Следование за логами (Ctrl+C для выхода):"
|
||||
sudo journalctl -u $SERVICE_NAME -f
|
||||
}
|
||||
|
||||
# Функция показа помощи
|
||||
show_help() {
|
||||
echo "Управление Linux Gaming News Bot"
|
||||
echo ""
|
||||
echo "Использование: $0 [команда]"
|
||||
echo ""
|
||||
echo "Команды:"
|
||||
echo " start - Запустить сервис"
|
||||
echo " stop - Остановить сервис"
|
||||
echo " restart - Перезапустить сервис"
|
||||
echo " status - Показать статус сервиса"
|
||||
echo " enable - Включить автозапуск"
|
||||
echo " disable - Отключить автозапуск"
|
||||
echo " logs - Показать последние логи"
|
||||
echo " logs N - Показать последние N строк логов"
|
||||
echo " follow - Следить за логами в реальном времени"
|
||||
echo " install - Установить сервис (требует sudo)"
|
||||
echo " uninstall - Удалить сервис (требует sudo)"
|
||||
echo " help - Показать эту справку"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Проверка существования сервиса
|
||||
check_service_exists() {
|
||||
if ! systemctl list-unit-files | grep -q "^$SERVICE_NAME.service"; then
|
||||
print_error "Сервис $SERVICE_NAME не установлен"
|
||||
print_info "Запустите: sudo ./install-service.sh"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Основная логика
|
||||
case "${1}" in
|
||||
"start")
|
||||
check_service_exists
|
||||
print_info "Запуск сервиса..."
|
||||
sudo systemctl start $SERVICE_NAME
|
||||
show_status
|
||||
;;
|
||||
"stop")
|
||||
check_service_exists
|
||||
print_info "Остановка сервиса..."
|
||||
sudo systemctl stop $SERVICE_NAME
|
||||
show_status
|
||||
;;
|
||||
"restart")
|
||||
check_service_exists
|
||||
print_info "Перезапуск сервиса..."
|
||||
sudo systemctl restart $SERVICE_NAME
|
||||
show_status
|
||||
;;
|
||||
"status")
|
||||
check_service_exists
|
||||
show_status
|
||||
;;
|
||||
"enable")
|
||||
check_service_exists
|
||||
print_info "Включение автозапуска..."
|
||||
sudo systemctl enable $SERVICE_NAME
|
||||
print_success "Автозапуск включен"
|
||||
;;
|
||||
"disable")
|
||||
check_service_exists
|
||||
print_info "Отключение автозапуска..."
|
||||
sudo systemctl disable $SERVICE_NAME
|
||||
print_success "Автозапуск отключен"
|
||||
;;
|
||||
"logs")
|
||||
check_service_exists
|
||||
if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then
|
||||
show_logs $2
|
||||
else
|
||||
show_logs
|
||||
fi
|
||||
;;
|
||||
"follow")
|
||||
check_service_exists
|
||||
follow_logs
|
||||
;;
|
||||
"install")
|
||||
if [ ! -f "./install-service.sh" ]; then
|
||||
print_error "Файл install-service.sh не найден"
|
||||
exit 1
|
||||
fi
|
||||
sudo ./install-service.sh
|
||||
;;
|
||||
"uninstall")
|
||||
if [ ! -f "./uninstall-service.sh" ]; then
|
||||
print_error "Файл uninstall-service.sh не найден"
|
||||
exit 1
|
||||
fi
|
||||
sudo ./uninstall-service.sh
|
||||
;;
|
||||
"help"|"--help"|"-h")
|
||||
show_help
|
||||
;;
|
||||
"")
|
||||
print_warning "Команда не указана"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
print_error "Неизвестная команда: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
40
setup_venv.sh
Executable file
40
setup_venv.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт установки виртуального окружения для bot-news-linux-gaming
|
||||
# Запускать на сервере: bash setup_venv.sh
|
||||
|
||||
echo "=== Установка виртуального окружения для бота ==="
|
||||
|
||||
# 1. Установка python3-venv если не установлен
|
||||
echo "Проверка наличия python3-venv..."
|
||||
sudo apt update
|
||||
sudo apt install -y python3-venv python3-full
|
||||
|
||||
# 2. Создание виртуального окружения
|
||||
echo "Создание виртуального окружения..."
|
||||
python3 -m venv venv
|
||||
|
||||
# 3. Активация виртуального окружения
|
||||
echo "Активация виртуального окружения..."
|
||||
source venv/bin/activate
|
||||
|
||||
# 4. Обновление pip
|
||||
echo "Обновление pip..."
|
||||
pip install --upgrade pip
|
||||
|
||||
# 5. Установка зависимостей
|
||||
echo "Установка зависимостей..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 6. Удаление старых сессий
|
||||
echo "Удаление старых файлов сессий..."
|
||||
rm -f *.session
|
||||
|
||||
echo "=== Установка завершена ==="
|
||||
echo ""
|
||||
echo "Для запуска бота используйте:"
|
||||
echo " source venv/bin/activate"
|
||||
echo " python3 news-bot-modular.py"
|
||||
echo ""
|
||||
echo "Или запускайте напрямую:"
|
||||
echo " ./venv/bin/python3 news-bot-modular.py"
|
@@ -82,11 +82,58 @@ class TelegramNewsClient:
|
||||
return None
|
||||
|
||||
async def send_message(self, client, channel_username, content):
|
||||
"""Отправка сообщения в Telegram канал/топик с обработкой flood wait"""
|
||||
"""Отправка сообщения в Telegram канал/топик с обработкой flood wait и длинных сообщений"""
|
||||
try:
|
||||
# Получаем entity канала
|
||||
entity = await client.get_entity(channel_username)
|
||||
|
||||
# Telegram лимит: 4096 символов
|
||||
max_length = 4096
|
||||
|
||||
# Если сообщение слишком длинное, разбиваем его
|
||||
if len(content) > max_length:
|
||||
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)} частей")
|
||||
|
||||
# Отправляем каждую часть
|
||||
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 self._send_single_message(client, entity, part_content)
|
||||
|
||||
# Небольшая задержка между частями
|
||||
if i < len(parts):
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
# Обычная отправка
|
||||
await self._send_single_message(client, entity, content)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка получения entity канала '{channel_username}': {e}")
|
||||
self.logger.info("Убедитесь, что имя канала указано правильно и бот имеет доступ")
|
||||
|
||||
async def _send_single_message(self, client, entity, content):
|
||||
"""Отправка одного сообщения с обработкой flood wait"""
|
||||
while True:
|
||||
try:
|
||||
# Если указан topic_id, отправляем в топик
|
||||
@@ -108,10 +155,7 @@ class TelegramNewsClient:
|
||||
await asyncio.sleep(e.seconds)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки сообщения в Telegram: {e}")
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка получения entity канала '{channel_username}': {e}")
|
||||
self.logger.info("Убедитесь, что имя канала указано правильно и бот имеет доступ")
|
||||
raise
|
||||
|
||||
async def check_and_publish_news(self, news_list):
|
||||
"""Проверка и публикация новостей в Telegram"""
|
||||
@@ -151,7 +195,7 @@ class TelegramNewsClient:
|
||||
|
||||
self.logger.info(f"Новости для публикации в Telegram: {list_for_public}")
|
||||
|
||||
# Публикуем новости
|
||||
# Публикуем новости в обратном порядке, чтобы новые оказались сверху в ленте
|
||||
for topic_id, topic_title in reversed(list_for_public):
|
||||
from site_api import SiteAPI
|
||||
site_api = SiteAPI()
|
||||
|
68
uninstall-service.sh
Executable file
68
uninstall-service.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт удаления Linux Gaming News Bot systemd service
|
||||
|
||||
set -e # Выход при любой ошибке
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функции для цветного вывода
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Проверка запуска от root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_error "Скрипт должен быть запущен с правами root (sudo)"
|
||||
print_info "Используйте: sudo ./uninstall-service.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVICE_NAME="bot-news-linux-gaming"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
|
||||
print_info "Удаление сервиса $SERVICE_NAME..."
|
||||
|
||||
# Остановка сервиса если он запущен
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
print_info "Остановка сервиса..."
|
||||
systemctl stop $SERVICE_NAME
|
||||
fi
|
||||
|
||||
# Отключение автозапуска если включен
|
||||
if systemctl is-enabled --quiet $SERVICE_NAME; then
|
||||
print_info "Отключение автозапуска..."
|
||||
systemctl disable $SERVICE_NAME
|
||||
fi
|
||||
|
||||
# Удаление файла сервиса
|
||||
if [ -f "$SERVICE_FILE" ]; then
|
||||
print_info "Удаление файла сервиса..."
|
||||
rm "$SERVICE_FILE"
|
||||
else
|
||||
print_warning "Файл сервиса не найден: $SERVICE_FILE"
|
||||
fi
|
||||
|
||||
# Перезагрузка systemd
|
||||
print_info "Перезагрузка systemd..."
|
||||
systemctl daemon-reload
|
||||
|
||||
print_success "Сервис $SERVICE_NAME успешно удален!"
|
||||
print_info "Файлы проекта не затронуты и остались на месте."
|
18
vk_client.py
18
vk_client.py
@@ -107,11 +107,25 @@ class VKClient:
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.post(url=URL_VK_POST, params=params_post, data=data)
|
||||
response_json = response.json()
|
||||
|
||||
if response.status_code == 200:
|
||||
# Проверяем наличие ошибки в ответе VK API
|
||||
if 'error' in response_json:
|
||||
error_code = response_json['error']['error_code']
|
||||
error_msg = response_json['error']['error_msg']
|
||||
self.logger.error(f"Ошибка VK API {error_code}: {error_msg}")
|
||||
if 'request_params' in response_json['error']:
|
||||
self.logger.debug(f"Параметры запроса: {response_json['error']['request_params']}")
|
||||
# Не повторяем попытки при ошибках валидации
|
||||
if error_code == 100: # Invalid parameter
|
||||
return None
|
||||
elif 'response' in response_json:
|
||||
self.logger.info("Сообщение успешно опубликовано в VK")
|
||||
self.logger.debug(response.json())
|
||||
self.logger.debug(f"ID поста: {response_json['response'].get('post_id', 'неизвестен')}")
|
||||
return response
|
||||
else:
|
||||
self.logger.warning(f"Неожиданный формат ответа VK: {response_json}")
|
||||
else:
|
||||
self.logger.warning(f"Ошибка при публикации в VK: {response.status_code} - {response.reason} (попытка {attempt + 1}/{max_retries})")
|
||||
|
||||
@@ -155,7 +169,7 @@ class VKClient:
|
||||
|
||||
self.logger.info(f"Новости для публикации в VK: {list_for_public}")
|
||||
|
||||
# Публикуем новости
|
||||
# Публикуем новости в обратном порядке, чтобы новые оказались сверху в ленте
|
||||
for topic_id, topic_title in reversed(list_for_public):
|
||||
from site_api import SiteAPI
|
||||
site_api = SiteAPI()
|
||||
|
Reference in New Issue
Block a user