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

@@ -5,7 +5,7 @@ fbchat
Facebook Chat (`Messenger <https://www.messenger.com/>`__) for Python. This project was inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__.
**No XMPP or API key is needed**. Just use your ID and PASSWORD.
**No XMPP or API key is needed**. Just use your EMAIL and PASSWORD.
Installation
@@ -18,21 +18,20 @@ Simple:
$ pip install fbchat
Example
=======
Example Login
=============
.. code-block:: python
import fbchat
client = fbchat.Client("YOUR_ID", "YOUR_PASSWORD")
client = fbchat.Client('YOUR_EMAIL', 'YOUR_PASSWORD')
Sending a Message
=================
.. code-block:: python
friends = client.getUsers("FRIEND'S NAME") # return a list of names
friend = friends[0]
sent = client.send(friend.uid, "Your Message")

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python3.6 -msphinx
SPHINXPROJ = fbchat
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

1
docs/_static/license.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="80" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="80" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h47v20H0z"/><path fill="#007ec6" d="M47 0h33v20H47z"/><path fill="url(#b)" d="M0 0h80v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="23.5" y="15" fill="#010101" fill-opacity=".3">license</text><text x="23.5" y="14">license</text><text x="62.5" y="15" fill="#010101" fill-opacity=".3">BSD</text><text x="62.5" y="14">BSD</text></g></svg>

After

Width:  |  Height:  |  Size: 791 B

1
docs/_static/python-versions.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="154" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="154" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h49v20H0z"/><path fill="#007ec6" d="M49 0h105v20H49z"/><path fill="url(#b)" d="M0 0h154v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="24.5" y="15" fill="#010101" fill-opacity=".3">python</text><text x="24.5" y="14">python</text><text x="100.5" y="15" fill="#010101" fill-opacity=".3">2.7, 3.4, 3.5, 3.6</text><text x="100.5" y="14">2.7, 3.4, 3.5, 3.6</text></g></svg>

After

Width:  |  Height:  |  Size: 825 B

26
docs/_templates/layout.html vendored Normal file
View File

@@ -0,0 +1,26 @@
{% extends '!layout.html' %}
{% block extrahead %}
<script async defer src="https://buttons.github.io/buttons.js"></script>
<!-- Alabaster (krTheme++) Hacks, modified version of Kenneth Reitz' https://github.com/kennethreitz/requests/blob/master/docs/_templates/hacks.html -->
<style type="text/css">
/* Rezzy requires precise alignment. */
img.logo {margin-left: -20px!important;}
/* "Quick Search" should be capitalized. */
div#searchbox h3 {text-transform: capitalize;}
/* Go button should be behind input field */
div.sphinxsidebar div#searchbox input[type="text"] {width: 160px}
div#searchbox form div {display: inline-block;}
/* Make the document a little wider, less code is cut-off. */
div.document {width: 1008px;}
/* Much-improved spacing around code blocks. */
div.highlight pre {padding: 11px 14px;}
/* Remain Responsive! */
@media screen and (max-width: 1008px) {
div.sphinxsidebar {display: none;}
div.document {width: 100%!important;}
/* Have code blocks escape the document right-margin. */
div.highlight pre {margin-right: -30px;}
}
</style>
{% endblock %}

13
docs/_templates/sidebar.html vendored Normal file
View File

@@ -0,0 +1,13 @@
<h3>
<a href="{{ pathto(master_doc) }}">{{ _(project) }}</a>
</h3>
<p>
<a class="github-button" href="https://github.com/carpedm20/fbchat" data-size="large" data-show-count="true" aria-label="Star carpedm20/fbchat on GitHub">Star</a>
</p>
<p>
{{ _(shorttitle) }}
</p>
{{ toctree() }}

46
docs/api.rst Normal file
View File

@@ -0,0 +1,46 @@
.. module:: fbchat
.. highlight:: python
.. _api:
Full API
========
If you are looking for information on a specific function, class, or method, this part of the documentation is for you.
.. _api_client:
Client
------
.. todo::
Write introduction text about `Client`, and add documentation for all events
.. autoclass:: Client(email, password, user_agent=None, max_retries=5, session_cookies=None, logging_level=logging.INFO, set_default_events=True)
: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:
Models
------
These models are used in various functions, both as inputs and return values.
A good tip is to write ``from fbchat.models import *`` at the start of your source, so you can use these models freely
.. automodule:: fbchat.models
:members:
.. _api_utils:
Utils
-----
These functions and values are used internally by fbchat, and are subject to change. Do **NOT** rely on these to be backwards compatible!
.. automodule:: fbchat.utils
:members:

191
docs/conf.py Normal file
View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# fbchat documentation build configuration file, created by
# sphinx-quickstart on Thu May 25 15:43:01 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
import fbchat
import tests
from fbchat import __copyright__, __author__, __version__, __description__
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.viewcode'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'fbchat'
title = 'fbchat Documentation'
copyright = __copyright__
author = __author__
description = __description__
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = __version__
# The full version, including alpha/beta/rc tags.
release = __version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = project + 'doc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, project + '.tex', title,
author, 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, project, title,
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, project, title,
author, project, description,
'Miscellaneous'),
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/3/': None}
add_function_parentheses = False
html_theme_options = {
'show_powered_by': False,
'github_user': 'carpedm20',
'github_repo': project,
'github_banner': True,
'show_related': False
}
html_sidebars = {
'**': ['sidebar.html', 'searchbox.html']
}
html_show_sphinx = False
html_show_sourcelink = False
autoclass_content = 'init'
html_short_title = description

52
docs/examples.rst Normal file
View File

@@ -0,0 +1,52 @@
.. highlight:: python
.. _examples:
Examples
========
These are a few examples on how to use `fbchat`. Remember to swap out `<email>` and `<password>` for your email and password
Sending messages
----------------
This will send one of each message type to the specified thread
.. literalinclude:: ../examples/send.py
:language: python
Getting information
-------------------
This will show the different ways of fetching information about users and threads
.. literalinclude:: ../examples/get.py
:language: python
Echobot
-------
This will reply to any message with the same message
.. literalinclude:: ../examples/echobot.py
:language: python
Remove bot
----------
This will remove a user from a group if they write the message `Remove me!`
.. literalinclude:: ../examples/removebot.py
:language: python
"Keep it"-bot
-------------
This will prevent chat color, emoji, nicknames and chat name from being changed. It will also prevent people from being added and removed
.. literalinclude:: ../examples/keepbot.py
:language: python

51
docs/index.rst Normal file
View File

@@ -0,0 +1,51 @@
.. highlight:: python
.. fbchat documentation master file, created by
sphinx-quickstart on Thu May 25 15:43:01 2017.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. This documentation's layout is heavily inspired by requests' layout: https://requests.readthedocs.io
Some documentation is also partially copied from facebook-chat-api: https://github.com/Schmavery/facebook-chat-api
fbchat: Facebook Chat (Messenger) for Python
============================================
Release v\ |version|. (:ref:`install`)
.. generated with: https://img.shields.io/badge/license-BSD-blue.svg
.. image:: /_static/license.svg
:target: https://github.com/carpedm20/fbchat/blob/master/LICENSE.txt
.. generated with: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg
.. image:: /_static/python-versions.svg
:target: https://pypi.python.org/pypi/fbchat
Facebook Chat (`Messenger <https://www.facebook.com/messages/>`__) for Python. This project was inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__.
**No XMPP or API key is needed**. Just use your email and password.
Currently `fbchat` support Python 2.7, 3.4, 3.5 and 3.6:
`fbchat` works by emulating the browser. This means doing the exact same GET/POST requests and tricking Facebook into thinking we're accessing the website normally.
Because we're doing it this way, this API requires the credentials of a Facebook account.
.. 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, sending spammy looking URLs, logging in and out very quickly... Be responsible Facebook citizens.
.. note::
Facebook now has an `official API <https://developers.facebook.com/docs/messenger-platform>`_ for chat bots, so if you're familiar with node.js, this might be what you're looking for.
Overview
--------
.. toctree::
:maxdepth: 2
install
intro
examples
testing
api
todo

36
docs/install.rst Normal file
View File

@@ -0,0 +1,36 @@
.. highlight:: sh
.. _install:
Installation
============
Pip Install fbchat
------------------
To install fbchat, run this command::
$ pip install fbchat
If you don't have `pip <https://pip.pypa.io>`_ installed,
`this Python installation guide <http://docs.python-guide.org/en/latest/starting/installation/>`_
can guide you through the process.
Get the Source Code
-------------------
fbchat is developed on GitHub, where the code is
`always available <https://github.com/carpedm20/fbchat>`_.
You can either clone the public repository::
$ git clone git://github.com/carpedm20/fbchat.git
Or, download a `tarball <https://github.com/carpedm20/fbchat/tarball/master>`_::
$ curl -OL https://github.com/carpedm20/fbchat/tarball/master
# optionally, zipball is also available (for Windows users).
Once you have a copy of the source, you can embed it in your own Python
package, or install it into your site-packages easily::
$ python setup.py install

86
docs/intro.rst Normal file
View File

@@ -0,0 +1,86 @@
.. highlight:: python
.. _intro:
Introduction
============
.. todo::
Make a general introduction to `fbchat`
.. _intro_logging_in:
Logging in
----------
.. todo::
Write something about logging in, logging out, checking login, 2FA and the event `on2FACode`, here
.. _intro_sessions:
Sessions
--------
.. todo::
Make an introduction to and show example usage of sessions
.. _intro_sending:
Sending messages
----------------
.. todo::
Make an introduction to and show example usage of how you send information
.. literalinclude:: ../examples/send.py
:language: python
.. _intro_fetching:
Fetching information
--------------------
.. todo::
Make an introduction to and show example usage of fetching information
.. literalinclude:: ../examples/get.py
:language: python
.. _intro_thread_id:
Thread ids
----------
.. todo::
Make an introduction to and show example usage of thread ids
.. _intro_thread_type:
Thread types
------------
.. todo::
Make an introduction to and show example usage of thread types
.. _intro_message_ids:
Message ids
-----------
.. todo::
Make an introduction to and show example usage of message ids
.. _intro_events:
Events
------
.. todo::
Make an introduction to and show example usage of the event system

36
docs/make.bat Normal file
View File

@@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=python3.6 -msphinx
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=fbchat
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The Sphinx module was not found. Make sure you have Sphinx installed,
echo.then set the SPHINXBUILD environment variable to point to the full
echo.path of the 'sphinx-build' executable. Alternatively you may add the
echo.Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

28
docs/testing.rst Normal file
View File

@@ -0,0 +1,28 @@
.. highlight:: sh
.. _testing:
Testing
=======
To use these tests copy ``tests/data.json`` to ``tests/my_data.json`` or type the information manually in the terminal prompts.
- email: Your (or a test user's) email / phone number
- password: Your (or a test user's) password
- group_thread_id: A test group that will be used to test group functionality
- user_thread_id: A person that will be used to test kick/add functionality (This user should be in the group)
Please remember to test all supported python versions.
If you've made any changes to the 2FA functionality, test it with a 2FA enabled account.
If you only want to execute specific tests, pass the function names in the commandline (not including the `test_` prefix). Example::
$ python tests.py sendMessage sessions sendEmoji
.. warning::
Do not execute the full set of tests in too quick succession. This can get your account temporarily blocked for spam!
(You should execute the script at max about 10 times a day)
.. automodule:: tests
:members: TestFbchat
:undoc-members: TestFbchat

22
docs/todo.rst Normal file
View File

@@ -0,0 +1,22 @@
.. highlight:: python
.. _todo:
Todo
====
Functionality
-------------
- Implement Client.changeThreadEmoji
- Implement Client.changeNickname
- Implement Client.searchForThread
- This will use the graphql request API
- Implement Client.searchForMessage
- This will use the graphql request API
Documentation
-------------
.. todolist::

18
examples/echobot.py Normal file
View File

@@ -0,0 +1,18 @@
# -*- coding: UTF-8 -*-
from fbchat import log, Client
# Subclass fbchat.Client and override required methods
class EchoBot(Client):
def onMessage(self, mid, author_id, message, thread_id, thread_type, ts, metadata, msg):
self.markAsDelivered(author_id, thread_id)
self.markAsRead(author_id)
log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message)))
# If you're not the author, echo
if author_id != self.uid:
self.sendMessage(message, thread_id=thread_id, thread_type=thread_type)
client = EchoBot("<email>", "<password>")
client.listen()

8
examples/get.py Normal file
View File

@@ -0,0 +1,8 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client("<email>", "<password>")
# This should show example usage of getAllUsers, getUsers, getUserInfo, getThreadInfo, getThreadList and getUnread

56
examples/keepbot.py Normal file
View File

@@ -0,0 +1,56 @@
# -*- coding: UTF-8 -*-
from fbchat import log, Client
from fbchat.models import *
# Change this to your group id
old_thread_id = '1234567890'
# Change these to match your liking
old_color = ThreadColor.MESSENGER_BLUE
old_emoji = '👍'
old_title = 'Old school'
old_nicknames = {
'12345678901': 'Old School user nr. 1',
'12345678902': 'Old School user nr. 2',
'12345678903': 'Old School user nr. 3',
'12345678904': 'Old School user nr. 4'
}
class KeepBot(Client):
def onColorChange(self, mid, author_id, new_color, thread_id, thread_type, ts, metadata, msg):
if old_thread_id == thread_id and old_color != new_color:
log.info("{} changed the thread color. It will be changed back".format(author_id))
self.changeThreadColor(old_color, thread_id=thread_id)
def onEmojiChange(self, mid, author_id, new_emoji, thread_id, thread_type, ts, metadata, msg):
if old_thread_id == thread_id and new_emoji != old_emoji:
log.info("{} changed the thread emoji. It will be changed back".format(author_id))
# Not currently possible in `fbchat`
# self.changeThreadEmoji(old_emoji, thread_id=thread_id)
def onPeopleAdded(self, added_ids, author_id, thread_id, msg):
if old_thread_id == thread_id and author_id != self.uid:
log.info("{} got added. They will be removed".format(added_ids))
for added_id in added_ids:
self.removeUserFromGroup(added_id, thread_id=thread_id)
def onPersonRemoved(self, removed_id, author_id, thread_id, msg):
# No point in trying to add ourself
if old_thread_id == thread_id and removed_id != self.uid and author_id != self.uid:
log.info("{} got removed. They will be re-added".format(removed_id))
self.addUsersToGroup(removed_id, thread_id=thread_id)
def onTitleChange(self, mid, author_id, new_title, thread_id, thread_type, ts, metadata, msg):
if old_thread_id == thread_id and old_title != new_title:
log.info("{} changed the thread title. It will be changed back".format(author_id))
self.changeGroupTitle(old_title, thread_id=thread_id)
def onNicknameChange(self, mid, author_id, changed_for, new_nickname, thread_id, thread_type, ts, metadata, msg):
if old_thread_id == thread_id and changed_for in old_nicknames:
log.info("{} changed {}'s' nickname. It will be changed back".format(author_id, changed_for))
# Not currently possible in `fbchat`
# self.changeNickname(old_nicknames[changed_for], changed_for, thread_id=thread_id, thread_type=thread_type)
client = KeepBot("<email>", "<password>")
client.listen()

16
examples/removebot.py Normal file
View File

@@ -0,0 +1,16 @@
# -*- coding: UTF-8 -*-
from fbchat import log, Client
from fbchat.models import *
class RemoveBot(Client):
def onMessage(self, mid, author_id, message, thread_id, thread_type, ts, metadata, msg):
# 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)))
self.removeUserFromGroup(user_id, thread_id=thread_id)
else:
log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message)))
client = RemoveBot("<email>", "<password>")
client.listen()

25
examples/send.py Normal file
View File

@@ -0,0 +1,25 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client("<email>", "<password>")
# Change these to match your thread
thread_id = '1234567890'
thread_type = ThreadType.GROUP # Or ThreadType.USER
# This will send a message to the thread
client.sendMessage('Hey there', thread_id=thread_id, thread_type=thread_type)
# This will send the default emoji
client.sendEmoji(emoji=None, size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type)
# This will send the emoji `👍`
client.sendEmoji(emoji='👍', size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type)
# This will send the image called `image.png`
client.sendLocalImage('image.png', message='This is a local image', thread_id=thread_id, thread_type=thread_type)
# This will send the image at the url `https://example.com/image.png`
client.sendRemoteImage('https://example.com/image.png', message='This is a remote image', thread_id=thread_id, thread_type=thread_type)

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)

View File

@@ -21,6 +21,7 @@ version = None
author = None
email = None
source = None
description = None
with open(os.path.join('fbchat', '__init__.py')) as f:
for line in f:
if line.strip().startswith('__version__'):
@@ -31,7 +32,9 @@ with open(os.path.join('fbchat', '__init__.py')) as f:
email = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif line.strip().startswith('__source__'):
source = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif None not in (version, author, email, source):
elif line.strip().startswith('__description__'):
description = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif None not in (version, author, email, source, description):
break
setup(
@@ -40,7 +43,7 @@ setup(
author_email=email,
license='BSD License',
keywords=["facebook chat fbchat"],
description="Facebook Chat (Messenger) for Python",
description=description,
long_description=readme_content,
classifiers=[
'Development Status :: 2 - Pre-Alpha',

View File

@@ -13,36 +13,7 @@ from fbchat.models import *
logging_level = logging.ERROR
"""
Tests for fbchat
~~~~~~~~~~~~~~~~
To use these tests copy `tests/data.json` to `tests/my_data.json` or type this information manually in the terminal prompts.
- email: Your (or a test user's) email / phone number
- password: Your (or a test user's) password
- group_thread_id: A test group that will be used to test group functionality
- user_thread_id: A person that will be used to test kick/add functionality (This user should be in the group)
Please remember to test both python v. 2.7 and python v. 3.6!
If you've made any changes to the 2FA functionality, test it with a 2FA enabled account
If you only want to execute specific tests, pass the function names in the commandline
WARNING:
Do not execute the full set of tests in too quick succession. This can get you temporarily blocked for spam!
(You should execute the script at max about 10 times a day)
"""
class TestFbchat(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_loginFunctions(self):
self.assertTrue(client.isLoggedIn())
@@ -51,7 +22,7 @@ class TestFbchat(unittest.TestCase):
self.assertFalse(client.isLoggedIn())
with self.assertRaises(Exception):
client.login('not@email.com', 'not_password', max_retries=1)
client.login('<email>', '<password>', max_retries=1)
client.login(email, password)