From ee49936330e582fdd1fa49179b017462c536841f Mon Sep 17 00:00:00 2001 From: Anna Wiggins Date: Sun, 25 Oct 2015 20:51:48 -0400 Subject: [PATCH] ED_Tools initial commit. --- .gitignore | 1 + Readme.md | 20 ++++ companion.py | 276 ++++++++++++++++++++++++++++++++++++++++++++++++ config.py | 19 ++++ elite_info.py | 27 +++++ inara.py | 72 +++++++++++++ update_inara.py | 24 +++++ utils.py | 32 ++++++ 8 files changed, 471 insertions(+) create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 companion.py create mode 100644 config.py create mode 100755 elite_info.py create mode 100644 inara.py create mode 100755 update_inara.py create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..7890958 --- /dev/null +++ b/Readme.md @@ -0,0 +1,20 @@ +# Command-line tools for Elite: Dangerous + +Currently, this package provides two simple python scripts for Elite: Dangerous. + +### elite_info.py + +elite_info fetches the user's credit balance and current location from the E:D Companion API, +and prints this information. In the future it will be able to display additional data. + +### update_inara.py + +update_inara will fetch your location and credit balance from E:D, +and update your inara.cz profile automatically. + + +## Usage + +Run one of the tools to create a blank config file, or copy the provided one into ~/.ed_tool/. +Provide your username and password in the config, then run either script again. It will prompt +you for a verification code. Check your inbox for this code. (you should only have to do this once) diff --git a/companion.py b/companion.py new file mode 100644 index 0000000..77e4960 --- /dev/null +++ b/companion.py @@ -0,0 +1,276 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import requests +from collections import defaultdict +from cookielib import LWPCookieJar +import numbers +import os +from os.path import dirname, join +import sys +from sys import platform +import time + +if __debug__: + from traceback import print_exc + +from config import config + +holdoff = 120 # be nice +timeout = 10 # requests timeout + +URL_LOGIN = 'https://companion.orerve.net/user/login' +URL_CONFIRM = 'https://companion.orerve.net/user/confirm' +URL_QUERY = 'https://companion.orerve.net/profile' + + +# Map values reported by the Companion interface to names displayed in-game and recognized by trade tools + +category_map = { + 'Narcotics' : 'Legal Drugs', + 'Slaves' : 'Slavery', + 'NonMarketable' : False, +} + +commodity_map= { + 'Agricultural Medicines' : 'Agri-Medicines', + 'Ai Relics' : 'AI Relics', + 'Atmospheric Extractors' : 'Atmospheric Processors', + 'Auto Fabricators' : 'Auto-Fabricators', + 'Basic Narcotics' : 'Narcotics', + 'Bio Reducing Lichen' : 'Bioreducing Lichen', + 'Hazardous Environment Suits' : 'H.E. Suits', + 'Heliostatic Furnaces' : 'Microbial Furnaces', + 'Marine Supplies' : 'Marine Equipment', + 'Non Lethal Weapons' : 'Non-Lethal Weapons', + 'S A P8 Core Container' : 'SAP 8 Core Container', + 'Terrain Enrichment Systems' : 'Land Enrichment Systems', +} + +ship_map = { + 'adder' : 'Adder', + 'anaconda' : 'Anaconda', + 'asp' : 'Asp', + 'cobramkiii' : 'Cobra Mk III', + 'diamondback' : 'Diamondback Scout', + 'diamondbackxl' : 'Diamondback Explorer', + 'eagle' : 'Eagle', + 'empire_courier' : 'Imperial Courier', + 'empire_eagle' : 'Imperial Eagle', + 'empire_fighter' : 'Imperial Fighter', + 'empire_trader' : 'Imperial Clipper', + 'federation_dropship' : 'Federal Dropship', + 'federation_dropship_mkii' : 'Federal Assault Ship', + 'federation_gunship' : 'Federal Gunship', + 'federation_fighter' : 'F63 Condor', + 'ferdelance' : 'Fer-de-Lance', + 'hauler' : 'Hauler', + 'orca' : 'Orca', + 'python' : 'Python', + 'sidewinder' : 'Sidewinder', + 'type6' : 'Type-6 Transporter', + 'type7' : 'Type-7 Transporter', + 'type9' : 'Type-9 Heavy', + 'viper' : 'Viper', + 'vulture' : 'Vulture', +} + + +# Companion API sometimes returns an array as a json array, sometimes as a json object indexed by "int". +# This seems to depend on whether the there are 'gaps' in the Cmdr's data - i.e. whether the array is sparse. +# In practice these arrays aren't very sparse so just convert them to lists with any 'gaps' holding None. +def listify(thing): + if thing is None: + return [] # data is not present + elif isinstance(thing, list): + return thing # array is not sparse + elif isinstance(thing, dict): + retval = [] + for k,v in thing.iteritems(): + idx = int(k) + if idx >= len(retval): + retval.extend([None] * (idx - len(retval))) + retval.append(v) + else: + retval[idx] = v + return retval + else: + assert False, thing # we expect an array or a sparse array + return list(thing) # hope for the best + + +class ServerError(Exception): + def __unicode__(self): + return _('Error: Server is down') + def __str__(self): + return unicode(self).encode('utf-8') + +class CredentialsError(Exception): + def __unicode__(self): + return _('Error: Invalid Credentials') + def __str__(self): + return unicode(self).encode('utf-8') + +class VerificationRequired(Exception): + pass + +# Server companion.orerve.net uses a session cookie ("CompanionApp") to tie together login, verification +# and query. So route all requests through a single Session object which holds this state. + +class Session: + + STATE_NONE, STATE_INIT, STATE_AUTH, STATE_OK = range(4) + + def __init__(self): + self.state = Session.STATE_INIT + self.credentials = None + + # yuck suppress InsecurePlatformWarning + try: + from requests.packages import urllib3 + urllib3.disable_warnings() + except: + pass + + if platform=='win32' and getattr(sys, 'frozen', False): + os.environ['REQUESTS_CA_BUNDLE'] = join(dirname(sys.executable), 'cacert.pem') + + self.session = requests.Session() + self.session.headers['User-Agent'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D257' + self.session.cookies = LWPCookieJar(join(config.app_dir, 'cookies.txt')) + try: + self.session.cookies.load() + except IOError: + pass + + def login(self, username=None, password=None): + self.state = Session.STATE_INIT + if (not username or not password): + if not self.credentials: + raise CredentialsError() + else: + self.credentials = { 'email' : username, 'password' : password } + try: + r = self.session.post(URL_LOGIN, data = self.credentials, timeout=timeout) + except: + if __debug__: print_exc() + raise ServerError() + + if r.status_code != requests.codes.ok: + self.dump(r) + r.raise_for_status() + + if 'server error' in r.text: + self.dump(r) + raise ServerError() + elif 'Password' in r.text: + self.dump(r) + raise CredentialsError() + elif 'Verification Code' in r.text: + self.state = Session.STATE_AUTH + raise VerificationRequired() + else: + self.state = Session.STATE_OK + return r.status_code + + def verify(self, code): + if not code: + raise VerificationRequired() + r = self.session.post(URL_CONFIRM, data = {'code' : code}, timeout=timeout) + r.raise_for_status() + # verification doesn't actually return a yes/no, so log in again to determine state + try: + self.login() + except: + pass + + def query(self): + if self.state == Session.STATE_NONE: + raise Exception('General error') # Shouldn't happen - don't bother localizing + elif self.state == Session.STATE_INIT: + self.login() + elif self.state == Session.STATE_AUTH: + raise VerificationRequired() + try: + r = self.session.get(URL_QUERY, timeout=timeout) + except: + if __debug__: print_exc() + raise ServerError() + + if r.status_code != requests.codes.ok: + self.dump(r) + if r.status_code == requests.codes.forbidden or (r.history and r.url == URL_LOGIN): + # Start again - maybe our session cookie expired? + self.login() + return self.query() + + r.raise_for_status() + try: + data = r.json() + except: + self.dump(r) + raise ServerError() + + return data + + def close(self): + self.state = Session.STATE_NONE + try: + self.session.cookies.save() + self.session.close() + except: + pass + self.session = None + + # Fixup in-place anomalies in the recieved commodity data + def fixup(self, commodities): + i=0 + while i