1535 lines
58 KiB
Python
1535 lines
58 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
|
|
from volk_load import load_categories, load_all_events
|
|
|
|
# Загружаем настройки из .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', 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:
|
|
# Проверка режима работы
|
|
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/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/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)
|
|
|
|
# Регистрируем 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)
|