Files
Zilant2025/db_edit.py
T
2025-11-14 12:30:31 +03:00

1486 lines
55 KiB
Python

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 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/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', 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', 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/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({'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/<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:
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/<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)
# Регистрируем 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)