diff --git a/requirements.txt b/requirements.txt index 4a9fb58..57b4a72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,4 @@ Pillow==11.3.0 python-multipart==0.0.20 uvicorn[standard]==0.35.0 yt-dlp==2026.6.9 -instagrapi==2.1.2 +instagrapi==2.6.9 diff --git a/src/vk_parser_app/admin.py b/src/vk_parser_app/admin.py index cd346ec..e8f9549 100644 --- a/src/vk_parser_app/admin.py +++ b/src/vk_parser_app/admin.py @@ -37,7 +37,7 @@ from .workers.tg_poster import TelegramPoster from .workers.tg_reactor import TelegramReactor from .workers.vk_poster import VKPoster from .workers.vk_storage_uploader import TelegramStorageUploader -from .workers.insta_parser import InstaParserWorker, instagram_login +from .workers.insta_parser import InstaParserWorker, InstagramCodeRequired, instagram_login COOKIE_NAME = "vk_parser_admin" VK_OAUTH_VERIFIER_COOKIE = "vk_oauth_verifier" @@ -3623,6 +3623,8 @@ async def workers(request: Request): vk_schedule=await vk_poster_schedule_rows(), category_titles=CATEGORY_TITLES, prompt_hints=PROMPT_HINTS, + instagram_auth_status=setting_values.get("insta_auth_status") or "", + instagram_cooldown_until=setting_values.get("insta_cooldown_until") or "", ), ) @@ -3952,7 +3954,7 @@ async def worker_toggle(request: Request, worker_name: str, csrf_token: str = Fo @app.post("/instagram-auth/login") -async def instagram_auth_login(request: Request, csrf_token: str = Form(...)): +async def instagram_auth_login(request: Request, csrf_token: str = Form(...), verification_code: str = Form("")): user = await get_current_user(request) if not user: return redirect("/login") @@ -3966,7 +3968,7 @@ async def instagram_auth_login(request: Request, csrf_token: str = Form(...)): status_text = "missing login or password" else: try: - await asyncio.to_thread(instagram_login, login, password, proxy, session_path) + await asyncio.to_thread(instagram_login, login, password, proxy, session_path, verification_code.strip()) status_text = f"ok: session saved to {session_path or 'insta_session.json'} at {datetime.now(timezone.utc).isoformat()}" await pool.execute( """ @@ -3978,8 +3980,14 @@ async def instagram_auth_login(request: Request, csrf_token: str = Form(...)): updated_at=NOW() """ ) + except InstagramCodeRequired as exc: + status_text = f"code_required: {exc.choice}" except Exception as exc: - status_text = f"failed: {str(exc)[:1000]}" + exc_name = exc.__class__.__name__ + if exc_name in {"TwoFactorRequired", "ChallengeRequired"}: + status_text = f"code_required: {exc_name}" + else: + status_text = f"failed: {exc_name}: {str(exc)[:1000]}" await pool.execute( """ INSERT INTO app_settings(key, value_json, value_type, title, description, category, updated_by) @@ -3996,6 +4004,29 @@ async def instagram_auth_login(request: Request, csrf_token: str = Form(...)): return redirect("/workers") +@app.post("/instagram-auth/clear-cooldown") +async def instagram_auth_clear_cooldown(request: Request, csrf_token: str = Form(...)): + user = await get_current_user(request) + if not user: + return redirect("/login") + require_csrf(user, csrf_token) + pool = await get_pool() + await pool.execute( + """ + INSERT INTO app_settings(key, value_json, value_type, title, description, category, updated_by) + VALUES('insta_cooldown_until', '""'::jsonb, 'str', 'Cooldown until', '', 'Instagram Parser', $1) + ON CONFLICT (key) DO UPDATE + SET value_json='""'::jsonb, + description='manual reset', + updated_by=$1, + updated_at=NOW() + """, + user["id"], + ) + await audit(user["id"], "instagram.cooldown_reset", "setting", None) + return redirect("/workers") + + @app.post("/settings/save") async def settings_save(request: Request, csrf_token: str = Form(...), key: str = Form(...), value: str = Form(...)): user = await get_current_user(request) diff --git a/src/vk_parser_app/templates/workers.html b/src/vk_parser_app/templates/workers.html index 933df82..e138c72 100644 --- a/src/vk_parser_app/templates/workers.html +++ b/src/vk_parser_app/templates/workers.html @@ -8,25 +8,6 @@
Управление фоновыми процессами, категориями, расписанием и настройками AI.
-
-
-
-
- - Instagram -
-
Session-файл: настройка insta_session_path
-
-
- - -
-
-
-
@@ -313,12 +294,50 @@
{% for category, rows in settings|groupby("category") %} -
+
{{ category_titles.get(category, category) }}
+ {% if category == "Instagram Parser" %} +
+
+
+
+ + Авторизация Instagram +
+
+ Статус: {{ instagram_auth_status or "—" }} +
+ {% if instagram_cooldown_until %} +
+ Cooldown: {{ instagram_cooldown_until }} +
+ {% endif %} +
+
+ + +
+
+
+ +
+ + +
+ +
+
+ {% endif %}
{% for s in rows %} diff --git a/src/vk_parser_app/workers/insta_parser.py b/src/vk_parser_app/workers/insta_parser.py index ffc942e..a0e51a3 100644 --- a/src/vk_parser_app/workers/insta_parser.py +++ b/src/vk_parser_app/workers/insta_parser.py @@ -113,6 +113,12 @@ class InstagramAuthRequired(Exception): pass +class InstagramCodeRequired(Exception): + def __init__(self, choice: Any) -> None: + self.choice = choice + super().__init__(f"Instagram requested verification code: {choice}") + + def load_instagrapi(): from instagrapi import Client import instagrapi.exceptions as exc @@ -126,14 +132,22 @@ def load_instagrapi(): } -def instagram_login(login: str, password: str, proxy: str, session_path: str) -> None: +def instagram_login(login: str, password: str, proxy: str, session_path: str, verification_code: str = "") -> None: Client, _ = load_instagrapi() client = Client() if proxy: client.set_proxy(proxy) + if verification_code: + client.challenge_code_handler = lambda username, choice: verification_code + else: + def challenge_code_handler(username: str, choice: Any) -> str: + raise InstagramCodeRequired(choice) + + client.challenge_code_handler = challenge_code_handler path = Path(session_path or "insta_session.json") path.parent.mkdir(parents=True, exist_ok=True) - client.login(login, password) + login_kwargs = {"verification_code": verification_code} if verification_code else {} + client.login(login, password, **login_kwargs) client.dump_settings(path)