Compare commits
114 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1460b2f421 | ||
|
27f76ba659 | ||
|
589117b9e7 | ||
|
80300cd160 | ||
|
76171408cc | ||
|
c1800a174f | ||
|
8ae8435940 | ||
|
f916cb3b53 | ||
|
929c2137bf | ||
|
98056e91c5 | ||
|
944a7248c3 | ||
|
caa2ecd0b7 | ||
|
dfc2d0652f | ||
|
8d25540445 | ||
|
6ea174bfd4 | ||
|
56e43aec0e | ||
|
491d120c25 | ||
|
82d071d52c | ||
|
8190654a91 | ||
|
5e21702d16 | ||
|
3df4172237 | ||
|
e0710a2ec1 | ||
|
d20fc3b9ce | ||
|
f25faec108 | ||
|
2750658c3c | ||
|
e6bc5bbab3 | ||
|
de5f3a9d9e | ||
|
7f0da012c2 | ||
|
76ecbf5eb0 | ||
|
06881a4c70 | ||
|
c14fdd82db | ||
|
b1a02ad930 | ||
|
2b580c60e9 | ||
|
27ffba3b14 | ||
|
fb7bf437ba | ||
|
d8baf0b9e7 | ||
|
a6945fe880 | ||
|
6ff77dd8c7 | ||
|
1d925a608b | ||
|
646669ca75 | ||
|
0ec2baaa83 | ||
|
5abaaefd1c | ||
|
687afea0f2 | ||
|
7398d4fa2b | ||
|
d73c8c3627 | ||
|
f921b91c5b | ||
|
8ed3c1b159 | ||
|
4f947cdbb5 | ||
|
ec6c29052a | ||
|
6b117502f3 | ||
|
a367aa0b31 | ||
|
7f6843df55 | ||
|
4b485d54b6 | ||
|
e80a040db4 | ||
|
c357fd085b | ||
|
d0c5f29b0a | ||
|
3e7b20c379 | ||
|
f4a997c0ef | ||
|
102e74bb63 | ||
|
84fa15e44c | ||
|
7b8ecf8fe3 | ||
|
79ebf920ea | ||
|
0d05d42f70 | ||
|
95989b6da7 | ||
|
22e57f99a1 | ||
|
b9d29c0417 | ||
|
edc33db9e8 | ||
|
45d8b45d96 | ||
|
b6a6d7dc68 | ||
|
c57b84cd0b | ||
|
78e7841b5e | ||
|
e41d981449 | ||
|
381227af66 | ||
|
2f8d0728ba | ||
|
13bfc5f2f9 | ||
|
d4446280c7 | ||
|
861f17bc4d | ||
|
41bbe18e3d | ||
|
5f9c357a15 | ||
|
c089298f46 | ||
|
be968e0caa | ||
|
d32b7b612a | ||
|
8739318101 | ||
|
1ac569badd | ||
|
e38f891693 | ||
|
b7ea8e6001 | ||
|
b0bf5ba8e0 | ||
|
8169a5f776 | ||
|
492465a525 | ||
|
f185e44f93 | ||
|
5f2c318baf | ||
|
531a5b77d0 | ||
|
f9245cdfed | ||
|
47ea88e025 | ||
|
345a473ee0 | ||
|
c6dc432d06 | ||
|
af3bd55535 | ||
|
5fa1d86191 | ||
|
d4859b675a | ||
|
9aa427031e | ||
|
9e8fe7bc1e | ||
|
90813c959d | ||
|
940a65954c | ||
|
9b4e753a79 | ||
|
e0be9029e4 | ||
|
aa3faca246 | ||
|
2edb95dfdd | ||
|
e0bb9960fb | ||
|
71608845c0 | ||
|
0048e82151 | ||
|
0767ef4902 | ||
|
abe3357e67 | ||
|
19457efe9b | ||
|
487a2eb3e3 |
129
.travis.yml
129
.travis.yml
@@ -1,90 +1,61 @@
|
||||
sudo: false
|
||||
language: python
|
||||
conditions: v1
|
||||
python: 3.6
|
||||
|
||||
# There are two accounts made specifically for Travis, and the passwords are really only encrypted for obscurity
|
||||
# The global env variables `client1_email`, `client1_password`, `client2_email`, `client2_password` and `group_id`
|
||||
# are set on the Travis Settings page
|
||||
cache: pip
|
||||
|
||||
# The tests are run with `Limit concurrent jobs = 1`, since the tests can't use the clients simultaneously
|
||||
|
||||
install:
|
||||
- pip install -U -r requirements.txt
|
||||
- pip install -U -r dev-requirements.txt
|
||||
|
||||
cache:
|
||||
pip: true
|
||||
# Pytest caching is disabled, since TravisCI instances have different public IPs. Facebook doesn't like that,
|
||||
# and redirects you to the url `/checkpoint/block`, where you have to change the account's password
|
||||
# directories:
|
||||
# - .pytest_cache
|
||||
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 -m offline
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# The tests are split into online and offline versions.
|
||||
# The online tests are only run against the master branch.
|
||||
# Because:
|
||||
# Travis caching is per-branch and per-job, so even though we cache the Facebook sessions via. `.pytest_cache`
|
||||
# and in `tests.utils.load_client`, we need 6 new sessions per branch. This is usually the point where Facebook
|
||||
# starts complaining, and we have to manually fix it
|
||||
- python: 2.7
|
||||
before_install:
|
||||
- sudo apt-get -y install python3-pip python3-setuptools
|
||||
- sudo pip3 install flit
|
||||
install: flit install --python python --deps production --extras test
|
||||
- python: 3.4
|
||||
- python: 3.5
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
dist: xenial
|
||||
sudo: required
|
||||
- python: pypy3.5
|
||||
|
||||
- &test-online
|
||||
if: (branch = master OR tag IS present) AND type != pull_request
|
||||
stage: online tests
|
||||
script: scripts/travis-online
|
||||
- name: Lint
|
||||
before_install: skip
|
||||
install: pip install black
|
||||
script: black --check --verbose .
|
||||
|
||||
# Run online tests in all the supported python versions
|
||||
python: 2.7
|
||||
- <<: *test-online
|
||||
python: 3.4
|
||||
- <<: *test-online
|
||||
python: 3.5
|
||||
- <<: *test-online
|
||||
python: 3.6
|
||||
- <<: *test-online
|
||||
python: pypy
|
||||
- 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: true
|
||||
on:
|
||||
tags: true
|
||||
|
||||
# Run the expensive tests, with the python version most likely to break, aka. 2
|
||||
- <<: *test-online
|
||||
# Only run if the commit message includes [ci all] or [all ci]
|
||||
if: commit_message =~ /\[ci\s+all\]|\[all\s+ci\]/
|
||||
python: 2.7
|
||||
env: PYTEST_ADDOPTS='-m expensive'
|
||||
- stage: deploy
|
||||
name: PyPI
|
||||
if: tag IS present
|
||||
install: skip
|
||||
script: skip
|
||||
deploy:
|
||||
provider: script
|
||||
script: flit publish
|
||||
on:
|
||||
tags: true
|
||||
|
||||
- &test-offline
|
||||
# Ideally, it'd be nice to run the offline tests in every build, but since we can't run jobs concurrently (yet),
|
||||
# we'll disable them when they're not needed, and include them inside the online tests instead
|
||||
if: not ((branch = master OR tag IS present) AND type != pull_request)
|
||||
stage: offline tests
|
||||
script: scripts/travis-offline
|
||||
|
||||
# Run offline tests in all the supported python versions
|
||||
python: 2.7
|
||||
- <<: *test-offline
|
||||
python: 3.4
|
||||
- <<: *test-offline
|
||||
python: 3.5
|
||||
- <<: *test-offline
|
||||
python: 3.6
|
||||
- <<: *test-offline
|
||||
python: 3.6
|
||||
- <<: *test-offline
|
||||
python: pypy
|
||||
|
||||
# Deploy to PyPI
|
||||
- &deploy
|
||||
stage: deploy
|
||||
if: branch = master AND tag IS present
|
||||
install: skip
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: madsmtm
|
||||
password:
|
||||
secure: "VA0MLSrwIW/T2KjMwjLZCzrLHw8pJT6tAvb48t7qpBdm8x192hax61pz1TaBZoJvlzyBPFKvluftuclTc7yEFwzXe7Gjqgd/ODKZl/wXDr36hQ7BBOLPZujdwmWLvTzMh3eJZlvkgcLCzrvK3j2oW8cM/+FZeVi/5/FhVuJ4ofs="
|
||||
distributions: sdist bdist_wheel
|
||||
skip_existing: true
|
||||
|
||||
# We need the bdist_wheels from both Python 2 and 3
|
||||
python: 3.6
|
||||
- <<: *deploy
|
||||
python: 2.7
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: change
|
||||
|
@@ -9,6 +9,22 @@ That means that if you're submitting a breaking change, it will probably take a
|
||||
In that case, you can point your PR to the ``2.0.0-dev`` branch, where the API is being properly developed.
|
||||
Otherwise, just point it to ``master``.
|
||||
|
||||
Development Environment
|
||||
-----------------------
|
||||
|
||||
You can use `flit` to install the package as a symlink:
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ # *nix:
|
||||
$ flit install --symlink
|
||||
$ # Windows:
|
||||
$ flit install --pth-file
|
||||
|
||||
After that, you can ``import`` the module as normal.
|
||||
|
||||
Before committing, you should run ``black .`` in the main directory, to format your code.
|
||||
|
||||
Testing Environment
|
||||
-------------------
|
||||
|
||||
|
@@ -1,3 +0,0 @@
|
||||
include LICENSE
|
||||
include CONTRIBUTING.rst
|
||||
include README.rst
|
17
README.rst
17
README.rst
@@ -5,9 +5,9 @@ fbchat: Facebook Chat (Messenger) for Python
|
||||
:target: https://github.com/carpedm20/fbchat/tree/master/LICENSE
|
||||
:alt: License: BSD 3-Clause
|
||||
|
||||
.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6%20pypy-blue.svg
|
||||
.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6%203.7%20pypy-blue.svg
|
||||
:target: https://pypi.python.org/pypi/fbchat
|
||||
:alt: Supported python versions: 2.7, 3.4, 3.5, 3.6 and pypy
|
||||
:alt: Supported python versions: 2.7, 3.4, 3.5, 3.6, 3.7 and pypy
|
||||
|
||||
.. image:: https://readthedocs.org/projects/fbchat/badge/?version=master
|
||||
:target: https://fbchat.readthedocs.io
|
||||
@@ -17,6 +17,10 @@ fbchat: Facebook Chat (Messenger) for Python
|
||||
:target: https://travis-ci.org/carpedm20/fbchat
|
||||
:alt: Travis CI
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
:alt: Code style
|
||||
|
||||
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>`__.
|
||||
|
||||
@@ -27,17 +31,18 @@ or jump right into the code by viewing the `examples <https://github.com/carpedm
|
||||
|
||||
Installation:
|
||||
|
||||
.. code-block:: console
|
||||
.. code-block::
|
||||
|
||||
$ pip install fbchat
|
||||
|
||||
You can also install from source, by using `setuptools` (You need at least version 30.3.0):
|
||||
You can also install from source, by using `flit`:
|
||||
|
||||
.. code-block:: console
|
||||
.. code-block::
|
||||
|
||||
$ pip install flit
|
||||
$ git clone https://github.com/carpedm20/fbchat.git
|
||||
$ cd fbchat
|
||||
$ python setup.py install
|
||||
$ flit install
|
||||
|
||||
|
||||
Maintainer
|
||||
|
@@ -1,2 +0,0 @@
|
||||
pytest
|
||||
six
|
67
docs/conf.py
67
docs/conf.py
@@ -19,7 +19,8 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
import fbchat
|
||||
import tests
|
||||
@@ -36,27 +37,27 @@ from fbchat import __copyright__, __author__, __version__, __description__
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.viewcode'
|
||||
"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']
|
||||
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'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = 'fbchat'
|
||||
title = 'fbchat Documentation'
|
||||
project = "fbchat"
|
||||
title = "fbchat Documentation"
|
||||
copyright = __copyright__
|
||||
author = __author__
|
||||
description = __description__
|
||||
@@ -80,10 +81,10 @@ 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']
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
@@ -95,7 +96,7 @@ todo_include_todos = True
|
||||
# a list of builtin themes.
|
||||
#
|
||||
|
||||
html_theme = 'alabaster'
|
||||
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
|
||||
@@ -106,13 +107,13 @@ html_theme = 'alabaster'
|
||||
# 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']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = project + 'doc'
|
||||
htmlhelp_basename = project + "doc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
@@ -121,15 +122,12 @@ 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',
|
||||
@@ -138,20 +136,14 @@ latex_elements = {
|
||||
# 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'),
|
||||
]
|
||||
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)
|
||||
]
|
||||
man_pages = [(master_doc, project, title, [author], 1)]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
@@ -160,32 +152,27 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, project, title,
|
||||
author, project, description,
|
||||
'Miscellaneous'),
|
||||
(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}
|
||||
|
||||
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
|
||||
"show_powered_by": False,
|
||||
"github_user": "carpedm20",
|
||||
"github_repo": project,
|
||||
"github_banner": True,
|
||||
"show_related": False,
|
||||
}
|
||||
|
||||
html_sidebars = {
|
||||
'**': ['sidebar.html', 'searchbox.html']
|
||||
}
|
||||
html_sidebars = {"**": ["sidebar.html", "searchbox.html"]}
|
||||
|
||||
html_show_sphinx = False
|
||||
html_show_sourcelink = False
|
||||
autoclass_content = 'init'
|
||||
autoclass_content = "both"
|
||||
html_short_title = description
|
||||
|
@@ -3,10 +3,10 @@
|
||||
from fbchat import Client
|
||||
from fbchat.models import *
|
||||
|
||||
client = Client('<email>', '<password>')
|
||||
client = Client("<email>", "<password>")
|
||||
|
||||
print('Own id: {}'.format(client.uid))
|
||||
print("Own id: {}".format(client.uid))
|
||||
|
||||
client.send(Message(text='Hi me!'), thread_id=client.uid, thread_type=ThreadType.USER)
|
||||
client.send(Message(text="Hi me!"), thread_id=client.uid, thread_type=ThreadType.USER)
|
||||
|
||||
client.logout()
|
||||
|
@@ -14,5 +14,6 @@ class EchoBot(Client):
|
||||
if author_id != self.uid:
|
||||
self.send(message_object, thread_id=thread_id, thread_type=thread_type)
|
||||
|
||||
|
||||
client = EchoBot("<email>", "<password>")
|
||||
client.listen()
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from fbchat import Client
|
||||
from fbchat.models import *
|
||||
|
||||
client = Client('<email>', '<password>')
|
||||
client = Client("<email>", "<password>")
|
||||
|
||||
# Fetches a list of all users you're currently chatting with, as `User` objects
|
||||
users = client.fetchAllUsers()
|
||||
@@ -13,9 +13,9 @@ 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>']
|
||||
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>')
|
||||
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]))
|
||||
@@ -23,9 +23,9 @@ 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]
|
||||
user = client.searchForUsers("<name of user>")[0]
|
||||
|
||||
print('user ID: {}'.format(user.uid))
|
||||
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))
|
||||
@@ -40,7 +40,7 @@ print("Threads: {}".format(threads))
|
||||
|
||||
|
||||
# Gets the last 10 messages sent to the thread
|
||||
messages = client.fetchThreadMessages(thread_id='<thread id>', limit=10)
|
||||
messages = client.fetchThreadMessages(thread_id="<thread id>", limit=10)
|
||||
# Since the message come in reversed order, reverse them
|
||||
messages.reverse()
|
||||
|
||||
@@ -50,13 +50,13 @@ for message in messages:
|
||||
|
||||
|
||||
# If we have a thread id, we can use `fetchThreadInfo` to fetch a `Thread` object
|
||||
thread = client.fetchThreadInfo('<thread id>')['<thread id>']
|
||||
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]
|
||||
thread = client.searchForThreads("<name of thread>")[0]
|
||||
print("thread's name: {}".format(thread.name))
|
||||
print("thread's type: {}".format(thread.type))
|
||||
|
||||
|
@@ -5,57 +5,89 @@ from fbchat.models import *
|
||||
|
||||
client = Client("<email>", "<password>")
|
||||
|
||||
thread_id = '1234567890'
|
||||
thread_id = "1234567890"
|
||||
thread_type = ThreadType.GROUP
|
||||
|
||||
# Will send a message to the thread
|
||||
client.send(Message(text='<message>'), thread_id=thread_id, thread_type=thread_type)
|
||||
client.send(Message(text="<message>"), thread_id=thread_id, thread_type=thread_type)
|
||||
|
||||
# Will send the default `like` emoji
|
||||
client.send(Message(emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type)
|
||||
client.send(
|
||||
Message(emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type
|
||||
)
|
||||
|
||||
# Will send the emoji `👍`
|
||||
client.send(Message(text='👍', emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type)
|
||||
client.send(
|
||||
Message(text="👍", emoji_size=EmojiSize.LARGE),
|
||||
thread_id=thread_id,
|
||||
thread_type=thread_type,
|
||||
)
|
||||
|
||||
# Will send the sticker with ID `767334476626295`
|
||||
client.send(Message(sticker=Sticker('767334476626295')), thread_id=thread_id, thread_type=thread_type)
|
||||
client.send(
|
||||
Message(sticker=Sticker("767334476626295")),
|
||||
thread_id=thread_id,
|
||||
thread_type=thread_type,
|
||||
)
|
||||
|
||||
# Will send a message with a mention
|
||||
client.send(Message(text='This is a @mention', mentions=[Mention(thread_id, offset=10, length=8)]), thread_id=thread_id, thread_type=thread_type)
|
||||
client.send(
|
||||
Message(
|
||||
text="This is a @mention", mentions=[Mention(thread_id, offset=10, length=8)]
|
||||
),
|
||||
thread_id=thread_id,
|
||||
thread_type=thread_type,
|
||||
)
|
||||
|
||||
# Will send the image located at `<image path>`
|
||||
client.sendLocalImage('<image path>', message=Message(text='This is a local image'), thread_id=thread_id, thread_type=thread_type)
|
||||
client.sendLocalImage(
|
||||
"<image path>",
|
||||
message=Message(text="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=Message(text='This is a remote image'), thread_id=thread_id, thread_type=thread_type)
|
||||
client.sendRemoteImage(
|
||||
"<image url>",
|
||||
message=Message(text="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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
client.changeThreadEmoji("👍", thread_id=thread_id)
|
||||
|
||||
# Will react to a message with a 😍 emoji
|
||||
client.reactToMessage('<message id>', MessageReaction.LOVE)
|
||||
client.reactToMessage("<message id>", MessageReaction.LOVE)
|
||||
|
@@ -4,28 +4,33 @@ from fbchat import log, Client
|
||||
from fbchat.models import *
|
||||
|
||||
# Change this to your group id
|
||||
old_thread_id = '1234567890'
|
||||
old_thread_id = "1234567890"
|
||||
|
||||
# Change these to match your liking
|
||||
old_color = ThreadColor.MESSENGER_BLUE
|
||||
old_emoji = '👍'
|
||||
old_title = 'Old group chat name'
|
||||
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"
|
||||
"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))
|
||||
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))
|
||||
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):
|
||||
@@ -36,19 +41,43 @@ class KeepBot(Client):
|
||||
|
||||
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:
|
||||
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)
|
||||
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,
|
||||
)
|
||||
|
||||
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()
|
||||
|
@@ -3,15 +3,23 @@
|
||||
from fbchat import log, Client
|
||||
from fbchat.models import *
|
||||
|
||||
|
||||
class RemoveBot(Client):
|
||||
def onMessage(self, author_id, message_object, 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_object.text == 'Remove me!' and thread_type == ThreadType.GROUP:
|
||||
log.info('{} will be removed from {}'.format(author_id, thread_id))
|
||||
if message_object.text == "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(RemoveBot, self).onMessage(author_id=author_id, message_object=message_object, thread_id=thread_id, thread_type=thread_type, **kwargs)
|
||||
super(RemoveBot, self).onMessage(
|
||||
author_id=author_id,
|
||||
message_object=message_object,
|
||||
thread_id=thread_id,
|
||||
thread_type=thread_type,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
client = RemoveBot("<email>", "<password>")
|
||||
client.listen()
|
||||
|
@@ -1,29 +1,23 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""Facebook Chat (Messenger) for Python
|
||||
|
||||
"""
|
||||
fbchat
|
||||
~~~~~~
|
||||
|
||||
Facebook Chat (Messenger) for Python
|
||||
|
||||
:copyright: (c) 2015 - 2018 by Taehoon Kim
|
||||
:license: BSD 3-Clause, see LICENSE for more details.
|
||||
:copyright: (c) 2015 - 2019 by Taehoon Kim
|
||||
:license: BSD 3-Clause, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .models import *
|
||||
from .client import *
|
||||
|
||||
__title__ = 'fbchat'
|
||||
__version__ = '1.4.2'
|
||||
__description__ = 'Facebook Chat (Messenger) for Python'
|
||||
__title__ = "fbchat"
|
||||
__version__ = "1.6.4"
|
||||
__description__ = "Facebook Chat (Messenger) for Python"
|
||||
|
||||
__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim'
|
||||
__license__ = 'BSD 3-Clause'
|
||||
__copyright__ = "Copyright 2015 - 2019 by Taehoon Kim"
|
||||
__license__ = "BSD 3-Clause"
|
||||
|
||||
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
|
||||
__email__ = 'carpedm20@gmail.com'
|
||||
__author__ = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart"
|
||||
__email__ = "carpedm20@gmail.com"
|
||||
|
||||
__all__ = [
|
||||
'Client',
|
||||
]
|
||||
__all__ = ["Client"]
|
||||
|
48
fbchat/_attachment.py
Normal file
48
fbchat/_attachment.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class Attachment(object):
|
||||
"""Represents a Facebook attachment"""
|
||||
|
||||
#: The attachment ID
|
||||
uid = attr.ib(None)
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class UnsentMessage(Attachment):
|
||||
"""Represents an unsent message attachment"""
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class ShareAttachment(Attachment):
|
||||
"""Represents a shared item (eg. URL) that has been sent as a Facebook attachment"""
|
||||
|
||||
#: ID of the author of the shared post
|
||||
author = attr.ib(None)
|
||||
#: Target URL
|
||||
url = attr.ib(None)
|
||||
#: Original URL if Facebook redirects the URL
|
||||
original_url = attr.ib(None)
|
||||
#: Title of the attachment
|
||||
title = attr.ib(None)
|
||||
#: Description of the attachment
|
||||
description = attr.ib(None)
|
||||
#: Name of the source
|
||||
source = attr.ib(None)
|
||||
#: URL of the attachment image
|
||||
image_url = attr.ib(None)
|
||||
#: URL of the original image if Facebook uses `safe_image`
|
||||
original_image_url = attr.ib(None)
|
||||
#: Width of the image
|
||||
image_width = attr.ib(None)
|
||||
#: Height of the image
|
||||
image_height = attr.ib(None)
|
||||
#: List of additional attachments
|
||||
attachments = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
|
||||
|
||||
# Put here for backwards compatibility, so that the init argument order is preserved
|
||||
uid = attr.ib(None)
|
12
fbchat/_core.py
Normal file
12
fbchat/_core.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import aenum
|
||||
|
||||
|
||||
class Enum(aenum.Enum):
|
||||
"""Used internally by fbchat to support enumerations"""
|
||||
|
||||
def __repr__(self):
|
||||
# For documentation:
|
||||
return "{}.{}".format(type(self).__name__, self.name)
|
32
fbchat/_exception.py
Normal file
32
fbchat/_exception.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class FBchatException(Exception):
|
||||
"""Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this"""
|
||||
|
||||
|
||||
class FBchatFacebookError(FBchatException):
|
||||
#: The error code that Facebook returned
|
||||
fb_error_code = None
|
||||
#: The error message that Facebook returned (In the user's own language)
|
||||
fb_error_message = None
|
||||
#: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200)
|
||||
request_status_code = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message,
|
||||
fb_error_code=None,
|
||||
fb_error_message=None,
|
||||
request_status_code=None,
|
||||
):
|
||||
super(FBchatFacebookError, self).__init__(message)
|
||||
"""Thrown by fbchat when Facebook returns an error"""
|
||||
self.fb_error_code = str(fb_error_code)
|
||||
self.fb_error_message = fb_error_message
|
||||
self.request_status_code = request_status_code
|
||||
|
||||
|
||||
class FBchatUserError(FBchatException):
|
||||
"""Thrown by fbchat when wrong values are entered"""
|
197
fbchat/_file.py
Normal file
197
fbchat/_file.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from ._attachment import Attachment
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class FileAttachment(Attachment):
|
||||
"""Represents a file that has been sent as a Facebook attachment"""
|
||||
|
||||
#: Url where you can download the file
|
||||
url = attr.ib(None)
|
||||
#: Size of the file in bytes
|
||||
size = attr.ib(None)
|
||||
#: Name of the file
|
||||
name = attr.ib(None)
|
||||
#: Whether Facebook determines that this file may be harmful
|
||||
is_malicious = attr.ib(None)
|
||||
|
||||
# Put here for backwards compatibility, so that the init argument order is preserved
|
||||
uid = attr.ib(None)
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class AudioAttachment(Attachment):
|
||||
"""Represents an audio file that has been sent as a Facebook attachment"""
|
||||
|
||||
#: Name of the file
|
||||
filename = attr.ib(None)
|
||||
#: Url of the audio file
|
||||
url = attr.ib(None)
|
||||
#: Duration of the audioclip in milliseconds
|
||||
duration = attr.ib(None)
|
||||
#: Audio type
|
||||
audio_type = attr.ib(None)
|
||||
|
||||
# Put here for backwards compatibility, so that the init argument order is preserved
|
||||
uid = attr.ib(None)
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class ImageAttachment(Attachment):
|
||||
"""Represents an image that has been sent as a Facebook attachment
|
||||
|
||||
To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`, and pass
|
||||
it the uid of the image attachment
|
||||
"""
|
||||
|
||||
#: The extension of the original image (eg. 'png')
|
||||
original_extension = attr.ib(None)
|
||||
#: Width of original image
|
||||
width = attr.ib(None, converter=lambda x: None if x is None else int(x))
|
||||
#: Height of original image
|
||||
height = attr.ib(None, converter=lambda x: None if x is None else int(x))
|
||||
|
||||
#: Whether the image is animated
|
||||
is_animated = attr.ib(None)
|
||||
|
||||
#: URL to a thumbnail of the image
|
||||
thumbnail_url = attr.ib(None)
|
||||
|
||||
#: URL to a medium preview of the image
|
||||
preview_url = attr.ib(None)
|
||||
#: Width of the medium preview image
|
||||
preview_width = attr.ib(None)
|
||||
#: Height of the medium preview image
|
||||
preview_height = attr.ib(None)
|
||||
|
||||
#: URL to a large preview of the image
|
||||
large_preview_url = attr.ib(None)
|
||||
#: Width of the large preview image
|
||||
large_preview_width = attr.ib(None)
|
||||
#: Height of the large preview image
|
||||
large_preview_height = attr.ib(None)
|
||||
|
||||
#: URL to an animated preview of the image (eg. for gifs)
|
||||
animated_preview_url = attr.ib(None)
|
||||
#: Width of the animated preview image
|
||||
animated_preview_width = attr.ib(None)
|
||||
#: Height of the animated preview image
|
||||
animated_preview_height = attr.ib(None)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
original_extension=None,
|
||||
width=None,
|
||||
height=None,
|
||||
is_animated=None,
|
||||
thumbnail_url=None,
|
||||
preview=None,
|
||||
large_preview=None,
|
||||
animated_preview=None,
|
||||
**kwargs
|
||||
):
|
||||
super(ImageAttachment, self).__init__(**kwargs)
|
||||
self.original_extension = original_extension
|
||||
if width is not None:
|
||||
width = int(width)
|
||||
self.width = width
|
||||
if height is not None:
|
||||
height = int(height)
|
||||
self.height = height
|
||||
self.is_animated = is_animated
|
||||
self.thumbnail_url = thumbnail_url
|
||||
|
||||
if preview is None:
|
||||
preview = {}
|
||||
self.preview_url = preview.get("uri")
|
||||
self.preview_width = preview.get("width")
|
||||
self.preview_height = preview.get("height")
|
||||
|
||||
if large_preview is None:
|
||||
large_preview = {}
|
||||
self.large_preview_url = large_preview.get("uri")
|
||||
self.large_preview_width = large_preview.get("width")
|
||||
self.large_preview_height = large_preview.get("height")
|
||||
|
||||
if animated_preview is None:
|
||||
animated_preview = {}
|
||||
self.animated_preview_url = animated_preview.get("uri")
|
||||
self.animated_preview_width = animated_preview.get("width")
|
||||
self.animated_preview_height = animated_preview.get("height")
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class VideoAttachment(Attachment):
|
||||
"""Represents a video that has been sent as a Facebook attachment"""
|
||||
|
||||
#: Size of the original video in bytes
|
||||
size = attr.ib(None)
|
||||
#: Width of original video
|
||||
width = attr.ib(None)
|
||||
#: Height of original video
|
||||
height = attr.ib(None)
|
||||
#: Length of video in milliseconds
|
||||
duration = attr.ib(None)
|
||||
#: URL to very compressed preview video
|
||||
preview_url = attr.ib(None)
|
||||
|
||||
#: URL to a small preview image of the video
|
||||
small_image_url = attr.ib(None)
|
||||
#: Width of the small preview image
|
||||
small_image_width = attr.ib(None)
|
||||
#: Height of the small preview image
|
||||
small_image_height = attr.ib(None)
|
||||
|
||||
#: URL to a medium preview image of the video
|
||||
medium_image_url = attr.ib(None)
|
||||
#: Width of the medium preview image
|
||||
medium_image_width = attr.ib(None)
|
||||
#: Height of the medium preview image
|
||||
medium_image_height = attr.ib(None)
|
||||
|
||||
#: URL to a large preview image of the video
|
||||
large_image_url = attr.ib(None)
|
||||
#: Width of the large preview image
|
||||
large_image_width = attr.ib(None)
|
||||
#: Height of the large preview image
|
||||
large_image_height = attr.ib(None)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size=None,
|
||||
width=None,
|
||||
height=None,
|
||||
duration=None,
|
||||
preview_url=None,
|
||||
small_image=None,
|
||||
medium_image=None,
|
||||
large_image=None,
|
||||
**kwargs
|
||||
):
|
||||
super(VideoAttachment, self).__init__(**kwargs)
|
||||
self.size = size
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.duration = duration
|
||||
self.preview_url = preview_url
|
||||
|
||||
if small_image is None:
|
||||
small_image = {}
|
||||
self.small_image_url = small_image.get("uri")
|
||||
self.small_image_width = small_image.get("width")
|
||||
self.small_image_height = small_image.get("height")
|
||||
|
||||
if medium_image is None:
|
||||
medium_image = {}
|
||||
self.medium_image_url = medium_image.get("uri")
|
||||
self.medium_image_width = medium_image.get("width")
|
||||
self.medium_image_height = medium_image.get("height")
|
||||
|
||||
if large_image is None:
|
||||
large_image = {}
|
||||
self.large_image_url = large_image.get("uri")
|
||||
self.large_image_width = large_image.get("width")
|
||||
self.large_image_height = large_image.get("height")
|
74
fbchat/_group.py
Normal file
74
fbchat/_group.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from ._thread import ThreadType, Thread
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class Group(Thread):
|
||||
"""Represents a Facebook group. Inherits `Thread`"""
|
||||
|
||||
#: Unique list (set) of the group thread's participant user IDs
|
||||
participants = attr.ib(factory=set, converter=lambda x: set() if x is None else x)
|
||||
#: A dict, containing user nicknames mapped to their IDs
|
||||
nicknames = attr.ib(factory=dict, converter=lambda x: {} if x is None else x)
|
||||
#: A :class:`ThreadColor`. The groups's message color
|
||||
color = attr.ib(None)
|
||||
#: The groups's default emoji
|
||||
emoji = attr.ib(None)
|
||||
# Set containing user IDs of thread admins
|
||||
admins = attr.ib(factory=set, converter=lambda x: set() if x is None else x)
|
||||
# True if users need approval to join
|
||||
approval_mode = attr.ib(None)
|
||||
# Set containing user IDs requesting to join
|
||||
approval_requests = attr.ib(
|
||||
factory=set, converter=lambda x: set() if x is None else x
|
||||
)
|
||||
# Link for joining group
|
||||
join_link = attr.ib(None)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uid,
|
||||
participants=None,
|
||||
nicknames=None,
|
||||
color=None,
|
||||
emoji=None,
|
||||
admins=None,
|
||||
approval_mode=None,
|
||||
approval_requests=None,
|
||||
join_link=None,
|
||||
privacy_mode=None,
|
||||
**kwargs
|
||||
):
|
||||
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
|
||||
if participants is None:
|
||||
participants = set()
|
||||
self.participants = participants
|
||||
if nicknames is None:
|
||||
nicknames = []
|
||||
self.nicknames = nicknames
|
||||
self.color = color
|
||||
self.emoji = emoji
|
||||
if admins is None:
|
||||
admins = set()
|
||||
self.admins = admins
|
||||
self.approval_mode = approval_mode
|
||||
if approval_requests is None:
|
||||
approval_requests = set()
|
||||
self.approval_requests = approval_requests
|
||||
self.join_link = join_link
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class Room(Group):
|
||||
"""Deprecated. Use :class:`Group` instead"""
|
||||
|
||||
# True is room is not discoverable
|
||||
privacy_mode = attr.ib(None)
|
||||
|
||||
def __init__(self, uid, privacy_mode=None, **kwargs):
|
||||
super(Room, self).__init__(uid, **kwargs)
|
||||
self.type = ThreadType.ROOM
|
||||
self.privacy_mode = privacy_mode
|
48
fbchat/_location.py
Normal file
48
fbchat/_location.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from ._attachment import Attachment
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class LocationAttachment(Attachment):
|
||||
"""Represents a user location
|
||||
|
||||
Latitude and longitude OR address is provided by Facebook
|
||||
"""
|
||||
|
||||
#: Latitude of the location
|
||||
latitude = attr.ib(None)
|
||||
#: Longitude of the location
|
||||
longitude = attr.ib(None)
|
||||
#: URL of image showing the map of the location
|
||||
image_url = attr.ib(None, init=False)
|
||||
#: Width of the image
|
||||
image_width = attr.ib(None, init=False)
|
||||
#: Height of the image
|
||||
image_height = attr.ib(None, init=False)
|
||||
#: URL to Bing maps with the location
|
||||
url = attr.ib(None, init=False)
|
||||
# Address of the location
|
||||
address = attr.ib(None)
|
||||
|
||||
# Put here for backwards compatibility, so that the init argument order is preserved
|
||||
uid = attr.ib(None)
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class LiveLocationAttachment(LocationAttachment):
|
||||
"""Represents a live user location"""
|
||||
|
||||
#: Name of the location
|
||||
name = attr.ib(None)
|
||||
#: Timestamp when live location expires
|
||||
expiration_time = attr.ib(None)
|
||||
#: True if live location is expired
|
||||
is_expired = attr.ib(None)
|
||||
|
||||
def __init__(self, name=None, expiration_time=None, is_expired=None, **kwargs):
|
||||
super(LiveLocationAttachment, self).__init__(**kwargs)
|
||||
self.expiration_time = expiration_time
|
||||
self.is_expired = is_expired
|
123
fbchat/_message.py
Normal file
123
fbchat/_message.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from string import Formatter
|
||||
from ._core import Enum
|
||||
|
||||
|
||||
class EmojiSize(Enum):
|
||||
"""Used to specify the size of a sent emoji"""
|
||||
|
||||
LARGE = "369239383222810"
|
||||
MEDIUM = "369239343222814"
|
||||
SMALL = "369239263222822"
|
||||
|
||||
|
||||
class MessageReaction(Enum):
|
||||
"""Used to specify a message reaction"""
|
||||
|
||||
LOVE = "😍"
|
||||
SMILE = "😆"
|
||||
WOW = "😮"
|
||||
SAD = "😢"
|
||||
ANGRY = "😠"
|
||||
YES = "👍"
|
||||
NO = "👎"
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class Mention(object):
|
||||
"""Represents a @mention"""
|
||||
|
||||
#: The thread ID the mention is pointing at
|
||||
thread_id = attr.ib()
|
||||
#: The character where the mention starts
|
||||
offset = attr.ib(0)
|
||||
#: The length of the mention
|
||||
length = attr.ib(10)
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class Message(object):
|
||||
"""Represents a Facebook message"""
|
||||
|
||||
#: The actual message
|
||||
text = attr.ib(None)
|
||||
#: A list of :class:`Mention` objects
|
||||
mentions = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
|
||||
#: A :class:`EmojiSize`. Size of a sent emoji
|
||||
emoji_size = attr.ib(None)
|
||||
#: The message ID
|
||||
uid = attr.ib(None, init=False)
|
||||
#: ID of the sender
|
||||
author = attr.ib(None, init=False)
|
||||
#: Timestamp of when the message was sent
|
||||
timestamp = attr.ib(None, init=False)
|
||||
#: Whether the message is read
|
||||
is_read = attr.ib(None, init=False)
|
||||
#: A list of pepole IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages`
|
||||
read_by = attr.ib(factory=list, init=False)
|
||||
#: A dict with user's IDs as keys, and their :class:`MessageReaction` as values
|
||||
reactions = attr.ib(factory=dict, init=False)
|
||||
#: A :class:`Sticker`
|
||||
sticker = attr.ib(None)
|
||||
#: A list of attachments
|
||||
attachments = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
|
||||
#: A list of :class:`QuickReply`
|
||||
quick_replies = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
|
||||
#: Whether the message is unsent (deleted for everyone)
|
||||
unsent = attr.ib(False, init=False)
|
||||
|
||||
@classmethod
|
||||
def formatMentions(cls, text, *args, **kwargs):
|
||||
"""Like `str.format`, but takes tuples with a thread id and text instead.
|
||||
|
||||
Returns a `Message` object, with the formatted string and relevant mentions.
|
||||
|
||||
```
|
||||
>>> Message.formatMentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael"))
|
||||
<Message (None): "Hey 'Peter'! My name is Michael", mentions=[<Mention 1234: offset=4 length=7>, <Mention 4321: offset=24 length=7>] emoji_size=None attachments=[]>
|
||||
|
||||
>>> Message.formatMentions("Hey {p}! My name is {}", ("1234", "Michael"), p=("4321", "Peter"))
|
||||
<Message (None): 'Hey Peter! My name is Michael', mentions=[<Mention 4321: offset=4 length=5>, <Mention 1234: offset=22 length=7>] emoji_size=None attachments=[]>
|
||||
```
|
||||
"""
|
||||
result = ""
|
||||
mentions = list()
|
||||
offset = 0
|
||||
f = Formatter()
|
||||
field_names = [field_name[1] for field_name in f.parse(text)]
|
||||
automatic = "" in field_names
|
||||
i = 0
|
||||
|
||||
for (literal_text, field_name, format_spec, conversion) in f.parse(text):
|
||||
offset += len(literal_text)
|
||||
result += literal_text
|
||||
|
||||
if field_name is None:
|
||||
continue
|
||||
|
||||
if field_name == "":
|
||||
field_name = str(i)
|
||||
i += 1
|
||||
elif automatic and field_name.isdigit():
|
||||
raise ValueError(
|
||||
"cannot switch from automatic field numbering to manual field specification"
|
||||
)
|
||||
|
||||
thread_id, name = f.get_field(field_name, args, kwargs)[0]
|
||||
|
||||
if format_spec:
|
||||
name = f.format_field(name, format_spec)
|
||||
if conversion:
|
||||
name = f.convert_field(name, conversion)
|
||||
|
||||
result += name
|
||||
mentions.append(
|
||||
Mention(thread_id=thread_id, offset=offset, length=len(name))
|
||||
)
|
||||
offset += len(name)
|
||||
|
||||
message = cls(text=result, mentions=mentions)
|
||||
return message
|
38
fbchat/_page.py
Normal file
38
fbchat/_page.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from ._thread import ThreadType, Thread
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class Page(Thread):
|
||||
"""Represents a Facebook page. Inherits `Thread`"""
|
||||
|
||||
#: The page's custom url
|
||||
url = attr.ib(None)
|
||||
#: The name of the page's location city
|
||||
city = attr.ib(None)
|
||||
#: Amount of likes the page has
|
||||
likes = attr.ib(None)
|
||||
#: Some extra information about the page
|
||||
sub_title = attr.ib(None)
|
||||
#: The page's category
|
||||
category = attr.ib(None)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uid,
|
||||
url=None,
|
||||
city=None,
|
||||
likes=None,
|
||||
sub_title=None,
|
||||
category=None,
|
||||
**kwargs
|
||||
):
|
||||
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
|
28
fbchat/_plan.py
Normal file
28
fbchat/_plan.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class Plan(object):
|
||||
"""Represents a plan"""
|
||||
|
||||
#: ID of the plan
|
||||
uid = attr.ib(None, init=False)
|
||||
#: Plan time (unix time stamp), only precise down to the minute
|
||||
time = attr.ib(converter=int)
|
||||
#: Plan title
|
||||
title = attr.ib()
|
||||
#: Plan location name
|
||||
location = attr.ib(None, converter=lambda x: x or "")
|
||||
#: Plan location ID
|
||||
location_id = attr.ib(None, converter=lambda x: x or "")
|
||||
#: ID of the plan creator
|
||||
author_id = attr.ib(None, init=False)
|
||||
#: List of the people IDs who will take part in the plan
|
||||
going = attr.ib(factory=list, init=False)
|
||||
#: List of the people IDs who won't take part in the plan
|
||||
declined = attr.ib(factory=list, init=False)
|
||||
#: List of the people IDs who are invited to the plan
|
||||
invited = attr.ib(factory=list, init=False)
|
34
fbchat/_poll.py
Normal file
34
fbchat/_poll.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class Poll(object):
|
||||
"""Represents a poll"""
|
||||
|
||||
#: ID of the poll
|
||||
uid = attr.ib(None, init=False)
|
||||
#: Title of the poll
|
||||
title = attr.ib()
|
||||
#: List of :class:`PollOption`, can be fetched with :func:`fbchat.Client.fetchPollOptions`
|
||||
options = attr.ib()
|
||||
#: Options count
|
||||
options_count = attr.ib(None, init=False)
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class PollOption(object):
|
||||
"""Represents a poll option"""
|
||||
|
||||
#: ID of the poll option
|
||||
uid = attr.ib(None, init=False)
|
||||
#: Text of the poll option
|
||||
text = attr.ib()
|
||||
#: Whether vote when creating or client voted
|
||||
vote = attr.ib(False)
|
||||
#: ID of the users who voted for this poll option
|
||||
voters = attr.ib(None, init=False)
|
||||
#: Votes count
|
||||
votes_count = attr.ib(None, init=False)
|
76
fbchat/_quick_reply.py
Normal file
76
fbchat/_quick_reply.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from ._attachment import Attachment
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class QuickReply(object):
|
||||
"""Represents a quick reply"""
|
||||
|
||||
#: Payload of the quick reply
|
||||
payload = attr.ib(None)
|
||||
#: External payload for responses
|
||||
external_payload = attr.ib(None, init=False)
|
||||
#: Additional data
|
||||
data = attr.ib(None)
|
||||
#: Whether it's a response for a quick reply
|
||||
is_response = attr.ib(False)
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class QuickReplyText(QuickReply):
|
||||
"""Represents a text quick reply"""
|
||||
|
||||
#: Title of the quick reply
|
||||
title = attr.ib(None)
|
||||
#: URL of the quick reply image (optional)
|
||||
image_url = attr.ib(None)
|
||||
#: Type of the quick reply
|
||||
_type = "text"
|
||||
|
||||
def __init__(self, title=None, image_url=None, **kwargs):
|
||||
super(QuickReplyText, self).__init__(**kwargs)
|
||||
self.title = title
|
||||
self.image_url = image_url
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class QuickReplyLocation(QuickReply):
|
||||
"""Represents a location quick reply (Doesn't work on mobile)"""
|
||||
|
||||
#: Type of the quick reply
|
||||
_type = "location"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(QuickReplyLocation, self).__init__(**kwargs)
|
||||
self.is_response = False
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class QuickReplyPhoneNumber(QuickReply):
|
||||
"""Represents a phone number quick reply (Doesn't work on mobile)"""
|
||||
|
||||
#: URL of the quick reply image (optional)
|
||||
image_url = attr.ib(None)
|
||||
#: Type of the quick reply
|
||||
_type = "user_phone_number"
|
||||
|
||||
def __init__(self, image_url=None, **kwargs):
|
||||
super(QuickReplyPhoneNumber, self).__init__(**kwargs)
|
||||
self.image_url = image_url
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class QuickReplyEmail(QuickReply):
|
||||
"""Represents an email quick reply (Doesn't work on mobile)"""
|
||||
|
||||
#: URL of the quick reply image (optional)
|
||||
image_url = attr.ib(None)
|
||||
#: Type of the quick reply
|
||||
_type = "user_email"
|
||||
|
||||
def __init__(self, image_url=None, **kwargs):
|
||||
super(QuickReplyEmail, self).__init__(**kwargs)
|
||||
self.image_url = image_url
|
39
fbchat/_sticker.py
Normal file
39
fbchat/_sticker.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from ._attachment import Attachment
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class Sticker(Attachment):
|
||||
"""Represents a Facebook sticker that has been sent to a thread as an attachment"""
|
||||
|
||||
#: The sticker-pack's ID
|
||||
pack = attr.ib(None)
|
||||
#: Whether the sticker is animated
|
||||
is_animated = attr.ib(False)
|
||||
|
||||
# If the sticker is animated, the following should be present
|
||||
#: URL to a medium spritemap
|
||||
medium_sprite_image = attr.ib(None)
|
||||
#: URL to a large spritemap
|
||||
large_sprite_image = attr.ib(None)
|
||||
#: The amount of frames present in the spritemap pr. row
|
||||
frames_per_row = attr.ib(None)
|
||||
#: The amount of frames present in the spritemap pr. coloumn
|
||||
frames_per_col = attr.ib(None)
|
||||
#: The frame rate the spritemap is intended to be played in
|
||||
frame_rate = attr.ib(None)
|
||||
|
||||
#: URL to the sticker's image
|
||||
url = attr.ib(None)
|
||||
#: Width of the sticker
|
||||
width = attr.ib(None)
|
||||
#: Height of the sticker
|
||||
height = attr.ib(None)
|
||||
#: The sticker's label/name
|
||||
label = attr.ib(None)
|
||||
|
||||
def __init__(self, uid=None):
|
||||
super(Sticker, self).__init__(uid=uid)
|
81
fbchat/_thread.py
Normal file
81
fbchat/_thread.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from ._core import Enum
|
||||
|
||||
|
||||
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
|
||||
ROOM = 2
|
||||
PAGE = 3
|
||||
|
||||
|
||||
class ThreadLocation(Enum):
|
||||
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
|
||||
|
||||
INBOX = "INBOX"
|
||||
PENDING = "PENDING"
|
||||
ARCHIVED = "ARCHIVED"
|
||||
OTHER = "OTHER"
|
||||
|
||||
|
||||
class ThreadColor(Enum):
|
||||
"""Used to specify a thread colors"""
|
||||
|
||||
MESSENGER_BLUE = "#0084ff"
|
||||
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"
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class Thread(object):
|
||||
"""Represents a Facebook thread"""
|
||||
|
||||
#: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info
|
||||
uid = attr.ib(converter=str)
|
||||
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info
|
||||
type = attr.ib()
|
||||
#: A url to the thread's picture
|
||||
photo = attr.ib(None)
|
||||
#: The name of the thread
|
||||
name = attr.ib(None)
|
||||
#: Timestamp of last message
|
||||
last_message_timestamp = attr.ib(None)
|
||||
#: Number of messages in the thread
|
||||
message_count = attr.ib(None)
|
||||
#: Set :class:`Plan`
|
||||
plan = attr.ib(None)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
_type,
|
||||
uid,
|
||||
photo=None,
|
||||
name=None,
|
||||
last_message_timestamp=None,
|
||||
message_count=None,
|
||||
plan=None,
|
||||
):
|
||||
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
|
||||
self.plan = plan
|
76
fbchat/_user.py
Normal file
76
fbchat/_user.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import attr
|
||||
from ._core import Enum
|
||||
from ._thread import ThreadType, Thread
|
||||
|
||||
|
||||
class TypingStatus(Enum):
|
||||
"""Used to specify whether the user is typing or has stopped typing"""
|
||||
|
||||
STOPPED = 0
|
||||
TYPING = 1
|
||||
|
||||
|
||||
@attr.s(cmp=False, init=False)
|
||||
class User(Thread):
|
||||
"""Represents a Facebook user. Inherits `Thread`"""
|
||||
|
||||
#: The profile url
|
||||
url = attr.ib(None)
|
||||
#: The users first name
|
||||
first_name = attr.ib(None)
|
||||
#: The users last name
|
||||
last_name = attr.ib(None)
|
||||
#: Whether the user and the client are friends
|
||||
is_friend = attr.ib(None)
|
||||
#: The user's gender
|
||||
gender = attr.ib(None)
|
||||
#: From 0 to 1. How close the client is to the user
|
||||
affinity = attr.ib(None)
|
||||
#: The user's nickname
|
||||
nickname = attr.ib(None)
|
||||
#: The clients nickname, as seen by the user
|
||||
own_nickname = attr.ib(None)
|
||||
#: A :class:`ThreadColor`. The message color
|
||||
color = attr.ib(None)
|
||||
#: The default emoji
|
||||
emoji = attr.ib(None)
|
||||
|
||||
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
|
||||
):
|
||||
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
|
||||
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class ActiveStatus(object):
|
||||
#: Whether the user is active now
|
||||
active = attr.ib(None)
|
||||
#: Timestamp when the user was last active
|
||||
last_active = attr.ib(None)
|
||||
#: Whether the user is playing Messenger game now
|
||||
in_game = attr.ib(None)
|
3011
fbchat/client.py
3011
fbchat/client.py
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,8 @@ 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)
|
||||
WHITESPACE = re.compile(r"[ \t\n\r]*", FLAGS)
|
||||
|
||||
|
||||
class ConcatJSONDecoder(json.JSONDecoder):
|
||||
def decode(self, s, _w=WHITESPACE.match):
|
||||
@@ -21,367 +22,589 @@ class ConcatJSONDecoder(json.JSONDecoder):
|
||||
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:
|
||||
if not color:
|
||||
return ThreadColor.MESSENGER_BLUE
|
||||
try:
|
||||
return ThreadColor('#{}'.format(color[2:].lower()))
|
||||
except ValueError:
|
||||
raise FBchatException('Could not get ThreadColor from color: {}'.format(color))
|
||||
color = color[2:] # Strip the alpha value
|
||||
color_value = "#{}".format(color.lower())
|
||||
return enum_extend_if_invalid(ThreadColor, color_value)
|
||||
|
||||
|
||||
def get_customization_info(thread):
|
||||
if thread is None or thread.get('customization_info') is None:
|
||||
if thread is None or thread.get("customization_info") is None:
|
||||
return {}
|
||||
info = thread['customization_info']
|
||||
info = thread["customization_info"]
|
||||
|
||||
rtn = {
|
||||
'emoji': info.get('emoji'),
|
||||
'color': graphql_color_to_enum(info.get('outgoing_bubble_color'))
|
||||
"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 (
|
||||
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')
|
||||
if pc[0].get("participant_id") == uid:
|
||||
rtn["nickname"] = pc[0].get("nickname")
|
||||
else:
|
||||
rtn['own_nickname'] = pc[0].get('nickname')
|
||||
rtn["own_nickname"] = pc[0].get("nickname")
|
||||
if len(pc) > 1:
|
||||
if pc[1].get('participant_id') == uid:
|
||||
rtn['nickname'] = pc[1].get('nickname')
|
||||
if pc[1].get("participant_id") == uid:
|
||||
rtn["nickname"] = pc[1].get("nickname")
|
||||
else:
|
||||
rtn['own_nickname'] = pc[1].get('nickname')
|
||||
rtn["own_nickname"] = pc[1].get("nickname")
|
||||
return rtn
|
||||
|
||||
|
||||
def graphql_to_sticker(s):
|
||||
if not s:
|
||||
return None
|
||||
sticker = Sticker(
|
||||
uid=s['id']
|
||||
)
|
||||
if s.get('pack'):
|
||||
sticker.pack = s['pack'].get('id')
|
||||
if s.get('sprite_image'):
|
||||
sticker = Sticker(uid=s["id"])
|
||||
if s.get("pack"):
|
||||
sticker.pack = s["pack"].get("id")
|
||||
if s.get("sprite_image"):
|
||||
sticker.is_animated = True
|
||||
sticker.medium_sprite_image = s['sprite_image'].get('uri')
|
||||
sticker.large_sprite_image = s['sprite_image_2x'].get('uri')
|
||||
sticker.frames_per_row = s.get('frames_per_row')
|
||||
sticker.frames_per_col = s.get('frames_per_column')
|
||||
sticker.frame_rate = s.get('frame_rate')
|
||||
sticker.url = s.get('url')
|
||||
sticker.width = s.get('width')
|
||||
sticker.height = s.get('height')
|
||||
if s.get('label'):
|
||||
sticker.label = s['label']
|
||||
sticker.medium_sprite_image = s["sprite_image"].get("uri")
|
||||
sticker.large_sprite_image = s["sprite_image_2x"].get("uri")
|
||||
sticker.frames_per_row = s.get("frames_per_row")
|
||||
sticker.frames_per_col = s.get("frames_per_column")
|
||||
sticker.frame_rate = s.get("frame_rate")
|
||||
sticker.url = s.get("url")
|
||||
sticker.width = s.get("width")
|
||||
sticker.height = s.get("height")
|
||||
if s.get("label"):
|
||||
sticker.label = s["label"]
|
||||
return sticker
|
||||
|
||||
|
||||
def graphql_to_attachment(a):
|
||||
_type = a['__typename']
|
||||
if _type in ['MessageImage', 'MessageAnimatedImage']:
|
||||
_type = a["__typename"]
|
||||
if _type in ["MessageImage", "MessageAnimatedImage"]:
|
||||
return ImageAttachment(
|
||||
original_extension=a.get('original_extension') or (a['filename'].split('-')[0] if a.get('filename') else None),
|
||||
width=a.get('original_dimensions', {}).get('width'),
|
||||
height=a.get('original_dimensions', {}).get('height'),
|
||||
is_animated=_type=='MessageAnimatedImage',
|
||||
thumbnail_url=a.get('thumbnail', {}).get('uri'),
|
||||
preview=a.get('preview') or a.get('preview_image'),
|
||||
large_preview=a.get('large_preview'),
|
||||
animated_preview=a.get('animated_image'),
|
||||
uid=a.get('legacy_attachment_id')
|
||||
original_extension=a.get("original_extension")
|
||||
or (a["filename"].split("-")[0] if a.get("filename") else None),
|
||||
width=a.get("original_dimensions", {}).get("width"),
|
||||
height=a.get("original_dimensions", {}).get("height"),
|
||||
is_animated=_type == "MessageAnimatedImage",
|
||||
thumbnail_url=a.get("thumbnail", {}).get("uri"),
|
||||
preview=a.get("preview") or a.get("preview_image"),
|
||||
large_preview=a.get("large_preview"),
|
||||
animated_preview=a.get("animated_image"),
|
||||
uid=a.get("legacy_attachment_id"),
|
||||
)
|
||||
elif _type == 'MessageVideo':
|
||||
elif _type == "MessageVideo":
|
||||
return VideoAttachment(
|
||||
width=a.get('original_dimensions', {}).get('width'),
|
||||
height=a.get('original_dimensions', {}).get('height'),
|
||||
duration=a.get('playable_duration_in_ms'),
|
||||
preview_url=a.get('playable_url'),
|
||||
small_image=a.get('chat_image'),
|
||||
medium_image=a.get('inbox_image'),
|
||||
large_image=a.get('large_image'),
|
||||
uid=a.get('legacy_attachment_id')
|
||||
width=a.get("original_dimensions", {}).get("width"),
|
||||
height=a.get("original_dimensions", {}).get("height"),
|
||||
duration=a.get("playable_duration_in_ms"),
|
||||
preview_url=a.get("playable_url"),
|
||||
small_image=a.get("chat_image"),
|
||||
medium_image=a.get("inbox_image"),
|
||||
large_image=a.get("large_image"),
|
||||
uid=a.get("legacy_attachment_id"),
|
||||
)
|
||||
elif _type == 'MessageAudio':
|
||||
elif _type == "MessageAudio":
|
||||
return AudioAttachment(
|
||||
filename=a.get('filename'),
|
||||
url=a.get('playable_url'),
|
||||
duration=a.get('playable_duration_in_ms'),
|
||||
audio_type=a.get('audio_type')
|
||||
filename=a.get("filename"),
|
||||
url=a.get("playable_url"),
|
||||
duration=a.get("playable_duration_in_ms"),
|
||||
audio_type=a.get("audio_type"),
|
||||
)
|
||||
elif _type == 'MessageFile':
|
||||
elif _type == "MessageFile":
|
||||
return FileAttachment(
|
||||
url=a.get('url'),
|
||||
name=a.get('filename'),
|
||||
is_malicious=a.get('is_malicious'),
|
||||
uid=a.get('message_file_fbid')
|
||||
url=a.get("url"),
|
||||
name=a.get("filename"),
|
||||
is_malicious=a.get("is_malicious"),
|
||||
uid=a.get("message_file_fbid"),
|
||||
)
|
||||
else:
|
||||
return Attachment(
|
||||
uid=a.get('legacy_attachment_id')
|
||||
return Attachment(uid=a.get("legacy_attachment_id"))
|
||||
|
||||
|
||||
def graphql_to_extensible_attachment(a):
|
||||
story = a.get("story_attachment")
|
||||
if story:
|
||||
target = story.get("target")
|
||||
if target:
|
||||
_type = target["__typename"]
|
||||
if _type == "MessageLocation":
|
||||
url = story.get("url")
|
||||
address = get_url_parameter(get_url_parameter(url, "u"), "where1")
|
||||
try:
|
||||
latitude, longitude = [float(x) for x in address.split(", ")]
|
||||
address = None
|
||||
except ValueError:
|
||||
latitude, longitude = None, None
|
||||
rtn = LocationAttachment(
|
||||
uid=int(story["deduplication_key"]),
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
address=address,
|
||||
)
|
||||
media = story.get("media")
|
||||
if media and media.get("image"):
|
||||
image = media["image"]
|
||||
rtn.image_url = image.get("uri")
|
||||
rtn.image_width = image.get("width")
|
||||
rtn.image_height = image.get("height")
|
||||
rtn.url = url
|
||||
return rtn
|
||||
elif _type == "MessageLiveLocation":
|
||||
rtn = LiveLocationAttachment(
|
||||
uid=int(story["target"]["live_location_id"]),
|
||||
latitude=story["target"]["coordinate"]["latitude"]
|
||||
if story["target"].get("coordinate")
|
||||
else None,
|
||||
longitude=story["target"]["coordinate"]["longitude"]
|
||||
if story["target"].get("coordinate")
|
||||
else None,
|
||||
name=story["title_with_entities"]["text"],
|
||||
expiration_time=story["target"].get("expiration_time"),
|
||||
is_expired=story["target"].get("is_expired"),
|
||||
)
|
||||
media = story.get("media")
|
||||
if media and media.get("image"):
|
||||
image = media["image"]
|
||||
rtn.image_url = image.get("uri")
|
||||
rtn.image_width = image.get("width")
|
||||
rtn.image_height = image.get("height")
|
||||
rtn.url = story.get("url")
|
||||
return rtn
|
||||
elif _type in ["ExternalUrl", "Story"]:
|
||||
url = story.get("url")
|
||||
rtn = ShareAttachment(
|
||||
uid=a.get("legacy_attachment_id"),
|
||||
author=story["target"]["actors"][0]["id"]
|
||||
if story["target"].get("actors")
|
||||
else None,
|
||||
url=url,
|
||||
original_url=get_url_parameter(url, "u")
|
||||
if "/l.php?u=" in url
|
||||
else url,
|
||||
title=story["title_with_entities"].get("text"),
|
||||
description=story["description"].get("text")
|
||||
if story.get("description")
|
||||
else None,
|
||||
source=story["source"].get("text"),
|
||||
attachments=[
|
||||
graphql_to_subattachment(attachment)
|
||||
for attachment in story.get("subattachments")
|
||||
],
|
||||
)
|
||||
media = story.get("media")
|
||||
if media and media.get("image"):
|
||||
image = media["image"]
|
||||
rtn.image_url = image.get("uri")
|
||||
rtn.original_image_url = (
|
||||
get_url_parameter(rtn.image_url, "url")
|
||||
if "/safe_image.php" in rtn.image_url
|
||||
else rtn.image_url
|
||||
)
|
||||
rtn.image_width = image.get("width")
|
||||
rtn.image_height = image.get("height")
|
||||
return rtn
|
||||
else:
|
||||
return UnsentMessage(uid=a.get("legacy_attachment_id"))
|
||||
|
||||
|
||||
def graphql_to_subattachment(a):
|
||||
_type = a["target"]["__typename"]
|
||||
if _type == "Video":
|
||||
media = a["media"]
|
||||
return VideoAttachment(
|
||||
duration=media.get("playable_duration_in_ms"),
|
||||
preview_url=media.get("playable_url"),
|
||||
medium_image=media.get("image"),
|
||||
uid=a["target"].get("video_id"),
|
||||
)
|
||||
|
||||
|
||||
def graphql_to_live_location(a):
|
||||
return LiveLocationAttachment(
|
||||
uid=a["id"],
|
||||
latitude=a["coordinate"]["latitude"] / (10 ** 8)
|
||||
if not a.get("stopReason")
|
||||
else None,
|
||||
longitude=a["coordinate"]["longitude"] / (10 ** 8)
|
||||
if not a.get("stopReason")
|
||||
else None,
|
||||
name=a.get("locationTitle"),
|
||||
expiration_time=a["expirationTime"],
|
||||
is_expired=bool(a.get("stopReason")),
|
||||
)
|
||||
|
||||
|
||||
def graphql_to_poll(a):
|
||||
rtn = Poll(
|
||||
title=a.get('title') if a.get('title') else a.get("text"),
|
||||
options=[graphql_to_poll_option(m) for m in a.get('options')]
|
||||
title=a.get("title") if a.get("title") else a.get("text"),
|
||||
options=[graphql_to_poll_option(m) for m in a.get("options")],
|
||||
)
|
||||
rtn.uid = int(a["id"])
|
||||
rtn.options_count = a.get("total_count")
|
||||
return rtn
|
||||
|
||||
|
||||
def graphql_to_poll_option(a):
|
||||
if a.get('viewer_has_voted') is None:
|
||||
if a.get("viewer_has_voted") is None:
|
||||
vote = None
|
||||
elif isinstance(a['viewer_has_voted'], bool):
|
||||
vote = a['viewer_has_voted']
|
||||
elif isinstance(a["viewer_has_voted"], bool):
|
||||
vote = a["viewer_has_voted"]
|
||||
else:
|
||||
vote = a['viewer_has_voted'] == 'true'
|
||||
rtn = PollOption(
|
||||
text=a.get('text'),
|
||||
vote=vote
|
||||
)
|
||||
vote = a["viewer_has_voted"] == "true"
|
||||
rtn = PollOption(text=a.get("text"), vote=vote)
|
||||
rtn.uid = int(a["id"])
|
||||
rtn.voters = [m.get('node').get('id') for m in a.get('voters').get('edges')] if isinstance(a.get('voters'), dict) else a.get('voters')
|
||||
rtn.votes_count = a.get('voters').get('count') if isinstance(a.get('voters'), dict) else a.get('total_count')
|
||||
rtn.voters = (
|
||||
[m.get("node").get("id") for m in a.get("voters").get("edges")]
|
||||
if isinstance(a.get("voters"), dict)
|
||||
else a.get("voters")
|
||||
)
|
||||
rtn.votes_count = (
|
||||
a.get("voters").get("count")
|
||||
if isinstance(a.get("voters"), dict)
|
||||
else a.get("total_count")
|
||||
)
|
||||
return rtn
|
||||
|
||||
|
||||
def graphql_to_plan(a):
|
||||
if a.get('event_members'):
|
||||
if a.get("event_members"):
|
||||
rtn = Plan(
|
||||
time=a.get('event_time'),
|
||||
title=a.get('title'),
|
||||
location=a.get('location_name')
|
||||
time=a.get("event_time"),
|
||||
title=a.get("title"),
|
||||
location=a.get("location_name"),
|
||||
)
|
||||
if a.get('location_id') != 0:
|
||||
rtn.location_id = str(a.get('location_id'))
|
||||
rtn.uid = a.get('oid')
|
||||
rtn.author_id = a.get('creator_id')
|
||||
if a.get("location_id") != 0:
|
||||
rtn.location_id = str(a.get("location_id"))
|
||||
rtn.uid = a.get("oid")
|
||||
rtn.author_id = a.get("creator_id")
|
||||
guests = a.get("event_members")
|
||||
rtn.going = [uid for uid in guests if guests[uid] == "GOING"]
|
||||
rtn.declined = [uid for uid in guests if guests[uid] == "DECLINED"]
|
||||
rtn.invited = [uid for uid in guests if guests[uid] == "INVITED"]
|
||||
return rtn
|
||||
elif a.get('id') is None:
|
||||
elif a.get("id") is None:
|
||||
rtn = Plan(
|
||||
time=a.get('event_time'),
|
||||
title=a.get('event_title'),
|
||||
location=a.get('event_location_name'),
|
||||
location_id=a.get('event_location_id')
|
||||
time=a.get("event_time"),
|
||||
title=a.get("event_title"),
|
||||
location=a.get("event_location_name"),
|
||||
location_id=a.get("event_location_id"),
|
||||
)
|
||||
rtn.uid = a.get('event_id')
|
||||
rtn.author_id = a.get('event_creator_id')
|
||||
guests = json.loads(a.get('guest_state_list'))
|
||||
rtn.uid = a.get("event_id")
|
||||
rtn.author_id = a.get("event_creator_id")
|
||||
guests = json.loads(a.get("guest_state_list"))
|
||||
else:
|
||||
rtn = Plan(
|
||||
time=a.get('time'),
|
||||
title=a.get('event_title'),
|
||||
location=a.get('location_name')
|
||||
time=a.get("time"),
|
||||
title=a.get("event_title"),
|
||||
location=a.get("location_name"),
|
||||
)
|
||||
rtn.uid = a.get('id')
|
||||
rtn.author_id = a.get('lightweight_event_creator').get('id')
|
||||
guests = a.get('event_reminder_members').get('edges')
|
||||
rtn.going = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "GOING"]
|
||||
rtn.declined = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "DECLINED"]
|
||||
rtn.invited = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "INVITED"]
|
||||
rtn.uid = a.get("id")
|
||||
rtn.author_id = a.get("lightweight_event_creator").get("id")
|
||||
guests = a.get("event_reminder_members").get("edges")
|
||||
rtn.going = [
|
||||
m.get("node").get("id") for m in guests if m.get("guest_list_state") == "GOING"
|
||||
]
|
||||
rtn.declined = [
|
||||
m.get("node").get("id")
|
||||
for m in guests
|
||||
if m.get("guest_list_state") == "DECLINED"
|
||||
]
|
||||
rtn.invited = [
|
||||
m.get("node").get("id")
|
||||
for m in guests
|
||||
if m.get("guest_list_state") == "INVITED"
|
||||
]
|
||||
return rtn
|
||||
|
||||
|
||||
def graphql_to_quick_reply(q, is_response=False):
|
||||
data = dict()
|
||||
_type = q.get("content_type").lower()
|
||||
if q.get("payload"):
|
||||
data["payload"] = q["payload"]
|
||||
if q.get("data"):
|
||||
data["data"] = q["data"]
|
||||
if q.get("image_url") and _type is not QuickReplyLocation._type:
|
||||
data["image_url"] = q["image_url"]
|
||||
data["is_response"] = is_response
|
||||
if _type == QuickReplyText._type:
|
||||
if q.get("title") is not None:
|
||||
data["title"] = q["title"]
|
||||
rtn = QuickReplyText(**data)
|
||||
elif _type == QuickReplyLocation._type:
|
||||
rtn = QuickReplyLocation(**data)
|
||||
elif _type == QuickReplyPhoneNumber._type:
|
||||
rtn = QuickReplyPhoneNumber(**data)
|
||||
elif _type == QuickReplyEmail._type:
|
||||
rtn = QuickReplyEmail(**data)
|
||||
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'] = {}
|
||||
if message.get("message_sender") is None:
|
||||
message["message_sender"] = {}
|
||||
if message.get("message") is None:
|
||||
message["message"] = {}
|
||||
rtn = Message(
|
||||
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', [])],
|
||||
emoji_size=get_emojisize_from_tags(message.get('tags_list')),
|
||||
sticker=graphql_to_sticker(message.get('sticker'))
|
||||
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", [])
|
||||
],
|
||||
emoji_size=get_emojisize_from_tags(message.get("tags_list")),
|
||||
sticker=graphql_to_sticker(message.get("sticker")),
|
||||
)
|
||||
rtn.uid = str(message.get('message_id'))
|
||||
rtn.author = str(message.get('message_sender').get('id'))
|
||||
rtn.timestamp = message.get('timestamp_precise')
|
||||
if message.get('unread') is not None:
|
||||
rtn.is_read = not message['unread']
|
||||
rtn.reactions = {str(r['user']['id']):MessageReaction(r['reaction']) for r in message.get('message_reactions')}
|
||||
if message.get('blob_attachments') is not None:
|
||||
rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']]
|
||||
# TODO: This is still missing parsing:
|
||||
# message.get('extensible_attachment')
|
||||
rtn.uid = str(message.get("message_id"))
|
||||
rtn.author = str(message.get("message_sender").get("id"))
|
||||
rtn.timestamp = message.get("timestamp_precise")
|
||||
rtn.unsent = False
|
||||
if message.get("unread") is not None:
|
||||
rtn.is_read = not message["unread"]
|
||||
rtn.reactions = {
|
||||
str(r["user"]["id"]): enum_extend_if_invalid(MessageReaction, r["reaction"])
|
||||
for r in message.get("message_reactions")
|
||||
}
|
||||
if message.get("blob_attachments") is not None:
|
||||
rtn.attachments = [
|
||||
graphql_to_attachment(attachment)
|
||||
for attachment in message["blob_attachments"]
|
||||
]
|
||||
if message.get("platform_xmd_encoded"):
|
||||
quick_replies = json.loads(message["platform_xmd_encoded"]).get("quick_replies")
|
||||
if isinstance(quick_replies, list):
|
||||
rtn.quick_replies = [graphql_to_quick_reply(q) for q in quick_replies]
|
||||
elif isinstance(quick_replies, dict):
|
||||
rtn.quick_replies = [
|
||||
graphql_to_quick_reply(quick_replies, is_response=True)
|
||||
]
|
||||
if message.get("extensible_attachment") is not None:
|
||||
attachment = graphql_to_extensible_attachment(message["extensible_attachment"])
|
||||
if isinstance(attachment, UnsentMessage):
|
||||
rtn.unsent = True
|
||||
elif attachment:
|
||||
rtn.attachments.append(attachment)
|
||||
return rtn
|
||||
|
||||
|
||||
def graphql_to_user(user):
|
||||
if user.get('profile_picture') is None:
|
||||
user['profile_picture'] = {}
|
||||
if user.get("profile_picture") is None:
|
||||
user["profile_picture"] = {}
|
||||
c_info = get_customization_info(user)
|
||||
plan = None
|
||||
if user.get('event_reminders'):
|
||||
plan = graphql_to_plan(user['event_reminders']['nodes'][0]) if user['event_reminders'].get('nodes') else None
|
||||
if user.get("event_reminders"):
|
||||
plan = (
|
||||
graphql_to_plan(user["event_reminders"]["nodes"][0])
|
||||
if user["event_reminders"].get("nodes")
|
||||
else None
|
||||
)
|
||||
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.get(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'),
|
||||
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.get(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"),
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
def graphql_to_thread(thread):
|
||||
if thread['thread_type'] == 'GROUP':
|
||||
return graphql_to_group(thread)
|
||||
elif thread['thread_type'] == 'ONE_TO_ONE':
|
||||
if thread.get('big_image_src') is None:
|
||||
thread['big_image_src'] = {}
|
||||
c_info = get_customization_info(thread)
|
||||
participants = [node['messaging_actor'] for node in thread['all_participants']['nodes']]
|
||||
user = next(p for p in participants if p['id'] == thread['thread_key']['other_user_id'])
|
||||
last_message_timestamp = None
|
||||
if 'last_message' in thread:
|
||||
last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise']
|
||||
|
||||
first_name = user.get('short_name')
|
||||
def graphql_to_thread(thread):
|
||||
if thread["thread_type"] == "GROUP":
|
||||
return graphql_to_group(thread)
|
||||
elif thread["thread_type"] == "ONE_TO_ONE":
|
||||
if thread.get("big_image_src") is None:
|
||||
thread["big_image_src"] = {}
|
||||
c_info = get_customization_info(thread)
|
||||
participants = [
|
||||
node["messaging_actor"] for node in thread["all_participants"]["nodes"]
|
||||
]
|
||||
user = next(
|
||||
p for p in participants if p["id"] == thread["thread_key"]["other_user_id"]
|
||||
)
|
||||
last_message_timestamp = None
|
||||
if "last_message" in thread:
|
||||
last_message_timestamp = thread["last_message"]["nodes"][0][
|
||||
"timestamp_precise"
|
||||
]
|
||||
|
||||
first_name = user.get("short_name")
|
||||
if first_name is None:
|
||||
last_name = None
|
||||
else:
|
||||
last_name = user.get('name').split(first_name, 1).pop().strip()
|
||||
last_name = user.get("name").split(first_name, 1).pop().strip()
|
||||
|
||||
plan = None
|
||||
if thread.get('event_reminders'):
|
||||
plan = graphql_to_plan(thread['event_reminders']['nodes'][0]) if thread['event_reminders'].get('nodes') else None
|
||||
if thread.get("event_reminders"):
|
||||
plan = (
|
||||
graphql_to_plan(thread["event_reminders"]["nodes"][0])
|
||||
if thread["event_reminders"].get("nodes")
|
||||
else None
|
||||
)
|
||||
|
||||
return User(
|
||||
user['id'],
|
||||
url=user.get('url'),
|
||||
name=user.get('name'),
|
||||
user["id"],
|
||||
url=user.get("url"),
|
||||
name=user.get("name"),
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
is_friend=user.get('is_viewer_friend'),
|
||||
gender=GENDERS.get(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['big_image_src'].get('uri'),
|
||||
message_count=thread.get('messages_count'),
|
||||
is_friend=user.get("is_viewer_friend"),
|
||||
gender=GENDERS.get(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["big_image_src"].get("uri"),
|
||||
message_count=thread.get("messages_count"),
|
||||
last_message_timestamp=last_message_timestamp,
|
||||
plan=plan,
|
||||
)
|
||||
else:
|
||||
raise FBchatException('Unknown thread type: {}, with data: {}'.format(thread.get('thread_type'), thread))
|
||||
raise FBchatException(
|
||||
"Unknown thread type: {}, with data: {}".format(
|
||||
thread.get("thread_type"), thread
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def graphql_to_group(group):
|
||||
if group.get('image') is None:
|
||||
group['image'] = {}
|
||||
if group.get("image") is None:
|
||||
group["image"] = {}
|
||||
c_info = get_customization_info(group)
|
||||
last_message_timestamp = None
|
||||
if 'last_message' in group:
|
||||
last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise']
|
||||
if "last_message" in group:
|
||||
last_message_timestamp = group["last_message"]["nodes"][0]["timestamp_precise"]
|
||||
plan = None
|
||||
if group.get('event_reminders'):
|
||||
plan = graphql_to_plan(group['event_reminders']['nodes'][0]) if group['event_reminders'].get('nodes') else None
|
||||
if group.get("event_reminders"):
|
||||
plan = (
|
||||
graphql_to_plan(group["event_reminders"]["nodes"][0])
|
||||
if group["event_reminders"].get("nodes")
|
||||
else None
|
||||
)
|
||||
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'),
|
||||
admins = set([node.get('id') for node in group.get('thread_admins')]),
|
||||
approval_mode = bool(group.get('approval_mode')) if group.get('approval_mode') is not None else None,
|
||||
approval_requests = set(node["requester"]['id'] for node in group['group_approval_queue']['nodes']) if group.get('group_approval_queue') else None,
|
||||
join_link = group['joinable_mode'].get('link'),
|
||||
photo=group['image'].get('uri'),
|
||||
name=group.get('name'),
|
||||
message_count=group.get('messages_count'),
|
||||
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"),
|
||||
admins=set([node.get("id") for node in group.get("thread_admins")]),
|
||||
approval_mode=bool(group.get("approval_mode"))
|
||||
if group.get("approval_mode") is not None
|
||||
else None,
|
||||
approval_requests=set(
|
||||
node["requester"]["id"] for node in group["group_approval_queue"]["nodes"]
|
||||
)
|
||||
if group.get("group_approval_queue")
|
||||
else None,
|
||||
join_link=group["joinable_mode"].get("link"),
|
||||
photo=group["image"].get("uri"),
|
||||
name=group.get("name"),
|
||||
message_count=group.get("messages_count"),
|
||||
last_message_timestamp=last_message_timestamp,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
|
||||
def graphql_to_page(page):
|
||||
if page.get('profile_picture') is None:
|
||||
page['profile_picture'] = {}
|
||||
if page.get('city') is None:
|
||||
page['city'] = {}
|
||||
if page.get("profile_picture") is None:
|
||||
page["profile_picture"] = {}
|
||||
if page.get("city") is None:
|
||||
page["city"] = {}
|
||||
plan = None
|
||||
if page.get('event_reminders'):
|
||||
plan = graphql_to_plan(page['event_reminders']['nodes'][0]) if page['event_reminders'].get('nodes') else None
|
||||
if page.get("event_reminders"):
|
||||
plan = (
|
||||
graphql_to_plan(page["event_reminders"]["nodes"][0])
|
||||
if page["event_reminders"].get("nodes")
|
||||
else None
|
||||
)
|
||||
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'),
|
||||
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"),
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
rtn["q{}".format(i)] = query.value
|
||||
return json.dumps(rtn)
|
||||
|
||||
|
||||
def graphql_response_to_json(content):
|
||||
content = strip_to_json(content) # Usually only needed in some error cases
|
||||
content = strip_to_json(content) # Usually only needed in some error cases
|
||||
try:
|
||||
j = json.loads(content, cls=ConcatJSONDecoder)
|
||||
except Exception:
|
||||
raise FBchatException('Error while parsing JSON: {}'.format(repr(content)))
|
||||
raise FBchatException("Error while parsing JSON: {}".format(repr(content)))
|
||||
|
||||
rtn = [None]*(len(j))
|
||||
rtn = [None] * (len(j))
|
||||
for x in j:
|
||||
if 'error_results' in x:
|
||||
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']
|
||||
if "response" in value:
|
||||
rtn[int(key[1:])] = value["response"]
|
||||
else:
|
||||
rtn[int(key[1:])] = value['data']
|
||||
rtn[int(key[1:])] = value["data"]
|
||||
|
||||
log.debug(rtn)
|
||||
|
||||
return rtn
|
||||
|
||||
|
||||
class GraphQL(object):
|
||||
def __init__(self, query=None, doc_id=None, params=None):
|
||||
if params is None:
|
||||
params = {}
|
||||
if query is not None:
|
||||
self.value = {
|
||||
'priority': 0,
|
||||
'q': query,
|
||||
'query_params': params
|
||||
}
|
||||
self.value = {"priority": 0, "q": query, "query_params": params}
|
||||
elif doc_id is not None:
|
||||
self.value = {
|
||||
'doc_id': doc_id,
|
||||
'query_params': params
|
||||
}
|
||||
self.value = {"doc_id": doc_id, "query_params": params}
|
||||
else:
|
||||
raise FBchatUserError('A query or doc_id must be specified')
|
||||
|
||||
raise FBchatUserError("A query or doc_id must be specified")
|
||||
|
||||
FRAGMENT_USER = """
|
||||
QueryFragment User: User {
|
||||
@@ -476,8 +699,9 @@ class GraphQL(object):
|
||||
}
|
||||
"""
|
||||
|
||||
SEARCH_USER = """
|
||||
Query SearchUser(<search> = '', <limit> = 1) {
|
||||
SEARCH_USER = (
|
||||
"""
|
||||
Query SearchUser(<search> = '', <limit> = 10) {
|
||||
entities_named(<search>) {
|
||||
search_results.of_type(user).first(<limit>) as users {
|
||||
nodes {
|
||||
@@ -486,10 +710,13 @@ class GraphQL(object):
|
||||
}
|
||||
}
|
||||
}
|
||||
""" + FRAGMENT_USER
|
||||
"""
|
||||
+ FRAGMENT_USER
|
||||
)
|
||||
|
||||
SEARCH_GROUP = """
|
||||
Query SearchGroup(<search> = '', <limit> = 1, <pic_size> = 32) {
|
||||
SEARCH_GROUP = (
|
||||
"""
|
||||
Query SearchGroup(<search> = '', <limit> = 10, <pic_size> = 32) {
|
||||
viewer() {
|
||||
message_threads.with_thread_name(<search>).last(<limit>) as groups {
|
||||
nodes {
|
||||
@@ -498,10 +725,13 @@ class GraphQL(object):
|
||||
}
|
||||
}
|
||||
}
|
||||
""" + FRAGMENT_GROUP
|
||||
"""
|
||||
+ FRAGMENT_GROUP
|
||||
)
|
||||
|
||||
SEARCH_PAGE = """
|
||||
Query SearchPage(<search> = '', <limit> = 1) {
|
||||
SEARCH_PAGE = (
|
||||
"""
|
||||
Query SearchPage(<search> = '', <limit> = 10) {
|
||||
entities_named(<search>) {
|
||||
search_results.of_type(page).first(<limit>) as pages {
|
||||
nodes {
|
||||
@@ -510,10 +740,13 @@ class GraphQL(object):
|
||||
}
|
||||
}
|
||||
}
|
||||
""" + FRAGMENT_PAGE
|
||||
"""
|
||||
+ FRAGMENT_PAGE
|
||||
)
|
||||
|
||||
SEARCH_THREAD = """
|
||||
Query SearchThread(<search> = '', <limit> = 1) {
|
||||
SEARCH_THREAD = (
|
||||
"""
|
||||
Query SearchThread(<search> = '', <limit> = 10) {
|
||||
entities_named(<search>) {
|
||||
search_results.first(<limit>) as threads {
|
||||
nodes {
|
||||
@@ -525,4 +758,8 @@ class GraphQL(object):
|
||||
}
|
||||
}
|
||||
}
|
||||
""" + FRAGMENT_USER + FRAGMENT_GROUP + FRAGMENT_PAGE
|
||||
"""
|
||||
+ FRAGMENT_USER
|
||||
+ FRAGMENT_GROUP
|
||||
+ FRAGMENT_PAGE
|
||||
)
|
||||
|
604
fbchat/models.py
604
fbchat/models.py
@@ -1,583 +1,29 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""This file is here to maintain backwards compatability, and to re-export our models
|
||||
into the global module (see `__init__.py`).
|
||||
|
||||
A common pattern was to use `from fbchat.models import *`, hence we need this while
|
||||
transitioning to a better code structure.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
import enum
|
||||
|
||||
|
||||
class FBchatException(Exception):
|
||||
"""Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this"""
|
||||
|
||||
class FBchatFacebookError(FBchatException):
|
||||
#: The error code that Facebook returned
|
||||
fb_error_code = None
|
||||
#: The error message that Facebook returned (In the user's own language)
|
||||
fb_error_message = None
|
||||
#: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200)
|
||||
request_status_code = None
|
||||
def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None):
|
||||
super(FBchatFacebookError, self).__init__(message)
|
||||
"""Thrown by fbchat when Facebook returns an error"""
|
||||
self.fb_error_code = str(fb_error_code)
|
||||
self.fb_error_message = fb_error_message
|
||||
self.request_status_code = request_status_code
|
||||
|
||||
class FBchatUserError(FBchatException):
|
||||
"""Thrown by fbchat when wrong values are entered"""
|
||||
|
||||
class Thread(object):
|
||||
#: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info
|
||||
uid = None
|
||||
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info
|
||||
type = None
|
||||
#: A url to the thread's picture
|
||||
photo = None
|
||||
#: The name of the thread
|
||||
name = None
|
||||
#: Timestamp of last message
|
||||
last_message_timestamp = None
|
||||
#: Number of messages in the thread
|
||||
message_count = None
|
||||
#: Set :class:`Plan`
|
||||
plan = None
|
||||
def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=None, plan=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
|
||||
self.plan = plan
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
def __unicode__(self):
|
||||
return '<{} {} ({})>'.format(self.type.name, self.name, self.uid)
|
||||
|
||||
|
||||
class User(Thread):
|
||||
#: The profile url
|
||||
url = None
|
||||
#: The users first name
|
||||
first_name = None
|
||||
#: The users last name
|
||||
last_name = None
|
||||
#: Whether the user and the client are friends
|
||||
is_friend = None
|
||||
#: The user's gender
|
||||
gender = None
|
||||
#: From 0 to 1. How close the client is to the user
|
||||
affinity = None
|
||||
#: The user's nickname
|
||||
nickname = None
|
||||
#: The clients nickname, as seen by the user
|
||||
own_nickname = None
|
||||
#: A :class:`ThreadColor`. The message color
|
||||
color = None
|
||||
#: The default emoji
|
||||
emoji = None
|
||||
|
||||
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):
|
||||
"""Represents a Facebook user. Inherits `Thread`"""
|
||||
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 Group(Thread):
|
||||
#: Unique list (set) of the group thread's participant user IDs
|
||||
participants = None
|
||||
#: A dict, containing user nicknames mapped to their IDs
|
||||
nicknames = None
|
||||
#: A :class:`ThreadColor`. The groups's message color
|
||||
color = None
|
||||
#: The groups's default emoji
|
||||
emoji = None
|
||||
# Set containing user IDs of thread admins
|
||||
admins = None
|
||||
# True if users need approval to join
|
||||
approval_mode = None
|
||||
# Set containing user IDs requesting to join
|
||||
approval_requests = None
|
||||
# Link for joining group
|
||||
join_link = None
|
||||
|
||||
def __init__(self, uid, participants=None, nicknames=None, color=None, emoji=None, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs):
|
||||
"""Represents a Facebook group. Inherits `Thread`"""
|
||||
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
|
||||
if participants is None:
|
||||
participants = set()
|
||||
self.participants = participants
|
||||
if nicknames is None:
|
||||
nicknames = []
|
||||
self.nicknames = nicknames
|
||||
self.color = color
|
||||
self.emoji = emoji
|
||||
if admins is None:
|
||||
admins = set()
|
||||
self.admins = admins
|
||||
self.approval_mode = approval_mode
|
||||
if approval_requests is None:
|
||||
approval_requests = set()
|
||||
self.approval_requests = approval_requests
|
||||
self.join_link = join_link
|
||||
|
||||
|
||||
class Room(Group):
|
||||
# True is room is not discoverable
|
||||
privacy_mode = None
|
||||
|
||||
def __init__(self, uid, privacy_mode=None, **kwargs):
|
||||
"""Deprecated. Use :class:`Group` instead"""
|
||||
super(Room, self).__init__(uid, **kwargs)
|
||||
self.type = ThreadType.ROOM
|
||||
self.privacy_mode = privacy_mode
|
||||
|
||||
|
||||
class Page(Thread):
|
||||
#: The page's custom url
|
||||
url = None
|
||||
#: The name of the page's location city
|
||||
city = None
|
||||
#: Amount of likes the page has
|
||||
likes = None
|
||||
#: Some extra information about the page
|
||||
sub_title = None
|
||||
#: The page's category
|
||||
category = None
|
||||
|
||||
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 actual message
|
||||
text = None
|
||||
#: A list of :class:`Mention` objects
|
||||
mentions = None
|
||||
#: A :class:`EmojiSize`. Size of a sent emoji
|
||||
emoji_size = None
|
||||
#: The message ID
|
||||
uid = None
|
||||
#: ID of the sender
|
||||
author = None
|
||||
#: Timestamp of when the message was sent
|
||||
timestamp = None
|
||||
#: Whether the message is read
|
||||
is_read = None
|
||||
#: A list of pepole IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages`
|
||||
read_by = None
|
||||
#: A dict with user's IDs as keys, and their :class:`MessageReaction` as values
|
||||
reactions = None
|
||||
#: The actual message
|
||||
text = None
|
||||
#: A :class:`Sticker`
|
||||
sticker = None
|
||||
#: A list of attachments
|
||||
attachments = None
|
||||
|
||||
def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None):
|
||||
"""Represents a Facebook message"""
|
||||
self.text = text
|
||||
if mentions is None:
|
||||
mentions = []
|
||||
self.mentions = mentions
|
||||
self.emoji_size = emoji_size
|
||||
self.sticker = sticker
|
||||
if attachments is None:
|
||||
attachments = []
|
||||
self.attachments = attachments
|
||||
self.reactions = {}
|
||||
self.read_by = []
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
def __unicode__(self):
|
||||
return '<Message ({}): {}, mentions={} emoji_size={} attachments={}>'.format(self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments)
|
||||
|
||||
class Attachment(object):
|
||||
#: The attachment ID
|
||||
uid = None
|
||||
|
||||
def __init__(self, uid=None):
|
||||
"""Represents a Facebook attachment"""
|
||||
self.uid = uid
|
||||
|
||||
class Sticker(Attachment):
|
||||
#: The sticker-pack's ID
|
||||
pack = None
|
||||
#: Whether the sticker is animated
|
||||
is_animated = False
|
||||
|
||||
# If the sticker is animated, the following should be present
|
||||
#: URL to a medium spritemap
|
||||
medium_sprite_image = None
|
||||
#: URL to a large spritemap
|
||||
large_sprite_image = None
|
||||
#: The amount of frames present in the spritemap pr. row
|
||||
frames_per_row = None
|
||||
#: The amount of frames present in the spritemap pr. coloumn
|
||||
frames_per_col = None
|
||||
#: The frame rate the spritemap is intended to be played in
|
||||
frame_rate = None
|
||||
|
||||
#: URL to the sticker's image
|
||||
url = None
|
||||
#: Width of the sticker
|
||||
width = None
|
||||
#: Height of the sticker
|
||||
height = None
|
||||
#: The sticker's label/name
|
||||
label = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Represents a Facebook sticker that has been sent to a Facebook thread as an attachment"""
|
||||
super(Sticker, self).__init__(*args, **kwargs)
|
||||
|
||||
class ShareAttachment(Attachment):
|
||||
def __init__(self, **kwargs):
|
||||
"""Represents a shared item (eg. URL) that has been sent as a Facebook attachment - *Currently Incomplete!*"""
|
||||
super(ShareAttachment, self).__init__(**kwargs)
|
||||
|
||||
class FileAttachment(Attachment):
|
||||
#: Url where you can download the file
|
||||
url = None
|
||||
#: Size of the file in bytes
|
||||
size = None
|
||||
#: Name of the file
|
||||
name = None
|
||||
#: Whether Facebook determines that this file may be harmful
|
||||
is_malicious = None
|
||||
|
||||
def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs):
|
||||
"""Represents a file that has been sent as a Facebook attachment"""
|
||||
super(FileAttachment, self).__init__(**kwargs)
|
||||
self.url = url
|
||||
self.size = size
|
||||
self.name = name
|
||||
self.is_malicious = is_malicious
|
||||
|
||||
class AudioAttachment(Attachment):
|
||||
#: Name of the file
|
||||
filename = None
|
||||
#: Url of the audio file
|
||||
url = None
|
||||
#: Duration of the audioclip in milliseconds
|
||||
duration = None
|
||||
#: Audio type
|
||||
audio_type = None
|
||||
|
||||
def __init__(self, filename=None, url=None, duration=None, audio_type=None, **kwargs):
|
||||
"""Represents an audio file that has been sent as a Facebook attachment"""
|
||||
super(AudioAttachment, self).__init__(**kwargs)
|
||||
self.filename = filename
|
||||
self.url = url
|
||||
self.duration = duration
|
||||
self.audio_type = audio_type
|
||||
|
||||
class ImageAttachment(Attachment):
|
||||
#: The extension of the original image (eg. 'png')
|
||||
original_extension = None
|
||||
#: Width of original image
|
||||
width = None
|
||||
#: Height of original image
|
||||
height = None
|
||||
|
||||
#: Whether the image is animated
|
||||
is_animated = None
|
||||
|
||||
#: URL to a thumbnail of the image
|
||||
thumbnail_url = None
|
||||
|
||||
#: URL to a medium preview of the image
|
||||
preview_url = None
|
||||
#: Width of the medium preview image
|
||||
preview_width = None
|
||||
#: Height of the medium preview image
|
||||
preview_height = None
|
||||
|
||||
#: URL to a large preview of the image
|
||||
large_preview_url = None
|
||||
#: Width of the large preview image
|
||||
large_preview_width = None
|
||||
#: Height of the large preview image
|
||||
large_preview_height = None
|
||||
|
||||
#: URL to an animated preview of the image (eg. for gifs)
|
||||
animated_preview_url = None
|
||||
#: Width of the animated preview image
|
||||
animated_preview_width = None
|
||||
#: Height of the animated preview image
|
||||
animated_preview_height = None
|
||||
|
||||
def __init__(self, original_extension=None, width=None, height=None, is_animated=None, thumbnail_url=None, preview=None, large_preview=None, animated_preview=None, **kwargs):
|
||||
"""
|
||||
Represents an image that has been sent as a Facebook attachment
|
||||
To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`,
|
||||
and pass it the uid of the image attachment
|
||||
"""
|
||||
super(ImageAttachment, self).__init__(**kwargs)
|
||||
self.original_extension = original_extension
|
||||
if width is not None:
|
||||
width = int(width)
|
||||
self.width = width
|
||||
if height is not None:
|
||||
height = int(height)
|
||||
self.height = height
|
||||
self.is_animated = is_animated
|
||||
self.thumbnail_url = thumbnail_url
|
||||
|
||||
if preview is None:
|
||||
preview = {}
|
||||
self.preview_url = preview.get('uri')
|
||||
self.preview_width = preview.get('width')
|
||||
self.preview_height = preview.get('height')
|
||||
|
||||
if large_preview is None:
|
||||
large_preview = {}
|
||||
self.large_preview_url = large_preview.get('uri')
|
||||
self.large_preview_width = large_preview.get('width')
|
||||
self.large_preview_height = large_preview.get('height')
|
||||
|
||||
if animated_preview is None:
|
||||
animated_preview = {}
|
||||
self.animated_preview_url = animated_preview.get('uri')
|
||||
self.animated_preview_width = animated_preview.get('width')
|
||||
self.animated_preview_height = animated_preview.get('height')
|
||||
|
||||
class VideoAttachment(Attachment):
|
||||
#: Size of the original video in bytes
|
||||
size = None
|
||||
#: Width of original video
|
||||
width = None
|
||||
#: Height of original video
|
||||
height = None
|
||||
#: Length of video in milliseconds
|
||||
duration = None
|
||||
#: URL to very compressed preview video
|
||||
preview_url = None
|
||||
|
||||
#: URL to a small preview image of the video
|
||||
small_image_url = None
|
||||
#: Width of the small preview image
|
||||
small_image_width = None
|
||||
#: Height of the small preview image
|
||||
small_image_height = None
|
||||
|
||||
#: URL to a medium preview image of the video
|
||||
medium_image_url = None
|
||||
#: Width of the medium preview image
|
||||
medium_image_width = None
|
||||
#: Height of the medium preview image
|
||||
medium_image_height = None
|
||||
|
||||
#: URL to a large preview image of the video
|
||||
large_image_url = None
|
||||
#: Width of the large preview image
|
||||
large_image_width = None
|
||||
#: Height of the large preview image
|
||||
large_image_height = None
|
||||
|
||||
def __init__(self, size=None, width=None, height=None, duration=None, preview_url=None, small_image=None, medium_image=None, large_image=None, **kwargs):
|
||||
"""Represents a video that has been sent as a Facebook attachment"""
|
||||
super(VideoAttachment, self).__init__(**kwargs)
|
||||
self.size = size
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.duration = duration
|
||||
self.preview_url = preview_url
|
||||
|
||||
if small_image is None:
|
||||
small_image = {}
|
||||
self.small_image_url = small_image.get('uri')
|
||||
self.small_image_width = small_image.get('width')
|
||||
self.small_image_height = small_image.get('height')
|
||||
|
||||
if medium_image is None:
|
||||
medium_image = {}
|
||||
self.medium_image_url = medium_image.get('uri')
|
||||
self.medium_image_width = medium_image.get('width')
|
||||
self.medium_image_height = medium_image.get('height')
|
||||
|
||||
if large_image is None:
|
||||
large_image = {}
|
||||
self.large_image_url = large_image.get('uri')
|
||||
self.large_image_width = large_image.get('width')
|
||||
self.large_image_height = large_image.get('height')
|
||||
|
||||
|
||||
class Mention(object):
|
||||
#: The thread ID the mention is pointing at
|
||||
thread_id = None
|
||||
#: The character where the mention starts
|
||||
offset = None
|
||||
#: The length of the mention
|
||||
length = None
|
||||
|
||||
def __init__(self, thread_id, offset=0, length=10):
|
||||
"""Represents a @mention"""
|
||||
self.thread_id = thread_id
|
||||
self.offset = offset
|
||||
self.length = length
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
def __unicode__(self):
|
||||
return '<Mention {}: offset={} length={}>'.format(self.thread_id, self.offset, self.length)
|
||||
|
||||
class Poll(object):
|
||||
#: ID of the poll
|
||||
uid = None
|
||||
#: Title of the poll
|
||||
title = None
|
||||
#: List of :class:`PollOption`, can be fetched with :func:`fbchat.Client.fetchPollOptions`
|
||||
options = None
|
||||
#: Options count
|
||||
options_count = None
|
||||
|
||||
def __init__(self, title, options):
|
||||
"""Represents a poll"""
|
||||
self.title = title
|
||||
self.options = options
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
def __unicode__(self):
|
||||
return '<Poll ({}): {} options={}>'.format(self.uid, repr(self.title), self.options)
|
||||
|
||||
class PollOption(object):
|
||||
#: ID of the poll option
|
||||
uid = None
|
||||
#: Text of the poll option
|
||||
text = None
|
||||
#: Whether vote when creating or client voted
|
||||
vote = None
|
||||
#: ID of the users who voted for this poll option
|
||||
voters = None
|
||||
#: Votes count
|
||||
votes_count = None
|
||||
|
||||
def __init__(self, text, vote=False):
|
||||
"""Represents a poll option"""
|
||||
self.text = text
|
||||
self.vote = vote
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
def __unicode__(self):
|
||||
return '<PollOption ({}): {} voters={}>'.format(self.uid, repr(self.text), self.voters)
|
||||
|
||||
class Plan(object):
|
||||
#: ID of the plan
|
||||
uid = None
|
||||
#: Plan time (unix time stamp), only precise down to the minute
|
||||
time = None
|
||||
#: Plan title
|
||||
title = None
|
||||
#: Plan location name
|
||||
location = None
|
||||
#: Plan location ID
|
||||
location_id = None
|
||||
#: ID of the plan creator
|
||||
author_id = None
|
||||
#: List of the people IDs who will take part in the plan
|
||||
going = None
|
||||
#: List of the people IDs who won't take part in the plan
|
||||
declined = None
|
||||
#: List of the people IDs who are invited to the plan
|
||||
invited = None
|
||||
|
||||
def __init__(self, time, title, location=None, location_id=None):
|
||||
"""Represents a plan"""
|
||||
self.time = int(time)
|
||||
self.title = title
|
||||
self.location = location or ''
|
||||
self.location_id = location_id or ''
|
||||
self.author_id = None
|
||||
self.going = []
|
||||
self.declined = []
|
||||
self.invited = []
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
def __unicode__(self):
|
||||
return '<Plan ({}): {} time={}, location={}, location_id={}>'.format(self.uid, repr(self.title), self.time, repr(self.location), repr(self.location_id))
|
||||
|
||||
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
|
||||
ROOM = 2
|
||||
PAGE = 3
|
||||
|
||||
class ThreadLocation(Enum):
|
||||
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
|
||||
INBOX = 'INBOX'
|
||||
PENDING = 'PENDING'
|
||||
ARCHIVED = 'ARCHIVED'
|
||||
OTHER = 'OTHER'
|
||||
|
||||
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 = '#0084ff'
|
||||
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 = '👎'
|
||||
from ._core import Enum
|
||||
from ._exception import FBchatException, FBchatFacebookError, FBchatUserError
|
||||
from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread
|
||||
from ._user import TypingStatus, User, ActiveStatus
|
||||
from ._group import Group, Room
|
||||
from ._page import Page
|
||||
from ._message import EmojiSize, MessageReaction, Mention, Message
|
||||
from ._attachment import Attachment, UnsentMessage, ShareAttachment
|
||||
from ._sticker import Sticker
|
||||
from ._location import LocationAttachment, LiveLocationAttachment
|
||||
from ._file import FileAttachment, AudioAttachment, ImageAttachment, VideoAttachment
|
||||
from ._quick_reply import (
|
||||
QuickReply,
|
||||
QuickReplyText,
|
||||
QuickReplyLocation,
|
||||
QuickReplyPhoneNumber,
|
||||
QuickReplyEmail,
|
||||
)
|
||||
from ._poll import Poll, PollOption
|
||||
from ._plan import Plan
|
||||
|
222
fbchat/utils.py
222
fbchat/utils.py
@@ -11,13 +11,17 @@ from os.path import basename
|
||||
import warnings
|
||||
import logging
|
||||
import requests
|
||||
import aenum
|
||||
from .models import *
|
||||
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlencode, parse_qs, urlparse
|
||||
|
||||
basestring = (str, bytes)
|
||||
except ImportError:
|
||||
from urllib import urlencode
|
||||
from urlparse import parse_qs, urlparse
|
||||
|
||||
basestring = basestring
|
||||
|
||||
# Python 2's `input` executes the input, whereas `raw_input` just returns the input
|
||||
@@ -40,51 +44,52 @@ USER_AGENTS = [
|
||||
"Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
|
||||
"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",
|
||||
]
|
||||
|
||||
LIKES = {
|
||||
'large': EmojiSize.LARGE,
|
||||
'medium': EmojiSize.MEDIUM,
|
||||
'small': EmojiSize.SMALL,
|
||||
'l': EmojiSize.LARGE,
|
||||
'm': EmojiSize.MEDIUM,
|
||||
's': EmojiSize.SMALL
|
||||
"large": EmojiSize.LARGE,
|
||||
"medium": EmojiSize.MEDIUM,
|
||||
"small": EmojiSize.SMALL,
|
||||
"l": EmojiSize.LARGE,
|
||||
"m": EmojiSize.MEDIUM,
|
||||
"s": EmojiSize.SMALL,
|
||||
}
|
||||
|
||||
|
||||
GENDERS = {
|
||||
# For standard requests
|
||||
0: 'unknown',
|
||||
1: 'female_singular',
|
||||
2: 'male_singular',
|
||||
3: 'female_singular_guess',
|
||||
4: 'male_singular_guess',
|
||||
5: 'mixed',
|
||||
6: 'neuter_singular',
|
||||
7: 'unknown_singular',
|
||||
8: 'female_plural',
|
||||
9: 'male_plural',
|
||||
10: 'neuter_plural',
|
||||
11: 'unknown_plural',
|
||||
|
||||
0: "unknown",
|
||||
1: "female_singular",
|
||||
2: "male_singular",
|
||||
3: "female_singular_guess",
|
||||
4: "male_singular_guess",
|
||||
5: "mixed",
|
||||
6: "neuter_singular",
|
||||
7: "unknown_singular",
|
||||
8: "female_plural",
|
||||
9: "male_plural",
|
||||
10: "neuter_plural",
|
||||
11: "unknown_plural",
|
||||
# For graphql requests
|
||||
'UNKNOWN': 'unknown',
|
||||
'FEMALE': 'female_singular',
|
||||
'MALE': 'male_singular',
|
||||
#'': 'female_singular_guess',
|
||||
#'': 'male_singular_guess',
|
||||
#'': 'mixed',
|
||||
'NEUTER': 'neuter_singular',
|
||||
#'': 'unknown_singular',
|
||||
#'': 'female_plural',
|
||||
#'': 'male_plural',
|
||||
#'': 'neuter_plural',
|
||||
#'': 'unknown_plural',
|
||||
"UNKNOWN": "unknown",
|
||||
"FEMALE": "female_singular",
|
||||
"MALE": "male_singular",
|
||||
# '': 'female_singular_guess',
|
||||
# '': 'male_singular_guess',
|
||||
# '': 'mixed',
|
||||
"NEUTER": "neuter_singular",
|
||||
# '': 'unknown_singular',
|
||||
# '': 'female_plural',
|
||||
# '': 'male_plural',
|
||||
# '': 'neuter_plural',
|
||||
# '': 'unknown_plural',
|
||||
}
|
||||
|
||||
|
||||
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/"
|
||||
@@ -92,8 +97,12 @@ class ReqUrl(object):
|
||||
UNSEEN_THREADS = "https://www.facebook.com/mercury/unseen_thread_ids/"
|
||||
THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php"
|
||||
MOVE_THREAD = "https://www.facebook.com/ajax/mercury/move_thread.php"
|
||||
ARCHIVED_STATUS = "https://www.facebook.com/ajax/mercury/change_archived_status.php?dpr=1"
|
||||
PINNED_STATUS = "https://www.facebook.com/ajax/mercury/change_pinned_status.php?dpr=1"
|
||||
ARCHIVED_STATUS = (
|
||||
"https://www.facebook.com/ajax/mercury/change_archived_status.php?dpr=1"
|
||||
)
|
||||
PINNED_STATUS = (
|
||||
"https://www.facebook.com/ajax/mercury/change_pinned_status.php?dpr=1"
|
||||
)
|
||||
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"
|
||||
@@ -133,133 +142,180 @@ class ReqUrl(object):
|
||||
DELETE_THREAD = "https://www.facebook.com/ajax/mercury/delete_thread.php?dpr=1"
|
||||
DELETE_MESSAGES = "https://www.facebook.com/ajax/mercury/delete_messages.php?dpr=1"
|
||||
MUTE_THREAD = "https://www.facebook.com/ajax/mercury/change_mute_thread.php?dpr=1"
|
||||
MUTE_REACTIONS = "https://www.facebook.com/ajax/mercury/change_reactions_mute_thread/?dpr=1"
|
||||
MUTE_MENTIONS = "https://www.facebook.com/ajax/mercury/change_mentions_mute_thread/?dpr=1"
|
||||
MUTE_REACTIONS = (
|
||||
"https://www.facebook.com/ajax/mercury/change_reactions_mute_thread/?dpr=1"
|
||||
)
|
||||
MUTE_MENTIONS = (
|
||||
"https://www.facebook.com/ajax/mercury/change_mentions_mute_thread/?dpr=1"
|
||||
)
|
||||
CREATE_POLL = "https://www.facebook.com/messaging/group_polling/create_poll/?dpr=1"
|
||||
UPDATE_VOTE = "https://www.facebook.com/messaging/group_polling/update_vote/?dpr=1"
|
||||
GET_POLL_OPTIONS = "https://www.facebook.com/ajax/mercury/get_poll_options"
|
||||
SEARCH_MESSAGES = "https://www.facebook.com/ajax/mercury/search_snippets.php?dpr=1"
|
||||
MARK_SPAM = "https://www.facebook.com/ajax/mercury/mark_spam.php?dpr=1"
|
||||
UNSEND = "https://www.facebook.com/messaging/unsend_message/?dpr=1"
|
||||
|
||||
pull_channel = 0
|
||||
|
||||
def change_pull_channel(self, channel=None):
|
||||
if channel is None:
|
||||
self.pull_channel = (self.pull_channel + 1) % 5 # Pull channel will be 0-4
|
||||
self.pull_channel = (self.pull_channel + 1) % 5 # Pull channel will be 0-4
|
||||
else:
|
||||
self.pull_channel = channel
|
||||
self.STICKY = "https://{}-edge-chat.facebook.com/pull".format(self.pull_channel)
|
||||
self.PING = "https://{}-edge-chat.facebook.com/active_ping".format(self.pull_channel)
|
||||
self.PING = "https://{}-edge-chat.facebook.com/active_ping".format(
|
||||
self.pull_channel
|
||||
)
|
||||
|
||||
|
||||
facebookEncoding = 'UTF-8'
|
||||
facebookEncoding = "UTF-8"
|
||||
|
||||
|
||||
def now():
|
||||
return int(time()*1000)
|
||||
return int(time() * 1000)
|
||||
|
||||
|
||||
def strip_to_json(text):
|
||||
try:
|
||||
return text[text.index('{'):]
|
||||
return text[text.index("{") :]
|
||||
except ValueError:
|
||||
raise FBchatException('No JSON object found: {!r}'.format(text))
|
||||
raise FBchatException("No JSON object found: {!r}".format(text))
|
||||
|
||||
|
||||
def get_decoded_r(r):
|
||||
return get_decoded(r._content)
|
||||
|
||||
|
||||
def get_decoded(content):
|
||||
return content.decode(facebookEncoding)
|
||||
|
||||
|
||||
def parse_json(content):
|
||||
return json.loads(content)
|
||||
|
||||
|
||||
def get_json(r):
|
||||
return json.loads(strip_to_json(get_decoded_r(r)))
|
||||
|
||||
|
||||
def digitToChar(digit):
|
||||
if digit < 10:
|
||||
return str(digit)
|
||||
return chr(ord('a') + digit - 10)
|
||||
return chr(ord("a") + digit - 10)
|
||||
|
||||
|
||||
def str_base(number, base):
|
||||
if number < 0:
|
||||
return '-' + str_base(-number, base)
|
||||
return "-" + str_base(-number, base)
|
||||
(d, m) = divmod(number, base)
|
||||
if d > 0:
|
||||
return str_base(d, base) + digitToChar(m)
|
||||
return digitToChar(m)
|
||||
|
||||
|
||||
def generateMessageID(client_id=None):
|
||||
k = now()
|
||||
l = int(random() * 4294967295)
|
||||
return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id)
|
||||
|
||||
|
||||
def getSignatureID():
|
||||
return hex(int(random() * 2147483648))
|
||||
|
||||
|
||||
def generateOfflineThreadingID():
|
||||
ret = now()
|
||||
value = int(random() * 4294967295)
|
||||
string = ("0000000000000000000000" + format(value, 'b'))[-22:]
|
||||
msgs = format(ret, 'b') + string
|
||||
string = ("0000000000000000000000" + format(value, "b"))[-22:]
|
||||
msgs = format(ret, "b") + string
|
||||
return str(int(msgs, 2))
|
||||
|
||||
|
||||
def check_json(j):
|
||||
if j.get('error') is None:
|
||||
if j.get("error") is None:
|
||||
return
|
||||
if 'errorDescription' in j:
|
||||
if "errorDescription" in j:
|
||||
# 'errorDescription' is in the users own language!
|
||||
raise FBchatFacebookError('Error #{} when sending request: {}'.format(j['error'], j['errorDescription']), fb_error_code=j['error'], fb_error_message=j['errorDescription'])
|
||||
elif 'debug_info' in j['error'] and 'code' in j['error']:
|
||||
raise FBchatFacebookError('Error #{} when sending request: {}'.format(j['error']['code'], repr(j['error']['debug_info'])), fb_error_code=j['error']['code'], fb_error_message=j['error']['debug_info'])
|
||||
raise FBchatFacebookError(
|
||||
"Error #{} when sending request: {}".format(
|
||||
j["error"], j["errorDescription"]
|
||||
),
|
||||
fb_error_code=j["error"],
|
||||
fb_error_message=j["errorDescription"],
|
||||
)
|
||||
elif "debug_info" in j["error"] and "code" in j["error"]:
|
||||
raise FBchatFacebookError(
|
||||
"Error #{} when sending request: {}".format(
|
||||
j["error"]["code"], repr(j["error"]["debug_info"])
|
||||
),
|
||||
fb_error_code=j["error"]["code"],
|
||||
fb_error_message=j["error"]["debug_info"],
|
||||
)
|
||||
else:
|
||||
raise FBchatFacebookError('Error {} when sending request'.format(j['error']), fb_error_code=j['error'])
|
||||
raise FBchatFacebookError(
|
||||
"Error {} when sending request".format(j["error"]), fb_error_code=j["error"]
|
||||
)
|
||||
|
||||
|
||||
def check_request(r, as_json=True):
|
||||
if not r.ok:
|
||||
raise FBchatFacebookError('Error when sending request: Got {} response'.format(r.status_code), request_status_code=r.status_code)
|
||||
raise FBchatFacebookError(
|
||||
"Error when sending request: Got {} response".format(r.status_code),
|
||||
request_status_code=r.status_code,
|
||||
)
|
||||
|
||||
content = get_decoded_r(r)
|
||||
|
||||
if content is None or len(content) == 0:
|
||||
raise FBchatFacebookError('Error when sending request: Got empty response')
|
||||
raise FBchatFacebookError("Error when sending request: Got empty response")
|
||||
|
||||
if as_json:
|
||||
content = strip_to_json(content)
|
||||
try:
|
||||
j = json.loads(content)
|
||||
except ValueError:
|
||||
raise FBchatFacebookError('Error while parsing JSON: {!r}'.format(content))
|
||||
raise FBchatFacebookError("Error while parsing JSON: {!r}".format(content))
|
||||
check_json(j)
|
||||
log.debug(j)
|
||||
return j
|
||||
else:
|
||||
return content
|
||||
|
||||
|
||||
def get_jsmods_require(j, index):
|
||||
if j.get('jsmods') and j['jsmods'].get('require'):
|
||||
if j.get("jsmods") and j["jsmods"].get("require"):
|
||||
try:
|
||||
return j['jsmods']['require'][0][index][0]
|
||||
return j["jsmods"]["require"][0][index][0]
|
||||
except (KeyError, IndexError) as e:
|
||||
log.warning('Error when getting jsmods_require: {}. Facebook might have changed protocol'.format(j))
|
||||
log.warning(
|
||||
"Error when getting jsmods_require: {}. Facebook might have changed protocol".format(
|
||||
j
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_emojisize_from_tags(tags):
|
||||
if tags is None:
|
||||
return None
|
||||
tmp = [tag for tag in tags if tag.startswith('hot_emoji_size:')]
|
||||
tmp = [tag for tag in tags if tag.startswith("hot_emoji_size:")]
|
||||
if len(tmp) > 0:
|
||||
try:
|
||||
return LIKES[tmp[0].split(':')[1]]
|
||||
return LIKES[tmp[0].split(":")[1]]
|
||||
except (KeyError, IndexError):
|
||||
log.exception('Could not determine emoji size from {} - {}'.format(tags, tmp))
|
||||
log.exception(
|
||||
"Could not determine emoji size from {} - {}".format(tags, tmp)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def require_list(list_):
|
||||
if isinstance(list_, list):
|
||||
return set(list_)
|
||||
else:
|
||||
return set([list_])
|
||||
|
||||
|
||||
def mimetype_to_key(mimetype):
|
||||
if not mimetype:
|
||||
return "file_id"
|
||||
@@ -277,11 +333,13 @@ def get_files_from_urls(file_urls):
|
||||
r = requests.get(file_url)
|
||||
# We could possibly use r.headers.get('Content-Disposition'), see
|
||||
# https://stackoverflow.com/a/37060758
|
||||
files.append((
|
||||
basename(file_url),
|
||||
r.content,
|
||||
r.headers.get('Content-Type') or guess_type(file_url)[0],
|
||||
))
|
||||
files.append(
|
||||
(
|
||||
basename(file_url),
|
||||
r.content,
|
||||
r.headers.get("Content-Type") or guess_type(file_url)[0],
|
||||
)
|
||||
)
|
||||
return files
|
||||
|
||||
|
||||
@@ -289,11 +347,31 @@ def get_files_from_urls(file_urls):
|
||||
def get_files_from_paths(filenames):
|
||||
files = []
|
||||
for filename in filenames:
|
||||
files.append((
|
||||
basename(filename),
|
||||
open(filename, 'rb'),
|
||||
guess_type(filename)[0],
|
||||
))
|
||||
files.append(
|
||||
(basename(filename), open(filename, "rb"), guess_type(filename)[0])
|
||||
)
|
||||
yield files
|
||||
for fn, fp, ft in files:
|
||||
fp.close()
|
||||
|
||||
|
||||
def enum_extend_if_invalid(enumeration, value):
|
||||
try:
|
||||
return enumeration(value)
|
||||
except ValueError:
|
||||
log.warning(
|
||||
"Failed parsing {.__name__}({!r}). Extending enum.".format(
|
||||
enumeration, value
|
||||
)
|
||||
)
|
||||
aenum.extend_enum(enumeration, "UNKNOWN_{}".format(value).upper(), value)
|
||||
return enumeration(value)
|
||||
|
||||
|
||||
def get_url_parameters(url, *args):
|
||||
params = parse_qs(urlparse(url).query)
|
||||
return [params[arg][0] for arg in args if params.get(arg)]
|
||||
|
||||
|
||||
def get_url_parameter(url, param):
|
||||
return get_url_parameters(url, param)[0]
|
||||
|
65
pyproject.toml
Normal file
65
pyproject.toml
Normal file
@@ -0,0 +1,65 @@
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
exclude = '''
|
||||
/(
|
||||
\.git
|
||||
| \.pytest_cache
|
||||
| build
|
||||
| dist
|
||||
| venv
|
||||
)/
|
||||
'''
|
||||
|
||||
[build-system]
|
||||
requires = ["flit"]
|
||||
build-backend = "flit.buildapi"
|
||||
|
||||
[tool.flit.metadata]
|
||||
module = "fbchat"
|
||||
author = "Taehoon Kim"
|
||||
author-email = "carpedm20@gmail.com"
|
||||
maintainer = "Mads Marquart"
|
||||
maintainer-email = "madsmtm@gmail.com"
|
||||
home-page = "https://github.com/carpedm20/fbchat/"
|
||||
requires = [
|
||||
"aenum",
|
||||
"attrs~=18.2.0",
|
||||
"requests",
|
||||
"beautifulsoup4",
|
||||
]
|
||||
description-file = "README.rst"
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Information Technology",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: OS Independent",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4.0"
|
||||
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/"
|
||||
|
||||
[tool.flit.metadata.requires-extra]
|
||||
test = [
|
||||
"pytest~=4.0",
|
||||
"six",
|
||||
]
|
@@ -1,3 +0,0 @@
|
||||
requests
|
||||
beautifulsoup4
|
||||
enum34; python_version < '3.4'
|
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
python -m pytest -m offline --color=yes
|
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
if ! python -m pytest --color=yes; then
|
||||
echo << EOF
|
||||
-----------------------------------------------------------------
|
||||
-----------------------------------------------------------------
|
||||
-----------------------------------------------------------------
|
||||
|
||||
Some tests failed! Rerunning them, since they can be kinda flaky.
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-----------------------------------------------------------------
|
||||
-----------------------------------------------------------------
|
||||
EOF
|
||||
python -m pytest --last-failed --color=yes
|
||||
fi
|
51
setup.cfg
51
setup.cfg
@@ -1,51 +0,0 @@
|
||||
[metadata]
|
||||
name = fbchat
|
||||
version = attr: fbchat.__version__
|
||||
license = BSD 3-Clause
|
||||
license_file = LICENSE
|
||||
|
||||
author = Taehoon Kim
|
||||
author_email = carpedm20@gmail.com
|
||||
maintainer = Mads Marquart
|
||||
maintainer_email = madsmtm@gmail.com
|
||||
|
||||
description = Facebook Chat (Messenger) for Python
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
|
||||
keywords = Facebook FB Messenger Chat Api Bot
|
||||
classifiers =
|
||||
Development Status :: 3 - Alpha
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: Information Technology
|
||||
License :: OSI Approved :: BSD License
|
||||
Operating System :: OS Independent
|
||||
Natural Language :: English
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
Programming Language :: Python :: Implementation :: PyPy
|
||||
Topic :: Communications :: Chat
|
||||
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Topic :: Software Development :: Libraries :: Python Modules
|
||||
|
||||
url = https://github.com/carpedm20/fbchat/
|
||||
project_urls =
|
||||
Documentation = https://fbchat.readthedocs.io/
|
||||
Repository = https://github.com/carpedm20/fbchat/
|
||||
|
||||
[options]
|
||||
zip_safe = True
|
||||
include_package_data = True
|
||||
packages = find:
|
||||
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4.0
|
||||
install_requires =
|
||||
requests
|
||||
beautifulsoup4
|
||||
# May not work in pip with bdist_wheel
|
||||
# See https://wheel.readthedocs.io/en/latest/#defining-conditional-dependencies
|
||||
# It is therefore defined in setup.py
|
||||
# enum34; python_version < '3.4'
|
8
setup.py
8
setup.py
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(extras_require={':python_version < "3.4"': ['enum34']})
|
@@ -17,17 +17,21 @@ def user(client2):
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def group(pytestconfig):
|
||||
return {"id": load_variable("group_id", pytestconfig.cache), "type": ThreadType.GROUP}
|
||||
return {
|
||||
"id": load_variable("group_id", pytestconfig.cache),
|
||||
"type": ThreadType.GROUP,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", params=[
|
||||
"user", "group", pytest.param("none", marks=[pytest.mark.xfail()])
|
||||
])
|
||||
@pytest.fixture(
|
||||
scope="session",
|
||||
params=["user", "group", pytest.param("none", marks=[pytest.mark.xfail()])],
|
||||
)
|
||||
def thread(request, user, group):
|
||||
return {
|
||||
"user": user,
|
||||
"group": group,
|
||||
"none": {"id": "0", "type": ThreadType.GROUP}
|
||||
"none": {"id": "0", "type": ThreadType.GROUP},
|
||||
}[request.param]
|
||||
|
||||
|
||||
@@ -111,14 +115,14 @@ def compare(client, thread):
|
||||
def message_with_mentions(request, client, client2, group):
|
||||
text = "Hi there ["
|
||||
mentions = []
|
||||
if 'me' in request.param:
|
||||
if "me" in request.param:
|
||||
mentions.append(Mention(thread_id=client.uid, offset=len(text), length=2))
|
||||
text += "me, "
|
||||
if 'other' in request.param:
|
||||
if "other" in request.param:
|
||||
mentions.append(Mention(thread_id=client2.uid, offset=len(text), length=5))
|
||||
text += "other, "
|
||||
# Unused, because Facebook don't properly support sending mentions with groups as targets
|
||||
if 'group' in request.param:
|
||||
if "group" in request.param:
|
||||
mentions.append(Mention(thread_id=group["id"], offset=len(text), length=5))
|
||||
text += "group, "
|
||||
text += "nothing]"
|
||||
|
@@ -19,6 +19,11 @@ def test_fetch_thread_list(client1):
|
||||
assert len(threads) == 2
|
||||
|
||||
|
||||
def test_fetch_threads(client1):
|
||||
threads = client1.fetchThreads(limit=2)
|
||||
assert len(threads) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
|
||||
def test_fetch_message_emoji(client, emoji, emoji_size):
|
||||
mid = client.sendEmoji(emoji, emoji_size)
|
||||
@@ -43,7 +48,9 @@ def test_fetch_message_mentions(client, thread, message_with_mentions):
|
||||
mid = client.send(message_with_mentions)
|
||||
message, = client.fetchThreadMessages(limit=1)
|
||||
|
||||
assert subset(vars(message), uid=mid, author=client.uid, text=message_with_mentions.text)
|
||||
assert subset(
|
||||
vars(message), uid=mid, author=client.uid, text=message_with_mentions.text
|
||||
)
|
||||
# The mentions are not ordered by offset
|
||||
for m in message.mentions:
|
||||
assert vars(m) in [vars(x) for x in message_with_mentions.mentions]
|
||||
@@ -53,7 +60,9 @@ def test_fetch_message_info_mentions(client, thread, message_with_mentions):
|
||||
mid = client.send(message_with_mentions)
|
||||
message = client.fetchMessageInfo(mid, thread_id=thread["id"])
|
||||
|
||||
assert subset(vars(message), uid=mid, author=client.uid, text=message_with_mentions.text)
|
||||
assert subset(
|
||||
vars(message), uid=mid, author=client.uid, text=message_with_mentions.text
|
||||
)
|
||||
# The mentions are not ordered by offset
|
||||
for m in message.mentions:
|
||||
assert vars(m) in [vars(x) for x in message_with_mentions.mentions]
|
||||
|
@@ -9,17 +9,17 @@ from utils import random_hex, subset
|
||||
from time import time
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=[
|
||||
Plan(int(time()) + 100, random_hex()),
|
||||
pytest.param(
|
||||
Plan(int(time()), random_hex()),
|
||||
marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
|
||||
),
|
||||
pytest.param(
|
||||
Plan(0, None),
|
||||
marks=[pytest.mark.xfail()],
|
||||
),
|
||||
])
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
Plan(int(time()) + 100, random_hex()),
|
||||
pytest.param(
|
||||
Plan(int(time()), random_hex()),
|
||||
marks=[pytest.mark.xfail(raises=FBchatFacebookError)],
|
||||
),
|
||||
pytest.param(Plan(0, None), marks=[pytest.mark.xfail()]),
|
||||
],
|
||||
)
|
||||
def plan_data(request, client, user, thread, catch_event, compare):
|
||||
with catch_event("onPlanCreated") as x:
|
||||
client.createPlan(request.param, thread["id"])
|
||||
@@ -50,15 +50,14 @@ def test_fetch_plan_info(client, catch_event, plan_data):
|
||||
event, plan = plan_data
|
||||
fetched_plan = client.fetchPlanInfo(plan.uid)
|
||||
assert subset(
|
||||
vars(fetched_plan),
|
||||
time=plan.time,
|
||||
title=plan.title,
|
||||
author_id=int(client.uid),
|
||||
vars(fetched_plan), time=plan.time, title=plan.title, author_id=int(client.uid)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("take_part", [False, True])
|
||||
def test_change_plan_participation(client, thread, catch_event, compare, plan_data, take_part):
|
||||
def test_change_plan_participation(
|
||||
client, thread, catch_event, compare, plan_data, take_part
|
||||
):
|
||||
event, plan = plan_data
|
||||
with catch_event("onPlanParticipation") as x:
|
||||
client.changePlanParticipation(plan, take_part=take_part)
|
||||
@@ -94,18 +93,22 @@ def test_on_plan_ended(client, thread, catch_event, compare):
|
||||
with catch_event("onPlanEnded") as x:
|
||||
client.createPlan(Plan(int(time()) + 120, "Wait for ending"))
|
||||
x.wait(180)
|
||||
assert subset(x.res, thread_id=client.uid if thread["type"] == ThreadType.USER else thread["id"], thread_type=thread["type"])
|
||||
assert subset(
|
||||
x.res,
|
||||
thread_id=client.uid if thread["type"] == ThreadType.USER else thread["id"],
|
||||
thread_type=thread["type"],
|
||||
)
|
||||
|
||||
|
||||
#createPlan(self, plan, thread_id=None)
|
||||
#editPlan(self, plan, new_plan)
|
||||
#deletePlan(self, plan)
|
||||
#changePlanParticipation(self, plan, take_part=True)
|
||||
# createPlan(self, plan, thread_id=None)
|
||||
# editPlan(self, plan, new_plan)
|
||||
# deletePlan(self, plan)
|
||||
# changePlanParticipation(self, plan, take_part=True)
|
||||
|
||||
#onPlanCreated(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
#onPlanEnded(self, mid=None, plan=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
#onPlanEdited(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
#onPlanDeleted(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
#onPlanParticipation(self, mid=None, plan=None, take_part=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
# onPlanCreated(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
# onPlanEnded(self, mid=None, plan=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
# onPlanEdited(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
# onPlanDeleted(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
# onPlanParticipation(self, mid=None, plan=None, take_part=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
|
||||
|
||||
#fetchPlanInfo(self, plan_id)
|
||||
# fetchPlanInfo(self, plan_id)
|
||||
|
@@ -8,28 +8,40 @@ from fbchat.models import Poll, PollOption, ThreadType
|
||||
from utils import random_hex, subset
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=[
|
||||
Poll(title=random_hex(), options=[]),
|
||||
Poll(title=random_hex(), options=[
|
||||
PollOption(random_hex(), vote=True),
|
||||
PollOption(random_hex(), vote=True),
|
||||
]),
|
||||
Poll(title=random_hex(), options=[
|
||||
PollOption(random_hex(), vote=False),
|
||||
PollOption(random_hex(), vote=False),
|
||||
]),
|
||||
Poll(title=random_hex(), options=[
|
||||
PollOption(random_hex(), vote=True),
|
||||
PollOption(random_hex(), vote=True),
|
||||
PollOption(random_hex(), vote=False),
|
||||
PollOption(random_hex(), vote=False),
|
||||
PollOption(random_hex()),
|
||||
PollOption(random_hex()),
|
||||
]),
|
||||
pytest.param(
|
||||
Poll(title=None, options=[]), marks=[pytest.mark.xfail(raises=ValueError)]
|
||||
),
|
||||
])
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
Poll(title=random_hex(), options=[]),
|
||||
Poll(
|
||||
title=random_hex(),
|
||||
options=[
|
||||
PollOption(random_hex(), vote=True),
|
||||
PollOption(random_hex(), vote=True),
|
||||
],
|
||||
),
|
||||
Poll(
|
||||
title=random_hex(),
|
||||
options=[
|
||||
PollOption(random_hex(), vote=False),
|
||||
PollOption(random_hex(), vote=False),
|
||||
],
|
||||
),
|
||||
Poll(
|
||||
title=random_hex(),
|
||||
options=[
|
||||
PollOption(random_hex(), vote=True),
|
||||
PollOption(random_hex(), vote=True),
|
||||
PollOption(random_hex(), vote=False),
|
||||
PollOption(random_hex(), vote=False),
|
||||
PollOption(random_hex()),
|
||||
PollOption(random_hex()),
|
||||
],
|
||||
),
|
||||
pytest.param(
|
||||
Poll(title=None, options=[]), marks=[pytest.mark.xfail(raises=ValueError)]
|
||||
),
|
||||
],
|
||||
)
|
||||
def poll_data(request, client1, group, catch_event):
|
||||
with catch_event("onPollCreated") as x:
|
||||
client1.createPoll(request.param, thread_id=group["id"])
|
||||
@@ -45,11 +57,17 @@ def test_create_poll(client1, group, catch_event, poll_data):
|
||||
thread_id=group["id"],
|
||||
thread_type=ThreadType.GROUP,
|
||||
)
|
||||
assert subset(vars(event["poll"]), title=poll.title, options_count=len(poll.options))
|
||||
for recv_option in event["poll"].options: # The recieved options may not be the full list
|
||||
assert subset(
|
||||
vars(event["poll"]), title=poll.title, options_count=len(poll.options)
|
||||
)
|
||||
for recv_option in event[
|
||||
"poll"
|
||||
].options: # The recieved options may not be the full list
|
||||
old_option, = list(filter(lambda o: o.text == recv_option.text, poll.options))
|
||||
voters = [client1.uid] if old_option.vote else []
|
||||
assert subset(vars(recv_option), voters=voters, votes_count=len(voters), vote=False)
|
||||
assert subset(
|
||||
vars(recv_option), voters=voters, votes_count=len(voters), vote=False
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_poll_options(client1, group, catch_event, poll_data):
|
||||
@@ -62,11 +80,15 @@ def test_fetch_poll_options(client1, group, catch_event, poll_data):
|
||||
@pytest.mark.trylast
|
||||
def test_update_poll_vote(client1, group, catch_event, poll_data):
|
||||
event, poll, options = poll_data
|
||||
new_vote_ids = [o.uid for o in options[0:len(options):2] if not o.vote]
|
||||
re_vote_ids = [o.uid for o in options[0:len(options):2] if o.vote]
|
||||
new_vote_ids = [o.uid for o in options[0 : len(options) : 2] if not o.vote]
|
||||
re_vote_ids = [o.uid for o in options[0 : len(options) : 2] if o.vote]
|
||||
new_options = [random_hex(), random_hex()]
|
||||
with catch_event("onPollVoted") as x:
|
||||
client1.updatePollVote(event["poll"].uid, option_ids=new_vote_ids + re_vote_ids, new_options=new_options)
|
||||
client1.updatePollVote(
|
||||
event["poll"].uid,
|
||||
option_ids=new_vote_ids + re_vote_ids,
|
||||
new_options=new_options,
|
||||
)
|
||||
|
||||
assert subset(
|
||||
x.res,
|
||||
@@ -74,8 +96,12 @@ def test_update_poll_vote(client1, group, catch_event, poll_data):
|
||||
thread_id=group["id"],
|
||||
thread_type=ThreadType.GROUP,
|
||||
)
|
||||
assert subset(vars(x.res["poll"]), title=poll.title, options_count=len(options + new_options))
|
||||
assert subset(
|
||||
vars(x.res["poll"]), title=poll.title, options_count=len(options + new_options)
|
||||
)
|
||||
for o in new_vote_ids:
|
||||
assert o in x.res["added_options"]
|
||||
assert len(x.res["added_options"]) == len(new_vote_ids) + len(new_options)
|
||||
assert set(x.res["removed_options"]) == set(o.uid for o in options if o.vote and o.uid not in re_vote_ids)
|
||||
assert set(x.res["removed_options"]) == set(
|
||||
o.uid for o in options if o.vote and o.uid not in re_vote_ids
|
||||
)
|
||||
|
@@ -38,7 +38,12 @@ def test_send_mentions(client, catch_event, compare, message_with_mentions):
|
||||
mid = client.send(message_with_mentions)
|
||||
|
||||
assert compare(x, mid=mid, message=message_with_mentions.text)
|
||||
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=message_with_mentions.text)
|
||||
assert subset(
|
||||
vars(x.res["message_object"]),
|
||||
uid=mid,
|
||||
author=client.uid,
|
||||
text=message_with_mentions.text,
|
||||
)
|
||||
# The mentions are not ordered by offset
|
||||
for m in x.res["message_object"].mentions:
|
||||
assert vars(m) in [vars(x) for x in message_with_mentions.mentions]
|
||||
@@ -76,7 +81,15 @@ def test_send_images(client, catch_event, compare, method_name, url):
|
||||
|
||||
|
||||
def test_send_local_files(client, catch_event, compare):
|
||||
files = ["image.png", "image.jpg", "image.gif", "file.json", "file.txt", "audio.mp3", "video.mp4"]
|
||||
files = [
|
||||
"image.png",
|
||||
"image.jpg",
|
||||
"image.gif",
|
||||
"file.json",
|
||||
"file.txt",
|
||||
"audio.mp3",
|
||||
"video.mp4",
|
||||
]
|
||||
text = "Files sent locally"
|
||||
with catch_event("onMessage") as x:
|
||||
mid = client.sendLocalFiles(
|
||||
@@ -95,7 +108,10 @@ def test_send_remote_files(client, catch_event, compare):
|
||||
text = "Files sent from remote"
|
||||
with catch_event("onMessage") as x:
|
||||
mid = client.sendRemoteFiles(
|
||||
["https://github.com/carpedm20/fbchat/raw/master/tests/{}".format(f) for f in files],
|
||||
[
|
||||
"https://github.com/carpedm20/fbchat/raw/master/tests/{}".format(f)
|
||||
for f in files
|
||||
],
|
||||
message=Message(text),
|
||||
)
|
||||
|
||||
@@ -104,6 +120,6 @@ def test_send_remote_files(client, catch_event, compare):
|
||||
assert len(x.res["message_object"].attachments) == len(files)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('wave_first', [True, False])
|
||||
@pytest.mark.parametrize("wave_first", [True, False])
|
||||
def test_wave(client, wave_first):
|
||||
client.wave(wave_first)
|
||||
|
@@ -9,4 +9,4 @@ def test_catch_event(client2, catch_event):
|
||||
mid = "test"
|
||||
with catch_event("onMessage") as x:
|
||||
client2.onMessage(mid=mid)
|
||||
assert x.res['mid'] == mid
|
||||
assert x.res["mid"] == mid
|
||||
|
@@ -67,14 +67,19 @@ def test_change_nickname(client, client_all, catch_event, compare):
|
||||
assert compare(x, changed_for=client_all.uid, new_nickname=nickname)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("emoji", [
|
||||
"😀",
|
||||
"😂",
|
||||
"😕",
|
||||
"😍",
|
||||
pytest.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
pytest.param("not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"emoji",
|
||||
[
|
||||
"😀",
|
||||
"😂",
|
||||
"😕",
|
||||
"😍",
|
||||
pytest.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
pytest.param(
|
||||
"not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_change_emoji(client, catch_event, compare, emoji):
|
||||
with catch_event("onEmojiChange") as x:
|
||||
client.changeThreadEmoji(emoji)
|
||||
@@ -85,7 +90,9 @@ def test_change_image_local(client1, group, catch_event):
|
||||
url = path.join(path.dirname(__file__), "resources", "image.png")
|
||||
with catch_event("onImageChange") as x:
|
||||
image_id = client1.changeGroupImageLocal(url, group["id"])
|
||||
assert subset(x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"])
|
||||
assert subset(
|
||||
x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"]
|
||||
)
|
||||
|
||||
|
||||
# To be changed when merged into master
|
||||
@@ -93,7 +100,9 @@ def test_change_image_remote(client1, group, catch_event):
|
||||
url = "https://github.com/carpedm20/fbchat/raw/master/tests/image.png"
|
||||
with catch_event("onImageChange") as x:
|
||||
image_id = client1.changeGroupImageRemote(url, group["id"])
|
||||
assert subset(x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"])
|
||||
assert subset(
|
||||
x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -126,7 +135,7 @@ def test_typing_status(client, catch_event, compare, status):
|
||||
assert compare(x, status=status)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('require_admin_approval', [True, False])
|
||||
@pytest.mark.parametrize("require_admin_approval", [True, False])
|
||||
def test_change_approval_mode(client1, group, catch_event, require_admin_approval):
|
||||
with catch_event("onApprovalModeChange") as x:
|
||||
client1.changeGroupApprovalMode(require_admin_approval, group["id"])
|
||||
@@ -138,6 +147,7 @@ def test_change_approval_mode(client1, group, catch_event, require_admin_approva
|
||||
thread_id=group["id"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mute_time", [0, 10, 100, 1000, -1])
|
||||
def test_mute_thread(client, mute_time):
|
||||
assert client.muteThread(mute_time)
|
||||
|
@@ -106,7 +106,7 @@ def load_client(n, cache):
|
||||
client = Client(
|
||||
load_variable("client{}_email".format(n), cache),
|
||||
load_variable("client{}_password".format(n), cache),
|
||||
user_agent='Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36',
|
||||
user_agent="Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
|
||||
session_cookies=cache.get("client{}_session".format(n), None),
|
||||
max_tries=1,
|
||||
)
|
||||
|
Reference in New Issue
Block a user