Upload files to "/"

This commit is contained in:
2025-09-25 20:42:48 +03:00
parent 8644941092
commit f7071a017d
5 changed files with 845 additions and 0 deletions
+281
View File
@@ -0,0 +1,281 @@
# ensure_db.py
import mysql.connector
import os
import time
import random
from datetime import datetime
from mysql.connector import errorcode
from dotenv import load_dotenv
# Загрузка переменных окружения
load_dotenv()
# Конфигурация из переменных окружения
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')
}
LOG_FILE = os.getenv('LOG_FILE', 'vk_loader.log')
LOG_PREFIX = "DB_Ensure"
def log_message(message, max_retries=5, retry_delay=0.1):
"""Функция логирования с повторными попытками"""
if not LOG_FILE:
return False
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 attempt < max_retries - 1:
time.sleep(retry_delay * (1 + random.random()))
else:
print(f"Failed to log: {e}")
return False
def ensure_database_structure():
"""Основная функция проверки и создания БД/таблиц"""
conn = None
cursor = None
try:
# Подключение без указания базы данных
conn = mysql.connector.connect(
host=DB_CONFIG['host'],
user=DB_CONFIG['user'],
password=DB_CONFIG['password']
)
cursor = conn.cursor(buffered=True) # Используем буферизованный курсор
# Создание базы данных если не существует
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_CONFIG['database']} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
cursor.execute(f"USE {DB_CONFIG['database']}")
log_message("Database checked/created successfully")
# Список таблиц и их структур
tables = {
'posts': '''
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
vk_post_id BIGINT NOT NULL UNIQUE,
text TEXT,
image_url VARCHAR(2048),
vk_post_url VARCHAR(2048),
published_at DATETIME NULL,
is_poll BOOLEAN DEFAULT 0,
poll_question TEXT,
poll_options TEXT,
poll_multiple BOOLEAN DEFAULT 0,
poll_end_date DATETIME NULL,
marked_for_publication BOOLEAN DEFAULT 0,
published_in_tg BOOLEAN DEFAULT 0,
tg_publication_date DATETIME NULL,
tg_message_id BIGINT NULL,
tg_poll_id BIGINT NULL,
tg_markpost_id BIGINT NULL,
tg_poll_results TEXT DEFAULT '',
is_event BOOLEAN DEFAULT 0,
shortname VARCHAR(100),
action_number BIGINT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''',
'marks': '''
CREATE TABLE marks (
id INT AUTO_INCREMENT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tg_post_id BIGINT NOT NULL,
tg_user_id BIGINT NOT NULL,
tg_user_name VARCHAR(255),
autolink BOOLEAN DEFAULT 0,
reminded_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''',
'marks_evt': '''
CREATE TABLE marks_evt (
id INT AUTO_INCREMENT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tg_event_id BIGINT NOT NULL,
tg_user_id BIGINT NOT NULL,
tg_user_name VARCHAR(255),
autolink BOOLEAN DEFAULT 0,
reminded_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''',
'users': '''
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''',
'events': '''
CREATE TABLE events (
id INT AUTO_INCREMENT PRIMARY KEY,
id_event BIGINT NOT NULL,
number BIGINT NOT NULL,
unit_name VARCHAR(255),
name VARCHAR(255),
about VARCHAR(2048),
about_social_picture VARCHAR(2048),
modified DATETIME NULL,
added DATETIME NULL,
tags VARCHAR(255),
is_canceled BOOLEAN DEFAULT 0,
accepted BOOLEAN DEFAULT 0,
denied BOOLEAN DEFAULT 0,
is_visible BOOLEAN DEFAULT 0,
marked_to_publication BOOLEAN DEFAULT 0,
is_posted_tg BOOLEAN DEFAULT 0,
tg_message_id BIGINT NULL,
tg_info_id BIGINT NULL,
tg_posted_date DATETIME NULL,
announcement_link BIGINT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
''',
'interlinks': '''
CREATE TABLE interlinks (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id INT NOT NULL,
event_id INT NOT NULL,
created_at DATETIME NULL,
FOREIGN KEY (post_id)
REFERENCES posts(id)
ON DELETE CASCADE,
FOREIGN KEY (event_id)
REFERENCES events(id)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
'''
}
# Проверка и создание таблиц
for table_name, table_sql in tables.items():
try:
# Проверяем существование таблицы через information_schema
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = %s AND table_name = %s
""", (DB_CONFIG['database'], table_name))
if cursor.fetchone()[0] == 0:
# Таблица не существует, создаем её
cursor.execute(table_sql)
log_message(f"Table {table_name} created")
else:
log_message(f"Table {table_name} exists")
except mysql.connector.Error as err:
log_message(f"Error checking/creating table {table_name}: {err}")
raise err
# Проверка и добавление новых полей в существующие таблицы
table_alterations = {
'marks': [
{'column': 'autolink', 'type': 'BOOLEAN DEFAULT 0'},
{'column': 'reminded_at', 'type': 'DATETIME NULL'}
],
'marks_evt': [
{'column': 'autolink', 'type': 'BOOLEAN DEFAULT 0'},
{'column': 'reminded_at', 'type': 'DATETIME NULL'}
]
}
for table_name, alterations in table_alterations.items():
for alteration in alterations:
column = alteration['column']
column_type = alteration['type']
try:
# Проверяем существование столбца
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = %s AND table_name = %s AND column_name = %s
""", (DB_CONFIG['database'], table_name, column))
if cursor.fetchone()[0] == 0:
# Столбец не существует, добавляем его
cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column} {column_type}")
log_message(f"Added column {column} to table {table_name}")
else:
log_message(f"Column {column} already exists in table {table_name}")
except mysql.connector.Error as err:
log_message(f"Error checking/adding column {column} to table {table_name}: {err}")
# Создание индексов для таблицы marks
marks_indexes = [
'CREATE UNIQUE INDEX IF NOT EXISTS uniq_post_user ON marks (tg_post_id, tg_user_id)',
'CREATE INDEX IF NOT EXISTS idx_post_subscribers ON marks (tg_post_id)',
'CREATE INDEX IF NOT EXISTS idx_user_subscriptions ON marks (tg_user_id)',
'CREATE INDEX IF NOT EXISTS idx_created_at ON marks (created_at)',
'CREATE INDEX IF NOT EXISTS idx_post_created ON marks (tg_post_id, created_at)',
'CREATE INDEX IF NOT EXISTS idx_tg_post_id ON marks (tg_post_id)'
]
# Создание индексов для таблицы marks_evt
marks_evt_indexes = [
'CREATE UNIQUE INDEX IF NOT EXISTS uniq_event_user ON marks_evt (tg_event_id, tg_user_id)',
'CREATE INDEX IF NOT EXISTS idx_event_subscribers ON marks_evt (tg_event_id)',
'CREATE INDEX IF NOT EXISTS idx_user_event_subscriptions ON marks_evt (tg_user_id)',
'CREATE INDEX IF NOT EXISTS idx_marks_evt_created_at ON marks_evt (created_at)',
'CREATE INDEX IF NOT EXISTS idx_event_created ON marks_evt (tg_event_id, created_at)',
'CREATE INDEX IF NOT EXISTS idx_tg_event_id ON marks_evt (tg_event_id)'
]
# Создание индексов для таблицы events
events_indexes = [
'CREATE UNIQUE INDEX IF NOT EXISTS idx_events_id_event ON events (id_event)',
'CREATE INDEX IF NOT EXISTS idx_events_number ON events (number)',
'CREATE INDEX IF NOT EXISTS idx_events_is_visible ON events (is_visible)',
'CREATE INDEX IF NOT EXISTS idx_events_is_posted_tg ON events (is_posted_tg)',
]
# Создание индексов для таблицы interlinks
interlinks_indexes = [
'CREATE INDEX IF NOT EXISTS idx_interlinks_post_id ON interlinks (post_id)',
'CREATE INDEX IF NOT EXISTS idx_interlinks_event_id ON interlinks (event_id)',
'CREATE UNIQUE INDEX IF NOT EXISTS uniq_post_event ON interlinks (post_id, event_id)'
]
# Объединяем все индексы
all_indexes = marks_indexes + marks_evt_indexes + events_indexes + interlinks_indexes
for index_sql in all_indexes:
try:
cursor.execute(index_sql)
index_name = index_sql.split('IF NOT EXISTS')[1].split(' ON ')[0].strip()
log_message(f"Index created: {index_name}")
except mysql.connector.Error as err:
log_message(f"Error creating index: {err}")
conn.commit()
log_message("Database structure ensured successfully")
except mysql.connector.Error as err:
log_message(f"Database error: {err}")
raise
finally:
# Аккуратно закрываем соединение и курсор
try:
if cursor:
cursor.close()
except:
pass
try:
if conn and conn.is_connected():
conn.close()
except:
pass
if __name__ == "__main__":
ensure_database_structure()
+77
View File
@@ -0,0 +1,77 @@
import os
import mysql.connector
from mysql.connector import Error
from datetime import datetime, timedelta, timezone
from dotenv import load_dotenv
# Загрузка переменных окружения
load_dotenv()
MDB_HOST = os.getenv('MDB_HOST')
MDB_USER = os.getenv('MDB_USER')
MDB_PW = os.getenv('MDB_PW')
MDBASE = os.getenv('MDBASE')
def prefetch_all_events():
"""
Проверяет записи в таблице events и устанавливает флаг marked_to_publication
для записей, которые:
- имеют флаг is_visible = 1
- не имеют флага marked_to_publication = 1
- modified более часа назад от текущего времени (UTC)
"""
try:
# Подключение к базе данных
connection = mysql.connector.connect(
host=MDB_HOST,
user=MDB_USER,
password=MDB_PW,
database=MDBASE
)
# Установка временной зоны соединения в UTC
cursor_temp = connection.cursor()
cursor_temp.execute("SET time_zone = '+00:00'")
cursor_temp.close()
cursor = connection.cursor()
# Текущее время в UTC
current_time_utc = datetime.now(timezone.utc).replace(tzinfo=None)
one_hour_ago = current_time_utc - timedelta(hours=1)
# SQL запрос для обновления записей
update_query = """
UPDATE events
SET marked_to_publication = 1
WHERE is_visible = 1
AND marked_to_publication = 0
AND modified < %s
"""
# Выполнение запроса
cursor.execute(update_query, (one_hour_ago,))
updated_count = cursor.rowcount
# Фиксация изменений
connection.commit()
print(f"Обновлено записей: {updated_count}")
return updated_count
except Error as e:
print(f"Ошибка базы данных: {e}")
if 'connection' in locals() and connection.is_connected():
connection.rollback()
return 0
except Exception as e:
print(f"Общая ошибка: {e}")
return 0
finally:
if 'connection' in locals() and connection.is_connected():
cursor.close()
connection.close()
if __name__ == "__main__":
prefetch_all_events()
+329
View File
@@ -0,0 +1,329 @@
import os
import logging
import asyncio
import pymysql
import time
import html
from datetime import datetime, timezone, timedelta
from telegram import Bot, InlineKeyboardMarkup, InlineKeyboardButton
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')
DESC_PREFIX = os.getenv('DESC_PREFIX')
PZK_PREFIX = os.getenv('PZK_PREFIX')
LOG_FILE = os.getenv('LOG_FILE', 'evtg_publisher.log')
EVENT_POST_DELAY = int(os.getenv('EVENT_POST_DELAY', 0)) # Задержка в минутах
# Параметры длины сообщений
MAX_CAPTION_LENGTH = int(os.getenv('MAX_CAPTION_LENGTH', 1000))
MAX_TEXT_LENGTH = int(os.getenv('MAX_TEXT_LENGTH', 4000))
# Настройка логирования
logger = logging.getLogger('TG_EV_post')
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 format_tags(tags_str):
"""Форматирование тегов с добавлением #"""
if not tags_str:
return ""
tags = tags_str.split()
formatted_tags = []
for tag in tags:
if len(tag) > 1 and not tag.startswith('#'):
formatted_tags.append(f"#{tag}")
else:
formatted_tags.append(tag)
return " ".join(formatted_tags)
def prepare_text(text):
"""Подготовка текстовых полей с экранированием HTML-сущностей"""
if not text:
return ""
# Экранируем специальные символы HTML
text = html.escape(str(text))
return text
async def tg_post_event(bot, event_data):
"""Публикация одного события"""
try:
# Форматирование текста
tags = format_tags(event_data['tags'])
unit_name = prepare_text(event_data['unit_name'] or "")
name = prepare_text(event_data['name'] or "")
# Получаем и подготавливаем описание события
about_raw = event_data['about']
# Формируем базовый текст (теги + название площадки + название события)
caption_parts = []
if tags:
caption_parts.append(tags)
if unit_name:
caption_parts.append(unit_name)
if name:
caption_parts.append(f"<b>{name}</b>")
base_text = "\n".join(caption_parts)
base_text_length = len(base_text)
# Предварительный расчет строки ссылок (с запасом 20 символов для message_id)
number = event_data['number']
placeholder_message_id = '0' * 20 # Заполнитель для message_id
links_line_placeholder = f'🔎 <a href="{DESC_PREFIX}{number}/">Инфо</a> | 📌 <a href="{PZK_PREFIX}{number}/">Иду</a> | 🔜 <a href="https://t.me/{RESPONDER_BOT_NAME}?start=event_{placeholder_message_id}">Подписка</a>'
links_line_length = len(links_line_placeholder)
# Определяем максимальную длину в зависимости от типа сообщения
image_url = event_data['about_social_picture']
has_image = image_url and image_url.strip()
max_length = MAX_CAPTION_LENGTH if has_image else MAX_TEXT_LENGTH
# Вычисляем доступную длину для текста события
available_length = max_length - base_text_length - links_line_length - 3 # -3 для символов переноса строки
if available_length < 0:
available_length = 0
# Получаем обработанный текст с учетом доступной длины
about = get_event_text(about_raw, available_length) if about_raw else ""
# Формируем первоначальный текст
initial_text = f"{base_text}\n\n{about}" if about else base_text
# Логируем текст для отладки
logger.info(f"Текст для публикации события номер {event_data['number']}: {initial_text}")
# Публикация первоначального сообщения без ссылок
if has_image:
try:
message = await bot.send_photo(
chat_id=CHANNEL_ID,
photo=image_url,
caption=initial_text,
parse_mode="HTML",
disable_notification=True
)
except TelegramError as e:
logger.warning(f"Не удалось отправить изображение для события номер {event_data['number']}: {e}. Отправляем текстовое сообщение.")
message = await bot.send_message(
chat_id=CHANNEL_ID,
text=initial_text,
parse_mode="HTML",
disable_notification=True,
disable_web_page_preview=True # Отключаем предпросмотр ссылок
)
else:
message = await bot.send_message(
chat_id=CHANNEL_ID,
text=initial_text,
parse_mode="HTML",
disable_notification=True,
disable_web_page_preview=True # Отключаем предпросмотр ссылок
)
# Получаем ID сообщения для использования в ссылке подписки
message_id = message.message_id
# Формируем финальный текст с ссылками
links_line = f'🔎 <a href="{DESC_PREFIX}{number}/">Инфо</a> | 📌 <a href="{PZK_PREFIX}{number}/">Иду</a> | 🔜 <a href="https://t.me/{RESPONDER_BOT_NAME}?start=event_{message_id}">Подписка</a>'
final_text = f"{initial_text}\n\n{links_line}"
await asyncio.sleep(2)
# Редактируем сообщение, добавляя ссылки
if has_image:
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"Не удалось отредактировать подпись для события номер {event_data['number']}: {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 # Отключаем предпросмотр ссылок
)
return message_id # Возвращаем только message_id
except RetryAfter as e:
# Обработка ошибки FloodWait (429)
logger.error(f"Получена ошибка FloodWait (429) при публикации события номер {event_data['number']}: {e}")
logger.error(f"Необходимо подождать {e.retry_after} секунд перед следующей попыткой")
raise # Пробрасываем исключение выше для обработки на уровне цикла
except TelegramError as e:
logger.error(f"Ошибка Telegram при публикации события номер {event_data['number']}: {e}")
return None
except Exception as e:
logger.error(f"Неожиданная ошибка при публикации события номер {event_data['number']}: {e}")
return None
async def tg_post_event_by_id(id_event):
"""Публикация события по ID"""
logger.info(f"Запуск публикации для события ID {id_event}")
conn = None
try:
conn = pymysql.connect(
host=MDB_HOST,
user=MDB_USER,
password=MDB_PW,
database=MDBASE,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
# Вычисляем время, до которого должно быть изменено событие для публикации
modified_before = datetime.now(timezone.utc) - timedelta(minutes=EVENT_POST_DELAY)
with conn.cursor() as cursor:
cursor.execute("""
SELECT id_event, number, tags, unit_name, name, about, about_social_picture, tg_message_id
FROM events
WHERE id_event = %s
AND marked_to_publication = True
AND is_posted_tg = False
AND modified <= %s
""", (id_event, modified_before))
event = cursor.fetchone()
if not event:
logger.info(f"Событие ID {id_event} не найдено, уже опубликовано или изменено менее чем {EVENT_POST_DELAY} минут назад")
return
old_message_id = event.get('tg_message_id') # Сохраняем старый ID сообщения
bot = Bot(token=BOT_TOKEN)
tg_message_id = await tg_post_event(bot, event) # Получаем новый message_id
if tg_message_id:
# Используем UTC время для записи в базу данных
current_time_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
# Обновляем подписки если был предыдущий message_id
if old_message_id:
cursor.execute("""
UPDATE marks_evt
SET tg_event_id = %s
WHERE tg_event_id = %s
""", (tg_message_id, old_message_id))
logger.info(f"Обновлены подписки для события {id_event}: {cursor.rowcount} записей")
cursor.execute("""
UPDATE events
SET is_posted_tg = True,
tg_message_id = %s,
tg_posted_date = %s
WHERE id_event = %s
""", (tg_message_id, current_time_utc, id_event))
conn.commit()
logger.info(f"Событие номер {event['number']} успешно опубликовано")
else:
logger.error(f"Не удалось опубликовать событие ID {id_event}")
except RetryAfter as e:
raise e
except Exception as e:
logger.error(f"Ошибка при публикации: {e}")
finally:
if conn:
conn.close()
async def tg_post_all_events():
"""Публикация всех неопубликованных событий (максимум 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
)
# Вычисляем время, до которого должно быть изменено событие для публикации
modified_before = datetime.now(timezone.utc) - timedelta(minutes=EVENT_POST_DELAY)
with conn.cursor() as cursor:
cursor.execute("""
SELECT id_event
FROM events
WHERE marked_to_publication = True
AND is_posted_tg = False
AND modified <= %s
LIMIT 5
""", (modified_before,))
events = cursor.fetchall()
event_count = 0
for event in events:
try:
await tg_post_event_by_id(event['id_event'])
event_count += 1
# Задержка между публикациями разных событий
if event_count < len(events):
await asyncio.sleep(2)
except RetryAfter as e:
logger.error(f"Прерываем публикацию из-за ошибки FloodWait. Ожидание: {e.retry_after} секунд")
break
except Exception as e:
logger.error(f"Ошибка при публикации события ID {event['id_event']}: {e}")
# Продолжаем публикацию следующих событий, несмотря на ошибку
logger.info(f"Опубликовано событий в этом запуске: {event_count}")
except Exception as e:
logger.error(f"Ошибка при публикации: {e}")
finally:
if conn:
conn.close()
logger.info("Завершение работы скрипта")
if __name__ == "__main__":
asyncio.run(tg_post_all_events())
+156
View File
@@ -0,0 +1,156 @@
import re
def truncate_text(text, max_length):
"""Обрезает текст до максимальной длины с учетом границ слов"""
if text is None or not text:
return ""
if len(text) <= max_length:
return text
# Ищем последний пробел перед максимальной длиной
truncated = text[:max_length]
last_space = truncated.rfind(' ')
if last_space > 0:
return truncated[:last_space] + "..."
return truncated[:max_length-3] + "..."
def safe_text(text):
"""Обеспечивает безопасное использование текста (заменяет None на пустую строку)"""
return text if text is not None else ""
def replace_vk_links(text):
"""
Заменяет ссылки на сообщества ВК в форматах:
[club123|Название сообщества] -> Название сообщества
[id456|Имя пользователя] -> Имя пользователя
"""
if not text:
return text
# Регулярное выражение для поиска ссылок формата [типID|Текст]
pattern = r'\[(?:club|id)\d+\|([^\]]+)\]'
# Заменяем все вхождения на текст внутри ссылки
return re.sub(pattern, r'\1', text)
def remove_html_tags(text):
"""Удаляет HTML-теги с учетом специальной обработки <p> и <a>"""
if not text:
return text
# Заменяем <p> и </p> на переносы строк
text = re.sub(r'</?p\s*/?>', '\n', text, flags=re.IGNORECASE)
# Обрабатываем ссылки <a href="URL">Текст</a> -> Текст (URL)
def replace_a_tag(match):
url = match.group(1)
link_text = match.group(2)
return f"{link_text} ({url})"
text = re.sub(
r'<a\s+href="([^"]*)"[^>]*>(.*?)</a>',
replace_a_tag,
text,
flags=re.IGNORECASE | re.DOTALL
)
# Удаляем все оставшиеся HTML-теги
text = re.sub(r'<[^>]+>', '', text)
# Схлопываем множественные переносы строк
text = re.sub(r'\n+', '\n', text)
return text.strip()
def get_event_text(text, limit=950):
"""
Обрабатывает текст: удаляет HTML-теги и VK-ссылки, обрезает до указанной длины
:param text: исходный текст
:param limit: максимальная длина текста (по умолчанию 950)
:return: обработанный текст
"""
if not text:
return ""
# Удаляем HTML-теги
cleaned_text = remove_html_tags(text)
# Обрабатываем VK-ссылки
cleaned_text = replace_vk_links(cleaned_text)
# Обрезаем текст до указанной длины
return truncate_text(cleaned_text, limit)
def get_post_text(post_data, max_caption_length=1000, max_text_length=4000):
"""
Форматирует пост для публикации в Telegram
Возвращает словарь с элементами:
{
'base_text': основной текст поста,
'image_url': URL изображения (или None),
'vk_link': HTML-ссылка на оригинал в ВК,
'caption': подпись для изображения (с учетом ограничений длины),
'text_message': текстовое сообщение (с учетом ограничений длины),
'has_poll': есть ли опрос,
'is_event': является ли пост событием,
'poll_question': вопрос опроса,
'poll_options': варианты ответов опроса,
'poll_multiple': разрешен ли множественный выбор
}
"""
# Извлекаем данные из кортежа
vk_post_id = post_data[0]
text_content = safe_text(post_data[1])
image_url = safe_text(post_data[2])
vk_post_url = safe_text(post_data[3])
poll_question = safe_text(post_data[4])
poll_options = safe_text(post_data[5])
poll_multiple = post_data[6]
is_event = post_data[7]
has_poll = post_data[8] # Извлекаем значение is_poll
# Обрабатываем текст: заменяем ссылки на сообщества ВК
text_content = replace_vk_links(text_content)
# Базовый текст - только содержимое поста
base_text = text_content
vk_link = f'<a href="{vk_post_url}">Оригинал в ВК</a>' if vk_post_url else ""
# Формируем подпись для изображения
caption = None
if image_url:
full_caption = f"{base_text}\n\n{vk_link}" if vk_link else base_text
if len(full_caption) > max_caption_length:
available_length = max_caption_length - (len(vk_link) + 10) if vk_link else 0
truncated_text = truncate_text(base_text, available_length)
caption = f"{truncated_text}\n\n{vk_link}" if vk_link else truncated_text
else:
caption = full_caption
# Формируем текстовое сообщение (для постов без изображения)
text_message = None
if not image_url:
full_message = f"{base_text}\n\n{vk_link}" if vk_link else base_text
if len(full_message) > max_text_length:
available_length = max_text_length - (len(vk_link) + 10) if vk_link else 0
truncated_text = truncate_text(base_text, available_length)
text_message = f"{truncated_text}\n\n{vk_link}" if vk_link else truncated_text
else:
text_message = full_message
return {
'vk_post_id': vk_post_id,
'base_text': base_text,
'image_url': image_url if image_url else None,
'vk_link': vk_link,
'caption': caption,
'text_message': text_message,
'has_poll': has_poll, # Используем прямое значение из кортежа
'is_event': is_event,
'poll_question': poll_question,
'poll_options': poll_options,
'poll_multiple': poll_multiple
}
+2
View File
@@ -0,0 +1,2 @@
import secrets
print(secrets.token_hex(32))