Goodbye eveapi! You have served us well all these years!
Start stripping XML API stuff and implement ESI skill fetching.
This commit is contained in:
@@ -27,14 +27,12 @@ from eos.effectHandlerHelpers import HandledImplantBoosterList
|
||||
from eos.saveddata.implant import Implant
|
||||
from eos.saveddata.user import User
|
||||
from eos.saveddata.character import Character, Skill
|
||||
from eos.saveddata.ssocharacter import SsoCharacter
|
||||
|
||||
characters_table = Table("characters", saveddata_meta,
|
||||
Column("ID", Integer, primary_key=True),
|
||||
Column("name", String, nullable=False),
|
||||
Column("apiID", Integer),
|
||||
Column("apiKey", String),
|
||||
Column("defaultChar", Integer),
|
||||
Column("chars", String, nullable=True),
|
||||
Column("ssoCharacterID", ForeignKey("ssoCharacter.ID"), nullable=True),
|
||||
Column("defaultLevel", Integer, nullable=True),
|
||||
Column("alphaCloneID", Integer, nullable=True),
|
||||
Column("ownerID", ForeignKey("users.ID"), nullable=True),
|
||||
@@ -62,6 +60,6 @@ mapper(Character, characters_table,
|
||||
single_parent=True,
|
||||
primaryjoin=charImplants_table.c.charID == characters_table.c.ID,
|
||||
secondaryjoin=charImplants_table.c.implantID == Implant.ID,
|
||||
secondary=charImplants_table),
|
||||
secondary=charImplants_table)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -479,7 +479,7 @@ def getSsoCharacter(lookfor, clientHash, eager=None):
|
||||
filter = SsoCharacter.client == clientHash
|
||||
|
||||
if isinstance(lookfor, int):
|
||||
filter = and_(filter, SsoCharacter.characterID == lookfor)
|
||||
filter = and_(filter, SsoCharacter.ID == lookfor)
|
||||
elif isinstance(lookfor, str):
|
||||
filter = and_(filter, SsoCharacter.characterName == lookfor)
|
||||
else:
|
||||
|
||||
@@ -119,12 +119,9 @@ class Character(object):
|
||||
|
||||
return all0
|
||||
|
||||
def apiUpdateCharSheet(self, skills, secStatus=0):
|
||||
def clearSkills(self):
|
||||
del self.__skills[:]
|
||||
self.__skillIdMap.clear()
|
||||
for skillRow in skills:
|
||||
self.addSkill(Skill(self, skillRow["typeID"], skillRow["level"]))
|
||||
self.secStatus = secStatus
|
||||
|
||||
@property
|
||||
def ro(self):
|
||||
|
||||
@@ -119,11 +119,9 @@ class PFCrestPref(PreferenceView):
|
||||
def OnModeChange(self, event):
|
||||
self.settings.set('mode', event.GetInt())
|
||||
self.ToggleProxySettings(self.settings.get('mode'))
|
||||
Esi.restartService()
|
||||
|
||||
def OnServerChange(self, event):
|
||||
self.settings.set('server', event.GetInt())
|
||||
Esi.restartService()
|
||||
|
||||
def OnBtnApply(self, event):
|
||||
self.settings.set('clientID', self.inputClientID.GetValue().strip())
|
||||
|
||||
@@ -158,11 +158,11 @@ class CharacterEditor(wx.Frame):
|
||||
|
||||
self.sview = SkillTreeView(self.viewsNBContainer)
|
||||
self.iview = ImplantEditorView(self.viewsNBContainer, self)
|
||||
self.aview = APIView(self.viewsNBContainer)
|
||||
# self.aview = APIView(self.viewsNBContainer)
|
||||
|
||||
self.viewsNBContainer.AddPage(self.sview, "Skills")
|
||||
self.viewsNBContainer.AddPage(self.iview, "Implants")
|
||||
self.viewsNBContainer.AddPage(self.aview, "API")
|
||||
# self.viewsNBContainer.AddPage(self.aview, "API")
|
||||
|
||||
mainSizer.Add(self.viewsNBContainer, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
|
||||
@@ -151,9 +151,7 @@ class CharacterSelection(wx.Panel):
|
||||
def refreshApi(self, event):
|
||||
self.btnRefresh.Enable(False)
|
||||
sChar = Character.getInstance()
|
||||
ID, key, charName, chars = sChar.getApiDetails(self.getActiveCharacter())
|
||||
if charName:
|
||||
sChar.apiFetch(self.getActiveCharacter(), charName, self.refreshAPICallback)
|
||||
sChar.apiFetch(self.getActiveCharacter(), self.refreshAPICallback)
|
||||
|
||||
def refreshAPICallback(self, e=None):
|
||||
self.btnRefresh.Enable(True)
|
||||
@@ -178,7 +176,9 @@ class CharacterSelection(wx.Panel):
|
||||
self.charChoice.SetSelection(self.charCache)
|
||||
self.mainFrame.showCharacterEditor(event)
|
||||
return
|
||||
if sChar.getCharName(charID) not in ("All 0", "All 5") and sChar.apiEnabled(charID):
|
||||
|
||||
char = sChar.getCharacter(charID)
|
||||
if sChar.getCharName(charID) not in ("All 0", "All 5") and char.ssoCharacterID is not None:
|
||||
self.btnRefresh.Enable(True)
|
||||
else:
|
||||
self.btnRefresh.Enable(False)
|
||||
|
||||
@@ -99,7 +99,7 @@ class CrestFittings(wx.Frame):
|
||||
|
||||
self.charChoice.Clear()
|
||||
for char in chars:
|
||||
self.charChoice.Append(char.characterName, char.characterID)
|
||||
self.charChoice.Append(char.characterName, char.ID)
|
||||
|
||||
self.charChoice.SetSelection(0)
|
||||
|
||||
@@ -220,7 +220,7 @@ class ExportToEve(wx.Frame):
|
||||
|
||||
self.charChoice.Clear()
|
||||
for char in chars:
|
||||
self.charChoice.Append(char.characterName, char.characterID)
|
||||
self.charChoice.Append(char.characterName, char.ID)
|
||||
|
||||
self.charChoice.SetSelection(0)
|
||||
|
||||
@@ -344,7 +344,7 @@ class CrestMgmt(wx.Dialog):
|
||||
if item > -1:
|
||||
charID = self.lcCharacters.GetItemData(item)
|
||||
sCrest = Esi.getInstance()
|
||||
sCrest.delCrestCharacter(charID)
|
||||
sCrest.delSsoCharacter(charID)
|
||||
self.popCharList()
|
||||
|
||||
|
||||
|
||||
@@ -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='r') as charFile:
|
||||
sheet = ParseXML(charFile)
|
||||
char = sCharacter.new(sheet.name + " (imported)")
|
||||
sCharacter.apiUpdateCharSheet(char.ID, sheet.skills)
|
||||
except:
|
||||
# if it's not api XML data, try this
|
||||
# this is a horrible logic flow, but whatever
|
||||
try:
|
||||
charFile = open(path, mode='r').read()
|
||||
doc = minidom.parseString(charFile)
|
||||
if doc.documentElement.tagName not in ("SerializableCCPCharacter", "SerializableUriCharacter"):
|
||||
pyfalog.error("Incorrect EVEMon XML sheet")
|
||||
raise RuntimeError("Incorrect EVEMon XML sheet")
|
||||
name = doc.getElementsByTagName("name")[0].firstChild.nodeValue
|
||||
securitystatus = doc.getElementsByTagName("securityStatus")[0].firstChild.nodeValue or 0
|
||||
skill_els = doc.getElementsByTagName("skill")
|
||||
skills = []
|
||||
for skill in skill_els:
|
||||
if int(skill.getAttribute("typeID")) in all_skill_ids and (0 <= int(skill.getAttribute("level")) <= 5):
|
||||
skills.append({
|
||||
"typeID": int(skill.getAttribute("typeID")),
|
||||
"level": int(skill.getAttribute("level")),
|
||||
})
|
||||
else:
|
||||
pyfalog.error(
|
||||
"Attempted to import unknown skill {0} (ID: {1}) (Level: {2})",
|
||||
skill.getAttribute("name"),
|
||||
skill.getAttribute("typeID"),
|
||||
skill.getAttribute("level"),
|
||||
)
|
||||
char = sCharacter.new(name + " (EVEMon)")
|
||||
sCharacter.apiUpdateCharSheet(char.ID, skills, securitystatus)
|
||||
except Exception as e:
|
||||
pyfalog.error("Exception on character import:")
|
||||
pyfalog.error(e)
|
||||
continue
|
||||
charFile = open(path, mode='r').read()
|
||||
doc = minidom.parseString(charFile)
|
||||
if doc.documentElement.tagName not in ("SerializableCCPCharacter", "SerializableUriCharacter"):
|
||||
pyfalog.error("Incorrect EVEMon XML sheet")
|
||||
raise RuntimeError("Incorrect EVEMon XML sheet")
|
||||
name = doc.getElementsByTagName("name")[0].firstChild.nodeValue
|
||||
securitystatus = doc.getElementsByTagName("securityStatus")[0].firstChild.nodeValue or 0
|
||||
skill_els = doc.getElementsByTagName("skill")
|
||||
skills = []
|
||||
for skill in skill_els:
|
||||
if int(skill.getAttribute("typeID")) in all_skill_ids and (0 <= int(skill.getAttribute("level")) <= 5):
|
||||
skills.append({
|
||||
"typeID": int(skill.getAttribute("typeID")),
|
||||
"level": int(skill.getAttribute("level")),
|
||||
})
|
||||
else:
|
||||
pyfalog.error(
|
||||
"Attempted to import unknown skill {0} (ID: {1}) (Level: {2})",
|
||||
skill.getAttribute("name"),
|
||||
skill.getAttribute("typeID"),
|
||||
skill.getAttribute("level"),
|
||||
)
|
||||
char = sCharacter.new(name + " (EVEMon)")
|
||||
sCharacter.apiUpdateCharSheet(char.ID, skills, securitystatus)
|
||||
except Exception as e:
|
||||
pyfalog.error("Exception on character import:")
|
||||
pyfalog.error(e)
|
||||
continue
|
||||
|
||||
wx.CallAfter(self.callback)
|
||||
|
||||
@@ -344,6 +337,8 @@ class Character(object):
|
||||
|
||||
@staticmethod
|
||||
def getApiDetails(charID):
|
||||
# todo: fix this (or get rid of?)
|
||||
return ("", "", "", [])
|
||||
char = eos.db.getCharacter(charID)
|
||||
if char.chars is not None:
|
||||
chars = json.loads(char.chars)
|
||||
@@ -351,27 +346,8 @@ class Character(object):
|
||||
chars = None
|
||||
return char.apiID or "", char.apiKey or "", char.defaultChar or "", chars or []
|
||||
|
||||
def apiEnabled(self, charID):
|
||||
id_, key, default, _ = self.getApiDetails(charID)
|
||||
return id_ is not "" and key is not "" and default is not ""
|
||||
|
||||
@staticmethod
|
||||
def apiCharList(charID, userID, apiKey):
|
||||
char = eos.db.getCharacter(charID)
|
||||
|
||||
char.apiID = userID
|
||||
char.apiKey = apiKey
|
||||
|
||||
api = EVEAPIConnection()
|
||||
auth = api.auth(keyID=userID, vCode=apiKey)
|
||||
apiResult = auth.account.Characters()
|
||||
charList = [str(c.name) for c in apiResult.characters]
|
||||
|
||||
char.chars = json.dumps(charList)
|
||||
return charList
|
||||
|
||||
def apiFetch(self, charID, charName, callback):
|
||||
thread = UpdateAPIThread(charID, charName, (self.apiFetchCallback, callback))
|
||||
def apiFetch(self, charID, callback):
|
||||
thread = UpdateAPIThread(charID, (self.apiFetchCallback, callback))
|
||||
thread.start()
|
||||
|
||||
def apiFetchCallback(self, guiCallback, e=None):
|
||||
@@ -469,35 +445,30 @@ class Character(object):
|
||||
|
||||
|
||||
class UpdateAPIThread(threading.Thread):
|
||||
def __init__(self, charID, charName, callback):
|
||||
def __init__(self, charID, callback):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.name = "CheckUpdate"
|
||||
self.callback = callback
|
||||
self.charID = charID
|
||||
self.charName = charName
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
dbChar = eos.db.getCharacter(self.charID)
|
||||
dbChar.defaultChar = self.charName
|
||||
char = eos.db.getCharacter(self.charID)
|
||||
|
||||
api = EVEAPIConnection()
|
||||
auth = api.auth(keyID=dbChar.apiID, vCode=dbChar.apiKey)
|
||||
apiResult = auth.account.Characters()
|
||||
charID = None
|
||||
for char in apiResult.characters:
|
||||
if char.name == self.charName:
|
||||
charID = char.characterID
|
||||
break
|
||||
sEsi = Esi.getInstance()
|
||||
resp = sEsi.getSkills(char.ssoCharacterID)
|
||||
|
||||
if charID is None:
|
||||
return
|
||||
# todo: check if alpha. if so, pop up a question if they want to apply it as alpha. Use threading events to set the answer?
|
||||
char.clearSkills()
|
||||
for skillRow in resp["skills"]:
|
||||
char.addSkill(Skill(char, skillRow["skill_id"], skillRow["trained_skill_level"]))
|
||||
|
||||
sheet = auth.character(charID).CharacterSheet()
|
||||
charInfo = api.eve.CharacterInfo(characterID=charID)
|
||||
resp = sEsi.getSecStatus(char.ssoCharacterID)
|
||||
|
||||
char.secStatus = resp['security_status']
|
||||
|
||||
dbChar.apiUpdateCharSheet(sheet.skills, charInfo.securityStatus)
|
||||
self.callback[0](self.callback[1])
|
||||
except Exception:
|
||||
except Exception as ex:
|
||||
pyfalog.warn(ex)
|
||||
self.callback[0](self.callback[1], sys.exc_info())
|
||||
|
||||
133
service/esi.py
133
service/esi.py
@@ -2,12 +2,12 @@
|
||||
import wx
|
||||
from logbook import Logger
|
||||
import threading
|
||||
import copy
|
||||
import uuid
|
||||
import time
|
||||
import config
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
|
||||
import eos.db
|
||||
from eos.enum import Enum
|
||||
@@ -15,18 +15,13 @@ from eos.saveddata.ssocharacter import SsoCharacter
|
||||
import gui.globalEvents as GE
|
||||
from service.settings import CRESTSettings
|
||||
from service.server import StoppableHTTPServer, AuthHandler
|
||||
from service.pycrest.eve import EVE
|
||||
|
||||
from .esi_security_proxy import EsiSecurityProxy
|
||||
from esipy import EsiClient, EsiApp
|
||||
from esipy.cache import FileCache
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
server = "https://blitzmann.pythonanywhere.com"
|
||||
cache_path = os.path.join(config.savePath, config.ESI_CACHE)
|
||||
|
||||
if not os.path.exists(cache_path):
|
||||
@@ -34,32 +29,13 @@ if not os.path.exists(cache_path):
|
||||
|
||||
file_cache = FileCache(cache_path)
|
||||
|
||||
esiRdy = threading.Event()
|
||||
|
||||
|
||||
class Servers(Enum):
|
||||
TQ = 0
|
||||
SISI = 1
|
||||
|
||||
|
||||
class CrestModes(Enum):
|
||||
IMPLICIT = 0
|
||||
USER = 1
|
||||
|
||||
from utils.timer import Timer
|
||||
|
||||
|
||||
|
||||
class Esi(object):
|
||||
clientIDs = {
|
||||
Servers.TQ : 'f9be379951c046339dc13a00e6be7704',
|
||||
Servers.SISI: 'af87365240d644f7950af563b8418bad'
|
||||
}
|
||||
|
||||
# @todo: move this to settings
|
||||
clientCallback = 'http://localhost:6461'
|
||||
clientTest = True
|
||||
|
||||
esiapp = None
|
||||
esi_v1 = None
|
||||
esi_v4 = None
|
||||
@@ -68,12 +44,9 @@ class Esi(object):
|
||||
|
||||
@classmethod
|
||||
def initEsiApp(cls):
|
||||
with Timer("Main EsiApp") as t:
|
||||
cls.esiapp = EsiApp(cache=file_cache)
|
||||
with Timer('ESI v1') as t:
|
||||
cls.esi_v1 = cls.esiapp.get_v1_swagger
|
||||
with Timer('ESI v4') as t:
|
||||
cls.esi_v4 = cls.esiapp.get_v4_swagger
|
||||
cls.esiapp = EsiApp(cache=file_cache)
|
||||
cls.esi_v1 = cls.esiapp.get_v1_swagger
|
||||
cls.esi_v4 = cls.esiapp.get_v4_swagger
|
||||
|
||||
# esiRdy.set()
|
||||
|
||||
@@ -92,51 +65,16 @@ class Esi(object):
|
||||
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def restartService(cls):
|
||||
# This is here to reseed pycrest values when changing preferences
|
||||
# We first stop the server n case one is running, as creating a new
|
||||
# instance doesn't do this.
|
||||
if cls._instance.httpd:
|
||||
cls._instance.stopServer()
|
||||
cls._instance = Esi()
|
||||
cls._instance.mainFrame.updateCrestMenus(type=cls._instance.settings.get('mode'))
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
A note on login/logout events: the character login events happen
|
||||
whenever a characters is logged into via the SSO, regardless of mod.
|
||||
However, the mode should be send as an argument. Similarily,
|
||||
the Logout even happens whenever the character is deleted for either
|
||||
mode. The mode is sent as an argument, as well as the umber of
|
||||
characters still in the cache (if USER mode)
|
||||
"""
|
||||
Esi.initEsiApp()
|
||||
|
||||
|
||||
# prefetch = EsiInitThread()
|
||||
# prefetch.daemon = True
|
||||
# prefetch.start()
|
||||
|
||||
self.settings = CRESTSettings.getInstance()
|
||||
self.scopes = ['characterFittingsRead', 'characterFittingsWrite']
|
||||
|
||||
# these will be set when needed
|
||||
self.httpd = None
|
||||
self.state = None
|
||||
self.ssoTimer = None
|
||||
|
||||
self.eve_options = {
|
||||
'client_id': self.settings.get('clientID') if self.settings.get('mode') == CrestModes.USER else self.clientIDs.get(self.settings.get('server')),
|
||||
'api_key': self.settings.get('clientSecret') if self.settings.get('mode') == CrestModes.USER else None,
|
||||
'redirect_uri': self.clientCallback,
|
||||
'testing': self.isTestServer
|
||||
}
|
||||
|
||||
# Base EVE connection that is copied to all characters
|
||||
self.eve = EVE(**self.eve_options)
|
||||
|
||||
self.implicitCharacter = None
|
||||
|
||||
# The database cache does not seem to be working for some reason. Use
|
||||
@@ -151,73 +89,61 @@ class Esi(object):
|
||||
def isTestServer(self):
|
||||
return self.settings.get('server') == Servers.SISI
|
||||
|
||||
def delCrestCharacter(self, charID):
|
||||
char = eos.db.getSsoCharacter(charID)
|
||||
del self.charCache[char.ID]
|
||||
def delSsoCharacter(self, id):
|
||||
char = eos.db.getSsoCharacter(id)
|
||||
eos.db.remove(char)
|
||||
wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=len(self.charCache)))
|
||||
|
||||
def delAllCharacters(self):
|
||||
chars = eos.db.getSsoCharacters()
|
||||
for char in chars:
|
||||
eos.db.remove(char)
|
||||
self.charCache = {}
|
||||
wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0))
|
||||
|
||||
def getSsoCharacters(self):
|
||||
chars = eos.db.getSsoCharacters(config.getClientSecret())
|
||||
return chars
|
||||
|
||||
def getSsoCharacter(self, charID):
|
||||
def getSsoCharacter(self, id):
|
||||
"""
|
||||
Get character, and modify to include the eve connection
|
||||
"""
|
||||
char = eos.db.getSsoCharacter(charID, config.getClientSecret())
|
||||
if char.esi_client is None:
|
||||
char = eos.db.getSsoCharacter(id, config.getClientSecret())
|
||||
if char is not None and char.esi_client is None:
|
||||
char.esi_client = Esi.genEsiClient()
|
||||
char.esi_client.security.update_token(char.get_sso_data())
|
||||
return char
|
||||
|
||||
def getFittings(self, charID):
|
||||
char = self.getSsoCharacter(charID)
|
||||
print(repr(char))
|
||||
op = Esi.esi_v1.op['get_characters_character_id_fittings'](
|
||||
character_id=charID
|
||||
)
|
||||
def getSkills(self, id):
|
||||
char = self.getSsoCharacter(id)
|
||||
op = Esi.esi_v4.op['get_characters_character_id_skills'](character_id=char.characterID)
|
||||
resp = char.esi_client.request(op)
|
||||
return resp.data
|
||||
|
||||
def postFitting(self, charID, json_str):
|
||||
# @todo: new fitting ID can be recovered from resp.data,
|
||||
char = self.getSsoCharacter(charID)
|
||||
def getSecStatus(self, id):
|
||||
char = self.getSsoCharacter(id)
|
||||
op = Esi.esi_v4.op['get_characters_character_id'](character_id=char.characterID)
|
||||
resp = char.esi_client.request(op)
|
||||
return resp.data
|
||||
|
||||
def getFittings(self, id):
|
||||
char = self.getSsoCharacter(id)
|
||||
op = Esi.esi_v1.op['get_characters_character_id_fittings'](character_id=char.characterID)
|
||||
resp = char.esi_client.request(op)
|
||||
return resp.data
|
||||
|
||||
def postFitting(self, id, json_str):
|
||||
# @todo: new fitting ID can be recovered from resp.data,
|
||||
char = self.getSsoCharacter(id)
|
||||
op = Esi.esi_v1.op['post_characters_character_id_fittings'](
|
||||
character_id=char.characterID,
|
||||
fitting=json.loads(json_str)
|
||||
)
|
||||
|
||||
resp = char.esi_client.request(op)
|
||||
|
||||
return resp.data
|
||||
|
||||
def delFitting(self, charID, fittingID):
|
||||
char = self.getSsoCharacter(charID)
|
||||
print(repr(char))
|
||||
def delFitting(self, id, fittingID):
|
||||
char = self.getSsoCharacter(id)
|
||||
op = Esi.esi_v1.op['delete_characters_character_id_fittings_fitting_id'](
|
||||
character_id=charID,
|
||||
character_id=char.characterID,
|
||||
fitting_id=fittingID
|
||||
)
|
||||
|
||||
resp = char.esi_client.request(op)
|
||||
return resp.data
|
||||
|
||||
|
||||
def logout(self):
|
||||
"""Logout of implicit character"""
|
||||
pyfalog.debug("Character logout")
|
||||
self.implicitCharacter = None
|
||||
wx.PostEvent(self.mainFrame, GE.SsoLogout(type=self.settings.get('mode')))
|
||||
|
||||
def stopServer(self):
|
||||
pyfalog.debug("Stopping Server")
|
||||
self.httpd.stop()
|
||||
@@ -278,4 +204,3 @@ class Esi(object):
|
||||
|
||||
eos.db.save(currentCharacter)
|
||||
|
||||
wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) # todo: remove user / implicit authentication
|
||||
|
||||
1016
service/eveapi.py
1016
service/eveapi.py
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
version = "0.0.1"
|
||||
@@ -1,24 +0,0 @@
|
||||
import sys
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3: # pragma: no cover
|
||||
string_types = str,
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
else: # pragma: no cover
|
||||
string_types = str,
|
||||
text_type = str
|
||||
binary_type = str
|
||||
|
||||
|
||||
def text_(s, encoding='latin-1', errors='strict'): # pragma: no cover
|
||||
if isinstance(s, binary_type):
|
||||
return s.decode(encoding, errors)
|
||||
return s
|
||||
|
||||
|
||||
def bytes_(s, encoding='latin-1', errors='strict'): # pragma: no cover
|
||||
if isinstance(s, text_type):
|
||||
return s.encode(encoding, errors)
|
||||
return s
|
||||
@@ -1,2 +0,0 @@
|
||||
class APIException(Exception):
|
||||
pass
|
||||
@@ -1,318 +0,0 @@
|
||||
import base64
|
||||
from logbook import Logger
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import zlib
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
import config
|
||||
from service.pycrest.compat import bytes_, text_
|
||||
from service.pycrest.errors import APIException
|
||||
|
||||
from urllib.parse import urlparse, urlunparse, parse_qsl
|
||||
|
||||
try:
|
||||
import pickle
|
||||
except ImportError: # pragma: no cover
|
||||
# noinspection PyPep8Naming
|
||||
import pickle as pickle
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
cache_re = re.compile(r'max-age=([0-9]+)')
|
||||
|
||||
|
||||
class APICache(object):
|
||||
def put(self, key, value):
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def invalidate(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class FileCache(APICache):
|
||||
def __init__(self, path):
|
||||
self._cache = {}
|
||||
self.path = path
|
||||
if not os.path.isdir(self.path):
|
||||
os.mkdir(self.path, 0o700)
|
||||
|
||||
def _getpath(self, key):
|
||||
return os.path.join(self.path, str(hash(key)) + '.cache')
|
||||
|
||||
def put(self, key, value):
|
||||
with open(self._getpath(key), 'wb') as f:
|
||||
f.write(zlib.compress(pickle.dumps(value, -1)))
|
||||
self._cache[key] = value
|
||||
|
||||
def get(self, key):
|
||||
if key in self._cache:
|
||||
return self._cache[key]
|
||||
|
||||
try:
|
||||
with open(self._getpath(key), 'rb') as f:
|
||||
return pickle.loads(zlib.decompress(f.read()))
|
||||
except IOError as ex:
|
||||
pyfalog.debug("IO error opening zip file. (May not exist yet)")
|
||||
if ex.errno == 2: # file does not exist (yet)
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
def invalidate(self, key):
|
||||
self._cache.pop(key, None)
|
||||
|
||||
try:
|
||||
os.unlink(self._getpath(key))
|
||||
except OSError as ex:
|
||||
pyfalog.debug("Caught exception in invalidate")
|
||||
pyfalog.debug(ex)
|
||||
if ex.errno == 2: # does not exist
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class DictCache(APICache):
|
||||
def __init__(self):
|
||||
self._dict = {}
|
||||
|
||||
def get(self, key):
|
||||
return self._dict.get(key, None)
|
||||
|
||||
def put(self, key, value):
|
||||
self._dict[key] = value
|
||||
|
||||
def invalidate(self, key):
|
||||
self._dict.pop(key, None)
|
||||
|
||||
|
||||
class APIConnection(object):
|
||||
def __init__(self, additional_headers=None, user_agent=None, cache_dir=None, cache=None):
|
||||
# Set up a Requests Session
|
||||
session = requests.Session()
|
||||
if additional_headers is None:
|
||||
additional_headers = {}
|
||||
if user_agent is None:
|
||||
user_agent = "pyfa/{0} ({1})".format(config.version, config.tag)
|
||||
session.headers.update({
|
||||
"User-Agent": user_agent,
|
||||
"Accept": "application/json",
|
||||
})
|
||||
session.headers.update(additional_headers)
|
||||
session.mount('https://public-crest.eveonline.com', HTTPAdapter())
|
||||
self._session = session
|
||||
if cache:
|
||||
if isinstance(cache, APICache):
|
||||
self.cache = cache # Inherit from parents
|
||||
elif isinstance(cache, type):
|
||||
self.cache = cache() # Instantiate a new cache
|
||||
elif cache_dir:
|
||||
self.cache_dir = cache_dir
|
||||
self.cache = FileCache(self.cache_dir)
|
||||
else:
|
||||
self.cache = DictCache()
|
||||
|
||||
def get(self, resource, params=None):
|
||||
pyfalog.debug('Getting resource {0}', resource)
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
# remove params from resource URI (needed for paginated stuff)
|
||||
parsed_uri = urlparse(resource)
|
||||
qs = parsed_uri.query
|
||||
resource = urlunparse(parsed_uri._replace(query=''))
|
||||
prms = {}
|
||||
for tup in parse_qsl(qs):
|
||||
prms[tup[0]] = tup[1]
|
||||
|
||||
# params supplied to self.get() override parsed params
|
||||
for key in params:
|
||||
prms[key] = params[key]
|
||||
|
||||
# check cache
|
||||
key = (resource, frozenset(list(self._session.headers.items())), frozenset(list(prms.items())))
|
||||
cached = self.cache.get(key)
|
||||
if cached and cached['cached_until'] > time.time():
|
||||
pyfalog.debug('Cache hit for resource {0} (params={1})', resource, prms)
|
||||
return cached
|
||||
elif cached:
|
||||
pyfalog.debug('Cache stale for resource {0} (params={1})', resource, prms)
|
||||
self.cache.invalidate(key)
|
||||
else:
|
||||
pyfalog.debug('Cache miss for resource {0} (params={1})', resource, prms)
|
||||
|
||||
pyfalog.debug('Getting resource {0} (params={1})', resource, prms)
|
||||
res = self._session.get(resource, params=prms)
|
||||
if res.status_code != 200:
|
||||
raise APIException("Got unexpected status code from server: {0}" % res.status_code)
|
||||
|
||||
ret = res.json()
|
||||
|
||||
# cache result
|
||||
expires = self._get_expires(res)
|
||||
if expires > 0:
|
||||
ret.update({'cached_until': time.time() + expires})
|
||||
self.cache.put(key, ret)
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def _get_expires(response):
|
||||
if 'Cache-Control' not in response.headers:
|
||||
return 0
|
||||
if any([s in response.headers['Cache-Control'] for s in ['no-cache', 'no-store']]):
|
||||
return 0
|
||||
match = cache_re.search(response.headers['Cache-Control'])
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return 0
|
||||
|
||||
|
||||
class EVE(APIConnection):
|
||||
def __init__(self, **kwargs):
|
||||
self.api_key = kwargs.pop('api_key', None)
|
||||
self.client_id = kwargs.pop('client_id', None)
|
||||
self.redirect_uri = kwargs.pop('redirect_uri', None)
|
||||
if kwargs.pop('testing', False):
|
||||
self._public_endpoint = "http://public-crest-sisi.testeveonline.com/"
|
||||
self._authed_endpoint = "https://api-sisi.testeveonline.com/"
|
||||
self._image_server = "https://image.testeveonline.com/"
|
||||
self._oauth_endpoint = "https://sisilogin.testeveonline.com/oauth"
|
||||
else:
|
||||
self._public_endpoint = "https://public-crest.eveonline.com/"
|
||||
self._authed_endpoint = "https://crest-tq.eveonline.com/"
|
||||
self._image_server = "https://image.eveonline.com/"
|
||||
self._oauth_endpoint = "https://login.eveonline.com/oauth"
|
||||
self._endpoint = self._public_endpoint
|
||||
self._cache = {}
|
||||
self._data = None
|
||||
self.token = None
|
||||
self.refresh_token = None
|
||||
self.expires = None
|
||||
APIConnection.__init__(self, **kwargs)
|
||||
|
||||
def __call__(self):
|
||||
if not self._data:
|
||||
self._data = APIObject(self.get(self._endpoint), self)
|
||||
return self._data
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self._data.__getattr__(item)
|
||||
|
||||
def auth_uri(self, scopes=None, state=None):
|
||||
s = [] if not scopes else scopes
|
||||
grant_type = "token" if self.api_key is None else "code"
|
||||
|
||||
return "%s/authorize?response_type=%s&redirect_uri=%s&client_id=%s%s%s" % (
|
||||
self._oauth_endpoint,
|
||||
grant_type,
|
||||
self.redirect_uri,
|
||||
self.client_id,
|
||||
"&scope=%s" % '+'.join(s) if scopes else '',
|
||||
"&state=%s" % state if state else ''
|
||||
)
|
||||
|
||||
def _authorize(self, params):
|
||||
auth = text_(base64.b64encode(bytes_("%s:%s" % (self.client_id, self.api_key))))
|
||||
headers = {"Authorization": "Basic %s" % auth}
|
||||
res = self._session.post("%s/token" % self._oauth_endpoint, params=params, headers=headers)
|
||||
if res.status_code != 200:
|
||||
raise APIException("Got unexpected status code from API: %i" % res.status_code)
|
||||
return res.json()
|
||||
|
||||
def set_auth_values(self, res):
|
||||
self.__class__ = AuthedConnection
|
||||
self.token = res['access_token']
|
||||
self.refresh_token = res['refresh_token']
|
||||
self.expires = int(time.time()) + res['expires_in']
|
||||
self._endpoint = self._authed_endpoint
|
||||
self._session.headers.update({"Authorization": "Bearer %s" % self.token})
|
||||
|
||||
def authorize(self, code):
|
||||
res = self._authorize(params={"grant_type": "authorization_code", "code": code})
|
||||
self.set_auth_values(res)
|
||||
|
||||
def refr_authorize(self, refresh_token):
|
||||
res = self._authorize(params={"grant_type": "refresh_token", "refresh_token": refresh_token})
|
||||
self.set_auth_values(res)
|
||||
|
||||
def temptoken_authorize(self, access_token=None, expires_in=0, refresh_token=None):
|
||||
self.set_auth_values({'access_token': access_token,
|
||||
'refresh_token': refresh_token,
|
||||
'expires_in': expires_in})
|
||||
|
||||
|
||||
class AuthedConnection(EVE):
|
||||
def __call__(self):
|
||||
if not self._data:
|
||||
self._data = APIObject(self.get(self._endpoint), self)
|
||||
return self._data
|
||||
|
||||
def whoami(self):
|
||||
# if 'whoami' not in self._cache:
|
||||
# print "Setting this whoami cache"
|
||||
# self._cache['whoami'] = self.get("%s/verify" % self._oauth_endpoint)
|
||||
return self.get("%s/verify" % self._oauth_endpoint)
|
||||
|
||||
def get(self, resource, params=None):
|
||||
if self.refresh_token and int(time.time()) >= self.expires:
|
||||
self.refr_authorize(self.refresh_token)
|
||||
return super(self.__class__, self).get(resource, params)
|
||||
|
||||
def post(self, resource, data, params=None):
|
||||
if self.refresh_token and int(time.time()) >= self.expires:
|
||||
self.refr_authorize(self.refresh_token)
|
||||
return self._session.post(resource, data=data, params=params)
|
||||
|
||||
def delete(self, resource, params=None):
|
||||
if self.refresh_token and int(time.time()) >= self.expires:
|
||||
self.refr_authorize(self.refresh_token)
|
||||
return self._session.delete(resource, params=params)
|
||||
|
||||
|
||||
class APIObject(object):
|
||||
def __init__(self, parent, connection):
|
||||
self._dict = {}
|
||||
self.connection = connection
|
||||
for k, v in list(parent.items()):
|
||||
if type(v) is dict:
|
||||
self._dict[k] = APIObject(v, connection)
|
||||
elif type(v) is list:
|
||||
self._dict[k] = self._wrap_list(v)
|
||||
else:
|
||||
self._dict[k] = v
|
||||
|
||||
def _wrap_list(self, list_):
|
||||
new = []
|
||||
for item in list_:
|
||||
if type(item) is dict:
|
||||
new.append(APIObject(item, self.connection))
|
||||
elif type(item) is list:
|
||||
new.append(self._wrap_list(item))
|
||||
else:
|
||||
new.append(item)
|
||||
return new
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in self._dict:
|
||||
return self._dict[item]
|
||||
raise AttributeError(item)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
# Caching is now handled by APIConnection
|
||||
if 'href' in self._dict:
|
||||
return APIObject(self.connection.get(self._dict['href'], params=kwargs), self.connection)
|
||||
else:
|
||||
return self
|
||||
|
||||
def __str__(self): # pragma: no cover
|
||||
return self._dict.__str__()
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
return self._dict.__repr__()
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user