Merge branch 'esi' into test-3

This commit is contained in:
blitzmann
2018-04-28 20:35:14 -04:00
37 changed files with 1127 additions and 2375 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -12,7 +12,6 @@ __all__ = [
"miscData",
"targetResists",
"override",
"crest",
"implantSet",
"loadDefaultDatabaseValues"
]

View File

@@ -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)
}
)

View File

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

View File

@@ -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])

View File

@@ -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:

View File

@@ -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

View File

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

View File

@@ -6,7 +6,6 @@ __all__ = [
"pyfaDatabasePreferences",
"pyfaLoggingPreferences",
"pyfaEnginePreferences",
"pyfaStatViewPreferences",
"pyfaCrestPreferences"
]
"pyfaEsiPreferences",
"pyfaStatViewPreferences"]

View File

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

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

View File

@@ -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:

View File

@@ -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):

View File

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

View File

@@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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
View 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])

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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 "&lt;" 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']

View File

@@ -1 +0,0 @@
version = "0.0.1"

View File

@@ -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

View File

@@ -1,2 +0,0 @@
class APIException(Exception):
pass

View File

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

View File

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

View File

@@ -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):

View File

@@ -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):

View File

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