ownapi 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli.py +1017 -0
- compiler.py +948 -0
- core/__init__.py +19 -0
- core/api_keys.py +523 -0
- core/auto_repair.py +682 -0
- core/base_client.py +165 -0
- core/consent.py +169 -0
- core/form_automation.py +1044 -0
- core/hitl_queue.py +886 -0
- core/onboarding.py +1457 -0
- core/token_store.py +223 -0
- healer.py +2074 -0
- master_mcp.py +193 -0
- ownapi/__init__.py +94 -0
- ownapi/cli.py +309 -0
- ownapi/sdk.py +436 -0
- ownapi-1.0.0.dist-info/METADATA +287 -0
- ownapi-1.0.0.dist-info/RECORD +62 -0
- ownapi-1.0.0.dist-info/WHEEL +5 -0
- ownapi-1.0.0.dist-info/entry_points.txt +4 -0
- ownapi-1.0.0.dist-info/top_level.txt +10 -0
- recorder.py +486 -0
- registry/coingecko/__init__.py +5 -0
- registry/coingecko/client.py +84 -0
- registry/coingecko/schema.json +23 -0
- registry/edevlet/__init__.py +5 -0
- registry/edevlet/client.py +54 -0
- registry/edevlet/schema.json +14 -0
- registry/enuygun/__init__.py +5 -0
- registry/enuygun/client.py +78 -0
- registry/enuygun/schema.json +13 -0
- registry/github_trending/__init__.py +5 -0
- registry/github_trending/client.py +90 -0
- registry/github_trending/schema.json +31 -0
- registry/hackernews/__init__.py +5 -0
- registry/hackernews/client.py +90 -0
- registry/hackernews/schema.json +38 -0
- registry/hepsiburada/__init__.py +5 -0
- registry/hepsiburada/client.py +32 -0
- registry/hepsiburada/schema.json +23 -0
- registry/linkedin_jobs/__init__.py +5 -0
- registry/linkedin_jobs/client.py +241 -0
- registry/linkedin_jobs/schema.json +46 -0
- registry/pegasus/__init__.py +5 -0
- registry/pegasus/client.py +141 -0
- registry/pegasus/schema.json +76 -0
- registry/thy/__init__.py +5 -0
- registry/thy/client.py +1796 -0
- registry/thy/schema.json +46 -0
- registry/trendyol/__init__.py +5 -0
- registry/trendyol/client.py +228 -0
- registry/trendyol/schema.json +30 -0
- registry/ucak_bileti/client.py +94 -0
- registry/ucak_bileti/schema.json +47 -0
- registry/wikipedia/__init__.py +5 -0
- registry/wikipedia/client.py +64 -0
- registry/wikipedia/schema.json +31 -0
- registry/yahoo_finance/__init__.py +5 -0
- registry/yahoo_finance/client.py +98 -0
- registry/yahoo_finance/schema.json +22 -0
- server.py +891 -0
- static/index.html +2401 -0
healer.py
ADDED
|
@@ -0,0 +1,2074 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import urllib.request
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import parse_qs, urlencode, urlparse
|
|
15
|
+
|
|
16
|
+
from playwright.async_api import Page, Request, Response, async_playwright
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
|
|
20
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
21
|
+
try:
|
|
22
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
23
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
from recorder import IGNORED_DOMAIN_KEYWORDS
|
|
28
|
+
from core.token_store import TokenStore
|
|
29
|
+
|
|
30
|
+
console = Console(stderr=True, legacy_windows=False)
|
|
31
|
+
|
|
32
|
+
# Yenilenmesi gereken kritik kimlik doğrulama, oturum ve dinamik tarayıcı başlığı önekleri
|
|
33
|
+
HEALABLE_HEADER_PREFIXES: tuple[str, ...] = (
|
|
34
|
+
"cookie",
|
|
35
|
+
"authorization",
|
|
36
|
+
"x-csrf-token",
|
|
37
|
+
"x-csrf",
|
|
38
|
+
"x-xsrf",
|
|
39
|
+
"csrf",
|
|
40
|
+
"xsrf",
|
|
41
|
+
"x-algolia",
|
|
42
|
+
"x-api-key",
|
|
43
|
+
"api-key",
|
|
44
|
+
"x-auth",
|
|
45
|
+
"x-session",
|
|
46
|
+
"x-token",
|
|
47
|
+
"x-requested-with",
|
|
48
|
+
"user-agent",
|
|
49
|
+
"referer",
|
|
50
|
+
"origin",
|
|
51
|
+
"sec-ch-ua",
|
|
52
|
+
"sec-ch-ua-mobile",
|
|
53
|
+
"sec-ch-ua-platform",
|
|
54
|
+
"sec-fetch-dest",
|
|
55
|
+
"sec-fetch-mode",
|
|
56
|
+
"sec-fetch-site",
|
|
57
|
+
"x-px",
|
|
58
|
+
"px-",
|
|
59
|
+
"_px",
|
|
60
|
+
"pxcts",
|
|
61
|
+
"x-perimeterx",
|
|
62
|
+
"tafsessionid",
|
|
63
|
+
"taftrackingid",
|
|
64
|
+
"x-current-page-url",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# URL query string veya JSON body içinde taşınabilen token/oturum anahtarı önekleri
|
|
68
|
+
HEALABLE_TOKEN_KEYS: tuple[str, ...] = (
|
|
69
|
+
"x-algolia",
|
|
70
|
+
"api_key",
|
|
71
|
+
"apikey",
|
|
72
|
+
"api-key",
|
|
73
|
+
"token",
|
|
74
|
+
"access_token",
|
|
75
|
+
"auth",
|
|
76
|
+
"csrf",
|
|
77
|
+
"xsrf",
|
|
78
|
+
"sessionid",
|
|
79
|
+
"session_id",
|
|
80
|
+
"_px",
|
|
81
|
+
"px",
|
|
82
|
+
"taf",
|
|
83
|
+
"amadeus",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
PROJECT_ROOT: Path = Path(__file__).resolve().parent
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _clean_profile_dir(profile_dir: Path) -> None:
|
|
90
|
+
"""Eski kirlenmiş Akamai blok çerezlerini ve geçersiz oturum durumunu temizler."""
|
|
91
|
+
if not profile_dir.exists():
|
|
92
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
return
|
|
94
|
+
targets = [
|
|
95
|
+
profile_dir / "storage_state.json",
|
|
96
|
+
profile_dir / "Default" / "Network" / "Cookies",
|
|
97
|
+
profile_dir / "Default" / "Network" / "Cookies-journal",
|
|
98
|
+
profile_dir / "Default" / "Cookies",
|
|
99
|
+
profile_dir / "Default" / "Cookies-journal",
|
|
100
|
+
]
|
|
101
|
+
for item in targets:
|
|
102
|
+
if item.exists():
|
|
103
|
+
try:
|
|
104
|
+
item.unlink()
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _root_domain(hostname: str) -> str:
|
|
110
|
+
"""Subdomain farklarını yoksaymak için kök domain adını döndürür (örn: web.flypgs.com -> flypgs.com)."""
|
|
111
|
+
parts = hostname.lower().strip(".").split(".")
|
|
112
|
+
if len(parts) >= 3 and parts[-2] in ("com", "org", "net", "gov", "edu", "co"):
|
|
113
|
+
return ".".join(parts[-3:])
|
|
114
|
+
if len(parts) >= 2:
|
|
115
|
+
return ".".join(parts[-2:])
|
|
116
|
+
return hostname.lower()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _extract_response_text(res: Any) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Yanıt nesnesi zaten bir dict (sözlük) ise `.text` özniteliği aramaya kalkmadan
|
|
122
|
+
doğrudan `res.get('text')` veya `json.dumps(res)` olarak güvenli biçimde metne çevirir.
|
|
123
|
+
"""
|
|
124
|
+
if res is None:
|
|
125
|
+
return ""
|
|
126
|
+
if isinstance(res, str):
|
|
127
|
+
return res
|
|
128
|
+
if isinstance(res, dict):
|
|
129
|
+
if "text" in res and isinstance(res["text"], str):
|
|
130
|
+
return res["text"]
|
|
131
|
+
if "response_sample" in res and isinstance(res["response_sample"], str):
|
|
132
|
+
return res["response_sample"]
|
|
133
|
+
try:
|
|
134
|
+
return json.dumps(res, ensure_ascii=False)
|
|
135
|
+
except Exception:
|
|
136
|
+
return str(res)
|
|
137
|
+
if isinstance(res, list):
|
|
138
|
+
try:
|
|
139
|
+
return json.dumps(res, ensure_ascii=False)
|
|
140
|
+
except Exception:
|
|
141
|
+
return str(res)
|
|
142
|
+
if hasattr(res, "text"):
|
|
143
|
+
attr = getattr(res, "text")
|
|
144
|
+
return attr() if callable(attr) else str(attr)
|
|
145
|
+
return str(res)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ResponseDict(dict):
|
|
149
|
+
"""
|
|
150
|
+
Hem standart Python sözlüğü (`dict`) olarak çalışan hem de istemci kodlarında
|
|
151
|
+
`response.text`, `response.json()` veya `response['response_sample']` eriÅŸimlerini
|
|
152
|
+
AttributeError / KeyError fırlatmadan destekleyen hibrit sözlük sınıfı.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def text(self) -> str:
|
|
157
|
+
return _extract_response_text(dict(self))
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def status_code(self) -> int:
|
|
161
|
+
val = self.get("status_code", self.get("status", 200))
|
|
162
|
+
return int(val) if isinstance(val, (int, float)) else 200
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def ok(self) -> bool:
|
|
166
|
+
return bool(self.get("ok", 200 <= self.status_code < 400))
|
|
167
|
+
|
|
168
|
+
def json(self) -> dict[str, Any]:
|
|
169
|
+
return dict(self)
|
|
170
|
+
|
|
171
|
+
def __getitem__(self, key: Any) -> Any:
|
|
172
|
+
if key not in self and key in ("text", "response_sample", "content"):
|
|
173
|
+
return self.text
|
|
174
|
+
return super().__getitem__(key)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _merge_cookie_strings(base_cookie: str, new_cookie: str) -> str:
|
|
178
|
+
"""İki Cookie başlığı dizgesini anahtar bazında birleştirir (yeni değerler eskileri günceller)."""
|
|
179
|
+
cookie_map: dict[str, str] = {}
|
|
180
|
+
for raw in (base_cookie, new_cookie):
|
|
181
|
+
if not raw:
|
|
182
|
+
continue
|
|
183
|
+
for part in raw.split(";"):
|
|
184
|
+
part = part.strip()
|
|
185
|
+
if "=" in part:
|
|
186
|
+
k, v = part.split("=", 1)
|
|
187
|
+
cookie_map[k.strip()] = v.strip()
|
|
188
|
+
return "; ".join(f"{k}={v}" for k, v in cookie_map.items())
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def update_payload_deep(payload: Any, updates: dict[str, Any]) -> Any:
|
|
192
|
+
"""
|
|
193
|
+
İstek gövdesindeki (body) hem üst düzey hem de iç içe (nested, örn: flightSearchList[0])
|
|
194
|
+
alanları verilen anahtar-değer çiftleriyle günceller.
|
|
195
|
+
"""
|
|
196
|
+
if not isinstance(payload, (dict, list)) or not updates:
|
|
197
|
+
return payload
|
|
198
|
+
|
|
199
|
+
updated = copy.deepcopy(payload)
|
|
200
|
+
|
|
201
|
+
def _walk(node: Any) -> bool:
|
|
202
|
+
matched_any = False
|
|
203
|
+
if isinstance(node, dict):
|
|
204
|
+
for k in list(node.keys()):
|
|
205
|
+
if k in updates and updates[k] is not None:
|
|
206
|
+
node[k] = updates[k]
|
|
207
|
+
matched_any = True
|
|
208
|
+
if isinstance(node[k], (dict, list)):
|
|
209
|
+
if _walk(node[k]):
|
|
210
|
+
matched_any = True
|
|
211
|
+
elif isinstance(node, list):
|
|
212
|
+
for item in node:
|
|
213
|
+
if isinstance(item, (dict, list)):
|
|
214
|
+
if _walk(item):
|
|
215
|
+
matched_any = True
|
|
216
|
+
return matched_any
|
|
217
|
+
|
|
218
|
+
_walk(updated)
|
|
219
|
+
# Eğer iç içe hiçbir yerde eşleşmediyse ve üst düzey dict ise üst düzeye de ekle
|
|
220
|
+
if isinstance(updated, dict):
|
|
221
|
+
for uk, uv in updates.items():
|
|
222
|
+
if uv is not None and uk in updated:
|
|
223
|
+
updated[uk] = uv
|
|
224
|
+
return updated
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _find_chrome_executable() -> str:
|
|
228
|
+
"""Kullanıcının sistemindeki orijinal Google Chrome yürütülebilir dosya yolunu bulur."""
|
|
229
|
+
candidates = [
|
|
230
|
+
os.environ.get("PROGRAMFILES", r"C:\Program Files") + r"\Google\Chrome\Application\chrome.exe",
|
|
231
|
+
os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)") + r"\Google\Chrome\Application\chrome.exe",
|
|
232
|
+
os.environ.get("LOCALAPPDATA", "") + r"\Google\Chrome\Application\chrome.exe",
|
|
233
|
+
]
|
|
234
|
+
for c in candidates:
|
|
235
|
+
if c and os.path.exists(c):
|
|
236
|
+
return c
|
|
237
|
+
return "chrome"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def _ensure_cdp_chrome(
|
|
241
|
+
profile_dir: Path,
|
|
242
|
+
port: int = 9222,
|
|
243
|
+
initial_url: str = "https://www.turkishairlines.com/tr-tr/",
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Eğer portta açık Chrome oturumu yoksa kullanıcının normal Chrome uygulamasını remote debugging portuyla başlatır."""
|
|
246
|
+
try:
|
|
247
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=0.5) as resp:
|
|
248
|
+
if resp.status == 200:
|
|
249
|
+
return
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
chrome_exe = _find_chrome_executable()
|
|
254
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
255
|
+
console.print(
|
|
256
|
+
Panel.fit(
|
|
257
|
+
f"[bold cyan]🚀 Kullanıcının Gerçek Google Chrome Tarayıcısı Başlatılıyor (CDP Port {port})[/bold cyan]\n"
|
|
258
|
+
f"[dim]Yol:[/dim] [white]{chrome_exe}[/white]\n"
|
|
259
|
+
f"[dim]Hedef:[/dim] [green]{initial_url}[/green]",
|
|
260
|
+
title="Chrome DevTools Protocol (CDP)",
|
|
261
|
+
border_style="cyan",
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
subprocess.Popen(
|
|
265
|
+
[
|
|
266
|
+
chrome_exe,
|
|
267
|
+
f"--remote-debugging-port={port}",
|
|
268
|
+
f"--user-data-dir={profile_dir.resolve()}",
|
|
269
|
+
"--no-first-run",
|
|
270
|
+
"--no-default-browser-check",
|
|
271
|
+
initial_url,
|
|
272
|
+
],
|
|
273
|
+
stdout=subprocess.DEVNULL,
|
|
274
|
+
stderr=subprocess.DEVNULL,
|
|
275
|
+
)
|
|
276
|
+
for _ in range(30):
|
|
277
|
+
await asyncio.sleep(0.5)
|
|
278
|
+
try:
|
|
279
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=0.5) as resp:
|
|
280
|
+
if resp.status == 200:
|
|
281
|
+
break
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class SessionHealer:
|
|
287
|
+
"""
|
|
288
|
+
Oturum çerezleri (Cookie), CSRF, Authorization veya dinamik başlıklar süresi dolduğunda
|
|
289
|
+
(401, 403 veya 429 hatalarında) hedef siteyi ziyaret ederek taze kimlik bilgilerini
|
|
290
|
+
yakalayan, session_tokens.json dosyasına kaydeden ve isteği taze oturum tüneliyle
|
|
291
|
+
tekrarlayan Self-Healing motoru.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __init__(
|
|
295
|
+
self,
|
|
296
|
+
traffic_path: Path | str = PROJECT_ROOT / "captured_traffic.json",
|
|
297
|
+
tokens_path: Path | str = PROJECT_ROOT / "session_tokens.json",
|
|
298
|
+
profile_dir: Path | str = PROJECT_ROOT / ".browser_profile",
|
|
299
|
+
timeout_ms: int = 15_000,
|
|
300
|
+
user_wait_sec: int = 5,
|
|
301
|
+
challenge_wait_sec: int = 45,
|
|
302
|
+
target_url: str | None = None,
|
|
303
|
+
tenant_id: str = "default",
|
|
304
|
+
service: str = "thy",
|
|
305
|
+
token_store: TokenStore | None = None,
|
|
306
|
+
mode: str = "local",
|
|
307
|
+
cloud_cdp_url: str | None = None,
|
|
308
|
+
non_interactive: bool = False,
|
|
309
|
+
) -> None:
|
|
310
|
+
self.traffic_path = Path(traffic_path)
|
|
311
|
+
self.tokens_path = Path(tokens_path)
|
|
312
|
+
self.profile_dir = Path(profile_dir)
|
|
313
|
+
self.timeout_ms = timeout_ms
|
|
314
|
+
self.user_wait_sec = user_wait_sec
|
|
315
|
+
self.challenge_wait_sec = challenge_wait_sec
|
|
316
|
+
self.target_url = target_url
|
|
317
|
+
self.tenant_id = tenant_id or "default"
|
|
318
|
+
self.service = service or "thy"
|
|
319
|
+
self.token_store = token_store or TokenStore(legacy_tokens_path=self.tokens_path)
|
|
320
|
+
self.mode = (mode or os.getenv("OWNAPI_BROWSER_MODE", "local")).strip().lower()
|
|
321
|
+
if self.mode not in ("local", "cloud"):
|
|
322
|
+
self.mode = "local"
|
|
323
|
+
self.cloud_cdp_url = cloud_cdp_url or os.getenv("OWNAPI_CLOUD_CDP_URL", "").strip() or None
|
|
324
|
+
self.non_interactive = bool(
|
|
325
|
+
non_interactive
|
|
326
|
+
or os.getenv("OWNAPI_NON_INTERACTIVE", "").strip().lower() in ("1", "true", "yes")
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def _reset_window_placement(profile_path: Path) -> None:
|
|
331
|
+
"""Kalıcı profilde kaydedilmiş ekran dışı / simge durumundaki pencere konumunu temizler."""
|
|
332
|
+
prefs_file = profile_path / "Default" / "Preferences"
|
|
333
|
+
if prefs_file.exists():
|
|
334
|
+
try:
|
|
335
|
+
prefs_data = json.loads(prefs_file.read_text(encoding="utf-8"))
|
|
336
|
+
if "browser" in prefs_data and "window_placement" in prefs_data["browser"]:
|
|
337
|
+
prefs_data["browser"].pop("window_placement", None)
|
|
338
|
+
prefs_file.write_text(json.dumps(prefs_data), encoding="utf-8")
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
async def _detect_challenge(page: Page) -> bool:
|
|
344
|
+
"""Sayfada PerimeterX, Akamai, Cloudflare veya Captcha doğrulama ekranı olup olmadığını denetler."""
|
|
345
|
+
try:
|
|
346
|
+
content = (await page.content()).lower()
|
|
347
|
+
return any(
|
|
348
|
+
kw in content
|
|
349
|
+
for kw in (
|
|
350
|
+
"px-captcha",
|
|
351
|
+
"perimeterx",
|
|
352
|
+
"press & hold",
|
|
353
|
+
"basılı tutun",
|
|
354
|
+
"robot olmadığınızı",
|
|
355
|
+
"human verification",
|
|
356
|
+
"cpr_chlge",
|
|
357
|
+
"cf-turnstile",
|
|
358
|
+
"hcaptcha",
|
|
359
|
+
"recaptcha",
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
except Exception:
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
async def _apply_otp_code_if_present(
|
|
366
|
+
self,
|
|
367
|
+
page: Page,
|
|
368
|
+
otp_code: str | None,
|
|
369
|
+
tenant_id: str | None = None,
|
|
370
|
+
) -> bool:
|
|
371
|
+
"""
|
|
372
|
+
HITL Onay Merkezi üzerinden kullanıcı `otp_code` (SMS / E-Posta / 2FA doğrulama kodu)
|
|
373
|
+
gönderdiğinde, açık olan Playwright `page` üzerindeki OTP input alanını otomatik bulur,
|
|
374
|
+
kodu yazar, gönder (submit/onayla) butonuna tıklar ve oturumu kaydeder.
|
|
375
|
+
"""
|
|
376
|
+
clean_code = str(otp_code or "").strip()
|
|
377
|
+
if not clean_code or page is None:
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
console.print(
|
|
381
|
+
f"[bold cyan]🔑 [HITL OTP] Uzaktan SMS/E-Posta doğrulama kodu ({clean_code}) sayfaya uygulanıyor...[/bold cyan]"
|
|
382
|
+
)
|
|
383
|
+
filled = False
|
|
384
|
+
try:
|
|
385
|
+
# 1. Çok kutucuklu (maxlength=1) parçalı OTP girişi kontrolü
|
|
386
|
+
split_boxes = page.locator('input[maxlength="1"]:visible')
|
|
387
|
+
split_count = await split_boxes.count()
|
|
388
|
+
if split_count >= 4 and len(clean_code) >= 4:
|
|
389
|
+
for idx, ch in enumerate(clean_code[:split_count]):
|
|
390
|
+
box = split_boxes.nth(idx)
|
|
391
|
+
await box.click(timeout=2000)
|
|
392
|
+
await box.fill(ch, timeout=2000)
|
|
393
|
+
filled = True
|
|
394
|
+
|
|
395
|
+
# 2. Tekil semantik OTP / SMS / Doğrulama Kodu input seçicileri
|
|
396
|
+
if not filled:
|
|
397
|
+
otp_selectors = [
|
|
398
|
+
'input[autocomplete="one-time-code"]:visible',
|
|
399
|
+
'input[name*="otp" i]:visible',
|
|
400
|
+
'input[name*="sms" i]:visible',
|
|
401
|
+
'input[name*="code" i]:visible',
|
|
402
|
+
'input[name*="kod" i]:visible',
|
|
403
|
+
'input[name*="dogrulama" i]:visible',
|
|
404
|
+
'input[name*="verify" i]:visible',
|
|
405
|
+
'input[name*="pin" i]:visible',
|
|
406
|
+
'input[id*="otp" i]:visible',
|
|
407
|
+
'input[id*="sms" i]:visible',
|
|
408
|
+
'input[id*="code" i]:visible',
|
|
409
|
+
'input[id*="kod" i]:visible',
|
|
410
|
+
'input[id*="dogrulama" i]:visible',
|
|
411
|
+
'input[id*="verify" i]:visible',
|
|
412
|
+
'input[placeholder*="kod" i]:visible',
|
|
413
|
+
'input[placeholder*="code" i]:visible',
|
|
414
|
+
'input[placeholder*="sms" i]:visible',
|
|
415
|
+
'input[placeholder*="otp" i]:visible',
|
|
416
|
+
'input[placeholder*="doğrulama" i]:visible',
|
|
417
|
+
'input[placeholder*="dogrulama" i]:visible',
|
|
418
|
+
'input[placeholder*="onay" i]:visible',
|
|
419
|
+
'input[aria-label*="kod" i]:visible',
|
|
420
|
+
'input[aria-label*="code" i]:visible',
|
|
421
|
+
'input[aria-label*="sms" i]:visible',
|
|
422
|
+
'input[aria-label*="otp" i]:visible',
|
|
423
|
+
'input[type="tel"]:visible',
|
|
424
|
+
'input[type="number"]:visible',
|
|
425
|
+
'input[type="text"]:visible',
|
|
426
|
+
]
|
|
427
|
+
target_locator = None
|
|
428
|
+
for sel in otp_selectors:
|
|
429
|
+
loc = page.locator(sel)
|
|
430
|
+
if await loc.count() > 0:
|
|
431
|
+
target_locator = loc.first
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
if target_locator is not None:
|
|
435
|
+
await target_locator.click(timeout=3000)
|
|
436
|
+
await target_locator.fill(clean_code, timeout=3000)
|
|
437
|
+
filled = True
|
|
438
|
+
|
|
439
|
+
if not filled:
|
|
440
|
+
# JS tabanlı yedek doldurma
|
|
441
|
+
filled = bool(
|
|
442
|
+
await page.evaluate(
|
|
443
|
+
"""(code) => {
|
|
444
|
+
const inputs = Array.from(document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"])'));
|
|
445
|
+
const visible = inputs.filter(el => el.offsetParent !== null || el.getClientRects().length > 0);
|
|
446
|
+
const target = visible[0] || inputs[0];
|
|
447
|
+
if (!target) return false;
|
|
448
|
+
target.focus();
|
|
449
|
+
target.value = code;
|
|
450
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
451
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
452
|
+
return true;
|
|
453
|
+
}""",
|
|
454
|
+
clean_code,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if filled:
|
|
459
|
+
# 3. Gönder / Onayla / Doğrula / Devam Et butonunu bul ve tıkla
|
|
460
|
+
submit_selectors = [
|
|
461
|
+
'button[type="submit"]:visible',
|
|
462
|
+
'input[type="submit"]:visible',
|
|
463
|
+
'button:has-text("Onayla"):visible',
|
|
464
|
+
'button:has-text("Doğrula"):visible',
|
|
465
|
+
'button:has-text("Dogrula"):visible',
|
|
466
|
+
'button:has-text("Gönder"):visible',
|
|
467
|
+
'button:has-text("Gonder"):visible',
|
|
468
|
+
'button:has-text("Devam"):visible',
|
|
469
|
+
'button:has-text("Giriş"):visible',
|
|
470
|
+
'button:has-text("Verify"):visible',
|
|
471
|
+
'button:has-text("Confirm"):visible',
|
|
472
|
+
'button:has-text("Submit"):visible',
|
|
473
|
+
]
|
|
474
|
+
clicked_submit = False
|
|
475
|
+
for btn_sel in submit_selectors:
|
|
476
|
+
btn_loc = page.locator(btn_sel)
|
|
477
|
+
if await btn_loc.count() > 0:
|
|
478
|
+
await btn_loc.first.click(timeout=3000)
|
|
479
|
+
clicked_submit = True
|
|
480
|
+
break
|
|
481
|
+
|
|
482
|
+
if not clicked_submit:
|
|
483
|
+
try:
|
|
484
|
+
await page.keyboard.press("Enter")
|
|
485
|
+
except Exception:
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
await asyncio.sleep(1.0)
|
|
489
|
+
|
|
490
|
+
# 4. Güncel çerezleri ve oturumu TokenStore'a kaydet
|
|
491
|
+
try:
|
|
492
|
+
ctx = page.context
|
|
493
|
+
cookies = await ctx.cookies()
|
|
494
|
+
ua = await page.evaluate("() => navigator.userAgent")
|
|
495
|
+
self.token_store.save_session(
|
|
496
|
+
tenant_id=tenant_id or self.tenant_id,
|
|
497
|
+
service=self.service,
|
|
498
|
+
cookies=cookies,
|
|
499
|
+
user_agent=ua,
|
|
500
|
+
metadata={
|
|
501
|
+
"target_url": page.url,
|
|
502
|
+
"healed_at": datetime.now(timezone.utc).isoformat(),
|
|
503
|
+
"otp_verified": True,
|
|
504
|
+
},
|
|
505
|
+
)
|
|
506
|
+
except Exception:
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
console.print(
|
|
510
|
+
f"[bold green]✔ [HITL OTP] Doğrulama kodu ({clean_code}) başarıyla girildi ve onaylandı![/bold green]"
|
|
511
|
+
)
|
|
512
|
+
return True
|
|
513
|
+
except Exception as exc:
|
|
514
|
+
console.print(f"[dim yellow]⚠️ OTP kodu uygulama uyarısı: {exc}[/dim yellow]")
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
async def _handle_human_challenge_if_present(self, page: Page, wait_sec: int | None = None) -> None:
|
|
518
|
+
"""
|
|
519
|
+
PerimeterX veya Captcha ekranı tespit edilirse kullanıcıya doğrulamayı
|
|
520
|
+
tamamlaması için süre tanır. Zorunlu headless=False sayesinde pencere görünürdür.
|
|
521
|
+
Süre dolduğunda pencereyi kapatmak yerine kullanıcı ENTER tuşuna basana kadar bekler.
|
|
522
|
+
"""
|
|
523
|
+
max_wait = wait_sec if wait_sec is not None else self.challenge_wait_sec
|
|
524
|
+
is_challenge = await self._detect_challenge(page)
|
|
525
|
+
if is_challenge:
|
|
526
|
+
console.print(
|
|
527
|
+
Panel.fit(
|
|
528
|
+
"[bold red]ğŸ›¡ï¸ PerimeterX / Güvenlik DoÄŸrulaması Algılandı![/bold red]\n"
|
|
529
|
+
"[bold white]Lütfen ekranda açılan Chrome tarayıcı penceresinde 'Basılı Tutun' veya Captcha doğrulamasını tamamlayın.[/bold white]\n"
|
|
530
|
+
f"[dim]Kullanıcı doğrulaması için {max_wait} saniye otomatik dinleniyor...[/dim]",
|
|
531
|
+
title="Doğrulama Ekranı",
|
|
532
|
+
border_style="red",
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
challenge_resolved = False
|
|
536
|
+
for _ in range(max_wait * 2):
|
|
537
|
+
await asyncio.sleep(0.5)
|
|
538
|
+
if not await self._detect_challenge(page):
|
|
539
|
+
console.print("[bold green]✔ Doğrulama otomatik olarak algılandı ve tamamlandı, veri akışına devam ediliyor![/bold green]")
|
|
540
|
+
challenge_resolved = True
|
|
541
|
+
await asyncio.sleep(1.0)
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
if not challenge_resolved:
|
|
545
|
+
console.print(
|
|
546
|
+
"\n[bold yellow]👉 Doğrulamayı tamamladıysanız devam etmek için terminalde [bold green]ENTER[/bold green] tuşuna basın...[/bold yellow]"
|
|
547
|
+
)
|
|
548
|
+
await asyncio.to_thread(input)
|
|
549
|
+
console.print("[bold green]✔ Kullanıcı onayı alındı, işleme devam ediliyor![/bold green]")
|
|
550
|
+
await asyncio.sleep(1.0)
|
|
551
|
+
elif self.user_wait_sec > 0:
|
|
552
|
+
console.print(
|
|
553
|
+
f"[dim yellow]ğŸ‘ï¸ Tarayıcı penceresi ekranda açık (headless=False). "
|
|
554
|
+
f"Kullanıcı kontrolü ve sayfa stabilizasyonu için {self.user_wait_sec} sn bekleniyor...[/dim yellow]"
|
|
555
|
+
)
|
|
556
|
+
await page.wait_for_timeout(self.user_wait_sec * 1000)
|
|
557
|
+
|
|
558
|
+
def _get_target_info(self) -> tuple[str, set[str]]:
|
|
559
|
+
"""captured_traffic.json içinden hedef URL'yi ve API domainlerini okur."""
|
|
560
|
+
if not self.traffic_path.exists():
|
|
561
|
+
return "https://www.google.com", set()
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
data = json.loads(self.traffic_path.read_text(encoding="utf-8"))
|
|
565
|
+
target_url = str(data.get("target_url", "")).strip()
|
|
566
|
+
api_domains: set[str] = set()
|
|
567
|
+
for ep in data.get("endpoints", []):
|
|
568
|
+
domain = ep.get("domain")
|
|
569
|
+
if domain:
|
|
570
|
+
api_domains.add(str(domain).lower())
|
|
571
|
+
return target_url, api_domains
|
|
572
|
+
except Exception:
|
|
573
|
+
return "", set()
|
|
574
|
+
|
|
575
|
+
@staticmethod
|
|
576
|
+
def _is_ignored(url: str) -> bool:
|
|
577
|
+
try:
|
|
578
|
+
parsed = urlparse(url)
|
|
579
|
+
hostname = (parsed.hostname or "").lower()
|
|
580
|
+
if not hostname:
|
|
581
|
+
return True
|
|
582
|
+
return any(kw in hostname for kw in IGNORED_DOMAIN_KEYWORDS)
|
|
583
|
+
except Exception:
|
|
584
|
+
return True
|
|
585
|
+
|
|
586
|
+
@classmethod
|
|
587
|
+
def _extract_healable_headers(cls, headers: dict[str, str]) -> dict[str, str]:
|
|
588
|
+
extracted: dict[str, str] = {}
|
|
589
|
+
for k, v in headers.items():
|
|
590
|
+
lower_k = k.lower().strip()
|
|
591
|
+
if any(lower_k == prefix or lower_k.startswith(prefix) for prefix in HEALABLE_HEADER_PREFIXES):
|
|
592
|
+
extracted[lower_k] = v
|
|
593
|
+
return extracted
|
|
594
|
+
|
|
595
|
+
@classmethod
|
|
596
|
+
def _extract_healable_query_params(cls, url: str) -> dict[str, str]:
|
|
597
|
+
extracted: dict[str, str] = {}
|
|
598
|
+
parsed = urlparse(url)
|
|
599
|
+
raw_params = parse_qs(parsed.query, keep_blank_values=False)
|
|
600
|
+
for k, values in raw_params.items():
|
|
601
|
+
lower_k = k.lower().strip()
|
|
602
|
+
if values and any(lower_k == prefix or lower_k.startswith(prefix) for prefix in HEALABLE_TOKEN_KEYS):
|
|
603
|
+
extracted[k] = values[-1]
|
|
604
|
+
return extracted
|
|
605
|
+
|
|
606
|
+
@classmethod
|
|
607
|
+
def _extract_healable_body_tokens(cls, request: Request) -> dict[str, Any]:
|
|
608
|
+
"""Tüm istek gövdesi alanlarını dinamik sözlüğe bağlar."""
|
|
609
|
+
extracted: dict[str, Any] = {}
|
|
610
|
+
try:
|
|
611
|
+
post_data = request.post_data
|
|
612
|
+
if not post_data:
|
|
613
|
+
return extracted
|
|
614
|
+
parsed = json.loads(post_data)
|
|
615
|
+
if isinstance(parsed, dict):
|
|
616
|
+
for k, v in parsed.items():
|
|
617
|
+
extracted[k] = v
|
|
618
|
+
except Exception:
|
|
619
|
+
pass
|
|
620
|
+
return extracted
|
|
621
|
+
|
|
622
|
+
async def aheal(
|
|
623
|
+
self,
|
|
624
|
+
target_url: str | None = None,
|
|
625
|
+
retry_request: dict[str, Any] | None = None,
|
|
626
|
+
proxy: str | None = None,
|
|
627
|
+
tenant_id: str | None = None,
|
|
628
|
+
) -> dict[str, Any]:
|
|
629
|
+
"""
|
|
630
|
+
Playwright launch_persistent_context (headless=False,
|
|
631
|
+
args=['--disable-blink-features=AutomationControlled']) ile doÄŸrudan sayfaya gider.
|
|
632
|
+
Kullanıcı doğrulamayı geçip ENTER'a basınca tüm çerezleri (_abck, bm_sz, ak_bmsc, _px3 vb.)
|
|
633
|
+
ve gerçek User-Agent'ı alarak session_tokens.json ve TokenStore'a kaydeder.
|
|
634
|
+
"""
|
|
635
|
+
target_goto = target_url or "https://www.turkishairlines.com/tr-tr/ucak-bileti/rezervasyon/"
|
|
636
|
+
|
|
637
|
+
console.print(
|
|
638
|
+
Panel.fit(
|
|
639
|
+
f"[bold yellow]⚡ Self-Healing & Taze Oturum Tüneli Devrede![/bold yellow]\n"
|
|
640
|
+
f"[cyan]Hedef Gezinme URL'si:[/cyan] {target_goto}\n"
|
|
641
|
+
"[dim]PerimeterX ve Akamai doğrulaması doğrudan bu sayfada çözülüyor...[/dim]",
|
|
642
|
+
title="Shadow-API SessionHealer",
|
|
643
|
+
border_style="yellow",
|
|
644
|
+
)
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
self.profile_dir.mkdir(parents=True, exist_ok=True)
|
|
648
|
+
|
|
649
|
+
cookies: list[dict[str, Any]] = []
|
|
650
|
+
user_agent: str = ""
|
|
651
|
+
active_tenant = tenant_id or self.tenant_id
|
|
652
|
+
|
|
653
|
+
is_headless_env = bool(
|
|
654
|
+
self.mode == "cloud"
|
|
655
|
+
or (sys.platform.startswith("linux") and not os.getenv("DISPLAY"))
|
|
656
|
+
)
|
|
657
|
+
launch_kwargs: dict[str, Any] = {
|
|
658
|
+
"user_data_dir": str(self.profile_dir.resolve()),
|
|
659
|
+
"headless": is_headless_env,
|
|
660
|
+
"args": [
|
|
661
|
+
"--disable-blink-features=AutomationControlled",
|
|
662
|
+
"--no-sandbox",
|
|
663
|
+
"--disable-setuid-sandbox",
|
|
664
|
+
"--disable-dev-shm-usage",
|
|
665
|
+
],
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async with async_playwright() as p:
|
|
669
|
+
context = await p.chromium.launch_persistent_context(**launch_kwargs)
|
|
670
|
+
stored_pw_cookies = self.token_store.get_playwright_cookies(
|
|
671
|
+
tenant_id=active_tenant,
|
|
672
|
+
service=self.service,
|
|
673
|
+
default_url=target_goto,
|
|
674
|
+
)
|
|
675
|
+
if stored_pw_cookies:
|
|
676
|
+
try:
|
|
677
|
+
await context.add_cookies(stored_pw_cookies)
|
|
678
|
+
except Exception:
|
|
679
|
+
pass
|
|
680
|
+
|
|
681
|
+
page = context.pages[0] if context.pages else await context.new_page()
|
|
682
|
+
|
|
683
|
+
console.print(f"[bold cyan]🌠Tarayıcı açılıyor:[/bold cyan] [white]{target_goto}[/white]")
|
|
684
|
+
try:
|
|
685
|
+
await page.goto(target_goto, wait_until="domcontentloaded")
|
|
686
|
+
except Exception as e:
|
|
687
|
+
console.print(f"[dim]Sayfa gezinme bildirimi: {e}[/dim]")
|
|
688
|
+
|
|
689
|
+
await asyncio.to_thread(input, "Sayfa yüklendikten ve varsa doğrulamayı geçtikten sonra ENTER'a basın...")
|
|
690
|
+
console.print("[bold green]✔ Devam ediliyor... Çerezler ve gerçek User-Agent alınıyor.[/bold green]")
|
|
691
|
+
|
|
692
|
+
try:
|
|
693
|
+
cookies = await context.cookies()
|
|
694
|
+
except Exception:
|
|
695
|
+
cookies = []
|
|
696
|
+
|
|
697
|
+
try:
|
|
698
|
+
user_agent = await page.evaluate("() => navigator.userAgent")
|
|
699
|
+
except Exception:
|
|
700
|
+
user_agent = ""
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
await context.storage_state(path=str(self.profile_dir / "storage_state.json"))
|
|
704
|
+
except Exception:
|
|
705
|
+
pass
|
|
706
|
+
|
|
707
|
+
await context.close()
|
|
708
|
+
|
|
709
|
+
cookie_parts = [f"{c['name']}={c['value']}" for c in cookies if c.get("name")]
|
|
710
|
+
cookie_str = "; ".join(cookie_parts)
|
|
711
|
+
|
|
712
|
+
parsed_target = urlparse(target_goto)
|
|
713
|
+
origin_url = f"{parsed_target.scheme}://{parsed_target.netloc}" if parsed_target.scheme and parsed_target.netloc else "https://www.turkishairlines.com"
|
|
714
|
+
|
|
715
|
+
fresh_headers = {
|
|
716
|
+
"cookie": cookie_str,
|
|
717
|
+
"user-agent": user_agent,
|
|
718
|
+
"accept": "application/json, text/plain, */*",
|
|
719
|
+
"content-type": "application/json",
|
|
720
|
+
"origin": origin_url,
|
|
721
|
+
"referer": target_goto,
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
token_bundle: dict[str, Any] = {
|
|
725
|
+
"target_url": target_goto,
|
|
726
|
+
"healed_at": datetime.now(timezone.utc).isoformat(),
|
|
727
|
+
"cookie": cookie_str,
|
|
728
|
+
"user-agent": user_agent,
|
|
729
|
+
"headers": fresh_headers,
|
|
730
|
+
"query_params": {},
|
|
731
|
+
"body_tokens": {},
|
|
732
|
+
**fresh_headers,
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
self.token_store.save_session(
|
|
736
|
+
tenant_id=active_tenant,
|
|
737
|
+
service=self.service,
|
|
738
|
+
cookies=cookies,
|
|
739
|
+
user_agent=user_agent,
|
|
740
|
+
metadata=token_bundle,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
console.print(
|
|
744
|
+
f"[bold green]✔ Self-Healing tamamlandı![/bold green] "
|
|
745
|
+
f"Tenant: [cyan]{active_tenant}[/cyan] | "
|
|
746
|
+
f"Çerez Sayısı: [cyan]{len(cookies)}[/cyan] | "
|
|
747
|
+
f"User-Agent: [cyan]{user_agent[:45]}...[/cyan]"
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
return token_bundle
|
|
751
|
+
|
|
752
|
+
def heal(
|
|
753
|
+
self,
|
|
754
|
+
target_url: str | None = None,
|
|
755
|
+
tenant_id: str | None = None,
|
|
756
|
+
) -> dict[str, Any]:
|
|
757
|
+
"""Senkron kodlardan doğrudan çağrılabilen Self-Healing metodu."""
|
|
758
|
+
return self._run_sync(self.aheal(target_url=target_url, tenant_id=tenant_id))
|
|
759
|
+
|
|
760
|
+
async def aexecute_in_browser(
|
|
761
|
+
self,
|
|
762
|
+
method: str,
|
|
763
|
+
url: str,
|
|
764
|
+
headers: dict[str, str] | None = None,
|
|
765
|
+
params: dict[str, Any] | None = None,
|
|
766
|
+
json_payload: Any = None,
|
|
767
|
+
target_url: str | None = None,
|
|
768
|
+
user_wait_sec: int | None = None,
|
|
769
|
+
challenge_wait_sec: int | None = None,
|
|
770
|
+
tenant_id: str | None = None,
|
|
771
|
+
mode: str | None = None,
|
|
772
|
+
non_interactive: bool | None = None,
|
|
773
|
+
form_params: dict[str, Any] | None = None,
|
|
774
|
+
auto_fill_form: bool | None = None,
|
|
775
|
+
) -> dict[str, Any]:
|
|
776
|
+
"""
|
|
777
|
+
OwnAPI User-Delegated Browser Middleware:
|
|
778
|
+
- mode='local' (varsayılan): Kullanıcının gerçek yerel Chrome oturumunu açar
|
|
779
|
+
(headless=False, args=['--disable-blink-features=AutomationControlled']).
|
|
780
|
+
- mode='cloud': Bulut tabanlı Docker / Remote Browser (CDP / Browserless) oturumuna bağlanır
|
|
781
|
+
veya izole bulut konteyner bağlamında çalıştırır.
|
|
782
|
+
- non_interactive=True (veya OWNAPI_NON_INTERACTIVE=1): Captcha/WAF çıktığında terminalde
|
|
783
|
+
input() ile beklemek yerine görevi HITLQueueManager kuyruğuna ekler ve
|
|
784
|
+
PENDING_USER_APPROVAL yanıtı döndürür.
|
|
785
|
+
TokenStore'daki izole çerezleri tarayıcıya enjekte eder, hedef sayfaya gider ve
|
|
786
|
+
doğrudan tarayıcı içi page.evaluate(fetch) ile hedef isteği yürütür.
|
|
787
|
+
"""
|
|
788
|
+
req_headers = dict(headers or {})
|
|
789
|
+
req_params = dict(params or {})
|
|
790
|
+
req_payload = copy.deepcopy(json_payload)
|
|
791
|
+
active_tenant = tenant_id or self.tenant_id
|
|
792
|
+
active_mode = (mode or self.mode or "local").strip().lower()
|
|
793
|
+
if active_mode not in ("local", "cloud"):
|
|
794
|
+
active_mode = "local"
|
|
795
|
+
env_ni = os.getenv("OWNAPI_NON_INTERACTIVE", "").strip().lower() in ("1", "true", "yes")
|
|
796
|
+
active_non_interactive = (
|
|
797
|
+
bool(non_interactive or env_ni)
|
|
798
|
+
if non_interactive is not None
|
|
799
|
+
else bool(self.non_interactive or env_ni)
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
full_req_url = f"{url}?{urlencode(req_params, doseq=True)}" if req_params else url
|
|
803
|
+
parsed_req = urlparse(full_req_url)
|
|
804
|
+
default_origin = (
|
|
805
|
+
f"{parsed_req.scheme}://{parsed_req.netloc}/"
|
|
806
|
+
if parsed_req.scheme and parsed_req.netloc
|
|
807
|
+
else "https://www.turkishairlines.com/tr-tr/"
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
is_thy_domain = (
|
|
811
|
+
self.service.lower() == "thy"
|
|
812
|
+
or "turkishairlines.com" in full_req_url.lower()
|
|
813
|
+
)
|
|
814
|
+
is_thy_availability = (
|
|
815
|
+
"turkishairlines.com" in full_req_url.lower()
|
|
816
|
+
and urlparse(full_req_url).path.rstrip("/") == "/api/v1/availability"
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
raw_visit_url = target_url or self.target_url or default_origin
|
|
820
|
+
if is_thy_domain:
|
|
821
|
+
# THY için direkt boş API linkine (/api/...) veya eski 404 rezervasyon alt sayfasına gitmek yerine
|
|
822
|
+
# her zaman önce ana sayfada (https://www.turkishairlines.com/tr-tr/) oturum ve çerezleri tazele
|
|
823
|
+
visit_url = "https://www.turkishairlines.com/tr-tr/"
|
|
824
|
+
else:
|
|
825
|
+
parsed_visit = urlparse(raw_visit_url)
|
|
826
|
+
if (
|
|
827
|
+
raw_visit_url.rstrip("/") == full_req_url.rstrip("/")
|
|
828
|
+
or "/api/" in parsed_visit.path.lower()
|
|
829
|
+
):
|
|
830
|
+
# HITL veya Auto-Repair sırasında target_url yanlışlıkla API uç noktası olarak gelirse
|
|
831
|
+
# şemadaki target_url veya kök domain ana sayfasına yönlendir
|
|
832
|
+
schema_file = PROJECT_ROOT / "registry" / self.service / "schema.json"
|
|
833
|
+
resolved_target = default_origin
|
|
834
|
+
if schema_file.exists():
|
|
835
|
+
try:
|
|
836
|
+
s_data = json.loads(schema_file.read_text(encoding="utf-8"))
|
|
837
|
+
cand = str(s_data.get("target_url", "")).strip()
|
|
838
|
+
if cand and "/api/" not in urlparse(cand).path.lower():
|
|
839
|
+
resolved_target = cand
|
|
840
|
+
except Exception:
|
|
841
|
+
pass
|
|
842
|
+
visit_url = resolved_target
|
|
843
|
+
else:
|
|
844
|
+
visit_url = raw_visit_url
|
|
845
|
+
|
|
846
|
+
if is_thy_domain:
|
|
847
|
+
req_headers["Referer"] = "https://www.turkishairlines.com/"
|
|
848
|
+
req_headers["Origin"] = "https://www.turkishairlines.com"
|
|
849
|
+
req_headers["referer"] = "https://www.turkishairlines.com/"
|
|
850
|
+
req_headers["origin"] = "https://www.turkishairlines.com"
|
|
851
|
+
req_headers.setdefault("x-country", "TR")
|
|
852
|
+
req_headers.setdefault("x-language", "tr")
|
|
853
|
+
req_headers.setdefault("accept", "application/json, text/plain, */*")
|
|
854
|
+
req_headers.setdefault("content-type", "application/json")
|
|
855
|
+
|
|
856
|
+
if is_thy_availability and (
|
|
857
|
+
not isinstance(req_payload, dict)
|
|
858
|
+
or not req_payload.get("originDestinationInformationList")
|
|
859
|
+
):
|
|
860
|
+
try:
|
|
861
|
+
from registry.thy.client import build_thy_availability_payload
|
|
862
|
+
req_payload = build_thy_availability_payload()
|
|
863
|
+
except Exception:
|
|
864
|
+
req_payload = {
|
|
865
|
+
"selectedBookerSearch": "R",
|
|
866
|
+
"selectedCabinClass": "ECONOMY",
|
|
867
|
+
"moduleType": "TICKETING",
|
|
868
|
+
"passengerTypeList": [{"quantity": 1, "code": "ADULT"}],
|
|
869
|
+
"originDestinationInformationList": [
|
|
870
|
+
{
|
|
871
|
+
"originAirportCode": "IST",
|
|
872
|
+
"originCountryCode": "TR",
|
|
873
|
+
"originMultiPort": True,
|
|
874
|
+
"originDomestic": True,
|
|
875
|
+
"destinationAirportCode": "ESB",
|
|
876
|
+
"destinationCountryCode": "TR",
|
|
877
|
+
"destinationMultiPort": False,
|
|
878
|
+
"destinationDomestic": True,
|
|
879
|
+
"departureDate": "02-03-2027",
|
|
880
|
+
"originCityCode": "IST",
|
|
881
|
+
"destinationCityCode": "ANK",
|
|
882
|
+
"originCity": "İstanbul",
|
|
883
|
+
"destinationCity": "Ankara",
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
"originAirportCode": "ESB",
|
|
887
|
+
"originCountryCode": "TR",
|
|
888
|
+
"originMultiPort": False,
|
|
889
|
+
"originDomestic": True,
|
|
890
|
+
"destinationAirportCode": "IST",
|
|
891
|
+
"destinationCountryCode": "TR",
|
|
892
|
+
"destinationMultiPort": True,
|
|
893
|
+
"destinationDomestic": True,
|
|
894
|
+
"departureDate": "09-03-2027",
|
|
895
|
+
"originCityCode": "ANK",
|
|
896
|
+
"destinationCityCode": "IST",
|
|
897
|
+
"originCity": "Ankara",
|
|
898
|
+
"destinationCity": "İstanbul",
|
|
899
|
+
},
|
|
900
|
+
],
|
|
901
|
+
"savedDate": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
|
902
|
+
"preselectedOptionDetails": [],
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
mode_label = (
|
|
906
|
+
f"â˜ï¸ CLOUD ({self.cloud_cdp_url or 'Docker/Headless Container'})"
|
|
907
|
+
if active_mode == "cloud"
|
|
908
|
+
else "💻 LOCAL (User Chrome Session)"
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
console.print(
|
|
912
|
+
Panel.fit(
|
|
913
|
+
f"[bold cyan]🌠User-Delegated Browser Fetch Tüneli Devrede![/bold cyan]\n"
|
|
914
|
+
f"[dim]Servis / Tenant:[/dim] [magenta]{self.service}[/magenta] ([cyan]{active_tenant}[/cyan])\n"
|
|
915
|
+
f"[dim]Çalışma Modu:[/dim] [yellow]{mode_label}[/yellow]\n"
|
|
916
|
+
f"[dim]Metot / Uç Nokta:[/dim] [green]{method.upper()}[/green] [white]{full_req_url}[/white]\n"
|
|
917
|
+
f"[dim]Oturum Sayfası:[/dim] [cyan]{visit_url}[/cyan]",
|
|
918
|
+
title="OwnAPI Browser Middleware",
|
|
919
|
+
border_style="cyan",
|
|
920
|
+
)
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
self.profile_dir.mkdir(parents=True, exist_ok=True)
|
|
924
|
+
# Kalıcı profil çerezlerini koru - _clean_profile_dir KESİNLİKLE çağrılmaz
|
|
925
|
+
|
|
926
|
+
def _build_validate_payload(p_load: Any) -> dict[str, Any]:
|
|
927
|
+
if not isinstance(p_load, dict):
|
|
928
|
+
return {}
|
|
929
|
+
val = {
|
|
930
|
+
"selectedBookerSearch": p_load.get("selectedBookerSearch", "R"),
|
|
931
|
+
"selectedCabinClass": p_load.get("selectedCabinClass", "ECONOMY"),
|
|
932
|
+
"moduleType": p_load.get("moduleType", "TICKETING"),
|
|
933
|
+
"passengerTypeList": p_load.get("passengerTypeList", [{"quantity": 1, "code": "ADULT"}]),
|
|
934
|
+
"originDestinationInformationList": [],
|
|
935
|
+
}
|
|
936
|
+
for s in p_load.get("originDestinationInformationList", []):
|
|
937
|
+
if isinstance(s, dict):
|
|
938
|
+
val["originDestinationInformationList"].append({
|
|
939
|
+
"originAirportCode": s.get("originAirportCode"),
|
|
940
|
+
"originMultiPort": s.get("originMultiPort", False),
|
|
941
|
+
"destinationAirportCode": s.get("destinationAirportCode"),
|
|
942
|
+
"destinationMultiPort": s.get("destinationMultiPort", False),
|
|
943
|
+
"departureDate": s.get("departureDate"),
|
|
944
|
+
})
|
|
945
|
+
return val
|
|
946
|
+
|
|
947
|
+
def _build_thy_search_state_url(
|
|
948
|
+
p_load: Any,
|
|
949
|
+
cid: str | None = None,
|
|
950
|
+
candidate_url: str | None = None,
|
|
951
|
+
) -> str:
|
|
952
|
+
"""
|
|
953
|
+
THY /api/v1/availability isteğinden önce Playwright'ın gidip sayfa state'ini
|
|
954
|
+
oluşturması gereken gidiş-dönüş (veya tek yön) arama URL'sini üretir.
|
|
955
|
+
"""
|
|
956
|
+
active_cid = cid or str(uuid.uuid4())
|
|
957
|
+
if candidate_url and "turkishairlines.com" in candidate_url.lower():
|
|
958
|
+
parsed_c = urlparse(candidate_url)
|
|
959
|
+
if (
|
|
960
|
+
"/ucak-bileti/rezervasyon/" in parsed_c.path
|
|
961
|
+
and "origin=" in parsed_c.query
|
|
962
|
+
and "departureDate=" in parsed_c.query
|
|
963
|
+
):
|
|
964
|
+
if not cid or f"cId={cid}" in parsed_c.query:
|
|
965
|
+
return candidate_url
|
|
966
|
+
|
|
967
|
+
try:
|
|
968
|
+
from registry.thy.client import build_thy_search_url_from_payload
|
|
969
|
+
return build_thy_search_url_from_payload(
|
|
970
|
+
p_load if isinstance(p_load, dict) else None,
|
|
971
|
+
cid=active_cid,
|
|
972
|
+
)
|
|
973
|
+
except Exception:
|
|
974
|
+
from urllib.parse import urlencode
|
|
975
|
+
|
|
976
|
+
orig = "IST"
|
|
977
|
+
dest = "ESB"
|
|
978
|
+
dep_date = "02-03-2027"
|
|
979
|
+
ret_date: str | None = "09-03-2027"
|
|
980
|
+
trip_type = "R"
|
|
981
|
+
cabin = "ECONOMY"
|
|
982
|
+
adults = 1
|
|
983
|
+
is_domestic = True
|
|
984
|
+
|
|
985
|
+
if isinstance(p_load, dict):
|
|
986
|
+
trip_type = str(p_load.get("selectedBookerSearch") or "R").upper()
|
|
987
|
+
cabin = str(p_load.get("selectedCabinClass") or "ECONOMY").upper()
|
|
988
|
+
od_l = p_load.get("originDestinationInformationList")
|
|
989
|
+
if isinstance(od_l, list) and len(od_l) >= 1 and isinstance(od_l[0], dict):
|
|
990
|
+
seg0 = od_l[0]
|
|
991
|
+
orig = str(seg0.get("originAirportCode") or orig)
|
|
992
|
+
dest = str(seg0.get("destinationAirportCode") or dest)
|
|
993
|
+
dep_date = str(seg0.get("departureDate") or dep_date)
|
|
994
|
+
is_domestic = bool(
|
|
995
|
+
seg0.get("originDomestic", True) and seg0.get("destinationDomestic", True)
|
|
996
|
+
)
|
|
997
|
+
if len(od_l) >= 2 and isinstance(od_l[1], dict):
|
|
998
|
+
ret_date = str(od_l[1].get("departureDate") or "") or None
|
|
999
|
+
elif trip_type == "O":
|
|
1000
|
+
ret_date = None
|
|
1001
|
+
|
|
1002
|
+
base_path = (
|
|
1003
|
+
"/tr-tr/ucak-bileti/rezervasyon/ic-hat-musaitlik/"
|
|
1004
|
+
if is_domestic
|
|
1005
|
+
else "/tr-tr/ucak-bileti/rezervasyon/rezervasyon-durumu/"
|
|
1006
|
+
)
|
|
1007
|
+
q_params: dict[str, Any] = {
|
|
1008
|
+
"cId": active_cid,
|
|
1009
|
+
"origin": orig,
|
|
1010
|
+
"destination": dest,
|
|
1011
|
+
"departureDate": dep_date,
|
|
1012
|
+
}
|
|
1013
|
+
if ret_date:
|
|
1014
|
+
q_params["returnDate"] = ret_date
|
|
1015
|
+
q_params["tripType"] = "R" if ret_date else "O"
|
|
1016
|
+
q_params["cabinClass"] = cabin
|
|
1017
|
+
q_params["ADT"] = adults
|
|
1018
|
+
return f"https://www.turkishairlines.com{base_path}?{urlencode(q_params)}"
|
|
1019
|
+
|
|
1020
|
+
def _is_populated_availability(json_obj_or_str: Any) -> bool:
|
|
1021
|
+
if isinstance(json_obj_or_str, str):
|
|
1022
|
+
try:
|
|
1023
|
+
json_obj_or_str = json.loads(json_obj_or_str)
|
|
1024
|
+
except Exception:
|
|
1025
|
+
return False
|
|
1026
|
+
if not isinstance(json_obj_or_str, dict):
|
|
1027
|
+
return False
|
|
1028
|
+
d = json_obj_or_str.get("data")
|
|
1029
|
+
if isinstance(d, dict):
|
|
1030
|
+
if "showBanner" in d and not d.get("originDestinationInformationList"):
|
|
1031
|
+
return False
|
|
1032
|
+
od_l = d.get("originDestinationInformationList", [])
|
|
1033
|
+
if isinstance(od_l, list) and od_l:
|
|
1034
|
+
for od_item in od_l:
|
|
1035
|
+
if isinstance(od_item, dict) and (od_item.get("originDestinationOptionList") or od_item.get("flightOptionList") or od_item.get("options")):
|
|
1036
|
+
return True
|
|
1037
|
+
air_l = d.get("availabilityInfoResponse")
|
|
1038
|
+
if isinstance(air_l, list) and any(len(sub) > 0 for sub in air_l if isinstance(sub, list)):
|
|
1039
|
+
return True
|
|
1040
|
+
if d.get("economyStartingPrice") or d.get("businessStartingPrice"):
|
|
1041
|
+
return True
|
|
1042
|
+
return False
|
|
1043
|
+
|
|
1044
|
+
validate_url = "https://www.turkishairlines.com/api/v1/availability/validate" if is_thy_availability else None
|
|
1045
|
+
val_payload = _build_validate_payload(req_payload) if is_thy_availability else None
|
|
1046
|
+
|
|
1047
|
+
fetch_js = """async ({ validateUrl, validateBody, url, method, headers, body }) => {
|
|
1048
|
+
const forbidden = [
|
|
1049
|
+
'user-agent', 'cookie', 'host',
|
|
1050
|
+
'content-length', 'connection', 'keep-alive', 'accept-encoding'
|
|
1051
|
+
];
|
|
1052
|
+
const safeHeaders = {};
|
|
1053
|
+
for (const [k, v] of Object.entries(headers || {})) {
|
|
1054
|
+
const lk = k.toLowerCase().trim();
|
|
1055
|
+
if (!forbidden.includes(lk) && !lk.startsWith('sec-')) {
|
|
1056
|
+
safeHeaders[k] = v;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const isThy = (url || '').toLowerCase().includes('turkishairlines.com');
|
|
1060
|
+
if (isThy) {
|
|
1061
|
+
safeHeaders['Referer'] = headers?.['Referer'] || headers?.['referer'] || 'https://www.turkishairlines.com/';
|
|
1062
|
+
safeHeaders['Origin'] = headers?.['Origin'] || headers?.['origin'] || 'https://www.turkishairlines.com';
|
|
1063
|
+
}
|
|
1064
|
+
const customReferrer = safeHeaders['Referer'] || safeHeaders['referer'] || (isThy ? 'https://www.turkishairlines.com/' : undefined);
|
|
1065
|
+
const hasBody = body !== null && body !== undefined && method !== 'GET' && method !== 'HEAD';
|
|
1066
|
+
if (hasBody && !safeHeaders['content-type'] && !safeHeaders['Content-Type']) {
|
|
1067
|
+
safeHeaders['content-type'] = 'application/json';
|
|
1068
|
+
}
|
|
1069
|
+
if (!safeHeaders['accept'] && !safeHeaders['Accept']) {
|
|
1070
|
+
safeHeaders['accept'] = 'application/json, text/plain, */*';
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// 1. THY ise oturum durumunu hazırlamak için /api/v1/availability/validate çağrısı
|
|
1074
|
+
if (validateUrl && validateBody) {
|
|
1075
|
+
try {
|
|
1076
|
+
const valOpts = {
|
|
1077
|
+
method: 'POST',
|
|
1078
|
+
headers: safeHeaders,
|
|
1079
|
+
credentials: 'include',
|
|
1080
|
+
referrer: customReferrer,
|
|
1081
|
+
body: typeof validateBody === 'string' ? validateBody : JSON.stringify(validateBody)
|
|
1082
|
+
};
|
|
1083
|
+
const valRes = await fetch(validateUrl, valOpts);
|
|
1084
|
+
const valText = await valRes.text();
|
|
1085
|
+
if (valText.includes('PXDqLTvkTO') || valText.includes('blockScript')) {
|
|
1086
|
+
return { status: valRes.status, ok: false, text: valText, step: 'validate' };
|
|
1087
|
+
}
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
// validate hatasını yut ve ana isteğe devam et
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// 2. Ana hedef fetch() çağrısı
|
|
1094
|
+
const opts = {
|
|
1095
|
+
method: method || 'GET',
|
|
1096
|
+
headers: safeHeaders,
|
|
1097
|
+
credentials: 'include'
|
|
1098
|
+
};
|
|
1099
|
+
if (customReferrer) {
|
|
1100
|
+
opts.referrer = customReferrer;
|
|
1101
|
+
}
|
|
1102
|
+
if (hasBody) {
|
|
1103
|
+
opts.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
const res = await fetch(url, opts);
|
|
1107
|
+
const text = await res.text();
|
|
1108
|
+
return { status: res.status, ok: res.ok, text, step: 'main' };
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
try {
|
|
1111
|
+
const omitOpts = { ...opts, credentials: 'omit' };
|
|
1112
|
+
const res2 = await fetch(url, omitOpts);
|
|
1113
|
+
const text2 = await res2.text();
|
|
1114
|
+
return { status: res2.status, ok: res2.ok, text: text2, step: 'main_omit' };
|
|
1115
|
+
} catch (e2) {
|
|
1116
|
+
return { status: 0, ok: false, text: '', error: String(e), step: 'main' };
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}"""
|
|
1120
|
+
|
|
1121
|
+
eval_result: dict[str, Any] = {}
|
|
1122
|
+
|
|
1123
|
+
async with async_playwright() as p:
|
|
1124
|
+
remote_browser = None
|
|
1125
|
+
if active_mode == "cloud" and self.cloud_cdp_url:
|
|
1126
|
+
console.print(f"[bold cyan]â˜ï¸ Cloud Remote Browser baÄŸlantısı kuruluyor:[/bold cyan] [white]{self.cloud_cdp_url}[/white]")
|
|
1127
|
+
remote_browser = await p.chromium.connect_over_cdp(self.cloud_cdp_url)
|
|
1128
|
+
context = remote_browser.contexts[0] if remote_browser.contexts else await remote_browser.new_context()
|
|
1129
|
+
elif active_mode == "cloud":
|
|
1130
|
+
# Bulut/Docker konteyner hazırlık modu (headless izole profil)
|
|
1131
|
+
cloud_kwargs: dict[str, Any] = {
|
|
1132
|
+
"user_data_dir": str(self.profile_dir.resolve()),
|
|
1133
|
+
"headless": True,
|
|
1134
|
+
"args": [
|
|
1135
|
+
"--disable-blink-features=AutomationControlled",
|
|
1136
|
+
"--no-sandbox",
|
|
1137
|
+
"--disable-setuid-sandbox",
|
|
1138
|
+
"--disable-dev-shm-usage",
|
|
1139
|
+
],
|
|
1140
|
+
}
|
|
1141
|
+
context = await p.chromium.launch_persistent_context(**cloud_kwargs)
|
|
1142
|
+
else:
|
|
1143
|
+
is_headless_env = bool(
|
|
1144
|
+
sys.platform.startswith("linux") and not os.getenv("DISPLAY")
|
|
1145
|
+
)
|
|
1146
|
+
launch_kwargs: dict[str, Any] = {
|
|
1147
|
+
"user_data_dir": str(self.profile_dir.resolve()),
|
|
1148
|
+
"headless": is_headless_env,
|
|
1149
|
+
"args": [
|
|
1150
|
+
"--disable-blink-features=AutomationControlled",
|
|
1151
|
+
"--no-sandbox",
|
|
1152
|
+
"--disable-setuid-sandbox",
|
|
1153
|
+
"--disable-dev-shm-usage",
|
|
1154
|
+
],
|
|
1155
|
+
}
|
|
1156
|
+
context = await p.chromium.launch_persistent_context(**launch_kwargs)
|
|
1157
|
+
|
|
1158
|
+
# TokenStore'daki kayıtlı kiracı/servis çerezlerini tarayıcı bağlamına otomatik enjekte et
|
|
1159
|
+
stored_pw_cookies = self.token_store.get_playwright_cookies(
|
|
1160
|
+
tenant_id=active_tenant,
|
|
1161
|
+
service=self.service,
|
|
1162
|
+
default_url=visit_url,
|
|
1163
|
+
)
|
|
1164
|
+
if stored_pw_cookies:
|
|
1165
|
+
try:
|
|
1166
|
+
await context.add_cookies(stored_pw_cookies)
|
|
1167
|
+
except Exception:
|
|
1168
|
+
pass
|
|
1169
|
+
|
|
1170
|
+
page = context.pages[0] if context.pages else await context.new_page()
|
|
1171
|
+
if active_mode == "local":
|
|
1172
|
+
try:
|
|
1173
|
+
await page.bring_to_front()
|
|
1174
|
+
except Exception:
|
|
1175
|
+
pass
|
|
1176
|
+
|
|
1177
|
+
# Tarayıcı ağ trafiğini dinleyerek asıl /api/v1/availability istek ve yanıtlarını yakala
|
|
1178
|
+
captured_browser_request: dict[str, Any] = {}
|
|
1179
|
+
captured_browser_response: dict[str, Any] = {}
|
|
1180
|
+
|
|
1181
|
+
def handle_request(req):
|
|
1182
|
+
try:
|
|
1183
|
+
p_path = urlparse(req.url).path.rstrip('/')
|
|
1184
|
+
if p_path == "/api/v1/availability" and req.method == "POST":
|
|
1185
|
+
captured_browser_request["url"] = req.url
|
|
1186
|
+
captured_browser_request["headers"] = dict(req.headers)
|
|
1187
|
+
captured_browser_request["post_data"] = req.post_data_json if req.post_data else None
|
|
1188
|
+
captured_browser_request["raw_post_data"] = req.post_data
|
|
1189
|
+
except Exception:
|
|
1190
|
+
pass
|
|
1191
|
+
|
|
1192
|
+
def handle_response(res):
|
|
1193
|
+
try:
|
|
1194
|
+
p_path = urlparse(res.url).path.rstrip('/')
|
|
1195
|
+
if p_path == "/api/v1/availability" and res.request.method == "POST":
|
|
1196
|
+
async def _read():
|
|
1197
|
+
try:
|
|
1198
|
+
captured_browser_response["status"] = res.status
|
|
1199
|
+
captured_browser_response["text"] = await res.text()
|
|
1200
|
+
except Exception:
|
|
1201
|
+
pass
|
|
1202
|
+
asyncio.create_task(_read())
|
|
1203
|
+
except Exception:
|
|
1204
|
+
pass
|
|
1205
|
+
|
|
1206
|
+
if is_thy_availability:
|
|
1207
|
+
page.on("request", handle_request)
|
|
1208
|
+
page.on("response", handle_response)
|
|
1209
|
+
|
|
1210
|
+
async def _prepare_thy_search_page_state(
|
|
1211
|
+
page_obj: Any,
|
|
1212
|
+
p_load: Any,
|
|
1213
|
+
hdrs: dict[str, Any],
|
|
1214
|
+
cid: str | None = None,
|
|
1215
|
+
) -> str:
|
|
1216
|
+
"""
|
|
1217
|
+
WAF/Captcha geçildikten veya HITL onayı (resume) alındıktan sonra,
|
|
1218
|
+
doğrudan /api/v1/availability isteği atmadan önce:
|
|
1219
|
+
1. Tarayıcıda ön doğrulama (pax-type-codes, availability/validate) ve storage state'ini hazırlar,
|
|
1220
|
+
2. İlgili gidiş-dönüş arama URL'sine (page.goto) giderek sayfa state'ini oluşturur,
|
|
1221
|
+
3. Başlıkları (Referer, x-current-page-url, x-client-id) aktif arama sayfasıyla senkronize eder.
|
|
1222
|
+
"""
|
|
1223
|
+
active_cid = (
|
|
1224
|
+
cid
|
|
1225
|
+
or hdrs.get("x-client-id")
|
|
1226
|
+
or hdrs.get("x-clientid")
|
|
1227
|
+
or str(uuid.uuid4())
|
|
1228
|
+
)
|
|
1229
|
+
search_state_url = _build_thy_search_state_url(
|
|
1230
|
+
p_load,
|
|
1231
|
+
cid=active_cid,
|
|
1232
|
+
candidate_url=raw_visit_url,
|
|
1233
|
+
)
|
|
1234
|
+
parsed_s = urlparse(search_state_url)
|
|
1235
|
+
route_path = (
|
|
1236
|
+
f"{parsed_s.path}?{parsed_s.query}"
|
|
1237
|
+
if parsed_s.query
|
|
1238
|
+
else parsed_s.path
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
hdrs["x-client-id"] = active_cid
|
|
1242
|
+
hdrs["x-clientid"] = active_cid
|
|
1243
|
+
hdrs["x-current-page-url"] = route_path
|
|
1244
|
+
hdrs["Referer"] = search_state_url
|
|
1245
|
+
hdrs["referer"] = search_state_url
|
|
1246
|
+
hdrs["Origin"] = "https://www.turkishairlines.com"
|
|
1247
|
+
hdrs["origin"] = "https://www.turkishairlines.com"
|
|
1248
|
+
|
|
1249
|
+
val_body = _build_validate_payload(p_load)
|
|
1250
|
+
|
|
1251
|
+
# Adım 1: Arama sayfasına gitmeden önce session/localStorage ve validate state'ini hazırla
|
|
1252
|
+
try:
|
|
1253
|
+
await page_obj.evaluate(
|
|
1254
|
+
"""async ({ cid, validateBody, fullPayload, routePath }) => {
|
|
1255
|
+
try {
|
|
1256
|
+
const stateObj = {
|
|
1257
|
+
cId: cid,
|
|
1258
|
+
bookingSearch: fullPayload || validateBody,
|
|
1259
|
+
updatedAt: new Date().toISOString()
|
|
1260
|
+
};
|
|
1261
|
+
sessionStorage.setItem('thy_booking_search_state', JSON.stringify(stateObj));
|
|
1262
|
+
sessionStorage.setItem('cId', cid);
|
|
1263
|
+
localStorage.setItem('thy_last_cid', cid);
|
|
1264
|
+
} catch (_) {}
|
|
1265
|
+
|
|
1266
|
+
const commonHeaders = {
|
|
1267
|
+
'accept': 'application/json, text/plain, */*',
|
|
1268
|
+
'content-type': 'application/json',
|
|
1269
|
+
'x-country': 'TR',
|
|
1270
|
+
'x-language': 'tr',
|
|
1271
|
+
'x-client-id': cid,
|
|
1272
|
+
'x-clientid': cid,
|
|
1273
|
+
'x-current-page-url': '/tr-tr'
|
|
1274
|
+
};
|
|
1275
|
+
try {
|
|
1276
|
+
const odList = (validateBody && validateBody.originDestinationInformationList) || [];
|
|
1277
|
+
await fetch('https://www.turkishairlines.com/api/v1/passenger/pax-type-codes', {
|
|
1278
|
+
method: 'POST',
|
|
1279
|
+
headers: commonHeaders,
|
|
1280
|
+
credentials: 'include',
|
|
1281
|
+
body: JSON.stringify({
|
|
1282
|
+
selectedBookerSearch: (validateBody && validateBody.selectedBookerSearch) || 'R',
|
|
1283
|
+
moduleType: 'TICKETING',
|
|
1284
|
+
originDestinationInformationList: odList.slice(0, 1)
|
|
1285
|
+
})
|
|
1286
|
+
});
|
|
1287
|
+
} catch (_) {}
|
|
1288
|
+
|
|
1289
|
+
if (validateBody) {
|
|
1290
|
+
try {
|
|
1291
|
+
await fetch('https://www.turkishairlines.com/api/v1/availability/validate', {
|
|
1292
|
+
method: 'POST',
|
|
1293
|
+
headers: commonHeaders,
|
|
1294
|
+
credentials: 'include',
|
|
1295
|
+
body: JSON.stringify(validateBody)
|
|
1296
|
+
});
|
|
1297
|
+
} catch (_) {}
|
|
1298
|
+
}
|
|
1299
|
+
}""",
|
|
1300
|
+
{
|
|
1301
|
+
"cid": active_cid,
|
|
1302
|
+
"validateBody": val_body,
|
|
1303
|
+
"fullPayload": p_load,
|
|
1304
|
+
"routePath": route_path,
|
|
1305
|
+
},
|
|
1306
|
+
)
|
|
1307
|
+
except Exception:
|
|
1308
|
+
pass
|
|
1309
|
+
|
|
1310
|
+
# Adım 2: Sayfada arama formu varsa Generic Form Automation ile otonom doldur
|
|
1311
|
+
try:
|
|
1312
|
+
await _run_smart_form_automation(page_obj, p_load)
|
|
1313
|
+
except Exception:
|
|
1314
|
+
pass
|
|
1315
|
+
|
|
1316
|
+
# Adım 3: İlgili gidiş-dönüş arama URL'sine (search_state_url) giderek sayfa state'ini oluştur
|
|
1317
|
+
console.print(
|
|
1318
|
+
f"[bold cyan]🧭 THY arama URL'sine gidilerek sayfa state'i oluşturuluyor:[/bold cyan] [white]{search_state_url}[/white]"
|
|
1319
|
+
)
|
|
1320
|
+
try:
|
|
1321
|
+
await page_obj.goto(
|
|
1322
|
+
search_state_url,
|
|
1323
|
+
wait_until="domcontentloaded",
|
|
1324
|
+
timeout=15_000,
|
|
1325
|
+
)
|
|
1326
|
+
except Exception as nav_exc:
|
|
1327
|
+
console.print(f"[dim]THY arama sayfası gezinme bildirimi: {nav_exc}[/dim]")
|
|
1328
|
+
|
|
1329
|
+
await asyncio.sleep(3.0)
|
|
1330
|
+
return search_state_url
|
|
1331
|
+
|
|
1332
|
+
async def _run_smart_form_automation(
|
|
1333
|
+
page_obj: Any,
|
|
1334
|
+
p_load: Any = None,
|
|
1335
|
+
) -> dict[str, Any]:
|
|
1336
|
+
"""
|
|
1337
|
+
Genel Amaçlı Akıllı Form Otomasyonu (Generic Form Automation):
|
|
1338
|
+
1. Sayfa yüklendiğinde origin/destination ve departure/return date input'larını
|
|
1339
|
+
semantik olarak (placeholder, name, aria-label, id, label) bulup otonom doldurur.
|
|
1340
|
+
2. Autocomplete listesi çıkarsa ilk eşleşen sonuca tıklar.
|
|
1341
|
+
3. Arama / uçuş bul butonunu otomatik tespit edip tıklayarak SPA state'ini oluşturur.
|
|
1342
|
+
"""
|
|
1343
|
+
if auto_fill_form is False:
|
|
1344
|
+
return {"executed": False}
|
|
1345
|
+
try:
|
|
1346
|
+
from core.form_automation import (
|
|
1347
|
+
extract_semantic_form_params,
|
|
1348
|
+
run_generic_form_automation,
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
active_body = p_load if p_load is not None else req_payload
|
|
1352
|
+
sem_params = extract_semantic_form_params(
|
|
1353
|
+
json_payload=active_body,
|
|
1354
|
+
params=req_params,
|
|
1355
|
+
url=full_req_url,
|
|
1356
|
+
target_url=raw_visit_url,
|
|
1357
|
+
explicit_form_params=form_params,
|
|
1358
|
+
)
|
|
1359
|
+
has_route_or_date = bool(
|
|
1360
|
+
sem_params.get("origin")
|
|
1361
|
+
or sem_params.get("destination")
|
|
1362
|
+
or sem_params.get("departure_date")
|
|
1363
|
+
or sem_params.get("return_date")
|
|
1364
|
+
)
|
|
1365
|
+
if not has_route_or_date and not auto_fill_form and not form_params:
|
|
1366
|
+
return {"executed": False}
|
|
1367
|
+
|
|
1368
|
+
return await run_generic_form_automation(
|
|
1369
|
+
page=page_obj,
|
|
1370
|
+
form_params=sem_params,
|
|
1371
|
+
json_payload=active_body,
|
|
1372
|
+
params=req_params,
|
|
1373
|
+
url=full_req_url,
|
|
1374
|
+
target_url=raw_visit_url,
|
|
1375
|
+
submit_form=True,
|
|
1376
|
+
)
|
|
1377
|
+
except Exception as fa_exc:
|
|
1378
|
+
console.print(f"[dim]Generic Form Automation uyarısı: {fa_exc}[/dim]")
|
|
1379
|
+
return {"executed": False}
|
|
1380
|
+
|
|
1381
|
+
console.print(f"[bold cyan]🌐 Tarayıcı açılıyor:[/bold cyan] [white]{visit_url}[/white]")
|
|
1382
|
+
try:
|
|
1383
|
+
await page.goto(visit_url, wait_until="domcontentloaded", timeout=12_000)
|
|
1384
|
+
except Exception as e:
|
|
1385
|
+
console.print(f"[dim]Sayfa gezinme bildirimi: {e}[/dim]")
|
|
1386
|
+
|
|
1387
|
+
wait_seconds = 3 if is_thy_availability else 1
|
|
1388
|
+
console.print(f"[dim]Frontend oturumunun yerleşmesi için {wait_seconds} saniye bekleniyor...[/dim]")
|
|
1389
|
+
await asyncio.sleep(wait_seconds)
|
|
1390
|
+
|
|
1391
|
+
from core.hitl_queue import HITLQueueManager
|
|
1392
|
+
|
|
1393
|
+
hitl_mgr = HITLQueueManager()
|
|
1394
|
+
active_hitl_task_id: str | None = None
|
|
1395
|
+
is_resuming_hitl = os.getenv("OWNAPI_HITL_RESUMING", "0").strip() == "1"
|
|
1396
|
+
env_otp_code = os.getenv("OWNAPI_HITL_OTP_CODE", "").strip() or None
|
|
1397
|
+
pause_and_wait = (
|
|
1398
|
+
not is_resuming_hitl
|
|
1399
|
+
and os.getenv("OWNAPI_HITL_PAUSE_AND_WAIT", "1").strip().lower() not in ("0", "false", "no")
|
|
1400
|
+
)
|
|
1401
|
+
pause_timeout_sec = float(os.getenv("OWNAPI_HITL_PAUSE_TIMEOUT", "300"))
|
|
1402
|
+
|
|
1403
|
+
if is_resuming_hitl and env_otp_code:
|
|
1404
|
+
await self._apply_otp_code_if_present(page, env_otp_code, tenant_id=active_tenant)
|
|
1405
|
+
|
|
1406
|
+
# Eğer THY availability çağrısıysa ve ana sayfada WAF engeli yoksa (veya HITL resume modundaysak),
|
|
1407
|
+
# doğrudan /api/v1/availability isteği atmadan ÖNCE arama URL'sine gidip sayfa state'ini oluştur!
|
|
1408
|
+
if is_thy_availability and (is_resuming_hitl or not await self._detect_challenge(page)):
|
|
1409
|
+
await _prepare_thy_search_page_state(
|
|
1410
|
+
page,
|
|
1411
|
+
req_payload,
|
|
1412
|
+
req_headers,
|
|
1413
|
+
cid=req_headers.get("x-client-id") or req_headers.get("x-clientid"),
|
|
1414
|
+
)
|
|
1415
|
+
val_payload = _build_validate_payload(req_payload)
|
|
1416
|
+
elif not is_thy_availability and (is_resuming_hitl or not await self._detect_challenge(page)):
|
|
1417
|
+
# Diğer tüm siteler (Pegasus, Enuygun, Obilet veya yeni eklenen siteler) için
|
|
1418
|
+
# Generic Form Automation katmanını çalıştırarak SPA state'ini oluştur!
|
|
1419
|
+
await _run_smart_form_automation(page, req_payload)
|
|
1420
|
+
|
|
1421
|
+
# Eğer arama URL'sine gidildiğinde sayfa kendi başına dolu /api/v1/availability yanıtı aldıysa onu kullan
|
|
1422
|
+
pre_captured = captured_browser_response.get("text", "")
|
|
1423
|
+
if is_thy_availability and pre_captured and _is_populated_availability(pre_captured):
|
|
1424
|
+
console.print("[bold green]✔ Arama sayfası state oluşumu sırasında dolu uçuş yanıtı yakalandı![/bold green]")
|
|
1425
|
+
eval_result = {
|
|
1426
|
+
"status": captured_browser_response.get("status", 200),
|
|
1427
|
+
"ok": True,
|
|
1428
|
+
"text": pre_captured,
|
|
1429
|
+
"step": "search_page_navigation",
|
|
1430
|
+
}
|
|
1431
|
+
else:
|
|
1432
|
+
# 1. Doğrudan açık tarayıcı sekmesi içinden fetch() isteğini tetikle
|
|
1433
|
+
console.print(f"[dim]Tarayıcı içi fetch({method.upper()} {full_req_url}) isteği gönderiliyor...[/dim]")
|
|
1434
|
+
try:
|
|
1435
|
+
eval_result = await asyncio.wait_for(
|
|
1436
|
+
page.evaluate(
|
|
1437
|
+
fetch_js,
|
|
1438
|
+
{
|
|
1439
|
+
"validateUrl": validate_url,
|
|
1440
|
+
"validateBody": val_payload,
|
|
1441
|
+
"url": full_req_url,
|
|
1442
|
+
"method": method.upper(),
|
|
1443
|
+
"headers": req_headers,
|
|
1444
|
+
"body": req_payload,
|
|
1445
|
+
},
|
|
1446
|
+
),
|
|
1447
|
+
timeout=15.0,
|
|
1448
|
+
)
|
|
1449
|
+
except Exception as eval_exc:
|
|
1450
|
+
console.print(f"[dim]In-page fetch zaman aşımı/uyarısı ({eval_exc}), context.request.fetch kullanılıyor...[/dim]")
|
|
1451
|
+
eval_result = {"status": 0, "ok": False, "text": "", "error": str(eval_exc)}
|
|
1452
|
+
|
|
1453
|
+
if eval_result.get("status", 0) == 0:
|
|
1454
|
+
try:
|
|
1455
|
+
ctx_resp = await context.request.fetch(
|
|
1456
|
+
full_req_url,
|
|
1457
|
+
method=method.upper(),
|
|
1458
|
+
headers=req_headers,
|
|
1459
|
+
data=json.dumps(req_payload) if isinstance(req_payload, (dict, list)) else req_payload,
|
|
1460
|
+
timeout=15_000,
|
|
1461
|
+
)
|
|
1462
|
+
ctx_text = await ctx_resp.text()
|
|
1463
|
+
eval_result = {
|
|
1464
|
+
"status": ctx_resp.status,
|
|
1465
|
+
"ok": ctx_resp.ok,
|
|
1466
|
+
"text": ctx_text,
|
|
1467
|
+
"step": "context_request",
|
|
1468
|
+
}
|
|
1469
|
+
except Exception as ctx_err:
|
|
1470
|
+
console.print(f"[dim]Context request uyarısı: {ctx_err}[/dim]")
|
|
1471
|
+
|
|
1472
|
+
resp_text = _extract_response_text(
|
|
1473
|
+
eval_result.get("text", "") if isinstance(eval_result, dict) else eval_result
|
|
1474
|
+
)
|
|
1475
|
+
resp_status = eval_result.get("status", 0) if isinstance(eval_result, dict) else 200
|
|
1476
|
+
is_challenge = (
|
|
1477
|
+
"PXDqLTvkTO" in resp_text
|
|
1478
|
+
or "blockScript" in resp_text
|
|
1479
|
+
or resp_status in (403, 429)
|
|
1480
|
+
or "wrapper--notfound" in resp_text
|
|
1481
|
+
or "Reference Code:" in resp_text
|
|
1482
|
+
or (not (eval_result.get("ok") if isinstance(eval_result, dict) else True) and await self._detect_challenge(page))
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
# 2. Eğer PerimeterX / Akamai / Cloudflare doğrulaması geldiyse
|
|
1486
|
+
if is_challenge:
|
|
1487
|
+
console.print("[bold yellow]⚠️ Güvenlik / İnsan doğrulaması tespit edildi![/bold yellow]")
|
|
1488
|
+
try:
|
|
1489
|
+
await page.evaluate(
|
|
1490
|
+
"""(text) => {
|
|
1491
|
+
try {
|
|
1492
|
+
const data = JSON.parse(text);
|
|
1493
|
+
if (data && data.blockScript) {
|
|
1494
|
+
window._pxAppId = data.appId;
|
|
1495
|
+
window._pxJsRef = data.jsRef;
|
|
1496
|
+
const s = document.createElement('script');
|
|
1497
|
+
s.src = data.blockScript;
|
|
1498
|
+
document.body.appendChild(s);
|
|
1499
|
+
}
|
|
1500
|
+
} catch (_) {}
|
|
1501
|
+
}""",
|
|
1502
|
+
resp_text,
|
|
1503
|
+
)
|
|
1504
|
+
except Exception:
|
|
1505
|
+
pass
|
|
1506
|
+
|
|
1507
|
+
if not is_resuming_hitl:
|
|
1508
|
+
paused_shot_b64 = await hitl_mgr.capture_page_screenshot_b64(page)
|
|
1509
|
+
hitl_task = hitl_mgr.enqueue_task(
|
|
1510
|
+
tenant_id=active_tenant,
|
|
1511
|
+
service=self.service,
|
|
1512
|
+
method=method.upper(),
|
|
1513
|
+
url=url,
|
|
1514
|
+
params=req_params,
|
|
1515
|
+
headers=req_headers,
|
|
1516
|
+
json_payload=req_payload,
|
|
1517
|
+
target_url=_build_thy_search_state_url(req_payload) if is_thy_availability else visit_url,
|
|
1518
|
+
reason=f"Captcha / WAF / PerimeterX doğrulaması algılandı (HTTP {resp_status})",
|
|
1519
|
+
in_place_paused=pause_and_wait,
|
|
1520
|
+
execution_mode=active_mode,
|
|
1521
|
+
screenshot_b64=paused_shot_b64,
|
|
1522
|
+
)
|
|
1523
|
+
active_hitl_task_id = hitl_task["task_id"]
|
|
1524
|
+
|
|
1525
|
+
if not pause_and_wait:
|
|
1526
|
+
try:
|
|
1527
|
+
await context.close()
|
|
1528
|
+
except Exception:
|
|
1529
|
+
pass
|
|
1530
|
+
if remote_browser is not None:
|
|
1531
|
+
try:
|
|
1532
|
+
await remote_browser.close()
|
|
1533
|
+
except Exception:
|
|
1534
|
+
pass
|
|
1535
|
+
return ResponseDict(
|
|
1536
|
+
{
|
|
1537
|
+
"status": "PAUSED",
|
|
1538
|
+
"legacy_status": "PENDING_USER_APPROVAL",
|
|
1539
|
+
"agent_state": "paused",
|
|
1540
|
+
"task_id": hitl_task["task_id"],
|
|
1541
|
+
"service": self.service,
|
|
1542
|
+
"tenant_id": active_tenant,
|
|
1543
|
+
"reason": hitl_task["reason"],
|
|
1544
|
+
"message": "Ajan PAUSED durumuna alındı; Web Dashboard üzerinden onay bekleniyor",
|
|
1545
|
+
}
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
console.print(
|
|
1549
|
+
f"[bold yellow]⏸️ [HITL PAUSED] Ajan görevi ({hitl_task['task_id']}) öldürülmedi, 'PAUSED' durumuna alındı.[/bold yellow]\n"
|
|
1550
|
+
f"[cyan]👉 Web Dashboard üzerinden onay verildiğinde kaldığı yerden ('resume') devam edecek...[/cyan]"
|
|
1551
|
+
)
|
|
1552
|
+
resumed_task = await hitl_mgr.await_task_resume(
|
|
1553
|
+
task_id=hitl_task["task_id"],
|
|
1554
|
+
poll_interval=0.5,
|
|
1555
|
+
timeout_sec=pause_timeout_sec,
|
|
1556
|
+
)
|
|
1557
|
+
if resumed_task is None:
|
|
1558
|
+
try:
|
|
1559
|
+
await context.close()
|
|
1560
|
+
except Exception:
|
|
1561
|
+
pass
|
|
1562
|
+
if remote_browser is not None:
|
|
1563
|
+
try:
|
|
1564
|
+
await remote_browser.close()
|
|
1565
|
+
except Exception:
|
|
1566
|
+
pass
|
|
1567
|
+
return ResponseDict(
|
|
1568
|
+
{
|
|
1569
|
+
"status": "PAUSED",
|
|
1570
|
+
"legacy_status": "PENDING_USER_APPROVAL",
|
|
1571
|
+
"agent_state": "paused",
|
|
1572
|
+
"task_id": hitl_task["task_id"],
|
|
1573
|
+
"service": self.service,
|
|
1574
|
+
"tenant_id": active_tenant,
|
|
1575
|
+
"reason": hitl_task["reason"],
|
|
1576
|
+
"message": "Kullanıcı doğrulaması bekleniyor (.sessions/hitl_state.json)",
|
|
1577
|
+
}
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
console.print(
|
|
1581
|
+
f"[bold green]▶️ [HITL RESUMED] Kullanıcı onayı alındı ({hitl_task['task_id']}), ajan kaldığı yerden devam ediyor...[/bold green]"
|
|
1582
|
+
)
|
|
1583
|
+
resumed_otp = str(resumed_task.get("otp_code") or os.getenv("OWNAPI_HITL_OTP_CODE") or "").strip() or None
|
|
1584
|
+
if resumed_otp:
|
|
1585
|
+
await self._apply_otp_code_if_present(page, resumed_otp, tenant_id=active_tenant)
|
|
1586
|
+
else:
|
|
1587
|
+
await asyncio.sleep(2.0)
|
|
1588
|
+
if env_otp_code:
|
|
1589
|
+
await self._apply_otp_code_if_present(page, env_otp_code, tenant_id=active_tenant)
|
|
1590
|
+
|
|
1591
|
+
# WAF/Captcha geçildikten ve HITL onayı alındıktan sonra:
|
|
1592
|
+
# Doğrudan API isteği atmadan ÖNCE Generic Form Automation ve arama URL'si ile sayfa state'ini oluştur!
|
|
1593
|
+
if is_thy_availability:
|
|
1594
|
+
fresh_waf_cid = str(uuid.uuid4())
|
|
1595
|
+
await _prepare_thy_search_page_state(
|
|
1596
|
+
page,
|
|
1597
|
+
req_payload,
|
|
1598
|
+
req_headers,
|
|
1599
|
+
cid=fresh_waf_cid,
|
|
1600
|
+
)
|
|
1601
|
+
val_payload = _build_validate_payload(req_payload)
|
|
1602
|
+
else:
|
|
1603
|
+
await _run_smart_form_automation(page, req_payload)
|
|
1604
|
+
|
|
1605
|
+
captured_after_waf = captured_browser_response.get("text", "")
|
|
1606
|
+
if is_thy_availability and captured_after_waf and _is_populated_availability(captured_after_waf):
|
|
1607
|
+
console.print("[bold green]✔ WAF sonrası arama sayfası state oluşumundan dolu uçuş yanıtı yakalandı![/bold green]")
|
|
1608
|
+
eval_result = {
|
|
1609
|
+
"status": captured_browser_response.get("status", 200),
|
|
1610
|
+
"ok": True,
|
|
1611
|
+
"text": captured_after_waf,
|
|
1612
|
+
}
|
|
1613
|
+
else:
|
|
1614
|
+
eval_result = await page.evaluate(
|
|
1615
|
+
fetch_js,
|
|
1616
|
+
{
|
|
1617
|
+
"validateUrl": validate_url,
|
|
1618
|
+
"validateBody": val_payload,
|
|
1619
|
+
"url": full_req_url,
|
|
1620
|
+
"method": method.upper(),
|
|
1621
|
+
"headers": req_headers,
|
|
1622
|
+
"body": req_payload,
|
|
1623
|
+
},
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
# 3. THY Özel Kontrolü: Eğer Error-DS-30006 dönerse veya availabilityInfoResponse boş ([[], []]) geldiyse
|
|
1627
|
+
if is_thy_availability:
|
|
1628
|
+
resp_text = _extract_response_text(eval_result)
|
|
1629
|
+
is_empty_air = False
|
|
1630
|
+
try:
|
|
1631
|
+
chk_res = json.loads(resp_text)
|
|
1632
|
+
if isinstance(chk_res, dict):
|
|
1633
|
+
data_part = chk_res.get("data")
|
|
1634
|
+
if isinstance(data_part, dict) and data_part.get("availabilityInfoResponse") == [[], []]:
|
|
1635
|
+
is_empty_air = True
|
|
1636
|
+
except Exception:
|
|
1637
|
+
pass
|
|
1638
|
+
|
|
1639
|
+
is_populated = _is_populated_availability(resp_text)
|
|
1640
|
+
is_ds_error = (
|
|
1641
|
+
"Error-DS-30006" in resp_text
|
|
1642
|
+
or "DS-30006" in resp_text
|
|
1643
|
+
or "işleminizi gerçekleştiremiyoruz" in resp_text
|
|
1644
|
+
or (is_empty_air and not is_populated)
|
|
1645
|
+
)
|
|
1646
|
+
|
|
1647
|
+
if is_ds_error or not is_populated:
|
|
1648
|
+
if is_empty_air:
|
|
1649
|
+
console.print("\n[bold yellow]⚠️ Boş availabilityInfoResponse: [[], []] alındı (oturum durumu henüz hazır değil).[/bold yellow]")
|
|
1650
|
+
else:
|
|
1651
|
+
console.print("\n[bold yellow]⚠️ Backend oturum/şema uyarısı alındı.[/bold yellow]")
|
|
1652
|
+
|
|
1653
|
+
if not is_resuming_hitl:
|
|
1654
|
+
paused_thy_shot_b64 = await hitl_mgr.capture_page_screenshot_b64(page)
|
|
1655
|
+
if active_hitl_task_id is None:
|
|
1656
|
+
hitl_task = hitl_mgr.enqueue_task(
|
|
1657
|
+
tenant_id=active_tenant,
|
|
1658
|
+
service=self.service,
|
|
1659
|
+
method=method.upper(),
|
|
1660
|
+
url=url,
|
|
1661
|
+
params=req_params,
|
|
1662
|
+
headers=req_headers,
|
|
1663
|
+
json_payload=req_payload,
|
|
1664
|
+
target_url=_build_thy_search_state_url(req_payload),
|
|
1665
|
+
reason="THY oturum/müsaitlik ön etkileşimi veya WAF doğrulaması gerekiyor",
|
|
1666
|
+
in_place_paused=pause_and_wait,
|
|
1667
|
+
execution_mode=active_mode,
|
|
1668
|
+
screenshot_b64=paused_thy_shot_b64,
|
|
1669
|
+
)
|
|
1670
|
+
active_hitl_task_id = hitl_task["task_id"]
|
|
1671
|
+
else:
|
|
1672
|
+
if paused_thy_shot_b64:
|
|
1673
|
+
hitl_mgr.update_task_state(
|
|
1674
|
+
active_hitl_task_id,
|
|
1675
|
+
status="PAUSED",
|
|
1676
|
+
screenshot_b64=paused_thy_shot_b64,
|
|
1677
|
+
execution_mode=active_mode,
|
|
1678
|
+
)
|
|
1679
|
+
hitl_task, _ = hitl_mgr.get_task(active_hitl_task_id, tenant_id=active_tenant)
|
|
1680
|
+
hitl_task = hitl_task or {"task_id": active_hitl_task_id, "reason": "THY doğrulaması gerekiyor"}
|
|
1681
|
+
|
|
1682
|
+
if not pause_and_wait:
|
|
1683
|
+
try:
|
|
1684
|
+
await context.close()
|
|
1685
|
+
except Exception:
|
|
1686
|
+
pass
|
|
1687
|
+
if remote_browser is not None:
|
|
1688
|
+
try:
|
|
1689
|
+
await remote_browser.close()
|
|
1690
|
+
except Exception:
|
|
1691
|
+
pass
|
|
1692
|
+
return ResponseDict(
|
|
1693
|
+
{
|
|
1694
|
+
"status": "PAUSED",
|
|
1695
|
+
"legacy_status": "PENDING_USER_APPROVAL",
|
|
1696
|
+
"agent_state": "paused",
|
|
1697
|
+
"task_id": hitl_task["task_id"],
|
|
1698
|
+
"service": self.service,
|
|
1699
|
+
"tenant_id": active_tenant,
|
|
1700
|
+
"reason": hitl_task["reason"],
|
|
1701
|
+
"message": "Ajan PAUSED durumuna alındı; Web Dashboard üzerinden onay bekleniyor",
|
|
1702
|
+
}
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1705
|
+
console.print(
|
|
1706
|
+
f"[bold yellow]⏸️ [HITL PAUSED] THY görevi ({hitl_task['task_id']}) 'PAUSED' durumuna alındı.[/bold yellow]\n"
|
|
1707
|
+
f"[cyan]👉 Web Dashboard üzerinden onay verildiğinde kaldığı yerden ('resume') devam edecek...[/cyan]"
|
|
1708
|
+
)
|
|
1709
|
+
resumed_task = await hitl_mgr.await_task_resume(
|
|
1710
|
+
task_id=hitl_task["task_id"],
|
|
1711
|
+
poll_interval=0.5,
|
|
1712
|
+
timeout_sec=pause_timeout_sec,
|
|
1713
|
+
)
|
|
1714
|
+
if resumed_task is None:
|
|
1715
|
+
try:
|
|
1716
|
+
await context.close()
|
|
1717
|
+
except Exception:
|
|
1718
|
+
pass
|
|
1719
|
+
if remote_browser is not None:
|
|
1720
|
+
try:
|
|
1721
|
+
await remote_browser.close()
|
|
1722
|
+
except Exception:
|
|
1723
|
+
pass
|
|
1724
|
+
return ResponseDict(
|
|
1725
|
+
{
|
|
1726
|
+
"status": "PAUSED",
|
|
1727
|
+
"legacy_status": "PENDING_USER_APPROVAL",
|
|
1728
|
+
"agent_state": "paused",
|
|
1729
|
+
"task_id": hitl_task["task_id"],
|
|
1730
|
+
"service": self.service,
|
|
1731
|
+
"tenant_id": active_tenant,
|
|
1732
|
+
"reason": hitl_task["reason"],
|
|
1733
|
+
"message": "Kullanıcı doğrulaması bekleniyor (.sessions/hitl_state.json)",
|
|
1734
|
+
}
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
console.print(
|
|
1738
|
+
f"[bold green]▶️ [HITL RESUMED] Kullanıcı onayı alındı ({hitl_task['task_id']}), THY görevi kaldığı yerden devam ediyor...[/bold green]"
|
|
1739
|
+
)
|
|
1740
|
+
resumed_thy_otp = str(resumed_task.get("otp_code") or os.getenv("OWNAPI_HITL_OTP_CODE") or "").strip() or None
|
|
1741
|
+
if resumed_thy_otp:
|
|
1742
|
+
await self._apply_otp_code_if_present(page, resumed_thy_otp, tenant_id=active_tenant)
|
|
1743
|
+
else:
|
|
1744
|
+
await asyncio.sleep(2.0)
|
|
1745
|
+
if env_otp_code:
|
|
1746
|
+
await self._apply_otp_code_if_present(page, env_otp_code, tenant_id=active_tenant)
|
|
1747
|
+
|
|
1748
|
+
captured_text = captured_browser_response.get("text", "")
|
|
1749
|
+
if captured_text and _is_populated_availability(captured_text):
|
|
1750
|
+
console.print("[bold green]✔ Tarayıcı trafiğinden dolu uçuş yanıtı başarıyla yakalandı ve kullanılıyor![/bold green]")
|
|
1751
|
+
eval_result = {
|
|
1752
|
+
"status": captured_browser_response.get("status", 200),
|
|
1753
|
+
"ok": True,
|
|
1754
|
+
"text": captured_text,
|
|
1755
|
+
}
|
|
1756
|
+
else:
|
|
1757
|
+
fresh_call_cid = str(uuid.uuid4())
|
|
1758
|
+
retry_headers = dict(captured_browser_request.get("headers") or req_headers)
|
|
1759
|
+
retry_url = captured_browser_request.get("url") or full_req_url
|
|
1760
|
+
retry_payload = captured_browser_request.get("post_data") or req_payload
|
|
1761
|
+
|
|
1762
|
+
# HITL resume sonrası doğrudan /api/v1/availability isteği atmadan ÖNCE
|
|
1763
|
+
# Playwright tarayıcısını gidiş-dönüş parametreleriyle arama URL'sine götür ve sayfa state'ini oluştur!
|
|
1764
|
+
await _prepare_thy_search_page_state(
|
|
1765
|
+
page,
|
|
1766
|
+
retry_payload,
|
|
1767
|
+
retry_headers,
|
|
1768
|
+
cid=fresh_call_cid,
|
|
1769
|
+
)
|
|
1770
|
+
val_payload = _build_validate_payload(retry_payload)
|
|
1771
|
+
|
|
1772
|
+
captured_after_nav = captured_browser_response.get("text", "")
|
|
1773
|
+
if captured_after_nav and _is_populated_availability(captured_after_nav):
|
|
1774
|
+
console.print("[bold green]✔ Arama sayfasına gezinme sonrası dolu uçuş yanıtı yakalandı![/bold green]")
|
|
1775
|
+
eval_result = {
|
|
1776
|
+
"status": captured_browser_response.get("status", 200),
|
|
1777
|
+
"ok": True,
|
|
1778
|
+
"text": captured_after_nav,
|
|
1779
|
+
}
|
|
1780
|
+
else:
|
|
1781
|
+
eval_result = await page.evaluate(
|
|
1782
|
+
fetch_js,
|
|
1783
|
+
{
|
|
1784
|
+
"validateUrl": validate_url,
|
|
1785
|
+
"validateBody": val_payload,
|
|
1786
|
+
"url": retry_url,
|
|
1787
|
+
"method": method.upper(),
|
|
1788
|
+
"headers": retry_headers,
|
|
1789
|
+
"body": retry_payload,
|
|
1790
|
+
},
|
|
1791
|
+
)
|
|
1792
|
+
|
|
1793
|
+
# 4. Taze çerezleri ve User-Agent'ı TokenStore'a (.sessions/{tenant_id}/{service}_tokens.json) kaydet
|
|
1794
|
+
try:
|
|
1795
|
+
cookies = await context.cookies()
|
|
1796
|
+
user_agent = await page.evaluate("() => navigator.userAgent")
|
|
1797
|
+
cookie_str = "; ".join(f"{c['name']}={c['value']}" for c in cookies if c.get("name"))
|
|
1798
|
+
parsed_visit = urlparse(visit_url)
|
|
1799
|
+
origin_val = f"{parsed_visit.scheme}://{parsed_visit.netloc}" if parsed_visit.scheme and parsed_visit.netloc else "https://www.turkishairlines.com"
|
|
1800
|
+
|
|
1801
|
+
token_bundle = {
|
|
1802
|
+
"target_url": visit_url,
|
|
1803
|
+
"healed_at": datetime.now(timezone.utc).isoformat(),
|
|
1804
|
+
"cookie": cookie_str,
|
|
1805
|
+
"user-agent": user_agent,
|
|
1806
|
+
"headers": {
|
|
1807
|
+
"cookie": cookie_str,
|
|
1808
|
+
"user-agent": user_agent,
|
|
1809
|
+
"accept": "application/json",
|
|
1810
|
+
"content-type": "application/json",
|
|
1811
|
+
"origin": origin_val,
|
|
1812
|
+
"referer": visit_url,
|
|
1813
|
+
},
|
|
1814
|
+
"query_params": {},
|
|
1815
|
+
"body_tokens": {},
|
|
1816
|
+
}
|
|
1817
|
+
self.token_store.save_session(
|
|
1818
|
+
tenant_id=active_tenant,
|
|
1819
|
+
service=self.service,
|
|
1820
|
+
cookies=cookies,
|
|
1821
|
+
user_agent=user_agent,
|
|
1822
|
+
metadata=token_bundle,
|
|
1823
|
+
)
|
|
1824
|
+
await context.storage_state(path=str(self.profile_dir / "storage_state.json"))
|
|
1825
|
+
except Exception:
|
|
1826
|
+
pass
|
|
1827
|
+
|
|
1828
|
+
try:
|
|
1829
|
+
await context.close()
|
|
1830
|
+
except Exception:
|
|
1831
|
+
pass
|
|
1832
|
+
if remote_browser is not None:
|
|
1833
|
+
try:
|
|
1834
|
+
await remote_browser.close()
|
|
1835
|
+
except Exception:
|
|
1836
|
+
pass
|
|
1837
|
+
|
|
1838
|
+
# Dönen yanıtı parse et ve döndür (dict ise .text aramaya kalkmadan doğrudan işle)
|
|
1839
|
+
if isinstance(eval_result, dict) and "text" not in eval_result and "status" not in eval_result:
|
|
1840
|
+
final_res_obj: Any = ResponseDict(eval_result)
|
|
1841
|
+
if active_hitl_task_id:
|
|
1842
|
+
hitl_mgr.update_task_state(active_hitl_task_id, status="COMPLETED", response=dict(final_res_obj))
|
|
1843
|
+
return final_res_obj
|
|
1844
|
+
|
|
1845
|
+
final_text = _extract_response_text(eval_result)
|
|
1846
|
+
status_val = eval_result.get("status", 200) if isinstance(eval_result, dict) else 200
|
|
1847
|
+
is_ok = bool(eval_result.get("ok", 200 <= status_val < 400)) if isinstance(eval_result, dict) else True
|
|
1848
|
+
|
|
1849
|
+
try:
|
|
1850
|
+
parsed_json = json.loads(final_text)
|
|
1851
|
+
if isinstance(parsed_json, dict):
|
|
1852
|
+
final_res_obj = ResponseDict(parsed_json)
|
|
1853
|
+
if active_hitl_task_id:
|
|
1854
|
+
hitl_mgr.update_task_state(active_hitl_task_id, status="COMPLETED", response=dict(final_res_obj))
|
|
1855
|
+
return final_res_obj
|
|
1856
|
+
if active_hitl_task_id:
|
|
1857
|
+
hitl_mgr.update_task_state(active_hitl_task_id, status="COMPLETED", response=parsed_json)
|
|
1858
|
+
return parsed_json
|
|
1859
|
+
except Exception:
|
|
1860
|
+
if is_ok and final_text and not (isinstance(eval_result, dict) and eval_result.get("error")):
|
|
1861
|
+
final_res_obj = ResponseDict(
|
|
1862
|
+
{
|
|
1863
|
+
"success": True,
|
|
1864
|
+
"status_code": status_val,
|
|
1865
|
+
"ok": True,
|
|
1866
|
+
"text": final_text,
|
|
1867
|
+
"response_sample": final_text[:3000],
|
|
1868
|
+
}
|
|
1869
|
+
)
|
|
1870
|
+
if active_hitl_task_id:
|
|
1871
|
+
hitl_mgr.update_task_state(active_hitl_task_id, status="COMPLETED", response=dict(final_res_obj))
|
|
1872
|
+
return final_res_obj
|
|
1873
|
+
final_res_obj = ResponseDict(
|
|
1874
|
+
{
|
|
1875
|
+
"status_code": status_val,
|
|
1876
|
+
"text": final_text,
|
|
1877
|
+
"response_sample": final_text[:3000],
|
|
1878
|
+
"error": (eval_result.get("error") if isinstance(eval_result, dict) else None)
|
|
1879
|
+
or "Yanıt JSON formatında ayrıştırılamadı",
|
|
1880
|
+
}
|
|
1881
|
+
)
|
|
1882
|
+
if active_hitl_task_id:
|
|
1883
|
+
hitl_mgr.update_task_state(active_hitl_task_id, status="COMPLETED", response=dict(final_res_obj))
|
|
1884
|
+
return final_res_obj
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def execute_in_browser(
|
|
1888
|
+
self,
|
|
1889
|
+
method: str,
|
|
1890
|
+
url: str,
|
|
1891
|
+
headers: dict[str, str] | None = None,
|
|
1892
|
+
params: dict[str, Any] | None = None,
|
|
1893
|
+
json_payload: Any = None,
|
|
1894
|
+
target_url: str | None = None,
|
|
1895
|
+
user_wait_sec: int | None = None,
|
|
1896
|
+
challenge_wait_sec: int | None = None,
|
|
1897
|
+
tenant_id: str | None = None,
|
|
1898
|
+
mode: str | None = None,
|
|
1899
|
+
non_interactive: bool | None = None,
|
|
1900
|
+
form_params: dict[str, Any] | None = None,
|
|
1901
|
+
auto_fill_form: bool | None = None,
|
|
1902
|
+
) -> dict[str, Any]:
|
|
1903
|
+
"""
|
|
1904
|
+
OwnAPI User-Delegated Browser Middleware:
|
|
1905
|
+
İsteği seçilen modda ('local' yerel Chrome veya 'cloud' Docker/Remote Browser)
|
|
1906
|
+
doğrudan tarayıcının kendi JavaScript ortamında (page.evaluate fetch) çalıştırır.
|
|
1907
|
+
"""
|
|
1908
|
+
return self._run_sync(
|
|
1909
|
+
self.aexecute_in_browser(
|
|
1910
|
+
method=method,
|
|
1911
|
+
url=url,
|
|
1912
|
+
headers=headers,
|
|
1913
|
+
params=params,
|
|
1914
|
+
json_payload=json_payload,
|
|
1915
|
+
target_url=target_url,
|
|
1916
|
+
user_wait_sec=user_wait_sec,
|
|
1917
|
+
challenge_wait_sec=challenge_wait_sec,
|
|
1918
|
+
tenant_id=tenant_id,
|
|
1919
|
+
mode=mode,
|
|
1920
|
+
non_interactive=non_interactive,
|
|
1921
|
+
form_params=form_params,
|
|
1922
|
+
auto_fill_form=auto_fill_form,
|
|
1923
|
+
)
|
|
1924
|
+
)
|
|
1925
|
+
|
|
1926
|
+
def retry_with_fresh_session(
|
|
1927
|
+
self,
|
|
1928
|
+
method: str,
|
|
1929
|
+
url: str,
|
|
1930
|
+
headers: dict[str, str],
|
|
1931
|
+
params: dict[str, Any] | None = None,
|
|
1932
|
+
json_payload: Any = None,
|
|
1933
|
+
target_url: str | None = None,
|
|
1934
|
+
) -> tuple[dict[str, str], dict[str, Any], Any, Any]:
|
|
1935
|
+
"""
|
|
1936
|
+
401/403/429 alındığında taze oturum açar, token ve başlıkları yeniler ve
|
|
1937
|
+
isteği taze oturum tüneli üzerinden tekrarlar.
|
|
1938
|
+
Dönüş: (güncel_headers, güncel_params, güncel_payload, tünel_yanıtı_veya_None)
|
|
1939
|
+
"""
|
|
1940
|
+
visit_url = target_url or headers.get("referer") or headers.get("origin")
|
|
1941
|
+
bundle = self._run_sync(
|
|
1942
|
+
self.aheal(
|
|
1943
|
+
target_url=visit_url,
|
|
1944
|
+
retry_request={
|
|
1945
|
+
"method": method,
|
|
1946
|
+
"url": url,
|
|
1947
|
+
"headers": headers,
|
|
1948
|
+
"params": params,
|
|
1949
|
+
"json_payload": json_payload,
|
|
1950
|
+
},
|
|
1951
|
+
)
|
|
1952
|
+
)
|
|
1953
|
+
tunnel_resp = bundle.get("_tunnel_response")
|
|
1954
|
+
new_headers, new_params, new_payload = self.apply_to_request(
|
|
1955
|
+
headers,
|
|
1956
|
+
params,
|
|
1957
|
+
json_payload,
|
|
1958
|
+
endpoint_url=url,
|
|
1959
|
+
)
|
|
1960
|
+
return new_headers, new_params, new_payload, tunnel_resp
|
|
1961
|
+
|
|
1962
|
+
@staticmethod
|
|
1963
|
+
def _run_sync(coro: Any) -> Any:
|
|
1964
|
+
try:
|
|
1965
|
+
loop = asyncio.get_running_loop()
|
|
1966
|
+
except RuntimeError:
|
|
1967
|
+
loop = None
|
|
1968
|
+
|
|
1969
|
+
if loop and loop.is_running():
|
|
1970
|
+
import concurrent.futures
|
|
1971
|
+
|
|
1972
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
1973
|
+
future = pool.submit(asyncio.run, coro)
|
|
1974
|
+
return future.result()
|
|
1975
|
+
return asyncio.run(coro)
|
|
1976
|
+
|
|
1977
|
+
def load_tokens(self, tenant_id: str | None = None, service: str | None = None) -> dict[str, Any]:
|
|
1978
|
+
"""TokenStore (veya geriye dönük uyumlulukla session_tokens.json) üzerinden oturum çerezlerini okur."""
|
|
1979
|
+
t_id = tenant_id or self.tenant_id
|
|
1980
|
+
svc = service or self.service
|
|
1981
|
+
return self.token_store.get_session(t_id, svc)
|
|
1982
|
+
|
|
1983
|
+
def load_profile_cookies(self, endpoint_url: str | None = None) -> str:
|
|
1984
|
+
"""Kalıcı tarayıcı profilindeki (.browser_profile/storage_state.json) çerezleri okur."""
|
|
1985
|
+
state_file = self.profile_dir / "storage_state.json"
|
|
1986
|
+
if not state_file.exists():
|
|
1987
|
+
return ""
|
|
1988
|
+
try:
|
|
1989
|
+
state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
1990
|
+
cookies = state.get("cookies", [])
|
|
1991
|
+
if not isinstance(cookies, list) or not cookies:
|
|
1992
|
+
return ""
|
|
1993
|
+
ep_host = (urlparse(endpoint_url).hostname or "").lower() if endpoint_url else ""
|
|
1994
|
+
ep_root = _root_domain(ep_host) if ep_host else ""
|
|
1995
|
+
matched: list[str] = []
|
|
1996
|
+
for c in cookies:
|
|
1997
|
+
if not isinstance(c, dict) or not c.get("name"):
|
|
1998
|
+
continue
|
|
1999
|
+
c_domain = str(c.get("domain", "")).lstrip(".").lower()
|
|
2000
|
+
c_root = _root_domain(c_domain) if c_domain else ""
|
|
2001
|
+
if not ep_root or not c_root or ep_root == c_root:
|
|
2002
|
+
matched.append(f"{c['name']}={c.get('value', '')}")
|
|
2003
|
+
return "; ".join(matched)
|
|
2004
|
+
except Exception:
|
|
2005
|
+
return ""
|
|
2006
|
+
|
|
2007
|
+
def apply_to_request(
|
|
2008
|
+
self,
|
|
2009
|
+
headers: dict[str, str],
|
|
2010
|
+
params: dict[str, Any] | None = None,
|
|
2011
|
+
payload: Any = None,
|
|
2012
|
+
endpoint_url: str | None = None,
|
|
2013
|
+
) -> Any:
|
|
2014
|
+
"""
|
|
2015
|
+
Mevcut istek başlıklarını (headers), query parametrelerini (params) ve gövdesini (payload)
|
|
2016
|
+
hem kalıcı profil (.browser_profile) çerezleriyle hem de session_tokens.json içindeki
|
|
2017
|
+
dinamik başlık ve token'larla günceller.
|
|
2018
|
+
Geriye uyumluluk için payload verilmemişse (headers, params), verilmişse (headers, params, payload) döndürür.
|
|
2019
|
+
"""
|
|
2020
|
+
merged_headers = dict(headers)
|
|
2021
|
+
merged_params = dict(params) if params is not None else {}
|
|
2022
|
+
merged_payload = copy.deepcopy(payload)
|
|
2023
|
+
|
|
2024
|
+
profile_cookie_str = self.load_profile_cookies(endpoint_url)
|
|
2025
|
+
if profile_cookie_str:
|
|
2026
|
+
merged_headers["cookie"] = _merge_cookie_strings(
|
|
2027
|
+
merged_headers.get("cookie", ""),
|
|
2028
|
+
profile_cookie_str,
|
|
2029
|
+
)
|
|
2030
|
+
|
|
2031
|
+
saved = self.load_tokens()
|
|
2032
|
+
if saved:
|
|
2033
|
+
saved_target = str(saved.get("target_url", "")).lower()
|
|
2034
|
+
same_service = False
|
|
2035
|
+
if endpoint_url and saved_target:
|
|
2036
|
+
ep_host = (urlparse(endpoint_url).hostname or "").lower()
|
|
2037
|
+
target_host = (urlparse(saved_target).hostname or "").lower()
|
|
2038
|
+
if ep_host and target_host:
|
|
2039
|
+
same_service = (_root_domain(ep_host) == _root_domain(target_host)) or (
|
|
2040
|
+
"algolia" in ep_host and ("algolia" in target_host or "hn.algolia" in target_host)
|
|
2041
|
+
)
|
|
2042
|
+
elif not saved_target:
|
|
2043
|
+
same_service = True
|
|
2044
|
+
|
|
2045
|
+
if same_service:
|
|
2046
|
+
saved_headers = saved.get("headers")
|
|
2047
|
+
if isinstance(saved_headers, dict):
|
|
2048
|
+
for k, v in saved_headers.items():
|
|
2049
|
+
if not v:
|
|
2050
|
+
continue
|
|
2051
|
+
if k.lower() == "cookie":
|
|
2052
|
+
merged_headers["cookie"] = _merge_cookie_strings(merged_headers.get("cookie", ""), str(v))
|
|
2053
|
+
else:
|
|
2054
|
+
merged_headers[k] = str(v)
|
|
2055
|
+
|
|
2056
|
+
saved_params = saved.get("query_params")
|
|
2057
|
+
if isinstance(saved_params, dict):
|
|
2058
|
+
for k, v in saved_params.items():
|
|
2059
|
+
if v is not None and k in merged_params:
|
|
2060
|
+
merged_params[k] = v
|
|
2061
|
+
|
|
2062
|
+
saved_body_tokens = saved.get("body_tokens")
|
|
2063
|
+
if isinstance(saved_body_tokens, dict) and saved_body_tokens and merged_payload is not None:
|
|
2064
|
+
merged_payload = update_payload_deep(merged_payload, saved_body_tokens)
|
|
2065
|
+
|
|
2066
|
+
if payload is not None:
|
|
2067
|
+
return merged_headers, merged_params, merged_payload
|
|
2068
|
+
return merged_headers, merged_params
|
|
2069
|
+
|
|
2070
|
+
|
|
2071
|
+
if __name__ == "__main__":
|
|
2072
|
+
healer = SessionHealer()
|
|
2073
|
+
healer.heal()
|
|
2074
|
+
|