Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
769b034d38 | |||
fd3d5f7301 | |||
2fa1b58336 | |||
9523350dc5 | |||
356db553b7 | |||
55712756d7 | |||
|
916a14062d | ||
|
43aa16c32d | ||
|
427ae6bc5e | ||
|
d650946531 | ||
|
8ac6dc4ae6 | ||
|
a6cf1d5c89 | ||
|
65b42e6532 | ||
|
8824a1c253 | ||
|
520258e339 | ||
|
435dfaf6d8 | ||
|
cf0e1e3a93 | ||
|
2319fc7c4a | ||
|
b35240bdda | ||
|
6141cc5a41 | ||
|
b1e438dae1 | ||
|
3c0f411be7 | ||
|
9ad0090b02 | ||
|
bec151a560 | ||
|
2087182ecf | ||
|
09627b71ae | ||
|
078bf9fc16 | ||
|
d33e36866d | ||
|
2a382ffaed | ||
|
18a3ffb90d |
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report if you're having trouble with `fbchat`
|
||||
|
||||
---
|
||||
|
||||
## Description of the problem
|
||||
Example: Logging in fails when the character `%` is in the password. A specific password that fails is `a_password_with_%`
|
||||
|
||||
## Code to reproduce
|
||||
```py
|
||||
# Example code
|
||||
from fbchat import Client
|
||||
client = Client("[REDACTED_USERNAME]", "a_password_with_%")
|
||||
```
|
||||
|
||||
## Traceback
|
||||
```
|
||||
Traceback (most recent call last):
|
||||
File "<test.py>", line 1, in <module>
|
||||
File "[site-packages]/fbchat/client.py", line 78, in __init__
|
||||
self.login(email, password, max_tries)
|
||||
File "[site-packages]/fbchat/client.py", line 407, in login
|
||||
raise FBchatException('Login failed. Check email/password. (Failed on URL: {})'.format(login_url))
|
||||
fbchat.FBchatException: Login failed. Check email/password. (Failed on URL: https://m.facebook.com/login.php?login_attempt=1)
|
||||
```
|
||||
|
||||
## Environment information
|
||||
- Python version
|
||||
- `fbchat` version
|
||||
- If relevant, output from `$ python -m pip list`
|
||||
|
||||
If you have done any research, include that.
|
||||
Make sure to redact all personal information.
|
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature that you'd like to see implemented
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
Example: There's no way to send messages to groups
|
||||
|
||||
## Research (if applicable)
|
||||
Example: I've found the URL `https://facebook.com/send_message.php`, to which you can send a POST requests with the following JSON:
|
||||
```json
|
||||
{
|
||||
"text": message_content,
|
||||
"fbid": group_id,
|
||||
"some_variable": ?
|
||||
}
|
||||
```
|
||||
But I don't know how what `some_variable` does, and it doesn't work without it. I've found some examples of `some_variable` to be: `MTIzNDU2Nzg5MA`, `MTIzNDU2Nzg5MQ` and `MTIzNDU2Nzg5Mg`
|
@@ -1,20 +0,0 @@
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
version: 2
|
||||
|
||||
formats:
|
||||
- pdf
|
||||
- htmlzip
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
install:
|
||||
- path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
# Disabled, until we can find a way to get sphinx-autodoc-typehints play nice with our
|
||||
# module renaming!
|
||||
fail_on_warning: false
|
53
.travis.yml
53
.travis.yml
@@ -1,53 +0,0 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python: 3.6
|
||||
|
||||
cache: pip
|
||||
|
||||
before_install: pip install flit
|
||||
# Use `--deps production` so that we don't install unnecessary dependencies
|
||||
install: flit install --deps production --extras test
|
||||
script: pytest
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- python: 3.5
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
- python: pypy3.5
|
||||
|
||||
- name: Lint
|
||||
before_install: skip
|
||||
install: pip install black
|
||||
script: black --check --verbose .
|
||||
|
||||
- stage: deploy
|
||||
name: GitHub Releases
|
||||
if: tag IS present
|
||||
install: skip
|
||||
script: flit build
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUB_OAUTH_TOKEN
|
||||
file_glob: true
|
||||
file: dist/*
|
||||
skip_cleanup: true
|
||||
draft: false
|
||||
on:
|
||||
tags: true
|
||||
|
||||
- stage: deploy
|
||||
name: PyPI
|
||||
if: tag IS present
|
||||
install: skip
|
||||
script: skip
|
||||
deploy:
|
||||
provider: script
|
||||
script: flit publish
|
||||
on:
|
||||
tags: true
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: change
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"esbonio.sphinx.confDir": "",
|
||||
"python.formatting.provider": "autopep8"
|
||||
}
|
71
README.rst
71
README.rst
@@ -1,30 +1,6 @@
|
||||
``fbchat`` - Facebook Messenger for Python
|
||||
==========================================
|
||||
|
||||
.. image:: https://badgen.net/pypi/v/fbchat
|
||||
:target: https://pypi.python.org/pypi/fbchat
|
||||
:alt: Project version
|
||||
|
||||
.. image:: https://badgen.net/badge/python/3.5,3.6,3.7,3.8,pypy?list=|
|
||||
:target: https://pypi.python.org/pypi/fbchat
|
||||
:alt: Supported python versions: 3.5, 3.6, 3.7, 3.8 and pypy
|
||||
|
||||
.. image:: https://badgen.net/pypi/license/fbchat
|
||||
:target: https://github.com/carpedm20/fbchat/tree/master/LICENSE
|
||||
:alt: License: BSD 3-Clause
|
||||
|
||||
.. image:: https://readthedocs.org/projects/fbchat/badge/?version=stable
|
||||
:target: https://fbchat.readthedocs.io
|
||||
:alt: Documentation
|
||||
|
||||
.. image:: https://badgen.net/travis/carpedm20/fbchat
|
||||
:target: https://travis-ci.org/carpedm20/fbchat
|
||||
:alt: Travis CI
|
||||
|
||||
.. image:: https://badgen.net/badge/code%20style/black/black
|
||||
:target: https://github.com/ambv/black
|
||||
:alt: Code style
|
||||
|
||||
A powerful and efficient library to interact with
|
||||
`Facebook's Messenger <https://www.facebook.com/messages/>`__, using just your email and password.
|
||||
|
||||
@@ -38,16 +14,13 @@ This is *not* an official API, Facebook has that `over here <https://developers.
|
||||
- Creating groups, setting the group emoji, changing nicknames, creating polls, etc.
|
||||
- Listening for, an reacting to messages and other events in real-time.
|
||||
- Type hints, and it has a modern codebase (e.g. only Python 3.5 and upwards).
|
||||
- ``async``/``await`` (COMING).
|
||||
|
||||
Essentially, everything you need to make an amazing Facebook bot!
|
||||
|
||||
|
||||
Version Warning
|
||||
---------------
|
||||
``v2`` is currently being developed at the ``master`` branch and it's highly unstable. If you want to view the old ``v1``, go `here <https://github.com/carpedm20/fbchat/tree/v1>`__.
|
||||
|
||||
Additionally, you can view the project's progress `here <https://github.com/carpedm20/fbchat/projects/2>`__.
|
||||
``v2`` is currently being developed at the ``master`` branch and it's highly unstable.
|
||||
|
||||
|
||||
Caveats
|
||||
@@ -58,14 +31,6 @@ Caveats
|
||||
However, there's a catch! **Using this library may not comply with Facebook's Terms Of Service!**, so be responsible Facebook citizens! We are not responsible if your account gets banned!
|
||||
|
||||
Additionally, **the APIs the library is calling is undocumented!** In theory, this means that your code could break tomorrow, without the slightest warning!
|
||||
If this happens to you, please report it, so that we can fix it as soon as possible!
|
||||
|
||||
.. inclusion-marker-intro-end
|
||||
.. This message doesn't make sense in the docs at Read The Docs, so we exclude it
|
||||
|
||||
With that out of the way, you may go to `Read The Docs <https://fbchat.readthedocs.io/>`__ to see the full documentation!
|
||||
|
||||
.. inclusion-marker-installation-start
|
||||
|
||||
|
||||
Installation
|
||||
@@ -73,40 +38,10 @@ Installation
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ pip install fbchat
|
||||
|
||||
If you don't have `pip <https://pip.pypa.io/>`_, `this guide <http://docs.python-guide.org/en/latest/starting/installation/>`_ can guide you through the process.
|
||||
|
||||
You can also install directly from source, provided you have ``pip>=19.0``:
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ pip install git+https://github.com/carpedm20/fbchat.git
|
||||
|
||||
.. inclusion-marker-installation-end
|
||||
|
||||
|
||||
Example Usage
|
||||
-------------
|
||||
|
||||
.. code-block::
|
||||
|
||||
import getpass
|
||||
import fbchat
|
||||
session = fbchat.Session.login("<email/phone number>", getpass.getpass())
|
||||
user = fbchat.User(session=session, id=session.user_id)
|
||||
user.send_text("Test message!")
|
||||
|
||||
More examples are available `here <https://github.com/carpedm20/fbchat/tree/master/examples>`__.
|
||||
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
- Mads Marquart / `@madsmtm <https://github.com/madsmtm>`__
|
||||
$ pip install git+https://git.karaolidis.com/karaolidis/fbchat.git
|
||||
|
||||
|
||||
Acknowledgements
|
||||
----------------
|
||||
|
||||
This project was originally inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__.
|
||||
This project is a fork of `fbchat <https://github.com/fbchat-dev/fbchat>`__ and was originally inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__.
|
||||
|
@@ -1,19 +0,0 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
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
BIN
docs/_static/find-group-id.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 59 KiB |
26
docs/_templates/layout.html
vendored
26
docs/_templates/layout.html
vendored
@@ -1,26 +0,0 @@
|
||||
{% 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
13
docs/_templates/sidebar.html
vendored
@@ -1,13 +0,0 @@
|
||||
<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() }}
|
@@ -1,13 +0,0 @@
|
||||
Attachments
|
||||
===========
|
||||
|
||||
.. autoclass:: Attachment()
|
||||
.. autoclass:: ShareAttachment()
|
||||
.. autoclass:: Sticker()
|
||||
.. autoclass:: LocationAttachment()
|
||||
.. autoclass:: LiveLocationAttachment()
|
||||
.. autoclass:: FileAttachment()
|
||||
.. autoclass:: AudioAttachment()
|
||||
.. autoclass:: ImageAttachment()
|
||||
.. autoclass:: VideoAttachment()
|
||||
.. autoclass:: ImageAttachment()
|
@@ -1,4 +0,0 @@
|
||||
Client
|
||||
======
|
||||
|
||||
.. autoclass:: Client
|
@@ -1,4 +0,0 @@
|
||||
Events
|
||||
======
|
||||
|
||||
.. autoclass:: Listener
|
@@ -1,11 +0,0 @@
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
.. autoexception:: FacebookError()
|
||||
.. autoexception:: HTTPError()
|
||||
.. autoexception:: ParseError()
|
||||
.. autoexception:: NotLoggedIn()
|
||||
.. autoexception:: ExternalError()
|
||||
.. autoexception:: GraphQLError()
|
||||
.. autoexception:: InvalidParameters()
|
||||
.. autoexception:: PleaseRefresh()
|
@@ -1,21 +0,0 @@
|
||||
.. module:: fbchat
|
||||
|
||||
.. Note: we're using () to hide the __init__ method where relevant
|
||||
|
||||
Full API
|
||||
========
|
||||
|
||||
If you are looking for information on a specific function, class, or method, this part of the documentation is for you.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
session
|
||||
client
|
||||
threads
|
||||
thread_data
|
||||
messages
|
||||
exceptions
|
||||
attachments
|
||||
events
|
||||
misc
|
@@ -1,8 +0,0 @@
|
||||
Messages
|
||||
========
|
||||
|
||||
.. autoclass:: Message
|
||||
.. autoclass:: Mention
|
||||
.. autoclass:: EmojiSize(Enum)
|
||||
:undoc-members:
|
||||
.. autoclass:: MessageData()
|
@@ -1,20 +0,0 @@
|
||||
Miscellaneous
|
||||
=============
|
||||
|
||||
.. autoclass:: ThreadLocation(Enum)
|
||||
:undoc-members:
|
||||
.. autoclass:: ActiveStatus()
|
||||
|
||||
.. autoclass:: QuickReply
|
||||
.. autoclass:: QuickReplyText
|
||||
.. autoclass:: QuickReplyLocation
|
||||
.. autoclass:: QuickReplyPhoneNumber
|
||||
.. autoclass:: QuickReplyEmail
|
||||
|
||||
.. autoclass:: Poll
|
||||
.. autoclass:: PollOption
|
||||
|
||||
.. autoclass:: Plan
|
||||
.. autoclass:: PlanData()
|
||||
.. autoclass:: GuestStatus(Enum)
|
||||
:undoc-members:
|
@@ -1,4 +0,0 @@
|
||||
Session
|
||||
=======
|
||||
|
||||
.. autoclass:: Session()
|
@@ -1,6 +0,0 @@
|
||||
Thread Data
|
||||
===========
|
||||
|
||||
.. autoclass:: PageData()
|
||||
.. autoclass:: UserData()
|
||||
.. autoclass:: GroupData()
|
@@ -1,8 +0,0 @@
|
||||
Threads
|
||||
=======
|
||||
|
||||
.. autoclass:: ThreadABC()
|
||||
.. autoclass:: Thread
|
||||
.. autoclass:: Page
|
||||
.. autoclass:: User
|
||||
.. autoclass:: Group
|
194
docs/conf.py
194
docs/conf.py
@@ -1,194 +0,0 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
os.environ["_FBCHAT_DISABLE_FIX_MODULE_METADATA"] = "1"
|
||||
|
||||
import fbchat
|
||||
|
||||
del os.environ["_FBCHAT_DISABLE_FIX_MODULE_METADATA"]
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = fbchat.__name__
|
||||
copyright = "Copyright 2015 - 2018 by Taehoon Kim and 2018 - 2020 by Mads Marquart"
|
||||
author = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart"
|
||||
description = fbchat.__doc__.split("\n")[0]
|
||||
|
||||
# The short X.Y version
|
||||
version = fbchat.__version__
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = fbchat.__version__
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = "2.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.viewcode",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinxcontrib.spelling",
|
||||
"sphinx_autodoc_typehints",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
rst_prolog = ".. currentmodule:: " + project
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#
|
||||
default_role = "any"
|
||||
|
||||
# Make the reference parsing more strict
|
||||
#
|
||||
nitpicky = True
|
||||
|
||||
# Prefer strict Python highlighting
|
||||
#
|
||||
highlight_language = "python3"
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#
|
||||
add_function_parentheses = False
|
||||
|
||||
|
||||
# -- 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 = {
|
||||
"show_powered_by": False,
|
||||
"github_user": "carpedm20",
|
||||
"github_repo": project,
|
||||
"github_banner": True,
|
||||
"show_related": False,
|
||||
}
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
html_sidebars = {"**": ["sidebar.html", "searchbox.html"]}
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#
|
||||
html_show_sphinx = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#
|
||||
html_show_sourcelink = False
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#
|
||||
html_short_title = description
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = project + "doc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
# 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", project, 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, project, [x.strip() for x in author.split(";")], 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, project, author, project, description, "Miscellaneous",)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ["search.html"]
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
# -- Options for autodoc extension ---------------------------------------
|
||||
|
||||
autoclass_content = "class"
|
||||
autodoc_member_order = "bysource"
|
||||
autodoc_default_options = {"members": True}
|
||||
|
||||
# -- Options for intersphinx extension ---------------------------------------
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {"https://docs.python.org/": None}
|
||||
|
||||
# -- Options for napoleon extension ----------------------------------------------
|
||||
|
||||
# Use Google style docstrings
|
||||
napoleon_google_docstring = True
|
||||
napoleon_numpy_docstring = False
|
||||
|
||||
# napoleon_use_admonition_for_examples = False
|
||||
# napoleon_use_admonition_for_notes = False
|
||||
# napoleon_use_admonition_for_references = False
|
||||
|
||||
# -- Options for spelling extension ----------------------------------------------
|
||||
|
||||
spelling_word_list_filename = [
|
||||
"spelling/names.txt",
|
||||
"spelling/technical.txt",
|
||||
"spelling/fixes.txt",
|
||||
]
|
||||
spelling_ignore_wiki_words = False
|
||||
# spelling_ignore_acronyms = False
|
||||
spelling_ignore_python_builtins = False
|
||||
spelling_ignore_importable_modules = False
|
@@ -1,55 +0,0 @@
|
||||
.. _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 interact 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
|
23
docs/faq.rst
23
docs/faq.rst
@@ -1,23 +0,0 @@
|
||||
Frequently Asked Questions
|
||||
==========================
|
||||
|
||||
The new version broke my application
|
||||
------------------------------------
|
||||
|
||||
``fbchat`` follows `Scemantic Versioning <https://semver.org/>`__ quite rigorously!
|
||||
|
||||
That means that breaking changes can *only* occur in major versions (e.g. ``v1.9.6`` -> ``v2.0.0``).
|
||||
|
||||
If you find that something breaks, and you didn't update to a new major version, then it is a bug, and we would be grateful if you reported it!
|
||||
|
||||
In case you're stuck with an old codebase, you can downgrade to a previous version of ``fbchat``, e.g. version ``1.9.6``:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ pip install fbchat==1.9.6
|
||||
|
||||
|
||||
Will you be supporting creating posts/events/pages and so on?
|
||||
-------------------------------------------------------------
|
||||
|
||||
We won't be focusing on anything else than chat-related things. This library is called ``fbCHAT``, after all!
|
@@ -1,23 +0,0 @@
|
||||
.. highlight:: sh
|
||||
.. See README.rst for explanation of these markers
|
||||
|
||||
.. include:: ../README.rst
|
||||
:end-before: inclusion-marker-intro-end
|
||||
|
||||
With that said, let's get started!
|
||||
|
||||
.. include:: ../README.rst
|
||||
:start-after: inclusion-marker-installation-start
|
||||
:end-before: inclusion-marker-installation-end
|
||||
|
||||
|
||||
Documentation Overview
|
||||
----------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
intro
|
||||
examples
|
||||
faq
|
||||
api/index
|
152
docs/intro.rst
152
docs/intro.rst
@@ -1,152 +0,0 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
Welcome, this page will guide you through the basic concepts of using ``fbchat``.
|
||||
|
||||
The hardest, and most error prone part is logging in, and managing your login session, so that is what we will look at first.
|
||||
|
||||
|
||||
Logging In
|
||||
----------
|
||||
|
||||
Everything in ``fbchat`` starts with getting an instance of `Session`. Currently there are two ways of doing that, `Session.login` and `Session.from_cookies`.
|
||||
|
||||
The follow example will prompt you for you password, and use it to login::
|
||||
|
||||
import getpass
|
||||
import fbchat
|
||||
session = fbchat.Session.login("<email/phone number>", getpass.getpass())
|
||||
# If your account requires a two factor authentication code:
|
||||
session = fbchat.Session.login(
|
||||
"<your email/phone number>",
|
||||
getpass.getpass(),
|
||||
lambda: getpass.getpass("2FA code"),
|
||||
)
|
||||
|
||||
However, **this is not something you should do often!** Logging in/out all the time *will* get your Facebook account locked!
|
||||
|
||||
Instead, you should start by using `Session.login`, and then store the cookies with `Session.get_cookies`, so that they can be used instead the next time your application starts.
|
||||
|
||||
Usability-wise, this is also better, since you won't have to re-type your password every time you want to login.
|
||||
|
||||
The following, quite lengthy, yet very import example, illustrates a way to do this:
|
||||
|
||||
.. literalinclude:: ../examples/session_handling.py
|
||||
|
||||
Assuming you have successfully completed the above, congratulations! Using ``fbchat`` should be mostly trouble free from now on!
|
||||
|
||||
|
||||
Understanding Thread Ids
|
||||
------------------------
|
||||
|
||||
At the core of any thread is its unique identifier, its ID.
|
||||
|
||||
A thread basically just means "something I can chat with", but more precisely, it can refer to a few things:
|
||||
- A Messenger group thread (`Group`)
|
||||
- The conversation between you and a single Facebook user (`User`)
|
||||
- The conversation between you and a Facebook Page (`Page`)
|
||||
|
||||
You can get your own user ID from `Session.user` with ``session.user.id``.
|
||||
|
||||
Getting the ID of a specific group thread is fairly trivial, you only need to login to `<https://www.messenger.com/>`_, 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.messenger.com/t/1234567890``, where ``1234567890`` would be the ID of the group.
|
||||
|
||||
The same method can be applied to some user accounts, though if they have set a custom URL, then you will have to use a different method.
|
||||
|
||||
An image to illustrate the process is shown below:
|
||||
|
||||
.. image:: /_static/find-group-id.png
|
||||
:alt: An image illustrating how to find the ID of a group
|
||||
|
||||
Once you have an ID, you can use it to create a `Group` or a `User` instance, which will allow you to do all sorts of things. To do this, you need a valid, logged in session::
|
||||
|
||||
group = fbchat.Group(session=session, id="<The id you found>")
|
||||
# Or for user threads
|
||||
user = fbchat.User(session=session, id="<The id you found>")
|
||||
|
||||
Just like threads, every message, poll, plan, attachment, action etc. you send or do on Facebook has a unique ID.
|
||||
|
||||
Below is an example of using such a message ID to get a `Message` instance::
|
||||
|
||||
# Provide the thread the message was created in, and it's ID
|
||||
message = fbchat.Message(thread=user, id="<The message id>")
|
||||
|
||||
|
||||
Fetching Information
|
||||
--------------------
|
||||
|
||||
Managing these ids yourself quickly becomes very cumbersome! Luckily, there are other, easier ways of getting `Group`/`User` instances.
|
||||
|
||||
You would start by creating a `Client` instance, which is basically just a helper on top of `Session`, that will allow you to do various things::
|
||||
|
||||
client = fbchat.Client(session=session)
|
||||
|
||||
Now, you could search for threads using `Client.search_for_threads`, or fetch a list of them using `Client.fetch_threads`::
|
||||
|
||||
# Fetch the 5 most likely search results
|
||||
# Uses Facebook's search functions, you don't have to specify the whole name, first names will usually be enough
|
||||
threads = list(client.search_for_threads("<name of the thread to search for>", limit=5))
|
||||
# Fetch the 5 most recent threads in your account
|
||||
threads = list(client.fetch_threads(limit=5))
|
||||
|
||||
Note the `list` statements; this is because the methods actually return `generators <https://wiki.python.org/moin/Generators>`__. If you don't know what that means, don't worry, it is just something you can use to make your code faster later.
|
||||
|
||||
The examples above will actually fetch `UserData`/`GroupData`, which are subclasses of `User`/`Group`. These model have extra properties, so you could for example print the names and ids of the fetched threads like this::
|
||||
|
||||
for thread in threads:
|
||||
print(f"{thread.id}: {thread.name}")
|
||||
|
||||
Once you have a thread, you can use that to fetch the messages therein::
|
||||
|
||||
for message in thread.fetch_messages(limit=20):
|
||||
print(message.text)
|
||||
|
||||
|
||||
Interacting with Threads
|
||||
------------------------
|
||||
|
||||
Once you have a `User`/`Group` instance, you can do things on them as described in `ThreadABC`, since they are subclasses of that.
|
||||
|
||||
Some functionality, like adding users to a `Group`, or blocking a `User`, logically only works the relevant threads, so see the full API documentation for that.
|
||||
|
||||
With that out of the way, let's see some examples!
|
||||
|
||||
The simplest way of interacting with a thread is by sending a message::
|
||||
|
||||
# Send a message to the user
|
||||
message = user.send_text("test message")
|
||||
|
||||
There are many types of messages you can send, see the full API documentation for more.
|
||||
|
||||
Notice how we held on to the sent message? The return type i a `Message` instance, so you can interact with it afterwards::
|
||||
|
||||
# React to the message with the 😍 emoji
|
||||
message.react("😍")
|
||||
|
||||
Besides sending messages, you can also interact with threads in other ways. An example is to change the thread color::
|
||||
|
||||
# Will change the thread color to the default blue
|
||||
thread.set_color("#0084ff")
|
||||
|
||||
|
||||
Listening & Events
|
||||
------------------
|
||||
|
||||
Now, we are finally at the point we have all been waiting for: Creating an automatic Facebook bot!
|
||||
|
||||
To get started, you create the functions you want to call on certain events::
|
||||
|
||||
def my_function(event: fbchat.MessageEvent):
|
||||
print(f"Message from {event.author.id}: {event.message.text}")
|
||||
|
||||
Then you create a `fbchat.Listener` object::
|
||||
|
||||
listener = fbchat.Listener(session=session, chat_on=False, foreground=False)
|
||||
|
||||
Which you can then use to receive events, and send them to your functions::
|
||||
|
||||
for event in listener.listen():
|
||||
if isinstance(event, fbchat.MessageEvent):
|
||||
my_function(event)
|
||||
|
||||
View the :ref:`examples` to see some more examples illustrating the event system.
|
@@ -1,35 +0,0 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the 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
|
@@ -1,3 +0,0 @@
|
||||
premade
|
||||
todo
|
||||
emoji
|
@@ -1,3 +0,0 @@
|
||||
Facebook
|
||||
GraphQL
|
||||
GitHub
|
@@ -1,17 +0,0 @@
|
||||
iterables
|
||||
iterable
|
||||
mimetype
|
||||
timestamp
|
||||
metadata
|
||||
spam
|
||||
spammy
|
||||
admin
|
||||
admins
|
||||
unsend
|
||||
unsends
|
||||
unmute
|
||||
spritemap
|
||||
online
|
||||
inbox
|
||||
subclassing
|
||||
codebase
|
@@ -2,7 +2,7 @@ import fbchat
|
||||
|
||||
session = fbchat.Session.login("<email>", "<password>")
|
||||
|
||||
client = fbchat.Client(session)
|
||||
client = fbchat.Client(session=session)
|
||||
|
||||
# Fetches a list of all users you're currently chatting with, as `User` objects
|
||||
users = client.fetch_all_users()
|
||||
|
@@ -60,7 +60,7 @@ thread.set_color("#0084ff")
|
||||
# Will change the thread emoji to `👍`
|
||||
thread.set_emoji("👍")
|
||||
|
||||
message = fbchat.Message(session=session, id="<message id>")
|
||||
message = fbchat.Message(thread=thread, id="<message id>")
|
||||
|
||||
# Will react to a message with a 😍 emoji
|
||||
message.react("😍")
|
||||
|
@@ -118,7 +118,7 @@ from ._listen import Listener
|
||||
|
||||
from ._client import Client
|
||||
|
||||
__version__ = "2.0.0a3"
|
||||
__version__ = "2.0.0a5"
|
||||
|
||||
__all__ = ("Session", "Listener", "Client")
|
||||
|
||||
|
@@ -419,7 +419,7 @@ class Client:
|
||||
Warning:
|
||||
This is not finished, and the API may change at any point!
|
||||
"""
|
||||
at = datetime.datetime.utcnow()
|
||||
at = _util.now()
|
||||
form = {
|
||||
"folders[0]": "inbox",
|
||||
"client": "mercury",
|
||||
@@ -558,7 +558,7 @@ class Client:
|
||||
"shouldSendReadReceipt": "true",
|
||||
}
|
||||
|
||||
for threads in threads:
|
||||
for thread in threads:
|
||||
data["ids[{}]".format(thread.id)] = "true" if read else "false"
|
||||
|
||||
j = self.session._payload_post("/ajax/mercury/change_read_status.php", data)
|
||||
|
@@ -124,11 +124,11 @@ def handle_graphql_errors(j):
|
||||
errors = j["errors"]
|
||||
if errors:
|
||||
error = errors[0] # TODO: Handle multiple errors
|
||||
# TODO: Use `severity` and `description`
|
||||
# TODO: Use `severity`
|
||||
raise GraphQLError(
|
||||
# TODO: What data is always available?
|
||||
message=error.get("summary", "Unknown error"),
|
||||
description=error.get("message", ""),
|
||||
description=error.get("message") or error.get("description") or "",
|
||||
code=error.get("code"),
|
||||
debug_info=error.get("debug_info"),
|
||||
)
|
||||
|
@@ -125,20 +125,12 @@ class Message:
|
||||
def react(self, reaction: Optional[str]):
|
||||
"""React to the message, or removes reaction.
|
||||
|
||||
Currently, you can use "❤", "😍", "😆", "😮", "😢", "😠", "👍" or "👎". It
|
||||
should be possible to add support for more, but we haven't figured that out yet.
|
||||
|
||||
Args:
|
||||
reaction: Reaction emoji to use, or if ``None``, removes reaction.
|
||||
|
||||
Example:
|
||||
>>> message.react("😍")
|
||||
"""
|
||||
if reaction and reaction not in SENDABLE_REACTIONS:
|
||||
raise ValueError(
|
||||
"Invalid reaction! Please use one of: {}".format(SENDABLE_REACTIONS)
|
||||
)
|
||||
|
||||
data = {
|
||||
"action": "ADD_REACTION" if reaction else "REMOVE_REACTION",
|
||||
"client_mutation_id": "1",
|
||||
|
@@ -15,7 +15,12 @@ from . import _graphql, _util, _exception
|
||||
from typing import Optional, Mapping, Callable, Any
|
||||
|
||||
|
||||
SERVER_JS_DEFINE_REGEX = re.compile(r'require\("ServerJSDefine"\)\)?\.handleDefines\(')
|
||||
SERVER_JS_DEFINE_REGEX = re.compile(
|
||||
r'(?:"ServerJS".{,100}\.handle\({.*"define":)'
|
||||
r'|(?:ServerJS.{,100}\.handleWithCustomApplyEach\(ScheduledApplyEach,{.*"define":)'
|
||||
r'|(?:require\("ServerJSDefine"\)\)?\.handleDefines\()'
|
||||
r'|(?:"require":\[\["ScheduledServerJS".{,100}"define":)'
|
||||
)
|
||||
SERVER_JS_DEFINE_JSON_DECODER = json.JSONDecoder()
|
||||
|
||||
|
||||
@@ -24,6 +29,8 @@ def parse_server_js_define(html: str) -> Mapping[str, Any]:
|
||||
# Find points where we should start parsing
|
||||
define_splits = SERVER_JS_DEFINE_REGEX.split(html)
|
||||
|
||||
# TODO: Extract jsmods "require" and "define" from `bigPipe.onPageletArrive`?
|
||||
|
||||
# Skip leading entry
|
||||
_, *define_splits = define_splits
|
||||
|
||||
@@ -32,8 +39,6 @@ def parse_server_js_define(html: str) -> Mapping[str, Any]:
|
||||
raise _exception.ParseError("Could not find any ServerJSDefine", data=html)
|
||||
if len(define_splits) < 2:
|
||||
raise _exception.ParseError("Could not find enough ServerJSDefine", data=html)
|
||||
if len(define_splits) > 2:
|
||||
raise _exception.ParseError("Found too many ServerJSDefine", data=define_splits)
|
||||
# Parse entries (should be two)
|
||||
for entry in define_splits:
|
||||
try:
|
||||
@@ -89,6 +94,12 @@ def session_factory() -> requests.Session:
|
||||
from . import __version__
|
||||
|
||||
session = requests.session()
|
||||
# Override Facebook's locale detection during the login process.
|
||||
# The locale is only used when giving errors back to the user, so giving the errors
|
||||
# back in English makes it easier for users to report.
|
||||
session.cookies = session.cookies = requests.cookies.merge_cookies(
|
||||
session.cookies, {"locale": "en_US"}
|
||||
)
|
||||
session.headers["Referer"] = "https://www.messenger.com/"
|
||||
# We won't try to set a fake user agent to mask our presence!
|
||||
# Facebook allows us access anyhow, and it makes our motives clearer:
|
||||
@@ -97,8 +108,12 @@ def session_factory() -> requests.Session:
|
||||
return session
|
||||
|
||||
|
||||
def login_cookies(at: datetime.datetime):
|
||||
return {"act": "{}/0".format(_util.datetime_to_millis(at))}
|
||||
|
||||
|
||||
def client_id_factory() -> str:
|
||||
return hex(int(random.random() * 2 ** 31))[2:]
|
||||
return hex(int(random.random() * 2**31))[2:]
|
||||
|
||||
|
||||
def find_form_request(html: str):
|
||||
@@ -106,7 +121,7 @@ def find_form_request(html: str):
|
||||
|
||||
form = soup.form
|
||||
if not form:
|
||||
raise _exception.ParseError("Could not find form to submit", data=soup)
|
||||
raise _exception.ParseError("Could not find form to submit", data=html)
|
||||
|
||||
url = form.get("action")
|
||||
if not url:
|
||||
@@ -130,38 +145,58 @@ def two_factor_helper(session: requests.Session, r, on_2fa_callback):
|
||||
|
||||
# You don't have to type a code if your device is already saved
|
||||
# Repeats if you get the code wrong
|
||||
while "approvals_code" not in data:
|
||||
while "approvals_code" in data:
|
||||
data["approvals_code"] = on_2fa_callback()
|
||||
log.info("Submitting 2FA code")
|
||||
r = session.post(url, data=data, allow_redirects=False)
|
||||
r = session.post(
|
||||
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
|
||||
)
|
||||
log.debug("2FA location: %s", r.headers.get("Location"))
|
||||
url, data = find_form_request(r.content.decode("utf-8"))
|
||||
|
||||
# TODO: Can be missing if checkup flow was done on another device in the meantime?
|
||||
if "name_action_selected" in data:
|
||||
data["name_action_selected"] = "save_device"
|
||||
log.info("Saving browser")
|
||||
r = session.post(url, data=data, allow_redirects=False)
|
||||
r = session.post(
|
||||
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
|
||||
)
|
||||
log.debug("2FA location: %s", r.headers.get("Location"))
|
||||
url = r.headers.get("Location")
|
||||
if url and url.startswith("https://www.messenger.com/login/auth_token/"):
|
||||
return url
|
||||
url, data = find_form_request(r.content.decode("utf-8"))
|
||||
|
||||
log.info("Starting Facebook checkup flow")
|
||||
r = session.post(url, data=data, allow_redirects=False)
|
||||
r = session.post(
|
||||
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
|
||||
)
|
||||
log.debug("2FA location: %s", r.headers.get("Location"))
|
||||
|
||||
url, data = find_form_request(r.content.decode("utf-8"))
|
||||
if "verification_method" in data:
|
||||
raise _exception.NotLoggedIn(
|
||||
"Your account is locked, and you need to log in using a browser, and verify it there!"
|
||||
)
|
||||
if "submit[This was me]" not in data or "submit[This wasn't me]" not in data:
|
||||
raise _exception.ParseError("Could not fill out form properly (2)", data=data)
|
||||
data["submit[This was me]"] = "[any value]"
|
||||
del data["submit[This wasn't me]"]
|
||||
log.info("Verifying login attempt")
|
||||
r = session.post(url, data=data, allow_redirects=False)
|
||||
r = session.post(
|
||||
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
|
||||
)
|
||||
log.debug("2FA location: %s", r.headers.get("Location"))
|
||||
|
||||
url, data = find_form_request(r.content.decode("utf-8"))
|
||||
if "name_action_selected" not in data:
|
||||
raise _exception.ParseError("Could not fill out form properly (3)", data=data)
|
||||
data["name_action_selected"] = "save_device"
|
||||
log.info("Saving device again")
|
||||
r = session.post(url, data=data, allow_redirects=False)
|
||||
|
||||
print(r.status_code, r.url, r.headers)
|
||||
r = session.post(
|
||||
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
|
||||
)
|
||||
log.debug("2FA location: %s", r.headers.get("Location"))
|
||||
return r.headers.get("Location")
|
||||
|
||||
|
||||
@@ -171,7 +206,6 @@ def get_error_data(html: str) -> Optional[str]:
|
||||
html, "html.parser", parse_only=bs4.SoupStrainer("form", id="login_form")
|
||||
)
|
||||
# Attempt to extract and format the error string
|
||||
# The error message is in the user's own language!
|
||||
return " ".join(list(soup.stripped_strings)[1:3]) or None
|
||||
|
||||
|
||||
@@ -232,7 +266,9 @@ class Session:
|
||||
on_2fa_callback: Function that will be called, in case a two factor
|
||||
authentication code is needed. This should return the requested code.
|
||||
|
||||
Only tested with SMS codes, might not work with authentication apps.
|
||||
Tested using SMS and authentication applications. If you have both
|
||||
enabled, you might not receive an SMS code, and you'll have to use the
|
||||
authentication application.
|
||||
|
||||
Note: Facebook limits the amount of codes they will give you, so if you
|
||||
don't receive a code, be patient, and try again later!
|
||||
@@ -275,6 +311,22 @@ class Session:
|
||||
"https://www.messenger.com/login/password/",
|
||||
data=data,
|
||||
allow_redirects=False,
|
||||
cookies=login_cookies(_util.now()),
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36",
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"accept-language": "en-HU,en;q=0.9,hu-HU;q=0.8,hu;q=0.7,en-US;q=0.6",
|
||||
"cache-control": "max-age=0",
|
||||
"origin": "https://www.messenger.com",
|
||||
"referer": "https://www.messenger.com/login/",
|
||||
"sec-ch-ua": '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-fetch-dest": "document",
|
||||
"sec-fetch-mode": "navigate",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"sec-fetch-user": "?1",
|
||||
"upgrade-insecure-requests": "1",
|
||||
},
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
_exception.handle_requests_error(e)
|
||||
@@ -292,17 +344,30 @@ class Session:
|
||||
raise _exception.NotLoggedIn(
|
||||
"2FA code required! Please supply `on_2fa_callback` to .login"
|
||||
)
|
||||
# Get a facebook.com url that handles the 2FA flow
|
||||
# Get a facebook.com/checkpoint/start url that handles the 2FA flow
|
||||
# This probably works differently for Messenger-only accounts
|
||||
url = _util.get_url_parameter(url, "next")
|
||||
# Explicitly allow redirects
|
||||
r = session.get(url, allow_redirects=True)
|
||||
if not url.startswith("https://www.facebook.com/checkpoint/start/"):
|
||||
raise _exception.ParseError("Failed 2fa flow (1)", data=url)
|
||||
|
||||
r = session.get(
|
||||
url, allow_redirects=False, cookies=login_cookies(_util.now())
|
||||
)
|
||||
url = r.headers.get("Location")
|
||||
if not url or not url.startswith("https://www.facebook.com/checkpoint/"):
|
||||
raise _exception.ParseError("Failed 2fa flow (2)", data=url)
|
||||
|
||||
r = session.get(
|
||||
url, allow_redirects=False, cookies=login_cookies(_util.now())
|
||||
)
|
||||
url = two_factor_helper(session, r, on_2fa_callback)
|
||||
|
||||
if not url.startswith("https://www.messenger.com/login/auth_token/"):
|
||||
raise _exception.ParseError("Failed 2fa flow", data=url)
|
||||
raise _exception.ParseError("Failed 2fa flow (3)", data=url)
|
||||
|
||||
r = session.get(url, allow_redirects=False)
|
||||
r = session.get(
|
||||
url, allow_redirects=False, cookies=login_cookies(_util.now())
|
||||
)
|
||||
url = r.headers.get("Location")
|
||||
|
||||
if url != "https://www.messenger.com/":
|
||||
@@ -362,7 +427,7 @@ class Session:
|
||||
|
||||
# Make a request to the main page to retrieve ServerJSDefine entries
|
||||
try:
|
||||
r = session.get(prefix_url("/"), allow_redirects=False)
|
||||
r = session.get(prefix_url("/"), allow_redirects=True)
|
||||
except requests.RequestException as e:
|
||||
_exception.handle_requests_error(e)
|
||||
_exception.handle_http_error(r.status_code)
|
||||
@@ -458,7 +523,7 @@ class Session:
|
||||
return self._post("/api/graphqlbatch/", data, as_graphql=True)
|
||||
|
||||
def _do_send_request(self, data):
|
||||
now = datetime.datetime.utcnow()
|
||||
now = _util.now()
|
||||
offline_threading_id = _util.generate_offline_threading_id()
|
||||
data["client"] = "mercury"
|
||||
data["author"] = "fbid:{}".format(self._user_id)
|
||||
@@ -483,3 +548,37 @@ class Session:
|
||||
return message_ids[0]
|
||||
except (KeyError, IndexError, TypeError) as e:
|
||||
raise _exception.ParseError("No message IDs could be found", data=j) from e
|
||||
|
||||
def _uri_share_data(self, data):
|
||||
data["image_height"] = 960
|
||||
data["image_width"] = 960
|
||||
data["__user"] = self.user.id
|
||||
j = self._post("/message_share_attachment/fromURI/", data)
|
||||
return j["payload"]["share_data"]
|
||||
|
||||
def to_file(self, filename):
|
||||
"""Save the session to a file.
|
||||
|
||||
Args:
|
||||
filename: The file to save the session to
|
||||
|
||||
Example:
|
||||
>>> session = fbchat.Session.from_cookies(cookies)
|
||||
>>> session.to_file("session.json")
|
||||
"""
|
||||
with open(filename, "w") as f:
|
||||
json.dump(self.get_cookies(), f)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filename):
|
||||
"""Load a session from a file.
|
||||
|
||||
Args:
|
||||
filename: The file to load the session from
|
||||
|
||||
Example:
|
||||
>>> session = fbchat.Session.from_file("session.json")
|
||||
"""
|
||||
with open(filename, "r") as f:
|
||||
cookies = json.load(f)
|
||||
return cls.from_cookies(cookies)
|
||||
|
@@ -105,6 +105,7 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
||||
mentions: Iterable["_models.Mention"] = None,
|
||||
files: Iterable[Tuple[str, str]] = None,
|
||||
reply_to_id: str = None,
|
||||
uri: str = None
|
||||
) -> str:
|
||||
"""Send a message to the thread.
|
||||
|
||||
@@ -114,10 +115,17 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
||||
files: Optional tuples, each containing an uploaded file's ID and mimetype.
|
||||
See `ThreadABC.send_files` for an example.
|
||||
reply_to_id: Optional message to reply to
|
||||
uri: Uri to formulate a sharable attachment with
|
||||
|
||||
Example:
|
||||
Send a message with a mention to a thread.
|
||||
|
||||
>>> mention = fbchat.Mention(thread_id="1234", offset=5, length=2)
|
||||
>>> thread.send_text("A message", mentions=[mention])
|
||||
>>> message_id = thread.send_text("A message", mentions=[mention])
|
||||
|
||||
Reply to the message.
|
||||
|
||||
>>> thread.send_text("A reply", reply_to_id=message_id)
|
||||
|
||||
Returns:
|
||||
The sent message
|
||||
@@ -132,6 +140,9 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
||||
|
||||
if files:
|
||||
data["has_attachment"] = True
|
||||
|
||||
if uri:
|
||||
data.update(self._generate_shareable_attachment(uri))
|
||||
|
||||
for i, (file_id, mimetype) in enumerate(files or ()):
|
||||
data["{}s[{}]".format(_util.mimetype_to_key(mimetype), i)] = file_id
|
||||
@@ -211,7 +222,7 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
||||
Example:
|
||||
Send a pinned location in Beijing, China.
|
||||
|
||||
>>> thread.send_location(39.9390731, 116.117273)
|
||||
>>> thread.send_pinned_location(39.9390731, 116.117273)
|
||||
"""
|
||||
self._send_location(False, latitude=latitude, longitude=longitude)
|
||||
|
||||
@@ -229,7 +240,53 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
||||
>>> thread.send_files(files)
|
||||
"""
|
||||
return self.send_text(text=None, files=files)
|
||||
|
||||
def send_uri(self, uri: str, **kwargs):
|
||||
"""Send a uri preview to a thread.
|
||||
Args:
|
||||
uri: uri to preview
|
||||
"""
|
||||
if kwargs.get('text') is None:
|
||||
kwargs['text'] = None
|
||||
self.send_text(uri=uri, **kwargs)
|
||||
|
||||
def _generate_shareable_attachment(self, uri):
|
||||
"""Send a uri preview to a thread.
|
||||
Args:
|
||||
uri: uri to preview
|
||||
Returns:
|
||||
:ref:`Message ID <intro_message_ids>` of the sent message
|
||||
Raises:
|
||||
FBchatException: If request failed
|
||||
"""
|
||||
url_data = self.session._uri_share_data({"uri": uri})
|
||||
data = self._to_send_data()
|
||||
data["action_type"] = "ma-type:user-generated-message"
|
||||
data["shareable_attachment[share_type]"] = url_data["share_type"]
|
||||
|
||||
# Most uri params will come back as dict
|
||||
if isinstance(url_data["share_params"], dict):
|
||||
data["has_attachment"] = True
|
||||
for key in url_data["share_params"]:
|
||||
if isinstance(url_data["share_params"][key], dict):
|
||||
for key2 in url_data["share_params"][key]:
|
||||
data[
|
||||
"shareable_attachment[share_params][{}][{}]".format(
|
||||
key, key2
|
||||
)
|
||||
] = url_data["share_params"][key][key2]
|
||||
else:
|
||||
data[
|
||||
"shareable_attachment[share_params][{}]".format(key)
|
||||
] = url_data["share_params"][key]
|
||||
|
||||
# Some (such as facebook profile pages) will just be a list
|
||||
else:
|
||||
data["has_attachment"] = False
|
||||
for index, val in enumerate(url_data["share_params"]):
|
||||
data["shareable_attachment[share_params][{}]".format(index)] = val
|
||||
return data
|
||||
|
||||
# xmd = {"quick_replies": []}
|
||||
# for quick_reply in quick_replies:
|
||||
# # TODO: Move this to `_quick_reply.py`
|
||||
|
@@ -56,23 +56,26 @@ def parse_json(text: str) -> Any:
|
||||
|
||||
|
||||
def generate_offline_threading_id():
|
||||
ret = datetime_to_millis(datetime.datetime.utcnow())
|
||||
ret = datetime_to_millis(now())
|
||||
value = int(random.random() * 4294967295)
|
||||
string = ("0000000000000000000000" + format(value, "b"))[-22:]
|
||||
msgs = format(ret, "b") + string
|
||||
return str(int(msgs, 2))
|
||||
|
||||
|
||||
def remove_version_from_module(module):
|
||||
return module.split("@", 1)[0]
|
||||
|
||||
|
||||
def get_jsmods_require(require) -> Mapping[str, Sequence[Any]]:
|
||||
rtn = {}
|
||||
for item in require:
|
||||
if len(item) == 1:
|
||||
(module,) = item
|
||||
rtn[module] = []
|
||||
rtn[remove_version_from_module(module)] = []
|
||||
continue
|
||||
method = "{}.{}".format(item[0], item[1])
|
||||
requirements = item[2]
|
||||
arguments = item[3]
|
||||
module, method, requirements, arguments = item
|
||||
method = "{}.{}".format(remove_version_from_module(module), method)
|
||||
rtn[method] = arguments
|
||||
return rtn
|
||||
|
||||
@@ -155,3 +158,11 @@ def timedelta_to_seconds(td: datetime.timedelta) -> int:
|
||||
The returned seconds will be rounded to the nearest whole number.
|
||||
"""
|
||||
return round(td.total_seconds())
|
||||
|
||||
|
||||
def now() -> datetime.datetime:
|
||||
"""The current time.
|
||||
|
||||
Similar to datetime.datetime.now(), but returns a non-naive datetime.
|
||||
"""
|
||||
return datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
|
@@ -12,7 +12,7 @@ author = "Taehoon Kim"
|
||||
author-email = "carpedm20@gmail.com"
|
||||
maintainer = "Mads Marquart"
|
||||
maintainer-email = "madsmtm@gmail.com"
|
||||
home-page = "https://github.com/carpedm20/fbchat/"
|
||||
home-page = "https://git.karaolidis.com/karaolidis/fbchat/"
|
||||
requires = [
|
||||
"attrs>=19.1",
|
||||
"requests~=2.19",
|
||||
@@ -47,8 +47,7 @@ keywords = "Facebook FB Messenger Library Chat Api Bot"
|
||||
license = "BSD 3-Clause"
|
||||
|
||||
[tool.flit.metadata.urls]
|
||||
Documentation = "https://fbchat.readthedocs.io/"
|
||||
Repository = "https://github.com/carpedm20/fbchat/"
|
||||
Repository = "https://git.karaolidis.com/karaolidis/fbchat/"
|
||||
|
||||
[tool.flit.metadata.requires-extra]
|
||||
test = [
|
||||
|
@@ -17,12 +17,51 @@ def session(pytestconfig):
|
||||
|
||||
pytestconfig.cache.set("session_cookies", session.get_cookies())
|
||||
|
||||
# TODO: Allow the main session object to be closed - and perhaps used in `with`?
|
||||
session._session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(session):
|
||||
return fbchat.Client(session=session)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def user(pytestconfig, session):
|
||||
user_id = pytestconfig.cache.get("user_id", None)
|
||||
if not user_id:
|
||||
user_id = input("A user you're chatting with's id: ")
|
||||
pytestconfig.cache.set("user_id", user_id)
|
||||
return fbchat.User(session=session, id=user_id)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def group(pytestconfig, session):
|
||||
group_id = pytestconfig.cache.get("group_id", None)
|
||||
if not group_id:
|
||||
group_id = input("A group you're chatting with's id: ")
|
||||
pytestconfig.cache.set("group_id", group_id)
|
||||
return fbchat.Group(session=session, id=group_id)
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
scope="session",
|
||||
params=[
|
||||
"user",
|
||||
"group",
|
||||
"self",
|
||||
pytest.param("invalid", marks=[pytest.mark.xfail()]),
|
||||
],
|
||||
)
|
||||
def any_thread(request, session, user, group):
|
||||
return {
|
||||
"user": user,
|
||||
"group": group,
|
||||
"self": session.user,
|
||||
"invalid": fbchat.Thread(session=session, id="0"),
|
||||
}[request.param]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def listener(session):
|
||||
return fbchat.Listener(session=session, chat_on=False, foreground=False)
|
||||
|
@@ -46,11 +46,6 @@ def test_undocumented(client):
|
||||
client.fetch_unseen()
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="need a way to get an image id")
|
||||
def test_fetch_image_url(client):
|
||||
client.fetch_image_url("TODO")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def open_resource(pytestconfig):
|
||||
def get_resource_inner(filename):
|
||||
@@ -60,6 +55,14 @@ def open_resource(pytestconfig):
|
||||
return get_resource_inner
|
||||
|
||||
|
||||
def test_upload_and_fetch_image_url(client, open_resource):
|
||||
with open_resource("image.png") as f:
|
||||
((id, mimetype),) = client.upload([("image.png", f, "image/png")])
|
||||
assert mimetype == "image/png"
|
||||
|
||||
assert client.fetch_image_url(id).startswith("http")
|
||||
|
||||
|
||||
def test_upload_image(client, open_resource):
|
||||
with open_resource("image.png") as f:
|
||||
_ = client.upload([("image.png", f, "image/png")])
|
||||
@@ -90,5 +93,24 @@ def test_upload_many(client, open_resource):
|
||||
)
|
||||
|
||||
|
||||
# def test_mark_as_read(client):
|
||||
# client.mark_as_read([thread1, thread2])
|
||||
def test_mark_as_read(client, user, group):
|
||||
client.mark_as_read([user, group], fbchat._util.now())
|
||||
|
||||
|
||||
def test_mark_as_unread(client, user, group):
|
||||
client.mark_as_unread([user, group], fbchat._util.now())
|
||||
|
||||
|
||||
def test_move_threads(client, user, group):
|
||||
client.move_threads(fbchat.ThreadLocation.PENDING, [user, group])
|
||||
client.move_threads(fbchat.ThreadLocation.INBOX, [user, group])
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="need to have threads to delete")
|
||||
def test_delete_threads():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="need to have messages to delete")
|
||||
def test_delete_messages():
|
||||
pass
|
||||
|
42
tests/online/test_send.py
Normal file
42
tests/online/test_send.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
import fbchat
|
||||
|
||||
pytestmark = pytest.mark.online
|
||||
|
||||
|
||||
# TODO: Verify return values
|
||||
|
||||
|
||||
def test_wave(any_thread):
|
||||
assert any_thread.wave(True)
|
||||
assert any_thread.wave(False)
|
||||
|
||||
|
||||
def test_send_text(any_thread):
|
||||
assert any_thread.send_text("Test")
|
||||
|
||||
|
||||
def test_send_text_with_mention(any_thread):
|
||||
mention = fbchat.Mention(thread_id=any_thread.id, offset=5, length=8)
|
||||
assert any_thread.send_text("Test @mention", mentions=[mention])
|
||||
|
||||
|
||||
def test_send_emoji(any_thread):
|
||||
assert any_thread.send_emoji("😀", size=fbchat.EmojiSize.LARGE)
|
||||
|
||||
|
||||
def test_send_sticker(any_thread):
|
||||
assert any_thread.send_sticker("1889713947839631")
|
||||
|
||||
|
||||
def test_send_location(any_thread):
|
||||
any_thread.send_location(51.5287718, -0.2416815)
|
||||
|
||||
|
||||
def test_send_pinned_location(any_thread):
|
||||
any_thread.send_pinned_location(39.9390731, 116.117273)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="need a way to use the uploaded files from test_client.py")
|
||||
def test_send_files(any_thread):
|
||||
pass
|
@@ -1,6 +1,6 @@
|
||||
import datetime
|
||||
import pytest
|
||||
from fbchat import ParseError
|
||||
from fbchat import ParseError, _util
|
||||
from fbchat._session import (
|
||||
parse_server_js_define,
|
||||
base36encode,
|
||||
@@ -13,7 +13,7 @@ from fbchat._session import (
|
||||
)
|
||||
|
||||
|
||||
def test_parse_server_js_define():
|
||||
def test_parse_server_js_define_old():
|
||||
html = """
|
||||
some data;require("TimeSliceImpl").guard(function(){(require("ServerJSDefine")).handleDefines([["DTSGInitialData",[],{"token":"123"},100]])
|
||||
|
||||
@@ -29,6 +29,20 @@ def test_parse_server_js_define():
|
||||
}
|
||||
|
||||
|
||||
def test_parse_server_js_define_new():
|
||||
html = """
|
||||
some data;require("TimeSliceImpl").guard(function(){new (require("ServerJS"))().handle({"define":[["DTSGInitialData",[],{"token":""},100]],"require":[...]});}, "ServerJS define", {"root":true})();
|
||||
more data
|
||||
<script><script>require("TimeSliceImpl").guard(function(){var s=new (require("ServerJS"))();s.handle({"define":[["DTSGInitData",[],{"token":"","async_get_token":""},3333]],"require":[...]});require("Run").onAfterLoad(function(){s.cleanup(require("TimeSliceImpl"))});}, "ServerJS define", {"root":true})();</script>
|
||||
other irrelevant data
|
||||
"""
|
||||
define = parse_server_js_define(html)
|
||||
assert define == {
|
||||
"DTSGInitialData": {"token": ""},
|
||||
"DTSGInitData": {"async_get_token": "", "token": ""},
|
||||
}
|
||||
|
||||
|
||||
def test_parse_server_js_define_error():
|
||||
with pytest.raises(ParseError, match="Could not find any"):
|
||||
parse_server_js_define("")
|
||||
@@ -59,7 +73,7 @@ def test_prefix_url():
|
||||
|
||||
def test_generate_message_id():
|
||||
# Returns random output, so hard to test more thoroughly
|
||||
assert generate_message_id(datetime.datetime.utcnow(), "def")
|
||||
assert generate_message_id(_util.now(), "def")
|
||||
|
||||
|
||||
def test_session_factory():
|
||||
|
@@ -68,6 +68,17 @@ def test_get_jsmods_require():
|
||||
}
|
||||
|
||||
|
||||
def test_get_jsmods_require_version_specifier():
|
||||
data = [
|
||||
["DimensionTracking@1234"],
|
||||
["CavalryLoggerImpl@2345", "startInstrumentation", [], []],
|
||||
]
|
||||
assert get_jsmods_require(data) == {
|
||||
"DimensionTracking": [],
|
||||
"CavalryLoggerImpl.startInstrumentation": [],
|
||||
}
|
||||
|
||||
|
||||
def test_get_jsmods_require_get_image_url():
|
||||
data = [
|
||||
[
|
||||
|
Reference in New Issue
Block a user