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()