|
|
@@ -8,7 +8,7 @@ import urllib.parse
|
|
|
from datetime import datetime
|
|
|
from typing import Dict, Any, Optional, List, Tuple
|
|
|
|
|
|
-from curl_cffi import requests
|
|
|
+from curl_cffi import requests, const
|
|
|
# 加密库
|
|
|
from cryptography.hazmat.primitives import serialization, hashes
|
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
|
@@ -18,7 +18,6 @@ from vs_plg import IVSPlg
|
|
|
from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
|
|
|
from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN
|
|
|
from toolkit.vs_cloud_api import VSCloudApi
|
|
|
-from toolkit.rule_engine import RuleEngine
|
|
|
|
|
|
# ----------------- 静态常量与辅助数据 -----------------
|
|
|
|
|
|
@@ -52,17 +51,18 @@ def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
|
|
|
return f"{local_part}@{new_domain}"
|
|
|
|
|
|
|
|
|
-# ----------------- VfsPlugin 类 -----------------
|
|
|
+def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%d"):
|
|
|
+ # 转换日期到YYYY-MM-DD 固定格式
|
|
|
+ dt = datetime.strptime(data_str, date_str_format)
|
|
|
+ return dt.strftime("%Y-%m-%d")
|
|
|
|
|
|
class VfsPlugin(IVSPlg):
|
|
|
def __init__(self, group_id: str):
|
|
|
self.group_id = group_id
|
|
|
self.config: Optional[VSPlgConfig] = None
|
|
|
self.free_config: Dict[str, Any] = {}
|
|
|
-
|
|
|
- self.session = requests.Session()
|
|
|
- # 模拟 Chrome 124
|
|
|
- self.session.impersonate = "chrome124"
|
|
|
+
|
|
|
+ self.session: Optional[requests.Session] = None
|
|
|
|
|
|
self.jwt_token = ""
|
|
|
self.user_agent = ""
|
|
|
@@ -88,23 +88,28 @@ class VfsPlugin(IVSPlg):
|
|
|
self.free_config = json.loads(config.free_config) if config.free_config else {}
|
|
|
except:
|
|
|
self.free_config = {}
|
|
|
-
|
|
|
- # 设置代理
|
|
|
- if config.proxy.ip:
|
|
|
- proxy_str = f"{config.proxy.scheme}://"
|
|
|
- if config.proxy.username:
|
|
|
- proxy_str += f"{config.proxy.username}:{config.proxy.password}@"
|
|
|
- proxy_str += f"{config.proxy.ip}:{config.proxy.port}"
|
|
|
- self.session.proxies = {"http": proxy_str, "https": proxy_str}
|
|
|
- VSC_DEBUG("vfs_plg", "[%s] Proxy set: %s", self.group_id, config.proxy.ip)
|
|
|
|
|
|
def health_check(self) -> bool:
|
|
|
- # 直接利用 VSError 的逻辑
|
|
|
return self.is_healthy
|
|
|
|
|
|
def create_session(self) -> None:
|
|
|
- """登录流程"""
|
|
|
- VSC_INFO("vfs_plg", "[%s] Starting login...", self.group_id)
|
|
|
+ # 初始化 Session
|
|
|
+ curlopt = {
|
|
|
+ const.CurlOpt.MAXAGE_CONN: 1800,
|
|
|
+ const.CurlOpt.MAXLIFETIME_CONN: 1800,
|
|
|
+ const.CurlOpt.VERBOSE: False,
|
|
|
+ }
|
|
|
+
|
|
|
+ self.session = requests.Session(
|
|
|
+ proxy=self._get_proxy_url(),
|
|
|
+ impersonate="chrome124",
|
|
|
+ curl_options=curlopt,
|
|
|
+ use_thread_local_curl=False,
|
|
|
+ http_version=const.CurlHttpVersion.V2TLS
|
|
|
+ )
|
|
|
+
|
|
|
+ # 获取真实IP
|
|
|
+ self.real_ip = self._get_realnetwork_ip()
|
|
|
|
|
|
# 1. Cloudflare Turnstile
|
|
|
cf_token = self._handle_cloudflare_challenge()
|
|
|
@@ -114,8 +119,8 @@ class VfsPlugin(IVSPlg):
|
|
|
password = self.config.account.password
|
|
|
enc_password = self._encrypt_password(password)
|
|
|
|
|
|
- mission_code = self.free_config.get("missionCode", "")
|
|
|
- country_code = self.free_config.get("countryCode", "")
|
|
|
+ mission_code = self.free_config.get("mission_code", "")
|
|
|
+ country_code = self.free_config.get("country_code", "")
|
|
|
|
|
|
client_src = self._get_client_source()
|
|
|
orange_src = self._get_orange_source(email)
|
|
|
@@ -141,7 +146,7 @@ class VfsPlugin(IVSPlg):
|
|
|
# 3. 发送登录请求 (包含 OPTIONS)
|
|
|
resp = self._perform_request("POST", url, headers=headers, data=data)
|
|
|
resp_json = resp.json()
|
|
|
- if "accessToken" in resp_json and resp_json["accessToken"]:
|
|
|
+ if resp_json.get('accessToken', ''):
|
|
|
self.jwt_token = resp_json["accessToken"]
|
|
|
VSC_INFO("vfs_plg", "[%s] Login successful, JWT obtained.", self.group_id)
|
|
|
return
|
|
|
@@ -157,17 +162,18 @@ class VfsPlugin(IVSPlg):
|
|
|
def query(self) -> VSQueryResult:
|
|
|
"""查询可预约 Slot"""
|
|
|
result = VSQueryResult()
|
|
|
- appt_types = self.free_config.get("appointmentType", [])
|
|
|
+ appt_types = self.free_config.get("appointment_types", [])
|
|
|
if not appt_types:
|
|
|
raise NotFoundError(message="No matching appointment configuration found.")
|
|
|
apt_config = random.choice(appt_types)
|
|
|
self._fetch_configurations(apt_config)
|
|
|
earliest_date = self._query_earliest_slot(apt_config)
|
|
|
result.success = False
|
|
|
- result.visa_type = apt_config.get("subcategoryCode", "")
|
|
|
- result.city = apt_config.get("vacCode", "")
|
|
|
- result.country = self.free_config.get("countryCode", "")
|
|
|
- result.routing_key = apt_config.get("routingKey", "")
|
|
|
+ result.availability_status = AvailabilityStatus.NoneAvailable
|
|
|
+ result.visa_type = apt_config.get("visa_type", "")
|
|
|
+ result.city = apt_config.get("city", "")
|
|
|
+ result.country = apt_config.get("country", "")
|
|
|
+ result.routing_key = apt_config.get("routing_key", "")
|
|
|
if earliest_date:
|
|
|
if "WaitList" in earliest_date:
|
|
|
result.success = True
|
|
|
@@ -177,65 +183,30 @@ class VfsPlugin(IVSPlg):
|
|
|
result.success = True
|
|
|
result.availability_status = AvailabilityStatus.Available
|
|
|
result.earliest_date = earliest_date
|
|
|
+
|
|
|
VSC_INFO("vfs_plg", "[%s] Found Slot: %s", self.group_id, earliest_date)
|
|
|
|
|
|
day_info = VSQueryResult.DateAvailability()
|
|
|
day_info.date = earliest_date
|
|
|
result.availability.append(day_info)
|
|
|
return result
|
|
|
-
|
|
|
- def _get_filtered_covered_months(self, start_date, end_date, from_date) -> List[str]:
|
|
|
- """
|
|
|
- 计算需要查询的月份列表,格式 YYYY-MM-DD (每月1号)
|
|
|
- """
|
|
|
- fmt = "%Y-%m-%d"
|
|
|
- # 默认值处理
|
|
|
- try:
|
|
|
- dt_start = datetime.strptime(start_date, fmt) if start_date else datetime.now()
|
|
|
- dt_end = datetime.strptime(end_date, fmt) if end_date else datetime.now().replace(year=datetime.now().year + 1)
|
|
|
-
|
|
|
- # from_date 格式可能是 DD/MM/YYYY (从 slot_info 来)
|
|
|
- try:
|
|
|
- dt_from = datetime.strptime(from_date, "%d/%m/%Y")
|
|
|
- except:
|
|
|
- dt_from = datetime.now()
|
|
|
- except:
|
|
|
- return []
|
|
|
-
|
|
|
- # 归一化到月初
|
|
|
- dt_start = dt_start.replace(day=1)
|
|
|
- dt_end = dt_end.replace(day=1)
|
|
|
- dt_from = dt_from.replace(day=1)
|
|
|
-
|
|
|
- # 起始点取 max(start, from)
|
|
|
- curr = max(dt_start, dt_from)
|
|
|
-
|
|
|
- months = []
|
|
|
- while curr <= dt_end:
|
|
|
- months.append(curr.strftime(fmt))
|
|
|
- # 下个月
|
|
|
- if curr.month == 12:
|
|
|
- curr = curr.replace(year=curr.year + 1, month=1)
|
|
|
- else:
|
|
|
- curr = curr.replace(month=curr.month + 1)
|
|
|
- return months
|
|
|
|
|
|
def book(self, slot_info: VSQueryResult, user_inputs) -> VSBookResult:
|
|
|
"""
|
|
|
执行完整的预约流程,包含:上传文档 -> 添加申请人 -> OTP -> 选时间 -> 锁定 -> 支付
|
|
|
"""
|
|
|
- user_email = user_inputs.get('email', 'get_visa_666@example.com')
|
|
|
+ user_email = user_inputs.get('email')
|
|
|
user_inputs['alias_email'] = get_alias_email(user_email, new_domain="gmail-app.com")
|
|
|
|
|
|
res = VSBookResult()
|
|
|
slot_routing_key = slot_info.routing_key
|
|
|
|
|
|
- from_date = slot_info.earliest_date if slot_info.earliest_date else datetime.now().strftime("%d/%m/%Y")
|
|
|
+ from_date = slot_info.earliest_date if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
|
|
|
|
|
|
apt_config = None
|
|
|
- appt_types = self.free_config.get("appointmentType", [])
|
|
|
+ appt_types = self.free_config.get("appointment_types", [])
|
|
|
for apt in appt_types:
|
|
|
- if apt.get("routingKey") == slot_routing_key or len(appt_types) == 1:
|
|
|
+ if apt.get("routing_key") == slot_routing_key:
|
|
|
apt_config = apt
|
|
|
break
|
|
|
|
|
|
@@ -244,7 +215,7 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
self._fetch_configurations(apt_config)
|
|
|
|
|
|
- sub_cc = apt_config.get("subcategoryCode")
|
|
|
+ sub_cc = apt_config.get("subcategory_code")
|
|
|
sub_conf = self.subcategory_conf.get(sub_cc, {})
|
|
|
|
|
|
# OCR 识别 / 文档上传
|
|
|
@@ -265,20 +236,20 @@ class VfsPlugin(IVSPlg):
|
|
|
is_waitlist = (slot_info.availability_status == AvailabilityStatus.Waitlist)
|
|
|
|
|
|
add_primary_retry = 0
|
|
|
- MAX_RETRY = 3
|
|
|
- success_add = False
|
|
|
+ MAX_RETRY = 6
|
|
|
|
|
|
while add_primary_retry < MAX_RETRY:
|
|
|
try:
|
|
|
final_urn = self._add_primary_applicant(apt_config, user_inputs, is_waitlist, ocr_enabled, enable_reference_number)
|
|
|
- success_add = True
|
|
|
+ if not final_urn:
|
|
|
+ raise NotFoundError(message="URN not found")
|
|
|
break
|
|
|
except Exception as e:
|
|
|
VSC_WARN("vfs_plg", "[%s] Add Applicant retry %d...", self.group_id, add_primary_retry)
|
|
|
- time.sleep(5)
|
|
|
+ time.sleep(10)
|
|
|
add_primary_retry += 1
|
|
|
|
|
|
- if not success_add:
|
|
|
+ if not final_urn:
|
|
|
raise BizLogicError(message="Failed to add primary applicant (Slot likely taken)")
|
|
|
|
|
|
VSC_INFO("vfs_plg", "[%s] Applicant Added. URN: %s", self.group_id, final_urn)
|
|
|
@@ -305,13 +276,8 @@ class VfsPlugin(IVSPlg):
|
|
|
raise BizLogicError(message='confirm waitlist failed')
|
|
|
|
|
|
# 规则引擎与日期筛选 (核心步骤 3)
|
|
|
- rules_str = user_inputs.get("rules", "")
|
|
|
- rule_engine = RuleEngine(rules_str)
|
|
|
-
|
|
|
expected_start = user_inputs.get("expected_start_date", "")
|
|
|
expected_end = user_inputs.get("expected_end_date", "")
|
|
|
- rule_engine.set_date_range_start(expected_start)
|
|
|
- rule_engine.set_date_range_end(expected_end)
|
|
|
|
|
|
# 计算需要扫描的月份, 如果 expected_start/end 为空,默认使用 from_date 所在月
|
|
|
months = self._get_filtered_covered_months(expected_start, expected_end, from_date)
|
|
|
@@ -329,11 +295,7 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
# 遍历月份寻找 Slot
|
|
|
for m_str in months:
|
|
|
- # 需要 DD/MM/YYYY
|
|
|
- dt_m = datetime.strptime(m_str, "%Y-%m-%d")
|
|
|
- converted_date = dt_m.strftime("%d/%m/%Y")
|
|
|
-
|
|
|
- ads = not self._query_slot_calendar(apt_config, final_urn, converted_date)
|
|
|
+ ads = self._query_slot_calendar(apt_config, final_urn, m_str)
|
|
|
|
|
|
# 过滤已知的 slots
|
|
|
new_ads = [d for d in ads if d not in all_ads]
|
|
|
@@ -345,7 +307,8 @@ class VfsPlugin(IVSPlg):
|
|
|
avail_candidates = [d for d in list(all_ads) if d not in forbidden_dates]
|
|
|
|
|
|
# 规则筛选
|
|
|
- sel_dates = rule_engine.select_date(avail_candidates, "%d/%m/%Y")
|
|
|
+ sel_dates = self._filter_dates(avail_candidates, expected_start, expected_end)
|
|
|
+ print(f'avail_candidates={avail_candidates}, sel_dates={sel_dates}')
|
|
|
if not sel_dates:
|
|
|
break
|
|
|
|
|
|
@@ -378,7 +341,7 @@ class VfsPlugin(IVSPlg):
|
|
|
break
|
|
|
|
|
|
if not found_slot:
|
|
|
- VSC_INFO("vfs_plg", "[%s] No valid slots found after Rule Engine filtering.", self.group_id)
|
|
|
+ VSC_INFO("vfs_plg", "[%s] No valid slots found.", self.group_id)
|
|
|
res.success = False
|
|
|
return res
|
|
|
|
|
|
@@ -391,7 +354,7 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
schedule_res = self._schedule(apt_config, final_urn, amount, currency, selected_slot_id)
|
|
|
|
|
|
- if schedule_res.get("IsAppointmentBooked"):
|
|
|
+ if not schedule_res.get("IsAppointmentBooked"):
|
|
|
VSC_INFO("vfs_plg", "[%s] IsAppointmentBooked is false", self.group_id)
|
|
|
res.success = False
|
|
|
return res
|
|
|
@@ -417,6 +380,61 @@ class VfsPlugin(IVSPlg):
|
|
|
res.session_id = saved_session['session_id']
|
|
|
return res
|
|
|
|
|
|
+ def _get_proxy_url(self):
|
|
|
+ # 构造代理
|
|
|
+ proxy_url = ""
|
|
|
+ if self.config.proxy.ip:
|
|
|
+ s = self.config.proxy
|
|
|
+ if s.username:
|
|
|
+ proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
|
|
|
+ else:
|
|
|
+ proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
|
|
|
+ return proxy_url
|
|
|
+
|
|
|
+ def _get_filtered_covered_months(self, start_date, end_date, from_date) -> List[str]:
|
|
|
+ """
|
|
|
+ 计算需要查询的月份列表,格式 YYYY-MM-DD (每月1号)
|
|
|
+ """
|
|
|
+ fmt = "%Y-%m-%d"
|
|
|
+ # 默认值处理
|
|
|
+ try:
|
|
|
+ dt_start = datetime.strptime(start_date, fmt) if start_date else datetime.now()
|
|
|
+ dt_end = datetime.strptime(end_date, fmt) if end_date else datetime.now().replace(year=datetime.now().year + 1)
|
|
|
+
|
|
|
+ try:
|
|
|
+ dt_from = datetime.strptime(from_date, fmt)
|
|
|
+ except:
|
|
|
+ dt_from = datetime.now()
|
|
|
+ except:
|
|
|
+ return []
|
|
|
+
|
|
|
+ # 归一化到月初
|
|
|
+ dt_start = dt_start.replace(day=1)
|
|
|
+ dt_end = dt_end.replace(day=1)
|
|
|
+ dt_from = dt_from.replace(day=1)
|
|
|
+
|
|
|
+ # 起始点取 max(start, from)
|
|
|
+ curr = max(dt_start, dt_from)
|
|
|
+
|
|
|
+ months = []
|
|
|
+ while curr <= dt_end:
|
|
|
+ months.append(curr.strftime(fmt))
|
|
|
+ # 下个月
|
|
|
+ if curr.month == 12:
|
|
|
+ curr = curr.replace(year=curr.year + 1, month=1)
|
|
|
+ else:
|
|
|
+ curr = curr.replace(month=curr.month + 1)
|
|
|
+ return months
|
|
|
+
|
|
|
+ def _get_realnetwork_ip(self):
|
|
|
+ url = "https://api.ipify.org/?format=json"
|
|
|
+ headers = {
|
|
|
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
|
+ 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
|
|
+ }
|
|
|
+ resp = self._perform_request('GET', url, headers=headers)
|
|
|
+ return resp.json()['ip']
|
|
|
+
|
|
|
def _confirm_waitlist(self, apt_config: Dict[str, Any], urn: str) -> bool:
|
|
|
"""
|
|
|
确认加入候补名单 (对应 C++ VFSApi::confirm_waitlist)
|
|
|
@@ -426,9 +444,9 @@ class VfsPlugin(IVSPlg):
|
|
|
headers["content-type"] = "application/json;charset=UTF-8"
|
|
|
|
|
|
data = {
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"loginUser": self.config.account.username,
|
|
|
"urn": urn,
|
|
|
"notificationType": "none",
|
|
|
@@ -454,20 +472,18 @@ class VfsPlugin(IVSPlg):
|
|
|
headers["content-type"] = "application/json;charset=UTF-8"
|
|
|
|
|
|
data = {
|
|
|
- "missioncode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missioncode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"loginUser": self.config.account.username,
|
|
|
"languageCode": "en-US",
|
|
|
- "visaCategoryCode": apt_config.get("subcategoryCode"),
|
|
|
+ "visaCategoryCode": apt_config.get("subcategory_code"),
|
|
|
"fileBytes": b64_str,
|
|
|
"selfiImageFileBytes": ""
|
|
|
}
|
|
|
|
|
|
- result = {}
|
|
|
resp = self._perform_request("POST", url, headers=headers, json_data=data)
|
|
|
- j = resp.json()
|
|
|
- result.update(j)
|
|
|
+ result = resp.json()
|
|
|
result["passportImageFilename"] = "passport_img.jpg"
|
|
|
result["passportImageFileBytes"] = b64_str
|
|
|
return result
|
|
|
@@ -571,7 +587,7 @@ class VfsPlugin(IVSPlg):
|
|
|
"Retryleft": "",
|
|
|
|
|
|
# 真实 IP 注入
|
|
|
- "ipAddress": self.real_ip or "127.0.0.1"
|
|
|
+ "ipAddress": self.real_ip
|
|
|
}
|
|
|
|
|
|
# --- 处理 Reference Number (Cover Letter) ---
|
|
|
@@ -588,11 +604,11 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
# --- 构造最外层 Payload ---
|
|
|
payload = {
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"loginUser": self.config.account.username,
|
|
|
- "visaCategoryCode": apt_config.get("subcategoryCode"),
|
|
|
+ "visaCategoryCode": apt_config.get("subcategory_code"),
|
|
|
"applicantList": [applicant], # 数组形式
|
|
|
|
|
|
"languageCode": "en-US",
|
|
|
@@ -619,9 +635,9 @@ class VfsPlugin(IVSPlg):
|
|
|
data = {
|
|
|
"urn": urn,
|
|
|
"loginUser": self.config.account.username,
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"OTP": "",
|
|
|
"otpAction": "GENERATE",
|
|
|
"languageCode": "en-US"
|
|
|
@@ -633,16 +649,15 @@ class VfsPlugin(IVSPlg):
|
|
|
def _applicant_otp_verify(self, apt_config, urn, otp) -> bool:
|
|
|
url = "https://lift-api.vfsglobal.com/appointment/applicantotp"
|
|
|
headers = self._get_common_headers(with_auth=True)
|
|
|
- # C++ specific: datacenter header
|
|
|
headers["datacenter"] = "GERMANY"
|
|
|
headers["content-type"] = "application/json;charset=UTF-8"
|
|
|
|
|
|
data = {
|
|
|
"urn": urn,
|
|
|
"loginUser": self.config.account.username,
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"OTP": otp,
|
|
|
"otpAction": "VALIDATE",
|
|
|
"languageCode": "en-US"
|
|
|
@@ -655,14 +670,15 @@ class VfsPlugin(IVSPlg):
|
|
|
url = "https://lift-api.vfsglobal.com/appointment/calendar"
|
|
|
headers = self._get_common_headers(with_auth=True)
|
|
|
headers["content-type"] = "application/json;charset=UTF-8"
|
|
|
-
|
|
|
+ dt_m = datetime.strptime(from_date, "%Y-%m-%d")
|
|
|
+ converted_date = dt_m.strftime("%d/%m/%Y")
|
|
|
data = {
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"loginUser": self.config.account.username,
|
|
|
- "visaCategoryCode": apt_config.get("subcategoryCode"),
|
|
|
- "fromDate": from_date,
|
|
|
+ "visaCategoryCode": apt_config.get("subcategory_code"),
|
|
|
+ "fromDate": converted_date,
|
|
|
"urn": urn,
|
|
|
"payCode": ""
|
|
|
}
|
|
|
@@ -672,11 +688,9 @@ class VfsPlugin(IVSPlg):
|
|
|
if calendars:
|
|
|
ads_out = []
|
|
|
for item in calendars:
|
|
|
- # C++ assumes "MM/DD/YYYY" -> "DD/MM/YYYY"
|
|
|
+ # "MM/DD/YYYY" -> "YYYY-MM-DD"
|
|
|
raw = item.get("date")
|
|
|
- # Normalize to DD/MM/YYYY
|
|
|
- dObj = datetime.strptime(raw, "%m/%d/%Y")
|
|
|
- ads_out.append(dObj.strftime("%d/%m/%Y"))
|
|
|
+ ads_out.append(to_yyyymmdd(raw, "%m/%d/%Y"))
|
|
|
return ads_out
|
|
|
return []
|
|
|
|
|
|
@@ -684,14 +698,15 @@ class VfsPlugin(IVSPlg):
|
|
|
url = "https://lift-api.vfsglobal.com/appointment/timeslot"
|
|
|
headers = self._get_common_headers(with_auth=True)
|
|
|
headers["content-type"] = "application/json;charset=UTF-8"
|
|
|
-
|
|
|
+ dt_m = datetime.strptime(slot_date, "%Y-%m-%d")
|
|
|
+ converted_date = dt_m.strftime("%d/%m/%Y")
|
|
|
data = {
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"loginUser": self.config.account.username,
|
|
|
- "visaCategoryCode": apt_config.get("subcategoryCode"),
|
|
|
- "slotDate": slot_date,
|
|
|
+ "visaCategoryCode": apt_config.get("subcategory_code"),
|
|
|
+ "slotDate": converted_date,
|
|
|
"urn": urn
|
|
|
}
|
|
|
resp = self._perform_request("POST", url, headers=headers, json_data=data)
|
|
|
@@ -703,22 +718,18 @@ class VfsPlugin(IVSPlg):
|
|
|
headers["content-type"] = "application/json;charset=UTF-8"
|
|
|
|
|
|
# ISO format conversion
|
|
|
- try:
|
|
|
- dt = datetime.strptime(earliest_date, "%d/%m/%Y")
|
|
|
- iso_date = dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
- except:
|
|
|
- iso_date = earliest_date
|
|
|
+ dt = datetime.strptime(earliest_date, "%Y-%m-%d")
|
|
|
|
|
|
data = {
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"loginUser": self.config.account.username,
|
|
|
"urn": urn,
|
|
|
- "firstEarliestSlotDate": earliest_date,
|
|
|
+ "firstEarliestSlotDate": dt.strftime("%d/%m/%Y"),
|
|
|
"action": "schedule",
|
|
|
- "ipAddress": self.real_ip or "127.0.0.1",
|
|
|
- "eadAppointmentDetail": iso_date
|
|
|
+ "ipAddress": self.real_ip,
|
|
|
+ "eadAppointmentDetail": dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
}
|
|
|
resp = self._perform_request("POST", url, headers=headers, json_data=data)
|
|
|
return resp.json().get("isSavedSuccess", False)
|
|
|
@@ -729,7 +740,7 @@ class VfsPlugin(IVSPlg):
|
|
|
1. 发送 OPTIONS 请求
|
|
|
2. 发送实际请求
|
|
|
"""
|
|
|
- print(f'[perform request] {method} {url}')
|
|
|
+ print(f'[perform request] {method} {url} {data} {json_data} {params}')
|
|
|
# --- 1. 发送 OPTIONS 请求 ---
|
|
|
try:
|
|
|
# OPTIONS 请求使用相同的 URL 和 headers (部分 header 如 content-length 会被自动处理)
|
|
|
@@ -786,21 +797,16 @@ class VfsPlugin(IVSPlg):
|
|
|
"""
|
|
|
完整实现的 Cloudflare Turnstile 验证逻辑
|
|
|
"""
|
|
|
- mission = self.free_config.get("missionCode", "")
|
|
|
- country = self.free_config.get("countryCode", "")
|
|
|
+ mission = self.free_config.get("mission_code", "")
|
|
|
+ country = self.free_config.get("country_code", "")
|
|
|
if not mission or not country:
|
|
|
- raise NotFoundError(message="Missing missionCode or countryCode in free_config")
|
|
|
+ raise NotFoundError(message="Missing mission_code or country_code in free_config")
|
|
|
|
|
|
website_url = f"https://visa.vfsglobal.com/{country}/en/{mission}/login"
|
|
|
|
|
|
# 构造代理字符串传给打码平台 (格式: http://user:pass@ip:port)
|
|
|
- proxy_str = ""
|
|
|
- if self.config.proxy.ip:
|
|
|
- proxy_str = f"{self.config.proxy.scheme}://"
|
|
|
- if self.config.proxy.username:
|
|
|
- proxy_str += f"{self.config.proxy.username}:{self.config.proxy.password}@"
|
|
|
- proxy_str += f"{self.config.proxy.ip}:{self.config.proxy.port}"
|
|
|
-
|
|
|
+ proxy_str = self._get_proxy_url()
|
|
|
+
|
|
|
# 2. 提交任务
|
|
|
VSC_INFO("vfs_plg", "[%s] Submitting Turnstile task for %s...", self.group_id, website_url)
|
|
|
task_out = VSCloudApi.Instance().submit_anti_turnstile_task(proxy_str, website_url)
|
|
|
@@ -876,8 +882,8 @@ class VfsPlugin(IVSPlg):
|
|
|
raise BizLogicError(message="Captcha task timeout (120s)")
|
|
|
|
|
|
def _get_common_headers(self, with_auth=True) -> Dict[str, str]:
|
|
|
- mission = self.free_config.get("missionCode", "")
|
|
|
- country = self.free_config.get("countryCode", "")
|
|
|
+ mission = self.free_config.get("mission_code", "")
|
|
|
+ country = self.free_config.get("country_code", "")
|
|
|
lang = self.free_config.get("language", "en")
|
|
|
route = f"{country}/{lang}/{mission}"
|
|
|
|
|
|
@@ -905,10 +911,10 @@ class VfsPlugin(IVSPlg):
|
|
|
max_retries = self.free_config.get("slot_query_max_retries", 2)
|
|
|
|
|
|
data = {
|
|
|
- "missioncode": self.free_config.get("missionCode"),
|
|
|
- "countrycode": self.free_config.get("countryCode"),
|
|
|
- "vacCode": apt_config.get("vacCode"),
|
|
|
- "visaCategoryCode": apt_config.get("subcategoryCode"),
|
|
|
+ "missioncode": self.free_config.get("mission_code"),
|
|
|
+ "countrycode": self.free_config.get("country_code"),
|
|
|
+ "vacCode": apt_config.get("vac_code"),
|
|
|
+ "visaCategoryCode": apt_config.get("subcategory_code"),
|
|
|
"roleName": "Individual",
|
|
|
"loginUser": self.config.account.username,
|
|
|
"payCode": ""
|
|
|
@@ -949,26 +955,17 @@ class VfsPlugin(IVSPlg):
|
|
|
j = resp.json()
|
|
|
if j.get("earliestSlotLists"):
|
|
|
raw_date = j["earliestSlotLists"][0]["date"]
|
|
|
- dt = datetime.strptime(raw_date, "%m/%d/%Y %H:%M:%S")
|
|
|
- return dt.strftime("%m/%d/%Y")
|
|
|
-
|
|
|
+ return to_yyyymmdd(raw_date, "%m/%d/%Y %H:%M:%S")
|
|
|
return ""
|
|
|
|
|
|
-
|
|
|
- def _fmt_date(self, yyyy_mm_dd):
|
|
|
- try:
|
|
|
- return datetime.strptime(yyyy_mm_dd, "%Y-%m-%d").strftime("%d/%m/%Y")
|
|
|
- except:
|
|
|
- return yyyy_mm_dd
|
|
|
-
|
|
|
def _fetch_configurations(self, apt_config: Dict[str, Any]):
|
|
|
# 1. 获取所有中心配置 (query_center)
|
|
|
if not self.center_conf:
|
|
|
self.center_conf = self._query_center()
|
|
|
|
|
|
# 2. 获取 Visa Category 配置
|
|
|
- vac_code = apt_config.get("vacCode")
|
|
|
- category_code = apt_config.get("categoryCode")
|
|
|
+ vac_code = apt_config.get("vac_code")
|
|
|
+ category_code = apt_config.get("category_code")
|
|
|
|
|
|
# 检查目标 category_code 是否已在缓存中
|
|
|
if category_code not in self.category_conf:
|
|
|
@@ -987,7 +984,7 @@ class VfsPlugin(IVSPlg):
|
|
|
raise NotFoundError(message=f"{self.group_id} Category code {category_code} not found in VAC {vac_code}")
|
|
|
|
|
|
# 3. 获取 Visa SubCategory 配置
|
|
|
- sub_category_code = apt_config.get("subcategoryCode")
|
|
|
+ sub_category_code = apt_config.get("subcategory_code")
|
|
|
if sub_category_code not in self.subcategory_conf:
|
|
|
visa_subcategories = self._query_visa_sub_category(vac_code, category_code)
|
|
|
|
|
|
@@ -1002,16 +999,16 @@ class VfsPlugin(IVSPlg):
|
|
|
raise NotFoundError(message=f"{self.group_id} SubCategory code {sub_category_code} not found")
|
|
|
|
|
|
def _query_center(self) -> List:
|
|
|
- mission = self.free_config.get("missionCode")
|
|
|
- country = self.free_config.get("countryCode")
|
|
|
+ mission = self.free_config.get("mission_code")
|
|
|
+ country = self.free_config.get("country_code")
|
|
|
url = f"https://lift-api.vfsglobal.com/master/center/{mission}/{country}/en-US"
|
|
|
headers = self._get_common_headers(with_auth=False)
|
|
|
resp = self._perform_request("GET", url, headers=headers)
|
|
|
return resp.json()
|
|
|
|
|
|
def _query_visa_category(self, center_code: str) -> List:
|
|
|
- mission = self.free_config.get("missionCode")
|
|
|
- country = self.free_config.get("countryCode")
|
|
|
+ mission = self.free_config.get("mission_code")
|
|
|
+ country = self.free_config.get("country_code")
|
|
|
enc_center = urllib.parse.quote(center_code)
|
|
|
url = f"https://lift-api.vfsglobal.com/master/visacategory/{mission}/{country}/{enc_center}/en-US"
|
|
|
headers = self._get_common_headers(with_auth=False)
|
|
|
@@ -1020,8 +1017,8 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
|
|
|
def _query_visa_sub_category(self, center_code: str, category_code: str) -> List:
|
|
|
- mission = self.free_config.get("missionCode")
|
|
|
- country = self.free_config.get("countryCode")
|
|
|
+ mission = self.free_config.get("mission_code")
|
|
|
+ country = self.free_config.get("country_code")
|
|
|
enc_center = urllib.parse.quote(center_code)
|
|
|
enc_cat = urllib.parse.quote(category_code)
|
|
|
url = f"https://lift-api.vfsglobal.com/master/subvisacategory/{mission}/{country}/{enc_center}/{enc_cat}/en-US"
|
|
|
@@ -1077,8 +1074,8 @@ class VfsPlugin(IVSPlg):
|
|
|
password = self.config.account.password
|
|
|
enc_password = self._encrypt_password(password)
|
|
|
|
|
|
- mission_code = self.free_config.get("missionCode", "")
|
|
|
- country_code = self.free_config.get("countryCode", "")
|
|
|
+ mission_code = self.free_config.get("mission_code", "")
|
|
|
+ country_code = self.free_config.get("country_code", "")
|
|
|
|
|
|
# 2. 生成加密 Source (每次请求时间戳不同,建议重新生成)
|
|
|
client_src = self._get_client_source()
|
|
|
@@ -1124,8 +1121,8 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
data = {
|
|
|
"loginUser": self.config.account.username,
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
"urn": urn,
|
|
|
"applicants": []
|
|
|
}
|
|
|
@@ -1138,9 +1135,9 @@ class VfsPlugin(IVSPlg):
|
|
|
headers["content-type"] = "application/json;charset=UTF-8"
|
|
|
|
|
|
data = {
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"loginUser": self.config.account.username,
|
|
|
"urn": urn,
|
|
|
"languageCode": "en-US"
|
|
|
@@ -1148,7 +1145,7 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
resp = self._perform_request("POST", url, headers=headers, json_data=data)
|
|
|
j = resp.json()
|
|
|
- return float(j.get("totalamount", 0.0)), j["feeDetails"][0].get("currency", "")
|
|
|
+ return j.get("totalamount"), j["feeDetails"][0].get("currency")
|
|
|
|
|
|
|
|
|
def _schedule(self, apt_config, urn, amount, currency, slot_id) -> Dict:
|
|
|
@@ -1157,9 +1154,9 @@ class VfsPlugin(IVSPlg):
|
|
|
headers["content-type"] = "application/json;charset=UTF-8"
|
|
|
|
|
|
data = {
|
|
|
- "missionCode": self.free_config.get("missionCode"),
|
|
|
- "countryCode": self.free_config.get("countryCode"),
|
|
|
- "centerCode": apt_config.get("vacCode"),
|
|
|
+ "missionCode": self.free_config.get("mission_code"),
|
|
|
+ "countryCode": self.free_config.get("country_code"),
|
|
|
+ "centerCode": apt_config.get("vac_code"),
|
|
|
"loginUser": self.config.account.username,
|
|
|
"urn": urn,
|
|
|
"notificationType": "none",
|
|
|
@@ -1168,10 +1165,10 @@ class VfsPlugin(IVSPlg):
|
|
|
"RequestRefNo": "",
|
|
|
"clientId": "",
|
|
|
"merchantId": "",
|
|
|
- "amount": amount,
|
|
|
+ "amount": amount,
|
|
|
"currency": currency
|
|
|
},
|
|
|
- "allocationId": slot_id,
|
|
|
+ "allocationId": str(slot_id),
|
|
|
"CanVFSReachoutToApplicant": True
|
|
|
}
|
|
|
|
|
|
@@ -1198,6 +1195,31 @@ class VfsPlugin(IVSPlg):
|
|
|
else:
|
|
|
raise NotFoundError(message='payment link not found')
|
|
|
|
|
|
+ def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
|
|
|
+ """
|
|
|
+ 根据用户的期望范围筛选可用日期
|
|
|
+
|
|
|
+ :param dates: API 返回的可用日期列表 (YYYY-MM-DD)
|
|
|
+ :param start_str: 用户期望开始日期 (YYYY-MM-DD)
|
|
|
+ :param end_str: 用户期望结束日期 (YYYY-MM-DD)
|
|
|
+ :return: 符合要求的日期列表
|
|
|
+ """
|
|
|
+ # 如果没有设置范围,则不过滤,返回所有日期
|
|
|
+ if not start_str or not end_str:
|
|
|
+ return dates
|
|
|
+
|
|
|
+ valid_dates = []
|
|
|
+ # 截取前10位以防带有时分秒
|
|
|
+ s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
|
|
|
+ e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
|
|
|
+
|
|
|
+ for date_str in dates:
|
|
|
+ curr_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
|
+ # 比较范围 (闭区间)
|
|
|
+ if s_date <= curr_date <= e_date:
|
|
|
+ valid_dates.append(date_str)
|
|
|
+ random.shuffle(valid_dates)
|
|
|
+ return valid_dates
|
|
|
|
|
|
def _save_http_session(self, page_url):
|
|
|
"""
|