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 = 60 # 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 category_map = { 'Narcotics' : 'Legal Drugs', 'Slaves' : 'Slavery', 'Waste ' : 'Waste', 'NonMarketable' : False, # Don't report these } 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 Explorer', 'cobramkiii' : 'Cobra MkIII', '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 MkIII', '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