import os import sys import time import json import uuid import zipfile import shutil import asyncio import logging from pathlib import Path from typing import Dict, Any, Optional, Tuple from aiohttp import web from datetime import datetime from config import settings from agy_engine import session_manager logger = logging.getLogger("AGYWebUploader") UPLOAD_SUBDOMAIN = "upload.msa.artacloud.ir" UPLOAD_SERVER_PORT = 35555 # In-memory auth tokens for project upload sessions: # token -> { "chat_id": int, "project_name": str, "workspace": str, "created_at": float, "expires_at": float } UPLOAD_TOKENS: Dict[str, Dict[str, Any]] = {} TOKEN_TTL_SECONDS = 3600 * 4 # 4 hours validity def create_upload_token(chat_id: int, project_name: str) -> str: """Generates an expiring upload token for a specific project.""" session = session_manager.get_or_create(chat_id) accessible = session_manager.get_all_accessible_projects(chat_id) proj = accessible.get(project_name) if not proj: for k, p in accessible.items(): if p.name == project_name: proj = p break if not proj: proj = session_manager.get_current_project(chat_id) workspace = proj.workspace if proj else str(Path("/root/projects") / str(chat_id) / project_name) resolved_name = proj.name if proj else project_name token = uuid.uuid4().hex now = time.time() UPLOAD_TOKENS[token] = { "chat_id": chat_id, "project_name": resolved_name, "workspace": workspace, "created_at": now, "expires_at": now + TOKEN_TTL_SECONDS, } _cleanup_expired_tokens() return token def get_token_info(token: str) -> Optional[Dict[str, Any]]: _cleanup_expired_tokens() info = UPLOAD_TOKENS.get(token) if not info: return None if time.time() > info["expires_at"]: UPLOAD_TOKENS.pop(token, None) return None return info def _cleanup_expired_tokens(): now = time.time() expired = [t for t, data in UPLOAD_TOKENS.items() if now > data.get("expires_at", 0)] for t in expired: UPLOAD_TOKENS.pop(t, None) def get_upload_url(token: str) -> str: return f"https://{UPLOAD_SUBDOMAIN}/?token={token}" HTML_PAGE_TEMPLATE = """
پروژه فعال: {project_name}
لطفاً لینک آپلود را مستقیماً از ربات تلگرام دریافت کنید.
", content_type="text/html", status=401, ) info = get_token_info(token) if not info: return web.Response( text="لطفاً از طریق ربات تلگرام مجدداً روی دکمه آپلود کلیک فرمایید.
", content_type="text/html", status=403, ) html = HTML_PAGE_TEMPLATE.replace("{project_name}", info.get("project_name", "Unknown")).replace("{token}", token) return web.Response(text=html, content_type="text/html") async def handle_upload_api(request: web.Request) -> web.Response: try: reader = await request.multipart() except Exception as e: return web.json_response({"error": f"Invalid multipart request: {str(e)}"}, status=400) token = None extract_zip_opt = False caption_txt = "" saved_file_path: Optional[Path] = None file_name = "uploaded_file" file_size = 0 while True: part = await reader.next() if part is None: break if part.name == "token": token = (await part.text()).strip() elif part.name == "extract_zip": val = (await part.text()).strip().lower() extract_zip_opt = val in ("true", "1", "yes", "on") elif part.name == "caption": caption_txt = (await part.text()).strip() elif part.name == "file": raw_filename = part.filename or f"upload_{int(time.time())}.bin" file_name = Path(raw_filename).name # sanitize # Temporary save in a buffer or target directory temp_dir = Path("/root/telegram-agy-bot/uploads_temp") temp_dir.mkdir(parents=True, exist_ok=True) saved_file_path = temp_dir / f"{uuid.uuid4().hex[:8]}_{file_name}" with open(saved_file_path, "wb") as f: while True: chunk = await part.read_chunk(1024 * 1024) # positional size in bytes if not chunk: break f.write(chunk) file_size += len(chunk) if not token or not saved_file_path or not saved_file_path.exists(): return web.json_response({"error": "فایل یا پارامترهای درخواست ناقص هستند."}, status=400) token_info = get_token_info(token) if not token_info: if saved_file_path.exists(): saved_file_path.unlink() return web.json_response({"error": "توکن آپلود منقضی شده یا نامعتبر است."}, status=403) chat_id = token_info["chat_id"] project_name = token_info["project_name"] workspace = Path(token_info["workspace"]) workspace.mkdir(parents=True, exist_ok=True) dest_uploads_dir = workspace / "uploads" dest_uploads_dir.mkdir(parents=True, exist_ok=True) final_dest_path = dest_uploads_dir / file_name # Move from temp to destination shutil.move(str(saved_file_path), str(final_dest_path)) extracted_msg = "" is_zip = file_name.lower().endswith((".zip", ".tar.gz", ".tgz", ".tar")) if is_zip and extract_zip_opt: try: if file_name.lower().endswith(".zip"): with zipfile.ZipFile(final_dest_path, "r") as zf: zf.extractall(workspace) extracted_msg = f"📦 فایل زیپ مستقیماً در مسیر پروژه ({workspace}) استخراج شد." elif file_name.lower().endswith((".tar.gz", ".tgz", ".tar")): import tarfile with tarfile.open(final_dest_path, "r:*") as tf: tf.extractall(workspace) extracted_msg = f"📦 آرشیو فشرده در مسیر پروژه ({workspace}) استخراج شد." except Exception as ze: logger.error(f"Failed to auto-extract archive {final_dest_path}: {ze}") extracted_msg = f"⚠️ فایل در پوشه uploads ذخیره شد اما استخراج خودکار با خطا مواجه گردید: {ze}" # Notify Telegram bot and run agent turn asyncio.create_task(_notify_telegram_and_trigger_agent( chat_id=chat_id, project_name=project_name, workspace=str(workspace), file_path=str(final_dest_path), file_name=file_name, file_size=file_size, extracted=bool(is_zip and extract_zip_opt and extracted_msg and "استخراج شد" in extracted_msg), extracted_msg=extracted_msg, caption=caption_txt, )) size_mb = f"{file_size / (1024 * 1024):.2f} MB" return web.json_response({ "success": True, "filename": file_name, "size": size_mb, "message": f"فایل {file_name} ({size_mb}) با موفقیت آپلود گردید.{project_name}\n"
f"• 📄 نام فایل: {file_name}\n"
f"• 💾 حجم: {size_mb}\n"
f"• 📂 مسیر ذخیره: {file_path}\n"
)
if extracted:
notification_text += f"• 🗜️ وضعیت آرشیو: در ریشه پروژه ({workspace}) اکسترکت شد.\n"
if caption:
notification_text += f"\n💬 پیام شما: «{caption}»\n"
try:
await TELEGRAM_APP.bot.send_message(
chat_id=chat_id,
text=notification_text,
parse_mode="HTML",
)
except Exception as e:
logger.error(f"Failed to send telegram notification: {e}")
# Build prompt for AI
ai_prompt = (
f"[User uploaded file `{file_name}` ({size_mb}) via Web Uploader to `{file_path}`]\n"
f"Project workspace: `{workspace}`\n"
)
if extracted:
ai_prompt += f"The archive has been extracted into `{workspace}`.\n"
else:
ai_prompt += f"The file is saved at `{file_path}`.\n"
if caption:
ai_prompt += f"User instructions / caption: {caption}\n\n"
else:
ai_prompt += f"Please review the uploaded files in `{workspace}` and proceed with assisting the user.\n\n"
try:
from bot import process_agent_turn_by_chat_id
await process_agent_turn_by_chat_id(TELEGRAM_APP, chat_id, ai_prompt)
except Exception as turn_err:
logger.error(f"Failed to trigger agent turn for web upload: {turn_err}", exc_info=True)
async def start_web_uploader_server():
"""Starts the standalone aiohttp web server on port 35555."""
app = web.Application(client_max_size=2048 * 1024 * 1024) # 2GB max upload limit
app.router.add_get("/", handle_index_page)
app.router.add_post("/api/upload", handle_upload_api)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "127.0.0.1", UPLOAD_SERVER_PORT)
await site.start()
logger.info(f"Web Uploader server started on 127.0.0.1:{UPLOAD_SERVER_PORT}")