Move over all esi stuff to it's own class, which the esi service extends from. Also fix an issue in the EVE fittings browser where deleting a fit didn't actually remove it from the list of fits (due to that list being populated by the return data of ESI, which can be cached). More clean up.

This commit is contained in:
blitzmann
2018-05-12 13:53:56 -04:00
parent 0365f71c00
commit 5cc6b6c69c
4 changed files with 239 additions and 217 deletions

View File

@@ -32,23 +32,11 @@ class SsoCharacter(object):
self.accessToken = accessToken
self.refreshToken = refreshToken
self.accessTokenExpires = None
self.esi_client = None
@reconstructor
def init(self):
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()
}
pass
def is_token_expired(self):
if self.accessTokenExpires is None:

View File

@@ -1,5 +1,3 @@
import time
import webbrowser
import json
# noinspection PyPackageRequirements
import wx
@@ -15,8 +13,8 @@ from gui.display import Display
import gui.globalEvents as GE
from logbook import Logger
import calendar
from service.esi import Esi, APIException
from service.esi import Esi
from service.esiAccess import APIException
from service.port import ESIExportException
pyfalog = Logger(__name__)
@@ -110,11 +108,11 @@ class EveFittings(wx.Frame):
waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self)
try:
fittings = sEsi.getFittings(self.getActiveCharacter())
self.fittings = sEsi.getFittings(self.getActiveCharacter())
# self.cacheTime = fittings.get('cached_until')
# self.updateCacheStatus(None)
# self.cacheTimer.Start(1000)
self.fitTree.populateSkillTree(fittings)
self.fitTree.populateSkillTree(self.fittings)
del waitDialog
except requests.exceptions.ConnectionError:
msg = "Connection error, please check your internet connection"
@@ -149,6 +147,9 @@ class EveFittings(wx.Frame):
if dlg.ShowModal() == wx.ID_YES:
try:
sEsi.delFitting(self.getActiveCharacter(), data['fitting_id'])
# repopulate the fitting list
self.fitTree.populateSkillTree(self.fittings)
self.fitView.update([])
except requests.exceptions.ConnectionError:
msg = "Connection error, please check your internet connection"
pyfalog.error(msg)
@@ -156,8 +157,9 @@ class EveFittings(wx.Frame):
class ESIExceptionHandler(object):
# todo: make this a generate excetpion handler for all calls
def __init__(self, parentWindow, ex):
if ex.response['error'] == "invalid_token":
if ex.response['error'].startswith('Token is not valid'):
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",
@@ -361,9 +363,13 @@ class FittingsTreeView(wx.Panel):
tree = self.fittingsTreeCtrl
tree.DeleteChildren(root)
sEsi = Esi.getInstance()
dict = {}
fits = data
for fit in fits:
if (fit['fitting_id'] in sEsi.fittings_deleted):
continue
ship = getItem(fit['ship_type_id'])
if ship.name not in dict:
dict[ship.name] = []

View File

@@ -2,82 +2,33 @@
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
from service.esiAccess import APIException
import gui.globalEvents as GE
from service.server import StoppableHTTPServer, AuthHandler
from service.settings import EsiSettings
from service.esiAccess import EsiAccess
import wx
from requests import Session
pyfalog = Logger(__name__)
from urllib.parse import urlencode
# todo: reimplement Caching for calls
# from esipy.cache import FileCache
# file_cache = FileCache(cache_path)
# cache_path = os.path.join(config.savePath, config.ESI_CACHE)
#
# if not os.path.exists(cache_path):
# os.mkdir(cache_path)
#
sso_url = "https://www.pyfa.io" # "https://login.eveonline.com" for actual login
esi_url = "https://esi.tech.ccp.is"
oauth_authorize = '%s/oauth/authorize' % sso_url
oauth_token = '%s/oauth/token' % sso_url
class APIException(Exception):
""" Exception for SSO related errors """
def __init__(self, url, code, json_response):
self.url = url
self.status_code = code
self.response = json_response
super(APIException, self).__init__(str(self))
def __str__(self):
if 'error' in self.response:
return 'HTTP Error %s: %s' % (self.status_code,
self.response['error'])
elif 'message' in self.response:
return 'HTTP Error %s: %s' % (self.status_code,
self.response['message'])
return 'HTTP Error %s' % (self.status_code)
class ESIEndpoints(Enum):
CHAR = "/v4/characters/{character_id}/"
CHAR_SKILLS = "/v4/characters/{character_id}/skills/" # prepend https://esi.evetech.net/
CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/"
CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/"
class Servers(Enum):
TQ = 0
SISI = 1
class LoginMethod(Enum):
SERVER = 0
MANUAL = 1
class Esi(object):
class Esi(EsiAccess):
_instance = None
@classmethod
@@ -88,6 +39,7 @@ class Esi(object):
return cls._instance
def __init__(self):
super().__init__()
self.settings = EsiSettings.getInstance()
# these will be set when needed
@@ -97,32 +49,14 @@ class Esi(object):
self.implicitCharacter = None
# until I can get around to making proper caching and modifications to said cache, storee deleted fittings here
# so that we can easily hide them in the fitting browser
self.fittings_deleted = set()
# need these here to post events
import gui.mainFrame # put this here to avoid loop
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
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': (
'pyfa v{}'.format(config.version)
)
})
def delSsoCharacter(self, id):
char = eos.db.getSsoCharacter(id, config.getClientSecret())
@@ -139,66 +73,35 @@ class Esi(object):
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)
resp = self.get(char, ESIEndpoints.CHAR_SKILLS, character_id=char.characterID)
# resp = self.check_response(char.esi_client.request(op))
resp = super().getSkills(char)
return resp.json()
def getSecStatus(self, id):
char = self.getSsoCharacter(id)
resp = self.get(char, ESIEndpoints.CHAR, character_id=char.characterID)
resp = super().getSecStatus(char)
return resp.json()
def getFittings(self, id):
char = self.getSsoCharacter(id)
resp = self.get(char, ESIEndpoints.CHAR_FITTINGS, character_id=char.characterID)
resp = super().getFittings(char)
return resp.json()
def postFitting(self, id, json_str):
# @todo: new fitting ID can be recovered from resp.data,
char = self.getSsoCharacter(id)
resp = self.post(char, ESIEndpoints.CHAR_FITTINGS, json_str, character_id=char.characterID)
resp = super().postFitting(char, json_str)
return resp.json()
def delFitting(self, id, fittingID):
char = self.getSsoCharacter(id)
self.delete(char, ESIEndpoints.CHAR_DEL_FIT, character_id=char.characterID, fitting_id=fittingID)
def check_response(self, resp):
# if resp.status >= 400:
# raise EsiException(resp.status)
return resp
@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())
super().delFitting(char, fittingID)
self.fittings_deleted.add(fittingID)
def login(self):
serverAddr = None
@@ -213,24 +116,6 @@ class Esi(object):
self.httpd.stop()
self.httpd = None
def getLoginURI(self, redirect=None):
self.state = str(uuid.uuid4())
args = {
'state': self.state,
'pyfa_version': config.version,
'login_method': self.settings.get('loginMode'),
'client_hash': config.getClientSecret()
}
if redirect is not None:
args['redirect'] = redirect
return '%s?%s' % (
oauth_authorize,
urlencode(args)
)
def startServer(self): # todo: break this out into two functions: starting the server, and getting the URI
pyfalog.debug("Starting server")
@@ -248,44 +133,6 @@ class Esi(object):
return 'http://localhost:{}'.format(port)
def get_oauth_header(self, token):
""" Return the Bearer Authorization header required in oauth calls
:return: a dict with the authorization header
"""
return {'Authorization': 'Bearer %s' % token}
def get_refresh_token_params(self, refreshToken):
""" 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 refreshToken is None:
raise AttributeError('No refresh token is defined.')
return {
'data': {
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
},
'url': self.oauth_token,
}
def refresh(self, ssoChar):
request_data = self.get_refresh_token_params(config.cipher.decrypt(ssoChar.refreshToken).decode())
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(ssoChar, json_res)
return json_res
def handleLogin(self, ssoInfo):
auth_response = json.loads(base64.b64decode(ssoInfo))
@@ -326,33 +173,3 @@ class Esi(object):
self.handleLogin(message['SSOInfo'][0])
def __before_request(self, ssoChar):
if ssoChar.is_token_expired():
json_response = self.refresh(ssoChar)
# AFTER_TOKEN_REFRESH.send(**json_response)
if ssoChar.accessToken is not None:
self._session.headers.update(self.get_oauth_header(ssoChar.accessToken))
def get(self, ssoChar, endpoint, *args, **kwargs):
self.__before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._session.get("{}{}".format(esi_url, endpoint))
# check for warnings, also status > 400
def post(self, ssoChar, endpoint, json, *args, **kwargs):
self.__before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._session.post("{}{}".format(esi_url, endpoint), data=json)
# check for warnings, also status > 400
def delete(self, ssoChar, endpoint, *args, **kwargs):
self.__before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._session.delete("{}{}".format(esi_url, endpoint))
# check for warnings, also status > 400

211
service/esiAccess.py Normal file
View File

@@ -0,0 +1,211 @@
# noinspection PyPackageRequirements
from logbook import Logger
import uuid
import time
import config
import datetime
from eos.enum import Enum
from eos.saveddata.ssocharacter import SsoCharacter
from service.settings import EsiSettings
from requests import Session
from urllib.parse import urlencode
pyfalog = Logger(__name__)
# todo: reimplement Caching for calls
# from esipy.cache import FileCache
# file_cache = FileCache(cache_path)
# cache_path = os.path.join(config.savePath, config.ESI_CACHE)
#
# if not os.path.exists(cache_path):
# os.mkdir(cache_path)
#
# todo: move these over to getters that automatically determine which endpoint we use.
sso_url = "https://www.pyfa.io" # "https://login.eveonline.com" for actual login
esi_url = "https://esi.tech.ccp.is"
oauth_authorize = '%s/oauth/authorize' % sso_url
oauth_token = '%s/oauth/token' % sso_url
class APIException(Exception):
""" Exception for SSO related errors """
def __init__(self, url, code, json_response):
self.url = url
self.status_code = code
self.response = json_response
super(APIException, self).__init__(str(self))
def __str__(self):
if 'error' in self.response:
return 'HTTP Error %s: %s' % (self.status_code,
self.response['error'])
elif 'message' in self.response:
return 'HTTP Error %s: %s' % (self.status_code,
self.response['message'])
return 'HTTP Error %s' % (self.status_code)
class ESIEndpoints(Enum):
CHAR = "/v4/characters/{character_id}/"
CHAR_SKILLS = "/v4/characters/{character_id}/skills/"
CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/"
CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/"
# class Servers(Enum):
# TQ = 0
# SISI = 1
class EsiAccess(object):
def __init__(self):
if sso_url is None or sso_url == "":
raise AttributeError("sso_url cannot be None or empty "
"without app parameter")
self.settings = EsiSettings.getInstance()
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': (
'pyfa v{}'.format(config.version)
)
})
def getSkills(self, char):
return self.get(char, ESIEndpoints.CHAR_SKILLS, character_id=char.characterID)
def getSecStatus(self, char):
return self.get(char, ESIEndpoints.CHAR, character_id=char.characterID)
def getFittings(self, char):
return self.get(char, ESIEndpoints.CHAR_FITTINGS, character_id=char.characterID)
def postFitting(self, char, json_str):
# @todo: new fitting ID can be recovered from resp.data,
return self.post(char, ESIEndpoints.CHAR_FITTINGS, json_str, character_id=char.characterID)
def delFitting(self, char, fittingID):
return self.delete(char, ESIEndpoints.CHAR_DEL_FIT, character_id=char.characterID, fitting_id=fittingID)
@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())
def getLoginURI(self, redirect=None):
self.state = str(uuid.uuid4())
args = {
'state': self.state,
'pyfa_version': config.version,
'login_method': self.settings.get('loginMode'),
'client_hash': config.getClientSecret()
}
if redirect is not None:
args['redirect'] = redirect
return '%s?%s' % (
oauth_authorize,
urlencode(args)
)
def get_oauth_header(self, token):
""" Return the Bearer Authorization header required in oauth calls
:return: a dict with the authorization header
"""
return {'Authorization': 'Bearer %s' % token}
def get_refresh_token_params(self, refreshToken):
""" 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 refreshToken is None:
raise AttributeError('No refresh token is defined.')
return {
'data': {
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
},
'url': self.oauth_token,
}
def refresh(self, ssoChar):
request_data = self.get_refresh_token_params(config.cipher.decrypt(ssoChar.refreshToken).decode())
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(ssoChar, json_res)
return json_res
def _before_request(self, ssoChar):
if ssoChar.is_token_expired():
pyfalog.info("Refreshing token for {}".format(ssoChar.characterName))
self.refresh(ssoChar)
if ssoChar.accessToken is not None:
self._session.headers.update(self.get_oauth_header(ssoChar.accessToken))
def _after_request(self, resp):
if ("warning" in resp.headers):
pyfalog.warn("{} - {}".format(resp.headers["warning"], resp.url))
if resp.status_code >= 400:
raise APIException(
resp.url,
resp.status_code,
resp.json()
)
return resp
def get(self, ssoChar, endpoint, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.get("{}{}".format(esi_url, endpoint)))
# check for warnings, also status > 400
def post(self, ssoChar, endpoint, json, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.post("{}{}".format(esi_url, endpoint), data=json))
# check for warnings, also status > 400
def delete(self, ssoChar, endpoint, *args, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.delete("{}{}".format(esi_url, endpoint)))
# check for warnings, also status > 400