__init__.py 4.9 KB

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