Fabian Peter Hammerle 5 years ago
commit
1e7fc4b920
8 changed files with 241 additions and 0 deletions
  1. 10 0
      .gitignore
  2. 21 0
      LICENSE.txt
  3. 2 0
      README.md
  4. 62 0
      scripts/tooncher
  5. 2 0
      setup.cfg
  6. 21 0
      setup.py
  7. 27 0
      tests/test_.py
  8. 96 0
      tooncher/__init__.py

+ 10 - 0
.gitignore

@@ -0,0 +1,10 @@
+# Compiled python modules.
+*.pyc
+
+# Setuptools distribution folder.
+/dist/
+
+# Python egg metadata, regenerated from source files by setuptools.
+/*.egg-info
+
+/.cache

+ 21 - 0
LICENSE.txt

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2017 Fabian Peter Hammerle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 2 - 0
README.md

@@ -0,0 +1,2 @@
+# tooncher
+automates toontown rewritten's login process

+ 62 - 0
scripts/tooncher

@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# PYTHON_ARGCOMPLETE_OK
+
+import os
+import sys
+import tooncher
+import yaml
+
+def run(username, config_path, engine_path = None):
+
+    if os.path.exists(config_path):
+        with open(config_path) as f:
+            config = yaml.load(f.read())
+    else:
+        config = {}
+
+    if engine_path is None:
+        engine_path = config['engine_path'] if 'engine_path' in config else None
+    if engine_path is None:
+        raise Exception('missing path to toontown engine')
+
+    accounts = config['accounts'] if 'accounts' in config else []
+
+    for account in accounts:
+        if account['username'] == username:
+            tooncher.launch(
+                engine_path = engine_path,
+                username = account['username'],
+                password = account['password'],
+                )
+
+def _init_argparser():
+
+    import argparse
+    argparser = argparse.ArgumentParser(description = None)
+    argparser.add_argument('username')
+    argparser.add_argument(
+        '--config',
+        '-c',
+        metavar = 'path',
+        dest = 'config_path',
+        help = 'path to config file (default: %(default)s)',
+        default = os.path.join(os.path.expanduser('~'), '.tooncher'),
+        )
+    return argparser
+
+def main(argv):
+
+    argparser = _init_argparser()
+    try:
+        import argcomplete
+        argcomplete.autocomplete(argparser)
+    except ImportError:
+        pass
+    args = argparser.parse_args(argv)
+
+    run(**vars(args))
+
+    return 0
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))

+ 2 - 0
setup.cfg

@@ -0,0 +1,2 @@
+[metadata]
+description-file = README.md

+ 21 - 0
setup.py

@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+
+from setuptools import setup
+
+import glob
+
+setup(
+    name = 'tooncher',
+    packages = ['tooncher'],
+    version = '0.1',
+    description = "automates toontown rewritten's login process",
+    author = 'Fabian Peter Hammerle',
+    author_email = 'fabian.hammerle@gmail.com',
+    url = 'https://github.com/fphammerle/tooncher',
+    download_url = 'https://github.com/fphammerle/tooncher/tarball/0.1',
+    keywords = ['game', 'launcher', 'toontown rewritten', 'ttr'],
+    classifiers = [],
+    scripts = glob.glob('scripts/*'),
+    install_requires = ['pyyaml'],
+    tests_require = ['pytest'],
+    )

+ 27 - 0
tests/test_.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+import pytest
+
+import shutil
+import subprocess
+import tooncher
+
+
+def test_start_engine():
+    p = tooncher.start_engine(
+        engine_path=shutil.which('printenv'),
+        gameserver='gameserver',
+        playcookie='cookie',
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+    )
+    assert isinstance(p, subprocess.Popen)
+    stdout, stderr = p.communicate()
+    assert b'' == stderr
+    env = stdout.strip().split(b'\n')
+    assert b'TTR_GAMESERVER=gameserver' in env
+    assert b'TTR_PLAYCOOKIE=cookie' in env
+
+
+def test_api_request_invasions():
+    resp_data = tooncher.api_request(tooncher.INVASIONS_API_URL)
+    assert 'invasions' in resp_data

+ 96 - 0
tooncher/__init__.py

@@ -0,0 +1,96 @@
+import json
+import os
+import subprocess
+import urllib.parse
+import urllib.request
+
+"""
+official api documentation:
+https://github.com/ToontownRewritten/api-doc/blob/master/login.md
+https://github.com/ToontownRewritten/api-doc/blob/master/invasions.md
+"""
+
+INVASIONS_API_URL = 'https://www.toontownrewritten.com/api/invasions?format=json'
+LOGIN_API_URL = 'https://www.toontownrewritten.com/api/login?format=json'
+
+
+def start_engine(engine_path, gameserver, playcookie, **kwargs):
+    return subprocess.Popen(
+        args=[engine_path],
+        cwd=os.path.dirname(engine_path),
+        env={
+            'TTR_GAMESERVER': gameserver,
+            'TTR_PLAYCOOKIE': playcookie,
+        },
+        **kwargs,
+    )
+
+
+def api_request(url, params=None):
+    resp = urllib.request.urlopen(
+        url=url,
+        data=urllib.parse.urlencode(params).encode('ascii')
+            if params else None,
+    )
+    return json.loads(resp.read().decode('ascii'))
+
+
+class LoginSuccessful:
+
+    def __init__(self, playcookie, gameserver):
+        self.playcookie = playcookie
+        self.gameserver = gameserver
+
+
+class LoginDelayed:
+
+    def __init__(self, queue_token):
+        self.queue_token = queue_token
+
+
+def login(username=None, password=None, queue_token=None):
+    if username is not None and queue_token is None:
+        assert password is not None
+        req_params = {
+            'username': username,
+            'password': password,
+        }
+    elif username is None and queue_token is not None:
+        req_params = {
+            'queueToken': queue_token,
+        }
+    else:
+        raise Exception('either specify username or queue token')
+    resp_data = api_request(
+        url=LOGIN_API_URL,
+        params=req_params,
+    )
+    if resp_data['success'] == 'true':
+        return LoginSuccessful(
+            playcookie=resp_data['cookie'],
+            gameserver=resp_data['gameserver'],
+        )
+    elif resp_data['success'] == 'delayed':
+        return LoginDelayed(
+            queue_token=resp_data['queueToken'],
+        )
+    else:
+        raise Exception(repr(resp_data))
+
+
+def launch(engine_path, username, password):
+    result = login(
+        username=username,
+        password=password,
+    )
+    if isinstance(result, LoginDelayed):
+        result = login(queue_token=result.queue_token)
+    if isinstance(result, LoginSuccessful):
+        p = start_engine(
+            engine_path=engine_path,
+            gameserver=result.gameserver,
+            playcookie=result.playcookie,
+        )
+        p.wait()
+    else:
+        raise Exception(repr(result))