fix: make instagram auth visible and manual
This commit is contained in:
+1
-1
@@ -10,4 +10,4 @@ Pillow==11.3.0
|
|||||||
python-multipart==0.0.20
|
python-multipart==0.0.20
|
||||||
uvicorn[standard]==0.35.0
|
uvicorn[standard]==0.35.0
|
||||||
yt-dlp==2026.6.9
|
yt-dlp==2026.6.9
|
||||||
instagrapi==2.1.2
|
instagrapi==2.6.9
|
||||||
|
|||||||
@@ -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, instagram_login
|
from .workers.insta_parser import InstaParserWorker, InstagramCodeRequired, 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"
|
||||||
@@ -3623,6 +3623,8 @@ async def workers(request: Request):
|
|||||||
vk_schedule=await vk_poster_schedule_rows(),
|
vk_schedule=await vk_poster_schedule_rows(),
|
||||||
category_titles=CATEGORY_TITLES,
|
category_titles=CATEGORY_TITLES,
|
||||||
prompt_hints=PROMPT_HINTS,
|
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")
|
@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)
|
user = await get_current_user(request)
|
||||||
if not user:
|
if not user:
|
||||||
return redirect("/login")
|
return redirect("/login")
|
||||||
@@ -3966,7 +3968,7 @@ async def instagram_auth_login(request: Request, csrf_token: str = Form(...)):
|
|||||||
status_text = "missing login or password"
|
status_text = "missing login or password"
|
||||||
else:
|
else:
|
||||||
try:
|
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()}"
|
status_text = f"ok: session saved to {session_path or 'insta_session.json'} at {datetime.now(timezone.utc).isoformat()}"
|
||||||
await pool.execute(
|
await pool.execute(
|
||||||
"""
|
"""
|
||||||
@@ -3978,8 +3980,14 @@ async def instagram_auth_login(request: Request, csrf_token: str = Form(...)):
|
|||||||
updated_at=NOW()
|
updated_at=NOW()
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
except InstagramCodeRequired as exc:
|
||||||
|
status_text = f"code_required: {exc.choice}"
|
||||||
except Exception as exc:
|
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(
|
await pool.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO app_settings(key, value_json, value_type, title, description, category, updated_by)
|
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")
|
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")
|
@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,25 +8,6 @@
|
|||||||
<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">
|
||||||
@@ -313,12 +294,50 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
{% for category, rows in settings|groupby("category") %}
|
{% for category, rows in settings|groupby("category") %}
|
||||||
<details class="card group/details" data-workers-details="settings:{{ category }}">
|
<details class="card group/details" data-workers-details="settings:{{ category }}" {% if category == "Instagram Parser" %}open{% endif %}>
|
||||||
<summary class="p-4 flex items-center justify-between cursor-pointer select-none hover:bg-app-surfaceHover transition-colors border-b border-app-border list-none">
|
<summary class="p-4 flex items-center justify-between cursor-pointer select-none hover:bg-app-surfaceHover transition-colors border-b border-app-border list-none">
|
||||||
<div class="text-lg font-bold text-white">{{ category_titles.get(category, category) }}</div>
|
<div class="text-lg font-bold text-white">{{ category_titles.get(category, category) }}</div>
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i>
|
<i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="p-6 bg-app-bg/30">
|
<div class="p-6 bg-app-bg/30">
|
||||||
|
{% if category == "Instagram Parser" %}
|
||||||
|
<div class="mb-8 p-5 bg-app-surface border border-app-border rounded-xl flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<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-2 break-words">
|
||||||
|
Статус: <span class="font-mono text-app-textMain">{{ instagram_auth_status or "—" }}</span>
|
||||||
|
</div>
|
||||||
|
{% if instagram_cooldown_until %}
|
||||||
|
<div class="text-xs text-app-warning mt-2 break-words">
|
||||||
|
Cooldown: <span class="font-mono">{{ instagram_cooldown_until }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/instagram-auth/clear-cooldown" class="m-0">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
|
||||||
|
<button class="btn btn-surface btn-sm" type="submit">
|
||||||
|
<i data-lucide="timer-reset" class="w-4 h-4"></i>
|
||||||
|
Сбросить cooldown
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/instagram-auth/login" class="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3 items-end">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Код из SMS/email/2FA, если Instagram его просит</label>
|
||||||
|
<input name="verification_code" class="input w-full font-mono" autocomplete="one-time-code" placeholder="Оставь пустым для первой попытки">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<i data-lucide="key-round" class="w-4 h-4"></i>
|
||||||
|
Войти
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
{% for s in rows %}
|
{% for s in rows %}
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ class InstagramAuthRequired(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InstagramCodeRequired(Exception):
|
||||||
|
def __init__(self, choice: Any) -> None:
|
||||||
|
self.choice = choice
|
||||||
|
super().__init__(f"Instagram requested verification code: {choice}")
|
||||||
|
|
||||||
|
|
||||||
def load_instagrapi():
|
def load_instagrapi():
|
||||||
from instagrapi import Client
|
from instagrapi import Client
|
||||||
import instagrapi.exceptions as exc
|
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, _ = load_instagrapi()
|
||||||
client = Client()
|
client = Client()
|
||||||
if proxy:
|
if proxy:
|
||||||
client.set_proxy(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 = Path(session_path or "insta_session.json")
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
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)
|
client.dump_settings(path)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user