Merge branch 'pr/338'

This commit is contained in:
Mads Marquart
2019-01-31 19:29:54 +01:00
2 changed files with 150 additions and 50 deletions

View File

@@ -62,6 +62,8 @@ class Client(object):
self.default_thread_id = None self.default_thread_id = None
self.default_thread_type = None self.default_thread_type = None
self.req_url = ReqUrl() self.req_url = ReqUrl()
self._markAlive = True
self._buddylist = dict()
if not user_agent: if not user_agent:
user_agent = choice(USER_AGENTS) user_agent = choice(USER_AGENTS)
@@ -1008,6 +1010,20 @@ class Client(object):
data = self._getPrivateData() data = self._getPrivateData()
return [j['display_email'] for j in data['all_emails']] return [j['display_email'] for j in data['all_emails']]
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(str(user_id))
""" """
END FETCH METHODS END FETCH METHODS
""" """
@@ -1901,7 +1917,7 @@ class Client(object):
.. todo:: .. todo::
Documenting this 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 return r.ok
def friendConnect(self, friend_id): def friendConnect(self, friend_id):
@@ -2129,7 +2145,7 @@ class Client(object):
} }
self._get(self.req_url.PING, data, fix_request=True, as_json=False) 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.""" """Call pull api with seq value to get message data."""
data = { data = {
@@ -2137,7 +2153,7 @@ class Client(object):
"sticky_token": self.sticky, "sticky_token": self.sticky,
"sticky_pool": self.pool, "sticky_pool": self.pool,
"clientid": self.client_id, "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) j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True)
@@ -2237,7 +2253,7 @@ class Client(object):
image_metadata = fetch_data.get("image_with_metadata") image_metadata = fetch_data.get("image_with_metadata")
image_id = int(image_metadata["legacy_attachment_id"]) if image_metadata else None 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, 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 # Nickname change
elif delta_type == "change_thread_nickname": elif delta_type == "change_thread_nickname":
@@ -2545,12 +2561,48 @@ class Client(object):
# Chat timestamp # Chat timestamp
elif mtype == "chatproxy-presence": elif mtype == "chatproxy-presence":
buddylist = {} buddylist = dict()
for _id in m.get('buddyList', {}): for _id in m.get('buddyList', {}):
payload = m['buddyList'][_id] payload = m['buddyList'][_id]
buddylist[_id] = payload.get('lat')
last_active = payload.get('lat')
active = payload.get('p') in [2, 3]
in_game = int(_id) in m.get('gamers', {})
buddylist[_id] = last_active
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) 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
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 # Unknown message type
else: else:
self.onUnknownMesssageType(msg=m) self.onUnknownMesssageType(msg=m)
@@ -2566,20 +2618,24 @@ class Client(object):
""" """
self.listening = True self.listening = True
def doOneListen(self, markAlive=True): def doOneListen(self, markAlive=None):
""" """
Does one cycle of the listening loop. Does one cycle of the listening loop.
This method is useful if you want to control fbchat from an external event 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 .. warning::
:type markAlive: bool `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 :return: Whether the loop should keep running
:rtype: bool :rtype: bool
""" """
if markAlive is not None:
self._markAlive = markAlive
try: try:
if markAlive: if self._markAlive:
self._ping() self._ping()
content = self._pullMessage(markAlive) content = self._pullMessage()
if content: if content:
self._parseMessage(content) self._parseMessage(content)
except KeyboardInterrupt: except KeyboardInterrupt:
@@ -2606,21 +2662,33 @@ class Client(object):
self.listening = False self.listening = False
self.sticky, self.pool = (None, None) self.sticky, self.pool = (None, None)
def listen(self, markAlive=True): def listen(self, markAlive=None):
""" """
Initializes and runs the listening loop continually Initializes and runs the listening loop continually
:param markAlive: Whether this should ping the Facebook server each time the loop runs :param markAlive: Whether this should ping the Facebook server each time the loop runs
:type markAlive: bool :type markAlive: bool
""" """
if markAlive is not None:
self.setActiveStatus(markAlive)
self.startListening() self.startListening()
self.onListening() self.onListening()
while self.listening and self.doOneListen(markAlive): while self.listening and self.doOneListen():
pass pass
self.stopListening() self.stopListening()
def setActiveStatus(self, 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 END LISTEN METHODS
""" """
@@ -2732,15 +2800,18 @@ class Client(object):
log.info("Title change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_title)) 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 Called when the client is listening, and somebody changes the image of a thread
:param mid: The action ID :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 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_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 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)) log.info("{} changed thread image in {}".format(author_id, thread_id))
@@ -3018,41 +3089,6 @@ class Client(object):
""" """
log.info("{} sent live location info in {} ({}) with latitude {} and longitude {}".format(author_id, thread_id, thread_type, location.latitude, location.longitude)) log.info("{} sent live location info in {} ({}) with latitude {} and longitude {}".format(author_id, thread_id, thread_type, location.latitude, location.longitude))
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): 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:: .. todo::
@@ -3231,6 +3267,51 @@ class Client(object):
else: else:
log.info("{} won't take part in {} in {} ({})".format(author_id, plan, thread_id, thread_type.name)) 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 END EVENTS
""" """

View File

@@ -674,6 +674,25 @@ class Plan(object):
def __unicode__(self): def __unicode__(self):
return '<Plan ({}): {} time={}, location={}, location_id={}>'.format(self.uid, repr(self.title), self.time, repr(self.location), repr(self.location_id)) return '<Plan ({}): {} time={}, location={}, location_id={}>'.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 '<ActiveStatus: active={} last_active={} in_game={}>'.format(self.active, self.last_active, self.in_game)
class Enum(aenum.Enum): class Enum(aenum.Enum):
"""Used internally by fbchat to support enumerations""" """Used internally by fbchat to support enumerations"""
def __repr__(self): def __repr__(self):