""" Web Admin Interface - Full Functional Admin Panel Веб-интерфейс с полным функционалом управления ботом """ import logging import json from fastapi import FastAPI, Request, Depends, HTTPException, status, Form, Query from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, update, delete from sqlalchemy.orm import Session from datetime import datetime, timedelta import secrets import config import aiohttp from database.db import AsyncSessionLocal, get_db from database.models import ( User, Message, Poll, Ad, PaymentReminder, Schedule, Achievement, Thank, Event, EventParticipant, InitiativeGroup, ProfileHistory, Announcement, Service, VerificationRequest, SpyLog ) logger = logging.getLogger(__name__) # Приложение app = FastAPI( title="Домовой Бот - Полная Админ Панель", description="Веб-интерфейс с полным функционалом", version="2.1.3" ) # Шаблоны templates = Jinja2Templates(directory="web/templates") # Статика app.mount("/static", StaticFiles(directory="web/static"), name="static") # Безопасность 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=status.HTTP_401_UNAUTHORIZED, 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))) users_count = users_count.scalar() messages_count = await session.execute(select(func.count(Message.id))) messages_count = messages_count.scalar() verified_count = await session.execute( select(func.count(User.user_id)).where(User.verified == True) ) verified_count = verified_count.scalar() ig_count = await session.execute( select(func.count(InitiativeGroup.id)).where(InitiativeGroup.is_active == True) ) ig_count = ig_count.scalar() return templates.TemplateResponse("dashboard.html", { "request": request, "username": username, "stats": { "users": users_count, "messages": messages_count, "verified": verified_count, "ig_members": ig_count } }) # ============================================================================= # ПОЛЬЗОВАТЕЛИ - API # ============================================================================= @app.get("/api/users") async def api_get_users( username: str = Depends(get_current_admin), search: str = Query(None), apartment: str = Query(None), verified: bool = Query(None), limit: int = Query(100) ): """Получить список пользователей с фильтрами""" async with AsyncSessionLocal() as session: stmt = select(User) if search: stmt = stmt.where( (User.full_name.ilike(f"%{search}%")) | (User.username.ilike(f"%{search}%")) ) if apartment: stmt = stmt.where(User.apartment.ilike(f"%{apartment}%")) if verified is not None: stmt = stmt.where(User.verified == verified) stmt = stmt.order_by(User.user_id.desc()).limit(limit) result = await session.execute(stmt) users = list(result.scalars().all()) return { "users": [ { "user_id": u.user_id, "full_name": u.full_name, "username": u.username, "apartment": u.apartment, "verified": u.verified, "rating": u.rating, "message_count": u.message_count, "is_admin": u.is_admin, "spy_score": u.spy_score, "toxicity_score": u.toxicity_score } for u in users ] } @app.post("/api/users/{user_id}/verify") async def api_verify_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: raise HTTPException(status_code=404, detail="Пользователь не найден") user.verified = True user.verification_date = datetime.utcnow() await session.commit() # Уведомить пользователя try: async with aiohttp.ClientSession() as http_session: await http_session.post( f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage", json={ "chat_id": user_id, "text": f"✅ Верификация подтверждена!\n\nТеперь у вас есть доступ ко всем функциям бота.", "parse_mode": "HTML" } ) except: pass return {"success": True, "message": f"Пользователь {user_id} верифицирован"} @app.post("/api/users/{user_id}/unverify") async def api_unverify_user( user_id: int, reason: str = Form(None), username: str = Depends(get_current_admin) ): """Снять верификацию с пользователя""" async with AsyncSessionLocal() as session: user = await session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="Пользователь не найден") user.verified = False await session.commit() # Уведомить пользователя try: async with aiohttp.ClientSession() as http_session: await http_session.post( f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage", json={ "chat_id": user_id, "text": f"⚠️ Верификация отозвана\n\nПричина: {reason or 'Без указания причины'}\n\nУ вас есть 7 дней на повторную верификацию.", "parse_mode": "HTML" } ) except: pass return {"success": True, "message": f"Верификация пользователя {user_id} отозвана"} @app.post("/api/users/{user_id}/set_admin") async def api_set_admin( user_id: int, is_admin: bool = Form(True), username: str = Depends(get_current_admin) ): """Назначить/снять права админа""" async with AsyncSessionLocal() as session: user = await session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="Пользователь не найден") user.is_admin = is_admin await session.commit() return {"success": True, "message": f"Пользователь {user_id} {'назначен' if is_admin else 'снят с должности'} админа"} @app.get("/api/initiative_group") async def api_get_ig_members(username: str = Depends(get_current_admin)): """Получить список участников ИГ""" async with AsyncSessionLocal() as session: stmt = select(InitiativeGroup).where(InitiativeGroup.is_active == True) result = await session.execute(stmt) members = list(result.scalars().all()) return { "members": [ { "user_id": m.user_id, "role": m.role, "joined_at": m.joined_at.isoformat(), "name": m.user.full_name if m.user else f"user{m.user_id}", "apartment": m.user.apartment if m.user else None } for m in members ] } @app.post("/api/initiative_group/add") async def api_add_ig_member( user_id: int = Form(...), role: str = Form("member"), username: str = Depends(get_current_admin) ): """Добавить в ИГ""" async with AsyncSessionLocal() as session: # Проверяем есть ли уже existing = await session.get(InitiativeGroup, user_id) if existing: existing.is_active = True existing.role = role else: member = InitiativeGroup(user_id=user_id, role=role) session.add(member) await session.commit() return {"success": True, "message": f"Пользователь {user_id} добавлен в ИГ"} @app.post("/api/initiative_group/remove") async def api_remove_ig_member( user_id: int = Form(...), username: str = Depends(get_current_admin) ): """Удалить из ИГ""" async with AsyncSessionLocal() as session: member = await session.get(InitiativeGroup, user_id) if member: member.is_active = False await session.commit() return {"success": True, "message": f"Пользователь {user_id} удалён из ИГ"} # ============================================================================= # ТЕЛЕФОНЫ - API # ============================================================================= @app.get("/api/phones") async def api_get_phones(username: str = Depends(get_current_admin)): """Получить список телефонов""" import os phones_file = "data/phones.json" if os.path.exists(phones_file): with open(phones_file, 'r', encoding='utf-8') as f: phones_data = json.load(f) return {"success": True, "data": phones_data} else: return {"success": False, "data": {}} @app.post("/api/phones/update") async def api_update_phones( request: Request, username: str = Depends(get_current_admin) ): """Обновить телефоны""" import os data = await request.json() phones_file = "data/phones.json" try: with open(phones_file, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) return {"success": True, "message": "Телефоны обновлены"} except Exception as e: return {"success": False, "error": str(e)} # ============================================================================= # ОБЪЯВЛЕНИЯ - API # ============================================================================= @app.get("/api/announcements") async def api_get_announcements(username: str = Depends(get_current_admin)): """Получить список объявлений""" async with AsyncSessionLocal() as session: stmt = select(Announcement).order_by(Announcement.created_at.desc()) result = await session.execute(stmt) announcements = list(result.scalars().all()) return { "announcements": [ { "id": a.id, "title": a.title, "text": a.text, "category": a.category, "is_active": a.is_active, "created_at": a.created_at.isoformat() } for a in announcements ] } @app.post("/api/announcements/create") async def api_create_announcement( title: str = Form(...), text: str = Form(...), category: str = Form("general"), username: str = Depends(get_current_admin) ): """Создать объявление""" async with AsyncSessionLocal() as session: announcement = Announcement( title=title, text=text, category=category, created_by=username ) session.add(announcement) await session.commit() return {"success": True, "message": "Объявление создано"} # ============================================================================= # ВЫПОЛНЕНИЕ КОМАНД БОТА # ============================================================================= @app.post("/api/command") async def api_command( request: Request, username: str = Depends(get_current_admin) ): """Выполнить команду бота""" data = await request.json() command = data.get('command', '') args = data.get('args', '') if not command: return JSONResponse({"success": False, "error": "Нет команды"}) try: async with aiohttp.ClientSession() as http_session: full_command = f"/{command}" if args: full_command += f" {args}" result = await http_session.post( f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage", json={ "chat_id": config.ADMIN_CHAT_ID, "text": full_command } ) return JSONResponse({ "success": True, "message": f"Команда /{command} отправлена" }) except Exception as e: return JSONResponse({ "success": False, "error": str(e) }, status_code=500) @app.get("/api/commands") async def api_commands_list(username: str = Depends(get_current_admin)): """Список доступных команд""" commands = { "admin": [ {"cmd": "admin", "desc": "Админ-панель"}, {"cmd": "stats", "desc": "Статистика чата"}, {"cmd": "spies", "desc": "Подозрительные пользователи"}, {"cmd": "users", "desc": "Список жильцов"}, {"cmd": "verify", "desc": "Заявки на верификацию"}, {"cmd": "export", "desc": "Экспорт данных"}, {"cmd": "rating", "desc": "Рейтинг пользователей"}, ], "initiative_group": [ {"cmd": "ig_add", "desc": "Добавить в ИГ"}, {"cmd": "ig_remove", "desc": "Удалить из ИГ"}, {"cmd": "ig_list", "desc": "Список ИГ"}, {"cmd": "ig_stats", "desc": "Статистика ИГ"}, {"cmd": "ig_broadcast", "desc": "Рассылка для ИГ"}, ], "events": [ {"cmd": "event_add", "desc": "Добавить событие"}, {"cmd": "events", "desc": "Список событий"}, {"cmd": "participants", "desc": "Участники события"}, ], "security": [ {"cmd": "unverify", "desc": "Отозвать верификацию"}, {"cmd": "unverified_list", "desc": "Список не верифицированных"}, {"cmd": "ban", "desc": "Забанить пользователя"}, {"cmd": "unban", "desc": "Разбанить"}, ], "phones": [ {"cmd": "add_phone", "desc": "Добавить телефон"}, {"cmd": "edit_phone", "desc": "Редактировать телефон"}, {"cmd": "remove_phone", "desc": "Удалить телефон"}, ] } return JSONResponse({"commands": commands}) # ============================================================================= # СТРАНИЦЫ # ============================================================================= @app.get("/users", response_class=HTMLResponse) async def users_page(request: Request, username: str = Depends(get_current_admin)): """Страница пользователей""" return templates.TemplateResponse("users.html", { "request": request, "username": username }) @app.get("/initiative_group", response_class=HTMLResponse) async def ig_page(request: Request, username: str = Depends(get_current_admin)): """Страница ИГ""" return templates.TemplateResponse("initiative_group.html", { "request": request, "username": username }) @app.get("/phones", response_class=HTMLResponse) async def phones_page(request: Request, username: str = Depends(get_current_admin)): """Страница телефонов""" return templates.TemplateResponse("phones.html", { "request": request, "username": username }) @app.get("/announcements", response_class=HTMLResponse) async def announcements_page(request: Request, username: str = Depends(get_current_admin)): """Страница объявлений""" return templates.TemplateResponse("announcements.html", { "request": request, "username": username }) @app.get("/commands", response_class=HTMLResponse) async def commands_page(request: Request, username: str = Depends(get_current_admin)): """Страница команд""" return templates.TemplateResponse("commands.html", { "request": request, "username": username }) # ============================================================================= # ЗАПУСК # ============================================================================= def run_web_server(): """Запустить веб-сервер""" import uvicorn uvicorn.run( app, host=config.WEB_ADMIN_HOST, port=config.WEB_ADMIN_PORT, log_level="info" ) if __name__ == "__main__": run_web_server()