Fixes 502/503 errors and a the 1357004 error

Thereby also moving ReqUrl to self.req_url
This commit is contained in:
Mads Marquart
2017-09-19 23:08:48 +02:00
parent cb8b0915de
commit a64982583b
2 changed files with 116 additions and 55 deletions

View File

@@ -43,7 +43,7 @@ class Client(object):
:type max_tries: int :type max_tries: int
:type session_cookies: dict :type session_cookies: dict
:type logging_level: int :type logging_level: int
:raises: Exception on failed login :raises: FBchatException on failed login
""" """
self.sticky, self.pool = (None, None) self.sticky, self.pool = (None, None)
@@ -54,14 +54,15 @@ class Client(object):
self.client = 'mercury' self.client = 'mercury'
self.default_thread_id = None self.default_thread_id = None
self.default_thread_type = None self.default_thread_type = None
self.req_url = ReqUrl()
if not user_agent: if not user_agent:
user_agent = choice(USER_AGENTS) user_agent = choice(USER_AGENTS)
self._header = { self._header = {
'Content-Type' : 'application/x-www-form-urlencoded', 'Content-Type' : 'application/x-www-form-urlencoded',
'Referer' : ReqUrl.BASE, 'Referer' : self.req_url.BASE,
'Origin' : ReqUrl.BASE, 'Origin' : self.req_url.BASE,
'User-Agent' : user_agent, 'User-Agent' : user_agent,
'Connection' : 'keep-alive', 'Connection' : 'keep-alive',
} }
@@ -91,13 +92,50 @@ class Client(object):
self.req_counter += 1 self.req_counter += 1
return payload return payload
def _get(self, url, query=None, timeout=30): def _fix_1357004(self, error_code):
payload = self._generatePayload(query) """
return self._session.get(url, headers=self._header, params=payload, timeout=timeout) This fixes "Please try closing and re-opening your browser window" errors (1357004)
This error usually happens after 1-2 days of inactivity
It may be a bad idea to do this in an exception handler, if you have a better method, please suggest it!
"""
if error_code == '1357004':
log.warning('Got error #1357004. Doing a _postLogin, and resending request')
self._postLogin()
return True
return False
def _post(self, url, query=None, timeout=30): def _get(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload = self._generatePayload(query) payload = self._generatePayload(query)
return self._session.post(url, headers=self._header, data=payload, timeout=timeout) r = self._session.get(url, headers=self._header, params=payload, timeout=timeout)
if not fix_request:
return r
try:
return check_request(r, as_json=as_json)
except FBchatFacebookError as e:
if error_retries > 0 and self._fix_1357004(e.fb_error_code):
return self._get(url, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1)
raise e
def _post(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload = self._generatePayload(query)
r = self._session.post(url, headers=self._header, data=payload, timeout=timeout)
if not fix_request:
return r
try:
return check_request(r, as_json=as_json)
except FBchatFacebookError as e:
if error_retries > 0 and self._fix_1357004(e.fb_error_code):
return self._post(url, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1)
raise e
def _graphql(self, payload, error_retries=3):
content = self._post(self.req_url.GRAPHQL, payload, fix_request=True, as_json=False)
try:
return graphql_response_to_json(content)
except FBchatFacebookError as e:
if error_retries > 0 and self._fix_1357004(e.fb_error_code):
return self._graphql(payload, error_retries=error_retries-1)
raise e
def _cleanGet(self, url, query=None, timeout=30): def _cleanGet(self, url, query=None, timeout=30):
return self._session.get(url, headers=self._header, params=query, timeout=timeout) return self._session.get(url, headers=self._header, params=query, timeout=timeout)
@@ -106,38 +144,42 @@ class Client(object):
self.req_counter += 1 self.req_counter += 1
return self._session.post(url, headers=self._header, data=query, timeout=timeout) return self._session.post(url, headers=self._header, data=query, timeout=timeout)
def _postFile(self, url, files=None, query=None, timeout=30): def _postFile(self, url, files=None, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload=self._generatePayload(query) payload=self._generatePayload(query)
# Removes 'Content-Type' from the header # Removes 'Content-Type' from the header
headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type') headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type')
return self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files) r = self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files)
if not fix_request:
return r
try:
return check_request(r, as_json=as_json)
except FBchatFacebookError as e:
if error_retries > 0 and self._fix_1357004(e.fb_error_code):
return self._postFile(url, files=files, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1)
raise e
def graphql_requests(self, *queries): def graphql_requests(self, *queries):
""" """
.. todo:: .. todo::
Documenting this Documenting this
:raises: Exception if request failed :raises: FBchatException if request failed
""" """
payload = {
return tuple(self._graphql({
'method': 'GET', 'method': 'GET',
'response_format': 'json', 'response_format': 'json',
'queries': graphql_queries_to_json(*queries) 'queries': graphql_queries_to_json(*queries)
} }))
j = graphql_response_to_json(checkRequest(self._post(ReqUrl.GRAPHQL, payload), do_json_check=False))
return tuple(j)
def graphql_request(self, query): def graphql_request(self, query):
""" """
Shorthand for `graphql_requests(query)[0]` Shorthand for `graphql_requests(query)[0]`
:raises: Exception if request failed :raises: FBchatException if request failed
""" """
return self.graphql_requests(query)[0] return self.graphql_requests(query)[0]
""" """
END INTERNAL REQUEST METHODS END INTERNAL REQUEST METHODS
""" """
@@ -159,12 +201,12 @@ class Client(object):
self.start_time = now() self.start_time = now()
self.uid = self._session.cookies.get_dict().get('c_user') self.uid = self._session.cookies.get_dict().get('c_user')
if self.uid is None: if self.uid is None:
raise Exception('Could not find c_user cookie') raise FBchatException('Could not find c_user cookie')
self.uid = str(self.uid) self.uid = str(self.uid)
self.user_channel = "p_" + self.uid self.user_channel = "p_" + self.uid
self.ttstamp = '' self.ttstamp = ''
r = self._get(ReqUrl.BASE) r = self._get(self.req_url.BASE)
soup = bs(r.text, "lxml") soup = bs(r.text, "lxml")
self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value'] self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value']
self.fb_h = soup.find("input", {'name':'h'})['value'] self.fb_h = soup.find("input", {'name':'h'})['value']
@@ -198,13 +240,13 @@ class Client(object):
if not (self.email and self.password): if not (self.email and self.password):
raise FBchatUserError("Email and password not found.") raise FBchatUserError("Email and password not found.")
soup = bs(self._get(ReqUrl.MOBILE).text, "lxml") soup = bs(self._get(self.req_url.MOBILE).text, "lxml")
data = dict((elem['name'], elem['value']) for elem in soup.findAll("input") if elem.has_attr('value') and elem.has_attr('name')) data = dict((elem['name'], elem['value']) for elem in soup.findAll("input") if elem.has_attr('value') and elem.has_attr('name'))
data['email'] = self.email data['email'] = self.email
data['pass'] = self.password data['pass'] = self.password
data['login'] = 'Log In' data['login'] = 'Log In'
r = self._cleanPost(ReqUrl.LOGIN, data) r = self._cleanPost(self.req_url.LOGIN, data)
# Usually, 'Checkpoint' will refer to 2FA # Usually, 'Checkpoint' will refer to 2FA
if ('checkpoint' in r.url and if ('checkpoint' in r.url and
@@ -213,7 +255,7 @@ class Client(object):
# Sometimes Facebook tries to show the user a "Save Device" dialog # Sometimes Facebook tries to show the user a "Save Device" dialog
if 'save-device' in r.url: if 'save-device' in r.url:
r = self._cleanGet(ReqUrl.SAVE_DEVICE) r = self._cleanGet(self.req_url.SAVE_DEVICE)
if 'home' in r.url: if 'home' in r.url:
self._postLogin() self._postLogin()
@@ -234,7 +276,7 @@ class Client(object):
data['codes_submitted'] = 0 data['codes_submitted'] = 0
log.info('Submitting 2FA code.') log.info('Submitting 2FA code.')
r = self._cleanPost(ReqUrl.CHECKPOINT, data) r = self._cleanPost(self.req_url.CHECKPOINT, data)
if 'home' in r.url: if 'home' in r.url:
return r return r
@@ -246,14 +288,14 @@ class Client(object):
data['name_action_selected'] = 'save_device' data['name_action_selected'] = 'save_device'
data['submit[Continue]'] = 'Continue' data['submit[Continue]'] = 'Continue'
log.info('Saving browser.') # At this stage, we have dtsg, nh, name_action_selected, submit[Continue] log.info('Saving browser.') # At this stage, we have dtsg, nh, name_action_selected, submit[Continue]
r = self._cleanPost(ReqUrl.CHECKPOINT, data) r = self._cleanPost(self.req_url.CHECKPOINT, data)
if 'home' in r.url: if 'home' in r.url:
return r return r
del(data['name_action_selected']) del(data['name_action_selected'])
log.info('Starting Facebook checkup flow.') # At this stage, we have dtsg, nh, submit[Continue] log.info('Starting Facebook checkup flow.') # At this stage, we have dtsg, nh, submit[Continue]
r = self._cleanPost(ReqUrl.CHECKPOINT, data) r = self._cleanPost(self.req_url.CHECKPOINT, data)
if 'home' in r.url: if 'home' in r.url:
return r return r
@@ -261,7 +303,7 @@ class Client(object):
del(data['submit[Continue]']) del(data['submit[Continue]'])
data['submit[This was me]'] = 'This Was Me' data['submit[This was me]'] = 'This Was Me'
log.info('Verifying login attempt.') # At this stage, we have dtsg, nh, submit[This was me] log.info('Verifying login attempt.') # At this stage, we have dtsg, nh, submit[This was me]
r = self._cleanPost(ReqUrl.CHECKPOINT, data) r = self._cleanPost(self.req_url.CHECKPOINT, data)
if 'home' in r.url: if 'home' in r.url:
return r return r
@@ -270,7 +312,7 @@ class Client(object):
data['submit[Continue]'] = 'Continue' data['submit[Continue]'] = 'Continue'
data['name_action_selected'] = 'save_device' data['name_action_selected'] = 'save_device'
log.info('Saving device again.') # At this stage, we have dtsg, nh, submit[Continue], name_action_selected log.info('Saving device again.') # At this stage, we have dtsg, nh, submit[Continue], name_action_selected
r = self._cleanPost(ReqUrl.CHECKPOINT, data) r = self._cleanPost(self.req_url.CHECKPOINT, data)
return r return r
def isLoggedIn(self): def isLoggedIn(self):
@@ -281,7 +323,7 @@ class Client(object):
:rtype: bool :rtype: bool
""" """
# Send a request to the login url, to see if we're directed to the home page # Send a request to the login url, to see if we're directed to the home page
r = self._cleanGet(ReqUrl.LOGIN) r = self._cleanGet(self.req_url.LOGIN)
return 'home' in r.url return 'home' in r.url
def getSession(self): def getSession(self):
@@ -309,7 +351,7 @@ class Client(object):
# Load cookies into current session # Load cookies into current session
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies) self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
self._postLogin() self._postLogin()
except Exception as e: except FBchatException as e:
self.exception('Failed loading session') self.exception('Failed loading session')
self._resetValues() self._resetValues()
return False return False
@@ -361,7 +403,7 @@ class Client(object):
'h': self.fb_h 'h': self.fb_h
} }
r = self._get(ReqUrl.LOGOUT, data) r = self._get(self.req_url.LOGOUT, data)
self._resetValues() self._resetValues()
@@ -425,7 +467,7 @@ class Client(object):
data = { data = {
'viewer': self.uid, 'viewer': self.uid,
} }
j = checkRequest(self._post(ReqUrl.ALL_USERS, query=data)) j = self._post(self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True)
if j.get('payload') is None: if j.get('payload') is None:
raise FBchatException('Missing payload while fetching users: {}'.format(j)) raise FBchatException('Missing payload while fetching users: {}'.format(j))
@@ -519,7 +561,7 @@ class Client(object):
data = { data = {
"ids[{}]".format(i): _id for i, _id in enumerate(ids) "ids[{}]".format(i): _id for i, _id in enumerate(ids)
} }
j = checkRequest(self._post(ReqUrl.INFO, data)) j = self._post(self.req_url.INFO, data, fix_request=True, as_json=True)
if j.get('payload') is None or j['payload'].get('profiles') is None: if j.get('payload') is None or j['payload'].get('profiles') is None:
raise FBchatException('No users/pages returned: {}'.format(j)) raise FBchatException('No users/pages returned: {}'.format(j))
@@ -726,7 +768,7 @@ class Client(object):
'inbox[limit]' : limit, 'inbox[limit]' : limit,
} }
j = checkRequest(self._post(ReqUrl.THREADS, data)) j = self._post(self.req_url.THREADS, data, fix_request=True, as_json=True)
if j.get('payload') is None: if j.get('payload') is None:
raise FBchatException('Missing payload: {}, with data: {}'.format(j, data)) raise FBchatException('Missing payload: {}, with data: {}'.format(j, data))
@@ -767,7 +809,7 @@ class Client(object):
# 'last_action_timestamp': 0 # 'last_action_timestamp': 0
} }
j = checkRequest(self._post(ReqUrl.THREAD_SYNC, form)) j = self._post(self.req_url.THREAD_SYNC, form, fix_request=True, as_json=True)
return { return {
"message_counts": j['payload']['message_counts'], "message_counts": j['payload']['message_counts'],
@@ -826,7 +868,7 @@ class Client(object):
def _doSendRequest(self, data): def _doSendRequest(self, data):
"""Sends the data to `SendURL`, and returns the message ID or None on failure""" """Sends the data to `SendURL`, and returns the message ID or None on failure"""
j = checkRequest(self._post(ReqUrl.SEND, data)) j = self._post(self.req_url.SEND, data, fix_request=True, as_json=True)
try: try:
message_ids = [action['message_id'] for action in j['payload']['actions'] if 'message_id' in action] message_ids = [action['message_id'] for action in j['payload']['actions'] if 'message_id' in action]
@@ -898,13 +940,13 @@ class Client(object):
def _uploadImage(self, image_path, data, mimetype): def _uploadImage(self, image_path, data, mimetype):
"""Upload an image and get the image_id for sending in a message""" """Upload an image and get the image_id for sending in a message"""
j = checkRequest(self._postFile(ReqUrl.UPLOAD, { j = self._postFile(self.req_url.UPLOAD, {
'file': ( 'file': (
image_path, image_path,
data, data,
mimetype mimetype
) )
})) }, fix_request=True, as_json=True)
# Return the image_id # Return the image_id
return j['payload']['metadata'][0]['image_id'] return j['payload']['metadata'][0]['image_id']
@@ -1014,7 +1056,7 @@ class Client(object):
"tid": thread_id "tid": thread_id
} }
j = checkRequest(self._post(ReqUrl.REMOVE_USER, data)) j = self._post(self.req_url.REMOVE_USER, data, fix_request=True, as_json=True)
def changeThreadTitle(self, title, thread_id=None, thread_type=ThreadType.USER): def changeThreadTitle(self, title, thread_id=None, thread_type=ThreadType.USER):
""" """
@@ -1061,7 +1103,7 @@ class Client(object):
'thread_or_other_fbid': thread_id 'thread_or_other_fbid': thread_id
} }
j = checkRequest(self._post(ReqUrl.THREAD_NICKNAME, data)) j = self._post(self.req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True)
def changeThreadColor(self, color, thread_id=None): def changeThreadColor(self, color, thread_id=None):
""" """
@@ -1079,7 +1121,7 @@ class Client(object):
'thread_or_other_fbid': thread_id 'thread_or_other_fbid': thread_id
} }
j = checkRequest(self._post(ReqUrl.THREAD_COLOR, data)) j = self._post(self.req_url.THREAD_COLOR, data, fix_request=True, as_json=True)
def changeThreadEmoji(self, emoji, thread_id=None): def changeThreadEmoji(self, emoji, thread_id=None):
""" """
@@ -1098,7 +1140,7 @@ class Client(object):
'thread_or_other_fbid': thread_id 'thread_or_other_fbid': thread_id
} }
j = checkRequest(self._post(ReqUrl.THREAD_EMOJI, data)) j = self._post(self.req_url.THREAD_EMOJI, data, fix_request=True, as_json=True)
def reactToMessage(self, message_id, reaction): def reactToMessage(self, message_id, reaction):
""" """
@@ -1130,7 +1172,7 @@ class Client(object):
.replace('u%27', '%27')\ .replace('u%27', '%27')\
.replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1]) .replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1])
j = checkRequest(self._post('{}/?{}'.format(ReqUrl.MESSAGE_REACTION, url_part))) j = self._post('{}/?{}'.format(self.req_url.MESSAGE_REACTION, url_part), fix_request=True, as_json=True)
def setTypingStatus(self, status, thread_id=None, thread_type=None): def setTypingStatus(self, status, thread_id=None, thread_type=None):
""" """
@@ -1152,7 +1194,7 @@ class Client(object):
"source": "mercury-chat" "source": "mercury-chat"
} }
j = checkRequest(self._post(ReqUrl.TYPING, data)) j = self._post(self.req_url.TYPING, data, fix_request=True, as_json=True)
""" """
END SEND METHODS END SEND METHODS
@@ -1168,7 +1210,7 @@ class Client(object):
"thread_ids[%s][0]" % userID: threadID "thread_ids[%s][0]" % userID: threadID
} }
r = self._post(ReqUrl.DELIVERED, data) r = self._post(self.req_url.DELIVERED, data)
return r.ok return r.ok
def markAsRead(self, userID): def markAsRead(self, userID):
@@ -1182,7 +1224,7 @@ class Client(object):
"ids[%s]" % userID: True "ids[%s]" % userID: True
} }
r = self._post(ReqUrl.READ_STATUS, data) r = self._post(self.req_url.READ_STATUS, data)
return r.ok return r.ok
def markAsSeen(self): def markAsSeen(self):
@@ -1190,7 +1232,7 @@ class Client(object):
.. todo:: .. todo::
Documenting this Documenting this
""" """
r = self._post(ReqUrl.MARK_SEEN, {"seen_timestamp": 0}) r = self._post(self.req_url.MARK_SEEN, {"seen_timestamp": 0})
return r.ok return r.ok
def friendConnect(self, friend_id): def friendConnect(self, friend_id):
@@ -1203,7 +1245,7 @@ class Client(object):
"action": "confirm" "action": "confirm"
} }
r = self._post(ReqUrl.CONNECT, data) r = self._post(self.req_url.CONNECT, data)
return r.ok return r.ok
@@ -1223,7 +1265,7 @@ class Client(object):
'viewer_uid': self.uid, 'viewer_uid': self.uid,
'state': 'active' 'state': 'active'
} }
checkRequest(self._get(ReqUrl.PING, data), do_json_check=False) self._get(self.req_url.PING, data, fix_request=True, as_json=False)
def _fetchSticky(self): def _fetchSticky(self):
"""Call pull api to get sticky and pool parameter, newer api needs these parameters to work""" """Call pull api to get sticky and pool parameter, newer api needs these parameters to work"""
@@ -1234,7 +1276,7 @@ class Client(object):
"clientid": self.client_id "clientid": self.client_id
} }
j = checkRequest(self._get(ReqUrl.STICKY, data)) j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True)
if j.get('lb_info') is None: if j.get('lb_info') is None:
raise FBchatException('Missing lb_info: {}'.format(j)) raise FBchatException('Missing lb_info: {}'.format(j))
@@ -1251,7 +1293,7 @@ class Client(object):
"clientid": self.client_id, "clientid": self.client_id,
} }
j = checkRequest(self._get(ReqUrl.STICKY, data)) j = self._get(ReqUrl.STICKY, data, fix_request=True, as_json=True)
self.seq = j.get('seq', '0') self.seq = j.get('seq', '0')
return j return j
@@ -1437,7 +1479,8 @@ class Client(object):
:rtype: bool :rtype: bool
""" """
try: try:
if markAlive: self._ping(self.sticky, self.pool) if markAlive:
self._ping(self.sticky, self.pool)
content = self._pullMessage(self.sticky, self.pool) content = self._pullMessage(self.sticky, self.pool)
if content: if content:
self._parseMessage(content) self._parseMessage(content)
@@ -1446,7 +1489,15 @@ class Client(object):
except requests.Timeout: except requests.Timeout:
pass pass
except requests.ConnectionError: except requests.ConnectionError:
# If the client has lost their internet connection, keep trying every 30 seconds
time.sleep(30) time.sleep(30)
except FBchatFacebookError as e:
# Fix 502 and 503 pull errors
if e.request_status_code in [502, 503]:
self.req_url.change_pull_channel()
self.startListening()
else:
raise e
except Exception as e: except Exception as e:
return self.onListenError(exception=e) return self.onListenError(exception=e)

View File

@@ -93,6 +93,16 @@ class ReqUrl(object):
TYPING = "https://www.facebook.com/ajax/messaging/typ.php" TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
GRAPHQL = "https://www.facebook.com/api/graphqlbatch/" GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
pull_channel = 0
def change_pull_channel(self, channel=None):
if channel is None:
self.pull_channel = (self.pull_channel + 1) % 5 # Pull channel will be 0-4
else:
self.pull_channel = channel
self.STICKY = "https://{}-edge-chat.facebook.com/pull".format(self.pull_channel)
self.PING = "https://{}-edge-chat.facebook.com/active_ping".format(self.pull_channel)
facebookEncoding = 'UTF-8' facebookEncoding = 'UTF-8'
@@ -153,7 +163,7 @@ def check_json(j):
else: else:
raise FBchatFacebookError('Error {} when sending request'.format(j['error']), fb_error_code=j['error']) raise FBchatFacebookError('Error {} when sending request'.format(j['error']), fb_error_code=j['error'])
def checkRequest(r, do_json_check=True): def check_request(r, as_json=True):
if not r.ok: if not r.ok:
raise FBchatFacebookError('Error when sending request: Got {} response'.format(r.status_code), request_error_code=r.status_code) raise FBchatFacebookError('Error when sending request: Got {} response'.format(r.status_code), request_error_code=r.status_code)
@@ -162,7 +172,7 @@ def checkRequest(r, do_json_check=True):
if content is None or len(content) == 0: if content is None or len(content) == 0:
raise FBchatFacebookError('Error when sending request: Got empty response') raise FBchatFacebookError('Error when sending request: Got empty response')
if do_json_check: if as_json:
content = strip_to_json(content) content = strip_to_json(content)
try: try:
j = json.loads(content) j = json.loads(content)