domovoy_bot/web/app_reference.py

1547 lines
65 KiB
Python
Raw 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.

"""
Web Admin Interface v3.0 - MEGA ADMIN PANEL
Полноценное управление ВСЕМ функционалом бота
"""
import logging
import json
import aiohttp
from pathlib import Path
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form, File, UploadFile
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, update, delete, desc
from datetime import datetime, timedelta, timezone
import secrets
import config
# ===== ЧАСОВОЙ ПОЯС (Ульяновск, UTC+4) =====
LOCAL_TZ = timezone(timedelta(hours=4)) # Самара/Ульяновск
def to_local(dt: datetime) -> datetime:
"""Конвертировать UTC datetime в локальное время (Ульяновск, UTC+4)"""
if dt is None:
return None
if dt.tzinfo is None:
# Считаем что naive datetime — это UTC
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(LOCAL_TZ)
def fmt_local(dt: datetime, fmt: str = '%d.%m.%Y %H:%M') -> str:
"""Форматировать UTC datetime в строку локального времени"""
local_dt = to_local(dt)
return local_dt.strftime(fmt) if local_dt else '-'
from database.db import AsyncSessionLocal
from database.models import (
User, Message, Poll, Ad, PaymentReminder, Schedule,
VerificationRequest, Service, Announcement, Event, ScheduledPost,
Broadcast, BroadcastRead, Digest
)
logger = logging.getLogger(__name__)
app = FastAPI(title="Домовой Бот - MEGA Admin Panel", version="3.0.0")
# Добавляем CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Разрешаем все origins (для локального использования)
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
templates = Jinja2Templates(directory="web/templates")
app.mount("/static", StaticFiles(directory="web/static"), name="static")
app.mount("/static/service_images", StaticFiles(directory="data/service_images"), name="service_images")
security = HTTPBasic()
def get_current_admin(credentials: HTTPBasicCredentials = Depends(security)):
"""Проверка админа"""
correct_username = secrets.compare_digest(credentials.username, config.WEB_ADMIN_LOGIN)
correct_password = secrets.compare_digest(credentials.password, config.WEB_ADMIN_PASSWORD)
if not (correct_username and correct_password):
raise HTTPException(status_code=401, detail="Неверный логин или пароль",
headers={"WWW-Authenticate": "Basic"})
return credentials.username
# ============================================================================
# ГЛАВНАЯ
# ============================================================================
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request, username: str = Depends(get_current_admin)):
"""Дашборд"""
async with AsyncSessionLocal() as session:
users_count = (await session.execute(select(func.count(User.user_id)))).scalar()
verified_count = (await session.execute(
select(func.count(User.user_id)).where(User.verified == True))).scalar()
messages_count = (await session.execute(select(func.count(Message.id)))).scalar()
active_ads = (await session.execute(
select(func.count(Ad.ad_id)).where(Ad.is_active == True))).scalar()
active_polls = (await session.execute(
select(func.count(Poll.poll_id)).where(Poll.is_active == True))).scalar()
upcoming_events = (await session.execute(
select(func.count(Event.id)).where(Event.is_active == True))).scalar()
# Получаем количество ожидающих верификации
from database.models import VerificationRequest
pending_count = (await session.execute(
select(func.count(VerificationRequest.id)).where(VerificationRequest.status == 'pending')
)).scalar()
return templates.TemplateResponse("dashboard.html", {
"request": request, "username": username,
"stats": {
"users": users_count, "verified": verified_count,
"messages": messages_count, "ads": active_ads,
"polls": active_polls, "events": upcoming_events
},
"pending_count": pending_count
})
# ============================================================================
# ПОЛЬЗОВАТЕЛИ
# ============================================================================
@app.get("/users", response_class=HTMLResponse)
async def users_page(request: Request, search: str = "", filter: str = "", username: str = Depends(get_current_admin)):
"""Пользователи с фильтрами"""
async with AsyncSessionLocal() as session:
stmt = select(User)
# Применяем фильтр
if filter == 'verified':
stmt = stmt.where(User.verified == True)
elif filter == 'unverified':
stmt = stmt.where(User.verified == False)
elif filter == 'active_unverified':
stmt = stmt.where(User.verified == False, User.message_count >= 50)
elif filter == 'ig':
# Только ИГ
from database.models import InitiativeGroup
ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
ig_result = await session.execute(ig_stmt)
ig_user_ids = [row[0] for row in ig_result.all()]
stmt = stmt.where(User.user_id.in_(ig_user_ids))
# filter == 'all' или пустой — показываем всех
# Поиск
if search:
stmt = stmt.where(
(User.first_name.ilike(f"%{search}%")) |
(User.last_name.ilike(f"%{search}%")) |
(User.username.ilike(f"%{search}%")) |
(User.apartment.ilike(f"%{search}%"))
)
stmt = stmt.order_by(User.user_id.desc()).limit(100)
users = list((await session.execute(stmt)).scalars().all())
# Получаем список ИГ
from database.models import InitiativeGroup
ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True)
ig_result = await session.execute(ig_stmt)
ig_user_ids = set(row[0] for row in ig_result.all())
# Получаем заявки на верификацию
from database.models import VerificationRequest
pending_stmt = select(VerificationRequest).where(
VerificationRequest.status == 'pending'
).order_by(VerificationRequest.created_at.desc())
pending_result = await session.execute(pending_stmt)
pending_requests = [
{
'id': req.id,
'user_id': req.user_id,
'user_name': req.user.full_name if req.user else None,
'username': req.user.username if req.user else None,
'apartment': req.apartment,
'created_at': req.created_at
}
for req in pending_result.scalars().all()
]
# Статистика
total_count = (await session.execute(select(func.count(User.user_id)))).scalar()
verified_count = (await session.execute(select(func.count(User.user_id)).where(User.verified == True))).scalar()
unverified_count = total_count - verified_count
active_unverified = (await session.execute(
select(func.count(User.user_id)).where(User.verified == False, User.message_count >= 50)
)).scalar()
ig_count = len(ig_user_ids)
return templates.TemplateResponse("users.html",
{"request": request, "username": username, "users": users, "search": search, "current_filter": filter,
"stats": {"total": total_count, "verified": verified_count, "unverified": unverified_count, "active_unverified": active_unverified, "ig": ig_count},
"ig_user_ids": ig_user_ids,
"pending_requests": pending_requests,
"pending_count": len(pending_requests)})
@app.post("/api/users/auto_verify")
async def api_auto_verify_users(username: str = Depends(get_current_admin)):
"""Автоверификация активных пользователей (ТОЛЬКО с квартирой!)"""
async with AsyncSessionLocal() as session:
# Находим активных (50+ сообщений) которые не верифицированы
# И у которых УЖЕ ЕСТЬ квартира!
stmt = select(User).where(
User.verified == False,
User.message_count >= 50,
User.apartment != None,
User.apartment != ''
)
result = await session.execute(stmt)
active_users = list(result.scalars().all())
# Верифицируем всех
verified_count = 0
for user in active_users:
user.verified = True
if not user.verification_date:
user.verification_date = datetime.utcnow()
verified_count += 1
await session.commit()
return JSONResponse({"success": True, "message": f"Верифицировано {verified_count} активных пользователей с квартирой"})
@app.post("/api/user/{user_id}/update")
async def api_update_user(user_id: int, request: Request,
username: str = Depends(get_current_admin)):
"""Обновить пользователя"""
data = await request.json()
async with AsyncSessionLocal() as session:
update_data = {}
if 'apartment' in data: update_data['apartment'] = data['apartment'].upper()
if 'phone' in data: update_data['phone'] = data['phone']
if 'verified' in data:
update_data['verified'] = bool(data['verified'])
if data['verified']: update_data['verification_date'] = datetime.utcnow()
if 'banned' in data: update_data['is_banned'] = bool(data['banned'])
if 'rating' in data: update_data['rating'] = int(data['rating'])
if update_data:
await session.execute(update(User).where(User.user_id == user_id).values(**update_data))
await session.commit()
return JSONResponse({"success": True})
@app.get("/api/user/{user_id}")
async def api_get_user(user_id: int, username: str = Depends(get_current_admin)):
"""Получить данные пользователя"""
async with AsyncSessionLocal() as session:
user = await session.get(User, user_id)
if not user:
return JSONResponse({"error": "Пользователь не найден"}, status_code=404)
return JSONResponse({
"user": {
"id": user.user_id,
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"apartment": user.apartment,
"phone": user.phone,
"verified": user.verified,
"rating": user.rating,
"message_count": user.message_count,
"is_banned": user.is_banned,
}
})
@app.post("/api/user/{user_id}/ban")
async def api_ban_user(user_id: int, username: str = Depends(get_current_admin)):
"""Забанить"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(is_banned=True))
await session.commit()
return JSONResponse({"success": True, "message": "Забанен"})
@app.post("/api/user/{user_id}/unban")
async def api_unban_user(user_id: int, username: str = Depends(get_current_admin)):
"""Разбанить"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(is_banned=False))
await session.commit()
return JSONResponse({"success": True, "message": "Разбанен"})
@app.post("/api/user/{user_id}/verify")
async def api_verify_user(user_id: int, username: str = Depends(get_current_admin)):
"""Верифицировать"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(
verified=True, verification_date=datetime.utcnow()))
await session.commit()
return JSONResponse({"success": True, "message": "Верифицирован"})
@app.post("/api/user/{user_id}/unverify")
async def api_unverify_user(user_id: int, username: str = Depends(get_current_admin)):
"""Отозвать верификацию"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(verified=False))
await session.commit()
return JSONResponse({"success": True, "message": "Верификация отозвана"})
@app.post("/api/user/{user_id}/make_admin")
async def api_make_admin(user_id: int, username: str = Depends(get_current_admin)):
"""Назначить админом"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(is_admin=True))
await session.commit()
return JSONResponse({"success": True, "message": "Пользователь теперь админ"})
@app.post("/api/user/{user_id}/remove_admin")
async def api_remove_admin(user_id: int, username: str = Depends(get_current_admin)):
"""Снять админа"""
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == user_id).values(is_admin=False))
await session.commit()
return JSONResponse({"success": True, "message": "Админ снят"})
@app.post("/api/user/{user_id}/add_to_ig")
async def api_add_to_ig(user_id: int, username: str = Depends(get_current_admin)):
"""Добавить в Инициативную Группу"""
from services.initiative_group import InitiativeGroupService
async with AsyncSessionLocal() as session:
ig_service = InitiativeGroupService(session)
success = await ig_service.add_member(user_id, 'member')
if success:
return JSONResponse({"success": True, "message": "Добавлен в ИГ"})
else:
return JSONResponse({"error": "Не удалось добавить в ИГ"}, status_code=400)
@app.post("/api/user/{user_id}/remove_from_ig")
async def api_remove_from_ig(user_id: int, username: str = Depends(get_current_admin)):
"""Удалить из Инициативной Группы"""
from services.initiative_group import InitiativeGroupService
async with AsyncSessionLocal() as session:
ig_service = InitiativeGroupService(session)
success = await ig_service.remove_member(user_id)
if success:
return JSONResponse({"success": True, "message": "Удалён из ИГ"})
else:
return JSONResponse({"error": "Не найден в ИГ"}, status_code=400)
# ============================================================================
# ВЕРИФИКАЦИЯ
# ============================================================================
@app.get("/verification", response_class=HTMLResponse)
async def verification_page(request: Request, username: str = Depends(get_current_admin)):
"""Верификация"""
async with AsyncSessionLocal() as session:
pending = list((await session.execute(
select(VerificationRequest).where(VerificationRequest.status == 'pending')
.order_by(VerificationRequest.created_at.desc()).limit(50)
)).scalars().all())
unverified = list((await session.execute(
select(User).where(User.verified == False).order_by(User.join_date.desc()).limit(50)
)).scalars().all())
return templates.TemplateResponse("verification.html", {
"request": request, "username": username,
"pending_requests": pending, "unverified_users": unverified
})
@app.post("/api/verification/{req_id}/approve")
async def api_approve_verification(req_id: int, username: str = Depends(get_current_admin)):
"""Одобрить верификацию"""
async with AsyncSessionLocal() as session:
req = (await session.execute(select(VerificationRequest).where(
VerificationRequest.id == req_id))).scalar_one_or_none()
if req:
req.status = 'approved'
req.reviewed_at = datetime.utcnow()
await session.execute(update(User).where(User.user_id == req.user_id).values(
verified=True, apartment=req.apartment, verification_date=datetime.utcnow()))
await session.commit()
return JSONResponse({"success": True})
@app.post("/api/verification/{req_id}/reject")
async def api_reject_verification(req_id: int, username: str = Depends(get_current_admin)):
"""Отклонить верификацию"""
async with AsyncSessionLocal() as session:
req = (await session.execute(select(VerificationRequest).where(
VerificationRequest.id == req_id))).scalar_one_or_none()
if req:
req.status = 'rejected'
req.reviewed_at = datetime.utcnow()
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# ТЕЛЕФОНЫ И СЛУЖБЫ
# ============================================================================
@app.get("/phones", response_class=HTMLResponse)
async def phones_page(request: Request, username: str = Depends(get_current_admin)):
"""Телефоны и службы"""
async with AsyncSessionLocal() as session:
services = list((await session.execute(select(Service).order_by(Service.category, Service.name)
)).scalars().all())
return templates.TemplateResponse("phones.html", {
"request": request, "username": username, "services": services
})
@app.post("/api/service/add")
async def api_add_service(request: Request, username: str = Depends(get_current_admin)):
"""Добавить службу"""
data = await request.json()
async with AsyncSessionLocal() as session:
service = Service(
name=data['name'],
phone=data['phone'],
category=data.get('category', 'other'),
description=data.get('description', ''),
verified_by_admin=True,
image_path=data.get('image_path', None)
)
session.add(service)
await session.commit()
return JSONResponse({"success": True, "message": "Служба добавлена"})
@app.post("/api/service/upload_image")
async def api_upload_service_image(
file: UploadFile = File(...),
category: str = Form(...),
username: str = Depends(get_current_admin)
):
"""Загрузить картинку для службы"""
import os
import uuid
from pathlib import Path
# Создаём папку для картинок
images_dir = Path("data/service_images")
images_dir.mkdir(parents=True, exist_ok=True)
# Генерируем уникальное имя файла
file_extension = file.filename.split(".")[-1] if "." in file.filename else "jpg"
unique_filename = f"{category}_{uuid.uuid4().hex}.{file_extension}"
file_path = images_dir / unique_filename
# Сохраняем файл
try:
with open(file_path, "wb") as f:
content = await file.read()
f.write(content)
# Обновляем все службы этой категории
async with AsyncSessionLocal() as session:
await session.execute(
update(Service)
.where(Service.category == category)
.values(image_path=str(file_path))
)
await session.commit()
return JSONResponse({
"success": True,
"message": f"Картинка загружена",
"file_path": str(file_path)
})
except Exception as e:
logger.error(f"Ошибка загрузки картинки: {e}")
return JSONResponse({
"success": False,
"error": str(e)
}, status_code=500)
@app.post("/api/service/{service_id}/update")
async def api_update_service(service_id: int, request: Request,
username: str = Depends(get_current_admin)):
"""Обновить службу"""
data = await request.json()
async with AsyncSessionLocal() as session:
await session.execute(update(Service).where(Service.id == service_id).values(**data))
await session.commit()
return JSONResponse({"success": True})
@app.post("/api/service/{service_id}/delete")
async def api_delete_service(service_id: int, username: str = Depends(get_current_admin)):
"""Удалить службу"""
async with AsyncSessionLocal() as session:
await session.execute(delete(Service).where(Service.service_id == service_id))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# ОБЪЯВЛЕНИЯ
# ============================================================================
@app.get("/ads", response_class=HTMLResponse)
async def ads_page(request: Request, username: str = Depends(get_current_admin)):
"""Объявления"""
async with AsyncSessionLocal() as session:
ads = list((await session.execute(select(Ad).order_by(Ad.created_at.desc()).limit(100)
)).scalars().all())
return templates.TemplateResponse("ads.html", {
"request": request, "username": username, "ads": ads
})
@app.post("/api/ad/add")
async def api_add_ad(request: Request, username: str = Depends(get_current_admin)):
"""Добавить объявление"""
data = await request.json()
async with AsyncSessionLocal() as session:
ad = Ad(
title=data['title'],
description=data.get('text', ''),
category=data.get('category', 'other'),
user_id=config.ADMIN_USER_ID,
is_active=data.get('is_active', True),
is_moderated=True
)
session.add(ad)
await session.commit()
return JSONResponse({"success": True, "message": "Объявление добавлено"})
@app.post("/api/ad/{ad_id}/delete")
async def api_delete_ad(ad_id: int, username: str = Depends(get_current_admin)):
"""Удалить объявление"""
async with AsyncSessionLocal() as session:
await session.execute(update(Ad).where(Ad.ad_id == ad_id).values(is_active=False))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# РАССЫЛКИ — единая страница создания и статистики
# ============================================================================
@app.get("/broadcast", response_class=HTMLResponse)
async def broadcast_page(request: Request, username: str = Depends(get_current_admin)):
"""Рассылки — создание и статистика"""
from database.models import Broadcast, BroadcastRead
from sqlalchemy import func
async with AsyncSessionLocal() as session:
# История рассылок
stmt = select(Broadcast).order_by(Broadcast.created_at.desc()).limit(50)
result = await session.execute(stmt)
broadcasts = list(result.scalars().all())
# Считаем прочтения для каждой
broadcasts_data = []
for b in broadcasts:
read_count = (await session.execute(
select(func.count(BroadcastRead.id)).where(BroadcastRead.broadcast_id == b.id)
)).scalar() or 0
broadcasts_data.append({
'id': b.id,
'text': b.text[:100] + '...' if len(b.text) > 100 else b.text,
'sent_at': fmt_local(b.sent_at) if b.sent_at else '-',
'total_sent': b.total_sent,
'read_count': read_count,
'unread_count': b.total_sent - read_count,
'read_percent': round((read_count / b.total_sent * 100), 1) if b.total_sent > 0 else 0,
'is_reminder_sent': b.is_reminder_sent,
'broadcast_type': b.broadcast_type or 'regular',
})
return templates.TemplateResponse("broadcast.html", {
"request": request,
"username": username,
"broadcasts": broadcasts_data,
})
# ============================================================================
# ЛОГИРОВАНИЕ АКТИВНОСТИ
# ============================================================================
@app.get("/api/activity_log")
async def api_get_activity_log(
username: str = Depends(get_current_admin),
action_type: str = "",
days: int = 30
):
"""Получить логи активности"""
from sqlalchemy import text, desc
async with AsyncSessionLocal() as session:
stmt = text('''
SELECT * FROM activity_log
WHERE (:action_type = '' OR action_type = :action_type)
AND created_at >= datetime('now', '-' || :days || ' days')
ORDER BY created_at DESC
LIMIT 100
''')
result = await session.execute(stmt, {"action_type": action_type, "days": str(days)})
logs = [
{
"id": row[0],
"user_id": row[1],
"action_type": row[2],
"action_data": row[3],
"recipients_count": row[4],
"created_at": row[5]
}
for row in result.all()
]
return JSONResponse({"success": True, "logs": logs})
# ============================================================================
# ОПРОСЫ
# ============================================================================
@app.get("/polls", response_class=HTMLResponse)
async def polls_page(request: Request, username: str = Depends(get_current_admin)):
"""Опросы"""
async with AsyncSessionLocal() as session:
polls = list((await session.execute(select(Poll).order_by(Poll.created_at.desc()).limit(50)
)).scalars().all())
return templates.TemplateResponse("polls.html", {
"request": request, "username": username, "polls": polls
})
@app.post("/api/poll/create")
async def api_create_poll(request: Request, username: str = Depends(get_current_admin)):
"""Создать опрос"""
data = await request.json()
async with AsyncSessionLocal() as session:
poll = Poll(
question=data['question'],
options=json.dumps(data['options']),
votes='{}',
created_by=config.ADMIN_USER_ID,
is_active=data.get('is_active', True),
message_id=0, # Заглушка
chat_id=config.ADMIN_CHAT_ID
)
session.add(poll)
await session.commit()
return JSONResponse({"success": True, "message": "Опрос создан"})
@app.post("/api/poll/{poll_id}/close")
async def api_close_poll(poll_id: int, username: str = Depends(get_current_admin)):
"""Закрыть опрос"""
async with AsyncSessionLocal() as session:
await session.execute(update(Poll).where(Poll.poll_id == poll_id).values(is_active=False))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# СОБЫТИЯ
# ============================================================================
@app.get("/events", response_class=HTMLResponse)
async def events_page(request: Request, username: str = Depends(get_current_admin)):
"""События"""
async with AsyncSessionLocal() as session:
events = list((await session.execute(select(Event).order_by(Event.event_date.desc()).limit(50)
)).scalars().all())
return templates.TemplateResponse("events.html", {
"request": request, "username": username, "events": events
})
@app.post("/api/event/add")
async def api_add_event(request: Request, username: str = Depends(get_current_admin)):
"""Добавить событие"""
data = await request.json()
async with AsyncSessionLocal() as session:
event = Event(
title=data['title'], description=data.get('description', ''),
event_type=data.get('event_type', 'meeting'),
event_date=datetime.fromisoformat(data['event_date']),
created_by=config.ADMIN_USER_ID,
is_active=data.get('is_active', True)
)
session.add(event)
await session.commit()
return JSONResponse({"success": True, "message": "Событие добавлено"})
# ============================================================================
# РАСПИСАНИЯ
# ============================================================================
@app.get("/schedules", response_class=HTMLResponse)
async def schedules_page(request: Request, username: str = Depends(get_current_admin)):
"""Расписания"""
async with AsyncSessionLocal() as session:
schedules = list((await session.execute(select(Schedule).order_by(Schedule.start_time.desc())
)).scalars().all())
return templates.TemplateResponse("schedules.html", {
"request": request, "username": username, "schedules": schedules
})
@app.post("/api/schedule/add")
async def api_add_schedule(request: Request, username: str = Depends(get_current_admin)):
"""Добавить расписание"""
data = await request.json()
async with AsyncSessionLocal() as session:
schedule = Schedule(
title=data['title'], schedule_type=data.get('schedule_type', 'water'),
start_time=datetime.fromisoformat(data['start_time']),
end_time=datetime.fromisoformat(data['end_time']) if data.get('end_time') else None,
description=data.get('description', ''), is_active=data.get('is_active', True)
)
session.add(schedule)
await session.commit()
return JSONResponse({"success": True, "message": "Расписание добавлено"})
@app.post("/api/schedule/{schedule_id}/delete")
async def api_delete_schedule(schedule_id: int, username: str = Depends(get_current_admin)):
"""Удалить расписание"""
async with AsyncSessionLocal() as session:
await session.execute(update(Schedule).where(
Schedule.schedule_id == schedule_id).values(is_active=False))
await session.commit()
return JSONResponse({"success": True})
# ============================================================================
# ЭКСПОРТ
# ============================================================================
@app.get("/export", response_class=HTMLResponse)
async def export_page(request: Request, username: str = Depends(get_current_admin)):
"""Экспорт"""
return templates.TemplateResponse("export.html", {
"request": request, "username": username
})
@app.get("/api/export/json")
async def api_export_json(username: str = Depends(get_current_admin)):
"""Экспорт JSON"""
from services.analytics import ChatAnalytics
async with AsyncSessionLocal() as session:
analytics = ChatAnalytics(session)
filepath = await analytics.export_to_json()
return JSONResponse({"success": True, "filepath": filepath})
# ============================================================================
# ЗАПЛАНИРОВАННЫЕ ПОСТЫ
# ============================================================================
@app.get("/scheduled_posts", response_class=HTMLResponse)
async def scheduled_posts_page(request: Request, username: str = Depends(get_current_admin)):
"""Страница запланированных постов"""
return templates.TemplateResponse("scheduled_posts.html", {
"request": request,
"username": username
})
@app.post("/api/scheduled_posts/create")
async def api_create_scheduled_post(
text: str = Form(...),
topic_name: str = Form(...),
recipients: str = Form("chat_only"),
scheduled_time: str = Form(...),
photo: UploadFile = File(None),
document: UploadFile = File(None), # Новый параметр — документ
username: str = Depends(get_current_admin)
):
"""Создать запланированный пост (с фото ИЛИ документом)"""
from datetime import datetime
from config import ADMIN_CHAT_ID, get_topic_id, get_proxy_url
import aiohttp
logger.info(f"📅 СОЗДАНИЕ ЗАПЛАНИРОВАННОГО ПОСТА: тема={topic_name}, время={scheduled_time}")
try:
# Парсим время
scheduled_dt = datetime.fromisoformat(scheduled_time)
# Если нет timezone — считаем что это локальное время (UTC+4)
# и конвертируем в UTC для scheduler
if scheduled_dt.tzinfo is None:
from datetime import timezone as tz
local_tz = tz(timedelta(hours=4)) # Ульяновск UTC+4
scheduled_dt = scheduled_dt.replace(tzinfo=local_tz) # Помечаем как локальное
scheduled_dt = scheduled_dt.astimezone(tz.utc).replace(tzinfo=None) # Конвертируем в UTC
logger.info(f"⏰ Время конвертирована из локального в UTC: {scheduled_dt}")
else:
scheduled_dt = scheduled_dt.astimezone(tz.utc).replace(tzinfo=None)
logger.info(f"⏰ Время уже с timezone, конвертирована в UTC: {scheduled_dt}")
# Проверяем что время в будущем (по UTC)
if scheduled_dt < datetime.utcnow():
return JSONResponse({
"success": False,
"error": "Время должно быть в будущем"
}, status_code=400)
# Получаем topic_id из topic_name
topic_id = get_topic_id(topic_name)
# Если есть фото - загружаем в Telegram
photo_file_id = None
if photo and photo.filename:
temp_path = Path("data") / f"scheduled_post_{photo.filename}"
with open(temp_path, "wb") as f:
content = await photo.read()
f.write(content)
try:
async with aiohttp.ClientSession() as session:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendPhoto"
data = aiohttp.FormData()
data.add_field('chat_id', str(config.ADMIN_USER_ID))
data.add_field('photo', open(temp_path, 'rb'), filename=photo.filename)
data.add_field('caption', 'Preview')
async with session.post(url, data=data, proxy=get_proxy_url()) as resp:
result = await resp.json()
if result.get('ok'):
photo_file_id = result['result']['photo'][-1]['file_id']
except Exception as e:
logger.error(f"Ошибка загрузки фото: {e}")
photo_file_id = None
# Если есть документ - загружаем в Telegram
document_file_id = None
document_name = None
if document and document.filename:
# Проверяем расширение
allowed_ext = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.zip', '.rar']
file_ext = Path(document.filename).suffix.lower()
if file_ext not in allowed_ext:
return JSONResponse({
"success": False,
"error": f"Неподдерживаемый формат файла. Разрешены: {', '.join(allowed_ext)}"
}, status_code=400)
# Сохраняем временно
temp_path = Path("data") / f"scheduled_doc_{document.filename}"
with open(temp_path, "wb") as f:
content = await document.read()
f.write(content)
try:
async with aiohttp.ClientSession() as session:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendDocument"
data = aiohttp.FormData()
data.add_field('chat_id', str(config.ADMIN_USER_ID))
with open(temp_path, 'rb') as doc_file:
data.add_field('document', doc_file, filename=document.filename)
data.add_field('caption', 'Document preview')
async with session.post(url, data=data, proxy=get_proxy_url()) as resp:
result = await resp.json()
if result.get('ok'):
document_file_id = result['result']['document']['file_id']
document_name = document.filename
logger.info(f"✅ Документ загружен: {document_name}, file_id={document_file_id}")
else:
error_msg = result.get('description', 'Неизвестная ошибка')
logger.error(f"Ошибка загрузки документа: {error_msg}")
except Exception as e:
logger.error(f"Ошибка загрузки документа: {e}")
document_file_id = None
document_name = None
# Удаляем временный файл
if temp_path.exists():
temp_path.unlink()
# Сохраняем в БД
async with AsyncSessionLocal() as session:
scheduled_post = ScheduledPost(
text=text,
photo_file_id=photo_file_id,
document_file_id=document_file_id,
document_name=document_name,
topic_id=topic_id,
topic_name=topic_name,
recipients=recipients,
scheduled_time=scheduled_dt,
status='pending',
created_by=config.ADMIN_USER_ID
)
session.add(scheduled_post)
await session.commit()
await session.refresh(scheduled_post)
logger.info(f"✅ Пост создан: id={scheduled_post.id}")
attachment_info = ""
if document_name:
attachment_info = f" + 📄 {document_name}"
elif photo_file_id:
attachment_info = " + 📷 фото"
return JSONResponse({
"success": True,
"message": f"Пост запланирован на {scheduled_dt.strftime('%d.%m.%Y %H:%M')}{attachment_info}",
"post_id": scheduled_post.id
})
except ValueError as e:
return JSONResponse({
"success": False,
"error": f"Неверный формат времени: {e}"
}, status_code=400)
except Exception as e:
logger.error(f"Ошибка создания поста: {e}")
return JSONResponse({
"success": False,
"error": str(e)
}, status_code=500)
@app.get("/api/scheduled_posts/list")
async def api_list_scheduled_posts(username: str = Depends(get_current_admin)):
"""Список запланированных постов"""
from datetime import timedelta
async with AsyncSessionLocal() as session:
stmt = select(ScheduledPost).order_by(ScheduledPost.scheduled_time.desc())
result = await session.execute(stmt)
posts = list(result.scalars().all())
# Часовой пояс пользователя (UTC+4 для Ульяновска)
user_tz_offset = timedelta(hours=4) # Europe/Ulyanovsk
posts_data = []
for post in posts:
# Конвертируем UTC в локальное время
local_time = post.scheduled_time + user_tz_offset
posts_data.append({
"id": post.id,
"text": post.text,
"topic_name": post.topic_name,
"topic_emoji": post.get_topic_emoji(),
"recipients": post.recipients,
"scheduled_time": local_time.strftime('%d.%m.%Y %H:%M'), # Уже локальное!
"status": post.status,
"status_emoji": post.get_status_emoji(),
"created_at": fmt_local(post.created_at),
"has_photo": bool(post.photo_file_id)
})
return JSONResponse({
"success": True,
"posts": posts_data,
"timezone": "UTC+4 (Ульяновск/Самара)"
})
@app.post("/api/scheduled_posts/{post_id}/cancel")
async def api_cancel_scheduled_post(post_id: int, username: str = Depends(get_current_admin)):
"""Отменить запланированный пост"""
async with AsyncSessionLocal() as session:
stmt = select(ScheduledPost).where(ScheduledPost.id == post_id)
result = await session.execute(stmt)
post = result.scalar_one_or_none()
if not post:
return JSONResponse({
"success": False,
"error": "Пост не найден"
}, status_code=404)
if post.status != 'pending':
return JSONResponse({
"success": False,
"error": f"Нельзя отменить пост со статусом {post.status}"
}, status_code=400)
post.status = 'cancelled'
await session.commit()
logger.info(f"🚫 Пост отменён: id={post_id}")
return JSONResponse({
"success": True,
"message": "Пост отменён"
})
# ============================================================================
# НАСТРОЙКИ (перенесено в /broadcast)
# ============================================================================
# Настройка лимита рассылки ИГ теперь в /broadcast
@app.post("/api/settings/ig_broadcast_limit")
async def api_set_ig_broadcast_limit(
request: Request,
username: str = Depends(get_current_admin)
):
"""Установить лимит рассылки ИГ"""
from sqlalchemy import text
data = await request.json()
limit = data.get('limit', 0)
async with AsyncSessionLocal() as session:
await session.execute(text('''
INSERT OR REPLACE INTO settings (key, value, description)
VALUES ('ig_broadcast_limit', :limit, 'Лимит рассылки ИГ в секундах')
'''), {"limit": str(limit)})
await session.commit()
return JSONResponse({"success": True, "message": f"Лимит установлен: {limit} сек"})
@app.get("/api/settings/ig_broadcast_limit")
async def api_get_ig_broadcast_limit(
username: str = Depends(get_current_admin)
):
"""Получить лимит рассылки ИГ"""
from sqlalchemy import text
async with AsyncSessionLocal() as session:
result = await session.execute(text('''
SELECT value FROM settings WHERE key = 'ig_broadcast_limit'
'''))
row = result.first()
if row:
return JSONResponse({"success": True, "value": row[0]})
else:
return JSONResponse({"success": True, "value": "0"})
def run_web_server(host: str = "0.0.0.0", port: int = 8000):
"""Запуск веб-сервера"""
import uvicorn
uvicorn.run(app, host=host, port=port)
@app.post("/api/broadcast/send_reminder/{broadcast_id}")
async def api_send_broadcast_reminder(broadcast_id: int, username: str = Depends(get_current_admin)):
"""Отправить напоминание непрочитавшим"""
from handlers.smart_broadcast import send_reminder_to_unread
from bot_instance import get_bot
bot = get_bot()
sent_count = await send_reminder_to_unread(bot, broadcast_id)
return JSONResponse({
"success": True,
"message": f"Напоминание отправлено {sent_count} пользователям"
})
@app.get("/api/broadcasts/list")
async def api_get_broadcasts_list(limit: int = 10, username: str = Depends(get_current_admin)):
"""Получить список последних рассылок"""
from database.models import Broadcast, BroadcastRead
from sqlalchemy import func, desc
async with AsyncSessionLocal() as session:
stmt = select(Broadcast).order_by(Broadcast.created_at.desc()).limit(limit)
result = await session.execute(stmt)
broadcasts = list(result.scalars().all())
total = (await session.execute(select(func.count(Broadcast.id)))).scalar() or 0
broadcasts_data = []
for b in broadcasts:
read_count = (await session.execute(
select(func.count(BroadcastRead.id)).where(BroadcastRead.broadcast_id == b.id)
)).scalar() or 0
broadcasts_data.append({
'id': b.id,
'text': b.text,
'sent_at': fmt_local(b.sent_at) if b.sent_at else '-',
'total_sent': b.total_sent,
'read_count': read_count,
'read_percent': round((read_count / b.total_sent * 100), 1) if b.total_sent > 0 else 0,
'broadcast_type': b.broadcast_type,
})
return JSONResponse({
"success": True,
"total": total,
"broadcasts": broadcasts_data
})
@app.post("/api/broadcast/create")
async def api_create_broadcast(
text: str = Form(...),
photo: UploadFile = File(None),
recipients: str = Form("all_and_chat"),
has_read_button: bool = Form(True),
dry_run: bool = Form(False), # Режим теста БЕЗ отправки
username: str = Depends(get_current_admin)
):
"""Создать и отправить рассылку из веб-панели"""
# dry_run = тестирование без реальной отправки
if dry_run:
from database.models import User
from services.initiative_group import InitiativeGroupService
async with AsyncSessionLocal() as session:
if recipients == 'ig_only':
ig_service = InitiativeGroupService(session)
ig_user_ids = await ig_service.get_member_ids(active_only=True)
stmt = select(User).where(User.user_id.in_(ig_user_ids))
elif recipients == 'all_verified':
stmt = select(User).where(User.verified == True)
else:
stmt = select(User).where(User.verified == True)
result = await session.execute(stmt)
users = list(result.scalars().all())
logger.info(f"🧪 DRY RUN рассылки: получатели={recipients}, пользователей={len(users)}")
return JSONResponse({
"success": True,
"dry_run": True,
"message": f"Тестовый режим. Рассылка получила бы {len(users)} пользователей",
"recipients_count": len(users)
})
import aiohttp
from database.models import Broadcast
from datetime import datetime
photo_file_id = None
# Если есть фото - загружаем через Telegram Bot API
if photo and photo.filename:
try:
# Сохраняем временно
temp_path = Path("data") / f"broadcast_{photo.filename}"
content = await photo.read()
with open(temp_path, "wb") as f:
f.write(content)
# Загружаем фото через Telegram API для получения file_id
async with aiohttp.ClientSession() as session:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendPhoto"
# Если бот использует прокси - добавляем его в aiohttp
proxy = config.get_proxy_url()
data = aiohttp.FormData()
data.add_field('chat_id', config.ADMIN_USER_ID)
# Открываем файл и сразу передаём в FormData
with open(temp_path, 'rb') as photo_file:
data.add_field('photo', photo_file, filename=photo.filename)
data.add_field('caption', 'broadcast_test')
data.add_field('parse_mode', 'HTML')
async with session.post(url, data=data, proxy=proxy) as resp:
text = await resp.text()
# Проверяем что ответ успешный
if resp.status != 200:
logger.error(f"Ошибка загрузки фото: HTTP {resp.status}, ответ: {text}")
raise Exception(f"Telegram API вернул ошибку: HTTP {resp.status}")
# Пытаемся распарсить JSON
try:
result = json.loads(text)
except json.JSONDecodeError as e:
logger.error(f"Ошибка парсинга JSON от Telegram API: {e}, ответ: {text}")
raise Exception(f"Telegram API вернул не-JSON ответ: {text[:100]}")
if result.get('ok'):
photo_file_id = result['result']['photo'][-1]['file_id']
logger.info(f"✅ Фото загружено, file_id: {photo_file_id}")
else:
error_msg = result.get('description', 'Неизвестная ошибка')
logger.error(f"Ошибка загрузки фото: {error_msg}")
raise Exception(f"Ошибка Telegram API: {error_msg}")
# Удаляем временный файл
if temp_path.exists():
temp_path.unlink()
except Exception as e:
logger.error(f"❌ Ошибка загрузки фото для рассылки: {e}")
# Не падаем, просто продолжаем без фото
photo_file_id = None
# Получаем бота из глобального экземпляра
from bot_instance import get_bot
bot = get_bot()
# Отправляем рассылку
try:
from handlers.smart_broadcast import send_smart_broadcast
success = await send_smart_broadcast(bot, text, recipients, photo_file_id)
if success:
return JSONResponse({
"success": True,
"message": f"Рассылка отправлена получателям: {recipients}"
})
else:
return JSONResponse({
"error": "Ошибка при отправке рассылки. Проверьте логи бота."
}, status_code=500)
except Exception as e:
logger.error(f"❌ Критическая ошибка при отправке рассылки: {e}")
import traceback
traceback.print_exc()
return JSONResponse({
"error": f"Критическая ошибка: {str(e)}"
}, status_code=500)
@app.get("/api/broadcast/readers/{broadcast_id}")
async def api_get_broadcast_readers(broadcast_id: int, username: str = Depends(get_current_admin)):
"""Получить список прочитавших"""
from database.models import BroadcastRead, User
async with AsyncSessionLocal() as session:
stmt = (
select(User, BroadcastRead.read_at)
.join(BroadcastRead, User.user_id == BroadcastRead.user_id)
.where(BroadcastRead.broadcast_id == broadcast_id)
.order_by(BroadcastRead.read_at.desc())
)
result = await session.execute(stmt)
rows = result.all()
readers = [
{
'user_id': user.user_id,
'name': user.full_name,
'apartment': user.apartment,
'read_at': fmt_local(read_at),
}
for user, read_at in rows
]
return JSONResponse({"success": True, "readers": readers})
@app.get("/api/broadcast/non_readers/{broadcast_id}")
async def api_get_broadcast_non_readers(broadcast_id: int, username: str = Depends(get_current_admin)):
"""Получить список НЕ прочитавших"""
from database.models import BroadcastRead, User
from sqlalchemy import select as sql_select, not_
async with AsyncSessionLocal() as session:
# Кто прочитал
read_stmt = select(BroadcastRead.user_id).where(BroadcastRead.broadcast_id == broadcast_id)
read_result = await session.execute(read_stmt)
read_user_ids = set(row[0] for row in read_result.all())
# Все верифицированные
all_stmt = select(User).where(User.verified == True)
all_result = await session.execute(all_stmt)
all_users = list(all_result.scalars().all())
# Кто НЕ прочитал
non_readers = [
{
'user_id': u.user_id,
'name': u.full_name,
'apartment': u.apartment,
}
for u in all_users if u.user_id not in read_user_ids
]
return JSONResponse({"success": True, "non_readers": non_readers})
# ============================================================================
# V3.1 - ДАЙДЖЕСТЫ
# ============================================================================
@app.post("/api/digest/generate")
async def api_generate_digest(username: str = Depends(get_current_admin)):
"""Сгенерировать дайджест прямо сейчас (из веб-панели)"""
from services.digest_service import DigestService
from database.models import Digest
from datetime import date
try:
async with AsyncSessionLocal() as session:
service = DigestService(session)
# Генерируем
digest_data = await service.generate_weekly_digest()
if not digest_data:
return JSONResponse({
"success": False,
"error": "Не удалось сгенерировать дайджест. Проверьте логи."
}, status_code=500)
# Проверяем нет ли уже за эту неделю
check_stmt = select(Digest).where(
Digest.week_number == digest_data['week_number'],
Digest.year == digest_data['year']
)
result = await session.execute(check_stmt)
existing = result.scalar_one_or_none()
if existing:
return JSONResponse({
"success": True,
"message": f"Дайджест за неделю #{digest_data['week_number']}/{digest_data['year']} уже существует (ID: #{existing.id}). Вы можете отправить его в разделе 'Архив'.",
"digest_id": existing.id,
"already_exists": True
})
# Сохраняем
digest_id = await service.save_digest(digest_data, status='pending_approval')
# Отправляем превью админу в личку через HTTP
from config import get_proxy_url
async with aiohttp.ClientSession() as http_session:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
keyboard = {
"inline_keyboard": [
[{"text": "🌐 Утвердить в веб-панели", "url": "http://localhost:8000/digests"}]
]
}
payload = {
'chat_id': config.ADMIN_USER_ID,
'text': f"📰 <b>Дайджест за неделю готов!</b>\n\n"
f"Неделя #{digest_data['week_number']}/{digest_data['year']}\n"
f"Период: {digest_data['period_start'].strftime('%d.%m')} - {digest_data['period_end'].strftime('%d.%m.%Y')}\n\n"
f"Зайдите в веб-панель для утверждения:\n"
f"http://localhost:8000/digests",
'parse_mode': 'HTML',
'reply_markup': json.dumps(keyboard)
}
async with http_session.post(url, json=payload, proxy=get_proxy_url()) as resp:
await resp.json()
return JSONResponse({
"success": True,
"message": f"✅ Дайджест #{digest_id} сгенерирован! Превью отправлено вам в Telegram.",
"digest_id": digest_id
})
except Exception as e:
import traceback
logger.error(f"Ошибка генерации дайджеста: {e}")
traceback.print_exc()
return JSONResponse({
"success": False,
"error": f"Критическая ошибка: {str(e)}"
}, status_code=500)
@app.get("/digests", response_class=HTMLResponse)
async def digests_page(request: Request, username: str = Depends(get_current_admin)):
"""Страница дайджестов"""
from services.digest_service import DigestService
async with AsyncSessionLocal() as session:
service = DigestService(session)
pending = await service.get_pending_digests()
archive = await service.get_digest_archive()
return templates.TemplateResponse("digests.html", {
"request": request,
"username": username,
"pending_digests": pending,
"archive_digests": archive,
})
@app.post("/api/digest/{digest_id}/approve")
async def api_approve_digest(digest_id: int, username: str = Depends(get_current_admin)):
"""Утвердить дайджест"""
from services.digest_service import DigestService
async with AsyncSessionLocal() as session:
service = DigestService(session)
success = await service.update_digest_status(
digest_id,
'approved',
approved_by=config.ADMIN_USER_ID
)
if success:
return JSONResponse({"success": True, "message": "Дайджест утверждён"})
else:
return JSONResponse({"error": "Ошибка утверждения"}, status_code=500)
@app.post("/api/digest/{digest_id}/reject")
async def api_reject_digest(digest_id: int, request: Request, username: str = Depends(get_current_admin)):
"""Отклонить дайджест"""
from services.digest_service import DigestService
data = await request.json()
reason = data.get('reason', 'Не указана')
async with AsyncSessionLocal() as session:
service = DigestService(session)
success = await service.update_digest_status(
digest_id,
'rejected',
rejected_by=config.ADMIN_USER_ID,
rejection_reason=reason
)
if success:
return JSONResponse({"success": True, "message": "Дайджест отклонён"})
else:
return JSONResponse({"error": "Ошибка отклонения"}, status_code=500)
@app.post("/api/digest/{digest_id}/send")
async def api_send_digest(digest_id: int, username: str = Depends(get_current_admin)):
"""Отправить утверждённый дайджест"""
from services.digest_service import DigestService
from database.models import User, Digest
import aiohttp
async with AsyncSessionLocal() as session:
# Получаем дайджест
digest = await session.get(Digest, digest_id)
if not digest or digest.status != 'approved':
return JSONResponse({"error": "Дайджест не утверждён"}, status_code=400)
# Получаем всех верифицированных
stmt = select(User).where(User.verified == True)
result = await session.execute(stmt)
users = list(result.scalars().all())
text = digest.digest_text
sent_count = 0
error_count = 0
message_id = None
# Сначала в чат
try:
async with aiohttp.ClientSession() as http_session:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
params = {
'chat_id': config.ADMIN_CHAT_ID,
'text': f"📰 Еженедельный дайджест\n\n{text}",
'parse_mode': 'HTML'
}
async with http_session.post(url, json=params, timeout=30) as resp:
if resp.status == 200:
resp_data = await resp.json()
message_id = resp_data['result']['message_id']
except Exception as e:
logger.error(f"Ошибка отправки в чат: {e}")
# Потом пользователям
async with aiohttp.ClientSession() as http_session:
for user in users:
try:
url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
params = {
'chat_id': user.user_id,
'text': f"📰 Еженедельный дайджест\n\n{text}",
'parse_mode': 'HTML'
}
async with http_session.post(url, json=params, timeout=10) as resp:
if resp.status == 200:
sent_count += 1
except Exception as e:
logger.error(f"Ошибка отправки пользователю {user.user_id}: {e}")
error_count += 1
# Обновляем статус
service = DigestService(session)
await service.update_digest_status(digest_id, 'sent', message_id=message_id)
return JSONResponse({
"success": True,
"message": f"Дайджест отправлен {sent_count} пользователям, ошибок: {error_count}"
})
@app.get("/api/digest/{digest_id}")
async def api_get_digest(digest_id: int, username: str = Depends(get_current_admin)):
"""Получить данные дайджеста"""
from database.models import Digest
async with AsyncSessionLocal() as session:
digest = await session.get(Digest, digest_id)
if not digest:
return JSONResponse({"error": "Дайджест не найден"}, status_code=404)
return JSONResponse({
"digest": {
"id": digest.id,
"week": digest.week_number,
"year": digest.year,
"period_label": digest.period_label,
"status": digest.status,
"text": digest.digest_text or '',
"created_at": fmt_local(digest.created_at),
}
})
@app.post("/api/digest/{digest_id}/update_text")
async def api_update_digest_text(digest_id: int, request: Request, username: str = Depends(get_current_admin)):
"""Обновить текст дайджеста"""
from services.digest_service import DigestService
data = await request.json()
new_text = data.get('text', '')
async with AsyncSessionLocal() as session:
service = DigestService(session)
success = await service.update_digest_text(digest_id, new_text)
if success:
return JSONResponse({"success": True, "message": "Текст обновлён"})
else:
return JSONResponse({"error": "Ошибка обновления текста"}, status_code=500)
@app.get("/digest_settings", response_class=HTMLResponse)
async def digest_settings_page(request: Request, username: str = Depends(get_current_admin)):
"""Страница настроек дайджеста"""
return templates.TemplateResponse("digest_settings.html", {
"request": request,
"username": username,
})
if __name__ == "__main__":
run_web_server()