From 0d75c09036cb530f69601195f14451e248533365 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 10 May 2017 14:54:07 +0200 Subject: [PATCH] Added support for deprecating items, and maybe support for python 2.7 - Changed `test_data.js` to `test_data.json` - Added `deprecated` decorator - Added `deprecation` function - Readded old functions, and marked them as deprecated - Changed parameters back to being type-in-specific (support for python 2.x) - Deprecated `info_log` and `debug` init paramters --- .gitignore | 1 + fbchat/__init__.py | 2 +- fbchat/client.py | 215 +++++++++++++++++++++++++++++++++++---------- fbchat/models.py | 21 ++++- fbchat/stickers.py | 8 -- fbchat/utils.py | 28 ++++++ test_data.js | 6 -- tests.py | 50 ++++++----- 8 files changed, 245 insertions(+), 86 deletions(-) delete mode 100644 fbchat/stickers.py delete mode 100644 test_data.js diff --git a/.gitignore b/.gitignore index dbca6db..08261f5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ develop-eggs docs/_build/ # Data for tests +test_data.json tests.data \ No newline at end of file diff --git a/fbchat/__init__.py b/fbchat/__init__.py index c58baca..e41d9f6 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -15,7 +15,7 @@ from .client import * __copyright__ = 'Copyright 2015 by Taehoon Kim' -__version__ = '0.10.1' +__version__ = '0.10.2' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan' __email__ = 'carpedm20@gmail.com' diff --git a/fbchat/client.py b/fbchat/client.py index 137277f..8dd90fd 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -14,14 +14,12 @@ import requests import logging from uuid import uuid1 -import warnings from random import choice from datetime import datetime from bs4 import BeautifulSoup as bs from mimetypes import guess_type from .utils import * from .models import * -from .stickers import * import time import sys @@ -58,6 +56,9 @@ facebookEncoding = 'UTF-8' # Log settings log = logging.getLogger("client") log.setLevel(logging.DEBUG) +# Creates the console handler +handler = logging.StreamHandler() +log.addHandler(handler) class Client(object): @@ -67,16 +68,17 @@ class Client(object): documentation for the API. """ - def __init__(self, email, password, debug=True, info_log=True, user_agent=None, max_retries=5, session_cookies=None): + 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` :param password: Facebook account password - :param debug: Configures the logger to `debug` logging_level - :param info_log: Configures the logger to `info` logging_level + :param debug: Configures the logger to `debug` logging_level (deprecated) + :param info_log: Configures the logger to `info` logging_level (deprecated) :param user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list (see utils.py) :param max_retries: Maximum number of times to retry login :param session_cookies: Cookie dict from a previous session (Will default to login if these are invalid) + :param logging_level: Configures the logger to logging_level """ self.sticky, self.pool = (None, None) @@ -86,9 +88,8 @@ class Client(object): self.payloadDefault = {} self.client = 'mercury' self.listening = False - self.is_def_thread_set = False - self.def_thread_id = None - self.def_thread_type = None + self.default_thread_id = None + self.default_thread_type = None self.threads = [] if not user_agent: @@ -104,21 +105,39 @@ class Client(object): # Configure the logger differently based on the 'debug' and 'info_log' parameters if debug: + deprecation('Client(debug)', deprecated_in='0.6.0', details='Use Client(logging_level) instead') logging_level = logging.DEBUG elif info_log: + deprecation('Client(info_log)', deprecated_in='0.6.0', details='Use Client(logging_level) instead') logging_level = logging.INFO - else: - logging_level = logging.WARNING - # Creates the console handler - handler = logging.StreamHandler() handler.setLevel(logging_level) - log.addHandler(handler) # 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(): self.login(email, password, max_retries) + @deprecated(deprecated_in='0.6.0', details='Use log. instead') + def _console(self, msg): + """Assumes an INFO level and log it. + + This method shouldn't be used anymore. + Use the log itself: + >>> import logging + >>> from fbchat.client import log + >>> log.setLevel(logging.DEBUG) + + You can do the same thing by adding the 'debug' argument: + >>> from fbchat import Client + >>> client = Client("...", "...", debug=True) + """ + 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 @@ -154,7 +173,7 @@ class Client(object): self.payloadDefault = {} self.client_id = hex(int(random()*2147483648))[2:] self.start_time = now() - self.uid = str(self._session.cookies['c_user']) + self.uid = int(self._session.cookies['c_user']) self.user_channel = "p_" + str(self.uid) self.ttstamp = '' @@ -294,7 +313,8 @@ class Client(object): return True def login(self, email, password, max_retries=5): - self.onLoggingIn(email=email) + # Logging in + log.info("Logging in {}...".format(email)) if not (email and password): raise Exception("Email and password not set.") @@ -308,7 +328,7 @@ class Client(object): time.sleep(1) continue else: - self.onLoggedIn(email=email) + log.info("Login of {} successful.".format(email)) break else: raise Exception("Login failed. Check email/password.") @@ -327,16 +347,19 @@ class Client(object): self.req_counter = 1 self.seq = "0" return r + + @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)) - def setDefaultThreadId(self, thread_id=str, thread_type=ThreadType): - """Sets default recipient to send messages and images to. + def setDefaultThread(self, thread_id=None, thread_type=ThreadType.USER): + """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.def_thread_id = thread_id - self.def_thread_type = thread_type - self.is_def_thread_set = True + 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 @@ -424,7 +447,7 @@ class Client(object): SEND METHODS """ - def _send(self, thread_id=None, message=None, thread_type=None, emoji_size=None, image_id=None, add_user_ids=None, new_title=None): + 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): """Send a message with given thread id :param thread_id: the user id or thread id that you want to send a message to @@ -436,11 +459,12 @@ class Client(object): :return: a list of message ids of the sent message(s) """ - if thread_id is None and self.is_def_thread_set: - thread_id = self.def_thread_id - thread_type = self.def_thread_type - elif thread_id is None and not self.is_def_thread_set: - raise ValueError('Default Thread ID is not set.') + if thread_id is None: + if self.default_thread_id is not None: + thread_id = self.default_thread_id + thread_type = self.default_thread_type + else: + raise ValueError('Thread ID is not set.') messageAndOTID = generateOfflineThreadingID() timestamp = now() @@ -538,7 +562,11 @@ class Client(object): log.debug("With data {}".format(data)) return message_ids - def sendMessage(self, message: str, thread_id: str = None, thread_type: ThreadType = None): + @deprecated(deprecated_in='0.10.2', details='Use specific functions (eg. sendMessage()) instead') + def send(self, recipient_id=None, message=None, is_user=True, like=None, image_id=None, add_user_ids=None): + 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): """ Sends a message to given (or default, if not) thread with an additional image. :param message: message to send @@ -546,19 +574,20 @@ class Client(object): :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, message, thread_type, None, None, None, None) + return self._send(thread_id=thread_id, message=message, thread_type=thread_type) - def sendEmoji(self, emoji_size: EmojiSize, thread_id: str = None, thread_type: ThreadType = None): + def sendEmoji(self, thread_id, size=Size.MEDIUM, emoji=None, thread_type=ThreadType.USER): """ Sends an emoji to given (or default, if not) thread. - :param emoji_size: size of emoji to send + :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, None, thread_type, emoji_size, None, None, None) + return self._send(thread_id=thread_id, thread_type=thread_type, emoji_size=size) - def sendRemoteImage(self, image_url: str, message: str = None, thread_id: str = None, thread_type: ThreadType = None): + def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): """ Sends an image from given URL to given (or default, if not) thread. :param image_url: URL of an image to upload and send @@ -567,13 +596,22 @@ class Client(object): :param thread_type: specify whether thread_id is user or group chat :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') + thread_type = isUserToThreadType(is_user) + if image is not None: + deprecation('sendRemoteImage(image)', deprecated_in='0.10.2', details='Use sendRemoteImage(image_url) instead') + image_url = image mimetype = guess_type(image_url)[0] remote_image = requests.get(image_url).content image_id = self._uploadImage({'file': (image_url, remote_image, mimetype)}) - return self._send(thread_id, message, thread_type, None, image_id, None, None) + 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: str, message: str = None, thread_id: str = None, thread_type: ThreadType = None): + def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): """ Sends an image from given URL to given (or default, if not) thread. :param image_path: path of an image to upload and send @@ -582,20 +620,29 @@ class Client(object): :param thread_type: specify whether thread_id is user or group chat :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 sendLocalImage(thread_id) instead') + thread_id = recipient_id + if is_user is not None: + deprecation('sendRemoteImage(is_user)', deprecated_in='0.10.2', details='Use sendLocalImage(thread_type) instead') + thread_type = isUserToThreadType(is_user) + if image is not None: + deprecation('sendRemoteImage(image)', deprecated_in='0.10.2', details='Use sendLocalImage(image_path) instead') + image_path = image mimetype = guess_type(image_path)[0] image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)}) - return self._send(thread_id, message, thread_type, None, image_id, None, None) + return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) - def addUsersToChat(self, user_list: list, thread_id: str = None): + def addUsersToChat(self, user_ids, thread_id=None): """ Adds users to given (or default, if not) thread. - :param user_list: list of users to add + :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, None, ThreadType.GROUP, None, None, user_list, None) + return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=users) - def removeUserFromChat(self, user_id: str, thread_id: str = None): + def removeUserFromChat(self, user_id, thread_id=None): """ Adds users to given (or default, if not) thread. :param user_id: user ID to remove @@ -617,18 +664,28 @@ class Client(object): return r.ok - def changeThreadTitle(self, new_title: str, thread_id: str = None): + @deprecated(deprecated_in='0.10.2', details='Use removeUserFromChat() instead') + def add_users_to_chat(self, threadID, userID): + if not isinstance(userID, list): + userID = [userID] + return self.addUsersToChat(userID, thread_id=threadID) + + @deprecated(deprecated_in='0.10.2', details='Use removeUserFromChat() instead') + def remove_user_from_chat(self, threadID, userID): + return self.removeUserFromChat(userID, thread_id=threadID) + + @deprecated(deprecated_in='0.10.2', details='Use changeGroupTitle() instead') + def changeThreadTitle(self, threadID, newTitle): + return self.changeGroupTitle(newTitle, thread_id=threadID) + + def changeGroupTitle(self, title, thread_id=None): """ Change title of a group conversation. - :param new_title: new group chat title + :param title: new group chat title :param thread_id: group chat ID :return: a list of message ids of the sent message(s) """ - if thread_id is None and self.def_thread_type == ThreadType.GROUP: - thread_id = self.def_thread_id - elif thread_id is None: - raise ValueError('Default Thread ID is not set.') - return self._send(thread_id, None, ThreadType.GROUP, None, None, None, new_title) + return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, new_title=title) """ END SEND METHODS @@ -647,7 +704,7 @@ class Client(object): # Strip the start and parse out the returned image_id return json.loads(response_content[9:])['payload']['metadata'][0]['image_id'] - def getThreadInfo(self, last_n=20, thread_id: str = None, thread_type: ThreadType = None): + def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER): """Get the info of one Thread :param last_n: number of retrieved messages from start (default 20) @@ -969,3 +1026,65 @@ class Client(object): if len(full_data)==1: full_data=full_data[0] return full_data + + + + 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) + + @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) + 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 + + + def on_unknown_type(self, m): + """subclass Client and override this method to add custom behavior on event""" + log.debug("Unknown type {}".format(m)) + diff --git a/fbchat/models.py b/fbchat/models.py index 4a4400a..2b49caf 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import sys +from enum import Enum class Base(): def __repr__(self): @@ -37,7 +38,25 @@ class TypingStatus(Enum): DELETED = 0 TYPING = 1 -class EmojiSize(Enum): + +# WIP +class StickerSize(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 +} +LIKES['large'] = LIKES['l'] +LIKES['medium'] =LIKES['m'] +LIKES['small'] = LIKES['s'] 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 292d422..fb4fee0 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 +import warnings 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", @@ -62,3 +63,30 @@ def generateOfflineThreadingID() : msgs = bin(ret) + string return str(int(msgs,2)) +def isUserToThreadType(is_user): + return ThreadType.USER if is_user else ThreadType.GROUP + +def deprecation(name, deprecated_in=None, details='', stacklevel=3): + """This is a function which should be used to mark parameters as deprecated. + It will result in a warning being emmitted when the parameter is used. + """ + warning = "{} is deprecated".format(name) + if deprecated_in: + warning += ' in v. {}'.format(deprecated_in) + if details: + warning += '. {}'.format(details) + + warnings.simplefilter('always', DeprecationWarning) + warnings.warn(warning, category=DeprecationWarning, stacklevel=stacklevel) + warnings.simplefilter('default', DeprecationWarning) + +def deprecated(deprecated_in=None, details=''): + """This is a decorator which can be used to mark functions as deprecated. + It will result in a warning being emmitted when the decorated function is used. + """ + def wrap(func, *args, **kwargs): + def wrapped_func(*args, **kwargs): + deprecation(func.__qualname__, deprecated_in, details, stacklevel=2) + return func(*args, **kwargs) + return wrapped_func + return wrap diff --git a/test_data.js b/test_data.js deleted file mode 100644 index f6d6250..0000000 --- a/test_data.js +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "", - "password": "", - "user_thread_id": "", - "group_thread_id": "" -} \ No newline at end of file diff --git a/tests.py b/tests.py index df26469..b8134e1 100644 --- a/tests.py +++ b/tests.py @@ -10,21 +10,27 @@ import unittest import sys from os import path -# Disable logging -logging.basicConfig(level=100) -fbchat.log.setLevel(100) +#Setup logging +logging.basicConfig(level=logging.INFO) """ Tests for fbchat ~~~~~~~~~~~~~~~~ -To use these tests, put: -- email -- password -- a group_uid -- a user_uid (the user will be kicked from the group and then added again) `test_data.js`, -or type them manually in the terminal prompts +To use these tests, make a json file called test_data.json, put this example in it, and fill in the gaps: +{ + "email": "example@email.com", + "password": "example_password", + "group_thread_id": 0, + "user_thread_id": 0 +} +or type this information manually in the terminal prompts. + +- email: Your (or a test user's) email / phone number +- password: Your (or a test user's) password +- group_thread_id: A test group that will be used to test group functionality +- user_thread_id: A person that will be used to test kick/add functionality (This user should be in the group) Please remember to test both python v. 2.7 and python v. 3.6! @@ -101,8 +107,8 @@ class TestFbchat(unittest.TestCase): self.assertTrue(client.sendRemoteImage(image_url, 'test_send_user_images_remote', user_uid, ThreadType.USER)) self.assertTrue(client.sendRemoteImage(image_url, 'test_send_group_images_remote', group_uid, ThreadType.GROUP)) # Idk why but doesnt work, payload is null - # self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', user_uid, ThreadType.USER)) - # self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', group_uid, ThreadType.GROUP)) + self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', user_uid, ThreadType.USER)) + self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', group_uid, ThreadType.GROUP)) def test_getThreadInfo(self): client.sendMessage('test_user_getThreadInfo', user_uid, ThreadType.USER) @@ -117,12 +123,12 @@ class TestFbchat(unittest.TestCase): self.assertEquals(info[0].author, 'fbid:' + client.uid) self.assertEquals(info[0].body, 'test_group_getThreadInfo') - # def test_markAs(self): - # # To be implemented (requires some form of manual watching) - # pass + def test_markAs(self): + # To be implemented (requires some form of manual watching) + pass - # def test_listen(self): - # client.doOneListen() + def test_listen(self): + client.doOneListen() def test_getUserInfo(self): info = client.getUserInfo(4) @@ -165,15 +171,15 @@ if __name__ == 'tests': try: with open(path.join(path.dirname(__file__), 'test_data.js'), 'r') as f: json = json.load(f) - email = json["email"] - password = json["password"] - user_uid = json["user_thread_id"] - group_uid = json["group_thread_id"] + email = json['email'] + password = json['password'] + user_uid = json['user_thread_id'] + group_uid = json['group_thread_id'] except (IOError, IndexError) as e: email = input('Email: ') password = getpass.getpass() - group_uid = input('Please enter a group uid (To test group functionality): ') - user_uid = input('Please enter a user uid (To test kicking/adding functionality): ') + group_uid = input('Please enter a group thread id (To test group functionality): ') + user_uid = input('Please enter a user thread id (To test kicking/adding functionality): ') print('Logging in...') client = fbchat.Client(email, password)