From 345a473ee06b5eb15b4b72cee593baa0a000fb8d Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 22 Sep 2018 21:34:44 +0200 Subject: [PATCH 1/7] `ActiveStatus` model --- fbchat/models.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/fbchat/models.py b/fbchat/models.py index cb4f678..019aa8e 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -520,6 +520,25 @@ class Plan(object): def __unicode__(self): return ''.format(self.uid, repr(self.title), self.time, repr(self.location), repr(self.location_id)) +class ActiveStatus(object): + #: Whether the user is active now + active = None + #: Timestamp when the user was last active + last_active = None + #: Whether the user is playing Messenger game now + in_game = None + + def __init__(self, active=None, last_active=None, in_game=None): + self.active = active + self.last_active = last_active + self.in_game = in_game + + def __repr__(self): + return self.__unicode__() + + def __unicode__(self): + return ''.format(self.active, self.last_active, self.in_game) + class Enum(enum.Enum): """Used internally by fbchat to support enumerations""" def __repr__(self): From 47ea88e025b99c3cb5133e66fa872be1a7c05ac9 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 22 Sep 2018 21:52:40 +0200 Subject: [PATCH 2/7] Read commit description - Fixed `onImageChange` documentation and added missing `msg` parameter - Moved `on` methods to the right place - Added changing client active status while listening - Added fetching friends' active status --- fbchat/client.py | 141 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 44 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 69c858d..d256c6b 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -62,6 +62,8 @@ class Client(object): self.default_thread_id = None self.default_thread_type = None self.req_url = ReqUrl() + self._markAlive = True + self._buddylist = dict() if not user_agent: user_agent = choice(USER_AGENTS) @@ -989,6 +991,15 @@ class Client(object): plan = graphql_to_plan(j["payload"]) return plan + def getUserActiveStatus(self, user_id): + """ + Gets friend active status as an :class:`models.ActiveStatus` object + + :param user_id: ID of the user + :rtype: models.ActiveStatus + """ + return self._buddylist.get(user_id) + """ END FETCH METHODS """ @@ -1747,7 +1758,7 @@ class Client(object): .. todo:: Documenting this """ - r = self._post(self.req_url.MARK_SEEN, {"seen_timestamp": 0}) + r = self._post(self.req_url.MARK_SEEN, {"seen_timestamp": now()}) return r.ok def friendConnect(self, friend_id): @@ -2090,7 +2101,7 @@ class Client(object): image_metadata = fetch_data.get("image_with_metadata") image_id = int(image_metadata["legacy_attachment_id"]) if image_metadata else None self.onImageChange(mid=mid, author_id=author_id, new_image=image_id, thread_id=thread_id, - thread_type=ThreadType.GROUP, ts=ts) + thread_type=ThreadType.GROUP, ts=ts, msg=m) # Nickname change elif delta_type == "change_thread_nickname": @@ -2335,12 +2346,27 @@ class Client(object): # Chat timestamp elif mtype == "chatproxy-presence": - buddylist = {} + buddylist = dict() for _id in m.get('buddyList', {}): payload = m['buddyList'][_id] - buddylist[_id] = payload.get('lat') + last_active = payload.get('lat') + active = time.time() - last_active <= 60 + in_game = int(_id) in m.get('gamers', {}) + buddylist[_id] = last_active + self._buddylist[_id] = ActiveStatus(active=active, last_active=last_active, in_game=in_game) self.onChatTimestamp(buddylist=buddylist, msg=m) + elif mtype == "buddylist_overlay": + statuses = dict() + for _id in m.get('overlay', {}): + payload = m['overlay'][_id] + last_active = payload.get('la') + active = payload.get('a') in [2, 3] + in_game = self._buddylist[_id].in_game if self._buddylist.get(_id) else False + self._buddylist[_id] = ActiveStatus(active=active, last_active=last_active, in_game=in_game) + statuses[_id] = ActiveStatus(active=active, last_active=last_active, in_game=in_game) + self.onBuddylistOverlay(statuses=statuses, msg=m) + # Unknown message type else: self.onUnknownMesssageType(msg=m) @@ -2357,7 +2383,7 @@ class Client(object): self.listening = True self.sticky, self.pool = self._fetchSticky() - def doOneListen(self, markAlive=True): + def doOneListen(self, markAlive=None): """ Does one cycle of the listening loop. This method is useful if you want to control fbchat from an external event loop @@ -2367,6 +2393,8 @@ class Client(object): :return: Whether the loop should keep running :rtype: bool """ + if markAlive is None: + markAlive = self._markAlive try: if markAlive: self._ping(self.sticky, self.pool) @@ -2397,21 +2425,33 @@ class Client(object): self.listening = False self.sticky, self.pool = (None, None) - def listen(self, markAlive=True): + def listen(self, markAlive=None): """ Initializes and runs the listening loop continually :param markAlive: Whether this should ping the Facebook server each time the loop runs :type markAlive: bool """ + if markAlive is not None: + self._markAlive = markAlive + self.startListening() self.onListening() - while self.listening and self.doOneListen(markAlive): + while self.listening and self.doOneListen(): pass self.stopListening() + def setActiveStatus(markAlive): + """ + Changes client active status while listening + + :param markAlive: Whether to show if client is active + :type markAlive: bool + """ + self._markAlive = markAlive + """ END LISTEN METHODS """ @@ -2523,15 +2563,18 @@ class Client(object): log.info("Title change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_title)) - def onImageChange(self, mid=None, author_id=None, new_image=None, thread_id=None, thread_type=ThreadType.GROUP, ts=None): + def onImageChange(self, mid=None, author_id=None, new_image=None, thread_id=None, thread_type=ThreadType.GROUP, ts=None, msg=None): """ Called when the client is listening, and somebody changes the image of a thread :param mid: The action ID - :param new_image: The ID of the new image :param author_id: The ID of the person who changed the image + :param new_image: The ID of the new image :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` :param ts: A timestamp of the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType """ log.info("{} changed thread image in {}".format(author_id, thread_id)) @@ -2723,41 +2766,6 @@ class Client(object): """ log.info("{} played \"{}\" in {} ({})".format(author_id, game_name, thread_id, thread_type.name)) - def onQprimer(self, ts=None, msg=None): - """ - Called when the client just started listening - - :param ts: A timestamp of the action - :param msg: A full set of the data recieved - """ - pass - - def onChatTimestamp(self, buddylist=None, msg=None): - """ - 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=None): - """ - Called when the client is listening, and some unknown data was recieved - - :param msg: A full set of the data recieved - """ - log.debug('Unknown message received: {}'.format(msg)) - - def onMessageError(self, exception=None, msg=None): - """ - Called when an error was encountered while parsing recieved data - - :param exception: The exception that was encountered - :param msg: A full set of the data recieved - """ - log.exception('Exception in parsing of {}'.format(msg)) - def onCallStarted(self, mid=None, caller_id=None, is_video_call=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): """ .. todo:: @@ -2935,6 +2943,51 @@ class Client(object): else: log.info("{} won't take part in {} in {} ({})".format(author_id, plan, thread_id, thread_type.name)) + def onQprimer(self, ts=None, msg=None): + """ + Called when the client just started listening + + :param ts: A timestamp of the action + :param msg: A full set of the data recieved + """ + pass + + def onChatTimestamp(self, buddylist=None, msg=None): + """ + 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 onBuddylistOverlay(self, statuses=None, msg=None): + """ + Called when the client is listening and client receives information about friend active status + + :param statuses: Dictionary with user IDs as keys and :class:`models.ActiveStatus` as values + :param msg: A full set of the data recieved + :type statuses: dict + """ + log.debug('Buddylist overlay received: {}'.format(statuses)) + + def onUnknownMesssageType(self, msg=None): + """ + Called when the client is listening, and some unknown data was recieved + + :param msg: A full set of the data recieved + """ + log.debug('Unknown message received: {}'.format(msg)) + + def onMessageError(self, exception=None, msg=None): + """ + Called when an error was encountered while parsing recieved data + + :param exception: The exception that was encountered + :param msg: A full set of the data recieved + """ + log.exception('Exception in parsing of {}'.format(msg)) + """ END EVENTS """ From e38f8916934caf50f8d48f0c210ec7a4a5affeeb Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Tue, 30 Oct 2018 21:48:55 +0100 Subject: [PATCH 3/7] Active status fixes --- fbchat/client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index d256c6b..77b6b0a 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -993,7 +993,10 @@ class Client(object): def getUserActiveStatus(self, user_id): """ - Gets friend active status as an :class:`models.ActiveStatus` object + Gets friend active status as an :class:`models.ActiveStatus` object. + + .. warning:: + Only works when listening. :param user_id: ID of the user :rtype: models.ActiveStatus @@ -2433,7 +2436,7 @@ class Client(object): :type markAlive: bool """ if markAlive is not None: - self._markAlive = markAlive + self.setActiveStatus(markAlive) self.startListening() self.onListening() @@ -2443,7 +2446,7 @@ class Client(object): self.stopListening() - def setActiveStatus(markAlive): + def setActiveStatus(self, markAlive): """ Changes client active status while listening From d0c5f29b0a97d4ed5c08ca7e472d7c00ac4b2c99 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 5 Jan 2019 18:24:23 +0100 Subject: [PATCH 4/7] Fixed getting active status --- fbchat/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index b8d050f..e8e019a 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -989,14 +989,16 @@ class Client(object): def getUserActiveStatus(self, user_id): """ Gets friend active status as an :class:`models.ActiveStatus` object. + Returns `None` if status isn't known. .. warning:: Only works when listening. :param user_id: ID of the user + :return: Given user active status :rtype: models.ActiveStatus """ - return self._buddylist.get(user_id) + return self._buddylist.get(str(user_id)) """ END FETCH METHODS From c357fd085bd44014dd2a624ff024e1d4cf617bc9 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 5 Jan 2019 18:36:48 +0100 Subject: [PATCH 5/7] Better listening for buddylist overlay and chatbox presence --- fbchat/client.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index e8e019a..858cbcd 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -2491,22 +2491,43 @@ class Client(object): buddylist = dict() for _id in m.get('buddyList', {}): payload = m['buddyList'][_id] + last_active = payload.get('lat') - active = time.time() - last_active <= 60 + active = payload.get('p') in [2, 3] in_game = int(_id) in m.get('gamers', {}) + buddylist[_id] = last_active - self._buddylist[_id] = ActiveStatus(active=active, last_active=last_active, in_game=in_game) + + if self._buddylist.get(_id): + self._buddylist[_id].last_active = last_active + self._buddylist[_id].active = active + self._buddylist[_id].in_game = in_game + else: + self._buddylist[_id] = ActiveStatus(active=active, last_active=last_active, in_game=in_game) + self.onChatTimestamp(buddylist=buddylist, msg=m) + # Buddylist overlay elif mtype == "buddylist_overlay": statuses = dict() for _id in m.get('overlay', {}): payload = m['overlay'][_id] + last_active = payload.get('la') active = payload.get('a') in [2, 3] in_game = self._buddylist[_id].in_game if self._buddylist.get(_id) else False - self._buddylist[_id] = ActiveStatus(active=active, last_active=last_active, in_game=in_game) - statuses[_id] = ActiveStatus(active=active, last_active=last_active, in_game=in_game) + + status = ActiveStatus(active=active, last_active=last_active, in_game=in_game) + + if self._buddylist.get(_id): + self._buddylist[_id].last_active = last_active + self._buddylist[_id].active = active + self._buddylist[_id].in_game = in_game + else: + self._buddylist[_id] = status + + statuses[_id] = status + self.onBuddylistOverlay(statuses=statuses, msg=m) # Unknown message type From e80a040db4bdfbd76b6ccb063afee62901491cc1 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 5 Jan 2019 18:48:40 +0100 Subject: [PATCH 6/7] Deprecate markAlive parameter in doOneListen and _pullMessage --- fbchat/client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 858cbcd..df5da1c 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -2072,7 +2072,7 @@ class Client(object): } self._get(self.req_url.PING, data, fix_request=True, as_json=False) - def _pullMessage(self, markAlive=True): + def _pullMessage(self): """Call pull api with seq value to get message data.""" data = { @@ -2080,7 +2080,7 @@ class Client(object): "sticky_token": self.sticky, "sticky_pool": self.pool, "clientid": self.client_id, - 'state': 'active' if markAlive else 'offline', + 'state': 'active' if self._markAlive else 'offline', } j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) @@ -2545,22 +2545,22 @@ class Client(object): """ self.listening = True - def doOneListen(self, markAlive=None): + def doOneListen(self): """ Does one cycle of the listening loop. This method is useful if you want to control fbchat from an external event loop - :param markAlive: Whether this should ping the Facebook server before running - :type markAlive: bool + .. warning:: + `markAlive` parameter is deprecated now, use :func:`fbchat.Client.setActiveStatus` + or `markAlive` parameter in :func:`fbchat.Client.listen` instead. + :return: Whether the loop should keep running :rtype: bool """ - if markAlive is None: - markAlive = self._markAlive try: - if markAlive: + if self._markAlive: self._ping() - content = self._pullMessage(markAlive) + content = self._pullMessage() if content: self._parseMessage(content) except KeyboardInterrupt: From 2b580c60e9d2933986118d8d8909e22d92fb91fb Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 31 Jan 2019 19:23:46 +0100 Subject: [PATCH 7/7] Readd deprecated `markAlive` parameter --- fbchat/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index df5da1c..3a3da70 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -2545,7 +2545,7 @@ class Client(object): """ self.listening = True - def doOneListen(self): + def doOneListen(self, markAlive=None): """ Does one cycle of the listening loop. This method is useful if you want to control fbchat from an external event loop @@ -2557,6 +2557,8 @@ class Client(object): :return: Whether the loop should keep running :rtype: bool """ + if markAlive is not None: + self._markAlive = markAlive try: if self._markAlive: self._ping()