Files
2026-08-02 16:52:48 +03:00

2056 lines
80 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
from volk_load import load_categories, load_all_events
from volk_cat_publish import tg_post_category_by_id, tg_post_all_categories
from test_tg_poster import make_test_post
# Загружаем настройки из .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 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')
g.workmode = os.getenv('WORKMODE', 'ZILANT')
# Маршруты 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'],
'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/<int:event_id>/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/mark_all_for_publication', methods=['POST'])
@login_required
def api_mark_all_events_for_publication():
"""Отмечает все события для публикации"""
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("""
UPDATE events
SET marked_to_publication = 1
WHERE marked_to_publication = 0
""")
count = cursor.rowcount
db.conn.commit()
client_ip = get_client_ip()
log_event(f"Отмечено для публикации: {count} событий, IP {client_ip}")
return jsonify({'success': True, 'count': count})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
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/<int:event_id>', 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/<int:event_id>', 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/<int:post_id>')
@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/<int:event_id>')
@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/<int:post_id>/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/mark_all_for_publication', methods=['POST'])
@login_required
def api_mark_all_posts_for_publication():
"""Отмечает все посты для публикации"""
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("""
UPDATE posts
SET marked_for_publication = 1
WHERE marked_for_publication = 0
""")
count = cursor.rowcount
db.conn.commit()
client_ip = get_client_ip()
log_event(f"Отмечено для публикации: {count} постов, IP {client_ip}")
return jsonify({'success': True, 'count': count})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
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/<int:post_id>', 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/<int:post_id>', 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/test_post', methods=['POST'])
@login_required
def api_test_post():
try:
log_event("Запуск публикации тестового сообщения...")
result = make_test_post()
if result:
log_event("Тестовое сообщение успешно опубликовано")
return jsonify({'success': True})
else:
error_msg = "Не удалось опубликовать тестовое сообщение"
log_event(error_msg)
return jsonify({'success': False, 'error': error_msg}), 500
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/<int:post_id>', 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({'success': False, 'error': 'Post not found'}), 404
log_event(f"Генерация названия для поста ID: {post_id}")
shortname = generate_ai_shortname(post['text'])
if not shortname:
return jsonify({
'success': False,
'error': 'Не удалось сгенерировать название (пустой ответ AI)',
}), 500
# После долгого AI-запроса MySQL-соединение могло протухнуть
try:
db.conn.ping(reconnect=True)
except Exception:
db.close()
db = get_db()
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:
try:
db.close()
except Exception:
pass
@bp.route('/api/publish_post/<int:post_id>', 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:
# Устанавливаем правильную политику event loop для работы с anyio
# Это критично для правильной работы httpx в отдельном потоке
if sys.platform == 'win32':
# На Windows используем ProactorEventLoop
policy = asyncio.WindowsProactorEventLoopPolicy()
else:
# На Linux используем DefaultEventLoopPolicy
policy = asyncio.DefaultEventLoopPolicy()
asyncio.set_event_loop_policy(policy)
# Создаем новый event loop для этого потока
# Это важно при работе с gunicorn, который может иметь свой event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Устанавливаем переменную окружения для anyio backend
import os
os.environ['ANYIO_BACKEND'] = 'asyncio'
try:
result = loop.run_until_complete(publish_to_tg(post['vk_post_id']))
return result
finally:
# Закрываем event loop после завершения
# Это важно для предотвращения утечек ресурсов
try:
# Отменяем все незавершенные задачи
pending = asyncio.all_tasks(loop)
for task in pending:
task.cancel()
# Ждем завершения отмененных задач
if pending:
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
except Exception:
pass
finally:
loop.close()
# Сбрасываем политику event loop
asyncio.set_event_loop_policy(None)
except Exception as e:
import traceback
log_event(f"Ошибка в run_async_func: {str(e)}\n{traceback.format_exc()}")
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
# Проверяем, что публикация действительно успешна (результат True)
if result is not True:
error_msg = f"Публикация поста ID: {post_id} не удалась (результат: {result})"
log_event(error_msg)
return jsonify({'success': False, 'error': error_msg}), 500
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:
# Проверка режима работы
workmode = os.getenv('WORKMODE', 'ZILANT')
if workmode != 'ZILANT':
error_msg = "Рескан Зилант-сервера доступен только в режиме ZILANT"
log_event(error_msg)
return jsonify({'success': False, 'error': error_msg}), 403
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_all_categories', methods=['POST'])
@login_required
def api_publish_all_categories():
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_categories())
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_category/<category_id>', methods=['POST'])
@login_required
def api_publish_category(category_id):
try:
# Сначала проверяем существование категории
db = get_db()
with db.conn.cursor() as cursor:
cursor.execute("SELECT ID, TITLE FROM categories WHERE ID = %s", (category_id,))
category = cursor.fetchone()
if not category:
return jsonify({'success': False, 'error': 'Category not found'}), 404
db.close()
log_event(f"Запуск публикации площадки ID: {category_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(tg_post_category_by_id(category_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 {category_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
@bp.route('/api/load_categories', methods=['POST'])
@login_required
def api_load_categories():
try:
# Проверка режима работы
workmode = os.getenv('WORKMODE', 'ZILANT')
if workmode != 'VOLK':
error_msg = "Загрузка категорий доступна только в режиме VOLK"
log_event(error_msg)
return jsonify({'success': False, 'error': error_msg}), 403
log_event("Запуск загрузки категорий...")
load_categories()
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/categories')
@login_required
def api_categories():
"""Получает список всех категорий (площадок) из таблицы categories"""
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("""
SELECT AUTO_ID, ID, TITLE, description, marked_for_publication, is_tg_published
FROM categories
ORDER BY TITLE, ID
""")
categories = cursor.fetchall()
categories_list = []
for cat in categories:
categories_list.append({
'auto_id': cat['AUTO_ID'],
'id': cat['ID'],
'title': cat['TITLE'] or '',
'description': cat['description'] or '',
'marked_for_publication': bool(cat['marked_for_publication']) if cat['marked_for_publication'] is not None else False,
'is_tg_published': bool(cat['is_tg_published']) if cat['is_tg_published'] is not None else False
})
return jsonify(categories_list)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/categories/<category_id>')
@login_required
def api_category_details(category_id):
"""Получает детали категории по ID"""
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("""
SELECT AUTO_ID, ID, TITLE, description, image_url, tg_id, is_tg_published, tg_published_date, marked_for_publication
FROM categories
WHERE ID = %s
""", (category_id,))
category = cursor.fetchone()
if not category:
return jsonify({'error': 'Category not found'}), 404
# Получаем количество событий для этой категории
cursor.execute("""
SELECT COUNT(*) as event_count
FROM events
WHERE unit_name = %s
""", (category_id,))
event_count_result = cursor.fetchone()
event_count = event_count_result['event_count'] if event_count_result else 0
return jsonify({
'auto_id': category['AUTO_ID'],
'id': category['ID'],
'title': category['TITLE'] or '',
'description': category['description'] or '',
'image_url': category['image_url'] or '',
'tg_id': category['tg_id'],
'is_tg_published': bool(category['is_tg_published']) if category['is_tg_published'] is not None else False,
'tg_published_date': category['tg_published_date'].isoformat() if category['tg_published_date'] else None,
'marked_for_publication': bool(category['marked_for_publication']) if category['marked_for_publication'] is not None else False,
'event_count': event_count
})
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/categories', methods=['POST'])
@login_required
def api_create_category():
"""Создает новую категорию"""
db = get_db()
try:
data = request.json
with db.conn.cursor() as cursor:
cursor.execute("""
INSERT INTO categories (ID, TITLE, description, image_url, tg_id, marked_for_publication, is_tg_published, tg_published_date)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""", (
data.get('id'),
data.get('title', ''),
data.get('description', ''),
data.get('image_url', ''),
data.get('tg_id'),
bool(data.get('marked_for_publication', False)),
bool(data.get('is_tg_published', False)),
data.get('tg_published_date') or None
))
db.conn.commit()
client_ip = get_client_ip()
log_event(f"Создана площадка: {data.get('id')}, IP {client_ip}")
return jsonify({'success': True})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/categories/<category_id>', methods=['PUT'])
@login_required
def api_update_category(category_id):
"""Обновляет категорию"""
db = get_db()
try:
data = request.json
new_id = data.get('id', '').strip()
if not new_id:
return jsonify({'success': False, 'error': 'ID is required'}), 400
with db.conn.cursor() as cursor:
# Сначала проверяем существование категории по старому ID
cursor.execute("SELECT AUTO_ID FROM categories WHERE ID = %s", (category_id,))
category = cursor.fetchone()
if not category:
return jsonify({'success': False, 'error': 'Category not found'}), 404
auto_id = category['AUTO_ID']
# Если новый ID отличается от старого, проверяем, не занят ли он
if new_id != category_id:
cursor.execute("SELECT AUTO_ID FROM categories WHERE ID = %s", (new_id,))
existing = cursor.fetchone()
if existing and existing['AUTO_ID'] != auto_id:
return jsonify({'success': False, 'error': f'Category with ID "{new_id}" already exists'}), 400
# Обновляем категорию, включая ID если он изменился
cursor.execute("""
UPDATE categories
SET ID = %s, TITLE = %s, description = %s, image_url = %s, tg_id = %s,
marked_for_publication = %s, is_tg_published = %s, tg_published_date = %s
WHERE AUTO_ID = %s
""", (
new_id,
data.get('title', ''),
data.get('description', ''),
data.get('image_url', ''),
data.get('tg_id'),
bool(data.get('marked_for_publication', False)),
bool(data.get('is_tg_published', False)),
data.get('tg_published_date') or None,
auto_id
))
db.conn.commit()
if cursor.rowcount > 0:
client_ip = get_client_ip()
log_event(f"Обновлена площадка: {category_id} -> {new_id}, IP {client_ip}")
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': 'Category not found'}), 404
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/categories/<category_id>', methods=['DELETE'])
@login_required
def api_delete_category(category_id):
"""Удаляет категорию"""
db = get_db()
try:
with db.conn.cursor() as cursor:
# Проверяем наличие привязанных событий
cursor.execute("""
SELECT COUNT(*) as event_count
FROM events
WHERE unit_name = %s
""", (category_id,))
event_count_result = cursor.fetchone()
event_count = event_count_result['event_count'] if event_count_result else 0
if event_count > 0:
return jsonify({
'success': False,
'error': 'Cannot delete category with linked events',
'event_count': event_count
}), 400
# Получаем название категории для лога
cursor.execute("SELECT TITLE FROM categories WHERE ID = %s", (category_id,))
category = cursor.fetchone()
category_title = category['TITLE'] if category else category_id
cursor.execute("DELETE FROM categories WHERE ID = %s", (category_id,))
db.conn.commit()
if cursor.rowcount > 0:
client_ip = get_client_ip()
log_event(f"Удалена площадка: {category_title}, ID: {category_id}, IP {client_ip}")
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': 'Category not found'}), 404
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/categories/<category_id>/toggle_publication', methods=['POST'])
@login_required
def api_toggle_category_publication(category_id):
"""Переключает статус публикации категории"""
db = get_db()
try:
with db.conn.cursor() as cursor:
# Получаем текущее значение
cursor.execute("SELECT marked_for_publication FROM categories WHERE ID = %s", (category_id,))
category = cursor.fetchone()
if not category:
return jsonify({'success': False, 'error': 'Category not found'}), 404
# Переключаем значение
new_value = not bool(category['marked_for_publication'])
cursor.execute("""
UPDATE categories
SET marked_for_publication = %s
WHERE ID = %s
""", (new_value, category_id))
db.conn.commit()
client_ip = get_client_ip()
log_event(f"Изменен статус публикации площадки: {category_id}, новый статус: {new_value}, IP {client_ip}")
return jsonify({'success': True, 'marked_for_publication': new_value})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/categories/mark_all_for_publication', methods=['POST'])
@login_required
def api_mark_all_categories_for_publication():
"""Отмечает все категории для публикации"""
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("""
UPDATE categories
SET marked_for_publication = 1
WHERE marked_for_publication = 0
""")
count = cursor.rowcount
db.conn.commit()
client_ip = get_client_ip()
log_event(f"Отмечено для публикации: {count} площадок, IP {client_ip}")
return jsonify({'success': True, 'count': count})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/categories/<category_id>/events_count')
@login_required
def api_category_events_count(category_id):
"""Получает количество событий, привязанных к категории"""
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("""
SELECT COUNT(*) as event_count
FROM events
WHERE unit_name = %s
""", (category_id,))
result = cursor.fetchone()
return jsonify({'count': result['event_count'] if result else 0})
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/events/count')
@login_required
def api_events_count():
"""Получает количество событий в таблице events"""
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("SELECT COUNT(*) as count FROM events")
result = cursor.fetchone()
return jsonify({'count': result['count'] if result else 0})
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/run_volk_rescan', methods=['POST'])
@login_required
def api_run_volk_rescan():
try:
# Проверка режима работы
workmode = os.getenv('WORKMODE', 'ZILANT')
if workmode != 'VOLK':
error_msg = "Рескан ВОЛК доступен только в режиме VOLK"
log_event(error_msg)
return jsonify({'success': False, 'error': error_msg}), 403
log_event("Запуск рескана ВОЛК...")
load_all_events()
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/<int:event_id>', 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/<int:post_id>/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:
# Определяем статус события
status = "Не видим"
if event.get('is_posted_tg'):
status = "Опубликован"
elif event.get('marked_to_publication'):
status = "Для публикации"
elif event.get('is_visible'):
status = "Видим"
events_list.append({
'id': event['id'],
'name': event['name'],
'number': event['number'],
'unit_name': event['unit_name'],
'status': status
})
return jsonify(events_list)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/events/<int:event_id>/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:
# Определяем статус поста
status = "Не для публикации"
if post.get('published_in_tg'):
status = "Опубликован"
elif post.get('marked_for_publication'):
status = "Для публикации"
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 '',
'status': status
})
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/<venue_name>/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/<tag_name>/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/<int:post_id>')
@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/<int:event_id>')
@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)
@bp.route('/edit_category/<category_id>')
@login_required
def edit_category_page(category_id):
db = get_db()
try:
with db.conn.cursor() as cursor:
cursor.execute("SELECT ID, TITLE FROM categories WHERE ID = %s", (category_id,))
category = cursor.fetchone()
if not category:
flash('Площадка не найдена', 'danger')
return redirect(url_for('zilant.index'))
client_ip = get_client_ip()
log_event(f"Открыта площадка на редактирование: {category['TITLE']}, ID: {category_id}, IP {client_ip}")
finally:
db.close()
return render_template('edit_category.html', category_id=category_id)
@bp.route('/add_category')
@login_required
def add_category_page():
return render_template('edit_category.html', category_id=None)
# Регистрируем Blueprint в приложении
app.register_blueprint(bp)
# Инициализация базы данных
def init_db():
db = get_db()
try:
# Проверяем и создаем структуру БД
ensure_database_structure()
print("Структура базы данных проверена и создана при необходимости")
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)