Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
390b938c05 | ||
|
|
ee474774e1 | ||
|
|
5bfa479236 | ||
|
|
5ab0a40b03 | ||
|
|
ab4764f521 | ||
|
|
fb5e5ff404 | ||
|
|
314c23f4c9 | ||
|
|
78fbf36c38 | ||
|
|
8568e61cf0 | ||
|
|
7a26378a09 |
+1
-1
Submodule projects/232801087/myapp updated: ded79a9a9e...a6bf56fafb
Submodule
+1
Submodule projects/78649634/bildirchin added at e109f5d777
Submodule projects/78649634/copykar updated: f8468cdd1a...6e4b7e364d
Submodule projects/78649634/gasemi_tier updated: d27520ec0f...aca18b7229
@@ -360,6 +360,13 @@ class SessionManager:
|
||||
"created_at": p_obj.created_at,
|
||||
"description": p_obj.description,
|
||||
"conversation_titles": p_obj.conversation_titles,
|
||||
"active_branch": p_obj.active_branch,
|
||||
"ftp_host": p_obj.ftp_host,
|
||||
"ftp_port": p_obj.ftp_port,
|
||||
"ftp_user": p_obj.ftp_user,
|
||||
"ftp_password": p_obj.ftp_password,
|
||||
"ftp_path": p_obj.ftp_path,
|
||||
"ftp_tls": p_obj.ftp_tls,
|
||||
}
|
||||
curr = self.get_current_project(sess.chat_id)
|
||||
data[str(k)] = {
|
||||
|
||||
+242
-20
@@ -109,6 +109,9 @@ def get_lang_display(lang_code_or_name: str) -> str:
|
||||
norm = LANG_ALIASES.get(norm, norm)
|
||||
return AVAILABLE_LANGUAGES.get(norm, lang_code_or_name)
|
||||
|
||||
# Global active user input states for interactive prompts (e.g. FTP configuration wizards)
|
||||
USER_INPUT_STATES: dict[int, dict] = {}
|
||||
|
||||
# Logging configuration
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
@@ -389,34 +392,61 @@ async def build_ftp_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
||||
user=curr_proj.ftp_user,
|
||||
path=curr_proj.ftp_path,
|
||||
tls=curr_proj.ftp_tls,
|
||||
password=curr_proj.ftp_password,
|
||||
lang=session.language,
|
||||
)
|
||||
|
||||
tls_toggle_label = f"🔒 پروتکل امن: {'روشن ✅' if curr_proj.ftp_tls else 'خاموش ❌'}" if is_fa else f"🔒 FTPS/TLS: {'ON ✅' if curr_proj.ftp_tls else 'OFF ❌'}"
|
||||
|
||||
if is_fa:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("🚀 دیپلوی روی FTP (Clean Deploy)", callback_data="btn_ftp_deploy"),
|
||||
InlineKeyboardButton("🚀 دیپلوی نسخه پروداکشن روی FTP", callback_data="btn_ftp_deploy"),
|
||||
InlineKeyboardButton("🔍 تست اتصال FTP", callback_data="btn_ftp_test"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🐙 منوی گیت و برنچها", callback_data="btn_git_menu"),
|
||||
InlineKeyboardButton("📁 مدیریت پروژهها", callback_data="proj_menu"),
|
||||
InlineKeyboardButton("🌿 انتقال از dev به پروداکشن (Publish)", callback_data="btn_git_publish"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🌐 ویرایش هاست/پورت", callback_data="ftp_set:host"),
|
||||
InlineKeyboardButton("👤 ویرایش نام کاربری", callback_data="ftp_set:user"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🔑 ویرایش رمز عبور", callback_data="ftp_set:pass"),
|
||||
InlineKeyboardButton("📂 ویرایش مسیر مقصد", callback_data="ftp_set:path"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(tls_toggle_label, callback_data="ftp_toggle_tls"),
|
||||
InlineKeyboardButton("🗑️ پاکسازی اطلاعات", callback_data="ftp_clear_conf"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🐙 منوی گیت و برنچها", callback_data="btn_git_menu"),
|
||||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||||
],
|
||||
]
|
||||
else:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("🚀 Deploy via FTP (Clean)", callback_data="btn_ftp_deploy"),
|
||||
InlineKeyboardButton("🚀 Deploy Production to FTP", callback_data="btn_ftp_deploy"),
|
||||
InlineKeyboardButton("🔍 Test FTP Connection", callback_data="btn_ftp_test"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"),
|
||||
InlineKeyboardButton("📁 Projects Menu", callback_data="proj_menu"),
|
||||
InlineKeyboardButton("🌿 Publish dev to Production", callback_data="btn_git_publish"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🌐 Edit Host/Port", callback_data="ftp_set:host"),
|
||||
InlineKeyboardButton("👤 Edit Username", callback_data="ftp_set:user"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🔑 Edit Password", callback_data="ftp_set:pass"),
|
||||
InlineKeyboardButton("📂 Edit Remote Path", callback_data="ftp_set:path"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(tls_toggle_label, callback_data="ftp_toggle_tls"),
|
||||
InlineKeyboardButton("🗑️ Clear Config", callback_data="ftp_clear_conf"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"),
|
||||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||||
],
|
||||
]
|
||||
@@ -4687,11 +4717,11 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||
|
||||
elif data == "btn_git_publish":
|
||||
await query.answer("🚀 در حال انتشار به پروداکشن..." if is_fa else "🚀 Publishing to production...")
|
||||
await query.answer("🚀 در حال انتقال تغییرات از dev به production..." if is_fa else "🚀 Publishing to production...")
|
||||
if curr_proj:
|
||||
ok, res_msg, _ = await git_manager.git_publish(curr_proj.workspace, message="Publish via bot button", repo_name=curr_proj.name)
|
||||
ok, res_msg, _ = await git_manager.git_publish(curr_proj.workspace, message="Publish dev to production via bot button", repo_name=curr_proj.name)
|
||||
try:
|
||||
await query.answer("✅ انتشار به پروداکشن با موفقیت انجام شد!" if ok else "⚠️ خطا در انتشار", show_alert=not ok)
|
||||
await query.message.reply_html(res_msg, disable_web_page_preview=True)
|
||||
except Exception:
|
||||
pass
|
||||
text, markup = await build_git_menu(chat_id)
|
||||
@@ -4707,6 +4737,96 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
except Exception:
|
||||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||
|
||||
elif data.startswith("ftp_set:"):
|
||||
field_name = data.split(":")[1]
|
||||
if not curr_proj:
|
||||
await query.answer("⚠️ پروژهای یافت نشد." if is_fa else "⚠️ No project found.", show_alert=True)
|
||||
return
|
||||
|
||||
USER_INPUT_STATES[chat_id] = {
|
||||
"type": "ftp_set",
|
||||
"field": field_name,
|
||||
"project": curr_proj.name,
|
||||
}
|
||||
|
||||
field_prompts = {
|
||||
"host": (
|
||||
"🌐 <b>لطفاً آدرس هاست یا سرور FTP را ارسال کنید:</b>\n\n"
|
||||
"<i>مثال:</i> <code>ftp.mysite.com</code> یا <code>185.120.30.40:21</code>"
|
||||
) if is_fa else (
|
||||
"🌐 <b>Please enter the FTP Host address:</b>\n\n"
|
||||
"<i>Example:</i> <code>ftp.mysite.com</code> or <code>185.120.30.40:21</code>"
|
||||
),
|
||||
"user": (
|
||||
"👤 <b>لطفاً نام کاربری (Username) اتصال FTP را ارسال کنید:</b>\n\n"
|
||||
"<i>مثال:</i> <code>deployer@mysite.com</code> یا <code>ftpuser</code>"
|
||||
) if is_fa else (
|
||||
"👤 <b>Please enter the FTP Username:</b>\n\n"
|
||||
"<i>Example:</i> <code>deployer@mysite.com</code> or <code>ftpuser</code>"
|
||||
),
|
||||
"pass": (
|
||||
"🔑 <b>لطفاً رمز عبور (Password) اتصال FTP را ارسال کنید:</b>\n\n"
|
||||
"<i>(پیام پس از ثبت، جهت امنیت حذف یا پوشانده میشود)</i>"
|
||||
) if is_fa else (
|
||||
"🔑 <b>Please enter the FTP Password:</b>\n\n"
|
||||
"<i>(Message will be stored securely)</i>"
|
||||
),
|
||||
"path": (
|
||||
"📂 <b>لطفاً مسیر پوشه ریموت مقصد روی هاست را ارسال کنید:</b>\n\n"
|
||||
"<i>مثال:</i> <code>/public_html</code> یا <code>/domains/site.com/public_html</code>"
|
||||
) if is_fa else (
|
||||
"📂 <b>Please enter the remote target path:</b>\n\n"
|
||||
"<i>Example:</i> <code>/public_html</code>"
|
||||
),
|
||||
}
|
||||
|
||||
prompt_text = field_prompts.get(field_name, "لطفاً مقدار جدید را ارسال کنید:")
|
||||
cancel_kb = InlineKeyboardMarkup([[InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data="btn_ftp_menu")]])
|
||||
await query.edit_message_text(prompt_text, parse_mode=constants.ParseMode.HTML, reply_markup=cancel_kb)
|
||||
|
||||
elif data == "ftp_toggle_tls":
|
||||
if curr_proj:
|
||||
curr_proj.ftp_tls = not curr_proj.ftp_tls
|
||||
session_manager.save()
|
||||
status_str = "روشن (FTPS/TLS) 🔒" if curr_proj.ftp_tls else "خاموش (FTP معمولی) 🔓"
|
||||
await query.answer(f"✅ پروتکل امن: {status_str}")
|
||||
text, markup = await build_ftp_menu(chat_id)
|
||||
try:
|
||||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||||
except Exception:
|
||||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||
|
||||
elif data == "ftp_clear_conf":
|
||||
confirm_text = (
|
||||
"🗑️ <b>آیا از پاکسازی کامل اطلاعات FTP این پروژه اطمینان دارید؟</b>\n\n"
|
||||
"تمام مشخصات سرور، کاربر و رمز عبور حذف خواهند شد."
|
||||
) if is_fa else (
|
||||
"🗑️ <b>Are you sure you want to clear FTP settings for this project?</b>"
|
||||
)
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("💥 بله، پاک شود" if is_fa else "💥 Yes, Clear", callback_data="ftp_clear_do"),
|
||||
InlineKeyboardButton("❌ انصراف" if is_fa else "❌ Cancel", callback_data="btn_ftp_menu"),
|
||||
]
|
||||
]
|
||||
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||||
|
||||
elif data == "ftp_clear_do":
|
||||
if curr_proj:
|
||||
curr_proj.ftp_host = None
|
||||
curr_proj.ftp_port = 21
|
||||
curr_proj.ftp_user = None
|
||||
curr_proj.ftp_password = None
|
||||
curr_proj.ftp_path = "/"
|
||||
curr_proj.ftp_tls = False
|
||||
session_manager.save()
|
||||
await query.answer("🗑️ اطلاعات FTP با موفقیت پاک شد." if is_fa else "🗑️ FTP settings cleared.")
|
||||
text, markup = await build_ftp_menu(chat_id)
|
||||
try:
|
||||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||||
except Exception:
|
||||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||
|
||||
elif data == "btn_ftp_test":
|
||||
await query.answer("🔍 در حال بررسی اتصال FTP..." if is_fa else "🔍 Testing FTP connection...")
|
||||
if curr_proj and curr_proj.ftp_host:
|
||||
@@ -4718,9 +4838,24 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
path=curr_proj.ftp_path or "/",
|
||||
tls=curr_proj.ftp_tls,
|
||||
)
|
||||
icon = "✅" if test_ok else "❌"
|
||||
res_card = (
|
||||
f"{icon} <b>نتیجه تست اتصال FTP:</b>\n\n"
|
||||
f"• 🌐 <b>هاست:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
|
||||
f"• 👤 <b>نام کاربری:</b> <code>{escape_html(curr_proj.ftp_user or '(بدون نام کاربری)')}</code>\n"
|
||||
f"• 📂 <b>مسیر:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
|
||||
f"• 🔒 <b>پروتکل:</b> {'FTPS/TLS امن 🔒' if curr_proj.ftp_tls else 'FTP معمولی'}\n\n"
|
||||
f"📋 <b>گزارش ارتباط:</b>\n{test_msg}"
|
||||
) if is_fa else (
|
||||
f"{icon} <b>FTP Connection Test Result:</b>\n\n"
|
||||
f"• 🌐 <b>Host:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
|
||||
f"• 👤 <b>User:</b> <code>{escape_html(curr_proj.ftp_user or '(none)')}</code>\n"
|
||||
f"• 📂 <b>Path:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
|
||||
f"• 🔒 <b>Protocol:</b> {'Secure FTPS/TLS 🔒' if curr_proj.ftp_tls else 'Standard FTP'}\n\n"
|
||||
f"📋 <b>Status:</b>\n{test_msg}"
|
||||
)
|
||||
try:
|
||||
alert_text = ("✅ اتصال FTP برقرار است" if test_ok else "❌ خطا در اتصال FTP") + f"\n{test_msg}"
|
||||
await query.answer(alert_text[:200], show_alert=True)
|
||||
await query.message.reply_html(res_card, disable_web_page_preview=True)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
@@ -4735,14 +4870,10 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||
|
||||
elif data == "btn_ftp_deploy":
|
||||
await query.answer("🚀 در حال استقرار هوشمند روی FTP..." if is_fa else "🚀 Deploying to FTP...")
|
||||
await query.answer("🚀 در حال استقرار نسخه پروداکشن روی FTP..." if is_fa else "🚀 Deploying production to FTP...")
|
||||
if curr_proj and curr_proj.ftp_host:
|
||||
# Auto publish before deploy
|
||||
try:
|
||||
await git_manager.git_publish(curr_proj.workspace, message="Auto-publish before UI FTP deploy", repo_name=curr_proj.name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
status_msg = await query.message.reply_html("🚀 <i>در حال آمادهسازی و ارسال نسخه شاخه production به سرور FTP...</i>" if is_fa else "🚀 <i>Preparing and uploading production branch files to FTP...</i>")
|
||||
|
||||
res = await ftp_manager.deploy_project(
|
||||
workspace_path=curr_proj.workspace,
|
||||
host=curr_proj.ftp_host,
|
||||
@@ -4751,18 +4882,50 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
password=curr_proj.ftp_password or "",
|
||||
remote_path=curr_proj.ftp_path or "/",
|
||||
tls=curr_proj.ftp_tls,
|
||||
branch="production",
|
||||
)
|
||||
|
||||
if res.get("success"):
|
||||
files_up = res.get("files_uploaded", 0)
|
||||
files_skip = res.get("files_skipped", 0)
|
||||
dur = res.get("duration", 0)
|
||||
b_trans = res.get("bytes_transferred", 0)
|
||||
size_mb = f"{b_trans / (1024 * 1024):.2f} MB" if b_trans > 1024 * 1024 else f"{b_trans / 1024:.1f} KB"
|
||||
|
||||
report = (
|
||||
f"✅ <b>استقرار روی FTP با موفقیت انجام شد!</b>\n\n"
|
||||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||||
f"• 🌿 <b>شاخه سورس دیپلوی:</b> <code>production</code>\n"
|
||||
f"• 🌐 <b>سرور مقصد:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
|
||||
f"• 📂 <b>مسیر هاست:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
|
||||
f"• 📦 <b>تعداد فایلهای آپلود شده:</b> {files_up}\n"
|
||||
f"• 🧹 <b>فایلهای زائد و فیلتر شده:</b> {files_skip}\n"
|
||||
f"• 📊 <b>حجم کل منتقل شده:</b> {size_mb}\n"
|
||||
f"• ⏱️ <b>مدت زمان دیپلوی:</b> {dur} ثانیه"
|
||||
) if is_fa else (
|
||||
f"✅ <b>FTP Deployment Completed Successfully!</b>\n\n"
|
||||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||||
f"• 🌿 <b>Source Branch:</b> <code>production</code>\n"
|
||||
f"• 🌐 <b>Target Server:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
|
||||
f"• 📂 <b>Remote Path:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
|
||||
f"• 📦 <b>Files Uploaded:</b> {files_up}\n"
|
||||
f"• 🧹 <b>Excluded/Skipped:</b> {files_skip}\n"
|
||||
f"• 📊 <b>Transferred:</b> {size_mb}\n"
|
||||
f"• ⏱️ <b>Duration:</b> {dur}s"
|
||||
)
|
||||
try:
|
||||
await query.answer(f"✅ دیپلوی با موفقیت انجام شد ({files_up} فایل در {dur} ثانیه)", show_alert=True)
|
||||
await status_msg.edit_text(report, parse_mode=constants.ParseMode.HTML)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
err = res.get("error", "Error")
|
||||
err_report = (
|
||||
f"❌ <b>خطا در استقرار روی FTP:</b>\n\n"
|
||||
f"• ⚠️ <b>پیام خطا:</b> {escape_html(str(err))}\n"
|
||||
f"• 💡 <i>پیشنهاد: ابتدا دکمه تست اتصال را بزنید یا مشخصات هاست و مسیر را بررسی کنید.</i>"
|
||||
) if is_fa else f"❌ <b>FTP Deployment Error:</b>\n{escape_html(str(err))}"
|
||||
try:
|
||||
await query.answer(f"❌ خطا در دیپلوی: {err}"[:200], show_alert=True)
|
||||
await status_msg.edit_text(err_report, parse_mode=constants.ParseMode.HTML)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
@@ -6501,6 +6664,65 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
session = session_manager.get_or_create(chat_id)
|
||||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||||
|
||||
# 1. Check if user is in an interactive input state (e.g. configuring FTP fields)
|
||||
if chat_id in USER_INPUT_STATES:
|
||||
state = USER_INPUT_STATES.pop(chat_id)
|
||||
if state.get("type") == "ftp_set" and curr_proj:
|
||||
field = state.get("field")
|
||||
val = update.message.text.strip()
|
||||
|
||||
if field == "host":
|
||||
# Check for host:port format
|
||||
if ":" in val:
|
||||
h, p = val.rsplit(":", 1)
|
||||
curr_proj.ftp_host = h.strip()
|
||||
try:
|
||||
curr_proj.ftp_port = int(p.strip())
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
curr_proj.ftp_host = val
|
||||
elif field == "user":
|
||||
curr_proj.ftp_user = val
|
||||
elif field == "pass":
|
||||
curr_proj.ftp_password = val
|
||||
# Delete password message from chat if possible for security
|
||||
try:
|
||||
await update.message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
elif field == "path":
|
||||
curr_proj.ftp_path = val if val.startswith("/") else f"/{val}"
|
||||
|
||||
session_manager.save()
|
||||
|
||||
field_labels = {
|
||||
"host": "هاست و پورت",
|
||||
"user": "نام کاربری",
|
||||
"pass": "رمز عبور",
|
||||
"path": "مسیر مقصد",
|
||||
}
|
||||
label_fa = field_labels.get(field, field)
|
||||
ack_msg = f"✅ <b>{label_fa} با موفقیت ذخیره شد.</b>" if is_fa else f"✅ <b>{field} updated successfully.</b>"
|
||||
await update.message.reply_html(ack_msg)
|
||||
|
||||
# Auto-test connection if host, user and password are all present
|
||||
if curr_proj.ftp_host and curr_proj.ftp_user and curr_proj.ftp_password:
|
||||
test_ok, test_msg = await ftp_manager.test_connection(
|
||||
host=curr_proj.ftp_host,
|
||||
port=curr_proj.ftp_port,
|
||||
user=curr_proj.ftp_user or "",
|
||||
password=curr_proj.ftp_password or "",
|
||||
path=curr_proj.ftp_path or "/",
|
||||
tls=curr_proj.ftp_tls,
|
||||
)
|
||||
test_status = "✅ اتصال FTP با موفقیت برقرار و تایید شد." if test_ok else f"⚠️ هشدار در تست اتصال: {test_msg}"
|
||||
await update.message.reply_html(f"<i>{test_status}</i>")
|
||||
|
||||
text, markup = await build_ftp_menu(chat_id)
|
||||
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||
return
|
||||
|
||||
if not curr_proj:
|
||||
if is_fa:
|
||||
msg = (
|
||||
|
||||
@@ -1649,15 +1649,19 @@ async def execute_action(
|
||||
created_task,
|
||||
)
|
||||
|
||||
# Map types: 'project' -> project tier, 'user'/'private' -> user tier, 'global' -> rejected/downgraded for AI
|
||||
# Map types: 'project' -> project tier, 'user'/'private' -> user tier, 'global' -> global tier (if admin)
|
||||
is_admin_user = settings.is_admin(chat_id)
|
||||
if raw_type in ("project", "proj"):
|
||||
mem_type = "project"
|
||||
proj_name = req_project
|
||||
elif raw_type == "global":
|
||||
# Global memories are strictly bot-managed by Admin. AI cannot auto-save global memories.
|
||||
logger.info("AI attempted global memory save for user %s; safely converting to user memory.", chat_id)
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
if is_admin_user:
|
||||
mem_type = "global"
|
||||
proj_name = None
|
||||
else:
|
||||
logger.info("Non-admin user %s attempted global memory save; converting to user memory.", chat_id)
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
else:
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
@@ -1667,7 +1671,7 @@ async def execute_action(
|
||||
type_=mem_type,
|
||||
key=key,
|
||||
content=content,
|
||||
user_id=chat_id,
|
||||
user_id=None if mem_type == "global" else chat_id,
|
||||
project_name=proj_name,
|
||||
category=category,
|
||||
importance=importance,
|
||||
@@ -1720,28 +1724,37 @@ async def execute_action(
|
||||
created_task,
|
||||
)
|
||||
|
||||
is_admin_user = settings.is_admin(chat_id)
|
||||
if raw_type in ("project", "proj"):
|
||||
mem_type = "project"
|
||||
proj_name = req_project
|
||||
elif raw_type == "global" and is_admin_user:
|
||||
mem_type = "global"
|
||||
proj_name = None
|
||||
else:
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
|
||||
try:
|
||||
existing = memory_manager.get_by_key(type_=mem_type, key=key, user_id=chat_id, project_name=proj_name)
|
||||
existing = memory_manager.get_by_key(type_=mem_type, key=key, user_id=None if mem_type == "global" else chat_id, project_name=proj_name)
|
||||
cat = category or (existing.category if existing else "general")
|
||||
imp = int(importance_raw) if importance_raw and importance_raw.isdigit() else (existing.importance if existing else 1)
|
||||
item, is_created = memory_manager.save_or_update(
|
||||
type_=mem_type,
|
||||
key=key,
|
||||
content=content,
|
||||
user_id=chat_id,
|
||||
user_id=None if mem_type == "global" else chat_id,
|
||||
project_name=proj_name,
|
||||
category=cat,
|
||||
importance=imp,
|
||||
created_by=chat_id,
|
||||
)
|
||||
type_label = f"📁 پروژه (<code>{escape_html(proj_name or '')}</code>)" if item.is_project else "👤 کاربر (سراسری)"
|
||||
if item.is_project:
|
||||
type_label = f"📁 پروژه (<code>{escape_html(proj_name or '')}</code>)"
|
||||
elif item.is_user:
|
||||
type_label = "👤 کاربر (سراسری)"
|
||||
else:
|
||||
type_label = "🌐 عمومی (سیستم)"
|
||||
badge = (
|
||||
f"\n\n🧠 <b>خاطره هوش مصنوعی بهروزرسانی شد:</b>\n"
|
||||
f"• 🏷️ <b>بخش:</b> {type_label}\n"
|
||||
@@ -1768,16 +1781,24 @@ async def execute_action(
|
||||
key = attrs.get("key") or attrs.get("slug") or attrs.get("_default")
|
||||
raw_type = (attrs.get("type") or "user").lower().strip()
|
||||
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
||||
is_admin_user = settings.is_admin(chat_id)
|
||||
|
||||
mem_type = "project" if raw_type in ("project", "proj") else "user"
|
||||
proj_name = req_project if mem_type == "project" else None
|
||||
if raw_type in ("project", "proj"):
|
||||
mem_type = "project"
|
||||
proj_name = req_project
|
||||
elif raw_type == "global" and is_admin_user:
|
||||
mem_type = "global"
|
||||
proj_name = None
|
||||
else:
|
||||
mem_type = "user"
|
||||
proj_name = None
|
||||
|
||||
try:
|
||||
if mem_id_raw and mem_id_raw.isdigit():
|
||||
ok = memory_manager.delete_by_id(int(mem_id_raw), user_id=chat_id, is_admin=is_admin)
|
||||
ok = memory_manager.delete_by_id(int(mem_id_raw), user_id=chat_id, is_admin=is_admin_user)
|
||||
elif key:
|
||||
ok = memory_manager.delete_by_key(
|
||||
type_=mem_type, key=key, user_id=chat_id, project_name=proj_name, is_admin=is_admin
|
||||
type_=mem_type, key=key, user_id=None if mem_type == "global" else chat_id, project_name=proj_name, is_admin=is_admin_user
|
||||
)
|
||||
else:
|
||||
return "\n⚠️ لطفاً شناسه یا کلید خاطره جهت حذف را مشخص کنید." if is_fa else "\n⚠️ Please specify memory id or key to delete.", side_effects, created_task
|
||||
|
||||
Binary file not shown.
@@ -622,6 +622,7 @@ def format_ftp_info(
|
||||
user: Optional[str] = None,
|
||||
path: str = "/",
|
||||
tls: bool = False,
|
||||
password: Optional[str] = None,
|
||||
lang: str = "fa",
|
||||
) -> str:
|
||||
"""Formats FTP connection and deployment status for Telegram HTML."""
|
||||
@@ -630,18 +631,20 @@ def format_ftp_info(
|
||||
host_str = f"<code>{escape_html(host)}:{port}</code>" if host else ("<i>(تنظیم نشده)</i>" if is_fa else "<i>(Not configured)</i>")
|
||||
user_str = f"<code>{escape_html(user)}</code>" if user else ("<i>(ندارد)</i>" if is_fa else "<i>(None)</i>")
|
||||
path_str = f"<code>{escape_html(path)}</code>"
|
||||
pass_str = f"<code>{'•' * min(len(password), 8)}</code> <i>(تنظیم شده)</i>" if password else ("<i>(ندارد)</i>" if is_fa else "<i>(None)</i>")
|
||||
tls_str = "بله (FTPS/TLS امن) 🔒" if tls else "خیر (FTP معمولی)"
|
||||
tls_str_en = "Yes (Secure FTPS/TLS) 🔒" if tls else "No (Standard FTP)"
|
||||
|
||||
if is_fa:
|
||||
return (
|
||||
f"🚀 <b>تنظیمات استقرار و دیپلوی FTP پروژه</b>\n\n"
|
||||
f"🚀 <b>تنظیمات استقرار و مشخصات FTP پروژه</b>\n\n"
|
||||
f"• 📁 <b>پروژه:</b> <code>{clean_p}</code>\n"
|
||||
f"• 🌐 <b>سرور / هاست FTP:</b> {host_str}\n"
|
||||
f"• 🌐 <b>سرور / هاست:</b> {host_str}\n"
|
||||
f"• 👤 <b>نام کاربری:</b> {user_str}\n"
|
||||
f"• 🔑 <b>رمز عبور:</b> {pass_str}\n"
|
||||
f"• 📂 <b>مسیر مقصد روی هاست:</b> {path_str}\n"
|
||||
f"• 🔒 <b>پروتکل امن (FTPS):</b> {tls_str}\n\n"
|
||||
f"💡 <i>در زمان دیپلوی، هوش مصنوعی فایلهای زائد، کش، سشنها و مخزن گیت (.git) را به صورت خودکار فیلتر کرده و تنها سورس اصلی و تمیز به سرور منتقل میشود.</i>"
|
||||
f"💡 <i>میتوانید با دکمههای زیر هر فیلد را به دلخواه ویرایش، تست یا ذخیره کنید. در زمان دیپلوی، فایلهای زائد، کش و .git به صورت خودکار فیلتر میشوند.</i>"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
@@ -649,9 +652,10 @@ def format_ftp_info(
|
||||
f"• 📁 <b>Project:</b> <code>{clean_p}</code>\n"
|
||||
f"• 🌐 <b>FTP Host:</b> {host_str}\n"
|
||||
f"• 👤 <b>Username:</b> {user_str}\n"
|
||||
f"• 📂 <b>Remote Target Path:</b> {path_str}\n"
|
||||
f"• 🔑 <b>Password:</b> {pass_str}\n"
|
||||
f"• 📂 <b>Remote Path:</b> {path_str}\n"
|
||||
f"• 🔒 <b>Secure FTPS/TLS:</b> {tls_str_en}\n\n"
|
||||
f"💡 <i>During deployment, unnecessary cache, temp, sessions, and .git files are automatically excluded for a clean production upload.</i>"
|
||||
f"💡 <i>Use the buttons below to edit fields, test connection, or deploy. Junk and cache files are excluded automatically.</i>"
|
||||
)
|
||||
|
||||
|
||||
|
||||
+127
-29
@@ -8,6 +8,9 @@ from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Tuple, Set
|
||||
import ftplib
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
logger = logging.getLogger("AGYFTPManager")
|
||||
|
||||
@@ -52,6 +55,10 @@ DEFAULT_FTP_EXCLUDES = [
|
||||
"env",
|
||||
"env/*",
|
||||
|
||||
# Dependencies & local packages
|
||||
"node_modules",
|
||||
"node_modules/*",
|
||||
|
||||
# Logs & temp files
|
||||
"*.log",
|
||||
"tmp",
|
||||
@@ -77,22 +84,25 @@ def should_exclude(rel_path: str, custom_excludes: Optional[List[str]] = None) -
|
||||
"""Checks if a relative path matches any exclusion pattern."""
|
||||
excludes = DEFAULT_FTP_EXCLUDES + (custom_excludes or [])
|
||||
norm_path = rel_path.replace("\\", "/").strip("/")
|
||||
parts = norm_path.split("/")
|
||||
parts = [p for p in norm_path.split("/") if p]
|
||||
|
||||
for pattern in excludes:
|
||||
pat = pattern.replace("\\", "/").strip("/")
|
||||
# Check against full relative path
|
||||
if fnmatch.fnmatch(norm_path, pat):
|
||||
return True
|
||||
# Check against individual directory / file parts
|
||||
for part in parts:
|
||||
if fnmatch.fnmatch(part, pat):
|
||||
return True
|
||||
# If pattern is a directory (e.g. .git/* or .git), match if path starts with it
|
||||
clean_pat = pat.rstrip("/*")
|
||||
|
||||
# Check against full relative path
|
||||
if fnmatch.fnmatch(norm_path, pat) or fnmatch.fnmatch(norm_path, clean_pat):
|
||||
return True
|
||||
|
||||
# If path starts with directory pattern
|
||||
if norm_path == clean_pat or norm_path.startswith(clean_pat + "/"):
|
||||
return True
|
||||
|
||||
# Check against individual directory / file parts
|
||||
for part in parts:
|
||||
if fnmatch.fnmatch(part, pat) or fnmatch.fnmatch(part, clean_pat):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -109,17 +119,27 @@ class FTPManager:
|
||||
timeout: int = 15,
|
||||
) -> ftplib.FTP:
|
||||
"""Helper to create and authenticate an FTP or FTPS client."""
|
||||
clean_host = host.strip()
|
||||
clean_user = user.strip() if user else ""
|
||||
clean_pass = password.strip() if password else ""
|
||||
|
||||
if tls:
|
||||
ftp = ftplib.FTP_TLS(timeout=timeout)
|
||||
else:
|
||||
ftp = ftplib.FTP(timeout=timeout)
|
||||
|
||||
ftp.connect(host=host, port=port, timeout=timeout)
|
||||
ftp.login(user=user or "anonymous", passwd=password or "")
|
||||
|
||||
ftp.encoding = "utf-8"
|
||||
ftp.connect(host=clean_host, port=int(port), timeout=timeout)
|
||||
|
||||
if tls and isinstance(ftp, ftplib.FTP_TLS):
|
||||
ftp.auth()
|
||||
ftp.login(user=clean_user or "anonymous", passwd=clean_pass)
|
||||
ftp.prot_p() # Secure data connection
|
||||
|
||||
else:
|
||||
ftp.login(user=clean_user or "anonymous", passwd=clean_pass)
|
||||
|
||||
# Force passive mode (standard for firewalls / NAT)
|
||||
ftp.set_pasv(True)
|
||||
return ftp
|
||||
|
||||
async def test_connection(
|
||||
@@ -130,39 +150,72 @@ class FTPManager:
|
||||
password: str = "",
|
||||
path: str = "/",
|
||||
tls: bool = False,
|
||||
timeout: int = 10,
|
||||
timeout: int = 12,
|
||||
) -> Tuple[bool, str]:
|
||||
"""Tests FTP credentials and verifies access to the target remote directory."""
|
||||
if not host or not host.strip():
|
||||
return False, "آدرس هاست (Host) FTP مشخص نشده است."
|
||||
|
||||
def _test():
|
||||
ftp = None
|
||||
try:
|
||||
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout)
|
||||
|
||||
# Check root PWD
|
||||
init_pwd = ftp.pwd()
|
||||
|
||||
# Test CWD to target path if specified
|
||||
target_path = path.strip() if path else "/"
|
||||
if target_path and target_path != "/":
|
||||
try:
|
||||
ftp.cwd(target_path)
|
||||
except Exception as e:
|
||||
pwd = ftp.pwd()
|
||||
ftp.quit()
|
||||
return False, f"اتصال به FTP برقرار شد، اما مسیر ریموت «{target_path}» یافت نشد (مسیر فعلی: {pwd}): {e}"
|
||||
curr_pwd = ftp.pwd() if ftp else "/"
|
||||
try:
|
||||
ftp.quit()
|
||||
except Exception:
|
||||
pass
|
||||
return False, f"اتصال به سرور برقرار شد، اما مسیر ریموت «{target_path}» یافت نشد (مسیر فعلی: {curr_pwd}): {e}"
|
||||
|
||||
pwd = ftp.pwd()
|
||||
final_pwd = ftp.pwd()
|
||||
|
||||
# Test directory listing
|
||||
item_count = 0
|
||||
try:
|
||||
listing = ftp.nlst()
|
||||
count = len(listing)
|
||||
item_count = len(listing)
|
||||
except Exception:
|
||||
count = 0
|
||||
ftp.quit()
|
||||
return True, f"اتصال با موفقیت برقرار شد. مسیر فعلی: <code>{pwd}</code> (تعداد آیتمها: {count})"
|
||||
except (socket.gaierror, socket.timeout) as e:
|
||||
return False, f"خطای شبکه / نامعتبر بودن آدرس سرور ({host}:{port}): {e}"
|
||||
try:
|
||||
lines = []
|
||||
ftp.dir(lines.append)
|
||||
item_count = len(lines)
|
||||
except Exception:
|
||||
item_count = 0
|
||||
|
||||
try:
|
||||
ftp.quit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
tls_label = " (FTPS/TLS امن)" if tls else " (FTP معمولی)"
|
||||
return True, f"اتصال با موفقیت برقرار شد{tls_label} | مسیر: <code>{final_pwd}</code> | تعداد فایلها و پوشهها: {item_count}"
|
||||
|
||||
except socket.gaierror as e:
|
||||
return False, f"نام دامنه/هاست سرور یافت نشد ({host}): {e}"
|
||||
except (socket.timeout, TimeoutError) as e:
|
||||
return False, f"مهلت زمانی اتصال به سرور به پایان رسید (Timeout روی پورت {port}): {e}"
|
||||
except ConnectionRefusedError as e:
|
||||
return False, f"اتصال توسط سرور رد شد (پورت {port} بسته است یا FTP روی آن فعال نیست): {e}"
|
||||
except ftplib.error_perm as e:
|
||||
return False, f"خطای احراز هویت / دسترسی FTP: {e}"
|
||||
return False, f"خطای نام کاربری یا رمز عبور (دسترسی نامعتبر): {e}"
|
||||
except Exception as e:
|
||||
return False, f"خطا در برقراری ارتباط با FTP: {e}"
|
||||
finally:
|
||||
if ftp:
|
||||
try:
|
||||
ftp.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return await asyncio.to_thread(_test)
|
||||
|
||||
@@ -177,11 +230,16 @@ class FTPManager:
|
||||
tls: bool = False,
|
||||
custom_excludes: Optional[List[str]] = None,
|
||||
dry_run: bool = False,
|
||||
branch: str = "production",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Deploys files from workspace_path to the remote FTP server cleanly.
|
||||
Deploys files from the specified git branch (default: production) to the remote FTP server cleanly.
|
||||
Extracts files directly from the git branch to ensure exact production code is deployed.
|
||||
Skips all temporary, git, session, and junk files.
|
||||
"""
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
ws = Path(workspace_path).expanduser().resolve()
|
||||
if not ws.exists() or not ws.is_dir():
|
||||
return {
|
||||
@@ -199,8 +257,42 @@ class FTPManager:
|
||||
uploaded_files: List[str] = []
|
||||
skipped_files: List[str] = []
|
||||
total_bytes = 0
|
||||
temp_export_dir = None
|
||||
|
||||
try:
|
||||
# 1. Determine source directory: if git exists, export the exact production branch
|
||||
source_dir = ws
|
||||
if (ws / ".git").exists():
|
||||
try:
|
||||
# Check if branch exists
|
||||
branches_proc = subprocess.run(
|
||||
["git", "branch", "--list", branch],
|
||||
cwd=str(ws),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
target_branch = branch if branch in branches_proc.stdout else "HEAD"
|
||||
|
||||
temp_export_dir = Path(tempfile.mkdtemp(prefix="ftp_deploy_prod_"))
|
||||
# Use git archive to cleanly extract files from the target branch
|
||||
archive_proc = subprocess.Popen(
|
||||
["git", "archive", target_branch],
|
||||
cwd=str(ws),
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
tar_proc = subprocess.Popen(
|
||||
["tar", "-x", "-C", str(temp_export_dir)],
|
||||
stdin=archive_proc.stdout,
|
||||
)
|
||||
archive_proc.stdout.close()
|
||||
tar_proc.communicate()
|
||||
|
||||
if tar_proc.returncode == 0:
|
||||
source_dir = temp_export_dir
|
||||
except Exception as ge:
|
||||
logger.warning(f"Git archive export failed for {branch} (falling back to workspace): {ge}")
|
||||
source_dir = ws
|
||||
|
||||
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout=20)
|
||||
|
||||
# Navigate or create remote root path
|
||||
@@ -230,11 +322,11 @@ class FTPManager:
|
||||
|
||||
ensure_remote_dir(target_root)
|
||||
|
||||
# Scan workspace
|
||||
# Scan source directory
|
||||
all_files_to_upload: List[Tuple[Path, str]] = [] # (local_path, rel_path)
|
||||
|
||||
for root, dirs, files in os.walk(str(ws)):
|
||||
rel_dir = os.path.relpath(root, str(ws))
|
||||
for root, dirs, files in os.walk(str(source_dir)):
|
||||
rel_dir = os.path.relpath(root, str(source_dir))
|
||||
if rel_dir == ".":
|
||||
rel_dir = ""
|
||||
|
||||
@@ -309,6 +401,12 @@ class FTPManager:
|
||||
"duration": round(time.time() - start_time, 2),
|
||||
"uploaded_list": uploaded_files,
|
||||
}
|
||||
finally:
|
||||
if temp_export_dir and Path(temp_export_dir).exists():
|
||||
try:
|
||||
shutil.rmtree(temp_export_dir, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return await asyncio.to_thread(_run_deploy)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import html
|
||||
import logging
|
||||
import asyncio
|
||||
import urllib.request
|
||||
@@ -12,6 +13,11 @@ from config import settings
|
||||
|
||||
logger = logging.getLogger("AGYGitManager")
|
||||
|
||||
def escape_html(text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
return html.escape(str(text), quote=False)
|
||||
|
||||
DEFAULT_GITIGNORE = """# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
+154
-45
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user