|
|
|
@@ -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_fb_errors(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_fb_errors(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_fb_errors(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_fb_errors(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_fb_errors(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
|
|
|
|
|
"""
|
|
|
|
@@ -157,11 +199,14 @@ class Client(object):
|
|
|
|
|
self.payloadDefault = {}
|
|
|
|
|
self.client_id = hex(int(random()*2147483648))[2:]
|
|
|
|
|
self.start_time = now()
|
|
|
|
|
self.uid = str(self._session.cookies['c_user'])
|
|
|
|
|
self.uid = self._session.cookies.get_dict().get('c_user')
|
|
|
|
|
if self.uid is None:
|
|
|
|
|
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']
|
|
|
|
@@ -193,23 +238,25 @@ class Client(object):
|
|
|
|
|
|
|
|
|
|
def _login(self):
|
|
|
|
|
if not (self.email and self.password):
|
|
|
|
|
raise Exception("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['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 'Enter Security Code to Continue' in r.text:
|
|
|
|
|
if ('checkpoint' in r.url
|
|
|
|
|
and ('enter security code to continue' in r.text.lower()
|
|
|
|
|
or 'enter login code to continue' in r.text.lower())):
|
|
|
|
|
r = self._2FA(r)
|
|
|
|
|
|
|
|
|
|
# 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()
|
|
|
|
@@ -230,7 +277,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
|
|
|
|
@@ -242,14 +289,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
|
|
|
|
@@ -257,7 +304,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
|
|
|
|
@@ -266,7 +313,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):
|
|
|
|
@@ -277,7 +324,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):
|
|
|
|
@@ -305,7 +352,8 @@ class Client(object):
|
|
|
|
|
# Load cookies into current session
|
|
|
|
|
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
|
|
|
|
|
self._postLogin()
|
|
|
|
|
except Exception:
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.exception('Failed loading session')
|
|
|
|
|
self._resetValues()
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
@@ -318,15 +366,15 @@ class Client(object):
|
|
|
|
|
:param password: Facebook account password
|
|
|
|
|
:param max_tries: Maximum number of times to try logging in
|
|
|
|
|
:type max_tries: int
|
|
|
|
|
:raises: Exception on failed login
|
|
|
|
|
:raises: FBchatException on failed login
|
|
|
|
|
"""
|
|
|
|
|
self.onLoggingIn(email=email)
|
|
|
|
|
|
|
|
|
|
if max_tries < 1:
|
|
|
|
|
raise Exception('Cannot login: max_tries should be at least one')
|
|
|
|
|
raise FBchatUserError('Cannot login: max_tries should be at least one')
|
|
|
|
|
|
|
|
|
|
if not (email and password):
|
|
|
|
|
raise Exception('Email and password not set')
|
|
|
|
|
raise FBchatUserError('Email and password not set')
|
|
|
|
|
|
|
|
|
|
self.email = email
|
|
|
|
|
self.password = password
|
|
|
|
@@ -341,7 +389,7 @@ class Client(object):
|
|
|
|
|
self.onLoggedIn(email=email)
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('Login failed. Check email/password. (Failed on url: {})'.format(login_url))
|
|
|
|
|
raise FBchatUserError('Login failed. Check email/password. (Failed on url: {})'.format(login_url))
|
|
|
|
|
|
|
|
|
|
def logout(self):
|
|
|
|
|
"""
|
|
|
|
@@ -356,7 +404,7 @@ class Client(object):
|
|
|
|
|
'h': self.fb_h
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r = self._get(ReqUrl.LOGOUT, data)
|
|
|
|
|
r = self._get(self.req_url.LOGOUT, data)
|
|
|
|
|
|
|
|
|
|
self._resetValues()
|
|
|
|
|
|
|
|
|
@@ -414,15 +462,15 @@ class Client(object):
|
|
|
|
|
|
|
|
|
|
:return: :class:`models.User` objects
|
|
|
|
|
:rtype: list
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
'viewer': self.uid,
|
|
|
|
|
}
|
|
|
|
|
j = checkRequest(self._post(ReqUrl.ALL_USERS, query=data))
|
|
|
|
|
if not j['payload']:
|
|
|
|
|
raise Exception('Missing payload')
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
users = []
|
|
|
|
|
|
|
|
|
@@ -444,7 +492,7 @@ class Client(object):
|
|
|
|
|
:param limit: The max. amount of users to fetch
|
|
|
|
|
:return: :class:`models.User` objects, ordered by relevance
|
|
|
|
|
:rtype: list
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_USER, params={'search': name, 'limit': limit}))
|
|
|
|
@@ -458,7 +506,7 @@ class Client(object):
|
|
|
|
|
:param name: Name of the page
|
|
|
|
|
:return: :class:`models.Page` objects, ordered by relevance
|
|
|
|
|
:rtype: list
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_PAGE, params={'search': name, 'limit': limit}))
|
|
|
|
@@ -473,7 +521,7 @@ class Client(object):
|
|
|
|
|
:param limit: The max. amount of groups to fetch
|
|
|
|
|
:return: :class:`models.Group` objects, ordered by relevance
|
|
|
|
|
:rtype: list
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_GROUP, params={'search': name, 'limit': limit}))
|
|
|
|
@@ -488,7 +536,7 @@ class Client(object):
|
|
|
|
|
:param limit: The max. amount of groups to fetch
|
|
|
|
|
:return: :class:`models.User`, :class:`models.Group` and :class:`models.Page` objects, ordered by relevance
|
|
|
|
|
:rtype: list
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_THREAD, params={'search': name, 'limit': limit}))
|
|
|
|
@@ -514,10 +562,10 @@ 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 not j['payload']['profiles']:
|
|
|
|
|
raise Exception('No users/pages returned')
|
|
|
|
|
if j.get('payload') is None or j['payload'].get('profiles') is None:
|
|
|
|
|
raise FBchatException('No users/pages returned: {}'.format(j))
|
|
|
|
|
|
|
|
|
|
entries = {}
|
|
|
|
|
for _id in j['payload']['profiles']:
|
|
|
|
@@ -542,7 +590,7 @@ class Client(object):
|
|
|
|
|
'name': k.get('name')
|
|
|
|
|
}
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('{} had an unknown thread type: {}'.format(_id, k))
|
|
|
|
|
raise FBchatException('{} had an unknown thread type: {}'.format(_id, k))
|
|
|
|
|
|
|
|
|
|
log.debug(entries)
|
|
|
|
|
return entries
|
|
|
|
@@ -557,7 +605,7 @@ class Client(object):
|
|
|
|
|
:param user_ids: One or more user ID(s) to query
|
|
|
|
|
:return: :class:`models.User` objects, labeled by their ID
|
|
|
|
|
:rtype: dict
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
threads = self.fetchThreadInfo(*user_ids)
|
|
|
|
@@ -566,7 +614,7 @@ class Client(object):
|
|
|
|
|
if threads[k].type == ThreadType.USER:
|
|
|
|
|
users[k] = threads[k]
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('Thread {} was not a user'.format(threads[k]))
|
|
|
|
|
raise FBchatUserError('Thread {} was not a user'.format(threads[k]))
|
|
|
|
|
|
|
|
|
|
return users
|
|
|
|
|
|
|
|
|
@@ -580,7 +628,7 @@ class Client(object):
|
|
|
|
|
:param page_ids: One or more page ID(s) to query
|
|
|
|
|
:return: :class:`models.Page` objects, labeled by their ID
|
|
|
|
|
:rtype: dict
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
threads = self.fetchThreadInfo(*page_ids)
|
|
|
|
@@ -589,7 +637,7 @@ class Client(object):
|
|
|
|
|
if threads[k].type == ThreadType.PAGE:
|
|
|
|
|
pages[k] = threads[k]
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('Thread {} was not a page'.format(threads[k]))
|
|
|
|
|
raise FBchatUserError('Thread {} was not a page'.format(threads[k]))
|
|
|
|
|
|
|
|
|
|
return pages
|
|
|
|
|
|
|
|
|
@@ -600,7 +648,7 @@ class Client(object):
|
|
|
|
|
:param group_ids: One or more group ID(s) to query
|
|
|
|
|
:return: :class:`models.Group` objects, labeled by their ID
|
|
|
|
|
:rtype: dict
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
threads = self.fetchThreadInfo(*group_ids)
|
|
|
|
@@ -609,7 +657,7 @@ class Client(object):
|
|
|
|
|
if threads[k].type == ThreadType.GROUP:
|
|
|
|
|
groups[k] = threads[k]
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('Thread {} was not a group'.format(threads[k]))
|
|
|
|
|
raise FBchatUserError('Thread {} was not a group'.format(threads[k]))
|
|
|
|
|
|
|
|
|
|
return groups
|
|
|
|
|
|
|
|
|
@@ -623,7 +671,7 @@ class Client(object):
|
|
|
|
|
:param thread_ids: One or more thread ID(s) to query
|
|
|
|
|
:return: :class:`models.Thread` objects, labeled by their ID
|
|
|
|
|
:rtype: dict
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
queries = []
|
|
|
|
@@ -662,14 +710,14 @@ class Client(object):
|
|
|
|
|
elif entry.get('thread_type') == 'ONE_TO_ONE':
|
|
|
|
|
_id = entry['thread_key']['other_user_id']
|
|
|
|
|
if pages_and_users.get(_id) is None:
|
|
|
|
|
raise Exception('Could not fetch thread {}'.format(_id))
|
|
|
|
|
raise FBchatException('Could not fetch thread {}'.format(_id))
|
|
|
|
|
entry.update(pages_and_users[_id])
|
|
|
|
|
if entry['type'] == ThreadType.USER:
|
|
|
|
|
rtn[_id] = graphql_to_user(entry)
|
|
|
|
|
else:
|
|
|
|
|
rtn[_id] = graphql_to_page(entry)
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('{} had an unknown thread type: {}'.format(thread_ids[i], entry))
|
|
|
|
|
raise FBchatException('{} had an unknown thread type: {}'.format(thread_ids[i], entry))
|
|
|
|
|
|
|
|
|
|
return rtn
|
|
|
|
|
|
|
|
|
@@ -684,7 +732,7 @@ class Client(object):
|
|
|
|
|
:type before: int
|
|
|
|
|
:return: :class:`models.Message` objects
|
|
|
|
|
:rtype: list
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
j = self.graphql_request(GraphQL(doc_id='1386147188135407', params={
|
|
|
|
@@ -695,55 +743,64 @@ class Client(object):
|
|
|
|
|
'before': before
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
if j['message_thread'] is None:
|
|
|
|
|
raise Exception('Could not fetch thread {}'.format(thread_id))
|
|
|
|
|
if j.get('message_thread') is None:
|
|
|
|
|
raise FBchatException('Could not fetch thread {}: {}'.format(thread_id, j))
|
|
|
|
|
|
|
|
|
|
return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']]))
|
|
|
|
|
|
|
|
|
|
def fetchThreadList(self, offset=0, limit=20):
|
|
|
|
|
def fetchThreadList(self, offset=0, limit=20, thread_location=ThreadLocation.INBOX):
|
|
|
|
|
"""Get thread list of your facebook account
|
|
|
|
|
|
|
|
|
|
:param offset: The offset, from where in the list to recieve threads from
|
|
|
|
|
:param limit: Max. number of threads to retrieve. Capped at 20
|
|
|
|
|
:param thread_location: models.ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER
|
|
|
|
|
:type offset: int
|
|
|
|
|
:type limit: int
|
|
|
|
|
:return: :class:`models.Thread` objects
|
|
|
|
|
:rtype: list
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if limit > 20 or limit < 1:
|
|
|
|
|
raise Exception('`limit` should be between 1 and 20')
|
|
|
|
|
raise FBchatUserError('`limit` should be between 1 and 20')
|
|
|
|
|
|
|
|
|
|
if thread_location in ThreadLocation:
|
|
|
|
|
loc_str = thread_location.value
|
|
|
|
|
else:
|
|
|
|
|
raise FBchatUserError('"thread_location" must be a value of ThreadLocation')
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
'client' : self.client,
|
|
|
|
|
'inbox[offset]' : offset,
|
|
|
|
|
'inbox[limit]' : limit,
|
|
|
|
|
loc_str + '[offset]' : offset,
|
|
|
|
|
loc_str + '[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 Exception('Missing payload: {}, with data: {}'.format(j, data))
|
|
|
|
|
raise FBchatException('Missing payload: {}, with data: {}'.format(j, data))
|
|
|
|
|
|
|
|
|
|
participants = {}
|
|
|
|
|
for p in j['payload']['participants']:
|
|
|
|
|
if p['type'] == 'page':
|
|
|
|
|
participants[p['fbid']] = Page(p['fbid'], url=p['href'], photo=p['image_src'], name=p['name'])
|
|
|
|
|
elif p['type'] == 'user':
|
|
|
|
|
participants[p['fbid']] = User(p['fbid'], url=p['href'], first_name=p['short_name'], is_friend=p['is_friend'], gender=GENDERS[p['gender']], photo=p['image_src'], name=p['name'])
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('A participant had an unknown type {}: {}'.format(p['type'], p))
|
|
|
|
|
if 'participants' in j['payload']:
|
|
|
|
|
for p in j['payload']['participants']:
|
|
|
|
|
if p['type'] == 'page':
|
|
|
|
|
participants[p['fbid']] = Page(p['fbid'], url=p['href'], photo=p['image_src'], name=p['name'])
|
|
|
|
|
elif p['type'] == 'user':
|
|
|
|
|
participants[p['fbid']] = User(p['fbid'], url=p['href'], first_name=p['short_name'], is_friend=p['is_friend'], gender=GENDERS[p['gender']], photo=p['image_src'], name=p['name'])
|
|
|
|
|
else:
|
|
|
|
|
raise FBchatException('A participant had an unknown type {}: {}'.format(p['type'], p))
|
|
|
|
|
|
|
|
|
|
entries = []
|
|
|
|
|
for k in j['payload']['threads']:
|
|
|
|
|
if k['thread_type'] == 1:
|
|
|
|
|
if k['other_user_fbid'] not in participants:
|
|
|
|
|
raise Exception('A thread was not in participants: {}'.format(j['payload']))
|
|
|
|
|
entries.append(participants[k['other_user_fbid']])
|
|
|
|
|
elif k['thread_type'] == 2:
|
|
|
|
|
entries.append(Group(k['thread_fbid'], participants=set([p.strip('fbid:') for p in k['participants']]), photo=k['image_src'], name=k['name']))
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('A thread had an unknown thread type: {}'.format(k))
|
|
|
|
|
if 'threads' in j['payload']:
|
|
|
|
|
for k in j['payload']['threads']:
|
|
|
|
|
if k['thread_type'] == 1:
|
|
|
|
|
if k['other_user_fbid'] not in participants:
|
|
|
|
|
raise FBchatException('The thread {} was not in participants: {}'.format(k, j['payload']))
|
|
|
|
|
participants[k['other_user_fbid']].message_count = k['message_count']
|
|
|
|
|
entries.append(participants[k['other_user_fbid']])
|
|
|
|
|
elif k['thread_type'] == 2:
|
|
|
|
|
entries.append(Group(k['thread_fbid'], participants=set([p.strip('fbid:') for p in k['participants']]), photo=k['image_src'], name=k['name'], message_count=k['message_count']))
|
|
|
|
|
else:
|
|
|
|
|
raise FBchatException('A thread had an unknown thread type: {}'.format(k))
|
|
|
|
|
|
|
|
|
|
return entries
|
|
|
|
|
|
|
|
|
@@ -752,7 +809,7 @@ class Client(object):
|
|
|
|
|
.. todo::
|
|
|
|
|
Documenting this
|
|
|
|
|
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
form = {
|
|
|
|
|
'client': 'mercury_sync',
|
|
|
|
@@ -761,7 +818,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'],
|
|
|
|
@@ -820,7 +877,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]
|
|
|
|
@@ -828,7 +885,14 @@ class Client(object):
|
|
|
|
|
log.warning("Got multiple message ids' back: {}".format(message_ids))
|
|
|
|
|
message_id = message_ids[0]
|
|
|
|
|
except (KeyError, IndexError) as e:
|
|
|
|
|
raise Exception('Error when sending message: No message IDs could be found: {}'.format(j))
|
|
|
|
|
raise FBchatException('Error when sending message: No message IDs could be found: {}'.format(j))
|
|
|
|
|
|
|
|
|
|
# update JS token if received in response
|
|
|
|
|
if j.get('jsmods') is not None and j['jsmods'].get('require') is not None:
|
|
|
|
|
try:
|
|
|
|
|
self.payloadDefault['fb_dtsg'] = j['jsmods']['require'][0][3][0]
|
|
|
|
|
except (KeyError, IndexError) as e:
|
|
|
|
|
log.warning('Error when updating fb_dtsg. Facebook might have changed protocol!')
|
|
|
|
|
|
|
|
|
|
return message_id
|
|
|
|
|
|
|
|
|
@@ -841,7 +905,7 @@ class Client(object):
|
|
|
|
|
:param thread_type: See :ref:`intro_threads`
|
|
|
|
|
:type thread_type: models.ThreadType
|
|
|
|
|
:return: :ref:`Message ID <intro_message_ids>` of the sent message
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
|
|
|
|
data = self._getSendData(thread_id, thread_type)
|
|
|
|
@@ -865,7 +929,7 @@ class Client(object):
|
|
|
|
|
:type size: models.EmojiSize
|
|
|
|
|
:type thread_type: models.ThreadType
|
|
|
|
|
:return: :ref:`Message ID <intro_message_ids>` of the sent emoji
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
|
|
|
|
data = self._getSendData(thread_id, thread_type)
|
|
|
|
@@ -885,17 +949,20 @@ 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']
|
|
|
|
|
if not mimetype == 'image/gif':
|
|
|
|
|
return j['payload']['metadata'][0]['image_id']
|
|
|
|
|
else:
|
|
|
|
|
return j['payload']['metadata'][0]['gif_id']
|
|
|
|
|
|
|
|
|
|
def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER):
|
|
|
|
|
def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER, is_gif=False):
|
|
|
|
|
"""
|
|
|
|
|
Sends an already uploaded image to a thread. (Used by :func:`Client.sendRemoteImage` and :func:`Client.sendLocalImage`)
|
|
|
|
|
|
|
|
|
@@ -903,9 +970,10 @@ class Client(object):
|
|
|
|
|
:param message: Additional message
|
|
|
|
|
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
|
|
|
|
|
:param thread_type: See :ref:`intro_threads`
|
|
|
|
|
:param is_gif: if sending GIF, True, else False
|
|
|
|
|
:type thread_type: models.ThreadType
|
|
|
|
|
:return: :ref:`Message ID <intro_message_ids>` of the sent image
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
|
|
|
|
data = self._getSendData(thread_id, thread_type)
|
|
|
|
@@ -916,7 +984,10 @@ class Client(object):
|
|
|
|
|
data['specific_to_list[0]'] = 'fbid:' + str(thread_id)
|
|
|
|
|
data['specific_to_list[1]'] = 'fbid:' + str(self.uid)
|
|
|
|
|
|
|
|
|
|
data['image_ids[0]'] = image_id
|
|
|
|
|
if not is_gif:
|
|
|
|
|
data['image_ids[0]'] = image_id
|
|
|
|
|
else:
|
|
|
|
|
data['gif_ids[0]'] = image_id
|
|
|
|
|
|
|
|
|
|
return self._doSendRequest(data)
|
|
|
|
|
|
|
|
|
@@ -930,30 +1001,32 @@ class Client(object):
|
|
|
|
|
:param thread_type: See :ref:`intro_threads`
|
|
|
|
|
:type thread_type: models.ThreadType
|
|
|
|
|
:return: :ref:`Message ID <intro_message_ids>` of the sent image
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
|
|
|
|
mimetype = guess_type(image_url)[0]
|
|
|
|
|
is_gif = (mimetype == 'image/gif')
|
|
|
|
|
remote_image = requests.get(image_url).content
|
|
|
|
|
image_id = self._uploadImage(image_url, remote_image, mimetype)
|
|
|
|
|
return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type)
|
|
|
|
|
return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type, is_gif=is_gif)
|
|
|
|
|
|
|
|
|
|
def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER):
|
|
|
|
|
"""
|
|
|
|
|
Sends a local image to a thread
|
|
|
|
|
|
|
|
|
|
:param image_path: URL of an image to upload and send
|
|
|
|
|
:param image_path: Path of an image to upload and send
|
|
|
|
|
:param message: Additional message
|
|
|
|
|
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
|
|
|
|
|
:param thread_type: See :ref:`intro_threads`
|
|
|
|
|
:type thread_type: models.ThreadType
|
|
|
|
|
:return: :ref:`Message ID <intro_message_ids>` of the sent image
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
|
|
|
|
mimetype = guess_type(image_path)[0]
|
|
|
|
|
is_gif = (mimetype == 'image/gif')
|
|
|
|
|
image_id = self._uploadImage(image_path, open(image_path, 'rb'), mimetype)
|
|
|
|
|
return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type)
|
|
|
|
|
return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type, is_gif=is_gif)
|
|
|
|
|
|
|
|
|
|
def addUsersToGroup(self, user_ids, thread_id=None):
|
|
|
|
|
"""
|
|
|
|
@@ -963,7 +1036,7 @@ class Client(object):
|
|
|
|
|
:param thread_id: Group ID to add people to. See :ref:`intro_threads`
|
|
|
|
|
:type user_ids: list
|
|
|
|
|
:return: :ref:`Message ID <intro_message_ids>` of the executed action
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, None)
|
|
|
|
|
data = self._getSendData(thread_id, ThreadType.GROUP)
|
|
|
|
@@ -979,7 +1052,7 @@ class Client(object):
|
|
|
|
|
|
|
|
|
|
for i, user_id in enumerate(user_ids):
|
|
|
|
|
if user_id == self.uid:
|
|
|
|
|
raise Exception('Error when adding users: Cannot add self to group thread')
|
|
|
|
|
raise FBchatUserError('Error when adding users: Cannot add self to group thread')
|
|
|
|
|
else:
|
|
|
|
|
data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id)
|
|
|
|
|
|
|
|
|
@@ -991,7 +1064,7 @@ class Client(object):
|
|
|
|
|
|
|
|
|
|
:param user_id: User ID to remove
|
|
|
|
|
:param thread_id: Group ID to remove people from. See :ref:`intro_threads`
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, None)
|
|
|
|
@@ -1001,7 +1074,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):
|
|
|
|
|
"""
|
|
|
|
@@ -1012,7 +1085,7 @@ class Client(object):
|
|
|
|
|
:param thread_id: Group ID to change title of. See :ref:`intro_threads`
|
|
|
|
|
:param thread_type: See :ref:`intro_threads`
|
|
|
|
|
:type thread_type: models.ThreadType
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
|
|
|
@@ -1038,7 +1111,7 @@ class Client(object):
|
|
|
|
|
:param thread_id: User/Group ID to change color of. See :ref:`intro_threads`
|
|
|
|
|
:param thread_type: See :ref:`intro_threads`
|
|
|
|
|
:type thread_type: models.ThreadType
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
|
|
|
|
|
|
|
|
@@ -1048,7 +1121,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):
|
|
|
|
|
"""
|
|
|
|
@@ -1057,7 +1130,7 @@ class Client(object):
|
|
|
|
|
:param color: New thread color
|
|
|
|
|
:param thread_id: User/Group ID to change color of. See :ref:`intro_threads`
|
|
|
|
|
:type color: models.ThreadColor
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, None)
|
|
|
|
|
|
|
|
|
@@ -1066,7 +1139,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):
|
|
|
|
|
"""
|
|
|
|
@@ -1076,7 +1149,7 @@ class Client(object):
|
|
|
|
|
|
|
|
|
|
:param color: New thread emoji
|
|
|
|
|
:param thread_id: User/Group ID to change emoji of. See :ref:`intro_threads`
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, None)
|
|
|
|
|
|
|
|
|
@@ -1085,7 +1158,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):
|
|
|
|
|
"""
|
|
|
|
@@ -1094,7 +1167,7 @@ class Client(object):
|
|
|
|
|
:param message_id: :ref:`Message ID <intro_message_ids>` to react to
|
|
|
|
|
:param reaction: Reaction emoji to use
|
|
|
|
|
:type reaction: models.MessageReaction
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
full_data = {
|
|
|
|
|
"doc_id": 1491398900900362,
|
|
|
|
@@ -1117,7 +1190,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):
|
|
|
|
|
"""
|
|
|
|
@@ -1128,7 +1201,7 @@ class Client(object):
|
|
|
|
|
:param thread_type: See :ref:`intro_threads`
|
|
|
|
|
:type status: models.TypingStatus
|
|
|
|
|
:type thread_type: models.ThreadType
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
thread_id, thread_type = self._getThread(thread_id, None)
|
|
|
|
|
|
|
|
|
@@ -1139,7 +1212,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
|
|
|
|
@@ -1155,7 +1228,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):
|
|
|
|
@@ -1169,7 +1242,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):
|
|
|
|
@@ -1177,7 +1250,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):
|
|
|
|
@@ -1190,7 +1263,7 @@ class Client(object):
|
|
|
|
|
"action": "confirm"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r = self._post(ReqUrl.CONNECT, data)
|
|
|
|
|
r = self._post(self.req_url.CONNECT, data)
|
|
|
|
|
return r.ok
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1210,7 +1283,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"""
|
|
|
|
@@ -1221,10 +1294,10 @@ 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 'lb_info' not in j:
|
|
|
|
|
raise Exception('Missing lb_info')
|
|
|
|
|
if j.get('lb_info') is None:
|
|
|
|
|
raise FBchatException('Missing lb_info: {}'.format(j))
|
|
|
|
|
|
|
|
|
|
return j['lb_info']['sticky'], j['lb_info']['pool']
|
|
|
|
|
|
|
|
|
@@ -1238,7 +1311,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
|
|
|
|
@@ -1389,6 +1462,14 @@ class Client(object):
|
|
|
|
|
elif mtype == "deltaflow":
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Chat timestamp
|
|
|
|
|
elif mtype == "chatproxy-presence":
|
|
|
|
|
buddylist = {}
|
|
|
|
|
for _id in m.get('buddyList', {}):
|
|
|
|
|
payload = m['buddyList'][_id]
|
|
|
|
|
buddylist[_id] = payload.get('lat')
|
|
|
|
|
self.onChatTimestamp(buddylist=buddylist, msg=m)
|
|
|
|
|
|
|
|
|
|
# Unknown message type
|
|
|
|
|
else:
|
|
|
|
|
self.onUnknownMesssageType(msg=m)
|
|
|
|
@@ -1400,7 +1481,7 @@ class Client(object):
|
|
|
|
|
"""
|
|
|
|
|
Start listening from an external event loop
|
|
|
|
|
|
|
|
|
|
:raises: Exception if request failed
|
|
|
|
|
:raises: FBchatException if request failed
|
|
|
|
|
"""
|
|
|
|
|
self.listening = True
|
|
|
|
|
self.sticky, self.pool = self._fetchSticky()
|
|
|
|
@@ -1416,18 +1497,27 @@ class Client(object):
|
|
|
|
|
:rtype: bool
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
if markAlive: self._ping(self.sticky, self.pool)
|
|
|
|
|
try:
|
|
|
|
|
content = self._pullMessage(self.sticky, self.pool)
|
|
|
|
|
if content: self._parseMessage(content)
|
|
|
|
|
except requests.exceptions.RequestException:
|
|
|
|
|
pass
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return self.onListenError(exception=e)
|
|
|
|
|
if markAlive:
|
|
|
|
|
self._ping(self.sticky, self.pool)
|
|
|
|
|
content = self._pullMessage(self.sticky, self.pool)
|
|
|
|
|
if content:
|
|
|
|
|
self._parseMessage(content)
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
return False
|
|
|
|
|
except requests.exceptions.Timeout:
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
@@ -1469,7 +1559,7 @@ class Client(object):
|
|
|
|
|
|
|
|
|
|
def on2FACode(self):
|
|
|
|
|
"""Called when a 2FA code is needed to progress"""
|
|
|
|
|
input('Please enter your 2FA code --> ')
|
|
|
|
|
return input('Please enter your 2FA code --> ')
|
|
|
|
|
|
|
|
|
|
def onLoggedIn(self, email=None):
|
|
|
|
|
"""
|
|
|
|
@@ -1488,8 +1578,10 @@ class Client(object):
|
|
|
|
|
Called when an error was encountered while listening
|
|
|
|
|
|
|
|
|
|
:param exception: The exception that was encountered
|
|
|
|
|
:return: Whether the loop should keep running
|
|
|
|
|
"""
|
|
|
|
|
log.exception('Got exception while listening')
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def onMessage(self, mid=None, author_id=None, message=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}):
|
|
|
|
@@ -1676,6 +1768,14 @@ class Client(object):
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def onChatTimestamp(self, buddylist={}, msg={}):
|
|
|
|
|
"""
|
|
|
|
|
Called when the client receives chat online presence update
|
|
|
|
|
|
|
|
|
|
:param buddylist: A list of dicts with friend id and last seen timestamp
|
|
|
|
|
:param msg: A full set of the data recieved
|
|
|
|
|
"""
|
|
|
|
|
log.debug('Chat Timestamps received: {}'.format(buddylist))
|
|
|
|
|
|
|
|
|
|
def onUnknownMesssageType(self, msg={}):
|
|
|
|
|
"""
|
|
|
|
|