From c81d7d2bfb964af4035b2a23363e2de240bf5675 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 20 Jun 2017 14:57:23 +0200 Subject: [PATCH] Removed deprecations and new event system, improved other things Removed deprecations Removed new event system Added documentation for all events Added FAQ Changed Client.uid to Client.id Improved User model Prepared for support of pages --- docs/api.rst | 8 +- docs/examples.rst | 8 + docs/faq.rst | 44 ++ docs/index.rst | 4 + docs/intro.rst | 32 +- docs/todo.rst | 2 +- examples/basic_usage.py | 12 + examples/fetch.py | 30 +- examples/removebot.py | 5 +- fbchat/__init__.py | 2 +- fbchat/client.py | 978 +++++++++++++++++++++------------------- fbchat/event_hook.py | 28 -- fbchat/models.py | 118 +++-- fbchat/utils.py | 61 +-- tests.py | 136 +++--- 15 files changed, 768 insertions(+), 700 deletions(-) create mode 100644 docs/faq.rst create mode 100644 examples/basic_usage.py delete mode 100644 fbchat/event_hook.py diff --git a/docs/api.rst b/docs/api.rst index 1290f35..e08f19b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -16,15 +16,9 @@ Client This is the main class of `fbchat`, which contains all the methods you use to interract with Facebook. You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening) -.. todo:: - Add documentation for all events - -.. autoclass:: Client(email, password, user_agent=None, max_retries=5, session_cookies=None, logging_level=logging.INFO) +.. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO) :members: - .. automethod:: sendRemoteImage(image_url, message=None, thread_id=None, thread_type=ThreadType.USER) - .. automethod:: sendLocalImage(image_path, message=None, thread_id=None, thread_type=ThreadType.USER) - .. _api_models: diff --git a/docs/examples.rst b/docs/examples.rst index 50f0fa3..89da9dc 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -7,6 +7,14 @@ Examples These are a few examples on how to use `fbchat`. Remember to swap out `` and `` for your email and password +Basic example +------------- + +This will show basic usage of `fbchat` + +.. literalinclude:: ../examples/basic_usage.py + + Interacting with Threads ------------------------ diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..900f8a7 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,44 @@ +.. highlight:: python +.. module:: fbchat +.. _faq: + +Frequently asked questions +========================== + +Version X broke my installation +------------------------------- + +We try to provide backwards compatability where possible, but since we're not part of Facebook, +most of the things may be broken at any point in time + +Downgrade to an earlier version of fbchat, run this command + +.. code-block:: sh + + $ pip install fbchat== + +Where you replace ```` with the version you want to use + + +Will you be supporting creating posts/events/pages and so on? +------------------------------------------------------------- + +We won't be focusing on anything else than chat-related things. This API is called `fbCHAT`, after all ;) + + +Submitting Issues +----------------- + +If you're having trouble with some of the snippets, or you think some of the functionality is broken, +please feel free to submit an issue on `Github `_. +You should first login with ``logging_level`` set to ``logging.DEBUG``:: + + from fbchat import Client + import logging + client = Client('', '', logging_level=logging.DEBUG) + +Then you can submit the relevant parts of this log, and detailed steps on how to reproduce + +.. warning:: + Always remove your credentials from any debug information you may provide us. + Preferably, use a test account, in case you miss anything diff --git a/docs/index.rst b/docs/index.rst index 0f083a6..67a8c11 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,6 +36,9 @@ Currently `fbchat` support Python 2.7, 3.4, 3.5 and 3.6: This means doing the exact same GET/POST requests and tricking Facebook into thinking it's accessing the website normally. Therefore, this API requires the credentials of a Facebook account. +.. note:: + If you're having problems, please check the :ref:`faq`, before asking questions on Github + .. warning:: We are not responsible if your account gets banned for spammy activities, such as sending lots of messages to people you don't know, sending messages very quickly, @@ -60,3 +63,4 @@ Overview testing api todo + faq diff --git a/docs/intro.rst b/docs/intro.rst index 3e58bad..12c4d1f 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -53,9 +53,9 @@ These will specify whether the thread is a single user chat or a group chat. This is required for many of `fbchat`'s functions, since Facebook differetiates between these two internally Searching for group chats and finding their ID is not yet possible with `fbchat`, -but searching for users is possible via. :func:`Client.getUsers`. See :ref:`intro_fetching` +but searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching` -You can get your own user ID by using :any:`Client.uid` +You can get your own user ID by using :any:`Client.id` Getting the ID of a group chat is fairly trivial though, since you only need to navigate to ``_, click on the group you want to find the ID of, and then read the id from the address bar. @@ -108,7 +108,7 @@ like adding users to and removing users from a group chat, logically only works The simplest way of using `fbchat` is to send a message. The following snippet will, as you've probably already figured out, send the message `test message` to your account:: - message_id = client.sendMessage('test message', thread_id=client.uid, thread_type=ThreadType.USER) + message_id = client.sendMessage('test message', thread_id=client.id, thread_type=ThreadType.USER) You can see a full example showing all the possible thread interactions with `fbchat` by going to :ref:`examples` @@ -120,12 +120,12 @@ Fetching Information You can use `fbchat` to fetch basic information like user names, profile pictures, thread names and user IDs -You can retrieve a user's ID with :func:`Client.getUsers`. +You can retrieve a user's ID with :func:`Client.searchForUsers`. The following snippet will search for users by their name, take the first (and most likely) user, and then get their user ID from the result:: - users = client.getUsers('') + users = client.searchForUsers('') user = users[0] - print("User's ID: {}".format(user.uid)) + print("User's ID: {}".format(user.id)) print("User's name: {}".format(user.name)) print("User's profile picture url: {}".format(user.photo)) print("User's main url: {}".format(user.url)) @@ -198,23 +198,3 @@ and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function stil the API actually requires that you include ``**kwargs`` as your final argument. View the :ref:`examples` to see some more examples illustrating the event system - - -.. _intro_submitting: - -Submitting Issues ------------------ - -If you're having trouble with some of the snippets shown here, or you think some of the functionality is broken, -please feel free to submit an issue on `Github `_. -One side note is that you should first login with ``logging_level`` set to ``logging.DEBUG``:: - - from fbchat import Client - import logging - client = Client('', '', logging_level=logging.DEBUG) - -Then you can submit the relevant parts of this log, and detailed steps on how to reproduce - -.. warning:: - Always remove your credentials from any debug information you may provide us. - Preferably, use a test account, in case you miss anything diff --git a/docs/todo.rst b/docs/todo.rst index e2d773a..8059ab1 100644 --- a/docs/todo.rst +++ b/docs/todo.rst @@ -17,7 +17,7 @@ Missing Functionality - This will use the graphql request API - Implement chatting with pages - This might require a new :class:`models.ThreadType`, something like ``ThreadType.PAGE`` -- Rework `User`, `Thread` and `Message` models, and rework fething methods, to make the whole process more streamlined +- Rework `Message` model, to make the whole process more streamlined Documentation diff --git a/examples/basic_usage.py b/examples/basic_usage.py new file mode 100644 index 0000000..ebd3268 --- /dev/null +++ b/examples/basic_usage.py @@ -0,0 +1,12 @@ +# -*- coding: UTF-8 -*- + +from fbchat import Client +from fbchat.models import * + +client = Client('', '') + +print('Own id: {}'.format(client.id)) + +client.sendMessage('Hi me!', thread_id=self.id, thread_type=ThreadType.USER) + +client.logout() diff --git a/examples/fetch.py b/examples/fetch.py index 13af24e..3ed48ec 100644 --- a/examples/fetch.py +++ b/examples/fetch.py @@ -3,42 +3,44 @@ from fbchat import Client from fbchat.models import * -client = Client("", "") +client = Client('', '') # Fetches a list of all users you're currently chatting with, as `User` objects -users = client.getAllUsers() +users = client.fetchAllUsers() -print('user IDs: {}'.format(user.uid for user in users)) -print("user's names: {}".format(user.name for user in users)) +print("users' IDs: {}".format(user.uid for user in users)) +print("users' names: {}".format(user.name for user in users)) # If we have a user id, we can use `getUserInfo` to fetch a `User` object -user = client.getUserInfo('') +user = client.fetchUserInfo('')[''] # We can also query both mutiple users together, which returns list of `User` objects -users = client.getUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>') +users = client.fetchUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>') -print('User INFO: {}'.format(user)) -print("User's INFO: {}".format(users)) +print("user's name: {}".format(user.name)) +print("users' names: {}".format(users[k].name for k in users)) -# `getUsers` searches for the user and gives us a list of the results, +# `searchForUsers` searches for the user and gives us a list of the results, # and then we just take the first one, aka. the most likely one: -user = client.getUsers('')[0] +user = client.searchForUsers('')[0] print('user ID: {}'.format(user.uid)) print("user's name: {}".format(user.name)) +print("user's photo: {}".format(user.photo)) +print("Is user client's friend: {}".format(user.is_friend)) -# Fetches a list of all threads you're currently chatting with -threads = client.getThreadList() +# Fetches a list of the 20 top threads you're currently chatting with +threads = client.fetchThreadList() # Fetches the next 10 threads -threads += client.getThreadList(start=20, length=10) +threads += client.fetchThreadList(offset=20, amount=10) print("Thread's INFO: {}".format(threads)) # Gets the last 10 messages sent to the thread -messages = client.getThreadInfo(last_n=10, thread_id='', thread_type=ThreadType) +messages = client.fetchThreadMessages(offset=0, amount=10, thread_id='', thread_type=ThreadType) # Since the message come in reversed order, reverse them messages.reverse() diff --git a/examples/removebot.py b/examples/removebot.py index 80d1601..f387d90 100644 --- a/examples/removebot.py +++ b/examples/removebot.py @@ -7,10 +7,11 @@ class RemoveBot(Client): def onMessage(self, author_id, message, thread_id, thread_type, **kwargs): # We can only kick people from group chats, so no need to try if it's a user chat if message == 'Remove me!' and thread_type == ThreadType.GROUP: - log.info("{} will be removed from {}".format(author_id, thread_id)) + log.info('{} will be removed from {}'.format(author_id, thread_id)) self.removeUserFromGroup(author_id, thread_id=thread_id) else: - log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message)) + # Sends the data to the inherited onMessage, so that we can still see when a message is recieved + super(type(self), self).onMessage(author_id=author_id, message=message, thread_id=thread_id, thread_type=thread_type, **kwargs) client = RemoveBot("", "") client.listen() diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 54c6674..adcdc17 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -14,7 +14,7 @@ from datetime import datetime from .client import * __copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year) -__version__ = '0.10.4' +__version__ = '1.0.0' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' __email__ = 'carpedm20@gmail.com' diff --git a/fbchat/client.py b/fbchat/client.py index 8fc850c..69900e7 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import requests -import logging import urllib from uuid import uuid1 from random import choice @@ -11,45 +10,8 @@ from bs4 import BeautifulSoup as bs from mimetypes import guess_type from .utils import * from .models import * -from .event_hook import * import time -# Python 2's `input` executes the input, whereas `raw_input` just returns the input -try: - input = raw_input -except NameError: - pass - -# Log settings -log = logging.getLogger("client") -log.setLevel(logging.DEBUG) -# Creates the console handler -handler = logging.StreamHandler() -log.addHandler(handler) - - - -#: This function needs the logger -def check_request(r, check_json=True): - if not r.ok: - log.warning('Error when sending request: Got {} response'.format(r.status_code)) - return None - - content = get_decoded(r) - - if content is None or len(content) == 0: - log.warning('Error when sending request: Got empty response') - return None - - if check_json: - j = json.loads(strip_to_json(content)) - if 'error' in j: - # 'errorDescription' is in the users own language! - log.warning('Error #{} when sending request: {}'.format(j['error'], j['errorDescription'])) - return None - return j - else: - return r class Client(object): @@ -60,25 +22,24 @@ class Client(object): listening = False """Whether the client is listening. Used when creating an external event loop to determine when to stop listening""" - uid = None + id = None """ - The id of the client. - Can be used a `thread_id`. See :ref:`intro_threads` for more info. + The ID of the client. + Can be used as `thread_id`. See :ref:`intro_threads` for more info. Note: Modifying this results in undefined behaviour """ - 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, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO): """Initializes and logs in the client :param email: Facebook `email`, `id` or `phone number` :param password: Facebook account password :param user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list (see :any:`utils.USER_AGENTS`) - :param max_retries: Maximum number of times to retry login + :param max_tries: Maximum number of times to try logging in :param session_cookies: Cookies from a previous session (Will default to login if these are invalid) :param logging_level: Configures the `logging level `_. Defaults to `INFO` - :type max_retries: int + :type max_tries: int :type session_cookies: dict :type logging_level: int :raises: Exception on failed login @@ -94,9 +55,6 @@ class Client(object): self.default_thread_type = None self.threads = [] - self._setupEventHooks() - self._setupOldEventHooks() - if not user_agent: user_agent = choice(USER_AGENTS) @@ -108,138 +66,15 @@ class Client(object): 'Connection' : 'keep-alive', } - # Configure the logger differently based on the 'debug' and 'info_log' parameters - if debug: - deprecation('Client(debug)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use Client(logging_level) instead') - logging_level = logging.DEBUG - elif info_log: - deprecation('Client(info_log)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use Client(logging_level) instead') - logging_level = logging.INFO - 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.isLoggedIn(): - self.login(email, password, max_retries) + self.login(email, password, max_tries) - def _setEventHook(self, event_name, *functions): - if not hasattr(type(self), event_name): - eventhook = EventHook(*functions) - setattr(self, event_name, eventhook) - - def _setupEventHooks(self): - self._setEventHook('onLoggingIn', lambda email: log.info("Logging in {}...".format(email))) - - self._setEventHook('on2FACode', lambda: input('Please enter your 2FA code --> ')) - - self._setEventHook('onLoggedIn', lambda email: log.info("Login of {} successful.".format(email))) - - self._setEventHook('onListening', lambda: log.info("Listening...")) - - self._setEventHook('onListenError', lambda exception: raise_exception(exception)) - - - self._setEventHook('onMessage', lambda mid, author_id, message, thread_id, thread_type, ts, metadata, msg:\ - log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message))) - - self._setEventHook('onColorChange', lambda mid, author_id, new_color, thread_id, thread_type, ts, metadata, msg:\ - log.info("Color change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_color))) - - self._setEventHook('onEmojiChange', lambda mid, author_id, new_emoji, thread_id, thread_type, ts, metadata, msg:\ - log.info("Emoji change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_emoji))) - - self._setEventHook('onTitleChange', lambda mid, author_id, new_title, thread_id, thread_type, ts, metadata, msg:\ - log.info("Title change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_title))) - - self._setEventHook('onNicknameChange', lambda mid, author_id, changed_for, new_nickname, thread_id, thread_type, ts, metadata, msg:\ - log.info("Nickname change from {} in {} ({}) for {}: {}".format(author_id, thread_id, thread_type.name, changed_for, new_nickname))) - - - self._setEventHook('onMessageSeen', lambda seen_by, thread_id, thread_type, seen_ts, delivered_ts, metadata, msg:\ - log.info("Messages seen by {} in {} ({}) at {}s".format(seen_by, thread_id, thread_type.name, seen_ts/1000))) - - self._setEventHook('onMessageDelivered', lambda msg_ids, delivered_for, thread_id, thread_type, ts, metadata, msg:\ - log.info("Messages {} delivered to {} in {} ({}) at {}s".format(msg_ids, delivered_for, thread_id, thread_type.name, ts/1000))) - - self._setEventHook('onMarkedSeen', lambda threads, seen_ts, delivered_ts, metadata, msg:\ - log.info("Marked messages as seen in threads {} at {}s".format([(x[0], x[1].name) for x in threads], seen_ts/1000))) - - - self._setEventHook('onPeopleAdded', lambda mid, added_ids, author_id, thread_id, ts, msg:\ - log.info("{} added: {}".format(author_id, ', '.join(added_ids)))) - - self._setEventHook('onPersonRemoved', lambda mid, removed_id, author_id, thread_id, ts, msg:\ - log.info("{} removed: {}".format(author_id, removed_id))) - - self._setEventHook('onFriendRequest', lambda from_id, msg: log.info("Friend request from {}".format(from_id))) - - self._setEventHook('onInbox', lambda unseen, unread, recent_unread, msg: log.info('Inbox event: {}, {}, {}'.format(unseen, unread, recent_unread))) - - self._setEventHook('onQprimer', lambda made, msg: None) - - - self._setEventHook('onUnknownMesssageType', lambda msg: log.debug('Unknown message received: {}'.format(msg))) - - self._setEventHook('onMessageError', lambda exception, msg: log.exception('Exception in parsing of {}'.format(msg))) - - def _checkOldEventHook(self, old_event, new_event, deprecated_in='0.10.3', removed_in='0.15.0'): - if hasattr(type(self), old_event): - deprecation('Client.{}'.format(old_event), deprecated_in=deprecated_in, removed_in=removed_in, details='Use new event system instead (specifically Client.{})'.format(new_event)) - if not hasattr(type(self), new_event): - return True - return False - - def _setupOldEventHooks(self): - if self._checkOldEventHook('on_message', 'onMessage', deprecated_in='0.7.0', removed_in='0.12.0'): - self.onMessage += lambda mid, author_id, message, thread_id, thread_type, ts, metadata, msg:\ - self.on_message(mid, author_id, None, message, metadata) - - if self._checkOldEventHook('on_message_new', 'onMessage'): - self.onMessage += lambda mid, author_id, message, thread_id, thread_type, ts, metadata, msg:\ - self.on_message_new(mid, author_id, message, metadata, thread_id, True if thread_type is ThreadType.USER else False) - - if self._checkOldEventHook('on_friend_request', 'onFriendRequest'): - self.onFriendRequest += lambda from_id, msg: self.on_friend_request(from_id) - - if self._checkOldEventHook('on_typing', 'onTyping'): - self.onTyping += lambda author_id, typing_status, msg: self.on_typing(author_id) - - if self._checkOldEventHook('on_read', 'onSeen'): - self.onSeen += lambda seen_by, thread_id, timestamp, msg: self.on_read(seen_by, thread_id, timestamp) - - if self._checkOldEventHook('on_people_added', 'onPeopleAdded'): - self.onPeopleAdded += lambda added_ids, author_id, thread_id, msg: self.on_people_added(added_ids, author_id, thread_id) - - if self._checkOldEventHook('on_person_removed', 'onPersonRemoved'): - self.onPersonRemoved += lambda removed_id, author_id, thread_id, msg: self.on_person_removed(removed_id, author_id, thread_id) - - if self._checkOldEventHook('on_inbox', 'onInbox'): - self.onInbox += lambda unseen, unread, recent_unread, msg: self.on_inbox(None, unseen, unread, None, recent_unread, None) - - if self._checkOldEventHook('on_qprimer', ''): - pass - - if self._checkOldEventHook('on_message_error', 'onMessageError'): - self.onMessageError += lambda exception, msg: self.on_message_error(exception, msg) - - if self._checkOldEventHook('on_unknown_type', 'onUnknownMesssageType'): - self.onUnknownMesssageType += lambda msg: self.on_unknown_type(msg) - - @deprecated(deprecated_in='0.6.0', removed_in='0.11.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) + """ + INTERNAL REQUEST METHODS + """ def _generatePayload(self, query): """Adds the following defaults to the payload: @@ -274,12 +109,20 @@ class Client(object): headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type') return self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files) + """ + END INTERNAL REQUEST METHODS + """ + + """ + LOGIN METHODS + """ + def _postLogin(self): self.payloadDefault = {} self.client_id = hex(int(random()*2147483648))[2:] self.start_time = now() - self.uid = str(self._session.cookies['c_user']) - self.user_channel = "p_" + self.uid + self.id = str(self._session.cookies['c_user']) + self.user_channel = "p_" + self.id self.ttstamp = '' r = self._get(ReqUrl.BASE) @@ -293,7 +136,7 @@ class Client(object): self.ttstamp += '2' # Set default payload self.payloadDefault['__rev'] = int(r.text.split('"client_revision":',1)[1].split(",",1)[0]) - self.payloadDefault['__user'] = self.uid + self.payloadDefault['__user'] = self.id self.payloadDefault['__a'] = '1' self.payloadDefault['ttstamp'] = self.ttstamp self.payloadDefault['fb_dtsg'] = self.fb_dtsg @@ -302,8 +145,8 @@ class Client(object): 'channel' : self.user_channel, 'partition' : '-2', 'clientid' : self.client_id, - 'viewer_uid' : self.uid, - 'uid' : self.uid, + 'viewer_uid' : self.id, + 'uid' : self.id, 'state' : 'active', 'format' : 'json', 'idle' : 0, @@ -429,37 +272,40 @@ class Client(object): self._postLogin() return True - def login(self, email, password, max_retries=5): + def login(self, email, password, max_tries=5): """ Uses `email` and `password` to login the user (If the user is already logged in, this will do a re-login) :param email: Facebook `email` or `id` or `phone number` :param password: Facebook account password - :param max_retries: Maximum number of times to retry login - :type max_retries: int + :param max_tries: Maximum number of times to try logging in + :type max_tries: int :raises: Exception on failed login """ self.onLoggingIn(email=email) + if max_tries < 1: + raise Exception('Cannot login: max_tries should be at least one') + if not (email and password): - raise Exception("Email and password not set.") + raise Exception('Email and password not set') self.email = email self.password = password - for i in range(1, max_retries+1): + for i in range(1, max_tries+1): login_successful, login_url = self._login() if not login_successful: - log.warning("Attempt #{} failed{}".format(i,{True:', retrying'}.get(i < max_retries, ''))) + log.warning('Attempt #{} failed{}'.format(i, {True:', retrying'}.get(i < max_tries, ''))) time.sleep(1) continue else: self.onLoggedIn(email=email) break else: - raise Exception("Login failed. Check email/password. (Failed on url: {})".format(login_url)) + raise Exception('Login failed. Check email/password. (Failed on url: {})'.format(login_url)) - def logout(self, timeout=30): + def logout(self): """ Safely logs out the client @@ -472,19 +318,40 @@ class Client(object): 'h': self.fb_h } - payload=self._generatePayload(data) - r = self._session.get(ReqUrl.LOGOUT, headers=self._header, params=payload, timeout=timeout) + r = self._get(ReqUrl.LOGOUT, data) + # reset value self.payloadDefault={} self._session = requests.session() self.req_counter = 1 self.seq = "0" + self.id = None return r.ok - @deprecated(deprecated_in='0.10.2', removed_in='0.15.0', details='Use setDefaultThread instead') - def setDefaultRecipient(self, recipient_id, is_user=True): - self.setDefaultThread(str(recipient_id), thread_type=isUserToThreadType(is_user)) + """ + END LOGIN METHODS + """ + + """ + DEFAULT THREAD METHODS + """ + + def _getThread(self, given_thread_id=None, given_thread_type=None): + """ + Checks if thread ID is given, checks if default is set and returns correct values + + :raises ValueError: If thread ID is not given and there is no default + :return: Thread ID and thread type + :rtype: tuple + """ + if given_thread_id is None: + if self.default_thread_id is not None: + return self.default_thread_id, self.default_thread_type + else: + raise ValueError('Thread ID is not set') + else: + return given_thread_id, given_thread_type def setDefaultThread(self, thread_id, thread_type): """Sets default thread to send messages to @@ -500,174 +367,205 @@ class Client(object): """Resets default thread""" self.setDefaultThread(None, None) - def _getThread(self, given_thread_id=None, given_thread_type=None): - """ - Checks if thread ID is given, checks if default is set and returns correct values - - :raises ValueError: If thread ID is not given and there is no default - :return: Thread ID and thread type - :rtype: tuple - """ - if given_thread_id is None: - if self.default_thread_id is not None: - return self.default_thread_id, self.default_thread_type - else: - raise ValueError('Thread ID is not set.') - else: - return given_thread_id, given_thread_type + """ + END DEFAULT THREAD METHODS + """ """ FETCH METHODS """ - def getAllUsers(self): + def fetchAllUsers(self): """ - Gets all users from chat with info included + Gets all users the client is currently chatting with :return: :class:`models.User` objects :rtype: list + :raises: Exception if request failed """ data = { - 'viewer': self.uid, + 'viewer': self.id, } - r = self._post(ReqUrl.ALL_USERS, query=data) - if not r.ok or len(r.text) == 0: - return None - j = get_json(r) + j = checkRequest(self._post(ReqUrl.ALL_USERS, query=data)) if not j['payload']: - return None - payload = j['payload'] + raise Exception('Missing payload') + users = [] - for k in payload.keys(): - try: - user = User._adaptFromChat(payload[k]) - except KeyError: - continue - - users.append(User(user)) + for key in j['payload']: + k = j['payload'][key] + users.append(User(k['id'], first_name=k['firstName'], url=k['uri'], photo=k['thumbSrc'], name=k['name'], is_friend=k['is_friend'], gender=GENDERS[k['gender']])) return users - def getUsers(self, name): - """Find and get user by his/her name - - :param name: name of a person - :return: :class:`models.User` objects, ordered by relevance - :rtype: list - """ - + def _searchFor(self, name): payload = { 'value' : name.lower(), - 'viewer' : self.uid, - 'rsp' : "search", - 'context' : "search", - 'path' : "/home.php", + 'viewer' : self.id, + 'rsp' : 'search', + 'context' : 'search', + 'path' : '/home.php', 'request_id' : str(uuid1()), } - r = self._get(ReqUrl.SEARCH, payload) - self.j = j = get_json(r) + j = checkRequest(self._get(ReqUrl.SEARCH, payload)) - users = [] - for entry in j['payload']['entries']: - if entry['type'] == 'user': - users.append(User(entry)) - return users # have bug TypeError: __repr__ returned non-string (type bytes) + entries = [] + for k in j['payload']['entries']: + if k['type'] in ['user', 'friend']: + entries.append(User(k['uid'], url=k['path'], first_name=k['firstname'], last_name=k['lastname'], is_friend=k['is_connected'], photo=k['photo'], name=k['text'])) + if k['type'] == 'page': + if 'city_text' not in k: + k['city_text'] = None + entries.append(Page(k['uid'], url=k['path'], city=k['city_text'], likees=k['feedback_count'], sub_text=k['subtext'], photo=k['photo'], name=k['text'])) + return entries - def getUserInfo(self, *user_ids): - """Get user info from id. Unordered. + def searchForUsers(self, name): + """Find and get user by his/her name - .. todo:: - Make this return a list of User objects - - :param user_ids: One or more user ID(s) to query - :return: (list of) raw user data + :param name: name of a user + :return: :class:`models.User` objects, ordered by relevance + :rtype: list + :raises: Exception if request failed """ - def fbidStrip(_fbid): - # Stripping of `fbid:` from author_id - if type(_fbid) == int: - return _fbid + entries = self._searchFor(name) + return [k for k in entries if k.type == ThreadType.USER] - if type(_fbid) in [str, bytes] and 'fbid:' in _fbid: - return int(_fbid[5:]) + def searchForPages(self, name): + """Find and get page by its name - user_ids = [fbidStrip(uid) for uid in user_ids] + :param name: name of a page + :return: :class:`models.Page` objects, ordered by relevance + :rtype: list + :raises: Exception if request failed + """ + entries = self._searchFor(name) + return [k for k in entries if k.type == ThreadType.PAGE] + + def _fetchInfo(self, *ids): data = { - "ids[{}]".format(i): uid for i, uid in enumerate(user_ids) + "ids[{}]".format(i): _id for i, _id in enumerate(ids) } - r = self._post(ReqUrl.USER_INFO, data) - info = get_json(r) - full_data = [details for profile,details in info['payload']['profiles'].items()] - if len(full_data) == 1: - full_data = full_data[0] - return full_data + j = checkRequest(self._post(ReqUrl.INFO, data)) - def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER): + if not j['payload']['profiles']: + raise Exception('No users returned') + + entries = {} + for _id in j['payload']['profiles']: + k = j['payload']['profiles'][_id] + if k['type'] in ['user', 'friend']: + entries[_id] = User(_id, url=k['uri'], first_name=k['firstName'], is_friend=k['is_friend'], gender=GENDERS[k['gender']], photo=k['thumbSrc'], name=k['name']) + if k['type'] == 'page': + entries[_id] = Page(_id, url=k['uri'], city=None, likees=None, sub_text=None, photo=k['thumbSrc'], name=k['name']) + + return entries + + def fetchUserInfo(self, *user_ids): + """Get users' info from ids, unordered + + :param user_ids: One or more user ID(s) to query + :return: :class:`models.User` objects + :rtype: dict + :raises: Exception if request failed + """ + + entries = self._fetchInfo(*user_ids) + users = {} + for k in entries: + if entries[k].type == ThreadType.USER: + users[k] = entries[k] + + return users + + def fetchPageInfo(self, *page_ids): + """Get page's info from ids, unordered + + :param user_ids: One or more page ID(s) to query + :return: :class:`models.Page` objects + :rtype: dict + :raises: Exception if request failed + """ + + entries = self._fetchInfo(*user_ids) + users = {} + for k in entries: + if entries[k].type == ThreadType.PAGE: + users[k] = entries[k] + + return users + + def fetchThreadMessages(self, offset=0, amount=20, thread_id=None, thread_type=ThreadType.USER): """Get the last messages in a thread - :param last_n: Number of messages to retrieve + .. todo:: + Fix this. Facebook broke it somehow. Also, clean up return values + + :param offset: Where to start retrieving messages from + :param amount: Number of messages to retrieve :param thread_id: User/Group ID to retrieve from. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` - :type last_n: int + :type offset: int + :type amount: int :type thread_type: models.ThreadType :return: Dictionaries, containing message data :rtype: list + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, thread_type) - assert last_n > 0, 'length must be positive integer, got %d' % last_n + if amount < 1: + raise Exception('`amount` must be a positive integer, got {}'.format(amount)) if thread_type == ThreadType.USER: key = 'user_ids' elif thread_type == ThreadType.GROUP: key = 'thread_fbids' - data = {'messages[{}][{}][offset]'.format(key, thread_id): 0, - 'messages[{}][{}][limit]'.format(key, thread_id): last_n - 1, - 'messages[{}][{}][timestamp]'.format(key, thread_id): now()} + data = { + 'messages[{}][{}][offset]'.format(key, thread_id): offset, + 'messages[{}][{}][limit]'.format(key, thread_id): 19, + 'messages[{}][{}][timestamp]'.format(key, thread_id): now() + } - r = self._post(ReqUrl.MESSAGES, query=data) - if not r.ok or len(r.text) == 0: - return [] - - j = get_json(r) + j = checkRequest(self._post(ReqUrl.MESSAGES, query=data)) if not j['payload']: - return [] + raise Exception('Missing payload: {}, with data: {}'.format(j, data)) messages = [] for message in j['payload'].get('actions'): messages.append(Message(**message)) return list(reversed(messages)) - def getThreadList(self, start=0, length=20): + def fetchThreadList(self, offset=0, amount=20): """Get thread list of your facebook account - :param start: The offset, from where in the list to recieve threads from - :param length: The amount of threads to recieve. Maximum of 20 - :type start: int - :type length: int + .. todo:: + Clean up return values + + :param offset: The offset, from where in the list to recieve threads from + :param amount: The amount of threads to recieve. Maximum of 20 + :type offset: int + :type amount: int :return: Dictionaries, containing thread data :rtype: list + :raises: Exception if request failed """ - assert length < 21, '`length` is deprecated, max. last 20 threads are returned' + if amount > 20 or amount < 1: + raise Exception('`amount` should be between 1 and 20') data = { 'client' : self.client, 'inbox[offset]' : start, - 'inbox[limit]' : length, + 'inbox[limit]' : amount, } - r = self._post(ReqUrl.THREADS, data) - if not r.ok or len(r.text) == 0: - return [] - - j = get_json(r) + j = checkRequest(self._post(ReqUrl.THREADS, data)) # Get names for people participants = {} @@ -690,10 +588,12 @@ class Client(object): return self.threads - def getUnread(self): + def fetchUnread(self): """ .. todo:: Documenting this + + :raises: Exception if request failed """ form = { 'client': 'mercury_sync', @@ -702,7 +602,7 @@ class Client(object): # 'last_action_timestamp': 0 } - j = check_request(self._post(ReqUrl.THREAD_SYNC, form)) + j = checkRequest(self._post(ReqUrl.THREAD_SYNC, form)) return { "message_counts": j['payload']['message_counts'], @@ -724,7 +624,7 @@ class Client(object): date = datetime.now() data = { 'client': self.client, - 'author' : 'fbid:' + str(self.uid), + 'author' : 'fbid:' + str(self.id), 'timestamp' : timestamp, 'timestamp_absolute' : 'Today', 'timestamp_relative' : str(date.hour) + ":" + str(date.minute).zfill(2), @@ -752,7 +652,7 @@ class Client(object): } # Set recipient - if thread_type == ThreadType.USER: + if thread_type in [ThreadType.USER, ThreadType.PAGE]: data["other_user_fbid"] = thread_id elif thread_type == ThreadType.GROUP: data["thread_fbid"] = thread_id @@ -760,13 +660,8 @@ class Client(object): return data def _doSendRequest(self, data): - """Sends the data to `SendURL`, and returns the message id or None on failure""" - r = self._post(ReqUrl.SEND, data) - - j = check_request(r) - - if j is None: - return None + """Sends the data to `SendURL`, and returns the message ID or None on failure""" + j = checkRequest(self._post(ReqUrl.SEND, data)) try: message_ids = [action['message_id'] for action in j['payload']['actions'] if 'message_id' in action] @@ -774,28 +669,13 @@ class Client(object): log.warning("Got multiple message ids' back: {}".format(message_ids)) message_id = message_ids[0] except (KeyError, IndexError) as e: - log.warning('Error when sending message: No message ids could be found') - return None + raise Exception('Error when sending message: No message IDs could be found') log.info('Message sent.') - log.debug('Sending {}'.format(r)) - log.debug('With data {}'.format(data)) - log.debug('Recieved message id {}'.format(message_id)) + log.debug('Sending with data {}'.format(data)) + log.debug('Recieved message ID {}'.format(message_id)) return message_id - @deprecated(deprecated_in='0.10.2', removed_in='0.15.0', 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): - if add_user_ids: - return self.addUsersToChat(user_ids=add_user_ids, thread_id=recipient_id) - elif image_id: - return self.sendImage(image_id=image_id, message=message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) - elif like: - if not like in LIKES: - like = 'l' # Backwards compatability - return self.sendEmoji(emoji=None, size=LIKES[like], thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) - else: - return self.sendMessage(message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) - def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): """ Sends a message to a thread @@ -804,7 +684,8 @@ class Client(object): :param thread_id: User/Group ID to send to. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` :type thread_type: models.ThreadType - :return: :ref:`Message ID ` of the sent message or `None` on failure + :return: :ref:`Message ID ` of the sent message + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) @@ -813,7 +694,7 @@ class Client(object): data['body'] = message or '' data['has_attachment'] = False data['specific_to_list[0]'] = 'fbid:' + thread_id - data['specific_to_list[1]'] = 'fbid:' + self.uid + data['specific_to_list[1]'] = 'fbid:' + self.id return self._doSendRequest(data) @@ -827,14 +708,15 @@ class Client(object): :param thread_type: See :ref:`intro_threads` :type size: models.EmojiSize :type thread_type: models.ThreadType - :return: :ref:`Message ID ` of the sent emoji or `None` on failure + :return: :ref:`Message ID ` of the sent emoji + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) data['action_type'] = 'ma-type:user-generated-message' data['has_attachment'] = False data['specific_to_list[0]'] = 'fbid:' + thread_id - data['specific_to_list[1]'] = 'fbid:' + self.uid + data['specific_to_list[1]'] = 'fbid:' + self.id if emoji: data['body'] = emoji @@ -845,19 +727,15 @@ class Client(object): return self._doSendRequest(data) def _uploadImage(self, image_path, data, mimetype): - """Upload an image and get the image_id for sending in a message + """Upload an image and get the image_id for sending in a message""" - :param image: a tuple of (file name, data, mime type) to upload to facebook - """ - - r = self._postFile(ReqUrl.UPLOAD, { + j = checkRequest(self._postFile(ReqUrl.UPLOAD, { 'file': ( image_path, data, mimetype ) - }) - j = get_json(r) + })) # Return the image_id return j['payload']['metadata'][0]['image_id'] @@ -870,7 +748,8 @@ class Client(object): :param thread_id: User/Group ID to send to. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` :type thread_type: models.ThreadType - :return: :ref:`Message ID ` of the sent image or `None` on failure + :return: :ref:`Message ID ` of the sent image + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, thread_type) data = self._getSendData(thread_id, thread_type) @@ -879,14 +758,13 @@ class Client(object): data['body'] = message or '' data['has_attachment'] = True data['specific_to_list[0]'] = 'fbid:' + str(thread_id) - data['specific_to_list[1]'] = 'fbid:' + str(self.uid) + data['specific_to_list[1]'] = 'fbid:' + str(self.id) data['image_ids[0]'] = image_id return self._doSendRequest(data) - 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): """ Sends an image from a URL to a thread @@ -895,27 +773,16 @@ class Client(object): :param thread_id: User/Group ID to send to. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` :type thread_type: models.ThreadType - :return: :ref:`Message ID ` of the sent image or `None` on failure + :return: :ref:`Message ID ` of the sent image + :raises: Exception if request failed """ - if recipient_id is not None: - deprecation('sendRemoteImage(recipient_id)', deprecated_in='0.10.2', removed_in='0.15.0', 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', removed_in='0.15.0', details='Use sendRemoteImage(thread_type) instead') - thread_type = isUserToThreadType(is_user) - if image is not None: - deprecation('sendRemoteImage(image)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use sendRemoteImage(image_url) instead') - image_url = image - thread_id, thread_type = self._getThread(thread_id, thread_type) mimetype = guess_type(image_url)[0] remote_image = requests.get(image_url).content image_id = self._uploadImage(image_url, remote_image, mimetype) return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type) - 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 + def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER): """ Sends a local image to a thread @@ -924,18 +791,9 @@ class Client(object): :param thread_id: User/Group ID to send to. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` :type thread_type: models.ThreadType - :return: :ref:`Message ID ` of the sent image or `None` on failure + :return: :ref:`Message ID ` of the sent image + :raises: Exception if request failed """ - if recipient_id is not None: - deprecation('sendLocalImage(recipient_id)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use sendLocalImage(thread_id) instead') - thread_id = recipient_id - if is_user is not None: - deprecation('sendLocalImage(is_user)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use sendLocalImage(thread_type) instead') - thread_type = isUserToThreadType(is_user) - if image is not None: - deprecation('sendLocalImage(image)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use sendLocalImage(image_path) instead') - image_path = image - thread_id, thread_type = self._getThread(thread_id, thread_type) mimetype = guess_type(image_path)[0] image_id = self._uploadImage(image_path, open(image_path, 'rb'), mimetype) @@ -945,10 +803,11 @@ class Client(object): """ Adds users to a group. - :param user_ids: One or more user ids to add + :param user_ids: One or more user IDs to add :param thread_id: Group ID to add people to. See :ref:`intro_threads` :type user_ids: list - :return: :ref:`Message ID ` of the executed action or `None` on failure + :return: :ref:`Message ID ` of the executed action + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, None) data = self._getSendData(thread_id, ThreadType.GROUP) @@ -963,10 +822,8 @@ class Client(object): user_ids = set(user_ids) for i, user_id in enumerate(user_ids): - if user_id == self.uid: - log.warning('Error when adding users: Cannot add self to group chat') - if len(user_ids) == 0: - return None + if user_id == self.id: + raise Exception('Error when adding users: Cannot add self to group thread') else: data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id) @@ -978,8 +835,7 @@ class Client(object): :param user_id: User ID to remove :param thread_id: Group ID to remove people from. See :ref:`intro_threads` - :return: True if the action was successful - :rtype: bool + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, None) @@ -989,39 +845,19 @@ class Client(object): "tid": thread_id } - j = check_request(self._post(ReqUrl.REMOVE_USER, data)) - - return False if j is None else True - - @deprecated(deprecated_in='0.10.2', removed_in='0.15.0', details='Use removeUserFromGroup() instead') - def add_users_to_chat(self, threadID, userID): - if not isinstance(userID, list): - userID = [userID] - return self.addUsersToGroup(userID, thread_id=threadID) - - @deprecated(deprecated_in='0.10.2', removed_in='0.15.0', details='Use removeUserFromGroup() instead') - def remove_user_from_chat(self, threadID, userID): - return self.removeUserFromGroup(userID, thread_id=threadID) + j = checkRequest(self._post(ReqUrl.REMOVE_USER, data)) def changeThreadTitle(self, title, thread_id=None, thread_type=ThreadType.USER): """ Changes title of a thread. If this is executed on a user thread, this will change the nickname of that user, effectively changing the title - :param title: New group chat title + :param title: New group thread title :param thread_id: Group ID to change title of. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` :type thread_type: models.ThreadType - :return: True if the action was successful - :rtype: bool + :raises: Exception if request failed """ - # For backwards compatability. Previously `thread_id` and `title` were swapped around - try: - int(thread_id) - except ValueError: - deprecation('changeThreadTitle(threadID, newTitle)', deprecated_in='0.10.2', removed_in='0.15.0', details='Use changeThreadTitle(title, thread_id, thread_type) instead') - title, thread_id, thread_type = thread_id, title, ThreadType.GROUP - thread_id, thread_type = self._getThread(thread_id, thread_type) if thread_type == ThreadType.USER: @@ -1034,8 +870,6 @@ class Client(object): data['log_message_data[name]'] = title data['log_message_type'] = 'log:thread-name' - return False if self._doSendRequest(data) is None else True - def changeNickname(self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER): """ Changes the nickname of a user in a thread @@ -1045,8 +879,7 @@ class Client(object): :param thread_id: User/Group ID to change color of. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` :type thread_type: models.ThreadType - :return: True if the action was successful - :rtype: bool + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, thread_type) @@ -1056,9 +889,7 @@ class Client(object): 'thread_or_other_fbid': thread_id } - j = check_request(self._post(ReqUrl.THREAD_NICKNAME, data)) - - return False if j is None else True + j = checkRequest(self._post(ReqUrl.THREAD_NICKNAME, data)) def changeThreadColor(self, color, thread_id=None): """ @@ -1067,8 +898,7 @@ class Client(object): :param color: New thread color :param thread_id: User/Group ID to change color of. See :ref:`intro_threads` :type color: models.ThreadColor - :return: True if the action was successful - :rtype: bool + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, None) @@ -1077,9 +907,7 @@ class Client(object): 'thread_or_other_fbid': thread_id } - j = check_request(self._post(ReqUrl.THREAD_COLOR, data)) - - return False if j is None else True + j = checkRequest(self._post(ReqUrl.THREAD_COLOR, data)) def changeThreadEmoji(self, emoji, thread_id=None): """ @@ -1089,8 +917,7 @@ class Client(object): :param color: New thread emoji :param thread_id: User/Group ID to change emoji of. See :ref:`intro_threads` - :return: True if the action was successful - :rtype: bool + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, None) @@ -1099,19 +926,16 @@ class Client(object): 'thread_or_other_fbid': thread_id } - j = check_request(self._post(ReqUrl.THREAD_EMOJI, data)) - - return False if j is None else True + j = checkRequest(self._post(ReqUrl.THREAD_EMOJI, data)) def reactToMessage(self, message_id, reaction): """ - Reacts to a message. + Reacts to a message :param message_id: :ref:`Message ID ` to react to :param reaction: Reaction emoji to use :type reaction: models.MessageReaction - :return: True if the action was successful - :rtype: bool + :raises: Exception if request failed """ full_data = { "doc_id": 1491398900900362, @@ -1120,7 +944,7 @@ class Client(object): "data": { "action": "ADD_REACTION", "client_mutation_id": "1", - "actor_id": self.uid, + "actor_id": self.id, "message_id": str(message_id), "reaction": reaction.value } @@ -1129,14 +953,12 @@ class Client(object): try: url_part = urllib.parse.urlencode(full_data) except AttributeError: - # This is a very hacky solution, please suggest a better one ;) + # This is a very hacky solution for python 2 support, please suggest a better one ;) url_part = urllib.urlencode(full_data)\ .replace('u%27', '%27')\ .replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1]) - j = check_request(self._post('{}/?{}'.format(ReqUrl.MESSAGE_REACTION, url_part))) - - return False if j is None else True + j = checkRequest(self._post('{}/?{}'.format(ReqUrl.MESSAGE_REACTION, url_part))) def setTypingStatus(self, status, thread_id=None, thread_type=None): """ @@ -1147,8 +969,7 @@ class Client(object): :param thread_type: See :ref:`intro_threads` :type status: models.TypingStatus :type thread_type: models.ThreadType - :return: True if the action was successful - :rtype: bool + :raises: Exception if request failed """ thread_id, thread_type = self._getThread(thread_id, None) @@ -1159,9 +980,7 @@ class Client(object): "source": "mercury-chat" } - j = check_request(self._post(ReqUrl.TYPING, data)) - - return False if j is None else True + j = checkRequest(self._post(ReqUrl.TYPING, data)) """ END SEND METHODS @@ -1202,10 +1021,6 @@ class Client(object): r = self._post(ReqUrl.MARK_SEEN, {"seen_timestamp": 0}) return r.ok - @deprecated(deprecated_in='0.10.2', removed_in='0.15.0', details='Use friendConnect() instead') - def friend_connect(self, friend_id): - return self.friendConnect(friend_id) - def friendConnect(self, friend_id): """ .. todo:: @@ -1219,22 +1034,25 @@ class Client(object): r = self._post(ReqUrl.CONNECT, data) return r.ok + + """ + LISTEN METHODS + """ + def _ping(self, sticky): data = { 'channel': self.user_channel, 'clientid': self.client_id, 'partition': -2, 'cap': 0, - 'uid': self.uid, + 'uid': self.id, 'sticky': sticky, - 'viewer_uid': self.uid + 'viewer_uid': self.id } - return False if check_request(self._get(ReqUrl.PING, data), check_json=False) is None else True + checkRequest(self._get(ReqUrl.PING, data), check_json=False) - def _getSticky(self): - """Call pull api to get sticky and pool parameter, - newer api needs these parameter to work. - """ + def _fetchSticky(self): + """Call pull api to get sticky and pool parameter, newer api needs these parameters to work""" data = { "msgs_recv": 0, @@ -1242,15 +1060,12 @@ class Client(object): "clientid": self.client_id } - r = self._get(ReqUrl.STICKY, data) - j = get_json(r) + j = checkRequest(self._get(ReqUrl.STICKY, data)) if 'lb_info' not in j: - raise Exception('Get sticky pool error') + raise Exception('Missing lb_info') - sticky = j['lb_info']['sticky'] - pool = j['lb_info']['pool'] - return sticky, pool + return j['lb_info']['sticky'], j['lb_info']['pool'] def _pullMessage(self, sticky, pool): """Call pull api with seq value to get message data.""" @@ -1262,8 +1077,7 @@ class Client(object): "clientid": self.client_id, } - r = self._get(ReqUrl.STICKY, data) - j = get_json(r) + j = checkRequest(self._get(ReqUrl.STICKY, data)) self.seq = j.get('seq', '0') return j @@ -1281,7 +1095,7 @@ class Client(object): if mtype == "delta": def getThreadIdAndThreadType(msg_metadata): - """Returns a tuple consisting of thread id and thread type""" + """Returns a tuple consisting of thread ID and thread type""" id_thread = None type_thread = None if 'threadFbId' in msg_metadata['threadKey']: @@ -1368,7 +1182,7 @@ class Client(object): delivered_ts = int(delta["watermarkTimestampMs"]) thread_id, thread_type = getThreadIdAndThreadType(delta) self.onMessageSeen(seen_by=seen_by, thread_id=thread_id, thread_type=thread_type, - seen_ts=seen_ts, delivered_ts=delivered_ts, metadata=metadata, msg=m) + seen_ts=seen_ts, ts=delivered_ts, metadata=metadata, msg=m) continue # Messages marked as seen @@ -1415,7 +1229,7 @@ class Client(object): # Happens on every login elif mtype == "qprimer": - self.onQprimer(made=m.get("made"), msg=m) + self.onQprimer(ts=m.get("made"), msg=m) # Is sent before any other message elif mtype == "deltaflow": @@ -1428,58 +1242,49 @@ class Client(object): except Exception as e: self.onMessageError(exception=e, msg=m) - - @deprecated(deprecated_in='0.10.2', removed_in='0.15.0', details='Use startListening() instead') - def start_listening(self): - return self.startListening() - def startListening(self): - """Start listening from an external event loop.""" + """ + Start listening from an external event loop + + :raises: Exception if request failed + """ self.listening = True - self.sticky, self.pool = self._getSticky() - - - @deprecated(deprecated_in='0.10.2', removed_in='0.15.0', details='Use doOneListen() instead') - def do_one_listen(self, markAlive=True): - return self.doOneListen(markAlive) + self.sticky, self.pool = self._fetchSticky() def doOneListen(self, markAlive=True): """ Does one cycle of the listening loop. This method is useful if you want to control fbchat from an external event loop + .. note:: + markAlive is currently broken, and is ignored + :param markAlive: Whether this should ping the Facebook server before running :type markAlive: bool :return: Whether the loop should keep running :rtype: bool """ try: - if markAlive: self._ping(self.sticky) + #if markAlive: self._ping(self.sticky) try: content = self._pullMessage(self.sticky, self.pool) if content: self._parseMessage(content) - except requests.exceptions.RequestException as e: + except requests.exceptions.RequestException: pass + except Exception as e: + return self.onListenError(exception=e) except KeyboardInterrupt: return False except requests.exceptions.Timeout: pass - except Exception as e: - return self.onListenError(e) return True - - @deprecated(deprecated_in='0.10.2', removed_in='0.15.0', details='Use stopListening() instead') - def stop_listening(self): - return self.stopListening() - def stopListening(self): """Cleans up the variables from startListening""" self.listening = False self.sticky, self.pool = (None, None) - def listen(self, markAlive=True): """ Initializes and runs the listening loop continually @@ -1494,3 +1299,250 @@ class Client(object): pass self.stopListening() + + """ + END LISTEN METHODS + """ + + """ + EVENTS + """ + + def onLoggingIn(self, email=None): + """ + Called when the client is logging in + + :param email: The email of the client + """ + log.info("Logging in {}...".format(email)) + + def on2FACode(self): + """Called when a 2FA code is needed to progress""" + input('Please enter your 2FA code --> ') + + def onLoggedIn(self, email=None): + """ + Called when the client is successfully logged in + + :param email: The email of the client + """ + log.info("Login of {} successful.".format(email)) + + def onListening(self): + """Called when the client is listening""" + log.info("Listening...") + + def onListenError(self, exception=None): + """ + Called when an error was encountered while listening + + :param exception: The exception that was encountered + """ + raise exception + + + def onMessage(self, mid=None, author_id=None, message=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): + """ + Called when the client is listening, and somebody sends a message + + :param mid: The message ID + :param author_id: The ID of the author + :param message: The message + :param thread_id: Thread ID that the message was sent to. See :ref:`intro_threads` + :param thread_type: Type of thread that the message was sent to. See :ref:`intro_threads` + :param ts: The timestamp of the message + :param metadata: Extra metadata about the message + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message)) + + def onColorChange(self, mid=None, author_id=None, new_color=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): + """ + Called when the client is listening, and somebody changes a thread's color + + :param mid: The action ID + :param author_id: The ID of the person who changed the color + :param new_color: The new color + :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 metadata: Extra metadata about the action + :param msg: A full set of the data recieved + :type new_color: models.ThreadColor + :type thread_type: models.ThreadType + """ + log.info("Color change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_color)) + + def onEmojiChange(self, mid=None, author_id=None, new_emoji=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): + """ + Called when the client is listening, and somebody changes a thread's emoji + + :param mid: The action ID + :param author_id: The ID of the person who changed the emoji + :param new_emoji: The new emoji + :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 metadata: Extra metadata about the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("Emoji change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_emoji)) + + def onTitleChange(self, mid=None, author_id=None, new_title=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): + """ + Called when the client is listening, and somebody changes the title of a thread + + :param mid: The action ID + :param author_id: The ID of the person who changed the title + :param new_title: The new title + :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 metadata: Extra metadata about the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("Title change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_title)) + + def onNicknameChange(self, mid=None, author_id=None, changed_for=None, new_nickname=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): + """ + Called when the client is listening, and somebody changes the nickname of a person + + :param mid: The action ID + :param author_id: The ID of the person who changed the nickname + :param changed_for: The ID of the person whom got their nickname changed + :param new_nickname: The new nickname + :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 metadata: Extra metadata about the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("Nickname change from {} in {} ({}) for {}: {}".format(author_id, thread_id, thread_type.name, changed_for, new_nickname)) + + + def onMessageSeen(self, seen_by=None, thread_id=None, thread_type=ThreadType.USER, seen_ts=None, ts=None, metadata=None, msg={}): + """ + Called when the client is listening, and somebody marks a message as seen + + :param seen_by: The ID of the person who marked the message as seen + :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 seen_ts: A timestamp of when the person saw the message + :param ts: A timestamp of the action + :param metadata: Extra metadata about the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("Messages seen by {} in {} ({}) at {}s".format(seen_by, thread_id, thread_type.name, seen_ts/1000)) + + def onMessageDelivered(self, msg_ids=None, delivered_for=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): + """ + Called when the client is listening, and somebody marks messages as delivered + + :param msg_ids: The messages that are marked as delivered + :param delivered_for: The person that marked the messages as delivered + :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 metadata: Extra metadata about the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("Messages {} delivered to {} in {} ({}) at {}s".format(msg_ids, delivered_for, thread_id, thread_type.name, ts/1000)) + + def onMarkedSeen(self, threads=None, seen_ts=None, ts=None, metadata=None, msg={}): + """ + Called when the client is listening, and the client has successfully marked threads as seen + + :param threads: The threads that were marked + :param author_id: The ID of the person who changed the emoji + :param seen_ts: A timestamp of when the threads were seen + :param ts: A timestamp of the action + :param metadata: Extra metadata about the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("Marked messages as seen in threads {} at {}s".format([(x[0], x[1].name) for x in threads], seen_ts/1000)) + + + def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, ts=None, msg={}): + """ + Called when the client is listening, and somebody adds people to a group thread + + :param mid: The action ID + :param added_ids: The IDs of the people who got added + :param author_id: The ID of the person who added the people + :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param ts: A timestamp of the action + :param msg: A full set of the data recieved + """ + log.info("{} added: {}".format(author_id, ', '.join(added_ids))) + + def onPersonRemoved(self, mid=None, removed_id=None, author_id=None, thread_id=None, ts=None, msg={}): + """ + Called when the client is listening, and somebody removes a person from a group thread + + :param mid: The action ID + :param removed_id: The ID of the person who got removed + :param author_id: The ID of the person who removed the person + :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param ts: A timestamp of the action + :param msg: A full set of the data recieved + """ + log.info("{} removed: {}".format(author_id, removed_id)) + + def onFriendRequest(self, from_id=None, msg={}): + """ + Called when the client is listening, and somebody sends a friend request + + :param from_id: The ID of the person that sent the request + :param msg: A full set of the data recieved + """ + log.info("Friend request from {}".format(from_id)) + + def onInbox(self, unseen=None, unread=None, recent_unread=None, msg={}): + """ + .. todo:: + Documenting this + + :param unseen: -- + :param unread: -- + :param recent_unread: -- + :param msg: A full set of the data recieved + """ + log.info('Inbox event: {}, {}, {}'.format(unseen, unread, recent_unread)) + + def onQprimer(self, made=None, msg={}): + """ + 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 onUnknownMesssageType(self, msg={}): + """ + 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={}): + """ + 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 + """ diff --git a/fbchat/event_hook.py b/fbchat/event_hook.py deleted file mode 100644 index 9fa24b6..0000000 --- a/fbchat/event_hook.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: UTF-8 -*- - -class EventHook(object): - """ - A simple implementation of the Observer-Pattern. - All listeners added to this will be called, regardless of parameters - """ - - def __init__(self, *args): - self._handlers = list(args) - - def add(self, handler): - return self.__iadd__(handler) - - def remove(self, handler): - return self.__isub__(handler) - - def __iadd__(self, handler): - self._handlers.append(handler) - return self - - def __isub__(self, handler): - self._handlers.remove(handler) - return self - - def __call__(self, *args, **kwargs): - for handler in self._handlers: - handler(*args, **kwargs) diff --git a/fbchat/models.py b/fbchat/models.py index 657cc4e..517424e 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -3,81 +3,76 @@ from __future__ import unicode_literals import enum -class User(object): - """Represents a Facebook User""" - +class Thread(object): #: The unique identifier of the user. Can be used a `thread_id`. See :ref:`intro_threads` for more info - uid = None - #: Currently always set to `user`. Might change in the future - type = 'user' - #: The profile picture of the user + id = None + #: Specifies the type of thread. Uses ThreadType + type = None + #: The thread's picture photo = None - #: The profile url - url = None - #: The name of the user + #: The name of the thread name = None - #: Only used by :any:`Client.getUsers`. Each user is assigned a score between 0 and 1, based on how likely it is that they were the person being searched for - score = None - #: Dictionary containing raw userdata from when the :class:`User` was created - data = None - def __init__(self, data): - """Represents a Facebook User""" - if data['type'] != 'user': - raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path'])) - self.uid = data['uid'] - self.type = data['type'] - self.photo = data['photo'] - self.url = data['path'] - self.name = data['text'] - self.score = float(data['score']) - self.data = data + def __init__(self, _type, _id, photo=None, name=None): + """Represents a Facebook thread""" + self.id = str(_id) + self.type = _type + self.photo = photo + self.name = name def __repr__(self): return self.__unicode__() def __unicode__(self): - return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url) + return '<{} {} ({})>'.format(self.type.name, self.name, self.id) - @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 - :return: :class:`User` object +class User(Thread): + #: The profile url + url = None + #: The users first name + first_name = None + #: The users last name + last_name = None + #: Whether the user and the client are friends + is_friend = None + #: The user's gender + gender = None - '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 - """ + def __init__(self, _id, url=None, first_name=None, last_name=None, is_friend=None, gender=None, **kwargs): + """Represents a Facebook user. Inherits `Thread`""" + super(User, self).__init__(ThreadType.USER, _id, **kwargs) + self.url = url + self.first_name = first_name + self.last_name = last_name + self.is_friend = is_friend + self.gender = gender - return { - 'type': 'user', - 'uid': user_in_chat['id'], - 'photo': user_in_chat['thumbSrc'], - 'path': user_in_chat['uri'], - 'text': user_in_chat['name'], - 'score': 1, - 'data': user_in_chat, - } -class Thread(object): - """Represents a thread. Currently just acts as a dict""" - def __init__(self, **entries): - self.__dict__.update(entries) +class Group(Thread): + def __init__(self, _id, **kwargs): + """Represents a Facebook group. Inherits `Thread`""" + super(Group, self).__init__(ThreadType.GROUP, _id, **kwargs) + + +class Page(Thread): + #: The page's custom url + url = None + #: The name of the page's location city + city = None + #: Amount of likes the page has + likees = None + #: Some extra information about the page + sub_text = None + + def __init__(self, _id, url=None, city=None, likees=None, sub_text=None, **kwargs): + """Represents a Facebook page. Inherits `Thread`""" + super(Page, self).__init__(ThreadType.PAGE, _id, **kwargs) + self.url = url + self.city = city + self.likees = likees + self.sub_text = sub_text + class Message(object): """Represents a message. Currently just acts as a dict""" @@ -94,6 +89,7 @@ class ThreadType(Enum): """Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info""" USER = 1 GROUP = 2 + PAGE = 3 class TypingStatus(Enum): """Used to specify whether the user is typing or has stopped typing""" diff --git a/fbchat/utils.py b/fbchat/utils.py index 718cf63..e36b882 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -6,8 +6,22 @@ import json from time import time from random import random import warnings +import logging from .models import * +# Python 2's `input` executes the input, whereas `raw_input` just returns the input +try: + input = raw_input +except NameError: + pass + +# Log settings +log = logging.getLogger("client") +log.setLevel(logging.DEBUG) +# Creates the console handler +handler = logging.StreamHandler() +log.addHandler(handler) + #: Default list of user agents 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", @@ -49,7 +63,7 @@ class ReqUrl(object): STICKY = "https://0-edge-chat.facebook.com/pull" PING = "https://0-channel-proxy-06-ash2.facebook.com/active_ping" UPLOAD = "https://upload.facebook.com/ajax/mercury/upload.php" - USER_INFO = "https://www.facebook.com/chat/user_info/" + INFO = "https://www.facebook.com/chat/user_info/" CONNECT = "https://www.facebook.com/ajax/add_friend/action.php?dpr=1" REMOVE_USER = "https://www.facebook.com/chat/remove_participants/" LOGOUT = "https://www.facebook.com/logout.php" @@ -82,7 +96,7 @@ def get_decoded(r): def get_json(r): return json.loads(strip_to_json(get_decoded(r))) -def digit_to_char(digit): +def digitToChar(digit): if digit < 10: return str(digit) return chr(ord('a') + digit - 10) @@ -92,8 +106,8 @@ def str_base(number, base): return '-' + str_base(-number, base) (d, m) = divmod(number, base) if d > 0: - return str_base(d, base) + digit_to_char(m) - return digit_to_char(m) + return str_base(d, base) + digitToChar(m) + return digitToChar(m) def generateMessageID(client_id=None): k = now() @@ -110,31 +124,20 @@ def generateOfflineThreadingID(): msgs = format(ret, 'b') + string return str(int(msgs, 2)) -def isUserToThreadType(is_user): - return ThreadType.USER if is_user else ThreadType.GROUP +def checkRequest(r, check_json=True): + if not r.ok: + raise Exception('Error when sending request: Got {} response'.format(r.status_code)) -def raise_exception(e): - raise e + content = get_decoded(r) -def deprecation(name, deprecated_in=None, removed_in=None, details='', stacklevel=3): - """Used to mark parameters as deprecated. Will result in a warning being emmitted when the parameter is used.""" - warning = "Client.{} is deprecated".format(name) - if deprecated_in: - warning += ' in v. {}'.format(deprecated_in) - if removed_in: - warning += ' and will be removed in v. {}'.format(removed_in) - if details: - warning += '. {}'.format(details) + if content is None or len(content) == 0: + raise Exception('Error when sending request: Got empty response') - warnings.simplefilter('always', DeprecationWarning) - warnings.warn(warning, category=DeprecationWarning, stacklevel=stacklevel) - warnings.simplefilter('default', DeprecationWarning) - -def deprecated(deprecated_in=None, removed_in=None, details=''): - """A decorator used to mark functions as deprecated. 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.__name__, deprecated_in=deprecated_in, removed_in=removed_in, details=details, stacklevel=3) - return func(*args, **kwargs) - return wrapped_func - return wrap + if check_json: + j = json.loads(strip_to_json(content)) + if 'error' in j: + # 'errorDescription' is in the users own language! + raise Exception('Error #{} when sending request: {}'.format(j['error'], j['errorDescription'])) + return j + else: + return r diff --git a/tests.py b/tests.py index 001e178..cc322d7 100644 --- a/tests.py +++ b/tests.py @@ -27,7 +27,7 @@ class CustomClient(Client): self.got_qprimer = False super(type(self), self).__init__(*args, **kwargs) - def onQprimer(self, made, msg): + def onQprimer(self, msg, **kwargs): self.got_qprimer = True class TestFbchat(unittest.TestCase): @@ -49,7 +49,7 @@ class TestFbchat(unittest.TestCase): self.assertFalse(client.isLoggedIn()) with self.assertRaises(Exception): - client.login('', '', max_retries=1) + client.login('', '', max_tries=1) client.login(email, password) @@ -64,10 +64,10 @@ class TestFbchat(unittest.TestCase): def test_defaultThread(self): # setDefaultThread - client.setDefaultThread(group_uid, ThreadType.GROUP) + client.setDefaultThread(group_id, ThreadType.GROUP) self.assertTrue(client.sendMessage('test_default_recipient★')) - client.setDefaultThread(user_uid, ThreadType.USER) + client.setDefaultThread(user_id, ThreadType.USER) self.assertTrue(client.sendMessage('test_default_recipient★')) # resetDefaultThread @@ -75,57 +75,58 @@ class TestFbchat(unittest.TestCase): with self.assertRaises(ValueError): client.sendMessage('should_not_send') - def test_getAllUsers(self): - users = client.getAllUsers() + def test_fetchAllUsers(self): + users = client.fetchAllUsers() self.assertGreater(len(users), 0) - def test_getUsers(self): - users = client.getUsers('Mark Zuckerberg') + def test_searchForUsers(self): + users = client.searchForUsers('Mark Zuckerberg') self.assertGreater(len(users), 0) u = users[0] # Test if values are set correctly - self.assertIsInstance(u.uid, int) - self.assertEqual(u.type, 'user') + self.assertEqual(u.id, '4') + self.assertEqual(u.type, ThreadType.USER) self.assertEqual(u.photo[:4], 'http') self.assertEqual(u.url[:4], 'http') self.assertEqual(u.name, 'Mark Zuckerberg') - self.assertGreater(u.score, 0) def test_sendEmoji(self): - self.assertTrue(client.sendEmoji(size=EmojiSize.SMALL, thread_id=user_uid, thread_type=ThreadType.USER)) - self.assertTrue(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=user_uid, thread_type=ThreadType.USER)) - self.assertTrue(client.sendEmoji('😆', EmojiSize.LARGE, user_uid, ThreadType.USER)) + self.assertIsNotNone(client.sendEmoji(size=EmojiSize.SMALL, thread_id=user_id, thread_type=ThreadType.USER)) + self.assertIsNotNone(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=user_id, thread_type=ThreadType.USER)) + self.assertIsNotNone(client.sendEmoji('😆', EmojiSize.LARGE, user_id, ThreadType.USER)) - self.assertTrue(client.sendEmoji(size=EmojiSize.SMALL, thread_id=group_uid, thread_type=ThreadType.GROUP)) - self.assertTrue(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=group_uid, thread_type=ThreadType.GROUP)) - self.assertTrue(client.sendEmoji('😆', EmojiSize.LARGE, group_uid, ThreadType.GROUP)) + self.assertIsNotNone(client.sendEmoji(size=EmojiSize.SMALL, thread_id=group_id, thread_type=ThreadType.GROUP)) + self.assertIsNotNone(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=group_id, thread_type=ThreadType.GROUP)) + self.assertIsNotNone(client.sendEmoji('😆', EmojiSize.LARGE, group_id, ThreadType.GROUP)) def test_sendMessage(self): - self.assertIsNotNone(client.sendMessage('test_send_user★', user_uid, ThreadType.USER)) - self.assertIsNotNone(client.sendMessage('test_send_group★', group_uid, ThreadType.GROUP)) - self.assertIsNone(client.sendMessage('test_send_user_should_fail★', user_uid, ThreadType.GROUP)) - self.assertIsNone(client.sendMessage('test_send_group_should_fail★', group_uid, ThreadType.USER)) + self.assertIsNotNone(client.sendMessage('test_send_user★', user_id, ThreadType.USER)) + self.assertIsNotNone(client.sendMessage('test_send_group★', group_id, ThreadType.GROUP)) + with self.assertRaises(Exception): + client.sendMessage('test_send_user_should_fail★', user_id, ThreadType.GROUP) + with self.assertRaises(Exception): + client.sendMessage('test_send_group_should_fail★', group_id, ThreadType.USER) def test_sendImages(self): image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png' image_local_url = path.join(path.dirname(__file__), 'tests/image.png') - 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)) - 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.sendRemoteImage(image_url, 'test_send_user_images_remote★', user_id, ThreadType.USER)) + self.assertTrue(client.sendRemoteImage(image_url, 'test_send_group_images_remote★', group_id, ThreadType.GROUP)) + self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', user_id, ThreadType.USER)) + self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', group_id, ThreadType.GROUP)) - def test_getThreadInfo(self): - client.sendMessage('test_user_getThreadInfo★', user_uid, ThreadType.USER) + def test_fetchThreadMessages(self): + client.sendMessage('test_user_getThreadInfo★', thread_id=user_id, thread_type=ThreadType.USER) - info = client.getThreadInfo(20, user_uid, ThreadType.USER) + info = client.fetchThreadMessages(offset=0, amount=2, thread_id=user_id, thread_type=ThreadType.USER) self.assertEqual(info[0].author, 'fbid:' + client.uid) self.assertEqual(info[0].body, 'test_user_getThreadInfo★') - client.sendMessage('test_group_getThreadInfo★', group_uid, ThreadType.GROUP) + client.sendMessage('test_group_getThreadInfo★', thread_id=group_id, thread_type=ThreadType.GROUP) - info = client.getThreadInfo(20, group_uid, ThreadType.GROUP) + info = client.fetchThreadMessages(offset=0, amount=2, thread_id=group_id, thread_type=ThreadType.GROUP) self.assertEqual(info[0].author, 'fbid:' + client.uid) self.assertEqual(info[0].body, 'test_group_getThreadInfo★') @@ -136,58 +137,57 @@ class TestFbchat(unittest.TestCase): self.assertTrue(client.got_qprimer) - def test_getUserInfo(self): - info = client.getUserInfo(4) - self.assertEqual(info['name'], 'Mark Zuckerberg') + def test_fetchUserInfo(self): + info = client.fetchUserInfo('4')['4'] + self.assertEqual(info.name, 'Mark Zuckerberg') def test_removeAddFromGroup(self): - self.assertTrue(client.removeUserFromGroup(user_uid, thread_id=group_uid)) - self.assertTrue(client.addUsersToGroup(user_uid, thread_id=group_uid)) + client.removeUserFromGroup(user_id, thread_id=group_id) + client.addUsersToGroup(user_id, thread_id=group_id) def test_changeThreadTitle(self): - self.assertTrue(client.changeThreadTitle('test_changeThreadTitle★', thread_id=group_uid, thread_type=ThreadType.GROUP)) - self.assertTrue(client.changeThreadTitle('test_changeThreadTitle★', thread_id=user_uid, thread_type=ThreadType.USER)) + client.changeThreadTitle('test_changeThreadTitle★', thread_id=group_id, thread_type=ThreadType.GROUP) + client.changeThreadTitle('test_changeThreadTitle★', thread_id=user_id, thread_type=ThreadType.USER) def test_changeNickname(self): - self.assertTrue(client.changeNickname('test_changeNicknameSelf★', client.uid, thread_id=user_uid, thread_type=ThreadType.USER)) - self.assertTrue(client.changeNickname('test_changeNicknameOther★', user_uid, thread_id=user_uid, thread_type=ThreadType.USER)) - self.assertTrue(client.changeNickname('test_changeNicknameSelf★', client.uid, thread_id=group_uid, thread_type=ThreadType.GROUP)) - self.assertTrue(client.changeNickname('test_changeNicknameOther★', user_uid, thread_id=group_uid, thread_type=ThreadType.GROUP)) + client.changeNickname('test_changeNicknameSelf★', client.id, thread_id=user_id, thread_type=ThreadType.USER) + client.changeNickname('test_changeNicknameOther★', user_id, thread_id=user_id, thread_type=ThreadType.USER) + client.changeNickname('test_changeNicknameSelf★', client.id, thread_id=group_id, thread_type=ThreadType.GROUP) + client.changeNickname('test_changeNicknameOther★', user_id, thread_id=group_id, thread_type=ThreadType.GROUP) def test_changeThreadEmoji(self): - self.assertTrue(client.changeThreadEmoji('😀', group_uid)) - self.assertTrue(client.changeThreadEmoji('😀', user_uid)) - self.assertTrue(client.changeThreadEmoji('😆', group_uid)) - self.assertTrue(client.changeThreadEmoji('😆', user_uid)) + client.changeThreadEmoji('😀', group_id) + client.changeThreadEmoji('😀', user_id) + client.changeThreadEmoji('😆', group_id) + client.changeThreadEmoji('😆', user_id) def test_changeThreadColor(self): - self.assertTrue(client.changeThreadColor(ThreadColor.BRILLIANT_ROSE, group_uid)) - self.assertTrue(client.changeThreadColor(ThreadColor.MESSENGER_BLUE, group_uid)) - self.assertTrue(client.changeThreadColor(ThreadColor.BRILLIANT_ROSE, user_uid)) - self.assertTrue(client.changeThreadColor(ThreadColor.MESSENGER_BLUE, user_uid)) + client.changeThreadColor(ThreadColor.BRILLIANT_ROSE, group_id) + client.changeThreadColor(ThreadColor.MESSENGER_BLUE, group_id) + client.changeThreadColor(ThreadColor.BRILLIANT_ROSE, user_id) + client.changeThreadColor(ThreadColor.MESSENGER_BLUE, user_id) def test_reactToMessage(self): - mid = client.sendMessage('test_reactToMessage★', user_uid, ThreadType.USER) - self.assertTrue(client.reactToMessage(mid, MessageReaction.LOVE)) - mid = client.sendMessage('test_reactToMessage★', group_uid, ThreadType.GROUP) - self.assertTrue(client.reactToMessage(mid, MessageReaction.LOVE)) + mid = client.sendMessage('test_reactToMessage★', user_id, ThreadType.USER) + client.reactToMessage(mid, MessageReaction.LOVE) + mid = client.sendMessage('test_reactToMessage★', group_id, ThreadType.GROUP) + client.reactToMessage(mid, MessageReaction.LOVE) def test_setTypingStatus(self): - self.assertTrue(client.sendMessage('Hi', thread_id=user_uid, thread_type=ThreadType.USER)) - self.assertTrue(client.setTypingStatus(TypingStatus.TYPING, thread_id=user_uid, thread_type=ThreadType.USER)) - self.assertTrue(client.setTypingStatus(TypingStatus.STOPPED, thread_id=user_uid, thread_type=ThreadType.USER)) - self.assertTrue(client.setTypingStatus(TypingStatus.TYPING, thread_id=group_uid, thread_type=ThreadType.GROUP)) - self.assertTrue(client.setTypingStatus(TypingStatus.STOPPED, thread_id=group_uid, thread_type=ThreadType.GROUP)) + client.setTypingStatus(TypingStatus.TYPING, thread_id=user_id, thread_type=ThreadType.USER) + client.setTypingStatus(TypingStatus.STOPPED, thread_id=user_id, thread_type=ThreadType.USER) + client.setTypingStatus(TypingStatus.TYPING, thread_id=group_id, thread_type=ThreadType.GROUP) + client.setTypingStatus(TypingStatus.STOPPED, thread_id=group_id, thread_type=ThreadType.GROUP) -def start_test(param_client, param_group_uid, param_user_uid, tests=[]): +def start_test(param_client, param_group_id, param_user_id, tests=[]): global client - global group_uid - global user_uid + global group_id + global user_id client = param_client - group_uid = param_group_uid - user_uid = param_user_uid + group_id = param_group_id + user_id = param_user_id tests = ['test_' + test if 'test_' != test[:5] else test for test in tests] @@ -213,16 +213,16 @@ if __name__ == '__main__': json = json.load(f) email = json['email'] password = json['password'] - user_uid = json['user_thread_id'] - group_uid = json['group_thread_id'] + user_id = json['user_thread_id'] + group_id = json['group_thread_id'] except (IOError, IndexError) as e: email = input('Email: ') password = getpass() - 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): ') + group_id = input('Please enter a group thread id (To test group functionality): ') + user_id = input('Please enter a user thread id (To test kicking/adding functionality): ') print('Logging in...') client = CustomClient(email, password, logging_level=logging_level) # Warning! Taking user input directly like this could be dangerous! Use only for testing purposes! - start_test(client, group_uid, user_uid, argv[1:]) + start_test(client, group_id, user_id, argv[1:])