diff --git a/recipes/2.SimpleWebAPI/README.rst b/recipes/2.SimpleWebAPI/README.rst new file mode 100644 index 0000000..f636a72 --- /dev/null +++ b/recipes/2.SimpleWebAPI/README.rst @@ -0,0 +1,85 @@ +Simple Web API recipe +--------------------- + +Valve doesn't have a Web API for everything, and they don't need to. +We are going to use the ``steam`` and ``flask`` to build a Web API. +First, we need to install ``flask``. + +.. code:: bash + + (env)$ pip install flask + +``run_webapi.py`` contains our HTTP server app and ``steam_worker.py`` is +a modified version of persistent login recipe that will talk with steam. + +Let's run the app: + +.. code:: bash + + (env)$ python run_webapi.py + 2016-11-01 00:00:01,000 | SimpleWebAPI | Simple Web API recipe + 2016-11-01 00:00:02,000 | SimpleWebAPI | ------------------------------ + 2016-11-01 00:00:03,000 | SimpleWebAPI | Starting Steam worker... + Username: myusername + Password: + 2016-11-01 00:00:04,000 | Steam Worker | Connected to (u'1.2.3.4', 27018) + 2016-11-01 00:00:05,000 | SimpleWebAPI | Starting HTTP server... + 2016-11-01 00:00:06,000 | Steam Worker | ------------------------------ + 2016-11-01 00:00:07,000 | Steam Worker | Logged on as: FriendlyGhost + ... + 127.0.0.1 - - [2016-01-01 00:00:08] "GET /ISteamApps/GetPlayerCount/?appid=0 HTTP/1.1" 200 155 0.262596 + ... + + +Here are the available endpoints: + +.. code:: bash + + $ curl -s 127.0.0.1:5000/ISteamApps/GetProductInfo/?appids=570,730 | head -56 + { + "apps": [ + { + "appid": 570, + "appinfo": { + "appid": "570", + "common": { + "clienticon": "c0d15684e6c186289b50dfe083f5c562c57e8fb6", + "clienttga": "5ca2b133f8fdf56c3d81dd73d1254f95f0614265", + "community_hub_visible": "1", + "controllervr": { + "steamvr": "1" + }, + "exfgls": "1", + "gameid": "570", + "header_image": { + "english": "header.jpg" + }, + "icon": "0bbb630d63262dd66d2fdd0f7d37e8661a410075", + "linuxclienticon": "e1c520b6a98b1fed674a117e9356cdb9ddc6d40c", + "logo": "d4f836839254be08d8e9dd333ecc9a01782c26d2", + "logo_small": "d4f836839254be08d8e9dd333ecc9a01782c26d2_thumb", + "metacritic_fullurl": "http://www.metacritic.com/game/pc/dota-2?ftag=MCD-06-10aaa1f", + "metacritic_name": "Dota 2", + "metacritic_score": "90", + + + + $ curl -s 127.0.0.1:5000/ISteamApps/GetProductChanges/?since_changenumber=2397700 | head -10 + { + "app_changes": [ + { + "appid": 730, + "change_number": 2409212 + }, + { + "appid": 740, + "change_number": 2409198 + }, + + + + $ curl 127.0.0.1:5000/ISteamApps/GetPlayerCount/?appid=0 + { + "eresult": 1, + "player_count": 2727080 + } diff --git a/recipes/2.SimpleWebAPI/run_webapi.py b/recipes/2.SimpleWebAPI/run_webapi.py new file mode 100644 index 0000000..fddb475 --- /dev/null +++ b/recipes/2.SimpleWebAPI/run_webapi.py @@ -0,0 +1,55 @@ +from getpass import getpass +from gevent.wsgi import WSGIServer +from steam_worker import SteamWorker +from flask import Flask, request, abort, jsonify + +import logging +logging.basicConfig(format="%(asctime)s | %(name)s | %(message)s", level=logging.INFO) +LOG = logging.getLogger('SimpleWebAPI') + +app = Flask('SimpleWebAPI') + +@app.route("/ISteamApps/GetProductInfo/", methods=['GET']) +def GetProductInfo(): + appids = request.args.get('appids', '') + pkgids = request.args.get('packageids', '') + + if not appids and not pkgids: + return jsonify({}) + + appids = map(int, appids.split(',')) if appids else [] + pkgids = map(int, pkgids.split(',')) if pkgids else [] + + return jsonify(worker.get_product_info(appids, pkgids) or {}) + +@app.route("/ISteamApps/GetProductChanges/", methods=['GET']) +def GetProductChanges(): + chgnum = int(request.args.get('since_changenumber', 0)) + return jsonify(worker.get_product_changes(chgnum)) + +@app.route("/ISteamApps/GetPlayerCount/", methods=['GET']) +def GetPlayerCount(): + appid = int(request.args.get('appid', 0)) + return jsonify(worker.get_player_count(appid)) + + +if __name__ == "__main__": + LOG.info("Simple Web API recipe") + LOG.info("-"*30) + LOG.info("Starting Steam worker...") + + worker = SteamWorker() + + try: + worker.start(username=raw_input('Username: '), password=getpass()) + except: + raise SystemExit + + LOG.info("Starting HTTP server...") + http_server = WSGIServer(('', 5000), app) + + try: + http_server.serve_forever() + except KeyboardInterrupt: + LOG.info("Exit requested") + worker.close() diff --git a/recipes/2.SimpleWebAPI/steam_worker.py b/recipes/2.SimpleWebAPI/steam_worker.py new file mode 100644 index 0000000..0596c8e --- /dev/null +++ b/recipes/2.SimpleWebAPI/steam_worker.py @@ -0,0 +1,129 @@ +import logging +import gevent +from binascii import hexlify +from steam import SteamClient +from steam.core.msg import MsgProto +from steam.enums.emsg import EMsg +from steam.util import proto_to_dict +import vdf + +LOG = logging.getLogger("Steam Worker") + + +class SteamWorker(object): + def __init__(self): + self.logged_on_once = False + self.logon_details = {} + + self.steam = client = SteamClient() + client.set_credential_location(".") + + @client.on("error") + def handle_error(result): + LOG.info("Logon result: %s", repr(result)) + + @client.on("channel_secured") + def send_login(): + if client.relogin_available: + client.relogin() + else: + client.login(**self.logon_details) + self.logon_details.pop('auth_code', None) + self.logon_details.pop('two_factor_code', None) + + @client.on("connected") + def handle_connected(): + LOG.info("Connected to %s", client.current_server_addr) + + @client.on("reconnect") + def handle_reconnect(delay): + LOG.info("Reconnect in %ds...", delay) + + @client.on("disconnected") + def handle_disconnect(): + LOG.info("Disconnected.") + + if self.logged_on_once: + LOG.info("Reconnecting...") + client.reconnect(maxdelay=30) + + @client.on("auth_code_required") + def auth_code_prompt(is_2fa, mismatch): + if mismatch: + LOG.info("Previous code was incorrect") + + if is_2fa: + code = raw_input("Enter 2FA Code: ") + self.logon_details['two_factor_code'] = code + else: + code = raw_input("Enter Email Code: ") + self.logon_details['auth_code'] = code + + client.connect() + + @client.on("logged_on") + def handle_after_logon(): + self.logged_on_once = True + + LOG.info("-"*30) + LOG.info("Logged on as: %s", client.user.name) + LOG.info("Community profile: %s", client.steam_id.community_url) + LOG.info("Last logon: %s", client.user.last_logon) + LOG.info("Last logoff: %s", client.user.last_logoff) + LOG.info("-"*30) + + + def start(self, username, password): + self.logon_details = { + 'username': username, + 'password': password, + } + + self.steam.connect() + self.steam.wait_event('logged_on') + + def close(self): + if self.steam.connected: + self.logged_on_once = False + LOG.info("Logout") + self.steam.logout() + + def get_product_info(self, appids=[], packageids=[]): + resp = self.steam.send_job_and_wait(MsgProto(EMsg.ClientPICSProductInfoRequest), + { + 'apps': map(lambda x: {'appid': x}, appids), + 'packages': map(lambda x: {'packageid': x}, packageids), + }, + timeout=10 + ) + + if not resp: return {} + + resp = proto_to_dict(resp) + + for app in resp.get('apps', []): + app['appinfo'] = vdf.loads(app.pop('buffer').rstrip('\x00'))['appinfo'] + app['sha'] = hexlify(app['sha']) + for pkg in resp.get('packages', []): + pkg['appinfo'] = vdf.binary_loads(pkg.pop('buffer')[4:])[str(pkg['packageid'])] + pkg['sha'] = hexlify(pkg['sha']) + + return resp + + def get_product_changes(self, since_change_number): + resp = self.steam.send_job_and_wait(MsgProto(EMsg.ClientPICSChangesSinceRequest), + { + 'since_change_number': since_change_number, + 'send_app_info_changes': True, + 'send_package_info_changes': True, + }, + timeout=10 + ) + return proto_to_dict(resp) or {} + + def get_player_count(self, appid): + resp = self.steam.send_job_and_wait(MsgProto(EMsg.ClientGetNumberOfCurrentPlayersDP), + {'appid': appid}, + timeout=10 + ) + return proto_to_dict(resp) or {}