From 3b2e5a320589b9a83b6b90bd56f8ad4c555ef70b Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 5 Aug 2026 21:28:15 +0500 Subject: [PATCH] feat: add manual instagram session login --- db/migrations/046_insta_parser_settings.sql | 1 + db/migrations/047_insta_manual_auth.sql | 12 +++++ src/vk_parser_app/admin.py | 49 ++++++++++++++++++++- src/vk_parser_app/templates/workers.html | 21 ++++++++- src/vk_parser_app/workers/insta_parser.py | 44 ++++++++++++------ 5 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 db/migrations/047_insta_manual_auth.sql diff --git a/db/migrations/046_insta_parser_settings.sql b/db/migrations/046_insta_parser_settings.sql index e6562de..af1322c 100644 --- a/db/migrations/046_insta_parser_settings.sql +++ b/db/migrations/046_insta_parser_settings.sql @@ -8,6 +8,7 @@ VALUES ('insta_password', '""'::jsonb, 'secret', 'Instagram password', 'Password for the Instagram account used by instagrapi.', 'Instagram Parser'), ('insta_proxy_url', '""'::jsonb, 'str', 'Instagram proxy URL', 'Optional stable proxy, for example http://user:pass@host:port.', 'Instagram Parser'), ('insta_session_path', '"insta_session.json"'::jsonb, 'str', 'Instagram session path', 'Path to the persisted instagrapi session settings file.', 'Instagram Parser'), + ('insta_auth_status', '""'::jsonb, 'str', 'Instagram auth status', 'Last manual Instagram login result.', 'Instagram Parser'), ('insta_fetch_count', '5'::jsonb, 'int', 'Posts to inspect', 'How many latest posts to inspect per account visit.', 'Instagram Parser'), ('insta_delay_base_minutes', '35'::jsonb, 'int', 'Account visit delay, minutes', 'Base pause after checking one Instagram account.', 'Instagram Parser'), ('insta_delay_random_minutes', '5'::jsonb, 'int', 'Delay random spread, minutes', 'Random +/- spread added to the base account visit delay.', 'Instagram Parser'), diff --git a/db/migrations/047_insta_manual_auth.sql b/db/migrations/047_insta_manual_auth.sql new file mode 100644 index 0000000..f80d5b5 --- /dev/null +++ b/db/migrations/047_insta_manual_auth.sql @@ -0,0 +1,12 @@ +INSERT INTO app_settings(key, value_json, value_type, title, description, category) +VALUES + ('insta_auth_status', '""'::jsonb, 'str', 'Instagram auth status', 'Last manual Instagram login result.', 'Instagram Parser') +ON CONFLICT (key) DO NOTHING; + +UPDATE app_settings +SET description='Used only by the manual Instagram login button. The parser reads the saved session file and does not auto-login.' +WHERE key='insta_login'; + +UPDATE app_settings +SET description='Used only by the manual Instagram login button. The parser reads the saved session file and does not auto-login.' +WHERE key='insta_password'; diff --git a/src/vk_parser_app/admin.py b/src/vk_parser_app/admin.py index 53acc71..cd346ec 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 +from .workers.insta_parser import InstaParserWorker, instagram_login COOKIE_NAME = "vk_parser_admin" VK_OAUTH_VERIFIER_COOKIE = "vk_oauth_verifier" @@ -196,6 +196,7 @@ CATEGORY_TITLES = { "MAX Poster": "MAX-постер", "Publishing": "Публикации", "Daily Report": "Ежедневный отчет", + "Instagram Parser": "Instagram-парсер", "Parser": "Парсер", "Uploader": "Аплоадер", "VK": "VK API", @@ -212,6 +213,7 @@ CATEGORY_ORDER = { "Site Poster": 52, "Publishing": 53, "Daily Report": 55, + "Instagram Parser": 56, "Parser": 60, "VK": 70, "Uploader": 80, @@ -3949,6 +3951,51 @@ async def worker_toggle(request: Request, worker_name: str, csrf_token: str = Fo return redirect("/workers") +@app.post("/instagram-auth/login") +async def instagram_auth_login(request: Request, csrf_token: str = Form(...)): + user = await get_current_user(request) + if not user: + return redirect("/login") + require_csrf(user, csrf_token) + login = str(await fetch_setting("insta_login", "") or "").strip() + password = str(await fetch_setting("insta_password", "") or "").strip() + proxy = str(await fetch_setting("insta_proxy_url", "") or "").strip() + session_path = str(await fetch_setting("insta_session_path", "insta_session.json") or "").strip() + pool = await get_pool() + if not login or not password: + status_text = "missing login or password" + else: + try: + await asyncio.to_thread(instagram_login, login, password, proxy, session_path) + status_text = f"ok: session saved to {session_path or 'insta_session.json'} at {datetime.now(timezone.utc).isoformat()}" + await pool.execute( + """ + INSERT INTO app_settings(key, value_json, value_type, title, description, category) + VALUES('insta_cooldown_until', '""'::jsonb, 'str', 'Cooldown until', '', 'Instagram Parser') + ON CONFLICT (key) DO UPDATE + SET value_json='""'::jsonb, + description='', + updated_at=NOW() + """ + ) + except Exception as exc: + status_text = f"failed: {str(exc)[:1000]}" + await pool.execute( + """ + INSERT INTO app_settings(key, value_json, value_type, title, description, category, updated_by) + VALUES('insta_auth_status', $1::jsonb, 'str', 'Instagram auth status', 'Last manual Instagram login result.', 'Instagram Parser', $2) + ON CONFLICT (key) DO UPDATE + SET value_json=$1::jsonb, + updated_by=$2, + updated_at=NOW() + """, + json.dumps(status_text), + user["id"], + ) + await audit(user["id"], "instagram.login", "setting", None, {"status": status_text}) + 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 93c0952..933df82 100644 --- a/src/vk_parser_app/templates/workers.html +++ b/src/vk_parser_app/templates/workers.html @@ -8,6 +8,25 @@
Управление фоновыми процессами, категориями, расписанием и настройками AI.
+
+
+
+
+ + Instagram +
+
Session-файл: настройка insta_session_path
+
+
+ + +
+
+
+
@@ -428,7 +447,7 @@ const form = event.target; if (!(form instanceof HTMLFormElement)) return; const action = form.getAttribute("action") || ""; - if (action.startsWith("/workers") || action.startsWith("/settings") || action.startsWith("/branding") || action.includes("-schedule/")) { + if (action.startsWith("/workers") || action.startsWith("/instagram-auth") || action.startsWith("/settings") || action.startsWith("/branding") || action.includes("-schedule/")) { saveDetails(); sessionStorage.setItem(scrollKey, String(window.scrollY)); } diff --git a/src/vk_parser_app/workers/insta_parser.py b/src/vk_parser_app/workers/insta_parser.py index b998cac..ffc942e 100644 --- a/src/vk_parser_app/workers/insta_parser.py +++ b/src/vk_parser_app/workers/insta_parser.py @@ -109,6 +109,10 @@ class NeverRaised(Exception): pass +class InstagramAuthRequired(Exception): + pass + + def load_instagrapi(): from instagrapi import Client import instagrapi.exceptions as exc @@ -122,6 +126,17 @@ def load_instagrapi(): } +def instagram_login(login: str, password: str, proxy: str, session_path: str) -> None: + Client, _ = load_instagrapi() + client = Client() + if proxy: + client.set_proxy(proxy) + path = Path(session_path or "insta_session.json") + path.parent.mkdir(parents=True, exist_ok=True) + client.login(login, password) + client.dump_settings(path) + + class InstaParserWorker: def __init__(self) -> None: self.pool = None @@ -246,7 +261,7 @@ class InstaParserWorker: except ValueError: return None - def setup_client(self, login: str, password: str, proxy: str, session_path: str) -> None: + def setup_client(self, proxy: str, session_path: str) -> None: if self.client is not None: return Client, exceptions = load_instagrapi() @@ -255,10 +270,10 @@ class InstaParserWorker: if proxy: client.set_proxy(proxy) path = Path(session_path or "insta_session.json") - if path.exists(): - client.load_settings(path) - client.login(login, password) - client.dump_settings(path) + if not path.exists(): + raise InstagramAuthRequired(f"Instagram session file not found: {path}") + client.load_settings(path) + client.get_timeline_feed() self.client = client async def save_post(self, source: dict, post: dict, status: str, skip_reason: str | None, media: list[dict]) -> int | None: @@ -415,23 +430,24 @@ class InstaParserWorker: await self.heartbeat.beat(self.pool, status="cooldown", meta={"until": until.isoformat()}) return False - login = str(await fetch_setting("insta_login", "") or "").strip() - password = str(await fetch_setting("insta_password", "") or "").strip() proxy = str(await fetch_setting("insta_proxy_url", "") or "").strip() session_path = str(await fetch_setting("insta_session_path", "insta_session.json") or "").strip() fetch_count = max(1, min(20, await fetch_int_setting("insta_fetch_count", 5))) cooldown_hours = max(1, await fetch_int_setting("insta_cooldown_hours", 12)) request_pause_sec = max(0.0, await fetch_float_setting("insta_request_pause_sec", 2.0)) - if not login or not password: - await self.heartbeat.beat(self.pool, status="missing_credentials") - return False - try: - await asyncio.to_thread(self.setup_client, login, password, proxy, session_path) + await asyncio.to_thread(self.setup_client, proxy, session_path) + except InstagramAuthRequired as exc: + await self.heartbeat.beat(self.pool, status="auth_required", meta={"error": str(exc)}) + await self.set_cooldown(cooldown_hours, str(exc)) + await send_parser_error_alert(WORKER_INSTA_PARSER, "auth", str(exc)) + return False except Exception as exc: - await self.set_cooldown(cooldown_hours, f"login failed: {exc}") - await send_parser_error_alert(WORKER_INSTA_PARSER, "login", str(exc)) + self.client = None + await self.heartbeat.beat(self.pool, status="auth_required", meta={"error": str(exc)}) + await self.set_cooldown(cooldown_hours, f"session failed: {exc}") + await send_parser_error_alert(WORKER_INSTA_PARSER, "session", str(exc)) return False source = await self.active_source()