1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-15 08:40:34 +03:00
Jonathan Harris b28355d182 Merge EDSM-NET/master
Squashed commit of the following:

commit a13a5c431396a131aad21927f2b4cc43b948d417
Author: AnthorNet <anthor.net@gmail.com>
Date:   Sun Dec 17 16:51:58 2017 +0100

    Ready for testing

commit 3e5915075b20e824580e5c3d879bce467dcb8b8d
Author: AnthorNet <anthor.net@gmail.com>
Date:   Sat Dec 16 21:47:22 2017 +0100

    Fix message json encoding

commit 868030578422f9c8568d5ba21d6f3aa132c01232
Author: AnthorNet <anthor.net@gmail.com>
Date:   Sat Dec 16 17:54:10 2017 +0100

    Fix double Dict conversion

commit 7693031f6f09c7119c51f91abbed7241e5ad6a1c
Author: AnthorNet <anthor.net@gmail.com>
Date:   Sat Dec 16 17:31:19 2017 +0100

    Fix transient state format

commit 54c96e31774ba5bfa2d19ac45b07d355dbefe46b
Author: AnthorNet <anthor.net@gmail.com>
Date:   Sat Dec 16 17:25:02 2017 +0100

    Fix calling discarded event callback

commit 24d006947f3bbb6607f893042d783c9232402c28
Author: AnthorNet <anthor.net@gmail.com>
Date:   Fri Dec 15 15:11:28 2017 +0100

    Retry Discarded Event list if empty

commit 4defedba965489d2d0c009189a193f9ecabc84fd
Author: AnthorNet <anthor.net@gmail.com>
Date:   Fri Dec 15 14:43:28 2017 +0100

    Prepare API migration
2018-01-02 16:35:06 +00:00

335 lines
13 KiB
Python

#
# System display and EDSM lookup
#
import json
import requests
import sys
import time
import urllib2
from calendar import timegm
from Queue import Queue
from threading import Thread
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
import outfitting
import plug
if __debug__:
from traceback import print_exc
EDSM_POLL = 0.1
_TIMEOUT = 20
FAKE = ['CQC', 'Training', 'Destination'] # Fake systems that shouldn't be sent to EDSM
this = sys.modules[__name__] # For holding module globals
this.session = requests.Session()
this.queue = Queue() # Items to be sent to EDSM by worker thread
this.discardedEvents = [] # List discarded events from 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')
this.thread = Thread(target = worker, name = 'EDSM worker')
this.thread.daemon = True
this.thread.start()
# Get the last Discarded events from EDSM
if __debug__: print('Calling Discarded Events API endpoint')
this.queue.put(('https://www.edsm.net/api-journal-v1/discard', {}, discarded_callback))
return 'EDSM'
def plugin_app(parent):
this.system_label = tk.Label(parent, text = _('System') + ':') # Main window
this.system = HyperlinkLabel(parent, compound=tk.RIGHT, popup_copy = True)
this.system.bind_all('<<EDSMStatus>>', update_status)
return (this.system_label, this.system)
def plugin_stop():
# Signal thread to close and wait for it
this.queue.put(None)
this.thread.join()
this.thread = None
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 and Cmdr status to EDSM'), 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.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = this.log.get() and this.log_button['state'] 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') or []
apikeys = config.get('edsm_apikeys') or []
if cmdr in cmdrs:
idx = cmdrs.index(cmdr)
usernames.extend([''] * (1 + idx - len(usernames)))
usernames[idx] = this.user.get().strip()
apikeys.extend([''] * (1 + idx - len(apikeys)))
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 and config.get('edsm_usernames') and config.get('edsm_apikeys'):
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'])
# If Discarded events still emtpy, retry
if not this.discardedEvents:
this.queue.put(('https://www.edsm.net/api-journal-v1/discard', {}, discarded_callback))
# Send interesting events to EDSM
if config.getint('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr):
try:
# Send the journal entry if not in the discarded events
if entry['event'] not in this.discardedEvents:
sendEntry(cmdr, system, station, entry, state)
except Exception as e:
if __debug__: print_exc()
return unicode(e)
# Update system data
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()
# Worker thread
def worker():
while True:
item = this.queue.get()
if not item:
return # Closing
else:
(url, data, callback) = item
retrying = 0
while retrying < 3:
try:
#if __debug__: print(data)
r = this.session.post(url, data=data, timeout=_TIMEOUT)
r.raise_for_status()
reply = r.json()
if callback:
callback(reply)
else:
(msgnum, msg) = reply['msgnum'], reply['msg']
if msgnum // 100 not in (1,4):
if __debug__: print(_('Error: EDSM {MSG}').format(MSG=msg))
plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg))
break
except:
if __debug__: print_exc()
retrying += 1
else:
if callback:
callback(None)
else:
plug.show_error(_("Error: Can't connect to EDSM"))
# Queue a call to the EDSM journal API with args (which should be quoted)
def call(cmdr, args, callback=None):
(username, apikey) = credentials(cmdr)
args = dict(args)
args['commanderName'] = username
args['apiKey'] = apikey
args['fromSoftware'] = applongname
args['fromSoftwareVersion'] = appversion
this.queue.put(('https://www.edsm.net/api-journal-v1', args, callback))
# Send journal entry
def sendEntry(cmdr, system, station, entry, state):
if entry['event'] in this.discardedEvents:
return
if entry['event'] in ['Location', 'FSDJump'] and entry['StarSystem'] in FAKE:
return
# Update callback on needed events
if entry['event'] in ['Location', 'FSDJump']:
eventCallback = writelog_callback
else:
eventCallback = null_callback
# Introduce transient states into the event
entry['_systemName'] = system;
#entry['_systemCoordinates'] = gameStatus.coordinates; #TODO: Track system coordinates
entry['_stationName'] = station;
entry['_shipId'] = state['ShipID'];
# Make the API call
call(cmdr, { 'message': json.dumps(entry) }, eventCallback)
def writelog_callback(reply):
#if __debug__: print(reply)
this.lastlookup = reply['events'][0] # Get first response while we send events one by one
this.system.event_generate('<<EDSMStatus>>', when="tail") # calls update_status in main thread
def update_status(event=None):
reply = this.lastlookup
# Message numbers: 1xx = OK, 2xx = fatal error, 3xx = error (but not generated in practice), 4xx = ignorable errors
if not reply:
this.system['image'] = this._IMG_ERROR
plug.show_error(_("Error: Can't connect to EDSM"))
elif reply['msgnum'] // 100 not in (1,4):
this.system['image'] = this._IMG_ERROR
plug.show_error(_('Error: EDSM {MSG}').format(MSG=reply['msg']))
elif reply.get('systemCreated'):
this.system['image'] = this._IMG_NEW
else:
this.system['image'] = this._IMG_KNOWN
# When we don't care about return msgnum from EDSM
def null_callback(reply):
if not reply:
plug.show_error(_("Error: Can't connect to EDSM"))# When we don't care about return msgnum from EDSM
# Grab the discarded list
def discarded_callback(reply):
if not reply:
plug.show_error(_("Error: Can't get EDSM DIscarded Events"))
else:
this.discardedEvents = reply