diff --git a/main.py b/main.py index 985e5e1..30a6d05 100644 --- a/main.py +++ b/main.py @@ -3,9 +3,9 @@ import time import json import urllib.parse import traceback -import subprocess # <--- اضافه شد -import sys # <--- اضافه شد -import threading # <--- اضافه شد +import subprocess +import sys +import threading from flask import Flask, request, jsonify, send_file from playwright.sync_api import sync_playwright @@ -15,6 +15,10 @@ CHROMIUM_PATH = "/usr/bin/chromium-browser" if not os.path.exists(CHROMIUM_PATH): CHROMIUM_PATH = "/usr/bin/chromium" +# --- قفل برای صف‌بندی درخواست‌های سنگین --- +# این قفل باعث می‌شود لایک/فالو/کامنت یکی‌یکی انجام شوند +TASK_LOCK = threading.Lock() + # مرورگر اصلی (فقط یک بار اجرا می‌شود) playwright_instance = None browser = None @@ -130,228 +134,237 @@ def inject_cookies(context, cookies_data): # 🚀 مسیرها (Routes) # ======================================================= +@app.route('/') +def health_check(): + """این مسیر همیشه سریع جواب می‌دهد، حتی اگر ربات مشغول باشد""" + return "OK - Server is Alive", 200 + @app.route('/auto_like', methods=['POST']) def auto_like(): - context = None - page = None - try: - # 1. دریافت داده‌ها - data = request.json - proxy_str = data.get('proxy') # <--- گرفتن پروکسی از ورودی - cookies = data.get('cookies') - post_url = data.get('post_url') - - print(f"Request received for LIKE using proxy: {proxy_str}") - - # 2. ساخت کانتکست اختصاصی - context = create_context_with_proxy(proxy_str) - page = context.new_page() - - # 3. تزریق کوکی و رفتن به صفحه - inject_cookies(context, cookies) - + # استفاده از قفل: اگر کسی داخل باشد، بقیه منتظر می‌مانند (صف) + with TASK_LOCK: + context = None + page = None try: - page.goto(post_url, wait_until="networkidle", timeout=60000) - except: pass - - # 4. لاگیک لایک (همان کد هوشمند قبلی) - status = "Failed" - target_btn = None - - # تشخیص وضعیت - for _ in range(20): - if page.locator('svg[aria-label="Unlike"]').count() > 0 or page.locator('svg[aria-label="نپسندیدن"]').count() > 0: - status = "Already Liked" - break + # 1. دریافت داده‌ها + data = request.json + proxy_str = data.get('proxy') + cookies = data.get('cookies') + post_url = data.get('post_url') - likes = page.locator('svg[aria-label="Like"]') - if likes.count() == 0: likes = page.locator('svg[aria-label="پسندیدن"]') - - if likes.count() > 0 and likes.first.is_visible(): - target_btn = likes.first - break - page.wait_for_timeout(1000) + print(f"Request received for LIKE using proxy: {proxy_str}") - if status == "Already Liked": - pass - elif target_btn: - target_btn.click(force=True) - page.wait_for_timeout(2000) - if page.locator('svg[aria-label="Unlike"]').count() > 0: - status = "Success: Liked [Verified]" + # 2. ساخت کانتکست اختصاصی + context = create_context_with_proxy(proxy_str) + page = context.new_page() + + # 3. تزریق کوکی و رفتن به صفحه + inject_cookies(context, cookies) + + try: + page.goto(post_url, wait_until="networkidle", timeout=60000) + except: pass + + # 4. لاگیک لایک + status = "Failed" + target_btn = None + + # تشخیص وضعیت + for _ in range(20): + if page.locator('svg[aria-label="Unlike"]').count() > 0 or page.locator('svg[aria-label="نپسندیدن"]').count() > 0: + status = "Already Liked" + break + + likes = page.locator('svg[aria-label="Like"]') + if likes.count() == 0: likes = page.locator('svg[aria-label="پسندیدن"]') + + if likes.count() > 0 and likes.first.is_visible(): + target_btn = likes.first + break + page.wait_for_timeout(1000) + + if status == "Already Liked": + pass + elif target_btn: + target_btn.click(force=True) + page.wait_for_timeout(2000) + if page.locator('svg[aria-label="Unlike"]').count() > 0: + status = "Success: Liked [Verified]" + else: + status = "Success: Clicked (No Verify)" else: - status = "Success: Clicked (No Verify)" - else: - page.mouse.dblclick(page.viewport_size['width']/2, page.viewport_size['height']/2) - status = "Success: DoubleTap" + page.mouse.dblclick(page.viewport_size['width']/2, page.viewport_size['height']/2) + status = "Success: DoubleTap" - page.screenshot(path="current_view.png") - return jsonify({"status": status, "proxy_used": proxy_str}) + page.screenshot(path="current_view.png") + return jsonify({"status": status, "proxy_used": proxy_str}) - except Exception as e: - traceback.print_exc() - return jsonify({"error": str(e)}), 500 - finally: - # بسیار مهم: بستن کانتکست برای آزاد کردن رم و آی‌پی - if context: context.close() + except Exception as e: + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + finally: + if context: context.close() @app.route('/auto_comment', methods=['POST']) def auto_comment(): - context = None - page = None - try: - data = request.json - proxy_str = data.get('proxy') - cookies = data.get('cookies') - post_url = data.get('post_url') - comment_text = data.get('comment_text') - - print(f"Request received for COMMENT using proxy: {proxy_str}") - - context = create_context_with_proxy(proxy_str) - page = context.new_page() - inject_cookies(context, cookies) - + with TASK_LOCK: + context = None + page = None try: - page.goto(post_url, wait_until="networkidle", timeout=60000) - except: pass + data = request.json + proxy_str = data.get('proxy') + cookies = data.get('cookies') + post_url = data.get('post_url') + comment_text = data.get('comment_text') - if page.locator("input[name='username']").count() > 0: - return jsonify({"status": "Failed", "reason": "Login Redirect"}), 401 + print(f"Request received for COMMENT using proxy: {proxy_str}") - # لاگیک کامنت (همان کد نهایی و وریفای شده) - bubbles = [page.locator(s) for s in ['svg[aria-label="Comment"]', 'svg[aria-label="محاوره"]', 'svg[class*="_ab6-"]']] - bubbles.append(page.get_by_role("button").filter(has_text="Comment")) - - for b in bubbles: - if b.count() > 0 and b.first.is_visible(): - b.first.click(force=True) - break - - input_ready = False - for _ in range(30): - placeholders = page.get_by_text("Add a comment...", exact=False) - if placeholders.count() > 0 and placeholders.last.is_visible(): - placeholders.last.click(force=True) - input_ready = True - break - forms = page.locator("form") - if forms.count() > 0 and forms.last.is_visible(): - forms.last.click(force=True) - input_ready = True - break - page.wait_for_timeout(1000) - - status = "Failed" - if input_ready: - page.wait_for_timeout(1000) - candidates = [ - page.locator('div[contenteditable="true"][role="textbox"]').last, - page.locator('form textarea').last - ] - final_box = next((c for c in candidates if c.is_visible()), None) + context = create_context_with_proxy(proxy_str) + page = context.new_page() + inject_cookies(context, cookies) - if final_box: - final_box.click() - final_box.type(comment_text, delay=100) - page.wait_for_timeout(500) - - post_btn = page.locator('div[role="button"]').filter(has_text="Post").last - if not post_btn.is_visible(): post_btn = page.locator('div[role="button"]').filter(has_text="ارسال").last - - if post_btn.is_visible(): - post_btn.click() - status = "Success: Posted [Verified]" - else: - page.keyboard.press("Enter") - status = "Success: Enter [Verified]" + try: + page.goto(post_url, wait_until="networkidle", timeout=60000) + except: pass - page.screenshot(path="current_view.png") - return jsonify({"status": status, "proxy_used": proxy_str}) + if page.locator("input[name='username']").count() > 0: + return jsonify({"status": "Failed", "reason": "Login Redirect"}), 401 - except Exception as e: - traceback.print_exc() - return jsonify({"error": str(e)}), 500 - finally: - if context: context.close() + # لاگیک کامنت + bubbles = [page.locator(s) for s in ['svg[aria-label="Comment"]', 'svg[aria-label="محاوره"]', 'svg[class*="_ab6-"]']] + bubbles.append(page.get_by_role("button").filter(has_text="Comment")) + + for b in bubbles: + if b.count() > 0 and b.first.is_visible(): + b.first.click(force=True) + break + + input_ready = False + for _ in range(30): + placeholders = page.get_by_text("Add a comment...", exact=False) + if placeholders.count() > 0 and placeholders.last.is_visible(): + placeholders.last.click(force=True) + input_ready = True + break + forms = page.locator("form") + if forms.count() > 0 and forms.last.is_visible(): + forms.last.click(force=True) + input_ready = True + break + page.wait_for_timeout(1000) + + status = "Failed" + if input_ready: + page.wait_for_timeout(1000) + candidates = [ + page.locator('div[contenteditable="true"][role="textbox"]').last, + page.locator('form textarea').last + ] + final_box = next((c for c in candidates if c.is_visible()), None) + + if final_box: + final_box.click() + final_box.type(comment_text, delay=100) + page.wait_for_timeout(500) + + post_btn = page.locator('div[role="button"]').filter(has_text="Post").last + if not post_btn.is_visible(): post_btn = page.locator('div[role="button"]').filter(has_text="ارسال").last + + if post_btn.is_visible(): + post_btn.click() + status = "Success: Posted [Verified]" + else: + page.keyboard.press("Enter") + status = "Success: Enter [Verified]" + + page.screenshot(path="current_view.png") + return jsonify({"status": status, "proxy_used": proxy_str}) + + except Exception as e: + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + finally: + if context: context.close() @app.route('/auto_follow', methods=['POST']) def auto_follow(): - context = None - page = None - try: - data = request.json - proxy_str = data.get('proxy') - cookies = data.get('cookies') - target_user = data.get('target_username') - - print(f"Request received for FOLLOW using proxy: {proxy_str}") - - context = create_context_with_proxy(proxy_str) - page = context.new_page() - inject_cookies(context, cookies) - + with TASK_LOCK: + context = None + page = None try: - page.goto(f"https://www.instagram.com/{target_user}/", wait_until="networkidle", timeout=60000) - except: pass + data = request.json + proxy_str = data.get('proxy') + cookies = data.get('cookies') + target_user = data.get('target_username') - status = "Failed" - target_btn = None - done_keys = ["Following", "Requested", "دنبال شد", "درخواست شد"] - follow_keys = ["Follow", "Follow Back", "دنبال کردن"] - - for _ in range(20): - buttons = page.locator("header button") - if buttons.count() == 0: buttons = page.locator("button") - found = False - for i in range(buttons.count()): - btn = buttons.nth(i) - if not btn.is_visible(): continue - txt = btn.text_content().strip() - - if any(k in txt for k in done_keys): - status = f"Already: {txt}" - found = True; break - - if ("Message" in txt or "پیام" in txt) and page.get_by_text("Follow", exact=True).count() == 0: - pass + print(f"Request received for FOLLOW using proxy: {proxy_str}") - if any(k == txt or k in txt for k in follow_keys) and "Following" not in txt: - target_btn = btn - found = True; break - if found: break - page.wait_for_timeout(1000) + context = create_context_with_proxy(proxy_str) + page = context.new_page() + inject_cookies(context, cookies) + + try: + page.goto(f"https://www.instagram.com/{target_user}/", wait_until="networkidle", timeout=60000) + except: pass - if "Already" in status: - pass - elif target_btn: - target_btn.click(force=True) - for _ in range(15): + status = "Failed" + target_btn = None + done_keys = ["Following", "Requested", "دنبال شد", "درخواست شد"] + follow_keys = ["Follow", "Follow Back", "دنبال کردن"] + + for _ in range(20): + buttons = page.locator("header button") + if buttons.count() == 0: buttons = page.locator("button") + found = False + for i in range(buttons.count()): + btn = buttons.nth(i) + if not btn.is_visible(): continue + txt = btn.text_content().strip() + + if any(k in txt for k in done_keys): + status = f"Already: {txt}" + found = True; break + + if ("Message" in txt or "پیام" in txt) and page.get_by_text("Follow", exact=True).count() == 0: + pass + + if any(k == txt or k in txt for k in follow_keys) and "Following" not in txt: + target_btn = btn + found = True; break + if found: break page.wait_for_timeout(1000) - full_text = page.locator("header").text_content() - if any(k in full_text for k in done_keys): - status = "Success: Followed [Verified]" - break - if "Success" not in status: status = "Success: Clicked (Timeout)" - - page.screenshot(path="current_view.png") - return jsonify({"status": status, "target": target_user}) - except Exception as e: - return jsonify({"error": str(e)}), 500 - finally: - if context: context.close() + if "Already" in status: + pass + elif target_btn: + target_btn.click(force=True) + for _ in range(15): + page.wait_for_timeout(1000) + full_text = page.locator("header").text_content() + if any(k in full_text for k in done_keys): + status = "Success: Followed [Verified]" + break + if "Success" not in status: status = "Success: Clicked (Timeout)" + + page.screenshot(path="current_view.png") + return jsonify({"status": status, "target": target_user}) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + finally: + if context: context.close() @app.route('/screenshot') def serve_screenshot(): + # اسکرین شات پشت قفل نمی‌ماند if os.path.exists("current_view.png"): return send_file("current_view.png", mimetype='image/png') return "No image" # ======================================================= -# 🔄 تابع آپدیت خودکار (اضافه شده) +# 🔄 تابع آپدیت خودکار # ======================================================= def auto_update_loop(): """هر 60 ثانیه چک می‌کند اگر آپدیت آمده بود، برنامه را ریستارت می‌کند""" @@ -381,7 +394,6 @@ def auto_update_loop(): print("[UPDATER] Restarting application...\n") # 5. Restart - # بستن مرورگر قبل از ریستارت (اختیاری ولی تمیزتر) if playwright_instance: playwright_instance.stop() @@ -389,7 +401,7 @@ def auto_update_loop(): except Exception as e: print(f"[UPDATER ERROR]: {e}") - time.sleep(60) # اگر خطا داد، صبر کن و دوباره تلاش کن + time.sleep(60) if __name__ == '__main__': # --- شروع ترد آپدیت کننده در پس‌زمینه --- @@ -397,6 +409,8 @@ if __name__ == '__main__': update_thread.start() print(">>> Auto-updater started in background...") - # حتما threaded=False باشد تا تداخل در Playwright پیش نیاید start_browser_engine() # استارت اولیه موتور - app.run(host='0.0.0.0', port=5000, threaded=False) + + # تغییر مهم: threaded=True فعال شد + # درخواست‌های همزمان پذیرفته می‌شوند، اما توابع سنگین با TASK_LOCK صف‌بندی می‌شوند + app.run(host='0.0.0.0', port=5000, threaded=True) \ No newline at end of file