756 lines
27 KiB
Python
756 lines
27 KiB
Python
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 = """<!DOCTYPE html>
|
|
<html lang="fa" dir="rtl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>آپلود فایل به پروژه {project_name} | Antigravity</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;600;700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--primary: #3b82f6;
|
|
--primary-hover: #2563eb;
|
|
--bg-dark: #0f172a;
|
|
--card-bg: rgba(30, 41, 59, 0.75);
|
|
--card-border: rgba(255, 255, 255, 0.1);
|
|
--text-main: #f8fafc;
|
|
--text-muted: #94a3b8;
|
|
--accent-green: #10b981;
|
|
--accent-purple: #8b5cf6;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-dark);
|
|
background-image:
|
|
radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
|
|
radial-gradient(at 100% 100%, rgba(139, 92, 246, 0.15) 0px, transparent 50%);
|
|
color: var(--text-main);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
max-width: 580px;
|
|
background: var(--card-bg);
|
|
backdrop-filter: blur(16px);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 24px;
|
|
padding: 32px 28px;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
animation: fadeIn 0.5s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(16px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.logo-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: rgba(59, 130, 246, 0.12);
|
|
color: #60a5fa;
|
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
padding: 6px 14px;
|
|
border-radius: 100px;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 800;
|
|
color: #ffffff;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.project-badge {
|
|
color: #38bdf8;
|
|
background: rgba(56, 189, 248, 0.1);
|
|
padding: 2px 10px;
|
|
border-radius: 8px;
|
|
font-family: monospace;
|
|
direction: ltr;
|
|
display: inline-block;
|
|
}
|
|
|
|
.drop-zone {
|
|
border: 2px dashed rgba(59, 130, 246, 0.4);
|
|
border-radius: 18px;
|
|
padding: 36px 20px;
|
|
text-align: center;
|
|
background: rgba(15, 23, 42, 0.4);
|
|
cursor: pointer;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
position: relative;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.drop-zone:hover, .drop-zone.dragover {
|
|
border-color: #60a5fa;
|
|
background: rgba(59, 130, 246, 0.08);
|
|
transform: scale(1.01);
|
|
}
|
|
|
|
.drop-zone input[type="file"] {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.upload-icon {
|
|
font-size: 3rem;
|
|
margin-bottom: 12px;
|
|
display: block;
|
|
}
|
|
|
|
.drop-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.drop-desc {
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.options-box {
|
|
background: rgba(15, 23, 42, 0.5);
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
border-radius: 14px;
|
|
padding: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
font-size: 0.92rem;
|
|
font-weight: 500;
|
|
color: #e2e8f0;
|
|
user-select: none;
|
|
}
|
|
|
|
.checkbox-label input[type="checkbox"] {
|
|
width: 18px;
|
|
height: 18px;
|
|
accent-color: var(--primary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.input-group {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.input-group label {
|
|
display: block;
|
|
font-size: 0.82rem;
|
|
color: var(--text-muted);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.input-group input[type="text"] {
|
|
width: 100%;
|
|
background: rgba(30, 41, 59, 0.8);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
color: #fff;
|
|
padding: 10px 14px;
|
|
border-radius: 10px;
|
|
font-size: 0.9rem;
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.input-group input[type="text"]:focus {
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.file-info-box {
|
|
display: none;
|
|
background: rgba(59, 130, 246, 0.08);
|
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
margin-bottom: 20px;
|
|
font-size: 0.9rem;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.file-name-txt {
|
|
font-weight: 600;
|
|
direction: ltr;
|
|
text-align: right;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.file-size-txt {
|
|
color: #93c5fd;
|
|
font-size: 0.82rem;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.progress-bar-container {
|
|
display: none;
|
|
background: rgba(15, 23, 42, 0.8);
|
|
border-radius: 100px;
|
|
height: 10px;
|
|
overflow: hidden;
|
|
margin-bottom: 20px;
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 100%;
|
|
width: 0%;
|
|
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
|
transition: width 0.2s ease;
|
|
border-radius: 100px;
|
|
}
|
|
|
|
.btn-upload {
|
|
width: 100%;
|
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
color: #fff;
|
|
border: none;
|
|
padding: 14px 20px;
|
|
border-radius: 14px;
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 4px 14px 0 rgba(37, 99, 235, 0.39);
|
|
}
|
|
|
|
.btn-upload:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 20px 0 rgba(37, 99, 235, 0.5);
|
|
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
|
}
|
|
|
|
.btn-upload:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.status-msg {
|
|
margin-top: 18px;
|
|
font-size: 0.9rem;
|
|
text-align: center;
|
|
border-radius: 12px;
|
|
padding: 12px;
|
|
display: none;
|
|
}
|
|
|
|
.status-success {
|
|
background: rgba(16, 185, 129, 0.12);
|
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
color: #34d399;
|
|
}
|
|
|
|
.status-error {
|
|
background: rgba(239, 68, 68, 0.12);
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
color: #f87171;
|
|
}
|
|
|
|
.footer-note {
|
|
text-align: center;
|
|
font-size: 0.78rem;
|
|
color: var(--text-muted);
|
|
margin-top: 20px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<div class="logo-badge">⚡ Antigravity Workspace File Drop</div>
|
|
<h1>آپلود فایل در پروژه</h1>
|
|
<p style="color: var(--text-muted); font-size: 0.9rem; margin-top: 6px;">
|
|
پروژه فعال: <span class="project-badge">{project_name}</span>
|
|
</p>
|
|
</div>
|
|
|
|
<form id="uploadForm">
|
|
<div class="drop-zone" id="dropZone">
|
|
<input type="file" id="fileInput" name="file" required>
|
|
<span class="upload-icon">📁</span>
|
|
<div class="drop-title">فایل خود را اینجا بکشید یا کلیک کنید</div>
|
|
<div class="drop-desc">پشتیبانی از فایلهای فشرده (ZIP)، کدهای برنامه، تصاویر و اسناد (بدون محدودیت ۲۰ مگابایت)</div>
|
|
</div>
|
|
|
|
<div class="file-info-box" id="fileInfoBox">
|
|
<span class="file-name-txt" id="fileNameTxt"></span>
|
|
<span class="file-size-txt" id="fileSizeTxt"></span>
|
|
</div>
|
|
|
|
<div class="options-box">
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" id="extractZip" name="extract_zip" checked>
|
|
<span>🗜️ در صورت فشرده بودن (ZIP/TAR)، محتویات مستقیماً در پوشه پروژه استخراج (Unzip) شود</span>
|
|
</label>
|
|
|
|
<div class="input-group">
|
|
<label for="captionInput">💬 پیام یا دستور برای هوش مصنوعی (اختیاری):</label>
|
|
<input type="text" id="captionInput" placeholder="مثال: فایل زیپ سورس پروژه است، آن را باز کن و بررسی کن...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="progress-bar-container" id="progressBarContainer">
|
|
<div class="progress-bar" id="progressBar"></div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-upload" id="submitBtn">🚀 شروع آپلود فایل</button>
|
|
</form>
|
|
|
|
<div id="statusMsg" class="status-msg"></div>
|
|
|
|
<div class="footer-note">
|
|
امنیت و ایزولاسیون: فایلها مستقیماً در فضای ابری سرور و پروژه اختصاصی شما قرار میگیرند.
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const token = "{token}";
|
|
const dropZone = document.getElementById('dropZone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const fileInfoBox = document.getElementById('fileInfoBox');
|
|
const fileNameTxt = document.getElementById('fileNameTxt');
|
|
const fileSizeTxt = document.getElementById('fileSizeTxt');
|
|
const uploadForm = document.getElementById('uploadForm');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const progressContainer = document.getElementById('progressBarContainer');
|
|
const progressBar = document.getElementById('progressBar');
|
|
const statusMsg = document.getElementById('statusMsg');
|
|
const extractZip = document.getElementById('extractZip');
|
|
const captionInput = document.getElementById('captionInput');
|
|
|
|
function formatBytes(bytes, decimals = 2) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
}
|
|
|
|
fileInput.addEventListener('change', () => {
|
|
if (fileInput.files.length > 0) {
|
|
const file = fileInput.files[0];
|
|
fileNameTxt.textContent = file.name;
|
|
fileSizeTxt.textContent = formatBytes(file.size);
|
|
fileInfoBox.style.display = 'flex';
|
|
|
|
// Auto check unzip for zip files
|
|
if (file.name.toLowerCase().endsWith('.zip') || file.name.toLowerCase().endsWith('.tar.gz')) {
|
|
extractZip.checked = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropZone.classList.add('dragover');
|
|
}, false);
|
|
});
|
|
|
|
['dragleave'].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropZone.classList.remove('dragover');
|
|
}, false);
|
|
});
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropZone.classList.remove('dragover');
|
|
|
|
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
fileInput.files = e.dataTransfer.files;
|
|
const file = e.dataTransfer.files[0];
|
|
fileNameTxt.textContent = file.name;
|
|
fileSizeTxt.textContent = formatBytes(file.size);
|
|
fileInfoBox.style.display = 'flex';
|
|
|
|
if (file.name.toLowerCase().endsWith('.zip') || file.name.toLowerCase().endsWith('.tar.gz') || file.name.toLowerCase().endsWith('.tar')) {
|
|
extractZip.checked = true;
|
|
}
|
|
}
|
|
}, false);
|
|
|
|
uploadForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
if (!fileInput.files || !fileInput.files.length) {
|
|
alert('لطفاً ابتدا فایلی را انتخاب کنید یا به این کادر بکشید.');
|
|
return;
|
|
}
|
|
|
|
const file = fileInput.files[0];
|
|
const formData = new FormData();
|
|
formData.append('token', token);
|
|
formData.append('extract_zip', extractZip.checked ? 'true' : 'false');
|
|
formData.append('caption', captionInput.value.trim());
|
|
formData.append('file', file);
|
|
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = '⏳ در حال آپلود و ذخیرهسازی...';
|
|
progressContainer.style.display = 'block';
|
|
progressBar.style.width = '0%';
|
|
statusMsg.style.display = 'none';
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', '/api/upload', true);
|
|
|
|
xhr.upload.onprogress = (event) => {
|
|
if (event.lengthComputable) {
|
|
const percentComplete = (event.loaded / event.total) * 100;
|
|
progressBar.style.width = percentComplete + '%';
|
|
}
|
|
};
|
|
|
|
xhr.onload = function() {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '🚀 شروع آپلود فایل';
|
|
if (xhr.status === 200) {
|
|
const resp = JSON.parse(xhr.responseText);
|
|
statusMsg.className = 'status-msg status-success';
|
|
statusMsg.style.display = 'block';
|
|
statusMsg.innerHTML = '✅ <b>فایل با موفقیت در پروژه ذخیره شد!</b><br><br>' + (resp.message || '') + '<br><br>💡 هوش مصنوعی در تلگرام مطلع شد و در حال پردازش درخواست شماست.';
|
|
uploadForm.reset();
|
|
fileInfoBox.style.display = 'none';
|
|
} else {
|
|
let errMsg = 'خطا در آپلود فایل.';
|
|
try {
|
|
const errResp = JSON.parse(xhr.responseText);
|
|
if (errResp.error) errMsg = errResp.error;
|
|
} catch(e) {}
|
|
statusMsg.className = 'status-msg status-error';
|
|
statusMsg.style.display = 'block';
|
|
statusMsg.innerHTML = '❌ ' + errMsg;
|
|
}
|
|
};
|
|
|
|
xhr.onerror = function() {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '🚀 شروع آپلود فایل';
|
|
statusMsg.className = 'status-msg status-error';
|
|
statusMsg.style.display = 'block';
|
|
statusMsg.innerHTML = '❌ خطای شبکه در هنگام ارسال فایل. لطفاً دوباره تلاش کنید.';
|
|
};
|
|
|
|
xhr.send(formData);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Global reference to Telegram Application to trigger agent turn
|
|
TELEGRAM_APP = None
|
|
|
|
def set_telegram_app(app):
|
|
global TELEGRAM_APP
|
|
TELEGRAM_APP = app
|
|
|
|
async def handle_index_page(request: web.Request) -> web.Response:
|
|
token = request.query.get("token", "").strip()
|
|
if not token:
|
|
return web.Response(
|
|
text="<h1>⛔ دسترسی نامعتبر / Unauthorized</h1><p>لطفاً لینک آپلود را مستقیماً از ربات تلگرام دریافت کنید.</p>",
|
|
content_type="text/html",
|
|
status=401,
|
|
)
|
|
|
|
info = get_token_info(token)
|
|
if not info:
|
|
return web.Response(
|
|
text="<h1>⚠️ لینک آپلود منقضی شده است / Token Expired</h1><p>لطفاً از طریق ربات تلگرام مجدداً روی دکمه آپلود کلیک فرمایید.</p>",
|
|
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"فایل <b>{file_name}</b> ({size_mb}) با موفقیت آپلود گردید.<br>{extracted_msg}",
|
|
})
|
|
|
|
|
|
async def _notify_telegram_and_trigger_agent(
|
|
chat_id: int,
|
|
project_name: str,
|
|
workspace: str,
|
|
file_path: str,
|
|
file_name: str,
|
|
file_size: int,
|
|
extracted: bool,
|
|
extracted_msg: str,
|
|
caption: str,
|
|
):
|
|
"""Sends notification to Telegram and invokes AGY turn."""
|
|
if not TELEGRAM_APP:
|
|
logger.warning("TELEGRAM_APP not set in web_uploader")
|
|
return
|
|
|
|
size_mb = f"{file_size / (1024 * 1024):.2f} MB"
|
|
session = session_manager.get_or_create(chat_id)
|
|
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
|
|
|
notification_text = (
|
|
f"📥 <b>فایل جدید از طریق پنل وب آپلود شد!</b>\n\n"
|
|
f"• 📁 <b>پروژه:</b> <code>{project_name}</code>\n"
|
|
f"• 📄 <b>نام فایل:</b> <code>{file_name}</code>\n"
|
|
f"• 💾 <b>حجم:</b> <code>{size_mb}</code>\n"
|
|
f"• 📂 <b>مسیر ذخیره:</b> <code>{file_path}</code>\n"
|
|
)
|
|
if extracted:
|
|
notification_text += f"• 🗜️ <b>وضعیت آرشیو:</b> در ریشه پروژه (<code>{workspace}</code>) اکسترکت شد.\n"
|
|
if caption:
|
|
notification_text += f"\n💬 <b>پیام شما:</b> <i>«{caption}»</i>\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}")
|