Upload files to "/"

This commit is contained in:
2025-09-25 20:43:09 +03:00
parent f7071a017d
commit cdb85f61c7
4 changed files with 1891 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
#!/bin/bash
# Директория со скриптами
SCRIPTS_DIR="/opt/testbot"
# Активация виртуального окружения
VENV_ACTIVATE="$SCRIPTS_DIR/venv/bin/activate"
# Список скриптов для запуска в порядке выполнения
SCRIPTS=("vk_load.py" "zk_load.py" "db_update_shortname.py" "evt_prefetch.py" "tg_publish.py" "evtg_publish.py")
# Интервалы между скриптами (в секундах)
DELAYS=(2 2 2 2 30)
# Лог-файл
LOG_FILE="/opt/testbot/runner.log"
# Функция для логирования
log_message() {
local message="$1"
echo "$(date '+%Y-%m-%d %H:%M:%S') - $message" | tee -a "$LOG_FILE"
}
# Проверка и настройка лог-файла
setup_logfile() {
# Проверяем, существует ли файл и можно ли в него писать
if [ ! -f "$LOG_FILE" ]; then
# Пытаемся создать файл
if ! touch "$LOG_FILE" 2>/dev/null; then
# Если не получается, пробуем с sudo
if sudo touch "$LOG_FILE" 2>/dev/null; then
sudo chown $(whoami) "$LOG_FILE"
sudo chmod 644 "$LOG_FILE"
log_message "Создан лог-файл с помощью sudo: $LOG_FILE"
else
echo "ОШИБКА: Не удалось создать лог-файл $LOG_FILE!"
exit 1
fi
fi
fi
# Проверяем права на запись
if [ ! -w "$LOG_FILE" ]; then
# Пытаемся изменить права
if sudo chown $(whoami) "$LOG_FILE" 2>/dev/null || chmod 644 "$LOG_FILE" 2>/dev/null; then
log_message "Изменены права на лог-файл: $LOG_FILE"
else
echo "ОШИБКА: Нет прав на запись в $LOG_FILE!"
exit 1
fi
fi
}
# Настройка лог-файла
setup_logfile
log_message "=== ЗАПУСК СКРИПТА ==="
log_message "Пользователь: $(whoami)"
log_message "Директория со скриптами: $SCRIPTS_DIR"
# Проверка существования директории
if [ ! -d "$SCRIPTS_DIR" ]; then
log_message "ОШИБКА: Директория $SCRIPTS_DIR не существует!"
exit 1
fi
# Активация виртуального окружения
if [ -f "$VENV_ACTIVATE" ]; then
source "$VENV_ACTIVATE"
log_message "Активировано виртуальное окружение: $VENV_ACTIVATE"
else
log_message "ОШИБКА: Файл активации venv $VENV_ACTIVATE не найден!"
exit 1
fi
# Переход в директорию со скриптами
cd "$SCRIPTS_DIR" || {
log_message "ОШИБКА: Не удалось перейти в $SCRIPTS_DIR"
exit 1
}
# Запуск скриптов
for i in "${!SCRIPTS[@]}"; do
script="${SCRIPTS[i]}"
# Проверка существования скрипта
if [ ! -f "$script" ]; then
log_message "ПРЕДУПРЕЖДЕНИЕ: Скрипт $script не найден, пропускаем."
continue
fi
log_message "Запуск скрипта: $script"
# Запуск Python-скрипта с записью вывода в лог
if python3 "$script" >> "$LOG_FILE" 2>&1; then
log_message "УСПЕХ: Скрипт $script завершился успешно."
else
exit_code=$?
log_message "ОШИБКА: Скрипт $script завершился с ошибкой (код: $exit_code)!"
log_message "=== ЗАВЕРШЕНИЕ С ОШИБКОЙ ==="
exit $exit_code
fi
# Пауза после скрипта (кроме последнего)
if [ $i -lt $((${#SCRIPTS[@]} - 1)) ]; then
delay="${DELAYS[i]}"
log_message "Ожидание $delay секунд перед следующим скриптом..."
sleep $delay
fi
done
log_message "ВСЕ СКРИПТЫ ВЫПОЛНЕНЫ УСПЕШНО!"
log_message "=== ЗАВЕРШЕНИЕ ==="
+1206
View File
File diff suppressed because it is too large Load Diff
+324
View File
@@ -0,0 +1,324 @@
import os
import logging
import asyncio
import pymysql
import time
import html
import json
import re
from datetime import datetime, timezone
from telegram import Bot, PollOption
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')
LOG_FILE = os.getenv('LOG_FILE', 'post_publisher.log')
ORG_MESSAGE_TAG = os.getenv('ORG_MESSAGE_TAG', '') # Хештег для определения организационных сообщений
# Параметры длины сообщений
MAX_CAPTION_LENGTH = int(os.getenv('MAX_CAPTION_LENGTH', 1000))
MAX_TEXT_LENGTH = int(os.getenv('MAX_TEXT_LENGTH', 4000))
# Настройка логирования
logger = logging.getLogger('TG_Poster')
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 prepare_text(text):
"""Подготовка текстовых полей с экранированием HTML-сущностей"""
if not text:
return ""
# Экранируем специальные символы HTML
text = html.escape(str(text))
return text
def has_org_tag(text):
"""Проверяет, содержит ли текст организационный хештег"""
if not text or not ORG_MESSAGE_TAG:
return False
# Ищем хештег (с учетом возможных пробелов и других символов)
pattern = r'#?' + re.escape(ORG_MESSAGE_TAG) + r'\b'
return bool(re.search(pattern, text, re.IGNORECASE))
async def publish_to_tg(vk_post_id):
"""Публикация одной записи по VK post ID"""
logger.info(f"Запуск публикации для записи VK ID {vk_post_id}")
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:
cursor.execute("""
SELECT id, vk_post_id, text, image_url, vk_post_url, is_poll, poll_question,
poll_options, poll_multiple, poll_end_date, is_event
FROM posts
WHERE vk_post_id = %s
AND marked_for_publication = True
AND published_in_tg = False
""", (vk_post_id,))
post = cursor.fetchone()
if not post:
logger.info(f"Запись VK ID {vk_post_id} не найдена или уже опубликована")
return False
bot = Bot(token=BOT_TOKEN)
# Проверяем наличие организационного хештега
is_org_message = has_org_tag(post['text'])
# Предварительный расчет строки ссылок (с запасом 20 символов для message_id)
placeholder_message_id = '0' * 20 # Заполнитель для message_id
# Формируем ссылки
links = []
# Ссылка на оригинал в ВК
if post['vk_post_url'] and (post['vk_post_url'].startswith('http://') or post['vk_post_url'].startswith('https://')):
links.append(f'<a href="{post["vk_post_url"]}">Оригинал в ВК</a>')
# Ссылка на подписку (только для событий)
if post['is_event']:
links.append(f'<a href="https://t.me/{RESPONDER_BOT_NAME}?start=post_{placeholder_message_id}">🔜 Подписка</a>')
# Формируем строку ссылок
links_line = " | ".join(links) if links else ""
links_line_length = len(links_line)
# Определяем максимальную длину в зависимости от типа сообщения
# Если это организационное сообщение, используем текстовый лимит
image_url = post['image_url']
has_image = image_url and image_url.strip() and not is_org_message
max_length = MAX_CAPTION_LENGTH if has_image else MAX_TEXT_LENGTH
# Вычисляем доступную длину для текста
available_length = max_length - links_line_length - 2 # -2 для символов переноса строки
if available_length < 0:
available_length = 0
# Получаем обработанный текст с учетом доступной длины
text = get_event_text(post['text'], available_length) if post['text'] else ""
# Публикация первоначального сообщения без ссылок
if has_image and not is_org_message:
try:
message = await bot.send_photo(
chat_id=CHANNEL_ID,
photo=image_url,
caption=text,
parse_mode="HTML",
disable_notification=True
)
except TelegramError as e:
logger.warning(f"Не удалось отправить изображение для записи VK ID {vk_post_id}: {e}. Отправляем текстовое сообщение.")
message = await bot.send_message(
chat_id=CHANNEL_ID,
text=text,
parse_mode="HTML",
disable_notification=True,
disable_web_page_preview=True
)
else:
# Если это организационное сообщение или нет изображения, отправляем текстовое сообщение
message = await bot.send_message(
chat_id=CHANNEL_ID,
text=text,
parse_mode="HTML",
disable_notification=True,
disable_web_page_preview=True
)
# Получаем ID сообщения для использования в ссылке подписки
message_id = message.message_id
# Формируем финальный текст с ссылками
if links:
# Обновляем ссылку подписки с реальным message_id
updated_links = []
if post['vk_post_url'] and (post['vk_post_url'].startswith('http://') or post['vk_post_url'].startswith('https://')):
updated_links.append(f'<a href="{post["vk_post_url"]}">Оригинал в ВК</a>')
if post['is_event']:
updated_links.append(f'<a href="https://t.me/{RESPONDER_BOT_NAME}?start=post_{message_id}">🔜 Подписка</a>')
links_line = " | ".join(updated_links)
final_text = f"{text}\n\n{links_line}"
await asyncio.sleep(2) # задержка между обращениями к телеграм
# Редактируем сообщение, добавляя ссылки
if has_image and not is_org_message:
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"Не удалось отредактировать подпись для записи VK ID {vk_post_id}: {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
)
await asyncio.sleep(2) # задержка между обращениями к телеграм
# Публикация опроса, если требуется
poll_message_id = None
if post['is_poll'] and post['poll_question'] and post['poll_options']:
try:
# Парсим варианты ответа
options = json.loads(post['poll_options'])
poll_options = [PollOption(option, 0) for option in options]
# Отправляем опрос (в каналах можно отправлять только анонимные опросы)
poll_message = await bot.send_poll(
chat_id=CHANNEL_ID,
question=post['poll_question'],
options=poll_options,
is_anonymous=True, # В каналах можно отправлять только анонимные опросы
allows_multiple_answers=post['poll_multiple'],
close_date=post['poll_end_date'] if post['poll_end_date'] else None,
disable_notification=True
)
poll_message_id = poll_message.message_id
logger.info(f"Опубликован опрос для записи VK ID {vk_post_id}")
except Exception as e:
logger.error(f"Ошибка при публикации опроса для записи VK ID {vk_post_id}: {e}")
# Обновляем запись в базе данных
current_time_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("""
UPDATE posts
SET published_in_tg = True,
tg_publication_date = %s,
tg_message_id = %s,
tg_poll_id = %s
WHERE vk_post_id = %s
""", (current_time_utc, message_id, poll_message_id, vk_post_id))
conn.commit()
logger.info(f"Запись VK ID {vk_post_id} успешно опубликована")
return True
except RetryAfter as e:
logger.error(f"Получена ошибка FloodWait (429) при публикации записи VK ID {vk_post_id}: {e}")
logger.error(f"Необходимо подождать {e.retry_after} секунд перед следующей попыткой")
raise
except TelegramError as e:
logger.error(f"Ошибка Telegram при публикации записи VK ID {vk_post_id}: {e}")
return False
except Exception as e:
logger.error(f"Неожиданная ошибка при публикации записи VK ID {vk_post_id}: {e}")
return False
finally:
if conn:
conn.close()
async def publish_to_tg_all():
"""Публикация всех неопубликованных записей (максимум 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:
cursor.execute("""
SELECT vk_post_id
FROM posts
WHERE marked_for_publication = True
AND published_in_tg = False
LIMIT 5
""")
posts = cursor.fetchall()
post_count = 0
for post in posts:
try:
success = await publish_to_tg(post['vk_post_id'])
if success:
post_count += 1
# Задержка между публикациями разных записей
if post_count < len(posts):
await asyncio.sleep(2)
except RetryAfter as e:
logger.error(f"Прерываем публикацию из-за ошибки FloodWait. Ожидание: {e.retry_after} секунд")
break
except Exception as e:
logger.error(f"Ошибка при публикации записи VK ID {post['vk_post_id']}: {e}")
# Продолжаем публикацию следующих записей, несмотря на ошибку
logger.info(f"Опубликовано записей в этом запуске: {post_count}")
except Exception as e:
logger.error(f"Ошибка при публикации: {e}")
finally:
if conn:
conn.close()
logger.info("Завершение работы скрипта")
if __name__ == "__main__":
asyncio.run(publish_to_tg_all())
+248
View File
@@ -0,0 +1,248 @@
import mysql.connector
import requests
import json
import os
import time
import random
from datetime import datetime
from mysql.connector import errorcode
from dotenv import load_dotenv
from ensure_db import ensure_database_structure
# Загрузка переменных окружения из .env файла
load_dotenv()
# Получение конфигурации из переменных окружения
VK_GROUP_ID = int(os.getenv('VK_GROUP_ID', -3342146))
VK_ACCESS_TOKEN = os.getenv('VK_ACCESS_TOKEN', '')
VK_API_VERSION = os.getenv('VK_API_VERSION', '5.131')
IS_EVENT = os.getenv('IS_EVENT', 'false').lower() in ('true', '1', 'yes', 'on')
AUTO_PUBLISH = os.getenv('AUTO_PUBLISH', 'false').lower() in ('true', '1', 'yes', 'on')
LOG_FILE = os.getenv('LOG_FILE', 'vk_loader.log') # Путь к лог-файлу
LOG_PREFIX = "VK_loader" # Уникальный префикс для идентификации скрипта
# Конфигурация MariaDB из .env
DB_CONFIG = {
'host': os.getenv('MDB_HOST', 'localhost'),
'user': os.getenv('MDB_USER', 'root'),
'password': os.getenv('MDB_PW', ''),
'database': os.getenv('MDBASE', 'vk_posts')
}
def log_message(message, max_retries=5, retry_delay=0.1):
"""
Записывает сообщение в лог-файл с обработкой блокировок
и идентификатором скрипта
"""
if not LOG_FILE:
return
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Добавляем идентификатор скрипта к сообщению
log_line = f"[{timestamp}] [{LOG_PREFIX}] {message}\n"
for attempt in range(max_retries):
try:
with open(LOG_FILE, 'a', encoding='utf-8') as log:
log.write(log_line)
return True
except (IOError, OSError) as e:
if "locked" in str(e).lower() and attempt < max_retries - 1:
# Случайная задержка для уменьшения коллизий
sleep_time = retry_delay * (1 + random.random() * 0.5)
time.sleep(sleep_time)
else:
# Если не удалось записать после всех попыток
print(f"Ошибка записи в лог: {e}")
print(f"Сообщение для лога: {log_line.strip()}")
return False
def get_vk_posts():
"""Получает последние 10 постов из VK группы"""
url = 'https://api.vk.ru/method/wall.get'
params = {
'owner_id': VK_GROUP_ID,
'count': 10,
'access_token': VK_ACCESS_TOKEN,
'v': VK_API_VERSION,
'extended': 1
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if 'error' in data:
error_msg = f"VK API Error: {data['error']['error_msg']}"
log_message(error_msg)
raise Exception(error_msg)
return data['response']['items']
except requests.exceptions.RequestException as e:
error_msg = f"Ошибка подключения к VK API: {e}"
log_message(error_msg)
raise Exception(error_msg)
def process_post(post):
"""Извлекает необходимые данные из поста VK"""
post_id = post['id']
vk_post_url = f"https://vk.ru/wall{VK_GROUP_ID}_{post_id}"
published_at = datetime.utcfromtimestamp(post['date']).strftime('%Y-%m-%d %H:%M:%S')
# Инициализация полей опроса
is_poll = 0
poll_question = None
poll_options = None
poll_multiple = False
poll_end_date = None
# Инициализация поля изображения
image_url = None
# Обработка вложений
for attachment in post.get('attachments', []):
if attachment['type'] == 'poll':
is_poll = 1
poll = attachment['poll']
poll_question = poll['question']
poll_options = json.dumps([answer['text'] for answer in poll['answers']])
poll_multiple = bool(poll.get('multiple', False))
if poll.get('end_date', 0) > 0:
poll_end_date = datetime.utcfromtimestamp(poll['end_date']).strftime('%Y-%m-%d %H:%M:%S')
elif attachment['type'] == 'photo' and not image_url:
# Берем только первое изображение
photo = attachment['photo']
sizes = photo.get('sizes', [])
if sizes:
# Выбираем изображение максимального качества
max_size = max(sizes, key=lambda s: s.get('width', 0) * s.get('height', 0))
image_url = max_size['url']
return {
'vk_post_id': post_id,
'text': post['text'],
'image_url': image_url,
'vk_post_url': vk_post_url,
'published_at': published_at,
'is_poll': is_poll,
'poll_question': poll_question,
'poll_options': poll_options,
'poll_multiple': poll_multiple,
'poll_end_date': poll_end_date
}
def save_to_database(posts):
"""Сохраняет новые посты в MariaDB с учетом настроек публикации"""
try:
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor()
new_posts_count = 0
for post in posts:
try:
# Добавляем поля is_event, marked_for_publication, shortname и action_number
cursor.execute('''
INSERT INTO posts (
vk_post_id, text, image_url, vk_post_url, published_at,
is_poll, poll_question, poll_options, poll_multiple, poll_end_date,
is_event, marked_for_publication, shortname, action_number
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE vk_post_id = VALUES(vk_post_id)
''', (
post['vk_post_id'],
post['text'],
post['image_url'],
post['vk_post_url'],
post['published_at'],
post['is_poll'],
post['poll_question'],
post['poll_options'],
int(post['poll_multiple']),
post['poll_end_date'],
IS_EVENT,
int(AUTO_PUBLISH), # Значение из .env
None, # shortname - пока не используется, устанавливаем NULL
None # action_number - пока не используется, устанавливаем NULL
))
if cursor.rowcount > 0:
new_posts_count += 1
# Логируем добавление нового поста
log_message(f"Добавлен новый пост: ID {post['vk_post_id']}, Дата: {post['published_at']}, Автопубликация: {'Да' if AUTO_PUBLISH else 'Нет'}")
except mysql.connector.Error as err:
error_msg = f"Ошибка при вставке поста {post['vk_post_id']}: {err}"
log_message(error_msg)
continue
conn.commit()
return new_posts_count
except mysql.connector.Error as err:
error_msg = f"Ошибка подключения к базе данных: {err}"
log_message(error_msg)
return 0
finally:
if 'conn' in locals() and conn.is_connected():
cursor.close()
conn.close()
def vk_load_10():
"""
Экспортируемая функция для загрузки 10 последних постов из VK
Возвращает количество добавленных постов
"""
# Стартовая информация
start_msg = f"Старт скрипта | База: {DB_CONFIG['database']}@{DB_CONFIG['host']} | IS_EVENT: {IS_EVENT} | AUTO_PUBLISH: {AUTO_PUBLISH}"
print(start_msg)
log_message(start_msg)
# Обеспечиваем структуру БД
try:
ensure_database_structure()
except Exception as e:
error_msg = f"Не удалось создать структуру базы данных: {e}"
print(error_msg)
log_message(error_msg)
return 0
# Получаем посты из VK
try:
vk_posts = get_vk_posts()
status_msg = f"Получено {len(vk_posts)} постов из VK"
print(status_msg)
log_message(status_msg)
except Exception as e:
error_msg = f"Ошибка при получении данных из VK: {e}"
print(error_msg)
log_message(error_msg)
return 0
# Обрабатываем и сохраняем посты
processed_posts = [process_post(post) for post in vk_posts]
new_count = save_to_database(processed_posts)
# Фиксируем результат
if new_count > 0:
result_msg = f"Добавлено {new_count} новых постов в базу данных"
else:
result_msg = "Новые посты не обнаружены"
print(result_msg)
log_message(result_msg)
# Завершение работы
end_msg = "Работа скрипта завершена"
print(end_msg)
log_message(end_msg)
return new_count
def main():
"""Основная функция для запуска скрипта из командной строки"""
vk_load_10()
if __name__ == "__main__":
main()