Skip to content

Commit 0445034

Browse files
committed
Merge branch 'master' into stable
2 parents bba46f1 + 87868ca commit 0445034

File tree

7 files changed

+112
-64
lines changed

7 files changed

+112
-64
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ Patches and Suggestions
2121
- jamslater
2222
- rjansen
2323
- ricklhp7
24+
- hunterjm

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Changelog
44
---------
55

66

7+
0.1.3 (2015-10-28)
8+
++++++++++++++++++
9+
* core: bump clientversion for android/ios emulation
10+
* core: add tradeStatus (thanks to hunterjm #161)
11+
* exceptions: add code, reason, string to FutError
12+
713
0.1.2 (2015-09-28)
814
++++++++++++++++++
915
* core: fix baseId calculation

fut/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"""
1919

2020
__title__ = 'fut'
21-
__version__ = '0.1.2'
21+
__version__ = '0.1.3'
2222
__author__ = 'Piotr Staroszczyk'
2323
__author_email__ = 'piotr.staroszczyk@get24.org'
2424
__license__ = 'GNU GPL v3'

fut/config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# chrome 45 @ win10
1+
# chrome 46 @ win10
22
headers = {
3-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36',
3+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36',
44
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
55
'Accept-Encoding': 'gzip,deflate,sdch',
66
'Accept-Language': 'en-US,en;q=0.8',
@@ -36,4 +36,6 @@
3636
# 'X-Requested-With': 'com.ea.fifaultimate_row', # ultimate app identifier?
3737
}
3838

39+
flash_agent = 'ShockwaveFlash/19.0.0.226'
40+
3941
cookies_file = 'cookies.txt'

fut/core.py

Lines changed: 83 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
except ImportError:
2121
import json
2222

23-
from .config import headers, headers_and, headers_ios, cookies_file
23+
from .config import headers, headers_and, headers_ios, flash_agent, cookies_file
2424
from .log import logger
2525
from .urls import urls
2626
from .exceptions import (FutError, ExpiredSession, InternalServerError,
2727
UnknownError, PermissionDenied, Captcha,
2828
Conflict, MaxSessions, MultipleSession,
29-
FeatureDisabled, doLoginFail)
29+
FeatureDisabled, doLoginFail, NoUltimateTeam)
3030
from .EAHashingAlgorithm import EAHashingAlgorithm
3131

3232

@@ -50,35 +50,16 @@ def baseId(resource_id, return_version=False):
5050
return abs(resource_id)
5151

5252

53-
def itemParse(item_data):
53+
def itemParse(item_data, full=True):
5454
"""Parser for item data. Returns nice dictionary."""
55-
return {
55+
# TODO: parse all data
56+
item_data = {
5657
'tradeId': item_data.get('tradeId'),
5758
'buyNowPrice': item_data.get('buyNowPrice'),
5859
'tradeState': item_data.get('tradeState'),
5960
'bidState': item_data.get('bidState'),
6061
'startingBid': item_data.get('startingBid'),
6162
'id': item_data['itemData']['id'],
62-
'timestamp': item_data['itemData']['timestamp'], # auction start
63-
'rating': item_data['itemData']['rating'],
64-
'assetId': item_data['itemData']['assetId'],
65-
'resourceId': item_data['itemData']['resourceId'],
66-
'itemState': item_data['itemData']['itemState'],
67-
'rareflag': item_data['itemData']['rareflag'],
68-
'formation': item_data['itemData']['formation'],
69-
'leagueId': item_data['itemData'].get('leagueId'),
70-
'injuryType': item_data['itemData'].get('injuryType'),
71-
'injuryGames': item_data['itemData']['injuryGames'],
72-
'lastSalePrice': item_data['itemData']['lastSalePrice'],
73-
'fitness': item_data['itemData']['fitness'],
74-
'training': item_data['itemData']['training'],
75-
'suspension': item_data['itemData']['suspension'],
76-
'contract': item_data['itemData']['contract'],
77-
# 'position': item_data['itemData']['preferredPosition'],
78-
'playStyle': item_data['itemData'].get('playStyle'), # used only for players
79-
'discardValue': item_data['itemData']['discardValue'],
80-
'itemType': item_data['itemData']['itemType'],
81-
'owners': item_data['itemData']['owners'],
8263
'offers': item_data.get('offers'),
8364
'currentBid': item_data.get('currentBid'),
8465
'expires': item_data.get('expires'), # seconds left
@@ -87,6 +68,32 @@ def itemParse(item_data):
8768
'sellerName': item_data.get('sellerName'),
8869
'watched': item_data.get('watched'),
8970
}
71+
if full:
72+
item_data.update({
73+
'timestamp': item_data['itemData']['timestamp'], # auction start
74+
'rating': item_data['itemData']['rating'],
75+
'assetId': item_data['itemData']['assetId'],
76+
'resourceId': item_data['itemData']['resourceId'],
77+
'itemState': item_data['itemData']['itemState'],
78+
'rareflag': item_data['itemData']['rareflag'],
79+
'formation': item_data['itemData']['formation'],
80+
'leagueId': item_data['itemData'].get('leagueId'),
81+
'injuryType': item_data['itemData'].get('injuryType'),
82+
'injuryGames': item_data['itemData']['injuryGames'],
83+
'lastSalePrice': item_data['itemData']['lastSalePrice'],
84+
'fitness': item_data['itemData']['fitness'],
85+
'training': item_data['itemData']['training'],
86+
'suspension': item_data['itemData']['suspension'],
87+
'contract': item_data['itemData']['contract'],
88+
# 'position': item_data['itemData']['preferredPosition'],
89+
'playStyle': item_data['itemData'].get('playStyle'), # used only for players
90+
'discardValue': item_data['itemData']['discardValue'],
91+
'itemType': item_data['itemData']['itemType'],
92+
'cardType': item_data['itemData'].get("cardsubtypeid"), # used only for cards
93+
'owners': item_data['itemData']['owners'],
94+
})
95+
return item_data
96+
9097

9198
''' # different urls (platforms)
9299
def cardInfo(resource_id):
@@ -96,6 +103,7 @@ def cardInfo(resource_id):
96103
return requests.get(url).json()
97104
'''
98105

106+
99107
# TODO: optimize messages, xml parser might be faster
100108
def nations():
101109
rc = requests.get(urls('pc')['messages']).content
@@ -105,6 +113,7 @@ def nations():
105113
nations[int(i[0])] = i[1]
106114
return nations
107115

116+
108117
def leagues(year=2016):
109118
rc = requests.get(urls('pc')['messages']).content
110119
data = re.findall('<trans-unit resname="global.leagueFull.%s.league([0-9]+)">\n <source>(.+)</source>' % year, rc)
@@ -113,6 +122,7 @@ def leagues(year=2016):
113122
leagues[int(i[0])] = i[1]
114123
return leagues
115124

125+
116126
def teams(year=2016):
117127
rc = requests.get(urls('pc')['messages']).content
118128
data = re.findall('<trans-unit resname="global.teamFull.%s.team([0-9]+)">\n <source>(.+)</source>' % year, rc)
@@ -121,6 +131,7 @@ def teams(year=2016):
121131
teams[int(i[0])] = i[1]
122132
return teams
123133

134+
124135
class Core(object):
125136
def __init__(self, email, passwd, secret_answer, platform='pc', code=None, emulate=None, debug=False, cookies=cookies_file):
126137
self.cookies_file = cookies # TODO: map self.cookies to requests.Session.cookies?
@@ -167,16 +178,16 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, emul
167178
game_sku = 'FFA16PS4'
168179
platform = 'ps3' # ps4 not available?
169180
else:
170-
raise FutError('Wrong platform. (Valid ones are pc/xbox/xbox360/ps3/ps4)')
181+
raise FutError(reason='Wrong platform. (Valid ones are pc/xbox/xbox360/ps3/ps4)')
171182
# if self.r.get(self.urls['main_site']+'/fifa/api/isUserLoggedIn').json()['isLoggedIn']:
172183
# return True # no need to log in again
173184
# emulate
174185
if emulate == 'ios':
175186
sku = 'FUT16IOS'
176-
clientVersion = 11
187+
clientVersion = 15
177188
elif emulate == 'and':
178189
sku = 'FUT16AND'
179-
clientVersion = 11
190+
clientVersion = 15
180191
# TODO: need more info about log in procedure in game
181192
# elif emulate == 'xbox':
182193
# sku = 'FFA16XBX' # FFA14CAP ?
@@ -191,7 +202,7 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, emul
191202
sku = 'FUT16WEB'
192203
clientVersion = 1
193204
else:
194-
raise FutError('Invalid emulate parameter. (Valid ones are and/ios).') # pc/ps3/xbox/
205+
raise FutError(reason='Invalid emulate parameter. (Valid ones are and/ios).') # pc/ps3/xbox/
195206
# === login
196207
self.urls['login'] = self.r.get(self.urls['fut_home']).url
197208
self.r.headers['Referer'] = self.urls['login'] # prepare headers
@@ -215,21 +226,21 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, emul
215226
# TODO: pick code from codes.txt?
216227
if not code:
217228
self.saveSession()
218-
raise FutError('Error during login process - code is required.')
229+
raise FutError(reason='Error during login process - code is required.')
219230
self.r.headers['Referer'] = url = rc.url
220231
# self.r.headers['Upgrade-Insecure-Requests'] = 1 # ?
221232
# self.r.headers['Origin'] = 'https://signin.ea.com'
222233
rc = self.r.post(url, {'twofactorCode': code, '_trustThisDevice': 'on', 'trustThisDevice': 'on', '_eventId': 'submit'}).text
223234
if 'Incorrect code entered' in rc or 'Please enter a valid security code' in rc:
224-
raise FutError('Error during login process - provided code is incorrect.')
235+
raise FutError(reason='Error during login process - provided code is incorrect.')
225236
self.logger.debug(rc)
226237
if 'Set Up an App Authenticator' in rc:
227238
rc = self.r.post(url.replace('s2', 's3'), {'_eventId': 'cancel', 'appDevice': 'IPHONE'}).text
228239
self.logger.debug(rc)
229240

230241
self.r.headers['Referer'] = self.urls['login']
231242
if self.r.get(self.urls['main_site'] + '/fifa/api/isUserLoggedIn').json()['isLoggedIn'] is not True: # TODO: parse error?
232-
raise FutError('Error during login process (probably invalid email, password or code).')
243+
raise FutError(reason='Error during login process (probably invalid email, password or code).')
233244
# TODO: catch invalid data exception
234245
# self.nucleus_id = re.search('userid : "([0-9]+)"', rc.text).group(1) # we'll get it later
235246

@@ -239,7 +250,7 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, emul
239250
self.logger.debug(rc.content)
240251
rc = rc.text
241252
# if 'EASW_ID' not in rc:
242-
# raise FutError('Error during login process (probably invalid email or password).')
253+
# raise FutError(reason='Error during login process (probably invalid email or password).')
243254
self.nucleus_id = re.search("var EASW_ID = '([0-9]+)';", rc).group(1)
244255
self.build_cl = re.search("var BUILD_CL = '([0-9]+)';", rc).group(1)
245256
# self.urls['fut_base'] = re.search("var BASE_FUT_URL = '(https://.+?)';", rc).group(1)
@@ -259,12 +270,17 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, emul
259270
})
260271
rc = self.r.get(self.urls['acc_info'], params={'_': int(time() * 1000)})
261272
self.logger.debug(rc.content)
262-
rc = rc.json()['userAccountInfo']['personas'][0]
263-
self.persona_id = rc['personaId']
264-
self.persona_name = rc['personaName']
265-
self.clubs = [i for i in rc['userClubList']]
266-
# sort clubs by lastAccessTime (latest first)
267-
self.clubs.sort(key=lambda i: i['lastAccessTime'], reverse=True)
273+
# pick persona (first valid for given game_sku)
274+
personas = rc.json()['userAccountInfo']['personas']
275+
for p in personas:
276+
# self.clubs = [i for i in p['userClubList']]
277+
# sort clubs by lastAccessTime (latest first)
278+
# self.clubs.sort(key=lambda i: i['lastAccessTime'], reverse=True)
279+
for c in p['userClubList']:
280+
if game_sku in c['skuAccessList']:
281+
self.persona_id = p['personaId']
282+
self.persona_name = p['personaName']
283+
break
268284

269285
# authorization
270286
self.r.headers.update({ # prepare headers
@@ -314,7 +330,7 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, emul
314330
rc = rc.json()
315331
if rc['string'] != 'OK': # we've got error
316332
if 'Answers do not match' in rc['reason']:
317-
raise FutError('Error during login process (invalid secret answer).')
333+
raise FutError(reason='Error during login process (invalid secret answer).')
318334
else:
319335
raise UnknownError
320336
self.r.headers['Content-Type'] = 'application/json'
@@ -326,7 +342,7 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, emul
326342
del self.r.headers['X-UT-Route']
327343
self.r.headers.update({
328344
# 'X-HTTP-Method-Override': 'GET', # __request__ method manages this
329-
'X-Requested-With': 'ShockwaveFlash/19.0.0.162',
345+
'X-Requested-With': flash_agent,
330346
'Referer': 'https://www.easports.com/iframe/fut16/bundles/futweb/web/flash/FifaUltimateTeam.swf',
331347
'Origin': 'https://www.easports.com',
332348
# 'Content-Type': 'application/json', # already set
@@ -366,20 +382,25 @@ def __request__(self, method, url, *args, **kwargs):
366382
rc = rc.json()
367383
# error control
368384
if 'code' and 'reason' in rc: # error
369-
if rc['reason'] == 'expired session':
370-
raise ExpiredSession
371-
elif rc.get('string') == 'Internal Server Error (ut)':
372-
raise InternalServerError
373-
elif rc.get('string') == 'Permission Denied':
374-
raise PermissionDenied
375-
elif rc.get('string') == 'Captcha Triggered':
385+
err_code = rc['code']
386+
err_reason = rc['reason']
387+
err_string = rc.get('string') # "human readable" reason?
388+
if err_reason == 'expired session': # code?
389+
raise ExpiredSession(err_code, err_reason, err_string)
390+
elif err_code == '500' or err_string == 'Internal Server Error (ut)':
391+
raise InternalServerError(err_code, err_reason, err_string)
392+
elif err_code == '489' or err_string == 'Feature Disabled':
393+
raise FeatureDisabled(err_code, err_reason, err_string)
394+
elif err_code == '465' or err_string == 'No User':
395+
raise NoUltimateTeam(err_code, err_reason, err_string)
396+
elif err_code == '461' or err_string == 'Permission Denied':
397+
raise PermissionDenied(err_code, err_reason, err_string)
398+
elif err_code == '459' or err_string == 'Captcha Triggered':
376399
# img = self.r.get(self.urls['fut_captcha_img'], params={'_': int(time()*1000), 'token': captcha_token}).content # doesnt work - check headers
377400
img = None
378-
raise Captcha(captcha_token, img)
379-
elif rc.get('string') == 'Conflict':
380-
raise Conflict
381-
elif rc.get('string') == 'Feature Disabled':
382-
raise FeatureDisabled
401+
raise Captcha(err_code, err_reason, err_string, captcha_token, img)
402+
elif err_code == '409' or err_string == 'Conflict':
403+
raise Conflict(err_code, err_reason, err_string)
383404
else:
384405
raise UnknownError(rc.__str__())
385406
self.saveSession()
@@ -515,7 +536,7 @@ def searchAuctions(self, ctype, level=None, category=None, assetId=None, defId=N
515536

516537
def bid(self, trade_id, bid):
517538
"""Make a bid."""
518-
rc = self.__get__(self.urls['fut']['TradeStatus'], params={'tradeIds': trade_id})['auctionInfo'][0]
539+
rc = self.tradeStatus(trade_id)[0]
519540
if rc['currentBid'] < bid and self.credits >= bid:
520541
data = {'bid': bid}
521542
url = '{0}/{1}/bid'.format(self.urls['fut']['PostBid'], trade_id)
@@ -556,6 +577,14 @@ def squads(self):
556577
return self.squad(squad_id='list')
557578
'''
558579

580+
def tradeStatus(self, trade_id):
581+
if not isinstance(trade_id, (list, tuple)):
582+
trade_id = (trade_id,)
583+
trade_id = (str(i) for i in trade_id)
584+
params = {'itemdata': 'true', 'tradeIds': ','.join(trade_id)}
585+
rc = self.__get__(self.urls['fut']['TradeStatus'], params=params)
586+
return [itemParse(i, full=False) for i in rc['auctionInfo']]
587+
559588
def tradepile(self):
560589
"""Returns items in tradepile."""
561590
rc = self.__get__(self.urls['fut']['TradePile'])
@@ -642,6 +671,7 @@ def pileSize(self):
642671

643672
def stats(self):
644673
"""Returns all stats."""
674+
# TODO: add self.urls['fut']['Stats']
645675
# won-draw-loss
646676
rc = self.__get__(self.urls['fut']['user'])
647677
data = {

fut/exceptions.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
class FutError(RuntimeError):
1313
"""There was an ambiguous exception that occurred while handling
1414
your request."""
15+
def __init__(self, code=None, reason=None, string=None):
16+
self.code = code
17+
self.reason = reason
18+
self.string = string
1519

1620

1721
class UnknownError(FutError):
@@ -24,18 +28,28 @@ class ExpiredSession(FutError):
2428
you should send at least one request every ~10 minutes."""
2529

2630

31+
class MaxSessions(FutError):
32+
"""[503] Service Unavailable (ut) - max session."""
33+
34+
2735
class InternalServerError(FutError):
2836
"""[500] Internal Server Error (ut). (invalid parameters?)"""
2937

3038

31-
class MaxSessions(FutError):
32-
"""[503] Service Unavailable (ut) - max session."""
39+
'''
40+
class InvalidCookie(FutError):
41+
"""[482] Invalid cookie."""
42+
'''
3343

3444

3545
class FeatureDisabled(FutError):
3646
"""[480] Feature Disabled."""
3747

3848

49+
class NoUltimateTeam(FutError):
50+
"""[465] No Ultimate Team."""
51+
52+
3953
class PermissionDenied(FutError):
4054
"""[461] Permission Denied. (outbid?)"""
4155

@@ -62,8 +76,3 @@ class MultipleSession(Unauthorized):
6276
# class doLoginFail(Forbidden):
6377
class doLoginFail(Unauthorized):
6478
"""[403] Forbidden (ut)."""
65-
66-
'''
67-
class InvalidCookie(FutError):
68-
"""[482] Invalid cookie."""
69-
'''

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
__title__ = 'fut'
10-
__version__ = '0.1.2'
10+
__version__ = '0.1.3'
1111
__author__ = 'Piotr Staroszczyk'
1212
__author_email__ = 'piotr.staroszczyk@get24.org'
1313
__license__ = 'GNU GPL v3'

0 commit comments

Comments
 (0)