vs_cloud_api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # toolkit/vs_cloud_api.py
  2. import requests
  3. import json
  4. import time
  5. import urllib.parse
  6. from typing import Dict, Any, Optional
  7. from vs_log_macros import VSC_ERROR, VSC_INFO, VSC_DEBUG # type: ignore
  8. class VSCloudApi:
  9. """
  10. @brief VSCloudApi 的 Python 实现 (1:1 对应 C++ 版本)
  11. 用于对接 http://45.137.220.138:8888 的云端服务
  12. """
  13. _instance = None
  14. def __new__(cls, *args, **kwargs):
  15. if cls._instance is None:
  16. cls._instance = super(VSCloudApi, cls).__new__(cls)
  17. # 初始化默认配置,对应 C++ 构造函数参数
  18. cls._instance.base_url = "http://45.137.220.138:8000"
  19. cls._instance.api_token = "7x9EjFpmv7GjZc6AfVeqxuUBANpqkpkHAtxJM7CAW5oZhs0nEyCJBy39N4XXs5hgfYWXw3jFrcgXqQ42HAx9Qvwtk9vC2GvKBbWz"
  20. cls._instance.session = requests.Session()
  21. return cls._instance
  22. @staticmethod
  23. def Instance():
  24. return VSCloudApi()
  25. def _get_headers(self, content_type: str = "application/json") -> Dict[str, str]:
  26. return {
  27. "Authorization": self.api_token,
  28. "Content-Type": content_type,
  29. "Accept": "application/json, text/plain, */*"
  30. }
  31. def _handle_request(self, method: str, endpoint: str, **kwargs) -> Optional[Any]:
  32. url = f"{self.base_url}{endpoint}"
  33. try:
  34. resp = self.session.request(method, url, timeout=30, **kwargs)
  35. if resp.status_code == 200:
  36. try:
  37. return resp.json()
  38. except json.JSONDecodeError:
  39. # 部分接口可能返回纯文本或空,视情况而定
  40. return resp.text
  41. else:
  42. VSC_ERROR("vs_cloud", f"Request failed: {method} {url} [{resp.status_code}] {resp.text}")
  43. except Exception as e:
  44. VSC_ERROR("vs_cloud", f"Request exception: {method} {url} - {str(e)}")
  45. return None
  46. def query_user_pending(self, tech_provider: str, pending_users_out: Dict[str, Any]) -> bool:
  47. """查询待处理用户"""
  48. params = {"tech_provider": tech_provider}
  49. headers = self._get_headers()
  50. # C++: headers = curl_slist_append(headers, auth.c_str());
  51. result = self._handle_request("GET", "/api/autobooking/pending", params=params, headers=headers)
  52. if result is not None:
  53. pending_users_out.update(result)
  54. return True
  55. return False
  56. def query_user_info(self, uid: str, userinfo_out: Dict[str, Any]) -> bool:
  57. """查询 VFS 用户信息"""
  58. headers = self._get_headers()
  59. result = self._handle_request("GET", f"/api/autobooking/{uid}", headers=headers)
  60. if result is not None:
  61. userinfo_out.update(result)
  62. return True
  63. return False
  64. def update_user_info(self, uid: str, uinfo: Dict[str, Any], updated_userinfo_out: Dict[str, Any]) -> bool:
  65. """更新 VFS 用户信息"""
  66. headers = self._get_headers()
  67. # C++: if (!uinfo.is_null()) put_data = uinfo.dump();
  68. result = self._handle_request("PUT", f"/api/autobooking/{uid}", headers=headers, json=uinfo)
  69. if result is not None:
  70. updated_userinfo_out.update(result)
  71. return True
  72. return False
  73. def submit_anti_turnstile_task(self, proxy: str, website_url: str, task_out: Dict[str, Any]) -> bool:
  74. """
  75. 提交反 Turnstile 任务
  76. 注意 C++ 逻辑:args 字段是 JSON dump 后的字符串
  77. """
  78. headers = self._get_headers()
  79. args = {
  80. "proxy": proxy,
  81. "websiteUrl": website_url
  82. }
  83. payload = {
  84. "command": "AntiCloudflareTurnstileTask",
  85. "args": json.dumps(args), # C++: data["args"] = args.dump();
  86. "status": 0
  87. }
  88. result = self._handle_request("POST", "/api/tasks", headers=headers, json=payload)
  89. if result is not None:
  90. task_out.update(result)
  91. return True
  92. return False
  93. def get_anti_turnstile_result(self, task_id: str, result_out: Dict[str, Any]) -> bool:
  94. """获取反 Turnstile 结果"""
  95. headers = self._get_headers()
  96. result = self._handle_request("GET", f"/api/tasks/{task_id}", headers=headers)
  97. if result is not None:
  98. result_out.update(result)
  99. return True
  100. return False
  101. def submit_anticloudflare_task(self, proxy: str, website_url: str) -> Optional[Dict]:
  102. """
  103. 提交 AntiCloudflareTask (用于 TLSContact 5s 盾)
  104. """
  105. # 注意:这里路径根据之前的 C++ 移植经验是 /api/tasks,
  106. # 如果你确定是 /common/api/tasks,请修改这里。为了兼容现有 base_url,我使用 /api/tasks
  107. # url = f"https://{self._base_url}/common/api/tasks"
  108. url = f"{self.base_url}/api/tasks"
  109. args = {
  110. 'proxy': proxy,
  111. 'websiteUrl': website_url
  112. }
  113. data = {
  114. "command": "AntiCloudflareTask", # 关键:Command 名字不同
  115. "args": json.dumps(args),
  116. "status": 0
  117. }
  118. headers = {
  119. 'Authorization': self.api_token,
  120. 'Content-Type': 'application/json'
  121. }
  122. try:
  123. response = self.session.post(url, headers=headers, json=data, timeout=30)
  124. if response.status_code != 200:
  125. VSC_ERROR("vs_cloud", f"submit_anticloudflare_task failed: {response.status_code}")
  126. return None
  127. return response.json()
  128. except Exception as e:
  129. VSC_ERROR("vs_cloud", f"submit_anticloudflare_task exception: {e}")
  130. return None
  131. def get_anticloudflare_result(self, task_id, retry_interval=5, max_retries=20) -> Optional[Dict]:
  132. """
  133. 获取 AntiCloudflareTask 结果 (带轮询)
  134. """
  135. url = f"{self.base_url}/api/tasks/{task_id}"
  136. headers = {
  137. 'Authorization': self.api_token
  138. }
  139. try:
  140. for attempt in range(max_retries):
  141. response = self.session.get(url, headers=headers, timeout=30)
  142. if response.status_code == 200:
  143. data = response.json()
  144. # status 2 表示成功
  145. if data.get("status") == 2:
  146. return data
  147. elif data.get("status") == 3:
  148. VSC_ERROR("vs_cloud", f"AntiCloudflareTask failed: {data.get('result')}")
  149. return None
  150. else:
  151. # VSC_DEBUG("vs_cloud", f"Task {task_id} not ready, retrying...")
  152. time.sleep(retry_interval)
  153. else:
  154. VSC_ERROR("vs_cloud", f"Error getting task result: {response.text}")
  155. break
  156. VSC_ERROR("vs_cloud", "Max retries reached, AntiCloudflareTask not completed.")
  157. except Exception as e:
  158. VSC_ERROR("vs_cloud", f"get_anticloudflare_result exception: {e}")
  159. return None
  160. def create_http_session(
  161. self,
  162. session_id: str,
  163. cookies: str,
  164. local_storage: str,
  165. user_agent: str,
  166. proxy: str,
  167. page: str,
  168. created_session_out: Dict[str, Any]
  169. ) -> bool:
  170. """创建 http session"""
  171. headers = self._get_headers()
  172. payload = {
  173. "local_storage": local_storage,
  174. "cookies": cookies,
  175. "user_agent": user_agent,
  176. "proxy": proxy,
  177. "page": page,
  178. "session_id": session_id
  179. }
  180. result = self._handle_request("POST", "/api/http-session", headers=headers, json=payload)
  181. if result is not None:
  182. # result 可能是 None 如果解析失败,或者是一个 dict
  183. if isinstance(result, dict):
  184. created_session_out.update(result)
  185. return True
  186. return False
  187. def fetch_mail_content(
  188. self,
  189. email: str,
  190. sender: str,
  191. recipient: str,
  192. subject_keywords: str,
  193. body_keywords: str,
  194. sent_date: str,
  195. expiry: int,
  196. content_out: list # Python string is immutable, use list to simulate pointer behavior like C++
  197. ) -> bool:
  198. """
  199. 获取邮件内容
  200. C++ logic: query params in URL + POST method + empty body
  201. """
  202. params = {
  203. "email": email,
  204. "sender": sender,
  205. "recipient": recipient,
  206. "subjectKeywords": subject_keywords,
  207. "bodyKeywords": body_keywords,
  208. "sentDate": sent_date,
  209. "expiry": str(expiry)
  210. }
  211. headers = self._get_headers()
  212. # C++: curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
  213. # requests.post(url, params=params) 会把 params 拼接到 URL 后面
  214. url = f"{self.base_url}/api/email-authorizations/fetch"
  215. try:
  216. resp = self.session.post(url, headers=headers, params=params, data="", timeout=30)
  217. if resp.status_code == 200:
  218. # C++: content.assign(response_string.data(), response_string.length());
  219. # 这里假设 content_out 是一个 list,我们将结果 append 进去或修改第一个元素
  220. if isinstance(content_out, list):
  221. content_out.clear()
  222. content_out.append(resp.text)
  223. return True
  224. else:
  225. VSC_ERROR("vs_cloud", f"fetch_mail_content failed: {resp.status_code} {resp.text}")
  226. except Exception as e:
  227. VSC_ERROR("vs_cloud", f"fetch_mail_content exception: {e}")
  228. return False
  229. def fetch_mail_content_from_top(
  230. self,
  231. email: str,
  232. sender: str,
  233. recipient: str,
  234. subject_keywords: str,
  235. body_keywords: str,
  236. top: int,
  237. content_out: list
  238. ) -> bool:
  239. """从顶部获取邮件内容"""
  240. params = {
  241. "email": email,
  242. "sender": sender,
  243. "recipient": recipient,
  244. "subjectKeywords": subject_keywords,
  245. "bodyKeywords": body_keywords,
  246. "top": str(top)
  247. }
  248. headers = self._get_headers()
  249. url = f"{self.base_url}/api/email-authorizations/fetch-top"
  250. try:
  251. resp = self.session.post(url, headers=headers, params=params, data="", timeout=30)
  252. if resp.status_code == 200:
  253. if isinstance(content_out, list):
  254. content_out.clear()
  255. content_out.append(resp.text)
  256. return True
  257. else:
  258. VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top failed: {resp.status_code} {resp.text}")
  259. except Exception as e:
  260. VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top exception: {e}")
  261. return False