Compare commits

..

84 Commits

Author SHA1 Message Date
Mads Marquart
c51a332560 Version up, thanks to @PythonNut 2017-08-27 23:03:50 +02:00
Mads Marquart
a73d2feed6 Merge pull request #193 from PythonNut/master
Fix UNKNOWN gender in graphql requests
2017-08-27 23:02:05 +02:00
PythonNut
6929193e9d Fix UNKNOWN gender in graphql requests 2017-08-13 23:10:11 +00:00
Mads Marquart
fea4ad9e89 Version up, Thanks to ritu99 2017-08-10 15:25:38 +02:00
Mads Marquart
68099049d4 Merge pull request #189 from ritu99/master
Added Message Count to thread information
2017-08-10 15:22:22 +02:00
Ritvik Annam
44cf08bdfd fetchThreadInfo now pulls message_count 2017-08-10 01:15:29 -05:00
Ritvik Annam
9e32cf17a4 fetchThreadList now pulls message_count 2017-08-10 00:53:06 -05:00
Mads Marquart
0661367ebb Properly fixed #182 2017-08-02 23:08:34 +02:00
Mads Marquart
3c07e42ba2 Version up, fixed #182 2017-07-26 23:13:19 +02:00
Mads Marquart
2cd6376818 Merge pull request #178 from Bankde/fix-fail-after-running-for-days
Fix issue when running for long time
2017-07-26 23:03:49 +02:00
Mads Marquart
5e7f7750de Fixed enums in python 2.7, thanks to @liamkirsh 2017-07-12 14:52:15 +02:00
Bankde@hotmail.com
2a223ec6db fix array indexing (I don't know why fb do that) 2017-07-10 10:25:23 +07:00
Mads Marquart
a99108fff6 Version up thanks to @Bankde 2017-07-09 20:55:06 +02:00
Mads Marquart
8de4698cc4 Merge pull request #174 from Bankde/fix-error-in-python2
No FileNotFoundError in py2
2017-07-09 20:53:47 +02:00
Bankde@hotmail.com
637319ec2c add token update 2017-07-10 00:51:51 +07:00
Bankde@hotmail.com
f9398564cd replace FileNotFoundError with IOError so it can work in Py2 2017-07-05 09:18:13 +07:00
Mads Marquart
b57f423eb4 Version up thanks to @aaronlewism 2017-07-01 12:48:02 +02:00
Mads Marquart
3093f1f2b6 Merge pull request #173 from aaronlewism/master
Check for alternate 2Factor page text
2017-07-01 12:46:11 +02:00
Aaron Lewis
961777e0c1 Check for alternate 2Factor page text 2017-06-29 13:21:25 -07:00
Mads Marquart
d7139701f7 Fixed typo, improved formatting. Thanks to @JarbasAI! 2017-06-29 20:04:01 +02:00
Mads Marquart
c6bac17d48 Merge pull request #172 from JarbasAI/patch-1
Add on chat presence event
2017-06-29 19:55:49 +02:00
Mads Marquart
3638fc5356 Made fetchThreadInfo able to fetch own user's info 2017-06-29 19:53:29 +02:00
Jarbas
aca9176f7f Add on chat presence event
Last_seen time stamps were handled in unknown message type, this info is freely available and potentially useful
2017-06-29 17:56:14 +01:00
Mads Marquart
0d5e4f6d3f Version up thanks to @enwar3 2017-06-29 16:03:55 +02:00
Mads Marquart
92a5ffdef8 Merge pull request #170 from OMGWINNING/master
Add extensible_attachment field to Message for fb share objects
2017-06-29 16:02:27 +02:00
Joe Lau
b3359fccdb Add last_message_timestamp to Thread objects 2017-06-28 18:08:45 -07:00
Joe Lau
d8f7366d1f Add extensible_attachment field to Message for fb share objects 2017-06-28 13:19:17 -07:00
Mads Marquart
ff94dc20af Minor cleanup 2017-06-28 16:06:13 +02:00
Mads Marquart
a8df0a548f Minor fixes 2017-06-28 14:42:11 +02:00
Mads Marquart
13d0dc4ba4 Fixed ChangeThreadTitle and ThreadColor.MESSENGER_BLUE 2017-06-28 14:30:29 +02:00
Mads Marquart
64125a1aca Updated to 1.0.6, thanks to @enwar3 2017-06-28 10:24:44 +02:00
Mads Marquart
4feae03092 Merge pull request #169 from OMGWINNING/master
Handle empty participant_customizations field
2017-06-28 10:23:33 +02:00
Joe Lau
5f993c2bf8 Use .get() instead 2017-06-27 16:16:51 -07:00
Joe Lau
35bbcbffba Add __init__.py 2017-06-26 17:54:25 -07:00
Joe Lau
5faca54d67 Handle empty participant_customizations field 2017-06-26 14:16:57 -07:00
Mads Marquart
82496b8e04 Minor fixes 2017-06-26 17:02:32 +02:00
Mads Marquart
2d74ec7823 Made getAllUsers more stable 2017-06-26 15:42:26 +02:00
Mads Marquart
1d42c4d3a6 Updated to 1.0.4, added fetchThread&GroupInfo and improved models 2017-06-26 15:41:58 +02:00
Mads Marquart
4a8ef00442 Fixed a few bugs, updated to v. 1.0.3 2017-06-26 11:37:54 +02:00
Mads Marquart
add06ffa7a I was having trouble with PyPI ;) 2017-06-22 23:35:28 +02:00
Mads Marquart
fbb8d8e24a Update README.rst 2017-06-22 22:54:12 +02:00
Mads Marquart
cd0e001219 Update README.rst 2017-06-22 22:50:10 +02:00
Mads Marquart
bf53f4fc74 Updated to 1.0.0 2017-06-22 22:43:26 +02:00
Mads Marquart
11e59e023c Added GraphQL requests 2017-06-22 22:38:15 +02:00
Mads Marquart
c81d7d2bfb Removed deprecations and new event system, improved other things
Removed deprecations
Removed new event system
Added documentation for all events
Added FAQ
Changed Client.uid to Client.id
Improved User model
Prepared for support of pages
2017-06-20 14:57:23 +02:00
Mads Marquart
0885796fa8 Fix for Facebook's changed API, and removed test print statement 2017-06-02 15:59:24 +02:00
Mads Marquart
1279481b62 Update README.rst 2017-05-28 22:38:30 +02:00
Mads Marquart
5fb3412915 Update README.rst 2017-05-28 22:34:18 +02:00
Mads Marquart
c708a5ecf6 Fixed README! 2017-05-28 22:33:50 +02:00
Mads Marquart
e4f29e5f2b Fixed README.rst? 2017-05-28 21:58:00 +02:00
Mads Marquart
779fa409e5 Fixed README.rst (again) 2017-05-28 21:56:58 +02:00
Mads Marquart
fc8c2dfa14 Fixed README.rst 2017-05-28 21:52:54 +02:00
Mads Marquart
9f7d308961 Added requirements.txt 2017-05-28 21:19:31 +02:00
Mads Marquart
8dacc37ba9 More documentation work, changed addUsersToGroup back to taking a list of user IDs
Created new README, and finished `intro`
2017-05-28 21:11:16 +02:00
Mads Marquart
39eafa5a3e Fixed examples, added changeNickname and changeThreadEmoji, changed changeGroupTitle back to changeThreadTitle
I also removed the parameter `set_default_events` from __init__, since
it's not really necessary
Also added testing of examples and simple testing of listen functions
2017-05-26 18:48:37 +02:00
Mads Marquart
d2741ca419 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
2017-05-26 13:38:54 +02:00
Mads Marquart
a76ebbb22a Added python 2.7 support, reworked events
- Reworked events, so now they support python 2.7 (I had to remove some
functionality though, but that was a little unnecessary anyway)
- Events now support the old style of writing, for people who's more
comfortable with that: ```python
class EchoBot(fbchat.Client):
    def onMessage(self, *args, **kwargs):
        self.something(*args, **kwargs)
```
While still supporting the new method:
```python
class EchoBot(fbchat.Client):
    def __init__(self, *args, **kwargs):
         super(EchoBot, self).__init__(*args, **kwargs)
         self.onMessage += lamda *args, **kwargs: self.something(*args,
**kwargs)
```
- Included `msg` as a parameter in every event function, since it's
useful if you want to extract some of the other data
- Moved test data to the folder `tests`
- Fixed various other functions, and improved stability
2017-05-22 20:33:00 +02:00
Mads Marquart
83a45ebc03 Changed names with Chat to Group or Thread respectively, improved error handling, and changed _doSendRequest to return a single message id 2017-05-21 23:12:30 +02:00
Mads Marquart
76c2c65a7b Fixed sendLocalImage, changed get_json, improved tests
- Changed get_json to take a `requests` response, and then return the
json (While checking encoding and removing unnecessary characters)
- Fixed sendLocalImage, the problem was that the `_getThread` call was
missing a parameter (Took me hours ;) )
- Removed 3 second delay between tests, I felt it was unnecessary
- Updated tests to no longer use deprecated functions
2017-05-21 21:56:56 +02:00
Mads Marquart
99a7d0d534 Added removed in v. x warning to deprecations, and improved login error messages 2017-05-18 20:08:11 +02:00
Mads T Marquart
8e68531ce4 Merge pull request #145 from Dainius14/dev
Add more events and send methods, some fixes
2017-05-17 16:19:32 +02:00
Dainius
ed7b8488cb rename _setThread() to _getThreadId() 2017-05-17 14:20:07 +03:00
Dainius
386cb4a6c1 fix on seen, on delivered, on marked as seen methods 2017-05-16 21:20:46 +03:00
Dainius
c95544dcb0 add typing indicator 2017-05-16 19:35:00 +03:00
Dainius
4083348c40 add reaction to messages. move request URLs to utils 2017-05-16 19:20:46 +03:00
Dainius
b1cccf4173 fix emoji sending and tests
My bad. Test data is still being commited, changed it to a sample file instead
2017-05-16 14:02:09 +03:00
Dainius
e1e1a0d611 add thread color change 2017-05-16 12:03:20 +03:00
Mads T Marquart
fb88f8d459 Merge pull request #142 from Torxed/master
Changed traceback.print_exc() to logging.exception
2017-05-15 15:50:15 +02:00
Mads T Marquart
0fdab3968d Merge branch 'development' into master 2017-05-15 15:49:14 +02:00
Lord Anton Hvornum
ac0e72d167 There's no such thing as 'unicode' in Py3 2017-05-11 21:23:48 +02:00
Lord Anton Hvornum
e8fbaefa72 There's no such thing as 'unicode' in Py3 2017-05-11 21:23:18 +02:00
Lord Anton Hvornum
de21eafe7b Swapped out for a better error output. You had no idea where errors occured before. 2017-05-11 21:18:27 +02:00
Mads T Marquart
44b3b1330a Created test_data.json
See [here](f63b9d7c4a (commitcomment-22103626)) for more info
2017-05-11 17:35:56 +02:00
Mads T Marquart
fd2e554b98 Updated version 2017-05-11 16:29:48 +02:00
Mads T Marquart
d7acb9a40d Changed default emoji size to small
Also updated version
2017-05-11 16:29:41 +02:00
Mads Marquart
f63b9d7c4a Reworked old events, added deprecation warnings, and improved _send
- Added a new system to show an error if old events are used
- Removed `test_data.json`, since I don't want to risk that anyone
accidentally commits their username & password
- Finished work on `sendEmoji`
- Split `_send` into two parts:
`_getSendData` and `_doSendRequest`, which allows for an easier way of
adding new send requests
2017-05-11 12:55:44 +02:00
Mads T Marquart
7a0c64bf9e Merge pull request #141 from Dainius14/dev
reintroduce things skipped on conflict
2017-05-11 10:26:21 +02:00
Dainius
ef352f097a update test_data.json 2017-05-10 18:44:20 +03:00
Dainius
357083efce reintroduce things skipped on conflict 2017-05-10 18:16:41 +03:00
Mads Marquart
0d75c09036 Added support for deprecating items, and maybe support for python 2.7
- Changed `test_data.js` to `test_data.json`
- Added `deprecated` decorator
- Added `deprecation` function
- Readded old functions, and marked them as deprecated
- Changed parameters back to being type-in-specific (support for python
2.x)
- Deprecated `info_log` and `debug` init paramters
2017-05-10 14:54:07 +02:00
Mads T Marquart
58c7e08d12 Merge pull request #140 into development branch from Dainius14/dev 2017-05-10 11:10:16 +02:00
Dainius
b5443daeb1 Merge branch 'development' into dev 2017-05-10 12:02:39 +03:00
Dainius
5da3e5e4bf update tests 2017-05-09 21:27:32 +03:00
Dainius
f4dec2e48e update send methods 2017-05-09 10:25:04 +03:00
35 changed files with 3188 additions and 902 deletions

4
.gitignore vendored
View File

@@ -1,5 +1,7 @@
*py[co] *py[co]
.idea/
# Test scripts # Test scripts
*.sh *.sh
@@ -23,4 +25,6 @@ develop-eggs
docs/_build/ docs/_build/
# Data for tests # Data for tests
my_test_data.json
my_data.json
tests.data tests.data

View File

@@ -1,119 +1,30 @@
====== fbchat: Facebook Chat (Messenger) for Python
fbchat ============================================
======
.. image:: https://img.shields.io/badge/license-BSD-blue.svg
:target: LICENSE.txt
:alt: License: BSD
Facebook Chat (`Messenger <https://www.messenger.com/>`__) for Python. This project was inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__. .. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg
:target: https://pypi.python.org/pypi/fbchat
:alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6
**No XMPP or API key is needed**. Just use your ID and PASSWORD. .. image:: https://readthedocs.org/projects/fbchat/badge/?version=master
:target: https://fbchat.readthedocs.io
:alt: Documentation
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>`__.
Installation **No XMPP or API key is needed**. Just use your email and password.
============
Simple: Go to `Read the Docs <https://fbchat.readthedocs.io>`__ to see the full documentation,
or jump right into the code by viewing the `examples <examples>`__
Installation:
.. code-block:: console .. code-block:: console
$ pip install fbchat $ pip install fbchat
© Copyright 2015 - 2017 by Taehoon Kim / `@carpedm20 <http://carpedm20.github.io/about/>`__
Example
=======
.. code-block:: python
import fbchat
client = fbchat.Client("YOUR_ID", "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")
if sent:
print("Message sent successfully!")
# IMAGES
client.sendLocalImage(friend.uid,message='<message text>',image='<path/to/image/file>') # send local image
imgurl = "http://i.imgur.com/LDQ2ITV.jpg"
client.sendRemoteImage(friend.uid,message='<message text>', image=imgurl) # send image from image url
Getting user info from user id
==============================
.. code-block:: python
friend1 = client.getUsers('<friend name 1>')[0]
friend2 = client.getUsers('<friend name 2>')[0]
friend1_info = client.getUserInfo(friend1.uid) # returns dict with details
both_info = client.getUserInfo(friend1.uid,friend2.uid) # query both together, returns list of dicts
friend1_name = friend1_info['name']
Getting last messages sent
==========================
.. code-block:: python
last_messages = client.getThreadInfo(friend.uid, last_n=20)
last_messages.reverse() # messages come in reversed order
for message in last_messages:
print(message.body)
Example Echobot
===============
.. code-block:: python
import fbchat
#subclass fbchat.Client and override required methods
class EchoBot(fbchat.Client):
def __init__(self,email, password, debug=True, user_agent=None):
fbchat.Client.__init__(self,email, password, debug, user_agent)
def on_message(self, mid, author_id, author_name, message, metadata):
self.markAsDelivered(author_id, mid) #mark delivered
self.markAsRead(author_id) #mark read
print("%s said: %s"%(author_id, message))
#if you are not the author, echo
if str(author_id) != str(self.uid):
self.send(author_id,message)
bot = EchoBot("<email>", "<password>")
bot.listen()
Saving session
==========================
.. code-block:: python
session_cookies = client.setSession()
# save session_cookies
Loading session
==========================
.. code-block:: python
client = fbchat.Client(None, None, session_cookies=session_cookies)
# OR
client.setSession(session_cookies)
Authors
=======
Taehoon Kim / `@carpedm20 <http://carpedm20.github.io/about/>`__

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)

BIN
docs/_static/find-group-id.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

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() }}

44
docs/api.rst Normal file
View File

@@ -0,0 +1,44 @@
.. 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
------
This is the main class of `fbchat`, which contains all the methods you use to interract with Facebook.
You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening)
.. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO)
:members:
.. _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:
:undoc-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

56
docs/examples.rst Normal file
View File

@@ -0,0 +1,56 @@
.. 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
Basic example
-------------
This will show basic usage of `fbchat`
.. literalinclude:: ../examples/basic_usage.py
Interacting with Threads
------------------------
This will interract with the thread in every way `fbchat` supports
.. literalinclude:: ../examples/interract.py
Fetching Information
--------------------
This will show the different ways of fetching information about users and threads
.. literalinclude:: ../examples/fetch.py
Echobot
-------
This will reply to any message with the same message
.. literalinclude:: ../examples/echobot.py
Remove Bot
----------
This will remove a user from a group if they write the message `Remove me!`
.. literalinclude:: ../examples/removebot.py
"Prevent changes"-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

44
docs/faq.rst Normal file
View File

@@ -0,0 +1,44 @@
.. highlight:: python
.. module:: fbchat
.. _faq:
FAQ
===
Version X broke my installation
-------------------------------
We try to provide backwards compatability where possible, but since we're not part of Facebook,
most of the things may be broken at any point in time
Downgrade to an earlier version of fbchat, run this command
.. code-block:: sh
$ pip install fbchat==<X>
Where you replace ``<X>`` with the version you want to use
Will you be supporting creating posts/events/pages and so on?
-------------------------------------------------------------
We won't be focusing on anything else than chat-related things. This API is called `fbCHAT`, after all ;)
Submitting Issues
-----------------
If you're having trouble with some of the snippets, or you think some of the functionality is broken,
please feel free to submit an issue on `Github <https://github.com/carpedm20/fbchat>`_.
You should first login with ``logging_level`` set to ``logging.DEBUG``::
from fbchat import Client
import logging
client = Client('<email>', '<password>', logging_level=logging.DEBUG)
Then you can submit the relevant parts of this log, and detailed steps on how to reproduce
.. warning::
Always remove your credentials from any debug information you may provide us.
Preferably, use a test account, in case you miss anything

66
docs/index.rst Normal file
View File

@@ -0,0 +1,66 @@
.. highlight:: python
.. module:: fbchat
.. 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
:alt: License: BSD
.. 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
:alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6
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 it's accessing the website normally.
Therefore, this API requires the credentials of a Facebook account.
.. note::
If you're having problems, please check the :ref:`faq`, before asking questions on Github
.. warning::
We are not responsible if your account gets banned for spammy activities,
such as sending lots of messages to people you don't know, sending messages very quickly,
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.
If you're already familiar with the basics of how Facebook works internally, go to :ref:`examples` to see example usage of `fbchat`
Overview
--------
.. toctree::
:maxdepth: 2
install
intro
examples
testing
api
todo
faq

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

200
docs/intro.rst Normal file
View File

@@ -0,0 +1,200 @@
.. highlight:: python
.. module:: fbchat
.. _intro:
Introduction
============
`fbchat` uses your email and password to communicate with the Facebook server.
That means that you should always store your password in a seperate file, in case e.g. someone looks over your shoulder while you're writing code.
You should also make sure that the file's access control is appropriately restrictive
.. _intro_logging_in:
Logging In
----------
Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt
(If you want to supply the code in another fasion, overwrite :func:`Client.on2FACode`)::
from fbchat import Client
from fbchat.models import *
client = Client('<email>', '<password>')
Replace ``<email>`` and ``<password>`` with your email and password respectively
.. note::
For ease of use then most of the code snippets in this document will assume you've already completed the login process
Though the second line, ``from fbchat.models import *``, is not strictly neccesary here, later code snippets will assume you've done this
If you want to change how verbose `fbchat` is, change the logging level (in :class:`Client`)
Throughout your code, if you want to check whether you are still logged in, use :func:`Client.isLoggedIn`.
An example would be to login again if you've been logged out, using :func:`Client.login`::
if not client.isLoggedIn():
client.login('<email>', '<password>')
When you're done using the client, and want to securely logout, use :func:`Client.logout`::
client.logout()
.. _intro_threads:
Threads
-------
A thread can refer to two things: A Messenger group chat or a single Facebook user
:class:`models.ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``.
These will specify whether the thread is a single user chat or a group chat.
This is required for many of `fbchat`'s functions, since Facebook differetiates between these two internally
Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`,
and searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching`
You can get your own user ID by using :any:`Client.uid`
Getting the ID of a group chat is fairly trivial otherwise, since you only need to navigate to `<https://www.facebook.com/messages/>`_,
click on the group you want to find the ID of, and then read the id from the address bar.
The URL will look something like this: ``https://www.facebook.com/messages/t/1234567890``, where ``1234567890`` would be the ID of the group.
An image to illustrate this is shown below:
.. image:: /_static/find-group-id.png
:alt: An image illustrating how to find the ID of a group
The same method can be applied to some user accounts, though if they've set a custom URL, then you'll just see that URL instead
Here's an snippet showing the usage of thread IDs and thread types, where ``<user id>`` and ``<group id>``
corresponds to the ID of a single user, and the ID of a group respectively::
client.sendMessage('<message>', thread_id='<user id>', thread_type=ThreadType.USER)
client.sendMessage('<message>', thread_id='<group id>', thread_type=ThreadType.GROUP)
Some functions (e.g. :func:`Client.changeThreadColor`) don't require a thread type, so in these cases you just provide the thread ID::
client.changeThreadColor(ThreadColor.BILOBA_FLOWER, thread_id='<user id>')
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id='<group id>')
.. _intro_message_ids:
Message IDs
-----------
Every message you send on Facebook has a unique ID, and every action you do in a thread,
like changing a nickname or adding a person, has a unique ID too.
Some of `fbchat`'s functions require these ID's, like :func:`Client.reactToMessage`,
and some of then provide this ID, like :func:`Client.sendMessage`.
This snippet shows how to send a message, and then use the returned ID to react to that message with a 😍 emoji::
message_id = client.sendMessage('message', thread_id=thread_id, thread_type=thread_type)
client.reactToMessage(message_id, MessageReaction.LOVE)
.. _intro_interacting:
Interacting with Threads
------------------------
`fbchat` provides multiple functions for interacting with threads
Most functionality works on all threads, though some things,
like adding users to and removing users from a group chat, logically only works on group chats
The simplest way of using `fbchat` is to send a message.
The following snippet will, as you've probably already figured out, send the message `test message` to your account::
message_id = client.sendMessage('test message', thread_id=client.uid, thread_type=ThreadType.USER)
You can see a full example showing all the possible thread interactions with `fbchat` by going to :ref:`examples`
.. _intro_fetching:
Fetching Information
--------------------
You can use `fbchat` to fetch basic information like user names, profile pictures, thread names and user IDs
You can retrieve a user's ID with :func:`Client.searchForUsers`.
The following snippet will search for users by their name, take the first (and most likely) user, and then get their user ID from the result::
users = client.searchForUsers('<name of user>')
user = users[0]
print("User's ID: {}".format(user.uid))
print("User's name: {}".format(user.name))
print("User's profile picture url: {}".format(user.photo))
print("User's main url: {}".format(user.url))
Since this uses Facebook's search functions, you don't have to specify the whole name, first names will usually be enough
You can see a full example showing all the possible ways to fetch information with `fbchat` by going to :ref:`examples`
.. _intro_sessions:
Sessions
--------
`fbchat` provides functions to retrieve and set the session cookies.
This will enable you to store the session cookies in a seperate file, so that you don't have to login each time you start your script.
Use :func:`Client.getSession` to retrieve the cookies::
session_cookies = client.getSession()
Then you can use :func:`Client.setSession`::
client.setSession(session_cookies)
Or you can set the ``session_cookies`` on your initial login.
(If the session cookies are invalid, your email and password will be used to login instead)::
client = Client('<email>', '<password>', session_cookies=session_cookies)
.. warning::
You session cookies can be just as valueable as you password, so store them with equal care
.. _intro_events:
Listening & Events
------------------
To use the listening functions `fbchat` offers (like :func:`Client.listen`),
you have to define what should be executed when certain events happen.
By default, (most) events will just be a `logging.info` statement,
meaning it will simply print information to the console when an event happens
.. note::
You can identify the event methods by their `on` prefix, e.g. `onMessage`
The event actions can be changed by subclassing the :class:`Client`, and then overwriting the event methods::
class CustomClient(Client):
def onMessage(self, mid, author_id, message, thread_id, thread_type, ts, metadata, msg, **kwargs):
# Do something with the message here
pass
client = CustomClient('<email>', '<password>')
**Notice:** The following snippet is as equally valid as the previous one::
class CustomClient(Client):
def onMessage(self, message, author_id, thread_id, thread_type, **kwargs):
# Do something with the message here
pass
client = CustomClient('<email>', '<password>')
The change was in the parameters that our `onMessage` method took: ``message`` and ``author_id`` got swapped,
and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function still works, since we included ``**kwargs``
.. note::
Therefore, for both backwards and forwards compatability,
the API actually requires that you include ``**kwargs`` as your final argument.
View the :ref:`examples` to see some more examples illustrating the event system

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=python -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

29
docs/testing.rst Normal file
View File

@@ -0,0 +1,29 @@
.. highlight:: sh
.. module:: fbchat
.. _testing:
Testing
=======
To use the 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

24
docs/todo.rst Normal file
View File

@@ -0,0 +1,24 @@
.. highlight:: python
.. module:: fbchat
.. _todo:
Todo
====
This page will be periodically updated to show missing features and documentation
Missing Functionality
---------------------
- Implement Client.searchForMessage
- This will use the graphql request API
- Implement chatting with pages properly
- Write better FAQ
- Explain usage of graphql
Documentation
-------------
.. todolist::

12
examples/basic_usage.py Normal file
View File

@@ -0,0 +1,12 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client('<email>', '<password>')
print('Own id: {}'.format(client.uid))
client.sendMessage('Hi me!', thread_id=client.uid, thread_type=ThreadType.USER)
client.logout()

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, author_id, message, thread_id, thread_type, **kwargs):
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()

64
examples/fetch.py Normal file
View File

@@ -0,0 +1,64 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client('<email>', '<password>')
# Fetches a list of all users you're currently chatting with, as `User` objects
users = client.fetchAllUsers()
print("users' IDs: {}".format(user.uid for user in users))
print("users' names: {}".format(user.name for user in users))
# If we have a user id, we can use `fetchUserInfo` to fetch a `User` object
user = client.fetchUserInfo('<user id>')['<user id>']
# We can also query both mutiple users together, which returns list of `User` objects
users = client.fetchUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>')
print("user's name: {}".format(user.name))
print("users' names: {}".format(users[k].name for k in users))
# `searchForUsers` searches for the user and gives us a list of the results,
# and then we just take the first one, aka. the most likely one:
user = client.searchForUsers('<name of user>')[0]
print('user ID: {}'.format(user.uid))
print("user's name: {}".format(user.name))
print("user's photo: {}".format(user.photo))
print("Is user client's friend: {}".format(user.is_friend))
# Fetches a list of the 20 top threads you're currently chatting with
threads = client.fetchThreadList()
# Fetches the next 10 threads
threads += client.fetchThreadList(offset=20, limit=10)
print("Threads: {}".format(threads))
# Gets the last 10 messages sent to the thread
messages = client.fetchThreadMessages(thread_id='<thread id>', limit=10)
# Since the message come in reversed order, reverse them
messages.reverse()
# Prints the content of all the messages
for message in messages:
print(message.text)
# If we have a thread id, we can use `fetchThreadInfo` to fetch a `Thread` object
thread = client.fetchThreadInfo('<thread id>')['<thread id>']
print("thread's name: {}".format(thread.name))
print("thread's type: {}".format(thread.type))
# `searchForThreads` searches works like `searchForUsers`, but gives us a list of threads instead
thread = client.searchForThreads('<name of thread>')[0]
print("thread's name: {}".format(thread.name))
print("thread's type: {}".format(thread.type))
# Here should be an example of `getUnread`

55
examples/interract.py Normal file
View File

@@ -0,0 +1,55 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client("<email>", "<password>")
thread_id = '1234567890'
thread_type = ThreadType.GROUP
# Will send a message to the thread
client.sendMessage('<message>', thread_id=thread_id, thread_type=thread_type)
# Will send the default `like` emoji
client.sendEmoji(emoji=None, size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type)
# Will send the emoji `👍`
client.sendEmoji(emoji='👍', size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type)
# Will send the image located at `<image path>`
client.sendLocalImage('<image path>', message='This is a local image', thread_id=thread_id, thread_type=thread_type)
# Will download the image at the url `<image url>`, and then send it
client.sendRemoteImage('<image url>', message='This is a remote image', thread_id=thread_id, thread_type=thread_type)
# Only do these actions if the thread is a group
if thread_type == ThreadType.GROUP:
# Will remove the user with ID `<user id>` from the thread
client.removeUserFromGroup('<user id>', thread_id=thread_id)
# Will add the user with ID `<user id>` to the thread
client.addUsersToGroup('<user id>', thread_id=thread_id)
# Will add the users with IDs `<1st user id>`, `<2nd user id>` and `<3th user id>` to the thread
client.addUsersToGroup(['<1st user id>', '<2nd user id>', '<3rd user id>'], thread_id=thread_id)
# Will change the nickname of the user `<user_id>` to `<new nickname>`
client.changeNickname('<new nickname>', '<user id>', thread_id=thread_id, thread_type=thread_type)
# Will change the title of the thread to `<title>`
client.changeThreadTitle('<title>', thread_id=thread_id, thread_type=thread_type)
# Will set the typing status of the thread to `TYPING`
client.setTypingStatus(TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type)
# Will change the thread color to `MESSENGER_BLUE`
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id=thread_id)
# Will change the thread emoji to `👍`
client.changeThreadEmoji('👍', thread_id=thread_id)
# Will react to a message with a 😍 emoji
client.reactToMessage('<message id>', MessageReaction.LOVE)

54
examples/keepbot.py Normal file
View File

@@ -0,0 +1,54 @@
# -*- 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 group chat name'
old_nicknames = {
'12345678901': "User nr. 1's nickname",
'12345678902': "User nr. 2's nickname",
'12345678903': "User nr. 3's nickname",
'12345678904': "User nr. 4's nickname"
}
class KeepBot(Client):
def onColorChange(self, author_id, new_color, thread_id, thread_type, **kwargs):
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, author_id, new_emoji, thread_id, thread_type, **kwargs):
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))
self.changeThreadEmoji(old_emoji, thread_id=thread_id)
def onPeopleAdded(self, added_ids, author_id, thread_id, **kwargs):
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, **kwargs):
# 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, author_id, new_title, thread_id, thread_type, **kwargs):
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.changeThreadTitle(old_title, thread_id=thread_id, thread_type=thread_type)
def onNicknameChange(self, author_id, changed_for, new_nickname, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and changed_for in old_nicknames and old_nicknames[changed_for] != new_nickname:
log.info("{} changed {}'s' nickname. It will be changed back".format(author_id, changed_for))
self.changeNickname(old_nicknames[changed_for], changed_for, thread_id=thread_id, thread_type=thread_type)
client = KeepBot("<email>", "<password>")
client.listen()

17
examples/removebot.py Normal file
View File

@@ -0,0 +1,17 @@
# -*- coding: UTF-8 -*-
from fbchat import log, Client
from fbchat.models import *
class RemoveBot(Client):
def onMessage(self, author_id, message, thread_id, thread_type, **kwargs):
# We can only kick people from group chats, so no need to try if it's a user chat
if message == 'Remove me!' and thread_type == ThreadType.GROUP:
log.info('{} will be removed from {}'.format(author_id, thread_id))
self.removeUserFromGroup(author_id, thread_id=thread_id)
else:
# Sends the data to the inherited onMessage, so that we can still see when a message is recieved
super(type(self), self).onMessage(author_id=author_id, message=message, thread_id=thread_id, thread_type=thread_type, **kwargs)
client = RemoveBot("<email>", "<password>")
client.listen()

View File

@@ -1,5 +1,10 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from .client import *
""" """
fbchat fbchat
~~~~~~ ~~~~~~
@@ -11,15 +16,13 @@
""" """
from .client import * __copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__version__ = '1.0.19'
__copyright__ = 'Copyright 2015 by Taehoon Kim'
__version__ = '0.9.3'
__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',

File diff suppressed because it is too large Load Diff

290
fbchat/graphql.py Normal file
View File

@@ -0,0 +1,290 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import json
import re
from .models import *
from .utils import *
# Shameless copy from https://stackoverflow.com/a/8730674
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
class ConcatJSONDecoder(json.JSONDecoder):
def decode(self, s, _w=WHITESPACE.match):
s_len = len(s)
objs = []
end = 0
while end != s_len:
obj, end = self.raw_decode(s, idx=_w(s, end).end())
end = _w(s, end).end()
objs.append(obj)
return objs
# End shameless copy
def graphql_color_to_enum(color):
if color is None:
return None
if len(color) == 0:
return ThreadColor.MESSENGER_BLUE
try:
return ThreadColor('#{}'.format(color[2:].lower()))
except ValueError:
raise Exception('Could not get ThreadColor from color: {}'.format(color))
def get_customization_info(thread):
if thread is None or thread.get('customization_info') is None:
return {}
info = thread['customization_info']
rtn = {
'emoji': info.get('emoji'),
'color': graphql_color_to_enum(info.get('outgoing_bubble_color'))
}
if thread.get('thread_type') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'):
rtn['nicknames'] = {}
for k in info.get('participant_customizations', []):
rtn['nicknames'][k['participant_id']] = k.get('nickname')
elif info.get('participant_customizations'):
uid = thread.get('thread_key', {}).get('other_user_id') or thread.get('id')
pc = info['participant_customizations']
if len(pc) > 0:
if pc[0].get('participant_id') == uid:
rtn['nickname'] = pc[0].get('nickname')
else:
rtn['own_nickname'] = pc[0].get('nickname')
if len(pc) > 1:
if pc[1].get('participant_id') == uid:
rtn['nickname'] = pc[1].get('nickname')
else:
rtn['own_nickname'] = pc[1].get('nickname')
return rtn
def graphql_to_message(message):
if message.get('message_sender') is None:
message['message_sender'] = {}
if message.get('message') is None:
message['message'] = {}
is_read = None
if message.get('unread') is not None:
is_read = not message['unread']
return Message(
message.get('message_id'),
author=message.get('message_sender').get('id'),
timestamp=message.get('timestamp_precise'),
is_read=is_read,
reactions=message.get('message_reactions'),
text=message.get('message').get('text'),
mentions=[Mention(m.get('entity', {}).get('id'), offset=m.get('offset'), length=m.get('length')) for m in message.get('message').get('ranges', [])],
sticker=message.get('sticker'),
attachments=message.get('blob_attachments'),
extensible_attachment=message.get('extensible_attachment')
)
def graphql_to_user(user):
if user.get('profile_picture') is None:
user['profile_picture'] = {}
c_info = get_customization_info(user)
return User(
user['id'],
url=user.get('url'),
first_name=user.get('first_name'),
last_name=user.get('last_name'),
is_friend=user.get('is_viewer_friend'),
gender=GENDERS[user.get('gender')],
affinity=user.get('affinity'),
nickname=c_info.get('nickname'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
own_nickname=c_info.get('own_nickname'),
photo=user['profile_picture'].get('uri'),
name=user.get('name'),
message_count=user.get('messages_count')
)
def graphql_to_group(group):
if group.get('image') is None:
group['image'] = {}
c_info = get_customization_info(group)
return Group(
group['thread_key']['thread_fbid'],
participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]),
nicknames=c_info.get('nicknames'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
photo=group['image'].get('uri'),
name=group.get('name'),
message_count=group.get('messages_count')
)
def graphql_to_page(page):
if page.get('profile_picture') is None:
page['profile_picture'] = {}
if page.get('city') is None:
page['city'] = {}
return Page(
page['id'],
url=page.get('url'),
city=page.get('city').get('name'),
category=page.get('category_type'),
photo=page['profile_picture'].get('uri'),
name=page.get('name'),
message_count=page.get('messages_count')
)
def graphql_queries_to_json(*queries):
"""
Queries should be a list of GraphQL objects
"""
rtn = {}
for i, query in enumerate(queries):
rtn['q{}'.format(i)] = query.value
return json.dumps(rtn)
def graphql_response_to_json(content):
j = json.loads(content, cls=ConcatJSONDecoder)
rtn = [None]*(len(j))
for x in j:
if 'error_results' in x:
del rtn[-1]
continue
check_json(x)
[(key, value)] = x.items()
check_json(value)
if 'response' in value:
rtn[int(key[1:])] = value['response']
else:
rtn[int(key[1:])] = value['data']
log.debug(rtn)
return rtn
class GraphQL(object):
def __init__(self, query=None, doc_id=None, params={}):
if query is not None:
self.value = {
'priority': 0,
'q': query,
'query_params': params
}
elif doc_id is not None:
self.value = {
'doc_id': doc_id,
'query_params': params
}
else:
raise Exception('A query or doc_id must be specified')
FRAGMENT_USER = """
QueryFragment User: User {
id,
name,
first_name,
last_name,
profile_picture.width(<pic_size>).height(<pic_size>) {
uri
},
is_viewer_friend,
url,
gender,
viewer_affinity
}
"""
FRAGMENT_GROUP = """
QueryFragment Group: MessageThread {
name,
thread_key {
thread_fbid
},
image {
uri
},
is_group_thread,
all_participants {
nodes {
messaging_actor {
id
}
}
},
customization_info {
participant_customizations {
participant_id,
nickname
},
outgoing_bubble_color,
emoji
}
}
"""
FRAGMENT_PAGE = """
QueryFragment Page: Page {
id,
name,
profile_picture.width(32).height(32) {
uri
},
url,
category_type,
city {
name
}
}
"""
SEARCH_USER = """
Query SearchUser(<search> = '', <limit> = 1) {
entities_named(<search>) {
search_results.of_type(user).first(<limit>) as users {
nodes {
@User
}
}
}
}
""" + FRAGMENT_USER
SEARCH_GROUP = """
Query SearchGroup(<search> = '', <limit> = 1, <pic_size> = 32) {
viewer() {
message_threads.with_thread_name(<search>).last(<limit>) as groups {
nodes {
@Group
}
}
}
}
""" + FRAGMENT_GROUP
SEARCH_PAGE = """
Query SearchPage(<search> = '', <limit> = 1) {
entities_named(<search>) {
search_results.of_type(page).first(<limit>) as pages {
nodes {
@Page
}
}
}
}
""" + FRAGMENT_PAGE
SEARCH_THREAD = """
Query SearchThread(<search> = '', <limit> = 1) {
entities_named(<search>) {
search_results.first(<limit>) as threads {
nodes {
__typename,
@User,
@Group,
@Page
}
}
}
}
""" + FRAGMENT_USER + FRAGMENT_GROUP + FRAGMENT_PAGE

View File

@@ -1,31 +1,232 @@
from __future__ import unicode_literals # -*- coding: UTF-8 -*-
import sys
from __future__ import unicode_literals
import enum
class Thread(object):
#: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info
uid = str
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info
type = None
#: The thread's picture
photo = str
#: The name of the thread
name = str
#: Timestamp of last message
last_message_timestamp = str
#: Number of messages in the thread
message_count = int
def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=None):
"""Represents a Facebook thread"""
self.uid = str(uid)
self.type = _type
self.photo = photo
self.name = name
self.last_message_timestamp = last_message_timestamp
self.message_count = message_count
class Base():
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 '<{} {} ({})>'.format(self.type.name, self.name, self.uid)
class User(Base):
def __init__(self, data):
if data['type'] != 'user':
raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path']))
self.uid = data['uid']
self.type = data['type']
self.photo = data['photo']
self.url = data['path']
self.name = data['text']
self.score = data['score']
self.data = data class User(Thread):
#: The profile url
url = str
#: The users first name
first_name = str
#: The users last name
last_name = str
#: Whether the user and the client are friends
is_friend = bool
#: The user's gender
gender = str
#: From 0 to 1. How close the client is to the user
affinity = float
#: The user's nickname
nickname = str
#: The clients nickname, as seen by the user
own_nickname = str
#: A :class:`ThreadColor`. The message color
color = None
#: The default emoji
emoji = str
class Thread(): def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs):
def __init__(self, **entries): """Represents a Facebook user. Inherits `Thread`"""
self.__dict__.update(entries) super(User, self).__init__(ThreadType.USER, uid, **kwargs)
self.url = url
self.first_name = first_name
self.last_name = last_name
self.is_friend = is_friend
self.gender = gender
self.affinity = affinity
self.nickname = nickname
self.own_nickname = own_nickname
self.color = color
self.emoji = emoji
class Message():
def __init__(self, **entries): class Group(Thread):
self.__dict__.update(entries) #: Unique list (set) of the group thread's participant user IDs
participants = set
#: Dict, containing user nicknames mapped to their IDs
nicknames = dict
#: A :class:`ThreadColor`. The groups's message color
color = None
#: The groups's default emoji
emoji = str
def __init__(self, uid, participants=set(), nicknames=[], color=None, emoji=None, **kwargs):
"""Represents a Facebook group. Inherits `Thread`"""
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
self.participants = participants
self.nicknames = nicknames
self.color = color
self.emoji = emoji
class Page(Thread):
#: The page's custom url
url = str
#: The name of the page's location city
city = str
#: Amount of likes the page has
likes = int
#: Some extra information about the page
sub_title = str
#: The page's category
category = str
def __init__(self, uid, url=None, city=None, likes=None, sub_title=None, category=None, **kwargs):
"""Represents a Facebook page. Inherits `Thread`"""
super(Page, self).__init__(ThreadType.PAGE, uid, **kwargs)
self.url = url
self.city = city
self.likes = likes
self.sub_title = sub_title
self.category = category
class Message(object):
#: The message ID
uid = str
#: ID of the sender
author = int
#: Timestamp of when the message was sent
timestamp = str
#: Whether the message is read
is_read = bool
#: A list of message reactions
reactions = list
#: The actual message
text = str
#: A list of :class:`Mention` objects
mentions = list
#: An ID of a sent sticker
sticker = str
#: A list of attachments
attachments = list
#: An extensible attachment, e.g. share object
extensible_attachment = dict
def __init__(self, uid, author=None, timestamp=None, is_read=None, reactions=[], text=None, mentions=[], sticker=None, attachments=[], extensible_attachment={}):
"""Represents a Facebook message"""
self.uid = uid
self.author = author
self.timestamp = timestamp
self.is_read = is_read
self.reactions = reactions
self.text = text
self.mentions = mentions
self.sticker = sticker
self.attachments = attachments
self.extensible_attachment = extensible_attachment
class Mention(object):
#: The user ID the mention is pointing at
user_id = str
#: The character where the mention starts
offset = int
#: The length of the mention
length = int
def __init__(self, user_id, offset=0, length=10):
"""Represents a @mention"""
self.user_id = user_id
self.offset = offset
self.length = length
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_threads` for more info"""
USER = 1
GROUP = 2
PAGE = 3
class TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing"""
STOPPED = 0
TYPING = 1
class EmojiSize(Enum):
"""Used to specify the size of a sent emoji"""
LARGE = '369239383222810'
MEDIUM = '369239343222814'
SMALL = '369239263222822'
class ThreadColor(Enum):
"""Used to specify a thread colors"""
MESSENGER_BLUE = ''
VIKING = '#44bec7'
GOLDEN_POPPY = '#ffc300'
RADICAL_RED = '#fa3c4c'
SHOCKING = '#d696bb'
PICTON_BLUE = '#6699cc'
FREE_SPEECH_GREEN = '#13cf13'
PUMPKIN = '#ff7e29'
LIGHT_CORAL = '#e68585'
MEDIUM_SLATE_BLUE = '#7646ff'
DEEP_SKY_BLUE = '#20cef5'
FERN = '#67b868'
CAMEO = '#d4a88c'
BRILLIANT_ROSE = '#ff5ca1'
BILOBA_FLOWER = '#a695c7'
class MessageReaction(Enum):
"""Used to specify a message reaction"""
LOVE = '😍'
SMILE = '😆'
WOW = '😮'
SAD = '😢'
ANGRY = '😠'
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'),
'😮': ('0001f62e', '%F0%9F%98%AE'),
'😢': ('0001f622', '%F0%9F%98%A2'),
'😠': ('0001f620', '%F0%9F%98%A0'),
'👍': ('0001f44d', '%F0%9F%91%8D'),
'👎': ('0001f44e', '%F0%9F%91%8E')
}

View File

@@ -1,8 +0,0 @@
LIKES={
'l': '369239383222810',
'm': '369239343222814',
's': '369239263222822'
}
LIKES['large'] = LIKES['l']
LIKES['medium'] =LIKES['m']
LIKES['small'] = LIKES['s']

View File

@@ -1,7 +1,28 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import re import re
import json import json
from time import time from time import time
from random import random from random import random
import warnings
import logging
from .models import *
# Python 2's `input` executes the input, whereas `raw_input` just returns the input
try:
input = raw_input
except NameError:
pass
# Log settings
log = logging.getLogger("client")
log.setLevel(logging.DEBUG)
# Creates the console handler
handler = logging.StreamHandler()
log.addHandler(handler)
#: Default list of user agents
USER_AGENTS = [ 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",
@@ -10,7 +31,9 @@ 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 = {
# For standard requests
0: 'unknown', 0: 'unknown',
1: 'female_singular', 1: 'female_singular',
2: 'male_singular', 2: 'male_singular',
@@ -23,18 +46,75 @@ GENDERS = {
9: 'male_plural', 9: 'male_plural',
10: 'neuter_plural', 10: 'neuter_plural',
11: 'unknown_plural', 11: 'unknown_plural',
# For graphql requests
'UNKNOWN': 'unknown',
'FEMALE': 'female_singular',
'MALE': 'male_singular',
#'': 'female_singular_guess',
#'': 'male_singular_guess',
#'': 'mixed',
#'': 'neuter_singular',
#'': 'unknown_singular',
#'': 'female_plural',
#'': 'male_plural',
#'': 'neuter_plural',
#'': 'unknown_plural',
None: None
} }
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/"
THREAD_SYNC = "https://www.facebook.com/ajax/mercury/thread_sync.php"
THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php"
MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php"
READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php"
DELIVERED = "https://www.facebook.com/ajax/mercury/delivery_receipts.php"
MARK_SEEN = "https://www.facebook.com/ajax/mercury/mark_seen.php"
BASE = "https://www.facebook.com"
MOBILE = "https://m.facebook.com/"
STICKY = "https://0-edge-chat.facebook.com/pull"
PING = "https://0-edge-chat.facebook.com/active_ping"
UPLOAD = "https://upload.facebook.com/ajax/mercury/upload.php"
INFO = "https://www.facebook.com/chat/user_info/"
CONNECT = "https://www.facebook.com/ajax/add_friend/action.php?dpr=1"
REMOVE_USER = "https://www.facebook.com/chat/remove_participants/"
LOGOUT = "https://www.facebook.com/logout.php"
ALL_USERS = "https://www.facebook.com/chat/user_info_all"
SAVE_DEVICE = "https://m.facebook.com/login/save-device/cancel/"
CHECKPOINT = "https://m.facebook.com/login/checkpoint/"
THREAD_COLOR = "https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1"
THREAD_NICKNAME = "https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1"
THREAD_EMOJI = "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&dpr=1"
MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation"
TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
facebookEncoding = 'UTF-8'
def now(): def now():
return int(time()*1000) return int(time()*1000)
def strip_to_json(text): def strip_to_json(text):
try:
return text[text.index('{'):] return text[text.index('{'):]
except ValueError:
raise Exception('No JSON object found: {}, {}'.format(repr(text), text.index('{')))
def get_json(text): def get_decoded_r(r):
return json.loads(strip_to_json(text)) return get_decoded(r._content)
def digit_to_char(digit): def get_decoded(content):
return content.decode(facebookEncoding)
def get_json(r):
return json.loads(strip_to_json(get_decoded_r(r)))
def digitToChar(digit):
if digit < 10: if digit < 10:
return str(digit) return str(digit)
return chr(ord('a') + digit - 10) return chr(ord('a') + digit - 10)
@@ -44,21 +124,50 @@ def str_base(number,base):
return '-' + str_base(-number, base) return '-' + str_base(-number, base)
(d, m) = divmod(number, base) (d, m) = divmod(number, base)
if d > 0: if d > 0:
return str_base(d, base) + digit_to_char(m) return str_base(d, base) + digitToChar(m)
return digit_to_char(m) return digitToChar(m)
def generateMessageID(client_id=None): def generateMessageID(client_id=None):
k = now() k = now()
l = int(random() * 4294967295) l = int(random() * 4294967295)
return ("<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id)); return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id)
def getSignatureID(): def getSignatureID():
return hex(int(random() * 2147483648)) return hex(int(random() * 2147483648))
def generateOfflineThreadingID(): def generateOfflineThreadingID():
ret = now() ret = now()
value = int(random() * 4294967295); value = int(random() * 4294967295)
string = ("0000000000000000000000" + bin(value))[-22:] string = ("0000000000000000000000" + format(value, 'b'))[-22:]
msgs = bin(ret) + string msgs = format(ret, 'b') + string
return str(int(msgs, 2)) return str(int(msgs, 2))
def check_json(j):
if 'error' in j and j['error'] is not None:
if 'errorDescription' in j:
# 'errorDescription' is in the users own language!
raise Exception('Error #{} when sending request: {}'.format(j['error'], j['errorDescription']))
elif 'debug_info' in j['error']:
raise Exception('Error #{} when sending request: {}'.format(j['error']['code'], repr(j['error']['debug_info'])))
else:
raise Exception('Error {} when sending request'.format(j['error']))
def checkRequest(r, do_json_check=True):
if not r.ok:
raise Exception('Error when sending request: Got {} response'.format(r.status_code))
content = get_decoded_r(r)
if content is None or len(content) == 0:
raise Exception('Error when sending request: Got empty response')
if do_json_check:
content = strip_to_json(content)
try:
j = json.loads(content)
except Exception as e:
raise Exception('Error while parsing JSON: {}'.format(repr(content)), e)
check_json(j)
return j
else:
return content

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
requests
lxml
beautifulsoup4
enum34

View File

@@ -16,11 +16,16 @@ except ImportError:
with open('README.rst') as f: with open('README.rst') as f:
readme_content = f.read().strip() readme_content = f.read().strip()
try:
requirements = [line.rstrip('\n') for line in open(os.path.join('fbchat.egg-info', 'requires.txt'))]
except IOError:
requirements = [line.rstrip('\n') for line in open('requirements.txt')]
version = None 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 +36,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 +47,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',
@@ -67,11 +74,7 @@ setup(
], ],
include_package_data=True, include_package_data=True,
packages=['fbchat'], packages=['fbchat'],
install_requires=[ install_requires=requirements,
'requests',
'lxml',
'beautifulsoup4'
],
url=source, url=source,
version=version, version=version,
zip_safe=True, zip_safe=True,

258
tests.py
View File

@@ -1,134 +1,205 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import json
import logging import logging
import fbchat
import getpass
import unittest import unittest
import sys from getpass import getpass
from os import path from sys import argv
from os import path, chdir
from glob import glob
from fbchat import Client
from fbchat.models import *
import py_compile
# Disable logging logging_level = logging.ERROR
logging.basicConfig(level=100)
fbchat.log.setLevel(100)
""" """
Tests for fbchat Testing script for `fbchat`.
~~~~~~~~~~~~~~~~ Full documentation on https://fbchat.readthedocs.io/
To use these tests, put:
- email
- password
- a group_uid
- a user_uid (the user will be kicked from the group and then added again)
(seperated these by a newline) in a file called `tests.data`, or type them manually in the terminal prompts
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
""" """
class CustomClient(Client):
def __init__(self, *args, **kwargs):
self.got_qprimer = False
super(type(self), self).__init__(*args, **kwargs)
def onQprimer(self, msg, **kwargs):
self.got_qprimer = True
class TestFbchat(unittest.TestCase): class TestFbchat(unittest.TestCase):
def test_login_functions(self): def test_examples(self):
self.assertTrue(client.is_logged_in()) # Checks for syntax errors in the examples
chdir('examples')
for f in glob('*.txt'):
print(f)
with self.assertRaises(py_compile.PyCompileError):
py_compile.compile(f)
chdir('..')
def test_loginFunctions(self):
self.assertTrue(client.isLoggedIn())
client.logout() client.logout()
self.assertFalse(client.is_logged_in()) 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_tries=1)
client.login(email, password) client.login(email, password)
self.assertTrue(client.is_logged_in()) self.assertTrue(client.isLoggedIn())
def test_sessions(self): def test_sessions(self):
global client global client
session_cookies = client.getSession() session_cookies = client.getSession()
client = fbchat.Client(email, password, session_cookies=session_cookies) client = CustomClient(email, password, session_cookies=session_cookies, logging_level=logging_level)
self.assertTrue(client.is_logged_in()) self.assertTrue(client.isLoggedIn())
def test_setDefaultRecipient(self): def test_defaultThread(self):
client.setDefaultRecipient(client.uid, is_user=True) # setDefaultThread
self.assertTrue(client.send(message="test_default_recipient")) client.setDefaultThread(group_id, ThreadType.GROUP)
self.assertTrue(client.sendMessage('test_default_recipient★'))
def test_getAllUsers(self): client.setDefaultThread(user_id, ThreadType.USER)
users = client.getAllUsers() self.assertTrue(client.sendMessage('test_default_recipient★'))
# resetDefaultThread
client.resetDefaultThread()
with self.assertRaises(ValueError):
client.sendMessage('should_not_send')
def test_fetchAllUsers(self):
users = client.fetchAllUsers()
self.assertGreater(len(users), 0) self.assertGreater(len(users), 0)
def test_getUsers(self): def test_searchFor(self):
users = client.getUsers("Mark Zuckerberg") users = client.searchForUsers('Mark Zuckerberg')
self.assertGreater(len(users), 0) self.assertGreater(len(users), 0)
u = users[0] u = users[0]
# Test if values are set correctly # Test if values are set correctly
self.assertIsInstance(u.uid, int) self.assertEqual(u.uid, '4')
self.assertEquals(u.type, 'user') self.assertEqual(u.type, ThreadType.USER)
self.assertEquals(u.photo[:4], 'http') self.assertEqual(u.photo[:4], 'http')
self.assertEquals(u.url[:4], 'http') self.assertEqual(u.url[:4], 'http')
self.assertEquals(u.name, 'Mark Zuckerberg') self.assertEqual(u.name, 'Mark Zuckerberg')
self.assertGreater(u.score, 0)
def test_send_likes(self): group_name = client.changeThreadTitle('tést_searchFor', thread_id=group_id, thread_type=ThreadType.GROUP)
self.assertTrue(client.send(client.uid, like='s')) groups = client.searchForGroups('')
self.assertTrue(client.send(client.uid, like='m')) self.assertGreater(len(groups), 0)
self.assertTrue(client.send(client.uid, like='l'))
self.assertTrue(client.send(group_uid, like='s', is_user=False))
self.assertTrue(client.send(group_uid, like='m', is_user=False))
self.assertTrue(client.send(group_uid, like='l', is_user=False))
def test_send(self): def test_sendEmoji(self):
self.assertTrue(client.send(client.uid, message='test_send_user')) self.assertIsNotNone(client.sendEmoji(size=EmojiSize.SMALL, thread_id=user_id, thread_type=ThreadType.USER))
self.assertTrue(client.send(group_uid, message='test_send_group', is_user=False)) self.assertIsNotNone(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=user_id, thread_type=ThreadType.USER))
self.assertIsNotNone(client.sendEmoji('😆', EmojiSize.LARGE, user_id, ThreadType.USER))
def test_send_images(self): self.assertIsNotNone(client.sendEmoji(size=EmojiSize.SMALL, thread_id=group_id, thread_type=ThreadType.GROUP))
self.assertIsNotNone(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=group_id, thread_type=ThreadType.GROUP))
self.assertIsNotNone(client.sendEmoji('😆', EmojiSize.LARGE, group_id, ThreadType.GROUP))
def test_sendMessage(self):
self.assertIsNotNone(client.sendMessage('test_send_user★', user_id, ThreadType.USER))
self.assertIsNotNone(client.sendMessage('test_send_group★', group_id, ThreadType.GROUP))
with self.assertRaises(Exception):
client.sendMessage('test_send_user_should_fail★', user_id, ThreadType.GROUP)
with self.assertRaises(Exception):
client.sendMessage('test_send_group_should_fail★', group_id, ThreadType.USER)
def test_sendImages(self):
image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png' image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png'
image_local_url = path.join(path.dirname(__file__), 'test_image.png') image_local_url = path.join(path.dirname(__file__), 'tests/image.png')
self.assertTrue(client.sendRemoteImage(client.uid, message='test_send_user_images_remote', image=image_url)) self.assertTrue(client.sendRemoteImage(image_url, 'test_send_user_images_remote', user_id, ThreadType.USER))
self.assertTrue(client.sendLocalImage(client.uid, message='test_send_user_images_local', image=image_local_url)) self.assertTrue(client.sendRemoteImage(image_url, 'test_send_group_images_remote★', group_id, ThreadType.GROUP))
self.assertTrue(client.sendRemoteImage(group_uid, message='test_send_group_images_remote', is_user=False, image=image_url)) self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', user_id, ThreadType.USER))
self.assertTrue(client.sendLocalImage(group_uid, message='test_send_group_images_local', is_user=False, image=image_local_url)) self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', group_id, ThreadType.GROUP))
def test_getThreadInfo(self): def test_fetchThreadList(self):
info = client.getThreadInfo(client.uid, last_n=1) client.fetchThreadList(offset=0, limit=20)
self.assertEquals(info[0].author, 'fbid:' + str(client.uid))
client.send(group_uid, message='test_getThreadInfo', is_user=False)
info = client.getThreadInfo(group_uid, last_n=1, is_user=False)
self.assertEquals(info[0].author, 'fbid:' + str(client.uid))
self.assertEquals(info[0].body, 'test_getThreadInfo')
def test_markAs(self): def test_fetchThreadMessages(self):
# To be implemented (requires some form of manual watching) client.sendMessage('test_user_getThreadInfo★', thread_id=user_id, thread_type=ThreadType.USER)
pass
messages = client.fetchThreadMessages(thread_id=user_id, limit=1)
self.assertEqual(messages[0].author, client.uid)
self.assertEqual(messages[0].text, 'test_user_getThreadInfo★')
client.sendMessage('test_group_getThreadInfo★', thread_id=group_id, thread_type=ThreadType.GROUP)
messages = client.fetchThreadMessages(thread_id=group_id, limit=1)
self.assertEqual(messages[0].author, client.uid)
self.assertEqual(messages[0].text, 'test_group_getThreadInfo★')
def test_listen(self): def test_listen(self):
client.do_one_listen() client.startListening()
client.doOneListen()
client.stopListening()
def test_getUserInfo(self): self.assertTrue(client.got_qprimer)
info = client.getUserInfo(4)
self.assertEquals(info['name'], 'Mark Zuckerberg')
def test_remove_add_from_chat(self): def test_fetchInfo(self):
self.assertTrue(client.remove_user_from_chat(group_uid, user_uid)) info = client.fetchUserInfo('4')['4']
self.assertTrue(client.add_users_to_chat(group_uid, user_uid)) self.assertEqual(info.name, 'Mark Zuckerberg')
info = client.fetchGroupInfo(group_id)[group_id]
self.assertEqual(info.type, ThreadType.GROUP)
def test_removeAddFromGroup(self):
client.removeUserFromGroup(user_id, thread_id=group_id)
client.addUsersToGroup(user_id, thread_id=group_id)
def test_changeThreadTitle(self): def test_changeThreadTitle(self):
self.assertTrue(client.changeThreadTitle(group_uid, 'test_changeThreadTitle')) client.changeThreadTitle('test_changeThreadTitle', thread_id=group_id, thread_type=ThreadType.GROUP)
client.changeThreadTitle('test_changeThreadTitle★', thread_id=user_id, thread_type=ThreadType.USER)
def test_changeNickname(self):
client.changeNickname('test_changeNicknameSelf★', client.uid, thread_id=user_id, thread_type=ThreadType.USER)
client.changeNickname('test_changeNicknameOther★', user_id, thread_id=user_id, thread_type=ThreadType.USER)
client.changeNickname('test_changeNicknameSelf★', client.uid, thread_id=group_id, thread_type=ThreadType.GROUP)
client.changeNickname('test_changeNicknameOther★', user_id, thread_id=group_id, thread_type=ThreadType.GROUP)
def test_changeThreadEmoji(self):
client.changeThreadEmoji('😀', group_id)
client.changeThreadEmoji('😀', user_id)
client.changeThreadEmoji('😆', group_id)
client.changeThreadEmoji('😆', user_id)
def test_changeThreadColor(self):
client.changeThreadColor(ThreadColor.BRILLIANT_ROSE, group_id)
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, group_id)
client.changeThreadColor(ThreadColor.BRILLIANT_ROSE, user_id)
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, user_id)
def test_reactToMessage(self):
mid = client.sendMessage('test_reactToMessage★', user_id, ThreadType.USER)
client.reactToMessage(mid, MessageReaction.LOVE)
mid = client.sendMessage('test_reactToMessage★', group_id, ThreadType.GROUP)
client.reactToMessage(mid, MessageReaction.LOVE)
def test_setTypingStatus(self):
client.setTypingStatus(TypingStatus.TYPING, thread_id=user_id, thread_type=ThreadType.USER)
client.setTypingStatus(TypingStatus.STOPPED, thread_id=user_id, thread_type=ThreadType.USER)
client.setTypingStatus(TypingStatus.TYPING, thread_id=group_id, thread_type=ThreadType.GROUP)
client.setTypingStatus(TypingStatus.STOPPED, thread_id=group_id, thread_type=ThreadType.GROUP)
def start_test(param_client, param_group_uid, param_user_uid, tests=[]): def start_test(param_client, param_group_id, param_user_id, tests=[]):
global client global client
global group_uid global group_id
global user_uid global user_id
client = param_client client = param_client
group_uid = param_group_uid group_id = param_group_id
user_uid = param_user_uid user_id = param_user_id
tests = ['test_' + test if 'test_' != test[:5] else test for test in tests]
if len(tests) == 0: if len(tests) == 0:
suite = unittest.TestLoader().loadTestsFromTestCase(TestFbchat) suite = unittest.TestLoader().loadTestsFromTestCase(TestFbchat)
@@ -138,6 +209,7 @@ def start_test(param_client, param_group_uid, param_user_uid, tests=[]):
unittest.TextTestRunner(verbosity=2).run(suite) unittest.TextTestRunner(verbosity=2).run(suite)
client = None
if __name__ == '__main__': if __name__ == '__main__':
# Python 3 does not use raw_input, whereas Python 2 does # Python 3 does not use raw_input, whereas Python 2 does
@@ -147,22 +219,20 @@ if __name__ == '__main__':
pass pass
try: try:
with open(path.join(path.dirname(__file__), 'tests.data'), 'r') as f: with open(path.join(path.dirname(__file__), 'tests/my_data.json'), 'r') as f:
content = f.readlines() json = json.load(f)
content = [x.strip() for x in content if len(x.strip()) != 0] email = json['email']
email = content[0] password = json['password']
password = content[1] user_id = json['user_thread_id']
group_uid = content[2] group_id = json['group_thread_id']
user_uid = content[3]
except (IOError, IndexError) as e: except (IOError, IndexError) as e:
email = input('Email: ') email = input('Email: ')
password = getpass.getpass() password = getpass()
group_uid = input('Please enter a group uid (To test group functionality): ') group_id = input('Please enter a group thread id (To test group functionality): ')
user_uid = input('Please enter a user uid (To test kicking/adding functionality): ') user_id = input('Please enter a user thread id (To test kicking/adding functionality): ')
print ('Logging in') print('Logging in...')
client = fbchat.Client(email, password) client = CustomClient(email, password, logging_level=logging_level)
# Warning! Taking user input directly like this could be dangerous! Use only for testing purposes! # Warning! Taking user input directly like this could be dangerous! Use only for testing purposes!
start_test(client, group_uid, user_uid, sys.argv[1:]) start_test(client, group_id, user_id, argv[1:])

6
tests/data.json Normal file
View File

@@ -0,0 +1,6 @@
{
"email": "",
"password": "",
"user_thread_id": "",
"group_thread_id": ""
}

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB