diff --git a/app/main.py b/app/main.py index 93bd93d..4aa7686 100644 --- a/app/main.py +++ b/app/main.py @@ -2166,6 +2166,66 @@ async def subscribe_via_bot(request: Request, token: str = "", name: str = ""): return resp +def _verify_pay_link(tg, job, exp, sig): + """Проверяет подписанную ссылку оплаты файла из бота (раздельные параметры).""" + if not SUB_LINK_SECRET: + return None + try: + if float(exp) < time.time(): + return None + expect = hmac.new(SUB_LINK_SECRET.encode(), f"{tg}:{exp}:{job}".encode(), hashlib.sha256).hexdigest() + if not hmac.compare_digest(str(sig), expect): + return None + return int(tg) + except Exception: + return None + + +@app.get("/pay") +async def pay_held_file(request: Request, tg: str = "", job: str = "", exp: str = "", sig: str = "", name: str = ""): + """Deep-link из бота на оплату придержанного файла: логиним tg: + открываем пейволл по job.""" + tgid = _verify_pay_link(tg, job, exp, sig) + if not tgid or not job: + return RedirectResponse(f"{SITE_BASE_URL}/?login=error", status_code=302) + account_id = f"tg:{tgid}" + _db_ensure_account(account_id) + sess = _db_create_session(account_id, "telegram", (name or "").strip()[:64] or "Telegram", "") + resp = RedirectResponse(f"{SITE_BASE_URL}/?pay_job={_urlparse.quote(job)}", status_code=302) + resp.set_cookie(SESSION_COOKIE, sess, max_age=SESSION_DAYS * 86400, + httponly=True, secure=True, samesite="lax", path="/") + return resp + + +@app.post("/api/held/release") +async def held_release(payload: dict, request: Request): + """Освободить придержанный файл БЕЗ оплаты — только владельцу с активной подпиской.""" + if not isinstance(payload, dict): + payload = {} + acc = _resolve_account(request, payload.get("user_id")) + job_id = (payload.get("job_id") or "").strip()[:128] + if not acc or not job_id: + raise HTTPException(status_code=400, detail="bad request") + if not _db_sub_active(acc): + raise HTTPException(status_code=403, detail="Нет активной подписки") + + def _release(): + with sqlite3.connect(DB_PATH, timeout=10) as con: + row = con.execute("SELECT user_id, url_res_f FROM FILES WHERE name_or_f=?", (job_id,)).fetchone() + if not row: + return False + uid_owner, urf = row + if (urf or "").strip() != "awaiting_payment": + return False + if acc.startswith("tg:") and str(uid_owner) != acc.split(":", 1)[1]: + return False + con.execute("UPDATE FILES SET url_res_f='-' WHERE name_or_f=?", (job_id,)) + con.commit() + return True + + ok = await asyncio.to_thread(_release) + return {"released": bool(ok)} + + @app.get("/api/me") async def me(request: Request): sess = _db_get_session(request.cookies.get(SESSION_COOKIE)) diff --git a/app/static/index.html b/app/static/index.html index 05fda9e..63f9de4 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -1371,6 +1371,7 @@ sub_autorenew_off: 'Автопродление выключено', sub_autorenew_disable: 'Отключить', sub_autorenew_enable: 'Включить', + bot_file_released: 'Файл освобождён — обработка началась. Результат придёт в бот.', paywall_cancel: 'Отмена', paywall_need_login: 'Для подписки войдите в аккаунт', seo_title: 'Как работает pdf2scan', @@ -1515,6 +1516,7 @@ sub_autorenew_off: 'Auto-renewal off', sub_autorenew_disable: 'Turn off', sub_autorenew_enable: 'Turn on', + bot_file_released: 'File released — processing started. The result will arrive in the bot.', paywall_cancel: 'Cancel', paywall_need_login: 'Log in to subscribe', seo_title: 'How pdf2scan works', @@ -1659,6 +1661,7 @@ sub_autorenew_off: 'Automatische Verlängerung aus', sub_autorenew_disable: 'Ausschalten', sub_autorenew_enable: 'Einschalten', + bot_file_released: 'Datei freigegeben — Verarbeitung gestartet. Das Ergebnis kommt in den Bot.', paywall_cancel: 'Abbrechen', paywall_need_login: 'Zum Abonnieren anmelden', seo_title: 'So funktioniert pdf2scan', @@ -1803,6 +1806,7 @@ sub_autorenew_off: 'Renouvellement automatique désactivé', sub_autorenew_disable: 'Désactiver', sub_autorenew_enable: 'Activer', + bot_file_released: 'Fichier débloqué — traitement lancé. Le résultat arrivera dans le bot.', paywall_cancel: 'Annuler', paywall_need_login: 'Connectez-vous pour vous abonner', seo_title: 'Comment fonctionne pdf2scan', @@ -1947,6 +1951,7 @@ sub_autorenew_off: 'Rinnovo automatico disattivato', sub_autorenew_disable: 'Disattiva', sub_autorenew_enable: 'Attiva', + bot_file_released: 'File sbloccato — elaborazione avviata. Il risultato arriverà nel bot.', paywall_cancel: 'Annulla', paywall_need_login: 'Accedi per abbonarti', seo_title: 'Come funziona pdf2scan', @@ -2091,6 +2096,7 @@ sub_autorenew_off: 'Renovación automática desactivada', sub_autorenew_disable: 'Desactivar', sub_autorenew_enable: 'Activar', + bot_file_released: 'Archivo liberado — procesamiento iniciado. El resultado llegará al bot.', paywall_cancel: 'Cancelar', paywall_need_login: 'Inicia sesión para suscribirte', seo_title: 'Cómo funciona pdf2scan', @@ -2235,6 +2241,7 @@ sub_autorenew_off: 'Renovação automática desativada', sub_autorenew_disable: 'Desativar', sub_autorenew_enable: 'Ativar', + bot_file_released: 'Arquivo liberado — processamento iniciado. O resultado chegará no bot.', paywall_cancel: 'Cancelar', paywall_need_login: 'Entre para assinar', seo_title: 'Como funciona o pdf2scan', @@ -2628,6 +2635,22 @@ if (!d.active) showPaywall(null, paywallFilePrice, (d.price || paywallSubPrice), 'renew'); else scrollToSubCard(); } + // Пришли по ссылке оплаты файла из бота — открываем пейволл для этого задания + if (window._payJobAfterLoad) { + const job = window._payJobAfterLoad; + window._payJobAfterLoad = null; + if (d.active) { + // уже есть подписка — освобождаем файл бесплатно, без повторной оплаты + const body = { job_id: job }; + if (tgUser) body.user_id = tgUser.user_id || tgUser.id || 0; + try { + await fetch('/api/held/release', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); + showPaySuccess(t('bot_file_released')); + } catch (_) {} + } else { + showPaywall(job, paywallFilePrice, (d.price || paywallSubPrice), 'file'); + } + } } catch (_) { renderSubStatus({ active: false }); } } function renderSubStatus(d) { @@ -2833,6 +2856,12 @@ window._openSubAfterLoad = true; try { history.replaceState({}, '', location.pathname); } catch (_) {} } + // Приход по ссылке оплаты файла из бота (/pay → /?pay_job=): + // после загрузки сессии открываем пейволл для этого задания. + if (p.has('pay_job')) { + window._payJobAfterLoad = p.get('pay_job'); + try { history.replaceState({}, '', location.pathname); } catch (_) {} + } })(); // ── Drop zone ─────────────────────────────────────────────────────────────