feat: add manual instagram session login
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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';
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user