diff --git a/.gitignore b/.gitignore index 83f055c..3548ffa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ fut.egg-info/ .coverage coverage.xml cookies.txt +token.txt test.py test_old.py testemu.py @@ -18,3 +19,4 @@ test.log testemu.py screenlog.0 codecov.yml +club_players.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e580a03..1d75d4f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,18 @@ Changelog --------- +0.3.6 (2017-11-12) +^^^^^^^^^^^^^^^^^^ + +* add sbsSetChallenges (thanks to dan-gamble #330) +* readme polish (thanks to syndac) +* add tradepileClear +* add sbsSquad +* add sendToSbs +* add clubConsumables +* correct version param in pinevents +* save token between logins (maybe cookies are not needed?) + 0.3.5 (2017-10-26) ^^^^^^^^^^^^^^^^^^ diff --git a/README.rst b/README.rst index 63f1881..21f6172 100644 --- a/README.rst +++ b/README.rst @@ -50,6 +50,12 @@ Consumables database provided by koolaidjones: https://github.com/koolaidjones/F `Click here to get Slack invitation `_ +PHP ported version by InkedCurtis +--------------------------------- + +If You prefer php language, there is ported version made by InkedCurtis: https://github.com/InkedCurtis/FUT-API + + AutoBuyer GUI ------------- @@ -76,7 +82,9 @@ Optional parameters: .. code-block:: python >>> import fut - >>> fut = fut.Core('email', 'password', 'secret answer') + >>> session = fut.Core('email', 'password', 'secret answer') + +Be sure to set :code:`platform=` to your platform and :code:`sms=True` if you use SMS for 2 Factor Authentication. Search ------ @@ -103,7 +111,7 @@ Optional parameters: .. code-block:: python - >>> items = fut.searchAuctions('development') + >>> items = session.searchAuctions('development') Bid --- @@ -114,7 +122,7 @@ Optional parameters: .. code-block:: python - >>> fut.bid(items[0]['trade_id'], 600) + >>> session.bid(items[0]['trade_id'], 600) Sell ---- @@ -126,7 +134,8 @@ Optional parameters: .. code-block:: python - >>> fut.sell(item['item_id'], 150) + >>> session.sell(item['item_id'], 150) +Before selling a newly-bought item, you have to send it to the tradpile. :code:`session.sendToTradepile(item_id)` Quick sell ---------- @@ -136,14 +145,14 @@ single item: .. code-block:: python >>> item_id = 123456789 - >>> fut.quickSell(item_id) + >>> session.quickSell(item_id) multiple items: .. code-block:: python >>> item_id = [123456789, 987654321] - >>> fut.quickSell(item_id) + >>> session.quickSell(item_id) Piles (Watchlist / Tradepile / Unassigned / Squad / Club) --------------------------------------------------------- @@ -151,24 +160,24 @@ Piles (Watchlist / Tradepile / Unassigned / Squad / Club) .. code-block:: python - >>> items = fut.tradepile() - >>> items = fut.unassigned() - >>> items = fut.squad() - >>> items = fut.club(count=10, level=10, type=1, start=0) - >>> items = fut.clubConsumablesDetails() - >>> fut.sendToTradepile(trade_id, item_id) # add card to tradepile - >>> fut.sendToClub(trade_id, item_id) # add card to club - >>> fut.sendToWatchlist(trade_id) # add card to watchlist - >>> fut.tradepileDelete(trade_id) # removes item from tradepile - >>> fut.watchlistDelete(trade_id) # removes item from watch list (you can pass single str/ing or list/tuple of ids - like in quickSell) - - >>> fut.tradepile_size # tradepile size (slots) + >>> items = session.tradepile() + >>> items = session.unassigned() + >>> items = session.squad() + >>> items = session.club(count=10, level=10, type=1, start=0) + >>> items = session.clubConsumablesDetails() + >>> session.sendToTradepile(item_id) # add card to tradepile + >>> session.sendToClub(trade_id, item_id) # add card to club + >>> session.sendToWatchlist(trade_id) # add card to watchlist + >>> session.tradepileDelete(trade_id) # removes item from tradepile + >>> session.watchlistDelete(trade_id) # removes item from watch list (you can pass single str/ing or list/tuple of ids - like in quickSell) + + >>> session.tradepile_size # tradepile size (slots) 80 - >> len(fut.tradepile()) # tradepile fulfilment (number of cards in tradepile) + >> len(session.tradepile()) # tradepile fulfilment (number of cards in tradepile) 20 - >>> fut.watchlist_size # watchlist size (slots) + >>> session.watchlist_size # watchlist size (slots) 30 - >> len(fut.watchlist()) # watchlist fulfilment (number of cards in watchlist) + >> len(session.watchlist()) # watchlist fulfilment (number of cards in watchlist) 10 Credits @@ -178,7 +187,7 @@ It's cached on every request so if you want the most accurate info call fut.kepp .. code-block:: python - >>> fut.credits + >>> session.credits 600 Relist @@ -188,7 +197,7 @@ Relists all expired cards in tradepile. .. code-block:: python - >>> fut.relist() # relist all expired cards in tradepile + >>> session.relist() # relist all expired cards in tradepile Apply consumable ---------------- @@ -200,7 +209,7 @@ Apply consumable on player. .. code-block:: python - >>> fut.applyConsumable(item_id, resource_id) + >>> session.applyConsumable(item_id, resource_id) Card stats and definiction IDs ------------------------------ @@ -209,7 +218,7 @@ Returns stats and definition IDs for each card variation. .. code-block:: python - >>> fut.searchDefinition(asset_id, start=0, count=35) + >>> session.searchDefinition(asset_id, start=0, count=35) Keepalive --------- @@ -218,7 +227,7 @@ Sends keepalive ping and returns current credits amount (you have to make at lea .. code-block:: python - >>> fut.keepalive() + >>> session.keepalive() 650 Logout @@ -228,7 +237,7 @@ Logs out nicely (like clicking on logout button). .. code-block:: python - >>> fut.logout() + >>> session.logout() Database @@ -315,12 +324,26 @@ to be continued ;-) Problems -------- +Getting "requests.exceptions.SSLError:....'utas.mob.v4.fut.ea.com' doesn't match 'utas.mobapp.fut.ea.com'"? +^^^^ +This is a new error, but here's a temporary fix to try: + +1. Re-download the api from github +2. Go into fut/urls.py +3. On line 7, change :code:`auth_url = rc['authURL']` to :code:`auth_url = 'utas.mobapp.fut.ea.com'` +4. Run `python setup.py install` +5. Try your script again +6. **Please report in the Slack channel whether or not this worked!!** + + Bans ^^^^ To avoid getting ban take a look at our little discussion/guide thread: https://github.com/oczkers/fut/issues/259 +Generally speaking, you should send no more than 500 requests per hour and 5000 requests per day. Be somewhat human. If you encounter a captcha, try to answer/solve it as soon as possible. + Somehow i've sent card to full tradepile and it disappeared ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -328,7 +351,7 @@ Make space in tradepile and just call one command to restore it: .. code-block:: python - fut.sendToTradepile(-1, id) + session.sendToTradepile(-1, id) I've got card with None tradeId so cannot move/trade it @@ -338,7 +361,7 @@ Make space in tradepile and just call one command to restore it: .. code-block:: python - fut.sendToTradepile(-1, id) + session.sendToTradepile(-1, id) PermissionDenied exceptions raises when trying to sell cards directly from watchlist diff --git a/fut/__init__.py b/fut/__init__.py index ad36253..24e49b4 100644 --- a/fut/__init__.py +++ b/fut/__init__.py @@ -9,11 +9,11 @@ Basic usage: >>> import fut - >>> fifa = fut.Core('email', 'password', 'secret_answer') - >>> items = fut.searchAuctions('development') - >>> fut.bid(items[0]['trade_id'], 600) + >>> session = fut.Core('email', 'password', 'secret_answer') + >>> items = session.searchAuctions('development') + >>> session.bid(items[0]['trade_id'], 600) True - >>> fut.sell(item['item_id'], 150) + >>> session.sell(item['item_id'], 150) 1123321 @@ -23,7 +23,7 @@ """ __title__ = 'fut' -__version__ = '0.3.5' +__version__ = '0.3.6' __author__ = 'Piotr Staroszczyk' __author_email__ = 'piotr.staroszczyk@get24.org' __license__ = 'GNU GPL v3' diff --git a/fut/config.py b/fut/config.py index d9da9e2..4c9d0d1 100644 --- a/fut/config.py +++ b/fut/config.py @@ -38,5 +38,6 @@ cookies_file = 'cookies.txt' +token_file = 'token.txt' timeout = 15 # defaulf global timeout delay = (1, 3) # default mininum delay between requests (random range) diff --git a/fut/core.py b/fut/core.py index 13a0cd9..1102c7c 100644 --- a/fut/core.py +++ b/fut/core.py @@ -22,11 +22,12 @@ try: # python2 compatibility input = raw_input + FileNotFoundError except NameError: - pass + FileNotFoundError = IOError from .pin import Pin -from .config import headers, headers_and, headers_ios, cookies_file, timeout, delay +from .config import headers, headers_and, headers_ios, cookies_file, token_file, timeout, delay from .log import logger from .urls import client_id, auth_url, card_info_url, messages_url from .exceptions import (FutError, ExpiredSession, InternalServerError, @@ -126,6 +127,7 @@ def itemParse(item_data, full=True): 'marketDataMaxPrice': item_data['itemData'].get('marketDataMaxPrice'), 'count': item_data.get('count'), # consumables only (?) 'untradeableCount': item_data.get('untradeableCount'), # consumables only (?) + 'loans': item_data.get('loans'), }) if 'item' in item_data: # consumables only (?) return_data.update({ @@ -272,10 +274,11 @@ 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): + def __init__(self, email, passwd, secret_answer, platform='pc', code=None, totp=None, sms=False, emulate=None, debug=False, cookies=cookies_file, token=token_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.token_file = token self.timeout = timeout self.delay = delay self.request_time = 0 @@ -288,10 +291,92 @@ def __init__(self, email, passwd, secret_answer, platform='pc', code=None, totp= logger(save=debug) # init root logger self.logger = logger(__name__) # TODO: validate fut request response (200 OK) - self.__login__(email, passwd, secret_answer, platform=platform, code=code, totp=totp, sms=sms, emulate=emulate, proxies=proxies) + self.__launch__(email, passwd, secret_answer, platform=platform, code=code, totp=totp, sms=sms, emulate=emulate, proxies=proxies) - def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp=None, sms=False, emulate=None, proxies=None): - """Log in. + def __login__(self, email, passwd, code=None, totp=None, sms=False): + """Log in - needed only if we don't have access token or it's expired.""" + params = {'prompt': 'login', + 'accessToken': 'null', + 'client_id': client_id, + 'response_type': 'token', + 'display': 'web2/login', + 'locale': 'en_US', + 'redirect_uri': 'https://www.easports.com/fifa/ultimate-team/web-app/auth.html', + 'scope': 'basic.identity offline signin'} + self.r.headers['Referer'] = 'https://www.easports.com/fifa/ultimate-team/web-app/' + rc = self.r.get('https://accounts.ea.com/connect/auth', params=params, timeout=self.timeout) + # TODO: validate (captcha etc.) + if rc.url != 'https://www.easports.com/fifa/ultimate-team/web-app/auth.html': # redirect target # this check is probably not needed + self.r.headers['Referer'] = rc.url + # origin required? + data = {'email': email, + 'password': passwd, + 'country': 'US', # is it important? + 'phoneNumber': '', # TODO: add phone code verification + 'passwordForPhone': '', + 'gCaptchaResponse': '', + 'isPhoneNumberLogin': 'false', # TODO: add phone login + 'isIncompletePhone': '', + '_rememberMe': 'on', + 'rememberMe': 'on', + '_eventId': 'submit'} + rc = self.r.post(rc.url, data=data, timeout=self.timeout) + # rc = rc.text + + if "'successfulLogin': false" in rc.text: + failedReason = re.search('general-error">\s+
\s+
\s+(.*)\s.+', rc.text).group(1) + # Your credentials are incorrect or have expired. Please try again or reset your password. + raise FutError(reason=failedReason) + + if 'var redirectUri' in rc.text: + rc = self.r.get(rc.url, params={'_eventId': 'end'}) # initref param was missing here + + ''' # pops out only on first launch + if 'FIFA Ultimate Team needs to update your Account to help protect your gameplay experience.' in rc: # request email/sms code + self.r.headers['Referer'] = rc.url # s2 + rc = self.r.post(rc.url.replace('s2', 's3'), {'_eventId': 'submit'}, timeout=self.timeout).content + self.r.headers['Referer'] = rc.url # s3 + rc = self.r.post(rc.url, {'twofactorType': 'EMAIL', 'country': 0, 'phoneNumber': '', '_eventId': 'submit'}, timeout=self.timeout) + ''' + + # click button to send code + if 'Login Verification' in rc.text: # click button to get code sent + if totp: + rc = self.r.post(rc.url, {'_eventId': 'submit', 'codeType': 'APP'}) + code = pyotp.TOTP(totp).now() + elif sms: + rc = self.r.post(rc.url, {'_eventId': 'submit', 'codeType': 'SMS'}) + else: # email + rc = self.r.post(rc.url, {'_eventId': 'submit', 'codeType': 'EMAIL'}) + + # if 'We sent a security code to your' in rc.text or 'Your security code was sent to' in rc.text or 'Enter the 6-digit verification code' in rc.text or 'We have sent a security code' in rc.text: # post code + if 'Enter your security code' in rc.text: + # TODO: 'We sent a security code to your email' / 'We sent a security code to your ?' + # TODO: pick code from codes.txt? + if not code: + # self.saveSession() + # raise FutError(reason='Error during login process - code is required.') + code = input('Enter code: ') + self.r.headers['Referer'] = url = rc.url + # self.r.headers['Upgrade-Insecure-Requests'] = '1' # ? + # self.r.headers['Origin'] = 'https://signin.ea.com' + rc = self.r.post(url.replace('s3', 's4'), {'oneTimeCode': code, '_trustThisDevice': 'on', 'trustThisDevice': 'on', '_eventId': 'submit'}, timeout=self.timeout) + # rc = rc.text + if 'Incorrect code entered' in rc.text or 'Please enter a valid security code' in rc.text: + raise FutError(reason='Error during login process - provided code is incorrect.') + if 'Set Up an App Authenticator' in rc.text: # may we drop this? + rc = self.r.post(url.replace('s3', 's4'), {'_eventId': 'cancel', 'appDevice': 'IPHONE'}, timeout=self.timeout) + # rc = rc.text + + rc = re.match('https://www.easports.com/fifa/ultimate-team/web-app/auth.html#access_token=(.+?)&token_type=(.+?)&expires_in=[0-9]+', rc.url) + self.access_token = rc.group(1) + self.token_type = rc.group(2) + # TODO?: refresh after expires_in + + self.saveSession() + + def __launch__(self, email, passwd, secret_answer, platform='pc', code=None, totp=None, sms=False, emulate=None, proxies=None): + """Launch futweb :params email: Email. :params passwd: Password. @@ -314,6 +399,11 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp # load saved cookies/session if self.cookies_file: self.r.cookies = LWPCookieJar(self.cookies_file) + try: + with open(self.token_file, 'r') as f: + self.token_type, self.access_token = f.readline().replace('\n', '').replace('\r', '').split(' ') # removing \n \r just to make sure + except FileNotFoundError: + self.__login__(email=email, passwd=passwd, code=code, totp=totp, sms=sms) try: self.r.cookies.load(ignore_discard=True) # is it good idea to load discarded cookies after long time? except IOError: @@ -366,85 +456,20 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp raise FutError(reason='Invalid emulate parameter. (Valid ones are and/ios).') # pc/ps3/xbox/ self.sku = sku # TODO: use self.sku in all class self.sku_a = 'FFT18' - params = {'prompt': 'login', - 'accessToken': 'null', - 'client_id': client_id, + + # === launch futweb + params = {'accessToken': self.access_token, + 'client_id': 'FIFA-18-WEBCLIENT', 'response_type': 'token', 'display': 'web2/login', 'locale': 'en_US', 'redirect_uri': 'https://www.easports.com/fifa/ultimate-team/web-app/auth.html', 'scope': 'basic.identity offline signin'} - self.r.headers['Referer'] = 'https://www.easports.com/fifa/ultimate-team/web-app/' - rc = self.r.get('https://accounts.ea.com/connect/auth', params=params, timeout=self.timeout) - # TODO: validate (captcha etc.) - if rc.url != 'https://www.easports.com/fifa/ultimate-team/web-app/auth.html': # redirect target # TODO: move (only?) this to separate method - login and rename __login__ to launch - self.r.headers['Referer'] = rc.url - # origin required? - data = {'email': email, - 'password': passwd, - 'country': 'US', # is it important? - 'phoneNumber': '', # TODO: add phone code verification - 'passwordForPhone': '', - 'gCaptchaResponse': '', - 'isPhoneNumberLogin': 'false', # TODO: add phone login - 'isIncompletePhone': '', - '_rememberMe': 'on', - 'rememberMe': 'on', - '_eventId': 'submit'} - rc = self.r.post(rc.url, data=data, timeout=self.timeout) - # rc = rc.text - - if "'successfulLogin': false" in rc.text: - failedReason = re.search('general-error">\s+
\s+
\s+(.*)\s.+', rc.text).group(1) - # Your credentials are incorrect or have expired. Please try again or reset your password. - raise FutError(reason=failedReason) - - if 'var redirectUri' in rc.text: - rc = self.r.get(rc.url, params={'_eventId': 'end'}) # initref param was missing here - - ''' # pops out only on first launch - if 'FIFA Ultimate Team needs to update your Account to help protect your gameplay experience.' in rc: # request email/sms code - self.r.headers['Referer'] = rc.url # s2 - rc = self.r.post(rc.url.replace('s2', 's3'), {'_eventId': 'submit'}, timeout=self.timeout).content - self.r.headers['Referer'] = rc.url # s3 - rc = self.r.post(rc.url, {'twofactorType': 'EMAIL', 'country': 0, 'phoneNumber': '', '_eventId': 'submit'}, timeout=self.timeout) - ''' - - # click button to send code - if 'Login Verification' in rc.text: # click button to get code sent - if totp: - rc = self.r.post(rc.url, {'_eventId': 'submit', 'codeType': 'APP'}) - code = pyotp.TOTP(totp).now() - elif sms: - rc = self.r.post(rc.url, {'_eventId': 'submit', 'codeType': 'SMS'}) - else: # email - rc = self.r.post(rc.url, {'_eventId': 'submit', 'codeType': 'EMAIL'}) - - # if 'We sent a security code to your' in rc.text or 'Your security code was sent to' in rc.text or 'Enter the 6-digit verification code' in rc.text or 'We have sent a security code' in rc.text: # post code - if 'Enter your security code' in rc.text: - # TODO: 'We sent a security code to your email' / 'We sent a security code to your ?' - # TODO: pick code from codes.txt? - if not code: - # self.saveSession() - # raise FutError(reason='Error during login process - code is required.') - code = input('Enter code: ') - self.r.headers['Referer'] = url = rc.url - # self.r.headers['Upgrade-Insecure-Requests'] = '1' # ? - # self.r.headers['Origin'] = 'https://signin.ea.com' - rc = self.r.post(url.replace('s3', 's4'), {'oneTimeCode': code, '_trustThisDevice': 'on', 'trustThisDevice': 'on', '_eventId': 'submit'}, timeout=self.timeout) - # rc = rc.text - if 'Incorrect code entered' in rc.text or 'Please enter a valid security code' in rc.text: - raise FutError(reason='Error during login process - provided code is incorrect.') - if 'Set Up an App Authenticator' in rc.text: # may we drop this? - rc = self.r.post(url.replace('s3', 's4'), {'_eventId': 'cancel', 'appDevice': 'IPHONE'}, timeout=self.timeout) - # rc = rc.text + rc = self.r.get('https://accounts.ea.com/connect/auth', params=params) + rc = re.match('https://www.easports.com/fifa/ultimate-team/web-app/auth.html#access_token=(.+?)&token_type=(.+?)&expires_in=[0-9]+', rc.url) + self.access_token = rc.group(1) + self.token_type = rc.group(2) - rc = re.match('https://www.easports.com/fifa/ultimate-team/web-app/auth.html#access_token=(.+?)&token_type=(.+?)&expires_in=[0-9]+', rc.url) - access_token = rc.group(1) - token_type = rc.group(2) - - # === launch futweb - # TODO!: move to separate method __launch__, do not call login when not necessary # self.r.headers['Referer'] = 'https://www.easports.com/fifa/ultimate-team/web-app/auth.html' rc = self.r.get('https://www.easports.com/fifa/ultimate-team/web-app/', timeout=self.timeout).text # year = re.search('fut_year = "([0-9]{4}])"', rc).group(1) # use this to construct urls, sku etc. @@ -452,8 +477,12 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp # TODO: config self.r.headers['Referer'] = 'https://www.easports.com/fifa/ultimate-team/web-app/' self.r.headers['Accept'] = 'application/json' - self.r.headers['Authorization'] = '%s %s' % (token_type, access_token) - rc = self.r.get('https://gateway.ea.com/proxy/identity/pids/me').json() + self.r.headers['Authorization'] = '%s %s' % (self.token_type, self.access_token) + rc = self.r.get('https://gateway.ea.com/proxy/identity/pids/me').json() # TODO: validate response + if rc.get('error') == 'invalid_access_token': + print('invalid token') + self.__login__(email=email, passwd=passwd, totp=totp, sms=sms) + return self.__launch__(email=email, passwd=passwd, secret_answer=secret_answer, platform=platform, code=code, totp=totp, sms=sms, emulate=emulate, proxies=proxies) self.nucleus_id = rc['pid']['externalRefValue'] # or pidId self.dob = rc['pid']['dob'] # tos_version = rc['tosVersion'] @@ -504,7 +533,7 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp params = {'client_id': 'FOS-SERVER', # i've seen in some js/json response but cannot find now 'redirect_uri': 'nucleus:rest', 'response_type': 'code', - 'access_token': access_token} + 'access_token': self.access_token} rc = self.r.get('https://accounts.ea.com/connect/auth', params=params).json() auth_code = rc['code'] @@ -537,11 +566,6 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp raise UnknownError(rc.__str__()) self.r.headers['X-UT-SID'] = self.sid = rc['sid'] - # init pin - self.pin = Pin(sid=self.sid, nucleus_id=self.nucleus_id, persona_id=self.persona_id, dob=self.dob[:-3], platform=platform) - events = [self.pin.event('login', status='success')] - self.pin.send(events) - # validate (secret question) self.r.headers['Easw-Session-Data-Nucleus-Id'] = self.nucleus_id rc = self.r.get('https://%s/ut/game/fifa18/phishing/question' % self.fut_host, params={'_': self._}, timeout=self.timeout).json() @@ -562,6 +586,11 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp self._ += 1 self.r.headers['X-UT-PHISHING-TOKEN'] = self.token = rc['token'] + # init pin + self.pin = Pin(sid=self.sid, nucleus_id=self.nucleus_id, persona_id=self.persona_id, dob=self.dob[:-3], platform=platform) + events = [self.pin.event('login', status='success')] + self.pin.send(events) + # get basic user info # TODO: parse usermassinfo and change _usermassinfo to userinfo # TODO?: usermassinfo as separate method && ability to refresh piles etc. @@ -575,6 +604,16 @@ def __login__(self, email, passwd, secret_answer, platform='pc', code=None, totp self.tradepile_size = piles['tradepile'] self.watchlist_size = piles['watchlist'] + # refresh token + # params = {'response_type': 'token', + # 'redirect_uri': 'nucleus:rest', + # 'prompt': 'none', + # 'client_id': 'ORIGIN_JS_SDK'} + # rc = self.r.get('https://accounts.ea.com/connect/auth', params=params).json() + # self.access_token = rc['access_token'] + # self.token_type = rc['token_type'] + # expired_in + self.saveSession() # pinEvents - home screen @@ -612,7 +651,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.3, 0)) # respect 1s minimum delay between requests + time.sleep(max(self.request_time - time.time() + 1.4, 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) @@ -695,6 +734,7 @@ def __sendToPile__(self, pile, trade_id=None, item_id=None): self.logger.info("{0} (itemId: {1}) moved to {2} Pile".format(trade_id, item_id, pile)) else: self.logger.error("{0} (itemId: {1}) NOT MOVED to {2} Pile. REASON: {3}".format(trade_id, item_id, pile, rc['itemData'][0]['reason'])) + # if rc['itemData'][0]['reason'] == 'Duplicate Item Type' and rc['itemData'][0]['errorCode'] == 472: # errorCode check is enought? return rc['itemData'][0]['success'] def logout(self, save=True): @@ -769,6 +809,8 @@ def saveSession(self): """Save cookies/session.""" if self.cookies_file: self.r.cookies.save(ignore_discard=True) + with open(self.token_file, 'w') as f: + f.write('%s %s' % (self.token_type, self.access_token)) def baseId(self, *args, **kwargs): """Calculate base id and version from a resource id.""" @@ -932,10 +974,12 @@ def club(self, sort='desc', ctype='player', defId='', start=0, count=91, level=N if start == 0: if ctype == 'player': pgid = 'Club - Players - List View' - elif ctype == 'item': - pgid = 'Club - Club Items - List View' - else: # TODO: THIS IS WRONG, detect all ctypes + elif ctype == 'staff': + pgid = 'Club - Staff - List View' + elif ctype in ('item', 'kit', 'ball', 'badge', 'stadium'): pgid = 'Club - Club Items - List View' + # else: # TODO: THIS IS probably WRONG, detect all ctypes + # pgid = 'Club - Club Items - List View' events = [self.pin.event('page_view', pgid)] self.pin.send(events) @@ -949,27 +993,21 @@ def clubStaff(self): rc = self.__request__(method, url) return rc # TODO?: parse - # def clubConsumables(self): - # """Return all consumables stats in dictionary.""" - # rc = self.__get__(self.urls['fut']['ClubConsumableSearch']) # or ClubConsumableStats? - # consumables = {} - # for i in rc: - # if i['contextValue'] == 1: - # level = 'gold' - # elif i['contextValue'] == 2: - # level = 'silver' - # elif i['contextValue'] == 3: - # level = 'bronze' - # consumables[i['type']] = {'level': level, - # 'type': i['type'], # need list of all types - # 'contextId': i['contextId'], # dunno what is it - # 'count': i['typeValue']} - # return consumables - - # def clubConsumablesDetails(self): - # """Return all consumables details.""" - # rc = self.__get__('{0}{1}'.format(self.urls['fut']['ClubConsumableSearch'], '/development')) - # return [{itemParse(i) for i in rc.get('itemData', ())}] + def clubConsumables(self, fast=False): + """Return all consumables from club.""" + method = 'GET' + url = 'club/consumables/development' + + rc = self.__request__(method, url) + + events = [self.pin.event('page_view', 'Hub - Club')] + self.pin.send(events, fast=fast) + events = [self.pin.event('page_view', 'Club - Consumables')] + self.pin.send(events, fast=fast) + events = [self.pin.event('page_view', 'Club - Consumables - List View')] + self.pin.send(events, fast=fast) + + return [{itemParse(i) for i in rc.get('itemData', ())}] def squad(self, squad_id=0, persona_id=None): """Return a squad. @@ -1113,6 +1151,14 @@ def tradepileDelete(self, trade_id): # item_id instead of trade_id? # TODO: validate status code return True + def tradepileClear(self): + """Removes all sold items from tradepile.""" + method = 'DELETE' + url = 'trade/sold' + + self.__request__(method, url) + # return True + def sendToTradepile(self, item_id, safe=True): """Send to tradepile (alias for __sendToPile__). @@ -1140,7 +1186,35 @@ def sendToWatchlist(self, trade_id): data = {'auctionInfo': [{'id': trade_id}]} return self.__request__(method, url, data=json.dumps(data)) - # + + def sendToSbs(self, challenge_id, item_id): + """Send card FROM CLUB to first free slot in sbs squad.""" + # TODO?: multiple item_ids + method = 'PUT' + url = 'sbs/challenge/%s/squad' % challenge_id + + squad = self.sbsSquad(challenge_id) + players = [] + moved = False + n = 0 + for i in squad['squad']['players']: + if i['itemData']['id'] == item_id: # item already in sbs # TODO?: report reason + return False + if i['itemData']['id'] == 0 and not moved: + i['itemData']['id'] = item_id + moved = True + players.append({"index": n, + "itemData": {"id": i['itemData']['id'], + "dream": False}}) + n += 1 + data = {'players': players} + + if not moved: + return False + else: + self.__request__(method, url, data=json.dumps(data)) + return True + # def relist(self, clean=False): # """Relist all tradepile. Returns True or number of deleted (sold) if clean was set. # @@ -1292,6 +1366,30 @@ def sbsSets(self): return rc # TODO?: parse + def sbsSetChallenges(self, set_id): + method = 'GET' + url = 'sbs/setId/%s/challenges' % set_id + + rc = self.__request__(method, url) + + # pinEvents + events = [self.pin.event('page_view', 'SBC - Challenges')] + self.pin.send(events) + + return rc # TODO?: parse + + def sbsSquad(self, challenge_id): + method = 'GET' + url = 'sbs/challenge/%s/squad' % challenge_id + + rc = self.__request__(method, url) + + # pinEvents + events = [self.pin.event('page_view', 'SBC - Squad')] + self.pin.send(events) + + return rc + def objectives(self, scope='all'): method = 'GET' url = 'user/dynamicobjectives' diff --git a/fut/pin.py b/fut/pin.py index 6cbf70c..77934ec 100644 --- a/fut/pin.py +++ b/fut/pin.py @@ -16,7 +16,7 @@ from datetime import datetime from fut.config import headers -from fut.urls import pin_url, v +from fut.urls import pin_url from fut.exceptions import FutError @@ -36,6 +36,7 @@ def __init__(self, sku=None, sid='', nucleus_id=0, persona_id='', dob=False, pla self.plat = re.search('plat:"(.+?)"', rc).group(1) self.et = re.search('et:"(.+?)"', rc).group(1) self.pidt = re.search('pidt:"(.+?)"', rc).group(1) + self.v = re.search('APP_VERSION="([0-9\.]+)"', rc).group(1) self.r = requests.Session() self.r.headers = headers @@ -101,7 +102,7 @@ def send(self, events, fast=False): "tidt": self.tidt, "tid": self.sku, "rel": self.rel, - "v": v, + "v": self.v, "ts_post": self.__ts(), "sid": self.sid, "gid": self.gid, # convert to int? diff --git a/fut/urls.py b/fut/urls.py index 983dbc7..a9cc214 100644 --- a/fut/urls.py +++ b/fut/urls.py @@ -16,9 +16,8 @@ if rc['pin'] != {"b": True, "bf": 500, "bs": 10, "e": True, "r": 3, "rf": 300}: print('>>> WARNING: ping variables changed: %s' % rc['pin']) -if rc['patch']['f'] != 1800000: +if rc['patch']['f'] != 1800000: # this is probably dead value print('>>> WARNING: patch version changed: %s' % rc['patch']['f']) -v = '18.0.0' if rc['futweb_maintenance']: raise FutError('Futweb maintenance, please retry in few minutes.') diff --git a/setup.py b/setup.py index 0aa46e5..f421534 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __title__ = 'fut' -__version__ = '0.3.5' +__version__ = '0.3.6' __author__ = 'Piotr Staroszczyk' __author_email__ = 'piotr.staroszczyk@get24.org' __license__ = 'GNU GPL v3'