Ported GTS Phase I over from spline-pokedex. veekun-promotions/2010050901
authorEevee <git@veekun.com>
Fri, 30 Apr 2010 05:07:59 +0000 (22:07 -0700)
committerEevee <git@veekun.com>
Fri, 30 Apr 2010 05:07:59 +0000 (22:07 -0700)
15 files changed:
.gitignore [new file with mode: 0644]
migration/README [new file with mode: 0644]
migration/__init__.py [new file with mode: 0755]
migration/manage.py [new file with mode: 0644]
migration/migrate.cfg [new file with mode: 0644]
migration/versions/001_Create_a_permanent_deposits_table.py [new file with mode: 0644]
migration/versions/__init__.py [new file with mode: 0755]
setup.py [new file with mode: 0644]
spline/__init__.py [new file with mode: 0644]
spline/plugins/__init__.py [new file with mode: 0644]
spline/plugins/gts/__init__.py [new file with mode: 0644]
spline/plugins/gts/controllers/__init__.py [new file with mode: 0644]
spline/plugins/gts/controllers/gts.py [new file with mode: 0644]
spline/plugins/gts/model/__init__.py [new file with mode: 0644]
spline/plugins/gts/templates/css/gts.mako [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..6df398f
--- /dev/null
@@ -0,0 +1,7 @@
+*.swp
+*.pyc
+
+*.egg-info/
+/*.db
+/data
+/migration.py
diff --git a/migration/README b/migration/README
new file mode 100644 (file)
index 0000000..6218f8c
--- /dev/null
@@ -0,0 +1,4 @@
+This is a database migration repository.
+
+More information at
+http://code.google.com/p/sqlalchemy-migrate/
diff --git a/migration/__init__.py b/migration/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/migration/manage.py b/migration/manage.py
new file mode 100644 (file)
index 0000000..0add9c2
--- /dev/null
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+from migrate.versioning.shell import main
+
+main(repository='migration')
diff --git a/migration/migrate.cfg b/migration/migrate.cfg
new file mode 100644 (file)
index 0000000..2deb5c1
--- /dev/null
@@ -0,0 +1,20 @@
+[db_settings]
+# Used to identify which repository this database is versioned under.
+# You can use the name of your project.
+repository_id=spline-gts
+
+# The name of the database table used to track the schema version.
+# This name shouldn't already be used by your project.
+# If this is changed once a database is under version control, you'll need to 
+# change the table name in each database too. 
+version_table=migrate_version
+
+# When committing a change script, Migrate will attempt to generate the 
+# sql for all supported databases; normally, if one of them fails - probably
+# because you don't have that database installed - it is ignored and the 
+# commit continues, perhaps ending successfully. 
+# Databases in this list MUST compile successfully during a commit, or the 
+# entire commit will fail. List the databases your application will actually 
+# be using to ensure your updates to that database work properly.
+# This must be a list; example: ['postgres','sqlite']
+required_dbs=[]
diff --git a/migration/versions/001_Create_a_permanent_deposits_table.py b/migration/versions/001_Create_a_permanent_deposits_table.py
new file mode 100644 (file)
index 0000000..d019e7d
--- /dev/null
@@ -0,0 +1,19 @@
+from sqlalchemy import *
+from migrate import *
+
+from sqlalchemy.ext.declarative import declarative_base
+TableBase = declarative_base(bind=migrate_engine)
+
+
+class GTSPokemon(TableBase):
+    __tablename__ = 'gts_pokemon'
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    pid = Column(Integer)
+    pokemon_blob = Column(Binary(292), nullable=False)
+
+
+def upgrade():
+    GTSPokemon.__table__.create()
+
+def downgrade():
+    GTSPokemon.__table__.drop()
diff --git a/migration/versions/__init__.py b/migration/versions/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..7dd0cfc
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,20 @@
+from setuptools import setup, find_packages
+setup(
+    name = 'spline-gts',
+    version = '0.1',
+    packages = find_packages(),
+    
+    install_requires = [
+        'spline',
+        'spline-users',
+        'pokedex',
+    ],
+
+    include_package_data = True,
+
+    zip_safe = False,
+
+    entry_points = {'spline.plugins': 'gts = spline.plugins.gts:GTSPlugin'},
+
+    namespace_packages = ['spline', 'spline.plugins'],
+)
diff --git a/spline/__init__.py b/spline/__init__.py
new file mode 100644 (file)
index 0000000..de40ea7
--- /dev/null
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/spline/plugins/__init__.py b/spline/plugins/__init__.py
new file mode 100644 (file)
index 0000000..de40ea7
--- /dev/null
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/spline/plugins/gts/__init__.py b/spline/plugins/gts/__init__.py
new file mode 100644 (file)
index 0000000..c1b0fb5
--- /dev/null
@@ -0,0 +1,39 @@
+from pkg_resources import resource_filename
+
+from pylons import c, session
+
+from spline.lib.plugin import PluginBase
+from spline.lib.plugin import PluginBase, PluginLink, Priority
+import spline.model as model
+import spline.model.meta as meta
+
+import spline.plugins.gts.controllers.gts
+import spline.plugins.gts.model
+
+def add_routes_hook(map, *args, **kwargs):
+    """Hook to inject some of our behavior into the routes configuration."""
+    # These are the GTS URLs
+    map.connect('/pokemondpds/worldexchange/{page}.asp', controller='gts', action='dispatch')
+    map.connect('/pokemondpds/common/{page}.asp', controller='gts', action='dispatch')
+
+
+class GTSPlugin(PluginBase):
+    def controllers(self):
+        return dict(
+            gts = spline.plugins.gts.controllers.gts.GTSController,
+        )
+
+    def model(self):
+        return [
+            spline.plugins.gts.model.GTSPokemon,
+        ]
+
+    def template_dirs(self):
+        return [
+            (resource_filename(__name__, 'templates'), Priority.NORMAL)
+        ]
+
+    def hooks(self):
+        return [
+            ('routes_mapping',    Priority.NORMAL,      add_routes_hook),
+        ]
diff --git a/spline/plugins/gts/controllers/__init__.py b/spline/plugins/gts/controllers/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/spline/plugins/gts/controllers/gts.py b/spline/plugins/gts/controllers/gts.py
new file mode 100644 (file)
index 0000000..98526a8
--- /dev/null
@@ -0,0 +1,214 @@
+# encoding: utf8
+from __future__ import absolute_import, division
+
+from base64 import urlsafe_b64decode
+from collections import namedtuple
+from itertools import izip
+import logging
+from random import sample
+from string import uppercase, lowercase, digits
+import struct
+
+import pokedex.db
+import pokedex.db.tables as tables
+import pokedex.formulae
+from pokedex.savefile import PokemonSave
+from pylons import config, request, response, session, tmpl_context as c, url
+from pylons.controllers.util import abort, redirect_to
+from sqlalchemy import and_, or_, not_
+from sqlalchemy.orm import aliased, contains_eager, eagerload, eagerload_all, join
+from sqlalchemy.orm.exc import NoResultFound
+from sqlalchemy.sql import func
+
+from spline import model
+from spline.model import meta
+from spline.lib.base import BaseController, render
+from spline.lib import helpers as h
+
+from spline.plugins.gts.model import GTSPokemon
+
+log = logging.getLogger(__name__)
+
+
+### Utility functions
+
+def gts_prng(seed):
+    """Implements the GTS's linear congruential generator.
+
+    I love typing that out.  It makes me sounds so smart.
+
+    Yields magical numbers."""
+    # 0xabcd => 0xabcdabcd
+    seed = seed | (seed << 16)
+    while True:
+        seed = (seed * 0x45 + 0x1111) & 0x7fffffff  # signed dword!
+        yield (seed >> 16) & 0xff
+
+def stream_decipher(data, keystream):
+    """Reverses a stream cipher, given iterable data and keystream.
+
+    Yields decrypted bytes.
+    """
+    for c, key in izip(data, keystream):
+        new_c = (ord(c) ^ key) & 0xff
+        yield chr(new_c)
+
+
+def decrypt_data(data):
+    """Takes a binary blob uploaded from a game and returns the original binary
+    blob.  Depending on your perspective, the returned value may be more
+    intelligible.
+    """
+    # GTS encryption is a simple stream cipher.
+    # The first four bytes of the data are a header containing an obfuscated
+    # key; the rest is the message
+    obf_key_blob, message = data[0:4], data[4:]
+    obf_key, = struct.unpack('>I', obf_key_blob)
+    key = obf_key ^ 0x4a3b2c1d
+
+    # Data is XORed with the output of an LCG, like everything else in Pokémon
+    return ''.join( stream_decipher(message, gts_prng(key)) )
+
+
+### Controller!
+
+def dbg(*args):
+    #print ' '.join(args)
+    pass
+
+class GTSController(BaseController):
+
+    def dispatch(self, page):
+        """We do our own dispatching for two reasons:
+
+        1. All requests do challenge/response before anything, and it's easier
+        to do that and then dispatch than copy/paste some block of code to
+        every action.
+
+        2. Easier to dump stuff if desired.
+        """
+
+        dbg(request)
+
+        # Always return binary!
+        response.headers['Content-type'] = 'application/octet-stream'
+
+        if 'hash' in request.params:
+            # Okay, already done the response.  Decrypt, then dispatch!
+            if request.params['data']:
+                # Note: base64 doesn't like unicode.  Go figure.  It's binary
+                # junk, anyway.
+                encrypted_data = urlsafe_b64decode(str(request.params['data']))
+                data = decrypt_data(encrypted_data)
+                data = data[4:]  # data always starts with pid; we don't care
+            else:
+                data = ''
+
+            method = 'page_' + page
+            if not hasattr(self, method):
+                dbg("NOT YET IMPLEMENTED?")
+                abort(404)
+
+            res = getattr(self, method)(request.params['pid'], data)
+            dbg("RESPONDING WITH ", type(res), len(res), repr(res))
+            return res
+
+        # No hash.  Need to issue a challenge.  It's random, so whatever
+        return ''.join(sample( uppercase + lowercase + digits, 32 ))
+
+    ### Actual pages
+
+    def page_info(self, pid, data):
+        """info.asp
+
+        Apparently always returns 0x0001.  Probably just a ping to see if the
+        server is up.
+        """
+
+        return '\x01\x00'
+
+    def page_setProfile(self, pid, data):
+        """setProfile.asp
+
+        Only Pt and later check this page.  Don't know why.  It returns eight
+        NULs, every time.
+        """
+
+        return '\x00' * 8
+
+    def page_result(self, pid, data):
+        u"""result.asp
+
+        The good part!
+
+        This checks the game's status on the GTS.  If there's nothing
+        interesting to report, it returns 0x0005.  If the game has a Pokémon up
+        on the GTS, it returns 0x0004.
+
+        However...  if the game has a Pokémon waiting to come to it, it returns
+        that entire Pokémon struct!  No header, no encryption.  Just a regular
+        ol' Pokémon blob.
+        """
+
+        # Check for an existing Pokémon
+        # TODO support multiple!
+        try:
+            stored_pokemon = meta.Session.query(model.GTSPokemon) \
+                .filter_by(pid=pid) \
+                .one()
+            # We've got one!  Cool, send it back.  The game will ask us to
+            # delete it after receiving successfully
+            pokemon_save = PokemonSave(stored_pokemon.pokemon_blob)
+            return pokemon_save.as_encrypted
+
+        except:
+            # Nothing
+            return '\x05\x00'
+
+    def page_delete(self, pid, data):
+        u"""delete.asp
+
+        If a Pokémon is received from result.asp, the game requests this page
+        to confirm that the incoming Pokémon may be deleted.
+
+        Returns 0x0001.
+        """
+
+        meta.Session.query(model.GTSPokemon).filter_by(pid=pid).delete()
+        meta.Session.commit()
+
+        return '\x01\x00'
+
+    def page_post(self, pid, data):
+        u"""post.asp
+
+        Deposits a Pokémon in the GTS.  Returns 0x0001 on success, or 0x000c if
+        the deposit is rejected.
+        """
+
+        try:
+            # The uploaded Pokémon is encrypted, which is not very useful
+            pokemon_save = PokemonSave(data, encrypted=True)
+
+            # Create a record...
+            stored_pokemon = model.GTSPokemon(
+                pid=pid,
+                pokemon_blob=pokemon_save.as_struct,
+            )
+            meta.Session.add(stored_pokemon)
+            meta.Session.commit()
+            return '\x01\x00'
+        except:
+            # If that failed (presumably due to unique key collision), we're
+            # already storing something.  Reject!
+            return '\x0c\x00'
+
+    def page_post_finish(self, pid, data):
+        u"""post_finish.asp
+
+        Surely this does something, but for the life of me I can't figure out
+        what.
+
+        Returns 0x0001.
+        """
+        return '\x01\x00'
diff --git a/spline/plugins/gts/model/__init__.py b/spline/plugins/gts/model/__init__.py
new file mode 100644 (file)
index 0000000..3d73b54
--- /dev/null
@@ -0,0 +1,11 @@
+from sqlalchemy import Column, ForeignKey
+from sqlalchemy.orm import relation
+from sqlalchemy.types import Binary, Integer, Unicode
+
+from spline.model.meta import TableBase
+
+class GTSPokemon(TableBase):
+    __tablename__ = 'gts_pokemon'
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    pid = Column(Integer)
+    pokemon_blob = Column(Binary(292), nullable=False)
diff --git a/spline/plugins/gts/templates/css/gts.mako b/spline/plugins/gts/templates/css/gts.mako
new file mode 100644 (file)
index 0000000..e69de29