Skip to content

Commit

Permalink
Merge branch 'master' into stable
Browse files Browse the repository at this point in the history
  • Loading branch information
oczkers committed Oct 28, 2017
2 parents 92feeb7 + 0943b32 commit 92dc8f2
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 64 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ fut.egg-info/
coverage.xml
cookies.txt
test.py
test_old.py
testemu.py
log.log
content.log
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ Changelog
---------


0.3.5 (2017-10-26)
^^^^^^^^^^^^^^^^^^

* various pinEvents improvements
* remove default buy now price for sell method to avoid mistakes
* add buyPack
* add objectives
* add duplicates list
* add level param to club method
* correct tradeStatus params
* check tradeStatus after selling item like webapp do
* add marketDataMaxPrice & marketDataMinPrice to item data parser

0.3.4 (2017-10-18)
^^^^^^^^^^^^^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion fut/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"""

__title__ = 'fut'
__version__ = '0.3.4'
__version__ = '0.3.5'
__author__ = 'Piotr Staroszczyk'
__author_email__ = 'piotr.staroszczyk@get24.org'
__license__ = 'GNU GPL v3'
Expand Down
132 changes: 79 additions & 53 deletions fut/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
UnknownError, PermissionDenied, Captcha,
Conflict, MaxSessions, MultipleSession,
Unauthorized, FeatureDisabled, doLoginFail,
NoUltimateTeam)
NoUltimateTeam, MarketLocked)
from .EAHashingAlgorithm import EAHashingAlgorithm


Expand Down Expand Up @@ -122,6 +122,8 @@ def itemParse(item_data, full=True):
'nation': item_data['itemData'].get('nation'), # nation_id?
'year': item_data['itemData'].get('resourceGameYear'), # alias
'resourceGameYear': item_data['itemData'].get('resourceGameYear'),
'marketDataMinPrice': item_data['itemData'].get('marketDataMinPrice'),
'marketDataMaxPrice': item_data['itemData'].get('marketDataMaxPrice'),
'count': item_data.get('count'), # consumables only (?)
'untradeableCount': item_data.get('untradeableCount'), # consumables only (?)
})
Expand Down Expand Up @@ -272,6 +274,7 @@ def playstyles(year=2018, timeout=timeout):
class Core(object):
def __init__(self, email, passwd, secret_answer, platform='pc', code=None, totp=None, sms=False, emulate=None, debug=False, cookies=cookies_file, timeout=timeout, delay=delay, proxies=None):
self.credits = 0
self.duplicates = []
self.cookies_file = cookies # TODO: map self.cookies to requests.Session.cookies?
self.timeout = timeout
self.delay = delay
Expand Down Expand Up @@ -567,11 +570,6 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp
if self._usermassinfo['settings']['configs'][2]['value'] == 0:
raise FutError(reason='Transfer market is probably disabled on this account.') # if tradingEnabled = 0

# pinEvents - boot_end
events = [self.pin.event('connection'),
self.pin.event('boot_end', end_reason='normal')]
self.pin.send(events)

# size of piles
piles = self.pileSize()
self.tradepile_size = piles['tradepile']
Expand All @@ -583,6 +581,11 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp
events = [self.pin.event('page_view', 'Hub - Home')]
self.pin.send(events)

# pinEvents - boot_end
events = [self.pin.event('connection'),
self.pin.event('boot_end', end_reason='normal')]
self.pin.send(events)

self.keepalive() # credits

# def __shards__(self):
Expand All @@ -609,7 +612,7 @@ def __request__(self, method, url, data={}, params={}, fast=False):
time.sleep(max(self.request_time - time.time() + random.randrange(self.delay[0], self.delay[1] + 1), 0)) # respect minimum delay
self.r.options(url, params=params)
else:
time.sleep(max(self.request_time - time.time() + 1.1, 0)) # respect 1s minimum delay between requests
time.sleep(max(self.request_time - time.time() + 1.3, 0)) # respect 1s minimum delay between requests
self.request_time = time.time() # save request time for delay calculations
if method.upper() == 'GET':
rc = self.r.get(url, data=data, params=params, timeout=self.timeout)
Expand All @@ -621,59 +624,45 @@ def __request__(self, method, url, data={}, params={}, fast=False):
rc = self.r.delete(url, data=data, params=params, timeout=self.timeout)
self.logger.debug("response: {0}".format(rc.content))
if not rc.ok: # status != 200
rcj = rc.json()
if rc.status_code == 429:
raise FutError('429 Too many requests')
if rc.status_code == 401:
# TODO?: send pinEvent https://gist.github.com/oczkers/7e5de70915b87262ddea961c49180fd6
print(rc.content)
raise ExpiredSession()
elif rc.status_code == 426:
raise FutError('426 Too many requests')
elif rc.status_code in (512, 521):
raise FutError('512/521 Temporary ban or just too many requests.')
elif rc.status_code == 461:
raise PermissionDenied(461) # You are not allowed to bid on this trade TODO: add code, reason etc
elif rc.status_code == 429:
raise FutError('429 Too many requests')
elif rc.status_code == 458:
print(rc.headers)
print(rc.status_code)
print(rc.cookies)
print(rc.content)
# pinEvents
events = [self.pin.event('error')]
self.pin.send(events)
raise Captcha()
elif rc.status_code == 401 and rcj['reason'] == 'expired session':
raise ExpiredSession(rcj['code'], rcj['reason'], rcj['message'])
elif rc.status_code == 460:
raise PermissionDenied(460)
elif rc.status_code == 461:
raise PermissionDenied(461) # You are not allowed to bid on this trade TODO: add code, reason etc
elif rc.status_code == 494:
raise MarketLocked()
elif rc.status_code in (512, 521):
raise FutError('512/521 Temporary ban or just too many requests.')
# it makes sense to print headers, status_code, etc. only when we don't know what happened
print(rc.headers)
print(rc.status_code)
print(rc.cookies)
print(rc.content)
raise UnknownError(rc.content)
# this whole error handling section might be moot now since they no longer return status_code = 200 when there's an error
# TODO: determine which of the errors (500, 489, 465, 461, 459, 401, 409) should actually be handled in the block above
if rc.text == '':
rc = {}
else:
captcha_token = rc.headers.get('Proxy-Authorization', '').replace('captcha=', '') # captcha token (always AAAA ?)
rc = rc.json()
# error control
if 'code' and 'reason' in rc: # error
err_code = rc['code']
err_reason = rc['reason']
err_string = rc.get('string') # "human readable" reason?
if err_reason == 'expired session': # code?
raise ExpiredSession(err_code, err_reason, err_string)
elif err_code == '500' or err_string == 'Internal Server Error (ut)':
raise InternalServerError(err_code, err_reason, err_string)
elif err_code == '489' or err_string == 'Feature Disabled':
raise FeatureDisabled(err_code, err_reason, err_string)
elif err_code == '465' or err_string == 'No User':
raise NoUltimateTeam(err_code, err_reason, err_string)
elif err_code == '461' or err_string == 'Permission Denied':
raise PermissionDenied(err_code, err_reason, err_string)
elif err_code == '459' or err_string == 'Captcha Triggered':
# img = self.r.get(self.urls['fut_captcha_img'], params={'_': int(time.time()*1000), 'token': captcha_token}, timeout=self.timeout).content # doesnt work - check headers
img = None
raise Captcha(err_code, err_reason, err_string, captcha_token, img)
elif err_code == '401' or err_string == 'Unauthorized':
raise Unauthorized(err_code, err_reason, err_string)
elif err_code == '409' or err_string == 'Conflict':
raise Conflict(err_code, err_reason, err_string)
else:
raise UnknownError(rc.__str__())
if 'credits' in rc and rc['credits']:
self.credits = rc['credits']
if 'duplicateItemIdList' in rc:
self.duplicates = [i['itemId'] for i in rc['duplicateItemIdList']]
self.saveSession()
return rc

Expand Down Expand Up @@ -828,8 +817,9 @@ def searchDefinition(self, asset_id, start=0, count=46):

def search(self, ctype, level=None, category=None, assetId=None, defId=None,
min_price=None, max_price=None, min_buy=None, max_buy=None,
league=None, club=None, position=None, zone=None, nationality=None, rare=False,
playStyle=None, start=0, page_size=16):
league=None, club=None, position=None, zone=None, nationality=None,
rare=False, playStyle=None, start=0, page_size=16,
fast=False):
"""Prepare search request, send and return parsed data as a dict.
:param ctype: [development / ? / ?] Card type.
Expand Down Expand Up @@ -858,7 +848,7 @@ def search(self, ctype, level=None, category=None, assetId=None, defId=None,
# pinEvents
if start == 0:
events = [self.pin.event('page_view', 'Transfer Market Search')]
self.pin.send(events)
self.pin.send(events, fast=fast)

# if start > 0 and page_size == 16:
# if not self.emulate: # wbeapp
Expand Down Expand Up @@ -890,12 +880,12 @@ def search(self, ctype, level=None, category=None, assetId=None, defId=None,
if rare: params['rare'] = 'SP'
if playStyle: params['playStyle'] = playStyle

rc = self.__request__(method, url, params=params) # TODO: catch 426 429 512 521 - temporary ban
rc = self.__request__(method, url, params=params, fast=fast)

# pinEvents
if start == 0:
events = [self.pin.event('page_view', 'Transfer Market Results - List View')]
self.pin.send(events)
self.pin.send(events, fast=fast)

return [itemParse(i) for i in rc.get('auctionInfo', ())]

Expand Down Expand Up @@ -928,12 +918,14 @@ def bid(self, trade_id, bid, fast=False):
else:
return False

def club(self, sort='desc', ctype='player', defId='', start=0, count=91):
def club(self, sort='desc', ctype='player', defId='', start=0, count=91, level=None):
"""Return items in your club, excluding consumables."""
method = 'GET'
url = 'club'

params = {'sort': sort, 'type': ctype, 'defId': defId, 'start': start, 'count': count}
if level:
params['level'] = level
rc = self.__request__(method, url, params=params)

# pinEvent
Expand Down Expand Up @@ -1018,7 +1010,7 @@ def tradeStatus(self, trade_id):
if not isinstance(trade_id, (list, tuple)):
trade_id = (trade_id,)
trade_id = (str(i) for i in trade_id)
params = {'itemdata': 'true', 'tradeIds': ','.join(trade_id)} # multiple trade_ids not tested
params = {'tradeIds': ','.join(trade_id)} # multiple trade_ids not tested
rc = self.__request__(method, url, params=params)
return [itemParse(i, full=False) for i in rc['auctionInfo']]

Expand Down Expand Up @@ -1061,12 +1053,12 @@ def unassigned(self):

return [itemParse({'itemData': i}) for i in rc.get('itemData', ())]

def sell(self, item_id, bid, buy_now=10000, duration=3600):
def sell(self, item_id, bid, buy_now, duration=3600, fast=False):
"""Start auction. Returns trade_id.
:params item_id: Item id.
:params bid: Stard bid.
:params buy_now: Buy now price (Default: 10000).
:params buy_now: Buy now price.
:params duration: Auction duration in seconds (Default: 3600).
"""
method = 'POST'
Expand All @@ -1075,6 +1067,8 @@ def sell(self, item_id, bid, buy_now=10000, duration=3600):
# TODO: auto send to tradepile
data = {'buyNowPrice': buy_now, 'startingBid': bid, 'duration': duration, 'itemData': {'id': item_id}}
rc = self.__request__(method, url, data=json.dumps(data), params={'sku_a': self.sku_a})
if not fast: # tradeStatus check like webapp do
self.tradeStatus(rc['id'])
return rc['id']

def quickSell(self, item_id):
Expand Down Expand Up @@ -1257,6 +1251,25 @@ def messages(self):
# url = '{0}/{1}'.format(self.urls['fut']['ActiveMessage'], message_id)
# self.__delete__(url)

def buyPack(self, pack_id, currency='COINS'):
# TODO: merge with openPack
method = 'POST'
url = 'purchased/items'

# pinEvents
events = [self.pin.event('page_view', 'Hub - Store')]
self.pin.send(events)

data = {'packId': pack_id,
'currency': currency}
rc = self.__request__(method, url, data=json.dumps(data))

# pinEvents
# events = [self.pin.event('page_view', 'Unassigned Items - List View')]
# self.pin.send(events)

return rc # TODO: parse response

def openPack(self, pack_id):
method = 'POST'
url = 'purchased/items'
Expand All @@ -1272,4 +1285,17 @@ def sbsSets(self):
url = 'sbs/sets'

rc = self.__request__(method, url)

# pinEvents
events = [self.pin.event('page_view', 'Hub - SBC')]
self.pin.send(events)

return rc # TODO?: parse

def objectives(self, scope='all'):
method = 'GET'
url = 'user/dynamicobjectives'

params = {'scope': scope}
rc = self.__request__(method, url, params=params)
return rc
6 changes: 6 additions & 0 deletions fut/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class InternalServerError(FutError):
"""[500] Internal Server Error (ut). (invalid parameters?)"""


class MarketLocked(FutError):
"""[494] If this is a new account, you need to unlock the transfer market
by playing games and completing the starter objectives.
If this is an older account, you may be banned from using the transfer market on the web app."""


'''
class InvalidCookie(FutError):
"""[482] Invalid cookie."""
Expand Down
25 changes: 16 additions & 9 deletions fut/pin.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ def __init__(self, sku=None, sid='', nucleus_id=0, persona_id='', dob=False, pla
self.custom = {"networkAccess": "W"} # wifi?
# TODO?: full boot process when there is no session (boot start)

self.custom['service_plat'] = platform
self.s = 4 # event id | 3 before "was sent" without session/persona/nucleus id so we can probably omit
self.custom['service_plat'] = platform[:3]
self.s = 2 # event id | before "was sent" without session/persona/nucleus id so we can probably omit

def __ts(self):
# TODO: add ability to random something
ts = datetime.now()
ts = datetime.utcnow()
ts = ts.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
return ts

Expand All @@ -64,9 +64,7 @@ def event(self, en, pgid=False, status=False, source=False, end_reason=False):
"pidm": {"nucleus": self.nucleus_id},
"didm": {"uuid": "0"}, # what is it?
"ts_event": self.__ts(),
"en": en},
'userid': self.persona_id, # not needed before session?
'type': 'utas'} # not needed before session?
"en": en}}
if self.dob: # date of birth yyyy-mm
data['core']['dob'] = self.dob
if pgid:
Expand All @@ -82,14 +80,22 @@ def event(self, en, pgid=False, status=False, source=False, end_reason=False):
if end_reason:
data['end_reason'] = end_reason

if en == 'page_view':
if en == 'login':
data['type'] = 'utas'
data['userid'] = self.persona_id
elif en == 'page_view':
data['type'] = 'menu'
elif en == 'error':
data['server_type'] = 'utas'
data['errid'] = 'server_error'
data['type'] = 'disconnect'
data['sid'] = self.sid

self.s += 1 # bump event id

return data

def send(self, events):
def send(self, events, fast=False):
time.sleep(0.5 + random() / 50)
data = {"taxv": self.taxv, # convert to float?
"tidt": self.tidt,
Expand All @@ -106,7 +112,8 @@ def send(self, events):
"custom": self.custom,
"events": events}
# print(data) # DEBUG
self.r.options(pin_url)
if not fast:
self.r.options(pin_url)
rc = self.r.post(pin_url, data=json.dumps(data)).json()
if rc['status'] != 'ok':
raise FutError('PinEvent is NOT OK, probably they changed something.')
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


__title__ = 'fut'
__version__ = '0.3.4'
__version__ = '0.3.5'
__author__ = 'Piotr Staroszczyk'
__author_email__ = 'piotr.staroszczyk@get24.org'
__license__ = 'GNU GPL v3'
Expand Down

0 comments on commit 92dc8f2

Please sign in to comment.