Compare commits

...

152 Commits

Author SHA1 Message Date
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
Mads Marquart
3443a233f4 Fix pytest "Applying marks directly to parameters" deprecation 2018-12-09 15:02:48 +01:00
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
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
Mads Marquart
b4b8914448 Version up, thanks to @kapi2289 2018-09-27 21:53:12 +02:00
Mads Marquart
2ea2c89b4a Fixed markAsRead and markAsUnread, fixes #336 2018-09-27 21:44:04 +02:00
Mads Marquart
479ca59a6a Merge pull request #341 from kapi2289/read_by
[Feature] New `read_by` attribute of `Message`
2018-09-27 20:56:13 +02:00
Mads Marquart
343f987a78 Merge pull request #340 from kapi2289/fix_fetch_thread_list
[Fix] `fetchThreadList` fix
2018-09-27 20:27:03 +02:00
Kacper Ziubryniewicz
bad9c7a4b9 read_by handling 2018-09-24 20:33:43 +02:00
Kacper Ziubryniewicz
576e0949e0 New read_by attribute in Message 2018-09-24 20:32:04 +02:00
Kacper Ziubryniewicz
d807648d2b fetchThreadList fix 2018-09-24 16:50:15 +02:00
Kacper Ziubryniewicz
0ae213c240 Merge pull request #1 from carpedm20/master
Merge `master`
2018-09-12 17:41:53 +02:00
Mads Marquart
08117e7a54 Fixed examples, see #332
The examples were using generator expressions instead of list comprehensions
2018-09-09 14:24:20 +02:00
Mads Marquart
51c3226070 Merge pull request #326 from kapi2289/merge_rooms
Merge `Room` with `Group`
2018-09-09 14:09:36 +02:00
Mads Marquart
5396d19d7d Merge pull request #327 from kapi2289/fix_active
`markAlive` fix
2018-09-09 14:07:48 +02:00
Kacper Ziubryniewicz
11501e6899 Fix Room model initialization 2018-09-03 15:05:11 +02:00
Kacper Ziubryniewicz
4eb49b9119 Backwards compability for Rooms 2018-08-31 13:25:37 +02:00
Kacper Ziubryniewicz
4c2da22750 markAlive fix 2018-08-30 20:28:32 +02:00
Kacper Ziubryniewicz
753b9cbae2 Merge Room with Group methods 2018-08-30 19:57:47 +02:00
Kacper Ziubryniewicz
2c73cabe22 Merge Room with Group graphql methods 2018-08-30 19:57:12 +02:00
Kacper Ziubryniewicz
d6ca091b7b Merge Room with Group model 2018-08-30 19:56:18 +02:00
Mads Marquart
f0e849e9c0 Version up, thanks to @kapi2289, @gave92, @ThatAlexanderA and @1ttric 2018-08-30 00:08:27 +02:00
Mads Marquart
ddcbd6a790 Merge pull request #318 from kapi2289/master
Bunch of new methods, bunch of fixes, bunch of tests
2018-08-29 23:55:17 +02:00
Mads Marquart
28e3b6285e Made mute methods raise if they errored 2018-08-29 23:51:33 +02:00
Mads Marquart
348db90f7b Fixes for Python 2.7 compatibility 2018-08-29 23:50:35 +02:00
Mads Marquart
0d780b9b80 Added tests for plans 2018-08-29 21:31:28 +02:00
Mads Marquart
8ab718becd Added poll tests 2018-08-29 16:49:33 +02:00
Kacper Ziubryniewicz
1943c357fa Message searching rebuild
Changed message searching methods to return generators and added `search`
2018-08-29 15:14:26 +02:00
Mads Marquart
3be0d8389b Changed changeThreadImageX to changeGroupImageX 2018-08-29 14:37:29 +02:00
Kacper Ziubryniewicz
d7d1c83276 MessageReactionFix is not needed anymore 2018-08-29 14:33:48 +02:00
Mads Marquart
8591e2ffd5 Fixed createGroup implementation 2018-08-29 14:08:11 +02:00
Mads Marquart
c2225bf2fd Added more tests 2018-08-29 14:07:44 +02:00
Mads Marquart
0617d7b49f Fixed _usersApproval, fixed changeThreadImage methods, more tests 2018-08-29 12:17:16 +02:00
Mads Marquart
42b288ee98 Fixed onAdminRemoved and onAdminAdded, and added tests for that 2018-08-29 11:15:59 +02:00
Mads Marquart
ead7203e40 Added tests for fetchMessageInfo 2018-08-29 11:03:46 +02:00
Mads Marquart
bd2b947255 More test improvements 2018-08-29 10:14:18 +02:00
Mads Marquart
f367bd2d0d Improved test setup 2018-08-29 10:12:10 +02:00
Kacper Ziubryniewicz
a8ce44b109 Added searching for messages in all threads 2018-08-27 19:37:49 +02:00
Kacper Ziubryniewicz
3b43d3f0bd Few fixes 2018-08-27 14:08:19 +02:00
Kacper Ziubryniewicz
06da486140 Backwards compability for plans/event reminders 2018-08-24 21:56:31 +02:00
Kacper Ziubryniewicz
a24a7d5636 Small documentation fix 2018-08-23 21:10:47 +02:00
Mads Marquart
bc197fd665 Changed sendXFiles to only needing file url / path 2018-08-23 20:38:55 +02:00
Kacper Ziubryniewicz
e35cc71cf4 Fix plan fetching from threads 2018-08-23 12:17:22 +02:00
Kacper Ziubryniewicz
7aa774b4ef Update utils.py 2018-08-20 23:12:36 +02:00
Kacper Ziubryniewicz
9bb2de79fa Update client.py 2018-08-20 23:12:10 +02:00
Kacper Ziubryniewicz
21246144ab Update client.py 2018-08-20 17:09:18 +02:00
Kacper Ziubryniewicz
0e0845914b Update graphql.py 2018-08-20 16:57:37 +02:00
Kacper Ziubryniewicz
778e827277 Update models.py 2018-08-20 16:57:10 +02:00
Kacper Ziubryniewicz
f36d4fa38d client - Event to Plan 2018-08-19 15:28:22 +02:00
Kacper Ziubryniewicz
5b89c2d504 utils - Event to Plan 2018-08-19 15:25:02 +02:00
Kacper Ziubryniewicz
49b213bb2d graphql - Event to Plan 2018-08-19 15:24:28 +02:00
Kacper Ziubryniewicz
aed75c7d1b Changed Event model to Plan 2018-08-19 15:23:44 +02:00
Mads Marquart
ac51e4e4d5 Removed trailing whitespace 2018-08-13 21:28:17 +02:00
Kacper Ziubryniewicz
d8d84ae629 Fix event_reminders for pages 2018-08-11 14:29:31 +02:00
Kacper Ziubryniewicz
3f75f8ed31 Added markAsSpam 2018-08-10 12:03:14 +02:00
Kacper Ziubryniewicz
8aef4dc2ec Added mark as spam request 2018-08-10 12:02:47 +02:00
Kacper Ziubryniewicz
b1e7ec706b Fix event_reminders 2018-08-10 10:03:51 +02:00
Kacper Ziubryniewicz
b5cd780360 Added message searching 2018-08-10 09:09:17 +02:00
Kacper Ziubryniewicz
a8da94ee6d Added request for message searching 2018-08-10 09:08:34 +02:00
Kacper Ziubryniewicz
f564c732d4 Added event reminder methods 2018-08-09 20:05:59 +02:00
Kacper Ziubryniewicz
8beb1e5753 Update graphql.py 2018-08-09 20:04:20 +02:00
Kacper Ziubryniewicz
d98d802a33 New Event model 2018-08-09 20:02:45 +02:00
Kacper Ziubryniewicz
d750f29fad New event reminder requests 2018-08-09 20:01:52 +02:00
Kacper Ziubryniewicz
f425d32846 Added poll methods 2018-08-05 22:15:42 +02:00
Kacper Ziubryniewicz
043d6b492d Fix in new graphql methods 2018-08-05 22:09:03 +02:00
Kacper Ziubryniewicz
0bcccfa65e Added graphql_to_poll and graphql_to_poll_option 2018-08-05 22:01:43 +02:00
Kacper Ziubryniewicz
0716b1b8d8 Added requests for poll events 2018-08-05 21:58:48 +02:00
Kacper Ziubryniewicz
47168e682d Added Poll and PollOption models 2018-08-05 21:56:32 +02:00
Kacper Ziubryniewicz
718d864dc8 Added file, video and audio sending 2018-08-04 00:43:36 +02:00
Kacper Ziubryniewicz
22a691ec0f Fix waveToThread 2018-08-03 21:55:06 +02:00
Kacper Ziubryniewicz
dfcc826b7e Added waveToThread and markAsUnread 2018-08-02 23:31:35 +02:00
Kacper Ziubryniewicz
d1ee664ef5 Added deleteMesseges request url 2018-08-01 22:55:42 +02:00
Kacper Ziubryniewicz
abcc6518bb Added deleteMessages method 2018-08-01 22:53:48 +02:00
Kacper Ziubryniewicz
2ef9ec3358 Added call events
Added onCallStarted, onCallEnded and onUserJoinedCall but this methods are for group calls only. I can't find how to fetch private call start, I found only how to fetch private call end.
2018-07-31 23:16:45 +02:00
Kacper Ziubryniewicz
f84cf3bf2d Added fetchMessageInfo by mid and thread_id
Added fetchMessageInfo and fixed onImageChange when removing thread image
2018-07-31 20:12:24 +02:00
Kacper Ziubryniewicz
bdcc2d2fa4 Added acceptUsersToGroup and denyUsersFromGroup 2018-07-31 13:23:35 +02:00
Kacper Ziubryniewicz
7e8e7f15a4 Update client.py 2018-07-31 12:09:03 +02:00
Kacper Ziubryniewicz
1ca3ad6237 Forgot about thread_type in new methods. Added it! 2018-07-31 11:56:43 +02:00
Kacper Ziubryniewicz
f3c878d949 Update client.py 2018-07-31 11:48:25 +02:00
Kacper Ziubryniewicz
ee0c30ebb1 Update utils.py 2018-07-31 11:33:20 +02:00
Kacper Ziubryniewicz
c2f0c908d9 Added thread muting 2018-07-31 11:30:41 +02:00
kapi2289
3edaaa0400 Added deleteThreads
Added deleteThreads and made few fixes
2018-07-31 10:40:10 +02:00
kapi2289
21a443baf2 Update client.py 2018-07-31 00:03:19 +02:00
kapi2289
f6f47b5500 Merge branch 'master' into master 2018-07-29 15:20:12 +02:00
Mads Marquart
920c724656 Merge pull request #317 from gave92/master
Fix 2FA for non-English users
2018-07-28 19:34:39 +02:00
Mads Marquart
e50b814e07 Merge pull request #316 from ThatAlexanderA/patch-1
Added `createGroup`
2018-07-28 19:32:55 +02:00
kapi2289
2294082168 Documentation fix #2 2018-07-20 15:24:18 +02:00
kapi2289
2661a28936 Multiple admins adding/removing
Changed
addGroupAdmin, removeGroupAdmin
to
addGroupAdmins, removeGroupAdmins
2018-07-20 12:42:18 +02:00
kapi2289
31a6834b1f Documentation fix 2018-07-20 12:01:05 +02:00
kapi2289
f66d98bcfe Wrong change #2 2018-07-20 11:56:39 +02:00
kapi2289
ed7466621f Wrong change 2018-07-20 11:51:03 +02:00
kapi2289
ead450aeb8 Update utils.py 2018-07-19 17:38:04 +02:00
kapi2289
d934cefa8b New methods and few fixes
Added: addGroupAdmin, removeGroupAdmin, changeGroupApprovalMode, blockUser, unblockUser, moveThread, onImageChange, onAdminsAdded, onAdminsRemoved, onApprovalModeChange
I did this all day, because I love this library and I want to be part of it :D
2018-07-19 17:36:54 +02:00
kapi2289
41807837b8 Small typo fix 2018-07-16 21:46:58 +02:00
Marco Gavelli
4419c816f5 Fix 2FA for non english FB 2018-07-15 12:37:20 +02:00
ThatAlexanderA
4993da727a Added create group url 2018-07-14 12:42:18 +02:00
ThatAlexanderA
86a163e337 Added create group def 2018-07-14 12:40:42 +02:00
Mads Marquart
c2fb602bee Disabled travis pytest caching, now the tests should be pretty stable 2018-07-12 17:42:34 +02:00
Mads Marquart
f565d6f31a Merge pull request #311 from kapi2289/master
Fixed changeThreadTitle and added changeThreadImage
2018-07-12 16:47:23 +02:00
kapi2289
5af01bb8ff Added documentation 2018-07-08 14:37:44 +02:00
kapi2289
714e783e0d Update client.py 2018-07-07 22:39:02 +02:00
Mads Marquart
fb1b0afddb Merge pull request #306 from carpedm20/improve_community_profile
Improve community profile
2018-07-07 15:36:43 +02:00
kapi2289
e6fdc56d25 Update utils.py 2018-07-03 23:14:48 +02:00
kapi2289
5b965e63f8 Update client.py 2018-07-03 23:13:47 +02:00
Mads Marquart
af86550e71 Merge pull request #307 from 1ttric/master
Fix: Name edge case results in IndexError
2018-07-02 14:07:11 +02:00
Will Vesey
e57ae069a7 Fix name edge case 2018-06-27 13:54:45 -04:00
Mads Marquart
39adc646e6 Revert adding FBchatRedirectError 2018-06-27 11:14:55 +02:00
Mads Marquart
0947e77082 Fixed FBchatRedirectError 2018-06-27 11:07:16 +02:00
Mads Marquart
637b0ded09 Added FBchatRedirectError 2018-06-27 10:45:11 +02:00
Mads Marquart
9b7a84ea45 Added more debug info, to fix a wierd bug 2018-06-26 10:40:01 +02:00
Mads Marquart
ead696cbad Attempted to improve TravisCI online tests 2018-06-24 12:20:17 +02:00
Mads Marquart
da23ad5eb5 Merge branch 'test_travis_config' 2018-06-21 21:42:54 +02:00
Mads Marquart
b63a0dfa01 Made the offline tests colorful ;) 2018-06-21 21:38:52 +02:00
Mads Marquart
6c00724a84 Removed unnecessary env 2018-06-21 21:30:58 +02:00
Mads Marquart
7619224809 Removed travis_fold test 2018-06-21 21:29:11 +02:00
Mads Marquart
e0d3dd9050 New TravisCI setup, using build stages 2018-06-21 21:13:17 +02:00
Mads Marquart
71bf5e0e4f Added CONTRIBUTING.rst 2018-06-21 17:12:01 +02:00
Mads Marquart
540e530420 Added Contributor Covenant Code of Conduct 2018-06-21 17:11:46 +02:00
Mads Marquart
070a8cad15 Removed wrong templates 2018-06-21 16:52:21 +02:00
Mads Marquart
5d094b38b0 Merge pull request #305 from carpedm20/issue_templates
Add issue templates via. Github's `Create Issue Template` feature
2018-06-21 16:42:25 +02:00
Mads Marquart
af3d385ff5 Add issue templates via. Github's Create Issue Template feature 2018-06-21 16:41:27 +02:00
Mads Marquart
c352a0d698 Modified license, so it's correctly recognised by licensee
It _should_ be okay, since the modified version is less permissive
The only real addition is `Neither the name of the copyright holder nor`
2018-06-21 15:36:54 +02:00
Mads Marquart
060f64b4d2 Rename LICENSE.txt to LICENSE 2018-06-21 15:29:52 +02:00
Mads Marquart
4f032cd946 Fixed a few exception values, see #303 2018-06-21 15:23:43 +02:00
Mads Marquart
cee6039ec3 Prevent builds from failing the deploy [ci skip]
Every job runs the build stage, which is fine, since we need the different `wheel` packages, but they failed, since the files were already present on PyPI
2018-06-20 16:54:03 +02:00
Mads Marquart
c8f8b818e0 Version up, thanks to @orenyomtov and @ ThatAlexanderA
* Added `removeFriend` method, #298
* Removed `lxml` from dependencies, #301
* Moved configuration to setup.cfg instead of setup.py
2018-06-20 15:53:57 +02:00
Mads Marquart
08922ae284 Moved Travis account configuration into Travis Settings 2018-06-20 14:29:43 +02:00
Mads Marquart
51d606a54e Merge pull request #298 from ThatAlexanderA/master
Added remove friend
2018-06-20 14:29:00 +02:00
Mads Marquart
2b76d71c67 Merge branch 'master' into alexander_master 2018-06-20 13:51:32 +02:00
Mads Marquart
67edd19eb8 Small formatting fixes 2018-06-20 13:51:12 +02:00
Mads Marquart
eaaa526cfc Merge pull request #301 from orenyomtov/patch-4
Replace lxml with Python's built in html.parser
2018-06-20 13:46:56 +02:00
Mads Marquart
843c0f6c37 Merge branch 'master' into patch-4 2018-06-20 13:38:59 +02:00
Mads Marquart
44ebf38e47 Updated setup.py and requirements, now we use setup.cfg 2018-06-20 13:35:56 +02:00
Mads Marquart
d640e7d2ea Enabled pypy and pytest session caching, updated README 2018-06-19 13:49:10 +02:00
Oren
66736519ed Remove lxml dependency 2018-06-14 16:20:57 +03:00
Oren
73f4c98be9 Remove lxml dependency 2018-06-14 16:20:35 +03:00
Oren
b2ff7fefaa Replace lxml with Python's built in html.parser 2018-06-14 16:19:09 +03:00
ThatAlexanderA
c7cbbdd1c8 Changed dict to query, replaced print with log 2018-06-05 21:56:31 +02:00
ThatAlexanderA
b599033c54 Updated removeFirend 2018-06-05 18:41:09 +02:00
ThatAlexanderA
91778f43b7 Update client.py 2018-06-04 16:19:40 +02:00
ThatAlexanderA
e3602e83ce Added Remove Friend URl 2018-06-04 16:18:32 +02:00
ThatAlexanderA
36742bf30b Added remove friend def 2018-06-04 16:16:53 +02:00
ThatAlexanderA
c842be3a52 Update client.py 2018-06-04 13:32:15 +02:00
ThatAlexanderA
a264fac2b4 Update utils.py 2018-06-04 13:29:23 +02:00
42 changed files with 2295 additions and 485 deletions

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report if you're having trouble with `fbchat`
---
## Description of the problem
Example: Logging in fails when the character `%` is in the password. A specific password that fails is `a_password_with_%`
## Code to reproduce
```py
# Example code
from fbchat import Client
client = Client("[REDACTED_USERNAME]", "a_password_with_%")
```
## Traceback
```
Traceback (most recent call last):
File "<test.py>", line 1, in <module>
File "[site-packages]/fbchat/client.py", line 78, in __init__
self.login(email, password, max_tries)
File "[site-packages]/fbchat/client.py", line 407, in login
raise FBchatUserError('Login failed. Check email/password. (Failed on url: {})'.format(login_url))
fbchat.models.FBchatUserError: Login failed. Check email/password. (Failed on url: https://m.facebook.com/login.php?login_attempt=1)
```
## Environment information
- Python version
- `fbchat` version
- If relevant, output from `$ python -m pip list`
If you have done any research, include that.
Make sure to redact all personal information.

View File

@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest a feature that you'd like to see implemented
---
## Description
Example: There's no way to send messages to groups
## Research (if applicable)
Example: I've found the URL `https://facebook.com/send_message.php`, to which you can send a POST requests with the following JSON:
```json
{
"text": message_content,
"fbid": group_id,
"some_variable": ?
}
```
But I don't know how what `some_variable` does, and it doesn't work without it. I've found some examples of `some_variable` to be: `MTIzNDU2Nzg5MA`, `MTIzNDU2Nzg5MQ` and `MTIzNDU2Nzg5Mg`

2
.gitignore vendored
View File

@@ -8,9 +8,11 @@
# Packages # Packages
*.egg *.egg
*.egg-info *.egg-info
*.dist-info
dist dist
build build
eggs eggs
.eggs
parts parts
bin bin
var var

View File

@@ -1,35 +1,90 @@
sudo: false sudo: false
language: python language: python
python: conditions: v1
- 2.7
- 3.4
- 3.5
- 3.6
env: # There are two accounts made specifically for Travis, and the passwords are really only encrypted for obscurity
global: # The global env variables `client1_email`, `client1_password`, `client2_email`, `client2_password` and `group_id`
# These two accounts are made specifically for this purpose, and the passwords are really only encrypted for obscurity # are set on the Travis Settings page
# In reality, you could probably still login with the accounts by looking at log output from travis-ci.org
- client1_email=travis.fbchat1@gmail.com # Facebook ID: 100023782141139
- secure: "W1NON6qaLnvYIOVoC93MXkmbAIkUkHcGREBwN0BSVM3cLuMduk4VVkz6PY2T8bnntGYVwicXwcn5aNJ6pDue17TBZqGPk/tdpws8mnAZUtBYhpkIFTTlyh5kJSZejx9fd5s4nceGpH6ofCCnNxPp2PdHKU8piqnQYZVQ4cFNNDE=" # client1_password
- client2_email=fbchat.travis2@gmail.com # Facebook ID: 100026538491708
- secure: "V7RB3go2Tc/DdW1x9DkMI+vCfnOgiS3ygmFCABs/GjfPZjZL7VLMJgYGlx0cjeeeN+Oxa2GrhczRAKeMdGB6Ss2lGGAVs6cjJ56ODuBHWT6/FNzLjtDkTnjD+Kfh0l8ZOdxTF3MQ6M/9hU6z5ek+XYGr7u+/7wOYZ5L2cK5MaQ0=" # client2_password
- group_id=1463789480385605
before_script: # The tests are run with `Limit concurrent jobs = 1`, since the tests can't use the clients simultaneously
- if [[ "$TRAVIS_PYTHON_VERSION" = "2.7" ]]; then export PYTEST_ADDOPTS='-m ""'; fi; # expensive tests (otherwise disabled in pytest.ini)
- if [[ "$TRAVIS_PULL_REQUEST" != false ]]; then export PYTEST_ADDOPTS='-m offline'; fi; # offline tests only
script: python -m pytest || python -m pytest --lf; # Run failed tests twice install:
- pip install -U -r requirements.txt
- pip install -U -r dev-requirements.txt
cache: pip 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
deploy: jobs:
provider: pypi include:
user: madsmtm # The tests are split into online and offline versions.
password: # The online tests are only run against the master branch.
secure: "VA0MLSrwIW/T2KjMwjLZCzrLHw8pJT6tAvb48t7qpBdm8x192hax61pz1TaBZoJvlzyBPFKvluftuclTc7yEFwzXe7Gjqgd/ODKZl/wXDr36hQ7BBOLPZujdwmWLvTzMh3eJZlvkgcLCzrvK3j2oW8cM/+FZeVi/5/FhVuJ4ofs=" # Because:
on: # Travis caching is per-branch and per-job, so even though we cache the Facebook sessions via. `.pytest_cache`
python: 3.6 # and in `tests.utils.load_client`, we need 6 new sessions per branch. This is usually the point where Facebook
branch: master # starts complaining, and we have to manually fix it
tags: true
- &test-online
if: (branch = master OR tag IS present) AND type != pull_request
stage: online tests
script: scripts/travis-online
# Run online tests in all the supported python versions
python: 2.7
- <<: *test-online
python: 3.4
- <<: *test-online
python: 3.5
- <<: *test-online
python: 3.6
- <<: *test-online
python: pypy
# Run the expensive tests, with the python version most likely to break, aka. 2
- <<: *test-online
# Only run if the commit message includes [ci all] or [all ci]
if: commit_message =~ /\[ci\s+all\]|\[all\s+ci\]/
python: 2.7
env: PYTEST_ADDOPTS='-m expensive'
- &test-offline
# Ideally, it'd be nice to run the offline tests in every build, but since we can't run jobs concurrently (yet),
# we'll disable them when they're not needed, and include them inside the online tests instead
if: not ((branch = master OR tag IS present) AND type != pull_request)
stage: offline tests
script: scripts/travis-offline
# Run offline tests in all the supported python versions
python: 2.7
- <<: *test-offline
python: 3.4
- <<: *test-offline
python: 3.5
- <<: *test-offline
python: 3.6
- <<: *test-offline
python: 3.6
- <<: *test-offline
python: pypy
# Deploy to PyPI
- &deploy
stage: deploy
if: branch = master AND tag IS present
install: skip
deploy:
provider: pypi
user: madsmtm
password:
secure: "VA0MLSrwIW/T2KjMwjLZCzrLHw8pJT6tAvb48t7qpBdm8x192hax61pz1TaBZoJvlzyBPFKvluftuclTc7yEFwzXe7Gjqgd/ODKZl/wXDr36hQ7BBOLPZujdwmWLvTzMh3eJZlvkgcLCzrvK3j2oW8cM/+FZeVi/5/FhVuJ4ofs="
distributions: sdist bdist_wheel
skip_existing: true
# We need the bdist_wheels from both Python 2 and 3
python: 3.6
- <<: *deploy
python: 2.7

75
CODE_OF_CONDUCT Normal file
View File

@@ -0,0 +1,75 @@
Contributor Covenant Code of Conduct
Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at carpedm20@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the projects leadership.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

22
CONTRIBUTING.rst Normal file
View File

@@ -0,0 +1,22 @@
Contributing to fbchat
======================
Thanks for reading this, all contributions are very much welcome!
Please be aware that ``fbchat`` uses `Scemantic Versioning <https://semver.org/>`__
That means that if you're submitting a breaking change, it will probably take a while before it gets considered.
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``.
Testing Environment
-------------------
The tests use `pytest <https://docs.pytest.org/>`__, and to work they need two Facebook accounts, and a group thread between these.
To set these up, you should export the following environment variables:
``client1_email``, ``client1_password``, ``client2_email``, ``client2_password`` and ``group_id``
If you're not able to do this, consider simply running ``pytest -m offline``.
And if you're adding new functionality, if possible, make sure to create a new test for it.

View File

@@ -1,4 +1,4 @@
New BSD License BSD 3-Clause License
Copyright (c) 2015, Taehoon Kim Copyright (c) 2015, Taehoon Kim
All rights reserved. All rights reserved.
@@ -13,8 +13,9 @@ modification, are permitted provided that the following conditions are met:
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
* The names of its contributors may not be used to endorse or promote products * Neither the name of the copyright holder nor the names of its
derived from this software without specific prior written permission. contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

View File

@@ -1,4 +1,3 @@
include LICENSE.txt include LICENSE
include MANIFEST.in include CONTRIBUTING.rst
include README.rst include README.rst
include setup.py

View File

@@ -2,24 +2,28 @@ fbchat: Facebook Chat (Messenger) for Python
============================================ ============================================
.. image:: https://img.shields.io/badge/license-BSD-blue.svg .. image:: https://img.shields.io/badge/license-BSD-blue.svg
:target: LICENSE.txt :target: https://github.com/carpedm20/fbchat/tree/master/LICENSE
:alt: License: BSD :alt: License: BSD 3-Clause
.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg .. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6%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 and 3.6 :alt: Supported python versions: 2.7, 3.4, 3.5, 3.6 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
:alt: Documentation :alt: Documentation
.. image:: https://travis-ci.org/carpedm20/fbchat.svg?branch=master
:target: https://travis-ci.org/carpedm20/fbchat
:alt: Travis CI
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>`__.
**No XMPP or API key is needed**. Just use your email and password. **No XMPP or API key is needed**. Just use your email and password.
Go to `Read the Docs <https://fbchat.readthedocs.io>`__ to see the full documentation, Go to `Read the Docs <https://fbchat.readthedocs.io>`__ to see the full documentation,
or jump right into the code by viewing the `examples <examples>`__ or jump right into the code by viewing the `examples <https://github.com/carpedm20/fbchat/tree/master/examples>`__
Installation: Installation:
@@ -27,6 +31,15 @@ Installation:
$ pip install fbchat $ pip install fbchat
You can also install from source, by using `setuptools` (You need at least version 30.3.0):
.. code-block:: console
$ git clone https://github.com/carpedm20/fbchat.git
$ cd fbchat
$ python setup.py install
Maintainer Maintainer
---------- ----------

2
dev-requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
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

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

@@ -8,8 +8,8 @@ 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()
print("users' IDs: {}".format(user.uid for user in users)) print("users' IDs: {}".format([user.uid for user in users]))
print("users' names: {}".format(user.name for user in users)) print("users' names: {}".format([user.name for user in users]))
# If we have a user id, we can use `fetchUserInfo` to fetch a `User` object # If we have a user id, we can use `fetchUserInfo` to fetch a `User` object
@@ -18,7 +18,7 @@ user = client.fetchUserInfo('<user id>')['<user id>']
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]))
# `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,

View File

@@ -1,28 +1,28 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from .client import *
""" """
fbchat fbchat
~~~~~~ ~~~~~~
Facebook Chat (Messenger) for Python Facebook Chat (Messenger) for Python
:copyright: (c) 2015 by Taehoon Kim. :copyright: (c) 2015 - 2018 by Taehoon Kim
:license: BSD, see LICENSE for more details. :license: BSD 3-Clause, see LICENSE for more details.
""" """
from __future__ import unicode_literals
from .client import *
__title__ = 'fbchat'
__version__ = '1.4.2'
__description__ = 'Facebook Chat (Messenger) for Python'
__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim'
__license__ = 'BSD 3-Clause'
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__version__ = '1.3.8'
__license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com' __email__ = 'carpedm20@gmail.com'
__source__ = 'https://github.com/carpedm20/fbchat/'
__description__ = 'Facebook Chat (Messenger) for Python'
__all__ = [ __all__ = [
'Client', 'Client',

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,7 @@ def get_customization_info(thread):
'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') in ('GROUP', 'ROOM') or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'): if thread.get('thread_type') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'):
rtn['nicknames'] = {} rtn['nicknames'] = {}
for k in info.get('participant_customizations', []): for k in info.get('participant_customizations', []):
rtn['nicknames'][k['participant_id']] = k.get('nickname') rtn['nicknames'][k['participant_id']] = k.get('nickname')
@@ -128,6 +128,71 @@ def graphql_to_attachment(a):
uid=a.get('legacy_attachment_id') uid=a.get('legacy_attachment_id')
) )
def graphql_to_poll(a):
rtn = Poll(
title=a.get('title') if a.get('title') else a.get("text"),
options=[graphql_to_poll_option(m) for m in a.get('options')]
)
rtn.uid = int(a["id"])
rtn.options_count = a.get("total_count")
return rtn
def graphql_to_poll_option(a):
if a.get('viewer_has_voted') is None:
vote = None
elif isinstance(a['viewer_has_voted'], bool):
vote = a['viewer_has_voted']
else:
vote = a['viewer_has_voted'] == 'true'
rtn = PollOption(
text=a.get('text'),
vote=vote
)
rtn.uid = int(a["id"])
rtn.voters = [m.get('node').get('id') for m in a.get('voters').get('edges')] if isinstance(a.get('voters'), dict) else a.get('voters')
rtn.votes_count = a.get('voters').get('count') if isinstance(a.get('voters'), dict) else a.get('total_count')
return rtn
def graphql_to_plan(a):
if a.get('event_members'):
rtn = Plan(
time=a.get('event_time'),
title=a.get('title'),
location=a.get('location_name')
)
if a.get('location_id') != 0:
rtn.location_id = str(a.get('location_id'))
rtn.uid = a.get('oid')
rtn.author_id = a.get('creator_id')
guests = a.get("event_members")
rtn.going = [uid for uid in guests if guests[uid] == "GOING"]
rtn.declined = [uid for uid in guests if guests[uid] == "DECLINED"]
rtn.invited = [uid for uid in guests if guests[uid] == "INVITED"]
return rtn
elif a.get('id') is None:
rtn = Plan(
time=a.get('event_time'),
title=a.get('event_title'),
location=a.get('event_location_name'),
location_id=a.get('event_location_id')
)
rtn.uid = a.get('event_id')
rtn.author_id = a.get('event_creator_id')
guests = json.loads(a.get('guest_state_list'))
else:
rtn = Plan(
time=a.get('time'),
title=a.get('event_title'),
location=a.get('location_name')
)
rtn.uid = a.get('id')
rtn.author_id = a.get('lightweight_event_creator').get('id')
guests = a.get('event_reminder_members').get('edges')
rtn.going = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "GOING"]
rtn.declined = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "DECLINED"]
rtn.invited = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "INVITED"]
return rtn
def graphql_to_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'] = {}
@@ -155,6 +220,9 @@ 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
if user.get('event_reminders'):
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'),
@@ -169,7 +237,8 @@ def graphql_to_user(user):
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,
) )
def graphql_to_thread(thread): def graphql_to_thread(thread):
@@ -185,12 +254,22 @@ def graphql_to_thread(thread):
if 'last_message' in thread: if 'last_message' in thread:
last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise'] last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise']
first_name = user.get('short_name')
if first_name is None:
last_name = None
else:
last_name = user.get('name').split(first_name, 1).pop().strip()
plan = None
if thread.get('event_reminders'):
plan = graphql_to_plan(thread['event_reminders']['nodes'][0]) if thread['event_reminders'].get('nodes') else None
return User( return User(
user['id'], user['id'],
url=user.get('url'), url=user.get('url'),
name=user.get('name'), name=user.get('name'),
first_name=user.get('short_name'), first_name=first_name,
last_name=user.get('name').split(user.get('short_name'),1).pop().strip(), 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'),
@@ -200,7 +279,8 @@ def graphql_to_thread(thread):
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,
) )
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))
@@ -212,36 +292,24 @@ def graphql_to_group(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
if group.get('event_reminders'):
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([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]),
nicknames=c_info.get('nicknames'), nicknames=c_info.get('nicknames'),
color=c_info.get('color'), color=c_info.get('color'),
emoji=c_info.get('emoji'), emoji=c_info.get('emoji'),
admins = set([node.get('id') for node in group.get('thread_admins')]),
approval_mode = bool(group.get('approval_mode')) if group.get('approval_mode') is not None else None,
approval_requests = set(node["requester"]['id'] for node in group['group_approval_queue']['nodes']) if group.get('group_approval_queue') else None,
join_link = group['joinable_mode'].get('link'),
photo=group['image'].get('uri'), photo=group['image'].get('uri'),
name=group.get('name'), name=group.get('name'),
message_count=group.get('messages_count'), message_count=group.get('messages_count'),
last_message_timestamp=last_message_timestamp last_message_timestamp=last_message_timestamp,
) plan=plan,
def graphql_to_room(room):
if room.get('image') is None:
room['image'] = {}
c_info = get_customization_info(room)
return Room(
room['thread_key']['thread_fbid'],
participants=set([node['messaging_actor']['id'] for node in room['all_participants']['nodes']]),
nicknames=c_info.get('nicknames'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
photo=room['image'].get('uri'),
name=room.get('name'),
message_count=room.get('messages_count'),
admins = set([node.get('id') for node in room.get('thread_admins')]),
approval_mode = bool(room.get('approval_mode')),
approval_requests = set(node.get('id') for node in room['thread_queue_metadata'].get('approval_requests', {}).get('nodes')),
join_link = room['joinable_mode'].get('link'),
privacy_mode = bool(room.get('privacy_mode')),
) )
def graphql_to_page(page): def graphql_to_page(page):
@@ -249,6 +317,9 @@ def graphql_to_page(page):
page['profile_picture'] = {} page['profile_picture'] = {}
if page.get('city') is None: if page.get('city') is None:
page['city'] = {} page['city'] = {}
plan = None
if page.get('event_reminders'):
plan = graphql_to_plan(page['event_reminders']['nodes'][0]) if page['event_reminders'].get('nodes') else None
return Page( return Page(
page['id'], page['id'],
url=page.get('url'), url=page.get('url'),
@@ -256,7 +327,8 @@ def graphql_to_page(page):
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,
) )
def graphql_queries_to_json(*queries): def graphql_queries_to_json(*queries):
@@ -351,6 +423,40 @@ class GraphQL(object):
}, },
outgoing_bubble_color, outgoing_bubble_color,
emoji emoji
},
thread_admins {
id
},
group_approval_queue {
nodes {
requester {
id
}
}
},
approval_mode,
joinable_mode {
mode,
link
},
event_reminders {
nodes {
id,
lightweight_event_creator {
id
},
time,
location_name,
event_title,
event_reminder_members {
edges {
node {
id
},
guest_list_state
}
}
}
} }
} }
""" """

View File

@@ -37,7 +37,9 @@ class Thread(object):
last_message_timestamp = None last_message_timestamp = None
#: Number of messages in the thread #: Number of messages in the thread
message_count = None message_count = None
def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, 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""" """Represents a Facebook thread"""
self.uid = str(uid) self.uid = str(uid)
self.type = _type self.type = _type
@@ -45,6 +47,7 @@ class Thread(object):
self.name = name self.name = name
self.last_message_timestamp = last_message_timestamp self.last_message_timestamp = last_message_timestamp
self.message_count = message_count self.message_count = message_count
self.plan = plan
def __repr__(self): def __repr__(self):
return self.__unicode__() return self.__unicode__()
@@ -99,8 +102,16 @@ class Group(Thread):
color = None color = None
#: The groups's default emoji #: The groups's default emoji
emoji = None 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, **kwargs): 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`""" """Represents a Facebook group. Inherits `Thread`"""
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs) super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
if participants is None: if participants is None:
@@ -111,24 +122,6 @@ class Group(Thread):
self.nicknames = nicknames self.nicknames = nicknames
self.color = color self.color = color
self.emoji = emoji self.emoji = emoji
class Room(Group):
# 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 room
join_link = None
# True is room is not discoverable
privacy_mode = None
def __init__(self, uid, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs):
"""Represents a Facebook room. Inherits `Group`"""
super(Room, self).__init__(uid, **kwargs)
self.type = ThreadType.ROOM
if admins is None: if admins is None:
admins = set() admins = set()
self.admins = admins self.admins = admins
@@ -137,6 +130,16 @@ class Room(Group):
approval_requests = set() approval_requests = set()
self.approval_requests = approval_requests self.approval_requests = approval_requests
self.join_link = join_link 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 self.privacy_mode = privacy_mode
@@ -177,6 +180,8 @@ class Message(object):
timestamp = None timestamp = None
#: Whether the message is read #: Whether the message is read
is_read = None 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 #: A dict with user's IDs as keys, and their :class:`MessageReaction` as values
reactions = None reactions = None
#: The actual message #: The actual message
@@ -198,6 +203,7 @@ class Message(object):
attachments = [] attachments = []
self.attachments = attachments self.attachments = attachments
self.reactions = {} self.reactions = {}
self.read_by = []
def __repr__(self): def __repr__(self):
return self.__unicode__() return self.__unicode__()
@@ -436,6 +442,87 @@ class Mention(object):
def __unicode__(self): def __unicode__(self):
return '<Mention {}: offset={} length={}>'.format(self.thread_id, self.offset, self.length) 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): class Enum(enum.Enum):
"""Used internally by fbchat to support enumerations""" """Used internally by fbchat to support enumerations"""
def __repr__(self): def __repr__(self):
@@ -446,8 +533,8 @@ class ThreadType(Enum):
"""Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info""" """Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info"""
USER = 1 USER = 1
GROUP = 2 GROUP = 2
ROOM = 2
PAGE = 3 PAGE = 3
ROOM = 4
class ThreadLocation(Enum): class ThreadLocation(Enum):
"""Used to specify where a thread is located (inbox, pending, archived, other).""" """Used to specify where a thread is located (inbox, pending, archived, other)."""

View File

@@ -5,8 +5,12 @@ import re
import json import json
from time import time from time import time
from random import random from random import random
from contextlib import contextmanager
from mimetypes import guess_type
from os.path import basename
import warnings import warnings
import logging import logging
import requests
from .models import * from .models import *
try: try:
@@ -48,16 +52,6 @@ LIKES = {
's': EmojiSize.SMALL 's': EmojiSize.SMALL
} }
MessageReactionFix = {
'😍': ('0001f60d', '%F0%9F%98%8D'),
'😆': ('0001f606', '%F0%9F%98%86'),
'😮': ('0001f62e', '%F0%9F%98%AE'),
'😢': ('0001f622', '%F0%9F%98%A2'),
'😠': ('0001f620', '%F0%9F%98%A0'),
'👍': ('0001f44d', '%F0%9F%91%8D'),
'👎': ('0001f44e', '%F0%9F%91%8E')
}
GENDERS = { GENDERS = {
# For standard requests # For standard requests
@@ -97,6 +91,9 @@ class ReqUrl(object):
UNREAD_THREADS = "https://www.facebook.com/ajax/mercury/unread_threads.php" UNREAD_THREADS = "https://www.facebook.com/ajax/mercury/unread_threads.php"
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"
ARCHIVED_STATUS = "https://www.facebook.com/ajax/mercury/change_archived_status.php?dpr=1"
PINNED_STATUS = "https://www.facebook.com/ajax/mercury/change_pinned_status.php?dpr=1"
MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php" 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"
@@ -116,12 +113,33 @@ class ReqUrl(object):
THREAD_COLOR = "https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1" THREAD_COLOR = "https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1"
THREAD_NICKNAME = "https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1" THREAD_NICKNAME = "https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1"
THREAD_EMOJI = "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&dpr=1" THREAD_EMOJI = "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&dpr=1"
THREAD_IMAGE = "https://www.facebook.com/messaging/set_thread_image/?dpr=1"
THREAD_NAME = "https://www.facebook.com/messaging/set_thread_name/?dpr=1"
MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation" MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation"
TYPING = "https://www.facebook.com/ajax/messaging/typ.php" TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
GRAPHQL = "https://www.facebook.com/api/graphqlbatch/" GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
ATTACHMENT_PHOTO = "https://www.facebook.com/mercury/attachments/photo/" ATTACHMENT_PHOTO = "https://www.facebook.com/mercury/attachments/photo/"
EVENT_REMINDER = "https://www.facebook.com/ajax/eventreminder/create" PLAN_CREATE = "https://www.facebook.com/ajax/eventreminder/create"
PLAN_INFO = "https://www.facebook.com/ajax/eventreminder"
PLAN_CHANGE = "https://www.facebook.com/ajax/eventreminder/submit"
PLAN_PARTICIPATION = "https://www.facebook.com/ajax/eventreminder/rsvp"
MODERN_SETTINGS_MENU = "https://www.facebook.com/bluebar/modern_settings_menu/" MODERN_SETTINGS_MENU = "https://www.facebook.com/bluebar/modern_settings_menu/"
REMOVE_FRIEND = "https://m.facebook.com/a/removefriend.php"
BLOCK_USER = "https://www.facebook.com/messaging/block_messages/?dpr=1"
UNBLOCK_USER = "https://www.facebook.com/messaging/unblock_messages/?dpr=1"
SAVE_ADMINS = "https://www.facebook.com/messaging/save_admins/?dpr=1"
APPROVAL_MODE = "https://www.facebook.com/messaging/set_approval_mode/?dpr=1"
CREATE_GROUP = "https://m.facebook.com/messages/send/?icm=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"
MUTE_THREAD = "https://www.facebook.com/ajax/mercury/change_mute_thread.php?dpr=1"
MUTE_REACTIONS = "https://www.facebook.com/ajax/mercury/change_reactions_mute_thread/?dpr=1"
MUTE_MENTIONS = "https://www.facebook.com/ajax/mercury/change_mentions_mute_thread/?dpr=1"
CREATE_POLL = "https://www.facebook.com/messaging/group_polling/create_poll/?dpr=1"
UPDATE_VOTE = "https://www.facebook.com/messaging/group_polling/update_vote/?dpr=1"
GET_POLL_OPTIONS = "https://www.facebook.com/ajax/mercury/get_poll_options"
SEARCH_MESSAGES = "https://www.facebook.com/ajax/mercury/search_snippets.php?dpr=1"
MARK_SPAM = "https://www.facebook.com/ajax/mercury/mark_spam.php?dpr=1"
pull_channel = 0 pull_channel = 0
@@ -143,7 +161,7 @@ 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: {}, {}'.format(repr(text), text.index('{'))) 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)
@@ -210,8 +228,9 @@ def check_request(r, as_json=True):
try: try:
j = json.loads(content) j = json.loads(content)
except ValueError: except ValueError:
raise FBchatFacebookError('Error while parsing JSON: {}'.format(repr(content))) raise FBchatFacebookError('Error while parsing JSON: {!r}'.format(content))
check_json(j) check_json(j)
log.debug(j)
return j return j
else: else:
return content return content
@@ -234,3 +253,47 @@ def get_emojisize_from_tags(tags):
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_):
if isinstance(list_, list):
return set(list_)
else:
return set([list_])
def mimetype_to_key(mimetype):
if not mimetype:
return "file_id"
if mimetype == "image/gif":
return "gif_id"
x = mimetype.split("/")
if x[0] in ["video", "image", "audio"]:
return "%s_id" % x[0]
return "file_id"
def get_files_from_urls(file_urls):
files = []
for file_url in file_urls:
r = requests.get(file_url)
# We could possibly use r.headers.get('Content-Disposition'), see
# https://stackoverflow.com/a/37060758
files.append((
basename(file_url),
r.content,
r.headers.get('Content-Type') or guess_type(file_url)[0],
))
return files
@contextmanager
def get_files_from_paths(filenames):
files = []
for filename in filenames:
files.append((
basename(filename),
open(filename, 'rb'),
guess_type(filename)[0],
))
yield files
for fn, fp, ft in files:
fp.close()

View File

@@ -1,5 +1,3 @@
requests requests
lxml
beautifulsoup4 beautifulsoup4
enum34; python_version < '3.4' enum34; python_version < '3.4'
six

5
scripts/travis-offline Executable file
View File

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

18
scripts/travis-online Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -ex
if ! python -m pytest --color=yes; then
echo << EOF
-----------------------------------------------------------------
-----------------------------------------------------------------
-----------------------------------------------------------------
Some tests failed! Rerunning them, since they can be kinda flaky.
-----------------------------------------------------------------
-----------------------------------------------------------------
-----------------------------------------------------------------
EOF
python -m pytest --last-failed --color=yes
fi

51
setup.cfg Normal file
View File

@@ -0,0 +1,51 @@
[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'

82
setup.py Normal file → Executable file
View File

@@ -1,82 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
""" from setuptools import setup
Setup script for fbchat
"""
import os
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
with open('README.rst') as f: setup(extras_require={':python_version < "3.4"': ['enum34']})
readme_content = f.read().strip()
requirements = [
'requests',
'lxml',
'beautifulsoup4'
]
extras_requirements = {
':python_version < "3.4"': ['enum34']
}
version = None
author = None
email = None
source = None
description = None
with open(os.path.join('fbchat', '__init__.py')) as f:
for line in f:
if line.strip().startswith('__version__'):
version = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif line.strip().startswith('__author__'):
author = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif line.strip().startswith('__email__'):
email = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif line.strip().startswith('__source__'):
source = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif line.strip().startswith('__description__'):
description = line.split('=')[1].strip().replace('"', '').replace("'", '')
elif None not in (version, author, email, source, description):
break
setup(
name='fbchat',
author=author,
author_email=email,
license='BSD License',
keywords=["facebook chat fbchat"],
description=description,
long_description=readme_content,
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Communications :: Chat',
],
include_package_data=True,
packages=['fbchat'],
install_requires=requirements,
extras_require=extras_requirements,
url=source,
version=version,
zip_safe=True,
)

View File

@@ -7,7 +7,7 @@ import json
from utils import * from utils import *
from contextlib import contextmanager from contextlib import contextmanager
from fbchat.models import ThreadType from fbchat.models import ThreadType, Message, Mention
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@@ -20,9 +20,15 @@ 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.fixture(scope="session", params=[
"user", "group", pytest.param("none", marks=[pytest.mark.xfail()])
])
def thread(request, user, group): def thread(request, user, group):
return user if request.param == "user" else group return {
"user": user,
"group": group,
"none": {"id": "0", "type": ThreadType.GROUP}
}[request.param]
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@@ -37,7 +43,7 @@ def client2(pytestconfig):
yield c yield c
@pytest.fixture # (scope="session") @pytest.fixture(scope="module")
def client(client1, thread): def client(client1, thread):
client1.setDefaultThread(thread["id"], thread["type"]) client1.setDefaultThread(thread["id"], thread["type"])
yield client1 yield client1
@@ -80,12 +86,12 @@ def catch_event(client2):
try: try:
# Make the client send a messages to itself, so the blocking pull request will return # Make the client send a messages to itself, so the blocking pull request will return
# This is probably not safe, since the client is making two requests simultaneously # This is probably not safe, since the client is making two requests simultaneously
client2.sendMessage("Shutdown", client2.uid) client2.sendMessage(random_hex(), client2.uid)
finally: finally:
t.join() t.join()
@pytest.fixture # (scope="session") @pytest.fixture(scope="module")
def compare(client, thread): def compare(client, thread):
def inner(caught_event, **kwargs): def inner(caught_event, **kwargs):
d = { d = {
@@ -99,3 +105,21 @@ def compare(client, thread):
return subset(caught_event.res, **d) return subset(caught_event.res, **d)
return inner return inner
@pytest.fixture(params=["me", "other", "me other"])
def message_with_mentions(request, client, client2, group):
text = "Hi there ["
mentions = []
if 'me' in request.param:
mentions.append(Mention(thread_id=client.uid, offset=len(text), length=2))
text += "me, "
if 'other' in request.param:
mentions.append(Mention(thread_id=client2.uid, offset=len(text), length=5))
text += "other, "
# Unused, because Facebook don't properly support sending mentions with groups as targets
if 'group' in request.param:
mentions.append(Mention(thread_id=group["id"], offset=len(text), length=5))
text += "group, "
text += "nothing]"
return Message(text, mentions=mentions)

View File

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

BIN
tests/resources/audio.mp3 Normal file

Binary file not shown.

View File

@@ -0,0 +1,4 @@
{
"some": "data",
"in": "here"
}

1
tests/resources/file.txt Normal file
View File

@@ -0,0 +1 @@
This is just a text file

BIN
tests/resources/image.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
tests/resources/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
tests/resources/video.mp4 Normal file

Binary file not shown.

View File

@@ -6,32 +6,20 @@ import pytest
from os import path from os import path
from fbchat.models import ThreadType, Message, Mention, EmojiSize, Sticker from fbchat.models import ThreadType, Message, Mention, EmojiSize, Sticker
from utils import subset from utils import subset, STICKER_LIST, EMOJI_LIST
def test_fetch_all_users(client): def test_fetch_all_users(client1):
users = client.fetchAllUsers() users = client1.fetchAllUsers()
assert len(users) > 0 assert len(users) > 0
def test_fetch_thread_list(client): def test_fetch_thread_list(client1):
threads = client.fetchThreadList(limit=2) threads = client1.fetchThreadList(limit=2)
assert len(threads) == 2 assert len(threads) == 2
@pytest.mark.parametrize( @pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
"emoji, emoji_size",
[
("😆", EmojiSize.SMALL),
("😆", EmojiSize.MEDIUM),
("😆", EmojiSize.LARGE),
# These fail because the emoji is made into a sticker
# This should be fixed
pytest.mark.xfail((None, EmojiSize.SMALL)),
pytest.mark.xfail((None, EmojiSize.MEDIUM)),
pytest.mark.xfail((None, EmojiSize.LARGE)),
],
)
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)
message, = client.fetchThreadMessages(limit=1) message, = client.fetchThreadMessages(limit=1)
@@ -41,25 +29,52 @@ def test_fetch_message_emoji(client, emoji, emoji_size):
) )
def test_fetch_message_mentions(client): @pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
text = "This is a test of fetchThreadMessages" def test_fetch_message_info_emoji(client, thread, emoji, emoji_size):
mentions = [Mention(client.uid, offset=10, length=4)] mid = client.sendEmoji(emoji, emoji_size)
message = client.fetchMessageInfo(mid, thread_id=thread["id"])
mid = client.send(Message(text, mentions=mentions)) assert subset(
vars(message), uid=mid, author=client.uid, text=emoji, emoji_size=emoji_size
)
def test_fetch_message_mentions(client, thread, 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=text) assert subset(vars(message), uid=mid, author=client.uid, text=message_with_mentions.text)
for i, m in enumerate(mentions): # The mentions are not ordered by offset
assert vars(message.mentions[i]) == vars(m) for m in message.mentions:
assert vars(m) in [vars(x) for x in message_with_mentions.mentions]
@pytest.mark.parametrize("sticker_id", ["767334476626295"]) def test_fetch_message_info_mentions(client, thread, message_with_mentions):
def test_fetch_message_sticker(client, sticker_id): mid = client.send(message_with_mentions)
mid = client.send(Message(sticker=Sticker(sticker_id))) message = client.fetchMessageInfo(mid, thread_id=thread["id"])
assert subset(vars(message), uid=mid, author=client.uid, text=message_with_mentions.text)
# The mentions are not ordered by offset
for m in message.mentions:
assert vars(m) in [vars(x) for x in message_with_mentions.mentions]
@pytest.mark.parametrize("sticker", STICKER_LIST)
def test_fetch_message_sticker(client, sticker):
mid = client.send(Message(sticker=sticker))
message, = client.fetchThreadMessages(limit=1) message, = client.fetchThreadMessages(limit=1)
assert subset(vars(message), uid=mid, author=client.uid) assert subset(vars(message), uid=mid, author=client.uid)
assert subset(vars(message.sticker), uid=sticker_id) assert subset(vars(message.sticker), uid=sticker.uid)
@pytest.mark.parametrize("sticker", STICKER_LIST)
def test_fetch_message_info_sticker(client, thread, sticker):
mid = client.send(Message(sticker=sticker))
message = client.fetchMessageInfo(mid, thread_id=thread["id"])
assert subset(vars(message), uid=mid, author=client.uid)
assert subset(vars(message.sticker), uid=sticker.uid)
def test_fetch_info(client1, group): def test_fetch_info(client1, group):
@@ -71,9 +86,7 @@ def test_fetch_info(client1, group):
def test_fetch_image_url(client): def test_fetch_image_url(client):
url = path.join(path.dirname(__file__), "image.png") client.sendLocalFiles([path.join(path.dirname(__file__), "resources", "image.png")])
client.sendLocalImage(url)
message, = client.fetchThreadMessages(limit=1) message, = client.fetchThreadMessages(limit=1)
assert client.fetchImageUrl(message.attachments[0].uid) assert client.fetchImageUrl(message.attachments[0].uid)

View File

@@ -5,8 +5,19 @@ from __future__ import unicode_literals
import pytest import pytest
from fbchat.models import Message, MessageReaction from fbchat.models import Message, MessageReaction
from utils import subset
def test_set_reaction(client): def test_set_reaction(client):
mid = client.send(Message(text="This message will be reacted to")) mid = client.send(Message(text="This message will be reacted to"))
client.reactToMessage(mid, MessageReaction.LOVE) client.reactToMessage(mid, MessageReaction.LOVE)
def test_delete_messages(client):
text1 = "This message will stay"
text2 = "This message will be removed"
mid1 = client.sendMessage(text1)
mid2 = client.sendMessage(text2)
client.deleteMessages(mid2)
message, = client.fetchThreadMessages(limit=1)
assert subset(vars(message), uid=mid1, author=client.uid, text=text1)

111
tests/test_plans.py Normal file
View File

@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from fbchat.models import Plan, FBchatFacebookError, ThreadType
from utils import random_hex, subset
from time import time
@pytest.fixture(scope="module", params=[
Plan(int(time()) + 100, random_hex()),
pytest.param(
Plan(int(time()), random_hex()),
marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
),
pytest.param(
Plan(0, None),
marks=[pytest.mark.xfail()],
),
])
def plan_data(request, client, user, thread, catch_event, compare):
with catch_event("onPlanCreated") as x:
client.createPlan(request.param, thread["id"])
assert compare(x)
assert subset(
vars(x.res["plan"]),
time=request.param.time,
title=request.param.title,
author_id=client.uid,
going=[client.uid],
declined=[],
)
plan_id = x.res["plan"]
assert user["id"] in x.res["plan"].invited
request.param.uid = x.res["plan"].uid
yield x.res, request.param
with catch_event("onPlanDeleted") as x:
client.deletePlan(plan_id)
assert compare(x)
@pytest.mark.tryfirst
def test_create_delete_plan(plan_data):
pass
def test_fetch_plan_info(client, catch_event, plan_data):
event, plan = plan_data
fetched_plan = client.fetchPlanInfo(plan.uid)
assert subset(
vars(fetched_plan),
time=plan.time,
title=plan.title,
author_id=int(client.uid),
)
@pytest.mark.parametrize("take_part", [False, True])
def test_change_plan_participation(client, thread, catch_event, compare, plan_data, take_part):
event, plan = plan_data
with catch_event("onPlanParticipation") as x:
client.changePlanParticipation(plan, take_part=take_part)
assert compare(x, take_part=take_part)
assert subset(
vars(x.res["plan"]),
time=plan.time,
title=plan.title,
author_id=client.uid,
going=[client.uid] if take_part else [],
declined=[client.uid] if not take_part else [],
)
@pytest.mark.trylast
def test_edit_plan(client, thread, catch_event, compare, plan_data):
event, plan = plan_data
new_plan = Plan(plan.time + 100, random_hex())
with catch_event("onPlanEdited") as x:
client.editPlan(plan, new_plan)
assert compare(x)
assert subset(
vars(x.res["plan"]),
time=new_plan.time,
title=new_plan.title,
author_id=client.uid,
)
@pytest.mark.trylast
@pytest.mark.expensive
def test_on_plan_ended(client, thread, catch_event, compare):
with catch_event("onPlanEnded") as x:
client.createPlan(Plan(int(time()) + 120, "Wait for ending"))
x.wait(180)
assert subset(x.res, thread_id=client.uid if thread["type"] == ThreadType.USER else thread["id"], thread_type=thread["type"])
#createPlan(self, plan, thread_id=None)
#editPlan(self, plan, new_plan)
#deletePlan(self, plan)
#changePlanParticipation(self, plan, take_part=True)
#onPlanCreated(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
#onPlanEnded(self, mid=None, plan=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
#onPlanEdited(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
#onPlanDeleted(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
#onPlanParticipation(self, mid=None, plan=None, take_part=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None)
#fetchPlanInfo(self, plan_id)

81
tests/test_polls.py Normal file
View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from fbchat.models import Poll, PollOption, ThreadType
from utils import random_hex, subset
@pytest.fixture(scope="module", params=[
Poll(title=random_hex(), options=[]),
Poll(title=random_hex(), options=[
PollOption(random_hex(), vote=True),
PollOption(random_hex(), vote=True),
]),
Poll(title=random_hex(), options=[
PollOption(random_hex(), vote=False),
PollOption(random_hex(), vote=False),
]),
Poll(title=random_hex(), options=[
PollOption(random_hex(), vote=True),
PollOption(random_hex(), vote=True),
PollOption(random_hex(), vote=False),
PollOption(random_hex(), vote=False),
PollOption(random_hex()),
PollOption(random_hex()),
]),
pytest.param(
Poll(title=None, options=[]), marks=[pytest.mark.xfail(raises=ValueError)]
),
])
def poll_data(request, client1, group, catch_event):
with catch_event("onPollCreated") as x:
client1.createPoll(request.param, thread_id=group["id"])
options = client1.fetchPollOptions(x.res["poll"].uid)
return x.res, request.param, options
def test_create_poll(client1, group, catch_event, poll_data):
event, poll, _ = poll_data
assert subset(
event,
author_id=client1.uid,
thread_id=group["id"],
thread_type=ThreadType.GROUP,
)
assert subset(vars(event["poll"]), title=poll.title, options_count=len(poll.options))
for recv_option in event["poll"].options: # The recieved options may not be the full list
old_option, = list(filter(lambda o: o.text == recv_option.text, poll.options))
voters = [client1.uid] if old_option.vote else []
assert subset(vars(recv_option), voters=voters, votes_count=len(voters), vote=False)
def test_fetch_poll_options(client1, group, catch_event, poll_data):
_, poll, options = poll_data
assert len(options) == len(poll.options)
for option in options:
assert subset(vars(option))
@pytest.mark.trylast
def test_update_poll_vote(client1, group, catch_event, poll_data):
event, poll, options = poll_data
new_vote_ids = [o.uid for o in options[0:len(options):2] if not o.vote]
re_vote_ids = [o.uid for o in options[0:len(options):2] if o.vote]
new_options = [random_hex(), random_hex()]
with catch_event("onPollVoted") as x:
client1.updatePollVote(event["poll"].uid, option_ids=new_vote_ids + re_vote_ids, new_options=new_options)
assert subset(
x.res,
author_id=client1.uid,
thread_id=group["id"],
thread_type=ThreadType.GROUP,
)
assert subset(vars(x.res["poll"]), title=poll.title, options_count=len(options + new_options))
for o in new_vote_ids:
assert o in x.res["added_options"]
assert len(x.res["added_options"]) == len(new_vote_ids) + len(new_options)
assert set(x.res["removed_options"]) == set(o.uid for o in options if o.vote and o.uid not in re_vote_ids)

View File

@@ -5,20 +5,11 @@ from __future__ import unicode_literals
import pytest import pytest
from os import path from os import path
from fbchat.models import Message, Mention, EmojiSize, FBchatFacebookError, Sticker from fbchat.models import FBchatFacebookError, Message, Mention
from utils import subset from utils import subset, STICKER_LIST, EMOJI_LIST, TEXT_LIST
@pytest.mark.parametrize( @pytest.mark.parametrize("text", TEXT_LIST)
"text",
[
"test_send",
"😆",
"\\\n\t%?&'\"",
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
"a" * 20000, # Maximum amount of characters you can send
],
)
def test_send_text(client, catch_event, compare, text): def test_send_text(client, catch_event, compare, text):
with catch_event("onMessage") as x: with catch_event("onMessage") as x:
mid = client.sendMessage(text) mid = client.sendMessage(text)
@@ -27,19 +18,7 @@ def test_send_text(client, catch_event, compare, text):
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text) assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
@pytest.mark.parametrize( @pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
"emoji, emoji_size",
[
("😆", EmojiSize.SMALL),
("😆", EmojiSize.MEDIUM),
("😆", EmojiSize.LARGE),
# These fail because the emoji is made into a sticker
# This should be fixed
pytest.mark.xfail((None, EmojiSize.SMALL)),
pytest.mark.xfail((None, EmojiSize.MEDIUM)),
pytest.mark.xfail((None, EmojiSize.LARGE)),
],
)
def test_send_emoji(client, catch_event, compare, emoji, emoji_size): def test_send_emoji(client, catch_event, compare, emoji, emoji_size):
with catch_event("onMessage") as x: with catch_event("onMessage") as x:
mid = client.sendEmoji(emoji, emoji_size) mid = client.sendEmoji(emoji, emoji_size)
@@ -54,42 +33,28 @@ def test_send_emoji(client, catch_event, compare, emoji, emoji_size):
) )
@pytest.mark.xfail(raises=FBchatFacebookError) def test_send_mentions(client, catch_event, compare, message_with_mentions):
@pytest.mark.parametrize("message", [Message("a" * 20001)])
def test_send_invalid(client, message):
client.send(message)
def test_send_mentions(client, client2, thread, catch_event, compare):
text = "Hi there @me, @other and @thread"
mentions = [
dict(thread_id=client.uid, offset=9, length=3),
dict(thread_id=client2.uid, offset=14, length=6),
dict(thread_id=thread["id"], offset=26, length=7),
]
with catch_event("onMessage") as x: with catch_event("onMessage") as x:
mid = client.send(Message(text, mentions=[Mention(**d) for d in mentions])) mid = client.send(message_with_mentions)
assert compare(x, mid=mid, message=text) assert compare(x, mid=mid, message=message_with_mentions.text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=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 mentions assert vars(m) in [vars(x) for x in message_with_mentions.mentions]
@pytest.mark.parametrize( @pytest.mark.parametrize("sticker", STICKER_LIST)
"sticker_id", def test_send_sticker(client, catch_event, compare, sticker):
["767334476626295", pytest.mark.xfail("0", raises=FBchatFacebookError)],
)
def test_send_sticker(client, catch_event, compare, sticker_id):
with catch_event("onMessage") as x: with catch_event("onMessage") as x:
mid = client.send(Message(sticker=Sticker(sticker_id))) mid = client.send(Message(sticker=sticker))
assert compare(x, mid=mid) assert compare(x, mid=mid)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid) assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid)
assert subset(vars(x.res["message_object"].sticker), uid=sticker_id) assert subset(vars(x.res["message_object"].sticker), uid=sticker.uid)
# Kept for backwards compatibility
@pytest.mark.parametrize( @pytest.mark.parametrize(
"method_name, url", "method_name, url",
[ [
@@ -97,7 +62,7 @@ def test_send_sticker(client, catch_event, compare, sticker_id):
"sendRemoteImage", "sendRemoteImage",
"https://github.com/carpedm20/fbchat/raw/master/tests/image.png", "https://github.com/carpedm20/fbchat/raw/master/tests/image.png",
), ),
("sendLocalImage", path.join(path.dirname(__file__), "image.png")), ("sendLocalImage", path.join(path.dirname(__file__), "resources", "image.png")),
], ],
) )
def test_send_images(client, catch_event, compare, method_name, url): def test_send_images(client, catch_event, compare, method_name, url):
@@ -108,3 +73,37 @@ def test_send_images(client, catch_event, compare, method_name, url):
assert compare(x, mid=mid, message=text) assert compare(x, mid=mid, message=text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text) assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
assert x.res["message_object"].attachments[0] assert x.res["message_object"].attachments[0]
def test_send_local_files(client, catch_event, compare):
files = ["image.png", "image.jpg", "image.gif", "file.json", "file.txt", "audio.mp3", "video.mp4"]
text = "Files sent locally"
with catch_event("onMessage") as x:
mid = client.sendLocalFiles(
[path.join(path.dirname(__file__), "resources", f) for f in files],
message=Message(text),
)
assert compare(x, mid=mid, message=text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
assert len(x.res["message_object"].attachments) == len(files)
# To be changed when merged into master
def test_send_remote_files(client, catch_event, compare):
files = ["image.png", "data.json"]
text = "Files sent from remote"
with catch_event("onMessage") as x:
mid = client.sendRemoteFiles(
["https://github.com/carpedm20/fbchat/raw/master/tests/{}".format(f) for f in files],
message=Message(text),
)
assert compare(x, mid=mid, message=text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
assert len(x.res["message_object"].attachments) == len(files)
@pytest.mark.parametrize('wave_first', [True, False])
def test_wave(client, wave_first):
client.wave(wave_first)

View File

@@ -12,7 +12,7 @@ from fbchat.models import (
ThreadColor, ThreadColor,
) )
from utils import random_hex, subset from utils import random_hex, subset
from os import environ from os import path
def test_remove_from_and_add_to_group(client1, client2, group, catch_event): def test_remove_from_and_add_to_group(client1, client2, group, catch_event):
@@ -31,16 +31,28 @@ def test_remove_from_and_add_to_group(client1, client2, group, catch_event):
) )
@pytest.mark.xfail( def test_remove_from_and_add_admins_to_group(client1, client2, group, catch_event):
raises=FBchatFacebookError, reason="Apparently changeThreadTitle is broken" # Test both methods, while ensuring that the user gets added as group admin
) try:
def test_change_title(client1, catch_event, group): with catch_event("onAdminRemoved") as x:
client1.removeGroupAdmins(client2.uid, group["id"])
assert subset(
x.res, removed_id=client2.uid, author_id=client1.uid, thread_id=group["id"]
)
finally:
with catch_event("onAdminAdded") as x:
client1.addGroupAdmins(client2.uid, group["id"])
assert subset(
x.res, added_id=client2.uid, author_id=client1.uid, thread_id=group["id"]
)
def test_change_title(client1, group, catch_event):
title = random_hex() title = random_hex()
with catch_event("onTitleChange") as x: with catch_event("onTitleChange") as x:
mid = client1.changeThreadTitle(title, group["id"]) client1.changeThreadTitle(title, group["id"], thread_type=ThreadType.GROUP)
assert subset( assert subset(
x.res, x.res,
mid=mid,
author_id=client1.uid, author_id=client1.uid,
new_title=title, new_title=title,
thread_id=group["id"], thread_id=group["id"],
@@ -55,17 +67,33 @@ 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.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)
assert compare(x, new_emoji=emoji) assert compare(x, new_emoji=emoji)
@pytest.mark.xfail(raises=FBchatFacebookError) def test_change_image_local(client1, group, catch_event):
@pytest.mark.parametrize("emoji", ["🙃", "not an emoji"]) url = path.join(path.dirname(__file__), "resources", "image.png")
def test_change_emoji_invalid(client, emoji): with catch_event("onImageChange") as x:
client.changeThreadEmoji(emoji) image_id = client1.changeGroupImageLocal(url, 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
def test_change_image_remote(client1, group, catch_event):
url = "https://github.com/carpedm20/fbchat/raw/master/tests/image.png"
with catch_event("onImageChange") as x:
image_id = client1.changeGroupImageRemote(url, group["id"])
assert subset(x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -73,7 +101,7 @@ def test_change_emoji_invalid(client, emoji):
[ [
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
], ],
) )
@@ -83,9 +111,7 @@ def test_change_color(client, catch_event, compare, color):
assert compare(x, new_color=color) assert compare(x, new_color=color)
@pytest.mark.xfail( @pytest.mark.xfail(raises=FBchatFacebookError, reason="Should fail, but doesn't")
raises=FBchatFacebookError, strict=False, reason="Should fail, but doesn't"
)
def test_change_color_invalid(client): def test_change_color_invalid(client):
class InvalidColor: class InvalidColor:
value = "#0077ff" value = "#0077ff"
@@ -98,3 +124,31 @@ def test_typing_status(client, catch_event, compare, status):
with catch_event("onTyping") as x: with catch_event("onTyping") as x:
client.setTypingStatus(status) client.setTypingStatus(status)
assert compare(x, status=status) assert compare(x, status=status)
@pytest.mark.parametrize('require_admin_approval', [True, False])
def test_change_approval_mode(client1, group, catch_event, require_admin_approval):
with catch_event("onApprovalModeChange") as x:
client1.changeGroupApprovalMode(require_admin_approval, group["id"])
assert subset(
x.res,
approval_mode=require_admin_approval,
author_id=client1.uid,
thread_id=group["id"],
)
@pytest.mark.parametrize("mute_time", [0, 10, 100, 1000, -1])
def test_mute_thread(client, mute_time):
assert client.muteThread(mute_time)
assert client.unmuteThread()
def test_mute_thread_reactions(client):
assert client.muteThreadReactions()
assert client.unmuteThreadReactions()
def test_mute_thread_mentions(client):
assert client.muteThreadMentions()
assert client.unmuteThreadMentions()

View File

@@ -5,17 +5,46 @@ from __future__ import unicode_literals
import threading import threading
import logging import logging
import six import six
import pytest
from os import environ from os import environ
from random import randrange from random import randrange
from contextlib import contextmanager from contextlib import contextmanager
from six import viewitems from six import viewitems
from fbchat import Client from fbchat import Client
from fbchat.models import ThreadType from fbchat.models import ThreadType, EmojiSize, FBchatFacebookError, Sticker
log = logging.getLogger("fbchat.tests").addHandler(logging.NullHandler()) log = logging.getLogger("fbchat.tests").addHandler(logging.NullHandler())
EMOJI_LIST = [
("😆", EmojiSize.SMALL),
("😆", EmojiSize.MEDIUM),
("😆", EmojiSize.LARGE),
# These fail in `catch_event` because the emoji is made into a sticker
# This should be fixed
pytest.param(None, EmojiSize.SMALL, marks=[pytest.mark.xfail()]),
pytest.param(None, EmojiSize.MEDIUM, marks=[pytest.mark.xfail()]),
pytest.param(None, EmojiSize.LARGE, marks=[pytest.mark.xfail()]),
]
STICKER_LIST = [
Sticker("767334476626295"),
pytest.param(Sticker("0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
pytest.param(Sticker(None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
]
TEXT_LIST = [
"test_send",
"😆",
"\\\n\t%?&'\"",
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
"a" * 20000, # Maximum amount of characters you can send
pytest.param("a" * 20001, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
pytest.param(None, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
]
class ClientThread(threading.Thread): class ClientThread(threading.Thread):
def __init__(self, client, *args, **kwargs): def __init__(self, client, *args, **kwargs):
self.client = client self.client = client
@@ -77,7 +106,9 @@ 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',
session_cookies=cache.get("client{}_session".format(n), None), session_cookies=cache.get("client{}_session".format(n), None),
max_tries=1,
) )
yield client yield client
cache.set("client{}_session".format(n), client.getSession()) cache.set("client{}_session".format(n), client.getSession())