|
|
@@ -17,6 +17,8 @@ from vs_plg import IVSPlg
|
|
|
from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
|
|
|
from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
|
|
|
from toolkit.proxy_tunnel import ProxyTunnel
|
|
|
+from utils.mouse import HumanMouse
|
|
|
+from utils.keyboard import HumanKeyboard
|
|
|
|
|
|
|
|
|
class BrowserResponse:
|
|
|
@@ -51,6 +53,8 @@ class TlsPlugin(IVSPlg):
|
|
|
self.is_healthy = True
|
|
|
self.logger = None
|
|
|
|
|
|
+ self.mouse = None
|
|
|
+ self.keyboard = None
|
|
|
self.page: Optional[ChromiumPage] = None
|
|
|
self.travel_group: Optional[Dict] = None
|
|
|
|
|
|
@@ -67,7 +71,7 @@ class TlsPlugin(IVSPlg):
|
|
|
def get_group_id(self) -> str:
|
|
|
return self.group_id
|
|
|
|
|
|
- def set_log(self, logger: Callable[[str], None]) -> None:
|
|
|
+ def set_log(self, logger: Callable[[str], None]):
|
|
|
self.logger = logger
|
|
|
|
|
|
def _log(self, message):
|
|
|
@@ -162,53 +166,36 @@ class TlsPlugin(IVSPlg):
|
|
|
|
|
|
try:
|
|
|
self.page = ChromiumPage(co)
|
|
|
-
|
|
|
- apt_config = self.free_config.get('apt_config', {})
|
|
|
- if not apt_config:
|
|
|
- raise NotFoundError("apt_config config missing")
|
|
|
-
|
|
|
- login_url = "https://visas-fr.tlscontact.com/en-us/login"
|
|
|
- params = {
|
|
|
- "issuerId": apt_config["code"],
|
|
|
- "country": apt_config["country"],
|
|
|
- "vac": apt_config["code"],
|
|
|
- "redirect": f"/en-us/country/{apt_config['country']}/vac/{apt_config['code']}"
|
|
|
- }
|
|
|
- full_login_url = f"{login_url}?{urlencode(params)}"
|
|
|
-
|
|
|
- self._log(f"Navigating: {full_login_url}")
|
|
|
- self.page.get(full_login_url)
|
|
|
-
|
|
|
- cf = CloudflareBypasser(self.page, log=self.config.debug)
|
|
|
- if not cf.bypass(max_retry=15):
|
|
|
+ tls_url = self.free_config.get('tls_url', '')
|
|
|
+ self._log(f"Navigating: {tls_url}")
|
|
|
+ self.page.get(tls_url)
|
|
|
+ time.sleep(5)
|
|
|
+ cf_bypasser = CloudflareBypasser(self.page, log=True)
|
|
|
+ if not cf_bypasser.bypass(max_retry=15):
|
|
|
raise BizLogicError("Cloudflare bypass timeout")
|
|
|
+ time.sleep(3)
|
|
|
+ cf_bypasser.handle_waiting_room()
|
|
|
|
|
|
- wait_start = time.time()
|
|
|
- while True:
|
|
|
- html = self.page.html.lower()
|
|
|
- cloudflare_waitingroom_indicators = [
|
|
|
- "file d'attente" in html,
|
|
|
- 'waiting room' in html
|
|
|
- ]
|
|
|
- if any(cloudflare_waitingroom_indicators):
|
|
|
- if time.time() - wait_start > 60 * 60:
|
|
|
- raise BizLogicError(message="Cloudflare waiting room timeout (1h).")
|
|
|
-
|
|
|
- self._log("In Waiting Room... Waiting for auto-refresh.")
|
|
|
- time.sleep(30)
|
|
|
- else:
|
|
|
- break
|
|
|
-
|
|
|
- # --- 登录页面检查 ---
|
|
|
- if not self.page.ele('#email-input-field'):
|
|
|
- self._log("Reloading Login Page...")
|
|
|
- self.page.get(full_login_url)
|
|
|
- if not self.page.wait.ele_displayed('#email-input-field', timeout=15):
|
|
|
- self._save_screenshot("login_load_fail")
|
|
|
- raise BizLogicError("Login form not loaded")
|
|
|
+ self._log("Init humanize tools...")
|
|
|
+ self.mouse = HumanMouse(self.page, debug=True)
|
|
|
+ self.keyboard = HumanKeyboard(self.page)
|
|
|
+ self._log("Random mouse start position...")
|
|
|
+ viewport_width = self.page.rect.viewport_size[0]
|
|
|
+ viewport_height = self.page.rect.viewport_size[1]
|
|
|
+ init_x = random.randint(10, viewport_width - 10)
|
|
|
+ init_y = random.randint(10, viewport_height - 10)
|
|
|
+ self.mouse.move(init_x, init_y)
|
|
|
+
|
|
|
+ btn_selector = '#btn-login'
|
|
|
+ if not self.page.wait.ele_displayed(btn_selector, timeout=3):
|
|
|
+ register_btn = self.page.ele("tag:a@@href:login")
|
|
|
+ self.mouse.human_click_ele(register_btn)
|
|
|
+ time.sleep(3)
|
|
|
+ if not self.page.wait.ele_displayed(btn_selector, timeout=10):
|
|
|
+ raise BizLogicError(message=f"Can't find selector={btn_selector}")
|
|
|
+ time.sleep(random.uniform(0.5, 1))
|
|
|
|
|
|
- # --- JS 注入登录 ---
|
|
|
- g_token = ""
|
|
|
+ recaptchav2_token = ""
|
|
|
if self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]'):
|
|
|
self._log("Solving ReCaptcha...")
|
|
|
rc_params = {
|
|
|
@@ -217,60 +204,81 @@ class TlsPlugin(IVSPlg):
|
|
|
"siteKey": "6LcDpXcfAAAAAM7wOEsF_38DNsL20tTvPTKxpyn0",
|
|
|
"apiToken": self.free_config.get("capsolver_key", "")
|
|
|
}
|
|
|
- g_token = self._solve_recaptcha(rc_params)
|
|
|
+ recaptchav2_token = self._solve_recaptcha(rc_params)
|
|
|
|
|
|
username = self.config.account.username
|
|
|
password = self.config.account.password
|
|
|
|
|
|
- js_login = f"""
|
|
|
- var u = document.getElementById('email-input-field');
|
|
|
- if(u) {{ u.value = "{username}"; u.dispatchEvent(new Event('input', {{bubbles:true}})); }}
|
|
|
+ input_ele = self.page.ele('#email-input-field')
|
|
|
+ self.mouse.human_click_ele(input_ele)
|
|
|
+ time.sleep(random.uniform(0.2, 0.6))
|
|
|
+ self.keyboard.type_text(username, humanize=True)
|
|
|
|
|
|
- var p = document.getElementById('password-input-field');
|
|
|
- if(p) {{ p.value = "{password}"; p.dispatchEvent(new Event('input', {{bubbles:true}})); }}
|
|
|
-
|
|
|
- var g = document.getElementById('g-recaptcha-response');
|
|
|
- if(g) {{ g.value = "{g_token}"; }}
|
|
|
+ time.sleep(random.uniform(0.5, 1.2))
|
|
|
+
|
|
|
+ pwd_ele = self.page.ele('#password-input-field')
|
|
|
+ self.mouse.human_click_ele(pwd_ele)
|
|
|
+ time.sleep(random.uniform(0.2, 0.6))
|
|
|
+ self.keyboard.type_text(password, humanize=True)
|
|
|
|
|
|
- var btn = document.getElementById('btn-login');
|
|
|
- if(btn) {{ btn.click(); return true; }} else {{ return false; }}
|
|
|
- """
|
|
|
+ if recaptchav2_token:
|
|
|
+ inject_recaptchav2_token_js = f"""
|
|
|
+ var g = document.getElementById('g-recaptcha-response');
|
|
|
+ if(g) {{ g.value = "{recaptchav2_token}"; }}
|
|
|
+ """
|
|
|
+ self._log("Inject ReCaptchaV2 Token via JS...")
|
|
|
+ self.page.run_js(inject_recaptchav2_token_js)
|
|
|
+ time.sleep(random.uniform(0.5, 1.0))
|
|
|
|
|
|
- self._log("Submitting Login via JS...")
|
|
|
- if not self.page.run_js(js_login):
|
|
|
- raise BizLogicError("Login button missing")
|
|
|
+ self._log("Submitting Login...")
|
|
|
+ time.sleep(random.uniform(0.3, 0.8))
|
|
|
+ login_btn = self.page.ele('#btn-login')
|
|
|
+ self.mouse.human_click_ele(login_btn)
|
|
|
|
|
|
self._log("Waiting for redirect...")
|
|
|
self.page.wait.url_change('login-actions', exclude=True, timeout=45)
|
|
|
|
|
|
time.sleep(3)
|
|
|
if "login-actions" in self.page.url or "auth" in self.page.url:
|
|
|
- err = "Unknown Login Error"
|
|
|
- if "Invalid username" in self.page.html:
|
|
|
- err = "Invalid Credentials"
|
|
|
self._save_screenshot("login_submit_fail")
|
|
|
- raise BizLogicError(f"Login Failed: {err}")
|
|
|
+ raise BizLogicError(message="Login Failed! Invalid credentials or Captcha rejected.")
|
|
|
|
|
|
- self._log("Waiting for dashboard...")
|
|
|
- btn_selector = 'xpath://button[.//span[@data-testid="btn-create-new-travel-group"]]'
|
|
|
- if not self.page.wait.ele_displayed(btn_selector, timeout=10):
|
|
|
- raise BizLogicError(message=f"Waiting for selector={btn_selector} timeout")
|
|
|
- html_content = self.page.html
|
|
|
- groups = self._parse_travel_groups(html_content)
|
|
|
- target_city = apt_config['city'].lower()
|
|
|
+ self.page.wait.load_start()
|
|
|
+ time.sleep(5)
|
|
|
+
|
|
|
+ groups = self._parse_travel_groups(self.page.html)
|
|
|
+ location = self.free_config.get('location')
|
|
|
for g in groups:
|
|
|
- if g['location'].lower() == target_city:
|
|
|
+ if g['location'] == location:
|
|
|
self.travel_group = g
|
|
|
break
|
|
|
|
|
|
if not self.travel_group:
|
|
|
self._save_screenshot("group_not_found")
|
|
|
- raise NotFoundError(f"Group not found for {target_city}")
|
|
|
+ raise NotFoundError(f"Group not found for {location}")
|
|
|
|
|
|
- formgroup_id = self.travel_group.get('group_number')
|
|
|
- btn_selector = f'tag:button@@name=formGroupId@@value={formgroup_id}'
|
|
|
- self._log(f"Select group_id={formgroup_id} via JS...")
|
|
|
- self.page.ele(btn_selector).click(by_js=True)
|
|
|
+ formgroup_id = self.travel_group.get('group_number')
|
|
|
+
|
|
|
+ btn_selector = f'tag:button@@name=formGroupId@@value={formgroup_id}'
|
|
|
+ self._log(f"Waiting for visible button to render: {formgroup_id}...")
|
|
|
+
|
|
|
+ self.page.wait.eles_loaded(btn_selector, timeout=15)
|
|
|
+
|
|
|
+ buttons = self.page.eles(btn_selector)
|
|
|
+ select_btn = None
|
|
|
+ for btn in reversed(buttons):
|
|
|
+ try:
|
|
|
+ w, h = btn.rect.size
|
|
|
+ if w > 0 and h > 0:
|
|
|
+ select_btn = btn
|
|
|
+ break
|
|
|
+ except Exception:
|
|
|
+ continue
|
|
|
+ if not select_btn:
|
|
|
+ self._save_screenshot("visible_button_not_found")
|
|
|
+ raise BizLogicError(f"Can't find any visible Select button for group {formgroup_id}")
|
|
|
+ time.sleep(random.uniform(0.5, 1.2))
|
|
|
+ self.mouse.human_click_ele(select_btn)
|
|
|
|
|
|
self._log("Waiting for url redirect...")
|
|
|
self.page.wait.url_change('travel-groups', exclude=True, timeout=45)
|
|
|
@@ -291,7 +299,7 @@ class TlsPlugin(IVSPlg):
|
|
|
self._log(f"Waiting for selector={btn_selector} to render...")
|
|
|
if not self.page.wait.ele_displayed(btn_selector, timeout=15):
|
|
|
raise BizLogicError(message=f"Waiting for selector={btn_selector} timeout")
|
|
|
- self.page.ele(btn_selector).click(by_js=True)
|
|
|
+ self.mouse.human_click_ele(self.page.ele(btn_selector))
|
|
|
|
|
|
self._log("Waiting for url redirect...")
|
|
|
self.page.wait.url_change('service-level', exclude=True, timeout=45)
|
|
|
@@ -309,6 +317,7 @@ class TlsPlugin(IVSPlg):
|
|
|
|
|
|
except Exception as e:
|
|
|
self._log(f"Session Create Error: {e}")
|
|
|
+ time.sleep(3600)
|
|
|
self.cleanup()
|
|
|
raise e
|
|
|
|
|
|
@@ -316,7 +325,6 @@ class TlsPlugin(IVSPlg):
|
|
|
res = VSQueryResult()
|
|
|
res.success = False
|
|
|
group_num = self.travel_group['group_number']
|
|
|
- apt_config = self.free_config.get('apt_config', {})
|
|
|
interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
|
|
|
|
|
|
target_date_obj = datetime.strptime(interest_month, "%m-%Y")
|
|
|
@@ -398,16 +406,9 @@ class TlsPlugin(IVSPlg):
|
|
|
|
|
|
else:
|
|
|
self._log(f"Already on '{target_month_text}'. Executing silent JS fetch...")
|
|
|
- url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
|
|
|
- params = {
|
|
|
- 'location': apt_config["code"],
|
|
|
- 'month': interest_month
|
|
|
- }
|
|
|
-
|
|
|
- resp = self._perform_request("GET", url, params=params, retry_count=1)
|
|
|
- html_content = resp.text
|
|
|
- self._check_page_is_session_expired_or_invalid('Book your appointment', html_content)
|
|
|
- all_slots = self._parse_appointment_slots(html_content)
|
|
|
+ resp = self._perform_request("GET", self.page.url, retry_count=1)
|
|
|
+ self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
|
|
|
+ all_slots = self._parse_appointment_slots(resp.text)
|
|
|
|
|
|
target_labels = self.free_config.get("target_labels", ["", "pta"])
|
|
|
slots = [s for s in all_slots if s.get("label") in target_labels]
|