Compare commits

...

389 Commits

Author SHA1 Message Date
Mads Marquart
4f947cdbb5 Version up, thanks to @kapi2289 and @kaushalvivek 2019-01-25 16:01:47 +01:00
Mads Marquart
ec6c29052a Merge pull request #371 from carpedm20/fix-enums
Fix `ThreadColor` and `MessageReaction` enums
2019-01-24 22:42:41 +01:00
Mads Marquart
6b117502f3 Merge branch 'master' into fix-enums 2019-01-24 22:40:44 +01:00
Mads Marquart
3e7b20c379 Merge pull request #377 from kapi2289/fix-fbchatexception
Fixed typos in FBchatException
2019-01-05 18:20:38 +01:00
Kacper Ziubryniewicz
f4a997c0ef Fixed typos in FBchatException 2019-01-05 17:55:54 +01:00
Mads Marquart
84fa15e44c Merge pull request #333 from kapi2289/extensible_attachments
[Feature] Extensible attachments
2019-01-04 21:06:11 +01:00
Kacper Ziubryniewicz
7b8ecf8fe3 Changed deleted to unsent 2019-01-04 20:02:00 +01:00
Kacper Ziubryniewicz
95989b6da7 Merge branch 'master' into extensible_attachments 2018-12-23 14:58:03 +01:00
Kacper Ziubryniewicz
22e57f99a1 deleted attribute of Message
and batter handling of deleted (unsended) messages
2018-12-23 14:56:27 +01:00
Kacper Ziubryniewicz
b9d29c0417 Removed addReaction, removeReaction, _react
(and undeprecated `reactToMessage`)
2018-12-23 14:45:17 +01:00
Mads Marquart
45d8b45d96 Fix enum_extend_if_invalid warning 2018-12-12 23:22:08 +01:00
Mads Marquart
b6a6d7dc68 Move enum_extend_if_invalid to utils.py 2018-12-12 23:06:16 +01:00
Mads Marquart
c57b84cd0b Refactor enum extending 2018-12-12 23:04:26 +01:00
Mads Marquart
78e7841b5e Extend MessageReaction when encountering unknown values 2018-12-12 22:53:23 +01:00
Mads Marquart
e41d981449 Extend ThreadColor when encountering unknown values 2018-12-12 22:44:19 +01:00
Mads Marquart
381227af66 Make use aenum instead of the default enum 2018-12-12 22:39:31 +01:00
Mads Marquart
2f8d0728ba Merge pull request #366 from kaushalvivek/master
Fix for issue #365
2018-12-10 21:16:57 +01:00
kaushalvivek
13bfc5f2f9 Fix for search limit 2018-12-10 14:46:04 +05:30
Mads Marquart
f8d3b571ba Version up, thanks to @ekohilas and @kapi2289 2018-12-09 21:21:00 +01:00
Mads Marquart
64b1e52d4c Merge pull request #357 from carpedm20/fixed-listening
Fixed listening
2018-12-09 19:23:33 +01:00
Mads Marquart
b650f7ee9a Merge pull request #367 from carpedm20/fix-pytest-deprecation
Fix pytest "Applying marks directly to parameters" deprecation
2018-12-09 19:23:20 +01:00
Kacper Ziubryniewicz
d4446280c7 Detecting when someone unsends a message 2018-12-09 15:27:01 +01:00
Mads Marquart
3443a233f4 Fix pytest "Applying marks directly to parameters" deprecation 2018-12-09 15:02:48 +01:00
Kacper Ziubryniewicz
861f17bc4d Added DeletedMessage attachment 2018-12-09 14:55:10 +01:00
Kacper Ziubryniewicz
41bbe18e3d Unsending messages 2018-12-09 14:36:23 +01:00
Vivek Kaushal
d32b7b612a Fix for issue #365 2018-12-07 21:26:48 +05:30
Mads Marquart
160386be62 Added support for request_batch parsing in _parseMessage 2018-11-09 20:08:26 +01:00
Mads Marquart
64bdde8f33 Sticky and pool parameters can be set after the inital _fetchSticky 2018-11-07 20:06:10 +01:00
Kacper Ziubryniewicz
8739318101 Sending voice clips 2018-10-30 22:24:47 +01:00
Kacper Ziubryniewicz
1ac569badd Sending pinned or current location 2018-10-30 22:21:05 +01:00
Mads Marquart
89a277c354 Merge pull request #354 from ekohilas/master
separate spellchecked docs
2018-10-28 12:46:48 +01:00
Mads Marquart
8238387c7d Merge pull request #353 from ekohilas/docstrings
completed todo for graphql_requests
2018-10-28 12:45:37 +01:00
ekohilas
6c829581af completed todo for graphql_requests 2018-10-27 02:02:15 +11:00
ekohilas
d180650c1b spellchecked docs 2018-10-25 18:18:19 +11:00
Mads Marquart
772bf5518f Merge pull request #346 from kapi2289/remove_unnecessary
Remove unnecessary code
2018-10-07 16:50:31 +02:00
Kacper Ziubryniewicz
153dc0bdad Remove unnecessary code 2018-10-07 16:27:19 +02:00
Kacper Ziubryniewicz
b7ea8e6001 New sendLocation method 2018-09-29 13:48:08 +02:00
Kacper Ziubryniewicz
b0bf5ba8e0 Update graphql.py 2018-09-29 13:42:11 +02:00
Kacper Ziubryniewicz
8169a5f776 Changed LocationAttachment 2018-09-29 13:40:38 +02:00
Mads Marquart
b4b8914448 Version up, thanks to @kapi2289 2018-09-27 21:53:12 +02:00
Mads Marquart
2ea2c89b4a Fixed markAsRead and markAsUnread, fixes #336 2018-09-27 21:44:04 +02:00
Mads Marquart
479ca59a6a Merge pull request #341 from kapi2289/read_by
[Feature] New `read_by` attribute of `Message`
2018-09-27 20:56:13 +02:00
Mads Marquart
343f987a78 Merge pull request #340 from kapi2289/fix_fetch_thread_list
[Fix] `fetchThreadList` fix
2018-09-27 20:27:03 +02:00
Kacper Ziubryniewicz
bad9c7a4b9 read_by handling 2018-09-24 20:33:43 +02:00
Kacper Ziubryniewicz
576e0949e0 New read_by attribute in Message 2018-09-24 20:32:04 +02:00
Kacper Ziubryniewicz
d807648d2b fetchThreadList fix 2018-09-24 16:50:15 +02:00
Kacper Ziubryniewicz
c6dc432d06 Move on methods to the right place 2018-09-22 20:39:41 +02:00
Kacper Ziubryniewicz
9e8fe7bc1e Fix Python 2.7 compability 2018-09-15 11:34:16 +02:00
Kacper Ziubryniewicz
90813c959d Added get_url_parameters util method 2018-09-15 11:21:35 +02:00
Kacper Ziubryniewicz
940a65954c Read commit description
Added:
- Detecting extensible attachments
- Fetching live user location
- New methods for message reacting
- New `on` methods: `onReactionAdded`, `onReactionRemoved`, `onBlock`, `onUnblock`, `onLiveLocation`
- Fixed `size` of attachments
2018-09-12 17:52:38 +02:00
Kacper Ziubryniewicz
9b4e753a79 Added graphql methods for extensible attachments 2018-09-12 17:48:35 +02:00
Kacper Ziubryniewicz
e0be9029e4 Added extensible attachments models 2018-09-12 17:48:00 +02:00
Kacper Ziubryniewicz
0ae213c240 Merge pull request #1 from carpedm20/master
Merge `master`
2018-09-12 17:41:53 +02:00
Mads Marquart
08117e7a54 Fixed examples, see #332
The examples were using generator expressions instead of list comprehensions
2018-09-09 14:24:20 +02:00
Mads Marquart
51c3226070 Merge pull request #326 from kapi2289/merge_rooms
Merge `Room` with `Group`
2018-09-09 14:09:36 +02:00
Mads Marquart
5396d19d7d Merge pull request #327 from kapi2289/fix_active
`markAlive` fix
2018-09-09 14:07:48 +02:00
Kacper Ziubryniewicz
11501e6899 Fix Room model initialization 2018-09-03 15:05:11 +02:00
Kacper Ziubryniewicz
4eb49b9119 Backwards compability for Rooms 2018-08-31 13:25:37 +02:00
Kacper Ziubryniewicz
4c2da22750 markAlive fix 2018-08-30 20:28:32 +02:00
Kacper Ziubryniewicz
753b9cbae2 Merge Room with Group methods 2018-08-30 19:57:47 +02:00
Kacper Ziubryniewicz
2c73cabe22 Merge Room with Group graphql methods 2018-08-30 19:57:12 +02:00
Kacper Ziubryniewicz
d6ca091b7b Merge Room with Group model 2018-08-30 19:56:18 +02:00
Mads Marquart
f0e849e9c0 Version up, thanks to @kapi2289, @gave92, @ThatAlexanderA and @1ttric 2018-08-30 00:08:27 +02:00
Mads Marquart
ddcbd6a790 Merge pull request #318 from kapi2289/master
Bunch of new methods, bunch of fixes, bunch of tests
2018-08-29 23:55:17 +02:00
Mads Marquart
28e3b6285e Made mute methods raise if they errored 2018-08-29 23:51:33 +02:00
Mads Marquart
348db90f7b Fixes for Python 2.7 compatibility 2018-08-29 23:50:35 +02:00
Mads Marquart
0d780b9b80 Added tests for plans 2018-08-29 21:31:28 +02:00
Mads Marquart
8ab718becd Added poll tests 2018-08-29 16:49:33 +02:00
Kacper Ziubryniewicz
1943c357fa Message searching rebuild
Changed message searching methods to return generators and added `search`
2018-08-29 15:14:26 +02:00
Mads Marquart
3be0d8389b Changed changeThreadImageX to changeGroupImageX 2018-08-29 14:37:29 +02:00
Kacper Ziubryniewicz
d7d1c83276 MessageReactionFix is not needed anymore 2018-08-29 14:33:48 +02:00
Mads Marquart
8591e2ffd5 Fixed createGroup implementation 2018-08-29 14:08:11 +02:00
Mads Marquart
c2225bf2fd Added more tests 2018-08-29 14:07:44 +02:00
Mads Marquart
0617d7b49f Fixed _usersApproval, fixed changeThreadImage methods, more tests 2018-08-29 12:17:16 +02:00
Mads Marquart
42b288ee98 Fixed onAdminRemoved and onAdminAdded, and added tests for that 2018-08-29 11:15:59 +02:00
Mads Marquart
ead7203e40 Added tests for fetchMessageInfo 2018-08-29 11:03:46 +02:00
Mads Marquart
bd2b947255 More test improvements 2018-08-29 10:14:18 +02:00
Mads Marquart
f367bd2d0d Improved test setup 2018-08-29 10:12:10 +02:00
Kacper Ziubryniewicz
a8ce44b109 Added searching for messages in all threads 2018-08-27 19:37:49 +02:00
Kacper Ziubryniewicz
3b43d3f0bd Few fixes 2018-08-27 14:08:19 +02:00
Kacper Ziubryniewicz
06da486140 Backwards compability for plans/event reminders 2018-08-24 21:56:31 +02:00
Kacper Ziubryniewicz
a24a7d5636 Small documentation fix 2018-08-23 21:10:47 +02:00
Mads Marquart
bc197fd665 Changed sendXFiles to only needing file url / path 2018-08-23 20:38:55 +02:00
Kacper Ziubryniewicz
e35cc71cf4 Fix plan fetching from threads 2018-08-23 12:17:22 +02:00
Kacper Ziubryniewicz
7aa774b4ef Update utils.py 2018-08-20 23:12:36 +02:00
Kacper Ziubryniewicz
9bb2de79fa Update client.py 2018-08-20 23:12:10 +02:00
Kacper Ziubryniewicz
21246144ab Update client.py 2018-08-20 17:09:18 +02:00
Kacper Ziubryniewicz
0e0845914b Update graphql.py 2018-08-20 16:57:37 +02:00
Kacper Ziubryniewicz
778e827277 Update models.py 2018-08-20 16:57:10 +02:00
Kacper Ziubryniewicz
f36d4fa38d client - Event to Plan 2018-08-19 15:28:22 +02:00
Kacper Ziubryniewicz
5b89c2d504 utils - Event to Plan 2018-08-19 15:25:02 +02:00
Kacper Ziubryniewicz
49b213bb2d graphql - Event to Plan 2018-08-19 15:24:28 +02:00
Kacper Ziubryniewicz
aed75c7d1b Changed Event model to Plan 2018-08-19 15:23:44 +02:00
Mads Marquart
ac51e4e4d5 Removed trailing whitespace 2018-08-13 21:28:17 +02:00
Kacper Ziubryniewicz
d8d84ae629 Fix event_reminders for pages 2018-08-11 14:29:31 +02:00
Kacper Ziubryniewicz
3f75f8ed31 Added markAsSpam 2018-08-10 12:03:14 +02:00
Kacper Ziubryniewicz
8aef4dc2ec Added mark as spam request 2018-08-10 12:02:47 +02:00
Kacper Ziubryniewicz
b1e7ec706b Fix event_reminders 2018-08-10 10:03:51 +02:00
Kacper Ziubryniewicz
b5cd780360 Added message searching 2018-08-10 09:09:17 +02:00
Kacper Ziubryniewicz
a8da94ee6d Added request for message searching 2018-08-10 09:08:34 +02:00
Kacper Ziubryniewicz
f564c732d4 Added event reminder methods 2018-08-09 20:05:59 +02:00
Kacper Ziubryniewicz
8beb1e5753 Update graphql.py 2018-08-09 20:04:20 +02:00
Kacper Ziubryniewicz
d98d802a33 New Event model 2018-08-09 20:02:45 +02:00
Kacper Ziubryniewicz
d750f29fad New event reminder requests 2018-08-09 20:01:52 +02:00
Kacper Ziubryniewicz
f425d32846 Added poll methods 2018-08-05 22:15:42 +02:00
Kacper Ziubryniewicz
043d6b492d Fix in new graphql methods 2018-08-05 22:09:03 +02:00
Kacper Ziubryniewicz
0bcccfa65e Added graphql_to_poll and graphql_to_poll_option 2018-08-05 22:01:43 +02:00
Kacper Ziubryniewicz
0716b1b8d8 Added requests for poll events 2018-08-05 21:58:48 +02:00
Kacper Ziubryniewicz
47168e682d Added Poll and PollOption models 2018-08-05 21:56:32 +02:00
Kacper Ziubryniewicz
718d864dc8 Added file, video and audio sending 2018-08-04 00:43:36 +02:00
Kacper Ziubryniewicz
22a691ec0f Fix waveToThread 2018-08-03 21:55:06 +02:00
Kacper Ziubryniewicz
dfcc826b7e Added waveToThread and markAsUnread 2018-08-02 23:31:35 +02:00
Kacper Ziubryniewicz
d1ee664ef5 Added deleteMesseges request url 2018-08-01 22:55:42 +02:00
Kacper Ziubryniewicz
abcc6518bb Added deleteMessages method 2018-08-01 22:53:48 +02:00
Kacper Ziubryniewicz
2ef9ec3358 Added call events
Added onCallStarted, onCallEnded and onUserJoinedCall but this methods are for group calls only. I can't find how to fetch private call start, I found only how to fetch private call end.
2018-07-31 23:16:45 +02:00
Kacper Ziubryniewicz
f84cf3bf2d Added fetchMessageInfo by mid and thread_id
Added fetchMessageInfo and fixed onImageChange when removing thread image
2018-07-31 20:12:24 +02:00
Kacper Ziubryniewicz
bdcc2d2fa4 Added acceptUsersToGroup and denyUsersFromGroup 2018-07-31 13:23:35 +02:00
Kacper Ziubryniewicz
7e8e7f15a4 Update client.py 2018-07-31 12:09:03 +02:00
Kacper Ziubryniewicz
1ca3ad6237 Forgot about thread_type in new methods. Added it! 2018-07-31 11:56:43 +02:00
Kacper Ziubryniewicz
f3c878d949 Update client.py 2018-07-31 11:48:25 +02:00
Kacper Ziubryniewicz
ee0c30ebb1 Update utils.py 2018-07-31 11:33:20 +02:00
Kacper Ziubryniewicz
c2f0c908d9 Added thread muting 2018-07-31 11:30:41 +02:00
kapi2289
3edaaa0400 Added deleteThreads
Added deleteThreads and made few fixes
2018-07-31 10:40:10 +02:00
kapi2289
21a443baf2 Update client.py 2018-07-31 00:03:19 +02:00
kapi2289
f6f47b5500 Merge branch 'master' into master 2018-07-29 15:20:12 +02:00
Mads Marquart
920c724656 Merge pull request #317 from gave92/master
Fix 2FA for non-English users
2018-07-28 19:34:39 +02:00
Mads Marquart
e50b814e07 Merge pull request #316 from ThatAlexanderA/patch-1
Added `createGroup`
2018-07-28 19:32:55 +02:00
kapi2289
2294082168 Documentation fix #2 2018-07-20 15:24:18 +02:00
kapi2289
2661a28936 Multiple admins adding/removing
Changed
addGroupAdmin, removeGroupAdmin
to
addGroupAdmins, removeGroupAdmins
2018-07-20 12:42:18 +02:00
kapi2289
31a6834b1f Documentation fix 2018-07-20 12:01:05 +02:00
kapi2289
f66d98bcfe Wrong change #2 2018-07-20 11:56:39 +02:00
kapi2289
ed7466621f Wrong change 2018-07-20 11:51:03 +02:00
kapi2289
ead450aeb8 Update utils.py 2018-07-19 17:38:04 +02:00
kapi2289
d934cefa8b New methods and few fixes
Added: addGroupAdmin, removeGroupAdmin, changeGroupApprovalMode, blockUser, unblockUser, moveThread, onImageChange, onAdminsAdded, onAdminsRemoved, onApprovalModeChange
I did this all day, because I love this library and I want to be part of it :D
2018-07-19 17:36:54 +02:00
kapi2289
41807837b8 Small typo fix 2018-07-16 21:46:58 +02:00
Marco Gavelli
4419c816f5 Fix 2FA for non english FB 2018-07-15 12:37:20 +02:00
ThatAlexanderA
4993da727a Added create group url 2018-07-14 12:42:18 +02:00
ThatAlexanderA
86a163e337 Added create group def 2018-07-14 12:40:42 +02:00
Mads Marquart
c2fb602bee Disabled travis pytest caching, now the tests should be pretty stable 2018-07-12 17:42:34 +02:00
Mads Marquart
f565d6f31a Merge pull request #311 from kapi2289/master
Fixed changeThreadTitle and added changeThreadImage
2018-07-12 16:47:23 +02:00
kapi2289
5af01bb8ff Added documentation 2018-07-08 14:37:44 +02:00
kapi2289
714e783e0d Update client.py 2018-07-07 22:39:02 +02:00
Mads Marquart
fb1b0afddb Merge pull request #306 from carpedm20/improve_community_profile
Improve community profile
2018-07-07 15:36:43 +02:00
kapi2289
e6fdc56d25 Update utils.py 2018-07-03 23:14:48 +02:00
kapi2289
5b965e63f8 Update client.py 2018-07-03 23:13:47 +02:00
Mads Marquart
af86550e71 Merge pull request #307 from 1ttric/master
Fix: Name edge case results in IndexError
2018-07-02 14:07:11 +02:00
Will Vesey
e57ae069a7 Fix name edge case 2018-06-27 13:54:45 -04:00
Mads Marquart
39adc646e6 Revert adding FBchatRedirectError 2018-06-27 11:14:55 +02:00
Mads Marquart
0947e77082 Fixed FBchatRedirectError 2018-06-27 11:07:16 +02:00
Mads Marquart
637b0ded09 Added FBchatRedirectError 2018-06-27 10:45:11 +02:00
Mads Marquart
9b7a84ea45 Added more debug info, to fix a wierd bug 2018-06-26 10:40:01 +02:00
Mads Marquart
ead696cbad Attempted to improve TravisCI online tests 2018-06-24 12:20:17 +02:00
Mads Marquart
da23ad5eb5 Merge branch 'test_travis_config' 2018-06-21 21:42:54 +02:00
Mads Marquart
b63a0dfa01 Made the offline tests colorful ;) 2018-06-21 21:38:52 +02:00
Mads Marquart
6c00724a84 Removed unnecessary env 2018-06-21 21:30:58 +02:00
Mads Marquart
7619224809 Removed travis_fold test 2018-06-21 21:29:11 +02:00
Mads Marquart
e0d3dd9050 New TravisCI setup, using build stages 2018-06-21 21:13:17 +02:00
Mads Marquart
71bf5e0e4f Added CONTRIBUTING.rst 2018-06-21 17:12:01 +02:00
Mads Marquart
540e530420 Added Contributor Covenant Code of Conduct 2018-06-21 17:11:46 +02:00
Mads Marquart
070a8cad15 Removed wrong templates 2018-06-21 16:52:21 +02:00
Mads Marquart
5d094b38b0 Merge pull request #305 from carpedm20/issue_templates
Add issue templates via. Github's `Create Issue Template` feature
2018-06-21 16:42:25 +02:00
Mads Marquart
af3d385ff5 Add issue templates via. Github's Create Issue Template feature 2018-06-21 16:41:27 +02:00
Mads Marquart
c352a0d698 Modified license, so it's correctly recognised by licensee
It _should_ be okay, since the modified version is less permissive
The only real addition is `Neither the name of the copyright holder nor`
2018-06-21 15:36:54 +02:00
Mads Marquart
060f64b4d2 Rename LICENSE.txt to LICENSE 2018-06-21 15:29:52 +02:00
Mads Marquart
4f032cd946 Fixed a few exception values, see #303 2018-06-21 15:23:43 +02:00
Mads Marquart
cee6039ec3 Prevent builds from failing the deploy [ci skip]
Every job runs the build stage, which is fine, since we need the different `wheel` packages, but they failed, since the files were already present on PyPI
2018-06-20 16:54:03 +02:00
Mads Marquart
c8f8b818e0 Version up, thanks to @orenyomtov and @ ThatAlexanderA
* Added `removeFriend` method, #298
* Removed `lxml` from dependencies, #301
* Moved configuration to setup.cfg instead of setup.py
2018-06-20 15:53:57 +02:00
Mads Marquart
08922ae284 Moved Travis account configuration into Travis Settings 2018-06-20 14:29:43 +02:00
Mads Marquart
51d606a54e Merge pull request #298 from ThatAlexanderA/master
Added remove friend
2018-06-20 14:29:00 +02:00
Mads Marquart
2b76d71c67 Merge branch 'master' into alexander_master 2018-06-20 13:51:32 +02:00
Mads Marquart
67edd19eb8 Small formatting fixes 2018-06-20 13:51:12 +02:00
Mads Marquart
eaaa526cfc Merge pull request #301 from orenyomtov/patch-4
Replace lxml with Python's built in html.parser
2018-06-20 13:46:56 +02:00
Mads Marquart
843c0f6c37 Merge branch 'master' into patch-4 2018-06-20 13:38:59 +02:00
Mads Marquart
44ebf38e47 Updated setup.py and requirements, now we use setup.cfg 2018-06-20 13:35:56 +02:00
Mads Marquart
d640e7d2ea Enabled pypy and pytest session caching, updated README 2018-06-19 13:49:10 +02:00
Oren
66736519ed Remove lxml dependency 2018-06-14 16:20:57 +03:00
Oren
73f4c98be9 Remove lxml dependency 2018-06-14 16:20:35 +03:00
Oren
b2ff7fefaa Replace lxml with Python's built in html.parser 2018-06-14 16:19:09 +03:00
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
Mads Marquart
32c72c2f35 Version up, thanks to @Dante383 2017-12-10 20:08:13 +01:00
Dante
42ae0035af typo in function name
checkRequest --> check_request
2017-12-10 14:16:17 +01:00
Mads Marquart
96e28fdbe6 Fixed error when recieving share attachments 2017-11-16 15:14:46 +01:00
Taehoon Kim
0f889f50cf Update README.rst 2017-11-14 17:25:01 +09:00
Mads Marquart
478eaebdec Removed copyright icon from README.rst, fixing #219 2017-10-21 18:47:12 +02:00
Mads Marquart
7ecf229db5 See commit description
- Deprecated `sendMessage` and `sendEmoji` in favor of `send`
- (Almost) Fully integrated attachment support
- Updated tests
- General cleanup
2017-10-21 17:59:44 +02:00
Mads Marquart
dda75c6099 Merge remote-tracking branch 'svaikstude/feature/@mention' 2017-10-21 12:10:58 +02:00
Mads Marquart
28d5ac9f90 Merge branch 'attachment-support' 2017-10-21 12:09:52 +02:00
Mads Marquart
52acfb4636 Removed json from main client.py 2017-10-21 11:58:57 +02:00
Mads Marquart
2a64bad385 Merge branch 'feature/eventReminder' 2017-10-21 11:49:37 +02:00
Mads Marquart
1a73699f1a Merge branch 'mutable-default' 2017-10-21 11:48:06 +02:00
Mads Marquart
1b5a7a0063 Merge branch 'rooms' 2017-10-21 11:46:28 +02:00
ekohilas
4b3eb440cf fixed missing get 2017-10-20 03:17:26 +11:00
ekohilas
d1f457866b fixed dict typo 2017-10-20 03:08:27 +11:00
ekohilas
6f29aa82cb fixed class mistype 2017-10-15 15:15:56 +11:00
ekohilas
b1a2ff7d84 updated for python2.7 2017-10-15 03:56:09 +11:00
ekohilas
883b16e251 fixed simple merge 2017-10-08 03:32:20 +11:00
ekohilas
116b39cf6a fixed superclass init error 2017-10-08 03:25:25 +11:00
ekohilas
eae1db9c7d removed list and rstrip 2017-10-08 03:25:25 +11:00
ekohilas
730bab5d40 added rooms under thread_info 2017-10-08 03:25:10 +11:00
ekohilas
d52dac233e made appropriate changes to default args of rooms 2017-10-08 03:22:15 +11:00
ekohilas
1f37277a8d started adding rooms 2017-10-08 03:22:15 +11:00
ekohilas
15014d7055 merging from upstream
Merge remote-tracking branch 'upstream/master'
2017-10-07 15:06:31 +11:00
ekohilas
7a35ca05b1 fixed all mutable default argument gotchas 2017-10-07 14:15:45 +11:00
svaikstude
be6b6909d9 Update client.py 2017-10-06 17:05:09 +02:00
Manvydas Kriauciunas
42c1d26b2e new feature to support @mention in sendMessage 2017-10-06 16:31:25 +02:00
Manvydas Kriauciunas
d38f8ad2ec new feature eventReminder added 2017-10-06 16:23:47 +02:00
Mads Marquart
023fd58f05 Version up, thanks to @ekohilas 2017-10-03 22:46:04 +02:00
Mads Marquart
ad10a8f07f Merge pull request #213 from ekohilas/requirements_fix
Updates setup.py for better compatibility with setuptools
2017-10-03 22:44:56 +02:00
Mads Marquart
7d6cf039d4 Merge branch 'master' into requirements_fix 2017-10-03 22:44:03 +02:00
ekohilas
f0271e17b0 updated for older setuptools 2017-10-04 01:57:02 +11:00
Mads Marquart
57954816b2 Version up, thanks to @WeiTang114 2017-10-03 08:29:34 +02:00
Mads Marquart
3e4e1f9bb9 Merge pull request #212 from WeiTang114/gif_support_2
Add Gif support to send(Local/Remote)Image
2017-10-03 08:26:10 +02:00
Mads Marquart
7340918209 Merge pull request #211 from WeiTang114/fetch_pending_thread_2
Enable fetching pending/archived threads
2017-10-03 08:25:58 +02:00
Tang
707df4f941 use mimetype to see if it's a GIF
thanks to @madsmtm's good idea
2017-10-03 03:29:15 +08:00
Tang
8eb6b83411 Update for feedback by @madsmtm
1. Add ThreadLocation Enum in models.
2. avoid using build-in name "type" as parameter name
3. replace ValueError with FBchatUserError

thanks to @madsmtm
2017-10-03 03:05:08 +08:00
Tang
e0aedd617b add param is_gif to doc of functions 2017-10-03 01:40:19 +08:00
Tang
ee81620c14 Add send GIF images support
When uploading and sending GIF images, the keys are explicitly changed
to "gif_id" or "gif_ids" rather than "image_id" or "image_ids".
2017-10-03 01:39:20 +08:00
Tang
2d027af71a Enable fetching pending/archived threads
Add "type" parameter to fetchThreadList().
type can be 'inbox', 'pending' or 'archived'

If set to 'pending', it can fetch messages from unknown users.
It is quite useful to build a service accepting requests from anyone.

For example, in doOneListen(), fetch pending messages once for a while
to handle the messages from strangers.
2017-10-03 01:37:25 +08:00
Mads Marquart
9d5f06b810 Fixed pip setup 2017-09-30 19:17:40 +02:00
Mads Marquart
b8fdcda2fb Properly uploading requirements (pip requires changed version number) 2017-09-30 01:15:41 +02:00
Mads Marquart
0dac7b7b81 Version up, thanks to @ekohilas 2017-09-27 21:20:20 +02:00
Mads Marquart
b750e753d6 Merge pull request #206 from ekohilas/master
Fixes 2FA bug and updates pip requirements
2017-09-27 21:19:14 +02:00
ekohilas
ee33e92bed added conditional enum34 2.7 requirement 2017-09-27 19:24:35 +10:00
ekohilas
7413a643f6 fixed 2FA bug 2017-09-27 19:23:58 +10:00
Mads Marquart
34452f9220 Changed API description 2017-09-26 16:49:10 +02:00
Mads Marquart
24831b2462 Merge branch 'master' into attachment-support 2017-09-26 16:45:47 +02:00
Mads Marquart
cd4a18cb5a Version up 2017-09-25 20:02:35 +02:00
Mads Marquart
c00b3df8b2 Merge pull request #201 from madsmtm/improved-stability
Possibly fixes #175, added custom Exception classes
2017-09-25 20:01:23 +02:00
Mads Marquart
1beb821b2c Added function to fetch url from image id
Fixes #84
2017-09-21 23:58:50 +02:00
Mads Marquart
a58791048a Added attachment and mention support in onMessage
Deprecated `message` in `onMessage`
2017-09-21 22:32:11 +02:00
Mads Marquart
f0c6e8612f Fixed typo and made name more generic 2017-09-21 10:09:48 +02:00
Mads Marquart
1cebbf92e6 Fixed loading sessions 2017-09-20 11:31:44 +02:00
Mads Marquart
a64982583b Fixes 502/503 errors and a the 1357004 error
Thereby also moving ReqUrl to self.req_url
2017-09-19 23:08:48 +02:00
Mads Marquart
cb8b0915de Improved default doOneListen loop 2017-09-19 16:42:03 +02:00
Mads Marquart
1d2576b06d More custom exceptions 2017-09-19 16:36:24 +02:00
Mads Marquart
ead9a3c0e9 Improved error handling, and improved uid-loading
Requests would sometimes throw an error while retrieving the c_user cookie (If there were multiple cookies with this name)
2017-09-19 16:36:08 +02:00
Mads Marquart
59ba418faa Added custom exceptions
Added `FBchatException`, `FBchatFacebookError` and `FBchatUserError`, which can help in differentiating between errors
2017-09-19 16:31:53 +02:00
Mads Marquart
c51a332560 Version up, thanks to @PythonNut 2017-08-27 23:03:50 +02:00
Mads Marquart
a73d2feed6 Merge pull request #193 from PythonNut/master
Fix UNKNOWN gender in graphql requests
2017-08-27 23:02:05 +02:00
PythonNut
6929193e9d Fix UNKNOWN gender in graphql requests 2017-08-13 23:10:11 +00:00
Mads Marquart
fea4ad9e89 Version up, Thanks to ritu99 2017-08-10 15:25:38 +02:00
Mads Marquart
68099049d4 Merge pull request #189 from ritu99/master
Added Message Count to thread information
2017-08-10 15:22:22 +02:00
Ritvik Annam
44cf08bdfd fetchThreadInfo now pulls message_count 2017-08-10 01:15:29 -05:00
Ritvik Annam
9e32cf17a4 fetchThreadList now pulls message_count 2017-08-10 00:53:06 -05:00
Mads Marquart
0661367ebb Properly fixed #182 2017-08-02 23:08:34 +02:00
Mads Marquart
3c07e42ba2 Version up, fixed #182 2017-07-26 23:13:19 +02:00
Mads Marquart
2cd6376818 Merge pull request #178 from Bankde/fix-fail-after-running-for-days
Fix issue when running for long time
2017-07-26 23:03:49 +02:00
Mads Marquart
5e7f7750de Fixed enums in python 2.7, thanks to @liamkirsh 2017-07-12 14:52:15 +02:00
Bankde@hotmail.com
2a223ec6db fix array indexing (I don't know why fb do that) 2017-07-10 10:25:23 +07:00
Mads Marquart
a99108fff6 Version up thanks to @Bankde 2017-07-09 20:55:06 +02:00
Mads Marquart
8de4698cc4 Merge pull request #174 from Bankde/fix-error-in-python2
No FileNotFoundError in py2
2017-07-09 20:53:47 +02:00
Bankde@hotmail.com
637319ec2c add token update 2017-07-10 00:51:51 +07:00
Bankde@hotmail.com
f9398564cd replace FileNotFoundError with IOError so it can work in Py2 2017-07-05 09:18:13 +07:00
Mads Marquart
b57f423eb4 Version up thanks to @aaronlewism 2017-07-01 12:48:02 +02:00
Mads Marquart
3093f1f2b6 Merge pull request #173 from aaronlewism/master
Check for alternate 2Factor page text
2017-07-01 12:46:11 +02:00
Aaron Lewis
961777e0c1 Check for alternate 2Factor page text 2017-06-29 13:21:25 -07:00
Mads Marquart
d7139701f7 Fixed typo, improved formatting. Thanks to @JarbasAI! 2017-06-29 20:04:01 +02:00
Mads Marquart
c6bac17d48 Merge pull request #172 from JarbasAI/patch-1
Add on chat presence event
2017-06-29 19:55:49 +02:00
Mads Marquart
3638fc5356 Made fetchThreadInfo able to fetch own user's info 2017-06-29 19:53:29 +02:00
Jarbas
aca9176f7f Add on chat presence event
Last_seen time stamps were handled in unknown message type, this info is freely available and potentially useful
2017-06-29 17:56:14 +01:00
Mads Marquart
0d5e4f6d3f Version up thanks to @enwar3 2017-06-29 16:03:55 +02:00
Mads Marquart
92a5ffdef8 Merge pull request #170 from OMGWINNING/master
Add extensible_attachment field to Message for fb share objects
2017-06-29 16:02:27 +02:00
Joe Lau
b3359fccdb Add last_message_timestamp to Thread objects 2017-06-28 18:08:45 -07:00
Joe Lau
d8f7366d1f Add extensible_attachment field to Message for fb share objects 2017-06-28 13:19:17 -07:00
Mads Marquart
ff94dc20af Minor cleanup 2017-06-28 16:06:13 +02:00
Mads Marquart
a8df0a548f Minor fixes 2017-06-28 14:42:11 +02:00
Mads Marquart
13d0dc4ba4 Fixed ChangeThreadTitle and ThreadColor.MESSENGER_BLUE 2017-06-28 14:30:29 +02:00
Mads Marquart
64125a1aca Updated to 1.0.6, thanks to @enwar3 2017-06-28 10:24:44 +02:00
Mads Marquart
4feae03092 Merge pull request #169 from OMGWINNING/master
Handle empty participant_customizations field
2017-06-28 10:23:33 +02:00
Joe Lau
5f993c2bf8 Use .get() instead 2017-06-27 16:16:51 -07:00
Joe Lau
35bbcbffba Add __init__.py 2017-06-26 17:54:25 -07:00
Joe Lau
5faca54d67 Handle empty participant_customizations field 2017-06-26 14:16:57 -07:00
Mads Marquart
82496b8e04 Minor fixes 2017-06-26 17:02:32 +02:00
Mads Marquart
2d74ec7823 Made getAllUsers more stable 2017-06-26 15:42:26 +02:00
Mads Marquart
1d42c4d3a6 Updated to 1.0.4, added fetchThread&GroupInfo and improved models 2017-06-26 15:41:58 +02:00
Mads Marquart
4a8ef00442 Fixed a few bugs, updated to v. 1.0.3 2017-06-26 11:37:54 +02:00
Mads Marquart
add06ffa7a I was having trouble with PyPI ;) 2017-06-22 23:35:28 +02:00
Mads Marquart
fbb8d8e24a Update README.rst 2017-06-22 22:54:12 +02:00
Mads Marquart
cd0e001219 Update README.rst 2017-06-22 22:50:10 +02:00
Mads Marquart
bf53f4fc74 Updated to 1.0.0 2017-06-22 22:43:26 +02:00
Mads Marquart
11e59e023c Added GraphQL requests 2017-06-22 22:38:15 +02:00
Mads Marquart
c81d7d2bfb Removed deprecations and new event system, improved other things
Removed deprecations
Removed new event system
Added documentation for all events
Added FAQ
Changed Client.uid to Client.id
Improved User model
Prepared for support of pages
2017-06-20 14:57:23 +02:00
Mads Marquart
0885796fa8 Fix for Facebook's changed API, and removed test print statement 2017-06-02 15:59:24 +02:00
Mads Marquart
1279481b62 Update README.rst 2017-05-28 22:38:30 +02:00
Mads Marquart
5fb3412915 Update README.rst 2017-05-28 22:34:18 +02:00
Mads Marquart
c708a5ecf6 Fixed README! 2017-05-28 22:33:50 +02:00
Mads Marquart
e4f29e5f2b Fixed README.rst? 2017-05-28 21:58:00 +02:00
Mads Marquart
779fa409e5 Fixed README.rst (again) 2017-05-28 21:56:58 +02:00
Mads Marquart
fc8c2dfa14 Fixed README.rst 2017-05-28 21:52:54 +02:00
Mads Marquart
9f7d308961 Added requirements.txt 2017-05-28 21:19:31 +02:00
Mads Marquart
8dacc37ba9 More documentation work, changed addUsersToGroup back to taking a list of user IDs
Created new README, and finished `intro`
2017-05-28 21:11:16 +02:00
Mads Marquart
39eafa5a3e Fixed examples, added changeNickname and changeThreadEmoji, changed changeGroupTitle back to changeThreadTitle
I also removed the parameter `set_default_events` from __init__, since
it's not really necessary
Also added testing of examples and simple testing of listen functions
2017-05-26 18:48:37 +02:00
Mads Marquart
d2741ca419 Added baseline for sphinx documentation and on2FACode event
The docs are still very WIP, but they should be functional. Just
execute `make html` in the docs folder, and you should be able to
navigate to `/docs/_build/html` and view it in your browser
2017-05-26 13:38:54 +02:00
Mads Marquart
a76ebbb22a Added python 2.7 support, reworked events
- Reworked events, so now they support python 2.7 (I had to remove some
functionality though, but that was a little unnecessary anyway)
- Events now support the old style of writing, for people who's more
comfortable with that: ```python
class EchoBot(fbchat.Client):
    def onMessage(self, *args, **kwargs):
        self.something(*args, **kwargs)
```
While still supporting the new method:
```python
class EchoBot(fbchat.Client):
    def __init__(self, *args, **kwargs):
         super(EchoBot, self).__init__(*args, **kwargs)
         self.onMessage += lamda *args, **kwargs: self.something(*args,
**kwargs)
```
- Included `msg` as a parameter in every event function, since it's
useful if you want to extract some of the other data
- Moved test data to the folder `tests`
- Fixed various other functions, and improved stability
2017-05-22 20:33:00 +02:00
Mads Marquart
83a45ebc03 Changed names with Chat to Group or Thread respectively, improved error handling, and changed _doSendRequest to return a single message id 2017-05-21 23:12:30 +02:00
Mads Marquart
76c2c65a7b Fixed sendLocalImage, changed get_json, improved tests
- Changed get_json to take a `requests` response, and then return the
json (While checking encoding and removing unnecessary characters)
- Fixed sendLocalImage, the problem was that the `_getThread` call was
missing a parameter (Took me hours ;) )
- Removed 3 second delay between tests, I felt it was unnecessary
- Updated tests to no longer use deprecated functions
2017-05-21 21:56:56 +02:00
Mads Marquart
99a7d0d534 Added removed in v. x warning to deprecations, and improved login error messages 2017-05-18 20:08:11 +02:00
Mads T Marquart
8e68531ce4 Merge pull request #145 from Dainius14/dev
Add more events and send methods, some fixes
2017-05-17 16:19:32 +02:00
Dainius
ed7b8488cb rename _setThread() to _getThreadId() 2017-05-17 14:20:07 +03:00
Dainius
386cb4a6c1 fix on seen, on delivered, on marked as seen methods 2017-05-16 21:20:46 +03:00
Dainius
c95544dcb0 add typing indicator 2017-05-16 19:35:00 +03:00
Dainius
4083348c40 add reaction to messages. move request URLs to utils 2017-05-16 19:20:46 +03:00
Dainius
b1cccf4173 fix emoji sending and tests
My bad. Test data is still being commited, changed it to a sample file instead
2017-05-16 14:02:09 +03:00
Dainius
e1e1a0d611 add thread color change 2017-05-16 12:03:20 +03:00
Mads T Marquart
fb88f8d459 Merge pull request #142 from Torxed/master
Changed traceback.print_exc() to logging.exception
2017-05-15 15:50:15 +02:00
Mads T Marquart
0fdab3968d Merge branch 'development' into master 2017-05-15 15:49:14 +02:00
Lord Anton Hvornum
ac0e72d167 There's no such thing as 'unicode' in Py3 2017-05-11 21:23:48 +02:00
Lord Anton Hvornum
e8fbaefa72 There's no such thing as 'unicode' in Py3 2017-05-11 21:23:18 +02:00
Lord Anton Hvornum
de21eafe7b Swapped out for a better error output. You had no idea where errors occured before. 2017-05-11 21:18:27 +02:00
Mads T Marquart
44b3b1330a Created test_data.json
See [here](f63b9d7c4a (commitcomment-22103626)) for more info
2017-05-11 17:35:56 +02:00
Mads T Marquart
fd2e554b98 Updated version 2017-05-11 16:29:48 +02:00
Mads T Marquart
d7acb9a40d Changed default emoji size to small
Also updated version
2017-05-11 16:29:41 +02:00
Mads Marquart
f63b9d7c4a Reworked old events, added deprecation warnings, and improved _send
- Added a new system to show an error if old events are used
- Removed `test_data.json`, since I don't want to risk that anyone
accidentally commits their username & password
- Finished work on `sendEmoji`
- Split `_send` into two parts:
`_getSendData` and `_doSendRequest`, which allows for an easier way of
adding new send requests
2017-05-11 12:55:44 +02:00
Mads T Marquart
7a0c64bf9e Merge pull request #141 from Dainius14/dev
reintroduce things skipped on conflict
2017-05-11 10:26:21 +02:00
Dainius
ef352f097a update test_data.json 2017-05-10 18:44:20 +03:00
Dainius
357083efce reintroduce things skipped on conflict 2017-05-10 18:16:41 +03:00
Mads Marquart
0d75c09036 Added support for deprecating items, and maybe support for python 2.7
- Changed `test_data.js` to `test_data.json`
- Added `deprecated` decorator
- Added `deprecation` function
- Readded old functions, and marked them as deprecated
- Changed parameters back to being type-in-specific (support for python
2.x)
- Deprecated `info_log` and `debug` init paramters
2017-05-10 14:54:07 +02:00
Mads T Marquart
58c7e08d12 Merge pull request #140 into development branch from Dainius14/dev 2017-05-10 11:10:16 +02:00
Dainius
b5443daeb1 Merge branch 'development' into dev 2017-05-10 12:02:39 +03:00
Dainius
5da3e5e4bf update tests 2017-05-09 21:27:32 +03:00
Dainius
f4dec2e48e update send methods 2017-05-09 10:25:04 +03:00
63 changed files with 6666 additions and 1117 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`

13
.gitignore vendored
View File

@@ -1,14 +1,18 @@
*py[co]
.idea/
# Test scripts
*.sh
# Packages
*.egg
*.egg-info
*.dist-info
dist
build
eggs
.eggs
parts
bin
var
@@ -22,5 +26,12 @@ develop-eggs
# Sphinx documentation
docs/_build/
# Data for tests
# Scripts and data for tests
my_tests.py
my_test_data.json
my_data.json
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
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
and/or other materials provided with the distribution.
* The names of its contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
* Neither the name of the copyright holder nor the names of its
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"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

View File

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

View File

@@ -1,119 +1,47 @@
======
fbchat
======
fbchat: Facebook Chat (Messenger) for Python
============================================
.. image:: https://img.shields.io/badge/license-BSD-blue.svg
:target: https://github.com/carpedm20/fbchat/tree/master/LICENSE
:alt: License: BSD 3-Clause
Facebook Chat (`Messenger <https://www.messenger.com/>`__) for Python. This project was inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__.
.. 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
:alt: Supported python versions: 2.7, 3.4, 3.5, 3.6 and pypy
**No XMPP or API key is needed**. Just use your ID and PASSWORD.
.. image:: https://readthedocs.org/projects/fbchat/badge/?version=master
:target: https://fbchat.readthedocs.io
:alt: Documentation
.. image:: https://travis-ci.org/carpedm20/fbchat.svg?branch=master
:target: https://travis-ci.org/carpedm20/fbchat
:alt: Travis CI
Installation
============
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>`__.
Simple:
**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,
or jump right into the code by viewing the `examples <https://github.com/carpedm20/fbchat/tree/master/examples>`__
Installation:
.. code-block:: console
$ pip install fbchat
You can also install from source, by using `setuptools` (You need at least version 30.3.0):
Example
=======
.. code-block:: console
.. code-block:: python
import fbchat
client = fbchat.Client("YOUR_ID", "YOUR_PASSWORD")
$ git clone https://github.com/carpedm20/fbchat.git
$ cd fbchat
$ python setup.py install
Sending a Message
=================
Maintainer
----------
.. code-block:: python
friends = client.getUsers("FRIEND'S NAME") # return a list of names
friend = friends[0]
sent = client.send(friend.uid, "Your Message")
if sent:
print("Message sent successfully!")
# IMAGES
client.sendLocalImage(friend.uid,message='<message text>',image='<path/to/image/file>') # send local image
imgurl = "http://i.imgur.com/LDQ2ITV.jpg"
client.sendRemoteImage(friend.uid,message='<message text>', image=imgurl) # send image from image url
Getting user info from user id
==============================
.. code-block:: python
friend1 = client.getUsers('<friend name 1>')[0]
friend2 = client.getUsers('<friend name 2>')[0]
friend1_info = client.getUserInfo(friend1.uid) # returns dict with details
both_info = client.getUserInfo(friend1.uid,friend2.uid) # query both together, returns list of dicts
friend1_name = friend1_info['name']
Getting last messages sent
==========================
.. code-block:: python
last_messages = client.getThreadInfo(friend.uid, last_n=20)
last_messages.reverse() # messages come in reversed order
for message in last_messages:
print(message.body)
Example Echobot
===============
.. code-block:: python
import fbchat
#subclass fbchat.Client and override required methods
class EchoBot(fbchat.Client):
def __init__(self,email, password, debug=True, user_agent=None):
fbchat.Client.__init__(self,email, password, debug, user_agent)
def on_message(self, mid, author_id, author_name, message, metadata):
self.markAsDelivered(author_id, mid) #mark delivered
self.markAsRead(author_id) #mark read
print("%s said: %s"%(author_id, message))
#if you are not the author, echo
if str(author_id) != str(self.uid):
self.send(author_id,message)
bot = EchoBot("<email>", "<password>")
bot.listen()
Saving session
==========================
.. code-block:: python
session_cookies = client.setSession()
# save session_cookies
Loading session
==========================
.. code-block:: python
client = fbchat.Client(None, None, session_cookies=session_cookies)
# OR
client.setSession(session_cookies)
Authors
=======
Taehoon Kim / `@carpedm20 <http://carpedm20.github.io/about/>`__
- Mads Marquart / `@madsmtm <https://github.com/madsmtm>`__
- Taehoon Kim / `@carpedm20 <http://carpedm20.github.io/about/>`__

2
dev-requirements.txt Normal file
View File

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

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python3.6 -msphinx
SPHINXPROJ = fbchat
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

BIN
docs/_static/find-group-id.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

1
docs/_static/license.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="80" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="80" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h47v20H0z"/><path fill="#007ec6" d="M47 0h33v20H47z"/><path fill="url(#b)" d="M0 0h80v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="23.5" y="15" fill="#010101" fill-opacity=".3">license</text><text x="23.5" y="14">license</text><text x="62.5" y="15" fill="#010101" fill-opacity=".3">BSD</text><text x="62.5" y="14">BSD</text></g></svg>

After

Width:  |  Height:  |  Size: 791 B

1
docs/_static/python-versions.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="154" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="154" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h49v20H0z"/><path fill="#007ec6" d="M49 0h105v20H49z"/><path fill="url(#b)" d="M0 0h154v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="24.5" y="15" fill="#010101" fill-opacity=".3">python</text><text x="24.5" y="14">python</text><text x="100.5" y="15" fill="#010101" fill-opacity=".3">2.7, 3.4, 3.5, 3.6</text><text x="100.5" y="14">2.7, 3.4, 3.5, 3.6</text></g></svg>

After

Width:  |  Height:  |  Size: 825 B

26
docs/_templates/layout.html vendored Normal file
View File

@@ -0,0 +1,26 @@
{% extends '!layout.html' %}
{% block extrahead %}
<script async defer src="https://buttons.github.io/buttons.js"></script>
<!-- Alabaster (krTheme++) Hacks, modified version of Kenneth Reitz' https://github.com/kennethreitz/requests/blob/master/docs/_templates/hacks.html -->
<style type="text/css">
/* Rezzy requires precise alignment. */
img.logo {margin-left: -20px!important;}
/* "Quick Search" should be capitalized. */
div#searchbox h3 {text-transform: capitalize;}
/* Go button should be behind input field */
div.sphinxsidebar div#searchbox input[type="text"] {width: 160px}
div#searchbox form div {display: inline-block;}
/* Make the document a little wider, less code is cut-off. */
div.document {width: 1008px;}
/* Much-improved spacing around code blocks. */
div.highlight pre {padding: 11px 14px;}
/* Remain Responsive! */
@media screen and (max-width: 1008px) {
div.sphinxsidebar {display: none;}
div.document {width: 100%!important;}
/* Have code blocks escape the document right-margin. */
div.highlight pre {margin-right: -30px;}
}
</style>
{% endblock %}

13
docs/_templates/sidebar.html vendored Normal file
View File

@@ -0,0 +1,13 @@
<h3>
<a href="{{ pathto(master_doc) }}">{{ _(project) }}</a>
</h3>
<p>
<a class="github-button" href="https://github.com/carpedm20/fbchat" data-size="large" data-show-count="true" aria-label="Star carpedm20/fbchat on GitHub">Star</a>
</p>
<p>
{{ _(shorttitle) }}
</p>
{{ toctree() }}

44
docs/api.rst Normal file
View File

@@ -0,0 +1,44 @@
.. module:: fbchat
.. highlight:: python
.. _api:
Full API
========
If you are looking for information on a specific function, class, or method, this part of the documentation is for you.
.. _api_client:
Client
------
This is the main class of `fbchat`, which contains all the methods you use to interact with Facebook.
You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening)
.. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO)
:members:
.. _api_models:
Models
------
These models are used in various functions, both as inputs and return values.
A good tip is to write ``from fbchat.models import *`` at the start of your source, so you can use these models freely
.. automodule:: fbchat.models
:members:
:undoc-members:
.. _api_utils:
Utils
-----
These functions and values are used internally by fbchat, and are subject to change. Do **NOT** rely on these to be backwards compatible!
.. automodule:: fbchat.utils
:members:

191
docs/conf.py Normal file
View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# fbchat documentation build configuration file, created by
# sphinx-quickstart on Thu May 25 15:43:01 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
import fbchat
import tests
from fbchat import __copyright__, __author__, __version__, __description__
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.viewcode'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'fbchat'
title = 'fbchat Documentation'
copyright = __copyright__
author = __author__
description = __description__
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = __version__
# The full version, including alpha/beta/rc tags.
release = __version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = project + 'doc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, project + '.tex', title,
author, 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, project, title,
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, project, title,
author, project, description,
'Miscellaneous'),
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/3/': None}
add_function_parentheses = False
html_theme_options = {
'show_powered_by': False,
'github_user': 'carpedm20',
'github_repo': project,
'github_banner': True,
'show_related': False
}
html_sidebars = {
'**': ['sidebar.html', 'searchbox.html']
}
html_show_sphinx = False
html_show_sourcelink = False
autoclass_content = 'init'
html_short_title = description

56
docs/examples.rst Normal file
View File

@@ -0,0 +1,56 @@
.. highlight:: python
.. _examples:
Examples
========
These are a few examples on how to use `fbchat`. Remember to swap out `<email>` and `<password>` for your email and password
Basic example
-------------
This will show basic usage of `fbchat`
.. literalinclude:: ../examples/basic_usage.py
Interacting with Threads
------------------------
This will interact with the thread in every way `fbchat` supports
.. literalinclude:: ../examples/interract.py
Fetching Information
--------------------
This will show the different ways of fetching information about users and threads
.. literalinclude:: ../examples/fetch.py
Echobot
-------
This will reply to any message with the same message
.. literalinclude:: ../examples/echobot.py
Remove Bot
----------
This will remove a user from a group if they write the message `Remove me!`
.. literalinclude:: ../examples/removebot.py
"Prevent changes"-Bot
---------------------
This will prevent chat color, emoji, nicknames and chat name from being changed.
It will also prevent people from being added and removed
.. literalinclude:: ../examples/keepbot.py

44
docs/faq.rst Normal file
View File

@@ -0,0 +1,44 @@
.. highlight:: python
.. module:: fbchat
.. _faq:
FAQ
===
Version X broke my installation
-------------------------------
We try to provide backwards compatibility where possible, but since we're not part of Facebook,
most of the things may be broken at any point in time
Downgrade to an earlier version of fbchat, run this command
.. code-block:: sh
$ pip install fbchat==<X>
Where you replace ``<X>`` with the version you want to use
Will you be supporting creating posts/events/pages and so on?
-------------------------------------------------------------
We won't be focusing on anything else than chat-related things. This API is called `fbCHAT`, after all ;)
Submitting Issues
-----------------
If you're having trouble with some of the snippets, or you think some of the functionality is broken,
please feel free to submit an issue on `Github <https://github.com/carpedm20/fbchat>`_.
You should first login with ``logging_level`` set to ``logging.DEBUG``::
from fbchat import Client
import logging
client = Client('<email>', '<password>', logging_level=logging.DEBUG)
Then you can submit the relevant parts of this log, and detailed steps on how to reproduce
.. warning::
Always remove your credentials from any debug information you may provide us.
Preferably, use a test account, in case you miss anything

66
docs/index.rst Normal file
View File

@@ -0,0 +1,66 @@
.. highlight:: python
.. module:: fbchat
.. fbchat documentation master file, created by
sphinx-quickstart on Thu May 25 15:43:01 2017.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. This documentation's layout is heavily inspired by requests' layout: https://requests.readthedocs.io
Some documentation is also partially copied from facebook-chat-api: https://github.com/Schmavery/facebook-chat-api
fbchat: Facebook Chat (Messenger) for Python
============================================
Release v\ |version|. (:ref:`install`)
.. generated with: https://img.shields.io/badge/license-BSD-blue.svg
.. image:: /_static/license.svg
:target: https://github.com/carpedm20/fbchat/blob/master/LICENSE.txt
:alt: License: BSD
.. generated with: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg
.. image:: /_static/python-versions.svg
:target: https://pypi.python.org/pypi/fbchat
:alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6
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>`_.
**No XMPP or API key is needed**. Just use your email and password.
Currently `fbchat` support Python 2.7, 3.4, 3.5 and 3.6:
`fbchat` works by emulating the browser.
This means doing the exact same GET/POST requests and tricking Facebook into thinking it's accessing the website normally.
Therefore, this API requires the credentials of a Facebook account.
.. note::
If you're having problems, please check the :ref:`faq`, before asking questions on Github
.. warning::
We are not responsible if your account gets banned for spammy activities,
such as sending lots of messages to people you don't know, sending messages very quickly,
sending spammy looking URLs, logging in and out very quickly... Be responsible Facebook citizens.
.. note::
Facebook now has an `official API <https://developers.facebook.com/docs/messenger-platform>`_ for chat bots,
so if you're familiar with node.js, this might be what you're looking for.
If you're already familiar with the basics of how Facebook works internally, go to :ref:`examples` to see example usage of `fbchat`
Overview
--------
.. toctree::
:maxdepth: 2
install
intro
examples
testing
api
todo
faq

36
docs/install.rst Normal file
View File

@@ -0,0 +1,36 @@
.. highlight:: sh
.. _install:
Installation
============
Pip Install fbchat
------------------
To install fbchat, run this command::
$ pip install fbchat
If you don't have `pip <https://pip.pypa.io>`_ installed,
`this Python installation guide <http://docs.python-guide.org/en/latest/starting/installation/>`_
can guide you through the process.
Get the Source Code
-------------------
fbchat is developed on GitHub, where the code is
`always available <https://github.com/carpedm20/fbchat>`_.
You can either clone the public repository::
$ git clone git://github.com/carpedm20/fbchat.git
Or, download a `tarball <https://github.com/carpedm20/fbchat/tarball/master>`_::
$ curl -OL https://github.com/carpedm20/fbchat/tarball/master
# optionally, zipball is also available (for Windows users).
Once you have a copy of the source, you can embed it in your own Python
package, or install it into your site-packages easily::
$ python setup.py install

200
docs/intro.rst Normal file
View File

@@ -0,0 +1,200 @@
.. highlight:: python
.. module:: fbchat
.. _intro:
Introduction
============
`fbchat` uses your email and password to communicate with the Facebook server.
That means that you should always store your password in a separate file, in case e.g. someone looks over your shoulder while you're writing code.
You should also make sure that the file's access control is appropriately restrictive
.. _intro_logging_in:
Logging In
----------
Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt
(If you want to supply the code in another fashion, overwrite :func:`Client.on2FACode`)::
from fbchat import Client
from fbchat.models import *
client = Client('<email>', '<password>')
Replace ``<email>`` and ``<password>`` with your email and password respectively
.. note::
For ease of use then most of the code snippets in this document will assume you've already completed the login process
Though the second line, ``from fbchat.models import *``, is not strictly neccesary here, later code snippets will assume you've done this
If you want to change how verbose `fbchat` is, change the logging level (in :class:`Client`)
Throughout your code, if you want to check whether you are still logged in, use :func:`Client.isLoggedIn`.
An example would be to login again if you've been logged out, using :func:`Client.login`::
if not client.isLoggedIn():
client.login('<email>', '<password>')
When you're done using the client, and want to securely logout, use :func:`Client.logout`::
client.logout()
.. _intro_threads:
Threads
-------
A thread can refer to two things: A Messenger group chat or a single Facebook user
:class:`models.ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``.
These will specify whether the thread is a single user chat or a group chat.
This is required for many of `fbchat`'s functions, since Facebook differentiates between these two internally
Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`,
and searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching`
You can get your own user ID by using :any:`Client.uid`
Getting the ID of a group chat is fairly trivial otherwise, since you only need to navigate to `<https://www.facebook.com/messages/>`_,
click on the group you want to find the ID of, and then read the id from the address bar.
The URL will look something like this: ``https://www.facebook.com/messages/t/1234567890``, where ``1234567890`` would be the ID of the group.
An image to illustrate this is shown below:
.. image:: /_static/find-group-id.png
:alt: An image illustrating how to find the ID of a group
The same method can be applied to some user accounts, though if they've set a custom URL, then you'll just see that URL instead
Here's an snippet showing the usage of thread IDs and thread types, where ``<user id>`` and ``<group id>``
corresponds to the ID of a single user, and the ID of a group respectively::
client.send(Message(text='<message>'), thread_id='<user id>', thread_type=ThreadType.USER)
client.send(Message(text='<message>'), thread_id='<group id>', thread_type=ThreadType.GROUP)
Some functions (e.g. :func:`Client.changeThreadColor`) don't require a thread type, so in these cases you just provide the thread ID::
client.changeThreadColor(ThreadColor.BILOBA_FLOWER, thread_id='<user id>')
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id='<group id>')
.. _intro_message_ids:
Message IDs
-----------
Every message you send on Facebook has a unique ID, and every action you do in a thread,
like changing a nickname or adding a person, has a unique ID too.
Some of `fbchat`'s functions require these ID's, like :func:`Client.reactToMessage`,
and some of then provide this ID, like :func:`Client.sendMessage`.
This snippet shows how to send a message, and then use the returned ID to react to that message with a 😍 emoji::
message_id = client.send(Message(text='message'), thread_id=thread_id, thread_type=thread_type)
client.reactToMessage(message_id, MessageReaction.LOVE)
.. _intro_interacting:
Interacting with Threads
------------------------
`fbchat` provides multiple functions for interacting with threads
Most functionality works on all threads, though some things,
like adding users to and removing users from a group chat, logically only works on group chats
The simplest way of using `fbchat` is to send a message.
The following snippet will, as you've probably already figured out, send the message `test message` to your account::
message_id = client.send(Message(text='test message'), thread_id=client.uid, thread_type=ThreadType.USER)
You can see a full example showing all the possible thread interactions with `fbchat` by going to :ref:`examples`
.. _intro_fetching:
Fetching Information
--------------------
You can use `fbchat` to fetch basic information like user names, profile pictures, thread names and user IDs
You can retrieve a user's ID with :func:`Client.searchForUsers`.
The following snippet will search for users by their name, take the first (and most likely) user, and then get their user ID from the result::
users = client.searchForUsers('<name of user>')
user = users[0]
print("User's ID: {}".format(user.uid))
print("User's name: {}".format(user.name))
print("User's profile picture url: {}".format(user.photo))
print("User's main url: {}".format(user.url))
Since this uses Facebook's search functions, you don't have to specify the whole name, first names will usually be enough
You can see a full example showing all the possible ways to fetch information with `fbchat` by going to :ref:`examples`
.. _intro_sessions:
Sessions
--------
`fbchat` provides functions to retrieve and set the session cookies.
This will enable you to store the session cookies in a separate file, so that you don't have to login each time you start your script.
Use :func:`Client.getSession` to retrieve the cookies::
session_cookies = client.getSession()
Then you can use :func:`Client.setSession`::
client.setSession(session_cookies)
Or you can set the ``session_cookies`` on your initial login.
(If the session cookies are invalid, your email and password will be used to login instead)::
client = Client('<email>', '<password>', session_cookies=session_cookies)
.. warning::
You session cookies can be just as valueable as you password, so store them with equal care
.. _intro_events:
Listening & Events
------------------
To use the listening functions `fbchat` offers (like :func:`Client.listen`),
you have to define what should be executed when certain events happen.
By default, (most) events will just be a `logging.info` statement,
meaning it will simply print information to the console when an event happens
.. note::
You can identify the event methods by their `on` prefix, e.g. `onMessage`
The event actions can be changed by subclassing the :class:`Client`, and then overwriting the event methods::
class CustomClient(Client):
def onMessage(self, mid, author_id, message_object, thread_id, thread_type, ts, metadata, msg, **kwargs):
# Do something with message_object here
pass
client = CustomClient('<email>', '<password>')
**Notice:** The following snippet is as equally valid as the previous one::
class CustomClient(Client):
def onMessage(self, message_object, author_id, thread_id, thread_type, **kwargs):
# Do something with message_object here
pass
client = CustomClient('<email>', '<password>')
The change was in the parameters that our `onMessage` method took: ``message_object`` and ``author_id`` got swapped,
and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function still works, since we included ``**kwargs``
.. note::
Therefore, for both backwards and forwards compatability,
the API actually requires that you include ``**kwargs`` as your final argument.
View the :ref:`examples` to see some more examples illustrating the event system

36
docs/make.bat Normal file
View File

@@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=python -msphinx
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=fbchat
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The Sphinx module was not found. Make sure you have Sphinx installed,
echo.then set the SPHINXBUILD environment variable to point to the full
echo.path of the 'sphinx-build' executable. Alternatively you may add the
echo.Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

29
docs/testing.rst Normal file
View File

@@ -0,0 +1,29 @@
.. highlight:: sh
.. module:: fbchat
.. _testing:
Testing
=======
To use the tests, copy ``tests/data.json`` to ``tests/my_data.json`` or type the information manually in the terminal prompts.
- email: Your (or a test user's) email / phone number
- password: Your (or a test user's) password
- group_thread_id: A test group that will be used to test group functionality
- user_thread_id: A person that will be used to test kick/add functionality (This user should be in the group)
Please remember to test all supported python versions.
If you've made any changes to the 2FA functionality, test it with a 2FA enabled account.
If you only want to execute specific tests, pass the function names in the command line (not including the `test_` prefix). Example::
$ python tests.py sendMessage sessions sendEmoji
.. warning::
Do not execute the full set of tests in too quick succession. This can get your account temporarily blocked for spam!
(You should execute the script at max about 10 times a day)
.. automodule:: tests
:members: TestFbchat
:undoc-members: TestFbchat

24
docs/todo.rst Normal file
View File

@@ -0,0 +1,24 @@
.. highlight:: python
.. module:: fbchat
.. _todo:
Todo
====
This page will be periodically updated to show missing features and documentation
Missing Functionality
---------------------
- Implement Client.searchForMessage
- This will use the graphql request API
- Implement chatting with pages properly
- Write better FAQ
- Explain usage of graphql
Documentation
-------------
.. todolist::

12
examples/basic_usage.py Normal file
View File

@@ -0,0 +1,12 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client('<email>', '<password>')
print('Own id: {}'.format(client.uid))
client.send(Message(text='Hi me!'), thread_id=client.uid, thread_type=ThreadType.USER)
client.logout()

18
examples/echobot.py Normal file
View File

@@ -0,0 +1,18 @@
# -*- coding: UTF-8 -*-
from fbchat import log, Client
# Subclass fbchat.Client and override required methods
class EchoBot(Client):
def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
self.markAsDelivered(thread_id, message_object.uid)
self.markAsRead(thread_id)
log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))
# If you're not the author, echo
if author_id != self.uid:
self.send(message_object, thread_id=thread_id, thread_type=thread_type)
client = EchoBot("<email>", "<password>")
client.listen()

64
examples/fetch.py Normal file
View File

@@ -0,0 +1,64 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client('<email>', '<password>')
# Fetches a list of all users you're currently chatting with, as `User` objects
users = client.fetchAllUsers()
print("users' IDs: {}".format([user.uid for user in users]))
print("users' names: {}".format([user.name for user in users]))
# If we have a user id, we can use `fetchUserInfo` to fetch a `User` object
user = client.fetchUserInfo('<user id>')['<user id>']
# We can also query both mutiple users together, which returns list of `User` objects
users = client.fetchUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>')
print("user's name: {}".format(user.name))
print("users' names: {}".format([users[k].name for k in users]))
# `searchForUsers` searches for the user and gives us a list of the results,
# and then we just take the first one, aka. the most likely one:
user = client.searchForUsers('<name of user>')[0]
print('user ID: {}'.format(user.uid))
print("user's name: {}".format(user.name))
print("user's photo: {}".format(user.photo))
print("Is user client's friend: {}".format(user.is_friend))
# Fetches a list of the 20 top threads you're currently chatting with
threads = client.fetchThreadList()
# Fetches the next 10 threads
threads += client.fetchThreadList(offset=20, limit=10)
print("Threads: {}".format(threads))
# Gets the last 10 messages sent to the thread
messages = client.fetchThreadMessages(thread_id='<thread id>', limit=10)
# Since the message come in reversed order, reverse them
messages.reverse()
# Prints the content of all the messages
for message in messages:
print(message.text)
# If we have a thread id, we can use `fetchThreadInfo` to fetch a `Thread` object
thread = client.fetchThreadInfo('<thread id>')['<thread id>']
print("thread's name: {}".format(thread.name))
print("thread's type: {}".format(thread.type))
# `searchForThreads` searches works like `searchForUsers`, but gives us a list of threads instead
thread = client.searchForThreads('<name of thread>')[0]
print("thread's name: {}".format(thread.name))
print("thread's type: {}".format(thread.type))
# Here should be an example of `getUnread`

61
examples/interract.py Normal file
View File

@@ -0,0 +1,61 @@
# -*- coding: UTF-8 -*-
from fbchat import Client
from fbchat.models import *
client = Client("<email>", "<password>")
thread_id = '1234567890'
thread_type = ThreadType.GROUP
# Will send a message to the thread
client.send(Message(text='<message>'), thread_id=thread_id, thread_type=thread_type)
# Will send the default `like` emoji
client.send(Message(emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type)
# Will send the emoji `👍`
client.send(Message(text='👍', emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type)
# Will send the sticker with ID `767334476626295`
client.send(Message(sticker=Sticker('767334476626295')), thread_id=thread_id, thread_type=thread_type)
# Will send a message with a mention
client.send(Message(text='This is a @mention', mentions=[Mention(thread_id, offset=10, length=8)]), thread_id=thread_id, thread_type=thread_type)
# Will send the image located at `<image path>`
client.sendLocalImage('<image path>', message=Message(text='This is a local image'), thread_id=thread_id, thread_type=thread_type)
# Will download the image at the url `<image url>`, and then send it
client.sendRemoteImage('<image url>', message=Message(text='This is a remote image'), thread_id=thread_id, thread_type=thread_type)
# Only do these actions if the thread is a group
if thread_type == ThreadType.GROUP:
# Will remove the user with ID `<user id>` from the thread
client.removeUserFromGroup('<user id>', thread_id=thread_id)
# Will add the user with ID `<user id>` to the thread
client.addUsersToGroup('<user id>', thread_id=thread_id)
# Will add the users with IDs `<1st user id>`, `<2nd user id>` and `<3th user id>` to the thread
client.addUsersToGroup(['<1st user id>', '<2nd user id>', '<3rd user id>'], thread_id=thread_id)
# Will change the nickname of the user `<user_id>` to `<new nickname>`
client.changeNickname('<new nickname>', '<user id>', thread_id=thread_id, thread_type=thread_type)
# Will change the title of the thread to `<title>`
client.changeThreadTitle('<title>', thread_id=thread_id, thread_type=thread_type)
# Will set the typing status of the thread to `TYPING`
client.setTypingStatus(TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type)
# Will change the thread color to `MESSENGER_BLUE`
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id=thread_id)
# Will change the thread emoji to `👍`
client.changeThreadEmoji('👍', thread_id=thread_id)
# Will react to a message with a 😍 emoji
client.reactToMessage('<message id>', MessageReaction.LOVE)

54
examples/keepbot.py Normal file
View File

@@ -0,0 +1,54 @@
# -*- coding: UTF-8 -*-
from fbchat import log, Client
from fbchat.models import *
# Change this to your group id
old_thread_id = '1234567890'
# Change these to match your liking
old_color = ThreadColor.MESSENGER_BLUE
old_emoji = '👍'
old_title = 'Old group chat name'
old_nicknames = {
'12345678901': "User nr. 1's nickname",
'12345678902': "User nr. 2's nickname",
'12345678903': "User nr. 3's nickname",
'12345678904': "User nr. 4's nickname"
}
class KeepBot(Client):
def onColorChange(self, author_id, new_color, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and old_color != new_color:
log.info("{} changed the thread color. It will be changed back".format(author_id))
self.changeThreadColor(old_color, thread_id=thread_id)
def onEmojiChange(self, author_id, new_emoji, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and new_emoji != old_emoji:
log.info("{} changed the thread emoji. It will be changed back".format(author_id))
self.changeThreadEmoji(old_emoji, thread_id=thread_id)
def onPeopleAdded(self, added_ids, author_id, thread_id, **kwargs):
if old_thread_id == thread_id and author_id != self.uid:
log.info("{} got added. They will be removed".format(added_ids))
for added_id in added_ids:
self.removeUserFromGroup(added_id, thread_id=thread_id)
def onPersonRemoved(self, removed_id, author_id, thread_id, **kwargs):
# No point in trying to add ourself
if old_thread_id == thread_id and removed_id != self.uid and author_id != self.uid:
log.info("{} got removed. They will be re-added".format(removed_id))
self.addUsersToGroup(removed_id, thread_id=thread_id)
def onTitleChange(self, author_id, new_title, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and old_title != new_title:
log.info("{} changed the thread title. It will be changed back".format(author_id))
self.changeThreadTitle(old_title, thread_id=thread_id, thread_type=thread_type)
def onNicknameChange(self, author_id, changed_for, new_nickname, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and changed_for in old_nicknames and old_nicknames[changed_for] != new_nickname:
log.info("{} changed {}'s' nickname. It will be changed back".format(author_id, changed_for))
self.changeNickname(old_nicknames[changed_for], changed_for, thread_id=thread_id, thread_type=thread_type)
client = KeepBot("<email>", "<password>")
client.listen()

17
examples/removebot.py Normal file
View File

@@ -0,0 +1,17 @@
# -*- coding: UTF-8 -*-
from fbchat import log, Client
from fbchat.models import *
class RemoveBot(Client):
def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
# We can only kick people from group chats, so no need to try if it's a user chat
if message_object.text == 'Remove me!' and thread_type == ThreadType.GROUP:
log.info('{} will be removed from {}'.format(author_id, thread_id))
self.removeUserFromGroup(author_id, thread_id=thread_id)
else:
# Sends the data to the inherited onMessage, so that we can still see when a message is recieved
super(RemoveBot, self).onMessage(author_id=author_id, message_object=message_object, thread_id=thread_id, thread_type=thread_type, **kwargs)
client = RemoveBot("<email>", "<password>")
client.listen()

View File

@@ -6,20 +6,23 @@
Facebook Chat (Messenger) for Python
:copyright: (c) 2015 by Taehoon Kim.
:license: BSD, see LICENSE for more details.
:copyright: (c) 2015 - 2018 by Taehoon Kim
:license: BSD 3-Clause, see LICENSE for more details.
"""
from __future__ import unicode_literals
from .client import *
__title__ = 'fbchat'
__version__ = '1.5.0'
__description__ = 'Facebook Chat (Messenger) for Python'
__copyright__ = 'Copyright 2015 by Taehoon Kim'
__version__ = '0.9.3'
__license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan'
__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim'
__license__ = 'BSD 3-Clause'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com'
__source__ = 'https://github.com/carpedm20/fbchat/'
__all__ = [
'Client',

File diff suppressed because it is too large Load Diff

610
fbchat/graphql.py Normal file
View File

@@ -0,0 +1,610 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import json
import re
from .models import *
from .utils import *
# Shameless copy from https://stackoverflow.com/a/8730674
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
class ConcatJSONDecoder(json.JSONDecoder):
def decode(self, s, _w=WHITESPACE.match):
s_len = len(s)
objs = []
end = 0
while end != s_len:
obj, end = self.raw_decode(s, idx=_w(s, end).end())
end = _w(s, end).end()
objs.append(obj)
return objs
# End shameless copy
def graphql_color_to_enum(color):
if color is None:
return None
if not color:
return ThreadColor.MESSENGER_BLUE
color = color[2:] # Strip the alpha value
color_value = '#{}'.format(color.lower())
return enum_extend_if_invalid(ThreadColor, color_value)
def get_customization_info(thread):
if thread is None or thread.get('customization_info') is None:
return {}
info = thread['customization_info']
rtn = {
'emoji': info.get('emoji'),
'color': graphql_color_to_enum(info.get('outgoing_bubble_color'))
}
if thread.get('thread_type') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'):
rtn['nicknames'] = {}
for k in info.get('participant_customizations', []):
rtn['nicknames'][k['participant_id']] = k.get('nickname')
elif info.get('participant_customizations'):
uid = thread.get('thread_key', {}).get('other_user_id') or thread.get('id')
pc = info['participant_customizations']
if len(pc) > 0:
if pc[0].get('participant_id') == uid:
rtn['nickname'] = pc[0].get('nickname')
else:
rtn['own_nickname'] = pc[0].get('nickname')
if len(pc) > 1:
if pc[1].get('participant_id') == uid:
rtn['nickname'] = pc[1].get('nickname')
else:
rtn['own_nickname'] = pc[1].get('nickname')
return rtn
def graphql_to_sticker(s):
if not s:
return None
sticker = Sticker(
uid=s['id']
)
if s.get('pack'):
sticker.pack = s['pack'].get('id')
if s.get('sprite_image'):
sticker.is_animated = True
sticker.medium_sprite_image = s['sprite_image'].get('uri')
sticker.large_sprite_image = s['sprite_image_2x'].get('uri')
sticker.frames_per_row = s.get('frames_per_row')
sticker.frames_per_col = s.get('frames_per_column')
sticker.frame_rate = s.get('frame_rate')
sticker.url = s.get('url')
sticker.width = s.get('width')
sticker.height = s.get('height')
if s.get('label'):
sticker.label = s['label']
return sticker
def graphql_to_attachment(a):
_type = a['__typename']
if _type in ['MessageImage', 'MessageAnimatedImage']:
return ImageAttachment(
original_extension=a.get('original_extension') or (a['filename'].split('-')[0] if a.get('filename') else None),
width=a.get('original_dimensions', {}).get('width'),
height=a.get('original_dimensions', {}).get('height'),
is_animated=_type=='MessageAnimatedImage',
thumbnail_url=a.get('thumbnail', {}).get('uri'),
preview=a.get('preview') or a.get('preview_image'),
large_preview=a.get('large_preview'),
animated_preview=a.get('animated_image'),
uid=a.get('legacy_attachment_id')
)
elif _type == 'MessageVideo':
return VideoAttachment(
width=a.get('original_dimensions', {}).get('width'),
height=a.get('original_dimensions', {}).get('height'),
duration=a.get('playable_duration_in_ms'),
preview_url=a.get('playable_url'),
small_image=a.get('chat_image'),
medium_image=a.get('inbox_image'),
large_image=a.get('large_image'),
uid=a.get('legacy_attachment_id')
)
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':
return FileAttachment(
url=a.get('url'),
name=a.get('filename'),
is_malicious=a.get('is_malicious'),
uid=a.get('message_file_fbid')
)
else:
return Attachment(
uid=a.get('legacy_attachment_id')
)
def graphql_to_extensible_attachment(a):
story = a.get('story_attachment')
if story:
target = story.get('target')
if target:
_type = target['__typename']
if _type == 'MessageLocation':
latitude, longitude = get_url_parameter(get_url_parameter(story['url'], 'u'), 'where1').split(", ")
rtn = LocationAttachment(
uid=int(story['deduplication_key']),
latitude=float(latitude),
longitude=float(longitude),
)
if story['media']:
rtn.image_url = story['media']['image']['uri']
rtn.image_width = story['media']['image']['width']
rtn.image_height = story['media']['image']['height']
rtn.url = story['url']
return rtn
elif _type == 'MessageLiveLocation':
rtn = LiveLocationAttachment(
uid=int(story['target']['live_location_id']),
latitude=story['target']['coordinate']['latitude'] if story['target'].get('coordinate') else None,
longitude=story['target']['coordinate']['longitude'] if story['target'].get('coordinate') else None,
name=story['title_with_entities']['text'],
expiration_time=story['target']['expiration_time'] if story['target'].get('expiration_time') else None,
is_expired=story['target']['is_expired'],
)
if story['media']:
rtn.image_url = story['media']['image']['uri']
rtn.image_width = story['media']['image']['width']
rtn.image_height = story['media']['image']['height']
rtn.url = story['url']
return rtn
elif _type in ['ExternalUrl', 'Story']:
return ShareAttachment(
uid=a.get('legacy_attachment_id'),
author=story['target']['actors'][0]['id'] if story['target'].get('actors') else None,
url=story['url'],
original_url=get_url_parameter(story['url'], 'u') if "/l.php?u=" in story['url'] else story['url'],
title=story['title_with_entities'].get('text'),
description=story['description'].get('text'),
source=story['source']['text'],
image_url=story['media']['image']['uri'] if story.get('media') else None,
original_image_url=(get_url_parameter(story['media']['image']['uri'], 'url') if "/safe_image.php" in story['media']['image']['uri'] else story['media']['image']['uri']) if story.get('media') else None,
image_width=story['media']['image']['width'] if story.get('media') else None,
image_height=story['media']['image']['height'] if story.get('media') else None,
attachments=[graphql_to_subattachment(attachment) for attachment in story.get('subattachments')],
)
else:
return UnsentMessage(
uid=a.get('legacy_attachment_id'),
)
def graphql_to_subattachment(a):
_type = a['target']['__typename']
if _type == 'Video':
return VideoAttachment(
duration=a['media'].get('playable_duration_in_ms'),
preview_url=a['media'].get('playable_url'),
medium_image=a['media'].get('image'),
uid=a['target'].get('video_id'),
)
def graphql_to_live_location(a):
return LiveLocationAttachment(
uid=a['id'],
latitude=a['coordinate']['latitude'] / (10 ** 8) if not a.get('stopReason') else None,
longitude=a['coordinate']['longitude'] / (10 ** 8) if not a.get('stopReason') else None,
name=a.get('locationTitle'),
expiration_time=a['expirationTime'],
is_expired=bool(a.get('stopReason')),
)
def graphql_to_poll(a):
rtn = Poll(
title=a.get('title') if a.get('title') else a.get('text'),
options=[graphql_to_poll_option(m) for m in a.get('options')]
)
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):
if message.get('message_sender') is None:
message['message_sender'] = {}
if message.get('message') is None:
message['message'] = {}
rtn = Message(
text=message.get('message').get('text'),
mentions=[Mention(m.get('entity', {}).get('id'), offset=m.get('offset'), length=m.get('length')) for m in message.get('message').get('ranges', [])],
emoji_size=get_emojisize_from_tags(message.get('tags_list')),
sticker=graphql_to_sticker(message.get('sticker'))
)
rtn.uid = str(message.get('message_id'))
rtn.author = str(message.get('message_sender').get('id'))
rtn.timestamp = message.get('timestamp_precise')
rtn.unsent = False
if message.get('unread') is not None:
rtn.is_read = not message['unread']
rtn.reactions = {
str(r['user']['id']): enum_extend_if_invalid(MessageReaction, r['reaction'])
for r in message.get('message_reactions')
}
if message.get('blob_attachments') is not None:
rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']]
if message.get('extensible_attachment') is not None:
attachment = graphql_to_extensible_attachment(message['extensible_attachment'])
if isinstance(attachment, UnsentMessage):
rtn.unsent = True
elif attachment:
rtn.attachments.append(attachment)
return rtn
def graphql_to_user(user):
if user.get('profile_picture') is None:
user['profile_picture'] = {}
c_info = get_customization_info(user)
plan = None
if user.get('event_reminders'):
plan = graphql_to_plan(user['event_reminders']['nodes'][0]) if user['event_reminders'].get('nodes') else None
return User(
user['id'],
url=user.get('url'),
first_name=user.get('first_name'),
last_name=user.get('last_name'),
is_friend=user.get('is_viewer_friend'),
gender=GENDERS.get(user.get('gender')),
affinity=user.get('affinity'),
nickname=c_info.get('nickname'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
own_nickname=c_info.get('own_nickname'),
photo=user['profile_picture'].get('uri'),
name=user.get('name'),
message_count=user.get('messages_count'),
plan=plan,
)
def graphql_to_thread(thread):
if thread['thread_type'] == 'GROUP':
return graphql_to_group(thread)
elif thread['thread_type'] == 'ONE_TO_ONE':
if thread.get('big_image_src') is None:
thread['big_image_src'] = {}
c_info = get_customization_info(thread)
participants = [node['messaging_actor'] for node in thread['all_participants']['nodes']]
user = next(p for p in participants if p['id'] == thread['thread_key']['other_user_id'])
last_message_timestamp = None
if 'last_message' in thread:
last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise']
first_name = user.get('short_name')
if first_name is None:
last_name = None
else:
last_name = user.get('name').split(first_name, 1).pop().strip()
plan = None
if thread.get('event_reminders'):
plan = graphql_to_plan(thread['event_reminders']['nodes'][0]) if thread['event_reminders'].get('nodes') else None
return User(
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):
if group.get('image') is None:
group['image'] = {}
c_info = get_customization_info(group)
last_message_timestamp = None
if 'last_message' in group:
last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise']
plan = None
if group.get('event_reminders'):
plan = graphql_to_plan(group['event_reminders']['nodes'][0]) if group['event_reminders'].get('nodes') else None
return Group(
group['thread_key']['thread_fbid'],
participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]),
nicknames=c_info.get('nicknames'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
admins = set([node.get('id') for node in group.get('thread_admins')]),
approval_mode = bool(group.get('approval_mode')) if group.get('approval_mode') is not None else None,
approval_requests = set(node["requester"]['id'] for node in group['group_approval_queue']['nodes']) if group.get('group_approval_queue') else None,
join_link = group['joinable_mode'].get('link'),
photo=group['image'].get('uri'),
name=group.get('name'),
message_count=group.get('messages_count'),
last_message_timestamp=last_message_timestamp,
plan=plan,
)
def graphql_to_page(page):
if page.get('profile_picture') is None:
page['profile_picture'] = {}
if page.get('city') is None:
page['city'] = {}
plan = None
if page.get('event_reminders'):
plan = graphql_to_plan(page['event_reminders']['nodes'][0]) if page['event_reminders'].get('nodes') else None
return Page(
page['id'],
url=page.get('url'),
city=page.get('city').get('name'),
category=page.get('category_type'),
photo=page['profile_picture'].get('uri'),
name=page.get('name'),
message_count=page.get('messages_count'),
plan=plan,
)
def graphql_queries_to_json(*queries):
"""
Queries should be a list of GraphQL objects
"""
rtn = {}
for i, query in enumerate(queries):
rtn['q{}'.format(i)] = query.value
return json.dumps(rtn)
def graphql_response_to_json(content):
content = strip_to_json(content) # Usually only needed in some error cases
try:
j = json.loads(content, cls=ConcatJSONDecoder)
except Exception:
raise FBchatException('Error while parsing JSON: {}'.format(repr(content)))
rtn = [None]*(len(j))
for x in j:
if 'error_results' in x:
del rtn[-1]
continue
check_json(x)
[(key, value)] = x.items()
check_json(value)
if 'response' in value:
rtn[int(key[1:])] = value['response']
else:
rtn[int(key[1:])] = value['data']
log.debug(rtn)
return rtn
class GraphQL(object):
def __init__(self, query=None, doc_id=None, params=None):
if params is None:
params = {}
if query is not None:
self.value = {
'priority': 0,
'q': query,
'query_params': params
}
elif doc_id is not None:
self.value = {
'doc_id': doc_id,
'query_params': params
}
else:
raise FBchatUserError('A query or doc_id must be specified')
FRAGMENT_USER = """
QueryFragment User: User {
id,
name,
first_name,
last_name,
profile_picture.width(<pic_size>).height(<pic_size>) {
uri
},
is_viewer_friend,
url,
gender,
viewer_affinity
}
"""
FRAGMENT_GROUP = """
QueryFragment Group: MessageThread {
name,
thread_key {
thread_fbid
},
image {
uri
},
is_group_thread,
all_participants {
nodes {
messaging_actor {
id
}
}
},
customization_info {
participant_customizations {
participant_id,
nickname
},
outgoing_bubble_color,
emoji
},
thread_admins {
id
},
group_approval_queue {
nodes {
requester {
id
}
}
},
approval_mode,
joinable_mode {
mode,
link
},
event_reminders {
nodes {
id,
lightweight_event_creator {
id
},
time,
location_name,
event_title,
event_reminder_members {
edges {
node {
id
},
guest_list_state
}
}
}
}
}
"""
FRAGMENT_PAGE = """
QueryFragment Page: Page {
id,
name,
profile_picture.width(32).height(32) {
uri
},
url,
category_type,
city {
name
}
}
"""
SEARCH_USER = """
Query SearchUser(<search> = '', <limit> = 10) {
entities_named(<search>) {
search_results.of_type(user).first(<limit>) as users {
nodes {
@User
}
}
}
}
""" + FRAGMENT_USER
SEARCH_GROUP = """
Query SearchGroup(<search> = '', <limit> = 10, <pic_size> = 32) {
viewer() {
message_threads.with_thread_name(<search>).last(<limit>) as groups {
nodes {
@Group
}
}
}
}
""" + FRAGMENT_GROUP
SEARCH_PAGE = """
Query SearchPage(<search> = '', <limit> = 10) {
entities_named(<search>) {
search_results.of_type(page).first(<limit>) as pages {
nodes {
@Page
}
}
}
}
""" + FRAGMENT_PAGE
SEARCH_THREAD = """
Query SearchThread(<search> = '', <limit> = 10) {
entities_named(<search>) {
search_results.first(<limit>) as threads {
nodes {
__typename,
@User,
@Group,
@Page
}
}
}
}
""" + FRAGMENT_USER + FRAGMENT_GROUP + FRAGMENT_PAGE

View File

@@ -1,31 +1,662 @@
from __future__ import unicode_literals
import sys
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import aenum
class FBchatException(Exception):
"""Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this"""
class FBchatFacebookError(FBchatException):
#: The error code that Facebook returned
fb_error_code = None
#: The error message that Facebook returned (In the user's own language)
fb_error_message = None
#: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200)
request_status_code = None
def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None):
super(FBchatFacebookError, self).__init__(message)
"""Thrown by fbchat when Facebook returns an error"""
self.fb_error_code = str(fb_error_code)
self.fb_error_message = fb_error_message
self.request_status_code = request_status_code
class FBchatUserError(FBchatException):
"""Thrown by fbchat when wrong values are entered"""
class Thread(object):
#: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info
uid = None
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info
type = None
#: A url to the thread's picture
photo = None
#: The name of the thread
name = None
#: Timestamp of last message
last_message_timestamp = None
#: Number of messages in the thread
message_count = None
#: Set :class:`Plan`
plan = None
def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=None, plan=None):
"""Represents a Facebook thread"""
self.uid = str(uid)
self.type = _type
self.photo = photo
self.name = name
self.last_message_timestamp = last_message_timestamp
self.message_count = message_count
self.plan = plan
class Base():
def __repr__(self):
uni = self.__unicode__()
return uni.encode('utf-8') if sys.version_info < (3, 0) else uni
return self.__unicode__()
def __unicode__(self):
return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url)
return '<{} {} ({})>'.format(self.type.name, self.name, self.uid)
class User(Base):
def __init__(self, data):
if data['type'] != 'user':
raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path']))
self.uid = data['uid']
self.type = data['type']
self.photo = data['photo']
self.url = data['path']
self.name = data['text']
self.score = data['score']
self.data = data
class User(Thread):
#: The profile url
url = None
#: The users first name
first_name = None
#: The users last name
last_name = None
#: Whether the user and the client are friends
is_friend = None
#: The user's gender
gender = None
#: From 0 to 1. How close the client is to the user
affinity = None
#: The user's nickname
nickname = None
#: The clients nickname, as seen by the user
own_nickname = None
#: A :class:`ThreadColor`. The message color
color = None
#: The default emoji
emoji = None
class Thread():
def __init__(self, **entries):
self.__dict__.update(entries)
def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs):
"""Represents a Facebook user. Inherits `Thread`"""
super(User, self).__init__(ThreadType.USER, uid, **kwargs)
self.url = url
self.first_name = first_name
self.last_name = last_name
self.is_friend = is_friend
self.gender = gender
self.affinity = affinity
self.nickname = nickname
self.own_nickname = own_nickname
self.color = color
self.emoji = emoji
class Message():
def __init__(self, **entries):
self.__dict__.update(entries)
class Group(Thread):
#: Unique list (set) of the group thread's participant user IDs
participants = None
#: A dict, containing user nicknames mapped to their IDs
nicknames = None
#: A :class:`ThreadColor`. The groups's message color
color = None
#: The groups's default emoji
emoji = None
# Set containing user IDs of thread admins
admins = None
# True if users need approval to join
approval_mode = None
# Set containing user IDs requesting to join
approval_requests = None
# Link for joining group
join_link = None
def __init__(self, uid, participants=None, nicknames=None, color=None, emoji=None, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs):
"""Represents a Facebook group. Inherits `Thread`"""
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
if participants is None:
participants = set()
self.participants = participants
if nicknames is None:
nicknames = []
self.nicknames = nicknames
self.color = color
self.emoji = emoji
if admins is None:
admins = set()
self.admins = admins
self.approval_mode = approval_mode
if approval_requests is None:
approval_requests = set()
self.approval_requests = approval_requests
self.join_link = join_link
class Room(Group):
# True is room is not discoverable
privacy_mode = None
def __init__(self, uid, privacy_mode=None, **kwargs):
"""Deprecated. Use :class:`Group` instead"""
super(Room, self).__init__(uid, **kwargs)
self.type = ThreadType.ROOM
self.privacy_mode = privacy_mode
class Page(Thread):
#: The page's custom url
url = None
#: The name of the page's location city
city = None
#: Amount of likes the page has
likes = None
#: Some extra information about the page
sub_title = None
#: The page's category
category = None
def __init__(self, uid, url=None, city=None, likes=None, sub_title=None, category=None, **kwargs):
"""Represents a Facebook page. Inherits `Thread`"""
super(Page, self).__init__(ThreadType.PAGE, uid, **kwargs)
self.url = url
self.city = city
self.likes = likes
self.sub_title = sub_title
self.category = category
class Message(object):
#: The actual message
text = None
#: A list of :class:`Mention` objects
mentions = None
#: A :class:`EmojiSize`. Size of a sent emoji
emoji_size = None
#: The message ID
uid = None
#: ID of the sender
author = None
#: Timestamp of when the message was sent
timestamp = None
#: Whether the message is read
is_read = None
#: A list of pepole IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages`
read_by = None
#: A dict with user's IDs as keys, and their :class:`MessageReaction` as values
reactions = None
#: The actual message
text = None
#: A :class:`Sticker`
sticker = None
#: A list of attachments
attachments = None
#: Whether the message is unsent (deleted for everyone)
unsent = None
def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None):
"""Represents a Facebook message"""
self.text = text
if mentions is None:
mentions = []
self.mentions = mentions
self.emoji_size = emoji_size
self.sticker = sticker
if attachments is None:
attachments = []
self.attachments = attachments
self.reactions = {}
self.read_by = []
self.deleted = False
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<Message ({}): {}, mentions={} emoji_size={} attachments={}>'.format(self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments)
class Attachment(object):
#: The attachment ID
uid = None
def __init__(self, uid=None):
"""Represents a Facebook attachment"""
self.uid = uid
class UnsentMessage(Attachment):
def __init__(self, *args, **kwargs):
"""Represents an unsent message attachment"""
super(UnsentMessage, self).__init__(*args, **kwargs)
class Sticker(Attachment):
#: The sticker-pack's ID
pack = None
#: Whether the sticker is animated
is_animated = False
# If the sticker is animated, the following should be present
#: URL to a medium spritemap
medium_sprite_image = None
#: URL to a large spritemap
large_sprite_image = None
#: The amount of frames present in the spritemap pr. row
frames_per_row = None
#: The amount of frames present in the spritemap pr. coloumn
frames_per_col = None
#: The frame rate the spritemap is intended to be played in
frame_rate = None
#: URL to the sticker's image
url = None
#: Width of the sticker
width = None
#: Height of the sticker
height = None
#: The sticker's label/name
label = None
def __init__(self, *args, **kwargs):
"""Represents a Facebook sticker that has been sent to a Facebook thread as an attachment"""
super(Sticker, self).__init__(*args, **kwargs)
class ShareAttachment(Attachment):
#: ID of the author of the shared post
author = None
#: Target URL
url = None
#: Original URL if Facebook redirects the URL
original_url = None
#: Title of the attachment
title = None
#: Description of the attachment
description = None
#: Name of the source
source = None
#: URL of the attachment image
image_url = None
#: URL of the original image if Facebook uses `safe_image`
original_image_url = None
#: Width of the image
image_width = None
#: Height of the image
image_height = None
#: List of additional attachments
attachments = None
def __init__(self, author=None, url=None, original_url=None, title=None, description=None, source=None, image_url=None, original_image_url=None, image_width=None, image_height=None, attachments=None, **kwargs):
"""Represents a shared item (eg. URL) that has been sent as a Facebook attachment"""
super(ShareAttachment, self).__init__(**kwargs)
self.author = author
self.url = url
self.original_url = original_url
self.title = title
self.description = description
self.source = source
self.image_url = image_url
self.original_image_url = original_image_url
self.image_width = image_width
self.image_height = image_height
if attachments is None:
attachments = []
self.attachments = attachments
class LocationAttachment(Attachment):
#: Latidute of the location
latitude = None
#: Longitude of the location
longitude = None
#: URL of image showing the map of the location
image_url = None
#: Width of the image
image_width = None
#: Height of the image
image_height = None
#: URL to Bing maps with the location
url = None
def __init__(self, latitude=None, longitude=None, **kwargs):
"""Represents a user location"""
super(LocationAttachment, self).__init__(**kwargs)
self.latitude = latitude
self.longitude = longitude
class LiveLocationAttachment(LocationAttachment):
#: Name of the location
name = None
#: Timestamp when live location expires
expiration_time = None
#: True if live location is expired
is_expired = None
def __init__(self, name=None, expiration_time=None, is_expired=None, **kwargs):
"""Represents a live user location"""
super(LiveLocationAttachment, self).__init__(**kwargs)
self.expiration_time = expiration_time
self.is_expired = is_expired
class FileAttachment(Attachment):
#: Url where you can download the file
url = None
#: Size of the file in bytes
size = None
#: Name of the file
name = None
#: Whether Facebook determines that this file may be harmful
is_malicious = None
def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs):
"""Represents a file that has been sent as a Facebook attachment"""
super(FileAttachment, self).__init__(**kwargs)
self.url = url
self.size = size
self.name = name
self.is_malicious = is_malicious
class AudioAttachment(Attachment):
#: Name of the file
filename = None
#: Url of the audio file
url = None
#: Duration of the audioclip in milliseconds
duration = None
#: Audio type
audio_type = None
def __init__(self, filename=None, url=None, duration=None, audio_type=None, **kwargs):
"""Represents an audio file that has been sent as a Facebook attachment"""
super(AudioAttachment, self).__init__(**kwargs)
self.filename = filename
self.url = url
self.duration = duration
self.audio_type = audio_type
class ImageAttachment(Attachment):
#: The extension of the original image (eg. 'png')
original_extension = None
#: Width of original image
width = None
#: Height of original image
height = None
#: Whether the image is animated
is_animated = None
#: URL to a thumbnail of the image
thumbnail_url = None
#: URL to a medium preview of the image
preview_url = None
#: Width of the medium preview image
preview_width = None
#: Height of the medium preview image
preview_height = None
#: URL to a large preview of the image
large_preview_url = None
#: Width of the large preview image
large_preview_width = None
#: Height of the large preview image
large_preview_height = None
#: URL to an animated preview of the image (eg. for gifs)
animated_preview_url = None
#: Width of the animated preview image
animated_preview_width = None
#: Height of the animated preview image
animated_preview_height = None
def __init__(self, original_extension=None, width=None, height=None, is_animated=None, thumbnail_url=None, preview=None, large_preview=None, animated_preview=None, **kwargs):
"""
Represents an image that has been sent as a Facebook attachment
To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`,
and pass it the uid of the image attachment
"""
super(ImageAttachment, self).__init__(**kwargs)
self.original_extension = original_extension
if width is not None:
width = int(width)
self.width = width
if height is not None:
height = int(height)
self.height = height
self.is_animated = is_animated
self.thumbnail_url = thumbnail_url
if preview is None:
preview = {}
self.preview_url = preview.get('uri')
self.preview_width = preview.get('width')
self.preview_height = preview.get('height')
if large_preview is None:
large_preview = {}
self.large_preview_url = large_preview.get('uri')
self.large_preview_width = large_preview.get('width')
self.large_preview_height = large_preview.get('height')
if animated_preview is None:
animated_preview = {}
self.animated_preview_url = animated_preview.get('uri')
self.animated_preview_width = animated_preview.get('width')
self.animated_preview_height = animated_preview.get('height')
class VideoAttachment(Attachment):
#: Size of the original video in bytes
size = None
#: Width of original video
width = None
#: Height of original video
height = None
#: Length of video in milliseconds
duration = None
#: URL to very compressed preview video
preview_url = None
#: URL to a small preview image of the video
small_image_url = None
#: Width of the small preview image
small_image_width = None
#: Height of the small preview image
small_image_height = None
#: URL to a medium preview image of the video
medium_image_url = None
#: Width of the medium preview image
medium_image_width = None
#: Height of the medium preview image
medium_image_height = None
#: URL to a large preview image of the video
large_image_url = None
#: Width of the large preview image
large_image_width = None
#: Height of the large preview image
large_image_height = None
def __init__(self, size=None, width=None, height=None, duration=None, preview_url=None, small_image=None, medium_image=None, large_image=None, **kwargs):
"""Represents a video that has been sent as a Facebook attachment"""
super(VideoAttachment, self).__init__(**kwargs)
self.size = size
self.width = width
self.height = height
self.duration = duration
self.preview_url = preview_url
if small_image is None:
small_image = {}
self.small_image_url = small_image.get('uri')
self.small_image_width = small_image.get('width')
self.small_image_height = small_image.get('height')
if medium_image is None:
medium_image = {}
self.medium_image_url = medium_image.get('uri')
self.medium_image_width = medium_image.get('width')
self.medium_image_height = medium_image.get('height')
if large_image is None:
large_image = {}
self.large_image_url = large_image.get('uri')
self.large_image_width = large_image.get('width')
self.large_image_height = large_image.get('height')
class Mention(object):
#: The thread ID the mention is pointing at
thread_id = None
#: The character where the mention starts
offset = None
#: The length of the mention
length = None
def __init__(self, thread_id, offset=0, length=10):
"""Represents a @mention"""
self.thread_id = thread_id
self.offset = offset
self.length = length
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<Mention {}: offset={} length={}>'.format(self.thread_id, self.offset, self.length)
class Poll(object):
#: ID of the poll
uid = None
#: Title of the poll
title = None
#: List of :class:`PollOption`, can be fetched with :func:`fbchat.Client.fetchPollOptions`
options = None
#: Options count
options_count = None
def __init__(self, title, options):
"""Represents a poll"""
self.title = title
self.options = options
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<Poll ({}): {} options={}>'.format(self.uid, repr(self.title), self.options)
class PollOption(object):
#: ID of the poll option
uid = None
#: Text of the poll option
text = None
#: Whether vote when creating or client voted
vote = None
#: ID of the users who voted for this poll option
voters = None
#: Votes count
votes_count = None
def __init__(self, text, vote=False):
"""Represents a poll option"""
self.text = text
self.vote = vote
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<PollOption ({}): {} voters={}>'.format(self.uid, repr(self.text), self.voters)
class Plan(object):
#: ID of the plan
uid = None
#: Plan time (unix time stamp), only precise down to the minute
time = None
#: Plan title
title = None
#: Plan location name
location = None
#: Plan location ID
location_id = None
#: ID of the plan creator
author_id = None
#: List of the people IDs who will take part in the plan
going = None
#: List of the people IDs who won't take part in the plan
declined = None
#: List of the people IDs who are invited to the plan
invited = None
def __init__(self, time, title, location=None, location_id=None):
"""Represents a plan"""
self.time = int(time)
self.title = title
self.location = location or ''
self.location_id = location_id or ''
self.author_id = None
self.going = []
self.declined = []
self.invited = []
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<Plan ({}): {} time={}, location={}, location_id={}>'.format(self.uid, repr(self.title), self.time, repr(self.location), repr(self.location_id))
class Enum(aenum.Enum):
"""Used internally by fbchat to support enumerations"""
def __repr__(self):
# For documentation:
return '{}.{}'.format(type(self).__name__, self.name)
class ThreadType(Enum):
"""Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info"""
USER = 1
GROUP = 2
ROOM = 2
PAGE = 3
class ThreadLocation(Enum):
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
INBOX = 'INBOX'
PENDING = 'PENDING'
ARCHIVED = 'ARCHIVED'
OTHER = 'OTHER'
class TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing"""
STOPPED = 0
TYPING = 1
class EmojiSize(Enum):
"""Used to specify the size of a sent emoji"""
LARGE = '369239383222810'
MEDIUM = '369239343222814'
SMALL = '369239263222822'
class ThreadColor(Enum):
"""Used to specify a thread colors"""
MESSENGER_BLUE = '#0084ff'
VIKING = '#44bec7'
GOLDEN_POPPY = '#ffc300'
RADICAL_RED = '#fa3c4c'
SHOCKING = '#d696bb'
PICTON_BLUE = '#6699cc'
FREE_SPEECH_GREEN = '#13cf13'
PUMPKIN = '#ff7e29'
LIGHT_CORAL = '#e68585'
MEDIUM_SLATE_BLUE = '#7646ff'
DEEP_SKY_BLUE = '#20cef5'
FERN = '#67b868'
CAMEO = '#d4a88c'
BRILLIANT_ROSE = '#ff5ca1'
BILOBA_FLOWER = '#a695c7'
class MessageReaction(Enum):
"""Used to specify a message reaction"""
LOVE = '😍'
SMILE = '😆'
WOW = '😮'
SAD = '😢'
ANGRY = '😠'
YES = '👍'
NO = '👎'

View File

@@ -1,8 +0,0 @@
LIKES={
'l': '369239383222810',
'm': '369239343222814',
's': '369239263222822'
}
LIKES['large'] = LIKES['l']
LIKES['medium'] =LIKES['m']
LIKES['small'] = LIKES['s']

View File

@@ -1,7 +1,41 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import re
import json
from time import time
from random import random
from contextlib import contextmanager
from mimetypes import guess_type
from os.path import basename
import warnings
import logging
import requests
import aenum
from .models import *
try:
from urllib.parse import urlencode, parse_qs, urlparse
basestring = (str, bytes)
except ImportError:
from urllib import urlencode
from urlparse import parse_qs, urlparse
basestring = basestring
# Python 2's `input` executes the input, whereas `raw_input` just returns the input
try:
input = raw_input
except NameError:
pass
# Log settings
log = logging.getLogger("client")
log.setLevel(logging.DEBUG)
# Creates the console handler
handler = logging.StreamHandler()
log.addHandler(handler)
#: Default list of user agents
USER_AGENTS = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/601.1.10 (KHTML, like Gecko) Version/8.0.5 Safari/601.1.10",
@@ -10,7 +44,19 @@ USER_AGENTS = [
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"
]
LIKES = {
'large': EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM,
'small': EmojiSize.SMALL,
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
GENDERS = {
# For standard requests
0: 'unknown',
1: 'female_singular',
2: 'male_singular',
@@ -23,42 +69,249 @@ GENDERS = {
9: 'male_plural',
10: 'neuter_plural',
11: 'unknown_plural',
# For graphql requests
'UNKNOWN': 'unknown',
'FEMALE': 'female_singular',
'MALE': 'male_singular',
#'': 'female_singular_guess',
#'': 'male_singular_guess',
#'': 'mixed',
'NEUTER': 'neuter_singular',
#'': 'unknown_singular',
#'': 'female_plural',
#'': 'male_plural',
#'': 'neuter_plural',
#'': 'unknown_plural',
}
class ReqUrl(object):
"""A class containing all urls used by `fbchat`"""
SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
SEND = "https://www.facebook.com/messaging/send/"
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"
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"
READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php"
DELIVERED = "https://www.facebook.com/ajax/mercury/delivery_receipts.php"
MARK_SEEN = "https://www.facebook.com/ajax/mercury/mark_seen.php"
BASE = "https://www.facebook.com"
MOBILE = "https://m.facebook.com/"
STICKY = "https://0-edge-chat.facebook.com/pull"
PING = "https://0-edge-chat.facebook.com/active_ping"
UPLOAD = "https://upload.facebook.com/ajax/mercury/upload.php"
INFO = "https://www.facebook.com/chat/user_info/"
CONNECT = "https://www.facebook.com/ajax/add_friend/action.php?dpr=1"
REMOVE_USER = "https://www.facebook.com/chat/remove_participants/"
LOGOUT = "https://www.facebook.com/logout.php"
ALL_USERS = "https://www.facebook.com/chat/user_info_all"
SAVE_DEVICE = "https://m.facebook.com/login/save-device/cancel/"
CHECKPOINT = "https://m.facebook.com/login/checkpoint/"
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_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"
TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
ATTACHMENT_PHOTO = "https://www.facebook.com/mercury/attachments/photo/"
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"
UNSEND = "https://www.facebook.com/messaging/unsend_message/?dpr=1"
pull_channel = 0
def change_pull_channel(self, channel=None):
if channel is None:
self.pull_channel = (self.pull_channel + 1) % 5 # Pull channel will be 0-4
else:
self.pull_channel = channel
self.STICKY = "https://{}-edge-chat.facebook.com/pull".format(self.pull_channel)
self.PING = "https://{}-edge-chat.facebook.com/active_ping".format(self.pull_channel)
facebookEncoding = 'UTF-8'
def now():
return int(time()*1000)
def strip_to_json(text):
try:
return text[text.index('{'):]
except ValueError:
raise FBchatException('No JSON object found: {!r}'.format(text))
def get_json(text):
return json.loads(strip_to_json(text))
def get_decoded_r(r):
return get_decoded(r._content)
def digit_to_char(digit):
def get_decoded(content):
return content.decode(facebookEncoding)
def parse_json(content):
return json.loads(content)
def get_json(r):
return json.loads(strip_to_json(get_decoded_r(r)))
def digitToChar(digit):
if digit < 10:
return str(digit)
return chr(ord('a') + digit - 10)
def str_base(number,base):
def str_base(number, base):
if number < 0:
return '-' + str_base(-number, base)
(d, m) = divmod(number, base)
if d > 0:
return str_base(d, base) + digit_to_char(m)
return digit_to_char(m)
return str_base(d, base) + digitToChar(m)
return digitToChar(m)
def generateMessageID(client_id=None):
k = now()
l = int(random() * 4294967295)
return ("<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id));
return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id)
def getSignatureID():
return hex(int(random() * 2147483648))
def generateOfflineThreadingID() :
def generateOfflineThreadingID():
ret = now()
value = int(random() * 4294967295);
string = ("0000000000000000000000" + bin(value))[-22:]
msgs = bin(ret) + string
return str(int(msgs,2))
value = int(random() * 4294967295)
string = ("0000000000000000000000" + format(value, 'b'))[-22:]
msgs = format(ret, 'b') + string
return str(int(msgs, 2))
def check_json(j):
if j.get('error') is None:
return
if 'errorDescription' in j:
# 'errorDescription' is in the users own language!
raise FBchatFacebookError('Error #{} when sending request: {}'.format(j['error'], j['errorDescription']), fb_error_code=j['error'], fb_error_message=j['errorDescription'])
elif 'debug_info' in j['error'] and 'code' in j['error']:
raise FBchatFacebookError('Error #{} when sending request: {}'.format(j['error']['code'], repr(j['error']['debug_info'])), fb_error_code=j['error']['code'], fb_error_message=j['error']['debug_info'])
else:
raise FBchatFacebookError('Error {} when sending request'.format(j['error']), fb_error_code=j['error'])
def check_request(r, as_json=True):
if not r.ok:
raise FBchatFacebookError('Error when sending request: Got {} response'.format(r.status_code), request_status_code=r.status_code)
content = get_decoded_r(r)
if content is None or len(content) == 0:
raise FBchatFacebookError('Error when sending request: Got empty response')
if as_json:
content = strip_to_json(content)
try:
j = json.loads(content)
except ValueError:
raise FBchatFacebookError('Error while parsing JSON: {!r}'.format(content))
check_json(j)
log.debug(j)
return j
else:
return content
def get_jsmods_require(j, index):
if j.get('jsmods') and j['jsmods'].get('require'):
try:
return j['jsmods']['require'][0][index][0]
except (KeyError, IndexError) as e:
log.warning('Error when getting jsmods_require: {}. Facebook might have changed protocol'.format(j))
return None
def get_emojisize_from_tags(tags):
if tags is None:
return None
tmp = [tag for tag in tags if tag.startswith('hot_emoji_size:')]
if len(tmp) > 0:
try:
return LIKES[tmp[0].split(':')[1]]
except (KeyError, IndexError):
log.exception('Could not determine emoji size from {} - {}'.format(tags, tmp))
return None
def require_list(list_):
if isinstance(list_, list):
return set(list_)
else:
return set([list_])
def mimetype_to_key(mimetype):
if not mimetype:
return "file_id"
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()
def enum_extend_if_invalid(enumeration, value):
try:
return enumeration(value)
except ValueError:
log.warning("Failed parsing {.__name__}({!r}). Extending enum.".format(enumeration, value))
aenum.extend_enum(enumeration, "UNKNOWN_{}".format(value).upper(), value)
return enumeration(value)
def get_url_parameters(url, *args):
params = parse_qs(urlparse(url).query)
return [params[arg][0] for arg in args if params.get(arg)]
def get_url_parameter(url, param):
return get_url_parameters(url, param)[0]

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"

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
requests
beautifulsoup4
aenum

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

48
setup.cfg Normal file
View File

@@ -0,0 +1,48 @@
[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 =
aenum
requests
beautifulsoup4

78
setup.py Normal file → Executable file
View File

@@ -1,78 +1,8 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
"""
Setup script for fbchat
"""
from setuptools import setup
import os
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
with open('README.rst') as f:
readme_content = f.read().strip()
version = None
author = None
email = None
source = 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 None not in (version, author, email, source):
break
setup(
name='fbchat',
author=author,
author_email=email,
license='BSD License',
keywords=["facebook chat fbchat"],
description="Facebook Chat (Messenger) for Python",
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=[
'requests',
'lxml',
'beautifulsoup4'
],
url=source,
version=version,
zip_safe=True,
)
setup()

168
tests.py
View File

@@ -1,168 +0,0 @@
#!/usr/bin/env python
import logging
import fbchat
import getpass
import unittest
import sys
from os import path
# Disable logging
logging.basicConfig(level=100)
fbchat.log.setLevel(100)
"""
Tests for fbchat
~~~~~~~~~~~~~~~~
To use these tests, put:
- email
- password
- a group_uid
- a user_uid (the user will be kicked from the group and then added again)
(seperated these by a newline) in a file called `tests.data`, or type them manually in the terminal prompts
Please remember to test both python v. 2.7 and python v. 3.6!
If you've made any changes to the 2FA functionality, test it with a 2FA enabled account
If you only want to execute specific tests, pass the function names in the commandline
"""
class TestFbchat(unittest.TestCase):
def test_login_functions(self):
self.assertTrue(client.is_logged_in())
client.logout()
self.assertFalse(client.is_logged_in())
with self.assertRaises(Exception):
client.login("not@email.com", "not_password", max_retries=1)
client.login(email, password)
self.assertTrue(client.is_logged_in())
def test_sessions(self):
global client
session_cookies = client.getSession()
client = fbchat.Client(email, password, session_cookies=session_cookies)
self.assertTrue(client.is_logged_in())
def test_setDefaultRecipient(self):
client.setDefaultRecipient(client.uid, is_user=True)
self.assertTrue(client.send(message="test_default_recipient"))
def test_getAllUsers(self):
users = client.getAllUsers()
self.assertGreater(len(users), 0)
def test_getUsers(self):
users = client.getUsers("Mark Zuckerberg")
self.assertGreater(len(users), 0)
u = users[0]
# Test if values are set correctly
self.assertIsInstance(u.uid, int)
self.assertEquals(u.type, 'user')
self.assertEquals(u.photo[:4], 'http')
self.assertEquals(u.url[:4], 'http')
self.assertEquals(u.name, 'Mark Zuckerberg')
self.assertGreater(u.score, 0)
def test_send_likes(self):
self.assertTrue(client.send(client.uid, like='s'))
self.assertTrue(client.send(client.uid, like='m'))
self.assertTrue(client.send(client.uid, like='l'))
self.assertTrue(client.send(group_uid, like='s', is_user=False))
self.assertTrue(client.send(group_uid, like='m', is_user=False))
self.assertTrue(client.send(group_uid, like='l', is_user=False))
def test_send(self):
self.assertTrue(client.send(client.uid, message='test_send_user'))
self.assertTrue(client.send(group_uid, message='test_send_group', is_user=False))
def test_send_images(self):
image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png'
image_local_url = path.join(path.dirname(__file__), 'test_image.png')
self.assertTrue(client.sendRemoteImage(client.uid, message='test_send_user_images_remote', image=image_url))
self.assertTrue(client.sendLocalImage(client.uid, message='test_send_user_images_local', image=image_local_url))
self.assertTrue(client.sendRemoteImage(group_uid, message='test_send_group_images_remote', is_user=False, image=image_url))
self.assertTrue(client.sendLocalImage(group_uid, message='test_send_group_images_local', is_user=False, image=image_local_url))
def test_getThreadInfo(self):
info = client.getThreadInfo(client.uid, last_n=1)
self.assertEquals(info[0].author, 'fbid:' + str(client.uid))
client.send(group_uid, message='test_getThreadInfo', is_user=False)
info = client.getThreadInfo(group_uid, last_n=1, is_user=False)
self.assertEquals(info[0].author, 'fbid:' + str(client.uid))
self.assertEquals(info[0].body, 'test_getThreadInfo')
def test_markAs(self):
# To be implemented (requires some form of manual watching)
pass
def test_listen(self):
client.do_one_listen()
def test_getUserInfo(self):
info = client.getUserInfo(4)
self.assertEquals(info['name'], 'Mark Zuckerberg')
def test_remove_add_from_chat(self):
self.assertTrue(client.remove_user_from_chat(group_uid, user_uid))
self.assertTrue(client.add_users_to_chat(group_uid, user_uid))
def test_changeThreadTitle(self):
self.assertTrue(client.changeThreadTitle(group_uid, 'test_changeThreadTitle'))
def start_test(param_client, param_group_uid, param_user_uid, tests=[]):
global client
global group_uid
global user_uid
client = param_client
group_uid = param_group_uid
user_uid = param_user_uid
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)
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.data'), 'r') as f:
content = f.readlines()
content = [x.strip() for x in content if len(x.strip()) != 0]
email = content[0]
password = content[1]
group_uid = content[2]
user_uid = content[3]
except (IOError, IndexError) as e:
email = input('Email: ')
password = getpass.getpass()
group_uid = input('Please enter a group uid (To test group functionality): ')
user_uid = input('Please enter a user uid (To test kicking/adding functionality): ')
print ('Logging in')
client = fbchat.Client(email, password)
# Warning! Taking user input directly like this could be dangerous! Use only for testing purposes!
start_test(client, group_uid, user_uid, sys.argv[1:])

125
tests/conftest.py Normal file
View File

@@ -0,0 +1,125 @@
# -*- 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.param("none", marks=[pytest.mark.xfail()])
])
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)

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)

111
tests/test_plans.py Normal file
View File

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

81
tests/test_polls.py Normal file
View File

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

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.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
pytest.param("not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
])
def test_change_emoji(client, catch_event, compare, emoji):
with catch_event("onEmojiChange") as x:
client.changeThreadEmoji(emoji)
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.param(x, marks=[pytest.mark.expensive()])
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.param(None, EmojiSize.SMALL, marks=[pytest.mark.xfail()]),
pytest.param(None, EmojiSize.MEDIUM, marks=[pytest.mark.xfail()]),
pytest.param(None, EmojiSize.LARGE, marks=[pytest.mark.xfail()]),
]
STICKER_LIST = [
Sticker("767334476626295"),
pytest.param(Sticker("0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
pytest.param(Sticker(None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
]
TEXT_LIST = [
"test_send",
"😆",
"\\\n\t%?&'\"",
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
"a" * 20000, # Maximum amount of characters you can send
pytest.param("a" * 20001, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
pytest.param(None, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
]
class ClientThread(threading.Thread):
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())