From 8644941092031c8549b5f16eb0484d23f93a5824 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 25 Sep 2025 20:41:08 +0300 Subject: [PATCH] Upload files to "/" --- .env | 35 ++ alter_about_field.py | 45 ++ db_backup.sh | 1 + db_edit.py | 1355 ++++++++++++++++++++++++++++++++++++++++ db_update_shortname.py | 244 ++++++++ 5 files changed, 1680 insertions(+) create mode 100644 .env create mode 100644 alter_about_field.py create mode 100644 db_backup.sh create mode 100644 db_edit.py create mode 100644 db_update_shortname.py diff --git a/.env b/.env new file mode 100644 index 0000000..4ed2426 --- /dev/null +++ b/.env @@ -0,0 +1,35 @@ +# Общие настройки +SECRET_KEY=24af5f41dba9d8cc0dbc1841660da0b14d7df86ab5e0ed973cdd3bc2dbac23f6 +LOG_FILE = /var/log/zilantposter.log + +# логин и пароль по умолчанию для первого запуска веб-контроллера +DEFNM=admin +DEFPW=Z1l@ntAdm1n23 + +# VK access +VK_GROUP_ID = -3342146 +VK_ACCESS_TOKEN = 'e3732a7ee3732a7ee3732a7ea7e044a75fee373e3732a7e8bf4e48a52d012e946b952a8' +VK_API_VERSION = '5.131' + +# MySQL database +MDBASE = 'zilant_posts' +MDB_HOST = 'localhost' +MDB_USER = 'zilantposter' +MDB_PW = 'Z1l@ntp08t3r' + +# Poster bot +BOT_TOKEN = "8301828103:AAHDuBkw7EUkmMvv5TPpVJaLeeBLu-EB21U" + +# Poster channel +CHANNEL_ID = "@posto1638" +# DB_NAME = "vk_posts.db" +MAX_CAPTION_LENGTH = 1000 +MAX_TEXT_LENGTH = 4000 + +# Button scripts +RESCAN_SCRIPT = "vk_load.py" +PUBLISH_SCRIPT = "dummy.py" + +# Event status +IS_EVENT = false +AUTO_PUBLISH = false diff --git a/alter_about_field.py b/alter_about_field.py new file mode 100644 index 0000000..5ca0546 --- /dev/null +++ b/alter_about_field.py @@ -0,0 +1,45 @@ +import os +import mysql.connector +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация базы данных из переменных окружения +DB_CONFIG = { + 'host': os.getenv('MDB_HOST'), + 'user': os.getenv('MDB_USER'), + 'password': os.getenv('MDB_PW'), + 'database': os.getenv('MDBASE') +} + +def alter_column_type(): + try: + # Устанавливаем соединение с базой данных + connection = mysql.connector.connect(**DB_CONFIG) + cursor = connection.cursor() + + # Выполняем ALTER TABLE запрос + alter_query = """ + ALTER TABLE events + MODIFY COLUMN about TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + """ + cursor.execute(alter_query) + + # Подтверждаем изменения + connection.commit() + + print("Тип поля 'about' успешно изменен на TEXT") + + except mysql.connector.Error as error: + print(f"Ошибка при изменении типа поля: {error}") + if connection: + connection.rollback() + finally: + if cursor: + cursor.close() + if connection: + connection.close() + +if __name__ == "__main__": + alter_column_type() diff --git a/db_backup.sh b/db_backup.sh new file mode 100644 index 0000000..55c7b94 --- /dev/null +++ b/db_backup.sh @@ -0,0 +1 @@ +mysqldump -utestuser -pT38tU83r69 testposts > test_posts_backup.sql diff --git a/db_edit.py b/db_edit.py new file mode 100644 index 0000000..aeca913 --- /dev/null +++ b/db_edit.py @@ -0,0 +1,1355 @@ +import pymysql +import json +import re +import requests +import os +import subprocess +import sys +from dotenv import load_dotenv +from flask import Flask, render_template, request, jsonify, Response, redirect, url_for, g, flash, Blueprint +from io import BytesIO +from PIL import Image +import threading +from datetime import datetime +from werkzeug.middleware.proxy_fix import ProxyFix +from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user +from werkzeug.security import generate_password_hash, check_password_hash +import time +import errno + +# Импортируем необходимые функции из модулей +from vk_load import vk_load_10 +from ensure_db import ensure_database_structure +from tg_publish import publish_to_tg_all, publish_to_tg +from db_update_shortname import ai_shortname_all, generate_ai_shortname +from evtg_publish import tg_post_event_by_id, tg_post_all_events +from zk_load import load_json_all + +# Загружаем настройки из .env +load_dotenv() + +# Создаем Flask приложение +app = Flask(__name__) +app.config['PREFIX'] = os.getenv('WEBCTRL_PREFIX', '/zilant') # Префикс для всех URL +app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') + +# Middleware для корректировки URL в обратных прокси +app.wsgi_app = ProxyFix( + app.wsgi_app, + x_for=1, x_proto=1, x_host=1, x_prefix=1 +) + +# Инициализация Flask-Login +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'zilant.login' + +# Создаем Blueprint с префиксом +bp = Blueprint('zilant', __name__, url_prefix=app.config['PREFIX']) + +# Функция для получения IP-адреса клиента +def get_client_ip(): + # Проверяем заголовки прокси + if 'X-Forwarded-For' in request.headers: + # Берем первый IP из списка + return request.headers['X-Forwarded-For'].split(',')[0].strip() + return request.remote_addr + +# Функция для логирования событий с обработкой блокировки файла +def log_event(message): + log_file = os.getenv('LOG_FILE') + if not log_file: + return + + timestamp = datetime.now().strftime('[%Y-%m-%d %H:%M:%S]') + full_message = f"{timestamp} [DB_editor] {message}\n" + + max_attempts = 5 + delay = 0.2 # Задержка между попытками в секундах + + for attempt in range(max_attempts): + try: + with open(log_file, 'a', encoding='utf-8') as f: + f.write(full_message) + break # Успешная запись, выходим из цикла + except IOError as e: + if e.errno == errno.EAGAIN or getattr(e, 'winerror', None) == 33: # Проверка блокировки (Linux/Windows) + if attempt < max_attempts - 1: + time.sleep(delay) # Ждем перед повторной попытку + continue + # Для других ошибок выводим сообщение + print(f"Ошибка записи в лог: {str(e)}") + break + except Exception as e: + print(f"Критическая ошибка при записи в лог: {str(e)}") + break + +# Функция для получения идентификатора поста +def get_post_identifier(post): + if post and post.get('vk_post_id') and post['vk_post_id'] != 0: + return f"VK_ID: {post['vk_post_id']}" + else: + return f"ID: {post['id']}" + +# Модель пользователя +class User(UserMixin): + def __init__(self, id, username): + self.id = id + self.username = username + +# Вспомогательный класс для работы с MariaDB +class DatabaseManager: + def __init__(self): + self.conn = pymysql.connect( + host=os.getenv('MDB_HOST'), + user=os.getenv('MDB_USER'), + password=os.getenv('MDB_PW'), + database=os.getenv('MDBASE'), + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + + def get_posts(self): + with self.conn.cursor() as cursor: + cursor.execute(""" + SELECT id, vk_post_id, shortname, text, published_at, + marked_for_publication, published_in_tg, is_event, is_poll, action_number + FROM posts + ORDER BY published_at DESC + """) + return cursor.fetchall() + + def get_post(self, post_id): + with self.conn.cursor() as cursor: + cursor.execute("SELECT * FROM posts WHERE id = %s", (post_id,)) + return cursor.fetchone() + + def get_events(self): + with self.conn.cursor() as cursor: + cursor.execute(""" + SELECT id, id_event, number, unit_name, name, about, about_social_picture, + modified, added, tags, is_canceled, accepted, denied, is_visible, + marked_to_publication, is_posted_tg, tg_message_id, tg_posted_date, + announcement_link + FROM events + ORDER BY added DESC + """) + return cursor.fetchall() + + def get_event(self, event_id): + with self.conn.cursor() as cursor: + cursor.execute("SELECT * FROM events WHERE id = %s", (event_id,)) + return cursor.fetchone() + + def toggle_event_publication(self, event_id): + with self.conn.cursor() as cursor: + cursor.execute(""" + UPDATE events + SET marked_to_publication = NOT marked_to_publication + WHERE id = %s + """, (event_id,)) + self.conn.commit() + + cursor.execute("SELECT marked_to_publication FROM events WHERE id = %s", (event_id,)) + return cursor.fetchone()['marked_to_publication'] + + def add_event(self, data): + # Функции для очистки данных + def clean_date(value): + return None if value == '' else value + + def clean_int(value, default=0): + if value is None or value == '': + return default + try: + return int(value) + except (TypeError, ValueError): + return default + + def clean_bool(value): + if value == '': + return 0 + return 1 if value else 0 + + with self.conn.cursor() as cursor: + query = """ + INSERT INTO events ( + id_event, number, unit_name, name, about, about_social_picture, + modified, added, tags, is_canceled, accepted, denied, is_visible, + marked_to_publication, is_posted_tg, tg_message_id, tg_posted_date, + announcement_link + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """ + values = ( + clean_int(data.get('id_event'), 0), + clean_int(data.get('number'), 0), + data.get('unit_name', ''), + data.get('name', ''), + data.get('about', ''), + data.get('about_social_picture', ''), + clean_date(data.get('modified')), + clean_date(data.get('added')), + data.get('tags', ''), + clean_bool(data.get('is_canceled')), + clean_bool(data.get('accepted')), + clean_bool(data.get('denied')), + clean_bool(data.get('is_visible')), + clean_bool(data.get('marked_to_publication')), + clean_bool(data.get('is_posted_tg')), + clean_int(data.get('tg_message_id'), None), + clean_date(data.get('tg_posted_date')), + clean_int(data.get('announcement_link'), None) + ) + cursor.execute(query, values) + self.conn.commit() + return cursor.lastrowid + + def update_event(self, event_id, data): + # Функции для очистки данных + def clean_date(value): + return None if value == '' else value + + def clean_int(value, default=0): + if value is None or value == '': + return default + try: + return int(value) + except (TypeError, ValueError): + return default + + def clean_bool(value): + if value == '': + return 0 + return 1 if value else 0 + + with self.conn.cursor() as cursor: + query = """ + UPDATE events SET + id_event = %s, number = %s, unit_name = %s, name = %s, about = %s, + about_social_picture = %s, modified = %s, added = %s, tags = %s, + is_canceled = %s, accepted = %s, denied = %s, is_visible = %s, + marked_to_publication = %s, is_posted_tg = %s, tg_message_id = %s, + tg_posted_date = %s, announcement_link = %s + WHERE id = %s + """ + values = ( + clean_int(data.get('id_event'), 0), + clean_int(data.get('number'), 0), + data.get('unit_name', ''), + data.get('name', ''), + data.get('about', ''), + data.get('about_social_picture', ''), + clean_date(data.get('modified')), + clean_date(data.get('added')), + data.get('tags', ''), + clean_bool(data.get('is_canceled')), + clean_bool(data.get('accepted')), + clean_bool(data.get('denied')), + clean_bool(data.get('is_visible')), + clean_bool(data.get('marked_to_publication')), + clean_bool(data.get('is_posted_tg')), + clean_int(data.get('tg_message_id'), None), + clean_date(data.get('tg_posted_date')), + clean_int(data.get('announcement_link'), None), + event_id + ) + cursor.execute(query, values) + self.conn.commit() + return True # Всегда возвращаем True при успешном выполнении запроса + + def delete_event(self, event_id): + with self.conn.cursor() as cursor: + cursor.execute("DELETE FROM events WHERE id = %s", (event_id,)) + self.conn.commit() + return cursor.rowcount > 0 + + def get_marks_by_tg_message_id(self, tg_message_id): + """Получение подписок по ID сообщения в Telegram""" + with self.conn.cursor() as cursor: + cursor.execute(""" + SELECT tg_user_name, tg_user_id, created_at + FROM marks + WHERE tg_post_id = %s + ORDER BY created_at DESC + """, (tg_message_id,)) + return cursor.fetchall() + + def toggle_publication(self, post_id): + with self.conn.cursor() as cursor: + cursor.execute(""" + UPDATE posts + SET marked_for_publication = NOT marked_for_publication + WHERE id = %s + """, (post_id,)) + self.conn.commit() + + cursor.execute("SELECT marked_for_publication FROM posts WHERE id = %s", (post_id,)) + return cursor.fetchone()['marked_for_publication'] + + def add_post(self, data): + # Функции для очистки данных + def clean_date(value): + return None if value == '' else value + + def clean_int(value): + if value == '': + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + with self.conn.cursor() as cursor: + query = """ + INSERT INTO posts ( + vk_post_id, shortname, text, image_url, vk_post_url, published_at, + is_event, is_poll, poll_question, poll_options, poll_multiple, poll_end_date, + marked_for_publication, published_in_tg, tg_publication_date, tg_message_id, + tg_poll_id, tg_markpost_id, tg_poll_results, action_number + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """ + values = ( + clean_int(data['vk_post_id']), + data.get('shortname', ''), + data['text'], + data['image_url'], + data['vk_post_url'], + clean_date(data.get('published_at')), + data.get('is_event', 0), + data.get('is_poll', 0), + data.get('poll_question', ''), + data.get('poll_options', ''), + data.get('poll_multiple', 0), + clean_date(data.get('poll_end_date')), + data.get('marked_for_publication', 0), + data.get('published_in_tg', 0), + clean_date(data.get('tg_publication_date')), + clean_int(data.get('tg_message_id', '')), + clean_int(data.get('tg_poll_id', '')), + clean_int(data.get('tg_markpost_id', '')), + data.get('tg_poll_results', ''), + clean_int(data.get('action_number', '')) + ) + cursor.execute(query, values) + self.conn.commit() + return cursor.lastrowid + + def update_post(self, post_id, data): + # Функции для очистки данных + def clean_date(value): + return None if value == '' else value + + def clean_int(value): + if value == '': + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + with self.conn.cursor() as cursor: + query = """ + UPDATE posts SET + vk_post_id = %s, shortname = %s, text = %s, image_url = %s, vk_post_url = %s, + published_at = %s, is_event = %s, is_poll = %s, poll_question = %s, poll_options = %s, + poll_multiple = %s, poll_end_date = %s, marked_for_publication = %s, + published_in_tg = %s, tg_publication_date = %s, tg_message_id = %s, + tg_poll_id = %s, tg_markpost_id = %s, tg_poll_results = %s, action_number = %s + WHERE id = %s + """ + values = ( + clean_int(data['vk_post_id']), + data.get('shortname', ''), + data['text'], + data['image_url'], + data['vk_post_url'], + clean_date(data.get('published_at')), + data.get('is_event', 0), + data.get('is_poll', 0), + data.get('poll_question', ''), + data.get('poll_options', ''), + data.get('poll_multiple', 0), + clean_date(data.get('poll_end_date')), + data.get('marked_for_publication', 0), + data.get('published_in_tg', 0), + clean_date(data.get('tg_publication_date')), + clean_int(data.get('tg_message_id', '')), + clean_int(data.get('tg_poll_id', '')), + clean_int(data.get('tg_markpost_id', '')), + data.get('tg_poll_results', ''), + clean_int(data.get('action_number', '')), + post_id + ) + cursor.execute(query, values) + self.conn.commit() + return cursor.rowcount > 0 + + def delete_post(self, post_id): + with self.conn.cursor() as cursor: + cursor.execute("DELETE FROM posts WHERE id = %s", (post_id,)) + self.conn.commit() + return cursor.rowcount > 0 + + # Методы для работы с пользователями + def get_user_by_username(self, username): + with self.conn.cursor() as cursor: + cursor.execute("SELECT * FROM users WHERE username = %s", (username,)) + return cursor.fetchone() + + def get_user_by_id(self, user_id): + with self.conn.cursor() as cursor: + cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) + return cursor.fetchone() + + def create_user(self, username, password): + with self.conn.cursor() as cursor: + hashed_password = generate_password_hash(password) + cursor.execute( + "INSERT INTO users (username, password) VALUES (%s, %s)", + (username, hashed_password) + ) + self.conn.commit() + return cursor.lastrowid + + def update_user_password(self, user_id, new_password): + with self.conn.cursor() as cursor: + hashed_password = generate_password_hash(new_password) + cursor.execute( + "UPDATE users SET password = %s WHERE id = %s", + (hashed_password, user_id) + ) + self.conn.commit() + return cursor.rowcount > 0 + + # Новые методы для работы со связями + def check_interlink(self, post_id, event_id): + """Проверяет существование связи между постом и событием""" + with self.conn.cursor() as cursor: + cursor.execute( + "SELECT COUNT(*) as count FROM interlinks WHERE post_id = %s AND event_id = %s", + (post_id, event_id) + ) + result = cursor.fetchone() + return result['count'] > 0 + + def create_interlink(self, post_id, event_id): + """Создает связь между постом и событием""" + with self.conn.cursor() as cursor: + cursor.execute( + "INSERT INTO interlinks (post_id, event_id, created_at) VALUES (%s, %s, UTC_TIMESTAMP())", + (post_id, event_id) + ) + self.conn.commit() + return cursor.lastrowid + + def delete_interlink(self, post_id, event_id): + """Удаляет связь между постом и событием""" + with self.conn.cursor() as cursor: + cursor.execute( + "DELETE FROM interlinks WHERE post_id = %s AND event_id = %s", + (post_id, event_id) + ) + self.conn.commit() + return cursor.rowcount > 0 + + def get_linked_events(self, post_id): + """Получает все события, связанные с постом""" + with self.conn.cursor() as cursor: + cursor.execute(""" + SELECT e.* FROM events e + JOIN interlinks i ON e.id = i.event_id + WHERE i.post_id = %s + ORDER BY i.created_at DESC + """, (post_id,)) + return cursor.fetchall() + + def get_linked_posts(self, event_id): + """Получает все посты, связанные с событием""" + with self.conn.cursor() as cursor: + cursor.execute(""" + SELECT p.* FROM posts p + JOIN interlinks i ON p.id = i.post_id + WHERE i.event_id = %s + ORDER BY i.created_at DESC + """, (event_id,)) + return cursor.fetchall() + + def get_all_interlinks(self): + """Получает все связи между постами и событиями""" + with self.conn.cursor() as cursor: + cursor.execute(""" + SELECT i.*, p.shortname as post_name, e.name as event_name + FROM interlinks i + LEFT JOIN posts p ON i.post_id = p.id + LEFT JOIN events e ON i.event_id = e.id + ORDER BY i.created_at DESC + """) + return cursor.fetchall() + + def close(self): + self.conn.close() + +# Инициализация БД +def get_db(): + return DatabaseManager() + +# Загрузчик пользователя для Flask-Login +@login_manager.user_loader +def load_user(user_id): + db = get_db() + try: + user_data = db.get_user_by_id(user_id) + if user_data: + return User(user_data['id'], user_data['username']) + return None + finally: + db.close() + +# Middleware для добавления префикса в g +@bp.before_request +def before_request(): + g.prefix = app.config['PREFIX'] + +# Маршруты Blueprint +@bp.route('/') +@login_required +def index(): + return render_template('index.html', + rescan_script=os.getenv('RESCAN_SCRIPT'), + publish_script=os.getenv('PUBLISH_SCRIPT')) + +@bp.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('zilant.index')) + + client_ip = get_client_ip() + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + remember = bool(request.form.get('remember')) + + db = get_db() + try: + user_data = db.get_user_by_username(username) + if user_data and check_password_hash(user_data['password'], password): + user = User(user_data['id'], user_data['username']) + login_user(user, remember=remember) + log_event(f"Пользователь {username} вошел в систему с IP {client_ip}") # Логирование входа + return redirect(url_for('zilant.index')) + else: + # Логируем неудачную попытку входа + log_event(f"Неудачная попытка входа: логин '{username}', IP {client_ip}") + flash('Неверное имя пользователя или пароль', 'danger') + finally: + db.close() + + return render_template('login.html') + +@bp.route('/change_password', methods=['GET', 'POST']) +@login_required +def change_password(): + client_ip = get_client_ip() + + if request.method == 'POST': + current_password = request.form.get('current_password') + new_password = request.form.get('new_password') + confirm_password = request.form.get('confirm_password') + + if new_password != confirm_password: + flash('Новый пароль и подтверждение не совпадают', 'danger') + return redirect(url_for('zilant.change_password')) + + db = get_db() + try: + user_data = db.get_user_by_id(current_user.id) + if not user_data: + flash('Пользователь не найден', 'danger') + return redirect(url_for('zilant.change_password')) + + if not check_password_hash(user_data['password'], current_password): + flash('Текущий пароль неверен', 'danger') + return redirect(url_for('zilant.change_password')) + + if db.update_user_password(current_user.id, new_password): + flash('Пароль успешно изменен', 'success') + log_event(f"Пользователь {current_user.username} изменил пароль с IP {client_ip}") + return redirect(url_for('zilant.index')) + else: + flash('Ошибка при изменении пароля', 'danger') + except Exception as e: + flash(f'Ошибка: {str(e)}', 'danger') + finally: + db.close() + + return render_template('change_password.html') + +@bp.route('/logout') +@login_required +def logout(): + username = current_user.username + client_ip = get_client_ip() + logout_user() + log_event(f"Пользователь {username} вышел из системы с IP {client_ip}") + return redirect(url_for('zilant.login')) + +@bp.route('/api/posts') +@login_required +def api_posts(): + db = get_db() + try: + posts = db.get_posts() + posts_list = [] + for post in posts: + posts_list.append({ + 'id': post['id'], + 'vk_post_id': post['vk_post_id'], + 'shortname': post['shortname'], + 'text_preview': post['text'][:50] + '...' if post['text'] else '', + 'published_at': post['published_at'], + 'marked_for_publication': bool(post['marked_for_publication']), + 'published_in_tg': bool(post['published_in_tg']), + 'is_event': bool(post['is_event']), + 'is_poll': bool(post['is_poll']), + 'action_number': post['action_number'] + }) + return jsonify(posts_list) + finally: + db.close() + +@bp.route('/api/events') +@login_required +def api_events(): + db = get_db() + try: + events = db.get_events() + events_list = [] + for event in events: + events_list.append({ + 'id': event['id'], + 'id_event': event['id_event'], + 'number': event['number'], + 'unit_name': event['unit_name'], + 'name': event['name'], + 'about': event['about'], + 'about_social_picture': event['about_social_picture'], + 'modified': event['modified'], + 'added': event['added'], + 'tags': event['tags'], + 'is_canceled': bool(event['is_canceled']), + 'accepted': bool(event['accepted']), + 'denied': bool(event['denied']), + 'is_visible': bool(event['is_visible']), + 'marked_to_publication': bool(event['marked_to_publication']), + 'is_posted_tg': bool(event['is_posted_tg']), + 'tg_message_id': event['tg_message_id'], + 'tg_posted_date': event['tg_posted_date'], + 'announcement_link': event['announcement_link'] + }) + return jsonify(events_list) + finally: + db.close() + +# Добавляем новые API endpoints для работы с событиями +@bp.route('/api/events//toggle_publication', methods=['POST']) +@login_required +def api_toggle_event_publication(event_id): + db = get_db() + try: + new_value = db.toggle_event_publication(event_id) + event = db.get_event(event_id) + client_ip = get_client_ip() + log_event(f"Изменен статус публикации события ID: {event_id}, Новое значение: {'Да' if new_value else 'Нет'}, IP {client_ip}") + return jsonify({ + 'success': True, + 'new_value': new_value + }) + finally: + db.close() + +@bp.route('/api/events', methods=['POST']) +@login_required +def api_add_event(): + data = request.json + db = get_db() + try: + event_id = db.add_event(data) + event = db.get_event(event_id) + client_ip = get_client_ip() + log_event(f"Добавлено новое событие: {event['name']}, ID: {event_id}, IP {client_ip}") + return jsonify({ + 'success': True, + 'event_id': event_id + }), 201 + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/events/', methods=['PUT']) +@login_required +def api_update_event(event_id): + data = request.json + db = get_db() + try: + if not db.get_event(event_id): + return jsonify({'error': 'Event not found'}), 404 + + db.update_event(event_id, data) + + # Определяем тип действия (сохранение или отмена) + action = "сохранение" if request.json.get('save_action') != "cancel" else "отмена редактирования" + event = db.get_event(event_id) + client_ip = get_client_ip() + log_event(f"Событие обновлено: {event['name']}, ID: {event_id}, Действие: {action}, IP {client_ip}") + + return jsonify({'success': True, 'event_id': event_id}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/events/', methods=['DELETE']) +@login_required +def api_delete_event(event_id): + db = get_db() + try: + event = db.get_event(event_id) + if not event: + return jsonify({'error': 'Event not found'}), 404 + + success = db.delete_event(event_id) + if success: + client_ip = get_client_ip() + log_event(f"Удалено событие: {event['name']}, ID: {event_id}, IP {client_ip}") + return jsonify({'success': success}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/posts/') +@login_required +def api_post_details(post_id): + db = get_db() + try: + post = db.get_post(post_id) + if not post: + return jsonify({'error': 'Post not found'}), 404 + + # Получаем подписки, если есть tg_message_id + marks = [] + if post.get('tg_message_id'): + marks = db.get_marks_by_tg_message_id(post['tg_message_id']) + + # Конвертируем булевы значения + for key in post: + if isinstance(post[key], int) and key in [ + 'is_event', 'is_poll', 'poll_multiple', 'marked_for_publication', 'published_in_tg' + ]: + post[key] = bool(post[key]) + + # Добавляем подписки к данным поста + post['marks'] = marks + + return jsonify(post) + finally: + db.close() + +@bp.route('/api/events/') +@login_required +def api_event_details(event_id): + db = get_db() + try: + event = db.get_event(event_id) + if not event: + return jsonify({'error': 'Event not found'}), 404 + + # Конвертируем булевы значения + for key in event: + if isinstance(event[key], int) and key in [ + 'is_canceled', 'accepted', 'denied', 'is_visible', + 'marked_to_publication', 'is_posted_tg' + ]: + event[key] = bool(event[key]) + + return jsonify(event) + finally: + db.close() + +@bp.route('/api/posts//toggle_publication', methods=['POST']) +@login_required +def api_toggle_publication(post_id): + db = get_db() + try: + new_value = db.toggle_publication(post_id) + post = db.get_post(post_id) + client_ip = get_client_ip() + identifier = get_post_identifier(post) + log_event(f"Изменен статус публикации: {identifier}, Новое значение: {'Да' if new_value else 'Нет'}, IP {client_ip}") + return jsonify({ + 'success': True, + 'new_value': new_value + }) + finally: + db.close() + +@bp.route('/api/posts', methods=['POST']) +@login_required +def api_add_post(): + data = request.json + db = get_db() + try: + if not data.get('vk_post_id'): + return jsonify({'error': 'VK ID is required'}), 400 + + post_id = db.add_post(data) + post = db.get_post(post_id) + client_ip = get_client_ip() + identifier = get_post_identifier(post) + log_event(f"Добавлен новый пост: {identifier}, Дата: {data.get('published_at', 'не указана')}, IP {client_ip}") + return jsonify({ + 'success': True, + 'post_id': post_id + }), 201 + except pymysql.err.IntegrityError as e: + return jsonify({'error': 'Post with this VK ID already exists'}), 400 + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/posts/', methods=['PUT']) +@login_required +def api_update_post(post_id): + data = request.json + db = get_db() + try: + if not db.get_post(post_id): + return jsonify({'error': 'Post not found'}), 404 + + success = db.update_post(post_id, data) + if success: + # Определяем тип действия (сохранение или отмена) + action = "сохранение" if request.json.get('save_action') != "cancel" else "отмена редактирования" + post = db.get_post(post_id) + client_ip = get_client_ip() + identifier = get_post_identifier(post) + log_event(f"Пост обновлен: {identifier}, Действие: {action}, IP {client_ip}") + return jsonify({'success': success, 'post_id': post_id}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/posts/', methods=['DELETE']) +@login_required +def api_delete_post(post_id): + db = get_db() + try: + post = db.get_post(post_id) + if not post: + return jsonify({'error': 'Post not found'}), 404 + + success = db.delete_post(post_id) + if success: + client_ip = get_client_ip() + identifier = get_post_identifier(post) + log_event(f"Удален пост: {identifier}, IP {client_ip}") + return jsonify({'success': success}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/run_script', methods=['POST']) +@login_required +def api_run_script(): + script_type = request.json.get('script') + + if script_type == 'rescan': + try: + # Запускаем функцию vk_load_10 и ждем ее завершения + log_event("Запуск рескана ВК...") + vk_load_10() + log_event("Рескан ВК выполнен успешно") + return jsonify({'success': True}) + except Exception as e: + error_msg = f"Ошибка при выполнении рескана ВК: {str(e)}" + log_event(error_msg) + return jsonify({'success': False, 'error': error_msg}), 500 + + elif script_type == 'publish': + try: + # Запускаем функцию публикации в Telegram + log_event("Запуск публикации в Telegram...") + + import asyncio + from threading import Thread + import queue + + # Функция для запуска в отдельном потоке + def run_async_func(): + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(publish_to_tg_all()) + return result + except Exception as e: + return e + + # Создаем очередь для передачи результата + q = queue.Queue() + # Запускаем в отдельном потоке + thread = Thread(target=lambda: q.put(run_async_func())) + thread.start() + thread.join() + result = q.get() + + if isinstance(result, Exception): + raise result + + log_event("Публикация в Telegram выполнена успешно") + return jsonify({'success': True}) + except Exception as e: + error_msg = f"Ошибка при выполнении публикации в Telegram: {str(e)}" + log_event(error_msg) + return jsonify({'success': False, 'error': error_msg}), 500 + + return jsonify({'success': False, 'error': 'Unknown script type'}), 400 + +@bp.route('/api/generate_all_shortnames', methods=['POST']) +@login_required +def api_generate_all_shortnames(): + try: + log_event("Запуск генерации названий для всех постов...") + ai_shortname_all() + log_event("Генерация названий завершена успешно") + return jsonify({'success': True}) + except Exception as e: + error_msg = f"Ошибка при генерации названий: {str(e)}" + log_event(error_msg) + return jsonify({'success': False, 'error': error_msg}), 500 + +@bp.route('/api/generate_shortname/', methods=['POST']) +@login_required +def api_generate_shortname(post_id): + db = get_db() + try: + post = db.get_post(post_id) + if not post: + return jsonify({'error': 'Post not found'}), 404 + + log_event(f"Генерация названия для поста ID: {post_id}") + shortname = generate_ai_shortname(post['text']) + + # Обновляем shortname в базе данных + with db.conn.cursor() as cursor: + cursor.execute( + "UPDATE posts SET shortname = %s WHERE id = %s", + (shortname, post_id) + ) + db.conn.commit() + + log_event(f"Сгенерировано название для поста ID: {post_id} - '{shortname}'") + return jsonify({'success': True, 'shortname': shortname}) + except Exception as e: + error_msg = f"Ошибка при генерации названия: {str(e)}" + log_event(error_msg) + return jsonify({'success': False, 'error': error_msg}), 500 + finally: + db.close() + +@bp.route('/api/publish_post/', methods=['POST']) +@login_required +def api_publish_post(post_id): + db = get_db() + try: + post = db.get_post(post_id) + if not post: + return jsonify({'error': 'Post not found'}), 404 + + log_event(f"Запуск публикации поста ID: {post_id}") + + import asyncio + from threading import Thread + import queue + + # Функция для запуска в отдельном потоке + def run_async_func(): + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(publish_to_tg(post['vk_post_id'])) + return result + except Exception as e: + return e + + # Создаем очередь для передачи результата + q = queue.Queue() + # Запускаем в отдельном потоке + thread = Thread(target=lambda: q.put(run_async_func())) + thread.start() + thread.join() + result = q.get() + + if isinstance(result, Exception): + raise result + + log_event(f"Публикация поста ID: {post_id} выполнена успешно") + return jsonify({'success': True}) + except Exception as e: + error_msg = f"Ошибка при публикации поста: {str(e)}" + log_event(error_msg) + return jsonify({'success': False, 'error': error_msg}), 500 + finally: + db.close() + +# Новые API endpoints для работы с Зилант-сервером и публикацией событий +@bp.route('/api/run_zk_rescan', methods=['POST']) +@login_required +def api_run_zk_rescan(): + try: + load_json_all() + log_event("Рескан Зилант-сервера выполнен успешно") + return jsonify({'success': True}) + except Exception as e: + error_msg = f"Ошибка при рескане Зилант-сервера: {str(e)}" + log_event(error_msg) + return jsonify({'success': False, 'error': error_msg}), 500 + +@bp.route('/api/publish_all_events', methods=['POST']) +@login_required +def api_publish_all_events(): + try: + log_event("Запуск публикации всех событий...") + + # Создаем асинхронную обертку для запуска в отдельном потоке + import asyncio + from threading import Thread + import queue + + def run_async_func(): + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(tg_post_all_events()) + return result + except Exception as e: + return e + + # Создаем очередь для передачи результата + q = queue.Queue() + # Запускаем в отдельном потоке + thread = Thread(target=lambda: q.put(run_async_func())) + thread.start() + thread.join() + result = q.get() + + if isinstance(result, Exception): + raise result + + log_event("Публикация всех событий выполнена успешно") + return jsonify({'success': True}) + except Exception as e: + error_msg = f"Ошибка при публикации всех событий: {str(e)}" + log_event(error_msg) + return jsonify({'success': False, 'error': error_msg}), 500 + +@bp.route('/api/publish_event/', methods=['POST']) +@login_required +def api_publish_event(event_id): + try: + # Сначала получаем событие из базы данных + db = get_db() + event = db.get_event(event_id) + if not event: + return jsonify({'success': False, 'error': 'Event not found'}), 404 + + log_event(f"Запуск публикации события ID: {event_id} (id_event: {event['id_event']})") + + # Создаем асинхронную обертку для запуска в отдельном потоке + import asyncio + from threading import Thread + import queue + + def run_async_func(): + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + # Передаем id_event вместо внутреннего id + result = loop.run_until_complete(tg_post_event_by_id(event['id_event'])) + return result + except Exception as e: + return e + + # Создаем очередь для передачи результата + q = queue.Queue() + # Запускаем в отдельном потоке + thread = Thread(target=lambda: q.put(run_async_func())) + thread.start() + thread.join() + result = q.get() + + if isinstance(result, Exception): + raise result + + log_event(f"Публикация события ID: {event_id} (id_event: {event['id_event']}) выполнена успешно") + return jsonify({'success': True}) + except Exception as e: + error_msg = f"Ошибка при публикации события: {str(e)}" + log_event(error_msg) + return jsonify({'success': False, 'error': error_msg}), 500 + finally: + if 'db' in locals(): + db.close() + +# Новые API endpoints для работы со связями +@bp.route('/api/interlinks', methods=['GET']) +@login_required +def api_check_interlink(): + post_id = request.args.get('post_id') + event_id = request.args.get('event_id') + + if not post_id or not event_id: + return jsonify({'error': 'Missing parameters'}), 400 + + db = get_db() + try: + exists = db.check_interlink(post_id, event_id) + return jsonify({'exists': exists}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/interlinks', methods=['POST']) +@login_required +def api_create_interlink(): + data = request.json + post_id = data.get('post_id') + event_id = data.get('event_id') + + if not post_id or not event_id: + return jsonify({'error': 'Missing parameters'}), 400 + + db = get_db() + try: + link_id = db.create_interlink(post_id, event_id) + return jsonify({'success': True, 'link_id': link_id}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/interlinks', methods=['DELETE']) +@login_required +def api_delete_interlink(): + data = request.json + post_id = data.get('post_id') + event_id = data.get('event_id') + + if not post_id or not event_id: + return jsonify({'error': 'Missing parameters'}), 400 + + db = get_db() + try: + success = db.delete_interlink(post_id, event_id) + return jsonify({'success': success}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +# Новые API endpoints для получения связанных событий и постов +@bp.route('/api/posts//linked_events') +@login_required +def api_get_linked_events(post_id): + db = get_db() + try: + events = db.get_linked_events(post_id) + events_list = [] + for event in events: + events_list.append({ + 'id': event['id'], + 'name': event['name'], + 'number': event['number'], + 'unit_name': event['unit_name'] + }) + return jsonify(events_list) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/events//linked_posts') +@login_required +def api_get_linked_posts(event_id): + db = get_db() + try: + posts = db.get_linked_posts(event_id) + posts_list = [] + for post in posts: + posts_list.append({ + 'id': post['id'], + 'vk_post_id': post['vk_post_id'], + 'shortname': post['shortname'], + 'text_preview': post['text'][:50] + '...' if post['text'] else '' + }) + return jsonify(posts_list) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/api/interlinks/all', methods=['GET']) +@login_required +def api_get_all_interlinks(): + db = get_db() + try: + interlinks = db.get_all_interlinks() + return jsonify(interlinks) + except Exception as e: + return jsonify({'error': str(e)}), 500 + finally: + db.close() + +@bp.route('/image_proxy') +@login_required +def image_proxy(): + image_url = request.args.get('url') + if not image_url: + return 'Missing image URL', 400 + + try: + response = requests.get(image_url, timeout=10) + response.raise_for_status() + + img = Image.open(BytesIO(response.content)) + max_size = 800 + width, height = img.size + + if width > max_size or height > max_size: + ratio = min(max_size/width, max_size/height) + new_size = (int(width * ratio), int(height * ratio)) + img = img.resize(new_size, Image.LANCZOS) + + output = BytesIO() + img.convert('RGB').save(output, format='JPEG', quality=85) + output.seek(0) + + return Response(output, mimetype='image/jpeg') + except Exception as e: + return f'Error loading image: {str(e)}', 500 + +@bp.route('/edit_post/') +@login_required +def edit_post_page(post_id): + db = get_db() + try: + post = db.get_post(post_id) + if not post: + flash('Пост не найден', 'danger') + return redirect(url_for('zilant.index')) + + client_ip = get_client_ip() + identifier = get_post_identifier(post) + log_event(f"Открыт пост на редактирование: {identifier}, IP {client_ip}") + finally: + db.close() + return render_template('edit_post.html', post_id=post_id) + +@bp.route('/add_post') +@login_required +def add_post_page(): + return render_template('edit_post.html', post_id=None) + +@bp.route('/edit_event/') +@login_required +def edit_event_page(event_id): + db = get_db() + try: + event = db.get_event(event_id) + if not event: + flash('Событие не найдено', 'danger') + return redirect(url_for('zilant.index')) + + client_ip = get_client_ip() + log_event(f"Открыто событие на редактирование: {event['name']}, ID: {event_id}, IP {client_ip}") + finally: + db.close() + return render_template('edit_event.html', event_id=event_id) + +@bp.route('/add_event') +@login_required +def add_event_page(): + return render_template('edit_event.html', event_id=None) + +# Регистрируем Blueprint в приложении +app.register_blueprint(bp) + +# Инициализация базы данных +def init_db(): + db = get_db() + try: + # Проверяем и создаем структуру БД + ensure_database_structure() + print("Структура базы данных проверена и создана при необходимости") + + # Проверяем и создаем таблицу events, если она не существует +# with db.conn.cursor() as cursor: +# cursor.execute(""" +# CREATE TABLE IF NOT EXISTS 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 DEFAULT NULL, +# tg_posted_date DATETIME NULL, +# announcement_link BIGINT NULL DEFAULT NULL +# ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 +# """) +# db.conn.commit() +# print("Таблица events проверена и создана при необходимости") + + # Создаем администратора по умолчанию + with db.conn.cursor() as cursor: + cursor.execute("SELECT COUNT(*) as count FROM users") + if cursor.fetchone()['count'] == 0: + username = os.getenv('DEFNM') + password = os.getenv('DEFPW') + if username and password: + hashed_password = generate_password_hash(password) + cursor.execute( + "INSERT INTO users (username, password) VALUES (%s, %s)", + (username, hashed_password) + ) + print(f"Created default user: {username}") + else: + print("Environment variables DEFNM/DEFPW not set. Admin not created.") + + db.conn.commit() + except Exception as e: + print(f"Database initialization error: {str(e)}") + raise + finally: + db.close() + +# Запуск приложения +if __name__ == '__main__': + init_db() + port = int(os.getenv('WEBCTRL_PORT', 5003)) + app.run(host='0.0.0.0', port=port, debug=True) diff --git a/db_update_shortname.py b/db_update_shortname.py new file mode 100644 index 0000000..6fd5045 --- /dev/null +++ b/db_update_shortname.py @@ -0,0 +1,244 @@ +import os +import pymysql +from dotenv import load_dotenv +import requests +import json +import re +import time +import sys +import random +from datetime import datetime + +# Загрузка переменных окружения +load_dotenv() + +# Конфигурация из переменных окружения +DB_CONFIG = { + 'host': os.getenv('MDB_HOST'), + 'user': os.getenv('MDB_USER'), + 'password': os.getenv('MDB_PW'), + 'database': os.getenv('MDBASE'), + 'charset': 'utf8mb4' +} + +OPENROUTER_CONFIG = { + 'api_key': os.getenv('OR_KEY'), + 'model': os.getenv('OR_MODEL_NAME') +} + +# Настройки журналирования +LOG_FILE = os.getenv('LOG_FILE', 'ai_namer.log') # Путь к лог-файлу +LOG_PREFIX = "AI_namer" # Уникальный префикс для идентификации скрипта + +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 _generate_shortname_by_api(text): + """Внутренняя функция для генерации краткого названия через OpenRouter API""" + url = "https://openrouter.ai/api/v1/chat/completions" + headers = { + "Authorization": f"Bearer {OPENROUTER_CONFIG['api_key']}", + "Content-Type": "application/json" + } + + prompt = f""" +Прочитай нижеприведенный анонс и сформируй краткое (до 80 знаков) название этого события для каталога. +Место, дата и время проведения для каталога несущественны. +Важно название и, в зависимости от того, что это за событие, выступающие на нем или проводящие его. +Для данного каталога допустимо использование никнеймов вместо официальных имен и фамилий. + +Ответ должен содержать только одно название без каких-либо объяснений, комментариев и вариантов. + +АНОНС: {text} +Название:""" + + data = { + "model": OPENROUTER_CONFIG['model'], + "messages": [{"role": "user", "content": prompt}], + "max_tokens": 5000, + "temperature": 0.3 + } + + try: + response = requests.post(url, headers=headers, json=data, timeout=60) + + # Обработка ошибки 429 (Too Many Requests) + if response.status_code == 429: + error_info = { + "status_code": response.status_code, + "headers": dict(response.headers), + "body": response.text + } + + # Пытаемся распарсить JSON ответ + try: + error_info["body"] = response.json() + except: + pass + + log_message(f"Ошибка 429: Превышен лимит запросов. Ответ API: {json.dumps(error_info, ensure_ascii=False)}") + print("Ошибка 429: Превышен лимит запросов. Подробности в логе.") + sys.exit(1) + + response.raise_for_status() + result = response.json() + + # Извлекаем только текст ответа + shortname = result['choices'][0]['message']['content'].strip() + + # Удаляем возможные кавычки и лишние символы + shortname = re.sub(r'^["\']|["\']$', '', shortname) + + # Обрезаем до 80 символов + return shortname[:80] + + except requests.exceptions.HTTPError as e: + if e.response.status_code == 429: + error_info = { + "status_code": e.response.status_code, + "headers": dict(e.response.headers), + "body": e.response.text + } + + # Пытаемся распарсить JSON ответ + try: + error_info["body"] = e.response.json() + except: + pass + + log_message(f"Ошибка 429: Превышен лимит запросов. Ответ API: {json.dumps(error_info, ensure_ascii=False)}") + print("Ошибка 429: Превышен лимит запросов. Подробности в логе.") + sys.exit(1) + else: + error_msg = f"HTTP ошибка при генерации названия: {e}" + log_message(error_msg) + print(error_msg) + return None + except Exception as e: + error_msg = f"Ошибка при генерации названия: {e}" + log_message(error_msg) + print(error_msg) + return None + +def generate_ai_shortname(text): + """ + Генерирует краткое название для описанного в тексте мероприятия. + + Args: + text (str): Текст описания мероприятия + + Returns: + str: Краткое название (до 80 знаков) или None в случае ошибки + """ + log_message(f"Начало генерации названия для текста длиной {len(text)} символов") + + if not text or not text.strip(): + log_message("Получен пустой текст для генерации названия") + return None + + shortname = _generate_shortname_by_api(text) + + if shortname: + log_message(f"Успешно сгенерировано название: {shortname}") + else: + log_message("Не удалось сгенерировать название") + + return shortname + +def ai_shortname_all(): + """ + Обрабатывает все записи в базе данных с пустым полем shortname, + генерируя для них краткие названия с помощью нейросети. + """ + log_message("Запуск обработки всех записей с пустым shortname") + + # Подключение к БД + try: + connection = pymysql.connect(**DB_CONFIG) + log_message("Успешное подключение к базе данных") + except Exception as e: + error_msg = f"Ошибка подключения к базе данных: {e}" + log_message(error_msg) + print(error_msg) + return + + try: + with connection.cursor() as cursor: + # Выбор записей с пустым shortname + cursor.execute("SELECT id, text FROM posts WHERE shortname IS NULL OR shortname = ''") + posts = cursor.fetchall() + + log_message(f"Найдено {len(posts)} записей для обработки") + print(f"Найдено {len(posts)} записей для обработки") + + for i, (post_id, text) in enumerate(posts, 1): + if not text: + continue + + log_message(f"Обрабатывается запись {i}/{len(posts)} (ID: {post_id})") + print(f"Обрабатывается запись {i}/{len(posts)} (ID: {post_id})") + + shortname = generate_ai_shortname(text) + + if shortname: + # Обновление записи + try: + cursor.execute( + "UPDATE posts SET shortname = %s WHERE id = %s", + (shortname, post_id) + ) + connection.commit() + log_message(f"Запись {post_id} успешно обновлена: {shortname}") + print(f"Обновлено: {shortname}") + except Exception as e: + error_msg = f"Ошибка обновления записи {post_id}: {e}" + log_message(error_msg) + print(error_msg) + else: + log_message(f"Не удалось сгенерировать название для записи {post_id}") + print("Не удалось сгенерировать название") + + # Небольшая пауза между запросами + time.sleep(1) + + log_message("Обработка всех записей завершена") + print("Обработка завершена") + except SystemExit: + # Перехватываем системный выход для корректного закрытия соединения + log_message("Завершение работы из-за ошибки 429") + print("Завершение работы из-за ошибки 429") + except Exception as e: + error_msg = f"Произошла ошибка при обработке записей: {e}" + log_message(error_msg) + print(error_msg) + finally: + connection.close() + log_message("Соединение с базой данных закрыто") + +if __name__ == "__main__": + ai_shortname_all()