Merge branch 'esi' into test-3
This commit is contained in:
25
config.py
25
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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,6 @@ __all__ = [
|
||||
"miscData",
|
||||
"targetResists",
|
||||
"override",
|
||||
"crest",
|
||||
"implantSet",
|
||||
"loadDefaultDatabaseValues"
|
||||
]
|
||||
|
||||
@@ -17,24 +17,24 @@
|
||||
# along with eos. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# ===============================================================================
|
||||
|
||||
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)
|
||||
@@ -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])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
@@ -6,7 +6,6 @@ __all__ = [
|
||||
"pyfaDatabasePreferences",
|
||||
"pyfaLoggingPreferences",
|
||||
"pyfaEnginePreferences",
|
||||
"pyfaStatViewPreferences",
|
||||
"pyfaCrestPreferences"
|
||||
]
|
||||
"pyfaEsiPreferences",
|
||||
"pyfaStatViewPreferences"]
|
||||
|
||||
|
||||
@@ -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()
|
||||
137
gui/builtinPreferenceViews/pyfaEsiPreferences.py
Normal file
137
gui/builtinPreferenceViews/pyfaEsiPreferences.py
Normal file
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -43,7 +43,7 @@ from gui.builtinPreferenceViews import ( # noqa: E402, F401
|
||||
pyfaGeneralPreferences,
|
||||
pyfaNetworkPreferences,
|
||||
pyfaHTMLExportPreferences,
|
||||
pyfaCrestPreferences,
|
||||
pyfaEsiPreferences,
|
||||
pyfaContextMenuPreferences,
|
||||
pyfaStatViewPreferences,
|
||||
pyfaUpdatePreferences,
|
||||
|
||||
25
gui/ssoLogin.py
Normal file
25
gui/ssoLogin.py
Normal file
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
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())
|
||||
|
||||
228
service/crest.py
228
service/crest.py
@@ -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))
|
||||
271
service/esi.py
Normal file
271
service/esi.py
Normal file
@@ -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])
|
||||
235
service/esi_security_proxy.py
Normal file
235
service/esi_security_proxy.py
Normal file
@@ -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
|
||||
1016
service/eveapi.py
1016
service/eveapi.py
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
@@ -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'] = "<pyfa:%d />" % 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']
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
version = "0.0.1"
|
||||
@@ -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
|
||||
@@ -1,2 +0,0 @@
|
||||
class APIException(Exception):
|
||||
pass
|
||||
@@ -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__()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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 = "<h2>Error</h2>\n<p>{}</p>".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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user