Fixes 502/503 errors and a the 1357004
error
Thereby also moving ReqUrl to self.req_url
This commit is contained in:
157
fbchat/client.py
157
fbchat/client.py
@@ -43,7 +43,7 @@ class Client(object):
|
||||
:type max_tries: int
|
||||
:type session_cookies: dict
|
||||
:type logging_level: int
|
||||
:raises: Exception on failed login
|
||||
:raises: FBchatException on failed login
|
||||
"""
|
||||
|
||||
self.sticky, self.pool = (None, None)
|
||||
@@ -54,14 +54,15 @@ class Client(object):
|
||||
self.client = 'mercury'
|
||||
self.default_thread_id = None
|
||||
self.default_thread_type = None
|
||||
self.req_url = ReqUrl()
|
||||
|
||||
if not user_agent:
|
||||
user_agent = choice(USER_AGENTS)
|
||||
|
||||
self._header = {
|
||||
'Content-Type' : 'application/x-www-form-urlencoded',
|
||||
'Referer' : ReqUrl.BASE,
|
||||
'Origin' : ReqUrl.BASE,
|
||||
'Referer' : self.req_url.BASE,
|
||||
'Origin' : self.req_url.BASE,
|
||||
'User-Agent' : user_agent,
|
||||
'Connection' : 'keep-alive',
|
||||
}
|
||||
@@ -91,13 +92,50 @@ class Client(object):
|
||||
self.req_counter += 1
|
||||
return payload
|
||||
|
||||
def _get(self, url, query=None, timeout=30):
|
||||
payload = self._generatePayload(query)
|
||||
return self._session.get(url, headers=self._header, params=payload, timeout=timeout)
|
||||
def _fix_1357004(self, error_code):
|
||||
"""
|
||||
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)
|
||||
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):
|
||||
return self._session.get(url, headers=self._header, params=query, timeout=timeout)
|
||||
@@ -106,38 +144,42 @@ class Client(object):
|
||||
self.req_counter += 1
|
||||
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)
|
||||
# Removes 'Content-Type' from the header
|
||||
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):
|
||||
"""
|
||||
.. todo::
|
||||
Documenting this
|
||||
|
||||
:raises: Exception if request failed
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
payload = {
|
||||
|
||||
return tuple(self._graphql({
|
||||
'method': 'GET',
|
||||
'response_format': 'json',
|
||||
'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):
|
||||
"""
|
||||
Shorthand for `graphql_requests(query)[0]`
|
||||
|
||||
:raises: Exception if request failed
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
return self.graphql_requests(query)[0]
|
||||
|
||||
|
||||
"""
|
||||
END INTERNAL REQUEST METHODS
|
||||
"""
|
||||
@@ -159,12 +201,12 @@ class Client(object):
|
||||
self.start_time = now()
|
||||
self.uid = self._session.cookies.get_dict().get('c_user')
|
||||
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.user_channel = "p_" + self.uid
|
||||
self.ttstamp = ''
|
||||
|
||||
r = self._get(ReqUrl.BASE)
|
||||
r = self._get(self.req_url.BASE)
|
||||
soup = bs(r.text, "lxml")
|
||||
self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value']
|
||||
self.fb_h = soup.find("input", {'name':'h'})['value']
|
||||
@@ -198,13 +240,13 @@ class Client(object):
|
||||
if not (self.email and self.password):
|
||||
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['email'] = self.email
|
||||
data['pass'] = self.password
|
||||
data['login'] = 'Log In'
|
||||
|
||||
r = self._cleanPost(ReqUrl.LOGIN, data)
|
||||
r = self._cleanPost(self.req_url.LOGIN, data)
|
||||
|
||||
# Usually, 'Checkpoint' will refer to 2FA
|
||||
if ('checkpoint' in r.url and
|
||||
@@ -213,7 +255,7 @@ class Client(object):
|
||||
|
||||
# Sometimes Facebook tries to show the user a "Save Device" dialog
|
||||
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:
|
||||
self._postLogin()
|
||||
@@ -234,7 +276,7 @@ class Client(object):
|
||||
data['codes_submitted'] = 0
|
||||
log.info('Submitting 2FA code.')
|
||||
|
||||
r = self._cleanPost(ReqUrl.CHECKPOINT, data)
|
||||
r = self._cleanPost(self.req_url.CHECKPOINT, data)
|
||||
|
||||
if 'home' in r.url:
|
||||
return r
|
||||
@@ -246,14 +288,14 @@ class Client(object):
|
||||
data['name_action_selected'] = 'save_device'
|
||||
data['submit[Continue]'] = '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:
|
||||
return r
|
||||
|
||||
del(data['name_action_selected'])
|
||||
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:
|
||||
return r
|
||||
@@ -261,7 +303,7 @@ class Client(object):
|
||||
del(data['submit[Continue]'])
|
||||
data['submit[This was me]'] = '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:
|
||||
return r
|
||||
@@ -270,7 +312,7 @@ class Client(object):
|
||||
data['submit[Continue]'] = 'Continue'
|
||||
data['name_action_selected'] = 'save_device'
|
||||
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
|
||||
|
||||
def isLoggedIn(self):
|
||||
@@ -281,7 +323,7 @@ class Client(object):
|
||||
:rtype: bool
|
||||
"""
|
||||
# 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
|
||||
|
||||
def getSession(self):
|
||||
@@ -309,7 +351,7 @@ class Client(object):
|
||||
# Load cookies into current session
|
||||
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
|
||||
self._postLogin()
|
||||
except Exception as e:
|
||||
except FBchatException as e:
|
||||
self.exception('Failed loading session')
|
||||
self._resetValues()
|
||||
return False
|
||||
@@ -361,7 +403,7 @@ class Client(object):
|
||||
'h': self.fb_h
|
||||
}
|
||||
|
||||
r = self._get(ReqUrl.LOGOUT, data)
|
||||
r = self._get(self.req_url.LOGOUT, data)
|
||||
|
||||
self._resetValues()
|
||||
|
||||
@@ -425,7 +467,7 @@ class Client(object):
|
||||
data = {
|
||||
'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:
|
||||
raise FBchatException('Missing payload while fetching users: {}'.format(j))
|
||||
|
||||
@@ -519,7 +561,7 @@ class Client(object):
|
||||
data = {
|
||||
"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:
|
||||
raise FBchatException('No users/pages returned: {}'.format(j))
|
||||
@@ -726,7 +768,7 @@ class Client(object):
|
||||
'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:
|
||||
raise FBchatException('Missing payload: {}, with data: {}'.format(j, data))
|
||||
|
||||
@@ -767,7 +809,7 @@ class Client(object):
|
||||
# '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 {
|
||||
"message_counts": j['payload']['message_counts'],
|
||||
@@ -826,7 +868,7 @@ class Client(object):
|
||||
|
||||
def _doSendRequest(self, data):
|
||||
"""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:
|
||||
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):
|
||||
"""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': (
|
||||
image_path,
|
||||
data,
|
||||
mimetype
|
||||
)
|
||||
}))
|
||||
}, fix_request=True, as_json=True)
|
||||
# Return the image_id
|
||||
return j['payload']['metadata'][0]['image_id']
|
||||
|
||||
@@ -1014,7 +1056,7 @@ class Client(object):
|
||||
"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):
|
||||
"""
|
||||
@@ -1061,7 +1103,7 @@ class Client(object):
|
||||
'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):
|
||||
"""
|
||||
@@ -1079,7 +1121,7 @@ class Client(object):
|
||||
'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):
|
||||
"""
|
||||
@@ -1098,7 +1140,7 @@ class Client(object):
|
||||
'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):
|
||||
"""
|
||||
@@ -1130,7 +1172,7 @@ class Client(object):
|
||||
.replace('u%27', '%27')\
|
||||
.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):
|
||||
"""
|
||||
@@ -1152,7 +1194,7 @@ class Client(object):
|
||||
"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
|
||||
@@ -1168,7 +1210,7 @@ class Client(object):
|
||||
"thread_ids[%s][0]" % userID: threadID
|
||||
}
|
||||
|
||||
r = self._post(ReqUrl.DELIVERED, data)
|
||||
r = self._post(self.req_url.DELIVERED, data)
|
||||
return r.ok
|
||||
|
||||
def markAsRead(self, userID):
|
||||
@@ -1182,7 +1224,7 @@ class Client(object):
|
||||
"ids[%s]" % userID: True
|
||||
}
|
||||
|
||||
r = self._post(ReqUrl.READ_STATUS, data)
|
||||
r = self._post(self.req_url.READ_STATUS, data)
|
||||
return r.ok
|
||||
|
||||
def markAsSeen(self):
|
||||
@@ -1190,7 +1232,7 @@ class Client(object):
|
||||
.. todo::
|
||||
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
|
||||
|
||||
def friendConnect(self, friend_id):
|
||||
@@ -1203,7 +1245,7 @@ class Client(object):
|
||||
"action": "confirm"
|
||||
}
|
||||
|
||||
r = self._post(ReqUrl.CONNECT, data)
|
||||
r = self._post(self.req_url.CONNECT, data)
|
||||
return r.ok
|
||||
|
||||
|
||||
@@ -1223,7 +1265,7 @@ class Client(object):
|
||||
'viewer_uid': self.uid,
|
||||
'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):
|
||||
"""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
|
||||
}
|
||||
|
||||
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:
|
||||
raise FBchatException('Missing lb_info: {}'.format(j))
|
||||
@@ -1251,7 +1293,7 @@ class Client(object):
|
||||
"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')
|
||||
return j
|
||||
@@ -1437,7 +1479,8 @@ class Client(object):
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
if markAlive: self._ping(self.sticky, self.pool)
|
||||
if markAlive:
|
||||
self._ping(self.sticky, self.pool)
|
||||
content = self._pullMessage(self.sticky, self.pool)
|
||||
if content:
|
||||
self._parseMessage(content)
|
||||
@@ -1446,7 +1489,15 @@ class Client(object):
|
||||
except requests.Timeout:
|
||||
pass
|
||||
except requests.ConnectionError:
|
||||
# If the client has lost their internet connection, keep trying every 30 seconds
|
||||
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:
|
||||
return self.onListenError(exception=e)
|
||||
|
||||
|
@@ -93,6 +93,16 @@ class ReqUrl(object):
|
||||
TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
|
||||
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'
|
||||
|
||||
@@ -153,7 +163,7 @@ def check_json(j):
|
||||
else:
|
||||
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:
|
||||
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:
|
||||
raise FBchatFacebookError('Error when sending request: Got empty response')
|
||||
|
||||
if do_json_check:
|
||||
if as_json:
|
||||
content = strip_to_json(content)
|
||||
try:
|
||||
j = json.loads(content)
|
||||
|
Reference in New Issue
Block a user