Upload files to "/"
This commit is contained in:
+281
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user