From 9e8166c13d94e0eeb35c2f1a6ccff08db30c88d4 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 11 Jan 2018 20:54:33 -0500 Subject: [PATCH 01/39] Add ESI service skeleton, and my customer ESI Proxy class --- requirements.txt | 1 + service/esi.py | 46 +++++++ service/esi_security_proxy.py | 238 ++++++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100644 service/esi.py create mode 100644 service/esi_security_proxy.py diff --git a/requirements.txt b/requirements.txt index 694a2d348..8216804cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ python-dateutil urllib3 requests == 2.0.0 sqlalchemy == 1.0.5 +esipy == 0.3.0 \ No newline at end of file diff --git a/service/esi.py b/service/esi.py new file mode 100644 index 000000000..0125c42c4 --- /dev/null +++ b/service/esi.py @@ -0,0 +1,46 @@ +# noinspection PyPackageRequirements +import wx +from logbook import Logger +import threading +import copy +import uuid +import time + +import eos.db +from eos.enum import Enum +from eos.saveddata.crestchar import CrestChar +import gui.globalEvents as GE +from service.settings import CRESTSettings +from service.server import StoppableHTTPServer, AuthHandler +from service.pycrest.eve import EVE + +pyfalog = Logger(__name__) + + +class Servers(Enum): + TQ = 0 + SISI = 1 + + +class CrestModes(Enum): + IMPLICIT = 0 + USER = 1 + + +class ESI(object): + # @todo: move this to settings + clientCallback = 'http://localhost:6461' + clientTest = True + + _instance = None + + @classmethod + def getInstance(cls): + if cls._instance is None: + cls._instance = ESI() + + return cls._instance + + + def __init__(self): + pass diff --git a/service/esi_security_proxy.py b/service/esi_security_proxy.py new file mode 100644 index 000000000..f6263d5e8 --- /dev/null +++ b/service/esi_security_proxy.py @@ -0,0 +1,238 @@ +# -*- encoding: utf-8 -*- +""" EsiPy Security Proxy - An ESI Security class that directs authentication towards a third-party service. +Client key/secret not needed. +""" + +from __future__ import absolute_import + +import base64 +import logging +import time + +from requests import Session +from requests.utils import quote +from six.moves.urllib.parse import urlparse + +from esipy.events import AFTER_TOKEN_REFRESH +from esipy.exceptions import APIException +LOGGER = logging.getLogger(__name__) + + +class EsiSecurityProxy(object): + """ Contains all the OAuth2 knowledge for ESI use. + Based on pyswagger Security object, to be used with pyswagger BaseClient + implementation. + """ + + def __init__( + self, + redirect_uri, + **kwargs): + """ Init the ESI Security Object + + :param redirect_uri: the uri to redirect the user after login into SSO + :param sso_url: the default sso URL used when no "app" is provided + :param esi_url: the default esi URL used for verify endpoint + :param app: (optionnal) the pyswagger app object + :param security_name: (optionnal) the name of the object holding the + informations in the securityDefinitions, used to check authed endpoint + """ + + app = kwargs.pop('app', None) + sso_url = kwargs.pop('sso_url', "https://login.eveonline.com") + esi_url = kwargs.pop('esi_url', "https://esi.tech.ccp.is") + + self.security_name = kwargs.pop('security_name', 'evesso') + self.redirect_uri = redirect_uri + + # we provide app object, so we don't use sso_url + if app is not None: + # check if the security_name exists in the securityDefinition + security = app.root.securityDefinitions.get( + self.security_name, + None + ) + if security is None: + raise NameError( + "%s is not defined in the securityDefinitions" % + self.security_name + ) + + self.oauth_authorize = security.authorizationUrl + + # some URL we still need to "manually" define... sadly + # we parse the authUrl so we don't care if it's TQ or SISI. + # https://github.com/ccpgames/esi-issues/issues/92 + parsed_uri = urlparse(security.authorizationUrl) + self.oauth_token = '%s://%s/oauth/token' % ( + parsed_uri.scheme, + parsed_uri.netloc + ) + + # no app object is provided, so we use direct URLs + else: + if sso_url is None or sso_url == "": + raise AttributeError("sso_url cannot be None or empty " + "without app parameter") + + self.oauth_authorize = '%s/oauth/authorize' % sso_url + self.oauth_token = '%s/oauth/token' % sso_url + + # use ESI url for verify, since it's better for caching + if esi_url is None or esi_url == "": + raise AttributeError("esi_url cannot be None or empty") + self.oauth_verify = '%s/verify/' % esi_url + + # session request stuff + self._session = Session() + self._session.headers.update({ + 'Accept': 'application/json', + 'User-Agent': ( + 'EsiPy/Security/ - ' + 'https://github.com/Kyria/EsiPy' + ) + }) + + # token data + self.refresh_token = None + self.access_token = None + self.token_expiry = None + + def __get_oauth_header(self): + """ Return the Bearer Authorization header required in oauth calls + + :return: a dict with the authorization header + """ + return {'Authorization': 'Bearer %s' % self.access_token} + + def __make_token_request_parameters(self, params): + """ Return the token uri from the securityDefinition + + :param params: the data given to the request + :return: the oauth/token uri + """ + request_params = { + 'data': params, + 'url': self.oauth_token, + } + + return request_params + + def get_auth_uri(self, state=None, redirect='http://localhost:8080'): + """ Constructs the full auth uri and returns it. + + :param state: The state to pass through the auth process + :param redirect: The URI that the proxy server will redirect to + :return: the authorizationUrl with the correct parameters. + """ + + return '%s?redirect=%s%s' % ( + self.oauth_authorize, + quote(redirect, safe=''), + '&state=%s' % state if state else '' + ) + + def get_refresh_token_params(self): + """ Return the param object for the post() call to get the access_token + from the refresh_token + + :param code: the refresh token + :return: a dict with the url, params and header + """ + if self.refresh_token is None: + raise AttributeError('No refresh token is defined.') + + return self.__make_token_request_parameters( + { + 'grant_type': 'refresh_token', + 'refresh_token': self.refresh_token, + } + ) + + def update_token(self, response_json): + """ Update access_token, refresh_token and token_expiry from the + response body. + The response must be converted to a json object before being passed as + a parameter + + :param response_json: the response body to use. + """ + self.access_token = response_json['access_token'] + self.token_expiry = int(time.time()) + response_json['expires_in'] + + if 'refresh_token' in response_json: + self.refresh_token = response_json['refresh_token'] + + def is_token_expired(self, offset=0): + """ Return true if the token is expired. + + The offset can be used to change the expiry time: + - positive value decrease the time (sooner) + - negative value increase the time (later) + If the expiry is not set, always return True. This case allow the users + to define a security object, only knowing the refresh_token and get + a new access_token / expiry_time without errors. + + :param offset: the expiry offset (in seconds) [default: 0] + :return: boolean true if expired, else false. + """ + if self.token_expiry is None: + return True + return int(time.time()) >= (self.token_expiry - offset) + + def refresh(self): + """ Update the auth data (tokens) using the refresh token in auth. + """ + request_data = self.get_refresh_token_params() + res = self._session.post(**request_data) + if res.status_code != 200: + raise APIException( + request_data['url'], + res.status_code, + res.json() + ) + json_res = res.json() + self.update_token(json_res) + return json_res + + def verify(self): + """ Make a get call to the oauth/verify endpoint to get the user data + + :return: the json with the data. + """ + res = self._session.get( + self.oauth_verify, + headers=self.__get_oauth_header() + ) + if res.status_code != 200: + raise APIException( + self.oauth_verify, + res.status_code, + res.json() + ) + return res.json() + + def __call__(self, request): + """ Check if the request need security header and apply them. + Required for pyswagger.core.BaseClient.request(). + + :param request: the pyswagger request object to check + :return: the updated request. + """ + if not request._security: + return request + + if self.is_token_expired(): + json_response = self.refresh() + AFTER_TOKEN_REFRESH.send(**json_response) + + for security in request._security: + if self.security_name not in security: + LOGGER.warning( + "Missing Securities: [%s]" % ", ".join(security.keys()) + ) + continue + if self.access_token is not None: + request._p['header'].update(self.__get_oauth_header()) + + return request From 23761483807f1c241cfa3094b61a27b7e8fa1729 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Fri, 2 Feb 2018 02:05:49 -0500 Subject: [PATCH 02/39] Start ripping CREST stuff out, add new SSOCharacter stuff in. Long process ahead. --- config.py | 5 +++ eos/db/saveddata/crest.py | 18 +++++++---- eos/db/saveddata/queries.py | 16 +++++----- eos/saveddata/crestchar.py | 34 -------------------- eos/saveddata/ssocharacter.py | 60 +++++++++++++++++++++++++++++++++++ gui/mainFrame.py | 12 ++----- gui/mainMenuBar.py | 18 +++++------ service/crest.py | 22 ++++--------- service/esi.py | 2 +- 9 files changed, 101 insertions(+), 86 deletions(-) delete mode 100644 eos/saveddata/crestchar.py create mode 100644 eos/saveddata/ssocharacter.py diff --git a/config.py b/config.py index 339a19e0a..1127b72ae 100644 --- a/config.py +++ b/config.py @@ -3,6 +3,7 @@ import sys from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \ StreamHandler, TimedRotatingFileHandler, WARNING +import hashlib pyfalog = Logger(__name__) @@ -43,6 +44,10 @@ LOGLEVEL_MAP = { } +def getClientSecret(): + return hashlib.sha3_256("This is a secret, this will not remain in here for long".encode('utf-8')).hexdigest() + + def isFrozen(): if hasattr(sys, 'frozen'): return True diff --git a/eos/db/saveddata/crest.py b/eos/db/saveddata/crest.py index 28f77a983..035cdb60d 100644 --- a/eos/db/saveddata/crest.py +++ b/eos/db/saveddata/crest.py @@ -22,13 +22,17 @@ from sqlalchemy.orm import mapper import datetime from eos.db import saveddata_meta -from eos.saveddata.crestchar import CrestChar +from eos.saveddata.ssocharacter import SsoCharacter -crest_table = Table("crest", saveddata_meta, +sso_table = Table("ssoCharacter", saveddata_meta, Column("ID", Integer, primary_key=True), - Column("name", String, nullable=False, unique=True), - Column("refresh_token", String, nullable=False), - # These records aren't updated. Instead, they are dropped and created, hence we don't have a modified field - Column("created", DateTime, nullable=True, default=datetime.datetime.now)) + Column("client", String, nullable=False), + Column("characterID", Integer, nullable=False, unique=True), + Column("characterName", String, nullable=False, unique=True), + Column("refreshToken", String, nullable=False), + Column("accessToken", String, nullable=False), + Column("accessTokenExpires", DateTime, nullable=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)) -mapper(CrestChar, crest_table) +mapper(SsoCharacter, sso_table) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 98902007f..b3c1840e8 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -27,7 +27,7 @@ from eos.db.saveddata.fit import projectedFits_table from eos.db.util import processEager, processWhere from eos.saveddata.price import Price from eos.saveddata.user import User -from eos.saveddata.crestchar import CrestChar +from eos.saveddata.ssocharacter import SsoCharacter from eos.saveddata.damagePattern import DamagePattern from eos.saveddata.targetResists import TargetResists from eos.saveddata.character import Character @@ -467,27 +467,27 @@ def getProjectedFits(fitID): raise TypeError("Need integer as argument") -def getCrestCharacters(eager=None): +def getSsoCharacters(eager=None): eager = processEager(eager) with sd_lock: - characters = saveddata_session.query(CrestChar).options(*eager).all() + characters = saveddata_session.query(SsoCharacter).options(*eager).all() return characters -@cachedQuery(CrestChar, 1, "lookfor") -def getCrestCharacter(lookfor, eager=None): +@cachedQuery(SsoCharacter, 1, "lookfor") +def getSsoCharacter(lookfor, eager=None): if isinstance(lookfor, int): if eager is None: with sd_lock: - character = saveddata_session.query(CrestChar).get(lookfor) + character = saveddata_session.query(SsoCharacter).get(lookfor) else: eager = processEager(eager) with sd_lock: - character = saveddata_session.query(CrestChar).options(*eager).filter(CrestChar.ID == lookfor).first() + character = saveddata_session.query(SsoCharacter).options(*eager).filter(SsoCharacter.ID == lookfor).first() elif isinstance(lookfor, str): eager = processEager(eager) with sd_lock: - character = saveddata_session.query(CrestChar).options(*eager).filter(CrestChar.name == lookfor).first() + character = saveddata_session.query(SsoCharacter).options(*eager).filter(SsoCharacter.name == lookfor).first() else: raise TypeError("Need integer or string as argument") return character diff --git a/eos/saveddata/crestchar.py b/eos/saveddata/crestchar.py deleted file mode 100644 index ce6ab14fe..000000000 --- a/eos/saveddata/crestchar.py +++ /dev/null @@ -1,34 +0,0 @@ -# =============================================================================== -# Copyright (C) 2010 Diego Duclos -# -# This file is part of eos. -# -# eos is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# eos is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with eos. If not, see . -# =============================================================================== - -from sqlalchemy.orm import reconstructor - - -# from tomorrow import threads - - -class CrestChar(object): - def __init__(self, id, name, refresh_token=None): - self.ID = id - self.name = name - self.refresh_token = refresh_token - - @reconstructor - def init(self): - pass diff --git a/eos/saveddata/ssocharacter.py b/eos/saveddata/ssocharacter.py new file mode 100644 index 000000000..8fca830cb --- /dev/null +++ b/eos/saveddata/ssocharacter.py @@ -0,0 +1,60 @@ +# =============================================================================== +# Copyright (C) 2010 Diego Duclos +# +# This file is part of eos. +# +# eos is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# eos is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with eos. If not, see . +# =============================================================================== + +from sqlalchemy.orm import reconstructor +import datetime +import time + +# from tomorrow import threads + + +class SsoCharacter(object): + def __init__(self, id, charID, name, client, accessToken, refreshToken=None): + self.ID = id + self.characterID = charID + self.characterName = name + self.client = client + self.accessToken = accessToken + self.refreshToken = refreshToken + + @reconstructor + def init(self): + pass + + def get_sso_data(self): + """ Little "helper" function to get formated data for esipy security + """ + return { + 'access_token': self.accessToken, + 'refresh_token': self.refreshToken, + 'expires_in': ( + self.accessTokenExpires - datetime.utcnow() + ).total_seconds() + } + + def update_token(self, tokenResponse): + """ helper function to update token data from SSO response """ + self.accessToken = tokenResponse['access_token'] + self.accessTokenExpires = datetime.fromtimestamp( + time.time() + tokenResponse['expires_in'], + ) + if 'refresh_token' in tokenResponse: + self.refreshToken = tokenResponse['refresh_token'] + if self.esi_client is not None: + self.esi_client.security.update_token(tokenResponse) \ No newline at end of file diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 2aa6bb750..735db0d80 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -662,16 +662,8 @@ class MainFrame(wx.Frame): menu.Enable(menu.exportToEveId, not enable) def ssoHandler(self, event): - sCrest = Crest.getInstance() - if sCrest.settings.get('mode') == CrestModes.IMPLICIT: - if sCrest.implicitCharacter is not None: - sCrest.logout() - else: - uri = sCrest.startServer() - webbrowser.open(uri) - else: - dlg = CrestMgmt(self) - dlg.Show() + dlg = CrestMgmt(self) + dlg.Show() def exportToEve(self, event): dlg = ExportToEve(self) diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index be9322010..8b5b77e60 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -28,8 +28,8 @@ import gui.globalEvents as GE from gui.bitmap_loader import BitmapLoader from logbook import Logger -from service.crest import Crest -from service.crest import CrestModes +# from service.crest import Crest +# from service.crest import CrestModes pyfalog = Logger(__name__) @@ -134,21 +134,19 @@ class MainMenuBar(wx.MenuBar): preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui")) windowMenu.Append(preferencesItem) - self.sCrest = Crest.getInstance() + # self.sCrest = Crest.getInstance() # CREST Menu crestMenu = wx.Menu() self.Append(crestMenu, "&CREST") - if self.sCrest.settings.get('mode') != CrestModes.IMPLICIT: - crestMenu.Append(self.ssoLoginId, "Manage Characters") - else: - crestMenu.Append(self.ssoLoginId, "Login to EVE") + + crestMenu.Append(self.ssoLoginId, "Manage Characters") crestMenu.Append(self.eveFittingsId, "Browse EVE Fittings") crestMenu.Append(self.exportToEveId, "Export To EVE") - if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0: - self.Enable(self.eveFittingsId, False) - self.Enable(self.exportToEveId, False) + # if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0: + self.Enable(self.eveFittingsId, False) + self.Enable(self.exportToEveId, False) if not self.mainFrame.disableOverrideEditor: windowMenu.AppendSeparator() diff --git a/service/crest.py b/service/crest.py index ff1765298..51c9f1309 100644 --- a/service/crest.py +++ b/service/crest.py @@ -8,7 +8,7 @@ import time import eos.db from eos.enum import Enum -from eos.saveddata.crestchar import CrestChar +from eos.saveddata.ssocharacter import SsoCharacter import gui.globalEvents as GE from service.settings import CRESTSettings from service.server import StoppableHTTPServer, AuthHandler @@ -100,40 +100,30 @@ class Crest(object): return self.settings.get('server') == Servers.SISI def delCrestCharacter(self, charID): - char = eos.db.getCrestCharacter(charID) + char = eos.db.getSsoCharacter(charID) del self.charCache[char.ID] eos.db.remove(char) wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=len(self.charCache))) def delAllCharacters(self): - chars = eos.db.getCrestCharacters() + chars = eos.db.getSsoCharacters() for char in chars: eos.db.remove(char) self.charCache = {} wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0)) def getCrestCharacters(self): - chars = eos.db.getCrestCharacters() - # I really need to figure out that DB cache problem, this is ridiculous - chars2 = [self.getCrestCharacter(char.ID) for char in chars] - return chars2 + chars = eos.db.getSsoCharacters() + return chars def getCrestCharacter(self, charID): """ Get character, and modify to include the eve connection """ - if self.settings.get('mode') == CrestModes.IMPLICIT: - if self.implicitCharacter.ID != charID: - raise ValueError("CharacterID does not match currently logged in character.") - return self.implicitCharacter - if charID in self.charCache: return self.charCache.get(charID) - char = eos.db.getCrestCharacter(charID) - if char and not hasattr(char, "eve"): - char.eve = EVE(**self.eve_options) - char.eve.temptoken_authorize(refresh_token=char.refresh_token) + char = eos.db.getSsoCharacter(charID) self.charCache[charID] = char return char diff --git a/service/esi.py b/service/esi.py index 0125c42c4..0f375a591 100644 --- a/service/esi.py +++ b/service/esi.py @@ -8,7 +8,7 @@ import time import eos.db from eos.enum import Enum -from eos.saveddata.crestchar import CrestChar +from eos.saveddata.ssocharacter import CrestChar import gui.globalEvents as GE from service.settings import CRESTSettings from service.server import StoppableHTTPServer, AuthHandler From c7360c8cc36c1ef5c46ba806bdd426e78901a408 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 7 Feb 2018 00:44:37 -0500 Subject: [PATCH 03/39] Get logging into EVE working and SSO characters saving, along with client hashes being stored alongside characters. --- config.py | 3 + eos/db/saveddata/crest.py | 11 ++-- eos/db/saveddata/queries.py | 27 +++++---- eos/saveddata/ssocharacter.py | 6 +- gui/crestFittings.py | 8 ++- service/crest.py | 105 +++++++++++++++++++--------------- service/esi_security_proxy.py | 3 - service/server.py | 10 ++-- 8 files changed, 95 insertions(+), 78 deletions(-) diff --git a/config.py b/config.py index 1127b72ae..bac448989 100644 --- a/config.py +++ b/config.py @@ -35,6 +35,9 @@ logPath = None loggingLevel = None logging_setup = None +ESI_AUTH_PROXY = "http://localhost:5015" # "https://blitzmann.pythonanywhere.com" // need to get this set up, and actually put on it's own domain +ESI_CACHE = 'esi_cache' + LOGLEVEL_MAP = { "critical": CRITICAL, "error": ERROR, diff --git a/eos/db/saveddata/crest.py b/eos/db/saveddata/crest.py index 035cdb60d..429da243d 100644 --- a/eos/db/saveddata/crest.py +++ b/eos/db/saveddata/crest.py @@ -17,7 +17,7 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, String, DateTime +from sqlalchemy import Table, Column, Integer, String, DateTime, UniqueConstraint from sqlalchemy.orm import mapper import datetime @@ -27,12 +27,15 @@ from eos.saveddata.ssocharacter import SsoCharacter sso_table = Table("ssoCharacter", saveddata_meta, Column("ID", Integer, primary_key=True), Column("client", String, nullable=False), - Column("characterID", Integer, nullable=False, unique=True), - Column("characterName", String, nullable=False, unique=True), + Column("characterID", Integer, nullable=False), + Column("characterName", String, nullable=False), Column("refreshToken", String, nullable=False), Column("accessToken", String, nullable=False), Column("accessTokenExpires", DateTime, nullable=False), Column("created", DateTime, nullable=True, default=datetime.datetime.now), - Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)) + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), + UniqueConstraint('client', 'characterID', name='uix_client_characterID'), + UniqueConstraint('client', 'characterName', name='uix_client_characterName') + ) mapper(SsoCharacter, sso_table) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index b3c1840e8..9d4eaf55f 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -467,29 +467,28 @@ def getProjectedFits(fitID): raise TypeError("Need integer as argument") -def getSsoCharacters(eager=None): +def getSsoCharacters(clientHash, eager=None): eager = processEager(eager) with sd_lock: - characters = saveddata_session.query(SsoCharacter).options(*eager).all() + characters = saveddata_session.query(SsoCharacter).filter(SsoCharacter.client == clientHash).options(*eager).all() return characters @cachedQuery(SsoCharacter, 1, "lookfor") -def getSsoCharacter(lookfor, eager=None): +def getSsoCharacter(lookfor, clientHash, eager=None): + filter = SsoCharacter.client == clientHash + if isinstance(lookfor, int): - if eager is None: - with sd_lock: - character = saveddata_session.query(SsoCharacter).get(lookfor) - else: - eager = processEager(eager) - with sd_lock: - character = saveddata_session.query(SsoCharacter).options(*eager).filter(SsoCharacter.ID == lookfor).first() + filter = and_(filter, SsoCharacter.characterID == lookfor) elif isinstance(lookfor, str): - eager = processEager(eager) - with sd_lock: - character = saveddata_session.query(SsoCharacter).options(*eager).filter(SsoCharacter.name == lookfor).first() + filter = and_(filter, SsoCharacter.characterName == lookfor) else: raise TypeError("Need integer or string as argument") + + eager = processEager(eager) + with sd_lock: + character = saveddata_session.query(SsoCharacter).options(*eager).filter(filter).first() + return character @@ -544,7 +543,7 @@ def commit(): try: saveddata_session.commit() saveddata_session.flush() - except Exception: + except Exception as ex: saveddata_session.rollback() exc_info = sys.exc_info() raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) diff --git a/eos/saveddata/ssocharacter.py b/eos/saveddata/ssocharacter.py index 8fca830cb..88fcc4a24 100644 --- a/eos/saveddata/ssocharacter.py +++ b/eos/saveddata/ssocharacter.py @@ -25,13 +25,13 @@ import time class SsoCharacter(object): - def __init__(self, id, charID, name, client, accessToken, refreshToken=None): - self.ID = id + def __init__(self, charID, name, client, accessToken=None, refreshToken=None): self.characterID = charID self.characterName = name self.client = client self.accessToken = accessToken self.refreshToken = refreshToken + self.esi_client = None @reconstructor def init(self): @@ -51,7 +51,7 @@ class SsoCharacter(object): def update_token(self, tokenResponse): """ helper function to update token data from SSO response """ self.accessToken = tokenResponse['access_token'] - self.accessTokenExpires = datetime.fromtimestamp( + self.accessTokenExpires = datetime.datetime.fromtimestamp( time.time() + tokenResponse['expires_in'], ) if 'refresh_token' in tokenResponse: diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 718b6654b..902d827e4 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -310,7 +310,9 @@ class CrestMgmt(wx.Dialog): self.lcCharacters = wx.ListCtrl(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LC_REPORT) self.lcCharacters.InsertColumn(0, heading='Character') - self.lcCharacters.InsertColumn(1, heading='Refresh Token') + self.lcCharacters.InsertColumn(1, heading='Character ID') + self.lcCharacters.InsertColumn(2, heading='Access Token') + self.lcCharacters.InsertColumn(3, heading='Refresh Token') self.popCharList() @@ -347,8 +349,8 @@ class CrestMgmt(wx.Dialog): self.lcCharacters.DeleteAllItems() for index, char in enumerate(chars): - self.lcCharacters.InsertItem(index, char.name) - self.lcCharacters.SetStringItem(index, 1, char.refresh_token) + self.lcCharacters.InsertItem(index, char.characterName) + self.lcCharacters.SetStringItem(index, 1, char.refreshToken) self.lcCharacters.SetItemData(index, char.ID) self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE) diff --git a/service/crest.py b/service/crest.py index 51c9f1309..2642b1438 100644 --- a/service/crest.py +++ b/service/crest.py @@ -5,6 +5,9 @@ import threading import copy import uuid import time +import config +import base64 +import json import eos.db from eos.enum import Enum @@ -14,8 +17,20 @@ from service.settings import CRESTSettings from service.server import StoppableHTTPServer, AuthHandler from service.pycrest.eve import EVE +from .esi_security_proxy import EsiSecurityProxy +from esipy import EsiClient +from esipy.cache import FileCache +import os + pyfalog = Logger(__name__) +server = "https://blitzmann.pythonanywhere.com" +cache_path = os.path.join(config.savePath, config.ESI_CACHE) + +if not os.path.exists(cache_path): + os.mkdir(cache_path) + +file_cache = FileCache(cache_path) class Servers(Enum): TQ = 0 @@ -39,6 +54,14 @@ class Crest(object): _instance = None + @classmethod + def genEsiClient(cls, security=None): + return EsiClient( + security=EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) if security is None else security, + cache=file_cache, + headers={'User-Agent': 'pyfa esipy'} + ) + @classmethod def getInstance(cls): if cls._instance is None: @@ -113,32 +136,30 @@ class Crest(object): wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0)) def getCrestCharacters(self): - chars = eos.db.getSsoCharacters() + chars = eos.db.getSsoCharacters(config.getClientSecret()) return chars - def getCrestCharacter(self, charID): + def getSsoCharacter(self, charID): """ Get character, and modify to include the eve connection """ - if charID in self.charCache: - return self.charCache.get(charID) - - char = eos.db.getSsoCharacter(charID) - self.charCache[charID] = char + char = eos.db.getSsoCharacter(charID, config.getClientSecret()) + if char.esi_client is None: + char.esi_client = Crest.genEsiClient() return char def getFittings(self, charID): - char = self.getCrestCharacter(charID) + char = self.getSsoCharacter(charID) return char.eve.get('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID)) def postFitting(self, charID, json): # @todo: new fitting ID can be recovered from Location header, # ie: Location -> https://api-sisi.testeveonline.com/characters/1611853631/fittings/37486494/ - char = self.getCrestCharacter(charID) + char = self.getSsoCharacter(charID) return char.eve.post('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID), data=json) def delFitting(self, charID, fittingID): - char = self.getCrestCharacter(charID) + char = self.getSsoCharacter(charID) return char.eve.delete('%scharacters/%d/fittings/%d/' % (char.eve._authed_endpoint, char.ID, fittingID)) def logout(self): @@ -154,19 +175,26 @@ class Crest(object): def startServer(self): pyfalog.debug("Starting server") + + # we need this to ensure that the previous get_request finishes, and then the socket will close if self.httpd: self.stopServer() time.sleep(1) - # we need this to ensure that the previous get_request finishes, and then the socket will close - self.httpd = StoppableHTTPServer(('localhost', 6461), AuthHandler) + + self.state = str(uuid.uuid4()) + self.httpd = StoppableHTTPServer(('localhost', 0), AuthHandler) + port = self.httpd.socket.getsockname()[1] + + esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) + + uri = esisecurity.get_auth_uri(state=self.state, redirect='http://localhost:{}'.format(port)) self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleLogin,)) - self.serverThread.name = "CRESTServer" + self.serverThread.name = "SsoCallbackServer" self.serverThread.daemon = True self.serverThread.start() - self.state = str(uuid.uuid4()) - return self.eve.auth_uri(scopes=self.scopes, state=self.state) + return uri def handleLogin(self, message): if not message: @@ -178,41 +206,26 @@ class Crest(object): pyfalog.debug("Handling CREST login with: {0}", message) - if 'access_token' in message: # implicit - eve = EVE(**self.eve_options) - eve.temptoken_authorize( - access_token=message['access_token'][0], - expires_in=int(message['expires_in'][0]) - ) - self.ssoTimer = threading.Timer(int(message['expires_in'][0]), self.logout) - self.ssoTimer.start() + auth_response = json.loads(base64.b64decode(message['SSOInfo'][0])) - eve() - info = eve.whoami() + # We need to preload the ESI Security object beforehand with the auth response so that we can use verify to + # get character information + # init the security object + esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) - pyfalog.debug("Got character info: {0}", info) + esisecurity.update_token(auth_response) - self.implicitCharacter = CrestChar(info['CharacterID'], info['CharacterName']) - self.implicitCharacter.eve = eve - # self.implicitCharacter.fetchImage() + # we get the character information + cdata = esisecurity.verify() + print(cdata) - wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.IMPLICIT)) - elif 'code' in message: - eve = EVE(**self.eve_options) - eve.authorize(message['code'][0]) - eve() - info = eve.whoami() + currentCharacter = self.getSsoCharacter(cdata['CharacterID']) - pyfalog.debug("Got character info: {0}", info) + if currentCharacter is None: + currentCharacter = SsoCharacter(cdata['CharacterID'], cdata['CharacterName'], config.getClientSecret()) + currentCharacter.esi_client = Crest.genEsiClient(esisecurity) + currentCharacter.update_token(auth_response) # this also sets the esi security token - # check if we have character already. If so, simply replace refresh_token - char = self.getCrestCharacter(int(info['CharacterID'])) - if char: - char.refresh_token = eve.refresh_token - else: - char = CrestChar(info['CharacterID'], info['CharacterName'], eve.refresh_token) - char.eve = eve - self.charCache[int(info['CharacterID'])] = char - eos.db.save(char) + eos.db.save(currentCharacter) - wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) + wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) # todo: remove user / implicit authentication diff --git a/service/esi_security_proxy.py b/service/esi_security_proxy.py index f6263d5e8..21d0f8510 100644 --- a/service/esi_security_proxy.py +++ b/service/esi_security_proxy.py @@ -26,11 +26,9 @@ class EsiSecurityProxy(object): def __init__( self, - redirect_uri, **kwargs): """ Init the ESI Security Object - :param redirect_uri: the uri to redirect the user after login into SSO :param sso_url: the default sso URL used when no "app" is provided :param esi_url: the default esi URL used for verify endpoint :param app: (optionnal) the pyswagger app object @@ -43,7 +41,6 @@ class EsiSecurityProxy(object): esi_url = kwargs.pop('esi_url', "https://esi.tech.ccp.is") self.security_name = kwargs.pop('security_name', 'evesso') - self.redirect_uri = redirect_uri # we provide app object, so we don't use sso_url if app is not None: diff --git a/service/server.py b/service/server.py index 76c5a964e..34261294a 100644 --- a/service/server.py +++ b/service/server.py @@ -82,14 +82,14 @@ class AuthHandler(http.server.BaseHTTPRequestHandler): try: if step2: self.server.callback(parts) - pyfalog.info("Successfully logged into CREST.") + pyfalog.info("Successfully logged into EVE.") msg = "If you see this message then it means you should be logged into CREST. You may close this window and return to the application." else: # For implicit mode, we have to serve up the page which will take the hash and redirect useing a querystring pyfalog.info("Processing response from EVE Online.") msg = "Processing response from EVE Online" except Exception as ex: - pyfalog.error("Error in CREST AuthHandler") + pyfalog.error("Error logging into EVE") pyfalog.error(ex) msg = "

Error

\n

{}

".format(ex.message) finally: @@ -109,10 +109,10 @@ class AuthHandler(http.server.BaseHTTPRequestHandler): class StoppableHTTPServer(http.server.HTTPServer): def server_bind(self): http.server.HTTPServer.server_bind(self) - self.settings = CRESTSettings.getInstance() + # self.settings = CRESTSettings.getInstance() # Allow listening for x seconds - sec = self.settings.get('timeout') + sec = 120 pyfalog.debug("Running server for {0} seconds", sec) self.socket.settimeout(1) @@ -131,7 +131,7 @@ class StoppableHTTPServer(http.server.HTTPServer): pass def stop(self): - pyfalog.warning("Setting CREST server to stop.") + pyfalog.warning("Setting pyfa server to stop.") self.run = False def handle_timeout(self): From eea80195939b1ff4e114e70e7cda42dadc91e574 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 7 Feb 2018 01:21:22 -0500 Subject: [PATCH 04/39] Get fitting browser to show up with proper characters (still not functional). Start testing ways to store the esipy app on the service (it can take a few seconds to initialize due to network calls) --- gui/crestFittings.py | 21 +++++++-------------- gui/mainFrame.py | 2 +- gui/mainMenuBar.py | 4 ++-- service/crest.py | 21 +++++++++++++++++++-- utils/timer.py | 2 +- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 902d827e4..f01258f80 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -34,16 +34,9 @@ class CrestFittings(wx.Frame): characterSelectSizer = wx.BoxSizer(wx.HORIZONTAL) - if sCrest.settings.get('mode') == CrestModes.IMPLICIT: - self.stLogged = wx.StaticText(self, wx.ID_ANY, "Currently logged in as %s" % sCrest.implicitCharacter.name, - wx.DefaultPosition, wx.DefaultSize) - self.stLogged.Wrap(-1) - - characterSelectSizer.Add(self.stLogged, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - else: - self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, []) - characterSelectSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - self.updateCharList() + self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, []) + characterSelectSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) + self.updateCharList() self.fetchBtn = wx.Button(self, wx.ID_ANY, "Fetch Fits", wx.DefaultPosition, wx.DefaultSize, 5) characterSelectSizer.Add(self.fetchBtn, 0, wx.ALL, 5) @@ -99,14 +92,14 @@ class CrestFittings(wx.Frame): def updateCharList(self): sCrest = Crest.getInstance() - chars = sCrest.getCrestCharacters() + chars = sCrest.getSsoCharacters() if len(chars) == 0: self.Close() self.charChoice.Clear() for char in chars: - self.charChoice.Append(char.name, char.ID) + self.charChoice.Append(char.characterName, char.characterID) self.charChoice.SetSelection(0) @@ -232,7 +225,7 @@ class ExportToEve(wx.Frame): def updateCharList(self): sCrest = Crest.getInstance() - chars = sCrest.getCrestCharacters() + chars = sCrest.getSsoCharacters() if len(chars) == 0: self.Close() @@ -344,7 +337,7 @@ class CrestMgmt(wx.Dialog): def popCharList(self): sCrest = Crest.getInstance() - chars = sCrest.getCrestCharacters() + chars = sCrest.getSsoCharacters() self.lcCharacters.DeleteAllItems() diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 735db0d80..95c57e119 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -657,7 +657,7 @@ class MainFrame(wx.Frame): menu.Enable(menu.exportToEveId, False) else: menu.SetLabel(menu.ssoLoginId, "Manage Characters") - enable = len(sCrest.getCrestCharacters()) == 0 + enable = len(sCrest.getSsoCharacters()) == 0 menu.Enable(menu.eveFittingsId, not enable) menu.Enable(menu.exportToEveId, not enable) diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index 8b5b77e60..e9e63b82b 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -145,8 +145,8 @@ class MainMenuBar(wx.MenuBar): crestMenu.Append(self.exportToEveId, "Export To EVE") # if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0: - self.Enable(self.eveFittingsId, False) - self.Enable(self.exportToEveId, False) + self.Enable(self.eveFittingsId, True) + self.Enable(self.exportToEveId, True) if not self.mainFrame.disableOverrideEditor: windowMenu.AppendSeparator() diff --git a/service/crest.py b/service/crest.py index 2642b1438..60d043707 100644 --- a/service/crest.py +++ b/service/crest.py @@ -18,9 +18,11 @@ from service.server import StoppableHTTPServer, AuthHandler from service.pycrest.eve import EVE from .esi_security_proxy import EsiSecurityProxy -from esipy import EsiClient +from esipy import EsiClient, EsiApp from esipy.cache import FileCache import os +import logging + pyfalog = Logger(__name__) @@ -41,6 +43,7 @@ class CrestModes(Enum): IMPLICIT = 0 USER = 1 +from utils.timer import Timer class Crest(object): clientIDs = { @@ -54,6 +57,16 @@ class Crest(object): _instance = None + @classmethod + def initEsiApp(cls): + with Timer() as t: + cls.esiapp = EsiApp(cache=None) + + with Timer() as t: + cls.esi_v1 = cls.esiapp.get_v1_swagger + with Timer() as t: + cls.esi_v4 = cls.esiapp.get_v4_swagger + @classmethod def genEsiClient(cls, security=None): return EsiClient( @@ -135,7 +148,7 @@ class Crest(object): self.charCache = {} wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0)) - def getCrestCharacters(self): + def getSsoCharacters(self): chars = eos.db.getSsoCharacters(config.getClientSecret()) return chars @@ -150,6 +163,10 @@ class Crest(object): def getFittings(self, charID): char = self.getSsoCharacter(charID) + op = esi_v1.op['get_characters_character_id_fittings']( + character_id=self.currentCharacter.character_id + ) + resp = self.currentCharacter.esi_client.request(op) return char.eve.get('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID)) def postFitting(self, charID, json): diff --git a/utils/timer.py b/utils/timer.py index 9b7daf4dc..da7042f59 100644 --- a/utils/timer.py +++ b/utils/timer.py @@ -19,7 +19,7 @@ class Timer(object): def checkpoint(self, name=''): text = 'Timer - {timer} - {checkpoint} - {last:.2f}ms ({elapsed:.2f}ms elapsed)'.format( timer=self.name, - checkpoint=str(name, "utf-8"), + checkpoint=name, last=self.last, elapsed=self.elapsed ).strip() From e77dddc15be3221f45ae8f8c6a03f2c7d7727c4b Mon Sep 17 00:00:00 2001 From: blitzmann Date: Wed, 7 Feb 2018 02:07:42 -0500 Subject: [PATCH 05/39] More work on getting fittings form a character via ESI. It's starting to be come a pain working with this client with having to background it until it initializes. Thinking about rolling my own, considering we only need a few calls and not a whole package. --- eos/saveddata/ssocharacter.py | 4 ++-- gui/crestFittings.py | 5 ----- gui/mainFrame.py | 2 ++ service/crest.py | 29 +++++++++++++++++++++++++---- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/eos/saveddata/ssocharacter.py b/eos/saveddata/ssocharacter.py index 88fcc4a24..95314f5e1 100644 --- a/eos/saveddata/ssocharacter.py +++ b/eos/saveddata/ssocharacter.py @@ -35,7 +35,7 @@ class SsoCharacter(object): @reconstructor def init(self): - pass + self.esi_client = None def get_sso_data(self): """ Little "helper" function to get formated data for esipy security @@ -44,7 +44,7 @@ class SsoCharacter(object): 'access_token': self.accessToken, 'refresh_token': self.refreshToken, 'expires_in': ( - self.accessTokenExpires - datetime.utcnow() + self.accessTokenExpires - datetime.datetime.utcnow() ).total_seconds() } diff --git a/gui/crestFittings.py b/gui/crestFittings.py index f01258f80..dc46476f7 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -126,11 +126,6 @@ class CrestFittings(wx.Frame): event.Skip() def getActiveCharacter(self): - sCrest = Crest.getInstance() - - if sCrest.settings.get('mode') == CrestModes.IMPLICIT: - return sCrest.implicitCharacter.ID - selection = self.charChoice.GetCurrentSelection() return self.charChoice.GetClientData(selection) if selection is not None else None diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 95c57e119..9d3185c07 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -166,6 +166,8 @@ class MainFrame(wx.Frame): i = wx.Icon(BitmapLoader.getBitmap("pyfa", "gui")) self.SetIcon(i) + sCrest = Crest() + # Create the layout and windows mainSizer = wx.BoxSizer(wx.HORIZONTAL) diff --git a/service/crest.py b/service/crest.py index 60d043707..3b920a23b 100644 --- a/service/crest.py +++ b/service/crest.py @@ -34,6 +34,9 @@ if not os.path.exists(cache_path): file_cache = FileCache(cache_path) +esiRdy = threading.Event() + + class Servers(Enum): TQ = 0 SISI = 1 @@ -45,6 +48,17 @@ class CrestModes(Enum): from utils.timer import Timer + + +class EsiInitThread(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.name = "EsiInitThread" + + def run(self): + Crest.initEsiApp() + + class Crest(object): clientIDs = { Servers.TQ : 'f9be379951c046339dc13a00e6be7704', @@ -67,6 +81,8 @@ class Crest(object): with Timer() as t: cls.esi_v4 = cls.esiapp.get_v4_swagger + esiRdy.set() + @classmethod def genEsiClient(cls, security=None): return EsiClient( @@ -103,6 +119,10 @@ class Crest(object): characters still in the cache (if USER mode) """ + prefetch = EsiInitThread() + prefetch.daemon = True + prefetch.start() + self.settings = CRESTSettings.getInstance() self.scopes = ['characterFittingsRead', 'characterFittingsWrite'] @@ -159,15 +179,16 @@ class Crest(object): char = eos.db.getSsoCharacter(charID, config.getClientSecret()) if char.esi_client is None: char.esi_client = Crest.genEsiClient() + char.esi_client.security.update_token(char.get_sso_data()) return char def getFittings(self, charID): char = self.getSsoCharacter(charID) - op = esi_v1.op['get_characters_character_id_fittings']( - character_id=self.currentCharacter.character_id + op = Crest.esi_v1.op['get_characters_character_id_fittings']( + character_id=charID ) - resp = self.currentCharacter.esi_client.request(op) - return char.eve.get('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID)) + resp = char.esi_client.request(op) + return resp def postFitting(self, charID, json): # @todo: new fitting ID can be recovered from Location header, From e025bff99b86b26d2293e882cf099c818c017936 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 8 Feb 2018 01:24:40 -0500 Subject: [PATCH 06/39] Get ESI fitting import working completely. Use a file cache for EsiApp to prevent long startup times (possibly cache for about a week or so, and start in background if it needs to be gotten again?) --- gui/crestFittings.py | 17 +++++++++-------- gui/mainFrame.py | 2 -- service/crest.py | 30 +++++++++++------------------- service/port.py | 12 ++++++------ 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/gui/crestFittings.py b/gui/crestFittings.py index dc46476f7..4f3d8ea42 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -134,9 +134,9 @@ class CrestFittings(wx.Frame): try: waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self) fittings = sCrest.getFittings(self.getActiveCharacter()) - self.cacheTime = fittings.get('cached_until') - self.updateCacheStatus(None) - self.cacheTimer.Start(1000) + # self.cacheTime = fittings.get('cached_until') + # self.updateCacheStatus(None) + # self.cacheTimer.Start(1000) self.fitTree.populateSkillTree(fittings) del waitDialog except requests.exceptions.ConnectionError: @@ -385,11 +385,12 @@ class FittingsTreeView(wx.Panel): tree.DeleteChildren(root) dict = {} - fits = data['items'] + fits = data for fit in fits: - if fit['ship']['name'] not in dict: - dict[fit['ship']['name']] = [] - dict[fit['ship']['name']].append(fit) + ship = getItem(fit['ship_type_id']) + if ship.name not in dict: + dict[ship.name ] = [] + dict[ship.name ].append(fit) for name, fits in dict.items(): shipID = tree.AppendItem(root, name) @@ -412,7 +413,7 @@ class FittingsTreeView(wx.Panel): for item in fit['items']: try: - cargo = Cargo(getItem(item['type']['id'])) + cargo = Cargo(getItem(item['type_id'])) cargo.amount = item['quantity'] list.append(cargo) except Exception as e: diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 9d3185c07..95c57e119 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -166,8 +166,6 @@ class MainFrame(wx.Frame): i = wx.Icon(BitmapLoader.getBitmap("pyfa", "gui")) self.SetIcon(i) - sCrest = Crest() - # Create the layout and windows mainSizer = wx.BoxSizer(wx.HORIZONTAL) diff --git a/service/crest.py b/service/crest.py index 3b920a23b..5072ed782 100644 --- a/service/crest.py +++ b/service/crest.py @@ -50,15 +50,6 @@ from utils.timer import Timer -class EsiInitThread(threading.Thread): - def __init__(self): - threading.Thread.__init__(self) - self.name = "EsiInitThread" - - def run(self): - Crest.initEsiApp() - - class Crest(object): clientIDs = { Servers.TQ : 'f9be379951c046339dc13a00e6be7704', @@ -73,15 +64,14 @@ class Crest(object): @classmethod def initEsiApp(cls): - with Timer() as t: - cls.esiapp = EsiApp(cache=None) - - with Timer() as t: + with Timer("Main EsiApp") as t: + cls.esiapp = EsiApp(cache=file_cache) + with Timer('ESI v1') as t: cls.esi_v1 = cls.esiapp.get_v1_swagger - with Timer() as t: + with Timer('ESI v4') as t: cls.esi_v4 = cls.esiapp.get_v4_swagger - esiRdy.set() + # esiRdy.set() @classmethod def genEsiClient(cls, security=None): @@ -118,10 +108,12 @@ class Crest(object): mode. The mode is sent as an argument, as well as the umber of characters still in the cache (if USER mode) """ + Crest.initEsiApp() - prefetch = EsiInitThread() - prefetch.daemon = True - prefetch.start() + + # prefetch = EsiInitThread() + # prefetch.daemon = True + # prefetch.start() self.settings = CRESTSettings.getInstance() self.scopes = ['characterFittingsRead', 'characterFittingsWrite'] @@ -188,7 +180,7 @@ class Crest(object): character_id=charID ) resp = char.esi_client.request(op) - return resp + return resp.data def postFitting(self, charID, json): # @todo: new fitting ID can be recovered from Location header, diff --git a/service/port.py b/service/port.py index c61237b5c..1623d7005 100644 --- a/service/port.py +++ b/service/port.py @@ -491,7 +491,7 @@ class Port(object): # If JSON-style start, parse as CREST/JSON if firstLine[0] == '{': - return "JSON", (cls.importCrest(string),) + return "JSON", (cls.importESI(string),) # If we've got source file name which is used to describe ship name # and first line contains something like [setup name], detect as eft config file @@ -509,7 +509,7 @@ class Port(object): return "DNA", (cls.importDna(string),) @staticmethod - def importCrest(str_): + def importESI(str_): sMkt = Market.getInstance() fitobj = Fit() @@ -521,11 +521,11 @@ class Port(object): fitobj.notes = refobj['description'] try: - refobj = refobj['ship']['id'] + ship = refobj['ship_type_id'] try: - fitobj.ship = Ship(sMkt.getItem(refobj)) + fitobj.ship = Ship(sMkt.getItem(ship)) except ValueError: - fitobj.ship = Citadel(sMkt.getItem(refobj)) + fitobj.ship = Citadel(sMkt.getItem(ship)) except: pyfalog.warning("Caught exception in importCrest") return None @@ -535,7 +535,7 @@ class Port(object): moduleList = [] for module in items: try: - item = sMkt.getItem(module['type']['id'], eager="group.category") + item = sMkt.getItem(module['type_id'], eager="group.category") if module['flag'] == INV_FLAG_DRONEBAY: d = Drone(item) d.amount = module['quantity'] From 5fbe623ae63ac0349d95c51e46f737d51613512b Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 8 Feb 2018 01:38:24 -0500 Subject: [PATCH 07/39] get fit deletion working. Need to be aware that we are still using cached fit listing... --- eos/db/saveddata/queries.py | 2 +- gui/crestFittings.py | 8 ++++---- service/crest.py | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 9d4eaf55f..087561070 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -474,7 +474,7 @@ def getSsoCharacters(clientHash, eager=None): return characters -@cachedQuery(SsoCharacter, 1, "lookfor") +@cachedQuery(SsoCharacter, 1, "lookfor", "clientHash") def getSsoCharacter(lookfor, clientHash, eager=None): filter = SsoCharacter.client == clientHash diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 4f3d8ea42..760cf40b6 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -161,12 +161,12 @@ class CrestFittings(wx.Frame): data = json.loads(self.fitTree.fittingsTreeCtrl.GetItemData(selection)) dlg = wx.MessageDialog(self, - "Do you really want to delete %s (%s) from EVE?" % (data['name'], data['ship']['name']), + "Do you really want to delete %s (%s) from EVE?" % (data['name'], getItem(data['ship_type_id']).name), "Confirm Delete", wx.YES | wx.NO | wx.ICON_QUESTION) if dlg.ShowModal() == wx.ID_YES: try: - sCrest.delFitting(self.getActiveCharacter(), data['fittingID']) + sCrest.delFitting(self.getActiveCharacter(), data['fitting_id']) except requests.exceptions.ConnectionError: msg = "Connection error, please check your internet connection" pyfalog.error(msg) @@ -389,8 +389,8 @@ class FittingsTreeView(wx.Panel): for fit in fits: ship = getItem(fit['ship_type_id']) if ship.name not in dict: - dict[ship.name ] = [] - dict[ship.name ].append(fit) + dict[ship.name] = [] + dict[ship.name].append(fit) for name, fits in dict.items(): shipID = tree.AppendItem(root, name) diff --git a/service/crest.py b/service/crest.py index 5072ed782..ce06a0e6c 100644 --- a/service/crest.py +++ b/service/crest.py @@ -60,6 +60,10 @@ class Crest(object): clientCallback = 'http://localhost:6461' clientTest = True + esiapp = None + esi_v1 = None + esi_v4 = None + _instance = None @classmethod @@ -176,6 +180,7 @@ class Crest(object): def getFittings(self, charID): char = self.getSsoCharacter(charID) + print(repr(char)) op = Crest.esi_v1.op['get_characters_character_id_fittings']( character_id=charID ) @@ -190,7 +195,15 @@ class Crest(object): def delFitting(self, charID, fittingID): char = self.getSsoCharacter(charID) - return char.eve.delete('%scharacters/%d/fittings/%d/' % (char.eve._authed_endpoint, char.ID, fittingID)) + print(repr(char)) + op = Crest.esi_v1.op['delete_characters_character_id_fittings_fitting_id']( + character_id=charID, + fitting_id=fittingID + ) + + resp = char.esi_client.request(op) + return resp.data + def logout(self): """Logout of implicit character""" From dfba03319087e82fe45c25313defd51d8b2d7a6e Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 8 Feb 2018 01:50:52 -0500 Subject: [PATCH 08/39] Implement posting fit to EVE server --- gui/crestFittings.py | 39 ++++++++++++++------------------------- gui/mainFrame.py | 2 +- service/crest.py | 15 +++++++++++---- service/port.py | 26 +++++++------------------- 4 files changed, 33 insertions(+), 49 deletions(-) diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 760cf40b6..fdd82312f 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -185,17 +185,10 @@ class ExportToEve(wx.Frame): mainSizer = wx.BoxSizer(wx.VERTICAL) hSizer = wx.BoxSizer(wx.HORIZONTAL) - if sCrest.settings.get('mode') == CrestModes.IMPLICIT: - self.stLogged = wx.StaticText(self, wx.ID_ANY, "Currently logged in as %s" % sCrest.implicitCharacter.name, - wx.DefaultPosition, wx.DefaultSize) - self.stLogged.Wrap(-1) - - hSizer.Add(self.stLogged, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - else: - self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, []) - hSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - self.updateCharList() - self.charChoice.SetSelection(0) + self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, []) + hSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) + self.updateCharList() + self.charChoice.SetSelection(0) self.exportBtn = wx.Button(self, wx.ID_ANY, "Export Fit", wx.DefaultPosition, wx.DefaultSize, 5) hSizer.Add(self.exportBtn, 0, wx.ALL, 5) @@ -227,7 +220,7 @@ class ExportToEve(wx.Frame): self.charChoice.Clear() for char in chars: - self.charChoice.Append(char.name, char.ID) + self.charChoice.Append(char.characterName, char.characterID) self.charChoice.SetSelection(0) @@ -249,11 +242,6 @@ class ExportToEve(wx.Frame): event.Skip() def getActiveCharacter(self): - sCrest = Crest.getInstance() - - if sCrest.settings.get('mode') == CrestModes.IMPLICIT: - return sCrest.implicitCharacter.ID - selection = self.charChoice.GetCurrentSelection() return self.charChoice.GetClientData(selection) if selection is not None else None @@ -272,16 +260,17 @@ class ExportToEve(wx.Frame): try: sFit = Fit.getInstance() - data = sPort.exportCrest(sFit.getFit(fitID)) + data = sPort.exportESI(sFit.getFit(fitID)) res = sCrest.postFitting(self.getActiveCharacter(), data) - self.statusbar.SetStatusText("%d: %s" % (res.status_code, res.reason), 0) - try: - text = json.loads(res.text) - self.statusbar.SetStatusText(text['message'], 1) - except ValueError: - pyfalog.warning("Value error on loading JSON.") - self.statusbar.SetStatusText("", 1) + self.statusbar.SetStatusText("", 0) + self.statusbar.SetStatusText("", 1) + # try: + # text = json.loads(res.text) + # self.statusbar.SetStatusText(text['message'], 1) + # except ValueError: + # pyfalog.warning("Value error on loading JSON.") + # self.statusbar.SetStatusText("", 1) except requests.exceptions.ConnectionError: msg = "Connection error, please check your internet connection" pyfalog.error(msg) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 95c57e119..46e376976 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -740,7 +740,7 @@ class MainFrame(wx.Frame): def clipboardCrest(self): fit = db_getFit(self.getActiveFit()) - toClipboard(Port.exportCrest(fit)) + toClipboard(Port.exportESI(fit)) def clipboardXml(self): fit = db_getFit(self.getActiveFit()) diff --git a/service/crest.py b/service/crest.py index ce06a0e6c..2f85815b8 100644 --- a/service/crest.py +++ b/service/crest.py @@ -187,11 +187,18 @@ class Crest(object): resp = char.esi_client.request(op) return resp.data - def postFitting(self, charID, json): - # @todo: new fitting ID can be recovered from Location header, - # ie: Location -> https://api-sisi.testeveonline.com/characters/1611853631/fittings/37486494/ + def postFitting(self, charID, json_str): + # @todo: new fitting ID can be recovered from resp.data, char = self.getSsoCharacter(charID) - return char.eve.post('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID), data=json) + + op = Crest.esi_v1.op['post_characters_character_id_fittings']( + character_id=char.characterID, + fitting=json.loads(json_str) + ) + + resp = char.esi_client.request(op) + + return resp.data def delFitting(self, charID, fittingID): char = self.getSsoCharacter(charID) diff --git a/service/port.py b/service/port.py index 1623d7005..c3cf352c7 100644 --- a/service/port.py +++ b/service/port.py @@ -381,7 +381,7 @@ class Port(object): """Service which houses all import/export format functions""" @classmethod - def exportCrest(cls, ofit, callback=None): + def exportESI(cls, ofit, callback=None): # A few notes: # max fit name length is 50 characters # Most keys are created simply because they are required, but bogus data is okay @@ -396,9 +396,7 @@ class Port(object): # max length is 50 characters name = ofit.name[:47] + '...' if len(ofit.name) > 50 else ofit.name fit['name'] = name - fit['ship']['href'] = "%sinventory/types/%d/" % (eve._authed_endpoint, ofit.ship.item.ID) - fit['ship']['id'] = ofit.ship.item.ID - fit['ship']['name'] = '' + fit['ship_type_id'] = ofit.ship.item.ID # 2017/03/29 NOTE: "<" or "<" is Ignored # fit['description'] = "" % ofit.ID @@ -426,9 +424,7 @@ class Port(object): slotNum[slot] += 1 item['quantity'] = 1 - item['type']['href'] = "%sinventory/types/%d/" % (eve._authed_endpoint, module.item.ID) - item['type']['id'] = module.item.ID - item['type']['name'] = '' + item['type_id'] = module.item.ID fit['items'].append(item) if module.charge and sFit.serviceFittingOptions["exportCharges"]: @@ -441,36 +437,28 @@ class Port(object): item = nested_dict() item['flag'] = INV_FLAG_CARGOBAY item['quantity'] = cargo.amount - item['type']['href'] = "%sinventory/types/%d/" % (eve._authed_endpoint, cargo.item.ID) - item['type']['id'] = cargo.item.ID - item['type']['name'] = '' + item['type_id'] = cargo.item.ID fit['items'].append(item) for chargeID, amount in list(charges.items()): item = nested_dict() item['flag'] = INV_FLAG_CARGOBAY item['quantity'] = amount - item['type']['href'] = "%sinventory/types/%d/" % (eve._authed_endpoint, chargeID) - item['type']['id'] = chargeID - item['type']['name'] = '' + item['type_id'] = chargeID fit['items'].append(item) for drone in ofit.drones: item = nested_dict() item['flag'] = INV_FLAG_DRONEBAY item['quantity'] = drone.amount - item['type']['href'] = "%sinventory/types/%d/" % (eve._authed_endpoint, drone.item.ID) - item['type']['id'] = drone.item.ID - item['type']['name'] = '' + item['type_id'] = drone.item.ID fit['items'].append(item) for fighter in ofit.fighters: item = nested_dict() item['flag'] = INV_FLAG_FIGHTER item['quantity'] = fighter.amountActive - item['type']['href'] = "%sinventory/types/%d/" % (eve._authed_endpoint, fighter.item.ID) - item['type']['id'] = fighter.item.ID - item['type']['name'] = fighter.item.name + item['type_id'] = fighter.item.ID fit['items'].append(item) return json.dumps(fit) From cb392e7e5f2bc5cb7ed089f3764a085e5033046d Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 8 Feb 2018 01:52:38 -0500 Subject: [PATCH 09/39] Rename a few things --- .../pyfaCrestPreferences.py | 8 +- gui/crestFittings.py | 22 +- gui/mainFrame.py | 8 +- service/crest.py | 281 ------------------ service/esi.py | 243 ++++++++++++++- service/port.py | 4 +- 6 files changed, 260 insertions(+), 306 deletions(-) delete mode 100644 service/crest.py diff --git a/gui/builtinPreferenceViews/pyfaCrestPreferences.py b/gui/builtinPreferenceViews/pyfaCrestPreferences.py index f2dbe694d..f3da3dbeb 100644 --- a/gui/builtinPreferenceViews/pyfaCrestPreferences.py +++ b/gui/builtinPreferenceViews/pyfaCrestPreferences.py @@ -11,7 +11,7 @@ from service.settings import CRESTSettings # noinspection PyPackageRequirements from wx.lib.intctrl import IntCtrl -from service.crest import Crest +from service.esi import Esi class PFCrestPref(PreferenceView): @@ -119,16 +119,16 @@ class PFCrestPref(PreferenceView): def OnModeChange(self, event): self.settings.set('mode', event.GetInt()) self.ToggleProxySettings(self.settings.get('mode')) - Crest.restartService() + Esi.restartService() def OnServerChange(self, event): self.settings.set('server', event.GetInt()) - Crest.restartService() + Esi.restartService() def OnBtnApply(self, event): self.settings.set('clientID', self.inputClientID.GetValue().strip()) self.settings.set('clientSecret', self.inputClientSecret.GetValue().strip()) - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() sCrest.delAllCharacters() def ToggleProxySettings(self, mode): diff --git a/gui/crestFittings.py b/gui/crestFittings.py index fdd82312f..69e8b6085 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -16,7 +16,7 @@ import gui.globalEvents as GE from logbook import Logger import calendar -from service.crest import Crest, CrestModes +from service.esi import Esi, CrestModes pyfalog = Logger(__name__) @@ -30,7 +30,7 @@ class CrestFittings(wx.Frame): self.mainFrame = parent mainSizer = wx.BoxSizer(wx.VERTICAL) - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() characterSelectSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -91,7 +91,7 @@ class CrestFittings(wx.Frame): event.Skip() def updateCharList(self): - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() chars = sCrest.getSsoCharacters() if len(chars) == 0: @@ -130,7 +130,7 @@ class CrestFittings(wx.Frame): return self.charChoice.GetClientData(selection) if selection is not None else None def fetchFittings(self, event): - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() try: waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self) fittings = sCrest.getFittings(self.getActiveCharacter()) @@ -154,7 +154,7 @@ class CrestFittings(wx.Frame): self.mainFrame._openAfterImport(fits) def deleteFitting(self, event): - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() selection = self.fitView.fitSelection if not selection: return @@ -181,7 +181,7 @@ class ExportToEve(wx.Frame): self.mainFrame = parent self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() mainSizer = wx.BoxSizer(wx.VERTICAL) hSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -212,7 +212,7 @@ class ExportToEve(wx.Frame): self.Centre(wx.BOTH) def updateCharList(self): - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() chars = sCrest.getSsoCharacters() if len(chars) == 0: @@ -256,7 +256,7 @@ class ExportToEve(wx.Frame): return self.statusbar.SetStatusText("Sending request and awaiting response", 1) - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() try: sFit = Fit.getInstance() @@ -320,7 +320,7 @@ class CrestMgmt(wx.Dialog): event.Skip() def popCharList(self): - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() chars = sCrest.getSsoCharacters() self.lcCharacters.DeleteAllItems() @@ -335,7 +335,7 @@ class CrestMgmt(wx.Dialog): @staticmethod def addChar(event): - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() uri = sCrest.startServer() webbrowser.open(uri) @@ -343,7 +343,7 @@ class CrestMgmt(wx.Dialog): item = self.lcCharacters.GetFirstSelected() if item > -1: charID = self.lcCharacters.GetItemData(item) - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() sCrest.delCrestCharacter(charID) self.popCharList() diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 46e376976..7cef8ea3f 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -82,8 +82,8 @@ import threading import webbrowser import wx.adv -from service.crest import Crest -from service.crest import CrestModes +from service.esi import Esi +from service.esi import CrestModes from gui.crestFittings import CrestFittings, ExportToEve, CrestMgmt disableOverrideEditor = False @@ -614,7 +614,7 @@ class MainFrame(wx.Frame): dlg.Show() def updateTitle(self, event): - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() char = sCrest.implicitCharacter if char: t = time.gmtime(char.eve.expires - time.time()) @@ -649,7 +649,7 @@ class MainFrame(wx.Frame): self.SetTitle(self.title) menu = self.GetMenuBar() - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() if type == CrestModes.IMPLICIT: menu.SetLabel(menu.ssoLoginId, "Login to EVE") diff --git a/service/crest.py b/service/crest.py deleted file mode 100644 index 2f85815b8..000000000 --- a/service/crest.py +++ /dev/null @@ -1,281 +0,0 @@ -# noinspection PyPackageRequirements -import wx -from logbook import Logger -import threading -import copy -import uuid -import time -import config -import base64 -import json - -import eos.db -from eos.enum import Enum -from eos.saveddata.ssocharacter import SsoCharacter -import gui.globalEvents as GE -from service.settings import CRESTSettings -from service.server import StoppableHTTPServer, AuthHandler -from service.pycrest.eve import EVE - -from .esi_security_proxy import EsiSecurityProxy -from esipy import EsiClient, EsiApp -from esipy.cache import FileCache -import os -import logging - - -pyfalog = Logger(__name__) - -server = "https://blitzmann.pythonanywhere.com" -cache_path = os.path.join(config.savePath, config.ESI_CACHE) - -if not os.path.exists(cache_path): - os.mkdir(cache_path) - -file_cache = FileCache(cache_path) - -esiRdy = threading.Event() - - -class Servers(Enum): - TQ = 0 - SISI = 1 - - -class CrestModes(Enum): - IMPLICIT = 0 - USER = 1 - -from utils.timer import Timer - - - -class Crest(object): - clientIDs = { - Servers.TQ : 'f9be379951c046339dc13a00e6be7704', - Servers.SISI: 'af87365240d644f7950af563b8418bad' - } - - # @todo: move this to settings - clientCallback = 'http://localhost:6461' - clientTest = True - - esiapp = None - esi_v1 = None - esi_v4 = None - - _instance = None - - @classmethod - def initEsiApp(cls): - with Timer("Main EsiApp") as t: - cls.esiapp = EsiApp(cache=file_cache) - with Timer('ESI v1') as t: - cls.esi_v1 = cls.esiapp.get_v1_swagger - with Timer('ESI v4') as t: - cls.esi_v4 = cls.esiapp.get_v4_swagger - - # esiRdy.set() - - @classmethod - def genEsiClient(cls, security=None): - return EsiClient( - security=EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) if security is None else security, - cache=file_cache, - headers={'User-Agent': 'pyfa esipy'} - ) - - @classmethod - def getInstance(cls): - if cls._instance is None: - cls._instance = Crest() - - return cls._instance - - @classmethod - def restartService(cls): - # This is here to reseed pycrest values when changing preferences - # We first stop the server n case one is running, as creating a new - # instance doesn't do this. - if cls._instance.httpd: - cls._instance.stopServer() - cls._instance = Crest() - cls._instance.mainFrame.updateCrestMenus(type=cls._instance.settings.get('mode')) - return cls._instance - - def __init__(self): - """ - A note on login/logout events: the character login events happen - whenever a characters is logged into via the SSO, regardless of mod. - However, the mode should be send as an argument. Similarily, - the Logout even happens whenever the character is deleted for either - mode. The mode is sent as an argument, as well as the umber of - characters still in the cache (if USER mode) - """ - Crest.initEsiApp() - - - # prefetch = EsiInitThread() - # prefetch.daemon = True - # prefetch.start() - - self.settings = CRESTSettings.getInstance() - self.scopes = ['characterFittingsRead', 'characterFittingsWrite'] - - # these will be set when needed - self.httpd = None - self.state = None - self.ssoTimer = None - - self.eve_options = { - 'client_id': self.settings.get('clientID') if self.settings.get('mode') == CrestModes.USER else self.clientIDs.get(self.settings.get('server')), - 'api_key': self.settings.get('clientSecret') if self.settings.get('mode') == CrestModes.USER else None, - 'redirect_uri': self.clientCallback, - 'testing': self.isTestServer - } - - # Base EVE connection that is copied to all characters - self.eve = EVE(**self.eve_options) - - self.implicitCharacter = None - - # The database cache does not seem to be working for some reason. Use - # this as a temporary measure - self.charCache = {} - - # need these here to post events - import gui.mainFrame # put this here to avoid loop - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - - @property - def isTestServer(self): - return self.settings.get('server') == Servers.SISI - - def delCrestCharacter(self, charID): - char = eos.db.getSsoCharacter(charID) - del self.charCache[char.ID] - eos.db.remove(char) - wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=len(self.charCache))) - - def delAllCharacters(self): - chars = eos.db.getSsoCharacters() - for char in chars: - eos.db.remove(char) - self.charCache = {} - wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0)) - - def getSsoCharacters(self): - chars = eos.db.getSsoCharacters(config.getClientSecret()) - return chars - - def getSsoCharacter(self, charID): - """ - Get character, and modify to include the eve connection - """ - char = eos.db.getSsoCharacter(charID, config.getClientSecret()) - if char.esi_client is None: - char.esi_client = Crest.genEsiClient() - char.esi_client.security.update_token(char.get_sso_data()) - return char - - def getFittings(self, charID): - char = self.getSsoCharacter(charID) - print(repr(char)) - op = Crest.esi_v1.op['get_characters_character_id_fittings']( - character_id=charID - ) - resp = char.esi_client.request(op) - return resp.data - - def postFitting(self, charID, json_str): - # @todo: new fitting ID can be recovered from resp.data, - char = self.getSsoCharacter(charID) - - op = Crest.esi_v1.op['post_characters_character_id_fittings']( - character_id=char.characterID, - fitting=json.loads(json_str) - ) - - resp = char.esi_client.request(op) - - return resp.data - - def delFitting(self, charID, fittingID): - char = self.getSsoCharacter(charID) - print(repr(char)) - op = Crest.esi_v1.op['delete_characters_character_id_fittings_fitting_id']( - character_id=charID, - fitting_id=fittingID - ) - - resp = char.esi_client.request(op) - return resp.data - - - def logout(self): - """Logout of implicit character""" - pyfalog.debug("Character logout") - self.implicitCharacter = None - wx.PostEvent(self.mainFrame, GE.SsoLogout(type=self.settings.get('mode'))) - - def stopServer(self): - pyfalog.debug("Stopping Server") - self.httpd.stop() - self.httpd = None - - def startServer(self): - pyfalog.debug("Starting server") - - # we need this to ensure that the previous get_request finishes, and then the socket will close - if self.httpd: - self.stopServer() - time.sleep(1) - - self.state = str(uuid.uuid4()) - self.httpd = StoppableHTTPServer(('localhost', 0), AuthHandler) - port = self.httpd.socket.getsockname()[1] - - esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) - - uri = esisecurity.get_auth_uri(state=self.state, redirect='http://localhost:{}'.format(port)) - - self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleLogin,)) - self.serverThread.name = "SsoCallbackServer" - self.serverThread.daemon = True - self.serverThread.start() - - return uri - - def handleLogin(self, message): - if not message: - raise Exception("Could not parse out querystring parameters.") - - if message['state'][0] != self.state: - pyfalog.warn("OAUTH state mismatch") - raise Exception("OAUTH State Mismatch.") - - pyfalog.debug("Handling CREST login with: {0}", message) - - auth_response = json.loads(base64.b64decode(message['SSOInfo'][0])) - - # We need to preload the ESI Security object beforehand with the auth response so that we can use verify to - # get character information - # init the security object - esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) - - esisecurity.update_token(auth_response) - - # we get the character information - cdata = esisecurity.verify() - print(cdata) - - currentCharacter = self.getSsoCharacter(cdata['CharacterID']) - - if currentCharacter is None: - currentCharacter = SsoCharacter(cdata['CharacterID'], cdata['CharacterName'], config.getClientSecret()) - currentCharacter.esi_client = Crest.genEsiClient(esisecurity) - currentCharacter.update_token(auth_response) # this also sets the esi security token - - eos.db.save(currentCharacter) - - wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) # todo: remove user / implicit authentication diff --git a/service/esi.py b/service/esi.py index 0f375a591..d065ffb90 100644 --- a/service/esi.py +++ b/service/esi.py @@ -5,17 +5,37 @@ import threading import copy import uuid import time +import config +import base64 +import json import eos.db from eos.enum import Enum -from eos.saveddata.ssocharacter import CrestChar +from eos.saveddata.ssocharacter import SsoCharacter import gui.globalEvents as GE from service.settings import CRESTSettings from service.server import StoppableHTTPServer, AuthHandler from service.pycrest.eve import EVE +from .esi_security_proxy import EsiSecurityProxy +from esipy import EsiClient, EsiApp +from esipy.cache import FileCache +import os +import logging + + pyfalog = Logger(__name__) +server = "https://blitzmann.pythonanywhere.com" +cache_path = os.path.join(config.savePath, config.ESI_CACHE) + +if not os.path.exists(cache_path): + os.mkdir(cache_path) + +file_cache = FileCache(cache_path) + +esiRdy = threading.Event() + class Servers(Enum): TQ = 0 @@ -26,21 +46,236 @@ class CrestModes(Enum): IMPLICIT = 0 USER = 1 +from utils.timer import Timer + + + +class Esi(object): + clientIDs = { + Servers.TQ : 'f9be379951c046339dc13a00e6be7704', + Servers.SISI: 'af87365240d644f7950af563b8418bad' + } -class ESI(object): # @todo: move this to settings clientCallback = 'http://localhost:6461' clientTest = True + esiapp = None + esi_v1 = None + esi_v4 = None + _instance = None + @classmethod + def initEsiApp(cls): + with Timer("Main EsiApp") as t: + cls.esiapp = EsiApp(cache=file_cache) + with Timer('ESI v1') as t: + cls.esi_v1 = cls.esiapp.get_v1_swagger + with Timer('ESI v4') as t: + cls.esi_v4 = cls.esiapp.get_v4_swagger + + # esiRdy.set() + + @classmethod + def genEsiClient(cls, security=None): + return EsiClient( + security=EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) if security is None else security, + cache=file_cache, + headers={'User-Agent': 'pyfa esipy'} + ) + @classmethod def getInstance(cls): if cls._instance is None: - cls._instance = ESI() + cls._instance = Esi() return cls._instance + @classmethod + def restartService(cls): + # This is here to reseed pycrest values when changing preferences + # We first stop the server n case one is running, as creating a new + # instance doesn't do this. + if cls._instance.httpd: + cls._instance.stopServer() + cls._instance = Esi() + cls._instance.mainFrame.updateCrestMenus(type=cls._instance.settings.get('mode')) + return cls._instance def __init__(self): - pass + """ + A note on login/logout events: the character login events happen + whenever a characters is logged into via the SSO, regardless of mod. + However, the mode should be send as an argument. Similarily, + the Logout even happens whenever the character is deleted for either + mode. The mode is sent as an argument, as well as the umber of + characters still in the cache (if USER mode) + """ + Esi.initEsiApp() + + + # prefetch = EsiInitThread() + # prefetch.daemon = True + # prefetch.start() + + self.settings = CRESTSettings.getInstance() + self.scopes = ['characterFittingsRead', 'characterFittingsWrite'] + + # these will be set when needed + self.httpd = None + self.state = None + self.ssoTimer = None + + self.eve_options = { + 'client_id': self.settings.get('clientID') if self.settings.get('mode') == CrestModes.USER else self.clientIDs.get(self.settings.get('server')), + 'api_key': self.settings.get('clientSecret') if self.settings.get('mode') == CrestModes.USER else None, + 'redirect_uri': self.clientCallback, + 'testing': self.isTestServer + } + + # Base EVE connection that is copied to all characters + self.eve = EVE(**self.eve_options) + + self.implicitCharacter = None + + # The database cache does not seem to be working for some reason. Use + # this as a temporary measure + self.charCache = {} + + # need these here to post events + import gui.mainFrame # put this here to avoid loop + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + @property + def isTestServer(self): + return self.settings.get('server') == Servers.SISI + + def delCrestCharacter(self, charID): + char = eos.db.getSsoCharacter(charID) + del self.charCache[char.ID] + eos.db.remove(char) + wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=len(self.charCache))) + + def delAllCharacters(self): + chars = eos.db.getSsoCharacters() + for char in chars: + eos.db.remove(char) + self.charCache = {} + wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0)) + + def getSsoCharacters(self): + chars = eos.db.getSsoCharacters(config.getClientSecret()) + return chars + + def getSsoCharacter(self, charID): + """ + Get character, and modify to include the eve connection + """ + char = eos.db.getSsoCharacter(charID, config.getClientSecret()) + if char.esi_client is None: + char.esi_client = Esi.genEsiClient() + char.esi_client.security.update_token(char.get_sso_data()) + return char + + def getFittings(self, charID): + char = self.getSsoCharacter(charID) + print(repr(char)) + op = Esi.esi_v1.op['get_characters_character_id_fittings']( + character_id=charID + ) + resp = char.esi_client.request(op) + return resp.data + + def postFitting(self, charID, json_str): + # @todo: new fitting ID can be recovered from resp.data, + char = self.getSsoCharacter(charID) + + op = Esi.esi_v1.op['post_characters_character_id_fittings']( + character_id=char.characterID, + fitting=json.loads(json_str) + ) + + resp = char.esi_client.request(op) + + return resp.data + + def delFitting(self, charID, fittingID): + char = self.getSsoCharacter(charID) + print(repr(char)) + op = Esi.esi_v1.op['delete_characters_character_id_fittings_fitting_id']( + character_id=charID, + fitting_id=fittingID + ) + + resp = char.esi_client.request(op) + return resp.data + + + def logout(self): + """Logout of implicit character""" + pyfalog.debug("Character logout") + self.implicitCharacter = None + wx.PostEvent(self.mainFrame, GE.SsoLogout(type=self.settings.get('mode'))) + + def stopServer(self): + pyfalog.debug("Stopping Server") + self.httpd.stop() + self.httpd = None + + def startServer(self): + pyfalog.debug("Starting server") + + # we need this to ensure that the previous get_request finishes, and then the socket will close + if self.httpd: + self.stopServer() + time.sleep(1) + + self.state = str(uuid.uuid4()) + self.httpd = StoppableHTTPServer(('localhost', 0), AuthHandler) + port = self.httpd.socket.getsockname()[1] + + esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) + + uri = esisecurity.get_auth_uri(state=self.state, redirect='http://localhost:{}'.format(port)) + + self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleLogin,)) + self.serverThread.name = "SsoCallbackServer" + self.serverThread.daemon = True + self.serverThread.start() + + return uri + + def handleLogin(self, message): + if not message: + raise Exception("Could not parse out querystring parameters.") + + if message['state'][0] != self.state: + pyfalog.warn("OAUTH state mismatch") + raise Exception("OAUTH State Mismatch.") + + pyfalog.debug("Handling CREST login with: {0}", message) + + auth_response = json.loads(base64.b64decode(message['SSOInfo'][0])) + + # We need to preload the ESI Security object beforehand with the auth response so that we can use verify to + # get character information + # init the security object + esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) + + esisecurity.update_token(auth_response) + + # we get the character information + cdata = esisecurity.verify() + print(cdata) + + currentCharacter = self.getSsoCharacter(cdata['CharacterID']) + + if currentCharacter is None: + currentCharacter = SsoCharacter(cdata['CharacterID'], cdata['CharacterName'], config.getClientSecret()) + currentCharacter.esi_client = Esi.genEsiClient(esisecurity) + currentCharacter.update_token(auth_response) # this also sets the esi security token + + eos.db.save(currentCharacter) + + wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) # todo: remove user / implicit authentication diff --git a/service/port.py b/service/port.py index c3cf352c7..8f2851bf6 100644 --- a/service/port.py +++ b/service/port.py @@ -49,7 +49,7 @@ from service.market import Market from utils.strfunctions import sequential_rep, replace_ltgt from abc import ABCMeta, abstractmethod -from service.crest import Crest +from service.esi import Esi from collections import OrderedDict pyfalog = Logger(__name__) @@ -388,7 +388,7 @@ class Port(object): nested_dict = lambda: collections.defaultdict(nested_dict) fit = nested_dict() - sCrest = Crest.getInstance() + sCrest = Esi.getInstance() sFit = svcFit.getInstance() eve = sCrest.eve From 33bf5234d09a22af5a7d5cfba7f24a22520c0dd3 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 8 Feb 2018 23:05:01 -0500 Subject: [PATCH 10/39] Goodbye eveapi! You have served us well all these years! Start stripping XML API stuff and implement ESI skill fetching. --- eos/db/saveddata/character.py | 8 +- eos/db/saveddata/queries.py | 2 +- eos/saveddata/character.py | 5 +- .../pyfaCrestPreferences.py | 2 - gui/characterEditor.py | 4 +- gui/characterSelection.py | 8 +- gui/crestFittings.py | 6 +- service/character.py | 129 +-- service/esi.py | 133 +-- service/eveapi.py | 1016 ----------------- service/pycrest/__init__.py | 1 - service/pycrest/compat.py | 24 - service/pycrest/errors.py | 2 - service/pycrest/eve.py | 318 ------ service/pycrest/weak_ciphers.py | 132 --- 15 files changed, 93 insertions(+), 1697 deletions(-) delete mode 100644 service/eveapi.py delete mode 100644 service/pycrest/__init__.py delete mode 100644 service/pycrest/compat.py delete mode 100644 service/pycrest/errors.py delete mode 100644 service/pycrest/eve.py delete mode 100644 service/pycrest/weak_ciphers.py diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index c87817541..8bf7d59c3 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -27,14 +27,12 @@ from eos.effectHandlerHelpers import HandledImplantBoosterList from eos.saveddata.implant import Implant from eos.saveddata.user import User from eos.saveddata.character import Character, Skill +from eos.saveddata.ssocharacter import SsoCharacter characters_table = Table("characters", saveddata_meta, Column("ID", Integer, primary_key=True), Column("name", String, nullable=False), - Column("apiID", Integer), - Column("apiKey", String), - Column("defaultChar", Integer), - Column("chars", String, nullable=True), + Column("ssoCharacterID", ForeignKey("ssoCharacter.ID"), nullable=True), Column("defaultLevel", Integer, nullable=True), Column("alphaCloneID", Integer, nullable=True), Column("ownerID", ForeignKey("users.ID"), nullable=True), @@ -62,6 +60,6 @@ mapper(Character, characters_table, single_parent=True, primaryjoin=charImplants_table.c.charID == characters_table.c.ID, secondaryjoin=charImplants_table.c.implantID == Implant.ID, - secondary=charImplants_table), + secondary=charImplants_table) } ) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 087561070..e582eef87 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -479,7 +479,7 @@ def getSsoCharacter(lookfor, clientHash, eager=None): filter = SsoCharacter.client == clientHash if isinstance(lookfor, int): - filter = and_(filter, SsoCharacter.characterID == lookfor) + filter = and_(filter, SsoCharacter.ID == lookfor) elif isinstance(lookfor, str): filter = and_(filter, SsoCharacter.characterName == lookfor) else: diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 278a05d92..77551a5ed 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -119,12 +119,9 @@ class Character(object): return all0 - def apiUpdateCharSheet(self, skills, secStatus=0): + def clearSkills(self): del self.__skills[:] self.__skillIdMap.clear() - for skillRow in skills: - self.addSkill(Skill(self, skillRow["typeID"], skillRow["level"])) - self.secStatus = secStatus @property def ro(self): diff --git a/gui/builtinPreferenceViews/pyfaCrestPreferences.py b/gui/builtinPreferenceViews/pyfaCrestPreferences.py index f3da3dbeb..c94cc1328 100644 --- a/gui/builtinPreferenceViews/pyfaCrestPreferences.py +++ b/gui/builtinPreferenceViews/pyfaCrestPreferences.py @@ -119,11 +119,9 @@ class PFCrestPref(PreferenceView): def OnModeChange(self, event): self.settings.set('mode', event.GetInt()) self.ToggleProxySettings(self.settings.get('mode')) - Esi.restartService() def OnServerChange(self, event): self.settings.set('server', event.GetInt()) - Esi.restartService() def OnBtnApply(self, event): self.settings.set('clientID', self.inputClientID.GetValue().strip()) diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 080940c7d..f8b79ae8c 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -158,11 +158,11 @@ class CharacterEditor(wx.Frame): self.sview = SkillTreeView(self.viewsNBContainer) self.iview = ImplantEditorView(self.viewsNBContainer, self) - self.aview = APIView(self.viewsNBContainer) + # self.aview = APIView(self.viewsNBContainer) self.viewsNBContainer.AddPage(self.sview, "Skills") self.viewsNBContainer.AddPage(self.iview, "Implants") - self.viewsNBContainer.AddPage(self.aview, "API") + # self.viewsNBContainer.AddPage(self.aview, "API") mainSizer.Add(self.viewsNBContainer, 1, wx.EXPAND | wx.ALL, 5) diff --git a/gui/characterSelection.py b/gui/characterSelection.py index aba1c68fb..a75306436 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -151,9 +151,7 @@ class CharacterSelection(wx.Panel): def refreshApi(self, event): self.btnRefresh.Enable(False) sChar = Character.getInstance() - ID, key, charName, chars = sChar.getApiDetails(self.getActiveCharacter()) - if charName: - sChar.apiFetch(self.getActiveCharacter(), charName, self.refreshAPICallback) + sChar.apiFetch(self.getActiveCharacter(), self.refreshAPICallback) def refreshAPICallback(self, e=None): self.btnRefresh.Enable(True) @@ -178,7 +176,9 @@ class CharacterSelection(wx.Panel): self.charChoice.SetSelection(self.charCache) self.mainFrame.showCharacterEditor(event) return - if sChar.getCharName(charID) not in ("All 0", "All 5") and sChar.apiEnabled(charID): + + char = sChar.getCharacter(charID) + if sChar.getCharName(charID) not in ("All 0", "All 5") and char.ssoCharacterID is not None: self.btnRefresh.Enable(True) else: self.btnRefresh.Enable(False) diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 69e8b6085..ed8750355 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -99,7 +99,7 @@ class CrestFittings(wx.Frame): self.charChoice.Clear() for char in chars: - self.charChoice.Append(char.characterName, char.characterID) + self.charChoice.Append(char.characterName, char.ID) self.charChoice.SetSelection(0) @@ -220,7 +220,7 @@ class ExportToEve(wx.Frame): self.charChoice.Clear() for char in chars: - self.charChoice.Append(char.characterName, char.characterID) + self.charChoice.Append(char.characterName, char.ID) self.charChoice.SetSelection(0) @@ -344,7 +344,7 @@ class CrestMgmt(wx.Dialog): if item > -1: charID = self.lcCharacters.GetItemData(item) sCrest = Esi.getInstance() - sCrest.delCrestCharacter(charID) + sCrest.delSsoCharacter(charID) self.popCharList() diff --git a/service/character.py b/service/character.py index 0a36bb42a..b6562032e 100644 --- a/service/character.py +++ b/service/character.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with pyfa. If not, see . # ============================================================================= - import sys import copy import itertools @@ -34,10 +33,10 @@ import wx import config import eos.db -from service.eveapi import EVEAPIConnection, ParseXML +from service.esi import Esi from eos.saveddata.implant import Implant as es_Implant -from eos.saveddata.character import Character as es_Character +from eos.saveddata.character import Character as es_Character, Skill from eos.saveddata.module import Slot as es_Slot, Module as es_Module from eos.saveddata.fighter import Fighter as es_Fighter @@ -52,6 +51,9 @@ class CharacterImportThread(threading.Thread): self.callback = callback def run(self): + wx.CallAfter(self.callback) + # todo: Fix character import (don't need CCP SML anymore, only support evemon?) + return paths = self.paths sCharacter = Character.getInstance() all5_character = es_Character("All 5", 5) @@ -62,43 +64,34 @@ class CharacterImportThread(threading.Thread): for path in paths: try: - # we try to parse api XML data first - with open(path, mode='r') as charFile: - sheet = ParseXML(charFile) - char = sCharacter.new(sheet.name + " (imported)") - sCharacter.apiUpdateCharSheet(char.ID, sheet.skills) - except: - # if it's not api XML data, try this - # this is a horrible logic flow, but whatever - try: - charFile = open(path, mode='r').read() - doc = minidom.parseString(charFile) - if doc.documentElement.tagName not in ("SerializableCCPCharacter", "SerializableUriCharacter"): - pyfalog.error("Incorrect EVEMon XML sheet") - raise RuntimeError("Incorrect EVEMon XML sheet") - name = doc.getElementsByTagName("name")[0].firstChild.nodeValue - securitystatus = doc.getElementsByTagName("securityStatus")[0].firstChild.nodeValue or 0 - skill_els = doc.getElementsByTagName("skill") - skills = [] - for skill in skill_els: - if int(skill.getAttribute("typeID")) in all_skill_ids and (0 <= int(skill.getAttribute("level")) <= 5): - skills.append({ - "typeID": int(skill.getAttribute("typeID")), - "level": int(skill.getAttribute("level")), - }) - else: - pyfalog.error( - "Attempted to import unknown skill {0} (ID: {1}) (Level: {2})", - skill.getAttribute("name"), - skill.getAttribute("typeID"), - skill.getAttribute("level"), - ) - char = sCharacter.new(name + " (EVEMon)") - sCharacter.apiUpdateCharSheet(char.ID, skills, securitystatus) - except Exception as e: - pyfalog.error("Exception on character import:") - pyfalog.error(e) - continue + charFile = open(path, mode='r').read() + doc = minidom.parseString(charFile) + if doc.documentElement.tagName not in ("SerializableCCPCharacter", "SerializableUriCharacter"): + pyfalog.error("Incorrect EVEMon XML sheet") + raise RuntimeError("Incorrect EVEMon XML sheet") + name = doc.getElementsByTagName("name")[0].firstChild.nodeValue + securitystatus = doc.getElementsByTagName("securityStatus")[0].firstChild.nodeValue or 0 + skill_els = doc.getElementsByTagName("skill") + skills = [] + for skill in skill_els: + if int(skill.getAttribute("typeID")) in all_skill_ids and (0 <= int(skill.getAttribute("level")) <= 5): + skills.append({ + "typeID": int(skill.getAttribute("typeID")), + "level": int(skill.getAttribute("level")), + }) + else: + pyfalog.error( + "Attempted to import unknown skill {0} (ID: {1}) (Level: {2})", + skill.getAttribute("name"), + skill.getAttribute("typeID"), + skill.getAttribute("level"), + ) + char = sCharacter.new(name + " (EVEMon)") + sCharacter.apiUpdateCharSheet(char.ID, skills, securitystatus) + except Exception as e: + pyfalog.error("Exception on character import:") + pyfalog.error(e) + continue wx.CallAfter(self.callback) @@ -344,6 +337,8 @@ class Character(object): @staticmethod def getApiDetails(charID): + # todo: fix this (or get rid of?) + return ("", "", "", []) char = eos.db.getCharacter(charID) if char.chars is not None: chars = json.loads(char.chars) @@ -351,27 +346,8 @@ class Character(object): chars = None return char.apiID or "", char.apiKey or "", char.defaultChar or "", chars or [] - def apiEnabled(self, charID): - id_, key, default, _ = self.getApiDetails(charID) - return id_ is not "" and key is not "" and default is not "" - - @staticmethod - def apiCharList(charID, userID, apiKey): - char = eos.db.getCharacter(charID) - - char.apiID = userID - char.apiKey = apiKey - - api = EVEAPIConnection() - auth = api.auth(keyID=userID, vCode=apiKey) - apiResult = auth.account.Characters() - charList = [str(c.name) for c in apiResult.characters] - - char.chars = json.dumps(charList) - return charList - - def apiFetch(self, charID, charName, callback): - thread = UpdateAPIThread(charID, charName, (self.apiFetchCallback, callback)) + def apiFetch(self, charID, callback): + thread = UpdateAPIThread(charID, (self.apiFetchCallback, callback)) thread.start() def apiFetchCallback(self, guiCallback, e=None): @@ -469,35 +445,30 @@ class Character(object): class UpdateAPIThread(threading.Thread): - def __init__(self, charID, charName, callback): + def __init__(self, charID, callback): threading.Thread.__init__(self) self.name = "CheckUpdate" self.callback = callback self.charID = charID - self.charName = charName def run(self): try: - dbChar = eos.db.getCharacter(self.charID) - dbChar.defaultChar = self.charName + char = eos.db.getCharacter(self.charID) - api = EVEAPIConnection() - auth = api.auth(keyID=dbChar.apiID, vCode=dbChar.apiKey) - apiResult = auth.account.Characters() - charID = None - for char in apiResult.characters: - if char.name == self.charName: - charID = char.characterID - break + sEsi = Esi.getInstance() + resp = sEsi.getSkills(char.ssoCharacterID) - if charID is None: - return + # todo: check if alpha. if so, pop up a question if they want to apply it as alpha. Use threading events to set the answer? + char.clearSkills() + for skillRow in resp["skills"]: + char.addSkill(Skill(char, skillRow["skill_id"], skillRow["trained_skill_level"])) - sheet = auth.character(charID).CharacterSheet() - charInfo = api.eve.CharacterInfo(characterID=charID) + resp = sEsi.getSecStatus(char.ssoCharacterID) + + char.secStatus = resp['security_status'] - dbChar.apiUpdateCharSheet(sheet.skills, charInfo.securityStatus) self.callback[0](self.callback[1]) - except Exception: + except Exception as ex: + pyfalog.warn(ex) self.callback[0](self.callback[1], sys.exc_info()) diff --git a/service/esi.py b/service/esi.py index d065ffb90..3e47af41a 100644 --- a/service/esi.py +++ b/service/esi.py @@ -2,12 +2,12 @@ import wx from logbook import Logger import threading -import copy import uuid import time import config import base64 import json +import os import eos.db from eos.enum import Enum @@ -15,18 +15,13 @@ from eos.saveddata.ssocharacter import SsoCharacter import gui.globalEvents as GE from service.settings import CRESTSettings from service.server import StoppableHTTPServer, AuthHandler -from service.pycrest.eve import EVE from .esi_security_proxy import EsiSecurityProxy from esipy import EsiClient, EsiApp from esipy.cache import FileCache -import os -import logging - pyfalog = Logger(__name__) -server = "https://blitzmann.pythonanywhere.com" cache_path = os.path.join(config.savePath, config.ESI_CACHE) if not os.path.exists(cache_path): @@ -34,32 +29,13 @@ if not os.path.exists(cache_path): file_cache = FileCache(cache_path) -esiRdy = threading.Event() - class Servers(Enum): TQ = 0 SISI = 1 -class CrestModes(Enum): - IMPLICIT = 0 - USER = 1 - -from utils.timer import Timer - - - class Esi(object): - clientIDs = { - Servers.TQ : 'f9be379951c046339dc13a00e6be7704', - Servers.SISI: 'af87365240d644f7950af563b8418bad' - } - - # @todo: move this to settings - clientCallback = 'http://localhost:6461' - clientTest = True - esiapp = None esi_v1 = None esi_v4 = None @@ -68,12 +44,9 @@ class Esi(object): @classmethod def initEsiApp(cls): - with Timer("Main EsiApp") as t: - cls.esiapp = EsiApp(cache=file_cache) - with Timer('ESI v1') as t: - cls.esi_v1 = cls.esiapp.get_v1_swagger - with Timer('ESI v4') as t: - cls.esi_v4 = cls.esiapp.get_v4_swagger + cls.esiapp = EsiApp(cache=file_cache) + cls.esi_v1 = cls.esiapp.get_v1_swagger + cls.esi_v4 = cls.esiapp.get_v4_swagger # esiRdy.set() @@ -92,51 +65,16 @@ class Esi(object): return cls._instance - @classmethod - def restartService(cls): - # This is here to reseed pycrest values when changing preferences - # We first stop the server n case one is running, as creating a new - # instance doesn't do this. - if cls._instance.httpd: - cls._instance.stopServer() - cls._instance = Esi() - cls._instance.mainFrame.updateCrestMenus(type=cls._instance.settings.get('mode')) - return cls._instance - def __init__(self): - """ - A note on login/logout events: the character login events happen - whenever a characters is logged into via the SSO, regardless of mod. - However, the mode should be send as an argument. Similarily, - the Logout even happens whenever the character is deleted for either - mode. The mode is sent as an argument, as well as the umber of - characters still in the cache (if USER mode) - """ Esi.initEsiApp() - - # prefetch = EsiInitThread() - # prefetch.daemon = True - # prefetch.start() - self.settings = CRESTSettings.getInstance() - self.scopes = ['characterFittingsRead', 'characterFittingsWrite'] # these will be set when needed self.httpd = None self.state = None self.ssoTimer = None - self.eve_options = { - 'client_id': self.settings.get('clientID') if self.settings.get('mode') == CrestModes.USER else self.clientIDs.get(self.settings.get('server')), - 'api_key': self.settings.get('clientSecret') if self.settings.get('mode') == CrestModes.USER else None, - 'redirect_uri': self.clientCallback, - 'testing': self.isTestServer - } - - # Base EVE connection that is copied to all characters - self.eve = EVE(**self.eve_options) - self.implicitCharacter = None # The database cache does not seem to be working for some reason. Use @@ -151,73 +89,61 @@ class Esi(object): def isTestServer(self): return self.settings.get('server') == Servers.SISI - def delCrestCharacter(self, charID): - char = eos.db.getSsoCharacter(charID) - del self.charCache[char.ID] + def delSsoCharacter(self, id): + char = eos.db.getSsoCharacter(id) eos.db.remove(char) - wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=len(self.charCache))) - - def delAllCharacters(self): - chars = eos.db.getSsoCharacters() - for char in chars: - eos.db.remove(char) - self.charCache = {} - wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0)) def getSsoCharacters(self): chars = eos.db.getSsoCharacters(config.getClientSecret()) return chars - def getSsoCharacter(self, charID): + def getSsoCharacter(self, id): """ Get character, and modify to include the eve connection """ - char = eos.db.getSsoCharacter(charID, config.getClientSecret()) - if char.esi_client is None: + char = eos.db.getSsoCharacter(id, config.getClientSecret()) + if char is not None and char.esi_client is None: char.esi_client = Esi.genEsiClient() char.esi_client.security.update_token(char.get_sso_data()) return char - def getFittings(self, charID): - char = self.getSsoCharacter(charID) - print(repr(char)) - op = Esi.esi_v1.op['get_characters_character_id_fittings']( - character_id=charID - ) + def getSkills(self, id): + char = self.getSsoCharacter(id) + op = Esi.esi_v4.op['get_characters_character_id_skills'](character_id=char.characterID) resp = char.esi_client.request(op) return resp.data - def postFitting(self, charID, json_str): - # @todo: new fitting ID can be recovered from resp.data, - char = self.getSsoCharacter(charID) + def getSecStatus(self, id): + char = self.getSsoCharacter(id) + op = Esi.esi_v4.op['get_characters_character_id'](character_id=char.characterID) + resp = char.esi_client.request(op) + return resp.data + def getFittings(self, id): + char = self.getSsoCharacter(id) + op = Esi.esi_v1.op['get_characters_character_id_fittings'](character_id=char.characterID) + resp = char.esi_client.request(op) + return resp.data + + def postFitting(self, id, json_str): + # @todo: new fitting ID can be recovered from resp.data, + char = self.getSsoCharacter(id) op = Esi.esi_v1.op['post_characters_character_id_fittings']( character_id=char.characterID, fitting=json.loads(json_str) ) - resp = char.esi_client.request(op) - return resp.data - def delFitting(self, charID, fittingID): - char = self.getSsoCharacter(charID) - print(repr(char)) + def delFitting(self, id, fittingID): + char = self.getSsoCharacter(id) op = Esi.esi_v1.op['delete_characters_character_id_fittings_fitting_id']( - character_id=charID, + character_id=char.characterID, fitting_id=fittingID ) - resp = char.esi_client.request(op) return resp.data - - def logout(self): - """Logout of implicit character""" - pyfalog.debug("Character logout") - self.implicitCharacter = None - wx.PostEvent(self.mainFrame, GE.SsoLogout(type=self.settings.get('mode'))) - def stopServer(self): pyfalog.debug("Stopping Server") self.httpd.stop() @@ -278,4 +204,3 @@ class Esi(object): eos.db.save(currentCharacter) - wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) # todo: remove user / implicit authentication diff --git a/service/eveapi.py b/service/eveapi.py deleted file mode 100644 index a06cf2efe..000000000 --- a/service/eveapi.py +++ /dev/null @@ -1,1016 +0,0 @@ -# ----------------------------------------------------------------------------- -# eveapi - EVE Online API access -# -# Copyright (c)2007-2014 Jamie "Entity" van den Berge -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE -# -# ----------------------------------------------------------------------------- -# -# Version: 1.3.0 - 27 May 2014 -# - Added set_user_agent() module-level function to set the User-Agent header -# to be used for any requests by the library. If this function is not used, -# a warning will be thrown for every API request. -# -# Version: 1.2.9 - 14 September 2013 -# - Updated error handling: Raise an AuthenticationError in case -# the API returns HTTP Status Code 403 - Forbidden -# -# Version: 1.2.8 - 9 August 2013 -# - the XML value cast function (_autocast) can now be changed globally to a -# custom one using the set_cast_func(func) module-level function. -# -# Version: 1.2.7 - 3 September 2012 -# - Added get() method to Row object. -# -# Version: 1.2.6 - 29 August 2012 -# - Added finer error handling + added setup.py to allow distributing eveapi -# through pypi. -# -# Version: 1.2.5 - 1 August 2012 -# - Row objects now have __hasattr__ and __contains__ methods -# -# Version: 1.2.4 - 12 April 2012 -# - API version of XML response now available as _meta.version -# -# Version: 1.2.3 - 10 April 2012 -# - fix for tags of the form -# -# Version: 1.2.2 - 27 February 2012 -# - fix for the workaround in 1.2.1. -# -# Version: 1.2.1 - 23 February 2012 -# - added workaround for row tags missing attributes that were defined -# in their rowset (this should fix ContractItems) -# -# Version: 1.2.0 - 18 February 2012 -# - fix handling of empty XML tags. -# - improved proxy support a bit. -# -# Version: 1.1.9 - 2 September 2011 -# - added workaround for row tags with attributes that were not defined -# in their rowset (this should fix AssetList) -# -# Version: 1.1.8 - 1 September 2011 -# - fix for inconsistent columns attribute in rowsets. -# -# Version: 1.1.7 - 1 September 2011 -# - auth() method updated to work with the new authentication scheme. -# -# Version: 1.1.6 - 27 May 2011 -# - Now supports composite keys for IndexRowsets. -# - Fixed calls not working if a path was specified in the root url. -# -# Version: 1.1.5 - 27 Januari 2011 -# - Now supports (and defaults to) HTTPS. Non-SSL proxies will still work by -# explicitly specifying http:// in the url. -# -# Version: 1.1.4 - 1 December 2010 -# - Empty explicit CDATA tags are now properly handled. -# - _autocast now receives the name of the variable it's trying to typecast, -# enabling custom/future casting functions to make smarter decisions. -# -# Version: 1.1.3 - 6 November 2010 -# - Added support for anonymous CDATA inside row tags. This makes the body of -# mails in the rows of char/MailBodies available through the .data attribute. -# -# Version: 1.1.2 - 2 July 2010 -# - Fixed __str__ on row objects to work properly with unicode strings. -# -# Version: 1.1.1 - 10 Januari 2010 -# - Fixed bug that causes nested tags to not appear in rows of rowsets created -# from normal Elements. This should fix the corp.MemberSecurity method, -# which now returns all data for members. [jehed] -# -# Version: 1.1.0 - 15 Januari 2009 -# - Added Select() method to Rowset class. Using it avoids the creation of -# temporary row instances, speeding up iteration considerably. -# - Added ParseXML() function, which can be passed arbitrary API XML file or -# string objects. -# - Added support for proxy servers. A proxy can be specified globally or -# per api connection instance. [suggestion by graalman] -# - Some minor refactoring. -# - Fixed deprecation warning when using Python 2.6. -# -# Version: 1.0.7 - 14 November 2008 -# - Added workaround for rowsets that are missing the (required!) columns -# attribute. If missing, it will use the columns found in the first row. -# Note that this is will still break when expecting columns, if the rowset -# is empty. [Flux/Entity] -# -# Version: 1.0.6 - 18 July 2008 -# - Enabled expat text buffering to avoid content breaking up. [BigWhale] -# -# Version: 1.0.5 - 03 February 2008 -# - Added workaround to make broken XML responses (like the "row:name" bug in -# eve/CharacterID) work as intended. -# - Bogus datestamps before the epoch in XML responses are now set to 0 to -# avoid breaking certain date/time functions. [Anathema Matou] -# -# Version: 1.0.4 - 23 December 2007 -# - Changed _autocast() to use timegm() instead of mktime(). [Invisible Hand] -# - Fixed missing attributes of elements inside rows. [Elandra Tenari] -# -# Version: 1.0.3 - 13 December 2007 -# - Fixed keyless columns bugging out the parser (in CorporationSheet for ex.) -# -# Version: 1.0.2 - 12 December 2007 -# - Fixed parser not working with indented XML. -# -# Version: 1.0.1 -# - Some micro optimizations -# -# Version: 1.0 -# - Initial release -# -# Requirements: -# Python 2.4+ -# -# ----------------------------------------------------------------------------- - - -# ----------------------------------------------------------------------------- -# This eveapi has been modified for pyfa. -# -# Specifically, the entire network request/response has been substituted for -# pyfa's own implementation in service.network -# -# Additionally, various other parts have been changed to support urllib2 -# responses instead of httplib -# ----------------------------------------------------------------------------- - - -import urllib.parse -import copy - -from xml.parsers import expat -from time import strptime -from calendar import timegm - -from service.network import Network - -proxy = None -proxySSL = False - -_default_useragent = "eveapi.py/1.3" -_useragent = None # use set_user_agent() to set this. - - -# ----------------------------------------------------------------------------- - - -def set_cast_func(func): - """Sets an alternative value casting function for the XML parser. - The function must have 2 arguments; key and value. It should return a - value or object of the type appropriate for the given attribute name/key. - func may be None and will cause the default _autocast function to be used. - """ - global _castfunc - _castfunc = _autocast if func is None else func - - -def set_user_agent(user_agent_string): - """Sets a User-Agent for any requests sent by the library.""" - global _useragent - _useragent = user_agent_string - - -class Error(Exception): - def __init__(self, code, message): - self.code = code - self.args = (message.rstrip("."),) - - def __unicode__(self): - return '%s [code=%s]' % (self.args[0], self.code) - - -class RequestError(Error): - pass - - -class AuthenticationError(Error): - pass - - -class ServerError(Error): - pass - - -def EVEAPIConnection(url="api.eveonline.com", cacheHandler=None, proxy=None, proxySSL=False): - # Creates an API object through which you can call remote functions. - # - # The following optional arguments may be provided: - # - # url - root location of the EVEAPI server - # - # proxy - (host,port) specifying a proxy server through which to request - # the API pages. Specifying a proxy overrides default proxy. - # - # proxySSL - True if the proxy requires SSL, False otherwise. - # - # cacheHandler - an object which must support the following interface: - # - # retrieve(host, path, params) - # - # Called when eveapi wants to fetch a document. - # host is the address of the server, path is the full path to - # the requested document, and params is a dict containing the - # parameters passed to this api call (keyID, vCode, etc). - # The method MUST return one of the following types: - # - # None - if your cache did not contain this entry - # str/unicode - eveapi will parse this as XML - # Element - previously stored object as provided to store() - # file-like object - eveapi will read() XML from the stream. - # - # store(host, path, params, doc, obj) - # - # Called when eveapi wants you to cache this item. - # You can use obj to get the info about the object (cachedUntil - # and currentTime, etc) doc is the XML document the object - # was generated from. It's generally best to cache the XML, not - # the object, unless you pickle the object. Note that this method - # will only be called if you returned None in the retrieve() for - # this object. - # - - if not url.startswith("http"): - url = "https://" + url - p = urllib.parse.urlparse(url, "https") - if p.path and p.path[-1] == "/": - p.path = p.path[:-1] - ctx = _RootContext(None, p.path, {}, {}) - ctx._handler = cacheHandler - ctx._scheme = p.scheme - ctx._host = p.netloc - ctx._proxy = proxy or globals()["proxy"] - ctx._proxySSL = proxySSL or globals()["proxySSL"] - return ctx - - -def ParseXML(file_or_string): - try: - return _ParseXML(file_or_string, False, None) - except TypeError: - raise TypeError("XML data must be provided as string or file-like object") - - -def _ParseXML(response, fromContext, storeFunc): - # pre/post-process XML or Element data - - if fromContext and isinstance(response, Element): - obj = response - elif type(response) in (str, str): - obj = _Parser().Parse(response, False) - elif hasattr(response, "read"): - obj = _Parser().Parse(response, True) - else: - raise TypeError("retrieve method must return None, string, file-like object or an Element instance") - - error = getattr(obj, "error", False) - if error: - if error.code >= 500: - raise ServerError(error.code, error.data) - elif error.code >= 200: - raise AuthenticationError(error.code, error.data) - elif error.code >= 100: - raise RequestError(error.code, error.data) - else: - raise Error(error.code, error.data) - - result = getattr(obj, "result", False) - if not result: - raise RuntimeError("API object does not contain result") - - if fromContext and storeFunc: - # call the cache handler to store this object - storeFunc(obj) - - # make metadata available to caller somehow - result._meta = obj - - return result - - -# ----------------------------------------------------------------------------- -# API Classes -# ----------------------------------------------------------------------------- - - -_listtypes = (list, tuple, dict) -_unspecified = [] - - -class _Context(object): - def __init__(self, root, path, parentDict, newKeywords=None): - self._root = root or self - self._path = path - if newKeywords: - if parentDict: - self.parameters = parentDict.copy() - else: - self.parameters = {} - self.parameters.update(newKeywords) - else: - self.parameters = parentDict or {} - - def context(self, *args, **kw): - if kw or args: - path = self._path - if args: - path += "/" + "/".join(args) - return self.__class__(self._root, path, self.parameters, kw) - else: - return self - - def __getattr__(self, this): - # perform arcane attribute majick trick - return _Context(self._root, self._path + "/" + this, self.parameters) - - def __call__(self, **kw): - if kw: - # specified keywords override contextual ones - for k, v in self.parameters.items(): - if k not in kw: - kw[k] = v - else: - # no keywords provided, just update with contextual ones. - kw.update(self.parameters) - - # now let the root context handle it further - return self._root(self._path, **kw) - - -class _AuthContext(_Context): - def character(self, characterID): - # returns a copy of this connection object but for every call made - # through it, it will add the folder "/char" to the url, and the - # characterID to the parameters passed. - return _Context(self._root, self._path + "/char", self.parameters, {"characterID": characterID}) - - def corporation(self, characterID): - # same as character except for the folder "/corp" - return _Context(self._root, self._path + "/corp", self.parameters, {"characterID": characterID}) - - -class _RootContext(_Context): - def auth(self, **kw): - if len(kw) == 2 and (("keyID" in kw and "vCode" in kw) or ("userID" in kw and "apiKey" in kw)): - return _AuthContext(self._root, self._path, self.parameters, kw) - raise ValueError("Must specify keyID and vCode") - - def setcachehandler(self, handler): - self._root._handler = handler - - def __call__(self, path, **kw): - # convert list type arguments to something the API likes - for k, v in kw.items(): - if isinstance(v, _listtypes): - kw[k] = ','.join(map(str, list(v))) - - cache = self._root._handler - - # now send the request - path += ".xml.aspx" - - if cache: - response = cache.retrieve(self._host, path, kw) - else: - response = None - - if response is None: - network = Network.getInstance() - - req = self._scheme + '://' + self._host + path - - response = network.request(req, network.EVE, kw) - - if cache: - store = True - response = response.read() - else: - store = False - else: - store = False - - retrieve_fallback = cache and getattr(cache, "retrieve_fallback", False) - if retrieve_fallback: - # implementor is handling fallbacks... - try: - return _ParseXML(response, True, - store and (lambda obj: cache.store(self._host, path, kw, response, obj))) - except Error as e: - response = retrieve_fallback(self._host, path, kw, reason=e) - if response is not None: - return response - raise - else: - # implementor is not handling fallbacks... - return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj))) - - -# ----------------------------------------------------------------------------- -# XML Parser -# ----------------------------------------------------------------------------- - - -def _autocast(key, value): - # attempts to cast an XML string to the most probable type. - try: - if value.strip("-").isdigit(): - return int(value) - except ValueError: - pass - - try: - return float(value) - except ValueError: - pass - - if len(value) == 19 and value[10] == ' ': - # it could be a date string - try: - return max(0, int(timegm(strptime(value, "%Y-%m-%d %H:%M:%S")))) - except OverflowError: - pass - except ValueError: - pass - - # couldn't cast. return string unchanged. - return value - - -_castfunc = _autocast - - -class _Parser(object): - def Parse(self, data, isStream=False): - self.container = self.root = None - self._cdata = False - p = expat.ParserCreate() - p.StartElementHandler = self.tag_start - p.CharacterDataHandler = self.tag_cdata - p.StartCdataSectionHandler = self.tag_cdatasection_enter - p.EndCdataSectionHandler = self.tag_cdatasection_exit - p.EndElementHandler = self.tag_end - p.ordered_attributes = True - p.buffer_text = True - - if isStream: - p.ParseFile(data) - else: - p.Parse(data, True) - return self.root - - def tag_cdatasection_enter(self): - # encountered an explicit CDATA tag. - self._cdata = True - - def tag_cdatasection_exit(self): - if self._cdata: - # explicit CDATA without actual data. expat doesn't seem - # to trigger an event for this case, so do it manually. - # (_cdata is set False by this call) - self.tag_cdata("") - else: - self._cdata = False - - def tag_start(self, name, attributes): - # - # If there's a colon in the tag name, cut off the name from the colon - # onward. This is a workaround to make certain bugged XML responses - # (such as eve/CharacterID.xml.aspx) work. - if ":" in name: - name = name[:name.index(":")] - # - - if name == "rowset": - # for rowsets, use the given name - try: - columns = attributes[attributes.index('columns') + 1].replace(" ", "").split(",") - except ValueError: - # rowset did not have columns tag set (this is a bug in API) - # columns will be extracted from first row instead. - columns = [] - - try: - priKey = attributes[attributes.index('key') + 1] - this = IndexRowset(cols=columns, key=priKey) - except ValueError: - this = Rowset(cols=columns) - - this._name = attributes[attributes.index('name') + 1] - this.__catch = "row" # tag to auto-add to rowset. - else: - this = Element() - this._name = name - - this.__parent = self.container - - if self.root is None: - # We're at the root. The first tag has to be "eveapi" or we can't - # really assume the rest of the xml is going to be what we expect. - if name != "eveapi": - raise RuntimeError("Invalid API response") - try: - this.version = attributes[attributes.index("version") + 1] - except KeyError: - raise RuntimeError("Invalid API response") - self.root = this - - if isinstance(self.container, Rowset) and (self.container.__catch == this._name): - # - # - check for missing columns attribute (see above). - # - check for missing row attributes. - # - check for extra attributes that were not defined in the rowset, - # such as rawQuantity in the assets lists. - # In either case the tag is assumed to be correct and the rowset's - # columns are overwritten with the tag's version, if required. - numAttr = len(attributes) / 2 - numCols = len(self.container._cols) - if numAttr < numCols and (attributes[-2] == self.container._cols[-1]): - # the row data is missing attributes that were defined in the rowset. - # missing attributes' values will be set to None. - fixed = [] - row_idx = 0 - hdr_idx = 0 - numAttr *= 2 - for col in self.container._cols: - if col == attributes[row_idx]: - fixed.append(_castfunc(col, attributes[row_idx + 1])) - row_idx += 2 - else: - fixed.append(None) - hdr_idx += 1 - self.container.append(fixed) - else: - if not self.container._cols or (numAttr > numCols): - # the row data contains more attributes than were defined. - self.container._cols = attributes[0::2] - self.container.append( - [_castfunc(attributes[i], attributes[i + 1]) for i in range(0, len(attributes), 2)] - ) - # - - this._isrow = True - this._attributes = this._attributes2 = None - else: - this._isrow = False - this._attributes = attributes - this._attributes2 = [] - - self.container = self._last = this - self.has_cdata = False - - def tag_cdata(self, data): - self.has_cdata = True - if self._cdata: - # unset cdata flag to indicate it's been handled. - self._cdata = False - else: - if data in ("\r\n", "\n") or data.strip() != data: - return - - this = self.container - data = _castfunc(this._name, data) - - if this._isrow: - # sigh. anonymous data inside rows makes Entity cry. - # for the love of Jove, CCP, learn how to use rowsets. - parent = this.__parent - _row = parent._rows[-1] - _row.append(data) - if len(parent._cols) < len(_row): - parent._cols.append("data") - - elif this._attributes: - # this tag has attributes, so we can't simply assign the cdata - # as an attribute to the parent tag, as we'll lose the current - # tag's attributes then. instead, we'll assign the data as - # attribute of this tag. - this.data = data - else: - # this was a simple data without attributes. - # we won't be doing anything with this actual tag so we can just - # bind it to its parent (done by __tag_end) - setattr(this.__parent, this._name, data) - - def tag_end(self, name): - this = self.container - - if this is self.root: - del this._attributes - # this.__dict__.pop("_attributes", None) - return - - # we're done with current tag, so we can pop it off. This means that - # self.container will now point to the container of element 'this'. - self.container = this.__parent - del this.__parent - - attributes = this.__dict__.pop("_attributes") - attributes2 = this.__dict__.pop("_attributes2") - if attributes is None: - # already processed this tag's closure early, in tag_start() - return - - if self.container._isrow: - # Special case here. tags inside a row! Such tags have to be - # added as attributes of the row. - parent = self.container.__parent - - # get the row line for this element from its parent rowset - _row = parent._rows[-1] - - # add this tag's value to the end of the row - _row.append(getattr(self.container, this._name, this)) - - # fix columns if neccessary. - if len(parent._cols) < len(_row): - parent._cols.append(this._name) - else: - # see if there's already an attribute with this name (this shouldn't - # really happen, but it doesn't hurt to handle this case! - sibling = getattr(self.container, this._name, None) - if sibling is None: - if (not self.has_cdata) and (self._last is this) and (name != "rowset"): - if attributes: - # tag of the form - e = Element() - e._name = this._name - setattr(self.container, this._name, e) - for i in range(0, len(attributes), 2): - setattr(e, attributes[i], attributes[i + 1]) - else: - # tag of the form: , treat as empty string. - setattr(self.container, this._name, "") - else: - self.container._attributes2.append(this._name) - setattr(self.container, this._name, this) - - # Note: there aren't supposed to be any NON-rowset tags containing - # multiples of some tag or attribute. Code below handles this case. - elif isinstance(sibling, Rowset): - # its doppelganger is a rowset, append this as a row to that. - row = [_castfunc(attributes[i], attributes[i + 1]) for i in range(0, len(attributes), 2)] - row.extend([getattr(this, col) for col in attributes2]) - sibling.append(row) - elif isinstance(sibling, Element): - # parent attribute is an element. This means we're dealing - # with multiple of the same sub-tag. Change the attribute - # into a Rowset, adding the sibling element and this one. - rs = Rowset() - rs.__catch = rs._name = this._name - row = [_castfunc(attributes[i], attributes[i + 1]) for i in range(0, len(attributes), 2)] + \ - [getattr(this, col) for col in attributes2] - rs.append(row) - row = [getattr(sibling, attributes[i]) for i in range(0, len(attributes), 2)] + \ - [getattr(sibling, col) for col in attributes2] - rs.append(row) - rs._cols = [attributes[i] for i in range(0, len(attributes), 2)] + [col for col in attributes2] - setattr(self.container, this._name, rs) - else: - # something else must have set this attribute already. - # (typically the data case in tag_data()) - pass - - # Now fix up the attributes and be done with it. - for i in range(0, len(attributes), 2): - this.__dict__[attributes[i]] = _castfunc(attributes[i], attributes[i + 1]) - - return - - -# ----------------------------------------------------------------------------- -# XML Data Containers -# ----------------------------------------------------------------------------- -# The following classes are the various container types the XML data is -# unpacked into. -# -# Note that objects returned by API calls are to be treated as read-only. This -# is not enforced, but you have been warned. -# ----------------------------------------------------------------------------- - - -class Element(object): - _name = None - - # Element is a namespace for attributes and nested tags - def __str__(self): - return "" % self._name - - -_fmt = "%s:%s".__mod__ - - -def _cmp(self, a, b): - return (a > b) - (a < b) - - -class Row(object): - # A Row is a single database record associated with a Rowset. - # The fields in the record are accessed as attributes by their respective - # column name. - # - # To conserve resources, Row objects are only created on-demand. This is - # typically done by Rowsets (e.g. when iterating over the rowset). - - def __init__(self, cols=None, row=None): - self._cols = cols or [] - self._row = row or [] - - def __bool__(self): - return True - - def __ne__(self, other): - return self.__cmp__(other) - - def __eq__(self, other): - return self.__cmp__(other) == 0 - - def __cmp__(self, other): - if type(other) != type(self): - raise TypeError("Incompatible comparison type") - return _cmp(self._cols, other._cols) or _cmp(self._row, other._row) - - def __hasattr__(self, this): - if this in self._cols: - return self._cols.index(this) < len(self._row) - return False - - __contains__ = __hasattr__ - - def get(self, this, default=None): - if (this in self._cols) and (self._cols.index(this) < len(self._row)): - return self._row[self._cols.index(this)] - return default - - def __getattr__(self, this): - try: - return self._row[self._cols.index(this)] - except: - raise AttributeError(this) - - def __getitem__(self, this): - return self._row[self._cols.index(this)] - - def __str__(self): - return "Row(" + ','.join(map(_fmt, list(zip(self._cols, self._row)))) + ")" - - -class Rowset(object): - # Rowsets are collections of Row objects. - # - # Rowsets support most of the list interface: - # iteration, indexing and slicing - # - # As well as the following methods: - # - # IndexedBy(column) - # Returns an IndexRowset keyed on given column. Requires the column to - # be usable as primary key. - # - # GroupedBy(column) - # Returns a FilterRowset keyed on given column. FilterRowset objects - # can be accessed like dicts. See FilterRowset class below. - # - # SortBy(column, reverse=True) - # Sorts rowset in-place on given column. for a descending sort, - # specify reversed=True. - # - # SortedBy(column, reverse=True) - # Same as SortBy, except this returns a new rowset object instead of - # sorting in-place. - # - # Select(columns, row=False) - # Yields a column values tuple (value, ...) for each row in the rowset. - # If only one column is requested, then just the column value is - # provided instead of the values tuple. - # When row=True, each result will be decorated with the entire row. - # - - def IndexedBy(self, column): - return IndexRowset(self._cols, self._rows, column) - - def GroupedBy(self, column): - return FilterRowset(self._cols, self._rows, column) - - def SortBy(self, column, reverse=False): - ix = self._cols.index(column) - self.sort(key=lambda e: e[ix], reverse=reverse) - - def SortedBy(self, column, reverse=False): - rs = self[:] - rs.SortBy(column, reverse) - return rs - - def Select(self, *columns, **options): - if len(columns) == 1: - i = self._cols.index(columns[0]) - if options.get("row", False): - for line in self._rows: - yield (line, line[i]) - else: - for line in self._rows: - yield line[i] - else: - i = list(map(self._cols.index, columns)) - if options.get("row", False): - for line in self._rows: - yield line, [line[x] for x in i] - else: - for line in self._rows: - yield [line[x] for x in i] - - # ------------- - - def __init__(self, cols=None, rows=None): - self._cols = cols or [] - self._rows = rows or [] - - def append(self, row): - if isinstance(row, list): - self._rows.append(row) - elif isinstance(row, Row) and len(row._cols) == len(self._cols): - self._rows.append(row._row) - else: - raise TypeError("incompatible row type") - - def __add__(self, other): - if isinstance(other, Rowset): - if len(other._cols) == len(self._cols): - self._rows += other._rows - raise TypeError("rowset instance expected") - - def __bool__(self): - return not not self._rows - - def __len__(self): - return len(self._rows) - - def copy(self): - return self[:] - - def __getitem__(self, ix): - if type(ix) is slice: - return Rowset(self._cols, self._rows[ix]) - return Row(self._cols, self._rows[ix]) - - def sort(self, *args, **kw): - self._rows.sort(*args, **kw) - - def __str__(self): - return "Rowset(columns=[%s], rows=%d)" % (','.join(self._cols), len(self)) - - def __getstate__(self): - return self._cols, self._rows - - def __setstate__(self, state): - self._cols, self._rows = state - - -class IndexRowset(Rowset): - # An IndexRowset is a Rowset that keeps an index on a column. - # - # The interface is the same as Rowset, but provides an additional method: - # - # Get(key [, default]) - # Returns the Row mapped to provided key in the index. If there is no - # such key in the index, KeyError is raised unless a default value was - # specified. - # - - def Get(self, key, *default): - row = self._items.get(key, None) - if row is None: - if default: - return default[0] - raise KeyError(key) - return Row(self._cols, row) - - # ------------- - - def __init__(self, cols=None, rows=None, key=None): - try: - if "," in key: - self._ki = ki = [cols.index(k) for k in key.split(",")] - self.composite = True - else: - self._ki = ki = cols.index(key) - self.composite = False - except IndexError: - raise ValueError("Rowset has no column %s" % key) - - Rowset.__init__(self, cols, rows) - self._key = key - - if self.composite: - self._items = dict((tuple([row[k] for k in ki]), row) for row in self._rows) - else: - self._items = dict((row[ki], row) for row in self._rows) - - def __getitem__(self, ix): - if type(ix) is slice: - return IndexRowset(self._cols, self._rows[ix], self._key) - return Rowset.__getitem__(self, ix) - - def append(self, row): - Rowset.append(self, row) - if self.composite: - self._items[tuple([row[k] for k in self._ki])] = row - else: - self._items[row[self._ki]] = row - - def __getstate__(self): - return Rowset.__getstate__(self), self._items, self._ki - - def __setstate__(self, state): - state, self._items, self._ki = state - Rowset.__setstate__(self, state) - - -class FilterRowset(object): - # A FilterRowset works much like an IndexRowset, with the following - # differences: - # - FilterRowsets are accessed much like dicts - # - Each key maps to a Rowset, containing only the rows where the value - # of the column this FilterRowset was made on matches the key. - - def __init__(self, cols=None, rows=None, key=None, key2=None, dict_=None): - if dict_ is not None: - self._items = items = dict_ - elif cols is not None: - self._items = items = {} - - idfield = cols.index(key) - if not key2: - for row in rows: - id_ = row[idfield] - if id_ in items: - items[id_].append(row) - else: - items[id_] = [row] - else: - idfield2 = cols.index(key2) - for row in rows: - id_ = row[idfield] - if id_ in items: - items[id_][row[idfield2]] = row - else: - items[id_] = {row[idfield2]: row} - - self._cols = cols - self.key = key - self.key2 = key2 - self._bind() - - def _bind(self): - items = self._items - self.keys = items.keys - self.iterkeys = items.iterkeys - self.__contains__ = items.__contains__ - self.has_key = items.has_key - self.__len__ = items.__len__ - self.__iter__ = items.__iter__ - - def copy(self): - return FilterRowset(self._cols[:], None, self.key, self.key2, dict_=copy.deepcopy(self._items)) - - def get(self, key, default=_unspecified): - try: - return self[key] - except KeyError: - if default is _unspecified: - raise - return default - - def __getitem__(self, i): - if self.key2: - return IndexRowset(self._cols, None, self.key2, self._items.get(i, {})) - return Rowset(self._cols, self._items[i]) - - def __getstate__(self): - return self._cols, self._rows, self._items, self.key, self.key2 - - def __setstate__(self, state): - self._cols, self._rows, self._items, self.key, self.key2 = state - self._bind() diff --git a/service/pycrest/__init__.py b/service/pycrest/__init__.py deleted file mode 100644 index 5820a7290..000000000 --- a/service/pycrest/__init__.py +++ /dev/null @@ -1 +0,0 @@ -version = "0.0.1" diff --git a/service/pycrest/compat.py b/service/pycrest/compat.py deleted file mode 100644 index 8f4a35e0b..000000000 --- a/service/pycrest/compat.py +++ /dev/null @@ -1,24 +0,0 @@ -import sys - -PY3 = sys.version_info[0] == 3 - -if PY3: # pragma: no cover - string_types = str, - text_type = str - binary_type = bytes -else: # pragma: no cover - string_types = str, - text_type = str - binary_type = str - - -def text_(s, encoding='latin-1', errors='strict'): # pragma: no cover - if isinstance(s, binary_type): - return s.decode(encoding, errors) - return s - - -def bytes_(s, encoding='latin-1', errors='strict'): # pragma: no cover - if isinstance(s, text_type): - return s.encode(encoding, errors) - return s diff --git a/service/pycrest/errors.py b/service/pycrest/errors.py deleted file mode 100644 index 4216deabb..000000000 --- a/service/pycrest/errors.py +++ /dev/null @@ -1,2 +0,0 @@ -class APIException(Exception): - pass diff --git a/service/pycrest/eve.py b/service/pycrest/eve.py deleted file mode 100644 index 9ea88ce23..000000000 --- a/service/pycrest/eve.py +++ /dev/null @@ -1,318 +0,0 @@ -import base64 -from logbook import Logger -import os -import re -import time -import zlib - -import requests -from requests.adapters import HTTPAdapter - -import config -from service.pycrest.compat import bytes_, text_ -from service.pycrest.errors import APIException - -from urllib.parse import urlparse, urlunparse, parse_qsl - -try: - import pickle -except ImportError: # pragma: no cover - # noinspection PyPep8Naming - import pickle as pickle - -pyfalog = Logger(__name__) -cache_re = re.compile(r'max-age=([0-9]+)') - - -class APICache(object): - def put(self, key, value): - raise NotImplementedError - - def get(self, key): - raise NotImplementedError - - def invalidate(self, key): - raise NotImplementedError - - -class FileCache(APICache): - def __init__(self, path): - self._cache = {} - self.path = path - if not os.path.isdir(self.path): - os.mkdir(self.path, 0o700) - - def _getpath(self, key): - return os.path.join(self.path, str(hash(key)) + '.cache') - - def put(self, key, value): - with open(self._getpath(key), 'wb') as f: - f.write(zlib.compress(pickle.dumps(value, -1))) - self._cache[key] = value - - def get(self, key): - if key in self._cache: - return self._cache[key] - - try: - with open(self._getpath(key), 'rb') as f: - return pickle.loads(zlib.decompress(f.read())) - except IOError as ex: - pyfalog.debug("IO error opening zip file. (May not exist yet)") - if ex.errno == 2: # file does not exist (yet) - return None - else: - raise - - def invalidate(self, key): - self._cache.pop(key, None) - - try: - os.unlink(self._getpath(key)) - except OSError as ex: - pyfalog.debug("Caught exception in invalidate") - pyfalog.debug(ex) - if ex.errno == 2: # does not exist - pass - else: - raise - - -class DictCache(APICache): - def __init__(self): - self._dict = {} - - def get(self, key): - return self._dict.get(key, None) - - def put(self, key, value): - self._dict[key] = value - - def invalidate(self, key): - self._dict.pop(key, None) - - -class APIConnection(object): - def __init__(self, additional_headers=None, user_agent=None, cache_dir=None, cache=None): - # Set up a Requests Session - session = requests.Session() - if additional_headers is None: - additional_headers = {} - if user_agent is None: - user_agent = "pyfa/{0} ({1})".format(config.version, config.tag) - session.headers.update({ - "User-Agent": user_agent, - "Accept": "application/json", - }) - session.headers.update(additional_headers) - session.mount('https://public-crest.eveonline.com', HTTPAdapter()) - self._session = session - if cache: - if isinstance(cache, APICache): - self.cache = cache # Inherit from parents - elif isinstance(cache, type): - self.cache = cache() # Instantiate a new cache - elif cache_dir: - self.cache_dir = cache_dir - self.cache = FileCache(self.cache_dir) - else: - self.cache = DictCache() - - def get(self, resource, params=None): - pyfalog.debug('Getting resource {0}', resource) - if params is None: - params = {} - - # remove params from resource URI (needed for paginated stuff) - parsed_uri = urlparse(resource) - qs = parsed_uri.query - resource = urlunparse(parsed_uri._replace(query='')) - prms = {} - for tup in parse_qsl(qs): - prms[tup[0]] = tup[1] - - # params supplied to self.get() override parsed params - for key in params: - prms[key] = params[key] - - # check cache - key = (resource, frozenset(list(self._session.headers.items())), frozenset(list(prms.items()))) - cached = self.cache.get(key) - if cached and cached['cached_until'] > time.time(): - pyfalog.debug('Cache hit for resource {0} (params={1})', resource, prms) - return cached - elif cached: - pyfalog.debug('Cache stale for resource {0} (params={1})', resource, prms) - self.cache.invalidate(key) - else: - pyfalog.debug('Cache miss for resource {0} (params={1})', resource, prms) - - pyfalog.debug('Getting resource {0} (params={1})', resource, prms) - res = self._session.get(resource, params=prms) - if res.status_code != 200: - raise APIException("Got unexpected status code from server: {0}" % res.status_code) - - ret = res.json() - - # cache result - expires = self._get_expires(res) - if expires > 0: - ret.update({'cached_until': time.time() + expires}) - self.cache.put(key, ret) - - return ret - - @staticmethod - def _get_expires(response): - if 'Cache-Control' not in response.headers: - return 0 - if any([s in response.headers['Cache-Control'] for s in ['no-cache', 'no-store']]): - return 0 - match = cache_re.search(response.headers['Cache-Control']) - if match: - return int(match.group(1)) - return 0 - - -class EVE(APIConnection): - def __init__(self, **kwargs): - self.api_key = kwargs.pop('api_key', None) - self.client_id = kwargs.pop('client_id', None) - self.redirect_uri = kwargs.pop('redirect_uri', None) - if kwargs.pop('testing', False): - self._public_endpoint = "http://public-crest-sisi.testeveonline.com/" - self._authed_endpoint = "https://api-sisi.testeveonline.com/" - self._image_server = "https://image.testeveonline.com/" - self._oauth_endpoint = "https://sisilogin.testeveonline.com/oauth" - else: - self._public_endpoint = "https://public-crest.eveonline.com/" - self._authed_endpoint = "https://crest-tq.eveonline.com/" - self._image_server = "https://image.eveonline.com/" - self._oauth_endpoint = "https://login.eveonline.com/oauth" - self._endpoint = self._public_endpoint - self._cache = {} - self._data = None - self.token = None - self.refresh_token = None - self.expires = None - APIConnection.__init__(self, **kwargs) - - def __call__(self): - if not self._data: - self._data = APIObject(self.get(self._endpoint), self) - return self._data - - def __getattr__(self, item): - return self._data.__getattr__(item) - - def auth_uri(self, scopes=None, state=None): - s = [] if not scopes else scopes - grant_type = "token" if self.api_key is None else "code" - - return "%s/authorize?response_type=%s&redirect_uri=%s&client_id=%s%s%s" % ( - self._oauth_endpoint, - grant_type, - self.redirect_uri, - self.client_id, - "&scope=%s" % '+'.join(s) if scopes else '', - "&state=%s" % state if state else '' - ) - - def _authorize(self, params): - auth = text_(base64.b64encode(bytes_("%s:%s" % (self.client_id, self.api_key)))) - headers = {"Authorization": "Basic %s" % auth} - res = self._session.post("%s/token" % self._oauth_endpoint, params=params, headers=headers) - if res.status_code != 200: - raise APIException("Got unexpected status code from API: %i" % res.status_code) - return res.json() - - def set_auth_values(self, res): - self.__class__ = AuthedConnection - self.token = res['access_token'] - self.refresh_token = res['refresh_token'] - self.expires = int(time.time()) + res['expires_in'] - self._endpoint = self._authed_endpoint - self._session.headers.update({"Authorization": "Bearer %s" % self.token}) - - def authorize(self, code): - res = self._authorize(params={"grant_type": "authorization_code", "code": code}) - self.set_auth_values(res) - - def refr_authorize(self, refresh_token): - res = self._authorize(params={"grant_type": "refresh_token", "refresh_token": refresh_token}) - self.set_auth_values(res) - - def temptoken_authorize(self, access_token=None, expires_in=0, refresh_token=None): - self.set_auth_values({'access_token': access_token, - 'refresh_token': refresh_token, - 'expires_in': expires_in}) - - -class AuthedConnection(EVE): - def __call__(self): - if not self._data: - self._data = APIObject(self.get(self._endpoint), self) - return self._data - - def whoami(self): - # if 'whoami' not in self._cache: - # print "Setting this whoami cache" - # self._cache['whoami'] = self.get("%s/verify" % self._oauth_endpoint) - return self.get("%s/verify" % self._oauth_endpoint) - - def get(self, resource, params=None): - if self.refresh_token and int(time.time()) >= self.expires: - self.refr_authorize(self.refresh_token) - return super(self.__class__, self).get(resource, params) - - def post(self, resource, data, params=None): - if self.refresh_token and int(time.time()) >= self.expires: - self.refr_authorize(self.refresh_token) - return self._session.post(resource, data=data, params=params) - - def delete(self, resource, params=None): - if self.refresh_token and int(time.time()) >= self.expires: - self.refr_authorize(self.refresh_token) - return self._session.delete(resource, params=params) - - -class APIObject(object): - def __init__(self, parent, connection): - self._dict = {} - self.connection = connection - for k, v in list(parent.items()): - if type(v) is dict: - self._dict[k] = APIObject(v, connection) - elif type(v) is list: - self._dict[k] = self._wrap_list(v) - else: - self._dict[k] = v - - def _wrap_list(self, list_): - new = [] - for item in list_: - if type(item) is dict: - new.append(APIObject(item, self.connection)) - elif type(item) is list: - new.append(self._wrap_list(item)) - else: - new.append(item) - return new - - def __getattr__(self, item): - if item in self._dict: - return self._dict[item] - raise AttributeError(item) - - def __call__(self, **kwargs): - # Caching is now handled by APIConnection - if 'href' in self._dict: - return APIObject(self.connection.get(self._dict['href'], params=kwargs), self.connection) - else: - return self - - def __str__(self): # pragma: no cover - return self._dict.__str__() - - def __repr__(self): # pragma: no cover - return self._dict.__repr__() diff --git a/service/pycrest/weak_ciphers.py b/service/pycrest/weak_ciphers.py deleted file mode 100644 index b18a1a552..000000000 --- a/service/pycrest/weak_ciphers.py +++ /dev/null @@ -1,132 +0,0 @@ -import datetime -import ssl -import warnings - -from requests.adapters import HTTPAdapter - -try: - from requests.packages import urllib3 - from requests.packages.urllib3.util import ssl_ - from requests.packages.urllib3.exceptions import ( - SystemTimeWarning, - SecurityWarning, - ) - from requests.packages.urllib3.packages.ssl_match_hostname import \ - match_hostname -except: - import urllib3 - from urllib3.util import ssl_ - from urllib3.exceptions import SystemTimeWarning, SecurityWarning - from urllib3.packages.ssl_match_hostname import match_hostname - - -class WeakCiphersHTTPSConnection(urllib3.connection.VerifiedHTTPSConnection): # pragma: no cover - - # Python versions >=2.7.9 and >=3.4.1 do not (by default) allow ciphers - # with MD5. Unfortunately, the CREST public server _only_ supports - # TLS_RSA_WITH_RC4_128_MD5 (as of 5 Jan 2015). The cipher list below is - # nearly identical except for allowing that cipher as a last resort (and - # excluding export versions of ciphers). - DEFAULT_CIPHERS = ( - 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:' - 'ECDH+HIGH:DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:' - 'RSA+3DES:ECDH+RC4:DH+RC4:RSA+RC4:!aNULL:!eNULL:!EXP:-MD5:RSA+RC4+MD5' - ) - - def __init__(self, host, port, ciphers=None, **kwargs): - self.ciphers = ciphers if ciphers is not None else self.DEFAULT_CIPHERS - super(WeakCiphersHTTPSConnection, self).__init__(host, port, **kwargs) - - def connect(self): - # Yup, copied in VerifiedHTTPSConnection.connect just to change the - # default cipher list. - - # Add certificate verification - conn = self._new_conn() - - resolved_cert_reqs = ssl_.resolve_cert_reqs(self.cert_reqs) - resolved_ssl_version = ssl_.resolve_ssl_version(self.ssl_version) - - hostname = self.host - if getattr(self, '_tunnel_host', None): - # _tunnel_host was added in Python 2.6.3 - # (See: http://hg.python.org/cpython/rev/0f57b30a152f) - - self.sock = conn - # Calls self._set_hostport(), so self.host is - # self._tunnel_host below. - self._tunnel() - # Mark this connection as not reusable - self.auto_open = 0 - - # Override the host with the one we're requesting data from. - hostname = self._tunnel_host - - is_time_off = datetime.date.today() < urllib3.connection.RECENT_DATE - if is_time_off: - warnings.warn(( - 'System time is way off (before {0}). This will probably ' - 'lead to SSL verification errors').format( - urllib3.connection.RECENT_DATE), - SystemTimeWarning - ) - - # Wrap socket using verification with the root certs in - # trusted_root_certs - self.sock = ssl_.ssl_wrap_socket( - conn, - self.key_file, - self.cert_file, - cert_reqs=resolved_cert_reqs, - ca_certs=self.ca_certs, - server_hostname=hostname, - ssl_version=resolved_ssl_version, - ciphers=self.ciphers, - ) - - if self.assert_fingerprint: - ssl_.assert_fingerprint(self.sock.getpeercert(binary_form=True), - self.assert_fingerprint) - elif resolved_cert_reqs != ssl.CERT_NONE \ - and self.assert_hostname is not False: - cert = self.sock.getpeercert() - if not cert.get('subjectAltName', ()): - warnings.warn(( - 'Certificate has no `subjectAltName`, falling back to check for a `commonName` for now. ' - 'This feature is being removed by major browsers and deprecated by RFC 2818. ' - '(See https://github.com/shazow/urllib3/issues/497 for details.)'), - SecurityWarning - ) - match_hostname(cert, self.assert_hostname or hostname) - - self.is_verified = (resolved_cert_reqs == ssl.CERT_REQUIRED or self.assert_fingerprint is not None) - - -class WeakCiphersHTTPSConnectionPool(urllib3.connectionpool.HTTPSConnectionPool): - ConnectionCls = WeakCiphersHTTPSConnection - - -class WeakCiphersPoolManager(urllib3.poolmanager.PoolManager): - def _new_pool(self, scheme, host, port): - if scheme == 'https': - return WeakCiphersHTTPSConnectionPool(host, port, **self.connection_pool_kw) - return super(WeakCiphersPoolManager, self)._new_pool(scheme, host, port) - - -class WeakCiphersAdapter(HTTPAdapter): - """"Transport adapter" that allows us to use TLS_RSA_WITH_RC4_128_MD5.""" - - def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs): - # Rewrite of the requests.adapters.HTTPAdapter.init_poolmanager method - # to use WeakCiphersPoolManager instead of urllib3's PoolManager - self._pool_connections = connections - self._pool_maxsize = maxsize - self._pool_block = block - - self.poolmanager = WeakCiphersPoolManager( - num_pools=connections, - maxsize=maxsize, - block=block, - strict=True, - **pool_kwargs - ) From 7b0f672f049fd3a64d856a31ea4d911323e2e36c Mon Sep 17 00:00:00 2001 From: blitzmann Date: Fri, 9 Feb 2018 18:25:53 -0500 Subject: [PATCH 11/39] Get encrypted refresh tokens working --- config.py | 19 ++++++++++++++++++- eos/saveddata/ssocharacter.py | 13 ++----------- gui/crestFittings.py | 2 +- gui/mainFrame.py | 1 - service/esi.py | 25 +++++++++++++++++++++++-- 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/config.py b/config.py index bac448989..fb834f33e 100644 --- a/config.py +++ b/config.py @@ -5,6 +5,8 @@ from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, StreamHandler, TimedRotatingFileHandler, WARNING import hashlib +from cryptography.fernet import Fernet + pyfalog = Logger(__name__) # Load variable overrides specific to distribution type @@ -34,6 +36,8 @@ gameDB = None logPath = None loggingLevel = None logging_setup = None +cipher = None +clientHash = None ESI_AUTH_PROXY = "http://localhost:5015" # "https://blitzmann.pythonanywhere.com" // need to get this set up, and actually put on it's own domain ESI_CACHE = 'esi_cache' @@ -48,7 +52,7 @@ LOGLEVEL_MAP = { def getClientSecret(): - return hashlib.sha3_256("This is a secret, this will not remain in here for long".encode('utf-8')).hexdigest() + return clientHash def isFrozen(): @@ -90,6 +94,8 @@ def defPaths(customSavePath=None): global gameDB global saveInRoot global logPath + global cipher + global clientHash pyfalog.debug("Configuring Pyfa") @@ -114,6 +120,17 @@ def defPaths(customSavePath=None): __createDirs(savePath) + # get cipher object based on secret key of this client (stores encryption cipher for ESI refresh token) + secret_file = os.path.join(savePath, "{}.secret".format(hashlib.sha3_256(pyfaPath.encode('utf-8')).hexdigest())) + if not os.path.exists(secret_file): + with open(secret_file, "wb") as _file: + _file.write(Fernet.generate_key()) + + with open(secret_file, 'rb') as fp: + key = fp.read() + clientHash = hashlib.sha3_256(key).hexdigest() + cipher = Fernet(key) + # if isFrozen(): # os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(pyfaPath, "cacert.pem") # os.environ["SSL_CERT_FILE"] = os.path.join(pyfaPath, "cacert.pem") diff --git a/eos/saveddata/ssocharacter.py b/eos/saveddata/ssocharacter.py index 95314f5e1..db15442d0 100644 --- a/eos/saveddata/ssocharacter.py +++ b/eos/saveddata/ssocharacter.py @@ -31,8 +31,10 @@ class SsoCharacter(object): self.client = client self.accessToken = accessToken self.refreshToken = refreshToken + self.accessTokenExpires = None self.esi_client = None + @reconstructor def init(self): self.esi_client = None @@ -47,14 +49,3 @@ class SsoCharacter(object): self.accessTokenExpires - datetime.datetime.utcnow() ).total_seconds() } - - def update_token(self, tokenResponse): - """ helper function to update token data from SSO response """ - self.accessToken = tokenResponse['access_token'] - self.accessTokenExpires = datetime.datetime.fromtimestamp( - time.time() + tokenResponse['expires_in'], - ) - if 'refresh_token' in tokenResponse: - self.refreshToken = tokenResponse['refresh_token'] - if self.esi_client is not None: - self.esi_client.security.update_token(tokenResponse) \ No newline at end of file diff --git a/gui/crestFittings.py b/gui/crestFittings.py index ed8750355..32c863812 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -16,7 +16,7 @@ import gui.globalEvents as GE from logbook import Logger import calendar -from service.esi import Esi, CrestModes +from service.esi import Esi pyfalog = Logger(__name__) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 7cef8ea3f..7e6eb9db2 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -83,7 +83,6 @@ import webbrowser import wx.adv from service.esi import Esi -from service.esi import CrestModes from gui.crestFittings import CrestFittings, ExportToEve, CrestMgmt disableOverrideEditor = False diff --git a/service/esi.py b/service/esi.py index 3e47af41a..5014c63c8 100644 --- a/service/esi.py +++ b/service/esi.py @@ -10,6 +10,7 @@ import json import os import eos.db +import datetime from eos.enum import Enum from eos.saveddata.ssocharacter import SsoCharacter import gui.globalEvents as GE @@ -104,7 +105,7 @@ class Esi(object): char = eos.db.getSsoCharacter(id, config.getClientSecret()) if char is not None and char.esi_client is None: char.esi_client = Esi.genEsiClient() - char.esi_client.security.update_token(char.get_sso_data()) + char.esi_client.security.update_token(Esi.get_sso_data(char)) return char def getSkills(self, id): @@ -144,6 +145,26 @@ class Esi(object): resp = char.esi_client.request(op) return resp.data + @staticmethod + def get_sso_data(char): + """ Little "helper" function to get formated data for esipy security + """ + return { + 'access_token': char.accessToken, + 'refresh_token': config.cipher.decrypt(char.refreshToken).decode(), + 'expires_in': (char.accessTokenExpires - datetime.datetime.utcnow()).total_seconds() + } + + @staticmethod + def update_token(char, tokenResponse): + """ helper function to update token data from SSO response """ + char.accessToken = tokenResponse['access_token'] + char.accessTokenExpires = datetime.datetime.fromtimestamp(time.time() + tokenResponse['expires_in']) + if 'refresh_token' in tokenResponse: + char.refreshToken = config.cipher.encrypt(tokenResponse['refresh_token'].encode()) + if char.esi_client is not None: + char.esi_client.security.update_token(tokenResponse) + def stopServer(self): pyfalog.debug("Stopping Server") self.httpd.stop() @@ -200,7 +221,7 @@ class Esi(object): if currentCharacter is None: currentCharacter = SsoCharacter(cdata['CharacterID'], cdata['CharacterName'], config.getClientSecret()) currentCharacter.esi_client = Esi.genEsiClient(esisecurity) - currentCharacter.update_token(auth_response) # this also sets the esi security token + Esi.update_token(currentCharacter, auth_response) # this also sets the esi security token eos.db.save(currentCharacter) From fe1c4cc4d4db5b65a909347780168d91c32d8323 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Mon, 12 Feb 2018 21:43:23 -0500 Subject: [PATCH 12/39] Make a callback for token refresh, not sure how I'm gonna handle this one yet --- service/esi.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/service/esi.py b/service/esi.py index 5014c63c8..bf665470f 100644 --- a/service/esi.py +++ b/service/esi.py @@ -25,6 +25,8 @@ pyfalog = Logger(__name__) cache_path = os.path.join(config.savePath, config.ESI_CACHE) +from esipy.events import AFTER_TOKEN_REFRESH + if not os.path.exists(cache_path): os.mkdir(cache_path) @@ -69,6 +71,8 @@ class Esi(object): def __init__(self): Esi.initEsiApp() + AFTER_TOKEN_REFRESH.add_receiver(self.tokenUpdate) + self.settings = CRESTSettings.getInstance() # these will be set when needed @@ -86,6 +90,10 @@ class Esi(object): import gui.mainFrame # put this here to avoid loop self.mainFrame = gui.mainFrame.MainFrame.getInstance() + def tokenUpdate(self, **kwargs): + print(kwargs) + pass + @property def isTestServer(self): return self.settings.get('server') == Servers.SISI @@ -105,7 +113,9 @@ class Esi(object): char = eos.db.getSsoCharacter(id, config.getClientSecret()) if char is not None and char.esi_client is None: char.esi_client = Esi.genEsiClient() - char.esi_client.security.update_token(Esi.get_sso_data(char)) + Esi.update_token(char, Esi.get_sso_data(char)) # don't use update_token on security directly, se still need to apply the values here + print(repr(char)) + eos.db.commit() return char def getSkills(self, id): From 8276746dad07f3463af7203a95a0489302023a70 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 10 Mar 2018 15:32:52 -0500 Subject: [PATCH 13/39] Do custom EsiPY prefix, change sso endpoint to look at pyfa.io instead of localhost --- config.py | 4 ++-- service/esi.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index 35ab81c76..1944695e9 100644 --- a/config.py +++ b/config.py @@ -40,7 +40,7 @@ logging_setup = None cipher = None clientHash = None -ESI_AUTH_PROXY = "http://localhost:5015" # "https://blitzmann.pythonanywhere.com" // need to get this set up, and actually put on it's own domain +ESI_AUTH_PROXY = "https://www.pyfa.io" # "http://localhost:5015" ESI_CACHE = 'esi_cache' LOGLEVEL_MAP = { @@ -140,7 +140,7 @@ def defPaths(customSavePath=None): # os.environ["SSL_CERT_FILE"] = os.path.join(pyfaPath, "cacert.pem") # The database where we store all the fits etc - saveDB = os.path.join(savePath, "saveddata-py3-dev.db") + saveDB = os.path.join(savePath, "saveddata-py3-esi-dev.db") # The database where the static EVE data from the datadump is kept. # This is not the standard sqlite datadump but a modified version created by eos diff --git a/service/esi.py b/service/esi.py index bf665470f..f1b1e5b1d 100644 --- a/service/esi.py +++ b/service/esi.py @@ -8,6 +8,7 @@ import config import base64 import json import os +import config import eos.db import datetime @@ -47,7 +48,7 @@ class Esi(object): @classmethod def initEsiApp(cls): - cls.esiapp = EsiApp(cache=file_cache) + cls.esiapp = EsiApp(cache=file_cache, cache_time=None, cache_prefix='pyfa{0}-esipy-'.format(config.version)) cls.esi_v1 = cls.esiapp.get_v1_swagger cls.esi_v4 = cls.esiapp.get_v4_swagger From 30c1ab125c6457f9d0a47bbe5b5765e4e213c412 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 10 Mar 2018 17:58:47 -0500 Subject: [PATCH 14/39] Increase esipy requirement --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8d9811795..d43bebe51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ matplotlib >= 2.0.0 python-dateutil requests >= 2.0.0 sqlalchemy >= 1.0.5 -esipy == 0.3.0 +esipy == 0.3.3 markdown2 packaging roman From 6c6e8a9972dfe0e14f1803b808d539c71fd0f0e0 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 11 Mar 2018 14:47:57 -0400 Subject: [PATCH 15/39] Do some error handling --- gui/crestFittings.py | 25 ++++++++++++++++++++++++- service/esi.py | 5 +++-- service/server.py | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 32c863812..46ad72663 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -17,6 +17,8 @@ import gui.globalEvents as GE from logbook import Logger import calendar from service.esi import Esi +from esipy.exceptions import APIException + pyfalog = Logger(__name__) @@ -131,8 +133,9 @@ class CrestFittings(wx.Frame): def fetchFittings(self, event): sCrest = Esi.getInstance() + waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self) + try: - waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self) fittings = sCrest.getFittings(self.getActiveCharacter()) # self.cacheTime = fittings.get('cached_until') # self.updateCacheStatus(None) @@ -143,6 +146,12 @@ class CrestFittings(wx.Frame): msg = "Connection error, please check your internet connection" pyfalog.error(msg) self.statusbar.SetStatusText(msg) + except APIException as ex: + del waitDialog # Can't do this in a finally because then it obscures the message dialog + ESIExceptionHandler(self, ex) + except Exception as ex: + del waitDialog + def importFitting(self, event): selection = self.fitView.fitSelection @@ -173,6 +182,20 @@ class CrestFittings(wx.Frame): self.statusbar.SetStatusText(msg) +class ESIExceptionHandler(object): + def __init__(self, parentWindow, ex): + if ex.response['error'] == "invalid_token": + dlg = wx.MessageDialog(parentWindow, + "There was an error retrieving fits for the selected character due to an invalid token. Please try " + "logging into the character again to reset the token.", "Invalid Token", + wx.OK | wx.ICON_ERROR) + dlg.ShowModal() + pyfalog.error(ex) + else: + # We don't know how to handle the error, raise it for the global error handler to pick it up + raise ex + + class ExportToEve(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="Export fit to EVE", pos=wx.DefaultPosition, diff --git a/service/esi.py b/service/esi.py index f1b1e5b1d..274797af6 100644 --- a/service/esi.py +++ b/service/esi.py @@ -227,12 +227,13 @@ class Esi(object): cdata = esisecurity.verify() print(cdata) - currentCharacter = self.getSsoCharacter(cdata['CharacterID']) + currentCharacter = self.getSsoCharacter(cdata['CharacterName']) if currentCharacter is None: currentCharacter = SsoCharacter(cdata['CharacterID'], cdata['CharacterName'], config.getClientSecret()) currentCharacter.esi_client = Esi.genEsiClient(esisecurity) - Esi.update_token(currentCharacter, auth_response) # this also sets the esi security token + + Esi.update_token(currentCharacter, auth_response) # this also sets the esi security token eos.db.save(currentCharacter) diff --git a/service/server.py b/service/server.py index 34261294a..48e386451 100644 --- a/service/server.py +++ b/service/server.py @@ -85,7 +85,7 @@ class AuthHandler(http.server.BaseHTTPRequestHandler): pyfalog.info("Successfully logged into EVE.") msg = "If you see this message then it means you should be logged into CREST. You may close this window and return to the application." else: - # For implicit mode, we have to serve up the page which will take the hash and redirect useing a querystring + # For implicit mode, we have to serve up the page which will take the hash and redirect using a querystring pyfalog.info("Processing response from EVE Online.") msg = "Processing response from EVE Online" except Exception as ex: From bbdf1ee6ccc24e661fc854ea89a2750c098c65f6 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 11 Mar 2018 15:31:41 -0400 Subject: [PATCH 16/39] More tweaks / error handling for esi --- gui/crestFittings.py | 13 +++++++++---- service/port.py | 9 +++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 46ad72663..763d722bf 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -18,7 +18,7 @@ from logbook import Logger import calendar from service.esi import Esi from esipy.exceptions import APIException - +from service.port import ESIExportException pyfalog = Logger(__name__) @@ -147,12 +147,11 @@ class CrestFittings(wx.Frame): pyfalog.error(msg) self.statusbar.SetStatusText(msg) except APIException as ex: - del waitDialog # Can't do this in a finally because then it obscures the message dialog + del waitDialog # Can't do this in a finally because then it obscures the message dialog ESIExceptionHandler(self, ex) except Exception as ex: del waitDialog - def importFitting(self, event): selection = self.fitView.fitSelection if not selection: @@ -186,7 +185,7 @@ class ESIExceptionHandler(object): def __init__(self, parentWindow, ex): if ex.response['error'] == "invalid_token": dlg = wx.MessageDialog(parentWindow, - "There was an error retrieving fits for the selected character due to an invalid token. Please try " + "There was an error validating characters' SSO token. Please try " "logging into the character again to reset the token.", "Invalid Token", wx.OK | wx.ICON_ERROR) dlg.ShowModal() @@ -298,6 +297,12 @@ class ExportToEve(wx.Frame): msg = "Connection error, please check your internet connection" pyfalog.error(msg) self.statusbar.SetStatusText(msg) + except ESIExportException as ex: + pyfalog.error(ex) + self.statusbar.SetStatusText("ERROR", 0) + self.statusbar.SetStatusText(ex.args[0], 1) + except APIException as ex: + ESIExceptionHandler(self, ex) class CrestMgmt(wx.Dialog): diff --git a/service/port.py b/service/port.py index 7542dedb7..ec15fdf34 100644 --- a/service/port.py +++ b/service/port.py @@ -54,6 +54,10 @@ from abc import ABCMeta, abstractmethod from service.esi import Esi from collections import OrderedDict + +class ESIExportException(Exception): + pass + pyfalog = Logger(__name__) EFT_SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE] @@ -348,8 +352,6 @@ class Port(object): sCrest = Esi.getInstance() sFit = svcFit.getInstance() - eve = sCrest.eve - # max length is 50 characters name = ofit.name[:47] + '...' if len(ofit.name) > 50 else ofit.name fit['name'] = name @@ -418,6 +420,9 @@ class Port(object): item['type_id'] = fighter.item.ID fit['items'].append(item) + if len(fit['items']) == 0: + raise ESIExportException("Cannot export fitting: module list cannot be empty.") + return json.dumps(fit) @classmethod From 5169c35d5c387591e73dd931b1528283ab00f339 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 11 Mar 2018 16:50:12 -0400 Subject: [PATCH 17/39] Implement SSO Character delete, Remove some unused character logout code (still need to hook this up with character delete functionality) --- gui/mainFrame.py | 29 ----------------------------- service/esi.py | 3 ++- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index c893449c0..b36f9b4ce 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -237,10 +237,6 @@ class MainFrame(wx.Frame): self.sUpdate.CheckUpdate(self.ShowUpdateBox) self.Bind(GE.EVT_SSO_LOGIN, self.onSSOLogin) - self.Bind(GE.EVT_SSO_LOGOUT, self.onSSOLogout) - - self.titleTimer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self.updateTitle, self.titleTimer) def ShowUpdateBox(self, release, version): dlg = UpdateDialog(self, release, version) @@ -612,36 +608,11 @@ class MainFrame(wx.Frame): dlg = CrestFittings(self) dlg.Show() - def updateTitle(self, event): - sCrest = Esi.getInstance() - char = sCrest.implicitCharacter - if char: - t = time.gmtime(char.eve.expires - time.time()) - sTime = time.strftime("%H:%M:%S", t) - newTitle = "%s | %s - %s" % (self.title, char.name, sTime) - self.SetTitle(newTitle) - def onSSOLogin(self, event): menu = self.GetMenuBar() menu.Enable(menu.eveFittingsId, True) menu.Enable(menu.exportToEveId, True) - if event.type == CrestModes.IMPLICIT: - menu.SetLabel(menu.ssoLoginId, "Logout Character") - self.titleTimer.Start(1000) - - def onSSOLogout(self, event): - self.titleTimer.Stop() - self.SetTitle(self.title) - - menu = self.GetMenuBar() - if event.type == CrestModes.IMPLICIT or event.numChars == 0: - menu.Enable(menu.eveFittingsId, False) - menu.Enable(menu.exportToEveId, False) - - if event.type == CrestModes.IMPLICIT: - menu.SetLabel(menu.ssoLoginId, "Login to EVE") - def updateCrestMenus(self, type): # in case we are logged in when switching, change title back self.titleTimer.Stop() diff --git a/service/esi.py b/service/esi.py index 274797af6..1fe3a7cac 100644 --- a/service/esi.py +++ b/service/esi.py @@ -100,7 +100,7 @@ class Esi(object): return self.settings.get('server') == Servers.SISI def delSsoCharacter(self, id): - char = eos.db.getSsoCharacter(id) + char = eos.db.getSsoCharacter(id, config.getClientSecret()) eos.db.remove(char) def getSsoCharacters(self): @@ -236,4 +236,5 @@ class Esi(object): Esi.update_token(currentCharacter, auth_response) # this also sets the esi security token eos.db.save(currentCharacter) + wx.PostEvent(self.mainFrame, GE.SsoLogin(character = currentCharacter)) From 75f9a0252a974da01a130f0cba666d4e12c16a18 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 11 Mar 2018 17:38:51 -0400 Subject: [PATCH 18/39] Start working on character to SSO character mapping --- eos/db/__init__.py | 2 +- eos/db/saveddata/__init__.py | 2 +- eos/db/saveddata/character.py | 1 - eos/db/saveddata/{crest.py => ssoCharacter.py} | 7 ++++++- service/esi.py | 14 +++++++++----- 5 files changed, 17 insertions(+), 9 deletions(-) rename eos/db/saveddata/{crest.py => ssoCharacter.py} (86%) diff --git a/eos/db/__init__.py b/eos/db/__init__.py index c3bdb9739..a5b3d6c7e 100644 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -76,7 +76,7 @@ sd_lock = threading.RLock() # noinspection PyPep8 from eos.db.gamedata import alphaClones, attribute, category, effect, group, icon, item, marketGroup, metaData, metaGroup, queries, traits, unit # noinspection PyPep8 -from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, \ +from eos.db.saveddata import booster, cargo, character, ssoCharacter, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, \ miscData, module, override, price, queries, skill, targetResists, user # Import queries diff --git a/eos/db/saveddata/__init__.py b/eos/db/saveddata/__init__.py index 7dbf2a45d..cc581e3ef 100644 --- a/eos/db/saveddata/__init__.py +++ b/eos/db/saveddata/__init__.py @@ -12,7 +12,7 @@ __all__ = [ "miscData", "targetResists", "override", - "crest", + "ssoCharacter", "implantSet", "loadDefaultDatabaseValues" ] diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index 8bf7d59c3..f3c6f8f8d 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -32,7 +32,6 @@ from eos.saveddata.ssocharacter import SsoCharacter characters_table = Table("characters", saveddata_meta, Column("ID", Integer, primary_key=True), Column("name", String, nullable=False), - Column("ssoCharacterID", ForeignKey("ssoCharacter.ID"), nullable=True), Column("defaultLevel", Integer, nullable=True), Column("alphaCloneID", Integer, nullable=True), Column("ownerID", ForeignKey("users.ID"), nullable=True), diff --git a/eos/db/saveddata/crest.py b/eos/db/saveddata/ssoCharacter.py similarity index 86% rename from eos/db/saveddata/crest.py rename to eos/db/saveddata/ssoCharacter.py index 429da243d..80610890a 100644 --- a/eos/db/saveddata/crest.py +++ b/eos/db/saveddata/ssoCharacter.py @@ -17,7 +17,7 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, String, DateTime, UniqueConstraint +from sqlalchemy import Table, Column, Integer, String, DateTime, UniqueConstraint, ForeignKey from sqlalchemy.orm import mapper import datetime @@ -38,4 +38,9 @@ sso_table = Table("ssoCharacter", saveddata_meta, UniqueConstraint('client', 'characterName', name='uix_client_characterName') ) +sso_character_map_table = Table("ssoCharacterMap", saveddata_meta, + Column("characterID", ForeignKey("characters.ID"), primary_key=True), + Column("ssoCharacterID", ForeignKey("ssoCharacter.ID"), primary_key=True), + ) + mapper(SsoCharacter, sso_table) diff --git a/service/esi.py b/service/esi.py index 1fe3a7cac..c174021dd 100644 --- a/service/esi.py +++ b/service/esi.py @@ -44,15 +44,18 @@ class Esi(object): esi_v1 = None esi_v4 = None + _initializing = None + _instance = None @classmethod def initEsiApp(cls): - cls.esiapp = EsiApp(cache=file_cache, cache_time=None, cache_prefix='pyfa{0}-esipy-'.format(config.version)) - cls.esi_v1 = cls.esiapp.get_v1_swagger - cls.esi_v4 = cls.esiapp.get_v4_swagger - - # esiRdy.set() + if cls._initializing is None: + cls._initializing = True + cls.esiapp = EsiApp(cache=file_cache, cache_time=None, cache_prefix='pyfa{0}-esipy-'.format(config.version)) + cls.esi_v1 = cls.esiapp.get_v1_swagger + cls.esi_v4 = cls.esiapp.get_v4_swagger + cls._initializing = False @classmethod def genEsiClient(cls, security=None): @@ -102,6 +105,7 @@ class Esi(object): def delSsoCharacter(self, id): char = eos.db.getSsoCharacter(id, config.getClientSecret()) eos.db.remove(char) + wx.PostEvent(self.mainFrame, GE.SsoLogout(charID=id)) def getSsoCharacters(self): chars = eos.db.getSsoCharacters(config.getClientSecret()) From be19b7414ab460a3f5acec312ac8dff5b422176d Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 11 Mar 2018 19:39:58 -0400 Subject: [PATCH 19/39] Make a relation between characters and sso chars --- eos/db/saveddata/character.py | 14 ++++++++++++-- eos/effectHandlerHelpers.py | 7 +++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index f3c6f8f8d..a4c3476ad 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -23,7 +23,8 @@ import datetime from eos.db import saveddata_meta from eos.db.saveddata.implant import charImplants_table -from eos.effectHandlerHelpers import HandledImplantBoosterList +from eos.db.saveddata.ssoCharacter import sso_character_map_table +from eos.effectHandlerHelpers import HandledImplantBoosterList, HandledSsoCharacterList from eos.saveddata.implant import Implant from eos.saveddata.user import User from eos.saveddata.character import Character, Skill @@ -59,6 +60,15 @@ mapper(Character, characters_table, single_parent=True, primaryjoin=charImplants_table.c.charID == characters_table.c.ID, secondaryjoin=charImplants_table.c.implantID == Implant.ID, - secondary=charImplants_table) + secondary=charImplants_table), + "_Character__ssoCharacters" : relation( + SsoCharacter, + collection_class=HandledSsoCharacterList, + cascade='all,delete-orphan', + backref='character', + single_parent=True, + primaryjoin=sso_character_map_table.c.characterID == characters_table.c.ID, + secondaryjoin=sso_character_map_table.c.ssoCharacterID == SsoCharacter.ID, + secondary=sso_character_map_table) } ) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 9d86e51e3..3013ffd7a 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -205,6 +205,13 @@ class HandledImplantBoosterList(HandledList): HandledList.append(self, thing) +class HandledSsoCharacterList(HandledList): + def append(self, character): + for x in self: + print(x) + HandledList.append(self, character) + + class HandledProjectedModList(HandledList): def append(self, proj): if proj.isInvalid: From f52f39984f0304af2dec9c7d062b1da0a3e71ff4 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Mon, 12 Mar 2018 21:13:18 -0400 Subject: [PATCH 20/39] Add api view back to character editor (non functioning) --- gui/characterEditor.py | 100 ++--------------------------------------- 1 file changed, 3 insertions(+), 97 deletions(-) diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 79602dbd7..ae75c11b1 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -170,11 +170,11 @@ class CharacterEditor(wx.Frame): self.sview = SkillTreeView(self.viewsNBContainer) self.iview = ImplantEditorView(self.viewsNBContainer, self) - # self.aview = APIView(self.viewsNBContainer) + self.aview = APIView(self.viewsNBContainer) self.viewsNBContainer.AddPage(self.sview, "Skills") self.viewsNBContainer.AddPage(self.iview, "Implants") - # self.viewsNBContainer.AddPage(self.aview, "API") + self.viewsNBContainer.AddPage(self.aview, "API") mainSizer.Add(self.viewsNBContainer, 1, wx.EXPAND | wx.ALL, 5) @@ -722,15 +722,13 @@ class APIView(wx.Panel): self.charEditor = self.Parent.Parent # first parent is Notebook, second is Character Editor self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) - self.apiUrlCreatePredefined = "https://community.eveonline.com/support/api-key/CreatePredefined?accessMask=8" - self.apiUrlKeyList = "https://community.eveonline.com/support/api-key/" pmainSizer = wx.BoxSizer(wx.VERTICAL) hintSizer = wx.BoxSizer(wx.HORIZONTAL) hintSizer.AddStretchSpacer() self.stDisabledTip = wx.StaticText(self, wx.ID_ANY, - "You cannot add API Details for All 0 and All 5 characters.\n" + "You cannot link All 0 or All 5 characters to an EVE character.\n" "Please select another character or make a new one.", style=wx.ALIGN_CENTER) self.stDisabledTip.Wrap(-1) hintSizer.Add(self.stDisabledTip, 0, wx.TOP | wx.BOTTOM, 10) @@ -743,20 +741,6 @@ class APIView(wx.Panel): fgSizerInput.SetFlexibleDirection(wx.BOTH) fgSizerInput.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) - self.m_staticIDText = wx.StaticText(self, wx.ID_ANY, "keyID:", wx.DefaultPosition, wx.DefaultSize, 0) - self.m_staticIDText.Wrap(-1) - fgSizerInput.Add(self.m_staticIDText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) - - self.inputID = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0) - fgSizerInput.Add(self.inputID, 1, wx.ALL | wx.EXPAND, 5) - - self.m_staticKeyText = wx.StaticText(self, wx.ID_ANY, "vCode:", wx.DefaultPosition, wx.DefaultSize, 0) - self.m_staticKeyText.Wrap(-1) - fgSizerInput.Add(self.m_staticKeyText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) - - self.inputKey = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0) - fgSizerInput.Add(self.inputKey, 0, wx.ALL | wx.EXPAND, 5) - self.m_staticCharText = wx.StaticText(self, wx.ID_ANY, "Character:", wx.DefaultPosition, wx.DefaultSize, 0) self.m_staticCharText.Wrap(-1) fgSizerInput.Add(self.m_staticCharText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) @@ -765,47 +749,14 @@ class APIView(wx.Panel): self.charChoice.Append("No Selection", 0) fgSizerInput.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 5) - self.charChoice.Enable(False) - pmainSizer.Add(fgSizerInput, 0, wx.EXPAND, 5) btnSizer = wx.BoxSizer(wx.HORIZONTAL) - btnSizer.AddStretchSpacer() - - self.btnFetchCharList = wx.Button(self, wx.ID_ANY, "Get Characters") - btnSizer.Add(self.btnFetchCharList, 0, wx.ALL, 2) - self.btnFetchCharList.Bind(wx.EVT_BUTTON, self.fetchCharList) - - self.btnFetchSkills = wx.Button(self, wx.ID_ANY, "Fetch Skills") - btnSizer.Add(self.btnFetchSkills, 0, wx.ALL, 2) - self.btnFetchSkills.Bind(wx.EVT_BUTTON, self.fetchSkills) - self.btnFetchSkills.Enable(False) - - btnSizer.AddStretchSpacer() pmainSizer.Add(btnSizer, 0, wx.EXPAND, 5) self.stStatus = wx.StaticText(self, wx.ID_ANY, wx.EmptyString) pmainSizer.Add(self.stStatus, 0, wx.ALL, 5) - pmainSizer.AddStretchSpacer() - self.stAPITip = wx.StaticText(self, wx.ID_ANY, - "You can create a pre-defined key here (only CharacterSheet is required):", - wx.DefaultPosition, wx.DefaultSize, 0) - self.stAPITip.Wrap(-1) - - pmainSizer.Add(self.stAPITip, 0, wx.ALL, 2) - - self.hlEveAPI = wx.lib.agw.hyperlink.HyperLinkCtrl(self, wx.ID_ANY, label=self.apiUrlCreatePredefined) - pmainSizer.Add(self.hlEveAPI, 0, wx.ALL, 2) - - self.stAPITip2 = wx.StaticText(self, wx.ID_ANY, "Or, you can choose an existing key from:", wx.DefaultPosition, - wx.DefaultSize, 0) - self.stAPITip2.Wrap(-1) - pmainSizer.Add(self.stAPITip2, 0, wx.ALL, 2) - - self.hlEveAPI2 = wx.lib.agw.hyperlink.HyperLinkCtrl(self, wx.ID_ANY, label=self.apiUrlKeyList) - pmainSizer.Add(self.hlEveAPI2, 0, wx.ALL, 2) - self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) self.SetSizer(pmainSizer) @@ -817,8 +768,6 @@ class APIView(wx.Panel): activeChar = self.charEditor.entityEditor.getActiveEntity() ID, key, char, chars = sChar.getApiDetails(activeChar.ID) - self.inputID.SetValue(str(ID)) - self.inputKey.SetValue(key) self.charChoice.Clear() @@ -827,12 +776,10 @@ class APIView(wx.Panel): self.charChoice.Append(charName) self.charChoice.SetStringSelection(char) self.charChoice.Enable(True) - self.btnFetchSkills.Enable(True) else: self.charChoice.Append("No characters...", 0) self.charChoice.SetSelection(0) self.charChoice.Enable(False) - self.btnFetchSkills.Enable(False) if activeChar.name in ("All 0", "All 5"): self.Enable(False) @@ -846,47 +793,6 @@ class APIView(wx.Panel): if event is not None: event.Skip() - def fetchCharList(self, event): - self.stStatus.SetLabel("") - if self.inputID.GetLineText(0) == "" or self.inputKey.GetLineText(0) == "": - self.stStatus.SetLabel("Invalid keyID or vCode!") - return - - sChar = Character.getInstance() - try: - activeChar = self.charEditor.entityEditor.getActiveEntity() - list = sChar.apiCharList(activeChar.ID, self.inputID.GetLineText(0), self.inputKey.GetLineText(0)) - except AuthenticationError as e: - msg = "Authentication failure. Please check keyID and vCode combination." - pyfalog.info(msg) - self.stStatus.SetLabel(msg) - except TimeoutError as e: - msg = "Request timed out. Please check network connectivity and/or proxy settings." - pyfalog.info(msg) - self.stStatus.SetLabel(msg) - except Exception as e: - pyfalog.error(e) - self.stStatus.SetLabel("Error:\n%s" % e) - else: - self.charChoice.Clear() - for charName in list: - self.charChoice.Append(charName) - - self.btnFetchSkills.Enable(True) - self.charChoice.Enable(True) - - self.Layout() - - self.charChoice.SetSelection(0) - - def fetchSkills(self, event): - charName = self.charChoice.GetString(self.charChoice.GetSelection()) - if charName: - sChar = Character.getInstance() - activeChar = self.charEditor.entityEditor.getActiveEntity() - sChar.apiFetch(activeChar.ID, charName, self.__fetchCallback) - self.stStatus.SetLabel("Getting skills for {}".format(charName)) - def __fetchCallback(self, e=None): charName = self.charChoice.GetString(self.charChoice.GetSelection()) if e is None: From 9d379d966c7efd2f4abdddcc4e627e3a9b61245b Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Wed, 14 Mar 2018 17:53:48 -0400 Subject: [PATCH 21/39] Implement working character associations with SSO character --- eos/effectHandlerHelpers.py | 11 +++++--- eos/saveddata/character.py | 12 +++++++++ eos/saveddata/ssocharacter.py | 6 +++++ gui/characterEditor.py | 49 +++++++++++++++++++++++++---------- service/character.py | 16 ++++++++++++ 5 files changed, 77 insertions(+), 17 deletions(-) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 3013ffd7a..f890ae0ed 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -205,11 +205,14 @@ class HandledImplantBoosterList(HandledList): HandledList.append(self, thing) -class HandledSsoCharacterList(HandledList): +class HandledSsoCharacterList(list): def append(self, character): - for x in self: - print(x) - HandledList.append(self, character) + old = next((x for x in self if x.client == character.client), None) + if old is not None: + pyfalog.warning("Removing SSO Character with same hash: {}".format(repr(old))) + list.remove(self, old) + + list.append(self, character) class HandledProjectedModList(HandledList): diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 77551a5ed..05ff456fa 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -163,6 +163,18 @@ class Character(object): def name(self, name): self.savedName = name + def setSsoCharacter(self, character, clientHash): + if character is not None: + self.__ssoCharacters.append(character) + else: + for x in self.__ssoCharacters: + if x.client == clientHash: + self.__ssoCharacters.remove(x) + + + def getSsoCharacter(self, clientHash): + return next((x for x in self.__ssoCharacters if x.client == clientHash), None) + @property def alphaCloneID(self): return self.__alphaCloneID diff --git a/eos/saveddata/ssocharacter.py b/eos/saveddata/ssocharacter.py index db15442d0..9ffed8d46 100644 --- a/eos/saveddata/ssocharacter.py +++ b/eos/saveddata/ssocharacter.py @@ -49,3 +49,9 @@ class SsoCharacter(object): self.accessTokenExpires - datetime.datetime.utcnow() ).total_seconds() } + + + def __repr__(self): + return "SsoCharacter(ID={}, name={}, client={}) at {}".format( + self.ID, self.characterName, self.client, hex(id(self)) + ) diff --git a/gui/characterEditor.py b/gui/characterEditor.py index ae75c11b1..dcb14bfb3 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -33,6 +33,7 @@ from gui.builtinViews.implantEditor import BaseImplantEditorView from gui.builtinViews.entityEditor import EntityEditor, BaseValidator, TextEntryValidatedDialog from service.fit import Fit from service.character import Character +from service.esi import Esi from service.network import AuthenticationError, TimeoutError from service.market import Market from logbook import Logger @@ -174,7 +175,7 @@ class CharacterEditor(wx.Frame): self.viewsNBContainer.AddPage(self.sview, "Skills") self.viewsNBContainer.AddPage(self.iview, "Implants") - self.viewsNBContainer.AddPage(self.aview, "API") + self.viewsNBContainer.AddPage(self.aview, "EVE SSO") mainSizer.Add(self.viewsNBContainer, 1, wx.EXPAND | wx.ALL, 5) @@ -746,7 +747,6 @@ class APIView(wx.Panel): fgSizerInput.Add(self.m_staticCharText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) self.charChoice = wx.Choice(self, wx.ID_ANY, style=0) - self.charChoice.Append("No Selection", 0) fgSizerInput.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 5) pmainSizer.Add(fgSizerInput, 0, wx.EXPAND, 5) @@ -759,28 +759,51 @@ class APIView(wx.Panel): self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) + self.charChoice.Bind(wx.EVT_CHOICE, self.ssoCharChanged) + self.SetSizer(pmainSizer) self.Layout() self.charChanged(None) - def charChanged(self, event): + def ssoCharChanged(self, event): sChar = Character.getInstance() activeChar = self.charEditor.entityEditor.getActiveEntity() + sChar.setSsoCharacter(activeChar.ID, self.getActiveCharacter()) + event.Skip() - ID, key, char, chars = sChar.getApiDetails(activeChar.ID) + def getActiveCharacter(self): + selection = self.charChoice.GetCurrentSelection() + return self.charChoice.GetClientData(selection) if selection is not -1 else None + + def charChanged(self, event): + sChar = Character.getInstance() + sEsi = Esi.getInstance() + + activeChar = self.charEditor.entityEditor.getActiveEntity() + sso = sChar.getSsoCharacter(activeChar.ID) + + ssoChars = sEsi.getSsoCharacters() self.charChoice.Clear() - if chars: - for charName in chars: - self.charChoice.Append(charName) - self.charChoice.SetStringSelection(char) - self.charChoice.Enable(True) - else: - self.charChoice.Append("No characters...", 0) - self.charChoice.SetSelection(0) - self.charChoice.Enable(False) + noneID = self.charChoice.Append("None", None) + for char in ssoChars: + currId = self.charChoice.Append(char.characterName, char.ID) + + if sso is not None and char.ID == sso.ID: + self.charChoice.SetSelection(currId) + if sso is None: + self.charChoice.SetSelection(noneID) + # + # if chars: + # for charName in chars: + # self.charChoice.Append(charName) + # self.charChoice.SetStringSelection(char) + # else: + # self.charChoice.Append("No characters...", 0) + # self.charChoice.SetSelection(0) + # if activeChar.name in ("All 0", "All 5"): self.Enable(False) self.stDisabledTip.Show() diff --git a/service/character.py b/service/character.py index ce5246489..6e5b07a0c 100644 --- a/service/character.py +++ b/service/character.py @@ -347,6 +347,22 @@ class Character(object): chars = None return char.apiID or "", char.apiKey or "", char.defaultChar or "", chars or [] + @staticmethod + def getSsoCharacter(charID): + char = eos.db.getCharacter(charID) + sso = char.getSsoCharacter(config.getClientSecret()) + return sso + + @staticmethod + def setSsoCharacter(charID, ssoCharID): + char = eos.db.getCharacter(charID) + if ssoCharID is not None: + sso = eos.db.getSsoCharacter(ssoCharID, config.getClientSecret()) + char.setSsoCharacter(sso, config.getClientSecret()) + else: + char.setSsoCharacter(None, config.getClientSecret()) + eos.db.commit() + def apiFetch(self, charID, callback): thread = UpdateAPIThread(charID, (self.apiFetchCallback, callback)) thread.start() From 9839efc2dc0afe6311153f99dfc1f87edb7aa425 Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Wed, 14 Mar 2018 18:40:38 -0400 Subject: [PATCH 22/39] Fix sso character relationship to not delete sso character (whoops) --- eos/db/saveddata/character.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index a4c3476ad..5bf9a1cd7 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -64,9 +64,7 @@ mapper(Character, characters_table, "_Character__ssoCharacters" : relation( SsoCharacter, collection_class=HandledSsoCharacterList, - cascade='all,delete-orphan', - backref='character', - single_parent=True, + cascade='all', primaryjoin=sso_character_map_table.c.characterID == characters_table.c.ID, secondaryjoin=sso_character_map_table.c.ssoCharacterID == SsoCharacter.ID, secondary=sso_character_map_table) From 4c6f68b07ef983b770fe9034d97ae550c94177ef Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Wed, 14 Mar 2018 18:46:49 -0400 Subject: [PATCH 23/39] Move sso character stuff to character file (for now while I try to figure out how to properly delete these) --- eos/db/saveddata/character.py | 38 ++++++++++++++++++++++++++++---- eos/db/saveddata/ssoCharacter.py | 21 ------------------ 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index 5bf9a1cd7..7de696ab8 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -17,19 +17,21 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, String, DateTime, Float +from sqlalchemy import Table, Column, Integer, ForeignKey, String, DateTime, Float, UniqueConstraint from sqlalchemy.orm import relation, mapper import datetime from eos.db import saveddata_meta from eos.db.saveddata.implant import charImplants_table -from eos.db.saveddata.ssoCharacter import sso_character_map_table from eos.effectHandlerHelpers import HandledImplantBoosterList, HandledSsoCharacterList from eos.saveddata.implant import Implant from eos.saveddata.user import User from eos.saveddata.character import Character, Skill from eos.saveddata.ssocharacter import SsoCharacter + + + characters_table = Table("characters", saveddata_meta, Column("ID", Integer, primary_key=True), Column("name", String, nullable=False), @@ -40,6 +42,36 @@ characters_table = Table("characters", saveddata_meta, Column("created", DateTime, nullable=True, default=datetime.datetime.now), Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)) +sso_table = Table("ssoCharacter", saveddata_meta, + Column("ID", Integer, primary_key=True), + Column("client", String, nullable=False), + Column("characterID", Integer, nullable=False), + Column("characterName", String, nullable=False), + Column("refreshToken", String, nullable=False), + Column("accessToken", String, nullable=False), + Column("accessTokenExpires", DateTime, nullable=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), + UniqueConstraint('client', 'characterID', name='uix_client_characterID'), + UniqueConstraint('client', 'characterName', name='uix_client_characterName') + ) + +sso_character_map_table = Table("ssoCharacterMap", saveddata_meta, + Column("characterID", ForeignKey("characters.ID"), primary_key=True), + Column("ssoCharacterID", ForeignKey("ssoCharacter.ID"), primary_key=True), + ) + + +mapper(SsoCharacter, sso_table, + properties={ + "_SsoCharacter__maps": relation( + Character, + cascade="all", + secondary=sso_character_map_table + ), +} +) + mapper(Character, characters_table, properties={ "_Character__alphaCloneID": characters_table.c.alphaCloneID, @@ -65,8 +97,6 @@ mapper(Character, characters_table, SsoCharacter, collection_class=HandledSsoCharacterList, cascade='all', - primaryjoin=sso_character_map_table.c.characterID == characters_table.c.ID, - secondaryjoin=sso_character_map_table.c.ssoCharacterID == SsoCharacter.ID, secondary=sso_character_map_table) } ) diff --git a/eos/db/saveddata/ssoCharacter.py b/eos/db/saveddata/ssoCharacter.py index 80610890a..b8210b197 100644 --- a/eos/db/saveddata/ssoCharacter.py +++ b/eos/db/saveddata/ssoCharacter.py @@ -23,24 +23,3 @@ import datetime from eos.db import saveddata_meta from eos.saveddata.ssocharacter import SsoCharacter - -sso_table = Table("ssoCharacter", saveddata_meta, - Column("ID", Integer, primary_key=True), - Column("client", String, nullable=False), - Column("characterID", Integer, nullable=False), - Column("characterName", String, nullable=False), - Column("refreshToken", String, nullable=False), - Column("accessToken", String, nullable=False), - Column("accessTokenExpires", DateTime, nullable=False), - Column("created", DateTime, nullable=True, default=datetime.datetime.now), - Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), - UniqueConstraint('client', 'characterID', name='uix_client_characterID'), - UniqueConstraint('client', 'characterName', name='uix_client_characterName') - ) - -sso_character_map_table = Table("ssoCharacterMap", saveddata_meta, - Column("characterID", ForeignKey("characters.ID"), primary_key=True), - Column("ssoCharacterID", ForeignKey("ssoCharacter.ID"), primary_key=True), - ) - -mapper(SsoCharacter, sso_table) From b74654a5b34335acb6535a5d5e1fe1702fc1bfec Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Thu, 15 Mar 2018 02:58:58 -0400 Subject: [PATCH 24/39] Fix relationships for sso / characters --- eos/db/saveddata/character.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index 7de696ab8..23c05ca26 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -62,15 +62,7 @@ sso_character_map_table = Table("ssoCharacterMap", saveddata_meta, ) -mapper(SsoCharacter, sso_table, - properties={ - "_SsoCharacter__maps": relation( - Character, - cascade="all", - secondary=sso_character_map_table - ), -} -) +mapper(SsoCharacter, sso_table) mapper(Character, characters_table, properties={ @@ -96,7 +88,7 @@ mapper(Character, characters_table, "_Character__ssoCharacters" : relation( SsoCharacter, collection_class=HandledSsoCharacterList, - cascade='all', + backref='characters', secondary=sso_character_map_table) } ) From 8153b80d058261b9a8aa40dc973d1119f38d0404 Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Thu, 15 Mar 2018 19:57:58 -0400 Subject: [PATCH 25/39] Work around for sso character not being delete from characters after sso deleted, some other minor tweaks --- eos/db/__init__.py | 2 +- eos/db/saveddata/__init__.py | 1 - eos/db/saveddata/character.py | 2 +- eos/db/saveddata/ssoCharacter.py | 25 ------------------ gui/characterEditor.py | 44 ++++++++++++++++++++++++++------ gui/crestFittings.py | 11 +++----- gui/mainMenuBar.py | 2 +- service/esi.py | 6 +++++ 8 files changed, 48 insertions(+), 45 deletions(-) delete mode 100644 eos/db/saveddata/ssoCharacter.py diff --git a/eos/db/__init__.py b/eos/db/__init__.py index a5b3d6c7e..dd027841b 100644 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -76,7 +76,7 @@ sd_lock = threading.RLock() # noinspection PyPep8 from eos.db.gamedata import alphaClones, attribute, category, effect, group, icon, item, marketGroup, metaData, metaGroup, queries, traits, unit # noinspection PyPep8 -from eos.db.saveddata import booster, cargo, character, ssoCharacter, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, \ +from eos.db.saveddata import booster, cargo, character, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, \ miscData, module, override, price, queries, skill, targetResists, user # Import queries diff --git a/eos/db/saveddata/__init__.py b/eos/db/saveddata/__init__.py index cc581e3ef..ba1ddad73 100644 --- a/eos/db/saveddata/__init__.py +++ b/eos/db/saveddata/__init__.py @@ -12,7 +12,6 @@ __all__ = [ "miscData", "targetResists", "override", - "ssoCharacter", "implantSet", "loadDefaultDatabaseValues" ] diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index 23c05ca26..739a34a98 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -88,7 +88,7 @@ mapper(Character, characters_table, "_Character__ssoCharacters" : relation( SsoCharacter, collection_class=HandledSsoCharacterList, - backref='characters', + backref='characters', secondary=sso_character_map_table) } ) diff --git a/eos/db/saveddata/ssoCharacter.py b/eos/db/saveddata/ssoCharacter.py deleted file mode 100644 index b8210b197..000000000 --- a/eos/db/saveddata/ssoCharacter.py +++ /dev/null @@ -1,25 +0,0 @@ -# =============================================================================== -# Copyright (C) 2010 Diego Duclos -# -# This file is part of eos. -# -# eos is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# eos is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with eos. If not, see . -# =============================================================================== - -from sqlalchemy import Table, Column, Integer, String, DateTime, UniqueConstraint, ForeignKey -from sqlalchemy.orm import mapper -import datetime - -from eos.db import saveddata_meta -from eos.saveddata.ssocharacter import SsoCharacter diff --git a/gui/characterEditor.py b/gui/characterEditor.py index dcb14bfb3..3b89c7049 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -45,6 +45,7 @@ from gui.utils.clipboard import toClipboard, fromClipboard import roman import re +import webbrowser pyfalog = Logger(__name__) @@ -723,7 +724,6 @@ class APIView(wx.Panel): self.charEditor = self.Parent.Parent # first parent is Notebook, second is Character Editor self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) - pmainSizer = wx.BoxSizer(wx.VERTICAL) hintSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -733,6 +733,13 @@ class APIView(wx.Panel): "Please select another character or make a new one.", style=wx.ALIGN_CENTER) self.stDisabledTip.Wrap(-1) hintSizer.Add(self.stDisabledTip, 0, wx.TOP | wx.BOTTOM, 10) + + self.noCharactersTip = wx.StaticText(self, wx.ID_ANY, + "You haven't logging into EVE SSO with any characters yet. Please use the " + "button below to log into EVE.", style=wx.ALIGN_CENTER) + self.noCharactersTip.Wrap(-1) + hintSizer.Add(self.noCharactersTip, 0, wx.TOP | wx.BOTTOM, 10) + self.stDisabledTip.Hide() hintSizer.AddStretchSpacer() pmainSizer.Add(hintSizer, 0, wx.EXPAND, 5) @@ -744,26 +751,27 @@ class APIView(wx.Panel): self.m_staticCharText = wx.StaticText(self, wx.ID_ANY, "Character:", wx.DefaultPosition, wx.DefaultSize, 0) self.m_staticCharText.Wrap(-1) - fgSizerInput.Add(self.m_staticCharText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + fgSizerInput.Add(self.m_staticCharText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) self.charChoice = wx.Choice(self, wx.ID_ANY, style=0) - fgSizerInput.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 5) + fgSizerInput.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 10) pmainSizer.Add(fgSizerInput, 0, wx.EXPAND, 5) - btnSizer = wx.BoxSizer(wx.HORIZONTAL) - pmainSizer.Add(btnSizer, 0, wx.EXPAND, 5) - + self.addButton = wx.Button(self, wx.ID_ANY, "Log In with EVE SSO", wx.DefaultPosition, wx.DefaultSize, 0) + self.addButton.Bind(wx.EVT_BUTTON, self.addCharacter) + pmainSizer.Add(self.addButton, 0, wx.ALL | wx.ALIGN_CENTER, 5) self.stStatus = wx.StaticText(self, wx.ID_ANY, wx.EmptyString) pmainSizer.Add(self.stStatus, 0, wx.ALL, 5) - + self.charEditor.mainFrame.Bind(GE.EVT_SSO_LOGOUT, self.ssoListChanged) + self.charEditor.mainFrame.Bind(GE.EVT_SSO_LOGIN, self.ssoListChanged) self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) self.charChoice.Bind(wx.EVT_CHOICE, self.ssoCharChanged) self.SetSizer(pmainSizer) self.Layout() - self.charChanged(None) + self.ssoListChanged(None) def ssoCharChanged(self, event): sChar = Character.getInstance() @@ -771,10 +779,30 @@ class APIView(wx.Panel): sChar.setSsoCharacter(activeChar.ID, self.getActiveCharacter()) event.Skip() + def addCharacter(self, event): + sCrest = Esi.getInstance() + uri = sCrest.startServer() + webbrowser.open(uri) + def getActiveCharacter(self): selection = self.charChoice.GetCurrentSelection() return self.charChoice.GetClientData(selection) if selection is not -1 else None + def ssoListChanged(self, event): + sEsi = Esi.getInstance() + ssoChars = sEsi.getSsoCharacters() + + if len(ssoChars) == 0: + self.charChoice.Hide() + self.m_staticCharText.Hide() + self.noCharactersTip.Show() + else: + self.noCharactersTip.Hide() + self.m_staticCharText.Show() + self.charChoice.Show() + + self.charChanged(event) + def charChanged(self, event): sChar = Character.getInstance() sEsi = Esi.getInstance() diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 763d722bf..25b5cfcb8 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -115,10 +115,7 @@ class CrestFittings(wx.Frame): self.statusbar.SetStatusText("Cached for %s" % sTime, 0) def ssoLogout(self, event): - if event.type == CrestModes.IMPLICIT: - self.Close() - else: - self.updateCharList() + self.updateCharList() event.Skip() # continue event def OnClose(self, event): @@ -307,7 +304,7 @@ class ExportToEve(wx.Frame): class CrestMgmt(wx.Dialog): def __init__(self, parent): - wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="CREST Character Management", pos=wx.DefaultPosition, + wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="SSO Character Management", pos=wx.DefaultPosition, size=wx.Size(550, 250), style=wx.DEFAULT_DIALOG_STYLE) self.mainFrame = parent mainSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -316,8 +313,6 @@ class CrestMgmt(wx.Dialog): self.lcCharacters.InsertColumn(0, heading='Character') self.lcCharacters.InsertColumn(1, heading='Character ID') - self.lcCharacters.InsertColumn(2, heading='Access Token') - self.lcCharacters.InsertColumn(3, heading='Refresh Token') self.popCharList() @@ -355,7 +350,7 @@ class CrestMgmt(wx.Dialog): for index, char in enumerate(chars): self.lcCharacters.InsertItem(index, char.characterName) - self.lcCharacters.SetStringItem(index, 1, char.refreshToken) + self.lcCharacters.SetItem(index, 1, str(char.characterID)) self.lcCharacters.SetItemData(index, char.ID) self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE) diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index e9e63b82b..c2c5e3ed9 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -138,7 +138,7 @@ class MainMenuBar(wx.MenuBar): # CREST Menu crestMenu = wx.Menu() - self.Append(crestMenu, "&CREST") + self.Append(crestMenu, "EVE &SSO") crestMenu.Append(self.ssoLoginId, "Manage Characters") crestMenu.Append(self.eveFittingsId, "Browse EVE Fittings") diff --git a/service/esi.py b/service/esi.py index c174021dd..fc761dfb8 100644 --- a/service/esi.py +++ b/service/esi.py @@ -104,6 +104,12 @@ class Esi(object): def delSsoCharacter(self, id): char = eos.db.getSsoCharacter(id, config.getClientSecret()) + + # There is an issue in which the SSO character is not removed from any linked characters - a reference to the + # sso character remains even though the SSO character is deleted which should have deleted the link. This is a + # work around until we can figure out why. Manually delete SSOCharacter from all of it's characters + for x in char.characters: + x._Character__ssoCharacters.remove(char) eos.db.remove(char) wx.PostEvent(self.mainFrame, GE.SsoLogout(charID=id)) From 570df7f6456341e6cbeee4c6a2a443e7708c8b3b Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Thu, 15 Mar 2018 22:13:56 -0400 Subject: [PATCH 26/39] Fix a bug with the character selection, and automatically assign a character after logging into eve via character editor --- gui/characterEditor.py | 7 +++++++ gui/characterSelection.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 3b89c7049..acac41dc2 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -808,6 +808,11 @@ class APIView(wx.Panel): sEsi = Esi.getInstance() activeChar = self.charEditor.entityEditor.getActiveEntity() + + if event and event.EventType == GE.EVT_SSO_LOGIN.typeId and hasattr(event, 'character'): + # Automatically assign the character that was just logged into + sChar.setSsoCharacter(activeChar.ID, event.character.ID) + sso = sChar.getSsoCharacter(activeChar.ID) ssoChars = sEsi.getSsoCharacters() @@ -823,6 +828,8 @@ class APIView(wx.Panel): self.charChoice.SetSelection(currId) if sso is None: self.charChoice.SetSelection(noneID) + + # # if chars: # for charName in chars: diff --git a/gui/characterSelection.py b/gui/characterSelection.py index 532edb379..ea4afff01 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -178,7 +178,7 @@ class CharacterSelection(wx.Panel): return char = sChar.getCharacter(charID) - if sChar.getCharName(charID) not in ("All 0", "All 5") and char.ssoCharacterID is not None: + if sChar.getCharName(charID) not in ("All 0", "All 5") and sChar.getSsoCharacter(char.ID) is not None: self.btnRefresh.Enable(True) else: self.btnRefresh.Enable(False) From 79deca41c16223358c7837ddde192b05768c646b Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Thu, 15 Mar 2018 23:05:28 -0400 Subject: [PATCH 27/39] Fix skill fetching --- gui/characterSelection.py | 22 +++++++++++++++------- service/character.py | 6 ++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/gui/characterSelection.py b/gui/characterSelection.py index ea4afff01..cc63a1f82 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -79,6 +79,7 @@ class CharacterSelection(wx.Panel): self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) self.SetMinSize(wx.Size(25, -1)) + self.toggleRefreshButton() self.charChoice.Enable(False) @@ -159,11 +160,11 @@ class CharacterSelection(wx.Panel): self.refreshCharacterList() else: exc_type, exc_obj, exc_trace = e - pyfalog.warn("Error fetching API information for character") + pyfalog.warn("Error fetching skill information for character") pyfalog.warn(exc_obj) wx.MessageBox( - "Error fetching API information, please check your API details in the character editor and try again later", + "Error fetching skill information", "Error", wx.ICON_ERROR | wx.STAY_ON_TOP) def charChanged(self, event): @@ -177,17 +178,22 @@ class CharacterSelection(wx.Panel): self.mainFrame.showCharacterEditor(event) return - char = sChar.getCharacter(charID) - if sChar.getCharName(charID) not in ("All 0", "All 5") and sChar.getSsoCharacter(char.ID) is not None: - self.btnRefresh.Enable(True) - else: - self.btnRefresh.Enable(False) + self.toggleRefreshButton() sFit = Fit.getInstance() sFit.changeChar(fitID, charID) self.charCache = self.charChoice.GetCurrentSelection() wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + def toggleRefreshButton(self): + charID = self.getActiveCharacter() + sChar = Character.getInstance() + char = sChar.getCharacter(charID) + if sChar.getCharName(charID) not in ("All 0", "All 5") and sChar.getSsoCharacter(char.ID) is not None: + self.btnRefresh.Enable(True) + else: + self.btnRefresh.Enable(False) + def selectChar(self, charID): choice = self.charChoice numItems = len(choice.GetItems()) @@ -244,6 +250,8 @@ class CharacterSelection(wx.Panel): if not fit.calculated: self.charChanged(None) + self.toggleRefreshButton() + event.Skip() def exportSkills(self, evt): diff --git a/service/character.py b/service/character.py index 6e5b07a0c..9a3727620 100644 --- a/service/character.py +++ b/service/character.py @@ -474,14 +474,16 @@ class UpdateAPIThread(threading.Thread): char = eos.db.getCharacter(self.charID) sEsi = Esi.getInstance() - resp = sEsi.getSkills(char.ssoCharacterID) + sChar = Character.getInstance() + ssoChar = sChar.getSsoCharacter(char.ID) + resp = sEsi.getSkills(ssoChar.ID) # todo: check if alpha. if so, pop up a question if they want to apply it as alpha. Use threading events to set the answer? char.clearSkills() for skillRow in resp["skills"]: char.addSkill(Skill(char, skillRow["skill_id"], skillRow["trained_skill_level"])) - resp = sEsi.getSecStatus(char.ssoCharacterID) + resp = sEsi.getSecStatus(ssoChar.ID) char.secStatus = resp['security_status'] From 2c3957b2db609c84ede7525a47a785fc6c47ee7e Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Thu, 15 Mar 2018 23:07:34 -0400 Subject: [PATCH 28/39] Disable CREST settings in preferences --- gui/builtinPreferenceViews/__init__.py | 4 +--- gui/preferenceView.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/gui/builtinPreferenceViews/__init__.py b/gui/builtinPreferenceViews/__init__.py index e6b55fb3c..ceec31971 100644 --- a/gui/builtinPreferenceViews/__init__.py +++ b/gui/builtinPreferenceViews/__init__.py @@ -6,7 +6,5 @@ __all__ = [ "pyfaDatabasePreferences", "pyfaLoggingPreferences", "pyfaEnginePreferences", - "pyfaStatViewPreferences", - "pyfaCrestPreferences" -] + "pyfaStatViewPreferences"] diff --git a/gui/preferenceView.py b/gui/preferenceView.py index a52a720e4..42ce5cd05 100644 --- a/gui/preferenceView.py +++ b/gui/preferenceView.py @@ -43,7 +43,7 @@ from gui.builtinPreferenceViews import ( # noqa: E402, F401 pyfaGeneralPreferences, pyfaNetworkPreferences, pyfaHTMLExportPreferences, - pyfaCrestPreferences, + # pyfaCrestPreferences, pyfaContextMenuPreferences, pyfaStatViewPreferences, pyfaUpdatePreferences, From a3f532f62f5fdedf5de2af2fb9dce12e8fda5c70 Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Fri, 16 Mar 2018 18:01:47 -0400 Subject: [PATCH 29/39] Remove some cruft, and show EVE fitting when selecting it, rather than double clicking --- gui/crestFittings.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 25b5cfcb8..44e488cd9 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -80,9 +80,6 @@ class CrestFittings(wx.Frame): self.statusbar.SetFieldsCount() self.SetStatusBar(self.statusbar) - self.cacheTimer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self.updateCacheStatus, self.cacheTimer) - self.SetSizer(mainSizer) self.Layout() @@ -105,15 +102,6 @@ class CrestFittings(wx.Frame): self.charChoice.SetSelection(0) - def updateCacheStatus(self, event): - t = time.gmtime(self.cacheTime - time.time()) - - if calendar.timegm(t) < 0: # calendar.timegm gets seconds until time given - self.cacheTimer.Stop() - else: - sTime = time.strftime("%H:%M:%S", t) - self.statusbar.SetStatusText("Cached for %s" % sTime, 0) - def ssoLogout(self, event): self.updateCharList() event.Skip() # continue event @@ -121,7 +109,7 @@ class CrestFittings(wx.Frame): def OnClose(self, event): self.mainFrame.Unbind(GE.EVT_SSO_LOGOUT) self.mainFrame.Unbind(GE.EVT_SSO_LOGIN) - self.cacheTimer.Stop() # must be manually stopped, otherwise crash. See https://github.com/wxWidgets/Phoenix/issues/632 + # self.cacheTimer.Stop() # must be manually stopped, otherwise crash. See https://github.com/wxWidgets/Phoenix/issues/632 event.Skip() def getActiveCharacter(self): @@ -383,7 +371,7 @@ class FittingsTreeView(wx.Panel): self.root = tree.AddRoot("Fits") self.populateSkillTree(None) - self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.displayFit) + self.Bind(wx.EVT_TREE_SEL_CHANGED, self.displayFit) self.SetSizer(pmainSizer) From 0db125177fd5d5e9448f20a56d0ed0c5e7e098a1 Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Fri, 16 Mar 2018 18:05:49 -0400 Subject: [PATCH 30/39] Remove some more cruft --- gui/crestFittings.py | 5 +---- gui/mainFrame.py | 17 ++++------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/gui/crestFittings.py b/gui/crestFittings.py index 44e488cd9..c5ee71029 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -236,10 +236,7 @@ class ExportToEve(wx.Frame): event.Skip() def ssoLogout(self, event): - if event.type == CrestModes.IMPLICIT: - self.Close() - else: - self.updateCharList() + self.updateCharList() event.Skip() # continue event def OnClose(self, event): diff --git a/gui/mainFrame.py b/gui/mainFrame.py index b36f9b4ce..44523ae5a 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -614,22 +614,13 @@ class MainFrame(wx.Frame): menu.Enable(menu.exportToEveId, True) def updateCrestMenus(self, type): - # in case we are logged in when switching, change title back - self.titleTimer.Stop() - self.SetTitle(self.title) - menu = self.GetMenuBar() sCrest = Esi.getInstance() - if type == CrestModes.IMPLICIT: - menu.SetLabel(menu.ssoLoginId, "Login to EVE") - menu.Enable(menu.eveFittingsId, False) - menu.Enable(menu.exportToEveId, False) - else: - menu.SetLabel(menu.ssoLoginId, "Manage Characters") - enable = len(sCrest.getSsoCharacters()) == 0 - menu.Enable(menu.eveFittingsId, not enable) - menu.Enable(menu.exportToEveId, not enable) + menu.SetLabel(menu.ssoLoginId, "Manage Characters") + enable = len(sCrest.getSsoCharacters()) == 0 + menu.Enable(menu.eveFittingsId, not enable) + menu.Enable(menu.exportToEveId, not enable) def ssoHandler(self, event): dlg = CrestMgmt(self) From 8a10f0a766547a6d075187290cf98317ea5c543f Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Fri, 16 Mar 2018 18:06:33 -0400 Subject: [PATCH 31/39] sCrest > sEsi --- .../pyfaCrestPreferences.py | 4 +-- gui/characterEditor.py | 4 +-- gui/crestFittings.py | 36 +++++++++---------- gui/mainFrame.py | 4 +-- gui/mainMenuBar.py | 4 +-- service/port.py | 2 +- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/gui/builtinPreferenceViews/pyfaCrestPreferences.py b/gui/builtinPreferenceViews/pyfaCrestPreferences.py index c94cc1328..598019dab 100644 --- a/gui/builtinPreferenceViews/pyfaCrestPreferences.py +++ b/gui/builtinPreferenceViews/pyfaCrestPreferences.py @@ -126,8 +126,8 @@ class PFCrestPref(PreferenceView): def OnBtnApply(self, event): self.settings.set('clientID', self.inputClientID.GetValue().strip()) self.settings.set('clientSecret', self.inputClientSecret.GetValue().strip()) - sCrest = Esi.getInstance() - sCrest.delAllCharacters() + sEsi = Esi.getInstance() + sEsi.delAllCharacters() def ToggleProxySettings(self, mode): if mode: diff --git a/gui/characterEditor.py b/gui/characterEditor.py index acac41dc2..65dcb408f 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -780,8 +780,8 @@ class APIView(wx.Panel): event.Skip() def addCharacter(self, event): - sCrest = Esi.getInstance() - uri = sCrest.startServer() + sEsi = Esi.getInstance() + uri = sEsi.startServer() webbrowser.open(uri) def getActiveCharacter(self): diff --git a/gui/crestFittings.py b/gui/crestFittings.py index c5ee71029..2984aa647 100644 --- a/gui/crestFittings.py +++ b/gui/crestFittings.py @@ -32,7 +32,7 @@ class CrestFittings(wx.Frame): self.mainFrame = parent mainSizer = wx.BoxSizer(wx.VERTICAL) - sCrest = Esi.getInstance() + sEsi = Esi.getInstance() characterSelectSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -90,8 +90,8 @@ class CrestFittings(wx.Frame): event.Skip() def updateCharList(self): - sCrest = Esi.getInstance() - chars = sCrest.getSsoCharacters() + sEsi = Esi.getInstance() + chars = sEsi.getSsoCharacters() if len(chars) == 0: self.Close() @@ -117,11 +117,11 @@ class CrestFittings(wx.Frame): return self.charChoice.GetClientData(selection) if selection is not None else None def fetchFittings(self, event): - sCrest = Esi.getInstance() + sEsi = Esi.getInstance() waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self) try: - fittings = sCrest.getFittings(self.getActiveCharacter()) + fittings = sEsi.getFittings(self.getActiveCharacter()) # self.cacheTime = fittings.get('cached_until') # self.updateCacheStatus(None) # self.cacheTimer.Start(1000) @@ -147,7 +147,7 @@ class CrestFittings(wx.Frame): self.mainFrame._openAfterImport(fits) def deleteFitting(self, event): - sCrest = Esi.getInstance() + sEsi = Esi.getInstance() selection = self.fitView.fitSelection if not selection: return @@ -159,7 +159,7 @@ class CrestFittings(wx.Frame): if dlg.ShowModal() == wx.ID_YES: try: - sCrest.delFitting(self.getActiveCharacter(), data['fitting_id']) + sEsi.delFitting(self.getActiveCharacter(), data['fitting_id']) except requests.exceptions.ConnectionError: msg = "Connection error, please check your internet connection" pyfalog.error(msg) @@ -188,7 +188,7 @@ class ExportToEve(wx.Frame): self.mainFrame = parent self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) - sCrest = Esi.getInstance() + sEsi = Esi.getInstance() mainSizer = wx.BoxSizer(wx.VERTICAL) hSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -219,8 +219,8 @@ class ExportToEve(wx.Frame): self.Centre(wx.BOTH) def updateCharList(self): - sCrest = Esi.getInstance() - chars = sCrest.getSsoCharacters() + sEsi = Esi.getInstance() + chars = sEsi.getSsoCharacters() if len(chars) == 0: self.Close() @@ -260,12 +260,12 @@ class ExportToEve(wx.Frame): return self.statusbar.SetStatusText("Sending request and awaiting response", 1) - sCrest = Esi.getInstance() + sEsi = Esi.getInstance() try: sFit = Fit.getInstance() data = sPort.exportESI(sFit.getFit(fitID)) - res = sCrest.postFitting(self.getActiveCharacter(), data) + res = sEsi.postFitting(self.getActiveCharacter(), data) self.statusbar.SetStatusText("", 0) self.statusbar.SetStatusText("", 1) @@ -328,8 +328,8 @@ class CrestMgmt(wx.Dialog): event.Skip() def popCharList(self): - sCrest = Esi.getInstance() - chars = sCrest.getSsoCharacters() + sEsi = Esi.getInstance() + chars = sEsi.getSsoCharacters() self.lcCharacters.DeleteAllItems() @@ -343,16 +343,16 @@ class CrestMgmt(wx.Dialog): @staticmethod def addChar(event): - sCrest = Esi.getInstance() - uri = sCrest.startServer() + sEsi = Esi.getInstance() + uri = sEsi.startServer() webbrowser.open(uri) def delChar(self, event): item = self.lcCharacters.GetFirstSelected() if item > -1: charID = self.lcCharacters.GetItemData(item) - sCrest = Esi.getInstance() - sCrest.delSsoCharacter(charID) + sEsi = Esi.getInstance() + sEsi.delSsoCharacter(charID) self.popCharList() diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 44523ae5a..7824ea4b1 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -615,10 +615,10 @@ class MainFrame(wx.Frame): def updateCrestMenus(self, type): menu = self.GetMenuBar() - sCrest = Esi.getInstance() + sEsi = Esi.getInstance() menu.SetLabel(menu.ssoLoginId, "Manage Characters") - enable = len(sCrest.getSsoCharacters()) == 0 + enable = len(sEsi.getSsoCharacters()) == 0 menu.Enable(menu.eveFittingsId, not enable) menu.Enable(menu.exportToEveId, not enable) diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index c2c5e3ed9..c34091c03 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -134,7 +134,7 @@ class MainMenuBar(wx.MenuBar): preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui")) windowMenu.Append(preferencesItem) - # self.sCrest = Crest.getInstance() + # self.sEsi = Crest.getInstance() # CREST Menu crestMenu = wx.Menu() @@ -144,7 +144,7 @@ class MainMenuBar(wx.MenuBar): crestMenu.Append(self.eveFittingsId, "Browse EVE Fittings") crestMenu.Append(self.exportToEveId, "Export To EVE") - # if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0: + # if self.sEsi.settings.get('mode') == CrestModes.IMPLICIT or len(self.sEsi.getCrestCharacters()) == 0: self.Enable(self.eveFittingsId, True) self.Enable(self.exportToEveId, True) diff --git a/service/port.py b/service/port.py index ec15fdf34..5ea30d0a8 100644 --- a/service/port.py +++ b/service/port.py @@ -349,7 +349,7 @@ class Port(object): nested_dict = lambda: collections.defaultdict(nested_dict) fit = nested_dict() - sCrest = Esi.getInstance() + sEsi = Esi.getInstance() sFit = svcFit.getInstance() # max length is 50 characters From 3ec01a20c2f636431f4989f277363ed7583f17d0 Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Fri, 16 Mar 2018 18:08:01 -0400 Subject: [PATCH 32/39] Bunch of renaming to bring variables up to speed --- gui/copySelectDialog.py | 4 ++-- gui/{crestFittings.py => esiFittings.py} | 4 ++-- gui/mainFrame.py | 12 ++++++------ gui/mainMenuBar.py | 10 +++++----- service/esi.py | 9 +-------- service/network.py | 2 +- service/port.py | 2 +- service/server.py | 3 +-- 8 files changed, 19 insertions(+), 27 deletions(-) rename gui/{crestFittings.py => esiFittings.py} (99%) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index d2e3a656c..5675c835e 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -27,7 +27,7 @@ class CopySelectDialog(wx.Dialog): copyFormatEftImps = 1 copyFormatXml = 2 copyFormatDna = 3 - copyFormatCrest = 4 + copyFormatEsi = 4 copyFormatMultiBuy = 5 def __init__(self, parent): @@ -40,7 +40,7 @@ class CopySelectDialog(wx.Dialog): CopySelectDialog.copyFormatEftImps: "EFT text format", CopySelectDialog.copyFormatXml: "EVE native XML format", CopySelectDialog.copyFormatDna: "A one-line text format", - CopySelectDialog.copyFormatCrest: "A JSON format used for EVE CREST", + CopySelectDialog.copyFormatEsi: "A JSON format used for EVE CREST", CopySelectDialog.copyFormatMultiBuy: "MultiBuy text format"} selector = wx.RadioBox(self, wx.ID_ANY, label="Copy to the clipboard using:", choices=copyFormats, style=wx.RA_SPECIFY_ROWS) diff --git a/gui/crestFittings.py b/gui/esiFittings.py similarity index 99% rename from gui/crestFittings.py rename to gui/esiFittings.py index 2984aa647..2d8c5f9fa 100644 --- a/gui/crestFittings.py +++ b/gui/esiFittings.py @@ -23,7 +23,7 @@ from service.port import ESIExportException pyfalog = Logger(__name__) -class CrestFittings(wx.Frame): +class EveFittings(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="Browse EVE Fittings", pos=wx.DefaultPosition, size=wx.Size(550, 450), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL) @@ -287,7 +287,7 @@ class ExportToEve(wx.Frame): ESIExceptionHandler(self, ex) -class CrestMgmt(wx.Dialog): +class SsoCharacterMgmt(wx.Dialog): def __init__(self, parent): wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="SSO Character Management", pos=wx.DefaultPosition, size=wx.Size(550, 250), style=wx.DEFAULT_DIALOG_STYLE) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 7824ea4b1..2c8adaf90 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -83,7 +83,7 @@ import webbrowser import wx.adv from service.esi import Esi -from gui.crestFittings import CrestFittings, ExportToEve, CrestMgmt +from gui.esiFittings import EveFittings, ExportToEve, SsoCharacterMgmt disableOverrideEditor = False @@ -605,7 +605,7 @@ class MainFrame(wx.Frame): wx.PostEvent(self, GE.FitChanged(fitID=fitID)) def eveFittings(self, event): - dlg = CrestFittings(self) + dlg = EveFittings(self) dlg.Show() def onSSOLogin(self, event): @@ -613,7 +613,7 @@ class MainFrame(wx.Frame): menu.Enable(menu.eveFittingsId, True) menu.Enable(menu.exportToEveId, True) - def updateCrestMenus(self, type): + def updateEsiMenus(self, type): menu = self.GetMenuBar() sEsi = Esi.getInstance() @@ -623,7 +623,7 @@ class MainFrame(wx.Frame): menu.Enable(menu.exportToEveId, not enable) def ssoHandler(self, event): - dlg = CrestMgmt(self) + dlg = SsoCharacterMgmt(self) dlg.Show() def exportToEve(self, event): @@ -699,7 +699,7 @@ class MainFrame(wx.Frame): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportDna(fit)) - def clipboardCrest(self): + def clipboardEsi(self): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportESI(fit)) @@ -725,7 +725,7 @@ class MainFrame(wx.Frame): CopySelectDialog.copyFormatEftImps: self.clipboardEftImps, CopySelectDialog.copyFormatXml: self.clipboardXml, CopySelectDialog.copyFormatDna: self.clipboardDna, - CopySelectDialog.copyFormatCrest: self.clipboardCrest, + CopySelectDialog.copyFormatEsi: self.clipboardEsi, CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy} dlg = CopySelectDialog(self) dlg.ShowModal() diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index c34091c03..9c421f779 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -137,12 +137,12 @@ class MainMenuBar(wx.MenuBar): # self.sEsi = Crest.getInstance() # CREST Menu - crestMenu = wx.Menu() - self.Append(crestMenu, "EVE &SSO") + esiMMenu = wx.Menu() + self.Append(esiMMenu, "EVE &SSO") - crestMenu.Append(self.ssoLoginId, "Manage Characters") - crestMenu.Append(self.eveFittingsId, "Browse EVE Fittings") - crestMenu.Append(self.exportToEveId, "Export To EVE") + esiMMenu.Append(self.ssoLoginId, "Manage Characters") + esiMMenu.Append(self.eveFittingsId, "Browse EVE Fittings") + esiMMenu.Append(self.exportToEveId, "Export To EVE") # if self.sEsi.settings.get('mode') == CrestModes.IMPLICIT or len(self.sEsi.getCrestCharacters()) == 0: self.Enable(self.eveFittingsId, True) diff --git a/service/esi.py b/service/esi.py index fc761dfb8..10b4f0def 100644 --- a/service/esi.py +++ b/service/esi.py @@ -15,7 +15,6 @@ import datetime from eos.enum import Enum from eos.saveddata.ssocharacter import SsoCharacter import gui.globalEvents as GE -from service.settings import CRESTSettings from service.server import StoppableHTTPServer, AuthHandler from .esi_security_proxy import EsiSecurityProxy @@ -77,8 +76,6 @@ class Esi(object): AFTER_TOKEN_REFRESH.add_receiver(self.tokenUpdate) - self.settings = CRESTSettings.getInstance() - # these will be set when needed self.httpd = None self.state = None @@ -98,10 +95,6 @@ class Esi(object): print(kwargs) pass - @property - def isTestServer(self): - return self.settings.get('server') == Servers.SISI - def delSsoCharacter(self, id): char = eos.db.getSsoCharacter(id, config.getClientSecret()) @@ -222,7 +215,7 @@ class Esi(object): pyfalog.warn("OAUTH state mismatch") raise Exception("OAUTH State Mismatch.") - pyfalog.debug("Handling CREST login with: {0}", message) + pyfalog.debug("Handling SSO login with: {0}", message) auth_response = json.loads(base64.b64decode(message['SSOInfo'][0])) diff --git a/service/network.py b/service/network.py index 3b65b983a..c1eeb7d15 100644 --- a/service/network.py +++ b/service/network.py @@ -57,7 +57,7 @@ class Network(object): # Request constants - every request must supply this, as it is checked if # enabled or not via settings ENABLED = 1 - EVE = 2 # Mostly API, but also covers CREST requests + EVE = 2 # Mostly API, but also covers CREST requests. update: might be useless these days, this Network class needs to be reviewed PRICES = 4 UPDATE = 8 diff --git a/service/port.py b/service/port.py index 5ea30d0a8..7f58185be 100644 --- a/service/port.py +++ b/service/port.py @@ -474,7 +474,7 @@ class Port(object): except ValueError: fitobj.ship = Citadel(sMkt.getItem(ship)) except: - pyfalog.warning("Caught exception in importCrest") + pyfalog.warning("Caught exception in importESI") return None items.sort(key=lambda k: k['flag']) diff --git a/service/server.py b/service/server.py index 48e386451..07da471b8 100644 --- a/service/server.py +++ b/service/server.py @@ -4,7 +4,6 @@ import socket import threading from logbook import Logger -from service.settings import CRESTSettings pyfalog = Logger(__name__) # noinspection PyPep8 @@ -83,7 +82,7 @@ class AuthHandler(http.server.BaseHTTPRequestHandler): if step2: self.server.callback(parts) pyfalog.info("Successfully logged into EVE.") - msg = "If you see this message then it means you should be logged into CREST. You may close this window and return to the application." + msg = "If you see this message then it means you should be logged into EVE SSO. You may close this window and return to the application." else: # For implicit mode, we have to serve up the page which will take the hash and redirect using a querystring pyfalog.info("Processing response from EVE Online.") From 00e8e9d84a2df37f044ed296500e7d58b5e8b192 Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Sat, 17 Mar 2018 20:18:28 -0400 Subject: [PATCH 33/39] Send pyfa version to server when logging in --- service/esi.py | 2 +- service/esi_security_proxy.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/service/esi.py b/service/esi.py index 10b4f0def..f76f288d5 100644 --- a/service/esi.py +++ b/service/esi.py @@ -198,7 +198,7 @@ class Esi(object): esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) - uri = esisecurity.get_auth_uri(state=self.state, redirect='http://localhost:{}'.format(port)) + uri = esisecurity.get_auth_uri(state=self.state, redirect='http://localhost:{}'.format(port), pyfa_version=config.version) self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleLogin,)) self.serverThread.name = "SsoCallbackServer" diff --git a/service/esi_security_proxy.py b/service/esi_security_proxy.py index 21d0f8510..7c29f80b0 100644 --- a/service/esi_security_proxy.py +++ b/service/esi_security_proxy.py @@ -12,6 +12,7 @@ import time from requests import Session from requests.utils import quote from six.moves.urllib.parse import urlparse +from urllib.parse import urlencode from esipy.events import AFTER_TOKEN_REFRESH from esipy.exceptions import APIException @@ -115,7 +116,7 @@ class EsiSecurityProxy(object): return request_params - def get_auth_uri(self, state=None, redirect='http://localhost:8080'): + def get_auth_uri(self, *args, **kwargs): """ Constructs the full auth uri and returns it. :param state: The state to pass through the auth process @@ -123,10 +124,9 @@ class EsiSecurityProxy(object): :return: the authorizationUrl with the correct parameters. """ - return '%s?redirect=%s%s' % ( + return '%s?%s' % ( self.oauth_authorize, - quote(redirect, safe=''), - '&state=%s' % state if state else '' + urlencode(kwargs) ) def get_refresh_token_params(self): From 033647da61f5fd97f340fa255946140d502ed224 Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Sat, 17 Mar 2018 20:23:14 -0400 Subject: [PATCH 34/39] Move to a central login() method --- gui/characterEditor.py | 3 +-- gui/esiFittings.py | 3 +-- gui/globalEvents.py | 1 + service/esi.py | 7 +++++++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 65dcb408f..c7a2fef8c 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -781,8 +781,7 @@ class APIView(wx.Panel): def addCharacter(self, event): sEsi = Esi.getInstance() - uri = sEsi.startServer() - webbrowser.open(uri) + sEsi.login() def getActiveCharacter(self): selection = self.charChoice.GetCurrentSelection() diff --git a/gui/esiFittings.py b/gui/esiFittings.py index 2d8c5f9fa..31fdd5a71 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -344,8 +344,7 @@ class SsoCharacterMgmt(wx.Dialog): @staticmethod def addChar(event): sEsi = Esi.getInstance() - uri = sEsi.startServer() - webbrowser.open(uri) + sEsi.login() def delChar(self, event): item = self.lcCharacters.GetFirstSelected() diff --git a/gui/globalEvents.py b/gui/globalEvents.py index 1c1cc7e38..53784fcb6 100644 --- a/gui/globalEvents.py +++ b/gui/globalEvents.py @@ -5,5 +5,6 @@ FitChanged, FIT_CHANGED = wx.lib.newevent.NewEvent() CharListUpdated, CHAR_LIST_UPDATED = wx.lib.newevent.NewEvent() CharChanged, CHAR_CHANGED = wx.lib.newevent.NewEvent() +SsoLoggingIn, EVT_SSO_LOGGING_IN = wx.lib.newevent.NewEvent() SsoLogin, EVT_SSO_LOGIN = wx.lib.newevent.NewEvent() SsoLogout, EVT_SSO_LOGOUT = wx.lib.newevent.NewEvent() diff --git a/service/esi.py b/service/esi.py index f76f288d5..cd7879575 100644 --- a/service/esi.py +++ b/service/esi.py @@ -9,6 +9,7 @@ import base64 import json import os import config +import webbrowser import eos.db import datetime @@ -179,6 +180,12 @@ class Esi(object): if char.esi_client is not None: char.esi_client.security.update_token(tokenResponse) + def login(self): + # Switch off how we do things here depending on the mode of authentication + uri = self.startServer() + webbrowser.open(uri) + wx.PostEvent(self.mainFrame, GE.SsoLoggingIn()) + def stopServer(self): pyfalog.debug("Stopping Server") self.httpd.stop() From 199763bcca679e2bf68ff99ef5d6d397737be4fa Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Sat, 17 Mar 2018 20:31:38 -0400 Subject: [PATCH 35/39] Separate the server login handler from the sso info handler --- service/esi.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/service/esi.py b/service/esi.py index cd7879575..3dbd6496c 100644 --- a/service/esi.py +++ b/service/esi.py @@ -207,24 +207,15 @@ class Esi(object): uri = esisecurity.get_auth_uri(state=self.state, redirect='http://localhost:{}'.format(port), pyfa_version=config.version) - self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleLogin,)) + self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerLogin,)) self.serverThread.name = "SsoCallbackServer" self.serverThread.daemon = True self.serverThread.start() return uri - def handleLogin(self, message): - if not message: - raise Exception("Could not parse out querystring parameters.") - - if message['state'][0] != self.state: - pyfalog.warn("OAUTH state mismatch") - raise Exception("OAUTH State Mismatch.") - - pyfalog.debug("Handling SSO login with: {0}", message) - - auth_response = json.loads(base64.b64decode(message['SSOInfo'][0])) + def handleLogin(self, ssoInfo): + auth_response = json.loads(base64.b64decode(ssoInfo)) # We need to preload the ESI Security object beforehand with the auth response so that we can use verify to # get character information @@ -246,5 +237,16 @@ class Esi(object): Esi.update_token(currentCharacter, auth_response) # this also sets the esi security token eos.db.save(currentCharacter) - wx.PostEvent(self.mainFrame, GE.SsoLogin(character = currentCharacter)) + wx.PostEvent(self.mainFrame, GE.SsoLogin(character=currentCharacter)) + def handleServerLogin(self, message): + if not message: + raise Exception("Could not parse out querystring parameters.") + + if message['state'][0] != self.state: + pyfalog.warn("OAUTH state mismatch") + raise Exception("OAUTH State Mismatch.") + + pyfalog.debug("Handling SSO login with: {0}", message) + + self.handleLogin(message['SSOInfo'][0]) From 49181dce2be2a75614d0f606410ac022268f05ef Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Sat, 17 Mar 2018 21:08:11 -0400 Subject: [PATCH 36/39] Develop dialog to input SSO Info from pyfa.io, and handle the login. Still need to hook this up to a settings option --- gui/mainFrame.py | 10 ++++++++++ gui/ssoLogin.py | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 gui/ssoLogin.py diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 2c8adaf90..ffa0364b1 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -57,6 +57,7 @@ from gui.setEditor import ImplantSetEditorDlg from gui.devTools import DevTools from gui.preferenceDialog import PreferenceDialog from gui.graphFrame import GraphFrame +from gui.ssoLogin import SsoLogin from gui.copySelectDialog import CopySelectDialog from gui.utils.clipboard import toClipboard, fromClipboard from gui.updateDialog import UpdateDialog @@ -237,6 +238,15 @@ class MainFrame(wx.Frame): self.sUpdate.CheckUpdate(self.ShowUpdateBox) self.Bind(GE.EVT_SSO_LOGIN, self.onSSOLogin) + self.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin) + + def ShowSsoLogin(self, event): + dlg = SsoLogin(self) + if dlg.ShowModal() == wx.ID_OK: + sEsi = Esi.getInstance() + # todo: verify that this is a correct SSO Info block + sEsi.handleLogin(dlg.ssoInfoCtrl.Value.strip()) + def ShowUpdateBox(self, release, version): dlg = UpdateDialog(self, release, version) diff --git a/gui/ssoLogin.py b/gui/ssoLogin.py new file mode 100644 index 000000000..2ff364752 --- /dev/null +++ b/gui/ssoLogin.py @@ -0,0 +1,25 @@ +import wx + +class SsoLogin(wx.Dialog): + def __init__(self, parent): + wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="SSO Login", size=wx.Size(400, 240)) + + bSizer1 = wx.BoxSizer(wx.VERTICAL) + + text = wx.StaticText(self, wx.ID_ANY, "Copy and paste the block of text provided by pyfa.io, then click OK") + bSizer1.Add(text, 0, wx.ALL | wx.EXPAND, 10) + + self.ssoInfoCtrl = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, (-1, -1), style=wx.TE_MULTILINE) + self.ssoInfoCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL)) + self.ssoInfoCtrl.Layout() + + bSizer1.Add(self.ssoInfoCtrl, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10) + + bSizer3 = wx.BoxSizer(wx.VERTICAL) + bSizer3.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.BOTTOM | wx.EXPAND, 10) + + bSizer3.Add(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL), 0, wx.EXPAND) + bSizer1.Add(bSizer3, 0, wx.ALL | wx.EXPAND, 10) + + self.SetSizer(bSizer1) + self.Center() From 53451dfaf680cd657abe501b2f690d0ba5d41136 Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Sun, 18 Mar 2018 00:46:53 -0400 Subject: [PATCH 37/39] Implement a setting for manual vs server login method --- gui/builtinPreferenceViews/__init__.py | 1 + .../pyfaCrestPreferences.py | 148 ------------------ .../pyfaEsiPreferences.py | 137 ++++++++++++++++ gui/mainFrame.py | 14 +- gui/preferenceView.py | 2 +- service/esi.py | 42 +++-- service/settings.py | 22 +-- 7 files changed, 187 insertions(+), 179 deletions(-) delete mode 100644 gui/builtinPreferenceViews/pyfaCrestPreferences.py create mode 100644 gui/builtinPreferenceViews/pyfaEsiPreferences.py diff --git a/gui/builtinPreferenceViews/__init__.py b/gui/builtinPreferenceViews/__init__.py index ceec31971..32117a9ec 100644 --- a/gui/builtinPreferenceViews/__init__.py +++ b/gui/builtinPreferenceViews/__init__.py @@ -6,5 +6,6 @@ __all__ = [ "pyfaDatabasePreferences", "pyfaLoggingPreferences", "pyfaEnginePreferences", + "pyfaEsiPreferences", "pyfaStatViewPreferences"] diff --git a/gui/builtinPreferenceViews/pyfaCrestPreferences.py b/gui/builtinPreferenceViews/pyfaCrestPreferences.py deleted file mode 100644 index 598019dab..000000000 --- a/gui/builtinPreferenceViews/pyfaCrestPreferences.py +++ /dev/null @@ -1,148 +0,0 @@ -# noinspection PyPackageRequirements -import wx - -from gui.preferenceView import PreferenceView -from gui.bitmap_loader import BitmapLoader - -import gui.mainFrame - -from service.settings import CRESTSettings - -# noinspection PyPackageRequirements -from wx.lib.intctrl import IntCtrl - -from service.esi import Esi - - -class PFCrestPref(PreferenceView): - title = "CREST" - - def populatePanel(self, panel): - - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - self.settings = CRESTSettings.getInstance() - self.dirtySettings = False - dlgWidth = panel.GetParent().GetParent().ClientSize.width - mainSizer = wx.BoxSizer(wx.VERTICAL) - - self.stTitle = wx.StaticText(panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0) - self.stTitle.Wrap(-1) - self.stTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString)) - - mainSizer.Add(self.stTitle, 0, wx.ALL, 5) - - self.m_staticline1 = wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) - mainSizer.Add(self.m_staticline1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - self.stInfo = wx.StaticText(panel, wx.ID_ANY, - "Please see the pyfa wiki on GitHub for information regarding these options.", - wx.DefaultPosition, wx.DefaultSize, 0) - self.stInfo.Wrap(dlgWidth - 50) - mainSizer.Add(self.stInfo, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - rbSizer = wx.BoxSizer(wx.HORIZONTAL) - self.rbMode = wx.RadioBox(panel, -1, "Mode", wx.DefaultPosition, wx.DefaultSize, - ['Implicit', 'User-supplied details'], 1, wx.RA_SPECIFY_COLS) - self.rbServer = wx.RadioBox(panel, -1, "Server", wx.DefaultPosition, wx.DefaultSize, - ['Tranquility', 'Singularity'], 1, wx.RA_SPECIFY_COLS) - - self.rbMode.SetSelection(self.settings.get('mode')) - self.rbServer.SetSelection(self.settings.get('server')) - - rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5) - rbSizer.Add(self.rbServer, 1, wx.ALL, 5) - - self.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange) - self.rbServer.Bind(wx.EVT_RADIOBOX, self.OnServerChange) - - mainSizer.Add(rbSizer, 1, wx.ALL | wx.EXPAND, 0) - - timeoutSizer = wx.BoxSizer(wx.HORIZONTAL) - - self.stTimout = wx.StaticText(panel, wx.ID_ANY, "Timeout (seconds):", wx.DefaultPosition, wx.DefaultSize, 0) - self.stTimout.Wrap(-1) - - timeoutSizer.Add(self.stTimout, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) - - self.intTimeout = IntCtrl(panel, max=300000, limited=True, value=self.settings.get('timeout')) - timeoutSizer.Add(self.intTimeout, 0, wx.ALL, 5) - self.intTimeout.Bind(wx.lib.intctrl.EVT_INT, self.OnTimeoutChange) - - mainSizer.Add(timeoutSizer, 0, wx.ALL | wx.EXPAND, 0) - - detailsTitle = wx.StaticText(panel, wx.ID_ANY, "CREST client details", wx.DefaultPosition, wx.DefaultSize, 0) - detailsTitle.Wrap(-1) - detailsTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString)) - - mainSizer.Add(detailsTitle, 0, wx.ALL, 5) - mainSizer.Add(wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, - wx.EXPAND, 5) - - fgAddrSizer = wx.FlexGridSizer(2, 2, 0, 0) - fgAddrSizer.AddGrowableCol(1) - fgAddrSizer.SetFlexibleDirection(wx.BOTH) - fgAddrSizer.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) - - self.stSetID = wx.StaticText(panel, wx.ID_ANY, "Client ID:", wx.DefaultPosition, wx.DefaultSize, 0) - self.stSetID.Wrap(-1) - fgAddrSizer.Add(self.stSetID, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) - - self.inputClientID = wx.TextCtrl(panel, wx.ID_ANY, self.settings.get('clientID'), wx.DefaultPosition, - wx.DefaultSize, 0) - - fgAddrSizer.Add(self.inputClientID, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) - - self.stSetSecret = wx.StaticText(panel, wx.ID_ANY, "Client Secret:", wx.DefaultPosition, wx.DefaultSize, 0) - self.stSetSecret.Wrap(-1) - - fgAddrSizer.Add(self.stSetSecret, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) - - self.inputClientSecret = wx.TextCtrl(panel, wx.ID_ANY, self.settings.get('clientSecret'), wx.DefaultPosition, - wx.DefaultSize, 0) - - fgAddrSizer.Add(self.inputClientSecret, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) - - self.btnApply = wx.Button(panel, wx.ID_ANY, "Save Client Settings", wx.DefaultPosition, wx.DefaultSize, 0) - self.btnApply.Bind(wx.EVT_BUTTON, self.OnBtnApply) - - mainSizer.Add(fgAddrSizer, 0, wx.EXPAND, 5) - mainSizer.Add(self.btnApply, 0, wx.ALIGN_RIGHT, 5) - - self.ToggleProxySettings(self.settings.get('mode')) - - panel.SetSizer(mainSizer) - panel.Layout() - - def OnTimeoutChange(self, event): - self.settings.set('timeout', event.GetEventObject().GetValue()) - - def OnModeChange(self, event): - self.settings.set('mode', event.GetInt()) - self.ToggleProxySettings(self.settings.get('mode')) - - def OnServerChange(self, event): - self.settings.set('server', event.GetInt()) - - def OnBtnApply(self, event): - self.settings.set('clientID', self.inputClientID.GetValue().strip()) - self.settings.set('clientSecret', self.inputClientSecret.GetValue().strip()) - sEsi = Esi.getInstance() - sEsi.delAllCharacters() - - def ToggleProxySettings(self, mode): - if mode: - self.stSetID.Enable() - self.inputClientID.Enable() - self.stSetSecret.Enable() - self.inputClientSecret.Enable() - else: - self.stSetID.Disable() - self.inputClientID.Disable() - self.stSetSecret.Disable() - self.inputClientSecret.Disable() - - def getImage(self): - return BitmapLoader.getBitmap("eve", "gui") - - -PFCrestPref.register() diff --git a/gui/builtinPreferenceViews/pyfaEsiPreferences.py b/gui/builtinPreferenceViews/pyfaEsiPreferences.py new file mode 100644 index 000000000..029003a83 --- /dev/null +++ b/gui/builtinPreferenceViews/pyfaEsiPreferences.py @@ -0,0 +1,137 @@ +# noinspection PyPackageRequirements +import wx + +from gui.preferenceView import PreferenceView +from gui.bitmap_loader import BitmapLoader + +import gui.mainFrame + +from service.settings import EsiSettings + +# noinspection PyPackageRequirements +from wx.lib.intctrl import IntCtrl + +from service.esi import Esi + + +class PFEsiPref(PreferenceView): + title = "EVE SSO" + + def populatePanel(self, panel): + + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.settings = EsiSettings.getInstance() + self.dirtySettings = False + dlgWidth = panel.GetParent().GetParent().ClientSize.width + mainSizer = wx.BoxSizer(wx.VERTICAL) + + self.stTitle = wx.StaticText(panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0) + self.stTitle.Wrap(-1) + self.stTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString)) + + mainSizer.Add(self.stTitle, 0, wx.ALL, 5) + + self.m_staticline1 = wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) + mainSizer.Add(self.m_staticline1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) + + self.stInfo = wx.StaticText(panel, wx.ID_ANY, + "Please see the pyfa wiki on GitHub for information regarding these options.", + wx.DefaultPosition, wx.DefaultSize, 0) + self.stInfo.Wrap(dlgWidth - 50) + mainSizer.Add(self.stInfo, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) + + rbSizer = wx.BoxSizer(wx.HORIZONTAL) + self.rbMode = wx.RadioBox(panel, -1, "Login Authentication Method", wx.DefaultPosition, wx.DefaultSize, + ['Local Server', 'Manual'], 1, wx.RA_SPECIFY_COLS) + self.rbMode.SetItemToolTip(0, "This options starts a local webserver that the web application will call back to with information about the character login.") + self.rbMode.SetItemToolTip(1, "This option prompts users to copy and paste information from the web application to allow for character login. Use this if having issues with the local server.") + # self.rbServer = wx.RadioBox(panel, -1, "Server", wx.DefaultPosition, wx.DefaultSize, + # ['Tranquility', 'Singularity'], 1, wx.RA_SPECIFY_COLS) + + self.rbMode.SetSelection(self.settings.get('loginMode')) + # self.rbServer.SetSelection(self.settings.get('server')) + + rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5) + # rbSizer.Add(self.rbServer, 1, wx.ALL, 5) + + self.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange) + # self.rbServer.Bind(wx.EVT_RADIOBOX, self.OnServerChange) + + mainSizer.Add(rbSizer, 1, wx.ALL | wx.EXPAND, 0) + + timeoutSizer = wx.BoxSizer(wx.HORIZONTAL) + + # self.stTimout = wx.StaticText(panel, wx.ID_ANY, "Timeout (seconds):", wx.DefaultPosition, wx.DefaultSize, 0) + # self.stTimout.Wrap(-1) + # + # timeoutSizer.Add(self.stTimout, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + # self.intTimeout = IntCtrl(panel, max=300000, limited=True, value=self.settings.get('timeout')) + # timeoutSizer.Add(self.intTimeout, 0, wx.ALL, 5) + # self.intTimeout.Bind(wx.lib.intctrl.EVT_INT, self.OnTimeoutChange) + # + # mainSizer.Add(timeoutSizer, 0, wx.ALL | wx.EXPAND, 0) + + # detailsTitle = wx.StaticText(panel, wx.ID_ANY, "CREST client details", wx.DefaultPosition, wx.DefaultSize, 0) + # detailsTitle.Wrap(-1) + # detailsTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString)) + # + # mainSizer.Add(detailsTitle, 0, wx.ALL, 5) + # mainSizer.Add(wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, + # wx.EXPAND, 5) + + # fgAddrSizer = wx.FlexGridSizer(2, 2, 0, 0) + # fgAddrSizer.AddGrowableCol(1) + # fgAddrSizer.SetFlexibleDirection(wx.BOTH) + # fgAddrSizer.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) + # + # self.stSetID = wx.StaticText(panel, wx.ID_ANY, "Client ID:", wx.DefaultPosition, wx.DefaultSize, 0) + # self.stSetID.Wrap(-1) + # fgAddrSizer.Add(self.stSetID, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + # + # self.inputClientID = wx.TextCtrl(panel, wx.ID_ANY, self.settings.get('clientID'), wx.DefaultPosition, + # wx.DefaultSize, 0) + # + # fgAddrSizer.Add(self.inputClientID, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) + # + # self.stSetSecret = wx.StaticText(panel, wx.ID_ANY, "Client Secret:", wx.DefaultPosition, wx.DefaultSize, 0) + # self.stSetSecret.Wrap(-1) + # + # fgAddrSizer.Add(self.stSetSecret, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + # + # self.inputClientSecret = wx.TextCtrl(panel, wx.ID_ANY, self.settings.get('clientSecret'), wx.DefaultPosition, + # wx.DefaultSize, 0) + # + # fgAddrSizer.Add(self.inputClientSecret, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) + # + # self.btnApply = wx.Button(panel, wx.ID_ANY, "Save Client Settings", wx.DefaultPosition, wx.DefaultSize, 0) + # self.btnApply.Bind(wx.EVT_BUTTON, self.OnBtnApply) + # + # mainSizer.Add(fgAddrSizer, 0, wx.EXPAND, 5) + # mainSizer.Add(self.btnApply, 0, wx.ALIGN_RIGHT, 5) + + # self.ToggleProxySettings(self.settings.get('loginMode')) + + panel.SetSizer(mainSizer) + panel.Layout() + + def OnTimeoutChange(self, event): + self.settings.set('timeout', event.GetEventObject().GetValue()) + + def OnModeChange(self, event): + self.settings.set('loginMode', event.GetInt()) + + def OnServerChange(self, event): + self.settings.set('server', event.GetInt()) + + def OnBtnApply(self, event): + self.settings.set('clientID', self.inputClientID.GetValue().strip()) + self.settings.set('clientSecret', self.inputClientSecret.GetValue().strip()) + sEsi = Esi.getInstance() + sEsi.delAllCharacters() + + def getImage(self): + return BitmapLoader.getBitmap("eve", "gui") + + +PFEsiPref.register() diff --git a/gui/mainFrame.py b/gui/mainFrame.py index ffa0364b1..c9e5fa09c 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -83,7 +83,7 @@ import threading import webbrowser import wx.adv -from service.esi import Esi +from service.esi import Esi, LoginMethod from gui.esiFittings import EveFittings, ExportToEve, SsoCharacterMgmt disableOverrideEditor = False @@ -241,12 +241,12 @@ class MainFrame(wx.Frame): self.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin) def ShowSsoLogin(self, event): - dlg = SsoLogin(self) - if dlg.ShowModal() == wx.ID_OK: - sEsi = Esi.getInstance() - # todo: verify that this is a correct SSO Info block - sEsi.handleLogin(dlg.ssoInfoCtrl.Value.strip()) - + if getattr(event, "login_mode", LoginMethod.SERVER) == LoginMethod.MANUAL: + dlg = SsoLogin(self) + if dlg.ShowModal() == wx.ID_OK: + sEsi = Esi.getInstance() + # todo: verify that this is a correct SSO Info block + sEsi.handleLogin(dlg.ssoInfoCtrl.Value.strip()) def ShowUpdateBox(self, release, version): dlg = UpdateDialog(self, release, version) diff --git a/gui/preferenceView.py b/gui/preferenceView.py index 42ce5cd05..9ce2e676d 100644 --- a/gui/preferenceView.py +++ b/gui/preferenceView.py @@ -43,7 +43,7 @@ from gui.builtinPreferenceViews import ( # noqa: E402, F401 pyfaGeneralPreferences, pyfaNetworkPreferences, pyfaHTMLExportPreferences, - # pyfaCrestPreferences, + pyfaEsiPreferences, pyfaContextMenuPreferences, pyfaStatViewPreferences, pyfaUpdatePreferences, diff --git a/service/esi.py b/service/esi.py index 3dbd6496c..675a705ee 100644 --- a/service/esi.py +++ b/service/esi.py @@ -17,6 +17,7 @@ from eos.enum import Enum from eos.saveddata.ssocharacter import SsoCharacter import gui.globalEvents as GE from service.server import StoppableHTTPServer, AuthHandler +from service.settings import EsiSettings from .esi_security_proxy import EsiSecurityProxy from esipy import EsiClient, EsiApp @@ -39,6 +40,11 @@ class Servers(Enum): SISI = 1 +class LoginMethod(Enum): + SERVER = 0 + MANUAL = 1 + + class Esi(object): esiapp = None esi_v1 = None @@ -75,6 +81,8 @@ class Esi(object): def __init__(self): Esi.initEsiApp() + self.settings = EsiSettings.getInstance() + AFTER_TOKEN_REFRESH.add_receiver(self.tokenUpdate) # these will be set when needed @@ -119,7 +127,7 @@ class Esi(object): if char is not None and char.esi_client is None: char.esi_client = Esi.genEsiClient() Esi.update_token(char, Esi.get_sso_data(char)) # don't use update_token on security directly, se still need to apply the values here - print(repr(char)) + eos.db.commit() return char @@ -181,17 +189,33 @@ class Esi(object): char.esi_client.security.update_token(tokenResponse) def login(self): - # Switch off how we do things here depending on the mode of authentication - uri = self.startServer() + serverAddr = None + if self.settings.get('loginMode') == LoginMethod.SERVER: + serverAddr = self.startServer() + uri = self.getLoginURI(serverAddr) webbrowser.open(uri) - wx.PostEvent(self.mainFrame, GE.SsoLoggingIn()) + wx.PostEvent(self.mainFrame, GE.SsoLoggingIn(login_mode=self.settings.get('loginMode'))) def stopServer(self): pyfalog.debug("Stopping Server") self.httpd.stop() self.httpd = None - def startServer(self): + def getLoginURI(self, redirect=None): + self.state = str(uuid.uuid4()) + esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) + + args = { + 'state': self.state, + 'pyfa_version': config.version, + } + + if redirect is not None: + args['redirect'] = redirect + + return esisecurity.get_auth_uri(**args) + + def startServer(self): # todo: break this out into two functions: starting the server, and getting the URI pyfalog.debug("Starting server") # we need this to ensure that the previous get_request finishes, and then the socket will close @@ -199,20 +223,14 @@ class Esi(object): self.stopServer() time.sleep(1) - self.state = str(uuid.uuid4()) self.httpd = StoppableHTTPServer(('localhost', 0), AuthHandler) port = self.httpd.socket.getsockname()[1] - - esisecurity = EsiSecurityProxy(sso_url=config.ESI_AUTH_PROXY) - - uri = esisecurity.get_auth_uri(state=self.state, redirect='http://localhost:{}'.format(port), pyfa_version=config.version) - self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerLogin,)) self.serverThread.name = "SsoCallbackServer" self.serverThread.daemon = True self.serverThread.start() - return uri + return 'http://localhost:{}'.format(port) def handleLogin(self, ssoInfo): auth_response = json.loads(base64.b64decode(ssoInfo)) diff --git a/service/settings.py b/service/settings.py index 7517255c5..ae5ce0e11 100644 --- a/service/settings.py +++ b/service/settings.py @@ -352,32 +352,32 @@ class UpdateSettings(object): self.serviceUpdateSettings[type] = value -class CRESTSettings(object): +class EsiSettings(object): _instance = None @classmethod def getInstance(cls): if cls._instance is None: - cls._instance = CRESTSettings() + cls._instance = EsiSettings() return cls._instance def __init__(self): - # mode - # 0 - Implicit authentication - # 1 - User-supplied client details - serviceCRESTDefaultSettings = {"mode": 0, "server": 0, "clientID": "", "clientSecret": "", "timeout": 60} + # LoginMode: + # 0 - Server Start Up + # 1 - User copy and paste data from website to pyfa + defaults = {"loginMode": 0, "clientID": "", "clientSecret": "", "timeout": 60} - self.serviceCRESTSettings = SettingsProvider.getInstance().getSettings( - "pyfaServiceCRESTSettings", - serviceCRESTDefaultSettings + self.settings = SettingsProvider.getInstance().getSettings( + "pyfaServiceEsiSettings", + defaults ) def get(self, type): - return self.serviceCRESTSettings[type] + return self.settings[type] def set(self, type, value): - self.serviceCRESTSettings[type] = value + self.settings[type] = value class StatViewSettings(object): From 48d90c1eca34b63f8b0519522f88e1c2d22ca04e Mon Sep 17 00:00:00 2001 From: Ryan Holmes Date: Sun, 18 Mar 2018 00:52:09 -0400 Subject: [PATCH 38/39] Add login method to login request --- service/esi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/service/esi.py b/service/esi.py index 675a705ee..add64537b 100644 --- a/service/esi.py +++ b/service/esi.py @@ -208,6 +208,7 @@ class Esi(object): args = { 'state': self.state, 'pyfa_version': config.version, + 'login_method': self.settings.get('loginMode') } if redirect is not None: From 117d51caabe1f413cc50109492f18dac169ca330 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 25 Mar 2018 21:42:51 -0400 Subject: [PATCH 39/39] change back to the py3-dev database --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 6eace3a98..78d575b1c 100644 --- a/config.py +++ b/config.py @@ -140,7 +140,7 @@ def defPaths(customSavePath=None): # os.environ["SSL_CERT_FILE"] = os.path.join(pyfaPath, "cacert.pem") # The database where we store all the fits etc - saveDB = os.path.join(savePath, "saveddata-py3-esi-dev.db") + saveDB = os.path.join(savePath, "saveddata-py3-dev.db") # The database where the static EVE data from the datadump is kept. # This is not the standard sqlite datadump but a modified version created by eos