""" Web Admin Interface v3.0 - MEGA ADMIN PANEL Полноценное управление ВСЕМ функционалом бота """ import logging import json import aiohttp 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 sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, update, delete, desc from datetime import datetime, timedelta import secrets import config from database.db import AsyncSessionLocal from database.models import ( User, Message, Poll, Ad, PaymentReminder, Schedule, VerificationRequest, Service, Announcement, Event ) logger = logging.getLogger(__name__) app = FastAPI(title="Домовой Бот - MEGA Admin Panel", version="3.0.0") 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() 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 } }) # ============================================================================ # ПОЛЬЗОВАТЕЛИ # ============================================================================ @app.get("/users", response_class=HTMLResponse) async def users_page(request: Request, search: str = "", username: str = Depends(get_current_admin)): """Пользователи""" async with AsyncSessionLocal() as session: stmt = select(User) if search: stmt = stmt.where((User.full_name.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()) # Статистика 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() active_unverified = (await session.execute( select(func.count(User.user_id)).where(User.verified == False, User.message_count >= 50) )).scalar() return templates.TemplateResponse("users.html", {"request": request, "username": username, "users": users, "search": search, "stats": {"total": total_count, "verified": verified_count, "unverified": total_count - verified_count, "active_unverified": active_unverified}}) @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) 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.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.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)): """Рассылки""" return templates.TemplateResponse("broadcast.html", { "request": request, "username": username }) @app.get("/broadcast", response_class=HTMLResponse) async def broadcast_page(request: Request, username: str = Depends(get_current_admin)): """Страница рассылок""" return templates.TemplateResponse("broadcast.html", { "request": request, "username": username }) @app.post("/api/broadcast/send") async def api_send_broadcast( text: str = Form(...), photo: UploadFile = File(None), username: str = Depends(get_current_admin) ): """Отправить рассылку""" import aiohttp from pathlib import Path photo_id = None # Если есть картинка — загружаем if photo and photo.filename: # Сохраняем временно temp_path = Path("data") / f"broadcast_{photo.filename}" with open(temp_path, "wb") as f: content = await photo.read() f.write(content) # Загружаем в Telegram try: async with aiohttp.ClientSession() as session: # Отправляем фото чтобы получить file_id url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendPhoto" data = aiohttp.FormData() data.add_field('chat_id', config.ADMIN_USER_ID) data.add_field('photo', open(temp_path, 'rb'), filename=photo.filename) data.add_field('caption', 'Test') async with session.post(url, data=data) as resp: result = await resp.json() if result.get('ok'): photo_id = result['result']['photo'][-1]['file_id'] except Exception as e: logger.error(f"Ошибка загрузки фото: {e}") # Рассылаем async with AsyncSessionLocal() as session: stmt = select(User).where(User.verified == True) result = await session.execute(stmt) users = list(result.scalars().all()) success_count = 0 error_count = 0 async with aiohttp.ClientSession() as http_session: for user in users: try: url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/" if photo_id: url += "sendPhoto" data = aiohttp.FormData() data.add_field('chat_id', user.user_id) data.add_field('photo', photo_id) data.add_field('caption', f"📢 Объявление от администрации\n\n{text}") async with http_session.post(url, data=data, timeout=30) as resp: if resp.status == 200: success_count += 1 else: url += "sendMessage" data = aiohttp.FormData() data.add_field('chat_id', user.user_id) data.add_field('text', f"📢 Объявление от администрации\n\n{text}") data.add_field('parse_mode', 'HTML') async with http_session.post(url, data=data, timeout=30) as resp: if resp.status == 200: success_count += 1 except Exception as e: error_count += 1 return JSONResponse({ "success": True, "message": f"Отправлено: {success_count}, Ошибок: {error_count}" }) # ============================================================================ # ОПРОСЫ # ============================================================================ @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("/settings", response_class=HTMLResponse) async def settings_page(request: Request, username: str = Depends(get_current_admin)): """Настройки""" return templates.TemplateResponse("settings.html", { "request": request, "username": username, "config": { "bot_token": config.BOT_TOKEN[:20] + "...", "admin_chat_id": config.ADMIN_CHAT_ID, "admin_user_id": config.ADMIN_USER_ID, "use_proxy": config.USE_PROXY, "web_login": config.WEB_ADMIN_LOGIN } }) def run_web_server(host: str = "0.0.0.0", port: int = 8000): """Запуск веб-сервера""" import uvicorn uvicorn.run(app, host=host, port=port) if __name__ == "__main__": run_web_server()