From 1700f95810afb53e4895bac8a345d50a8a7e1e5b Mon Sep 17 00:00:00 2001 From: Dainius Date: Sat, 6 May 2017 00:16:20 +0300 Subject: [PATCH 1/3] add event hooks, change method names --- fbchat/client.py | 361 +++++++++++++++++++------------------------ fbchat/event_hook.py | 57 +++++++ fbchat/models.py | 55 +++++++ fbchat/stickers.py | 8 - fbchat/utils.py | 12 +- 5 files changed, 281 insertions(+), 212 deletions(-) create mode 100644 fbchat/event_hook.py delete mode 100644 fbchat/stickers.py diff --git a/fbchat/client.py b/fbchat/client.py index 0595db3..d5914eb 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -21,9 +21,9 @@ from bs4 import BeautifulSoup as bs from mimetypes import guess_type from .utils import * from .models import * -from .stickers import * import time -import sys +from .event_hook import EventHook + # Python 3 does not have raw_input, whereas Python 2 has and it's more secure try: @@ -75,7 +75,6 @@ class Client(object): import fbchat chat = fbchat.Client(email, password) """ - if do_login and not (email and password): raise Exception("Email and password not found.") @@ -103,6 +102,37 @@ class Client(object): 11: 'unknown_plural', } + # Setup event hooks + self.onLoggingIn = EventHook() + self.onLoggedIn = EventHook() + + self.onMessage = EventHook(mid=str, author_id=str, message=str, recipient_id=int, msg_type=MsgType, ts=str, metadata=dict) + self.onColorChange = EventHook(mid=str, author_id=str, new_color=str, changed_for=str, msg_type=MsgType, ts=str) + self.onTitleChange = EventHook(mid=str, author_id=str, new_title=str, changed_for=str, thread_id=str, ts=str) + self.onTyping = EventHook(author_id=int, typing_status=TypingStatus) + self.onSeen = EventHook(seen_by=str, thread_id=str, timestamp=str) + + self.onInbox = EventHook(unseen=int, unread=int, recent_unread=int) + self.onPeopleAdded = EventHook(added_ids=list, author_id=str, thread_id=str) + self.onPersonRemoved = EventHook(removed_id=str, author_id=str, thread_id=str) + self.onFriendRequest = EventHook(from_id=str) + + # Setup event handlers + self.onLoggingIn += lambda: log.info("Logging in...") + self.onLoggedIn += lambda: log.info("Login successful.") + self.onListening += lambda: log.info("Listening...") + + self.onMessage += lambda mid, author_id, message, recipient_id, msg_type, ts, metadata:\ + log.info("Message from %s: %s" % (author_id, message)) + self.onColorChange += lambda mid, author_id, new_color, changed_for, msg_type, ts:\ + log.info("%s changed color to %s" % (author_id, new_color)) + self.onTitleChange += lambda mid, author_id, new_title, changed_for, thread_id, ts:\ + log.info("%s changed title of %s to %s" % (author_id, changed_for, new_title)) + self.onPeopleAdded += lambda added_ids, author_id, thread_id:\ + log.info("%s added: %s" % (author_id, [x for x in added_ids])) + self.onPersonRemoved += lambda removed_id, author_id, thread_id:\ + log.info("%s removed: %s" % (author_id, removed_id)) + if not user_agent: user_agent = choice(USER_AGENTS) @@ -151,11 +181,6 @@ class Client(object): DeprecationWarning) log.debug(msg) - def _setttstamp(self): - for i in self.fb_dtsg: - self.ttstamp += str(ord(i)) - self.ttstamp += '2' - def _generatePayload(self, query): """Adds the following defaults to the payload: __rev, __user, __a, ttstamp, fb_dtsg, __req @@ -187,7 +212,7 @@ class Client(object): payload=self._generatePayload(None) return self._session.post(url, data=payload, timeout=timeout, files=files) - def _post_login(self): + def _postLogin(self): self.payloadDefault = {} self.client_id = hex(int(random()*2147483648))[2:] self.start_time = now() @@ -201,7 +226,11 @@ class Client(object): log.debug(r.url) self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value'] self.fb_h = soup.find("input", {'name':'h'})['value'] - self._setttstamp() + + for i in self.fb_dtsg: + self.ttstamp += str(ord(i)) + self.ttstamp += '2' + # Set default payload self.payloadDefault['__rev'] = int(r.text.split('"revision":',1)[1].split(",",1)[0]) self.payloadDefault['__user'] = self.uid @@ -246,7 +275,7 @@ class Client(object): r = self._cleanGet(SaveDeviceURL) if 'home' in r.url: - self._post_login() + self._postLogin() return True else: return False @@ -328,14 +357,13 @@ class Client(object): return False # Load cookies into current session self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, j) - self._post_login() + self._postLogin() return True except Exception as e: raise Exception('Invalid json in {}, or bad merging of cookies'.format(sessionfile)) def login(self, email, password, max_retries=5): - # Logging in - log.info("Logging in...") + self.onLoggingIn() self.email = email self.password = password @@ -346,7 +374,7 @@ class Client(object): time.sleep(1) continue else: - log.info("Login successful.") + self.onLoggedIn() break else: raise Exception("Login failed. Check email/password.") @@ -376,39 +404,6 @@ class Client(object): self.def_is_user = is_user self.is_def_recipient_set = True - def _adapt_user_in_chat_to_user_model(self, user_in_chat): - """ Adapts user info from chat to User model acceptable initial dict - - :param user_in_chat: user info from chat - - 'dir': None, - 'mThumbSrcSmall': None, - 'is_friend': False, - 'is_nonfriend_messenger_contact': True, - 'alternateName': '', - 'i18nGender': 16777216, - 'vanity': '', - 'type': 'friend', - 'searchTokens': ['Voznesenskij', 'Sergej'], - 'thumbSrc': 'https://fb-s-b-a.akamaihd.net/h-ak-xfa1/v/t1.0-1/c9.0.32.32/p32x32/10354686_10150004552801856_220367501106153455_n.jpg?oh=71a87d76d4e4d17615a20c43fb8dbb47&oe=59118CE4&__gda__=1493753268_ae75cef40e9785398e744259ccffd7ff', - 'mThumbSrcLarge': None, - 'firstName': 'Sergej', - 'name': 'Sergej Voznesenskij', - 'uri': 'https://www.facebook.com/profile.php?id=100014812758264', - 'id': '100014812758264', - 'gender': 2 - """ - - return { - 'type': 'user', - 'uid': user_in_chat['id'], - 'photo': user_in_chat['thumbSrc'], - 'path': user_in_chat['uri'], - 'text': user_in_chat['name'], - 'score': '', - 'data': user_in_chat, - } - def getAllUsers(self): """ Gets all users from chat with info included """ @@ -426,7 +421,7 @@ class Client(object): for k in payload.keys(): try: - user = self._adapt_user_in_chat_to_user_model(payload[k]) + user = User.adaptFromChat(payload[k]) except KeyError: continue @@ -434,7 +429,6 @@ class Client(object): return users - def getUsers(self, name): """Find and get user by his/her name @@ -529,13 +523,10 @@ class Client(object): if image_id: data['image_ids[0]'] = image_id - if like: - try: - sticker = LIKES[like.lower()] - except KeyError: - # if user doesn't enter l or m or s, then use the large one - sticker = LIKES['l'] - data["sticker_id"] = sticker + if like and not type(like) is Sticker: + data["sticker_id"] = Sticker.SMALL.value + else: + data["sticker_id"] = like.value r = self._post(SendURL, data) @@ -556,7 +547,6 @@ class Client(object): log.debug("With data {}".format(data)) return True - def sendRemoteImage(self, recipient_id=None, message=None, is_user=True, image=''): """Send an image from a URL @@ -582,7 +572,6 @@ class Client(object): image_id = self.uploadImage({'file': (image, open(image, 'rb'), mimetype)}) return self.send(recipient_id, message, is_user, None, image_id) - def uploadImage(self, image): """Upload an image and get the image_id for sending in a message @@ -595,7 +584,6 @@ class Client(object): # Strip the start and parse out the returned image_id return json.loads(r._content[9:])['payload']['metadata'][0]['image_id'] - def getThreadInfo(self, userID, last_n=20, start=None, is_user=True): """Get the info of one Thread @@ -632,7 +620,6 @@ class Client(object): messages.append(Message(**message)) return list(reversed(messages)) - def getThreadList(self, start, length=20): """Get thread list of your facebook account. @@ -675,7 +662,6 @@ class Client(object): return self.threads - def getUnread(self): form = { 'client': 'mercury_sync', @@ -704,7 +690,6 @@ class Client(object): r = self._post(DeliveredURL, data) return r.ok - def markAsRead(self, userID): data = { "watermarkTimestamp": now(), @@ -715,13 +700,11 @@ class Client(object): r = self._post(ReadStatusURL, data) return r.ok - def markAsSeen(self): r = self._post(MarkSeenURL, {"seen_timestamp": 0}) return r.ok - - def friend_connect(self, friend_id): + def friendConnect(self, friend_id): data = { "to_friend": friend_id, "action": "confirm" @@ -731,7 +714,6 @@ class Client(object): return r.ok - def ping(self, sticky): data = { 'channel': self.user_channel, @@ -745,11 +727,8 @@ class Client(object): r = self._get(PingURL, data) return r.ok - def _getSticky(self): - """Call pull api to get sticky and pool parameter, - newer api needs these parameter to work. - """ + """Call pull api to get sticky and pool parameter, newer api needs these parameter to work.""" data = { "msgs_recv": 0, @@ -767,7 +746,6 @@ class Client(object): pool = j['lb_info']['pool'] return sticky, pool - def _pullMessage(self, sticky, pool): """Call pull api with seq value to get message data.""" @@ -785,7 +763,6 @@ class Client(object): self.seq = j.get('seq', '0') return j - def _parseMessage(self, content): """Get message and author name from content. May contains multiple messages in the content. @@ -794,71 +771,120 @@ class Client(object): if 'ms' not in content: return log.debug("Received {}".format(content["ms"])) - for m in content['ms']: + for m in content["ms"]: + mtype = m.get("type") try: - if m['type'] in ['m_messaging', 'messaging']: - if m['event'] in ['deliver']: - mid = m['message']['mid'] - message = m['message']['body'] - fbid = m['message']['sender_fbid'] - name = m['message']['sender_name'] - self.on_message(mid, fbid, name, message, m) - elif m['type'] in ['typ']: - self.on_typing(m.get("from")) - elif m['type'] in ['m_read_receipt']: - self.on_read(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) - elif m['type'] in ['inbox']: - viewer = m.get('realtime_viewer_fbid') - unseen = m.get('unseen') - unread = m.get('unread') - other_unseen = m.get('other_unseen') - other_unread = m.get('other_unread') - timestamp = m.get('seen_timestamp') - self.on_inbox(viewer, unseen, unread, other_unseen, other_unread, timestamp) - elif m['type'] in ['qprimer']: - self.on_qprimer(m.get('made')) - elif m['type'] in ['delta']: - if 'leftParticipantFbId' in m['delta']: - user_id = m['delta']['leftParticipantFbId'] - actor_id = m['delta']['messageMetadata']['actorFbId'] - thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] - self.on_person_removed(user_id, actor_id, thread_id) - elif 'addedParticipants' in m['delta']: - user_ids = [x['userFbId'] for x in m['delta']['addedParticipants']] - actor_id = m['delta']['messageMetadata']['actorFbId'] - thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] - self.on_people_added(user_ids, actor_id, thread_id) - elif 'messageMetadata' in m['delta']: - recipient_id = 0 - thread_type = None - if 'threadKey' in m['delta']['messageMetadata']: - if 'threadFbId' in m['delta']['messageMetadata']['threadKey']: - recipient_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] - thread_type = 'group' - elif 'otherUserFbId' in m['delta']['messageMetadata']['threadKey']: - recipient_id = m['delta']['messageMetadata']['threadKey']['otherUserFbId'] - thread_type = 'user' - mid = m['delta']['messageMetadata']['messageId'] - message = m['delta'].get('body','') - fbid = m['delta']['messageMetadata']['actorFbId'] - self.on_message_new(mid, fbid, message, m, recipient_id, thread_type) - elif m['type'] in ['jewel_requests_add']: - from_id = m['from'] - self.on_friend_request(from_id) + # Things that directly change chat + if mtype == "delta": + + def getRecipientIdAndMsgType(msg_metadata): + """Returns a tuple consisting of recipient id and message type""" + recipient = None + message_type = None + if 'threadFbId' in msg_metadata['threadKey']: + recipient = msg_metadata['threadKey']['threadFbId'] + message_type = MsgType.GROUP + elif 'otherUserFbId' in msg_metadata['threadKey']: + recipient = msg_metadata['threadKey']['otherUserFbId'] + message_type = MsgType.USER + return recipient, message_type + + delta = m["delta"] + delta_type = delta.get("type") + metadata = delta.get("messageMetadata") + + # Added participants + if 'addedParticipants' in delta: + added_ids = [x['userFbId'] for x in delta['addedParticipants']] + author_id = metadata['actorFbId'] + thread_id = metadata['threadKey']['threadFbId'] + ts = metadata["timestamp"] + self.onPeopleAdded(added_ids=added_ids, author_id=author_id, thread_id=thread_id, ts=ts) + + # Left/removed participants + elif 'leftParticipantFbId' in delta: + removed_id = delta['leftParticipantFbId'] + author_id = metadata['actorFbId'] + thread_id = metadata['threadKey']['threadFbId'] + ts = metadata["timestamp"] + self.onPersonRemoved(removed_id=removed_id, author_id=author_id, thread_id=thread_id, ts=ts) + + # Seen + elif delta.get("class") == "ReadReceipt": + seen_by = delta["actorFbId"] or delta["threadKey"]["otherUserFbId"] + thread_id = delta["threadKey"].get("threadFbId") + timestamp = delta["actionTimestampMs"] + self.onSeen(seen_by=seen_by, thread_id=thread_id, timestamp=timestamp) + + # Color change + elif delta_type == "change_thread_theme": + mid = metadata["messageId"] + author_id = metadata["actorFbId"] + new_color = delta["untypedData"]["theme_color"] + changed_for, msg_type = getRecipientIdAndMsgType(metadata) + ts = metadata["timestamp"] + self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, changed_for=changed_for, + msg_type=msg_type, ts=ts) + + # Title change + elif delta_type == "change_thread_nickname": + mid = metadata["messageId"] + author_id = metadata["actorFbId"] + changed_for = delta["untypedData"]["participant_id"] + new_title = delta["untypedData"]["nickanme"] + thread_id = metadata["threadKey"].get("threadFbId") + ts = metadata["timestamp"] + self.onTitleChange(mid=mid, author_id=author_id, changed_for=changed_for, new_title=new_title, + thread_id=thread_id, ts=ts) + + # New message + elif "messageMetadata" in delta: + mid = metadata['messageId'] + message = delta.get('body', '') + author_id = metadata['actorFbId'] + recipient_id, msg_type = getRecipientIdAndMsgType(metadata) + ts = metadata["timestamp"] + self.onMessage(mid=mid, author_id=author_id, message=message, + recipient_id=recipient_id, msg_type=msg_type, ts=ts, metadata=m) + + + if mtype == "inbox": + self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"]) + + # Typing + # elif mtype == "typ": + # author_id = str(m.get("from")) + # typing_status = TypingStatus(m.get("st")) + # self.onTyping(author_id=author_id, typing_status=typing_status) + + # Seen + elif mtype == "m_read_receipt": + + self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) + + # elif mtype in ['jewel_requests_add']: + # from_id = m['from'] + # self.on_friend_request(from_id) + + # Happens on every login + elif mtype == "qprimer": + pass + + # Is sent before any other message + elif mtype == "deltaflow": + pass + else: - log.debug("Unknown type {}".format(m)) + log.debug("Unknown type %s" % m) except Exception as e: - # ex_type, ex, tb = sys.exc_info() - self.on_message_error(sys.exc_info(), m) + log.debug(str(e)) - - def start_listening(self): + def startListening(self): """Start listening from an external event loop.""" self.listening = True self.sticky, self.pool = self._getSticky() - - def do_one_listen(self, markAlive=True): + def doOneListen(self, markAlive=True): """Does one cycle of the listening loop. This method is only useful if you want to control fbchat from an external event loop.""" @@ -874,22 +900,19 @@ class Client(object): except requests.exceptions.Timeout: pass - - def stop_listening(self): + def stopListening(self): """Cleans up the variables from start_listening.""" self.listening = False self.sticky, self.pool = (None, None) - def listen(self, markAlive=True): - self.start_listening() + self.startListening() + self.onListening() - log.info("Listening...") while self.listening: - self.do_one_listen(markAlive) - - self.stop_listening() + self.doOneListen(markAlive) + self.stopListening() def getUserInfo(self, *user_ids): """Get user info from id. Unordered. @@ -907,7 +930,6 @@ class Client(object): user_ids = [fbidStrip(uid) for uid in user_ids] - data = {"ids[{}]".format(i):uid for i,uid in enumerate(user_ids)} r = self._post(UserInfoURL, data) info = get_json(r.text) @@ -916,8 +938,7 @@ class Client(object): full_data=full_data[0] return full_data - - def remove_user_from_chat(self, threadID, userID): + def removeUserFromChat(self, threadID, userID): """Remove user (userID) from group chat (threadID) :param threadID: group chat id @@ -933,13 +954,12 @@ class Client(object): return r.ok - def add_users_to_chat(self, threadID, userID): + def addUserToChat(self, threadID, userID): """Add user (userID) to group chat (threadID) :param threadID: group chat id :param userID: user id to add to chat """ - return self.send(threadID, is_user=False, add_user_ids=[userID]) def changeThreadTitle(self, threadID, newTitle): @@ -983,58 +1003,3 @@ class Client(object): r = self._post(SendURL, data) return r.ok - - - def on_message_new(self, mid, author_id, message, metadata, recipient_id, thread_type): - """subclass Client and override this method to add custom behavior on event - - This version of on_message recieves recipient_id and thread_type. - For backwards compatability, this data is sent directly to the old on_message. - """ - self.on_message(mid, author_id, None, message, metadata) - - - def on_message(self, mid, author_id, author_name, message, metadata): - """subclass Client and override this method to add custom behavior on event""" - self.markAsDelivered(author_id, mid) - self.markAsRead(author_id) - log.info("%s said: %s" % (author_name, message)) - - - def on_friend_request(self, from_id): - """subclass Client and override this method to add custom behavior on event""" - log.info("Friend request from %s." % from_id) - - - def on_typing(self, author_id): - """subclass Client and override this method to add custom behavior on event""" - pass - - - def on_read(self, author, reader, time): - """subclass Client and override this method to add custom behavior on event""" - pass - - - def on_people_added(self, user_ids, actor_id, thread_id): - """subclass Client and override this method to add custom behavior on event""" - log.info("User(s) {} was added to {} by {}".format(repr(user_ids), thread_id, actor_id)) - - - def on_person_removed(self, user_id, actor_id, thread_id): - """subclass Client and override this method to add custom behavior on event""" - log.info("User {} was removed from {} by {}".format(user_id, thread_id, actor_id)) - - - def on_inbox(self, viewer, unseen, unread, other_unseen, other_unread, timestamp): - """subclass Client and override this method to add custom behavior on event""" - pass - - - def on_message_error(self, exception, message): - """subclass Client and override this method to add custom behavior on event""" - log.warning("Exception:\n{}".format(exception)) - - - def on_qprimer(self, timestamp): - pass diff --git a/fbchat/event_hook.py b/fbchat/event_hook.py new file mode 100644 index 0000000..00f64c0 --- /dev/null +++ b/fbchat/event_hook.py @@ -0,0 +1,57 @@ +import inspect + + +class EventHook(object): + """ + A simple implementation of the Observer-Pattern. + The user can specify an event signature upon inizializazion, + defined by kwargs in the form of argumentname=class (e.g. id=int). + The arguments' types are not checked in this implementation though. + Callables with a fitting signature can be added with += or removed with -=. + All listeners can be notified by calling the EventHook class with fitting + arguments. + + Thanks http://stackoverflow.com/a/35957226/5556222 + """ + + def __init__(self, **signature): + self._signature = signature + self._argnames = set(signature.keys()) + self._handlers = [] + + def _kwargs_str(self): + return ", ".join(k+"="+v.__name__ for k, v in self._signature.items()) + + def __iadd__(self, handler): + params = inspect.signature(handler).parameters + valid = True + argnames = set(n for n in params.keys()) + if argnames != self._argnames: + valid = False + for p in params.values(): + if p.kind == p.VAR_KEYWORD: + valid = True + break + if p.kind not in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY): + valid = False + break + if not valid: + raise ValueError("Listener must have these arguments: (%s)" + % self._kwargs_str()) + self._handlers.append(handler) + return self + + def __isub__(self, handler): + self._handlers.remove(handler) + return self + + def __call__(self, *args, **kwargs): + if args or set(kwargs.keys()) != self._argnames: + raise ValueError("This EventHook must be called with these " + + "keyword arguments: (%s)" % self._kwargs_str() + + ", but was called with: (%s)" %self._signature) + for handler in self._handlers[:]: + handler(**kwargs) + + def __repr__(self): + return "EventHook(%s)" % self._kwargs_str() diff --git a/fbchat/models.py b/fbchat/models.py index 73f9126..ebe3472 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals +from enum import Enum import sys + class Base(): def __repr__(self): uni = self.__unicode__() @@ -9,6 +11,7 @@ class Base(): def __unicode__(self): return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url) + class User(Base): def __init__(self, data): if data['type'] != 'user': @@ -22,10 +25,62 @@ class User(Base): self.data = data + @staticmethod + def adaptFromChat(user_in_chat): + """ Adapts user info from chat to User model acceptable initial dict + + :param user_in_chat: user info from chat + + 'dir': None, + 'mThumbSrcSmall': None, + 'is_friend': False, + 'is_nonfriend_messenger_contact': True, + 'alternateName': '', + 'i18nGender': 16777216, + 'vanity': '', + 'type': 'friend', + 'searchTokens': ['Voznesenskij', 'Sergej'], + 'thumbSrc': 'https://fb-s-b-a.akamaihd.net/h-ak-xfa1/v/t1.0-1/c9.0.32.32/p32x32/10354686_10150004552801856_220367501106153455_n.jpg?oh=71a87d76d4e4d17615a20c43fb8dbb47&oe=59118CE4&__gda__=1493753268_ae75cef40e9785398e744259ccffd7ff', + 'mThumbSrcLarge': None, + 'firstName': 'Sergej', + 'name': 'Sergej Voznesenskij', + 'uri': 'https://www.facebook.com/profile.php?id=100014812758264', + 'id': '100014812758264', + 'gender': 2 + """ + + return { + 'type': 'user', + 'uid': user_in_chat['id'], + 'photo': user_in_chat['thumbSrc'], + 'path': user_in_chat['uri'], + 'text': user_in_chat['name'], + 'score': '', + 'data': user_in_chat, + } + + class Thread(): def __init__(self, **entries): self.__dict__.update(entries) + class Message(): def __init__(self, **entries): self.__dict__.update(entries) + + +class MsgType(Enum): + USER = 1 + GROUP = 2 + + +class TypingStatus(Enum): + Deleted = 0 + Typing = 1 + + +class Sticker(Enum): + LARGE = '369239383222810' + MEDIUM = '369239343222814' + SMALL = '369239263222822' diff --git a/fbchat/stickers.py b/fbchat/stickers.py deleted file mode 100644 index 6e02adb..0000000 --- a/fbchat/stickers.py +++ /dev/null @@ -1,8 +0,0 @@ -LIKES={ - 'l': '369239383222810', - 'm': '369239343222814', - 's': '369239263222822' -} -LIKES['large'] = LIKES['l'] -LIKES['medium'] =LIKES['m'] -LIKES['small'] = LIKES['s'] diff --git a/fbchat/utils.py b/fbchat/utils.py index d8d3e65..22c782f 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -2,6 +2,7 @@ import re import json from time import time from random import random + USER_AGENTS = [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/601.1.10 (KHTML, like Gecko) Version/8.0.5 Safari/601.1.10", @@ -22,7 +23,7 @@ def digit_to_char(digit): return str(digit) return chr(ord('a') + digit - 10) -def str_base(number,base): +def str_base(number, base): if number < 0: return '-' + str_base(-number, base) (d, m) = divmod(number, base) @@ -33,15 +34,14 @@ def str_base(number,base): def generateMessageID(client_id=None): k = now() l = int(random() * 4294967295) - return ("<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id)); + return "<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id) def getSignatureID(): return hex(int(random() * 2147483648)) -def generateOfflineThreadingID() : +def generateOfflineThreadingID(): ret = now() - value = int(random() * 4294967295); + value = int(random() * 4294967295) string = ("0000000000000000000000" + bin(value))[-22:] msgs = bin(ret) + string - return str(int(msgs,2)) - + return str(int(msgs, 2)) From 27210047772b15ea7b62d20c85fbb55e780c6b4b Mon Sep 17 00:00:00 2001 From: Dainius Date: Mon, 8 May 2017 16:05:14 +0300 Subject: [PATCH 2/3] proper implementation of new message, color, emoji, title, nick change --- fbchat/client.py | 163 ++++++++++++++++++++++++++++------------------- fbchat/models.py | 2 +- 2 files changed, 99 insertions(+), 66 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index d5914eb..0577853 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -75,8 +75,6 @@ class Client(object): import fbchat chat = fbchat.Client(email, password) """ - if do_login and not (email and password): - raise Exception("Email and password not found.") self.is_def_recipient_set = False self.debug = debug @@ -105,34 +103,48 @@ class Client(object): # Setup event hooks self.onLoggingIn = EventHook() self.onLoggedIn = EventHook() + self.onListening = EventHook() - self.onMessage = EventHook(mid=str, author_id=str, message=str, recipient_id=int, msg_type=MsgType, ts=str, metadata=dict) - self.onColorChange = EventHook(mid=str, author_id=str, new_color=str, changed_for=str, msg_type=MsgType, ts=str) - self.onTitleChange = EventHook(mid=str, author_id=str, new_title=str, changed_for=str, thread_id=str, ts=str) - self.onTyping = EventHook(author_id=int, typing_status=TypingStatus) - self.onSeen = EventHook(seen_by=str, thread_id=str, timestamp=str) + self.onMessage = EventHook(mid=str, author_id=str, message=str, thread_id=int, thread_type=ThreadType, ts=str, metadata=dict) + self.onColorChange = EventHook(mid=str, author_id=str, new_color=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict) + self.onEmojiChange = EventHook(mid=str, author_id=str, new_emoji=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict) + self.onTitleChange = EventHook(mid=str, author_id=str, new_title=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict) + self.onNicknameChange = EventHook(mid=str, author_id=str, changed_for=str, new_title=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict) + # self.onTyping = EventHook(author_id=int, typing_status=TypingStatus) + # self.onSeen = EventHook(seen_by=str, thread_id=str, timestamp=str) self.onInbox = EventHook(unseen=int, unread=int, recent_unread=int) self.onPeopleAdded = EventHook(added_ids=list, author_id=str, thread_id=str) self.onPersonRemoved = EventHook(removed_id=str, author_id=str, thread_id=str) self.onFriendRequest = EventHook(from_id=str) + self.onUnknownMesssageType = EventHook(msg=dict) + # Setup event handlers self.onLoggingIn += lambda: log.info("Logging in...") self.onLoggedIn += lambda: log.info("Login successful.") self.onListening += lambda: log.info("Listening...") - self.onMessage += lambda mid, author_id, message, recipient_id, msg_type, ts, metadata:\ - log.info("Message from %s: %s" % (author_id, message)) - self.onColorChange += lambda mid, author_id, new_color, changed_for, msg_type, ts:\ - log.info("%s changed color to %s" % (author_id, new_color)) - self.onTitleChange += lambda mid, author_id, new_title, changed_for, thread_id, ts:\ - log.info("%s changed title of %s to %s" % (author_id, changed_for, new_title)) + self.onMessage += lambda mid, author_id, message, thread_id, thread_type, ts, metadata:\ + log.info("Message from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, message)) + + self.onColorChange += lambda mid, author_id, new_color, thread_id, thread_type, ts, metadata:\ + log.info("Color change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_color)) + self.onEmojiChange += lambda mid, author_id, new_emoji, thread_id, thread_type, ts, metadata:\ + log.info("Emoji change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_emoji)) + self.onTitleChange += lambda mid, author_id, new_title, thread_id, thread_type, ts, metadata:\ + log.info("Title change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_title)) + self.onNicknameChange += lambda mid, author_id, new_title, changed_for, thread_id, thread_type, ts, metadata:\ + log.info("Nickname change from %s in %s (%s) for %s: %s" % (author_id, thread_id, thread_type.name, changed_for, new_title)) + self.onPeopleAdded += lambda added_ids, author_id, thread_id:\ log.info("%s added: %s" % (author_id, [x for x in added_ids])) self.onPersonRemoved += lambda removed_id, author_id, thread_id:\ log.info("%s removed: %s" % (author_id, removed_id)) + self.onUnknownMesssageType += lambda msg:\ + log.info("Unknown message type received: %s" % msg) + if not user_agent: user_agent = choice(USER_AGENTS) @@ -777,77 +789,96 @@ class Client(object): # Things that directly change chat if mtype == "delta": - def getRecipientIdAndMsgType(msg_metadata): - """Returns a tuple consisting of recipient id and message type""" - recipient = None - message_type = None + def getThreadIdAndThreadType(msg_metadata): + """Returns a tuple consisting of thread id and thread type""" + id_thread = None + type_thread = None if 'threadFbId' in msg_metadata['threadKey']: - recipient = msg_metadata['threadKey']['threadFbId'] - message_type = MsgType.GROUP + id_thread = str(msg_metadata['threadKey']['threadFbId']) + type_thread = ThreadType.GROUP elif 'otherUserFbId' in msg_metadata['threadKey']: - recipient = msg_metadata['threadKey']['otherUserFbId'] - message_type = MsgType.USER - return recipient, message_type + id_thread = str(msg_metadata['threadKey']['otherUserFbId']) + type_thread = ThreadType.USER + return id_thread, type_thread delta = m["delta"] delta_type = delta.get("type") metadata = delta.get("messageMetadata") + if metadata is not None: + mid = metadata["messageId"] + author_id = str(metadata['actorFbId']) + ts = int(metadata["timestamp"]) + # Added participants if 'addedParticipants' in delta: - added_ids = [x['userFbId'] for x in delta['addedParticipants']] - author_id = metadata['actorFbId'] - thread_id = metadata['threadKey']['threadFbId'] - ts = metadata["timestamp"] - self.onPeopleAdded(added_ids=added_ids, author_id=author_id, thread_id=thread_id, ts=ts) + added_ids = [str(x['userFbId']) for x in delta['addedParticipants']] + thread_id = str(metadata['threadKey']['threadFbId']) + self.onPeopleAdded(mid=mid, added_ids=added_ids, author_id=author_id, thread_id=thread_id, ts=ts) + continue # Left/removed participants elif 'leftParticipantFbId' in delta: - removed_id = delta['leftParticipantFbId'] - author_id = metadata['actorFbId'] - thread_id = metadata['threadKey']['threadFbId'] - ts = metadata["timestamp"] - self.onPersonRemoved(removed_id=removed_id, author_id=author_id, thread_id=thread_id, ts=ts) - - # Seen - elif delta.get("class") == "ReadReceipt": - seen_by = delta["actorFbId"] or delta["threadKey"]["otherUserFbId"] - thread_id = delta["threadKey"].get("threadFbId") - timestamp = delta["actionTimestampMs"] - self.onSeen(seen_by=seen_by, thread_id=thread_id, timestamp=timestamp) + removed_id = str(delta['leftParticipantFbId']) + thread_id = str(metadata['threadKey']['threadFbId']) + self.onPersonRemoved(mid=mid, removed_id=removed_id, author_id=author_id, thread_id=thread_id, ts=ts) + continue # Color change elif delta_type == "change_thread_theme": - mid = metadata["messageId"] - author_id = metadata["actorFbId"] new_color = delta["untypedData"]["theme_color"] - changed_for, msg_type = getRecipientIdAndMsgType(metadata) - ts = metadata["timestamp"] - self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, changed_for=changed_for, - msg_type=msg_type, ts=ts) + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id, + thread_type=thread_type, ts=ts, metadata=metadata) + continue - # Title change + # Emoji change + elif delta_type == "change_thread_icon": + new_emoji = delta["untypedData"]["thread_icon"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onEmojiChange(mid=mid, author_id=author_id, new_emoji=new_emoji, thread_id=thread_id, + thread_type=thread_type, ts=ts, metadata=metadata) + continue + + # Thread title change + elif delta.get("class") == "ThreadName": + new_title = delta["name"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onTitleChange(mid=mid, author_id=author_id, new_title=new_title, thread_id=thread_id, + thread_type=thread_type, ts=ts, metadata=metadata) + continue + + # Nickname change elif delta_type == "change_thread_nickname": - mid = metadata["messageId"] - author_id = metadata["actorFbId"] - changed_for = delta["untypedData"]["participant_id"] - new_title = delta["untypedData"]["nickanme"] - thread_id = metadata["threadKey"].get("threadFbId") - ts = metadata["timestamp"] - self.onTitleChange(mid=mid, author_id=author_id, changed_for=changed_for, new_title=new_title, - thread_id=thread_id, ts=ts) + changed_for = str(delta["untypedData"]["participant_id"]) + new_title = delta["untypedData"]["nickname"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for, new_title=new_title, + thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata) + continue + + + # TODO properly implement these as they differ on different scenarios + # Seen + # elif delta.get("class") == "ReadReceipt": + # seen_by = delta["actorFbId"] or delta["threadKey"]["otherUserFbId"] + # thread_id = delta["threadKey"].get("threadFbId") + # self.onSeen(seen_by=seen_by, thread_id=thread_id, ts=ts) + # + # # Message delivered + # elif delta.get("class") == 'DeliveryReceipt': + # time_delivered = delta['deliveredWatermarkTimestampMs'] + # self.onDelivered() # New message - elif "messageMetadata" in delta: - mid = metadata['messageId'] + elif delta.get("class") == "NewMessage": message = delta.get('body', '') - author_id = metadata['actorFbId'] - recipient_id, msg_type = getRecipientIdAndMsgType(metadata) - ts = metadata["timestamp"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) self.onMessage(mid=mid, author_id=author_id, message=message, - recipient_id=recipient_id, msg_type=msg_type, ts=ts, metadata=m) - + thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=m) + continue + # Inbox if mtype == "inbox": self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"]) @@ -858,9 +889,9 @@ class Client(object): # self.onTyping(author_id=author_id, typing_status=typing_status) # Seen - elif mtype == "m_read_receipt": - - self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) + # elif mtype == "m_read_receipt": + # + # self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) # elif mtype in ['jewel_requests_add']: # from_id = m['from'] @@ -874,8 +905,10 @@ class Client(object): elif mtype == "deltaflow": pass + # Unknown message type else: - log.debug("Unknown type %s" % m) + self.onUnknownMesssageType(msg=m) + except Exception as e: log.debug(str(e)) diff --git a/fbchat/models.py b/fbchat/models.py index ebe3472..6c526a4 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -70,7 +70,7 @@ class Message(): self.__dict__.update(entries) -class MsgType(Enum): +class ThreadType(Enum): USER = 1 GROUP = 2 From 329f8e5ae861bc7668d6ed70bfafb5d7e6eb54f8 Mon Sep 17 00:00:00 2001 From: Dainius Date: Mon, 8 May 2017 17:02:28 +0300 Subject: [PATCH 3/3] fix of conflict resolution --- fbchat/client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index f8e0422..d16c136 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -90,8 +90,8 @@ class Client(object): self.threads = [] # Setup event hooks - self.onLoggingIn = EventHook() - self.onLoggedIn = EventHook() + self.onLoggingIn = EventHook(email=str) + self.onLoggedIn = EventHook(email=str) self.onListening = EventHook() self.onMessage = EventHook(mid=str, author_id=str, message=str, thread_id=int, thread_type=ThreadType, ts=str, metadata=dict) @@ -110,8 +110,8 @@ class Client(object): self.onUnknownMesssageType = EventHook(msg=dict) # Setup event handlers - self.onLoggingIn += lambda: log.info("Logging in...") - self.onLoggedIn += lambda: log.info("Login successful.") + self.onLoggingIn += lambda email: log.info("Logging in %s..." % email) + self.onLoggedIn += lambda email: log.info("Login of %s successful." % email) self.onListening += lambda: log.info("Listening...") self.onMessage += lambda mid, author_id, message, thread_id, thread_type, ts, metadata:\ @@ -333,7 +333,7 @@ class Client(object): def isLoggedIn(self): # Send a request to the login url, to see if we're directed to the home page. r = self._cleanGet(LoginURL) - return 'home' in r.url: + return 'home' in r.url def getSession(self): """Returns the session cookies"""