diff --git a/EDMarketConnector.py b/EDMarketConnector.py index c9a1544d..4bd912e0 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -54,7 +54,6 @@ import commodity from commodity import COMMODITY_CSV import td import eddn -import edsm import coriolis import edshipyard import loadout @@ -68,7 +67,6 @@ from theme import theme SERVER_RETRY = 5 # retry pause for Companion servers [s] -EDSM_POLL = 0.1 # Limits on local clock drift from EDDN gateway DRIFT_THRESHOLD = 3 * 60 @@ -87,7 +85,6 @@ class AppWindow: self.holdofftime = config.getint('querytime') + companion.holdoff self.session = companion.Session() - self.edsm = edsm.EDSM() self.eddn = eddn.EDDN(self) self.w = master @@ -113,19 +110,15 @@ class AppWindow: self.cmdr_label = tk.Label(frame) self.ship_label = tk.Label(frame) - self.system_label = tk.Label(frame) self.cmdr_label.grid(row=1, column=0, sticky=tk.W) self.ship_label.grid(row=2, column=0, sticky=tk.W) - self.system_label.grid(row=3, column=0, sticky=tk.W) self.cmdr = tk.Label(frame, anchor=tk.W) self.ship = HyperlinkLabel(frame, url = self.shipyard_url) - self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.system_url, popup_copy = True) self.cmdr.grid(row=1, column=1, sticky=tk.EW) self.ship.grid(row=2, column=1, sticky=tk.EW) - self.system.grid(row=3, column=1, sticky=tk.EW) for plugin in plug.PLUGINS: appitem = plugin.get_app(frame) @@ -347,7 +340,6 @@ class AppWindow: self.cmdr_label['text'] = _('Cmdr') + ':' # Main window self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or # Multicrew role label in main window _('Ship')) + ':' # Main window - self.system_label['text'] = _('System') + ':' # Main window self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window if platform == 'darwin': self.menubar.entryconfigure(1, label=_('File')) # Menu title @@ -434,7 +426,6 @@ class AppWindow: hotkeymgr.play_good() self.status['text'] = _('Fetching data...') self.button['state'] = self.theme_button['state'] = tk.DISABLED - self.edit_menu.entryconfigure(0, state=tk.DISABLED) # Copy self.w.update_idletasks() try: @@ -469,11 +460,10 @@ class AppWindow: self.ship['text'] = companion.ship_map.get(data['ship']['name'].lower(), data['ship']['name']) monitor.state['ShipID'] = data['ship']['id'] monitor.state['ShipType'] = data['ship']['name'].lower() - if not monitor.system: - self.system['text'] = data['lastSystem']['name'] - self.system['image'] = '' - self.status['text'] = '' - self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy + + if data['commander'].get('credits') is not None: + monitor.state['Credits'] = data['commander']['credits'] + monitor.state['Loan'] = data['commander'].get('debt', 0) # stuff we can do when not docked err = plug.notify_newdata(data, monitor.is_beta) @@ -484,16 +474,6 @@ class AppWindow: if config.getint('output') & config.OUT_SHIP: loadout.export(data) - # Send flightlog EDSM if FSDJump failed to do so - if config.getint('output') & config.OUT_SYS_EDSM and self.edsm.result['img'] == self.edsm._IMG_ERROR and not monitor.is_beta and not monitor.captain and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs') and config.get('edsm_usernames')[config.get('cmdrs').index(monitor.cmdr)]: - try: - self.edsm.writelog(querytime, monitor.system, monitor.coordinates, monitor.state['ShipID']) - except Exception as e: - if __debug__: print_exc() - self.status['text'] = unicode(e) - play_bad = True - self.edsmpoll() - if not (config.getint('output') & ~config.OUT_SHIP & config.OUT_STATION_ANY): # no station data requested - we're done pass @@ -546,27 +526,6 @@ class AppWindow: if not old_status: self.status['text'] = '' - # Update credits and ship info and send to EDSM - if not monitor.is_beta and config.getint('output') & config.OUT_SYS_EDSM: - try: - if data['commander'].get('credits') is not None: - monitor.state['Credits'] = data['commander']['credits'] - monitor.state['Loan'] = data['commander'].get('debt', 0) - self.edsm.setcredits(monitor.state['Credits'], monitor.state['Loan']) - ship = companion.ship(data) - if ship != self.edsm.lastship: - self.edsm.updateship(monitor.state['ShipID'], - monitor.state['ShipType'], - [ - ('linkToCoriolis', coriolis.url(data, monitor.is_beta)), - ('linkToEDShipyard', edshipyard.url(data, monitor.is_beta)), - ]) - self.edsm.lastship = ship - except Exception as e: - # Not particularly important so silent on failure - if __debug__: print_exc() - - except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, partial(self.verify, self.getandsend)) @@ -627,7 +586,6 @@ class AppWindow: if not entry: return - system_changed = monitor.system and self.system['text'] != monitor.system # Update main window if monitor.cmdr and monitor.state['Captain']: @@ -649,63 +607,13 @@ class AppWindow: self.ship_label['text'] = _('Ship') + ':' # Main window self.ship['text'] = '' - if self.system['text'] != monitor.system: - self.system['text'] = monitor.system or '' - self.edsm.link(monitor.system) - self.edsmpoll() + self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy + if entry['event'] in ['Undocked', 'StartJump', 'SetUserShipName', 'ShipyardBuy', 'ShipyardSell', 'ShipyardSwap', 'ModuleBuy', 'ModuleSell', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis', 'JoinACrew']: self.status['text'] = '' # Periodically clear any old error self.w.update_idletasks() - # Send interesting events to EDSM - if config.getint('output') & config.OUT_SYS_EDSM and not monitor.is_beta and not monitor.captain and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs') and config.get('edsm_usernames')[config.get('cmdrs').index(monitor.cmdr)]: - try: - # Update system status on startup - if entry['event'] in [None, 'StartUp'] and monitor.mode and monitor.system: - self.edsm.lookup(monitor.system) - - # Send credits to EDSM on new game (but not on startup - data might be old) - if entry['event'] == 'LoadGame': - self.edsm.setcredits(monitor.state['Credits'], monitor.state['Loan']) - - # Send rank info to EDSM on startup or change - if entry['event'] in ['StartUp', 'Progress', 'Promotion'] and monitor.state['Rank']: - self.edsm.setranks(monitor.state['Rank']) - - # Send ship info to EDSM on startup or change - if entry['event'] in ['StartUp', 'Loadout', 'LoadGame', 'SetUserShipName'] and monitor.cmdr and monitor.state['ShipID']: - self.edsm.setshipid(monitor.state['ShipID']) - props = [] - if monitor.state['ShipIdent'] is not None: - props.append(('shipIdent', monitor.state['ShipIdent'])) - if monitor.state['ShipName'] is not None: - props.append(('shipName', monitor.state['ShipName'])) - if monitor.state['PaintJob'] is not None: - props.append(('paintJob', monitor.state['PaintJob'])) - self.edsm.updateship(monitor.state['ShipID'], monitor.state['ShipType'], props) - elif entry['event'] in ['ShipyardBuy', 'ShipyardSell']: - self.edsm.sellship(entry.get('SellShipID')) - - # Send materials info to EDSM on startup or change - if entry['event'] in ['StartUp', 'LoadGame', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis']: - self.edsm.setmaterials(monitor.state['Raw'], monitor.state['Manufactured'], monitor.state['Encoded']) - - # Send paintjob info to EDSM on change - if entry['event'] in ['ModuleBuy', 'ModuleSell'] and entry['Slot'] == 'PaintJob': - self.edsm.updateship(monitor.state['ShipID'], monitor.state['ShipType'], [('paintJob', monitor.state['PaintJob'])]) - - # Write EDSM log on change - if monitor.mode and entry['event'] in ['Location', 'FSDJump']: - self.edsm.writelog(timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), monitor.system, monitor.coordinates, monitor.state['ShipID']) - - except Exception as e: - if __debug__: print_exc() - self.status['text'] = unicode(e) - if not config.getint('hotkey_mute'): - hotkeymgr.play_bad() - self.edsmpoll() - - # Companion login - do this after EDSM so any EDSM errors don't mask login errors + # Companion login if entry['event'] in [None, 'StartUp', 'NewCommander', 'LoadGame'] and monitor.cmdr: if config.get('cmdrs') and monitor.cmdr in config.get('cmdrs'): prefs.make_current(monitor.cmdr) @@ -737,7 +645,7 @@ class AppWindow: return # Plugin backwards compatibility - if system_changed: + if monitor.mode and entry['event'] in ['StartUp', 'Location', 'FSDJump']: plug.notify_system_changed(timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), monitor.system, monitor.coordinates) # Auto-Update after docking @@ -812,13 +720,6 @@ class AppWindow: if not config.getint('hotkey_mute'): hotkeymgr.play_bad() - def edsmpoll(self): - result = self.edsm.result - if result['done']: - self.system['image'] = result['img'] - else: - self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) - def shipyard_url(self, shipname=None): if not monitor.cmdr or not monitor.mode: @@ -856,9 +757,6 @@ class AppWindow: assert False, config.getint('shipyard') return False - def system_url(self, text): - return text and self.edsm.result['url'] - def cooldown(self): if time() < self.holdofftime: self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window diff --git a/EDMarketConnector.wxs b/EDMarketConnector.wxs index 9db4d3d9..62ac7908 100644 --- a/EDMarketConnector.wxs +++ b/EDMarketConnector.wxs @@ -138,6 +138,9 @@ + + + @@ -447,6 +450,7 @@ + diff --git a/L10n/en.template b/L10n/en.template index e9289929..e998af9b 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -19,7 +19,7 @@ /* CQC rank. [stats.py] */ "Amateur" = "Amateur"; -/* EDSM setting. [prefs.py] */ +/* EDSM setting. [edsm.py] */ "API Key" = "API Key"; /* Tab heading in settings. [prefs.py] */ @@ -70,7 +70,7 @@ /* Ranking. [stats.py] */ "Combat" = "Combat"; -/* EDSM setting. [prefs.py] */ +/* EDSM setting. [edsm.py] */ "Commander Name" = "Commander Name"; /* Combat rank. [stats.py] */ @@ -139,7 +139,7 @@ /* Top rank. [stats.py] */ "Elite" = "Elite"; -/* Section heading in settings. [prefs.py] */ +/* Section heading in settings. [edsm.py] */ "Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map credentials"; /* Ranking. [stats.py] */ @@ -430,7 +430,7 @@ /* CQC rank. [stats.py] */ "Semi Professional" = "Semi Professional"; -/* [prefs.py] */ +/* [edsm.py] */ "Send flight log to Elite Dangerous Star Map" = "Send flight log to Elite Dangerous Star Map"; /* Output setting. [prefs.py] */ @@ -475,7 +475,7 @@ /* Explorer rank. [stats.py] */ "Surveyor" = "Surveyor"; -/* Main window. [EDMarketConnector.py] */ +/* Main window. [edsm.py] */ "System" = "System"; /* Appearance setting. [prefs.py] */ diff --git a/config.py b/config.py index 866fab6a..847f7278 100644 --- a/config.py +++ b/config.py @@ -102,7 +102,7 @@ class Config: # OUT_STAT = 64 # No longer available OUT_SHIP_CORIOLIS = 128 # Replaced by OUT_SHIP OUT_STATION_ANY = OUT_MKT_EDDN|OUT_MKT_TD|OUT_MKT_CSV|OUT_SHIP|OUT_SHIP_EDS|OUT_SHIP_CORIOLIS - OUT_SYS_EDSM = 256 + # OUT_SYS_EDSM = 256 # Now a plugin # OUT_SYS_AUTO = 512 # Now always automatic OUT_MKT_MANUAL = 1024 OUT_SYS_EDDN = 2048 diff --git a/edsm.py b/edsm.py deleted file mode 100644 index 5cb63aa2..00000000 --- a/edsm.py +++ /dev/null @@ -1,233 +0,0 @@ -import json -import threading -from sys import platform -import time -import urllib2 - -import Tkinter as tk - -from config import appname, applongname, appversion, config -from monitor import monitor - -if __debug__: - from traceback import print_exc - - -if platform=='darwin': - # mimimal implementation of requests interface since OpenSSL 0.9.8 on OSX - # fails to negotiate with Cloudflare unless cipher is forced - - import ssl - - class Response(object): - - def __init__(self, url, status_code, headers, content): - self.url = url - self.status_code = status_code - self.headers = headers # actually a mimetools.Message instance - self.content = content - - def json(self): - return json.loads(self.content) - - def raise_for_status(self): - if self.status_code >= 400: - raise urllib2.HTTPError(self.url, self.status_code, '%d %s' % (self.status_code, self.status_code >= 500 and 'Server Error' or 'Client Error'), self.headers, None) - - class Session(object): - - def __init__(self): - self.headers = {} - sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1) # Requires Python >= 2.7.9 on OSX >= 10.10 - sslcontext.set_ciphers("ECCdraft:HIGH:!aNULL") - self.opener = urllib2.build_opener(urllib2.HTTPSHandler(context=sslcontext)) - self.opener.addheaders = [('User-Agent', '%s/%s' % (appname, appversion))] - - def get(self, url, timeout=None, headers={}): - try: - h = self.opener.open(url, timeout=timeout) - r = Response(h.geturl(), h.code, h.info(), h.read()) - h.close() - return r - except urllib2.HTTPError, e: - return Response(url, e.code, {}, '') # requests doesn't raise exceptions for HTTP errors - except: - raise - - def close(self): - pass -else: - from requests import Session - - -class EDSM: - - _TIMEOUT = 10 - FAKE = ['CQC', 'Training', 'Destination'] # Fake systems that shouldn't be sent to EDSM - - def __init__(self): - self.result = { 'img': None, 'url': None, 'done': True } - self.syscache = set() # Cache URLs of systems with known coordinates - self.session = Session() - self.lastship = None # Description of last ship that we sent to EDSM - - # Can't be in class definition since can only call PhotoImage after window is created - EDSM._IMG_KNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7rMpvNV4q932VSuRiPjQQAOw==') # green circle - EDSM._IMG_UNKNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kCADs=') # red circle - EDSM._IMG_NEW = tk.PhotoImage(data = 'R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==') - EDSM._IMG_ERROR = tk.PhotoImage(data = 'R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2LplEAADs=') # BBC Mode 5 '?' - - # Call an EDSM endpoint with args (which should be quoted) - def call(self, endpoint, args, check_msgnum=True): - try: - idx = config.get('cmdrs').index(monitor.cmdr) - url = 'https://www.edsm.net/%s?commanderName=%s&apiKey=%s&fromSoftware=%s&fromSoftwareVersion=%s' % ( - endpoint, - urllib2.quote(config.get('edsm_usernames')[idx].encode('utf-8')), - urllib2.quote(config.get('edsm_apikeys')[idx]), - urllib2.quote(applongname), - urllib2.quote(appversion), - ) + args - r = self.session.get(url, timeout=EDSM._TIMEOUT) - r.raise_for_status() - reply = r.json() - if not check_msgnum: - return reply - (msgnum, msg) = reply['msgnum'], reply['msg'] - except: - if __debug__: print_exc() - raise Exception(_("Error: Can't connect to EDSM")) - - # Message numbers: 1xx = OK, 2xx = fatal error, 3xx = error (but not generated in practice), 4xx = ignorable errors - if msgnum // 100 not in (1,4): - raise Exception(_('Error: EDSM {MSG}').format(MSG=msg)) - else: - return reply - - # Just set link without doing a lookup - def link(self, system_name): - self.cancel_lookup() - if not system_name or system_name in self.FAKE: - self.result = { 'img': '', 'url': None, 'done': True, 'uncharted': False } - else: - self.result = { 'img': '', 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False } - - def lookup(self, system_name, known=0): - self.cancel_lookup() - - if system_name in self.FAKE: - self.result = { 'img': '', 'url': None, 'done': True, 'uncharted': False } - elif known or system_name in self.syscache: - self.result = { 'img': EDSM._IMG_KNOWN, 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False } - else: - self.result = { 'img': EDSM._IMG_ERROR, 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False } - data = self.call('api-v1/system', '&sysname=%s&coords=1' % urllib2.quote(system_name), check_msgnum=False) - - if data == -1 or not data: - # System not present - but don't create it on the assumption that the caller will - self.result['img'] = EDSM._IMG_NEW - self.result['uncharted'] = True - elif data.get('coords'): - self.result['img'] = EDSM._IMG_KNOWN - self.syscache.add(system_name) - else: - self.result['img'] = EDSM._IMG_UNKNOWN - self.result['uncharted'] = True - - # Asynchronous version of the above - def start_lookup(self, system_name, known=0): - self.cancel_lookup() - - if system_name in self.FAKE: - self.result = { 'img': '', 'url': None, 'done': True, 'uncharted': False } - elif known or system_name in self.syscache: - self.result = { 'img': EDSM._IMG_KNOWN, 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False } - else: - self.result = { 'img': '', 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': False, 'uncharted': False } - self.thread = threading.Thread(target = self.worker, name = 'EDSM worker', args = (system_name, self.result)) - self.thread.daemon = True - self.thread.start() - - def cancel_lookup(self): - self.thread = None # orphan any existing thread - self.result = { 'img': '', 'url': None, 'done': True } # orphan existing thread's results - - def worker(self, system_name, result): - try: - data = self.call('api-v1/system', '&sysname=%s&coords=1' % urllib2.quote(system_name), check_msgnum=False) - - if data == -1 or not data: - # System not present - create it - result['img'] = EDSM._IMG_NEW - result['uncharted'] = True - elif data.get('coords'): - result['img'] = EDSM._IMG_KNOWN - self.syscache.add(system_name) - else: - result['img'] = EDSM._IMG_UNKNOWN - result['uncharted'] = True - except: - if __debug__: print_exc() - result['img'] = EDSM._IMG_ERROR - result['done'] = True - - - # Send flight log and also do lookup - def writelog(self, timestamp, system_name, coordinates, shipid = None): - - if system_name in self.FAKE: - self.result = { 'img': '', 'url': None, 'done': True, 'uncharted': False } - return - - self.result = { 'img': EDSM._IMG_ERROR, 'url': 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system_name), 'done': True, 'uncharted': False } - - args = '&systemName=%s&dateVisited=%s' % ( - urllib2.quote(system_name), - urllib2.quote(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(timestamp))), - ) - if coordinates: - args += '&x=%.3f&y=%.3f&z=%.3f' % coordinates - if shipid is not None: - args += '&shipId=%d' % shipid - reply = self.call('api-logs-v1/set-log', args) - - if reply.get('systemCreated'): - self.result['img'] = EDSM._IMG_NEW - else: - self.result['img'] = EDSM._IMG_KNOWN - self.syscache.add(system_name) - - def setranks(self, ranks): - args = '' - if ranks: - for k,v in ranks.iteritems(): - if v is not None: - args += '&%s=%s' % (k, urllib2.quote('%d;%d' % v)) - if args: - self.call('api-commander-v1/set-ranks', args) - - def setcredits(self, balance, loan): - if balance is not None: - self.call('api-commander-v1/set-credits', '&balance=%d&loan=%d' % (balance, loan)) - - def setmaterials(self, raw, manufactured, encoded): - self.call('api-commander-v1/set-materials', "&type=data&values=%s" % json.dumps(encoded, separators = (',', ':'))) - materials = {} - materials.update(raw) - materials.update(manufactured) - self.call('api-commander-v1/set-materials', "&type=materials&values=%s" % json.dumps(materials, separators = (',', ':'))) - - def setshipid(self, shipid): - if shipid is not None: - self.call('api-commander-v1/set-ship-id', '&shipId=%d' % shipid) - - def updateship(self, shipid, shiptype, props=[]): - if shipid is not None and shiptype: - args = '&shipId=%d&type=%s' % (shipid, shiptype) - for (slot, thing) in props: - args += '&%s=%s' % (slot, urllib2.quote(unicode(thing))) - self.call('api-commander-v1/update-ship', args) - - def sellship(self, shipid): - if shipid is not None: - self.call('api-commander-v1/sell-ship', '&shipId=%d' % shipid) diff --git a/plugins/edsm.py b/plugins/edsm.py new file mode 100644 index 00000000..a38be968 --- /dev/null +++ b/plugins/edsm.py @@ -0,0 +1,357 @@ +# +# System display and EDSM lookup +# + +import json +import requests +import sys +import time +import urllib2 +from calendar import timegm + +import Tkinter as tk +from ttkHyperlinkLabel import HyperlinkLabel +import myNotebook as nb + +from config import appname, applongname, appversion, config +import companion +import coriolis +import edshipyard + +if __debug__: + from traceback import print_exc + +EDSM_POLL = 0.1 +_TIMEOUT = 10 +FAKE = ['CQC', 'Training', 'Destination'] # Fake systems that shouldn't be sent to EDSM + + +this = sys.modules[__name__] # For holding module globals +this.syscache = set() # Cache URLs of systems with known coordinates +this.session = requests.Session() +this.lastship = None # Description of last ship that we sent to EDSM +this.lastlookup = False # whether the last lookup succeeded + +# Game state +this.multicrew = False # don't send captain's ship info to EDSM while on a crew + + +def plugin_start(): + # Can't be earlier since can only call PhotoImage after window is created + this._IMG_KNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7rMpvNV4q932VSuRiPjQQAOw==') # green circle + this._IMG_UNKNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kCADs=') # red circle + this._IMG_NEW = tk.PhotoImage(data = 'R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==') + this._IMG_ERROR = tk.PhotoImage(data = 'R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2LplEAADs=') # BBC Mode 5 '?' + + # Migrate old settings + if not config.get('edsm_cmdrs'): + if isinstance(config.get('cmdrs'), list) and config.get('edsm_usernames') and config.get('edsm_apikeys'): + # Migrate <= 2.34 settings + config.set('edsm_cmdrs', config.get('cmdrs')) + elif config.get('edsm_cmdrname'): + # Migrate <= 2.25 settings. edsm_cmdrs is unknown at this time + config.set('edsm_usernames', [config.get('edsm_cmdrname') or '']) + config.set('edsm_apikeys', [config.get('edsm_apikey') or '']) + config.delete('edsm_cmdrname') + config.delete('edsm_apikey') + if config.getint('output') & 256: + # Migrate <= 2.34 setting + config.set('edsm_out', 1) + config.delete('edsm_autoopen') + config.delete('edsm_historical') + + return 'EDSM' + +def plugin_app(parent): + this.system_label = tk.Label(parent, text = _('System') + ':') # Main window + this.system = HyperlinkLabel(parent, compound=tk.RIGHT, url = None, popup_copy = True) + return (this.system_label, this.system) + +def plugin_prefs(parent, cmdr, is_beta): + + PADX = 10 + BUTTONX = 12 # indent Checkbuttons and Radiobuttons + PADY = 2 # close spacing + + frame = nb.Frame(parent) + frame.columnconfigure(1, weight=1) + + HyperlinkLabel(frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate + this.log = tk.IntVar(value = config.getint('edsm_out') and 1) + this.log_button = nb.Checkbutton(frame, text=_('Send flight log to Elite Dangerous Star Map'), variable=this.log, command=prefsvarchanged) + this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) + + nb.Label(frame).grid(sticky=tk.W) # big spacer + this.label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings + this.label.grid(columnspan=2, padx=PADX, sticky=tk.W) + + this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window + this.cmdr_label.grid(row=10, padx=PADX, sticky=tk.W) + this.cmdr_text = nb.Label(frame) + this.cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W) + + this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting + this.user_label.grid(row=11, padx=PADX, sticky=tk.W) + this.user = nb.Entry(frame) + this.user.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) + + this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting + this.apikey_label.grid(row=12, padx=PADX, sticky=tk.W) + this.apikey = nb.Entry(frame) + this.apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW) + + prefs_cmdr_changed(cmdr, is_beta) + + return frame + +def prefs_cmdr_changed(cmdr, is_beta): + this.log_button['state'] = cmdr and not is_beta and tk.NORMAL or tk.DISABLED + this.user['state'] = tk.NORMAL + this.user.delete(0, tk.END) + this.apikey['state'] = tk.NORMAL + this.apikey.delete(0, tk.END) + if cmdr: + this.cmdr_text['text'] = cmdr + (is_beta and ' [Beta]' or '') + cred = credentials(cmdr) + if cred: + this.user.insert(0, cred[0]) + this.apikey.insert(0, cred[1]) + else: + this.cmdr_text['text'] = _('None') # No hotkey/shortcut currently defined + this.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = cmdr and not is_beta and this.log.get() and tk.NORMAL or tk.DISABLED + +def prefsvarchanged(): + this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.log.get() and tk.NORMAL or tk.DISABLED + +def prefs_changed(cmdr, is_beta): + this.system_label['text'] = _('System') + ':' # Main window + config.set('edsm_out', this.log.get()) + + if cmdr and not is_beta: + cmdrs = config.get('edsm_cmdrs') + usernames = config.get('edsm_usernames') + apikeys = config.get('edsm_apikeys') + if cmdr in cmdrs: + idx = cmdrs.index(cmdr) + usernames[idx] = this.user.get().strip() + apikeys[idx] = this.apikey.get().strip() + else: + config.set('edsm_cmdrs', cmdrs + [cmdr]) + usernames.append(this.user.get().strip()) + apikeys.append(this.apikey.get().strip()) + config.set('edsm_usernames', usernames) + config.set('edsm_apikeys', apikeys) + + +def credentials(cmdr): + # Credentials for cmdr + if not cmdr: + return None + + cmdrs = config.get('edsm_cmdrs') + if not cmdrs: + # Migrate from <= 2.25 + cmdrs = [cmdr] + config.set('edsm_cmdrs', cmdrs) + + if cmdr in cmdrs: + idx = cmdrs.index(cmdr) + return (config.get('edsm_usernames')[idx], config.get('edsm_apikeys')[idx]) + else: + return None + + +def journal_entry(cmdr, is_beta, system, station, entry, state): + + # Update display + if this.system['text'] != system: + this.system['text'] = system or '' + this.system['image'] = '' + if not system or system in FAKE: + this.system['url'] = None + this.lastlookup = True + else: + this.system['url'] = 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system) + this.lastlookup = False + this.system.update_idletasks() + + this.multicrew = bool(state['Role']) + + # Send interesting events to EDSM + if config.getint('edsm_out') and not is_beta and not multicrew and credentials(cmdr): + try: + # Send credits to EDSM on new game (but not on startup - data might be old) + if entry['event'] == 'LoadGame': + setcredits(cmdr, state['Credits'], state['Loan']) + + # Send rank info to EDSM on startup or change + if entry['event'] in ['StartUp', 'Progress', 'Promotion'] and state['Rank']: + setranks(cmdr, state['Rank']) + + # Send ship info to EDSM on startup or change + if entry['event'] in ['StartUp', 'Loadout', 'LoadGame', 'SetUserShipName'] and cmdr and state['ShipID']: + setshipid(cmdr, state['ShipID']) + props = [] + if state['ShipIdent'] is not None: + props.append(('shipIdent', state['ShipIdent'])) + if state['ShipName'] is not None: + props.append(('shipName', state['ShipName'])) + if state['PaintJob'] is not None: + props.append(('paintJob', state['PaintJob'])) + updateship(cmdr, state['ShipID'], state['ShipType'], props) + elif entry['event'] in ['ShipyardBuy', 'ShipyardSell']: + sellship(cmdr, entry.get('SellShipID')) + + # Send materials info to EDSM on startup or change + if entry['event'] in ['StartUp', 'LoadGame', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis']: + setmaterials(cmdr, state['Raw'], state['Manufactured'], state['Encoded']) + + # Send paintjob info to EDSM on change + if entry['event'] in ['ModuleBuy', 'ModuleSell'] and entry['Slot'] == 'PaintJob': + updateship(cmdr, state['ShipID'], state['ShipType'], [('paintJob', state['PaintJob'])]) + + # Write EDSM log on startup and change + if system and entry['event'] in ['Location', 'FSDJump']: + this.lastlookup = False + writelog(cmdr, timegm(time.strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), system, 'StarPos' in entry and tuple(entry['StarPos']), state['ShipID']) + this.lastlookup = True + this.system.update_idletasks() + + except Exception as e: + if __debug__: print_exc() + return unicode(e) + + +def cmdr_data(data, is_beta): + + system = data['lastSystem']['name'] + + if not this.system['text']: + this.system['text'] = system + this.system['image'] = '' + if not system or system in FAKE: + this.system['url'] = None + this.lastlookup = True + else: + this.system['url'] = 'https://www.edsm.net/show-system?systemName=%s' % urllib2.quote(system) + this.lastlookup = False + this.system.update_idletasks() + + if config.getint('edsm_out') and not is_beta and not multicrew and credentials(data['commander']['name']): + # Send flightlog to EDSM if FSDJump failed to do so + if not this.lastlookup: + try: + this.lastlookup = False + this.writelog(data['commander']['name'], int(time.time()), system, None, data['ship']['id']) + this.lastlookup = True + this.system.update_idletasks() + except Exception as e: + if __debug__: print_exc() + return unicode(e) + + # Update credits and ship info and send to EDSM + try: + if data['commander'].get('credits') is not None: + setcredits(data['commander']['name'], data['commander']['credits'], data['commander'].get('debt', 0)) + ship = companion.ship(data) + if ship != this.lastship: + updateship(data['commander']['name'], + data['ship']['id'], + data['ship']['name'].lower(), + [ + ('linkToCoriolis', coriolis.url(data, is_beta)), + ('linkToEDShipyard', edshipyard.url(data, is_beta)), + ]) + this.lastship = ship + except Exception as e: + # Not particularly important so silent on failure + if __debug__: print_exc() + + +# Call an EDSM endpoint with args (which should be quoted) +def call(cmdr, endpoint, args, check_msgnum=True): + try: + (username, apikey) = credentials(cmdr) + url = 'https://www.edsm.net/%s?commanderName=%s&apiKey=%s&fromSoftware=%s&fromSoftwareVersion=%s' % ( + endpoint, + urllib2.quote(username.encode('utf-8')), + urllib2.quote(apikey), + urllib2.quote(applongname), + urllib2.quote(appversion), + ) + args + r = this.session.get(url, timeout=_TIMEOUT) + r.raise_for_status() + reply = r.json() + if not check_msgnum: + return reply + (msgnum, msg) = reply['msgnum'], reply['msg'] + except: + if __debug__: print_exc() + raise Exception(_("Error: Can't connect to EDSM")) + + # Message numbers: 1xx = OK, 2xx = fatal error, 3xx = error (but not generated in practice), 4xx = ignorable errors + if msgnum // 100 not in (1,4): + raise Exception(_('Error: EDSM {MSG}').format(MSG=msg)) + else: + return reply + + +# Send flight log and also do lookup +def writelog(cmdr, timestamp, system_name, coordinates, shipid = None): + + if system_name in FAKE: + return + + args = '&systemName=%s&dateVisited=%s' % ( + urllib2.quote(system_name), + urllib2.quote(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(timestamp))), + ) + if coordinates: + args += '&x=%.3f&y=%.3f&z=%.3f' % coordinates + if shipid is not None: + args += '&shipId=%d' % shipid + try: + reply = call(cmdr, 'api-logs-v1/set-log', args) + if reply.get('systemCreated'): + this.system['image'] = this._IMG_NEW + else: + this.system['image'] = this._IMG_KNOWN + this.syscache.add(system_name) + except: + this.system['image'] = this._IMG_ERROR + raise + +def setranks(cmdr, ranks): + args = '' + if ranks: + for k,v in ranks.iteritems(): + if v is not None: + args += '&%s=%s' % (k, urllib2.quote('%d;%d' % v)) + if args: + call(cmdr, 'api-commander-v1/set-ranks', args) + +def setcredits(cmdr, balance, loan): + if balance is not None: + call(cmdr, 'api-commander-v1/set-credits', '&balance=%d&loan=%d' % (balance, loan)) + +def setmaterials(cmdr, raw, manufactured, encoded): + call(cmdr, 'api-commander-v1/set-materials', "&type=data&values=%s" % json.dumps(encoded, separators = (',', ':'))) + materials = {} + materials.update(raw) + materials.update(manufactured) + call(cmdr, 'api-commander-v1/set-materials', "&type=materials&values=%s" % json.dumps(materials, separators = (',', ':'))) + +def setshipid(cmdr, shipid): + if shipid is not None: + call(cmdr, 'api-commander-v1/set-ship-id', '&shipId=%d' % shipid) + +def updateship(cmdr, shipid, shiptype, props=[]): + if shipid is not None and shiptype: + args = '&shipId=%d&type=%s' % (shipid, shiptype) + for (slot, thing) in props: + args += '&%s=%s' % (slot, urllib2.quote(unicode(thing))) + call(cmdr, 'api-commander-v1/update-ship', args) + +def sellship(cmdr, shipid): + if shipid is not None: + call(cmdr, 'api-commander-v1/sell-ship', '&shipId=%d' % shipid) diff --git a/prefs.py b/prefs.py index 3b80c5d5..82d91401 100644 --- a/prefs.py +++ b/prefs.py @@ -182,36 +182,6 @@ class PreferencesDialog(tk.Toplevel): notebook.add(eddnframe, text='EDDN') # Not translated - - edsmframe = nb.Frame(notebook) - edsmframe.columnconfigure(1, weight=1) - - HyperlinkLabel(edsmframe, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate - self.edsm_log = tk.IntVar(value = (output & config.OUT_SYS_EDSM) and 1) - self.edsm_log_button = nb.Checkbutton(edsmframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.edsm_log, command=self.outvarchanged) - self.edsm_log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) - - nb.Label(edsmframe).grid(sticky=tk.W) # big spacer - self.edsm_label = HyperlinkLabel(edsmframe, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings - self.edsm_label.grid(columnspan=2, padx=PADX, sticky=tk.W) - - self.edsm_cmdr_label = nb.Label(edsmframe, text=_('Cmdr')) # Main window - self.edsm_cmdr_label.grid(row=10, padx=PADX, sticky=tk.W) - self.edsm_cmdr_text = nb.Label(edsmframe) - self.edsm_cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W) - - self.edsm_user_label = nb.Label(edsmframe, text=_('Commander Name')) # EDSM setting - self.edsm_user_label.grid(row=11, padx=PADX, sticky=tk.W) - self.edsm_user = nb.Entry(edsmframe) - self.edsm_user.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) - - self.edsm_apikey_label = nb.Label(edsmframe, text=_('API Key')) # EDSM setting - self.edsm_apikey_label.grid(row=12, padx=PADX, sticky=tk.W) - self.edsm_apikey = nb.Entry(edsmframe) - self.edsm_apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW) - - notebook.add(edsmframe, text='EDSM') # Not translated - # build plugin prefs tabs for plugin in plug.PLUGINS: plugframe = plugin.get_prefs(notebook, monitor.cmdr, monitor.is_beta) @@ -406,22 +376,14 @@ class PreferencesDialog(tk.Toplevel): self.username.delete(0, tk.END) self.password['state'] = tk.NORMAL self.password.delete(0, tk.END) - self.edsm_user['state'] = tk.NORMAL - self.edsm_user.delete(0, tk.END) - self.edsm_apikey['state'] = tk.NORMAL - self.edsm_apikey.delete(0, tk.END) if monitor.cmdr and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs'): config_idx = config.get('cmdrs').index(monitor.cmdr) self.username.insert(0, config.get('fdev_usernames')[config_idx] or '') self.password.insert(0, config.get_password(config.get('fdev_usernames')[config_idx]) or '') - self.edsm_user.insert(0, config.get('edsm_usernames')[config_idx] or '') - self.edsm_apikey.insert(0, config.get('edsm_apikeys')[config_idx] or '') elif monitor.cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'): # migration from <= 2.25 self.username.insert(0, config.get('username') or '') self.password.insert(0, config.get('password') or '') - self.edsm_user.insert(0,config.get('edsm_cmdrname') or '') - self.edsm_apikey.insert(0, config.get('edsm_apikey') or '') if self.cmdr is not False: # Don't notify on first run plug.notify_prefs_cmdr_changed(monitor.cmdr, monitor.is_beta) self.cmdr = monitor.cmdr @@ -452,11 +414,6 @@ class PreferencesDialog(tk.Toplevel): self.eddn_system_button['state']= logvalid and tk.NORMAL or tk.DISABLED self.eddn_delay_button['state'] = logvalid and eddn.replayfile and self.eddn_system.get() and tk.NORMAL or tk.DISABLED - self.edsm_log_button['state'] = logvalid and tk.NORMAL or tk.DISABLED - edsm_state = logvalid and monitor.cmdr and self.edsm_log.get() and tk.NORMAL or tk.DISABLED - self.edsm_label['state'] = self.edsm_cmdr_label['state'] = self.edsm_user_label['state'] = self.edsm_apikey_label['state'] = edsm_state - self.edsm_cmdr_text['state'] = self.edsm_user['state'] = self.edsm_apikey['state'] = edsm_state - def filebrowse(self, title, pathvar): if platform != 'win32': import tkFileDialog @@ -598,14 +555,10 @@ class PreferencesDialog(tk.Toplevel): if not config.get('cmdrs'): config.set('cmdrs', [self.cmdr]) config.set('fdev_usernames', [self.username.get().strip()]) - config.set('edsm_usernames', [self.edsm_user.get().strip()]) - config.set('edsm_apikeys', [self.edsm_apikey.get().strip()]) else: idx = config.get('cmdrs').index(self.cmdr) if self.cmdr in config.get('cmdrs') else -1 _putfirst('cmdrs', idx, self.cmdr) _putfirst('fdev_usernames', idx, self.username.get().strip()) - _putfirst('edsm_usernames', idx, self.edsm_user.get().strip()) - _putfirst('edsm_apikeys', idx, self.edsm_apikey.get().strip()) config.set('output', (self.out_td.get() and config.OUT_MKT_TD) + @@ -614,8 +567,7 @@ class PreferencesDialog(tk.Toplevel): (self.out_ship.get() and config.OUT_SHIP) + (self.eddn_station.get() and config.OUT_MKT_EDDN) + (self.eddn_system.get() and config.OUT_SYS_EDDN) + - (self.eddn_delay.get() and config.OUT_SYS_DELAY) + - (self.edsm_log.get() and config.OUT_SYS_EDSM)) + (self.eddn_delay.get() and config.OUT_SYS_DELAY)) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) logdir = self.logdir.get() @@ -764,12 +716,8 @@ def migrate(current_cmdr): config.set_password(config.get('username'), config.get('password')) # Can fail on Linux config.set('cmdrs', [current_cmdr]) config.set('fdev_usernames', [config.get('username')]) - config.set('edsm_usernames', [config.get('edsm_cmdrname') or '']) - config.set('edsm_apikeys', [config.get('edsm_apikey') or '']) config.delete('username') config.delete('password') - config.delete('edsm_cmdrname') - config.delete('edsm_apikey') # Put current Cmdr first in the lists def make_current(current_cmdr): @@ -777,8 +725,6 @@ def make_current(current_cmdr): idx = config.get('cmdrs').index(current_cmdr) _putfirst('cmdrs', idx) _putfirst('fdev_usernames', idx) - _putfirst('edsm_usernames', idx) - _putfirst('edsm_apikeys', idx) def _putfirst(setting, config_idx, new_value=None): assert config_idx>=0 or new_value is not None, (setting, config_idx, new_value) diff --git a/setup.py b/setup.py index 012fd763..bd872c2a 100755 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ if sys.platform=='darwin': APP = 'EDMarketConnector.py' APPCMD = 'EDMC.py' SHORTVERSION = ''.join(VERSION.split('.')[:3]) -PLUGINS = [ 'plugins/eddb.py' ] +PLUGINS = [ 'plugins/eddb.py', 'plugins/edsm.py' ] if sys.platform=='darwin': OPTIONS = { 'py2app':