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>`__. 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 Installation
@@ -18,21 +18,20 @@ Simple:
$ pip install fbchat $ pip install fbchat
Example Example Login
======= =============
.. code-block:: python .. code-block:: python
import fbchat import fbchat
client = fbchat.Client("YOUR_ID", "YOUR_PASSWORD") client = fbchat.Client('YOUR_EMAIL', 'YOUR_PASSWORD')
Sending a Message Sending a Message
================= =================
.. code-block:: python .. code-block:: python
friends = client.getUsers("FRIEND'S NAME") # return a list of names friends = client.getUsers("FRIEND'S NAME") # return a list of names
friend = friends[0] friend = friends[0]
sent = client.send(friend.uid, "Your Message") 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. :license: BSD, see LICENSE for more details.
""" """
from datetime import datetime
from .client import * from .client import *
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__copyright__ = 'Copyright 2015 by Taehoon Kim'
__version__ = '0.10.4' __version__ = '0.10.4'
__license__ = 'BSD' __license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com' __email__ = 'carpedm20@gmail.com'
__source__ = 'https://github.com/carpedm20/fbchat/' __source__ = 'https://github.com/carpedm20/fbchat/'
__description__ = 'Facebook Chat (Messenger) for Python'
__all__ = [ __all__ = [
'Client', 'Client',

View File

@@ -1,16 +1,5 @@
# -*- coding: UTF-8 -*- # -*- 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 from __future__ import unicode_literals
import requests import requests
import logging import logging
@@ -24,7 +13,6 @@ from .utils import *
from .models import * from .models import *
from .event_hook import * from .event_hook import *
import time import time
import sys
# Python 2's `input` executes the input, whereas `raw_input` just returns the input # Python 2's `input` executes the input, whereas `raw_input` just returns the input
try: try:
@@ -32,8 +20,6 @@ try:
except NameError: except NameError:
pass pass
# Log settings # Log settings
log = logging.getLogger("client") log = logging.getLogger("client")
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)
@@ -45,23 +31,25 @@ log.addHandler(handler)
class Client(object): class Client(object):
"""A client for the Facebook Chat (Messenger). """A client for the Facebook Chat (Messenger).
See http://github.com/carpedm20/fbchat for complete See https://fbchat.readthedocs.io for complete documentation of the API.
documentation for the API.
""" """
def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5, 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): 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 password: Facebook account password
:param debug: Configures the logger to `debug` 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 :any:`utils.USER_AGENTS`)
:param info_log: Configures the logger to `info` logging_level (deprecated)
:param user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list (see utils.py)
:param max_retries: Maximum number of times to retry login :param 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 session_cookies: Cookies from a previous session (Will default to login if these are invalid)
:param logging_level: Configures the logger to logging_level :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 :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) self.sticky, self.pool = (None, None)
@@ -115,6 +103,8 @@ class Client(object):
def _setupEventHooks(self): def _setupEventHooks(self):
self._setEventHook('onLoggingIn', lambda email: log.info("Logging in {}...".format(email))) 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('onLoggedIn', lambda email: log.info("Login of {} successful.".format(email)))
self._setEventHook('onListening', lambda: log.info("Listening...")) 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:\ 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))) 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:\ 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_title))) 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:\ 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") soup = bs(r.text, "lxml")
data = dict() data = dict()
s = input('Please enter your 2FA code --> ') s = self.on2FACode()
data['approvals_code'] = s data['approvals_code'] = s
data['fb_dtsg'] = soup.find("input", {'name':'fb_dtsg'})['value'] data['fb_dtsg'] = soup.find("input", {'name':'fb_dtsg'})['value']
data['nh'] = soup.find("input", {'name':'nh'})['value'] data['nh'] = soup.find("input", {'name':'nh'})['value']
@@ -372,34 +363,68 @@ class Client(object):
r = self._cleanPost(ReqUrl.CHECKPOINT, data) r = self._cleanPost(ReqUrl.CHECKPOINT, data)
return r 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): 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) r = self._cleanGet(ReqUrl.LOGIN)
return 'home' in r.url return 'home' in r.url
def getSession(self): def getSession(self):
"""Returns the session cookies""" """Retrieves session cookies
:return: A dictionay containing session cookies
:rtype: dict
"""
return self._session.cookies.get_dict() return self._session.cookies.get_dict()
def setSession(self, session_cookies): def setSession(self, session_cookies):
"""Loads session cookies """Loads session cookies
:param session_cookies: dictionary containing session cookies :param session_cookies: A dictionay containing session cookies
Return false if session_cookies does not contain proper 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 # Quick check to see if session_cookies is formatted properly
if not session_cookies or 'c_user' not in session_cookies: if not session_cookies or 'c_user' not in session_cookies:
return False return False
# Load cookies into current session # Load cookies into current session
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies) self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
self._postLogin() self._postLogin()
return True return True
def login(self, email, password, max_retries=5): 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) self.onLoggingIn(email=email)
if not (email and password): if not (email and password):
raise Exception("Email and password not set.") 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)) raise Exception("Login failed. Check email/password. (Failed on url: {})".format(login_url))
def logout(self, timeout=30): 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 = { data = {
'ref': "mb", 'ref': "mb",
'h': self.fb_h 'h': self.fb_h
@@ -438,28 +473,26 @@ class Client(object):
self.setDefaultThread(str(recipient_id), thread_type=isUserToThreadType(is_user)) self.setDefaultThread(str(recipient_id), thread_type=isUserToThreadType(is_user))
def setDefaultThread(self, thread_id, thread_type): def setDefaultThread(self, thread_id, thread_type):
# type: (str, ThreadType) -> None """Sets default thread to send messages to
"""Sets default thread to send messages and images to.
:param thread_id: user/group ID to default to :param thread_id: User/Group ID to default to. See :ref:`intro_thread_id`
:param thread_type: type of thread_id :param thread_type: See :ref:`intro_thread_type`
:type thread_type: models.ThreadType
""" """
self.default_thread_id = thread_id self.default_thread_id = thread_id
self.default_thread_type = thread_type self.default_thread_type = thread_type
def resetDefaultThread(self): def resetDefaultThread(self):
# type: () -> None """Resets default thread"""
"""Resets default thread.""" self.setDefaultThread(None, None)
self.default_thread_id = None
self.default_thread_type = None
def _getThread(self, given_thread_id, given_thread_type): def _getThread(self, given_thread_id=None, given_thread_type=None):
# type: (str, ThreadType) -> (str, ThreadType)
""" """
Checks if thread ID is given, checks if default is set and returns correct values 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 :raises ValueError: If thread ID is not given and there is no default
:return: tuple of thread ID and thread type :return: Thread ID and thread type
:rtype: tuple
""" """
if given_thread_id is None: if given_thread_id is None:
if self.default_thread_id is not None: if self.default_thread_id is not None:
@@ -469,8 +502,17 @@ class Client(object):
else: else:
return given_thread_id, given_thread_type return given_thread_id, given_thread_type
"""
GET METHODS
"""
def getAllUsers(self): 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 = { data = {
'viewer': self.uid, 'viewer': self.uid,
@@ -486,7 +528,7 @@ class Client(object):
for k in payload.keys(): for k in payload.keys():
try: try:
user = User.adaptFromChat(payload[k]) user = User._adaptFromChat(payload[k])
except KeyError: except KeyError:
continue continue
@@ -498,6 +540,8 @@ class Client(object):
"""Find and get user by his/her name """Find and get user by his/her name
:param name: name of a person :param name: name of a person
:return: :class:`models.User` objects, ordered by relevance
:rtype: list
""" """
payload = { payload = {
@@ -518,6 +562,144 @@ class Client(object):
users.append(User(entry)) users.append(User(entry))
return users # have bug TypeError: __repr__ returned non-string (type bytes) 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 SEND METHODS
""" """
@@ -564,25 +746,12 @@ class Client(object):
return data 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): def _doSendRequest(self, data):
"""Sends the data to `SendURL`, and returns the message id""" """Sends the data to `SendURL`, and returns the message id"""
r = self._post(ReqUrl.SEND, data) r = self._post(ReqUrl.SEND, data)
j = self._checkRequest(r) j = self._checkRequest(r)
if j is None: if j is None:
return None return None
@@ -615,14 +784,14 @@ class Client(object):
return self.sendMessage(message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user)) return self.sendMessage(message, thread_id=recipient_id, thread_type=isUserToThreadType(is_user))
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.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. Sends a message to a thread
:param message: message to send :param message: Message to send
:param thread_id: user/group chat ID :param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: specify whether thread_id is user or group chat :param thread_type: See :ref:`intro_thread_type`
:return: the message id of the message :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) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type) data = self._getSendData(thread_id, thread_type)
@@ -636,15 +805,16 @@ class Client(object):
return self._doSendRequest(data) return self._doSendRequest(data)
def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER): def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER):
# type: (str, EmojiSize, str, ThreadType) -> list
""" """
Sends an emoji. If emoji and size are not specified a small like is sent. Sends an emoji to a thread
:param emoji: the chosen emoji to send. If not specified, default thread emoji is sent :param emoji: The chosen emoji to send. If not specified, the thread's default emoji is sent
:param size: size of emoji to send :param size: If not specified, a small emoji is sent
:param thread_id: user/group chat ID :param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: specify whether thread_id is user or group chat :param thread_type: See :ref:`intro_thread_type`
:return: the message id of the emoji :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) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type) data = self._getSendData(thread_id, thread_type)
@@ -661,8 +831,34 @@ class Client(object):
return self._doSendRequest(data) 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): 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) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(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, def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER,
recipient_id=None, is_user=None, image=None): 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 image_url: URL of an image to upload and send
:param message: additional message :param message: Additional message
:param thread_id: user/group chat ID :param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: specify whether thread_id is user or group chat :param thread_type: See :ref:`intro_thread_type`
:return: the message id of the message :type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent image
""" """
if recipient_id is not None: 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') 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): recipient_id=None, is_user=None, image=None):
# type: (str, str, str, ThreadType) -> list # type: (str, str, str, ThreadType) -> list
""" """
Sends an image from given URL to given (or default, if not) thread. Sends a local image to a thread
:param image_path: path of an image to upload and send :param image_path: URL of an image to upload and send
:param message: additional message :param message: Additional message
:param thread_id: user/group chat ID :param thread_id: User/Group ID to send to. See :ref:`intro_thread_id`
:param thread_type: specify whether thread_id is user or group chat :param thread_type: See :ref:`intro_thread_type`
:return: the message id of the message :type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent image
""" """
if recipient_id is not None: 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') 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) 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): def addUsersToGroup(self, user_ids, thread_id=None):
# type: (list, str) -> list
""" """
Adds users to the given (or default, if not) group. Adds users to a group.
:param user_ids: list of user ids to add :param user_ids: User ids to add
:param thread_id: group chat ID :param thread_id: Group ID to add people to. See :ref:`intro_thread_id`
:return: the message id of the "message" :type user_ids: list
:return: :ref:`Message ID <intro_message_ids>` of the sent "message"
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = self._getSendData(thread_id, ThreadType.GROUP) data = self._getSendData(thread_id, ThreadType.GROUP)
@@ -746,19 +943,26 @@ class Client(object):
data['action_type'] = 'ma-type:log-message' data['action_type'] = 'ma-type:log-message'
data['log_message_type'] = 'log:subscribe' data['log_message_type'] = 'log:subscribe'
# Make list of users unique
user_ids = set(user_ids)
for i, user_id in enumerate(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) return self._doSendRequest(data)
def removeUserFromGroup(self, user_id, thread_id=None): def removeUserFromGroup(self, user_id, thread_id=None):
# type: (str, str) -> bool
""" """
Adds users to the given (or default, if not) group. Removes users from a group.
:param user_id: user ID to remove :param user_id: User ID to remove
:param thread_id: group chat ID :param thread_id: Group ID to remove people from. See :ref:`intro_thread_id`
:return: whether the action was successful :return: :ref:`Message ID <intro_message_ids>` of the sent "message"
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
@@ -767,7 +971,7 @@ class Client(object):
"uid": user_id, "uid": user_id,
"tid": thread_id "tid": thread_id
} }
j = self._checkRequest(self._post(ReqUrl.REMOVE_USER, data)) j = self._checkRequest(self._post(ReqUrl.REMOVE_USER, data))
return False if j is None else True return False if j is None else True
@@ -788,11 +992,14 @@ class Client(object):
def changeGroupTitle(self, title, thread_id=None): def changeGroupTitle(self, title, thread_id=None):
""" """
Change title of a group conversation. Changes title of a group conversation.
:param title: new group chat title .. todo::
:param thread_id: group chat ID Check whether this can work on group threads, and if it does, change it (back) to changeThreadTitle
:return: the message id of the "message"
: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) thread_id, thread_type = self._getThread(thread_id, None)
data = self._getSendData(thread_id, ThreadType.GROUP) data = self._getSendData(thread_id, ThreadType.GROUP)
@@ -803,19 +1010,19 @@ class Client(object):
return self._doSendRequest(data) return self._doSendRequest(data)
def changeThreadColor(self, new_color, thread_id=None): def changeThreadColor(self, color, thread_id=None):
# type: (ThreadColor, str, ThreadType) -> bool
""" """
Changes thread color to specified color. For more info about color names - see wiki. Changes thread color
:param new_color: new color name :param color: New thread color
:param thread_id: user/group chat ID :param thread_id: User/Group ID to change color of. See :ref:`intro_thread_id`
:return: whether the action was successful :type color: models.ThreadColor
:return: (*bool*) True if the action was successful
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = { data = {
"color_choice": new_color.value, "color_choice": color.value,
"thread_or_other_fbid": thread_id "thread_or_other_fbid": thread_id
} }
@@ -824,13 +1031,14 @@ class Client(object):
return False if j is None else True return False if j is None else True
def reactToMessage(self, message_id, reaction): def reactToMessage(self, message_id, reaction):
# type: (str, MessageReaction) -> bool
""" """
Reacts to a message. Reacts to a message.
:param message_id: message ID to react to :param message_id: :ref:`Message ID <intro_message_ids>` to react to
:param reaction: reaction emoji to send :param reaction: Reaction emoji to use
:return: whether the action was successful :type reaction: models.MessageReaction
:return: True if the action was successful
:rtype: bool
""" """
full_data = { full_data = {
"doc_id": 1491398900900362, "doc_id": 1491398900900362,
@@ -852,20 +1060,22 @@ class Client(object):
url_part = urllib.urlencode(full_data)\ url_part = urllib.urlencode(full_data)\
.replace('u%27', '%27')\ .replace('u%27', '%27')\
.replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1]) .replace('%5CU{}'.format(MessageReactionFix[reaction.value][0]), MessageReactionFix[reaction.value][1])
j = self._checkRequest(self._post('{}/?{}'.format(ReqUrl.MESSAGE_REACTION, url_part))) j = self._checkRequest(self._post('{}/?{}'.format(ReqUrl.MESSAGE_REACTION, url_part)))
return False if j is None else True return False if j is None else True
def setTypingStatus(self, status, thread_id=None, thread_type=None): def setTypingStatus(self, status, thread_id=None, thread_type=None):
# type: (TypingStatus, str, ThreadType) -> bool
""" """
Sets users typing status. Sets users typing status in a thread
:param status: specify whether the status is typing or not (TypingStatus) :param status: Specify the typing status
:param thread_id: user/group chat ID :param thread_id: User/Group ID to change status in. See :ref:`intro_thread_id`
:param thread_type: specify whether thread_id is user or group chat :param thread_type: See :ref:`intro_thread_type`
:return: True if status changed :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) thread_id, thread_type = self._getThread(thread_id, None)
@@ -881,126 +1091,14 @@ class Client(object):
return False if j is None else True 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): def markAsDelivered(self, userID, threadID):
"""
.. todo::
Documenting this
"""
data = { data = {
"message_ids[0]": threadID, "message_ids[0]": threadID,
"thread_ids[%s][0]" % userID: threadID "thread_ids[%s][0]" % userID: threadID
@@ -1010,6 +1108,10 @@ class Client(object):
return r.ok return r.ok
def markAsRead(self, userID): def markAsRead(self, userID):
"""
.. todo::
Documenting this
"""
data = { data = {
"watermarkTimestamp": now(), "watermarkTimestamp": now(),
"shouldSendReadReceipt": True, "shouldSendReadReceipt": True,
@@ -1020,6 +1122,10 @@ class Client(object):
return r.ok return r.ok
def markAsSeen(self): def markAsSeen(self):
"""
.. todo::
Documenting this
"""
r = self._post(ReqUrl.MARK_SEEN, {"seen_timestamp": 0}) r = self._post(ReqUrl.MARK_SEEN, {"seen_timestamp": 0})
return r.ok return r.ok
@@ -1028,7 +1134,10 @@ class Client(object):
return self.friendConnect(friend_id) return self.friendConnect(friend_id)
def friendConnect(self, friend_id): def friendConnect(self, friend_id):
# type: (str) -> bool """
.. todo::
Documenting this
"""
data = { data = {
"to_friend": friend_id, "to_friend": friend_id,
"action": "confirm" "action": "confirm"
@@ -1038,6 +1147,10 @@ class Client(object):
return r.ok return r.ok
def ping(self, sticky): def ping(self, sticky):
"""
.. todo::
Documenting this
"""
data = { data = {
'channel': self.user_channel, 'channel': self.user_channel,
'clientid': self.client_id, 'clientid': self.client_id,
@@ -1138,7 +1251,7 @@ class Client(object):
# Color change # Color change
elif delta_type == "change_thread_theme": 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) thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id, 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) thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
@@ -1163,10 +1276,10 @@ class Client(object):
# Nickname change # Nickname change
elif delta_type == "change_thread_nickname": elif delta_type == "change_thread_nickname":
changed_for = str(delta["untypedData"]["participant_id"]) changed_for = str(delta["untypedData"]["participant_id"])
new_title = delta["untypedData"]["nickname"] new_nickname = delta["untypedData"]["nickname"]
thread_id, thread_type = getThreadIdAndThreadType(metadata) thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for, 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) thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata)
continue continue
@@ -1263,10 +1376,15 @@ class Client(object):
return self.doOneListen(markAlive) return self.doOneListen(markAlive)
def doOneListen(self, markAlive=True): def doOneListen(self, markAlive=True):
# type: (bool) -> None """
"""Does one cycle of the listening loop. Does one cycle of the listening loop.
This method is only useful if you want to control fbchat from an This method is useful if you want to control fbchat from an external event loop
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: try:
if markAlive: self.ping(self.sticky) if markAlive: self.ping(self.sticky)
try: try:
@@ -1275,11 +1393,13 @@ class Client(object):
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
pass pass
except KeyboardInterrupt: except KeyboardInterrupt:
self.listening = False return False
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
pass pass
except Exception as e: 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') @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() return self.stopListening()
def stopListening(self): def stopListening(self):
"""Cleans up the variables from start_listening.""" """Cleans up the variables from startListening"""
self.listening = False self.listening = False
self.sticky, self.pool = (None, None) self.sticky, self.pool = (None, None)
def listen(self, markAlive=True): 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.startListening()
self.onListening() self.onListening()
while self.listening: while self.listening and self.doOneListen(markAlive):
self.doOneListen(markAlive) pass
self.stopListening() 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 -*- # -*- coding: UTF-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import sys import enum
from 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): def __init__(self, data):
"""Represents a Facebook User"""
if data['type'] != 'user': if data['type'] != 'user':
raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path'])) raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path']))
self.uid = data['uid'] self.uid = data['uid']
@@ -13,21 +30,21 @@ class User:
self.photo = data['photo'] self.photo = data['photo']
self.url = data['path'] self.url = data['path']
self.name = data['text'] self.name = data['text']
self.score = data['score'] self.score = float(data['score'])
self.data = data self.data = data
def __repr__(self): def __repr__(self):
uni = self.__unicode__() return self.__unicode__()
return uni.encode('utf-8') if sys.version_info < (3, 0) else uni
def __unicode__(self): def __unicode__(self):
return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url) return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url)
@staticmethod @staticmethod
def adaptFromChat(user_in_chat): def _adaptFromChat(user_in_chat):
""" Adapts user info from chat to User model acceptable initial dict """Adapts user info from chat to User model acceptable initial dict
:param user_in_chat: user info from chat :param user_in_chat: user info from chat
:return: :class:`User` object
'dir': None, 'dir': None,
'mThumbSrcSmall': None, 'mThumbSrcSmall': None,
@@ -53,42 +70,42 @@ class User:
'photo': user_in_chat['thumbSrc'], 'photo': user_in_chat['thumbSrc'],
'path': user_in_chat['uri'], 'path': user_in_chat['uri'],
'text': user_in_chat['name'], 'text': user_in_chat['name'],
'score': '', 'score': 1,
'data': user_in_chat, 'data': user_in_chat,
} }
class Thread(object):
class Thread:
def __init__(self, **entries): def __init__(self, **entries):
self.__dict__.update(entries) self.__dict__.update(entries)
class Message: class Message(object):
def __init__(self, **entries): def __init__(self, **entries):
self.__dict__.update(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): class ThreadType(Enum):
"""Used to specify what type of Facebook thread is being used. See :ref:`intro_thread_type` for more info"""
USER = 1 USER = 1
GROUP = 2 GROUP = 2
class TypingStatus(Enum): class TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing"""
STOPPED = 0 STOPPED = 0
TYPING = 1 TYPING = 1
class EmojiSize(Enum): class EmojiSize(Enum):
"""Used to specify the size of a sent emoji"""
LARGE = '369239383222810' LARGE = '369239383222810'
MEDIUM = '369239343222814' MEDIUM = '369239343222814'
SMALL = '369239263222822' 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): class ThreadColor(Enum):
"""Used to specify a thread colors"""
MESSENGER_BLUE = '' MESSENGER_BLUE = ''
VIKING = '#44bec7' VIKING = '#44bec7'
GOLDEN_POPPY = '#ffc300' GOLDEN_POPPY = '#ffc300'
@@ -106,6 +123,7 @@ class ThreadColor(Enum):
BILOBA_FLOWER = '#a695c7' BILOBA_FLOWER = '#a695c7'
class MessageReaction(Enum): class MessageReaction(Enum):
"""Used to specify a message reaction"""
LOVE = '😍' LOVE = '😍'
SMILE = '😆' SMILE = '😆'
WOW = '😮' WOW = '😮'
@@ -114,6 +132,15 @@ class MessageReaction(Enum):
YES = '👍' YES = '👍'
NO = '👎' NO = '👎'
LIKES = {
'large': EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM,
'small': EmojiSize.SMALL,
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
MessageReactionFix = { MessageReactionFix = {
'😍': ('0001f60d', '%F0%9F%98%8D'), '😍': ('0001f60d', '%F0%9F%98%8D'),
'😆': ('0001f606', '%F0%9F%98%86'), '😆': ('0001f606', '%F0%9F%98%86'),

View File

@@ -8,6 +8,7 @@ from random import random
import warnings import warnings
from .models import * from .models import *
#: Default list of user agents
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_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", "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 (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" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"
] ]
GENDERS = { GENDERS = {
0: 'unknown', 0: 'unknown',
1: 'female_singular', 1: 'female_singular',
@@ -31,7 +33,8 @@ GENDERS = {
11: 'unknown_plural', 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" SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
LOGIN = "https://m.facebook.com/login.php?login_attempt=1" LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
SEND = "https://www.facebook.com/messaging/send/" SEND = "https://www.facebook.com/messaging/send/"
@@ -68,14 +71,14 @@ def strip_to_json(text):
except ValueError as e: except ValueError as e:
return None return None
def get_dencoded(r): def get_decoded(r):
if not isinstance(r._content, str): if not isinstance(r._content, str):
return r._content.decode(facebookEncoding) return r._content.decode(facebookEncoding)
else: else:
return r._content return r._content
def get_json(r): 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): def digit_to_char(digit):
if digit < 10: if digit < 10:
@@ -112,9 +115,7 @@ def raise_exception(e):
raise e raise e
def deprecation(name, deprecated_in=None, removed_in=None, details='', stacklevel=3): 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. """Used to mark parameters as deprecated. Will result in a warning being emmitted when the parameter is used."""
It will result in a warning being emmitted when the parameter is used.
"""
warning = "Client.{} is deprecated".format(name) warning = "Client.{} is deprecated".format(name)
if deprecated_in: if deprecated_in:
warning += ' in v. {}'.format(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) warning += ' and will be removed in v. {}'.format(removed_in)
if details: if details:
warning += '. {}'.format(details) warning += '. {}'.format(details)
warnings.simplefilter('always', DeprecationWarning) warnings.simplefilter('always', DeprecationWarning)
warnings.warn(warning, category=DeprecationWarning, stacklevel=stacklevel) warnings.warn(warning, category=DeprecationWarning, stacklevel=stacklevel)
warnings.simplefilter('default', DeprecationWarning) warnings.simplefilter('default', DeprecationWarning)
def deprecated(deprecated_in=None, removed_in=None, details=''): def deprecated(deprecated_in=None, removed_in=None, details=''):
"""This is a decorator which can be used to mark functions as deprecated. """A decorator used to mark functions as deprecated. Will result in a warning being emmitted when the decorated function is used."""
It will result in a warning being emmitted when the decorated function is used.
"""
def wrap(func, *args, **kwargs): def wrap(func, *args, **kwargs):
def wrapped_func(*args, **kwargs): def wrapped_func(*args, **kwargs):
deprecation(func.__name__, deprecated_in=deprecated_in, removed_in=removed_in, details=details, stacklevel=3) 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 author = None
email = None email = None
source = None source = None
description = None
with open(os.path.join('fbchat', '__init__.py')) as f: with open(os.path.join('fbchat', '__init__.py')) as f:
for line in f: for line in f:
if line.strip().startswith('__version__'): 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("'", '') email = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif line.strip().startswith('__source__'): elif line.strip().startswith('__source__'):
source = line.split('=')[1].strip().replace('"', '').replace("'", '') 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 break
setup( setup(
@@ -40,7 +43,7 @@ setup(
author_email=email, author_email=email,
license='BSD License', license='BSD License',
keywords=["facebook chat fbchat"], keywords=["facebook chat fbchat"],
description="Facebook Chat (Messenger) for Python", description=description,
long_description=readme_content, long_description=readme_content,
classifiers=[ classifiers=[
'Development Status :: 2 - Pre-Alpha', 'Development Status :: 2 - Pre-Alpha',

View File

@@ -13,36 +13,7 @@ from fbchat.models import *
logging_level = logging.ERROR 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): class TestFbchat(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_loginFunctions(self): def test_loginFunctions(self):
self.assertTrue(client.isLoggedIn()) self.assertTrue(client.isLoggedIn())
@@ -51,7 +22,7 @@ class TestFbchat(unittest.TestCase):
self.assertFalse(client.isLoggedIn()) self.assertFalse(client.isLoggedIn())
with self.assertRaises(Exception): 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) client.login(email, password)