diff --git a/config.py b/config.py index 1ce7b1445..78d575b1c 100644 --- a/config.py +++ b/config.py @@ -3,6 +3,9 @@ import sys from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \ StreamHandler, TimedRotatingFileHandler, WARNING +import hashlib + +from cryptography.fernet import Fernet pyfalog = Logger(__name__) @@ -34,6 +37,11 @@ gameDB = None logPath = None loggingLevel = None logging_setup = None +cipher = None +clientHash = None + +ESI_AUTH_PROXY = "https://www.pyfa.io" # "http://localhost:5015" +ESI_CACHE = 'esi_cache' LOGLEVEL_MAP = { "critical": CRITICAL, @@ -44,6 +52,10 @@ LOGLEVEL_MAP = { } +def getClientSecret(): + return clientHash + + def isFrozen(): if hasattr(sys, 'frozen'): return True @@ -86,6 +98,8 @@ def defPaths(customSavePath=None): global gameDB global saveInRoot global logPath + global cipher + global clientHash pyfalog.debug("Configuring Pyfa") @@ -110,6 +124,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/db/__init__.py b/eos/db/__init__.py index c3bdb9739..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, crest, 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 7dbf2a45d..ba1ddad73 100644 --- a/eos/db/saveddata/__init__.py +++ b/eos/db/saveddata/__init__.py @@ -12,7 +12,6 @@ __all__ = [ "miscData", "targetResists", "override", - "crest", "implantSet", "loadDefaultDatabaseValues" ] diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index c87817541..739a34a98 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -17,24 +17,24 @@ # 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.effectHandlerHelpers import HandledImplantBoosterList +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), - Column("apiID", Integer), - Column("apiKey", String), - Column("defaultChar", Integer), - Column("chars", String, nullable=True), Column("defaultLevel", Integer, nullable=True), Column("alphaCloneID", Integer, nullable=True), Column("ownerID", ForeignKey("users.ID"), nullable=True), @@ -42,6 +42,28 @@ 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) + mapper(Character, characters_table, properties={ "_Character__alphaCloneID": characters_table.c.alphaCloneID, @@ -63,5 +85,10 @@ mapper(Character, characters_table, primaryjoin=charImplants_table.c.charID == characters_table.c.ID, secondaryjoin=charImplants_table.c.implantID == Implant.ID, secondary=charImplants_table), + "_Character__ssoCharacters" : relation( + SsoCharacter, + collection_class=HandledSsoCharacterList, + backref='characters', + secondary=sso_character_map_table) } ) diff --git a/eos/db/saveddata/crest.py b/eos/db/saveddata/crest.py deleted file mode 100644 index 28f77a983..000000000 --- a/eos/db/saveddata/crest.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 import Table, Column, Integer, String, DateTime -from sqlalchemy.orm import mapper -import datetime - -from eos.db import saveddata_meta -from eos.saveddata.crestchar import CrestChar - -crest_table = Table("crest", 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)) - -mapper(CrestChar, crest_table) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 98902007f..e582eef87 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,29 +467,28 @@ def getProjectedFits(fitID): raise TypeError("Need integer as argument") -def getCrestCharacters(eager=None): +def getSsoCharacters(clientHash, eager=None): eager = processEager(eager) with sd_lock: - characters = saveddata_session.query(CrestChar).options(*eager).all() + characters = saveddata_session.query(SsoCharacter).filter(SsoCharacter.client == clientHash).options(*eager).all() return characters -@cachedQuery(CrestChar, 1, "lookfor") -def getCrestCharacter(lookfor, eager=None): +@cachedQuery(SsoCharacter, 1, "lookfor", "clientHash") +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(CrestChar).get(lookfor) - else: - eager = processEager(eager) - with sd_lock: - character = saveddata_session.query(CrestChar).options(*eager).filter(CrestChar.ID == lookfor).first() + filter = and_(filter, SsoCharacter.ID == lookfor) elif isinstance(lookfor, str): - eager = processEager(eager) - with sd_lock: - character = saveddata_session.query(CrestChar).options(*eager).filter(CrestChar.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/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 9d86e51e3..f890ae0ed 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -205,6 +205,16 @@ class HandledImplantBoosterList(HandledList): HandledList.append(self, thing) +class HandledSsoCharacterList(list): + def 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): def append(self, proj): if proj.isInvalid: diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index f222f3a17..05ff456fa 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -119,16 +119,9 @@ class Character(object): return all0 - def apiUpdateCharSheet(self, skills, secStatus=0): - for skillRow in skills: - try: - skill = self.getSkill(int(skillRow["typeID"])) - skill.setLevel(int(skillRow["level"]), persist=True, ignoreRestrict=True) - except: - # if setting a skill doesn't work, it's not the end of the world, just quietly pass - pass - - self.secStatus = secStatus + def clearSkills(self): + del self.__skills[:] + self.__skillIdMap.clear() @property def ro(self): @@ -170,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/crestchar.py b/eos/saveddata/ssocharacter.py similarity index 50% rename from eos/saveddata/crestchar.py rename to eos/saveddata/ssocharacter.py index ce6ab14fe..9ffed8d46 100644 --- a/eos/saveddata/crestchar.py +++ b/eos/saveddata/ssocharacter.py @@ -18,17 +18,40 @@ # =============================================================================== from sqlalchemy.orm import reconstructor - +import datetime +import time # 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 +class SsoCharacter(object): + 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.accessTokenExpires = None + self.esi_client = None + @reconstructor def init(self): - pass + self.esi_client = None + + 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.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/builtinPreferenceViews/__init__.py b/gui/builtinPreferenceViews/__init__.py index e6b55fb3c..32117a9ec 100644 --- a/gui/builtinPreferenceViews/__init__.py +++ b/gui/builtinPreferenceViews/__init__.py @@ -6,7 +6,6 @@ __all__ = [ "pyfaDatabasePreferences", "pyfaLoggingPreferences", "pyfaEnginePreferences", - "pyfaStatViewPreferences", - "pyfaCrestPreferences" -] + "pyfaEsiPreferences", + "pyfaStatViewPreferences"] diff --git a/gui/builtinPreferenceViews/pyfaCrestPreferences.py b/gui/builtinPreferenceViews/pyfaCrestPreferences.py deleted file mode 100644 index f2dbe694d..000000000 --- a/gui/builtinPreferenceViews/pyfaCrestPreferences.py +++ /dev/null @@ -1,150 +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.crest import Crest - - -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')) - Crest.restartService() - - def OnServerChange(self, event): - self.settings.set('server', event.GetInt()) - Crest.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.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/characterEditor.py b/gui/characterEditor.py index af8453710..c7a2fef8c 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 @@ -44,6 +45,7 @@ from gui.utils.clipboard import toClipboard, fromClipboard import roman import re +import webbrowser pyfalog = Logger(__name__) @@ -174,7 +176,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) @@ -722,18 +724,22 @@ 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) + + 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) @@ -743,97 +749,95 @@ 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) + 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) - self.charChoice.Append("No Selection", 0) - fgSizerInput.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 5) - - self.charChoice.Enable(False) + fgSizerInput.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 10) 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.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) - - 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.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() + activeChar = self.charEditor.entityEditor.getActiveEntity() + sChar.setSsoCharacter(activeChar.ID, self.getActiveCharacter()) + event.Skip() + + def addCharacter(self, event): + sEsi = Esi.getInstance() + sEsi.login() + + 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() + activeChar = self.charEditor.entityEditor.getActiveEntity() - ID, key, char, chars = sChar.getApiDetails(activeChar.ID) - self.inputID.SetValue(str(ID)) - self.inputKey.SetValue(key) + 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() self.charChoice.Clear() - if chars: - for charName in chars: - 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) + 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() @@ -846,47 +850,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: diff --git a/gui/characterSelection.py b/gui/characterSelection.py index 916253283..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) @@ -151,9 +152,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) @@ -161,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): @@ -178,16 +177,23 @@ 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): - 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/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 69% rename from gui/crestFittings.py rename to gui/esiFittings.py index 718b6654b..31fdd5a71 100644 --- a/gui/crestFittings.py +++ b/gui/esiFittings.py @@ -16,12 +16,14 @@ import gui.globalEvents as GE from logbook import Logger import calendar -from service.crest import Crest, CrestModes +from service.esi import Esi +from esipy.exceptions import APIException +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) @@ -30,20 +32,13 @@ class CrestFittings(wx.Frame): self.mainFrame = parent mainSizer = wx.BoxSizer(wx.VERTICAL) - sCrest = Crest.getInstance() + sEsi = Esi.getInstance() 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) @@ -85,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() @@ -98,63 +90,52 @@ class CrestFittings(wx.Frame): event.Skip() def updateCharList(self): - sCrest = Crest.getInstance() - chars = sCrest.getCrestCharacters() + sEsi = Esi.getInstance() + chars = sEsi.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.ID) 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): - if event.type == CrestModes.IMPLICIT: - self.Close() - else: - self.updateCharList() + self.updateCharList() event.Skip() # continue event 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): - 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 def fetchFittings(self, event): - sCrest = Crest.getInstance() + sEsi = 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) - self.cacheTimer.Start(1000) + fittings = sEsi.getFittings(self.getActiveCharacter()) + # self.cacheTime = fittings.get('cached_until') + # self.updateCacheStatus(None) + # self.cacheTimer.Start(1000) self.fitTree.populateSkillTree(fittings) del waitDialog except requests.exceptions.ConnectionError: 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 @@ -166,25 +147,39 @@ class CrestFittings(wx.Frame): self.mainFrame._openAfterImport(fits) def deleteFitting(self, event): - sCrest = Crest.getInstance() + sEsi = Esi.getInstance() selection = self.fitView.fitSelection if not selection: return 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']) + sEsi.delFitting(self.getActiveCharacter(), data['fitting_id']) except requests.exceptions.ConnectionError: msg = "Connection error, please check your internet connection" pyfalog.error(msg) 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 validating characters' SSO 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, @@ -193,21 +188,14 @@ class ExportToEve(wx.Frame): self.mainFrame = parent self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) - sCrest = Crest.getInstance() + sEsi = Esi.getInstance() 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) @@ -231,15 +219,15 @@ class ExportToEve(wx.Frame): self.Centre(wx.BOTH) def updateCharList(self): - sCrest = Crest.getInstance() - chars = sCrest.getCrestCharacters() + sEsi = Esi.getInstance() + chars = sEsi.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.ID) self.charChoice.SetSelection(0) @@ -248,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): @@ -261,11 +246,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 @@ -280,29 +260,36 @@ class ExportToEve(wx.Frame): return self.statusbar.SetStatusText("Sending request and awaiting response", 1) - sCrest = Crest.getInstance() + sEsi = Esi.getInstance() try: sFit = Fit.getInstance() - data = sPort.exportCrest(sFit.getFit(fitID)) - res = sCrest.postFitting(self.getActiveCharacter(), data) + data = sPort.exportESI(sFit.getFit(fitID)) + res = sEsi.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) 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): +class SsoCharacterMgmt(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) @@ -310,7 +297,7 @@ 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.popCharList() @@ -341,14 +328,14 @@ class CrestMgmt(wx.Dialog): event.Skip() def popCharList(self): - sCrest = Crest.getInstance() - chars = sCrest.getCrestCharacters() + sEsi = Esi.getInstance() + chars = sEsi.getSsoCharacters() 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.SetItem(index, 1, str(char.characterID)) self.lcCharacters.SetItemData(index, char.ID) self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE) @@ -356,16 +343,15 @@ class CrestMgmt(wx.Dialog): @staticmethod def addChar(event): - sCrest = Crest.getInstance() - uri = sCrest.startServer() - webbrowser.open(uri) + sEsi = Esi.getInstance() + sEsi.login() def delChar(self, event): item = self.lcCharacters.GetFirstSelected() if item > -1: charID = self.lcCharacters.GetItemData(item) - sCrest = Crest.getInstance() - sCrest.delCrestCharacter(charID) + sEsi = Esi.getInstance() + sEsi.delSsoCharacter(charID) self.popCharList() @@ -381,7 +367,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) @@ -395,11 +381,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) @@ -422,7 +409,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/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/gui/mainFrame.py b/gui/mainFrame.py index 5e354148f..c9e5fa09c 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 @@ -82,9 +83,8 @@ import threading import webbrowser import wx.adv -from service.crest import Crest -from service.crest import CrestModes -from gui.crestFittings import CrestFittings, ExportToEve, CrestMgmt +from service.esi import Esi, LoginMethod +from gui.esiFittings import EveFittings, ExportToEve, SsoCharacterMgmt disableOverrideEditor = False @@ -238,10 +238,15 @@ 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.Bind(GE.EVT_SSO_LOGGING_IN, self.ShowSsoLogin) - self.titleTimer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self.updateTitle, self.titleTimer) + def ShowSsoLogin(self, event): + 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) @@ -610,68 +615,26 @@ class MainFrame(wx.Frame): wx.PostEvent(self, GE.FitChanged(fitID=fitID)) def eveFittings(self, event): - dlg = CrestFittings(self) + dlg = EveFittings(self) dlg.Show() - def updateTitle(self, event): - sCrest = Crest.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) - + def updateEsiMenus(self, type): menu = self.GetMenuBar() - if event.type == CrestModes.IMPLICIT or event.numChars == 0: - menu.Enable(menu.eveFittingsId, False) - menu.Enable(menu.exportToEveId, False) + sEsi = Esi.getInstance() - 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() - self.SetTitle(self.title) - - menu = self.GetMenuBar() - sCrest = Crest.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.getCrestCharacters()) == 0 - menu.Enable(menu.eveFittingsId, not enable) - menu.Enable(menu.exportToEveId, not enable) + menu.SetLabel(menu.ssoLoginId, "Manage Characters") + enable = len(sEsi.getSsoCharacters()) == 0 + menu.Enable(menu.eveFittingsId, not enable) + 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 = SsoCharacterMgmt(self) + dlg.Show() def exportToEve(self, event): dlg = ExportToEve(self) @@ -746,9 +709,9 @@ 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.exportCrest(fit)) + toClipboard(Port.exportESI(fit)) def clipboardXml(self): fit = db_getFit(self.getActiveFit()) @@ -772,7 +735,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 be9322010..9c421f779 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.sEsi = 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.eveFittingsId, "Browse EVE Fittings") - crestMenu.Append(self.exportToEveId, "Export To EVE") + esiMMenu = wx.Menu() + self.Append(esiMMenu, "EVE &SSO") - if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0: - self.Enable(self.eveFittingsId, False) - self.Enable(self.exportToEveId, False) + 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) + self.Enable(self.exportToEveId, True) if not self.mainFrame.disableOverrideEditor: windowMenu.AppendSeparator() diff --git a/gui/preferenceView.py b/gui/preferenceView.py index a52a720e4..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/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() diff --git a/requirements.txt b/requirements.txt index 2b38e8685..d43bebe51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ matplotlib >= 2.0.0 python-dateutil requests >= 2.0.0 sqlalchemy >= 1.0.5 +esipy == 0.3.3 markdown2 packaging roman diff --git a/service/character.py b/service/character.py index 9bbf8a6cf..9a3727620 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='rb') as charFile: - sheet = ParseXML(charFile) - char = sCharacter.new(sheet.name + " (imported)") - sCharacter.apiUpdateCharSheet(char.ID, sheet.skills, 0) - 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 = float(doc.getElementsByTagName("securityStatus")[0].firstChild.nodeValue) or 0.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) @@ -345,6 +338,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) @@ -352,27 +347,24 @@ 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 getSsoCharacter(charID): + char = eos.db.getCharacter(charID) + sso = char.getSsoCharacter(config.getClientSecret()) + return sso @staticmethod - def apiCharList(charID, userID, apiKey): + 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() - 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): @@ -470,35 +462,32 @@ 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() + sChar = Character.getInstance() + ssoChar = sChar.getSsoCharacter(char.ID) + resp = sEsi.getSkills(ssoChar.ID) - 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(ssoChar.ID) + + 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/crest.py b/service/crest.py deleted file mode 100644 index ff1765298..000000000 --- a/service/crest.py +++ /dev/null @@ -1,228 +0,0 @@ -# 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 Crest(object): - clientIDs = { - Servers.TQ : 'f9be379951c046339dc13a00e6be7704', - Servers.SISI: 'af87365240d644f7950af563b8418bad' - } - - # @todo: move this to settings - clientCallback = 'http://localhost:6461' - clientTest = True - - _instance = None - - @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) - """ - - 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.getCrestCharacter(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() - 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 - - 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) - self.charCache[charID] = char - return char - - def getFittings(self, charID): - char = self.getCrestCharacter(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) - return char.eve.post('%scharacters/%d/fittings/' % (char.eve._authed_endpoint, char.ID), data=json) - - def delFitting(self, charID, fittingID): - char = self.getCrestCharacter(charID) - return char.eve.delete('%scharacters/%d/fittings/%d/' % (char.eve._authed_endpoint, char.ID, fittingID)) - - 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") - 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.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleLogin,)) - self.serverThread.name = "CRESTServer" - self.serverThread.daemon = True - self.serverThread.start() - - self.state = str(uuid.uuid4()) - return self.eve.auth_uri(scopes=self.scopes, state=self.state) - - 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) - - 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() - - eve() - info = eve.whoami() - - pyfalog.debug("Got character info: {0}", info) - - self.implicitCharacter = CrestChar(info['CharacterID'], info['CharacterName']) - self.implicitCharacter.eve = eve - # self.implicitCharacter.fetchImage() - - 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() - - pyfalog.debug("Got character info: {0}", info) - - # 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) - - wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) diff --git a/service/esi.py b/service/esi.py new file mode 100644 index 000000000..add64537b --- /dev/null +++ b/service/esi.py @@ -0,0 +1,271 @@ +# noinspection PyPackageRequirements +import wx +from logbook import Logger +import threading +import uuid +import time +import config +import base64 +import json +import os +import config +import webbrowser + +import eos.db +import datetime +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 +from esipy.cache import FileCache + +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) + +file_cache = FileCache(cache_path) + + +class Servers(Enum): + TQ = 0 + SISI = 1 + + +class LoginMethod(Enum): + SERVER = 0 + MANUAL = 1 + + +class Esi(object): + esiapp = None + esi_v1 = None + esi_v4 = None + + _initializing = None + + _instance = None + + @classmethod + def initEsiApp(cls): + 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): + 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() + + return cls._instance + + def __init__(self): + Esi.initEsiApp() + + self.settings = EsiSettings.getInstance() + + AFTER_TOKEN_REFRESH.add_receiver(self.tokenUpdate) + + # these will be set when needed + self.httpd = None + self.state = None + self.ssoTimer = None + + 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() + + def tokenUpdate(self, **kwargs): + print(kwargs) + pass + + 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)) + + def getSsoCharacters(self): + chars = eos.db.getSsoCharacters(config.getClientSecret()) + return chars + + def getSsoCharacter(self, id): + """ + Get character, and modify to include the eve connection + """ + char = eos.db.getSsoCharacter(id, config.getClientSecret()) + 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 + + eos.db.commit() + return char + + 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 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, id, fittingID): + char = self.getSsoCharacter(id) + op = Esi.esi_v1.op['delete_characters_character_id_fittings_fitting_id']( + character_id=char.characterID, + fitting_id=fittingID + ) + 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 login(self): + 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(login_mode=self.settings.get('loginMode'))) + + def stopServer(self): + pyfalog.debug("Stopping Server") + self.httpd.stop() + self.httpd = None + + 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, + 'login_method': self.settings.get('loginMode') + } + + 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 + if self.httpd: + self.stopServer() + time.sleep(1) + + self.httpd = StoppableHTTPServer(('localhost', 0), AuthHandler) + port = self.httpd.socket.getsockname()[1] + self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerLogin,)) + self.serverThread.name = "SsoCallbackServer" + self.serverThread.daemon = True + self.serverThread.start() + + return 'http://localhost:{}'.format(port) + + 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 + # 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['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 + + eos.db.save(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]) diff --git a/service/esi_security_proxy.py b/service/esi_security_proxy.py new file mode 100644 index 000000000..7c29f80b0 --- /dev/null +++ b/service/esi_security_proxy.py @@ -0,0 +1,235 @@ +# -*- 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 urllib.parse import urlencode + +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, + **kwargs): + """ Init the ESI Security Object + + :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') + + # 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, *args, **kwargs): + """ 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?%s' % ( + self.oauth_authorize, + urlencode(kwargs) + ) + + 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 diff --git a/service/eveapi.py b/service/eveapi.py index be48f8833..e69de29bb 100644 --- a/service/eveapi.py +++ b/service/eveapi.py @@ -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, params=kw) - - if cache: - store = True - response = response.text - 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.text, True, - store and (lambda obj: cache.store(self._host, path, kw, response.text, 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.text, 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/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 7307697a5..7f58185be 100644 --- a/service/port.py +++ b/service/port.py @@ -51,9 +51,13 @@ 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 + +class ESIExportException(Exception): + pass + pyfalog = Logger(__name__) EFT_SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE] @@ -338,24 +342,20 @@ 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 nested_dict = lambda: collections.defaultdict(nested_dict) fit = nested_dict() - sCrest = Crest.getInstance() + sEsi = 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 - 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 @@ -383,9 +383,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"]: @@ -398,38 +396,33 @@ 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) + if len(fit['items']) == 0: + raise ESIExportException("Cannot export fitting: module list cannot be empty.") + return json.dumps(fit) @classmethod @@ -445,7 +438,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 @@ -463,7 +456,7 @@ class Port(object): return "DNA", (cls.importDna(string),) @staticmethod - def importCrest(str_): + def importESI(str_): sMkt = Market.getInstance() fitobj = Fit() @@ -475,13 +468,13 @@ 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") + pyfalog.warning("Caught exception in importESI") return None items.sort(key=lambda k: k['flag']) @@ -489,7 +482,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'] 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 index 6a30e9162..e69de29bb 100644 --- a/service/pycrest/eve.py +++ b/service/pycrest/eve.py @@ -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}".format(config.getVersion) - 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 - ) diff --git a/service/server.py b/service/server.py index 76c5a964e..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 @@ -82,14 +81,14 @@ class AuthHandler(http.server.BaseHTTPRequestHandler): try: if step2: self.server.callback(parts) - pyfalog.info("Successfully logged into CREST.") - 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." + pyfalog.info("Successfully logged into EVE.") + 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 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: - pyfalog.error("Error in CREST AuthHandler") + pyfalog.error("Error logging into EVE") pyfalog.error(ex) msg = "

Error

\n

{}

".format(ex.message) finally: @@ -109,10 +108,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 +130,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): 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): 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()