Compare commits

...

185 Commits

Author SHA1 Message Date
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
Mads Marquart
6116bc9ca4 addUsersToGroup can no longer return the message id
Updated documentation and tests
2018-06-06 16:39:23 +02: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
Mads Marquart
7bf6a9fadc Version up, thanks to @2FWAH
* Fixed `onTyping`
* Fixed `changeThreadColor` with `MESSENGER_BLUE `
2018-06-05 13:17:46 +02:00
Mads Marquart
4490360e11 Changed encrypted passwords to point to the free TravisCI version 2018-06-05 13:16:14 +02:00
Mads Marquart
a4dfe0d279 changeThreadColor now works with MESSENGER_BLUE again 2018-06-05 12:55:03 +02:00
Mads Marquart
47679d1d3b Merge remote-tracking branch '2FWAH/fix-ontyping' 2018-06-05 12:51:43 +02:00
Mads Marquart
62e17daf78 thread_fbid is not available with typ, there thread_id = author_id
Also enabled tests
2018-06-04 23:57:50 +02:00
2FWAH
1f359f2a72 Call onTyping on "typ" or "ttyp" messages
FB returns "typ" for ONE-TO-ONE conversations and "ttyp" for GROUP conversations.
2018-06-04 23:25:50 +02:00
2FWAH
cebe7a28c0 Fix onTyping detection
FB changed the format of typing notification messages:
- update "mtype" from "typ" to "ttyp".
- Get thread ID from "to" to "thread_fbid" ("thread" looks the same)
2018-06-04 23:25:50 +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
Mads Marquart
e614800d5f Update encrypted passwords 2018-06-04 13:57:21 +02:00
Mads Marquart
151a114235 TravisCI integration and updated test suite (#296)
* Make TravisCI setup

* Use pytest, move tests to seperate files

* Added system to check if `onX` events were successfully executed
2018-06-04 13:44:04 +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
Mads Marquart
38f66147cb Version up, thanks to @orenyomtov and @Abhinav2812
Also fixed `Client.isLoggedIn`
2018-05-18 17:35:03 +02:00
Mads Marquart
ffa26c20b5 Merge branch 'patch-1' 2018-05-18 16:58:32 +02:00
Abhinav2812
430ada7f84 Resolve FBChatException
Resolve the error `fbchat.models.FBchatException: Could not get ThreadColor from color: FF0084FF` when threadcolor is set to default (MESSENGER_BLUE)
2018-05-16 17:54:37 +05:30
Mads Marquart
988e37eb42 Merge remote-tracking branch 'orenyomtov/patch-3' 2018-05-08 16:51:03 +02:00
Mads Marquart
1938b90bce Merge remote-tracking branch 'orenyomtov/patch-2' 2018-05-08 16:50:56 +02:00
Mads Marquart
f61d1403f3 Merge remote-tracking branch 'orenyomtov/patch-1' 2018-05-08 16:50:48 +02:00
Oren
d228f34f64 Eliminate an unnecessary HTTP request during login
This change eliminates requesting and downloading the entire FB home page (~160kb) every login.
2018-05-08 15:40:46 +03:00
Oren
97049556ed Update obtaining fb_dtsg and fb_h
fb_dtsg is sometimes returned inside an HTML comment, and beautifulsoup can't find it - in that case we'll use a regular expression to extract it.

fb_h is sometimes not returned in the HTML of req_url.BASE (in my experience, when resuming a session using session_cookies).

Following the discussion here:
https://github.com/Schmavery/facebook-chat-api/issues/505
I learned it is used for logging out, and can be found in the response of `https://www.facebook.com/bluebar/modern_settings_menu/`.

I included support for fetching it from there.

Because this library is used many more times for logging in, than for logging out, instead of adding an extra HTTP request during login, I decided to perform it during logout, only in case fb_h is not found in the HTML of req_url.BASE.
2018-05-08 12:41:22 +03:00
Oren
b64c6a94cc Add MODERN_SETTINGS_MENU url to ReqUrl
It is used to obtain the fb_h value
2018-05-08 12:18:15 +03:00
Oren
edc655bae7 Fix IndexError: list index out of range bug
When the returned `short_name` is null, `fbchat` throws an exception:

```python
  File "/usr/local/lib/python2.7/site-packages/fbchat/client.py", line 792, in fetchThreadList
    return [graphql_to_thread(node) for node in j['viewer']['message_threads']['nodes']]
  File "/usr/local/lib/python2.7/site-packages/fbchat/graphql.py", line 193, in graphql_to_thread
    last_name=user.get('name').split(user.get('short_name'),1)[1].strip(),
IndexError: list index out of range
```

This commit fixes that scenario by accessing the last item in the list via `.pop()` instead of via `[1]`
2018-05-07 19:50:43 +03:00
Mads Marquart
884af48270 Version up, thanks to @gave92
Properly fixed `markAsRead`, @gave92  reminded me that I forgot to change the `True` to `'true'` when removing `encode_params`
2018-03-21 10:05:07 +01:00
Mads Marquart
95f018fad3 Fixed example Echobot 2018-03-19 21:40:51 +01:00
Mads Marquart
b44758a195 Version up, thanks to @gave92
Fix `markAsRead` and `fetchUnread`; fixes #261
Added the `ssl_verify` instance variable, which allows disabling SSL varification for proxies
2018-03-19 21:28:48 +01:00
Mads Marquart
f1c20d490e Removed encode_params from PR, as discussed in #269 2018-03-19 21:15:23 +01:00
Mads Marquart
04372d498e Merge pull request #269 from gave92/FetchUnread
Fix `markAsRead` and `fetchUnread`; fixes #261
Added the `ssl_verify` instance variable, which allows disabling SSL varification for proxies
2018-03-19 21:08:45 +01:00
Marco Gavelli
63ea899605 fix for python3 2018-03-19 20:47:41 +01:00
Marco Gavelli
4fdd145d1e verify in _postFile 2018-03-19 16:52:22 +01:00
Marco Gavelli
57ee68b0e0 added documentation to markAsRead 2018-03-19 16:38:19 +01:00
Marco Gavelli
99c6884681 added documentation to fetchUnread 2018-03-19 16:29:26 +01:00
Marco Gavelli
1c1438e9bc fix for markAsRead, fetchUnread 2018-03-18 11:18:46 +01:00
Marco Gavelli
22f1b3e489 fix FetchUnread 2018-03-17 19:32:45 +01:00
Mads Marquart
fb1ad5800c Minor fix for searchFor. See comments on #266 2018-03-05 22:07:16 +01:00
Taehoon Kim
4dd15b05ef version up thanks to @2FWAH's PR #266 #267 2018-03-03 22:49:25 +09:00
Taehoon Kim
d7cdb644c4 Merge pull request #265 from 2FWAH/fix-fetchThreadList-archived
Fix ThreadLocation to work with new GraphQL and archived threads
2018-03-03 22:22:21 +09:00
Taehoon Kim
bfcf4950b3 Merge pull request #266 from 2FWAH/fill-last_message_timestamp-in-fetchThreadList
Add last_message_timestamp support
2018-03-03 22:21:49 +09:00
Taehoon Kim
6612c97f05 Merge pull request #267 from danijeljw/patch-1
duplicate lines removed from setup
2018-03-03 22:20:28 +09:00
Danijel-James Wynyard
b92cf62726 duplicate lines removed 2018-03-03 12:08:05 +11:00
2FWAH
a53ba33a81 Set offset to 'None' by default 2018-02-23 09:23:34 +01:00
2FWAH
c04d38cf63 Handle last_message_timestamp
Set last_message_timestamp for one to one and group conversations.
2018-02-22 19:53:56 +01:00
2FWAH
a051adcbc0 Fix ThreadLocation to work with new GraphQL 2018-02-22 17:49:26 +01:00
Mads Marquart
900a9cdf72 Version up, thanks to @gave92
`fetchThreadList` is updated with a GraphQL implementation. See #241
2018-02-18 22:40:13 +01:00
Mads Marquart
611b329934 Merge pull request #259 from gave92/fetchThreadListGraphQL
Added GraphQL alternative to fetchThreadList; fixes #241
2018-02-18 22:36:23 +01:00
Mads Marquart
2642788bc1 Merged fetchThreadListGraphQL into fetchThreadList 2018-02-18 22:32:12 +01:00
Marco Gavelli
8268445f0b Changed return type for ONE_TO_ONE to User 2018-02-18 22:49:47 +01:00
Marco Gavelli
c12dcd9263 Added GraphQL alternative to fetchThreadList; fixes #241 2018-02-17 14:29:31 +01:00
Mads Marquart
3142524809 Version up, thanks to @DeltaF1
`onFriendRequest` functionality is restored
2018-02-07 11:30:19 +01:00
Mads Marquart
4c9d3bd9d7 Merge pull request #255 from DeltaF1/master
Restored onFriendRequest functionality
2018-02-07 11:29:04 +01:00
DeltaF1
ba103066b8 Restored onFriendRequest functionality 2018-02-06 00:30:35 -05:00
Mads Marquart
0b0d6179a2 Version up, thanks to @sdnian
`fetchThreadMessages` and `listen` can now parse AudioAttachments
2018-01-30 17:20:47 +01:00
Mads Marquart
e8806d4ef8 Merge pull request #254 from sdnian/bransh1
modify AudioAttachment function
2018-01-30 17:15:55 +01:00
Steve Nian
c96e5f174c update 2018-01-30 20:22:18 +08:00
Steve Nian
315242e069 update 2018-01-30 20:17:09 +08:00
Steve Nian
a94fa5fbe3 AudioAttachment 2018-01-30 17:33:29 +08:00
Mads Marquart
90203afdd0 Fixes documentation error 2018-01-23 20:20:13 +01:00
Mads Marquart
2c0d098852 Fixes #240, small backwards-compatablitity issue when sending images 2018-01-08 21:55:11 +01:00
Mads Marquart
e4290cd465 Version up, thanks to @lobstr 2018-01-02 13:40:50 +01:00
Mads Marquart
46b85dec5c Merge remote-tracking branch 'lobstr/master' 2018-01-02 13:40:25 +01:00
Mads Marquart
bbc34bd009 Added onTyping method 2018-01-02 13:33:13 +01:00
cirrux
c495317e65 Fix setTypingStatus to send correctly 2018-01-01 23:11:35 -05:00
cirrux
a946050228 Re-enable typing notification 2017-12-31 12:27:55 -05:00
cirrux
83789dcefa Fix attachment parsing for newer structure 2017-12-26 19:12:10 -05:00
Mads Marquart
4f1f9bf1ce Fixed errors on unknown genders 2017-12-15 23:46:47 +01:00
42 changed files with 2919 additions and 683 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`

9
.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
@@ -24,7 +26,12 @@ develop-eggs
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# Data for tests # Scripts and data for tests
my_tests.py
my_test_data.json my_test_data.json
my_data.json my_data.json
tests.data tests.data
.pytest_cache
# Virtual environment
venv/

90
.travis.yml Normal file
View File

@@ -0,0 +1,90 @@
sudo: false
language: python
conditions: v1
# There are two accounts made specifically for Travis, and the passwords are really only encrypted for obscurity
# The global env variables `client1_email`, `client1_password`, `client2_email`, `client2_password` and `group_id`
# are set on the Travis Settings page
# The tests are run with `Limit concurrent jobs = 1`, since the tests can't use the clients simultaneously
install:
- pip install -U -r requirements.txt
- pip install -U -r dev-requirements.txt
cache:
pip: true
# Pytest caching is disabled, since TravisCI instances have different public IPs. Facebook doesn't like that,
# and redirects you to the url `/checkpoint/block`, where you have to change the account's password
# directories:
# - .pytest_cache
jobs:
include:
# The tests are split into online and offline versions.
# The online tests are only run against the master branch.
# Because:
# Travis caching is per-branch and per-job, so even though we cache the Facebook sessions via. `.pytest_cache`
# and in `tests.utils.load_client`, we need 6 new sessions per branch. This is usually the point where Facebook
# starts complaining, and we have to manually fix it
- &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

@@ -5,8 +5,8 @@ from fbchat import log, Client
# Subclass fbchat.Client and override required methods # Subclass fbchat.Client and override required methods
class EchoBot(Client): class EchoBot(Client):
def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs): def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
self.markAsDelivered(author_id, thread_id) self.markAsDelivered(thread_id, message_object.uid)
self.markAsRead(author_id) self.markAsRead(thread_id)
log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name)) log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))

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.0'
__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.1.2'
__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

@@ -109,6 +109,13 @@ def graphql_to_attachment(a):
large_image=a.get('large_image'), large_image=a.get('large_image'),
uid=a.get('legacy_attachment_id') uid=a.get('legacy_attachment_id')
) )
elif _type == 'MessageAudio':
return AudioAttachment(
filename=a.get('filename'),
url=a.get('playable_url'),
duration=a.get('playable_duration_in_ms'),
audio_type=a.get('audio_type')
)
elif _type == 'MessageFile': elif _type == 'MessageFile':
return FileAttachment( return FileAttachment(
url=a.get('url'), url=a.get('url'),
@@ -121,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'] = {}
@@ -148,13 +220,14 @@ 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 = graphql_to_plan(user['event_reminders']['nodes'][0]) if user.get('event_reminders', dict()).get('nodes') else None
return User( return User(
user['id'], user['id'],
url=user.get('url'), url=user.get('url'),
first_name=user.get('first_name'), first_name=user.get('first_name'),
last_name=user.get('last_name'), last_name=user.get('last_name'),
is_friend=user.get('is_viewer_friend'), is_friend=user.get('is_viewer_friend'),
gender=GENDERS[user.get('gender')], gender=GENDERS.get(user.get('gender')),
affinity=user.get('affinity'), affinity=user.get('affinity'),
nickname=c_info.get('nickname'), nickname=c_info.get('nickname'),
color=c_info.get('color'), color=c_info.get('color'),
@@ -162,13 +235,60 @@ 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):
if thread['thread_type'] == 'GROUP':
return graphql_to_group(thread)
elif thread['thread_type'] == 'ONE_TO_ONE':
if thread.get('big_image_src') is None:
thread['big_image_src'] = {}
c_info = get_customization_info(thread)
participants = [node['messaging_actor'] for node in thread['all_participants']['nodes']]
user = next(p for p in participants if p['id'] == thread['thread_key']['other_user_id'])
last_message_timestamp = None
if 'last_message' in thread:
last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise']
first_name = user.get('short_name')
if first_name is None:
last_name = None
else:
last_name = user.get('name').split(first_name, 1).pop().strip()
plan = graphql_to_plan(thread['event_reminders']['nodes'][0]) if thread.get('event_reminders', dict()).get('nodes') else None
return User(
user['id'],
url=user.get('url'),
name=user.get('name'),
first_name=first_name,
last_name=last_name,
is_friend=user.get('is_viewer_friend'),
gender=GENDERS.get(user.get('gender')),
affinity=user.get('affinity'),
nickname=c_info.get('nickname'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
own_nickname=c_info.get('own_nickname'),
photo=user['big_image_src'].get('uri'),
message_count=thread.get('messages_count'),
last_message_timestamp=last_message_timestamp,
plan=plan,
)
else:
raise FBchatException('Unknown thread type: {}, with data: {}'.format(thread.get('thread_type'), thread))
def graphql_to_group(group): def graphql_to_group(group):
if group.get('image') is None: if group.get('image') is None:
group['image'] = {} group['image'] = {}
c_info = get_customization_info(group) c_info = get_customization_info(group)
last_message_timestamp = None
if 'last_message' in group:
last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise']
plan = graphql_to_plan(group['event_reminders']['nodes'][0]) if group.get('event_reminders', dict()).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']]),
@@ -177,13 +297,16 @@ def graphql_to_group(group):
emoji=c_info.get('emoji'), emoji=c_info.get('emoji'),
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,
plan=plan,
) )
def graphql_to_room(room): def graphql_to_room(room):
if room.get('image') is None: if room.get('image') is None:
room['image'] = {} room['image'] = {}
c_info = get_customization_info(room) c_info = get_customization_info(room)
plan = graphql_to_plan(room['event_reminders']['nodes'][0]) if room.get('event_reminders', dict()).get('nodes') else None
return Room( return Room(
room['thread_key']['thread_fbid'], room['thread_key']['thread_fbid'],
participants=set([node['messaging_actor']['id'] for node in room['all_participants']['nodes']]), participants=set([node['messaging_actor']['id'] for node in room['all_participants']['nodes']]),
@@ -198,6 +321,7 @@ def graphql_to_room(room):
approval_requests = set(node.get('id') for node in room['thread_queue_metadata'].get('approval_requests', {}).get('nodes')), 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'), join_link = room['joinable_mode'].get('link'),
privacy_mode = bool(room.get('privacy_mode')), privacy_mode = bool(room.get('privacy_mode')),
plan=plan,
) )
def graphql_to_page(page): def graphql_to_page(page):
@@ -205,6 +329,7 @@ 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 = graphql_to_plan(page['event_reminders']['nodes'][0]) if page.get('event_reminders', dict()).get('nodes') else None
return Page( return Page(
page['id'], page['id'],
url=page.get('url'), url=page.get('url'),
@@ -212,7 +337,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):

View File

@@ -9,11 +9,11 @@ class FBchatException(Exception):
class FBchatFacebookError(FBchatException): class FBchatFacebookError(FBchatException):
#: The error code that Facebook returned #: The error code that Facebook returned
fb_error_code = str fb_error_code = None
#: The error message that Facebook returned (In the user's own language) #: The error message that Facebook returned (In the user's own language)
fb_error_message = str fb_error_message = None
#: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200) #: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200)
request_status_code = int request_status_code = None
def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None): def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None):
super(FBchatFacebookError, self).__init__(message) super(FBchatFacebookError, self).__init__(message)
"""Thrown by fbchat when Facebook returns an error""" """Thrown by fbchat when Facebook returns an error"""
@@ -26,18 +26,20 @@ class FBchatUserError(FBchatException):
class Thread(object): class Thread(object):
#: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info #: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info
uid = str uid = None
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info #: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info
type = None type = None
#: The thread's picture #: A url to the thread's picture
photo = str photo = None
#: The name of the thread #: The name of the thread
name = str name = None
#: Timestamp of last message #: Timestamp of last message
last_message_timestamp = str last_message_timestamp = None
#: Number of messages in the thread #: Number of messages in the thread
message_count = int 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__()
@@ -55,25 +58,25 @@ class Thread(object):
class User(Thread): class User(Thread):
#: The profile url #: The profile url
url = str url = None
#: The users first name #: The users first name
first_name = str first_name = None
#: The users last name #: The users last name
last_name = str last_name = None
#: Whether the user and the client are friends #: Whether the user and the client are friends
is_friend = bool is_friend = None
#: The user's gender #: The user's gender
gender = str gender = None
#: From 0 to 1. How close the client is to the user #: From 0 to 1. How close the client is to the user
affinity = float affinity = None
#: The user's nickname #: The user's nickname
nickname = str nickname = None
#: The clients nickname, as seen by the user #: The clients nickname, as seen by the user
own_nickname = str own_nickname = None
#: A :class:`ThreadColor`. The message color #: A :class:`ThreadColor`. The message color
color = None color = None
#: The default emoji #: The default emoji
emoji = str emoji = None
def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs): def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs):
"""Represents a Facebook user. Inherits `Thread`""" """Represents a Facebook user. Inherits `Thread`"""
@@ -92,13 +95,13 @@ class User(Thread):
class Group(Thread): class Group(Thread):
#: Unique list (set) of the group thread's participant user IDs #: Unique list (set) of the group thread's participant user IDs
participants = set participants = None
#: Dict, containing user nicknames mapped to their IDs #: A dict, containing user nicknames mapped to their IDs
nicknames = dict nicknames = None
#: A :class:`ThreadColor`. The groups's message color #: A :class:`ThreadColor`. The groups's message color
color = None color = None
#: The groups's default emoji #: The groups's default emoji
emoji = str emoji = 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, **kwargs):
"""Represents a Facebook group. Inherits `Thread`""" """Represents a Facebook group. Inherits `Thread`"""
@@ -115,15 +118,15 @@ class Group(Thread):
class Room(Group): class Room(Group):
# Set containing user IDs of thread admins # Set containing user IDs of thread admins
admins = set admins = None
# True if users need approval to join # True if users need approval to join
approval_mode = bool approval_mode = None
# Set containing user IDs requesting to join # Set containing user IDs requesting to join
approval_requests = set approval_requests = None
# Link for joining room # Link for joining room
join_link = str join_link = None
# True is room is not discoverable # True is room is not discoverable
privacy_mode = bool privacy_mode = None
def __init__(self, uid, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs): 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`""" """Represents a Facebook room. Inherits `Group`"""
@@ -142,15 +145,15 @@ class Room(Group):
class Page(Thread): class Page(Thread):
#: The page's custom url #: The page's custom url
url = str url = None
#: The name of the page's location city #: The name of the page's location city
city = str city = None
#: Amount of likes the page has #: Amount of likes the page has
likes = int likes = None
#: Some extra information about the page #: Some extra information about the page
sub_title = str sub_title = None
#: The page's category #: The page's category
category = str category = None
def __init__(self, uid, url=None, city=None, likes=None, sub_title=None, category=None, **kwargs): def __init__(self, uid, url=None, city=None, likes=None, sub_title=None, category=None, **kwargs):
"""Represents a Facebook page. Inherits `Thread`""" """Represents a Facebook page. Inherits `Thread`"""
@@ -166,7 +169,7 @@ class Message(object):
#: The actual message #: The actual message
text = None text = None
#: A list of :class:`Mention` objects #: A list of :class:`Mention` objects
mentions = [] mentions = None
#: A :class:`EmojiSize`. Size of a sent emoji #: A :class:`EmojiSize`. Size of a sent emoji
emoji_size = None emoji_size = None
#: The message ID #: The message ID
@@ -178,13 +181,13 @@ class Message(object):
#: Whether the message is read #: Whether the message is read
is_read = None is_read = 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 = {} reactions = None
#: The actual message #: The actual message
text = None text = None
#: A :class:`Sticker` #: A :class:`Sticker`
sticker = None sticker = None
#: A list of attachments #: A list of attachments
attachments = [] attachments = None
def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None): def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None):
"""Represents a Facebook message""" """Represents a Facebook message"""
@@ -207,7 +210,7 @@ class Message(object):
class Attachment(object): class Attachment(object):
#: The attachment ID #: The attachment ID
uid = str uid = None
def __init__(self, uid=None): def __init__(self, uid=None):
"""Represents a Facebook attachment""" """Represents a Facebook attachment"""
@@ -251,13 +254,13 @@ class ShareAttachment(Attachment):
class FileAttachment(Attachment): class FileAttachment(Attachment):
#: Url where you can download the file #: Url where you can download the file
url = str url = None
#: Size of the file in bytes #: Size of the file in bytes
size = int size = None
#: Name of the file #: Name of the file
name = str name = None
#: Whether Facebook determines that this file may be harmful #: Whether Facebook determines that this file may be harmful
is_malicious = bool is_malicious = None
def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs): def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs):
"""Represents a file that has been sent as a Facebook attachment""" """Represents a file that has been sent as a Facebook attachment"""
@@ -267,45 +270,58 @@ class FileAttachment(Attachment):
self.name = name self.name = name
self.is_malicious = is_malicious self.is_malicious = is_malicious
class AudioAttachment(FileAttachment): class AudioAttachment(Attachment):
def __init__(self, **kwargs): #: Name of the file
"""Represents an audio file that has been sent as a Facebook attachment - *Currently Incomplete!*""" filename = None
super(StickerAttachment, self).__init__(**kwargs) #: Url of the audio file
url = None
#: Duration of the audioclip in milliseconds
duration = None
#: Audio type
audio_type = None
def __init__(self, filename=None, url=None, duration=None, audio_type=None, **kwargs):
"""Represents an audio file that has been sent as a Facebook attachment"""
super(AudioAttachment, self).__init__(**kwargs)
self.filename = filename
self.url = url
self.duration = duration
self.audio_type = audio_type
class ImageAttachment(Attachment): class ImageAttachment(Attachment):
#: The extension of the original image (eg. 'png') #: The extension of the original image (eg. 'png')
original_extension = str original_extension = None
#: Width of original image #: Width of original image
width = int width = None
#: Height of original image #: Height of original image
height = int height = None
#: Whether the image is animated #: Whether the image is animated
is_animated = bool is_animated = None
#: URL to a thumbnail of the image #: URL to a thumbnail of the image
thumbnail_url = str thumbnail_url = None
#: URL to a medium preview of the image #: URL to a medium preview of the image
preview_url = str preview_url = None
#: Width of the medium preview image #: Width of the medium preview image
preview_width = int preview_width = None
#: Height of the medium preview image #: Height of the medium preview image
preview_height = int preview_height = None
#: URL to a large preview of the image #: URL to a large preview of the image
large_preview_url = str large_preview_url = None
#: Width of the large preview image #: Width of the large preview image
large_preview_width = int large_preview_width = None
#: Height of the large preview image #: Height of the large preview image
large_preview_height = int large_preview_height = None
#: URL to an animated preview of the image (eg. for gifs) #: URL to an animated preview of the image (eg. for gifs)
animated_preview_url = str animated_preview_url = None
#: Width of the animated preview image #: Width of the animated preview image
animated_preview_width = int animated_preview_width = None
#: Height of the animated preview image #: Height of the animated preview image
animated_preview_height = int animated_preview_height = None
def __init__(self, original_extension=None, width=None, height=None, is_animated=None, thumbnail_url=None, preview=None, large_preview=None, animated_preview=None, **kwargs): def __init__(self, original_extension=None, width=None, height=None, is_animated=None, thumbnail_url=None, preview=None, large_preview=None, animated_preview=None, **kwargs):
""" """
@@ -344,36 +360,36 @@ class ImageAttachment(Attachment):
class VideoAttachment(Attachment): class VideoAttachment(Attachment):
#: Size of the original video in bytes #: Size of the original video in bytes
size = int size = None
#: Width of original video #: Width of original video
width = int width = None
#: Height of original video #: Height of original video
height = int height = None
#: Length of video in milliseconds #: Length of video in milliseconds
duration = int duration = None
#: URL to very compressed preview video #: URL to very compressed preview video
preview_url = str preview_url = None
#: URL to a small preview image of the video #: URL to a small preview image of the video
small_image_url = str small_image_url = None
#: Width of the small preview image #: Width of the small preview image
small_image_width = int small_image_width = None
#: Height of the small preview image #: Height of the small preview image
small_image_height = int small_image_height = None
#: URL to a medium preview image of the video #: URL to a medium preview image of the video
medium_image_url = str medium_image_url = None
#: Width of the medium preview image #: Width of the medium preview image
medium_image_width = int medium_image_width = None
#: Height of the medium preview image #: Height of the medium preview image
medium_image_height = int medium_image_height = None
#: URL to a large preview image of the video #: URL to a large preview image of the video
large_image_url = str large_image_url = None
#: Width of the large preview image #: Width of the large preview image
large_image_width = int large_image_width = None
#: Height of the large preview image #: Height of the large preview image
large_image_height = int large_image_height = None
def __init__(self, size=None, width=None, height=None, duration=None, preview_url=None, small_image=None, medium_image=None, large_image=None, **kwargs): def __init__(self, size=None, width=None, height=None, duration=None, preview_url=None, small_image=None, medium_image=None, large_image=None, **kwargs):
"""Represents a video that has been sent as a Facebook attachment""" """Represents a video that has been sent as a Facebook attachment"""
@@ -405,11 +421,11 @@ class VideoAttachment(Attachment):
class Mention(object): class Mention(object):
#: The thread ID the mention is pointing at #: The thread ID the mention is pointing at
thread_id = str thread_id = None
#: The character where the mention starts #: The character where the mention starts
offset = int offset = None
#: The length of the mention #: The length of the mention
length = int length = None
def __init__(self, thread_id, offset=0, length=10): def __init__(self, thread_id, offset=0, length=10):
"""Represents a @mention""" """Represents a @mention"""
@@ -423,6 +439,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):
@@ -438,10 +535,10 @@ class ThreadType(Enum):
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)."""
INBOX = 'inbox' INBOX = 'INBOX'
PENDING = 'pending' PENDING = 'PENDING'
ARCHIVED = 'action:archived' ARCHIVED = 'ARCHIVED'
OTHER = 'other' OTHER = 'OTHER'
class TypingStatus(Enum): class TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing""" """Used to specify whether the user is typing or has stopped typing"""
@@ -456,7 +553,7 @@ class EmojiSize(Enum):
class ThreadColor(Enum): class ThreadColor(Enum):
"""Used to specify a thread colors""" """Used to specify a thread colors"""
MESSENGER_BLUE = '' MESSENGER_BLUE = '#0084ff'
VIKING = '#44bec7' VIKING = '#44bec7'
GOLDEN_POPPY = '#ffc300' GOLDEN_POPPY = '#ffc300'
RADICAL_RED = '#fa3c4c' RADICAL_RED = '#fa3c4c'

View File

@@ -5,10 +5,21 @@ 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:
from urllib.parse import urlencode
basestring = (str, bytes)
except ImportError:
from urllib import urlencode
basestring = basestring
# Python 2's `input` executes the input, whereas `raw_input` just returns the input # Python 2's `input` executes the input, whereas `raw_input` just returns the input
try: try:
input = raw_input input = raw_input
@@ -41,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
@@ -74,13 +75,12 @@ GENDERS = {
#'': 'female_singular_guess', #'': 'female_singular_guess',
#'': 'male_singular_guess', #'': 'male_singular_guess',
#'': 'mixed', #'': 'mixed',
#'': 'neuter_singular', 'NEUTER': 'neuter_singular',
#'': 'unknown_singular', #'': 'unknown_singular',
#'': 'female_plural', #'': 'female_plural',
#'': 'male_plural', #'': 'male_plural',
#'': 'neuter_plural', #'': 'neuter_plural',
#'': 'unknown_plural', #'': 'unknown_plural',
None: None
} }
class ReqUrl(object): class ReqUrl(object):
@@ -88,8 +88,12 @@ class ReqUrl(object):
SEARCH = "https://www.facebook.com/ajax/typeahead/search.php" SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
LOGIN = "https://m.facebook.com/login.php?login_attempt=1" LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
SEND = "https://www.facebook.com/messaging/send/" SEND = "https://www.facebook.com/messaging/send/"
THREAD_SYNC = "https://www.facebook.com/ajax/mercury/thread_sync.php" UNREAD_THREADS = "https://www.facebook.com/ajax/mercury/unread_threads.php"
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"
@@ -109,11 +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/"
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
@@ -135,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)
@@ -202,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
@@ -226,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()

6
pytest.ini Normal file
View File

@@ -0,0 +1,6 @@
[pytest]
xfail_strict=true
markers =
offline: Offline tests, aka. tests that can be executed without the need of a client
expensive: Expensive tests, which should be executed sparingly
addopts = -m "not expensive"

View File

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

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,84 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
"""
Setup script for fbchat
"""
import os
try:
from setuptools import setup 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 :: Developers',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: BSD License',
'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,
)

260
tests.py
View File

@@ -1,260 +0,0 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import json
import logging
import unittest
from getpass import getpass
from sys import argv
from os import path, chdir
from glob import glob
from fbchat import Client
from fbchat.models import *
import py_compile
logging_level = logging.ERROR
"""
Testing script for `fbchat`.
Full documentation on https://fbchat.readthedocs.io/
"""
test_sticker_id = '767334476626295'
class CustomClient(Client):
def __init__(self, *args, **kwargs):
self.got_qprimer = False
super(type(self), self).__init__(*args, **kwargs)
def onQprimer(self, msg, **kwargs):
self.got_qprimer = True
class TestFbchat(unittest.TestCase):
def test_examples(self):
# Checks for syntax errors in the examples
chdir('examples')
for f in glob('*.txt'):
print(f)
with self.assertRaises(py_compile.PyCompileError):
py_compile.compile(f)
chdir('..')
def test_loginFunctions(self):
self.assertTrue(client.isLoggedIn())
client.logout()
self.assertFalse(client.isLoggedIn())
with self.assertRaises(Exception):
client.login('<email>', '<password>', max_tries=1)
client.login(email, password)
self.assertTrue(client.isLoggedIn())
def test_sessions(self):
global client
session_cookies = client.getSession()
client = CustomClient(email, password, session_cookies=session_cookies, logging_level=logging_level)
self.assertTrue(client.isLoggedIn())
def test_defaultThread(self):
# setDefaultThread
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
self.assertTrue(client.send(Message(text='test_default_recipient★')))
# resetDefaultThread
client.resetDefaultThread()
with self.assertRaises(ValueError):
client.send(Message(text='should_not_send'))
def test_fetchAllUsers(self):
users = client.fetchAllUsers()
self.assertGreater(len(users), 0)
def test_searchFor(self):
users = client.searchForUsers('Mark Zuckerberg')
self.assertGreater(len(users), 0)
u = users[0]
# Test if values are set correctly
self.assertEqual(u.uid, '4')
self.assertEqual(u.type, ThreadType.USER)
self.assertEqual(u.photo[:4], 'http')
self.assertEqual(u.url[:4], 'http')
self.assertEqual(u.name, 'Mark Zuckerberg')
group_name = client.changeThreadTitle('tést_searchFor', thread_id=group_id, thread_type=ThreadType.GROUP)
groups = client.searchForGroups('')
self.assertGreater(len(groups), 0)
def test_send(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
self.assertIsNotNone(client.send(Message(emoji_size=EmojiSize.SMALL)))
self.assertIsNotNone(client.send(Message(emoji_size=EmojiSize.MEDIUM)))
self.assertIsNotNone(client.send(Message(text='😆', emoji_size=EmojiSize.LARGE)))
self.assertIsNotNone(client.send(Message(text='test_send★')))
with self.assertRaises(FBchatFacebookError):
self.assertIsNotNone(client.send(Message(text='test_send_should_fail★'), thread_id=thread['id'], thread_type=(ThreadType.GROUP if thread['type'] == ThreadType.USER else ThreadType.USER)))
self.assertIsNotNone(client.send(Message(text='Hi there @user', mentions=[Mention(user_id, offset=9, length=5)])))
self.assertIsNotNone(client.send(Message(text='Hi there @group', mentions=[Mention(group_id, offset=9, length=6)])))
self.assertIsNotNone(client.send(Message(sticker=Sticker(test_sticker_id))))
def test_sendImages(self):
image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png'
image_local_url = path.join(path.dirname(__file__), 'tests/image.png')
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
mentions = [Mention(thread['id'], offset=26, length=4)]
self.assertTrue(client.sendRemoteImage(image_url, Message(text='test_send_image_remote_to_@you★', mentions=mentions)))
self.assertTrue(client.sendLocalImage(image_local_url, Message(text='test_send_image_local_to__@you★', mentions=mentions)))
def test_fetchThreadList(self):
client.fetchThreadList(offset=0, limit=20)
def test_fetchThreadMessages(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.send(Message(text='test_getThreadInfo★'))
messages = client.fetchThreadMessages(limit=1)
self.assertEqual(messages[0].author, client.uid)
self.assertEqual(messages[0].text, 'test_getThreadInfo★')
def test_listen(self):
client.startListening()
client.doOneListen()
client.stopListening()
self.assertTrue(client.got_qprimer)
def test_fetchInfo(self):
info = client.fetchUserInfo('4')['4']
self.assertEqual(info.name, 'Mark Zuckerberg')
info = client.fetchGroupInfo(group_id)[group_id]
self.assertEqual(info.type, ThreadType.GROUP)
def test_removeAddFromGroup(self):
client.removeUserFromGroup(user_id, thread_id=group_id)
client.addUsersToGroup(user_id, thread_id=group_id)
def test_changeThreadTitle(self):
for thread in threads:
client.changeThreadTitle('test_changeThreadTitle★', thread_id=thread['id'], thread_type=thread['type'])
def test_changeNickname(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.changeNickname('test_changeNicknameSelf★', client.uid)
client.changeNickname('test_changeNicknameOther★', user_id)
def test_changeThreadEmoji(self):
for thread in threads:
client.changeThreadEmoji('😀', thread_id=thread['id'])
client.changeThreadEmoji('😀', thread_id=thread['id'])
def test_changeThreadColor(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.changeThreadColor(ThreadColor.BRILLIANT_ROSE)
client.changeThreadColor(ThreadColor.MESSENGER_BLUE)
def test_reactToMessage(self):
for thread in threads:
mid = client.send(Message(text='test_reactToMessage★'), thread_id=thread['id'], thread_type=thread['type'])
client.reactToMessage(mid, MessageReaction.LOVE)
def test_setTypingStatus(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.setTypingStatus(TypingStatus.TYPING)
client.setTypingStatus(TypingStatus.STOPPED)
def start_test(param_client, param_group_id, param_user_id, param_threads, tests=[]):
global client
global group_id
global user_id
global threads
client = param_client
group_id = param_group_id
user_id = param_user_id
threads = param_threads
tests = ['test_' + test if 'test_' != test[:5] else test for test in tests]
if len(tests) == 0:
suite = unittest.TestLoader().loadTestsFromTestCase(TestFbchat)
else:
suite = unittest.TestSuite(map(TestFbchat, tests))
print('Starting test(s)')
unittest.TextTestRunner(verbosity=2).run(suite)
client = None
if __name__ == '__main__':
# Python 3 does not use raw_input, whereas Python 2 does
try:
input = raw_input
except Exception as e:
pass
try:
with open(path.join(path.dirname(__file__), 'tests/my_data.json'), 'r') as f:
j = json.load(f)
email = j['email']
password = j['password']
user_id = j['user_thread_id']
group_id = j['group_thread_id']
session = j.get('session')
except (IOError, IndexError) as e:
email = input('Email: ')
password = getpass()
group_id = input('Please enter a group thread id (To test group functionality): ')
user_id = input('Please enter a user thread id (To test kicking/adding functionality): ')
threads = [
{
'id': user_id,
'type': ThreadType.USER
},
{
'id': group_id,
'type': ThreadType.GROUP
}
]
print('Logging in...')
client = CustomClient(email, password, logging_level=logging_level, session_cookies=session)
# Warning! Taking user input directly like this could be dangerous! Use only for testing purposes!
start_test(client, group_id, user_id, threads, argv[1:])
with open(path.join(path.dirname(__file__), 'tests/my_data.json'), 'w') as f:
session = None
try:
session = client.getSession()
except Exception:
print('Unable to fetch client session!')
json.dump({
'email': email,
'password': password,
'user_thread_id': user_id,
'group_thread_id': group_id,
'session': session
}, f)

123
tests/conftest.py Normal file
View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
import json
from utils import *
from contextlib import contextmanager
from fbchat.models import ThreadType, Message, Mention
@pytest.fixture(scope="session")
def user(client2):
return {"id": client2.uid, "type": ThreadType.USER}
@pytest.fixture(scope="session")
def group(pytestconfig):
return {"id": load_variable("group_id", pytestconfig.cache), "type": ThreadType.GROUP}
@pytest.fixture(scope="session", params=["user", "group", pytest.mark.xfail("none")])
def thread(request, user, group):
return {
"user": user,
"group": group,
"none": {"id": "0", "type": ThreadType.GROUP}
}[request.param]
@pytest.fixture(scope="session")
def client1(pytestconfig):
with load_client(1, pytestconfig.cache) as c:
yield c
@pytest.fixture(scope="session")
def client2(pytestconfig):
with load_client(2, pytestconfig.cache) as c:
yield c
@pytest.fixture(scope="module")
def client(client1, thread):
client1.setDefaultThread(thread["id"], thread["type"])
yield client1
client1.resetDefaultThread()
@pytest.fixture(scope="session", params=["client1", "client2"])
def client_all(request, client1, client2):
return client1 if request.param == "client1" else client2
@pytest.fixture(scope="session")
def catch_event(client2):
t = ClientThread(client2)
t.start()
@contextmanager
def inner(method_name):
caught = CaughtValue()
old_method = getattr(client2, method_name)
# Will be called by the other thread
def catch_value(*args, **kwargs):
old_method(*args, **kwargs)
# Make sure the `set` is only called once
if not caught.is_set():
caught.set(kwargs)
setattr(client2, method_name, catch_value)
yield caught
caught.wait()
if not caught.is_set():
raise ValueError("The value could not be caught")
setattr(client2, method_name, old_method)
yield inner
t.should_stop.set()
try:
# 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
client2.sendMessage(random_hex(), client2.uid)
finally:
t.join()
@pytest.fixture(scope="module")
def compare(client, thread):
def inner(caught_event, **kwargs):
d = {
"author_id": client.uid,
"thread_id": client.uid
if thread["type"] == ThreadType.USER
else thread["id"],
"thread_type": thread["type"],
}
d.update(kwargs)
return subset(caught_event.res, **d)
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.

55
tests/test_base.py Normal file
View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
import py_compile
from glob import glob
from os import path, environ
from fbchat import Client
from fbchat.models import FBchatUserError, Message
@pytest.mark.offline
def test_examples():
# Compiles the examples, to check for syntax errors
for name in glob(path.join(path.dirname(__file__), "../examples", "*.py")):
py_compile.compile(name)
@pytest.mark.trylast
@pytest.mark.expensive
def test_login(client1):
assert client1.isLoggedIn()
email = client1.email
password = client1.password
client1.logout()
assert not client1.isLoggedIn()
with pytest.raises(FBchatUserError):
client1.login("<invalid email>", "<invalid password>", max_tries=1)
client1.login(email, password)
assert client1.isLoggedIn()
@pytest.mark.trylast
def test_sessions(client1):
session = client1.getSession()
Client("no email needed", "no password needed", session_cookies=session)
client1.setSession(session)
assert client1.isLoggedIn()
@pytest.mark.tryfirst
def test_default_thread(client1, thread):
client1.setDefaultThread(thread["id"], thread["type"])
assert client1.send(Message(text="Sent to the specified thread"))
client1.resetDefaultThread()
with pytest.raises(ValueError):
client1.send(Message(text="Should not be sent"))

92
tests/test_fetch.py Normal file
View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from os import path
from fbchat.models import ThreadType, Message, Mention, EmojiSize, Sticker
from utils import subset, STICKER_LIST, EMOJI_LIST
def test_fetch_all_users(client1):
users = client1.fetchAllUsers()
assert len(users) > 0
def test_fetch_thread_list(client1):
threads = client1.fetchThreadList(limit=2)
assert len(threads) == 2
@pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
def test_fetch_message_emoji(client, emoji, emoji_size):
mid = client.sendEmoji(emoji, emoji_size)
message, = client.fetchThreadMessages(limit=1)
assert subset(
vars(message), uid=mid, author=client.uid, text=emoji, emoji_size=emoji_size
)
@pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
def test_fetch_message_info_emoji(client, thread, emoji, emoji_size):
mid = client.sendEmoji(emoji, emoji_size)
message = client.fetchMessageInfo(mid, thread_id=thread["id"])
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)
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]
def test_fetch_message_info_mentions(client, thread, message_with_mentions):
mid = client.send(message_with_mentions)
message = client.fetchMessageInfo(mid, thread_id=thread["id"])
assert subset(vars(message), uid=mid, author=client.uid, text=message_with_mentions.text)
# 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)
assert subset(vars(message), uid=mid, author=client.uid)
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):
info = client1.fetchUserInfo("4")["4"]
assert info.name == "Mark Zuckerberg"
info = client1.fetchGroupInfo(group["id"])[group["id"]]
assert info.type == ThreadType.GROUP
def test_fetch_image_url(client):
client.sendLocalFiles([path.join(path.dirname(__file__), "resources", "image.png")])
message, = client.fetchThreadMessages(limit=1)
assert client.fetchImageUrl(message.attachments[0].uid)

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from fbchat.models import Message, MessageReaction
from utils import subset
def test_set_reaction(client):
mid = client.send(Message(text="This message will be reacted to"))
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)

105
tests/test_plans.py Normal file
View File

@@ -0,0 +1,105 @@
# -*- 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.mark.xfail(Plan(int(time()), random_hex()), raises=FBchatFacebookError),
pytest.mark.xfail(Plan(0, None)),
])
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)

79
tests/test_polls.py Normal file
View File

@@ -0,0 +1,79 @@
# -*- 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.mark.xfail(Poll(title=None, options=[]), 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)

18
tests/test_search.py Normal file
View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from fbchat.models import ThreadType
def test_search_for(client1):
users = client1.searchForUsers("Mark Zuckerberg")
assert len(users) > 0
u = users[0]
assert u.uid == "4"
assert u.type == ThreadType.USER
assert u.photo[:4] == "http"
assert u.url[:4] == "http"
assert u.name == "Mark Zuckerberg"

109
tests/test_send.py Normal file
View File

@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from os import path
from fbchat.models import FBchatFacebookError, Message, Mention
from utils import subset, STICKER_LIST, EMOJI_LIST, TEXT_LIST
@pytest.mark.parametrize("text", TEXT_LIST)
def test_send_text(client, catch_event, compare, text):
with catch_event("onMessage") as x:
mid = client.sendMessage(text)
assert compare(x, mid=mid, message=text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
@pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
def test_send_emoji(client, catch_event, compare, emoji, emoji_size):
with catch_event("onMessage") as x:
mid = client.sendEmoji(emoji, emoji_size)
assert compare(x, mid=mid, message=emoji)
assert subset(
vars(x.res["message_object"]),
uid=mid,
author=client.uid,
text=emoji,
emoji_size=emoji_size,
)
def test_send_mentions(client, catch_event, compare, message_with_mentions):
with catch_event("onMessage") as x:
mid = client.send(message_with_mentions)
assert compare(x, mid=mid, message=message_with_mentions.text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=message_with_mentions.text)
# The mentions are not ordered by offset
for m in x.res["message_object"].mentions:
assert vars(m) in [vars(x) for x in message_with_mentions.mentions]
@pytest.mark.parametrize("sticker", STICKER_LIST)
def test_send_sticker(client, catch_event, compare, sticker):
with catch_event("onMessage") as x:
mid = client.send(Message(sticker=sticker))
assert compare(x, mid=mid)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid)
assert subset(vars(x.res["message_object"].sticker), uid=sticker.uid)
# Kept for backwards compatibility
@pytest.mark.parametrize(
"method_name, url",
[
(
"sendRemoteImage",
"https://github.com/carpedm20/fbchat/raw/master/tests/image.png",
),
("sendLocalImage", path.join(path.dirname(__file__), "resources", "image.png")),
],
)
def test_send_images(client, catch_event, compare, method_name, url):
text = "An image sent with {}".format(method_name)
with catch_event("onMessage") as x:
mid = getattr(client, method_name)(url, Message(text))
assert compare(x, mid=mid, message=text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
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)

12
tests/test_tests.py Normal file
View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
def test_catch_event(client2, catch_event):
mid = "test"
with catch_event("onMessage") as x:
client2.onMessage(mid=mid)
assert x.res['mid'] == mid

View File

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from fbchat.models import (
Message,
ThreadType,
FBchatFacebookError,
TypingStatus,
ThreadColor,
)
from utils import random_hex, subset
from os import path
def test_remove_from_and_add_to_group(client1, client2, group, catch_event):
# Test both methods, while ensuring that the user gets added to the group
try:
with catch_event("onPersonRemoved") as x:
client1.removeUserFromGroup(client2.uid, group["id"])
assert subset(
x.res, removed_id=client2.uid, author_id=client1.uid, thread_id=group["id"]
)
finally:
with catch_event("onPeopleAdded") as x:
client1.addUsersToGroup(client2.uid, group["id"])
assert subset(
x.res, added_ids=[client2.uid], author_id=client1.uid, thread_id=group["id"]
)
def test_remove_from_and_add_admins_to_group(client1, client2, group, catch_event):
# Test both methods, while ensuring that the user gets added as group admin
try:
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()
with catch_event("onTitleChange") as x:
client1.changeThreadTitle(title, group["id"], thread_type=ThreadType.GROUP)
assert subset(
x.res,
author_id=client1.uid,
new_title=title,
thread_id=group["id"],
thread_type=ThreadType.GROUP,
)
def test_change_nickname(client, client_all, catch_event, compare):
nickname = random_hex()
with catch_event("onNicknameChange") as x:
client.changeNickname(nickname, client_all.uid)
assert compare(x, changed_for=client_all.uid, new_nickname=nickname)
@pytest.mark.parametrize("emoji", [
"😀",
"😂",
"😕",
"😍",
pytest.mark.xfail("🙃", raises=FBchatFacebookError),
pytest.mark.xfail("not an emoji", raises=FBchatFacebookError)
])
def test_change_emoji(client, catch_event, compare, emoji):
with catch_event("onEmojiChange") as x:
client.changeThreadEmoji(emoji)
assert compare(x, new_emoji=emoji)
def test_change_image_local(client1, group, catch_event):
url = path.join(path.dirname(__file__), "resources", "image.png")
with catch_event("onImageChange") as x:
image_id = client1.changeGroupImageLocal(url, group["id"])
assert subset(x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"])
# 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(
"color",
[
x
if x in [ThreadColor.MESSENGER_BLUE, ThreadColor.PUMPKIN]
else pytest.mark.expensive(x)
for x in ThreadColor
],
)
def test_change_color(client, catch_event, compare, color):
with catch_event("onColorChange") as x:
client.changeThreadColor(color)
assert compare(x, new_color=color)
@pytest.mark.xfail(raises=FBchatFacebookError, reason="Should fail, but doesn't")
def test_change_color_invalid(client):
class InvalidColor:
value = "#0077ff"
client.changeThreadColor(InvalidColor())
@pytest.mark.parametrize("status", TypingStatus)
def test_typing_status(client, catch_event, compare, status):
with catch_event("onTyping") as x:
client.setTypingStatus(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()

114
tests/utils.py Normal file
View File

@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import threading
import logging
import six
import pytest
from os import environ
from random import randrange
from contextlib import contextmanager
from six import viewitems
from fbchat import Client
from fbchat.models import ThreadType, EmojiSize, FBchatFacebookError, Sticker
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.mark.xfail((None, EmojiSize.SMALL)),
pytest.mark.xfail((None, EmojiSize.MEDIUM)),
pytest.mark.xfail((None, EmojiSize.LARGE)),
]
STICKER_LIST = [
Sticker("767334476626295"),
pytest.mark.xfail(Sticker("0"), raises=FBchatFacebookError),
pytest.mark.xfail(Sticker(None), raises=FBchatFacebookError),
]
TEXT_LIST = [
"test_send",
"😆",
"\\\n\t%?&'\"",
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
"a" * 20000, # Maximum amount of characters you can send
pytest.mark.xfail("a" * 20001, raises=FBchatFacebookError),
pytest.mark.xfail(None, raises=FBchatFacebookError),
]
class ClientThread(threading.Thread):
def __init__(self, client, *args, **kwargs):
self.client = client
self.should_stop = threading.Event()
super(ClientThread, self).__init__(*args, **kwargs)
def start(self):
self.client.startListening()
self.client.doOneListen() # QPrimer, Facebook now knows we're about to start pulling
super(ClientThread, self).start()
def run(self):
while not self.should_stop.is_set() and self.client.doOneListen():
pass
self.client.stopListening()
if six.PY2:
event_class = threading._Event
else:
event_class = threading.Event
class CaughtValue(event_class):
def set(self, res):
self.res = res
super(CaughtValue, self).set()
def wait(self, timeout=3):
super(CaughtValue, self).wait(timeout=timeout)
def random_hex(length=20):
return "{:X}".format(randrange(16 ** length))
def subset(a, **b):
print(a)
print(b)
return viewitems(b) <= viewitems(a)
def load_variable(name, cache):
var = environ.get(name, None)
if var is not None:
if cache.get(name, None) != var:
cache.set(name, var)
return var
var = cache.get(name, None)
if var is None:
raise ValueError("Variable {!r} neither in environment nor cache".format(name))
return var
@contextmanager
def load_client(n, cache):
client = Client(
load_variable("client{}_email".format(n), cache),
load_variable("client{}_password".format(n), cache),
user_agent='Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36',
session_cookies=cache.get("client{}_session".format(n), None),
max_tries=1,
)
yield client
cache.set("client{}_session".format(n), client.getSession())