mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-21 11:27:38 +03:00
PKCE OAuth2 access to cAPI
This commit is contained in:
parent
01b7f13148
commit
f17f5d3f25
17
EDMC.py
17
EDMC.py
@ -109,17 +109,13 @@ try:
|
||||
if cmdr.lower() == args.p.lower():
|
||||
break
|
||||
else:
|
||||
raise companion.CredentialsError
|
||||
username = config.get('fdev_usernames')[idx]
|
||||
companion.session.login(username, config.get_password(username), monitor.is_beta)
|
||||
elif config.get('cmdrs'):
|
||||
raise companion.CredentialsError()
|
||||
companion.session.login(cmdr, monitor.is_beta)
|
||||
else:
|
||||
cmdrs = config.get('cmdrs') or []
|
||||
if monitor.cmdr not in cmdrs:
|
||||
raise companion.CredentialsError
|
||||
username = config.get('fdev_usernames')[cmdrs.index(monitor.cmdr)]
|
||||
companion.session.login(username, config.get_password(username), monitor.is_beta)
|
||||
else: # <= 2.25 not yet migrated
|
||||
companion.session.login(config.get('username'), config.get('password'), monitor.is_beta)
|
||||
raise companion.CredentialsError()
|
||||
companion.session.login(monitor.cmdr, monitor.is_beta)
|
||||
querytime = int(time())
|
||||
data = companion.session.station()
|
||||
config.set('querytime', querytime)
|
||||
@ -237,6 +233,3 @@ except companion.SKUError as e:
|
||||
except companion.CredentialsError as e:
|
||||
sys.stderr.write('Invalid Credentials\n')
|
||||
sys.exit(EXIT_CREDENTIALS)
|
||||
except companion.VerificationRequired:
|
||||
sys.stderr.write('Verification Required\n')
|
||||
sys.exit(EXIT_VERIFICATION)
|
||||
|
@ -53,6 +53,7 @@ import prefs
|
||||
import plug
|
||||
from hotkey import hotkeymgr
|
||||
from monitor import monitor
|
||||
from protocol import ProtocolHandler
|
||||
from dashboard import dashboard
|
||||
from theme import theme
|
||||
|
||||
@ -69,6 +70,9 @@ class AppWindow:
|
||||
|
||||
def __init__(self, master):
|
||||
|
||||
# Start a protocol handler to handle cAPI registration
|
||||
self.protocolhandler = ProtocolHandler(master)
|
||||
|
||||
self.holdofftime = config.getint('querytime') + companion.holdoff
|
||||
|
||||
self.w = master
|
||||
@ -76,7 +80,6 @@ class AppWindow:
|
||||
self.w.rowconfigure(0, weight=1)
|
||||
self.w.columnconfigure(0, weight=1)
|
||||
|
||||
self.authdialog = None
|
||||
self.prefsdialog = None
|
||||
|
||||
plug.load_plugins(master)
|
||||
@ -275,6 +278,7 @@ class AppWindow:
|
||||
self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring
|
||||
self.w.bind_all('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring
|
||||
self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar
|
||||
self.w.bind_all('<<CompanionAuthEvent>>', self.auth) # cAPI auth
|
||||
self.w.bind_all('<<Quit>>', self.onexit) # Updater
|
||||
|
||||
# Load updater after UI creation (for WinSparkle)
|
||||
@ -288,14 +292,10 @@ class AppWindow:
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# Migration from <= 2.25
|
||||
if not config.get('cmdrs') and config.get('username') and config.get('password'):
|
||||
try:
|
||||
companion.session.login(config.get('username'), config.get('password'), False)
|
||||
data = companion.session.profile()
|
||||
prefs.migrate(data['commander']['name'])
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
# Migration from <= 3.30
|
||||
for username in config.get('fdev_usernames') or []:
|
||||
config.delete_password(username)
|
||||
config.delete('fdev_usernames')
|
||||
config.delete('username')
|
||||
config.delete('password')
|
||||
config.delete('logdir')
|
||||
@ -366,15 +366,8 @@ class AppWindow:
|
||||
self.button['state'] = self.theme_button['state'] = tk.DISABLED
|
||||
self.w.update_idletasks()
|
||||
try:
|
||||
if not monitor.cmdr or not config.get('cmdrs') or monitor.cmdr not in config.get('cmdrs'):
|
||||
raise companion.CredentialsError()
|
||||
idx = config.get('cmdrs').index(monitor.cmdr)
|
||||
username = config.get('fdev_usernames')[idx]
|
||||
companion.session.login(username, config.get_password(username), monitor.is_beta)
|
||||
self.status['text'] = ''
|
||||
except companion.VerificationRequired:
|
||||
if not self.authdialog:
|
||||
self.authdialog = prefs.AuthenticationDialog(self.w, partial(self.verify, self.login))
|
||||
if companion.session.login(monitor.cmdr, monitor.is_beta):
|
||||
self.status['text'] = ''
|
||||
except companion.ServerError as e:
|
||||
self.status['text'] = unicode(e)
|
||||
except Exception as e:
|
||||
@ -382,19 +375,6 @@ class AppWindow:
|
||||
self.status['text'] = unicode(e)
|
||||
self.cooldown()
|
||||
|
||||
# callback after verification code
|
||||
def verify(self, callback, code):
|
||||
self.authdialog = None
|
||||
try:
|
||||
companion.session.verify(code)
|
||||
config.save() # Save settings now for use by command-line app
|
||||
except Exception as e:
|
||||
if __debug__: print_exc()
|
||||
self.button['state'] = self.theme_button['state'] = tk.NORMAL
|
||||
self.status['text'] = unicode(e)
|
||||
else:
|
||||
return callback() # try again
|
||||
|
||||
def getandsend(self, event=None, retrying=False):
|
||||
|
||||
auto_update = not event
|
||||
@ -481,10 +461,6 @@ class AppWindow:
|
||||
if config.getint('output') & config.OUT_MKT_TD:
|
||||
td.export(fixed)
|
||||
|
||||
except companion.VerificationRequired:
|
||||
if not self.authdialog:
|
||||
self.authdialog = prefs.AuthenticationDialog(self.w, partial(self.verify, self.getandsend))
|
||||
|
||||
# Companion API problem
|
||||
except (companion.ServerError, companion.ServerLagging) as e:
|
||||
if retrying:
|
||||
@ -570,15 +546,10 @@ class AppWindow:
|
||||
|
||||
# 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)
|
||||
if not config.get('cmdrs') or monitor.cmdr not in config.get('cmdrs'):
|
||||
config.set('cmdrs', (config.get('cmdrs') or []) + [monitor.cmdr])
|
||||
if config.getint('output') & config.OUT_MKT_EDDN:
|
||||
self.login()
|
||||
elif config.get('cmdrs') and entry['event'] == 'NewCommander':
|
||||
cmdrs = config.get('cmdrs')
|
||||
cmdrs[0] = monitor.cmdr # New Cmdr uses same credentials as old
|
||||
config.set('cmdrs', cmdrs)
|
||||
elif not self.prefsdialog:
|
||||
self.prefsdialog = prefs.PreferencesDialog(self.w, self.postprefs) # First run or failed migration
|
||||
|
||||
if not entry['event'] or not monitor.mode:
|
||||
return # Startup or in CQC
|
||||
@ -603,6 +574,18 @@ class AppWindow:
|
||||
if entry['event'] in ['StartUp', 'Location', 'Docked'] and monitor.station and not config.getint('output') & config.OUT_MKT_MANUAL and config.getint('output') & config.OUT_STATION_ANY:
|
||||
self.w.after(int(SERVER_RETRY * 1000), self.getandsend)
|
||||
|
||||
# cAPI auth
|
||||
def auth(self, event=None):
|
||||
try:
|
||||
companion.session.auth_callback(self.protocolhandler.lastpayload)
|
||||
self.status['text'] = ''
|
||||
except companion.ServerError as e:
|
||||
self.status['text'] = unicode(e)
|
||||
except Exception as e:
|
||||
if __debug__: print_exc()
|
||||
self.status['text'] = unicode(e)
|
||||
self.cooldown()
|
||||
|
||||
# Handle Status event
|
||||
def dashboard_event(self, event):
|
||||
entry = dashboard.status
|
||||
@ -673,9 +656,6 @@ class AppWindow:
|
||||
if f:
|
||||
with open(f, 'wt') as h:
|
||||
h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8'))
|
||||
except companion.VerificationRequired:
|
||||
if not self.authdialog:
|
||||
self.authdialog = prefs.AuthenticationDialog(self.w, partial(self.verify, self.save_raw))
|
||||
except companion.ServerError as e:
|
||||
self.status['text'] = str(e)
|
||||
except Exception as e:
|
||||
@ -686,6 +666,7 @@ class AppWindow:
|
||||
if platform!='darwin' or self.w.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+')))
|
||||
self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen
|
||||
self.protocolhandler.close()
|
||||
hotkeymgr.unregister()
|
||||
dashboard.close()
|
||||
monitor.close()
|
||||
@ -736,7 +717,6 @@ if __name__ == "__main__":
|
||||
import ctypes
|
||||
from ctypes.wintypes import *
|
||||
EnumWindows = ctypes.windll.user32.EnumWindows
|
||||
EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
|
||||
GetClassName = ctypes.windll.user32.GetClassNameW
|
||||
GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int]
|
||||
GetWindowText = ctypes.windll.user32.GetWindowTextW
|
||||
@ -754,6 +734,7 @@ if __name__ == "__main__":
|
||||
return buf.value
|
||||
return None
|
||||
|
||||
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
|
||||
def enumwindowsproc(hWnd, lParam):
|
||||
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
|
||||
cls = ctypes.create_unicode_buffer(257)
|
||||
@ -764,7 +745,7 @@ if __name__ == "__main__":
|
||||
sys.exit(0)
|
||||
return True
|
||||
|
||||
EnumWindows(EnumWindowsProc(enumwindowsproc), 0)
|
||||
EnumWindows(enumwindowsproc, 0)
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# By default py2exe tries to write log to dirname(sys.executable) which fails when installed
|
||||
|
@ -90,6 +90,30 @@
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
|
||||
<!-- http://wixtoolset.org/documentation/manual/v3/howtos/files_and_registry/write_a_registry_entry.html -->
|
||||
<Component Id="RegistryEntries" Guid="*">
|
||||
<RegistryKey Root="HKCR" Key="edmc">
|
||||
<RegistryValue Type="string" Value=""/>
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value="$(var.PRODUCTLONGNAME)"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]EDMarketConnector.exe,0"/>
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell">
|
||||
<RegistryKey Key="open">
|
||||
<RegistryKey Key="command">
|
||||
<RegistryValue Type="string" Value='"[INSTALLDIR]EDMarketConnector.exe" "%1"'/>
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="ddeexec">
|
||||
<RegistryValue Type="string" Value='Open("%1")'/>
|
||||
<!--<RegistryKey Key="application">
|
||||
<RegistryValue Type="string" Value="$(var.PRODUCTNAME)"/>
|
||||
</RegistryKey> -->
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
|
||||
<!-- Generate with `heat.exe dir dist.win32 -gg -sfrag -suid -out foo.wxs` -->
|
||||
<!-- Sadly too late for auto-generated Component UUIDs -->
|
||||
|
||||
@ -476,6 +500,7 @@
|
||||
</Directory>
|
||||
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentRef Id="RegistryEntries" />
|
||||
<ComponentRef Id="MainExecutable" />
|
||||
<ComponentRef Id="_ctypes.pyd" />
|
||||
<ComponentRef Id="_hashlib.pyd" />
|
||||
|
@ -58,9 +58,6 @@
|
||||
/* Main window. [EDMarketConnector.py] */
|
||||
"Cmdr" = "Cmdr";
|
||||
|
||||
/* Privacy setting. [prefs.py] */
|
||||
"Cmdr name" = "Cmdr name";
|
||||
|
||||
/* Ranking. [stats.py] */
|
||||
"Combat" = "Combat";
|
||||
|
||||
@ -85,9 +82,6 @@
|
||||
/* Ranking. [stats.py] */
|
||||
"CQC" = "CQC";
|
||||
|
||||
/* Section heading in settings. [prefs.py] */
|
||||
"Credentials" = "Credentials";
|
||||
|
||||
/* Combat rank. [stats.py] */
|
||||
"Dangerous" = "Dangerous";
|
||||
|
||||
@ -169,9 +163,6 @@
|
||||
/* [companion.py] */
|
||||
"Error: Invalid Credentials" = "Error: Invalid Credentials";
|
||||
|
||||
/* [companion.py] */
|
||||
"Error: Verification failed" = "Error: Verification failed";
|
||||
|
||||
/* Raised when the user has multiple accounts and the username/password setting is not for the account they're currently playing OR the user has reset their Cmdr and the Companion API server is still returning data for the old Cmdr. [companion.py] */
|
||||
"Error: Wrong Cmdr" = "Error: Wrong Cmdr";
|
||||
|
||||
@ -226,11 +217,6 @@
|
||||
/* Hotkey/Shortcut settings prompt on Windows. [prefs.py] */
|
||||
"Hotkey" = "Hotkey";
|
||||
|
||||
/* [prefs.py] */
|
||||
"How do you want to be identified in the saved data" = "How do you want to be identified in the saved data";
|
||||
|
||||
/* Tab heading in settings. [prefs.py] */
|
||||
"Identity" = "Identity";
|
||||
|
||||
/* Section heading in settings. [inara.py] */
|
||||
"Inara credentials" = "Inara credentials";
|
||||
@ -301,9 +287,6 @@
|
||||
/* Dark theme color setting. [prefs.py] */
|
||||
"Normal text" = "Normal text";
|
||||
|
||||
/* Displayed when credentials settings are greyed out. [prefs.py] */
|
||||
"Not available while E:D is at the main menu" = "Not available while E:D is at the main menu";
|
||||
|
||||
/* Combat rank. [stats.py] */
|
||||
"Novice" = "Novice";
|
||||
|
||||
@ -325,9 +308,6 @@
|
||||
/* Empire rank. [stats.py] */
|
||||
"Outsider" = "Outsider";
|
||||
|
||||
/* Use same text as E:D Launcher's login dialog. [prefs.py] */
|
||||
"Password" = "Password";
|
||||
|
||||
/* Explorer rank. [stats.py] */
|
||||
"Pathfinder" = "Pathfinder";
|
||||
|
||||
@ -352,9 +332,6 @@
|
||||
/* Use same text as E:D Launcher's verification dialog. [prefs.py] */
|
||||
"Please enter the code into the box below." = "Please enter the code into the box below.";
|
||||
|
||||
/* Use same text as E:D Launcher's login dialog. [prefs.py] */
|
||||
"Please log in with your Elite: Dangerous account details" = "Please log in with your Elite: Dangerous account details";
|
||||
|
||||
/* Tab heading in settings. [prefs.py] */
|
||||
"Plugins" = "Plugins";
|
||||
|
||||
@ -379,15 +356,9 @@
|
||||
/* Empire rank. [stats.py] */
|
||||
"Prince" = "Prince";
|
||||
|
||||
/* Section heading in settings. [prefs.py] */
|
||||
"Privacy" = "Privacy";
|
||||
|
||||
/* CQC rank. [stats.py] */
|
||||
"Professional" = "Professional";
|
||||
|
||||
/* Privacy setting. [prefs.py] */
|
||||
"Pseudo-anonymized ID" = "Pseudo-anonymized ID";
|
||||
|
||||
/* Explorer rank. [stats.py] */
|
||||
"Ranger" = "Ranger";
|
||||
|
||||
@ -505,9 +476,6 @@
|
||||
/* Update button in main window. [EDMarketConnector.py] */
|
||||
"Update" = "Update";
|
||||
|
||||
/* Use same text as E:D Launcher's login dialog. [prefs.py] */
|
||||
"Username (Email)" = "Username (Email)";
|
||||
|
||||
/* Status dialog subtitle - CR value of ship. [stats.py] */
|
||||
"Value" = "Value";
|
||||
|
||||
|
206
companion.py
206
companion.py
@ -1,24 +1,32 @@
|
||||
import base64
|
||||
import csv
|
||||
import requests
|
||||
from cookielib import LWPCookieJar
|
||||
from cookielib import LWPCookieJar # No longer needed but retained in case plugins use it
|
||||
from email.utils import parsedate
|
||||
import hashlib
|
||||
import json
|
||||
import numbers
|
||||
import os
|
||||
from os.path import dirname, isfile, join
|
||||
import sys
|
||||
import time
|
||||
from traceback import print_exc
|
||||
import urlparse
|
||||
import webbrowser
|
||||
import zlib
|
||||
|
||||
from config import appname, appversion, config
|
||||
|
||||
holdoff = 60 # be nice
|
||||
timeout = 10 # requests timeout
|
||||
|
||||
CLIENT_ID = None # Replace with FDev Client Id
|
||||
SERVER_AUTH = 'https://auth.frontierstore.net'
|
||||
URL_AUTH = '/auth'
|
||||
URL_TOKEN = '/token'
|
||||
|
||||
SERVER_LIVE = 'https://companion.orerve.net'
|
||||
SERVER_BETA = 'https://pts-companion.orerve.net'
|
||||
URL_LOGIN = '/user/login'
|
||||
URL_CONFIRM = '/user/confirm'
|
||||
URL_QUERY = '/profile'
|
||||
URL_MARKET = '/market'
|
||||
URL_SHIPYARD= '/shipyard'
|
||||
@ -136,24 +144,94 @@ class CmdrError(Exception):
|
||||
def __str__(self):
|
||||
return unicode(self).encode('utf-8')
|
||||
|
||||
class VerificationRequired(Exception):
|
||||
def __unicode__(self):
|
||||
return _('Error: Verification failed')
|
||||
def __str__(self):
|
||||
return unicode(self).encode('utf-8')
|
||||
|
||||
class Auth:
|
||||
|
||||
def __init__(self, cmdr):
|
||||
self.cmdr = cmdr
|
||||
self.session = requests.Session()
|
||||
self.session.headers['User-Agent'] = 'EDCD-%s-%s' % (appname, appversion)
|
||||
self.verifier = self.state = None
|
||||
|
||||
def refresh(self):
|
||||
# Try refresh token
|
||||
self.verifier = None
|
||||
cmdrs = config.get('cmdrs')
|
||||
idx = cmdrs.index(self.cmdr)
|
||||
tokens = config.get('fdev_apikeys') or []
|
||||
tokens = tokens + [''] * (len(cmdrs) - len(tokens))
|
||||
if tokens[idx]:
|
||||
data = {
|
||||
'grant_type': 'refresh_token',
|
||||
'client_id': CLIENT_ID,
|
||||
'refresh_token': tokens[idx],
|
||||
}
|
||||
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=timeout)
|
||||
if r.status_code == requests.codes.ok:
|
||||
data = r.json()
|
||||
tokens[idx] = data.get('refresh_token', '')
|
||||
config.set('fdev_apikeys', tokens)
|
||||
config.save() # Save settings now for use by command-line app
|
||||
return data.get('access_token')
|
||||
else:
|
||||
self.dump(r)
|
||||
|
||||
# New request
|
||||
self.verifier = self.base64URLEncode(os.urandom(32))
|
||||
self.state = self.base64URLEncode(os.urandom(8)) # Keep small to stay under 256 ShellExecute limit on Windows
|
||||
webbrowser.open('%s%s?response_type=code&approval_prompt=auto&client_id=%s&code_challenge=%s&code_challenge_method=S256&state=%s&redirect_uri=edmc://auth' % (SERVER_AUTH, URL_AUTH, CLIENT_ID, self.base64URLEncode(hashlib.sha256(self.verifier).digest()), self.state))
|
||||
|
||||
def authorize(self, payload):
|
||||
if not self.verifier or not self.state or not '?' in payload:
|
||||
raise CredentialsError()
|
||||
try:
|
||||
data = urlparse.parse_qs(payload[payload.index('?')+1:])
|
||||
if data['state'][0] != self.state:
|
||||
raise CredentialsError()
|
||||
code = data['code'][0]
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
raise CredentialsError()
|
||||
|
||||
data = {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': CLIENT_ID,
|
||||
'code_verifier': self.verifier,
|
||||
'code': code,
|
||||
'redirect_uri': 'edmc://auth',
|
||||
}
|
||||
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=timeout)
|
||||
if r.status_code == requests.codes.ok:
|
||||
data = r.json()
|
||||
cmdrs = config.get('cmdrs')
|
||||
idx = cmdrs.index(self.cmdr)
|
||||
tokens = config.get('fdev_apikeys') or []
|
||||
tokens = tokens + [''] * (len(cmdrs) - len(tokens))
|
||||
tokens[idx] = data.get('refresh_token', '')
|
||||
config.set('fdev_apikeys', tokens)
|
||||
config.save() # Save settings now for use by command-line app
|
||||
return data.get('access_token')
|
||||
else:
|
||||
self.dump(r)
|
||||
return None
|
||||
|
||||
def dump(self, r):
|
||||
print_exc()
|
||||
print 'Auth\t' + r.url, r.status_code, r.headers, r.text.encode('utf-8')
|
||||
|
||||
def base64URLEncode(self, text):
|
||||
return base64.urlsafe_b64encode(text).replace('=', '')
|
||||
|
||||
# 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)
|
||||
STATE_INIT, STATE_AUTH, STATE_OK = range(3)
|
||||
|
||||
def __init__(self):
|
||||
self.state = Session.STATE_INIT
|
||||
self.credentials = None
|
||||
self.session = None
|
||||
self.auth = None
|
||||
|
||||
# yuck suppress InsecurePlatformWarning under Python < 2.7.9 which lacks SNI support
|
||||
if sys.version_info < (2,7,9):
|
||||
@ -163,87 +241,68 @@ class Session:
|
||||
if getattr(sys, 'frozen', False):
|
||||
os.environ['REQUESTS_CA_BUNDLE'] = join(config.respath, 'cacert.pem')
|
||||
|
||||
def login(self, username=None, password=None, is_beta=False):
|
||||
if (not username or not password):
|
||||
if not self.credentials:
|
||||
raise CredentialsError()
|
||||
else:
|
||||
credentials = self.credentials
|
||||
else:
|
||||
credentials = { 'email' : username, 'password' : password, 'beta' : is_beta }
|
||||
|
||||
def login(self, cmdr, is_beta=False):
|
||||
if not CLIENT_ID:
|
||||
raise CredentialsError()
|
||||
credentials = {'cmdr': cmdr, 'beta': is_beta}
|
||||
if self.credentials == credentials and self.state == Session.STATE_OK:
|
||||
return # already logged in
|
||||
return True # already logged in
|
||||
|
||||
if not self.credentials or self.credentials['email'] != credentials['email'] or self.credentials['beta'] != credentials['beta']:
|
||||
if not self.credentials or self.credentials['cmdr'] != credentials['cmdr'] or self.credentials['beta'] != credentials['beta']:
|
||||
# changed account
|
||||
self.close()
|
||||
self.session = requests.Session()
|
||||
self.session.headers['User-Agent'] = 'EDCD-%s-%s' % (appname, appversion)
|
||||
cookiefile = join(config.app_dir, 'cookies-%s.txt' % hashlib.md5(credentials['email']).hexdigest())
|
||||
if not isfile(cookiefile) and isfile(join(config.app_dir, 'cookies.txt')):
|
||||
os.rename(join(config.app_dir, 'cookies.txt'), cookiefile) # migration from <= 2.25
|
||||
self.session.cookies = LWPCookieJar(cookiefile)
|
||||
try:
|
||||
self.session.cookies.load()
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
self.server = credentials['beta'] and SERVER_BETA or SERVER_LIVE
|
||||
self.credentials = credentials
|
||||
self.server = credentials['beta'] and SERVER_BETA or SERVER_LIVE
|
||||
self.state = Session.STATE_INIT
|
||||
try:
|
||||
r = self.session.post(self.server + URL_LOGIN, data = self.credentials, timeout=timeout)
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
raise ServerError()
|
||||
|
||||
if r.status_code != requests.codes.ok or 'server error' in r.text:
|
||||
self.dump(r)
|
||||
raise ServerError()
|
||||
elif r.url == self.server + URL_LOGIN: # would have redirected away if success
|
||||
self.dump(r)
|
||||
if 'purchase' in r.text.lower():
|
||||
raise SKUError()
|
||||
else:
|
||||
raise CredentialsError()
|
||||
elif r.url == self.server + URL_CONFIRM: # redirected to verification page
|
||||
self.state = Session.STATE_AUTH
|
||||
raise VerificationRequired()
|
||||
self.auth = Auth(cmdr)
|
||||
access_token = self.auth.refresh()
|
||||
if access_token:
|
||||
self.auth = None
|
||||
self.start(access_token)
|
||||
return True
|
||||
else:
|
||||
self.state = Session.STATE_OK
|
||||
return r.status_code
|
||||
self.state = Session.STATE_AUTH
|
||||
return False
|
||||
# Wait for callback
|
||||
|
||||
def verify(self, code):
|
||||
if not code:
|
||||
raise VerificationRequired()
|
||||
r = self.session.post(self.server + URL_CONFIRM, data = {'code' : code}, timeout=timeout)
|
||||
if r.status_code != requests.codes.ok or r.url == self.server + URL_CONFIRM: # would have redirected away if success
|
||||
raise VerificationRequired()
|
||||
self.session.cookies.save() # Save cookies now for use by command-line app
|
||||
self.login()
|
||||
# Callback from protocol handler
|
||||
def auth_callback(self, payload):
|
||||
if self.state != Session.STATE_AUTH:
|
||||
raise CredentialsError() # Shouldn't be getting a callback
|
||||
access_token = self.auth.authorize(payload)
|
||||
if access_token:
|
||||
self.auth = None
|
||||
self.start(access_token)
|
||||
else:
|
||||
self.state = Session.STATE_INIT
|
||||
raise CredentialsError() # Bad thing happened
|
||||
|
||||
def start(self, access_token):
|
||||
self.session = requests.Session()
|
||||
self.session.headers['Authorization'] = 'Bearer %s' % access_token
|
||||
self.session.headers['User-Agent'] = 'EDCD-%s-%s' % (appname, appversion)
|
||||
self.state = Session.STATE_OK
|
||||
|
||||
def query(self, endpoint):
|
||||
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()
|
||||
if self.state == Session.STATE_INIT:
|
||||
if self.login():
|
||||
return self.query(endpoint)
|
||||
elif self.state == Session.STATE_AUTH:
|
||||
raise VerificationRequired()
|
||||
raise CredentialsError()
|
||||
|
||||
try:
|
||||
r = self.session.get(self.server + endpoint, timeout=timeout)
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
raise ServerError()
|
||||
|
||||
if r.status_code == requests.codes.forbidden or r.url == self.server + URL_LOGIN:
|
||||
# Start again - maybe our session cookie expired?
|
||||
self.state = Session.STATE_INIT
|
||||
return self.query(endpoint)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
# Start again - maybe our token expired
|
||||
self.dump(r)
|
||||
raise ServerError()
|
||||
self.close()
|
||||
if self.login(self.credentials['cmdr'], self.credentials['beta']):
|
||||
return self.query(endpoint)
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
@ -279,10 +338,9 @@ class Session:
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
self.state = Session.STATE_NONE
|
||||
self.state = Session.STATE_INIT
|
||||
if self.session:
|
||||
try:
|
||||
self.session.cookies.save()
|
||||
self.session.close()
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
|
93
prefs.py
93
prefs.py
@ -98,41 +98,6 @@ class PreferencesDialog(tk.Toplevel):
|
||||
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
||||
PADY = 2 # close spacing
|
||||
|
||||
credframe = nb.Frame(notebook)
|
||||
credframe.columnconfigure(1, weight=1)
|
||||
|
||||
nb.Label(credframe, text=_('Credentials')).grid(padx=PADX, sticky=tk.W) # Section heading in settings
|
||||
ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW)
|
||||
self.cred_label = nb.Label(credframe)
|
||||
self.cred_label.grid(padx=PADX, columnspan=2, sticky=tk.W)
|
||||
self.cmdr_label = nb.Label(credframe, text=_('Cmdr')) # Main window
|
||||
self.cmdr_label.grid(row=10, padx=PADX, sticky=tk.W)
|
||||
self.username_label = nb.Label(credframe, text=_('Username (Email)')) # Use same text as E:D Launcher's login dialog
|
||||
self.username_label.grid(row=11, padx=PADX, sticky=tk.W)
|
||||
self.password_label = nb.Label(credframe, text=_('Password')) # Use same text as E:D Launcher's login dialog
|
||||
self.password_label.grid(row=12, padx=PADX, sticky=tk.W)
|
||||
|
||||
self.cmdr_text = nb.Label(credframe)
|
||||
self.cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W)
|
||||
self.username = nb.Entry(credframe)
|
||||
self.username.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
|
||||
if monitor.cmdr:
|
||||
self.username.focus_set()
|
||||
self.password = nb.Entry(credframe, show=u'•')
|
||||
self.password.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
|
||||
|
||||
nb.Label(credframe).grid(sticky=tk.W) # big spacer
|
||||
nb.Label(credframe, text=_('Privacy')).grid(padx=PADX, sticky=tk.W) # Section heading in settings
|
||||
ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW)
|
||||
|
||||
self.out_anon= tk.IntVar(value = config.getint('anonymous') and 1)
|
||||
nb.Label(credframe, text=_('How do you want to be identified in the saved data')).grid(columnspan=2, padx=PADX, sticky=tk.W)
|
||||
nb.Radiobutton(credframe, text=_('Cmdr name'), variable=self.out_anon, value=0).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Privacy setting
|
||||
nb.Radiobutton(credframe, text=_('Pseudo-anonymized ID'), variable=self.out_anon, value=1).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Privacy setting
|
||||
|
||||
notebook.add(credframe, text=_('Identity')) # Tab heading in settings
|
||||
|
||||
|
||||
outframe = nb.Frame(notebook)
|
||||
outframe.columnconfigure(0, weight=1)
|
||||
|
||||
@ -351,25 +316,6 @@ class PreferencesDialog(tk.Toplevel):
|
||||
def cmdrchanged(self, event=None):
|
||||
if self.cmdr != monitor.cmdr or self.is_beta != monitor.is_beta:
|
||||
# Cmdr has changed - update settings
|
||||
if monitor.cmdr:
|
||||
self.cred_label['text'] = _('Please log in with your Elite: Dangerous account details') # Use same text as E:D Launcher's login dialog
|
||||
else:
|
||||
self.cred_label['text'] = _('Not available while E:D is at the main menu') # Displayed when credentials settings are greyed out
|
||||
|
||||
self.cmdr_label['state'] = self.username_label['state'] = self.password_label['state'] = self.cmdr_text['state'] = self.username['state'] = self.password['state'] = monitor.cmdr and tk.NORMAL or tk.DISABLED
|
||||
self.cmdr_text['text'] = (monitor.cmdr or _('None')) + (monitor.is_beta and ' [Beta]' or '') # No hotkey/shortcut currently defined
|
||||
self.username['state'] = tk.NORMAL
|
||||
self.username.delete(0, tk.END)
|
||||
self.password['state'] = tk.NORMAL
|
||||
self.password.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 '')
|
||||
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 '')
|
||||
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
|
||||
@ -522,19 +468,6 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
|
||||
def apply(self):
|
||||
if self.cmdr:
|
||||
if self.password.get().strip():
|
||||
config.set_password(self.username.get().strip(), self.password.get().strip()) # Can fail if keyring not unlocked
|
||||
else:
|
||||
config.delete_password(self.username.get().strip()) # user may have cleared the password field
|
||||
if not config.get('cmdrs'):
|
||||
config.set('cmdrs', [self.cmdr])
|
||||
config.set('fdev_usernames', [self.username.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())
|
||||
|
||||
config.set('output',
|
||||
(self.out_td.get() and config.OUT_MKT_TD) +
|
||||
(self.out_csv.get() and config.OUT_MKT_CSV) +
|
||||
@ -568,8 +501,6 @@ class PreferencesDialog(tk.Toplevel):
|
||||
config.set('dark_highlight', self.theme_colors[1])
|
||||
theme.apply(self.parent)
|
||||
|
||||
config.set('anonymous', self.out_anon.get())
|
||||
|
||||
# Notify
|
||||
if self.callback:
|
||||
self.callback()
|
||||
@ -687,27 +618,3 @@ class AuthenticationDialog(tk.Toplevel):
|
||||
self.parent.wm_attributes('-topmost', config.getint('always_ontop') and 1 or 0)
|
||||
self.destroy()
|
||||
if self.callback: self.callback(None)
|
||||
|
||||
# migration from <= 2.25. Assumes current Cmdr corresponds to the saved credentials
|
||||
def migrate(current_cmdr):
|
||||
if current_cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'):
|
||||
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.delete('username')
|
||||
config.delete('password')
|
||||
|
||||
# Put current Cmdr first in the lists
|
||||
def make_current(current_cmdr):
|
||||
if current_cmdr and config.get('cmdrs') and current_cmdr in config.get('cmdrs'):
|
||||
idx = config.get('cmdrs').index(current_cmdr)
|
||||
_putfirst('cmdrs', idx)
|
||||
_putfirst('fdev_usernames', idx)
|
||||
|
||||
def _putfirst(setting, config_idx, new_value=None):
|
||||
assert config_idx>=0 or new_value is not None, (setting, config_idx, new_value)
|
||||
values = config.get(setting)
|
||||
values.insert(0, new_value if config_idx<0 else values.pop(config_idx))
|
||||
if new_value is not None:
|
||||
values[0] = new_value
|
||||
config.set(setting, values)
|
||||
|
194
protocol.py
Normal file
194
protocol.py
Normal file
@ -0,0 +1,194 @@
|
||||
# edmc: protocol handler for cAPI authorisation
|
||||
|
||||
|
||||
import threading
|
||||
import urllib2
|
||||
from sys import platform
|
||||
|
||||
from config import appname
|
||||
|
||||
|
||||
class GenericProtocolHandler:
|
||||
|
||||
def __init__(self, master):
|
||||
self.master = master
|
||||
self.lastpayload = None
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def event(self, url):
|
||||
if url.startswith('edmc://'):
|
||||
self.lastpayload = url[7:]
|
||||
self.master.event_generate('<<CompanionAuthEvent>>', when="tail")
|
||||
|
||||
|
||||
if platform == 'darwin':
|
||||
|
||||
import struct
|
||||
import objc
|
||||
from AppKit import NSAppleEventManager, NSObject
|
||||
|
||||
kInternetEventClass = kAEGetURL = struct.unpack('>l', b'GURL')[0]
|
||||
keyDirectObject = struct.unpack('>l', b'----')[0]
|
||||
|
||||
class ProtocolHandler(GenericProtocolHandler):
|
||||
|
||||
POLL = 100 # ms
|
||||
|
||||
def __init__(self, master):
|
||||
GenericProtocolHandler.__init__(self, master)
|
||||
self.eventhandler = EventHandler.alloc().init()
|
||||
self.eventhandler.handler = self
|
||||
self.lasturl = None
|
||||
|
||||
def poll(self):
|
||||
# No way of signalling to Tkinter from within the callback handler block that doesn't cause Python to crash, so poll.
|
||||
if self.lasturl:
|
||||
self.event(self.lasturl)
|
||||
self.lasturl = None
|
||||
|
||||
class EventHandler(NSObject):
|
||||
|
||||
def init(self):
|
||||
self = objc.super(EventHandler, self).init()
|
||||
NSAppleEventManager.sharedAppleEventManager().setEventHandler_andSelector_forEventClass_andEventID_(self, 'handleEvent:withReplyEvent:', kInternetEventClass, kAEGetURL)
|
||||
return self
|
||||
|
||||
def handleEvent_withReplyEvent_(self, event, replyEvent):
|
||||
self.handler.lasturl = urllib2.unquote(event.paramDescriptorForKeyword_(keyDirectObject).stringValue()).strip()
|
||||
self.handler.master.after(ProtocolHandler.POLL, self.handler.poll)
|
||||
|
||||
|
||||
elif platform == 'win32':
|
||||
|
||||
from ctypes import *
|
||||
from ctypes.wintypes import *
|
||||
|
||||
class WNDCLASS(Structure):
|
||||
_fields_ = [('style', UINT),
|
||||
('lpfnWndProc', WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM)),
|
||||
('cbClsExtra', INT),
|
||||
('cbWndExtra', INT),
|
||||
('hInstance', HINSTANCE),
|
||||
('hIcon', HICON),
|
||||
('hCursor', c_void_p),
|
||||
('hbrBackground', HBRUSH),
|
||||
('lpszMenuName', LPCWSTR),
|
||||
('lpszClassName', LPCWSTR)
|
||||
]
|
||||
|
||||
CW_USEDEFAULT = 0x80000000
|
||||
|
||||
CreateWindowEx = windll.user32.CreateWindowExW
|
||||
CreateWindowEx.argtypes = [DWORD, LPCWSTR, LPCWSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID]
|
||||
CreateWindowEx.restype = HWND
|
||||
RegisterClass = windll.user32.RegisterClassW
|
||||
RegisterClass.argtypes = [POINTER(WNDCLASS)]
|
||||
DefWindowProc = windll.user32.DefWindowProcW
|
||||
|
||||
GetMessage = windll.user32.GetMessageW
|
||||
TranslateMessage = windll.user32.TranslateMessage
|
||||
DispatchMessage = windll.user32.DispatchMessageW
|
||||
PostThreadMessage = windll.user32.PostThreadMessageW
|
||||
SendMessage = windll.user32.SendMessageW
|
||||
SendMessage.argtypes = [HWND, UINT, WPARAM, LPARAM]
|
||||
PostMessage = windll.user32.PostMessageW
|
||||
PostMessage.argtypes = [HWND, UINT, WPARAM, LPARAM]
|
||||
|
||||
WM_QUIT = 0x0012
|
||||
WM_DDE_INITIATE = 0x03E0
|
||||
WM_DDE_TERMINATE = 0x03E1
|
||||
WM_DDE_ACK = 0x03E4
|
||||
WM_DDE_EXECUTE = 0x03E8
|
||||
|
||||
PackDDElParam = windll.user32.PackDDElParam
|
||||
PackDDElParam.argtypes = [UINT, LPARAM, LPARAM]
|
||||
|
||||
GlobalAddAtom = windll.kernel32.GlobalAddAtomW
|
||||
GlobalAddAtom.argtypes = [LPWSTR]
|
||||
GlobalAddAtom.restype = ATOM
|
||||
GlobalGetAtomName = windll.kernel32.GlobalGetAtomNameW
|
||||
GlobalGetAtomName.argtypes = [ATOM, LPWSTR, INT]
|
||||
GlobalGetAtomName.restype = UINT
|
||||
GlobalLock = windll.kernel32.GlobalLock
|
||||
GlobalLock.argtypes = [HGLOBAL]
|
||||
GlobalLock.restype = LPVOID
|
||||
GlobalUnlock = windll.kernel32.GlobalUnlock
|
||||
GlobalUnlock.argtypes = [HGLOBAL]
|
||||
GlobalUnlock.restype = BOOL
|
||||
|
||||
|
||||
@WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM)
|
||||
def WndProc(hwnd, message, wParam, lParam):
|
||||
service = create_unicode_buffer(256)
|
||||
topic = create_unicode_buffer(256)
|
||||
if message == WM_DDE_INITIATE:
|
||||
if ((lParam & 0xffff == 0 or (GlobalGetAtomName(lParam & 0xffff, service, 256) and service.value == appname)) and
|
||||
(lParam >> 16 == 0 or (GlobalGetAtomName(lParam >> 16, topic, 256) and topic.value.lower() == 'system'))):
|
||||
SendMessage(wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, GlobalAddAtom(appname), GlobalAddAtom('System')))
|
||||
return 0
|
||||
return DefWindowProc(hwnd, message, wParam, lParam)
|
||||
|
||||
|
||||
class ProtocolHandler(GenericProtocolHandler):
|
||||
|
||||
def __init__(self, master):
|
||||
GenericProtocolHandler.__init__(self, master)
|
||||
self.thread = threading.Thread(target=self.worker, name='DDE worker')
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def close(self):
|
||||
thread = self.thread
|
||||
if thread:
|
||||
self.thread = None
|
||||
PostThreadMessage(thread.ident, WM_QUIT, 0, 0)
|
||||
thread.join() # Wait for it to quit
|
||||
|
||||
def worker(self):
|
||||
wndclass = WNDCLASS()
|
||||
wndclass.style = 0
|
||||
wndclass.lpfnWndProc = WndProc
|
||||
wndclass.cbClsExtra = 0
|
||||
wndclass.cbWndExtra = 0
|
||||
wndclass.hInstance = windll.kernel32.GetModuleHandleW(0)
|
||||
wndclass.hIcon = None
|
||||
wndclass.hCursor = None
|
||||
wndclass.hbrBackground = None
|
||||
wndclass.lpszMenuName = None
|
||||
wndclass.lpszClassName = 'DDEServer'
|
||||
|
||||
if RegisterClass(byref(wndclass)):
|
||||
hwnd = CreateWindowEx(0,
|
||||
wndclass.lpszClassName,
|
||||
"DDE Server",
|
||||
0,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
self.master.winfo_id(), # Don't use HWND_MESSAGE since the window won't get DDE broadcasts
|
||||
None,
|
||||
wndclass.hInstance,
|
||||
None)
|
||||
msg = MSG()
|
||||
while GetMessage(byref(msg), None, 0, 0) != 0:
|
||||
if msg.message == WM_DDE_EXECUTE:
|
||||
args = wstring_at(GlobalLock(msg.lParam)).strip()
|
||||
GlobalUnlock(msg.lParam)
|
||||
if args.lower().startswith('open("') and args.endswith('")'):
|
||||
self.event(urllib2.unquote(args[6:-2]).strip())
|
||||
PostMessage(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam))
|
||||
else:
|
||||
PostMessage(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0, msg.lParam))
|
||||
elif msg.message == WM_DDE_TERMINATE:
|
||||
PostMessage(msg.wParam, WM_DDE_TERMINATE, hwnd, 0)
|
||||
else:
|
||||
TranslateMessage(byref(msg))
|
||||
DispatchMessage(byref(msg))
|
||||
else:
|
||||
print 'Failed to register DDE for cAPI'
|
||||
|
||||
else: # Linux
|
||||
|
||||
class ProtocolHandler(GenericProtocolHandler):
|
||||
pass
|
||||
|
8
setup.py
8
setup.py
@ -86,7 +86,15 @@ if sys.platform=='darwin':
|
||||
'CFBundleLocalizations': sorted(set([x[:-len('.lproj')] for x in os.listdir(join(SPARKLE, 'Resources')) if x.endswith('.lproj')]) | set([x[:-len('.strings')] for x in os.listdir('L10n') if x.endswith('.strings')])), # https://github.com/sparkle-project/Sparkle/issues/238
|
||||
'CFBundleShortVersionString': VERSION,
|
||||
'CFBundleVersion': VERSION,
|
||||
'CFBundleURLTypes': [
|
||||
{
|
||||
'CFBundleTypeRole': 'Viewer',
|
||||
'CFBundleURLName': 'uk.org.marginal.%s.URLScheme' % APPNAME.lower(),
|
||||
'CFBundleURLSchemes': ['edmc'],
|
||||
}
|
||||
],
|
||||
'LSMinimumSystemVersion': '10.10',
|
||||
'NSAppleScriptEnabled': True,
|
||||
'NSHumanReadableCopyright': u'© 2015-2018 Jonathan Harris',
|
||||
'SUEnableAutomaticChecks': True,
|
||||
'SUShowReleaseNotes': True,
|
||||
|
3
stats.py
3
stats.py
@ -177,7 +177,6 @@ class StatsDialog():
|
||||
def __init__(self, app):
|
||||
self.parent = app.w
|
||||
self.status = app.status
|
||||
self.verify = app.verify
|
||||
self.showstats()
|
||||
|
||||
def showstats(self):
|
||||
@ -189,8 +188,6 @@ class StatsDialog():
|
||||
|
||||
try:
|
||||
data = companion.session.profile()
|
||||
except companion.VerificationRequired:
|
||||
return prefs.AuthenticationDialog(self.parent, partial(self.verify, self.showstats))
|
||||
except companion.ServerError as e:
|
||||
self.status['text'] = str(e)
|
||||
return
|
||||
|
Loading…
x
Reference in New Issue
Block a user