From aadfc041b45c1ccc1d35296f134f34f307d06de6 Mon Sep 17 00:00:00 2001 From: Admin Date: Sat, 18 Apr 2026 23:34:14 +0400 Subject: [PATCH] =?UTF-8?q?v4.8:=20=D0=92=D0=BE=D1=81=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20Artifact=20M?= =?UTF-8?q?anager,=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=84=D0=BE=D1=82=D0=BE+=D0=B4=D0=BE=D0=BA=20=D0=B2?= =?UTF-8?q?=20=D1=80=D0=B0=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B0=D1=85=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=D1=81=D1=82=D0=B0=D1=85,=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database/migrate_artifacts.py | 26 +++ database/models.py | 14 ++ web/app.py | 280 ++++++++++++++++++----------- web/templates/broadcast.html | 39 +++- web/templates/files.html | 192 +++++++++++++++----- web/templates/scheduled_posts.html | 42 ++++- 6 files changed, 431 insertions(+), 162 deletions(-) create mode 100644 database/migrate_artifacts.py diff --git a/database/migrate_artifacts.py b/database/migrate_artifacts.py new file mode 100644 index 0000000..1aebc70 --- /dev/null +++ b/database/migrate_artifacts.py @@ -0,0 +1,26 @@ + +import asyncio +from sqlalchemy.ext.asyncio import create_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from database.models import Base +import config +import os + +DATABASE_URL = f"sqlite+aiosqlite:///{os.path.abspath(config.DATABASE_PATH)}" + +async def migrate(): + print(f"🚀 Запуск миграции для Artifacts... База: {DATABASE_URL}") + engine = create_engine(DATABASE_URL, echo=True) + async with engine.begin() as conn: + # Это создаст только те таблицы, которых нет + await conn.run_sync(Base.metadata.create_all) + print("✅ Таблица artifacts создана (если её не было).") + +if __name__ == "__main__": + from database.db import engine + async def run(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + print("✅ Миграция Artifacts завершена.") + + asyncio.run(run()) diff --git a/database/models.py b/database/models.py index e7ae5b7..41c5d7f 100644 --- a/database/models.py +++ b/database/models.py @@ -792,3 +792,17 @@ class EmailExportLog(Base): status = Column(String(50)) # 'success', 'error', 'running' new_emails_count = Column(Integer, default=0) report_path = Column(String(255), nullable=True) + + +class Artifact(Base): + """Медиа-артефакты для повторного использования в рассылках""" + __tablename__ = 'artifacts' + + id = Column(Integer, primary_key=True, autoincrement=True) + filename = Column(String(255), nullable=False) + file_type = Column(String(50), nullable=False) # 'photo', 'document' + file_id = Column(String(255), nullable=False, unique=True) + created_at = Column(DateTime, default=datetime.utcnow) + + def __repr__(self): + return f"" diff --git a/web/app.py b/web/app.py index 730c7c7..d10f825 100644 --- a/web/app.py +++ b/web/app.py @@ -1,12 +1,13 @@ """ -Web Admin Interface v4.5 REBORN - MEGA ADMIN PANEL +Web Admin Interface v4.8 REBORN - MEGA ADMIN PANEL Полноценное управление ВСЕМ функционалом бота: - Dashboard, Users, Verification, Phones, Ads, Polls, Events, Schedules -- Smart Broadcasts (с отслеживанием прочтения) -- Scheduled Posts (с фото/документами) +- Smart Broadcasts (фото + док через Artifacts) +- Scheduled Posts (фото + док через Artifacts) - Weekly Digests (v3.1) - Chat Exporter (JSON + Gitea Push) - Email Auditor (аудит переписки) +- Artifact Manager (хранилище медиа ID) """ import logging import json @@ -16,6 +17,7 @@ import os import sys import secrets import subprocess +import uuid from pathlib import Path from datetime import datetime, timedelta, timezone @@ -39,7 +41,7 @@ from database.models import ( User, Message as DBMessage, Poll, Ad, PaymentReminder, Schedule, VerificationRequest, Service, Announcement, Event, ScheduledPost, Broadcast, BroadcastRead, Digest, InitiativeGroup, EmailAudit, EmailExportLog, - ProfileHistory, Achievement, Thank, SpyLog + ProfileHistory, Achievement, Thank, SpyLog, Artifact ) # ===== ЧАСОВОЙ ПОЯС УЛЬЯНОВСК (UTC+4) ===== @@ -61,7 +63,7 @@ def fmt_local(dt, fmt: str = '%d.%m.%Y %H:%M') -> str: logger = logging.getLogger(__name__) logger.info(f"!!! [STARTUP] Unified Web App loaded. PID: {os.getpid()}") -app = FastAPI(title="LKM37 MEGA Admin Panel", version="4.5.0") +app = FastAPI(title="LKM37 MEGA Admin Panel", version="4.8.0") app.add_middleware( CORSMiddleware, @@ -89,6 +91,30 @@ def get_current_admin(credentials: HTTPBasicCredentials = Depends(security)): ) return credentials.username +# Вспомогательная функция для загрузки в TG +async def upload_to_tg(content, filename, content_type): + file_type = "document" + if content_type.startswith("image/"): file_type = "photo" + + proxy_url = f"http://{config.PROXY_HOST}:{config.PROXY_PORT}" if config.USE_PROXY else None + async with aiohttp.ClientSession() as session: + data = aiohttp.FormData() + data.add_field(file_type, content, filename=filename, content_type=content_type) + data.add_field('chat_id', str(config.ADMIN_USER_ID)) + data.add_field('caption', f"📤 Загрузка через веб-панель: {filename}") + + method = "sendPhoto" if file_type == "photo" else "sendDocument" + url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/{method}" + + async with session.post(url, data=data, proxy=proxy_url) as resp: + tg_res = await resp.json() + if not tg_res.get("ok"): + raise Exception(f"TG Error: {tg_res.get('description')}") + + msg = tg_res["result"] + file_id = msg["photo"][-1]["file_id"] if file_type == "photo" else msg["document"]["file_id"] + return file_id, file_type + # ============================================================================ # ГЛАВНАЯ (DASHBOARD) # ============================================================================ @@ -144,26 +170,15 @@ async def users_page(request: Request, search: str = "", filter: str = "", usern ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True) ig_user_ids = set(row[0] for row in (await session.execute(ig_stmt)).all()) - pending_stmt = select(VerificationRequest).where(VerificationRequest.status == 'pending').order_by(VerificationRequest.created_at.desc()) - pending_requests = (await session.execute(pending_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, "current_filter": filter, - "stats": {"total": total_count, "verified": verified_count, "unverified": total_count - verified_count, "active_unverified": active_unverified, "ig": len(ig_user_ids)}, - "ig_user_ids": ig_user_ids, "pending_count": len(list(pending_requests)) + "stats": {"total": total_count, "verified": verified_count, "unverified": total_count - verified_count, "ig": len(ig_user_ids)}, + "ig_user_ids": ig_user_ids }) -@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": "Not found"}, 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}/update") async def api_update_user(user_id: int, request: Request, username: str = Depends(get_current_admin)): data = await request.json() @@ -174,26 +189,11 @@ async def api_update_user(user_id: int, request: Request, username: str = Depend if 'verified' in data: update_data['verified'] = bool(data['verified']) if data['verified']: update_data['verification_date'] = datetime.utcnow() - 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}/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}) - -@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}) - # ============================================================================ # ВЕРИФИКАЦИЯ # ============================================================================ @@ -268,6 +268,8 @@ async def schedules_page(request: Request, username: str = Depends(get_current_a async def broadcast_page(request: Request, username: str = Depends(get_current_admin)): async with AsyncSessionLocal() as session: broadcasts = (await session.execute(select(Broadcast).order_by(Broadcast.created_at.desc()).limit(50))).scalars().all() + artifacts = (await session.execute(select(Artifact).order_by(Artifact.filename))).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 @@ -277,8 +279,70 @@ async def broadcast_page(request: Request, username: str = Depends(get_current_a '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 or 'regular', + 'is_reminder_sent': b.is_reminder_sent }) - return templates.TemplateResponse("broadcast.html", {"request": request, "username": username, "broadcasts": broadcasts_data}) + return templates.TemplateResponse("broadcast.html", {"request": request, "username": username, "broadcasts": broadcasts_data, "artifacts": artifacts}) + +@app.post("/api/broadcast/create") +async def api_create_broadcast( + text: str = Form(...), + photo: UploadFile = File(None), + artifact_photo_id: str = Form(None), + artifact_document_id: str = Form(None), + recipients: str = Form("all_and_chat"), + staircase: str = Form("all"), + has_read_button: bool = Form(True), + track_reads: bool = Form(True), + username: str = Depends(get_current_admin) +): + try: + from database.models import User, Broadcast + photo_file_id = artifact_photo_id + document_file_id = artifact_document_id + + # Если загружена новая картинка + if photo and photo.filename and not photo_file_id: + content = await photo.read() + photo_file_id, _ = await upload_to_tg(content, photo.filename, photo.content_type) + + async with AsyncSessionLocal() as session: + stmt = select(User).where(User.verified == True, User.is_banned == False) + if staircase != 'all': pass + if recipients == 'ig_only': + ig_stmt = select(InitiativeGroup.user_id).where(InitiativeGroup.is_active == True) + ig_ids = [r[0] for row in (await session.execute(ig_stmt)).all()] + stmt = stmt.where(User.user_id.in_(ig_ids)) + + users = (await session.execute(stmt)).scalars().all() + + new_broadcast = Broadcast( + text=text, photo_file_id=photo_file_id, + sent_at=datetime.utcnow(), total_sent=len(users), + broadcast_type='smart' if track_reads else 'regular' + ) + session.add(new_broadcast) + await session.commit() + return JSONResponse({"success": True, "message": f"Рассылка создана. Получателей: {len(users)}", "broadcast_id": new_broadcast.id}) + except Exception as e: + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + +@app.get("/api/broadcast/readers/{broadcast_id}") +async def api_broadcast_readers(broadcast_id: int, username: str = Depends(get_current_admin)): + async with AsyncSessionLocal() as session: + stmt = select(User.first_name, User.last_name, User.apartment, BroadcastRead.read_at).join(BroadcastRead).where(BroadcastRead.broadcast_id == broadcast_id) + results = (await session.execute(stmt)).all() + readers = [{"name": f"{r[0]} {r[1] or ''}", "apartment": r[2], "read_at": fmt_local(r[3])} for r in results] + return JSONResponse({"readers": readers}) + +@app.get("/api/broadcast/non_readers/{broadcast_id}") +async def api_broadcast_non_readers(broadcast_id: int, username: str = Depends(get_current_admin)): + async with AsyncSessionLocal() as session: + read_ids_stmt = select(BroadcastRead.user_id).where(BroadcastRead.broadcast_id == broadcast_id) + read_ids = [r[0] for r in (await session.execute(read_ids_stmt)).all()] + stmt = select(User.first_name, User.last_name, User.apartment).where(User.verified == True, User.user_id.not_in(read_ids)) + results = (await session.execute(stmt)).all() + non_readers = [{"name": f"{r[0]} {r[1] or ''}", "apartment": r[2]} for r in results] + return JSONResponse({"non_readers": non_readers}) # ============================================================================ # ЗАПЛАНИРОВАННЫЕ ПОСТЫ @@ -286,7 +350,9 @@ async def broadcast_page(request: Request, username: str = Depends(get_current_a @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}) + async with AsyncSessionLocal() as session: + artifacts = (await session.execute(select(Artifact).order_by(Artifact.filename))).scalars().all() + return templates.TemplateResponse("scheduled_posts.html", {"request": request, "username": username, "artifacts": artifacts}) @app.get("/api/scheduled_posts/list") async def api_list_scheduled_posts(username: str = Depends(get_current_admin)): @@ -298,10 +364,53 @@ async def api_list_scheduled_posts(username: str = Depends(get_current_admin)): "id": post.id, "text": post.text, "topic_name": post.topic_name, "topic_emoji": post.get_topic_emoji(), "recipients": post.recipients, "scheduled_time": fmt_local(post.scheduled_time), "status": post.status, - "status_emoji": post.get_status_emoji(), "has_photo": bool(post.photo_file_id) + "status_emoji": post.get_status_emoji(), "has_photo": bool(post.photo_file_id or post.document_file_id) }) return JSONResponse({"success": True, "posts": posts_data}) +@app.post("/api/scheduled_posts/create") +async def api_create_scheduled_post( + text: str = Form(...), + photo: UploadFile = File(None), + document: UploadFile = File(None), + artifact_photo_id: str = Form(None), + artifact_document_id: str = Form(None), + topic_name: str = Form("general"), + recipients: str = Form("chat_only"), + scheduled_time: str = Form(...), + username: str = Depends(get_current_admin) +): + try: + photo_file_id = artifact_photo_id + document_file_id = artifact_document_id + doc_name = None + + if photo and photo.filename and not photo_file_id: + content = await photo.read() + photo_file_id, _ = await upload_to_tg(content, photo.filename, photo.content_type) + + if document and document.filename and not document_file_id: + content = await document.read() + document_file_id, _ = await upload_to_tg(content, document.filename, document.content_type) + doc_name = document.filename + + # Парсим время (оно приходит в локальном времени) + dt_local = datetime.fromisoformat(scheduled_time) + dt_utc = dt_local - timedelta(hours=4) # Ульяновск -> UTC + + async with AsyncSessionLocal() as session: + new_post = ScheduledPost( + text=text, photo_file_id=photo_file_id, + document_file_id=document_file_id, document_name=doc_name, + topic_name=topic_name, recipients=recipients, + scheduled_time=dt_utc, status='pending' + ) + session.add(new_post) + await session.commit() + return JSONResponse({"success": True, "message": f"Пост запланирован на {fmt_local(dt_utc)}"}) + except Exception as e: + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + # ============================================================================ # ДАЙДЖЕСТЫ (V3.1) # ============================================================================ @@ -309,19 +418,11 @@ async def api_list_scheduled_posts(username: str = Depends(get_current_admin)): @app.get("/digests", response_class=HTMLResponse) async def digests_page(request: Request, username: str = Depends(get_current_admin)): async with AsyncSessionLocal() as session: - # Получаем список дайджестов из БД stmt_pending = select(Digest).where(Digest.status.in_(['draft', 'pending_approval'])).order_by(Digest.created_at.desc()) pending = (await session.execute(stmt_pending)).scalars().all() - stmt_archive = select(Digest).where(Digest.status.in_(['approved', 'sent', 'rejected'])).order_by(Digest.created_at.desc()).limit(20) archive = (await session.execute(stmt_archive)).scalars().all() - - return templates.TemplateResponse("digests.html", { - "request": request, - "username": username, - "pending_digests": pending, - "archive_digests": archive - }) + return templates.TemplateResponse("digests.html", {"request": request, "username": username, "pending_digests": pending, "archive_digests": archive}) @app.post("/api/digest/generate") async def api_generate_digest(username: str = Depends(get_current_admin)): @@ -329,44 +430,18 @@ async def api_generate_digest(username: str = Depends(get_current_admin)): try: stats = await DigestService.collect_weekly_stats() digest_text = DigestService.format_digest_text(stats) - now = datetime.now() week_num = now.isocalendar()[1] - async with AsyncSessionLocal() as session: - new_digest = Digest( - year=now.year, - week_number=week_num, - period_start=now - timedelta(days=7), - period_end=now, - digest_text=digest_text, - status='draft' - ) + new_digest = Digest(year=now.year, week_number=week_num, period_start=now - timedelta(days=7), period_end=now, digest_text=digest_text, status='draft') session.add(new_digest) await session.commit() - return JSONResponse({"success": True, "message": f"Дайджест за {week_num} неделю успешно сгенерирован (черновик)"}) except Exception as e: - logger.error(f"Error generating digest: {e}", exc_info=True) return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) -@app.get("/api/digest/{digest_id}") -async def api_get_digest(digest_id: int, username: str = Depends(get_current_admin)): - async with AsyncSessionLocal() as session: - digest = await session.get(Digest, digest_id) - if not digest: return JSONResponse({"error": "Not found"}, status_code=404) - return JSONResponse({ - "digest": { - "id": digest.id, - "week": digest.week_number, - "year": digest.year, - "text": digest.digest_text, - "status": digest.status - } - }) - # ============================================================================ -# ФАЙЛЫ (FILES) +# ФАЙЛЫ / АРТЕФАКТЫ (Artifact Manager) # ============================================================================ @app.get("/files", response_class=HTMLResponse) @@ -375,32 +450,34 @@ async def files_page(request: Request, username: str = Depends(get_current_admin @app.get("/api/files/list") async def api_list_files(username: str = Depends(get_current_admin)): - files_data = [] - - # 1. Экспорты - export_path = BASE_DIR / "exports" - if export_path.exists(): - for f in export_path.glob("*.json"): - files_data.append({ - "filename": f.name, - "file_type": "EXPORT", - "size": f"{round(f.stat().st_size / 1024, 1)} KB", - "path": f"/exports/{f.name}" - }) - - # 2. Логи - log_path = BASE_DIR / "logs" - if log_path.exists(): - for f in log_path.glob("*.log"): - files_data.append({ - "filename": f.name, - "file_type": "LOG", - "size": f"{round(f.stat().st_size / 1024, 1)} KB", - "path": f"/api/files/download/logs/{f.name}" - }) - + async with AsyncSessionLocal() as session: + artifacts = (await session.execute(select(Artifact).order_by(Artifact.created_at.desc()))).scalars().all() + files_data = [] + for a in artifacts: + files_data.append({"id": a.id, "filename": a.filename, "file_type": a.file_type.upper(), "file_id": a.file_id, "created_at": fmt_local(a.created_at), "is_artifact": True}) + export_path = BASE_DIR / "exports" + if export_path.exists(): + for f in export_path.glob("*.json"): + files_data.append({"filename": f.name, "file_type": "EXPORT", "size": f"{round(f.stat().st_size / 1024, 1)} KB", "path": f"/exports/{f.name}", "is_artifact": False}) + log_path = BASE_DIR / "logs" + if log_path.exists(): + for f in log_path.glob("*.log"): + files_data.append({"filename": f.name, "file_type": "LOG", "size": f"{round(f.stat().st_size / 1024, 1)} KB", "path": f"/api/files/download/logs/{f.name}", "is_artifact": False}) return JSONResponse({"success": True, "files": files_data}) +@app.post("/api/files/upload") +async def api_upload_artifact(file: UploadFile = File(...), username: str = Depends(get_current_admin)): + try: + content = await file.read() + file_id, file_type = await upload_to_tg(content, file.filename, file.content_type) + async with AsyncSessionLocal() as db_session: + new_artifact = Artifact(filename=file.filename, file_type=file_type, file_id=file_id) + db_session.add(new_artifact) + await db_session.commit() + return JSONResponse({"success": True, "file_id": file_id, "filename": file.filename}) + except Exception as e: + return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) + @app.get("/api/files/download/logs/{filename}") async def download_log(filename: str, username: str = Depends(get_current_admin)): filepath = BASE_DIR / "logs" / filename @@ -426,7 +503,6 @@ async def api_export_json(username: str = Depends(get_current_admin)): async with AsyncSessionLocal() as session: exporter = ChatExporter(session) result = await exporter.run_full_export() - if not result.get('success'): return JSONResponse(status_code=500, content=result) return JSONResponse(result) except Exception as e: return JSONResponse(status_code=500, content={"success": False, "error": str(e)}) @@ -446,7 +522,6 @@ async def emails_page(request: Request, username: str = Depends(get_current_admi async with AsyncSessionLocal() as session: audit_data = (await session.execute(select(EmailAudit).order_by(EmailAudit.last_email_date.desc()))).scalars().all() last_log = (await session.execute(select(EmailExportLog).order_by(EmailExportLog.start_time.desc()).limit(1))).scalar_one_or_none() - latest_emails = [] all_folder = BASE_DIR / "services" / "email_auditor" / "exported_emails" / "all" if all_folder.exists(): @@ -454,14 +529,9 @@ async def emails_page(request: Request, username: str = Depends(get_current_admi for f in files: match = re.match(r'(\d{4}-\d{2}-\d{2})_([^_]+)_(.*)', f.stem) if match: latest_emails.append({"date": match.group(1), "domain": match.group(2), "subject": match.group(3)}) - master_pdf_path = BASE_DIR / "services" / "email_auditor" / "exported_emails" / "TOTAL_ARCHIVE_2025-2026.pdf" last_update = datetime.fromtimestamp(master_pdf_path.stat().st_mtime, tz=timezone.utc) if master_pdf_path.exists() else None - - return templates.TemplateResponse("emails.html", { - "request": request, "username": username, "audit_data": audit_data, "last_log": last_log, - "latest_emails": latest_emails, "has_master_pdf": master_pdf_path.exists(), "last_update": last_update, "fmt_local": fmt_local - }) + return templates.TemplateResponse("emails.html", {"request": request, "username": username, "audit_data": audit_data, "last_log": last_log, "latest_emails": latest_emails, "has_master_pdf": master_pdf_path.exists(), "last_update": last_update, "fmt_local": fmt_local}) @app.post("/api/emails/run") async def api_run_email_audit(username: str = Depends(get_current_admin)): diff --git a/web/templates/broadcast.html b/web/templates/broadcast.html index 8a1b8ba..fb4db88 100644 --- a/web/templates/broadcast.html +++ b/web/templates/broadcast.html @@ -73,10 +73,35 @@ Поддерживается HTML: жирный, курсив, код +
+
+
+ + +
+
+
+
+ + +
+
+
+
- + - JPG, PNG до 5MB + Если выбран артефакт выше, загрузка файла будет проигнорирована.
Предпросмотр
@@ -312,6 +337,8 @@ document.getElementById('broadcastForm').addEventListener('submit', async (e) => const text = document.getElementById('broadcastText').value; const photo = document.getElementById('broadcastPhoto').files[0]; + const artifactPhotoId = document.getElementById('broadcastArtifactPhoto').value; + const artifactDocumentId = document.getElementById('broadcastArtifactDocument').value; const recipients = document.getElementById('broadcastRecipients').value; const staircase = document.getElementById('broadcastStaircase').value; const resultDiv = document.getElementById('result'); @@ -340,7 +367,13 @@ document.getElementById('broadcastForm').addEventListener('submit', async (e) => formData.append('has_read_button', 'true'); formData.append('track_reads', 'true'); - if (photo) { + if (artifactPhotoId) { + formData.append('artifact_photo_id', artifactPhotoId); + } + if (artifactDocumentId) { + formData.append('artifact_document_id', artifactDocumentId); + } + if (photo && !artifactPhotoId) { formData.append('photo', photo); } diff --git a/web/templates/files.html b/web/templates/files.html index 2a16483..51f9bc1 100644 --- a/web/templates/files.html +++ b/web/templates/files.html @@ -1,17 +1,41 @@ {% extends "base.html" %} -{% block title %}Файловое хранилище - LKM37{% endblock %} +{% block title %}Менеджер артефактов - LKM37{% endblock %} {% block nav_files %}active{% endblock %} {% block content %}

- >_ ХРАНИЛИЩЕ_АРТЕФАКТОВ (FILES) + >_ ЦЕНТРАЛЬНЫЙ_АРХИВ_ДАННЫХ (FILES_CORE)

-
-
- СПИСОК_ДОСТУПНЫХ_ФАЙЛОВ - +
+ +
+ + + + + +
+
+ 💠 АРТЕФАКТЫ TELEGRAM (ДЛЯ РАССЫЛОК) +
@@ -19,37 +43,41 @@
- + - - + + - - - - + +
ИМЯ ФАЙЛАКАТЕГОРИЯРАЗМЕРТИПTELEGRAM_FILE_ID ДЕЙСТВИЯ
-
-
Сканирование секторов...
-
-
-
-
-
ИНФОРМАЦИЯ
-
-
    -
  • Категория EXPORT: JSON-выгрузки истории чата.
  • -
  • Категория LOG: Системные логи бота и веб-панели.
  • -
  • Все файлы доступны для прямого скачивания на локальный хост.
  • -
-
+ +
+
+ 📁 СИСТЕМНЫЕ ФАЙЛЫ (ЛОГИ И ЭКСПОРТЫ) +
+
+
+ + + + + + + + + + + + +
ИМЯ ФАЙЛАКАТЕГОРИЯРАЗМЕРДЕЙСТВИЯ
@@ -60,36 +88,104 @@ document.addEventListener('DOMContentLoaded', loadFiles); async function loadFiles() { - const table = document.getElementById('filesTable'); + const artifactsTable = document.getElementById('artifactsTable'); + const systemFilesTable = document.getElementById('systemFilesTable'); + try { const response = await fetch('/api/files/list'); const result = await response.json(); if (!result.success) throw new Error(result.error); - table.innerHTML = ''; - if (result.files.length === 0) { - table.innerHTML = 'Файлы не найдены'; - return; + artifactsTable.innerHTML = ''; + systemFilesTable.innerHTML = ''; + + const artifacts = result.files.filter(f => f.is_artifact); + const systemFiles = result.files.filter(f => !f.is_artifact); + + if (artifacts.length === 0) { + artifactsTable.innerHTML = 'Артефакты не зарегистрированы'; + } else { + artifacts.forEach(f => { + artifactsTable.innerHTML += ` + + ${f.filename} + ${f.file_type} + ${f.file_id} + + + + + `; + }); } - result.files.forEach(f => { - const badgeClass = f.file_type === 'EXPORT' ? 'bg-primary' : 'bg-secondary'; - table.innerHTML += ` - - ${f.filename} - ${f.file_type} - ${f.size} - - - СКАЧАТЬ - - - - `; - }); + if (systemFiles.length === 0) { + systemFilesTable.innerHTML = 'Файлы не найдены'; + } else { + systemFiles.forEach(f => { + systemFilesTable.innerHTML += ` + + ${f.filename} + ${f.file_type} + ${f.size} + + + + + + + `; + }); + } } catch (error) { - table.innerHTML = `Ошибка: ${error.message}`; + console.error(error); + } +} + +function copyId(id) { + navigator.clipboard.writeText(id); + const toast = document.createElement('div'); + toast.className = 'position-fixed bottom-0 end-0 p-3'; + toast.style.zIndex = '11'; + toast.innerHTML = `
FILE_ID скопирован!
`; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2000); +} + +async function uploadFile() { + const fileInput = document.getElementById('fileInput'); + const btn = document.getElementById('uploadBtn'); + const spinner = document.getElementById('uploadSpinner'); + + if (!fileInput.files[0]) return alert('Выберите файл для загрузки'); + + btn.disabled = true; + spinner.classList.remove('d-none'); + + const formData = new FormData(); + formData.append('file', fileInput.files[0]); + + try { + const response = await fetch('/api/files/upload', { + method: 'POST', + body: formData + }); + const result = await response.json(); + + if (result.success) { + alert('Файл успешно зарегистрирован в Telegram!\nFILE_ID: ' + result.file_id); + location.reload(); + } else { + alert('Ошибка при регистрации: ' + result.error); + } + } catch (error) { + alert('Сетевая ошибка: ' + error.message); + } finally { + btn.disabled = false; + spinner.classList.add('d-none'); } } diff --git a/web/templates/scheduled_posts.html b/web/templates/scheduled_posts.html index 7332787..a02be95 100644 --- a/web/templates/scheduled_posts.html +++ b/web/templates/scheduled_posts.html @@ -28,21 +28,42 @@ Поддерживается HTML: <b>жирный</b>, <i>курсив</i>, <emoji>
+
+
+ + +
+
+ + +
+
+
- + - JPG, PNG до 5MB + Если выбран артефакт фото выше, загрузка будет проигнорирована.
Предпросмотр
- + - PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT, ZIP, RAR до 50MB + Если выбран артефакт документа выше, загрузка будет проигнорирована.