diff --git a/.gitignore b/.gitignore index 08261f5..98f10f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *py[co] +.idea/ + # Test scripts *.sh diff --git a/fbchat/client.py b/fbchat/client.py index 8dd90fd..1d144ad 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -20,6 +20,7 @@ from bs4 import BeautifulSoup as bs from mimetypes import guess_type from .utils import * from .models import * +from .event_hook import * import time import sys @@ -68,7 +69,8 @@ class Client(object): documentation for the API. """ - def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5, session_cookies=None, logging_level=logging.INFO): + def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5, + session_cookies=None, logging_level=logging.INFO): """A client for the Facebook Chat (Messenger). :param email: Facebook `email` or `id` or `phone number` @@ -92,6 +94,8 @@ class Client(object): self.default_thread_type = None self.threads = [] + self._setupEventHooks() + if not user_agent: user_agent = choice(USER_AGENTS) @@ -114,9 +118,55 @@ class Client(object): handler.setLevel(logging_level) # If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login - if not session_cookies or not self.setSession(session_cookies) or not self.is_logged_in(): + if not session_cookies or not self.setSession(session_cookies) or not self.isLoggedIn(): self.login(email, password, max_retries) + def _setupEventHooks(self): + # Setup event hooks + 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) + 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 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:\ + 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) + @deprecated(deprecated_in='0.6.0', details='Use log. instead') def _console(self, msg): """Assumes an INFO level and log it. @@ -133,11 +183,6 @@ class Client(object): """ 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 @@ -169,7 +214,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() @@ -183,7 +228,9 @@ 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 @@ -228,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 @@ -284,13 +331,10 @@ class Client(object): r = self._cleanPost(CheckpointURL, data) return r - def is_logged_in(self): + def isLoggedIn(self): # Send a request to the login url, to see if we're directed to the home page. r = self._cleanGet(LoginURL) - if 'home' in r.url: - return True - else: - return False + return 'home' in r.url def getSession(self): """Returns the session cookies""" @@ -309,12 +353,11 @@ class Client(object): # Load cookies into current session self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies) - self._post_login() + self._postLogin() return True def login(self, email, password, max_retries=5): - # Logging in - log.info("Logging in {}...".format(email)) + self.onLoggingIn(email=email) if not (email and password): raise Exception("Email and password not set.") @@ -328,7 +371,7 @@ class Client(object): time.sleep(1) continue else: - log.info("Login of {} successful.".format(email)) + self.onLoggedIn(email=email) break else: raise Exception("Login failed. Check email/password.") @@ -350,49 +393,23 @@ class Client(object): @deprecated(deprecated_in='0.10.2', details='Use setDefaultThread instead') def setDefaultRecipient(self, recipient_id, is_user=True): - self.setDefaultThread(recipient_id, thread_type=isUserToThreadType(is_user)) + self.setDefaultThread(str(recipient_id), thread_type=isUserToThreadType(is_user)) - def setDefaultThread(self, thread_id=None, thread_type=ThreadType.USER): + def setDefaultThread(self, thread_id, thread_type): + # type: (str, ThreadType) -> None """Sets default thread to send messages and images to. - + :param thread_id: user/group ID to default to :param thread_type: type of thread_id """ self.default_thread_id = thread_id self.default_thread_type = thread_type - 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 resetDefaultThread(self): + # type: () -> None + """Resets default thread.""" + self.default_thread_id = None + self.default_thread_type = None def getAllUsers(self): """ Gets all users from chat with info included """ @@ -411,7 +428,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 @@ -448,6 +465,7 @@ class Client(object): """ def _send(self, thread_id=None, message=None, thread_type=ThreadType.USER, emoji_size=None, image_id=None, add_user_ids=None, new_title=None): + # type: (str, str, ThreadType, EmojiSize, str, list, str) -> list """Send a message with given thread id :param thread_id: the user id or thread id that you want to send a message to @@ -538,7 +556,7 @@ class Client(object): if not r.ok: log.warning('Error when sending message: Got {} response'.format(r.status_code)) - return False + return None response_content = {} if isinstance(r.content, str) is False: @@ -547,15 +565,15 @@ class Client(object): if 'error' in j: # 'errorDescription' is in the users own language! log.warning('Error #{} when sending message: {}'.format(j['error'], j['errorDescription'])) - return False + return None message_ids = [] try: message_ids += [action['message_id'] for action in j['payload']['actions'] if 'message_id' in action] - message_ids[0] # Try accessing element + message_ids[0] # Try accessing element except (KeyError, IndexError) as e: log.warning('Error when sending message: No message ids could be found') - return False + return None log.info('Message sent.') log.debug("Sending {}".format(r)) @@ -567,8 +585,10 @@ class Client(object): return self._send(thread_id=recipient_id, message=message, thread_type=isUserToThreadType(is_user), emoji_size=LIKES[like], image_id=image_id, add_user_ids=add_user_ids) def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): + # type: (str, str, ThreadType) -> list """ Sends a message to given (or default, if not) thread with an additional image. + :param message: message to send :param thread_id: user/group chat ID :param thread_type: specify whether thread_id is user or group chat @@ -576,20 +596,25 @@ class Client(object): """ return self._send(thread_id=thread_id, message=message, thread_type=thread_type) - def sendEmoji(self, thread_id, size=Size.MEDIUM, emoji=None, thread_type=ThreadType.USER): + def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER): + # type: (str, EmojiSize, str, ThreadType) -> list """ Sends an emoji to given (or default, if not) thread. + + :param emoji: WIP :param size: size of emoji to send :param thread_id: user/group chat ID - :param emoji: WIP :param thread_type: specify whether thread_id is user or group chat :return: a list of message ids of the sent message(s) """ return self._send(thread_id=thread_id, thread_type=thread_type, emoji_size=size) - def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): + def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER, + recipient_id=None, is_user=None, image=None): + # type: (str, str, str, ThreadType) -> list """ Sends an image from given URL to given (or default, if not) thread. + :param image_url: URL of an image to upload and send :param message: additional message :param thread_id: user/group chat ID @@ -597,7 +622,6 @@ class Client(object): :return: a list of message ids of the sent message(s) """ if recipient_id is not None: - deprecation('sendRemoteImage(recipient_id)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_id) instead') thread_id = recipient_id if is_user is not None: deprecation('sendRemoteImage(is_user)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_type) instead') @@ -611,9 +635,12 @@ class Client(object): return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) # Doesn't upload properly - def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): + def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER, + recipient_id=None, is_user=None, image=None): + # type: (str, str, str, ThreadType) -> list """ Sends an image from given URL to given (or default, if not) thread. + :param image_path: path of an image to upload and send :param message: additional message :param thread_id: user/group chat ID @@ -634,15 +661,18 @@ class Client(object): return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) def addUsersToChat(self, user_ids, thread_id=None): + # type: (list, str) -> list """ Adds users to given (or default, if not) thread. + :param user_ids: list of user ids to add :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=users) + return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=user_ids) def removeUserFromChat(self, user_id, thread_id=None): + # type: (str, str) -> bool """ Adds users to given (or default, if not) thread. :param user_id: user ID to remove @@ -705,6 +735,7 @@ class Client(object): return json.loads(response_content[9:])['payload']['metadata'][0]['image_id'] def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER): + # type: (int, str, ThreadType) -> list """Get the info of one Thread :param last_n: number of retrieved messages from start (default 20) @@ -745,6 +776,7 @@ class Client(object): def getThreadList(self, start, length=20): + # type: (int, int) -> list """Get thread list of your facebook account. :param start: the start index of a thread @@ -786,7 +818,6 @@ class Client(object): return self.threads - def getUnread(self): form = { 'client': 'mercury_sync', @@ -815,7 +846,6 @@ class Client(object): r = self._post(DeliveredURL, data) return r.ok - def markAsRead(self, userID): data = { "watermarkTimestamp": now(), @@ -826,23 +856,24 @@ class Client(object): r = self._post(ReadStatusURL, data) return r.ok - def markAsSeen(self): r = self._post(MarkSeenURL, {"seen_timestamp": 0}) return r.ok - + @deprecated(deprecated_in='0.10.2', details='Use friendConnect() instead') def friend_connect(self, friend_id): + return self.friendConnect(friend_id) + + def friendConnect(self, friend_id): + # type: (str) -> bool data = { "to_friend": friend_id, "action": "confirm" } r = self._post(ConnectURL, data) - return r.ok - def ping(self, sticky): data = { 'channel': self.user_channel, @@ -856,7 +887,6 @@ 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. @@ -878,7 +908,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.""" @@ -896,80 +925,161 @@ 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. - """ + """Get message and author name from content. May contain multiple messages in the content.""" 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 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']: + id_thread = str(msg_metadata['threadKey']['threadFbId']) + type_thread = ThreadType.GROUP + elif 'otherUserFbId' in msg_metadata['threadKey']: + 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 = [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 = 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": + new_color = delta["untypedData"]["theme_color"] + 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 + + # 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": + 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 delta.get("class") == "NewMessage": + message = delta.get('body', '') + thread_id, thread_type = getThreadIdAndThreadType(metadata) + self.onMessage(mid=mid, author_id=author_id, message=message, + 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"]) + + # 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 + + # Unknown message type else: - self.on_unknown_type(m) + self.onUnknownMesssageType(msg=m) + except Exception as e: - # ex_type, ex, tb = sys.exc_info() - self.on_message_error(sys.exc_info(), m) + log.debug(str(e)) + @deprecated(deprecated_in='0.10.2', details='Use startListening() instead') def start_listening(self): + return self.startListening() + + def startListening(self): """Start listening from an external event loop.""" self.listening = True self.sticky, self.pool = self._getSticky() + @deprecated(deprecated_in='0.10.2', details='Use doOneListen() instead') def do_one_listen(self, markAlive=True): + return self.doOneListen(markAlive) + + def doOneListen(self, markAlive=True): + # type: (bool) -> None """Does one cycle of the listening loop. This method is only useful if you want to control fbchat from an external event loop.""" @@ -986,20 +1096,24 @@ class Client(object): pass + @deprecated(deprecated_in='0.10.2', details='Use stopListening() instead') def stop_listening(self): + return self.stopListening() + + 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.doOneListen(markAlive) - self.stop_listening() + self.stopListening() def getUserInfo(self, *user_ids): @@ -1037,7 +1151,6 @@ class Client(object): """ self.on_message(mid, author_id, None, message, metadata) - @deprecated(deprecated_in='0.7.0', details='Use on_message_new() instead') 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) 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 2b49caf..5c414ca 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -2,15 +2,7 @@ from __future__ import unicode_literals import sys from enum import Enum -class Base(): - def __repr__(self): - uni = self.__unicode__() - return uni.encode('utf-8') if sys.version_info < (3, 0) else uni - - def __unicode__(self): - return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url) - -class User(Base): +class User: def __init__(self, data): if data['type'] != 'user': raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path'])) @@ -22,11 +14,53 @@ class User(Base): self.score = data['score'] self.data = data -class Thread(): + def __repr__(self): + uni = self.__unicode__() + return uni.encode('utf-8') if sys.version_info < (3, 0) else uni + + def __unicode__(self): + return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url) + + @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(): +class Message: def __init__(self, **entries): self.__dict__.update(entries) @@ -40,22 +74,15 @@ class TypingStatus(Enum): # WIP -class StickerSize(Enum): +class EmojiSize(Enum): LARGE = '369239383222810' MEDIUM = '369239343222814' SMALL = '369239263222822' -#class Size(Enum): -# LARGE = 'large' -# MEDIUM = 'medium' -# SMALL = 'small' - -Size = StickerSize - LIKES = { - 'l': Size.LARGE, - 'm': Size.MEDIUM, - 's': Size.SMALL + 'l': EmojiSize.LARGE, + 'm': EmojiSize.MEDIUM, + 's': EmojiSize.SMALL } LIKES['large'] = LIKES['l'] LIKES['medium'] =LIKES['m'] diff --git a/fbchat/utils.py b/fbchat/utils.py index fb4fee0..4751b99 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -26,6 +26,7 @@ GENDERS = { 11: 'unknown_plural', } + def now(): return int(time()*1000) @@ -40,7 +41,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) @@ -51,17 +52,17 @@ 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)) def isUserToThreadType(is_user): return ThreadType.USER if is_user else ThreadType.GROUP