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, auto_unit 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 get_venues(self): """Получает список всех уникальных площадок из таблицы events""" with self.conn.cursor() as cursor: cursor.execute(""" SELECT DISTINCT unit_name FROM events WHERE unit_name IS NOT NULL AND unit_name != '' ORDER BY unit_name """) return cursor.fetchall() 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, auto_unit ) VALUES (%s, %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', '')), data.get('auto_unit', '') ) 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, auto_unit = %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', '')), data.get('auto_unit', ''), 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'] g.bot_port = os.getenv('WEBHOOK_PORT', '5005') g.webhook_secret = os.getenv('WEBHOOK_SECRET') g.webhook_url = os.getenv('WEBHOOK_URL') # Маршруты 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/venues') @login_required def api_venues(): db = get_db() try: venues = db.get_venues() return jsonify(venues) finally: db.close() @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'], 'auto_unit': post['auto_unit'] }) 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() # Новые API endpoints для работы с площадками @bp.route('/api/venues') @login_required def api_venues(): """Получает список всех уникальных площадок с количеством событий""" db = get_db() try: with db.conn.cursor() as cursor: cursor.execute(""" SELECT unit_name as name, COUNT(*) as event_count FROM events WHERE unit_name IS NOT NULL AND unit_name != '' GROUP BY unit_name ORDER BY unit_name """) venues = cursor.fetchall() venues_list = [] for venue in venues: venues_list.append({ 'name': venue['name'], 'event_count': venue['event_count'] }) return jsonify(venues_list) except Exception as e: return jsonify({'error': str(e)}), 500 finally: db.close() @bp.route('/api/venues//events') @login_required def api_venue_events(venue_name): """Получает список событий для указанной площадки с информацией о привязке к посту""" post_id = request.args.get('post_id') if not post_id: return jsonify({'error': 'post_id parameter is required'}), 400 db = get_db() try: with db.conn.cursor() as cursor: # Получаем события площадки с информацией о привязке к посту cursor.execute(""" SELECT e.id, e.name, e.number, e.unit_name, CASE WHEN i.post_id IS NOT NULL THEN 1 ELSE 0 END as linked FROM events e LEFT JOIN interlinks i ON e.id = i.event_id AND i.post_id = %s WHERE e.unit_name = %s ORDER BY e.name """, (post_id, venue_name)) events = cursor.fetchall() events_list = [] for event in events: events_list.append({ 'id': event['id'], 'name': event['name'], 'number': event['number'], 'unit_name': event['unit_name'], 'linked': bool(event['linked']) }) return jsonify(events_list) except Exception as e: return jsonify({'error': str(e)}), 500 finally: db.close() # API endpoints для работы с тэгами @bp.route('/api/tags') @login_required def api_tags(): """Получает список всех уникальных тэгов с количеством событий""" db = get_db() try: with db.conn.cursor() as cursor: # Получаем все тэги и разбиваем их по пробелам cursor.execute(""" SELECT tags FROM events WHERE tags IS NOT NULL AND tags != '' """) events = cursor.fetchall() # Собираем все уникальные тэги tag_counts = {} for event in events: tags = event['tags'].split() for tag in tags: tag = tag.strip() if tag: tag_counts[tag] = tag_counts.get(tag, 0) + 1 # Сортируем по количеству событий (убывание), затем по названию sorted_tags = sorted(tag_counts.items(), key=lambda x: (-x[1], x[0])) tags_list = [] for tag, count in sorted_tags: tags_list.append({ 'name': tag, 'event_count': count }) return jsonify(tags_list) except Exception as e: return jsonify({'error': str(e)}), 500 finally: db.close() @bp.route('/api/tags//events') @login_required def api_tag_events(tag_name): """Получает список событий для указанного тэга с информацией о привязке к посту""" post_id = request.args.get('post_id') if not post_id: return jsonify({'error': 'post_id parameter is required'}), 400 db = get_db() try: with db.conn.cursor() as cursor: # Получаем события с указанным тэгом с информацией о привязке к посту cursor.execute(""" SELECT e.id, e.name, e.number, e.unit_name, e.tags, CASE WHEN i.post_id IS NOT NULL THEN 1 ELSE 0 END as linked FROM events e LEFT JOIN interlinks i ON e.id = i.event_id AND i.post_id = %s WHERE e.tags LIKE %s ORDER BY e.name """, (post_id, f'%{tag_name}%')) events = cursor.fetchall() events_list = [] for event in events: # Проверяем, что тэг действительно присутствует (не подстрока другого тэга) tags = event['tags'].split() if event['tags'] else [] if tag_name in tags: events_list.append({ 'id': event['id'], 'name': event['name'], 'number': event['number'], 'unit_name': event['unit_name'], 'tags': event['tags'], 'linked': bool(event['linked']) }) return jsonify(events_list) 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)