__init__.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import dataclasses
  2. import datetime
  3. import json
  4. import os
  5. import pathlib
  6. import ssl
  7. import subprocess
  8. import sys
  9. import typing
  10. import urllib.parse
  11. import urllib.request
  12. # official api documentation:
  13. # https://github.com/ToontownRewritten/api-doc/blob/master/login.md
  14. # https://github.com/ToontownRewritten/api-doc/blob/master/invasions.md
  15. _LOGIN_API_URL = "https://www.toontownrewritten.com/api/login?format=json"
  16. def start_engine(
  17. engine_path: pathlib.Path, gameserver: str, playcookie: str, **popen_kwargs
  18. ) -> subprocess.Popen:
  19. # without XAUTHORITY:
  20. # > :display:x11display(error): Could not open display ":0.0".
  21. # > :ToonBase: Default graphics pipe is glxGraphicsPipe (OpenGL).
  22. # > :ToonBase(warning): Unable to open 'onscreen' window.
  23. # > Traceback (most recent call last):
  24. # > File "<compiled '__voltorbmain__'>", line 0, in <module>
  25. # > [...]
  26. # > File "<compiled 'direct.vlt8f63e471.ShowBase'>", line 0, in vltf05fd21b
  27. # > Exception: Could not open window.
  28. # optirun sets plenty of env vars
  29. env = os.environ.copy()
  30. env["TTR_GAMESERVER"] = gameserver
  31. env["TTR_PLAYCOOKIE"] = playcookie
  32. # .resolve(strict=True/False) is not available in python3.5
  33. engine_path = engine_path.resolve()
  34. if sys.platform == "darwin":
  35. env["DYLD_LIBRARY_PATH"] = str(engine_path.parent.joinpath("Libraries.bundle"))
  36. env["DYLD_FRAMEWORK_PATH"] = str(engine_path.parent.joinpath("Frameworks"))
  37. return subprocess.Popen(
  38. args=[str(engine_path)],
  39. cwd=str(engine_path.parent), # str conversion for compatibility with python3.5
  40. env=env,
  41. **popen_kwargs,
  42. )
  43. def _api_request(
  44. *, url: str, params: typing.Optional[dict] = None, validate_ssl_cert: bool = True
  45. ):
  46. request = urllib.request.Request(
  47. url=url,
  48. data=urllib.parse.urlencode(params).encode("ascii") if params else None,
  49. headers={"User-Agent": "tooncher"},
  50. )
  51. if validate_ssl_cert is False: # explicit check to avoid catching None
  52. # > To revert to [...] unverified behavior ssl._create_unverified_context()
  53. # > can be passed to the context parameter.
  54. # https://docs.python.org/3.8/library/http.client.html
  55. ssl_context: typing.Optional[ssl.SSLContext] = (
  56. # pylint: disable=protected-access; recommended in python docs
  57. ssl._create_unverified_context()
  58. )
  59. else:
  60. ssl_context = None
  61. with urllib.request.urlopen(request, context=ssl_context) as response:
  62. return json.loads(response.read().decode("ascii"))
  63. @dataclasses.dataclass
  64. class _LoginSuccessful:
  65. playcookie: str
  66. gameserver: str
  67. @dataclasses.dataclass
  68. class _LoginDelayed:
  69. queue_token: str
  70. def _login(
  71. *,
  72. username: typing.Optional[str] = None,
  73. password: typing.Optional[str] = None,
  74. queue_token: typing.Optional[str] = None,
  75. validate_ssl_cert: bool = True,
  76. ) -> typing.Union[_LoginSuccessful, _LoginDelayed]:
  77. if username is not None and queue_token is None:
  78. assert password is not None
  79. req_params = {
  80. "username": username,
  81. "password": password,
  82. }
  83. elif username is None and queue_token is not None:
  84. req_params = {
  85. "queueToken": queue_token,
  86. }
  87. else:
  88. raise ValueError("either specify username or queue token")
  89. resp_data = _api_request(
  90. url=_LOGIN_API_URL,
  91. params=req_params,
  92. validate_ssl_cert=validate_ssl_cert,
  93. )
  94. if resp_data["success"] == "true":
  95. return _LoginSuccessful(
  96. playcookie=resp_data["cookie"],
  97. gameserver=resp_data["gameserver"],
  98. )
  99. if resp_data["success"] == "delayed":
  100. return _LoginDelayed(queue_token=resp_data["queueToken"])
  101. raise RuntimeError(repr(resp_data))
  102. def launch(
  103. engine_path: pathlib.Path,
  104. username: str,
  105. password: str,
  106. validate_ssl_certs: bool = True,
  107. cpu_limit_percent: typing.Optional[int] = None,
  108. ) -> None:
  109. result = _login(
  110. username=username,
  111. password=password,
  112. validate_ssl_cert=validate_ssl_certs,
  113. )
  114. if isinstance(result, _LoginDelayed):
  115. result = _login(
  116. queue_token=result.queue_token,
  117. validate_ssl_cert=validate_ssl_certs,
  118. )
  119. if not isinstance(result, _LoginSuccessful):
  120. raise RuntimeError(f"unexpected response: {result!r}")
  121. process = start_engine(
  122. engine_path=engine_path,
  123. gameserver=result.gameserver,
  124. playcookie=result.playcookie,
  125. )
  126. if cpu_limit_percent is not None:
  127. subprocess.run(
  128. args=[
  129. "cpulimit",
  130. "--pid",
  131. str(process.pid),
  132. "--limit",
  133. str(cpu_limit_percent),
  134. # '--verbose',
  135. ],
  136. check=True,
  137. )
  138. process.wait()