feat: add manual instagram session login

This commit is contained in:
Your Name
2026-08-05 21:28:15 +05:00
parent fae611109e
commit 3b2e5a3205
5 changed files with 111 additions and 16 deletions
@@ -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'),
+12
View File
@@ -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';
+48 -1
View File
@@ -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)
+20 -1
View File
@@ -8,6 +8,25 @@
<div class="text-app-textMuted text-sm">Управление фоновыми процессами, категориями, расписанием и настройками AI.</div>
</div>
<div class="card mb-8">
<div class="p-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<div class="font-bold text-white flex items-center gap-2">
<i data-lucide="instagram" class="w-5 h-5 text-pink-400"></i>
Instagram
</div>
<div class="text-xs text-app-textMuted mt-1">Session-файл: настройка <span class="font-mono">insta_session_path</span></div>
</div>
<form method="post" action="/instagram-auth/login" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button class="btn btn-primary btn-sm" type="submit">
<i data-lucide="key-round" class="w-4 h-4"></i>
Войти и сохранить session
</button>
</form>
</div>
</div>
<!-- Workers Panel -->
<div class="card overflow-hidden mb-8">
<div class="overflow-x-auto">
@@ -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));
}
+29 -13
View File
@@ -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():
if not path.exists():
raise InstagramAuthRequired(f"Instagram session file not found: {path}")
client.load_settings(path)
client.login(login, password)
client.dump_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()