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_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_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_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_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_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'),
|
('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.tg_reactor import TelegramReactor
|
||||||
from .workers.vk_poster import VKPoster
|
from .workers.vk_poster import VKPoster
|
||||||
from .workers.vk_storage_uploader import TelegramStorageUploader
|
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"
|
COOKIE_NAME = "vk_parser_admin"
|
||||||
VK_OAUTH_VERIFIER_COOKIE = "vk_oauth_verifier"
|
VK_OAUTH_VERIFIER_COOKIE = "vk_oauth_verifier"
|
||||||
@@ -196,6 +196,7 @@ CATEGORY_TITLES = {
|
|||||||
"MAX Poster": "MAX-постер",
|
"MAX Poster": "MAX-постер",
|
||||||
"Publishing": "Публикации",
|
"Publishing": "Публикации",
|
||||||
"Daily Report": "Ежедневный отчет",
|
"Daily Report": "Ежедневный отчет",
|
||||||
|
"Instagram Parser": "Instagram-парсер",
|
||||||
"Parser": "Парсер",
|
"Parser": "Парсер",
|
||||||
"Uploader": "Аплоадер",
|
"Uploader": "Аплоадер",
|
||||||
"VK": "VK API",
|
"VK": "VK API",
|
||||||
@@ -212,6 +213,7 @@ CATEGORY_ORDER = {
|
|||||||
"Site Poster": 52,
|
"Site Poster": 52,
|
||||||
"Publishing": 53,
|
"Publishing": 53,
|
||||||
"Daily Report": 55,
|
"Daily Report": 55,
|
||||||
|
"Instagram Parser": 56,
|
||||||
"Parser": 60,
|
"Parser": 60,
|
||||||
"VK": 70,
|
"VK": 70,
|
||||||
"Uploader": 80,
|
"Uploader": 80,
|
||||||
@@ -3949,6 +3951,51 @@ async def worker_toggle(request: Request, worker_name: str, csrf_token: str = Fo
|
|||||||
return redirect("/workers")
|
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")
|
@app.post("/settings/save")
|
||||||
async def settings_save(request: Request, csrf_token: str = Form(...), key: str = Form(...), value: str = Form(...)):
|
async def settings_save(request: Request, csrf_token: str = Form(...), key: str = Form(...), value: str = Form(...)):
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
|
|||||||
@@ -8,6 +8,25 @@
|
|||||||
<div class="text-app-textMuted text-sm">Управление фоновыми процессами, категориями, расписанием и настройками AI.</div>
|
<div class="text-app-textMuted text-sm">Управление фоновыми процессами, категориями, расписанием и настройками AI.</div>
|
||||||
</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 -->
|
<!-- Workers Panel -->
|
||||||
<div class="card overflow-hidden mb-8">
|
<div class="card overflow-hidden mb-8">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
@@ -428,7 +447,7 @@
|
|||||||
const form = event.target;
|
const form = event.target;
|
||||||
if (!(form instanceof HTMLFormElement)) return;
|
if (!(form instanceof HTMLFormElement)) return;
|
||||||
const action = form.getAttribute("action") || "";
|
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();
|
saveDetails();
|
||||||
sessionStorage.setItem(scrollKey, String(window.scrollY));
|
sessionStorage.setItem(scrollKey, String(window.scrollY));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ class NeverRaised(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InstagramAuthRequired(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def load_instagrapi():
|
def load_instagrapi():
|
||||||
from instagrapi import Client
|
from instagrapi import Client
|
||||||
import instagrapi.exceptions as exc
|
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:
|
class InstaParserWorker:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.pool = None
|
self.pool = None
|
||||||
@@ -246,7 +261,7 @@ class InstaParserWorker:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
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:
|
if self.client is not None:
|
||||||
return
|
return
|
||||||
Client, exceptions = load_instagrapi()
|
Client, exceptions = load_instagrapi()
|
||||||
@@ -255,10 +270,10 @@ class InstaParserWorker:
|
|||||||
if proxy:
|
if proxy:
|
||||||
client.set_proxy(proxy)
|
client.set_proxy(proxy)
|
||||||
path = Path(session_path or "insta_session.json")
|
path = Path(session_path or "insta_session.json")
|
||||||
if path.exists():
|
if not path.exists():
|
||||||
client.load_settings(path)
|
raise InstagramAuthRequired(f"Instagram session file not found: {path}")
|
||||||
client.login(login, password)
|
client.load_settings(path)
|
||||||
client.dump_settings(path)
|
client.get_timeline_feed()
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
async def save_post(self, source: dict, post: dict, status: str, skip_reason: str | None, media: list[dict]) -> int | None:
|
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()})
|
await self.heartbeat.beat(self.pool, status="cooldown", meta={"until": until.isoformat()})
|
||||||
return False
|
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()
|
proxy = str(await fetch_setting("insta_proxy_url", "") or "").strip()
|
||||||
session_path = str(await fetch_setting("insta_session_path", "insta_session.json") 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)))
|
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))
|
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))
|
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:
|
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:
|
except Exception as exc:
|
||||||
await self.set_cooldown(cooldown_hours, f"login failed: {exc}")
|
self.client = None
|
||||||
await send_parser_error_alert(WORKER_INSTA_PARSER, "login", str(exc))
|
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
|
return False
|
||||||
|
|
||||||
source = await self.active_source()
|
source = await self.active_source()
|
||||||
|
|||||||
Reference in New Issue
Block a user