From a159999879f53cd42c3cc02dbfe884b875786cb5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 18:37:43 +0100 Subject: [PATCH 1/8] Remove redundant client variables --- fbchat/_client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index bb8347b0..09a91cd4 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -73,7 +73,6 @@ def __init__( self.seq = "0" # See `createPoll` for the reason for using `OrderedDict` here self.payloadDefault = OrderedDict() - self.client = "mercury" self.default_thread_id = None self.default_thread_type = None self.req_url = ReqUrl() @@ -308,12 +307,10 @@ def _resetValues(self): def _postLogin(self): self.payloadDefault = OrderedDict() self.client_id = hex(int(random() * 2147483648))[2:] - self.start_time = now() 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_{}".format(self.uid) self.ttstamp = "" r = self._get(self.req_url.BASE) @@ -1260,7 +1257,7 @@ def _getSendData(self, message=None, thread_id=None, thread_type=ThreadType.USER messageAndOTID = generateOfflineThreadingID() timestamp = now() data = { - "client": self.client, + "client": "mercury", "author": "fbid:{}".format(self.uid), "timestamp": timestamp, "source": "source:chat:web", @@ -2388,7 +2385,7 @@ def unmuteThreadMentions(self, thread_id=None): def _ping(self): data = { - "channel": self.user_channel, + "channel": "p_" + self.uid, "clientid": self.client_id, "partition": -2, "cap": 0, From 6ab298f6e819185ffea87771607cf91790a25802 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 18:38:57 +0100 Subject: [PATCH 2/8] Remove temporary `_postLogin` variables from the client --- fbchat/_client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 09a91cd4..4c325721 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -311,32 +311,32 @@ def _postLogin(self): if self.uid is None: raise FBchatException("Could not find c_user cookie") self.uid = str(self.uid) - self.ttstamp = "" r = self._get(self.req_url.BASE) soup = bs(r.text, "html.parser") fb_dtsg_element = soup.find("input", {"name": "fb_dtsg"}) if fb_dtsg_element: - self.fb_dtsg = fb_dtsg_element["value"] + fb_dtsg = fb_dtsg_element["value"] else: - self.fb_dtsg = re.search(r'name="fb_dtsg" value="(.*?)"', r.text).group(1) + fb_dtsg = re.search(r'name="fb_dtsg" value="(.*?)"', r.text).group(1) fb_h_element = soup.find("input", {"name": "h"}) if fb_h_element: self.fb_h = fb_h_element["value"] - for i in self.fb_dtsg: - self.ttstamp += str(ord(i)) - self.ttstamp += "2" + ttstamp = "" + for i in fb_dtsg: + ttstamp += str(ord(i)) + ttstamp += "2" # Set default payload self.payloadDefault["__rev"] = int( r.text.split('"client_revision":', 1)[1].split(",", 1)[0] ) self.payloadDefault["__user"] = self.uid self.payloadDefault["__a"] = "1" - self.payloadDefault["ttstamp"] = self.ttstamp - self.payloadDefault["fb_dtsg"] = self.fb_dtsg + self.payloadDefault["ttstamp"] = ttstamp + self.payloadDefault["fb_dtsg"] = fb_dtsg def _login(self): if not (self.email and self.password): From a079797fcae82c77e5f04ac1b4dd6949698e3852 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 18:39:56 +0100 Subject: [PATCH 3/8] Remove email/password client variables --- fbchat/_client.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 4c325721..28da1c11 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -99,9 +99,6 @@ def __init__( or not self.isLoggedIn() ): self.login(email, password, max_tries) - else: - self.email = email - self.password = password """ INTERNAL REQUEST METHODS @@ -338,18 +335,15 @@ def _postLogin(self): self.payloadDefault["ttstamp"] = ttstamp self.payloadDefault["fb_dtsg"] = fb_dtsg - def _login(self): - if not (self.email and self.password): - raise FBchatUserError("Email and password not found.") - + def _login(self, email, password): soup = bs(self._get(self.req_url.MOBILE).text, "html.parser") 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["email"] = email + data["pass"] = password data["login"] = "Log In" r = self._cleanPost(self.req_url.LOGIN, data) @@ -489,11 +483,8 @@ def login(self, email, password, max_tries=5): if not (email and password): raise FBchatUserError("Email and password not set") - self.email = email - self.password = password - for i in range(1, max_tries + 1): - login_successful, login_url = self._login() + login_successful, login_url = self._login(email, password) if not login_successful: log.warning( "Attempt #{} failed{}".format( From 9b81365b0a40bf67d07a402d2072ec3a29475f7e Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 18:50:11 +0100 Subject: [PATCH 4/8] Privatize `fb_h` and `client_id` variables These are sparsely used and badly named, so probably not externally depended on externally --- fbchat/_client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 28da1c11..c443c89a 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -303,7 +303,7 @@ def _resetValues(self): def _postLogin(self): self.payloadDefault = OrderedDict() - self.client_id = hex(int(random() * 2147483648))[2:] + self._client_id = hex(int(random() * 2147483648))[2:] self.uid = self._session.cookies.get_dict().get("c_user") if self.uid is None: raise FBchatException("Could not find c_user cookie") @@ -320,7 +320,7 @@ def _postLogin(self): fb_h_element = soup.find("input", {"name": "h"}) if fb_h_element: - self.fb_h = fb_h_element["value"] + self._fb_h = fb_h_element["value"] ttstamp = "" for i in fb_dtsg: @@ -510,11 +510,11 @@ def logout(self): :return: True if the action was successful :rtype: bool """ - if not hasattr(self, "fb_h"): + if not hasattr(self, "_fb_h"): h_r = self._post(self.req_url.MODERN_SETTINGS_MENU, {"pmid": "4"}) - self.fb_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1) + self._fb_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1) - data = {"ref": "mb", "h": self.fb_h} + data = {"ref": "mb", "h": self._fb_h} r = self._get(self.req_url.LOGOUT, data) @@ -1254,7 +1254,7 @@ def _getSendData(self, message=None, thread_id=None, thread_type=ThreadType.USER "source": "source:chat:web", "offline_threading_id": messageAndOTID, "message_id": messageAndOTID, - "threading_id": generateMessageID(self.client_id), + "threading_id": generateMessageID(self._client_id), "ephemeral_ttl_mode:": "0", } @@ -2377,7 +2377,7 @@ def unmuteThreadMentions(self, thread_id=None): def _ping(self): data = { "channel": "p_" + self.uid, - "clientid": self.client_id, + "clientid": self._client_id, "partition": -2, "cap": 0, "uid": self.uid, @@ -2394,7 +2394,7 @@ def _pullMessage(self): "msgs_recv": 0, "sticky_token": self.sticky, "sticky_pool": self.pool, - "clientid": self.client_id, + "clientid": self._client_id, "state": "active" if self._markAlive else "offline", } return self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) From 2f973f129dffc623a6f6210535f5aab1ed15a85c Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 18:52:01 +0100 Subject: [PATCH 5/8] Privatize default_thread_X client variables We have a setter method for them, so there should be no need to access these directly! --- fbchat/_client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index c443c89a..aa04b822 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -73,8 +73,8 @@ def __init__( self.seq = "0" # See `createPoll` for the reason for using `OrderedDict` here self.payloadDefault = OrderedDict() - self.default_thread_id = None - self.default_thread_type = None + self._default_thread_id = None + self._default_thread_type = None self.req_url = ReqUrl() self._markAlive = True self._buddylist = dict() @@ -539,8 +539,8 @@ def _getThread(self, given_thread_id=None, given_thread_type=None): :rtype: tuple """ if given_thread_id is None: - if self.default_thread_id is not None: - return self.default_thread_id, self.default_thread_type + if self._default_thread_id is not None: + return self._default_thread_id, self._default_thread_type else: raise ValueError("Thread ID is not set") else: @@ -554,8 +554,8 @@ def setDefaultThread(self, thread_id, thread_type): :param thread_type: See :ref:`intro_threads` :type thread_type: models.ThreadType """ - self.default_thread_id = thread_id - self.default_thread_type = thread_type + self._default_thread_id = thread_id + self._default_thread_type = thread_type def resetDefaultThread(self): """Resets default thread""" From c688d6406238b3bfaea78dc30ea2d38aee9a7870 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 19:19:45 +0100 Subject: [PATCH 6/8] Make Client.uid read-only Modifying `uid` was previously documented as giving undefined behaviour, now it'll throw an error --- fbchat/_client.py | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index aa04b822..7d770783 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -37,13 +37,14 @@ class Client(object): """Verify ssl certificate, set to False to allow debugging with a proxy""" listening = False """Whether the client is listening. Used when creating an external event loop to determine when to stop listening""" - uid = None - """ - The ID of the client. - Can be used as `thread_id`. See :ref:`intro_threads` for more info. - Note: Modifying this results in undefined behaviour - """ + @property + def uid(self): + """The ID of the client. + + Can be used as `thread_id`. See :ref:`intro_threads` for more info. + """ + return self._uid def __init__( self, @@ -299,15 +300,15 @@ def _resetValues(self): self._session = requests.session() self.req_counter = 1 self.seq = "0" - self.uid = None + self._uid = None def _postLogin(self): self.payloadDefault = OrderedDict() self._client_id = hex(int(random() * 2147483648))[2:] - self.uid = self._session.cookies.get_dict().get("c_user") - if self.uid is None: + 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._uid = str(self._uid) r = self._get(self.req_url.BASE) soup = bs(r.text, "html.parser") @@ -330,7 +331,7 @@ def _postLogin(self): self.payloadDefault["__rev"] = int( r.text.split('"client_revision":', 1)[1].split(",", 1)[0] ) - self.payloadDefault["__user"] = self.uid + self.payloadDefault["__user"] = self._uid self.payloadDefault["__a"] = "1" self.payloadDefault["ttstamp"] = ttstamp self.payloadDefault["fb_dtsg"] = fb_dtsg @@ -660,7 +661,7 @@ def fetchAllUsers(self): :rtype: list :raises: FBchatException if request failed """ - data = {"viewer": self.uid} + data = {"viewer": self._uid} j = self._post( self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True ) @@ -1249,7 +1250,7 @@ def _getSendData(self, message=None, thread_id=None, thread_type=ThreadType.USER timestamp = now() data = { "client": "mercury", - "author": "fbid:{}".format(self.uid), + "author": "fbid:{}".format(self._uid), "timestamp": timestamp, "source": "source:chat:web", "offline_threading_id": messageAndOTID, @@ -1709,7 +1710,7 @@ def createGroup(self, message, user_ids): if len(user_ids) < 2: raise FBchatUserError("Error when creating group: Not enough participants") - for i, user_id in enumerate(user_ids + [self.uid]): + for i, user_id in enumerate(user_ids + [self._uid]): data["specific_to_list[{}]".format(i)] = "fbid:{}".format(user_id) message_id, thread_id = self._doSendRequest(data, get_thread_id=True) @@ -1737,7 +1738,7 @@ def addUsersToGroup(self, user_ids, thread_id=None): user_ids = require_list(user_ids) for i, user_id in enumerate(user_ids): - if user_id == self.uid: + if user_id == self._uid: raise FBchatUserError( "Error when adding users: Cannot add self to group thread" ) @@ -1813,7 +1814,7 @@ def _usersApproval(self, user_ids, approve, thread_id=None): data = { "client_mutation_id": "0", - "actor_id": self.uid, + "actor_id": self._uid, "thread_fbid": thread_id, "user_ids": user_ids, "response": "ACCEPT" if approve else "DENY", @@ -1972,7 +1973,7 @@ def reactToMessage(self, message_id, reaction): data = { "action": "ADD_REACTION" if reaction else "REMOVE_REACTION", "client_mutation_id": "1", - "actor_id": self.uid, + "actor_id": self._uid, "message_id": str(message_id), "reaction": reaction.value if reaction else None, } @@ -2376,14 +2377,14 @@ def unmuteThreadMentions(self, thread_id=None): def _ping(self): data = { - "channel": "p_" + self.uid, + "channel": "p_" + self._uid, "clientid": self._client_id, "partition": -2, "cap": 0, - "uid": self.uid, + "uid": self._uid, "sticky_token": self.sticky, "sticky_pool": self.pool, - "viewer_uid": self.uid, + "viewer_uid": self._uid, "state": "active", } self._get(self.req_url.PING, data, fix_request=True, as_json=False) @@ -2984,7 +2985,7 @@ def _parseMessage(self, content): thread_id = str(thread_id) else: thread_type = ThreadType.USER - if author_id == self.uid: + if author_id == self._uid: thread_id = m.get("to") else: thread_id = author_id From 0fd86d05a110c9953503b246b13c554e832bdf59 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 19:03:48 +0100 Subject: [PATCH 7/8] Privatize sticky and pool client variables These have complicated semantics, and so are hopefully not depended on externally --- fbchat/_client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 7d770783..b01a5b36 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -68,7 +68,7 @@ def __init__( :type logging_level: int :raises: FBchatException on failed login """ - self.sticky, self.pool = (None, None) + self._sticky, self._pool = (None, None) self._session = requests.session() self.req_counter = 1 self.seq = "0" @@ -2382,8 +2382,8 @@ def _ping(self): "partition": -2, "cap": 0, "uid": self._uid, - "sticky_token": self.sticky, - "sticky_pool": self.pool, + "sticky_token": self._sticky, + "sticky_pool": self._pool, "viewer_uid": self._uid, "state": "active", } @@ -2393,8 +2393,8 @@ def _pullMessage(self): """Call pull api with seq value to get message data.""" data = { "msgs_recv": 0, - "sticky_token": self.sticky, - "sticky_pool": self.pool, + "sticky_token": self._sticky, + "sticky_pool": self._pool, "clientid": self._client_id, "state": "active" if self._markAlive else "offline", } @@ -2951,8 +2951,8 @@ def _parseMessage(self, content): self.seq = content.get("seq", "0") if "lb_info" in content: - self.sticky = content["lb_info"]["sticky"] - self.pool = content["lb_info"]["pool"] + self._sticky = content["lb_info"]["sticky"] + self._pool = content["lb_info"]["pool"] if "batches" in content: for batch in content["batches"]: @@ -3098,7 +3098,7 @@ def doOneListen(self, markAlive=None): def stopListening(self): """Cleans up the variables from startListening""" self.listening = False - self.sticky, self.pool = (None, None) + self._sticky, self._pool = (None, None) def listen(self, markAlive=None): """ From bfca20bb129675707cc466de21dbc58d504f426f Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 19:03:27 +0100 Subject: [PATCH 8/8] Privatize req_counter, payloadDefault and seq client variables These have complicated semantics, and so are hopefully not depended on externally --- fbchat/_client.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index b01a5b36..d80918c3 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -70,10 +70,10 @@ def __init__( """ self._sticky, self._pool = (None, None) self._session = requests.session() - self.req_counter = 1 - self.seq = "0" + self._req_counter = 1 + self._seq = "0" # See `createPoll` for the reason for using `OrderedDict` here - self.payloadDefault = OrderedDict() + self._payload_default = OrderedDict() self._default_thread_id = None self._default_thread_type = None self.req_url = ReqUrl() @@ -109,12 +109,12 @@ def _generatePayload(self, query): """Adds the following defaults to the payload: __rev, __user, __a, ttstamp, fb_dtsg, __req """ - payload = self.payloadDefault.copy() + payload = self._payload_default.copy() if query: payload.update(query) - payload["__req"] = str_base(self.req_counter, 36) - payload["seq"] = self.seq - self.req_counter += 1 + payload["__req"] = str_base(self._req_counter, 36) + payload["seq"] = self._seq + self._req_counter += 1 return payload def _fix_fb_errors(self, error_code): @@ -212,7 +212,7 @@ def _cleanGet(self, url, query=None, timeout=30, allow_redirects=True): ) def _cleanPost(self, url, query=None, timeout=30): - self.req_counter += 1 + self._req_counter += 1 return self._session.post( url, headers=self._header, @@ -296,14 +296,14 @@ def graphql_request(self, query): """ def _resetValues(self): - self.payloadDefault = OrderedDict() + self._payload_default = OrderedDict() self._session = requests.session() - self.req_counter = 1 - self.seq = "0" + self._req_counter = 1 + self._seq = "0" self._uid = None def _postLogin(self): - self.payloadDefault = OrderedDict() + self._payload_default = OrderedDict() self._client_id = hex(int(random() * 2147483648))[2:] self._uid = self._session.cookies.get_dict().get("c_user") if self._uid is None: @@ -328,13 +328,13 @@ def _postLogin(self): ttstamp += str(ord(i)) ttstamp += "2" # Set default payload - self.payloadDefault["__rev"] = int( + self._payload_default["__rev"] = int( r.text.split('"client_revision":', 1)[1].split(",", 1)[0] ) - self.payloadDefault["__user"] = self._uid - self.payloadDefault["__a"] = "1" - self.payloadDefault["ttstamp"] = ttstamp - self.payloadDefault["fb_dtsg"] = fb_dtsg + self._payload_default["__user"] = self._uid + self._payload_default["__a"] = "1" + self._payload_default["ttstamp"] = ttstamp + self._payload_default["fb_dtsg"] = fb_dtsg def _login(self, email, password): soup = bs(self._get(self.req_url.MOBILE).text, "html.parser") @@ -1320,7 +1320,7 @@ def _doSendRequest(self, data, get_thread_id=False): # update JS token if received in response fb_dtsg = get_jsmods_require(j, 2) if fb_dtsg is not None: - self.payloadDefault["fb_dtsg"] = fb_dtsg + self._payload_default["fb_dtsg"] = fb_dtsg try: message_ids = [ @@ -2069,7 +2069,7 @@ def createPoll(self, poll, thread_id=None): # We're using ordered dicts, because the Facebook endpoint that parses the POST # parameters is badly implemented, and deals with ordering the options wrongly. - # This also means we had to change `client.payloadDefault` to an ordered dict, + # This also means we had to change `client._payload_default` to an ordered dict, # since that's being copied in between this point and the `requests` call # # If you can find a way to fix this for the endpoint, or if you find another @@ -2948,7 +2948,7 @@ def getThreadIdAndThreadType(msg_metadata): def _parseMessage(self, content): """Get message and author name from content. May contain multiple messages in the content.""" - self.seq = content.get("seq", "0") + self._seq = content.get("seq", "0") if "lb_info" in content: self._sticky = content["lb_info"]["sticky"]