plugin.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. """ChatGPT platform plugin for zhuce6."""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. import random
  5. import string
  6. from pathlib import Path
  7. from typing import Any
  8. from core.base_mailbox import BaseMailbox, create_mailbox
  9. from core.base_platform import Account, AccountStatus, BasePlatform, RegisterConfig
  10. from core.mailbox_dedupe import get_mailbox_dedupe_store
  11. from core.registry import register
  12. class MailboxEmailServiceAdapter:
  13. def __init__(self, mailbox: BaseMailbox) -> None:
  14. self.mailbox = mailbox
  15. self._account = None
  16. def create_email(self, config: dict[str, Any] | None = None) -> dict[str, Any]:
  17. del config
  18. self._account = self.mailbox.get_email()
  19. return {
  20. "email": self._account.email,
  21. "service_id": self._account.account_id,
  22. "token": self._account.account_id,
  23. }
  24. def get_verification_code(
  25. self,
  26. email: str | None = None,
  27. email_id: str | None = None,
  28. timeout: int = 120,
  29. pattern: str | None = None,
  30. otp_sent_at: float | None = None,
  31. ) -> str:
  32. del email, email_id, pattern, otp_sent_at
  33. if self._account is None:
  34. return ""
  35. return self.mailbox.wait_for_code(self._account, keyword="", timeout=timeout)
  36. @register
  37. class ChatGPTPlatform(BasePlatform):
  38. name = "chatgpt"
  39. display_name = "ChatGPT"
  40. version = "0.1.0"
  41. def __init__(self, config: RegisterConfig | None = None, mailbox: BaseMailbox | None = None) -> None:
  42. super().__init__(config)
  43. self.mailbox = mailbox
  44. def check_valid(self, account: Account) -> bool:
  45. try:
  46. from platforms.chatgpt.payment import check_subscription_status
  47. class _AccountView:
  48. pass
  49. view = _AccountView()
  50. extra = account.extra or {}
  51. view.access_token = extra.get("access_token") or account.token
  52. view.cookies = extra.get("cookies", "")
  53. status = check_subscription_status(view, proxy=self.config.proxy if self.config else None)
  54. return status not in ("expired", "invalid", "banned", None)
  55. except Exception:
  56. return False
  57. def _resolve_mail_provider(self) -> str:
  58. return str((self.config.extra or {}).get("mail_provider", "cfmail")).strip() or "cfmail"
  59. def _resolve_cfmail_profile_name(self) -> str:
  60. return str((self.config.extra or {}).get("cfmail_profile_name", "auto")).strip() or "auto"
  61. def _resolve_mailbox(self, provider_name: str) -> BaseMailbox:
  62. if self.mailbox is not None:
  63. return self.mailbox
  64. return create_mailbox(
  65. provider_name,
  66. proxy=self.config.proxy if self.config else None,
  67. profile_name=self._resolve_cfmail_profile_name(),
  68. )
  69. def _run_registration(self, email: str | None = None, password: str | None = None) -> dict[str, Any]:
  70. from platforms.chatgpt.register import RegistrationEngine
  71. provider_name = self._resolve_mail_provider()
  72. mailbox = self._resolve_mailbox(provider_name)
  73. mailbox_dedupe_store = get_mailbox_dedupe_store(
  74. state_file=Path.cwd() / "state" / "seen_mailboxes.jsonl",
  75. pool_dir=self.config.output_dir if self.config and self.config.output_dir else Path.cwd() / "pool",
  76. )
  77. engine = RegistrationEngine(
  78. email_service=MailboxEmailServiceAdapter(mailbox),
  79. proxy_url=self.config.proxy if self.config else None,
  80. mailbox_dedupe_store=mailbox_dedupe_store,
  81. )
  82. if email:
  83. engine.email = email
  84. engine.password = password
  85. result = engine.run()
  86. payload = result.to_dict()
  87. metadata = payload.setdefault("metadata", {})
  88. metadata["mail_provider"] = provider_name
  89. metadata["cfmail_profile_name"] = self._resolve_cfmail_profile_name()
  90. return payload
  91. def run_preflight(self, email: str | None = None, password: str | None = None) -> dict[str, Any]:
  92. from platforms.chatgpt.register import RegistrationEngine
  93. provider_name = self._resolve_mail_provider()
  94. mailbox = self._resolve_mailbox(provider_name)
  95. mailbox_dedupe_store = get_mailbox_dedupe_store(
  96. state_file=Path.cwd() / "state" / "seen_mailboxes.jsonl",
  97. pool_dir=self.config.output_dir if self.config and self.config.output_dir else Path.cwd() / "pool",
  98. )
  99. engine = RegistrationEngine(
  100. email_service=MailboxEmailServiceAdapter(mailbox),
  101. proxy_url=self.config.proxy if self.config else None,
  102. mailbox_dedupe_store=mailbox_dedupe_store,
  103. )
  104. if email:
  105. engine.email = email
  106. engine.password = password
  107. result = engine.run_preflight()
  108. payload = result.to_dict()
  109. metadata = payload.setdefault("metadata", {})
  110. metadata["mail_provider"] = provider_name
  111. metadata["cfmail_profile_name"] = self._resolve_cfmail_profile_name()
  112. return payload
  113. def run_register_once(
  114. self,
  115. email: str | None = None,
  116. password: str | None = None,
  117. *,
  118. write_pool: bool = True,
  119. pool_dir: Path | None = None,
  120. ) -> dict[str, Any]:
  121. from platforms.chatgpt.fingerprint import build_registration_provenance
  122. from platforms.chatgpt.pool import write_token_record
  123. from platforms.chatgpt.register import RegistrationEngine
  124. provider_name = self._resolve_mail_provider()
  125. mailbox = self._resolve_mailbox(provider_name)
  126. target_pool_dir = pool_dir or Path.cwd() / "pool"
  127. mailbox_dedupe_store = get_mailbox_dedupe_store(
  128. state_file=Path.cwd() / "state" / "seen_mailboxes.jsonl",
  129. pool_dir=target_pool_dir,
  130. )
  131. engine = RegistrationEngine(
  132. email_service=MailboxEmailServiceAdapter(mailbox),
  133. proxy_url=self.config.proxy if self.config else None,
  134. mailbox_dedupe_store=mailbox_dedupe_store,
  135. )
  136. if email:
  137. engine.email = email
  138. engine.password = password
  139. result = engine.run()
  140. payload = result.to_dict()
  141. metadata = payload.setdefault("metadata", {})
  142. metadata["mail_provider"] = provider_name
  143. metadata["cfmail_profile_name"] = self._resolve_cfmail_profile_name()
  144. if result.success and write_pool:
  145. mailbox_account = getattr(adapter := engine.email_service, "_account", None)
  146. mailbox_payload = {
  147. "email": result.email,
  148. "account_id": "",
  149. "extra": {},
  150. }
  151. if mailbox_account is not None:
  152. mailbox_payload = {
  153. "email": str(getattr(mailbox_account, "email", "") or result.email).strip() or result.email,
  154. "account_id": str(getattr(mailbox_account, "account_id", "") or "").strip(),
  155. "extra": dict(getattr(mailbox_account, "extra", {}) or {}),
  156. }
  157. token_data = {
  158. "type": "codex",
  159. "email": result.email,
  160. "password": result.password,
  161. "mail_provider": provider_name,
  162. "mailbox": mailbox_payload,
  163. "expired": metadata.get("expired") or "",
  164. "id_token": result.id_token,
  165. "account_id": result.account_id,
  166. "access_token": result.access_token,
  167. "last_refresh": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
  168. "refresh_token": result.refresh_token,
  169. }
  170. token_data.update(
  171. build_registration_provenance(
  172. metadata,
  173. proxy_url=self.config.proxy if self.config else None,
  174. cfmail_profile_name=self._resolve_cfmail_profile_name(),
  175. )
  176. )
  177. written_path = write_token_record(token_data, target_pool_dir)
  178. payload["pool_file"] = str(written_path)
  179. payload["written_to_pool"] = True
  180. else:
  181. payload["pool_file"] = ""
  182. payload["written_to_pool"] = False
  183. return payload
  184. def exchange_callback(
  185. self,
  186. callback_url: str,
  187. expected_state: str,
  188. code_verifier: str,
  189. *,
  190. write_pool: bool = True,
  191. pool_dir: Path | None = None,
  192. ) -> dict[str, Any]:
  193. from platforms.chatgpt.oauth import OAuthManager
  194. from platforms.chatgpt.pool import write_token_record
  195. try:
  196. token_data = OAuthManager(proxy_url=self.config.proxy if self.config else None).handle_callback(
  197. callback_url=callback_url,
  198. expected_state=expected_state,
  199. code_verifier=code_verifier,
  200. )
  201. pool_file = ""
  202. if write_pool:
  203. target_dir = pool_dir or Path.cwd() / "pool"
  204. written_path = write_token_record(token_data, target_dir)
  205. pool_file = str(written_path)
  206. return {
  207. "success": True,
  208. "stage": "oauth_callback_exchanged",
  209. "email": str(token_data.get("email") or ""),
  210. "account_id": str(token_data.get("account_id") or ""),
  211. "written_to_pool": write_pool,
  212. "pool_file": pool_file,
  213. "token_data": token_data,
  214. "source": "callback_exchange",
  215. }
  216. except Exception as exc:
  217. return {
  218. "success": False,
  219. "stage": "oauth_callback_exchange",
  220. "error_message": str(exc),
  221. "written_to_pool": False,
  222. "pool_file": "",
  223. "token_data": {},
  224. "source": "callback_exchange",
  225. }
  226. def register(self, email: str | None = None, password: str | None = None) -> Account:
  227. if not password:
  228. password = "".join(random.choices(string.ascii_letters + string.digits + "!@#$", k=16))
  229. payload = self._run_registration(email=email, password=password)
  230. if not payload.get("success"):
  231. raise RuntimeError(str(payload.get("error_message") or "registration flow failed"))
  232. return Account(
  233. platform="chatgpt",
  234. email=str(payload.get("email") or ""),
  235. password=str(payload.get("password") or password),
  236. user_id=str(payload.get("account_id") or ""),
  237. token=str(payload.get("access_token") or ""),
  238. status=AccountStatus.REGISTERED,
  239. extra={
  240. "access_token": payload.get("access_token", ""),
  241. "refresh_token": payload.get("refresh_token", ""),
  242. "id_token": payload.get("id_token", ""),
  243. "session_token": payload.get("session_token", ""),
  244. "workspace_id": payload.get("workspace_id", ""),
  245. },
  246. )
  247. def get_platform_actions(self) -> list[dict[str, Any]]:
  248. return [
  249. {"id": "refresh_token", "label": "Refresh token", "params": []},
  250. {
  251. "id": "payment_link",
  252. "label": "Generate payment link",
  253. "params": [
  254. {"key": "country", "label": "Country", "type": "select", "options": ["US", "SG", "TR", "HK"]},
  255. {"key": "plan", "label": "Plan", "type": "select", "options": ["plus", "team"]},
  256. ],
  257. },
  258. {
  259. "id": "upload_cpa",
  260. "label": "Upload CPA",
  261. "params": [
  262. {"key": "api_url", "label": "CPA API URL", "type": "text"},
  263. {"key": "api_key", "label": "CPA API key", "type": "text"},
  264. ],
  265. },
  266. ]
  267. def execute_action(self, action_id: str, account: Account, params: dict[str, Any]) -> dict[str, Any]:
  268. proxy = self.config.proxy if self.config else None
  269. extra = account.extra or {}
  270. class _AccountView:
  271. pass
  272. view = _AccountView()
  273. view.email = account.email
  274. view.access_token = extra.get("access_token") or account.token
  275. view.refresh_token = extra.get("refresh_token", "")
  276. view.session_token = extra.get("session_token", "")
  277. view.client_id = extra.get("client_id", "app_EMoamEEZ73f0CkXaXp7hrann")
  278. view.cookies = extra.get("cookies", "")
  279. view.id_token = extra.get("id_token", "")
  280. view.account_id = extra.get("account_id", account.user_id)
  281. view.last_refresh = extra.get("last_refresh")
  282. view.expires_at = extra.get("expires_at")
  283. if action_id == "refresh_token":
  284. from platforms.chatgpt.token_refresh import TokenRefreshManager
  285. result = TokenRefreshManager(proxy_url=proxy).refresh_account(view)
  286. if result.success:
  287. return {
  288. "ok": True,
  289. "data": {
  290. "access_token": result.access_token,
  291. "refresh_token": result.refresh_token,
  292. },
  293. }
  294. return {"ok": False, "error": result.error_message}
  295. if action_id == "payment_link":
  296. from platforms.chatgpt.payment import generate_plus_link, generate_team_link
  297. plan = params.get("plan", "plus")
  298. country = params.get("country", "US")
  299. url = generate_plus_link(view, proxy=proxy, country=country)
  300. if plan == "team":
  301. url = generate_team_link(view, proxy=proxy, country=country)
  302. return {"ok": bool(url), "data": {"url": url}}
  303. if action_id == "upload_cpa":
  304. from platforms.chatgpt.cpa_upload import generate_token_json, upload_to_cpa
  305. ok, message = upload_to_cpa(
  306. generate_token_json(view),
  307. api_url=params.get("api_url"),
  308. api_key=params.get("api_key"),
  309. )
  310. return {"ok": ok, "data": message}
  311. raise NotImplementedError(f"Unknown action: {action_id}")