From f7071a017dc2a7d52817cfe548a83b62fe712e9d Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 25 Sep 2025 20:42:48 +0300 Subject: [PATCH] Upload files to "/" --- ensure_db.py | 281 +++++++++++++++++++++++++++++++++++++++++ evt_prefetch.py | 77 ++++++++++++ evtg_publish.py | 329 ++++++++++++++++++++++++++++++++++++++++++++++++ formatter.py | 156 +++++++++++++++++++++++ genkey.py | 2 + 5 files changed, 845 insertions(+) create mode 100644 ensure_db.py create mode 100644 evt_prefetch.py create mode 100644 evtg_publish.py create mode 100644 formatter.py create mode 100644 genkey.py diff --git a/ensure_db.py b/ensure_db.py new file mode 100644 index 0000000..fcd9d6a --- /dev/null +++ b/ensure_db.py @@ -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() diff --git a/evt_prefetch.py b/evt_prefetch.py new file mode 100644 index 0000000..5e2a300 --- /dev/null +++ b/evt_prefetch.py @@ -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() diff --git a/evtg_publish.py b/evtg_publish.py new file mode 100644 index 0000000..1127dcb --- /dev/null +++ b/evtg_publish.py @@ -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"{name}") + 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'🔎 Инфо | 📌 Иду | 🔜 Подписка' + 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'🔎 Инфо | 📌 Иду | 🔜 Подписка' + 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()) diff --git a/formatter.py b/formatter.py new file mode 100644 index 0000000..7af08b7 --- /dev/null +++ b/formatter.py @@ -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-теги с учетом специальной обработки

и """ + if not text: + return text + + # Заменяем

и

на переносы строк + text = re.sub(r'', '\n', text, flags=re.IGNORECASE) + + # Обрабатываем ссылки Текст -> Текст (URL) + def replace_a_tag(match): + url = match.group(1) + link_text = match.group(2) + return f"{link_text} ({url})" + + text = re.sub( + r']*>(.*?)', + 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'Оригинал в ВК' 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 + } diff --git a/genkey.py b/genkey.py new file mode 100644 index 0000000..9c41e7c --- /dev/null +++ b/genkey.py @@ -0,0 +1,2 @@ +import secrets +print(secrets.token_hex(32))