Added baseline for sphinx documentation and on2FACode event

The docs are still very WIP, but they should be functional. Just
execute `make html` in the docs folder, and you should be able to
navigate to `/docs/_build/html` and view it in your browser
This commit is contained in:
Mads Marquart
2017-05-26 13:38:54 +02:00
parent a76ebbb22a
commit d2741ca419
26 changed files with 1191 additions and 361 deletions

View File

@@ -10,16 +10,16 @@
:license: BSD, see LICENSE for more details.
"""
from datetime import datetime
from .client import *
__copyright__ = 'Copyright 2015 by Taehoon Kim'
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__version__ = '0.10.4'
__license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com'
__source__ = 'https://github.com/carpedm20/fbchat/'
__description__ = 'Facebook Chat (Messenger) for Python'
__all__ = [
'Client',

View File

@@ -1,16 +1,5 @@
# -*- coding: UTF-8 -*-
"""
fbchat
~~~~~~
Facebook Chat (Messenger) for Python
:copyright: (c) 2015 by Taehoon Kim.
:copyright: (c) 2015-2016 by PidgeyL.
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals
import requests
import logging
@@ -24,7 +13,6 @@ from .utils import *
from .models import *
from .event_hook import *
import time
import sys
# Python 2's `input` executes the input, whereas `raw_input` just returns the input
try:
@@ -32,8 +20,6 @@ try:
except NameError:
pass
# Log settings
log = logging.getLogger("client")
log.setLevel(logging.DEBUG)
@@ -45,23 +31,25 @@ log.addHandler(handler)
class Client(object):
"""A client for the Facebook Chat (Messenger).
See http://github.com/carpedm20/fbchat for complete
documentation for the API.
See https://fbchat.readthedocs.io for complete documentation of the API.
"""
def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5,
session_cookies=None, logging_level=logging.INFO, set_default_events=True):
"""A client for the Facebook Chat (Messenger).
"""Initializes and logs in the client
:param email: Facebook `email` or `id` or `phone number`
:param email: Facebook `email`, `id` or `phone number`
:param password: Facebook account password
: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 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 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
:param set_default_events: Specifies whether the default logging.info events should be initialized
:param session_cookies: Cookies from a previous session (Will default to login if these are invalid)
:param logging_level: Configures the `logging level <https://docs.python.org/3/library/logging.html#logging-levels>`_. Defaults to `INFO`
:param set_default_events: Specifies whether the default `logging.info` events should be initialized
:type max_retries: int
:type session_cookies: dict
:type logging_level: int
:type set_default_events: bool
:raises: Exception on failed login
"""
self.sticky, self.pool = (None, None)
@@ -115,6 +103,8 @@ class Client(object):
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..."))
@@ -134,8 +124,8 @@ class Client(object):
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, new_title, changed_for, 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_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:\
@@ -325,7 +315,8 @@ class Client(object):
soup = bs(r.text, "lxml")
data = dict()
s = input('Please enter your 2FA code --> ')
s = self.on2FACode()
data['approvals_code'] = s
data['fb_dtsg'] = soup.find("input", {'name':'fb_dtsg'})['value']
data['nh'] = soup.find("input", {'name':'nh'})['value']
@@ -372,34 +363,68 @@ class Client(object):
r = self._cleanPost(ReqUrl.CHECKPOINT, data)
return r
def _checkRequest(self, r):
if not r.ok:
log.warning('Error when sending request: Got {} response'.format(r.status_code))
return None
j = get_json(r)
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
def isLoggedIn(self):
# Send a request to the login url, to see if we're directed to the home page.
"""
Sends a request to Facebook to check the login status
:return: True if the client is still logged in
:rtype: bool
"""
# Send a request to the login url, to see if we're directed to the home page
r = self._cleanGet(ReqUrl.LOGIN)
return 'home' in r.url
def getSession(self):
"""Returns the session cookies"""
"""Retrieves session cookies
:return: A dictionay containing session cookies
:rtype: dict
"""
return self._session.cookies.get_dict()
def setSession(self, session_cookies):
"""Loads session cookies
:param session_cookies: dictionary containing session cookies
Return false if session_cookies does not contain proper cookies
:param session_cookies: A dictionay containing session cookies
:type session_cookies: dict
:return: False if `session_cookies` does not contain proper cookies
:rtype: bool
"""
# Quick check to see if session_cookies is formatted properly
if not session_cookies or 'c_user' not in session_cookies:
return False
# Load cookies into current session
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
self._postLogin()
return True
def login(self, email, password, max_retries=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
:raises: Exception on failed login
"""
self.onLoggingIn(email=email)
if not (email and password):
raise Exception("Email and password not set.")
@@ -419,6 +444,16 @@ class Client(object):
raise Exception("Login failed. Check email/password. (Failed on url: {})".format(login_url))
def logout(self, timeout=30):
"""
Safely logs out the client
.. todo::
Possibly check return parameter with _checkRequest, and the write documentation about the return
:param timeout: See `requests timeout <http://docs.python-requests.org/en/master/user/advanced/#timeouts>`_
:return:
:rtype:
"""
data = {
'ref': "mb",
'h': self.fb_h
@@ -438,28 +473,26 @@ class Client(object):
self.setDefaultThread(str(recipient_id), thread_type=isUserToThreadType(is_user))
def setDefaultThread(self, thread_id, thread_type):
# type: (str, ThreadType) -> None
"""Sets default thread to send messages and images to.
"""Sets default thread to send messages to
:param thread_id: user/group ID to default to
:param thread_type: type of thread_id
:param thread_id: User/Group ID to default to. See :ref:`intro_thread_id`
:param thread_type: See :ref:`intro_thread_type`
:type thread_type: models.ThreadType
"""
self.default_thread_id = thread_id
self.default_thread_type = thread_type
def resetDefaultThread(self):
# type: () -> None
"""Resets default thread."""
self.default_thread_id = None
self.default_thread_type = None
"""Resets default thread"""
self.setDefaultThread(None, None)
def _getThread(self, given_thread_id, given_thread_type):
# type: (str, ThreadType) -> (str, ThreadType)
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: tuple of thread ID and thread type
: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:
@@ -469,8 +502,17 @@ class Client(object):
else:
return given_thread_id, given_thread_type
"""
GET METHODS
"""
def getAllUsers(self):
""" Gets all users from chat with info included """
"""
Gets all users from chat with info included
:return: :class:`models.User` objects
:rtype: list
"""
data = {
'viewer': self.uid,
@@ -486,7 +528,7 @@ class Client(object):
for k in payload.keys():
try:
user = User.adaptFromChat(payload[k])
user = User._adaptFromChat(payload[k])
except KeyError:
continue
@@ -498,6 +540,8 @@ class Client(object):
"""Find and get user by his/her name
:param name: name of a person
:return: :class:`models.User` objects, ordered by relevance
:rtype: list
"""
payload = {
@@ -518,6 +562,144 @@ class Client(object):
users.append(User(entry))
return users # have bug TypeError: __repr__ returned non-string (type bytes)
def getUserInfo(self, *user_ids):
"""Get user info from id. Unordered.
:param user_ids: One or more user ID(s) to query
:return: A raw dataset containing user information
"""
def fbidStrip(_fbid):
# Stripping of `fbid:` from author_id
if type(_fbid) == int:
return _fbid
if type(_fbid) in [str, bytes] and 'fbid:' in _fbid:
return int(_fbid[5:])
user_ids = [fbidStrip(uid) for uid in user_ids]
data = {
"ids[{}]".format(i): uid for i, uid in enumerate(user_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
def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER):
"""Get the last messages in a thread
:param last_n: Number of messages to retrieve
:param thread_id: User/Group ID to retrieve from. See :ref:`intro_thread_id`
:param thread_type: See :ref:`intro_thread_type`
:type last_n: int
:type thread_type: models.ThreadType
:return: Dictionaries, containing message data
:rtype: list
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
assert last_n > 0, 'length must be positive integer, got %d' % last_n
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()}
r = self._post(ReqUrl.MESSAGES, query=data)
if not r.ok or len(r.text) == 0:
return []
j = get_json(r)
if not j['payload']:
return []
messages = []
for message in j['payload'].get('actions'):
messages.append(Message(**message))
return list(reversed(messages))
def getThreadList(self, start=0, length=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
:return: Dictionaries, containing thread data
:rtype: list
"""
assert length < 21, '`length` is deprecated, max. last 20 threads are returned'
data = {
'client' : self.client,
'inbox[offset]' : start,
'inbox[limit]' : length,
}
r = self._post(ReqUrl.THREADS, data)
if not r.ok or len(r.text) == 0:
return []
j = get_json(r)
# Get names for people
participants = {}
try:
for participant in j['payload']['participants']:
participants[participant["fbid"]] = participant["name"]
except Exception:
log.exception('Exception while getting names for people in getThreadList. {}'.format(j))
# Prevent duplicates in self.threads
threadIDs = [getattr(x, "thread_id") for x in self.threads]
for thread in j['payload']['threads']:
if thread["thread_id"] not in threadIDs:
try:
thread["other_user_name"] = participants[int(thread["other_user_fbid"])]
except:
thread["other_user_name"] = ""
t = Thread(**thread)
self.threads.append(t)
return self.threads
def getUnread(self):
"""
.. todo::
Documenting this
"""
form = {
'client': 'mercury_sync',
'folders[0]': 'inbox',
'last_action_timestamp': now() - 60*1000
# 'last_action_timestamp': 0
}
r = self._post(ReqUrl.THREAD_SYNC, form)
if not r.ok or len(r.text) == 0:
return None
j = get_json(r)
result = {
"message_counts": j['payload']['message_counts'],
"unseen_threads": j['payload']['unseen_thread_ids']
}
return result
"""
END GET METHODS
"""
"""
SEND METHODS
"""
@@ -564,25 +746,12 @@ class Client(object):
return data
def _checkRequest(self, r):
if not r.ok:
log.warning('Error when sending request: Got {} response'.format(r.status_code))
return None
j = get_json(r)
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
def _doSendRequest(self, data):
"""Sends the data to `SendURL`, and returns the message id"""
r = self._post(ReqUrl.SEND, data)
j = self._checkRequest(r)
if j is None:
return None
@@ -615,14 +784,14 @@ class Client(object):
return self.sendMessage(message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user))
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
# type: (str, str, ThreadType) -> list
"""
Sends a message to given (or default, if not) thread with an additional image.
:param message: message to send
:param thread_id: user/group chat ID
:param thread_type: specify whether thread_id is user or group chat
:return: the message id of the message
Sends a message to a thread
:param message: Message to send
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: See :ref:`intro_thread_type`
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent message
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type)
@@ -636,15 +805,16 @@ class Client(object):
return self._doSendRequest(data)
def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER):
# type: (str, EmojiSize, str, ThreadType) -> list
"""
Sends an emoji. If emoji and size are not specified a small like is sent.
:param emoji: the chosen emoji to send. If not specified, default thread emoji is sent
:param size: size of emoji to send
:param thread_id: user/group chat ID
:param thread_type: specify whether thread_id is user or group chat
:return: the message id of the emoji
Sends an emoji to a thread
:param emoji: The chosen emoji to send. If not specified, the thread's default emoji is sent
:param size: If not specified, a small emoji is sent
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: See :ref:`intro_thread_type`
:type size: models.EmojiSize
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent emoji
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type)
@@ -661,8 +831,34 @@ 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
:param image: a tuple of (file name, data, mime type) to upload to facebook
"""
r = self._postFile(ReqUrl.UPLOAD, {
'file': (
image_path,
data,
mimetype
)
})
j = get_json(r)
# Return the image_id
return j['payload']['metadata'][0]['image_id']
def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER):
"""Sends an already uploaded image with the id image_id to the thread"""
"""
Sends an already uploaded image to a thread. (Used by :any:`Client.sendRemoteImage` and :any:`Client.sendLocalImage`)
:param image_id: ID of an image that's already uploaded to Facebook
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: See :ref:`intro_thread_type`
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent image
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type)
@@ -678,15 +874,15 @@ class Client(object):
def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER,
recipient_id=None, is_user=None, image=None):
# type: (str, str, str, ThreadType) -> list
"""
Sends an image from given URL to given (or default, if not) thread.
Sends an image from a URL to a thread
:param image_url: URL of an image to upload and send
:param message: additional message
:param thread_id: user/group chat ID
:param thread_type: specify whether thread_id is user or group chat
:return: the message id of the message
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: See :ref:`intro_thread_type`
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent image
"""
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')
@@ -708,13 +904,14 @@ class Client(object):
recipient_id=None, is_user=None, image=None):
# type: (str, str, str, ThreadType) -> list
"""
Sends an image from given URL to given (or default, if not) thread.
:param image_path: path of an image to upload and send
:param message: additional message
:param thread_id: user/group chat ID
:param thread_type: specify whether thread_id is user or group chat
:return: the message id of the message
Sends a local image to a thread
:param image_path: URL of an image to upload and send
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: See :ref:`intro_thread_type`
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent image
"""
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')
@@ -732,13 +929,13 @@ class Client(object):
return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type)
def addUsersToGroup(self, user_ids, thread_id=None):
# type: (list, str) -> list
"""
Adds users to the given (or default, if not) group.
:param user_ids: list of user ids to add
:param thread_id: group chat ID
:return: the message id of the "message"
Adds users to a group.
:param user_ids: User ids to add
:param thread_id: Group ID to add people to. See :ref:`intro_thread_id`
:type user_ids: list
:return: :ref:`Message ID <intro_message_ids>` of the sent "message"
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = self._getSendData(thread_id, ThreadType.GROUP)
@@ -746,19 +943,26 @@ class Client(object):
data['action_type'] = 'ma-type:log-message'
data['log_message_type'] = 'log:subscribe'
# Make list of users unique
user_ids = set(user_ids)
for i, user_id in enumerate(user_ids):
data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id)
if user_id == self.uid:
log.warning('Error when adding users: Cannot add self to group chat')
if len(user_ids) == 0:
return None
else:
data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id)
return self._doSendRequest(data)
def removeUserFromGroup(self, user_id, thread_id=None):
# type: (str, str) -> bool
"""
Adds users to the given (or default, if not) group.
:param user_id: user ID to remove
:param thread_id: group chat ID
:return: whether the action was successful
Removes users from a group.
:param user_id: User ID to remove
:param thread_id: Group ID to remove people from. See :ref:`intro_thread_id`
:return: :ref:`Message ID <intro_message_ids>` of the sent "message"
"""
thread_id, thread_type = self._getThread(thread_id, None)
@@ -767,7 +971,7 @@ class Client(object):
"uid": user_id,
"tid": thread_id
}
j = self._checkRequest(self._post(ReqUrl.REMOVE_USER, data))
return False if j is None else True
@@ -788,11 +992,14 @@ class Client(object):
def changeGroupTitle(self, title, thread_id=None):
"""
Change title of a group conversation.
:param title: new group chat title
:param thread_id: group chat ID
:return: the message id of the "message"
Changes title of a group conversation.
.. todo::
Check whether this can work on group threads, and if it does, change it (back) to changeThreadTitle
:param title: New group chat title
:param thread_id: Group ID to change title of. See :ref:`intro_thread_id`
:return: :ref:`Message ID <intro_message_ids>` of the sent "message"
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = self._getSendData(thread_id, ThreadType.GROUP)
@@ -803,19 +1010,19 @@ class Client(object):
return self._doSendRequest(data)
def changeThreadColor(self, new_color, thread_id=None):
# type: (ThreadColor, str, ThreadType) -> bool
def changeThreadColor(self, color, thread_id=None):
"""
Changes thread color to specified color. For more info about color names - see wiki.
:param new_color: new color name
:param thread_id: user/group chat ID
:return: whether the action was successful
Changes thread color
:param color: New thread color
:param thread_id: User/Group ID to change color of. See :ref:`intro_thread_id`
:type color: models.ThreadColor
:return: (*bool*) True if the action was successful
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {
"color_choice": new_color.value,
"color_choice": color.value,
"thread_or_other_fbid": thread_id
}
@@ -824,13 +1031,14 @@ class Client(object):
return False if j is None else True
def reactToMessage(self, message_id, reaction):
# type: (str, MessageReaction) -> bool
"""
Reacts to a message.
:param message_id: message ID to react to
:param reaction: reaction emoji to send
:return: whether the action was successful
:param message_id: :ref:`Message ID <intro_message_ids>` to react to
:param reaction: Reaction emoji to use
:type reaction: models.MessageReaction
:return: True if the action was successful
:rtype: bool
"""
full_data = {
"doc_id": 1491398900900362,
@@ -852,20 +1060,22 @@ class Client(object):
url_part = urllib.urlencode(full_data)\
.replace('u%27', '%27')\
.replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1])
j = self._checkRequest(self._post('{}/?{}'.format(ReqUrl.MESSAGE_REACTION, url_part)))
return False if j is None else True
def setTypingStatus(self, status, thread_id=None, thread_type=None):
# type: (TypingStatus, str, ThreadType) -> bool
"""
Sets users typing status.
:param status: specify whether the status is typing or not (TypingStatus)
:param thread_id: user/group chat ID
:param thread_type: specify whether thread_id is user or group chat
:return: True if status changed
Sets users typing status in a thread
:param status: Specify the typing status
:param thread_id: User/Group ID to change status in. See :ref:`intro_thread_id`
:param thread_type: See :ref:`intro_thread_type`
:type status: models.TypingStatus
:type thread_type: models.ThreadType
:return: True if the action was successful
:rtype: bool
"""
thread_id, thread_type = self._getThread(thread_id, None)
@@ -881,126 +1091,14 @@ class Client(object):
return False if j is None else True
"""
END SEND METHODS
END SEND METHODS
"""
def _uploadImage(self, image_path, data, mimetype):
"""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, {
'file': (
image_path,
data,
mimetype
)
})
j = get_json(r)
# Return the image_id
return j['payload']['metadata'][0]['image_id']
def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER):
# type: (int, str, ThreadType) -> list
"""Get the info of one Thread
:param last_n: number of retrieved messages from start (default 20)
:param thread_id: user/group chat ID
:param thread_type: specify whether thread_id is user or group chat
:return: a list of messages
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
assert last_n > 0, 'length must be positive integer, got %d' % last_n
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()}
r = self._post(ReqUrl.MESSAGES, query=data)
if not r.ok or len(r.text) == 0:
return []
j = get_json(r)
if not j['payload']:
return []
messages = []
for message in j['payload'].get('actions'):
messages.append(Message(**message))
return list(reversed(messages))
def getThreadList(self, start, length=20):
# type: (int, int) -> list
"""Get thread list of your facebook account.
:param start: the start index of a thread
:param length: (optional) the length of a thread
"""
assert length < 21, '`length` is deprecated, max. last 20 threads are returned'
data = {
'client' : self.client,
'inbox[offset]' : start,
'inbox[limit]' : length,
}
r = self._post(ReqUrl.THREADS, data)
if not r.ok or len(r.text) == 0:
return []
j = get_json(r)
# Get names for people
participants = {}
try:
for participant in j['payload']['participants']:
participants[participant["fbid"]] = participant["name"]
except Exception:
log.exception('Exception while getting names for people in getThreadList. {}'.format(j))
# Prevent duplicates in self.threads
threadIDs = [getattr(x, "thread_id") for x in self.threads]
for thread in j['payload']['threads']:
if thread["thread_id"] not in threadIDs:
try:
thread["other_user_name"] = participants[int(thread["other_user_fbid"])]
except:
thread["other_user_name"] = ""
t = Thread(**thread)
self.threads.append(t)
return self.threads
def getUnread(self):
form = {
'client': 'mercury_sync',
'folders[0]': 'inbox',
'last_action_timestamp': now() - 60*1000
# 'last_action_timestamp': 0
}
r = self._post(ReqUrl.THREAD_SYNC, form)
if not r.ok or len(r.text) == 0:
return None
j = get_json(r)
result = {
"message_counts": j['payload']['message_counts'],
"unseen_threads": j['payload']['unseen_thread_ids']
}
return result
def markAsDelivered(self, userID, threadID):
"""
.. todo::
Documenting this
"""
data = {
"message_ids[0]": threadID,
"thread_ids[%s][0]" % userID: threadID
@@ -1010,6 +1108,10 @@ class Client(object):
return r.ok
def markAsRead(self, userID):
"""
.. todo::
Documenting this
"""
data = {
"watermarkTimestamp": now(),
"shouldSendReadReceipt": True,
@@ -1020,6 +1122,10 @@ class Client(object):
return r.ok
def markAsSeen(self):
"""
.. todo::
Documenting this
"""
r = self._post(ReqUrl.MARK_SEEN, {"seen_timestamp": 0})
return r.ok
@@ -1028,7 +1134,10 @@ class Client(object):
return self.friendConnect(friend_id)
def friendConnect(self, friend_id):
# type: (str) -> bool
"""
.. todo::
Documenting this
"""
data = {
"to_friend": friend_id,
"action": "confirm"
@@ -1038,6 +1147,10 @@ class Client(object):
return r.ok
def ping(self, sticky):
"""
.. todo::
Documenting this
"""
data = {
'channel': self.user_channel,
'clientid': self.client_id,
@@ -1138,7 +1251,7 @@ class Client(object):
# Color change
elif delta_type == "change_thread_theme":
new_color = delta["untypedData"]["theme_color"]
new_color = ThreadColor(delta["untypedData"]["theme_color"])
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id,
thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
@@ -1163,10 +1276,10 @@ class Client(object):
# Nickname change
elif delta_type == "change_thread_nickname":
changed_for = str(delta["untypedData"]["participant_id"])
new_title = delta["untypedData"]["nickname"]
new_nickname = delta["untypedData"]["nickname"]
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for,
new_title=new_title,
new_nickname=new_nickname,
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata)
continue
@@ -1263,10 +1376,15 @@ class Client(object):
return self.doOneListen(markAlive)
def doOneListen(self, markAlive=True):
# type: (bool) -> None
"""Does one cycle of the listening loop.
This method is only useful if you want to control fbchat from an
external event loop."""
"""
Does one cycle of the listening loop.
This method is useful if you want to control fbchat from an external event loop
:param markAlive: Whether this should ping the Facebook server before running
:type markAlive: bool
:return: Whether the loop should keep running
:rtype: bool
"""
try:
if markAlive: self.ping(self.sticky)
try:
@@ -1275,11 +1393,13 @@ class Client(object):
except requests.exceptions.RequestException as e:
pass
except KeyboardInterrupt:
self.listening = False
return False
except requests.exceptions.Timeout:
pass
except Exception as e:
self.onListenError(e)
return self.onListenError(e)
return True
@deprecated(deprecated_in='0.10.2', removed_in='0.15.0', details='Use stopListening() instead')
@@ -1287,43 +1407,22 @@ class Client(object):
return self.stopListening()
def stopListening(self):
"""Cleans up the variables from start_listening."""
"""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
:param markAlive: Whether this should ping the Facebook server each time the loop runs
:type markAlive: bool
"""
self.startListening()
self.onListening()
while self.listening:
self.doOneListen(markAlive)
while self.listening and self.doOneListen(markAlive):
pass
self.stopListening()
def getUserInfo(self, *user_ids):
"""Get user info from id. Unordered.
:param user_ids: one or more user id(s) to query
"""
def fbidStrip(_fbid):
# Stripping of `fbid:` from author_id
if type(_fbid) == int:
return _fbid
if type(_fbid) in [str, bytes] and 'fbid:' in _fbid:
return int(_fbid[5:])
user_ids = [fbidStrip(uid) for uid in user_ids]
data = {"ids[{}]".format(i):uid for i,uid in enumerate(user_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

View File

@@ -1,11 +1,28 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import sys
from enum import Enum
import enum
class User(object):
"""Represents a Facebook User"""
#: The unique identifier of the user. Can be used a `thread_id`. See :ref:`intro_thread_id` for more info
uid = None
#: Currently always set to `user`. Might change in the future
type = 'user'
#: The profile picture of the user
photo = None
#: The profile url
url = None
#: The name of the user
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
class User:
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']
@@ -13,21 +30,21 @@ class User:
self.photo = data['photo']
self.url = data['path']
self.name = data['text']
self.score = data['score']
self.score = float(data['score'])
self.data = data
def __repr__(self):
uni = self.__unicode__()
return uni.encode('utf-8') if sys.version_info < (3, 0) else uni
return self.__unicode__()
def __unicode__(self):
return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url)
@staticmethod
def adaptFromChat(user_in_chat):
""" Adapts user info from chat to User model acceptable initial dict
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
'dir': None,
'mThumbSrcSmall': None,
@@ -53,42 +70,42 @@ class User:
'photo': user_in_chat['thumbSrc'],
'path': user_in_chat['uri'],
'text': user_in_chat['name'],
'score': '',
'score': 1,
'data': user_in_chat,
}
class Thread:
class Thread(object):
def __init__(self, **entries):
self.__dict__.update(entries)
class Message:
class Message(object):
def __init__(self, **entries):
self.__dict__.update(entries)
class Enum(enum.Enum):
"""Used internally by fbchat to support enumerations"""
def __repr__(self):
# For documentation:
return '{}.{}'.format(type(self).__name__, self.name)
class ThreadType(Enum):
"""Used to specify what type of Facebook thread is being used. See :ref:`intro_thread_type` for more info"""
USER = 1
GROUP = 2
class TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing"""
STOPPED = 0
TYPING = 1
class EmojiSize(Enum):
"""Used to specify the size of a sent emoji"""
LARGE = '369239383222810'
MEDIUM = '369239343222814'
SMALL = '369239263222822'
LIKES = {
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
LIKES['large'] = LIKES['l']
LIKES['medium'] =LIKES['m']
LIKES['small'] = LIKES['s']
class ThreadColor(Enum):
"""Used to specify a thread colors"""
MESSENGER_BLUE = ''
VIKING = '#44bec7'
GOLDEN_POPPY = '#ffc300'
@@ -106,6 +123,7 @@ class ThreadColor(Enum):
BILOBA_FLOWER = '#a695c7'
class MessageReaction(Enum):
"""Used to specify a message reaction"""
LOVE = '😍'
SMILE = '😆'
WOW = '😮'
@@ -114,6 +132,15 @@ class MessageReaction(Enum):
YES = '👍'
NO = '👎'
LIKES = {
'large': EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM,
'small': EmojiSize.SMALL,
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
MessageReactionFix = {
'😍': ('0001f60d', '%F0%9F%98%8D'),
'😆': ('0001f606', '%F0%9F%98%86'),

View File

@@ -8,6 +8,7 @@ from random import random
import warnings
from .models import *
#: 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",
"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",
@@ -16,6 +17,7 @@ USER_AGENTS = [
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"
]
GENDERS = {
0: 'unknown',
1: 'female_singular',
@@ -31,7 +33,8 @@ GENDERS = {
11: 'unknown_plural',
}
class ReqUrl:
class ReqUrl(object):
"""A class containing all urls used by `fbchat`"""
SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
SEND = "https://www.facebook.com/messaging/send/"
@@ -68,14 +71,14 @@ def strip_to_json(text):
except ValueError as e:
return None
def get_dencoded(r):
def get_decoded(r):
if not isinstance(r._content, str):
return r._content.decode(facebookEncoding)
else:
return r._content
def get_json(r):
return json.loads(strip_to_json(get_dencoded(r)))
return json.loads(strip_to_json(get_decoded(r)))
def digit_to_char(digit):
if digit < 10:
@@ -112,9 +115,7 @@ def raise_exception(e):
raise e
def deprecation(name, deprecated_in=None, removed_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.
"""
"""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)
@@ -122,15 +123,13 @@ def deprecation(name, deprecated_in=None, removed_in=None, details='', stackleve
warning += ' and will be removed in v. {}'.format(removed_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, removed_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.
"""
"""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)