ED_Tools initial commit.

This commit is contained in:
Anna Rose 2015-10-25 20:51:48 -04:00
commit ee49936330
8 changed files with 471 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.pyc

20
Readme.md Normal file
View File

@ -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)

276
companion.py Normal file
View File

@ -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<len(commodities):
commodity = commodities[i]
# Check all required numeric fields are present and are numeric
# Catches "demandBracket": "" for some phantom commodites in ED 1.3
for thing in ['buyPrice', 'sellPrice', 'demand', 'demandBracket', 'stock', 'stockBracket']:
if not isinstance(commodity.get(thing), numbers.Number):
if __debug__: print 'Invalid "%s":"%s" (%s) for "%s"' % (thing, commodity.get(thing), type(commodity.get(thing)), commodity.get('name', ''))
break
else:
if not category_map.get(commodity['categoryname'], True): # Check marketable
pass
elif not commodity.get('categoryname', '').strip():
if __debug__: print 'Missing "categoryname" for "%s"' % commodity.get('name', '')
elif not commodity.get('name', '').strip():
if __debug__: print 'Missing "name" for a commodity in "%s"' % commodity.get('categoryname', '')
elif not commodity['demandBracket'] in range(4):
if __debug__: print 'Invalid "demandBracket":"%s" for "%s"' % (commodity['demandBracket'], commodity['name'])
elif not commodity['stockBracket'] in range(4):
if __debug__: print 'Invalid "stockBracket":"%s" for "%s"' % (commodity['stockBracket'], commodity['name'])
else:
# Rewrite text fields
commodity['categoryname'] = category_map.get(commodity['categoryname'].strip(),
commodity['categoryname'].strip())
commodity['name'] = commodity_map.get(commodity['name'].strip(),
commodity['name'].strip())
# Force demand and stock to zero if their corresponding bracket is zero
# Fixes spurious "demand": 1 in ED 1.3
if not commodity['demandBracket']:
commodity['demand'] = 0
if not commodity['stockBracket']:
commodity['stock'] = 0
# We're good
i+=1
continue
# Skip the commodity
commodities.pop(i)
return commodities
def dump(self, r):
if __debug__:
print 'Status\t%s' % r.status_code
print 'URL\t%s' % r.url
print 'Headers\t%s' % r.headers
print ('Content:\n%s' % r.text).encode('utf-8')

19
config.py Normal file
View File

@ -0,0 +1,19 @@
# This class exists to satisfy dependencies in companion.py.
# It's mostly a stub; I don't want to pull EDMC's config.py
# without first understanding more of its semantics.
# For username/password settings, see Readme.md
from os import path
import os
class Config():
app_dir = path.join(path.expanduser('~'), '.ed_tool/')
def __init__(self):
try:
os.mkdir(self.app_dir)
except OSError:
pass # Ignore existing directory
config = Config()

27
elite_info.py Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/python
from companion import Session, VerificationRequired
import os
import utils
def main():
settings = utils.get_settings()
session = Session()
try:
session.login(settings.get('ed_companion', 'username'), settings.get('ed_companion', 'password'))
except VerificationRequired:
code = raw_input("Input Verification Code: ")
session.verify(code)
data = session.query()
# Now we have the data!
print "Commander %s" % data['commander']['name']
print "Credits: %s" % data['commander']['credits']
print "Location: %s" % data['lastSystem']['name']
session.close()
if __name__ == "__main__":
main()

72
inara.py Normal file
View File

@ -0,0 +1,72 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Creates connections to inara.cz to retrieve and update player info.
import requests
import sys
URL_BASE = "http://inara.cz/"
URL_LOGIN = URL_BASE
URL_CMDR = URL_BASE + "cmdr/"
class ServerError(Exception):
pass
class CredentialsError(Exception):
pass
class Session(requests.Session):
def __init__(self):
requests.Session.__init__(self)
def inara_login(self, username, password):
if (not username or not password):
raise CredentialsError()
data = {
"loginid": username,
"loginpass": password,
"formact": "ENT_LOGIN",
"location": "intro"
}
self._inara_handled_request(self.post, URL_LOGIN, data=data)
def inara_update_credits(self, credits):
data = {
"location": "cmdr",
"formact": "USER_CREDITS_SET",
"playercredits": credits,
"playercreditsassets": None,
"oass": 48126920,
}
self._inara_handled_request(self.post, URL_CMDR, data=data)
def inara_update_location(self, location):
data = {
'formact': 'USER_LOCATION_SET',
'playercurloc': location
}
self._inara_handled_request(self.post, URL_CMDR, data=data)
def _inara_handled_request(self, func, url, data=None):
r = func(url, data=data)
r.raise_for_status()
def _inara_dump(self, r):
print "Request:"
print 'Headers\t%s' % r.request.headers
print 'Data\t%s' % r.request.body
print ""
print "Response:"
print 'Status\t%s' % r.status_code
print 'URL\t%s' % r.url
print 'Headers\t%s' % r.headers
print 'Logged in? ', "Yes" if "Logout" in r.text else "No"
print ""

24
update_inara.py Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/python
import companion
import inara
import utils
settings = utils.get_settings()
companion_session = companion.Session()
inara_session = inara.Session()
try:
companion_session.login(settings.get('ed_companion', 'username'), settings.get('ed_companion', 'password'))
except companion.VerificationRequired:
code = raw_input("Input Verification Code: ")
companion_session.verify(code)
inara_session.inara_login(settings.get('inara', 'username'), settings.get('inara', 'password'))
inara_session._inara_handled_request(inara_session.post, inara.URL_BASE)
data = companion_session.query()
inara_session.inara_update_credits(data['commander']['credits'])
inara_session.inara_update_location(data['lastSystem']['name'])
companion_session.close()

32
utils.py Normal file
View File

@ -0,0 +1,32 @@
from ConfigParser import ConfigParser
from config import config
import os
def get_settings():
"""
Try to read the settings from file into ConfigParser object.
If the config file isn't found, initialize it and bail.
"""
filename = os.path.join(config.app_dir, 'settings.conf')
settings = ConfigParser()
if os.path.isfile(filename):
settings.read(filename)
else:
init_settings(settings, filename)
return settings
def init_settings(settings, filename):
settings.add_section('ed_companion')
settings.add_section('inara')
settings.set('ed_companion', 'username', '')
settings.set('ed_companion', 'password', '')
settings.set('inara', 'username', '')
settings.set('inara', 'password', '')
with open(filename, 'wb') as f:
settings.write(f)
raise Exception("Missing configuration. Please edit %s and run the program again." % filename)