Compare commits

..

126 Commits

Author SHA1 Message Date
Mads Marquart
1460b2f421 Version up, thanks to @oneblue and @darylkell 2019-03-10 16:33:44 +01:00
Mads Marquart
27f76ba659 Merge pull request #400 from carpedm20/pull-delta-refactor
Move pull delta parsing into separate method
2019-02-24 20:52:26 +01:00
Mads Marquart
589117b9e7 Move pull delta parsing into _parseDelta (commit 2) 2019-02-24 20:45:44 +01:00
Mads Marquart
80300cd160 Move pull delta parsing into _parseDelta (commit 1) 2019-02-24 20:45:01 +01:00
Mads Marquart
76171408cc Merge pull request #399 from carpedm20/attrs
Use attrs to declare our models
2019-02-24 20:24:21 +01:00
Mads Marquart
c1800a174f Update minimum attrs version 2019-02-24 20:18:11 +01:00
Mads Marquart
8ae8435940 Use attrs, to remove verbose __init__ and __repr__ methods
Backwards compatibility is strictly preserved in `__init__`, including parameter names, defaults and position. Whenever that's difficult using `attrs`, the custom `__init__` is kept instead (for the time being).

`__repr__` methods have changed to the format `attrs` use, but people don't rely on this for anything other than debug output, so it shouldn't be a problem.
2019-02-24 20:18:07 +01:00
Mads Marquart
f916cb3b53 Add attrs as dependency 2019-02-24 20:18:04 +01:00
Mads Marquart
929c2137bf Move model docstrings into the class level, out of init 2019-02-24 20:18:00 +01:00
Mads Marquart
98056e91c5 Split models.py into several files (#398)
* Move exception models into separate file
* Move thread model into separate file
* Move user model into separate file
* Move group and room models into separate file
* Move page model into separate file
* Move message model into separate file
* Move basic attachment models into separate file
* Move sticker model into separate file
* Move location models into separate file
* Move file attachment models into separate file
* Move mention model to reside with message model
* Move quick reply models into separate file
* Move poll models into separate file
* Move plan model into separate file
* Move active status model to reside with user model
* Move core enum model into separate file
* Move thread-related enums to reside with thread model
* Move typingstatus model to reside with user model
* Move emojisize and reaction enums to reside wtih message model
2019-02-24 20:06:59 +01:00
Mads Marquart
944a7248c3 Disable travis email notifications 2019-02-24 02:17:03 +01:00
darylkell
caa2ecd0b7 Fix LocationAttachment (#395)
Set `LocationAttachment.address` instead of `latitude` and `longitude`, when no GPS coords are supplied. Fixes #392
2019-02-19 12:19:20 +01:00
Blue
dfc2d0652f Make fetchUnread and fetchUnseen include group chats (#394)
* Correct fetchUnread and fetchUnseen to include 1:1 chats and group chats
2019-02-18 22:37:16 +01:00
Mads Marquart
8d25540445 Version up, thanks to @kapi2289 2019-02-03 22:07:44 +01:00
Mads Marquart
6ea174bfd4 Merge pull request #389 from kapi2289/fix-388
Fix #388 issue
2019-02-03 22:06:26 +01:00
Kacper Ziubryniewicz
56e43aec0e Apply suggestions and fixes from review 2019-02-03 19:03:43 +01:00
Kacper Ziubryniewicz
491d120c25 Fix #388 issue 2019-02-03 14:45:10 +01:00
Mads Marquart
82d071d52c Version up 2019-01-31 21:27:04 +01:00
Mads Marquart
8190654a91 Add section about black in CONTRIBUTING.rst 2019-01-31 21:09:15 +01:00
Mads Marquart
5e21702d16 Add black code style badge 2019-01-31 21:00:17 +01:00
Mads Marquart
3df4172237 Add travis format checking step 2019-01-31 20:59:48 +01:00
Mads Marquart
e0710a2ec1 Format strings using black 2019-01-31 20:55:22 +01:00
Mads Marquart
d20fc3b9ce Format using black (without string normalization) 2019-01-31 20:54:32 +01:00
Mads Marquart
f25faec108 Version up 2019-01-31 20:26:17 +01:00
Mads Marquart
2750658c3c Fix #385 2019-01-31 20:26:04 +01:00
Mads Marquart
e6bc5bbab3 Version up, thanks to @kapi2289 and @2FWAH 2019-01-31 20:20:17 +01:00
Mads Marquart
de5f3a9d9e Merge branch 'pr/300' 2019-01-31 20:13:27 +01:00
Mads Marquart
7f0da012c2 Few nitpicky fixes 2019-01-31 20:12:59 +01:00
Mads Marquart
76ecbf5eb0 Merge branch 'pr/325' 2019-01-31 19:57:22 +01:00
Mads Marquart
06881a4c70 Add formatMentions docstring 2019-01-31 19:56:35 +01:00
Mads Marquart
c14fdd82db Merge branch 'pr/338' 2019-01-31 19:29:54 +01:00
Mads Marquart
b1a02ad930 Merge pull request #342 from kapi2289/quick_replies
[Feature] Quick replies
2019-01-31 19:26:03 +01:00
Mads Marquart
2b580c60e9 Readd deprecated markAlive parameter 2019-01-31 19:23:46 +01:00
Mads Marquart
27ffba3b14 Fix a few isinstance checks 2019-01-31 19:21:52 +01:00
Mads Marquart
fb7bf437ba Merge pull request #384 from carpedm20/github-releases-ci
Automatic GitHub Releases
2019-01-25 20:01:37 +01:00
Mads Marquart
d8baf0b9e7 Put automatic GitHub releases in the draft state
This is done so that I can edit the description as needed, before publishing
2019-01-25 19:35:20 +01:00
Mads Marquart
a6945fe880 Merge branch 'disable-online-tests' 2019-01-25 19:18:03 +01:00
Mads Marquart
6ff77dd8c7 Merge pull request #382 from carpedm20/flit
Use `flit` as our build system
2019-01-25 19:14:04 +01:00
Mads Marquart
1d925a608b Update pypy version to 3.5 2019-01-25 18:53:14 +01:00
Mads Marquart
646669ca75 Add Github Releases deployment 2019-01-25 18:49:56 +01:00
Mads Marquart
0ec2baaa83 Add Python 3.7 testing 2019-01-25 18:49:35 +01:00
Mads Marquart
5abaaefd1c Disable Travis online tests 2019-01-25 18:49:08 +01:00
Mads Marquart
687afea0f2 Pin minimum pytest version to fix tests 2019-01-25 17:45:15 +01:00
Mads Marquart
7398d4fa2b Use --python option to properly install the package under Python 2.7 2019-01-25 17:14:14 +01:00
Mads Marquart
d73c8c3627 Fix travis setup for running flit under Python 2.7 2019-01-25 17:05:35 +01:00
Mads Marquart
f921b91c5b Make travis use flit 2019-01-25 16:43:22 +01:00
Mads Marquart
8ed3c1b159 Use flit instead of setuptools
Mostly just a simple move from `setup.cfg` -> `pyproject.toml`. Had to reformat the description in `__init__` a little though.
2019-01-25 16:36:09 +01:00
Mads Marquart
4f947cdbb5 Version up, thanks to @kapi2289 and @kaushalvivek 2019-01-25 16:01:47 +01:00
Mads Marquart
ec6c29052a Merge pull request #371 from carpedm20/fix-enums
Fix `ThreadColor` and `MessageReaction` enums
2019-01-24 22:42:41 +01:00
Mads Marquart
6b117502f3 Merge branch 'master' into fix-enums 2019-01-24 22:40:44 +01:00
Kacper Ziubryniewicz
a367aa0b31 Replying on location quick replies 2019-01-05 20:40:45 +01:00
Kacper Ziubryniewicz
7f6843df55 Better quick reply types 2019-01-05 20:06:28 +01:00
Kacper Ziubryniewicz
4b485d54b6 Merge remote-tracking branch 'origin/master' into quick_replies 2019-01-05 19:29:32 +01:00
Kacper Ziubryniewicz
e80a040db4 Deprecate markAlive parameter in doOneListen and _pullMessage 2019-01-05 18:48:40 +01:00
Kacper Ziubryniewicz
c357fd085b Better listening for buddylist overlay and chatbox presence 2019-01-05 18:36:48 +01:00
Kacper Ziubryniewicz
d0c5f29b0a Fixed getting active status 2019-01-05 18:24:23 +01:00
Mads Marquart
3e7b20c379 Merge pull request #377 from kapi2289/fix-fbchatexception
Fixed typos in FBchatException
2019-01-05 18:20:38 +01:00
Kacper Ziubryniewicz
f4a997c0ef Fixed typos in FBchatException 2019-01-05 17:55:54 +01:00
Kacper Ziubryniewicz
102e74bb63 Merge remote-tracking branch 'origin/master' into active_status 2019-01-05 17:46:27 +01:00
Mads Marquart
84fa15e44c Merge pull request #333 from kapi2289/extensible_attachments
[Feature] Extensible attachments
2019-01-04 21:06:11 +01:00
Kacper Ziubryniewicz
7b8ecf8fe3 Changed deleted to unsent 2019-01-04 20:02:00 +01:00
Kacper Ziubryniewicz
79ebf920ea More on responding to quick replies 2019-01-03 23:28:23 +01:00
Kacper Ziubryniewicz
0d05d42f70 getPhoneNumbers and getEmails methods 2019-01-03 22:54:47 +01:00
Kacper Ziubryniewicz
95989b6da7 Merge branch 'master' into extensible_attachments 2018-12-23 14:58:03 +01:00
Kacper Ziubryniewicz
22e57f99a1 deleted attribute of Message
and batter handling of deleted (unsended) messages
2018-12-23 14:56:27 +01:00
Kacper Ziubryniewicz
b9d29c0417 Removed addReaction, removeReaction, _react
(and undeprecated `reactToMessage`)
2018-12-23 14:45:17 +01:00
Kacper Ziubryniewicz
edc33db9e8 Few fixes in quick replies 2018-12-23 14:36:26 +01:00
Mads Marquart
45d8b45d96 Fix enum_extend_if_invalid warning 2018-12-12 23:22:08 +01:00
Mads Marquart
b6a6d7dc68 Move enum_extend_if_invalid to utils.py 2018-12-12 23:06:16 +01:00
Mads Marquart
c57b84cd0b Refactor enum extending 2018-12-12 23:04:26 +01:00
Mads Marquart
78e7841b5e Extend MessageReaction when encountering unknown values 2018-12-12 22:53:23 +01:00
Mads Marquart
e41d981449 Extend ThreadColor when encountering unknown values 2018-12-12 22:44:19 +01:00
Mads Marquart
381227af66 Make use aenum instead of the default enum 2018-12-12 22:39:31 +01:00
Mads Marquart
2f8d0728ba Merge pull request #366 from kaushalvivek/master
Fix for issue #365
2018-12-10 21:16:57 +01:00
kaushalvivek
13bfc5f2f9 Fix for search limit 2018-12-10 14:46:04 +05:30
Mads Marquart
f8d3b571ba Version up, thanks to @ekohilas and @kapi2289 2018-12-09 21:21:00 +01:00
Mads Marquart
64b1e52d4c Merge pull request #357 from carpedm20/fixed-listening
Fixed listening
2018-12-09 19:23:33 +01:00
Mads Marquart
b650f7ee9a Merge pull request #367 from carpedm20/fix-pytest-deprecation
Fix pytest "Applying marks directly to parameters" deprecation
2018-12-09 19:23:20 +01:00
Kacper Ziubryniewicz
d4446280c7 Detecting when someone unsends a message 2018-12-09 15:27:01 +01:00
Mads Marquart
3443a233f4 Fix pytest "Applying marks directly to parameters" deprecation 2018-12-09 15:02:48 +01:00
Kacper Ziubryniewicz
861f17bc4d Added DeletedMessage attachment 2018-12-09 14:55:10 +01:00
Kacper Ziubryniewicz
41bbe18e3d Unsending messages 2018-12-09 14:36:23 +01:00
Kacper Ziubryniewicz
5f9c357a15 Fixed graphql and added method for replying on quick replies 2018-12-09 01:07:33 +01:00
Kacper Ziubryniewicz
c089298f46 Sending new quick replies 2018-12-09 00:57:58 +01:00
Kacper Ziubryniewicz
be968e0caa New models for quick replies 2018-12-09 00:32:44 +01:00
Vivek Kaushal
d32b7b612a Fix for issue #365 2018-12-07 21:26:48 +05:30
Mads Marquart
160386be62 Added support for request_batch parsing in _parseMessage 2018-11-09 20:08:26 +01:00
Mads Marquart
64bdde8f33 Sticky and pool parameters can be set after the inital _fetchSticky 2018-11-07 20:06:10 +01:00
Kacper Ziubryniewicz
8739318101 Sending voice clips 2018-10-30 22:24:47 +01:00
Kacper Ziubryniewicz
1ac569badd Sending pinned or current location 2018-10-30 22:21:05 +01:00
Kacper Ziubryniewicz
e38f891693 Active status fixes 2018-10-30 21:48:55 +01:00
Mads Marquart
89a277c354 Merge pull request #354 from ekohilas/master
separate spellchecked docs
2018-10-28 12:46:48 +01:00
Mads Marquart
8238387c7d Merge pull request #353 from ekohilas/docstrings
completed todo for graphql_requests
2018-10-28 12:45:37 +01:00
ekohilas
6c829581af completed todo for graphql_requests 2018-10-27 02:02:15 +11:00
ekohilas
d180650c1b spellchecked docs 2018-10-25 18:18:19 +11:00
Mads Marquart
772bf5518f Merge pull request #346 from kapi2289/remove_unnecessary
Remove unnecessary code
2018-10-07 16:50:31 +02:00
Kacper Ziubryniewicz
153dc0bdad Remove unnecessary code 2018-10-07 16:27:19 +02:00
Kacper Ziubryniewicz
b7ea8e6001 New sendLocation method 2018-09-29 13:48:08 +02:00
Kacper Ziubryniewicz
b0bf5ba8e0 Update graphql.py 2018-09-29 13:42:11 +02:00
Kacper Ziubryniewicz
8169a5f776 Changed LocationAttachment 2018-09-29 13:40:38 +02:00
Kacper Ziubryniewicz
492465a525 Update graphql.py 2018-09-25 18:00:44 +02:00
Kacper Ziubryniewicz
f185e44f93 Update models.py 2018-09-25 17:59:16 +02:00
Kacper Ziubryniewicz
5f2c318baf Sending quick replies 2018-09-24 21:04:21 +02:00
Kacper Ziubryniewicz
531a5b77d0 GraphQL method for quick replies 2018-09-24 20:57:19 +02:00
Kacper Ziubryniewicz
f9245cdfed New model and Message attribute
New `QuickReply` model and `quick_replies` attribute of `Message` model.
2018-09-24 20:54:25 +02:00
Kacper Ziubryniewicz
47ea88e025 Read commit description
- Fixed `onImageChange` documentation and added missing `msg` parameter
- Moved `on` methods to the right place
- Added changing client active status while listening
- Added fetching friends' active status
2018-09-22 21:52:40 +02:00
Kacper Ziubryniewicz
345a473ee0 ActiveStatus model 2018-09-22 21:34:44 +02:00
Kacper Ziubryniewicz
c6dc432d06 Move on methods to the right place 2018-09-22 20:39:41 +02:00
2FWAH
af3bd55535 Add basic test for fetchThreads 2018-09-21 19:43:39 +02:00
2FWAH
5fa1d86191 Add before, after and limit parameters to fetchThreads 2018-09-21 19:12:46 +02:00
2FWAH
d4859b675a Fix ident for _forcedFetch 2018-09-21 17:36:16 +02:00
2FWAH
9aa427031e Merge from upstream and solve conflict in fbchat/client.py 2018-09-21 17:29:58 +02:00
Kacper Ziubryniewicz
9e8fe7bc1e Fix Python 2.7 compability 2018-09-15 11:34:16 +02:00
Kacper Ziubryniewicz
90813c959d Added get_url_parameters util method 2018-09-15 11:21:35 +02:00
Kacper Ziubryniewicz
940a65954c Read commit description
Added:
- Detecting extensible attachments
- Fetching live user location
- New methods for message reacting
- New `on` methods: `onReactionAdded`, `onReactionRemoved`, `onBlock`, `onUnblock`, `onLiveLocation`
- Fixed `size` of attachments
2018-09-12 17:52:38 +02:00
Kacper Ziubryniewicz
9b4e753a79 Added graphql methods for extensible attachments 2018-09-12 17:48:35 +02:00
Kacper Ziubryniewicz
e0be9029e4 Added extensible attachments models 2018-09-12 17:48:00 +02:00
Kacper Ziubryniewicz
aa3faca246 Added formatMentions method 2018-08-30 15:57:16 +02:00
2FWAH
2edb95dfdd Fetch missing users in a single request 2018-06-12 08:38:02 +02:00
2FWAH
e0bb9960fb Check if list is empty with if instead of len() 2018-06-12 08:15:53 +02:00
2FWAH
71608845c0 Use snake case convention 2018-06-12 07:55:16 +02:00
2FWAH
0048e82151 Fix typo in fetchAllUsersFromThreads 2018-06-07 21:58:00 +02:00
2FWAH
0767ef4902 Add fetchAllUsersFromThreads
Add a method to get all users involved in threads (given as a parameter)
2018-06-01 23:27:34 +02:00
2FWAH
abe3357e67 Explicit parameter thread_location 2018-06-01 23:08:03 +02:00
2FWAH
19457efe9b Fix call to fetchThreadList
Use "self" instead of "client"
2018-06-01 23:06:02 +02:00
2FWAH
487a2eb3e3 Add fetchThreads method
Add a method to get all threads in Location (INBOX, ARCHIVED...)
2018-06-01 22:59:56 +02:00
50 changed files with 4238 additions and 2096 deletions

View File

@@ -1,90 +1,61 @@
sudo: false sudo: false
language: python language: python
conditions: v1 python: 3.6
# There are two accounts made specifically for Travis, and the passwords are really only encrypted for obscurity cache: pip
# The global env variables `client1_email`, `client1_password`, `client2_email`, `client2_password` and `group_id`
# are set on the Travis Settings page
# The tests are run with `Limit concurrent jobs = 1`, since the tests can't use the clients simultaneously before_install: pip install flit
# Use `--deps production` so that we don't install unnecessary dependencies
install: install: flit install --deps production --extras test
- pip install -U -r requirements.txt script: pytest -m offline
- 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
jobs: jobs:
include: include:
# The tests are split into online and offline versions. - python: 2.7
# The online tests are only run against the master branch. before_install:
# Because: - sudo apt-get -y install python3-pip python3-setuptools
# Travis caching is per-branch and per-job, so even though we cache the Facebook sessions via. `.pytest_cache` - sudo pip3 install flit
# and in `tests.utils.load_client`, we need 6 new sessions per branch. This is usually the point where Facebook install: flit install --python python --deps production --extras test
# starts complaining, and we have to manually fix it - python: 3.4
- python: 3.5
- python: 3.6
- python: 3.7
dist: xenial
sudo: required
- python: pypy3.5
- &test-online - name: Lint
if: (branch = master OR tag IS present) AND type != pull_request before_install: skip
stage: online tests install: pip install black
script: scripts/travis-online script: black --check --verbose .
# Run online tests in all the supported python versions - stage: deploy
python: 2.7 name: Github Releases
- <<: *test-online if: tag IS present
python: 3.4 install: skip
- <<: *test-online script: flit build
python: 3.5 deploy:
- <<: *test-online provider: releases
python: 3.6 api_key: $GITHUB_OAUTH_TOKEN
- <<: *test-online file_glob: true
python: pypy file: dist/*
skip_cleanup: true
draft: true
on:
tags: true
# Run the expensive tests, with the python version most likely to break, aka. 2 - stage: deploy
- <<: *test-online name: PyPI
# Only run if the commit message includes [ci all] or [all ci] if: tag IS present
if: commit_message =~ /\[ci\s+all\]|\[all\s+ci\]/ install: skip
python: 2.7 script: skip
env: PYTEST_ADDOPTS='-m expensive' deploy:
provider: script
script: flit publish
on:
tags: true
- &test-offline notifications:
# Ideally, it'd be nice to run the offline tests in every build, but since we can't run jobs concurrently (yet), email:
# we'll disable them when they're not needed, and include them inside the online tests instead on_success: never
if: not ((branch = master OR tag IS present) AND type != pull_request) on_failure: change
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

View File

@@ -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. 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``. 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 Testing Environment
------------------- -------------------

View File

@@ -1,3 +0,0 @@
include LICENSE
include CONTRIBUTING.rst
include README.rst

View File

@@ -5,9 +5,9 @@ fbchat: Facebook Chat (Messenger) for Python
:target: https://github.com/carpedm20/fbchat/tree/master/LICENSE :target: https://github.com/carpedm20/fbchat/tree/master/LICENSE
:alt: License: BSD 3-Clause :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 :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 .. image:: https://readthedocs.org/projects/fbchat/badge/?version=master
:target: https://fbchat.readthedocs.io :target: https://fbchat.readthedocs.io
@@ -17,6 +17,10 @@ fbchat: Facebook Chat (Messenger) for Python
:target: https://travis-ci.org/carpedm20/fbchat :target: https://travis-ci.org/carpedm20/fbchat
:alt: Travis CI :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. 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>`__. 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: Installation:
.. code-block:: console .. code-block::
$ pip install fbchat $ 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 $ git clone https://github.com/carpedm20/fbchat.git
$ cd fbchat $ cd fbchat
$ python setup.py install $ flit install
Maintainer Maintainer

View File

@@ -1,2 +0,0 @@
pytest
six

View File

@@ -13,7 +13,7 @@ If you are looking for information on a specific function, class, or method, thi
Client Client
------ ------
This is the main class of `fbchat`, which contains all the methods you use to interract with Facebook. This is the main class of `fbchat`, which contains all the methods you use to interact with Facebook.
You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening) You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening)
.. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO) .. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO)

View File

@@ -19,7 +19,8 @@
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath(".."))
import fbchat import fbchat
import tests import tests
@@ -36,27 +37,27 @@ from fbchat import __copyright__, __author__, __version__, __description__
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', "sphinx.ext.autodoc",
'sphinx.ext.intersphinx', "sphinx.ext.intersphinx",
'sphinx.ext.todo', "sphinx.ext.todo",
'sphinx.ext.viewcode' "sphinx.ext.viewcode",
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
# source_suffix = ['.rst', '.md'] # source_suffix = ['.rst', '.md']
source_suffix = '.rst' source_suffix = ".rst"
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = 'fbchat' project = "fbchat"
title = 'fbchat Documentation' title = "fbchat Documentation"
copyright = __copyright__ copyright = __copyright__
author = __author__ author = __author__
description = __description__ description = __description__
@@ -80,10 +81,10 @@ language = None
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path # 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. # 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. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True todo_include_todos = True
@@ -95,7 +96,7 @@ todo_include_todos = True
# a list of builtin themes. # 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 # 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 # 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, # 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, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ["_static"]
# -- Options for HTMLHelp output ------------------------------------------ # -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = project + 'doc' htmlhelp_basename = project + "doc"
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
@@ -121,15 +122,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# #
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# #
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# #
# 'preamble': '', # 'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
# #
# 'figure_align': 'htbp', # 'figure_align': 'htbp',
@@ -138,20 +136,14 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [(master_doc, project + ".tex", title, author, "manual")]
(master_doc, project + '.tex', title,
author, 'manual'),
]
# -- Options for manual page output --------------------------------------- # -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, project, title, [author], 1)]
(master_doc, project, title,
[author], 1)
]
# -- Options for Texinfo output ------------------------------------------- # -- Options for Texinfo output -------------------------------------------
@@ -160,32 +152,27 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, project, title, (master_doc, project, title, author, project, description, "Miscellaneous")
author, project, description,
'Miscellaneous'),
] ]
# Example configuration for intersphinx: refer to the Python standard library. # 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 add_function_parentheses = False
html_theme_options = { html_theme_options = {
'show_powered_by': False, "show_powered_by": False,
'github_user': 'carpedm20', "github_user": "carpedm20",
'github_repo': project, "github_repo": project,
'github_banner': True, "github_banner": True,
'show_related': False "show_related": False,
} }
html_sidebars = { html_sidebars = {"**": ["sidebar.html", "searchbox.html"]}
'**': ['sidebar.html', 'searchbox.html']
}
html_show_sphinx = False html_show_sphinx = False
html_show_sourcelink = False html_show_sourcelink = False
autoclass_content = 'init' autoclass_content = "both"
html_short_title = description html_short_title = description

View File

@@ -18,7 +18,7 @@ This will show basic usage of `fbchat`
Interacting with Threads Interacting with Threads
------------------------ ------------------------
This will interract with the thread in every way `fbchat` supports This will interact with the thread in every way `fbchat` supports
.. literalinclude:: ../examples/interract.py .. literalinclude:: ../examples/interract.py

View File

@@ -8,7 +8,7 @@ FAQ
Version X broke my installation Version X broke my installation
------------------------------- -------------------------------
We try to provide backwards compatability where possible, but since we're not part of Facebook, We try to provide backwards compatibility where possible, but since we're not part of Facebook,
most of the things may be broken at any point in time most of the things may be broken at any point in time
Downgrade to an earlier version of fbchat, run this command Downgrade to an earlier version of fbchat, run this command

View File

@@ -6,7 +6,7 @@ Introduction
============ ============
`fbchat` uses your email and password to communicate with the Facebook server. `fbchat` uses your email and password to communicate with the Facebook server.
That means that you should always store your password in a seperate file, in case e.g. someone looks over your shoulder while you're writing code. That means that you should always store your password in a separate file, in case e.g. someone looks over your shoulder while you're writing code.
You should also make sure that the file's access control is appropriately restrictive You should also make sure that the file's access control is appropriately restrictive
@@ -16,7 +16,7 @@ Logging In
---------- ----------
Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt
(If you want to supply the code in another fasion, overwrite :func:`Client.on2FACode`):: (If you want to supply the code in another fashion, overwrite :func:`Client.on2FACode`)::
from fbchat import Client from fbchat import Client
from fbchat.models import * from fbchat.models import *
@@ -50,7 +50,7 @@ A thread can refer to two things: A Messenger group chat or a single Facebook us
:class:`models.ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``. :class:`models.ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``.
These will specify whether the thread is a single user chat or a group chat. These will specify whether the thread is a single user chat or a group chat.
This is required for many of `fbchat`'s functions, since Facebook differetiates between these two internally This is required for many of `fbchat`'s functions, since Facebook differentiates between these two internally
Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`, Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`,
and searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching` and searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching`
@@ -141,7 +141,7 @@ Sessions
-------- --------
`fbchat` provides functions to retrieve and set the session cookies. `fbchat` provides functions to retrieve and set the session cookies.
This will enable you to store the session cookies in a seperate file, so that you don't have to login each time you start your script. This will enable you to store the session cookies in a separate file, so that you don't have to login each time you start your script.
Use :func:`Client.getSession` to retrieve the cookies:: Use :func:`Client.getSession` to retrieve the cookies::
session_cookies = client.getSession() session_cookies = client.getSession()

View File

@@ -15,7 +15,7 @@ To use the tests, copy ``tests/data.json`` to ``tests/my_data.json`` or type the
Please remember to test all supported python versions. Please remember to test all supported python versions.
If you've made any changes to the 2FA functionality, test it with a 2FA enabled account. If you've made any changes to the 2FA functionality, test it with a 2FA enabled account.
If you only want to execute specific tests, pass the function names in the commandline (not including the `test_` prefix). Example:: If you only want to execute specific tests, pass the function names in the command line (not including the `test_` prefix). Example::
$ python tests.py sendMessage sessions sendEmoji $ python tests.py sendMessage sessions sendEmoji

View File

@@ -3,10 +3,10 @@
from fbchat import Client from fbchat import Client
from fbchat.models import * 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() client.logout()

View File

@@ -14,5 +14,6 @@ class EchoBot(Client):
if author_id != self.uid: if author_id != self.uid:
self.send(message_object, thread_id=thread_id, thread_type=thread_type) self.send(message_object, thread_id=thread_id, thread_type=thread_type)
client = EchoBot("<email>", "<password>") client = EchoBot("<email>", "<password>")
client.listen() client.listen()

View File

@@ -3,7 +3,7 @@
from fbchat import Client from fbchat import Client
from fbchat.models import * 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 # Fetches a list of all users you're currently chatting with, as `User` objects
users = client.fetchAllUsers() 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 # 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 # 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("user's name: {}".format(user.name))
print("users' names: {}".format([users[k].name for k in users])) 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, # `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: # 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 name: {}".format(user.name))
print("user's photo: {}".format(user.photo)) print("user's photo: {}".format(user.photo))
print("Is user client's friend: {}".format(user.is_friend)) 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 # 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 # Since the message come in reversed order, reverse them
messages.reverse() 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 # 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 name: {}".format(thread.name))
print("thread's type: {}".format(thread.type)) print("thread's type: {}".format(thread.type))
# `searchForThreads` searches works like `searchForUsers`, but gives us a list of threads instead # `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 name: {}".format(thread.name))
print("thread's type: {}".format(thread.type)) print("thread's type: {}".format(thread.type))

View File

@@ -5,57 +5,89 @@ from fbchat.models import *
client = Client("<email>", "<password>") client = Client("<email>", "<password>")
thread_id = '1234567890' thread_id = "1234567890"
thread_type = ThreadType.GROUP thread_type = ThreadType.GROUP
# Will send a message to the thread # 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 # 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 `👍` # 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` # 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 # 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>` # 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 # 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 # Only do these actions if the thread is a group
if thread_type == ThreadType.GROUP: if thread_type == ThreadType.GROUP:
# Will remove the user with ID `<user id>` from the thread # 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 # 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 # 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>` # 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>` # 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` # 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` # Will change the thread color to `MESSENGER_BLUE`
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id=thread_id) client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id=thread_id)
# Will change the thread emoji to `👍` # 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 # Will react to a message with a 😍 emoji
client.reactToMessage('<message id>', MessageReaction.LOVE) client.reactToMessage("<message id>", MessageReaction.LOVE)

View File

@@ -4,28 +4,33 @@ from fbchat import log, Client
from fbchat.models import * from fbchat.models import *
# Change this to your group id # Change this to your group id
old_thread_id = '1234567890' old_thread_id = "1234567890"
# Change these to match your liking # Change these to match your liking
old_color = ThreadColor.MESSENGER_BLUE old_color = ThreadColor.MESSENGER_BLUE
old_emoji = '👍' old_emoji = "👍"
old_title = 'Old group chat name' old_title = "Old group chat name"
old_nicknames = { old_nicknames = {
'12345678901': "User nr. 1's nickname", "12345678901": "User nr. 1's nickname",
'12345678902': "User nr. 2's nickname", "12345678902": "User nr. 2's nickname",
'12345678903': "User nr. 3's nickname", "12345678903": "User nr. 3's nickname",
'12345678904': "User nr. 4's nickname" "12345678904": "User nr. 4's nickname",
} }
class KeepBot(Client): class KeepBot(Client):
def onColorChange(self, author_id, new_color, thread_id, thread_type, **kwargs): def onColorChange(self, author_id, new_color, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and old_color != new_color: 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) self.changeThreadColor(old_color, thread_id=thread_id)
def onEmojiChange(self, author_id, new_emoji, thread_id, thread_type, **kwargs): def onEmojiChange(self, author_id, new_emoji, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and new_emoji != old_emoji: 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) self.changeThreadEmoji(old_emoji, thread_id=thread_id)
def onPeopleAdded(self, added_ids, author_id, thread_id, **kwargs): 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): def onPersonRemoved(self, removed_id, author_id, thread_id, **kwargs):
# No point in trying to add ourself # 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)) log.info("{} got removed. They will be re-added".format(removed_id))
self.addUsersToGroup(removed_id, thread_id=thread_id) self.addUsersToGroup(removed_id, thread_id=thread_id)
def onTitleChange(self, author_id, new_title, thread_id, thread_type, **kwargs): def onTitleChange(self, author_id, new_title, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and old_title != new_title: 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)) log.info(
self.changeThreadTitle(old_title, thread_id=thread_id, thread_type=thread_type) "{} 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 = KeepBot("<email>", "<password>")
client.listen() client.listen()

View File

@@ -3,15 +3,23 @@
from fbchat import log, Client from fbchat import log, Client
from fbchat.models import * from fbchat.models import *
class RemoveBot(Client): class RemoveBot(Client):
def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs): 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 # 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: if message_object.text == "Remove me!" and thread_type == ThreadType.GROUP:
log.info('{} will be removed from {}'.format(author_id, thread_id)) log.info("{} will be removed from {}".format(author_id, thread_id))
self.removeUserFromGroup(author_id, thread_id=thread_id) self.removeUserFromGroup(author_id, thread_id=thread_id)
else: else:
# Sends the data to the inherited onMessage, so that we can still see when a message is recieved # 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 = RemoveBot("<email>", "<password>")
client.listen() client.listen()

View File

@@ -1,29 +1,23 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
"""Facebook Chat (Messenger) for Python
""" :copyright: (c) 2015 - 2019 by Taehoon Kim
fbchat :license: BSD 3-Clause, see LICENSE for more details.
~~~~~~
Facebook Chat (Messenger) for Python
:copyright: (c) 2015 - 2018 by Taehoon Kim
:license: BSD 3-Clause, see LICENSE for more details.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from .models import *
from .client import * from .client import *
__title__ = 'fbchat' __title__ = "fbchat"
__version__ = '1.4.1' __version__ = "1.6.4"
__description__ = 'Facebook Chat (Messenger) for Python' __description__ = "Facebook Chat (Messenger) for Python"
__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim' __copyright__ = "Copyright 2015 - 2019 by Taehoon Kim"
__license__ = 'BSD 3-Clause' __license__ = "BSD 3-Clause"
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' __author__ = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart"
__email__ = 'carpedm20@gmail.com' __email__ = "carpedm20@gmail.com"
__all__ = [ __all__ = ["Client"]
'Client',
]

48
fbchat/_attachment.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,8 @@ from .utils import *
# Shameless copy from https://stackoverflow.com/a/8730674 # Shameless copy from https://stackoverflow.com/a/8730674
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL 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): class ConcatJSONDecoder(json.JSONDecoder):
def decode(self, s, _w=WHITESPACE.match): def decode(self, s, _w=WHITESPACE.match):
@@ -21,367 +22,589 @@ class ConcatJSONDecoder(json.JSONDecoder):
end = _w(s, end).end() end = _w(s, end).end()
objs.append(obj) objs.append(obj)
return objs return objs
# End shameless copy # End shameless copy
def graphql_color_to_enum(color): def graphql_color_to_enum(color):
if color is None: if color is None:
return None return None
if len(color) == 0: if not color:
return ThreadColor.MESSENGER_BLUE return ThreadColor.MESSENGER_BLUE
try: color = color[2:] # Strip the alpha value
return ThreadColor('#{}'.format(color[2:].lower())) color_value = "#{}".format(color.lower())
except ValueError: return enum_extend_if_invalid(ThreadColor, color_value)
raise FBchatException('Could not get ThreadColor from color: {}'.format(color))
def get_customization_info(thread): 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 {} return {}
info = thread['customization_info'] info = thread["customization_info"]
rtn = { rtn = {
'emoji': info.get('emoji'), "emoji": info.get("emoji"),
'color': graphql_color_to_enum(info.get('outgoing_bubble_color')) "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'): if (
rtn['nicknames'] = {} thread.get("thread_type") == "GROUP"
for k in info.get('participant_customizations', []): or thread.get("is_group_thread")
rtn['nicknames'][k['participant_id']] = k.get('nickname') or thread.get("thread_key", {}).get("thread_fbid")
elif info.get('participant_customizations'): ):
uid = thread.get('thread_key', {}).get('other_user_id') or thread.get('id') rtn["nicknames"] = {}
pc = info['participant_customizations'] 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 len(pc) > 0:
if pc[0].get('participant_id') == uid: if pc[0].get("participant_id") == uid:
rtn['nickname'] = pc[0].get('nickname') rtn["nickname"] = pc[0].get("nickname")
else: else:
rtn['own_nickname'] = pc[0].get('nickname') rtn["own_nickname"] = pc[0].get("nickname")
if len(pc) > 1: if len(pc) > 1:
if pc[1].get('participant_id') == uid: if pc[1].get("participant_id") == uid:
rtn['nickname'] = pc[1].get('nickname') rtn["nickname"] = pc[1].get("nickname")
else: else:
rtn['own_nickname'] = pc[1].get('nickname') rtn["own_nickname"] = pc[1].get("nickname")
return rtn return rtn
def graphql_to_sticker(s): def graphql_to_sticker(s):
if not s: if not s:
return None return None
sticker = Sticker( sticker = Sticker(uid=s["id"])
uid=s['id'] if s.get("pack"):
) sticker.pack = s["pack"].get("id")
if s.get('pack'): if s.get("sprite_image"):
sticker.pack = s['pack'].get('id')
if s.get('sprite_image'):
sticker.is_animated = True sticker.is_animated = True
sticker.medium_sprite_image = s['sprite_image'].get('uri') sticker.medium_sprite_image = s["sprite_image"].get("uri")
sticker.large_sprite_image = s['sprite_image_2x'].get('uri') sticker.large_sprite_image = s["sprite_image_2x"].get("uri")
sticker.frames_per_row = s.get('frames_per_row') sticker.frames_per_row = s.get("frames_per_row")
sticker.frames_per_col = s.get('frames_per_column') sticker.frames_per_col = s.get("frames_per_column")
sticker.frame_rate = s.get('frame_rate') sticker.frame_rate = s.get("frame_rate")
sticker.url = s.get('url') sticker.url = s.get("url")
sticker.width = s.get('width') sticker.width = s.get("width")
sticker.height = s.get('height') sticker.height = s.get("height")
if s.get('label'): if s.get("label"):
sticker.label = s['label'] sticker.label = s["label"]
return sticker return sticker
def graphql_to_attachment(a): def graphql_to_attachment(a):
_type = a['__typename'] _type = a["__typename"]
if _type in ['MessageImage', 'MessageAnimatedImage']: if _type in ["MessageImage", "MessageAnimatedImage"]:
return ImageAttachment( return ImageAttachment(
original_extension=a.get('original_extension') or (a['filename'].split('-')[0] if a.get('filename') else None), original_extension=a.get("original_extension")
width=a.get('original_dimensions', {}).get('width'), or (a["filename"].split("-")[0] if a.get("filename") else None),
height=a.get('original_dimensions', {}).get('height'), width=a.get("original_dimensions", {}).get("width"),
is_animated=_type=='MessageAnimatedImage', height=a.get("original_dimensions", {}).get("height"),
thumbnail_url=a.get('thumbnail', {}).get('uri'), is_animated=_type == "MessageAnimatedImage",
preview=a.get('preview') or a.get('preview_image'), thumbnail_url=a.get("thumbnail", {}).get("uri"),
large_preview=a.get('large_preview'), preview=a.get("preview") or a.get("preview_image"),
animated_preview=a.get('animated_image'), large_preview=a.get("large_preview"),
uid=a.get('legacy_attachment_id') animated_preview=a.get("animated_image"),
uid=a.get("legacy_attachment_id"),
) )
elif _type == 'MessageVideo': elif _type == "MessageVideo":
return VideoAttachment( return VideoAttachment(
width=a.get('original_dimensions', {}).get('width'), width=a.get("original_dimensions", {}).get("width"),
height=a.get('original_dimensions', {}).get('height'), height=a.get("original_dimensions", {}).get("height"),
duration=a.get('playable_duration_in_ms'), duration=a.get("playable_duration_in_ms"),
preview_url=a.get('playable_url'), preview_url=a.get("playable_url"),
small_image=a.get('chat_image'), small_image=a.get("chat_image"),
medium_image=a.get('inbox_image'), medium_image=a.get("inbox_image"),
large_image=a.get('large_image'), large_image=a.get("large_image"),
uid=a.get('legacy_attachment_id') uid=a.get("legacy_attachment_id"),
) )
elif _type == 'MessageAudio': elif _type == "MessageAudio":
return AudioAttachment( return AudioAttachment(
filename=a.get('filename'), filename=a.get("filename"),
url=a.get('playable_url'), url=a.get("playable_url"),
duration=a.get('playable_duration_in_ms'), duration=a.get("playable_duration_in_ms"),
audio_type=a.get('audio_type') audio_type=a.get("audio_type"),
) )
elif _type == 'MessageFile': elif _type == "MessageFile":
return FileAttachment( return FileAttachment(
url=a.get('url'), url=a.get("url"),
name=a.get('filename'), name=a.get("filename"),
is_malicious=a.get('is_malicious'), is_malicious=a.get("is_malicious"),
uid=a.get('message_file_fbid') uid=a.get("message_file_fbid"),
) )
else: else:
return Attachment( return Attachment(uid=a.get("legacy_attachment_id"))
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): def graphql_to_poll(a):
rtn = Poll( rtn = Poll(
title=a.get('title') if a.get('title') else a.get("text"), title=a.get("title") if a.get("title") else a.get("text"),
options=[graphql_to_poll_option(m) for m in a.get('options')] options=[graphql_to_poll_option(m) for m in a.get("options")],
) )
rtn.uid = int(a["id"]) rtn.uid = int(a["id"])
rtn.options_count = a.get("total_count") rtn.options_count = a.get("total_count")
return rtn return rtn
def graphql_to_poll_option(a): def graphql_to_poll_option(a):
if a.get('viewer_has_voted') is None: if a.get("viewer_has_voted") is None:
vote = None vote = None
elif isinstance(a['viewer_has_voted'], bool): elif isinstance(a["viewer_has_voted"], bool):
vote = a['viewer_has_voted'] vote = a["viewer_has_voted"]
else: else:
vote = a['viewer_has_voted'] == 'true' vote = a["viewer_has_voted"] == "true"
rtn = PollOption( rtn = PollOption(text=a.get("text"), vote=vote)
text=a.get('text'),
vote=vote
)
rtn.uid = int(a["id"]) 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.voters = (
rtn.votes_count = a.get('voters').get('count') if isinstance(a.get('voters'), dict) else a.get('total_count') [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 return rtn
def graphql_to_plan(a): def graphql_to_plan(a):
if a.get('event_members'): if a.get("event_members"):
rtn = Plan( rtn = Plan(
time=a.get('event_time'), time=a.get("event_time"),
title=a.get('title'), title=a.get("title"),
location=a.get('location_name') location=a.get("location_name"),
) )
if a.get('location_id') != 0: if a.get("location_id") != 0:
rtn.location_id = str(a.get('location_id')) rtn.location_id = str(a.get("location_id"))
rtn.uid = a.get('oid') rtn.uid = a.get("oid")
rtn.author_id = a.get('creator_id') rtn.author_id = a.get("creator_id")
guests = a.get("event_members") guests = a.get("event_members")
rtn.going = [uid for uid in guests if guests[uid] == "GOING"] rtn.going = [uid for uid in guests if guests[uid] == "GOING"]
rtn.declined = [uid for uid in guests if guests[uid] == "DECLINED"] rtn.declined = [uid for uid in guests if guests[uid] == "DECLINED"]
rtn.invited = [uid for uid in guests if guests[uid] == "INVITED"] rtn.invited = [uid for uid in guests if guests[uid] == "INVITED"]
return rtn return rtn
elif a.get('id') is None: elif a.get("id") is None:
rtn = Plan( rtn = Plan(
time=a.get('event_time'), time=a.get("event_time"),
title=a.get('event_title'), title=a.get("event_title"),
location=a.get('event_location_name'), location=a.get("event_location_name"),
location_id=a.get('event_location_id') location_id=a.get("event_location_id"),
) )
rtn.uid = a.get('event_id') rtn.uid = a.get("event_id")
rtn.author_id = a.get('event_creator_id') rtn.author_id = a.get("event_creator_id")
guests = json.loads(a.get('guest_state_list')) guests = json.loads(a.get("guest_state_list"))
else: else:
rtn = Plan( rtn = Plan(
time=a.get('time'), time=a.get("time"),
title=a.get('event_title'), title=a.get("event_title"),
location=a.get('location_name') location=a.get("location_name"),
) )
rtn.uid = a.get('id') rtn.uid = a.get("id")
rtn.author_id = a.get('lightweight_event_creator').get('id') rtn.author_id = a.get("lightweight_event_creator").get("id")
guests = a.get('event_reminder_members').get('edges') 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.going = [
rtn.declined = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "DECLINED"] m.get("node").get("id") for m in guests if m.get("guest_list_state") == "GOING"
rtn.invited = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "INVITED"] ]
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 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): def graphql_to_message(message):
if message.get('message_sender') is None: if message.get("message_sender") is None:
message['message_sender'] = {} message["message_sender"] = {}
if message.get('message') is None: if message.get("message") is None:
message['message'] = {} message["message"] = {}
rtn = Message( rtn = Message(
text=message.get('message').get('text'), 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', [])], mentions=[
emoji_size=get_emojisize_from_tags(message.get('tags_list')), Mention(
sticker=graphql_to_sticker(message.get('sticker')) 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.uid = str(message.get("message_id"))
rtn.author = str(message.get('message_sender').get('id')) rtn.author = str(message.get("message_sender").get("id"))
rtn.timestamp = message.get('timestamp_precise') rtn.timestamp = message.get("timestamp_precise")
if message.get('unread') is not None: rtn.unsent = False
rtn.is_read = not message['unread'] if message.get("unread") is not None:
rtn.reactions = {str(r['user']['id']):MessageReaction(r['reaction']) for r in message.get('message_reactions')} rtn.is_read = not message["unread"]
if message.get('blob_attachments') is not None: rtn.reactions = {
rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']] str(r["user"]["id"]): enum_extend_if_invalid(MessageReaction, r["reaction"])
# TODO: This is still missing parsing: for r in message.get("message_reactions")
# message.get('extensible_attachment') }
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 return rtn
def graphql_to_user(user): def graphql_to_user(user):
if user.get('profile_picture') is None: if user.get("profile_picture") is None:
user['profile_picture'] = {} user["profile_picture"] = {}
c_info = get_customization_info(user) c_info = get_customization_info(user)
plan = None plan = None
if user.get('event_reminders'): if user.get("event_reminders"):
plan = graphql_to_plan(user['event_reminders']['nodes'][0]) if user['event_reminders'].get('nodes') else None plan = (
graphql_to_plan(user["event_reminders"]["nodes"][0])
if user["event_reminders"].get("nodes")
else None
)
return User( return User(
user['id'], user["id"],
url=user.get('url'), url=user.get("url"),
first_name=user.get('first_name'), first_name=user.get("first_name"),
last_name=user.get('last_name'), last_name=user.get("last_name"),
is_friend=user.get('is_viewer_friend'), is_friend=user.get("is_viewer_friend"),
gender=GENDERS.get(user.get('gender')), gender=GENDERS.get(user.get("gender")),
affinity=user.get('affinity'), affinity=user.get("affinity"),
nickname=c_info.get('nickname'), nickname=c_info.get("nickname"),
color=c_info.get('color'), color=c_info.get("color"),
emoji=c_info.get('emoji'), emoji=c_info.get("emoji"),
own_nickname=c_info.get('own_nickname'), own_nickname=c_info.get("own_nickname"),
photo=user['profile_picture'].get('uri'), photo=user["profile_picture"].get("uri"),
name=user.get('name'), name=user.get("name"),
message_count=user.get('messages_count'), message_count=user.get("messages_count"),
plan=plan, 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: if first_name is None:
last_name = None last_name = None
else: 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 plan = None
if thread.get('event_reminders'): if thread.get("event_reminders"):
plan = graphql_to_plan(thread['event_reminders']['nodes'][0]) if thread['event_reminders'].get('nodes') else None plan = (
graphql_to_plan(thread["event_reminders"]["nodes"][0])
if thread["event_reminders"].get("nodes")
else None
)
return User( return User(
user['id'], user["id"],
url=user.get('url'), url=user.get("url"),
name=user.get('name'), name=user.get("name"),
first_name=first_name, first_name=first_name,
last_name=last_name, last_name=last_name,
is_friend=user.get('is_viewer_friend'), is_friend=user.get("is_viewer_friend"),
gender=GENDERS.get(user.get('gender')), gender=GENDERS.get(user.get("gender")),
affinity=user.get('affinity'), affinity=user.get("affinity"),
nickname=c_info.get('nickname'), nickname=c_info.get("nickname"),
color=c_info.get('color'), color=c_info.get("color"),
emoji=c_info.get('emoji'), emoji=c_info.get("emoji"),
own_nickname=c_info.get('own_nickname'), own_nickname=c_info.get("own_nickname"),
photo=user['big_image_src'].get('uri'), photo=user["big_image_src"].get("uri"),
message_count=thread.get('messages_count'), message_count=thread.get("messages_count"),
last_message_timestamp=last_message_timestamp, last_message_timestamp=last_message_timestamp,
plan=plan, plan=plan,
) )
else: 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): def graphql_to_group(group):
if group.get('image') is None: if group.get("image") is None:
group['image'] = {} group["image"] = {}
c_info = get_customization_info(group) c_info = get_customization_info(group)
last_message_timestamp = None last_message_timestamp = None
if 'last_message' in group: if "last_message" in group:
last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise'] last_message_timestamp = group["last_message"]["nodes"][0]["timestamp_precise"]
plan = None plan = None
if group.get('event_reminders'): if group.get("event_reminders"):
plan = graphql_to_plan(group['event_reminders']['nodes'][0]) if group['event_reminders'].get('nodes') else None plan = (
graphql_to_plan(group["event_reminders"]["nodes"][0])
if group["event_reminders"].get("nodes")
else None
)
return Group( return Group(
group['thread_key']['thread_fbid'], group["thread_key"]["thread_fbid"],
participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]), participants=set(
nicknames=c_info.get('nicknames'), [
color=c_info.get('color'), node["messaging_actor"]["id"]
emoji=c_info.get('emoji'), for node in group["all_participants"]["nodes"]
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, nicknames=c_info.get("nicknames"),
join_link = group['joinable_mode'].get('link'), color=c_info.get("color"),
photo=group['image'].get('uri'), emoji=c_info.get("emoji"),
name=group.get('name'), admins=set([node.get("id") for node in group.get("thread_admins")]),
message_count=group.get('messages_count'), 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, last_message_timestamp=last_message_timestamp,
plan=plan, plan=plan,
) )
def graphql_to_page(page): def graphql_to_page(page):
if page.get('profile_picture') is None: if page.get("profile_picture") is None:
page['profile_picture'] = {} page["profile_picture"] = {}
if page.get('city') is None: if page.get("city") is None:
page['city'] = {} page["city"] = {}
plan = None plan = None
if page.get('event_reminders'): if page.get("event_reminders"):
plan = graphql_to_plan(page['event_reminders']['nodes'][0]) if page['event_reminders'].get('nodes') else None plan = (
graphql_to_plan(page["event_reminders"]["nodes"][0])
if page["event_reminders"].get("nodes")
else None
)
return Page( return Page(
page['id'], page["id"],
url=page.get('url'), url=page.get("url"),
city=page.get('city').get('name'), city=page.get("city").get("name"),
category=page.get('category_type'), category=page.get("category_type"),
photo=page['profile_picture'].get('uri'), photo=page["profile_picture"].get("uri"),
name=page.get('name'), name=page.get("name"),
message_count=page.get('messages_count'), message_count=page.get("messages_count"),
plan=plan, plan=plan,
) )
def graphql_queries_to_json(*queries): def graphql_queries_to_json(*queries):
""" """
Queries should be a list of GraphQL objects Queries should be a list of GraphQL objects
""" """
rtn = {} rtn = {}
for i, query in enumerate(queries): for i, query in enumerate(queries):
rtn['q{}'.format(i)] = query.value rtn["q{}".format(i)] = query.value
return json.dumps(rtn) return json.dumps(rtn)
def graphql_response_to_json(content): 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: try:
j = json.loads(content, cls=ConcatJSONDecoder) j = json.loads(content, cls=ConcatJSONDecoder)
except Exception: 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: for x in j:
if 'error_results' in x: if "error_results" in x:
del rtn[-1] del rtn[-1]
continue continue
check_json(x) check_json(x)
[(key, value)] = x.items() [(key, value)] = x.items()
check_json(value) check_json(value)
if 'response' in value: if "response" in value:
rtn[int(key[1:])] = value['response'] rtn[int(key[1:])] = value["response"]
else: else:
rtn[int(key[1:])] = value['data'] rtn[int(key[1:])] = value["data"]
log.debug(rtn) log.debug(rtn)
return rtn return rtn
class GraphQL(object): class GraphQL(object):
def __init__(self, query=None, doc_id=None, params=None): def __init__(self, query=None, doc_id=None, params=None):
if params is None: if params is None:
params = {} params = {}
if query is not None: if query is not None:
self.value = { self.value = {"priority": 0, "q": query, "query_params": params}
'priority': 0,
'q': query,
'query_params': params
}
elif doc_id is not None: elif doc_id is not None:
self.value = { self.value = {"doc_id": doc_id, "query_params": params}
'doc_id': doc_id,
'query_params': params
}
else: else:
raise FBchatUserError('A query or doc_id must be specified') raise FBchatUserError("A query or doc_id must be specified")
FRAGMENT_USER = """ FRAGMENT_USER = """
QueryFragment User: User { QueryFragment User: User {
@@ -476,8 +699,9 @@ class GraphQL(object):
} }
""" """
SEARCH_USER = """ SEARCH_USER = (
Query SearchUser(<search> = '', <limit> = 1) { """
Query SearchUser(<search> = '', <limit> = 10) {
entities_named(<search>) { entities_named(<search>) {
search_results.of_type(user).first(<limit>) as users { search_results.of_type(user).first(<limit>) as users {
nodes { nodes {
@@ -486,10 +710,13 @@ class GraphQL(object):
} }
} }
} }
""" + FRAGMENT_USER """
+ FRAGMENT_USER
)
SEARCH_GROUP = """ SEARCH_GROUP = (
Query SearchGroup(<search> = '', <limit> = 1, <pic_size> = 32) { """
Query SearchGroup(<search> = '', <limit> = 10, <pic_size> = 32) {
viewer() { viewer() {
message_threads.with_thread_name(<search>).last(<limit>) as groups { message_threads.with_thread_name(<search>).last(<limit>) as groups {
nodes { nodes {
@@ -498,10 +725,13 @@ class GraphQL(object):
} }
} }
} }
""" + FRAGMENT_GROUP """
+ FRAGMENT_GROUP
)
SEARCH_PAGE = """ SEARCH_PAGE = (
Query SearchPage(<search> = '', <limit> = 1) { """
Query SearchPage(<search> = '', <limit> = 10) {
entities_named(<search>) { entities_named(<search>) {
search_results.of_type(page).first(<limit>) as pages { search_results.of_type(page).first(<limit>) as pages {
nodes { nodes {
@@ -510,10 +740,13 @@ class GraphQL(object):
} }
} }
} }
""" + FRAGMENT_PAGE """
+ FRAGMENT_PAGE
)
SEARCH_THREAD = """ SEARCH_THREAD = (
Query SearchThread(<search> = '', <limit> = 1) { """
Query SearchThread(<search> = '', <limit> = 10) {
entities_named(<search>) { entities_named(<search>) {
search_results.first(<limit>) as threads { search_results.first(<limit>) as threads {
nodes { nodes {
@@ -525,4 +758,8 @@ class GraphQL(object):
} }
} }
} }
""" + FRAGMENT_USER + FRAGMENT_GROUP + FRAGMENT_PAGE """
+ FRAGMENT_USER
+ FRAGMENT_GROUP
+ FRAGMENT_PAGE
)

View File

@@ -1,583 +1,29 @@
# -*- coding: UTF-8 -*- # -*- 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 from __future__ import unicode_literals
import enum
from ._core import Enum
class FBchatException(Exception): from ._exception import FBchatException, FBchatFacebookError, FBchatUserError
"""Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this""" from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread
from ._user import TypingStatus, User, ActiveStatus
class FBchatFacebookError(FBchatException): from ._group import Group, Room
#: The error code that Facebook returned from ._page import Page
fb_error_code = None from ._message import EmojiSize, MessageReaction, Mention, Message
#: The error message that Facebook returned (In the user's own language) from ._attachment import Attachment, UnsentMessage, ShareAttachment
fb_error_message = None from ._sticker import Sticker
#: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200) from ._location import LocationAttachment, LiveLocationAttachment
request_status_code = None from ._file import FileAttachment, AudioAttachment, ImageAttachment, VideoAttachment
def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None): from ._quick_reply import (
super(FBchatFacebookError, self).__init__(message) QuickReply,
"""Thrown by fbchat when Facebook returns an error""" QuickReplyText,
self.fb_error_code = str(fb_error_code) QuickReplyLocation,
self.fb_error_message = fb_error_message QuickReplyPhoneNumber,
self.request_status_code = request_status_code QuickReplyEmail,
)
class FBchatUserError(FBchatException): from ._poll import Poll, PollOption
"""Thrown by fbchat when wrong values are entered""" from ._plan import Plan
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 = '👎'

View File

@@ -11,13 +11,17 @@ from os.path import basename
import warnings import warnings
import logging import logging
import requests import requests
import aenum
from .models import * from .models import *
try: try:
from urllib.parse import urlencode from urllib.parse import urlencode, parse_qs, urlparse
basestring = (str, bytes) basestring = (str, bytes)
except ImportError: except ImportError:
from urllib import urlencode from urllib import urlencode
from urlparse import parse_qs, urlparse
basestring = basestring basestring = basestring
# Python 2's `input` executes the input, whereas `raw_input` just returns the input # Python 2's `input` executes the input, whereas `raw_input` just returns the input
@@ -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.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 (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 (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 = { LIKES = {
'large': EmojiSize.LARGE, "large": EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM, "medium": EmojiSize.MEDIUM,
'small': EmojiSize.SMALL, "small": EmojiSize.SMALL,
'l': EmojiSize.LARGE, "l": EmojiSize.LARGE,
'm': EmojiSize.MEDIUM, "m": EmojiSize.MEDIUM,
's': EmojiSize.SMALL "s": EmojiSize.SMALL,
} }
GENDERS = { GENDERS = {
# For standard requests # For standard requests
0: 'unknown', 0: "unknown",
1: 'female_singular', 1: "female_singular",
2: 'male_singular', 2: "male_singular",
3: 'female_singular_guess', 3: "female_singular_guess",
4: 'male_singular_guess', 4: "male_singular_guess",
5: 'mixed', 5: "mixed",
6: 'neuter_singular', 6: "neuter_singular",
7: 'unknown_singular', 7: "unknown_singular",
8: 'female_plural', 8: "female_plural",
9: 'male_plural', 9: "male_plural",
10: 'neuter_plural', 10: "neuter_plural",
11: 'unknown_plural', 11: "unknown_plural",
# For graphql requests # For graphql requests
'UNKNOWN': 'unknown', "UNKNOWN": "unknown",
'FEMALE': 'female_singular', "FEMALE": "female_singular",
'MALE': 'male_singular', "MALE": "male_singular",
#'': 'female_singular_guess', # '': 'female_singular_guess',
#'': 'male_singular_guess', # '': 'male_singular_guess',
#'': 'mixed', # '': 'mixed',
'NEUTER': 'neuter_singular', "NEUTER": "neuter_singular",
#'': 'unknown_singular', # '': 'unknown_singular',
#'': 'female_plural', # '': 'female_plural',
#'': 'male_plural', # '': 'male_plural',
#'': 'neuter_plural', # '': 'neuter_plural',
#'': 'unknown_plural', # '': 'unknown_plural',
} }
class ReqUrl(object): class ReqUrl(object):
"""A class containing all urls used by `fbchat`""" """A class containing all urls used by `fbchat`"""
SEARCH = "https://www.facebook.com/ajax/typeahead/search.php" SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
LOGIN = "https://m.facebook.com/login.php?login_attempt=1" LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
SEND = "https://www.facebook.com/messaging/send/" SEND = "https://www.facebook.com/messaging/send/"
@@ -92,8 +97,12 @@ class ReqUrl(object):
UNSEEN_THREADS = "https://www.facebook.com/mercury/unseen_thread_ids/" UNSEEN_THREADS = "https://www.facebook.com/mercury/unseen_thread_ids/"
THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php" THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php"
MOVE_THREAD = "https://www.facebook.com/ajax/mercury/move_thread.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" ARCHIVED_STATUS = (
PINNED_STATUS = "https://www.facebook.com/ajax/mercury/change_pinned_status.php?dpr=1" "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" MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php"
READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php" READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php"
DELIVERED = "https://www.facebook.com/ajax/mercury/delivery_receipts.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_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" 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_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_REACTIONS = (
MUTE_MENTIONS = "https://www.facebook.com/ajax/mercury/change_mentions_mute_thread/?dpr=1" "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" 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" 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" 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" 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" 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 pull_channel = 0
def change_pull_channel(self, channel=None): def change_pull_channel(self, channel=None):
if channel is 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: else:
self.pull_channel = channel self.pull_channel = channel
self.STICKY = "https://{}-edge-chat.facebook.com/pull".format(self.pull_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(): def now():
return int(time()*1000) return int(time() * 1000)
def strip_to_json(text): def strip_to_json(text):
try: try:
return text[text.index('{'):] return text[text.index("{") :]
except ValueError: 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): def get_decoded_r(r):
return get_decoded(r._content) return get_decoded(r._content)
def get_decoded(content): def get_decoded(content):
return content.decode(facebookEncoding) return content.decode(facebookEncoding)
def parse_json(content): def parse_json(content):
return json.loads(content) return json.loads(content)
def get_json(r): def get_json(r):
return json.loads(strip_to_json(get_decoded_r(r))) return json.loads(strip_to_json(get_decoded_r(r)))
def digitToChar(digit): def digitToChar(digit):
if digit < 10: if digit < 10:
return str(digit) return str(digit)
return chr(ord('a') + digit - 10) return chr(ord("a") + digit - 10)
def str_base(number, base): def str_base(number, base):
if number < 0: if number < 0:
return '-' + str_base(-number, base) return "-" + str_base(-number, base)
(d, m) = divmod(number, base) (d, m) = divmod(number, base)
if d > 0: if d > 0:
return str_base(d, base) + digitToChar(m) return str_base(d, base) + digitToChar(m)
return digitToChar(m) return digitToChar(m)
def generateMessageID(client_id=None): def generateMessageID(client_id=None):
k = now() k = now()
l = int(random() * 4294967295) l = int(random() * 4294967295)
return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id) return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id)
def getSignatureID(): def getSignatureID():
return hex(int(random() * 2147483648)) return hex(int(random() * 2147483648))
def generateOfflineThreadingID(): def generateOfflineThreadingID():
ret = now() ret = now()
value = int(random() * 4294967295) value = int(random() * 4294967295)
string = ("0000000000000000000000" + format(value, 'b'))[-22:] string = ("0000000000000000000000" + format(value, "b"))[-22:]
msgs = format(ret, 'b') + string msgs = format(ret, "b") + string
return str(int(msgs, 2)) return str(int(msgs, 2))
def check_json(j): def check_json(j):
if j.get('error') is None: if j.get("error") is None:
return return
if 'errorDescription' in j: if "errorDescription" in j:
# 'errorDescription' is in the users own language! # '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']) raise FBchatFacebookError(
elif 'debug_info' in j['error'] and 'code' in j['error']: "Error #{} when sending request: {}".format(
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']) 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: 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): def check_request(r, as_json=True):
if not r.ok: 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) content = get_decoded_r(r)
if content is None or len(content) == 0: 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: if as_json:
content = strip_to_json(content) content = strip_to_json(content)
try: try:
j = json.loads(content) j = json.loads(content)
except ValueError: except ValueError:
raise FBchatFacebookError('Error while parsing JSON: {!r}'.format(content)) raise FBchatFacebookError("Error while parsing JSON: {!r}".format(content))
check_json(j) check_json(j)
log.debug(j) log.debug(j)
return j return j
else: else:
return content return content
def get_jsmods_require(j, index): 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: try:
return j['jsmods']['require'][0][index][0] return j["jsmods"]["require"][0][index][0]
except (KeyError, IndexError) as e: 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 return None
def get_emojisize_from_tags(tags): def get_emojisize_from_tags(tags):
if tags is None: if tags is None:
return 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: if len(tmp) > 0:
try: try:
return LIKES[tmp[0].split(':')[1]] return LIKES[tmp[0].split(":")[1]]
except (KeyError, IndexError): 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 return None
def require_list(list_): def require_list(list_):
if isinstance(list_, list): if isinstance(list_, list):
return set(list_) return set(list_)
else: else:
return set([list_]) return set([list_])
def mimetype_to_key(mimetype): def mimetype_to_key(mimetype):
if not mimetype: if not mimetype:
return "file_id" return "file_id"
@@ -277,11 +333,13 @@ def get_files_from_urls(file_urls):
r = requests.get(file_url) r = requests.get(file_url)
# We could possibly use r.headers.get('Content-Disposition'), see # We could possibly use r.headers.get('Content-Disposition'), see
# https://stackoverflow.com/a/37060758 # https://stackoverflow.com/a/37060758
files.append(( files.append(
basename(file_url), (
r.content, basename(file_url),
r.headers.get('Content-Type') or guess_type(file_url)[0], r.content,
)) r.headers.get("Content-Type") or guess_type(file_url)[0],
)
)
return files return files
@@ -289,11 +347,31 @@ def get_files_from_urls(file_urls):
def get_files_from_paths(filenames): def get_files_from_paths(filenames):
files = [] files = []
for filename in filenames: for filename in filenames:
files.append(( files.append(
basename(filename), (basename(filename), open(filename, "rb"), guess_type(filename)[0])
open(filename, 'rb'), )
guess_type(filename)[0],
))
yield files yield files
for fn, fp, ft in files: for fn, fp, ft in files:
fp.close() 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
View 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",
]

View File

@@ -1,3 +0,0 @@
requests
beautifulsoup4
enum34; python_version < '3.4'

View File

@@ -1,5 +0,0 @@
#!/bin/bash
set -ex
python -m pytest -m offline --color=yes

View File

@@ -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

View File

@@ -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'

View File

@@ -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']})

View File

@@ -17,15 +17,21 @@ def user(client2):
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def group(pytestconfig): 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.mark.xfail("none")]) @pytest.fixture(
scope="session",
params=["user", "group", pytest.param("none", marks=[pytest.mark.xfail()])],
)
def thread(request, user, group): def thread(request, user, group):
return { return {
"user": user, "user": user,
"group": group, "group": group,
"none": {"id": "0", "type": ThreadType.GROUP} "none": {"id": "0", "type": ThreadType.GROUP},
}[request.param] }[request.param]
@@ -109,14 +115,14 @@ def compare(client, thread):
def message_with_mentions(request, client, client2, group): def message_with_mentions(request, client, client2, group):
text = "Hi there [" text = "Hi there ["
mentions = [] mentions = []
if 'me' in request.param: if "me" in request.param:
mentions.append(Mention(thread_id=client.uid, offset=len(text), length=2)) mentions.append(Mention(thread_id=client.uid, offset=len(text), length=2))
text += "me, " text += "me, "
if 'other' in request.param: if "other" in request.param:
mentions.append(Mention(thread_id=client2.uid, offset=len(text), length=5)) mentions.append(Mention(thread_id=client2.uid, offset=len(text), length=5))
text += "other, " text += "other, "
# Unused, because Facebook don't properly support sending mentions with groups as targets # 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)) mentions.append(Mention(thread_id=group["id"], offset=len(text), length=5))
text += "group, " text += "group, "
text += "nothing]" text += "nothing]"

View File

@@ -19,6 +19,11 @@ def test_fetch_thread_list(client1):
assert len(threads) == 2 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) @pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
def test_fetch_message_emoji(client, emoji, emoji_size): def test_fetch_message_emoji(client, emoji, emoji_size):
mid = client.sendEmoji(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) mid = client.send(message_with_mentions)
message, = client.fetchThreadMessages(limit=1) 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 # The mentions are not ordered by offset
for m in message.mentions: for m in message.mentions:
assert vars(m) in [vars(x) for x in message_with_mentions.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) mid = client.send(message_with_mentions)
message = client.fetchMessageInfo(mid, thread_id=thread["id"]) 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 # The mentions are not ordered by offset
for m in message.mentions: for m in message.mentions:
assert vars(m) in [vars(x) for x in message_with_mentions.mentions] assert vars(m) in [vars(x) for x in message_with_mentions.mentions]

View File

@@ -9,11 +9,17 @@ from utils import random_hex, subset
from time import time from time import time
@pytest.fixture(scope="module", params=[ @pytest.fixture(
Plan(int(time()) + 100, random_hex()), scope="module",
pytest.mark.xfail(Plan(int(time()), random_hex()), raises=FBchatFacebookError), params=[
pytest.mark.xfail(Plan(0, None)), 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): def plan_data(request, client, user, thread, catch_event, compare):
with catch_event("onPlanCreated") as x: with catch_event("onPlanCreated") as x:
client.createPlan(request.param, thread["id"]) client.createPlan(request.param, thread["id"])
@@ -44,15 +50,14 @@ def test_fetch_plan_info(client, catch_event, plan_data):
event, plan = plan_data event, plan = plan_data
fetched_plan = client.fetchPlanInfo(plan.uid) fetched_plan = client.fetchPlanInfo(plan.uid)
assert subset( assert subset(
vars(fetched_plan), vars(fetched_plan), time=plan.time, title=plan.title, author_id=int(client.uid)
time=plan.time,
title=plan.title,
author_id=int(client.uid),
) )
@pytest.mark.parametrize("take_part", [False, True]) @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 event, plan = plan_data
with catch_event("onPlanParticipation") as x: with catch_event("onPlanParticipation") as x:
client.changePlanParticipation(plan, take_part=take_part) client.changePlanParticipation(plan, take_part=take_part)
@@ -88,18 +93,22 @@ def test_on_plan_ended(client, thread, catch_event, compare):
with catch_event("onPlanEnded") as x: with catch_event("onPlanEnded") as x:
client.createPlan(Plan(int(time()) + 120, "Wait for ending")) client.createPlan(Plan(int(time()) + 120, "Wait for ending"))
x.wait(180) 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) # createPlan(self, plan, thread_id=None)
#editPlan(self, plan, new_plan) # editPlan(self, plan, new_plan)
#deletePlan(self, plan) # deletePlan(self, plan)
#changePlanParticipation(self, plan, take_part=True) # 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) # 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) # 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) # 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) # 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) # 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)

View File

@@ -8,26 +8,40 @@ from fbchat.models import Poll, PollOption, ThreadType
from utils import random_hex, subset from utils import random_hex, subset
@pytest.fixture(scope="module", params=[ @pytest.fixture(
Poll(title=random_hex(), options=[]), scope="module",
Poll(title=random_hex(), options=[ params=[
PollOption(random_hex(), vote=True), Poll(title=random_hex(), options=[]),
PollOption(random_hex(), vote=True), Poll(
]), title=random_hex(),
Poll(title=random_hex(), options=[ options=[
PollOption(random_hex(), vote=False), PollOption(random_hex(), vote=True),
PollOption(random_hex(), vote=False), PollOption(random_hex(), vote=True),
]), ],
Poll(title=random_hex(), options=[ ),
PollOption(random_hex(), vote=True), Poll(
PollOption(random_hex(), vote=True), title=random_hex(),
PollOption(random_hex(), vote=False), options=[
PollOption(random_hex(), vote=False), PollOption(random_hex(), vote=False),
PollOption(random_hex()), PollOption(random_hex(), vote=False),
PollOption(random_hex()), ],
]), ),
pytest.mark.xfail(Poll(title=None, options=[]), raises=ValueError), 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): def poll_data(request, client1, group, catch_event):
with catch_event("onPollCreated") as x: with catch_event("onPollCreated") as x:
client1.createPoll(request.param, thread_id=group["id"]) client1.createPoll(request.param, thread_id=group["id"])
@@ -43,11 +57,17 @@ def test_create_poll(client1, group, catch_event, poll_data):
thread_id=group["id"], thread_id=group["id"],
thread_type=ThreadType.GROUP, thread_type=ThreadType.GROUP,
) )
assert subset(vars(event["poll"]), title=poll.title, options_count=len(poll.options)) assert subset(
for recv_option in event["poll"].options: # The recieved options may not be the full list 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)) old_option, = list(filter(lambda o: o.text == recv_option.text, poll.options))
voters = [client1.uid] if old_option.vote else [] 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): def test_fetch_poll_options(client1, group, catch_event, poll_data):
@@ -60,11 +80,15 @@ def test_fetch_poll_options(client1, group, catch_event, poll_data):
@pytest.mark.trylast @pytest.mark.trylast
def test_update_poll_vote(client1, group, catch_event, poll_data): def test_update_poll_vote(client1, group, catch_event, poll_data):
event, poll, options = poll_data event, poll, options = poll_data
new_vote_ids = [o.uid for o in options[0:len(options):2] if not 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] re_vote_ids = [o.uid for o in options[0 : len(options) : 2] if o.vote]
new_options = [random_hex(), random_hex()] new_options = [random_hex(), random_hex()]
with catch_event("onPollVoted") as x: 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( assert subset(
x.res, x.res,
@@ -72,8 +96,12 @@ def test_update_poll_vote(client1, group, catch_event, poll_data):
thread_id=group["id"], thread_id=group["id"],
thread_type=ThreadType.GROUP, 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: for o in new_vote_ids:
assert o in x.res["added_options"] assert o in x.res["added_options"]
assert len(x.res["added_options"]) == len(new_vote_ids) + len(new_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
)

View File

@@ -38,7 +38,12 @@ def test_send_mentions(client, catch_event, compare, message_with_mentions):
mid = client.send(message_with_mentions) mid = client.send(message_with_mentions)
assert compare(x, mid=mid, message=message_with_mentions.text) 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 # The mentions are not ordered by offset
for m in x.res["message_object"].mentions: for m in x.res["message_object"].mentions:
assert vars(m) in [vars(x) for x in message_with_mentions.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): 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" text = "Files sent locally"
with catch_event("onMessage") as x: with catch_event("onMessage") as x:
mid = client.sendLocalFiles( mid = client.sendLocalFiles(
@@ -95,7 +108,10 @@ def test_send_remote_files(client, catch_event, compare):
text = "Files sent from remote" text = "Files sent from remote"
with catch_event("onMessage") as x: with catch_event("onMessage") as x:
mid = client.sendRemoteFiles( 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), 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) 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): def test_wave(client, wave_first):
client.wave(wave_first) client.wave(wave_first)

View File

@@ -9,4 +9,4 @@ def test_catch_event(client2, catch_event):
mid = "test" mid = "test"
with catch_event("onMessage") as x: with catch_event("onMessage") as x:
client2.onMessage(mid=mid) client2.onMessage(mid=mid)
assert x.res['mid'] == mid assert x.res["mid"] == mid

View File

@@ -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) assert compare(x, changed_for=client_all.uid, new_nickname=nickname)
@pytest.mark.parametrize("emoji", [ @pytest.mark.parametrize(
"😀", "emoji",
"😂", [
"😕", "😀",
"😍", "😂",
pytest.mark.xfail("🙃", raises=FBchatFacebookError), "😕",
pytest.mark.xfail("not an emoji", raises=FBchatFacebookError) "😍",
]) 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): def test_change_emoji(client, catch_event, compare, emoji):
with catch_event("onEmojiChange") as x: with catch_event("onEmojiChange") as x:
client.changeThreadEmoji(emoji) 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") url = path.join(path.dirname(__file__), "resources", "image.png")
with catch_event("onImageChange") as x: with catch_event("onImageChange") as x:
image_id = client1.changeGroupImageLocal(url, group["id"]) 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 # 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" url = "https://github.com/carpedm20/fbchat/raw/master/tests/image.png"
with catch_event("onImageChange") as x: with catch_event("onImageChange") as x:
image_id = client1.changeGroupImageRemote(url, group["id"]) 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( @pytest.mark.parametrize(
@@ -101,7 +110,7 @@ def test_change_image_remote(client1, group, catch_event):
[ [
x x
if x in [ThreadColor.MESSENGER_BLUE, ThreadColor.PUMPKIN] if x in [ThreadColor.MESSENGER_BLUE, ThreadColor.PUMPKIN]
else pytest.mark.expensive(x) else pytest.param(x, marks=[pytest.mark.expensive()])
for x in ThreadColor for x in ThreadColor
], ],
) )
@@ -126,7 +135,7 @@ def test_typing_status(client, catch_event, compare, status):
assert compare(x, status=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): def test_change_approval_mode(client1, group, catch_event, require_admin_approval):
with catch_event("onApprovalModeChange") as x: with catch_event("onApprovalModeChange") as x:
client1.changeGroupApprovalMode(require_admin_approval, group["id"]) 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"], thread_id=group["id"],
) )
@pytest.mark.parametrize("mute_time", [0, 10, 100, 1000, -1]) @pytest.mark.parametrize("mute_time", [0, 10, 100, 1000, -1])
def test_mute_thread(client, mute_time): def test_mute_thread(client, mute_time):
assert client.muteThread(mute_time) assert client.muteThread(mute_time)

View File

@@ -23,15 +23,15 @@ EMOJI_LIST = [
("😆", EmojiSize.LARGE), ("😆", EmojiSize.LARGE),
# These fail in `catch_event` because the emoji is made into a sticker # These fail in `catch_event` because the emoji is made into a sticker
# This should be fixed # This should be fixed
pytest.mark.xfail((None, EmojiSize.SMALL)), pytest.param(None, EmojiSize.SMALL, marks=[pytest.mark.xfail()]),
pytest.mark.xfail((None, EmojiSize.MEDIUM)), pytest.param(None, EmojiSize.MEDIUM, marks=[pytest.mark.xfail()]),
pytest.mark.xfail((None, EmojiSize.LARGE)), pytest.param(None, EmojiSize.LARGE, marks=[pytest.mark.xfail()]),
] ]
STICKER_LIST = [ STICKER_LIST = [
Sticker("767334476626295"), Sticker("767334476626295"),
pytest.mark.xfail(Sticker("0"), raises=FBchatFacebookError), pytest.param(Sticker("0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
pytest.mark.xfail(Sticker(None), raises=FBchatFacebookError), pytest.param(Sticker(None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
] ]
TEXT_LIST = [ TEXT_LIST = [
@@ -40,8 +40,8 @@ TEXT_LIST = [
"\\\n\t%?&'\"", "\\\n\t%?&'\"",
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط", "ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
"a" * 20000, # Maximum amount of characters you can send "a" * 20000, # Maximum amount of characters you can send
pytest.mark.xfail("a" * 20001, raises=FBchatFacebookError), pytest.param("a" * 20001, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
pytest.mark.xfail(None, raises=FBchatFacebookError), pytest.param(None, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
] ]
@@ -106,7 +106,7 @@ def load_client(n, cache):
client = Client( client = Client(
load_variable("client{}_email".format(n), cache), load_variable("client{}_email".format(n), cache),
load_variable("client{}_password".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), session_cookies=cache.get("client{}_session".format(n), None),
max_tries=1, max_tries=1,
) )